evolclaw 2.1.0 → 2.1.1

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.
package/README.md CHANGED
@@ -128,10 +128,10 @@ evolclaw init wechat
128
128
  },
129
129
  "idleMonitor": {
130
130
  "enabled": true, // 任务超时监控开关
131
- "timeout": 120000, // 超时阈值(ms),默认 2 分钟
131
+ "timeout": 120, // 超时阈值(秒),默认 120
132
132
  "safeModeThreshold": 3 // 连续超时 N 次后进入安全模式(设为 0 禁用安全模式)
133
133
  },
134
- "flushDelay": 4000 // 工具活动消息聚合发送间隔(ms),默认 4 秒
134
+ "flushDelay": 4 // 工具活动消息聚合发送间隔(秒),默认 4 秒
135
135
  }
136
136
  ```
137
137
 
@@ -139,6 +139,7 @@ evolclaw init wechat
139
139
  - `apiKey`:配置文件 → `ANTHROPIC_AUTH_TOKEN` 环境变量 → `~/.claude/settings.json`
140
140
  - `baseUrl`:配置文件 → `ANTHROPIC_BASE_URL` 环境变量 → `~/.claude/settings.json`
141
141
  - `model`:配置文件 → `~/.claude/settings.json` → 默认 `sonnet`
142
+ - `effort`:配置文件 → `~/.claude/settings.json` → SDK 默认值(`auto`)
142
143
 
143
144
  ### 3. 运行
144
145
 
@@ -192,6 +193,7 @@ evolclaw/
192
193
  - `/slist` - 列出当前项目的所有会话
193
194
  - `/s <名称>` - 切换到指定会话
194
195
  - `/name <新名称>` - 重命名当前会话
196
+ - `/del <名称>` - 删除指定会话(仅解绑,不删除文件)
195
197
  - `/status` - 显示会话状态
196
198
  - `/help` - 显示所有命令
197
199
 
@@ -212,8 +214,11 @@ evolclaw/
212
214
  - `/safe` - 进入安全模式
213
215
 
214
216
  **模型管理**:
215
- - `/model` - 显示当前模型和可用列表
216
- - `/model <model-id>` - 切换模型
217
+ - `/model` - 显示当前模型和推理强度
218
+ - `/model <model>` - 切换模型
219
+ - `/model <effort>` - 切换推理强度(low / medium / high / max)
220
+ - `/model <model> <effort>` - 同时切换模型和推理强度
221
+ - `/model auto` - 恢复 SDK 默认推理强度
217
222
 
218
223
  ## 技术栈
219
224
 
@@ -228,7 +228,8 @@ export class FeishuChannel {
228
228
  logger.error('[Feishu] Failed to process message:', error);
229
229
  }
230
230
  },
231
- 'im.message.message_read_v1': async () => { }
231
+ 'im.message.message_read_v1': async () => { },
232
+ 'im.message.reaction.created_v1': async () => { }
232
233
  });
233
234
  this.wsClient = new lark.WSClient({
234
235
  appId: this.config.appId,
@@ -336,6 +337,11 @@ export class FeishuChannel {
336
337
  logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
337
338
  }
338
339
  catch (error) {
340
+ // 230011: 消息已被撤回,降级为普通消息重试
341
+ if (error.response?.data?.code === 230011 && options?.replyToMessageId) {
342
+ logger.warn('[Feishu] Message withdrawn (230011), retrying without reply');
343
+ return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
344
+ }
339
345
  logger.error('[Feishu] Failed to send message:', error);
340
346
  throw error;
341
347
  }
package/dist/cli.js CHANGED
@@ -537,8 +537,9 @@ async function cmdRestartMonitor() {
537
537
  for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
538
538
  log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
539
539
  await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
540
- // 调用 claude CLI 修复
541
- const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, log);
540
+ // 调用 claude CLI 修复(递增超时:3/4/5 分钟)
541
+ const timeout = (2 + attempt) * 60 * 1000;
542
+ const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, timeout, log);
542
543
  if (!healed) {
543
544
  log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
544
545
  continue;
@@ -633,7 +634,7 @@ async function spawnAndWaitReady(p, log, timeout) {
633
634
  /**
634
635
  * 调用 claude CLI 进行自动修复
635
636
  */
636
- async function invokeClaude(p, attempt, maxAttempts, log) {
637
+ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
637
638
  const projectDir = getPackageRoot();
638
639
  const selfHealLog = p.selfHealLog;
639
640
  const stdoutLog = path.join(p.logs, 'stdout.log');
@@ -659,26 +660,31 @@ async function invokeClaude(p, attempt, maxAttempts, log) {
659
660
 
660
661
  注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
661
662
  try {
662
- log(`Invoking claude CLI (attempt ${attempt})...`);
663
+ log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
663
664
  const { stdout, stderr } = await execFileAsync('claude', [
664
665
  '-p', prompt,
665
666
  '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
666
667
  '--output-format', 'text',
667
668
  ], {
668
669
  cwd: projectDir,
669
- timeout: 5 * 60 * 1000, // 5 分钟超时
670
+ timeout,
670
671
  env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
671
672
  maxBuffer: 10 * 1024 * 1024,
672
673
  });
673
674
  if (stdout)
674
675
  log(`Claude output: ${stdout.slice(0, 500)}`);
675
676
  if (stderr)
676
- log(`Claude stderr: ${stderr.slice(0, 200)}`);
677
+ log(`Claude stderr: ${stderr.slice(0, 500)}`);
677
678
  log(`Claude CLI completed (attempt ${attempt})`);
678
679
  return true;
679
680
  }
680
681
  catch (error) {
681
- log(`Claude CLI error: ${error.message?.slice(0, 300) || error}`);
682
+ const msg = error.message || String(error);
683
+ log(`Claude CLI error: ${msg.slice(0, 800)}`);
684
+ if (error.stdout)
685
+ log(`Stdout: ${String(error.stdout).slice(0, 500)}`);
686
+ if (error.stderr)
687
+ log(`Stderr: ${String(error.stderr).slice(0, 500)}`);
682
688
  return false;
683
689
  }
684
690
  }
package/dist/config.js CHANGED
@@ -37,7 +37,10 @@ export function resolveAnthropicConfig(config) {
37
37
  const model = config.agents?.anthropic?.model
38
38
  || settings.model
39
39
  || 'sonnet';
40
- return { apiKey, baseUrl, model };
40
+ const effort = config.agents?.anthropic?.effort
41
+ || settings.effortLevel
42
+ || undefined;
43
+ return { apiKey, baseUrl, model, effort };
41
44
  }
42
45
  export function loadConfig(configPath = resolvePaths().config) {
43
46
  if (!fs.existsSync(configPath)) {
@@ -10,6 +10,7 @@ import { encodePath } from '../utils/platform.js';
10
10
  export class AgentRunner {
11
11
  apiKey;
12
12
  model;
13
+ effort;
13
14
  baseUrl;
14
15
  config;
15
16
  activeSessions = new Map();
@@ -19,6 +20,7 @@ export class AgentRunner {
19
20
  constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
20
21
  this.apiKey = apiKey;
21
22
  this.model = model || 'sonnet';
23
+ this.effort = undefined;
22
24
  this.baseUrl = baseUrl;
23
25
  this.config = config;
24
26
  this.onSessionIdUpdate = onSessionIdUpdate;
@@ -38,10 +40,38 @@ export class AgentRunner {
38
40
  getModel() {
39
41
  return this.model;
40
42
  }
43
+ setEffort(effort) {
44
+ this.effort = effort;
45
+ }
46
+ getEffort() {
47
+ return this.effort;
48
+ }
49
+ syncFromUserSettings() {
50
+ try {
51
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
52
+ if (!fs.existsSync(settingsPath))
53
+ return;
54
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
55
+ if (settings.model && settings.model !== this.model) {
56
+ logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
57
+ this.model = settings.model;
58
+ }
59
+ const newEffort = settings.effortLevel || undefined;
60
+ if (newEffort !== this.effort) {
61
+ logger.info(`[AgentRunner] Synced effort from ~/.claude/settings.json: ${newEffort ?? 'auto'}`);
62
+ this.effort = newEffort;
63
+ }
64
+ }
65
+ catch (error) {
66
+ logger.debug(`[AgentRunner] Failed to sync from ~/.claude/settings.json:`, error);
67
+ }
68
+ }
41
69
  setCompactStartCallback(callback) {
42
70
  this.onCompactStart = callback;
43
71
  }
44
72
  async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
73
+ // 同步用户级配置到内存
74
+ this.syncFromUserSettings();
45
75
  ensureDir(projectPath);
46
76
  ensureDir(path.join(projectPath, '.claude'));
47
77
  // 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
@@ -117,9 +147,11 @@ export class AgentRunner {
117
147
  const useSettingSources = this.config?.agents?.anthropic?.useSettingSources !== false;
118
148
  const enableSummaries = this.config?.agents?.anthropic?.agentProgressSummaries !== false;
119
149
  // 公共 options(新旧模式共用)
150
+ logger.info(`[AgentRunner] runQuery model=${this.model} effort=${this.effort ?? 'auto'}`);
120
151
  const commonOptions = {
121
152
  cwd: projectPath,
122
153
  model: this.model,
154
+ ...(this.effort ? { effort: this.effort } : {}),
123
155
  canUseTool,
124
156
  permissionMode: 'default',
125
157
  persistSession: true,
@@ -1,9 +1,48 @@
1
1
  import { renameSession as sdkRenameSession, forkSession as sdkForkSession, listSessions as sdkListSessions } from '@anthropic-ai/claude-agent-sdk';
2
- import { saveConfig, resolvePaths, getPackageRoot } from '../config.js';
2
+ import { resolvePaths, getPackageRoot } from '../config.js';
3
3
  import { logger } from '../utils/logger.js';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
+ import os from 'os';
6
7
  const availableModels = ['opus', 'sonnet', 'haiku', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'];
8
+ const availableEfforts = ['low', 'medium', 'high', 'max'];
9
+ function effortBar(level) {
10
+ const levels = {
11
+ low: '◆◇◇◇', medium: '◆◆◇◇', high: '◆◆◆◇', max: '◆◆◆◆'
12
+ };
13
+ return levels[level] || '◆◆◇◇';
14
+ }
15
+ /**
16
+ * 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
17
+ */
18
+ function writeUserSettings(updates) {
19
+ try {
20
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
21
+ let settings = {};
22
+ if (fs.existsSync(settingsPath)) {
23
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
24
+ }
25
+ if (updates.model !== undefined)
26
+ settings.model = updates.model;
27
+ if (updates.effortLevel !== undefined) {
28
+ if (updates.effortLevel === null) {
29
+ delete settings.effortLevel;
30
+ }
31
+ else {
32
+ settings.effortLevel = updates.effortLevel;
33
+ }
34
+ }
35
+ const claudeDir = path.join(os.homedir(), '.claude');
36
+ if (!fs.existsSync(claudeDir)) {
37
+ fs.mkdirSync(claudeDir, { recursive: true });
38
+ }
39
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
40
+ return { success: true };
41
+ }
42
+ catch (error) {
43
+ return { success: false, error: error.message };
44
+ }
45
+ }
7
46
  /**
8
47
  * 计算两个字符串的 Levenshtein 距离(编辑距离)
9
48
  */
@@ -46,7 +85,7 @@ function formatIdleTime(ms) {
46
85
  return '刚刚';
47
86
  }
48
87
  // 支持的命令列表
49
- const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork'];
88
+ const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del'];
50
89
  // 命令别名映射
51
90
  const aliases = {
52
91
  '/p': '/project',
@@ -56,7 +95,7 @@ const aliases = {
56
95
  // 命令快速路径前缀(不进入消息队列的命令)
57
96
  // 注意:/clear, /compact, /safe 故意不在此列表中,它们需要进入队列触发中断机制
58
97
  // /stop 是快速命令:直接调用 agentRunner.interrupt(),不走队列(否则队列自动中断后 /stop 检测不到活跃任务)
59
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/p ', '/s ', '/name '];
98
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/del', '/p ', '/s ', '/name '];
60
99
  export class CommandHandler {
61
100
  sessionManager;
62
101
  agentRunner;
@@ -89,6 +128,11 @@ export class CommandHandler {
89
128
  return session.id;
90
129
  return `${channel}-${channelId}`;
91
130
  }
131
+ /** 从 session 提取话题回复选项 */
132
+ getThreadSendOpts(session) {
133
+ const rootId = session.metadata?.feishu?.rootId;
134
+ return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
135
+ }
92
136
  /** 获取活跃会话,无会话时返回统一错误提示 */
93
137
  async ensureSession(channel, channelId, threadId) {
94
138
  if (threadId) {
@@ -136,14 +180,14 @@ export class CommandHandler {
136
180
  const { isOwner: checkOwner } = await import('../config.js');
137
181
  // 话题内禁用部分命令
138
182
  if (threadId) {
139
- const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork'];
183
+ const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del'];
140
184
  const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
141
185
  if (isBlocked)
142
186
  return '⚠️ 话题中不支持此命令';
143
187
  }
144
188
  const isAdmin = !userId || checkOwner(this.config, channel, userId);
145
189
  if (normalizedContent.startsWith('/')) {
146
- const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/s '];
190
+ const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
147
191
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
148
192
  if (!isUserCommand && !isAdmin) {
149
193
  return '❌ 无权限:此命令仅限管理员使用';
@@ -178,6 +222,7 @@ export class CommandHandler {
178
222
  /slist - 列出当前项目的所有会话
179
223
  /s, /session <名称> - 切换到指定会话
180
224
  /name, /rename <新名称> - 重命名当前会话
225
+ /del <名称> - 删除指定会话(仅解绑,不删除文件)
181
226
  /status - 显示会话状态
182
227
 
183
228
  ❓ 帮助:
@@ -195,6 +240,7 @@ export class CommandHandler {
195
240
  /slist - 列出当前项目的所有会话
196
241
  /s, /session <名称> - 切换到指定会话
197
242
  /name, /rename <新名称> - 重命名当前会话
243
+ /del <名称> - 删除指定会话(仅解绑,不删除文件)
198
244
  /fork [名称] - 分支当前会话(从当前对话点创建分支)
199
245
  /status - 显示会话状态
200
246
  /clear - 清空当前会话的对话历史
@@ -207,31 +253,94 @@ export class CommandHandler {
207
253
  /safe - 进入安全模式
208
254
 
209
255
  🤖 模型管理:
210
- /model [model-id] - 查看或切换模型
256
+ /model [model] [effort] - 查看或切换模型/推理强度
211
257
 
212
258
  ❓ 帮助:
213
259
  /help - 显示此帮助信息`;
