@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局任务库:`_Control_Center/tasks/<任务>/`。
|
|
3
|
+
* 全局可同时承担多个任务,每个任务有独立交接、体系注册与计划;任务可整体独立为项目或杂项会话。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { NL, readUtf8, isFile, isDir } from './text.mjs';
|
|
8
|
+
import { assertTaskName } from './role.mjs';
|
|
9
|
+
import { renderRegistry, syncGlobalRefs } from './system.mjs';
|
|
10
|
+
|
|
11
|
+
const HANDOVER = [
|
|
12
|
+
'# 交接(全局任务)',
|
|
13
|
+
'',
|
|
14
|
+
'## 1. 正在做的事',
|
|
15
|
+
'',
|
|
16
|
+
'未开始;由全局会话在任务启动时补写。',
|
|
17
|
+
'',
|
|
18
|
+
'## 2. 下一步',
|
|
19
|
+
'',
|
|
20
|
+
'- 待定。',
|
|
21
|
+
'',
|
|
22
|
+
].join(NL);
|
|
23
|
+
|
|
24
|
+
function copyIf(src, dst) {
|
|
25
|
+
if (!isFile(src) || fs.existsSync(dst)) return false;
|
|
26
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
27
|
+
fs.copyFileSync(src, dst);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 建立全局任务库:目录、交接、体系注册、规则与模板副本;随后重建全局总注册。
|
|
33
|
+
* @returns {{ dir: string, created: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function createGlobalTask(ws, task) {
|
|
36
|
+
const name = String(task || '').trim();
|
|
37
|
+
assertTaskName(name);
|
|
38
|
+
const dir = path.join(String(ws || ''), '_Control_Center', 'tasks', name);
|
|
39
|
+
if (isDir(dir) && fs.readdirSync(dir).length) throw new Error('全局任务已存在且非空:' + dir);
|
|
40
|
+
fs.mkdirSync(path.join(dir, '_archive'), { recursive: true });
|
|
41
|
+
const created = [];
|
|
42
|
+
const files = [
|
|
43
|
+
['Handover.md', HANDOVER],
|
|
44
|
+
['System.md', renderRegistry({ role: 'global-task', task: name }, { structure: [], files: [], plan: '' })],
|
|
45
|
+
];
|
|
46
|
+
for (const [rel, body] of files) {
|
|
47
|
+
const dst = path.join(dir, rel);
|
|
48
|
+
if (fs.existsSync(dst)) continue;
|
|
49
|
+
fs.writeFileSync(dst, body, 'utf8');
|
|
50
|
+
created.push(rel);
|
|
51
|
+
}
|
|
52
|
+
// 暂任规范与交接规范随任务库独立,避免暂任方跨库读取全局规则。
|
|
53
|
+
const rules = path.join(String(ws || ''), '_Control_Center', 'rules');
|
|
54
|
+
for (const rel of ['Delegation_Rules.md', 'Handover_Rules.md']) {
|
|
55
|
+
if (copyIf(path.join(rules, rel), path.join(dir, rel))) created.push(rel);
|
|
56
|
+
}
|
|
57
|
+
const tpl = path.join(String(ws || ''), '_Control_Center', 'templates');
|
|
58
|
+
for (const rel of ['Plan.skeleton.md', 'System.skeleton.md']) {
|
|
59
|
+
if (copyIf(path.join(tpl, rel), path.join(dir, rel))) created.push(rel);
|
|
60
|
+
}
|
|
61
|
+
syncGlobalRefs(ws);
|
|
62
|
+
return { dir, created };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 读全局任务交接正文(不存在返回空串)。 */
|
|
66
|
+
export function readTaskHandover(ws, task) {
|
|
67
|
+
const file = path.join(String(ws || ''), '_Control_Center', 'tasks', String(task || ''), 'Handover.md');
|
|
68
|
+
try { return readUtf8(file); } catch { return ''; }
|
|
69
|
+
}
|
package/src/lib/guide.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 设置页「使用说明」与「工具明细」的内容:面向人,不面向模型。
|
|
3
3
|
* 数据放宿主侧,客户端只负责渲染——改文案不用动客户端 bundle。
|
|
4
|
-
*
|
|
4
|
+
* 行文用标准书面语:示例一律写成可以直接提出的要求。
|
|
5
5
|
* 水位与工具数由 facts.mjs、names.mjs 推导,不在这里重复写死。
|
|
6
6
|
*/
|
|
7
7
|
import { compactSummaryText, watermark } from './facts.mjs';
|
|
@@ -9,11 +9,11 @@ import { ALL_TOOL_NAMES } from './names.mjs';
|
|
|
9
9
|
|
|
10
10
|
/** 首次安装、尚未接入工作区时的下一步(设置页与工具输出共用同一份)。 */
|
|
11
11
|
export const SETUP_STEPS = [
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
12
|
+
'准备一个长期保留的目录,作为全局根的上级目录;不得选用临时目录或下载目录。',
|
|
13
|
+
'在本页下方的输入框中填写该目录的绝对路径,再点击「一键部署」。插件将在其中建立 _Workspace 与整套结构,注册为「全局」工作区,并新建一个「全局」会话。',
|
|
14
|
+
'在左侧「全局」工作区中打开新会话,即可开始。若只需先建立文件体系,可点击「仅建立文件体系」,之后再注册工作区。',
|
|
15
|
+
'工作区已接入但尚无全局会话时,本页会单独显示「全局会话」卡片,可随时补建。',
|
|
16
|
+
'新建项目或杂项会话时,应指明目标工作区;找不到工作区时,插件会列出已注册的工作区,不会静默落到其他位置。',
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
export function buildGuide(cfg) {
|
|
@@ -21,84 +21,80 @@ export function buildGuide(cfg) {
|
|
|
21
21
|
const compact = compactSummaryText(cfg);
|
|
22
22
|
const toolCount = ALL_TOOL_NAMES.length;
|
|
23
23
|
return {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
[
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'项目与杂项的骨架由各自会话注册;构建型、动画型等项目不必套用插件类项目的文档集。',
|
|
99
|
-
'迁移只搬运文件与清单,不改写文件内容:包内写死的绝对路径会以清单形式报出,由用户或项目会话自行修改;库内引用一律使用相对路径。',
|
|
100
|
-
'插件本体为公开件,不含任何用户项目、会话或账号数据;用户数据与角色表都保存在本机。',
|
|
101
|
-
],
|
|
24
|
+
intro: [
|
|
25
|
+
'插件功能主要在会话中调用:用中文说明要做的事,全局会话会选择相应工具执行,不必记命令;设置页另提供授权、迁移与会话角色等操作入口。',
|
|
26
|
+
'工具与提示按会话角色载入:全局会话可见全部 ' + toolCount + ' 个工具,项目与杂项会话仅载入所需的部分工具,普通会话不载入插件工具,也不注入提示段。角色在下方「会话角色」中指定。',
|
|
27
|
+
'本页除首次安装、全局前置提示词与会话角色外均为只读;需要最新读数时,点击右上角的「刷新」。',
|
|
28
|
+
],
|
|
29
|
+
situations: [
|
|
30
|
+
{ when: '首次安装', say: '部署全局文件体系', what: '本页「一键部署」一次完成 3 步:建立文件体系、注册「全局」工作区、建立「全局」会话' },
|
|
31
|
+
{ when: '作业开始与结束', say: '先出一份起始简报', what: '汇总上下文水位、通用约束条闸门、会话清点、交接漂移与阶段建议(ho_status view=brief)' },
|
|
32
|
+
{ when: '查阅规则', say: '取出 Charter 第 7.2.3 节', what: '按节返回规则正文,避免整篇读取;插件用法用 doc="runtime"(ho_charter)' },
|
|
33
|
+
{ when: '交接与压缩续跑', say: '写好交接文档后压缩并继续', what: '按固定流程重写交接(读旧交接、归档、读模板、新建写入),体检后登记压缩;未完成用 resume=continue 续跑,已收尾用 resume=wait(ho_handover、ho_checkpoint)' },
|
|
34
|
+
{ when: '上下文到 35% 或 50%', say: '查询本会话的上下文水位', what: compact + ';2 条线各后台提醒一次,不生成用户消息(ho_status view=phase)' },
|
|
35
|
+
{ when: '换任', say: '让在任会话自己完成换任', what: '先由当前用户授权:聊天框发送 /allow-succeed,或在设置页「授权」卡点击「授权换任」;在任会话随后校验交接、建立接任会话并登记在任(ho_succeed)' },
|
|
36
|
+
{ when: '会话角色', say: '把 Demo1 设为项目角色', what: '写角色表,当场收窄该会话的工具与提示,并投递首轮指令以建立体系;项目与杂项互转默认先快照再移动,也可选择直接建(ho_sessions action=role)' },
|
|
37
|
+
{ when: '通知会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会话之间可直接通信,不必经全局转述;该动作会消耗目标会话一整轮 token,应尽量少用(peer action=send)' },
|
|
38
|
+
{ when: '多会话协作', say: '把这三件事分给三个会话并收结果', what: '先逐个 send 派任务,再用 collect 列出要等待的会话;先完成者先返回,同时完成者一并返回。被派方完成即结束回合,不发送确认或进度消息(peer action=collect)' },
|
|
39
|
+
{ when: '全局前置提示词', say: '将这段设为全局前置提示词', what: '在本页保存到本机;仅在开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
|
|
40
|
+
{ when: '项目骨架', say: '注册 Demo 的骨架', what: '读取、注册、校验项目或杂项的骨架档案;档案文件由该项目会话自己写(ho_profile)' },
|
|
41
|
+
{ when: '体系注册', say: '把本项目的体系注册写进提示段', what: '每个任务一份体系注册,写目录结构、规范文件与计划;全局总注册只列各任务体系文件路径;写入后热更新(ho_docmap action=write)' },
|
|
42
|
+
{ when: '行动计划', say: '为这个任务建立行动计划', what: '写任务根 PLAN.md;旧计划先归档且不再读取;压缩或换任后按「先交接、再计划、后执行」阅读(ho_docmap action=plan)' },
|
|
43
|
+
{ when: '暂任交出与归还', say: '把本任务交给另一个会话', what: '先刷新交接,再按任务库内 Delegation_Rules.md 把提示词写入 Delegation_Prompt.md;归还时刷新交接,并在交接中写明暂任者(ho_handover)' },
|
|
44
|
+
{ when: '查看会话', say: '列出当前会话', what: '水位、运行状态、活跃或沉默、角色与最后活动,并给出本轮通知名单(ho_sessions)' },
|
|
45
|
+
{ when: '打包迁移', say: '把 Demo 打包成迁移包', what: '收集文件、逐文件 sha256、7z(无 7-Zip 时用内置 tar.gz)、自检与绝对路径体检(ho_backup action=pack)' },
|
|
46
|
+
{ when: '在新机器恢复', say: '预演这次恢复', what: '校验、列出覆盖清单、备份后写入;默认仅预演(ho_backup action=restore)' },
|
|
47
|
+
{ when: '归档会话', say: '把这个会话归档', what: 'DSH 只提供归档;再次唤醒须先解归档(ho_sessions action=archive、unarchive)' },
|
|
48
|
+
{ when: '建立工作区', say: '把这个目录注册为工作区', what: 'init 在指定目录下建立整套全局文件体系;list、create、delete 管理注册,删除只删注册(ho_workspace)' },
|
|
49
|
+
{ when: '旧体系迁移', say: '把旧文档定位并入体系注册', what: '迁移模块读取旧 docmap,并入各任务体系注册,并把旧目录归档;旧目录不删除(ho_workspace action=migrate)' },
|
|
50
|
+
{ when: '自检', say: '检查插件当前状态', what: '服务可用性、压缩调度器与压缩计数、本会话角色与工具集、水位与迁移状态(ho_status view=diag)' },
|
|
51
|
+
{ when: '查在任', say: '现在 Demo1 由谁在任', what: '按全局、项目、杂项查询现任会话的名字与会话 id(ho_office);也可用命令行 node scripts/office.mjs 查询' },
|
|
52
|
+
{ when: '写作规范', say: '按写作规范检查这份文档', what: '执行 _Control_Center/standards/写作规范/README.md 的 W 条款;插件自带的语体门禁扫描 README、docs、templates 与 src' },
|
|
53
|
+
],
|
|
54
|
+
groups: [
|
|
55
|
+
{
|
|
56
|
+
title: '只读工具(不消耗任何会话 token)',
|
|
57
|
+
tools: [
|
|
58
|
+
['ho_status', '会话状态:view=phase 水位与建议(默认)、brief 起始简报、diag 插件自检、drift 交接漂移、tokens token 体检'],
|
|
59
|
+
['ho_gate', '通用约束条闸门(按库型判定)+交接文档体检+体系注册路径检查'],
|
|
60
|
+
['ho_sessions', '会话清点与通知名单;也用于设定角色、身份互转、建会话、出提示词、归档'],
|
|
61
|
+
['ho_charter', '按节返回规则正文;doc="runtime" 取完整插件手册'],
|
|
62
|
+
['ho_office', '在任登记表:查在任;action=register 用于会话首次注册自己的名字'],
|
|
63
|
+
['ho_docmap', '体系注册与文档定位:read、write、check;action=plan 读写计划并归档旧计划'],
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: '写工具(会改动文件;均带备份或预演)',
|
|
68
|
+
tools: [
|
|
69
|
+
['ho_handover', '读写、新建、追加、归档本会话自己的交接;整篇重写用 new 先归档旧交接再新建写入,写后按角色体检'],
|
|
70
|
+
['ho_checkpoint', '写交接、登记压缩;回合结束后压缩,并按 resume 决定是否续跑'],
|
|
71
|
+
['ho_changelog', '按固定表格追加一行台账'],
|
|
72
|
+
['ho_backup', 'action:snapshot 库快照、check 前置闸门、pack 打包、verify 校验、restore 恢复(默认预演)'],
|
|
73
|
+
['ho_workspace', 'init 首次建体系;scaffold 补齐;migrate 旧体系迁移;list、create、delete 工作区;move-* 迁移全局根目录'],
|
|
74
|
+
['ho_profile', '项目或杂项骨架:读取、注册、校验、取预设'],
|
|
75
|
+
['ho_succeed', '换任自服务:在任会话经当前用户授权后自行校验交接、建接任会话并登记在任'],
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: '平级会话工具',
|
|
80
|
+
tools: [
|
|
81
|
+
['peer', 'action:list 会话一览、status 单会话详情、send 投消息(wait=true 同轮等回复)、wait 等单个回复、collect 收集先完成的最终回复、peek 读近期会话历史、rename 改标题、new 新建'],
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
rules: [
|
|
86
|
+
'水位 ' + w.remind + ' 与 ' + w.remind2 + ' 各提醒一次(工具结果末尾追加);' + w.handover + ' 后可压缩,' + w.hard + ' 为硬上限;同一会话最多压缩 ' + w.max + ' 次,第 ' + (w.max + 1) + ' 次换任。',
|
|
87
|
+
'压缩不可逆:历史会被摘要替换,因此必须先写出合格的交接文档;体检未通过时,插件拒绝安排压缩。',
|
|
88
|
+
'压缩或换任后按「先读交接、再读计划、后执行」恢复;没有计划则跳过第二步。',
|
|
89
|
+
'会话之间可直接用 peer(action=send)通信,不必经全局转述;默认沉默仍适用:仅在到交接线、需当前用户决定、发现全局口径有误时发送一条。',
|
|
90
|
+
'当前用户授权(压缩或换任)写入后,目标会话的提示段会立刻多一行提示与到期时刻;授权过期或使用后自动消失,不留占位。',
|
|
91
|
+
'交接文件按角色分离:全局写 _Control_Center/records/Global_Handover.md,项目写 _Project_Meta/<项目>/Handover.md,杂项写 _Misc/tasks/<任务>/Handover.md,互不串用。',
|
|
92
|
+
'体系注册按任务独立:项目在 _Project_Meta/<项目>/System.md,杂项在 _Misc/tasks/<任务>/System.md,全局任务在 _Control_Center/tasks/<任务>/System.md;全局总注册 _Control_Center/records/systems.md 只列路径。',
|
|
93
|
+
'身份在项目与杂项之间互转时,先按归档规范打整库快照,再移动;用户也可选择直接建,沿用同一模板体系。',
|
|
94
|
+
'会话本身不迁移:跨机器、跨平台的连续性依托交接文档。',
|
|
95
|
+
'全局前置提示词属于用户内容,不是插件约束;关闭或留空时不占用注入点位。',
|
|
96
|
+
'插件本体为公开件,不含任何用户项目、会话或账号数据;用户数据与角色表都保存在本机。',
|
|
97
|
+
],
|
|
102
98
|
};
|
|
103
99
|
}
|
|
104
100
|
|
package/src/lib/handover-io.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
/** 交接文档读写:原子写 + 写前备份 + 写后体检(判据复用 lib/lint.mjs)。路径按会话角色解析。 */
|
|
1
|
+
/** 交接文档读写:原子写 + 写前备份 + 存档(整篇重写先归档)+ 写后体检(判据复用 lib/lint.mjs)。路径按会话角色解析。 */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
4
5
|
import { readUtf8 } from './text.mjs';
|
|
5
6
|
import { lintGlobal, lintProject } from './lint.mjs';
|
|
6
|
-
import { snapshotDirFor } from './role.mjs';
|
|
7
|
+
import { snapshotDirFor, archiveDirFor } from './role.mjs';
|
|
7
8
|
|
|
8
9
|
function stamp() {
|
|
9
10
|
const d = new Date();
|
|
@@ -11,6 +12,10 @@ function stamp() {
|
|
|
11
12
|
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + "-" + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
function sha256(file) {
|
|
16
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
/** 读交接:{ path, text };文件不存在返回 text: null。 */
|
|
15
20
|
export function readHandover(file) {
|
|
16
21
|
try { return { path: file, text: readUtf8(file) }; }
|
|
@@ -30,26 +35,51 @@ export function archiveStamp(d) {
|
|
|
30
35
|
/**
|
|
31
36
|
* 归档文件名:`<原文件名>_<YYYYMMDD_HHMMSS>.md`。
|
|
32
37
|
* 「原文件名」取源文件主名(不含扩展名):全局 `Global_Handover_…`、项目与杂项 `Handover_…`;
|
|
33
|
-
* 不加链标识——归档目录本身已按链隔离(规范 §
|
|
38
|
+
* 不加链标识——归档目录本身已按链隔离(规范 §5)。
|
|
34
39
|
*/
|
|
35
40
|
export function archiveFileName(file, stampText) {
|
|
36
41
|
const base = String(file || "").split(/[\\/]/).pop().replace(/\.md$/i, "");
|
|
37
42
|
return base + "_" + String(stampText) + ".md";
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 归档交接:复制到规范位置并校验 sha256;同名不覆盖,同一秒追加 `-2`、`-3`。
|
|
47
|
+
* @param {object} [opts] { info?: 角色信息, kind?: 'global'|'project'|'misc', archiveDir?: 强制指定目录 }
|
|
48
|
+
* @returns {{ dest, bytes, sha256 }}
|
|
49
|
+
*/
|
|
50
|
+
export function archiveHandover(ws, file, opts) {
|
|
51
|
+
const o = opts || {};
|
|
52
|
+
const info = o.info || { role: o.kind === 'project' ? 'project' : o.kind === 'misc' ? 'misc' : 'global' };
|
|
53
|
+
const dir = o.archiveDir || archiveDirFor(ws, info);
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
const stampText = archiveStamp(new Date());
|
|
56
|
+
let dest = path.join(dir, archiveFileName(file, stampText));
|
|
57
|
+
for (let n = 2; fs.existsSync(dest) && n < 100; n++) dest = path.join(dir, archiveFileName(file, stampText + "-" + n));
|
|
58
|
+
if (fs.existsSync(dest)) throw new Error("归档目标已存在,不覆盖:" + dest);
|
|
59
|
+
fs.copyFileSync(file, dest);
|
|
60
|
+
const a = sha256(file);
|
|
61
|
+
const b = sha256(dest);
|
|
62
|
+
if (a !== b) {
|
|
63
|
+
try { fs.rmSync(dest, { force: true }); } catch { /* 删不掉就留着,报错已说明路径 */ }
|
|
64
|
+
throw new Error("归档校验失败:sha256 不一致,未写入新交接:" + dest);
|
|
65
|
+
}
|
|
66
|
+
return { dest, bytes: fs.statSync(dest).size, sha256: b };
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
/**
|
|
41
70
|
* 写交接:先把旧文件复制到 <作用域>/Snapshots/pm-handover-<stamp>/,再原子替换,最后按角色体检。
|
|
42
|
-
*
|
|
71
|
+
* `opts.noBackup` 为 true 时跳过备份(整篇重写已先归档)。
|
|
72
|
+
* @param {object} [opts] { info?: 角色信息(决定体检判据与备份目录), kind?: 'global'|'project'|'misc', noBackup?: boolean }
|
|
43
73
|
* @returns {{ path, backup, verdict, reasons, bytes }}
|
|
44
74
|
*/
|
|
45
75
|
export function writeHandover(ws, file, text, mode, opts) {
|
|
46
76
|
const o = opts || {};
|
|
47
77
|
const info = o.info || { role: o.kind === 'project' ? 'project' : o.kind === 'misc' ? 'misc' : 'global' };
|
|
48
78
|
let dir = null;
|
|
49
|
-
try { dir = snapshotDirFor(ws, info); } catch { dir = path.join(ws, '_Temp', '_Global', 'Snapshots'); }
|
|
79
|
+
try { dir = o.snapshotDir || snapshotDirFor(ws, info); } catch { dir = path.join(ws, '_Temp', '_Global', 'Snapshots'); }
|
|
50
80
|
let backup = null;
|
|
51
81
|
try {
|
|
52
|
-
if (fs.existsSync(file)) {
|
|
82
|
+
if (!o.noBackup && fs.existsSync(file)) {
|
|
53
83
|
const bdir = path.join(dir, "pm-handover-" + stamp());
|
|
54
84
|
fs.mkdirSync(bdir, { recursive: true });
|
|
55
85
|
backup = path.join(bdir, path.basename(file));
|
|
@@ -67,3 +97,36 @@ export function writeHandover(ws, file, text, mode, opts) {
|
|
|
67
97
|
: lintGlobal(file);
|
|
68
98
|
return { path: file, backup, verdict: res.verdict, reasons: res.reasons, bytes: Buffer.byteLength(body, "utf8") };
|
|
69
99
|
}
|
|
100
|
+
|
|
101
|
+
function findSameArchive(dir, sha) {
|
|
102
|
+
try {
|
|
103
|
+
for (const name of fs.readdirSync(dir)) {
|
|
104
|
+
if (!/\.md$/i.test(name)) continue;
|
|
105
|
+
const p = path.join(dir, name);
|
|
106
|
+
try { if (sha256(p) === sha) return p; } catch { /* 读不到就跳过 */ }
|
|
107
|
+
}
|
|
108
|
+
} catch { /* 目录不存在时按未归档处理 */ }
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 整篇重写:先归档旧交接(文件存在时),再新建空白文件写入,最后体检。
|
|
114
|
+
* 固定流程的落点:读旧交接由调用方完成,归档与新建由本函数一次做完。
|
|
115
|
+
* 归档幂等:归档目录里已有内容相同的副本时复用,不重复复制。
|
|
116
|
+
* @returns 同 writeHandover,另加 archived: { dest, bytes, sha256, reused } | null
|
|
117
|
+
*/
|
|
118
|
+
export function rewriteHandover(ws, file, text, opts) {
|
|
119
|
+
const o = opts || {};
|
|
120
|
+
let archived = null;
|
|
121
|
+
if (fs.existsSync(file)) {
|
|
122
|
+
const info = o.info || { role: o.kind === 'project' ? 'project' : o.kind === 'misc' ? 'misc' : 'global' };
|
|
123
|
+
const dir = o.archiveDir || archiveDirFor(ws, info);
|
|
124
|
+
const sha = sha256(file);
|
|
125
|
+
const same = findSameArchive(dir, sha);
|
|
126
|
+
archived = same
|
|
127
|
+
? { dest: same, bytes: fs.statSync(same).size, sha256: sha, reused: true }
|
|
128
|
+
: Object.assign(archiveHandover(ws, file, o), { reused: false });
|
|
129
|
+
}
|
|
130
|
+
const res = writeHandover(ws, file, text, "write", Object.assign({}, o, { noBackup: true }));
|
|
131
|
+
return Object.assign({}, res, { archived });
|
|
132
|
+
}
|