dsh-comfyui-canvas 0.1.4 → 0.1.5
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 +4 -4
- package/README.zh.md +4 -4
- package/docs/screenshots/02-workflow-dunhuang.png +0 -0
- package/docs/screenshots/04-gallery.png +0 -0
- package/docs/screenshots/05-batch-portraits.png +0 -0
- package/docs/screenshots/06-template-library.png +0 -0
- package/lib/client.js +94 -0
- package/lib/index.js +214 -0
- package/package.json +1 -1
- package/screenshots.json +6 -3
- package/docs/screenshots/01-startup-prompt.png +0 -0
- /package/docs/screenshots/{02-canvas-ready.png → 01-split-canvas-chat.png} +0 -0
- /package/docs/screenshots/{03-workflow-output.png → 03-output-grid.png} +0 -0
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)
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
> **✅ DSH version compatibility (since v0.1.4)**: the split-screen layout is **fully self-contained** in the plugin — it uses only official DSH slots (`conversation.session.header.utilities`) and DOM `data-*` anchors, with **zero core modifications**. Works on any official DSH **v0.1.x** (including v0.1.2+ with the breaking client refresh) without patches. The earlier split-rail implementation depended on private core patches; v0.1.4 removes that dependency entirely.
|
|
13
13
|
> - ❌ **Non-official desktop wrappers** (e.g. the community `dsh-desktop`) are not guaranteed compatible — they bundle an upstream version that may be ahead of or behind this plugin's baseline; rely on official DSH.
|
|
14
14
|
|
|
15
|
-

|
|
16
16
|
|
|
17
|
-
Embed
|
|
17
|
+
**From chat to canvas to artwork — drive ComfyUI as a visual workflow IDE inside DSH.** Embed **ComfyUI** (local or cloud) as a split-screen canvas in [DeepSeek Harness](https://github.com/DeepSeek-Harness/DSH) Web: the agent sparks ideas, writes prompts and scripts right in the chat, applies them live to the canvas in front of you, and produces images, music, video, and 3D. From idea to finished output without ever leaving the conversation or switching front-ends:
|
|
18
18
|
|
|
19
19
|
- **Canvas ops** — compose and arrange pipelines, read/write workflows, edit nodes, wire links, run, tune parameters, and debug errors, all live and WYSIWYG on the exact canvas you are looking at
|
|
20
20
|
- **Production tasks** — batch parameter sweeps (`batch_run`) and automatic output-image retrieval back into the chat (`get_outputs`), powering multi-modal creative and batch generation across images, music, video, and 3D
|
|
@@ -37,7 +37,7 @@ This package is the DSH-side plugin, and it ships the ComfyUI-side bridge node t
|
|
|
37
37
|
| **Upkeep tool** | `comfyui_upgrade` one-click updates the ComfyUI core and every git-backed custom node (concurrent, dirty-safe); `comfyui_config` reports the active connection, canvas focus, project directory, and a bridge-auth handshake check (`bridgeAuthEffective`). |
|
|
38
38
|
| **Node dev tools** | `comfyui_read_source` / `comfyui_edit_source` / `comfyui_reload` — read and edit custom-node source under custom_nodes/ and restart ComfyUI from the conversation, then verify on the canvas. |
|
|
39
39
|
| **Canvas focus mode** | The agent can tell (via `comfyui_config`) whether canvas split mode is active for the current session, and focus on canvas work only then. Session-isolated. |
|
|
40
|
-
| **Settings page** | ComfyUI base URL / port / network mode / bridge token / launch command / project directory / rail width. Changes apply live. Customized nav icon with ComfyUI logo. |
|
|
40
|
+
| **Settings page** | ComfyUI base URL / port / network mode / bridge token / launch command / project directory / rail width. Changes apply live. Shows the plugin version and supports check / one-click update (installs the latest npm release; a DSH restart is required). Customized nav icon with ComfyUI logo. |
|
|
41
41
|
| **Split layout** | The **ComfyUI** button in the session header opens canvas-on-the-left + official chat rail on the right (state is session-isolated). Only data-* anchors and CSS variables are used — no DSH core class names, so the layout survives upstream styling changes. |
|
|
42
42
|
|
|
43
43
|
## Install
|
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)
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
> **✅ DSH 版本兼容(v0.1.4 起)**:分屏布局已完全**自包含**在插件内——只用官方 DSH 插槽(`conversation.session.header.utilities`)与 DOM `data-*` 锚点,**零核心改动**。任何官方 DSH **v0.1.x**(含带破坏性 client 更新的 v0.1.2+)都开箱即用、无需补丁。此前分屏 rail 依赖 DSH 私有核心补丁,v0.1.4 已彻底移除该依赖。
|
|
13
13
|
> - ❌ **非官方桌面封装**(如 `dsh-desktop` 社区版)不保证兼容——它内部跑的上游版本可能超前/滞后于本插件基线,请以官方 DSH 为准。
|
|
14
14
|
|
|
15
|
-

