evolclaw 3.3.0 → 3.4.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 (44) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +7 -3
  3. package/dist/agents/claude-runner.js +23 -27
  4. package/dist/agents/codex-runner.js +90 -6
  5. package/dist/agents/runner-types.js +30 -0
  6. package/dist/aun/outbox.js +14 -2
  7. package/dist/channels/aun.js +506 -108
  8. package/dist/channels/feishu.js +29 -5
  9. package/dist/cli/agent-command.js +591 -0
  10. package/dist/cli/agent.js +15 -3
  11. package/dist/cli/aun-commands.js +1444 -0
  12. package/dist/cli/ctl-command.js +78 -0
  13. package/dist/cli/daemon-commands.js +2707 -0
  14. package/dist/cli/index.js +12 -5027
  15. package/dist/cli/restart-monitor.js +539 -0
  16. package/dist/cli/watch-logs.js +33 -0
  17. package/dist/core/channel-loader.js +4 -1
  18. package/dist/core/command/command-handler.js +1189 -0
  19. package/dist/core/command/menu-handler.js +1478 -0
  20. package/dist/core/command/slash-gate.js +142 -0
  21. package/dist/core/command/slash-handler.js +2090 -0
  22. package/dist/core/evolagent-registry.js +81 -0
  23. package/dist/core/evolagent.js +16 -0
  24. package/dist/core/message/im-renderer.js +67 -49
  25. package/dist/core/message/message-bridge.js +30 -9
  26. package/dist/core/message/message-processor.js +200 -122
  27. package/dist/core/message/message-queue.js +68 -0
  28. package/dist/core/permission.js +16 -0
  29. package/dist/core/session/session-manager.js +59 -13
  30. package/dist/core/stats/db.js +20 -0
  31. package/dist/core/stats/writer.js +3 -3
  32. package/dist/data/error-dict.json +7 -0
  33. package/dist/index.js +49 -6
  34. package/dist/ipc.js +99 -0
  35. package/dist/utils/cross-platform.js +35 -0
  36. package/dist/utils/ecweb-launch.js +49 -0
  37. package/dist/utils/error-utils.js +18 -5
  38. package/dist/utils/npm-ops.js +38 -8
  39. package/dist/utils/stats.js +63 -6
  40. package/kits/eck_manifest.json +0 -12
  41. package/package.json +2 -3
  42. package/dist/core/command-handler.js +0 -4235
  43. package/dist/core/message/response-depth.js +0 -56
  44. package/kits/templates/system-fragments/response-depth.md +0 -16
