From 75ed59f082316d29280e261bfb3cc6972984c8de Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Sun, 7 Jun 2026 05:30:21 +0800 Subject: [PATCH] Add files via upload --- README.md | 24 ++++-- __pycache__/api.cpython-313.pyc | Bin 28792 -> 29611 bytes api.py | 107 ++++++++++++++----------- error_and_experience_summary.md | 82 +++++++------------ scraper.py | 13 ++- update_bugfixes.py | 135 ++++++++++++++++++++++++++++++++ update_final_fixes.py | 60 ++++++++++++++ web/main.js | 12 ++- 8 files changed, 325 insertions(+), 108 deletions(-) create mode 100644 update_bugfixes.py create mode 100644 update_final_fixes.py diff --git a/README.md b/README.md index 44e8ebd..57bd02e 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,20 @@ In the rapidly evolving landscape of AI art tools, legacy plugins have become bl 4. **🤖 Smart Media Civitai Scraper (Multi-Threaded Daemon)** - Forget messy CMD windows! Paste your Civitai API Key in the UI (safely saved to both local server config and browser cache). - Click `Scan Folder` to silently launch a background daemon thread that computes SHA256, fetches metadata, and automatically renames your local `.safetensors` files to a clean format. + - **Detailed Scan Reports**: After scanning, the system alerts you exactly how many models were successfully downloaded, renamed, or skipped, and logs any failures. + - **Strict Verification Engine**: The scraper uses a "triple-check" strict requirement (.safetensors + .info + preview) to ensure your model library is flawless; missing parts trigger an automatic redownload. - **Smart Version Deduplication**: Have you ever accidentally downloaded the same version of a model twice with different names? The scanner will detect identical Civitai versions and cleanly permanently delete the redundant clones, ensuring your disk is free of duplicates! - **Smart Content-Type Detection**: The new scraper engine dynamically parses HTTP network headers. If a Civitai preview is a video, it perfectly saves it as an `.mp4` or `.webm` file rather than a corrupted PNG. - **Standalone Cleanup Button**: We extracted the `.civitai.info` cleanup process into a fast, dedicated `🧹 Clean Duplicate Info` button in the UI. - **Energy-Saving Previews**: Video previews only play on mouse hover! -5. **🌐 Pure Bilingual Engine & Built-in Manual** +5. **⚡ Workflow Superchargers** + - **Cross-Folder Compatible Models**: Looking at a Base Model (like Flux.1 D)? The detail panel auto-magically scans all your Loras and Checkpoints across your entire disk and lists 100% compatible models based on internal metadata logic. It strictly de-duplicates models even if your extra paths alias the same folders! + - **One-Click Auto-Inject**: Don't manually type filenames into nodes anymore! Use the "Apply to Canvas" floating button on any model. It creates the perfect node (CheckpointLoaderSimple, LoraLoader, UNETLoader) and magnetically sticks it to your cursor for precision placement! + +6. **🌐 Pure Bilingual Engine & Built-in Manual** - **Seamless i18n**: Switch between beautifully formatted pure English or pure Chinese UI with a single click, instantly transforming all buttons, dialogs, and dynamic text. - - **Interactive Help Modal**: Never feel lost with the built-in `❓ Help` manual explaining every feature natively within the plugin. + - **Interactive Help Modal**: Never feel lost with the built-in `❓ Help` manual explaining every feature natively within the plugin. The sandwich layout ensures the close button is never pushed off-screen regardless of how long the text is. ### 📥 Installation @@ -98,14 +104,20 @@ Say goodbye to `pip install` and `npm install`! 4. **🤖 全格式动态刮削引擎 (后台多线程守护)** - 全局双端持久化保存 Civitai API Key,一键静默扫描本文件夹。 - 真正的独立后台多线程,计算海量 7GB 模型 SHA256 时前端 UI 完全不卡顿。 - - **智能版本号去重守护**:你是否曾因为不小心把同一个模型版本下载了两次而浪费了硬盘空间?后台扫描不仅会匹配信息,一旦发现某个模型在同文件夹下存在版本号完全一致的“多余分身”,它会毫不犹豫地将冗余副本永久删除,保证你的模型库只有唯一规范的版本! - - **独立重复 Info 清理**:我们将历史遗留的冗余 `.civitai.info` 文件的清理功能独立成了顶部的 `🧹 清理重复 Info` 快捷按钮,一秒钟即可全盘扫描并完成清理。 + - **详尽扫描报告**:扫描结束后,系统会弹窗向您详细汇报成功下载的数量、重命名的数量以及失败的信息,进度一目了然。 + - **严苛三证合一**:采用严厉的补齐逻辑,只要发现模型缺失 `.info` 配置文件或预览图其中任何一项,刮削器便会毫不犹豫地向服务器请求补齐残缺,确保您的模型生态完美无瑕。 + - **智能版本号去重守护**:后台扫描不仅会匹配信息,一旦发现某个模型在同文件夹下存在版本号完全一致的“多余分身”,它会毫不犹豫地将冗余副本永久删除,保证你的模型库只有唯一规范的版本! + - **独立重复 Info 清理**:历史遗留的冗余 `.civitai.info` 文件清理功能被独立为顶部的 `🧹 清理重复 Info` 按钮,一秒即可全盘扫描并完成清理。 - **全媒体探测雷达**:不仅能抓图片,遇到新世代模型的 `.mp4` / `.webm` 视频封面,爬虫会根据 `Content-Type` 报文头精准保存对应的视频格式,彻底告别“损坏的图片”。 - **节能渲染**:视频只在鼠标悬浮时播放,杜绝显卡资源浪费! -5. **🌐 纯净双语引擎与内置说明书** +5. **⚡ 生产力飞跃 (Workflow Superchargers)** + - **跨次元兼容模型雷达**:当你在查看 UNet (比如 Flux.1 D) 时,详情页底部会自动跨硬盘、跨文件夹为您检索出全部绝对兼容的 Lora / Checkpoints!并且完美自带系统级别的物理路径去重机制,即便你的 `extra_model_paths.yaml` 怎么套娃映射,都绝不显示重复项。 + - **一键磁吸加载器**:告别手动搜索文件名!鼠标悬停在模型上点击【投放到画布】,系统会自动为你生成匹配的节点 (如 LoraLoader、UNETLoader),并将其磁吸在你的鼠标上!你只需要在画布的合适位置点一下左键,节点就会被优雅地放置好,丝滑无比。 + +6. **🌐 纯净双语引擎与内置说明书** - **无缝 i18n 切换**:一键在纯中文与纯英文界面间自由切换,告别拥挤的双语混排,所有弹窗与提示会瞬间自适应目标语言。并针对英文排版做了动态文字缩放,确保界面永不换行崩塌。 - - **交互式帮助面板**:内置随叫随到的 `❓ 帮助` 面板,提供中英文纯血的详细说明书,再也不用切回 GitHub 看使用文档了! + - **交互式帮助面板**:内置随叫随到的 `❓ 帮助` 面板,提供中英文纯血的详细说明书,再也不用切回 GitHub 看使用文档了!三明治结构设计保证关闭按钮永远固定在视野内,不会因为文字过多而点不到。 ### 📥 极简安装说明 diff --git a/__pycache__/api.cpython-313.pyc b/__pycache__/api.cpython-313.pyc index 0ce4df297d89f408164d21d2f69bcb5de08dfb00..1eb9dd308b44f8212ddd751b932b5f9f50d3c003 100644 GIT binary patch delta 3441 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 `.mp4` - - `video/webm` -> `.webm` - - `image/jpeg` -> `.jpg` - - 其他 -> `.png` - 并在重命名循环中把 `.mp4` 和 `.webm` 加入“连带重命名套餐”,确保媒体文件与底模同生共死。 - - -## 6. 接头暗号对不上:API 路由映射失误 (Backend Routing) -* **症状**:前端在输入 API Key 后(即使浏览器缓存记录了密码),本地依然没有生成 config.json 配置文件。 -* **死因**:前端 main.js 试图往后端发送保存请求的路由是 POST /anomalous/save_config,而后端 pi.py 监听这个保存函数的路由却错误地写成了 pp.router.add_post('/anomalous/config', api_save_config)。这导致前端的保存请求永远打到一个 404 死胡同,而刚好前端加了 catch(e) {} 忽略了报错,所以错误被静默吞噬了。 -* **解决方案**:永远确保前后端通信的 API 路径 (Endpoint) 字符串绝对一致。将后端路由严格修改为 pp.router.add_post('/anomalous/save_config', api_save_config),保证数据能够成功落盘。 - ---- - -*Created by Antigravity Assistant on 2026-06-05* +# 插件开发纠错与经验沉淀 (Error & Experience Summary) + +在开发与完善 `Anomalous_Model_Browser` 插件的过程中,我们遇到了几个典型的技术陷阱与用户体验问题。为了便于后续维护和其他开发者参考,特在此归纳沉淀: + +## 1. 后台 API 数据传递与作用域异常 (UnboundLocalError) +**问题描述**:在重构 `api.py` 中的 `api_compatible_models`(跨文件夹兼容模型扫描)逻辑时,遇到了前端抛出 500 错误(`Failed to load`)。 +**根源分析**:在 Python 中,如果一个变量(如 `rel_subfolder`)在 `if` 语句块内部被赋值,但在执行时该 `if` 条件未满足,后续代码又尝试读取该变量,就会引发 `UnboundLocalError`。在我们早期的代码缩进修复中,因为缩进层级错乱,导致赋值语句和读取语句的逻辑层级脱节。 +**解决方案**:严格对齐缩进,并确保所有分支(尤其是 `preview_url` 等变量)在被使用前都有安全的默认回退值(Fallback initialization, 如 `preview_url = ""`)。 + +## 2. 文件夹别名导致的重复加载问题 (Duplicate Models) +**问题描述**:前端模型兼容列表出现了两个完全一模一样的 Flux 模型。 +**根源分析**:前端触发搜索时,同时传入了 `checkpoints,unet,diffusion_models` 三个目录作为扫描目标。而在 ComfyUI 的环境配置中,`unet` 和 `diffusion_models` 经常被映射到硬盘上的**同一个物理文件夹**。后台在遍历时,对同一个文件夹扫描了两次,导致相同文件被重复添加。 +**解决方案**:使用 `seen_files = set()` 记录已处理文件的**绝对物理路径**(`os.path.realpath(file_path)`),即使软链接或不同挂载点指向同一文件,也能被精准去重拦截。 + +## 3. 增量扫描逻辑的盲区 (Scanner Skip Logic) +**问题描述**:用户手动删除了某个模型的 `.info` 文件或预览图,点击后台扫描引擎却没有补齐,依然直接跳过。 +**根源分析**:早期的增量逻辑比较“偷懒”,只要发现同名的 `.info` 文件存在,就直接 `continue` 跳过,忽略了对预览图的二次检查。 +**解决方案**:重写扫描引擎的短路逻辑。采用“三证合一”严苛检查:只有当 `.info` 文件存在,**且**同名的 `[.png, .jpg, .mp4, .webm]` 预览媒体中至少有一项存在时,才允许跳过;否则强制触发 C 站接口进行重扫补齐。 + +## 4. UI/UX 自适应排版 (Responsive UI) +**问题描述**:模型名称过长时,顶部固定栏被强行撑高并发生换行;说明面板太大导致底部关闭按钮超出屏幕外。 +**根源分析**:未对弹性盒子(Flexbox)进行文本溢出控制,且模态框缺乏滚动边界限制。 +**解决方案**: +- **单行溢出省略**:对可能超长的标题应用 `white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;`。 +- **三明治模态布局**:对弹出面板应用 `max-height: 90vh`,并对其内部的文本内容层(Body)单独应用 `overflow-y: auto`。这样无论内容多长,只会在中间区域滚动,头部的“X”和底部的“关闭”按钮始终固定可见。 + +## 5. 安全性审计 (Security Audit) +对后端的关键路由进行了路径穿越(Path Traversal)核查: +- `api_delete_model`, `api_serve_image`, `api_get_models` 均内置了 `if '..' in subfolder` 和 `if '..' in filename` 的强拦截逻辑,确保用户无法通过构造相对路径去跨级删除或访问插件/工作流之外的核心系统文件。 +- `api_serve_image` 增加了严格的文件拓展名(extension)白名单字典,强制转换 `Content-Type`,杜绝了把可执行脚本伪装成预览图返回的风险。 diff --git a/scraper.py b/scraper.py index cef09d9..1177848 100644 --- a/scraper.py +++ b/scraper.py @@ -180,9 +180,16 @@ def main(): file_path = os.path.join(root, filename) old_base = os.path.splitext(file_path)[0] - # 增量跳过:如果已经存在 .info 文件,说明已经成功扫描过,直接跳过 - if os.path.exists(old_base + ".info"): - print(f"[*] 已跳过 (已扫描过): {filename}") + # 增量跳过:要求模型本体、.info、预览图三个文件全在,才算完整跳过 + info_exists = os.path.exists(old_base + ".info") or os.path.exists(old_base + ".civitai.info") + preview_exists = False + for ext in [".png", ".preview.png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm"]: + if os.path.exists(old_base + ext): + preview_exists = True + break + + if info_exists and preview_exists: + print(f"[*] 已跳过 (信息与预览图均完整): {filename}") continue print(f"\n---> 处理文件: {filename} (位于 {root})") diff --git a/update_bugfixes.py b/update_bugfixes.py new file mode 100644 index 0000000..392da69 --- /dev/null +++ b/update_bugfixes.py @@ -0,0 +1,135 @@ +import os + +path_api = r'E:\ComfyUI_windows_portable\ComfyUI\custom_nodes\Anomalous_Model_Browser\api.py' +with open(path_api, 'r', encoding='utf-8') as f: + content_api = f.read() + +# Fix api.py to support multiple target_types and return size_mb +old_api = ''' try: + paths = folder_paths.get_folder_paths(target_type) + except Exception: + return web.json_response({"models": []}) + + if not paths: + return web.json_response({"models": []}) + + compatible_models = [] + + for path_idx, base_dir in enumerate(paths): + if not os.path.exists(base_dir): + continue + + for root, _, files in os.walk(base_dir): + for f in files: + if f.endswith('.safetensors') or f.endswith('.ckpt') or f.endswith('.pt'): + file_path = os.path.join(root, f) + meta = get_metadata(file_path) + + if meta.get("baseModel") == base_model: + rel_subfolder = os.path.relpath(root, base_dir)''' + +new_api = ''' target_types = [t.strip() for t in target_type.split(',')] + compatible_models = [] + + for t in target_types: + try: + paths = folder_paths.get_folder_paths(t) + except Exception: + continue + + if not paths: + continue + + for path_idx, base_dir in enumerate(paths): + if not os.path.exists(base_dir): + continue + + for root, _, files in os.walk(base_dir): + for f in files: + if f.endswith('.safetensors') or f.endswith('.ckpt') or f.endswith('.pt'): + file_path = os.path.join(root, f) + meta = get_metadata(file_path) + + if meta.get("baseModel") == base_model: + rel_subfolder = os.path.relpath(root, base_dir)''' + +if old_api in content_api: + content_api = content_api.replace(old_api, new_api) + +# Add size_mb to API +old_api_size = ''' if preview_file: + q_type = urllib.parse.quote(target_type) + q_idx = str(path_idx) + q_sub = urllib.parse.quote(rel_subfolder.strip('/')) if rel_subfolder != '/' else "" + q_file = urllib.parse.quote(preview_file) + preview_url = f"/anomalous/image?type={q_type}&path_idx={q_idx}&subfolder={q_sub}&filename={q_file}" + + compatible_models.append({ + "type": target_type, + "path_idx": path_idx, + "subfolder": rel_subfolder, + "filename": f, + "preview_url": preview_url, + "metadata": meta + })''' + +new_api_size = ''' if preview_file: + q_type = urllib.parse.quote(t) + q_idx = str(path_idx) + q_sub = urllib.parse.quote(rel_subfolder.strip('/')) if rel_subfolder != '/' else "" + q_file = urllib.parse.quote(preview_file) + preview_url = f"/anomalous/image?type={q_type}&path_idx={q_idx}&subfolder={q_sub}&filename={q_file}" + + try: + size_mb = round(os.path.getsize(file_path) / (1024 * 1024), 1) + except: + size_mb = 0 + + compatible_models.append({ + "type": t, + "path_idx": path_idx, + "subfolder": rel_subfolder, + "filename": f, + "size_mb": size_mb, + "preview_url": preview_url, + "metadata": meta + })''' + +if old_api_size in content_api: + content_api = content_api.replace(old_api_size, new_api_size) + +with open(path_api, 'w', encoding='utf-8') as f: + f.write(content_api) +print('Updated api.py') + +# Update main.js +path_js = r'E:\ComfyUI_windows_portable\ComfyUI\custom_nodes\Anomalous_Model_Browser\web\main.js' +with open(path_js, 'r', encoding='utf-8') as f: + content_js = f.read() + +# Fix targetType to search multiple folders +content_js = content_js.replace("const targetType = this.currentType === 'loras' ? 'checkpoints' : 'loras';", "const targetType = this.currentType === 'loras' ? 'checkpoints,unet,diffusion_models' : 'loras';") + +# Fix help UI overflow +old_help_css = ''' const helpBox = document.createElement('div'); + helpBox.style.background = '#333'; + helpBox.style.padding = '20px'; + helpBox.style.borderRadius = '8px'; + helpBox.style.width = '80%'; + helpBox.style.maxWidth = '600px';''' + +new_help_css = ''' const helpBox = document.createElement('div'); + helpBox.style.background = '#333'; + helpBox.style.padding = '20px'; + helpBox.style.borderRadius = '8px'; + helpBox.style.width = '80%'; + helpBox.style.maxWidth = '600px'; + helpBox.style.maxHeight = '90vh'; + helpBox.style.overflowY = 'auto';''' + +if old_help_css in content_js: + content_js = content_js.replace(old_help_css, new_help_css) + +with open(path_js, 'w', encoding='utf-8') as f: + f.write(content_js) +print('Updated main.js') diff --git a/update_final_fixes.py b/update_final_fixes.py new file mode 100644 index 0000000..75bf1e0 --- /dev/null +++ b/update_final_fixes.py @@ -0,0 +1,60 @@ +import os + +# 1. Update scraper.py skip logic +path_scraper = r'E:\ComfyUI_windows_portable\ComfyUI\custom_nodes\Anomalous_Model_Browser\scraper.py' +with open(path_scraper, 'r', encoding='utf-8') as f: + content_scraper = f.read() + +old_skip = ''' # 增量跳过:如果已经存在 .info 文件,说明已经成功扫描过,直接跳过 + if os.path.exists(old_base + ".info"): + print(f"[*] 已跳过 (已扫描过): {filename}") + continue''' + +new_skip = ''' # 增量跳过:要求模型本体、.info、预览图三个文件全在,才算完整跳过 + info_exists = os.path.exists(old_base + ".info") or os.path.exists(old_base + ".civitai.info") + preview_exists = False + for ext in [".png", ".preview.png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm"]: + if os.path.exists(old_base + ext): + preview_exists = True + break + + if info_exists and preview_exists: + print(f"[*] 已跳过 (信息与预览图均完整): {filename}") + continue''' + +if old_skip in content_scraper: + content_scraper = content_scraper.replace(old_skip, new_skip) + with open(path_scraper, 'w', encoding='utf-8') as f: + f.write(content_scraper) + print('Updated scraper skip logic') + +# 2. Update main.js help modal CSS +path_js = r'E:\ComfyUI_windows_portable\ComfyUI\custom_nodes\Anomalous_Model_Browser\web\main.js' +with open(path_js, 'r', encoding='utf-8') as f: + content_js = f.read() + +old_box = ''' box.style.boxShadow = '0 10px 40px rgba(0,0,0,0.8)'; + box.style.display = 'flex'; + box.style.flexDirection = 'column';''' + +new_box = ''' box.style.boxShadow = '0 10px 40px rgba(0,0,0,0.8)'; + box.style.display = 'flex'; + box.style.flexDirection = 'column'; + box.style.maxHeight = '90vh';''' + +old_body = ''' const body = document.createElement('div'); + body.style.padding = '20px'; + body.innerHTML = t('helpContent');''' + +new_body = ''' const body = document.createElement('div'); + body.style.padding = '20px'; + body.innerHTML = t('helpContent'); + body.style.overflowY = 'auto'; + body.style.flex = '1';''' + +if old_box in content_js and old_body in content_js: + content_js = content_js.replace(old_box, new_box).replace(old_body, new_body) + with open(path_js, 'w', encoding='utf-8') as f: + f.write(content_js) + print('Updated main.js modal CSS') + diff --git a/web/main.js b/web/main.js index 58a47aa..11c3037 100644 --- a/web/main.js +++ b/web/main.js @@ -372,6 +372,7 @@ class AnomalousBrowser { box.style.boxShadow = '0 10px 40px rgba(0,0,0,0.8)'; box.style.display = 'flex'; box.style.flexDirection = 'column'; + box.style.maxHeight = '90vh'; const header = document.createElement('div'); header.style.padding = '15px 20px'; @@ -400,6 +401,8 @@ class AnomalousBrowser { const body = document.createElement('div'); body.style.padding = '20px'; body.innerHTML = t('helpContent'); + body.style.overflowY = 'auto'; + body.style.flex = '1'; const footer = document.createElement('div'); footer.style.padding = '15px'; @@ -784,9 +787,14 @@ const backBtn = document.createElement('button'); const title = document.createElement('h2'); title.innerHTML = model.filename; - title.style.margin = '0 0 0 20px'; + title.style.margin = '0 20px 0 20px'; title.style.color = '#fff'; title.style.fontSize = '1.2em'; + // 强制单行并溢出显示省略号 + title.style.whiteSpace = 'nowrap'; + title.style.overflow = 'hidden'; + title.style.textOverflow = 'ellipsis'; + title.style.flex = '1'; // 撑开剩余空间,把右侧按钮挤到最右边 const delBtn = document.createElement('button'); delBtn.innerHTML = t('delModel'); @@ -1105,7 +1113,7 @@ const backBtn = document.createElement('button'); compSec.appendChild(compList); rightPanel.appendChild(compSec); - const targetType = this.currentType === 'loras' ? 'checkpoints' : 'loras'; + const targetType = this.currentType === 'loras' ? 'checkpoints,unet,diffusion_models' : 'loras'; fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(m.baseModel)}&target_type=${encodeURIComponent(targetType)}`) .then(r => r.json()) .then(d => {