@mingmaple/dsh-handover 0.9.13 → 0.9.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -19
- package/docs/migrate.md +6 -6
- package/docs/releases/0.9.14.md +18 -0
- package/docs/releases/0.9.22.md +34 -0
- package/docs/troubleshoot.md +34 -10
- package/package.json +71 -71
- package/scripts/blank-dsh.mjs +29 -23
- package/scripts/blank-smoke.mjs +261 -78
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/src/client.js +159 -120
- package/src/index.mjs +65 -58
- package/src/lib/api.mjs +9 -7
- package/src/lib/brief.mjs +2 -2
- package/src/lib/config.mjs +3 -3
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +143 -0
- package/src/lib/gates.mjs +25 -1
- package/src/lib/guide.mjs +35 -44
- package/src/lib/lint.mjs +3 -2
- package/src/lib/manual.mjs +48 -9
- package/src/lib/mover.mjs +10 -10
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +22 -14
- package/src/lib/pack-ops.mjs +2 -2
- package/src/lib/peer.mjs +4 -4
- package/src/lib/role.mjs +8 -5
- package/src/lib/scaffold.mjs +42 -20
- package/src/lib/schedule.mjs +1 -2
- package/src/lib/session-create.mjs +1 -1
- package/src/lib/session-ops.mjs +49 -20
- package/src/lib/sessions.mjs +1 -1
- package/src/lib/status.mjs +205 -45
- package/src/lib/succession.mjs +27 -11
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- package/src/templates/Prompt_Templates.md +13 -13
- package/src/templates/Runtime_Manual.md +56 -30
- package/src/templates/rules/Handover_Rules.md +2 -1
- package/src/templates/rules/Migration_Rules.md +1 -1
- package/src/templates/standards/README.md +25 -0
- package/src/tools/ho_backup.mjs +135 -0
- package/src/tools/ho_checkpoint.mjs +12 -11
- package/src/tools/ho_docmap.mjs +153 -0
- package/src/tools/ho_gate.mjs +2 -1
- package/src/tools/ho_office.mjs +1 -1
- package/src/tools/ho_sessions.mjs +3 -4
- package/src/tools/ho_status.mjs +246 -0
- package/src/tools/ho_succeed.mjs +1 -1
- package/src/tools/ho_workspace.mjs +86 -14
- package/src/tools/peer.mjs +205 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
|
@@ -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
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peer —— 平级会话:一个入口,按 action 分流。
|
|
3
|
+
* list 会话一览(只读缓存,不激活任何会话)
|
|
4
|
+
* status 单会话详情
|
|
5
|
+
* send 投一条消息(唯一会消耗对方 token 的动作)
|
|
6
|
+
* peek 读取目标会话近期会话历史(内容进本会话上下文)
|
|
7
|
+
* rename 只改显示标题
|
|
8
|
+
* new 新建平级会话
|
|
9
|
+
* 原 peer_list、peer_status、peer_send、peer_peek、peer_rename、peer_new 合并于此。
|
|
10
|
+
* 动作级权限在这里再判一次:角色矩阵只决定「这个工具可不可见」,不细分动作。
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { textOutput } from '../lib/output.mjs';
|
|
14
|
+
import { PEER_NAME } from '../lib/names.mjs';
|
|
15
|
+
import { NL } from '../lib/text.mjs';
|
|
16
|
+
import {
|
|
17
|
+
readProjCache, rowLine, sessionRows, stamp, pressurePct, isSubagent, isBusy,
|
|
18
|
+
projectName, pick, peerMarker, resolveWorkspaceId, findSessionLog, decodeSessionLog, conversation,
|
|
19
|
+
} from '../lib/peer.mjs';
|
|
20
|
+
|
|
21
|
+
export const ACTIONS = ['list', 'status', 'send', 'peek', 'rename', 'new'];
|
|
22
|
+
const GLOBAL_ONLY = ['list', 'peek', 'rename'];
|
|
23
|
+
|
|
24
|
+
function roleOf(env, exec) {
|
|
25
|
+
try { return env.roleOf(exec) || { role: 'plain' }; } catch { return { role: 'plain' }; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function denyUnless(ok, action, why) {
|
|
29
|
+
if (!ok) throw new Error('peer(action="' + action + '"):' + why);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function listText(env, args) {
|
|
33
|
+
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(200, Math.trunc(args.limit))) : 40;
|
|
34
|
+
const controller = env.controller();
|
|
35
|
+
if (!controller || typeof controller.list !== 'function') {
|
|
36
|
+
throw new Error('peer(action="list"):sessionController 不可用(插件未注入该服务)');
|
|
37
|
+
}
|
|
38
|
+
const rows = await sessionRows(controller, args.include_subagents === true);
|
|
39
|
+
if (rows.length === 0) return 'no sessions';
|
|
40
|
+
return rows.slice(0, limit).map((row) => {
|
|
41
|
+
const cache = readProjCache(row.sessionId);
|
|
42
|
+
const state = row.running ? 'running' : row.agentAvailable ? 'idle' : 'cold';
|
|
43
|
+
return rowLine(row.sessionId, cache, state) + ' | ' + stamp(row.updatedAt);
|
|
44
|
+
}).join(NL);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function statusText(env, args) {
|
|
48
|
+
const controller = env.controller();
|
|
49
|
+
if (!controller || typeof controller.list !== 'function') {
|
|
50
|
+
throw new Error('peer(action="status"):sessionController 不可用(插件未注入该服务)');
|
|
51
|
+
}
|
|
52
|
+
const sid = String(args.session_id || '').trim();
|
|
53
|
+
if (!sid) throw new Error('peer(action="status"):session_id 不能为空');
|
|
54
|
+
const cache = readProjCache(sid);
|
|
55
|
+
const rows = await sessionRows(controller, true);
|
|
56
|
+
const row = rows.find((r) => r.sessionId === sid) ?? null;
|
|
57
|
+
if (cache === null && row === null) return 'unknown session: ' + sid;
|
|
58
|
+
const stats = pick(cache, 'record', 'rows', 'sessionStats', 'val') ?? {};
|
|
59
|
+
const pct = cache === null ? null : pressurePct(cache);
|
|
60
|
+
return [
|
|
61
|
+
'session ' + sid,
|
|
62
|
+
'kind ' + (cache === null ? '?' : isSubagent(cache) ? '子会话(subagent)' : '平级会话(peer)'),
|
|
63
|
+
'project ' + (cache === null ? '-' : projectName(cache)),
|
|
64
|
+
'pressure ' + (pct === null ? '?' : pct.toFixed(1) + '%') + (pct !== null && pct >= 32 ? ' 已进入预警区' : ''),
|
|
65
|
+
'state ' + (isBusy(cache) === true ? 'running' : row === null ? 'cold' : row.agentAvailable ? 'idle' : 'cold'),
|
|
66
|
+
'turns ' + String(stats.turns ?? '?') + ' steps ' + String(stats.steps ?? '?'),
|
|
67
|
+
'cwd ' + String(row?.cwd ?? pick(cache, 'record', 'identity', 'cwd') ?? '-'),
|
|
68
|
+
'preset ' + String(pick(cache, 'record', 'rows', 'agentPreset', 'val') ?? '-'),
|
|
69
|
+
'updated ' + stamp(row?.updatedAt ?? pick(cache, 'record', 'identity', 'createdAt')),
|
|
70
|
+
].join(NL);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function sendText(env, args, exec, info) {
|
|
74
|
+
const controller = env.controller();
|
|
75
|
+
if (!controller || typeof controller.prompt !== 'function') {
|
|
76
|
+
throw new Error('peer(action="send"):sessionController.prompt 不可用(插件未注入该服务)');
|
|
77
|
+
}
|
|
78
|
+
const target = String(args.session_id || '').trim();
|
|
79
|
+
if (!target) throw new Error('peer(action="send"):session_id 不能为空');
|
|
80
|
+
const message = typeof args.message === 'string' ? args.message : '';
|
|
81
|
+
if (message.trim() === '') throw new Error('peer(action="send"):message 不能为空');
|
|
82
|
+
const mode = args.mode === 'steer' ? 'steer' : 'queue';
|
|
83
|
+
// 来源标记:dsh 把宿主投递的消息记成 source.kind === "user",不加标记的话,
|
|
84
|
+
// 平级会话的消息在日志和界面上与用户消息完全无法区分。
|
|
85
|
+
const sender = exec?.agent?.session?.header?.id ?? 'unknown';
|
|
86
|
+
const marker = peerMarker(sender);
|
|
87
|
+
const body = message.startsWith(marker) ? message : marker + NL + message;
|
|
88
|
+
const receipt = await controller.prompt(
|
|
89
|
+
{ requestId: randomUUID(), sessionId: target, mode, content: [{ type: 'text', text: body }] },
|
|
90
|
+
new AbortController().signal,
|
|
91
|
+
);
|
|
92
|
+
return 'accepted=' + String(receipt.accepted) + ' mode=' + mode + ' target=' + target + ' from=' + sender;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function peekText(args) {
|
|
96
|
+
const sid = String(args.session_id || '').trim();
|
|
97
|
+
if (!sid) throw new Error('peer(action="peek"):session_id 不能为空');
|
|
98
|
+
const limit = Number.isFinite(args.limit) ? Math.max(1, Math.min(40, Math.trunc(args.limit))) : 8;
|
|
99
|
+
const file = findSessionLog(sid);
|
|
100
|
+
if (file === null) return 'no session log found for ' + sid;
|
|
101
|
+
const events = decodeSessionLog(file);
|
|
102
|
+
if (events === null) return 'this Node build has no zstd support; cannot read the session log';
|
|
103
|
+
const items = conversation(events, limit);
|
|
104
|
+
const head = 'session ' + sid + ' | 最近 ' + String(items.length) + ' 条 | ' + file;
|
|
105
|
+
if (items.length === 0) return head + NL + '(没有可读的会话历史)';
|
|
106
|
+
const body = items.map((it) => {
|
|
107
|
+
const one = it.text.replace(/\s+/g, ' ').trim();
|
|
108
|
+
const cut = one.length > 400 ? one.slice(0, 400) + ' …' : one;
|
|
109
|
+
return '[' + stamp(it.t) + ' ' + it.who + '] ' + cut;
|
|
110
|
+
}).join(NL);
|
|
111
|
+
return head + NL + body;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function renameText(env, args) {
|
|
115
|
+
const controller = env.controller();
|
|
116
|
+
if (!controller || typeof controller.rename !== 'function') {
|
|
117
|
+
throw new Error('peer(action="rename"):sessionController.rename 不可用(插件未注入该服务)');
|
|
118
|
+
}
|
|
119
|
+
const sid = String(args.session_id || '').trim();
|
|
120
|
+
if (!sid) throw new Error('peer(action="rename"):session_id 不能为空');
|
|
121
|
+
const title = String(args.title ?? '').trim();
|
|
122
|
+
if (title === '') throw new Error('peer(action="rename"):title 不能为空');
|
|
123
|
+
await controller.rename({ sessionId: sid, title });
|
|
124
|
+
return 'renamed ' + sid + ' -> ' + title;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function newText(env, args, info) {
|
|
128
|
+
const controller = env.controller();
|
|
129
|
+
if (!controller || typeof controller.create !== 'function') {
|
|
130
|
+
throw new Error('peer(action="new"):sessionController.create 不可用(插件未注入该服务)');
|
|
131
|
+
}
|
|
132
|
+
// 会话创建仅属全局;杂项可以把会话建在 Misc 下(落到本任务体系内)。
|
|
133
|
+
const wantsWorkspace = typeof args.workspace === 'string' && args.workspace.trim() !== '';
|
|
134
|
+
if (wantsWorkspace && args.cwd !== undefined) {
|
|
135
|
+
throw new Error('peer(action="new"):传 workspace 时不要再传 cwd —— session.create 只接受其中之一(cwd 会自动取该工作区的 path)');
|
|
136
|
+
}
|
|
137
|
+
const targetText = String(args.workspace || args.cwd || '').toLowerCase();
|
|
138
|
+
const allowed = info.role === 'global' || (info.role === 'misc' && targetText.indexOf('misc') !== -1);
|
|
139
|
+
denyUnless(allowed, 'new', '会话创建仅属全局(杂项可建到 Misc 下)');
|
|
140
|
+
let request = {};
|
|
141
|
+
if (wantsWorkspace) {
|
|
142
|
+
const workspaceId = resolveWorkspaceId(args.workspace);
|
|
143
|
+
if (workspaceId === null || workspaceId === undefined) {
|
|
144
|
+
throw new Error('peer(action="new"):找不到工作区「' + args.workspace + '」(可用 id / 标题 / 路径;数据源 storages/workspace.json)');
|
|
145
|
+
}
|
|
146
|
+
request = { workspaceId };
|
|
147
|
+
} else if (args.cwd !== undefined) {
|
|
148
|
+
request = { cwd: args.cwd };
|
|
149
|
+
}
|
|
150
|
+
const created = await controller.create(request);
|
|
151
|
+
const id = created.sessionId;
|
|
152
|
+
if (typeof args.title === 'string' && args.title.trim() !== '') {
|
|
153
|
+
try {
|
|
154
|
+
await controller.rename({ sessionId: id, title: args.title.trim() });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (typeof env.log === 'function') env.log('peer(action="new"):改标题失败:' + String(error));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let tail = '';
|
|
160
|
+
if (typeof args.message === 'string' && args.message.trim() !== '') {
|
|
161
|
+
await controller.prompt(
|
|
162
|
+
{ requestId: randomUUID(), sessionId: id, mode: 'queue', content: [{ type: 'text', text: args.message }] },
|
|
163
|
+
new AbortController().signal,
|
|
164
|
+
);
|
|
165
|
+
tail = ' (opening message delivered)';
|
|
166
|
+
}
|
|
167
|
+
return 'created ' + id + tail;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function pmPeerTool(env) {
|
|
171
|
+
return {
|
|
172
|
+
name: PEER_NAME,
|
|
173
|
+
description:
|
|
174
|
+
'平级会话:action=list 一览、status 详情、send 投消息(耗对方 token)、peek 读近期会话历史(耗本会话 token)、rename 改标题、new 新建。list、peek、rename 与 new 仅全局会话可用。',
|
|
175
|
+
parameters: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
required: ['action'],
|
|
179
|
+
properties: {
|
|
180
|
+
action: { type: 'string', enum: ACTIONS, description: '要做的动作' },
|
|
181
|
+
session_id: { type: 'string', description: 'status、send、peek、rename:目标会话 id' },
|
|
182
|
+
message: { type: 'string', description: 'send:要投递的正文' },
|
|
183
|
+
mode: { type: 'string', enum: ['queue', 'steer'], description: 'send:queue(默认)或 steer' },
|
|
184
|
+
limit: { type: 'number', description: 'list:最多多少行(默认 40);peek:最近多少条(默认 8,上限 40)' },
|
|
185
|
+
include_subagents: { type: 'boolean', description: 'list:同时列出子会话' },
|
|
186
|
+
title: { type: 'string', description: 'new、rename:显示标题' },
|
|
187
|
+
workspace: { type: 'string', description: 'new:工作区 id、标题或路径(与 cwd 互斥)' },
|
|
188
|
+
cwd: { type: 'string', description: 'new:新会话的工作目录' },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
output: textOutput(),
|
|
192
|
+
async execute(args, exec) {
|
|
193
|
+
const action = String(args.action || '');
|
|
194
|
+
if (ACTIONS.indexOf(action) === -1) throw new Error('peer:action 只能是 ' + ACTIONS.join(' | ') + '(收到 ' + JSON.stringify(action) + ')');
|
|
195
|
+
const info = roleOf(env, exec);
|
|
196
|
+
if (GLOBAL_ONLY.indexOf(action) !== -1) denyUnless(info.role === 'global', action, '该动作仅全局会话可用');
|
|
197
|
+
if (action === 'list') return { text: await listText(env, args) };
|
|
198
|
+
if (action === 'status') return { text: await statusText(env, args) };
|
|
199
|
+
if (action === 'send') return { text: await sendText(env, args, exec, info) };
|
|
200
|
+
if (action === 'peek') return { text: await peekText(args) };
|
|
201
|
+
if (action === 'rename') return { text: await renameText(env, args) };
|
|
202
|
+
return { text: await newText(env, args, info) };
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/tools/ho_brief.mjs
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/** ho_brief —— 一条命令汇总:水位 + 闸门 + 会话清点 + 阶段建议。 */
|
|
2
|
-
import { textOutput } from '../lib/output.mjs';
|
|
3
|
-
import { nameOf } from '../lib/names.mjs';
|
|
4
|
-
import { NL } from '../lib/text.mjs';
|
|
5
|
-
import { syncCheck } from '../lib/gates.mjs';
|
|
6
|
-
import { scanSessions, readSelfPressure } from '../lib/sessions.mjs';
|
|
7
|
-
import { composeBrief } from '../lib/brief.mjs';
|
|
8
|
-
import { decide } from '../lib/phase.mjs';
|
|
9
|
-
import { computeDrift } from '../lib/drift.mjs';
|
|
10
|
-
import { watermark } from '../lib/facts.mjs';
|
|
11
|
-
|
|
12
|
-
export function pmBriefTool(env) {
|
|
13
|
-
const w = watermark(env.cfg);
|
|
14
|
-
return {
|
|
15
|
-
name: nameOf('brief'),
|
|
16
|
-
description:
|
|
17
|
-
'起始与收束的单条命令:水位、闸门、会话清点、阶段建议。只读。',
|
|
18
|
-
parameters: {
|
|
19
|
-
type: 'object',
|
|
20
|
-
additionalProperties: false,
|
|
21
|
-
properties: {
|
|
22
|
-
threshold: { type: 'number', description: '预警阈值百分比(默认 ' + w.warnValue + ')' },
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
output: textOutput(),
|
|
26
|
-
async execute(args, exec) {
|
|
27
|
-
const ws = env.resolveWs(exec);
|
|
28
|
-
const warn = typeof args.threshold === "number" ? args.threshold : w.warnValue;
|
|
29
|
-
const gate = syncCheck(ws);
|
|
30
|
-
const meId = env.selfId(exec);
|
|
31
|
-
const self = readSelfPressure(env.cacheDir(), meId);
|
|
32
|
-
const scan = scanSessions({
|
|
33
|
-
dir: env.cacheDir(),
|
|
34
|
-
meId,
|
|
35
|
-
meCwd: env.selfCwd(exec),
|
|
36
|
-
warn,
|
|
37
|
-
silentHours: env.cfg.sessionScanHours,
|
|
38
|
-
});
|
|
39
|
-
const drift = computeDrift({ ws, cacheDir: env.cacheDir(), slack: env.cfg.driftSlack, all: false });
|
|
40
|
-
const ratio = self.used === null || self.win === null ? null : self.used / self.win;
|
|
41
|
-
const adv = decide({ ratio, compactionsUsed: 0, cfg: env.cfg, lockState: "unknown" });
|
|
42
|
-
const selfPct = self.used === null || self.win === null ? null : (self.used / self.win) * 100;
|
|
43
|
-
const ok = gate.ok && (selfPct === null || selfPct < warn);
|
|
44
|
-
const lines = composeBrief({
|
|
45
|
-
cfg: env.cfg,
|
|
46
|
-
selfUsed: self.used,
|
|
47
|
-
selfWin: self.win,
|
|
48
|
-
gate,
|
|
49
|
-
scan,
|
|
50
|
-
drift,
|
|
51
|
-
advice: { text: adv.action + " —— " + adv.reason },
|
|
52
|
-
ok,
|
|
53
|
-
});
|
|
54
|
-
return { text: lines.join(NL) };
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
package/src/tools/ho_diag.mjs
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/** ho_diag —— 诊断:服务是否可解析、调度器是否就绪、配置是否如预期。不写任何东西。 */
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import { nameOf } from '../lib/names.mjs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { textOutput } from '../lib/output.mjs';
|
|
6
|
-
import { NL } from '../lib/text.mjs';
|
|
7
|
-
import { eventsOf, countCompactions } from '../lib/events.mjs';
|
|
8
|
-
|
|
9
|
-
export function pmDiagTool(env, state) {
|
|
10
|
-
return {
|
|
11
|
-
name: nameOf('diag'),
|
|
12
|
-
description:
|
|
13
|
-
'插件自检:服务、调度器、压缩计数、工作区是否接入。只读。',
|
|
14
|
-
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
15
|
-
output: textOutput(),
|
|
16
|
-
async execute(args, exec) {
|
|
17
|
-
const out = [];
|
|
18
|
-
const meId = env.selfId(exec);
|
|
19
|
-
out.push("会话 " + (meId || "?"));
|
|
20
|
-
if (typeof env.version === "function") out.push("版本 " + env.version());
|
|
21
|
-
out.push("调度器 " + (state.schedulerReady() ? "就绪" : "未就绪" + (state.schedulerError() ? ":" + state.schedulerError() : "(稍后重试或重启)")));
|
|
22
|
-
if (typeof env.injectList === 'function') out.push('inject ' + env.injectList().join(' / '));
|
|
23
|
-
if (typeof env.initErrors === 'function') {
|
|
24
|
-
const errs = env.initErrors();
|
|
25
|
-
out.push('初始化 ' + (errs.length ? '有 ' + errs.length + ' 处失败:' + errs.join(';') : '无错误'));
|
|
26
|
-
}
|
|
27
|
-
if (typeof env.probeServices === "function") {
|
|
28
|
-
const svc = env.probeServices();
|
|
29
|
-
out.push("服务 " + Object.keys(svc).map((k) => k + "=" + (svc[k] ? "可用" : "—")).join(" "));
|
|
30
|
-
}
|
|
31
|
-
if (typeof env.guardLast === "function") {
|
|
32
|
-
const g = env.guardLast();
|
|
33
|
-
out.push("唤醒判定 " + (g
|
|
34
|
-
? (g.allow ? "放行" : "拦截") + " · 依据 " + g.source + (g.title ? " · 标题 " + g.title : "") + " · 会话 " + g.sid
|
|
35
|
-
: "本进程还没有 peer_send、peer_new 调用"));
|
|
36
|
-
}
|
|
37
|
-
if (typeof env.probeAgentScope === "function") {
|
|
38
|
-
const a = env.probeAgentScope(exec);
|
|
39
|
-
const show = ["compaction", "compactNow", "tokenMeter", "sessions", "llm", "agents"]
|
|
40
|
-
.map((k) => k + "=" + (a[k] ? "可用" : "—")).join(" ");
|
|
41
|
-
out.push("会话作用域 " + (a.ctx ? show : "无法获取 exec.agent.ctx"));
|
|
42
|
-
}
|
|
43
|
-
const st = typeof env.compactionState === "function"
|
|
44
|
-
? env.compactionState(exec)
|
|
45
|
-
: (() => { const c = countCompactions(eventsOf(exec)); return { used: c.used, lock: c.lock, source: 'events' }; })();
|
|
46
|
-
out.push("压缩 " + st.used + "/" + env.cfg.maxCompactions + " 次;锁 " + st.lock + ";来源 " + st.source);
|
|
47
|
-
const pct = env.selfPct(meId);
|
|
48
|
-
out.push("水位 " + (pct === null ? "读不到" : pct.toFixed(1) + "%") + (typeof env.staleNote === "function" ? env.staleNote(exec) : ""));
|
|
49
|
-
const ws = env.resolveWs(exec);
|
|
50
|
-
let wsOk = false;
|
|
51
|
-
try { wsOk = fs.existsSync(path.join(ws, '_Control_Center')); } catch { wsOk = false; }
|
|
52
|
-
if (typeof env.roleOf === "function") {
|
|
53
|
-
try {
|
|
54
|
-
const info = env.roleOf(exec);
|
|
55
|
-
const ts = typeof env.toolsetOf === 'function' ? env.toolsetOf(info.role) : null;
|
|
56
|
-
out.push("角色 " + info.role + (info.project ? "(项目 " + info.project + ")" : info.task ? "(任务 " + info.task + ")" : "") + " · 依据 " + info.source + (ts ? " · 可见工具 " + ts.allow.length + "/" + (ts.allow.length + ts.deny.length) : ""));
|
|
57
|
-
if (typeof env.roleState === "function") {
|
|
58
|
-
const rs = env.roleState();
|
|
59
|
-
const cnt = typeof rs.count === "number" ? " · 已装配 " + rs.count + " 个会话" : "";
|
|
60
|
-
out.push("角色装配 " + (rs.mode || "未知") + cnt + (rs.last ? " · 最近:" + rs.last.sid + " → " + rs.last.role + "(" + rs.last.at + ")" : ""));
|
|
61
|
-
}
|
|
62
|
-
} catch (e) {
|
|
63
|
-
out.push("角色 判定失败:" + (e && e.message ? e.message : e));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
out.push("工作区 " + ws + (wsOk
|
|
67
|
-
? " 已接入"
|
|
68
|
-
: " — 未接入(未找到 _Control_Center):让用户走设置页「首次安装」,或本会话直接 ho_init(parent=\"<目录>\", register=true)"));
|
|
69
|
-
try {
|
|
70
|
-
const mv = typeof env.moveInfo === "function" ? env.moveInfo() : null;
|
|
71
|
-
if (mv) out.push("迁移 " + mv.phase + " · " + mv.src + (mv.dst ? " → " + mv.dst : ""));
|
|
72
|
-
} catch { /* ignore */ }
|
|
73
|
-
out.push("配置 warn=" + env.cfg.warnRatio + " handover=" + env.cfg.handoverRatio + " hard=" + env.cfg.hardRatio + " max=" + env.cfg.maxCompactions);
|
|
74
|
-
if (typeof env.handoverFor === "function") {
|
|
75
|
-
try {
|
|
76
|
-
const hp = env.handoverFor(exec);
|
|
77
|
-
out.push("交接路径 " + (hp && hp.path ? hp.path + "(" + hp.kind + ")" : "无 —— " + ((hp && hp.reason) || "角色未定")));
|
|
78
|
-
} catch (e) {
|
|
79
|
-
out.push("交接路径 解析失败:" + (e && e.message ? e.message : e));
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
out.push("交接路径 " + (env.cfg.handoverPath || "(按角色解析)"));
|
|
83
|
-
}
|
|
84
|
-
return { text: out.join(NL) };
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
package/src/tools/ho_drift.mjs
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/** ho_drift —— 交接漂移体检(替代 handover_check.mjs)。 */
|
|
2
|
-
import { textOutput } from '../lib/output.mjs';
|
|
3
|
-
import { nameOf } from '../lib/names.mjs';
|
|
4
|
-
import { NL } from '../lib/text.mjs';
|
|
5
|
-
import { computeDrift, driftVerdict } from '../lib/drift.mjs';
|
|
6
|
-
|
|
7
|
-
export function pmDriftTool(env) {
|
|
8
|
-
return {
|
|
9
|
-
name: nameOf('drift'),
|
|
10
|
-
description:
|
|
11
|
-
'交接漂移体检:Δ=当前轮−交接轮,超过容忍轮数(默认 3)需刷新交接。只读。',
|
|
12
|
-
parameters: {
|
|
13
|
-
type: 'object',
|
|
14
|
-
additionalProperties: false,
|
|
15
|
-
properties: {
|
|
16
|
-
slack: { type: 'number', description: '容忍轮数(默认 3)' },
|
|
17
|
-
all: { type: 'boolean', description: '列全部会话,而不只是每个项目的在任会话' },
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
output: textOutput(),
|
|
21
|
-
async execute(args, exec) {
|
|
22
|
-
const ws = env.resolveWs(exec);
|
|
23
|
-
const res = computeDrift({
|
|
24
|
-
ws,
|
|
25
|
-
cacheDir: env.cacheDir(),
|
|
26
|
-
slack: typeof args.slack === 'number' ? args.slack : env.cfg.driftSlack,
|
|
27
|
-
all: args.all === true,
|
|
28
|
-
});
|
|
29
|
-
const out = [];
|
|
30
|
-
out.push('交接匹配检查(跟着交接走;冗余 ' + res.slack + ' 轮) 工作区根 ' + ws);
|
|
31
|
-
out.push(' 项目 会话类型 会话 当前轮 交接轮 Δ 判定');
|
|
32
|
-
for (const r of res.rows) {
|
|
33
|
-
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];
|
|
34
|
-
out.push(' ' + cells.join(' '));
|
|
35
|
-
}
|
|
36
|
-
if (!res.rows.length) out.push(' 读不到任何项目会话(Projects/ 下对应库不存在时属正常)');
|
|
37
|
-
out.push(driftVerdict(res));
|
|
38
|
-
return { text: out.join(NL) };
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
package/src/tools/ho_init.mjs
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ho_init —— 首次安装:在用户给的目录里建「全局文件体系」,可选顺带注册为工作区。
|
|
3
|
-
* 与设置页的「首次安装向导」同一套实现(lib/scaffold.mjs),因此两条路径行为一致。
|
|
4
|
-
*/
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import { nameOf } from '../lib/names.mjs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { textOutput } from '../lib/output.mjs';
|
|
9
|
-
import { NL } from '../lib/text.mjs';
|
|
10
|
-
import { createGlobalWorkspace } from '../lib/scaffold.mjs';
|
|
11
|
-
|
|
12
|
-
export function pmInitTool(env) {
|
|
13
|
-
return {
|
|
14
|
-
name: nameOf('init'),
|
|
15
|
-
description:
|
|
16
|
-
'首次安装:在指定目录(不存在会创建)下建全局文件体系(根为 <目录>/_Workspace),可顺带注册工作区。',
|
|
17
|
-
parameters: {
|
|
18
|
-
type: 'object',
|
|
19
|
-
additionalProperties: false,
|
|
20
|
-
required: ['parent'],
|
|
21
|
-
properties: {
|
|
22
|
-
parent: { type: 'string', description: '目标父目录绝对路径(不存在会创建);全局根建在它里面' },
|
|
23
|
-
name: { type: 'string', description: '全局根目录名,默认 _Workspace' },
|
|
24
|
-
register: { type: 'boolean', description: '建好后顺带注册为工作区(默认 false)' },
|
|
25
|
-
title: { type: 'string', description: 'register 时的工作区标题,默认「全局」' },
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
output: textOutput(),
|
|
29
|
-
async execute(args) {
|
|
30
|
-
const r = createGlobalWorkspace(args.parent, { name: args.name });
|
|
31
|
-
const out = [];
|
|
32
|
-
if (!r.ok) {
|
|
33
|
-
out.push('未建立:' + r.error);
|
|
34
|
-
if (r.root) out.push('目标:' + r.root);
|
|
35
|
-
out.push('');
|
|
36
|
-
out.push('用法:ho_init(parent="<已存在的目录绝对路径>")。插件只新建、不覆盖。');
|
|
37
|
-
return { text: out.join(NL) };
|
|
38
|
-
}
|
|
39
|
-
out.push('已建立全局文件体系:' + r.root);
|
|
40
|
-
out.push(' 目录 ' + r.created.length + ' 个:' + r.created.slice(0, 5).join('、') + (r.created.length > 5 ? ' 等' : ''));
|
|
41
|
-
out.push(' 文件 ' + r.written.length + ' 个:' + (r.written.length ? r.written.join('、') : '(都已存在,未覆盖)'));
|
|
42
|
-
if (r.existed) out.push(' 目标目录原为空目录,直接使用。');
|
|
43
|
-
if (r.createdParent) out.push(' 父目录原本不存在,已创建。');
|
|
44
|
-
if (r.skipped.length) out.push(' 跳过(已存在、未覆盖):' + r.skipped.join('、'));
|
|
45
|
-
|
|
46
|
-
let registered = null;
|
|
47
|
-
if (args.register === true) {
|
|
48
|
-
const reg = env.registry();
|
|
49
|
-
if (!reg || typeof reg.create !== 'function') {
|
|
50
|
-
out.push('');
|
|
51
|
-
out.push('注册未完成:workspaceRegistry 不可用。请把下面这个路径粘进 DSH 的新建工作区窗口:');
|
|
52
|
-
out.push(' ' + r.root);
|
|
53
|
-
return { text: out.join(NL) };
|
|
54
|
-
}
|
|
55
|
-
const title = String(args.title || '').trim() || '全局';
|
|
56
|
-
try {
|
|
57
|
-
const w = await reg.create(r.root, title);
|
|
58
|
-
registered = w;
|
|
59
|
-
out.push('');
|
|
60
|
-
out.push('已注册工作区:' + title + '(' + String(w && w.id ? w.id : '?') + ')');
|
|
61
|
-
} catch (e) {
|
|
62
|
-
out.push('');
|
|
63
|
-
out.push('注册失败:' + String(e && e.message ? e.message : e));
|
|
64
|
-
out.push('可把下面这个路径粘进 DSH 的新建工作区窗口:');
|
|
65
|
-
out.push(' ' + r.root);
|
|
66
|
-
return { text: out.join(NL) };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
out.push('');
|
|
71
|
-
out.push('下一步:');
|
|
72
|
-
if (!registered) {
|
|
73
|
-
out.push(' 1. 注册为工作区:ho_workspace(action="create", path="' + r.root + '", title="全局"),或把该路径粘进 DSH 的新建工作区窗口;');
|
|
74
|
-
out.push(' 2. 新建一个标题含「全局」的会话,闸门、交接体检、会话水位即接入。');
|
|
75
|
-
} else {
|
|
76
|
-
out.push(' 新建一个标题含「全局」的会话,闸门、交接体检、会话水位即接入。');
|
|
77
|
-
}
|
|
78
|
-
out.push('(也可以在设置页的 dsh-handover 中完成「首次安装」,效果相同。)');
|
|
79
|
-
return { text: out.join(NL) };
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
}
|