|
|
16
16
|
|
|
17
|
-
把 **ComfyUI**(本地或云端)以画布分屏嵌入 [DeepSeek Harness](https://github.com/DeepSeek-Harness/DSH) Web
|
|
17
|
+
**从对话到画布再到作品——DSH 里驾驭 ComfyUI 的可视化工作流 IDE。** 把 **ComfyUI**(本地或云端)以画布分屏嵌入 [DeepSeek Harness](https://github.com/DeepSeek-Harness/DSH) Web,agent 在对话里激发创意、书写提示词与脚本,实时落到你眼前的画布上,产出图像、音乐、视频、3D。从灵感到成品,全程不离开对话,不用切换任何前端工具:
|
|
18
18
|
|
|
19
19
|
- **画布操作**:搭建编排、读写工作流、修改节点、连线、运行、调整参数、工作流查错——所见即所得,实时落在你眼前的画布上
|
|
20
20
|
- **生产任务**:批量扫参(`batch_run`)、自动取回出图(`get_outputs`)带回对话,实现图像、音乐、视频、3D 等多任务智能创作与批量生产
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
| **节点开发工具** | `comfyui_read_source` / `comfyui_edit_source` / `comfyui_reload`——对话里直接读写 `custom_nodes/` 下的节点源码并重启 ComfyUI,再到画布上验证。 |
|
|
40
40
|
| **画布专注模式(会话隔离)** | agent 通过 `comfyui_config` 感知当前会话是否开启画布分屏,只在画布场景专注画布操作,且**按会话隔离**——多个会话互不干扰。 |
|
|
41
41
|
| **ComfyUI 报错处理** | `debug` 校验工作流并高亮报错节点(纯校验,不触发执行),agent 帮你定位/修复画布错误。 |
|
|
42
|
-
| **设置页** | ComfyUI 地址 / 端口 / 网络模式 / 桥接 Token / 启动命令 / 项目目录 /
|
|
42
|
+
| **设置页** | ComfyUI 地址 / 端口 / 网络模式 / 桥接 Token / 启动命令 / 项目目录 / 右侧面板宽度,实时生效;显示插件版本号并支持检查 / 一键更新(更新到 npm 最新版后需重启 DSH)。导航栏已自定义为 ComfyUI logo 图标。 |
|
|
43
43
|
| **对话栏** | 分屏 rail 复用官方完整对话(消息、输入、发送、贴图、授权弹窗全在),不再需要插件自绘迷你输入框与授权 overlay。 |
|
|
44
44
|
|
|
45
45
|
---
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -74,6 +74,32 @@ function buildCanvas() {
|
|
|
74
74
|
var actions = document.createElement("div");
|
|
75
75
|
actions.style.cssText = "margin-left:auto;display:flex;align-items:center;gap:12px;";
|
|
76
76
|
|
|
77
|
+
// 重启 / 关闭 ComfyUI 控制按钮(放在状态圆点前面):点击派发自定义事件,
|
|
78
|
+
// apply 里的监听器把它写进 settings(comfyControl),host watcher 执行
|
|
79
|
+
// kill / restart;结果由画布的可达性探测自动反映到状态圆点。
|
|
80
|
+
var controlButton = function (title, inner, action) {
|
|
81
|
+
var btn = document.createElement("button");
|
|
82
|
+
btn.type = "button";
|
|
83
|
+
btn.title = title;
|
|
84
|
+
btn.setAttribute("aria-label", title);
|
|
85
|
+
btn.innerHTML = inner;
|
|
86
|
+
btn.style.cssText = "background:transparent;border:none;padding:0;color:var(--dsw-alias-state-business-primary,#8ab4f8);cursor:pointer;display:inline-flex;align-items:center;";
|
|
87
|
+
btn.addEventListener("click", function () {
|
|
88
|
+
document.dispatchEvent(new CustomEvent("dsh-comfyui-control", { bubbles: true, detail: { action: action } }));
|
|
89
|
+
});
|
|
90
|
+
return btn;
|
|
91
|
+
};
|
|
92
|
+
var restartBtn = controlButton(
|
|
93
|
+
"重启 ComfyUI",
|
|
94
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2v6h-6"/><path d="M21 13a9 9 0 1 1-3-7.7L21 8"/></svg>',
|
|
95
|
+
"restart"
|
|
96
|
+
);
|
|
97
|
+
var stopBtn = controlButton(
|
|
98
|
+
"关闭 ComfyUI",
|
|
99
|
+
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>',
|
|
100
|
+
"stop"
|
|
101
|
+
);
|
|
102
|
+
|
|
77
103
|
var status = document.createElement("span");
|
|
78
104
|
status.id = "dsh-comfy-status";
|
|
79
105
|
status.title = "检测中…";
|
|
@@ -127,6 +153,8 @@ function buildCanvas() {
|
|
|
127
153
|
document.dispatchEvent(new CustomEvent("dsh-comfyui-toggle-rail", { bubbles: true }));
|
|
128
154
|
});
|
|
129
155
|
|
|
156
|
+
actions.appendChild(restartBtn);
|
|
157
|
+
actions.appendChild(stopBtn);
|
|
130
158
|
actions.appendChild(status);
|
|
131
159
|
actions.appendChild(refresh);
|
|
132
160
|
actions.appendChild(link);
|
|
@@ -286,6 +314,8 @@ function ComfyUIControl(props) {
|
|
|
286
314
|
background: "var(--dsw-alias-bg-layer-2, #1f1f23)", color: "var(--dsw-alias-label-primary, #e8e8e8)",
|
|
287
315
|
border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.25))",
|
|
288
316
|
borderRadius: "12px", boxShadow: "0 8px 32px rgba(0,0,0,.4)", fontFamily: "inherit",
|
|
317
|
+
// 内容纵向排列、全部居中:标题/说明/错误/按钮都在卡片中间,按钮与画布左右距离相等。
|
|
318
|
+
display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center",
|
|
289
319
|
};
|
|
290
320
|
var btnStyle = {
|
|
291
321
|
border: "none", borderRadius: "8px", padding: "10px 18px", fontSize: "13px",
|
|
@@ -776,6 +806,22 @@ function ComfyUISettingsRow(props) {
|
|
|
776
806
|
scope.set(field, next).catch(function () { /* recover handled by scope */ });
|
|
777
807
|
}
|
|
778
808
|
|
|
809
|
+
// 插件更新请求:settings 页「检查更新 / 更新」按钮把 updateRequest 写进
|
|
810
|
+
// settings,host watcher 执行 registry 检查或安装,并把结果写回
|
|
811
|
+
// pluginUpdate(status / latestVersion / message / checkedAt / updatedAt)。
|
|
812
|
+
function requestUpdate(action) {
|
|
813
|
+
if (!writable || !scope.set) return;
|
|
814
|
+
scope.set("updateRequest", { requested: true, action: action }).catch(function () { /* recover handled by scope */ });
|
|
815
|
+
}
|
|
816
|
+
var upStatus = value.pluginUpdate || {};
|
|
817
|
+
var btnRowStyle = {
|
|
818
|
+
border: "1px solid var(--dsw-alias-border-l2,#3a3a40)",
|
|
819
|
+
borderRadius: "6px", padding: "6px 12px",
|
|
820
|
+
fontSize: "12px", cursor: "pointer",
|
|
821
|
+
background: "transparent",
|
|
822
|
+
color: "var(--dsw-alias-label-primary,#e8e8e8)",
|
|
823
|
+
};
|
|
824
|
+
|
|
779
825
|
// 桥接鉴权握手(非启动路径):设置页打开 / token 变化时向 host 请求一次
|
|
780
826
|
// 探测(probeRequested=true → host 后台探测 → 结果写回 bridgeProbe)。
|
|
781
827
|
// host 代理是因为浏览器跨域读不到 ComfyUI 的状态码(CORS),而 host 端
|
|
@@ -863,6 +909,46 @@ function ComfyUISettingsRow(props) {
|
|
|
863
909
|
probeText ? React.createElement("div", {
|
|
864
910
|
style: { fontSize: "12px", color: probeColor, marginTop: "10px", lineHeight: "1.5" },
|
|
865
911
|
}, probeText) : null,
|
|
912
|
+
React.createElement("div", {
|
|
913
|
+
style: {
|
|
914
|
+
marginTop: "14px", paddingTop: "12px",
|
|
915
|
+
borderTop: "1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.25))",
|
|
916
|
+
display: "flex", flexDirection: "column", gap: "8px", alignItems: "flex-start",
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
React.createElement("div", { style: { fontSize: "13px", fontWeight: "600", color: "var(--dsw-alias-label-primary,#e8e8e8)" } }, "插件版本与更新"),
|
|
920
|
+
React.createElement("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-secondary,#9a9aa2)", lineHeight: 1.5 } },
|
|
921
|
+
"当前版本 v" + (value.pluginVersion || "?") +
|
|
922
|
+
(upStatus.latestVersion ? " · 最新 v" + upStatus.latestVersion : "")
|
|
923
|
+
),
|
|
924
|
+
upStatus.status && upStatus.status !== "idle" ? React.createElement("div", {
|
|
925
|
+
style: {
|
|
926
|
+
fontSize: "12px", lineHeight: 1.5, whiteSpace: "pre-wrap",
|
|
927
|
+
color: upStatus.status === "error"
|
|
928
|
+
? "var(--dsw-alias-state-error-primary,#f85149)"
|
|
929
|
+
: upStatus.status === "available"
|
|
930
|
+
? "var(--dsw-alias-state-warn-primary,#d29922)"
|
|
931
|
+
: "var(--dsw-alias-label-secondary,#9a9aa2)",
|
|
932
|
+
},
|
|
933
|
+
}, upStatus.message || "") : null,
|
|
934
|
+
upStatus.status === "done" ? React.createElement("div", {
|
|
935
|
+
style: { fontSize: "12px", color: "var(--dsw-alias-state-warn-primary,#d29922)", lineHeight: 1.5 },
|
|
936
|
+
}, "⚠️ 重启 DSH 后更新才会生效") : null,
|
|
937
|
+
React.createElement("button", {
|
|
938
|
+
type: "button",
|
|
939
|
+
disabled: !writable || upStatus.status === "checking" || upStatus.status === "updating",
|
|
940
|
+
onClick: function () { requestUpdate("check"); },
|
|
941
|
+
style: btnRowStyle,
|
|
942
|
+
}, upStatus.status === "checking" ? "检测中…" : "检查更新"),
|
|
943
|
+
upStatus.status === "available" ? React.createElement("button", {
|
|
944
|
+
type: "button",
|
|
945
|
+
disabled: !writable || upStatus.status === "updating",
|
|
946
|
+
onClick: function () { requestUpdate("update"); },
|
|
947
|
+
style: Object.assign({}, btnRowStyle, {
|
|
948
|
+
background: "var(--dsw-alias-state-business-primary,#3b82f6)", color: "#fff",
|
|
949
|
+
}),
|
|
950
|
+
}, upStatus.status === "updating" ? "更新中…" : "更新到 v" + (upStatus.latestVersion || "")) : null,
|
|
951
|
+
),
|
|
866
952
|
);
|
|
867
953
|
}
|
|
868
954
|
|
|
@@ -870,6 +956,14 @@ var inject = ["slots", "settingsScope"];
|
|
|
870
956
|
|
|
871
957
|
function apply(ctx) {
|
|
872
958
|
var scope = ctx.settingsScope.bind({ namespace: NAMESPACE });
|
|
959
|
+
// 画布顶栏「重启 / 关闭 ComfyUI」按钮 → 写 settings.comfyControl,
|
|
960
|
+
// host watcher 执行 kill / restart(结果由可达性探测反映到状态圆点)。
|
|
961
|
+
document.addEventListener("dsh-comfyui-control", function (e) {
|
|
962
|
+
var action = e && e.detail && e.detail.action === "restart" ? "restart" : "stop";
|
|
963
|
+
if (scope && typeof scope.set === "function") {
|
|
964
|
+
scope.set("comfyControl", { requested: true, action: action }).catch(function () { /* recover handled by scope */ });
|
|
965
|
+
}
|
|
966
|
+
});
|
|
873
967
|
// 让画布 iframe 使用设置里的地址(并跟随变更)。
|
|
874
968
|
var syncConfig = function () {
|
|
875
969
|
var snap = scope.getSnapshot();
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
16
16
|
import z from '@deepseek-ai/schemastery'
|
|
17
17
|
import path from 'node:path'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
18
20
|
import { mediaTypeOf, extractExecutionError, nextStemNumber, upsertRun } from './utils.js'
|
|
19
21
|
|
|
20
22
|
export const name = 'dsh-comfyui-canvas'
|
|
@@ -93,6 +95,46 @@ const ConfigSchema = z.object({
|
|
|
93
95
|
bridgeAuthProbe: z.string(),
|
|
94
96
|
checkedAt: z.number(),
|
|
95
97
|
}).default({ bridgeAuthEffective: null, bridgeAuthProbe: 'not-probed', checkedAt: 0 }),
|
|
98
|
+
/**
|
|
99
|
+
* This plugin's own version, published to settings at startup so the
|
|
100
|
+
* settings page can show it next to the update button.
|
|
101
|
+
*/
|
|
102
|
+
pluginVersion: z.string().default(''),
|
|
103
|
+
/**
|
|
104
|
+
* One-shot plugin-update request from the settings page: the client sets
|
|
105
|
+
* `{ requested: true, action: 'check' | 'update' }`, the host watcher runs
|
|
106
|
+
* the npm-registry check or the package install and writes the outcome into
|
|
107
|
+
* `pluginUpdate`, then clears `requested`. Installs require a DSH restart to
|
|
108
|
+
* take effect, which the client surfaces after a successful update.
|
|
109
|
+
*/
|
|
110
|
+
updateRequest: z.object({
|
|
111
|
+
requested: z.boolean(),
|
|
112
|
+
action: z.union([z.const('check'), z.const('update')]),
|
|
113
|
+
}).default({ requested: false, action: 'check' }),
|
|
114
|
+
/** Plugin-update state, written by the host watcher for the settings page. */
|
|
115
|
+
pluginUpdate: z.object({
|
|
116
|
+
status: z.string(), // idle | checking | up-to-date | available | updating | done | error
|
|
117
|
+
latestVersion: z.string(),
|
|
118
|
+
message: z.string(),
|
|
119
|
+
checkedAt: z.number(),
|
|
120
|
+
updatedAt: z.number(),
|
|
121
|
+
}).default({ status: 'idle', latestVersion: '', message: '', checkedAt: 0, updatedAt: 0 }),
|
|
122
|
+
/**
|
|
123
|
+
* One-shot ComfyUI control request from the canvas header: the client sets
|
|
124
|
+
* `{ requested: true, action: 'stop' | 'restart' }`, the host watcher kills
|
|
125
|
+
* the recorded launch process (falling back to whatever listens on the
|
|
126
|
+
* configured port) and, for 'restart', re-enters the launch path.
|
|
127
|
+
*/
|
|
128
|
+
comfyControl: z.object({
|
|
129
|
+
requested: z.boolean(),
|
|
130
|
+
action: z.union([z.const('stop'), z.const('restart')]),
|
|
131
|
+
}).default({ requested: false, action: 'stop' }),
|
|
132
|
+
/** Last control outcome, written by the host watcher. */
|
|
133
|
+
comfyControlResult: z.object({
|
|
134
|
+
status: z.string(), // idle | stopping | restarting | done | error
|
|
135
|
+
message: z.string(),
|
|
136
|
+
at: z.number(),
|
|
137
|
+
}).default({ status: 'idle', message: '', at: 0 }),
|
|
96
138
|
})
|
|
97
139
|
|
|
98
140
|
function renderJSON(_args, value) {
|
|
@@ -183,6 +225,9 @@ export function apply(ctx) {
|
|
|
183
225
|
// Function-level handle to the settings scope (used by comfyui_config's
|
|
184
226
|
// activeView read); undefined when the settings service is unavailable.
|
|
185
227
|
let settingsScope = undefined
|
|
228
|
+
// The background process handle of the ComfyUI launched by this plugin, so
|
|
229
|
+
// the canvas header's stop/restart buttons can kill it directly.
|
|
230
|
+
let comfyProc = null
|
|
186
231
|
|
|
187
232
|
// v0.1.3: in-memory run ledger for `.runs.json` traceability — keyed by
|
|
188
233
|
// prompt_id, recording the overrides each run was started with so downloads
|
|
@@ -261,6 +306,7 @@ export function apply(ctx) {
|
|
|
261
306
|
...(workdir ? { workdir } : {}),
|
|
262
307
|
sandboxPolicy: { mode: 'danger-full-access', workspaceRoot: process.cwd() },
|
|
263
308
|
}))
|
|
309
|
+
comfyProc = proc
|
|
264
310
|
console.log(`[dsh-comfyui-canvas] launching ComfyUI: ${cmd}`)
|
|
265
311
|
// A healthy launch keeps python alive well past this grace
|
|
266
312
|
// window; an immediate exit (bad command / sandbox denial /
|
|
@@ -309,6 +355,174 @@ export function apply(ctx) {
|
|
|
309
355
|
}
|
|
310
356
|
})()
|
|
311
357
|
})
|
|
358
|
+
|
|
359
|
+
// Publish this plugin's version once so the settings page can show it
|
|
360
|
+
// next to the update button (best effort — never blocks startup).
|
|
361
|
+
try {
|
|
362
|
+
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url))
|
|
363
|
+
const version = String(JSON.parse(readFileSync(pkgPath, 'utf8')).version || '')
|
|
364
|
+
if (version) scope.update({ pluginVersion: version })
|
|
365
|
+
} catch { /* best effort */ }
|
|
366
|
+
|
|
367
|
+
// Plugin-update watcher: the settings page sets updateRequest with
|
|
368
|
+
// action 'check' (query npm registry for the latest version) or
|
|
369
|
+
// 'update' (install the latest package into the DSH web profile).
|
|
370
|
+
// Installs run unconfined (the profile lives outside the session
|
|
371
|
+
// workspace) and require a DSH restart to take effect.
|
|
372
|
+
let updatingPlugin = false
|
|
373
|
+
scope.watch(() => {
|
|
374
|
+
const value = scope.get()
|
|
375
|
+
const req = value?.updateRequest
|
|
376
|
+
if (!req?.requested || updatingPlugin) return
|
|
377
|
+
updatingPlugin = true
|
|
378
|
+
void (async () => {
|
|
379
|
+
const setState = (patch) => {
|
|
380
|
+
try { scope.update(patch) } catch { /* best effort */ }
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const action = req.action === 'update' ? 'update' : 'check'
|
|
384
|
+
const current = String(value?.pluginVersion || '')
|
|
385
|
+
setState({
|
|
386
|
+
updateRequest: { requested: false, action },
|
|
387
|
+
pluginUpdate: {
|
|
388
|
+
status: action === 'update' ? 'updating' : 'checking',
|
|
389
|
+
latestVersion: '',
|
|
390
|
+
message: action === 'update' ? '正在安装最新版本…' : '正在查询最新版本…',
|
|
391
|
+
checkedAt: Date.now(),
|
|
392
|
+
updatedAt: 0,
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
const res = await fetch('https://registry.npmjs.org/dsh-comfyui-canvas/latest', {
|
|
396
|
+
signal: AbortSignal.timeout(10000),
|
|
397
|
+
})
|
|
398
|
+
if (!res.ok) throw new Error(`npm registry HTTP ${res.status}`)
|
|
399
|
+
const meta = await res.json().catch(() => null)
|
|
400
|
+
const latest = String(meta?.version || '')
|
|
401
|
+
if (!latest) throw new Error('npm registry returned no version')
|
|
402
|
+
if (action === 'check') {
|
|
403
|
+
const upToDate = latest === current
|
|
404
|
+
setState({
|
|
405
|
+
pluginUpdate: {
|
|
406
|
+
status: upToDate ? 'up-to-date' : 'available',
|
|
407
|
+
latestVersion: latest,
|
|
408
|
+
message: upToDate
|
|
409
|
+
? `已是最新版本 v${latest}`
|
|
410
|
+
: `发现新版本 v${latest}(当前 v${current || '?'})`,
|
|
411
|
+
checkedAt: Date.now(),
|
|
412
|
+
updatedAt: 0,
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
// Update: install the latest package into the DSH web profile.
|
|
418
|
+
const shell = ctx.get?.('shell')
|
|
419
|
+
if (!shell || typeof shell.start !== 'function') throw new Error('no shell service available to update the plugin')
|
|
420
|
+
const profileDir = process.env.DSH_HOME
|
|
421
|
+
? path.join(process.env.DSH_HOME, 'profiles', 'web')
|
|
422
|
+
: path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))
|
|
423
|
+
const proc = shell.start(shell.resolve({
|
|
424
|
+
command: 'pnpm add dsh-comfyui-canvas@latest',
|
|
425
|
+
workdir: profileDir,
|
|
426
|
+
sandboxPolicy: { mode: 'danger-full-access', workspaceRoot: process.cwd() },
|
|
427
|
+
}))
|
|
428
|
+
const outcome = await proc.done
|
|
429
|
+
if (!outcome || (outcome.exitCode ?? outcome.code) !== 0) {
|
|
430
|
+
throw new Error('install failed — see DSH logs')
|
|
431
|
+
}
|
|
432
|
+
setState({
|
|
433
|
+
pluginUpdate: {
|
|
434
|
+
status: 'done',
|
|
435
|
+
latestVersion: latest,
|
|
436
|
+
message: `已更新到 v${latest},重启 DSH 后生效`,
|
|
437
|
+
checkedAt: Date.now(),
|
|
438
|
+
updatedAt: Date.now(),
|
|
439
|
+
},
|
|
440
|
+
})
|
|
441
|
+
} catch (e) {
|
|
442
|
+
setState({
|
|
443
|
+
pluginUpdate: {
|
|
444
|
+
status: 'error',
|
|
445
|
+
latestVersion: '',
|
|
446
|
+
message: `更新失败:${e instanceof Error ? e.message : String(e)}`,
|
|
447
|
+
checkedAt: Date.now(),
|
|
448
|
+
updatedAt: 0,
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
} finally {
|
|
452
|
+
updatingPlugin = false
|
|
453
|
+
}
|
|
454
|
+
})()
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// ComfyUI control watcher: the canvas header's stop/restart buttons set
|
|
458
|
+
// comfyControl with action 'stop' (kill the recorded launch process, or
|
|
459
|
+
// whatever listens on the configured port as a fallback) or 'restart'
|
|
460
|
+
// (kill, then re-enter the launch path). The canvas status dot reflects
|
|
461
|
+
// the outcome through the existing reachability probe.
|
|
462
|
+
let controlling = false
|
|
463
|
+
scope.watch(() => {
|
|
464
|
+
const value = scope.get()
|
|
465
|
+
const ctrl = value?.comfyControl
|
|
466
|
+
if (!ctrl?.requested || controlling) return
|
|
467
|
+
controlling = true
|
|
468
|
+
void (async () => {
|
|
469
|
+
const setState = (patch) => {
|
|
470
|
+
try { scope.update(patch) } catch { /* best effort */ }
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
const action = ctrl.action === 'restart' ? 'restart' : 'stop'
|
|
474
|
+
setState({
|
|
475
|
+
comfyControl: { requested: false, action },
|
|
476
|
+
comfyControlResult: {
|
|
477
|
+
status: action === 'restart' ? 'restarting' : 'stopping',
|
|
478
|
+
message: action === 'restart' ? '正在重启 ComfyUI…' : '正在关闭 ComfyUI…',
|
|
479
|
+
at: Date.now(),
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
const shell = ctx.get?.('shell')
|
|
483
|
+
let killed = false
|
|
484
|
+
if (comfyProc && typeof comfyProc.kill === 'function') {
|
|
485
|
+
killed = comfyProc.kill()
|
|
486
|
+
try { await comfyProc.done } catch { /* already settled */ }
|
|
487
|
+
comfyProc = null
|
|
488
|
+
}
|
|
489
|
+
if (!killed && shell && typeof shell.start === 'function') {
|
|
490
|
+
// Fallback: kill whatever listens on the configured port. Uses
|
|
491
|
+
// netstat + PID parsing because Get-NetTCPConnection can come
|
|
492
|
+
// back empty in some environments even when the port is
|
|
493
|
+
// listening (observed on this host). The command is plain
|
|
494
|
+
// PowerShell: the pwsh-local executor passes it as ONE argv
|
|
495
|
+
// element to `pwsh -Command`, so there is no intermediate shell
|
|
496
|
+
// to double-quote for — wrapping it in another
|
|
497
|
+
// `powershell -NoProfile -Command "..."` would let the outer
|
|
498
|
+
// pwsh interpolate $_ / $pids and break the script.
|
|
499
|
+
let port = '8188'
|
|
500
|
+
try { port = new URL(resolveBase()).port || '8188' } catch { /* keep default */ }
|
|
501
|
+
const killer = shell.start(shell.resolve({
|
|
502
|
+
command: `$pids = netstat -ano | Select-String ':${port}' | Select-String 'LISTENING' | ForEach-Object { ($_ -split '\\s+')[-1] } | Sort-Object -Unique; foreach ($procId in $pids) { Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue }; Write-Output ('killed=' + ($pids | Measure-Object).Count)`,
|
|
503
|
+
sandboxPolicy: { mode: 'danger-full-access', workspaceRoot: process.cwd() },
|
|
504
|
+
}))
|
|
505
|
+
try { await killer.done } catch { /* best effort */ }
|
|
506
|
+
const out = (typeof killer.readOutput === 'function' ? killer.readOutput().delta : '') || ''
|
|
507
|
+
const matched = /killed=(\d+)/.exec(out)
|
|
508
|
+
killed = matched ? Number(matched[1]) > 0 : false
|
|
509
|
+
}
|
|
510
|
+
console.log(`[dsh-comfyui-canvas] comfyControl ${action}: killed=${killed}`)
|
|
511
|
+
if (action === 'restart') {
|
|
512
|
+
// Give the port a moment to release, then re-enter the launch path.
|
|
513
|
+
await new Promise((done) => setTimeout(done, 2500))
|
|
514
|
+
setState({ launchRequested: true })
|
|
515
|
+
setState({ comfyControlResult: { status: 'done', message: 'ComfyUI 已停止,正在重新启动…', at: Date.now() } })
|
|
516
|
+
} else {
|
|
517
|
+
setState({ comfyControlResult: { status: 'done', message: killed ? 'ComfyUI 已关闭' : '未找到运行中的 ComfyUI', at: Date.now() } })
|
|
518
|
+
}
|
|
519
|
+
} catch (e) {
|
|
520
|
+
setState({ comfyControlResult: { status: 'error', message: `操作失败:${e instanceof Error ? e.message : String(e)}`, at: Date.now() } })
|
|
521
|
+
} finally {
|
|
522
|
+
controlling = false
|
|
523
|
+
}
|
|
524
|
+
})()
|
|
525
|
+
})
|
|
312
526
|
} catch {
|
|
313
527
|
// Duplicate registration or unavailable provider — keep defaults.
|
|
314
528
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
package/screenshots.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
[
|
|
2
|
-
"docs/screenshots/01-
|
|
3
|
-
"docs/screenshots/02-
|
|
4
|
-
"docs/screenshots/03-
|
|
2
|
+
"docs/screenshots/01-split-canvas-chat.png",
|
|
3
|
+
"docs/screenshots/02-workflow-dunhuang.png",
|
|
4
|
+
"docs/screenshots/03-output-grid.png",
|
|
5
|
+
"docs/screenshots/04-gallery.png",
|
|
6
|
+
"docs/screenshots/05-batch-portraits.png",
|
|
7
|
+
"docs/screenshots/06-template-library.png"
|
|
5
8
|
]
|
|
Binary file
|
|
File without changes
|
|
File without changes
|