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.
- package/README.md +165 -2
- package/cordis.patch.yml +48 -0
- package/lib/apply.js +1361 -0
- package/lib/client-parts/apply.js +43 -0
- package/lib/client-parts/components/compaction.js +124 -0
- package/lib/client-parts/components/conductor-settings.js +62 -0
- package/lib/client-parts/components/group.js +115 -0
- package/lib/client-parts/components/panel.js +1265 -0
- package/lib/client-parts/components/processing.js +98 -0
- package/lib/client-parts/components/session-icon.js +20 -0
- package/lib/client-parts/components/settings.js +415 -0
- package/lib/client-parts/foundation/bootstrap.js +48 -0
- package/lib/client-parts/foundation/i18n.js +74 -0
- package/lib/client-parts/foundation/list-cache.js +28 -0
- package/lib/client-parts/foundation/styles.js +236 -0
- package/lib/client.js +2534 -0
- package/lib/core/config.js +159 -0
- package/lib/core/domain.js +157 -0
- package/lib/core/lazy.js +20 -0
- package/lib/core/state.js +106 -0
- package/lib/core.js +82 -0
- package/lib/eio-repair.js +204 -0
- package/lib/features/context-usage/index.js +219 -0
- package/lib/features/continue/eligibility.js +49 -0
- package/lib/features/continue/scan.js +134 -0
- package/lib/features/continue/session.js +235 -0
- package/lib/features/inject/preset.js +20 -0
- package/lib/features/member-model/index.js +14 -0
- package/lib/features/member-model/locate.js +157 -0
- package/lib/features/member-model/override.js +76 -0
- package/lib/features/member-model/setup.js +55 -0
- package/lib/features/member-model/switch.js +186 -0
- package/lib/features/rename/analysis.js +303 -0
- package/lib/features/rename/title.js +158 -0
- package/lib/features/rename/value.js +77 -0
- package/lib/features/search.js +119 -0
- package/lib/features/session-ops-routes.js +218 -0
- package/lib/features/session-ops.js +414 -0
- package/lib/group.js +303 -0
- package/lib/guards.js +28 -0
- package/lib/http.js +24 -0
- package/lib/i18n/en.json +204 -0
- package/lib/i18n/zh.json +204 -0
- package/lib/index.js +78 -0
- package/lib/repair.js +398 -0
- package/lib/seq-gap-repair.js +224 -0
- package/lib/session-codec.js +37 -0
- package/lib/session-log.js +46 -0
- package/lib/sessions/detach.js +113 -0
- package/lib/sessions/events.js +73 -0
- package/lib/sessions/interruption.js +73 -0
- package/lib/sessions/list-cache.js +175 -0
- package/lib/sessions/list.js +187 -0
- package/lib/sessions/route.js +60 -0
- package/lib/sessions/turn.js +21 -0
- package/lib/shared/constants.js +20 -0
- package/lib/shared/log.js +17 -0
- package/lib/shared/settings.js +14 -0
- package/lib/shared/test-hooks.js +93 -0
- package/lib/shared/util.js +6 -0
- package/lib/template-inject.js +289 -0
- package/lib/value.js +430 -0
- package/lib/zstd-frames.js +177 -0
- package/package.json +65 -3
- package/skills/dsh-session-conductor-functions.md +197 -0
- package/skills/dsh-session-conductor.md +151 -0
- package/skills/dsh-session-official-doc.md +245 -0
- 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
|
+
|