@mingmaple/dsh-handover 0.9.6 → 0.9.13

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 (50) hide show
  1. package/README.md +53 -17
  2. package/docs/migrate.md +1 -1
  3. package/docs/releases/0.9.12.md +28 -0
  4. package/docs/releases/0.9.13.md +30 -0
  5. package/docs/troubleshoot.md +43 -6
  6. package/package.json +70 -57
  7. package/scripts/blank-dsh.mjs +6 -5
  8. package/scripts/blank-smoke.mjs +268 -58
  9. package/scripts/build.mjs +3 -0
  10. package/scripts/check-docs.mjs +23 -2
  11. package/scripts/office.mjs +81 -0
  12. package/src/client.js +92 -23
  13. package/src/index.mjs +264 -145
  14. package/src/lib/api.mjs +67 -9
  15. package/src/lib/config.mjs +4 -3
  16. package/src/lib/consent.mjs +59 -20
  17. package/src/lib/facts.mjs +5 -4
  18. package/src/lib/guide.mjs +14 -7
  19. package/src/lib/handover-io.mjs +20 -0
  20. package/src/lib/manual.mjs +23 -12
  21. package/src/lib/names.mjs +2 -2
  22. package/src/lib/office.mjs +272 -0
  23. package/src/lib/pack-ops.mjs +3 -0
  24. package/src/lib/phase.mjs +6 -4
  25. package/src/lib/prompt.mjs +18 -0
  26. package/src/lib/role.mjs +52 -18
  27. package/src/lib/schedule.mjs +10 -14
  28. package/src/lib/session-create.mjs +42 -0
  29. package/src/lib/session-ops.mjs +183 -79
  30. package/src/lib/sessions.mjs +9 -8
  31. package/src/lib/status.mjs +8 -2
  32. package/src/lib/succession.mjs +244 -0
  33. package/src/lib/text.mjs +27 -0
  34. package/src/lib/webroute.mjs +1 -1
  35. package/src/lib/workspace.mjs +1 -1
  36. package/src/selftest.mjs +15 -0
  37. package/src/templates/Charter.skeleton.md +1 -1
  38. package/src/templates/Prompt_Templates.md +9 -17
  39. package/src/templates/Runtime_Manual.md +17 -14
  40. package/src/templates/rules/Handover_Rules.md +3 -2
  41. package/src/templates/rules/Misc_Task_Standard.md +3 -3
  42. package/src/tools/ho_checkpoint.mjs +11 -4
  43. package/src/tools/ho_diag.mjs +5 -3
  44. package/src/tools/ho_handover.mjs +7 -6
  45. package/src/tools/ho_office.mjs +59 -0
  46. package/src/tools/ho_profile.mjs +10 -2
  47. package/src/tools/ho_sessions.mjs +3 -2
  48. package/src/tools/ho_snapshot.mjs +2 -1
  49. package/src/tools/ho_succeed.mjs +24 -0
  50. package/src/tools/ho_workspace.mjs +7 -3
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 命令行查在任登记表:`node scripts/office.mjs 全局 全局3 Mingent`
4
+ *
5
+ * 与工具 ho_office 同一份实现、同一份记录(_Control_Center/records/Office.json),
6
+ * 区别只是不起会话也能查。工作区根按 --root → DSH_OFFICE_ROOT → 从 cwd 向上找 _Control_Center 的顺序解析。
7
+ */
8
+ import path from 'node:path';
9
+ import { isDir } from '../src/lib/text.mjs';
10
+ import { loadOffice, resolveOffice, rowsOf, formatRow, officePath, KIND_LABEL } from '../src/lib/office.mjs';
11
+
12
+ function walkUp(start) {
13
+ let cur = path.resolve(start);
14
+ for (let i = 0; i < 8 && cur; i++) {
15
+ if (isDir(path.join(cur, '_Control_Center'))) return cur;
16
+ const up = path.dirname(cur);
17
+ if (up === cur) break;
18
+ cur = up;
19
+ }
20
+ return '';
21
+ }
22
+
23
+ const argv = process.argv.slice(2);
24
+ let root = process.env.DSH_OFFICE_ROOT || process.env.DSH_HANDOVER_ROOT || '';
25
+ let json = false;
26
+ let all = false;
27
+ const queries = [];
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const a = argv[i];
30
+ if (a === '--root') { root = argv[++i] || ''; continue; }
31
+ if (a === '--json') { json = true; continue; }
32
+ if (a === '--all' || a === '-a') { all = true; continue; }
33
+ if (a === '-h' || a === '--help') {
34
+ console.log('用法:node scripts/office.mjs [查询…] [--all] [--json] [--root <工作区根>]');
35
+ console.log('示例:node scripts/office.mjs 全局 全局3 Mingent');
36
+ process.exit(0);
37
+ }
38
+ queries.push(a);
39
+ }
40
+ if (!root) root = walkUp(process.cwd());
41
+ if (!root) {
42
+ console.error('找不到工作区根(没有 _Control_Center):用 --root 指定,例如 --root E:\\_Project_Mate\\_Workspace');
43
+ process.exit(2);
44
+ }
45
+
46
+ const store = loadOffice(root);
47
+ const rows = rowsOf(store);
48
+ if (!rows.length) {
49
+ console.error('在任登记表是空的:' + officePath(root));
50
+ console.log('提示:由全局会话用 ho_sessions / 换任流程登记一次在任会话,登记表随之生成。');
51
+ process.exit(2);
52
+ }
53
+
54
+ if (all || !queries.length) {
55
+ if (json) {
56
+ console.log(JSON.stringify({ updatedAt: store.updatedAt || '', rows }, null, 2));
57
+ } else {
58
+ let lastKind = '';
59
+ for (const r of rows) {
60
+ if (r.kind !== lastKind) {
61
+ console.log((lastKind ? '\n' : '') + '【' + (KIND_LABEL[r.kind] || r.kind) + '】');
62
+ lastKind = r.kind;
63
+ }
64
+ console.log(formatRow(r));
65
+ }
66
+ }
67
+ process.exit(0);
68
+ }
69
+
70
+ let miss = 0;
71
+ const hits = [];
72
+ for (const q of queries) {
73
+ const r = resolveOffice(store, q);
74
+ if (!r.ok) { console.log(q + ' — ' + r.reason); miss++; continue; }
75
+ if (r.vacant) { console.log(q + ' — 类别「' + r.key + '」当前空缺(无人在任)'); continue; }
76
+ const row = { key: r.key, kind: r.lineage.kind, project: r.lineage.project, task: r.lineage.task, current: r.lineage.current };
77
+ if (!json) console.log(formatRow(row, q));
78
+ hits.push({ askedAs: q, via: r.via, key: r.key, kind: r.lineage.kind, name: r.lineage.current.name, sessionId: r.lineage.current.sessionId });
79
+ }
80
+ if (json) console.log(JSON.stringify({ updatedAt: store.updatedAt || '', hits }, null, 2));
81
+ process.exit(miss ? 1 : 0);
package/src/client.js CHANGED
@@ -263,6 +263,9 @@ window.__ModuleLoader__.load({
263
263
  const [created, setCreated] = React.useState("");
264
264
  const [setupMsg, setSetupMsg] = React.useState("");
265
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);
266
269
  const [working, setWorking] = React.useState(false);
267
270
  // 追加系统提示词
268
271
  const [extra, setExtra] = React.useState("");
@@ -332,13 +335,54 @@ window.__ModuleLoader__.load({
332
335
  setWorking(true); setSetupBad(false); setSetupMsg("");
333
336
  post("register", { path: created, title: "全局" }, 20000)
334
337
  .then((r) => {
335
- if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。新建一个标题含「全局」的会话即可接入。");
338
+ if (r && r.ok) setSetupMsg("已注册工作区:" + (r.title || "全局") + "(" + (r.id || "?") + ")。点「刷新」后可继续。");
336
339
  else { setSetupBad(true); setSetupMsg(((r && r.error) || "注册失败") + (r && r.hint ? ";" + r.hint : "")); }
337
340
  })
338
341
  .catch((e) => { setSetupBad(true); setSetupMsg("请求未完成:" + (e && e.message ? e.message : String(e))); })
339
342
  .then(() => setWorking(false));
340
343
  }, [created]);
341
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
+
342
386
  const saveExtra = React.useCallback(() => {
343
387
  setWorking(true); setExtraBad(false); setExtraMsg("");
344
388
  post("extra-prompt", { text: extra }, 20000)
@@ -444,31 +488,31 @@ window.__ModuleLoader__.load({
444
488
  // 压缩授权:只有用户在这里点一下,压缩才排得进去(AI 不能自己批)
445
489
  const [csSid, setCsSid] = React.useState("");
446
490
  const [csMsg, setCsMsg] = React.useState("");
447
- const doGrant = React.useCallback(() => {
491
+ const doGrant = React.useCallback((kind) => {
448
492
  setWorking(true); setCsMsg("");
449
- post("consent.grant", { session_id: csSid }, 15000)
493
+ post("consent.grant", { session_id: csSid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
450
494
  .then((r) => {
451
- if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可压缩一次,用后失效。"); load(); }
495
+ if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
452
496
  else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
453
497
  })
454
498
  .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
455
499
  .then(() => setWorking(false));
456
500
  }, [csSid, load]);
457
- const doGrantSid = React.useCallback((sid) => {
501
+ const doGrantSid = React.useCallback((sid, kind) => {
458
502
  setWorking(true); setCsMsg("");
459
- post("consent.grant", { session_id: sid }, 15000)
503
+ post("consent.grant", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
460
504
  .then((r) => {
461
- if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可压缩一次,用后失效。"); load(); }
505
+ if (r && r.ok) { setCsMsg("已授权会话 " + r.sessionId + ",5 分钟内可" + (r.kind === "succeed" ? "换任" : "压缩") + "一次,用后失效。"); load(); }
462
506
  else setCsMsg("授权未完成:" + ((r && r.error) || "原因未知"));
463
507
  })
464
508
  .catch((e) => setCsMsg("请求未完成:" + (e && e.message ? e.message : String(e))))
465
509
  .then(() => setWorking(false));
466
510
  }, [load]);
467
511
 
468
- const doRevoke = React.useCallback((sid) => {
512
+ const doRevoke = React.useCallback((sid, kind) => {
469
513
  setWorking(true); setCsMsg("");
470
- post("consent.revoke", { session_id: sid }, 15000)
471
- .then(() => { setCsMsg("已撤销 " + sid + " 的压缩授权。"); load(); })
514
+ post("consent.revoke", { session_id: sid, kind: kind === "succeed" ? "succeed" : "compact" }, 15000)
515
+ .then(() => { setCsMsg("已撤销 " + sid + " 的" + (kind === "succeed" ? "换任" : "压缩") + "授权。"); load(); })
472
516
  .catch(() => { /* 撤销失败不影响别的 */ })
473
517
  .then(() => setWorking(false));
474
518
  }, [load]);
@@ -536,30 +580,44 @@ window.__ModuleLoader__.load({
536
580
  // 排序原则:进来大概率要做的事在前,只读数据居中,说明文字最后。
537
581
  const sections = [];
538
582
 
539
- // 一、首次安装:仅在尚未接入工作区时出现,此时它是唯一可做的事。
583
+ // 一、首次安装与全局会话:未接入时给一键部署;已接入但还没有全局会话时给补建入口。
540
584
  const setup = data.setup;
541
585
  const notReady = !ws || ws.found === false;
542
- if (setup && notReady && (setup.steps || []).length) {
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) {
543
591
  const msg = setupMsg ? h("div", { className: setupBad ? "dshh-bad" : "dshh-ok" }, setupMsg) : null;
592
+ const steps = (setup && setup.steps) || [];
544
593
  sections.push(card("首次安装",
545
594
  h("div", null,
546
595
  h("div", { className: "dshh-muted" }, "插件已加载,工具与本页可用;闸门、交接体检与会话水位在接入工作区后显示。"),
547
- h("ol", { className: "dshh-list" }, setup.steps.map((s, i) => h("li", { key: i }, s))),
596
+ h("ol", { className: "dshh-list" }, steps.map((s, i) => h("li", { key: i }, s))),
548
597
  h("input", {
549
598
  className: "dshh-input",
550
599
  type: "text",
551
600
  value: parent,
552
- placeholder: "已存在目录的绝对路径,_Workspace 将建在其内",
601
+ placeholder: "粘贴一个绝对路径:全局根 _Workspace 会建在它里面",
553
602
  onChange: (e) => setParent(e.target.value),
554
603
  }),
555
604
  h("div", { className: "dshh-row" },
556
- h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: buildTree }, working ? "正在建立" : "建立全局文件体系"),
605
+ h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: deployAll }, working ? "正在部署" : "一键部署"),
606
+ h("button", { className: "dshh-btn", type: "button", disabled: working || !parent, onClick: buildTree }, working ? "正在建立" : "仅建立文件体系"),
557
607
  created ? h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: registerWs }, "注册为工作区") : null),
558
608
  created ? h("div", { className: "dshh-code" }, created) : null,
559
609
  msg)));
610
+ } else if (globalCount === 0) {
611
+ sections.push(card("全局会话",
612
+ h("div", null,
613
+ h("div", { className: "dshh-muted" }, "工作区已接入,但还没有「全局」会话。点下面的按钮新建一个,插件会把它登记为全局角色。"),
614
+ h("div", { className: "dshh-muted" }, "全局根:" + (ws.root || "")),
615
+ h("div", { className: "dshh-row" },
616
+ h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: makeGlobalSession }, working ? "正在创建" : "创建全局会话")),
617
+ globalMsg ? h("div", { className: globalBad ? "dshh-bad" : "dshh-ok" }, globalMsg) : null)));
560
618
  }
561
619
 
562
- // 二、压缩授权:唯一合法的「用户同意」入口,置顶。
620
+ // 二、压缩与换任授权:唯一合法的「用户同意」入口,置顶。
563
621
  const consentRows = Array.isArray(data.consent) ? data.consent : [];
564
622
  sections.push(card("压缩授权",
565
623
  h("div", null,
@@ -571,8 +629,18 @@ window.__ModuleLoader__.load({
571
629
  key: i,
572
630
  disabled: working,
573
631
  title: r.title || r.sid,
574
- onClick: () => doGrantSid(r.sid),
632
+ onClick: () => doGrantSid(r.sid, "compact"),
575
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)))),
576
644
  h("div", { className: "dshh-row" },
577
645
  h("input", {
578
646
  className: "dshh-inputSm",
@@ -581,15 +649,16 @@ window.__ModuleLoader__.load({
581
649
  placeholder: "或输入会话 id 或 8 位短号",
582
650
  onChange: (e) => setCsSid(e.target.value),
583
651
  }),
584
- h("button", { className: "dshh-btn", type: "button", disabled: working || !csSid, onClick: doGrant }, working ? "正在授权" : "授权")),
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") }, "授权换任")),
585
654
  consentRows.length
586
655
  ? h("table", { className: "dshh-table" },
587
656
  h("colgroup", null, h("col", { style: { width: "62%" } }), h("col", { style: { width: "18%" } }), h("col", { style: { width: "20%" } })),
588
657
  h("thead", null, h("tr", null, h("th", null, "已授权会话"), h("th", null, "剩余"), h("th", null, "操作"))),
589
658
  h("tbody", null, consentRows.map((r, i) => h("tr", { key: i },
590
- h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId)),
659
+ h("td", { className: "dshh-code", title: r.sessionId }, shortSid(r.sessionId) + (r.kind === "succeed" ? " · 换任" : " · 压缩")),
591
660
  h("td", { className: "dshh-num" }, Math.round((r.leftMs || 0) / 1000) + " 秒"),
592
- h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId) }, "撤销"))))))
661
+ h("td", null, h("button", { className: "dshh-btn", type: "button", disabled: working, onClick: () => doRevoke(r.sessionId, r.kind) }, "撤销"))))))
593
662
  : h("div", { className: "dshh-muted" }, "当前无有效授权。"),
594
663
  csMsg ? h("div", { className: "dshh-ok" }, csMsg) : null)));
