Add files via upload

This commit is contained in:
DemonGatanjieu
2026-06-07 05:30:21 +08:00
committed by GitHub
parent c184df7558
commit 75ed59f082
8 changed files with 325 additions and 108 deletions
+18 -6
View File
@@ -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 看使用文档了!三明治结构设计保证关闭按钮永远固定在视野内,不会因为文字过多而点不到。
### 📥 极简安装说明
Binary file not shown.
+62 -45
View File
@@ -469,56 +469,73 @@ async def api_compatible_models(request):
if not base_model:
return web.json_response({"models": []})
try:
paths = folder_paths.get_folder_paths(target_type)
except Exception:
return web.json_response({"models": []})
if not paths:
return web.json_response({"models": []})
target_types = [t.strip() for t in target_type.split(',')]
compatible_models = []
seen_files = set()
for path_idx, base_dir in enumerate(paths):
if not os.path.exists(base_dir):
for t in target_types:
try:
paths = folder_paths.get_folder_paths(t)
except Exception:
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 rel_subfolder == '.':
rel_subfolder = '/'
else:
rel_subfolder = '/' + rel_subfolder.replace('\\', '/')
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)
real_path = os.path.realpath(file_path)
if real_path in seen_files:
continue
seen_files.add(real_path)
meta = get_metadata(file_path)
m_bm = str(meta.get("baseModel", "")).strip().lower()
req_bm = str(base_model).strip().lower()
if m_bm and m_bm == req_bm:
rel_subfolder = os.path.relpath(root, base_dir)
if rel_subfolder == '.':
rel_subfolder = '/'
else:
rel_subfolder = '/' + rel_subfolder.replace('\\', '/')
base_name = os.path.splitext(f)[0]
preview_file = None
for ext in ['.preview.png', '.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm']:
if os.path.exists(os.path.join(root, base_name + ext)):
preview_file = base_name + ext
break
base_name = os.path.splitext(f)[0]
preview_file = None
for ext in ['.preview.png', '.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm']:
if os.path.exists(os.path.join(root, base_name + ext)):
preview_file = base_name + ext
break
preview_url = ""
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
})
preview_url = ""
preview_url = ""
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
})
return web.json_response({"models": compatible_models})
+30 -52
View File
@@ -1,52 +1,30 @@
# Anomalous Model Browser - 开发经验与踩坑总结日志
这份文档记录了在开发和重构 `Anomalous_Model_Browser` 插件过程中遇到的核心 Bug、崩溃原因以及对应的终极解决方案。供后续开发维护时参考,避免重蹈覆辙。
## 1. 前端 UI 注入陷阱:引号转义地狱 (DOM vs innerHTML)
* **症状**:按钮凭空消失、点击事件无法触发、页面结构错乱。
* **死因**:在 `main.js` 中重构右侧详细面板时,使用了极长且复杂的 ES6 模板字符串拼接 HTML,并通过 `innerHTML` 暴力注入。在这个过程中,大量的单引号、双引号、反引号以及 JS 变量相互嵌套,只要有一个未正确转义,整个浏览器的 HTML 解析器就会崩溃,导致后续 DOM 元素被直接截断。
* **解决方案**:**彻底抛弃复杂字符串注入**。必须老老实实使用原生的 `document.createElement()`、`el.appendChild()` 逐层构建 DOM 树,这样不仅彻底杜绝了转义问题,还能干净利落地绑定 `el.onclick` 等事件监听器,稳定性直接拉满。
## 2. Python 字符串拼接陷阱:正则表达式换行符 (SyntaxError)
* **症状**:扫描脚本 `scraper.py` 在启动的瞬间直接闪退报错 `SyntaxError: unterminated string literal`。
* **死因**:在用 Python 脚本去 patch (热更新) 另一段 Python 代码时,使用了普通的 `'''` 三引号字符串去包裹一段含有 `\r\n` 的正则表达式:`r'[\r\n\t]+'`。因为没有使用 Raw String (`r'''`),Python 在执行替换时直接把 `\r\n` 转译成了**真实的物理换行符**,导致最终写进目标文件里的正则字符串被硬生生劈成了两半,引发语法错误。
* **解决方案**:在涉及代码注入或正则拼接时,**永远小心对待反斜杠**。优先使用 `replace_file_content`,或者在 Python 热更代码中必须使用 Raw 多行字符串 `r'''...'''`。
## 3. Windows 控制台的致命伤:Emoji 编码崩溃 (UnicodeEncodeError)
* **症状**:当扫描到带有特殊符号(如“Plant Milk 🌿”)的模型时,整个扫描脚本瞬间崩溃,后续所有模型全部罢工。
* **死因**:Windows 系统的终端默认使用 `GBK` 编码。当 `scraper.py` 试图用 `print()` 打印含有 `🌿` (`\U0001f33f`) 这种多字节 Unicode 字符的文件名时,GBK 无法对其进行编码,直接抛出 `UnicodeEncodeError` 异常终止程序。
* **解决方案**:在任何需要输出日志的 Python 脚本开头,**强制接管系统标准输出的编码**:
```python
import sys
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except:
pass
```
## 4. 虚假的异步完成:多线程与进度锁 (Architecture Bug)
* **症状**:点击“扫描”后,网页立刻弹窗提示“扫描完成”,但此时大模型根本还没有下载任何图片。用户一旦此时刷新,只能看到旧的错乱文件。
* **死因**:后端 `api.py` 调用扫描脚本时使用了 `subprocess.Popen`,它是非阻塞的。`api.py` 把脚本丢到后台运行后,自己立刻就返回了 HTTP 200 OK。前端收到 OK 就以为扫描结束了,实际上后台还在吭哧吭哧计算几十 GB 文件的 SHA256。
* **解决方案**:引入**文件级进度锁**与**守护线程**。
在启动扫描前,生成一个 `.scan_in_progress` 的占位文件,然后开启一个 `threading.Thread` 去运行 `subprocess.run` (阻塞等待)。等到线程里的扫描任务真实结束后,再利用 `finally` 代码块删掉 `.scan_in_progress` 锁文件。前端必须轮询检测这个锁文件是否存在,消失了才算真正的 Complete。
## 5. 盲人摸象的媒体下载:图片损坏之谜 (Content-Type)
* **症状**:部分最新的模型(如 MiaoMiao RealSkin)下载下来的 `.png` 预览图在系统中显示“文件已损坏”或无法打开。
* **死因**:随着 Civitai 的升级,部分模型的首张预览图变成了 `.mp4` 或 `.webm` 格式的视频。而旧版下载函数 `download_image` 是“一刀切”的瞎子,不管三七二十一直接把下载流重命名为 `.png`。把视频文件强行加个 `.png` 后缀,图片浏览器自然无法解析。
* **解决方案**:重写下载器为 `download_media`,必须依靠解析 HTTP 报文头的 `Content-Type` 来决定最终命运:
- `video/mp4` -> `.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`,杜绝了把可执行脚本伪装成预览图返回的风险。
+10 -3
View File
@@ -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})")
+135
View File
@@ -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')
+60
View File
@@ -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')
+10 -2
View File
@@ -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 => {