dsh-comfyui-canvas 0.1.10 → 0.1.11
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/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> [中文](README.zh.md) · English
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
|
|
7
7
|
[](https://github.com/wbin0001/dsh-comfyui-canvas)
|
|
8
8
|
[](https://github.com/DeepSeek-Harness/DSH)
|
|
9
9
|
[](https://github.com/comfyanonymous/ComfyUI)
|
|
@@ -183,6 +183,18 @@ The built-in `comfyui-canvas-ops` skill instructs the agent to pick the right op
|
|
|
183
183
|
|
|
184
184
|
The agent decides the path up front: visual canvas mode when the user is watching / tuning, API/MCP mode for fast validation, batch sweeps and unattended runs.
|
|
185
185
|
|
|
186
|
+
### Workflow groups (organize complex pipelines)
|
|
187
|
+
|
|
188
|
+
`comfyui_group` organizes a complex canvas into **named groups** that survive save/load:
|
|
189
|
+
|
|
190
|
+
| Action | Behavior |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `add` | Create a named group around the given nodes; the box auto-fits the nodes (an explicit `bounding` is honored when given) |
|
|
193
|
+
| `add_nodes` / `remove_nodes` | Add / remove member nodes; the box re-fits automatically |
|
|
194
|
+
| `rename` / `list` | Rename a group / list groups with their members |
|
|
195
|
+
|
|
196
|
+
**Membership persistence**: saving the workflow serializes each group's member ids (`nodes`) next to `title` / `bounding` / `color`, and loading re-attaches them — a saved workflow reopens with the same boxes *and* their memberships intact.
|
|
197
|
+
|
|
186
198
|
## Requirements
|
|
187
199
|
|
|
188
200
|
- DeepSeek Harness Web (DSH), Node `^22.19.0 || >=24`
|
package/README.zh.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> 中文 · [English](README.md)
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
|
|
7
7
|
[](https://github.com/wbin0001/dsh-comfyui-canvas)
|
|
8
8
|
[](https://github.com/DeepSeek-Harness/DSH)
|
|
9
9
|
[](https://github.com/comfyanonymous/ComfyUI)
|
|
@@ -196,6 +196,18 @@ pip install comfy-cli # 独立 CLI,非 DSH 插件 —— 见 https://github.
|
|
|
196
196
|
|
|
197
197
|
agent 动手前先判断场景:用户在实时看画布 / 需要调优 → 画布路径;快速验证、批量扫参、无人值守 → API/MCP 路径。
|
|
198
198
|
|
|
199
|
+
### 工作流分组(组织复杂流水线)
|
|
200
|
+
|
|
201
|
+
`comfyui_group` 把复杂画布分成**具名组**,且分组随工作流保存/重载完整恢复:
|
|
202
|
+
|
|
203
|
+
| 命令 | 行为 |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `add` | 围绕指定节点创建具名组,组框**自动贴合**节点(也可传 `bounding` 精确定位) |
|
|
206
|
+
| `add_nodes` / `remove_nodes` | 增删组成员,组框自动重新贴合 |
|
|
207
|
+
| `rename` / `list` | 重命名 / 列出组及成员 |
|
|
208
|
+
|
|
209
|
+
**成员持久化**:保存工作流时把每个组的成员 id(`nodes`)与 `title` / `bounding` / `color` 一并写入 JSON,加载时自动重建成员关联——重开工作流后,组框和成员关系都原样保留。
|
|
210
|
+
|
|
199
211
|
---
|
|
200
212
|
|
|
201
213
|
## 环境要求
|
|
@@ -99,6 +99,36 @@ function applyWidgetValue(node, widget, value) {
|
|
|
99
99
|
node.setDirtyCanvas?.(true, true);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// v0.1.8: some LiteGraph builds ship LGraphGroup without addNode /
|
|
103
|
+
// computeBounding, so the bridge owns group membership and the bounding box
|
|
104
|
+
// instead of relying on those methods. `bounding` stays pos.concat(size), so
|
|
105
|
+
// saved workflows render the same box on load.
|
|
106
|
+
function groupBounds(nodeObjs, pad = 20) {
|
|
107
|
+
if (!nodeObjs.length) return null;
|
|
108
|
+
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
|
109
|
+
for (const n of nodeObjs) {
|
|
110
|
+
const [x, y] = n.pos;
|
|
111
|
+
const [w, h] = n.size;
|
|
112
|
+
x1 = Math.min(x1, x);
|
|
113
|
+
y1 = Math.min(y1, y);
|
|
114
|
+
x2 = Math.max(x2, x + w);
|
|
115
|
+
y2 = Math.max(y2, y + h);
|
|
116
|
+
}
|
|
117
|
+
return [x1 - pad, y1 - pad, x2 - x1 + 2 * pad, y2 - y1 + 2 * pad];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function groupSetNodes(group, nodeObjs, keepBounds = false) {
|
|
121
|
+
// LGraphGroup.nodes is a read-only getter in some frontends, so membership
|
|
122
|
+
// lives on a bridge-owned property instead of the group's own array.
|
|
123
|
+
group._dshNodes = Array.isArray(nodeObjs) ? nodeObjs.slice() : [];
|
|
124
|
+
if (keepBounds) return;
|
|
125
|
+
const bb = groupBounds(group._dshNodes);
|
|
126
|
+
if (bb) {
|
|
127
|
+
group.pos = [bb[0], bb[1]];
|
|
128
|
+
group.size = [bb[2], bb[3]];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
102
132
|
async function executeCommand(cmd, payload) {
|
|
103
133
|
const graph = app.graph;
|
|
104
134
|
const LiteGraph = await resolveLiteGraph();
|
|
@@ -331,10 +361,11 @@ async function executeCommand(cmd, payload) {
|
|
|
331
361
|
return { cleared: true, nodes: 0 };
|
|
332
362
|
}
|
|
333
363
|
|
|
334
|
-
// v0.1.
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
364
|
+
// v0.1.8: group nodes into named groups (complex-workflow organization).
|
|
365
|
+
// The bridge manages membership and the bounding box itself (groupSetNodes)
|
|
366
|
+
// because not every LiteGraph build ships LGraphGroup.addNode. Availability
|
|
367
|
+
// varies across custom frontends, so `list` reports groupingAvailable so
|
|
368
|
+
// the agent can degrade gracefully instead of hard-failing.
|
|
338
369
|
case "group": {
|
|
339
370
|
const { action, title, id, nodeIds } = payload ?? {};
|
|
340
371
|
const groups = graph?._groups ?? null;
|
|
@@ -349,10 +380,14 @@ async function executeCommand(cmd, payload) {
|
|
|
349
380
|
if (!title || typeof title !== "string") throw new Error("group add requires payload.title");
|
|
350
381
|
const nodeObjs = (Array.isArray(nodeIds) ? nodeIds : []).map((n) => graph.getNodeById(+n)).filter(Boolean);
|
|
351
382
|
const group = new LiteGraph.LGraphGroup(title);
|
|
352
|
-
|
|
383
|
+
if (Array.isArray(payload.bounding)) {
|
|
384
|
+
group.pos = [payload.bounding[0], payload.bounding[1]];
|
|
385
|
+
group.size = [payload.bounding[2], payload.bounding[3]];
|
|
386
|
+
}
|
|
387
|
+
groupSetNodes(group, nodeObjs, Array.isArray(payload.bounding));
|
|
353
388
|
groups.push(group);
|
|
354
389
|
scheduleReport();
|
|
355
|
-
return { groupingAvailable: true, id: groups.indexOf(group), title: group.title, nodes:
|
|
390
|
+
return { groupingAvailable: true, id: groups.indexOf(group), title: group.title, nodes: group._dshNodes.map((n) => String(n.id)) };
|
|
356
391
|
}
|
|
357
392
|
case "rename": {
|
|
358
393
|
const g = groups[+id];
|
|
@@ -364,22 +399,32 @@ async function executeCommand(cmd, payload) {
|
|
|
364
399
|
case "add_nodes": {
|
|
365
400
|
const g = groups[+id];
|
|
366
401
|
if (!g) throw new Error(`group not found: ${id}`);
|
|
402
|
+
const seen = new Set((g._dshNodes ?? []).map((n) => n.id));
|
|
403
|
+
const merged = (g._dshNodes ?? []).slice();
|
|
367
404
|
const added = [];
|
|
368
405
|
for (const n of Array.isArray(nodeIds) ? nodeIds : []) {
|
|
369
406
|
const node = graph.getNodeById(+n);
|
|
370
|
-
if (
|
|
407
|
+
if (!node || seen.has(node.id)) continue;
|
|
408
|
+
seen.add(node.id);
|
|
409
|
+
merged.push(node);
|
|
410
|
+
added.push(String(n));
|
|
371
411
|
}
|
|
412
|
+
groupSetNodes(g, merged);
|
|
372
413
|
scheduleReport();
|
|
373
414
|
return { groupingAvailable: true, id: +id, added };
|
|
374
415
|
}
|
|
375
416
|
case "remove_nodes": {
|
|
376
417
|
const g = groups[+id];
|
|
377
418
|
if (!g) throw new Error(`group not found: ${id}`);
|
|
419
|
+
const ids = new Set((Array.isArray(nodeIds) ? nodeIds : []).map((n) => String(n)));
|
|
378
420
|
const removed = [];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
421
|
+
groupSetNodes(g, (g._dshNodes ?? []).filter((n) => {
|
|
422
|
+
if (ids.has(String(n.id))) {
|
|
423
|
+
removed.push(String(n.id));
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}));
|
|
383
428
|
scheduleReport();
|
|
384
429
|
return { groupingAvailable: true, id: +id, removed };
|
|
385
430
|
}
|
|
@@ -389,7 +434,7 @@ async function executeCommand(cmd, payload) {
|
|
|
389
434
|
groups: groups.map((g, i) => ({
|
|
390
435
|
id: i,
|
|
391
436
|
title: g.title ?? "",
|
|
392
|
-
nodes: (g.
|
|
437
|
+
nodes: (g._dshNodes ?? []).map((n) => String(n.id)),
|
|
393
438
|
})),
|
|
394
439
|
};
|
|
395
440
|
}
|
|
@@ -449,10 +494,40 @@ app.registerExtension({
|
|
|
449
494
|
graph.configure = function (...args) {
|
|
450
495
|
const out = prevGraphConfigure.apply(this, args);
|
|
451
496
|
scheduleReport();
|
|
497
|
+
// v0.1.11: re-attach persisted group membership after a graph load so
|
|
498
|
+
// saved workflows restore which nodes belong to each group.
|
|
499
|
+
const data = args[0];
|
|
500
|
+
const groupsData = Array.isArray(data?.groups) ? data.groups : null;
|
|
501
|
+
const groups = this?._groups ?? null;
|
|
502
|
+
if (groupsData && Array.isArray(groups)) {
|
|
503
|
+
for (let i = 0; i < groups.length && i < groupsData.length; i++) {
|
|
504
|
+
const ids = groupsData[i]?.nodes;
|
|
505
|
+
if (!Array.isArray(ids)) continue;
|
|
506
|
+
const nodeObjs = ids.map((id) => this.getNodeById(+id)).filter(Boolean);
|
|
507
|
+
groupSetNodes(groups[i], nodeObjs, true);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
452
510
|
return out;
|
|
453
511
|
};
|
|
454
512
|
}
|
|
455
513
|
|
|
514
|
+
// v0.1.11: persist group membership in saved workflows — patch the group
|
|
515
|
+
// serializer so member ids land in the workflow JSON next to
|
|
516
|
+
// title/bounding/color, and the configure hook above restores them on load.
|
|
517
|
+
resolveLiteGraph().then((LG) => {
|
|
518
|
+
const proto = LG?.LGraphGroup?.prototype;
|
|
519
|
+
const orig = proto?.serialize;
|
|
520
|
+
if (!proto || typeof orig !== "function" || proto._dshSerializePatched) return;
|
|
521
|
+
proto._dshSerializePatched = true;
|
|
522
|
+
proto.serialize = function () {
|
|
523
|
+
const data = orig.call(this) ?? {};
|
|
524
|
+
if (Array.isArray(this._dshNodes) && this._dshNodes.length) {
|
|
525
|
+
data.nodes = this._dshNodes.map((n) => n.id);
|
|
526
|
+
}
|
|
527
|
+
return data;
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
|
|
456
531
|
// M1: listen for write commands pushed over the WebSocket.
|
|
457
532
|
// Per-tab targeting (M3): when the backend names a target clientId (the
|
|
458
533
|
// last one that reported), only that tab executes — several open ComfyUI
|
package/lib/client.js
CHANGED
|
@@ -1086,6 +1086,11 @@ function CanvasTab(props) {
|
|
|
1086
1086
|
};
|
|
1087
1087
|
fit();
|
|
1088
1088
|
outer.style.display = "flex";
|
|
1089
|
+
// 画布专注模式:告知 host 本会话正在画布标签(comfyui_config 按会话读取,
|
|
1090
|
+
// agent 据此知道你在画布开工;按会话隔离,卸载时写回 chat)。
|
|
1091
|
+
if (scope && typeof scope.set === "function" && props.sessionId) {
|
|
1092
|
+
setSessionView(scope, String(props.sessionId), "canvas");
|
|
1093
|
+
}
|
|
1089
1094
|
var sync = function () {
|
|
1090
1095
|
// frame.src 常带尾斜杠而 activeBase 不带——裸 !== 会让每次挂载都重置 src
|
|
1091
1096
|
// 导致 ComfyUI 重载。归一化后仅当地址真正变化才更新。
|
|
@@ -1104,6 +1109,9 @@ function CanvasTab(props) {
|
|
|
1104
1109
|
if (ro) ro.disconnect();
|
|
1105
1110
|
window.removeEventListener("resize", fit);
|
|
1106
1111
|
document.documentElement.removeAttribute("data-dsh-comfyui-fullscreen");
|
|
1112
|
+
if (scope && typeof scope.set === "function" && props.sessionId) {
|
|
1113
|
+
setSessionView(scope, String(props.sessionId), "chat");
|
|
1114
|
+
}
|
|
1107
1115
|
// 仅隐藏,不摘除:iframe 浏览上下文保活,切回秒开。
|
|
1108
1116
|
outer.style.display = "none";
|
|
1109
1117
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Give DeepSeek Harness agents live control of your ComfyUI canvas (local or cloud): embed it as a split-screen tab, then read/edit/run workflows, debug errors, fetch output images, and batch-sweep parameters straight from the chat. Ships the ComfyUI-side bridge node.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|