@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
package/src/lib/scaffold.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const DEFAULT_ROOT_NAME = '_Workspace';
|
|
|
20
20
|
const DIRS = [
|
|
21
21
|
'_Control_Center/records',
|
|
22
22
|
'_Control_Center/rules',
|
|
23
|
+
'_Control_Center/standards',
|
|
23
24
|
'_Control_Center/templates',
|
|
24
25
|
'_Control_Center/tools',
|
|
25
26
|
'_Control_Center/user',
|
|
@@ -35,6 +36,8 @@ const FILES = [
|
|
|
35
36
|
['_Control_Center/records/Global_Changelog.md', 'Changelog.skeleton.md'],
|
|
36
37
|
['_Control_Center/rules/Workspace_Charter.md', 'Charter.skeleton.md'],
|
|
37
38
|
['_Control_Center/templates/Prompt_Templates.md', 'Prompt_Templates.md'],
|
|
39
|
+
// 通用规范:一条规范一个子目录,各角色会话都可读,只有全局写。这里先铺说明,规范正文由会话按需建立。
|
|
40
|
+
['_Control_Center/standards/README.md', 'standards/README.md'],
|
|
38
41
|
// 规则类:工作区没有这份时,ho_charter 会退回同名内置版本;这里先铺一份可改的默认口径。
|
|
39
42
|
['_Control_Center/rules/Handover_Rules.md', 'rules/Handover_Rules.md'],
|
|
40
43
|
['_Control_Center/rules/Migration_Rules.md', 'rules/Migration_Rules.md'],
|
|
@@ -62,6 +65,41 @@ function normalizeParent(input) {
|
|
|
62
65
|
return { parent: abs, createdParent: !st };
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* 在既有的全局根上补齐缺失的目录与说明文件:只新建,不覆盖,不删任何东西。
|
|
70
|
+
* 用途:插件新增了目录(例如通用规范库)后,已部署的工作区靠它一次补齐。
|
|
71
|
+
*/
|
|
72
|
+
export function ensureScaffold(root) {
|
|
73
|
+
const abs = String(root || '');
|
|
74
|
+
let st = null;
|
|
75
|
+
try { st = fs.statSync(abs); } catch { st = null; }
|
|
76
|
+
if (!st || !st.isDirectory()) return { ok: false, error: '全局根不存在或不是目录:' + (abs || '(空)') };
|
|
77
|
+
|
|
78
|
+
const created = [];
|
|
79
|
+
const written = [];
|
|
80
|
+
const skipped = [];
|
|
81
|
+
for (const d of DIRS) {
|
|
82
|
+
const p = path.join(abs, d.split('/').join(path.sep));
|
|
83
|
+
if (!fs.existsSync(p)) created.push(d);
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(p, { recursive: true });
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return { ok: false, error: '建目录失败:' + d + '(' + String(e && e.message ? e.message : e) + ')', created, written, skipped };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const [rel, tpl] of FILES) {
|
|
91
|
+
const dst = path.join(abs, rel.split('/').join(path.sep));
|
|
92
|
+
if (fs.existsSync(dst)) { skipped.push(rel); continue; }
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(dst, template(tpl), 'utf8');
|
|
95
|
+
written.push(rel);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { ok: false, error: '写文件失败(' + rel + '):' + (e && e.message ? e.message : e), created, written, skipped };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, created, written, skipped };
|
|
101
|
+
}
|
|
102
|
+
|
|
65
103
|
/**
|
|
66
104
|
* 建全局根。
|
|
67
105
|
* @param parentInput 用户在设置页粘贴的路径
|
|
@@ -89,30 +127,14 @@ export function createGlobalWorkspace(parentInput, opts) {
|
|
|
89
127
|
existed = true;
|
|
90
128
|
}
|
|
91
129
|
|
|
92
|
-
const created = [];
|
|
93
130
|
try {
|
|
94
131
|
fs.mkdirSync(root, { recursive: true });
|
|
95
|
-
for (const d of DIRS) {
|
|
96
|
-
const p = path.join(root, d.split('/').join(path.sep));
|
|
97
|
-
fs.mkdirSync(p, { recursive: true });
|
|
98
|
-
created.push(d);
|
|
99
|
-
}
|
|
100
132
|
} catch (e) {
|
|
101
133
|
return { ok: false, root, error: '建目录失败:' + (e && e.message ? e.message : e) };
|
|
102
134
|
}
|
|
103
135
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (fs.existsSync(dst)) { skipped.push(rel); continue; }
|
|
109
|
-
try {
|
|
110
|
-
fs.writeFileSync(dst, template(tpl), 'utf8');
|
|
111
|
-
written.push(rel);
|
|
112
|
-
} catch (e) {
|
|
113
|
-
return { ok: false, root, created, written, error: '写文件失败(' + rel + '):' + (e && e.message ? e.message : e) };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { ok: true, root, existed, created, written, skipped };
|
|
136
|
+
// 铺目录与说明文件走同一套清单:init 与 scaffold 两处不得各写一份。
|
|
137
|
+
const sc = ensureScaffold(root);
|
|
138
|
+
if (!sc.ok) return { ok: false, root, created: sc.created, written: sc.written, error: sc.error };
|
|
139
|
+
return { ok: true, root, existed, created: sc.created, written: sc.written, skipped: sc.skipped };
|
|
118
140
|
}
|
package/src/lib/schedule.mjs
CHANGED
|
@@ -28,7 +28,6 @@ export function createScheduler(deps) {
|
|
|
28
28
|
const timers = new Map();
|
|
29
29
|
let disposed = false;
|
|
30
30
|
|
|
31
|
-
// 用全局定时器:cordis 的 ctx.setTimeout 绑定调用方 fiber,工具调用返回后可能被一并释放
|
|
32
31
|
// 用全局定时器:cordis 的 ctx.setTimeout 绑定调用方 fiber,工具调用返回后可能被一并释放。
|
|
33
32
|
function later(fn, ms) {
|
|
34
33
|
return setTimeout(fn, ms);
|
|
@@ -182,7 +181,7 @@ export function createScheduler(deps) {
|
|
|
182
181
|
pending.delete(sessionId);
|
|
183
182
|
note("checkpoint failed " + sessionId + ": " + msg);
|
|
184
183
|
try {
|
|
185
|
-
promptSelf(sessionId, "【dsh-handover】压缩未执行:" + msg + "
|
|
184
|
+
promptSelf(sessionId, "【dsh-handover】压缩未执行:" + msg + "。历史没有被压缩,交接已就绪;请把这条消息原样告诉当前用户,不要自行重试压缩。").catch(() => {});
|
|
186
185
|
} catch { /* ignore */ }
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -27,7 +27,7 @@ export async function newSessionIn(env, opts) {
|
|
|
27
27
|
if (!sessionId) return { error: 'create 没有返回 sessionId' };
|
|
28
28
|
const title = String(o.title || '');
|
|
29
29
|
if (title) {
|
|
30
|
-
try { if (typeof controller.rename === 'function') await controller.rename({ sessionId, title }); } catch { /*
|
|
30
|
+
try { if (typeof controller.rename === 'function') await controller.rename({ sessionId, title }); } catch { /* 改名失败可事后用 peer(action="rename") */ }
|
|
31
31
|
}
|
|
32
32
|
const role = String(o.role || '');
|
|
33
33
|
if (role && role !== 'plain' && typeof env.writeRole === 'function') {
|
package/src/lib/session-ops.mjs
CHANGED
|
@@ -176,14 +176,14 @@ function reuseBlocker(env, ws, kind, key) {
|
|
|
176
176
|
if (pct === null || pct === undefined || !(Number(pct) < watermark(env.cfg).warnValue)) return '';
|
|
177
177
|
const label = kind === 'global' ? '全局类别' : kind === 'project' ? '项目「' + key + '」' : '杂项任务「' + key + '」';
|
|
178
178
|
return '拒绝新建:' + label + '已有在任会话 ' + (cur.name || '(未命名)') + '(' + cur.sessionId + ')水位 '
|
|
179
|
-
+ Number(pct).toFixed(1) + '%,低于 ' + watermark(env.cfg).warn + ';建议用
|
|
179
|
+
+ Number(pct).toFixed(1) + '%,低于 ' + watermark(env.cfg).warn + ';建议用 peer(action="send")复用,确需另建请传 force=true。';
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
export async function sessionNew(env, args, exec) {
|
|
183
183
|
const ws = env.resolveWs(exec);
|
|
184
184
|
const controller = env.controller();
|
|
185
185
|
// 换任(新建接班会话)先问用户。
|
|
186
|
-
if (args.confirm !== true) throw new Error('
|
|
186
|
+
if (args.confirm !== true) throw new Error('新建会话(换任)需要当前用户确认:先向当前用户说明要建接班会话并得到同意,再传 confirm=true');
|
|
187
187
|
if (!controller) throw new Error('ctx.sessionController 不可用');
|
|
188
188
|
// 名字要落成目录:先校验,别等会话建完、写角色表时才失败(那样会留下一个没有角色的会话)。
|
|
189
189
|
if (args.project) assertProjectName(String(args.project));
|
|
@@ -238,10 +238,10 @@ export async function sessionNew(env, args, exec) {
|
|
|
238
238
|
|| (finalRole === 'misc' ? path.join(ws, '_Control_Center', 'rules', 'Misc_Task_Standard.md') : '');
|
|
239
239
|
const text = args.message || [
|
|
240
240
|
'先读交接:' + handover + '。',
|
|
241
|
-
'
|
|
241
|
+
'读毕先向当前用户报告,不得自行开展作业:一、当前停在哪一步;二、交接列有哪些待办;三、建议先做哪一条。待当前用户指派后再动手。交接无待办时如实说明,不得编造任务。',
|
|
242
242
|
'本会话 id:' + sessionId,
|
|
243
243
|
(finalRole === 'project' ? '全局会话 id:' : '创建者会话 id:') + meId,
|
|
244
|
-
'默认沉默:仅在三种时机各发一条
|
|
244
|
+
'默认沉默:仅在三种时机各发一条 peer(action="send")给全局:水位不低于 ' + watermark(env.cfg).warn + ' 且已写交接、需当前用户决定、全局口径有误。任务完成不报全局,任务属本会话内部事务。会话间可直接 peer(action="send"),不必经全局转述;不得每轮回执,不得发送「收到」「已完成」一类确认,全局以文件末态为准。',
|
|
245
245
|
].join(NL);
|
|
246
246
|
const r = await sendPrompt(env.controller(), sessionId, text, 'queue');
|
|
247
247
|
out.push('已投启动提示词 accepted=' + String(r && r.accepted) + '(' + text.length + ' 字)');
|
|
@@ -292,7 +292,7 @@ export function promptFor(env, opts) {
|
|
|
292
292
|
const roleText = kind === 'global'
|
|
293
293
|
? '第 ' + ordinal + ' 任全局(管理会话)'
|
|
294
294
|
: kind === 'project'
|
|
295
|
-
? '「' + project + '
|
|
295
|
+
? '「' + project + '」项目的执行会话'
|
|
296
296
|
: kind === 'misc' ? '杂项任务会话' : '新机器接手会话';
|
|
297
297
|
const scopeText = kind === 'project'
|
|
298
298
|
? '库根 Projects/' + project + ' 与 _Project_Meta/' + project
|
|
@@ -317,9 +317,9 @@ export function promptFor(env, opts) {
|
|
|
317
317
|
if (kind === 'project' && project) {
|
|
318
318
|
extraLines.push('库根:Projects/' + project + '/;元数据:_Project_Meta/' + project + '/;快照:_Temp/' + project + '/Snapshots/');
|
|
319
319
|
extraLines.push('作业前读库根 SKILL.md;修改文档时另读 _Control_Center/rules/ 对应规范。');
|
|
320
|
-
extraLines.push('
|
|
320
|
+
extraLines.push('进入预警区后写交接;请当前用户在聊天框发送 /allow-succeed 授权,再由本会话调用 ho_succeed 完成换任,不向全局请求。');
|
|
321
321
|
}
|
|
322
|
-
if (kind === 'misc') extraLines.push('标准见 rules/Misc_Task_Standard.md
|
|
322
|
+
if (kind === 'misc') extraLines.push('标准见 rules/Misc_Task_Standard.md;换任须当前用户授权,再由本会话调用 ho_succeed。');
|
|
323
323
|
if (kind === 'migration') extraLines.push('迁移口径见 rules/Migration_Rules.md;通过顶层闸门后再开始作业。');
|
|
324
324
|
if (o.extra) extraLines.push(String(o.extra));
|
|
325
325
|
return [body].concat(extraLines.length ? ['', ...extraLines] : []).join(NL);
|
|
@@ -344,7 +344,7 @@ export function sessionPrompt(env, args, exec) {
|
|
|
344
344
|
export async function sessionArchive(env, args, exec) {
|
|
345
345
|
const reg = env.registry();
|
|
346
346
|
if (!reg || typeof reg.archiveSession !== 'function') {
|
|
347
|
-
throw new Error('workspaceRegistry.archiveSession 不可用(先跑
|
|
347
|
+
throw new Error('workspaceRegistry.archiveSession 不可用(先跑 ho_status(view=diag) 看服务可见性)');
|
|
348
348
|
}
|
|
349
349
|
const sid = String(args.session_id || '').trim();
|
|
350
350
|
if (!sid) throw new Error('必须给 session_id');
|
|
@@ -370,7 +370,7 @@ export async function sessionArchive(env, args, exec) {
|
|
|
370
370
|
export async function sessionUnarchive(env, args) {
|
|
371
371
|
const reg = env.registry();
|
|
372
372
|
if (!reg || typeof reg.unarchiveSession !== 'function') {
|
|
373
|
-
throw new Error('workspaceRegistry.unarchiveSession 不可用(先跑
|
|
373
|
+
throw new Error('workspaceRegistry.unarchiveSession 不可用(先跑 ho_status(view=diag) 看服务可见性)');
|
|
374
374
|
}
|
|
375
375
|
const sid = String(args.session_id || '').trim();
|
|
376
376
|
if (!sid) throw new Error('必须给 session_id');
|
|
@@ -393,7 +393,7 @@ function orderHandoverText(env, sid, extra) {
|
|
|
393
393
|
lines.push(" 直接调用 ho_handover(action=\"write\", text=...),不要传 path:路径已按你的角色(" + (info && info.role ? info.role : "unknown") + ")解析。");
|
|
394
394
|
lines.push("2) 必写:① 当前进度(版本、产物、是否通过)② 环境与路径(版本号、安装位置、下载源)③ 下一步清单,按顺序列出,每条可直接执行 ④ 已完成待验收、要求未实现的部分 ⑤ 已知问题与已踩的错。");
|
|
395
395
|
lines.push("3) 机密只写位置,不写值:密码、token、密钥均写明所在文件。");
|
|
396
|
-
lines.push("4) 写完只回一条
|
|
396
|
+
lines.push("4) 写完只回一条 peer(action=\"send\")给全局:`交接已写 <绝对路径>`,附一句状态,不复述全文。");
|
|
397
397
|
lines.push("5) 不要压缩上下文,不要新建会话(新建会话属全局权限)。");
|
|
398
398
|
if (extra) lines.push("6) 全局附加:" + extra);
|
|
399
399
|
return lines.join(NL);
|
|
@@ -405,7 +405,7 @@ export async function sessionOrder(env, args) {
|
|
|
405
405
|
if (!sid) throw new Error("order 需要 session_id");
|
|
406
406
|
if (order !== "handover" && order !== "compact") throw new Error("order 只能是 handover 或 compact");
|
|
407
407
|
// 给别的会话下「交接、压缩」指令,也要先问用户。
|
|
408
|
-
if (args.confirm !== true) throw new Error("
|
|
408
|
+
if (args.confirm !== true) throw new Error("下交接、压缩指令需要当前用户确认:先说明会唤醒目标会话并让它交接或压缩,得到同意后传 confirm=true");
|
|
409
409
|
const extra = typeof args.extra === "string" ? args.extra.trim() : "";
|
|
410
410
|
const hp = env.handoverForSid(sid);
|
|
411
411
|
const info = env.infoOfSid(sid);
|
|
@@ -442,7 +442,7 @@ export async function sessionOrder(env, args) {
|
|
|
442
442
|
const max = (env.cfg && env.cfg.maxCompactions) || 3;
|
|
443
443
|
const n = used + 1;
|
|
444
444
|
if (n > max) {
|
|
445
|
-
out.push("已压满 " + used + "/" + max + ":不再压缩。应执行 ho_succeed
|
|
445
|
+
out.push("已压满 " + used + "/" + max + ":不再压缩。应执行 ho_succeed 自行换任(先请当前用户发送 /allow-succeed 授权);全局会话自身也可用 ho_sessions(action=\"new\")。新会话读毕交接会先向你报告,等待指派。");
|
|
446
446
|
return out.join(NL);
|
|
447
447
|
}
|
|
448
448
|
const recentSec = st && st.lastSummaryMs ? (Date.now() - st.lastSummaryMs) / 1000 : null;
|
|
@@ -454,15 +454,15 @@ export async function sessionOrder(env, args) {
|
|
|
454
454
|
const cs = checkConsent(sid);
|
|
455
455
|
if (!cs.ok) {
|
|
456
456
|
out.push("拒绝排压缩:" + cs.reason + "。");
|
|
457
|
-
out.push("
|
|
457
|
+
out.push("请当前用户到设置页的 dsh-handover 中,点击「授权」里的「授权压缩」(5 分钟内有效),再重下这条指令。");
|
|
458
458
|
return out.join(NL);
|
|
459
459
|
}
|
|
460
460
|
const resume = args.resume === "continue" ? "continue" : "wait";
|
|
461
461
|
env.schedule(sid, n, { resume });
|
|
462
|
-
out.push("
|
|
462
|
+
out.push("当前用户已授权(" + (cs.atText || '') + " 点击,剩 " + Math.round((cs.leftMs || 0) / 1000) + " 秒)。");
|
|
463
463
|
out.push("已登记压缩(它自己第 " + n + "/" + max + " 次):等它空闲后执行。");
|
|
464
464
|
if (resume === "wait") {
|
|
465
|
-
out.push("去向:wait(缺省)——
|
|
465
|
+
out.push("去向:wait(缺省)—— 压缩后不再唤醒它,等当前用户派新任务,或由新会话接手。");
|
|
466
466
|
} else {
|
|
467
467
|
out.push("去向:continue —— 压缩后它会自动收到**它自己的**续跑消息(带它自己的交接路径)。");
|
|
468
468
|
}
|
|
@@ -482,7 +482,7 @@ export async function sessionPlace(env, args) {
|
|
|
482
482
|
if (!sid) throw new Error("place 需要 session_id");
|
|
483
483
|
if (!key) throw new Error("place 需要 workspace(id、标题、路径都行)");
|
|
484
484
|
const reg = env.registry();
|
|
485
|
-
if (!reg || typeof reg.get !== "function") throw new Error("workspaceRegistry 不可用(先跑
|
|
485
|
+
if (!reg || typeof reg.get !== "function") throw new Error("workspaceRegistry 不可用(先跑 ho_status(view=diag))");
|
|
486
486
|
const wid = findWorkspaceId(key);
|
|
487
487
|
if (!wid) throw new Error("找不到工作区:" + key);
|
|
488
488
|
const w = reg.get(wid);
|
package/src/lib/sessions.mjs
CHANGED
|
@@ -37,7 +37,7 @@ function isPlausible(name) {
|
|
|
37
37
|
|
|
38
38
|
/** 返回 { project, real }:优先看 cwd 里的 Projects/<名>/,退回首条提示里的 Projects/<名>/ 或 「<名>」项目。 */
|
|
39
39
|
export function projectOf(data) {
|
|
40
|
-
//
|
|
40
|
+
// 先认路径:首条提示是自然语言的会话解析不出项目名,会被整行过滤,在任判定就可能落到已交出的旧会话身上。
|
|
41
41
|
// cwd 由宿主写入,最可靠。
|
|
42
42
|
const raw = String(cwdOf(data) || '').replace(/\\/g, '/');
|
|
43
43
|
const ci = raw.toLowerCase().indexOf('/projects/');
|
package/src/lib/status.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 设置页用的状态快照(只读):插件自身状态 + 闸门 + 交接体检 + 会话水位 + 最近一次守卫判定。
|
|
3
3
|
* 全部读本地文件,不唤醒任何会话、不花任何会话的 token。
|
|
4
4
|
* 任何一段出错都只让该段带 error 字段,不让整页 500。
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
7
8
|
import path from 'node:path';
|
|
8
|
-
import { isDir } from './text.mjs';
|
|
9
9
|
import { syncCheck } from './gates.mjs';
|
|
10
10
|
import { lintGlobal, lintProject } from './lint.mjs';
|
|
11
11
|
import { scanSessions } from './sessions.mjs';
|
|
@@ -15,20 +15,125 @@ import { buildGuide, SETUP_STEPS } from './guide.mjs';
|
|
|
15
15
|
import { readRoles, resolveRole, ROLE_LABEL, sessionMeta } from './role.mjs';
|
|
16
16
|
import { listConsent } from './consent.mjs';
|
|
17
17
|
import { listProfiles } from './profile.mjs';
|
|
18
|
+
import { loadOffice, readRegistryProjects, KIND_LABEL } from './office.mjs';
|
|
18
19
|
|
|
19
20
|
function guard(fn, fallback) {
|
|
20
21
|
try { return fn(); } catch (e) { return { ...fallback, error: String(e && e.message ? e.message : e) }; }
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* DSH 工作区索引:读 `~/.dsh/storages/workspace.json`(只读)。
|
|
26
|
+
* 返回 { list, bySession, archived }:
|
|
27
|
+
* - list:按登记顺序的工作区(key、title、path);
|
|
28
|
+
* - bySession:sessionId → 工作区;
|
|
29
|
+
* - archived:已归档会话 id,设置页不再罗列。
|
|
30
|
+
* 读不到时全部为空,调用方据此退化为「按会话目录分组、只按归档表过滤」。
|
|
31
|
+
*/
|
|
32
|
+
function loadWorkspaceIndex() {
|
|
33
|
+
const out = { list: [], bySession: new Map(), archived: new Set() };
|
|
34
|
+
let v = null;
|
|
35
|
+
try {
|
|
36
|
+
const file = path.join(os.homedir(), '.dsh', 'storages', 'workspace.json');
|
|
37
|
+
v = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
38
|
+
} catch { return out; }
|
|
39
|
+
if (!v || typeof v !== 'object') return out;
|
|
40
|
+
const tables = v.tables && v.tables.workspaces ? v.tables.workspaces : {};
|
|
41
|
+
const order = v.global && Array.isArray(v.global.workspaceIds) && v.global.workspaceIds.length
|
|
42
|
+
? v.global.workspaceIds : Object.keys(tables);
|
|
43
|
+
for (const key of order) {
|
|
44
|
+
const w = tables[key];
|
|
45
|
+
if (!w) continue;
|
|
46
|
+
const item = { key: String(key), title: String(w.title || ''), path: String(w.path || '') };
|
|
47
|
+
out.list.push(item);
|
|
48
|
+
for (const sid of (Array.isArray(w.sessionIds) ? w.sessionIds : [])) out.bySession.set(String(sid), item);
|
|
49
|
+
}
|
|
50
|
+
for (const sid of (v.global && Array.isArray(v.global.archivedSessionIds) ? v.global.archivedSessionIds : [])) {
|
|
51
|
+
out.archived.add(String(sid));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 仍然存在的会话目录:`~/.dsh/sessions` 下以 session- 开头的目录名(含已归档里恢复不了的会话就自然不在)。 */
|
|
57
|
+
function liveSessionDirs() {
|
|
58
|
+
const set = new Set();
|
|
59
|
+
try {
|
|
60
|
+
const walk = (dir, depth) => {
|
|
61
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
62
|
+
if (!e.isDirectory()) continue;
|
|
63
|
+
if (e.name.indexOf('session-') === 0) set.add(e.name);
|
|
64
|
+
else if (depth < 4) walk(path.join(dir, e.name), depth + 1);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
walk(path.join(os.homedir(), '.dsh', 'sessions'), 0);
|
|
68
|
+
} catch { /* 读不到就返回空集 */ }
|
|
69
|
+
return set;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 在任登记:Office.json 中每条登记(全局、各项目、各杂项任务)的现任。
|
|
73
|
+
* 返回 sessionId → { key, kind, kindLabel, name };读不到登记表时为空表,调用方据此退化为「不过滤」。
|
|
74
|
+
* 普通会话不登记,因此不会被当成在任会话。
|
|
75
|
+
*/
|
|
76
|
+
function loadDuty(ws) {
|
|
77
|
+
const map = new Map();
|
|
78
|
+
let office = null;
|
|
79
|
+
try { office = loadOffice(ws); } catch { office = null; }
|
|
80
|
+
const lineages = (office && office.lineages) || {};
|
|
81
|
+
for (const key of Object.keys(lineages)) {
|
|
82
|
+
const lin = lineages[key] || {};
|
|
83
|
+
const cur = lin.current;
|
|
84
|
+
if (!cur || !cur.sessionId) continue;
|
|
85
|
+
map.set(String(cur.sessionId), {
|
|
86
|
+
key,
|
|
87
|
+
kind: String(lin.kind || ''),
|
|
88
|
+
kindLabel: KIND_LABEL[lin.kind] || '',
|
|
89
|
+
project: String(lin.project || ''),
|
|
90
|
+
task: String(lin.task || ''),
|
|
91
|
+
name: String(cur.name || key),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return { map, office };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 项目交接体检:只查 Registry.md 已登记的项目,未登记的旧目录不在此列。 */
|
|
24
98
|
function lintProjects(ws) {
|
|
25
99
|
const out = [];
|
|
26
|
-
let
|
|
27
|
-
try {
|
|
28
|
-
for (const name of
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
100
|
+
let names = [];
|
|
101
|
+
try { names = readRegistryProjects(ws); } catch { return out; }
|
|
102
|
+
for (const name of names) {
|
|
103
|
+
const r = lintProject(path.join(ws, '_Project_Meta', name, 'Handover.md'));
|
|
104
|
+
out.push({ name, verdict: r.verdict, reasons: Array.isArray(r.reasons) ? r.reasons.slice(0, 5) : [] });
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 杂项目录:规范位置 `_Misc/tasks/<任务>/`,兼容旧布局 `_Misc/<任务>/`;两处都没有就返回空串。 */
|
|
110
|
+
function miscDir(ws, task) {
|
|
111
|
+
const name = String(task || '').trim();
|
|
112
|
+
if (!name) return '';
|
|
113
|
+
const cands = [path.join(ws, '_Misc', 'tasks', name), path.join(ws, '_Misc', name)];
|
|
114
|
+
for (const dir of cands) {
|
|
115
|
+
try { if (fs.statSync(dir).isDirectory()) return dir; } catch { /* 继续看下一个 */ }
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 杂项交接体检:按 Office.json 里已登记的杂项条目找任务目录。
|
|
122
|
+
* 登记名与目录名不一致(例如登记「示例任务」、目录名不同)时报「未找到」,
|
|
123
|
+
* 不猜近似目录:猜错会把别人的交接算到这条任务头上。
|
|
124
|
+
*/
|
|
125
|
+
function lintMisc(ws, office) {
|
|
126
|
+
const out = [];
|
|
127
|
+
const lineages = (office && office.lineages) || {};
|
|
128
|
+
for (const key of Object.keys(lineages)) {
|
|
129
|
+
const lin = lineages[key] || {};
|
|
130
|
+
if (String(lin.kind || '') !== 'misc') continue;
|
|
131
|
+
const name = String(lin.task || key).trim();
|
|
132
|
+
if (!name) continue;
|
|
133
|
+
const dir = miscDir(ws, name);
|
|
134
|
+
const r = dir
|
|
135
|
+
? lintProject(path.join(dir, 'Handover.md'))
|
|
136
|
+
: { verdict: '未找到', reasons: ['按登记名找不到任务目录:_Misc/tasks/' + name + ' 或 _Misc/' + name] };
|
|
32
137
|
out.push({ name, verdict: r.verdict, reasons: Array.isArray(r.reasons) ? r.reasons.slice(0, 5) : [] });
|
|
33
138
|
}
|
|
34
139
|
return out;
|
|
@@ -39,6 +144,9 @@ export function buildStatus(env, extra) {
|
|
|
39
144
|
const ws = routeWorkspace(env);
|
|
40
145
|
const found = hasCenter(ws);
|
|
41
146
|
const warn = Math.round((env.cfg.warnRatio || 0.32) * 100);
|
|
147
|
+
const duty = loadDuty(ws);
|
|
148
|
+
const onDuty = (sid) => (duty.map.size ? duty.map.has(String(sid || '')) : true);
|
|
149
|
+
const dutyOf = (sid) => duty.map.get(String(sid || '')) || null;
|
|
42
150
|
const out = {
|
|
43
151
|
generatedAt: stampTz(Date.now()),
|
|
44
152
|
plugin: {
|
|
@@ -68,7 +176,7 @@ export function buildStatus(env, extra) {
|
|
|
68
176
|
}
|
|
69
177
|
return map;
|
|
70
178
|
}, {}),
|
|
71
|
-
consent: guard(() => listConsent(), []),
|
|
179
|
+
consent: guard(() => listConsent().map((r) => ({ ...r, onduty: onDuty(r.sessionId) })), []),
|
|
72
180
|
profiles: guard(() => listProfiles(ws), []),
|
|
73
181
|
guide: guard(() => buildGuide(env.cfg), { intro: [], situations: [], groups: [], rules: [] }),
|
|
74
182
|
gate: null,
|
|
@@ -102,66 +210,120 @@ export function buildStatus(env, extra) {
|
|
|
102
210
|
out.handover = guard(() => {
|
|
103
211
|
const g = lintGlobal(path.join(ws, '_Control_Center', 'records', 'Global_Handover.md'));
|
|
104
212
|
const projects = lintProjects(ws);
|
|
213
|
+
const misc = lintMisc(ws, duty.office);
|
|
214
|
+
const bad = projects.concat(misc).filter((p) => p.verdict === '不合格');
|
|
105
215
|
return {
|
|
106
216
|
global: { verdict: g.verdict, reasons: Array.isArray(g.reasons) ? g.reasons.slice(0, 5) : [] },
|
|
107
217
|
projects,
|
|
108
|
-
|
|
218
|
+
misc,
|
|
219
|
+
fail: bad.length,
|
|
109
220
|
};
|
|
110
|
-
}, { global: { verdict: '读不到', reasons: [] }, projects: [], fail: 0 });
|
|
221
|
+
}, { global: { verdict: '读不到', reasons: [] }, projects: [], misc: [], fail: 0 });
|
|
111
222
|
|
|
112
223
|
out.sessions = guard(() => {
|
|
113
|
-
const
|
|
114
|
-
dir: env.cacheDir(),
|
|
115
|
-
|
|
116
|
-
meCwd: null,
|
|
117
|
-
warn,
|
|
118
|
-
silentHours: env.cfg.sessionScanHours,
|
|
119
|
-
projectsOnly: true,
|
|
224
|
+
const wide = scanSessions({
|
|
225
|
+
dir: env.cacheDir(), meId: null, meCwd: null, warn,
|
|
226
|
+
silentHours: env.cfg.sessionScanHours, projectsOnly: false,
|
|
120
227
|
});
|
|
121
228
|
const store = readRoles(env.cfg);
|
|
122
229
|
const roleOf = (sid, cwd, firstPrompt, title) => {
|
|
123
230
|
const meta = sessionMeta(sid);
|
|
124
231
|
return resolveRole({ cfg: env.cfg, roles: store, sessionId: sid, cwd, firstPrompt, subagent: meta.subagent, title });
|
|
125
232
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
233
|
+
// 工作区与归档:设置页按工作区分组罗列,已归档与目录已不存在的会话不再出现。
|
|
234
|
+
const wsIndex = loadWorkspaceIndex();
|
|
235
|
+
const live = liveSessionDirs();
|
|
236
|
+
// 会话 id 在缓存里有带与不带 session- 前缀两种写法,统一成带前缀再比。
|
|
237
|
+
const sidKey = (sid) => (/^session-/.test(String(sid)) ? String(sid) : 'session-' + String(sid));
|
|
238
|
+
const archived = (sid) => wsIndex.archived.has(sidKey(sid));
|
|
239
|
+
const stillThere = (sid) => live.size === 0 || live.has(sidKey(sid));
|
|
240
|
+
// 真实路径:旧根现为链接,靠 realpath 才能把老会话归到当前工作区。
|
|
241
|
+
const realCache = new Map();
|
|
242
|
+
const realOf = (p) => {
|
|
243
|
+
const key = String(p || '').toLowerCase();
|
|
244
|
+
if (realCache.has(key)) return realCache.get(key);
|
|
245
|
+
let v = '';
|
|
246
|
+
try { v = fs.realpathSync(String(p || '')); } catch { v = ''; }
|
|
247
|
+
realCache.set(key, v);
|
|
248
|
+
return v;
|
|
249
|
+
};
|
|
250
|
+
const wsReal = wsIndex.list.map((w) => ({
|
|
251
|
+
w,
|
|
252
|
+
real: String(realOf(w.path) || w.path || '').replace(/\\/g, '/').toLowerCase(),
|
|
253
|
+
}));
|
|
254
|
+
const wsOf = (sid, cwd) => {
|
|
255
|
+
const direct = wsIndex.bySession.get(sidKey(sid));
|
|
256
|
+
if (direct) return direct;
|
|
257
|
+
// 未登记到工作区的会话:按 cwd(先取真实路径)最长前缀归到工作区。
|
|
258
|
+
const c = String(realOf(cwd) || cwd || '').replace(/\\/g, '/').toLowerCase();
|
|
259
|
+
if (!c) return null;
|
|
260
|
+
let best = null;
|
|
261
|
+
for (const item of wsReal) {
|
|
262
|
+
const wp = item.real;
|
|
263
|
+
if (!wp) continue;
|
|
264
|
+
if ((c === wp || c.indexOf(wp + '/') === 0) && (!best || wp.length > best.real.length)) best = item;
|
|
265
|
+
}
|
|
266
|
+
return best ? best.w : null;
|
|
267
|
+
};
|
|
268
|
+
const mapRow = (r) => {
|
|
130
269
|
const meta = sessionMeta(r.sid);
|
|
131
270
|
const info = roleOf(r.sid, meta.cwd, meta.firstPrompt, r.project);
|
|
271
|
+
const d = dutyOf(r.sid);
|
|
272
|
+
const w = wsOf(r.sid, meta.cwd);
|
|
273
|
+
// 目录已不存在的老会话(旧树里删掉的项目库等)不再罗列。
|
|
274
|
+
const cwdOk = !meta.cwd || !!realOf(meta.cwd);
|
|
132
275
|
return {
|
|
133
276
|
sid: r.sid,
|
|
134
277
|
title: r.project === '-' ? '' : r.project,
|
|
278
|
+
project: r.project,
|
|
135
279
|
role: info.role,
|
|
136
280
|
roleLabel: ROLE_LABEL[info.role] || info.role,
|
|
137
281
|
source: info.source,
|
|
138
|
-
|
|
282
|
+
projectName: info.project,
|
|
139
283
|
task: info.task,
|
|
284
|
+
cwdOk,
|
|
285
|
+
// 工作区分组:没有登记归属时留空,客户端归入「未归属工作区」。
|
|
286
|
+
wsKey: w ? w.key : '',
|
|
287
|
+
wsTitle: w ? w.title : '',
|
|
288
|
+
wsPath: w ? w.path : String(meta.cwd || ''),
|
|
289
|
+
// 类别与名称以在任登记为准:登记过的会话才能说清自己是全局、项目还是杂项。
|
|
290
|
+
dutyKind: d ? d.kindLabel : '',
|
|
291
|
+
dutyKey: d ? d.key : '',
|
|
292
|
+
dutyName: d ? d.name : '',
|
|
140
293
|
pct: r.pct,
|
|
141
294
|
state: r.state,
|
|
142
|
-
active: r.mtime >=
|
|
295
|
+
active: r.mtime >= wide.cutoffMs,
|
|
296
|
+
onduty: !!d,
|
|
297
|
+
mtime: r.mtime,
|
|
143
298
|
seen: stampTz(r.mtime),
|
|
144
299
|
};
|
|
145
|
-
}
|
|
300
|
+
};
|
|
301
|
+
const all = wide.rows
|
|
302
|
+
.filter((r) => r.kind === '平级' && !archived(r.sid) && stillThere(r.sid))
|
|
303
|
+
.map(mapRow)
|
|
304
|
+
.filter((r) => r.wsKey || r.cwdOk);
|
|
305
|
+
// 在任集合:全局、各项目、各杂项任务的现任各一行;普通会话不在登记表里,因此不出现。
|
|
306
|
+
const rows = all.filter((r) => r.onduty).slice(0, 60);
|
|
307
|
+
// 通知:每条登记的在任会话,若本轮仍活跃;已交出的会话即使更活跃也不通知。
|
|
308
|
+
const notify = [];
|
|
309
|
+
for (const sid of duty.map.keys()) {
|
|
310
|
+
const hit = rows.find((r) => r.sid === sid);
|
|
311
|
+
if (hit && hit.mtime >= wide.cutoffMs) notify.push({ sid: hit.sid, key: hit.dutyKey, name: hit.dutyName });
|
|
312
|
+
}
|
|
313
|
+
const hot = rows.filter((r) => r.pct !== null && r.pct >= warn).length;
|
|
146
314
|
return {
|
|
147
|
-
rows
|
|
148
|
-
const meta = sessionMeta(r.sid);
|
|
149
|
-
const info = roleOf(r.sid, meta.cwd, meta.firstPrompt, r.project);
|
|
150
|
-
return {
|
|
151
|
-
sid: r.sid, project: r.project, pct: r.pct, state: r.state,
|
|
152
|
-
role: info.role, roleLabel: ROLE_LABEL[info.role] || info.role,
|
|
153
|
-
active: r.mtime >= scan.cutoffMs, seen: stampTz(r.mtime),
|
|
154
|
-
};
|
|
155
|
-
}),
|
|
315
|
+
rows,
|
|
156
316
|
all,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
317
|
+
workspaces: wsIndex.list.map((w) => ({ key: w.key, title: w.title || w.path, path: w.path })),
|
|
318
|
+
notify,
|
|
319
|
+
onduty: duty.map.size,
|
|
320
|
+
hot,
|
|
321
|
+
unknown: wide.unknown.length,
|
|
322
|
+
cutoff: stampTz(wide.cutoffMs),
|
|
323
|
+
silentHours: wide.silentHours,
|
|
324
|
+
verdict: hot
|
|
325
|
+
? hot + ' 个在任会话已进预警区(不低于 ' + warn + '%),应先收齐交接,再换会话'
|
|
326
|
+
: '没有在任会话进入预警区。',
|
|
165
327
|
};
|
|
166
328
|
}, { rows: [], notify: [], hot: 0, unknown: 0 });
|
|
167
329
|
|
|
@@ -171,7 +333,5 @@ export function buildStatus(env, extra) {
|
|
|
171
333
|
return all.filter((r) => r && r.role === 'global').length;
|
|
172
334
|
}, 0);
|
|
173
335
|
|
|
174
|
-
if (out.gate && !out.gate.ok && !out.gate.error) out.notes.push('通用约束条闸门未过:向对应项目会话派发,全局不代写项目库内文件。');
|
|
175
|
-
if (out.handover && out.handover.fail) out.notes.push(out.handover.fail + ' 个项目交接体检不合格,交接前必须先补。');
|
|
176
336
|
return out;
|
|
177
337
|
}
|
package/src/lib/succession.mjs
CHANGED
|
@@ -114,14 +114,14 @@ export async function runSuccession(env, exec, opts) {
|
|
|
114
114
|
|
|
115
115
|
// 2. 用户授权:AI 不能自行批准。
|
|
116
116
|
const selfId = env.selfId(exec);
|
|
117
|
-
if (!selfId) return reject('
|
|
117
|
+
if (!selfId) return reject('无法获取当前会话 id,无法校验授权。');
|
|
118
118
|
const cs = checkConsent(selfId, 'succeed');
|
|
119
119
|
if (!cs.ok) {
|
|
120
120
|
return reject([
|
|
121
121
|
cs.reason + '。',
|
|
122
122
|
'授权方式(任选其一):',
|
|
123
123
|
' ① 在聊天框发送 /allow-succeed(最快,仅授权本会话);',
|
|
124
|
-
' ② 在设置页的 dsh-handover
|
|
124
|
+
' ② 在设置页的 dsh-handover 中,点击「授权」里的「授权换任」。',
|
|
125
125
|
'授权有效期 5 分钟,用后失效。',
|
|
126
126
|
].join(NL));
|
|
127
127
|
}
|
|
@@ -188,14 +188,14 @@ export async function runSuccession(env, exec, opts) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
// 7. 拼启动提示词:末尾追加交接路径与「先读交接、读毕报到」。
|
|
191
|
-
// 「全局 id
|
|
191
|
+
// 「全局 id」填**现任全局**:项目与杂项换任时,已交出的旧会话不是全局,不能顶替这个位置。
|
|
192
192
|
let prompt = '';
|
|
193
193
|
try {
|
|
194
194
|
const globalId = kind === 'global' ? selfId : (currentGlobalId(ws) || selfId);
|
|
195
195
|
const base = promptFor(env, { ws, kind, project, task, title: name, sessionId: '', globalSessionId: globalId, extra: '' });
|
|
196
196
|
prompt = base + NL + NL + [
|
|
197
197
|
'接任交接:' + handoverFile,
|
|
198
|
-
'
|
|
198
|
+
'先读这份交接再动手;读毕向当前用户报告当前停在哪一步、交接列有哪些待办,然后等待当前用户指派任务。',
|
|
199
199
|
].join(NL);
|
|
200
200
|
} catch (e) {
|
|
201
201
|
return reject('启动提示词生成失败:' + msgOf(e));
|
|
@@ -215,7 +215,7 @@ export async function runSuccession(env, exec, opts) {
|
|
|
215
215
|
} catch (e) {
|
|
216
216
|
return [
|
|
217
217
|
'会话已建 ' + sessionId + '(' + name + '),但登记在任失败:' + msgOf(e),
|
|
218
|
-
'启动提示词尚未投递;先修好登记表,再用
|
|
218
|
+
'启动提示词尚未投递;先修好登记表,再用 peer(action="send")通知它。',
|
|
219
219
|
].join(NL);
|
|
220
220
|
}
|
|
221
221
|
|
|
@@ -253,8 +253,8 @@ export async function runSuccession(env, exec, opts) {
|
|
|
253
253
|
out.push('换任完成:' + name + '(' + sessionId + ')');
|
|
254
254
|
out.push('在任登记:' + key + (reg && reg.registry && reg.registry.changed && reg.registry.changed.length
|
|
255
255
|
? ',Registry.md 已同步在役列' : ''));
|
|
256
|
-
out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用
|
|
256
|
+
out.push('启动提示词:' + (delivered ? '已投递' : '未送达:' + deliveryError + '。会话已建,可用 peer(action="send")补投'));
|
|
257
257
|
out.push('交接登记:' + appendNote);
|
|
258
|
-
out.push('
|
|
258
|
+
out.push('本会话到此停止,等待当前用户在新会话中派任务。');
|
|
259
259
|
return out.join(NL);
|
|
260
260
|
}
|
package/src/lib/zstd-log.mjs
CHANGED