@mingmaple/dsh-handover 0.9.13 → 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.
- package/README.md +46 -19
- package/docs/migrate.md +6 -6
- package/docs/releases/0.9.14.md +18 -0
- package/docs/releases/0.9.22.md +34 -0
- package/docs/troubleshoot.md +34 -10
- package/package.json +71 -71
- package/scripts/blank-dsh.mjs +29 -23
- package/scripts/blank-smoke.mjs +261 -78
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/src/client.js +159 -120
- package/src/index.mjs +65 -58
- package/src/lib/api.mjs +9 -7
- package/src/lib/brief.mjs +2 -2
- package/src/lib/config.mjs +3 -3
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +143 -0
- package/src/lib/gates.mjs +25 -1
- package/src/lib/guide.mjs +35 -44
- package/src/lib/lint.mjs +3 -2
- package/src/lib/manual.mjs +48 -9
- package/src/lib/mover.mjs +10 -10
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +22 -14
- package/src/lib/pack-ops.mjs +2 -2
- package/src/lib/peer.mjs +4 -4
- package/src/lib/role.mjs +8 -5
- package/src/lib/scaffold.mjs +42 -20
- package/src/lib/schedule.mjs +1 -2
- package/src/lib/session-create.mjs +1 -1
- package/src/lib/session-ops.mjs +49 -20
- package/src/lib/sessions.mjs +1 -1
- package/src/lib/status.mjs +205 -45
- package/src/lib/succession.mjs +27 -11
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- package/src/templates/Prompt_Templates.md +13 -13
- package/src/templates/Runtime_Manual.md +56 -30
- package/src/templates/rules/Handover_Rules.md +2 -1
- package/src/templates/rules/Migration_Rules.md +1 -1
- package/src/templates/standards/README.md +25 -0
- package/src/tools/ho_backup.mjs +135 -0
- package/src/tools/ho_checkpoint.mjs +12 -11
- package/src/tools/ho_docmap.mjs +153 -0
- package/src/tools/ho_gate.mjs +2 -1
- package/src/tools/ho_office.mjs +1 -1
- package/src/tools/ho_sessions.mjs +3 -4
- package/src/tools/ho_status.mjs +246 -0
- package/src/tools/ho_succeed.mjs +1 -1
- package/src/tools/ho_workspace.mjs +86 -14
- package/src/tools/peer.mjs +205 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-handover —— 全局运行期插件。
|
|
3
3
|
* 判据进代码、流程进工具;规则正文留磁盘,需要时用 ho_charter 取节(完整流程 doc="runtime")。
|
|
4
|
-
* 按会话角色载入工具与提示段:全局
|
|
4
|
+
* 按会话角色载入工具与提示段:全局 14 个工具、项目 11、杂项 10、普通 0。
|
|
5
5
|
* 源码不热加载:改完要重启 DSH。
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'node:fs';
|
|
@@ -11,35 +11,25 @@ import { resolveConfig } from './lib/config.mjs';
|
|
|
11
11
|
import { projCacheDir, pressurePct } from './lib/sessions.mjs';
|
|
12
12
|
import { createScheduler } from './lib/schedule.mjs';
|
|
13
13
|
import { compactionStateOf, stalePressureNote } from './lib/events.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { pmStatusTool } from './tools/ho_status.mjs';
|
|
15
15
|
import { pmGateTool } from './tools/ho_gate.mjs';
|
|
16
16
|
import { pmSessionsTool } from './tools/ho_sessions.mjs';
|
|
17
|
-
import { pmPhaseTool } from './tools/ho_phase.mjs';
|
|
18
17
|
import { pmCharterTool } from './tools/ho_charter.mjs';
|
|
19
18
|
import { pmHandoverTool } from './tools/ho_handover.mjs';
|
|
20
19
|
import { pmCheckpointTool } from './tools/ho_checkpoint.mjs';
|
|
21
|
-
import { pmSnapshotTool } from './tools/ho_snapshot.mjs';
|
|
22
|
-
import { pmDiagTool } from './tools/ho_diag.mjs';
|
|
23
|
-
import { pmDriftTool } from './tools/ho_drift.mjs';
|
|
24
20
|
import { pmChangelogTool } from './tools/ho_changelog.mjs';
|
|
25
|
-
import {
|
|
26
|
-
import { pmPackTool } from './tools/ho_pack.mjs';
|
|
21
|
+
import { pmBackupTool } from './tools/ho_backup.mjs';
|
|
27
22
|
import { pmWorkspaceTool } from './tools/ho_workspace.mjs';
|
|
28
|
-
import { pmInitTool } from './tools/ho_init.mjs';
|
|
29
23
|
import { pmProfileTool } from './tools/ho_profile.mjs';
|
|
30
24
|
import { pmOfficeTool } from './tools/ho_office.mjs';
|
|
31
25
|
import { pmSucceedTool } from './tools/ho_succeed.mjs';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { pmPeerSendTool } from './tools/peer_send.mjs';
|
|
35
|
-
import { pmPeerNewTool } from './tools/peer_new.mjs';
|
|
36
|
-
import { pmPeerRenameTool } from './tools/peer_rename.mjs';
|
|
37
|
-
import { pmPeerPeekTool } from './tools/peer_peek.mjs';
|
|
26
|
+
import { pmDocmapTool } from './tools/ho_docmap.mjs';
|
|
27
|
+
import { pmPeerTool } from './tools/peer.mjs';
|
|
38
28
|
import { resolveRegistry } from './lib/registry.mjs';
|
|
39
29
|
import { registerStatusRoute } from './lib/webroute.mjs';
|
|
40
30
|
import { createApi, statusSnapshot } from './lib/api.mjs';
|
|
41
31
|
import { registerRpcCarrier } from './lib/rpc-carrier.mjs';
|
|
42
|
-
import { NAMES } from './lib/names.mjs';
|
|
32
|
+
import { NAMES, PEER_NAME } from './lib/names.mjs';
|
|
43
33
|
import { readExtraPrompt, writeExtraPrompt, EXTRA_PROMPT_ORDER, EXTRA_PROMPT_SECTION } from './lib/extra-prompt.mjs';
|
|
44
34
|
import {
|
|
45
35
|
ROLES, resolveRole, sessionMeta, handoverPathFor, toolsetFor,
|
|
@@ -50,12 +40,14 @@ import { sendPrompt } from './lib/prompt.mjs';
|
|
|
50
40
|
import { resolveShortSid } from './lib/api.mjs';
|
|
51
41
|
import { routeWorkspace } from './lib/workspace.mjs';
|
|
52
42
|
import { stampTz } from './lib/brief.mjs';
|
|
53
|
-
import { grantConsent } from './lib/consent.mjs';
|
|
43
|
+
import { grantConsent, activeConsentsFor } from './lib/consent.mjs';
|
|
44
|
+
import { docmapBlock } from './lib/docmap.mjs';
|
|
45
|
+
import { moveStatus } from './lib/mover.mjs';
|
|
54
46
|
|
|
55
47
|
export const name = 'dsh-handover';
|
|
56
48
|
// 必需服务(名字已核对:dsh-agent→agents、dsh-compaction→compaction、dsh-api-session-controller→sessionController)。
|
|
57
49
|
// 只 inject 一定存在的服务:多写一个不存在的名字,cordis 会让整个插件静默不激活。
|
|
58
|
-
// agents / compaction 用运行时解析(ctx[name] → ctx.get(name)),无法获取由
|
|
50
|
+
// agents / compaction 用运行时解析(ctx[name] → ctx.get(name)),无法获取由 ho_status(view="diag")报出来、ho_checkpoint 明确报错。
|
|
59
51
|
export const inject = ['tools', 'systemPrompt', 'sessionController'];
|
|
60
52
|
|
|
61
53
|
/** 自身包信息:dist 里读 ./package.json,源码树里退回 ../package.json。 */
|
|
@@ -110,6 +102,7 @@ export function apply(ctx, config) {
|
|
|
110
102
|
// 更早的内核忽略注册选项,只能退回 agent 作用域的 restrict;两条路径按能力二选一,不并行。
|
|
111
103
|
const perStep = (() => { try { return !!(ctx.tools && typeof ctx.tools.resolve === 'function'); } catch { return false; } })();
|
|
112
104
|
const sectionDispose = new Map(); // sessionId → 撤销提示段
|
|
105
|
+
const consentSig = new Map(); // sessionId → 上次装配时的授权签名(授权出现或过期时重装)
|
|
113
106
|
const legacyRestrict = new Map(); // sessionId → 撤销限制(仅无按步解析的内核)
|
|
114
107
|
const appliedRole = new Map(); // sessionId → 上次装配的角色
|
|
115
108
|
const roleCache = new Map(); // sessionId → { at, info }(每步过滤用,短 TTL)
|
|
@@ -153,7 +146,7 @@ export function apply(ctx, config) {
|
|
|
153
146
|
scheduler.request(sessionId, Object.assign({ n, consent: 'checked' }, opts || {})).catch(() => {});
|
|
154
147
|
},
|
|
155
148
|
registry() { return resolveRegistry(ctx); },
|
|
156
|
-
/** 迁移感知:根目录是否已搬到别处(旧路径变 junction)。给提示段与
|
|
149
|
+
/** 迁移感知:根目录是否已搬到别处(旧路径变 junction)。给提示段与 ho_status(view="diag")用。 */
|
|
157
150
|
moveInfo() {
|
|
158
151
|
try {
|
|
159
152
|
const src = cfg.workspaceRoot || walkUpToWorkspace(process.cwd());
|
|
@@ -202,7 +195,7 @@ export function apply(ctx, config) {
|
|
|
202
195
|
staleNote(exec) {
|
|
203
196
|
try { return stalePressureNote(this.selfId(exec)); } catch { return ''; }
|
|
204
197
|
},
|
|
205
|
-
/** 自身版本:从 dist/package.json 读,设置页与
|
|
198
|
+
/** 自身版本:从 dist/package.json 读,设置页与 ho_status(view="diag")共用同一值。 */
|
|
206
199
|
version() {
|
|
207
200
|
try { return pkgInfo().version; } catch { return '?'; }
|
|
208
201
|
},
|
|
@@ -288,7 +281,7 @@ export function apply(ctx, config) {
|
|
|
288
281
|
return saved;
|
|
289
282
|
};
|
|
290
283
|
|
|
291
|
-
/** 当前生效的裁剪机制,供
|
|
284
|
+
/** 当前生效的裁剪机制,供 ho_status(view="diag")报出。 */
|
|
292
285
|
function roleMode() {
|
|
293
286
|
if (cfg.roleGating === 'off') return '关闭(roleGating=off,所有会话可见全部工具)';
|
|
294
287
|
return perStep
|
|
@@ -337,7 +330,11 @@ export function apply(ctx, config) {
|
|
|
337
330
|
let text = '';
|
|
338
331
|
try {
|
|
339
332
|
const ws = workspaceForAgent(agent);
|
|
340
|
-
|
|
333
|
+
// 文档定位与授权状态都是「按会话现算」的:写盘后重装一次即可生效,不另开注入点。
|
|
334
|
+
const docmap = docmapBlock(ws, info);
|
|
335
|
+
const consents = activeConsentsFor(sid);
|
|
336
|
+
text = manualFor(info, { ws, cfg, move: typeof env.moveInfo === 'function' ? env.moveInfo() : null, docmap, consents });
|
|
337
|
+
consentSig.set(sid, consents.map((r) => r.kind + ':' + r.until).join('|'));
|
|
341
338
|
} catch (e) {
|
|
342
339
|
initErrors.push('角色提示段:' + msgOf(e));
|
|
343
340
|
return;
|
|
@@ -432,6 +429,18 @@ export function apply(ctx, config) {
|
|
|
432
429
|
} catch { return false; }
|
|
433
430
|
};
|
|
434
431
|
|
|
432
|
+
/** 重装某会话的提示段:文档定位写盘后、授权到达后各调一次;不唤醒、不投消息。 */
|
|
433
|
+
env.refreshPromptFor = (sid) => {
|
|
434
|
+
try {
|
|
435
|
+
const id = String(sid || '');
|
|
436
|
+
if (!id) return false;
|
|
437
|
+
const a = agentOf(id);
|
|
438
|
+
if (!a) return false;
|
|
439
|
+
applyRoleTo(a);
|
|
440
|
+
return true;
|
|
441
|
+
} catch { return false; }
|
|
442
|
+
};
|
|
443
|
+
|
|
435
444
|
// ---------------- 调度器(续跑文案要按角色取交接路径)
|
|
436
445
|
// 注意:调度器拿到的是会话 id 字符串,不是 exec;这里用按 id 解析的版本,直接读角色表与磁盘。
|
|
437
446
|
const schedDeps = { ctx, cfg, logger: (ctx.logger || null), roleOf: null, handoverFor: null };
|
|
@@ -472,7 +481,7 @@ export function apply(ctx, config) {
|
|
|
472
481
|
};
|
|
473
482
|
try { applyExtraPrompt(readExtraPrompt()); } catch (e) { initErrors.push('全局前置提示词:' + msgOf(e)); }
|
|
474
483
|
|
|
475
|
-
// ---------------- 工具注册(
|
|
484
|
+
// ---------------- 工具注册(13 个 ho_* + 1 个 peer)
|
|
476
485
|
// 0.2.2 起工具按「步」解析:可用工具集合每一步重算。子会话排除与角色过滤都在这里一次做完,
|
|
477
486
|
// 不再另用 agent 作用域 restrict —— 两套机制并行时,换角色要么等到重启、要么互相覆盖。
|
|
478
487
|
const roleToolsets = new Map(ROLES.map((r) => [r, new Set(toolsetFor(r).allow)]));
|
|
@@ -498,26 +507,21 @@ export function apply(ctx, config) {
|
|
|
498
507
|
};
|
|
499
508
|
}
|
|
500
509
|
const tools = [
|
|
501
|
-
|
|
510
|
+
pmStatusTool(env, { schedulerReady: () => scheduler !== null, schedulerError: () => schedulerError }),
|
|
502
511
|
pmGateTool(env),
|
|
503
512
|
pmSessionsTool(env),
|
|
504
|
-
pmPhaseTool(env),
|
|
505
513
|
pmCharterTool(env),
|
|
506
514
|
pmHandoverTool(env),
|
|
507
515
|
pmCheckpointTool(env),
|
|
508
|
-
pmSnapshotTool(env),
|
|
509
|
-
pmDiagTool(env, { schedulerReady: () => scheduler !== null, schedulerError: () => schedulerError }),
|
|
510
|
-
pmDriftTool(env),
|
|
511
516
|
pmChangelogTool(env),
|
|
512
|
-
|
|
513
|
-
pmPackTool(env),
|
|
517
|
+
pmBackupTool(env),
|
|
514
518
|
pmWorkspaceTool(env),
|
|
515
|
-
pmInitTool(env),
|
|
516
519
|
pmProfileTool(env),
|
|
517
520
|
pmOfficeTool(env),
|
|
518
521
|
pmSucceedTool(env),
|
|
522
|
+
pmDocmapTool(env),
|
|
519
523
|
];
|
|
520
|
-
const keys = ['
|
|
524
|
+
const keys = ['status', 'gate', 'sessions', 'charter', 'handover', 'checkpoint', 'changelog', 'backup', 'workspace', 'profile', 'office', 'succeed', 'docmap'];
|
|
521
525
|
for (let i = 0; i < tools.length; i++) {
|
|
522
526
|
const key = keys[i];
|
|
523
527
|
try {
|
|
@@ -531,54 +535,52 @@ export function apply(ctx, config) {
|
|
|
531
535
|
}
|
|
532
536
|
}
|
|
533
537
|
|
|
534
|
-
const peerTools = [
|
|
535
|
-
pmPeerListTool(env),
|
|
536
|
-
pmPeerStatusTool(env),
|
|
537
|
-
pmPeerSendTool(env),
|
|
538
|
-
pmPeerNewTool(env),
|
|
539
|
-
pmPeerRenameTool(env),
|
|
540
|
-
pmPeerPeekTool(env),
|
|
541
|
-
];
|
|
538
|
+
const peerTools = [pmPeerTool(env)];
|
|
542
539
|
for (const t of peerTools) {
|
|
543
540
|
try {
|
|
544
541
|
ctx.tools.register(t, { when: visibleTo(t && t.name ? String(t.name) : '') });
|
|
545
542
|
if (t && t.name) registered.add(t.name);
|
|
546
543
|
} catch (e) {
|
|
547
|
-
initErrors.push((t && t.name ? String(t.name) : '
|
|
544
|
+
initErrors.push((t && t.name ? String(t.name) : 'peer') + ':' + msgOf(e));
|
|
548
545
|
}
|
|
549
546
|
}
|
|
550
547
|
|
|
551
548
|
// ---------------- 会话守卫(Charter §7.2.4):新建会话与设定角色仅属全局;
|
|
552
|
-
// 会话之间允许直接
|
|
549
|
+
// 会话之间允许直接 peer(action=send)通信(不必经全局转述),directPeer=off 时退回「只能向全局报告」的旧口径。
|
|
553
550
|
if (cfg.wakeAuthority !== 'off' && typeof ctx.tools.guard === 'function') {
|
|
554
551
|
try {
|
|
555
552
|
ctx.tools.guard((exec) => {
|
|
556
553
|
const nm = exec && exec.name;
|
|
557
|
-
if (nm !==
|
|
554
|
+
if (nm !== PEER_NAME) return undefined;
|
|
555
|
+
let args = (exec && exec.arguments) || {};
|
|
556
|
+
if (typeof args === 'string') { try { args = JSON.parse(args); } catch { args = {}; } }
|
|
557
|
+
const action = String(args.action || '');
|
|
558
|
+
// list/status/peek/rename 的权限由工具内部按角色判,这里只管两件有外部影响的事。
|
|
559
|
+
if (action !== 'send' && action !== 'new') return undefined;
|
|
558
560
|
const sid = env.selfId(exec) || '';
|
|
559
|
-
const args = (exec && exec.arguments) || {};
|
|
560
561
|
let info;
|
|
561
562
|
try { info = env.roleOf(exec); } catch { info = { role: 'plain', source: 'error' }; }
|
|
562
563
|
const mk = (allow, why) => {
|
|
563
564
|
guardLast = { sid, allow, source: why, title: titleOf(sid) };
|
|
564
565
|
if (allow) return undefined;
|
|
565
|
-
if (
|
|
566
|
+
if (action === 'new') return '会话创建仅属全局:非全局会话不得新建平级会话(杂项落到 Misc 除外)。';
|
|
566
567
|
return '会话间通信受限(directPeer=off):非全局会话只能向现任全局报告,不得唤醒其他会话。';
|
|
567
568
|
};
|
|
568
|
-
if (info.role === 'global') return mk(true, '
|
|
569
|
-
if (
|
|
570
|
-
//
|
|
569
|
+
if (info.role === 'global') return mk(true, '全局角色');
|
|
570
|
+
if (action === 'send') {
|
|
571
|
+
// 会话之间可直接通信;向全局报告也在此路。directPeer=off 才退回旧口径。
|
|
571
572
|
const target = String(args.session_id || '');
|
|
572
573
|
let t = null;
|
|
573
574
|
try { t = target ? infoFor(target, {}) : null; } catch { t = null; }
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
return mk(
|
|
575
|
+
const tRole = t ? t.role : '未知';
|
|
576
|
+
if (t && t.role === 'global') return mk(true, '向现任全局报告');
|
|
577
|
+
if (cfg.directPeer !== 'off') return mk(true, '会话之间直接通信(' + info.role + ' → ' + tRole + ')');
|
|
578
|
+
return mk(false, 'directPeer=off:仅允许向全局报告(' + info.role + ' → ' + tRole + ')');
|
|
577
579
|
}
|
|
578
|
-
//
|
|
580
|
+
// action=new:只有杂项可以落到 Misc
|
|
579
581
|
const target = String(args.workspace || args.cwd || '').toLowerCase();
|
|
580
|
-
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, '
|
|
581
|
-
return mk(false,
|
|
582
|
+
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, '杂项会话新建到 Misc');
|
|
583
|
+
return mk(false, '非全局角色不得新建平级会话(' + info.role + ')');
|
|
582
584
|
});
|
|
583
585
|
} catch { /* 老版本没有 guard:退化为规则约束 */ }
|
|
584
586
|
}
|
|
@@ -629,7 +631,10 @@ export function apply(ctx, config) {
|
|
|
629
631
|
const sid = String((h && h.id) || agent.id || '');
|
|
630
632
|
if (!sid) return;
|
|
631
633
|
const info = infoFor(sid, scopedMetaOf(h, sid));
|
|
632
|
-
|
|
634
|
+
// 角色没变也要看授权签名:用户刚从设置页或斜杠命令授权时,下一轮就该看见那一行。
|
|
635
|
+
let sig = '';
|
|
636
|
+
try { sig = activeConsentsFor(sid).map((r) => r.kind + ':' + r.until).join('|'); } catch { sig = ''; }
|
|
637
|
+
if (appliedRole.get(sid) === info.role && consentSig.get(sid) === sig) return;
|
|
633
638
|
env.log('开轮前换装:' + sid + ' · 已装 ' + (appliedRole.get(sid) || '-') + ' · 目标 ' + info.role);
|
|
634
639
|
applyRoleTo(agent, info);
|
|
635
640
|
} catch (e) { env.log('开轮前装配失败:' + msgOf(e)); }
|
|
@@ -700,7 +705,7 @@ export function apply(ctx, config) {
|
|
|
700
705
|
const allowCommands = [
|
|
701
706
|
{
|
|
702
707
|
name: 'allow-compact',
|
|
703
|
-
description: '
|
|
708
|
+
description: '允许本会话压缩一次(当前用户授权,5 分钟有效;可带会话短号授权别的会话)',
|
|
704
709
|
hint: '可留空=本会话;填 8 位短号或 session-id 则授权那个会话',
|
|
705
710
|
kind: 'compact',
|
|
706
711
|
note: '聊天页命令 /allow-compact',
|
|
@@ -708,7 +713,7 @@ export function apply(ctx, config) {
|
|
|
708
713
|
},
|
|
709
714
|
{
|
|
710
715
|
name: 'allow-succeed',
|
|
711
|
-
description: '
|
|
716
|
+
description: '允许本会话换任一次(当前用户授权,5 分钟有效;可带会话短号授权别的会话)',
|
|
712
717
|
hint: '可留空=本会话;填 8 位短号或 session-id 则授权那个会话',
|
|
713
718
|
kind: 'succeed',
|
|
714
719
|
note: '聊天页命令 /allow-succeed',
|
|
@@ -725,8 +730,10 @@ export function apply(ctx, config) {
|
|
|
725
730
|
handler: ({ agent, rawInput }) => {
|
|
726
731
|
const arg = String(rawInput || '').trim();
|
|
727
732
|
const sid = arg ? resolveShortSid(env, arg) : (agent && agent.id ? String(agent.id) : '');
|
|
728
|
-
if (!sid) return { kind: 'error', text: '
|
|
733
|
+
if (!sid) return { kind: 'error', text: '无法获取会话 id:请写成 /' + cmd.name + ' session-xxxx' };
|
|
729
734
|
const g = grantConsent(sid, cmd.note, cmd.kind);
|
|
735
|
+
// 授权一到就重装提示段:目标会话下一轮(哪怕现在空闲)就能看见那一行。
|
|
736
|
+
try { env.refreshPromptFor(sid); } catch { /* 装配失败不影响授权本身 */ }
|
|
730
737
|
try { env.log(cmd.action + '授权(命令):' + sid); } catch { /* ignore */ }
|
|
731
738
|
return { kind: 'success', text: '已允许 ' + sid + ' 在 ' + g.minutes + ' 分钟内' + cmd.action + '一次(用掉即失效)。' };
|
|
732
739
|
},
|
package/src/lib/api.mjs
CHANGED
|
@@ -218,21 +218,21 @@ export function createApi(env, info) {
|
|
|
218
218
|
if (method === 'move.start') {
|
|
219
219
|
const src = currentRoot(env);
|
|
220
220
|
if (!src) return bad('no-workspace', '未找到全局根(工作区未接入)');
|
|
221
|
-
env.log('
|
|
221
|
+
env.log('迁移:开始复制 ' + src + ' → ' + String(p.to || ''));
|
|
222
222
|
const startedAt = Date.now();
|
|
223
223
|
const r = await startMove({ src, to: p.to });
|
|
224
|
-
env.log('
|
|
224
|
+
env.log('迁移:' + (r.ok ? '校验通过' : '校验有差异') + ',用时 ' + Math.round((Date.now() - startedAt) / 1000) + ' s');
|
|
225
225
|
return r.ok ? ok(r) : { ok: false, error: { code: 'verify-failed', message: '复制完成但校验有差异,未生成切换脚本', details: r.report }, http: 500 };
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
if (method === 'move.switch') {
|
|
229
229
|
const r = stageSwitch({ relaunch: p.relaunch === true });
|
|
230
|
-
env.log('
|
|
230
|
+
env.log('迁移:后台切换助手 ' + (r.already ? '无需再派(已切换)' : '已派出 pid=' + r.pid));
|
|
231
231
|
return ok(r);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
if (method === 'move.status') {
|
|
235
|
-
//
|
|
235
|
+
// 一律以迁移标记里的源路径为准:修改登记生效后当前工作区根已经变成新位置,
|
|
236
236
|
// 再拿它去比对会把「已经是新位置」误报成「还指着旧路径」。
|
|
237
237
|
return ok(moveStatus());
|
|
238
238
|
}
|
|
@@ -242,6 +242,8 @@ export function createApi(env, info) {
|
|
|
242
242
|
if (!sid) return bad('bad-request', '缺少 session_id');
|
|
243
243
|
const kind = p.kind === 'succeed' ? 'succeed' : 'compact';
|
|
244
244
|
const g = grantConsent(sid, kind === 'succeed' ? '设置页点击换任' : '设置页点击', kind);
|
|
245
|
+
// 授权一到就重装目标会话的提示段:空闲会话下一轮即可看见,不必等它自己去查。
|
|
246
|
+
if (typeof env.refreshPromptFor === 'function') { try { env.refreshPromptFor(sid); } catch { /* ignore */ } }
|
|
245
247
|
env.log((kind === 'succeed' ? '换任' : '压缩') + '授权:' + sid + '(5 分钟)');
|
|
246
248
|
invalidateStatus(env);
|
|
247
249
|
return ok(g);
|
|
@@ -265,20 +267,20 @@ export function createApi(env, info) {
|
|
|
265
267
|
|
|
266
268
|
if (method === 'move.retarget-stage') {
|
|
267
269
|
const r = stageRetarget();
|
|
268
|
-
env.log('
|
|
270
|
+
env.log('迁移:登记修改登记助手 ' + (r.already ? '无需(已在新位置)' : '已派出 pid=' + r.pid));
|
|
269
271
|
return ok(r);
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
if (method === 'move.repair') {
|
|
273
275
|
const r = repairLink();
|
|
274
|
-
env.log('
|
|
276
|
+
env.log('迁移:重建链接 ' + (r.already ? '已存在' : r.src + ' → ' + r.dst));
|
|
275
277
|
invalidateStatus(env);
|
|
276
278
|
return ok(r);
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
if (method === 'move.cleanup') {
|
|
280
282
|
const r = await cleanupOld({ confirm: p.confirm === true });
|
|
281
|
-
env.log('
|
|
283
|
+
env.log('迁移:清理旧备份 ' + (r.removed || []).length + ' 个');
|
|
282
284
|
return ok(r);
|
|
283
285
|
}
|
|
284
286
|
|
package/src/lib/brief.mjs
CHANGED
|
@@ -59,7 +59,7 @@ export function composeBrief(input) {
|
|
|
59
59
|
const skipProjects = new Set(scan.skip.map((r) => r.project));
|
|
60
60
|
out.push("活跃 " + notifProjects.size + " 个项目,需要通知 " + scan.notify.length + " 个会话:" + (scan.notify.length ? scan.notify.map((r) => r.project + "(" + sidShort(r.sid) + ",在任)").join("、") : "无"));
|
|
61
61
|
out.push("沉默 " + skipProjects.size + " 个项目,跳过 " + scan.skip.length + " 个会话(不唤醒,省 token)");
|
|
62
|
-
if (scan.hot.length) out.push("预警 " + scan.hot.length + " 个会话已进预警区(≥" + Math.round(scan.warn) + "%)——
|
|
62
|
+
if (scan.hot.length) out.push("预警 " + scan.hot.length + " 个会话已进预警区(≥" + Math.round(scan.warn) + "%)—— 全为已交出的旧会话则可忽略");
|
|
63
63
|
}
|
|
64
64
|
if (input.drift) {
|
|
65
65
|
out.push('Δ交接 ' + driftVerdict(input.drift));
|
|
@@ -69,6 +69,6 @@ export function composeBrief(input) {
|
|
|
69
69
|
out.push('Δ交接 读不到(会话日志不可用)');
|
|
70
70
|
}
|
|
71
71
|
if (input.advice && input.advice.text) out.push('阶段 ' + input.advice.text);
|
|
72
|
-
out.push('提示 ' + (input.ok ? '
|
|
72
|
+
out.push('提示 ' + (input.ok ? '按「活跃」名单逐个用 peer(action="send")广播本会话 id 并记回 §3;可正常开始作业' : '本会话已过阈值或闸门异常,应先收齐交接,再开展新任务'));
|
|
73
73
|
return out;
|
|
74
74
|
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -11,14 +11,14 @@ export const DEFAULTS = {
|
|
|
11
11
|
rolesPath: '', // 留空 = ~/.dsh/storages/dsh-handover.roles.json
|
|
12
12
|
// 续跑提示:{n} {max} {handover} {role} 会被替换成真值;{handover} 是本会话自己的交接绝对路径。
|
|
13
13
|
// 只在助手登记压缩时选 resume=continue(默认)才投递;resume=wait 表示已收尾,不唤醒。
|
|
14
|
-
resumePrompt: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。第一步:ho_handover(action=read) 取回本会话的交接({handover});然后按 §2「下一步」继续(若 §2
|
|
14
|
+
resumePrompt: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。第一步:ho_handover(action=read) 取回本会话的交接({handover});然后按 §2「下一步」继续(若 §2 已无待办,向当前用户一句话收尾并停止)。不要复述交接内容,不要重做已完成的事。完整流程:ho_charter(doc="runtime")。',
|
|
15
15
|
resumePromptUnknown: '【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已被摘要替换。本会话是普通会话,不参与交接;如需继续,请由全局会话为其指定角色。',
|
|
16
16
|
compactTimeoutMs: 120000,
|
|
17
17
|
compactAttempts: 3,
|
|
18
18
|
waitIdleMs: 180000, // 等会话空闲的上限(超过就放弃并回告)
|
|
19
19
|
driftSlack: 3, // Δ交接容忍轮数(超过即需刷交接)
|
|
20
|
-
wakeAuthority: 'on', // 会话守卫硬拦:on / off(off=连
|
|
21
|
-
directPeer: 'on', // 会话之间直接
|
|
20
|
+
wakeAuthority: 'on', // 会话守卫硬拦:on / off(off=连 peer 的 new 动作也不拦)
|
|
21
|
+
directPeer: 'on', // 会话之间直接 peer(action="send"):on 允许(不经全局转述);off 退回「只能向全局报告」的旧口径
|
|
22
22
|
roleGating: 'on', // 按角色裁剪工具与提示段:on / off(off=所有会话都看得见全部工具)
|
|
23
23
|
reuseCheck: true, // 建新会话前查在任会话水位(低于 warnRatio 拒绝)
|
|
24
24
|
prepackMinAgeMin: 90, // 打包前要求会话静止分钟数
|
package/src/lib/consent.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export function checkConsent(sessionId, kind) {
|
|
|
96
96
|
return {
|
|
97
97
|
ok: false,
|
|
98
98
|
kind: k,
|
|
99
|
-
reason: k === 'succeed' ? '
|
|
99
|
+
reason: k === 'succeed' ? '当前用户还没有授权换任(聊天页命令 /allow-succeed)' : '当前用户还没有在设置页点击「授权压缩」',
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
return { ok: true, kind: k, until: hit.until, leftMs: hit.until - now, atText: hit.atText || '' };
|
|
@@ -121,3 +121,56 @@ export function listConsent() {
|
|
|
121
121
|
})).filter((r) => r.leftMs > 0);
|
|
122
122
|
return rows.sort((a, b) => a.sessionId.localeCompare(b.sessionId, 'zh') || a.kind.localeCompare(b.kind));
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 某会话当前有效的授权(只读,不清过期条目)。
|
|
127
|
+
* 提示段与 ho_status 共用:授权一旦写入,下一轮重建提示段时就能看见;过期自然消失。
|
|
128
|
+
*/
|
|
129
|
+
export function activeConsentsFor(sessionId) {
|
|
130
|
+
const sid = String(sessionId || '');
|
|
131
|
+
if (!sid) return [];
|
|
132
|
+
const all = readAll();
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const rows = [];
|
|
135
|
+
for (const key of Object.keys(all)) {
|
|
136
|
+
const entry = all[key];
|
|
137
|
+
if (!entry || !entry.until || entry.until <= now) continue;
|
|
138
|
+
if (sidOfKey(key) !== sid) continue;
|
|
139
|
+
rows.push({
|
|
140
|
+
kind: kindOfEntry(key, entry),
|
|
141
|
+
until: entry.until,
|
|
142
|
+
leftMs: entry.until - now,
|
|
143
|
+
atText: entry.atText || '',
|
|
144
|
+
note: entry.note || '',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return rows.sort((a, b) => a.kind.localeCompare(b.kind));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const KIND_LABEL = { compact: '压缩', succeed: '换任' };
|
|
151
|
+
|
|
152
|
+
function untilText(ms) {
|
|
153
|
+
const d = new Date(ms);
|
|
154
|
+
const off = -d.getTimezoneOffset();
|
|
155
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
156
|
+
const a = Math.abs(off);
|
|
157
|
+
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' '
|
|
158
|
+
+ pad(d.getHours()) + ':' + pad(d.getMinutes()) + ' ' + (off >= 0 ? '+' : '-') + pad(Math.floor(a / 60)) + pad(a % 60);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 提示段用:每种有效授权一行,极短;无授权返回空数组(不占位)。 */
|
|
162
|
+
export function consentPromptLines(rows) {
|
|
163
|
+
return (rows || []).map((r) => '当前用户已授权' + KIND_LABEL[r.kind] + ',至 ' + untilText(r.until) + '(一次性,用后失效)。');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** ho_status 用:一行状态,压缩与换任各说一句,未授权的写「无授权」。 */
|
|
167
|
+
export function formatConsentLine(rows) {
|
|
168
|
+
const hit = new Map((rows || []).map((r) => [r.kind, r]));
|
|
169
|
+
const parts = CONSENT_KINDS.map((k) => {
|
|
170
|
+
const r = hit.get(k);
|
|
171
|
+
if (!r) return KIND_LABEL[k] + ' 无授权';
|
|
172
|
+
const min = Math.max(1, Math.ceil(r.leftMs / 60000));
|
|
173
|
+
return KIND_LABEL[k] + ' 剩余 ' + min + ' 分钟(至 ' + untilText(r.until) + ',一次性)';
|
|
174
|
+
});
|
|
175
|
+
return parts.join(';');
|
|
176
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文档定位 map(docmap):每个会话(全局、每个项目、每个杂项任务)一份文件,写「本类别有哪些文档、各自一句话介绍、什么时候该读」。
|
|
3
|
+
*
|
|
4
|
+
* 定位:只指路,不规定读多少。是否读正文由被注入的会话自己判断;
|
|
5
|
+
* 空 map 不注入,提示段其余文字一字不动。文件放在 _Control_Center/records/docmap/,
|
|
6
|
+
* 随工作区迁移;写入者必须是在任会话(由 tools/ho_docmap.mjs 用在任登记表判定)。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { NL, readUtf8, isFile, isDir } from './text.mjs';
|
|
11
|
+
|
|
12
|
+
export const DOCMAP_DIR = path.join('_Control_Center', 'records', 'docmap');
|
|
13
|
+
export const COLUMNS = ['路径', '一句话介绍', '何时读'];
|
|
14
|
+
|
|
15
|
+
/** 由会话角色推导所属的登记(全局/项目/杂项);普通会话没有 map。 */
|
|
16
|
+
export function lineageOf(info) {
|
|
17
|
+
const role = info && info.role ? info.role : '';
|
|
18
|
+
if (role === 'global') return { kind: 'global', project: '', task: '', key: '全局' };
|
|
19
|
+
if (role === 'project' && info.project) return { kind: 'project', project: String(info.project), task: '', key: String(info.project) };
|
|
20
|
+
if (role === 'misc' && info.task) return { kind: 'misc', project: '', task: String(info.task), key: String(info.task) };
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function lineageLabel(info) {
|
|
25
|
+
const lin = lineageOf(info);
|
|
26
|
+
if (!lin) return '';
|
|
27
|
+
if (lin.kind === 'project') return '项目「' + lin.project + '」';
|
|
28
|
+
if (lin.kind === 'misc') return '杂项任务「' + lin.task + '」';
|
|
29
|
+
return '全局';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 文件名:global.md / project-<项目名>.md / misc-<任务名>.md(相对工作区根)。 */
|
|
33
|
+
export function docmapRelPath(info) {
|
|
34
|
+
const lin = lineageOf(info);
|
|
35
|
+
if (!lin) return null;
|
|
36
|
+
const stem = lin.kind === 'global' ? 'global' : lin.kind === 'project' ? 'project-' + lin.project : 'misc-' + lin.task;
|
|
37
|
+
return path.join(DOCMAP_DIR, stem + '.md');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function docmapAbsPath(ws, info) {
|
|
41
|
+
const rel = docmapRelPath(info);
|
|
42
|
+
return rel ? path.join(String(ws || ''), rel) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripTicks(s) {
|
|
46
|
+
return String(s || '').replace(/`/g, '').trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 解析表格行:跳过表头与分隔行;三列之外的多余列合并进第三列。 */
|
|
50
|
+
export function parseDocmap(text) {
|
|
51
|
+
const entries = [];
|
|
52
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
53
|
+
const line = raw.trim();
|
|
54
|
+
if (!line.startsWith('|')) continue;
|
|
55
|
+
const cells = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => stripTicks(c));
|
|
56
|
+
if (!cells.length) continue;
|
|
57
|
+
if (cells.length < 2) continue;
|
|
58
|
+
if (cells[0] === COLUMNS[0]) continue; // 表头
|
|
59
|
+
if (cells.every((c) => /^[-:\s]*$/.test(c))) continue; // 分隔行
|
|
60
|
+
const p = cells[0];
|
|
61
|
+
if (!p) continue;
|
|
62
|
+
const about = cells[1] || '';
|
|
63
|
+
const when = cells.length > 3 ? cells.slice(2).join(',') : (cells[2] || '');
|
|
64
|
+
entries.push({ path: p, about, when });
|
|
65
|
+
}
|
|
66
|
+
return entries;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readDocmap(ws, info) {
|
|
70
|
+
const file = docmapAbsPath(ws, info);
|
|
71
|
+
if (!file) return { file: null, exists: false, entries: [] };
|
|
72
|
+
if (!isFile(file)) return { file, exists: false, entries: [] };
|
|
73
|
+
let text = '';
|
|
74
|
+
try { text = readUtf8(file); } catch { return { file, exists: true, entries: [], error: '读取失败' }; }
|
|
75
|
+
return { file, exists: true, entries: parseDocmap(text) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 渲染文件正文(写盘用):标题一行、表格、一句判定。 */
|
|
79
|
+
export function renderDocmap(info, entries) {
|
|
80
|
+
const label = lineageLabel(info);
|
|
81
|
+
const lines = ['# 文档定位(' + label + ')', '', '| ' + COLUMNS.join(' | ') + ' |', '|' + COLUMNS.map(() => '---').join('|') + '|'];
|
|
82
|
+
for (const e of entries) lines.push('| `' + e.path + '` | ' + e.about + ' | ' + (e.when || '') + ' |');
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('只指路:是否读正文由你判断,不要无目的通读。');
|
|
85
|
+
return lines.join(NL) + NL;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 注入提示段的文本:空 map 返回空串(不占位)。
|
|
90
|
+
* 形如「文档定位(项目「X」;只指路…)」+ 每条一行,路径加反引号。
|
|
91
|
+
*/
|
|
92
|
+
export function docmapBlock(ws, info) {
|
|
93
|
+
const map = readDocmap(ws, info);
|
|
94
|
+
if (!map.entries.length) return '';
|
|
95
|
+
const lines = ['文档定位(' + lineageLabel(info) + ';只指路,是否读正文由你判断,不要无目的通读)'];
|
|
96
|
+
for (const e of map.entries) {
|
|
97
|
+
lines.push('- ' + e.path + (e.about ? ' —— ' + e.about : '') + (e.when ? '(' + e.when + ')' : ''));
|
|
98
|
+
}
|
|
99
|
+
return lines.join(NL);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 路径是否合法且存在:只收工作区相对路径,不得越出工作区。 */
|
|
103
|
+
export function pathProblem(ws, p) {
|
|
104
|
+
const raw = String(p || '').trim();
|
|
105
|
+
if (!raw) return '路径为空';
|
|
106
|
+
if (/^[a-zA-Z]:/.test(raw) || raw.startsWith('/') || raw.startsWith('\\')) return '要写工作区相对路径,不写绝对路径';
|
|
107
|
+
const norm = path.normalize(raw).replace(/\\/g, '/');
|
|
108
|
+
if (norm === '..' || norm.startsWith('../')) return '不得指向工作区之外';
|
|
109
|
+
const abs = path.join(String(ws || ''), norm);
|
|
110
|
+
if (!isFile(abs) && !isDir(abs)) return '不存在:' + norm;
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 逐条校验:返回 [{ path, problem }]。 */
|
|
115
|
+
export function validateEntries(ws, entries) {
|
|
116
|
+
const bad = [];
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
const problem = pathProblem(ws, e.path);
|
|
119
|
+
if (problem) bad.push({ path: e.path, problem });
|
|
120
|
+
}
|
|
121
|
+
return bad;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 闸门用:检查所有 map 的路径存在性(只判存在,不判字数)。 */
|
|
125
|
+
export function checkDocmaps(ws) {
|
|
126
|
+
const dir = path.join(String(ws || ''), DOCMAP_DIR);
|
|
127
|
+
const rows = [];
|
|
128
|
+
let files = [];
|
|
129
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); } catch { return { dir, rows, missing: 0 }; }
|
|
130
|
+
for (const name of files) {
|
|
131
|
+
const file = path.join(dir, name);
|
|
132
|
+
const stem = name.slice(0, -3);
|
|
133
|
+
let info = null;
|
|
134
|
+
if (stem === 'global') info = { role: 'global' };
|
|
135
|
+
else if (stem.startsWith('project-')) info = { role: 'project', project: stem.slice(8) };
|
|
136
|
+
else if (stem.startsWith('misc-')) info = { role: 'misc', task: stem.slice(5) };
|
|
137
|
+
let entries = [];
|
|
138
|
+
try { entries = parseDocmap(readUtf8(file)); } catch { entries = []; }
|
|
139
|
+
const bad = validateEntries(ws, entries);
|
|
140
|
+
rows.push({ name, label: info ? lineageLabel(info) : '(文件名不符合规范)', entries: entries.length, bad });
|
|
141
|
+
}
|
|
142
|
+
return { dir, rows, missing: rows.reduce((a, r) => a + r.bad.length, 0) };
|
|
143
|
+
}
|
package/src/lib/gates.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { readUtf8, isFile, isDir } from './text.mjs';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { charterPath, canonicalBlock, TITLE_KEY, WATER_KEY } from './charter.mjs';
|
|
6
6
|
import { readProfile, listProfiles } from './profile.mjs';
|
|
7
|
+
import { checkDocmaps } from './docmap.mjs';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* 读 Registry.md 的「形态」列 → { 项目名: 形态 }。
|
|
@@ -61,7 +62,6 @@ function libraryRoots(ws) {
|
|
|
61
62
|
return out;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
/** 同步复核:返回 { libraries, total, missing, verdict, ok }。 */
|
|
65
65
|
/** 插件内置的 Charter 骨架(dist/templates/Charter.skeleton.md);工作区那份只是覆盖。 */
|
|
66
66
|
function bundledCharter() {
|
|
67
67
|
try {
|
|
@@ -72,6 +72,7 @@ function bundledCharter() {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** 同步复核:返回 { libraries, total, missing, verdict, ok }。 */
|
|
75
76
|
export function syncCheck(ws) {
|
|
76
77
|
const charter = charterPath(ws);
|
|
77
78
|
// 口径:**插件自带的这套默认规则才是本体**,工作区里的同名文件是「本地化覆盖」。
|
|
@@ -145,4 +146,27 @@ export function renderSyncCheck(res) {
|
|
|
145
146
|
out.push('');
|
|
146
147
|
out.push('判定:' + res.verdict);
|
|
147
148
|
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 文档定位 map:只检查表内路径是否仍然存在(不判字数、不判内容)。 */
|
|
152
|
+
export function docmapReport(ws) {
|
|
153
|
+
return checkDocmaps(ws);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function renderDocmapCheck(res) {
|
|
157
|
+
const out = [];
|
|
158
|
+
out.push('文档定位 map 目录 ' + res.dir);
|
|
159
|
+
if (!res.rows.length) {
|
|
160
|
+
out.push(' 还没有任何 map(空 map 不注入提示段,属正常)');
|
|
161
|
+
out.push('判定:无需检查');
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
for (const r of res.rows) {
|
|
165
|
+
out.push(' ' + r.name + ' ' + r.label + ' ' + r.entries + ' 条 ' + (r.bad.length ? '失效 ' + r.bad.length + ' 条' : '路径全部存在'));
|
|
166
|
+
for (const b of r.bad.slice(0, 3)) out.push(' 失效:' + b.path + ' —— ' + b.problem);
|
|
167
|
+
}
|
|
168
|
+
out.push('判定:' + (res.missing === 0
|
|
169
|
+
? '全部落位。'
|
|
170
|
+
: res.missing + ' 条路径失效 —— 由该类别在任会话用 ho_docmap(action="write") 更新。'));
|
|
171
|
+
return out;
|
|
148
172
|
}
|