Files
DemonGatanjieu-Anomalous_Mo…/docs/architecture/lessons.md
T
DemonGatanjieuandClaude Opus 5.5 47442d00f6 Keep the Saves list still during a press
A new picture puts itself first in its line, so a press on a picture
ended on another one and was no click. While the main button is held on
a button of the list, what comes waits until the press and its click
are over.

Tested with a real mouse on an isolated ComfyUI: a press held until a
new picture came opened the pressed picture 4 times of 4 (0 before), and
16 of 16 long presses during runs answered (13 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 21:21:41 +08:00

16 KiB
Raw Blame History

踩过的坑(Lessons)

反复出现的坑和严重故障。每条写清:现象、根因、怎么发现的、以后怎么做。


2026-09-26 · v1.57.2:一条 :has() 样式让拖动画布掉帧(严重)

现象:B 站用户反馈装了插件后拖动工作流 10% low 只有二十几帧;和 ComfyUI Prompt Assistant 一起装时,大工作流只剩个位数帧甚至卡死。插件界面关着也一样。

根因:web/styles/01-shell-tools.css(b6acd09,2026-09-19,进了 v1.57 / v1.57.1)

body:has(#anomalous-toolbox-modal[style*="display: flex"]) #anomalous-sidebar-tooltip-bubble, …

:has() 里放属性选择器(这里是 [style*=…]),浏览器在页面上任何元素的该属性变化时,都要重新判断 body:has(...)。ComfyUI 平移、缩放画布时每一帧都在改每个 DOM 控件(文本框)的 style,于是每帧都要付出和页面元素数量成正比的样式计算。Prompt Assistant 给每个文本框加一组按钮,页面元素多十几倍,开销跟着放大。两个插件本身没有冲突。

实测(真 ComfyUI 前端 1.53.6 + Chromium,300 节点工作流平移 120 帧,样式重算合计):

情况 修复前 修复后
不装插件 60 ms —
只装 AMB 690 ms 63 ms
AMB + Prompt Assistant 2300 ms(约 19 ms/帧,超过 60 帧的一整帧) 86 ms
只装 Prompt Assistant 80 ms —

逐条删规则二分:只删掉这 7 条 :has() 就回到基线,其中罪魁是带 [style*=…] 的那条;button:has(.x) 等其他 :has 测不出开销。

修复:删掉这条规则。它是多余的:工具箱和设置窗口打开前都会调用 hideTooltipImmediately(),showSharedTooltip 在窗口开着时也会拒绝显示。

以后怎么做

  • 不要用 :has() 或 [style*="display: …"] 去判断界面开关状态。要表示“开着”,由 JS 加减一个 class(作用在自己的容器上),或者直接在 JS 里处理。
  • 全局生效的昂贵选择器(body:has、html:has、*、未限定作用域的 button、textarea)会作用在 ComfyUI 的每一帧上。新增前先问:它会不会在画布拖动时被反复判断?
  • 性能问题要实测,别只看代码猜:
    • 装一个真的 ComfyUI(--cpu 即可),用 Playwright 在空白处拖动画布;
    • 用 CDP 的 Performance.getMetrics 比较 RecalcStyleDuration;
    • 再在页面里删掉可疑规则做二分(遍历 document.styleSheets,对插件自己的样式表 deleteRule)。
    • 没有显卡时总帧时间不准,但样式计算的倍数可信。

2026-09-26 · 修复只在本机 main,发布版没带上

现象:素材库“选中节点时卡片错位”(应用按钮重复、图标重复成 ⚡ ⚡、标签日期挤在一起)的修复 01fc925、9e761d0 早在 9 月 24 日就做了,但只进了 feature/audio-engines(通过“Merge main … into feature”)。GitHub 上的 main 并没有,发布版 v1.57.1 的用户一直带着这个问题。

根因:修复提交在本机 main 上,只把 main 合进了 feature,本机 main 从未推送。

以后怎么做

  • 发布或回答“修了没有”之前,用 git branch -r --contains <提交> 确认修复在远端的哪个分支,别只看本机。
  • 本机 main 有未推送提交时,先推送或说明,再做合并。
  • 这次已把两个提交 cherry-pick 进远端 main(e1ac84f、4e8603b),随 v1.57.2 发布。

2026-09-26 · 发布热修复的检查项

  • 改了 CSS 或 JS,要同步改缓存参数:web/styles.css 里对应的 @import …?v=、browser_entry.js 的 styles.css?v=、main.js 引用 browser_entry.js 的 ?v=。不然用户刷新后浏览器可能还用旧文件。feature 分支已去掉部分 ?v=,合并时以各分支现状为准。
  • CHANGELOG.md 是 CRLF、LF 混合换行。用脚本编辑时按原字节读写(newline=''),否则整份文件都会显示成改动。
  • 云端会话能推分支,推不了标签,GitHub 工具也不能创建 Release。发布要在网页上完成:Draft a new release → 填新标签 → Target 选 main 并核对提交号 → 粘贴说明 → Publish。插件的版本面板只列正式 Release,用 ComfyUI Manager 更新的用户则直接拉 main 最新提交。

2026-09-27 · 拆文件漏了 import,保存工作流在发布版里整个坏掉

现象:v1.57 到 v1.57.2,工作流页“保存当前工作流”只要工作流里有模型加载器就提示“保存工作流配方失败”。B 站用户反馈“就没成功过”。素材库走的是另一条路,所以能存。

根因:refactor(recipes): separate schema images and storage(6ad975c)把函数搬进 recipe_images.py,但它调用的 _resolve_exact_model_reference 和 hashlib 留在了旧文件里。同一轮拆分还让 media_routes.py 少了 time、tempfile:卡片缩略图出错后被 except Exception 吞掉,静默退回原图。另外 model_metadata.py 少了 _resolve_paths_to_model_info_sync。Python 只在那一行真正执行时才报 NameError,导入和其他测试都通过。唯一的保存测试关掉了模型预览快照,正好绕开了出错的那一行。

以后怎么做

  • 拆模块后跑 tests/test_backend_names.py:后端文件读到却从没绑定的名字,它都会列出来。
  • 行为测试要走默认路径。recipe_payload 默认关着快照,新测试 test_saving_with_preview_snapshots_captures_the_model_cover 专门走默认的“开着快照”。
  • 真实按钮至少点一次:在隔离的 ComfyUI 里点“保存当前工作流”,一眼就能看出问题。

2026-10-12 · 页面往下滚后,点“返回”点到了下面的模型(第二次)

现象:从画布节点存的搭配(节点搭配),页面往下滚一点后,点“‹ 全部搭配”打开的是被滚到标题下面的模型封面。普通搭配和搭配列表没事。

根因:和 2026-10-09 修过的是同一个 Chrome 行为:滚动框里有 overflow: hidden 的元素(这里是模型封面的圆角框)或 content-visibility: auto 时,滚出可见区域的部分在点击判断里仍算“在那里”,盖住了上面的标题按钮。上次只给当时有问题的两个滚动框加了 clip-path: inset(0),之后新做的节点搭配页面又放进了带圆角封面的行。

怎么发现的:在隔离的 ComfyUI 里把节点存成搭配,把编辑区分 12 步滚到底,每一步用 document.elementFromPoint 取标题里每个按钮中心点的元素,看是不是按钮自己;滚到 140px 起就变成了封面 IMG。

以后怎么做

  • 标题、标签栏下面的滚动框一律加 clip-path: inset(0)(它对点击判断也生效)。overflow 本身裁不住这种情况。
  • 新页面或新内容放进滚动框后,用上面的办法扫一遍:每个滚动框分步滚到底,检查每个可见按钮都能拿到自己的点击。
  • 2026-11-03 第三次:当前节点 →「查看全部」打开的历史生成图库,滚下去后点“关闭图库”点到了图片。它的样式写在代码里(ui_gallery.js),搜 CSS 找不到,所以上次没扫到。现在它和模型选择器的卡片区都加了 clip-path: inset(0);扫的时候连写在代码里的滚动框一起扫(在页面上找 overflow-y 是 auto 或 scroll 的元素,不只搜 CSS 文件)。

2026-10-08 · ComfyUI-Manager 审核 Anomalous_TTS:三类安全口子

现象:Anomalous_TTS 申请收录进 ComfyUI-Manager(Comfy-Org/ComfyUI-Manager#3342),维护者两轮审核退回,先后指出:

  1. 下载来的东西被当代码执行:中文多音字模型从个人 ModelScope 账号的 master 下载,不核对指纹,解压后执行包里的 config.py;英文词典读 pickle。而网页请求还能改“去哪个文件夹找这些文件”,等于网页能决定运行什么代码。
  2. 网页请求决定删改哪里:POST /anomalous_tts/storage 从请求里取新的存放位置,跨盘移动后对旧位置 rmtree。只检查“是不是本机请求”不够:本机网页也不该决定在哪里建、删文件夹。
  3. 按请求里的路径读任意文件:导入预览、导入检查会返回请求点名的任何文件内容。

根因:这些接口最初是为了方便(在界面里选文件夹、试听本机音频、一键挪角色),没有把“网页请求是不可信的输入”当成前提;“只接受本机请求”被当成了足够的保护。

怎么改的(接口版本 13):配置文件当数据解析,词典从文本建,不读写 pickle,测试拦住 pickle.load / exec_module / eval(;下载锁定版本并先核对 SHA-256 再解压;存放位置、角色文件夹、底模来源只从用户手写的设置文件读;导入只收上传的文件,没有接口再接受本机路径。

以后怎么做

  • 两个仓库的规范都写了安全底线(本仓库 AGENTS.md 第 5 节“安全底线”,Anomalous_TTS 的 ARCHITECTURE.md“不能破的规矩”)。新接口按它写,审查时逐条对照。
  • 本仓库的现状(2026-10-09 统计):55 个会写文件的接口,只有 MCP、模型导入、整理三处检查了本机请求;删除模型、改模型信息、开始下载、导入备份等在 --listen 时局域网里的其他设备也能调用。没有执行下载内容、pickle、eval 的代码。2026-10-12 的 v1.58.1 一次补齐:api/local_guard.py 一个中间件把关 /anomalous/ 下所有会改东西的请求,只接受本机(见 backend.md“Requests from other devices”)。

2026-10-13 · v1.58.0–v1.58.2:同一种 :has() 又掉帧(第二次)

现象:#22(Lindemellis):216 个节点的工作流,开着 Prompt Assistant 时一次画布重绘 465–770 ms,拖动只有 2–3 帧。只删掉一条规则就回到 53 ms。

根因:图标栏(46facb9,2026-09-29,随 v1.58.0 发布)在 web/styles/15-shell-rail.css 里又写了

#anomalous-container.anomalous-has-rail:has(> .anomalous-nb-modal[style*="display: flex"]) #anomalous-sidebar-wrapper

和上面 v1.57.2 那条是同一种写法::has() 里按 style 属性匹配。v1.57.2 修好三天后就写回来了,因为经验只写在这份文档里,没有东西拦着;当时的性能实测也只跑了 main 和音频分支,没跑图标栏所在的分支。

修复:工作区开没开由 JS 在容器上加减 anomalous-workspace-open,CSS 只看这个 class(#22 的修复)。

以后怎么做

  • node tools/check_structure.mjs 现在会拒绝 :has() 里出现 [style…],报出文件和行号。
  • 同一个坑第二次出现,就把它做成检查,不再只补一段经验。

2026-10-29 · 一张 2.7 亿像素的图让图库卡死

现象:AI 把 5120×3328 的图再放大 4 倍,存成 20480×13312、324 MB 的 PNG。之后 AMB 图库一翻到它就很卡,AI 工作台里也一样。

根因:Pillow 默认拒绝打开超过约 1.79 亿像素的图(防“解压炸弹”)。缩略图生成用 except Exception: return source_path 兜底,失败时把原图当缩略图发给浏览器;浏览器每次都要下载 324 MB、解开 2.7 亿像素。看大图、AI 时间线放大、图片详情读参数也都直接拿原图。

怎么发现的:用户说图库“真的卡”,按文件大小找到这张图,在 ComfyUI 的 Python 里打开它,报 DecompressionBombError。

以后怎么做

  • 缩略图失败的兜底只能是“小文件原样”或占位图,永远不是大原图(media_routes.ORIGINAL_CARD_BYTES)。
  • 超过 Pillow 上限的图在子进程里缩(huge_picture.py),内存用完就还,ComfyUI 进程不碰它。
  • 要比缩略图大的地方用 /anomalous/output_preview,不用 /view 原图;只有用户点“打开原图”才给原图。

2026-10-29 · 为一个小函数引出循环导入,AMB 整个页面加载失败

现象:隔离的 ComfyUI 上 AMB 没有加载,window.anomalousBrowserInstance 不存在,AI 的每个画布操作都报“页面没有及时回应”。控制台:Cannot access 'marker' before initialization。

根因:看大图的模块 ui_gallery_viewer.js 为了 formatSize 去导入 ui_model_download.js,后者经设置页、browser.js、导航一路导回 ui_ai_workbench.js;而 ui_ai_images.js → ui_gallery_detail.js → ui_gallery_viewer.js 又在这条链的开头。于是 ui_ai_workbench.js 在模块顶层调用 setMarker 时,ui_ai_images.js 还没执行完。结构检查和测试都只看语法和单个模块,抓不到。8197 上看过的页面恰好没触发这个加载顺序。

以后怎么做

  • 底层模块(看图、卡片、纯函数)只从同样底层的模块取工具函数,不从“页面”模块(下载、设置、浏览器主对象)取,哪怕只是一个小函数;需要就挪到没有导入的小文件里(file_size.js)。
  • 改了导入关系,在真的 ComfyUI 页面上确认 window.anomalousBrowserInstance 存在、控制台没有 before initialization,再提交。

2026-11-05 · 偶尔打开 ComfyUI,AMB 整个不见了

现象:没有悬浮按钮,没有窗口,控制台里是几十条 Cannot access 'shared' before initialization;刷新一下又好了。大约十几次加载碰到一次。

原因:ComfyUI 把 web/ 下每个 .js 都当成扩展,同时各自导入一遍,谁先跑完全看网络。AMB 的模块有一个很大的循环导入,ui_ai_workbench.js 加载时就要写 ui_ai_results.js 里的 const shared。从 main.js 进来顺序是对的;碰巧先从 ui_ai_results.js 或 ui_ai_step_body.js 进来,shared 还没初始化,这个模块就报错,而且错误会被缓存,整个 AMB 跟着起不来。

做法:shared 搬进不导入任何东西的 ui_ai_shared.js。tests/module_cycles.mjs 查“加载时就读同一个循环里的 const”,这种写法以后过不了测试。

以后:会在加载时用到的值,放在循环外面的模块里。别只从 main.js 想加载顺序。要逐个模块验证,在浏览器里给每个模块开一个 iframe 单独 import。

2026-11-05 · 存档条和存档页的按钮第一下没反应

现象:在提示词框打完字直接点条上的 +,什么都没存,再点一下才存上;先点一下画布空白处就没事。改完次数直接点运行也一样。运行途中,正在输入的次数会被清掉。

根因:draw 每次都把整行换成新元素,而按下和松开之间元素离开了页面,浏览器就不发 click。这段时间里能触发重画的有三样:ComfyUI 到 window 的 mouseup 才发现文本框的改动并发 graphChanged(不是失焦时的 change,打字时它也不看);保存次数的请求十几毫秒就回来;运行结束的 status。

做法:ui_dom.js 的 settle 在已有元素上更新,一格只留给同一个存档,正在输入的框不动。图片行自己填内容,图没变就原样留着。元素留着还不够:新图排第一,整行右移,松手时鼠标下已经是另一张图,也不会有 click。所以按在存档页的按钮上时列表不动,这期间来的内容等松手、click 发完再画(holdStill)。

以后怎么做

  • 用户可能正按着或正输入的控件,不要在异步事件里整块重建。
  • 处理函数搬到留下的元素上后,要用事件上的元素,不能用闭包里新建的那个;存着元素的字段(part.line)也要重新取。第一版漏了这点,存下的次数全是 1,只查“点击到了”的测试还是全过:要查结果存对了。
  • 这类问题要用真鼠标、按住约 90 毫秒来测,element.click() 测不出来。

2026-11-04 · 存档跑出来全是画布上的内容

现象:两个存档跑 3 + 1 次,只出了一轮图。四次提交一模一样,第二个存档的改动和新种子都没进去,后三次全走缓存;运行前的检查查的也是画布。

根因:ComfyUI-Manager 把 app.graphToPrompt 包了一层,调用原函数时不带参数,传进去的离屏图被丢掉,ComfyUI 就用默认的画布。测试环境没装 Manager,所以当时没测出来。

以后怎么做

  • 往 ComfyUI 的公共方法传参数前,先想到别的插件可能包过它;能从结果核对的就核对(snapshot_plan.compiled 看返回的 workflow.id)。
  • 依赖 app、api 上这类方法的功能,验证时要带上 Manager 这样的常见包装;测试环境没有,就在页面上照它的写法包一层。