dsh-comfyui-canvas 0.1.9 → 0.1.10

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.
Files changed (3) hide show
  1. package/lib/client.js +204 -26
  2. package/lib/index.js +1835 -1784
  3. package/package.json +1 -1
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 scope && typeof scope.subscribe === "function" ? scope.subscribe(cb) : function () {}; },
528
- function () { return scope && typeof scope.getSnapshot === "function" ? scope.getSnapshot() : null; },
529
- function () { return scope && typeof scope.getSnapshot === "function" ? scope.getSnapshot() : null; },
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 scope && typeof scope.subscribe === "function" ? scope.subscribe(cb) : function () {}; },
785
- function () { return scope && typeof scope.getSnapshot === "function" ? scope.getSnapshot() : null; },
786
- function () { return scope && typeof scope.getSnapshot === "function" ? scope.getSnapshot() : null; },
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 || !scope.set) return;
845
+ if (!writable) return;
813
846
  var next = raw === "" ? null : raw;
814
- scope.set(field, next).catch(function () { /* recover handled by scope */ });
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 || !scope.set) return;
822
- scope.set("updateRequest", { requested: true, action: action }).catch(function () { /* recover handled by scope */ });
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,130 @@ 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
- var inject = ["slots"];
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
+ var sync = function () {
1090
+ // frame.src 常带尾斜杠而 activeBase 不带——裸 !== 会让每次挂载都重置 src
1091
+ // 导致 ComfyUI 重载。归一化后仅当地址真正变化才更新。
1092
+ var want = String(activeBase).replace(/\/+$/, "");
1093
+ var cur = String(frame.src).replace(/\/+$/, "");
1094
+ if (cur !== want) frame.src = activeBase;
1095
+ };
1096
+ var unsub = scope && typeof scope.subscribe === "function" ? scope.subscribe(sync) : memorySubscribe(sync);
1097
+ // 画布标签全屏:隐藏常驻 composer(见 data-dsh-comfyui-fullscreen CSS)。
1098
+ document.documentElement.setAttribute("data-dsh-comfyui-fullscreen", "1");
1099
+ var ro = typeof ResizeObserver === "function" ? new ResizeObserver(fit) : null;
1100
+ if (ro) ro.observe(host);
1101
+ window.addEventListener("resize", fit);
1102
+ return function () {
1103
+ if (typeof unsub === "function") unsub();
1104
+ if (ro) ro.disconnect();
1105
+ window.removeEventListener("resize", fit);
1106
+ document.documentElement.removeAttribute("data-dsh-comfyui-fullscreen");
1107
+ // 仅隐藏,不摘除:iframe 浏览上下文保活,切回秒开。
1108
+ outer.style.display = "none";
1109
+ };
1110
+ }, []);
1111
+ return React.createElement("div", {
1112
+ ref: hostRef,
1113
+ style: { position: "relative", width: "100%", height: "100%", background: "var(--dsw-alias-bg-layer-1,#232324)", overflow: "hidden" },
1114
+ }, React.createElement(CanvasStatusOverlay, { scope: scope, sessionId: props.sessionId || "root" }));
1115
+ }
1116
+
1117
+ var inject = ["slots", "configForms"];
964
1118
 
965
1119
  function apply(ctx) {
966
- // 2026-09-24: 核心 #4587 移除了客户端 settingsScope 服务,且 cordis ctx 代理对
967
- // 未注入服务的属性访问(ctx.settingsScope)会直接抛 "cannot get property...
968
- // without inject",导致客户端 apply 失败。改用 ctx.get()(缺失返回 undefined)。
969
- var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScope") : undefined;
970
- var scope = settingsScopeService && typeof settingsScopeService.bind === "function"
971
- ? settingsScopeService.bind({ namespace: NAMESPACE })
972
- : undefined;
1120
+ // 2026-09-25: DSH 0.1.7 新核心 — 配置经 entry Config(profile 支撑)的
1121
+ // ConfigForm 读写(ctx.configForms.get(NAMESPACE),接口与旧 scope 一致);
1122
+ // 缺失时回退到模块级内存存储(会话内有效)。旧 settingsScope 服务已移除。
1123
+ var configForm = null;
1124
+ if (ctx.configForms && typeof ctx.configForms.get === "function") {
1125
+ try { configForm = ctx.configForms.get(NAMESPACE); } catch (e) { configForm = null; }
1126
+ }
1127
+ var scope = configForm && typeof configForm.subscribe === "function"
1128
+ && typeof configForm.getSnapshot === "function" ? configForm : null;
973
1129
  // 画布顶栏「重启 / 关闭 ComfyUI」按钮 → 写 settings.comfyControl,
974
1130
  // host watcher 执行 kill / restart(结果由可达性探测反映到状态圆点)。
975
1131
  document.addEventListener("dsh-comfyui-control", function (e) {
@@ -980,7 +1136,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
980
1136
  });
981
1137
  // 让画布 iframe 使用设置里的地址(并跟随变更)。
982
1138
  var syncConfig = function () {
983
- var snap = scope ? scope.getSnapshot() : undefined;
1139
+ var snap = scope ? scope.getSnapshot() : memorySnapshot();
984
1140
  var v = snap && snap.value;
985
1141
  var base = v && typeof v.baseUrl === "string" && v.baseUrl
986
1142
  ? v.baseUrl : DEFAULT_BASE;
@@ -991,7 +1147,11 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
991
1147
  var state = canvasState();
992
1148
  if (state.canvas) {
993
1149
  var frame = state.canvas.querySelector("iframe");
994
- if (frame && frame.src !== base) frame.src = base;
1150
+ // frame.src 常带尾斜杠而 base 不带——裸 !== 恒真会把分屏 iframe 每次
1151
+ // 重载;归一化后仅地址真正变化才更新(与标签 sync 同款修复)。
1152
+ if (frame && String(frame.src).replace(/\/+$/, "") !== String(base).replace(/\/+$/, "")) {
1153
+ frame.src = base;
1154
+ }
995
1155
  var link = state.canvas.querySelector("a");
996
1156
  if (link) link.href = base;
997
1157
  }
@@ -999,6 +1159,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
999
1159
  applyRailWidth(v?.railWidth != null ? v.railWidth : DEFAULT_RAIL);
1000
1160
  };
1001
1161
  if (scope) scope.subscribe(syncConfig);
1162
+ else memorySubscribe(syncConfig);
1002
1163
 
1003
1164
  // 全局看门狗:保证分屏标记与真实分屏态一致,杜绝 data-dsh-split-on 残留。
1004
1165
  // 挤压规则只在 html[data-dsh-split-on="1"] 时生效;一旦残留打上该属性,
@@ -1012,7 +1173,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
1012
1173
  return tab ? (tab.textContent || "").trim() : "";
1013
1174
  };
