dsh-comfyui-canvas 0.1.1

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/client.js ADDED
@@ -0,0 +1,810 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-comfyui-canvas", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ var React = require("react");
4
+
5
+ var NAMESPACE = "dsh-comfyui-canvas";
6
+ var DEFAULT_BASE = "http://127.0.0.1:8188";
7
+ var DEFAULT_RAIL = 360;
8
+ var STATE_KEY = "__dsh_comfyui_state__";
9
+
10
+ // 当前生效的 ComfyUI 地址(默认;设置行更改后由 scope 覆盖)。
11
+ var activeBase = DEFAULT_BASE;
12
+
13
+ // 画布 iframe 永久挂载在 body 上,从不动它:标签切换只改显隐。
14
+ // 激活时用 fixed 定位对齐视图区(含顶栏),卸载时 visibility:hidden,
15
+ // iframe 文档保持存活,切换标签/插件重载都不会重新加载 ComfyUI。
16
+ function canvasState() {
17
+ var state = window[STATE_KEY];
18
+ if (!state) {
19
+ state = {};
20
+ Object.defineProperty(window, STATE_KEY, { value: state, enumerable: false, configurable: true, writable: true });
21
+ }
22
+ return state;
23
+ }
24
+
25
+ // 按会话更新 activeViewBySession 映射(读-改-写整张 map,settings 写入串行安全)。
26
+ // 只改自己 sessionId 的键,其它会话不受影响 → 画布模式感知是会话隔离的。
27
+ function setSessionView(sc, sessionId, mode) {
28
+ var snap = sc.getSnapshot ? sc.getSnapshot() : null;
29
+ var value = snap && snap.value ? snap.value : {};
30
+ var map = {};
31
+ var current = value.activeViewBySession;
32
+ if (current && typeof current === "object") {
33
+ for (var k in current) map[k] = current[k];
34
+ }
35
+ map[String(sessionId)] = mode;
36
+ sc.set("activeViewBySession", map).catch(function () {});
37
+ }
38
+
39
+ function buildCanvas() {
40
+ var outer = document.createElement("div");
41
+ outer.style.cssText = "position:fixed;visibility:hidden;z-index:40;display:flex;flex-direction:column;overflow:hidden;background:#1e1e1e;";
42
+
43
+ var bar = document.createElement("div");
44
+ bar.style.cssText = "flex:none;display:flex;align-items:center;gap:10px;padding:6px 12px;border-bottom:1px solid rgba(128,128,128,.25);font-size:13px;color:#e8e8e8;background:#202024;";
45
+ var title = document.createElement("strong");
46
+ title.textContent = "ComfyUI";
47
+
48
+ // 顶栏右侧动作组(只显示图标,hover 用 title 浮现文字,垂直居中):
49
+ // 状态圆点 + 「刷新画布」按钮 + 「在新标签页打开」外链图标,与标题分居两端。
50
+ var actions = document.createElement("div");
51
+ actions.style.cssText = "margin-left:auto;display:flex;align-items:center;gap:12px;";
52
+
53
+ var status = document.createElement("span");
54
+ status.id = "dsh-comfy-status";
55
+ status.title = "检测中…";
56
+ status.style.cssText = "width:8px;height:8px;border-radius:50%;background:#9a9aa2;display:inline-block;flex:none;";
57
+
58
+ // 刷新画布:重载 iframe(跨域 iframe 不能直接调 contentWindow.location.reload,
59
+ // 给 URL 加时间戳 query 参数触发重新导航)。只重载画布本身,不动 DSH 页面。
60
+ // 用 query 而非 hash:ComfyUI 前端对 location.hash 有内部语义(恢复工作流等)。
61
+ var refresh = document.createElement("button");
62
+ refresh.type = "button";
63
+ refresh.title = "刷新画布";
64
+ refresh.setAttribute("aria-label", "刷新画布");
65
+ refresh.innerHTML = '<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"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>';
66
+ refresh.style.cssText = "background:transparent;border:none;padding:0;color:#8ab4f8;cursor:pointer;display:inline-flex;align-items:center;";
67
+ refresh.addEventListener("click", function () {
68
+ var state = canvasState();
69
+ if (!state.canvas) return;
70
+ var f = state.canvas.querySelector("iframe");
71
+ if (!f) return;
72
+ var url;
73
+ try { url = new URL(f.src || activeBase); } catch (e) { url = null; }
74
+ if (url) {
75
+ url.searchParams.set("_dsh_reload", String(Date.now()));
76
+ f.src = url.toString();
77
+ } else {
78
+ f.src = (f.src || activeBase).split("#")[0] + "?_dsh_reload=" + Date.now();
79
+ }
80
+ });
81
+
82
+ var link = document.createElement("a");
83
+ link.innerHTML = '<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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
84
+ link.href = activeBase;
85
+ link.target = "_blank";
86
+ link.rel = "noreferrer";
87
+ link.title = "在新标签页打开";
88
+ link.style.cssText = "color:#8ab4f8;text-decoration:none;display:inline-flex;align-items:center;";
89
+
90
+ actions.appendChild(status);
91
+ actions.appendChild(refresh);
92
+ actions.appendChild(link);
93
+ bar.appendChild(title);
94
+ bar.appendChild(actions);
95
+
96
+ var frame = document.createElement("iframe");
97
+ frame.src = activeBase;
98
+ frame.title = "ComfyUI 画布";
99
+ frame.setAttribute("allow", "clipboard-read; clipboard-write; fullscreen");
100
+ // 不设 referrerpolicy:iframe 内请求的 Referer 本来就是 ComfyUI 自己的
101
+ // URL(127.0.0.1:8188),不会泄漏 DSH 的 URL;显式 no-referrer 反而会
102
+ // 让节点预览图(/view)与原生标签页行为不一致,导致预览不显示。
103
+ frame.style.cssText = "flex:1 1 0;min-height:0;width:100%;border:0;display:block;background:#1e1e1e;";
104
+
105
+ outer.appendChild(bar);
106
+ outer.appendChild(frame);
107
+ document.body.appendChild(outer);
108
+ return outer;
109
+ }
110
+
111
+ // 同步顶栏状态圆点:颜色 + hover 文字(title)。
112
+ function updateCanvasStatus(online) {
113
+ var state = canvasState();
114
+ if (!state.canvas) return;
115
+ var dot = state.canvas.querySelector("#dsh-comfy-status");
116
+ if (!dot) return;
117
+ if (online === true) {
118
+ dot.style.background = "#22c55e";
119
+ dot.title = "ComfyUI 运行中";
120
+ } else if (online === false) {
121
+ dot.style.background = "#ef4444";
122
+ dot.title = "ComfyUI 未启动";
123
+ } else {
124
+ dot.style.background = "#9a9aa2";
125
+ dot.title = "检测中…";
126
+ }
127
+ }
128
+
129
+ // 对齐宿主视图区并显示;宿主宽高为 0(未渲染)时保持隐藏。
130
+ function positionAt(host, outer) {
131
+ var rect = host.getBoundingClientRect();
132
+ if (rect.width < 8 || rect.height < 8) {
133
+ outer.style.visibility = "hidden";
134
+ return false;
135
+ }
136
+ outer.style.left = rect.left + "px";
137
+ outer.style.top = rect.top + "px";
138
+ outer.style.width = rect.width + "px";
139
+ outer.style.height = rect.height + "px";
140
+ outer.style.visibility = "visible";
141
+ return true;
142
+ }
143
+
144
+ // 把 rail 宽度实时应用到分屏对话栏。
145
+ // 注意:CSS module 类名是 hash 的(如 _1Hmdja_splitRail),不能写死 `.splitRail`,
146
+ // 必须用属性包含匹配 + 标签过滤(主 rail 是 <aside>,其子块是 <div>)。
147
+ function applyRailWidth(width) {
148
+ var w = Number(width);
149
+ if (!(w >= 120 && w <= 1200)) w = DEFAULT_RAIL;
150
+ var all = document.querySelectorAll('[class*="splitRail"]');
151
+ for (var i = 0; i < all.length; i++) {
152
+ var el = all[i];
153
+ if (el.tagName === "ASIDE") {
154
+ el.style.width = w + "px";
155
+ el.style.flex = "none";
156
+ }
157
+ }
158
+ }
159
+
160
+ // 把分屏 rail 的图片预览并入输入框内部:rail composer 默认把图片区
161
+ // (splitRailImages)渲染在 textarea 上方、是独立 flex 子项,视觉上图片
162
+ // 在输入框外面。这里注入 CSS 把整个 composer 变成一个带边框的输入容器,
163
+ // 图片 chip 和 textarea 都在同一个框内。CSS module 类名是 hash 的,用
164
+ // 属性包含匹配命中可读段(与 applyRailWidth 同样的手法)。
165
+ function injectRailComposerStyles() {
166
+ var styleId = "dsh-comfyui-canvas-rail-composer";
167
+ if (document.getElementById(styleId)) return;
168
+ var style = document.createElement("style");
169
+ style.id = styleId;
170
+ style.textContent = [
171
+ '[class*="splitRailComposer"] {',
172
+ ' position: relative !important;',
173
+ ' border: 1px solid var(--dsw-alias-border-l2, #3a3a40) !important;',
174
+ ' border-radius: 10px;',
175
+ ' background: var(--dsw-alias-bg-field, #1f1f23);',
176
+ // 核心注释:rail 右侧 gap 天生比左侧宽约 8px,原始 padding 左12右5 就是
177
+ // 为了补偿。我们覆盖成面板后必须保留这个补偿:右侧 margin 收窄,否则
178
+ // 面板右边缘到侧边栏的距离会明显比左侧宽。
179
+ ' margin: 0 2px 8px 8px;',
180
+ ' padding: 8px 8px 38px 8px;',
181
+ '}',
182
+ // send 按钮从 inputWrap 内部定位提升到 composer 面板:inputWrap 改 static、
183
+ // composer 已设 relative,send 的 absolute 基准就变成面板本身 → 落在真正
184
+ // 的右下角红框处,而不是 textarea 的右下角。
185
+ '[class*="splitRailInputWrap"] {',
186
+ ' position: static !important;',
187
+ '}',
188
+ // textarea 保持透明无边框(面板即边框),并去掉聚焦时的白色高亮外框。
189
+ '[class*="splitRailInputWrap"] [class*="splitRailInput"] {',
190
+ ' border: none !important;',
191
+ ' background: transparent !important;',
192
+ ' box-shadow: none !important;',
193
+ ' outline: none !important;',
194
+ '}',
195
+ '[class*="splitRailInputWrap"] [class*="splitRailInput"]:focus,',
196
+ '[class*="splitRailInputWrap"] [class*="splitRailInput"]:focus-visible {',
197
+ ' outline: none !important;',
198
+ ' border: none !important;',
199
+ ' box-shadow: none !important;',
200
+ '}',
201
+ '[class*="splitRailComposer"] [class*="splitRailSend"] {',
202
+ ' position: absolute !important;',
203
+ ' right: 8px !important;',
204
+ ' bottom: 8px !important;',
205
+ '}',
206
+ ].join("\n");
207
+ document.head.appendChild(style);
208
+ }
209
+
210
+ // 在 rail 图片预览处注入「+」号:点它选本地图片,走 DSH 官方附件通道
211
+ // (构造 paste 事件派发到 rail 的 textarea,其 onPaste 会调用 addDraftImages)。
212
+ // 「+」号放在 .splitRailImages(flex-wrap 图片区)末尾,有图时自动跟随、
213
+ // 多图自动向后移;无图时置于 composer 顶部同一行。MutationObserver 跟随
214
+ // rail 重渲染时图片区的出现/消失。
215
+ function injectRailComposerPlus() {
216
+ var plusId = "dsh-comfyui-canvas-rail-plus";
217
+ if (document.getElementById(plusId)) return;
218
+
219
+ var fileInput = document.createElement("input");
220
+ fileInput.type = "file";
221
+ fileInput.accept = "image/png,image/jpeg,image/webp,image/gif";
222
+ fileInput.multiple = true;
223
+ fileInput.style.display = "none";
224
+ document.body.appendChild(fileInput);
225
+
226
+ var plus = document.createElement("button");
227
+ plus.id = plusId;
228
+ plus.type = "button";
229
+ plus.textContent = "+";
230
+ plus.title = "添加图片";
231
+ plus.setAttribute("aria-label", "添加图片");
232
+ plus.style.cssText = [
233
+ "flex:none;width:28px;height:28px;border:1px dashed var(--dsw-alias-border-l2,#3a3a40);",
234
+ "border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#9a9aa2);",
235
+ "cursor:pointer;font-size:16px;line-height:1;display:grid;place-items:center;margin:2px 0;",
236
+ ].join("");
237
+
238
+ fileInput.addEventListener("change", function () {
239
+ var files = Array.prototype.slice.call(fileInput.files || []);
240
+ fileInput.value = "";
241
+ if (files.length === 0) return;
242
+ var composer = document.querySelector('[class*="splitRailComposer"]');
243
+ var textarea = composer ? composer.querySelector("textarea") : null;
244
+ if (!textarea) return;
245
+ var dt = new DataTransfer();
246
+ for (var i = 0; i < files.length; i++) dt.items.add(files[i]);
247
+ var evt = new ClipboardEvent("paste", { clipboardData: dt, bubbles: true, cancelable: true });
248
+ textarea.dispatchEvent(evt);
249
+ });
250
+
251
+ plus.addEventListener("click", function () { fileInput.click(); });
252
+
253
+ var place = function () {
254
+ var composer = document.querySelector('[class*="splitRailComposer"]');
255
+ if (!composer) return;
256
+ var images = composer.querySelector('[class*="splitRailImages"]');
257
+ if (images) {
258
+ // 有图:跟随图片区末尾,恢复静态布局(去掉绝对定位)。
259
+ plus.style.position = "";
260
+ plus.style.left = "";
261
+ plus.style.bottom = "";
262
+ // React 不认识手动插入的「+」号:新增图片时会 append 到图片区末尾
263
+ // (落在「+」号后面),所以只要「+」不是容器最后一个子节点就移到末尾,
264
+ // 保证顺序永远是 [图…][+]。
265
+ if (plus.parentNode !== images || images.lastElementChild !== plus) {
266
+ images.appendChild(plus);
267
+ }
268
+ } else {
269
+ // 无图:「+」号绝对定位到面板左下角,与右下角的发送按钮同一水平线。
270
+ plus.style.position = "absolute";
271
+ plus.style.left = "8px";
272
+ plus.style.bottom = "8px";
273
+ if (plus.parentNode !== composer) composer.appendChild(plus);
274
+ }
275
+ };
276
+
277
+ place();
278
+
279
+ // 只在 rail 相关的 DOM 变化时才重排「+」号,并用短节流合并突发批次,
280
+ // 避免繁忙页面里每次 body 变异(聊天滚动、侧栏重绘等)都跑一遍 place()。
281
+ var inRail = function (node) {
282
+ return !!node && typeof node.closest === "function" && !!node.closest('[class*="splitRail"]');
283
+ };
284
+ var affectsRail = function (mutation) {
285
+ if (inRail(mutation.target)) return true;
286
+ for (var i = 0; i < mutation.addedNodes.length; i++) {
287
+ if (inRail(mutation.addedNodes[i])) return true;
288
+ }
289
+ for (var j = 0; j < mutation.removedNodes.length; j++) {
290
+ if (inRail(mutation.removedNodes[j])) return true;
291
+ }
292
+ return false;
293
+ };
294
+ var pending = null;
295
+ var schedulePlace = function () {
296
+ if (pending) return;
297
+ pending = setTimeout(function () {
298
+ pending = null;
299
+ place();
300
+ }, 50);
301
+ };
302
+ var observer = new MutationObserver(function (mutations) {
303
+ for (var i = 0; i < mutations.length; i++) {
304
+ if (affectsRail(mutations[i])) { schedulePlace(); return; }
305
+ }
306
+ });
307
+ observer.observe(document.body, { childList: true, subtree: true });
308
+ }
309
+
310
+ // 设置导航图标(非侵入式,不改 DSH 核心):
311
+ // DSH 0.1.x 的 settings.section 只投影 id/order/label,图标由设置壳从内置 id
312
+ // 列表里选(外部 section 落到通用齿轮)。这里仿 dsh-better-sidebar 的做法:
313
+ // 用 MutationObserver 找到「文字 == 本插件 section label」的导航按钮,打上
314
+ // 标记,再用本插件 CSS 的 SVG mask 把那个齿轮替换成 ComfyUI 方形 C 图标。
315
+ // 不碰 DSH 核心文件;dispose 时移除标记与 observer,HMR 安全。
316
+ const SETTINGS_NAV_MARKER = "data-dsh-comfyui-canvas-settings-nav";
317
+
318
+ // ComfyUI 方形 C 图标(两个斜切圆角块),单色 currentColor。
319
+ // URL 编码的 SVG path(viewBox 30 28 122 126),供 CSS mask 使用。
320
+ const COMFYUI_NAV_MASK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='30 28 122 126'%3E%3Cpath fill='black' d='M88.218 35.663C83.809 37.836 81.579 41.233 79.462 49C76.672 59.236 76.92 59 68.933 59C61.118 59 56.643 60.502 53.141 64.301C50.092 67.61 36.74 113.081 37.595 117.249C38.465 121.495 41.516 123 49.254 123C57.436 123 57.918 123.669 55.621 131.853C53.473 139.511 53.541 144.274 55.829 146.345C57.426 147.791 60.77 148 82.252 148H106.847L109.959 145.381C113.987 141.991 115.125 139.591 118.435 127.5C121.512 116.26 121.09 112.745 116.453 110.982C115.032 110.442 105.847 110 96.042 110C81.448 110 78.114 109.739 77.661 108.56C77.065 107.006 85.703 75.314 87.286 73.25C87.995 72.325 93.352 71.999 107.872 71.996C134.256 71.991 134.414 71.897 139.945 52.769C145.339 34.114 145.174 33.992 114.527 34.023C95.367 34.042 90.949 34.317 88.218 35.663Z'/%3E%3C/svg%3E";
321
+
322
+ /** 注入替换设置导航图标的 CSS(隐藏核心齿轮、用 mask 画 C)。 */
323
+ function injectSettingsNavIconStyles() {
324
+ var styleId = "dsh-comfyui-canvas-settings-nav-css";
325
+ if (document.getElementById(styleId)) return;
326
+ var style = document.createElement("style");
327
+ style.id = styleId;
328
+ style.textContent = [
329
+ "[" + SETTINGS_NAV_MARKER + "] > svg:first-child { display: none; }",
330
+ "[" + SETTINGS_NAV_MARKER + "]::before {",
331
+ " content: '';",
332
+ " flex: none;",
333
+ " width: 16px;",
334
+ " height: 16px;",
335
+ " background: currentColor;",
336
+ " -webkit-mask: url(\"" + COMFYUI_NAV_MASK + "\") center / contain no-repeat;",
337
+ " mask: url(\"" + COMFYUI_NAV_MASK + "\") center / contain no-repeat;",
338
+ "}",
339
+ ].join("\n");
340
+ document.head.appendChild(style);
341
+ }
342
+
343
+ /**
344
+ * 标记本插件在设置导航中的那一行(按 label 文字匹配),配合上面的 CSS
345
+ * 把该行的核心齿轮替换成 ComfyUI C 图标。
346
+ * @param label - locale-aware section label 解析函数。
347
+ * @returns disposer:断开观察并移除本插件打下的所有标记。
348
+ */
349
+ function registerSettingsNavIcon(label) {
350
+ var disposed = false;
351
+ var sync = function () {
352
+ if (disposed) return;
353
+ var current = String(label() || "").trim();
354
+ var buttons = document.querySelectorAll('[role="dialog"] nav button');
355
+ for (var i = 0; i < buttons.length; i++) {
356
+ var text = (buttons[i].textContent || "").trim();
357
+ if (current.length > 0 && text === current) {
358
+ buttons[i].setAttribute(SETTINGS_NAV_MARKER, "");
359
+ } else {
360
+ buttons[i].removeAttribute(SETTINGS_NAV_MARKER);
361
+ }
362
+ }
363
+ };
364
+ sync();
365
+ var observer = new MutationObserver(sync);
366
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
367
+ return function () {
368
+ disposed = true;
369
+ observer.disconnect();
370
+ var marked = document.querySelectorAll("[" + SETTINGS_NAV_MARKER + "]");
371
+ for (var i = 0; i < marked.length; i++) marked[i].removeAttribute(SETTINGS_NAV_MARKER);
372
+ };
373
+ }
374
+
375
+ // 分屏画布视图下 DSH 核心不渲染 conversation.composer 链条,授权弹窗
376
+ // 永远不会出现。这里由插件自己渲染:读会话 pending 里的 approval 项,
377
+ // 覆盖在画布上,允许一次 / 拒绝,通过 wait.respond 回传。
378
+ function ComfyUIApprovalOverlay(props) {
379
+ var wait = props.wait;
380
+ var payload = wait && wait.payload ? wait.payload : {};
381
+ var answered = React.useState(false);
382
+ var isAnswered = answered[0];
383
+ var setAnswered = answered[1];
384
+
385
+ function answer(outcome) {
386
+ if (isAnswered || !wait) return;
387
+ setAnswered(true);
388
+ var value = {
389
+ sessionId: wait.sessionId,
390
+ approvalId: payload.approvalId,
391
+ outcome: outcome,
392
+ };
393
+ wait.respond({ ok: true, value: value }).then(
394
+ function () { /* resolved frame 会自动把它从 pending 移除 */ },
395
+ function () { setAnswered(false); },
396
+ );
397
+ }
398
+
399
+ var wrapStyle = {
400
+ // 绝对定位于 host 内部(host 为 relative):只盖画布视图区,不再整屏
401
+ // fixed 遮挡顶部会话标签栏,待审批时仍可切换会话标签。
402
+ position: "absolute", left: 0, top: 0, right: 0, bottom: 0,
403
+ zIndex: 80, display: "flex", alignItems: "center", justifyContent: "center",
404
+ background: "rgba(0,0,0,0.35)", padding: "24px",
405
+ };
406
+ var cardStyle = {
407
+ width: "min(520px, 100%)", maxHeight: "80%", overflow: "auto",
408
+ background: "var(--dsw-alias-bg-base, #202024)", color: "var(--dsw-alias-label-primary, #e8e8e8)",
409
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.25))",
410
+ borderRadius: "12px", boxShadow: "0 8px 32px rgba(0,0,0,.4)", fontFamily: "inherit",
411
+ };
412
+ var stripStyle = {
413
+ display: "flex", alignItems: "center", gap: "8px",
414
+ padding: "10px 14px", borderBottom: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.25))",
415
+ fontSize: "13px", fontWeight: "600", color: "#b8860b",
416
+ };
417
+ var bodyStyle = { padding: "14px", fontSize: "13px", lineHeight: 1.6 };
418
+ var headStyle = { fontSize: "14px", fontWeight: "600", marginBottom: "8px", whiteSpace: "pre-wrap", wordBreak: "break-word" };
419
+ var subStyle = { color: "var(--dsw-alias-label-secondary, #9a9aa2)", marginBottom: "12px", wordBreak: "break-word" };
420
+ var actionStyle = {
421
+ display: "flex", gap: "10px", justifyContent: "flex-end",
422
+ padding: "0 14px 14px",
423
+ };
424
+ var btnBase = {
425
+ border: "none", borderRadius: "8px", padding: "8px 16px", fontSize: "13px",
426
+ fontWeight: "600", cursor: "pointer",
427
+ };
428
+
429
+ return React.createElement("div", { style: wrapStyle },
430
+ React.createElement("div", { style: cardStyle },
431
+ React.createElement("div", { style: stripStyle },
432
+ React.createElement("span", { style: { width: 8, height: 8, borderRadius: "50%", background: "#b8860b", display: "inline-block" } }),
433
+ "等待审批",
434
+ ),
435
+ React.createElement("div", { style: bodyStyle },
436
+ React.createElement("div", { style: headStyle },
437
+ payload.reason || ("工具 " + (payload.toolName || "未知") + " 请求越权执行"),
438
+ ),
439
+ payload.toolName && React.createElement("div", { style: subStyle }, "工具:" + payload.toolName),
440
+ ),
441
+ React.createElement("div", { style: actionStyle },
442
+ React.createElement("button", {
443
+ style: Object.assign({}, btnBase, {
444
+ background: "transparent", color: "var(--dsw-alias-label-secondary, #9a9aa2)",
445
+ border: "1px solid var(--dsw-alias-border-l2, #3a3a40)",
446
+ }),
447
+ disabled: isAnswered,
448
+ onClick: function () { answer("rejected"); },
449
+ }, "拒绝"),
450
+ React.createElement("button", {
451
+ style: Object.assign({}, btnBase, {
452
+ background: "var(--dsw-alias-state-business-primary, #3b82f6)", color: "#fff",
453
+ }),
454
+ disabled: isAnswered,
455
+ onClick: function () { answer("allowed-once"); },
456
+ }, "允许一次"),
457
+ ),
458
+ ),
459
+ );
460
+ }
461
+
462
+ // ComfyUI 控制条:
463
+ // - 在线:不渲染任何覆盖层——状态由顶栏右侧圆点图标呈现(hover 显示
464
+ // 「ComfyUI 运行中」),画布完全可见。
465
+ // - 离线/检测中:居中启动卡片,仅覆盖画布视图区(绝对定位于 host 内),
466
+ // 不遮挡顶部会话标签栏,可正常切换会话标签。
467
+ function ComfyUIControl(props) {
468
+ var online = props.online;
469
+ // 离线/检测中的启动卡片仍需要这两个:starting 控制按钮文案/禁用态,
470
+ // onStart 触发启动。
471
+ var starting = props.starting === true;
472
+ var onStart = props.onStart;
473
+ // 上次启动失败的原因(host 写入 settings.launchError),启动卡片展示,
474
+ // 避免用户只看到"正在启动…"卡死而不知道哪一步失败了。
475
+ var error = typeof props.error === "string" && props.error ? props.error : "";
476
+
477
+ // 在线:状态已并入顶栏圆点,这里不再渲染任何遮挡层。
478
+ if (online === true) return null;
479
+
480
+ // 离线/检测中:居中启动卡片。
481
+ var wrapStyle = {
482
+ position: "absolute", left: 0, top: 0, right: 0, bottom: 0,
483
+ zIndex: 79, display: "flex", alignItems: "center", justifyContent: "center",
484
+ background: "rgba(0,0,0,0.45)", padding: "24px",
485
+ };
486
+ var cardStyle = {
487
+ width: "min(460px, 100%)", padding: "20px",
488
+ background: "var(--dsw-alias-bg-base, #202024)", color: "var(--dsw-alias-label-primary, #e8e8e8)",
489
+ border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.25))",
490
+ borderRadius: "12px", boxShadow: "0 8px 32px rgba(0,0,0,.4)", fontFamily: "inherit",
491
+ };
492
+ var btnStyle = {
493
+ border: "none", borderRadius: "8px", padding: "10px 18px", fontSize: "13px",
494
+ fontWeight: "600", cursor: "pointer",
495
+ background: "var(--dsw-alias-state-business-primary, #3b82f6)", color: "#fff",
496
+ };
497
+ var errStyle = {
498
+ fontSize: "12px", color: "#ef4444", lineHeight: 1.5, marginTop: "12px",
499
+ wordBreak: "break-word", whiteSpace: "pre-wrap",
500
+ };
501
+ var detecting = online === null;
502
+ return React.createElement("div", { style: wrapStyle },
503
+ React.createElement("div", { style: cardStyle },
504
+ React.createElement("div", { style: { fontSize: "14px", fontWeight: 600, marginBottom: "8px" } }, "ComfyUI 未启动"),
505
+ React.createElement("div", { style: { fontSize: "13px", color: "var(--dsw-alias-label-secondary, #9a9aa2)", lineHeight: 1.6, marginBottom: "16px" } },
506
+ "检测到 ComfyUI 服务未运行。点击下方按钮会用设置里的「启动命令」后台拉起 ComfyUI,启动完成后画布自动加载。"
507
+ ),
508
+ error ? React.createElement("div", { style: errStyle }, error) : null,
509
+ React.createElement("button", {
510
+ style: btnStyle, disabled: starting || detecting,
511
+ onClick: function () { if (onStart) onStart(); },
512
+ }, starting ? "正在启动…" : detecting ? "检测中…" : "启动 ComfyUI"),
513
+ ),
514
+ );
515
+ }
516
+
517
+ function ComfyUICanvasView(props) {
518
+ var sessionId = String(props.sessionId ?? "root");
519
+ var rootRef = React.useRef(null);
520
+ // 读取当前会话的 pending 授权项:DSH 核心在分屏布局下不渲染
521
+ // conversation.composer 链条(授权弹窗所在),这里由画布视图自己接管。
522
+ // useSession 由 runtime 作为 session-scope 标准 props 注入。
523
+ var pending = typeof props.useSession === "function"
524
+ ? (props.useSession(function (s) { return s && s.pending ? s.pending : []; }) || [])
525
+ : [];
526
+ var approval = null;
527
+ for (var i = 0; i < pending.length; i++) {
528
+ if (pending[i] && pending[i].kind === "approval") { approval = pending[i]; break; }
529
+ }
530
+ // 上报当前激活视图:本组件在 ComfyUI 分屏画布标签激活时才被渲染,
531
+ // 挂载 = 用户正看着画布,卸载 = 切回普通对话。写入共享 settings 里
532
+ // 按会话 id 的映射 activeViewBySession(只改自己的 sessionId 键),
533
+ // host 端 comfyui_config 工具据此让 agent 感知画布专注模式,且会话隔离。
534
+ React.useEffect(function () {
535
+ var sc = props.scope;
536
+ if (sc && typeof sc.set === "function") {
537
+ setSessionView(sc, sessionId, "canvas");
538
+ }
539
+ return function () {
540
+ if (sc && typeof sc.set === "function") {
541
+ setSessionView(sc, sessionId, "chat");
542
+ }
543
+ };
544
+ }, [sessionId]);
545
+ React.useEffect(function () {
546
+ var host = rootRef.current;
547
+ if (!host) return;
548
+ var state = canvasState();
549
+ if (!state.canvas) state.canvas = buildCanvas();
550
+ var outer = state.canvas;
551
+ var observer = null;
552
+ positionAt(host, outer);
553
+ if (typeof ResizeObserver === "function") {
554
+ observer = new ResizeObserver(function () {
555
+ if (host.isConnected) positionAt(host, outer);
556
+ });
557
+ observer.observe(host);
558
+ }
559
+ return function () {
560
+ if (observer) observer.disconnect();
561
+ outer.style.visibility = "hidden";
562
+ };
563
+ }, [sessionId]);
564
+
565
+ // ComfyUI 可达性检测:每隔几秒探一下。注意用 mode:'no-cors' —— DSH 页面
566
+ // (127.0.0.1:3080) 跨域探测 ComfyUI (127.0.0.1:8188) 会被 CORS 拦截,no-cors
567
+ // 只要服务可达就 resolve(opaque),不可达才 reject,适合纯可达性探测。
568
+ var onlineState = React.useState(null); // null=检测中, true=在线, false=离线
569
+ var online = onlineState[0];
570
+ var setOnline = onlineState[1];
571
+ React.useEffect(function () {
572
+ var stopped = false;
573
+ var timer = null;
574
+ // 同步顶栏状态圆点(初始检测中)。
575
+ updateCanvasStatus(null);
576
+ var probe = function () {
577
+ fetch(activeBase + "/system_stats", { method: "GET", mode: "no-cors", signal: AbortSignal.timeout(2500) })
578
+ .then(function () {
579
+ if (stopped) return;
580
+ setOnline(true);
581
+ setStarting(false); // 服务已可达,清掉"正在启动"态
582
+ updateCanvasStatus(true);
583
+ })
584
+ .catch(function () {
585
+ if (stopped) return;
586
+ setOnline(false);
587
+ updateCanvasStatus(false);
588
+ });
589
+ };
590
+ probe();
591
+ timer = setInterval(probe, 4000);
592
+ return function () {
593
+ stopped = true;
594
+ if (timer) clearInterval(timer);
595
+ };
596
+ }, []);
597
+
598
+ // 请求 host 启动 ComfyUI(写 launchRequested=true,host watcher 负责拉起)。
599
+ var startState = React.useState(false);
600
+ var isStarting = startState[0];
601
+ var setStarting = startState[1];
602
+ // 上次启动失败的原因(host 写入 settings.launchError)。随 scope 订阅更新,
603
+ // 启动卡片据此展示具体失败原因,而不是让用户一直停在"正在启动…"。
604
+ var errorState = React.useState("");
605
+ var launchError = errorState[0];
606
+ var setLaunchError = errorState[1];
607
+ React.useEffect(function () {
608
+ var sc = props.scope;
609
+ if (!sc || typeof sc.subscribe !== "function" || typeof sc.getSnapshot !== "function") return;
610
+ var sync = function () {
611
+ var snap = sc.getSnapshot();
612
+ var v = snap && snap.value ? snap.value : {};
613
+ setLaunchError(typeof v.launchError === "string" ? v.launchError : "");
614
+ };
615
+ sync();
616
+ var unsub = sc.subscribe(sync);
617
+ return function () { if (typeof unsub === "function") unsub(); };
618
+ }, []);
619
+ var requestLaunch = function () {
620
+ var sc = props.scope;
621
+ if (!sc || typeof sc.set !== "function") return;
622
+ setStarting(true);
623
+ sc.set("launchRequested", true).catch(function () {
624
+ setStarting(false);
625
+ });
626
+ };
627
+
628
+ // 启动超时:host 用 launchCommand 拉起 ComfyUI 通常几十秒内完成;超过
629
+ // 45 秒仍未探测到在线就复位"正在启动…",允许用户重试,避免永久卡死。
630
+ React.useEffect(function () {
631
+ if (!isStarting) return;
632
+ var timer = setTimeout(function () {
633
+ setStarting(false);
634
+ }, 45000);
635
+ return function () { clearTimeout(timer); };
636
+ }, [isStarting]);
637
+
638
+ // 控制条常驻:离线显示启动卡片、在线显示右上角状态提示。
639
+ // 渲染在 host 内部(host 为 relative),绝对定位只覆盖画布视图区,
640
+ // 不再整屏 fixed 遮挡顶部会话标签栏,可正常切换会话标签。
641
+ // 重启不做 UI 按钮——由智能体在画布内通过自定义节点操作。
642
+ var control = React.createElement(ComfyUIControl, {
643
+ online: online, starting: isStarting, onStart: requestLaunch, error: launchError,
644
+ });
645
+ // 授权弹窗渲染在 host 内部(与控制条同级),absolute 定位锚定 host 的
646
+ // relative 边界 → 只盖画布视图区,不会挡住顶栏/右侧 rail。
647
+ var hostChildren = [control];
648
+ if (approval) {
649
+ hostChildren.push(React.createElement(ComfyUIApprovalOverlay, { wait: approval }));
650
+ }
651
+ var hostEl = React.createElement("div", {
652
+ ref: rootRef, style: { flex: "1 1 0", minHeight: 0, minWidth: 0, position: "relative" },
653
+ }, hostChildren);
654
+ return hostEl;
655
+ }
656
+
657
+ // ------- M3: Settings section —— 左侧导航独立页 “ComfyUI 画布” -------
658
+
659
+ function ComfyUISettingsRow(props) {
660
+ var scope = props.scope;
661
+ var snapshot = React.useSyncExternalStore(
662
+ function (cb) { return scope.subscribe(cb); },
663
+ function () { return scope.getSnapshot(); },
664
+ );
665
+ var value = (snapshot && snapshot.value) || {};
666
+ var baseUrl = typeof value.baseUrl === "string" && value.baseUrl ? value.baseUrl : DEFAULT_BASE;
667
+ var port = value.port != null ? String(value.port) : "";
668
+ var networkMode = typeof value.networkMode === "string" && value.networkMode ? value.networkMode : "loopback";
669
+ var bridgeToken = typeof value.bridgeToken === "string" ? value.bridgeToken : "";
670
+ var launchCommand = typeof value.launchCommand === "string" ? value.launchCommand : "";
671
+ var comfyuiDir = typeof value.comfyuiDir === "string" ? value.comfyuiDir : "";
672
+ var railWidth = value.railWidth != null ? Number(value.railWidth) : DEFAULT_RAIL;
673
+ var writable = !snapshot || snapshot.writable !== false;
674
+
675
+ var inputStyle = {
676
+ width: "100%", boxSizing: "border-box", background: "#1f1f23", color: "#e8e8e8",
677
+ border: "1px solid #3a3a40", borderRadius: "6px", padding: "6px 8px",
678
+ fontSize: "13px", fontFamily: "inherit", marginTop: "4px",
679
+ };
680
+ var labelStyle = { fontSize: "12px", color: "#9a9aa2", marginTop: "10px", display: "block" };
681
+ var rowStyle = { padding: "12px", display: "flex", flexDirection: "column", gap: "2px" };
682
+ var titleStyle = { fontSize: "13px", fontWeight: "600" };
683
+
684
+ function setField(field, raw) {
685
+ if (!writable || !scope.set) return;
686
+ var next = raw === "" ? null : raw;
687
+ scope.set(field, next).catch(function () { /* recover handled by scope */ });
688
+ }
689
+
690
+ return React.createElement("div", { style: rowStyle },
691
+ React.createElement("div", { style: titleStyle }, "ComfyUI 画布"),
692
+ React.createElement("label", { style: labelStyle }, "ComfyUI 地址"),
693
+ React.createElement("input", {
694
+ style: inputStyle, value: baseUrl, disabled: !writable,
695
+ spellCheck: false, placeholder: DEFAULT_BASE,
696
+ onChange: function (e) { setField("baseUrl", e.target.value); },
697
+ }),
698
+ React.createElement("label", { style: labelStyle }, "端口(可选,留空用地址里的)"),
699
+ React.createElement("input", {
700
+ style: inputStyle, value: port, disabled: !writable,
701
+ spellCheck: false, placeholder: "8188",
702
+ onChange: function (e) { setField("port", e.target.value === "" ? "" : Number(e.target.value)); },
703
+ }),
704
+ React.createElement("label", { style: labelStyle }, "网络模式"),
705
+ React.createElement("select", {
706
+ style: inputStyle, value: networkMode, disabled: !writable,
707
+ onChange: function (e) { setField("networkMode", e.target.value); },
708
+ },
709
+ React.createElement("option", { value: "loopback" }, "本地回环 (127.0.0.1)"),
710
+ React.createElement("option", { value: "lan" }, "局域网"),
711
+ React.createElement("option", { value: "cloud-selfhosted" }, "云端自部署"),
712
+ React.createElement("option", { value: "saas" }, "托管 SaaS"),
713
+ ),
714
+ React.createElement("label", { style: labelStyle }, "桥接 Token(可选,与 ComfyUI 端 DSH_BRIDGE_TOKEN 一致,留空不鉴权)"),
715
+ React.createElement("input", {
716
+ style: inputStyle, value: bridgeToken, disabled: !writable,
717
+ type: "password", spellCheck: false, autoComplete: "off",
718
+ placeholder: "留空 = 关闭鉴权(同 ComfyUI 默认信任模型)",
719
+ onChange: function (e) { setField("bridgeToken", e.target.value); },
720
+ }),
721
+ React.createElement("label", { style: labelStyle }, "启动命令(启动按钮 / agent 自启)"),
722
+ React.createElement("input", {
723
+ style: inputStyle, value: launchCommand, disabled: !writable,
724
+ spellCheck: false, placeholder: "例如 ComfyUI启动器.bat",
725
+ onChange: function (e) { setField("launchCommand", e.target.value); },
726
+ }),
727
+ React.createElement("label", { style: labelStyle }, "ComfyUI 安装目录(升级 / 启动用,留空则用环境变量)"),
728
+ React.createElement("input", {
729
+ style: inputStyle, value: comfyuiDir, disabled: !writable,
730
+ spellCheck: false, placeholder: "例如 C:\\path\\to\\ComfyUI",
731
+ onChange: function (e) { setField("comfyuiDir", e.target.value); },
732
+ }),
733
+ React.createElement("label", { style: labelStyle }, "右侧面板宽度 (px)(画布宽度 = 总宽 − 右侧面板)"),
734
+ React.createElement("input", {
735
+ style: inputStyle, value: String(railWidth), disabled: !writable,
736
+ type: "number", min: 120, max: 1200, step: 10,
737
+ onChange: function (e) { setField("railWidth", Number(e.target.value)); },
738
+ }),
739
+ );
740
+ }
741
+
742
+ var inject = ["slots", "settingsScope"];
743
+
744
+ function apply(ctx) {
745
+ var scope = ctx.settingsScope.bind({ namespace: NAMESPACE });
746
+ // 让画布 iframe 使用设置里的地址(并跟随变更)。
747
+ var syncConfig = function () {
748
+ var snap = scope.getSnapshot();
749
+ var v = snap && snap.value;
750
+ var base = v && typeof v.baseUrl === "string" && v.baseUrl
751
+ ? v.baseUrl : DEFAULT_BASE;
752
+ if (v && v.port != null && v.port !== "" && /^\d+$/.test(String(v.port))) {
753
+ base = base.replace(/:\d+$/, "") + ":" + String(v.port);
754
+ }
755
+ activeBase = base;
756
+ var state = canvasState();
757
+ if (state.canvas) {
758
+ var frame = state.canvas.querySelector("iframe");
759
+ if (frame && frame.src !== base) frame.src = base;
760
+ var link = state.canvas.querySelector("a");
761
+ if (link) link.href = base;
762
+ }
763
+ // 实时应用右侧面板宽度。
764
+ applyRailWidth(v?.railWidth != null ? v.railWidth : DEFAULT_RAIL);
765
+ };
766
+ scope.subscribe(syncConfig);
767
+ syncConfig();
768
+
769
+ // 分屏 rail 图片预览并入输入框内部(CSS 覆盖,不动 DSH 核心)。
770
+ injectRailComposerStyles();
771
+ // rail 图片预览处注入「+」号(选本地图片,走 DSH 官方附件通道)。
772
+ injectRailComposerPlus();
773
+
774
+ // 设置导航图标(非侵入式):注入替换 CSS + 按 label 标记自己的导航行。
775
+ // 用 ctx.effect 包装,插件重载/卸载时自动 dispose,HMR 安全。
776
+ var settingsLabel = function () { return "ComfyUI 画布"; };
777
+ if (ctx.effect) {
778
+ ctx.effect(function () {
779
+ injectSettingsNavIconStyles();
780
+ return registerSettingsNavIcon(settingsLabel);
781
+ }, "dsh-comfyui-canvas: settings navigation icon");
782
+ } else {
783
+ injectSettingsNavIconStyles();
784
+ registerSettingsNavIcon(settingsLabel);
785
+ }
786
+
787
+ ctx.slots.inject("conversation.view", function () {
788
+ return ctx.slots.register({
789
+ name: "conversation.view",
790
+ id: "comfyui-canvas",
791
+ order: 30,
792
+ label: function () { return "ComfyUI"; },
793
+ meta: { split: true },
794
+ inject: function (sessionId) { return { sessionId: String(sessionId), scope: scope }; }
795
+ }, ComfyUICanvasView);
796
+ });
797
+
798
+ ctx.slots.inject("settings.section", function () {
799
+ return ctx.slots.register({
800
+ name: "settings.section",
801
+ id: "comfyui-canvas",
802
+ order: 40,
803
+ label: settingsLabel,
804
+ inject: function () { return { scope: scope }; },
805
+ }, ComfyUISettingsRow);
806
+ });
807
+ }
808
+
809
+ module.exports = { apply: apply, inject: inject };
810
+ return module.exports; } });