evolclaw 2.8.1 → 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.
@@ -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;
@@ -130,11 +130,39 @@ export class CommandHandler {
130
130
  statsCollector;
131
131
  agentMap;
132
132
  defaultAgentId;
133
- /** 按 agentId 获取 agent,回退到默认 */
134
- getAgent(agentId) {
135
- if (agentId && this.agentMap.has(agentId))
136
- return this.agentMap.get(agentId);
137
- return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
133
+ agentRegistry;
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;
138
166
  }
139
167
  constructor(sessionManager, agentRunnerOrMap, config, messageCache, eventBus, defaultAgentId) {
140
168
  this.sessionManager = sessionManager;
@@ -143,12 +171,200 @@ export class CommandHandler {
143
171
  this.eventBus = eventBus;
144
172
  if (agentRunnerOrMap instanceof Map) {
145
173
  this.agentMap = agentRunnerOrMap;
146
- this.defaultAgentId = defaultAgentId || 'claude';
174
+ this.defaultAgentId = defaultAgentId || '[default]::claude';
147
175
  }
148
176
  else {
149
- this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
150
- 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}`;
180
+ }
181
+ }
182
+ /** 注入 EvolAgentRegistry,用于判断通道是否被 EvolAgent 管理 */
183
+ setAgentRegistry(registry) {
184
+ this.agentRegistry = registry;
185
+ }
186
+ /** 返回管理当前通道的 EvolAgent(非 default),无则返回 null */
187
+ getOwningAgent(channel) {
188
+ if (!this.agentRegistry)
189
+ return null;
190
+ const agent = this.agentRegistry.resolveByChannel(channel);
191
+ if (!agent || agent.isDefault)
192
+ return null;
193
+ return agent;
194
+ }
195
+ /** 返回当前通道的有效项目路径:agent-owned 用 agent.projectPath;否则用全局 defaultPath。*/
196
+ getEffectiveDefaultPath(channel) {
197
+ const owning = this.getOwningAgent(channel);
198
+ if (owning)
199
+ return owning.projectPath;
200
+ return this.config.projects?.defaultPath || process.cwd();
201
+ }
202
+ /**
203
+ * 返回当前通道有效的 projects.list(agent-owned 用 agent.json 的;否则全局 evolclaw.json 的)。
204
+ * 都没配 list 时回退到 defaultPath 单项目。
205
+ */
206
+ getEffectiveProjects(channel) {
207
+ const owning = this.getOwningAgent(channel);
208
+ if (owning) {
209
+ return owning.getProjects();
210
+ }
211
+ return this.projects;
212
+ }
213
+ /**
214
+ * 添加项目到当前通道范围(agent-owned 写 agent.json;default 写 evolclaw.json)。
215
+ */
216
+ async addProjectInScope(channel, name, projectPath) {
217
+ const owning = this.getOwningAgent(channel);
218
+ if (owning) {
219
+ try {
220
+ owning.addProject(name, projectPath);
221
+ }
222
+ catch (e) {
223
+ return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
224
+ }
225
+ return undefined;
226
+ }
227
+ if (!this.config.projects) {
228
+ this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
229
+ }
230
+ if (!this.config.projects.list) {
231
+ this.config.projects.list = {};
232
+ }
233
+ this.config.projects.list[name] = projectPath;
234
+ try {
235
+ const { saveConfig } = await import('../config.js');
236
+ saveConfig(this.config);
237
+ }
238
+ catch (e) {
239
+ return `⚠️ 写入 evolclaw.json 失败: ${e?.message || e}`;
240
+ }
241
+ // Refresh in-memory list cache (this.projects getter reads from this.config)
242
+ return undefined;
243
+ }
244
+ /**
245
+ * 持久化 baseagent.model:agent-owned 写到 agent.json;否则写 evolclaw.json 或 ~/.claude/settings.json。
246
+ * 返回错误信息或 undefined。
247
+ */
248
+ persistBaseagentModel(channel, baseagentName, newModel) {
249
+ const owning = this.getOwningAgent(channel);
250
+ if (owning) {
251
+ try {
252
+ owning.setBaseagentModel(newModel);
253
+ }
254
+ catch (e) {
255
+ return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
256
+ }
257
+ return undefined;
258
+ }
259
+ // DefaultAgent / 无 owning agent:保留原"就近原则"
260
+ if (!this.config.agents)
261
+ this.config.agents = {};
262
+ const isCodex = baseagentName === 'codex';
263
+ if (isCodex) {
264
+ if (!this.config.agents.codex)
265
+ this.config.agents.codex = {};
266
+ if (newModel)
267
+ this.config.agents.codex.model = newModel;
268
+ try {
269
+ saveConfig(this.config);
270
+ }
271
+ catch (e) {
272
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
273
+ }
274
+ return undefined;
275
+ }
276
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
277
+ if (configuredInEvolclaw) {
278
+ if (!this.config.agents.claude)
279
+ this.config.agents.claude = {};
280
+ if (newModel)
281
+ this.config.agents.claude.model = newModel;
282
+ try {
283
+ saveConfig(this.config);
284
+ }
285
+ catch (e) {
286
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
287
+ }
288
+ return undefined;
289
+ }
290
+ // Fallback: ~/.claude/settings.json
291
+ const updates = {};
292
+ if (newModel)
293
+ updates.model = newModel;
294
+ const writeResult = writeUserSettings(updates);
295
+ if (!writeResult.success) {
296
+ return `⚠️ 写入用户配置失败: ${writeResult.error}`;
297
+ }
298
+ return undefined;
299
+ }
300
+ /**
301
+ * 持久化 baseagent.effort:agent-owned 写到 agent.json;否则就近原则。
302
+ */
303
+ persistBaseagentEffort(channel, baseagentName, newEffort) {
304
+ const owning = this.getOwningAgent(channel);
305
+ if (owning) {
306
+ try {
307
+ owning.setBaseagentEffort(newEffort);
308
+ }
309
+ catch (e) {
310
+ return `⚠️ 写入 agent.json 失败: ${e?.message || e}`;
311
+ }
312
+ return undefined;
313
+ }
314
+ if (!this.config.agents)
315
+ this.config.agents = {};
316
+ const isCodex = baseagentName === 'codex';
317
+ if (isCodex) {
318
+ if (newEffort === undefined) {
319
+ if (this.config.agents.codex?.reasoning) {
320
+ delete this.config.agents.codex.reasoning;
321
+ try {
322
+ saveConfig(this.config);
323
+ }
324
+ catch { }
325
+ }
326
+ }
327
+ else {
328
+ if (!this.config.agents.codex)
329
+ this.config.agents.codex = {};
330
+ this.config.agents.codex.reasoning = newEffort;
331
+ try {
332
+ saveConfig(this.config);
333
+ }
334
+ catch (e) {
335
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
336
+ }
337
+ }
338
+ return undefined;
339
+ }
340
+ const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
341
+ if (configuredInEvolclaw) {
342
+ if (newEffort === undefined) {
343
+ delete this.config.agents.claude.effort;
344
+ try {
345
+ saveConfig(this.config);
346
+ }
347
+ catch { }
348
+ }
349
+ else {
350
+ if (!this.config.agents.claude)
351
+ this.config.agents.claude = {};
352
+ this.config.agents.claude.effort = newEffort;
353
+ try {
354
+ saveConfig(this.config);
355
+ }
356
+ catch (e) {
357
+ return `⚠️ 写入 evolclaw.json 失败: ${e.message}`;
358
+ }
359
+ }
360
+ return undefined;
151
361
  }
362
+ const updates = { effortLevel: newEffort ?? null };
363
+ const writeResult = writeUserSettings(updates);
364
+ if (!writeResult.success) {
365
+ return `⚠️ 写入用户配置失败: ${writeResult.error}`;
366
+ }
367
+ return undefined;
152
368
  }
153
369
  /** 项目列表快捷访问(list 缺失时用 defaultPath 作为唯一项目) */
154
370
  get projects() {
@@ -491,14 +707,16 @@ export class CommandHandler {
491
707
  return items;
492
708
  }
493
709
  if (cmd === '/p') {
494
- const list = this.config.projects?.list || {};
710
+ // Use agent-scoped project list: agent-owned channels see their agent.json's
711
+ // projects.list; default channel sees evolclaw.json's projects.list
712
+ const list = this.getEffectiveProjects(channel);
495
713
  return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
496
714
  }
497
715
  if (cmd === '/agent') {
498
- return [...this.agentMap.keys()].map(name => ({ value: name, label: name }));
716
+ return this.getAvailableBaseagents(channel).map(name => ({ value: name, label: name }));
499
717
  }
500
718
  if (cmd === '/model') {
501
- const agent = this.getAgent(session?.agentId);
719
+ const agent = this.getAgent(channel, session?.agentId);
502
720
  if (hasModelSwitcher(agent) && agent.listModels) {
503
721
  const models = await agent.listModels() ?? [];
504
722
  if (models.length > 0)
@@ -507,8 +725,19 @@ export class CommandHandler {
507
725
  return null;
508
726
  }
509
727
  if (cmd === '/restart') {
728
+ // /restart 是服务级操作(重连/重启进程),仅限 default 通道。
729
+ // EvolAgent 通道返回空菜单(用户在 agent-owned 通道上无可选项)
730
+ if (this.getOwningAgent(channel))
731
+ return [];
510
732
  const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
511
- const channels = [...this.adapters.keys()].map(name => ({ value: name, label: name, desc: '重连此渠道' }));
733
+ // 列出所有 channel type
734
+ const visibleTypes = new Set();
735
+ for (const [name] of this.adapters) {
736
+ const t = this.channelTypeMap.get(name);
737
+ if (t)
738
+ visibleTypes.add(t);
739
+ }
740
+ const channels = [...visibleTypes].map(type => ({ value: type, label: type, desc: '重连此类型所有渠道实例' }));
512
741
  if (isOwner)
513
742
  channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' });
514
743
  return channels;
@@ -536,7 +765,7 @@ export class CommandHandler {
536
765
  const identity = this.sessionManager.resolveIdentity(channel, userId);
537
766
  if (identity.role !== 'owner')
538
767
  return { error: '无权限' };
539
- const permAgent = this.getAgent(session.agentId);
768
+ const permAgent = this.getAgent(channel, session.agentId);
540
769
  const validModes = hasPermissionController(permAgent)
541
770
  ? permAgent.listModes().filter(m => m.available).map(m => m.key)
542
771
  : ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
@@ -578,7 +807,7 @@ export class CommandHandler {
578
807
  const policy = this.getPolicy(channel);
579
808
  // 按当前会话选择 agent 后端
580
809
  const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
581
- const agent = this.getAgent(activeSession?.agentId);
810
+ const agent = this.getAgent(channel, activeSession?.agentId);
582
811
  // 规范化命令(将别名转换为完整命令)
583
812
  let normalizedContent = content;
584
813
  for (const [alias, full] of Object.entries(aliases)) {
@@ -599,6 +828,20 @@ export class CommandHandler {
599
828
  return '⚠️ 话题中不支持此命令';
600
829
  }
601
830
  }
831
+ // Agent-owned 通道:禁止项目切换和 agent 切换
832
+ const owningAgent = this.getOwningAgent(channel);
833
+ if (owningAgent) {
834
+ const isProjectCmd = normalizedContent === '/project' || normalizedContent.startsWith('/project ') ||
835
+ normalizedContent === '/bind' || normalizedContent.startsWith('/bind ') ||
836
+ normalizedContent === '/plist' ||
837
+ normalizedContent === '/p' || normalizedContent.startsWith('/p ');
838
+ if (isProjectCmd) {
839
+ return `❌ 当前通道由 agent [${owningAgent.name}] 管理,项目已锁定为 ${owningAgent.projectPath}`;
840
+ }
841
+ if (normalizedContent.startsWith('/agent ')) {
842
+ return `❌ 当前通道由 agent [${owningAgent.name}] 管理,baseagent 已锁定为 ${owningAgent.baseagent}`;
843
+ }
844
+ }
602
845
  // 权限检查:区分用户级命令和管理级命令
603
846
  const isOwner = identity.role === 'owner';
604
847
  const isAdmin = identity.role === 'owner' || identity.role === 'admin';
@@ -606,8 +849,9 @@ export class CommandHandler {
606
849
  if (normalizedContent.startsWith('/')) {
607
850
  // guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
608
851
  const guestGroupCommands = [
609
- '/status', '/help', '/check', '/chatmode',
610
- '/model', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
852
+ '/status', '/help', '/evolhelp', '/check', '/chatmode',
853
+ '/model', '/setmodel', '/effort', '/agent', '/perm', '/activity', '/safe', '/stop',
854
+ '/resume',
611
855
  ];
612
856
  const userCommands = activeChatType === 'group' && !isAdmin
613
857
  ? guestGroupCommands
@@ -638,7 +882,7 @@ export class CommandHandler {
638
882
  // 话题中:检查话题 session 是否在处理(不创建)
639
883
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
640
884
  if (threadSession) {
641
- const threadAgent = this.getAgent(threadSession.agentId);
885
+ const threadAgent = this.getAgent(channel, threadSession.agentId);
642
886
  if (threadAgent.hasActiveStream(threadSession.id)) {
643
887
  return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
644
888
  }
@@ -732,7 +976,7 @@ export class CommandHandler {
732
976
  ' /check - 检查渠道状态',
733
977
  ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
734
978
  ...(isAdmin ? [
735
- ' /restart <channel> - 重连指定渠道',
979
+ ' /restart <type> - 重连该类型所有渠道实例(服务级,admin+)',
736
980
  ] : []),
737
981
  ...(isOwner ? [
738
982
  ' /restart - 重启服务',
@@ -746,6 +990,64 @@ export class CommandHandler {
746
990
  ];
747
991
  return lines.join('\n');
748
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
+ }
749
1051
  // /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
750
1052
  if (normalizedContent.startsWith('/perm')) {
751
1053
  const args = normalizedContent.slice(5).trim();
@@ -754,7 +1056,7 @@ export class CommandHandler {
754
1056
  if ('error' in permResult)
755
1057
  return permResult.error;
756
1058
  const { session: permSession } = permResult;
757
- const permAgent = this.getAgent(permSession.agentId);
1059
+ const permAgent = this.getAgent(channel, permSession.agentId);
758
1060
  // /perm(无参数):显示当前模式和可选模式
759
1061
  if (!args) {
760
1062
  if (!hasPermissionController(permAgent)) {
@@ -893,6 +1195,89 @@ export class CommandHandler {
893
1195
  this.interactionRouter.handle({ type: 'interaction.response', id: targetId, action: args, operatorId: userId });
894
1196
  return `✓ 已回答`;
895
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
+ }
896
1281
  // /agent 命令:查看或切换 Agent 后端
897
1282
  if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
898
1283
  const args = normalizedContent.slice(6).trim();
@@ -900,9 +1285,12 @@ export class CommandHandler {
900
1285
  if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
901
1286
  return '❌ 无权限:此命令仅限管理员使用';
902
1287
  }
903
- const available = [...this.agentMap.keys()];
1288
+ const available = this.getAvailableBaseagents(channel);
904
1289
  if (!args) {
905
- 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();
906
1294
  // 尝试发送交互卡片
907
1295
  if (this.interactionRouter && available.length > 1) {
908
1296
  const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
@@ -948,7 +1336,7 @@ export class CommandHandler {
948
1336
  }
949
1337
  return `当前 Agent: ${currentAgent}`;
950
1338
  }
951
- if (!this.agentMap.has(args)) {
1339
+ if (!available.includes(args)) {
952
1340
  return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
953
1341
  }
954
1342
  const result = await this.ensureSession(channel, channelId, threadId);
@@ -969,6 +1357,74 @@ export class CommandHandler {
969
1357
  let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
970
1358
  return agentSwitchResponse;
971
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
+ }
972
1428
  // /model 命令:查看或切换模型/推理强度
973
1429
  if (normalizedContent.startsWith('/model')) {
974
1430
  const args = normalizedContent.slice(6).trim();
@@ -977,7 +1433,7 @@ export class CommandHandler {
977
1433
  if ('error' in modelResult)
978
1434
  return modelResult.error;
979
1435
  const { session: modelSession } = modelResult;
980
- const modelAgent = this.getAgent(modelSession.agentId);
1436
+ const modelAgent = this.getAgent(channel, modelSession.agentId);
981
1437
  const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
982
1438
  if (!args) {
983
1439
  const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
@@ -1091,68 +1547,16 @@ export class CommandHandler {
1091
1547
  modelAgent.setEffort?.(newEffort);
1092
1548
  changes.push(`推理强度: ${newEffort}`);
1093
1549
  }
1094
- // 持久化:写回来源(就近原则)
1095
- // evolclaw.json 配了 → 写 evolclaw.json
1096
- // evolclaw.json 没配 agent 全局配置
1097
- if (isCodexAgent) {
1098
- const configuredInEvolclaw = !!(this.config.agents?.codex?.model || this.config.agents?.codex?.reasoning);
1099
- if (configuredInEvolclaw) {
1100
- if (!this.config.agents.codex)
1101
- this.config.agents.codex = {};
1102
- if (newModel)
1103
- this.config.agents.codex.model = newModel;
1104
- if (newEffort)
1105
- this.config.agents.codex.reasoning = newEffort;
1106
- try {
1107
- saveConfig(this.config);
1108
- }
1109
- catch (error) {
1110
- return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
1111
- }
1112
- }
1113
- else {
1114
- // Codex 全局配置(~/.codex/config.toml)目前不支持写入,回退到 evolclaw.json
1115
- if (!this.config.agents.codex)
1116
- this.config.agents.codex = {};
1117
- if (newModel)
1118
- this.config.agents.codex.model = newModel;
1119
- if (newEffort)
1120
- this.config.agents.codex.reasoning = newEffort;
1121
- try {
1122
- saveConfig(this.config);
1123
- }
1124
- catch (error) {
1125
- return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
1126
- }
1127
- }
1550
+ // 持久化:agent-owned channel 写到 agent.json;default 走原"就近原则"
1551
+ if (newModel) {
1552
+ const err = this.persistBaseagentModel(channel, modelAgent.name, newModel);
1553
+ if (err)
1554
+ return `${err}\n已更新运行时配置,但未持久化`;
1128
1555
  }
1129
- else {
1130
- const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1131
- if (configuredInEvolclaw) {
1132
- if (!this.config.agents.claude)
1133
- this.config.agents.claude = {};
1134
- if (newModel)
1135
- this.config.agents.claude.model = newModel;
1136
- if (newEffort)
1137
- this.config.agents.claude.effort = newEffort;
1138
- try {
1139
- saveConfig(this.config);
1140
- }
1141
- catch (error) {
1142
- return `⚠️ 写入 evolclaw.json 失败: ${error.message}\n已更新运行时配置,但未持久化`;
1143
- }
1144
- }
1145
- else {
1146
- const updates = {};
1147
- if (newModel)
1148
- updates.model = newModel;
1149
- if (newEffort)
1150
- updates.effortLevel = newEffort;
1151
- const writeResult = writeUserSettings(updates);
1152
- if (!writeResult.success) {
1153
- return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
1154
- }
1155
- }
1556
+ if (newEffort) {
1557
+ const err = this.persistBaseagentEffort(channel, modelAgent.name, newEffort);
1558
+ if (err)
1559
+ return `${err}\n已更新运行时配置,但未持久化`;
1156
1560
  }
1157
1561
  return `✓ 已切换\n ${changes.join('\n ')}`;
1158
1562
  }
@@ -1163,7 +1567,7 @@ export class CommandHandler {
1163
1567
  if ('error' in effortResult)
1164
1568
  return effortResult.error;
1165
1569
  const { session: effortSession } = effortResult;
1166
- const effortAgent = this.getAgent(effortSession.agentId);
1570
+ const effortAgent = this.getAgent(channel, effortSession.agentId);
1167
1571
  const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
1168
1572
  const efforts = getAvailableEfforts(effortAgent, currentModel);
1169
1573
  const currentEffort = effortAgent.getEffort?.() || 'auto';
@@ -1231,29 +1635,9 @@ export class CommandHandler {
1231
1635
  // /effort auto:恢复 SDK 默认
1232
1636
  if (args === 'auto') {
1233
1637
  effortAgent.setEffort?.(undefined);
1234
- const isCodex = effortAgent.name === 'codex';
1235
- if (isCodex) {
1236
- if (this.config.agents?.codex?.reasoning) {
1237
- delete this.config.agents.codex.reasoning;
1238
- try {
1239
- saveConfig(this.config);
1240
- }
1241
- catch { }
1242
- }
1243
- }
1244
- else {
1245
- const configuredInEvolclaw = !!this.config.agents?.claude?.effort;
1246
- if (configuredInEvolclaw) {
1247
- delete this.config.agents.claude.effort;
1248
- try {
1249
- saveConfig(this.config);
1250
- }
1251
- catch { }
1252
- }
1253
- else {
1254
- writeUserSettings({ effortLevel: null });
1255
- }
1256
- }
1638
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, undefined);
1639
+ if (err)
1640
+ return `${err}\n已更新运行时配置,但未持久化`;
1257
1641
  return '✓ 推理强度已恢复为 auto (SDK默认)';
1258
1642
  }
1259
1643
  // /effort <level>:切换推理强度
@@ -1265,34 +1649,9 @@ export class CommandHandler {
1265
1649
  }
1266
1650
  const newEffort = args;
1267
1651
  effortAgent.setEffort?.(newEffort);
1268
- // 持久化
1269
- if (!this.config.agents)
1270
- this.config.agents = {};
1271
- const isCodex = effortAgent.name === 'codex';
1272
- if (isCodex) {
1273
- if (!this.config.agents.codex)
1274
- this.config.agents.codex = {};
1275
- this.config.agents.codex.reasoning = newEffort;
1276
- try {
1277
- saveConfig(this.config);
1278
- }
1279
- catch { }
1280
- }
1281
- else {
1282
- const configuredInEvolclaw = !!(this.config.agents?.claude?.model || this.config.agents?.claude?.effort);
1283
- if (configuredInEvolclaw) {
1284
- if (!this.config.agents.claude)
1285
- this.config.agents.claude = {};
1286
- this.config.agents.claude.effort = newEffort;
1287
- try {
1288
- saveConfig(this.config);
1289
- }
1290
- catch { }
1291
- }
1292
- else {
1293
- writeUserSettings({ effortLevel: newEffort });
1294
- }
1295
- }
1652
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
1653
+ if (err)
1654
+ return `${err}\n已更新运行时配置,但未持久化`;
1296
1655
  return `✓ 推理强度: ${newEffort}`;
1297
1656
  }
1298
1657
  // /aid 命令:AID 身份管理(list / new)
@@ -1424,7 +1783,7 @@ export class CommandHandler {
1424
1783
  owner: 'owner-dm-only',
1425
1784
  none: 'none',
1426
1785
  };
1427
- const currentMode = getChannelShowActivities(this.config, channel);
1786
+ const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? getChannelShowActivities(this.config, channel);
1428
1787
  // 模式描述列表(用于 body 和文本降级)
1429
1788
  const modeDescriptions = [
1430
1789
  { key: 'all', configVal: 'all', label: '全部显示' },
@@ -1495,7 +1854,12 @@ export class CommandHandler {
1495
1854
  // 切换操作仅 owner
1496
1855
  if (!isOwner)
1497
1856
  return '❌ 中间输出模式切换仅限 owner';
1498
- setChannelShowActivities(this.config, channel, newMode);
1857
+ if (this.agentRegistry?.setShowActivities) {
1858
+ this.agentRegistry.setShowActivities(channel, newMode);
1859
+ }
1860
+ else {
1861
+ setChannelShowActivities(this.config, channel, newMode);
1862
+ }
1499
1863
  return `✅ 中间输出模式: ${activityArg}(${label})`;
1500
1864
  }
1501
1865
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
@@ -1526,7 +1890,7 @@ export class CommandHandler {
1526
1890
  if (threadId) {
1527
1891
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
1528
1892
  if (threadSession) {
1529
- const threadAgent = this.getAgent(threadSession.agentId);
1893
+ const threadAgent = this.getAgent(channel, threadSession.agentId);
1530
1894
  if (threadAgent.hasActiveStream(threadSession.id)) {
1531
1895
  return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
1532
1896
  }
@@ -1544,7 +1908,7 @@ export class CommandHandler {
1544
1908
  if ('error' in stopResult)
1545
1909
  return '当前没有正在处理的任务';
1546
1910
  const { session: stopSession } = stopResult;
1547
- const stopAgent = this.getAgent(stopSession.agentId);
1911
+ const stopAgent = this.getAgent(channel, stopSession.agentId);
1548
1912
  const sessionKey = stopSession.id;
1549
1913
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
1550
1914
  const hasActive = stopAgent.hasActiveStream(sessionKey);
@@ -1553,7 +1917,12 @@ export class CommandHandler {
1553
1917
  }
1554
1918
  await stopAgent.interrupt(sessionKey);
1555
1919
  // 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
1556
- this.eventBus.publish({ type: 'message:interrupted', sessionId: sessionKey, reason: 'stop' });
1920
+ this.eventBus.publish({
1921
+ type: 'message:interrupted',
1922
+ sessionId: sessionKey,
1923
+ reason: 'stop',
1924
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '[default]',
1925
+ });
1557
1926
  // 强制清除 processing_state
1558
1927
  this.sessionManager.clearProcessing(sessionKey);
1559
1928
  return '✓ 已发送中断信号,任务将尽快停止';
@@ -1564,7 +1933,7 @@ export class CommandHandler {
1564
1933
  if ('error' in result)
1565
1934
  return result.error;
1566
1935
  const { session } = result;
1567
- const sessionAgent = this.getAgent(session.agentId);
1936
+ const sessionAgent = this.getAgent(channel, session.agentId);
1568
1937
  if (!sessionAgent.capabilities?.clear) {
1569
1938
  return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代`;
1570
1939
  }
@@ -1596,7 +1965,7 @@ export class CommandHandler {
1596
1965
  if ('error' in result)
1597
1966
  return result.error;
1598
1967
  const { session } = result;
1599
- const sessionAgent = this.getAgent(session.agentId);
1968
+ const sessionAgent = this.getAgent(channel, session.agentId);
1600
1969
  if (!sessionAgent.capabilities?.compact) {
1601
1970
  return `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact`;
1602
1971
  }
@@ -1626,7 +1995,7 @@ export class CommandHandler {
1626
1995
  // 尝试获取活跃会话(话题时直接查找话题 session)
1627
1996
  let session;
1628
1997
  if (threadId) {
1629
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
1998
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
1630
1999
  }
1631
2000
  else {
1632
2001
  session = await this.sessionManager.getActiveSession(channel, channelId);
@@ -1638,7 +2007,7 @@ export class CommandHandler {
1638
2007
  normalizedContent.startsWith('/project') ||
1639
2008
  normalizedContent === '/pwd' ||
1640
2009
  normalizedContent === '/status')) {
1641
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd());
2010
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel));
1642
2011
  }
1643
2012
  // /status 命令:显示会话状态
1644
2013
  if (normalizedContent === '/status') {
@@ -1647,7 +2016,7 @@ export class CommandHandler {
1647
2016
  return `❌ 无法创建会话,请检查配置`;
1648
2017
  }
1649
2018
  const sessionKey = this.getQueueKey(session, channel, channelId);
1650
- const sessionAgent = this.getAgent(session.agentId);
2019
+ const sessionAgent = this.getAgent(channel, session.agentId);
1651
2020
  const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
1652
2021
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
1653
2022
  const isThread = !!session.threadId;
@@ -1707,7 +2076,7 @@ export class CommandHandler {
1707
2076
  return `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称`;
1708
2077
  }
1709
2078
  }
1710
- const projectPath = session?.projectPath || this.config.projects?.defaultPath || process.cwd();
2079
+ const projectPath = session?.projectPath || this.getEffectiveDefaultPath(channel);
1711
2080
  const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.defaultAgentId);
