dsh-session-conductor 1.3.11 → 1.3.16

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 (86) hide show
  1. package/README.md +23 -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/command.js +61 -7
  31. package/lib/features/member-model/create-teammate.js +0 -0
  32. package/lib/features/member-model/index.js +15 -1
  33. package/lib/features/member-model/lead.js +73 -0
  34. package/lib/features/member-model/locate.js +82 -1
  35. package/lib/features/member-model/override.js +0 -0
  36. package/lib/features/member-model/permission-command.js +138 -0
  37. package/lib/features/member-model/permission.js +213 -0
  38. package/lib/features/member-model/setup.js +0 -0
  39. package/lib/features/member-model/switch.js +254 -23
  40. package/lib/features/member-model/tools.js +324 -0
  41. package/lib/features/memory-recall/index.js +0 -0
  42. package/lib/features/rename/analysis.js +0 -0
  43. package/lib/features/rename/title.js +0 -0
  44. package/lib/features/rename/value.js +0 -0
  45. package/lib/features/session-action-routes.js +435 -0
  46. package/lib/features/session-messaging/index.js +0 -0
  47. package/lib/features/session-messaging/logic.js +0 -0
  48. package/lib/features/session-messaging/tools.js +0 -0
  49. package/lib/features/session-ops-routes.js +0 -0
  50. package/lib/features/settings-routes.js +400 -0
  51. package/lib/features/trash.js +0 -0
  52. package/lib/features/value-index.js +0 -0
  53. package/lib/features/value-report-routes.js +211 -0
  54. package/lib/group.js +0 -0
  55. package/lib/i18n/en.json +0 -0
  56. package/lib/i18n/zh.json +0 -0
  57. package/lib/index.js +0 -0
  58. package/lib/repair.js +0 -0
  59. package/lib/seq-gap-repair.js +0 -0
  60. package/lib/session-codec.js +0 -0
  61. package/lib/session-log.js +0 -0
  62. package/lib/sessions/detach.js +0 -0
  63. package/lib/sessions/events.js +0 -0
  64. package/lib/sessions/interruption.js +0 -0
  65. package/lib/sessions/list-cache.js +0 -0
  66. package/lib/sessions/list.js +0 -0
  67. package/lib/sessions/lookup.js +0 -0
  68. package/lib/sessions/route.js +0 -0
  69. package/lib/sessions/session-id.js +0 -0
  70. package/lib/sessions/tail-io.js +185 -0
  71. package/lib/sessions/turn.js +0 -0
  72. package/lib/shared/atomic-file.js +0 -0
  73. package/lib/shared/constants.js +0 -0
  74. package/lib/shared/json-store.js +0 -0
  75. package/lib/shared/log.js +0 -0
  76. package/lib/shared/timing.js +0 -0
  77. package/lib/shared/util.js +0 -0
  78. package/lib/template-inject.js +0 -0
  79. package/lib/value.js +0 -0
  80. package/lib/zstd-frames.js +0 -0
  81. package/package.json +1 -1
  82. package/scripts/check-member-model.mjs +0 -0
  83. package/skills/dsh-session-conductor-functions.md +0 -0
  84. package/skills/dsh-session-conductor.md +0 -0
  85. package/skills/dsh-session-official-doc.md +0 -0
  86. package/skills/task-completion-report.md +0 -0
