@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/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';
|
|
@@ -11,60 +12,57 @@ import { resolveConfig } from './lib/config.mjs';
|
|
|
11
12
|
import { projCacheDir, pressurePct } from './lib/sessions.mjs';
|
|
12
13
|
import { createScheduler } from './lib/schedule.mjs';
|
|
13
14
|
import { compactionStateOf, stalePressureNote } from './lib/events.mjs';
|
|
14
|
-
import {
|
|
15
|
+
import { pmStatusTool } from './tools/ho_status.mjs';
|
|
15
16
|
import { pmGateTool } from './tools/ho_gate.mjs';
|
|
16
17
|
import { pmSessionsTool } from './tools/ho_sessions.mjs';
|
|
17
|
-
import { pmPhaseTool } from './tools/ho_phase.mjs';
|
|
18
18
|
import { pmCharterTool } from './tools/ho_charter.mjs';
|
|
19
19
|
import { pmHandoverTool } from './tools/ho_handover.mjs';
|
|
20
20
|
import { pmCheckpointTool } from './tools/ho_checkpoint.mjs';
|
|
21
|
-
import { pmSnapshotTool } from './tools/ho_snapshot.mjs';
|
|
22
|
-
import { pmDiagTool } from './tools/ho_diag.mjs';
|
|
23
|
-
import { pmDriftTool } from './tools/ho_drift.mjs';
|
|
24
21
|
import { pmChangelogTool } from './tools/ho_changelog.mjs';
|
|
25
|
-
import {
|
|
26
|
-
import { pmPackTool } from './tools/ho_pack.mjs';
|
|
22
|
+
import { pmBackupTool } from './tools/ho_backup.mjs';
|
|
27
23
|
import { pmWorkspaceTool } from './tools/ho_workspace.mjs';
|
|
28
|
-
import { pmInitTool } from './tools/ho_init.mjs';
|
|
29
24
|
import { pmProfileTool } from './tools/ho_profile.mjs';
|
|
30
25
|
import { pmOfficeTool } from './tools/ho_office.mjs';
|
|
31
26
|
import { pmSucceedTool } from './tools/ho_succeed.mjs';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import { pmPeerSendTool } from './tools/peer_send.mjs';
|
|
35
|
-
import { pmPeerNewTool } from './tools/peer_new.mjs';
|
|
36
|
-
import { pmPeerRenameTool } from './tools/peer_rename.mjs';
|
|
37
|
-
import { pmPeerPeekTool } from './tools/peer_peek.mjs';
|
|
27
|
+
import { pmDocmapTool } from './tools/ho_docmap.mjs';
|
|
28
|
+
import { pmPeerTool, collabMemberReason } from './tools/peer.mjs';
|
|
38
29
|
import { resolveRegistry } from './lib/registry.mjs';
|
|
39
30
|
import { registerStatusRoute } from './lib/webroute.mjs';
|
|
40
31
|
import { createApi, statusSnapshot } from './lib/api.mjs';
|
|
41
32
|
import { registerRpcCarrier } from './lib/rpc-carrier.mjs';
|
|
42
|
-
import { NAMES } from './lib/names.mjs';
|
|
33
|
+
import { NAMES, PEER_NAME } from './lib/names.mjs';
|
|
43
34
|
import { readExtraPrompt, writeExtraPrompt, EXTRA_PROMPT_ORDER, EXTRA_PROMPT_SECTION } from './lib/extra-prompt.mjs';
|
|
44
35
|
import {
|
|
45
36
|
ROLES, resolveRole, sessionMeta, handoverPathFor, toolsetFor,
|
|
46
37
|
readRoles, writeRole, assertProjectName, assertTaskName,
|
|
47
38
|
} from './lib/role.mjs';
|
|
48
|
-
import { manualFor,
|
|
39
|
+
import { manualFor, firstRoundMessageFor } from './lib/manual.mjs';
|
|
49
40
|
import { sendPrompt } from './lib/prompt.mjs';
|
|
50
41
|
import { resolveShortSid } from './lib/api.mjs';
|
|
51
42
|
import { routeWorkspace } from './lib/workspace.mjs';
|
|
52
43
|
import { stampTz } from './lib/brief.mjs';
|
|
53
|
-
import { grantConsent } from './lib/consent.mjs';
|
|
44
|
+
import { grantConsent, activeConsentsFor } from './lib/consent.mjs';
|
|
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';
|
|
49
|
+
import { moveStatus } from './lib/mover.mjs';
|
|
54
50
|
|
|
55
51
|
export const name = 'dsh-handover';
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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 明确报错。
|
|
59
57
|
export const inject = ['tools', 'systemPrompt', 'sessionController'];
|
|
60
58
|
|
|
61
|
-
/** 自身包信息:dist
|
|
59
|
+
/** 自身包信息:dist 内读取 ./package.json,源码树内回退到 ../package.json。 */
|
|
62
60
|
function pkgInfo() {
|
|
63
61
|
for (const rel of ['./package.json', '../package.json']) {
|
|
64
62
|
try {
|
|
65
63
|
const j = JSON.parse(fs.readFileSync(new URL(rel, import.meta.url), 'utf8'));
|
|
66
64
|
if (j && j.version) return { name: String(j.name || ''), version: String(j.version) };
|
|
67
|
-
} catch { /*
|
|
65
|
+
} catch { /* 尝试下一个 */ }
|
|
68
66
|
}
|
|
69
67
|
return { name: '@mingmaple/dsh-handover', version: '?' };
|
|
70
68
|
}
|
|
@@ -77,13 +75,14 @@ function walkUpToWorkspace(start) {
|
|
|
77
75
|
if (up === cur) break;
|
|
78
76
|
cur = up;
|
|
79
77
|
}
|
|
80
|
-
// cwd
|
|
81
|
-
//
|
|
82
|
-
//
|
|
78
|
+
// cwd 的父目录已删除(或 cwd 为盘符根)时,向上无法找到工作区。
|
|
79
|
+
// 若原样返回 start,写操作会在该失效路径上重建一套 _Control_Center。
|
|
80
|
+
// 该目录没有对应工作区,后续流程无法识别。
|
|
81
|
+
// 此时以登记表记录的工作区为准;登记表也查不到时才返回 start。
|
|
83
82
|
try {
|
|
84
83
|
const fromReg = routeWorkspace({ cfg: {} });
|
|
85
84
|
if (fromReg && fromReg !== start && isDir(path.join(fromReg, '_Control_Center'))) return fromReg;
|
|
86
|
-
} catch { /*
|
|
85
|
+
} catch { /* 登记表无法读取时保持旧行为 */ }
|
|
87
86
|
return start;
|
|
88
87
|
}
|
|
89
88
|
|
|
@@ -92,7 +91,7 @@ function msgOf(e) {
|
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
|
-
*
|
|
94
|
+
* 压缩调度器依赖的 2 个函数:入参为**会话 id**,须按 id 解析角色与交接路径。
|
|
96
95
|
* 单独导出,便于冒烟测试直接验证。
|
|
97
96
|
*/
|
|
98
97
|
export function schedDepsFor(env) {
|
|
@@ -105,11 +104,13 @@ export function schedDepsFor(env) {
|
|
|
105
104
|
export function apply(ctx, config) {
|
|
106
105
|
const cfg = resolveConfig(config);
|
|
107
106
|
const initErrors = [];
|
|
108
|
-
const registered = new Set(); //
|
|
109
|
-
// 0.2.2
|
|
110
|
-
// 更早的内核忽略注册选项,只能退回 agent 作用域的 restrict
|
|
107
|
+
const registered = new Set(); // 已注册的工具名(旧内核的限制式裁剪使用)
|
|
108
|
+
// 自 0.2.2 起,工具按「步」解析(tools.resolve):可用工具集合每步重算,角色过滤写入注册条件。
|
|
109
|
+
// 更早的内核忽略注册选项,只能退回 agent 作用域的 restrict。2 条路径按能力择一,不并行。
|
|
111
110
|
const perStep = (() => { try { return !!(ctx.tools && typeof ctx.tools.resolve === 'function'); } catch { return false; } })();
|
|
112
111
|
const sectionDispose = new Map(); // sessionId → 撤销提示段
|
|
112
|
+
const systemSig = new Map(); // sessionId → 上次装配时的体系注册签名(热更新)
|
|
113
|
+
const consentSig = new Map(); // sessionId → 上次装配时的授权签名(授权出现或过期时重装)
|
|
113
114
|
const legacyRestrict = new Map(); // sessionId → 撤销限制(仅无按步解析的内核)
|
|
114
115
|
const appliedRole = new Map(); // sessionId → 上次装配的角色
|
|
115
116
|
const roleCache = new Map(); // sessionId → { at, info }(每步过滤用,短 TTL)
|
|
@@ -149,11 +150,11 @@ export function apply(ctx, config) {
|
|
|
149
150
|
},
|
|
150
151
|
schedule(sessionId, n, opts) {
|
|
151
152
|
if (!scheduler) throw new Error("压缩调度器未就绪" + (schedulerError ? ":" + schedulerError : "(服务尚未注入)"));
|
|
152
|
-
// consent
|
|
153
|
+
// consent 已由入口工具校验并消费;此处不再复查,复查必然无效。
|
|
153
154
|
scheduler.request(sessionId, Object.assign({ n, consent: 'checked' }, opts || {})).catch(() => {});
|
|
154
155
|
},
|
|
155
156
|
registry() { return resolveRegistry(ctx); },
|
|
156
|
-
/**
|
|
157
|
+
/** 迁移感知:根目录是否已迁移到其他位置(原路径成为 junction)。供提示段与 ho_status(view="diag")使用。 */
|
|
157
158
|
moveInfo() {
|
|
158
159
|
try {
|
|
159
160
|
const src = cfg.workspaceRoot || walkUpToWorkspace(process.cwd());
|
|
@@ -202,7 +203,7 @@ export function apply(ctx, config) {
|
|
|
202
203
|
staleNote(exec) {
|
|
203
204
|
try { return stalePressureNote(this.selfId(exec)); } catch { return ''; }
|
|
204
205
|
},
|
|
205
|
-
/** 自身版本:从 dist/package.json
|
|
206
|
+
/** 自身版本:从 dist/package.json 读取,设置页与 ho_status(view="diag")共用同一值。 */
|
|
206
207
|
version() {
|
|
207
208
|
try { return pkgInfo().version; } catch { return '?'; }
|
|
208
209
|
},
|
|
@@ -213,7 +214,7 @@ export function apply(ctx, config) {
|
|
|
213
214
|
},
|
|
214
215
|
};
|
|
215
216
|
|
|
216
|
-
// ---------------- 会话标题:运行时 header.title
|
|
217
|
+
// ---------------- 会话标题:运行时 header.title 往往为空,改读投影缓存
|
|
217
218
|
function titleOf(sessionId) {
|
|
218
219
|
try {
|
|
219
220
|
const a = ctx.agents && typeof ctx.agents.get === 'function' ? ctx.agents.get(sessionId) : null;
|
|
@@ -237,9 +238,9 @@ export function apply(ctx, config) {
|
|
|
237
238
|
try {
|
|
238
239
|
const a = ctx.agents && typeof ctx.agents.get === 'function' ? ctx.agents.get(want) : null;
|
|
239
240
|
if (a) return a;
|
|
240
|
-
} catch { /*
|
|
241
|
-
//
|
|
242
|
-
//
|
|
241
|
+
} catch { /* 回退到列表查找 */ }
|
|
242
|
+
// 宿主的注册表键不保证等于会话 id:无法取到实例时,按会话头 id 在活体列表中再查一次。
|
|
243
|
+
// 缺这一层,休眠会话在设置页修改角色时会被判为「不在内存」,须等重启才装配。
|
|
243
244
|
try {
|
|
244
245
|
const list = ctx.agents && typeof ctx.agents.list === 'function' ? (ctx.agents.list() || []) : [];
|
|
245
246
|
for (const a of list) {
|
|
@@ -251,8 +252,9 @@ export function apply(ctx, config) {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
255
|
+
* 仅凭会话头即可判定的子级会话。
|
|
256
|
+
* 子会话与团队成员的会话都带 origin=subagent 或 parentSession。
|
|
257
|
+
* 只读 header,不访问磁盘,可置于「每一步」的条件启用中。
|
|
256
258
|
*/
|
|
257
259
|
function isChildSession(agent) {
|
|
258
260
|
try {
|
|
@@ -281,19 +283,20 @@ export function apply(ctx, config) {
|
|
|
281
283
|
env.toolsetOf = (role) => toolsetFor(role);
|
|
282
284
|
env.roleState = () => ({ mode: roleMode(), last: roleState.last, count: appliedRole.size });
|
|
283
285
|
env.rolesSnapshot = () => readRoles(cfg).sessions;
|
|
284
|
-
//
|
|
286
|
+
// 写角色表是所有角色变更的唯一入口。
|
|
287
|
+
// 写入后立即使每步过滤的短缓存失效,下一步即按新角色判定。
|
|
285
288
|
env.writeRole = (sid, patch) => {
|
|
286
289
|
const saved = writeRole(cfg, sid, patch);
|
|
287
290
|
roleCache.delete(String(sid || ''));
|
|
288
291
|
return saved;
|
|
289
292
|
};
|
|
290
293
|
|
|
291
|
-
/** 当前生效的裁剪机制,供
|
|
294
|
+
/** 当前生效的裁剪机制,供 ho_status(view="diag")报出。 */
|
|
292
295
|
function roleMode() {
|
|
293
296
|
if (cfg.roleGating === 'off') return '关闭(roleGating=off,所有会话可见全部工具)';
|
|
294
297
|
return perStep
|
|
295
298
|
? '按步过滤(工具表每步重算,换角色下一轮即生效)'
|
|
296
|
-
: '
|
|
299
|
+
: '旧式收窄(内核不支持按步解析,改用 agent 作用域 restrict)';
|
|
297
300
|
}
|
|
298
301
|
|
|
299
302
|
function handoverInfoFor(info, ws) {
|
|
@@ -304,7 +307,7 @@ export function apply(ctx, config) {
|
|
|
304
307
|
return Object.assign({ ws, role: info.role, project: info.project, task: info.task }, t);
|
|
305
308
|
}
|
|
306
309
|
|
|
307
|
-
/**
|
|
310
|
+
/** 仅按会话 id 推断工作区根:优先活体 cwd,其次会话元数据,最后取配置。 */
|
|
308
311
|
function workspaceForSid(sid) {
|
|
309
312
|
if (cfg.workspaceRoot) return String(cfg.workspaceRoot);
|
|
310
313
|
const a = agentOf(sid);
|
|
@@ -313,7 +316,7 @@ export function apply(ctx, config) {
|
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
env.handoverFor = (exec) => handoverInfoFor(env.roleOf(exec), env.resolveWs(exec));
|
|
316
|
-
/**
|
|
319
|
+
/** 跨会话使用:按 id 计算该会话自身的角色与交接路径,不借用调用者上下文。 */
|
|
317
320
|
env.handoverForSid = (sid) => {
|
|
318
321
|
const id = String(sid || '');
|
|
319
322
|
return handoverInfoFor(infoFor(id, {}), workspaceForSid(id));
|
|
@@ -322,7 +325,7 @@ export function apply(ctx, config) {
|
|
|
322
325
|
env.agentOfSid = (sid) => agentOf(String(sid || ''));
|
|
323
326
|
env.compactionStateOfSid = (sid) => compactionStateOf(String(sid || ''), null);
|
|
324
327
|
|
|
325
|
-
// ----------------
|
|
328
|
+
// ---------------- 按角色装配:工具可见性由每步条件即时计算,此处只维护提示段(旧内核另加 restrict)
|
|
326
329
|
function workspaceForAgent(agent) {
|
|
327
330
|
if (cfg.workspaceRoot) return cfg.workspaceRoot;
|
|
328
331
|
const cwd = agent && agent.session && agent.session.header ? String(agent.session.header.cwd || '') : '';
|
|
@@ -335,24 +338,33 @@ export function apply(ctx, config) {
|
|
|
335
338
|
if (typeof old === 'function') { try { old(); } catch { /* 旧段已失效 */ } }
|
|
336
339
|
sectionDispose.delete(sid);
|
|
337
340
|
let text = '';
|
|
341
|
+
let ws = '';
|
|
338
342
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
343
|
+
ws = workspaceForAgent(agent);
|
|
344
|
+
// 体系注册与授权状态均按会话实时计算:写入磁盘后重装一次即生效,不另设注入点。
|
|
345
|
+
const system = systemBlock(ws, info);
|
|
346
|
+
const consents = activeConsentsFor(sid);
|
|
347
|
+
text = manualFor(info, { ws, cfg, move: typeof env.moveInfo === 'function' ? env.moveInfo() : null, system, consents });
|
|
348
|
+
consentSig.set(sid, consents.map((r) => r.kind + ':' + r.until).join('|'));
|
|
349
|
+
systemSig.set(sid, registrySignature(ws, info));
|
|
341
350
|
} catch (e) {
|
|
342
351
|
initErrors.push('角色提示段:' + msgOf(e));
|
|
343
352
|
return;
|
|
344
353
|
}
|
|
345
354
|
const sc = agent && agent.ctx ? agent.ctx : null;
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
}
|
|
353
364
|
}
|
|
354
365
|
}
|
|
355
366
|
|
|
367
|
+
|
|
356
368
|
/** 旧内核(没有按步解析)才用的限制式裁剪。 */
|
|
357
369
|
function applyLegacyRestrict(agent, sid, info) {
|
|
358
370
|
const old = legacyRestrict.get(sid);
|
|
@@ -367,7 +379,7 @@ export function apply(ctx, config) {
|
|
|
367
379
|
} catch { /* 旧内核裁剪失败只影响可见工具,不影响其余功能 */ }
|
|
368
380
|
}
|
|
369
381
|
|
|
370
|
-
/**
|
|
382
|
+
/** 使一个活体会话的提示段与本会话当前角色对齐。info 可传入已解析的角色,省去一次判定。 */
|
|
371
383
|
function applyRoleTo(agent, infoOpt) {
|
|
372
384
|
const h = agent && agent.session ? agent.session.header : null;
|
|
373
385
|
const sid = String((h && h.id) || (agent && agent.id) || '');
|
|
@@ -382,32 +394,98 @@ export function apply(ctx, config) {
|
|
|
382
394
|
roleState.last = { sid, role: info.role, at: stampTz(Date.now()) };
|
|
383
395
|
}
|
|
384
396
|
|
|
385
|
-
/**
|
|
397
|
+
/** 设置角色:写入角色表,按需完成身份互转,存活会话当场更换提示段,随后投递首轮指令。 */
|
|
386
398
|
async function applyRole(input) {
|
|
387
399
|
const i = input || {};
|
|
388
400
|
const sid = String(i.sessionId || '').trim();
|
|
389
401
|
const role = String(i.role || '').trim();
|
|
390
402
|
const project = String(i.project || '').trim();
|
|
391
403
|
const task = String(i.task || '').trim();
|
|
392
|
-
if (!sid) return { ok: false, error: '
|
|
393
|
-
if (ROLES.indexOf(role) === -1) return { ok: false, error: '
|
|
394
|
-
if (role === 'project' && !project) return { ok: false, error: '
|
|
395
|
-
if (role === 'misc' && !task) return { ok: false, error: '
|
|
396
|
-
//
|
|
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
|
+
// 名称要落成目录:不合法时立即拒绝,不待写角色表或拼接路径时才失败。
|
|
397
409
|
if (role === 'project') { try { assertProjectName(project); } catch (e) { return { ok: false, error: msgOf(e) }; } }
|
|
398
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
|
+
}
|
|
399
437
|
try { writeRole(cfg, sid, { role, project, task, source: 'user' }); }
|
|
400
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; }
|
|
401
468
|
roleCache.delete(sid);
|
|
402
469
|
const info = { role, project, task, source: 'store', sessionId: sid };
|
|
403
470
|
let live = false;
|
|
404
471
|
try {
|
|
405
472
|
const a = agentOf(sid);
|
|
406
473
|
if (a) { live = true; applyRoleTo(a, info); }
|
|
407
|
-
} catch { /*
|
|
474
|
+
} catch { /* 活体会话装配失败不影响写盘 */ }
|
|
408
475
|
let delivered = false;
|
|
409
476
|
let error = '';
|
|
410
|
-
|
|
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
|
+
});
|
|
411
489
|
if (i.wake !== false && msg) {
|
|
412
490
|
try {
|
|
413
491
|
const r = await sendPrompt(env.controller(), sid, msg, 'queue');
|
|
@@ -416,11 +494,16 @@ export function apply(ctx, config) {
|
|
|
416
494
|
error = msgOf(e);
|
|
417
495
|
}
|
|
418
496
|
}
|
|
419
|
-
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
|
+
};
|
|
420
503
|
}
|
|
421
504
|
env.applyRole = applyRole;
|
|
422
505
|
|
|
423
|
-
/**
|
|
506
|
+
/** 会话新建后立即按已登记角色装配(不唤醒会话、不投递消息)。 */
|
|
424
507
|
env.applyScopedLive = (sid, info) => {
|
|
425
508
|
try {
|
|
426
509
|
const a = agentOf(String(sid || ''));
|
|
@@ -432,8 +515,20 @@ export function apply(ctx, config) {
|
|
|
432
515
|
} catch { return false; }
|
|
433
516
|
};
|
|
434
517
|
|
|
435
|
-
|
|
436
|
-
|
|
518
|
+
/** 重装某会话的提示段:文档定位写盘后与授权到达后各调用一次;不唤醒会话,不投递消息。 */
|
|
519
|
+
env.refreshPromptFor = (sid) => {
|
|
520
|
+
try {
|
|
521
|
+
const id = String(sid || '');
|
|
522
|
+
if (!id) return false;
|
|
523
|
+
const a = agentOf(id);
|
|
524
|
+
if (!a) return false;
|
|
525
|
+
applyRoleTo(a);
|
|
526
|
+
return true;
|
|
527
|
+
} catch { return false; }
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// ---------------- 调度器(续跑文案须按角色取交接路径)
|
|
531
|
+
// 注意:调度器收到的是会话 id 字符串,不是 exec;此处使用按 id 解析的版本,直接读取角色表与磁盘。
|
|
437
532
|
const schedDeps = { ctx, cfg, logger: (ctx.logger || null), roleOf: null, handoverFor: null };
|
|
438
533
|
try {
|
|
439
534
|
scheduler = createScheduler(schedDeps);
|
|
@@ -441,8 +536,8 @@ export function apply(ctx, config) {
|
|
|
441
536
|
} catch (e) { schedulerError = msgOf(e); }
|
|
442
537
|
Object.assign(schedDeps, schedDepsFor(env));
|
|
443
538
|
|
|
444
|
-
// ---------------- 用户全局前置提示词(段序 950
|
|
445
|
-
//
|
|
539
|
+
// ---------------- 用户全局前置提示词(段序 950,位于工具段之前)
|
|
540
|
+
// 与插件自身的会话约束无关:仅在用户开启且内容非空时注册;关闭或留空时不占用注入位置。
|
|
446
541
|
let extraDispose = null;
|
|
447
542
|
function applyExtraPrompt(state) {
|
|
448
543
|
try { if (typeof extraDispose === 'function') extraDispose(); } catch { /* 旧段已失效 */ }
|
|
@@ -472,11 +567,11 @@ export function apply(ctx, config) {
|
|
|
472
567
|
};
|
|
473
568
|
try { applyExtraPrompt(readExtraPrompt()); } catch (e) { initErrors.push('全局前置提示词:' + msgOf(e)); }
|
|
474
569
|
|
|
475
|
-
// ---------------- 工具注册(
|
|
476
|
-
// 0.2.2
|
|
477
|
-
// 不再另用 agent 作用域 restrict
|
|
570
|
+
// ---------------- 工具注册(13 个 ho_* 与 1 个 peer)
|
|
571
|
+
// 自 0.2.2 起,工具按「步」解析:可用工具集合每一步重算。子会话排除与角色过滤在此一并完成,
|
|
572
|
+
// 不再另用 agent 作用域 restrict;与 restrict 并用时,换角色或须等待重启,或互相覆盖。
|
|
478
573
|
const roleToolsets = new Map(ROLES.map((r) => [r, new Set(toolsetFor(r).allow)]));
|
|
479
|
-
/**
|
|
574
|
+
/** 每步过滤:子会话一律不可见;角色按当前角色表计算;无 agent 的路径(命令行、演练)放行。 */
|
|
480
575
|
function visibleTo(name) {
|
|
481
576
|
return (c) => {
|
|
482
577
|
const agent = c && c.agent ? c.agent : null;
|
|
@@ -497,101 +592,135 @@ export function apply(ctx, config) {
|
|
|
497
592
|
return allow ? allow.has(name) : true;
|
|
498
593
|
};
|
|
499
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
|
+
}
|
|
500
618
|
const tools = [
|
|
501
|
-
|
|
619
|
+
pmStatusTool(env, { schedulerReady: () => scheduler !== null, schedulerError: () => schedulerError }),
|
|
502
620
|
pmGateTool(env),
|
|
503
621
|
pmSessionsTool(env),
|
|
504
|
-
pmPhaseTool(env),
|
|
505
622
|
pmCharterTool(env),
|
|
506
623
|
pmHandoverTool(env),
|
|
507
624
|
pmCheckpointTool(env),
|
|
508
|
-
pmSnapshotTool(env),
|
|
509
|
-
pmDiagTool(env, { schedulerReady: () => scheduler !== null, schedulerError: () => schedulerError }),
|
|
510
|
-
pmDriftTool(env),
|
|
511
625
|
pmChangelogTool(env),
|
|
512
|
-
|
|
513
|
-
pmPackTool(env),
|
|
626
|
+
pmBackupTool(env),
|
|
514
627
|
pmWorkspaceTool(env),
|
|
515
|
-
pmInitTool(env),
|
|
516
628
|
pmProfileTool(env),
|
|
517
629
|
pmOfficeTool(env),
|
|
518
630
|
pmSucceedTool(env),
|
|
631
|
+
pmDocmapTool(env),
|
|
519
632
|
];
|
|
520
|
-
const keys = ['
|
|
633
|
+
const keys = ['status', 'gate', 'sessions', 'charter', 'handover', 'checkpoint', 'changelog', 'backup', 'workspace', 'profile', 'office', 'succeed', 'docmap'];
|
|
521
634
|
for (let i = 0; i < tools.length; i++) {
|
|
522
635
|
const key = keys[i];
|
|
523
636
|
try {
|
|
524
|
-
//
|
|
637
|
+
// 名称以工厂自带的为准;工厂未写时才用位置表补充,顺序错位时报告(不再静默使用错误名称)。
|
|
525
638
|
if (!tools[i].name) tools[i].name = NAMES[key];
|
|
526
639
|
else if (tools[i].name !== NAMES[key]) initErrors.push('工具名错位:数组第 ' + i + ' 项是 ' + tools[i].name + ',位置表期望 ' + NAMES[key]);
|
|
527
|
-
ctx.tools.register(tools[i], { when: visibleTo(tools[i].name) });
|
|
640
|
+
ctx.tools.register(withReminderTail(tools[i]), { when: visibleTo(tools[i].name) });
|
|
528
641
|
registered.add(tools[i].name);
|
|
529
642
|
} catch (e) {
|
|
530
643
|
initErrors.push(key + ':' + msgOf(e));
|
|
531
644
|
}
|
|
532
645
|
}
|
|
533
646
|
|
|
534
|
-
const peerTools = [
|
|
535
|
-
pmPeerListTool(env),
|
|
536
|
-
pmPeerStatusTool(env),
|
|
537
|
-
pmPeerSendTool(env),
|
|
538
|
-
pmPeerNewTool(env),
|
|
539
|
-
pmPeerRenameTool(env),
|
|
540
|
-
pmPeerPeekTool(env),
|
|
541
|
-
];
|
|
647
|
+
const peerTools = [pmPeerTool(env)];
|
|
542
648
|
for (const t of peerTools) {
|
|
543
649
|
try {
|
|
544
|
-
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) : '') });
|
|
545
651
|
if (t && t.name) registered.add(t.name);
|
|
546
652
|
} catch (e) {
|
|
547
|
-
initErrors.push((t && t.name ? String(t.name) : '
|
|
653
|
+
initErrors.push((t && t.name ? String(t.name) : 'peer') + ':' + msgOf(e));
|
|
548
654
|
}
|
|
549
655
|
}
|
|
550
656
|
|
|
551
|
-
// ---------------- 会话守卫(Charter §7.2.4
|
|
552
|
-
//
|
|
657
|
+
// ---------------- 会话守卫(Charter §7.2.4):新建会话与设置角色仅属全局;
|
|
658
|
+
// 会话之间可直接用 peer(action=send)通信,无须经全局转述。
|
|
659
|
+
// directPeer=off 时退回「只能向全局报告」的旧口径。
|
|
553
660
|
if (cfg.wakeAuthority !== 'off' && typeof ctx.tools.guard === 'function') {
|
|
554
661
|
try {
|
|
555
662
|
ctx.tools.guard((exec) => {
|
|
556
663
|
const nm = exec && exec.name;
|
|
557
|
-
if (nm !==
|
|
664
|
+
if (nm !== PEER_NAME) return undefined;
|
|
665
|
+
let args = (exec && exec.arguments) || {};
|
|
666
|
+
if (typeof args === 'string') { try { args = JSON.parse(args); } catch { args = {}; } }
|
|
667
|
+
const action = String(args.action || '');
|
|
668
|
+
// list/status/peek/rename 的权限由工具内部按角色判定,此处只处理 2 类有外部影响的动作。
|
|
669
|
+
if (action !== 'send' && action !== 'new') return undefined;
|
|
558
670
|
const sid = env.selfId(exec) || '';
|
|
559
|
-
const args = (exec && exec.arguments) || {};
|
|
560
671
|
let info;
|
|
561
672
|
try { info = env.roleOf(exec); } catch { info = { role: 'plain', source: 'error' }; }
|
|
562
673
|
const mk = (allow, why) => {
|
|
563
674
|
guardLast = { sid, allow, source: why, title: titleOf(sid) };
|
|
564
675
|
if (allow) return undefined;
|
|
565
|
-
if (
|
|
566
|
-
return '会话间通信受限(directPeer=off
|
|
676
|
+
if (action === 'new') return '会话创建仅属全局:非全局会话不得新建平级会话(杂项会话建在 Misc 下者除外)。';
|
|
677
|
+
return '会话间通信受限(directPeer=off):非全局会话只能向在任全局会话报告,不得唤醒其他会话。';
|
|
567
678
|
};
|
|
568
|
-
if (info.role === 'global') return mk(true, '
|
|
569
|
-
if (
|
|
570
|
-
//
|
|
679
|
+
if (info.role === 'global') return mk(true, '全局角色');
|
|
680
|
+
if (action === 'send') {
|
|
681
|
+
// 会话之间可直接通信;向全局报告亦走此路径。仅 directPeer=off 时退回旧口径。
|
|
571
682
|
const target = String(args.session_id || '');
|
|
572
683
|
let t = null;
|
|
573
684
|
try { t = target ? infoFor(target, {}) : null; } catch { t = null; }
|
|
574
|
-
|
|
575
|
-
if (
|
|
576
|
-
return mk(
|
|
685
|
+
const tRole = t ? t.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 + ')');
|
|
577
689
|
}
|
|
578
|
-
//
|
|
690
|
+
// action=new:只有杂项会话可以建在 Misc 下
|
|
579
691
|
const target = String(args.workspace || args.cwd || '').toLowerCase();
|
|
580
|
-
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, '
|
|
581
|
-
return mk(false,
|
|
692
|
+
if (info.role === 'misc' && target.indexOf('misc') !== -1) return mk(true, '杂项会话新建于 Misc');
|
|
693
|
+
return mk(false, '非全局角色不得新建平级会话(' + info.role + ')');
|
|
694
|
+
});
|
|
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;
|
|
582
711
|
});
|
|
583
|
-
} catch { /*
|
|
712
|
+
} catch { /* 旧版本无 guard:退化为规则约束 */ }
|
|
584
713
|
}
|
|
585
714
|
|
|
586
715
|
// ---------------- 会话生命周期:创建时装配提示段;开轮前按角色表核对一次
|
|
587
|
-
//
|
|
716
|
+
// 此处不处理工具可见性:自 0.2.2 起,每步条件已按当前角色计算(见 visibleTo),换角色下一轮即生效。
|
|
588
717
|
function scopedMetaOf(h, sid) {
|
|
589
718
|
return { title: (h && h.title) || titleOf(sid), cwd: (h && h.cwd) || '' };
|
|
590
719
|
}
|
|
591
720
|
if (typeof ctx.on === 'function') {
|
|
592
721
|
try {
|
|
593
|
-
// 事件作用域:宿主以 agent
|
|
594
|
-
//
|
|
722
|
+
// 事件作用域:宿主以 agent 作为事件的作用域载体。
|
|
723
|
+
// 插件在根作用域直接注册时,收不到其他会话的事件。传入 { global: true } 才能收到全部会话的事件(0.2.2 支持;旧内核忽略该项,退回直接注册)。
|
|
595
724
|
const onAgentEvent = (name, fn) => {
|
|
596
725
|
try { ctx.on(name, fn, { global: true }); return true; }
|
|
597
726
|
catch { try { ctx.on(name, fn); return true; } catch (e) { env.log('会话事件监听失败(' + name + '):' + msgOf(e)); return false; } }
|
|
@@ -599,7 +728,11 @@ export function apply(ctx, config) {
|
|
|
599
728
|
onAgentEvent('agent/created', (payload) => {
|
|
600
729
|
try {
|
|
601
730
|
const agent = payload && payload.agent ? payload.agent : null;
|
|
602
|
-
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
|
+
}
|
|
603
736
|
} catch (e) { env.log('角色装配失败:' + msgOf(e)); }
|
|
604
737
|
});
|
|
605
738
|
onAgentEvent('agent/disposed', (payload) => {
|
|
@@ -615,22 +748,29 @@ export function apply(ctx, config) {
|
|
|
615
748
|
if (typeof ld === 'function') { try { ld(); } catch { /* ignore */ } }
|
|
616
749
|
legacyRestrict.delete(sid);
|
|
617
750
|
appliedRole.delete(sid);
|
|
751
|
+
systemSig.delete(sid);
|
|
618
752
|
roleCache.delete(sid);
|
|
619
753
|
} catch { /* ignore */ }
|
|
620
754
|
});
|
|
621
|
-
//
|
|
622
|
-
//
|
|
755
|
+
// 开轮前核对:休眠期间角色可能已被修改,宿主恢复同一活体时不保证再次发出创建事件。
|
|
756
|
+
// 状态切换到 running 时核对一次,使提示段与旧内核的限制同角色表对齐。
|
|
623
757
|
onAgentEvent('agent/status', (payload) => {
|
|
624
758
|
try {
|
|
625
|
-
if (!payload
|
|
759
|
+
if (!payload) return;
|
|
626
760
|
const agent = payload.agent || null;
|
|
627
761
|
if (!agent) return;
|
|
628
762
|
const h = agent.session && agent.session.header ? agent.session.header : null;
|
|
629
763
|
const sid = String((h && h.id) || agent.id || '');
|
|
630
764
|
if (!sid) return;
|
|
765
|
+
if (payload.status !== 'running') return;
|
|
631
766
|
const info = infoFor(sid, scopedMetaOf(h, sid));
|
|
632
|
-
|
|
633
|
-
|
|
767
|
+
// 角色未变时仍须核对授权签名与体系注册签名:授权到达或注册改写后,下一轮即应看到新内容。
|
|
768
|
+
let sig = '';
|
|
769
|
+
try { sig = activeConsentsFor(sid).map((r) => r.kind + ':' + r.until).join('|'); } catch { sig = ''; }
|
|
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);
|
|
634
774
|
applyRoleTo(agent, info);
|
|
635
775
|
} catch (e) { env.log('开轮前装配失败:' + msgOf(e)); }
|
|
636
776
|
});
|
|
@@ -638,9 +778,9 @@ export function apply(ctx, config) {
|
|
|
638
778
|
initErrors.push('会话事件监听:' + msgOf(e));
|
|
639
779
|
}
|
|
640
780
|
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
// agents 未在 inject
|
|
781
|
+
// 插件重新加载(停用后启用,或启动时活体会话早于插件)时,已存在的会话不再收到
|
|
782
|
+
// 创建事件;此处补充一次装配,否则其提示段会随旧实例一起消失。
|
|
783
|
+
// agents 未在 inject 中声明,激活瞬间可能尚未就绪,故首次未取到时延迟重试一次。
|
|
644
784
|
const reapplyLive = () => {
|
|
645
785
|
let agents = null;
|
|
646
786
|
try { agents = ctx.agents; } catch { agents = null; }
|
|
@@ -650,9 +790,15 @@ export function apply(ctx, config) {
|
|
|
650
790
|
if (!agents || typeof agents.list !== 'function') return 0;
|
|
651
791
|
let n = 0;
|
|
652
792
|
for (const a of agents.list()) {
|
|
653
|
-
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 { /* 单个会话装配失败不影响插件激活 */ }
|
|
654
800
|
}
|
|
655
|
-
if (n > 0) env.log('
|
|
801
|
+
if (n > 0) env.log('补充装配 ' + n + ' 个活体会话');
|
|
656
802
|
return n;
|
|
657
803
|
};
|
|
658
804
|
try {
|
|
@@ -660,10 +806,10 @@ export function apply(ctx, config) {
|
|
|
660
806
|
const t = setTimeout(() => { try { reapplyLive(); } catch { /* ignore */ } }, 1200);
|
|
661
807
|
if (t && typeof t.unref === 'function') t.unref();
|
|
662
808
|
}
|
|
663
|
-
} catch { /*
|
|
809
|
+
} catch { /* 无法取得 agents 服务时跳过补充装配 */ }
|
|
664
810
|
}
|
|
665
811
|
|
|
666
|
-
// ----------------
|
|
812
|
+
// ---------------- 设置页数据接口:主通道为 DSH 认证的 /api 载体,旧通道为 /dsh-handover HTTP 路由
|
|
667
813
|
try {
|
|
668
814
|
const info = pkgInfo();
|
|
669
815
|
const pageInfo = {
|
|
@@ -676,9 +822,9 @@ export function apply(ctx, config) {
|
|
|
676
822
|
env.api = api;
|
|
677
823
|
registerStatusRoute(ctx, env, pageInfo, api);
|
|
678
824
|
registerRpcCarrier(ctx, api, (m) => env.log(m));
|
|
679
|
-
//
|
|
825
|
+
// 预热状态快照:设置页直接读取缓存,避免请求时现算等待。
|
|
680
826
|
try {
|
|
681
|
-
//
|
|
827
|
+
// 仅在最近 5 分钟内有人查看设置页时预热:无人查看即不计算,避免空转占用宿主主线程。
|
|
682
828
|
const warm = () => {
|
|
683
829
|
if (Date.now() - (env.lastStatusCallAt || 0) > 5 * 60 * 1000) return;
|
|
684
830
|
try { statusSnapshot(env, pageInfo, 'warm'); } catch { /* ignore */ }
|
|
@@ -693,23 +839,23 @@ export function apply(ctx, config) {
|
|
|
693
839
|
initErrors.push('设置页路由:' + msgOf(e));
|
|
694
840
|
}
|
|
695
841
|
|
|
696
|
-
// ----------------
|
|
697
|
-
//
|
|
698
|
-
//
|
|
842
|
+
// ---------------- 聊天页命令:用户在输入框输入 /allow-compact、/allow-succeed 即可授权
|
|
843
|
+
// 设计理由:设置页须切换页面并填写会话 id,操作步骤较多。斜杠命令由用户亲手输入,
|
|
844
|
+
// 天然满足「同意只能由用户作出」的判据,且不产生模型消息、不消耗 token。
|
|
699
845
|
try {
|
|
700
846
|
const allowCommands = [
|
|
701
847
|
{
|
|
702
848
|
name: 'allow-compact',
|
|
703
|
-
description: '
|
|
704
|
-
hint: '
|
|
849
|
+
description: '允许本会话压缩一次(由当前用户授权,5 分钟有效;可附会话短号授权其他会话)',
|
|
850
|
+
hint: '可留空,默认授权本会话;填写 8 位短号或 session-id 则授权对应会话',
|
|
705
851
|
kind: 'compact',
|
|
706
852
|
note: '聊天页命令 /allow-compact',
|
|
707
853
|
action: '压缩',
|
|
708
854
|
},
|
|
709
855
|
{
|
|
710
856
|
name: 'allow-succeed',
|
|
711
|
-
description: '
|
|
712
|
-
hint: '
|
|
857
|
+
description: '允许本会话换任一次(由当前用户授权,5 分钟有效;可附会话短号授权其他会话)',
|
|
858
|
+
hint: '可留空,默认授权本会话;填写 8 位短号或 session-id 则授权对应会话',
|
|
713
859
|
kind: 'succeed',
|
|
714
860
|
note: '聊天页命令 /allow-succeed',
|
|
715
861
|
action: '换任',
|
|
@@ -725,10 +871,12 @@ export function apply(ctx, config) {
|
|
|
725
871
|
handler: ({ agent, rawInput }) => {
|
|
726
872
|
const arg = String(rawInput || '').trim();
|
|
727
873
|
const sid = arg ? resolveShortSid(env, arg) : (agent && agent.id ? String(agent.id) : '');
|
|
728
|
-
if (!sid) return { kind: 'error', text: '
|
|
874
|
+
if (!sid) return { kind: 'error', text: '无法获取会话 id:请写成 /' + cmd.name + ' session-xxxx' };
|
|
729
875
|
const g = grantConsent(sid, cmd.note, cmd.kind);
|
|
876
|
+
// 授权到达后立即重装提示段:目标会话下一轮即可看到该行(当前空闲亦然)。
|
|
877
|
+
try { env.refreshPromptFor(sid); } catch { /* 装配失败不影响授权本身 */ }
|
|
730
878
|
try { env.log(cmd.action + '授权(命令):' + sid); } catch { /* ignore */ }
|
|
731
|
-
return { kind: 'success', text: '已允许 ' + sid + ' 在 ' + g.minutes + ' 分钟内' + cmd.action + '
|
|
879
|
+
return { kind: 'success', text: '已允许 ' + sid + ' 在 ' + g.minutes + ' 分钟内' + cmd.action + '一次(用后即失效)。' };
|
|
732
880
|
},
|
|
733
881
|
});
|
|
734
882
|
if (typeof d === 'function' && typeof ctx.effect === 'function') {
|
|
@@ -736,7 +884,7 @@ export function apply(ctx, config) {
|
|
|
736
884
|
}
|
|
737
885
|
}
|
|
738
886
|
};
|
|
739
|
-
//
|
|
887
|
+
// 可选服务:可用 ctx.inject 时走注入(旧宿主没有 commands 时插件照常激活),否则直接获取。
|
|
740
888
|
if (typeof ctx.inject === 'function') {
|
|
741
889
|
ctx.inject(['commands'], (scope) => {
|
|
742
890
|
try { registerAllow(scope && scope.commands); } catch (e) { initErrors.push('命令注册:' + msgOf(e)); }
|
|
@@ -747,4 +895,27 @@ export function apply(ctx, config) {
|
|
|
747
895
|
} catch (e) {
|
|
748
896
|
initErrors.push('命令注册:' + msgOf(e));
|
|
749
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
|
+
}
|
|
750
921
|
}
|