evolclaw 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +49 -27
  2. package/data/evolclaw.sample.json +6 -3
  3. package/dist/agents/claude-runner.js +125 -52
  4. package/dist/agents/codex-runner.js +10 -5
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +247 -84
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +132 -50
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +750 -209
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +216 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
  16. package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
  17. package/dist/{utils → core/message}/stream-debouncer.js +1 -1
  18. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  19. package/dist/core/permission.js +212 -11
  20. package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
  21. package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
  22. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  23. package/dist/{utils → core/session}/session-file-health.js +1 -1
  24. package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
  25. package/dist/index.js +138 -54
  26. package/dist/{core/ipc-server.js → ipc.js} +36 -1
  27. package/dist/types.js +3 -0
  28. package/dist/utils/cross-platform.js +38 -1
  29. package/dist/utils/error-utils.js +130 -5
  30. package/dist/utils/init-channel.js +649 -0
  31. package/dist/utils/init.js +55 -150
  32. package/dist/utils/logger.js +8 -3
  33. package/dist/utils/media-cache.js +207 -0
  34. package/dist/{core → utils}/stats-collector.js +16 -0
  35. package/package.json +3 -3
  36. package/dist/core/message-bridge.js +0 -187
  37. package/dist/utils/init-feishu.js +0 -263
  38. package/dist/utils/init-wechat.js +0 -172
  39. package/dist/utils/ipc-client.js +0 -36
  40. package/dist/utils/permission-utils.js +0 -71
  41. /package/dist/{utils → core/message}/message-cache.js +0 -0
  42. /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
  43. /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
@@ -1,15 +1,34 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
2
  import { saveConfig, resolvePaths, getPackageRoot, getOwner } from '../config.js';
3
3
  import { logger } from '../utils/logger.js';
4
+ import crypto from 'crypto';
4
5
  import path from 'path';
5
6
  import fs from 'fs';
6
7
  import os from 'os';
