evolclaw 2.8.2 → 2.8.3

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.
@@ -988,15 +988,26 @@ export class AgentRunner {
988
988
  // Plugin implementation
989
989
  export class ClaudeAgentPlugin {
990
990
  name = 'claude';
991
- isEnabled(config) {
992
- return true;
993
- }
994
- createAgent(config, callbacks) {
995
- const anthropic = resolveAnthropicConfig(config);
996
- const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, config);
991
+ isEnabled(_globalConfig, agent) {
992
+ // Only instantiate this baseagent for agents that declare it.
993
+ return !!agent.config.agents?.claude;
994
+ }
995
+ createAgent(globalConfig, agent, callbacks) {
996
+ // Per-agent override: read from agent.json's agents.claude block first.
997
+ const override = agent.config.agents?.claude;
998
+ const anthropic = resolveAnthropicConfig(globalConfig, override);
999
+ // Merge per-agent claude block into config so runner reads useSettingSources etc.
1000
+ const merged = {
1001
+ ...globalConfig,
1002
+ agents: {
1003
+ ...(globalConfig.agents || {}),
1004
+ claude: { ...(globalConfig.agents?.claude || {}), ...(override || {}) },
1005
+ },
1006
+ };
1007
+ const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
997
1008
  if (anthropic.effort) {
998
1009
  agentRunner.setEffort(anthropic.effort);
999
1010
  }
1000
- return { agent: agentRunner };
1011
+ return { evolagentName: agent.name, baseagent: 'claude', agent: agentRunner };
1001
1012
  }
1002
1013
  }
@@ -301,17 +301,28 @@ export class CodexRunner {
301
301
  // ── Plugin ──
302
302
  export class CodexAgentPlugin {
303
303
  name = 'codex';
304
- isEnabled(config) {
304
+ isEnabled(globalConfig, agent) {
305
+ if (!agent.config.agents?.codex)
306
+ return false;
305
307
  try {
306
- const resolved = resolveOpenaiConfig(config);
308
+ const override = agent.config.agents.codex;
309
+ const resolved = resolveOpenaiConfig(globalConfig, override);
307
310
  return !!resolved.apiKey;
308
311
  }
309
312
  catch {
310
313
  return false;
311
314
  }
312
315
  }
313
- createAgent(config, callbacks) {
314
- const resolved = resolveOpenaiConfig(config);
315
- return { agent: new CodexRunner(config, callbacks) };
316
+ createAgent(globalConfig, agent, callbacks) {
317
+ const override = agent.config.agents?.codex;
318
+ // Synthesize a per-agent config view so CodexRunner sees its own credentials.
319
+ const merged = {
320
+ ...globalConfig,
321
+ agents: {
322
+ ...(globalConfig.agents || {}),
323
+ codex: { ...(globalConfig.agents?.codex || {}), ...(override || {}) },
324
+ },
325
+ };
326
+ return { evolagentName: agent.name, baseagent: 'codex', agent: new CodexRunner(merged, callbacks) };
316
327
  }
317
328
  }
@@ -405,16 +405,27 @@ export class GeminiRunner {
405
405
  // ── Plugin ──
406
406
  export class GeminiAgentPlugin {
407
407
  name = 'gemini';
408
- isEnabled(config) {
408
+ isEnabled(globalConfig, agent) {
409
+ if (!agent.config.agents?.gemini)
410
+ return false;
409
411
  try {
410
- const resolved = resolveGoogleConfig(config);
412
+ const override = agent.config.agents.gemini;
413
+ const resolved = resolveGoogleConfig(globalConfig, override);
411
414
  return !!resolved.cliPath;
412
415
  }
413
416
  catch {
414
417
  return false;
415
418
  }
416
419
  }
417
- createAgent(config, callbacks) {
418
- return { agent: new GeminiRunner(config, callbacks) };
420
+ createAgent(globalConfig, agent, callbacks) {
421
+ const override = agent.config.agents?.gemini;
422
+ const merged = {
423
+ ...globalConfig,
424
+ agents: {
425
+ ...(globalConfig.agents || {}),
426
+ gemini: { ...(globalConfig.agents?.gemini || {}), ...(override || {}) },
427
+ },
428
+ };
429
+ return { evolagentName: agent.name, baseagent: 'gemini', agent: new GeminiRunner(merged, callbacks) };
419
430
  }
420
431
  }
@@ -80,6 +80,11 @@ export class AUNChannel {
80
80
  */
81
81
  async callAndTrace(method, params, opts) {
82
82
  this.trace('OUT', method, params);
83
+ // [DIAG-STALE] 记录调用瞬间 SDK 内部 _state,证明是否在 reconnecting 中误发
84
+ const sdkStateBefore = this.client?._state ?? 'no-client';
85
+ if (sdkStateBefore !== 'connected') {
86
+ logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} on non-connected SDK: sdk_state=${sdkStateBefore} evolclaw_connected=${this.connected}`);
87
+ }
83
88
  try {
84
89
  const result = await this.client.call(method, params);
85
90
  if (!opts?.silentOk) {
@@ -97,6 +102,9 @@ export class AUNChannel {
97
102
  code: e?.code,
98
103
  name: e?.name,
99
104
  });
105
+ // [DIAG-STALE] 失败时再记录一次 SDK _state,看错误类型是否为 ConnectionError
106
+ const sdkStateAfter = this.client?._state ?? 'no-client';
107
+ logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} FAILED: err_name=${e?.name ?? '?'} err_code=${e?.code ?? '?'} sdk_state_before=${sdkStateBefore} sdk_state_after=${sdkStateAfter} evolclaw_connected=${this.connected}`);
100
108
  logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
101
109
  throw e;
102
110
  }
@@ -887,6 +895,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
887
895
  if (!data || typeof data !== 'object')
888
896
  return;
889
897
  const state = data.state ?? '';
898
+ // [DIAG-STALE] 记录状态切换瞬间 evolclaw 的 connected 标志和 SDK 的内部 _state,
899
+ // 用于证明"reconnecting 时 connected 保持 true,导致 sendMessage 误放行"的假设
900
+ const sdkState = this.client?._state ?? 'no-client';
901
+ const connectedBefore = this.connected;
902
+ logger.info(`[AUN][DIAG-STALE] connection.state event: state=${state} attempt=${data.attempt ?? '-'} | connected_before=${connectedBefore} sdk_state=${sdkState}`);
890
903
  if (state === 'connected') {
891
904
  this.connected = true;
892
905
  this.reconnectAttempt = 0;
@@ -952,6 +965,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
952
965
  this.recallHandler = handler;
953
966
  }
954
967
  async sendMessage(channelId, text, context) {
968
+ // [DIAG-STALE] 进入 sendMessage 时记录 evolclaw connected 标志和 SDK _state,
969
+ // 用于检测两者是否一致:若 connected=true 但 sdk_state != 'connected',即为 stale 状态
970
+ const sdkStateOnEntry = this.client?._state ?? 'no-client';
971
+ if (this.connected !== (sdkStateOnEntry === 'connected')) {
972
+ logger.warn(`[AUN][DIAG-STALE] sendMessage entry MISMATCH: connected=${this.connected} sdk_state=${sdkStateOnEntry} channel=${channelId} text=${text.slice(0, 40)}`);
973
+ }
974
+ else {
975
+ logger.debug(`[AUN][DIAG-STALE] sendMessage entry: connected=${this.connected} sdk_state=${sdkStateOnEntry} channel=${channelId}`);
976
+ }
955
977
  if (!this.connected || !this.client) {
956
978
  logger.warn(`${this.logPrefix()} Cannot send: not connected`);
957
979
  return;
package/dist/config.js CHANGED
@@ -48,82 +48,97 @@ function loadCodexSettings() {
48
48
  catch { }
49
49
  return {};
50
50
  }
51
- export function resolveAnthropicConfig(config) {
51
+ /**
52
+ * Resolve anthropic credentials with optional override (from agent.json).
53
+ *
54
+ * Priority: override > globalConfig.agents.claude > env > ~/.claude/settings.json
55
+ *
56
+ * Override is matched against the same shape as `config.agents.claude` so
57
+ * EvolAgent's `agents.claude` block is wired in directly.
58
+ */
59
+ export function resolveAnthropicConfig(config, override) {
52
60
  const settings = loadClaudeSettings();
53
- // 过滤占位符,视为未配置
54
- const configApiKey = config.agents?.claude?.apiKey;
55
- const isPlaceholderKey = !configApiKey ||
56
- configApiKey.includes('your-') ||
57
- configApiKey.includes('placeholder');
58
- const apiKey = (isPlaceholderKey ? null : configApiKey)
61
+ const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
62
+ // apiKey: override → global → env → settings.json
63
+ const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
64
+ const globalApiKey = isPlaceholder(config.agents?.claude?.apiKey) ? undefined : config.agents?.claude?.apiKey;
65
+ const apiKey = overrideApiKey
66
+ || globalApiKey
59
67
  || process.env.ANTHROPIC_AUTH_TOKEN
60
68
  || settings.env?.ANTHROPIC_AUTH_TOKEN;
61
69
  if (!apiKey) {
62
- throw new Error('No API key found. Set one of: agents.claude.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
70
+ throw new Error('No API key found. Set one of: agents.claude.apiKey (per-agent or global), env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
63
71
  }
64
- // baseUrl 也过滤占位符
65
- const configBaseUrl = config.agents?.claude?.baseUrl;
66
- const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
67
- const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
72
+ const isPlaceholderUrl = (v) => !v || v.includes('api.anthropic.com');
73
+ const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
74
+ const globalBaseUrl = isPlaceholderUrl(config.agents?.claude?.baseUrl) ? undefined : config.agents?.claude?.baseUrl;
75
+ const baseUrl = overrideBaseUrl
76
+ || globalBaseUrl
68
77
  || process.env.ANTHROPIC_BASE_URL
69
78
  || settings.env?.ANTHROPIC_BASE_URL;
70
- const model = config.agents?.claude?.model
79
+ const model = override?.model
80
+ || config.agents?.claude?.model
71
81
  || settings.model
72
82
  || 'sonnet';
73
- const effort = config.agents?.claude?.effort
83
+ const effort = override?.effort
84
+ || config.agents?.claude?.effort
74
85
  || settings.effortLevel
75
86
  || undefined;
76
- const configExecPath = config.agents?.claude?.pathToClaudeCodeExecutable;
77
- const isPlaceholderExec = !configExecPath || configExecPath.includes('your-') || configExecPath.includes('placeholder');
78
- const pathToClaudeCodeExecutable = isPlaceholderExec ? undefined : configExecPath;
87
+ const pickExec = (v) => (!v || v.includes('your-') || v.includes('placeholder')) ? undefined : v;
88
+ const pathToClaudeCodeExecutable = pickExec(override?.pathToClaudeCodeExecutable)
89
+ || pickExec(config.agents?.claude?.pathToClaudeCodeExecutable);
79
90
  return { apiKey, baseUrl, model, effort, pathToClaudeCodeExecutable };
80
91
  }
81
- export function resolveOpenaiConfig(config) {
92
+ export function resolveOpenaiConfig(config, override) {
82
93
  const codexSettings = loadCodexSettings();
83
- // 过滤占位符,视为未配置
84
- const configApiKey = config.agents?.codex?.apiKey;
85
- const isPlaceholderKey = !configApiKey ||
86
- configApiKey.includes('your-') ||
87
- configApiKey.includes('placeholder');
88
- const apiKey = (isPlaceholderKey ? null : configApiKey)
94
+ const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
95
+ const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
96
+ const globalApiKey = isPlaceholder(config.agents?.codex?.apiKey) ? undefined : config.agents?.codex?.apiKey;
97
+ const apiKey = overrideApiKey
98
+ || globalApiKey
89
99
  || process.env.OPENAI_API_KEY
90
100
  || codexSettings.apiKey;
91
101
  if (!apiKey) {
92
- throw new Error('No OpenAI API key found. Set one of: agents.codex.apiKey, env OPENAI_API_KEY, or ~/.codex/auth.json');
102
+ throw new Error('No OpenAI API key found. Set one of: agents.codex.apiKey (per-agent or global), env OPENAI_API_KEY, or ~/.codex/auth.json');
93
103
  }
94
- // baseUrl 也过滤占位符(与 anthropic 保持一致:只检查默认域名)
95
- const configBaseUrl = config.agents?.codex?.baseUrl;
96
- const isPlaceholderUrl = configBaseUrl?.includes('api.openai.com');
97
- const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
104
+ const isPlaceholderUrl = (v) => !v || v.includes('api.openai.com');
105
+ const overrideBaseUrl = isPlaceholderUrl(override?.baseUrl) ? undefined : override?.baseUrl;
106
+ const globalBaseUrl = isPlaceholderUrl(config.agents?.codex?.baseUrl) ? undefined : config.agents?.codex?.baseUrl;
107
+ const baseUrl = overrideBaseUrl
108
+ || globalBaseUrl
98
109
  || process.env.OPENAI_BASE_URL
99
110
  || codexSettings.baseUrl
100
111
  || undefined;
101
- const model = config.agents?.codex?.model
112
+ const model = override?.model
113
+ || config.agents?.codex?.model
102
114
  || codexSettings.model
103
115
  || 'gpt-5.2-codex';
104
- const effort = config.agents?.codex?.effort || config.agents?.codex?.reasoning || undefined;
116
+ const effort = override?.effort
117
+ || override?.reasoning
118
+ || config.agents?.codex?.effort
119
+ || config.agents?.codex?.reasoning
120
+ || undefined;
105
121
  return { apiKey, baseUrl, model, effort };
106
122
  }
107
- export function resolveGoogleConfig(config) {
123
+ export function resolveGoogleConfig(config, override) {
108
124
  const googleCfg = config.agents?.gemini;
109
- // CLI path: config which gemini
110
- let cliPath = googleCfg?.cliPath || '';
125
+ const isPlaceholder = (v) => !v || v.includes('your-') || v.includes('placeholder');
126
+ let cliPath = override?.cliPath || googleCfg?.cliPath || '';
111
127
  if (!cliPath) {
112
128
  cliPath = commandExists('gemini') ? 'gemini' : '';
113
129
  }
114
- // Model: config default
115
- const model = googleCfg?.model || 'gemini-2.5-flash';
116
- // API key: config env (optional, CLI has OAuth)
117
- const configApiKey = googleCfg?.apiKey;
118
- const isPlaceholder = !configApiKey || configApiKey.includes('your-') || configApiKey.includes('placeholder');
119
- const apiKey = (isPlaceholder ? undefined : configApiKey)
130
+ const model = override?.model || googleCfg?.model || 'gemini-2.5-flash';
131
+ const overrideApiKey = isPlaceholder(override?.apiKey) ? undefined : override?.apiKey;
132
+ const globalApiKey = isPlaceholder(googleCfg?.apiKey) ? undefined : googleCfg?.apiKey;
133
+ const apiKey = overrideApiKey
134
+ || globalApiKey
120
135
  || process.env.GEMINI_API_KEY
121
136
  || process.env.GOOGLE_API_KEY
122
137
  || undefined;
123
- const mode = googleCfg?.mode || 'cli';
124
- const useVertex = googleCfg?.useVertex || false;
125
- const project = googleCfg?.project || process.env.GOOGLE_CLOUD_PROJECT || undefined;
126
- const location = googleCfg?.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
138
+ const mode = override?.mode || googleCfg?.mode || 'cli';
139
+ const useVertex = override?.useVertex ?? googleCfg?.useVertex ?? false;
140
+ const project = override?.project || googleCfg?.project || process.env.GOOGLE_CLOUD_PROJECT || undefined;
141
+ const location = override?.location || googleCfg?.location || process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
127
142
  return { cliPath, model, apiKey, mode, useVertex, project, location };
128
143
  }
129
144
  export function loadConfig(configPath = resolvePaths().config) {
@@ -1,14 +1,12 @@
1
1
  /**
2
2
  * Agent Plugin System
3
3
  *
4
- * Provides a lightweight plugin interface for agent integration.
4
+ * Per-EvolAgent runner instantiation: each (EvolAgent × baseagent) gets its
5
+ * own runner so that runtime state (model/effort/permissionMode/credentials)
6
+ * is fully isolated.
5
7
  */
6
8
  import { logger } from '../utils/logger.js';
7
- /**
8
- * Agent Loader
9
- *
10
- * Manages agent plugin registration and creation.
11
- */
9
+ /** Agent Loader — produces one runner per (EvolAgent × baseagent). */
12
10
  export class AgentLoader {
13
11
  plugins = new Map();
14
12
  register(plugin) {
@@ -18,20 +16,37 @@ export class AgentLoader {
18
16
  this.plugins.set(plugin.name, plugin);
19
17
  logger.debug(`Registered agent plugin: ${plugin.name}`);
20
18
  }
21
- createAll(config, callbacks) {
19
+ /**
20
+ * Iterate over all EvolAgents (DefaultAgent + each named agent in registry)
21
+ * × all registered plugins. Each successful (agent, plugin) pair yields one
22
+ * runner instance.
23
+ */
24
+ createAll(globalConfig, registry, callbacks) {
22
25
  const instances = [];
23
- for (const [name, plugin] of this.plugins) {
24
- if (!plugin.isEnabled(config)) {
25
- logger.info(`Agent '${name}' is disabled, skipping`);
26
- continue;
27
- }
28
- try {
29
- const instance = plugin.createAgent(config, callbacks);
30
- instances.push(instance);
31
- logger.info(`✓ Agent '${name}' instance created`);
32
- }
33
- catch (error) {
34
- logger.error(`✗ Failed to create agent '${name}':`, error);
26
+ const allAgents = [];
27
+ const def = registry.get('[default]');
28
+ if (def)
29
+ allAgents.push(def);
30
+ for (const a of registry.runnableAgents())
31
+ allAgents.push(a);
32
+ for (const agent of allAgents) {
33
+ for (const [pluginName, plugin] of this.plugins) {
34
+ if (!plugin.isEnabled(globalConfig, agent)) {
35
+ logger.debug(`Plugin '${pluginName}' disabled for agent '${agent.name}', skipping`);
36
+ continue;
37
+ }
38
+ try {
39
+ const instance = plugin.createAgent(globalConfig, agent, callbacks);
40
+ if (!instance) {
41
+ logger.debug(`Plugin '${pluginName}' returned null for agent '${agent.name}', skipping`);
42
+ continue;
43
+ }
44
+ instances.push(instance);
45
+ logger.info(`✓ Runner created: agent=${instance.evolagentName} baseagent=${instance.baseagent}`);
46
+ }
47
+ catch (error) {
48
+ logger.error(`✗ Failed to create runner for agent='${agent.name}' baseagent='${pluginName}':`, error);
49
+ }
35
50
  }
36
51
  }
37
52
  return instances;
@@ -104,7 +104,7 @@ function formatIdleTime(ms) {
104
104
  return '刚刚';
105
105
  }
106
106
  // 支持的命令列表
107
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode', '/ask'];
107
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/aid', '/agentmd', '/chatmode', '/ask', '/resume'];
108
108
  // 命令别名映射
109
109
  const aliases = {
110
110
  '/p': '/project',
@@ -113,7 +113,7 @@ const aliases = {
113
113
  '/rw': '/rewind'
114
114
  };
115
115
  // 命令快速路径前缀(所有命令都不进入消息队列)
116
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/agentmd', '/ask'];
116
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check', '/p ', '/s ', '/name', '/rewind', '/rw', '/rw ', '/activity', '/chatmode', '/aid', '/agentmd', '/ask', '/resume'];
117
117
  export class CommandHandler {
118
118
  sessionManager;
119
119
  config;
@@ -131,11 +131,38 @@ export class CommandHandler {
131
131
  agentMap;
132
132
  defaultAgentId;
133
133
  agentRegistry;
134
- /** 按 agentId 获取 agent,回退到默认 */
135
- getAgent(agentId) {
136
- if (agentId && this.agentMap.has(agentId))
137
- return this.agentMap.get(agentId);
138
- return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
134
+ /**
135
+ * Get the runner for a (channel, baseagent) pair.
136
+ *
137
+ * Resolves the owning EvolAgent via the registry; falls back to default key.
138
+ * `baseagent` typically comes from `session.agentId` (e.g. 'claude').
139
+ */
140
+ getAgent(channel, baseagent) {
141
+ if (channel && baseagent) {
142
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
143
+ const key = `${evolName}::${baseagent}`;
144
+ if (this.agentMap.has(key))
145
+ return this.agentMap.get(key);
146
+ }
147
+ if (this.agentMap.has(this.defaultAgentId))
148
+ return this.agentMap.get(this.defaultAgentId);
149
+ return this.agentMap.values().next().value;
150
+ }
151
+ /** Return the list of baseagents available to a given channel (per-EvolAgent isolation). */
152
+ getAvailableBaseagents(channel) {
153
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
154
+ const prefix = `${evolName}::`;
155
+ const result = [];
156
+ for (const key of this.agentMap.keys()) {
157
+ if (key.startsWith(prefix))
158
+ result.push(key.slice(prefix.length));
159
+ }
160
+ return result;
161
+ }
162
+ /** Extract the baseagent component from `defaultAgentId` (e.g. `[default]::claude` → `claude`). */
163
+ parseDefaultBaseagent() {
164
+ const idx = this.defaultAgentId.indexOf('::');
165
+ return idx >= 0 ? this.defaultAgentId.slice(idx + 2) : this.defaultAgentId;
139
166
  }
140
167
  constructor(sessionManager, agentRunnerOrMap, config, messageCache, eventBus, defaultAgentId) {
141
168
  this.sessionManager = sessionManager;
@@ -144,11 +171,12 @@ export class CommandHandler {
144
171
  this.eventBus = eventBus;
145
172
  if (agentRunnerOrMap instanceof Map) {
146
173
  this.agentMap = agentRunnerOrMap;
147
- this.defaultAgentId = defaultAgentId || 'claude';
174
+ this.defaultAgentId = defaultAgentId || '[default]::claude';
148
175
  }
149
176
  else {
150
- this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
151
- this.defaultAgentId = agentRunnerOrMap.name;
177
+ // Backward-compat single-runner path: treat as DefaultAgent's claude.
178
+ this.agentMap = new Map([[`[default]::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
179
+ this.defaultAgentId = `[default]::${agentRunnerOrMap.name}`;
152
180
  }
153
181
  }
154
182
  /** 注入 EvolAgentRegistry,用于判断通道是否被 EvolAgent 管理 */
@@ -685,10 +713,10 @@ export class CommandHandler {
685
713
  return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
686
714
  }
687
715
  if (cmd === '/agent') {
688
- return [...this.agentMap.keys()].map(name => ({ value: name, label: name }));
716
+ return this.getAvailableBaseagents(channel).map(name => ({ value: name, label: name }));
689
717
  }
690
718
  if (cmd === '/model') {
691
- const agent = this.getAgent(session?.agentId);
719
+ const agent = this.getAgent(channel, session?.agentId);
692
720
  if (hasModelSwitcher(agent) && agent.listModels) {
693
721
  const models = await agent.listModels() ?? [];
694
722
  if (models.length > 0)
@@ -737,7 +765,7 @@ export class CommandHandler {
737
765
  const identity = this.sessionManager.resolveIdentity(channel, userId);
738
766
  if (identity.role !== 'owner')
739
767
  return { error: '无权限' };
740
- const permAgent = this.getAgent(session.agentId);
768
+ const permAgent = this.getAgent(channel, session.agentId);
741
769
  const validModes = hasPermissionController(permAgent)
742
770
  ? permAgent.listModes().filter(m => m.available).map(m => m.key)
743
771
  : ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
@@ -779,7 +807,7 @@ export class CommandHandler {
779
807
  const policy = this.getPolicy(channel);
780
808
  // 按当前会话选择 agent 后端
781
809
  const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
782
- const agent = this.getAgent(activeSession?.agentId);
810
+ const agent = this.getAgent(channel, activeSession?.agentId);
783
811
  // 规范化命令(将别名转换为完整命令)
784
812
  let normalizedContent = content;
785
813
  for (const [alias, full] of Object.entries(aliases)) {
@@ -821,8 +849,9 @@ export class CommandHandler {
821
849
  if (normalizedContent.startsWith('/')) {
822
850
  // guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
823
851
  const guestGroupCommands = [
824
- '/status', '/help', '/check', '/chatmode',
825
- '/model', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
852
+ '/status', '/help', '/evolhelp', '/check', '/chatmode',
853
+ '/model', '/setmodel', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
854
+ '/resume',
826
855
  ];
827
856
  const userCommands = activeChatType === 'group' && !isAdmin
828
857
  ? guestGroupCommands
@@ -853,7 +882,7 @@ export class CommandHandler {
853
882
  // 话题中:检查话题 session 是否在处理(不创建)
854
883
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
855
884
  if (threadSession) {
856
- const threadAgent = this.getAgent(threadSession.agentId);
885
+ const threadAgent = this.getAgent(channel, threadSession.agentId);
857
886
  if (threadAgent.hasActiveStream(threadSession.id)) {
858
887
  return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
859
888
  }
@@ -961,6 +990,64 @@ export class CommandHandler {
961
990
  ];
962
991
  return lines.join('\n');
963
992
  }
993
+ // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
994
+ if (normalizedContent === '/evolhelp') {
995
+ const cmds = [];
996
+ // 项目管理
997
+ cmds.push({ command: '/pwd', description: '显示当前项目路径', category: '项目管理', roles: ['admin', 'owner'] });
998
+ cmds.push({ command: '/p', aliases: ['/project', '/plist'], args: '[name|path]', description: '列出或切换项目', category: '项目管理', roles: ['admin', 'owner'] });
999
+ if (isOwner) {
1000
+ cmds.push({ command: '/bind', args: '<path>', description: '绑定新项目目录', category: '项目管理', roles: ['owner'] });
1001
+ }
1002
+ // 会话管理
1003
+ cmds.push({ command: '/new', args: '[名称]', description: '创建新会话(清空历史请用此命令,可选命名)', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
1004
+ cmds.push({ command: '/s', aliases: ['/session', '/slist'], args: '[cli|名称|序号|uuid]', description: '列出或切换会话', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
1005
+ cmds.push({ command: '/name', aliases: ['/rename'], args: '<新名称>', description: '重命名当前会话', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
1006
+ cmds.push({ command: '/del', args: '<名称>', description: '删除指定会话(仅解绑,不删除文件)', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
1007
+ if (isAdmin) {
1008
+ cmds.push({ command: '/fork', args: '[名称]', description: '分支当前会话(从当前对话点创建分支)', category: '会话管理', roles: ['admin', 'owner'] });
1009
+ cmds.push({ command: '/rewind', aliases: ['/rw'], args: '[N] [chat|file|all]', description: '查看历史/撤销指定轮次', category: '会话管理', roles: ['admin', 'owner'] });
1010
+ cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
1011
+ }
1012
+ // Agent 与模型
1013
+ if (isAdmin) {
1014
+ cmds.push({ command: '/agent', args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1015
+ cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1016
+ cmds.push({ command: '/setmodel', description: '返回 JSON 格式的模型列表(供程序解析)', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1017
+ cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
1018
+ }
1019
+ // 权限管理
1020
+ if (isAdmin) {
1021
+ cmds.push({ command: '/perm', args: isOwner ? '<auto|bypass|request|edit|plan|noask>' : undefined, description: '查看当前权限模式', category: '权限管理', roles: ['admin', 'owner'] });
1022
+ cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
1023
+ }
1024
+ // 运维
1025
+ cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
1026
+ cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
1027
+ cmds.push({ command: '/check', description: '检查渠道状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
1028
+ if (isAdmin) {
1029
+ cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '运维', roles: ['admin', 'owner'] });
1030
+ cmds.push({ command: '/restart', args: '<channel>', description: '重连指定渠道', category: '运维', roles: ['admin', 'owner'] });
1031
+ }
1032
+ if (isOwner) {
1033
+ cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['owner'] });
1034
+ cmds.push({ command: '/file', args: '[channel] <path>', description: '发送项目内文件', category: '运维', roles: ['owner'] });
1035
+ cmds.push({ command: '/aid', args: '[list|new <aid>]', description: 'AID 管理', category: '运维', roles: ['owner'] });
1036
+ cmds.push({ command: '/agentmd', args: '[put|set <内容>]', description: '管理 agent.md', category: '运维', roles: ['owner'] });
1037
+ }
1038
+ // 会话模式
1039
+ if (isAdmin) {
1040
+ cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '会话管理', roles: ['admin', 'owner'] });
1041
+ }
1042
+ // 交互
1043
+ cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['guest', 'admin', 'owner'] });
1044
+ cmds.push({ command: '/resume', description: '查看当前项目的 Claude 会话记录', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
1045
+ // 帮助
1046
+ cmds.push({ command: '/help', description: '显示帮助信息', category: '帮助', roles: ['guest', 'admin', 'owner'] });
1047
+ cmds.push({ command: '/evolhelp', description: '返回 JSON 格式命令列表', category: '帮助', roles: ['guest', 'admin', 'owner'] });
1048
+ const categories = [...new Set(cmds.map(c => c.category))];
1049
+ return JSON.stringify({ commands: cmds, categories });
1050
+ }
964
1051
  // /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
965
1052
  if (normalizedContent.startsWith('/perm')) {
966
1053
  const args = normalizedContent.slice(5).trim();
@@ -969,7 +1056,7 @@ export class CommandHandler {
969
1056
  if ('error' in permResult)
970
1057
  return permResult.error;
971
1058
  const { session: permSession } = permResult;
972
- const permAgent = this.getAgent(permSession.agentId);
1059
+ const permAgent = this.getAgent(channel, permSession.agentId);
973
1060
  // /perm(无参数):显示当前模式和可选模式
974
1061
  if (!args) {
975
1062
  if (!hasPermissionController(permAgent)) {
@@ -1108,6 +1195,89 @@ export class CommandHandler {
1108
1195
  this.interactionRouter.handle({ type: 'interaction.response', id: targetId, action: args, operatorId: userId });
1109
1196
  return `✓ 已回答`;
1110
1197
  }
1198
+ // /resume 命令:返回当前项目的 Claude 会话记录(JSON)
1199
+ if (normalizedContent === '/resume' || normalizedContent.startsWith('/resume ')) {
1200
+ const resumeResult = await this.ensureSession(channel, channelId, threadId);
1201
+ if ('error' in resumeResult)
1202
+ return resumeResult.error;
1203
+ const { session: resumeSession } = resumeResult;
1204
+ try {
1205
+ const { encodePath } = await import('../utils/cross-platform.js');
1206
+ const homeDir = os.homedir();
1207
+ const encodedPath = encodePath(resumeSession.projectPath);
1208
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
1209
+ if (!fs.existsSync(projectDir)) {
1210
+ return '❌ 未找到 Claude 会话记录目录';
1211
+ }
1212
+ const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
1213
+ if (jsonlFiles.length === 0) {
1214
+ return '❌ 当前项目没有 Claude 会话记录';
1215
+ }
1216
+ const sessions = [];
1217
+ for (const file of jsonlFiles) {
1218
+ const filePath = path.join(projectDir, file);
1219
+ const sessionId = file.replace('.jsonl', '');
1220
+ let lastTimestamp = '';
1221
+ let firstUserMessage = '';
1222
+ let model = '';
1223
+ let branch = '';
1224
+ let turns = 0;
1225
+ try {
1226
+ const content = fs.readFileSync(filePath, 'utf-8');
1227
+ const lines = content.split('\n').filter(l => l.trim());
1228
+ for (const line of lines) {
1229
+ const event = JSON.parse(line);
1230
+ if (event.timestamp && event.timestamp > lastTimestamp) {
1231
+ lastTimestamp = event.timestamp;
1232
+ }
1233
+ if (event.gitBranch && !branch) {
1234
+ branch = event.gitBranch;
1235
+ }
1236
+ if (event.type === 'user' && event.message?.role === 'user') {
1237
+ const msgContent = event.message.content;
1238
+ const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
1239
+ if (!isToolResult) {
1240
+ turns++;
1241
+ if (!firstUserMessage) {
1242
+ if (typeof msgContent === 'string') {
1243
+ firstUserMessage = msgContent.slice(0, 100);
1244
+ }
1245
+ else if (Array.isArray(msgContent)) {
1246
+ const textBlock = msgContent.find((c) => c.type === 'text');
1247
+ if (textBlock?.text) {
1248
+ firstUserMessage = textBlock.text.slice(0, 100);
1249
+ }
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ if (event.type === 'assistant' && event.message?.model && !model) {
1255
+ model = event.message.model;
1256
+ }
1257
+ }
1258
+ }
1259
+ catch {
1260
+ continue;
1261
+ }
1262
+ if (!lastTimestamp)
1263
+ continue;
1264
+ sessions.push({
1265
+ sessionId,
1266
+ lastMessageTime: lastTimestamp,
1267
+ firstUserMessage: firstUserMessage || '(无消息)',
1268
+ model: model || 'unknown',
1269
+ turns,
1270
+ branch: branch || 'unknown',
1271
+ });
1272
+ }
1273
+ sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
1274
+ return JSON.stringify(sessions, null, 2);
1275
+ }
1276
+ catch (error) {
1277
+ logger.error('[CommandHandler] /resume failed:', error);
1278
+ return `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}`;
1279
+ }
1280
+ }
1111
1281
  // /agent 命令:查看或切换 Agent 后端
1112
1282
  if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
1113
1283
  const args = normalizedContent.slice(6).trim();
@@ -1115,9 +1285,12 @@ export class CommandHandler {
1115
1285
  if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
1116
1286
  return '❌ 无权限:此命令仅限管理员使用';
1117
1287
  }
1118
- const available = [...this.agentMap.keys()];
1288
+ const available = this.getAvailableBaseagents(channel);
1119
1289
  if (!args) {
1120
- const currentAgent = activeSession?.agentId || this.defaultAgentId;
1290
+ // currentAgent: 当前 session baseagent,或该 channel 所属 evolagent 的 baseagent
1291
+ const currentAgent = activeSession?.agentId
1292
+ || this.agentRegistry?.resolveByChannel(channel)?.baseagent
1293
+ || this.parseDefaultBaseagent();
1121
1294
  // 尝试发送交互卡片
1122
1295
  if (this.interactionRouter && available.length > 1) {
1123
1296
  const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
@@ -1163,7 +1336,7 @@ export class CommandHandler {
1163
1336
  }
1164
1337
  return `当前 Agent: ${currentAgent}`;
1165
1338
  }
1166
- if (!this.agentMap.has(args)) {
1339
+ if (!available.includes(args)) {
1167
1340
  return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
1168
1341
  }
1169
1342
  const result = await this.ensureSession(channel, channelId, threadId);
@@ -1184,6 +1357,74 @@ export class CommandHandler {
1184
1357
  let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
1185
1358
  return agentSwitchResponse;
1186
1359
  }
1360
+ // /setmodel 命令:返回 JSON 格式的模型列表(供程序解析)
1361
+ if (normalizedContent === '/setmodel' || normalizedContent.startsWith('/setmodel ')) {
1362
+ const setmodelResult = await this.ensureSession(channel, channelId, threadId);
1363
+ if ('error' in setmodelResult)
1364
+ return setmodelResult.error;
1365
+ const { session: setmodelSession } = setmodelResult;
1366
+ const setmodelAgent = this.getAgent(channel, setmodelSession.agentId);
1367
+ const currentModel = hasModelSwitcher(setmodelAgent) ? setmodelAgent.getModel() : setmodelAgent.name;
1368
+ const efforts = getAvailableEfforts(setmodelAgent, currentModel);
1369
+ const currentEffort = setmodelAgent.getEffort?.() || 'auto';
1370
+ // 获取 API URL 用于请求 /models
1371
+ let apiBaseUrl;
1372
+ try {
1373
+ const configBaseUrl = this.config.agents?.claude?.baseUrl;
1374
+ const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
1375
+ if (configBaseUrl && !isPlaceholderUrl) {
1376
+ apiBaseUrl = configBaseUrl;
1377
+ }
1378
+ else if (process.env.ANTHROPIC_BASE_URL) {
1379
+ apiBaseUrl = process.env.ANTHROPIC_BASE_URL;
1380
+ }
1381
+ else {
1382
+ const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
1383
+ if (fs.existsSync(claudeSettingsPath)) {
1384
+ const claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf-8'));
1385
+ if (claudeSettings.env?.ANTHROPIC_BASE_URL) {
1386
+ apiBaseUrl = claudeSettings.env.ANTHROPIC_BASE_URL;
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+ catch { }
1392
+ let modelListData = null;
1393
+ if (apiBaseUrl) {
1394
+ try {
1395
+ const modelsUrl = apiBaseUrl.replace(/\/+$/, '') + '/v1/models';
1396
+ const controller = new AbortController();
1397
+ const timeout = setTimeout(() => controller.abort(), 5000);
1398
+ const resp = await fetch(modelsUrl, {
1399
+ signal: controller.signal,
1400
+ headers: { 'Authorization': `Bearer ${this.config.agents?.claude?.apiKey || process.env.ANTHROPIC_AUTH_TOKEN || ''}` },
1401
+ });
1402
+ clearTimeout(timeout);
1403
+ if (resp.ok) {
1404
+ modelListData = await resp.json();
1405
+ }
1406
+ }
1407
+ catch { }
1408
+ }
1409
+ // 兜底模型列表
1410
+ if (!modelListData || !modelListData.data || modelListData.data.length === 0) {
1411
+ const now = Math.floor(Date.now() / 1000);
1412
+ modelListData = {
1413
+ object: 'list',
1414
+ data: [
1415
+ { id: 'claude-opus-4-7', object: 'model', created: now, owned_by: 'anthropic' },
1416
+ { id: 'claude-opus-4-6', object: 'model', created: now, owned_by: 'anthropic' },
1417
+ { id: 'claude-sonnet-4-6', object: 'model', created: now, owned_by: 'anthropic' },
1418
+ ],
1419
+ };
1420
+ }
1421
+ return JSON.stringify({
1422
+ current_model: currentModel,
1423
+ current_effort: currentEffort,
1424
+ available_efforts: efforts,
1425
+ models: modelListData,
1426
+ }, null, 2);
1427
+ }
1187
1428
  // /model 命令:查看或切换模型/推理强度
1188
1429
  if (normalizedContent.startsWith('/model')) {
1189
1430
  const args = normalizedContent.slice(6).trim();
@@ -1192,7 +1433,7 @@ export class CommandHandler {
1192
1433
  if ('error' in modelResult)
1193
1434
  return modelResult.error;
1194
1435
  const { session: modelSession } = modelResult;
1195
- const modelAgent = this.getAgent(modelSession.agentId);
1436
+ const modelAgent = this.getAgent(channel, modelSession.agentId);
1196
1437
  const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
1197
1438
  if (!args) {
1198
1439
  const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
@@ -1326,7 +1567,7 @@ export class CommandHandler {
1326
1567
  if ('error' in effortResult)
1327
1568
  return effortResult.error;
1328
1569
  const { session: effortSession } = effortResult;
1329
- const effortAgent = this.getAgent(effortSession.agentId);
1570
+ const effortAgent = this.getAgent(channel, effortSession.agentId);
1330
1571
  const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
1331
1572
  const efforts = getAvailableEfforts(effortAgent, currentModel);
1332
1573
  const currentEffort = effortAgent.getEffort?.() || 'auto';
@@ -1649,7 +1890,7 @@ export class CommandHandler {
1649
1890
  if (threadId) {
1650
1891
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
1651
1892
  if (threadSession) {
1652
- const threadAgent = this.getAgent(threadSession.agentId);
1893
+ const threadAgent = this.getAgent(channel, threadSession.agentId);
1653
1894
  if (threadAgent.hasActiveStream(threadSession.id)) {
1654
1895
  return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
1655
1896
  }
@@ -1667,7 +1908,7 @@ export class CommandHandler {
1667
1908
  if ('error' in stopResult)
1668
1909
  return '当前没有正在处理的任务';
1669
1910
  const { session: stopSession } = stopResult;
1670
- const stopAgent = this.getAgent(stopSession.agentId);
1911
+ const stopAgent = this.getAgent(channel, stopSession.agentId);
1671
1912
  const sessionKey = stopSession.id;
1672
1913
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
1673
1914
  const hasActive = stopAgent.hasActiveStream(sessionKey);
@@ -1692,7 +1933,7 @@ export class CommandHandler {
1692
1933
  if ('error' in result)
1693
1934
  return result.error;
1694
1935
  const { session } = result;
1695
- const sessionAgent = this.getAgent(session.agentId);
1936
+ const sessionAgent = this.getAgent(channel, session.agentId);
1696
1937
  if (!sessionAgent.capabilities?.clear) {
1697
1938
  return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代`;
1698
1939
  }
@@ -1724,7 +1965,7 @@ export class CommandHandler {
1724
1965
  if ('error' in result)
1725
1966
  return result.error;
1726
1967
  const { session } = result;
1727
- const sessionAgent = this.getAgent(session.agentId);
1968
+ const sessionAgent = this.getAgent(channel, session.agentId);
1728
1969
  if (!sessionAgent.capabilities?.compact) {
1729
1970
  return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact`;
1730
1971
  }
@@ -1775,7 +2016,7 @@ export class CommandHandler {
1775
2016
  return `❌ 无法创建会话,请检查配置`;
1776
2017
  }
1777
2018
  const sessionKey = this.getQueueKey(session, channel, channelId);
1778
- const sessionAgent = this.getAgent(session.agentId);
2019
+ const sessionAgent = this.getAgent(channel, session.agentId);
1779
2020
  const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
1780
2021
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
1781
2022
  const isThread = !!session.threadId;
@@ -2776,7 +3017,7 @@ export class CommandHandler {
2776
3017
  return `❌ 删除失败`;
2777
3018
  }
2778
3019
  this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
2779
- const targetAgent = this.getAgent(targetSession.agentId);
3020
+ const targetAgent = this.getAgent(channel, targetSession.agentId);
2780
3021
  await targetAgent.closeSession(targetSession.id);
2781
3022
  return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
2782
3023
  }
@@ -2789,7 +3030,7 @@ export class CommandHandler {
2789
3030
  if (!session.agentSessionId) {
2790
3031
  return `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
2791
3032
  }
2792
- const forkAgent = this.getAgent(session.agentId);
3033
+ const forkAgent = this.getAgent(channel, session.agentId);
2793
3034
  if (!forkAgent.capabilities?.fork) {
2794
3035
  return `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代`;
2795
3036
  }
@@ -2810,7 +3051,7 @@ export class CommandHandler {
2810
3051
  if ('error' in result)
2811
3052
  return result.error;
2812
3053
  const { session } = result;
2813
- const rewindAgent = this.getAgent(session.agentId);
3054
+ const rewindAgent = this.getAgent(channel, session.agentId);
2814
3055
  if (rewindAgent.name !== 'claude') {
2815
3056
  return '❌ /rewind 仅支持 Claude 后端';
2816
3057
  }
@@ -2847,7 +3088,7 @@ export class CommandHandler {
2847
3088
  if ('error' in repairResult)
2848
3089
  return repairResult.error;
2849
3090
  const { session: repairSession } = repairResult;
2850
- const repairAgent = this.getAgent(repairSession.agentId);
3091
+ const repairAgent = this.getAgent(channel, repairSession.agentId);
2851
3092
  const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
2852
3093
  try {
2853
3094
  if (!repairSession.agentSessionId) {
@@ -105,10 +105,21 @@ export class EvolAgentRegistry {
105
105
  buildDefaultAgent(globalConfig) {
106
106
  const agents = globalConfig.agents || {};
107
107
  const defaultName = agents.defaultAgent || 'claude';
108
+ // Include ALL declared baseagents (not just defaultName) so that
109
+ // AgentLoader creates runners for each, enabling /agent switching.
110
+ const baseagentBlock = {};
111
+ const KNOWN_BASEAGENTS = ['claude', 'codex', 'gemini', 'hermes'];
112
+ for (const ba of KNOWN_BASEAGENTS) {
113
+ if (agents[ba] !== undefined)
114
+ baseagentBlock[ba] = agents[ba];
115
+ }
116
+ if (Object.keys(baseagentBlock).length === 0) {
117
+ baseagentBlock[defaultName] = agents[defaultName] || {};
118
+ }
108
119
  const cfg = {
109
120
  name: '[default]',
110
121
  enabled: true,
111
- agents: { [defaultName]: agents[defaultName] || {} },
122
+ agents: baseagentBlock,
112
123
  channels: globalConfig.channels || {},
113
124
  projects: { defaultPath: globalConfig.projects?.defaultPath || process.cwd() },
114
125
  chatmode: globalConfig.chatmode,
@@ -32,11 +32,25 @@ export class MessageProcessor {
32
32
  interactionRouter;
33
33
  messageQueue;
34
34
  skillsEnsured = false; // 全局 SKILLS.md 是否已确保
35
- /** 按 agentId 获取 agent,回退到默认 */
36
- getAgent(agentId) {
37
- if (agentId && this.agentMap.has(agentId))
38
- return this.agentMap.get(agentId);
39
- return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
35
+ /**
36
+ * Get the runner for a given (channel, baseagent) pair.
37
+ *
38
+ * - `channel` is used to look up the owning EvolAgent (via registry).
39
+ * - `baseagent` (e.g. 'claude') comes from `session.agentId`.
40
+ *
41
+ * Falls back to `defaultAgentId` (a composite key, e.g. `[default]::claude`)
42
+ * when no match is found.
43
+ */
44
+ getAgent(channel, baseagent) {
45
+ if (channel && baseagent) {
46
+ const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '[default]';
47
+ const key = `${evolName}::${baseagent}`;
48
+ if (this.agentMap.has(key))
49
+ return this.agentMap.get(key);
50
+ }
51
+ if (this.agentMap.has(this.defaultAgentId))
52
+ return this.agentMap.get(this.defaultAgentId);
53
+ return this.agentMap.values().next().value;
40
54
  }
41
55
  /** 获取可用 agent 列表 */
42
56
  getAvailableAgents() {
@@ -57,12 +71,12 @@ export class MessageProcessor {
57
71
  this.commandHandler = commandHandler;
58
72
  if (agentRunnerOrMap instanceof Map) {
59
73
  this.agentMap = agentRunnerOrMap;
60
- this.defaultAgentId = defaultAgentId || 'claude';
74
+ this.defaultAgentId = defaultAgentId || '[default]::claude';
61
75
  }
62
76
  else {
63
- // 向后兼容:单个 agentRunner
64
- this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
65
- this.defaultAgentId = agentRunnerOrMap.name;
77
+ // Backward-compat single-runner path.
78
+ this.agentMap = new Map([[`[default]::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
79
+ this.defaultAgentId = `[default]::${agentRunnerOrMap.name}`;
66
80
  }
67
81
  // 监听中断事件,标记被中断的 session
68
82
  this.eventBus.subscribe('message:interrupted', (event) => {
@@ -176,7 +190,7 @@ export class MessageProcessor {
176
190
  logger.debug(`[MessageProcessor] Agent context resolved: ${agentContext.name} (${agentContext.baseagent})`);
177
191
  }
178
192
  // 按 session.agentId 选择 agent 后端
179
- const agent = this.getAgent(session.agentId);
193
+ const agent = this.getAgent(channelKey, session.agentId);
180
194
  const monitorEnabled = this.config.idleMonitor?.enabled !== false;
181
195
  const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
182
196
  // 计算是否抑制中间输出(工具活动 + 流式文本)
@@ -302,7 +316,7 @@ export class MessageProcessor {
302
316
  return;
303
317
  }
304
318
  const { adapter, options } = channelInfo;
305
- const agent = this.getAgent(session.agentId);
319
+ const agent = this.getAgent(channelKey, session.agentId);
306
320
  const streamKey = session.id;
307
321
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
308
322
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
@@ -98,7 +98,7 @@ export class MessageQueue {
98
98
  agentName: this.processingAgent.get(queueKey),
99
99
  });
100
100
  if (this.interruptCallback) {
101
- this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
101
+ this.interruptCallback(sessionKey, this.currentAgentId, this.processingAgent.get(queueKey)).catch(() => { });
102
102
  }
103
103
  }
104
104
  else {
@@ -282,7 +282,7 @@ export class MessageQueue {
282
282
  agentName: this.processingAgent.get(this.currentSessionKey),
283
283
  });
284
284
  if (this.interruptCallback) {
285
- this.interruptCallback(sessionKey, this.currentAgentId).catch(() => { });
285
+ this.interruptCallback(sessionKey, this.currentAgentId, this.processingAgent.get(this.currentSessionKey)).catch(() => { });
286
286
  }
287
287
  return true;
288
288
  }
package/dist/index.js CHANGED
@@ -132,27 +132,28 @@ async function main() {
132
132
  sessionManager.registerFileAdapter(new ClaudeSessionFileAdapter());
133
133
  sessionManager.registerFileAdapter(new CodexSessionFileAdapter());
134
134
  sessionManager.registerFileAdapter(new GeminiSessionFileAdapter());
135
- // Agent 插件系统
135
+ // Agent 插件系统:每个 EvolAgent × 每个 baseagent 一个独立 runner(H1/H2 修复)
136
136
  const agentLoader = new AgentLoader();
137
137
  agentLoader.register(new ClaudeAgentPlugin());
138
138
  agentLoader.register(new CodexAgentPlugin());
139
139
  agentLoader.register(new GeminiAgentPlugin());
140
- const agentInstances = agentLoader.createAll(config, {
140
+ const agentInstances = agentLoader.createAll(config, agentRegistry, {
141
141
  onSessionIdUpdate: async (sessionId, agentSessionId) => {
142
142
  await sessionManager.updateAgentSessionIdBySessionId(sessionId, agentSessionId);
143
143
  },
144
144
  });
145
- // 构建 agent map,支持按 agentId 路由(当前默认使用第一个 agent)
145
+ // agentMap 复合键:${evolagentName}::${baseagent}
146
146
  const agentMap = new Map();
147
147
  for (const inst of agentInstances) {
148
- agentMap.set(inst.agent.name, inst.agent);
148
+ agentMap.set(`${inst.evolagentName}::${inst.baseagent}`, inst.agent);
149
149
  }
150
150
  const defaultAgent = config.agents?.defaultAgent || 'claude';
151
- const agentRunner = agentMap.get(defaultAgent) || agentInstances[0]?.agent;
151
+ const defaultAgentKey = `[default]::${defaultAgent}`;
152
+ const agentRunner = agentMap.get(defaultAgentKey) || agentInstances[0]?.agent;
152
153
  if (!agentRunner) {
153
- throw new Error('No agent backend available. Check agents config.');
154
+ throw new Error('No agent backend available. Check agents config (no runners created).');
154
155
  }
155
- logger.info(`✓ Agent runner ready (default: ${agentRunner.name}, available: ${[...agentMap.keys()].join(', ')})`);
156
+ logger.info(`✓ Runners ready (default key: ${defaultAgentKey}, total: ${agentMap.size}, keys: ${[...agentMap.keys()].join(', ')})`);
156
157
  // 权限审批网关
157
158
  const permissionGateway = new PermissionGateway();
158
159
  permissionGateway.setEventBus(eventBus);
@@ -222,7 +223,7 @@ async function main() {
222
223
  // 启动迁移:将 sessions.channel 从 channelType 回填为实例名
223
224
  sessionManager.migrateChannelToInstanceName();
224
225
  // 创建命令处理器
225
- const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgent);
226
+ const cmdHandler = new CommandHandler(sessionManager, agentMap, config, messageCache, eventBus, defaultAgentKey);
226
227
  cmdHandler.setPermissionGateway(permissionGateway);
227
228
  cmdHandler.setInteractionRouter(interactionRouter);
228
229
  cmdHandler.setStatsCollector(statsCollector);
@@ -237,7 +238,7 @@ async function main() {
237
238
  }
238
239
  };
239
240
  return cmdHandler.handle(content, channel, channelId, sendFn, userId, threadId);
240
- }, defaultAgent);
241
+ }, defaultAgentKey);
241
242
  // 回填 processor 和 messageQueue 的引用
242
243
  cmdHandler.setProcessor(processor);
243
244
  // Inject EvolAgentRegistry (methods added by T6/T7)
@@ -260,8 +261,11 @@ async function main() {
260
261
  await processor.processMessage(message);
261
262
  });
262
263
  // 设置中断回调(精确中断正在处理的 agent)
263
- messageQueue.setInterruptCallback(async (sessionKey, agentId) => {
264
- const agent = agentMap.get(agentId || defaultAgent);
264
+ messageQueue.setInterruptCallback(async (sessionKey, agentId, evolagentName) => {
265
+ const baseagent = agentId || defaultAgent;
266
+ const evol = evolagentName || '[default]';
267
+ const agent = agentMap.get(`${evol}::${baseagent}`)
268
+ || agentMap.get(defaultAgentKey);
265
269
  if (agent?.hasActiveStream(sessionKey)) {
266
270
  await agent.interrupt(sessionKey);
267
271
  }
@@ -522,12 +526,16 @@ async function main() {
522
526
  sessionManager.clearProcessing(session.id);
523
527
  continue;
524
528
  }
525
- const agent = agentMap.get(session.agentId) || agentMap.get(defaultAgent);
529
+ // 复合键:${evolagentName}::${baseagent},从 channel 反查 evolagent
530
+ const owningAgent = agentRegistry.resolveByChannel(session.channel);
531
+ const evolName = owningAgent?.name || '[default]';
532
+ const baseagentName = session.agentId || defaultAgent;
533
+ const agent = agentMap.get(`${evolName}::${baseagentName}`) || agentMap.get(defaultAgentKey);
526
534
  if (!agent) {
527
535
  sessionManager.clearProcessing(session.id);
528
536
  continue;
529
537
  }
530
- logger.info(`[Resume] Resuming session: ${session.id} (agent: ${session.agentId})`);
538
+ logger.info(`[Resume] Resuming session: ${session.id} (agent: ${evolName}::${baseagentName})`);
531
539
  const resumeMessage = {
532
540
  channel: session.channel,
533
541
  channelId: session.channelId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.8.2",
3
+ "version": "2.8.3",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",