@mingmaple/dsh-handover 0.9.14 → 0.9.22

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 (64) hide show
  1. package/README.md +46 -19
  2. package/docs/migrate.md +6 -6
  3. package/docs/releases/0.9.22.md +34 -0
  4. package/docs/troubleshoot.md +34 -10
  5. package/package.json +71 -71
  6. package/scripts/blank-dsh.mjs +29 -23
  7. package/scripts/blank-smoke.mjs +205 -87
  8. package/scripts/e2e-migration.mjs +2 -2
  9. package/scripts/office.mjs +3 -3
  10. package/src/client.js +159 -120
  11. package/src/index.mjs +65 -58
  12. package/src/lib/api.mjs +9 -7
  13. package/src/lib/brief.mjs +2 -2
  14. package/src/lib/config.mjs +3 -3
  15. package/src/lib/consent.mjs +54 -1
  16. package/src/lib/docmap.mjs +143 -0
  17. package/src/lib/gates.mjs +25 -1
  18. package/src/lib/guide.mjs +35 -44
  19. package/src/lib/lint.mjs +3 -2
  20. package/src/lib/manual.mjs +48 -9
  21. package/src/lib/mover.mjs +10 -10
  22. package/src/lib/names.mjs +14 -19
  23. package/src/lib/office.mjs +22 -14
  24. package/src/lib/pack-ops.mjs +2 -2
  25. package/src/lib/peer.mjs +4 -4
  26. package/src/lib/role.mjs +8 -5
  27. package/src/lib/scaffold.mjs +42 -20
  28. package/src/lib/schedule.mjs +1 -2
  29. package/src/lib/session-create.mjs +1 -1
  30. package/src/lib/session-ops.mjs +16 -16
  31. package/src/lib/sessions.mjs +1 -1
  32. package/src/lib/status.mjs +205 -45
  33. package/src/lib/succession.mjs +7 -7
  34. package/src/lib/zstd-log.mjs +1 -1
  35. package/src/selftest.mjs +6 -6
  36. package/src/templates/Prompt_Templates.md +13 -13
  37. package/src/templates/Runtime_Manual.md +56 -30
  38. package/src/templates/rules/Handover_Rules.md +2 -1
  39. package/src/templates/rules/Migration_Rules.md +1 -1
  40. package/src/templates/standards/README.md +25 -0
  41. package/src/tools/ho_backup.mjs +135 -0
  42. package/src/tools/ho_checkpoint.mjs +12 -11
  43. package/src/tools/ho_docmap.mjs +153 -0
  44. package/src/tools/ho_gate.mjs +2 -1
  45. package/src/tools/ho_office.mjs +1 -1
  46. package/src/tools/ho_sessions.mjs +3 -4
  47. package/src/tools/ho_status.mjs +246 -0
  48. package/src/tools/ho_succeed.mjs +1 -1
  49. package/src/tools/ho_workspace.mjs +86 -14
  50. package/src/tools/peer.mjs +205 -0
  51. package/src/tools/ho_brief.mjs +0 -57
  52. package/src/tools/ho_diag.mjs +0 -87
  53. package/src/tools/ho_drift.mjs +0 -41
  54. package/src/tools/ho_init.mjs +0 -82
  55. package/src/tools/ho_pack.mjs +0 -38
  56. package/src/tools/ho_phase.mjs +0 -37
  57. package/src/tools/ho_snapshot.mjs +0 -104
  58. package/src/tools/ho_tokens.mjs +0 -57
  59. package/src/tools/peer_list.mjs +0 -37
  60. package/src/tools/peer_new.mjs +0 -75
  61. package/src/tools/peer_peek.mjs +0 -41
  62. package/src/tools/peer_rename.mjs +0 -31
  63. package/src/tools/peer_send.mjs +0 -52
  64. package/src/tools/peer_status.mjs +0 -46
@@ -15,7 +15,7 @@ const argIdx = process.argv.indexOf('--dist');
15
15
  const dist = argIdx !== -1 ? path.resolve(process.argv[argIdx + 1]) : path.join(repo, 'dist');
16
16
 
17
17
  const imp = (rel) => import(pathToFileURL(path.join(dist, rel)).href);
18
- const { pmPackTool } = await imp('tools/ho_pack.mjs');
18
+ const { pmBackupTool } = await imp('tools/ho_backup.mjs');
19
19
  const { resolveConfig } = await imp('lib/config.mjs');
20
20
 
21
21
  const fails = [];
@@ -47,7 +47,7 @@ const env = {
47
47
  selfId: () => null,
48
48
  selfCwd: () => null,
49
49
  };
50
- const packTool = pmPackTool(env);
50
+ const packTool = pmBackupTool(env);
51
51
  /** action 分流:pack/verify/restore 都走同一个工具(合并口径)。 */
