evolclaw 2.3.0 → 2.5.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.
@@ -1,5 +1,5 @@
1
1
  import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
2
- import { saveConfig, resolvePaths, getPackageRoot, getOwner } from '../config.js';
2
+ import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import crypto from 'crypto';
5
5
  import path from 'path';
@@ -22,10 +22,10 @@ function formatModelUsage(agent, model) {
22
22
  const efforts = getAvailableEfforts(agent, model);
23
23
  const lines = [
24
24
  '用法:',
25
- ' /model <model> 切换模型',
25
+ ' /model <模型> 切换模型',
26
26
  ];
27
27
  if (efforts.length > 0) {
28
- lines.push(' /model <model> <effort> 切换模型+推理强度');
28
+ lines.push(' /model <模型> <强度> 切换模型+推理强度');
29
29
  lines.push(' /effort [level] 查看或切换推理强度');
30
30
  }
31
31
  return lines.join('\n');
@@ -103,15 +103,16 @@ function formatIdleTime(ms) {
103
103
  return '刚刚';
104
104
  }
105
105
  // 支持的命令列表
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'];
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', '/rewind', '/activity', '/agentmd'];
107
107
  // 命令别名映射
108
108
  const aliases = {
109
109
  '/p': '/project',
110
110
  '/s': '/session',
111
- '/name': '/rename'
111
+ '/name': '/rename',
112
+ '/rw': '/rewind'
112
113
  };
113
114
  // 命令快速路径前缀(所有命令都不进入消息队列)
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 '];
115
+ 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 ', '/rewind', '/rw', '/rw ', '/activity'];
115
116
  export class CommandHandler {
116
117
  sessionManager;
117
118
  config;
@@ -237,15 +238,8 @@ export class CommandHandler {
237
238
  return false;
238
239
  const wrappedCallback = async (action, values, operatorId) => {
239
240
  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
- }
241
+ // 已完成交互的卡片:保留原始内容,仅禁用按钮(不标记为"已过期")
242
+ // "已过期"仅用于被新卡片替代的旧卡片(invalidateOldCards)
249
243
  };
250
244
  this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
251
245
  return true;
@@ -322,33 +316,44 @@ export class CommandHandler {
322
316
  }
323
317
  /**
324
318
  * 返回结构化命令菜单(供 menu.query 使用)
325
- * admin 看到全部命令分组,guest 仅看到用户级命令
319
+ * owner 看到全部命令,admin 看到管理级命令(不含 owner-only),guest 仅看到用户级命令
326
320
  */
327
- getMenuItems(isAdmin) {
321
+ getMenuItems(role, chatType = 'private') {
322
+ const isOwner = role === 'owner';
323
+ const isAdmin = role === 'owner' || role === 'admin';
328
324
  const items = [];
325
+ if (!isAdmin && chatType === 'group') {
326
+ return [
327
+ {
328
+ group: '其他',
329
+ commands: [
330
+ { cmd: '/status', label: '显示会话状态' },
331
+ { cmd: '/check', label: '检查渠道健康' },
332
+ { cmd: '/help', label: '显示帮助信息' },
333
+ ]
334
+ }
335
+ ];
336
+ }
329
337
  if (isAdmin) {
330
338
  items.push({
331
339
  group: '项目管理',
332
340
  commands: [
333
341
  { cmd: '/pwd', label: '显示当前项目路径' },
334
- { cmd: '/plist', label: '列出所有配置的项目' },
335
- { cmd: '/p', args: '<name|path>', label: '切换项目' },
336
- { cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
342
+ { cmd: '/p', args: '[name|path]', label: '列出或切换项目' },
343
+ ...(isOwner ? [{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' }] : []),
337
344
  ]
338
345
  });
339
346
  }
340
347
  items.push({
341
348
  group: '会话管理',
342
349
  commands: [
343
- { cmd: '/new', args: '[name]', label: '创建新会话' },
344
- { cmd: '/slist', label: '列出当前项目的所有会话' },
345
- { cmd: '/slist', args: 'cli', label: '列出 CLI 会话(未导入的)' },
346
- { cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
350
+ { cmd: '/new', args: '[name]', label: '创建新会话(清空历史请用此命令)' },
351
+ { cmd: '/s', args: '[cli|name|index|uuid]', label: '列出或切换会话(cli 查看未导入的 CLI 会话)' },
347
352
  { cmd: '/name', args: '<name>', label: '重命名当前会话' },
348
353
  { cmd: '/del', args: '<name>', label: '删除指定会话' },
349
354
  ...(isAdmin ? [
350
355
  { cmd: '/fork', args: '[name]', label: '分支当前会话' },
351
- { cmd: '/clear', label: '清空会话对话历史' },
356
+ { cmd: '/rewind', args: '[N] [chat|file|all]', label: '查看历史/撤销指定轮次 (别名: /rw)' },
352
357
  { cmd: '/compact', label: '压缩会话上下文' },
353
358
  ] : []),
354
359
  ]
@@ -365,7 +370,7 @@ export class CommandHandler {
365
370
  items.push({
366
371
  group: '权限管理',
367
372
  commands: [
368
- { cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
373
+ { cmd: '/perm', args: isOwner ? '[mode|allow|always|deny]' : '[allow|always|deny]', label: '权限模式管理' },
369
374
  ]
370
375
  });
371
376
  items.push({
@@ -373,11 +378,16 @@ export class CommandHandler {
373
378
  commands: [
374
379
  { cmd: '/status', label: '显示会话状态' },
375
380
  { cmd: '/stop', label: '中断当前任务' },
376
- { cmd: '/restart', label: '重启服务' },
377
- { cmd: '/repair', label: '检查并修复会话' },
378
- { cmd: '/safe', label: '进入安全模式' },
379
- { cmd: '/send', args: '[渠道] <path>', label: '发送项目内文件' },
380
- { cmd: '/check', args: '[rty <channel>]', label: '检查渠道状态或重连指定渠道' },
381
+ { cmd: '/check', label: '检查渠道状态' },
382
+ { cmd: '/activity', args: '[all|dm|owner|none]', label: '查看/控制中间输出显示模式' },
383
+ ...(isAdmin ? [
384
+ { cmd: '/restart', args: '<channel>', label: '重连指定渠道' },
385
+ ] : []),
386
+ ...(isOwner ? [
387
+ { cmd: '/restart', label: '重启服务' },
388
+ { cmd: '/send', args: '[channel] <path>', label: '发送项目内文件' },
389
+ { cmd: '/agentmd', args: '[put|set <内容>]', label: '管理 agent.md' },
390
+ ] : []),
381
391
  ]
382
392
  });
383
393
  }
@@ -386,6 +396,7 @@ export class CommandHandler {
386
396
  group: '其他',
387
397
  commands: [
388
398
  { cmd: '/status', label: '显示会话状态' },
399
+ { cmd: '/check', label: '检查渠道健康' },
389
400
  ]
390
401
  });
391
402
  }
@@ -433,16 +444,23 @@ export class CommandHandler {
433
444
  }
434
445
  }
435
446
  // 权限检查:区分用户级命令和管理级命令
436
- const isAdmin = identity.role === 'owner';
447
+ const isOwner = identity.role === 'owner';
448
+ const isAdmin = identity.role === 'owner' || identity.role === 'admin';
449
+ const activeChatType = activeSession?.chatType || 'private';
437
450
  if (normalizedContent.startsWith('/')) {
438
- const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
451
+ const guestGroupCommands = ['/status', '/help', '/check'];
452
+ const userCommands = activeChatType === 'group' && !isAdmin
453
+ ? guestGroupCommands
454
+ : ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
439
455
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
440
456
  if (!isUserCommand && !isAdmin) {
441
- return '❌ 无权限:此命令仅限管理员使用';
457
+ return activeChatType === 'group'
458
+ ? '❌ 无权限:当前群聊仅支持 /status 和 /help'
459
+ : '❌ 无权限:此命令仅限管理员使用';
442
460
  }
443
461
  }
444
462
  // 空闲检查:某些命令需要等待当前会话空闲
445
- const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
463
+ const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent', '/rewind'];
446
464
  if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
447
465
  if (threadId) {
448
466
  // 话题中:检查话题 session 是否在处理(不创建)
@@ -480,41 +498,50 @@ export class CommandHandler {
480
498
  return undefined;
481
499
  // /help 命令不需要会话
482
500
  if (normalizedContent === '/help') {
501
+ if (!isAdmin && activeChatType === 'group') {
502
+ const lines = [
503
+ '可用命令:',
504
+ '',
505
+ '其他:',
506
+ ' /status - 显示会话状态',
507
+ ' /check - 检查渠道健康',
508
+ ' /help - 显示此帮助信息',
509
+ ];
510
+ return lines.join('\n');
511
+ }
483
512
  if (!isAdmin) {
484
513
  const lines = [
485
514
  '可用命令:',
486
515
  '',
487
516
  '🔄 会话管理:',
488
- ' /new [名称] - 创建新会话(可选命名)',
489
- ' /slist - 列出当前项目的所有会话',
490
- ' /slist cli - 列出 CLI 会话(未导入的)',
491
- ' /s, /session <名称|序号|uuid> - 切换到指定会话',
492
- ' /name, /rename <新名称> - 重命名当前会话',
517
+ ' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
518
+ ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
519
+ ' /name <新名称> - 重命名当前会话',
493
520
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
494
521
  ' /status - 显示会话状态',
522
+ ' /check - 检查渠道健康',
495
523
  '',
496
524
  '❓ 帮助:',
497
525
  ' /help - 显示此帮助信息',
498
526
  ];
499
527
  return lines.join('\n');
500
528
  }
529
+ // admin+ 基础命令
501
530
  const lines = [
502
531
  '可用命令:',
503
532
  '',
504
533
  '📁 项目管理:',
505
534
  ' /pwd - 显示当前项目路径',
506
- ' /plist - 列出所有配置的项目',
507
- ' /p, /project <name|path> - 切换项目',
508
- ' /bind <path> - 绑定新项目目录',
535
+ ' /p [name|path] - 列出或切换项目',
536
+ ...(isOwner ? [' /bind <path> - 绑定新项目目录'] : []),
509
537
  '',
510
538
  '🔄 会话管理:',
511
- ' /new [名称] - 创建新会话(可选命名)',
512
- ' /slist - 列出当前项目的所有会话',
513
- ' /s, /session <名称> - 切换到指定会话',
514
- ' /name, /rename <新名称> - 重命名当前会话',
539
+ ' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
540
+ ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
541
+ ' /name <新名称> - 重命名当前会话',
515
542
  ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
516
543
  ' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
517
- ' /clear - 清空当前会话的对话历史',
544
+ ' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
518
545
  ' /compact - 压缩会话上下文(减少 token 用量)',
519
546
  '',
520
547
  '🤖 Agent 与模型:',
@@ -524,16 +551,22 @@ export class CommandHandler {
524
551
  '',
525
552
  '🔐 权限管理:',
526
553
  ' /perm - 查看当前权限模式',
527
- ' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
554
+ ...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
528
555
  ' /perm allow|always|deny - 审批权限请求',
529
556
  '',
530
557
  '🛠️ 运维:',
531
558
  ' /status - 显示会话状态',
532
559
  ' /stop - 中断当前任务',
533
- ' /restart - 重启服务',
534
- ' /repair - 检查并修复会话',
535
- ' /safe - 进入安全模式',
536
- ' /send [渠道] <路径> - 发送项目内文件',
560
+ ' /check - 检查渠道状态',
561
+ ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
562
+ ...(isAdmin ? [
563
+ ' /restart <channel> - 重连指定渠道',
564
+ ] : []),
565
+ ...(isOwner ? [
566
+ ' /restart - 重启服务',
567
+ ' /send [channel] <path> - 发送项目内文件',
568
+ ' /agentmd [put|set <内容>] - 管理 agent.md',
569
+ ] : []),
537
570
  '',
538
571
  '❓ 帮助:',
539
572
  ' /help - 显示此帮助信息',
@@ -554,7 +587,8 @@ export class CommandHandler {
554
587
  if (!hasPermissionController(permAgent)) {
555
588
  return '❌ 权限控制不可用';
556
589
  }
557
- const currentMode = permSession.metadata?.permissionMode ?? 'bypass';
590
+ const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'readonly';
591
+ const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
558
592
  const modes = permAgent.listModes();
559
593
  // 尝试发送交互卡片
560
594
  if (this.interactionRouter) {
@@ -568,7 +602,7 @@ export class CommandHandler {
568
602
  kind: {
569
603
  kind: 'action',
570
604
  title: '🔐 权限模式',
571
- body: availableModes.map(m => `${m.key === currentMode ? '' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
605
+ body: availableModes.map(m => `${m.key === currentMode ? '' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
572
606
  buttons: availableModes.map(m => ({
573
607
  key: m.key,
574
608
  label: m.key === currentMode ? `✓ ${m.key}` : m.key,
@@ -596,7 +630,7 @@ export class CommandHandler {
596
630
  }
597
631
  // 降级:文本
598
632
  const modeList = modes.map(m => {
599
- const prefix = m.key === currentMode ? '' : ' ';
633
+ const prefix = m.key === currentMode ? '' : ' ';
600
634
  const suffix = m.available ? '' : ' ⚠️ 不可用';
601
635
  return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
602
636
  }).join('\n');
@@ -636,9 +670,9 @@ export class CommandHandler {
636
670
  if (!matched.available) {
637
671
  return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
638
672
  }
639
- // guest 用户只能保持 readonly 模式
640
- if (identity.role !== 'owner' && arg !== 'readonly') {
641
- return '❌ 当前身份无法切换权限模式';
673
+ // guest admin 用户不能切换权限模式(仅 owner)
674
+ if (!isOwner) {
675
+ return '❌ 权限模式切换仅限 owner';
642
676
  }
643
677
  const metadata = permSession.metadata || {};
644
678
  metadata.permissionMode = arg;
@@ -655,8 +689,9 @@ export class CommandHandler {
655
689
  return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
656
690
  }
657
691
  // /agent 命令:查看或切换 Agent 后端
658
- if (normalizedContent.startsWith('/agent')) {
659
- if (!isAdmin)
692
+ if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
693
+ // 群聊中 owner only,私聊中 admin+
694
+ if (activeChatType === 'group' ? !isOwner : !isAdmin)
660
695
  return '❌ 无权限:此命令仅限管理员使用';
661
696
  const args = normalizedContent.slice(6).trim();
662
697
  const available = [...this.agentMap.keys()];
@@ -774,7 +809,7 @@ export class CommandHandler {
774
809
  return null;
775
810
  }
776
811
  // 降级:文本
777
- const modelList = models.map((m) => `- ${m}`).join('\n');
812
+ const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
778
813
  const effortHint = efforts.length > 0
779
814
  ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
780
815
  : '';
@@ -798,7 +833,7 @@ export class CommandHandler {
798
833
  newModel = arg;
799
834
  }
800
835
  else {
801
- const modelList = models.map((m) => `- ${m}`).join('\n');
836
+ const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
802
837
  const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
803
838
  return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
804
839
  }
@@ -964,8 +999,9 @@ export class CommandHandler {
964
999
  }
965
1000
  // 降级:文本
966
1001
  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>`;
1002
+ const allItems = [...efforts, 'auto'];
1003
+ const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
1004
+ return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
969
1005
  }
970
1006
  // /effort auto:恢复 SDK 默认
971
1007
  if (args === 'auto') {
@@ -1034,6 +1070,151 @@ export class CommandHandler {
1034
1070
  }
1035
1071
  return `✓ 推理强度: ${newEffort}`;
1036
1072
  }
1073
+ // /activity 命令:控制中间输出显示模式
1074
+ if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
1075
+ if (!isOwner)
1076
+ return '❌ 无权限:此命令仅限 owner 使用';
1077
+ const adapter = this.adapters.get(channel);
1078
+ if (!adapter?.uploadAgentMd)
1079
+ return '❌ 当前通道不支持 agent.md 操作';
1080
+ const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
1081
+ const arg = normalizedContent.slice(9).trim();
1082
+ // put — read local ~/.aun/AIDs/{aid}/agent.md and upload
1083
+ if (arg === 'put') {
1084
+ if (!selfAid)
1085
+ return '❌ 未连接,无法确定本地 AID';
1086
+ try {
1087
+ const { readFileSync } = await import('node:fs');
1088
+ const { join } = await import('node:path');
1089
+ const { homedir } = await import('node:os');
1090
+ const localPath = join(homedir(), '.aun', 'AIDs', selfAid, 'agent.md');
1091
+ const content = readFileSync(localPath, 'utf-8');
1092
+ await adapter.uploadAgentMd(content);
1093
+ return '✅ agent.md 已发布';
1094
+ }
1095
+ catch (e) {
1096
+ return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
1097
+ }
1098
+ }
1099
+ // set <content> — upload inline content and sync to local
1100
+ if (arg.startsWith('set ')) {
1101
+ const content = arg.slice(4).trim();
1102
+ if (!content)
1103
+ return '用法:/agentmd set <内容>';
1104
+ if (!selfAid)
1105
+ return '❌ 未连接,无法确定本地 AID';
1106
+ try {
1107
+ await adapter.uploadAgentMd(content);
1108
+ const { writeFileSync, mkdirSync } = await import('node:fs');
1109
+ const { join } = await import('node:path');
1110
+ const { homedir } = await import('node:os');
1111
+ const localDir = join(homedir(), '.aun', 'AIDs', selfAid);
1112
+ mkdirSync(localDir, { recursive: true });
1113
+ writeFileSync(join(localDir, 'agent.md'), content, 'utf-8');
1114
+ return '✅ agent.md 已更新并发布到AUN网络';
1115
+ }
1116
+ catch (e) {
1117
+ return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
1118
+ }
1119
+ }
1120
+ // view — /agentmd or /agentmd <aid>
1121
+ const aidToView = arg || selfAid;
1122
+ if (!aidToView)
1123
+ return '用法:/agentmd [<aid>] | put | set <内容>';
1124
+ try {
1125
+ const md = await adapter.downloadAgentMd(aidToView);
1126
+ if (!md || !md.trim())
1127
+ return `ℹ️ ${aidToView} 尚未设置 agent.md`;
1128
+ return `\`\`\`\n${md.slice(0, 1500)}\n\`\`\``;
1129
+ }
1130
+ catch (e) {
1131
+ const msg = String(e.message || e);
1132
+ if (msg.includes('not found') || msg.includes('404')) {
1133
+ return `ℹ️ ${aidToView} 尚未设置 agent.md`;
1134
+ }
1135
+ return `❌ 获取失败: ${msg.slice(0, 100)}`;
1136
+ }
1137
+ }
1138
+ if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
1139
+ if (!isAdmin)
1140
+ return '❌ 无权限:此命令仅限管理员使用';
1141
+ const activityArg = normalizedContent.slice(9).trim();
1142
+ const modeMap = {
1143
+ all: 'all',
1144
+ dm: 'dm-only',
1145
+ owner: 'owner-dm-only',
1146
+ none: 'none',
1147
+ };
1148
+ const currentMode = getChannelShowActivities(this.config, channel);
1149
+ // 模式描述列表(用于 body 和文本降级)
1150
+ const modeDescriptions = [
1151
+ { key: 'all', configVal: 'all', label: '全部显示' },
1152
+ { key: 'dm', configVal: 'dm-only', label: '仅私聊显示' },
1153
+ { key: 'owner', configVal: 'owner-dm-only', label: '仅 owner 私聊显示' },
1154
+ { key: 'none', configVal: 'none', label: '全部静默' },
1155
+ ];
1156
+ if (!activityArg) {
1157
+ // 无参数:显示当前模式 + Action 卡片
1158
+ if (this.interactionRouter) {
1159
+ const requestId = `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
1160
+ const body = modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n');
1161
+ const buttons = modeDescriptions.map(m => ({
1162
+ key: m.key,
1163
+ label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
1164
+ style: m.configVal === currentMode ? 'primary' : 'default',
1165
+ }));
1166
+ const interaction = {
1167
+ type: 'interaction',
1168
+ id: requestId,
1169
+ channelId,
1170
+ sessionId: activeSession?.id || requestId,
1171
+ kind: {
1172
+ kind: 'action',
1173
+ title: '📋 中间输出模式',
1174
+ body,
1175
+ buttons,
1176
+ },
1177
+ };
1178
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1179
+ const cardSent = await this.sendInteractionCard({
1180
+ channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
1181
+ callback: async (action, _values, operatorId) => {
1182
+ const newMode = modeMap[action];
1183
+ if (newMode && newMode !== currentMode) {
1184
+ if (userId && operatorId && operatorId !== userId)
1185
+ return;
1186
+ const result = await this.handle(`/activity ${action}`, channel, channelId, undefined, userId, threadId);
1187
+ if (result) {
1188
+ const adapter = this.adapters.get(channel);
1189
+ adapter?.sendText(channelId, result, replyCtx);
1190
+ }
1191
+ }
1192
+ },
1193
+ });
1194
+ if (cardSent)
1195
+ return null;
1196
+ }
1197
+ // 降级:文本
1198
+ const modeList = modeDescriptions.map(m => {
1199
+ const prefix = m.configVal === currentMode ? '✓' : ' ';
1200
+ return ` ${prefix} ${m.key} (${m.label})`;
1201
+ }).join('\n');
1202
+ return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
1203
+ }
1204
+ const newMode = modeMap[activityArg];
1205
+ if (!newMode) {
1206
+ return `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none`;
1207
+ }
1208
+ const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
1209
+ if (newMode === currentMode) {
1210
+ return `📋 中间输出模式已是 ${activityArg}(${label})`;
1211
+ }
1212
+ // 切换操作仅 owner
1213
+ if (!isOwner)
1214
+ return '❌ 中间输出模式切换仅限 owner';
1215
+ setChannelShowActivities(this.config, channel, newMode);
1216
+ return `✅ 中间输出模式: ${activityArg}(${label})`;
1217
+ }
1037
1218
  // /stop 命令:中断当前任务
1038
1219
  if (normalizedContent === '/stop') {
1039
1220
  const stopResult = await this.ensureSession(channel, channelId, threadId);
@@ -1180,22 +1361,11 @@ export class CommandHandler {
1180
1361
  if (health.consecutiveErrors > 0) {
1181
1362
  lines.push(`异常计数: ${health.consecutiveErrors}`);
1182
1363
  }
1183
- if (health.safeMode) {
1184
- lines.push(`安全模式: 是 ⚠️`);
1185
- }
1186
1364
  lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
1187
1365
  }
1188
1366
  else {
1189
1367
  lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1190
1368
  }
1191
- if (health.safeMode) {
1192
- lines.push('');
1193
- lines.push('⚠️ 当前处于安全模式(历史上下文已禁用)');
1194
- lines.push('');
1195
- lines.push('退出方式:');
1196
- lines.push('1. /repair - 检查并修复会话(推荐,保留历史)');
1197
- lines.push('2. /new [名称] - 创建新会话(清空历史)');
1198
- }
1199
1369
  if (health.lastError) {
1200
1370
  lines.push('');
1201
1371
  lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
@@ -1229,31 +1399,12 @@ export class CommandHandler {
1229
1399
  await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
1230
1400
  await agent.closeSession(session.id);
1231
1401
  }
1232
- return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /slist 查看`;
1402
+ return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
1233
1403
  }
1234
- // /check 命令:检查渠道状态 / 手动重连指定渠道
1404
+ // /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
1235
1405
  if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
1236
- if (!isAdmin)
1237
- return '❌ 无权限:此命令仅限管理员使用';
1238
1406
  const subCmd = normalizedContent.slice('/check'.length).trim();
1239
- // /check rty <channel> 重连指定渠道
1240
- if (subCmd.startsWith('rty')) {
1241
- const target = subCmd.slice('rty'.length).trim();
1242
- if (!target) {
1243
- return '❌ 请指定渠道名称,例如:/check rty feishu';
1244
- }
1245
- const ch = this.channelObjects.get(target);
1246
- if (!ch) {
1247
- const available = [...this.channelObjects.keys()].join(', ') || '无';
1248
- return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
1249
- }
1250
- if (!ch.reconnect) {
1251
- return `❌ 渠道 "${target}" 不支持重连`;
1252
- }
1253
- const result = await ch.reconnect();
1254
- return `🔄 ${target} 重连: ${result}`;
1255
- }
1256
- // Default: show full system health check
1407
+ // Default: show system health check (non-admin 仅看摘要)
1257
1408
  const lines = ['📡 渠道状态:'];
1258
1409
  // Group by channelType
1259
1410
  const groups = new Map();
@@ -1272,6 +1423,13 @@ export class CommandHandler {
1272
1423
  groups.set(type, []);
1273
1424
  groups.get(type).push({ name, status });
1274
1425
  }
1426
+ if (!isAdmin) {
1427
+ // guest/user: 仅显示渠道健康摘要
1428
+ const total = [...groups.values()].flat().length;
1429
+ const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
1430
+ lines.push(` ${healthy}/${total} 渠道正常`);
1431
+ return lines.join('\n');
1432
+ }
1275
1433
  for (const [type, instances] of groups) {
1276
1434
  if (instances.length === 1) {
1277
1435
  lines.push(` ${instances[0].name}: ${instances[0].status}`);
@@ -1291,7 +1449,6 @@ export class CommandHandler {
1291
1449
  ? this.statsCollector.getSnapshot().uptimeMs
1292
1450
  : process.uptime() * 1000;
1293
1451
  lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
1294
- lines.push(` 当前安全模式会话: ${this.sessionManager.getSafeModeSessionCount()}`);
1295
1452
  // 近 1 小时统计
1296
1453
  if (this.statsCollector) {
1297
1454
  const snap = this.statsCollector.getSnapshot();
@@ -1311,16 +1468,34 @@ export class CommandHandler {
1311
1468
  lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
1312
1469
  }
1313
1470
  lines.push(` 被中断: ${h.interrupts}`);
1314
- lines.push(` 进入安全模式: ${h.safeModeEntries}`);
1315
1471
  if (h.completed > 0) {
1316
1472
  lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
1317
1473
  }
1318
1474
  }
1319
- lines.push('', '💡 /check rty <channel> — 重连指定渠道');
1320
1475
  return lines.join('\n');
1321
1476
  }
1322
- // /restart 命令:重启服务
1323
- if (normalizedContent === '/restart') {
1477
+ // /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
1478
+ if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
1479
+ const restartArg = normalizedContent.slice('/restart'.length).trim();
1480
+ // /restart <channel> — 重连指定渠道(admin only)
1481
+ if (restartArg) {
1482
+ if (!isAdmin)
1483
+ return '❌ 无权限:渠道重连仅限管理员使用';
1484
+ const target = restartArg;
1485
+ const ch = this.channelObjects.get(target);
1486
+ if (!ch) {
1487
+ const available = [...this.channelObjects.keys()].join(', ') || '无';
1488
+ return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
1489
+ }
1490
+ if (!ch.reconnect) {
1491
+ return `❌ 渠道 "${target}" 不支持重连`;
1492
+ }
1493
+ const result = await ch.reconnect();
1494
+ return `🔄 ${target} 重连: ${result}`;
1495
+ }
1496
+ // /restart(无参数)— 重启整个服务(owner only)
1497
+ if (!isOwner)
1498
+ return '❌ 无权限:服务重启仅限 owner 使用';
1324
1499
  const allSessions = await this.sessionManager.listSessions(channel, channelId);
1325
1500
  const sessionsWithMessages = allSessions
1326
1501
  .filter(s => this.messageCache.hasMessages(s.id))
@@ -1389,8 +1564,10 @@ export class CommandHandler {
1389
1564
  }
1390
1565
  return `当前项目: ${session.projectPath}`;
1391
1566
  }
1392
- // /send 命令:发送项目内文件,支持 /send path 和 /send channel path
1567
+ // /send 命令:发送项目内文件,支持 /send path 和 /send channel path(owner only)
1393
1568
  if (normalizedContent.startsWith('/send')) {
1569
+ if (!isOwner)
1570
+ return '❌ 无权限:此命令仅限 owner 使用';
1394
1571
  // 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
1395
1572
  // 还原: 将 [text](url) 替换为 text
1396
1573
  const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
@@ -1562,7 +1739,7 @@ export class CommandHandler {
1562
1739
  }));
1563
1740
  const bodyLines = entries.map(e => {
1564
1741
  const status = buildStatusText(e);
1565
- const prefix = e.isCurrent ? '' : '•';
1742
+ const prefix = e.isCurrent ? '' : '•';
1566
1743
  return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
1567
1744
  });
1568
1745
  const interaction = {
@@ -1602,6 +1779,7 @@ export class CommandHandler {
1602
1779
  const prefix = entry.isCurrent ? ' ✓' : ' ';
1603
1780
  lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
1604
1781
  }
1782
+ lines.push('', '提示: 使用 /p <名称> 切换项目');
1605
1783
  return lines.join('\n');
1606
1784
  }
1607
1785
  // /project(无参数):直接复用 /plist 逻辑(含卡片交互)
@@ -1641,7 +1819,7 @@ export class CommandHandler {
1641
1819
  else {
1642
1820
  projectPath = this.projects[arg];
1643
1821
  if (!projectPath) {
1644
- return `❌ 项目 "${arg}" 不存在\n提示: 使用 /plist 查看可用项目`;
1822
+ return `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目`;
1645
1823
  }
1646
1824
  projectName = arg;
1647
1825
  }
@@ -1700,8 +1878,10 @@ export class CommandHandler {
1700
1878
  }
1701
1879
  return response;
1702
1880
  }
1703
- // /bind 命令:持久化项目到配置(不切换)
1881
+ // /bind 命令:持久化项目到配置(不切换)(owner only)
1704
1882
  if (normalizedContent.startsWith('/bind ')) {
1883
+ if (!isOwner)
1884
+ return '❌ 无权限:此命令仅限 owner 使用';
1705
1885
  const projectPath = normalizedContent.slice(6).trim();
1706
1886
  if (!projectPath)
1707
1887
  return '用法: /bind <路径>';
@@ -1746,7 +1926,7 @@ export class CommandHandler {
1746
1926
  请先执行以下操作之一:
1747
1927
  1. 发送任意消息 - 自动创建新会话
1748
1928
  2. /new [名称] - 创建命名会话
1749
- 3. /project <项目> - 切换到指定项目`;
1929
+ 3. /p <项目> - 切换到指定项目`;
1750
1930
  }
1751
1931
  const showCliOnly = normalizedContent === '/slist cli';
1752
1932
  // /slist cli — 仅显示 CLI 会话
@@ -1876,7 +2056,7 @@ export class CommandHandler {
1876
2056
  };
1877
2057
  });
1878
2058
  const bodyLines = displaySessions.map(ds => {
1879
- const prefix = ds.isActive ? '' : '•';
2059
+ const prefix = ds.isActive ? '' : '•';
1880
2060
  const threadTag = ds.session.threadId ? '[话题] ' : '';
1881
2061
  const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
1882
2062
  const fileMark = ds.fileMissing ? '❌ ' : '';
@@ -1940,13 +2120,17 @@ export class CommandHandler {
1940
2120
  lines.push('');
1941
2121
  }
1942
2122
  lines.push('使用 /s <序号、name或8位uuid> 切换会话');
1943
- lines.push('使用 /slist cli 查看 CLI 会话');
2123
+ lines.push('使用 /s cli 查看 CLI 会话');
1944
2124
  return lines.join('\n');
1945
2125
  }
1946
2126
  // /session(无参数):直接复用 /slist 逻辑(含卡片交互)
1947
2127
  if (normalizedContent === '/session') {
1948
2128
  return this.handle('/slist', channel, channelId, undefined, userId, threadId);
1949
2129
  }
2130
+ // /session cli(= /s cli):列出未导入的 CLI 会话
2131
+ if (normalizedContent === '/session cli') {
2132
+ return this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
2133
+ }
1950
2134
  // /session 或 /s 命令:切换会话
1951
2135
  if (normalizedContent.startsWith('/session ')) {
1952
2136
  const sessionName = normalizedContent.slice(9).trim();
@@ -1967,7 +2151,7 @@ export class CommandHandler {
1967
2151
  targetSession = visibleSessions[idx - 1];
1968
2152
  }
1969
2153
  else {
1970
- return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /slist 查看可用会话`;
2154
+ return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
1971
2155
  }
1972
2156
  }
1973
2157
  if (!targetSession && sessionName.length === 8) {
@@ -1992,7 +2176,7 @@ export class CommandHandler {
1992
2176
  }
1993
2177
  }
1994
2178
  if (!targetSession) {
1995
- return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
2179
+ return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
1996
2180
  }
1997
2181
  const lastInput = targetSession.agentSessionId
1998
2182
  ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
@@ -2071,14 +2255,14 @@ export class CommandHandler {
2071
2255
  targetSession = visibleSessions[idx - 1];
2072
2256
  }
2073
2257
  else {
2074
- return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /slist 查看可用会话`;
2258
+ return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
2075
2259
  }
2076
2260
  }
2077
2261
  if (!targetSession && sessionName.length === 8) {
2078
2262
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2079
2263
  }
2080
2264
  if (!targetSession) {
2081
- return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
2265
+ return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
2082
2266
  }
2083
2267
  if (targetSession.id === session.id) {
2084
2268
  return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
@@ -2109,38 +2293,66 @@ export class CommandHandler {
2109
2293
  const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
2110
2294
  const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
2111
2295
  this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
2112
- return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /slist 查看所有会话,/s <名称> 切换回原会话`;
2296
+ return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话`;
2113
2297
  }
2114
2298
  catch (error) {
2115
2299
  logger.error('[CommandHandler] Fork session failed:', error);
2116
2300
  return `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}`;
2117
2301
  }
2118
2302
  }
2119
- // /repair 命令:检查并修复会话
2303
+ // /rewind 命令:查看历史 / 回退会话
2304
+ if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
2305
+ const result = await this.ensureSession(channel, channelId, threadId);
2306
+ if ('error' in result)
2307
+ return result.error;
2308
+ const { session } = result;
2309
+ const rewindAgent = this.getAgent(session.agentId);
2310
+ if (rewindAgent.name !== 'claude') {
2311
+ return '❌ /rewind 仅支持 Claude 后端';
2312
+ }
2313
+ if (!session.agentSessionId) {
2314
+ return '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind';
2315
+ }
2316
+ if (!rewindAgent.getSessionMessages) {
2317
+ return '❌ 当前 Agent 不支持 /rewind';
2318
+ }
2319
+ const args = normalizedContent.slice('/rewind'.length).trim();
2320
+ if (!args) {
2321
+ return await this.handleRewindList(session, rewindAgent);
2322
+ }
2323
+ const parts = args.split(/\s+/);
2324
+ const turnNum = parseInt(parts[0], 10);
2325
+ if (isNaN(turnNum) || turnNum < 1) {
2326
+ return '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)';
2327
+ }
2328
+ const mode = parts[1]?.toLowerCase();
2329
+ if (!mode) {
2330
+ return `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)`;
2331
+ }
2332
+ if (!['chat', 'file', 'all'].includes(mode)) {
2333
+ return `❌ 无效模式 "${mode}",可选:chat | file | all`;
2334
+ }
2335
+ return await this.handleRewind(session, rewindAgent, turnNum, mode);
2336
+ }
2337
+ // /repair 命令:检查并修复会话文件
2120
2338
  if (normalizedContent === '/repair') {
2121
2339
  const repairResult = await this.ensureSession(channel, channelId, threadId);
2122
2340
  if ('error' in repairResult)
2123
2341
  return repairResult.error;
2124
2342
  const { session: repairSession } = repairResult;
2125
- const health = await this.sessionManager.getHealthStatus(repairSession.id);
2126
- if (!health.safeMode) {
2127
- return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
2128
- }
2129
2343
  const repairAgent = this.getAgent(repairSession.agentId);
2130
2344
  const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
2131
2345
  try {
2132
2346
  if (!repairSession.agentSessionId) {
2133
2347
  await this.sessionManager.resetHealthStatus(repairSession.id);
2134
- this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
2135
- return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
2348
+ return `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器`;
2136
2349
  }
2137
2350
  // 通过 agent 定位 session 文件
2138
2351
  const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
2139
2352
  if (!sessionFile) {
2140
2353
  // 文件不存在(已被删除或从未创建),直接重置
2141
2354
  await this.sessionManager.resetHealthStatus(repairSession.id);
2142
- this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
2143
- return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
2355
+ return `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器`;
2144
2356
  }
2145
2357
  const healthCheck = await checkSessionFile(sessionFile);
2146
2358
  if (healthCheck.corrupt) {
@@ -2150,42 +2362,195 @@ export class CommandHandler {
2150
2362
  await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
2151
2363
  repairAgent.updateSessionId(repairSession.id, '');
2152
2364
  await this.sessionManager.resetHealthStatus(repairSession.id);
2153
- this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
2154
- return `✓ 修复完成,已退出安全模式\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
2365
+ return `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
2155
2366
  }
2156
2367
  if (healthCheck.issues.length > 0) {
2157
2368
  await this.sessionManager.resetHealthStatus(repairSession.id);
2158
- this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
2159
2369
  return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
2160
2370
  }
2161
2371
  await this.sessionManager.resetHealthStatus(repairSession.id);
2162
- this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
2163
- return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器\n- 已恢复正常会话模式`;
2372
+ return `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器`;
2164
2373
  }
2165
2374
  catch (error) {
2166
2375
  logger.error('[Repair] Failed:', error);
2167
2376
  return `❌ 修复失败: ${error.message}`;
2168
2377
  }
2169
2378
  }
2170
- // /safe 命令:手动进入安全模式
2379
+ // /safe 命令:安全模式已禁用
2171
2380
  if (normalizedContent === '/safe') {
2172
- const safeResult = await this.ensureSession(channel, channelId, threadId);
2173
- if ('error' in safeResult)
2174
- return safeResult.error;
2175
- const { session: safeSession } = safeResult;
2176
- await this.sessionManager.setSafeMode(safeSession.id, true);
2177
- this.eventBus.publish({ type: 'session:safe-mode-entered', sessionId: safeSession.id, reason: 'manual' });
2178
- return `✓ 已进入安全模式
2179
-
2180
- 当前行为:
2181
- - 暂时不加载会话历史(每次对话独立)
2182
- - 所有功能正常可用(读写文件、执行命令等)
2183
- - 不会丢失历史数据(仍保存在 .claude/ 目录)
2184
-
2185
- 退出安全模式:
2186
- - 使用 /repair 检查并修复会话
2187
- - 使用 /new 创建全新会话`;
2381
+ return `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。`;
2188
2382
  }
2189
2383
  return null;
2190
2384
  }
2385
+ // ── /rewind helpers ──
2386
+ async handleRewindList(session, agent) {
2387
+ try {
2388
+ const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
2389
+ const turns = this.buildTurnList(messages);
2390
+ if (turns.length === 0) {
2391
+ return '📋 当前会话暂无对话记录';
2392
+ }
2393
+ const lines = turns.map(t => `#${t.index} ${t.userContent}`);
2394
+ return [
2395
+ `📋 会话历史 (共 ${turns.length} 轮)`,
2396
+ '',
2397
+ ...lines,
2398
+ '',
2399
+ '💡 /rewind <N> chat|file|all — 撤销第N轮',
2400
+ ].join('\n');
2401
+ }
2402
+ catch (error) {
2403
+ logger.error('[CommandHandler] Failed to read session messages:', error);
2404
+ return `❌ 读取会话历史失败: ${error instanceof Error ? error.message : '未知错误'}`;
2405
+ }
2406
+ }
2407
+ async handleRewind(session, agent, turnNum, mode) {
2408
+ try {
2409
+ const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
2410
+ const turns = this.buildTurnList(messages);
2411
+ if (turnNum < 1 || turnNum > turns.length) {
2412
+ return `❌ 轮次超出范围,当前共 ${turns.length} 轮`;
2413
+ }
2414
+ // /rewind N = 撤销第N轮(及之后),保留 1..N-1
2415
+ const rewindTarget = turns[turnNum - 1]; // 被撤销的轮次(用于文件回退)
2416
+ const keepTarget = turnNum >= 2 ? turns[turnNum - 2] : null; // 保留到的轮次(用于对话回退)
2417
+ const results = [];
2418
+ // 文件回退(立即执行)
2419
+ if (mode === 'file' || mode === 'all') {
2420
+ if (!agent.rewindFiles) {
2421
+ return '❌ 当前 Agent 不支持文件回退';
2422
+ }
2423
+ const fileResult = await agent.rewindFiles(session.agentSessionId, session.projectPath, rewindTarget.userUuid);
2424
+ if (!fileResult.canRewind) {
2425
+ if (mode === 'file') {
2426
+ return `❌ 当前会话无文件快照,无法回退文件${fileResult.error ? `\n原因: ${fileResult.error}` : ''}`;
2427
+ }
2428
+ results.push(`⚠️ 文件回退失败${fileResult.error ? `: ${fileResult.error}` : '(无文件快照)'}`);
2429
+ }
2430
+ else {
2431
+ const detail = fileResult.filesChanged
2432
+ ? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
2433
+ : '';
2434
+ results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
2435
+ }
2436
+ }
2437
+ // 对话回退(延迟执行 — 下次发消息时生效)
2438
+ if (mode === 'chat' || mode === 'all') {
2439
+ if (keepTarget) {
2440
+ const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
2441
+ await this.sessionManager.updateSession(session.id, { metadata: meta });
2442
+ }
2443
+ else {
2444
+ // N=1:撤销全部对话,清空 session 从头开始
2445
+ const meta = { ...(session.metadata || {}) };
2446
+ delete meta.resumeAt;
2447
+ await this.sessionManager.updateSession(session.id, {
2448
+ metadata: meta,
2449
+ agentSessionId: null,
2450
+ });
2451
+ }
2452
+ const discarded = turns.length - turnNum + 1;
2453
+ const keepDesc = keepTarget
2454
+ ? `回退到第 ${turnNum - 1} 轮:"${keepTarget.userContent}"`
2455
+ : '已清空全部对话历史';
2456
+ results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
2457
+ }
2458
+ this.eventBus.publish({
2459
+ type: 'session:rewind',
2460
+ sessionId: session.id,
2461
+ turnNum,
2462
+ mode,
2463
+ });
2464
+ return results.join('\n');
2465
+ }
2466
+ catch (error) {
2467
+ logger.error('[CommandHandler] Rewind failed:', error);
2468
+ return `❌ 回退失败: ${error instanceof Error ? error.message : '未知错误'}`;
2469
+ }
2470
+ }
2471
+ buildTurnList(messages) {
2472
+ const turns = [];
2473
+ let pendingUser = null;
2474
+ for (const msg of messages) {
2475
+ if (msg.type === 'user') {
2476
+ const m = msg.message;
2477
+ if (Array.isArray(m?.content) && m.content.every((c) => c.type === 'tool_result')) {
2478
+ continue;
2479
+ }
2480
+ const content = this.extractUserContent(msg.message);
2481
+ if (content) {
2482
+ pendingUser = { content, uuid: msg.uuid };
2483
+ }
2484
+ }
2485
+ else if (msg.type === 'assistant' && pendingUser) {
2486
+ turns.push({
2487
+ index: turns.length + 1,
2488
+ userContent: pendingUser.content,
2489
+ userUuid: pendingUser.uuid,
2490
+ assistantUuid: msg.uuid,
2491
+ });
2492
+ pendingUser = null;
2493
+ }
2494
+ }
2495
+ return turns;
2496
+ }
2497
+ // ── Agent Ctl ──
2498
+ static CTL_COMMANDS = [
2499
+ '/help', '/status', '/check',
2500
+ '/model', '/effort', '/perm',
2501
+ '/compact', '/activity', '/send', '/restart', '/agentmd',
2502
+ ];
2503
+ /**
2504
+ * Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
2505
+ * 复用现有 slash cmd 逻辑,权限继承 session 用户角色
2506
+ */
2507
+ async handleCtl(cmd, sessionId) {
2508
+ // 1. 白名单检查
2509
+ const inputCmd = cmd.split(' ')[0];
2510
+ if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
2511
+ return { ok: false, error: `不允许的指令: ${inputCmd}` };
2512
+ }
2513
+ // 2. 通过 sessionId 查 session
2514
+ const session = await this.sessionManager.getSessionById(sessionId);
2515
+ if (!session) {
2516
+ return { ok: false, error: '无效的 session' };
2517
+ }
2518
+ // 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
2519
+ const userId = session.metadata?.peerId;
2520
+ // 4. send 路径限制:只允许 projectPath 下的文件
2521
+ if (cmd.startsWith('/send')) {
2522
+ const sendArgs = cmd.slice(5).trim();
2523
+ const parts = sendArgs.split(/\s+/);
2524
+ const filePath = parts[parts.length - 1];
2525
+ if (filePath) {
2526
+ const resolved = path.resolve(session.projectPath, filePath);
2527
+ if (!resolved.startsWith(session.projectPath)) {
2528
+ return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
2529
+ }
2530
+ }
2531
+ }
2532
+ // 5. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
2533
+ try {
2534
+ const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
2535
+ userId);
2536
+ return { ok: true, result: result ?? '(无输出)' };
2537
+ }
2538
+ catch (err) {
2539
+ return { ok: false, error: err.message };
2540
+ }
2541
+ }
2542
+ extractUserContent(message) {
2543
+ const m = message;
2544
+ let text = '';
2545
+ if (typeof m?.content === 'string') {
2546
+ text = m.content;
2547
+ }
2548
+ else if (Array.isArray(m?.content)) {
2549
+ text = m.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ');
2550
+ }
2551
+ text = text.trim().replace(/\s+/g, ' ');
2552
+ if (!text)
2553
+ return '';
2554
+ return text.length > 50 ? text.substring(0, 50) + '…' : text;
2555
+ }
2191
2556
  }