Compare commits

...
6 Commits
Author SHA1 Message Date
DemonGatanjieuandClaude Opus 5.5 a47e85ff90 v1.62.3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:33:43 +08:00
DemonGatanjieuandClaude Opus 5.5 6d1776d856 Tests no longer look like node tables to ComfyUI-Manager
Manager reads every .py file of a plugin, tests too, and listed ten names
from two test files as AMB's nodes, the only match for Power Lora Loader
among them, so a workflow missing it could offer AMB in place of rgthree.
The tables are written so its scanner passes them by, and a new test uses
the scanner's own patterns to keep any file from looking like one again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:33:42 +08:00
DemonGatanjieuandClaude Opus 5.5 e6a9c3be93 v1.62.2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:49:01 +08:00
DemonGatanjieuandClaude Opus 5.5 b34936a99f Fix workflow cards squeezed into strips
A card clips its content, so plain grid rows shrank to fit the list's height
instead of letting it scroll: with many workflows every card ended up 56 px
tall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:49:00 +08:00
DemonGatanjieuandClaude Opus 5.5 8a4c5d5c72 v1.62.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:52:28 +08:00
DemonGatanjieuandClaude Opus 5.5 b9fd30ad72 Fix Anomalous sometimes not starting
ComfyUI imports every script under web/ on its own and at once, so any module
of the big import cycle can run first. ui_ai_workbench.js wrote to a const of
ui_ai_results.js as it loaded, which failed when ui_ai_results.js or
ui_ai_step_body.js was entered first, and the whole plugin went with it until
a reload. The value now lives in a module that imports nothing, and
tests/module_cycles.mjs refuses the pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 01:52:28 +08:00
12 changed files with 159 additions and 18 deletions
+1 -1
View File
@@ -363,7 +363,7 @@ covered by the `styles.css` manifest.
task adds `links` {from, next}, begin_task gives an AI the task it continues and what was
decided there (`continues`). `ui_ai_task_links.js` shows them under the task's title and picks
one to link. `tests/test_ai_task_links.py`. `ui_ai_recent.js` the home page's list, `ui_ai_results.js` what a
step made and where it leads. `ui_ai_outputs.js` is what the whole task made: a line over the
step made and where it leads, `ui_ai_shared.js` what the page's parts share. `ui_ai_outputs.js` is what the whole task made: a line over the
timeline counting it, and the side column's Results view (`ai_task_view.taskOutputs`: the
pictures by the step that brought them, the workflow, the combos, the newest picture's prompts,
the models used and download requests — `ai_tasks.results_of` names the downloads — and the sounds),
+15
View File
@@ -1,5 +1,20 @@
# 📈 Anomalous Model Browser Changelog
## v1.62.3 (Fix: ComfyUI-Manager took other packs' nodes for AMB's) — 2026-11-05
- ComfyUI-Manager 不再把别的插件的节点算到 AMB 头上:AMB 没有节点,是测试文件里的假节点表让它误以为有,于是缺 Power Lora Loader 等节点时会推荐装 AMB。Manager 重新扫描后生效。AMB 本身用起来没有变化。
- ComfyUI-Manager no longer takes other packs' nodes for AMB's: AMB has no nodes, but node tables in its test files looked like some, so a missing Power Lora Loader and the like could offer AMB. Takes effect once Manager scans again. AMB itself works as before.
## v1.62.2 (Fix: workflow cards squeezed into strips) — 2026-11-05
- 工作流很多时,卡片不再被压成细条,列表正常滚动。
- With many workflows, the cards are no longer squeezed into thin strips; the list scrolls.
## v1.62.1 (Fix: Anomalous sometimes did not start) — 2026-11-05
- 修好一个偶发的启动故障:有时打开 ComfyUI,AMB 整个没加载,悬浮按钮和窗口都没有,刷新一下又好了。
- Fixed a startup failure that came now and then: ComfyUI opened with no Anomalous at all, no button and no window, until a reload.
## v1.62.0 (AI Workbench, AI Builds Workflows on the Canvas, Repaint an Area, Materials Side Panel) — 2026-10-29
- 音频库搬进了“角色语音”页:页面顶上的标签现在是 **角色 | 配音 | 音频库**,左边栏少了一项。加了 AI 工作台以后左边栏有 10 项,数字键只有 1–9,音频库就没键可按了;现在 9 打开角色语音,并回到你上次停的那个标签。
+1 -1
View File
@@ -9,7 +9,7 @@
[![ComfyUI Manager](https://img.shields.io/badge/ComfyUI-Manager-green?style=for-the-badge&logo=comfyui)](https://github.com/ltdrdata/ComfyUI-Manager)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](LICENSE)
[![Changelog](https://img.shields.io/badge/📖_Changelog-v1.62.0-blue?style=for-the-badge)](CHANGELOG.md)
[![Changelog](https://img.shields.io/badge/📖_Changelog-v1.62.3-blue?style=for-the-badge)](CHANGELOG.md)
[![Bilibili Video](https://img.shields.io/badge/Bilibili-视频演示-00A1D6?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV1boHx61Enc/)
[![Bilibili Voices](https://img.shields.io/badge/Bilibili-配音演示-00A1D6?style=for-the-badge&logo=bilibili)](https://www.bilibili.com/video/BV1bkpZ6uEZQ/)
[![Reddit](https://img.shields.io/badge/Reddit-r%2Fcomfyui-FF4500?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/comfyui/s/n38ihf3DLf)
+10
View File
@@ -123,3 +123,13 @@ body:has(#anomalous-toolbox-modal[style*="display: flex"]) #anomalous-sidebar-to
**以后怎么做**
- `node tools/check_structure.mjs` 现在会拒绝 `:has()` 里出现 `[style…]`,报出文件和行号。
- 同一个坑第二次出现,就把它做成检查,不再只补一段经验。
## 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。
+69
View File
@@ -0,0 +1,69 @@
// ComfyUI imports every .js under web/ on its own, all at once, so any module of an import cycle can happen
// to run first. A module that uses, as it loads, a `const` of a module in its own cycle then fails now and
// then, and AMB with it: no button, no window (docs/architecture/lessons.md). Such a value goes in a module
// outside the cycle.
import assert from 'node:assert/strict';
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const folder = join(dirname(fileURLToPath(import.meta.url)), '..', 'web', 'modules');
const IMPORT = /^import\s+\{([^}]*)\}\s+from\s+'\.\/([\w.]+\.js)'/gm;
const modules = new Map(readdirSync(folder).filter(name => name.endsWith('.js')).map((name) => {
const text = readFileSync(join(folder, name), 'utf8').replace(/\r\n/g, '\n');
const imports = [...text.matchAll(IMPORT)].map(([, names, from]) => ({ from, names: names.split(',').map(item => item.trim().split(/\s+as\s+/)).filter(([first]) => first) }));
return [name, { text, imports, to: new Set(imports.map(item => item.from)) }];
}));
/** The modules in cycles, each with the set of those in the same cycle (Tarjan, on the imports). */
function cycles() {
const index = new Map();
const low = new Map();
const stack = [];
const groups = new Map();
const visit = (name) => {
index.set(name, index.size);
low.set(name, index.get(name));
stack.push(name);
for (const next of modules.get(name)?.to || []) {
if (!modules.has(next)) continue;
if (!index.has(next)) {
visit(next);
low.set(name, Math.min(low.get(name), low.get(next)));
} else if (stack.includes(next)) low.set(name, Math.min(low.get(name), index.get(next)));
}
if (low.get(name) !== index.get(name)) return;
const group = new Set();
let member;
do {
member = stack.pop();
group.add(member);
} while (member !== name);
for (const item of group) groups.set(item, group);
};
for (const name of modules.keys()) if (!index.has(name)) visit(name);
return groups;
}
/** Is `local` used by a line at the top of `text`, that is one run as the module loads? */
function usedAsItLoads(text, local) {
return text.split('\n').some(line => /^[\w(]/.test(line)
&& !/^(import|function|async function|export (async )?function|export default function|class)\b/.test(line)
&& new RegExp(`\\b${local}\\b`).test(line.split('=>')[0]));
}
const groups = cycles();
const found = [];
for (const [name, { text, imports }] of modules) {
for (const { from, names } of imports) {
if (!groups.get(name)?.has(from) || from === name) continue;
const source = modules.get(from).text;
for (const [exported, local = exported] of names) {
const isValue = new RegExp(`^export (const|let) ${exported}\\b`, 'm').test(source);
if (isValue && usedAsItLoads(text, local)) found.push(`${name} uses ${exported} of ${from} as it loads`);
}
}
}
assert.deepEqual(found, []);
console.log('Module cycles: nothing reads a value of its own cycle as it loads.');
+37
View File
@@ -0,0 +1,37 @@
"""ComfyUI-Manager reads every .py file of a plugin, tests too, and lists whatever looks like a node
table as that plugin's nodes. AMB has none (AD-024), so no file may look like one: a table in a test once
made Manager offer AMB to people who missed Power Lora Loader, in place of rgthree."""
import re
import unittest
from pathlib import Path
PLUGIN = Path(__file__).resolve().parents[1]
# As ComfyUI-Manager's scanner.py (scan_in_file) has them.
TABLE = re.compile(r"_CLASS_MAPPINGS\s*(?::\s*\w+\s*)?=\s*(?:\\\s*)?{([^}]*)}", re.MULTILINE | re.DOTALL)
OTHERS = (r'^[^=]*_CLASS_MAPPINGS\["(.*?)"\]', r"^[^=]*_CLASS_MAPPINGS\['(.*?)'\]",
r'@register_node\("(.+)",\s*\".+"\)', r'"(\w+)"\s*:\s*{"class":\s*\w+\s*')
def claims(code):
"""What the scanner would take for nodes in `code`: the tables with something in them, and the other forms."""
found = [table.strip() for table in TABLE.findall(code) if table.strip()]
return found + [name for pattern in OTHERS for name in re.findall(pattern, code)]
class ManagerScanTests(unittest.TestCase):
def test_the_check_sees_a_table(self):
name = "NODE_CLASS_" + "MAPPINGS" # in two, or this file would be the one to look like a table
self.assertTrue(claims(name + ' = {"Some Node": object}'))
self.assertFalse(claims(name + " = {}"))
def test_no_file_looks_like_a_node_table(self):
for path in PLUGIN.rglob("*.py"):
parts = path.relative_to(PLUGIN).parts
if "__pycache__" in parts or any(part.startswith(".") for part in parts):
continue # the scanner leaves these out too
self.assertEqual(claims(path.read_text(encoding="utf-8", errors="ignore")), [], path)
if __name__ == "__main__":
unittest.main()
+12 -11
View File
@@ -65,18 +65,19 @@ class UsageTests(unittest.TestCase):
{"type": "loras", "root": 0, "rel": "never.safetensors", "size": 9},
{"type": "vae", "root": 0, "rel": "hashed.safetensors", "size": 1},
]
registered = { # by this name, not as the keyword's value: tests/test_manager_scan.py says why
"KSampler": node("nodes", ["MODEL"], ["LATENT"]),
"Power Lora Loader (rgthree)": node("custom_nodes.rgthree-comfy", ["MODEL"], ["MODEL"]),
"easy loraStack": node("custom_nodes.ComfyUI-Easy-Use"),
"easy seed": node("custom_nodes.ComfyUI-Easy-Use.py.nodes"),
"Unused Thing": node("custom_nodes.forgotten-pack"),
"DetailerForEach": node("custom_nodes.ComfyUI-Impact-Pack", ["IMAGE", "BBOX_DETECTOR"], ["IMAGE"]),
"UltralyticsDetectorProvider": node("custom_nodes.comfyui-impact-subpack", [], ["BBOX_DETECTOR"]),
"Genie Speak": node("custom_nodes.Genie-TTS", ["STRING"], ["AUDIO"]),
"Manager Thing": node("custom_nodes.ComfyUI-Manager"),
}
nodes = types.SimpleNamespace(
NODE_CLASS_MAPPINGS={
"KSampler": node("nodes", ["MODEL"], ["LATENT"]),
"Power Lora Loader (rgthree)": node("custom_nodes.rgthree-comfy", ["MODEL"], ["MODEL"]),
"easy loraStack": node("custom_nodes.ComfyUI-Easy-Use"),
"easy seed": node("custom_nodes.ComfyUI-Easy-Use.py.nodes"),
"Unused Thing": node("custom_nodes.forgotten-pack"),
"DetailerForEach": node("custom_nodes.ComfyUI-Impact-Pack", ["IMAGE", "BBOX_DETECTOR"], ["IMAGE"]),
"UltralyticsDetectorProvider": node("custom_nodes.comfyui-impact-subpack", [], ["BBOX_DETECTOR"]),
"Genie Speak": node("custom_nodes.Genie-TTS", ["STRING"], ["AUDIO"]),
"Manager Thing": node("custom_nodes.ComfyUI-Manager"),
},
NODE_CLASS_MAPPINGS=registered,
LOADED_MODULE_DIRS={name: str(custom / name) for name in packs},
EXTENSION_WEB_DIRS={"rgthree-comfy": str(custom / "rgthree-comfy" / "js"), "page-only": str(custom / "page-only" / "web")},
)
+3 -2
View File
@@ -11,7 +11,8 @@ PLUGIN = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(PLUGIN), str(PLUGIN.parents[1])]
from api import tts_version, version_manager as vm
NODES = 'NODE_CLASS_MAPPINGS = {"AnomalousTTS_CharacterSpeech": object}\n'
MAPPINGS = 'NODE_CLASS_' + 'MAPPINGS' # in two: tests/test_manager_scan.py says why
NODES = MAPPINGS + ' = {"AnomalousTTS_CharacterSpeech": object}\n'
def git(cwd, *args):
@@ -51,7 +52,7 @@ class TtsVersionTests(unittest.TestCase):
def test_only_the_official_clone_is_found(self):
self.assertIsNone(tts_version.find_checkout())
self.clone('A_fork', 'https://github.com/someone/Anomalous_TTS.git')
self.clone('Other_pack', 'https://github.com/DemonGatanjieu/Anomalous_TTS', nodes='NODE_CLASS_MAPPINGS = {}\n')
self.clone('Other_pack', 'https://github.com/DemonGatanjieu/Anomalous_TTS', nodes=MAPPINGS + ' = {}\n')
(self.nodes / 'Plain_copy').mkdir()
Path(self.nodes / 'Plain_copy' / 'nodes.py').write_text(NODES, encoding='utf-8') # no git: Manager's zip, a copy
self.assertIsNone(tts_version.find_checkout())
+1 -2
View File
@@ -22,8 +22,7 @@ import { canvasChanges } from './ui_canvas_changes.js';
import { estimateBox, runProgress } from './ui_ai_run_live.js';
import { runMapBox } from './ui_ai_run_map.js';
import { iconButton } from './ui_ai_icons.js';
export const shared = { owner: null, taskId: null, entries: new Map(), player: null, openMap: null }; // openMap: ui_ai_workbench.js
import { shared } from './ui_ai_shared.js';
/** A run step's progress with its stop button (while the task shown is the run's). */
const liveRun = progress => runProgress(progress, shared.taskId ? { task: shared.taskId, step: progress.step } : null);
+7
View File
@@ -0,0 +1,7 @@
/**
* What the parts of the AI page share: AMB's window, the task shown, its canvas entries, the player and
* the way to a run's map. It imports nothing, so it stands before any of them runs: ui_ai_workbench.js
* writes here as it loads, and ComfyUI may run any of AMB's modules first (tests/module_cycles.mjs).
*/
export const shared = { owner: null, taskId: null, entries: new Map(), player: null, openMap: null };
+2 -1
View File
@@ -44,7 +44,8 @@ import { iconButton } from './ui_ai_icons.js';
import { listToggle, renderTaskList, stateIcon } from './ui_ai_task_list.js';
import { flowMap } from './ui_ai_flow.js';
import { drawRecentAiTasks } from './ui_ai_recent.js';
import { locateNode, openModel, resultLinks, shared, updateRunProgress } from './ui_ai_results.js';
import { locateNode, openModel, resultLinks, updateRunProgress } from './ui_ai_results.js';
import { shared } from './ui_ai_shared.js';
import { drawOutputsLine, newOutputs } from './ui_ai_outputs.js';
import { compareBar, setMarker } from './ui_ai_images.js';
import { openMarker } from './ui_ai_marker.js';
+1
View File
@@ -94,6 +94,7 @@
.anomalous-wf-list.is-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-auto-rows: max-content; /* a card clips its content, so plain rows would shrink to fit instead of scrolling */
align-content: start;
gap: 14px;
}