@mingmaple/dsh-handover 0.9.22 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -113
- package/docs/migrate.md +52 -41
- package/docs/releases/0.9.12.md +2 -3
- package/docs/releases/0.9.14.md +1 -5
- package/docs/releases/0.9.22.md +1 -1
- package/docs/releases/1.0.0.md +33 -0
- package/docs/troubleshoot.md +221 -61
- package/package.json +5 -2
- package/scripts/behavior.mjs +398 -0
- package/scripts/blank-dsh.mjs +12 -2
- package/scripts/blank-smoke.mjs +95 -44
- package/scripts/check-docs.mjs +7 -0
- package/scripts/portability.mjs +71 -0
- package/scripts/style-gate.mjs +571 -0
- package/src/client.js +1028 -994
- package/src/index.mjs +273 -109
- package/src/lib/absrefs.mjs +1 -1
- package/src/lib/api.mjs +9 -3
- package/src/lib/archive.mjs +1 -1
- package/src/lib/brief.mjs +8 -6
- package/src/lib/config.mjs +21 -6
- package/src/lib/docmap.mjs +5 -8
- package/src/lib/drift.mjs +1 -1
- package/src/lib/events.mjs +1 -1
- package/src/lib/facts.mjs +19 -12
- package/src/lib/gates.mjs +12 -12
- package/src/lib/global-task.mjs +69 -0
- package/src/lib/guide.mjs +80 -75
- package/src/lib/handover-io.mjs +69 -6
- package/src/lib/identity.mjs +330 -0
- package/src/lib/legacy.mjs +89 -0
- package/src/lib/lint.mjs +14 -11
- package/src/lib/manual.mjs +117 -80
- package/src/lib/mover.mjs +18 -14
- package/src/lib/office.mjs +42 -2
- package/src/lib/pack-ops.mjs +10 -10
- package/src/lib/pathkey.mjs +24 -0
- package/src/lib/phase.mjs +8 -13
- package/src/lib/profile.mjs +1 -1
- package/src/lib/reminder.mjs +110 -0
- package/src/lib/role.mjs +1 -1
- package/src/lib/scaffold.mjs +30 -10
- package/src/lib/schedule.mjs +5 -5
- package/src/lib/session-create.mjs +2 -2
- package/src/lib/session-ops.mjs +76 -47
- package/src/lib/sessions.mjs +40 -2
- package/src/lib/sevenzip.mjs +10 -1
- package/src/lib/status.mjs +337 -336
- package/src/lib/succession.mjs +14 -14
- package/src/lib/system.mjs +364 -0
- package/src/templates/Changelog.skeleton.md +3 -3
- package/src/templates/Charter.skeleton.md +13 -9
- package/src/templates/Handover.global.skeleton.md +20 -13
- package/src/templates/Handover.template.md +89 -0
- package/src/templates/Plan.skeleton.md +34 -0
- package/src/templates/Prompt_Templates.md +91 -10
- package/src/templates/Runtime_Manual.md +254 -139
- package/src/templates/System.skeleton.md +31 -0
- package/src/templates/Systems.global.skeleton.md +13 -0
- package/src/templates/rules/Delegation_Rules.md +45 -0
- package/src/templates/rules/Handover_Rules.md +46 -24
- package/src/templates/rules/Migration_Rules.md +7 -7
- package/src/templates/rules/Misc_Task_Standard.md +17 -10
- package/src/templates/rules/Writing_Rules.md +33 -16
- package/src/templates/standards/README.md +19 -7
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203/README.md +120 -0
- package/src/templates/standards//345/206/231/344/275/234/350/247/204/350/214/203//344/276/235/346/215/256_/350/256/272/346/226/207.md +389 -0
- package/src/tools/ho_backup.mjs +8 -8
- package/src/tools/ho_changelog.mjs +4 -4
- package/src/tools/ho_charter.mjs +7 -6
- package/src/tools/ho_checkpoint.mjs +34 -31
- package/src/tools/ho_docmap.mjs +135 -49
- package/src/tools/ho_gate.mjs +9 -4
- package/src/tools/ho_handover.mjs +61 -35
- package/src/tools/ho_office.mjs +45 -4
- package/src/tools/ho_profile.mjs +3 -3
- package/src/tools/ho_sessions.mjs +43 -22
- package/src/tools/ho_status.mjs +14 -9
- package/src/tools/ho_succeed.mjs +3 -3
- package/src/tools/ho_workspace.mjs +83 -51
- package/src/tools/peer.mjs +219 -8
package/src/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* dsh-handover —— 全局运行期插件。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* 判定条件写入代码,执行流程写入工具。
|
|
4
|
+
* 规则正文保留在磁盘,需要时用 ho_charter 按节读取(完整流程 doc="runtime")。
|
|
5
|
+
* 按会话角色装配工具与提示段:全局 14 个工具、项目 11 个、杂项 10 个、普通 0 个。
|
|
6
|
+
* 源码不热加载:改动后须重启 DSH。
|
|
6
7
|
*/
|
|
7
8
|
import fs from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
@@ -24,7 +25,7 @@ import { pmProfileTool } from './tools/ho_profile.mjs';
|
|
|
24
25
|
import { pmOfficeTool } from './tools/ho_office.mjs';
|
|
25
26
|
import { pmSucceedTool } from './tools/ho_succeed.mjs';
|
|
26
27
|
import { pmDocmapTool } from './tools/ho_docmap.mjs';
|
|
27
|
-
import { pmPeerTool } from './tools/peer.mjs';
|
|
28
|
+
import { pmPeerTool, collabMemberReason } from './tools/peer.mjs';
|
|
28
29
|
import { resolveRegistry } from './lib/registry.mjs';
|
|
29
30
|
import { registerStatusRoute } from './lib/webroute.mjs';
|
|
30
31
|
import { createApi, statusSnapshot } from './lib/api.mjs';
|
|
@@ -35,28 +36,33 @@ import {
|
|
|
35
36
|
ROLES, resolveRole, sessionMeta, handoverPathFor, toolsetFor,
|
|
36
37
|
readRoles, writeRole, assertProjectName, assertTaskName,
|
|
37
38
|
} from './lib/role.mjs';
|
|
38
|
-
import { manualFor,
|
|
39
|
+
import { manualFor, firstRoundMessageFor } from './lib/manual.mjs';
|
|
39
40
|
import { sendPrompt } from './lib/prompt.mjs';
|
|
40
41
|
import { resolveShortSid } from './lib/api.mjs';
|
|
41
42
|
import { routeWorkspace } from './lib/workspace.mjs';
|
|
42
43
|
import { stampTz } from './lib/brief.mjs';
|
|
43
44
|
import { grantConsent, activeConsentsFor } from './lib/consent.mjs';
|
|
44
|
-
import {
|
|
45
|
+
import { systemBlock, registrySignature } from './lib/system.mjs';
|
|
46
|
+
import { reminderTailNote } from './lib/reminder.mjs';
|
|
47
|
+
import { migrateIdentity, removeRegistryProject } from './lib/identity.mjs';
|
|
48
|
+
import { loadOffice, lineageKeyOf, syncHolderKind, removeHolder, registerHolder } from './lib/office.mjs';
|
|
45
49
|
import { moveStatus } from './lib/mover.mjs';
|
|
46
50
|
|
|
47
51
|
export const name = 'dsh-handover';
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
52
|
+
// 必需服务(名称已核对):插件名与内核服务名一一对应。
|
|
53
|
+
// dsh-agent→agents、dsh-compaction→compaction、dsh-api-session-controller→sessionController。
|
|
54
|
+
// 只 inject 确定存在的服务:多写一个不存在的名称,cordis 会使整个插件静默不激活。
|
|
55
|
+
// agents 与 compaction 按运行期解析(ctx[name] → ctx.get(name))。
|
|
56
|
+
// 无法获取时由 ho_status(view="diag")报出,ho_checkpoint 明确报错。
|
|
51
57
|
export const inject = ['tools', 'systemPrompt', 'sessionController'];
|
|
52
58
|
|
|
53
|
-
/** 自身包信息:dist
|
|
59
|
+
/** 自身包信息:dist 内读取 ./package.json,源码树内回退到 ../package.json。 */
|
|
54
60
|
function pkgInfo() {
|
|
55
61
|
for (const rel of ['./package.json', '../package.json']) {
|
|
56
62
|
try {
|
|
57
63
|
const j = JSON.parse(fs.readFileSync(new URL(rel, import.meta.url), 'utf8'));
|
|
58
64
|
if (j && j.version) return { name: String(j.name || ''), version: String(j.version) };
|
|
59
|
-
} catch { /*
|
|
65
|
+
} catch { /* 尝试下一个 */ }
|
|
60
66
|
}
|
|
61
67
|
return { name: '@mingmaple/dsh-handover', version: '?' };
|
|
62
68
|
}
|
|
@@ -69,13 +75,14 @@ function walkUpToWorkspace(start) {
|
|
|
69
75
|
if (up === cur) break;
|
|
70
76
|
cur = up;
|
|
71
77
|
}
|
|
72
|
-
// cwd
|
|
73
|
-
//
|
|
74
|
-
//
|
|
78
|
+
// cwd 的父目录已删除(或 cwd 为盘符根)时,向上无法找到工作区。
|
|
79
|
+
// 若原样返回 start,写操作会在该失效路径上重建一套 _Control_Center。
|
|
80
|
+
// 该目录没有对应工作区,后续流程无法识别。
|
|
81
|
+
// 此时以登记表记录的工作区为准;登记表也查不到时才返回 start。
|
|
75
82
|
try {
|
|
76
83
|
const fromReg = routeWorkspace({ cfg: {} });
|
|
77
84
|
if (fromReg && fromReg !== start && isDir(path.join(fromReg, '_Control_Center'))) return fromReg;
|
|
78
|
-
} catch { /*
|
|
85
|
+
} catch { /* 登记表无法读取时保持旧行为 */ }
|
|
79
86
|
return start;
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -84,7 +91,7 @@ function msgOf(e) {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/**
|
|
87
|
-
*
|
|
94
|
+
* 压缩调度器依赖的 2 个函数:入参为**会话 id**,须按 id 解析角色与交接路径。
|
|
88
95
|
* 单独导出,便于冒烟测试直接验证。
|
|
89
96
|
*/
|
|
90
97
|
export function schedDepsFor(env) {
|
|
@@ -97,11 +104,12 @@ export function schedDepsFor(env) {
|
|
|
97
104
|
export function apply(ctx, config) {
|
|
98
105
|
const cfg = resolveConfig(config);
|
|
99
106
|
const initErrors = [];
|
|
100
|
-
const registered = new Set(); //
|
|
101
|
-
// 0.2.2
|
|
102
|
-
// 更早的内核忽略注册选项,只能退回 agent 作用域的 restrict
|
|
107
|
+
const registered = new Set(); // 已注册的工具名(旧内核的限制式裁剪使用)
|
|
108
|
+
// 自 0.2.2 起,工具按「步」解析(tools.resolve):可用工具集合每步重算,角色过滤写入注册条件。
|
|
109
|
+
// 更早的内核忽略注册选项,只能退回 agent 作用域的 restrict。2 条路径按能力择一,不并行。
|
|
103
110
|
const perStep = (() => { try { return !!(ctx.tools && typeof ctx.tools.resolve === 'function'); } catch { return false; } })();
|
|
104
111
|
const sectionDispose = new Map(); // sessionId → 撤销提示段
|
|
112
|
+
const systemSig = new Map(); // sessionId → 上次装配时的体系注册签名(热更新)
|
|
105
113
|
const consentSig = new Map(); // sessionId → 上次装配时的授权签名(授权出现或过期时重装)
|
|
106
114
|
const legacyRestrict = new Map(); // sessionId → 撤销限制(仅无按步解析的内核)
|
|
107
115
|
const appliedRole = new Map(); // sessionId → 上次装配的角色
|
|
@@ -142,11 +150,11 @@ export function apply(ctx, config) {
|
|
|
142
150
|
},
|
|
143
151
|
schedule(sessionId, n, opts) {
|
|
144
152
|
if (!scheduler) throw new Error("压缩调度器未就绪" + (schedulerError ? ":" + schedulerError : "(服务尚未注入)"));
|
|
145
|
-
// consent
|
|
153
|
+
// consent 已由入口工具校验并消费;此处不再复查,复查必然无效。
|
|
146
154
|
scheduler.request(sessionId, Object.assign({ n, consent: 'checked' }, opts || {})).catch(() => {});
|
|
147
155
|
},
|
|
148
156
|
registry() { return resolveRegistry(ctx); },
|
|
149
|
-
/**
|
|
157
|
+
/** 迁移感知:根目录是否已迁移到其他位置(原路径成为 junction)。供提示段与 ho_status(view="diag")使用。 */
|
|
150
158
|
moveInfo() {
|
|
151
159
|
try {
|
|
152
160
|
const src = cfg.workspaceRoot || walkUpToWorkspace(process.cwd());
|
|
@@ -195,7 +203,7 @@ export function apply(ctx, config) {
|
|
|
195
203
|
staleNote(exec) {
|
|
196
204
|
try { return stalePressureNote(this.selfId(exec)); } catch { return ''; }
|
|
197
205
|
},
|
|
198
|
-
/** 自身版本:从 dist/package.json
|
|
206
|
+
/** 自身版本:从 dist/package.json 读取,设置页与 ho_status(view="diag")共用同一值。 */
|
|
199
207
|
version() {
|
|
200
208
|
try { return pkgInfo().version; } catch { return '?'; }
|
|
201
209
|
},
|
|
@@ -206,7 +214,7 @@ export function apply(ctx, config) {
|
|
|
206
214
|
},
|
|
207
215
|
};
|
|
208
216
|
|
|
209
|
-
// ---------------- 会话标题:运行时 header.title
|
|
217
|
+
// ---------------- 会话标题:运行时 header.title 往往为空,改读投影缓存
|
|
210
218
|
function titleOf(sessionId) {
|
|
211
219
|
try {
|
|
212
220
|
const a = ctx.agents && typeof ctx.agents.get === 'function' ? ctx.agents.get(sessionId) : null;
|
|
@@ -230,9 +238,9 @@ export function apply(ctx, config) {
|
|
|
230
238
|
try {
|
|
231
239
|
const a = ctx.agents && typeof ctx.agents.get === 'function' ? ctx.agents.get(want) : null;
|
|
232
240
|
if (a) return a;
|
|
233
|
-
} catch { /*
|
|
234
|
-
//
|
|
235
|
-
//
|
|
241
|
+
} catch { /* 回退到列表查找 */ }
|
|
242
|
+
// 宿主的注册表键不保证等于会话 id:无法取到实例时,按会话头 id 在活体列表中再查一次。
|
|
243
|
+
// 缺这一层,休眠会话在设置页修改角色时会被判为「不在内存」,须等重启才装配。
|
|
236
244
|
try {
|
|
237
245
|
const list = ctx.agents && typeof ctx.agents.list === 'function' ? (ctx.agents.list() || []) : [];
|
|
238
246
|
for (const a of list) {
|
|
@@ -244,8 +252,9 @@ export function apply(ctx, config) {
|
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
255
|
+
* 仅凭会话头即可判定的子级会话。
|
|
256
|
+
* 子会话与团队成员的会话都带 origin=subagent 或 parentSession。
|
|
257
|
+
* 只读 header,不访问磁盘,可置于「每一步」的条件启用中。
|
|
249
258
|
*/
|
|
250
259
|
function isChildSession(agent) {
|
|
251
260
|
try {
|
|
@@ -274,7 +283,8 @@ export function apply(ctx, config) {
|
|
|
274
283
|
env.toolsetOf = (role) => toolsetFor(role);
|
|
275
284
|
env.roleState = () => ({ mode: roleMode(), last: roleState.last, count: appliedRole.size });
|
|
276
285
|
env.rolesSnapshot = () => readRoles(cfg).sessions;
|
|
277
|
-
//
|
|
286
|
+
// 写角色表是所有角色变更的唯一入口。
|
|
287
|
+
// 写入后立即使每步过滤的短缓存失效,下一步即按新角色判定。
|
|
278
288
|
env.writeRole = (sid, patch) => {
|
|
279
289
|
const saved = writeRole(cfg, sid, patch);
|
|
280
290
|
roleCache.delete(String(sid || ''));
|
|
@@ -286,7 +296,7 @@ export function apply(ctx, config) {
|
|
|
286
296
|
if (cfg.roleGating === 'off') return '关闭(roleGating=off,所有会话可见全部工具)';
|
|
287
297
|
return perStep
|
|
288
298
|
? '按步过滤(工具表每步重算,换角色下一轮即生效)'
|
|
289
|
-
: '
|
|
299
|
+
: '旧式收窄(内核不支持按步解析,改用 agent 作用域 restrict)';
|
|
290
300
|
}
|
|
291
301
|
|
|
292
302
|
function handoverInfoFor(info, ws) {
|
|
@@ -297,7 +307,7 @@ export function apply(ctx, config) {
|
|
|
297
307
|
return Object.assign({ ws, role: info.role, project: info.project, task: info.task }, t);
|
|
298
308
|
}
|
|
299
309
|
|
|
300
|
-
/**
|
|
310
|
+
/** 仅按会话 id 推断工作区根:优先活体 cwd,其次会话元数据,最后取配置。 */
|
|
301
311
|
function workspaceForSid(sid) {
|
|
302
312
|
if (cfg.workspaceRoot) return String(cfg.workspaceRoot);
|
|
303
313
|
const a = agentOf(sid);
|
|
@@ -306,7 +316,7 @@ export function apply(ctx, config) {
|
|
|
306
316
|
}
|
|
307
317
|
|
|
308
318
|
env.handoverFor = (exec) => handoverInfoFor(env.roleOf(exec), env.resolveWs(exec));
|
|
309
|
-
/**
|
|
319
|
+
/** 跨会话使用:按 id 计算该会话自身的角色与交接路径,不借用调用者上下文。 */
|
|
310
320
|
env.handoverForSid = (sid) => {
|
|
311
321
|
const id = String(sid || '');
|
|
312
322
|
return handoverInfoFor(infoFor(id, {}), workspaceForSid(id));
|
|
@@ -315,7 +325,7 @@ export function apply(ctx, config) {
|
|
|
315
325
|
env.agentOfSid = (sid) => agentOf(String(sid || ''));
|
|
316
326
|
env.compactionStateOfSid = (sid) => compactionStateOf(String(sid || ''), null);
|
|
317
327
|
|
|
318
|
-
// ----------------
|
|
328
|
+
// ---------------- 按角色装配:工具可见性由每步条件即时计算,此处只维护提示段(旧内核另加 restrict)
|
|
319
329
|
function workspaceForAgent(agent) {
|
|
320
330
|
if (cfg.workspaceRoot) return cfg.workspaceRoot;
|
|
321
331
|
const cwd = agent && agent.session && agent.session.header ? String(agent.session.header.cwd || '') : '';
|
|
@@ -328,28 +338,33 @@ export function apply(ctx, config) {
|
|
|
328
338
|
if (typeof old === 'function') { try { old(); } catch { /* 旧段已失效 */ } }
|
|
329
339
|
sectionDispose.delete(sid);
|
|
330
340
|
let text = '';
|
|
341
|
+
let ws = '';
|
|
331
342
|
try {
|
|
332
|
-
|
|
333
|
-
//
|
|
334
|
-
const
|
|
343
|
+
ws = workspaceForAgent(agent);
|
|
344
|
+
// 体系注册与授权状态均按会话实时计算:写入磁盘后重装一次即生效,不另设注入点。
|
|
345
|
+
const system = systemBlock(ws, info);
|
|
335
346
|
const consents = activeConsentsFor(sid);
|
|
336
|
-
text = manualFor(info, { ws, cfg, move: typeof env.moveInfo === 'function' ? env.moveInfo() : null,
|
|
347
|
+
text = manualFor(info, { ws, cfg, move: typeof env.moveInfo === 'function' ? env.moveInfo() : null, system, consents });
|
|
337
348
|
consentSig.set(sid, consents.map((r) => r.kind + ':' + r.until).join('|'));
|
|
349
|
+
systemSig.set(sid, registrySignature(ws, info));
|
|
338
350
|
} catch (e) {
|
|
339
351
|
initErrors.push('角色提示段:' + msgOf(e));
|
|
340
352
|
return;
|
|
341
353
|
}
|
|
342
354
|
const sc = agent && agent.ctx ? agent.ctx : null;
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
355
|
+
if (text && sc && sc.systemPrompt && typeof sc.systemPrompt.section === 'function') {
|
|
356
|
+
try {
|
|
357
|
+
sectionDispose.set(sid, sc.systemPrompt.section({ name: 'dsh-handover:manual', order: 100, text, interpolate: false }));
|
|
358
|
+
} catch (e) {
|
|
359
|
+
// 插件重载后,旧实例注册在 agent 作用域上的同名提示段仍然存在。
|
|
360
|
+
// 宿主按 agent 持有该段,插件卸载不移除;该段既无法移除,也不能重复注册。
|
|
361
|
+
// 因此视为已就位并沿用旧文本;修改文本须重启 DSH 或重建会话。
|
|
362
|
+
if (!/already registered/.test(msgOf(e))) initErrors.push('角色提示段:' + msgOf(e));
|
|
363
|
+
}
|
|
350
364
|
}
|
|
351
365
|
}
|
|
352
366
|
|
|
367
|
+
|
|
353
368
|
/** 旧内核(没有按步解析)才用的限制式裁剪。 */
|
|
354
369
|
function applyLegacyRestrict(agent, sid, info) {
|
|
355
370
|
const old = legacyRestrict.get(sid);
|
|
@@ -364,7 +379,7 @@ export function apply(ctx, config) {
|
|
|
364
379
|
} catch { /* 旧内核裁剪失败只影响可见工具,不影响其余功能 */ }
|
|
365
380
|
}
|
|
366
381
|
|
|
367
|
-
/**
|
|
382
|
+
/** 使一个活体会话的提示段与本会话当前角色对齐。info 可传入已解析的角色,省去一次判定。 */
|
|
368
383
|
function applyRoleTo(agent, infoOpt) {
|
|
369
384
|
const h = agent && agent.session ? agent.session.header : null;
|
|
370
385
|
const sid = String((h && h.id) || (agent && agent.id) || '');
|
|
@@ -379,32 +394,98 @@ export function apply(ctx, config) {
|
|
|
379
394
|
roleState.last = { sid, role: info.role, at: stampTz(Date.now()) };
|
|
380
395
|
}
|
|
381
396
|
|
|
382
|
-
/**
|
|
397
|
+
/** 设置角色:写入角色表,按需完成身份互转,存活会话当场更换提示段,随后投递首轮指令。 */
|
|
383
398
|
async function applyRole(input) {
|
|
384
399
|
const i = input || {};
|
|
385
400
|
const sid = String(i.sessionId || '').trim();
|
|
386
401
|
const role = String(i.role || '').trim();
|
|
387
402
|
const project = String(i.project || '').trim();
|
|
388
403
|
const task = String(i.task || '').trim();
|
|
389
|
-
if (!sid) return { ok: false, error: '
|
|
390
|
-
if (ROLES.indexOf(role) === -1) return { ok: false, error: '
|
|
391
|
-
if (role === 'project' && !project) return { ok: false, error: '
|
|
392
|
-
if (role === 'misc' && !task) return { ok: false, error: '
|
|
393
|
-
//
|
|
404
|
+
if (!sid) return { ok: false, error: '缺少会话 id' };
|
|
405
|
+
if (ROLES.indexOf(role) === -1) return { ok: false, error: '角色只接受 global、project、misc、plain' };
|
|
406
|
+
if (role === 'project' && !project) return { ok: false, error: '项目角色须填写项目名' };
|
|
407
|
+
if (role === 'misc' && !task) return { ok: false, error: '杂项角色须填写任务名' };
|
|
408
|
+
// 名称要落成目录:不合法时立即拒绝,不待写角色表或拼接路径时才失败。
|
|
394
409
|
if (role === 'project') { try { assertProjectName(project); } catch (e) { return { ok: false, error: msgOf(e) }; } }
|
|
395
410
|
if (role === 'misc') { try { assertTaskName(task); } catch (e) { return { ok: false, error: msgOf(e) }; } }
|
|
411
|
+
const ws = routeWorkspace(env);
|
|
412
|
+
// 仅在无全局会话时允许将会话设为全局;已有在任全局会话时,只允许同一会话重复确认。
|
|
413
|
+
if (role === 'global') {
|
|
414
|
+
try {
|
|
415
|
+
const cur = loadOffice(ws).lineages['全局'] && loadOffice(ws).lineages['全局'].current;
|
|
416
|
+
if (cur && cur.sessionId && String(cur.sessionId) !== sid) {
|
|
417
|
+
return { ok: false, error: '已有全局在任:' + (cur.name || '') + '(' + cur.sessionId + ')。同一时刻仅允许一个全局会话,须先在设置页处理在任会话。' };
|
|
418
|
+
}
|
|
419
|
+
} catch { /* 登记表无法读取时不阻断,按首次设置处理 */ }
|
|
420
|
+
}
|
|
421
|
+
const before = readRoles(cfg).sessions[sid] || {};
|
|
422
|
+
const prevRole = String(before.role || '');
|
|
423
|
+
let migration = null;
|
|
424
|
+
// 项目与杂项互转:先做快照再移动,或按用户选择直接新建。
|
|
425
|
+
if ((prevRole === 'project' || prevRole === 'misc') && (role === 'project' || role === 'misc') && prevRole !== role) {
|
|
426
|
+
try {
|
|
427
|
+
migration = migrateIdentity({
|
|
428
|
+
ws,
|
|
429
|
+
prev: { role: prevRole, project: String(before.project || ''), task: String(before.task || '') },
|
|
430
|
+
next: { role, project, task },
|
|
431
|
+
mode: i.migrate === 'direct' ? 'direct' : 'auto',
|
|
432
|
+
});
|
|
433
|
+
} catch (e) {
|
|
434
|
+
return { ok: false, error: '身份互转未完成:' + msgOf(e) };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
396
437
|
try { writeRole(cfg, sid, { role, project, task, source: 'user' }); }
|
|
397
438
|
catch (e) { return { ok: false, error: '写角色表失败:' + msgOf(e) }; }
|
|
439
|
+
// 注册同步:项目、杂项、全局角色更新名册类别;普通角色只清除注册,文件与目录原位保留。
|
|
440
|
+
try {
|
|
441
|
+
const oldProjectDir = prevRole === 'project' && before.project ? path.join(ws, '_Project_Meta', String(before.project)) : '';
|
|
442
|
+
const oldTaskDir = prevRole === 'misc' && before.task ? path.join(ws, '_Misc', 'tasks', String(before.task)) : '';
|
|
443
|
+
const markerOf = (dir) => path.join(dir, '.deregistered.json');
|
|
444
|
+
if (role === 'project') {
|
|
445
|
+
fs.mkdirSync(path.join(ws, '_Project_Meta', project), { recursive: true });
|
|
446
|
+
try { fs.rmSync(markerOf(path.join(ws, '_Project_Meta', project)), { force: true }); } catch { /* ignore */ }
|
|
447
|
+
}
|
|
448
|
+
if (role === 'misc') {
|
|
449
|
+
fs.mkdirSync(path.join(ws, '_Misc', 'tasks', task), { recursive: true });
|
|
450
|
+
try { fs.rmSync(markerOf(path.join(ws, '_Misc', 'tasks', task)), { force: true }); } catch { /* ignore */ }
|
|
451
|
+
}
|
|
452
|
+
if (role === 'plain') {
|
|
453
|
+
for (const dir of [oldProjectDir, oldTaskDir]) {
|
|
454
|
+
if (!dir || !fs.existsSync(dir)) continue;
|
|
455
|
+
try { fs.writeFileSync(markerOf(dir), JSON.stringify({ at: new Date().toISOString(), sessionId: sid }), 'utf8'); } catch { /* ignore */ }
|
|
456
|
+
}
|
|
457
|
+
if (prevRole === 'project' && before.project) { try { removeRegistryProject(ws, String(before.project)); } catch { /* ignore */ } }
|
|
458
|
+
if (prevRole === 'project' || prevRole === 'misc' || prevRole === 'global') {
|
|
459
|
+
try { removeHolder(ws, { kind: prevRole, project: String(before.project || ''), task: String(before.task || ''), sessionId: sid }); } catch { /* ignore */ }
|
|
460
|
+
}
|
|
461
|
+
} else if (role === 'project' || role === 'misc' || role === 'global') {
|
|
462
|
+
const okKind = syncHolderKind(ws, { kind: role, project, task, sessionId: sid });
|
|
463
|
+
if (!okKind) {
|
|
464
|
+
registerHolder(ws, { kind: role, project, task, name: titleOf(sid) || (role === 'project' ? project : task), sessionId: sid });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} catch (e) { void e; }
|
|
398
468
|
roleCache.delete(sid);
|
|
399
469
|
const info = { role, project, task, source: 'store', sessionId: sid };
|
|
400
470
|
let live = false;
|
|
401
471
|
try {
|
|
402
472
|
const a = agentOf(sid);
|
|
403
473
|
if (a) { live = true; applyRoleTo(a, info); }
|
|
404
|
-
} catch { /*
|
|
474
|
+
} catch { /* 活体会话装配失败不影响写盘 */ }
|
|
405
475
|
let delivered = false;
|
|
406
476
|
let error = '';
|
|
407
|
-
|
|
477
|
+
let registeredName = '';
|
|
478
|
+
try {
|
|
479
|
+
const lin = { project: role === 'project' ? project : '', task: role === 'misc' ? task : '', kind: role };
|
|
480
|
+
const cur = loadOffice(ws).lineages[lineageKeyOf(lin.kind, lin.project, lin.task)];
|
|
481
|
+
if (cur && cur.current && String(cur.current.sessionId) === sid) registeredName = String(cur.current.name || '');
|
|
482
|
+
} catch { /* 无法读取登记表时不写名字 */ }
|
|
483
|
+
const msg = firstRoundMessageFor(info, {
|
|
484
|
+
previousRole: prevRole,
|
|
485
|
+
migrate: i.migrate === 'direct' ? 'direct' : 'auto',
|
|
486
|
+
moveNote: migration ? migration.summary : '',
|
|
487
|
+
name: registeredName,
|
|
488
|
+
});
|
|
408
489
|
if (i.wake !== false && msg) {
|
|
409
490
|
try {
|
|
410
491
|
const r = await sendPrompt(env.controller(), sid, msg, 'queue');
|
|
@@ -413,11 +494,16 @@ export function apply(ctx, config) {
|
|
|
413
494
|
error = msgOf(e);
|
|
414
495
|
}
|
|
415
496
|
}
|
|
416
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
ok: true, role, project, task, live, delivered, error,
|
|
499
|
+
migrated: migration ? migration.mode : '',
|
|
500
|
+
migrationSummary: migration ? migration.summary : '',
|
|
501
|
+
registeredName,
|
|
502
|
+
};
|
|
417
503
|
}
|
|
418
504
|
env.applyRole = applyRole;
|
|
419
505
|
|
|
420
|
-
/**
|
|
506
|
+
/** 会话新建后立即按已登记角色装配(不唤醒会话、不投递消息)。 */
|
|
421
507
|
env.applyScopedLive = (sid, info) => {
|
|
422
508
|
try {
|
|
423
509
|
const a = agentOf(String(sid || ''));
|
|
@@ -429,7 +515,7 @@ export function apply(ctx, config) {
|
|
|
429
515
|
} catch { return false; }
|
|
430
516
|
};
|
|
431
517
|
|
|
432
|
-
/**
|
|
518
|
+
/** 重装某会话的提示段:文档定位写盘后与授权到达后各调用一次;不唤醒会话,不投递消息。 */
|
|
433
519
|
env.refreshPromptFor = (sid) => {
|
|
434
520
|
try {
|
|
435
521
|
const id = String(sid || '');
|
|
@@ -441,8 +527,8 @@ export function apply(ctx, config) {
|
|
|
441
527
|
} catch { return false; }
|
|
442
528
|
};
|
|
443
529
|
|
|
444
|
-
// ----------------
|
|
445
|
-
//
|
|
530
|
+
// ---------------- 调度器(续跑文案须按角色取交接路径)
|
|
531
|
+
// 注意:调度器收到的是会话 id 字符串,不是 exec;此处使用按 id 解析的版本,直接读取角色表与磁盘。
|
|
446
532
|
const schedDeps = { ctx, cfg, logger: (ctx.logger || null), roleOf: null, handoverFor: null };
|
|
447
533
|
try {
|
|
448
534
|
scheduler = createScheduler(schedDeps);
|
|
@@ -450,8 +536,8 @@ export function apply(ctx, config) {
|
|
|
450
536
|
} catch (e) { schedulerError = msgOf(e); }
|
|
451
537
|
Object.assign(schedDeps, schedDepsFor(env));
|
|
452
538
|
|
|
453
|
-
// ---------------- 用户全局前置提示词(段序 950
|
|
454
|
-
//
|
|
539
|
+
// ---------------- 用户全局前置提示词(段序 950,位于工具段之前)
|
|
540
|
+
// 与插件自身的会话约束无关:仅在用户开启且内容非空时注册;关闭或留空时不占用注入位置。
|
|
455
541
|
let extraDispose = null;
|
|
456
542
|
function applyExtraPrompt(state) {
|
|
457
543
|
try { if (typeof extraDispose === 'function') extraDispose(); } catch { /* 旧段已失效 */ }
|
|
@@ -481,11 +567,11 @@ export function apply(ctx, config) {
|
|
|
481
567
|
};
|
|
482
568
|
try { applyExtraPrompt(readExtraPrompt()); } catch (e) { initErrors.push('全局前置提示词:' + msgOf(e)); }
|
|
483
569
|
|
|
484
|
-
// ---------------- 工具注册(13 个 ho_*
|
|
485
|
-
// 0.2.2
|
|
486
|
-
// 不再另用 agent 作用域 restrict
|
|
570
|
+
// ---------------- 工具注册(13 个 ho_* 与 1 个 peer)
|
|
571
|
+
// 自 0.2.2 起,工具按「步」解析:可用工具集合每一步重算。子会话排除与角色过滤在此一并完成,
|
|
572
|
+
// 不再另用 agent 作用域 restrict;与 restrict 并用时,换角色或须等待重启,或互相覆盖。
|
|
487
573
|
const roleToolsets = new Map(ROLES.map((r) => [r, new Set(toolsetFor(r).allow)]));
|
|
488
|
-
/**
|
|
574
|
+
/** 每步过滤:子会话一律不可见;角色按当前角色表计算;无 agent 的路径(命令行、演练)放行。 */
|
|
489
575
|
function visibleTo(name) {
|
|
490
576
|
return (c) => {
|
|
491
577
|
const agent = c && c.agent ? c.agent : null;
|
|
@@ -506,6 +592,29 @@ export function apply(ctx, config) {
|
|
|
506
592
|
return allow ? allow.has(name) : true;
|
|
507
593
|
};
|
|
508
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* 提醒尾注入:插件工具返回结果后,在文本末尾追加一条 [System Note: …]。
|
|
597
|
+
* 只修改本次工具结果。
|
|
598
|
+
* 不改动系统提示词(避免改写前缀缓存),不写会话事件,不另起消息,不中断工具执行。
|
|
599
|
+
*/
|
|
600
|
+
function withReminderTail(def) {
|
|
601
|
+
if (!def || typeof def.execute !== 'function') return def;
|
|
602
|
+
const run = def.execute.bind(def);
|
|
603
|
+
return {
|
|
604
|
+
...def,
|
|
605
|
+
async execute(args, exec) {
|
|
606
|
+
const res = await run(args, exec);
|
|
607
|
+
try {
|
|
608
|
+
const sid = env.selfId(exec);
|
|
609
|
+
if (sid && res && typeof res.text === 'string' && res.text !== '') {
|
|
610
|
+
const note = reminderTailNote(cfg, sid);
|
|
611
|
+
if (note) return { ...res, text: res.text + String.fromCharCode(10, 10) + note };
|
|
612
|
+
}
|
|
613
|
+
} catch { /* 追加失败不影响工具结果 */ }
|
|
614
|
+
return res;
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
509
618
|
const tools = [
|
|
510
619
|
pmStatusTool(env, { schedulerReady: () => scheduler !== null, schedulerError: () => schedulerError }),
|
|
511
620
|
pmGateTool(env),
|
|
@@ -525,10 +634,10 @@ export function apply(ctx, config) {
|
|
|
525
634
|
for (let i = 0; i < tools.length; i++) {
|
|
526
635
|
const key = keys[i];
|
|
527
636
|
try {
|
|
528
|
-
//
|
|
637
|
+
// 名称以工厂自带的为准;工厂未写时才用位置表补充,顺序错位时报告(不再静默使用错误名称)。
|
|
529
638
|
if (!tools[i].name) tools[i].name = NAMES[key];
|
|
530
639
|
else if (tools[i].name !== NAMES[key]) initErrors.push('工具名错位:数组第 ' + i + ' 项是 ' + tools[i].name + ',位置表期望 ' + NAMES[key]);
|
|
531
|
-
ctx.tools.register(tools[i], { when: visibleTo(tools[i].name) });
|
|
640
|
+
ctx.tools.register(withReminderTail(tools[i]), { when: visibleTo(tools[i].name) });
|
|
532
641
|
registered.add(tools[i].name);
|
|
533
642
|
} catch (e) {
|
|
534
643
|
initErrors.push(key + ':' + msgOf(e));
|
|
@@ -538,15 +647,16 @@ export function apply(ctx, config) {
|
|
|
538
647
|
const peerTools = [pmPeerTool(env)];
|
|
539
648
|
for (const t of peerTools) {
|
|
540
649
|
try {
|
|
541
|
-
ctx.tools.register(t, { when: visibleTo(t && t.name ? String(t.name) : '') });
|
|
650
|
+
ctx.tools.register(withReminderTail(t), { when: visibleTo(t && t.name ? String(t.name) : '') });
|
|
542
651
|
if (t && t.name) registered.add(t.name);
|
|
543
652
|
} catch (e) {
|
|
544
653
|
initErrors.push((t && t.name ? String(t.name) : 'peer') + ':' + msgOf(e));
|
|
545
654
|
}
|
|
546
655
|
}
|
|
547
656
|
|
|
548
|
-
// ---------------- 会话守卫(Charter §7.2.4
|
|
549
|
-
//
|
|
657
|
+
// ---------------- 会话守卫(Charter §7.2.4):新建会话与设置角色仅属全局;
|
|
658
|
+
// 会话之间可直接用 peer(action=send)通信,无须经全局转述。
|
|
659
|
+
// directPeer=off 时退回「只能向全局报告」的旧口径。
|
|
550
660
|
if (cfg.wakeAuthority !== 'off' && typeof ctx.tools.guard === 'function') {
|
|
551
661
|
try {
|
|
552
662
|
ctx.tools.guard((exec) => {
|
|
@@ -555,7 +665,7 @@ export function apply(ctx, config) {
|
|
|
555
665
|
let args = (exec && exec.arguments) || {};
|
|
556
666
|
if (typeof args === 'string') { try { args = JSON.parse(args); } catch { args = {}; } }
|
|
557
667
|
const action = String(args.action || '');
|
|
558
|
-
// list/status/peek/rename
|
|
668
|
+
// list/status/peek/rename 的权限由工具内部按角色判定,此处只处理 2 类有外部影响的动作。
|
|
559
669
|
if (action !== 'send' && action !== 'new') return undefined;
|
|
560
670
|
const sid = env.selfId(exec) || '';
|
|
561
671
|
let info;
|
|
@@ -563,37 +673,54 @@ export function apply(ctx, config) {
|
|
|
563
673
|
const mk = (allow, why) => {
|
|
564
674
|
guardLast = { sid, allow, source: why, title: titleOf(sid) };
|
|
565
675
|
if (allow) return undefined;
|
|
566
|
-
if (action === 'new') return '
|
|
567
|
-
return '会话间通信受限(directPeer=off
|
|
676
|
+
if (action === 'new') return '会话创建仅属全局:非全局会话不得新建平级会话(杂项会话建在 Misc 下者除外)。';
|
|
677
|
+
return '会话间通信受限(directPeer=off):非全局会话只能向在任全局会话报告,不得唤醒其他会话。';
|
|
568
678
|
};
|
|
569
679
|
if (info.role === 'global') return mk(true, '全局角色');
|
|
570
680
|
if (action === 'send') {
|
|
571
|
-
//
|
|
681
|
+
// 会话之间可直接通信;向全局报告亦走此路径。仅 directPeer=off 时退回旧口径。
|
|
572
682
|
const target = String(args.session_id || '');
|
|
573
683
|
let t = null;
|
|
574
684
|
try { t = target ? infoFor(target, {}) : null; } catch { t = null; }
|
|
575
685
|
const tRole = t ? t.role : '未知';
|
|
576
|
-
if (t && t.role === 'global') return mk(true, '
|
|
577
|
-
if (cfg.directPeer !== 'off') return mk(true, '会话之间直接通信(' + info.role + '
|
|
578
|
-
return mk(false, 'directPeer=off:仅允许向全局报告(' + info.role + '
|
|
686
|
+
if (t && t.role === 'global') return mk(true, '向在任全局会话报告');
|
|
687
|
+
if (cfg.directPeer !== 'off') return mk(true, '会话之间直接通信(' + info.role + ' 至 ' + tRole + ')');
|
|
688
|
+
return mk(false, 'directPeer=off:仅允许向全局报告(' + info.role + ' 至 ' + tRole + ')');
|
|
579
689
|
}
|
|
580
|
-
// action=new
|
|
690
|
+
// action=new:只有杂项会话可以建在 Misc 下
|
|
581
691
|
const target = String(args.workspace || args.cwd || '').toLowerCase();
|
|
582
|
-
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, '
|
|
692
|
+
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, '杂项会话新建于 Misc');
|
|
583
693
|
return mk(false, '非全局角色不得新建平级会话(' + info.role + ')');
|
|
584
694
|
});
|
|
585
|
-
} catch { /*
|
|
695
|
+
} catch { /* 旧版本无 guard:退化为规则约束 */ }
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---------------- 协作守卫:协作注册期间,被交办方不得经由 DSH 自带的 send_message 发送明文消息。
|
|
699
|
+
// 不改动 DSH 本体,只挂一层工具守卫;普通会话与普通对话不受影响。
|
|
700
|
+
if (typeof ctx.tools.guard === 'function') {
|
|
701
|
+
try {
|
|
702
|
+
ctx.tools.guard((exec) => {
|
|
703
|
+
const nm = exec && exec.name;
|
|
704
|
+
if (nm !== 'send_message') return undefined;
|
|
705
|
+
const sid = env.selfId(exec) || '';
|
|
706
|
+
let why = '';
|
|
707
|
+
try { why = collabMemberReason(sid); } catch { why = ''; }
|
|
708
|
+
if (!why) return undefined;
|
|
709
|
+
guardLast = { sid, allow: false, source: '协作注册', title: titleOf(sid) };
|
|
710
|
+
return why;
|
|
711
|
+
});
|
|
712
|
+
} catch { /* 旧版本无 guard:退化为规则约束 */ }
|
|
586
713
|
}
|
|
587
714
|
|
|
588
715
|
// ---------------- 会话生命周期:创建时装配提示段;开轮前按角色表核对一次
|
|
589
|
-
//
|
|
716
|
+
// 此处不处理工具可见性:自 0.2.2 起,每步条件已按当前角色计算(见 visibleTo),换角色下一轮即生效。
|
|
590
717
|
function scopedMetaOf(h, sid) {
|
|
591
718
|
return { title: (h && h.title) || titleOf(sid), cwd: (h && h.cwd) || '' };
|
|
592
719
|
}
|
|
593
720
|
if (typeof ctx.on === 'function') {
|
|
594
721
|
try {
|
|
595
|
-
// 事件作用域:宿主以 agent
|
|
596
|
-
//
|
|
722
|
+
// 事件作用域:宿主以 agent 作为事件的作用域载体。
|
|
723
|
+
// 插件在根作用域直接注册时,收不到其他会话的事件。传入 { global: true } 才能收到全部会话的事件(0.2.2 支持;旧内核忽略该项,退回直接注册)。
|
|
597
724
|
const onAgentEvent = (name, fn) => {
|
|
598
725
|
try { ctx.on(name, fn, { global: true }); return true; }
|
|
599
726
|
catch { try { ctx.on(name, fn); return true; } catch (e) { env.log('会话事件监听失败(' + name + '):' + msgOf(e)); return false; } }
|
|
@@ -601,7 +728,11 @@ export function apply(ctx, config) {
|
|
|
601
728
|
onAgentEvent('agent/created', (payload) => {
|
|
602
729
|
try {
|
|
603
730
|
const agent = payload && payload.agent ? payload.agent : null;
|
|
604
|
-
if (agent)
|
|
731
|
+
if (agent) {
|
|
732
|
+
const h = agent.session && agent.session.header ? agent.session.header : null;
|
|
733
|
+
const sid0 = String((h && h.id) || agent.id || '');
|
|
734
|
+
applyRoleTo(agent);
|
|
735
|
+
}
|
|
605
736
|
} catch (e) { env.log('角色装配失败:' + msgOf(e)); }
|
|
606
737
|
});
|
|
607
738
|
onAgentEvent('agent/disposed', (payload) => {
|
|
@@ -617,25 +748,29 @@ export function apply(ctx, config) {
|
|
|
617
748
|
if (typeof ld === 'function') { try { ld(); } catch { /* ignore */ } }
|
|
618
749
|
legacyRestrict.delete(sid);
|
|
619
750
|
appliedRole.delete(sid);
|
|
751
|
+
systemSig.delete(sid);
|
|
620
752
|
roleCache.delete(sid);
|
|
621
753
|
} catch { /* ignore */ }
|
|
622
754
|
});
|
|
623
|
-
//
|
|
624
|
-
//
|
|
755
|
+
// 开轮前核对:休眠期间角色可能已被修改,宿主恢复同一活体时不保证再次发出创建事件。
|
|
756
|
+
// 状态切换到 running 时核对一次,使提示段与旧内核的限制同角色表对齐。
|
|
625
757
|
onAgentEvent('agent/status', (payload) => {
|
|
626
758
|
try {
|
|
627
|
-
if (!payload
|
|
759
|
+
if (!payload) return;
|
|
628
760
|
const agent = payload.agent || null;
|
|
629
761
|
if (!agent) return;
|
|
630
762
|
const h = agent.session && agent.session.header ? agent.session.header : null;
|
|
631
763
|
const sid = String((h && h.id) || agent.id || '');
|
|
632
764
|
if (!sid) return;
|
|
765
|
+
if (payload.status !== 'running') return;
|
|
633
766
|
const info = infoFor(sid, scopedMetaOf(h, sid));
|
|
634
|
-
//
|
|
767
|
+
// 角色未变时仍须核对授权签名与体系注册签名:授权到达或注册改写后,下一轮即应看到新内容。
|
|
635
768
|
let sig = '';
|
|
636
769
|
try { sig = activeConsentsFor(sid).map((r) => r.kind + ':' + r.until).join('|'); } catch { sig = ''; }
|
|
637
|
-
|
|
638
|
-
|
|
770
|
+
let sysSig = '';
|
|
771
|
+
try { sysSig = registrySignature(workspaceForAgent(agent), info); } catch { sysSig = ''; }
|
|
772
|
+
if (appliedRole.get(sid) === info.role && consentSig.get(sid) === sig && systemSig.get(sid) === sysSig) return;
|
|
773
|
+
env.log('开轮前重装提示段:' + sid + ' · 已装配 ' + (appliedRole.get(sid) || '-') + ' · 目标 ' + info.role);
|
|
639
774
|
applyRoleTo(agent, info);
|
|
640
775
|
} catch (e) { env.log('开轮前装配失败:' + msgOf(e)); }
|
|
641
776
|
});
|
|
@@ -643,9 +778,9 @@ export function apply(ctx, config) {
|
|
|
643
778
|
initErrors.push('会话事件监听:' + msgOf(e));
|
|
644
779
|
}
|
|
645
780
|
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
// agents 未在 inject
|
|
781
|
+
// 插件重新加载(停用后启用,或启动时活体会话早于插件)时,已存在的会话不再收到
|
|
782
|
+
// 创建事件;此处补充一次装配,否则其提示段会随旧实例一起消失。
|
|
783
|
+
// agents 未在 inject 中声明,激活瞬间可能尚未就绪,故首次未取到时延迟重试一次。
|
|
649
784
|
const reapplyLive = () => {
|
|
650
785
|
let agents = null;
|
|
651
786
|
try { agents = ctx.agents; } catch { agents = null; }
|
|
@@ -655,9 +790,15 @@ export function apply(ctx, config) {
|
|
|
655
790
|
if (!agents || typeof agents.list !== 'function') return 0;
|
|
656
791
|
let n = 0;
|
|
657
792
|
for (const a of agents.list()) {
|
|
658
|
-
try {
|
|
793
|
+
try {
|
|
794
|
+
// 插件重载时正在运行的会话不再收到 running 事件,此处补记轮内状态,
|
|
795
|
+
// 否则提醒须待下一轮才判定。
|
|
796
|
+
const h = a && a.session && a.session.header ? a.session.header : null;
|
|
797
|
+
const sid = String((h && h.id) || (a && a.id) || '');
|
|
798
|
+
applyRoleTo(a); n++;
|
|
799
|
+
} catch { /* 单个会话装配失败不影响插件激活 */ }
|
|
659
800
|
}
|
|
660
|
-
if (n > 0) env.log('
|
|
801
|
+
if (n > 0) env.log('补充装配 ' + n + ' 个活体会话');
|
|
661
802
|
return n;
|
|
662
803
|
};
|
|
663
804
|
try {
|
|
@@ -665,10 +806,10 @@ export function apply(ctx, config) {
|
|
|
665
806
|
const t = setTimeout(() => { try { reapplyLive(); } catch { /* ignore */ } }, 1200);
|
|
666
807
|
if (t && typeof t.unref === 'function') t.unref();
|
|
667
808
|
}
|
|
668
|
-
} catch { /*
|
|
809
|
+
} catch { /* 无法取得 agents 服务时跳过补充装配 */ }
|
|
669
810
|
}
|
|
670
811
|
|
|
671
|
-
// ----------------
|
|
812
|
+
// ---------------- 设置页数据接口:主通道为 DSH 认证的 /api 载体,旧通道为 /dsh-handover HTTP 路由
|
|
672
813
|
try {
|
|
673
814
|
const info = pkgInfo();
|
|
674
815
|
const pageInfo = {
|
|
@@ -681,9 +822,9 @@ export function apply(ctx, config) {
|
|
|
681
822
|
env.api = api;
|
|
682
823
|
registerStatusRoute(ctx, env, pageInfo, api);
|
|
683
824
|
registerRpcCarrier(ctx, api, (m) => env.log(m));
|
|
684
|
-
//
|
|
825
|
+
// 预热状态快照:设置页直接读取缓存,避免请求时现算等待。
|
|
685
826
|
try {
|
|
686
|
-
//
|
|
827
|
+
// 仅在最近 5 分钟内有人查看设置页时预热:无人查看即不计算,避免空转占用宿主主线程。
|
|
687
828
|
const warm = () => {
|
|
688
829
|
if (Date.now() - (env.lastStatusCallAt || 0) > 5 * 60 * 1000) return;
|
|
689
830
|
try { statusSnapshot(env, pageInfo, 'warm'); } catch { /* ignore */ }
|
|
@@ -698,23 +839,23 @@ export function apply(ctx, config) {
|
|
|
698
839
|
initErrors.push('设置页路由:' + msgOf(e));
|
|
699
840
|
}
|
|
700
841
|
|
|
701
|
-
// ----------------
|
|
702
|
-
//
|
|
703
|
-
//
|
|
842
|
+
// ---------------- 聊天页命令:用户在输入框输入 /allow-compact、/allow-succeed 即可授权
|
|
843
|
+
// 设计理由:设置页须切换页面并填写会话 id,操作步骤较多。斜杠命令由用户亲手输入,
|
|
844
|
+
// 天然满足「同意只能由用户作出」的判据,且不产生模型消息、不消耗 token。
|
|
704
845
|
try {
|
|
705
846
|
const allowCommands = [
|
|
706
847
|
{
|
|
707
848
|
name: 'allow-compact',
|
|
708
|
-
description: '
|
|
709
|
-
hint: '
|
|
849
|
+
description: '允许本会话压缩一次(由当前用户授权,5 分钟有效;可附会话短号授权其他会话)',
|
|
850
|
+
hint: '可留空,默认授权本会话;填写 8 位短号或 session-id 则授权对应会话',
|
|
710
851
|
kind: 'compact',
|
|
711
852
|
note: '聊天页命令 /allow-compact',
|
|
712
853
|
action: '压缩',
|
|
713
854
|
},
|
|
714
855
|
{
|
|
715
856
|
name: 'allow-succeed',
|
|
716
|
-
description: '
|
|
717
|
-
hint: '
|
|
857
|
+
description: '允许本会话换任一次(由当前用户授权,5 分钟有效;可附会话短号授权其他会话)',
|
|
858
|
+
hint: '可留空,默认授权本会话;填写 8 位短号或 session-id 则授权对应会话',
|
|
718
859
|
kind: 'succeed',
|
|
719
860
|
note: '聊天页命令 /allow-succeed',
|
|
720
861
|
action: '换任',
|
|
@@ -732,10 +873,10 @@ export function apply(ctx, config) {
|
|
|
732
873
|
const sid = arg ? resolveShortSid(env, arg) : (agent && agent.id ? String(agent.id) : '');
|
|
733
874
|
if (!sid) return { kind: 'error', text: '无法获取会话 id:请写成 /' + cmd.name + ' session-xxxx' };
|
|
734
875
|
const g = grantConsent(sid, cmd.note, cmd.kind);
|
|
735
|
-
//
|
|
876
|
+
// 授权到达后立即重装提示段:目标会话下一轮即可看到该行(当前空闲亦然)。
|
|
736
877
|
try { env.refreshPromptFor(sid); } catch { /* 装配失败不影响授权本身 */ }
|
|
737
878
|
try { env.log(cmd.action + '授权(命令):' + sid); } catch { /* ignore */ }
|
|
738
|
-
return { kind: 'success', text: '已允许 ' + sid + ' 在 ' + g.minutes + ' 分钟内' + cmd.action + '
|
|
879
|
+
return { kind: 'success', text: '已允许 ' + sid + ' 在 ' + g.minutes + ' 分钟内' + cmd.action + '一次(用后即失效)。' };
|
|
739
880
|
},
|
|
740
881
|
});
|
|
741
882
|
if (typeof d === 'function' && typeof ctx.effect === 'function') {
|
|
@@ -743,7 +884,7 @@ export function apply(ctx, config) {
|
|
|
743
884
|
}
|
|
744
885
|
}
|
|
745
886
|
};
|
|
746
|
-
//
|
|
887
|
+
// 可选服务:可用 ctx.inject 时走注入(旧宿主没有 commands 时插件照常激活),否则直接获取。
|
|
747
888
|
if (typeof ctx.inject === 'function') {
|
|
748
889
|
ctx.inject(['commands'], (scope) => {
|
|
749
890
|
try { registerAllow(scope && scope.commands); } catch (e) { initErrors.push('命令注册:' + msgOf(e)); }
|
|
@@ -754,4 +895,27 @@ export function apply(ctx, config) {
|
|
|
754
895
|
} catch (e) {
|
|
755
896
|
initErrors.push('命令注册:' + msgOf(e));
|
|
756
897
|
}
|
|
898
|
+
|
|
899
|
+
// 插件重载后,已存在的会话不会自动重装提示段,故补注册提醒段。
|
|
900
|
+
// 角色提示段(manual)由宿主按 agent 持有,同名段无法修改文本;提醒段为新段,可以补注册。
|
|
901
|
+
try {
|
|
902
|
+
const timer = setTimeout(() => {
|
|
903
|
+
Promise.resolve().then(async () => {
|
|
904
|
+
try {
|
|
905
|
+
const controller = typeof env.controller === 'function' ? env.controller() : null;
|
|
906
|
+
if (!controller || typeof controller.list !== 'function') return;
|
|
907
|
+
const rows = await controller.list();
|
|
908
|
+
for (const row of rows || []) {
|
|
909
|
+
if (!row || row.agentAvailable !== true) continue;
|
|
910
|
+
const info = env.infoOfSid(row.sessionId);
|
|
911
|
+
if (!info || ['global', 'project', 'misc'].indexOf(info.role) === -1) continue;
|
|
912
|
+
env.refreshPromptFor(row.sessionId);
|
|
913
|
+
}
|
|
914
|
+
} catch (e) { void e; }
|
|
915
|
+
});
|
|
916
|
+
}, 1500);
|
|
917
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => () => clearTimeout(timer), 'dsh-handover: 启动补注册提醒段');
|
|
918
|
+
} catch (e) {
|
|
919
|
+
initErrors.push('启动补注册:' + msgOf(e));
|
|
920
|
+
}
|
|
757
921
|
}
|