12 KiB
Anomalous Model Browser - Architecture Summary (架构指南)
This document provides a high-level overview of the Anomalous Model Browser plugin for ComfyUI. It is designed to quickly onboard new AI agents or developers to the project's structure, design philosophy, and critical subsystems.
1. Project Philosophy (设计理念)
- Zero Frameworks: No React, Vue, or build tools. Everything is Vanilla JS and CSS for maximum compatibility, minimum overhead, and zero compilation steps.
- Non-Intrusive Integration: Operates as a floating Gemini-style popover (
#anomalous-container) mounted via ComfyUI's standard UI extension system. It avoids altering the native ComfyUI canvas except when explicit interaction is required. - Strict DOM Obliteration: Rather than caching complex DOM structures (like
display: nonefor large grids), the UI strictly employsinnerHTML = ''when navigating between folders. This enforces immediate Garbage Collection, crucial for performance when users have thousands of models. - Offline-First Resilience: Model metadata is parsed from local
.infoand.civitai.infofiles or extracted directly from.safetensorsheaders via Pythonstruct. API calls to Civitai are explicit, user-initiated actions.
2. Directory Structure (核心文件结构)
Anomalous_Model_Browser/
├── __init__.py # ComfyUI Extension Entry Point. Registers nodes and sets WEB_DIRECTORY = "./web"
├── api/ # Backend Python API (Modularized)
│ ├── __init__.py # Appends all modular routes to server
│ ├── config.py # Configuration and paths setup
│ ├── metadata.py # Model metadata extraction and parsing
│ ├── models.py # Core model listing and routing (API endpoints)
│ ├── notebooks.py # Notebooks management logic
│ ├── scanner.py # Scanning engine for hash resolution and caching
│ └── utils.py # Shared backend utilities
├── scraper.py # Async HTTP client logic & web scraping (Root Level)
├── CHANGELOG.md # Version history
├── error_and_experience_summary.md # VERY IMPORTANT: Read this first to avoid past mistakes!
├── web/
│ ├── main.js # Frontend Vanilla JS Entry (Under 1000 lines, mounts modules to prototype)
│ ├── hash_resolver.js # Frontend auto-fix engine: scans workflows for missing models
│ ├── styles.css # Vanilla CSS with scoped classes (.anomalous-*)
│ └── modules/ # Extracted UI logic modules
│ ├── ui_sidebar.js # Sidebar and folder tree logic
│ ├── ui_detail.js # Model detail panel
│ ├── ui_doctor.js # Model Doctor and Assistant panel
│ ├── ui_notebooks.js # Notebooks editor
│ ├── ui_gallery.js # Fullscreen Gallery Viewer
│ ├── ui_grid.js # Main grid and model loading
│ └── locales.js # Dedicated multi-language dictionary (i18n)
├── docs/ # Supplemental documentation
3. Backend Architecture (Python API)
Modularized into the api/ package. Registered with ComfyUI's internal aiohttp server.
All endpoints are prefixed with /anomalous/.
Python Rules of Engagement:
- Never block the event loop: File I/O should ideally be offloaded or fast. Large files are handled via
struct(reading only the first 64KB for metadata), avoiding full reads. - Handle missing dependencies gracefully: Avoid 3rd-party pip requirements if possible.
- Always Restart ComfyUI: Any changes to the
api/package REQUIRE a full ComfyUI server restart to take effect. - Never use hardcoded backslash strings: When writing path manipulation code, use
os.seporos.path.normpath()instead ofreplace('\\', '/'). Editing tools may corrupt escaped backslashes silently. - Dynamic Folder Resolution (Config-Driven): Never hardcode folder types (like
checkpoints,loras). Always useapi.utils.get_active_folder_types()to enforce whitelist logic. Folders disabled by the user inconfig.jsonare completely skipped duringos.walk, ensuring zero I/O overhead for hidden directories.
4. Frontend Architecture (Vanilla JS)
Located in web/main.js (Under 1000 lines, fully refactored into ES Modules). Wraps its logic inside app.registerExtension({ name: "Anomalous.ModelBrowser", ... }).
The Modular Extraction Strategy (模块化拆分架构)
We have successfully transitioned from a monolithic main.js to a modular ES architecture.
Instead of fragmenting the class scope and losing context, we extracted all UI panels into web/modules/ and bound them back to the AnomalousBrowser.prototype.
- Modules: All major UI components (
ui_sidebar.js,ui_detail.js,ui_doctor.js,ui_notebooks.js,ui_gallery.js,ui_grid.js) are decoupled ES modules. - Shared State: Everything continues to live on the
AnomalousBrowserclass instance (this.xxx). No complex context passing required. - TOC (Table of Contents): The top of
main.jscontains a TOC mapping out the extracted module bindings.
UI Components (Dynamically Created):
- Sidebar (
anomalous-sidebar): Renders the nested folder structure. - Folder Manager (📁 Manage Folders): An interactive modal in
ui_sidebar.jsallowing drag-and-drop reordering and visibility toggling of folders. Drives backend I/O optimization. - Grid (
anomalous-grid): The main model display area. Emptied (innerHTML = '') and repopulated on every folder click. - Detail Panel (
anomalous-detail-panel): Slides out when a specific model is clicked. - Gallery Viewer (
ui_gallery.js): A fullscreen modal for viewing images. - Notebook (
ui_notebooks.js): A specialized editor for composing prompts and drag-dropping Lora/Model nodes. - Doctor Panel (
ui_doctor.js): Diagnoses model health for individual nodes or the entire workflow. Includes "View Profile" functionality.
JavaScript Rules of Engagement:
- Strict Localization (双语):
- Never use hardcoded UI strings.
- Always use the ternary operator bounded to the global state:
window.anomalous_browser_lang === 'zh' ? '中文' : 'English'. - Never use variables like
currentLang === 'zh'for DOM rendering, because they fail when closure scopes diverge from the global state.
- Avoid Global Scope Pollution: Scope all IDs and classes with
anomalous-. - Z-Index Tiers & Overlap Prevention (层叠规范):
- Base UI:
10000 - Overlay Modals:
10001to999999 - Critical Rule: Never arbitrarily assign z-indexes. A child modal MUST have a strictly higher z-index to prevent the parent from consuming clicks.
- Base UI:
- DOM ID Conflicts with CSS: When assigning an
idto an element dynamically, alwaysgrep_searchstyles.cssfirst.
5. Hash Resolver Subsystem & Scanning Engine (hash_resolver.js & scraper.py)
A highly complex subsystem responsible for automatically resolving missing/broken model references in workflows, and aggressively scanning models to build local caches.
The Global Hash Cache (window.anomalous_hash_cache)
- This is the absolute core of the resolution engine. It maps physical filenames to their exact SHA256 hashes.
- It is ALWAYS fetched on startup via
fetch('/anomalous/all_hashes'). Without this dictionary, hash injection and resolution are mathematically impossible. - Whenever a scan finishes (Scan Wizard or Deep Hash Scan),
window.anomalous_reload_hashes()MUST be called to synchronize the frontend dictionary with the newly generated.infofiles on the disk.
Model Provenance Binding (模型溯源绑定 - anomalous_inject_hash)
- Controls whether hashes are invisibly injected into the
extra_pnginfoand workflow JSON when a user saves a workflow or generates an image. - Logic Flow:
LGraph.prototype.serializeis hooked. If enabled, it intercepts the serialization, looks up every model widget's filename inwindow.anomalous_hash_cache, and explicitly writesextraObj.anomalous_hashes[node_id_filename] = {hash, size}.
The Triple-Fallback Scanning Engine (api/scanner.py & scraper.py)
When "Deep Hash Scan" is triggered, it runs in a background thread to prevent UI lockup. It uses a triple fallback to identify models:
- Fallback 1 (Header Hash Match): Extracts
modelspec.hash.sha256or Blake3/AutoV2 directly from the.safetensorsheader (O(1) speed). Hits Civitai API to fetch official metadata. - Fallback 2 (Full File SHA256): If the header has no hash, it brutally calculates the full file SHA256 (Slow) and hits Civitai.
- Fallback 3 (Offline Inference): If Civitai returns 404, it reads the Tensor Fingerprints (keys like
cond_stage_model) from the header to guess the Base Model (SDXL, SD 1.5, Flux, SD3) and builds a local.infofile.
- CRITICAL INJECTION: After a fallback succeeds, the engine FORCE INJECTS the matched hash into the resulting JSON dictionary at
["files"][0]["hashes"]["SHA256"]. This guarantees thatget_metadatawill always find a matching hash for resolution.
Resolution Execution (window.anomalous_resolve_all_missing_nodes)
Scans all nodes in app.graph._nodes that are colored red.
- Extracts the saved hash from the graph's
anomalous_hashes. - Sends it to
/anomalous/resolve_hash, which does a fast O(1) file size match first, then falls back to reading.infohashes. - If a match is found, it silently mutates the dropdown
value, removes the red color, and flags it asanomalous_auto_resolved.
6. Known Gotchas & Critical Context (新接手必读)
- Metadata Override Danger: If you write to a
.civitai.infofile, ALWAYS read it first and update the dictionary. Never just dump{custom_name: 'foo'}. - Search Context Trap: If the user searches using the sidebar while a detail panel is open, you MUST explicitly hide the detail panel and show the grid.
- Media Leakage: Destroying a DOM element that contains a playing
<video>or<audio>does not stop the audio. You must.pause()it before doinginnerHTML = ''. - Video Cover Compatibility & Interaction: Any frontend component displaying model preview covers MUST check if the file is
.mp4/.webm. For single main covers, use<video autoplay loop muted playsinline>. For grid/gallery views, you MUST implement "Hover-to-Play" to save performance: setautoplay=false,preload='metadata', and bindmouseovertoplay()andmouseouttopause(). CRITICAL GOTCHA: Do NOT usenew URL(url).pathnameto check extensions! Our backend serves images via query parameters (e.g.?filename=cover.mp4), sopathnamestrips the filename. Always test the full URL using regex like/\.(mp4|webm)(?:$|\?|&|#)/i. - Variable Naming Collisions: When moving UI elements, search for ALL references to the old
constdeclaration. A single duplicateconstin the same scope kills the entire module at parse time. - Data Scaling & Payload Limits: When passing arrays/lists (e.g. hundreds of selected files), NEVER append them to URL query parameters (
GETrequests), or you will trigger414 URI Too Long. UsePOSTwith aJSON Body. Similarly, NEVER pass massive arrays directly tosubprocess.runvia CLI arguments, as it will crash silently due to OS character limits (8191 on Windows). Always dump massive data to an intermediate.jsonfile for the subprocess to read.
For a complete list of past mistakes and detailed post-mortems, refer to
error_and_experience_summary.mdand.agents/logs/ai_lessons.mdin the root directory.