dsh-session-conductor 0.0.0-stage → 1.0.9

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 (68) hide show
  1. package/README.md +165 -2
  2. package/cordis.patch.yml +48 -0
  3. package/lib/apply.js +1361 -0
  4. package/lib/client-parts/apply.js +43 -0
  5. package/lib/client-parts/components/compaction.js +124 -0
  6. package/lib/client-parts/components/conductor-settings.js +62 -0
  7. package/lib/client-parts/components/group.js +115 -0
  8. package/lib/client-parts/components/panel.js +1265 -0
  9. package/lib/client-parts/components/processing.js +98 -0
  10. package/lib/client-parts/components/session-icon.js +20 -0
  11. package/lib/client-parts/components/settings.js +415 -0
  12. package/lib/client-parts/foundation/bootstrap.js +48 -0
  13. package/lib/client-parts/foundation/i18n.js +74 -0
  14. package/lib/client-parts/foundation/list-cache.js +28 -0
  15. package/lib/client-parts/foundation/styles.js +236 -0
  16. package/lib/client.js +2534 -0
  17. package/lib/core/config.js +159 -0
  18. package/lib/core/domain.js +157 -0
  19. package/lib/core/lazy.js +20 -0
  20. package/lib/core/state.js +106 -0
  21. package/lib/core.js +82 -0
  22. package/lib/eio-repair.js +204 -0
  23. package/lib/features/context-usage/index.js +219 -0
  24. package/lib/features/continue/eligibility.js +49 -0
  25. package/lib/features/continue/scan.js +134 -0
  26. package/lib/features/continue/session.js +235 -0
  27. package/lib/features/inject/preset.js +20 -0
  28. package/lib/features/member-model/index.js +14 -0
  29. package/lib/features/member-model/locate.js +157 -0
  30. package/lib/features/member-model/override.js +76 -0
  31. package/lib/features/member-model/setup.js +55 -0
  32. package/lib/features/member-model/switch.js +186 -0
  33. package/lib/features/rename/analysis.js +303 -0
  34. package/lib/features/rename/title.js +158 -0
  35. package/lib/features/rename/value.js +77 -0
  36. package/lib/features/search.js +119 -0
  37. package/lib/features/session-ops-routes.js +218 -0
  38. package/lib/features/session-ops.js +414 -0
  39. package/lib/group.js +303 -0
  40. package/lib/guards.js +28 -0
  41. package/lib/http.js +24 -0
  42. package/lib/i18n/en.json +204 -0
  43. package/lib/i18n/zh.json +204 -0
  44. package/lib/index.js +78 -0
  45. package/lib/repair.js +398 -0
  46. package/lib/seq-gap-repair.js +224 -0
  47. package/lib/session-codec.js +37 -0
  48. package/lib/session-log.js +46 -0
  49. package/lib/sessions/detach.js +113 -0
  50. package/lib/sessions/events.js +73 -0
  51. package/lib/sessions/interruption.js +73 -0
  52. package/lib/sessions/list-cache.js +175 -0
  53. package/lib/sessions/list.js +187 -0
  54. package/lib/sessions/route.js +60 -0
  55. package/lib/sessions/turn.js +21 -0
  56. package/lib/shared/constants.js +20 -0
  57. package/lib/shared/log.js +17 -0
  58. package/lib/shared/settings.js +14 -0
  59. package/lib/shared/test-hooks.js +93 -0
  60. package/lib/shared/util.js +6 -0
  61. package/lib/template-inject.js +289 -0
  62. package/lib/value.js +430 -0
  63. package/lib/zstd-frames.js +177 -0
  64. package/package.json +65 -3
  65. package/skills/dsh-session-conductor-functions.md +197 -0
  66. package/skills/dsh-session-conductor.md +151 -0
  67. package/skills/dsh-session-official-doc.md +245 -0
  68. package/skills/task-completion-report.md +56 -0
