@mingmaple/dsh-handover 0.9.22 → 1.0.0

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 (81) hide show
  1. package/README.md +117 -113
  2. package/docs/migrate.md +52 -41
  3. package/docs/releases/0.9.12.md +2 -3
  4. package/docs/releases/0.9.14.md +1 -5
  5. package/docs/releases/0.9.22.md +1 -1
  6. package/docs/releases/1.0.0.md +33 -0
  7. package/docs/troubleshoot.md +221 -61
  8. package/package.json +5 -2
  9. package/scripts/behavior.mjs +398 -0
  10. package/scripts/blank-dsh.mjs +12 -2
  11. package/scripts/blank-smoke.mjs +95 -44
  12. package/scripts/check-docs.mjs +7 -0
  13. package/scripts/portability.mjs +71 -0
  14. package/scripts/style-gate.mjs +571 -0
  15. package/src/client.js +1028 -994
  16. package/src/index.mjs +273 -109
  17. package/src/lib/absrefs.mjs +1 -1
  18. package/src/lib/api.mjs +9 -3
  19. package/src/lib/archive.mjs +1 -1
  20. package/src/lib/brief.mjs +8 -6
  21. package/src/lib/config.mjs +21 -6
  22. package/src/lib/docmap.mjs +5 -8
  23. package/src/lib/drift.mjs +1 -1
  24. package/src/lib/events.mjs +1 -1
  25. package/src/lib/facts.mjs +19 -12
  26. package/src/lib/gates.mjs +12 -12
  27. package/src/lib/global-task.mjs +69 -0
  28. package/src/lib/guide.mjs +80 -75
  29. package/src/lib/handover-io.mjs +69 -6
  30. package/src/lib/identity.mjs +330 -0
  31. package/src/lib/legacy.mjs +89 -0
  32. package/src/lib/lint.mjs +14 -11
  33. package/src/lib/manual.mjs +117 -80
  34. package/src/lib/mover.mjs +18 -14
  35. package/src/lib/office.mjs +42 -2
  36. package/src/lib/pack-ops.mjs +10 -10
  37. package/src/lib/pathkey.mjs +24 -0
  38. package/src/lib/phase.mjs +8 -13
  39. package/src/lib/profile.mjs +1 -1
  40. package/src/lib/reminder.mjs +110 -0
  41. package/src/lib/role.mjs +1 -1
  42. package/src/lib/scaffold.mjs +30 -10
  43. package/src/lib/schedule.mjs +5 -5
  44. package/src/lib/session-create.mjs +2 -2
  45. package/src/lib/session-ops.mjs +76 -47
  46. package/src/lib/sessions.mjs +40 -2
  47. package/src/lib/sevenzip.mjs +10 -1
  48. package/src/lib/status.mjs +337 -336
  49. package/src/lib/succession.mjs +14 -14
  50. package/src/lib/system.mjs +364 -0
  51. package/src/templates/Changelog.skeleton.md +3 -3
  52. package/src/templates/Charter.skeleton.md +13 -9
  53. package/src/templates/Handover.global.skeleton.md +20 -13
  54. package/src/templates/Handover.template.md +89 -0
  55. package/src/templates/Plan.skeleton.md +34 -0
  56. package/src/templates/Prompt_Templates.md +91 -10
  57. package/src/templates/Runtime_Manual.md +254 -139
  58. package/src/templates/System.skeleton.md +31 -0
  59. package/src/templates/Systems.global.skeleton.md +13 -0
  60. package/src/templates/rules/Delegation_Rules.md +45 -0
  61. package/src/templates/rules/Handover_Rules.md +46 -24
  62. package/src/templates/rules/Migration_Rules.md +7 -7
  63. package/src/templates/rules/Misc_Task_Standard.md +17 -10
  64. package/src/templates/rules/Writing_Rules.md +33 -16
  65. package/src/templates/standards/README.md +19 -7
  66. package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
  67. package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
  68. package/src/tools/ho_backup.mjs +8 -8
  69. package/src/tools/ho_changelog.mjs +4 -4
  70. package/src/tools/ho_charter.mjs +7 -6
  71. package/src/tools/ho_checkpoint.mjs +34 -31
  72. package/src/tools/ho_docmap.mjs +135 -49
  73. package/src/tools/ho_gate.mjs +9 -4
  74. package/src/tools/ho_handover.mjs +61 -35
  75. package/src/tools/ho_office.mjs +45 -4
  76. package/src/tools/ho_profile.mjs +3 -3
  77. package/src/tools/ho_sessions.mjs +43 -22
  78. package/src/tools/ho_status.mjs +14 -9
  79. package/src/tools/ho_succeed.mjs +3 -3
  80. package/src/tools/ho_workspace.mjs +83 -51
  81. package/src/tools/peer.mjs +219 -8
