@mingmaple/dsh-handover 0.9.22 → 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 +117 -113
- 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 +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- 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 +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- 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 +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- 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 +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- 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 +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- 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 -24
- 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 +19 -7
- 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 +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 迁移兼容模块(1.0):把旧版文档定位(docmap)并入体系注册。
|
|
3
|
+
*
|
|
4
|
+
* 本模块可整体删除:删除后不影响 1.0 的主实现;用户应先升级到 1.0.0 完成迁移,再升级后续版本。
|
|
5
|
+
* 迁移不删除旧文件:旧目录整体移入 _Archives/_Global/Backups/ 下并带时间标注。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { NL, readUtf8, isFile, isDir } from './text.mjs';
|
|
10
|
+
import { planRelPath } from './system.mjs';
|
|
11
|
+
import { parseDocmap } from './docmap.mjs';
|
|
12
|
+
|
|
13
|
+
export const LEGACY_DIR = path.join('_Control_Center', 'records', 'docmap');
|
|
14
|
+
|
|
15
|
+
function stamp() {
|
|
16
|
+
const d = new Date();
|
|
17
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
18
|
+
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 旧目录是否存在且含 .md 文件。 */
|
|
22
|
+
export function legacyInfo(ws) {
|
|
23
|
+
const dir = path.join(String(ws || ''), LEGACY_DIR);
|
|
24
|
+
let files = [];
|
|
25
|
+
try { files = fs.readdirSync(dir).filter((f) => f.endsWith('.md')); } catch { return { dir, exists: false, files: [] }; }
|
|
26
|
+
return { dir, exists: files.length > 0, files };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function taskInfoFromStem(stem) {
|
|
30
|
+
if (stem.indexOf('project-') === 0) return { role: 'project', project: stem.slice(8) };
|
|
31
|
+
if (stem.indexOf('misc-') === 0) return { role: 'misc', task: stem.slice(5) };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderSystem(info, rows, plan) {
|
|
36
|
+
const label = info.role === 'project' ? '项目「' + info.project + '」' : '杂项任务「' + info.task + '」';
|
|
37
|
+
const lines = ['# 体系注册(' + label + ')', '', '## 目录结构', '', '| 路径 | 用途 |', '|---|---|', '', '## 规范文件', '', '| 路径 | 用途 | 何时读 |', '|---|---|---|'];
|
|
38
|
+
for (const r of rows) lines.push('| `' + r.path + '` | ' + r.about + ' | ' + (r.when || '') + ' |');
|
|
39
|
+
lines.push('', '## 计划', '');
|
|
40
|
+
if (plan) lines.push('- `' + plan + '`:当前行动计划');
|
|
41
|
+
lines.push('', '本文件由 1.0 迁移从旧文档定位转换而来;后续由本任务在任会话用 ho_docmap 维护。');
|
|
42
|
+
return lines.join(NL) + NL;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeIfAbsent(file, body) {
|
|
46
|
+
if (isFile(file)) return false;
|
|
47
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
48
|
+
fs.writeFileSync(file, body, 'utf8');
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 执行迁移。返回 { ok, dir, converted, kept, archived, problems }。
|
|
54
|
+
* @param {string} ws 工作区根
|
|
55
|
+
*/
|
|
56
|
+
export function migrateLegacyDocmaps(ws) {
|
|
57
|
+
const root = String(ws || '');
|
|
58
|
+
const info = legacyInfo(root);
|
|
59
|
+
if (!info.exists) return { ok: true, dir: info.dir, converted: [], kept: [], archived: '', problems: [] };
|
|
60
|
+
const archiveRoot = path.join(root, '_Archives', '_Global', 'Backups', '1.0-docmap_' + stamp());
|
|
61
|
+
const problems = [];
|
|
62
|
+
const converted = [];
|
|
63
|
+
const kept = [];
|
|
64
|
+
for (const name of info.files) {
|
|
65
|
+
const file = path.join(info.dir, name);
|
|
66
|
+
let text = '';
|
|
67
|
+
try { text = readUtf8(file); } catch { problems.push('读取失败:' + name); continue; }
|
|
68
|
+
const stem = name.replace(/\.md$/, '');
|
|
69
|
+
const rows = parseDocmap(text);
|
|
70
|
+
if (stem === 'global') {
|
|
71
|
+
// 全局旧内容不是任务体系引用,保留为工作区文档清单,不注入提示段。
|
|
72
|
+
const dst = path.join(root, '_Control_Center', 'records', 'global-docs.md');
|
|
73
|
+
if (writeIfAbsent(dst, text)) kept.push('_Control_Center/records/global-docs.md');
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const t = taskInfoFromStem(stem);
|
|
77
|
+
if (!t) { problems.push('文件名不符合旧规范,未转换:' + name); continue; }
|
|
78
|
+
const dst = t.role === 'project'
|
|
79
|
+
? path.join(root, '_Project_Meta', t.project, 'System.md')
|
|
80
|
+
: path.join(root, '_Misc', 'tasks', t.task, 'System.md');
|
|
81
|
+
const plan = planRelPath(root, t);
|
|
82
|
+
if (writeIfAbsent(dst, renderSystem(t, rows, plan))) converted.push(path.relative(root, dst));
|
|
83
|
+
}
|
|
84
|
+
// 归档旧目录:整体移入备份位,不删除。
|
|
85
|
+
fs.mkdirSync(archiveRoot, { recursive: true });
|
|
86
|
+
const archived = path.join(archiveRoot, 'docmap');
|
|
87
|
+
fs.renameSync(info.dir, archived);
|
|
88
|
+
return { ok: problems.length === 0, dir: info.dir, converted, kept, archived, problems };
|
|
89
|
+
}
|
package/src/lib/lint.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const GLOBAL_SECTIONS = [
|
|
|
40
40
|
'2. 下一步',
|
|
41
41
|
'3. 待用户决定',
|
|
42
42
|
'4. 用户要求逐条落位',
|
|
43
|
-
'5.
|
|
43
|
+
'5. 风险与禁区',
|
|
44
44
|
'6. 关键路径',
|
|
45
45
|
'7. 会话 id 对照',
|
|
46
46
|
];
|
|
@@ -69,8 +69,11 @@ export function lintGlobal(file) {
|
|
|
69
69
|
try { text = readUtf8(file); } catch (e) { return { verdict: "读不到", reasons: ["读取失败:" + e.message] }; }
|
|
70
70
|
const lines = text.split(String.fromCharCode(10));
|
|
71
71
|
const bad = [];
|
|
72
|
-
// §3
|
|
73
|
-
const SEC_ALIAS = {
|
|
72
|
+
// §3 标题有新旧两种写法;§5 旧标题作兼容匹配(旧字按码点拼接,避免语体扫描误判)。
|
|
73
|
+
const SEC_ALIAS = {
|
|
74
|
+
'3. 待用户决定': ['3. 等用户' + String.fromCharCode(25293, 26495)],
|
|
75
|
+
'5. 风险与禁区': ['5. ' + String.fromCharCode(22353) + '点与禁区'],
|
|
76
|
+
};
|
|
74
77
|
for (const sec of GLOBAL_SECTIONS) {
|
|
75
78
|
const alt = SEC_ALIAS[sec] || [];
|
|
76
79
|
if (!hasHeading(lines, sec) && !alt.some((a) => hasHeading(lines, a))) bad.push("缺一节:## " + sec);
|
|
@@ -85,16 +88,16 @@ export function lintGlobal(file) {
|
|
|
85
88
|
if (!chainLine) bad.push('§0 缺「链内代数」行');
|
|
86
89
|
else {
|
|
87
90
|
const hasDi = chainLine.indexOf('第') !== -1 && chainLine.indexOf('代') !== -1;
|
|
88
|
-
if (!hasDi) bad.push('
|
|
91
|
+
if (!hasDi) bad.push('链内代数不符合「第 N 代」格式:' + chainLine.slice(0, 60));
|
|
89
92
|
}
|
|
90
93
|
const s2 = sectionLines(text, '下一步');
|
|
91
94
|
if (!s2) bad.push('缺 ## 2. 下一步');
|
|
92
|
-
else if (tableRows(s2).length <= 1 && bulletRows(s2).length === 0) bad.push('§2
|
|
95
|
+
else if (tableRows(s2).length <= 1 && bulletRows(s2).length === 0) bad.push('§2 未列出任何条目');
|
|
93
96
|
const s7 = sectionLines(text, '会话 id 对照');
|
|
94
97
|
if (!s7) bad.push('缺 ## 7. 会话 id 对照');
|
|
95
98
|
else {
|
|
96
|
-
if (!s7.some((l) => l.trim().startsWith('|') && l.indexOf('session-') !== -1)) bad.push('§7
|
|
97
|
-
if (s7.some((l) => l.indexOf('待回填') !== -1)) bad.push('§7
|
|
99
|
+
if (!s7.some((l) => l.trim().startsWith('|') && l.indexOf('session-') !== -1)) bad.push('§7 无任何带 session id 的行');
|
|
100
|
+
if (s7.some((l) => l.indexOf('待回填') !== -1)) bad.push('§7 仍留有「待回填」');
|
|
98
101
|
}
|
|
99
102
|
const ph = firstPlaceholder(text);
|
|
100
103
|
if (ph) bad.push("残留模板占位符:" + ph);
|
|
@@ -109,15 +112,15 @@ export function lintProject(file) {
|
|
|
109
112
|
const bad = [];
|
|
110
113
|
const gap = [];
|
|
111
114
|
const body = sectionLines(text, '正在做的事');
|
|
112
|
-
if (!body) gap.push("缺 ## 1. 正在做的事
|
|
115
|
+
if (!body) gap.push("缺 ## 1. 正在做的事 一节");
|
|
113
116
|
else {
|
|
114
117
|
const core = body.filter((l) => l.trim() !== '' && !l.trim().startsWith('>'));
|
|
115
118
|
// 只判「有没有实质内容」;不设字数下限(2026-11-11 口径:交接类文档不设体积上限)。
|
|
116
|
-
if (core.length === 0) bad.push("正在做的事
|
|
119
|
+
if (core.length === 0) bad.push("正在做的事 为空节(或仅有引用块说明),无实质内容");
|
|
117
120
|
}
|
|
118
121
|
const nxt = sectionLines(text, '下一步') || sectionLines(text, '待办');
|
|
119
122
|
if (!nxt) bad.push("缺「下一步待办」一节");
|
|
120
|
-
else if (tableRows(nxt).length <= 1 && bulletRows(nxt).length === 0) bad.push("
|
|
123
|
+
else if (tableRows(nxt).length <= 1 && bulletRows(nxt).length === 0) bad.push("「下一步待办」未列出任何条目");
|
|
121
124
|
const ph = firstPlaceholder(text);
|
|
122
125
|
if (ph) bad.push("残留模板占位符:" + ph);
|
|
123
126
|
if (bad.length) return { verdict: "不合格", reasons: bad };
|
|
@@ -148,6 +151,6 @@ export function renderLintProject(name, rows) {
|
|
|
148
151
|
if (it.res.verdict === "不合格") fails++;
|
|
149
152
|
else if (it.res.verdict === "缺节") gaps++;
|
|
150
153
|
}
|
|
151
|
-
out.push("判定:" + list.length + " 个项目 —— 不合格 " + fails + "
|
|
154
|
+
out.push("判定:" + list.length + " 个项目 —— 不合格 " + fails + " 个(须修正)、缺节 " + gaps + " 个(新规范尚未铺开,属正常)");
|
|
152
155
|
return out;
|
|
153
156
|
}
|
package/src/lib/manual.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* 逐角色的系统提示段与一次性引导(1.0)。
|
|
3
|
+
* 职责边界:本文件只写会话定位、写入范围、任务流程、水位与压缩、交接与体系注册、暂任与协作。
|
|
4
|
+
* 其他行为约束由用户通过「全局前置提示词」注入。
|
|
5
|
+
* 所有角色共用同一份任务流程文本,避免同一规则多处维护。
|
|
6
6
|
*/
|
|
7
7
|
import { handoverPathFor, scopeLabel } from './role.mjs';
|
|
8
8
|
import { handoverRuleText, watermark } from './facts.mjs';
|
|
@@ -10,61 +10,100 @@ import { consentPromptLines } from './consent.mjs';
|
|
|
10
10
|
|
|
11
11
|
const NL = String.fromCharCode(10);
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/** 插件完整手册的指针(各档角色共用)。 */
|
|
14
14
|
const RUNTIME_POINTER = '细节按节取:ho_charter(doc="runtime", section=…)。';
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
* 规范按名字被引用,本库(会话所在的工作区)与全局规范库都在范围内;细节写在规范库的 README 里。
|
|
19
|
-
*/
|
|
20
|
-
const STANDARDS_POINTER = '通用规范在 _Control_Center/standards/(每条一个子目录);当前用户提到「按某某规范进行」时,本库(本会话所在的工作区)与该目录都算查找范围。';
|
|
16
|
+
/** 通用规范的位置与查找范围(各档角色共用,必须同一句)。 */
|
|
17
|
+
const STANDARDS_POINTER = '通用规范在 _Control_Center/standards/(每条一个子目录)。当前用户提到「按某某规范进行」时,查找范围包括本库(本会话所在的工作区)与该目录。';
|
|
21
18
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* 后一句按角色分叉:全局直接写,项目与杂项报全局。
|
|
25
|
-
*/
|
|
26
|
-
const STANDARDS_WRITE_PREFIX = '当前用户提到「把某某写成规范」时:未指明范围就写本库;';
|
|
19
|
+
/** 写入范围:未指明写本库;明确写通用规范才写规范库。 */
|
|
20
|
+
const STANDARDS_WRITE_PREFIX = '当前用户提到「把某某写成规范」时:未指明范围则写本库;';
|
|
27
21
|
const STANDARDS_WRITE_TARGET = '说「写入通用规范库」「写成通用规范」「写成共用规范」的';
|
|
28
22
|
const STANDARDS_WRITE_GLOBAL = STANDARDS_WRITE_PREFIX + STANDARDS_WRITE_TARGET + ',写 _Control_Center/standards/。';
|
|
29
|
-
const STANDARDS_WRITE_SCOPED = STANDARDS_WRITE_PREFIX + STANDARDS_WRITE_TARGET + ',不得写 _Control_Center
|
|
23
|
+
const STANDARDS_WRITE_SCOPED = STANDARDS_WRITE_PREFIX + STANDARDS_WRITE_TARGET + ',不得写 _Control_Center,只报全局。';
|
|
30
24
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
25
|
+
/** 写作规范指针:只有一行,不注入正文。 */
|
|
26
|
+
const WRITING_POINTER = '写作规范见 _Control_Center/standards/写作规范/README.md(W-01 至 W-42);全部产出按它执行。';
|
|
27
|
+
|
|
28
|
+
/** 暂任与协作的落点:规范与提示词随任务库独立。 */
|
|
29
|
+
const DELEGATION_POINTER = '暂任与协作:规范在任务库 rules/Delegation_Rules.md,协作模板在 templates/Prompt_Templates.md。';
|
|
30
|
+
|
|
31
|
+
/** 协作收结果的口径:被派方不发消息,协调方按完成顺序收集最终回复。 */
|
|
32
|
+
const COLLAB_LINE = '协作收结果:进入协作前先以 peer(action="collab", sessions=[…]) 注册。协作期间被交办方不得向交办者或同组成员发送明文消息,工具端会拒绝,DSH 自带的 send_message 亦由插件守卫拦截;普通对话无需注册。协调方以 peer(action="collect", sessions=[…], timeout_ms=…) 取先完成者的最终回复;timeout_ms 按任务预估设置,最低 120000。collect 取回全部结果后,限制自动解除。';
|
|
33
|
+
|
|
34
|
+
/** 写交接的固定流程(各档角色共用):只写这一行,细则在规范与模板里。 */
|
|
35
|
+
const HANDOVER_FLOW = '写交接按固定流程:先读上一份交接,再归档上一份,然后读模板 _Control_Center/templates/Handover.md,最后以 ho_handover(action="new") 新建空白写入;不得在旧文件上续写。';
|
|
36
|
+
|
|
37
|
+
/** 复用准则:全局、项目、杂项各处同一句。 */
|
|
35
38
|
function reuseRule(cfg) {
|
|
36
|
-
return '指派任务或新建会话前,先用 ho_office
|
|
37
|
-
+ watermark(cfg).
|
|
39
|
+
return '指派任务或新建会话前,先用 ho_office(空参数)查询一次在任。同一类别已有在任会话且其水位低于 '
|
|
40
|
+
+ watermark(cfg).handover + ' 时,直接用 peer(action="send")复用,不新建;确需另建(原会话不可用、需并行、当前用户明确要求)时才新建。';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 两级任务流程全文(各档角色共用)。 */
|
|
44
|
+
function workflowLines(cfg) {
|
|
45
|
+
return [
|
|
46
|
+
'三、任务流程',
|
|
47
|
+
'- 层级一(轻量):单文件修改、局部命名、语法修复、格式化、内容读取,直接执行,跳过计划与查证。',
|
|
48
|
+
'- 当前用户说「讨论一下」,或已就流程向当前用户确认过时,执行前须再次确认;当前用户明确「直接做」的除外。',
|
|
49
|
+
'- 层级二(繁重):新增功能、多文件联动、重构、架构或接口变更,按下述顺序执行。',
|
|
50
|
+
'- 先判断纯窗口是否可能超过 40%(不计当前占用)。超过时,写计划就把拆子智能体安排进去,本会话只做统筹与收尾;不超过时,计划按本会话自行完成来写。',
|
|
51
|
+
'- 写行动计划 PLAN.md(任务根),随后回看上下文核对;发现缺口时给出方案询问当前用户,优劣不明时并列全部方案。',
|
|
52
|
+
'- 子智能体(细则见 Charter §十一 与运行时手册 §12.4):仅在单块 ≥15 步或需读 ≥10 个文件、确需隔离、且结果可机械验收时派出,并发 ≤3;单个子智能体自身亦需十余步,结论还需回读。能先写成脚本再抽检的,先写脚本。同一批文件的下一道工序交回原作者续用,不另开。提示词由本会话派发时,放入规范文件地址与体系说明;插件不向其上下文注入内容。',
|
|
53
|
+
'- 当前用户确认后执行。余量不足时先刷新交接,再请求压缩或换任;恢复后按「先读交接、再读计划、后执行」。',
|
|
54
|
+
'- 每轮只做与当前任务直接相关的事;未经要求的检查不设立,工具正常返回即予采信。',
|
|
55
|
+
];
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
/**
|
|
58
|
+
/** 上下文与压缩段落(各档角色共用)。 */
|
|
59
|
+
function rhythmLines(cfg) {
|
|
60
|
+
const w = watermark(cfg);
|
|
61
|
+
return [
|
|
62
|
+
'四、上下文与压缩',
|
|
63
|
+
'- 水位跨 ' + w.remind + ' 与 ' + w.remind2 + ' 时,由插件在本会话的工具结果末尾各追加一次提醒;不修改系统提示段、不生成用户消息、不独立成轮。',
|
|
64
|
+
'- 判断标准:' + w.remind + ' 档为完成剩余流程后不超过 ' + Math.round((cfg && cfg.margin1 !== undefined ? cfg.margin1 : 0.45) * 100) + '%;'
|
|
65
|
+
+ w.remind2 + ' 档为不超过 ' + Math.round((cfg && cfg.margin2 !== undefined ? cfg.margin2 : 0.55) * 100) + '%。',
|
|
66
|
+
'- ' + handoverRuleText(cfg) + '当前用户可随时要求换任。',
|
|
67
|
+
'- 压缩前先刷新交接;压缩后先读交接、再读计划、后执行。',
|
|
68
|
+
'- 压缩须由当前用户点击「授权压缩」,随后写交接并调用 ho_checkpoint(confirm=true);任务未完成传 resume=continue,已收尾传 resume=wait(不再唤醒)。',
|
|
69
|
+
'- 关键步骤传 at_risk=true 暂缓;85% 硬上限仍然生效。',
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 全局会话的常驻段:身份、权限、流程、节奏、交接与体系、按需取用。 */
|
|
41
74
|
export function globalManual(opts) {
|
|
42
75
|
const o = opts || {};
|
|
43
76
|
const lines = [
|
|
44
77
|
'dsh-handover 运行期(全局)',
|
|
45
78
|
'',
|
|
46
79
|
'一、身份',
|
|
47
|
-
'
|
|
80
|
+
'本会话为全局管理会话,协调会话、维护登记与交接。',
|
|
48
81
|
'',
|
|
49
82
|
'二、权限',
|
|
50
|
-
'可写 _Control_Center、_Project_Meta 与项目结构;Projects
|
|
51
|
-
'新建会话与设定角色仅属全局;会话间可直接 peer(action="send"
|
|
83
|
+
'可写 _Control_Center、_Project_Meta 与项目结构;Projects 及各项目库内文件只读,不得代写;项目内作业不由全局代办。',
|
|
84
|
+
'新建会话与设定角色仅属全局;会话间可直接 peer(action="send")。换任须当前用户授权(/allow-succeed 或设置页)后,由在任会话调用 ho_succeed。',
|
|
52
85
|
reuseRule(o.cfg),
|
|
53
86
|
'',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
...workflowLines(o.cfg),
|
|
88
|
+
'',
|
|
89
|
+
...rhythmLines(o.cfg),
|
|
57
90
|
'',
|
|
58
|
-
'
|
|
91
|
+
'五、交接与体系',
|
|
59
92
|
'交接:_Control_Center/records/Global_Handover.md。',
|
|
93
|
+
HANDOVER_FLOW,
|
|
94
|
+
'体系注册:_Control_Center/records/systems.md 只列各任务体系文件的路径;本段只注入这些引用,不注入各任务正文。',
|
|
95
|
+
'全局任务库:_Control_Center/tasks/<任务名>/;每个任务有独立交接与计划,任务可一键独立为项目或杂项会话。',
|
|
96
|
+
COLLAB_LINE,
|
|
97
|
+
WRITING_POINTER,
|
|
98
|
+
DELEGATION_POINTER,
|
|
60
99
|
'',
|
|
61
|
-
'
|
|
100
|
+
'六、按需取用',
|
|
62
101
|
RUNTIME_POINTER,
|
|
63
102
|
STANDARDS_POINTER,
|
|
64
103
|
STANDARDS_WRITE_GLOBAL,
|
|
65
104
|
];
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
105
|
+
const sys = o.system || '';
|
|
106
|
+
if (sys) lines.push('', sys);
|
|
68
107
|
const cs = consentBlock(o);
|
|
69
108
|
if (cs.length) lines.push('', ...cs);
|
|
70
109
|
const mv = moveLine(o);
|
|
@@ -72,13 +111,15 @@ export function globalManual(opts) {
|
|
|
72
111
|
return lines.join(NL);
|
|
73
112
|
}
|
|
74
113
|
|
|
75
|
-
/**
|
|
114
|
+
/** 项目与杂项会话的提示段:写入范围、交接、流程、体系注册内容、规范指针。 */
|
|
76
115
|
export function scopedManual(info, opts) {
|
|
77
116
|
const o = opts || {};
|
|
78
117
|
const hp = handoverPathFor(o.ws, info);
|
|
79
118
|
const label = scopeLabel(info);
|
|
80
119
|
const lines = [
|
|
81
120
|
'dsh-handover 运行期(' + label + ')',
|
|
121
|
+
'',
|
|
122
|
+
'一、身份与边界',
|
|
82
123
|
info.role === 'project'
|
|
83
124
|
? '只可写本库与元数据目录,不得写 _Control_Center 及其他项目。'
|
|
84
125
|
: '只可写本任务目录与元数据目录,不得写 _Control_Center、Projects 及其他任务。',
|
|
@@ -86,23 +127,34 @@ export function scopedManual(info, opts) {
|
|
|
86
127
|
? '库根:Projects/' + info.project + '/;元数据:_Project_Meta/' + info.project + '/。'
|
|
87
128
|
: '任务目录:_Misc/tasks/' + info.task + '/。',
|
|
88
129
|
info.role === 'project'
|
|
89
|
-
? '骨架:_Project_Meta/' + info.project + '/project.profile.json
|
|
90
|
-
: '骨架:_Misc/tasks/' + info.task + '/task.profile.json
|
|
130
|
+
? '骨架:_Project_Meta/' + info.project + '/project.profile.json;未注册时先通读本库并调用 ho_profile(action=register)。'
|
|
131
|
+
: '骨架:_Misc/tasks/' + info.task + '/task.profile.json;未注册时先通读本任务并调用 ho_profile(action=register)。',
|
|
91
132
|
info.role === 'project'
|
|
92
133
|
? '临时与归档:_Temp/' + info.project + '/{Snapshots,Scratch}、_Archives/' + info.project + '/。'
|
|
93
134
|
: '临时与归档:_Temp/_Misc/' + info.task + '/Snapshots/、_Archives/_Misc/' + info.task + '/。',
|
|
94
135
|
hp.path
|
|
95
136
|
? '交接:' + hp.path + '(ho_handover 默认)。'
|
|
96
|
-
: '
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
|
|
137
|
+
: '交接路径未解析(缺角色或名称),请先在设置页设定角色。',
|
|
138
|
+
HANDOVER_FLOW,
|
|
139
|
+
'名称已注册,不得自行修改;需要改名由全局处理。',
|
|
140
|
+
'',
|
|
141
|
+
...workflowLines(o.cfg),
|
|
142
|
+
'',
|
|
143
|
+
...rhythmLines(o.cfg),
|
|
144
|
+
'',
|
|
145
|
+
'五、交接与体系',
|
|
146
|
+
'会话间可直接 peer(action="send")。默认沉默:仅在水位达到 ' + watermark(o.cfg).remind + ' 且已写交接、需当前用户决定、全局口径有误 3 种情形向全局报告;任务完成不报全局。',
|
|
147
|
+
WRITING_POINTER,
|
|
148
|
+
DELEGATION_POINTER,
|
|
149
|
+
COLLAB_LINE,
|
|
150
|
+
'',
|
|
151
|
+
'六、按需取用',
|
|
100
152
|
RUNTIME_POINTER,
|
|
101
153
|
STANDARDS_POINTER,
|
|
102
154
|
STANDARDS_WRITE_SCOPED,
|
|
103
155
|
];
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
156
|
+
const sys = o.system || '';
|
|
157
|
+
if (sys) lines.push('', sys);
|
|
106
158
|
const cs = consentBlock(o);
|
|
107
159
|
if (cs.length) lines.push('', ...cs);
|
|
108
160
|
const mv = moveLine(o);
|
|
@@ -117,12 +169,6 @@ function moveLine(opts) {
|
|
|
117
169
|
return '已迁移至 ' + (mv.dst || '?') + ';' + (mv.src || '?') + ' 现为链接,勿再手工迁移。';
|
|
118
170
|
}
|
|
119
171
|
|
|
120
|
-
/** 文档定位(docmap)注入:空 map 不占位。 */
|
|
121
|
-
function docmapBlock(opts) {
|
|
122
|
-
const m = opts && opts.docmap;
|
|
123
|
-
return m ? [m] : [];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
172
|
/** 用户已授权的提示行:每种有效授权一行,无授权不占位。 */
|
|
127
173
|
function consentBlock(opts) {
|
|
128
174
|
try { return consentPromptLines(opts && opts.consents); } catch { return []; }
|
|
@@ -135,40 +181,31 @@ export function manualFor(info, opts) {
|
|
|
135
181
|
return '';
|
|
136
182
|
}
|
|
137
183
|
|
|
138
|
-
/**
|
|
139
|
-
|
|
184
|
+
/**
|
|
185
|
+
* 设定角色后投递的一次性首轮消息:讨论体系、注册名称;互转时先说明迁移结果。
|
|
186
|
+
* 由 applyRole 在写角色表之后投递,不改变角色本身。
|
|
187
|
+
*/
|
|
188
|
+
export function firstRoundMessageFor(info, opts) {
|
|
140
189
|
const o = opts || {};
|
|
141
190
|
const role = info && info.role ? info.role : 'plain';
|
|
142
191
|
if (role === 'plain') return '';
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
'
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
'
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return [
|
|
158
|
-
head,
|
|
159
|
-
'先通读本任务目录,确定本任务的骨架:工作文件、规则文件、交接、快照与归档位置。',
|
|
160
|
-
'把结论写入 _Misc/tasks/' + t + '/task.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
161
|
-
'写入后调用 ho_profile(action=register) 校验并登记。',
|
|
162
|
-
'约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects 与其他任务不得写入。工具:ho_backup、ho_handover、ho_status、ho_checkpoint、ho_profile、peer(action="send")。',
|
|
163
|
-
'完成后以一句话说明改动即可。',
|
|
164
|
-
].join(NL);
|
|
192
|
+
const label = scopeLabel(info);
|
|
193
|
+
const lines = [];
|
|
194
|
+
lines.push('【dsh-handover 角色设定】本会话已设为「' + label + '」。');
|
|
195
|
+
if (o.previousRole === 'project' || o.previousRole === 'misc') {
|
|
196
|
+
if (o.previousRole !== role) {
|
|
197
|
+
lines.push(o.migrate === 'direct'
|
|
198
|
+
? '已按「直接建」处理:旧文件未搬动,请按体系模板建立本任务体系。'
|
|
199
|
+
: '已按标准流程完成迁移:' + (o.moveNote || '旧库已快照后移动,位置见交接。'));
|
|
200
|
+
lines.push('若当前用户希望改用另一种方式,请说明差异,不得自行再次迁移。');
|
|
201
|
+
} else {
|
|
202
|
+
lines.push('角色未变:沿用既有体系与注册名。');
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
lines.push('首轮固定事项:一、了解本会话既有产出与任务;二、读体系模板并初拟体系;三、与当前用户确认一种方式:由本会话直接建成并注册,或先讨论定稿再建成并注册。');
|
|
165
206
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
'- Projects/、_Misc/tasks/、_Archives/、_Temp/ — 只维护结构,不代写项目库内文件。',
|
|
171
|
-
'约束:项目库内文件由该项目会话自行整理;骨架由项目自己注册,缺则派发,不代写。',
|
|
172
|
-
'完成后以一句话汇报即可。',
|
|
173
|
-
].join(NL);
|
|
207
|
+
lines.push('名称:' + (o.name ? '本会话注册名为「' + o.name + '」,不得自行修改。' : '本会话尚无注册名。与当前用户确认名称后,用 ho_office(action="register", name="…") 注册;注册后不得自行修改,改名须找全局。'));
|
|
208
|
+
lines.push('体系注册:用 ho_docmap(action="write", entries=[…]) 写入目录结构、规范文件与计划;模板见任务库 rules 与 templates 目录。');
|
|
209
|
+
lines.push('约束:只改动本会话所属范围;不每轮回执,不发送确认类消息。');
|
|
210
|
+
return lines.join(NL);
|
|
174
211
|
}
|
package/src/lib/mover.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import os from 'node:os';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import crypto from 'node:crypto';
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
|
+
import { pathKey } from './pathkey.mjs';
|
|
19
20
|
|
|
20
21
|
const MARKER_NAME = 'dsh-handover.move.json';
|
|
21
22
|
const HELPER_NAME = 'dsh-handover.move.mjs';
|
|
@@ -25,7 +26,10 @@ const STAMP_REL = path.join('_Temp', '_Global', 'Scratch', '.dsh-handover-verifi
|
|
|
25
26
|
|
|
26
27
|
function msgOf(e) { return String(e && e.message ? e.message : e); }
|
|
27
28
|
function tail(p) { return String(p || '').replace(/[\\/]+$/, ''); }
|
|
28
|
-
function canon(p) {
|
|
29
|
+
function canon(p) {
|
|
30
|
+
const t = tail(path.resolve(String(p || '')));
|
|
31
|
+
return process.platform === 'win32' ? t.toLowerCase() : t;
|
|
32
|
+
}
|
|
29
33
|
function samePath(a, b) { return !!a && !!b && canon(a) === canon(b); }
|
|
30
34
|
function isInside(parent, child) {
|
|
31
35
|
const p = canon(parent) + path.sep;
|
|
@@ -168,15 +172,15 @@ export async function probeMove({ src, to }) {
|
|
|
168
172
|
if (asLink) {
|
|
169
173
|
try { copyFrom = tail(fs.realpathSync(s)); } catch { copyFrom = tail(asLink); }
|
|
170
174
|
}
|
|
171
|
-
if (!fs.existsSync(path.join(copyFrom, '_Control_Center'))) warnings.push('
|
|
175
|
+
if (!fs.existsSync(path.join(copyFrom, '_Control_Center'))) warnings.push('源目录中无 _Control_Center:请确认该目录是否为全局根。');
|
|
172
176
|
|
|
173
177
|
const raw = String(to || '').trim();
|
|
174
178
|
if (!raw) throw new Error('缺少目标路径(to)');
|
|
175
|
-
if (!path.isAbsolute(raw)) problems.push('
|
|
179
|
+
if (!path.isAbsolute(raw)) problems.push('目标须为绝对路径:' + raw);
|
|
176
180
|
// 语义:to=目标**父目录**(把整个根目录搬进去);basename 与当前根同名则直接当新根。
|
|
177
181
|
const toRaw = tail(path.resolve(raw));
|
|
178
182
|
const rootName = path.basename(s);
|
|
179
|
-
const t = path.basename(toRaw)
|
|
183
|
+
const t = pathKey(path.basename(toRaw)) === pathKey(rootName) ? toRaw : path.join(toRaw, rootName);
|
|
180
184
|
if (samePath(t, s)) problems.push('目标与当前根相同');
|
|
181
185
|
else if (isInside(s, t)) problems.push('目标在当前根内部:' + t);
|
|
182
186
|
else if (isInside(t, s)) problems.push('目标是当前根的上级目录:' + t);
|
|
@@ -197,7 +201,7 @@ export async function probeMove({ src, to }) {
|
|
|
197
201
|
if (freeBytes !== null && freeBytes < srcSize.bytes * 1.15) {
|
|
198
202
|
problems.push('目标盘空间不足:可用 ' + fmtBytes(freeBytes) + ',需要约 ' + fmtBytes(srcSize.bytes * 1.15));
|
|
199
203
|
}
|
|
200
|
-
if (!freeBytes && freeBytes !== 0) warnings.push('
|
|
204
|
+
if (!freeBytes && freeBytes !== 0) warnings.push('无法取得目标盘可用空间(继续前请自行确认)');
|
|
201
205
|
|
|
202
206
|
return {
|
|
203
207
|
ok: problems.length === 0,
|
|
@@ -275,7 +279,7 @@ function alive(pid) {
|
|
|
275
279
|
if (!pid) return false;
|
|
276
280
|
try { process.kill(pid, 0); return true; } catch (e) { return e && e.code === 'EPERM'; }
|
|
277
281
|
}
|
|
278
|
-
function stamp(text) {
|
|
282
|
+
function stamp(text) { const t = String(text || '').replace(/[\\/]+$/, ''); return process.platform === 'win32' ? t.toLowerCase() : t; }
|
|
279
283
|
function stop(m) { log('[中止] ' + m); process.exit(1); }
|
|
280
284
|
|
|
281
285
|
(async () => {
|
|
@@ -295,7 +299,7 @@ function stop(m) { log('[中止] ' + m); process.exit(1); }
|
|
|
295
299
|
if (!fs.existsSync(dst)) stop('目标不存在:' + dst);
|
|
296
300
|
if (!fs.existsSync(src)) stop('源不存在:' + src);
|
|
297
301
|
const stampFile = path.join(dst, '_Temp', '_Global', 'Scratch', '.dsh-handover-verified.json');
|
|
298
|
-
if (!fs.existsSync(stampFile)) stop('
|
|
302
|
+
if (!fs.existsSync(stampFile)) stop('目标中无校验印章,可能并非本次复制所得');
|
|
299
303
|
let sp2 = null;
|
|
300
304
|
try { sp2 = JSON.parse(fs.readFileSync(stampFile, 'utf8')); } catch { stop('校验印章读不出来'); }
|
|
301
305
|
if (String(sp2.digest) !== String(m.digest)) stop('校验印章与标记不一致,目标可能被改动过');
|
|
@@ -313,7 +317,7 @@ function stop(m) { log('[中止] ' + m); process.exit(1); }
|
|
|
313
317
|
let target = '';
|
|
314
318
|
try { target = fs.readlinkSync(src); } catch { target = ''; }
|
|
315
319
|
if (!target || stamp(target) !== stamp(dst)) {
|
|
316
|
-
log('
|
|
320
|
+
log('链接指向不符:' + target + '(旧备份在 ' + old + ')');
|
|
317
321
|
stop('切换校验失败');
|
|
318
322
|
}
|
|
319
323
|
m.phase = 'switched';
|
|
@@ -352,7 +356,7 @@ function stop(m) { log('[中止] ' + m); process.exit(1); }
|
|
|
352
356
|
/** 派后台助手:等本进程(DSH)退出后自动切换。 */
|
|
353
357
|
export function stageSwitch({ relaunch, waitPid } = {}) {
|
|
354
358
|
const m = readMarker();
|
|
355
|
-
if (!m || !m.src || !m.dst) throw new Error('
|
|
359
|
+
if (!m || !m.src || !m.dst) throw new Error('无可用迁移标记,请先执行一次 move');
|
|
356
360
|
const st = moveStatus({ src: m.src });
|
|
357
361
|
if (st.phase === 'switched' || st.phase === 'switched+cleaned') return { ok: true, already: true, phase: st.phase, log: switchLogPath() };
|
|
358
362
|
if (st.phase !== 'verified') throw new Error('当前不是「已复制并校验通过」(phase=' + st.phase + '),不能切换');
|
|
@@ -381,7 +385,7 @@ export function stageSwitch({ relaunch, waitPid } = {}) {
|
|
|
381
385
|
child = spawn(process.execPath, args, { detached: true, stdio: 'ignore', windowsHide: true, env });
|
|
382
386
|
child.unref();
|
|
383
387
|
} catch (e) {
|
|
384
|
-
return { ok: false, spawned: false, error: '
|
|
388
|
+
return { ok: false, spawned: false, error: '派出助手失败:' + msgOf(e), helper: sp.helper, cmd: sp.cmd, sh: sp.sh, log: sp.log };
|
|
385
389
|
}
|
|
386
390
|
return { ok: true, spawned: true, pid: child.pid, waitPid: pid, helper: sp.helper, cmd: sp.cmd, sh: sp.sh, log: sp.log, relaunch: relaunch === true };
|
|
387
391
|
}
|
|
@@ -394,7 +398,7 @@ export function stageSwitch({ relaunch, waitPid } = {}) {
|
|
|
394
398
|
*/
|
|
395
399
|
export function repairLink() {
|
|
396
400
|
const m = readMarker();
|
|
397
|
-
if (!m || !m.src || !m.dst) throw new Error('
|
|
401
|
+
if (!m || !m.src || !m.dst) throw new Error('无迁移记录,无法重建链接');
|
|
398
402
|
if (!fs.existsSync(m.dst)) throw new Error('目标不存在(' + m.dst + '),先确认数据在哪里,本工具不动文件');
|
|
399
403
|
const kind = linkKind();
|
|
400
404
|
let st = null;
|
|
@@ -531,7 +535,7 @@ export function stageRetarget() {
|
|
|
531
535
|
child = spawn(process.execPath, [sp.helper, String(process.pid)], { detached: true, stdio: 'ignore', windowsHide: true, env });
|
|
532
536
|
child.unref();
|
|
533
537
|
} catch (e) {
|
|
534
|
-
return { ok: false, spawned: false, error: '
|
|
538
|
+
return { ok: false, spawned: false, error: '派出助手失败:' + msgOf(e), plan, script: sp };
|
|
535
539
|
}
|
|
536
540
|
return { ok: true, spawned: true, pid: child.pid, waitPid: process.pid, plan, script: sp };
|
|
537
541
|
}
|
|
@@ -639,7 +643,7 @@ export async function cleanupOld({ src, confirm } = {}) {
|
|
|
639
643
|
const st = moveStatus({ src });
|
|
640
644
|
if (st.phase !== 'switched') throw new Error('当前不是已切换状态(phase=' + st.phase + '),拒绝删除任何目录');
|
|
641
645
|
if (confirm !== true) throw new Error('删除旧备份需要明确确认(confirm=true)');
|
|
642
|
-
if (!st.olds.length) return { ok: true, removed: [], bytes: 0, note: '
|
|
646
|
+
if (!st.olds.length) return { ok: true, removed: [], bytes: 0, note: '未找到旧备份目录' };
|
|
643
647
|
const removed = []; const failed = []; let bytes = 0;
|
|
644
648
|
const base = path.basename(st.src) + '.old-';
|
|
645
649
|
for (const o of st.olds) {
|
|
@@ -658,6 +662,6 @@ export async function cleanupOld({ src, confirm } = {}) {
|
|
|
658
662
|
ok: failed.length === 0,
|
|
659
663
|
removed, failed, bytes,
|
|
660
664
|
note: failed.length ? ('有 ' + failed.length + ' 个旧备份未删净,可重试')
|
|
661
|
-
: (removed.length ? '已删除旧备份' : '
|
|
665
|
+
: (removed.length ? '已删除旧备份' : '无可删的旧备份'),
|
|
662
666
|
};
|
|
663
667
|
}
|
package/src/lib/office.mjs
CHANGED
|
@@ -90,7 +90,7 @@ export function nextName(store, key) {
|
|
|
90
90
|
*/
|
|
91
91
|
export function resolveOffice(store, query) {
|
|
92
92
|
const q = String(query || '').trim();
|
|
93
|
-
if (!q) return { ok: false, reason: '
|
|
93
|
+
if (!q) return { ok: false, reason: '查询为空' };
|
|
94
94
|
const keys = Object.keys((store && store.lineages) || {});
|
|
95
95
|
const lower = q.toLowerCase();
|
|
96
96
|
let key = '';
|
|
@@ -151,7 +151,7 @@ export function formatRow(row, askedAs) {
|
|
|
151
151
|
if (!cur || !cur.name) return (row && row.key ? row.key : '?') + ' — 当前空缺';
|
|
152
152
|
const label = KIND_LABEL[row.kind] || row.kind || '-';
|
|
153
153
|
// 只有「拿旧编号问现任」时才加来处标注;直接问类别名或现任名就不必重复。
|
|
154
|
-
const tail = askedAs && askedAs !== cur.name && askedAs !== row.key ? '(' + askedAs + '
|
|
154
|
+
const tail = askedAs && askedAs !== cur.name && askedAs !== row.key ? '(' + askedAs + ' 指向 ' + row.key + ')' : '';
|
|
155
155
|
return cur.name + ' ' + cur.sessionId + ' ' + label + (cur.since ? ' 自 ' + cur.since : '') + tail;
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -165,6 +165,11 @@ export function registerHolder(ws, opts) {
|
|
|
165
165
|
kind: o.kind, project: String(o.project || ''), task: String(o.task || ''),
|
|
166
166
|
workspace: String(o.workspace || ''), current: null, history: [],
|
|
167
167
|
});
|
|
168
|
+
// 同名类别互转(杂项与项目同名)必须改类别字段,否则名册一直停在旧类别。
|
|
169
|
+
lin.kind = o.kind;
|
|
170
|
+
lin.project = String(o.project || '');
|
|
171
|
+
lin.task = String(o.task || '');
|
|
172
|
+
if (o.workspace) lin.workspace = String(o.workspace);
|
|
168
173
|
if (!Array.isArray(lin.history)) lin.history = [];
|
|
169
174
|
const previous = lin.current && lin.current.name ? { ...lin.current } : null;
|
|
170
175
|
if (previous) {
|
|
@@ -185,6 +190,41 @@ export function registerHolder(ws, opts) {
|
|
|
185
190
|
return { key, previous, current: lin.current, registry };
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
/**
|
|
194
|
+
* 只同步类别字段:现任是同一会话时更新 kind、project、task,不写历史。
|
|
195
|
+
* 返回是否更新。
|
|
196
|
+
*/
|
|
197
|
+
export function syncHolderKind(ws, opts) {
|
|
198
|
+
const o = opts || {};
|
|
199
|
+
const key = lineageKeyOf(o.kind, o.project, o.task);
|
|
200
|
+
if (!key) return false;
|
|
201
|
+
const store = loadOffice(ws);
|
|
202
|
+
const lin = store.lineages[key];
|
|
203
|
+
if (!lin || !lin.current || String(lin.current.sessionId) !== String(o.sessionId || '')) return false;
|
|
204
|
+
lin.kind = o.kind;
|
|
205
|
+
lin.project = String(o.project || '');
|
|
206
|
+
lin.task = String(o.task || '');
|
|
207
|
+
saveOffice(ws, store);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 注销登记:现任是该会话时删除整条世系(历史一并移除,文件体系不动)。
|
|
213
|
+
* 返回是否删除。
|
|
214
|
+
*/
|
|
215
|
+
export function removeHolder(ws, opts) {
|
|
216
|
+
const o = opts || {};
|
|
217
|
+
const key = lineageKeyOf(o.kind, o.project, o.task);
|
|
218
|
+
if (!key) return false;
|
|
219
|
+
const store = loadOffice(ws);
|
|
220
|
+
const lin = store.lineages[key];
|
|
221
|
+
if (!lin) return false;
|
|
222
|
+
if (o.sessionId && lin.current && String(lin.current.sessionId) !== String(o.sessionId)) return false;
|
|
223
|
+
delete store.lineages[key];
|
|
224
|
+
saveOffice(ws, store);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
188
228
|
/**
|
|
189
229
|
* 读 Registry.md「项目清单」里的项目名(按表内顺序)。
|
|
190
230
|
* 只认那一张表:表头、分隔行与空行一律跳过;读不到文件时返回空数组。
|