@@ -0,0 +1,98 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-session-conductor-processing",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+
11
+ const NS = "sessionConductorProcessing";
12
+ const zh = {};
13
+ const en = {};
14
+ __scFillI18nDict("sessionConductorProcessing", zh, en);
15
+
16
+ /** 预览文本上限(字符):提示条只展示开头一段,避免撑高。 */
17
+ const PREVIEW_MAX_CHARS = 120;
18
+ /** 插槽顺序:排在原生排队 dock(order 20)之前,本条显示在排队列表上方。 */
19
+ const DOCK_ORDER = 19;
20
+
21
+ // 样式对齐原生 QueueDock(同容器宽度/圆角/row 高度/字体),保证与原生排队列表视觉连体
22
+ const css = `
23
+ .sm-processing-dock { box-sizing: border-box; width: calc(100% - var(--dsh-composer-side-clearance, 0px) - var(--dsh-composer-side-clearance, 0px) - var(--dsh-composer-dock-inset, 0px) - var(--dsh-composer-dock-inset, 0px)); max-width: calc(var(--dsh-composer-card-max-width, 748px) - var(--dsh-composer-dock-inset, 0px) - var(--dsh-composer-dock-inset, 0px)); margin: 0 auto calc(0px - var(--dsh-composer-stack-gap, 0px) - 3px); padding: 0 var(--dsh-composer-dock-inset, 0px); flex: none; }
24
+ .sm-processing-panel { background: var(--dsw-specific-tip, rgba(128,128,128,.06)); border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3)); border-bottom: none; border-radius: 12px 12px 0 0; width: 100%; padding: 2px 0; position: relative; }
25
+ .sm-processing-row { box-sizing: border-box; display: flex; align-items: center; gap: 10px; width: 100%; height: 36px; padding: 4px 12px; border-radius: 8px; color: var(--dsw-alias-label-primary-dimmed, #c9c9ce); font-family: Inter, var(--dsw-font-family); font-size: 13px; }
26
+ .sm-processing-spin { display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: sm-processing-rotate .8s linear infinite; flex: none; }
27
+ .sm-processing-text { min-width: 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
28
+ @keyframes sm-processing-rotate { to { transform: rotate(360deg); } }
29
+ `;
30
+ const styleId = "sm-processing-css";
31
+ if (typeof document !== "undefined" && !document.getElementById(styleId)) {
32
+ const s = document.createElement("style");
33
+ s.id = styleId;
34
+ s.textContent = css;
35
+ document.head.appendChild(s);
36
+ }
37
+
38
+ /** 从会话节点里提取当前回合(正在执行)的用户消息文本。 */
39
+ function activeUserText(nodes) {
40
+ if (!Array.isArray(nodes)) return "";
41
+ for (let i = nodes.length - 1; i >= 0; i--) {
42
+ const n = nodes[i];
43
+ if (n === null || n === undefined) continue;
44
+ const kind = n.kind;
45
+ if (kind === "assistant" || kind === "assistant-step" || kind === "tool-result" || kind === "command") break;
46
+ if (kind === "user") {
47
+ const text = typeof n.text === "string" ? n.text
48
+ : Array.isArray(n.content) ? n.content.filter((b) => b?.type === "text").map((b) => b.text).join(" ")
49
+ : "";
50
+ if (text) return text;
51
+ }
52
+ }
53
+ return "";
54
+ }
55
+
56
+ function ProcessingBar({ useSession, t }) {
57
+ const running = useSession((s) => s.running);
58
+ const nodes = useSession((s) => s.nodes);
59
+ const active = react.useMemo(() => activeUserText(nodes), [running, nodes]);
60
+ // 仅当「第一条消息执行中」时显示(执行完自动消失);排队消息由原生 dock 展示
61
+ if (!running || !active) return null;
62
+ const preview = active.replace(/\s+/g, " ").trim().slice(0, PREVIEW_MAX_CHARS);
63
+ const label = t("processing") + ":" + preview;
64
+ return react_jsx_runtime.jsx("div", {
65
+ className: "sm-processing-dock",
66
+ children: react_jsx_runtime.jsx("div", {
67
+ className: "sm-processing-panel",
68
+ children: react_jsx_runtime.jsx("div", {
69
+ className: "sm-processing-row",
70
+ title: preview,
71
+ children: [react_jsx_runtime.jsx("span", { className: "sm-processing-spin" }), react_jsx_runtime.jsx("span", { className: "sm-processing-text", children: label })],
72
+ }),
73
+ }),
74
+ });
75
+ }
76
+
77
+ function apply(ctx) {
78
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "sm-processing: dictionaries");
79
+ ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
80
+ name: "conversation.input.dock",
81
+ id: "sm-processing",
82
+ order: DOCK_ORDER, // 在原生排队 dock(order 20)之前:第一条条目显示在排队列表上方
83
+ locale: NS,
84
+ }, ProcessingBar));
85
+ }
86
+
87
+ const inject = ["slots", "locale"];
88
+ exports.apply = apply;
89
+ exports.inject = inject;
90
+ return module.exports;
91
+ }
92
+ });
93
+
94
+ /**
95
+ * dsh-session-conductor — 压缩模型选择设置卡片
96
+ * 设置 → 插件配置 → 「压缩模型」:会话模型旁单独选压缩用模型(省 pro 成本)。
97
+ * 可选:跟随会话模型(默认)/ 指定便宜模型(agnes 等)。写入 /api/session-conductor/compaction-model。
98
+ */
@@ -0,0 +1,20 @@
1
+ /** 会话卡图标内的矩形(24x24 viewBox 坐标系)。 */
2
+ const ICON_RECT_X = 3;
3
+ const ICON_RECT_Y = 4;
4
+ const ICON_RECT_WIDTH = 18;
5
+ const ICON_RECT_HEIGHT = 14;
6
+ const ICON_RECT_RADIUS = 2;
7
+
8
+ function SessionIcon(props) {
9
+ const s = props.size || 16;
10
+ return react_jsx_runtime.jsx("svg", {
11
+ width: s, height: s, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor",
12
+ strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true,
13
+ children: [
14
+ react_jsx_runtime.jsx("rect", { x: ICON_RECT_X, y: ICON_RECT_Y, width: ICON_RECT_WIDTH, height: ICON_RECT_HEIGHT, rx: ICON_RECT_RADIUS }),
15
+ react_jsx_runtime.jsx("path", { d: "M8 20h8" }),
16
+ react_jsx_runtime.jsx("path", { d: "M12 18v2" }),
17
+ ],
18
+ });
19
+ }
20
+
@@ -0,0 +1,415 @@
1
+ // ---------- 插件入口 ----------
2
+ const inject = ["slots", "locale"];
3
+
4
+ // 设置页必须单入口 + React.createElement,禁止 jsx-runtime.jsx。
5
+ // 外壳对 settings.section 渲染抛错会吞成空白,所以页面自己兜底。
6
+ const h = react.createElement.bind(react);
7
+ const settingsCss = [
8
+ ".scs_page{display:flex;flex-direction:column;gap:16px;padding:4px 0 24px}",
9
+ ".scs_hero{display:flex;flex-direction:column;gap:6px}",
10
+ ".scs_title{font-size:20px;font-weight:650;color:var(--dsw-alias-label-primary)}",
11
+ ".scs_desc{font-size:13px;line-height:1.6;color:var(--dsw-alias-label-tertiary);max-width:72ch;margin:0}",
12
+ ".scs_hint{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
13
+ ".scs_err{font-size:13px;color:var(--dsw-alias-label-error)}",
14
+ ".scs_card{display:flex;flex-direction:column;gap:12px;padding:16px;border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3)}",
15
+ ".scs_cardtitle{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}",
16
+ ".scs_note{font-size:12px;line-height:1.7;color:var(--dsw-alias-label-secondary);padding:10px 12px;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px}",
17
+ ".scs_row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
18
+ ".scs_btn{cursor:pointer;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);border-radius:8px;height:28px;padding:0 12px;font-size:12px}",
19
+ ".scs_btn.scs_on{border-color:var(--dsw-state-success-primary,#34a853);color:var(--dsw-state-success-primary,#34a853)}",
20
+ ".scs_btn.scs_off{border-color:var(--dsw-state-error-primary,#d9534f);color:var(--dsw-state-error-primary,#e5484d)}",
21
+ ".scs_input{flex:1;min-width:180px;height:28px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:0 10px;font-size:13px}",
22
+ ".scs_last{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:6px 8px;border:1px dashed var(--dsw-alias-border-l2);border-radius:6px;white-space:pre-wrap;word-break:break-all;max-height:80px;overflow:auto}",
23
+ ".scs_file{display:flex;align-items:center;gap:8px;font-size:12px}",
24
+ ".scs_del{cursor:pointer;border:none;background:transparent;color:#e5484d;font-size:11px}",
25
+ ".scs_select{height:28px;border-radius:6px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-size:12px;padding:0 8px}",
26
+ ".scs_ta{width:100%;min-height:72px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:8px 10px;font-size:12px;resize:vertical}",
27
+ // 悬浮窗(改:参考「选择目录」弹窗样式——mask 遮罩 + 居中 dialog + 面包屑 + 上级 + 列表 + 底部确认)
28
+ ".scs_pickermask{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}",
29
+ ".scs_pickerdialog{width:min(520px,88vw);max-height:70vh;display:flex;flex-direction:column;gap:8px;padding:14px;border:.5px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-3);box-shadow:0 12px 40px rgba(0,0,0,.35)}",
30
+ ".scs_pickerhead{display:flex;align-items:center;justify-content:space-between}",
31
+ ".scs_pickertitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}",
32
+ ".scs_pickerclose{font:inherit;font-size:13px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:2px 6px}",
33
+ ".scs_pickerpath{font-size:11px;color:var(--dsw-alias-label-secondary);word-break:break-all;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base)}",
34
+ ".scs_pickerup{font-size:12px;color:var(--dsw-alias-brand-primary);cursor:pointer;padding:3px 2px}",
35
+ ".scs_pickerlist{flex:1;min-height:120px;max-height:38vh;overflow-y:auto;display:flex;flex-direction:column;gap:2px}",
36
+ ".scs_pickerrow{font-size:12px;color:var(--dsw-alias-label-primary);cursor:pointer;padding:5px 8px;border-radius:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
37
+ ".scs_pickerrow:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}",
38
+ ".scs_pickerrow.scs_pickerselected{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);color:var(--dsw-alias-brand-primary)}",
39
+ ".scs_pickerhint{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:8px}",
40
+ ".scs_pickerfoot{display:flex;justify-content:flex-end}",
41
+ ".scs_pickermdfile{font-weight:600}",
42
+ ".scs_rule{display:flex;flex-direction:column;gap:8px;padding:10px;border:.5px dashed var(--dsw-alias-border-l2);border-radius:10px}",
43
+ ].join("");
44
+
45
+ function ensureSettingsCss() {
46
+ if (typeof document === "undefined") return;
47
+ if (document.querySelector('style[data-plugin-css="dsh-session-conductor"]')) return;
48
+ const tag = document.createElement("style");
49
+ tag.dataset.plugin = "dsh-session-conductor";
50
+ tag.dataset.pluginCss = "dsh-session-conductor";
51
+ tag.textContent = settingsCss;
52
+ document.head.appendChild(tag);
53
+ }
54
+
55
+ function trSettings(key) {
56
+ // 跨 NS + en 统一取词(修复此前只读 zh 的 bug)
57
+ return window.__SC_TR__(key);
58
+ }
59
+
60
+ // —— 模板注入:槽位卡片(独立组件,避免 MainTemplateSection 单函数过长)——
61
+ function TemplateSlotCard(props) {
62
+ const { slot, title, desc, m, busy, edits, importMode, fileRef, MODES, onToggle, onToggleEnforce, onOpenImport, onPickFile, onEdit, onSaveEdit, onRemove } = props;
63
+ const hasContent = !!m.name || (m.bytes > 0);
64
+ const text = edits[slot] !== undefined ? edits[slot] : (m.content ?? "");
65
+ return h("div", { className: "scs_rule", key: slot },
66
+ h("div", { className: "scs_row" },
67
+ h("b", { style: { minWidth: LABEL_MIN_WIDTH } }, title),
68
+ h("button", { type: "button", className: "scs_btn " + (m.enabled ? "scs_on" : "scs_off"), disabled: busy || !hasContent, onClick: () => onToggle(slot, !m.enabled) }, m.enabled ? __SC_TR__("tpl.injecting") : __SC_TR__("tpl.injectOff")),
69
+ slot === "plan" ? h("button", { type: "button", className: "scs_btn", disabled: busy, onClick: () => onToggleEnforce(!(m.enforce === true)) }, (m.enforce === true ? __SC_TR__("tpl.enforceOn") : __SC_TR__("tpl.enforceOff"))) : null,
70
+ ),
71
+ // 导入行:下拉选方式 + 单个导入按钮
72
+ h("div", { className: "scs_row", style: { marginTop: 8 } },
73
+ h("select", {
74
+ className: "scs_select", value: importMode[slot] || "local",
75
+ onChange: (e) => props.onImportMode(slot, e.target.value),
76
+ }, MODES.map((o) => h("option", { key: o.value, value: o.value }, o.label))),
77
+ h("button", { type: "button", className: "scs_btn", disabled: busy, onClick: () => onOpenImport(slot) }, __SC_TR__("tpl.import")),
78
+ h("input", { ref: fileRef, type: "file", accept: ".md,text/markdown", style: { display: "none" }, onChange: (e) => onPickFile(slot, e) }),
79
+ ),
80
+ // 内容显示 + 可再次编辑(动态高度、自动换行)
81
+ h("textarea", {
82
+ className: "scs_ta",
83
+ style: { width: "100%", minHeight: TEXTAREA_MIN_HEIGHT, maxHeight: TEXTAREA_MAX_HEIGHT, resize: "vertical", whiteSpace: "pre-wrap", overflowWrap: "break-word", wordBreak: "break-word", lineHeight: LINE_HEIGHT_TEXTAREA, fontFamily: "inherit" },
84
+ value: text,
85
+ placeholder: hasContent ? "" : __SC_TR__("tpl.emptyHint"),
86
+ onChange: (e) => onEdit(slot, e.target),
87
+ }),
88
+ h("div", { className: "scs_row", style: { marginTop: 6 } },
89
+ h("button", { type: "button", className: "scs_btn", disabled: busy || edits[slot] === undefined || !(edits[slot] || "").trim(), onClick: () => onSaveEdit(slot) }, __SC_TR__("tpl.saveEdit")),
90
+ h("button", { type: "button", className: "scs_del", disabled: busy || !hasContent, onClick: () => onRemove(slot) }, __SC_TR__("tpl.clear")),
91
+ ),
92
+ h("div", { className: "scs_hint" }, desc + (m.name ? __SC_TR__("tpl.current", { name: m.name }) + (m.url ? "(" + m.url + ")" : "") + (m.updatedAt ? " · " + new Date(m.updatedAt).toLocaleString() : "") : "")),
93
+ );
94
+ }
95
+
96
+ // —— 模板注入:统一悬浮窗(url 网址输入 / dir 目录浏览)——
97
+ function TemplatePickerModal(props) {
98
+ const { picker, busy, onClose, onBrowseDir, onPickMd, onImport } = props;
99
+ return h("div", { className: "scs_pickermask", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } },
100
+ h("div", { className: "scs_pickerdialog" },
101
+ h("div", { className: "scs_pickerhead" },
102
+ h("span", { className: "scs_pickertitle" }, picker.mode === "url" ? __SC_TR__("tpl.pickerUrl", { slot: picker.slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT) }) : (picker.mode === "dir" ? __SC_TR__("tpl.pickerDir", { slot: picker.slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT) }) : __SC_TR__("tpl.pickerPath", { slot: picker.slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT) }))),
103
+ h("button", { type: "button", className: "scs_pickerclose", onClick: () => onClose() }, "✕"),
104
+ ),
105
+ picker.mode === "url"
106
+ ? h("div", { className: "scs_row", style: { paddingTop: 4 } },
107
+ h("input", { className: "scs_input", style: { flex: 1 }, value: picker.url || "", placeholder: __SC_TR__("tpl.urlPlaceholder"), onChange: (e) => props.onUrlChange(e.target.value) }),
108
+ h("button", { type: "button", className: "scs_btn", disabled: busy || !(picker.url || "").trim(), onClick: () => onImport(picker.slot, "url", (picker.url || "").trim()) }, __SC_TR__("tpl.import")),
109
+ )
110
+ : h("div", {},
111
+ h("div", { className: "scs_pickerpath" }, picker.path || __SC_TR__("tpl.noPath")),
112
+ h("div", { className: "scs_pickerup", onClick: () => { if (picker.parent) onBrowseDir(picker.parent); } }, __SC_TR__("tpl.upDir")),
113
+ h("div", { className: "scs_pickerlist" },
114
+ picker.entries.length === 0 ? h("div", { className: "scs_pickerhint" }, __SC_TR__("tpl.emptyDir")) : null,
115
+ picker.entries.map((e) => h("div", {
116
+ key: e.path,
117
+ className: "scs_pickerrow" + (e.isMd ? " scs_pickermdfile" : "") + (picker.path === e.path ? " scs_pickerselected" : ""),
118
+ onClick: () => { if (e.isDir) onBrowseDir(e.path); else if (e.isMd) onPickMd(e.path); },
119
+ }, (e.isDir ? "📁 " : (e.isMd ? "📄 " : "· ")) + e.name + (e.isDir ? "/" : ""))),
120
+ ),
121
+ h("div", { className: "scs_pickerhint" }, __SC_TR__("tpl.dirHint")),
122
+ h("div", { className: "scs_pickerfoot" },
123
+ h("button", { type: "button", className: "scs_btn", disabled: busy || !(picker.path || "").trim(), onClick: () => onImport(picker.slot, "dir", (picker.path || "").trim()) }, __SC_TR__("tpl.importSelected")),
124
+ ),
125
+ ),
126
+ ),
127
+ );
128
+ }
129
+
130
+ const TEMPLATE_MODES = [
131
+ { value: "local", label: __SC_TR__("tpl.modeLocal") },
132
+ { value: "url", label: __SC_TR__("tpl.modeUrl") },
133
+ { value: "dir", label: __SC_TR__("tpl.modeDir") },
134
+ ];
135
+
136
+ function useTemplateFetch(st) {
137
+ const refresh = react.useCallback(() => {
138
+ window.__scFetch("/api/session-conductor/templates", { credentials: "same-origin" })
139
+ .then(async (res) => {
140
+ const body = await res.json().catch(() => ({}));
141
+ if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
142
+ st.setSlots(body.slots || {});
143
+ })
144
+ .catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }));
145
+ }, [st]);
146
+ react.useEffect(() => { refresh(); }, [refresh]);
147
+ const post = (payload) => window.__scFetch("/api/session-conductor/templates", {
148
+ method: "POST", credentials: "same-origin",
149
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
150
+ body: JSON.stringify(payload),
151
+ }).then(async (res) => {
152
+ const body = await res.json().catch(() => ({}));
153
+ if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
154
+ st.setSlots(body.slots || {});
155
+ return body;
156
+ });
157
+ return { refresh, post };
158
+ }
159
+
160
+ // —— 模板导入动作(本地设备上传 / 在线网址 / DSH 目录浏览)——
161
+ function useTemplateImports(st, post) {
162
+ const slotLabel = (slot) => (slot === "plan" ? __SC_TR__(I18N_TPL_PLAN_SHORT) : __SC_TR__(I18N_TPL_CLOSING_SHORT));
163
+ const onPickFile = (slot, event) => {
164
+ const f = event.target.files && event.target.files[0];
165
+ if (!f) return;
166
+ event.target.value = "";
167
+ st.setBusy(true); st.setMsg(null);
168
+ f.text()
169
+ .then((content) => post({ slot, name: f.name, content }))
170
+ .then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.imported", { slot: slotLabel(slot) }) }); st.setEdits((s) => ({ ...s, [slot]: content })); })
171
+ .catch((cause) => st.setMsg({ err: __SC_TR__("tpl.importFail") + ((cause && cause.message) || String(cause)) }))
172
+ .finally(() => st.setBusy(false));
173
+ };
174
+ const openImport = (slot) => {
175
+ const mode = st.importMode[slot] || "local";
176
+ st.setMsg(null);
177
+ if (mode === "local") {
178
+ if (st.fileRefs[slot].current) st.fileRefs[slot].current.click();
179
+ return;
180
+ }
181
+ if (mode === "url") {
182
+ st.setPicker({ slot, mode: "url", url: "", path: "", parent: null, entries: [] });
183
+ return;
184
+ }
185
+ window.__scFetch("/api/session-conductor/templates/dir", { credentials: "same-origin" })
186
+ .then(async (res) => {
187
+ const body = await res.json().catch(() => ({}));
188
+ if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
189
+ st.setPicker({ slot, mode, path: body.path, parent: body.parent, entries: body.entries || [] });
190
+ })
191
+ .catch((cause) => st.setMsg({ err: __SC_TR__("tpl.dirFail") + ((cause && cause.message) || String(cause)) }));
192
+ };
193
+ const browseDir = (path) => {
194
+ window.__scFetch("/api/session-conductor/templates/dir?path=" + encodeURIComponent(path), { credentials: "same-origin" })
195
+ .then(async (res) => {
196
+ const body = await res.json().catch(() => ({}));
197
+ if (!res.ok || !body.ok) throw new Error((body.error && body.error.message) || (HTTP_STATUS_PREFIX + res.status));
198
+ st.setPicker({ slot: st.picker.slot, mode: st.picker.mode, path: body.path, parent: body.parent, entries: body.entries || [] });
199
+ })
200
+ .catch((cause) => st.setMsg({ err: __SC_TR__("tpl.dirFail") + ((cause && cause.message) || String(cause)) }));
201
+ };
202
+ const importTemplate = (slot, mode, value) => {
203
+ st.setBusy(true); st.setMsg(null);
204
+ post(mode === "url" ? { slot, url: value } : { slot, action: "pickPath", path: value })
205
+ .then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.imported", { slot: slotLabel(slot) }) }); st.setPicker(null); })
206
+ .catch((cause) => st.setMsg({ err: __SC_TR__("tpl.importFail") + ((cause && cause.message) || String(cause)) }))
207
+ .finally(() => st.setBusy(false));
208
+ };
209
+ return { onPickFile, openImport, browseDir, importTemplate, slotLabel };
210
+ }
211
+
212
+ // —— 模板槽位变更动作(开关 / 强制门禁 / 编辑保存 / 清空)——
213
+ function useTemplateMutations(st, post, slotLabel) {
214
+ const toggle = (slot, enabled) => {
215
+ st.setBusy(true); st.setMsg(null);
216
+ post({ slot, enabled })
217
+ .then((body) => st.setMsg({ ok: (body.message) || __SC_TR__("tpl.toggled", { slot: slotLabel(slot) }) }))
218
+ .catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }))
219
+ .finally(() => st.setBusy(false));
220
+ };
221
+ const toggleEnforce = (enforce) => {
222
+ st.setBusy(true); st.setMsg(null);
223
+ post({ slot: "plan", enforce })
224
+ .then((body) => st.setMsg({ ok: (body.message) || __SC_TR__("tpl.enforceDone") }))
225
+ .catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }))
226
+ .finally(() => st.setBusy(false));
227
+ };
228
+ const onEdit = (slot, el) => {
229
+ const v = el.value;
230
+ st.setEdits((s) => ({ ...s, [slot]: v }));
231
+ el.style.height = "auto";
232
+ el.style.height = (el.scrollHeight + 2) + "px";
233
+ };
234
+ const saveEdit = (slot) => {
235
+ const content = (st.edits[slot] ?? "").trim();
236
+ if (!content) return;
237
+ st.setBusy(true); st.setMsg(null);
238
+ post({ slot, content })
239
+ .then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.updated", { slot: slotLabel(slot) }) }); st.setEdits((s) => ({ ...s, [slot]: undefined })); })
240
+ .catch((cause) => st.setMsg({ err: __SC_TR__("tpl.saveFail") + ((cause && cause.message) || String(cause)) }))
241
+ .finally(() => st.setBusy(false));
242
+ };
243
+ const onRemove = (slot) => {
244
+ st.setBusy(true); st.setMsg(null);
245
+ post({ slot, action: "remove" })
246
+ .then((body) => { st.setMsg({ ok: (body.message) || __SC_TR__("tpl.removed", { slot: slotLabel(slot) }) }); st.setEdits((s) => ({ ...s, [slot]: undefined })); })
247
+ .catch((cause) => st.setMsg({ err: (cause && cause.message) || String(cause) }))
248
+ .finally(() => st.setBusy(false));
249
+ };
250
+ return { toggle, toggleEnforce, onEdit, saveEdit, onRemove };
251
+ }
252
+
253
+ function MainTemplateSection() {
254
+ const [slots, setSlots] = react.useState(null);
255
+ const [busy, setBusy] = react.useState(false);
256
+ const [msg, setMsg] = react.useState(null);
257
+ const [edits, setEdits] = react.useState({});
258
+ const [importMode, setImportMode] = react.useState({ plan: "local", closing: "local" });
259
+ const [picker, setPicker] = react.useState(null);
260
+ const fileRefs = { plan: react.useRef(null), closing: react.useRef(null) };
261
+ const st = { slots, setSlots, busy, setBusy, msg, setMsg, edits, setEdits, importMode, setImportMode, picker, setPicker, fileRefs };
262
+ const { post } = useTemplateFetch(st);
263
+ const imports = useTemplateImports(st, post);
264
+ const muts = useTemplateMutations(st, post, imports.slotLabel);
265
+ return h("div", { className: "scs_card" },
266
+ h("div", { className: "scs_cardtitle" }, __SC_TR__("tpl.title")),
267
+ h("div", { className: "scs_hint" }, __SC_TR__("tpl.desc")),
268
+ (slots ? [
269
+ h(TemplateSlotCard, {
270
+ key: "plan", slot: "plan", title: __SC_TR__("tpl.planTitle"), desc: __SC_TR__("tpl.planDesc"),
271
+ m: slots.plan || { enabled: false, enforce: false, name: "", url: "", bytes: 0, updatedAt: 0, content: "" },
272
+ busy, edits, importMode, fileRef: fileRefs.plan, MODES: TEMPLATE_MODES,
273
+ onToggle: muts.toggle, onToggleEnforce: muts.toggleEnforce, onOpenImport: imports.openImport, onPickFile: imports.onPickFile, onEdit: muts.onEdit, onSaveEdit: muts.saveEdit, onRemove: muts.onRemove,
274
+ onImportMode: (s, v) => setImportMode((prev) => ({ ...prev, [s]: v })),
275
+ }),
276
+ h(TemplateSlotCard, {
277
+ key: "closing", slot: "closing", title: __SC_TR__("tpl.closingTitle"), desc: __SC_TR__("tpl.closingDesc"),
278
+ m: slots.closing || { enabled: false, enforce: false, name: "", url: "", bytes: 0, updatedAt: 0, content: "" },
279
+ busy, edits, importMode, fileRef: fileRefs.closing, MODES: TEMPLATE_MODES,
280
+ onToggle: muts.toggle, onToggleEnforce: muts.toggleEnforce, onOpenImport: imports.openImport, onPickFile: imports.onPickFile, onEdit: muts.onEdit, onSaveEdit: muts.saveEdit, onRemove: muts.onRemove,
281
+ onImportMode: (s, v) => setImportMode((prev) => ({ ...prev, [s]: v })),
282
+ }),
283
+ ] : h("div", { className: "scs_hint" }, __SC_TR__("tpl.loading"))),
284
+ msg ? h("div", { className: msg.err ? "scs_err" : "scs_hint" }, msg.err || msg.ok) : null,
285
+ picker ? h(TemplatePickerModal, {
286
+ picker, busy,
287
+ onClose: () => setPicker(null),
288
+ onBrowseDir: imports.browseDir,
289
+ onPickMd: (p) => setPicker((prev) => ({ ...prev, path: p })),
290
+ onUrlChange: (v) => setPicker((prev) => ({ ...prev, url: v })),
291
+ onImport: imports.importTemplate,
292
+ }) : null,
293
+ );
294
+ }
295
+
296
+
297
+
298
+ // ── 自动重命名模型选择:DSH 同款解析选择器(provider 分组 + 模型下拉)──
299
+ // 数据来自官方 modelCatalog(GET /api/session-conductor/auto-rename-model 透传),
300
+ // 选择持久化到插件 domain,自动重命名 resolveRoute 三级优先级最优先。
301
+ function AutoRenameModelSection() {
302
+ const [sel, setSel] = react.useState(null); // {provider, model} | null(null=跟随会话模型)
303
+ const [catalog, setCatalog] = react.useState(null); // 官方 modelCatalog
304
+ const [provider, setProvider] = react.useState(""); // 编辑态 provider
305
+ const [model, setModel] = react.useState(""); // 编辑态 model
306
+ const [busy, setBusy] = react.useState(false);
307
+ const [msg, setMsg] = react.useState(null);
308
+
309
+ react.useEffect(() => {
310
+ let alive = true;
311
+ window.__scFetch("/api/session-conductor/auto-rename-model", { headers: { accept: "application/json" } })
312
+ .then((r) => r.json())
313
+ .then((data) => {
314
+ if (!alive || !data?.ok) return;
315
+ setSel(data.selection ?? null);
316
+ setCatalog(data.catalog ?? null);
317
+ if (data.selection) {
318
+ setProvider(data.selection.provider);
319
+ setModel(data.selection.model);
320
+ }
321
+ })
322
+ .catch(() => { /* 目录获取失败:仍允许手动选择(下方兜底输入框) */ });
323
+ return () => { alive = false; };
324
+ }, []);
325
+
326
+ const save = async (next) => {
327
+ setBusy(true);
328
+ setMsg(null);
329
+ try {
330
+ const r = await window.__scFetch("/api/session-conductor/auto-rename-model", {
331
+ method: "POST",
332
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
333
+ body: JSON.stringify(next),
334
+ });
335
+ const payload = await r.json();
336
+ if (payload?.ok) {
337
+ setSel(payload.selection ?? null);
338
+ setMsg({ ok: payload.message || __SC_TR__("saved") });
339
+ } else {
340
+ setMsg({ err: payload?.error?.message || __SC_TR__("saveFail") });
341
+ }
342
+ } catch (e) {
343
+ setMsg({ err: String(e?.message ?? e) });
344
+ }
345
+ setBusy(false);
346
+ };
347
+
348
+ const groups = catalog?.groups ?? [];
349
+ const providers = groups.map((g) => ({ id: g.id, name: g.name, models: g.models ?? [] }));
350
+ const curProvider = providers.find((p) => p.id === provider) ?? null;
351
+ const models = curProvider?.models ?? [];
352
+ const follow = sel === null;
353
+
354
+ return h("div", { className: "scs_card" },
355
+ h("div", { className: "scs_cardtitle" }, __SC_TR__("armTitle")),
356
+ h("div", { className: "scs_hint" }, __SC_TR__("armDesc")),
357
+ h("div", { className: "scs_row", style: { gap: 8, alignItems: "center" } },
358
+ h("label", { style: { display: "flex", alignItems: "center", gap: 4 } },
359
+ h("input", { type: "radio", name: "sc-arm-follow", checked: follow, onChange: () => save({ follow: true }) }),
360
+ __SC_TR__("armFollow"),
361
+ ),
362
+ ),
363
+ h("div", { className: "scs_row", style: { gap: 8, alignItems: "center", flexWrap: "wrap" } },
364
+ h("select", { className: "scs_input", style: { width: SELECT_WIDTH_PROVIDER }, value: provider, disabled: busy, onChange: (e) => { setProvider(e.target.value); setModel(""); } },
365
+ h("option", { value: "" }, __SC_TR__("armProvider")),
366
+ providers.map((p) => h("option", { key: p.id, value: p.id }, p.name)),
367
+ ),
368
+ h("select", { className: "scs_input", style: { width: SELECT_WIDTH_MODEL }, value: model, disabled: busy || !provider, onChange: (e) => setModel(e.target.value) },
369
+ h("option", { value: "" }, __SC_TR__("armModel")),
370
+ models.map((m) => h("option", { key: m.id, value: m.id }, m.name || m.id)),
371
+ ),
372
+ h("button", { type: "button", className: "scs_btn", disabled: busy || !provider || !model, onClick: () => save({ provider, model }) }, __SC_TR__("armSet")),
373
+ ),
374
+ h("div", { className: "scs_hint" }, follow
375
+ ? __SC_TR__("armCurFollow")
376
+ : (sel ? `当前:${sel.provider}/${sel.model}` : __SC_TR__("armCurNone"))),
377
+ msg ? h("div", { className: msg.err ? "scs_err" : "scs_hint" }, msg.err || msg.ok) : null,
378
+ );
379
+ }
380
+
381
+ class SettingsErrorBoundary extends react.Component {
382
+ constructor(props) {
383
+ super(props);
384
+ this.state = { err: "" };
385
+ }
386
+ static getDerivedStateFromError(err) {
387
+ return { err: (err && err.message) || String(err) };
388
+ }
389
+ render() {
390
+ if (this.state.err) {
391
+ return h("div", { className: "scs_page" },
392
+ h("div", { className: "scs_title" }, __SC_TR__("settings.title")),
393
+ h("div", { className: "scs_err" }, __SC_TR__("renderFail") + this.state.err),
394
+ );
395
+ }
396
+ return this.props.children;
397
+ }
398
+ }
399
+
400
+ function ConductorSettingsPage() {
401
+ ensureSettingsCss();
402
+ return h(SettingsErrorBoundary, null,
403
+ h("div", { className: "scs_page" },
404
+ h("div", { className: "scs_hero" },
405
+ h("div", { className: "scs_title" }, trSettings("settings.title")),
406
+ h("p", { className: "scs_desc" }, trSettings("settings.desc")),
407
+ h("div", { className: "scs_hint" }, trSettings("settings.scopeHint")),
408
+ ),
409
+ h(AutoRenameModelSection, null),
410
+ h(MainTemplateSection, null),
411
+ h("div", { className: "scs_note" }, trSettings("settings.note")),
412
+ ),
413
+ );
414
+ }
415
+
@@ -0,0 +1,48 @@
1
+ var module = { exports: {} };
2
+ var exports = module.exports;
3
+ let react = require("react");
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+
6
+ // ---------- 文案 ----------
7
+
8
+ const NS = "sessionConductor";
9
+
10
+ const zh = {};
11
+ const en = {};
12
+ __scFillI18nDict("sessionConductor", zh, en);
13
+
14
+ // ---------- 工具 ----------
15
+
16
+ // 全局 fetch 超时兜底:DSH API 挂起(重负载/路由未注册)时界面不再无限等待。
17
+ // 与 fetch 同签名(url, opts, timeoutMs),默认 30s;分片统一走 __scFetch。
18
+ const FETCH_TIMEOUT_MS = 30000;
19
+ window.__scFetch = function (url, opts, timeoutMs) {
20
+ const controller = new AbortController();
21
+ const timer = setTimeout(() => controller.abort(), timeoutMs || FETCH_TIMEOUT_MS);
22
+ const next = opts ? Object.assign({}, opts, { signal: controller.signal }) : { signal: controller.signal };
23
+ return fetch(url, next).finally(() => clearTimeout(timer));
24
+ };
25
+
26
+ function basenameOf(p) {
27
+ const rawPath = String(p ?? "");
28
+ const cleaned = rawPath.replace(/[/\\]+$/, "");
29
+ const part = cleaned.split(/[/\\]/).pop();
30
+ return part || rawPath;
31
+ }
32
+
33
+ function timeAgo(ms, t) {
34
+ if (!Number.isFinite(ms)) return "";
35
+ const diff = Date.now() - ms;
36
+ const minute = 60e3;
37
+ const hour = 3600e3;
38
+ const day = 86400e3;
39
+ const month = 30 * day;
40
+ const year = 365 * day;
41
+ if (diff < minute) return t("meta.time.now");
42
+ if (diff < hour) return t("meta.time.minutes", { n: Math.floor(diff / minute) });
43
+ if (diff < day) return t("meta.time.hours", { n: Math.floor(diff / hour) });
44
+ if (diff < month) return t("meta.time.days", { n: Math.floor(diff / day) });
45
+ if (diff < year) return t("meta.time.months", { n: Math.floor(diff / month) });
46
+ return t("meta.time.years", { n: Math.floor(diff / year) });
47
+ }
48
+