dsh-session-conductor 1.3.13 → 1.3.17

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.
Files changed (87) hide show
  1. package/README.md +55 -1
  2. package/cordis.patch.yml +0 -0
  3. package/lib/apply.js +28 -1243
  4. package/lib/client-parts/apply.js +0 -0
  5. package/lib/client-parts/components/compaction.js +0 -0
  6. package/lib/client-parts/components/conductor-settings.js +0 -0
  7. package/lib/client-parts/components/group.js +0 -0
  8. package/lib/client-parts/components/panel.js +0 -0
  9. package/lib/client-parts/components/processing.js +0 -0
  10. package/lib/client-parts/components/session-icon.js +0 -0
  11. package/lib/client-parts/components/settings.js +0 -0
  12. package/lib/client-parts/foundation/bootstrap.js +0 -0
  13. package/lib/client-parts/foundation/i18n.js +0 -0
  14. package/lib/client-parts/foundation/list-cache.js +0 -0
  15. package/lib/client-parts/foundation/styles.js +0 -0
  16. package/lib/core/config.js +0 -0
  17. package/lib/core/domain.js +0 -0
  18. package/lib/core/index-store.js +0 -0
  19. package/lib/core/lazy.js +0 -0
  20. package/lib/core/state.js +0 -0
  21. package/lib/core.js +0 -0
  22. package/lib/eio-repair.js +0 -0
  23. package/lib/features/base-routes.js +156 -0
  24. package/lib/features/context-usage/index.js +0 -0
  25. package/lib/features/continue/eligibility.js +0 -0
  26. package/lib/features/continue/scan.js +0 -0
  27. package/lib/features/continue/session.js +0 -0
  28. package/lib/features/inject/preset.js +0 -0
  29. package/lib/features/member-model/batch.js +97 -0
  30. package/lib/features/member-model/clone-member.js +270 -0
  31. package/lib/features/member-model/command.js +66 -7
  32. package/lib/features/member-model/create-teammate.js +2 -2
  33. package/lib/features/member-model/index.js +17 -1
  34. package/lib/features/member-model/lead.js +73 -0
  35. package/lib/features/member-model/locate.js +82 -1
  36. package/lib/features/member-model/override.js +0 -0
  37. package/lib/features/member-model/permission-command.js +138 -0
  38. package/lib/features/member-model/permission.js +213 -0
  39. package/lib/features/member-model/setup.js +0 -0
  40. package/lib/features/member-model/switch.js +137 -0
  41. package/lib/features/member-model/tools.js +359 -0
  42. package/lib/features/memory-recall/index.js +0 -0
  43. package/lib/features/rename/analysis.js +0 -0
  44. package/lib/features/rename/title.js +0 -0
  45. package/lib/features/rename/value.js +0 -0
  46. package/lib/features/session-action-routes.js +435 -0
  47. package/lib/features/session-messaging/index.js +0 -0
  48. package/lib/features/session-messaging/logic.js +0 -0
  49. package/lib/features/session-messaging/tools.js +0 -0
  50. package/lib/features/session-ops-routes.js +0 -0
  51. package/lib/features/settings-routes.js +411 -0
  52. package/lib/features/trash.js +0 -0
  53. package/lib/features/value-index.js +0 -0
  54. package/lib/features/value-report-routes.js +211 -0
  55. package/lib/group.js +0 -0
  56. package/lib/i18n/en.json +0 -0
  57. package/lib/i18n/zh.json +0 -0
  58. package/lib/index.js +0 -0
  59. package/lib/repair.js +0 -0
  60. package/lib/seq-gap-repair.js +0 -0
  61. package/lib/session-codec.js +0 -0
  62. package/lib/session-log.js +0 -0
  63. package/lib/sessions/detach.js +0 -0
  64. package/lib/sessions/events.js +0 -0
  65. package/lib/sessions/interruption.js +0 -0
  66. package/lib/sessions/list-cache.js +0 -0
  67. package/lib/sessions/list.js +0 -0
  68. package/lib/sessions/lookup.js +0 -0
  69. package/lib/sessions/route.js +0 -0
  70. package/lib/sessions/session-id.js +0 -0
  71. package/lib/sessions/tail-io.js +30 -0
  72. package/lib/sessions/turn.js +0 -0
  73. package/lib/shared/atomic-file.js +0 -0
  74. package/lib/shared/constants.js +0 -0
  75. package/lib/shared/json-store.js +0 -0
  76. package/lib/shared/log.js +0 -0
  77. package/lib/shared/timing.js +0 -0
  78. package/lib/shared/util.js +0 -0
  79. package/lib/template-inject.js +0 -0
  80. package/lib/value.js +0 -0
  81. package/lib/zstd-frames.js +0 -0
  82. package/package.json +1 -1
  83. package/scripts/check-member-model.mjs +0 -0
  84. package/skills/dsh-session-conductor-functions.md +0 -0
  85. package/skills/dsh-session-conductor.md +0 -0
  86. package/skills/dsh-session-official-doc.md +0 -0
  87. package/skills/task-completion-report.md +0 -0