1014
1175
  var enforceSplitMarker = function () {
1015
- var snap = scope ? scope.getSnapshot() : undefined;
1176
+ var snap = scope ? scope.getSnapshot() : memorySnapshot();
1016
1177
  var v = snap && snap.value ? snap.value : {};
1017
1178
  var map = v.splitEnabledBySession || {};
1018
1179
  var anyEnabled = Object.keys(map).some(function (k) { return map[k] === true; });
@@ -1041,6 +1202,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
1041
1202
  };
1042
1203
  var splitWatchdog = null;
1043
1204
  if (scope) scope.subscribe(enforceSplitMarker);
1205
+ else memorySubscribe(enforceSplitMarker);
1044
1206
  if (typeof MutationObserver === "function") {
1045
1207
  splitWatchdog = new MutationObserver(enforceSplitMarker);
1046
1208
  splitWatchdog.observe(
@@ -1065,7 +1227,7 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
1065
1227
  var onNarrow = function () {
1066
1228
  var now = narrowQuery ? narrowQuery.matches : (window.innerWidth < 1200);
1067
1229
  if (now && !narrowWas) {
1068
- var snap = scope ? scope.getSnapshot() : undefined;
1230
+ var snap = scope ? scope.getSnapshot() : memorySnapshot();
1069
1231
  var v = snap && snap.value ? snap.value : {};
1070
1232
  var map = v.splitEnabledBySession || {};
1071
1233
  var anyEnabled = Object.keys(map).some(function (k) { return map[k] === true; });
@@ -1074,6 +1236,8 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
1074
1236
  for (var k in map) cleared[k] = false;
1075
1237
  if (scope && typeof scope.set === "function") {
1076
1238
  scope.set("splitEnabledBySession", cleared).catch(function () { /* recover handled by scope */ });
1239
+ } else {
1240
+ memorySetPath("splitEnabledBySession", cleared);
1077
1241
  }
1078
1242
  // 同步清理 DOM,避免 settings 异步落地前挤压样式/画布残留。
1079
1243
  document.documentElement.removeAttribute("data-dsh-split-on");
@@ -1137,6 +1301,11 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
1137
1301
  ' margin-left: auto !important;',
1138
1302
  ' overflow: hidden !important;',
1139
1303
  '}',
1304
+ // ComfyUI 画布标签全屏:隐藏常驻 composer(ConversationRoot 的
1305
+ // composerSeat 不随视图切换消失,需插件隐藏,否则画布被输入框占底)。
1306
+ 'html[data-dsh-comfyui-fullscreen="1"] [data-composer-seat] {',
1307
+ ' display: none !important;',
1308
+ '}',
1140
1309
  ].join("\n");
1141
1310
  document.head.appendChild(splitStyle);
1142
1311
  }
@@ -1154,10 +1323,19 @@ var settingsScopeService = typeof ctx.get === "function" ? ctx.get("settingsScop
1154
1323
  registerSettingsNavIcon(settingsLabel);
1155
1324
  }
1156
1325
 
1157
- // 分屏入口(唯一):会话标头右侧 utilities 槽(官方 0.1.2 原生插槽)。
1158
- // 不注册 conversation.view 条目——会话标签栏不再有 ComfyUI 标签,画布只在
1159
- // 分屏开启时出现(点本按钮 = 画布左 + 对话 rail 右)。布局完全由插件
1160
- // 覆盖层 + data-* 挤压实现。
1326
+ // ComfyUI 独立标签(第一入口):conversation.view 画布视图(全屏画布)。
1327
+ ctx.slots.inject("conversation.view", function () {
1328
+ return ctx.slots.register({
1329
+ name: "conversation.view",
1330
+ id: "comfyui-canvas-view",
1331
+ order: 20,
1332
+ label: function () { return "ComfyUI"; },
1333
+ inject: function (sessionId) { return { sessionId: String(sessionId), scope: scope }; }
1334
+ }, CanvasTab);
1335
+ });
1336
+
1337
+ // 分屏入口(第二入口,2026-09-25 起与独立标签并存):会话标头右侧 utilities 槽。
1338
+ // 在「对话」标签内开分屏 = 画布左 + 主对话 rail 右(可折叠),与旧版行为一致。
1161
1339
  ctx.slots.inject("conversation.session.header.utilities", function () {
1162
1340
  return ctx.slots.register({
1163
1341
  name: "conversation.session.header.utilities",