@mingmaple/dsh-handover 0.9.14 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -92
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +34 -0
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +230 -46
- package/package.json +74 -71
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +41 -25
- package/scripts/blank-smoke.mjs +278 -109
- package/scripts/check-docs.mjs +7 -0
- package/scripts/e2e-migration.mjs +2 -2
- package/scripts/office.mjs +3 -3
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -955
- package/src/index.mjs +313 -142
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +18 -10
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +23 -8
- package/src/lib/consent.mjs +54 -1
- package/src/lib/docmap.mjs +140 -0
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +30 -6
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +81 -85
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +17 -13
- package/src/lib/manual.mjs +135 -59
- package/src/lib/mover.mjs +23 -19
- package/src/lib/names.mjs +14 -19
- package/src/lib/office.mjs +63 -15
- package/src/lib/pack-ops.mjs +11 -11
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/peer.mjs +4 -4
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +9 -6
- package/src/lib/scaffold.mjs +68 -26
- package/src/lib/schedule.mjs +5 -6
- package/src/lib/session-create.mjs +3 -3
- package/src/lib/session-ops.mjs +80 -51
- package/src/lib/sessions.mjs +41 -3
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -176
- package/src/lib/succession.mjs +16 -16
- package/src/lib/system.mjs +364 -0
- package/src/lib/zstd-log.mjs +1 -1
- package/src/selftest.mjs +6 -6
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +101 -20
- package/src/templates/Runtime_Manual.md +263 -122
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -23
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +37 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +135 -0
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +36 -32
- package/src/tools/ho_docmap.mjs +239 -0
- package/src/tools/ho_gate.mjs +11 -5
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +46 -5
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +46 -26
- package/src/tools/ho_status.mjs +251 -0
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +146 -42
- package/src/tools/peer.mjs +416 -0
- package/src/tools/ho_brief.mjs +0 -57
- package/src/tools/ho_diag.mjs +0 -87
- package/src/tools/ho_drift.mjs +0 -41
- package/src/tools/ho_init.mjs +0 -82
- package/src/tools/ho_pack.mjs +0 -38
- package/src/tools/ho_phase.mjs +0 -37
- package/src/tools/ho_snapshot.mjs +0 -104
- package/src/tools/ho_tokens.mjs +0 -57
- package/src/tools/peer_list.mjs +0 -37
- package/src/tools/peer_new.mjs +0 -75
- package/src/tools/peer_peek.mjs +0 -41
- package/src/tools/peer_rename.mjs +0 -31
- package/src/tools/peer_send.mjs +0 -52
- package/src/tools/peer_status.mjs +0 -46
package/src/lib/session-ops.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import fs from 'node:fs';
|
|
7
8
|
import { NL, readUtf8, isDir, isFile } from './text.mjs';
|
|
8
9
|
import { handoverPathFor, assertProjectName, assertTaskName } from './role.mjs';
|
|
9
10
|
import { readHandover } from './handover-io.mjs';
|
|
@@ -13,11 +14,19 @@ import { watermark } from './facts.mjs';
|
|
|
13
14
|
import { sendPrompt } from './prompt.mjs';
|
|
14
15
|
import { newSessionIn } from './session-create.mjs';
|
|
15
16
|
import { lineageKeyOf, loadOffice, registerHolder } from './office.mjs';
|
|
17
|
+
import { migrateIdentity } from './identity.mjs';
|
|
18
|
+
import { pathKey } from './pathkey.mjs';
|
|
16
19
|
|
|
17
20
|
const FENCE = String.fromCharCode(96, 96, 96);
|
|
18
21
|
|
|
19
22
|
function normPath(p) {
|
|
20
|
-
return String(p || '')
|
|
23
|
+
return pathKey(String(p || ''));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 路径比较:先比写法;再取 realpath 比,兼容旧的 junction 路径与真实路径。 */
|
|
27
|
+
function sameLoosePath(a, b) {
|
|
28
|
+
if (normPath(a) === normPath(b)) return true;
|
|
29
|
+
try { return normPath(fs.realpathSync(String(a))) === normPath(fs.realpathSync(String(b))); } catch { return false; }
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
/** 在 ~/.dsh/storages/workspace.json 里按 id、标题、路径找 workspaceId。 */
|
|
@@ -25,12 +34,11 @@ function findWorkspaceId(key) {
|
|
|
25
34
|
const file = path.join(os.homedir(), '.dsh', 'storages', 'workspace.json');
|
|
26
35
|
const data = JSON.parse(readUtf8(file));
|
|
27
36
|
const tables = (data && data.tables && data.tables.workspaces) || {};
|
|
28
|
-
const want = normPath(key);
|
|
29
37
|
for (const id of Object.keys(tables)) {
|
|
30
38
|
const w = tables[id] || {};
|
|
31
39
|
if (id === key) return id;
|
|
32
40
|
if (w.title === key) return id;
|
|
33
|
-
if (
|
|
41
|
+
if (sameLoosePath(w.path, key)) return id;
|
|
34
42
|
}
|
|
35
43
|
return null;
|
|
36
44
|
}
|
|
@@ -56,7 +64,7 @@ export function resolveWorkspaceFor(env, exec, kind, project, task, opts) {
|
|
|
56
64
|
const pickWorkspace = (key) => {
|
|
57
65
|
const wid = findWorkspaceId(key);
|
|
58
66
|
if (!wid) {
|
|
59
|
-
throw new Error('找不到工作区:' + key + NL + '已注册的工作区:' + NL + choices + NL + '
|
|
67
|
+
throw new Error('找不到工作区:' + key + NL + '已注册的工作区:' + NL + choices + NL + '可传 id、标题或路径;如需新建工作区,用 ho_workspace(action="create", path="<目录>")。');
|
|
60
68
|
}
|
|
61
69
|
return wid;
|
|
62
70
|
};
|
|
@@ -68,8 +76,8 @@ export function resolveWorkspaceFor(env, exec, kind, project, task, opts) {
|
|
|
68
76
|
let wid = null;
|
|
69
77
|
try { wid = findWorkspaceId(o.cwd); } catch { wid = null; }
|
|
70
78
|
if (!wid) {
|
|
71
|
-
throw new Error('cwd
|
|
72
|
-
+ NL + '
|
|
79
|
+
throw new Error('cwd 不在任何已注册工作区中:' + o.cwd + NL + '已注册的工作区:' + NL + choices
|
|
80
|
+
+ NL + '处置方式:把 cwd 改为某个工作区的根,或先注册该目录:ho_workspace(action="create", path="' + o.cwd + '")。');
|
|
73
81
|
}
|
|
74
82
|
return wid;
|
|
75
83
|
}
|
|
@@ -82,16 +90,16 @@ export function resolveWorkspaceFor(env, exec, kind, project, task, opts) {
|
|
|
82
90
|
return p.indexOf('/projects/' + want) !== -1 || p.indexOf('/_project_meta/' + want) !== -1;
|
|
83
91
|
});
|
|
84
92
|
if (!hit) {
|
|
85
|
-
throw new Error('项目「' + project + '
|
|
86
|
-
+ NL + '
|
|
87
|
-
+ NL + '
|
|
93
|
+
throw new Error('项目「' + project + '」尚无对应工作区。' + NL + '已注册的工作区:' + NL + choices
|
|
94
|
+
+ NL + '方案一:先 ho_workspace(action="create", path="<该项目目录>", title="' + project + '"),再建会话;'
|
|
95
|
+
+ NL + '方案二:把 workspace 传成上列任一工作区。');
|
|
88
96
|
}
|
|
89
97
|
return String(hit.id);
|
|
90
98
|
}
|
|
91
99
|
if (wantGlobalWs) return pickWorkspace(ws);
|
|
92
|
-
throw new Error('
|
|
100
|
+
throw new Error('新建会话须指明工作区:传 workspace(id、标题或路径),或 cwd(某个已注册工作区的根)。' + NL
|
|
93
101
|
+ '已注册的工作区:' + NL + choices + NL
|
|
94
|
-
+ '全局会话落在标题含「全局」的工作区;项目会话同时传 project
|
|
102
|
+
+ '全局会话落在标题含「全局」的工作区;项目会话同时传 project 与该项目的工作区;杂项会话传该项目之外的独立工作区。');
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
/**
|
|
@@ -102,8 +110,8 @@ export function workspaceIdOfPath(env, p) {
|
|
|
102
110
|
const want = normPath(p);
|
|
103
111
|
if (!want || want === '-') return '';
|
|
104
112
|
const hit = listWorkspaces(env).find((r) => {
|
|
105
|
-
const rp =
|
|
106
|
-
return rp && rp !== '-' && rp
|
|
113
|
+
const rp = String(r.path || '');
|
|
114
|
+
return rp && rp !== '-' && sameLoosePath(rp, p);
|
|
107
115
|
});
|
|
108
116
|
return hit ? String(hit.id || '') : '';
|
|
109
117
|
}
|
|
@@ -173,17 +181,17 @@ function reuseBlocker(env, ws, kind, key) {
|
|
|
173
181
|
if (!cur || !cur.sessionId) return '';
|
|
174
182
|
let pct = null;
|
|
175
183
|
try { pct = typeof env.selfPct === 'function' ? env.selfPct(cur.sessionId) : null; } catch { pct = null; }
|
|
176
|
-
if (pct === null || pct === undefined || !(Number(pct) < watermark(env.cfg).
|
|
184
|
+
if (pct === null || pct === undefined || !(Number(pct) < watermark(env.cfg).reuseValue)) return '';
|
|
177
185
|
const label = kind === 'global' ? '全局类别' : kind === 'project' ? '项目「' + key + '」' : '杂项任务「' + key + '」';
|
|
178
|
-
return '拒绝新建:' + label + '已有在任会话 ' + (cur.name || '(未命名)') + '(' + cur.sessionId + '
|
|
179
|
-
+ Number(pct).toFixed(1) + '%,低于 ' + watermark(env.cfg).
|
|
186
|
+
return '拒绝新建:' + label + '已有在任会话 ' + (cur.name || '(未命名)') + '(' + cur.sessionId + '),水位 '
|
|
187
|
+
+ Number(pct).toFixed(1) + '%,低于 ' + watermark(env.cfg).handover + '。建议用 peer(action="send")复用;确需另建,请传 force=true。';
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
export async function sessionNew(env, args, exec) {
|
|
183
191
|
const ws = env.resolveWs(exec);
|
|
184
192
|
const controller = env.controller();
|
|
185
193
|
// 换任(新建接班会话)先问用户。
|
|
186
|
-
if (args.confirm !== true) throw new Error('
|
|
194
|
+
if (args.confirm !== true) throw new Error('新建会话(换任)须经当前用户确认:先向当前用户说明将建立接任会话并取得同意,再传 confirm=true');
|
|
187
195
|
if (!controller) throw new Error('ctx.sessionController 不可用');
|
|
188
196
|
// 名字要落成目录:先校验,别等会话建完、写角色表时才失败(那样会留下一个没有角色的会话)。
|
|
189
197
|
if (args.project) assertProjectName(String(args.project));
|
|
@@ -205,6 +213,24 @@ export async function sessionNew(env, args, exec) {
|
|
|
205
213
|
if (block) throw new Error(block);
|
|
206
214
|
}
|
|
207
215
|
|
|
216
|
+
// 全局任务整体独立:先把任务库移入项目或杂项位置,沿用现有体系,不重建、不重新注册。
|
|
217
|
+
let migrated = null;
|
|
218
|
+
if (args.from_task) {
|
|
219
|
+
let caller = null;
|
|
220
|
+
try { caller = env.roleOf(exec); } catch { caller = null; }
|
|
221
|
+
if (!caller || caller.role !== 'global') throw new Error('from_task 只对全局会话开放');
|
|
222
|
+
const oldTask = String(args.from_task).trim();
|
|
223
|
+
assertTaskName(oldTask);
|
|
224
|
+
if (finalRole !== 'project' && finalRole !== 'misc') throw new Error('from_task 须同时给 role=project 或 role=misc');
|
|
225
|
+
migrated = migrateIdentity({
|
|
226
|
+
ws,
|
|
227
|
+
prev: { role: 'global-task', task: oldTask },
|
|
228
|
+
next: { role: finalRole, project, task },
|
|
229
|
+
mode: 'auto',
|
|
230
|
+
});
|
|
231
|
+
out.push('全局任务已独立:' + migrated.summary);
|
|
232
|
+
}
|
|
233
|
+
|
|
208
234
|
// ---- 工作区解析:新建会话必须落到明确的工作区,不允许静默落进调用者所在的工作区。
|
|
209
235
|
const workspaceId = resolveWorkspaceFor(env, exec, finalRole, project, task, {
|
|
210
236
|
workspace: args.workspace,
|
|
@@ -238,10 +264,13 @@ export async function sessionNew(env, args, exec) {
|
|
|
238
264
|
|| (finalRole === 'misc' ? path.join(ws, '_Control_Center', 'rules', 'Misc_Task_Standard.md') : '');
|
|
239
265
|
const text = args.message || [
|
|
240
266
|
'先读交接:' + handover + '。',
|
|
241
|
-
'
|
|
267
|
+
'读毕先向当前用户报告,不得自行开展作业。报告 3 项:一、当前停在哪一步;二、交接列有哪些待办;三、建议先做哪一条。',
|
|
268
|
+
'待当前用户指派后再开始作业。交接无待办时如实说明,不得编造任务。',
|
|
242
269
|
'本会话 id:' + sessionId,
|
|
243
270
|
(finalRole === 'project' ? '全局会话 id:' : '创建者会话 id:') + meId,
|
|
244
|
-
'
|
|
271
|
+
'默认沉默:仅在下列三种时机向全局发送一条 peer(action="send"):水位不低于 ' + watermark(env.cfg).remind + ' 且已写交接;需当前用户决定;全局口径有误。',
|
|
272
|
+
'任务完成不报全局,任务属本会话内部事务。会话之间可直接 peer(action="send"),不必经全局转述。',
|
|
273
|
+
'不得每轮回执,不得发送「收到」「已完成」一类确认;全局以文件末态为准。',
|
|
245
274
|
].join(NL);
|
|
246
275
|
const r = await sendPrompt(env.controller(), sessionId, text, 'queue');
|
|
247
276
|
out.push('已投启动提示词 accepted=' + String(r && r.accepted) + '(' + text.length + ' 字)');
|
|
@@ -292,7 +321,7 @@ export function promptFor(env, opts) {
|
|
|
292
321
|
const roleText = kind === 'global'
|
|
293
322
|
? '第 ' + ordinal + ' 任全局(管理会话)'
|
|
294
323
|
: kind === 'project'
|
|
295
|
-
? '「' + project + '
|
|
324
|
+
? '「' + project + '」项目的执行会话'
|
|
296
325
|
: kind === 'misc' ? '杂项任务会话' : '新机器接手会话';
|
|
297
326
|
const scopeText = kind === 'project'
|
|
298
327
|
? '库根 Projects/' + project + ' 与 _Project_Meta/' + project
|
|
@@ -316,11 +345,11 @@ export function promptFor(env, opts) {
|
|
|
316
345
|
const extraLines = [];
|
|
317
346
|
if (kind === 'project' && project) {
|
|
318
347
|
extraLines.push('库根:Projects/' + project + '/;元数据:_Project_Meta/' + project + '/;快照:_Temp/' + project + '/Snapshots/');
|
|
319
|
-
extraLines.push('作业前读库根 SKILL.md;修改文档时另读 _Control_Center/rules/
|
|
320
|
-
extraLines.push('
|
|
348
|
+
extraLines.push('作业前读库根 SKILL.md;修改文档时另读 _Control_Center/rules/ 下的对应规范。');
|
|
349
|
+
extraLines.push('进入预警区后写交接。换任须由当前用户在聊天框发送 /allow-succeed 授权,再由本会话调用 ho_succeed 完成,不向全局请求。');
|
|
321
350
|
}
|
|
322
|
-
if (kind === 'misc') extraLines.push('标准见 rules/Misc_Task_Standard.md
|
|
323
|
-
if (kind === 'migration') extraLines.push('迁移口径见 rules/Migration_Rules.md
|
|
351
|
+
if (kind === 'misc') extraLines.push('标准见 rules/Misc_Task_Standard.md。换任须经当前用户授权,再由本会话调用 ho_succeed。');
|
|
352
|
+
if (kind === 'migration') extraLines.push('迁移口径见 rules/Migration_Rules.md。通过顶层闸门后再开始作业。');
|
|
324
353
|
if (o.extra) extraLines.push(String(o.extra));
|
|
325
354
|
return [body].concat(extraLines.length ? ['', ...extraLines] : []).join(NL);
|
|
326
355
|
}
|
|
@@ -344,7 +373,7 @@ export function sessionPrompt(env, args, exec) {
|
|
|
344
373
|
export async function sessionArchive(env, args, exec) {
|
|
345
374
|
const reg = env.registry();
|
|
346
375
|
if (!reg || typeof reg.archiveSession !== 'function') {
|
|
347
|
-
throw new Error('workspaceRegistry.archiveSession 不可用(先跑
|
|
376
|
+
throw new Error('workspaceRegistry.archiveSession 不可用(先跑 ho_status(view=diag) 看服务可见性)');
|
|
348
377
|
}
|
|
349
378
|
const sid = String(args.session_id || '').trim();
|
|
350
379
|
if (!sid) throw new Error('必须给 session_id');
|
|
@@ -363,21 +392,21 @@ export async function sessionArchive(env, args, exec) {
|
|
|
363
392
|
const archived = reg.archivedSessionIds;
|
|
364
393
|
if (archived && typeof archived.length === 'number') out.push('归档集合现有 ' + archived.length + ' 个');
|
|
365
394
|
} catch { /* ignore */ }
|
|
366
|
-
out.push('提醒:归档会话会被 archived-session-gate
|
|
395
|
+
out.push('提醒:归档会话会被 archived-session-gate 拦截,无法唤醒;如需使用,请先执行 action="unarchive"。');
|
|
367
396
|
return out.join(NL);
|
|
368
397
|
}
|
|
369
398
|
|
|
370
399
|
export async function sessionUnarchive(env, args) {
|
|
371
400
|
const reg = env.registry();
|
|
372
401
|
if (!reg || typeof reg.unarchiveSession !== 'function') {
|
|
373
|
-
throw new Error('workspaceRegistry.unarchiveSession 不可用(先跑
|
|
402
|
+
throw new Error('workspaceRegistry.unarchiveSession 不可用(先跑 ho_status(view=diag) 看服务可见性)');
|
|
374
403
|
}
|
|
375
404
|
const sid = String(args.session_id || '').trim();
|
|
376
405
|
if (!sid) throw new Error('必须给 session_id');
|
|
377
406
|
try { await reg.unarchiveSession(sid); } catch (e) {
|
|
378
407
|
throw new Error('解归档失败:' + String(e && e.message ? e.message : e));
|
|
379
408
|
}
|
|
380
|
-
return '已解归档 ' + sid + NL + '
|
|
409
|
+
return '已解归档 ' + sid + NL + '该会话已回到原工作区位置,可再次唤醒。';
|
|
381
410
|
}
|
|
382
411
|
|
|
383
412
|
// ---------------------------------------------------------------- action: order
|
|
@@ -387,14 +416,14 @@ function orderHandoverText(env, sid, extra) {
|
|
|
387
416
|
const hp = env.handoverForSid(sid);
|
|
388
417
|
const info = env.infoOfSid(sid);
|
|
389
418
|
const lines = [];
|
|
390
|
-
lines.push("
|
|
419
|
+
lines.push("【全局指令:执行交接流程】将当前工作写成交接。写毕立即停止操作,等待换任;本轮不再进行项目作业。");
|
|
391
420
|
lines.push("");
|
|
392
421
|
lines.push("1) 目标文件:" + (hp && hp.path ? hp.path : "(角色未定,须先由全局设定角色)"));
|
|
393
|
-
lines.push("
|
|
394
|
-
lines.push("2)
|
|
422
|
+
lines.push(" 按固定流程重写:① ho_handover(action=\"read\") 读旧交接;② ho_handover(action=\"archive\") 归档;③ 读模板 _Control_Center/templates/Handover.md;④ ho_handover(action=\"new\", text=...) 新建空白写入。不传 path:路径已按本会话角色(" + (info && info.role ? info.role : "unknown") + ")解析。");
|
|
423
|
+
lines.push("2) 必写 5 项:① 当前进度(版本、产物、是否通过);② 环境与路径(版本号、安装位置、下载源);③ 下一步清单,按顺序列出,每条可直接执行;④ 已完成待验收与要求未实现的部分;⑤ 已知问题与已出现的错误。");
|
|
395
424
|
lines.push("3) 机密只写位置,不写值:密码、token、密钥均写明所在文件。");
|
|
396
|
-
lines.push("4)
|
|
397
|
-
lines.push("5)
|
|
425
|
+
lines.push("4) 写毕只向全局回一条 peer(action=\"send\"):`交接已写 <绝对路径>`,附一句状态,不复述全文。");
|
|
426
|
+
lines.push("5) 不得压缩上下文,不得新建会话(新建会话属全局权限)。");
|
|
398
427
|
if (extra) lines.push("6) 全局附加:" + extra);
|
|
399
428
|
return lines.join(NL);
|
|
400
429
|
}
|
|
@@ -405,69 +434,69 @@ export async function sessionOrder(env, args) {
|
|
|
405
434
|
if (!sid) throw new Error("order 需要 session_id");
|
|
406
435
|
if (order !== "handover" && order !== "compact") throw new Error("order 只能是 handover 或 compact");
|
|
407
436
|
// 给别的会话下「交接、压缩」指令,也要先问用户。
|
|
408
|
-
if (args.confirm !== true) throw new Error("
|
|
437
|
+
if (args.confirm !== true) throw new Error("下交接、压缩指令需要当前用户确认:先说明将唤醒目标会话并要求其交接或压缩,取得同意后传 confirm=true");
|
|
409
438
|
const extra = typeof args.extra === "string" ? args.extra.trim() : "";
|
|
410
439
|
const hp = env.handoverForSid(sid);
|
|
411
440
|
const info = env.infoOfSid(sid);
|
|
412
441
|
const out = [];
|
|
413
442
|
out.push("目标 " + sid + " · 角色 " + ((info && info.role) || "unknown") + " · 交接 " + ((hp && hp.path) || "(未解析)"));
|
|
414
443
|
if (!hp || !hp.path) {
|
|
415
|
-
out.push("
|
|
444
|
+
out.push("没有交接路径:先为目标会话设定角色(ho_sessions(action=\"role\")),否则该会话会退回全局交接。");
|
|
416
445
|
return out.join(NL);
|
|
417
446
|
}
|
|
418
447
|
if (order === "handover") {
|
|
419
448
|
const text = orderHandoverText(env, sid, extra);
|
|
420
449
|
const r = await sendPrompt(env.controller(), sid, text, "queue");
|
|
421
|
-
out.push("
|
|
422
|
-
out.push("
|
|
450
|
+
out.push("已投递交接指令 accepted=" + String(r && r.accepted) + "(将唤醒目标会话一轮)");
|
|
451
|
+
out.push("收到目标会话回的 `交接已写 <路径>` 后,再用 ho_sessions(action=\"new\") 建立接任会话。");
|
|
423
452
|
return out.join(NL);
|
|
424
453
|
}
|
|
425
454
|
// order === compact
|
|
426
455
|
const agent = typeof env.agentOfSid === "function" ? env.agentOfSid(sid) : null;
|
|
427
456
|
if (!agent) {
|
|
428
|
-
out.push("
|
|
457
|
+
out.push("拒绝排压缩:目标会话不在内存(已休眠)。先让目标会话开一轮(ho_sessions(action=\"order\", order=\"handover\")),再排压缩。");
|
|
429
458
|
return out.join(NL);
|
|
430
459
|
}
|
|
431
460
|
const st = typeof env.compactionStateOfSid === "function" ? env.compactionStateOfSid(sid) : null;
|
|
432
461
|
if (st && st.lock === "open") {
|
|
433
|
-
out.push("
|
|
462
|
+
out.push("拒绝排压缩:目标会话日志中有未闭合的压缩锁(compaction/start 无配对 end)。待其结束后再复查。");
|
|
434
463
|
return out.join(NL);
|
|
435
464
|
}
|
|
436
465
|
const cur = readHandover(hp.path);
|
|
437
466
|
if (cur.text === null || String(cur.text).trim() === "") {
|
|
438
|
-
out.push("拒绝排压缩:磁盘上没有合格交接 " + hp.path + "
|
|
467
|
+
out.push("拒绝排压缩:磁盘上没有合格交接 " + hp.path + "。压缩会丢失历史,先执行 order=handover。");
|
|
439
468
|
return out.join(NL);
|
|
440
469
|
}
|
|
441
470
|
const used = st && typeof st.used === "number" ? st.used : 0;
|
|
442
471
|
const max = (env.cfg && env.cfg.maxCompactions) || 3;
|
|
443
472
|
const n = used + 1;
|
|
444
473
|
if (n > max) {
|
|
445
|
-
out.push("已压满 " + used + "/" + max + ":不再压缩。应执行 ho_succeed
|
|
474
|
+
out.push("已压满 " + used + "/" + max + ":不再压缩。应执行 ho_succeed 自行换任(先请当前用户发送 /allow-succeed 授权);全局会话自身也可用 ho_sessions(action=\"new\")。新会话读毕交接后会先向你报告,等待指派。");
|
|
446
475
|
return out.join(NL);
|
|
447
476
|
}
|
|
448
477
|
const recentSec = st && st.lastSummaryMs ? (Date.now() - st.lastSummaryMs) / 1000 : null;
|
|
449
478
|
if (recentSec !== null && recentSec < 120 && args.force !== true) {
|
|
450
|
-
out.push("拒绝排压缩:" + Math.round(recentSec) + "
|
|
479
|
+
out.push("拒绝排压缩:" + Math.round(recentSec) + " 秒前目标会话刚压缩过一次。如确需再次压缩,加 force=true。");
|
|
451
480
|
return out.join(NL);
|
|
452
481
|
}
|
|
453
482
|
// 严格闸门:代排也要用户点过授权,AI 不能自行批准。
|
|
454
483
|
const cs = checkConsent(sid);
|
|
455
484
|
if (!cs.ok) {
|
|
456
485
|
out.push("拒绝排压缩:" + cs.reason + "。");
|
|
457
|
-
out.push("
|
|
486
|
+
out.push("请当前用户在设置页的 dsh-handover 中,点击「授权」内的「授权压缩」(5 分钟内有效),再重新下达本指令。");
|
|
458
487
|
return out.join(NL);
|
|
459
488
|
}
|
|
460
489
|
const resume = args.resume === "continue" ? "continue" : "wait";
|
|
461
490
|
env.schedule(sid, n, { resume });
|
|
462
|
-
out.push("
|
|
463
|
-
out.push("
|
|
491
|
+
out.push("当前用户已授权(" + (cs.atText || '') + " 点击,剩余 " + Math.round((cs.leftMs || 0) / 1000) + " 秒)。");
|
|
492
|
+
out.push("已登记压缩(目标会话自身第 " + n + "/" + max + " 次),待目标会话空闲后执行。");
|
|
464
493
|
if (resume === "wait") {
|
|
465
|
-
out.push("去向:wait(缺省)——
|
|
494
|
+
out.push("去向:wait(缺省)—— 压缩后不再唤醒目标会话,等待当前用户派新任务,或由新会话接手。");
|
|
466
495
|
} else {
|
|
467
|
-
out.push("去向:continue ——
|
|
496
|
+
out.push("去向:continue —— 压缩后目标会话会自动收到其自身的续跑消息(含其自身交接路径)。");
|
|
468
497
|
}
|
|
469
|
-
out.push("
|
|
470
|
-
out.push("
|
|
498
|
+
out.push("压缩由插件调度器执行(compactNow 仅在空闲时可用),不向目标会话发消息令其自行压缩。");
|
|
499
|
+
out.push("若目标会话正在运行,调度器会等待其空闲(上限 " + Math.round(((env.cfg && env.cfg.waitIdleMs) || 180000) / 1000) + " 秒)。");
|
|
471
500
|
return out.join(NL);
|
|
472
501
|
}
|
|
473
502
|
|
|
@@ -482,7 +511,7 @@ export async function sessionPlace(env, args) {
|
|
|
482
511
|
if (!sid) throw new Error("place 需要 session_id");
|
|
483
512
|
if (!key) throw new Error("place 需要 workspace(id、标题、路径都行)");
|
|
484
513
|
const reg = env.registry();
|
|
485
|
-
if (!reg || typeof reg.get !== "function") throw new Error("workspaceRegistry 不可用(先跑
|
|
514
|
+
if (!reg || typeof reg.get !== "function") throw new Error("workspaceRegistry 不可用(先跑 ho_status(view=diag))");
|
|
486
515
|
const wid = findWorkspaceId(key);
|
|
487
516
|
if (!wid) throw new Error("找不到工作区:" + key);
|
|
488
517
|
const w = reg.get(wid);
|
|
@@ -490,6 +519,6 @@ export async function sessionPlace(env, args) {
|
|
|
490
519
|
await w.attachSession(sid);
|
|
491
520
|
const out = [];
|
|
492
521
|
out.push("已把 " + sid + " 移入工作区「" + (w.title || key) + "」" + (w.path ? "(" + w.path + ")" : ""));
|
|
493
|
-
out.push("
|
|
522
|
+
out.push("若提示 cwd 不匹配:目标会话的 cwd 与该工作区根不一致,用 ho_sessions(action=list) 查看 cwd。");
|
|
494
523
|
return out.join(NL);
|
|
495
524
|
}
|
package/src/lib/sessions.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** 会话清点:只读投影缓存,不激活任何会话、不消耗 token。 */
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { readJsonDir, pick, isSafeName } from './text.mjs';
|
|
@@ -26,6 +27,21 @@ export function isBusy(data) {
|
|
|
26
27
|
return pick(data, 'record', 'rows', 'sessionStats', 'val', 'openStep') !== null;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 会话最近活动时刻(epoch 毫秒):与 DSH 侧栏口径一致,取会话摘要里的最后提示时刻。
|
|
32
|
+
* 投影缓存文件的 mtime 会在批量迁移或重建缓存时被统一改写,不能当作活动时间。
|
|
33
|
+
* 读不到时退回文件 mtime。
|
|
34
|
+
*/
|
|
35
|
+
function activityMsOf(data, fallback) {
|
|
36
|
+
const lastPrompt = pick(data, 'record', 'rows', 'sessionListMetadata', 'val', 'lastPromptAt');
|
|
37
|
+
const lastMessage = pick(data, 'record', 'rows', 'timeContext', 'val', 'lastMessageTime');
|
|
38
|
+
let max = Number(fallback) || 0;
|
|
39
|
+
for (const v of [lastPrompt, lastMessage]) {
|
|
40
|
+
if (typeof v === 'number' && v > max) max = v;
|
|
41
|
+
}
|
|
42
|
+
return max;
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
function cwdOf(data) {
|
|
30
46
|
return pick(data, 'record', 'identity', 'cwd');
|
|
31
47
|
}
|
|
@@ -37,7 +53,7 @@ function isPlausible(name) {
|
|
|
37
53
|
|
|
38
54
|
/** 返回 { project, real }:优先看 cwd 里的 Projects/<名>/,退回首条提示里的 Projects/<名>/ 或 「<名>」项目。 */
|
|
39
55
|
export function projectOf(data) {
|
|
40
|
-
//
|
|
56
|
+
// 先认路径:首条提示是自然语言的会话解析不出项目名,会被整行过滤,在任判定就可能落到已交出的旧会话身上。
|
|
41
57
|
// cwd 由宿主写入,最可靠。
|
|
42
58
|
const raw = String(cwdOf(data) || '').replace(/\\/g, '/');
|
|
43
59
|
const ci = raw.toLowerCase().indexOf('/projects/');
|
|
@@ -86,10 +102,32 @@ export function readSelfPressure(dir, meId) {
|
|
|
86
102
|
return { used: null, win: null };
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
/** 只读一个会话的投影缓存文件,返回水位百分比;读不到返回 null。供逐回合提醒使用。 */
|
|
106
|
+
export function readSessionPressurePct(sessionId) {
|
|
107
|
+
const sid = String(sessionId || '');
|
|
108
|
+
if (!sid) return null;
|
|
109
|
+
const dir = projCacheDir();
|
|
110
|
+
const bare = sid.indexOf('session-') === 0 ? sid.slice(8) : sid;
|
|
111
|
+
const names = [];
|
|
112
|
+
for (const n of [sid, bare]) {
|
|
113
|
+
if (n && names.indexOf(n) === -1) names.push(n);
|
|
114
|
+
}
|
|
115
|
+
for (const stem of names) {
|
|
116
|
+
try {
|
|
117
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, stem + '.json'), 'utf8'));
|
|
118
|
+
const used = pick(data, 'record', 'rows', 'contextPressure', 'val', 'pressureTokens');
|
|
119
|
+
const win = pick(data, 'record', 'rows', 'contextPressure', 'val', 'contextWindow');
|
|
120
|
+
if (!used || !win) return null;
|
|
121
|
+
return (used / win) * 100;
|
|
122
|
+
} catch { /* 试下一个文件名 */ }
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
89
127
|
/** 扫描:返回 { rows, cutoff, hot, unknown, notify, skip }。 */
|
|
90
128
|
export function scanSessions(opts) {
|
|
91
129
|
const dir = opts.dir || projCacheDir();
|
|
92
|
-
const warn = opts.warn === undefined ?
|
|
130
|
+
const warn = opts.warn === undefined ? 35 : opts.warn;
|
|
93
131
|
const silentHours = opts.silentHours === undefined ? 12 : opts.silentHours;
|
|
94
132
|
const nowMs = opts.nowMs || Date.now();
|
|
95
133
|
const meId = opts.meId || null;
|
|
@@ -112,7 +150,7 @@ export function scanSessions(opts) {
|
|
|
112
150
|
project: pj.project,
|
|
113
151
|
pct: pressurePct(f.data),
|
|
114
152
|
state: isBusy(f.data) ? 'running' : 'idle',
|
|
115
|
-
mtime: f.mtimeMs,
|
|
153
|
+
mtime: activityMsOf(f.data, f.mtimeMs),
|
|
116
154
|
});
|
|
117
155
|
}
|
|
118
156
|
rows.sort((a, b) => {
|
package/src/lib/sevenzip.mjs
CHANGED
|
@@ -6,7 +6,14 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
const CANDIDATES = [
|
|
7
7
|
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
8
8
|
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
9
|
+
'/usr/bin/7z',
|
|
10
|
+
'/usr/local/bin/7z',
|
|
11
|
+
'/opt/homebrew/bin/7z',
|
|
12
|
+
'/usr/bin/7za',
|
|
13
|
+
'/usr/local/bin/7za',
|
|
14
|
+
'/opt/homebrew/bin/7zz',
|
|
9
15
|
];
|
|
16
|
+
const BARE = new Set(['7z', '7zz', '7za']);
|
|
10
17
|
|
|
11
18
|
function run7z(bin, args, opts) {
|
|
12
19
|
const res = spawnSync(bin, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024, ...(opts || {}) });
|
|
@@ -38,8 +45,10 @@ export function sevenZipBin(configured) {
|
|
|
38
45
|
if (configured) cands.push(configured);
|
|
39
46
|
for (const c of CANDIDATES) cands.push(c);
|
|
40
47
|
cands.push('7z');
|
|
48
|
+
cands.push('7zz');
|
|
49
|
+
cands.push('7za');
|
|
41
50
|
for (const c of cands) {
|
|
42
|
-
if (c
|
|
51
|
+
if (!BARE.has(c) && !fs.existsSync(c)) continue;
|
|
43
52
|
const r = run7z(c, ['i']);
|
|
44
53
|
if (r.code === 0) { probed = c; return c; }
|
|
45
54
|
}
|