dsh-comfyui 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/analyze.js +8 -4
- package/lib/convert.js +5 -3
- package/lib/graph.d.ts +29 -0
- package/lib/graph.js +99 -0
- package/lib/skill.js +3 -0
- package/package.json +1 -1
package/lib/analyze.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* graph links (with bypassed and dangling nodes excluded). The analysis feeds
|
|
6
6
|
* the extract (拆分) choices in the panel and the agent-facing skill.
|
|
7
7
|
*/
|
|
8
|
-
import { normalizeLinks } from './graph.js';
|
|
8
|
+
import { isVirtualNodeType, normalizeLinks, resolveVirtualLinks } from './graph.js';
|
|
9
9
|
function isObject(value) {
|
|
10
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
11
|
}
|
|
@@ -28,11 +28,15 @@ export function analyzeGraph(graph) {
|
|
|
28
28
|
return { ok: false, error: '无法解析图文件(缺少 nodes/links)' };
|
|
29
29
|
}
|
|
30
30
|
const nodes = graph.nodes;
|
|
31
|
-
|
|
31
|
+
// Virtual nodes (Set/Get wireless links, rgthree mode relays) are rewired
|
|
32
|
+
// out first, exactly as conversion does, so components follow the real data
|
|
33
|
+
// flow and the virtual nodes themselves never show up as components or as
|
|
34
|
+
// dangling nodes (Issue #8).
|
|
35
|
+
const links = resolveVirtualLinks(graph.nodes, normalizeLinks(graph.links));
|
|
32
36
|
const groups = Array.isArray(graph.groups) ? graph.groups : [];
|
|
33
|
-
const active = nodes.filter((node) => node.mode !== 4);
|
|
37
|
+
const active = nodes.filter((node) => node.mode !== 4 && !isVirtualNodeType(node.type));
|
|
34
38
|
const activeById = new Map(active.map((node) => [node.id, node]));
|
|
35
|
-
const bypassedCount = nodes.
|
|
39
|
+
const bypassedCount = nodes.filter((node) => node.mode === 4).length;
|
|
36
40
|
// Dangling nodes: active but touching no link at all.
|
|
37
41
|
const linkedIds = new Set();
|
|
38
42
|
for (const link of links) {
|
package/lib/convert.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Reroute / bypassed (mode 4) nodes are skipped with their links rewired.
|
|
8
8
|
* Nodes the conversion cannot represent fail loudly with the offending type.
|
|
9
9
|
*/
|
|
10
|
-
import { normalizeLinks } from './graph.js';
|
|
10
|
+
import { isVirtualNodeType, normalizeLinks, resolveVirtualLinks } from './graph.js';
|
|
11
11
|
/** Node types that exist only in the UI and carry no data flow. */
|
|
12
12
|
const UI_ONLY = new Set(['Note', 'StickyNote', 'Reroute', 'Fast Groups Bypasser (rgthree)']);
|
|
13
13
|
function isObject(value) {
|
|
@@ -253,7 +253,9 @@ export function convertGraphToApi(graph, objectInfo, options) {
|
|
|
253
253
|
// normalizeLinks accepts both the legacy positional rows and the v0.4
|
|
254
254
|
// frontend's object entries; anything unreadable is skipped, exactly like
|
|
255
255
|
// the old array-only guard did.
|
|
256
|
-
|
|
256
|
+
// Set/Get wireless links and rgthree mode relays are rewired out before
|
|
257
|
+
// anything resolves a link (Issue #8), shared with analyzeGraph.
|
|
258
|
+
for (const link of resolveVirtualLinks(rawNodes, normalizeLinks(rawLinks)))
|
|
257
259
|
links.set(link[0], link);
|
|
258
260
|
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
259
261
|
const included = options?.includeNodeIds;
|
|
@@ -272,7 +274,7 @@ export function convertGraphToApi(graph, objectInfo, options) {
|
|
|
272
274
|
const brokenInputs = new Map();
|
|
273
275
|
const dropReasons = new Map();
|
|
274
276
|
for (const node of candidates) {
|
|
275
|
-
if (node.type === '' || UI_ONLY.has(node.type))
|
|
277
|
+
if (node.type === '' || UI_ONLY.has(node.type) || isVirtualNodeType(node.type))
|
|
276
278
|
continue;
|
|
277
279
|
if (node.mode === 4)
|
|
278
280
|
continue;
|
package/lib/graph.d.ts
CHANGED
|
@@ -13,3 +13,32 @@ export type GraphLink = [number, number, number, number, number, string];
|
|
|
13
13
|
export declare function normalizeLink(raw: unknown): GraphLink | undefined;
|
|
14
14
|
/** Normalize a whole `links` array, skipping entries that are not readable. */
|
|
15
15
|
export declare function normalizeLinks(raw: unknown): GraphLink[];
|
|
16
|
+
/**
|
|
17
|
+
* Frontend-only nodes whose links are rewired or dropped before analysis and
|
|
18
|
+
* conversion (Issue #8). None of them exists on the server.
|
|
19
|
+
*
|
|
20
|
+
* - KJNodes `SetNode` / `GetNode`: "wireless" links paired by name. A GetNode's
|
|
21
|
+
* output is the value wired into the SetNode of the same name (and a
|
|
22
|
+
* SetNode's own passthrough output is its input).
|
|
23
|
+
* - rgthree `Mute / Bypass Relay` / `Repeater`: their `OPT_CONNECTION` links
|
|
24
|
+
* only propagate mute/bypass mode between nodes and carry no data. The mode
|
|
25
|
+
* they set is already saved on each target node, so the links are dropped.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SET_NODE = "SetNode";
|
|
28
|
+
export declare const GET_NODE = "GetNode";
|
|
29
|
+
/** Whether a node type is one of the virtual nodes {@link resolveVirtualLinks} removes. */
|
|
30
|
+
export declare function isVirtualNodeType(type: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Rewire the virtual nodes out of a graph's links, keeping link ids stable so
|
|
33
|
+
* node `inputs[].link` references stay valid.
|
|
34
|
+
*
|
|
35
|
+
* A GetNode resolves to the SetNode of the same name with the greatest `order`
|
|
36
|
+
* below its own (the scope rule the frontend applies), falling back to any
|
|
37
|
+
* SetNode of that name. A GetNode without a matching, wired SetNode (e.g. an
|
|
38
|
+
* empty optional slot) resolves to nothing, so its consumer stays unconnected.
|
|
39
|
+
* @param rawNodes - the graph's `nodes` array as saved.
|
|
40
|
+
* @param links - normalized links.
|
|
41
|
+
* @returns links with virtual-node hops replaced by their real origins, and
|
|
42
|
+
* every link touching a virtual node otherwise removed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveVirtualLinks(rawNodes: unknown, links: GraphLink[]): GraphLink[];
|
package/lib/graph.js
CHANGED
|
@@ -46,3 +46,102 @@ export function normalizeLinks(raw) {
|
|
|
46
46
|
}
|
|
47
47
|
return links;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Frontend-only nodes whose links are rewired or dropped before analysis and
|
|
51
|
+
* conversion (Issue #8). None of them exists on the server.
|
|
52
|
+
*
|
|
53
|
+
* - KJNodes `SetNode` / `GetNode`: "wireless" links paired by name. A GetNode's
|
|
54
|
+
* output is the value wired into the SetNode of the same name (and a
|
|
55
|
+
* SetNode's own passthrough output is its input).
|
|
56
|
+
* - rgthree `Mute / Bypass Relay` / `Repeater`: their `OPT_CONNECTION` links
|
|
57
|
+
* only propagate mute/bypass mode between nodes and carry no data. The mode
|
|
58
|
+
* they set is already saved on each target node, so the links are dropped.
|
|
59
|
+
*/
|
|
60
|
+
export const SET_NODE = 'SetNode';
|
|
61
|
+
export const GET_NODE = 'GetNode';
|
|
62
|
+
const MODE_LINK_NODES = new Set(['Mute / Bypass Relay (rgthree)', 'Mute / Bypass Repeater (rgthree)']);
|
|
63
|
+
/** Whether a node type is one of the virtual nodes {@link resolveVirtualLinks} removes. */
|
|
64
|
+
export function isVirtualNodeType(type) {
|
|
65
|
+
return type === SET_NODE || type === GET_NODE || MODE_LINK_NODES.has(type);
|
|
66
|
+
}
|
|
67
|
+
function virtualNodeFacts(raw) {
|
|
68
|
+
if (!isObject(raw))
|
|
69
|
+
return undefined;
|
|
70
|
+
const id = typeof raw.id === 'number' ? raw.id : Number(raw.id);
|
|
71
|
+
if (!Number.isFinite(id))
|
|
72
|
+
return undefined;
|
|
73
|
+
const values = Array.isArray(raw.widgets_values) ? raw.widgets_values : [];
|
|
74
|
+
const name = typeof values[0] === 'string' && values[0].trim() !== '' ? values[0] : undefined;
|
|
75
|
+
const inputs = Array.isArray(raw.inputs) ? raw.inputs : [];
|
|
76
|
+
const wired = inputs.find((entry) => isObject(entry) && typeof entry.link === 'number');
|
|
77
|
+
return {
|
|
78
|
+
id,
|
|
79
|
+
type: typeof raw.type === 'string' ? raw.type : '',
|
|
80
|
+
order: typeof raw.order === 'number' ? raw.order : Number.MAX_SAFE_INTEGER,
|
|
81
|
+
name,
|
|
82
|
+
firstInputLink: wired?.link,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Rewire the virtual nodes out of a graph's links, keeping link ids stable so
|
|
87
|
+
* node `inputs[].link` references stay valid.
|
|
88
|
+
*
|
|
89
|
+
* A GetNode resolves to the SetNode of the same name with the greatest `order`
|
|
90
|
+
* below its own (the scope rule the frontend applies), falling back to any
|
|
91
|
+
* SetNode of that name. A GetNode without a matching, wired SetNode (e.g. an
|
|
92
|
+
* empty optional slot) resolves to nothing, so its consumer stays unconnected.
|
|
93
|
+
* @param rawNodes - the graph's `nodes` array as saved.
|
|
94
|
+
* @param links - normalized links.
|
|
95
|
+
* @returns links with virtual-node hops replaced by their real origins, and
|
|
96
|
+
* every link touching a virtual node otherwise removed.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveVirtualLinks(rawNodes, links) {
|
|
99
|
+
const facts = new Map();
|
|
100
|
+
for (const raw of Array.isArray(rawNodes) ? rawNodes : []) {
|
|
101
|
+
const node = virtualNodeFacts(raw);
|
|
102
|
+
if (node !== undefined && isVirtualNodeType(node.type))
|
|
103
|
+
facts.set(node.id, node);
|
|
104
|
+
}
|
|
105
|
+
if (facts.size === 0)
|
|
106
|
+
return links;
|
|
107
|
+
const linkById = new Map(links.map((link) => [link[0], link]));
|
|
108
|
+
const setters = new Map();
|
|
109
|
+
for (const node of facts.values()) {
|
|
110
|
+
if (node.type !== SET_NODE || node.name === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
const list = setters.get(node.name) ?? [];
|
|
113
|
+
list.push(node);
|
|
114
|
+
setters.set(node.name, list);
|
|
115
|
+
}
|
|
116
|
+
const setterFor = (getter) => {
|
|
117
|
+
const candidates = getter.name === undefined ? [] : setters.get(getter.name) ?? [];
|
|
118
|
+
const before = candidates.filter((node) => node.order < getter.order);
|
|
119
|
+
const pool = before.length > 0 ? before : candidates;
|
|
120
|
+
return pool.reduce((best, node) => (best === undefined || node.order > best.order ? node : best), undefined);
|
|
121
|
+
};
|
|
122
|
+
/** The real [originId, originSlot] behind an origin, or undefined when it resolves to nothing. */
|
|
123
|
+
const realOrigin = (originId, originSlot, seen) => {
|
|
124
|
+
const node = facts.get(originId);
|
|
125
|
+
if (node === undefined)
|
|
126
|
+
return [originId, originSlot];
|
|
127
|
+
if (seen.has(originId))
|
|
128
|
+
return undefined;
|
|
129
|
+
seen.add(originId);
|
|
130
|
+
if (MODE_LINK_NODES.has(node.type))
|
|
131
|
+
return undefined;
|
|
132
|
+
const source = node.type === GET_NODE ? setterFor(node) : node;
|
|
133
|
+
const upstream = source?.firstInputLink === undefined ? undefined : linkById.get(source.firstInputLink);
|
|
134
|
+
return upstream === undefined ? undefined : realOrigin(upstream[1], upstream[2], seen);
|
|
135
|
+
};
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const link of links) {
|
|
138
|
+
// Links INTO a virtual node only feed the resolution above.
|
|
139
|
+
if (facts.has(link[3]))
|
|
140
|
+
continue;
|
|
141
|
+
const origin = realOrigin(link[1], link[2], new Set());
|
|
142
|
+
if (origin === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
out.push(origin[0] === link[1] && origin[1] === link[2] ? link : [link[0], origin[0], origin[1], link[3], link[4], link[5]]);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
package/lib/skill.js
CHANGED
|
@@ -66,6 +66,9 @@ TTS-Audio-Suite(\`{comfyuiDir}/custom_nodes/tts_audio_suite\`)的"🎭 Chara
|
|
|
66
66
|
- **\`control_after_generate\` 占位**:名为 \`seed\`/\`noise_seed\` 的 INT(或 object_info 显式声明 \`control_after_generate\`)前面端会渲染生成后控制下拉,其值占据 widgets_values 一个槽位但无 API 输入——提取时消费该值但不写入工作流,否则后续 widget 全部错位(如 TTS-Audio-Suite 的 seed 后跟 "fixed")。
|
|
67
67
|
- **对象型 widgets_values**(如 VHS_VideoCombine)按名取值,跳过内部状态(\`videopreview\` 等带 \`hidden\` 的对象)。
|
|
68
68
|
- **Reroute 与 bypass(mode 4)节点直通**:输出跟随其第一条有连线的输入。
|
|
69
|
+
- **虚拟节点先改线再转换**(分析与转换共用同一规则):KJNodes \`SetNode\`/\`GetNode\` 按名称配对——\`GetNode\` 的输出接到同名 \`SetNode\` 的上游(同名多个时取 \`order\` 小于它的最大者),找不到对应 \`SetNode\` 的消费端保持未连接;rgthree \`Mute / Bypass Relay\`/\`Repeater\` 的连线只传播静音/绕过状态、不传数据,直接丢弃(状态已保存在各节点的 \`mode\` 里)。
|
|
70
|
+
- **DynamicCombo V3 保持扁平**:主输入的值是所选 option key 字符串,子控件以 \`主名.子名\` 平铺(如 \`codec: "auto"\`),不要包成 \`{ key, inputs }\`——服务端会自己重组,包起来反而匹配不上,节点在 execute 时报缺参数。
|
|
71
|
+
- **子图(subgraph)实例不支持**:遇到请让用户在 ComfyUI 里把子图转换回普通节点后再提取。
|
|
69
72
|
- **未注册的 UI-only 节点**:若是 \`Primitive*\` 内联其第一个 widget 值;有输出被使用且非 Primitive → 报错。
|
|
70
73
|
- **输出槽位越界**(保存图里 SaveImage 声称 2 个输出但服务端只有 1 个)→ 断开该引用并给警告。
|
|
71
74
|
- **必需输入缺失**(required 里没有、且非 lazy/template 类型)→ 明确报错(源图本身断线),不产出"能转但跑不起来"的工作流。\`COMFY_AUTOGROW_V3\`/带 \`template\`/带 \`lazy\` 的输入豁免。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Let the DeepSeek Harness agent smartly drive a local or remote ComfyUI to generate anything, with workflow and asset management panels, per-workflow skill packs, a companion skill and a same-origin media proxy. / 让 DeepSeek Harness 的 Agent 智能驱动本地或远程 ComfyUI 生成任何内容。附带工作流、资产管理面板与技能包管理挂载。配套 skill 与同源媒体代理。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|