214
260
  }
215
- // /model 命令:查看或切换模型
261
+ // /model 命令:查看或切换模型/推理强度
216
262
  if (normalizedContent.startsWith('/model')) {
217
263
  const args = normalizedContent.slice(6).trim();
218
264
  if (!args) {
219
265
  const currentModel = this.agentRunner.getModel();
266
+ const currentEffort = this.agentRunner.getEffort() || 'auto';
267
+ const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : `${currentEffort} ${effortBar(currentEffort)}`;
220
268
  const modelList = availableModels.map(m => `- ${m}`).join('\n');
221
- return `当前模型: ${currentModel}\n\n可用模型:\n${modelList}\n\n用法: /model <model-id>`;
269
+ return `当前模型: ${currentModel}\n推理强度: ${effortDisplay}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')} / auto\n\n用法:\n /model <model> 切换模型\n /model <model> <effort> 切换模型+推理强度\n /model <effort> 仅切换推理强度\n /model auto 恢复SDK默认`;
270
+ }
271
+ const parts = args.split(/\s+/);
272
+ let newModel;
273
+ let newEffort;
274
+ if (parts.length === 1) {
275
+ const arg = parts[0];
276
+ if (arg === 'auto') {
277
+ // 清除 effort,恢复 SDK 默认
278
+ const result = await this.ensureSession(channel, channelId, threadId);
279
+ if ('error' in result)
280
+ return result.error;
281
+ const { session } = result;
282
+ const writeResult = writeUserSettings({ effortLevel: null });
283
+ if (!writeResult.success) {
284
+ return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
285
+ }
286
+ this.agentRunner.setEffort(undefined);
287
+ return '✓ 推理强度已恢复为 auto (SDK默认)';
288
+ }
289
+ // 单参数:模型 或 effort
290
+ if (availableEfforts.includes(arg)) {
291
+ newEffort = arg;
292
+ }
293
+ else if (availableModels.includes(arg)) {
294
+ newModel = arg;
295
+ }
296
+ else {
297
+ const modelList = availableModels.map(m => `- ${m}`).join('\n');
298
+ return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}\n\n推理强度: ${availableEfforts.join(' / ')}`;
299
+ }
222
300
  }
223
- if (!availableModels.includes(args)) {
224
- const modelList = availableModels.map(m => `- ${m}`).join('\n');
225
- return `❌ 无效的模型ID: ${args}\n\n可用模型:\n${modelList}`;
301
+ else {
302
+ // 双参数:model effort
303
+ const [modelArg, effortArg] = parts;
304
+ if (!availableModels.includes(modelArg)) {
305
+ return `❌ 无效的模型ID: ${modelArg}`;
306
+ }
307
+ if (!availableEfforts.includes(effortArg)) {
308
+ return `❌ 无效的推理强度: ${effortArg}\n可选: ${availableEfforts.join(' / ')}`;
309
+ }
310
+ newModel = modelArg;
311
+ newEffort = effortArg;
226
312
  }
227
313
  if (!this.config.agents)
228
314
  this.config.agents = {};
229
315
  if (!this.config.agents.anthropic)
230
316
  this.config.agents.anthropic = {};
231
- this.config.agents.anthropic.model = args;
232
- saveConfig(this.config);
233
- this.agentRunner.setModel(args);
234
- return `✓ 已切换到模型: ${args}`;
317
+ // 获取当前会话的项目路径
318
+ const result = await this.ensureSession(channel, channelId, threadId);
319
+ if ('error' in result)
320
+ return result.error;
321
+ const { session } = result;
322
+ const changes = [];
323
+ const updates = {};
324
+ if (newModel) {
325
+ updates.model = newModel;
326
+ this.agentRunner.setModel(newModel);
327
+ changes.push(`模型: ${newModel}`);
328
+ }
329
+ if (newEffort) {
330
+ const modelAfterSwitch = newModel ?? this.agentRunner.getModel();
331
+ if (newEffort === 'max' && !modelAfterSwitch.includes('opus')) {
332
+ return '⚠️ max 推理强度仅 Opus 模型支持(opus / claude-opus-4-6)';
333
+ }
334
+ updates.effortLevel = newEffort;
335
+ this.agentRunner.setEffort(newEffort);
336
+ changes.push(`推理强度: ${newEffort} ${effortBar(newEffort)}`);
337
+ }
338
+ // 写入用户级 ~/.claude/settings.json(与 Claude CLI 行为一致)
339
+ const writeResult = writeUserSettings(updates);
340
+ if (!writeResult.success) {
341
+ return `⚠️ 写入用户配置失败: ${writeResult.error}\n已更新运行时配置,但未持久化到 ~/.claude/settings.json`;
342
+ }
343
+ return `✓ 已切换\n ${changes.join('\n ')}`;
235
344
  }
236
345
  // /stop 命令:中断当前任务
237
346
  if (normalizedContent === '/stop') {
@@ -287,7 +396,7 @@ export class CommandHandler {
287
396
  ? session.projectPath
288
397
  : path.resolve(process.cwd(), session.projectPath);
289
398
  if (sendMessage) {
290
- await sendMessage(channelId, '⏳ 正在压缩会话上下文...');
399
+ await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getThreadSendOpts(session));
291
400
  }
292
401
  const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, projectPath);
293
402
  if (compacted) {
@@ -324,15 +433,7 @@ export class CommandHandler {
324
433
  const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey);
325
434
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
326
435
  const isThread = !!session.threadId;
327
- let activeStatus = isThread ? '话题' : (session.isActive ? '✓ 活跃' : '休眠');
328
- if ((isThread || session.isActive) && isCurrentlyProcessing) {
329
- if (queueLength > 0) {
330
- activeStatus += ` [处理中,队列${queueLength}条]`;
331
- }
332
- else {
333
- activeStatus += ' [处理中]';
334
- }
335
- }
436
+ const sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
336
437
  const projectName = this.getProjectName(session.projectPath);
337
438
  const health = await this.sessionManager.getHealthStatus(session.id);
338
439
  const timeSinceSuccess = Date.now() - health.lastSuccessTime;
@@ -351,10 +452,10 @@ export class CommandHandler {
351
452
  }
352
453
  const lines = [];
353
454
  if (isAdmin) {
354
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `活跃状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `异常计数: ${health.consecutiveErrors}`, `安全模式: ${health.safeMode ? '是 ⚠️' : '否 ✓'}`, `最后成功: ${timeStr}`, `Claude会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
455
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `异常计数: ${health.consecutiveErrors}`, `安全模式: ${health.safeMode ? '是 ⚠️' : '否 ✓'}`, `最后成功: ${timeStr}`, `Claude会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
355
456
  }
356
457
  else {
357
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `会话: ${session.name || '(未命名)'}`, `状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
458
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `会话: ${session.name || '(未命名)'}`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
358
459
  }
359
460
  if (health.safeMode) {
360
461
  lines.push('');
@@ -647,13 +748,16 @@ export class CommandHandler {
647
748
  const isProcessing = this.messageQueue.isProcessing(sessionKey);
648
749
  if (currentProjectSessions.length > 0) {
649
750
  lines.push('【EvolClaw 会话】');
650
- for (const s of currentProjectSessions) {
751
+ for (let i = 0; i < currentProjectSessions.length; i++) {
752
+ const s = currentProjectSessions[i];
651
753
  const prefix = s.isActive ? ' ✓' : ' ';
754
+ const num = `${i + 1}.`;
755
+ const threadTag = s.threadId ? '[话题] ' : '';
652
756
  const name = s.name || '(未命名)';
653
757
  const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
654
758
  const idleTime = formatIdleTime(Date.now() - s.updatedAt);
655
759
  if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId)) {
656
- lines.push(`${prefix} ❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
760
+ lines.push(`${prefix} ${num} ${threadTag}❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
657
761
  }
658
762
  else {
659
763
  let status = '[空闲]';
@@ -663,7 +767,7 @@ export class CommandHandler {
663
767
  else if (s.isActive) {
664
768
  status = '[活跃]';
665
769
  }
666
- lines.push(`${prefix} ${name} ${uuid} - ${idleTime} ${status}`);
770
+ lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
667
771
  }
668
772
  }
669
773
  lines.push('');
@@ -679,20 +783,32 @@ export class CommandHandler {
679
783
  }
680
784
  lines.push('');
681
785
  }
682
- lines.push('使用 /s <name或8位uuid> 切换会话');
786
+ lines.push('使用 /s <序号、name或8位uuid> 切换会话');
683
787
  return lines.join('\n');
684
788
  }
685
789
  // /session 或 /s 命令:切换会话
686
790
  if (normalizedContent.startsWith('/session ')) {
687
791
  const sessionName = normalizedContent.slice(9).trim();
688
792
  if (!sessionName)
689
- return '用法: /s <会话名称或前8位UUID>';
793
+ return '用法: /s <序号、会话名称或前8位UUID>';
690
794
  const sessionKey = `${channel}-${channelId}`;
691
795
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
692
796
  if (queueLength > 0) {
693
797
  return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
694
798
  }
695
799
  let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
800
+ // 序号切换:纯数字时按当前项目会话列表序号匹配
801
+ if (!targetSession && /^\d+$/.test(sessionName) && session) {
802
+ const idx = parseInt(sessionName, 10);
803
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
804
+ const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
805
+ if (idx >= 1 && idx <= projectSessions.length) {
806
+ targetSession = projectSessions[idx - 1];
807
+ }
808
+ else {
809
+ return `❌ 序号超出范围 (1-${projectSessions.length})\n使用 /slist 查看可用会话`;
810
+ }
811
+ }
696
812
  if (!targetSession && sessionName.length === 8) {
697
813
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
698
814
  }
@@ -729,11 +845,16 @@ export class CommandHandler {
729
845
  if (targetSession.id === session.id) {
730
846
  return `当前已在会话: ${targetSession.name || sessionName}`;
731
847
  }
848
+ // 阻止从主会话切换到话题会话
849
+ if (!session.threadId && targetSession.threadId) {
850
+ return `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用`;
851
+ }
732
852
  const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
733
853
  if (!switched) {
734
854
  return `❌ 切换会话失败`;
735
855
  }
736
- return `✓ 已切换到会话: ${targetSession.name || sessionName}\n 将继续之前的对话历史${lastInputLine}`;
856
+ const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
857
+ return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
737
858
  }
738
859
  // /rename 或 /name 命令:重命名当前会话
739
860
  if (normalizedContent.startsWith('/rename ')) {
@@ -767,6 +888,48 @@ export class CommandHandler {
767
888
  }
768
889
  return `✓ 已将当前会话重命名为: ${newName}`;
769
890
  }
891
+ // /del 命令:删除指定会话(仅解绑,不删除文件)
892
+ if (normalizedContent.startsWith('/del ')) {
893
+ const sessionName = normalizedContent.slice(5).trim();
894
+ if (!sessionName)
895
+ return '用法: /del <序号、会话名称或前8位UUID>';
896
+ if (!session) {
897
+ return `❌ 当前没有活跃会话`;
898
+ }
899
+ // 群聊权限检查:只有管理员可以删除
900
+ const isGroup = await this.isGroupChat(channel, channelId);
901
+ if (isGroup && !isAdmin) {
902
+ return `❌ 无权限:群聊中仅管理员可删除会话`;
903
+ }
904
+ let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
905
+ // 序号删除
906
+ if (!targetSession && /^\d+$/.test(sessionName)) {
907
+ const idx = parseInt(sessionName, 10);
908
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
909
+ const projectSessions = allSessions.filter(s => s.projectPath === session.projectPath);
910
+ if (idx >= 1 && idx <= projectSessions.length) {
911
+ targetSession = projectSessions[idx - 1];
912
+ }
913
+ else {
914
+ return `❌ 序号超出范围 (1-${projectSessions.length})\n使用 /slist 查看可用会话`;
915
+ }
916
+ }
917
+ if (!targetSession && sessionName.length === 8) {
918
+ targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
919
+ }
920
+ if (!targetSession) {
921
+ return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
922
+ }
923
+ if (targetSession.id === session.id) {
924
+ return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
925
+ }
926
+ const success = await this.sessionManager.unbindSession(targetSession.id);
927
+ if (!success) {
928
+ return `❌ 删除失败`;
929
+ }
930
+ await this.agentRunner.closeSession(targetSession.id);
931
+ return `✓ 已删除会话: ${targetSession.name || sessionName}\n会话文件已保留,可通过 CLI 访问`;
932
+ }
770
933
  // /fork 命令:分支当前会话
771
934
  if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
772
935
  const forkName = normalizedContent.slice(5).trim() || undefined;