Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a47e85ff90 | ||
|
|
6d1776d856 | ||
|
|
e6a9c3be93 | ||
|
|
b34936a99f | ||
|
|
8a4c5d5c72 | ||
|
|
b9fd30ad72 |
+1
-1
@@ -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),
|
||||
|
||||
@@ -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 打开角色语音,并回到你上次停的那个标签。
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
[](https://github.com/ltdrdata/ComfyUI-Manager)
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://www.bilibili.com/video/BV1boHx61Enc/)
|
||||
[](https://www.bilibili.com/video/BV1bkpZ6uEZQ/)
|
||||
[](https://www.reddit.com/r/comfyui/s/n38ihf3DLf)
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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.');
|
||||
@@ -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
@@ -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")},
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user