dsh-comfyui-canvas 0.1.9 → 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 +13 -1
- package/README.zh.md +13 -1
- package/comfyui-bridge/ComfyUI-DSH-Canvas/__init__.py +1 -1
- package/comfyui-bridge/ComfyUI-DSH-Canvas/entry/bridge.js +87 -12
- package/comfyui-bridge/ComfyUI-DSH-Canvas/version.json +1 -1
- package/lib/client.js +212 -26
- package/lib/index.js +1835 -1784
- package/package.json +1 -1
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
|
@@ -10,6 +10,31 @@ var STATE_KEY = "__dsh_comfyui_state__";
|
|
|
10
10
|
// 当前生效的 ComfyUI 地址(默认;设置行更改后由 scope 覆盖)。
|
|
11
11
|
var activeBase = DEFAULT_BASE;
|
|
12
12
|
|
|
13
|
+
// 内存回退设置(2026-09-24):核心 #4587 移除客户端 settingsScope 服务后
|
|
14
|
+
// scope 缺失,分屏/rail 状态改用模块级内存态维持交互(页面会话内有效,
|
|
15
|
+
// 不跨重启持久化;host 侧无感知,画布专注标记等 host 联动降级)。
|
|
16
|
+
var MEMORY_STORE = { splitEnabledBySession: {}, railClosedBySession: {}, railWidth: DEFAULT_RAIL };
|
|
17
|
+
var MEMORY_SNAPSHOT = { value: MEMORY_STORE, writable: true };
|
|
18
|
+
var MEMORY_LISTENERS = [];
|
|
19
|
+
function memoryNotify() {
|
|
20
|
+
var copy = MEMORY_LISTENERS.slice();
|
|
21
|
+
for (var i = 0; i < copy.length; i++) { try { copy[i](); } catch (e) { /* listener error ignored */ } }
|
|
22
|
+
}
|
|
23
|
+
function memorySubscribe(cb) {
|
|
24
|
+
MEMORY_LISTENERS.push(cb);
|
|
25
|
+
return function () {
|
|
26
|
+
var idx = MEMORY_LISTENERS.indexOf(cb);
|
|
27
|
+
if (idx >= 0) MEMORY_LISTENERS.splice(idx, 1);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function memorySnapshot() { return MEMORY_SNAPSHOT; }
|
|
31
|
+
function memorySetPath(key, value) {
|
|
32
|
+
MEMORY_STORE[key] = value;
|
|
33
|
+
// 换新引用(useSyncExternalStore 用 Object.is 判定变更),仅真实变更时更新。
|
|
34
|
+
MEMORY_SNAPSHOT = { value: MEMORY_STORE, writable: true };
|
|
35
|
+
memoryNotify();
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
// 画布 iframe 永久挂载在 body 上,从不动它:标签切换只改显隐。
|
|
14
39
|
// 激活时用 fixed 定位对齐视图区(含顶栏),卸载时 visibility:hidden,
|
|
15
40
|
// iframe 文档保持存活,切换标签/插件重载都不会重新加载 ComfyUI。
|
|
@@ -523,10 +548,11 @@ function positionSplit(outer) {
|
|
|
523
548
|
function ComfyUISplitToggle(props) {
|
|
524
549
|
var sessionId = String(props.sessionId ?? "root");
|
|
525
550
|
var scope = props.scope;
|
|
551
|
+
var useScopeStore = scope && typeof scope.subscribe === "function" && typeof scope.getSnapshot === "function";
|
|
526
552
|
var snapshot = React.useSyncExternalStore(
|
|
527
|
-
function (cb) { return
|
|
528
|
-
function () { return
|
|
529
|
-
function () { return
|
|
553
|
+
function (cb) { return useScopeStore ? scope.subscribe(cb) : memorySubscribe(cb); },
|
|
554
|
+
function () { return useScopeStore ? scope.getSnapshot() : memorySnapshot(); },
|
|
555
|
+
function () { return useScopeStore ? scope.getSnapshot() : memorySnapshot(); },
|
|
530
556
|
);
|
|
531
557
|
var value = (snapshot && snapshot.value) || {};
|
|
532
558
|
var map = value.splitEnabledBySession || {};
|
|
@@ -593,6 +619,8 @@ function ComfyUISplitToggle(props) {
|
|
|
593
619
|
applyRailWidth(w);
|
|
594
620
|
if (scope && typeof scope.set === "function") {
|
|
595
621
|
scope.set("railWidth", w).catch(function () { /* recover handled by scope */ });
|
|
622
|
+
} else {
|
|
623
|
+
memorySetPath("railWidth", w);
|
|
596
624
|
}
|
|
597
625
|
// 拖动中实时重算画布覆盖层与把手位置(rail 左缘随宽度变化),
|
|
598
626
|
// 否则把手会停在起点,脱离 rail 边界。
|
|
@@ -672,6 +700,8 @@ function ComfyUISplitToggle(props) {
|
|
|
672
700
|
next[sessionId] = !enabled;
|
|
673
701
|
if (scope && typeof scope.set === "function") {
|
|
674
702
|
scope.set("splitEnabledBySession", next).catch(function () { /* recover handled by scope */ });
|
|
703
|
+
} else {
|
|
704
|
+
memorySetPath("splitEnabledBySession", next);
|
|
675
705
|
}
|
|
676
706
|
};
|
|
677
707
|
|
|
@@ -681,6 +711,8 @@ function ComfyUISplitToggle(props) {
|
|
|
681
711
|
next[sessionId] = !railClosed;
|
|
682
712
|
if (scope && typeof scope.set === "function") {
|
|
683
713
|
scope.set("railClosedBySession", next).catch(function () { /* recover handled by scope */ });
|
|
714
|
+
} else {
|
|
715
|
+
memorySetPath("railClosedBySession", next);
|
|
684
716
|
}
|
|
685
717
|
};
|
|
686
718
|
|
|
@@ -780,10 +812,11 @@ function ComfyUISplitToggle(props) {
|
|
|
780
812
|
|
|
781
813
|
function ComfyUISettingsRow(props) {
|
|
782
814
|
var scope = props.scope;
|
|
815
|
+
var useScopeStore = scope && typeof scope.subscribe === "function" && typeof scope.getSnapshot === "function";
|
|
783
816
|
var snapshot = React.useSyncExternalStore(
|
|
784
|
-
function (cb) { return
|
|
785
|
-
function () { return
|
|
786
|
-
function () { return
|
|
817
|
+
function (cb) { return useScopeStore ? scope.subscribe(cb) : memorySubscribe(cb); },
|
|
818
|
+
function () { return useScopeStore ? scope.getSnapshot() : memorySnapshot(); },
|
|
819
|
+
function () { return useScopeStore ? scope.getSnapshot() : memorySnapshot(); },
|
|
787
820
|
);
|
|
788
821
|
var value = (snapshot && snapshot.value) || {};
|
|
789
822
|
var baseUrl = typeof value.baseUrl === "string" && value.baseUrl ? value.baseUrl : DEFAULT_BASE;
|
|
@@ -809,19 +842,32 @@ function ComfyUISettingsRow(props) {
|
|
|
809
842
|
var titleStyle = { fontSize: "13px", fontWeight: "600", color: "var(--dsw-alias-label-primary,#e8e8e8)" };
|
|
810
843
|
|
|
811
844
|
function setField(field, raw) {
|
|
812
|
-
if (!writable
|
|
845
|
+
if (!writable) return;
|
|
813
846
|
var next = raw === "" ? null : raw;
|
|
814
|
-
|
|
847
|
+
if (scope && typeof scope.set === "function") {
|
|
848
|
+
scope.set(field, next).catch(function () { /* recover handled by scope */ });
|
|
849
|
+
} else {
|
|
850
|
+
memorySetPath(field, next);
|
|
851
|
+
}
|
|
815
852
|
}
|
|
816
853
|
|
|
817
854
|
// 插件更新请求:settings 页「检查更新 / 更新」按钮把 updateRequest 写进
|
|
818
855
|
// settings,host watcher 执行 registry 检查或安装,并把结果写回
|
|
819
856
|
// pluginUpdate(status / latestVersion / message / checkedAt / updatedAt)。
|
|
820
857
|
function requestUpdate(action) {
|
|
821
|
-
if (!writable
|
|
822
|
-
|
|
858
|
+
if (!writable) return;
|
|
859
|
+
// 0.1.7 新核心下设置写入通道已迁移(settingsScope 移除):更新请在插件市场
|
|
860
|
+
// 操作。保留 scope 写入以兼容旧核心 host watcher;缺失时给出可见指引。
|
|
861
|
+
if (scope && typeof scope.set === "function") {
|
|
862
|
+
scope.set("updateRequest", { requested: true, action: action }).catch(function () { /* recover handled by scope */ });
|
|
863
|
+
} else {
|
|
864
|
+
setMarketNote("更新请在左侧「插件市场」中操作(DSH 0.1.7 起,本设置页的更新通道已随 settingsScope 一并迁移)。");
|
|
865
|
+
}
|
|
823
866
|
}
|
|
824
867
|
var upStatus = value.pluginUpdate || {};
|
|
868
|
+
var noteState = React.useState("");
|
|
869
|
+
var marketNote = noteState[0];
|
|
870
|
+
var setMarketNote = noteState[1];
|
|
825
871
|
var btnRowStyle = {
|
|
826
872
|
border: "1px solid var(--dsw-alias-border-l2,#3a3a40)",
|
|
827
873
|
borderRadius: "6px", padding: "6px 12px",
|
|
@@ -956,20 +1002,138 @@ function ComfyUISettingsRow(props) {
|
|
|
956
1002
|
background: "var(--dsw-alias-state-business-primary,#3b82f6)", color: "#fff",
|
|
957
1003
|
}),
|
|
958
1004
|
}, upStatus.status === "updating" ? "更新中…" : "更新到 v" + (upStatus.latestVersion || "")) : null,
|
|
1005
|
+
marketNote ? React.createElement("div", {
|
|
1006
|
+
style: { fontSize: "12px", color: "var(--dsw-alias-state-warn-primary,#d29922)", lineHeight: 1.5, whiteSpace: "pre-wrap" },
|
|
1007
|
+
}, marketNote) : null,
|
|
959
1008
|
),
|
|
960
1009
|
);
|
|
961
1010
|
}
|
|
962
1011
|
|
|
963
|
-
|
|
1012
|
+
// ComfyUI 独立标签视图(2026-09-25,替代分屏按钮):conversation.view
|
|
1013
|
+
// 容器内画布 —— iframe + 顶栏(刷新/重启/关闭)+ CanvasStatusOverlay 状态层。
|
|
1014
|
+
// 画布标签的 iframe + 顶栏持久实例(2026-09-25 v2):浏览器里 iframe 一旦脱离 DOM
|
|
1015
|
+
// (removeChild)其浏览上下文即被销毁,重挂必然重载。正确做法(旧 buildCanvas 同款):
|
|
1016
|
+
// 常驻挂到 body、永不 removeChild,激活时 fixed 定位到标签容器并 display:flex,
|
|
1017
|
+
// 离开时仅 display:none —— iframe 文档保活,切回秒开不重载。
|
|
1018
|
+
var TAB_CANVAS = null;
|
|
1019
|
+
|
|
1020
|
+
function CanvasTab(props) {
|
|
1021
|
+
var scope = props.scope;
|
|
1022
|
+
var hostRef = React.useRef(null);
|
|
1023
|
+
React.useEffect(function () {
|
|
1024
|
+
var host = hostRef.current;
|
|
1025
|
+
if (!host) return;
|
|
1026
|
+
var frame, bar, outer;
|
|
1027
|
+
if (!TAB_CANVAS) {
|
|
1028
|
+
outer = document.createElement("div");
|
|
1029
|
+
outer.style.cssText = "position:fixed;display:none;z-index:40;flex-direction:column;overflow:hidden;background:var(--dsw-alias-bg-layer-1,#232324);";
|
|
1030
|
+
bar = document.createElement("div");
|
|
1031
|
+
bar.style.cssText = "flex:none;display:flex;align-items:center;gap:12px;padding:0 12px;height:34px;background:var(--dsw-alias-bg-layer-1,#232324);border-bottom:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.25));font-size:13px;";
|
|
1032
|
+
var title = document.createElement("strong");
|
|
1033
|
+
title.textContent = "ComfyUI";
|
|
1034
|
+
bar.appendChild(title);
|
|
1035
|
+
var actionBtn = function (label, action) {
|
|
1036
|
+
var btn = document.createElement("button");
|
|
1037
|
+
btn.type = "button";
|
|
1038
|
+
btn.textContent = label;
|
|
1039
|
+
btn.title = label;
|
|
1040
|
+
btn.setAttribute("aria-label", label);
|
|
1041
|
+
btn.style.cssText = "background:transparent;border:none;color:var(--dsw-alias-state-business-primary,#8ab4f8);cursor:pointer;font-size:12px;padding:2px 6px;";
|
|
1042
|
+
btn.addEventListener("click", function () {
|
|
1043
|
+
document.dispatchEvent(new CustomEvent("dsh-comfyui-control", { bubbles: true, detail: { action: action } }));
|
|
1044
|
+
});
|
|
1045
|
+
return btn;
|
|
1046
|
+
};
|
|
1047
|
+
var refreshBtn = document.createElement("button");
|
|
1048
|
+
refreshBtn.type = "button";
|
|
1049
|
+
refreshBtn.textContent = "刷新";
|
|
1050
|
+
refreshBtn.title = "刷新画布";
|
|
1051
|
+
refreshBtn.setAttribute("aria-label", "刷新画布");
|
|
1052
|
+
refreshBtn.style.cssText = "background:transparent;border:none;color:var(--dsw-alias-state-business-primary,#8ab4f8);cursor:pointer;font-size:12px;padding:2px 6px;";
|
|
1053
|
+
refreshBtn.addEventListener("click", function () {
|
|
1054
|
+
var url;
|
|
1055
|
+
try { url = new URL(frame.src || activeBase); } catch (e) { url = null; }
|
|
1056
|
+
if (url) { url.searchParams.set("_dsh_reload", String(Date.now())); frame.src = url.toString(); }
|
|
1057
|
+
else { frame.src = (frame.src || activeBase).split("#")[0] + "?_dsh_reload=" + Date.now(); }
|
|
1058
|
+
});
|
|
1059
|
+
var right = document.createElement("div");
|
|
1060
|
+
right.style.cssText = "margin-left:auto;display:flex;align-items:center;gap:8px;";
|
|
1061
|
+
right.appendChild(refreshBtn);
|
|
1062
|
+
right.appendChild(actionBtn("重启", "restart"));
|
|
1063
|
+
right.appendChild(actionBtn("关闭", "stop"));
|
|
1064
|
+
bar.appendChild(right);
|
|
1065
|
+
frame = document.createElement("iframe");
|
|
1066
|
+
frame.title = "ComfyUI";
|
|
1067
|
+
frame.style.cssText = "flex:1;width:100%;border:none;display:block;background:#fff;";
|
|
1068
|
+
frame.src = activeBase;
|
|
1069
|
+
outer.appendChild(bar);
|
|
1070
|
+
outer.appendChild(frame);
|
|
1071
|
+
document.body.appendChild(outer);
|
|
1072
|
+
TAB_CANVAS = { outer: outer, frame: frame, bar: bar };
|
|
1073
|
+
} else {
|
|
1074
|
+
outer = TAB_CANVAS.outer;
|
|
1075
|
+
frame = TAB_CANVAS.frame;
|
|
1076
|
+
bar = TAB_CANVAS.bar;
|
|
1077
|
+
}
|
|
1078
|
+
var fit = function () {
|
|
1079
|
+
var h = hostRef.current;
|
|
1080
|
+
if (!h) return;
|
|
1081
|
+
var r = h.getBoundingClientRect();
|
|
1082
|
+
outer.style.left = r.left + "px";
|
|
1083
|
+
outer.style.top = r.top + "px";
|
|
1084
|
+
outer.style.width = r.width + "px";
|
|
1085
|
+
outer.style.height = r.height + "px";
|
|
1086
|
+
};
|
|
1087
|
+
fit();
|
|
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
|
+
}
|
|
1094
|
+
var sync = function () {
|
|
1095
|
+
// frame.src 常带尾斜杠而 activeBase 不带——裸 !== 会让每次挂载都重置 src
|
|
1096
|
+
// 导致 ComfyUI 重载。归一化后仅当地址真正变化才更新。
|
|
1097
|
+
var want = String(activeBase).replace(/\/+$/, "");
|
|
1098
|
+
var cur = String(frame.src).replace(/\/+$/, "");
|
|
1099
|
+
if (cur !== want) frame.src = activeBase;
|
|
1100
|
+
};
|
|
1101
|
+
var unsub = scope && typeof scope.subscribe === "function" ? scope.subscribe(sync) : memorySubscribe(sync);
|
|
1102
|
+
// 画布标签全屏:隐藏常驻 composer(见 data-dsh-comfyui-fullscreen CSS)。
|
|
1103
|
+
document.documentElement.setAttribute("data-dsh-comfyui-fullscreen", "1");
|
|
1104
|
+
var ro = typeof ResizeObserver === "function" ? new ResizeObserver(fit) : null;
|
|
1105
|
+
if (ro) ro.observe(host);
|
|
1106
|
+
window.addEventListener("resize", fit);
|
|
1107
|
+
return function () {
|
|
1108
|
+
if (typeof unsub === "function") unsub();
|
|
1109
|
+
if (ro) ro.disconnect();
|
|
1110
|
+
window.removeEventListener("resize", fit);
|
|
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
|
+
}
|
|
1115
|
+
// 仅隐藏,不摘除:iframe 浏览上下文保活,切回秒开。
|
|
1116
|
+
outer.style.display = "none";
|
|
1117
|
+
};
|
|
1118
|
+
}, []);
|
|
1119
|
+
return React.createElement("div", {
|
|
1120
|
+
ref: hostRef,
|
|
1121
|
+
style: { position: "relative", width: "100%", height: "100%", background: "var(--dsw-alias-bg-layer-1,#232324)", overflow: "hidden" },
|
|
1122
|
+
}, React.createElement(CanvasStatusOverlay, { scope: scope, sessionId: props.sessionId || "root" }));
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
var inject = ["slots", "configForms"];
|
|
964
1126
|
|
|
965
1127
|
function apply(ctx) {
|
|
966
|
-
// 2026-09-
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
var
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1128
|
+
// 2026-09-25: DSH 0.1.7 新核心 — 配置经 entry Config(profile 支撑)的
|
|
1129
|
+
// ConfigForm 读写(ctx.configForms.get(NAMESPACE),接口与旧 scope 一致);
|
|
1130
|
+
// 缺失时回退到模块级内存存储(会话内有效)。旧 settingsScope 服务已移除。
|
|
1131
|
+
var configForm = null;
|
|
1132
|
+
if (ctx.configForms && typeof ctx.configForms.get === "function") {
|
|
1133
|
+
try { configForm = ctx.configForms.get(NAMESPACE); } catch (e) { configForm = null; }
|
|
1134
|
+
}
|
|
1135
|
+
var scope = configForm && typeof configForm.subscribe === "function"
|
|
1136
|
+
&& typeof configForm.getSnapshot === "function" ? configForm : null;
|
|
973
1137
|
// 画布顶栏「重启 / 关闭 ComfyUI」按钮 → 写 settings.comfyControl,
|
|
974
1138
|
// host watcher 执行 kill / restart(结果由可达性探测反映到状态圆点)。
|
|
975
1139
|
document.addEventListener("dsh-comfyui-control", function (e) {
|
|
@@ -980,7 +1144,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
980
1144
|
});
|
|
981
1145
|
// 让画布 iframe 使用设置里的地址(并跟随变更)。
|
|
982
1146
|
var syncConfig = function () {
|
|
983
|
-
var snap = scope ? scope.getSnapshot() :
|
|
1147
|
+
var snap = scope ? scope.getSnapshot() : memorySnapshot();
|
|
984
1148
|
var v = snap && snap.value;
|
|
985
1149
|
var base = v && typeof v.baseUrl === "string" && v.baseUrl
|
|
986
1150
|
? v.baseUrl : DEFAULT_BASE;
|
|
@@ -991,7 +1155,11 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
991
1155
|
var state = canvasState();
|
|
992
1156
|
if (state.canvas) {
|
|
993
1157
|
var frame = state.canvas.querySelector("iframe");
|
|
994
|
-
|
|
1158
|
+
// frame.src 常带尾斜杠而 base 不带——裸 !== 恒真会把分屏 iframe 每次
|
|
1159
|
+
// 重载;归一化后仅地址真正变化才更新(与标签 sync 同款修复)。
|
|
1160
|
+
if (frame && String(frame.src).replace(/\/+$/, "") !== String(base).replace(/\/+$/, "")) {
|
|
1161
|
+
frame.src = base;
|
|
1162
|
+
}
|
|
995
1163
|
var link = state.canvas.querySelector("a");
|
|
996
1164
|
if (link) link.href = base;
|
|
997
1165
|
}
|
|
@@ -999,6 +1167,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
999
1167
|
applyRailWidth(v?.railWidth != null ? v.railWidth : DEFAULT_RAIL);
|
|
1000
1168
|
};
|
|
1001
1169
|
if (scope) scope.subscribe(syncConfig);
|
|
1170
|
+
else memorySubscribe(syncConfig);
|
|
1002
1171
|
|
|
1003
1172
|
// 全局看门狗:保证分屏标记与真实分屏态一致,杜绝 data-dsh-split-on 残留。
|
|
1004
1173
|
// 挤压规则只在 html[data-dsh-split-on="1"] 时生效;一旦残留打上该属性,
|
|
@@ -1012,7 +1181,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
1012
1181
|
return tab ? (tab.textContent || "").trim() : "";
|
|
1013
1182
|
};
|
|
1014
1183
|
var enforceSplitMarker = function () {
|
|
1015
|
-
var snap = scope ? scope.getSnapshot() :
|
|
1184
|
+
var snap = scope ? scope.getSnapshot() : memorySnapshot();
|
|
1016
1185
|
var v = snap && snap.value ? snap.value : {};
|
|
1017
1186
|
var map = v.splitEnabledBySession || {};
|
|
1018
1187
|
var anyEnabled = Object.keys(map).some(function (k) { return map[k] === true; });
|
|
@@ -1041,6 +1210,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
1041
1210
|
};
|
|
1042
1211
|
var splitWatchdog = null;
|
|
1043
1212
|
if (scope) scope.subscribe(enforceSplitMarker);
|
|
1213
|
+
else memorySubscribe(enforceSplitMarker);
|
|
1044
1214
|
if (typeof MutationObserver === "function") {
|
|
1045
1215
|
splitWatchdog = new MutationObserver(enforceSplitMarker);
|
|
1046
1216
|
splitWatchdog.observe(
|
|
@@ -1065,7 +1235,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
1065
1235
|
var onNarrow = function () {
|
|
1066
1236
|
var now = narrowQuery ? narrowQuery.matches : (window.innerWidth < 1200);
|
|
1067
1237
|
if (now && !narrowWas) {
|
|
1068
|
-
var snap = scope ? scope.getSnapshot() :
|
|
1238
|
+
var snap = scope ? scope.getSnapshot() : memorySnapshot();
|
|
1069
1239
|
var v = snap && snap.value ? snap.value : {};
|
|
1070
1240
|
var map = v.splitEnabledBySession || {};
|
|
1071
1241
|
var anyEnabled = Object.keys(map).some(function (k) { return map[k] === true; });
|
|
@@ -1074,6 +1244,8 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
1074
1244
|
for (var k in map) cleared[k] = false;
|
|
1075
1245
|
if (scope && typeof scope.set === "function") {
|
|
1076
1246
|
scope.set("splitEnabledBySession", cleared).catch(function () { /* recover handled by scope */ });
|
|
1247
|
+
} else {
|
|
1248
|
+
memorySetPath("splitEnabledBySession", cleared);
|
|
1077
1249
|
}
|
|
1078
1250
|
// 同步清理 DOM,避免 settings 异步落地前挤压样式/画布残留。
|
|
1079
1251
|
document.documentElement.removeAttribute("data-dsh-split-on");
|
|
@@ -1137,6 +1309,11 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
1137
1309
|
' margin-left: auto !important;',
|
|
1138
1310
|
' overflow: hidden !important;',
|
|
1139
1311
|
'}',
|
|
1312
|
+
// ComfyUI 画布标签全屏:隐藏常驻 composer(ConversationRoot 的
|
|
1313
|
+
// composerSeat 不随视图切换消失,需插件隐藏,否则画布被输入框占底)。
|
|
1314
|
+
'html[data-dsh-comfyui-fullscreen="1"] [data-composer-seat] {',
|
|
1315
|
+
' display: none !important;',
|
|
1316
|
+
'}',
|
|
1140
1317
|
].join("\n");
|
|
1141
1318
|
document.head.appendChild(splitStyle);
|
|
1142
1319
|
}
|
|
@@ -1154,10 +1331,19 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
|
|
|
1154
1331
|
registerSettingsNavIcon(settingsLabel);
|
|
1155
1332
|
}
|
|
1156
1333
|
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1334
|
+
// ComfyUI 独立标签(第一入口):conversation.view 画布视图(全屏画布)。
|
|
1335
|
+
ctx.slots.inject("conversation.view", function () {
|
|
1336
|
+
return ctx.slots.register({
|
|
1337
|
+
name: "conversation.view",
|
|
1338
|
+
id: "comfyui-canvas-view",
|
|
1339
|
+
order: 20,
|
|
1340
|
+
label: function () { return "ComfyUI"; },
|
|
1341
|
+
inject: function (sessionId) { return { sessionId: String(sessionId), scope: scope }; }
|
|
1342
|
+
}, CanvasTab);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
// 分屏入口(第二入口,2026-09-25 起与独立标签并存):会话标头右侧 utilities 槽。
|
|
1346
|
+
// 在「对话」标签内开分屏 = 画布左 + 主对话 rail 右(可折叠),与旧版行为一致。
|
|
1161
1347
|
ctx.slots.inject("conversation.session.header.utilities", function () {
|
|
1162
1348
|
return ctx.slots.register({
|
|
1163
1349
|
name: "conversation.session.header.utilities",
|