@mingmaple/dsh-handover 0.9.14 → 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.
- package/README.md +123 -92
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +34 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +230 -46
- package/package.json +74 -71
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +41 -25
- package/scripts/blank-smoke.mjs +278 -109
- package/scripts/check-docs.mjs +7 -0
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -955
- package/src/index.mjs +313 -142
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +18 -10
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +23 -8
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +140 -0
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +30 -6
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +81 -85
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +17 -13
- package/src/lib/manual.mjs +135 -59
- package/src/lib/mover.mjs +23 -19
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +63 -15
- package/src/lib/pack-ops.mjs +11 -11
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/peer.mjs +4 -4
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +9 -6
- package/src/lib/scaffold.mjs +68 -26
- package/src/lib/schedule.mjs +5 -6
- package/src/lib/session-create.mjs +3 -3
- package/src/lib/session-ops.mjs +80 -51
- package/src/lib/sessions.mjs +41 -3
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -176
- package/src/lib/succession.mjs +16 -16
- package/src/lib/system.mjs +364 -0
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +101 -20
- package/src/templates/Runtime_Manual.md +263 -122
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -23
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +37 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- 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
- package/src/tools/ho_backup.mjs +135 -0
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +36 -32
- package/src/tools/ho_docmap.mjs +239 -0
- package/src/tools/ho_gate.mjs +11 -5
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +46 -5
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +46 -26
- package/src/tools/ho_status.mjs +251 -0
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +146 -42
- package/src/tools/peer.mjs +416 -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/lib/absrefs.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export function renderAbsoluteRefs(res, opts) {
|
|
|
99
99
|
if (!res.totalHits) {
|
|
100
100
|
out.push(' 未发现写死的本机绝对路径');
|
|
101
101
|
} else {
|
|
102
|
-
out.push(' 发现 ' + res.hits.length + '
|
|
102
|
+
out.push(' 发现 ' + res.hits.length + ' 个文件中有 ' + res.totalHits + ' 处绝对路径(插件不改写这些内容):');
|
|
103
103
|
for (const h of res.hits.slice(0, o.max || 10)) {
|
|
104
104
|
out.push(' ' + h.path);
|
|
105
105
|
for (const it of h.items) out.push(' L' + it.line + ' [' + it.kind + '] ' + it.sample);
|
package/src/lib/api.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { pathKey } from './pathkey.mjs';
|
|
8
9
|
import { isDir } from './text.mjs';
|
|
9
10
|
import { buildStatus } from './status.mjs';
|
|
10
11
|
import { createGlobalWorkspace } from './scaffold.mjs';
|
|
@@ -79,8 +80,12 @@ async function createGlobalSession(env, rootPath, title) {
|
|
|
79
80
|
try {
|
|
80
81
|
const reg = env.registry && env.registry();
|
|
81
82
|
const list = reg && typeof reg.list === 'function' ? (reg.list() || []) : [];
|
|
82
|
-
const norm = (s) => String(s || '')
|
|
83
|
-
const
|
|
83
|
+
const norm = (s) => pathKey(String(s || ''));
|
|
84
|
+
const same = (a, b) => {
|
|
85
|
+
if (norm(a) === norm(b)) return true;
|
|
86
|
+
try { return norm(fs.realpathSync(String(a))) === norm(fs.realpathSync(String(b))); } catch { return false; }
|
|
87
|
+
};
|
|
88
|
+
const hit = list.find((w) => w && same(w.path, rootPath));
|
|
84
89
|
if (hit) workspaceId = String(hit.id || '');
|
|
85
90
|
} catch { workspaceId = ''; }
|
|
86
91
|
const made = await newSessionIn(env, {
|
|
@@ -157,7 +162,7 @@ export function createApi(env, info) {
|
|
|
157
162
|
if (!st || !st.isDirectory()) return bad('bad-request', '目录不存在或不是目录:' + target);
|
|
158
163
|
const reg = env.registry && env.registry();
|
|
159
164
|
if (!reg || typeof reg.create !== 'function') {
|
|
160
|
-
return { ok: false, error: { code: 'no-registry', message: 'workspaceRegistry 不可用', details: { hint: '
|
|
165
|
+
return { ok: false, error: { code: 'no-registry', message: 'workspaceRegistry 不可用', details: { hint: '请手工将该路径粘贴到 DSH 的新建工作区:' + target } }, http: 501 };
|
|
161
166
|
}
|
|
162
167
|
const title = String(p.title || '').trim() || target.split(/[\\/]/).filter(Boolean).pop() || target;
|
|
163
168
|
try {
|
|
@@ -203,6 +208,7 @@ export function createApi(env, info) {
|
|
|
203
208
|
project: p.project,
|
|
204
209
|
task: p.task,
|
|
205
210
|
wake: p.wake !== false,
|
|
211
|
+
migrate: p.migrate === 'direct' ? 'direct' : 'auto',
|
|
206
212
|
});
|
|
207
213
|
env.log('会话角色:' + (r.ok ? (r.role + ' · 存活=' + r.live + ' · 已投递=' + r.delivered) : ('失败 ' + r.error)));
|
|
208
214
|
invalidateStatus(env);
|
|
@@ -218,21 +224,21 @@ export function createApi(env, info) {
|
|
|
218
224
|
if (method === 'move.start') {
|
|
219
225
|
const src = currentRoot(env);
|
|
220
226
|
if (!src) return bad('no-workspace', '未找到全局根(工作区未接入)');
|
|
221
|
-
env.log('
|
|
227
|
+
env.log('迁移:开始复制 ' + src + ' → ' + String(p.to || ''));
|
|
222
228
|
const startedAt = Date.now();
|
|
223
229
|
const r = await startMove({ src, to: p.to });
|
|
224
|
-
env.log('
|
|
230
|
+
env.log('迁移:' + (r.ok ? '校验通过' : '校验有差异') + ',用时 ' + Math.round((Date.now() - startedAt) / 1000) + ' s');
|
|
225
231
|
return r.ok ? ok(r) : { ok: false, error: { code: 'verify-failed', message: '复制完成但校验有差异,未生成切换脚本', details: r.report }, http: 500 };
|
|
226
232
|
}
|
|
227
233
|
|
|
228
234
|
if (method === 'move.switch') {
|
|
229
235
|
const r = stageSwitch({ relaunch: p.relaunch === true });
|
|
230
|
-
env.log('
|
|
236
|
+
env.log('迁移:后台切换助手 ' + (r.already ? '无需再派(已切换)' : '已派出 pid=' + r.pid));
|
|
231
237
|
return ok(r);
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
if (method === 'move.status') {
|
|
235
|
-
//
|
|
241
|
+
// 一律以迁移标记里的源路径为准:修改登记生效后当前工作区根已经变成新位置,
|
|
236
242
|
// 再拿它去比对会把「已经是新位置」误报成「还指着旧路径」。
|
|
237
243
|
return ok(moveStatus());
|
|
238
244
|
}
|
|
@@ -242,6 +248,8 @@ export function createApi(env, info) {
|
|
|
242
248
|
if (!sid) return bad('bad-request', '缺少 session_id');
|
|
243
249
|
const kind = p.kind === 'succeed' ? 'succeed' : 'compact';
|
|
244
250
|
const g = grantConsent(sid, kind === 'succeed' ? '设置页点击换任' : '设置页点击', kind);
|
|
251
|
+
// 授权一到就重装目标会话的提示段:空闲会话下一轮即可看见,不必等它自己去查。
|
|
252
|
+
if (typeof env.refreshPromptFor === 'function') { try { env.refreshPromptFor(sid); } catch { /* ignore */ } }
|
|
245
253
|
env.log((kind === 'succeed' ? '换任' : '压缩') + '授权:' + sid + '(5 分钟)');
|
|
246
254
|
invalidateStatus(env);
|
|
247
255
|
return ok(g);
|
|
@@ -265,20 +273,20 @@ export function createApi(env, info) {
|
|
|
265
273
|
|
|
266
274
|
if (method === 'move.retarget-stage') {
|
|
267
275
|
const r = stageRetarget();
|
|
268
|
-
env.log('
|
|
276
|
+
env.log('迁移:登记修改登记助手 ' + (r.already ? '无需(已在新位置)' : '已派出 pid=' + r.pid));
|
|
269
277
|
return ok(r);
|
|
270
278
|
}
|
|
271
279
|
|
|
272
280
|
if (method === 'move.repair') {
|
|
273
281
|
const r = repairLink();
|
|
274
|
-
env.log('
|
|
282
|
+
env.log('迁移:重建链接 ' + (r.already ? '已存在' : r.src + ' → ' + r.dst));
|
|
275
283
|
invalidateStatus(env);
|
|
276
284
|
return ok(r);
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
if (method === 'move.cleanup') {
|
|
280
288
|
const r = await cleanupOld({ confirm: p.confirm === true });
|
|
281
|
-
env.log('
|
|
289
|
+
env.log('迁移:清理旧备份 ' + (r.removed || []).length + ' 个');
|
|
282
290
|
return ok(r);
|
|
283
291
|
}
|
|
284
292
|
|
package/src/lib/archive.mjs
CHANGED
|
@@ -188,7 +188,7 @@ export function extractTarGz(file, destDir) {
|
|
|
188
188
|
dirs++;
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
|
-
if (e.type === '1' || e.type === '2') { skipped.push('
|
|
191
|
+
if (e.type === '1' || e.type === '2') { skipped.push('链接条目不处理:' + rel); return; }
|
|
192
192
|
if (e.type !== '0' && e.type !== '') { skipped.push('未知条目类型 ' + e.type + ':' + rel); return; }
|
|
193
193
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
194
194
|
fs.writeFileSync(dest, e.body);
|
package/src/lib/brief.mjs
CHANGED
|
@@ -41,13 +41,15 @@ export function composeBrief(input) {
|
|
|
41
41
|
out.push('水位 无法读取,需人工判断');
|
|
42
42
|
} else {
|
|
43
43
|
const pct = (input.selfUsed / input.selfWin) * 100;
|
|
44
|
-
const
|
|
45
|
-
|
|
44
|
+
const r1 = (cfg.remindRatio === undefined ? 0.35 : cfg.remindRatio) * 100;
|
|
45
|
+
const r2 = (cfg.remind2Ratio === undefined ? 0.5 : cfg.remind2Ratio) * 100;
|
|
46
|
+
const tail = pct >= r2 ? '已过 50% 档,完成当前小步骤后停止' : pct >= r1 ? '已过 35% 档,应收束并刷新交接' : '可继续';
|
|
47
|
+
out.push("水位 " + pct.toFixed(1) + "%(" + thousands(input.selfUsed) + " / " + thousands(input.selfWin) + ")→ " + tail);
|
|
46
48
|
}
|
|
47
49
|
const gate = input.gate;
|
|
48
50
|
if (!gate || gate.error) {
|
|
49
51
|
out.push('闸门 ' + (gate && gate.error ? gate.error : '读不到') + ' · 退出码 ? · ?');
|
|
50
|
-
if (gate && gate.noWorkspace) out.push('
|
|
52
|
+
if (gate && gate.noWorkspace) out.push(' 尚未接入工作区:用 ho_workspace(action="create", path="<绝对目录>", title="全局") 接入,再新建一个标题含「全局」的会话。');
|
|
51
53
|
} else {
|
|
52
54
|
out.push("闸门 " + gate.total + " 库 · 正文 " + gate.bodyLines + " 行 · 退出码 " + (gate.ok ? 0 : 10) + " · 判定:" + gate.verdict);
|
|
53
55
|
}
|
|
@@ -58,8 +60,8 @@ export function composeBrief(input) {
|
|
|
58
60
|
const notifProjects = new Set(scan.notify.map((r) => r.project));
|
|
59
61
|
const skipProjects = new Set(scan.skip.map((r) => r.project));
|
|
60
62
|
out.push("活跃 " + notifProjects.size + " 个项目,需要通知 " + scan.notify.length + " 个会话:" + (scan.notify.length ? scan.notify.map((r) => r.project + "(" + sidShort(r.sid) + ",在任)").join("、") : "无"));
|
|
61
|
-
out.push("沉默 " + skipProjects.size + " 个项目,跳过 " + scan.skip.length + "
|
|
62
|
-
if (scan.hot.length) out.push("预警 " + scan.hot.length + "
|
|
63
|
+
out.push("沉默 " + skipProjects.size + " 个项目,跳过 " + scan.skip.length + " 个会话(不唤醒,以节省 token)");
|
|
64
|
+
if (scan.hot.length) out.push("预警 " + scan.hot.length + " 个会话已过 35% 线,全为已交出的旧会话时可忽略");
|
|
63
65
|
}
|
|
64
66
|
if (input.drift) {
|
|
65
67
|
out.push('Δ交接 ' + driftVerdict(input.drift));
|
|
@@ -69,6 +71,6 @@ export function composeBrief(input) {
|
|
|
69
71
|
out.push('Δ交接 读不到(会话日志不可用)');
|
|
70
72
|
}
|
|
71
73
|
if (input.advice && input.advice.text) out.push('阶段 ' + input.advice.text);
|
|
72
|
-
out.push('提示 ' + (input.ok ? '
|
|
74
|
+
out.push('提示 ' + (input.ok ? '按「活跃」名单逐个用 peer(action="send")广播本会话 id,并记回 §3;可正常开始作业' : '本会话已过 35% 线或闸门异常,应先收齐交接,再开展新任务'));
|
|
73
75
|
return out;
|
|
74
76
|
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/** dsh-handover 配置:全部可被 profile patch 的 config 覆盖。 */
|
|
2
2
|
export const DEFAULTS = {
|
|
3
3
|
workspaceRoot: '', // 留空则从会话 cwd 推导(向上找 _Control_Center 目录)
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
remindRatio: 0.35, // 35% 一次性后台提醒(不再设 32% 提醒线)
|
|
5
|
+
remind2Ratio: 0.50, // 50% 一次性后台提醒
|
|
6
|
+
margin1: 0.45, // 35% 档判断余量:完成剩余流程后不超过 45%
|
|
7
|
+
margin2: 0.55, // 50% 档判断余量:完成剩余流程后不超过 55%
|
|
8
|
+
pureWindowRatio: 0.40, // 纯窗口线:繁重任务预计超过 40% 时引入子智能体
|
|
9
|
+
handoverRatio: 0.35, // 35% 后可压缩;更早仅助手建议或用户主动并授权
|
|
6
10
|
hardRatio: 0.85,
|
|
7
11
|
maxCompactions: 3,
|
|
8
12
|
autoResume: true,
|
|
@@ -11,19 +15,30 @@ export const DEFAULTS = {
|
|
|
11
15
|
rolesPath: '', // 留空 = ~/.dsh/storages/dsh-handover.roles.json
|
|
12
16
|
// 续跑提示:{n} {max} {handover} {role} 会被替换成真值;{handover} 是本会话自己的交接绝对路径。
|
|
13
17
|
// 只在助手登记压缩时选 resume=continue(默认)才投递;resume=wait 表示已收尾,不唤醒。
|
|
14
|
-
resumePrompt:
|
|
15
|
-
|
|
18
|
+
resumePrompt: [
|
|
19
|
+
'【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已由摘要替换。',
|
|
20
|
+
'按以下顺序执行:',
|
|
21
|
+
'一、调用 ho_handover(action=read),取回本会话交接({handover})。',
|
|
22
|
+
'二、调用 ho_docmap(action="plan", mode="read"),读取行动计划;未建立计划时跳过本步。',
|
|
23
|
+
'三、按交接 §2 与行动计划继续执行;§2 无待办时,向当前用户作一句总结后收尾并停止。',
|
|
24
|
+
'约束:不复述交接内容;不重做已完成事项。',
|
|
25
|
+
'完整流程:ho_charter(doc="runtime")。',
|
|
26
|
+
].join(String.fromCharCode(10)),
|
|
27
|
+
resumePromptUnknown: [
|
|
28
|
+
'【dsh-handover 自动续跑·第 {n}/{max} 次压缩】压缩已完成,历史已由摘要替换。',
|
|
29
|
+
'本会话为普通会话,不参与交接;如需继续,由全局会话为其指定角色。',
|
|
30
|
+
].join(String.fromCharCode(10)),
|
|
16
31
|
compactTimeoutMs: 120000,
|
|
17
32
|
compactAttempts: 3,
|
|
18
33
|
waitIdleMs: 180000, // 等会话空闲的上限(超过就放弃并回告)
|
|
19
34
|
driftSlack: 3, // Δ交接容忍轮数(超过即需刷交接)
|
|
20
|
-
wakeAuthority: 'on', // 会话守卫硬拦:on / off(off=连
|
|
21
|
-
directPeer: 'on', // 会话之间直接
|
|
35
|
+
wakeAuthority: 'on', // 会话守卫硬拦:on / off(off=连 peer 的 new 动作也不拦)
|
|
36
|
+
directPeer: 'on', // 会话之间直接 peer(action="send"):on 允许(不经全局转述);off 退回「只能向全局报告」的旧口径
|
|
22
37
|
roleGating: 'on', // 按角色裁剪工具与提示段:on / off(off=所有会话都看得见全部工具)
|
|
23
|
-
reuseCheck: true, // 建新会话前查在任会话水位(低于
|
|
38
|
+
reuseCheck: true, // 建新会话前查在任会话水位(低于 handoverRatio 拒绝,建议复用)
|
|
24
39
|
prepackMinAgeMin: 90, // 打包前要求会话静止分钟数
|
|
25
40
|
packFormat: 'auto', // 归档格式:auto(有 7-Zip 用 7z,否则内置 tar.gz)、7z、tar
|
|
26
|
-
sevenZipPath: '', //
|
|
41
|
+
sevenZipPath: '', // 留空则自动探测:常见安装位置(Windows、macOS、Linux)与 PATH 上的 7z
|
|
27
42
|
};
|
|
28
43
|
|
|
29
44
|
export function resolveConfig(raw) {
|
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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 旧版文档定位(docmap)解析:1.0 起只由迁移兼容模块 `legacy.mjs` 使用,用于把旧表并入体系注册。
|
|
3
|
+
* 新写入一律走 `system.mjs`;本模块不再参与提示段注入。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { NL, readUtf8, isFile, isDir } from './text.mjs';
|
|
8
|
+
|
|
9
|
+
export const DOCMAP_DIR = path.join('_Control_Center', 'records', 'docmap');
|
|
10
|
+
export const COLUMNS = ['路径', '一句话介绍', '何时读'];
|
|
11
|
+
|
|
12
|
+
/** 由会话角色推导所属的登记(全局/项目/杂项);普通会话没有 map。 */
|
|
13
|
+
export function lineageOf(info) {
|
|
14
|
+
const role = info && info.role ? info.role : '';
|
|
15
|
+
if (role === 'global') return { kind: 'global', project: '', task: '', key: '全局' };
|
|
16
|
+
if (role === 'project' && info.project) return { kind: 'project', project: String(info.project), task: '', key: String(info.project) };
|
|
17
|
+
if (role === 'misc' && info.task) return { kind: 'misc', project: '', task: String(info.task), key: String(info.task) };
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function lineageLabel(info) {
|
|
22
|
+
const lin = lineageOf(info);
|
|
23
|
+
if (!lin) return '';
|
|
24
|
+
if (lin.kind === 'project') return '项目「' + lin.project + '」';
|
|
25
|
+
if (lin.kind === 'misc') return '杂项任务「' + lin.task + '」';
|
|
26
|
+
return '全局';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 文件名:global.md / project-<项目名>.md / misc-<任务名>.md(相对工作区根)。 */
|
|
30
|
+
export function docmapRelPath(info) {
|
|
31
|
+
const lin = lineageOf(info);
|
|
32
|
+
if (!lin) return null;
|
|
33
|
+
const stem = lin.kind === 'global' ? 'global' : lin.kind === 'project' ? 'project-' + lin.project : 'misc-' + lin.task;
|
|
34
|
+
return path.join(DOCMAP_DIR, stem + '.md');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function docmapAbsPath(ws, info) {
|
|
38
|
+
const rel = docmapRelPath(info);
|
|
39
|
+
return rel ? path.join(String(ws || ''), rel) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripTicks(s) {
|
|
43
|
+
return String(s || '').replace(/`/g, '').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 解析表格行:跳过表头与分隔行;三列之外的多余列合并进第三列。 */
|
|
47
|
+
export function parseDocmap(text) {
|
|
48
|
+
const entries = [];
|
|
49
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
50
|
+
const line = raw.trim();
|
|
51
|
+
if (!line.startsWith('|')) continue;
|
|
52
|
+
const cells = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => stripTicks(c));
|
|
53
|
+
if (!cells.length) continue;
|
|
54
|
+
if (cells.length < 2) continue;
|
|
55
|
+
if (cells[0] === COLUMNS[0]) continue; // 表头
|
|
56
|
+
if (cells.every((c) => /^[-:\s]*$/.test(c))) continue; // 分隔行
|
|
57
|
+
const p = cells[0];
|
|
58
|
+
if (!p) continue;
|
|
59
|
+
const about = cells[1] || '';
|
|
60
|
+
const when = cells.length > 3 ? cells.slice(2).join(',') : (cells[2] || '');
|
|
61
|
+
entries.push({ path: p, about, when });
|
|
62
|
+
}
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readDocmap(ws, info) {
|
|
67
|
+
const file = docmapAbsPath(ws, info);
|
|
68
|
+
if (!file) return { file: null, exists: false, entries: [] };
|
|
69
|
+
if (!isFile(file)) return { file, exists: false, entries: [] };
|
|
70
|
+
let text = '';
|
|
71
|
+
try { text = readUtf8(file); } catch { return { file, exists: true, entries: [], error: '读取失败' }; }
|
|
72
|
+
return { file, exists: true, entries: parseDocmap(text) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 渲染文件正文(写盘用):标题一行、表格、一句判定。 */
|
|
76
|
+
export function renderDocmap(info, entries) {
|
|
77
|
+
const label = lineageLabel(info);
|
|
78
|
+
const lines = ['# 文档定位(' + label + ')', '', '| ' + COLUMNS.join(' | ') + ' |', '|' + COLUMNS.map(() => '---').join('|') + '|'];
|
|
79
|
+
for (const e of entries) lines.push('| `' + e.path + '` | ' + e.about + ' | ' + (e.when || '') + ' |');
|
|
80
|
+
lines.push('');
|
|
81
|
+
lines.push('只指路:是否读正文由会话判断,不得无目的通读。');
|
|
82
|
+
return lines.join(NL) + NL;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 注入提示段的文本:空 map 返回空串(不占位)。
|
|
87
|
+
* 形如「文档定位(项目「X」;只指路…)」+ 每条一行,路径加反引号。
|
|
88
|
+
*/
|
|
89
|
+
export function docmapBlock(ws, info) {
|
|
90
|
+
const map = readDocmap(ws, info);
|
|
91
|
+
if (!map.entries.length) return '';
|
|
92
|
+
const lines = ['文档定位(' + lineageLabel(info) + ';只指路,是否读正文由会话判断,不得无目的通读)'];
|
|
93
|
+
for (const e of map.entries) {
|
|
94
|
+
lines.push('- ' + e.path + (e.about ? ' —— ' + e.about : '') + (e.when ? '(' + e.when + ')' : ''));
|
|
95
|
+
}
|
|
96
|
+
return lines.join(NL);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 路径是否合法且存在:只收工作区相对路径,不得越出工作区。 */
|
|
100
|
+
export function pathProblem(ws, p) {
|
|
101
|
+
const raw = String(p || '').trim();
|
|
102
|
+
if (!raw) return '路径为空';
|
|
103
|
+
if (/^[a-zA-Z]:/.test(raw) || raw.startsWith('/') || raw.startsWith('\\')) return '须写工作区相对路径,不得写绝对路径';
|
|
104
|
+
const norm = path.normalize(raw).replace(/\\/g, '/');
|
|
105
|
+
if (norm === '..' || norm.startsWith('../')) return '不得指向工作区之外';
|
|
106
|
+
const abs = path.join(String(ws || ''), norm);
|
|
107
|
+
if (!isFile(abs) && !isDir(abs)) return '不存在:' + norm;
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 逐条校验:返回 [{ path, problem }]。 */
|
|
112
|
+
export function validateEntries(ws, entries) {
|
|
113
|
+
const bad = [];
|
|
114
|
+
for (const e of entries) {
|
|
115
|
+
const problem = pathProblem(ws, e.path);
|
|
116
|
+
if (problem) bad.push({ path: e.path, problem });
|
|
117
|
+
}
|
|
118
|
+
return bad;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 闸门用:检查所有 map 的路径存在性(只判存在,不判字数)。 */
|
|
122
|
+
export function checkDocmaps(ws) {
|
|
123
|
+
const dir = path.join(String(ws || ''), DOCMAP_DIR);
|
|
124
|
+
const rows = [];
|
|
125
|
+
let files = [];
|
|
126
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')).sort(); } catch { return { dir, rows, missing: 0 }; }
|
|
127
|
+
for (const name of files) {
|
|
128
|
+
const file = path.join(dir, name);
|
|
129
|
+
const stem = name.slice(0, -3);
|
|
130
|
+
let info = null;
|
|
131
|
+
if (stem === 'global') info = { role: 'global' };
|
|
132
|
+
else if (stem.startsWith('project-')) info = { role: 'project', project: stem.slice(8) };
|
|
133
|
+
else if (stem.startsWith('misc-')) info = { role: 'misc', task: stem.slice(5) };
|
|
134
|
+
let entries = [];
|
|
135
|
+
try { entries = parseDocmap(readUtf8(file)); } catch { entries = []; }
|
|
136
|
+
const bad = validateEntries(ws, entries);
|
|
137
|
+
rows.push({ name, label: info ? lineageLabel(info) : '(文件名不符合规范)', entries: entries.length, bad });
|
|
138
|
+
}
|
|
139
|
+
return { dir, rows, missing: rows.reduce((a, r) => a + r.bad.length, 0) };
|
|
140
|
+
}
|
package/src/lib/drift.mjs
CHANGED
|
@@ -61,5 +61,5 @@ export function computeDrift(opts) {
|
|
|
61
61
|
export function driftVerdict(res) {
|
|
62
62
|
return '判定:' + res.fresh + ' 个已最新、' + res.need + ' 个需刷交接' +
|
|
63
63
|
(res.unknown ? '、' + res.unknown + ' 个需人工确认' : '') +
|
|
64
|
-
(res.need || res.unknown ? '
|
|
64
|
+
(res.need || res.unknown ? ';先令需刷交接的会话写交接,再迁移' : ';可直接迁移');
|
|
65
65
|
}
|
package/src/lib/events.mjs
CHANGED
|
@@ -85,5 +85,5 @@ export function stalePressureNote(sessionId) {
|
|
|
85
85
|
if (!st.lastSummaryMs) return '';
|
|
86
86
|
const m = projCacheMtimeOf(sessionId);
|
|
87
87
|
if (m === null || m >= st.lastSummaryMs) return '';
|
|
88
|
-
return '
|
|
88
|
+
return '(压缩刚完成,投影缓存尚未刷新,水位读数滞后一轮;下一次请求后恢复正常)';
|
|
89
89
|
}
|
package/src/lib/facts.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULTS } from './config.mjs';
|
|
7
7
|
|
|
8
|
-
/** 比例转百分比文本:0.
|
|
8
|
+
/** 比例转百分比文本:0.35 → "35%"。 */
|
|
9
9
|
function pctOf(ratio) {
|
|
10
10
|
const n = Math.round(Number(ratio) * 1000) / 10;
|
|
11
11
|
return (Number.isInteger(n) ? String(n) : n.toFixed(1)) + '%';
|
|
@@ -14,24 +14,31 @@ function pctOf(ratio) {
|
|
|
14
14
|
/** 水位与压缩口径:全部由配置推导。 */
|
|
15
15
|
export function watermark(cfg) {
|
|
16
16
|
const c = cfg || DEFAULTS;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
17
|
+
const pick = (k) => (c[k] !== undefined ? c[k] : DEFAULTS[k]);
|
|
18
|
+
const remind = pctOf(pick('remindRatio'));
|
|
19
|
+
const remind2 = pctOf(pick('remind2Ratio'));
|
|
20
|
+
const handover = pctOf(pick('handoverRatio'));
|
|
21
|
+
const hard = pctOf(pick('hardRatio'));
|
|
22
|
+
const max = pick('maxCompactions');
|
|
23
|
+
return {
|
|
24
|
+
remind, remind2, handover, hard, max,
|
|
25
|
+
remindValue: Math.round(pick('remindRatio') * 100),
|
|
26
|
+
remind2Value: Math.round(pick('remind2Ratio') * 100),
|
|
27
|
+
reuseValue: Math.round(pick('handoverRatio') * 100),
|
|
28
|
+
};
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
/** 一句式交接规则(全局与项目提示段共用)。 */
|
|
25
32
|
export function handoverRuleText(cfg) {
|
|
26
33
|
const w = watermark(cfg);
|
|
27
|
-
return
|
|
34
|
+
return w.handover + ' 后可压缩,' + w.hard + ' 为硬上限;至多压缩 '
|
|
28
35
|
+ w.max + ' 次,第 ' + (w.max + 1) + ' 次换任。';
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** 设置页使用说明里的紧写形式。 */
|
|
32
39
|
export function compactSummaryText(cfg) {
|
|
33
40
|
const w = watermark(cfg);
|
|
34
|
-
return w.
|
|
41
|
+
return w.remind + ' 与 ' + w.remind2 + ' 各提醒一次,' + w.handover + ' 后可压缩,' + w.hard + ' 硬上限;同一会话最多压缩 ' + w.max + ' 次';
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
/** 提示段与工具描述的加权 token 估算:CJK 与全角标点每字符 1 token,其余每 4 字符 1 token。 */
|
|
@@ -63,9 +70,9 @@ function isWide(cp) {
|
|
|
63
70
|
* 提示词效果优先于字数,实测值超出参考值时如实展示即可。
|
|
64
71
|
*/
|
|
65
72
|
export const TOKEN_BUDGET = {
|
|
66
|
-
globalTokens:
|
|
67
|
-
globalWorstTokens:
|
|
68
|
-
projectTokens:
|
|
69
|
-
projectWorstTokens:
|
|
73
|
+
globalTokens: 900,
|
|
74
|
+
globalWorstTokens: 1000,
|
|
75
|
+
projectTokens: 800,
|
|
76
|
+
projectWorstTokens: 900,
|
|
70
77
|
toolDescTokens: 70,
|
|
71
78
|
};
|
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 { checkRegistries } from './system.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
|
// 口径:**插件自带的这套默认规则才是本体**,工作区里的同名文件是「本地化覆盖」。
|
|
@@ -86,7 +87,7 @@ export function syncCheck(ws) {
|
|
|
86
87
|
builtin = true;
|
|
87
88
|
} else {
|
|
88
89
|
return {
|
|
89
|
-
error: '
|
|
90
|
+
error: '本实例尚未接入工作区:读不到 ' + charter,
|
|
90
91
|
path: charter,
|
|
91
92
|
ok: false,
|
|
92
93
|
noWorkspace: true,
|
|
@@ -94,7 +95,7 @@ export function syncCheck(ws) {
|
|
|
94
95
|
}
|
|
95
96
|
const blk = canonicalBlock(text);
|
|
96
97
|
if (!blk || !blk.body || blk.body.length === 0) {
|
|
97
|
-
return { error: '
|
|
98
|
+
return { error: '无法取得 §8.1.1.4 的正文(结构可能已变更)', path: charter, ok: false };
|
|
98
99
|
}
|
|
99
100
|
const rows = [];
|
|
100
101
|
let bad = 0;
|
|
@@ -118,19 +119,19 @@ export function syncCheck(ws) {
|
|
|
118
119
|
verdict: bad === 0
|
|
119
120
|
|
|
120
121
|
? "全部落位。"
|
|
121
|
-
: bad + '
|
|
122
|
+
: bad + ' 个库有缺,向对应项目会话派发(由该会话写进本库 RULES.md;全局不得修改项目库内文件)。',
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export function renderSyncCheck(res) {
|
|
126
127
|
const out = [];
|
|
127
128
|
if (res.error) { out.push(res.error + ':' + res.path); return out; }
|
|
128
|
-
out.push('通用约束条同步复核 库数 ' + res.total + ' 正文 ' + res.bodyLines + ' 行 来源 §8.1.1.4' + (res.builtin ? '
|
|
129
|
+
out.push('通用约束条同步复核 库数 ' + res.total + ' 正文 ' + res.bodyLines + ' 行 来源 §8.1.1.4' + (res.builtin ? '(工作区无 Workspace_Charter.md,采用插件内置口径)' : ''));
|
|
129
130
|
for (const r of res.libraries) {
|
|
130
131
|
const flag = r.ok ? '通过' : '未通过';
|
|
131
132
|
out.push(' ' + r.name + ' 标题 ' + (r.titleOk ? '通过' : '缺') + ' 正文 ' + (r.total - r.missing.length) + '/' + r.total +
|
|
132
133
|
' 水位自查 ' + (r.waterOk ? '通过' : '缺') + ' ' + flag);
|
|
133
|
-
for (const m of r.missing.slice(0, 3)) out.push("
|
|
134
|
+
for (const m of r.missing.slice(0, 3)) out.push(" 缺:" + m.slice(0, 70));
|
|
134
135
|
}
|
|
135
136
|
const ps = res.profiles || [];
|
|
136
137
|
if (ps.length) {
|
|
@@ -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
|
+
/** 体系注册:只检查表内路径是否仍然存在(不判字数、不判内容)。 */
|
|
152
|
+
export function docmapReport(ws) {
|
|
153
|
+
return checkRegistries(ws);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function renderDocmapCheck(res) {
|
|
157
|
+
const out = [];
|
|
158
|
+
out.push('体系注册 任务 ' + res.rows.length + ' 个');
|
|
159
|
+
if (!res.rows.length) {
|
|
160
|
+
out.push(' 尚无任何任务注册(空注册不注入正文,属正常)');
|
|
161
|
+
out.push('判定:无需检查');
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
for (const r of res.rows) {
|
|
165
|
+
out.push(' ' + r.label + ' ' + r.rel + ' ' + (r.exists ? 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
|
}
|