@@ -17,7 +17,9 @@
17
17
  // 就抛 ERR_MODULE_NOT_FOUND,连累整个插件加载失败(本项目在 defineTool / createUserMessage
18
18
  // 上踩过同类坑)。因此拿不到宿主能力时**跳过注册并如实留痕**,绝不影响插件其它功能。
19
19
  import { log } from "../../shared/log.js";
20
- import { findTargetAgent, collectTeamMembers } from "./locate.js";
20
+ import { findTargetAgent, collectTeamMembers, resolveTargets, isAllTarget } from "./locate.js";
21
+ import { readLeaderRoute } from "./lead.js";
22
+ import { applyModelToMany, renderBatchReport } from "./batch.js";
21
23
  import { getMemberModelOverride } from "./override.js";
22
24
  import { readLastRequestModel, registerSwitchWatch, switchAgentModel, peekLastRequestModel } from "./switch.js";
23
25
 
@@ -53,6 +55,13 @@ async function renderMemberList(ctx, invocation) {
53
55
  lines.push("要查**实际**在用什么(含历史请求,不只本次进程)或切换:");
54
56
  lines.push(` /${COMMAND_NAME} <成员名或 sessionId> ← 查实际模型`);
55
57
  lines.push(` /${COMMAND_NAME} <成员名或 sessionId> <provider>/<模型> ← 切换并回显查证`);
58
+ lines.push("");
59
+ lines.push("和别人对齐 / 批量:");
60
+ lines.push(` /${COMMAND_NAME} * auto ← 最常用:所有子代理都换成「我自己现在用的」模型`);
61
+ lines.push(` /${COMMAND_NAME} <成员名> auto ← 只让这一个成员换成我自己现在用的模型`);
62
+ lines.push(` /${COMMAND_NAME} * <provider>/<模型> ← 所有子代理换成一个指定的模型`);
63
+ lines.push("");
64
+ lines.push("* = 全部成员(不含我自己);auto = 「跟我保持一致」,不用去查我用的是哪个模型。");
56
65
  return lines.join("\n");
57
66
  }
58
67
 
