diff --git a/.agents/diagnostics/material_library_core_logic.md b/.agents/diagnostics/material_library_core_logic.md deleted file mode 100644 index 0997158..0000000 --- a/.agents/diagnostics/material_library_core_logic.md +++ /dev/null @@ -1,99 +0,0 @@ -# 素材库核心逻辑与验收基线 - -**更新**:2026-09-09,入口迁移、提示词搭配与节点应用融合。架构契约见 `docs/architecture/material-library.md`。 - -## 当前能做什么 - -素材库是人工精选和复用层,保留配方、提示词笔记和节点助手的直接使用入口。 - -- 从主图库或配方图库的生成 PNG 保存图片与完整工作流,也可保存单个或多个节点参数。 -- 按名称、标签、节点类型搜索;按类型和标签筛选;每页 48 条。 -- 在详情修改名称和标签;相同图片、素材类型和节点范围再次保存时提示重复,可确认另存。 -- 浏览模式点击卡片进入详情;选中单节点后的应用模式点击卡片写入匹配参数,多组候选需选择。 -- 完整素材可打开原始工作流;节点助手可按精确 node.type 应用节点参数。 - -## 真实数据类型 - -| kind | 内容与能力 | -| --- | --- | -| `image_workflow_snapshot` | 图片、完整 UI workflow、模型引用;可打开工作流或复用节点参数 | -| `image_node_selection` | 图片和完整来源依据保存在本地,只展示和复用已选节点;不能打开隐藏工作流 | -| `recipe_parameter_selection` | 配方或参数方案的选中节点,通过节点助手复用 | -| `prompt_note_bundle` | 提示词、翻译、配套模型;可载入新笔记 | -| `prompt_text` | 提示词与翻译,不含模型;可复制或载入新笔记 | - -前两类保留原图,其他类型没有图片资产。`reference_image` 目前只代表可查看保存的图片,尚不能复制到 input 或配置 LoadImage。 - -配方可保存全部、单节点或多选参数;提示词笔记可保存整份组合或仅提示词。保存后可直接“查看素材”。笔记素材能载入为新笔记,不覆盖原笔记;其中文字段是翻译,不是负面提示词。 - -图片来源需要带 UI workflow 的 PNG。只有 API prompt 的图片、损坏元数据、越界路径和超大图片会被拒绝。 - -## 保存与本地数据 - -数据在 ComfyUI 用户目录的 `workflows/anomalous_materials/`:JSON 记录配套 `.assets/<素材文件名去掉后缀>/` 下的原始 PNG 与 WebP 预览,不写回插件源码目录。 - -保存先暂存 JSON 和图片,资产就位后再发布记录。最终记录写入失败会回滚新增资产;名称和标签修改采用原子写入。删除只清理该素材及其私有图片,不影响原始出图。 - -提示词笔记独立保存在 `workflows/anomalous_notebooks`,旧文件采用可恢复复制并保留原件;素材副本与原笔记各自独立。 - -## 浏览与性能边界 - -- 列表只返回摘要;摘要按文件路径、大小、mtime、ctime 缓存,最多 4,096 条,检测文件新增、修改和删除。 -- 素材详情请求 `material_full?include_workflow=0`,只返回元信息与所选范围内的完整参数块。提示词节点默认展开,其他节点默认闭合,首次展开才创建参数 DOM;再次展开复用内容。 -- “全部展开/收起”根据实际展开状态切换。图片工作台中可勾选的节点卡保持默认闭合。 -- 点击“打开完整工作流”才请求 `include_workflow=1`,返回原始 workflow,`node_blocks` 为空,避免两份 widget 值。 -- 旧调用不传该参数时,完整素材仍保留旧返回格式;节点参数素材始终不返回隐藏 workflow,显式请求打开时返回 403。 -- 后端在工作线程读取、整理和序列化详情;节点选择先过滤再复制参数。磁盘中仍保留完整来源,读取记录时仍需解析它。 -- 搜索取消旧请求;返回列表保留筛选并释放详情 DOM。主图库复用短期目录快照,刷新和删除即时失效。 -- 图片工作台另有 16 条解析缓存、可取消导航和延迟模型预览查询。 - -**仍待优化**:节点助手按类型查询尚未分页;重复确认前已暂存图片,另存会重复处理。大量提示词节点、单个长提示词以及用户主动“全部展开”仍可能产生较多 DOM;本轮没有实现虚拟列表。 - -## 职责边界 - -- 素材库保存和展示内容,不判断模型身份。 -- 素材库与节点助手共用事务性应用,跳过种子;只搬运已有 Hash 证据,不自动全图修复。 -- 模型医生负责身份判断:完整文件哈希一致可自动修复,只有大小一致需确认当前节点,证据冲突必须拒绝。 -- 文件名、路径、封面、素材名和模型大小不能成为自动身份依据。 -- 提示词和动态名称使用文本 DOM,界面文案进入 `locales.js`。 - -## 关联模块 - -- `api/materials.py`:存储、摘要、筛选分页、详情响应、资产和节点类型查询。 -- `ui_materials.js`:素材列表、筛选、编辑、详情与工作流打开。 -- `material_inspector.js`:共享参数展示与惰性渲染。 -- `ui_gallery_detail.js`:图片工作台、图片与节点保存。 -- `ui_doctor.js`:按当前节点类型复用参数。 - -## 下一步开发顺序 - -已完成:配方参数与提示词笔记进入素材库、统一查看入口、主要页面按需展开。 - -1. 参考图片应用:先确定 input 子目录、重名、精确节点适配与失败回滚。 -2. 文件夹、批量管理、导入导出、全文提示词索引和视觉去重留待后续。 - -默认命名继续沿用:`配方名 · 节点标题/全部参数/选中 N 个模块`;`笔记名`,范围由“提示词笔记/提示词片段”标识。 - -## 固定冒烟清单 - -1. 点卡片进详情;点删除或快捷打开不会同时进入详情。 -2. 连续搜索取消旧请求,返回列表保留筛选条件。 -3. 名称或标签保存失败时显示失败,编辑内容可重试。 -4. 2,001 节点样例仅提示词默认展开,普通节点无参数内容 DOM;单节点和全部展开/收起均正常。 -5. 详情响应无 workflow;打开完整素材才读取原图,并保留 seed 和模型 Hash。节点参数素材不能请求隐藏工作流。 -6. 剪贴板失败有可见提示;再次复制保留原始提示词空白。 -7. 从节点助手应用参数仍跳过种子并按目标节点 ID 重映射 Hash。 - -本轮新增本地回归:`tests/test_material_details.py` 覆盖详情协议、兼容性和选中范围;`tests/material_inspector_lazy.mjs` 覆盖大列表惰性渲染和复制反馈。测试目录继续只供本地 AI 验证。 - - -## 2026-09-09 本轮交接(给 Gemini) - -- 左下角现在是独立素材库,不带工作台三个标签,也不预加载笔记;右上角仍是配方/提示词笔记。旧导入导出在素材库“更多”中。 -- 提示词面板先做简单版:正负词分别填写,拖入时选“放前面/放后面”。复杂通用/专用分组、排序和节点片段管理先不做;旧方案可读取为合成后的文字。 -- 素材库中带节点参数的卡片拖入时替换整组可用参数,保留种子和连线;提示词面板拖入时插入文字。两条路径都可撤销,不自动运行。 -- 拖动时窗口临时让出画布,松手后恢复;以实际落点节点为目标。多组参数/多个文本框需选择,不兼容或取消时不改节点。 -- 素材、原笔记、已存方案各自独立;草稿只保留到页面刷新,需主动保存。 -- Gemini 可调整间距、卡片、分组和视觉层级。保留“当前目标”“浏览/应用模式”“正负词”“撤销”信息;不要加入常驻轮询、全量详情预读或大片模糊动画。 -- 本轮本地检查:85 项 Python 测试、6 个 JS 回归脚本及语法/中英文键检查通过。按用户要求未做浏览器验收;真实 ComfyUI 节点兼容、窄屏、焦点与遮罩体验留待集中验收。 -- 后端新增路由,集中验收前需重启 ComfyUI。节点助手查询分页、图片素材包进出、参考图应用仍待后续。 diff --git a/.agents/logs/ai_lessons.md b/.agents/logs/ai_lessons.md deleted file mode 100644 index 89a9942..0000000 --- a/.agents/logs/ai_lessons.md +++ /dev/null @@ -1,740 +0,0 @@ -# 📖 AI Lessons — Error & Experience Summary (开发避坑与经验总结) - -This document serves as an architectural retrospective and UX diagnostic log for the development of the Anomalous Model Browser. - -## 68. Missing Model URLs in Hash Cache & Asynchronous Path Resolution in Model Source Hub - -**The Problem**: When opening the Model Source Hub (模型来源统一中控中心), active workflow models showed empty source URL inputs and "已配置: 0 / N", even when valid `.civitai.info` or `.info` sidecars with official download URLs existed on disk. This occurred because `/anomalous/all_hashes` only extracted `{"hash": ..., "size": ...}` and omitted the parsed `"url"`, while the frontend modal opened synchronously without querying `/anomalous/resolve_paths_to_previews`. Furthermore, the single-item auto-detect function tried direct browser `fetch('https://civitai.com/...')` which fails under firewall/CORS conditions instead of probing local sidecars first, and `api_update_metadata` lacked fallback path resolution when called with unknown folder types or relative subpaths from workflow scope. - -**The Solution**: -1. Enrich `/anomalous/all_hashes` payload with `"url": meta.get("source_url") or meta.get("civitai_url") or ""` so models in cache immediately provide URLs. -2. In `ui_model_sources.js`, invoke `resolveWorkflowModelsMetadata` upon opening the modal to resolve active models via `/anomalous/resolve_paths_to_previews` in the background, dynamically populating URLs, platform tags, types, and hashes. -3. In `autoDetectModelSource`, prioritize local sidecar and metadata lookup before falling back to Civitai cloud hash API or search. -4. In `api_update_metadata`, normalize path separators and add fallback model resolution via `_resolve_paths_to_model_info_sync` across all model roots so saving custom URLs to local sidecars from workflow scope works reliably. - - -## 67. Incomplete Try-Catch Block & Code Duplication in Submodules Silently Halts Extension Registration - -**The Problem**: During automated text replacements in `ui_doctor.js`, corrupted line splices created an unclosed `try` block without `catch`/`finally` (`SyntaxError: Missing catch or finally after try`), alongside duplicate loader append statements. Because `main.js` statically imports `ui_sidebar.js` which in turn imports `ui_doctor.js`, the browser's JavaScript engine rejected the entire ES module graph at evaluation time. Consequently, `app.registerExtension()` and the DOM creation logic for `#anomalous-trigger-btn` never ran, leading to the total disappearance of the floating trigger icon and topbar button in ComfyUI without visible error prompts on the canvas. - -**The Solution**: Always verify the closure of code blocks (especially `try { ... } catch (error) { ... }`) when performing targeted code refactorings. Validate module ASTs and link the entire frontend dependency tree using Node's experimental VM modules (`node --experimental-vm-modules --input-type=module -e "new vm.SourceTextModule(...)"`) across all frontend files before committing. In addition, ensure `#anomalous-trigger-btn` and `.anomalous-trigger-icon` have robust fallback flex layout styles and clean 1em SVG icon sizing to guarantee immediate visual clarity upon load. - -## 66. Unescaped Multiline Regex Literal Leads to ES Module SyntaxError & Disappearing Extension Icon - -**The Problem**: During prompt composer assembly logic updates, an accidental newline inside a regex literal (`split(/[,,\n]+/` broke into `/[,,` and `]+/`) created an invalid regular expression token (`SyntaxError: Invalid regular expression: missing /`). Under ES module import rules, any unhandled `SyntaxError` immediately halts module graph compilation. Because `main.js` imports `ui_materials.js` which in turn imports `ui_prompt_composer.js`, the entire extension crashed during initialization before reaching `app.registerExtension()` or `setup()`. This caused the extension entry icon (`#anomalous-trigger-btn`) and topbar entry to completely vanish from ComfyUI without an obvious UI-level trace. - -**The Solution**: Keep all character-class regex patterns strictly on a single line (e.g. `split(/[\s,,\n\r]+/)`). To prevent `node -c` false negatives on ES modules, test the real module import chain with `node --input-type=module -e "import('./web/main.js').catch(...)"` to catch syntax and token errors early. - -## 65. Missing Module Imports Under Global `try-catch` Masks as Data Fetch Failures - -**The Problem**: During visual refactoring of UI cards, if an external dependency (such as `bindMaterialDrag` from `material_drag.js`) is omitted from the file's `import` declarations, calling it inside `createRecipeCard` throws a runtime `ReferenceError`. Because card construction runs inside the global `try { ... } catch (error) { ... }` block of `refreshRecipes()`, this runtime error was swallowed and triggered the generic network failure message (`"无法加载工作流配方。"`), creating the false impression that backend data could not be fetched (even though the topbar filter counter had already successfully computed and displayed `1/1`). - -**The Solution**: Ensure every cross-module function called in the component is explicitly imported. Separate network data fetching from DOM list rendering inside `refreshRecipes()`, and log DOM rendering errors with stack traces so unexpected component exceptions do not falsely masquerade as network outages. - -## 64. On-Demand Dynamic Hashing & Slash Normalization for Missing Model Recovery - -When models are renamed locally and their sidecar `.info` files are deleted, static metadata caches and header inspections may return empty hash sets. Treating size-matching candidates as an immediate `identity_conflict` causes false-negative rejections. Instead, if candidate file size matches the provenance record and the file lacks cached hashes, calculate SHA256 on-demand. Upon cryptographic verification, write an offline inferred `.info` sidecar to persist provenance. Furthermore, always normalize path separators (`\` vs `/`) when indexing and querying workflow metadata (`extra.anomalous_hashes`) to avoid lookup misses across operating systems. - -## 60. Beware of `node -c` False Negatives with ES Modules - -When checking syntax using `node -c` for browser-targeted ES Module JavaScript files on Windows PowerShell, the Node.js CommonJS parser can stop at the first `export` keyword it encounters, throwing an early `SyntaxError: Unexpected token 'export'`. If a severe syntax error (such as an unclosed bracket or duplicated function definition) occurs earlier in the file, it will cause the browser to silently abort parsing the module without rendering the UI, yet `node -c` might not properly expose the real location of the mismatch if it bails out at the `export` keyword or misinterprets the module boundary. Never blindly trust `node -c` returning `0` if the browser UI disappears; prioritize a manual eyeball check of the replaced blocks and unclosed symbols. - - - -When a visual refactor merges information into a compact overview, do not remove a semantic tab until every dependent interaction has an intentional replacement. Model preview loading, model navigation, and return-state restoration were still designed around a Models tab, so removing only the tab made those actions unreachable. Keep tab state, asynchronous loading conditions, and return paths aligned with the visible navigation. - -## 57. Detail Return Must Restore the List Viewport - -Opening a detail panel should capture the list's semantic location and viewport, not only the selected item. Restore the folder/type and `scrollTop`/`scrollLeft` when the user returns to the grid, while keeping recipe-detail return state separate from browser-grid state. This prevents a visually correct Back action from still feeling like a navigation reset. - -## 58. Overlay Close Must Restore Its Covered Panel - -An overlay that only toggles its own visibility can reveal a blank host surface if the underlying panel was hidden by an earlier navigation path. Record the covered panel at overlay entry and restore it through one shared close function. Keep a safe visible fallback, such as the main grid, for stale or missing return state. - -## 59. Reused Overlay DOM Must Be Rehydrated on Reopen - -When an overlay is closed by hiding both its shell and its child sections, the already-initialized fast path must restore the active child section before returning. Refreshing data alone does not make a hidden DOM subtree visible and can produce a blank overlay on the second open. - -## 60. Refresh Modes Must Be Explicit Data, Not List Attributes - -Python lists cannot carry ad-hoc control flags. When enrichment needs a refresh mode, pass it as an explicit function argument so the code remains valid for every list implementation and the preserve-versus-recompute behavior is visible at the call site. - -## 61. Long Status Explanations Do Not Belong in Bounded Badges - -Keep status badges short and move the explanation to a tooltip or disclosure. Long localized text inside a `white-space: nowrap` flex item can overflow its card even when the parent has a nominal width; use concise labels plus `min-width: 0` and explicit wrapping for resilient layouts. - -## 62. Disabled Is Not the Same as Feedback - -For network-backed buttons, disabling the control prevents duplicate requests but does not tell the user that anything happened. Pair the disabled state with an immediate loading label, a visible progress cue, and a failure path that restores the original action. - -## 63. Native Title Tooltips Are Not Reliable UI Feedback - -Browser `title` hints are delayed, low-contrast, difficult to style, and poor for keyboard users. For explanations that must not cover neighboring content, an inline click-to-expand note is safer than an absolutely positioned tooltip and avoids duplicate browser/custom hints. -这份文档旨在记录 Anomalous 插件开发过程中的架构演进、UX 设计教训以及核心 Bug 排查经验。 - ---- - -## 60. Async UI Actions Need One Ownership Boundary - -**The Problem**: Card actions each implemented their own `try/catch`, disabled state, or no state at all. A failed request could leave a control stuck, while a double click could start two recipe mutations. - -**The Solution**: Put busy-state ownership, duplicate-click prevention, error reporting, and `finally` restoration in one small action runner. Individual actions should only describe their domain operation and return/throw normally. - -## 59. Cross-Panel Navigation Needs an Explicit Return Token - -**The Problem**: A recipe detail panel can launch a model detail panel owned by the main browser. Without a return token, the model Back button correctly returned to the model grid but lost the recipe tab, scroll position, and active view. Separately, Open/Append actions performed their work but left the detail Promise unresolved. - -**The Solution**: Treat recipe detail as a small controller with an idempotent finish/dispose path. Store only a lightweight payload, active tab, and scroll position for model navigation; consume that token from the main detail Back action. Every successful action must settle the controller, while external Workspace changes must dispose it as well. - -## 58. Identity, Preview, and Filename Are Different Evidence Classes - -**The Problem**: A recipe's saved model filename is useful for showing the user a basename and for locating a current-machine preview, but it cannot prove that a same-named local file is the same model. Package enrichment also rebuilt model references and accidentally discarded imported identity and historical preview descriptors. - -**The Solution**: Keep identity records and presentation descriptors keyed by node/widget/category/saved value, then rebuild only transient local availability around them. Resolver matching must use hash, exact size, and category rules; filename/path remains a locator only. Export must collect assets from every selected recipe version, remove local output paths, and commit replacement packages with rollback across recipe, assets, and history. - -## 57. A Summary Must Never Become the Save/Edit Source - -**The Problem**: Recipe metadata deliberately truncates generic widget values for fast cards and detail browsing. Reusing it for edit controls meant a long prompt could be saved back as its truncated preview. A canvas-based update also started with an empty pin selection, silently dropping valid prior key parameters. - -**The Solution**: Capture one serialized workflow snapshot before the save dialog, use it as the only outbound graph, and resolve editable/pinnable widget values from that snapshot by stable node ID plus widget index. Carry forward only pin keys that still resolve on the current graph and make dropped pins explicit. Server acceptance must validate basic graph topology and return a compact receipt so the UI can show what was accepted. - -## 51. A Bounded Card Summary Is Not a Detail Value (卡片摘要不能冒充详情值) - -**The Problem (问题)**: The recipe card intentionally limits generic widget text for fast browsing, but the detail panel reused that same summary. Long prompts and output settings looked saved yet could not be inspected or copied in full, while the negative prompt was hidden behind a positive-only overview layout. - -**The Solution (方案)**: Keep the compact summary for cards, then resolve the matching safe value from the already-loaded serialized workflow only in the detail panel. Present positive and negative prompts explicitly. Any long value must offer visible expand/collapse and copy actions; do not replace available data with an unexplained ellipsis. - -## 50. Host Prompt APIs Often Return an Envelope (宿主 Prompt API 可能返回包装对象) - -**The Problem (问题)**: ComfyUI's `api.queuePrompt` destructures the `{ output, workflow }` envelope returned by `app.graphToPrompt(graph)`. Passing only the nested `output` object loses the workflow metadata and produces an invalid queue payload. - -**The Solution (方案)**: When bridging a plugin action to a host API, inspect both the producer and consumer contracts and preserve the complete envelope at the boundary. Keep temporary graph conversion separate from the live canvas so this compatibility fix cannot introduce canvas mutation. - -## 52. Child Overlay Navigation Must Not Close Its Host (子覆盖层跳转不能关闭宿主) - -**The Problem (问题)**: Recipe model cards lived in a child Workspace overlay, but their local-model click handler called the browser-wide `close()` method. That method hides the host modal and releases its grid state, so the target model detail panel appeared to close immediately. - -**The Solution (方案)**: Treat the click as an in-place host-panel transition: prepare the model detail state, ensure the host modal remains visible, render the detail panel, and hide only the child recipe overlay. Keep the full close/release lifecycle reserved for explicit browser dismissal. - -**Related presentation rule (相关展示规则)**: Repeated model references need independent responsive cards. A shared dense container makes preview, identity, availability, and actions appear mixed together; a card grid preserves one-reference-per-unit without changing the saved workflow data. - -## 53. Model Composition Is Not a Generic Parameter Row (模型组成不能当作普通参数行) - -**The Problem (问题)**: Putting the base model and a comma-joined LoRA list into the same responsive key-value grid made long names compete with labels and unrelated sampling values. The browser wrapped the text into narrow columns, producing a tall, visually mixed summary. - -**The Solution (方案)**: Give model composition its own vertical section. Render the base model and each LoRA as an independent full-width block with safe wrapping and copy/expand controls; reserve the compact grid for short scalar values such as steps, CFG, sampler, and resolution. - -## 54. Saved Prompt Metadata Needs a Detail-Tab Contract (已保存提示词必须有明确详情页入口) - -**The Problem (问题)**: Prompt extraction already persisted `params.promptPositive` and `params.promptNegative`, but only the Overview invoked the prompt renderer. The Parameters tab only walked bounded generic node summaries, so a valid saved positive prompt looked absent. - -**The Solution (方案)**: Treat prompt summaries as first-class recipe parameters and render them in both Overview and Parameters. Keep the existing full-workflow lookup, copy control, and expand/collapse behavior so prompt display does not depend on the generic node summary limit. - -## 55. Native Prompt Nodes Must Survive Bounded Summaries (原生提示词节点不能被摘要限制吞掉) - -**The Problem (问题)**: A separate prompt summary in Parameters duplicated the native `CLIPTextEncode` rows, while older native nodes could be omitted from `params.nodes` when their text was only available in serialized `workflow.nodes[].widgets_values`. This made one polarity, commonly the positive prompt, appear missing. - -**The Solution (方案)**: Keep prompt summaries as an Overview convenience only. Parameters must use native node names and supplement the bounded summary with serialized `CLIPTextEncode` nodes, preserving each node's `text` widget and full-value lookup. - -## 56. Fallback Rows Must Have a Presentation Order (补回的参数行必须有展示顺序) - -**The Problem (问题)**: Appending a fallback-recovered positive CLIP node to the end of the parameter list left it far away from the existing negative CLIP node. - -**The Solution (方案)**: Rebuild the parameter presentation order from serialized workflow order, then group native `CLIPTextEncode` rows together. This changes only display ordering; node names, widget values, and the authoritative workflow remain unchanged. - -## 1. UX Scroll Traps (交互嵌套滚动陷阱) -**The Problem (问题)**: In the Notebook modal, the translation editor had a `max-height` and `overflow-y`. Since the parent modal was also scrollable, it created a "Scroll Trap". Hitting the bottom of the editor unpredictably transferred the scroll event to the outer body. -(在笔记本弹窗中,由于翻译编辑器固定了最大高度并允许滚动,导致与父级弹窗的滚动条发生冲突。用户滚动到底部时会突然把外层页面卷走,体验极差。) -**The Solution (方案)**: Removed the `max-height` and internal scrollbar. Letting the translation editor expand naturally utilizing the primary window scrollbar guarantees a continuous experience. -(彻底移除内部最大高度和滚动条。让编辑器自然撑开,统一使用最外层的主滚动条,保证心流不断档。) - -## 2. Artificial Metadata Pollution (伪造数据的反噬) -**The Problem (问题)**: To make the "Base Model" dropdown look complete, the backend artificially injected hardcoded architectures (`SDXL`, `HunyuanVideo`) even if the user didn't own them. This confused users. -(为了让“模型架构”下拉菜单看起来更“丰满”,后端硬编码注入了一些用户根本没下载过的新架构。结果让用户极度困惑,以为自己眼花了。) -**The Solution (方案)**: Data truth must strictly reflect the local disk state. Dropped artificial padding; the UI dropdown now accurately mirrors only the physical files on disk. -(数据必须绝对忠于本地磁盘。删除了所有的硬编码注入,确保下拉菜单严格反映本地真实的生态。) - -## 3. UI Strobe Effects on Grid Hovers (网格悬浮的“光污染”) -**The Problem (问题)**: Standard `transition: all 0.2s` meant quickly sweeping the mouse across the model grid caused all cards to rapidly light up like a strobe light. -(为卡片设置了标准的 0.2秒 悬浮动画,导致鼠标快速扫过网格时,所有卡片像迪厅频闪一样疯狂亮起,显得非常廉价。) -**The Solution (方案)**: Introduced a `transition-delay: 0.08s` low-pass filter. The recommended UX range for hover intent is 50ms - 100ms. This 80ms delay is imperceptible for intentional clicks but completely eliminates flashes from accidental sweeps. -(引入了 `0.08s` 动画延迟作为低通滤波器。根据人机交互体验,区分“无意滑过”与“有意悬停”的黄金推荐区间是 50ms - 100ms。我们采用的 80ms 延迟对正常点击毫无察觉,但完美过滤了鼠标无意扫过造成的闪烁。) - -## 4. The ComfyUI Manager Auto-Update File Wipe (Manager自动更新导致文件丢失) -**The Problem (问题)**: A custom documentation file (`docs/核心按钮操作指南_大白话版.md`) generated by AI locally was silently deleted when clicking "Update" on the node inside ComfyUI Manager. -(本地让 AI 写的说明文件,在点击 ComfyUI Manager 的更新按钮后离奇失踪了。) -**The Solution (方案)**: ComfyUI Manager's update mechanism often performs a raw `git pull` or hard reset. If a local file is not tracked in the remote GitHub repository (i.e., you haven't committed and pushed it), it is highly likely to be overwritten or wiped during the sync process. Always commit and push your documentation, or keep personal untracked notes outside the extension directory! -(ComfyUI Manager 的更新机制依赖 Git 同步(类似 `git reset --hard` 或强制 pull)。如果你在本地创建了文件却没有提交并推送到 GitHub,更新时 Manager 会强行把本地状态与云端对齐,导致这些“未追踪(untracked)”的文件被直接抹杀。教训:重要的文档一定要及时 push,或者别放在插件目录下!) - -## 5. "Hostage" Inline Confirmations (被“绑架”的内联确认) -**The Problem (问题)**: Clicking "Delete" changed the button to "Are you sure?" with a 3-second timeout, but lacked a "Cancel" button. Users misclicking were held hostage for 3 seconds. -(点击删除后会变成“确定删除吗?”并锁定 3 秒,但没有“取消”按钮。用户如果不小心点错,只能像被绑架一样干等 3 秒它恢复。) -**The Solution (方案)**: Wrapped the logic to instantly reveal a secondary `[✕]` cancel button upon the first click, giving control back immediately. -(在点击删除时,立马在旁边弹出 `[✕]` 取消按钮。点击取消直接清空倒计时,立刻把控制权还给用户。) - -## 6. First-load "Dead Interface" Avoidance (首次加载的“白屏假死”) -**The Problem (问题)**: Initializing the sidebar to the root directory `/` presented a blank screen if users only had models nested deep in subfolders. -(插件默认打开根目录 `/`。如果用户的模型全都在很深的子文件夹里,打开插件就会看到一片空白,以为插件坏了。) -**The Solution (方案)**: Implemented `firstLoadDone` to automatically force the UI to open the very first directory possessing `model_count > 0`. -(加入了智能寻路逻辑,插件首次加载时自动扫描目录树,直接定位并打开第一个存有模型的文件夹。) - -## 7. Structural Layout Breakages (流式布局的坍塌) -**The Problem (问题)**: Placing varying lengths of translation tags sequentially caused extreme layout jitter when hovering. -(把中英文标签简单并排放在一起,由于字符串长度不一,鼠标悬浮时极容易发生换行挤压,导致整个界面疯狂跳动。) -**The Solution (方案)**: Enforced a strict "Row-based" flex architecture (`align-items: stretch`). Each tag pair is locked into a geometrical row. -(采用极其严格的行级 Flex 布局,把中英文双语硬性锁死在同一个拉伸行内,无论字数多少,几何结构绝对稳定。) - -## 8. Modal State Persistence (弹窗状态的幽灵) -**The Problem (问题)**: Closing and reopening the Notebook modal showed a blank editor because the JS object cached `this.currentNotebook` but the DOM was obliterated. -(关掉笔记本弹窗再打开,右侧编辑器全空。因为 JS 内存里还记着上一次的状态,但 DOM 节点已经被销毁重建了。) -**The Solution (方案)**: Explicitly check cached state during modal initialization and force `renderNotebookEditor()` if necessary. -(在弹窗初始化时主动校验缓存,强制触发重新渲染,实现了完美的“会话级记忆”。) - -## 9. Missing Visual Feedback for Background Operations (无反馈的幽灵进程) -**The Problem (问题)**: The Notebook "Save" button executed a background `fetch` perfectly, but provided absolutely no visual feedback. Users assumed it was broken. -(点击“保存”按钮后,后台默默完成了请求,但按钮本身没有任何变化。用户以为没点上,引发数据丢失的焦虑。) -**The Solution (方案)**: Clicking Save instantly changes the button to a green `✅` state for 1.5 seconds. -(点击保存后强制变绿 `✅` 1.5 秒,给用户极强的心理安全感。) - -## 10. Spatial Geometry for Multi-Node Spawning (多节点生成的空间重叠) -**The Problem (问题)**: Spawning an entire Notebook (Main Model + Loras + CLIPs) caused all nodes to clump perfectly at `(0,0)`. -(一键生成整个笔记本(主模型+N个Lora+提示词)到画布时,所有节点全叠在同一个坐标上,变成了一坨。) -**The Solution (方案)**: Implemented a "Relative Offset Matrix Layout" (`X + 350*N`, `Y + 250`) and bound it to the `mousemove` event (Magnetic Sticking). -(加入了偏移矩阵计算,并绑定鼠标移动。用户可以直观地拖着排成流水的节点序列在画布上移动,然后再点击放下。) - -## 11. Localization Host-Environment Constraints (寄生环境的本地化陷阱) -**The Problem (问题)**: Reading `document.documentElement.lang` failed because ComfyUI forces the host HTML to `en-US`, breaking our Chinese translations. -(插件试图读取网页根节点的 lang 属性来决定语言。但 ComfyUI 强行把底座设成了纯英文,导致中国用户永远看不到中文提示。) -**The Solution (方案)**: Never trust the host document properties. Rerouted localization strictly to our internal `currentLang` variable. -(永远不要相信宿主环境。彻底切断外部依赖,纯靠插件内部自己的 `currentLang` 变量来统治中英文环境。) - -## 12. Strict Path Separator Validation (路径分隔符的玄学崩溃) -**The Problem (问题)**: Windows uses backslashes (`\`), but assigning `anime/model` to a node's combo widget failed ComfyUI's strict string matching, causing red nodes. -(Windows 的底层模型路径带反斜杠,但前端组件要求精确匹配字符串,哪怕是一个斜杠方向不对,节点也会标红报错。) -**The Solution (方案)**: Built `setWidgetValuePath` to normalize both target paths and native options with `.replace(/\\/g, '/')` before assignment. -(做了一个拦截器,强行把系统内的选项和我们要赋的值全部统一转成正斜杠再进行比较,直接绕过操作系统的路径陷阱。) - -## 13. Backend Starvation During Inference (算力挤兑导致的接口饿死) -**The Problem (问题)**: Fetching plugin data while ComfyUI is generating an image causes timeouts because of PyTorch GIL-locking. -(在 ComfyUI 画图满载的时候,点插件会导致接口超时。因为单线程的 Python 完全被 GPU 推理抢占了。) -**The Solution (方案)**: Accept it gracefully. Database management and intense GPU inference are mutually exclusive workflows. -(优雅地接受这个现实。不需要过度工程化地去做离线队列,画图和管理模型本来就不该同时进行。) - -## 14. DOM Obliteration vs Virtual DOM Hoarding (物理毁灭 vs 虚拟回收) -**The Problem (问题)**: Maintaining performance when navigating folders containing thousands of models. -(当一个文件夹里有上千个模型时,频繁切换文件夹会导致浏览器内存爆炸。) -**The Solution (方案)**: Instead of `display: none` hoarding, strictly enforce `this.grid.innerHTML = ''` to physically obliterate nodes and trigger immediate Garbage Collection. -(放弃“隐藏重用”的传统思路。切文件夹时直接暴力清空内部的 HTML 节点,强迫浏览器立马回收内存,保持零负担。) - -## 15. The "Missing Plugin Icon" Trap (LocalStorage 坐标越界飞天) -**The Problem (问题)**: If the floating trigger button was saved at `X=3000px` on a 4K monitor, loading it on a `1920px` laptop rendered it permanently off-screen. -(在大显示器上把插件按钮拖到了最右边,换到小笔记本上时,坐标超出了屏幕,按钮彻底消失,再也点不出来了。) -**The Solution (方案)**: Implemented a bounds clamp checking coordinates against `window.innerWidth`. If out of bounds, snap it to the bottom-right corner. -(在启动时强制对比当前屏幕大小,一旦发现越界,立刻把它抓回到右下角的安全区内。) - -## 16. The `[vite:preloadError]` Template Literal Crash (一个反引号引发的血案) -**The Problem (问题)**: A missing backtick in a dynamic ES Module dictionary crashed the entire ComfyUI extension loader before execution. -(在汉化字典里不小心删掉了一个反引号,导致 Vite 打包工具报语法错误,整个插件直接在加载前暴毙,UI 按钮全都没了。) -**The Solution (方案)**: JS zero-tolerance for syntax errors requires extreme validation. Fixed the missing backtick. -(原生 JS 没有任何编译容错,任何语法拼写都必须如履薄冰,及时修复了那个反引号。) - -## 17. Target Size Deduplication Bug (重叠目录的体积匹配悖论) -**The Problem (问题)**: Fixing models by file size required finding *exactly one* matching file. But users mapping `checkpoints` and `diffusion_models` to the same folder caused `os.walk` to find the same physical file twice, aborting the fix. -(物理寻址兜底算法要求“体积必须唯一”。但用户配置了多个虚拟路径指向同一个物理文件夹,导致同一个文件被扫出两次,系统误判为“体积不唯一”而放弃修复。) -**The Solution (方案)**: Added `os.path.realpath` deduplication matrix to condense multiple virtual paths back to a single hard drive sector. -(引入了绝对物理地址 `realpath` 去重机制,无论虚拟路径怎么重叠,物理层面上都将其归一化。) - -## 18. The "Lightweight Scan" Re-Hash Catastrophe (轻量扫描的重算灾难) -**The Problem (问题)**: The lightweight scan (`--skip-media`) demanded *both* an `.info` file and a preview image to skip hashing. Since it didn't download images, it forcefully re-hashed hundreds of GBs of models every time. -(轻量化极速扫描因为不会下载图片,导致“是否跳过计算”的判断条件(既有 info 又有图)永远不成立。引发了灾难性的全盘重新计算 SHA256,慢如蜗牛。) -**The Solution (方案)**: Modified skip logic conditionally: if explicitly skipping media, the existence of `.info` is sufficient to bypass hashing. -(加入了情境判断:只要是在极速模式下,不管有没有图,只要有文字信息就无条件跳过,恢复了真正的极速。) - -## 19. The Stale Dropdown Visual Residue (顽固的红框残留) -**The Problem (问题)**: Even after a model path was auto-fixed, ComfyUI maintained a permanent red error border on the node if the frontend cache was stale. -(底层其实已经修复并加载了正确的模型,但因为前端选项缓存没刷过来,ComfyUI 死死把节点标红,看着十分闹心。) -**The Solution (方案)**: Forcefully execute `delete node.color; delete node.bgcolor;` to violently strip native error states without requiring a refresh. -(既然修好了,就直接暴力删除节点的 `color` 属性,强行洗白红框,无需刷新网页。) - -## 20. The Silent Subprocess Death (DEVNULL 黑洞) -**The Problem (问题)**: `scraper.py` crashed due to Python indentation, but piping `stderr` to `DEVNULL` made it totally invisible. The UI waited forever. -(后台脚本缩进写错了导致崩溃,但代码里把报错信息塞进了无尽深渊 `DEVNULL`,没有任何提示,前端界面只能干等着,陷入死局。) -**The Solution (方案)**: Never pipe `stderr` to DEVNULL during active development. Let it log. -(开发阶段绝对不要屏蔽底层报错,必须让 `stderr` 打印到控制台以便排查。) - -## 21. Tiered Fallback Resolution for O(1) Scans (多级降级寻址打造 O(1) 扫描) -**The Problem (问题)**: Unscanned 10GB models required full disk read to calculate SHA256, taking minutes. -(即便是第一次扫描,遇到几十 GB 的新模型,完整计算 SHA256 也要几分钟,用户体验极度卡顿。) -**The Solution (方案)**: -1. 0ms: Read existing `.info`. (直接读取已有的 info) -2. 0.01s: Extract 64KB JSON header from `.safetensors` via `struct` for pre-computed hash. (用 struct 撬开文件头瞬间提取内置的 Hash,降维打击) -3. Fallback: Full SHA256 only for legacy models. (真找不到了才迫不得已做全量计算) - -## 22. Zero-API Tensor Fingerprinting (零API脱机张量基因识别) -**The Problem (问题)**: HuggingFace or private models returned 404 from Civitai, failing to generate `.info` and completely breaking UI integration. -(从 HF 拿到的或者是自己练的私模,由于 C站 没有记录,API 直接报错 404,导致模型彻底被忽略,前端里连看都看不到。) -**The Solution (方案)**: Read the first 500 Tensor Keys via struct. Identifying `double_blocks.0.img_attn` strictly confirms Flux.1 D architecture completely offline. -(不管你有没有网,强行读取模型前 500 层的网络结构。只要看到里面有 `double_blocks`,直接盖章确认为 Flux 架构。完全脱机,无缝融入前端兼容雷达。) - -## 23. Gemini-Style Popover UI (告别全屏遮罩,采用悬浮弹窗) -**The Problem (问题)**: Center-screen settings modals with blur backgrounds felt heavy and disconnected users from the workspace. -(原本的设置面板是个带有大面积模糊遮罩的全屏居中弹窗,显得特别笨重,像个垃圾网页游戏。) -**The Solution (方案)**: Refactored to a lightweight side-popover (`left: 100%; bottom: 15px;`) imitating Gemini's web UI, maintaining deep workflow immersion. -(改成绝对定位,像 Gemini 网页端那样从侧边栏右侧悄悄弹出一个悬浮气泡,点外侧自动收起,极大保证了沉浸感。) - -## 24. Dock & Mode Lock (全宽底座与多状态锁死) -**The Problem (问题)**: Floating capsules looked cramped, and entering fullscreen Gallery while the Sidebar menu remained clickable caused DOM rendering conflicts. -(底部按钮挤在一起极其难看。而且进入全屏图库的时候,居然还能点左上角的汉堡菜单,导致页面状态直接崩溃交织在一起。) -**The Solution (方案)**: Extended to a 100% width solid Dock. Disabled sidebar triggers during fullscreen views via strict State Locks. -(把底部做成了满宽的实体底座,稳固了视觉重心。一旦进入图库这种排他性的全视图,强行给菜单上互斥锁,彻底断绝乱按引发的 Bug。) - -## 25. Resolving "Missing" vs "Unscanned" Conflicts (防重叠逻辑拦截自相踩踏) -**The Problem (问题)**: Deleting a model and reloading the workflow popped BOTH the "Missing Model" dark modal and the "Unscanned Model" orange toast. -(把一个模型删掉后重新加载图片,系统一方面觉得“模型丢了”弹出黑框,一方面因为缓存里没它又觉得它是“没扫过的新模型”弹出橙黄色的警告,两个弹窗叠在一块极其弱智。) -**The Solution (方案)**: The unscanned detector now ignores nodes already flagged with red error colors (`node.color === "#FF3333"`), preventing system overlapping. -(加入了双轨防踩踏:未扫描检测器只要看到这个节点已经红了(彻底丢了),就主动闭嘴放弃拦截,边界极其清晰。) - -## 26. The CSS `zoom` Devastation (Zoom 属性对坐标系的降维打击) -**The Problem (问题)**: Using `zoom` to scale the UI broke bounding boxes. `container.offsetHeight > window.innerHeight` caused `Y` coordinates to become negative, permanently launching the window off-screen into the void. -(早期为了偷懒,用 CSS `zoom` 属性来做 UI 缩放。结果它彻底破坏了浏览器的物理坐标系,一旦窗口过大,防越界系统就算出了个负数坐标,导致插件整个闪现飞出屏幕外,再也拖不回来了。) -**The Solution (方案)**: Banned `zoom`. Transitioned to relative scaling via `font-size: calc(16px * var(--anomalous-scale))`. Fixed bounding clamps to execute `< 0` checks last. -(彻底封杀 zoom。改用 `calc(16px * scale)` 的底层字体相对推算放大法。并且把防负数的保底检测代码调到了逻辑的最后一行,永远杜绝负数坐标。) - -## 27. Vanilla JS "Fake" i18n Failure (原生状态管理漏斗下的假性汉化失效) -**The Problem (问题)**: Newly added permanent buttons ("UI Scale", "Reset") remained in Chinese when English was toggled. Since we lacked React/Vue two-way binding, they missed the global state update and got stuck. -(新加了“UI缩放”等按钮后,即使点了切换英文,它们还是中文。因为我们没有使用 Vue 的数据绑定,纯靠原生 JS。如果新加了常驻按钮,却忘了把它登记到顶层 `langBtn.onclick` 突变循环里,它就永远被隔离在旧时间线里了。) -(只要是常驻的按钮,必须强制在语言切换中枢里注册重写一遍。同时开发过程中如果遇到“代码没变”的灵异现象,第一反应永远是按 Ctrl+F5 强制破除浏览器静态缓存。) - -## 28. The Image Cropping Dilemma (图库的裁切与留白博弈) -**The Problem (问题)**: Using `object-fit: cover` in the image gallery created perfectly uniform grid squares, but brutally decapitated tall images (like 9:16 vertical character portraits). -(在图库排版时,为了让网格看起来绝对整齐,使用了 `object-fit: cover`。结果导致偏长的图片(如 9:16 的人物竖图)顶部和底部被无情裁掉,经常出现“砍头”现象。) -**The Solution (方案)**: Changed to `object-fit: contain` and paired it with a deep black background (`#000`). The images now scale down to fit entirely, and the resulting letterboxing seamlessly blends into the dark UI as a natural frame. -(改为 `object-fit: contain`,完整显示图片。由于底层卡片背景本身就是极黑(`#000`),缩小后产生的黑边完美融入了 UI 背景,不仅没有任何违和感,反而像是一圈高级的画框,一举两得。) - -## 29. Vanilla JS Global Click-Outside (原生悬浮面板的全局点击收起) -**The Problem (问题)**: The settings popover only closed if the user explicitly clicked the gear icon again. Clicking elsewhere on the canvas did nothing, making the popover feel rigid and intrusive. -(点击齿轮打开设置悬浮气泡后,必须再点一次齿轮才能关掉。如果随手点一下旁边或者点画布,面板居然不收回,交互显得非常死板和占眼。) -**The Solution (方案)**: Attached a temporary `mousedown` event listener to the `document` when the popover opens. If the click target is outside the modal, it triggers `display: none` and instantly `removeEventListener` to prevent memory leaks. -(面板弹出时,立刻给全局 `document` 挂载一个监听器。只要检测到鼠标点击了面板外部区域,立刻收缩面板,并且**顺手注销掉这个监听器**,实现了极度顺滑的 Gemini 式收放体验且毫无性能负担。) - -## 30. Context Preservation vs UX Intention (上下文保留与用户意图的冲突) -**The Problem (问题)**: After picking a new cover from the Gallery while inside the Model Edit Modal, the UI destroyed the modal, showed the gallery, and then *restored* the modal upon completion to 'preserve context'. However, users who just wanted to change the cover felt confused, thinking they still needed to click 'Save' to apply it. -(在编辑面板中点击更换封面后,UI 会先销毁面板,进入图库,选完后再把编辑面板弹回来以“保留上下文”。但用户其实只想换个封面,重新弹出的面板反而让他们误以为必须再点一次“保存”才算数。) -**The Solution (方案)**: Respect the primary intention. When the cover is changed, directly return to the model card/grid. Do not stubbornly restore intermediate modals unless necessary. -(顺应主要意图。换完封面直接回到网格卡片,干脆利落。不要去死板地恢复中间态面板。) - -## 31. The Missing Reference Crash (引用丢失导致的状态黑洞) -**The Problem (问题)**: A refactor failed to assign `this.models = data.models` in the load function. When a UI callback later tried to find the updated model using `this.models.find()`, JS threw an `undefined` error and aborted execution midway. This left the UI with both the gallery and grid hidden, resulting in a completely blank screen. -(重构时忘了一行赋值代码,导致 `this.models` 为空。后续 UI 切换时试图从中查找模型直接报错崩溃。由于崩溃发生在显示/隐藏面板的切换中途,导致所有面板都被隐藏,屏幕变成了彻底的“真空空白”。) -**The Solution (方案)**: Always ensure foundational state variables are populated before executing callbacks that depend on them. -(底层状态变量必须绝对可靠,一旦中途崩溃,UI 的半途状态是毁灭性的。) - -## 32. Backup Entanglement of Non-Platform Models (非平台模型的备份污染) -**The Problem (问题)**: To ensure a 'Reset' always had a fallback, changing any cover backed up the original file to `.civitai_bak`. For non-Civitai models, this meant user's manual `.png` covers were forcefully renamed to `.civitai_bak`, causing severe semantic confusion. -(为了保证“重置”功能永远有退路,之前的逻辑会把任何被替换的封面强行改名为 `.civitai_bak`。这就导致用户自己放的非 C 站模型封面,也被莫名其妙打上了 C 站的烙印,逻辑极其混乱。) -**The Solution (方案)**: Decouple custom covers by strictly saving them as `.preview.*` and leaving original standard `.png` files untouched. Resetting now just deletes the `.preview.*` file, naturally falling back to the untouched original, or copying the true `.civitai_bak` if it actually came from Civitai. -(让自定义封面独占 `.preview.*` 后缀,绝不触碰原有的标准封面。重置时只需删掉自定义后缀,系统就会自然回退。彻底切断了非 C 站模型与 C 站备份标识的无理纠缠。) - - -## 33. DOM Video Audio Leaks on innerHTML Clear (DOM 销毁导致的幽灵音频泄漏) -**The Problem (问题)**: When navigating from a model detail view containing an actively playing