mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -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._settings = settings_service
|
||||||
self._server_i18n = server_i18n
|
self._server_i18n = server_i18n
|
||||||
self._logger = logger
|
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:
|
async def handle(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
@@ -96,6 +127,7 @@ class ModelPageView:
|
|||||||
"request": request,
|
"request": request,
|
||||||
"folders": [],
|
"folders": [],
|
||||||
"t": self._server_i18n.get_translation,
|
"t": self._server_i18n.get_translation,
|
||||||
|
"version": self._app_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
if not is_initializing:
|
if not is_initializing:
|
||||||
|
|||||||
@@ -84,10 +84,7 @@ export class AppCore {
|
|||||||
|
|
||||||
// Start onboarding if needed (after everything is initialized)
|
// Start onboarding if needed (after everything is initialized)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Do not show onboarding if version-mismatch banner is visible
|
onboardingManager.start();
|
||||||
if (!bannerService.isBannerVisible('version-mismatch')) {
|
|
||||||
onboardingManager.start();
|
|
||||||
}
|
|
||||||
}, 1000); // Small delay to ensure all elements are rendered
|
}, 1000); // Small delay to ensure all elements are rendered
|
||||||
|
|
||||||
// Return the core instance for chaining
|
// 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_KEY = 'banner_history';
|
||||||
const BANNER_HISTORY_VIEWED_AT_KEY = 'banner_history_viewed_at';
|
const BANNER_HISTORY_VIEWED_AT_KEY = 'banner_history_viewed_at';
|
||||||
const BANNER_HISTORY_LIMIT = 20;
|
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
|
* Banner Service for managing notification banners
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import {
|
|||||||
setStorageItem,
|
setStorageItem,
|
||||||
getStoredVersionInfo,
|
getStoredVersionInfo,
|
||||||
setStoredVersionInfo,
|
setStoredVersionInfo,
|
||||||
isVersionMatch,
|
isVersionMatch
|
||||||
resetDismissedBanner
|
|
||||||
} from '../utils/storageHelpers.js';
|
} from '../utils/storageHelpers.js';
|
||||||
import { bannerService } from './BannerService.js';
|
import { bannerService } from './BannerService.js';
|
||||||
import { translate } from '../utils/i18nHelpers.js';
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
@@ -753,94 +752,14 @@ export class UpdateService {
|
|||||||
stored: getStoredVersionInfo()
|
stored: getStoredVersionInfo()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset dismissed status for version mismatch banner
|
// Silently update stored version info as cache busting handles the resource updates
|
||||||
resetDismissedBanner('version-mismatch');
|
setStoredVersionInfo(this.currentVersionInfo);
|
||||||
|
|
||||||
// Register and show the version mismatch banner
|
|
||||||
this.registerVersionMismatchBanner();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to check version info:', 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
|
// Create and export singleton instance
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<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/style.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/onboarding.css">
|
<link rel="stylesheet" href="/loras_static/css/onboarding.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/vendor/flag-icons/flag-icons.min.css">
|
<link rel="stylesheet" href="/loras_static/vendor/flag-icons/flag-icons.min.css">
|
||||||
{% block page_css %}{% endblock %}
|
{% block page_css %}{% endblock %}
|
||||||
<link rel="stylesheet" href="/loras_static/vendor/font-awesome/css/all.min.css"
|
<link rel="stylesheet" href="/loras_static/vendor/font-awesome/css/all.min.css"
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
{% if is_initializing %}
|
{% if is_initializing %}
|
||||||
<!-- Load initialization JavaScript -->
|
<!-- 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 %}
|
{% else %}
|
||||||
{% block main_script %}{% endblock %}
|
{% block main_script %}{% endblock %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -40,5 +40,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -40,5 +40,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -24,6 +24,6 @@
|
|||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
{% if not is_initializing %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
{% block page_id %}recipes{% endblock %}
|
{% block page_id %}recipes{% endblock %}
|
||||||
|
|
||||||
{% block page_css %}
|
{% block page_css %}
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/card.css">
|
<link rel="stylesheet" href="/loras_static/css/components/card.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css">
|
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css?v={{ version }}">
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css">
|
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css?v={{ version }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block additional_components %}
|
{% block additional_components %}
|
||||||
@@ -84,5 +84,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main_script %}
|
{% 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 %}
|
{% endblock %}
|
||||||
@@ -192,6 +192,6 @@
|
|||||||
|
|
||||||
{% block main_script %}
|
{% block main_script %}
|
||||||
{% if not is_initializing %}
|
{% 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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -234,7 +234,6 @@ describe('AppCore initialization flow', () => {
|
|||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
expect(onboardingManager.start).toHaveBeenCalledTimes(1);
|
expect(onboardingManager.start).toHaveBeenCalledTimes(1);
|
||||||
expect(bannerService.isBannerVisible).toHaveBeenCalledWith('version-mismatch');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not reinitialize once initialized', async () => {
|
it('does not reinitialize once initialized', async () => {
|
||||||
@@ -262,13 +261,4 @@ describe('AppCore initialization flow', () => {
|
|||||||
expect(BulkContextMenu).not.toHaveBeenCalled();
|
expect(BulkContextMenu).not.toHaveBeenCalled();
|
||||||
expect(bulkManager.setBulkContextMenu).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