595
664
 
@@ -825,16 +894,16 @@ window.__ModuleLoader__.load({
825
894
  ["工作区", ws.found ? h("span", { className: "dshh-code" }, ws.root) : verdict("未接入(未找到 _Control_Center)", false)],
826
895
  ])));
827
896
 
828
- // 十、唤醒权守卫
897
+ // 十、会话间通信守卫
829
898
  const g = data.guardLast;
830
- sections.push(card("唤醒权守卫",
899
+ sections.push(card("会话间通信守卫",
831
900
  g
832
901
  ? kv([
833
902
  ["结论", g.allow ? verdict("允许", true) : verdict("拒绝", false)],
834
903
  ["依据", g.source + (g.title ? ";会话标题 " + g.title : "")],
835
904
  ["会话", h("span", { className: "dshh-code" }, g.sid || "-")],
836
905
  ])
837
- : h("div", { className: "dshh-muted" }, "本进程尚未收到唤醒请求,暂无判定。")));
906
+ : h("div", { className: "dshh-muted" }, "本进程尚未收到会话间通信请求,暂无判定。")));
838
907
 
839
908
  if ((data.notes || []).length) {
840
909
  sections.push(card("待处理", h("ul", { className: "dshh-list" }, data.notes.map((n, i) => h("li", { key: i }, n)))));