@mingmaple/dsh-handover 0.9.14 → 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.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 +205 -87
- 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 +16 -16
- package/src/lib/sessions.mjs +1 -1
- package/src/lib/status.mjs +205 -45
- package/src/lib/succession.mjs +7 -7
- 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ho_docmap —— 本会话的文档定位 map(只指路,不规定读多少)。
|
|
3
|
+
*
|
|
4
|
+
* 写入范围由会话已登记的角色决定,不接受传入项目名或任务名:全局只写 global.md,
|
|
5
|
+
* 项目只写 project-<本项目>.md,杂项只写 misc-<本任务>.md。
|
|
6
|
+
* 写入者必须是在任会话(以 Office.json 为准);已交出的旧会话只读。
|
|
7
|
+
* 不设字数上限;写盘前逐条校验路径存在,缺项整条拒绝并列出。
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { textOutput } from '../lib/output.mjs';
|
|
12
|
+
import { nameOf } from '../lib/names.mjs';
|
|
13
|
+
import { NL } from '../lib/text.mjs';
|
|
14
|
+
import { loadOffice, lineageKeyOf } from '../lib/office.mjs';
|
|
15
|
+
import {
|
|
16
|
+
lineageOf, lineageLabel, docmapRelPath, docmapAbsPath, readDocmap,
|
|
17
|
+
renderDocmap, validateEntries,
|
|
18
|
+
} from '../lib/docmap.mjs';
|
|
19
|
+
|
|
20
|
+
export const ACTIONS = ['read', 'write', 'check'];
|
|
21
|
+
|
|
22
|
+
function ownLineage(env, exec) {
|
|
23
|
+
let info = null;
|
|
24
|
+
try { info = env.roleOf(exec); } catch { info = null; }
|
|
25
|
+
const lin = lineageOf(info);
|
|
26
|
+
return { info, lin };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function holderOf(ws, lin) {
|
|
30
|
+
try {
|
|
31
|
+
const store = loadOffice(ws);
|
|
32
|
+
const key = lineageKeyOf(lin.kind, lin.project, lin.task);
|
|
33
|
+
const row = store.lineages ? store.lineages[key] : null;
|
|
34
|
+
return { key, current: row && row.current ? row.current : null };
|
|
35
|
+
} catch {
|
|
36
|
+
return { key: '', current: null };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function entriesText(entries) {
|
|
41
|
+
if (!entries.length) return '(空 map:不注入提示段)';
|
|
42
|
+
return entries.map((e) => '- ' + e.path + (e.about ? ' —— ' + e.about : '') + (e.when ? '(' + e.when + ')' : '')).join(NL);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function pmDocmapTool(env) {
|
|
46
|
+
return {
|
|
47
|
+
name: nameOf('docmap'),
|
|
48
|
+
description:
|
|
49
|
+
'本会话的文档定位:action=read 读、write 写(逐条校验路径存在)、check 查自己的 map;只指路,不规定读多少。',
|
|
50
|
+
parameters: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
properties: {
|
|
54
|
+
action: { type: 'string', enum: ACTIONS, description: '缺省 read' },
|
|
55
|
+
entries: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
description: 'write:整份替换。每项 { path: 工作区相对路径, about: 一句话介绍, when: 何时读 }',
|
|
58
|
+
items: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
required: ['path', 'about'],
|
|
62
|
+
properties: {
|
|
63
|
+
path: { type: 'string', description: '工作区相对路径,必须存在' },
|
|
64
|
+
about: { type: 'string', description: '一句话介绍' },
|
|
65
|
+
when: { type: 'string', description: '什么时候该读(可选,建议写)' },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
output: textOutput(),
|
|
72
|
+
async execute(args, exec) {
|
|
73
|
+
const ws = env.resolveWs(exec);
|
|
74
|
+
const { info, lin } = ownLineage(env, exec);
|
|
75
|
+
if (!lin) throw new Error('本会话没有角色:文档定位只对全局、项目、杂项会话开放');
|
|
76
|
+
const action = typeof args.action === 'string' && args.action ? args.action : 'read';
|
|
77
|
+
if (ACTIONS.indexOf(action) === -1) throw new Error('action 只能是 ' + ACTIONS.join(' | ') + '(收到 ' + String(action) + ')');
|
|
78
|
+
const rel = docmapRelPath(info);
|
|
79
|
+
const abs = docmapAbsPath(ws, info);
|
|
80
|
+
const label = lineageLabel(info);
|
|
81
|
+
|
|
82
|
+
if (action === 'read') {
|
|
83
|
+
const map = readDocmap(ws, info);
|
|
84
|
+
const out = [];
|
|
85
|
+
out.push('文档定位(' + label + ') ' + rel);
|
|
86
|
+
out.push(map.exists ? '状态 已设置,' + map.entries.length + ' 条' : '状态 未设置(空 map 不注入提示段)');
|
|
87
|
+
if (map.entries.length) out.push('', entriesText(map.entries));
|
|
88
|
+
return { text: out.join(NL) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action === 'check') {
|
|
92
|
+
const map = readDocmap(ws, info);
|
|
93
|
+
const bad = validateEntries(ws, map.entries);
|
|
94
|
+
const out = [];
|
|
95
|
+
out.push('文档定位体检(' + label + ') ' + rel);
|
|
96
|
+
out.push(map.exists ? '条目 ' + map.entries.length + ' 条' : '未设置(空 map 不注入)');
|
|
97
|
+
for (const b of bad) out.push(' 失效:' + b.path + ' —— ' + b.problem);
|
|
98
|
+
out.push(bad.length ? '判定:' + bad.length + ' 条失效,请更新 map。' : '判定:路径全部存在。');
|
|
99
|
+
return { text: out.join(NL) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// action=write
|
|
103
|
+
const holder = holderOf(ws, lin);
|
|
104
|
+
const meId = String(env.selfId(exec) || '');
|
|
105
|
+
if (!holder.current) {
|
|
106
|
+
throw new Error('登记表里没有「' + (holder.key || label) + '」的在任记录:以在任记录为写入依据,先由全局完成登记');
|
|
107
|
+
}
|
|
108
|
+
if (!meId || String(holder.current.sessionId || '') !== meId) {
|
|
109
|
+
throw new Error('只有在任会话可以写:本条登记的在任会话为 ' + (holder.current.name || '?') + '(' + holder.current.sessionId + '),本会话为只读');
|
|
110
|
+
}
|
|
111
|
+
const list = Array.isArray(args.entries) ? args.entries : null;
|
|
112
|
+
if (!list) throw new Error('write 需要 entries 数组(每项 path、about、when)');
|
|
113
|
+
const entries = [];
|
|
114
|
+
for (const raw of list) {
|
|
115
|
+
const e = raw || {};
|
|
116
|
+
const p = String(e.path === undefined || e.path === null ? '' : e.path).trim();
|
|
117
|
+
const about = String(e.about === undefined || e.about === null ? '' : e.about).trim();
|
|
118
|
+
const when = String(e.when === undefined || e.when === null ? '' : e.when).trim();
|
|
119
|
+
if (!p) throw new Error('entries 里有空路径');
|
|
120
|
+
for (const [field, val] of [['path', p], ['about', about], ['when', when]]) {
|
|
121
|
+
if (val.indexOf('|') !== -1) throw new Error(field + ' 里不能出现竖线「|」(表格分隔符):' + val);
|
|
122
|
+
if (val.indexOf(String.fromCharCode(10)) !== -1) throw new Error(field + ' 里不能换行');
|
|
123
|
+
}
|
|
124
|
+
entries.push({ path: p, about, when });
|
|
125
|
+
}
|
|
126
|
+
const bad = validateEntries(ws, entries);
|
|
127
|
+
if (bad.length) {
|
|
128
|
+
const lines = ['未写入:下列路径不存在或不合法,逐条修正后重试。'];
|
|
129
|
+
for (const b of bad) lines.push(' - ' + b.path + ':' + b.problem);
|
|
130
|
+
lines.push('路径一律写工作区相对路径(例:Projects/<项目>/SKILL.md)。');
|
|
131
|
+
throw new Error(lines.join(NL));
|
|
132
|
+
}
|
|
133
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
134
|
+
let backup = '';
|
|
135
|
+
if (fs.existsSync(abs)) {
|
|
136
|
+
backup = abs + '.bak';
|
|
137
|
+
try { fs.copyFileSync(abs, backup); } catch { backup = ''; }
|
|
138
|
+
}
|
|
139
|
+
const body = renderDocmap(info, entries);
|
|
140
|
+
const tmp = abs + '.tmp-' + process.pid;
|
|
141
|
+
fs.writeFileSync(tmp, body, 'utf8');
|
|
142
|
+
fs.renameSync(tmp, abs);
|
|
143
|
+
let refreshed = false;
|
|
144
|
+
try { if (typeof env.refreshPromptFor === 'function') refreshed = env.refreshPromptFor(meId) === true; } catch { refreshed = false; }
|
|
145
|
+
const out = [];
|
|
146
|
+
out.push('已写入 ' + rel + '(' + entries.length + ' 条)' + (backup ? ';原文件备份为 ' + path.basename(backup) : ''));
|
|
147
|
+
out.push(refreshed ? '提示段已重装:下一轮即按新 map 注入。' : '提示段未能在本进程重装:重启 DSH 或让会话重建后生效。');
|
|
148
|
+
out.push('建议精炼:每条一句介绍、一句时机即可;不设字数上限,但太长的介绍会挤占提示段。');
|
|
149
|
+
if (!entries.length) out.push('空 map:不注入提示段(相当于撤销)。');
|
|
150
|
+
return { text: out.join(NL) };
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
package/src/tools/ho_gate.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { nameOf } from '../lib/names.mjs';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { textOutput } from '../lib/output.mjs';
|
|
6
6
|
import { NL } from '../lib/text.mjs';
|
|
7
|
-
import { syncCheck, renderSyncCheck } from '../lib/gates.mjs';
|
|
7
|
+
import { syncCheck, renderSyncCheck, docmapReport, renderDocmapCheck } from '../lib/gates.mjs';
|
|
8
8
|
import { lintGlobal, lintProject, renderLintGlobal, renderLintProject } from '../lib/lint.mjs';
|
|
9
9
|
import { SETUP_STEPS } from '../lib/guide.mjs';
|
|
10
10
|
|
|
@@ -53,6 +53,7 @@ export function pmGateTool(env) {
|
|
|
53
53
|
}
|
|
54
54
|
out.push(renderSyncCheck(g).join(NL));
|
|
55
55
|
out.push(renderLintProject(null, lintAllProjects(ws)).join(NL));
|
|
56
|
+
out.push(renderDocmapCheck(docmapReport(ws)).join(NL));
|
|
56
57
|
} else if (scope === "project") {
|
|
57
58
|
const name = args.project;
|
|
58
59
|
if (!name) throw new Error("scope=project 时必须给 project 参数");
|
package/src/tools/ho_office.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export function pmOfficeTool(env) {
|
|
|
13
13
|
type: 'object',
|
|
14
14
|
additionalProperties: false,
|
|
15
15
|
properties: {
|
|
16
|
-
query: { type: 'string', description: '要查的名字,如「全局」「全局3」「
|
|
16
|
+
query: { type: 'string', description: '要查的名字,如「全局」「全局3」「Demo1」「示例任务」;多个用空格或逗号分隔;留空=列出全部在任' },
|
|
17
17
|
json: { type: 'boolean', description: 'true 时输出机器可读 JSON(供脚本解析)' },
|
|
18
18
|
},
|
|
19
19
|
},
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { textOutput } from '../lib/output.mjs';
|
|
3
3
|
import { nameOf } from '../lib/names.mjs';
|
|
4
4
|
import { NL } from '../lib/text.mjs';
|
|
5
|
-
import path from 'node:path';
|
|
6
5
|
import { scanSessions } from '../lib/sessions.mjs';
|
|
7
6
|
import { syncCheck } from '../lib/gates.mjs';
|
|
8
7
|
import { stamp } from '../lib/brief.mjs';
|
|
@@ -37,9 +36,9 @@ function list(env, args, exec) {
|
|
|
37
36
|
}
|
|
38
37
|
const notifProjects = new Set(scan.notify.map((r) => r.project));
|
|
39
38
|
const skipProjects = new Set(scan.skip.map((r) => r.project));
|
|
40
|
-
out.push("通知判定:活跃项目 " + notifProjects.size + " 个,需要通知 " + scan.notify.length + " 个会话(仅各项目在任)/ 沉默项目 " + skipProjects.size + " 个,跳过 " + scan.skip.length + "
|
|
39
|
+
out.push("通知判定:活跃项目 " + notifProjects.size + " 个,需要通知 " + scan.notify.length + " 个会话(仅各项目在任)/ 沉默项目 " + skipProjects.size + " 个,跳过 " + scan.skip.length + " 个会话(沉默项目全部+已交出的旧会话)");
|
|
41
40
|
for (const r of scan.notify) out.push(" 要通知【在任】" + r.project + "(" + r.sid + ") 最后活动 " + stamp(r.mtime));
|
|
42
|
-
if (scan.skip.length) out.push("
|
|
41
|
+
if (scan.skip.length) out.push(" 跳过(沉默项目,含其已交出的旧会话):" + scan.skip.map((x) => x.project + "(" + x.sid.slice(-8) + ")").join("、"));
|
|
43
42
|
const gate = syncCheck(env.resolveWs(exec));
|
|
44
43
|
if (gate.error) out.push("通用约束条闸门(Charter §7.2.3):读不到 §8.1.1.4 正文 —— 请人工核 " + gate.path);
|
|
45
44
|
else if (!gate.ok) {
|
|
@@ -85,7 +84,7 @@ export function pmSessionsTool(env) {
|
|
|
85
84
|
sessionId: { type: 'string', description: 'prompt:新会话自己的 id' },
|
|
86
85
|
globalSessionId: { type: 'string', description: 'prompt:现任全局 id' },
|
|
87
86
|
extra: { type: 'string', description: 'prompt 附加一句;order 时作为「全局附加」拼进指令' },
|
|
88
|
-
confirm: { type: 'boolean', description: 'new、order
|
|
87
|
+
confirm: { type: 'boolean', description: 'new、order 需要当前用户明确同意后传 true;否则只报错、不做任何事' },
|
|
89
88
|
},
|
|
90
89
|
},
|
|
91
90
|
output: textOutput(),
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ho_status —— 会话状态:一个入口,按 view 分流。
|
|
3
|
+
* phase(默认):水位、压缩次数、锁、建议动作
|
|
4
|
+
* brief:水位、闸门、会话清点、交接漂移、阶段建议
|
|
5
|
+
* diag:服务可解析性、调度器、初始化错误、角色与工具集
|
|
6
|
+
* drift:交接漂移体检
|
|
7
|
+
* tokens:按会话 id 统计轮数、步数、每步 token
|
|
8
|
+
* 全部只读;原 ho_phase、ho_brief、ho_diag、ho_drift、ho_tokens 合并于此。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { textOutput } from '../lib/output.mjs';
|
|
13
|
+
import { nameOf } from '../lib/names.mjs';
|
|
14
|
+
import { NL, pick } from '../lib/text.mjs';
|
|
15
|
+
import { syncCheck } from '../lib/gates.mjs';
|
|
16
|
+
import { scanSessions, readSelfPressure } from '../lib/sessions.mjs';
|
|
17
|
+
import { composeBrief } from '../lib/brief.mjs';
|
|
18
|
+
import { decide } from '../lib/phase.mjs';
|
|
19
|
+
import { computeDrift, driftVerdict } from '../lib/drift.mjs';
|
|
20
|
+
import { watermark } from '../lib/facts.mjs';
|
|
21
|
+
import { eventsOf, countCompactions } from '../lib/events.mjs';
|
|
22
|
+
import { findLog, decodeLog, turnStarts } from '../lib/zstd-log.mjs';
|
|
23
|
+
import { activeConsentsFor, formatConsentLine } from '../lib/consent.mjs';
|
|
24
|
+
|
|
25
|
+
export const VIEWS = ['phase', 'brief', 'diag', 'drift', 'tokens'];
|
|
26
|
+
|
|
27
|
+
function analyzeLog(records) {
|
|
28
|
+
const turns = turnStarts(records).length;
|
|
29
|
+
let steps = 0, outTok = 0, maxResult = 0, toolResults = 0;
|
|
30
|
+
const perTurn = new Map();
|
|
31
|
+
for (const r of records) {
|
|
32
|
+
if (!r || !r.type) continue;
|
|
33
|
+
if (r.type === 'step/start') {
|
|
34
|
+
steps++;
|
|
35
|
+
const t = pick(r, 'data', 'turn');
|
|
36
|
+
if (typeof t === 'number') perTurn.set(t, (perTurn.get(t) || 0) + 1);
|
|
37
|
+
} else if (r.type === 'assistant/message') {
|
|
38
|
+
const u = pick(r, 'data', 'usage');
|
|
39
|
+
if (u && typeof u.outputTokens === 'number') outTok += u.outputTokens;
|
|
40
|
+
} else if (r.type === 'tool/result') {
|
|
41
|
+
toolResults++;
|
|
42
|
+
try { const len = JSON.stringify(pick(r, 'data') || '').length; if (len > maxResult) maxResult = len; } catch { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const heavyTurns = Array.from(perTurn.values()).filter((n) => n > 20).length;
|
|
46
|
+
return { turns, steps, outTok, maxResult, toolResults, heavyTurns, perStep: steps ? Math.round(outTok / steps) : 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 授权状态行:压缩与换任各一句,未授权写「无授权」。 */
|
|
50
|
+
function consentLine(meId) {
|
|
51
|
+
try { return formatConsentLine(activeConsentsFor(meId)); } catch { return ''; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function phaseText(env, args, exec) {
|
|
55
|
+
const meId = env.selfId(exec);
|
|
56
|
+
const real = env.selfPct(meId);
|
|
57
|
+
const ratio = typeof args.simulateRatio === 'number' ? args.simulateRatio : (real === null ? null : real / 100);
|
|
58
|
+
const count = env.compactionCount(exec);
|
|
59
|
+
const lock = env.lockState(exec);
|
|
60
|
+
const adv = decide({ ratio, compactionsUsed: count.used, cfg: env.cfg, lockState: lock });
|
|
61
|
+
const out = [];
|
|
62
|
+
out.push('会话 ' + (meId || '未知'));
|
|
63
|
+
out.push('水位 ' + (ratio === null ? '读不到' : (Math.round(ratio * 1000) / 10).toFixed(1) + '%')
|
|
64
|
+
+ (typeof args.simulateRatio === 'number' ? ' (演练模拟值)' : '')
|
|
65
|
+
+ (typeof env.staleNote === 'function' ? env.staleNote(exec) : ''));
|
|
66
|
+
out.push('压缩 ' + count.used + '/' + env.cfg.maxCompactions + ' 次' + (count.note ? ' ' + count.note : ''));
|
|
67
|
+
out.push('锁 ' + lock);
|
|
68
|
+
out.push('建议 ' + adv.action + ' —— ' + adv.reason);
|
|
69
|
+
const c = consentLine(meId);
|
|
70
|
+
if (c) out.push('授权 ' + c);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function briefText(env, args, exec) {
|
|
75
|
+
const ws = env.resolveWs(exec);
|
|
76
|
+
const w = watermark(env.cfg);
|
|
77
|
+
const warn = typeof args.threshold === 'number' ? args.threshold : w.warnValue;
|
|
78
|
+
const gate = syncCheck(ws);
|
|
79
|
+
const meId = env.selfId(exec);
|
|
80
|
+
const self = readSelfPressure(env.cacheDir(), meId);
|
|
81
|
+
const scan = scanSessions({
|
|
82
|
+
dir: env.cacheDir(),
|
|
83
|
+
meId,
|
|
84
|
+
meCwd: env.selfCwd(exec),
|
|
85
|
+
warn,
|
|
86
|
+
silentHours: env.cfg.sessionScanHours,
|
|
87
|
+
});
|
|
88
|
+
const drift = computeDrift({ ws, cacheDir: env.cacheDir(), slack: env.cfg.driftSlack, all: false });
|
|
89
|
+
const ratio = self.used === null || self.win === null ? null : self.used / self.win;
|
|
90
|
+
const adv = decide({ ratio, compactionsUsed: 0, cfg: env.cfg, lockState: 'unknown' });
|
|
91
|
+
const selfPct = self.used === null || self.win === null ? null : (self.used / self.win) * 100;
|
|
92
|
+
const ok = gate.ok && (selfPct === null || selfPct < warn);
|
|
93
|
+
const lines = composeBrief({
|
|
94
|
+
cfg: env.cfg,
|
|
95
|
+
selfUsed: self.used,
|
|
96
|
+
selfWin: self.win,
|
|
97
|
+
gate,
|
|
98
|
+
scan,
|
|
99
|
+
drift,
|
|
100
|
+
advice: { text: adv.action + ' —— ' + adv.reason },
|
|
101
|
+
ok,
|
|
102
|
+
});
|
|
103
|
+
const c = consentLine(meId);
|
|
104
|
+
if (c) lines.push('授权 ' + c);
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function diagText(env, state, exec) {
|
|
109
|
+
const out = [];
|
|
110
|
+
const meId = env.selfId(exec);
|
|
111
|
+
out.push('会话 ' + (meId || '?'));
|
|
112
|
+
if (typeof env.version === 'function') out.push('版本 ' + env.version());
|
|
113
|
+
out.push('调度器 ' + (state.schedulerReady() ? '就绪' : '未就绪' + (state.schedulerError() ? ':' + state.schedulerError() : '(稍后重试或重启)')));
|
|
114
|
+
if (typeof env.injectList === 'function') out.push('inject ' + env.injectList().join(' / '));
|
|
115
|
+
if (typeof env.initErrors === 'function') {
|
|
116
|
+
const errs = env.initErrors();
|
|
117
|
+
out.push('初始化 ' + (errs.length ? '有 ' + errs.length + ' 处失败:' + errs.join(';') : '无错误'));
|
|
118
|
+
}
|
|
119
|
+
if (typeof env.probeServices === 'function') {
|
|
120
|
+
const svc = env.probeServices();
|
|
121
|
+
out.push('服务 ' + Object.keys(svc).map((k) => k + '=' + (svc[k] ? '可用' : '—')).join(' '));
|
|
122
|
+
}
|
|
123
|
+
if (typeof env.guardLast === 'function') {
|
|
124
|
+
const g = env.guardLast();
|
|
125
|
+
out.push('通信判定 ' + (g
|
|
126
|
+
? (g.allow ? '放行' : '拦截') + ' · 依据 ' + g.source + (g.title ? ' · 标题 ' + g.title : '') + ' · 会话 ' + g.sid
|
|
127
|
+
: '本进程还没有 peer(action="send"、"new")调用'));
|
|
128
|
+
}
|
|
129
|
+
if (typeof env.probeAgentScope === 'function') {
|
|
130
|
+
const a = env.probeAgentScope(exec);
|
|
131
|
+
const show = ['compaction', 'compactNow', 'tokenMeter', 'sessions', 'llm', 'agents']
|
|
132
|
+
.map((k) => k + '=' + (a[k] ? '可用' : '—')).join(' ');
|
|
133
|
+
out.push('会话作用域 ' + (a.ctx ? show : '无法获取 exec.agent.ctx'));
|
|
134
|
+
}
|
|
135
|
+
const st = typeof env.compactionState === 'function'
|
|
136
|
+
? env.compactionState(exec)
|
|
137
|
+
: (() => { const c = countCompactions(eventsOf(exec)); return { used: c.used, lock: c.lock, source: 'events' }; })();
|
|
138
|
+
out.push('压缩 ' + st.used + '/' + env.cfg.maxCompactions + ' 次;锁 ' + st.lock + ';来源 ' + st.source);
|
|
139
|
+
const pct = env.selfPct(meId);
|
|
140
|
+
out.push('水位 ' + (pct === null ? '读不到' : pct.toFixed(1) + '%') + (typeof env.staleNote === 'function' ? env.staleNote(exec) : ''));
|
|
141
|
+
const ws = env.resolveWs(exec);
|
|
142
|
+
let wsOk = false;
|
|
143
|
+
try { wsOk = fs.existsSync(path.join(ws, '_Control_Center')); } catch { wsOk = false; }
|
|
144
|
+
if (typeof env.roleOf === 'function') {
|
|
145
|
+
try {
|
|
146
|
+
const info = env.roleOf(exec);
|
|
147
|
+
const ts = typeof env.toolsetOf === 'function' ? env.toolsetOf(info.role) : null;
|
|
148
|
+
out.push('角色 ' + info.role + (info.project ? '(项目 ' + info.project + ')' : info.task ? '(任务 ' + info.task + ')' : '') + ' · 依据 ' + info.source + (ts ? ' · 可见工具 ' + ts.allow.length + '/' + (ts.allow.length + ts.deny.length) : ''));
|
|
149
|
+
if (typeof env.roleState === 'function') {
|
|
150
|
+
const rs = env.roleState();
|
|
151
|
+
const cnt = typeof rs.count === 'number' ? ' · 已装配 ' + rs.count + ' 个会话' : '';
|
|
152
|
+
out.push('角色装配 ' + (rs.mode || '未知') + cnt + (rs.last ? ' · 最近:' + rs.last.sid + ' → ' + rs.last.role + '(' + rs.last.at + ')' : ''));
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
out.push('角色 判定失败:' + (e && e.message ? e.message : e));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
out.push('工作区 ' + ws + (wsOk
|
|
159
|
+
? ' 已接入'
|
|
160
|
+
: ' — 未接入(未找到 _Control_Center):让当前用户走设置页「首次安装」,或本会话直接 ho_workspace(action="init", parent="<目录>", register=true)'));
|
|
161
|
+
try {
|
|
162
|
+
const mv = typeof env.moveInfo === 'function' ? env.moveInfo() : null;
|
|
163
|
+
if (mv) out.push('迁移 ' + mv.phase + ' · ' + mv.src + (mv.dst ? ' → ' + mv.dst : ''));
|
|
164
|
+
} catch { /* ignore */ }
|
|
165
|
+
out.push('配置 warn=' + env.cfg.warnRatio + ' handover=' + env.cfg.handoverRatio + ' hard=' + env.cfg.hardRatio + ' max=' + env.cfg.maxCompactions);
|
|
166
|
+
if (typeof env.handoverFor === 'function') {
|
|
167
|
+
try {
|
|
168
|
+
const hp = env.handoverFor(exec);
|
|
169
|
+
out.push('交接路径 ' + (hp && hp.path ? hp.path + '(' + hp.kind + ')' : '无 —— ' + ((hp && hp.reason) || '角色未定')));
|
|
170
|
+
} catch (e) {
|
|
171
|
+
out.push('交接路径 解析失败:' + (e && e.message ? e.message : e));
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
out.push('交接路径 ' + (env.cfg.handoverPath || '(按角色解析)'));
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function driftText(env, args, exec) {
|
|
180
|
+
const ws = env.resolveWs(exec);
|
|
181
|
+
const res = computeDrift({
|
|
182
|
+
ws,
|
|
183
|
+
cacheDir: env.cacheDir(),
|
|
184
|
+
slack: typeof args.slack === 'number' ? args.slack : env.cfg.driftSlack,
|
|
185
|
+
all: args.all === true,
|
|
186
|
+
});
|
|
187
|
+
const out = [];
|
|
188
|
+
out.push('交接匹配检查(跟着交接走;冗余 ' + res.slack + ' 轮) 工作区根 ' + ws);
|
|
189
|
+
out.push(' 项目 会话类型 会话 当前轮 交接轮 Δ 判定');
|
|
190
|
+
for (const r of res.rows) {
|
|
191
|
+
const cells = [r.project, r.kind, r.sid.slice(-8), String(r.turns), r.handoverTurn === null ? '?' : String(r.handoverTurn), r.delta === null ? '?' : String(r.delta), r.verdict];
|
|
192
|
+
out.push(' ' + cells.join(' '));
|
|
193
|
+
}
|
|
194
|
+
if (!res.rows.length) out.push(' 读不到任何项目会话(Projects/ 下对应库不存在时属正常)');
|
|
195
|
+
out.push(driftVerdict(res));
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function tokensText(args) {
|
|
200
|
+
const out = [];
|
|
201
|
+
const list = Array.isArray(args.sessions) ? args.sessions.filter((s) => typeof s === 'string' && s.trim() !== '') : [];
|
|
202
|
+
if (!list.length) throw new Error('view=tokens 时 sessions 不能为空(给会话 id 数组)');
|
|
203
|
+
out.push('会话 轮 步 输出tok 每步tok 最贵返回 超20步轮 判定');
|
|
204
|
+
for (const sid of list) {
|
|
205
|
+
const full = sid.indexOf('session-') === 0 ? sid : 'session-' + sid;
|
|
206
|
+
const log = findLog(full);
|
|
207
|
+
if (!log) { out.push(sid + ' ERR 日志找不到'); continue; }
|
|
208
|
+
const recs = decodeLog(log);
|
|
209
|
+
if (!recs) { out.push(sid + ' ERR 日志解不出'); continue; }
|
|
210
|
+
const m = analyzeLog(recs);
|
|
211
|
+
const verdict = m.perStep > 2000 ? '偏贵(每步 >2k)' : m.heavyTurns > 0 ? '有超 20 步的轮' : '正常';
|
|
212
|
+
out.push([sid.slice(-8), m.turns, m.steps, m.outTok, m.perStep, Math.round(m.maxResult / 1024) + 'KB', m.heavyTurns, verdict].join(' '));
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function pmStatusTool(env, state) {
|
|
218
|
+
const w = watermark(env.cfg);
|
|
219
|
+
return {
|
|
220
|
+
name: nameOf('status'),
|
|
221
|
+
description:
|
|
222
|
+
'会话状态(只读):view=phase 水位与建议(默认)、brief 起始简报、diag 插件自检、drift 交接漂移、tokens token 体检。',
|
|
223
|
+
parameters: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
additionalProperties: false,
|
|
226
|
+
properties: {
|
|
227
|
+
view: { type: 'string', enum: VIEWS, description: '缺省 phase' },
|
|
228
|
+
threshold: { type: 'number', description: 'view=brief:预警阈值百分比(默认 ' + w.warnValue + ')' },
|
|
229
|
+
simulateRatio: { type: 'number', description: 'view=phase:演练用比例替代真实水位(0-1),不写任何状态' },
|
|
230
|
+
sessions: { type: 'array', items: { type: 'string' }, description: 'view=tokens:会话 id 列表' },
|
|
231
|
+
slack: { type: 'number', description: 'view=drift:容忍轮数(默认 3)' },
|
|
232
|
+
all: { type: 'boolean', description: 'view=drift:列全部会话,而不只是每个项目的在任会话' },
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
output: textOutput(),
|
|
236
|
+
async execute(args, exec) {
|
|
237
|
+
const view = typeof args.view === 'string' && args.view ? args.view : 'phase';
|
|
238
|
+
if (VIEWS.indexOf(view) === -1) throw new Error('view 只能是 ' + VIEWS.join(' | ') + '(收到 ' + String(view) + ')');
|
|
239
|
+
if (view === 'phase') return { text: phaseText(env, args, exec).join(NL) };
|
|
240
|
+
if (view === 'brief') return { text: briefText(env, args, exec).join(NL) };
|
|
241
|
+
if (view === 'diag') return { text: diagText(env, state, exec).join(NL) };
|
|
242
|
+
if (view === 'drift') return { text: driftText(env, args, exec).join(NL) };
|
|
243
|
+
return { text: tokensText(args).join(NL) };
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
package/src/tools/ho_succeed.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/** ho_workspace —— 工作区生命周期:list / create / delete
|
|
1
|
+
/** ho_workspace —— 工作区生命周期:list / create / delete / init(首次建体系)+ 全局根目录迁移。 */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { nameOf } from '../lib/names.mjs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { textOutput } from '../lib/output.mjs';
|
|
6
6
|
import { NL } from '../lib/text.mjs';
|
|
7
7
|
import { workspaceRows } from '../lib/registry.mjs';
|
|
8
|
+
import { createGlobalWorkspace, ensureScaffold } from '../lib/scaffold.mjs';
|
|
8
9
|
import { probeMove, startMove, moveStatus, cleanupOld, stageSwitch, repairLink, stageRetarget } from '../lib/mover.mjs';
|
|
9
10
|
import { currentRoot } from '../lib/api.mjs';
|
|
10
11
|
|
|
@@ -20,7 +21,7 @@ function gb(n) {
|
|
|
20
21
|
|
|
21
22
|
function renderProbe(p) {
|
|
22
23
|
const out = [];
|
|
23
|
-
out.push('
|
|
24
|
+
out.push('迁移预检:' + (p.ok ? '通过' : '不通过'));
|
|
24
25
|
out.push(' 源 ' + p.src + (p.srcIsLink ? '(已是链接,指向 ' + p.linkTarget + ')' : ''));
|
|
25
26
|
out.push(' 复制源 ' + p.copyFrom);
|
|
26
27
|
out.push(' 目标 ' + p.dst);
|
|
@@ -36,7 +37,7 @@ export function pmWorkspaceTool(env) {
|
|
|
36
37
|
return {
|
|
37
38
|
name: nameOf('workspace'),
|
|
38
39
|
description:
|
|
39
|
-
'
|
|
40
|
+
'工作区:init 首次建全局体系、scaffold 补齐缺失目录与说明、list、create、delete、move-* 搬整个全局根目录。删除只删注册。',
|
|
40
41
|
parameters: {
|
|
41
42
|
type: 'object',
|
|
42
43
|
additionalProperties: false,
|
|
@@ -44,15 +45,18 @@ export function pmWorkspaceTool(env) {
|
|
|
44
45
|
properties: {
|
|
45
46
|
action: {
|
|
46
47
|
type: 'string',
|
|
47
|
-
enum: ['list', 'create', 'delete', 'move-probe', 'move', 'move-switch', 'move-status', 'move-retarget', 'move-repair', 'move-cleanup'],
|
|
48
|
-
description: 'list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup',
|
|
48
|
+
enum: ['init', 'scaffold', 'list', 'create', 'delete', 'move-probe', 'move', 'move-switch', 'move-status', 'move-retarget', 'move-repair', 'move-cleanup'],
|
|
49
|
+
description: 'init / scaffold / list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup',
|
|
49
50
|
},
|
|
51
|
+
parent: { type: 'string', description: 'init 用:目标父目录绝对路径(不存在会创建);全局根建在它里面' },
|
|
52
|
+
name: { type: 'string', description: 'init 用:全局根目录名,默认 _Workspace' },
|
|
53
|
+
register: { type: 'boolean', description: 'init 用:建好后顺带注册为工作区(默认 false)' },
|
|
50
54
|
path: { type: 'string', description: 'create 用:目录绝对路径;默认要求已存在,ensure=true 时自动创建' },
|
|
51
|
-
title: { type: 'string', description: 'create
|
|
55
|
+
title: { type: 'string', description: 'create、init 用:工作区显示标题(init 默认「全局」)' },
|
|
52
56
|
ensure: { type: 'boolean', description: 'create 用:目录不存在时自动创建(默认 false,不替你建目录)' },
|
|
53
57
|
workspace: { type: 'string', description: 'delete 用:工作区 id、标题、路径' },
|
|
54
58
|
to: { type: 'string', description: 'move 用:新根绝对路径(父目录可自动建)' },
|
|
55
|
-
confirm: { type: 'boolean', description: 'move-cleanup 用:true
|
|
59
|
+
confirm: { type: 'boolean', description: 'move-cleanup 用:true 才真的删旧备份' },
|
|
56
60
|
},
|
|
57
61
|
},
|
|
58
62
|
output: textOutput(),
|
|
@@ -61,6 +65,74 @@ export function pmWorkspaceTool(env) {
|
|
|
61
65
|
const action = String(args.action || '');
|
|
62
66
|
const out = [];
|
|
63
67
|
|
|
68
|
+
if (action === 'init') {
|
|
69
|
+
const parent = String(args.parent || '').trim();
|
|
70
|
+
if (!parent) throw new Error('action="init" 需要 parent(目标父目录绝对路径)');
|
|
71
|
+
const r = createGlobalWorkspace(parent, { name: args.name });
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
out.push('未建立:' + r.error);
|
|
74
|
+
if (r.root) out.push('目标:' + r.root);
|
|
75
|
+
out.push('');
|
|
76
|
+
out.push('用法:ho_workspace(action="init", parent="<已存在的目录绝对路径>")。插件只新建、不覆盖。');
|
|
77
|
+
return { text: out.join(NL) };
|
|
78
|
+
}
|
|
79
|
+
out.push('已建立全局文件体系:' + r.root);
|
|
80
|
+
out.push(' 目录 ' + r.created.length + ' 个:' + r.created.slice(0, 5).join('、') + (r.created.length > 5 ? ' 等' : ''));
|
|
81
|
+
out.push(' 文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '(都已存在,未覆盖)'));
|
|
82
|
+
if (r.existed) out.push(' 目标目录原为空目录,直接使用。');
|
|
83
|
+
if (r.createdParent) out.push(' 父目录原本不存在,已创建。');
|
|
84
|
+
if (r.skipped.length) out.push(' 跳过(已存在、未覆盖):' + r.skipped.join('、'));
|
|
85
|
+
let registered = null;
|
|
86
|
+
if (args.register === true) {
|
|
87
|
+
if (!reg || typeof reg.create !== 'function') {
|
|
88
|
+
out.push('');
|
|
89
|
+
out.push('注册未完成:workspaceRegistry 不可用。请把下面这个路径粘进 DSH 的新建工作区窗口:');
|
|
90
|
+
out.push(' ' + r.root);
|
|
91
|
+
return { text: out.join(NL) };
|
|
92
|
+
}
|
|
93
|
+
const title = String(args.title || '').trim() || '全局';
|
|
94
|
+
try {
|
|
95
|
+
const w = await reg.create(r.root, title);
|
|
96
|
+
registered = w;
|
|
97
|
+
out.push('');
|
|
98
|
+
out.push('已注册工作区:' + title + '(' + String(w && w.id ? w.id : '?') + ')');
|
|
99
|
+
} catch (e) {
|
|
100
|
+
out.push('');
|
|
101
|
+
out.push('注册失败:' + String(e && e.message ? e.message : e));
|
|
102
|
+
out.push('可把下面这个路径粘进 DSH 的新建工作区窗口:');
|
|
103
|
+
out.push(' ' + r.root);
|
|
104
|
+
return { text: out.join(NL) };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
out.push('');
|
|
108
|
+
out.push('下一步:');
|
|
109
|
+
if (!registered) {
|
|
110
|
+
out.push(' 1. 注册为工作区:ho_workspace(action="create", path="' + r.root + '", title="全局"),或把该路径粘进 DSH 的新建工作区窗口;');
|
|
111
|
+
out.push(' 2. 新建一个标题含「全局」的会话,闸门、交接体检、会话水位即接入。');
|
|
112
|
+
} else {
|
|
113
|
+
out.push(' 新建一个标题含「全局」的会话,闸门、交接体检、会话水位即接入。');
|
|
114
|
+
}
|
|
115
|
+
out.push('(也可以在设置页的 dsh-handover 中完成「首次安装」,效果相同。)');
|
|
116
|
+
return { text: out.join(NL) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (action === 'scaffold') {
|
|
120
|
+
const root = currentRoot(env);
|
|
121
|
+
if (!root) throw new Error('未找到全局根(工作区未接入)');
|
|
122
|
+
const r = ensureScaffold(root);
|
|
123
|
+
if (!r.ok) {
|
|
124
|
+
out.push('未补齐:' + r.error);
|
|
125
|
+
out.push('根目录:' + root);
|
|
126
|
+
return { text: out.join(NL) };
|
|
127
|
+
}
|
|
128
|
+
out.push('已核对文件体系:' + root);
|
|
129
|
+
out.push(' 新建目录 ' + r.created.length + ' 个:' + (r.created.length ? r.created.join('、') : '(无缺失)'));
|
|
130
|
+
out.push(' 新建文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '(无缺失)'));
|
|
131
|
+
out.push(' 已存在未覆盖 ' + r.skipped.length + ' 个' + (r.skipped.length ? ':' + r.skipped.join('、') : ''));
|
|
132
|
+
out.push('只新建、不覆盖、不删:已有文件的内容一字未改。');
|
|
133
|
+
return { text: out.join(NL) };
|
|
134
|
+
}
|
|
135
|
+
|
|
64
136
|
if (action === 'list') {
|
|
65
137
|
if (reg && typeof reg.list === 'function') {
|
|
66
138
|
let list = [];
|
|
@@ -144,7 +216,7 @@ export function pmWorkspaceTool(env) {
|
|
|
144
216
|
out.push('下一步(三条,顺序不能换):');
|
|
145
217
|
out.push(' ① 完全退出 DSH(源目录被占用,改名会失败)');
|
|
146
218
|
out.push(' ② 执行 ho_workspace(action="move-switch") 派后台助手(推荐);助手会等待 DSH 退出后自动完成');
|
|
147
|
-
out.push(' ③ 重新启动 DSH
|
|
219
|
+
out.push(' ③ 重新启动 DSH;确认无误后可在设置页删除旧备份');
|
|
148
220
|
return { text: out.join(NL) };
|
|
149
221
|
}
|
|
150
222
|
|
|
@@ -160,8 +232,8 @@ export function pmWorkspaceTool(env) {
|
|
|
160
232
|
|
|
161
233
|
if (action === 'move-status') {
|
|
162
234
|
const st = moveStatus(); // 以标记里的源路径为准:修改登记后当前根已是新位置,拿它比对会误报
|
|
163
|
-
const label = { none: '
|
|
164
|
-
out.push('
|
|
235
|
+
const label = { none: '没有迁移记录', verified: '已复制并校验通过,待切换', switched: '已切换(原路径为链接)', partial: '复制有差异,未生成脚本' };
|
|
236
|
+
out.push('迁移状态:' + (label[st.phase] || st.phase));
|
|
165
237
|
out.push(' 源 ' + st.src + (st.linkTarget ? '(→ ' + st.linkTarget + ')' : ''));
|
|
166
238
|
if (st.dst) out.push(' 目标 ' + st.dst);
|
|
167
239
|
if (st.marker && st.marker.files) out.push(' 清单 ' + st.marker.files + ' 文件 · ' + (st.marker.digest || '').slice(0, 16) + '…');
|
|
@@ -175,10 +247,10 @@ export function pmWorkspaceTool(env) {
|
|
|
175
247
|
}
|
|
176
248
|
}
|
|
177
249
|
if (st.olds && st.olds.length) {
|
|
178
|
-
for (const o of st.olds) out.push('
|
|
250
|
+
for (const o of st.olds) out.push(' 旧备份 ' + o.path + ' ' + gb(o.bytes));
|
|
179
251
|
out.push(' 删除:ho_workspace(action="move-cleanup", confirm=true)');
|
|
180
252
|
} else if (st.phase === 'switched') {
|
|
181
|
-
out.push('
|
|
253
|
+
out.push(' 没有旧备份');
|
|
182
254
|
}
|
|
183
255
|
return { text: out.join(NL) };
|
|
184
256
|
}
|
|
@@ -203,7 +275,7 @@ export function pmWorkspaceTool(env) {
|
|
|
203
275
|
}
|
|
204
276
|
|
|
205
277
|
if (action === 'move-cleanup') {
|
|
206
|
-
if (args.confirm !== true) throw new Error('move-cleanup 需要 confirm=true
|
|
278
|
+
if (args.confirm !== true) throw new Error('move-cleanup 需要 confirm=true(会真删旧备份目录)');
|
|
207
279
|
const r = await cleanupOld({ confirm: true });
|
|
208
280
|
out.push(r.note);
|
|
209
281
|
for (const p of r.removed) out.push(' 已删 ' + p);
|
|
@@ -211,7 +283,7 @@ export function pmWorkspaceTool(env) {
|
|
|
211
283
|
return { text: out.join(NL) };
|
|
212
284
|
}
|
|
213
285
|
|
|
214
|
-
throw new Error('action 只能是 list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup');
|
|
286
|
+
throw new Error('action 只能是 init / scaffold / list / create / delete / move-probe / move / move-switch / move-status / move-retarget / move-repair / move-cleanup');
|
|
215
287
|
},
|
|
216
288
|
};
|
|
217
289
|
}
|