7
- const availableEfforts = ['low', 'medium', 'high', 'max'];
8
- function effortBar(level) {
9
- const levels = {
10
- low: '◆◇◇◇', medium: '◆◆◇◇', high: '◆◆◆◇', max: '◆◆◆◆'
11
- };
12
- return levels[level] || '◆◆◇◇';
8
+ const allEfforts = ['low', 'medium', 'high', 'max'];
9
+ const nonMaxEfforts = allEfforts.filter(e => e !== 'max');
10
+ function getAvailableEfforts(agent, model) {
11
+ if (agent.name === 'claude') {
12
+ if (model.includes('opus'))
13
+ return allEfforts;
14
+ return nonMaxEfforts;
15
+ }
16
+ if (agent.name === 'codex') {
17
+ return nonMaxEfforts;
18
+ }
19
+ return [];
20
+ }
21
+ function formatModelUsage(agent, model) {
22
+ const efforts = getAvailableEfforts(agent, model);
23
+ const lines = [
24
+ '用法:',
25
+ ' /model <model> 切换模型',
26
+ ];
27
+ if (efforts.length > 0) {
28
+ lines.push(' /model <model> <effort> 切换模型+推理强度');
29
+ lines.push(' /effort [level] 查看或切换推理强度');
30
+ }
31
+ return lines.join('\n');
13
32
  }
14
33
  /**
15
34
  * 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
@@ -84,7 +103,7 @@ function formatIdleTime(ms) {
84
103
  return '刚刚';
85
104
  }
86
105
  // 支持的命令列表
87
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check'];
106
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check'];
88
107
  // 命令别名映射
89
108
  const aliases = {
90
109
  '/p': '/project',
@@ -92,7 +111,7 @@ const aliases = {
92
111
  '/name': '/rename'
93
112
  };
94
113
  // 命令快速路径前缀(所有命令都不进入消息队列)
95
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name '];
114
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name '];
96
115
  export class CommandHandler {
97
116
  sessionManager;
98
117
  config;
@@ -101,9 +120,11 @@ export class CommandHandler {
101
120
  adapters = new Map();
102
121
  policies = new Map();
103
122
  channelObjects = new Map(); // name → actual channel instance (for /check)
123
+ channelTypeMap = new Map(); // name → channelType (for grouping)
104
124
  processor;
105
125
  messageQueue;
106
126
  permissionGateway;
127
+ interactionRouter;
107
128
  statsCollector;
108
129
  agentMap;
109
130
  defaultAgentId;
@@ -145,12 +166,16 @@ export class CommandHandler {
145
166
  const d = Math.floor(sec / 86400);
146
167
  const h = Math.floor((sec % 86400) / 3600);
147
168
  const m = Math.floor((sec % 3600) / 60);
169
+ const s = sec % 60;
148
170
  const parts = [];
149
171
  if (d > 0)
150
172
  parts.push(`${d}天`);
151
173
  if (h > 0)
152
174
  parts.push(`${h}时`);
153
- parts.push(`${m}分`);
175
+ if (m > 0)
176
+ parts.push(`${m}分`);
177
+ if (parts.length === 0)
178
+ parts.push(`${s}秒`);
154
179
  return parts.join('');
155
180
  }
156
181
  /** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
@@ -162,6 +187,69 @@ export class CommandHandler {
162
187
  getReplyContext(session) {
163
188
  return session.metadata?.replyContext;
164
189
  }
190
+ /**
191
+ * 尝试通过渠道适配器发送交互卡片。
192
+ * 返回 message_id 表示卡片已发送,false 表示降级为文本。
193
+ */
194
+ async trySendInteraction(channel, channelId, interaction, replyContext) {
195
+ const adapter = this.adapters.get(channel);
196
+ if (!adapter?.sendInteraction)
197
+ return false;
198
+ try {
199
+ return await adapter.sendInteraction(channelId, interaction, replyContext);
200
+ }
201
+ catch (e) {
202
+ logger.warn(`[CommandHandler] sendInteraction failed: ${e}`);
203
+ return false;
204
+ }
205
+ }
206
+ /** 作废某 session 下所有 pending 交互卡片(PATCH 禁用 + cancel) */
207
+ async invalidateOldCards(channel, sessionId) {
208
+ if (!this.interactionRouter)
209
+ return;
210
+ const adapter = this.adapters.get(channel);
211
+ const pending = this.interactionRouter.getPending(sessionId);
212
+ if (pending.length === 0)
213
+ return;
214
+ const disabledCard = {
215
+ config: { wide_screen_mode: true },
216
+ header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
217
+ elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
218
+ };
219
+ for (const id of pending) {
220
+ const msgId = this.interactionRouter.getMessageId(id);
221
+ if (msgId && adapter?.patchInteractionCard) {
222
+ adapter.patchInteractionCard(msgId, disabledCard).catch(() => { });
223
+ }
224
+ this.interactionRouter.cancel(id);
225
+ }
226
+ }
227
+ /**
228
+ * 发送交互卡片并注册回调。作废旧卡片 → 发送新卡片 → 注册到 interactionRouter。
229
+ * 返回 true 表示卡片已发送(调用方应 return null),false 表示降级到文本。
230
+ */
231
+ async sendInteractionCard(opts) {
232
+ if (!this.interactionRouter)
233
+ return false;
234
+ await this.invalidateOldCards(opts.channel, opts.sessionId);
235
+ const messageId = await this.trySendInteraction(opts.channel, opts.channelId, opts.interaction, opts.replyCtx);
236
+ if (!messageId)
237
+ return false;
238
+ const wrappedCallback = async (action, values, operatorId) => {
239
+ await opts.callback(action, values, operatorId);
240
+ const adapter = this.adapters.get(opts.channel);
241
+ if (adapter?.patchInteractionCard) {
242
+ const disabledCard = {
243
+ config: { wide_screen_mode: true },
244
+ header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
245
+ elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
246
+ };
247
+ adapter.patchInteractionCard(messageId, disabledCard).catch(() => { });
248
+ }
249
+ };
250
+ this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
251
+ return true;
252
+ }
165
253
  /** 获取活跃会话,无会话时返回统一错误提示 */
166
254
  async ensureSession(channel, channelId, threadId) {
167
255
  if (threadId) {
@@ -187,20 +275,37 @@ export class CommandHandler {
187
275
  setPermissionGateway(gateway) {
188
276
  this.permissionGateway = gateway;
189
277
  }
278
+ setInteractionRouter(router) {
279
+ this.interactionRouter = router;
280
+ }
190
281
  setStatsCollector(collector) {
191
282
  this.statsCollector = collector;
192
283
  }
193
284
  registerAdapter(adapter) {
194
- this.adapters.set(adapter.name, adapter);
285
+ this.adapters.set(adapter.channelName, adapter);
195
286
  }
196
- registerChannel(name, channel) {
287
+ registerChannel(name, channel, channelType) {
197
288
  this.channelObjects.set(name, channel);
289
+ if (channelType)
290
+ this.channelTypeMap.set(name, channelType);
291
+ }
292
+ /** 将实例名解析为渠道类型(用于 session 查询) */
293
+ resolveChannelType(channelName) {
294
+ return this.channelTypeMap.get(channelName) || channelName;
198
295
  }
199
296
  registerPolicy(channelName, policy) {
200
297
  this.policies.set(channelName, policy);
201
298
  }
202
299
  getAdapter(channelName) {
203
- return this.adapters.get(channelName);
300
+ // 先按实例名查找,再按 channelType 查找
301
+ let adapter = this.adapters.get(channelName);
302
+ if (adapter)
303
+ return adapter;
304
+ for (const [name, a] of this.adapters) {
305
+ if ((this.channelTypeMap.get(name) || name) === channelName)
306
+ return a;
307
+ }
308
+ return undefined;
204
309
  }
205
310
  getPolicy(channel) {
206
311
  return this.policies.get(channel) || {
@@ -237,6 +342,7 @@ export class CommandHandler {
237
342
  commands: [
238
343
  { cmd: '/new', args: '[name]', label: '创建新会话' },
239
344
  { cmd: '/slist', label: '列出当前项目的所有会话' },
345
+ { cmd: '/slist', args: 'cli', label: '列出 CLI 会话(未导入的)' },
240
346
  { cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
241
347
  { cmd: '/name', args: '<name>', label: '重命名当前会话' },
242
348
  { cmd: '/del', args: '<name>', label: '删除指定会话' },
@@ -252,13 +358,14 @@ export class CommandHandler {
252
358
  group: 'Agent 与模型',
253
359
  commands: [
254
360
  { cmd: '/agent', args: '[name]', label: '查看或切换 Agent 后端' },
255
- { cmd: '/model', args: '[model] [effort]', label: '查看或切换模型' },
361
+ { cmd: '/model', args: '[model]', label: '查看或切换模型' },
362
+ { cmd: '/effort', args: '[level]', label: '查看或切换推理强度' },
256
363
  ]
257
364
  });
258
365
  items.push({
259
366
  group: '权限管理',
260
367
  commands: [
261
- { cmd: '/perm', args: '[mode|allow|deny]', label: '权限模式管理' },
368
+ { cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
262
369
  ]
263
370
  });
264
371
  items.push({
@@ -300,7 +407,7 @@ export class CommandHandler {
300
407
  * 主命令处理入口
301
408
  */
302
409
  async handle(content, channel, channelId, sendMessage, userId, threadId) {
303
- // 解析身份
410
+ // 解析身份(按实例名)
304
411
  const identity = this.sessionManager.resolveIdentity(channel, userId);
305
412
  const policy = this.getPolicy(channel);
306
413
  // 按当前会话选择 agent 后端
@@ -335,7 +442,7 @@ export class CommandHandler {
335
442
  }
336
443
  }
337
444
  // 空闲检查:某些命令需要等待当前会话空闲
338
- const requiresIdle = ['/new', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind'];
445
+ const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
339
446
  if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
340
447
  if (threadId) {
341
448
  // 话题中:检查话题 session 是否在处理(不创建)
@@ -370,7 +477,7 @@ export class CommandHandler {
370
477
  }
371
478
  const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd));
372
479
  if (!isCmd)
373
- return null;
480
+ return undefined;
374
481
  // /help 命令不需要会话
375
482
  if (normalizedContent === '/help') {
376
483
  if (!isAdmin) {
@@ -380,6 +487,7 @@ export class CommandHandler {
380
487
  '🔄 会话管理:',
381
488
  ' /new [名称] - 创建新会话(可选命名)',
382
489
  ' /slist - 列出当前项目的所有会话',
490
+ ' /slist cli - 列出 CLI 会话(未导入的)',
383
491
  ' /s, /session <名称|序号|uuid> - 切换到指定会话',
384
492
  ' /name, /rename <新名称> - 重命名当前会话',
385
493
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
@@ -411,12 +519,13 @@ export class CommandHandler {
411
519
  '',
412
520
  '🤖 Agent 与模型:',
413
521
  ' /agent [name] - 查看或切换 Agent 后端',
414
- ' /model [model] [effort] - 查看或切换模型/推理强度',
522
+ ' /model [model] - 查看或切换模型',
523
+ ' /effort [level] - 查看或切换推理强度',
415
524
  '',
416
525
  '🔐 权限管理:',
417
526
  ' /perm - 查看当前权限模式',
418
- ' /perm <default|request|edit|plan|noask> - 切换权限模式',
419
- ' /perm allow|deny - 审批权限请求',
527
+ ' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
528
+ ' /perm allow|always|deny - 审批权限请求',
420
529
  '',
421
530
  '🛠️ 运维:',
422
531
  ' /status - 显示会话状态',
@@ -445,21 +554,60 @@ export class CommandHandler {
445
554
  if (!hasPermissionController(permAgent)) {
446
555
  return '❌ 权限控制不可用';
447
556
  }
448
- const currentMode = permSession.metadata?.permissionMode || 'default';
557
+ const currentMode = permSession.metadata?.permissionMode ?? 'bypass';
449
558
  const modes = permAgent.listModes();
559
+ // 尝试发送交互卡片
560
+ if (this.interactionRouter) {
561
+ const requestId = `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
562
+ const availableModes = modes.filter(m => m.available);
563
+ const interaction = {
564
+ type: 'interaction',
565
+ id: requestId,
566
+ channelId,
567
+ sessionId: permSession.id,
568
+ kind: {
569
+ kind: 'action',
570
+ title: '🔐 权限模式',
571
+ body: availableModes.map(m => `${m.key === currentMode ? '▶' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
572
+ buttons: availableModes.map(m => ({
573
+ key: m.key,
574
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
575
+ style: m.key === currentMode ? 'primary' : 'default',
576
+ })),
577
+ },
578
+ };
579
+ const replyCtx = this.getReplyContext(permSession);
580
+ const cardSent = await this.sendInteractionCard({
581
+ channel, channelId, sessionId: permSession.id, requestId, interaction, replyCtx,
582
+ callback: async (action, _values, operatorId) => {
583
+ if (action !== currentMode) {
584
+ if (userId && operatorId && operatorId !== userId)
585
+ return;
586
+ const result = await this.handle(`/perm ${action}`, channel, channelId, undefined, userId, threadId);
587
+ if (result) {
588
+ const adapter = this.adapters.get(channel);
589
+ adapter?.sendText(channelId, result, replyCtx);
590
+ }
591
+ }
592
+ },
593
+ });
594
+ if (cardSent)
595
+ return null;
596
+ }
597
+ // 降级:文本
450
598
  const modeList = modes.map(m => {
451
599
  const prefix = m.key === currentMode ? '▶' : ' ';
452
600
  const suffix = m.available ? '' : ' ⚠️ 不可用';
453
601
  return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
454
602
  }).join('\n');
455
- return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|deny 审批权限请求`;
603
+ return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
456
604
  }
457
605
  const parts = args.split(/\s+/);
458
- // /perm <mode> 或 /perm allow|deny:切换模式 / 快捷审批
606
+ // /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
459
607
  if (parts.length === 1) {
460
608
  const arg = parts[0];
461
- // /perm allow|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
462
- if (arg === 'allow' || arg === 'deny') {
609
+ // /perm allow|always|deny:快捷审批(自动找当前 session 唯一的 pending 请求)
610
+ if (arg === 'allow' || arg === 'always' || arg === 'deny') {
463
611
  if (!this.permissionGateway) {
464
612
  return '❌ 权限审批未启用';
465
613
  }
@@ -471,8 +619,14 @@ export class CommandHandler {
471
619
  return `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map(id => ` /perm ${id} ${arg}`).join('\n')}`;
472
620
  }
473
621
  const requestId = pendingIds[0];
474
- this.permissionGateway.resolvePermission(permSession.id, requestId, arg === 'allow');
475
- return arg === 'allow' ? `✓ 已授权,继续执行……` : `✓ 已拒绝`;
622
+ const decision = arg;
623
+ this.permissionGateway.resolvePermission(permSession.id, requestId, decision);
624
+ const labels = {
625
+ allow: '✓ 已授权(本次),继续执行……',
626
+ always: '✓ 已授权(始终允许该工具),继续执行……',
627
+ deny: '✓ 已拒绝'
628
+ };
629
+ return labels[decision];
476
630
  }
477
631
  // /perm <mode>:切换权限模式
478
632
  if (hasPermissionController(permAgent)) {
@@ -482,6 +636,10 @@ export class CommandHandler {
482
636
  if (!matched.available) {
483
637
  return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
484
638
  }
639
+ // guest 用户只能保持 readonly 模式
640
+ if (identity.role !== 'owner' && arg !== 'readonly') {
641
+ return '❌ 当前身份无法切换权限模式';
642
+ }
485
643
  const metadata = permSession.metadata || {};
486
644
  metadata.permissionMode = arg;
487
645
  await this.sessionManager.updateSession(permSession.id, { metadata });
@@ -489,12 +647,12 @@ export class CommandHandler {
489
647
  }
490
648
  }
491
649
  // 不是已知模式名也不是 allow/deny
492
- const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'default|request|edit|plan|noask';
493
- return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|deny`;
650
+ const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
651
+ return `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny`;
494
652
  }
495
653
  // 双参数不再支持,提示正确用法
496
- const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'default|request|edit|plan|noask';
497
- return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|deny`;
654
+ const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
655
+ return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
498
656
  }
499
657
  // /agent 命令:查看或切换 Agent 后端
500
658
  if (normalizedContent.startsWith('/agent')) {
@@ -504,7 +662,44 @@ export class CommandHandler {
504
662
  const available = [...this.agentMap.keys()];
505
663
  if (!args) {
506
664
  const currentAgent = activeSession?.agentId || this.defaultAgentId;
507
- const list = available.map(a => `${a === currentAgent ? '✓' : '-'} ${a}`).join('\n');
665
+ // 尝试发送交互卡片
666
+ if (this.interactionRouter && available.length > 1) {
667
+ const requestId = `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
668
+ const interaction = {
669
+ type: 'interaction',
670
+ id: requestId,
671
+ channelId,
672
+ sessionId: activeSession?.id || requestId,
673
+ kind: {
674
+ kind: 'action',
675
+ title: '🔌 切换 Agent',
676
+ buttons: available.map(a => ({
677
+ key: a,
678
+ label: a === currentAgent ? `✓ ${a}` : a,
679
+ style: a === currentAgent ? 'primary' : 'default',
680
+ })),
681
+ },
682
+ };
683
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
684
+ const cardSent = await this.sendInteractionCard({
685
+ channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
686
+ callback: async (action, _values, operatorId) => {
687
+ if (action !== currentAgent) {
688
+ if (userId && operatorId && operatorId !== userId)
689
+ return;
690
+ const result = await this.handle(`/agent ${action}`, channel, channelId, undefined, userId, threadId);
691
+ if (result) {
692
+ const adapter = this.adapters.get(channel);
693
+ adapter?.sendText(channelId, result, replyCtx);
694
+ }
695
+ }
696
+ },
697
+ });
698
+ if (cardSent)
699
+ return null;
700
+ }
701
+ // 降级:文本
702
+ const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
508
703
  return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
509
704
  }
510
705
  if (!this.agentMap.has(args)) {
@@ -514,15 +709,19 @@ export class CommandHandler {
514
709
  if ('error' in result)
515
710
  return result.error;
516
711
  const { session } = result;
517
- // 取消原会话的 pending 权限请求
712
+ // 取消原会话的 pending 权限请求和交互卡片
518
713
  if (this.permissionGateway) {
519
714
  this.permissionGateway.cancelAll(session.id);
520
715
  }
716
+ if (this.interactionRouter) {
717
+ this.interactionRouter.cancelAll(session.id);
718
+ }
521
719
  // 切换到目标 agent(恢复已有会话或创建新会话)
522
720
  const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
523
721
  const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
524
722
  const projectName = this.getProjectName(session.projectPath);
525
- return `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
723
+ let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
724
+ return agentSwitchResponse;
526
725
  }
527
726
  // /model 命令:查看或切换模型/推理强度
528
727
  if (normalizedContent.startsWith('/model')) {
@@ -536,54 +735,72 @@ export class CommandHandler {
536
735
  const models = hasModelSwitcher(modelAgent) ? modelAgent.listModels() : [];
537
736
  if (!args) {
538
737
  const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
738
+ const efforts = getAvailableEfforts(modelAgent, currentModel);
539
739
  const currentEffort = modelAgent.getEffort?.() || 'auto';
540
- const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : `${currentEffort} ${effortBar(currentEffort)}`;
740
+ // 尝试发送交互卡片
741
+ if (this.interactionRouter && models.length > 0) {
742
+ const requestId = `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
743
+ const interaction = {
744
+ type: 'interaction',
745
+ id: requestId,
746
+ channelId,
747
+ sessionId: modelSession.id,
748
+ kind: {
749
+ kind: 'action',
750
+ title: '🤖 切换模型',
751
+ buttons: models.map((m) => ({
752
+ key: m,
753
+ label: m === currentModel ? `✓ ${m}` : m,
754
+ style: m === currentModel ? 'primary' : 'default',
755
+ })),
756
+ },
757
+ };
758
+ const replyCtx = this.getReplyContext(modelSession);
759
+ const cardSent = await this.sendInteractionCard({
760
+ channel, channelId, sessionId: modelSession.id, requestId, interaction, replyCtx,
761
+ callback: async (action, _values, operatorId) => {
762
+ if (action !== currentModel) {
763
+ if (userId && operatorId && operatorId !== userId)
764
+ return;
765
+ const result = await this.handle(`/model ${action}`, channel, channelId, undefined, userId, threadId);
766
+ if (result) {
767
+ const adapter = this.adapters.get(channel);
768
+ adapter?.sendText(channelId, result, replyCtx);
769
+ }
770
+ }
771
+ },
772
+ });
773
+ if (cardSent)
774
+ return null;
775
+ }
776
+ // 降级:文本
541
777
  const modelList = models.map((m) => `- ${m}`).join('\n');
542
- return `当前模型: ${currentModel}\n推理强度: ${effortDisplay}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')} / auto\n\n用法:\n /model <model> 切换模型\n /model <model> <effort> 切换模型+推理强度\n /model <effort> 仅切换推理强度\n /model auto 恢复SDK默认`;
778
+ const effortHint = efforts.length > 0
779
+ ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
780
+ : '';
781
+ return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
543
782
  }
544
783
  const parts = args.split(/\s+/);
545
784
  let newModel;
546
785
  let newEffort;
547
786
  if (parts.length === 1) {
548
787
  const arg = parts[0];
549
- if (arg === 'auto') {
550
- modelAgent.setEffort?.(undefined);
551
- // 写回来源
552
- const isCodex = modelAgent.name === 'codex';
553
- if (isCodex) {
554
- if (this.config.agents?.openai?.reasoning) {
555
- delete this.config.agents.openai.reasoning;
556
- try {
557
- saveConfig(this.config);
558
- }
559
- catch { }
560
- }
561
- }
562
- else {
563
- const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
564
- if (configuredInEvolclaw) {
565
- delete this.config.agents.anthropic.effort;
566
- try {
567
- saveConfig(this.config);
568
- }
569
- catch { }
570
- }
571
- else {
572
- writeUserSettings({ effortLevel: null });
573
- }
574
- }
575
- return '✓ 推理强度已恢复为 auto (SDK默认)';
788
+ const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
789
+ const efforts = getAvailableEfforts(modelAgent, currentModel);
790
+ // effort 相关参数统一转发到 /effort
791
+ if (efforts.includes(arg) || arg === 'auto') {
792
+ return this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
576
793
  }
577
- // 单参数:模型 effort
578
- if (availableEfforts.includes(arg)) {
579
- newEffort = arg;
794
+ else if (allEfforts.includes(arg)) {
795
+ return `⚠️ 请使用 /effort ${arg} 调整推理强度`;
580
796
  }
581
797
  else if (models.includes(arg)) {
582
798
  newModel = arg;
583
799
  }
584
800
  else {
585
801
  const modelList = models.map((m) => `- ${m}`).join('\n');
586
- return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')}`;
802
+ const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
803
+ return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
587
804
  }
588
805
  }
589
806
  else {
@@ -592,8 +809,13 @@ export class CommandHandler {
592
809
  if (!models.includes(modelArg)) {
593
810
  return `❌ 无效的模型ID: ${modelArg}`;
594
811
  }
595
- if (!availableEfforts.includes(effortArg)) {
596
- return `❌ 无效的推理强度: ${effortArg}\n可选: ${availableEfforts.join(' / ')}`;
812
+ const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
813
+ if (targetEfforts.length === 0) {
814
+ return `⚠️ ${modelArg} 不支持推理强度设置`;
815
+ }
816
+ if (!targetEfforts.includes(effortArg)) {
817
+ const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
818
+ return `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}`;
597
819
  }
598
820
  newModel = modelArg;
599
821
  newEffort = effortArg;
@@ -613,12 +835,8 @@ export class CommandHandler {
613
835
  changes.push(`模型: ${newModel}`);
614
836
  }
615
837
  if (newEffort) {
616
- const modelAfterSwitch = newModel ?? (hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name);
617
- if (newEffort === 'max' && !modelAfterSwitch.includes('opus')) {
618
- return '⚠️ max 推理强度仅 Opus 模型支持(opus / claude-opus-4-6)';
619
- }
620
838
  modelAgent.setEffort?.(newEffort);
621
- changes.push(`推理强度: ${newEffort} ${effortBar(newEffort)}`);
839
+ changes.push(`推理强度: ${newEffort}`);
622
840
  }
623
841
  // 持久化:写回来源(就近原则)
624
842
  // evolclaw.json 配了 → 写 evolclaw.json
@@ -685,6 +903,137 @@ export class CommandHandler {
685
903
  }
686
904
  return `✓ 已切换\n ${changes.join('\n ')}`;
687
905
  }
906
+ // /effort 命令:查看或切换推理强度
907
+ if (normalizedContent.startsWith('/effort')) {
908
+ const args = normalizedContent.slice(7).trim();
909
+ const effortResult = await this.ensureSession(channel, channelId, threadId);
910
+ if ('error' in effortResult)
911
+ return effortResult.error;
912
+ const { session: effortSession } = effortResult;
913
+ const effortAgent = this.getAgent(effortSession.agentId);
914
+ const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
915
+ const efforts = getAvailableEfforts(effortAgent, currentModel);
916
+ const currentEffort = effortAgent.getEffort?.() || 'auto';
917
+ if (efforts.length === 0) {
918
+ return '⚠️ 当前模型不支持推理强度设置';
919
+ }
920
+ if (!args) {
921
+ // /effort(无参数):显示当前推理强度 + 发送 Action 卡片
922
+ if (this.interactionRouter) {
923
+ const requestId = `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
924
+ const buttons = [
925
+ ...efforts.map(e => ({
926
+ key: e,
927
+ label: e === currentEffort ? `✓ ${e}` : e,
928
+ style: e === currentEffort ? 'primary' : 'default',
929
+ })),
930
+ {
931
+ key: 'auto',
932
+ label: currentEffort === 'auto' ? '✓ auto' : 'auto',
933
+ style: currentEffort === 'auto' ? 'primary' : 'default',
934
+ },
935
+ ];
936
+ const interaction = {
937
+ type: 'interaction',
938
+ id: requestId,
939
+ channelId,
940
+ sessionId: effortSession.id,
941
+ kind: {
942
+ kind: 'action',
943
+ title: '⚡ 推理强度',
944
+ buttons,
945
+ },
946
+ };
947
+ const replyCtx = this.getReplyContext(effortSession);
948
+ const cardSent = await this.sendInteractionCard({
949
+ channel, channelId, sessionId: effortSession.id, requestId, interaction, replyCtx,
950
+ callback: async (action, _values, operatorId) => {
951
+ if (action !== currentEffort) {
952
+ if (userId && operatorId && operatorId !== userId)
953
+ return;
954
+ const result = await this.handle(`/effort ${action}`, channel, channelId, undefined, userId, threadId);
955
+ if (result) {
956
+ const adapter = this.adapters.get(channel);
957
+ adapter?.sendText(channelId, result, replyCtx);
958
+ }
959
+ }
960
+ },
961
+ });
962
+ if (cardSent)
963
+ return null;
964
+ }
965
+ // 降级:文本
966
+ const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
967
+ const effortList = efforts.map(e => `${e === currentEffort ? ' ✓' : ' '} ${e}`).join('\n');
968
+ return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n ${currentEffort === 'auto' ? ' ✓' : ' '} auto\n\n用法: /effort <level>`;
969
+ }
970
+ // /effort auto:恢复 SDK 默认
971
+ if (args === 'auto') {
972
+ effortAgent.setEffort?.(undefined);
973
+ const isCodex = effortAgent.name === 'codex';
974
+ if (isCodex) {
975
+ if (this.config.agents?.openai?.reasoning) {
976
+ delete this.config.agents.openai.reasoning;
977
+ try {
978
+ saveConfig(this.config);
979
+ }
980
+ catch { }
981
+ }
982
+ }
983
+ else {
984
+ const configuredInEvolclaw = !!this.config.agents?.anthropic?.effort;
985
+ if (configuredInEvolclaw) {
986
+ delete this.config.agents.anthropic.effort;
987
+ try {
988
+ saveConfig(this.config);
989
+ }
990
+ catch { }
991
+ }
992
+ else {
993
+ writeUserSettings({ effortLevel: null });
994
+ }
995
+ }
996
+ return '✓ 推理强度已恢复为 auto (SDK默认)';
997
+ }
998
+ // /effort <level>:切换推理强度
999
+ if (!efforts.includes(args)) {
1000
+ if (allEfforts.includes(args)) {
1001
+ return `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}`;
1002
+ }
1003
+ return `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto`;
1004
+ }
1005
+ const newEffort = args;
1006
+ effortAgent.setEffort?.(newEffort);
1007
+ // 持久化
1008
+ if (!this.config.agents)
1009
+ this.config.agents = {};
1010
+ const isCodex = effortAgent.name === 'codex';
1011
+ if (isCodex) {
1012
+ if (!this.config.agents.openai)
1013
+ this.config.agents.openai = {};
1014
+ this.config.agents.openai.reasoning = newEffort;
1015
+ try {
1016
+ saveConfig(this.config);
1017
+ }
1018
+ catch { }
1019
+ }
1020
+ else {
1021
+ const configuredInEvolclaw = !!(this.config.agents?.anthropic?.model || this.config.agents?.anthropic?.effort);
1022
+ if (configuredInEvolclaw) {
1023
+ if (!this.config.agents.anthropic)
1024
+ this.config.agents.anthropic = {};
1025
+ this.config.agents.anthropic.effort = newEffort;
1026
+ try {
1027
+ saveConfig(this.config);
1028
+ }
1029
+ catch { }
1030
+ }
1031
+ else {
1032
+ writeUserSettings({ effortLevel: newEffort });
1033
+ }
1034
+ }
1035
+ return `✓ 推理强度: ${newEffort}`;
1036
+ }
688
1037
  // /stop 命令:中断当前任务
689
1038
  if (normalizedContent === '/stop') {
690
1039
  const stopResult = await this.ensureSession(channel, channelId, threadId);
@@ -827,7 +1176,7 @@ export class CommandHandler {
827
1176
  }
828
1177
  const lines = [];
829
1178
  if (isAdmin) {
830
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
1179
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
831
1180
  if (health.consecutiveErrors > 0) {
832
1181
  lines.push(`异常计数: ${health.consecutiveErrors}`);
833
1182
  }
@@ -875,6 +1224,9 @@ export class CommandHandler {
875
1224
  timestamp: Date.now()
876
1225
  });
877
1226
  if (session) {
1227
+ // Reset agent backend state so the new
1228
+ // session starts with a fresh conversation history
1229
+ await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
878
1230
  await agent.closeSession(session.id);
879
1231
  }
880
1232
  return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /slist 查看`;
@@ -903,15 +1255,30 @@ export class CommandHandler {
903
1255
  }
904
1256
  // Default: show full system health check
905
1257
  const lines = ['📡 渠道状态:'];
1258
+ // Group by channelType
1259
+ const groups = new Map();
906
1260
  for (const [name] of this.adapters) {
1261
+ const type = this.channelTypeMap.get(name) || name;
907
1262
  const ch = this.channelObjects.get(name);
1263
+ let status;
908
1264
  if (ch?.getStatus) {
909
1265
  const s = ch.getStatus();
910
- const status = s.connected ? '✓ 已连接' : s.reconnectAttempt > 0 ? `⏳ 重连中 (${s.reconnectAttempt}/${s.maxAttempts})` : '✗ 断开';
911
- lines.push(` ${name}: ${status}`);
1266
+ status = s.connected ? '✓ 已连接' : s.reconnectAttempt > 0 ? `⏳ 重连中 (${s.reconnectAttempt}/${s.maxAttempts})` : '✗ 断开';
1267
+ }
1268
+ else {
1269
+ status = '✓ 已注册';
1270
+ }
1271
+ if (!groups.has(type))
1272
+ groups.set(type, []);
1273
+ groups.get(type).push({ name, status });
1274
+ }
1275
+ for (const [type, instances] of groups) {
1276
+ if (instances.length === 1) {
1277
+ lines.push(` ${instances[0].name}: ${instances[0].status}`);
912
1278
  }
913
1279
  else {
914
- lines.push(` ${name}: ✓ 已注册`);
1280
+ const parts = instances.map(i => `${i.name} ${i.status}`);
1281
+ lines.push(` ${type}: [${parts.join(', ')}]`);
915
1282
  }
916
1283
  }
917
1284
  // 队列状态
@@ -939,6 +1306,10 @@ export class CommandHandler {
939
1306
  else {
940
1307
  lines.push(` 处理出错: 0`);
941
1308
  }
1309
+ if (h.toolErrors > 0) {
1310
+ const toolBreakdown = Object.entries(h.toolErrorsByName).map(([t, c]) => `${t}: ${c}`).join(', ');
1311
+ lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
1312
+ }
942
1313
  lines.push(` 被中断: ${h.interrupts}`);
943
1314
  lines.push(` 进入安全模式: ${h.safeModeEntries}`);
944
1315
  if (h.completed > 0) {
@@ -957,6 +1328,33 @@ export class CommandHandler {
957
1328
  const count = this.messageCache.getCount(s.id);
958
1329
  return `${s.projectPath} 有 ${count} 条新消息`;
959
1330
  });
1331
+ // 执行重启逻辑(共用于卡片回调和文本确认)
1332
+ const executeRestart = async () => {
1333
+ let replyContext;
1334
+ if (threadId) {
1335
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
1336
+ replyContext = this.getReplyContext(threadSession);
1337
+ }
1338
+ const restartInfo = {
1339
+ channel,
1340
+ channelId,
1341
+ timestamp: Date.now(),
1342
+ ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
1343
+ };
1344
+ fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
1345
+ const { spawn } = await import('child_process');
1346
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
1347
+ detached: true,
1348
+ stdio: 'ignore',
1349
+ env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
1350
+ }).unref();
1351
+ this.eventBus.publish({ type: 'system:restart', channel, channelId });
1352
+ setTimeout(() => {
1353
+ logger.info('[System] Restarting by user command...');
1354
+ process.exit(0);
1355
+ }, 1000);
1356
+ };
1357
+ // 文本确认流程
960
1358
  if (sessionsWithMessages.length > 0) {
961
1359
  const restartKey = `${channel}-${channelId}`;
962
1360
  const restartConfirmFile = path.join(resolvePaths().dataDir, `restart-confirm-${restartKey}.json`);
@@ -976,30 +1374,7 @@ export class CommandHandler {
976
1374
  return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
977
1375
  }
978
1376
  }
979
- // 话题中 restart 时保存 replyContext 用于重启后回复到话题
980
- let replyContext;
981
- if (threadId) {
982
- const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
983
- replyContext = this.getReplyContext(threadSession);
984
- }
985
- const restartInfo = {
986
- channel,
987
- channelId,
988
- timestamp: Date.now(),
989
- ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
990
- };
991
- fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
992
- const { spawn } = await import('child_process');
993
- spawn('node', [path.join(getPackageRoot(), 'dist', 'cli.js'), 'restart-monitor'], {
994
- detached: true,
995
- stdio: 'ignore',
996
- env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
997
- }).unref();
998
- this.eventBus.publish({ type: 'system:restart', channel, channelId });
999
- setTimeout(() => {
1000
- logger.info('[System] Restarting by user command...');
1001
- process.exit(0);
1002
- }, 1000);
1377
+ await executeRestart();
1003
1378
  return '🔄 服务正在重启,请稍候...(约 5 秒后恢复)';
1004
1379
  }
1005
1380
  // /pwd 命令:显示当前项目路径
@@ -1022,26 +1397,43 @@ export class CommandHandler {
1022
1397
  if (!rawArg) {
1023
1398
  return '用法: /send <相对路径> 或 /send <渠道> <相对路径>\n示例: /send src/index.ts\n示例: /send feishu report.md';
1024
1399
  }
1025
- // 解析目标通道:第一个 token 若匹配已注册通道名则为目标通道
1400
+ // 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
1026
1401
  const tokens = rawArg.split(/\s+/);
1027
- const knownChannels = [...this.adapters.keys()];
1028
1402
  let targetChannel = channel;
1403
+ let targetLabel = channel;
1029
1404
  let filePath = rawArg;
1030
- if (tokens.length >= 2 && knownChannels.includes(tokens[0])) {
1031
- targetChannel = tokens[0];
1032
- filePath = tokens.slice(1).join(' ');
1405
+ if (tokens.length >= 2) {
1406
+ const spec = tokens[0];
1407
+ if (this.adapters.has(spec)) {
1408
+ // 精确实例名
1409
+ targetChannel = spec;
1410
+ targetLabel = spec;
1411
+ filePath = tokens.slice(1).join(' ');
1412
+ }
1413
+ else {
1414
+ // 按 channelType 查找第一个匹配的实例
1415
+ for (const [name] of this.adapters) {
1416
+ if ((this.channelTypeMap.get(name) || name) === spec) {
1417
+ targetChannel = name;
1418
+ targetLabel = spec;
1419
+ filePath = tokens.slice(1).join(' ');
1420
+ break;
1421
+ }
1422
+ }
1423
+ }
1033
1424
  }
1425
+ const isCrossChannel = targetChannel !== channel;
1034
1426
  // 跨通道仅限 owner
1035
- if (targetChannel !== channel && identity.role !== 'owner') {
1427
+ if (isCrossChannel && identity.role !== 'owner') {
1036
1428
  return '❌ 跨通道发送仅限管理员';
1037
1429
  }
1038
1430
  // 找目标 adapter
1039
1431
  const targetAdapter = this.adapters.get(targetChannel);
1040
1432
  if (!targetAdapter) {
1041
- return `❌ 通道 ${targetChannel} 未启用或不存在`;
1433
+ return `❌ 通道 ${targetLabel} 未启用或不存在`;
1042
1434
  }
1043
1435
  if (!targetAdapter.sendFile) {
1044
- return `❌ 通道 ${targetChannel} 不支持文件发送`;
1436
+ return `❌ 通道 ${targetLabel} 不支持文件发送`;
1045
1437
  }
1046
1438
  // 获取 session(需要 projectPath)
1047
1439
  const sendResult = await this.ensureSession(channel, channelId, threadId);
@@ -1076,22 +1468,22 @@ export class CommandHandler {
1076
1468
  }
1077
1469
  // 找目标 channelId
1078
1470
  let targetChannelId = channelId;
1079
- if (targetChannel !== channel) {
1471
+ if (isCrossChannel) {
1080
1472
  const ownerPeerId = getOwner(this.config, targetChannel);
1081
1473
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
1082
1474
  if (!targetChannelId) {
1083
- return `❌ 未找到 ${targetChannel} 的私聊会话,请先在该通道发送一条消息`;
1475
+ return `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`;
1084
1476
  }
1085
1477
  }
1086
1478
  // 发送文件
1087
1479
  try {
1088
- const replyCtx = targetChannel === channel ? this.getReplyContext(sendSession) : undefined;
1480
+ const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
1089
1481
  await targetAdapter.sendFile(targetChannelId, realPath, replyCtx);
1090
1482
  const sizeStr = stat.size < 1024 ? `${stat.size} B`
1091
1483
  : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
1092
1484
  : `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
1093
- return targetChannel !== channel
1094
- ? `📎 文件已通过 ${targetChannel} 发送: ${filePath} (${sizeStr})`
1485
+ return isCrossChannel
1486
+ ? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
1095
1487
  : `✅ 已发送: ${filePath} (${sizeStr})`;
1096
1488
  }
1097
1489
  catch (error) {
@@ -1115,73 +1507,110 @@ export class CommandHandler {
1115
1507
 
1116
1508
  提示:群聊不支持切换项目`;
1117
1509
  }
1118
- const lines = ['可用项目:'];
1119
- // 收集项目信息并按最近活跃排序
1510
+ // 收集项目信息并按最近活跃排序(唯一来源:evolclaw.json projects.list)
1120
1511
  const entries = [];
1121
- const configuredPaths = new Set();
1122
1512
  for (const [name, projectPath] of Object.entries(this.projects)) {
1123
- configuredPaths.add(projectPath);
1124
- const isCurrent = session?.projectPath === projectPath;
1513
+ // 跳过不存在的路径
1514
+ if (!fs.existsSync(projectPath))
1515
+ continue;
1516
+ const isCurrent = session ? path.resolve(session.projectPath) === path.resolve(projectPath) : false;
1125
1517
  const projectSession = await this.sessionManager.getSessionByProjectPath(channel, channelId, projectPath);
1126
1518
  entries.push({
1127
1519
  name, projectPath, projectSession, isCurrent,
1128
1520
  updatedAt: projectSession?.updatedAt ?? 0,
1129
1521
  });
1130
1522
  }
1131
- // Include bound projects not in config (created via /bind)
1132
- const allSessions = await this.sessionManager.listSessions(channel, channelId);
1133
- for (const s of allSessions) {
1134
- if (!configuredPaths.has(s.projectPath)) {
1135
- configuredPaths.add(s.projectPath);
1136
- const isCurrent = session?.projectPath === s.projectPath;
1137
- entries.push({
1138
- name: path.basename(s.projectPath), projectPath: s.projectPath, projectSession: s, isCurrent,
1139
- updatedAt: s.updatedAt ?? 0,
1140
- });
1141
- }
1142
- }
1143
1523
  // 当前活跃项目置顶,其余按 updatedAt 降序
1144
1524
  entries.sort((a, b) => {
1145
1525
  if (a.isCurrent !== b.isCurrent)
1146
1526
  return a.isCurrent ? -1 : 1;
1147
1527
  return b.updatedAt - a.updatedAt;
1148
1528
  });
1149
- for (const { name, projectPath, projectSession, isCurrent } of entries) {
1150
- const prefix = isCurrent ? ' ✓' : ' ';
1151
- if (!projectSession) {
1152
- lines.push(`${prefix} ${name} (${projectPath}) - 无会话`);
1153
- continue;
1154
- }
1155
- const statusParts = [];
1529
+ // 构建项目状态文本的辅助函数
1530
+ const buildStatusText = (entry) => {
1531
+ const { projectSession, isCurrent } = entry;
1532
+ if (!projectSession)
1533
+ return '无会话';
1534
+ const parts = [];
1156
1535
  if (isCurrent) {
1157
- statusParts.push('活跃');
1536
+ parts.push('活跃');
1158
1537
  }
1159
1538
  else {
1160
- const idleMs = Date.now() - projectSession.updatedAt;
1161
- statusParts.push(formatIdleTime(idleMs));
1539
+ parts.push(formatIdleTime(Date.now() - projectSession.updatedAt));
1162
1540
  }
1163
- // 用 DB processingState 判断处理状态
1164
1541
  const isProcessing = !!projectSession.processingState;
1165
1542
  if (isProcessing) {
1166
- const queueLength = this.messageQueue.getQueueLength(projectSession.id);
1167
- if (queueLength > 0) {
1168
- statusParts.push(`[处理中,队列${queueLength}条]`);
1169
- }
1170
- else {
1171
- statusParts.push('[处理中]');
1172
- }
1543
+ const qLen = this.messageQueue.getQueueLength(projectSession.id);
1544
+ parts.push(qLen > 0 ? `[处理中,队列${qLen}条]` : '[处理中]');
1173
1545
  }
1174
- const unreadCount = this.messageCache.getCount(projectSession.id);
1175
- if (unreadCount > 0) {
1176
- statusParts.push(`[${unreadCount}条新消息]`);
1546
+ const unread = this.messageCache.getCount(projectSession.id);
1547
+ if (unread > 0) {
1548
+ parts.push(`[${unread}条新消息]`);
1177
1549
  }
1178
1550
  else if (!isProcessing && !isCurrent) {
1179
- statusParts.push('[空闲]');
1551
+ parts.push('[空闲]');
1180
1552
  }
1181
- lines.push(`${prefix} ${name} (${projectPath}) - ${statusParts.join(' ')}`);
1553
+ return parts.join(' ');
1554
+ };
1555
+ // 尝试发送 ActionInteraction 卡片(每个项目一个按钮,一键切换)
1556
+ if (this.interactionRouter && entries.length > 0) {
1557
+ const requestId = `plist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1558
+ const buttons = entries.map(e => ({
1559
+ key: e.name,
1560
+ label: e.isCurrent ? `✓ ${e.name}` : e.name,
1561
+ style: e.isCurrent ? 'primary' : 'default',
1562
+ }));
1563
+ const bodyLines = entries.map(e => {
1564
+ const status = buildStatusText(e);
1565
+ const prefix = e.isCurrent ? '▶' : '•';
1566
+ return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
1567
+ });
1568
+ const interaction = {
1569
+ type: 'interaction',
1570
+ id: requestId,
1571
+ channelId,
1572
+ sessionId: activeSession?.id || requestId,
1573
+ kind: {
1574
+ kind: 'action',
1575
+ title: '📂 项目列表',
1576
+ body: bodyLines.join('\n'),
1577
+ buttons,
1578
+ },
1579
+ };
1580
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1581
+ const cardSent = await this.sendInteractionCard({
1582
+ channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
1583
+ callback: async (action, _values, operatorId) => {
1584
+ if (userId && operatorId && operatorId !== userId)
1585
+ return;
1586
+ const selectedEntry = entries.find(e => e.name === action);
1587
+ if (selectedEntry && !selectedEntry.isCurrent) {
1588
+ const result = await this.handle(`/project ${action}`, channel, channelId, undefined, userId, threadId);
1589
+ if (result) {
1590
+ const adapter = this.adapters.get(channel);
1591
+ adapter?.sendText(channelId, result, replyCtx);
1592
+ }
1593
+ }
1594
+ },
1595
+ });
1596
+ if (cardSent)
1597
+ return null;
1598
+ }
1599
+ // 降级:文本列表
1600
+ const lines = ['可用项目:'];
1601
+ for (const entry of entries) {
1602
+ const prefix = entry.isCurrent ? ' ✓' : ' ';
1603
+ lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
1182
1604
  }
1183
1605
  return lines.join('\n');
1184
1606
  }
1607
+ // /project(无参数):直接复用 /plist 逻辑(含卡片交互)
1608
+ if (normalizedContent === '/project') {
1609
+ if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
1610
+ // 群聊不能切换项目,交由 /plist 逻辑处理
1611
+ }
1612
+ return this.handle('/plist', channel, channelId, undefined, userId, threadId);
1613
+ }
1185
1614
  // /project 命令:切换项目(支持名称或路径)
1186
1615
  if (normalizedContent.startsWith('/project ')) {
1187
1616
  if (!policy.canSwitchProject(session?.chatType || 'private', identity.role)) {
@@ -1307,8 +1736,10 @@ export class CommandHandler {
1307
1736
  this.projects[projectName] = projectPath;
1308
1737
  return `✓ 已添加项目: ${projectName}\n 路径: ${projectPath}\n\n使用 /p ${projectName} 切换到该项目`;
1309
1738
  }
1310
- // /slist 命令:列出当前项目的所有会话
1311
- if (normalizedContent === '/slist') {
1739
+ // /slist 命令:列出当前项目的会话
1740
+ // /slist 仅 EvolClaw 会话
1741
+ // /slist cli — 仅 CLI 会话(未导入的)
1742
+ if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
1312
1743
  if (!session) {
1313
1744
  return `❌ 当前没有活跃会话
1314
1745
 
@@ -1317,6 +1748,75 @@ export class CommandHandler {
1317
1748
  2. /new [名称] - 创建命名会话
1318
1749
  3. /project <项目> - 切换到指定项目`;
1319
1750
  }
1751
+ const showCliOnly = normalizedContent === '/slist cli';
1752
+ // /slist cli — 仅显示 CLI 会话
1753
+ if (showCliOnly) {
1754
+ const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
1755
+ if (!canImportCli) {
1756
+ return '❌ 当前无权查看 CLI 会话';
1757
+ }
1758
+ const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
1759
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
1760
+ const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
1761
+ const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
1762
+ const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
1763
+ if (orphanCliSessions.length === 0) {
1764
+ return `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话`;
1765
+ }
1766
+ // 构建显示数据(复用于卡片和文本)
1767
+ const cliDisplayItems = orphanCliSessions.map(c => {
1768
+ const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
1769
+ const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
1770
+ const uuid = c.uuid.substring(0, 8);
1771
+ return { uuid, fullUuid: c.uuid, time, message };
1772
+ });
1773
+ // 尝试发送 ActionInteraction 卡片
1774
+ if (this.interactionRouter && cliDisplayItems.length > 0) {
1775
+ const requestId = `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1776
+ const buttons = cliDisplayItems.map(item => ({
1777
+ key: item.uuid,
1778
+ label: item.uuid,
1779
+ style: 'default',
1780
+ }));
1781
+ const bodyLines = cliDisplayItems.map(item => `• ${item.time} (${item.uuid}) "${item.message}"`);
1782
+ const interaction = {
1783
+ type: 'interaction',
1784
+ id: requestId,
1785
+ channelId,
1786
+ sessionId: session.id,
1787
+ kind: {
1788
+ kind: 'action',
1789
+ title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
1790
+ body: bodyLines.join('\n'),
1791
+ buttons,
1792
+ },
1793
+ };
1794
+ const replyCtx = this.getReplyContext(session);
1795
+ const cardSent = await this.sendInteractionCard({
1796
+ channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
1797
+ callback: async (action, _values, operatorId) => {
1798
+ if (userId && operatorId && operatorId !== userId)
1799
+ return;
1800
+ const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
1801
+ if (result) {
1802
+ const adapter = this.adapters.get(channel);
1803
+ adapter?.sendText(channelId, result, replyCtx);
1804
+ }
1805
+ },
1806
+ });
1807
+ if (cardSent)
1808
+ return null;
1809
+ }
1810
+ // 降级:文本列表
1811
+ const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
1812
+ for (const item of cliDisplayItems) {
1813
+ lines.push(` ${item.time} (${item.uuid}) "${item.message}"`);
1814
+ }
1815
+ lines.push('');
1816
+ lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
1817
+ return lines.join('\n');
1818
+ }
1819
+ // /slist — 仅显示 EvolClaw 会话
1320
1820
  const sessions = await this.sessionManager.listSessions(channel, channelId);
1321
1821
  const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
1322
1822
  // 从 SDK 同步会话名称(发现 CLI 改名)
@@ -1335,46 +1835,97 @@ export class CommandHandler {
1335
1835
  catch (error) {
1336
1836
  logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
1337
1837
  }
1338
- const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
1339
- const cliSessions = canImportCli
1340
- ? await this.sessionManager.scanCliSessions(session.projectPath, session.agentId)
1341
- : [];
1342
- const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
1838
+ // 构建可显示会话列表(复用于卡片和文本)
1839
+ const hideTopics = currentProjectSessions.length > 10;
1840
+ const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
1841
+ const maxDisplay = 10;
1842
+ const displaySessions = [];
1843
+ let displayIndex = 0;
1844
+ for (let i = 0; i < currentProjectSessions.length; i++) {
1845
+ const s = currentProjectSessions[i];
1846
+ if (hideTopics && s.threadId)
1847
+ continue;
1848
+ if (displayIndex >= maxDisplay)
1849
+ break;
1850
+ const isActive = s.metadata?.isActive === true;
1851
+ displayIndex++;
1852
+ const name = s.name || '(未命名)';
1853
+ const idleTime = formatIdleTime(Date.now() - s.updatedAt);
1854
+ const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
1855
+ let status = '[空闲]';
1856
+ if (fileMissing) {
1857
+ status = '[会话文件缺失]';
1858
+ }
1859
+ else if (!!s.processingState) {
1860
+ status = '[处理中]';
1861
+ }
1862
+ else if (isActive) {
1863
+ status = '[活跃]';
1864
+ }
1865
+ displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
1866
+ }
1867
+ // 尝试发送 ActionInteraction 卡片(每个会话一个按钮,一键切换)
1868
+ if (this.interactionRouter && displaySessions.length >= 1) {
1869
+ const requestId = `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1870
+ const buttons = displaySessions.map(ds => {
1871
+ const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
1872
+ return {
1873
+ key: String(ds.index),
1874
+ label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
1875
+ style: ds.isActive ? 'primary' : 'default',
1876
+ };
1877
+ });
1878
+ const bodyLines = displaySessions.map(ds => {
1879
+ const prefix = ds.isActive ? '▶' : '•';
1880
+ const threadTag = ds.session.threadId ? '[话题] ' : '';
1881
+ const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
1882
+ const fileMark = ds.fileMissing ? '❌ ' : '';
1883
+ return `${prefix} ${ds.index}. ${threadTag}${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
1884
+ });
1885
+ const interaction = {
1886
+ type: 'interaction',
1887
+ id: requestId,
1888
+ channelId,
1889
+ sessionId: session.id,
1890
+ kind: {
1891
+ kind: 'action',
1892
+ title: `📋 ${path.basename(session.projectPath)} 会话列表`,
1893
+ body: bodyLines.join('\n'),
1894
+ buttons,
1895
+ },
1896
+ };
1897
+ const replyCtx = this.getReplyContext(session);
1898
+ const cardSent = await this.sendInteractionCard({
1899
+ channel, channelId, sessionId: session.id, requestId, interaction, replyCtx,
1900
+ callback: async (action, _values, operatorId) => {
1901
+ if (userId && operatorId && operatorId !== userId)
1902
+ return;
1903
+ const target = displaySessions.find(ds => String(ds.index) === action);
1904
+ if (target && !target.isActive) {
1905
+ const result = await this.handle(`/session ${action}`, channel, channelId, undefined, userId, threadId);
1906
+ if (result) {
1907
+ const adapter = this.adapters.get(channel);
1908
+ adapter?.sendText(channelId, result, replyCtx);
1909
+ }
1910
+ }
1911
+ },
1912
+ });
1913
+ if (cardSent)
1914
+ return null;
1915
+ }
1916
+ // 降级:文本列表
1343
1917
  const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
1344
1918
  if (currentProjectSessions.length > 0) {
1345
- // 超过10个会话时隐藏话题会话(/slist 只能在主会话调用,话题内已禁用)
1346
- const hideTopics = currentProjectSessions.length > 10;
1347
- const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
1348
- const maxDisplay = 10;
1349
- lines.push('【EvolClaw 会话】');
1350
- let displayIndex = 0;
1351
- for (let i = 0; i < currentProjectSessions.length; i++) {
1352
- const s = currentProjectSessions[i];
1353
- if (hideTopics && s.threadId)
1354
- continue;
1355
- if (displayIndex >= maxDisplay)
1356
- break;
1357
- const isActive = s.metadata?.isActive === true;
1358
- displayIndex++;
1359
- const prefix = isActive ? ' ✓' : ' ';
1360
- const num = `${displayIndex}.`;
1361
- const threadTag = s.threadId ? '[话题] ' : '';
1362
- const name = s.name || '(未命名)';
1363
- const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
1364
- const idleTime = formatIdleTime(Date.now() - s.updatedAt);
1365
- if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId)) {
1366
- lines.push(`${prefix} ${num} ${threadTag}❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
1919
+ for (const ds of displaySessions) {
1920
+ const prefix = ds.isActive ? ' ✓' : ' ';
1921
+ const num = `${ds.index}.`;
1922
+ const threadTag = ds.session.threadId ? '[话题] ' : '';
1923
+ const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
1924
+ if (ds.fileMissing) {
1925
+ lines.push(`${prefix} ${num} ${threadTag}❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
1367
1926
  }
1368
1927
  else {
1369
- const sIsProcessing = !!s.processingState;
1370
- let status = '[空闲]';
1371
- if (sIsProcessing) {
1372
- status = '[处理中]';
1373
- }
1374
- else if (isActive) {
1375
- status = '[活跃]';
1376
- }
1377
- lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
1928
+ lines.push(`${prefix} ${num} ${threadTag}${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
1378
1929
  }
1379
1930
  }
1380
1931
  const hiddenCount = currentProjectSessions.length - displayIndex - topicCount;
@@ -1388,29 +1939,19 @@ export class CommandHandler {
1388
1939
  }
1389
1940
  lines.push('');
1390
1941
  }
1391
- const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid)).slice(0, 5);
1392
- if (orphanCliSessions.length > 0) {
1393
- lines.push('【CLI 会话】(最新5个)');
1394
- for (const c of orphanCliSessions) {
1395
- const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
1396
- const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
1397
- const uuid = c.uuid.substring(0, 8);
1398
- lines.push(` ${time} (${uuid}) "${message}"`);
1399
- }
1400
- lines.push('');
1401
- }
1402
1942
  lines.push('使用 /s <序号、name或8位uuid> 切换会话');
1943
+ lines.push('使用 /slist cli 查看 CLI 会话');
1403
1944
  return lines.join('\n');
1404
1945
  }
1946
+ // /session(无参数):直接复用 /slist 逻辑(含卡片交互)
1947
+ if (normalizedContent === '/session') {
1948
+ return this.handle('/slist', channel, channelId, undefined, userId, threadId);
1949
+ }
1405
1950
  // /session 或 /s 命令:切换会话
1406
1951
  if (normalizedContent.startsWith('/session ')) {
1407
1952
  const sessionName = normalizedContent.slice(9).trim();
1408
1953
  if (!sessionName)
1409
1954
  return '用法: /s <序号、会话名称或前8位UUID>';
1410
- const isProcessing = !!session?.processingState;
1411
- if (isProcessing) {
1412
- return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
1413
- }
1414
1955
  let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
1415
1956
  // 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
1416
1957
  if (!targetSession && /^\d+$/.test(sessionName) && session) {
@@ -1586,7 +2127,7 @@ export class CommandHandler {
1586
2127
  return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
1587
2128
  }
1588
2129
  const repairAgent = this.getAgent(repairSession.agentId);
1589
- const { checkSessionFile, backupSessionFile } = await import('../utils/session-file-health.js');
2130
+ const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
1590
2131
  try {
1591
2132
  if (!repairSession.agentSessionId) {
1592
2133
  await this.sessionManager.resetHealthStatus(repairSession.id);