52
52
  const run = (args) => packTool.execute(args, null);
53
53
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * 命令行查在任登记表:`node scripts/office.mjs 全局 全局3 Mingent`
3
+ * 命令行查在任登记表:`node scripts/office.mjs 全局 全局3 Demo1`
4
4
  *
5
5
  * 与工具 ho_office 同一份实现、同一份记录(_Control_Center/records/Office.json),
6
6
  * 区别只是不起会话也能查。工作区根按 --root → DSH_OFFICE_ROOT → 从 cwd 向上找 _Control_Center 的顺序解析。
@@ -32,14 +32,14 @@ for (let i = 0; i < argv.length; i++) {
32
32
  if (a === '--all' || a === '-a') { all = true; continue; }
33
33
  if (a === '-h' || a === '--help') {
34
34
  console.log('用法:node scripts/office.mjs [查询…] [--all] [--json] [--root <工作区根>]');
35
- console.log('示例:node scripts/office.mjs 全局 全局3 Mingent');
35
+ console.log('示例:node scripts/office.mjs 全局 全局3 Demo1');
36
36
  process.exit(0);
37
37
  }
38
38
  queries.push(a);
39
39
  }
40
40
  if (!root) root = walkUp(process.cwd());
41
41
  if (!root) {
42
- console.error('找不到工作区根(没有 _Control_Center):用 --root 指定,例如 --root E:\\_Project_Mate\\_Workspace');
42
+ console.error('找不到工作区根(没有 _Control_Center):用 --root 指定,例如 --root <全局根绝对路径>');
43
43
  process.exit(2);
44
44
  }
45
45
 
