evolclaw 3.2.0 → 3.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 (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -2
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -27
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1069 -141
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +28 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/storage/download.js +1 -1
  11. package/dist/aun/storage/upload.js +13 -1
  12. package/dist/channels/aun.js +406 -293
  13. package/dist/channels/dingtalk.js +77 -140
  14. package/dist/channels/feishu.js +97 -150
  15. package/dist/channels/qqbot.js +75 -138
  16. package/dist/channels/wechat.js +75 -136
  17. package/dist/channels/wecom.js +75 -138
  18. package/dist/cli/agent.js +8 -5
  19. package/dist/cli/index.js +177 -44
  20. package/dist/cli/init.js +33 -6
  21. package/dist/cli/model.js +1 -1
  22. package/dist/cli/stats.js +558 -0
  23. package/dist/cli/version.js +87 -0
  24. package/dist/cli/watch-msg.js +5 -2
  25. package/dist/config-store.js +12 -6
  26. package/dist/core/channel-loader.js +84 -82
  27. package/dist/core/command-handler.js +473 -114
  28. package/dist/core/evolagent-registry.js +1 -0
  29. package/dist/core/evolagent.js +1 -1
  30. package/dist/core/interaction-router.js +8 -0
  31. package/dist/core/message/command-handler-agent-control.js +63 -1
  32. package/dist/core/message/im-renderer.js +35 -13
  33. package/dist/core/message/items-formatter.js +9 -1
  34. package/dist/core/message/message-bridge.js +49 -21
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/message-processor.js +295 -35
  37. package/dist/core/message/message-queue.js +2 -2
  38. package/dist/core/message/pending-hints.js +232 -0
  39. package/dist/core/message/response-depth.js +56 -0
  40. package/dist/core/model/model-catalog.js +1 -1
  41. package/dist/core/model/model-scope.js +2 -2
  42. package/dist/core/permission.js +9 -12
  43. package/dist/core/relation/peer-identity.js +16 -1
  44. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  45. package/dist/core/session/session-manager.js +27 -13
  46. package/dist/core/session/session-title.js +26 -0
  47. package/dist/core/stats/billing.js +151 -0
  48. package/dist/core/stats/budget.js +93 -0
  49. package/dist/core/stats/db.js +314 -0
  50. package/dist/core/stats/eck-vars.js +84 -0
  51. package/dist/core/stats/index.js +10 -0
  52. package/dist/core/stats/normalizer.js +78 -0
  53. package/dist/core/stats/query.js +760 -0
  54. package/dist/core/stats/writer.js +115 -0
  55. package/dist/core/trigger/manager.js +34 -0
  56. package/dist/core/trigger/parser.js +9 -3
  57. package/dist/core/trigger/scheduler.js +20 -17
  58. package/dist/{agents → eck}/manifest-engine.js +20 -1
  59. package/dist/{agents → eck}/message-renderer.js +24 -1
  60. package/dist/index.js +130 -8
  61. package/dist/ipc.js +17 -1
  62. package/dist/utils/cross-platform.js +23 -5
  63. package/dist/utils/ecweb-pair.js +20 -0
  64. package/dist/utils/stats.js +14 -0
  65. package/kits/docs/evolclaw/INDEX.md +3 -1
  66. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  67. package/kits/docs/evolclaw/fs.md +131 -0
  68. package/kits/docs/evolclaw/group-fs.md +209 -0
  69. package/kits/docs/evolclaw/stats.md +70 -0
  70. package/kits/docs/venues/aun-group.md +29 -6
  71. package/kits/docs/venues/group.md +5 -4
  72. package/kits/eck_manifest.json +12 -0
  73. package/kits/eck_message_manifest.json +30 -3
  74. package/kits/rules/05-venue.md +1 -1
  75. package/kits/templates/message-fragments/inject-default.md +2 -0
  76. package/kits/templates/system-fragments/response-depth.md +16 -0
  77. package/package.json +4 -4
  78. package/dist/agents/baseagent-normalize.js +0 -19
  79. package/dist/core/relation/peer-key.js +0 -16
  80. package/dist/evolclaw-config.js +0 -11
  81. package/dist/utils/channel-helpers.js +0 -46
  82. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  83. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_PERMISSION_MODE } from '../types.js';
2
- import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
+ import { hasModelSwitcher, hasPermissionController } from '../agents/runner-types.js';
3
3
  import { getCodexEfforts } from '../agents/codex-runner.js';
4
4
  import { renderCommandCardAsText } from './interaction-router.js';
5
5
  import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
@@ -13,11 +13,13 @@ import { parseTriggerSet, parseTriggerUpdate } from './trigger/parser.js';
13
13
  import { calcNextFireAt } from './trigger/scheduler.js';
14
14
  import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../utils/npm-ops.js';
15
15
  import { tryParseChannelKey } from './channel-loader.js';
16
- import { loadDefaults } from '../config-store.js';
17
- import { loadEvolclawConfig } from '../evolclaw-config.js';
16
+ import { loadDefaults, loadEvolclawConfig } from '../config-store.js';
18
17
  import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './message/command-handler-agent-control.js';
18
+ import { displaySessionTitle } from './session/session-title.js';
19
19
  const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
20
20
  const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
21
+ const PERMISSION_MODE_KEYS = ['auto', 'bypass', 'readonly', 'plan', 'edit', 'request', 'noask'];
22
+ const PERMISSION_MODE_USAGE = PERMISSION_MODE_KEYS.join('|');
21
23
  // ── CLI 透传(menu.action name=cli action=exec)─────────────────────────
22
24
  // 经消息通道的远程命令执行(RCE):仅 owner、白名单内只读+配置命令、无 shell、超时+截断。
23
25
  // command → '*'(全部子命令放行) | Set(允许的子命令)。