1712
2081
  this.eventBus.publish({
1713
2082
  type: 'session:created',
@@ -1729,11 +2098,30 @@ export class CommandHandler {
1729
2098
  // /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
1730
2099
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
1731
2100
  const subCmd = normalizedContent.slice('/check'.length).trim();
2101
+ // 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
2102
+ // default 通道也仅显示 default 的渠道(不再展示 evolagents 的渠道)
2103
+ const checkOwningAgent = this.getOwningAgent(channel);
2104
+ let allowedChannels;
2105
+ if (checkOwningAgent) {
2106
+ allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
2107
+ }
2108
+ else {
2109
+ // default 范围:所有 channel 中,不属于任何 evolagent 的
2110
+ const defaultNames = [];
2111
+ for (const [name] of this.adapters) {
2112
+ const owner = this.agentRegistry?.resolveByChannel(name);
2113
+ if (!owner || owner.isDefault)
2114
+ defaultNames.push(name);
2115
+ }
2116
+ allowedChannels = new Set(defaultNames);
2117
+ }
1732
2118
  // Default: show system health check (non-admin 仅看摘要)
1733
2119
  const lines = ['📡 渠道状态:'];
1734
2120
  // Group by channelType
1735
2121
  const groups = new Map();
1736
2122
  for (const [name] of this.adapters) {
2123
+ if (!allowedChannels.has(name))
2124
+ continue;
1737
2125
  const type = this.channelTypeMap.get(name) || name;
1738
2126
  const ch = this.channelObjects.get(name);
1739
2127
  let status;
@@ -1764,19 +2152,21 @@ export class CommandHandler {
1764
2152
  lines.push(` ${type}: [${parts.join(', ')}]`);
1765
2153
  }
1766
2154
  }
1767
- // 队列状态
2155
+ // 当前 agent 名(用于 agent 维度 stats / queue 查询)
2156
+ const currentAgentName = checkOwningAgent?.name ?? '[default]';
2157
+ // 队列状态(按当前 agent 维度)
1768
2158
  lines.push('', '📬 队列状态:');
1769
- lines.push(` 待处理消息: ${this.messageQueue.getGlobalQueueLength()}`);
1770
- lines.push(` 处理中队列: ${this.messageQueue.getGlobalProcessingCount()}`);
1771
- // 运行概况
2159
+ lines.push(` 待处理消息: ${this.messageQueue.getQueueLengthByAgent(currentAgentName)}`);
2160
+ lines.push(` 处理中队列: ${this.messageQueue.getProcessingCountByAgent(currentAgentName)}`);
2161
+ // 运行概况(全局,进程级)
1772
2162
  lines.push('', '🖥️ 运行概况:');
1773
2163
  const uptimeMs = this.statsCollector
1774
2164
  ? this.statsCollector.getSnapshot().uptimeMs
1775
2165
  : process.uptime() * 1000;
1776
2166
  lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
1777
- // 近 1 小时统计
2167
+ // 近 1 小时统计(按当前 agent 维度)
1778
2168
  if (this.statsCollector) {
1779
- const snap = this.statsCollector.getSnapshot();
2169
+ const snap = this.statsCollector.getSnapshot(currentAgentName);
1780
2170
  const h = snap.lastHour;
1781
2171
  lines.push('', '📊 近 1 小时统计:');
1782
2172
  lines.push(` 收到消息: ${h.received}`);
@@ -1802,23 +2192,50 @@ export class CommandHandler {
1802
2192
  // /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
1803
2193
  if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
1804
2194
  const restartArg = normalizedContent.slice('/restart'.length).trim();
1805
- // /restart <channel> — 重连指定渠道(admin only
2195
+ // /restart <type> — 重连指定类型的所有渠道(admin only,evolclaw 服务级操作)
2196
+ // 服务级操作仅可从 default 通道发起,避免 evolagent owner/admin 越权
1806
2197
  if (restartArg) {
2198
+ if (this.getOwningAgent(channel)) {
2199
+ return '❌ 渠道重连只能从 DefaultAgent 通道发起(服务级操作)';
2200
+ }
1807
2201
  if (!isAdmin)
1808
2202
  return '❌ 无权限:渠道重连仅限管理员使用';
1809
- const target = restartArg;
1810
- const ch = this.channelObjects.get(target);
1811
- if (!ch) {
1812
- const available = [...this.channelObjects.keys()].join(', ') || '无';
1813
- return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
1814
- }
1815
- if (!ch.reconnect) {
1816
- return `❌ 渠道 "${target}" 不支持重连`;
2203
+ const type = restartArg;
2204
+ // /restart 是服务级操作:重连该 type 下的所有实例(不分 agent)
2205
+ const scopedNames = [];
2206
+ for (const [name] of this.adapters) {
2207
+ if (this.channelTypeMap.get(name) === type)
2208
+ scopedNames.push(name);
2209
+ }
2210
+ if (scopedNames.length === 0) {
2211
+ return `❌ 没有类型为 "${type}" 的渠道`;
2212
+ }
2213
+ const results = [];
2214
+ for (const name of scopedNames) {
2215
+ const ch = this.channelObjects.get(name);
2216
+ if (!ch) {
2217
+ results.push(`${name}: 未找到渠道对象`);
2218
+ continue;
2219
+ }
2220
+ if (!ch.reconnect) {
2221
+ results.push(`${name}: 不支持重连`);
2222
+ continue;
2223
+ }
2224
+ try {
2225
+ const result = await ch.reconnect();
2226
+ results.push(`${name}: ${result}`);
2227
+ }
2228
+ catch (e) {
2229
+ results.push(`${name}: 重连失败 - ${e?.message || e}`);
2230
+ }
1817
2231
  }
1818
- const result = await ch.reconnect();
1819
- return `🔄 ${target} 重连: ${result}`;
2232
+ return `🔄 重连 ${type}:\n ${results.join('\n ')}`;
2233
+ }
2234
+ // /restart(无参数)— 重启整个服务(owner only,且仅可从 default 通道触发)
2235
+ // 防止 evolagent 通道的 owner 越权杀整个 evolclaw 进程(影响所有租户)
2236
+ if (this.getOwningAgent(channel)) {
2237
+ return '❌ 服务重启只能从 DefaultAgent 通道发起。EvolAgent 通道仅可执行 /restart <type> 重连特定类型渠道';
1820
2238
  }
1821
- // /restart(无参数)— 重启整个服务(owner only)
1822
2239
  if (!isOwner)
1823
2240
  return '❌ 无权限:服务重启仅限 owner 使用';
1824
2241
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
@@ -1832,7 +2249,7 @@ export class CommandHandler {
1832
2249
  const executeRestart = async () => {
1833
2250
  let replyContext;
1834
2251
  if (threadId) {
1835
- const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
2252
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
1836
2253
  replyContext = this.getReplyContext(threadSession);
1837
2254
  }
1838
2255
  const restartInfo = {
@@ -1971,7 +2388,7 @@ export class CommandHandler {
1971
2388
  // 找目标 channelId
1972
2389
  let targetChannelId = channelId;
1973
2390
  if (isCrossChannel) {
1974
- const ownerPeerId = getOwner(this.config, targetChannel);
2391
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel) ?? getOwner(this.config, targetChannel);
1975
2392
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
1976
2393
  if (!targetChannelId) {
1977
2394
  return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
@@ -2226,27 +2643,19 @@ export class CommandHandler {
2226
2643
  }
2227
2644
  // 生成项目名称(使用目录名)
2228
2645
  const projectName = path.basename(projectPath);
2229
- // 检查是否已存在
2230
- if (this.projects[projectName]) {
2231
- const existingPath = this.projects[projectName];
2232
- if (existingPath === projectPath) {
2646
+ // 检查在当前 scope 内是否已存在
2647
+ const scopeProjects = this.getEffectiveProjects(channel);
2648
+ const existing = scopeProjects[projectName];
2649
+ if (existing) {
2650
+ if (existing === projectPath) {
2233
2651
  return `项目 "${projectName}" 已存在\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
2234
2652
  }
2235
- return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existingPath}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
2236
- }
2237
- // 添加到配置
2238
- if (!this.config.projects) {
2239
- this.config.projects = { defaultPath: process.cwd(), autoCreate: false, list: {} };
2653
+ return `❌ 项目名称 "${projectName}" 已被占用\n 现有路径: ${existing}\n 新路径: ${projectPath}\n\n请重命名目录或手动编辑配置文件`;
2240
2654
  }
2241
- if (!this.config.projects.list) {
2242
- this.config.projects.list = {};
2243
- }
2244
- this.config.projects.list[projectName] = projectPath;
2245
- // 保存配置
2246
- const { saveConfig } = await import('../config.js');
2247
- saveConfig(this.config);
2248
- // 更新内存中的项目列表
2249
- this.projects[projectName] = projectPath;
2655
+ // 写入:agent-owned channel → agent.json;default → evolclaw.json
2656
+ const err = await this.addProjectInScope(channel, projectName, projectPath);
2657
+ if (err)
2658
+ return err;
2250
2659
  return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
2251
2660
  }
2252
2661
  // /slist 命令:列出当前项目的会话
@@ -2608,7 +3017,7 @@ export class CommandHandler {
2608
3017
  return `❌ 删除失败`;
2609
3018
  }
2610
3019
  this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
2611
- const targetAgent = this.getAgent(targetSession.agentId);
3020
+ const targetAgent = this.getAgent(channel, targetSession.agentId);
2612
3021
  await targetAgent.closeSession(targetSession.id);
2613
3022
  return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
2614
3023
  }
@@ -2621,7 +3030,7 @@ export class CommandHandler {
2621
3030
  if (!session.agentSessionId) {
2622
3031
  return `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
2623
3032
  }
2624
- const forkAgent = this.getAgent(session.agentId);
3033
+ const forkAgent = this.getAgent(channel, session.agentId);
2625
3034
  if (!forkAgent.capabilities?.fork) {
2626
3035
  return `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代`;
2627
3036
  }
@@ -2642,7 +3051,7 @@ export class CommandHandler {
2642
3051
  if ('error' in result)
2643
3052
  return result.error;
2644
3053
  const { session } = result;
2645
- const rewindAgent = this.getAgent(session.agentId);
3054
+ const rewindAgent = this.getAgent(channel, session.agentId);
2646
3055
  if (rewindAgent.name !== 'claude') {
2647
3056
  return '❌ /rewind 仅支持 Claude 后端';
2648
3057
  }
@@ -2679,7 +3088,7 @@ export class CommandHandler {
2679
3088
  if ('error' in repairResult)
2680
3089
  return repairResult.error;
2681
3090
  const { session: repairSession } = repairResult;
2682
- const repairAgent = this.getAgent(repairSession.agentId);
3091
+ const repairAgent = this.getAgent(channel, repairSession.agentId);
2683
3092
  const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
2684
3093
  try {
2685
3094
  if (!repairSession.agentSessionId) {
@@ -2838,7 +3247,7 @@ export class CommandHandler {
2838
3247
  '/help', '/status', '/check', '/pwd',
2839
3248
  '/model', '/effort', '/perm', '/agent',
2840
3249
  '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
2841
- '/rename', '/name',
3250
+ '/rename', '/name', '/evolagent',
2842
3251
  ];
2843
3252
  /** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
2844
3253
  static CTL_READONLY = new Set(['/agent']);
@@ -2892,6 +3301,48 @@ export class CommandHandler {
2892
3301
  }
2893
3302
  // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
2894
3303
  const userId = session.metadata?.peerId;
3304
+ // 3.1 /evolagent: EvolAgent 管理(show identity / reload)
3305
+ if (cmd === '/evolagent' || cmd.startsWith('/evolagent ')) {
3306
+ const arg = cmd.slice('/evolagent'.length).trim();
3307
+ if (!arg) {
3308
+ const owning = this.getOwningAgent(session.channel);
3309
+ if (owning) {
3310
+ return { ok: true, result: `当前 EvolAgent: ${owning.name} (${owning.baseagent})` };
3311
+ }
3312
+ return { ok: true, result: '当前为 DefaultAgent 模式' };
3313
+ }
3314
+ if (arg.startsWith('reload ') || arg === 'reload') {
3315
+ const name = arg === 'reload' ? '' : arg.slice('reload '.length).trim();
3316
+ if (!name)
3317
+ return { ok: false, error: '用法: evolclaw ctl evolagent reload <name>' };
3318
+ // I8: reload is a structural op, require admin or owner
3319
+ if (!userId) {
3320
+ return { ok: false, error: '权限不足:evolagent reload 仅 owner/admin 可用' };
3321
+ }
3322
+ const identity = this.sessionManager.resolveIdentity(session.channel, userId);
3323
+ if (identity.role !== 'owner' && identity.role !== 'admin') {
3324
+ return { ok: false, error: '权限不足:evolagent reload 仅 owner/admin 可用' };
3325
+ }
3326
+ if (!this.agentRegistry)
3327
+ return { ok: false, error: 'EvolAgentRegistry not available' };
3328
+ const a = this.agentRegistry.get(name);
3329
+ if (!a)
3330
+ return { ok: false, error: `Agent "${name}" not found` };
3331
+ const hooks = globalThis.__evolclaw_reloadHooks;
3332
+ if (!hooks)
3333
+ return { ok: false, error: 'Reload hooks not initialized' };
3334
+ if (!this.agentRegistry.reload)
3335
+ return { ok: false, error: 'EvolAgentRegistry.reload not available' };
3336
+ try {
3337
+ await this.agentRegistry.reload(name, hooks);
3338
+ return { ok: true, result: `Agent "${name}" reloaded` };
3339
+ }
3340
+ catch (e) {
3341
+ return { ok: false, error: `Reload failed: ${e?.message || e}` };
3342
+ }
3343
+ }
3344
+ return { ok: false, error: '用法: evolclaw ctl evolagent [reload <name>]' };
3345
+ }
2895
3346
  // 4. /send 文本消息:直接通过 adapter 主动发送,不走 handle()
2896
3347
  if (cmd.startsWith('/send ') || cmd === '/send') {
2897
3348
  const text = cmd.startsWith('/send ') ? cmd.slice(6).trim() : '';