package/src/client.js CHANGED
@@ -64,7 +64,6 @@ window.__ModuleLoader__.load({
64
64
  document.head.appendChild(tag);
65
65
  }
66
66
 
67
- /** 依次尝试候选地址取状态;全失败时抛出带全部原因的错。 */
68
67
  /** 带超时的 fetch(旧通道回退用;没有超时就会永远「读取中」)。 */
69
68
  async function fetchWithTimeout(url, opts, ms) {
70
69
  const ac = new AbortController();
@@ -76,6 +75,7 @@ window.__ModuleLoader__.load({
76
75
  }
77
76
  }
78
77
 
78
+ /** 依次尝试候选地址取状态;全失败时抛出带全部原因的错。 */
79
79
  async function fetchStatus() {
80
80
  const reasons = [];
81
81
  for (const url of ENDPOINTS) {
@@ -107,7 +107,7 @@ window.__ModuleLoader__.load({
107
107
  }
108
108
 
109
109
  /**
110
- * 可折叠表格:先显示 pageSize 行,点「再展开」继续加 pageSize 行。
110
+ * 可折叠表格:先显示 pageSize 行,点击「再展开」继续追加 pageSize 行。
111
111
  * 列宽由 widths 百分比给定(表头固定、不横向滚动)。
112
112
  */
113
113
  function Folded(props) {
@@ -142,7 +142,7 @@ window.__ModuleLoader__.load({
142
142
  return "可用(参数 " + (conn.rpc.call.length || "?") + ")";
143
143
  }
144
144
 
145
- /** 与超时赛跑:宿主 rpc.call 不理会 AbortSignal,光 abort 会让页面永远「读取中」。 */
145
+ /** 与超时竞速:宿主 rpc.call 不理会 AbortSignal,仅中止请求无法结束等待,页面会停在「读取中」。 */
146
146
  function raceTimeout(promise, ms, label) {
147
147
  return new Promise((resolve, reject) => {
148
148
  const timer = setTimeout(() => reject(new Error(label + " 超时 " + ms + " ms")), ms);
@@ -160,7 +160,7 @@ window.__ModuleLoader__.load({
160
160
  // "[object Object]/api" 并报 invalid RPC target。
161
161
  const rpcObj = conn && conn.rpc && typeof conn.rpc.call === "function" ? conn.rpc : null;
162
162
  if (!rpcObj) throw new Error("宿主没有 connection.rpc(载体=" + carrierKind() + ")");
163
- // 不用 AbortController 主动中止:宿主处理可能超过 8 秒,只做与超时赛跑,
163
+ // 不用 AbortController 主动中止:宿主处理可能超过 8 秒,只做与超时竞速,
164
164
  // 让请求在宿主侧自然完成,慢一点也能拿到结果。
165
165
  const ac = new AbortController();
166
166
  const limit = ms || 30000;
@@ -267,19 +267,19 @@ window.__ModuleLoader__.load({
267
267
  const [globalMsg, setGlobalMsg] = React.useState("");
268
268
  const [globalBad, setGlobalBad] = React.useState(false);
269
269
  const [working, setWorking] = React.useState(false);
270
- // 追加系统提示词
270
+ // 全局前置提示词
271
271
  const [extra, setExtra] = React.useState("");
272
272
  const [extraMsg, setExtraMsg] = React.useState("");
273
273
  const [extraBad, setExtraBad] = React.useState(false);
274
274
  const [channel, setChannel] = React.useState("");
275
- // 搬迁:探针 / 复制 / 状态
275
+ // 迁移:探针 / 复制 / 状态
276
276
  const [mvTo, setMvTo] = React.useState("");
277
277
  const [mvBusy, setMvBusy] = React.useState(false);
278
278
  const [mvMsg, setMvMsg] = React.useState("");
279
279
  const [mvBad, setMvBad] = React.useState(false);
280
280
  const [mvProbe, setMvProbe] = React.useState(null);
281
281
  const [mvStatus, setMvStatus] = React.useState(null);
282
- // 删除旧副本的确认词。
282
+ // 删除旧备份的确认词。
283
283
  const [delWord, setDelWord] = React.useState("");
284
284
 
285
285
  const load = React.useCallback(() => {
@@ -335,7 +335,7 @@ window.__ModuleLoader__.load({
335
335
  setWorking(true); setSetupBad(false); setSetupMsg("");
336
336
  post("register", { path: created, title: "全局" }, 20000)
337
337
  .then((r) => {
338
- if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。点「刷新」后可继续。");
338
+ if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。点击「刷新」后可继续。");
339
339
  else { setSetupBad(true); setSetupMsg(((r && r.error) || "注册失败") + (r && r.hint ? ";" + r.hint : "")); }
340
340
  })
341
341
  .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
@@ -389,7 +389,7 @@ window.__ModuleLoader__.load({
389
389
  .then((r) => {
390
390
  if (r && r.ok) {
391
391
  if (r.unchanged) setExtraMsg(r.note || "内容未变,已跳过写入。");
392
- else setExtraMsg((r.chars ? "已保存,共 " + r.chars + " 字符,位置在工具说明之前。" : "已清空,不再注册提示段。") + (r.cacheNote ? " " + r.cacheNote : ""));
392
+ else setExtraMsg((r.chars ? "已保存,共 " + r.chars + " 字符。" : "已清空,不再注册提示段。") + (r.cacheNote ? " " + r.cacheNote : ""));
393
393
  }
394
394
  else { setExtraBad(true); setExtraMsg((r && r.error) || "保存失败"); }
395
395
  })
@@ -442,7 +442,7 @@ window.__ModuleLoader__.load({
442
442
  .then(() => setWorking(false));
443
443
  }, [load]);
444
444
 
445
- // 搬迁:探针 → 复制并校验 → 状态 → 清旧副本
445
+ // 迁移:探针 → 复制并校验 → 状态 → 清旧备份
446
446
  const refreshMove = React.useCallback(() => {
447
447
  rpc("move.status", {}, 10000).then((st) => setMvStatus(st)).catch(() => {});
448
448
  }, []);
@@ -479,20 +479,20 @@ window.__ModuleLoader__.load({
479
479
  if (!r) return;
480
480
  setMvBad(false);
481
481
  setMvMsg(r.already
482
- ? "当前已处于切换完成状态(原路径为链接),无需再次切换。"
483
- : "已就绪:请完全退出 DSH,退出后约 1 秒完成切换,等待 10 秒后再打开。日志:" + (r.log || ""));
482
+ ? "已处于切换完成状态(原路径为链接),无需再次切换。"
483
+ : "已就绪:完全退出 DSH 后约 1 秒完成切换,等待 10 秒再打开。日志:" + (r.log || ""));
484
484
  refreshMove();
485
485
  });
486
486
  }, [callMove, refreshMove]);
487
487
 
488
- // 压缩授权:只有用户在这里点一下,压缩才排得进去(AI 不能自己批)
488
+ // 压缩授权:只有用户在此授权,压缩才排得进;模型不能自批。
489
489
  const [csSid, setCsSid] = React.useState("");
490
490
  const [csMsg, setCsMsg] = React.useState("");
491
491
  const doGrant = React.useCallback((kind) => {
492
492
  setWorking(true); setCsMsg("");
493
493
  post("consent.grant", { session_id: csSid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
494
494
  .then((r) => {
495
- if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
495
+ if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ":5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
496
496
  else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
497
497
  })
498
498
  .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
@@ -502,7 +502,7 @@ window.__ModuleLoader__.load({
502
502
  setWorking(true); setCsMsg("");
503
503
  post("consent.grant", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
504
504
  .then((r) => {
505
- if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
505
+ if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ":5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
506
506
  else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
507
507
  })
508
508
  .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
@@ -539,7 +539,7 @@ window.__ModuleLoader__.load({
539
539
  const doCleanup = React.useCallback(() => {
540
540
  if (String(delWord || "").trim() !== "删除") {
541
541
  setMvBad(true);
542
- setMvMsg("确认词不正确。请输入「删除」两个字后再执行。");
542
+ setMvMsg("确认词不正确。输入「删除」后重试。");
543
543
  return;
544
544
  }
545
545
  callMove("move.cleanup", { confirm: true }, 180000).then((r) => {
@@ -557,13 +557,13 @@ window.__ModuleLoader__.load({
557
557
  const head = h("div", { className: "dshh-head" },
558
558
  h("h2", { className: "dshh-title" }, "dsh-handover"),
559
559
  h("button", { className: "dshh-btn", onClick: load, disabled: busy, type: "button" }, busy ? (data ? "正在刷新" : "正在读取") : "刷新"),
560
- h("span", { className: "dshh-muted" }, "数据通道 " + (channel || carrierKind()) + (diag.lastError ? ";上次失败 " + diag.lastError.slice(0, 90) : "")),
560
+ diag.lastError ? h("span", { className: "dshh-bad" }, "读取失败(通道 " + (channel || carrierKind()) + "):" + diag.lastError.slice(0, 90)) : null,
561
561
  data ? h("span", { className: "dshh-muted" }, "读数时间 " + data.generatedAt) : null);
562
562
 
563
563
  if (err && !data) {
564
564
  return h("div", { className: "dshh-root" }, head,
565
565
  h("div", { className: "dshh-bad" }, "无法取得状态:" + err),
566
- h("div", { className: "dshh-muted" }, "若插件刚安装,请确认已在配置中启用本插件,并已重启 DSH。"));
566
+ h("div", { className: "dshh-muted" }, "插件刚安装时,确认已在配置中启用并重启 DSH。"));
567
567
  }
568
568
  if (!data) {
569
569
  return h("div", { className: "dshh-root" }, head, h("div", { className: "dshh-muted" }, "正在读取状态数据。"));
@@ -577,7 +577,7 @@ window.__ModuleLoader__.load({
577
577
  const allSessions = (data.sessions && Array.isArray(data.sessions.all)) ? data.sessions.all : [];
578
578
  const byActivity = allSessions.slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
579
579
 
580
- // 排序原则:进来大概率要做的事在前,只读数据居中,说明文字最后。
580
+ // 排序原则:高频操作在前,只读数据居中,说明文字在后。
581
581
  const sections = [];
582
582
 
583
583
  // 一、首次安装与全局会话:未接入时给一键部署;已接入但还没有全局会话时给补建入口。
@@ -590,15 +590,17 @@ window.__ModuleLoader__.load({
590
590
  if (notReady) {
591
591
  const msg = setupMsg ? h("div", { className: setupBad ? "dshh-bad" : "dshh-ok" }, setupMsg) : null;
592
592
  const steps = (setup && setup.steps) || [];
593
+ const notes = Array.isArray(data.notes) ? data.notes : [];
593
594
  sections.push(card("首次安装",
594
595
  h("div", null,
595
- h("div", { className: "dshh-muted" }, "插件已加载,工具与本页可用;闸门、交接体检与会话水位在接入工作区后显示。"),
596
+ h("div", { className: "dshh-muted" }, "本页与工具已可用;接入工作区后显示闸门、交接体检与会话水位。"),
596
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,
597
599
  h("input", {
598
600
  className: "dshh-input",
599
601
  type: "text",
600
602
  value: parent,
601
- placeholder: "粘贴一个绝对路径:全局根 _Workspace 会建在它里面",
603
+ placeholder: "填写绝对路径:全局根 _Workspace 将建在该目录下",
602
604
  onChange: (e) => setParent(e.target.value),
603
605
  }),
604
606
  h("div", { className: "dshh-row" },
@@ -610,78 +612,89 @@ window.__ModuleLoader__.load({
610
612
  } else if (globalCount === 0) {
611
613
  sections.push(card("全局会话",
612
614
  h("div", null,
613
- h("div", { className: "dshh-muted" }, "工作区已接入,但还没有「全局」会话。点下面的按钮新建一个,插件会把它登记为全局角色。"),
615
+ h("div", { className: "dshh-muted" }, "工作区已接入,尚无「全局」会话。点击下方按钮新建,插件会将其登记为全局角色。"),
614
616
  h("div", { className: "dshh-muted" }, "全局根:" + (ws.root || "")),
615
617
  h("div", { className: "dshh-row" },
616
618
  h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: makeGlobalSession }, working ? "正在创建" : "创建全局会话")),
617
619
  globalMsg ? h("div", { className: globalBad ? "dshh-bad" : "dshh-ok" }, globalMsg) : null)));
618
620
  }
619
621
 
620
- // 二、压缩与换任授权:唯一合法的「用户同意」入口,置顶。
621
- const consentRows = Array.isArray(data.consent) ? data.consent : [];
622
- sections.push(card("压缩授权",
623
- h("div", null,
624
- h("div", { className: "dshh-muted" }, "压缩须由你授权:点「允许」后,该会话可在 5 分钟内压缩一次,用后失效。也可在对话中发送 /allow-compact。"),
625
- h("div", { className: "dshh-pick" },
626
- byActivity.slice(0, 10).map((r, i) => h("button", {
627
- className: "dshh-btn",
628
- type: "button",
629
- key: i,
630
- disabled: working,
631
- title: r.title || r.sid,
632
- onClick: () => doGrantSid(r.sid, "compact"),
633
- }, "允许 " + shortSid(r.sid)))),
634
- h("div", { className: "dshh-muted" }, "换任由在任会话自己执行,同样须由你授权:点「允许换任」后,该会话可在 5 分钟内换任一次。也可在对话中发送 /allow-succeed。"),
635
- h("div", { className: "dshh-pick" },
636
- byActivity.slice(0, 10).map((r, i) => h("button", {
637
- className: "dshh-btn",
638
- type: "button",
639
- key: i,
640
- disabled: working,
641
- title: r.title || r.sid,
642
- onClick: () => doGrantSid(r.sid, "succeed"),
643
- }, "允许换任 " + shortSid(r.sid)))),
644
- h("div", { className: "dshh-row" },
645
- h("input", {
646
- className: "dshh-inputSm",
647
- type: "text",
648
- value: csSid,
649
- placeholder: "或输入会话 id 或 8 位短号",
650
- onChange: (e) => setCsSid(e.target.value),
651
- }),
652
- h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("compact") }, working ? "正在授权" : "授权"),
653
- h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: () => doGrant("succeed") }, "授权换任")),
654
- consentRows.length
655
- ? h("table", { className: "dshh-table" },
656
- h("colgroup", null, h("col", { style: { width: "62%" } }), h("col", { style: { width: "18%" } }), h("col", { style: { width: "20%" } })),
657
- h("thead", null, h("tr", null, h("th", null, "已授权会话"), h("th", null, "剩余"), h("th", null, "操作"))),
658
- h("tbody", null, consentRows.map((r, i) => h("tr", { key: i },
659
- h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId) + (r.kind === "succeed" ? " · 换任" : " · 压缩")),
660
- h("td", { className: "dshh-num" }, Math.round((r.leftMs || 0) / 1000) + " 秒"),
661
- h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId, r.kind) }, "撤销"))))))
662
- : h("div", { className: "dshh-muted" }, "当前无有效授权。"),
663
- csMsg ? h("div", { className: "dshh-ok" }, csMsg) : null)));
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)));
664
673
 
665
674
  // 三、移动全局根目录:常态折叠为一行状态。
666
675
  {
667
676
  const st = mvStatus;
668
677
  const phaseLabel = {
669
678
  none: "尚无迁移记录",
670
- verified: "已复制并校验,等待切换",
679
+ verified: "已复制并校验,待切换",
671
680
  switched: "已切换,原路径为链接",
672
- "switched+cleaned": "已切换,旧副本已删除",
681
+ "switched+cleaned": "已切换,旧备份已删除",
673
682
  partial: "校验有差异,需重新复制",
674
683
  };
675
- const attention = !!(st && (st.linkBroken || st.srcExists === false || st.phase === "verified" || st.phase === "partial" || (st.retarget && st.retarget.needed) || (st.phase === "switched" && (st.olds || []).length)));
676
- const stateText = st ? "状态:" + (phaseLabel[st.phase] || st.phase) + (st.dst ? "(" + st.dst + ")" : "") : "正在读取迁移状态";
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
+ : "正在读取迁移状态";
677
690
  const kids = [];
678
- kids.push(h("div", { className: "dshh-muted" }, "复制并校验通过后才能切换;切换只改名与建立链接,不改动文件内容。"));
691
+ kids.push(h("div", { className: "dshh-muted" }, "切换只改名与建链接,不改动文件内容。"));
679
692
  if (st && st.phase && st.phase.indexOf("switched") === 0) {
680
- kids.push(h("div", { className: "dshh-muted" }, "原路径现为链接,请勿删除;若已删除,用「重建链接」恢复。"));
693
+ kids.push(h("div", { className: "dshh-muted" }, "原路径为指向新位置的链接;若已删除,可用「重建链接」恢复。"));
681
694
  }
682
695
  if (st && (st.srcExists === false || st.linkBroken)) {
683
696
  kids.push(h("div", { className: "dshh-bad" }, st.linkBroken
684
- ? "链接指向的新位置不存在:" + (st.dst || "") + "。请先确认真实数据位置;本页不改动文件。"
697
+ ? "链接指向的新位置不存在:" + (st.dst || "") + "。应先确认真实数据位置;本页不改动文件。"
685
698
  : "原路径不存在(可能已被删除):" + (st.src || "") + "。数据仍在 " + (st.dst || "") + "。"));
686
699
  if (st.canRepair) {
687
700
  kids.push(h("div", { className: "dshh-row" },
@@ -698,7 +711,7 @@ window.__ModuleLoader__.load({
698
711
  h("li", null, "回到本页,状态应为「已切换,原路径为链接」。")));
699
712
  kids.push(h("div", { className: "dshh-row" },
700
713
  h("button", { className: "dshh-btn", type: "button", disabled: mvBusy, onClick: doSwitch }, mvBusy ? "正在准备" : "准备切换(退出 DSH 后自动完成)")));
701
- kids.push(h("div", { className: "dshh-muted" }, "日志末行出现「切换完成」即为成功;失败会写明原因且不改动文件,可重试。助手未运行时手工执行:" + ((st.script && st.script.helper) || "")));
714
+ kids.push(h("div", { className: "dshh-muted" }, "助手未运行时手工执行:" + ((st.script && st.script.helper) || "")));
702
715
  }
703
716
  if (st && st.retarget && st.retarget.needed) {
704
717
  kids.push(h("div", { className: "dshh-warn" }, "DSH 登记仍指向旧路径:" + st.retarget.total + " 处,涉及 " + st.retarget.files.length + " 个文件。"));
@@ -712,8 +725,8 @@ window.__ModuleLoader__.load({
712
725
  if (st && st.retarget && st.retarget.needed === false) {
713
726
  kids.push(h("div", { className: "dshh-ok" }, "DSH 登记已指向新位置:" + (st.dst || "") + "。"));
714
727
  }
715
- if (st && st.phase === "switched" && (st.olds || []).length) {
716
- kids.push(h("div", { className: "dshh-muted" }, "旧副本:" + st.olds.map((o) => o.path + "(" + fmtBytes(o.bytes) + ")").join("、") + "。删除后无法恢复。"));
728
+ if (hasOlds) {
729
+ kids.push(h("div", { className: "dshh-muted" }, "旧备份:" + st.olds.map((o) => o.path + "(" + fmtBytes(o.bytes) + ")").join("、") + "。保留不影响使用;删除后无法恢复。"));
717
730
  kids.push(h("div", { className: "dshh-row" },
718
731
  h("input", {
719
732
  className: "dshh-inputSm",
@@ -722,7 +735,7 @@ window.__ModuleLoader__.load({
722
735
  placeholder: "输入「删除」以确认",
723
736
  onChange: (e) => setDelWord(e.target.value),
724
737
  }),
725
- h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || delWord.trim() !== "删除", onClick: doCleanup }, "删除旧副本(不可撤销)")));
738
+ h("button", { className: "dshh-btn", type: "button", disabled: mvBusy || delWord.trim() !== "删除", onClick: doCleanup }, "删除旧备份(不可撤销)")));
726
739
  }
727
740
  kids.push(h("div", { className: "dshh-row" },
728
741
  h("input", {
@@ -755,7 +768,23 @@ window.__ModuleLoader__.load({
755
768
  h("div", { className: "dshh-wrap" }, kids)))));
756
769
  }
757
770
 
758
- // 四、会话角色:角色列即当前值,设置成功后按钮切换为「通知该会话」。
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 });
759
788
  const roleRow = (r, i) => {
760
789
  const draft = (roleSel && roleSel[r.sid]) || {};
761
790
  const role = draft.role !== undefined ? draft.role : (r.role || "plain");
@@ -777,10 +806,10 @@ window.__ModuleLoader__.load({
777
806
  value: role,
778
807
  onChange: (e) => setRoleValue(r.sid, { role: e.target.value }),
779
808
  },
780
- h("option", { value: "plain" }, "普通"),
781
809
  h("option", { value: "global" }, "全局"),
782
810
  h("option", { value: "project" }, "项目"),
783
- h("option", { value: "misc" }, "杂项"))),
811
+ h("option", { value: "misc" }, "杂项"),
812
+ h("option", { value: "plain" }, "普通"))),
784
813
  h("td", null, wide ? h("input", {
785
814
  className: "dshh-inputSm",
786
815
  type: "text",
@@ -792,22 +821,25 @@ window.__ModuleLoader__.load({
792
821
  };
793
822
  sections.push(card("会话角色",
794
823
  h("div", null,
795
- h("div", { className: "dshh-muted" }, "选定角色后点「设置角色」,当场生效;按钮随即变为「通知该会话」,再点一次即向它发送整理指令并唤醒一轮。"),
796
- byActivity.length
797
- ? h(Folded, {
798
- rows: byActivity,
799
- pageSize: 5,
800
- cols: ["会话", "短号", "水位", "角色", "项目或任务名", "操作"],
801
- widths: ["30%", "9%", "7%", "12%", "20%", "22%"],
802
- render: roleRow,
803
- })
804
- : h("div", { className: "dshh-muted" }, "暂无可设置角色的会话。空白会话不在此列出。"),
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" }, "暂无可设置角色的会话。空白会话不列出。"),
805
837
  roleMsg ? h("div", { className: roleBad ? "dshh-bad" : "dshh-ok" }, roleMsg) : null)));
806
838
 
807
839
  // 五、全局前置提示词:默认折叠,只显示已保存字数。
808
840
  sections.push(card("全局前置提示词",
809
841
  h("div", null,
810
- h("div", { className: "dshh-muted" }, "只注入你填写的内容,不含插件约束;对全部会话生效,位置在工具说明之前。留空保存即不注册提示段。"),
842
+ h("div", { className: "dshh-muted" }, "只注入你填写的内容,不含插件约束;对全部会话生效,留空保存即不注册提示段。"),
811
843
  h("details", null,
812
844
  h("summary", { className: "dshh-sum" }, "已保存 " + (extra ? extra.length : 0) + " 字符,点击编辑"),
813
845
  h("textarea", {
@@ -822,12 +854,13 @@ window.__ModuleLoader__.load({
822
854
  h("span", { className: "dshh-muted" }, extra.length + " 字符")),
823
855
  extraMsg ? h("div", { className: extraBad ? "dshh-bad" : "dshh-ok" }, extraMsg) : null))));
824
856
 
825
- // 六、会话水位:只读数据区从这里开始。
857
+ // 六、会话水位:只读数据区从这里开始;只列在任会话(全局、项目、杂项各一行)。
826
858
  if (ss) {
827
- const byRecent = (ss.rows || []).slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
859
+ const byRecent = (ss.rows || []).filter((r) => r.onduty !== false).slice().sort((a, b) => String(b.seen || "").localeCompare(String(a.seen || "")));
828
860
  const waterRow = (r, i) => h("tr", { key: i },
829
- h("td", null, r.project),
861
+ h("td", null, r.dutyName || r.dutyKey || r.title || r.project || "(无名)"),
830
862
  h("td", { className: "dshh-code dshh-num" }, shortSid(r.sid)),
863
+ h("td", null, r.dutyKind || r.roleLabel || ""),
831
864
  h("td", { className: "dshh-num" }, r.pct === null || r.pct === undefined ? "?" : r.pct.toFixed(1) + "%"),
832
865
  h("td", null, r.state),
833
866
  h("td", null, r.active ? "活跃" : "沉默"),
@@ -836,17 +869,17 @@ window.__ModuleLoader__.load({
836
869
  h("div", null,
837
870
  h("div", { className: "dshh-muted" }, ss.verdict + "。活跃线 " + ss.cutoff + ",回溯 " + ss.silentHours + " 小时。"),
838
871
  (ss.notify || []).length
839
- ? h("div", null, "本轮需通知:" + ss.notify.map((r) => r.project + "(" + shortSid(r.sid) + ")").join("、"))
872
+ ? h("div", null, "本轮需通知:" + ss.notify.map((r) => (r.name || r.key || "") + "(" + shortSid(r.sid) + ")").join("、"))
840
873
  : h("div", { className: "dshh-muted" }, "本轮无需通知。"),
841
874
  byRecent.length
842
875
  ? h(Folded, {
843
876
  rows: byRecent,
844
877
  pageSize: 5,
845
- cols: ["项目", "会话", "水位", "状态", "活跃", "最后活动"],
846
- widths: ["22%", "13%", "9%", "9%", "9%", "38%"],
878
+ cols: ["会话", "短号", "类别", "水位", "状态", "活跃", "最后活动"],
879
+ widths: ["20%", "11%", "7%", "8%", "8%", "7%", "31%"],
847
880
  render: waterRow,
848
881
  })
849
- : h("div", { className: "dshh-muted" }, "暂无可显示的项目会话。"))));
882
+ : h("div", { className: "dshh-muted" }, "暂无可显示的在任会话。"))));
850
883
  }
851
884
 
852
885
  // 七、通用约束条闸门
@@ -868,20 +901,25 @@ window.__ModuleLoader__.load({
868
901
  h("tbody", null, rows)))));
869
902
  }
870
903
 
871
- // 八、交接文档体检:问题列只留问题本身,去掉括注与时间要求。
904
+ // 八、交接文档体检:全局一行,项目与杂项同一张表;问题列只留问题本身。
872
905
  if (ho) {
873
906
  const cleanReason = (x) => String(x || "").replace(/([^)]*)/g, "").replace(/\s+/g, " ").trim();
874
- const prows = (ho.projects || []).map((r, i) => h("tr", { key: i },
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),
875
911
  h("td", null, r.name),
876
- h("td", null, r.verdict === "通过" ? verdict("通过", true) : r.verdict === "缺节" ? h("span", { className: "dshh-warn" }, "缺节") : verdict(r.verdict, false)),
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)),
877
913
  h("td", null, (r.reasons || []).map(cleanReason).filter(Boolean).join(";"))));
878
914
  sections.push(card("交接文档体检",
879
915
  h("div", null,
880
916
  h("div", null, "全局 Global_Handover.md:", ho.global.verdict === "通过" ? verdict("通过", true) : verdict(ho.global.verdict, false),
881
917
  (ho.global.reasons || []).length ? h("span", { className: "dshh-muted" }, (ho.global.reasons || []).map(cleanReason).filter(Boolean).join(";")) : null),
882
- h("table", { className: "dshh-table" },
883
- h("thead", null, h("tr", null, h("th", null, "项目"), h("th", null, "结论"), h("th", null, "问题"))),
884
- h("tbody", null, prows)))));
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" }, "暂无已登记的项目或杂项。"))));
885
923
  }
886
924
 
887
925
  // 九、插件信息
@@ -890,26 +928,21 @@ window.__ModuleLoader__.load({
890
928
  ["工具数", String(p.tools)],
891
929
  ["压缩调度器", p.scheduler],
892
930
  ["注入项", (p.inject || []).join("、")],
893
- ["初始化", (p.initErrors && p.initErrors.length) ? h("span", { className: "dshh-bad" }, p.initErrors.join(";")) : verdict("无错误", true)],
931
+ ["初始化", (p.initErrors && p.initErrors.length) ? h("span", { className: "dshh-bad" }, p.initErrors.join(";")) : verdict("正常", true)],
894
932
  ["工作区", ws.found ? h("span", { className: "dshh-code" }, ws.root) : verdict("未接入(未找到 _Control_Center)", false)],
895
933
  ])));
896
934
 
897
- // 十、会话间通信守卫
935
+ // 十、最近一次会话间通信判定:只有存在判定时才出现。
898
936
  const g = data.guardLast;
899
- sections.push(card("会话间通信守卫",
900
- g
901
- ? kv([
902
- ["结论", g.allow ? verdict("允许", true) : verdict("拒绝", false)],
903
- ["依据", g.source + (g.title ? ";会话标题 " + g.title : "")],
904
- ["会话", h("span", { className: "dshh-code" }, g.sid || "-")],
905
- ])
906
- : h("div", { className: "dshh-muted" }, "本进程尚未收到会话间通信请求,暂无判定。")));
907
-
908
- if ((data.notes || []).length) {
909
- sections.push(card("待处理", h("ul", { className: "dshh-list" }, data.notes.map((n, i) => h("li", { key: i }, n)))));
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
+ ])));
910
943
  }
911
944
 
912
- // 末、使用说明:参考性质,放最后,默认折叠。
945
+ // 末、工具明细与使用说明:参考性质,放最后,默认折叠。
913
946
  const gd = data.guide;
914
947
  if (gd) {
915
948
  const sit = (gd.situations || []).map((r, i) => h("tr", { key: i },
@@ -921,16 +954,22 @@ window.__ModuleLoader__.load({
921
954
  h("table", { className: "dshh-table" }, h("tbody", null, (gr.tools || []).map((t, j) => h("tr", { key: j },
922
955
  h("td", { className: "dshh-code" }, t[0]),
923
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
+ }
924
964
  sections.push(card("使用说明",
925
965
  h("details", null,
926
- h("summary", { className: "dshh-sum" }, "交互方式、常见请求、工具清单与约定"),
966
+ h("summary", { className: "dshh-sum" }, "交互方式、常见请求与约定"),
927
967
  h("div", { className: "dshh-wrap" },
928
968
  (gd.intro || []).map((t, i) => h("div", { key: i }, t)),
929
969
  h("div", { className: "dshh-sub" }, "常见请求与对应动作"),
930
970
  h("table", { className: "dshh-table" },
931
971
  h("thead", null, h("tr", null, h("th", null, "时机"), h("th", null, "可以这样说"), h("th", null, "插件会执行的动作"))),
932
972
  h("tbody", null, sit)),
933
- groups,
934
973
  h("div", { className: "dshh-sub" }, "约定"),
935
974
  h("ul", { className: "dshh-list" }, (gd.rules || []).map((t, i) => h("li", { key: i }, t)))))));
936
975
  }