@@ -26,6 +28,7 @@ const nonMaxEfforts = allEfforts.filter(e => e !== 'max' && e !== 'xhigh');
26
28
  const CLI_EXEC_WHITELIST = {
27
29
  status: '*',
28
30
  model: '*',
31
+ stats: '*',
29
32
  agent: new Set(['list', 'show', 'get']),
30
33
  aid: new Set(['list', 'show', 'lookup']),
31
34
  storage: new Set(['ls', 'quota']),
@@ -134,8 +137,12 @@ function formatIdleTime(ms) {
134
137
  return `${minutes}分钟前`;
135
138
  return '刚刚';
136
139
  }
140
+ function isAdminRole(role) {
141
+ return role === 'owner' || role === 'admin';
142
+ }
137
143
  // 支持的命令列表
138
- const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
144
+ const commands = ['/new', '/pwd', '/help', '/evolhelp', '/status', '/restart', '/model', '/setmodel', '/effort', '/baseagent', '/slist', '/session', '/rename', '/stop', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/file', '/check', '/rewind', '/activity', '/chatmode', '/dispatch', '/ask', '/resume', '/aid', '/rpc', '/storage', '/agent', '/trigger', '/upgrade'];
145
+ const deprecatedCommands = ['/clear'];
139
146
  // 命令别名映射
140
147
  const aliases = {
141
148
  '/s': '/session',
@@ -463,6 +470,43 @@ export class CommandHandler {
463
470
  accumulateErrors: () => true,
464
471
  };
465
472
  }
473
+ resolveMenuChatType(channel, channelId, explicit) {
474
+ if (explicit)
475
+ return explicit;
476
+ const active = this.sessionManager.getActiveSessionSync(channel, channelId);
477
+ return active?.chatType === 'group' ? 'group' : 'private';
478
+ }
479
+ canReadTopics(role) {
480
+ return role !== 'anonymous';
481
+ }
482
+ canDeleteTopic(role, chatType, topic, userId) {
483
+ if (role === 'anonymous')
484
+ return false;
485
+ if (isAdminRole(role))
486
+ return true;
487
+ if (chatType === 'group')
488
+ return false;
489
+ return !!userId && topic.metadata?.peerId === userId;
490
+ }
491
+ buildTopicMenuItem(s) {
492
+ const displayName = displaySessionTitle(s.name, s.threadId || s.id.slice(0, 8));
493
+ const item = {
494
+ value: s.threadId,
495
+ label: displayName,
496
+ };
497
+ if (s.agentSessionId) {
498
+ item.agentSessionId = s.agentSessionId;
499
+ const fileInfo = this.sessionManager.getSessionFileInfo(s.projectPath, s.agentSessionId, s.agentId);
500
+ if (fileInfo.turns)
501
+ item.turns = fileInfo.turns;
502
+ const firstMsg = this.sessionManager.readSessionFirstMessage(s.projectPath, s.agentSessionId, s.agentId);
503
+ if (firstMsg)
504
+ item.preview = firstMsg.length > 80 ? firstMsg.slice(0, 80) + '...' : firstMsg;
505
+ }
506
+ if (s.updatedAt)
507
+ item.lastActive = s.updatedAt;
508
+ return item;
509
+ }
466
510
  /**
467
511
  * 返回结构化命令菜单(供 menu.query 使用)
468
512
  * owner 看到全部命令,admin 看到管理级命令(不含 owner-only),guest 仅看到用户级命令
@@ -470,9 +514,16 @@ export class CommandHandler {
470
514
  getMenuItems(role, chatType = 'private') {
471
515
  const isOwner = role === 'owner';
472
516
  const isAdmin = role === 'owner' || role === 'admin';
517
+ const canReadTopic = role !== 'anonymous';
473
518
  const items = [];
474
519
  if (!isAdmin && chatType === 'group') {
475
520
  return [
521
+ ...(canReadTopic ? [{
522
+ group: '话题管理',
523
+ commands: [
524
+ { cmd: '/topic', label: '话题管理', desc: '查看当前聊天的话题会话', next: { type: 'select', dynamic: true } },
525
+ ]
526
+ }] : []),
476
527
  {
477
528
  group: '其他',
478
529
  commands: [
@@ -488,6 +539,7 @@ export class CommandHandler {
488
539
  commands: [
489
540
  { cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
490
541
  { cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
542
+ ...(canReadTopic ? [{ cmd: '/topic', label: '话题管理', desc: '查看与管理当前聊天的话题会话', next: { type: 'select', dynamic: true } }] : []),
491
543
  { cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
492
544
  { cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
493
545
  ...(isAdmin ? [
@@ -526,6 +578,7 @@ export class CommandHandler {
526
578
  ...(isOwner ? [
527
579
  { value: 'auto', label: '自动模式', desc: '根据风险等级自动决定是否审批' },
528
580
  { value: 'bypass', label: '免审批模式', desc: '跳过所有工具审批确认' },
581
+ { value: 'readonly', label: '只读模式', desc: '允许读取和临时目录写入,拒绝项目文件修改' },
529
582
  { value: 'plan', label: '计划模式', desc: '仅允许只读操作,写操作需审批' },
530
583
  { value: 'edit', label: '编辑模式', desc: '允许文件编辑,其他操作需审批' },
531
584
  { value: 'request', label: '请求模式', desc: '所有操作均需审批' },
@@ -576,7 +629,7 @@ export class CommandHandler {
576
629
  return items;
577
630
  }
578
631
  /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
579
- async getSubMenuItems(cmd, channel, channelId, userId, args) {
632
+ async getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, _explicitChatType) {
580
633
  const session = await this.sessionManager.getActiveSession(channel, channelId);
581
634
  // ── 进程级 /agent list(owners 鉴权) ──
582
635
  if (cmd === '/agent') {
@@ -595,28 +648,44 @@ export class CommandHandler {
595
648
  if (!manager)
596
649
  return [];
597
650
  const scope = args?.options === 'all' ? 'all' : 'enabled';
598
- const role = this.sessionManager.resolveIdentity(channel, userId).role;
651
+ const role = (overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId)).role;
599
652
  const isAdmin = role === 'owner' || role === 'admin';
600
653
  const all = manager.listAll();
601
654
  const list = scope === 'all' ? all.active.concat(all.history) : manager.listActive();
602
655
  const visible = isAdmin ? list
603
656
  : list.filter((t) => t.createdByPeerId === (userId ?? '') && t.createdByChannel === channel);
604
657
  return visible.map((t) => ({
658
+ // 透传完整 trigger 字段(ECWeb Triggers 表逐列渲染需要)
659
+ ...t,
605
660
  value: t.id,
606
661
  label: t.name,
607
662
  desc: `${t.scheduleType}${t.nextFireAt ? ` | 下次 ${new Date(t.nextFireAt).toLocaleString()}` : ''}`,
663
+ // 状态标识:history 条目带 doneReason(fired/cancelled/expired),active 条目恒为 'active'
664
+ status: t.doneReason ?? 'active',
608
665
  }));
609
666
  }
667
+ if (cmd === '/topic') {
668
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
669
+ if (!this.canReadTopics(identity.role)) {
670
+ throw { code: 'FORBIDDEN', message: '无权限查看话题' };
671
+ }
672
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
673
+ return sessions
674
+ .filter(s => !!s.threadId)
675
+ .map(s => this.buildTopicMenuItem(s));
676
+ }
610
677
  if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
611
678
  const sessions = await this.sessionManager.listSessions(channel, channelId);
612
679
  const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
613
680
  const currentSession = session;
614
681
  const items = sessions
682
+ .filter(s => !s.threadId)
615
683
  .filter(s => !active || s.id !== active.id)
616
684
  .map(s => {
685
+ const displayName = displaySessionTitle(s.name, s.id.slice(0, 8));
617
686
  const item = {
618
687
  value: s.name || s.id.slice(0, 8),
619
- label: s.name || s.id.slice(0, 8),
688
+ label: displayName,
620
689
  selected: currentSession ? s.id === currentSession.id : false,
621
690
  };
622
691
  if (s.agentSessionId) {
@@ -707,7 +776,7 @@ export class CommandHandler {
707
776
  const permAgent = this.getAgent(channel, session?.agentId);
708
777
  const validModes = hasPermissionController(permAgent)
709
778
  ? permAgent.listModes().filter(m => m.available).map(m => m.key)
710
- : ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
779
+ : [...PERMISSION_MODE_KEYS];
711
780
  return validModes.map(m => ({ value: m, label: m, selected: m === currentMode }));
712
781
  }
713
782
  return null;
@@ -730,7 +799,7 @@ export class CommandHandler {
730
799
  return s ? null : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
731
800
  }
732
801
  /** menu.query — 查询当前值。 */
733
- async execMenuQuery(cmd, channel, channelId, userId, args) {
802
+ async execMenuQuery(cmd, channel, channelId, userId, args, _explicitChatType) {
734
803
  const cmdBase = cmd.trim().split(' ')[0];
735
804
  if (!cmdBase)
736
805
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -790,6 +859,54 @@ export class CommandHandler {
790
859
  data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
791
860
  return { data };
792
861
  }
862
+ if (cmdBase === '/topic') {
863
+ const identity = this.sessionManager.resolveIdentity(channel, userId);
864
+ if (!this.canReadTopics(identity.role)) {
865
+ return { error: '无权限查看话题', code: 'FORBIDDEN' };
866
+ }
867
+ const target = (args?.target ?? '').toString().trim();
868
+ if (!target)
869
+ return { error: '缺少 args.target', code: 'MISSING_VALUE' };
870
+ const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
871
+ if (!topic)
872
+ return { error: '话题不存在', code: 'NOT_FOUND' };
873
+ const sessionKey = this.getQueueKey(topic, channel, channelId);
874
+ const sessionAgent = this.getAgent(channel, topic.agentId);
875
+ const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
876
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
877
+ const health = await this.sessionManager.getHealthStatus(topic.id);
878
+ let processingDuration;
879
+ if (isProcessing && topic.processingState) {
880
+ const elapsed = Date.now() - parseInt(topic.processingState, 10);
881
+ if (!isNaN(elapsed) && elapsed > 0)
882
+ processingDuration = Math.floor(elapsed / 1000);
883
+ }
884
+ let turns = 0;
885
+ if (topic.agentSessionId) {
886
+ turns = this.sessionManager.getSessionFileInfo(topic.projectPath, topic.agentSessionId, topic.agentId).turns;
887
+ }
888
+ const data = {
889
+ threadId: topic.threadId,
890
+ name: topic.name || null,
891
+ agentSessionId: topic.agentSessionId || null,
892
+ status: isProcessing ? 'processing' : 'idle',
893
+ createdAt: topic.createdAt,
894
+ updatedAt: topic.updatedAt,
895
+ };
896
+ if (processingDuration !== undefined)
897
+ data.processingDuration = processingDuration;
898
+ if (queueLength > 0)
899
+ data.queueLength = queueLength;
900
+ if (turns > 0)
901
+ data.turns = turns;
902
+ if (health.lastSuccessTime)
903
+ data.lastSuccess = health.lastSuccessTime;
904
+ if (health.consecutiveErrors)
905
+ data.consecutiveErrors = health.consecutiveErrors;
906
+ if (health.lastError)
907
+ data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
908
+ return { data };
909
+ }
793
910
  if (cmdBase === '/baseagent') {
794
911
  const value = session?.agentId ?? evolagent?.config?.active_baseagent ?? null;
795
912
  return { data: { baseagent: value } };
@@ -863,6 +980,13 @@ export class CommandHandler {
863
980
  data.version = pkg.version;
864
981
  }
865
982
  catch { }
983
+ try {
984
+ const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
985
+ const fp2 = JSON.parse(fs.readFileSync(fp, 'utf-8'));
986
+ if (fp2?.version)
987
+ data.fastaunVersion = fp2.version;
988
+ }
989
+ catch { }
866
990
  const channels = owningAgent?.channelInstanceNames?.() ?? [];
867
991
  if (channels.length)
868
992
  data.channels = channels;
@@ -871,7 +995,7 @@ export class CommandHandler {
871
995
  return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
872
996
  }
873
997
  /** menu.update — 写入新值。 */
874
- async execMenuUpdate(cmd, value, channel, channelId, userId) {
998
+ async execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity) {
875
999
  const cmdBase = cmd.trim().split(' ')[0];
876
1000
  if (!cmdBase)
877
1001
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -879,7 +1003,7 @@ export class CommandHandler {
879
1003
  if (!arg)
880
1004
  return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
881
1005
  const { session, evolagent } = await this.loadMenuContext(channel, channelId);
882
- const identity = this.sessionManager.resolveIdentity(channel, userId);
1006
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
883
1007
  // ── 关系级 /trigger update(调度参数,value 为 JSON 字符串) ──
884
1008
  if (cmdBase === '/trigger') {
885
1009
  const owningAgent = this.getOwningAgent(channel);
@@ -1020,7 +1144,7 @@ export class CommandHandler {
1020
1144
  const permAgent = this.getAgent(channel, session.agentId);
1021
1145
  const validModes = hasPermissionController(permAgent)
1022
1146
  ? permAgent.listModes().filter(m => m.available).map(m => m.key)
1023
- : ['auto', 'bypass', 'plan', 'edit', 'request', 'noask'];
1147
+ : [...PERMISSION_MODE_KEYS];
1024
1148
  if (!validModes.includes(arg))
1025
1149
  return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
1026
1150
  const metadata = { ...(session.metadata || {}), permissionMode: arg };
@@ -1052,14 +1176,14 @@ export class CommandHandler {
1052
1176
  return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
1053
1177
  }
1054
1178
  /** menu.action — 触发动词。 */
1055
- async execMenuAction(cmd, action, args, channel, channelId, userId) {
1179
+ async execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType) {
1056
1180
  const cmdBase = cmd.trim().split(' ')[0];
1057
1181
  if (!cmdBase)
1058
1182
  return { error: '缺少命令', code: 'MISSING_CMD' };
1059
1183
  if (!action)
1060
1184
  return { error: '缺少 action', code: 'MISSING_VALUE' };
1061
1185
  const { session } = await this.loadMenuContext(channel, channelId);
1062
- const identity = this.sessionManager.resolveIdentity(channel, userId);
1186
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
1063
1187
  // ── 进程级 /agent(owners 鉴权,不依赖 session/channel) ──
1064
1188
  // NOTE(D5): 本次进程级 /agent 仅按 evolclaw.json owners 鉴权,任意 evolagent 的 AUN
1065
1189
  // channel 均可作为入口。part1(daemon 控制 AID)落地后,应叠加 isControlChannel(channelId)
@@ -1072,6 +1196,19 @@ export class CommandHandler {
1072
1196
  if (action === 'create') {
1073
1197
  a.project = resolveProjectPath(a.project, a.aid ?? '', loadDefaults());
1074
1198
  }
1199
+ // reload / disable / delete 会中断 agent 正在处理的任务,执行前检查是否繁忙。
1200
+ // 队列按 agent 名计数,故先用 registry 把 aid 解析成 name;force 跳过。
1201
+ if ((action === 'reload' || action === 'disable' || action === 'delete') && a.aid && !a.force) {
1202
+ const handle = this.agentRegistry?.get(a.aid) ?? null;
1203
+ const agentName = handle?.name;
1204
+ if (agentName) {
1205
+ const busy = this.messageQueue.getProcessingCountByAgent(agentName)
1206
+ + this.messageQueue.getQueueLengthByAgent(agentName);
1207
+ if (busy > 0) {
1208
+ return { error: `该 Agent 有 ${busy} 个任务执行中`, code: 'BUSY' };
1209
+ }
1210
+ }
1211
+ }
1075
1212
  return await execAgentAction(action, a, userId ?? '');
1076
1213
  }
1077
1214
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
@@ -1126,11 +1263,33 @@ export class CommandHandler {
1126
1263
  return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
1127
1264
  manager.moveToDone(trigger.id, 'cancelled');
1128
1265
  scheduler.cancel(trigger.id);
1129
- this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: userId ?? '' });
1266
+ this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, name: trigger.name, by: userId ?? '' });
1130
1267
  return { data: { id: trigger.id, cancelled: true } };
1131
1268
  }
1132
1269
  return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
1133
1270
  }
1271
+ if (cmdBase === '/topic') {
1272
+ if (action !== 'delete') {
1273
+ return { error: `不支持的 topic action: ${action}`, code: 'NOT_SUPPORTED' };
1274
+ }
1275
+ const target = (args?.target ?? '').toString().trim();
1276
+ if (!target)
1277
+ return { error: '缺少 args.target', code: 'MISSING_VALUE' };
1278
+ const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
1279
+ if (!topic)
1280
+ return { error: '话题不存在', code: 'NOT_FOUND' };
1281
+ const chatType = this.resolveMenuChatType(channel, channelId, explicitChatType);
1282
+ if (!this.canDeleteTopic(identity.role, chatType, topic, userId)) {
1283
+ return { error: '无权限删除话题', code: 'FORBIDDEN' };
1284
+ }
1285
+ const success = await this.sessionManager.unbindSession(topic.id);
1286
+ if (!success)
1287
+ return { error: '删除失败', code: 'DELETE_FAILED' };
1288
+ this.eventBus.publish({ type: 'session:deleted', sessionId: topic.id });
1289
+ const targetAgent = this.getAgent(channel, topic.agentId);
1290
+ await targetAgent.closeSession?.(topic.id);
1291
+ return { data: { deleted: true } };
1292
+ }
1134
1293
  // ── /session 系列 ──
1135
1294
  if (cmdBase === '/session' || cmdBase === '/s') {
1136
1295
  if (action === 'stop') {
@@ -1204,10 +1363,37 @@ export class CommandHandler {
1204
1363
  return { data: { action: 'restart', success: true } };
1205
1364
  }
1206
1365
  if (action === 'check') {
1207
- return await this.delegateAsAction(action, '/check', channel, channelId, userId);
1366
+ const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity });
1367
+ const structured = r.data?.structured ?? null;
1368
+ if (structured)
1369
+ return { data: { ...r.data, ...structured } };
1370
+ return r;
1208
1371
  }
1209
1372
  if (action === 'upgrade') {
1210
- return await this.delegateAsAction(action, '/upgrade', channel, channelId, userId);
1373
+ const devMode = isLinkedInstall();
1374
+ const localEvolclaw = getLocalVersion();
1375
+ // fastaun 本地版本:从 node_modules 读取(与 menu.query name=system 一致)
1376
+ let localFastaun = null;
1377
+ try {
1378
+ const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
1379
+ localFastaun = JSON.parse(fs.readFileSync(fp, 'utf-8'))?.version ?? null;
1380
+ }
1381
+ catch { }
1382
+ const [evolclawRemote, fastaunRemote, ecwebRemote] = await Promise.all([
1383
+ checkLatestVersion('evolclaw'),
1384
+ checkLatestVersion('@agentunion/fastaun'),
1385
+ checkLatestVersion('evolclaw-web'),
1386
+ ]);
1387
+ const cmp = (local, remote) => !!(local && remote && compareVersions(local, remote) < 0);
1388
+ return {
1389
+ data: {
1390
+ devMode,
1391
+ evolclaw: { local: localEvolclaw, remote: evolclawRemote, hasUpdate: cmp(localEvolclaw, evolclawRemote) },
1392
+ fastaun: { local: localFastaun, remote: fastaunRemote, hasUpdate: cmp(localFastaun, fastaunRemote) },
1393
+ // ecweb 本地版本由 ECWeb 进程自身注入(data.ecwebVersion),此处仅给 remote
1394
+ ecweb: { remote: ecwebRemote },
1395
+ },
1396
+ };
1211
1397
  }
1212
1398
  return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
1213
1399
  }
@@ -1232,6 +1418,79 @@ export class CommandHandler {
1232
1418
  }
1233
1419
  return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
1234
1420
  }
1421
+ /** ECWeb 专用入口:注入 owner identity,进程级操作检查 owners 非空。不暴露 cli。 */
1422
+ async execMenuForEcweb(payload) {
1423
+ const id = payload?.id ?? '';
1424
+ const name = payload?.name;
1425
+ if (name === 'cli' || payload?.cmd === '/cli') {
1426
+ return { type: 'menu.response', id, name, error: { code: 'NOT_SUPPORTED', message: 'cli 不在 ECWeb 控制范围' } };
1427
+ }
1428
+ const isProcessLevel = name === 'system' || name === 'agent';
1429
+ const owners = loadEvolclawConfig().owners ?? [];
1430
+ if (isProcessLevel && owners.length === 0) {
1431
+ return { type: 'menu.response', id, name, error: { code: 'FORBIDDEN', message: '请在 evolclaw.json 配置 owners 后使用进程级操作' } };
1432
+ }
1433
+ const ECWEB_CHANNEL = '__ecweb__';
1434
+ // payload.agent(aid 或 name)时,用该 agent 的首个 channel 实例作为 channel 参数,
1435
+ // 让 execMenuQuery / execMenuUpdate 能按真实 agent 解析 model/effort/perm 等会话级配置。
1436
+ // system / agent 两个进程级 name 不走此路径,仍用 ECWEB_CHANNEL。
1437
+ const agentChannelKey = (() => {
1438
+ if (!payload?.agent || isProcessLevel)
1439
+ return ECWEB_CHANNEL;
1440
+ const handle = this.agentRegistry?.get(payload.agent) ?? null;
1441
+ return handle?.channelInstanceNames()?.[0] ?? ECWEB_CHANNEL;
1442
+ })();
1443
+ const ownerIdentity = { role: 'owner', mode: 'interactive' };
1444
+ // 进程级操作用 owners[0] 让 isProcessLevelOwner() 通过;其余传 undefined
1445
+ const userId = isProcessLevel ? (owners[0] ?? '') : undefined;
1446
+ const nameMap = {
1447
+ pwd: '/pwd', session: '/session', baseagent: '/baseagent', model: '/model',
1448
+ topic: '/topic',
1449
+ effort: '/effort', chatmode: '/chatmode', dispatch: '/dispatch',
1450
+ permission: '/perm', activity: '/activity', system: '/system',
1451
+ agent: '/agent', trigger: '/trigger',
1452
+ };
1453
+ const cmd = name ? (nameMap[name] ?? payload.cmd) : payload.cmd;
1454
+ try {
1455
+ switch (payload?.type) {
1456
+ case 'menu.list':
1457
+ return { type: 'menu.response', id, data: this.getMenuItems('owner', 'private') };
1458
+ case 'menu.query': {
1459
+ if (!cmd)
1460
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
1461
+ const r = await this.execMenuQuery(cmd, agentChannelKey, agentChannelKey, userId, payload.args);
1462
+ return ecwebResp(id, name, r);
1463
+ }
1464
+ case 'menu.options': {
1465
+ if (!cmd)
1466
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
1467
+ const data = await this.getSubMenuItems(cmd, agentChannelKey, agentChannelKey, userId, payload.args, ownerIdentity) ?? [];
1468
+ return { type: 'menu.response', id, name, data };
1469
+ }
1470
+ case 'menu.update': {
1471
+ if (!cmd)
1472
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
1473
+ if (!payload.value)
1474
+ return ecwebErr(id, name, 'MISSING_VALUE', '缺少 value');
1475
+ const r = await this.execMenuUpdate(cmd, payload.value, agentChannelKey, agentChannelKey, userId, ownerIdentity);
1476
+ return ecwebResp(id, name, r);
1477
+ }
1478
+ case 'menu.action': {
1479
+ if (!cmd)
1480
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
1481
+ if (!payload.action)
1482
+ return ecwebErr(id, name, 'MISSING_VALUE', '缺少 action');
1483
+ const r = await this.execMenuAction(cmd, payload.action, payload.args, agentChannelKey, agentChannelKey, userId, ownerIdentity);
1484
+ return ecwebResp(id, name, r);
1485
+ }
1486
+ default:
1487
+ return ecwebErr(id, name, 'NOT_SUPPORTED', `未知类型: ${payload?.type}`);
1488
+ }
1489
+ }
1490
+ catch (e) {
1491
+ return ecwebErr(id, name, 'INTERNAL', e?.message ?? String(e));
1492
+ }
1493
+ }
1235
1494
  /**
1236
1495
  * CLI 透传执行:spawn `node dist/cli/index.js <argv>` 子进程,捕获输出回传。
1237
1496
  * 不 in-process 调用(CLI handler 用 console.log + process.exit,spawn 行为与终端一致且隔离)。
@@ -1304,7 +1563,7 @@ export class CommandHandler {
1304
1563
  /** 把 menu.action 委派给已有 slash 命令处理逻辑,把 OutboundPayload 包成结构化结果。 */
1305
1564
  async delegateAsAction(action, slashCmd, channel, channelId, userId, opts = {}) {
1306
1565
  try {
1307
- const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId);
1566
+ const result = await this._handleInternal(slashCmd, channel, channelId, undefined, userId, undefined, undefined, undefined, undefined, undefined, opts.overrideIdentity);
1308
1567
  if (result == null) {
1309
1568
  // null / undefined: 命令未识别或前置守卫拦截(如 idle 检查),视为失败
1310
1569
  return { error: '命令未执行(可能被前置守卫拦截)', code: 'EXEC_FAILED' };
@@ -1319,6 +1578,8 @@ export class CommandHandler {
1319
1578
  const data = { action, success: true };
1320
1579
  if (payload.text)
1321
1580
  data.message = payload.text;
1581
+ if (payload.structured)
1582
+ data.structured = payload.structured;
1322
1583
  // 对于切换/创建类动作,附加切换后的活跃 session 信息便于客户端继续操作
1323
1584
  if (opts.enrichSession) {
1324
1585
  const newSession = await this.sessionManager.getActiveSession(channel, channelId);
@@ -1344,13 +1605,13 @@ export class CommandHandler {
1344
1605
  const result = await this._handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID);
1345
1606
  return result;
1346
1607
  }
1347
- async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID) {
1608
+ async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity) {
1348
1609
  // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
1349
1610
  // 不应覆盖 session 中已有的正确值
1350
1611
  if (source === 'card-trigger')
1351
1612
  chatType = undefined;
1352
1613
  // 解析身份(按实例名)
1353
- const identity = this.sessionManager.resolveIdentity(channel, userId);
1614
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
1354
1615
  const policy = this.getPolicy(channel);
1355
1616
  // 按当前会话选择 agent 后端
1356
1617
  const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
@@ -1403,12 +1664,12 @@ export class CommandHandler {
1403
1664
  }
1404
1665
  // 空闲检查:某些命令需要等待当前会话空闲
1405
1666
  // 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
1406
- // - 始终需要 idle(无参即写):/clear /compact /repair /fork /new
1667
+ // - 始终需要 idle(无参即写):/compact /repair /fork /new
1407
1668
  // - 仅带参时需要 idle(无参是列表/用法):/session /baseagent /rewind
1408
1669
  // - /chatmode:在 handler 内部自行做写操作的 idle 检查
1409
1670
  // - /dispatch:在 handler 内部自行做写操作的 idle 检查
1410
1671
  // - /safe:已禁用 no-op,不再要求 idle
1411
- const idleAlways = ['/clear', '/compact', '/repair', '/fork', '/new'];
1672
+ const idleAlways = ['/compact', '/repair', '/fork', '/new'];
1412
1673
  const idleWhenArg = ['/session', '/baseagent', '/rewind'];
1413
1674
  const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
1414
1675
  idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
@@ -1436,7 +1697,8 @@ export class CommandHandler {
1436
1697
  // 检查是否以 / 开头(可能是命令)
1437
1698
  if (normalizedContent.startsWith('/')) {
1438
1699
  const inputCmd = normalizedContent.split(' ')[0];
1439
- const isValidCommand = commands.some(cmd => normalizedContent.startsWith(cmd));
1700
+ const isValidCommand = commands.some(cmd => normalizedContent.startsWith(cmd)) ||
1701
+ deprecatedCommands.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '));
1440
1702
  if (!isValidCommand) {
1441
1703
  const similar = commands.find(cmd => {
1442
1704
  const distance = levenshteinDistance(inputCmd, cmd);
@@ -1450,7 +1712,8 @@ export class CommandHandler {
1450
1712
  }
1451
1713
  }
1452
1714
  }
1453
- const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd));
1715
+ const isCmd = commands.some(cmd => normalizedContent.startsWith(cmd)) ||
1716
+ deprecatedCommands.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '));
1454
1717
  if (!isCmd)
1455
1718
  return undefined;
1456
1719
  // /help 命令不需要会话
@@ -1511,7 +1774,7 @@ export class CommandHandler {
1511
1774
  '',
1512
1775
  '🔐 权限管理:',
1513
1776
  ' /perm - 查看当前权限模式',
1514
- ...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
1777
+ ...(isOwner ? [` /perm <${PERMISSION_MODE_USAGE}> - 切换权限模式`] : []),
1515
1778
  ' /perm allow|always|deny - 审批权限请求',
1516
1779
  '',
1517
1780
  '🛠️ 运维:',
@@ -1558,7 +1821,7 @@ export class CommandHandler {
1558
1821
  }
1559
1822
  // 权限管理
1560
1823
  if (isAdmin) {
1561
- cmds.push({ command: '/perm', args: isOwner ? '<auto|bypass|request|edit|plan|noask>' : undefined, description: '查看当前权限模式', category: '权限管理', roles: ['admin', 'owner'] });
1824
+ cmds.push({ command: '/perm', args: isOwner ? `<${PERMISSION_MODE_USAGE}>` : undefined, description: '查看当前权限模式', category: '权限管理', roles: ['admin', 'owner'] });
1562
1825
  cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
1563
1826
  }
1564
1827
  // 运维
@@ -1690,11 +1953,11 @@ export class CommandHandler {
1690
1953
  }
1691
1954
  }
1692
1955
  // 不是已知模式名也不是 allow/deny
1693
- const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
1956
+ const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : PERMISSION_MODE_USAGE;
1694
1957
  return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny` };
1695
1958
  }
1696
1959
  // 双参数不再支持,提示正确用法
1697
- const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : 'auto|bypass|request|edit|plan|noask';
1960
+ const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : PERMISSION_MODE_USAGE;
1698
1961
  return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny` };
1699
1962
  }
1700
1963
  // /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
@@ -1864,7 +2127,7 @@ export class CommandHandler {
1864
2127
  const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
1865
2128
  const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
1866
2129
  const projectName = this.getProjectName(session.projectPath);
1867
- let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${newSession.name || '(未命名)'}\n ${hasExistingSession}`;
2130
+ let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${displaySessionTitle(newSession.name, '(未命名)')}\n ${hasExistingSession}`;
1868
2131
  if (source === 'card-trigger')
1869
2132
  return null;
1870
2133
  return { kind: 'command.result', text: agentSwitchResponse };
@@ -2352,7 +2615,8 @@ export class CommandHandler {
2352
2615
  const sessionKey = stopSession.id;
2353
2616
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
2354
2617
  const hasActive = stopAgent.hasActiveStream(sessionKey);
2355
- if (queueLength === 0 && !hasActive) {
2618
+ const isProcessing = this.messageQueue.isProcessing(sessionKey);
2619
+ if (queueLength === 0 && !hasActive && !isProcessing) {
2356
2620
  return { kind: 'command.result', text: '当前没有正在处理的任务' };
2357
2621
  }
2358
2622
  await stopAgent.interrupt(sessionKey);
@@ -2367,37 +2631,10 @@ export class CommandHandler {
2367
2631
  this.sessionManager.clearProcessing(sessionKey);
2368
2632
  return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
2369
2633
  }
2370
- // /clear 命令:通过 SDK /clear 清空会话历史
2634
+ // /clear 已移除:Claude/Codex/Gemini 对“清空当前 backend 历史”的语义不一致。
2635
+ // 统一使用 /new 创建新会话来开始全新上下文。
2371
2636
  if (normalizedContent === '/clear') {
2372
- const result = await this.ensureSession(channel, channelId, threadId, chatType);
2373
- if ('error' in result)
2374
- return { kind: 'command.error', text: result.error };
2375
- const { session } = result;
2376
- const sessionAgent = this.getAgent(channel, session.agentId);
2377
- if (!sessionAgent.capabilities?.clear) {
2378
- return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /clear\n\n可使用 /new 创建新会话替代` };
2379
- }
2380
- if (!session.agentSessionId) {
2381
- return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需清空' };
2382
- }
2383
- const projectPath = path.isAbsolute(session.projectPath)
2384
- ? session.projectPath
2385
- : path.resolve(process.cwd(), session.projectPath);
2386
- const releaseLock = this.messageQueue.acquireLock(session.id);
2387
- try {
2388
- const cleared = await sessionAgent.clearSession(session.id, session.agentSessionId, projectPath);
2389
- if (cleared) {
2390
- await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
2391
- sessionAgent.updateSessionId(session.id, '');
2392
- return { kind: 'command.result', text: '✅ 已清空当前会话的对话历史' };
2393
- }
2394
- else {
2395
- return { kind: 'command.error', text: '❌ 清空会话失败,请稍后重试' };
2396
- }
2397
- }
2398
- finally {
2399
- releaseLock();
2400
- }
2637
+ return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
2401
2638
  }
2402
2639
  // /compact 命令:手动压缩会话上下文
2403
2640
  if (normalizedContent === '/compact') {
@@ -2422,7 +2659,10 @@ export class CommandHandler {
2422
2659
  }
2423
2660
  const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
2424
2661
  if (compacted) {
2425
- return { kind: 'command.result', text: '✅ 会话上下文已压缩' };
2662
+ return {
2663
+ kind: 'command.result',
2664
+ text: '✅ 会话压缩完成',
2665
+ };
2426
2666
  }
2427
2667
  else {
2428
2668
  return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
@@ -2491,7 +2731,7 @@ export class CommandHandler {
2491
2731
  const chatModeLine = `会话模式: ${sessionMode}`;
2492
2732
  const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
2493
2733
  if (isAdmin) {
2494
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`);
2734
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${displaySessionTitle(session.name, '(未命名)')}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`);
2495
2735
  if (health.consecutiveErrors > 0) {
2496
2736
  lines.push(`异常计数: ${health.consecutiveErrors}`);
2497
2737
  }
@@ -2517,6 +2757,9 @@ export class CommandHandler {
2517
2757
  }
2518
2758
  }
2519
2759
  const projectPath = this.getEffectiveDefaultPath(channel);
2760
+ if (sendMessage && session) {
2761
+ await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
2762
+ }
2520
2763
  const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.primaryRunnerKey);
2521
2764
  this.eventBus.publish({
2522
2765
  type: 'session:created',
@@ -2539,12 +2782,16 @@ export class CommandHandler {
2539
2782
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
2540
2783
  const subCmd = normalizedContent.slice('/check'.length).trim();
2541
2784
  // 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
2542
- // default 通道也仅显示 default 的渠道(不再展示 evolagents 的渠道)
2785
+ // __ecweb__ ECWeb 系统级入口,展示全量渠道
2543
2786
  const checkOwningAgent = this.getOwningAgent(channel);
2544
2787
  let allowedChannels;
2545
2788
  if (checkOwningAgent) {
2546
2789
  allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
2547
2790
  }
2791
+ else if (channel === '__ecweb__') {
2792
+ // ECWeb 全局视图:展示所有渠道
2793
+ allowedChannels = new Set(this.adapters.keys());
2794
+ }
2548
2795
  else {
2549
2796
  // default 范围:不再有 default channel 概念,等价于"所有 channel"
2550
2797
  const defaultNames = [];
@@ -2632,7 +2879,29 @@ export class CommandHandler {
2632
2879
  lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
2633
2880
  }
2634
2881
  }
2635
- return { kind: 'command.result', text: lines.join('\n') };
2882
+ const checkSnap = this.statsCollector?.getSnapshot(currentAgentName);
2883
+ const structured = {
2884
+ channels: [...groups.entries()].map(([type, instances]) => ({ type, instances })),
2885
+ queue: {
2886
+ pending: this.messageQueue.getQueueLengthByAgent(currentAgentName),
2887
+ processing: this.messageQueue.getProcessingCountByAgent(currentAgentName),
2888
+ },
2889
+ uptimeMs,
2890
+ lastHour: checkSnap?.lastHour ?? null,
2891
+ evolagents: this.agentRegistry?.list().map((ag) => ({
2892
+ name: ag.name, aid: ag.aid ?? '', status: ag.status,
2893
+ baseagent: ag.baseagent ?? null,
2894
+ activeTasks: this.messageQueue.getProcessingCountByAgent(ag.name)
2895
+ + this.messageQueue.getQueueLengthByAgent(ag.name),
2896
+ error: ag.error,
2897
+ })) ?? [],
2898
+ baseagents: [...this.agentMap.entries()].map(([key, runner]) => ({
2899
+ name: key.split('::')[1] ?? key,
2900
+ activeStreams: runner.activeStreamCount?.() ?? 0,
2901
+ healthy: true,
2902
+ })),
2903
+ };
2904
+ return { kind: 'command.result', text: lines.join('\n'), structured };
2636
2905
  }
2637
2906
  // /restart 命令:重启服务(owner only)
2638
2907
  if (normalizedContent === '/restart') {
@@ -2873,7 +3142,7 @@ export class CommandHandler {
2873
3142
  }
2874
3143
  const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
2875
3144
  const sessions = await this.sessionManager.listSessions(channel, channelId);
2876
- const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
3145
+ const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
2877
3146
  const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
2878
3147
  const orphanCliSessions = cliSessions.filter(c => !dbSessionIds.has(c.uuid));
2879
3148
  if (orphanCliSessions.length === 0) {
@@ -2923,7 +3192,7 @@ export class CommandHandler {
2923
3192
  }
2924
3193
  // /slist — 仅显示 EvolClaw 会话
2925
3194
  const sessions = await this.sessionManager.listSessions(channel, channelId);
2926
- const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId?.startsWith('trigger-'));
3195
+ const currentProjectSessions = sessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
2927
3196
  // 从 SDK 同步会话名称(发现 CLI 改名)
2928
3197
  try {
2929
3198
  const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
@@ -2941,20 +3210,16 @@ export class CommandHandler {
2941
3210
  logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
2942
3211
  }
2943
3212
  // 构建可显示会话列表(复用于卡片和文本)
2944
- const hideTopics = currentProjectSessions.length > 10;
2945
- const topicCount = hideTopics ? currentProjectSessions.filter(s => s.threadId).length : 0;
2946
3213
  const maxDisplay = 10;
2947
3214
  const displaySessions = [];
2948
3215
  let displayIndex = 0;
2949
3216
  for (let i = 0; i < currentProjectSessions.length; i++) {
2950
3217
  const s = currentProjectSessions[i];
2951
- if (hideTopics && s.threadId)
2952
- continue;
2953
3218
  if (displayIndex >= maxDisplay)
2954
3219
  break;
2955
3220
  const isActive = s.metadata?.isActive === true;
2956
3221
  displayIndex++;
2957
- const name = s.name || '(未命名)';
3222
+ const name = displaySessionTitle(s.name, '(未命名)');
2958
3223
  const idleTime = formatIdleTime(Date.now() - s.updatedAt);
2959
3224
  const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
2960
3225
  let status = '[空闲]';
@@ -2973,10 +3238,9 @@ export class CommandHandler {
2973
3238
  if (this.interactionRouter && displaySessions.length >= 1) {
2974
3239
  const bodyLines = displaySessions.map(ds => {
2975
3240
  const prefix = ds.isActive ? '✓' : '•';
2976
- const threadTag = ds.session.threadId ? '[话题] ' : '';
2977
3241
  const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
2978
3242
  const fileMark = ds.fileMissing ? '❌ ' : '';
2979
- return `${prefix} ${ds.index}. ${threadTag}${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
3243
+ return `${prefix} ${ds.index}. ${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
2980
3244
  });
2981
3245
  const interaction = {
2982
3246
  type: 'interaction',
@@ -3011,22 +3275,19 @@ export class CommandHandler {
3011
3275
  for (const ds of displaySessions) {
3012
3276
  const prefix = ds.isActive ? ' ✓' : ' ';
3013
3277
  const num = `${ds.index}.`;
3014
- const threadTag = ds.session.threadId ? '[话题] ' : '';
3015
3278
  const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
3016
3279
  if (ds.fileMissing) {
3017
- lines.push(`${prefix} ${num} ${threadTag}❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
3280
+ lines.push(`${prefix} ${num} ❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
3018
3281
  }
3019
3282
  else {
3020
- lines.push(`${prefix} ${num} ${threadTag}${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
3283
+ lines.push(`${prefix} ${num} ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
3021
3284
  }
3022
3285
  }
3023
- const hiddenCount = currentProjectSessions.length - displayIndex - topicCount;
3024
- if (topicCount > 0 || hiddenCount > 0) {
3286
+ const hiddenCount = currentProjectSessions.length - displayIndex;
3287
+ if (hiddenCount > 0) {
3025
3288
  const parts = [];
3026
3289
  if (hiddenCount > 0)
3027
3290
  parts.push(`${hiddenCount} 个更早的会话`);
3028
- if (topicCount > 0)
3029
- parts.push(`${topicCount} 个话题会话`);
3030
3291
  lines.push(`\n (已隐藏 ${parts.join('、')})`);
3031
3292
  }
3032
3293
  lines.push('');
@@ -3055,12 +3316,7 @@ export class CommandHandler {
3055
3316
  if (!targetSession && /^\d+$/.test(sessionName) && session) {
3056
3317
  const idx = parseInt(sessionName, 10);
3057
3318
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
3058
- const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
3059
- // 与 /slist 显示逻辑一致:超过10个时隐藏非活跃话题会话
3060
- const hideTopics = projectSessions.length > 10;
3061
- const visibleSessions = hideTopics
3062
- ? projectSessions.filter(s => !s.threadId)
3063
- : projectSessions;
3319
+ const visibleSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
3064
3320
  if (idx >= 1 && idx <= visibleSessions.length) {
3065
3321
  targetSession = visibleSessions[idx - 1];
3066
3322
  }
@@ -3071,6 +3327,9 @@ export class CommandHandler {
3071
3327
  if (!targetSession && sessionName.length >= 8) {
3072
3328
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
3073
3329
  }
3330
+ if (targetSession?.threadId) {
3331
+ return { kind: 'command.error', text: `❌ 话题会话不支持通过 /s 切换\n请在对应话题内继续对话` };
3332
+ }
3074
3333
  const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
3075
3334
  if (!targetSession && sessionName.length >= 8 && canImport) {
3076
3335
  const projectPaths = Object.values(this.projects);
@@ -3085,7 +3344,7 @@ export class CommandHandler {
3085
3344
  const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
3086
3345
  this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
3087
3346
  const projectName = this.getProjectName(projectPath);
3088
- return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${imported.name}\n 项目: ${projectName}\n 将继续之前的对话历史` };
3347
+ return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${displaySessionTitle(imported.name, '(未命名)')}\n 项目: ${projectName}\n 将继续之前的对话历史` };
3089
3348
  }
3090
3349
  }
3091
3350
  }
@@ -3103,10 +3362,10 @@ export class CommandHandler {
3103
3362
  }
3104
3363
  if (source === 'card-trigger')
3105
3364
  return null;
3106
- return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
3365
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
3107
3366
  }
3108
3367
  if (targetSession.id === session.id) {
3109
- return { kind: 'command.result', text: `当前已在会话: ${targetSession.name || sessionName}` };
3368
+ return { kind: 'command.result', text: `当前已在会话: ${displaySessionTitle(targetSession.name, sessionName)}` };
3110
3369
  }
3111
3370
  // 阻止从主会话切换到话题会话
3112
3371
  if (!session.threadId && targetSession.threadId) {
@@ -3120,7 +3379,7 @@ export class CommandHandler {
3120
3379
  const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
3121
3380
  if (source === 'card-trigger')
3122
3381
  return null;
3123
- return { kind: 'command.result', text: `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}` };
3382
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}${continueHint}${lastInputLine}` };
3124
3383
  }
3125
3384
  // /rename 或 /name 命令:重命名当前会话
3126
3385
  if (normalizedContent === '/rename' || normalizedContent === '/name') {
@@ -3142,8 +3401,14 @@ export class CommandHandler {
3142
3401
  if (existing && existing.id !== session.id) {
3143
3402
  return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
3144
3403
  }
3145
- const oldName = session.name || '(未命名)';
3404
+ const oldName = displaySessionTitle(session.name, '(未命名)');
3146
3405
  const success = await this.sessionManager.renameSession(session.id, newName);
3406
+ if (success && session.agentSessionId) {
3407
+ const renameAgent = this.getAgent(channel, session.agentId);
3408
+ await renameAgent.setSessionName?.(session.agentSessionId, newName).catch(error => {
3409
+ logger.debug('[CommandHandler] Backend session rename sync failed:', error);
3410
+ });
3411
+ }
3147
3412
  if (!success) {
3148
3413
  return { kind: 'command.error', text: `❌ 重命名失败` };
3149
3414
  }
@@ -3167,11 +3432,7 @@ export class CommandHandler {
3167
3432
  if (!targetSession && /^\d+$/.test(sessionName)) {
3168
3433
  const idx = parseInt(sessionName, 10);
3169
3434
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
3170
- const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId);
3171
- const hideTopics = projectSessions.length > 10;
3172
- const visibleSessions = hideTopics
3173
- ? projectSessions.filter(s => !s.threadId)
3174
- : projectSessions;
3435
+ const visibleSessions = allSessions.filter(s => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
3175
3436
  if (idx >= 1 && idx <= visibleSessions.length) {
3176
3437
  targetSession = visibleSessions[idx - 1];
3177
3438
  }
@@ -3182,6 +3443,9 @@ export class CommandHandler {
3182
3443
  if (!targetSession && sessionName.length >= 8) {
3183
3444
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
3184
3445
  }
3446
+ if (targetSession?.threadId) {
3447
+ return { kind: 'command.error', text: `❌ 请使用话题管理删除话题会话` };
3448
+ }
3185
3449
  if (!targetSession) {
3186
3450
  return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
3187
3451
  }
@@ -3195,7 +3459,7 @@ export class CommandHandler {
3195
3459
  this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
3196
3460
  const targetAgent = this.getAgent(channel, targetSession.agentId);
3197
3461
  await targetAgent.closeSession(targetSession.id);
3198
- return { kind: 'command.result', text: `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问` };
3462
+ return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
3199
3463
  }
3200
3464
  // /fork 命令:分支当前会话
3201
3465
  if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
@@ -3213,8 +3477,19 @@ export class CommandHandler {
3213
3477
  try {
3214
3478
  const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
3215
3479
  const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
3480
+ await forkAgent.updateSessionMetadata?.(forkedSessionId, {
3481
+ gitInfo: {
3482
+ branch: null,
3483
+ commitHash: null,
3484
+ repositoryUrl: null,
3485
+ },
3486
+ evolclawSessionId: newSession.id,
3487
+ sourceSessionId: session.id,
3488
+ }).catch(error => {
3489
+ logger.debug('[CommandHandler] Backend fork metadata sync failed:', error);
3490
+ });
3216
3491
  this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
3217
- return { kind: 'command.result', text: `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
3492
+ return { kind: 'command.result', text: `✅ 会话已分支: ${displaySessionTitle(newSession.name, '(未命名)')}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
3218
3493
  }
3219
3494
  catch (error) {
3220
3495
  logger.error('[CommandHandler] Fork session failed:', error);
@@ -3228,14 +3503,11 @@ export class CommandHandler {
3228
3503
  return { kind: 'command.error', text: result.error };
3229
3504
  const { session } = result;
3230
3505
  const rewindAgent = this.getAgent(channel, session.agentId);
3231
- if (rewindAgent.name !== 'claude') {
3232
- return { kind: 'command.error', text: '❌ /rewind 仅支持 Claude 后端' };
3233
- }
3234
3506
  if (!session.agentSessionId) {
3235
3507
  return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
3236
3508
  }
3237
3509
  if (!rewindAgent.getSessionMessages) {
3238
- return { kind: 'command.error', text: '❌ 当前 Agent 不支持 /rewind' };
3510
+ return { kind: 'command.error', text: `❌ 当前 Agent (${rewindAgent.name}) 不支持 /rewind` };
3239
3511
  }
3240
3512
  const args = normalizedContent.slice('/rewind'.length).trim();
3241
3513
  if (!args) {
@@ -3379,7 +3651,7 @@ export class CommandHandler {
3379
3651
  }
3380
3652
  manager.moveToDone(trigger.id, 'cancelled');
3381
3653
  scheduler.cancel(trigger.id);
3382
- this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, by: peerId });
3654
+ this.eventBus.publish({ type: 'trigger:cancelled', triggerId: trigger.id, name: trigger.name, by: peerId });
3383
3655
  return `✅ 触发器已取消:**${trigger.name}**`;
3384
3656
  }
3385
3657
  // /trigger update <name|id> [--参数...]
@@ -3410,6 +3682,38 @@ export class CommandHandler {
3410
3682
  const now = Date.now();
3411
3683
  patch.nextFireAt = calcNextFireAt(patch.scheduleType, patch.scheduleValue, now);
3412
3684
  }
3685
+ // 跨渠道迁移:改了 targetChannel 时必须同步重算 targetChannelType,
3686
+ // 并按 session 策略重新绑定执行会话(与 set 路径保持一致,否则 trigger
3687
+ // 仍按旧 channelType 路由 / 仍绑在旧渠道的 boundSessionId 上)。
3688
+ const effectiveChannel = patch.targetChannel ?? trigger.targetChannel;
3689
+ const effectiveChannelId = patch.targetChannelId ?? trigger.targetChannelId;
3690
+ if (patch.targetChannel) {
3691
+ patch.targetChannelType = this.resolveChannelType(patch.targetChannel);
3692
+ }
3693
+ // 解析最终生效的 session 策略(patch 优先,否则沿用原 trigger)
3694
+ const effStrategy = patch.targetSessionStrategy ?? trigger.targetSessionStrategy;
3695
+ // 渠道或策略变化时,按策略重新绑定会话
3696
+ if (patch.targetChannel || patch.targetSessionStrategy) {
3697
+ if (effStrategy === 'current') {
3698
+ if (patch.targetChannel && patch.targetChannel !== trigger.targetChannel) {
3699
+ return '❌ 跨渠道不支持 --session current,请改用 latest 或 thread';
3700
+ }
3701
+ const active = await this.sessionManager.getActiveSession(effectiveChannel, effectiveChannelId);
3702
+ if (!active)
3703
+ return '❌ 目标渠道当前没有活跃会话,改用 --session latest 或先在该渠道发一条消息';
3704
+ patch.boundSessionId = active.id;
3705
+ }
3706
+ else if (effStrategy === 'thread') {
3707
+ const adapter = this.adapters.get(effectiveChannel);
3708
+ if (!adapter?.capabilities.thread)
3709
+ return '❌ 目标渠道不支持 thread 会话';
3710
+ }
3711
+ else {
3712
+ // latest 策略:清除旧的 boundSessionId(若有),按渠道动态取最新会话
3713
+ if (trigger.boundSessionId)
3714
+ patch.boundSessionId = undefined;
3715
+ }
3716
+ }
3413
3717
  let updated;
3414
3718
  try {
3415
3719
  updated = manager.update(trigger.id, patch);
@@ -3450,15 +3754,26 @@ export class CommandHandler {
3450
3754
  const nextFireAt = calcNextFireAt(parsed.scheduleType, parsed.scheduleValue, now);
3451
3755
  // Auto-generate name if not provided
3452
3756
  const name = parsed.name ?? `trigger-${Date.now().toString(36)}`;
3757
+ // Validate target channel exists
3758
+ const targetChannelName = parsed.targetChannel ?? channel;
3759
+ if (parsed.targetChannel && !this.adapters.has(parsed.targetChannel)) {
3760
+ return { ok: false, error: `目标渠道不存在或未启用:${parsed.targetChannel}` };
3761
+ }
3762
+ // Validate channelId format for AUN: must look like an AID (contains '.')
3763
+ const targetChannelType = this.resolveChannelType(targetChannelName);
3764
+ const targetChannelId = parsed.targetChannelId ?? channelId;
3765
+ if (targetChannelType === 'aun' && parsed.targetChannelId && !parsed.targetChannelId.includes('.')) {
3766
+ return { ok: false, error: `AUN 渠道的 --channelid 必须是 AID 格式(如 user.agentid.pub),收到:"${parsed.targetChannelId}"` };
3767
+ }
3453
3768
  const trigger = {
3454
3769
  id: crypto.randomUUID(),
3455
3770
  name,
3456
3771
  scheduleType: parsed.scheduleType,
3457
3772
  scheduleValue: parsed.scheduleValue,
3458
3773
  nextFireAt,
3459
- targetChannel: parsed.targetChannel ?? channel,
3460
- targetChannelId: parsed.targetChannelId ?? channelId,
3461
- targetChannelType: this.resolveChannelType(parsed.targetChannel ?? channel),
3774
+ targetChannel: targetChannelName,
3775
+ targetChannelId,
3776
+ targetChannelType,
3462
3777
  targetThreadId: parsed.targetThreadId,
3463
3778
  targetSessionStrategy: parsed.targetSessionStrategy,
3464
3779
  agentId: parsed.agentId,
@@ -3466,12 +3781,16 @@ export class CommandHandler {
3466
3781
  createdByPeerId: peerId,
3467
3782
  createdByChannel: channel,
3468
3783
  fireCount: 0,
3784
+ failCount: 0,
3469
3785
  createdAt: now,
3470
3786
  updatedAt: now,
3471
3787
  };
3472
3788
  try {
3473
3789
  // Strategy-based session binding
3474
3790
  if (parsed.targetSessionStrategy === 'current') {
3791
+ if (parsed.targetChannel && parsed.targetChannel !== channel) {
3792
+ return { ok: false, error: '跨渠道不支持 --session current,请改用 latest 或 thread' };
3793
+ }
3475
3794
  const active = await this.sessionManager.getActiveSession(channel, channelId);
3476
3795
  if (!active)
3477
3796
  return { ok: false, error: '当前没有活跃会话,改用 --session latest 或 thread' };
@@ -3556,12 +3875,26 @@ export class CommandHandler {
3556
3875
  const detail = fileResult.filesChanged
3557
3876
  ? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
3558
3877
  : '';
3559
- results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
3878
+ if (agent.capabilities?.fileRewind === 'git-head') {
3879
+ results.push(`✅ 已按 Git HEAD 恢复文件${detail}(Codex 当前不提供逐轮文件快照)`);
3880
+ }
3881
+ else {
3882
+ results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
3883
+ }
3560
3884
  }
3561
3885
  }
3562
- // 对话回退(延迟执行 下次发消息时生效)
3886
+ // 对话回退:Codex app-server 可直接 rollback;Claude 走 resumeAt 延迟到下次消息生效。
3563
3887
  if (mode === 'chat' || mode === 'all') {
3564
- if (keepTarget) {
3888
+ const discarded = turns.length - turnNum + 1;
3889
+ if (agent.rollbackSessionTurns) {
3890
+ const ok = await agent.rollbackSessionTurns(session.agentSessionId, session.projectPath, discarded);
3891
+ if (!ok)
3892
+ return '❌ 对话回退失败';
3893
+ const meta = { ...(session.metadata || {}) };
3894
+ delete meta.resumeAt;
3895
+ await this.sessionManager.updateSession(session.id, { metadata: meta });
3896
+ }
3897
+ else if (keepTarget) {
3565
3898
  const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
3566
3899
  await this.sessionManager.updateSession(session.id, { metadata: meta });
3567
3900
  }
@@ -3574,10 +3907,6 @@ export class CommandHandler {
3574
3907
  agentSessionId: null,
3575
3908
  });
3576
3909
  }
3577
- const discarded = turns.length - turnNum + 1;
3578
- const keepDesc = keepTarget
3579
- ? `回退到第 ${turnNum - 1} 轮:"${keepTarget.userContent}"`
3580
- : '已清空全部对话历史';
3581
3910
  results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
3582
3911
  }
3583
3912
  this.eventBus.publish({
@@ -3833,7 +4162,27 @@ export class CommandHandler {
3833
4162
  else if (Array.isArray(m?.content)) {
3834
4163
  text = m.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ');
3835
4164
  }
3836
- text = text.trim().replace(/\s+/g, ' ');
4165
+ text = text.trim();
4166
+ // Strip injection wrappers before previewing (outermost first):
4167
+ // 1. Interrupt wrapper: 【新消息插入】\n...\n【请无视之前中断继续处理】
4168
+ text = text.replace(/^【新消息插入】\s*/, '').replace(/\s*【请无视之前中断继续处理】$/, '').trim();
4169
+ // 2. Current format: ‹metadata›\ncontent (message-renderer item.md)
4170
+ if (text.startsWith('‹')) {
4171
+ const nl = text.indexOf('\n');
4172
+ if (nl !== -1)
4173
+ text = text.slice(nl + 1).trim();
4174
+ }
4175
+ // 3. Legacy XML format: <messages><message sender="..." time="...">content</message></messages>
4176
+ if (text.startsWith('<messages>')) {
4177
+ const parts = [];
4178
+ const re = /<message(?:\s[^>]*)?>([\s\S]*?)<\/message>/g;
4179
+ let match;
4180
+ while ((match = re.exec(text)) !== null)
4181
+ parts.push(match[1].trim());
4182
+ if (parts.length > 0)
4183
+ text = parts.join(' ');
4184
+ }
4185
+ text = text.replace(/\s+/g, ' ').trim();
3837
4186
  if (!text)
3838
4187
  return '';
3839
4188
  return text.length > 50 ? text.substring(0, 50) + '…' : text;
@@ -3846,6 +4195,16 @@ export function isProcessLevelOwner(peerId, owners) {
3846
4195
  return false;
3847
4196
  return (owners ?? []).includes(peerId);
3848
4197
  }
4198
+ /** ECWeb menu.response 错误信封。 */
4199
+ function ecwebErr(id, name, code, message) {
4200
+ return { type: 'menu.response', id, ...(name ? { name } : {}), error: { code, message } };
4201
+ }
4202
+ /** 把 execMenu* 的 {data}|{error} 结果转成 menu.response。 */
4203
+ function ecwebResp(id, name, result) {
4204
+ return 'error' in result
4205
+ ? { type: 'menu.response', id, ...(name ? { name } : {}), error: { code: result.code ?? 'EXEC_FAILED', message: result.error } }
4206
+ : { type: 'menu.response', id, ...(name ? { name } : {}), data: result.data };
4207
+ }
3849
4208
  /** 校验 menu 路径直传的 trigger 调度参数(绕过 parseTriggerSet 文本解析后必须自校验)。
3850
4209
  * 返回错误字符串表示非法;返回 null 表示通过。
3851
4210
  * 防止非法 scheduleType/scheduleValue 传到 calcNextFireAt 产出 NaN/throw,污染 scheduler heap。 */