package/src/client.js CHANGED
@@ -1,995 +1,1029 @@
1
- /**
2
- * dsh-handover 浏览器半:在「设置 → 交接」注册一个只读状态页。
3
- * 手工构建的 DSH client bundle(不用 JSX/打包器):只 require react,
4
- * 数据来自宿主侧注册的 GET /dsh-handover/status.json(只读、不唤醒任何会话)。
5
- *
6
- * 改了这个文件要重建 dist 并在 DSH 里重新激活插件(client bundle 不热更)。
7
- */
8
- window.__ModuleLoader__.load({
9
- id: '@mingmaple/dsh-handover',
10
- factory: (require) => {
11
- var module = { exports: {} };
12
- var exports = module.exports;
13
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
-
15
- const React = require("react");
16
- const h = React.createElement;
17
- /** 参与槽位注册的服务;connection=DSH 认证的 /api 载体(设置页取数的主通道)。 */
18
- const inject = ["slots", "connection"];
19
- let connection = null;
20
-
21
- /** 数据接口候选:绝对路径优先,再退相对(页面挂在子路径时仍能取到)。 */
22
- const ENDPOINTS = ["/dsh-handover/status.json", "dsh-handover/status.json"];
23
-
24
- const CSS_TAG_ID = "@mingmaple/dsh-handover/SettingsSection.css";
25
- const CSS = [
26
- ".dshh-root{display:flex;flex-direction:column;gap:14px;max-width:860px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.55}",
27
- ".dshh-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}",
28
- ".dshh-title{margin:0;font-size:16px;font-weight:600}",
29
- ".dshh-muted{color:var(--dsw-alias-label-tertiary)}",
30
- ".dshh-btn{font:inherit;cursor:pointer;background:0 0;color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 10px}",
31
- ".dshh-btn:disabled{opacity:.5;cursor:default}",
32
- ".dshh-card{border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}",
33
- ".dshh-cardTitle{margin:0;font-weight:600;font-size:13px}",
34
- ".dshh-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 14px}",
35
- ".dshh-k{color:var(--dsw-alias-label-tertiary)}",
36
- ".dshh-table{width:100%;border-collapse:collapse;table-layout:fixed}",
37
- ".dshh-table th,.dshh-table td{text-align:left;padding:3px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-size:12px;white-space:normal;overflow-wrap:anywhere;vertical-align:middle}",
38
- ".dshh-table th{white-space:nowrap;font-weight:600}",
39
- ".dshh-num{white-space:nowrap}",
40
- ".dshh-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px}",
41
- ".dshh-pick{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
42
- ".dshh-inputSm{font:inherit;font-size:12px;padding:2px 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary);box-sizing:border-box;width:100%}",
43
- ".dshh-ok{color:var(--dsw-alias-state-business-primary,#1a7f37)}",
44
- ".dshh-bad{color:var(--dsw-alias-state-error-primary,#c62828)}",
45
- ".dshh-warn{color:var(--dsw-alias-state-warning-primary,#b26a00)}",
46
- ".dshh-list{margin:0;padding-left:18px}",
47
- ".dshh-sub{margin:6px 0 2px;font-weight:600;color:var(--dsw-alias-label-primary)}",
48
- ".dshh-sum{cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:12px}",
49
- ".dshh-wrap td{white-space:normal}",
50
- ".dshh-input{font:inherit;width:100%;box-sizing:border-box;padding:5px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary)}",
51
- ".dshh-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:2px}",
52
- ".dshh-textarea{font:inherit;width:100%;box-sizing:border-box;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary);resize:vertical}",
53
- ".dshh-code{font-family:ui-monospace,Consolas,monospace;font-size:12px}",
54
- ].join("");
55
-
56
- /** 注入样式(按 tagId 去重,插件重复激活时不会堆叠)。 */
57
- function installCss() {
58
- if (typeof document === "undefined") return;
59
- if (document.querySelector('style[data-plugin-css="' + CSS_TAG_ID + '"]') !== null) return;
60
- const tag = document.createElement("style");
61
- tag.dataset.plugin = "@mingmaple/dsh-handover";
62
- tag.dataset.pluginCss = CSS_TAG_ID;
63
- tag.textContent = CSS;
64
- document.head.appendChild(tag);
65
- }
66
-
67
- /** 带超时的 fetch(旧通道回退用;没有超时就会永远「读取中」)。 */
68
- async function fetchWithTimeout(url, opts, ms) {
69
- const ac = new AbortController();
70
- const timer = setTimeout(() => ac.abort(), ms || 8000);
71
- try {
72
- return await fetch(url, Object.assign({ signal: ac.signal }, opts || {}));
73
- } finally {
74
- clearTimeout(timer);
75
- }
76
- }
77
-
78
- /** 依次尝试候选地址取状态;全失败时抛出带全部原因的错。 */
79
- async function fetchStatus() {
80
- const reasons = [];
81
- for (const url of ENDPOINTS) {
82
- try {
83
- const res = await fetchWithTimeout(url, { cache: "no-store", headers: { accept: "application/json" } }, 30000);
84
- if (!res.ok) { reasons.push(url + " → HTTP " + res.status); continue; }
85
- return await res.json();
86
- } catch (e) {
87
- reasons.push(url + " → " + (e && e.message ? e.message : String(e)));
88
- }
89
- }
90
- throw new Error(reasons.join(";") || "无法取得状态");
91
- }
92
-
93
- /** 通道诊断(显示于页面;出现异常时可直接定位,无需推测)。 */
94
- const diag = { lastError: "" };
95
-
96
- /** 页面级快照缓存:设置页切走再切回时先渲染旧读数,再后台刷新(不出现「正在读取」)。 */
97
- const state = { last: null };
98
-
99
- /** 读数时间保留完整值(含年份与时区),与页头一致。 */
100
- function shortTime(s) {
101
- return String(s || "");
102
- }
103
-
104
- /** 会话短号:去掉 session- 前缀,留前 8 位。 */
105
- function shortSid(s) {
106
- return String(s || "").replace(/^session-/, "").slice(0, 8);
107
- }
108
-
109
- /**
110
- * 可折叠表格:先显示 pageSize 行,点击「再展开」继续追加 pageSize 行。
111
- * 列宽由 widths 百分比给定(表头固定、不横向滚动)。
112
- */
113
- function Folded(props) {
114
- const rows = props.rows || [];
115
- const size = props.pageSize || 5;
116
- const [shown, setShown] = React.useState(size);
117
- const visible = rows.slice(0, shown);
118
- const rest = rows.length - visible.length;
119
- const more = rest > 0
120
- ? h("button", { className: "dshh-btn", type: "button", onClick: () => setShown(shown + size) }, "再展开 " + Math.min(size, rest) + " 个")
121
- : null;
122
- const fold = shown > size
123
- ? h("button", { className: "dshh-btn", type: "button", onClick: () => setShown(size) }, "收起")
124
- : null;
125
- return h(React.Fragment, null,
126
- h("table", { className: "dshh-table" },
127
- h("colgroup", null, (props.widths || []).map((w, i) => h("col", { key: i, style: { width: w } }))),
128
- h("thead", null, h("tr", null, (props.cols || []).map((c, i) => h("th", { key: i }, c)))),
129
- h("tbody", null, visible.map((r, i) => props.render(r, i)))),
130
- rows.length > size
131
- ? h("div", { className: "dshh-foot" },
132
- h("span", { className: "dshh-muted" }, "共 " + rows.length + " 个,已显示 " + visible.length + " 个"),
133
- more, fold)
134
- : null);
135
- }
136
-
137
- function carrierKind() {
138
- const conn = connection;
139
- if (!conn) return "无 connection";
140
- if (!conn.rpc) return "无 connection.rpc";
141
- if (typeof conn.rpc.call !== "function") return "无 rpc.call";
142
- return "可用(参数 " + (conn.rpc.call.length || "?") + ")";
143
- }
144
-
145
- /** 与超时竞速:宿主 rpc.call 不理会 AbortSignal,仅中止请求无法结束等待,页面会停在「读取中」。 */
146
- function raceTimeout(promise, ms, label) {
147
- return new Promise((resolve, reject) => {
148
- const timer = setTimeout(() => reject(new Error(label + " 超时 " + ms + " ms")), ms);
149
- Promise.resolve(promise).then(
150
- (v) => { clearTimeout(timer); resolve(v); },
151
- (e) => { clearTimeout(timer); reject(e); },
152
- );
153
- });
154
- }
155
-
156
- /** 主通道:DSH 认证的 /api 载体(宿主 registerRpcCarrier 注册)。 */
157
- async function rpc(method, payload, ms) {
158
- const conn = connection;
159
- // 必须保留 this=connection.rpc:宿主用 this 拼目标,换成别的对象会得到
160
- // "[object Object]/api" 并报 invalid RPC target。
161
- const rpcObj = conn && conn.rpc && typeof conn.rpc.call === "function" ? conn.rpc : null;
162
- if (!rpcObj) throw new Error("宿主没有 connection.rpc(载体=" + carrierKind() + ")");
163
- // 不用 AbortController 主动中止:宿主处理可能超过 8 秒,只做与超时竞速,
164
- // 让请求在宿主侧自然完成,慢一点也能拿到结果。
165
- const ac = new AbortController();
166
- const limit = ms || 30000;
167
- const t0 = Date.now();
168
- try {
169
- // 签名(与 dsh-im 一致):connection.rpc.call(target, endpoint, call, signal),且 this 必须是 connection.rpc。
170
- let r;
171
- try {
172
- r = await raceTimeout(
173
- rpcObj.call("/api", "dsh-handover", { method: method, payload: payload || {} }, ac.signal),
174
- limit + 500,
175
- "载体",
176
- );
177
- } catch (e) {
178
- if (!/invalid RPC target/i.test(String(e && e.message ? e.message : e))) throw e;
179
- // 退一步:有的宿主把 target 预置在 rpc 上,只收 (endpoint, call, signal)
180
- r = await raceTimeout(
181
- rpcObj.call("dsh-handover", { method: method, payload: payload || {} }, ac.signal),
182
- limit + 500,
183
- "载体",
184
- );
185
- }
186
- if (!r || r.ok !== true) {
187
- const err = (r && r.error) || {};
188
- throw new Error(String(err.message || "载体返回失败"));
189
- }
190
- return r.value;
191
- } catch (e) {
192
- diag.lastError = "载体:" + String(e && e.message ? e.message : e) + "(耗时 " + (Date.now() - t0) + " ms)";
193
- throw e;
194
- }
195
- }
196
-
197
- /** 写操作:先载体,失败再退 HTTP;成功统一补 ok:true,失败抛错。 */
198
- async function post(method, payload, ms) {
199
- try {
200
- const v = await rpc(method, payload, ms || 20000);
201
- return Object.assign({ ok: true }, v && typeof v === "object" ? v : { value: v });
202
- } catch (e1) {
203
- try {
204
- const b = await postJson("/dsh-handover/" + method, payload);
205
- return Object.assign({ ok: true }, b && typeof b === "object" ? b : { value: b });
206
- } catch (e2) {
207
- throw new Error(String(e1 && e1.message ? e1.message : e1) + "(HTTP 回退也失败:" + String(e2 && e2.message ? e2.message : e2) + ")");
208
- }
209
- }
210
- }
211
-
212
- /** POST JSON:同样先绝对路径再相对路径。 */
213
- async function postJson(pathname, payload) {
214
- const reasons = [];
215
- for (const base of ["", "."]) {
216
- const url = base + pathname;
217
- try {
218
- const res = await fetchWithTimeout(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }, 15000);
219
- let body = null;
220
- try { body = await res.json(); } catch (e) { body = null; }
221
- if (body) return body;
222
- reasons.push(url + " → HTTP " + res.status);
223
- } catch (e) {
224
- reasons.push(url + " → " + (e && e.message ? e.message : String(e)));
225
- }
226
- }
227
- throw new Error(reasons.join(";"));
228
- }
229
-
230
- function card(title, children) {
231
- return h("section", { className: "dshh-card" }, h("h3", { className: "dshh-cardTitle" }, title), children);
232
- }
233
-
234
- function kv(rows) {
235
- const kids = [];
236
- rows.forEach((r, i) => {
237
- kids.push(h("div", { className: "dshh-k", key: "k" + i }, r[0]));
238
- kids.push(h("div", { key: "v" + i }, r[1]));
239
- });
240
- return h("div", { className: "dshh-grid" }, kids);
241
- }
242
-
243
- function verdict(el, ok) {
244
- return h("span", { className: ok ? "dshh-ok" : "dshh-bad" }, el);
245
- }
246
-
247
- function fmtBytes(n) {
248
- const v = Number(n) || 0;
249
- if (v >= 1024 * 1024 * 1024) return (v / (1024 * 1024 * 1024)).toFixed(2) + " GB";
250
- if (v >= 1024 * 1024) return (v / (1024 * 1024)).toFixed(1) + " MB";
251
- if (v >= 1024) return (v / 1024).toFixed(1) + " KB";
252
- return v + " B";
253
- }
254
-
255
- /** 设置页正身:只读,一个刷新按钮。 */
256
- function HandoverSection() {
257
- // 首屏直接用上一次的读数(切走再切回不再出现「正在读取」),后台再刷新。
258
- const [data, setData] = React.useState(state.last);
259
- const [err, setErr] = React.useState("");
260
- const [busy, setBusy] = React.useState(false);
261
- // 首次安装向导
262
- const [parent, setParent] = React.useState("");
263
- const [created, setCreated] = React.useState("");
264
- const [setupMsg, setSetupMsg] = React.useState("");
265
- const [setupBad, setSetupBad] = React.useState(false);
266
- const [deployInfo, setDeployInfo] = React.useState(null);
267
- const [globalMsg, setGlobalMsg] = React.useState("");
268
- const [globalBad, setGlobalBad] = React.useState(false);
269
- const [working, setWorking] = React.useState(false);
270
- // 全局前置提示词
271
- const [extra, setExtra] = React.useState("");
272
- const [extraMsg, setExtraMsg] = React.useState("");
273
- const [extraBad, setExtraBad] = React.useState(false);
274
- const [channel, setChannel] = React.useState("");
275
- // 迁移:探针 / 复制 / 状态
276
- const [mvTo, setMvTo] = React.useState("");
277
- const [mvBusy, setMvBusy] = React.useState(false);
278
- const [mvMsg, setMvMsg] = React.useState("");
279
- const [mvBad, setMvBad] = React.useState(false);
280
- const [mvProbe, setMvProbe] = React.useState(null);
281
- const [mvStatus, setMvStatus] = React.useState(null);
282
- // 删除旧备份的确认词。
283
- const [delWord, setDelWord] = React.useState("");
284
-
285
- const load = React.useCallback(() => {
286
- if (state.last) setData(state.last);
287
- setBusy(true);
288
- setErr("");
289
- (async () => {
290
- let d = null;
291
- let via = "";
292
- try {
293
- d = await rpc("status", {}, 30000);
294
- via = "载体(/api)";
295
- } catch (e1) {
296
- try {
297
- d = await fetchStatus();
298
- via = "HTTP 回退";
299
- } catch (e2) {
300
- const msg = "载体(" + carrierKind() + "):" + String(e1 && e1.message ? e1.message : e1)
301
- + ";HTTP 回退:" + String(e2 && e2.message ? e2.message : e2);
302
- diag.lastError = msg;
303
- setErr(msg);
304
- setBusy(false);
305
- return;
306
- }
307
- }
308
- state.last = d;
309
- setData(d);
310
- setChannel(via);
311
- setExtra(typeof d.extraPrompt === "string" ? d.extraPrompt : "");
312
- try { setMvStatus(await rpc("move.status", {}, 8000)); } catch (e) { setMvStatus(null); }
313
- setBusy(false);
314
- })();
315
- }, []);
316
-
317
- const buildTree = React.useCallback(() => {
318
- setWorking(true); setSetupBad(false); setSetupMsg("");
319
- post("setup", { parent: parent }, 120000)
320
- .then((r) => {
321
- if (r && r.ok) {
322
- setCreated(r.root || "");
323
- setSetupMsg("已建立:目录 " + (r.created || []).length + " 个、文件 " + (r.written || []).length + " 个。下一步:注册为工作区。");
324
- } else {
325
- setSetupBad(true);
326
- setSetupMsg((r && r.error) || "建立失败");
327
- }
328
- })
329
- .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
330
- .then(() => setWorking(false));
331
- }, [parent]);
332
-
333
- const registerWs = React.useCallback(() => {
334
- if (!created) return;
335
- setWorking(true); setSetupBad(false); setSetupMsg("");
336
- post("register", { path: created, title: "全局" }, 20000)
337
- .then((r) => {
338
- if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。点击「刷新」后可继续。");
339
- else { setSetupBad(true); setSetupMsg(((r && r.error) || "注册失败") + (r && r.hint ? ";" + r.hint : "")); }
340
- })
341
- .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
342
- .then(() => setWorking(false));
343
- }, [created]);
344
-
345
- // 一键部署:建文件体系、注册「全局」工作区、建「全局」会话,一次做完。
346
- const deployAll = React.useCallback(() => {
347
- setWorking(true); setDeployInfo(null); setSetupBad(false); setSetupMsg("");
348
- post("deploy", { parent: parent }, 180000)
349
- .then((r) => {
350
- if (!r || !r.ok) {
351
- setSetupBad(true);
352
- setSetupMsg((r && r.error) || "部署失败");
353
- return;
354
- }
355
- const parts = [];
356
- parts.push("文件体系:" + r.root);
357
- if (r.workspace) parts.push("工作区:「" + r.workspace.title + "」(" + r.workspace.path + ")");
358
- else if (r.workspaceError) parts.push("工作区未注册:" + r.workspaceError);
359
- if (r.sessionId) parts.push("全局会话:" + r.sessionId + "。在左侧「" + (r.workspace ? r.workspace.title : "全局") + "」工作区打开它即可开始。");
360
- else if (r.sessionError) parts.push("会话未创建:" + r.sessionError);
361
- setCreated(r.root || "");
362
- setDeployInfo({ text: parts.join(";"), bad: false });
363
- load();
364
- })
365
- .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
366
- .then(() => setWorking(false));
367
- }, [parent, load]);
368
-
369
- // 工作区已接入但没有全局会话时,补建一个。
370
- const makeGlobalSession = React.useCallback(() => {
371
- setWorking(true); setGlobalBad(false); setGlobalMsg("");
372
- post("global-session", {}, 60000)
373
- .then((r) => {
374
- if (r && r.ok) {
375
- setGlobalMsg("已创建全局会话 " + r.sessionId + ",在左侧工作区打开它即可开始。");
376
- load();
377
- } else {
378
- setGlobalBad(true);
379
- setGlobalMsg((r && r.error) || "创建失败");
380
- }
381
- })
382
- .catch((e) => { setGlobalBad(true); setGlobalMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
383
- .then(() => setWorking(false));
384
- }, [load]);
385
-
386
- const saveExtra = React.useCallback(() => {
387
- setWorking(true); setExtraBad(false); setExtraMsg("");
388
- post("extra-prompt", { text: extra }, 20000)
389
- .then((r) => {
390
- if (r && r.ok) {
391
- if (r.unchanged) setExtraMsg(r.note || "内容未变,已跳过写入。");
392
- else setExtraMsg((r.chars ? "已保存,共 " + r.chars + " 字符。" : "已清空,不再注册提示段。") + (r.cacheNote ? " " + r.cacheNote : ""));
393
- }
394
- else { setExtraBad(true); setExtraMsg((r && r.error) || "保存失败"); }
395
- })
396
- .catch((e) => { setExtraBad(true); setExtraMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
397
- .then(() => setWorking(false));
398
- }, [extra]);
399
-
400
- // 会话角色
401
- const [roleSel, setRoleSel] = React.useState({});
402
- const [roleStage, setRoleStage] = React.useState({});
403
- const [roleMsg, setRoleMsg] = React.useState("");
404
- const [roleBad, setRoleBad] = React.useState(false);
405
-
406
- const setRoleValue = React.useCallback((sid, patch) => {
407
- setRoleSel((prev) => {
408
- const next = Object.assign({}, prev || {});
409
- next[sid] = Object.assign({}, next[sid] || {}, patch);
410
- return next;
411
- });
412
- // 改了角色就回到第一步,按钮重新显示「设置角色」。
413
- setRoleStage((prev) => {
414
- const next = Object.assign({}, prev || {});
415
- next[sid] = "";
416
- return next;
417
- });
418
- }, []);
419
-
420
- const submitRole = React.useCallback((sid, role, project, task, wake) => {
421
- setWorking(true); setRoleBad(false); setRoleMsg("");
422
- post("role", { session_id: sid, role: role, project: project, task: task, wake: wake }, 30000)
423
- .then((r) => {
424
- if (r && r.ok) {
425
- const liveText = r.live ? "已在当前会话生效" : "自下一轮起生效";
426
- const sendText = wake
427
- ? (r.delivered ? "整理指令已送达,该会话将被唤醒一轮" : ("整理指令未送达" + (r.error ? ":" + r.error : "")))
428
- : "";
429
- setRoleMsg("角色已设为" + (r.role || role) + "," + liveText + (sendText ? ";" + sendText : "。"));
430
- setRoleStage((prev) => {
431
- const next = Object.assign({}, prev || {});
432
- next[sid] = wake ? "notified" : "set";
433
- return next;
434
- });
435
- load();
436
- } else {
437
- setRoleBad(true);
438
- setRoleMsg((r && r.error) || "设置失败");
439
- }
440
- })
441
- .catch((e) => { setRoleBad(true); setRoleMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
442
- .then(() => setWorking(false));
443
- }, [load]);
444
-
445
- // 迁移:探针 → 复制并校验 → 状态 → 清旧备份
446
- const refreshMove = React.useCallback(() => {
447
- rpc("move.status", {}, 10000).then((st) => setMvStatus(st)).catch(() => {});
448
- }, []);
449
-
450
- const callMove = React.useCallback((method, payload, ms) => {
451
- setMvBusy(true); setMvBad(false); setMvMsg("");
452
- return rpc(method, payload, ms || 20000)
453
- .then((v) => v)
454
- .catch((e) => { setMvBad(true); setMvMsg(String(e && e.message ? e.message : e)); throw e; })
455
- .then((v) => v, () => null)
456
- .then((v) => { setMvBusy(false); return v; });
457
- }, []);
458
-
459
- const doProbe = React.useCallback(() => {
460
- callMove("move.probe", { to: mvTo }, 90000).then((p) => {
461
- if (!p) return;
462
- setMvProbe(p);
463
- setMvBad(!p.ok);
464
- setMvMsg(p.ok ? "预检通过,可以开始复制。" : "预检未通过,原因见下。");
465
- });
466
- }, [mvTo, callMove]);
467
-
468
- const doCopy = React.useCallback(() => {
469
- callMove("move.start", { to: mvTo }, 900000).then((r) => {
470
- if (!r) return;
471
- setMvBad(false);
472
- setMvMsg("复制与校验通过:共 " + r.report.files + " 个文件、" + fmtBytes(r.report.bytes) + "。切换脚本已生成。");
473
- refreshMove();
474
- });
475
- }, [mvTo, callMove, refreshMove]);
476
-
477
- const doSwitch = React.useCallback(() => {
478
- callMove("move.switch", {}, 30000).then((r) => {
479
- if (!r) return;
480
- setMvBad(false);
481
- setMvMsg(r.already
482
- ? "已处于切换完成状态(原路径为链接),无需再次切换。"
483
- : "已就绪:完全退出 DSH 后约 1 秒完成切换,等待 10 秒再打开。日志:" + (r.log || ""));
484
- refreshMove();
485
- });
486
- }, [callMove, refreshMove]);
487
-
488
- // 压缩授权:只有用户在此授权,压缩才排得进;模型不能自批。
489
- const [csSid, setCsSid] = React.useState("");
490
- const [csMsg, setCsMsg] = React.useState("");
491
- const doGrant = React.useCallback((kind) => {
492
- setWorking(true); setCsMsg("");
493
- post("consent.grant", { session_id: csSid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
494
- .then((r) => {
495
- if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ":5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
496
- else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
497
- })
498
- .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
499
- .then(() => setWorking(false));
500
- }, [csSid, load]);
501
- const doGrantSid = React.useCallback((sid, kind) => {
502
- setWorking(true); setCsMsg("");
503
- post("consent.grant", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
504
- .then((r) => {
505
- if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ":5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
506
- else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
507
- })
508
- .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
509
- .then(() => setWorking(false));
510
- }, [load]);
511
-
512
- const doRevoke = React.useCallback((sid, kind) => {
513
- setWorking(true); setCsMsg("");
514
- post("consent.revoke", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
515
- .then(() => { setCsMsg("已撤销 " + sid + " 的" + (kind === "succeed" ? "换任" : "压缩") + "授权。"); load(); })
516
- .catch(() => { /* 撤销失败不影响别的 */ })
517
- .then(() => setWorking(false));
518
- }, [load]);
519
-
520
- const doRetarget = React.useCallback(() => {
521
- callMove("move.retarget-stage", {}, 20000).then((r) => {
522
- if (!r) return;
523
- if (r.already) { setMvMsg("DSH 登记已在新位置,无需修改登记。"); return; }
524
- setMvBad(false);
525
- setMvMsg("已就绪:DSH 退出后改写登记,每份先备份,重启后生效。日志:" + ((r.script && r.script.log) || ""));
526
- refreshMove();
527
- });
528
- }, [callMove, refreshMove]);
529
-
530
- const doRepair = React.useCallback(() => {
531
- callMove("move.repair", {}, 20000).then((r) => {
532
- if (!r) return;
533
- setMvBad(false);
534
- setMvMsg(r.already ? "原路径已经是链接,无需重建。" : "已重建链接:" + r.src + " 现指向 " + r.dst + "(未改动任何文件)");
535
- refreshMove();
536
- });
537
- }, [callMove, refreshMove]);
538
-
539
- const doCleanup = React.useCallback(() => {
540
- if (String(delWord || "").trim() !== "删除") {
541
- setMvBad(true);
542
- setMvMsg("确认词不正确。输入「删除」后重试。");
543
- return;
544
- }
545
- callMove("move.cleanup", { confirm: true }, 180000).then((r) => {
546
- if (!r) return;
547
- setMvBad(false);
548
- setDelWord("");
549
- setMvMsg(r.note + ((r.removed || []).length ? ":" + r.removed.join("、") : ""));
550
- refreshMove();
551
- });
552
- }, [delWord, callMove, refreshMove]);
553
-
554
- // 有缓存时先出一帧旧读数,再拉新数据;没有缓存才等一次。
555
- React.useEffect(() => { installCss(); load(); }, []);
556
-
557
- const head = h("div", { className: "dshh-head" },
558
- h("h2", { className: "dshh-title" }, "dsh-handover"),
559
- h("button", { className: "dshh-btn", onClick: load, disabled: busy, type: "button" }, busy ? (data ? "正在刷新" : "正在读取") : "刷新"),
560
- diag.lastError ? h("span", { className: "dshh-bad" }, "读取失败(通道 " + (channel || carrierKind()) + "):" + diag.lastError.slice(0, 90)) : null,
561
- data ? h("span", { className: "dshh-muted" }, "读数时间 " + data.generatedAt) : null);
562
-
563
- if (err && !data) {
564
- return h("div", { className: "dshh-root" }, head,
565
- h("div", { className: "dshh-bad" }, "无法取得状态:" + err),
566
- h("div", { className: "dshh-muted" }, "插件刚安装时,确认已在配置中启用并重启 DSH。"));
567
- }
568
- if (!data) {
569
- return h("div", { className: "dshh-root" }, head, h("div", { className: "dshh-muted" }, "正在读取状态数据。"));
570
- }
571
-
572
- const p = data.plugin || {};
573
- const ws = data.workspace || {};
574
- const gate = data.gate;
575
- const ho = data.handover;
576
- const ss = data.sessions;
577
- const allSessions = (data.sessions && Array.isArray(data.sessions.all)) ? data.sessions.all : [];
578
- const byActivity = allSessions.slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
579
-
580
- // 排序原则:高频操作在前,只读数据居中,说明文字在后。
581
- const sections = [];
582
-
583
- // 一、首次安装与全局会话:未接入时给一键部署;已接入但还没有全局会话时给补建入口。
584
- const setup = data.setup;
585
- const notReady = !ws || ws.found === false;
586
- const globalCount = Number(data.globalSessions || 0);
587
- if (deployInfo) {
588
- sections.push(card("部署结果", h("div", { className: deployInfo.bad ? "dshh-bad" : "dshh-ok" }, deployInfo.text)));
589
- }
590
- if (notReady) {
591
- const msg = setupMsg ? h("div", { className: setupBad ? "dshh-bad" : "dshh-ok" }, setupMsg) : null;
592
- const steps = (setup && setup.steps) || [];
593
- const notes = Array.isArray(data.notes) ? data.notes : [];
594
- sections.push(card("首次安装",
595
- h("div", null,
596
- h("div", { className: "dshh-muted" }, "本页与工具已可用;接入工作区后显示闸门、交接体检与会话水位。"),
597
- h("ol", { className: "dshh-list" }, steps.map((s, i) => h("li", { key: i }, s))),
598
- notes.length ? h("ul", { className: "dshh-list" }, notes.map((n, i) => h("li", { key: i }, n))) : null,
599
- h("input", {
600
- className: "dshh-input",
601
- type: "text",
602
- value: parent,
603
- placeholder: "填写绝对路径:全局根 _Workspace 将建在该目录下",
604
- onChange: (e) => setParent(e.target.value),
605
- }),
606
- h("div", { className: "dshh-row" },
607
- h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: deployAll }, working ? "正在部署" : "一键部署"),
608
- h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: buildTree }, working ? "正在建立" : "仅建立文件体系"),
609
- created ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: registerWs }, "注册为工作区") : null),
610
- created ? h("div", { className: "dshh-code" }, created) : null,
611
- msg)));
612
- } else if (globalCount === 0) {
613
- sections.push(card("全局会话",
614
- h("div", null,
615
- h("div", { className: "dshh-muted" }, "工作区已接入,尚无「全局」会话。点击下方按钮新建,插件会将其登记为全局角色。"),
616
- h("div", { className: "dshh-muted" }, "全局根:" + (ws.root || "")),
617
- h("div", { className: "dshh-row" },
618
- h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: makeGlobalSession }, working ? "正在创建" : "创建全局会话")),
619
- globalMsg ? h("div", { className: globalBad ? "dshh-bad" : "dshh-ok" }, globalMsg) : null)));
620
- }
621
-
622
- // 二、授权:唯一合法的「用户同意」入口,置顶;只列在任会话,超过 5 个折叠。
623
- const consentRows = (Array.isArray(data.consent) ? data.consent : []).filter((r) => r.onduty !== false);
624
- const dutyRows = byActivity.filter((r) => r.onduty !== false);
625
- const grantRow = (kind) => (r, i) => h("tr", { key: i },
626
- h("td", null, r.title || "(无标题)"),
627
- h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
628
- h("td", null, h("button", {
629
- className: "dshh-btn",
630
- type: "button",
631
- disabled: working,
632
- title: r.sid,
633
- onClick: () => doGrantSid(r.sid, kind),
634
- }, kind === "succeed" ? "授权换任" : "授权压缩")));
635
- const grantTable = (kind) => h(Folded, {
636
- rows: dutyRows,
637
- pageSize: 5,
638
- cols: ["会话", "短号", "操作"],
639
- widths: ["58%", "12%", "30%"],
640
- render: grantRow(kind),
641
- });
642
- const authKids = [];
643
- if (dutyRows.length) {
644
- authKids.push(h("div", { className: "dshh-muted" }, "点击「授权压缩」后,该会话 5 分钟内可压缩一次,用后失效;也可在会话中发送 /allow-compact。"));
645
- authKids.push(grantTable("compact"));
646
- authKids.push(h("div", { className: "dshh-muted" }, "换任同样须授权:点击「授权换任」后 5 分钟内可换任一次;也可在会话中发送 /allow-succeed。"));
647
- authKids.push(grantTable("succeed"));
648
- } else {
649
- authKids.push(h("div", { className: "dshh-muted" }, "暂无可点选的在任会话;可在下方直接填会话 id 授权。"));
650
- }
651
- authKids.push(h("div", { className: "dshh-row" },
652
- h("input", {
653
- className: "dshh-inputSm",
654
- type: "text",
655
- value: csSid,
656
- placeholder: "或填写会话 id 或 8 位短号",
657
- onChange: (e) => setCsSid(e.target.value),
658
- }),
659
- h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("compact") }, working ? "正在授权" : "授权压缩"),
660
- h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("succeed") }, "授权换任")));
661
- if (consentRows.length) {
662
- authKids.push(h("div", { className: "dshh-ok" }, "有授权"));
663
- authKids.push(h("table", { className: "dshh-table" },
664
- h("colgroup", null, h("col", { style: { width: "62%" } }), h("col", { style: { width: "18%" } }), h("col", { style: { width: "20%" } })),
665
- h("thead", null, h("tr", null, h("th", null, "已授权会话"), h("th", null, "剩余"), h("th", null, "操作"))),
666
- h("tbody", null, consentRows.map((r, i) => h("tr", { key: i },
667
- h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId) + (r.kind === "succeed" ? " · 换任" : " · 压缩")),
668
- h("td", { className: "dshh-num" }, Math.round((r.leftMs || 0) / 1000) + " 秒"),
669
- h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId, r.kind) }, "撤销")))))));
670
- }
671
- if (csMsg) authKids.push(h("div", { className: "dshh-ok" }, csMsg));
672
- sections.push(card("授权", h("div", null, authKids)));
673
-
674
- // 三、移动全局根目录:常态折叠为一行状态。
675
- {
676
- const st = mvStatus;
677
- const phaseLabel = {
678
- none: "尚无迁移记录",
679
- verified: "已复制并校验,待切换",
680
- switched: "已切换,原路径为链接",
681
- "switched+cleaned": "已切换,旧备份已删除",
682
- partial: "校验有差异,需重新复制",
683
- };
684
- // 旧备份是否保留不影响使用,因此不计入「需要处理」;需要处理的只有会妨碍使用的情形。
685
- const hasOlds = !!(st && st.phase === "switched" && (st.olds || []).length);
686
- const attention = !!(st && (st.linkBroken || st.srcExists === false || st.phase === "verified" || st.phase === "partial" || (st.retarget && st.retarget.needed)));
687
- const stateText = st
688
- ? "状态:" + (phaseLabel[st.phase] || st.phase) + (st.dst ? "(" + st.dst + ")" : "") + (hasOlds ? ";旧备份保留,不影响使用" : "")
689
- : "正在读取迁移状态";
690
- const kids = [];
691
- kids.push(h("div", { className: "dshh-muted" }, "切换只改名与建链接,不改动文件内容。"));
692
- if (st && st.phase && st.phase.indexOf("switched") === 0) {
693
- kids.push(h("div", { className: "dshh-muted" }, "原路径为指向新位置的链接;若已删除,可用「重建链接」恢复。"));
694
- }
695
- if (st && (st.srcExists === false || st.linkBroken)) {
696
- kids.push(h("div", { className: "dshh-bad" }, st.linkBroken
697
- ? "链接指向的新位置不存在:" + (st.dst || "") + "。应先确认真实数据位置;本页不改动文件。"
698
- : "原路径不存在(可能已被删除):" + (st.src || "") + "。数据仍在 " + (st.dst || "") + "。"));
699
- if (st.canRepair) {
700
- kids.push(h("div", { className: "dshh-row" },
701
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doRepair }, "重建链接")));
702
- }
703
- }
704
- if (st && st.phase === "verified" && st.script) {
705
- kids.push(h("div", { className: "dshh-sub" }, "切换步骤"));
706
- kids.push(h("ol", { className: "dshh-list" },
707
- h("li", null, "点击「准备切换」,助手进入待命状态。"),
708
- h("li", null, "完全退出 DSH,含托盘与后台进程。"),
709
- h("li", null, "退出后自动执行,约 50 毫秒。"),
710
- h("li", null, "等待 10 秒后再打开 DSH。"),
711
- h("li", null, "回到本页,状态应为「已切换,原路径为链接」。")));
712
- kids.push(h("div", { className: "dshh-row" },
713
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doSwitch }, mvBusy ? "正在准备" : "准备切换(退出 DSH 后自动完成)")));
714
- kids.push(h("div", { className: "dshh-muted" }, "助手未运行时手工执行:" + ((st.script && st.script.helper) || "")));
715
- }
716
- if (st && st.retarget && st.retarget.needed) {
717
- kids.push(h("div", { className: "dshh-warn" }, "DSH 登记仍指向旧路径:" + st.retarget.total + " 处,涉及 " + st.retarget.files.length + " 个文件。"));
718
- kids.push(h("div", { className: "dshh-muted" }, "退出 DSH 时改写,每份先备份;重启后生效。"));
719
- kids.push(h("div", { className: "dshh-row" },
720
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doRetarget }, "把登记迁到新位置(退出 DSH 后生效)")));
721
- for (const rf of (st.retarget.files || [])) {
722
- kids.push(h("div", { className: "dshh-code dshh-muted" }, rf.path + " ×" + rf.count));
723
- }
724
- }
725
- if (st && st.retarget && st.retarget.needed === false) {
726
- kids.push(h("div", { className: "dshh-ok" }, "DSH 登记已指向新位置:" + (st.dst || "") + "。"));
727
- }
728
- if (hasOlds) {
729
- kids.push(h("div", { className: "dshh-muted" }, "旧备份:" + st.olds.map((o) => o.path + "(" + fmtBytes(o.bytes) + ")").join("、") + "。保留不影响使用;删除后无法恢复。"));
730
- kids.push(h("div", { className: "dshh-row" },
731
- h("input", {
732
- className: "dshh-inputSm",
733
- type: "text",
734
- value: delWord,
735
- placeholder: "输入「删除」以确认",
736
- onChange: (e) => setDelWord(e.target.value),
737
- }),
738
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || delWord.trim() !== "删除", onClick: doCleanup }, "删除旧备份(不可撤销)")));
739
- }
740
- kids.push(h("div", { className: "dshh-row" },
741
- h("input", {
742
- className: "dshh-input",
743
- type: "text",
744
- value: mvTo,
745
- placeholder: "新根目录的绝对路径",
746
- onChange: (e) => setMvTo(e.target.value),
747
- })));
748
- kids.push(h("div", { className: "dshh-row" },
749
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || !mvTo, onClick: doProbe }, mvBusy ? "正在预检" : "预检"),
750
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || !mvTo, onClick: doCopy }, "复制并校验"),
751
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: refreshMove }, "刷新状态")));
752
- if (mvProbe) {
753
- kids.push(kv([
754
- ["源", h("span", { className: "dshh-code" }, mvProbe.src)],
755
- ["目标", h("span", { className: "dshh-code" }, mvProbe.dst)],
756
- ["总大小", mvProbe.files + " 文件 · " + fmtBytes(mvProbe.bytes)],
757
- ["目标盘可用", mvProbe.freeBytes === null || mvProbe.freeBytes === undefined ? "?" : fmtBytes(mvProbe.freeBytes)],
758
- ["结论", mvProbe.ok ? verdict("通过", true) : verdict("不通过", false)],
759
- ]));
760
- for (const w of (mvProbe.warnings || [])) kids.push(h("div", { className: "dshh-warn" }, "注意:" + w));
761
- for (const x of (mvProbe.problems || [])) kids.push(h("div", { className: "dshh-bad" }, "未通过:" + x));
762
- }
763
- if (mvMsg) kids.push(h("div", { className: mvBad ? "dshh-bad" : "dshh-ok" }, mvMsg));
764
- sections.push(card("移动全局根目录",
765
- h("div", null,
766
- h("details", null,
767
- h("summary", { className: "dshh-sum" }, (attention ? "需要处理:" : "") + stateText),
768
- h("div", { className: "dshh-wrap" }, kids)))));
769
- }
770
-
771
- // 四、会话角色:按 DSH 工作区分组,默认全部折叠;组内按全局、项目、杂项、普通排列。
772
- const ROLE_ORDER = { global: 0, project: 1, misc: 2, plain: 3 };
773
- const byRole = byActivity.slice().sort((a, b) => {
774
- const ra = ROLE_ORDER[a.role] === undefined ? 9 : ROLE_ORDER[a.role];
775
- const rb = ROLE_ORDER[b.role] === undefined ? 9 : ROLE_ORDER[b.role];
776
- return ra !== rb ? ra - rb : String(b.seen || "").localeCompare(String(a.seen || ""));
777
- });
778
- const wsList = (ss && Array.isArray(ss.workspaces)) ? ss.workspaces : [];
779
- const roleGroups = [];
780
- const claimed = {};
781
- for (const w of wsList) {
782
- const rows = byRole.filter((r) => r.wsKey === w.key);
783
- claimed[w.key] = true;
784
- if (rows.length) roleGroups.push({ key: w.key, title: w.title || w.path || "工作区", path: w.path || "", rows });
785
- }
786
- const restRows = byRole.filter((r) => !claimed[r.wsKey]);
787
- if (restRows.length) roleGroups.push({ key: "", title: "未归属工作区", path: "", rows: restRows });
788
- const roleRow = (r, i) => {
789
- const draft = (roleSel && roleSel[r.sid]) || {};
790
- const role = draft.role !== undefined ? draft.role : (r.role || "plain");
791
- const project = draft.project !== undefined ? draft.project : (r.project || "");
792
- const task = draft.task !== undefined ? draft.task : (r.task || "");
793
- const wide = role === "project" || role === "misc";
794
- const stage = (roleStage && roleStage[r.sid]) || "";
795
- const action = stage === "notified"
796
- ? h("button", { className: "dshh-btn", type: "button", disabled: true }, "已通知")
797
- : stage === "set"
798
- ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => submitRole(r.sid, role, project, task, true) }, "通知该会话")
799
- : h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => submitRole(r.sid, role, project, task, false) }, "设置角色");
800
- return h("tr", { key: i },
801
- h("td", null, r.title || "(无标题)"),
802
- h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
803
- h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
804
- h("td", null, h("select", {
805
- className: "dshh-inputSm",
806
- value: role,
807
- onChange: (e) => setRoleValue(r.sid, { role: e.target.value }),
808
- },
809
- h("option", { value: "global" }, "全局"),
810
- h("option", { value: "project" }, "项目"),
811
- h("option", { value: "misc" }, "杂项"),
812
- h("option", { value: "plain" }, "普通"))),
813
- h("td", null, wide ? h("input", {
814
- className: "dshh-inputSm",
815
- type: "text",
816
- value: role === "project" ? project : task,
817
- placeholder: role === "project" ? "填写项目名" : "填写任务名",
818
- onChange: (e) => setRoleValue(r.sid, role === "project" ? { project: e.target.value } : { task: e.target.value }),
819
- }) : h("span", { className: "dshh-muted" }, "不需填写")),
820
- h("td", null, h("div", { className: "dshh-pick" }, action)));
821
- };
822
- sections.push(card("会话角色",
823
- h("div", null,
824
- roleGroups.length
825
- ? roleGroups.map((g, gi) => h("details", { key: gi },
826
- h("summary", { className: "dshh-sum" }, g.title + "(" + g.rows.length + " 个会话)"),
827
- h("div", { className: "dshh-wrap" },
828
- g.path ? h("div", { className: "dshh-muted dshh-code" }, g.path) : null,
829
- h(Folded, {
830
- rows: g.rows,
831
- pageSize: 5,
832
- cols: ["会话", "短号", "水位", "角色", "项目或任务名", "操作"],
833
- widths: ["30%", "9%", "7%", "12%", "20%", "22%"],
834
- render: roleRow,
835
- }))))
836
- : h("div", { className: "dshh-muted" }, "暂无可设置角色的会话。空白会话不列出。"),
837
- roleMsg ? h("div", { className: roleBad ? "dshh-bad" : "dshh-ok" }, roleMsg) : null)));
838
-
839
- // 五、全局前置提示词:默认折叠,只显示已保存字数。
840
- sections.push(card("全局前置提示词",
841
- h("div", null,
842
- h("div", { className: "dshh-muted" }, "只注入你填写的内容,不含插件约束;对全部会话生效,留空保存即不注册提示段。"),
843
- h("details", null,
844
- h("summary", { className: "dshh-sum" }, "已保存 " + (extra ? extra.length : 0) + " 字符,点击编辑"),
845
- h("textarea", {
846
- className: "dshh-textarea",
847
- rows: 6,
848
- value: extra,
849
- placeholder: "例如:一律用中文;改动前说明改动范围;简单任务不必拆分。",
850
- onChange: (e) => setExtra(e.target.value),
851
- }),
852
- h("div", { className: "dshh-row" },
853
- h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: saveExtra }, working ? "正在保存" : "保存"),
854
- h("span", { className: "dshh-muted" }, extra.length + " 字符")),
855
- extraMsg ? h("div", { className: extraBad ? "dshh-bad" : "dshh-ok" }, extraMsg) : null))));
856
-
857
- // 六、会话水位:只读数据区从这里开始;只列在任会话(全局、项目、杂项各一行)。
858
- if (ss) {
859
- const byRecent = (ss.rows || []).filter((r) => r.onduty !== false).slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
860
- const waterRow = (r, i) => h("tr", { key: i },
861
- h("td", null, r.dutyName || r.dutyKey || r.title || r.project || "(无名)"),
862
- h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
863
- h("td", null, r.dutyKind || r.roleLabel || ""),
864
- h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
865
- h("td", null, r.state),
866
- h("td", null, r.active ? "活跃" : "沉默"),
867
- h("td", { className: "dshh-muted dshh-num" }, shortTime(r.seen)));
868
- sections.push(card("会话水位",
869
- h("div", null,
870
- h("div", { className: "dshh-muted" }, ss.verdict + "。活跃线 " + ss.cutoff + ",回溯 " + ss.silentHours + " 小时。"),
871
- (ss.notify || []).length
872
- ? h("div", null, "本轮需通知:" + ss.notify.map((r) => (r.name || r.key || "") + "(" + shortSid(r.sid) + ")").join("、"))
873
- : h("div", { className: "dshh-muted" }, "本轮无需通知。"),
874
- byRecent.length
875
- ? h(Folded, {
876
- rows: byRecent,
877
- pageSize: 5,
878
- cols: ["会话", "短号", "类别", "水位", "状态", "活跃", "最后活动"],
879
- widths: ["20%", "11%", "7%", "8%", "8%", "7%", "31%"],
880
- render: waterRow,
881
- })
882
- : h("div", { className: "dshh-muted" }, "暂无可显示的在任会话。"))));
883
- }
884
-
885
- // 七、通用约束条闸门
886
- if (gate) {
887
- const rows = (gate.libraries || []).map((r, i) => h("tr", { key: i },
888
- h("td", null, r.name),
889
- h("td", null, r.titleOk ? "通过" : "缺"),
890
- h("td", null, (r.lines - r.missing) + "/" + r.lines),
891
- h("td", null, r.waterOk ? "通过" : "缺"),
892
- h("td", null, verdict(r.ok ? "通过" : "未通过", r.ok))));
893
- sections.push(card("通用约束条闸门",
894
- gate.error
895
- ? h("div", { className: "dshh-bad" }, gate.error)
896
- : h("div", null,
897
- h("div", { className: "dshh-muted" }, gate.total + " 个库,正文 " + gate.bodyLines + " 行;" + gate.verdict),
898
- h("table", { className: "dshh-table" },
899
- h("thead", null, h("tr", null,
900
- h("th", null, "库"), h("th", null, "标题"), h("th", null, "正文"), h("th", null, "水位自查"), h("th", null, "结论"))),
901
- h("tbody", null, rows)))));
902
- }
903
-
904
- // 八、交接文档体检:全局一行,项目与杂项同一张表;问题列只留问题本身。
905
- if (ho) {
906
- const cleanReason = (x) => String(x || "").replace(/([^)]*)/g, "").replace(/\s+/g, " ").trim();
907
- const rows = (ho.projects || []).map((r) => Object.assign({}, r, { kind: "项目" }))
908
- .concat((ho.misc || []).map((r) => Object.assign({}, r, { kind: "杂项" })));
909
- const prows = rows.map((r, i) => h("tr", { key: i },
910
- h("td", null, r.kind),
911
- h("td", null, r.name),
912
- h("td", null, r.verdict === "通过" ? verdict("通过", true) : (r.verdict === "缺节" || r.verdict === "读不到" || r.verdict === "未找到") ? h("span", { className: "dshh-warn" }, r.verdict) : verdict(r.verdict, false)),
913
- h("td", null, (r.reasons || []).map(cleanReason).filter(Boolean).join(";"))));
914
- sections.push(card("交接文档体检",
915
- h("div", null,
916
- h("div", null, "全局 Global_Handover.md:", ho.global.verdict === "通过" ? verdict("通过", true) : verdict(ho.global.verdict, false),
917
- (ho.global.reasons || []).length ? h("span", { className: "dshh-muted" }, (ho.global.reasons || []).map(cleanReason).filter(Boolean).join(";")) : null),
918
- prows.length
919
- ? h("table", { className: "dshh-table" },
920
- h("thead", null, h("tr", null, h("th", null, "类别"), h("th", null, "名称"), h("th", null, "结论"), h("th", null, "问题"))),
921
- h("tbody", null, prows))
922
- : h("div", { className: "dshh-muted" }, "暂无已登记的项目或杂项。"))));
923
- }
924
-
925
- // 九、插件信息
926
- sections.push(card("插件", kv([
927
- ["名称", h("span", { className: "dshh-code" }, p.name + "@" + p.version)],
928
- ["工具数", String(p.tools)],
929
- ["压缩调度器", p.scheduler],
930
- ["注入项", (p.inject || []).join("、")],
931
- ["初始化", (p.initErrors && p.initErrors.length) ? h("span", { className: "dshh-bad" }, p.initErrors.join(";")) : verdict("正常", true)],
932
- ["工作区", ws.found ? h("span", { className: "dshh-code" }, ws.root) : verdict("未接入(未找到 _Control_Center)", false)],
933
- ])));
934
-
935
- // 十、最近一次会话间通信判定:只有存在判定时才出现。
936
- const g = data.guardLast;
937
- if (g) {
938
- sections.push(card("最近一次会话间通信判定", kv([
939
- ["结论", g.allow ? verdict("允许", true) : verdict("拒绝", false)],
940
- ["依据", g.source + (g.title ? ";会话标题 " + g.title : "")],
941
- ["会话", h("span", { className: "dshh-code" }, g.sid || "-")],
942
- ])));
943
- }
944
-
945
- // 末、工具明细与使用说明:参考性质,放最后,默认折叠。
946
- const gd = data.guide;
947
- if (gd) {
948
- const sit = (gd.situations || []).map((r, i) => h("tr", { key: i },
949
- h("td", null, r.when),
950
- h("td", null, r.say),
951
- h("td", { className: "dshh-muted" }, r.what)));
952
- const groups = (gd.groups || []).map((gr, i) => h("div", { key: i },
953
- h("div", { className: "dshh-sub" }, gr.title),
954
- h("table", { className: "dshh-table" }, h("tbody", null, (gr.tools || []).map((t, j) => h("tr", { key: j },
955
- h("td", { className: "dshh-code" }, t[0]),
956
- h("td", { className: "dshh-muted" }, t[1])))))));
957
- const toolCount = (gd.groups || []).reduce((a, gr) => a + ((gr.tools || []).length), 0);
958
- if (toolCount) {
959
- sections.push(card("工具明细",
960
- h("details", null,
961
- h("summary", { className: "dshh-sum" }, "共 " + toolCount + " 个工具,点击展开"),
962
- h("div", { className: "dshh-wrap" }, groups))));
963
- }
964
- sections.push(card("使用说明",
965
- h("details", null,
966
- h("summary", { className: "dshh-sum" }, "交互方式、常见请求与约定"),
967
- h("div", { className: "dshh-wrap" },
968
- (gd.intro || []).map((t, i) => h("div", { key: i }, t)),
969
- h("div", { className: "dshh-sub" }, "常见请求与对应动作"),
970
- h("table", { className: "dshh-table" },
971
- h("thead", null, h("tr", null, h("th", null, "时机"), h("th", null, "可以这样说"), h("th", null, "插件会执行的动作"))),
972
- h("tbody", null, sit)),
973
- h("div", { className: "dshh-sub" }, "约定"),
974
- h("ul", { className: "dshh-list" }, (gd.rules || []).map((t, i) => h("li", { key: i }, t)))))));
975
- }
976
-
977
- return h("div", { className: "dshh-root" }, head, sections);
978
- }
979
-
980
- /** 把只读状态页注册进设置导航。 */
981
- function apply(ctx) {
982
- try { connection = (ctx && ctx.connection) || null; } catch (e) { connection = null; }
983
- ctx.slots.inject("settings.section", () => ctx.slots.register({
984
- name: "settings.section",
985
- id: "handover",
986
- order: 100,
987
- label: "dsh-handover",
988
- }, HandoverSection));
989
- }
990
-
991
- exports.apply = apply;
992
- exports.inject = inject;
993
- return module.exports;
994
- },
1
+ /**
2
+ * dsh-handover 浏览器侧:在「设置 → 交接」注册只读状态页。
3
+ * 手工构建的 DSH client bundle(不使用 JSX 与打包器):只 require react,
4
+ * 数据取自宿主侧注册的 GET /dsh-handover/status.json(只读,不唤醒任何会话)。
5
+ *
6
+ * 修改本文件后须重建 dist,并在 DSH 中重新激活插件(client bundle 不热更新)。
7
+ */
8
+ window.__ModuleLoader__.load({
9
+ id: '@mingmaple/dsh-handover',
10
+ factory: (require) => {
11
+ var module = { exports: {} };
12
+ var exports = module.exports;
13
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
+
15
+ const React = require("react");
16
+ const h = React.createElement;
17
+ /** 参与槽位注册的服务;connection 为 DSH 认证的 /api 载体(设置页取数的主通道)。 */
18
+ const inject = ["slots", "connection"];
19
+ let connection = null;
20
+
21
+ /** 数据接口候选:优先绝对路径,其次相对路径(页面挂在子路径时仍可读取)。 */
22
+ const ENDPOINTS = ["/dsh-handover/status.json", "dsh-handover/status.json"];
23
+
24
+ const CSS_TAG_ID = "@mingmaple/dsh-handover/SettingsSection.css";
25
+ const CSS = [
26
+ ".dshh-root{display:flex;flex-direction:column;gap:14px;max-width:860px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.55}",
27
+ ".dshh-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}",
28
+ ".dshh-title{margin:0;font-size:16px;font-weight:600}",
29
+ ".dshh-muted{color:var(--dsw-alias-label-tertiary)}",
30
+ ".dshh-btn{font:inherit;cursor:pointer;background:0 0;color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 10px}",
31
+ ".dshh-btn:disabled{opacity:.5;cursor:default}",
32
+ ".dshh-card{border:.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:8px}",
33
+ ".dshh-cardTitle{margin:0;font-weight:600;font-size:13px}",
34
+ ".dshh-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 14px}",
35
+ ".dshh-k{color:var(--dsw-alias-label-tertiary)}",
36
+ ".dshh-table{width:100%;border-collapse:collapse;table-layout:fixed}",
37
+ ".dshh-table th,.dshh-table td{text-align:left;padding:3px 6px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-size:12px;white-space:normal;overflow-wrap:anywhere;vertical-align:middle}",
38
+ ".dshh-table th{white-space:nowrap;font-weight:600}",
39
+ ".dshh-num{white-space:nowrap}",
40
+ ".dshh-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px}",
41
+ ".dshh-pick{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
42
+ ".dshh-inputSm{font:inherit;font-size:12px;padding:2px 6px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary);box-sizing:border-box;width:100%}",
43
+ ".dshh-ok{color:var(--dsw-alias-state-business-primary,#1a7f37)}",
44
+ ".dshh-bad{color:var(--dsw-alias-state-error-primary,#c62828)}",
45
+ ".dshh-warn{color:var(--dsw-alias-state-warning-primary,#b26a00)}",
46
+ ".dshh-list{margin:0;padding-left:18px}",
47
+ ".dshh-sub{margin:6px 0 2px;font-weight:600;color:var(--dsw-alias-label-primary)}",
48
+ ".dshh-sum{cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:12px}",
49
+ ".dshh-wrap td{white-space:normal}",
50
+ ".dshh-input{font:inherit;width:100%;box-sizing:border-box;padding:5px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary)}",
51
+ ".dshh-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:2px}",
52
+ ".dshh-textarea{font:inherit;width:100%;box-sizing:border-box;padding:6px 8px;border:.5px solid var(--dsw-alias-border-l2);border-radius:6px;background:0 0;color:var(--dsw-alias-label-primary);resize:vertical}",
53
+ ".dshh-code{font-family:ui-monospace,Consolas,monospace;font-size:12px}",
54
+ ].join("");
55
+
56
+ /** 注入样式(按 tagId 去重,插件重复激活时不会堆叠)。 */
57
+ function installCss() {
58
+ if (typeof document === "undefined") return;
59
+ if (document.querySelector('style[data-plugin-css="' + CSS_TAG_ID + '"]') !== null) return;
60
+ const tag = document.createElement("style");
61
+ tag.dataset.plugin = "@mingmaple/dsh-handover";
62
+ tag.dataset.pluginCss = CSS_TAG_ID;
63
+ tag.textContent = CSS;
64
+ document.head.appendChild(tag);
65
+ }
66
+
67
+ /** 带超时的 fetch(旧通道回退用;无超时则会一直停在「读取中」)。 */
68
+ async function fetchWithTimeout(url, opts, ms) {
69
+ const ac = new AbortController();
70
+ const timer = setTimeout(() => ac.abort(), ms || 8000);
71
+ try {
72
+ return await fetch(url, Object.assign({ signal: ac.signal }, opts || {}));
73
+ } finally {
74
+ clearTimeout(timer);
75
+ }
76
+ }
77
+
78
+ /** 依次尝试候选地址读取状态;全部失败时抛出含全部原因的错。 */
79
+ async function fetchStatus() {
80
+ const reasons = [];
81
+ for (const url of ENDPOINTS) {
82
+ try {
83
+ const res = await fetchWithTimeout(url, { cache: "no-store", headers: { accept: "application/json" } }, 30000);
84
+ if (!res.ok) { reasons.push(url + " → HTTP " + res.status); continue; }
85
+ return await res.json();
86
+ } catch (e) {
87
+ reasons.push(url + " → " + (e && e.message ? e.message : String(e)));
88
+ }
89
+ }
90
+ throw new Error(reasons.join(";") || "无法取得状态");
91
+ }
92
+
93
+ /** 通道诊断(显示于页面;出现异常时可直接定位,无须推测)。 */
94
+ const diag = { lastError: "" };
95
+
96
+ /** 页面级快照缓存:设置页切走再切回时先渲染旧读数,随后后台刷新,不出现「正在读取」。 */
97
+ const state = { last: null };
98
+
99
+ /** 读数时间保留完整值(含年份与时区),与页头一致。 */
100
+ function shortTime(s) {
101
+ return String(s || "");
102
+ }
103
+
104
+ /** 会话短号:去掉 session- 前缀,留前 8 位。 */
105
+ function shortSid(s) {
106
+ return String(s || "").replace(/^session-/, "").slice(0, 8);
107
+ }
108
+
109
+ /**
110
+ * 可折叠表格:先显示 pageSize 行,点击「再展开」继续追加 pageSize 行。
111
+ * 列宽由 widths 百分比给定(表头固定,不横向滚动)。
112
+ */
113
+ function Folded(props) {
114
+ const rows = props.rows || [];
115
+ const size = props.pageSize || 5;
116
+ const [shown, setShown] = React.useState(size);
117
+ const visible = rows.slice(0, shown);
118
+ const rest = rows.length - visible.length;
119
+ const more = rest > 0
120
+ ? h("button", { className: "dshh-btn", type: "button", onClick: () => setShown(shown + size) }, "再展开 " + Math.min(size, rest) + " 个")
121
+ : null;
122
+ const fold = shown > size
123
+ ? h("button", { className: "dshh-btn", type: "button", onClick: () => setShown(size) }, "收起")
124
+ : null;
125
+ return h(React.Fragment, null,
126
+ h("table", { className: "dshh-table" },
127
+ h("colgroup", null, (props.widths || []).map((w, i) => h("col", { key: i, style: { width: w } }))),
128
+ h("thead", null, h("tr", null, (props.cols || []).map((c, i) => h("th", { key: i }, c)))),
129
+ h("tbody", null, visible.map((r, i) => props.render(r, i)))),
130
+ rows.length > size
131
+ ? h("div", { className: "dshh-foot" },
132
+ h("span", { className: "dshh-muted" }, "共 " + rows.length + " 个,已显示 " + visible.length + " 个"),
133
+ more, fold)
134
+ : null);
135
+ }
136
+
137
+ function carrierKind() {
138
+ const conn = connection;
139
+ if (!conn) return "无 connection";
140
+ if (!conn.rpc) return "无 connection.rpc";
141
+ if (typeof conn.rpc.call !== "function") return "无 rpc.call";
142
+ return "可用(参数 " + (conn.rpc.call.length || "?") + ")";
143
+ }
144
+
145
+ /** 与超时竞速:宿主 rpc.call 不理会 AbortSignal,仅中止请求无法结束等待,页面会停在「读取中」。 */
146
+ function raceTimeout(promise, ms, label) {
147
+ return new Promise((resolve, reject) => {
148
+ const timer = setTimeout(() => reject(new Error(label + " 超时 " + ms + " ms")), ms);
149
+ Promise.resolve(promise).then(
150
+ (v) => { clearTimeout(timer); resolve(v); },
151
+ (e) => { clearTimeout(timer); reject(e); },
152
+ );
153
+ });
154
+ }
155
+
156
+ /** 主通道:DSH 认证的 /api 载体(由宿主 registerRpcCarrier 注册)。 */
157
+ async function rpc(method, payload, ms) {
158
+ const conn = connection;
159
+ // 必须保留 this=connection.rpc:宿主用 this 拼接目标,换成其他对象会得到
160
+ // "[object Object]/api" 并报 invalid RPC target。
161
+ const rpcObj = conn && conn.rpc && typeof conn.rpc.call === "function" ? conn.rpc : null;
162
+ if (!rpcObj) throw new Error("宿主没有 connection.rpc(载体=" + carrierKind() + ")");
163
+ // 不主动中止请求:宿主处理可能超过 8 秒,此处仅与超时竞速,
164
+ // 请求在宿主侧自然完成,耗时较长也能取得结果。
165
+ const ac = new AbortController();
166
+ const limit = ms || 30000;
167
+ const t0 = Date.now();
168
+ try {
169
+ // 调用签名(与 dsh-im 一致):connection.rpc.call(target, endpoint, call, signal)。
170
+ // this 须为 connection.rpc。
171
+ let r;
172
+ try {
173
+ r = await raceTimeout(
174
+ rpcObj.call("/api", "dsh-handover", { method: method, payload: payload || {} }, ac.signal),
175
+ limit + 500,
176
+ "载体",
177
+ );
178
+ } catch (e) {
179
+ if (!/invalid RPC target/i.test(String(e && e.message ? e.message : e))) throw e;
180
+ // 回退处理:部分宿主将 target 预置在 rpc 上,只接受 (endpoint, call, signal)
181
+ r = await raceTimeout(
182
+ rpcObj.call("dsh-handover", { method: method, payload: payload || {} }, ac.signal),
183
+ limit + 500,
184
+ "载体",
185
+ );
186
+ }
187
+ if (!r || r.ok !== true) {
188
+ const err = (r && r.error) || {};
189
+ throw new Error(String(err.message || "载体返回失败"));
190
+ }
191
+ return r.value;
192
+ } catch (e) {
193
+ diag.lastError = "载体:" + String(e && e.message ? e.message : e) + "(耗时 " + (Date.now() - t0) + " ms)";
194
+ throw e;
195
+ }
196
+ }
197
+
198
+ /** 写操作:先走载体,失败再回退 HTTP;成功统一补 ok:true,失败抛错。 */
199
+ async function post(method, payload, ms) {
200
+ try {
201
+ const v = await rpc(method, payload, ms || 20000);
202
+ return Object.assign({ ok: true }, v && typeof v === "object" ? v : { value: v });
203
+ } catch (e1) {
204
+ try {
205
+ const b = await postJson("/dsh-handover/" + method, payload);
206
+ return Object.assign({ ok: true }, b && typeof b === "object" ? b : { value: b });
207
+ } catch (e2) {
208
+ throw new Error(String(e1 && e1.message ? e1.message : e1) + "(HTTP 回退也失败:" + String(e2 && e2.message ? e2.message : e2) + ")");
209
+ }
210
+ }
211
+ }
212
+
213
+ /** POST JSON:同样先绝对路径、后相对路径。 */
214
+ async function postJson(pathname, payload) {
215
+ const reasons = [];
216
+ for (const base of ["", "."]) {
217
+ const url = base + pathname;
218
+ try {
219
+ const res = await fetchWithTimeout(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload || {}) }, 15000);
220
+ let body = null;
221
+ try { body = await res.json(); } catch (e) { body = null; }
222
+ if (body) return body;
223
+ reasons.push(url + " → HTTP " + res.status);
224
+ } catch (e) {
225
+ reasons.push(url + " → " + (e && e.message ? e.message : String(e)));
226
+ }
227
+ }
228
+ throw new Error(reasons.join(";"));
229
+ }
230
+
231
+ function card(title, children) {
232
+ return h("section", { className: "dshh-card" }, h("h3", { className: "dshh-cardTitle" }, title), children);
233
+ }
234
+
235
+ function kv(rows) {
236
+ const kids = [];
237
+ rows.forEach((r, i) => {
238
+ kids.push(h("div", { className: "dshh-k", key: "k" + i }, r[0]));
239
+ kids.push(h("div", { key: "v" + i }, r[1]));
240
+ });
241
+ return h("div", { className: "dshh-grid" }, kids);
242
+ }
243
+
244
+ function verdict(el, ok) {
245
+ return h("span", { className: ok ? "dshh-ok" : "dshh-bad" }, el);
246
+ }
247
+
248
+ function fmtBytes(n) {
249
+ const v = Number(n) || 0;
250
+ if (v >= 1024 * 1024 * 1024) return (v / (1024 * 1024 * 1024)).toFixed(2) + " GB";
251
+ if (v >= 1024 * 1024) return (v / (1024 * 1024)).toFixed(1) + " MB";
252
+ if (v >= 1024) return (v / 1024).toFixed(1) + " KB";
253
+ return v + " B";
254
+ }
255
+
256
+ /** 设置页主体:只读,仅一个刷新按钮。 */
257
+ function HandoverSection() {
258
+ // 首屏直接使用上一次的读数(切走再切回不再出现「正在读取」),随后后台刷新。
259
+ const [data, setData] = React.useState(state.last);
260
+ const [err, setErr] = React.useState("");
261
+ const [busy, setBusy] = React.useState(false);
262
+ // 首次安装向导
263
+ const [parent, setParent] = React.useState("");
264
+ const [created, setCreated] = React.useState("");
265
+ const [setupMsg, setSetupMsg] = React.useState("");
266
+ const [setupBad, setSetupBad] = React.useState(false);
267
+ const [deployInfo, setDeployInfo] = React.useState(null);
268
+ const [globalMsg, setGlobalMsg] = React.useState("");
269
+ const [globalBad, setGlobalBad] = React.useState(false);
270
+ const [working, setWorking] = React.useState(false);
271
+ // 全局前置提示词
272
+ const [extra, setExtra] = React.useState("");
273
+ const [extraMsg, setExtraMsg] = React.useState("");
274
+ const [extraBad, setExtraBad] = React.useState(false);
275
+ const [channel, setChannel] = React.useState("");
276
+ // 迁移:预检、复制、状态
277
+ const [mvTo, setMvTo] = React.useState("");
278
+ const [mvBusy, setMvBusy] = React.useState(false);
279
+ const [mvMsg, setMvMsg] = React.useState("");
280
+ const [mvBad, setMvBad] = React.useState(false);
281
+ const [mvProbe, setMvProbe] = React.useState(null);
282
+ const [mvStatus, setMvStatus] = React.useState(null);
283
+ // 删除旧备份的确认词。
284
+ const [delWord, setDelWord] = React.useState("");
285
+
286
+ const load = React.useCallback(() => {
287
+ if (state.last) setData(state.last);
288
+ setBusy(true);
289
+ setErr("");
290
+ (async () => {
291
+ let d = null;
292
+ let via = "";
293
+ try {
294
+ d = await rpc("status", {}, 30000);
295
+ via = "载体(/api)";
296
+ } catch (e1) {
297
+ try {
298
+ d = await fetchStatus();
299
+ via = "HTTP 回退";
300
+ } catch (e2) {
301
+ const msg = "载体(" + carrierKind() + "):" + String(e1 && e1.message ? e1.message : e1)
302
+ + ";HTTP 回退:" + String(e2 && e2.message ? e2.message : e2);
303
+ diag.lastError = msg;
304
+ setErr(msg);
305
+ setBusy(false);
306
+ return;
307
+ }
308
+ }
309
+ state.last = d;
310
+ setData(d);
311
+ setChannel(via);
312
+ setExtra(typeof d.extraPrompt === "string" ? d.extraPrompt : "");
313
+ try { setMvStatus(await rpc("move.status", {}, 8000)); } catch (e) { setMvStatus(null); }
314
+ setBusy(false);
315
+ })();
316
+ }, []);
317
+
318
+ const buildTree = React.useCallback(() => {
319
+ setWorking(true); setSetupBad(false); setSetupMsg("");
320
+ post("setup", { parent: parent }, 120000)
321
+ .then((r) => {
322
+ if (r && r.ok) {
323
+ setCreated(r.root || "");
324
+ setSetupMsg("已建立:目录 " + (r.created || []).length + " 个、文件 " + (r.written || []).length + " 个。下一步:注册为工作区。");
325
+ } else {
326
+ setSetupBad(true);
327
+ setSetupMsg((r && r.error) || "建立失败");
328
+ }
329
+ })
330
+ .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
331
+ .then(() => setWorking(false));
332
+ }, [parent]);
333
+
334
+ const registerWs = React.useCallback(() => {
335
+ if (!created) return;
336
+ setWorking(true); setSetupBad(false); setSetupMsg("");
337
+ post("register", { path: created, title: "全局" }, 20000)
338
+ .then((r) => {
339
+ if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。点击「刷新」后可继续。");
340
+ else { setSetupBad(true); setSetupMsg(((r && r.error) || "注册失败") + (r && r.hint ? ";" + r.hint : "")); }
341
+ })
342
+ .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
343
+ .then(() => setWorking(false));
344
+ }, [created]);
345
+
346
+ // 一键部署:建立文件体系、注册「全局」工作区、创建「全局」会话,一次完成。
347
+ const deployAll = React.useCallback(() => {
348
+ setWorking(true); setDeployInfo(null); setSetupBad(false); setSetupMsg("");
349
+ post("deploy", { parent: parent }, 180000)
350
+ .then((r) => {
351
+ if (!r || !r.ok) {
352
+ setSetupBad(true);
353
+ setSetupMsg((r && r.error) || "部署失败");
354
+ return;
355
+ }
356
+ const parts = [];
357
+ parts.push("文件体系:" + r.root);
358
+ if (r.workspace) parts.push("工作区:「" + r.workspace.title + "」(" + r.workspace.path + ")");
359
+ else if (r.workspaceError) parts.push("工作区未注册:" + r.workspaceError);
360
+ if (r.sessionId) parts.push("全局会话:" + r.sessionId + "。在左侧「" + (r.workspace ? r.workspace.title : "全局") + "」工作区打开它即可开始。");
361
+ else if (r.sessionError) parts.push("会话未创建:" + r.sessionError);
362
+ setCreated(r.root || "");
363
+ setDeployInfo({ text: parts.join(";"), bad: false });
364
+ load();
365
+ })
366
+ .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
367
+ .then(() => setWorking(false));
368
+ }, [parent, load]);
369
+
370
+ // 工作区已接入但尚无全局会话时,补建一个。
371
+ const makeGlobalSession = React.useCallback(() => {
372
+ setWorking(true); setGlobalBad(false); setGlobalMsg("");
373
+ post("global-session", {}, 60000)
374
+ .then((r) => {
375
+ if (r && r.ok) {
376
+ setGlobalMsg("已创建全局会话 " + r.sessionId + ",在左侧工作区打开它即可开始。");
377
+ load();
378
+ } else {
379
+ setGlobalBad(true);
380
+ setGlobalMsg((r && r.error) || "创建失败");
381
+ }
382
+ })
383
+ .catch((e) => { setGlobalBad(true); setGlobalMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
384
+ .then(() => setWorking(false));
385
+ }, [load]);
386
+
387
+ const saveExtra = React.useCallback(() => {
388
+ setWorking(true); setExtraBad(false); setExtraMsg("");
389
+ post("extra-prompt", { text: extra }, 20000)
390
+ .then((r) => {
391
+ if (r && r.ok) {
392
+ if (r.unchanged) setExtraMsg(r.note || "内容未变,已跳过写入。");
393
+ else setExtraMsg((r.chars ? "已保存,共 " + r.chars + " 字符。" : "已清空,不再注册提示段。") + (r.cacheNote ? " " + r.cacheNote : ""));
394
+ }
395
+ else { setExtraBad(true); setExtraMsg((r && r.error) || "保存失败"); }
396
+ })
397
+ .catch((e) => { setExtraBad(true); setExtraMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
398
+ .then(() => setWorking(false));
399
+ }, [extra]);
400
+
401
+ // 会话角色
402
+ const [roleSel, setRoleSel] = React.useState({});
403
+ const [roleStage, setRoleStage] = React.useState({});
404
+ const [roleMigrate, setRoleMigrate] = React.useState({});
405
+ const [roleMsg, setRoleMsg] = React.useState("");
406
+ const [roleBad, setRoleBad] = React.useState(false);
407
+
408
+ const setRoleValue = React.useCallback((sid, patch) => {
409
+ setRoleSel((prev) => {
410
+ const next = Object.assign({}, prev || {});
411
+ next[sid] = Object.assign({}, next[sid] || {}, patch);
412
+ return next;
413
+ });
414
+ // 修改角色后回到第一步,按钮重新显示「设置角色」。
415
+ setRoleStage((prev) => {
416
+ const next = Object.assign({}, prev || {});
417
+ next[sid] = "";
418
+ return next;
419
+ });
420
+ }, []);
421
+
422
+ const submitRole = React.useCallback((sid, role, project, task, wake) => {
423
+ const migrate = (roleMigrate && roleMigrate[sid]) || "auto";
424
+ setWorking(true); setRoleBad(false); setRoleMsg("");
425
+ post("role", { session_id: sid, role: role, project: project, task: task, wake: wake, migrate: migrate }, 30000)
426
+ .then((r) => {
427
+ if (r && r.ok) {
428
+ const liveText = r.live ? "已在当前会话生效" : "自下一轮起生效";
429
+ const sendText = wake
430
+ ? (r.delivered ? "首轮指令已送达,该会话将被唤醒一轮" : ("首轮指令未送达" + (r.error ? ":" + r.error : "")))
431
+ : "";
432
+ const migrateText = r.migrated ? ";身份互转方式 " + r.migrated + ":" + (r.migrationSummary || "") : "";
433
+ setRoleMsg("角色已设为" + (r.role || role) + "," + liveText + migrateText + (sendText ? ";" + sendText : "。"));
434
+ setRoleStage((prev) => {
435
+ const next = Object.assign({}, prev || {});
436
+ next[sid] = wake ? "notified" : "set";
437
+ return next;
438
+ });
439
+ load();
440
+ } else {
441
+ setRoleBad(true);
442
+ setRoleMsg((r && r.error) || "设置失败");
443
+ }
444
+ })
445
+ .catch((e) => { setRoleBad(true); setRoleMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
446
+ .then(() => setWorking(false));
447
+ }, [load, roleMigrate]);
448
+
449
+ // 迁移:探针 → 复制并校验 → 状态 → 清旧备份
450
+ const refreshMove = React.useCallback(() => {
451
+ rpc("move.status", {}, 10000).then((st) => setMvStatus(st)).catch(() => {});
452
+ }, []);
453
+
454
+ const callMove = React.useCallback((method, payload, ms) => {
455
+ setMvBusy(true); setMvBad(false); setMvMsg("");
456
+ return rpc(method, payload, ms || 20000)
457
+ .then((v) => v)
458
+ .catch((e) => { setMvBad(true); setMvMsg(String(e && e.message ? e.message : e)); throw e; })
459
+ .then((v) => v, () => null)
460
+ .then((v) => { setMvBusy(false); return v; });
461
+ }, []);
462
+
463
+ const doProbe = React.useCallback(() => {
464
+ callMove("move.probe", { to: mvTo }, 90000).then((p) => {
465
+ if (!p) return;
466
+ setMvProbe(p);
467
+ setMvBad(!p.ok);
468
+ setMvMsg(p.ok ? "预检通过,可开始复制。" : "预检未通过,原因见下。");
469
+ });
470
+ }, [mvTo, callMove]);
471
+
472
+ const doCopy = React.useCallback(() => {
473
+ callMove("move.start", { to: mvTo }, 900000).then((r) => {
474
+ if (!r) return;
475
+ setMvBad(false);
476
+ setMvMsg("复制与校验通过:共 " + r.report.files + " 个文件、" + fmtBytes(r.report.bytes) + "。切换脚本已生成。");
477
+ refreshMove();
478
+ });
479
+ }, [mvTo, callMove, refreshMove]);
480
+
481
+ const doSwitch = React.useCallback(() => {
482
+ callMove("move.switch", {}, 30000).then((r) => {
483
+ if (!r) return;
484
+ setMvBad(false);
485
+ setMvMsg(r.already
486
+ ? "已处于切换完成状态(原路径为链接),无需再次切换。"
487
+ : "已就绪:完全退出 DSH 后约 1 秒完成切换,等待 10 秒再打开。日志:" + (r.log || ""));
488
+ refreshMove();
489
+ });
490
+ }, [callMove, refreshMove]);
491
+
492
+ // 压缩授权:仅在用户于此授权后,压缩才可排入;模型不得自行批准。
493
+ const [csSid, setCsSid] = React.useState("");
494
+ const [csMsg, setCsMsg] = React.useState("");
495
+ const doGrant = React.useCallback((kind) => {
496
+ setWorking(true); setCsMsg("");
497
+ post("consent.grant", { session_id: csSid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
498
+ .then((r) => {
499
+ if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ":5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
500
+ else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
501
+ })
502
+ .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
503
+ .then(() => setWorking(false));
504
+ }, [csSid, load]);
505
+ const doGrantSid = React.useCallback((sid, kind) => {
506
+ setWorking(true); setCsMsg("");
507
+ post("consent.grant", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
508
+ .then((r) => {
509
+ if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ":5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
510
+ else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
511
+ })
512
+ .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
513
+ .then(() => setWorking(false));
514
+ }, [load]);
515
+
516
+ const doRevoke = React.useCallback((sid, kind) => {
517
+ setWorking(true); setCsMsg("");
518
+ post("consent.revoke", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
519
+ .then(() => { setCsMsg("已撤销 " + sid + " 的" + (kind === "succeed" ? "换任" : "压缩") + "授权。"); load(); })
520
+ .catch(() => { /* 撤销失败不影响其他操作 */ })
521
+ .then(() => setWorking(false));
522
+ }, [load]);
523
+
524
+ const doRetarget = React.useCallback(() => {
525
+ callMove("move.retarget-stage", {}, 20000).then((r) => {
526
+ if (!r) return;
527
+ if (r.already) { setMvMsg("DSH 登记已在新位置,无需修改登记。"); return; }
528
+ setMvBad(false);
529
+ setMvMsg("已就绪:DSH 退出后改写登记,每份先备份,重启后生效。日志:" + ((r.script && r.script.log) || ""));
530
+ refreshMove();
531
+ });
532
+ }, [callMove, refreshMove]);
533
+
534
+ const doRepair = React.useCallback(() => {
535
+ callMove("move.repair", {}, 20000).then((r) => {
536
+ if (!r) return;
537
+ setMvBad(false);
538
+ setMvMsg(r.already ? "原路径已经是链接,无需重建。" : "已重建链接:" + r.src + " 现指向 " + r.dst + "(未改动任何文件)。");
539
+ refreshMove();
540
+ });
541
+ }, [callMove, refreshMove]);
542
+
543
+ const doCleanup = React.useCallback(() => {
544
+ if (String(delWord || "").trim() !== "删除") {
545
+ setMvBad(true);
546
+ setMvMsg("确认词不正确。输入「删除」后重试。");
547
+ return;
548
+ }
549
+ callMove("move.cleanup", { confirm: true }, 180000).then((r) => {
550
+ if (!r) return;
551
+ setMvBad(false);
552
+ setDelWord("");
553
+ setMvMsg(r.note + ((r.removed || []).length ? ":" + r.removed.join("、") : ""));
554
+ refreshMove();
555
+ });
556
+ }, [delWord, callMove, refreshMove]);
557
+
558
+ // 有缓存时先渲染一帧旧读数,再拉取新数据;无缓存时才等待一次。
559
+ React.useEffect(() => { installCss(); load(); }, []);
560
+
561
+ const head = h("div", { className: "dshh-head" },
562
+ h("h2", { className: "dshh-title" }, "dsh-handover"),
563
+ h("button", { className: "dshh-btn", onClick: load, disabled: busy, type: "button" }, busy ? (data ? "正在刷新" : "正在读取") : "刷新"),
564
+ diag.lastError ? h("span", { className: "dshh-bad" }, "读取失败(通道 " + (channel || carrierKind()) + "):" + diag.lastError.slice(0, 90)) : null,
565
+ data ? h("span", { className: "dshh-muted" }, "读数时间 " + data.generatedAt) : null);
566
+
567
+ if (err && !data) {
568
+ return h("div", { className: "dshh-root" }, head,
569
+ h("div", { className: "dshh-bad" }, "无法取得状态:" + err),
570
+ h("div", { className: "dshh-muted" }, "插件刚安装时,确认已在配置中启用并重启 DSH。"));
571
+ }
572
+ if (!data) {
573
+ return h("div", { className: "dshh-root" }, head, h("div", { className: "dshh-muted" }, "正在读取状态数据。"));
574
+ }
575
+
576
+ const p = data.plugin || {};
577
+ const ws = data.workspace || {};
578
+ const gate = data.gate;
579
+ const ho = data.handover;
580
+ const ss = data.sessions;
581
+ const allSessions = (data.sessions && Array.isArray(data.sessions.all)) ? data.sessions.all : [];
582
+ const byActivity = allSessions.slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
583
+
584
+ // 排序原则:高频操作在前,只读数据居中,说明文字在后。
585
+ const sections = [];
586
+
587
+ // 一、首次安装与全局会话:未接入时提供一键部署;已接入但尚无全局会话时提供补建入口。
588
+ const setup = data.setup;
589
+ const notReady = !ws || ws.found === false;
590
+ const globalCount = Number(data.globalSessions || 0);
591
+ if (deployInfo) {
592
+ sections.push(card("部署结果", h("div", { className: deployInfo.bad ? "dshh-bad" : "dshh-ok" }, deployInfo.text)));
593
+ }
594
+ if (notReady) {
595
+ const msg = setupMsg ? h("div", { className: setupBad ? "dshh-bad" : "dshh-ok" }, setupMsg) : null;
596
+ const steps = (setup && setup.steps) || [];
597
+ const notes = Array.isArray(data.notes) ? data.notes : [];
598
+ sections.push(card("首次安装",
599
+ h("div", null,
600
+ h("div", { className: "dshh-muted" }, "本页与工具已可用;接入工作区后显示闸门、交接体检与会话水位。"),
601
+ h("ol", { className: "dshh-list" }, steps.map((s, i) => h("li", { key: i }, s))),
602
+ notes.length ? h("ul", { className: "dshh-list" }, notes.map((n, i) => h("li", { key: i }, n))) : null,
603
+ h("input", {
604
+ className: "dshh-input",
605
+ type: "text",
606
+ value: parent,
607
+ placeholder: "填写绝对路径:全局根 _Workspace 将建立在该目录下",
608
+ onChange: (e) => setParent(e.target.value),
609
+ }),
610
+ h("div", { className: "dshh-row" },
611
+ h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: deployAll }, working ? "正在部署" : "一键部署"),
612
+ h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: buildTree }, working ? "正在建立" : "仅建立文件体系"),
613
+ created ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: registerWs }, "注册为工作区") : null),
614
+ created ? h("div", { className: "dshh-code" }, created) : null,
615
+ msg)));
616
+ } else if (globalCount === 0) {
617
+ sections.push(card("全局会话",
618
+ h("div", null,
619
+ h("div", { className: "dshh-muted" }, "工作区已接入,尚无「全局」会话。点击下方按钮新建,插件会将其登记为全局角色。"),
620
+ h("div", { className: "dshh-muted" }, "全局根:" + (ws.root || "")),
621
+ h("div", { className: "dshh-row" },
622
+ h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: makeGlobalSession }, working ? "正在创建" : "创建全局会话")),
623
+ globalMsg ? h("div", { className: globalBad ? "dshh-bad" : "dshh-ok" }, globalMsg) : null)));
624
+ }
625
+
626
+ // 二、授权:唯一合法的「用户同意」入口,置顶;只列在任会话,超过 5 个折叠。
627
+ const consentRows = (Array.isArray(data.consent) ? data.consent : []).filter((r) => r.onduty !== false);
628
+ const dutyRows = byActivity.filter((r) => r.onduty !== false);
629
+ const grantRow = (kind) => (r, i) => h("tr", { key: i },
630
+ h("td", null, r.title || "(无标题)"),
631
+ h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
632
+ h("td", null, h("button", {
633
+ className: "dshh-btn",
634
+ type: "button",
635
+ disabled: working,
636
+ title: r.sid,
637
+ onClick: () => doGrantSid(r.sid, kind),
638
+ }, kind === "succeed" ? "授权换任" : "授权压缩")));
639
+ const grantTable = (kind) => h(Folded, {
640
+ rows: dutyRows,
641
+ pageSize: 5,
642
+ cols: ["会话", "短号", "操作"],
643
+ widths: ["58%", "12%", "30%"],
644
+ render: grantRow(kind),
645
+ });
646
+ const authKids = [];
647
+ if (dutyRows.length) {
648
+ authKids.push(h("div", { className: "dshh-muted" }, "点击「授权压缩」后,该会话 5 分钟内可压缩一次,用后失效;也可在会话中发送 /allow-compact。"));
649
+ authKids.push(grantTable("compact"));
650
+ authKids.push(h("div", { className: "dshh-muted" }, "换任亦须授权:点击「授权换任」后 5 分钟内可换任一次;也可在会话中发送 /allow-succeed。"));
651
+ authKids.push(grantTable("succeed"));
652
+ } else {
653
+ authKids.push(h("div", { className: "dshh-muted" }, "暂无可选的在任会话;可在下方直接填写会话 id 授权。"));
654
+ }
655
+ authKids.push(h("div", { className: "dshh-row" },
656
+ h("input", {
657
+ className: "dshh-inputSm",
658
+ type: "text",
659
+ value: csSid,
660
+ placeholder: "也可填写会话 id 或 8 位短号",
661
+ onChange: (e) => setCsSid(e.target.value),
662
+ }),
663
+ h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("compact") }, working ? "正在授权" : "授权压缩"),
664
+ h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("succeed") }, "授权换任")));
665
+ if (consentRows.length) {
666
+ authKids.push(h("div", { className: "dshh-ok" }, "有授权"));
667
+ authKids.push(h("table", { className: "dshh-table" },
668
+ h("colgroup", null, h("col", { style: { width: "62%" } }), h("col", { style: { width: "18%" } }), h("col", { style: { width: "20%" } })),
669
+ h("thead", null, h("tr", null, h("th", null, "已授权会话"), h("th", null, "剩余"), h("th", null, "操作"))),
670
+ h("tbody", null, consentRows.map((r, i) => h("tr", { key: i },
671
+ h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId) + (r.kind === "succeed" ? " · 换任" : " · 压缩")),
672
+ h("td", { className: "dshh-num" }, Math.round((r.leftMs || 0) / 1000) + " 秒"),
673
+ h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId, r.kind) }, "撤销")))))));
674
+ }
675
+ if (csMsg) authKids.push(h("div", { className: "dshh-ok" }, csMsg));
676
+ sections.push(card("授权", h("div", null, authKids)));
677
+
678
+ // 三、移动全局根目录:常态折叠为一行状态。
679
+ {
680
+ const st = mvStatus;
681
+ const phaseLabel = {
682
+ none: "尚无迁移记录",
683
+ verified: "已复制并校验,待切换",
684
+ switched: "已切换,原路径为链接",
685
+ "switched+cleaned": "已切换,旧备份已删除",
686
+ partial: "校验有差异,需重新复制",
687
+ };
688
+ // 旧备份是否保留不影响使用,因此不计入「需要处理」;需要处理的只有会妨碍使用的情形。
689
+ const hasOlds = !!(st && st.phase === "switched" && (st.olds || []).length);
690
+ const attention = !!(st && (st.linkBroken || st.srcExists === false || st.phase === "verified" || st.phase === "partial" || (st.retarget && st.retarget.needed)));
691
+ const stateText = st
692
+ ? "状态:" + (phaseLabel[st.phase] || st.phase) + (st.dst ? "(" + st.dst + ")" : "") + (hasOlds ? ";旧备份保留,不影响使用" : "")
693
+ : "正在读取迁移状态";
694
+ const kids = [];
695
+ kids.push(h("div", { className: "dshh-muted" }, "切换只改名并建立链接,不改动文件内容。"));
696
+ if (st && st.phase && st.phase.indexOf("switched") === 0) {
697
+ kids.push(h("div", { className: "dshh-muted" }, "原路径为指向新位置的链接;若已删除,可用「重建链接」恢复。"));
698
+ }
699
+ if (st && (st.srcExists === false || st.linkBroken)) {
700
+ kids.push(h("div", { className: "dshh-bad" }, st.linkBroken
701
+ ? "链接指向的新位置不存在:" + (st.dst || "") + "。应先确认真实数据位置;本页不改动文件。"
702
+ : "原路径不存在(可能已被删除):" + (st.src || "") + "。数据仍在 " + (st.dst || "") + "。"));
703
+ if (st.canRepair) {
704
+ kids.push(h("div", { className: "dshh-row" },
705
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doRepair }, "重建链接")));
706
+ }
707
+ }
708
+ if (st && st.phase === "verified" && st.script) {
709
+ kids.push(h("div", { className: "dshh-sub" }, "切换步骤"));
710
+ kids.push(h("ol", { className: "dshh-list" },
711
+ h("li", null, "点击「准备切换」,助手进入待命状态。"),
712
+ h("li", null, "完全退出 DSH,含托盘与后台进程。"),
713
+ h("li", null, "退出后自动执行,约 50 毫秒。"),
714
+ h("li", null, "等待 10 秒后再打开 DSH。"),
715
+ h("li", null, "回到本页,状态应为「已切换,原路径为链接」。")));
716
+ kids.push(h("div", { className: "dshh-row" },
717
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doSwitch }, mvBusy ? "正在准备" : "准备切换(退出 DSH 后自动完成)")));
718
+ kids.push(h("div", { className: "dshh-muted" }, "助手未运行时手工执行:" + ((st.script && st.script.helper) || "")));
719
+ }
720
+ if (st && st.retarget && st.retarget.needed) {
721
+ kids.push(h("div", { className: "dshh-warn" }, "DSH 登记仍指向旧路径:" + st.retarget.total + " 处,涉及 " + st.retarget.files.length + " 个文件。"));
722
+ kids.push(h("div", { className: "dshh-muted" }, "退出 DSH 时改写,每份先备份;重启后生效。"));
723
+ kids.push(h("div", { className: "dshh-row" },
724
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doRetarget }, "把登记迁到新位置(退出 DSH 后生效)")));
725
+ for (const rf of (st.retarget.files || [])) {
726
+ kids.push(h("div", { className: "dshh-code dshh-muted" }, rf.path + " ×" + rf.count));
727
+ }
728
+ }
729
+ if (st && st.retarget && st.retarget.needed === false) {
730
+ kids.push(h("div", { className: "dshh-ok" }, "DSH 登记已指向新位置:" + (st.dst || "") + "。"));
731
+ }
732
+ if (hasOlds) {
733
+ kids.push(h("div", { className: "dshh-muted" }, "旧备份:" + st.olds.map((o) => o.path + "(" + fmtBytes(o.bytes) + ")").join("、") + "。保留不影响使用;删除后无法恢复。"));
734
+ kids.push(h("div", { className: "dshh-row" },
735
+ h("input", {
736
+ className: "dshh-inputSm",
737
+ type: "text",
738
+ value: delWord,
739
+ placeholder: "输入「删除」以确认",
740
+ onChange: (e) => setDelWord(e.target.value),
741
+ }),
742
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || delWord.trim() !== "删除", onClick: doCleanup }, "删除旧备份(不可撤销)")));
743
+ }
744
+ kids.push(h("div", { className: "dshh-row" },
745
+ h("input", {
746
+ className: "dshh-input",
747
+ type: "text",
748
+ value: mvTo,
749
+ placeholder: "新根目录的绝对路径",
750
+ onChange: (e) => setMvTo(e.target.value),
751
+ })));
752
+ kids.push(h("div", { className: "dshh-row" },
753
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || !mvTo, onClick: doProbe }, mvBusy ? "正在预检" : "预检"),
754
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || !mvTo, onClick: doCopy }, "复制并校验"),
755
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: refreshMove }, "刷新状态")));
756
+ if (mvProbe) {
757
+ kids.push(kv([
758
+ ["源", h("span", { className: "dshh-code" }, mvProbe.src)],
759
+ ["目标", h("span", { className: "dshh-code" }, mvProbe.dst)],
760
+ ["总大小", mvProbe.files + " 文件 · " + fmtBytes(mvProbe.bytes)],
761
+ ["目标盘可用", mvProbe.freeBytes === null || mvProbe.freeBytes === undefined ? "?" : fmtBytes(mvProbe.freeBytes)],
762
+ ["结论", mvProbe.ok ? verdict("通过", true) : verdict("不通过", false)],
763
+ ]));
764
+ for (const w of (mvProbe.warnings || [])) kids.push(h("div", { className: "dshh-warn" }, "注意:" + w));
765
+ for (const x of (mvProbe.problems || [])) kids.push(h("div", { className: "dshh-bad" }, "未通过:" + x));
766
+ }
767
+ if (mvMsg) kids.push(h("div", { className: mvBad ? "dshh-bad" : "dshh-ok" }, mvMsg));
768
+ sections.push(card("移动全局根目录",
769
+ h("div", null,
770
+ h("details", null,
771
+ h("summary", { className: "dshh-sum" }, (attention ? "需要处理:" : "") + stateText),
772
+ h("div", { className: "dshh-wrap" }, kids)))));
773
+ }
774
+
775
+ // 四、会话角色:按 DSH 工作区分组,默认全部折叠;组内会话按时间倒序(与 DSH 侧栏一致)。
776
+ // 分组顺序取 status payload 的 workspaces(即 workspace.json 的 workspaceIds 权威展示顺序)。
777
+ const timeDesc = (a, b) => {
778
+ const am = Number(a.mtime);
779
+ const bm = Number(b.mtime);
780
+ if (Number.isFinite(am) && Number.isFinite(bm) && am !== bm) return bm - am;
781
+ const s = String(b.seen || '').localeCompare(String(a.seen || ''));
782
+ if (s !== 0) return s;
783
+ return String(a.sid || '').localeCompare(String(b.sid || ''));
784
+ };
785
+ const byTime = byActivity.slice().sort(timeDesc);
786
+ const wsList = (ss && Array.isArray(ss.workspaces)) ? ss.workspaces : [];
787
+ const roleGroups = [];
788
+ const claimed = {};
789
+ for (const w of wsList) {
790
+ const rows = byTime.filter((r) => r.wsKey === w.key);
791
+ claimed[w.key] = true;
792
+ if (rows.length) roleGroups.push({ key: w.key, title: w.title || w.path || '工作区', path: w.path || '', rows });
793
+ }
794
+ const restRows = byTime.filter((r) => !claimed[r.wsKey]);
795
+ if (restRows.length) roleGroups.push({ key: "", title: "未分组", path: "", rows: restRows });
796
+ const roleRow = (r, i) => {
797
+ const draft = (roleSel && roleSel[r.sid]) || {};
798
+ const role = draft.role !== undefined ? draft.role : (r.role || "plain");
799
+ const project = draft.project !== undefined ? draft.project : (r.project || "");
800
+ const task = draft.task !== undefined ? draft.task : (r.task || "");
801
+ const wide = role === "project" || role === "misc";
802
+ const stage = (roleStage && roleStage[r.sid]) || "";
803
+ // 已有在任全局会话时不再提供「全局」选项;当前即为全局的会话仍显示,避免看不出当前状态。
804
+ const canGlobal = Number(data.globalSessions || 0) === 0 || r.role === "global";
805
+ const convert = (r.role === "project" || r.role === "misc") && wide && role !== r.role;
806
+ const action = stage === "notified"
807
+ ? h("button", { className: "dshh-btn", type: "button", disabled: true }, "已通知")
808
+ : stage === "set"
809
+ ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => submitRole(r.sid, role, project, task, true) }, "通知该会话")
810
+ : stage === "confirm"
811
+ ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => submitRole(r.sid, role, project, task, false) }, "确认设置")
812
+ : h("button", {
813
+ className: "dshh-btn",
814
+ type: "button",
815
+ disabled: working,
816
+ onClick: () => {
817
+ setRoleStage((prev) => {
818
+ const next = Object.assign({}, prev || {});
819
+ next[r.sid] = "confirm";
820
+ return next;
821
+ });
822
+ setRoleBad(false);
823
+ setRoleMsg("请确认角色与名称,再点击「确认设置」。项目与杂项互转时,默认先快照再移动。");
824
+ },
825
+ }, "设置角色");
826
+ return h("tr", { key: i },
827
+ h("td", null, r.title || "(无标题)"),
828
+ h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
829
+ h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
830
+ h("td", null, h("select", {
831
+ className: "dshh-inputSm",
832
+ value: role,
833
+ onChange: (e) => setRoleValue(r.sid, { role: e.target.value }),
834
+ },
835
+ canGlobal ? h("option", { value: "global" }, "全局") : null,
836
+ h("option", { value: "project" }, "项目"),
837
+ h("option", { value: "misc" }, "杂项"),
838
+ h("option", { value: "plain" }, "普通"))),
839
+ h("td", null, wide ? h("div", null,
840
+ h("input", {
841
+ className: "dshh-inputSm",
842
+ type: "text",
843
+ value: role === "project" ? project : task,
844
+ placeholder: role === "project" ? "填写项目名" : "填写任务名",
845
+ onChange: (e) => setRoleValue(r.sid, role === "project" ? { project: e.target.value } : { task: e.target.value }),
846
+ }),
847
+ convert ? h("select", {
848
+ className: "dshh-inputSm",
849
+ value: (roleMigrate && roleMigrate[r.sid]) || "auto",
850
+ onChange: (e) => setRoleMigrate((prev) => Object.assign({}, prev || {}, { [r.sid]: e.target.value })),
851
+ },
852
+ h("option", { value: "auto" }, "先快照再移动"),
853
+ h("option", { value: "direct" }, "直接建新体系")) : null) : h("span", { className: "dshh-muted" }, "不需填写")),
854
+ h("td", null, h("div", { className: "dshh-pick" }, action)));
855
+ };
856
+ sections.push(card("会话角色",
857
+ h("div", null,
858
+ roleGroups.length
859
+ ? roleGroups.map((g, gi) => h("details", { key: gi },
860
+ h("summary", { className: "dshh-sum" }, g.title + "(" + g.rows.length + " 个会话)"),
861
+ h("div", { className: "dshh-wrap" },
862
+ g.path ? h("div", { className: "dshh-muted dshh-code" }, g.path) : null,
863
+ h(Folded, {
864
+ rows: g.rows,
865
+ pageSize: 5,
866
+ cols: ["会话", "短号", "水位", "角色", "项目或任务名", "操作"],
867
+ widths: ["30%", "9%", "7%", "12%", "20%", "22%"],
868
+ render: roleRow,
869
+ }))))
870
+ : h("div", { className: "dshh-muted" }, "暂无可设置角色的会话。空白会话不列出。"),
871
+ roleMsg ? h("div", { className: roleBad ? "dshh-bad" : "dshh-ok" }, roleMsg) : null)));
872
+
873
+ // 五、全局前置提示词:默认折叠,只显示已保存字数。
874
+ sections.push(card("全局前置提示词",
875
+ h("div", null,
876
+ h("div", { className: "dshh-muted" }, "只注入你填写的内容,不含插件约束;对全部会话生效,留空保存即不注册提示段。"),
877
+ h("details", null,
878
+ h("summary", { className: "dshh-sum" }, "已保存 " + (extra ? extra.length : 0) + " 字符,点击编辑"),
879
+ h("textarea", {
880
+ className: "dshh-textarea",
881
+ rows: 6,
882
+ value: extra,
883
+ placeholder: "例如:一律用中文;改动前说明改动范围;简单任务不必拆分。",
884
+ onChange: (e) => setExtra(e.target.value),
885
+ }),
886
+ h("div", { className: "dshh-row" },
887
+ h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: saveExtra }, working ? "正在保存" : "保存"),
888
+ h("span", { className: "dshh-muted" }, extra.length + " 字符")),
889
+ extraMsg ? h("div", { className: extraBad ? "dshh-bad" : "dshh-ok" }, extraMsg) : null))));
890
+
891
+ // 六、会话水位:只读数据区从这里开始;只列在任会话(全局、项目、杂项各一行)。
892
+ if (ss) {
893
+ const byRecent = (ss.rows || []).filter((r) => r.onduty !== false).slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
894
+ const waterRow = (r, i) => h("tr", { key: i },
895
+ h("td", null, r.dutyName || r.dutyKey || r.title || r.project || "(无名)"),
896
+ h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
897
+ h("td", null, r.dutyKind || r.roleLabel || ""),
898
+ h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
899
+ h("td", null, r.state),
900
+ h("td", null, r.active ? "活跃" : "沉默"),
901
+ h("td", { className: "dshh-muted dshh-num" }, shortTime(r.seen)));
902
+ sections.push(card("会话水位",
903
+ h("div", null,
904
+ h("div", { className: "dshh-muted" }, ss.verdict + "。活跃线 " + ss.cutoff + ",回溯 " + ss.silentHours + " 小时。"),
905
+ (ss.notify || []).length
906
+ ? h("div", null, "本轮需通知:" + ss.notify.map((r) => (r.name || r.key || "") + "(" + shortSid(r.sid) + ")").join("、"))
907
+ : h("div", { className: "dshh-muted" }, "本轮无需通知。"),
908
+ byRecent.length
909
+ ? h(Folded, {
910
+ rows: byRecent,
911
+ pageSize: 5,
912
+ cols: ["会话", "短号", "类别", "水位", "状态", "活跃", "最后活动"],
913
+ widths: ["20%", "11%", "7%", "8%", "8%", "7%", "31%"],
914
+ render: waterRow,
915
+ })
916
+ : h("div", { className: "dshh-muted" }, "暂无可显示的在任会话。"))));
917
+ }
918
+
919
+ // 七、通用约束条闸门
920
+ if (gate) {
921
+ const rows = (gate.libraries || []).map((r, i) => h("tr", { key: i },
922
+ h("td", null, r.name),
923
+ h("td", null, r.titleOk ? "通过" : "缺"),
924
+ h("td", null, (r.lines - r.missing) + "/" + r.lines),
925
+ h("td", null, r.waterOk ? "通过" : "缺"),
926
+ h("td", null, verdict(r.ok ? "通过" : "未通过", r.ok))));
927
+ sections.push(card("通用约束条闸门",
928
+ gate.error
929
+ ? h("div", { className: "dshh-bad" }, gate.error)
930
+ : h("div", null,
931
+ h("div", { className: "dshh-muted" }, gate.total + " 个库,正文 " + gate.bodyLines + " 行;" + gate.verdict),
932
+ h("table", { className: "dshh-table" },
933
+ h("thead", null, h("tr", null,
934
+ h("th", null, "库"), h("th", null, "标题"), h("th", null, "正文"), h("th", null, "水位自查"), h("th", null, "结论"))),
935
+ h("tbody", null, rows)))));
936
+ }
937
+
938
+ // 八、交接文档体检:全局一行,项目与杂项同一张表;问题列只留问题本身。
939
+ if (ho) {
940
+ const cleanReason = (x) => String(x || "").replace(/([^)]*)/g, "").replace(/\s+/g, " ").trim();
941
+ const rows = (ho.projects || []).map((r) => Object.assign({}, r, { kind: "项目" }))
942
+ .concat((ho.misc || []).map((r) => Object.assign({}, r, { kind: "杂项" })));
943
+ const prows = rows.map((r, i) => h("tr", { key: i },
944
+ h("td", null, r.kind),
945
+ h("td", null, r.name),
946
+ h("td", null, r.verdict === "通过" ? verdict("通过", true) : (r.verdict === "缺节" || r.verdict === "读不到" || r.verdict === "未找到") ? h("span", { className: "dshh-warn" }, r.verdict) : verdict(r.verdict, false)),
947
+ h("td", null, (r.reasons || []).map(cleanReason).filter(Boolean).join(";"))));
948
+ sections.push(card("交接文档体检",
949
+ h("div", null,
950
+ h("div", null, "全局 Global_Handover.md:", ho.global.verdict === "通过" ? verdict("通过", true) : verdict(ho.global.verdict, false),
951
+ (ho.global.reasons || []).length ? h("span", { className: "dshh-muted" }, (ho.global.reasons || []).map(cleanReason).filter(Boolean).join(";")) : null),
952
+ prows.length
953
+ ? h("table", { className: "dshh-table" },
954
+ h("thead", null, h("tr", null, h("th", null, "类别"), h("th", null, "名称"), h("th", null, "结论"), h("th", null, "问题"))),
955
+ h("tbody", null, prows))
956
+ : h("div", { className: "dshh-muted" }, "暂无已登记的项目或杂项。"))));
957
+ }
958
+
959
+ // 九、插件信息
960
+ sections.push(card("插件", kv([
961
+ ["名称", h("span", { className: "dshh-code" }, p.name + "@" + p.version)],
962
+ ["工具数", String(p.tools)],
963
+ ["压缩调度器", p.scheduler],
964
+ ["注入项", (p.inject || []).join("、")],
965
+ ["初始化", (p.initErrors && p.initErrors.length) ? h("span", { className: "dshh-bad" }, p.initErrors.join(";")) : verdict("正常", true)],
966
+ ["工作区", ws.found ? h("span", { className: "dshh-code" }, ws.root) : verdict("未接入(未找到 _Control_Center)", false)],
967
+ ])));
968
+
969
+ // 十、最近一次会话间通信判定:只有存在判定时才出现。
970
+ const g = data.guardLast;
971
+ if (g) {
972
+ sections.push(card("最近一次会话间通信判定", kv([
973
+ ["结论", g.allow ? verdict("允许", true) : verdict("拒绝", false)],
974
+ ["依据", g.source + (g.title ? ";会话标题 " + g.title : "")],
975
+ ["会话", h("span", { className: "dshh-code" }, g.sid || "-")],
976
+ ])));
977
+ }
978
+
979
+ // 末、工具明细与使用说明:参考性质,放最后,默认折叠。
980
+ const gd = data.guide;
981
+ if (gd) {
982
+ const sit = (gd.situations || []).map((r, i) => h("tr", { key: i },
983
+ h("td", null, r.when),
984
+ h("td", null, r.say),
985
+ h("td", { className: "dshh-muted" }, r.what)));
986
+ const groups = (gd.groups || []).map((gr, i) => h("div", { key: i },
987
+ h("div", { className: "dshh-sub" }, gr.title),
988
+ h("table", { className: "dshh-table" }, h("tbody", null, (gr.tools || []).map((t, j) => h("tr", { key: j },
989
+ h("td", { className: "dshh-code" }, t[0]),
990
+ h("td", { className: "dshh-muted" }, t[1])))))));
991
+ const toolCount = (gd.groups || []).reduce((a, gr) => a + ((gr.tools || []).length), 0);
992
+ if (toolCount) {
993
+ sections.push(card("工具明细",
994
+ h("details", null,
995
+ h("summary", { className: "dshh-sum" }, "共 " + toolCount + " 个工具,点击展开"),
996
+ h("div", { className: "dshh-wrap" }, groups))));
997
+ }
998
+ sections.push(card("使用说明",
999
+ h("details", null,
1000
+ h("summary", { className: "dshh-sum" }, "交互方式、常见请求与约定"),
1001
+ h("div", { className: "dshh-wrap" },
1002
+ (gd.intro || []).map((t, i) => h("div", { key: i }, t)),
1003
+ h("div", { className: "dshh-sub" }, "常见请求与对应动作"),
1004
+ h("table", { className: "dshh-table" },
1005
+ h("thead", null, h("tr", null, h("th", null, "时机"), h("th", null, "可以这样说"), h("th", null, "插件会执行的动作"))),
1006
+ h("tbody", null, sit)),
1007
+ h("div", { className: "dshh-sub" }, "约定"),
1008
+ h("ul", { className: "dshh-list" }, (gd.rules || []).map((t, i) => h("li", { key: i }, t)))))));
1009
+ }
1010
+
1011
+ return h("div", { className: "dshh-root" }, head, sections);
1012
+ }
1013
+
1014
+ /** 把只读状态页注册进设置导航。 */
1015
+ function apply(ctx) {
1016
+ try { connection = (ctx && ctx.connection) || null; } catch (e) { connection = null; }
1017
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1018
+ name: "settings.section",
1019
+ id: "handover",
1020
+ order: 100,
1021
+ label: "dsh-handover",
1022
+ }, HandoverSection));
1023
+ }
1024
+
1025
+ exports.apply = apply;
1026
+ exports.inject = inject;
1027
+ return module.exports;
1028
+ },
995
1029
  });