@@ -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} * <预设名> ← 全部成员切到该预设\n /${COMMAND_NAME} * auto ← 全部成员跟随主代理(Lead)当前预设`,
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
+ }
@@ -0,0 +1,213 @@
1
+ // dsh-session-conductor — 成员(子代理)权限预设的查看与切换
2
+ //
3
+ // 【这个文件解决什么】子代理会话在界面上无法手动切换权限预设(只有主会话能切),
4
+ // 一旦它的 `permission/preset` 指向一个当前不可用的预设,该成员就会永久唤不醒:
5
+ // 宿主 permission-presets 的 pinInitialPermission() 在会话发布前校验
6
+ // if (preset === 'auto' && autoAdmit === undefined) throw new Error('cannot restore preset "auto" without its active integration')
7
+ // 而 subagent 的 coldResume() 把这个异常统一包成 `subagent/not-resumable`,
8
+ // 报错文本完全指不到权限预设这一层。因此需要一条「不依赖界面、直接落到会话日志」的通道。
9
+ //
10
+ // 【为什么是追加事件而不是改历史】权限三个旋钮(preset / sandbox / approval)在宿主里是**折叠**语义,
11
+ // 宿主明确保证「The LAST such event is the session's override」。所以追加一条 `permission/preset`
12
+ // 就能让新值生效,已有事件一条不动、可删尾部帧回滚。
13
+ //
14
+ // 【为什么只写 preset 一条】切换预设时宿主自己会按 spec 补齐 sandbox / approval(见其 apply())。
15
+ // 这里不重复写:若目标预设的 sandbox/approval 与会话现值不同,回执会明确提示「需要宿主侧补齐」,
16
+ // 避免我们自己造出「preset 与 knobs 不一致」的标签失配。
17
+ import { log } from "../../shared/log.js";
18
+ import { findTargetAgent, collectTeamMembers } from "./locate.js";
19
+
20
+ /** 读会话日志路径(按 sessionId 定位,与切换模型链路同一套定位)。 */
21
+ async function locateLog(sessionId) {
22
+ const { findSessionLogById, sessionsRootOf } = await import("../../sessions/events.js");
23
+ const filePath = findSessionLogById(sessionId, sessionsRootOf());
24
+ return filePath ?? null;
25
+ }
26
+
27
+ /**
28
+ * 折叠读成员当前的权限状态(只看最后一条同类事件,与宿主折叠语义一致)。
29
+ *
30
+ * @param {string} sessionId 成员会话 id
31
+ * @returns {Promise<{preset: string|null, sandbox: string|null, approval: string|null, lastSeq: number, error?: string}>}
32
+ */
33
+ export async function readMemberPermission(sessionId) {
34
+ const filePath = await locateLog(sessionId);
35
+ if (!filePath) return { preset: null, sandbox: null, approval: null, lastSeq: -1, error: "会话日志不存在" };
36
+ const { readFile } = await import("node:fs/promises");
37
+ const { decodeAllFrames } = await import("../../zstd-frames.js");
38
+ let text;
39
+ try {
40
+ text = await decodeAllFrames(await readFile(filePath));
41
+ } catch (error) {
42
+ return { preset: null, sandbox: null, approval: null, lastSeq: -1, error: `日志解码失败:${String(error?.message ?? error)}` };
43
+ }
44
+ let preset = null;
45
+ let sandbox = null;
46
+ let approval = null;
47
+ let lastSeq = -1;
48
+ for (const line of text.split("\n")) {
49
+ if (line.trim() === "") continue;
50
+ let e;
51
+ try { e = JSON.parse(line); } catch { continue; }
52
+ if (typeof e.seq === "number" && e.seq > lastSeq) lastSeq = e.seq;
53
+ if (e.type === "permission/preset") preset = e.data?.preset ?? null;
54
+ else if (e.type === "sandbox/mode") sandbox = e.data?.mode ?? null;
55
+ else if (e.type === "approval/policy") approval = e.data?.policy ?? null;
56
+ }
57
+ return { preset, sandbox, approval, lastSeq };
58
+ }
59
+
60
+ /**
61
+ * 读宿主当前可选的预设目录(用于校验,避免把会话写成宿主无法恢复的值)。
62
+ *
63
+ * @param {object} ctx 插件 context
64
+ * @returns {Promise<{names: string[], defaults: string[], defaultPreset: string|null, specs: Map<string, {sandbox?: string, approval?: string}>} | {error: string}>}
65
+ */
66
+ export async function readPresetCatalog(ctx) {
67
+ const service = ctx.get?.("permissionPresets") ?? null;
68
+ if (!service || typeof service.catalog !== "function") {
69
+ return { error: "拿不到 permissionPresets.catalog(宿主未挂载该服务),无法校验预设名" };
70
+ }
71
+ let catalog;
72
+ try {
73
+ catalog = await service.catalog();
74
+ } catch (error) {
75
+ return { error: `读取预设目录失败:${String(error?.message ?? error)}` };
76
+ }
77
+ const options = Array.isArray(catalog?.options) ? catalog.options : [];
78
+ const names = options.map((o) => String(o?.name ?? o?.id ?? "")).filter(Boolean);
79
+ const specs = new Map();
80
+ for (const option of options) {
81
+ const name = String(option?.name ?? option?.id ?? "");
82
+ if (!name) continue;
83
+ specs.set(name, { sandbox: option?.sandbox, approval: option?.approval });
84
+ }
85
+ return {
86
+ names,
87
+ defaults: Array.isArray(catalog?.defaultOptions) ? catalog.defaultOptions.map((o) => String(o?.name ?? "")) : [],
88
+ defaultPreset: catalog?.defaultPreset ?? null,
89
+ specs,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * 把成员的权限预设切到目标值。
95
+ *
96
+ * 实现:定位会话日志 → 折叠读当前状态 → 校验预设名在宿主目录里 → 追加一条
97
+ * `permission/preset`(独立 zstd 帧、带 checksum、长度 CAS、fsync)→ 写后复核。
98
+ *
99
+ * @param {object} ctx 插件 context
100
+ * @param {string} sessionId 成员会话 id
101
+ * @param {string} preset 目标预设名
102
+ * @returns {Promise<{ok: true, from: string|null, to: string, seq: number, knobs: object, note?: string} | {error: string}>}
103
+ */
104
+ export async function switchMemberPermission(ctx, sessionId, preset) {
105
+ const target = String(preset ?? "").trim();
106
+ if (!target) return { error: "预设名不能为空" };
107
+
108
+ const catalog = await readPresetCatalog(ctx);
109
+ if (catalog.error) return { error: catalog.error };
110
+ if (!catalog.names.includes(target)) {
111
+ // 明确点出 auto 的特殊性:它只有在「自动审查集成」挂载时才可选,
112
+ // 否则写进去会让该会话下次发布时直接抛错(表现为唤不醒)。
113
+ const hint = target === "auto"
114
+ ? ";注意 auto 只有在「自动审查」集成启用时才可用,写入后一旦关闭该功能,该会话就会唤不醒"
115
+ : "";
116
+ return { error: `预设 "${target}" 不在当前可选列表里(可选:${catalog.names.join(" / ")})${hint}` };
117
+ }
118
+
119
+ const before = await readMemberPermission(sessionId);
120
+ if (before.error) return { error: before.error };
121
+ if (before.preset === target) {
122
+ return { ok: true, from: before.preset, to: target, seq: before.lastSeq, knobs: { sandbox: before.sandbox, approval: before.approval }, note: "已经是该预设,无需重复写入" };
123
+ }
124
+
125
+ const filePath = await locateLog(sessionId);
126
+ if (!filePath) return { error: "会话日志不存在" };
127
+ const { appendEventFrame } = await import("../../sessions/tail-io.js");
128
+ const written = await appendEventFrame(filePath, {
129
+ type: "permission/preset",
130
+ seq: before.lastSeq + 1,
131
+ time: Date.now(),
132
+ data: { preset: target },
133
+ });
134
+ if (!written.ok) return { error: `写入会话日志失败:${written.error}` };
135
+
136
+ // 写后复核:确认最后一条 preset 已生效(折叠语义下这就是当前值)
137
+ const after = await readMemberPermission(sessionId);
138
+ if (after.error) return { error: `写入后复核失败:${after.error}` };
139
+ if (after.preset !== target) return { error: `写入后复核不一致(期望 ${target},实际 ${after.preset})` };
140
+
141
+ // 预设有配套的 sandbox/approval 时给出提示:我们只写 preset,旋钮由宿主在下次加载时按 spec 补齐
142
+ const spec = catalog.specs.get(target);
143
+ let note;
144
+ if (spec && (spec.sandbox !== undefined || spec.approval !== undefined)) {
145
+ const mismatch = [];
146
+ if (spec.sandbox !== undefined && before.sandbox !== null && spec.sandbox !== before.sandbox) {
147
+ mismatch.push(`sandbox ${before.sandbox} → ${spec.sandbox}`);
148
+ }
149
+ if (spec.approval !== undefined && before.approval !== null && spec.approval !== before.approval) {
150
+ mismatch.push(`approval ${before.approval} → ${spec.approval}`);
151
+ }
152
+ if (mismatch.length > 0) {
153
+ note = `该预设自带 ${mismatch.join("、")};本工具只写 preset 事件,旋钮需宿主加载时补齐(若界面显示与实际不一致,属预期)`;
154
+ }
155
+ }
156
+ log(ctx, `权限切换:${sessionId} preset ${before.preset} → ${target}(seq ${written.seq})`);
157
+ return { ok: true, from: before.preset, to: target, seq: written.seq, knobs: { sandbox: before.sandbox, approval: before.approval }, note };
158
+ }
159
+
160
+ /** 成员标签(有名字就带上,便于人读)。 */
161
+ function memberLabel(found) {
162
+ const name = found?.member?.name;
163
+ return name ? `${name}(${found.sessionId})` : String(found?.sessionId ?? "?");
164
+ }
165
+
166
+ /**
167
+ * 渲染单个成员的权限快照(供工具与指令共用)。
168
+ *
169
+ * @param {object} found findTargetAgent 的返回值
170
+ * @returns {Promise<string[]>} 多行文本
171
+ */
172
+ export async function describeMemberPermission(found) {
173
+ const state = await readMemberPermission(found.sessionId);
174
+ if (state.error) return [`成员 ${memberLabel(found)}`, ` 读取失败:${state.error}`];
175
+ return [
176
+ `成员 ${memberLabel(found)}`,
177
+ ` 权限预设:${state.preset ?? "(未记录)"}`,
178
+ ` sandbox:${state.sandbox ?? "(未记录)"} | approval:${state.approval ?? "(未记录)"}`,
179
+ ` 末 seq:${state.lastSeq}`,
180
+ ];
181
+ }
182
+
183
+ /** 列出全部成员及其权限预设(指令无参形态)。 */
184
+ export async function renderMemberPermissionList(ctx, invocation) {
185
+ const members = collectTeamMembers(ctx, { caller: invocation?.agent ?? null });
186
+ if (members.length === 0) return "没读到任何成员。可能当前不是 Team 成员、名册不可用,或确实还没有队友。";
187
+ const catalog = await readPresetCatalog(ctx);
188
+ const lines = [`成员权限预设(共 ${members.length} 个)`, ""];
189
+ for (const m of members) {
190
+ const state = await readMemberPermission(m.sessionId);
191
+ const name = m.name ? `${m.name}(${m.sessionId})` : m.sessionId;
192
+ const warn = state.preset === "auto" && !catalog.error && !catalog.names.includes("auto")
193
+ ? " ⚠️ auto 当前不可用(自动审查未启用),该成员可能唤不醒"
194
+ : "";
195
+ lines.push(` ${name}:${state.preset ?? "(未记录)"}${warn}`);
196
+ }
197
+ lines.push("");
198
+ if (catalog.error) lines.push(`可选预设:${catalog.error}`);
199
+ else lines.push(`可选预设:${catalog.names.join(" / ")}`);
200
+ lines.push("");
201
+ lines.push("切换:/perm-switch <成员名或 sessionId> <预设名>");
202
+ return lines.join("\n");
203
+ }
204
+
205
+ /** 解析「成员 + 预设」两个参数(预设名不含斜杠,直接取剩余文本)。 */
206
+ export function parsePermissionArgs(rawInput) {
207
+ const raw = String(rawInput ?? "").trim();
208
+ if (raw === "") return { parts: [] };
209
+ const parts = raw.split(/\s+/).filter(Boolean);
210
+ return { parts };
211
+ }
212
+
213
+ export { findTargetAgent };
File without changes