@@ -151,14 +160,64 @@ async function switchMember(ctx, found, provider, model, invocation) {
151
160
  export async function handleModelSwitchCommand(ctx, invocation) {
152
161
  const raw = String(invocation?.rawInput ?? "").trim();
153
162
  const parts = raw.split(/\s+/).filter(Boolean);
163
+ const caller = invocation?.agent ?? null;
154
164
  try {
165
+ // 无参:列成员(保留原行为——列表和批量执行不能共用同一种输入,否则语义含糊)
155
166
  if (parts.length === 0) return { kind: "success", text: await renderMemberList(ctx, invocation) };
156
- const found = findTargetAgent(ctx, parts[0], { caller: invocation?.agent ?? null });
157
- if (found?.error) return { kind: "error", text: `找不到成员:${found.error}` };
158
- if (parts.length === 1) return { kind: "success", text: await renderMemberDetail(found) };
159
- const pair = splitPair(parts[1]);
160
- if (pair.error) return { kind: "error", text: pair.error };
161
- return await switchMember(ctx, found, pair.provider, pair.model, invocation);
167
+
168
+ const targetRaw = parts[0];
169
+ // 单参:查成员实际模型(`*` 没有「查」的意义,直接提示正确用法)
170
+ if (parts.length === 1) {
171
+ if (isAllTarget(targetRaw)) {
172
+ return {
173
+ kind: "error",
174
+ text: `批量操作需要指定模型:\n /${COMMAND_NAME} * auto ← 所有子代理换成我自己现在用的模型(最常用)\n /${COMMAND_NAME} * <provider>/<模型> ← 所有子代理换成指定模型`,
175
+ };
176
+ }
177
+ const found = findTargetAgent(ctx, targetRaw, { caller });
178
+ if (found?.error) return { kind: "error", text: `找不到成员:${found.error}` };
179
+ return { kind: "success", text: await renderMemberDetail(found) };
180
+ }
181
+
182
+ // 二参:切换(单个或全部)
183
+ const routeRaw = parts[1];
184
+ let provider;
185
+ let model;
186
+ let followedLead = null;
187
+ if (routeRaw.toLowerCase() === "auto") {
188
+ // auto = 跟随主代理(Lead):从请求级记录读它当前真实使用的模型
189
+ const lead = await readLeaderRoute(ctx, { caller });
190
+ if (lead.error) return { kind: "error", text: `跟随主代理失败:${lead.error}` };
191
+ provider = lead.provider;
192
+ model = lead.model;
193
+ followedLead = lead;
194
+ } else {
195
+ const pair = splitPair(routeRaw);
196
+ if (pair.error) return { kind: "error", text: pair.error };
197
+ provider = pair.provider;
198
+ model = pair.model;
199
+ }
200
+
201
+ const resolved = resolveTargets(ctx, targetRaw, { caller });
202
+ if (resolved?.error) return { kind: "error", text: `找不到成员:${resolved.error}` };
203
+
204
+ // 单个目标:沿用原有路径(带查证快照与查证任务登记)
205
+ if (!resolved.all && resolved.targets.length === 1) {
206
+ return await switchMember(ctx, resolved.targets[0], provider, model, invocation);
207
+ }
208
+
209
+ // 全部目标:串行逐个切,回执必须带完整名单
210
+ const report = await applyModelToMany(ctx, resolved.targets, { provider, model });
211
+ const extra = [];
212
+ if (followedLead) {
213
+ extra.push(`模型来源:跟随主代理 ${followedLead.sessionId}(${followedLead.modelSource})`);
214
+ for (const w of followedLead.warnings ?? []) extra.push(`提示:${w}`);
215
+ }
216
+ extra.push("批量切换逐条写入各成员会话日志;单个成员失败不影响其余成员。");
217
+ return {
218
+ kind: "success",
219
+ text: renderBatchReport(`批量切换模型 → ${provider}/${model}`, report, extra),
220
+ };
162
221
  } catch (error) {
163
222
  return { kind: "error", text: `执行失败:${String(error?.message ?? error)}` };
164
223
  }
@@ -29,7 +29,7 @@ import { switchAgentModel } from "./switch.js";
29
29
  * 占位任务:只为让成员被创建出来,不产生实质动作。
30
30
  * 写得极短是为了把首轮消耗压到最低(这一轮拿不到指定模型,越短越不亏)。
31
31
  */
32
- const PLACEHOLDER_PROMPT = "先待命:不要执行任何操作,不要调用任何工具,不要读写任何文件,等待后续消息。";
32
+ export const PLACEHOLDER_PROMPT = "先待命:不要执行任何操作,不要调用任何工具,不要读写任何文件,等待后续消息。";
33
33
 
34
34
  /**
35
35
  * 等成员真正空闲(中断的取消信号被观测完)再投递。
@@ -39,7 +39,7 @@ const PLACEHOLDER_PROMPT = "先待命:不要执行任何操作,不要调用
39
39
  * 用宿主自己的 `waitForChange` 等状态变化(事件驱动),不睡固定时间。
40
40
  * @returns {Promise<{idle:boolean, status:string}>}
41
41
  */
42
- async function waitUntilIdle(teams, caller, name, signal, budget = 60) {
42
+ export async function waitUntilIdle(teams, caller, name, signal, budget = 60) {
43
43
  if (typeof teams?.listMembers !== 'function' || typeof teams?.waitForChange !== 'function') {
44
44
  // 老宿主没有这两个能力:退化为「不等待」,行为与之前一致
45
45
  return { idle: false, status: 'unknown' };
@@ -8,7 +8,11 @@
8
8
  //
9
9
  // 对外只暴露本文件——内部文件不直接被域外引用,便于后续再细分或整体提升为顶层目录。
10
10
 
11
- export { pickTeamCaller, memberStatusError, validateModelPair, findTargetAgent } from "./locate.js";
11
+ export {
12
+ pickTeamCaller, memberStatusError, validateModelPair, findTargetAgent,
13
+ // 批量目标解析(空 / `*` / `all` = 全部成员,排除 Lead)与 Lead 定位
14
+ collectTeamMembers, isAllTarget, resolveTargets, resolveLeader,
15
+ } from "./locate.js";
12
16
  export { getMemberModelOverride, setMemberModelOverride, applyModelOverride, resetMemberModelCacheForTest } from "./override.js";
13
17
  export { waitForIdle, appendSelectionEventToLog, switchAgentModel } from "./switch.js";
14
18
  // 切换查证(事件驱动,注册 job 不阻塞):登记待查证项 → 每次请求记录实际模型 → 按事件判定 → 结果回报
@@ -19,3 +23,15 @@ export {
19
23
  export { resumeSetupFor } from "./setup.js";
20
24
  // 聊天框指令 /model-switch(用户视角入口,与 AI 工具共用同一条切换 + 查证路径)
21
25
  export { registerModelSwitchCommand, handleModelSwitchCommand } from "./command.js";
26
+ // 权限预设查看/切换(子代理在界面上切不了,只能写会话日志;与模型切换同一套成员定位)
27
+ export {
28
+ readMemberPermission, readPresetCatalog, switchMemberPermission,
29
+ describeMemberPermission, renderMemberPermissionList,
30
+ } from "./permission.js";
31
+ export { registerPermSwitchCommand, handlePermSwitchCommand } from "./permission-command.js";
32
+ // 「跟随主代理」的取值来源(Lead 当前真实模型 + 当前权限预设)
33
+ export { readLeaderRoute } from "./lead.js";
34
+ // 批量应用与回执渲染(串行执行、逐个隔离失败、回执带完整名单)
35
+ export { applyModelToMany, applyPermissionToMany, renderBatchReport, memberDisplay } from "./batch.js";
36
+ // 换名通道:新建成员 + 搬会话内容(成员名在宿主里不可变,只能走这条路)
37
+ export { cloneMemberAs, transplantSessionLog, renderCloneMemberResult } from "./clone-member.js";
@@ -0,0 +1,73 @@
1
+ // dsh-session-conductor — 「跟随主代理」的取值来源
2
+ //
3
+ // 【这个文件解决什么】切换模型/权限时把参数留空表示「跟主代理一致」,这样新建一批成员后
4
+ // 不用逐个查 Lead 当前用的什么。这里负责把「主代理当前到底是什么」查准:
5
+ // · 模型:优先请求级权威记录(会话日志里的 request/header,即**真实发出请求时用的值**),
6
+ // 退而用本进程内观察到的最近一次请求;都读不到就如实报错,绝不用名册里的静态字段冒充
7
+ // (名册里那个值可能是创建时的旧值,与当前实际不符)。
8
+ // · 权限:取会话日志里最后一条 permission/preset(宿主折叠语义:最后一条即生效值)。
9
+ import { peekLastRequestModel, readLastRequestModel } from "./switch.js";
10
+ import { readMemberPermission } from "./permission.js";
11
+ import { resolveLeader } from "./locate.js";
12
+
13
+ /**
14
+ * 读 Team Lead 当前的模型与权限预设。
15
+ *
16
+ * @param {object} ctx 插件 context
17
+ * @param {{caller?: object}} [opts] 显式发起方(指令/工具执行时的 agent)
18
+ * @returns {Promise<{sessionId: string, leadSource: string, provider: string|null, model: string|null,
19
+ * modelSource: string|null, preset: string|null, sandbox: string|null, approval: string|null,
20
+ * warnings: string[]} | {error: string}>}
21
+ */
22
+ export async function readLeaderRoute(ctx, { caller = null } = {}) {
23
+ const lead = resolveLeader(ctx, { caller });
24
+ if (lead?.error) return { error: lead.error };
25
+
26
+ const warnings = [];
27
+ let provider = null;
28
+ let model = null;
29
+ let modelSource = null;
30
+
31
+ // ① 请求级权威记录(日志里的 request/header)
32
+ try {
33
+ const { lastRequest } = await readLastRequestModel(lead.sessionId);
34
+ if (lastRequest?.provider && lastRequest?.model) {
35
+ provider = lastRequest.provider;
36
+ model = lastRequest.model;
37
+ modelSource = "请求级记录(会话日志 request/header)";
38
+ }
39
+ } catch (error) {
40
+ warnings.push(`读请求级记录失败:${String(error?.message ?? error)}`);
41
+ }
42
+ // ② 本进程内观察到的最近一次请求(日志未落盘时的兜底)
43
+ if (!provider) {
44
+ const live = peekLastRequestModel(lead.sessionId);
45
+ if (live?.provider && live?.model) {
46
+ provider = live.provider;
47
+ model = live.model;
48
+ modelSource = "本进程观察(日志尚未落盘)";
49
+ warnings.push("模型取自进程内观察,可能比日志更新;如与界面不一致请以界面为准");
50
+ }
51
+ }
52
+ if (!provider) {
53
+ return {
54
+ error: `读不到主代理(${lead.sessionId})当前使用的模型:会话日志里没有 request/header,进程内也没有观察记录。请显式指定 provider/model`,
55
+ };
56
+ }
57
+
58
+ // ③ 权限预设(最后一条 preset 生效)
59
+ const perm = await readMemberPermission(lead.sessionId);
60
+ if (perm.error) warnings.push(`读主代理权限失败:${perm.error}`);
61
+
62
+ return {
63
+ sessionId: lead.sessionId,
64
+ leadSource: lead.source,
65
+ provider,
66
+ model,
67
+ modelSource,
68
+ preset: perm.error ? null : perm.preset,
69
+ sandbox: perm.error ? null : perm.sandbox,
70
+ approval: perm.error ? null : perm.approval,
71
+ warnings,
72
+ };
73
+ }
@@ -199,7 +199,11 @@ export function collectTeamMembers(ctx, { caller: explicitCaller = null } = {})
199
199
  if (teams?.listMembers && caller) {
200
200
  try {
201
201
  for (const m of teams.listMembers(caller) ?? []) {
202
- push(m?.id ?? m?.sessionId, m?.name, { provider: m?.provider, model: m?.model, source: "名册" });
202
+ // role/status 是宿主 TeamMemberView 的既有字段:批量操作要排除 Lead 本人,
203
+ // 也要跳过 provisioning/failed 这类不可操作的成员,所以必须带上。
204
+ push(m?.id ?? m?.sessionId, m?.name, {
205
+ provider: m?.provider, model: m?.model, role: m?.role, status: m?.status, source: "名册",
206
+ });
203
207
  }
204
208
  } catch {
205
209
  // 名册不可用:继续用 agents 列表兜底
@@ -215,3 +219,80 @@ export function collectTeamMembers(ctx, { caller: explicitCaller = null } = {})
215
219
  }
216
220
  return out;
217
221
  }
222
+
223
+ /** 目标是否为「全部成员」:空、`*`、`all` 都算(大小写不敏感)。 */
224
+ export function isAllTarget(target) {
225
+ const t = String(target ?? "").trim().toLowerCase();
226
+ return t === "" || t === "*" || t === "all";
227
+ }
228
+
229
+ /**
230
+ * 把一次操作的 target 解析成「一个或多个」目标。
231
+ *
232
+ * 【为什么要批量形态】批量给所有子代理统一模型/权限是高频需求(新建一批成员后统一对齐),
233
+ * 逐个传 sessionId 既慢又容易漏。约定:空 / `*` / `all` = 全部成员(**排除 Lead 本人**,
234
+ * 因为「所有子代理」的语义就是下属,不该把 Lead 自己也改掉)。
235
+ *
236
+ * @param {object} ctx 插件 context
237
+ * @param {string} target 原始 target
238
+ * @param {{caller?: object}} [opts] 显式发起方
239
+ * @returns {{all: boolean, targets: Array<{sessionId: string, member?: object, agent?: object|null, matched?: string}>} | {error: string}}
240
+ */
241
+ export function resolveTargets(ctx, target, { caller: explicitCaller = null } = {}) {
242
+ const agents = ctx?.get?.("agents");
243
+ if (isAllTarget(target)) {
244
+ const members = collectTeamMembers(ctx, { caller: explicitCaller });
245
+ // 只取可操作的下属:排除 Lead,排除正在创建/已失败的成员
246
+ const usable = members.filter((m) => m.role !== "lead" && m.status !== "provisioning" && m.status !== "failed");
247
+ if (usable.length === 0) {
248
+ const why = members.length === 0
249
+ ? "名册里没有任何成员"
250
+ : `名册里有 ${members.length} 个条目,但没有可操作的下属(都排除了 Lead / provisioning / failed)`;
251
+ return { error: `没有可操作的目标成员(${why})` };
252
+ }
253
+ return {
254
+ all: true,
255
+ targets: usable.map((m) => ({
256
+ sessionId: m.id,
257
+ member: m,
258
+ agent: agents?.get?.(m.id) ?? null,
259
+ matched: "全部",
260
+ })),
261
+ };
262
+ }
263
+ const found = findTargetAgent(ctx, target, { caller: explicitCaller });
264
+ if (found?.error) return { error: found.error };
265
+ return { all: false, targets: [found] };
266
+ }
267
+
268
+ /**
269
+ * 定位 Team Lead 会话(「跟随主代理」的落点)。
270
+ *
271
+ * 【为什么要专门找 Lead】「模型/权限留空 = 跟主代理一致」里的主代理指 Team Lead,
272
+ * 而不是发起这次调用的那个成员——由成员自己发起时,跟随它本身毫无意义。
273
+ * 宿主 TeamMemberView 的 role 字段能直接识别,不需要猜。
274
+ *
275
+ * @param {object} ctx 插件 context
276
+ * @param {{caller?: object}} [opts] 显式发起方
277
+ * @returns {{sessionId: string, agent: object|null, source: string} | {error: string}}
278
+ */
279
+ export function resolveLeader(ctx, { caller: explicitCaller = null } = {}) {
280
+ const teams = ctx?.get?.("agentTeams");
281
+ const agents = ctx?.get?.("agents");
282
+ const caller = pickTeamCaller(ctx, explicitCaller);
283
+ if (!caller) return { error: "拿不到发起方,无法定位 Lead(可用 sessionId 显式指定目标)" };
284
+ let members = [];
285
+ try {
286
+ members = teams?.listMembers?.(caller) ?? [];
287
+ } catch (error) {
288
+ return { error: `读取名册失败:${String(error?.message ?? error)}` };
289
+ }
290
+ if (members.length === 0) return { error: "名册为空,无法定位 Lead(可用 sessionId 显式指定目标)" };
291
+ // 发起方自己就是 Lead:直接用(省一次 agents 查找)
292
+ const callerId = caller?.session?.id ?? null;
293
+ const selfRow = callerId ? members.find((m) => m?.id === callerId && m?.role === "lead") : null;
294
+ if (selfRow) return { sessionId: callerId, agent: caller, source: "发起方即 Lead" };
295
+ const leadRow = members.find((m) => m?.role === "lead");
296
+ if (!leadRow) return { error: "名册里没有 Lead 行(role=lead),无法定位主代理" };
297
+ return { sessionId: leadRow.id, agent: agents?.get?.(leadRow.id) ?? null, source: "名册 Lead 行" };
298
+ }
File without changes
@@ -0,0 +1,138 @@
1
+ // dsh-session-conductor — 权限预设指令 `/perm-switch` 与 AI 工具 `set_member_permission`
2
+ //
3
+ // 【为什么需要指令】子代理会话在界面上无法手动切换权限预设(界面只对主会话开放),
4
+ // 而权限预设一旦指向当前不可用的值(典型:`auto` 在「自动审查」关闭后悬空),
5
+ // 该成员就永久唤不醒,且宿主只报一句 `subagent/not-resumable`,完全指不到权限层。
6
+ // 所以必须提供一条不经界面的入口:用户直接在聊天框切,AI 也能用工具切。
7
+ //
8
+ // 【三条形态】与 `/model-switch` 对齐:
9
+ // · /perm-switch → 列出成员与各自权限预设(并标出 auto 悬空告警)
10
+ // · /perm-switch <成员|sessionId> → 查该成员的 preset / sandbox / approval
11
+ // · /perm-switch <成员|sessionId> <预设> → 切换(追加 permission/preset 事件)
12
+ //
13
+ // 【注册方式照宿主官方范例】ctx.commands.register({ definitionId, name, description, input, handler }),
14
+ // definitionId 用 @deepseek-ai/dsh-commands/brand 的 CommandDefinitionId() 包一层。
15
+ // 全程动态导入宿主包:工作区自测环境没有 @deepseek-ai/* 依赖,静态导入会在导入期
16
+ // ERR_MODULE_NOT_FOUND 并连累整个插件加载失败;拿不到就跳过注册并留痕。
17
+ import { log } from "../../shared/log.js";
18
+ import {
19
+ describeMemberPermission,
20
+ findTargetAgent,
21
+ renderMemberPermissionList,
22
+ readPresetCatalog,
23
+ switchMemberPermission,
24
+ } from "./permission.js";
25
+ import { resolveTargets, isAllTarget } from "./locate.js";
26
+ import { readLeaderRoute } from "./lead.js";
27
+ import { applyPermissionToMany, renderBatchReport } from "./batch.js";
28
+
29
+ const COMMAND_NAME = "perm-switch";
30
+ const DEFINITION_ID = "@deepseek-ai/dsh-session-conductor/command-perm-switch";
31
+
32
+ /**
33
+ * 指令处理入口(导出便于单测直接驱动,不必走宿主)。
34
+ *
35
+ * @param {object} ctx 插件 context
36
+ * @param {{rawInput?: string, agent?: object}} invocation 宿主给的调用信息
37
+ * @returns {Promise<{kind: "success"|"error", text: string}>} 交给聊天框渲染
38
+ */
39
+ export async function handlePermSwitchCommand(ctx, invocation) {
40
+ const raw = String(invocation?.rawInput ?? "").trim();
41
+ const parts = raw.split(/\s+/).filter(Boolean);
42
+ const caller = invocation?.agent ?? null;
43
+ try {
44
+ // 无参:列成员与各自预设(与批量执行分开,避免「空参数」有两种含义)
45
+ if (parts.length === 0) return { kind: "success", text: await renderMemberPermissionList(ctx, invocation) };
46
+
47
+ const targetRaw = parts[0];
48
+ if (parts.length === 1) {
49
+ if (isAllTarget(targetRaw)) {
50
+ return {
51
+ kind: "error",
52
+ text: `批量操作需要指定预设:\n /${COMMAND_NAME} * auto ← 所有子代理换成我自己现在的权限预设(最常用)\n /${COMMAND_NAME} * <预设名> ← 所有子代理换成指定预设`,
53
+ };
54
+ }
55
+ const found = findTargetAgent(ctx, targetRaw, { caller });
56
+ if (found?.error) return { kind: "error", text: `找不到成员:${found.error}` };
57
+ return { kind: "success", text: (await describeMemberPermission(found)).join("\n") };
58
+ }
59
+
60
+ // 二参:切换(单个或全部)
61
+ let preset = parts[1];
62
+ let followedLead = null;
63
+ if (preset.toLowerCase() === "auto") {
64
+ const lead = await readLeaderRoute(ctx, { caller });
65
+ if (lead.error) return { kind: "error", text: `跟随主代理失败:${lead.error}` };
66
+ if (!lead.preset) {
67
+ return { kind: "error", text: `主代理(${lead.sessionId})自己没有记录权限预设,无法跟随;请显式指定预设名` };
68
+ }
69
+ preset = lead.preset;
70
+ followedLead = lead;
71
+ }
72
+
73
+ const resolved = resolveTargets(ctx, targetRaw, { caller });
74
+ if (resolved?.error) return { kind: "error", text: `找不到成员:${resolved.error}` };
75
+
76
+ // 单个目标:沿用原有回执(含旋钮与可选预设)
77
+ if (!resolved.all && resolved.targets.length === 1) {
78
+ const found = resolved.targets[0];
79
+ const result = await switchMemberPermission(ctx, found.sessionId, preset);
80
+ if (result.error) return { kind: "error", text: `切换失败:${result.error}` };
81
+ const name = found.member?.name ? `${found.member.name}(${found.sessionId})` : found.sessionId;
82
+ const lines = [`已把成员 ${name} 的权限预设切成 ${result.to}`, `原预设:${result.from ?? "(未记录)"} | 新事件 seq:${result.seq}`];
83
+ if (followedLead) lines.push(`预设来源:跟随主代理 ${followedLead.sessionId}`);
84
+ if (result.note) lines.push(`说明:${result.note}`);
85
+ lines.push(`当前旋钮:sandbox=${result.knobs?.sandbox ?? "(未记录)"} | approval=${result.knobs?.approval ?? "(未记录)"}`);
86
+ const catalog = await readPresetCatalog(ctx);
87
+ if (!catalog.error) lines.push(`可选预设:${catalog.names.join(" / ")}`);
88
+ lines.push("该改动写在会话日志里,成员下次加载/唤醒时按折叠语义生效。");
89
+ return { kind: "success", text: lines.join("\n") };
90
+ }
91
+
92
+ // 全部目标:串行逐个切,回执带名单
93
+ const report = await applyPermissionToMany(ctx, resolved.targets, preset);
94
+ const extra = [];
95
+ if (followedLead) {
96
+ extra.push(`预设来源:跟随主代理 ${followedLead.sessionId}(${followedLead.preset})`);
97
+ for (const w of followedLead.warnings ?? []) extra.push(`提示:${w}`);
98
+ }
99
+ extra.push("批量切换逐条写入各成员会话日志;单个成员失败不影响其余成员。");
100
+ return {
101
+ kind: "success",
102
+ text: renderBatchReport(`批量切换权限预设 → ${preset}`, report, extra),
103
+ };
104
+ } catch (error) {
105
+ return { kind: "error", text: `执行失败:${String(error?.message ?? error)}` };
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 注册 `/perm-switch` 指令。
111
+ *
112
+ * @returns {Promise<boolean>} true = 已注册;false = 宿主 commands 服务或 brand 包不可用(已留痕)
113
+ */
114
+ export async function registerPermSwitchCommand(ctx) {
115
+ const commands = ctx?.get?.("commands");
116
+ if (!commands || typeof commands.register !== "function") {
117
+ log(ctx, "权限指令未注册:ctx.commands 不可用(宿主未挂载 commands 服务)");
118
+ return false;
119
+ }
120
+ let CommandDefinitionId = null;
121
+ try {
122
+ const mod = await import("@deepseek-ai/dsh-commands/brand");
123
+ if (typeof mod?.CommandDefinitionId === "function") CommandDefinitionId = mod.CommandDefinitionId;
124
+ } catch { /* 工作区自测环境没有宿主依赖 → 走下面的返回 false */ }
125
+ if (!CommandDefinitionId) {
126
+ log(ctx, "权限指令未注册:拿不到 @deepseek-ai/dsh-commands/brand 的 CommandDefinitionId(缺宿主依赖)");
127
+ return false;
128
+ }
129
+ commands.register({
130
+ definitionId: CommandDefinitionId(DEFINITION_ID),
131
+ name: COMMAND_NAME,
132
+ description: "查看或切换成员(子代理)的权限预设;无参 = 列出成员与各自预设",
133
+ input: { hint: "[<成员名|sessionId> [<预设名>]]" },
134
+ recordInput: false,
135
+ handler: (invocation) => handlePermSwitchCommand(ctx, invocation),
136
+ });
137
+ return true;
138
+ }