dsh-session-conductor 1.3.9 → 1.3.10

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/lib/apply.js CHANGED
@@ -1533,7 +1533,8 @@ export async function apply(ctx, config = {}) {
1533
1533
  // 先校验组合有效性——无效组合会让后续回合的模型请求被拒(表现为「切换生效但回合失败」)
1534
1534
  const pair = await validateModelPair(ctx, provider, model);
1535
1535
  if (pair.error) return `切换失败:${pair.error}`;
1536
- const found = findTargetAgent(ctx, args?.target);
1536
+ // 显式传发起方:与聊天指令 /model-switch 走**同一条**解析路径(抽公共后两边只此一份)
1537
+ const found = findTargetAgent(ctx, args?.target, { caller: exec?.agent ?? null });
1537
1538
  if (found.error) return `切换失败:${found.error}`;
1538
1539
  const switchResult = await switchAgentModel(ctx, found.sessionId, provider, model, found.agent);
1539
1540
  if (switchResult.error) return `切换失败:${switchResult.error}`;
@@ -17,7 +17,7 @@
17
17
  // 就抛 ERR_MODULE_NOT_FOUND,连累整个插件加载失败(本项目在 defineTool / createUserMessage
18
18
  // 上踩过同类坑)。因此拿不到宿主能力时**跳过注册并如实留痕**,绝不影响插件其它功能。
19
19
  import { log } from "../../shared/log.js";
20
- import { findTargetAgent, pickTeamCaller } from "./locate.js";
20
+ import { findTargetAgent, collectTeamMembers } from "./locate.js";
21
21
  import { getMemberModelOverride } from "./override.js";
22
22
  import { readLastRequestModel, registerSwitchWatch, switchAgentModel, peekLastRequestModel } from "./switch.js";
23
23
 
@@ -41,39 +41,9 @@ function memberLabel(found) {
41
41
  return name ? `${name}(${found.sessionId})` : String(found?.sessionId ?? "?");
42
42
  }
43
43
 
44
- /**
45
- * 名册:Team 成员(spawn_teammate 创建的队友,不在普通 agents 列表里)+ 普通 agents。
46
- * 两条来源都取不到时返回空数组(调用方据此给出可读提示)。
47
- */
48
- function collectMembers(ctx) {
49
- const out = [];
50
- const seen = new Set();
51
- const push = (id, name, extra = {}) => {
52
- if (!id || seen.has(id)) return;
53
- seen.add(id);
54
- out.push({ id, name: name ?? null, ...extra });
55
- };
56
- try {
57
- const teams = ctx?.get?.("agentTeams");
58
- const caller = pickTeamCaller(ctx);
59
- if (teams?.listMembers && caller) {
60
- for (const m of teams.listMembers(caller) ?? []) push(m?.id ?? m?.sessionId, m?.name, { provider: m?.provider, model: m?.model, source: "名册" });
61
- }
62
- } catch (error) {
63
- log(ctx, `模型指令:读 Team 名册失败(跳过该来源): ${String(error?.message ?? error)}`);
64
- }
65
- try {
66
- const agents = ctx?.get?.("agents");
67
- for (const a of agents?.list?.() ?? []) push(a?.session?.id ?? a?.id, a?.session?.title ?? null, { source: "agent" });
68
- } catch (error) {
69
- log(ctx, `模型指令:读 agents 列表失败(跳过该来源): ${String(error?.message ?? error)}`);
70
- }
71
- return out;
72
- }
73
-
74
44
  /** 无参:列成员与各自模型(**便宜来源**:本插件 override → 进程内最近一次请求 → 名册记录值)。 */
75
- async function renderMemberList(ctx) {
76
- const members = collectMembers(ctx);
45
+ async function renderMemberList(ctx, invocation) {
46
+ const members = collectTeamMembers(ctx, { caller: invocation?.agent ?? null });
77
47
  if (members.length === 0) {
78
48
  return "没读到任何成员。可能当前不是 Team 成员、名册不可用,或确实还没有队友。";
79
49
  }
@@ -164,8 +134,8 @@ export async function handleModelSwitchCommand(ctx, invocation) {
164
134
  const raw = String(invocation?.rawInput ?? "").trim();
165
135
  const parts = raw.split(/\s+/).filter(Boolean);
166
136
  try {
167
- if (parts.length === 0) return { kind: "success", text: await renderMemberList(ctx) };
168
- const found = findTargetAgent(ctx, parts[0]);
137
+ if (parts.length === 0) return { kind: "success", text: await renderMemberList(ctx, invocation) };
138
+ const found = findTargetAgent(ctx, parts[0], { caller: invocation?.agent ?? null });
169
139
  if (found?.error) return { kind: "error", text: `找不到成员:${found.error}` };
170
140
  if (parts.length === 1) return { kind: "success", text: await renderMemberDetail(found) };
171
141
  const pair = splitPair(parts[1]);
@@ -8,9 +8,13 @@
8
8
  * agentTeams.listMembers(caller) 需要一个 Team 成员作为 caller 来解析名册。
9
9
  * @returns {object|null} 可用 agent,取不到返回 null
10
10
  */
11
- export function pickTeamCaller(ctx) {
11
+ export function pickTeamCaller(ctx, preferred = null) {
12
12
  const agents = ctx.get("agents");
13
13
  if (!agents) return null;
14
+ // 0) 调用方显式给的发起方最可靠:命令/工具都能直接拿到 invocation.agent / exec.agent。
15
+ // currentInitiator() 依赖「当前在某个 agent 的异步链里」,命令处理器里经常拿不到 ——
16
+ // 这正是「工具能切、聊天指令说找不到成员」的根因。
17
+ if (preferred) return preferred;
14
18
  try {
15
19
  const initiator = agents.currentInitiator?.();
16
20
  if (initiator) return initiator;
@@ -93,14 +97,14 @@ export async function validateModelPair(ctx, provider, model) {
93
97
  * ③ 标题子串匹配(sessionTitle 快照)。
94
98
  * @returns {{sessionId: string, agent: object|null, matched: string, member?: object} | {error: string}}
95
99
  */
96
- export function findTargetAgent(ctx, target) {
100
+ export function findTargetAgent(ctx, target, { caller: explicitCaller = null } = {}) {
97
101
  const raw = String(target ?? "").trim();
98
102
  if (!raw) return { error: "target 不能为空(传 sessionId / 成员名 / 标题关键字)" };
99
103
  const agents = ctx.get("agents");
100
104
  const teams = ctx.get("agentTeams");
101
105
  // ① Team 名册:name 或 id 精确匹配优先
102
106
  if (teams?.listMembers && agents) {
103
- const caller = pickTeamCaller(ctx);
107
+ const caller = pickTeamCaller(ctx, explicitCaller);
104
108
  if (caller) {
105
109
  let members = [];
106
110
  try {
@@ -155,3 +159,45 @@ export function findTargetAgent(ctx, target) {
155
159
  if (hits.length === 0) return { error: `没有匹配「${raw}」的活跃成员(可用 list_agents 看成员列表)` };
156
160
  return { error: `「${raw}」匹配到 ${hits.length} 个成员,请用 sessionId 精确指定:${hits.map((a) => a?.session?.id).join(", ")}` };
157
161
  }
162
+
163
+ /**
164
+ * 统一名册:Team 成员(spawn_teammate 创建的队友,不在普通 agents 列表里)+ 普通 agents。
165
+ *
166
+ * 【为什么要抽公共】聊天指令与 AI 工具原先各写一份「谁是成员」:
167
+ * 命令版靠 `pickTeamCaller(ctx)` 猜发起方,在命令处理器里经常拿不到 ⇒ 名册为空 ⇒
168
+ * 报「没有匹配的活跃成员」,而同样的名字走 AI 工具(在 agent 异步链里调用)却能找到。
169
+ * 现在两处都调这一个函数,并且都显式把 invocation.agent / exec.agent 传进来。
170
+ *
171
+ * @param {object} ctx 插件上下文
172
+ * @param {{caller?: object}} [opts] caller = 发起方 agent(命令/工具里直接给,最可靠)
173
+ * @returns {Array<{id, name, provider?, model?, source}>} 去重后的成员列表(取不到返回空数组)
174
+ */
175
+ export function collectTeamMembers(ctx, { caller: explicitCaller = null } = {}) {
176
+ const out = [];
177
+ const seen = new Set();
178
+ const push = (id, name, extra = {}) => {
179
+ if (!id || seen.has(id)) return;
180
+ seen.add(id);
181
+ out.push({ id, name: name ?? null, ...extra });
182
+ };
183
+ const teams = ctx?.get?.("agentTeams");
184
+ const caller = pickTeamCaller(ctx, explicitCaller);
185
+ if (teams?.listMembers && caller) {
186
+ try {
187
+ for (const m of teams.listMembers(caller) ?? []) {
188
+ push(m?.id ?? m?.sessionId, m?.name, { provider: m?.provider, model: m?.model, source: "名册" });
189
+ }
190
+ } catch {
191
+ // 名册不可用:继续用 agents 列表兜底
192
+ }
193
+ }
194
+ try {
195
+ const agents = ctx?.get?.("agents");
196
+ for (const a of agents?.list?.() ?? []) {
197
+ push(a?.session?.id ?? a?.id, a?.session?.title ?? null, { source: "agent" });
198
+ }
199
+ } catch {
200
+ // agents 不可用:保持已有结果
201
+ }
202
+ return out;
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-conductor",
3
- "version": "1.3.9",
3
+ "version": "1.3.10",
4
4
  "description": "DSH session management enhancement: archive/restore sessions, batch & conditional delete, AI auto-rename, interrupted-session auto-resume, release idle sessions, workspace grouping, undo last message, repair damaged session logs, value analysis, full-text search, per-member model switching (list_models / set_member_model), and plan/closing template injection (system-prompt + context).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",