mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: Implement cache busting for static assets, remove client-side version mismatch banner, and add project overview documentation.
This commit is contained in:
84
GEMINI.md
Normal file
84
GEMINI.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# ComfyUI LoRA Manager
|
||||
|
||||
## Project Overview
|
||||
|
||||
ComfyUI LoRA Manager is a comprehensive extension for ComfyUI that streamlines the organization, downloading, and application of LoRA models. It functions as both a custom node within ComfyUI and a standalone application.
|
||||
|
||||
**Key Features:**
|
||||
* **Model Management:** Browse, organize, and download LoRA models (and Checkpoints/Embeddings) from Civitai and CivArchive.
|
||||
* **Visualization:** Preview images, videos, and trigger words.
|
||||
* **Workflow Integration:** "One-click" integration into ComfyUI workflows, preserving generation parameters.
|
||||
* **Recipe System:** Save and share LoRA combinations as "recipes".
|
||||
* **Architecture:** Hybrid Python backend (API, file management) and JavaScript/HTML frontend (Web UI).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
* `py/`: Core Python backend source code.
|
||||
* `lora_manager.py`: Main entry point for the ComfyUI node.
|
||||
* `routes/`: API route definitions (using `aiohttp` in standalone, or ComfyUI's server).
|
||||
* `services/`: Business logic (downloading, metadata, scanning).
|
||||
* `nodes/`: ComfyUI custom node implementations.
|
||||
* `static/`: Frontend static assets (CSS, JS, Images).
|
||||
* `templates/`: HTML templates (Jinja2).
|
||||
* `locales/`: Internationalization JSON files.
|
||||
* `web/comfyui/`: JavaScript extensions specifically for the ComfyUI interface.
|
||||
* `standalone.py`: Entry point for running the manager as a standalone web app.
|
||||
* `tests/`: Backend tests.
|
||||
* `requirements.txt`: Python runtime dependencies.
|
||||
* `package.json`: Frontend development dependencies and test scripts.
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
* Python 3.8+
|
||||
* Node.js (only for running frontend tests)
|
||||
|
||||
### Backend Setup
|
||||
1. Install Python dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Running in Standalone Mode
|
||||
You can run the manager independently of ComfyUI for development or management purposes.
|
||||
```bash
|
||||
python standalone.py --port 8188
|
||||
```
|
||||
|
||||
### Running in ComfyUI
|
||||
Ensure the folder is located in `ComfyUI/custom_nodes/`. ComfyUI will automatically load it upon startup.
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Tests (Pytest)
|
||||
1. Install development dependencies:
|
||||
```bash
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
2. Run tests:
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
* Coverage reports are generated in `coverage/backend/`.
|
||||
|
||||
### Frontend Tests (Vitest)
|
||||
1. Install Node dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
2. Run tests:
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
3. Run coverage:
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## Development Conventions
|
||||
|
||||
* **Python Style:** Follow PEP 8. Use snake_case for files/functions and PascalCase for classes.
|
||||
* **Frontend:** Standard ES modules. UI components often end in `_widget.js`.
|
||||
* **Configuration:** User settings are stored in `settings.json`. Developers should reference `settings.json.example`.
|
||||
* **Localization:** Update `locales/<lang>.json` and run `scripts/sync_translation_keys.py` when changing UI text.
|
||||
* **Documentation:** Architecture details are in `docs/architecture/` and `IFLOW.md`.
|
||||
@@ -61,6 +61,37 @@ class ModelPageView:
|
||||
self._settings = settings_service
|
||||
self._server_i18n = server_i18n
|
||||
self._logger = logger
|
||||
self._app_version = self._get_app_version()
|
||||
|
||||
def _get_app_version(self) -> str:
|
||||
version = "1.0.0"
|
||||
short_hash = "stable"
|
||||
try:
|
||||
import toml
|
||||
current_file = os.path.abspath(__file__)
|
||||
# Navigate up from py/routes/handlers/model_handlers.py to project root
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_file))))
|
||||
pyproject_path = os.path.join(root_dir, 'pyproject.toml')
|
||||
|
||||
if os.path.exists(pyproject_path):
|
||||
with open(pyproject_path, 'r', encoding='utf-8') as f:
|
||||
data = toml.load(f)
|
||||
version = data.get('project', {}).get('version', '1.0.0').replace('v', '')
|
||||
|
||||
# Try to get git info for granular cache busting
|
||||
git_dir = os.path.join(root_dir, '.git')
|
||||
if os.path.exists(git_dir):
|
||||
try:
|
||||
import git
|
||||
repo = git.Repo(root_dir)
|
||||
short_hash = repo.head.commit.hexsha[:7]
|
||||
except Exception:
|
||||
# Fallback if git is not available or not a repo
|
||||
pass
|
||||
except Exception as e:
|
||||
self._logger.debug(f"Failed to read version info for cache busting: {e}")
|
||||
|
||||
return f"{version}-{short_hash}"
|
||||
|
||||
async def handle(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
@@ -96,6 +127,7 @@ class ModelPageView:
|
||||
"request": request,
|
||||
"folders": [],
|
||||
"t": self._server_i18n.get_translation,
|
||||
"version": self._app_version,
|
||||
}
|
||||
|
||||
if not is_initializing:
|
||||
|
||||
@@ -84,10 +84,7 @@ export class AppCore {
|
||||
|
||||
// Start onboarding if needed (after everything is initialized)
|
||||
setTimeout(() => {
|
||||
// Do not show onboarding if version-mismatch banner is visible
|
||||
if (!bannerService.isBannerVisible('version-mismatch')) {
|
||||
onboardingManager.start();
|
||||
}
|
||||
onboardingManager.start();
|
||||
}, 1000); // Small delay to ensure all elements are rendered
|
||||
|
||||
// Return the core instance for chaining
|
||||
|
||||
@@ -17,7 +17,7 @@ const AFDIAN_URL = 'https://afdian.com/a/pixelpawsai';
|
||||
const BANNER_HISTORY_KEY = 'banner_history';
|
||||
const BANNER_HISTORY_VIEWED_AT_KEY = 'banner_history_viewed_at';
|
||||
const BANNER_HISTORY_LIMIT = 20;
|
||||
const HISTORY_EXCLUDED_IDS = new Set(['version-mismatch']);
|
||||
const HISTORY_EXCLUDED_IDS = new Set([]);
|
||||
|
||||
/**
|
||||
* Banner Service for managing notification banners
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
setStorageItem,
|
||||
getStoredVersionInfo,
|
||||
setStoredVersionInfo,
|
||||
isVersionMatch,
|
||||
resetDismissedBanner
|
||||
isVersionMatch
|
||||
} from '../utils/storageHelpers.js';
|
||||
import { bannerService } from './BannerService.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
@@ -753,94 +752,14 @@ export class UpdateService {
|
||||
stored: getStoredVersionInfo()
|
||||
});
|
||||
|
||||
// Reset dismissed status for version mismatch banner
|
||||
resetDismissedBanner('version-mismatch');
|
||||
|
||||
// Register and show the version mismatch banner
|
||||
this.registerVersionMismatchBanner();
|
||||
// Silently update stored version info as cache busting handles the resource updates
|
||||
setStoredVersionInfo(this.currentVersionInfo);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check version info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
registerVersionMismatchBanner() {
|
||||
// Get stored and current version for display
|
||||
const storedVersion = getStoredVersionInfo() || translate('common.status.unknown');
|
||||
const currentVersion = this.currentVersionInfo || translate('common.status.unknown');
|
||||
|
||||
bannerService.registerBanner('version-mismatch', {
|
||||
id: 'version-mismatch',
|
||||
title: translate('banners.versionMismatch.title', {}, 'Application Update Detected'),
|
||||
content: translate('banners.versionMismatch.content', {
|
||||
storedVersion,
|
||||
currentVersion
|
||||
}, `Your browser is running an outdated version of LoRA Manager (${storedVersion}). The server has been updated to version ${currentVersion}. Please refresh to ensure proper functionality.`),
|
||||
actions: [
|
||||
{
|
||||
text: translate('banners.versionMismatch.refreshNow', {}, 'Refresh Now'),
|
||||
icon: 'fas fa-sync',
|
||||
action: 'hardRefresh',
|
||||
type: 'primary'
|
||||
}
|
||||
],
|
||||
dismissible: false,
|
||||
priority: 10,
|
||||
countdown: 15,
|
||||
onRegister: (bannerElement) => {
|
||||
// Add countdown element
|
||||
const countdownEl = document.createElement('div');
|
||||
countdownEl.className = 'banner-countdown';
|
||||
countdownEl.innerHTML = `<span>${translate('banners.versionMismatch.refreshingIn', {}, 'Refreshing in')} <strong>15</strong> ${translate('banners.versionMismatch.seconds', {}, 'seconds')}...</span>`;
|
||||
bannerElement.querySelector('.banner-content').appendChild(countdownEl);
|
||||
|
||||
// Start countdown
|
||||
let seconds = 15;
|
||||
const countdownInterval = setInterval(() => {
|
||||
seconds--;
|
||||
const strongEl = countdownEl.querySelector('strong');
|
||||
if (strongEl) strongEl.textContent = seconds;
|
||||
|
||||
if (seconds <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
this.performHardRefresh();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Store interval ID for cleanup
|
||||
bannerElement.dataset.countdownInterval = countdownInterval;
|
||||
|
||||
// Add action button event handler
|
||||
const actionBtn = bannerElement.querySelector('.banner-action[data-action="hardRefresh"]');
|
||||
if (actionBtn) {
|
||||
actionBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
clearInterval(countdownInterval);
|
||||
this.performHardRefresh();
|
||||
});
|
||||
}
|
||||
},
|
||||
onRemove: (bannerElement) => {
|
||||
// Clear any existing interval
|
||||
const intervalId = bannerElement.dataset.countdownInterval;
|
||||
if (intervalId) {
|
||||
clearInterval(parseInt(intervalId));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
performHardRefresh() {
|
||||
// Update stored version info before refreshing
|
||||
setStoredVersionInfo(this.currentVersionInfo);
|
||||
|
||||
// Force a hard refresh by adding cache-busting parameter
|
||||
const cacheBuster = new Date().getTime();
|
||||
window.location.href = window.location.pathname +
|
||||
(window.location.search ? window.location.search + '&' : '?') +
|
||||
`cache=${cacheBuster}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export singleton instance
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<head>
|
||||
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/loras_static/css/style.css">
|
||||
<link rel="stylesheet" href="/loras_static/css/onboarding.css">
|
||||
<link rel="stylesheet" href="/loras_static/css/style.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/loras_static/css/onboarding.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/loras_static/vendor/flag-icons/flag-icons.min.css">
|
||||
{% block page_css %}{% endblock %}
|
||||
<link rel="stylesheet" href="/loras_static/vendor/font-awesome/css/all.min.css"
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
{% if is_initializing %}
|
||||
<!-- Load initialization JavaScript -->
|
||||
<script type="module" src="/loras_static/js/components/initialization.js"></script>
|
||||
<script type="module" src="/loras_static/js/components/initialization.js?v={{ version }}"></script>
|
||||
{% else %}
|
||||
{% block main_script %}{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block main_script %}
|
||||
<script type="module" src="/loras_static/js/checkpoints.js"></script>
|
||||
<script type="module" src="/loras_static/js/checkpoints.js?v={{ version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block main_script %}
|
||||
<script type="module" src="/loras_static/js/embeddings.js"></script>
|
||||
<script type="module" src="/loras_static/js/embeddings.js?v={{ version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
{% block main_script %}
|
||||
{% if not is_initializing %}
|
||||
<script type="module" src="/loras_static/js/loras.js"></script>
|
||||
<script type="module" src="/loras_static/js/loras.js?v={{ version }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -4,9 +4,9 @@
|
||||
{% block page_id %}recipes{% endblock %}
|
||||
|
||||
{% block page_css %}
|
||||
<link rel="stylesheet" href="/loras_static/css/components/card.css">
|
||||
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css">
|
||||
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css">
|
||||
<link rel="stylesheet" href="/loras_static/css/components/card.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css?v={{ version }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_components %}
|
||||
@@ -84,5 +84,5 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block main_script %}
|
||||
<script type="module" src="/loras_static/js/recipes.js"></script>
|
||||
<script type="module" src="/loras_static/js/recipes.js?v={{ version }}"></script>
|
||||
{% endblock %}
|
||||
@@ -192,6 +192,6 @@
|
||||
|
||||
{% block main_script %}
|
||||
{% if not is_initializing %}
|
||||
<script type="module" src="/loras_static/js/statistics.js"></script>
|
||||
<script type="module" src="/loras_static/js/statistics.js?v={{ version }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -234,7 +234,6 @@ describe('AppCore initialization flow', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(onboardingManager.start).toHaveBeenCalledTimes(1);
|
||||
expect(bannerService.isBannerVisible).toHaveBeenCalledWith('version-mismatch');
|
||||
});
|
||||
|
||||
it('does not reinitialize once initialized', async () => {
|
||||
@@ -262,13 +261,4 @@ describe('AppCore initialization flow', () => {
|
||||
expect(BulkContextMenu).not.toHaveBeenCalled();
|
||||
expect(bulkManager.setBulkContextMenu).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('suppresses onboarding when version mismatch banner is visible', async () => {
|
||||
bannerService.isBannerVisible.mockReturnValueOnce(true);
|
||||
|
||||
await appCore.initialize();
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(onboardingManager.start).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user