@@ -0,0 +1,2090 @@
1
+ import { DEFAULT_PERMISSION_MODE } from '../../types.js';
2
+ import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
3
+ import { getCodexEfforts } from '../../agents/codex-runner.js';
4
+ import { buildEnvelope } from '../message/message-processor.js';
5
+ import { resolvePaths, getPackageRoot } from '../../paths.js';
6
+ import { logger } from '../../utils/logger.js';
7
+ import crypto from 'crypto';
8
+ import path from 'path';
9
+ import fs from 'fs';
10
+ import os from 'os';
11
+ import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
12
+ import { loadEvolclawConfig } from '../../config-store.js';
13
+ import { isProcessLevelOwner } from './menu-handler.js';
14
+ import { execAgentAction } from '../message/command-handler-agent-control.js';
15
+ import { displaySessionTitle } from '../session/session-title.js';
16
+ import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, } from './slash-gate.js';
17
+ const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
18
+ const PERMISSION_MODE_KEYS = ['auto', 'bypass', 'readonly', 'plan', 'edit', 'request', 'noask'];
19
+ const PERMISSION_MODE_USAGE = PERMISSION_MODE_KEYS.join('|');
20
+ function getAvailableEfforts(agent, model) {
21
+ if (agent.name === 'claude')
22
+ return allEfforts;
23
+ if (agent.name === 'codex')
24
+ return getCodexEfforts(model);
25
+ return [];
26
+ }
27
+ function modelDisplayLabel(agent, model) {
28
+ const full = agent.resolveModelId?.(model);
29
+ return full && full !== model ? `${model} (${full})` : model;
30
+ }
31
+ function formatIdleTime(ms) {
32
+ const seconds = Math.floor(ms / 1000);
33
+ const minutes = Math.floor(seconds / 60);
34
+ const hours = Math.floor(minutes / 60);
35
+ const days = Math.floor(hours / 24);
36
+ if (days > 0)
37
+ return `${days}天前`;
38
+ if (hours > 0)
39
+ return `${hours}小时前`;
40
+ if (minutes > 0)
41
+ return `${minutes}分钟前`;
42
+ return '刚刚';
43
+ }
44
+ function getAgentBusyCount(handler, aid) {
45
+ if (!aid || !handler.agentRegistry)
46
+ return null;
47
+ const handle = handler.agentRegistry.get(aid) ?? null;
48
+ const agentName = handle?.name;
49
+ if (!agentName)
50
+ return null;
51
+ return (handler.messageQueue?.getProcessingCountByAgent?.(agentName) ?? 0)
52
+ + (handler.messageQueue?.getQueueLengthByAgent?.(agentName) ?? 0);
53
+ }
54
+ export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity) {
55
+ // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
56
+ // 不应覆盖 session 中已有的正确值
57
+ if (source === 'card-trigger')
58
+ chatType = undefined;
59
+ // 解析身份(按实例名)
60
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId);
61
+ const policy = this.getPolicy(channel);
62
+ // 按当前会话选择 agent 后端
63
+ const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
64
+ const agent = this.getAgent(channel, activeSession?.agentId);
65
+ // 规范化命令(将别名转换为完整命令)
66
+ const normalizedContent = normalizeSlashContent(content);
67
+ if (normalizedContent !== content) {
68
+ logger.debug(`[CommandHandler] normalized: "${content}" -> "${normalizedContent}"`);
69
+ }
70
+ logger.info(`[CommandHandler] handle: channel=${channel} channelId=${channelId} cmd="${normalizedContent.split(' ')[0]}" user=${userId ?? 'n/a'} role=${identity?.role ?? 'n/a'}`);
71
+ // Agent-owned 通道:禁止项目切换和 agent 切换
72
+ // 权限检查:区分用户级命令和管理级命令
73
+ const isOwner = identity.role === 'owner';
74
+ const isAdmin = identity.role === 'owner' || identity.role === 'admin';
75
+ const activeChatType = activeSession?.chatType || 'private';
76
+ const threadGuard = guardThreadCommand(normalizedContent, threadId);
77
+ if (threadGuard)
78
+ return threadGuard;
79
+ // daemon owner 判定(缓存一次,后续 /restart /reload 复用)
80
+ const evolclawConfig = loadEvolclawConfig();
81
+ const isDaemonOwner = isProcessLevelOwner(userId, evolclawConfig.owners);
82
+ // roleGuard 仅对进程级命令(/restart /reload)放行 daemon owner 绕过,
83
+ // 其余命令严格按 agent-channel 的 isAdmin 判定,不越权。
84
+ const isProcessLevelSlash = normalizedContent === '/restart' || normalizedContent === '/reload' || normalizedContent.startsWith('/reload ');
85
+ const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
86
+ if (roleGuard)
87
+ return roleGuard;
88
+ const idleGuard = await guardIdleCommand({
89
+ content: normalizedContent,
90
+ threadId,
91
+ channel,
92
+ channelId,
93
+ activeSession,
94
+ activeAgent: agent,
95
+ sessionManager: this.sessionManager,
96
+ messageQueue: this.messageQueue,
97
+ getAgentForSession: session => this.getAgent(channel, session.agentId),
98
+ });
99
+ if (idleGuard)
100
+ return idleGuard;
101
+ const knownGuard = guardKnownCommand(normalizedContent);
102
+ if (knownGuard)
103
+ return knownGuard;
104
+ const isCmd = isRecognizedSlashCommand(normalizedContent);
105
+ if (!isCmd)
106
+ return undefined;
107
+ // /help 命令不需要会话
108
+ if (normalizedContent === '/help') {
109
+ const appendProcessCommands = (lines) => {
110
+ if (!isDaemonOwner)
111
+ return;
112
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /reload [aid] - 热重载 Agent 配置');
113
+ };
114
+ if (!isAdmin && activeChatType === 'group') {
115
+ const lines = [
116
+ '可用命令:',
117
+ '',
118
+ '其他:',
119
+ ' /status - 显示会话状态',
120
+ ' /check - 检查渠道健康',
121
+ ' /help - 显示此帮助信息',
122
+ ];
123
+ appendProcessCommands(lines);
124
+ return { kind: 'command.result', text: lines.join('\n') };
125
+ }
126
+ if (!isAdmin) {
127
+ const lines = [
128
+ '可用命令:',
129
+ '',
130
+ '🔄 会话管理:',
131
+ ' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
132
+ ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
133
+ ' /name <新名称> - 重命名当前会话',
134
+ ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
135
+ ' /status - 显示会话状态',
136
+ ' /check - 检查渠道健康',
137
+ '',
138
+ '❓ 帮助:',
139
+ ' /help - 显示此帮助信息',
140
+ ];
141
+ appendProcessCommands(lines);
142
+ return { kind: 'command.result', text: lines.join('\n') };
143
+ }
144
+ // admin+ 基础命令
145
+ const lines = [
146
+ '可用命令:',
147
+ '',
148
+ '📁 项目:',
149
+ ' /pwd - 显示当前项目路径',
150
+ '',
151
+ '🔄 会话管理:',
152
+ ' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
153
+ ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
154
+ ' /name <新名称> - 重命名当前会话',
155
+ ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
156
+ ' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
157
+ ' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
158
+ ' /compact - 压缩会话上下文(减少 token 用量)',
159
+ '',
160
+ '🤖 Agent 与模型:',
161
+ ' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
162
+ ' /model [model] - 查看或切换模型',
163
+ ' /effort [level] - 查看或切换推理强度',
164
+ '',
165
+ '💬 聊天设置:',
166
+ ' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
167
+ ' /chatmode [interactive|proactive] - 查看/切换会话模式(被动响应或主动推进)',
168
+ ' /dispatch [mention|broadcast] - 查看/切换群聊分发模式(仅@响应或广播响应,仅群聊)',
169
+ '',
170
+ '🔐 权限管理:',
171
+ ' /perm - 查看当前权限模式',
172
+ ...(isAdmin ? [` /perm <${PERMISSION_MODE_USAGE}> - 切换权限模式`] : []),
173
+ ' /perm allow|always|deny - 审批权限请求',
174
+ '',
175
+ '🛠️ 运维:',
176
+ ' /status - 显示会话状态',
177
+ ' /stop - 中断当前任务',
178
+ ' /check - 检查渠道状态',
179
+ ...(isDaemonOwner ? [
180
+ ' /restart - 重启服务',
181
+ ' /reload [aid] - 热重载 Agent 配置',
182
+ ] : []),
183
+ ...(!isDaemonOwner && isAdmin ? [
184
+ ' /reload - 热重载当前 Agent 配置',
185
+ ] : []),
186
+ ...(isAdmin ? [
187
+ '',
188
+ '🧰 工具:',
189
+ ` /file ${isOwner ? '[channel] ' : ''}<path> - 发送项目内文件`,
190
+ ] : []),
191
+ '',
192
+ '❓ 帮助:',
193
+ ' /help - 显示此帮助信息',
194
+ ];
195
+ return { kind: 'command.result', text: lines.join('\n') };
196
+ }
197
+ // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
198
+ if (normalizedContent === '/evolhelp') {
199
+ const cmds = [];
200
+ // 项目
201
+ cmds.push({ command: '/pwd', description: '显示当前项目路径', category: '项目', roles: ['admin', 'owner'] });
202
+ // 会话管理
203
+ cmds.push({ command: '/new', args: '[名称]', description: '创建新会话(清空历史请用此命令,可选命名)', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
204
+ cmds.push({ command: '/s', aliases: ['/session', '/slist'], args: '[cli|名称|序号|uuid]', description: '列出或切换会话', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
205
+ cmds.push({ command: '/name', aliases: ['/rename'], args: '<新名称>', description: '重命名当前会话', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
206
+ cmds.push({ command: '/del', args: '<名称>', description: '删除指定会话(仅解绑,不删除文件)', category: '会话管理', roles: ['guest', 'admin', 'owner'] });
207
+ if (isAdmin) {
208
+ cmds.push({ command: '/fork', args: '[名称]', description: '分支当前会话(从当前对话点创建分支)', category: '会话管理', roles: ['admin', 'owner'] });
209
+ cmds.push({ command: '/rewind', aliases: ['/rw'], args: '[N] [chat|file|all]', description: '查看历史/撤销指定轮次', category: '会话管理', roles: ['admin', 'owner'] });
210
+ cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
211
+ }
212
+ // Agent 与模型
213
+ if (isAdmin) {
214
+ cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
215
+ cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
216
+ cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
217
+ }
218
+ // 权限管理
219
+ if (isAdmin) {
220
+ cmds.push({ command: '/perm', args: `<${PERMISSION_MODE_USAGE}>`, description: '查看或切换当前权限模式', category: '权限管理', roles: ['admin', 'owner'] });
221
+ cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
222
+ }
223
+ // 运维
224
+ cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
225
+ cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
226
+ cmds.push({ command: '/check', description: '检查渠道状态', category: '运维', roles: ['guest', 'admin', 'owner'] });
227
+ if (isAdmin) {
228
+ cmds.push({ command: '/activity', args: '[all|dm|owner|none]', description: '查看/控制中间输出显示模式', category: '聊天设置', roles: ['admin', 'owner'] });
229
+ }
230
+ if (isDaemonOwner) {
231
+ cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
232
+ cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
233
+ }
234
+ if (!isDaemonOwner && isAdmin) {
235
+ cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
236
+ }
237
+ if (isAdmin) {
238
+ cmds.push({ command: '/file', args: isOwner ? '[channel] <path>' : '<path>', description: '发送项目内文件', category: '工具', roles: ['admin', 'owner'] });
239
+ }
240
+ // 聊天设置
241
+ if (isAdmin) {
242
+ cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '聊天设置', roles: ['admin', 'owner'] });
243
+ cmds.push({ command: '/dispatch', args: '[mention|broadcast]', description: '查看/切换群聊分发模式(仅@响应或广播响应)', category: '聊天设置', roles: ['admin', 'owner'] });
244
+ }
245
+ // 交互
246
+ cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['guest', 'admin', 'owner'] });
247
+ // 帮助
248
+ cmds.push({ command: '/help', description: '显示帮助信息', category: '帮助', roles: ['guest', 'admin', 'owner'] });
249
+ const categories = [...new Set(cmds.map(c => c.category))];
250
+ return { kind: 'command.result', text: JSON.stringify({ commands: cmds, categories }) };
251
+ }
252
+ // /perm 命令:权限模式切换 + 权限审批(快速路径,不进入消息队列)
253
+ if (normalizedContent.startsWith('/perm')) {
254
+ const args = normalizedContent.slice(5).trim();
255
+ // 先获取正确的 session 和 agent(话题可能用不同 agent)
256
+ const permResult = await this.ensureSession(channel, channelId, threadId, chatType);
257
+ if ('error' in permResult)
258
+ return { kind: 'command.result', text: permResult.error };
259
+ const { session: permSession } = permResult;
260
+ const permAgent = this.getAgent(channel, permSession.agentId);
261
+ // /perm(无参数):显示当前模式和可选模式
262
+ if (!args) {
263
+ if (!hasPermissionController(permAgent)) {
264
+ return { kind: 'command.error', text: '❌ 权限控制不可用' };
265
+ }
266
+ const currentMode = permSession.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE;
267
+ const modes = permAgent.listModes();
268
+ // 尝试发送 CommandCard 卡片
269
+ {
270
+ const availableModes = modes.filter(m => m.available);
271
+ const interaction = {
272
+ type: 'interaction',
273
+ id: `perm-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
274
+ channelId,
275
+ sessionId: permSession.id,
276
+ initiatorId: userId,
277
+ kind: {
278
+ kind: 'command-card',
279
+ title: '🔐 权限模式',
280
+ body: availableModes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
281
+ buttons: availableModes.map(m => ({
282
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
283
+ command: `/perm ${m.key}`,
284
+ style: (m.key === currentMode ? 'primary' : 'default'),
285
+ disabled: m.key === currentMode,
286
+ })),
287
+ },
288
+ };
289
+ const replyCtx = this.getReplyContext(permSession);
290
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
291
+ if (cardResult === null)
292
+ return null;
293
+ return { kind: 'command.result', text: cardResult };
294
+ }
295
+ // 降级:文本
296
+ const modeList = modes.map(m => {
297
+ const prefix = m.key === currentMode ? '✓' : ' ';
298
+ const suffix = m.available ? '' : ' ⚠️ 不可用';
299
+ return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
300
+ }).join('\n');
301
+ if (isAdmin) {
302
+ return { kind: 'command.result', text: `权限模式: ${currentMode}\n\n${modeList}\n\n用法: /perm <模式> 或 allow|always|deny` };
303
+ }
304
+ return { kind: 'command.result', text: `当前权限模式: ${currentMode}` };
305
+ }
306
+ const parts = args.split(/\s+/);
307
+ // /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
308
+ if (parts.length === 1) {
309
+ const arg = parts[0];
310
+ // /perm allow|always|deny:快捷审批
311
+ // 优先走 InteractionRouter fallback(统一降级路径)
312
+ if (arg === 'allow' || arg === 'always' || arg === 'deny') {
313
+ const fb = await this.handleInteractionFallback('perm', arg, permSession.id, userId);
314
+ if (fb.matched)
315
+ return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
316
+ // fallback 不命中:走 permissionGateway 直接审批(兼容旧路径)
317
+ if (!this.permissionGateway) {
318
+ return { kind: 'command.error', text: '❌ 权限审批未启用' };
319
+ }
320
+ const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
321
+ if (pendingIds.length === 0) {
322
+ return { kind: 'command.error', text: '❌ 当前没有待审批的权限请求' };
323
+ }
324
+ if (pendingIds.length > 1) {
325
+ return { kind: 'command.error', text: `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map((id) => ` /perm ${id} ${arg}`).join('\n')}` };
326
+ }
327
+ const requestId = pendingIds[0];
328
+ const decision = arg;
329
+ this.permissionGateway.resolvePermission(permSession.id, requestId, decision);
330
+ const labels = {
331
+ allow: '✓ 已授权(本次),继续执行……',
332
+ always: '✓ 已授权(始终允许该工具),继续执行……',
333
+ deny: '✓ 已拒绝'
334
+ };
335
+ return { kind: 'command.result', text: labels[decision] };
336
+ }
337
+ // /perm <mode>:切换权限模式
338
+ if (hasPermissionController(permAgent)) {
339
+ const modes = permAgent.listModes();
340
+ const matched = modes.find(m => m.key === arg);
341
+ if (matched) {
342
+ if (!matched.available) {
343
+ return { kind: 'command.error', text: `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}` };
344
+ }
345
+ // 关系级权限模式切换允许 owner/admin;guest 只能查询当前模式。
346
+ if (!isAdmin) {
347
+ return { kind: 'command.error', text: '❌ 权限模式切换仅限管理员' };
348
+ }
349
+ const metadata = permSession.metadata || {};
350
+ metadata.permissionMode = arg;
351
+ await this.sessionManager.updateSession(permSession.id, { metadata });
352
+ if (this.shouldSuppressCardTriggerResult(source, channel))
353
+ return null;
354
+ return { kind: 'command.result', text: `✓ 权限模式已切换为: ${matched.key} (${matched.nameZh})\n${matched.description}` };
355
+ }
356
+ }
357
+ // 不是已知模式名也不是 allow/deny
358
+ const modeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : PERMISSION_MODE_USAGE;
359
+ return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm <${modeKeys}> 或 /perm allow|always|deny` };
360
+ }
361
+ // 双参数不再支持,提示正确用法
362
+ const allModeKeys = hasPermissionController(permAgent) ? permAgent.listModes().map(m => m.key).join('|') : PERMISSION_MODE_USAGE;
363
+ return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny` };
364
+ }
365
+ // /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
366
+ if (normalizedContent.startsWith('/ask')) {
367
+ const args = normalizedContent.slice(4).trim();
368
+ if (!args) {
369
+ const askResult = await this.ensureSession(channel, channelId, threadId, chatType);
370
+ if ('error' in askResult)
371
+ return { kind: 'command.result', text: askResult.error };
372
+ const pendingIds = this.interactionRouter?.getPending(askResult.session.id) || [];
373
+ if (pendingIds.length === 0)
374
+ return { kind: 'command.result', text: '当前没有待回答的问题' };
375
+ return { kind: 'command.result', text: `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>` };
376
+ }
377
+ const askResult = await this.ensureSession(channel, channelId, threadId, chatType);
378
+ if ('error' in askResult)
379
+ return { kind: 'command.result', text: askResult.error };
380
+ const fb = await this.handleInteractionFallback('ask', args, askResult.session.id, userId);
381
+ if (fb.matched)
382
+ return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
383
+ return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
384
+ }
385
+ // /resume 命令:返回当前项目的 Claude 会话记录(JSON)
386
+ if (normalizedContent === '/resume' || normalizedContent.startsWith('/resume ')) {
387
+ const resumeResult = await this.ensureSession(channel, channelId, threadId, chatType);
388
+ if ('error' in resumeResult)
389
+ return { kind: 'command.result', text: resumeResult.error };
390
+ const { session: resumeSession } = resumeResult;
391
+ try {
392
+ const { encodePath } = await import('../../utils/cross-platform.js');
393
+ const homeDir = os.homedir();
394
+ const encodedPath = encodePath(resumeSession.projectPath);
395
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
396
+ if (!fs.existsSync(projectDir)) {
397
+ return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
398
+ }
399
+ const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
400
+ if (jsonlFiles.length === 0) {
401
+ return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
402
+ }
403
+ const sessions = [];
404
+ for (const file of jsonlFiles) {
405
+ const filePath = path.join(projectDir, file);
406
+ const sessionId = file.replace('.jsonl', '');
407
+ let lastTimestamp = '';
408
+ let firstUserMessage = '';
409
+ let model = '';
410
+ let branch = '';
411
+ let turns = 0;
412
+ try {
413
+ const content = fs.readFileSync(filePath, 'utf-8');
414
+ const lines = content.split('\n').filter(l => l.trim());
415
+ for (const line of lines) {
416
+ const event = JSON.parse(line);
417
+ if (event.timestamp && event.timestamp > lastTimestamp) {
418
+ lastTimestamp = event.timestamp;
419
+ }
420
+ if (event.gitBranch && !branch) {
421
+ branch = event.gitBranch;
422
+ }
423
+ if (event.type === 'user' && event.message?.role === 'user') {
424
+ const msgContent = event.message.content;
425
+ const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
426
+ if (!isToolResult) {
427
+ turns++;
428
+ if (!firstUserMessage) {
429
+ if (typeof msgContent === 'string') {
430
+ firstUserMessage = msgContent.slice(0, 100);
431
+ }
432
+ else if (Array.isArray(msgContent)) {
433
+ const textBlock = msgContent.find((c) => c.type === 'text');
434
+ if (textBlock?.text) {
435
+ firstUserMessage = textBlock.text.slice(0, 100);
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ if (event.type === 'assistant' && event.message?.model && !model) {
442
+ model = event.message.model;
443
+ }
444
+ }
445
+ }
446
+ catch {
447
+ continue;
448
+ }
449
+ if (!lastTimestamp)
450
+ continue;
451
+ sessions.push({
452
+ sessionId,
453
+ lastMessageTime: lastTimestamp,
454
+ firstUserMessage: firstUserMessage || '(无消息)',
455
+ model: model || 'unknown',
456
+ turns,
457
+ branch: branch || 'unknown',
458
+ });
459
+ }
460
+ sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
461
+ return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
462
+ }
463
+ catch (error) {
464
+ logger.error('[CommandHandler] /resume failed:', error);
465
+ return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
466
+ }
467
+ }
468
+ // /baseagent 命令:查看或切换 Agent 后端
469
+ if (normalizedContent === '/baseagent' || normalizedContent.startsWith('/baseagent ')) {
470
+ const args = normalizedContent.slice(10).trim();
471
+ // 切换(带参)需权限:群聊 owner only,私聊 admin+;无参查询对所有人放开
472
+ if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
473
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
474
+ }
475
+ const available = this.getAvailableBaseagents(channel);
476
+ if (!args) {
477
+ // currentAgent: 当前 session 的 baseagent,或该 channel 所属 evolagent 的 baseagent
478
+ const currentAgent = activeSession?.agentId
479
+ || this.agentRegistry?.resolveByChannel(channel)?.baseagent
480
+ || this.parseDefaultBaseagent();
481
+ // 尝试发送 CommandCard 卡片
482
+ if (this.interactionRouter && available.length > 1) {
483
+ const interaction = {
484
+ type: 'interaction',
485
+ id: `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
486
+ channelId,
487
+ sessionId: activeSession?.id || `agent-${Date.now()}`,
488
+ initiatorId: userId,
489
+ kind: {
490
+ kind: 'command-card',
491
+ title: '🔌 切换 Agent',
492
+ buttons: available.map((a) => ({
493
+ label: a === currentAgent ? `✓ ${a}` : a,
494
+ command: `/baseagent ${a}`,
495
+ style: (a === currentAgent ? 'primary' : 'default'),
496
+ disabled: a === currentAgent,
497
+ })),
498
+ },
499
+ };
500
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
501
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: activeChatType === 'group' ? isOwner : isAdmin });
502
+ if (cardResult === null)
503
+ return null;
504
+ return { kind: 'command.result', text: cardResult };
505
+ }
506
+ // 降级:文本
507
+ const list = available.map((a) => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
508
+ const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
509
+ if (canSwitchAgent) {
510
+ return { kind: 'command.result', text: `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n用法: /baseagent <name>` };
511
+ }
512
+ return { kind: 'command.result', text: `当前 Agent: ${currentAgent}` };
513
+ }
514
+ if (!available.includes(args)) {
515
+ return { kind: 'command.error', text: `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}` };
516
+ }
517
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
518
+ if ('error' in result)
519
+ return { kind: 'command.error', text: result.error };
520
+ const { session } = result;
521
+ // 取消原会话的 pending 权限请求和交互卡片
522
+ if (this.permissionGateway) {
523
+ this.permissionGateway.cancelAll(session.id);
524
+ }
525
+ if (this.interactionRouter) {
526
+ this.interactionRouter.cancelAll(session.id);
527
+ }
528
+ // 切换到目标 agent(恢复已有会话或创建新会话)
529
+ const newSession = await this.sessionManager.switchAgent(channel, channelId, session.projectPath, args);
530
+ const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
531
+ const projectName = this.getProjectName(session.projectPath);
532
+ let agentSwitchResponse = `✓ 已切换 Agent: ${args}\n 项目: ${projectName}\n 会话: ${displaySessionTitle(newSession.name, '(未命名)')}\n ${hasExistingSession}`;
533
+ if (this.shouldSuppressCardTriggerResult(source, channel))
534
+ return null;
535
+ return { kind: 'command.result', text: agentSwitchResponse };
536
+ }
537
+ // /setmodel 命令:返回 JSON 格式的模型列表(供程序解析)
538
+ if (normalizedContent === '/setmodel' || normalizedContent.startsWith('/setmodel ')) {
539
+ const setmodelResult = await this.ensureSession(channel, channelId, threadId, chatType);
540
+ if ('error' in setmodelResult)
541
+ return { kind: 'command.result', text: setmodelResult.error };
542
+ const { session: setmodelSession } = setmodelResult;
543
+ const setmodelAgent = this.getAgent(channel, setmodelSession.agentId);
544
+ const currentModel = hasModelSwitcher(setmodelAgent) ? setmodelAgent.getModel() : setmodelAgent.name;
545
+ const efforts = getAvailableEfforts(setmodelAgent, currentModel);
546
+ const currentEffort = setmodelAgent.getEffort?.() || 'auto';
547
+ const now = Math.floor(Date.now() / 1000);
548
+ const modelIds = hasModelSwitcher(setmodelAgent) ? await setmodelAgent.listModels() : [];
549
+ const modelListData = {
550
+ object: 'list',
551
+ data: modelIds.map(id => ({ id, object: 'model', created: now, owned_by: setmodelAgent.name === 'codex' ? 'openai' : 'anthropic' })),
552
+ };
553
+ return { kind: 'command.result', text: JSON.stringify({
554
+ current_model: currentModel,
555
+ current_effort: currentEffort,
556
+ available_efforts: efforts,
557
+ models: modelListData,
558
+ }, null, 2) };
559
+ }
560
+ // /model 命令:查看或切换模型/推理强度
561
+ if (normalizedContent.startsWith('/model')) {
562
+ const args = normalizedContent.slice(6).trim();
563
+ // 获取当前会话(话题会话可能绑定不同 agent)
564
+ const modelResult = await this.ensureSession(channel, channelId, threadId, chatType);
565
+ if ('error' in modelResult)
566
+ return { kind: 'command.result', text: modelResult.error };
567
+ const { session: modelSession } = modelResult;
568
+ const modelAgent = this.getAgent(channel, modelSession.agentId);
569
+ const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
570
+ if (!args) {
571
+ const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
572
+ const efforts = getAvailableEfforts(modelAgent, currentModel);
573
+ const currentEffort = modelAgent.getEffort?.() || 'auto';
574
+ // 尝试发送 CommandCard 卡片
575
+ if (this.interactionRouter && models.length > 0) {
576
+ const interaction = {
577
+ type: 'interaction',
578
+ id: `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
579
+ channelId,
580
+ sessionId: modelSession.id,
581
+ initiatorId: userId,
582
+ kind: {
583
+ kind: 'command-card',
584
+ title: '🤖 切换模型',
585
+ buttons: models.map((m) => {
586
+ const display = modelDisplayLabel(modelAgent, m);
587
+ return {
588
+ label: m === currentModel ? `✓ ${display}` : display,
589
+ command: `/model ${m}`,
590
+ style: (m === currentModel ? 'primary' : 'default'),
591
+ disabled: m === currentModel,
592
+ };
593
+ }),
594
+ },
595
+ };
596
+ const replyCtx = this.getReplyContext(modelSession);
597
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
598
+ if (cardResult === null)
599
+ return null;
600
+ return { kind: 'command.result', text: cardResult };
601
+ }
602
+ // 降级:文本
603
+ const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
604
+ const effortHint = efforts.length > 0
605
+ ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
606
+ : '';
607
+ if (isAdmin) {
608
+ return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}\n\n可用模型:\n${modelList}\n\n用法: /model <模型>` };
609
+ }
610
+ return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}` };
611
+ }
612
+ // 带参(切换/调整)需 admin+;无参查询已在上方返回
613
+ if (!isAdmin)
614
+ return { kind: 'command.error', text: '❌ 无权限:切换模型仅限管理员使用' };
615
+ const parts = args.split(/\s+/);
616
+ let newModel;
617
+ let newEffort;
618
+ if (parts.length === 1) {
619
+ const arg = parts[0];
620
+ const currentModel = hasModelSwitcher(modelAgent) ? modelAgent.getModel() : modelAgent.name;
621
+ const efforts = getAvailableEfforts(modelAgent, currentModel);
622
+ // effort 相关参数统一转发到 /effort
623
+ if (efforts.includes(arg) || arg === 'auto') {
624
+ const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
625
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
626
+ }
627
+ else if (allEfforts.includes(arg)) {
628
+ return { kind: 'command.error', text: `⚠️ 请使用 /effort ${arg} 调整推理强度` };
629
+ }
630
+ else {
631
+ const resolvedArg = hasModelSwitcher(modelAgent) ? (modelAgent.resolveModelId?.(arg) ?? arg) : arg;
632
+ if (models.includes(resolvedArg)) {
633
+ newModel = resolvedArg;
634
+ }
635
+ else if (models.includes(arg)) {
636
+ newModel = arg;
637
+ }
638
+ else {
639
+ const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
640
+ const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
641
+ return { kind: 'command.error', text: `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}` };
642
+ }
643
+ }
644
+ }
645
+ else {
646
+ // 双参数:model effort
647
+ const [modelArgRaw, effortArg] = parts;
648
+ const modelArg = hasModelSwitcher(modelAgent)
649
+ ? (models.includes(modelArgRaw) ? modelArgRaw : (modelAgent.resolveModelId?.(modelArgRaw) ?? modelArgRaw))
650
+ : modelArgRaw;
651
+ if (!models.includes(modelArg)) {
652
+ return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArgRaw}` };
653
+ }
654
+ const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
655
+ if (targetEfforts.length === 0) {
656
+ return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
657
+ }
658
+ if (!targetEfforts.includes(effortArg)) {
659
+ const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
660
+ return { kind: 'command.result', text: `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}` };
661
+ }
662
+ newModel = modelArg;
663
+ newEffort = effortArg;
664
+ }
665
+ // 运行时 model/effort 切换已通过 EvolAgent.setBaseagentModel/setBaseagentEffort 持久化
666
+ const isCodexAgent = modelAgent.name === 'codex';
667
+ const changes = [];
668
+ if (newModel) {
669
+ modelAgent.setModel?.(newModel);
670
+ this.eventBus.publish({
671
+ type: 'runner:model-changed',
672
+ sessionId: modelSession.id,
673
+ model: newModel,
674
+ timestamp: Date.now()
675
+ });
676
+ changes.push(`模型: ${newModel}`);
677
+ }
678
+ if (newEffort) {
679
+ modelAgent.setEffort?.(newEffort);
680
+ changes.push(`推理强度: ${newEffort}`);
681
+ }
682
+ // 持久化:agent-owned channel 写到 agent.json;default 走原"就近原则"
683
+ if (newModel) {
684
+ const err = this.persistBaseagentModel(channel, modelAgent.name, newModel);
685
+ if (err)
686
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
687
+ }
688
+ if (newEffort) {
689
+ const err = this.persistBaseagentEffort(channel, modelAgent.name, newEffort);
690
+ if (err)
691
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
692
+ }
693
+ if (this.shouldSuppressCardTriggerResult(source, channel))
694
+ return null;
695
+ return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
696
+ }
697
+ // /effort 命令:查看或切换推理强度
698
+ if (normalizedContent.startsWith('/effort')) {
699
+ const args = normalizedContent.slice(7).trim();
700
+ const effortResult = await this.ensureSession(channel, channelId, threadId, chatType);
701
+ if ('error' in effortResult)
702
+ return { kind: 'command.result', text: effortResult.error };
703
+ const { session: effortSession } = effortResult;
704
+ const effortAgent = this.getAgent(channel, effortSession.agentId);
705
+ const currentModel = hasModelSwitcher(effortAgent) ? effortAgent.getModel() : effortAgent.name;
706
+ const efforts = getAvailableEfforts(effortAgent, currentModel);
707
+ const currentEffort = effortAgent.getEffort?.() || 'auto';
708
+ if (efforts.length === 0) {
709
+ return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
710
+ }
711
+ if (!args) {
712
+ // /effort(无参数):显示当前推理强度 + 发送 CommandCard 卡片
713
+ if (this.interactionRouter) {
714
+ const allItems = [...efforts, 'auto'];
715
+ const interaction = {
716
+ type: 'interaction',
717
+ id: `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
718
+ channelId,
719
+ sessionId: effortSession.id,
720
+ initiatorId: userId,
721
+ kind: {
722
+ kind: 'command-card',
723
+ title: '⚡ 推理强度',
724
+ buttons: allItems.map(e => ({
725
+ label: e === currentEffort ? `✓ ${e}` : e,
726
+ command: `/effort ${e}`,
727
+ style: (e === currentEffort ? 'primary' : 'default'),
728
+ disabled: e === currentEffort,
729
+ })),
730
+ },
731
+ };
732
+ const replyCtx = this.getReplyContext(effortSession);
733
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
734
+ if (cardResult === null)
735
+ return null;
736
+ return { kind: 'command.result', text: cardResult };
737
+ }
738
+ // 降级:文本
739
+ const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
740
+ const effortOptions = [...efforts, 'auto'].join(' / ');
741
+ if (isAdmin) {
742
+ return { kind: 'command.result', text: `推理强度: ${effortDisplay} 可选: ${effortOptions} 用法: /effort <level>` };
743
+ }
744
+ return { kind: 'command.result', text: `推理强度: ${effortDisplay}` };
745
+ }
746
+ // 带参(切换)需 admin+;无参查询已在上方返回
747
+ if (!isAdmin)
748
+ return { kind: 'command.error', text: '❌ 无权限:切换推理强度仅限管理员使用' };
749
+ // /effort auto:恢复 SDK 默认
750
+ if (args === 'auto') {
751
+ effortAgent.setEffort?.(undefined);
752
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, undefined);
753
+ if (err)
754
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
755
+ if (this.shouldSuppressCardTriggerResult(source, channel))
756
+ return null;
757
+ return { kind: 'command.result', text: '✓ 推理强度已恢复为 auto (SDK默认)' };
758
+ }
759
+ // /effort <level>:切换推理强度
760
+ if (!efforts.includes(args)) {
761
+ if (allEfforts.includes(args)) {
762
+ return { kind: 'command.error', text: `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}` };
763
+ }
764
+ return { kind: 'command.error', text: `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto` };
765
+ }
766
+ const newEffort = args;
767
+ effortAgent.setEffort?.(newEffort);
768
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
769
+ if (err)
770
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
771
+ if (this.shouldSuppressCardTriggerResult(source, channel))
772
+ return null;
773
+ return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
774
+ }
775
+ // /reload [aid] — 热重载 agent 配置
776
+ // daemon owner:可 reload 任意 aid(无参则 reload 自身所在 agent)
777
+ // agent channel owner/admin:仅可 reload 自身 agent
778
+ if (normalizedContent === '/reload' || normalizedContent.startsWith('/reload ')) {
779
+ const aidArg = normalizedContent.slice('/reload'.length).trim() || undefined;
780
+ const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
781
+ // 权限判断:daemon owner 或 agent channel 的 owner/admin
782
+ if (!isDaemonOwner && !isAdmin) {
783
+ return { kind: 'command.error', text: '❌ 无权限:/reload 仅限 daemon owner 或 agent owner/admin 使用' };
784
+ }
785
+ // agent channel 的 owner/admin 不能跨 agent reload
786
+ if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
787
+ return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
788
+ }
789
+ const targetAid = aidArg ?? selfAid;
790
+ if (!targetAid) {
791
+ return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
792
+ }
793
+ // 繁忙检查(同 menu /agent reload)
794
+ const busy = getAgentBusyCount(this, targetAid);
795
+ if (busy !== null && busy > 0) {
796
+ return { kind: 'command.error', text: `❌ 该 Agent 有 ${busy} 个任务执行中,请稍后重试` };
797
+ }
798
+ const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '');
799
+ if ('error' in res)
800
+ return { kind: 'command.error', text: `❌ reload 失败:${res.error}` };
801
+ return { kind: 'command.result', text: `✅ Agent ${targetAid} 配置已重载` };
802
+ }
803
+ // /agent, /aid, /rpc, /storage — 仅限 ctl 调用,slash 输入拒绝
804
+ if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ') ||
805
+ normalizedContent === '/aid' || normalizedContent.startsWith('/aid ') ||
806
+ normalizedContent === '/rpc' || normalizedContent.startsWith('/rpc ') ||
807
+ normalizedContent === '/storage' || normalizedContent.startsWith('/storage ')) {
808
+ return { kind: 'command.error', text: '❌ 此命令仅限 ctl 调用,不支持 slash 输入' };
809
+ }
810
+ if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
811
+ const activityArg = normalizedContent.slice(9).trim();
812
+ // 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
813
+ if (activityArg && !isAdmin)
814
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
815
+ // proactive 模式下流式输出全部静默,activity 配置无意义
816
+ if (activeSession?.sessionMode === 'proactive') {
817
+ return { kind: 'command.error', text: '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)' };
818
+ }
819
+ const modeMap = {
820
+ all: 'all',
821
+ dm: 'dm-only',
822
+ owner: 'owner-dm-only',
823
+ none: 'none',
824
+ };
825
+ const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
826
+ // 模式描述列表(用于 body 和文本降级)
827
+ const modeDescriptions = [
828
+ { key: 'all', configVal: 'all', label: '全部显示' },
829
+ { key: 'dm', configVal: 'dm-only', label: '仅私聊显示' },
830
+ { key: 'owner', configVal: 'owner-dm-only', label: '仅 owner 私聊显示' },
831
+ { key: 'none', configVal: 'none', label: '全部静默' },
832
+ ];
833
+ if (!activityArg) {
834
+ // 尝试发送 CommandCard 卡片
835
+ {
836
+ const interaction = {
837
+ type: 'interaction',
838
+ id: `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
839
+ channelId,
840
+ sessionId: activeSession?.id || '',
841
+ initiatorId: userId,
842
+ kind: {
843
+ kind: 'command-card',
844
+ title: '📋 中间输出模式',
845
+ body: modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n'),
846
+ buttons: modeDescriptions.map(m => ({
847
+ label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
848
+ command: `/activity ${m.key}`,
849
+ style: (m.configVal === currentMode ? 'primary' : 'default'),
850
+ disabled: m.configVal === currentMode,
851
+ })),
852
+ },
853
+ };
854
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
855
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
856
+ if (cardResult === null)
857
+ return null;
858
+ // 卡片降级:fall through 到下方文本输出
859
+ }
860
+ // 降级:文本
861
+ const modeList = modeDescriptions.map(m => {
862
+ const prefix = m.configVal === currentMode ? '✓' : '•';
863
+ return ` ${prefix} ${m.key} — ${m.label}`;
864
+ }).join('\n');
865
+ if (isOwner) {
866
+ return { kind: 'command.result', text: `中间输出: ${currentMode} 用法: /activity <all|dm|owner|none>` };
867
+ }
868
+ return { kind: 'command.result', text: `中间输出: ${currentMode}` };
869
+ }
870
+ const newMode = modeMap[activityArg];
871
+ if (!newMode) {
872
+ return { kind: 'command.error', text: `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none` };
873
+ }
874
+ const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
875
+ if (newMode === currentMode) {
876
+ return { kind: 'command.result', text: `📋 中间输出模式已是 ${activityArg}(${label})` };
877
+ }
878
+ // 切换操作仅 owner
879
+ if (!isOwner)
880
+ return { kind: 'command.error', text: '❌ 中间输出模式切换仅限 owner' };
881
+ if (this.agentRegistry?.setShowActivities) {
882
+ this.agentRegistry.setShowActivities(channel, newMode);
883
+ }
884
+ else {
885
+ return { kind: 'command.error', text: `⚠️ 找不到通道 "${channel}" 所属的 self-agent,无法持久化` };
886
+ }
887
+ if (this.shouldSuppressCardTriggerResult(source, channel))
888
+ return null;
889
+ return { kind: 'command.result', text: `✅ 中间输出模式: ${activityArg}(${label})` };
890
+ }
891
+ // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
892
+ // - 查看:所有人可用
893
+ // - 设置:单聊任何角色可设置;群聊仅管理员可设置
894
+ if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
895
+ const chatmodeResult = await this.ensureSession(channel, channelId, threadId, chatType);
896
+ if ('error' in chatmodeResult)
897
+ return { kind: 'command.result', text: chatmodeResult.error };
898
+ const chatmodeSession = chatmodeResult.session;
899
+ const arg = normalizedContent.slice(9).trim();
900
+ const currentMode = chatmodeSession.sessionMode || 'interactive';
901
+ const chatmodeChatType = chatmodeSession.chatType || activeChatType;
902
+ const isGroup = chatmodeChatType === 'group';
903
+ const canSwitch = !isGroup;
904
+ if (!arg) {
905
+ if (isGroup) {
906
+ return { kind: 'command.result', text: `📋 会话模式: proactive(群聊强制)` };
907
+ }
908
+ // 尝试发送 CommandCard 卡片
909
+ if (canSwitch) {
910
+ const modes = [
911
+ { key: 'interactive', name: '交互模式', desc: '被动响应:收到消息时才回复,回复直接显示' },
912
+ { key: 'proactive', name: '主动模式', desc: '主动推进:流式输出静默,由 Agent 自调 ctl send 发声' },
913
+ ];
914
+ const interaction = {
915
+ type: 'interaction',
916
+ id: `chatmode-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
917
+ channelId,
918
+ sessionId: chatmodeSession.id,
919
+ initiatorId: userId,
920
+ kind: {
921
+ kind: 'command-card',
922
+ title: '🔄 会话模式',
923
+ body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
924
+ buttons: modes.map(m => ({
925
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
926
+ command: `/chatmode ${m.key}`,
927
+ style: (m.key === currentMode ? 'primary' : 'default'),
928
+ disabled: m.key === currentMode,
929
+ })),
930
+ },
931
+ };
932
+ const replyCtx = this.getReplyContext(chatmodeSession);
933
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
934
+ if (cardResult === null)
935
+ return null;
936
+ // 卡片降级:fall through 到下方文本输出
937
+ }
938
+ // 降级:文本
939
+ if (canSwitch) {
940
+ return { kind: 'command.result', text: `会话模式: ${currentMode} 用法: /chatmode <interactive|proactive>` };
941
+ }
942
+ return { kind: 'command.result', text: `会话模式: ${currentMode}` };
943
+ }
944
+ if (arg !== 'interactive' && arg !== 'proactive') {
945
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: interactive / proactive` };
946
+ }
947
+ // 群聊强制 proactive,不可切换
948
+ if ((chatmodeSession.chatType || activeChatType) === 'group') {
949
+ return { kind: 'command.error', text: '❌ 群聊强制 proactive 模式,不可切换' };
950
+ }
951
+ if (arg === currentMode) {
952
+ return { kind: 'command.result', text: `📋 当前会话模式已是 ${arg}` };
953
+ }
954
+ // 仅在真正需要切换时才要求会话空闲
955
+ if (threadId) {
956
+ const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
957
+ if (threadSession) {
958
+ const threadAgent = this.getAgent(channel, threadSession.agentId);
959
+ if (threadAgent.hasActiveStream(threadSession.id) || this.messageQueue?.isProcessing(threadSession.id)) {
960
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
961
+ }
962
+ }
963
+ }
964
+ else if (agent.hasActiveStream(chatmodeSession.id) || this.messageQueue?.isProcessing(chatmodeSession.id)) {
965
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
966
+ }
967
+ await this.sessionManager.updateSession(chatmodeSession.id, { sessionMode: arg });
968
+ this.eventBus.publish({ type: 'session:chat-mode-changed', sessionId: chatmodeSession.id, mode: arg, timestamp: Date.now() });
969
+ if (this.shouldSuppressCardTriggerResult(source, channel))
970
+ return null;
971
+ return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
972
+ }
973
+ // /dispatch 命令:查看/切换群聊分发模式(mention | broadcast)
974
+ // 仅群聊可用;群聊中设置需管理员权限
975
+ if (normalizedContent === '/dispatch' || normalizedContent.startsWith('/dispatch ')) {
976
+ const dispatchResult = await this.ensureSession(channel, channelId, threadId, chatType);
977
+ if ('error' in dispatchResult)
978
+ return { kind: 'command.result', text: dispatchResult.error };
979
+ const dispatchSession = dispatchResult.session;
980
+ const dispatchChatType = dispatchSession.chatType || activeChatType;
981
+ if (dispatchChatType !== 'group') {
982
+ return { kind: 'command.error', text: '❌ /dispatch 仅在群聊中可用' };
983
+ }
984
+ const arg = normalizedContent.slice(9).trim();
985
+ const currentMode = dispatchSession.metadata?.dispatchModeOverride ?? dispatchSession.metadata?.dispatchMode ?? null;
986
+ if (!arg) {
987
+ const displayMode = currentMode ?? '未设置(跟随群设置)';
988
+ // 尝试发送 CommandCard 卡片
989
+ if (isAdmin) {
990
+ const modes = [
991
+ { key: 'mention', name: '提及模式', desc: '仅当被 @ 提及(含 @all)时响应群消息' },
992
+ { key: 'broadcast', name: '广播模式', desc: '群内所有消息都触发响应' },
993
+ ];
994
+ const interaction = {
995
+ type: 'interaction',
996
+ id: `dispatch-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
997
+ channelId,
998
+ sessionId: dispatchSession.id,
999
+ initiatorId: userId,
1000
+ kind: {
1001
+ kind: 'command-card',
1002
+ title: '📡 分发模式',
1003
+ body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
1004
+ buttons: modes.map(m => ({
1005
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
1006
+ command: `/dispatch ${m.key}`,
1007
+ style: (m.key === currentMode ? 'primary' : 'default'),
1008
+ disabled: m.key === currentMode,
1009
+ })),
1010
+ },
1011
+ };
1012
+ const replyCtx = this.getReplyContext(dispatchSession);
1013
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1014
+ if (cardResult === null)
1015
+ return null;
1016
+ // 卡片降级:fall through 到下方文本输出
1017
+ }
1018
+ // 降级:文本
1019
+ if (isAdmin) {
1020
+ return { kind: 'command.result', text: `分发模式: ${displayMode} 用法: /dispatch <mention|broadcast|clear>` };
1021
+ }
1022
+ return { kind: 'command.result', text: `分发模式: ${displayMode}` };
1023
+ }
1024
+ if (arg !== 'mention' && arg !== 'broadcast' && arg !== 'clear') {
1025
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention / broadcast / clear\n用法: /dispatch <模式>` };
1026
+ }
1027
+ if (!isAdmin) {
1028
+ return { kind: 'command.error', text: '❌ 无权限:群聊中切换分发模式仅限管理员使用' };
1029
+ }
1030
+ if (arg === 'clear') {
1031
+ if (!dispatchSession.metadata?.dispatchModeOverride) {
1032
+ return { kind: 'command.result', text: '当前无本地覆盖,已跟随群设置' };
1033
+ }
1034
+ const { dispatchModeOverride: _, ...rest } = dispatchSession.metadata;
1035
+ await this.sessionManager.updateSession(dispatchSession.id, { metadata: rest });
1036
+ this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: undefined, timestamp: Date.now() });
1037
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1038
+ return null;
1039
+ return { kind: 'command.result', text: '✅ 已清除本地覆盖,将跟随群设置' };
1040
+ }
1041
+ if (arg === currentMode) {
1042
+ return { kind: 'command.result', text: `当前已是 ${arg}` };
1043
+ }
1044
+ const metadata = { ...(dispatchSession.metadata || {}), dispatchModeOverride: arg };
1045
+ await this.sessionManager.updateSession(dispatchSession.id, { metadata });
1046
+ this.eventBus.publish({ type: 'session:dispatch-mode-changed', sessionId: dispatchSession.id, mode: arg, timestamp: Date.now() });
1047
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1048
+ return null;
1049
+ return { kind: 'command.result', text: `✅ 分发模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
1050
+ }
1051
+ // /stop 命令:中断当前任务
1052
+ if (normalizedContent === '/stop') {
1053
+ const stopResult = await this.ensureSession(channel, channelId, threadId, chatType);
1054
+ if ('error' in stopResult)
1055
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
1056
+ const { session: stopSession } = stopResult;
1057
+ const stopAgent = this.getAgent(channel, stopSession.agentId);
1058
+ const sessionKey = stopSession.id;
1059
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
1060
+ const hasActive = stopAgent.hasActiveStream(sessionKey);
1061
+ const isProcessing = this.messageQueue.isProcessing(sessionKey);
1062
+ if (queueLength === 0 && !hasActive && !isProcessing) {
1063
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
1064
+ }
1065
+ await stopAgent.interrupt(sessionKey);
1066
+ // 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
1067
+ this.eventBus.publish({
1068
+ type: 'task:interrupted',
1069
+ sessionId: sessionKey,
1070
+ reason: 'stop',
1071
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
1072
+ });
1073
+ // 强制清除 processing_state
1074
+ this.sessionManager.clearProcessing(sessionKey);
1075
+ return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
1076
+ }
1077
+ // /clear 已移除:Claude/Codex/Gemini 对“清空当前 backend 历史”的语义不一致。
1078
+ // 统一使用 /new 创建新会话来开始全新上下文。
1079
+ if (normalizedContent === '/clear') {
1080
+ return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
1081
+ }
1082
+ // /compact 命令:手动压缩会话上下文
1083
+ if (normalizedContent === '/compact') {
1084
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
1085
+ if ('error' in result)
1086
+ return { kind: 'command.error', text: result.error };
1087
+ const { session } = result;
1088
+ const sessionAgent = this.getAgent(channel, session.agentId);
1089
+ if (!sessionAgent.capabilities?.compact) {
1090
+ return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact` };
1091
+ }
1092
+ if (!session.agentSessionId) {
1093
+ return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
1094
+ }
1095
+ const projectPath = path.isAbsolute(session.projectPath)
1096
+ ? session.projectPath
1097
+ : path.resolve(process.cwd(), session.projectPath);
1098
+ const releaseLock = this.messageQueue.acquireLock(session.id);
1099
+ try {
1100
+ if (sendMessage) {
1101
+ await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
1102
+ }
1103
+ const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
1104
+ if (compacted) {
1105
+ return {
1106
+ kind: 'command.result',
1107
+ text: '✅ 会话压缩完成',
1108
+ };
1109
+ }
1110
+ else {
1111
+ return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
1112
+ }
1113
+ }
1114
+ finally {
1115
+ releaseLock();
1116
+ }
1117
+ }
1118
+ // 尝试获取活跃会话(话题时直接查找话题 session)
1119
+ let session;
1120
+ const resolvedSelfAID = selfAID ?? this.resolveSelfAID(channel);
1121
+ if (threadId) {
1122
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, chatType, undefined, resolvedSelfAID, this.resolveChannelType(channel));
1123
+ }
1124
+ else {
1125
+ session = await this.sessionManager.getActiveSession(channel, channelId);
1126
+ }
1127
+ // 如果没有会话,自动创建(所有后续命令都需要 session)
1128
+ if (!session) {
1129
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, chatType, undefined, resolvedSelfAID, this.resolveChannelType(channel));
1130
+ }
1131
+ // /status 命令:显示会话状态
1132
+ if (normalizedContent === '/status') {
1133
+ // session 现在总是存在(上面已自动创建)
1134
+ if (!session) {
1135
+ return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
1136
+ }
1137
+ const sessionKey = this.getQueueKey(session, channel, channelId);
1138
+ const sessionAgent = this.getAgent(channel, session.agentId);
1139
+ const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
1140
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
1141
+ const isThread = !!session.threadId;
1142
+ let sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
1143
+ // 处理中时显示时长
1144
+ if (isCurrentlyProcessing) {
1145
+ const elapsed = Date.now() - parseInt(session.processingState, 10);
1146
+ if (!isNaN(elapsed) && elapsed > 0) {
1147
+ const sec = Math.floor(elapsed / 1000);
1148
+ sessionStatus = sec < 60 ? `处理中 (${sec}秒)` :
1149
+ sec < 3600 ? `处理中 (${Math.floor(sec / 60)}分钟)` :
1150
+ `处理中 (${Math.floor(sec / 3600)}小时)`;
1151
+ }
1152
+ }
1153
+ const projectName = this.getProjectName(session.projectPath);
1154
+ const owningAgent = this.getOwningAgent(channel);
1155
+ const agentName = owningAgent?.name ?? 'DefaultAgent';
1156
+ const health = await this.sessionManager.getHealthStatus(session.id);
1157
+ const timeSinceSuccess = Date.now() - health.lastSuccessTime;
1158
+ const timeStr = timeSinceSuccess < 60000 ? '刚刚' :
1159
+ timeSinceSuccess < 3600000 ? `${Math.floor(timeSinceSuccess / 60000)}分钟前` :
1160
+ `${Math.floor(timeSinceSuccess / 3600000)}小时前`;
1161
+ // 获取会话文件信息并同步 name
1162
+ let sessionTurns = 0;
1163
+ if (session.agentSessionId) {
1164
+ const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId, session.agentId);
1165
+ sessionTurns = fileInfo.turns;
1166
+ if (fileInfo.title && fileInfo.title !== session.name) {
1167
+ await this.sessionManager.renameSession(session.id, fileInfo.title);
1168
+ session.name = fileInfo.title;
1169
+ }
1170
+ }
1171
+ const lines = [];
1172
+ const sessionMode = session.sessionMode || 'interactive';
1173
+ const dispatchMode = session.metadata?.dispatchModeOverride ?? session.metadata?.dispatchMode ?? '未设置(跟随群设置)';
1174
+ const chatModeLine = `会话模式: ${sessionMode}`;
1175
+ const dispatchModeLine = session.chatType === 'group' ? `分发模式: ${dispatchMode}` : null;
1176
+ if (isAdmin) {
1177
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${displaySessionTitle(session.name, '(未命名)')}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`);
1178
+ if (health.consecutiveErrors > 0) {
1179
+ lines.push(`异常计数: ${health.consecutiveErrors}`);
1180
+ }
1181
+ lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
1182
+ }
1183
+ else {
1184
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1185
+ }
1186
+ if (health.lastError) {
1187
+ lines.push('');
1188
+ lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
1189
+ lines.push(`错误信息: ${health.lastError.substring(0, 100)}`);
1190
+ }
1191
+ return { kind: 'command.result', text: lines.join('\n') };
1192
+ }
1193
+ // /new 命令:创建新会话(支持命名)
1194
+ if (normalizedContent.startsWith('/new')) {
1195
+ const sessionName = normalizedContent.slice(4).trim() || undefined;
1196
+ if (sessionName) {
1197
+ const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
1198
+ if (existing) {
1199
+ return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
1200
+ }
1201
+ }
1202
+ const projectPath = this.getEffectiveDefaultPath(channel);
1203
+ if (sendMessage && session) {
1204
+ await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
1205
+ }
1206
+ const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, session?.agentId || this.primaryRunnerKey);
1207
+ this.eventBus.publish({
1208
+ type: 'session:created',
1209
+ sessionId: newSession.id,
1210
+ channel,
1211
+ channelId,
1212
+ projectPath,
1213
+ name: sessionName,
1214
+ timestamp: Date.now()
1215
+ });
1216
+ if (session) {
1217
+ // Reset agent backend state so the new
1218
+ // session starts with a fresh conversation history
1219
+ await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
1220
+ await agent.closeSession(session.id);
1221
+ }
1222
+ return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 之前的对话历史已保留,可通过 /s 查看` };
1223
+ }
1224
+ // /check 命令:检查渠道状态(guest 可用,详情仅 admin)
1225
+ if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
1226
+ // 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
1227
+ // __ecweb__ 是 ECWeb 系统级入口,展示全量渠道
1228
+ const checkOwningAgent = this.getOwningAgent(channel);
1229
+ let allowedChannels;
1230
+ if (checkOwningAgent) {
1231
+ allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
1232
+ }
1233
+ else if (channel === '__ecweb__') {
1234
+ // ECWeb 全局视图:展示所有渠道
1235
+ allowedChannels = new Set(this.adapters.keys());
1236
+ }
1237
+ else {
1238
+ // default 范围:不再有 default channel 概念,等价于"所有 channel"
1239
+ const defaultNames = [];
1240
+ for (const [name] of this.adapters) {
1241
+ const owner = this.agentRegistry?.resolveByChannel(name);
1242
+ if (!owner)
1243
+ defaultNames.push(name);
1244
+ }
1245
+ allowedChannels = new Set(defaultNames);
1246
+ }
1247
+ // Default: show system health check (non-admin 仅看摘要)
1248
+ const checkAgentName = checkOwningAgent?.name ?? 'DefaultAgent';
1249
+ const lines = [`📡 渠道状态 (Agent: ${checkAgentName}):`];
1250
+ // Group by channelType
1251
+ const groups = new Map();
1252
+ for (const [name] of this.adapters) {
1253
+ if (!allowedChannels.has(name))
1254
+ continue;
1255
+ const type = this.resolveChannelType(name);
1256
+ const ch = this.channelObjects.get(name);
1257
+ let status;
1258
+ if (ch?.getStatus) {
1259
+ const s = ch.getStatus();
1260
+ status = s.connected ? '✓ 已连接' : '⏳ 重连中';
1261
+ }
1262
+ else {
1263
+ status = '✓ 已注册';
1264
+ }
1265
+ if (!groups.has(type))
1266
+ groups.set(type, []);
1267
+ groups.get(type).push({ name, status });
1268
+ }
1269
+ if (!isAdmin) {
1270
+ // guest/user: 仅显示渠道健康摘要
1271
+ const total = [...groups.values()].flat().length;
1272
+ const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
1273
+ lines.push(` ${healthy}/${total} 渠道正常`);
1274
+ return { kind: 'command.result', text: lines.join('\n') };
1275
+ }
1276
+ for (const [type, instances] of groups) {
1277
+ if (instances.length === 1) {
1278
+ lines.push(` ${type}: ${instances[0].status}`);
1279
+ }
1280
+ else {
1281
+ const parts = instances.map(i => {
1282
+ const seg = i.name.split('#');
1283
+ const instName = seg.length >= 3 ? seg.slice(2).join('#') : i.name;
1284
+ return `${i.status.includes('✓') ? '✓' : '⏳'} ${instName}`;
1285
+ });
1286
+ lines.push(` ${type}: ${parts.join(', ')}`);
1287
+ }
1288
+ }
1289
+ // 当前 agent 名(用于 agent 维度 stats / queue 查询)
1290
+ const currentAgentName = checkOwningAgent?.name ?? '<unknown>';
1291
+ // 队列状态(按当前 agent 维度)
1292
+ lines.push('', '📬 队列状态:');
1293
+ lines.push(` 待处理消息: ${this.messageQueue.getQueueLengthByAgent(currentAgentName)}`);
1294
+ lines.push(` 处理中队列: ${this.messageQueue.getProcessingCountByAgent(currentAgentName)}`);
1295
+ // 运行概况(全局,进程级)
1296
+ lines.push('', '🖥️ 运行概况:');
1297
+ const uptimeMs = this.statsCollector
1298
+ ? this.statsCollector.getSnapshot().uptimeMs
1299
+ : process.uptime() * 1000;
1300
+ lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
1301
+ // 近 1 小时统计(按当前 agent 维度)
1302
+ if (this.statsCollector) {
1303
+ const snap = this.statsCollector.getSnapshot(currentAgentName);
1304
+ const h = snap.lastHour;
1305
+ lines.push('', '📊 近 1 小时统计:');
1306
+ lines.push(` 收到消息: ${h.received}`);
1307
+ lines.push(` 完成处理: ${h.completed}`);
1308
+ if (h.errors > 0) {
1309
+ const breakdown = Object.entries(h.errorsByType).map(([t, c]) => `${t}: ${c}`).join(', ');
1310
+ lines.push(` 处理出错: ${h.errors} (${breakdown})`);
1311
+ }
1312
+ else {
1313
+ lines.push(` 处理出错: 0`);
1314
+ }
1315
+ if (h.toolErrors > 0) {
1316
+ const toolBreakdown = Object.entries(h.toolErrorsByName).map(([t, c]) => `${t}: ${c}`).join(', ');
1317
+ lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
1318
+ }
1319
+ lines.push(` 被中断: ${h.interrupts}`);
1320
+ if (h.completed > 0) {
1321
+ lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
1322
+ }
1323
+ }
1324
+ const checkSnap = this.statsCollector?.getSnapshot(currentAgentName);
1325
+ // AUN 渠道的 per-AID 连接富状态(reconnect / flap / lastError / kick)
1326
+ const aidStateByName = new Map();
1327
+ for (const [cname, cobj] of this.channelObjects) {
1328
+ if (typeof cobj?.getAidState === 'function') {
1329
+ try {
1330
+ aidStateByName.set(cname, cobj.getAidState());
1331
+ }
1332
+ catch { /* ignore */ }
1333
+ }
1334
+ }
1335
+ // 单个渠道实例的健康快照:基础连接态 + AUN 富状态
1336
+ const channelHealth = (cname) => {
1337
+ const type = this.resolveChannelType(cname);
1338
+ const cobj = this.channelObjects.get(cname);
1339
+ const seg = cname.split('#');
1340
+ const instName = seg.length >= 3 ? seg.slice(2).join('#') : cname;
1341
+ const aidState = aidStateByName.get(cname);
1342
+ // cobj 缺失 = 渠道未注册(如 disabled agent),视为未连接;
1343
+ // cobj 存在但无 getStatus = 已注册的活实例,视为已连接。
1344
+ let connected = cobj ? (cobj.getStatus ? !!cobj.getStatus().connected : true) : false;
1345
+ const h = { name: cname, instName, type, connected };
1346
+ if (aidState) {
1347
+ connected = aidState.status === 'connected';
1348
+ h.connected = connected;
1349
+ h.aidStatus = aidState.status;
1350
+ h.reconnectCount = aidState.reconnectCount ?? 0;
1351
+ h.flapCount = aidState.flapCount ?? 0;
1352
+ if (aidState.lastConnectedAt)
1353
+ h.lastConnectedAt = aidState.lastConnectedAt;
1354
+ if (aidState.lastError)
1355
+ h.lastError = String(aidState.lastError).slice(0, 80);
1356
+ if (aidState.kickDetail?.reason)
1357
+ h.kickReason = String(aidState.kickDetail.reason).slice(0, 80);
1358
+ }
1359
+ return h;
1360
+ };
1361
+ // 以 EvolAgent 为中心聚合:后端 + 渠道健康 + 负载,并记录已归属渠道
1362
+ const ownedNames = new Set();
1363
+ const evolagents = (this.agentRegistry?.list() ?? []).map((ag) => {
1364
+ const chans = (ag.channels ?? []).map((n) => { ownedNames.add(n); return channelHealth(n); });
1365
+ const processing = this.messageQueue.getProcessingCountByAgent(ag.name);
1366
+ const pending = this.messageQueue.getQueueLengthByAgent(ag.name);
1367
+ return {
1368
+ name: ag.name, aid: ag.aid ?? '', status: ag.status,
1369
+ baseagent: ag.baseagent ?? null,
1370
+ model: ag.model ?? null,
1371
+ effort: ag.effort ?? null,
1372
+ projectPath: ag.projectPath ?? null,
1373
+ processing, pending,
1374
+ activeTasks: processing + pending,
1375
+ activeSessions: ag.activeSessions ?? 0,
1376
+ lastActivity: ag.lastActivity ?? 0,
1377
+ error: ag.error,
1378
+ channels: chans,
1379
+ };
1380
+ });
1381
+ // 未归属到任何 EvolAgent 的渠道(系统级 / DefaultAgent)
1382
+ const unownedChannels = [];
1383
+ for (const [cname] of this.adapters) {
1384
+ if (!allowedChannels.has(cname) || ownedNames.has(cname))
1385
+ continue;
1386
+ unownedChannels.push(channelHealth(cname));
1387
+ }
1388
+ const structured = {
1389
+ channels: [...groups.entries()].map(([type, instances]) => ({ type, instances })),
1390
+ queue: {
1391
+ pending: this.messageQueue.getQueueLengthByAgent(currentAgentName),
1392
+ processing: this.messageQueue.getProcessingCountByAgent(currentAgentName),
1393
+ },
1394
+ uptimeMs,
1395
+ lastHour: checkSnap?.lastHour ?? null,
1396
+ evolagents,
1397
+ unownedChannels,
1398
+ };
1399
+ return { kind: 'command.result', text: lines.join('\n'), structured };
1400
+ }
1401
+ // /restart 命令:重启服务(进程级,仅 daemon owner)
1402
+ if (normalizedContent === '/restart') {
1403
+ // 进程级操作:必须是 daemon owner(evolclaw.json.owners),与 menu 协议 /system restart 一致。
1404
+ // agent-channel 的 owner 角色不足以重启整个 daemon。
1405
+ if (!isDaemonOwner) {
1406
+ return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 daemon owner 使用' };
1407
+ }
1408
+ const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
1409
+ const busy = getAgentBusyCount(this, selfAid);
1410
+ if (busy !== null && busy > 0) {
1411
+ return { kind: 'command.error', text: `❌ 该 Agent 有 ${busy} 个任务执行中,请稍后重试` };
1412
+ }
1413
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
1414
+ const sessionsWithMessages = allSessions
1415
+ .filter((s) => this.messageCache.hasMessages(s.id))
1416
+ .map((s) => {
1417
+ const count = this.messageCache.getCount(s.id);
1418
+ return `${s.projectPath} 有 ${count} 条新消息`;
1419
+ });
1420
+ // 执行重启逻辑(共用于卡片回调和文本确认)
1421
+ const executeRestart = async () => {
1422
+ let replyContext;
1423
+ if (threadId) {
1424
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, undefined, undefined, selfAID ?? this.resolveSelfAID(channel), this.resolveChannelType(channel));
1425
+ replyContext = this.getReplyContext(threadSession);
1426
+ }
1427
+ const restartInfo = {
1428
+ channel,
1429
+ channelId,
1430
+ timestamp: Date.now(),
1431
+ ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
1432
+ };
1433
+ fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
1434
+ const { spawn } = await import('child_process');
1435
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
1436
+ detached: true,
1437
+ stdio: 'ignore',
1438
+ env: { ...process.env, EVOLCLAW_HOME: resolvePaths().root }
1439
+ }).unref();
1440
+ this.eventBus.publish({ type: 'system:restart', channel, channelId });
1441
+ // 先发送重启反馈消息,等待发送完成后再 kill 进程
1442
+ // 避免消息还没发出去进程就退出了
1443
+ const adapter = this.adapters.get(channel);
1444
+ if (adapter) {
1445
+ try {
1446
+ const envelope = buildEnvelope({
1447
+ taskId: `restart-${Date.now()}`,
1448
+ channel,
1449
+ channelId,
1450
+ agentName: 'system',
1451
+ chatmode: 'interactive',
1452
+ replyContext,
1453
+ });
1454
+ await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
1455
+ // 等待消息发送完成后再延迟 kill
1456
+ await new Promise(resolve => setTimeout(resolve, 500));
1457
+ }
1458
+ catch (err) {
1459
+ logger.error('[System] Failed to send restart notification:', err);
1460
+ }
1461
+ }
1462
+ // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
1463
+ // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
1464
+ // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
1465
+ setTimeout(() => {
1466
+ logger.info('[System] Restarting by user command...');
1467
+ process.kill(process.pid, 'SIGTERM');
1468
+ }, 1000);
1469
+ return true;
1470
+ };
1471
+ // 文本确认流程
1472
+ if (sessionsWithMessages.length > 0) {
1473
+ const restartKey = `${channel}-${channelId}`;
1474
+ const restartConfirmFile = path.join(resolvePaths().dataDir, `restart-confirm-${restartKey}.json`);
1475
+ if (fs.existsSync(restartConfirmFile)) {
1476
+ const confirmInfo = JSON.parse(fs.readFileSync(restartConfirmFile, 'utf-8'));
1477
+ const now = Date.now();
1478
+ if (now - confirmInfo.timestamp < 10000) {
1479
+ fs.unlinkSync(restartConfirmFile);
1480
+ }
1481
+ else {
1482
+ fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: now }));
1483
+ return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
1484
+ }
1485
+ }
1486
+ else {
1487
+ fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: Date.now() }));
1488
+ return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
1489
+ }
1490
+ }
1491
+ await executeRestart();
1492
+ // executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
1493
+ return null;
1494
+ }
1495
+ // /upgrade 命令:检查版本更新,提示用户手动重启
1496
+ if (normalizedContent === '/upgrade') {
1497
+ if (!isAdmin)
1498
+ return { kind: 'command.error', text: '❌ 无权限:升级检查仅限管理员使用' };
1499
+ if (isLinkedInstall()) {
1500
+ return { kind: 'command.result', text: '⏭ 开发模式,跳过升级检查' };
1501
+ }
1502
+ const localVer = getLocalVersion();
1503
+ const remoteVer = await checkLatestVersion();
1504
+ if (!remoteVer) {
1505
+ return { kind: 'command.result', text: `⚠️ 无法连接 npm registry(当前版本 ${localVer})` };
1506
+ }
1507
+ if (compareVersions(localVer, remoteVer) >= 0) {
1508
+ return { kind: 'command.result', text: `✓ 已是最新版本 (${localVer})` };
1509
+ }
1510
+ return { kind: 'command.result', text: `📦 发现新版本 ${localVer} → ${remoteVer}\n执行 /restart 升级` };
1511
+ }
1512
+ // /pwd 命令:显示当前项目路径
1513
+ if (normalizedContent === '/pwd') {
1514
+ if (!session) {
1515
+ return { kind: 'command.error', text: `❌ 无法创建会话,请检查配置` };
1516
+ }
1517
+ const configName = this.getConfiguredProjectName(session.projectPath);
1518
+ if (configName) {
1519
+ return { kind: 'command.result', text: `当前项目: ${configName}\n路径: ${session.projectPath}` };
1520
+ }
1521
+ return { kind: 'command.result', text: `当前项目: ${session.projectPath}` };
1522
+ }
1523
+ // /file 命令:发送项目内文件,支持 /file path 和 /file channel path
1524
+ if (normalizedContent.startsWith('/file')) {
1525
+ if (!isAdmin)
1526
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
1527
+ // 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
1528
+ // 还原: 将 [text](url) 替换为 text
1529
+ const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
1530
+ if (!rawArg) {
1531
+ const usage = isOwner
1532
+ ? '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md'
1533
+ : '用法: /file <相对路径>\n示例: /file src/index.ts';
1534
+ return { kind: 'command.result', text: usage };
1535
+ }
1536
+ // 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
1537
+ const tokens = rawArg.split(/\s+/);
1538
+ let targetChannel = channel;
1539
+ let targetLabel = channel;
1540
+ let filePath = rawArg;
1541
+ if (tokens.length >= 2) {
1542
+ const spec = tokens[0];
1543
+ if (this.adapters.has(spec)) {
1544
+ // 精确实例名
1545
+ targetChannel = spec;
1546
+ targetLabel = spec;
1547
+ filePath = tokens.slice(1).join(' ');
1548
+ }
1549
+ else {
1550
+ // 按 channelType 查找第一个匹配的实例
1551
+ for (const [name] of this.adapters) {
1552
+ if ((this.channelTypeMap.get(name) || name) === spec) {
1553
+ targetChannel = name;
1554
+ targetLabel = spec;
1555
+ filePath = tokens.slice(1).join(' ');
1556
+ break;
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+ const isCrossChannel = targetChannel !== channel;
1562
+ // 跨通道不属于当前关系级操作,仍仅限 owner。
1563
+ if (isCrossChannel && identity.role !== 'owner') {
1564
+ return { kind: 'command.error', text: '❌ 跨通道发送仅限 owner' };
1565
+ }
1566
+ // 找目标 adapter
1567
+ const targetAdapter = this.adapters.get(targetChannel);
1568
+ if (!targetAdapter) {
1569
+ return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 未启用或不存在` };
1570
+ }
1571
+ if (!targetAdapter.capabilities?.file) {
1572
+ return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 不支持文件发送` };
1573
+ }
1574
+ // 获取 session(需要 projectPath)
1575
+ const sendResult = await this.ensureSession(channel, channelId, threadId, chatType);
1576
+ if ('error' in sendResult)
1577
+ return { kind: 'command.result', text: sendResult.error };
1578
+ const sendSession = sendResult.session;
1579
+ // 路径安全校验
1580
+ if (path.isAbsolute(filePath)) {
1581
+ return { kind: 'command.error', text: '❌ 不支持绝对路径\n请使用项目内的相对路径' };
1582
+ }
1583
+ if (filePath.split(path.sep).includes('..') || filePath.split('/').includes('..')) {
1584
+ return { kind: 'command.error', text: '❌ 不支持 .. 路径穿越' };
1585
+ }
1586
+ const resolvedPath = path.resolve(sendSession.projectPath, filePath);
1587
+ // 存在性检查
1588
+ if (!fs.existsSync(resolvedPath)) {
1589
+ return { kind: 'command.error', text: `❌ 文件不存在: ${filePath}` };
1590
+ }
1591
+ // 符号链接安全:realpath 后验证仍在项目目录内
1592
+ const realPath = fs.realpathSync(resolvedPath);
1593
+ const realProjectPath = fs.realpathSync(sendSession.projectPath);
1594
+ if (!realPath.startsWith(realProjectPath + path.sep) && realPath !== realProjectPath) {
1595
+ return { kind: 'command.error', text: '❌ 路径不允许: 文件不在项目目录内' };
1596
+ }
1597
+ const stat = fs.statSync(resolvedPath);
1598
+ if (stat.isDirectory()) {
1599
+ return { kind: 'command.error', text: '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持' };
1600
+ }
1601
+ const MAX_SIZE = 10 * 1024 * 1024;
1602
+ if (stat.size > MAX_SIZE) {
1603
+ return { kind: 'command.error', text: `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)` };
1604
+ }
1605
+ // 找目标 channelId
1606
+ let targetChannelId = channelId;
1607
+ if (isCrossChannel) {
1608
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel);
1609
+ targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
1610
+ if (!targetChannelId) {
1611
+ return { kind: 'command.error', text: `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息` };
1612
+ }
1613
+ }
1614
+ // 发送文件
1615
+ try {
1616
+ const replyCtx = isCrossChannel ? undefined : this.getReplyContext(sendSession);
1617
+ await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
1618
+ const sizeStr = stat.size < 1024 ? `${stat.size} B`
1619
+ : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
1620
+ : `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
1621
+ return { kind: 'command.result', text: isCrossChannel
1622
+ ? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
1623
+ : `✅ 已发送: ${filePath} (${sizeStr})` };
1624
+ }
1625
+ catch (error) {
1626
+ logger.error('[CommandHandler] /file failed:', error);
1627
+ return { kind: 'command.error', text: `❌ 文件发送失败: ${error.message || error}` };
1628
+ }
1629
+ }
1630
+ // /slist 命令:列出当前项目的会话
1631
+ // /slist — 仅 EvolClaw 会话
1632
+ // /slist cli — 仅 CLI 会话(未导入的)
1633
+ if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
1634
+ if (!session) {
1635
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
1636
+
1637
+ 请先执行以下操作之一:
1638
+ 1. 发送任意消息 - 自动创建新会话
1639
+ 2. /new [名称] - 创建命名会话` };
1640
+ }
1641
+ const showCliOnly = normalizedContent === '/slist cli';
1642
+ // /slist cli — 仅显示 CLI 会话
1643
+ if (showCliOnly) {
1644
+ const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
1645
+ if (!canImportCli) {
1646
+ return { kind: 'command.error', text: '❌ 当前无权查看 CLI 会话' };
1647
+ }
1648
+ const cliSessions = await this.sessionManager.scanCliSessions(session.projectPath, session.agentId);
1649
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
1650
+ const currentProjectSessions = sessions.filter((s) => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
1651
+ const dbSessionIds = new Set(currentProjectSessions.map((s) => s.agentSessionId).filter(Boolean));
1652
+ const orphanCliSessions = cliSessions.filter((c) => !dbSessionIds.has(c.uuid));
1653
+ if (orphanCliSessions.length === 0) {
1654
+ return { kind: 'command.result', text: `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话` };
1655
+ }
1656
+ // 构建显示数据(复用于卡片和文本)
1657
+ const cliDisplayItems = orphanCliSessions.map((c) => {
1658
+ const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
1659
+ const message = this.sessionManager.readSessionFirstMessage(session.projectPath, c.uuid, session.agentId) || '(无消息)';
1660
+ const uuid = c.uuid.substring(0, 8);
1661
+ return { uuid, fullUuid: c.uuid, time, message };
1662
+ });
1663
+ // 尝试发送 CommandCard 卡片
1664
+ if (this.interactionRouter && cliDisplayItems.length > 0) {
1665
+ const bodyLines = cliDisplayItems.map((item) => `• ${item.time} (${item.uuid}) "${item.message}"`);
1666
+ const interaction = {
1667
+ type: 'interaction',
1668
+ id: `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1669
+ channelId,
1670
+ sessionId: session.id,
1671
+ initiatorId: userId,
1672
+ kind: {
1673
+ kind: 'command-card',
1674
+ title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
1675
+ body: bodyLines.join('\n'),
1676
+ buttons: cliDisplayItems.map((item) => ({
1677
+ label: item.uuid,
1678
+ command: `/session ${item.uuid}`,
1679
+ style: 'default',
1680
+ })),
1681
+ },
1682
+ };
1683
+ const replyCtx = this.getReplyContext(session);
1684
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
1685
+ if (cardResult === null)
1686
+ return null;
1687
+ return { kind: 'command.result', text: cardResult };
1688
+ }
1689
+ // 降级:文本列表
1690
+ const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
1691
+ for (const item of cliDisplayItems) {
1692
+ lines.push(` ${item.time} (${item.uuid}) "${item.message}"`);
1693
+ }
1694
+ lines.push('');
1695
+ lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
1696
+ return { kind: 'command.result', text: lines.join('\n') };
1697
+ }
1698
+ // /slist — 仅显示 EvolClaw 会话
1699
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
1700
+ const currentProjectSessions = sessions.filter((s) => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
1701
+ // 从 SDK 同步会话名称(发现 CLI 改名)
1702
+ try {
1703
+ const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.agentId);
1704
+ for (const sdkSession of sdkSessions) {
1705
+ if (!sdkSession.title)
1706
+ continue;
1707
+ const dbSession = currentProjectSessions.find((s) => s.agentSessionId === sdkSession.sessionId);
1708
+ if (dbSession && sdkSession.title !== dbSession.name) {
1709
+ await this.sessionManager.renameSession(dbSession.id, sdkSession.title);
1710
+ dbSession.name = sdkSession.title;
1711
+ }
1712
+ }
1713
+ }
1714
+ catch (error) {
1715
+ logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
1716
+ }
1717
+ // 构建可显示会话列表(复用于卡片和文本)
1718
+ const maxDisplay = 10;
1719
+ const displaySessions = [];
1720
+ let displayIndex = 0;
1721
+ for (let i = 0; i < currentProjectSessions.length; i++) {
1722
+ const s = currentProjectSessions[i];
1723
+ if (displayIndex >= maxDisplay)
1724
+ break;
1725
+ const isActive = s.metadata?.isActive === true;
1726
+ displayIndex++;
1727
+ const name = displaySessionTitle(s.name, '(未命名)');
1728
+ const idleTime = formatIdleTime(Date.now() - s.updatedAt);
1729
+ const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.agentId));
1730
+ let status = '[空闲]';
1731
+ if (fileMissing) {
1732
+ status = '[会话文件缺失]';
1733
+ }
1734
+ else if (!!s.processingState) {
1735
+ status = '[处理中]';
1736
+ }
1737
+ else if (isActive) {
1738
+ status = '[活跃]';
1739
+ }
1740
+ displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
1741
+ }
1742
+ // 尝试发送 CommandCard 卡片(每个会话一个按钮,一键切换)
1743
+ if (this.interactionRouter && displaySessions.length >= 1) {
1744
+ const bodyLines = displaySessions.map(ds => {
1745
+ const prefix = ds.isActive ? '✓' : '•';
1746
+ const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
1747
+ const fileMark = ds.fileMissing ? '❌ ' : '';
1748
+ return `${prefix} ${ds.index}. ${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
1749
+ });
1750
+ const interaction = {
1751
+ type: 'interaction',
1752
+ id: `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1753
+ channelId,
1754
+ sessionId: session.id,
1755
+ initiatorId: userId,
1756
+ kind: {
1757
+ kind: 'command-card',
1758
+ title: `📋 ${path.basename(session.projectPath)} 会话列表`,
1759
+ body: bodyLines.join('\n'),
1760
+ buttons: displaySessions.map(ds => {
1761
+ const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
1762
+ return {
1763
+ label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
1764
+ command: `/session ${ds.index}`,
1765
+ style: (ds.isActive ? 'primary' : 'default'),
1766
+ disabled: ds.isActive,
1767
+ };
1768
+ }),
1769
+ },
1770
+ };
1771
+ const replyCtx = this.getReplyContext(session);
1772
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
1773
+ if (cardResult === null)
1774
+ return null;
1775
+ return { kind: 'command.result', text: cardResult };
1776
+ }
1777
+ // 降级:文本列表
1778
+ const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.agentId}] 会话列表:`, ''];
1779
+ if (currentProjectSessions.length > 0) {
1780
+ for (const ds of displaySessions) {
1781
+ const prefix = ds.isActive ? ' ✓' : ' ';
1782
+ const num = `${ds.index}.`;
1783
+ const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
1784
+ if (ds.fileMissing) {
1785
+ lines.push(`${prefix} ${num} ❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
1786
+ }
1787
+ else {
1788
+ lines.push(`${prefix} ${num} ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
1789
+ }
1790
+ }
1791
+ const hiddenCount = currentProjectSessions.length - displayIndex;
1792
+ if (hiddenCount > 0) {
1793
+ const parts = [];
1794
+ if (hiddenCount > 0)
1795
+ parts.push(`${hiddenCount} 个更早的会话`);
1796
+ lines.push(`\n (已隐藏 ${parts.join('、')})`);
1797
+ }
1798
+ lines.push('');
1799
+ }
1800
+ lines.push('使用 /s <序号、name或8位uuid> 切换会话');
1801
+ lines.push('使用 /s cli 查看 CLI 会话');
1802
+ return { kind: 'command.result', text: lines.join('\n') };
1803
+ }
1804
+ // /session(无参数):直接复用 /slist 逻辑(含卡片交互)
1805
+ if (normalizedContent === '/session') {
1806
+ const delegated = await this.handle('/slist', channel, channelId, undefined, userId, threadId);
1807
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
1808
+ }
1809
+ // /session cli(= /s cli):列出未导入的 CLI 会话
1810
+ if (normalizedContent === '/session cli') {
1811
+ const delegated = await this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
1812
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
1813
+ }
1814
+ // /session 或 /s 命令:切换会话
1815
+ if (normalizedContent.startsWith('/session ')) {
1816
+ const sessionName = normalizedContent.slice(9).trim();
1817
+ if (!sessionName)
1818
+ return { kind: 'command.result', text: '用法: /s <序号、会话名称或前8位UUID>' };
1819
+ let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
1820
+ // 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
1821
+ if (!targetSession && /^\d+$/.test(sessionName) && session) {
1822
+ const idx = parseInt(sessionName, 10);
1823
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
1824
+ const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
1825
+ if (idx >= 1 && idx <= visibleSessions.length) {
1826
+ targetSession = visibleSessions[idx - 1];
1827
+ }
1828
+ else {
1829
+ return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
1830
+ }
1831
+ }
1832
+ if (!targetSession && sessionName.length >= 8) {
1833
+ targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
1834
+ }
1835
+ if (targetSession?.threadId) {
1836
+ return { kind: 'command.error', text: `❌ 话题会话不支持通过 /s 切换\n请在对应话题内继续对话` };
1837
+ }
1838
+ const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
1839
+ if (!targetSession && sessionName.length >= 8 && canImport) {
1840
+ const projectPaths = Object.values(this.projects);
1841
+ if (session) {
1842
+ projectPaths.unshift(session.projectPath);
1843
+ }
1844
+ for (const projectPath of projectPaths) {
1845
+ const currentAgentId = session?.agentId || this.primaryRunnerKey;
1846
+ const cliSessions = await this.sessionManager.scanCliSessions(projectPath, currentAgentId);
1847
+ const cliSession = cliSessions.find((c) => c.uuid.startsWith(sessionName));
1848
+ if (cliSession) {
1849
+ const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentAgentId);
1850
+ this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
1851
+ const projectName = this.getProjectName(projectPath);
1852
+ return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${displaySessionTitle(imported.name, '(未命名)')}\n 项目: ${projectName}\n 将继续之前的对话历史` };
1853
+ }
1854
+ }
1855
+ }
1856
+ if (!targetSession) {
1857
+ return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
1858
+ }
1859
+ const lastInput = targetSession.agentSessionId
1860
+ ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
1861
+ : null;
1862
+ const lastInputLine = lastInput ? `\n 最后输入: "${lastInput}"` : '';
1863
+ if (!session) {
1864
+ const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
1865
+ if (!switched) {
1866
+ return { kind: 'command.error', text: `❌ 切换会话失败` };
1867
+ }
1868
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1869
+ return null;
1870
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
1871
+ }
1872
+ if (targetSession.id === session.id) {
1873
+ return { kind: 'command.result', text: `当前已在会话: ${displaySessionTitle(targetSession.name, sessionName)}` };
1874
+ }
1875
+ // 阻止从主会话切换到话题会话
1876
+ if (!session.threadId && targetSession.threadId) {
1877
+ return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
1878
+ }
1879
+ const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
1880
+ if (!switched) {
1881
+ return { kind: 'command.error', text: `❌ 切换会话失败` };
1882
+ }
1883
+ this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
1884
+ const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
1885
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1886
+ return null;
1887
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}${continueHint}${lastInputLine}` };
1888
+ }
1889
+ // /rename 或 /name 命令:重命名当前会话
1890
+ if (normalizedContent === '/rename' || normalizedContent === '/name') {
1891
+ return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
1892
+ }
1893
+ if (normalizedContent.startsWith('/rename ')) {
1894
+ const newName = normalizedContent.slice(8).trim();
1895
+ if (!newName)
1896
+ return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
1897
+ if (!session) {
1898
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
1899
+
1900
+ 请先执行以下操作之一:
1901
+ 1. 发送任意消息 - 自动创建新会话
1902
+ 2. /new [名称] - 创建命名会话
1903
+ 3. /session <名称> - 切换到已有会话` };
1904
+ }
1905
+ const existing = await this.sessionManager.getSessionByName(channel, channelId, newName);
1906
+ if (existing && existing.id !== session.id) {
1907
+ return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
1908
+ }
1909
+ const oldName = displaySessionTitle(session.name, '(未命名)');
1910
+ const success = await this.sessionManager.renameSession(session.id, newName);
1911
+ if (success && session.agentSessionId) {
1912
+ const renameAgent = this.getAgent(channel, session.agentId);
1913
+ await renameAgent.setSessionName?.(session.agentSessionId, newName).catch((error) => {
1914
+ logger.debug('[CommandHandler] Backend session rename sync failed:', error);
1915
+ });
1916
+ }
1917
+ if (!success) {
1918
+ return { kind: 'command.error', text: `❌ 重命名失败` };
1919
+ }
1920
+ this.eventBus.publish({ type: 'session:renamed', sessionId: session.id, oldName, newName });
1921
+ return { kind: 'command.result', text: `✓ 已将当前会话重命名为: ${newName}` };
1922
+ }
1923
+ // /del 命令:删除指定会话(仅解绑,不删除文件)
1924
+ if (normalizedContent.startsWith('/del ')) {
1925
+ const sessionName = normalizedContent.slice(5).trim();
1926
+ if (!sessionName)
1927
+ return { kind: 'command.result', text: '用法: /del <序号、会话名称或前8位UUID>' };
1928
+ if (!session) {
1929
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话` };
1930
+ }
1931
+ // 权限检查:policy 控制谁可以删除会话
1932
+ if (!policy.canDeleteSession(session.chatType || 'private', identity.role)) {
1933
+ return { kind: 'command.error', text: `❌ 无权限:群聊中仅管理员可删除会话` };
1934
+ }
1935
+ let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
1936
+ // 序号删除(与 /slist 显示序号一致)
1937
+ if (!targetSession && /^\d+$/.test(sessionName)) {
1938
+ const idx = parseInt(sessionName, 10);
1939
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
1940
+ const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.agentId === session.agentId && !s.threadId);
1941
+ if (idx >= 1 && idx <= visibleSessions.length) {
1942
+ targetSession = visibleSessions[idx - 1];
1943
+ }
1944
+ else {
1945
+ return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
1946
+ }
1947
+ }
1948
+ if (!targetSession && sessionName.length >= 8) {
1949
+ targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
1950
+ }
1951
+ if (targetSession?.threadId) {
1952
+ return { kind: 'command.error', text: `❌ 请使用话题管理删除话题会话` };
1953
+ }
1954
+ if (!targetSession) {
1955
+ return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
1956
+ }
1957
+ if (targetSession.id === session.id) {
1958
+ return { kind: 'command.error', text: `❌ 无法删除当前活跃会话\n请先切换到其他会话` };
1959
+ }
1960
+ const success = await this.sessionManager.unbindSession(targetSession.id);
1961
+ if (!success) {
1962
+ return { kind: 'command.error', text: `❌ 删除失败` };
1963
+ }
1964
+ this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
1965
+ const targetAgent = this.getAgent(channel, targetSession.agentId);
1966
+ await targetAgent.closeSession(targetSession.id);
1967
+ return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
1968
+ }
1969
+ // /fork 命令:分支当前会话
1970
+ if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
1971
+ const forkName = normalizedContent.slice(5).trim() || undefined;
1972
+ if (!session) {
1973
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话,无法分支` };
1974
+ }
1975
+ if (!session.agentSessionId) {
1976
+ return { kind: 'command.error', text: `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork` };
1977
+ }
1978
+ const forkAgent = this.getAgent(channel, session.agentId);
1979
+ if (!forkAgent.capabilities?.fork) {
1980
+ return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
1981
+ }
1982
+ try {
1983
+ const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
1984
+ const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
1985
+ await forkAgent.updateSessionMetadata?.(forkedSessionId, {
1986
+ gitInfo: {
1987
+ branch: null,
1988
+ commitHash: null,
1989
+ repositoryUrl: null,
1990
+ },
1991
+ evolclawSessionId: newSession.id,
1992
+ sourceSessionId: session.id,
1993
+ }).catch((error) => {
1994
+ logger.debug('[CommandHandler] Backend fork metadata sync failed:', error);
1995
+ });
1996
+ this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
1997
+ return { kind: 'command.result', text: `✅ 会话已分支: ${displaySessionTitle(newSession.name, '(未命名)')}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
1998
+ }
1999
+ catch (error) {
2000
+ logger.error('[CommandHandler] Fork session failed:', error);
2001
+ return { kind: 'command.error', text: `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}` };
2002
+ }
2003
+ }
2004
+ // /rewind 命令:查看历史 / 回退会话
2005
+ if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
2006
+ const result = await this.ensureSession(channel, channelId, threadId, chatType);
2007
+ if ('error' in result)
2008
+ return { kind: 'command.error', text: result.error };
2009
+ const { session } = result;
2010
+ const rewindAgent = this.getAgent(channel, session.agentId);
2011
+ if (!session.agentSessionId) {
2012
+ return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
2013
+ }
2014
+ if (!rewindAgent.getSessionMessages) {
2015
+ return { kind: 'command.error', text: `❌ 当前 Agent (${rewindAgent.name}) 不支持 /rewind` };
2016
+ }
2017
+ const args = normalizedContent.slice('/rewind'.length).trim();
2018
+ if (!args) {
2019
+ return { kind: 'command.result', text: await this.handleRewindList(session, rewindAgent) };
2020
+ }
2021
+ // 带参(执行回退,会删除文件/改对话)需 admin+
2022
+ if (!isAdmin)
2023
+ return { kind: 'command.error', text: '❌ 无权限:回退操作仅限管理员使用' };
2024
+ const parts = args.split(/\s+/);
2025
+ const turnNum = parseInt(parts[0], 10);
2026
+ if (isNaN(turnNum) || turnNum < 1) {
2027
+ return { kind: 'command.error', text: '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)' };
2028
+ }
2029
+ const mode = parts[1]?.toLowerCase();
2030
+ if (!mode) {
2031
+ return { kind: 'command.error', text: `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)` };
2032
+ }
2033
+ if (!['chat', 'file', 'all'].includes(mode)) {
2034
+ return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
2035
+ }
2036
+ return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
2037
+ }
2038
+ // /repair 命令:检查并修复会话文件
2039
+ if (normalizedContent === '/repair') {
2040
+ const repairResult = await this.ensureSession(channel, channelId, threadId, chatType);
2041
+ if ('error' in repairResult)
2042
+ return { kind: 'command.result', text: repairResult.error };
2043
+ const { session: repairSession } = repairResult;
2044
+ const repairAgent = this.getAgent(channel, repairSession.agentId);
2045
+ const { checkSessionFile, backupSessionFile } = await import('../session/session-file-health.js');
2046
+ try {
2047
+ if (!repairSession.agentSessionId) {
2048
+ await this.sessionManager.resetHealthStatus(repairSession.id);
2049
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器` };
2050
+ }
2051
+ // 通过 agent 定位 session 文件
2052
+ const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
2053
+ if (!sessionFile) {
2054
+ // 文件不存在(已被删除或从未创建),直接重置
2055
+ await this.sessionManager.resetHealthStatus(repairSession.id);
2056
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器` };
2057
+ }
2058
+ const healthCheck = await checkSessionFile(sessionFile);
2059
+ if (healthCheck.corrupt) {
2060
+ const backupPath = await backupSessionFile(sessionFile);
2061
+ const fsPromises = await import('fs/promises');
2062
+ await fsPromises.unlink(sessionFile);
2063
+ await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
2064
+ repairAgent.updateSessionId(repairSession.id, '');
2065
+ await this.sessionManager.resetHealthStatus(repairSession.id);
2066
+ return { kind: 'command.result', text: `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}` };
2067
+ }
2068
+ if (healthCheck.issues.length > 0) {
2069
+ await this.sessionManager.resetHealthStatus(repairSession.id);
2070
+ return { kind: 'command.error', text: `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。` };
2071
+ }
2072
+ await this.sessionManager.resetHealthStatus(repairSession.id);
2073
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器` };
2074
+ }
2075
+ catch (error) {
2076
+ logger.error('[Repair] Failed:', error);
2077
+ return { kind: 'command.error', text: `❌ 修复失败: ${error.message}` };
2078
+ }
2079
+ }
2080
+ // /safe 命令:安全模式已禁用
2081
+ if (normalizedContent === '/safe') {
2082
+ return { kind: 'command.result', text: `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。` };
2083
+ }
2084
+ // /trigger 命令
2085
+ if (normalizedContent === '/trigger' || normalizedContent.startsWith('/trigger ')) {
2086
+ const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId);
2087
+ return { kind: 'command.result', text };
2088
+ }
2089
+ return null;
2090
+ }