evolclaw 2.0.7 → 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.
@@ -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',
@@ -54,8 +93,9 @@ const aliases = {
54
93
  '/name': '/rename'
55
94
  };
56
95
  // 命令快速路径前缀(不进入消息队列的命令)
57
- // 注意:/stop, /clear, /compact, /safe 故意不在此列表中,它们需要进入队列触发中断机制
58
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/p ', '/s ', '/name '];
96
+ // 注意:/clear, /compact, /safe 故意不在此列表中,它们需要进入队列触发中断机制
97
+ // /stop 是快速命令:直接调用 agentRunner.interrupt(),不走队列(否则队列自动中断后 /stop 检测不到活跃任务)
98
+ const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/del', '/p ', '/s ', '/name '];
59
99
  export class CommandHandler {
60
100
  sessionManager;
61
101
  agentRunner;
@@ -82,8 +122,24 @@ export class CommandHandler {
82
122
  getProjectName(projectPath) {
83
123
  return this.getConfiguredProjectName(projectPath) || path.basename(projectPath);
84
124
  }
125
+ /** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
126
+ getQueueKey(session, channel, channelId) {
127
+ if (session?.threadId)
128
+ return session.id;
129
+ return `${channel}-${channelId}`;
130
+ }
131
+ /** 从 session 提取话题回复选项 */
132
+ getThreadSendOpts(session) {
133
+ const rootId = session.metadata?.feishu?.rootId;
134
+ return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
135
+ }
85
136
  /** 获取活跃会话,无会话时返回统一错误提示 */
86
- async ensureSession(channel, channelId) {
137
+ async ensureSession(channel, channelId, threadId) {
138
+ if (threadId) {
139
+ // 话题会话:按 thread_id 查找
140
+ const session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
141
+ return { session };
142
+ }
87
143
  const session = await this.sessionManager.getActiveSession(channel, channelId);
88
144
  if (!session) {
89
145
  return { error: '❌ 当前没有活跃会话\n使用 /new 创建新会话' };
@@ -111,7 +167,7 @@ export class CommandHandler {
111
167
  /**
112
168
  * 主命令处理入口
113
169
  */
114
- async handle(content, channel, channelId, sendMessage, userId) {
170
+ async handle(content, channel, channelId, sendMessage, userId, threadId) {
115
171
  // 规范化命令(将别名转换为完整命令)
116
172
  let normalizedContent = content;
117
173
  for (const [alias, full] of Object.entries(aliases)) {
@@ -122,9 +178,16 @@ export class CommandHandler {
122
178
  }
123
179
  // 权限检查:区分用户级命令和管理级命令
124
180
  const { isOwner: checkOwner } = await import('../config.js');
181
+ // 话题内禁用部分命令
182
+ if (threadId) {
183
+ const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork', '/del'];
184
+ const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
185
+ if (isBlocked)
186
+ return '⚠️ 话题中不支持此命令';
187
+ }
125
188
  const isAdmin = !userId || checkOwner(this.config, channel, userId);
126
189
  if (normalizedContent.startsWith('/')) {
127
- const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/s '];
190
+ const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
128
191
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
129
192
  if (!isUserCommand && !isAdmin) {
130
193
  return '❌ 无权限:此命令仅限管理员使用';
@@ -159,6 +222,7 @@ export class CommandHandler {
159
222
  /slist - 列出当前项目的所有会话
160
223
  /s, /session <名称> - 切换到指定会话
161
224
  /name, /rename <新名称> - 重命名当前会话
225
+ /del <名称> - 删除指定会话(仅解绑,不删除文件)
162
226
  /status - 显示会话状态
163
227
 
164
228
  ❓ 帮助:
@@ -176,6 +240,7 @@ export class CommandHandler {
176
240
  /slist - 列出当前项目的所有会话
177
241
  /s, /session <名称> - 切换到指定会话
178
242
  /name, /rename <新名称> - 重命名当前会话
243
+ /del <名称> - 删除指定会话(仅解绑,不删除文件)
179
244
  /fork [名称] - 分支当前会话(从当前对话点创建分支)
180
245
  /status - 显示会话状态
181
246
  /clear - 清空当前会话的对话历史
@@ -188,37 +253,109 @@ export class CommandHandler {
188
253
  /safe - 进入安全模式
189
254
 
190
255
  🤖 模型管理:
191
- /model [model-id] - 查看或切换模型
256
+ /model [model] [effort] - 查看或切换模型/推理强度
192
257
 
193
258
  ❓ 帮助:
194
259
  /help - 显示此帮助信息`;
195
260
  }
196
- // /model 命令:查看或切换模型
261
+ // /model 命令:查看或切换模型/推理强度
197
262
  if (normalizedContent.startsWith('/model')) {
198
263
  const args = normalizedContent.slice(6).trim();
199
264
  if (!args) {
200
265
  const currentModel = this.agentRunner.getModel();
266
+ const currentEffort = this.agentRunner.getEffort() || 'auto';
267
+ const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : `${currentEffort} ${effortBar(currentEffort)}`;
201
268
  const modelList = availableModels.map(m => `- ${m}`).join('\n');
202
- 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默认`;
203
270
  }
204
- if (!availableModels.includes(args)) {
205
- const modelList = availableModels.map(m => `- ${m}`).join('\n');
206
- return `❌ 无效的模型ID: ${args}\n\n可用模型:\n${modelList}`;
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
+ }
300
+ }
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;
207
312
  }
208
313
  if (!this.config.agents)
209
314
  this.config.agents = {};
210
315
  if (!this.config.agents.anthropic)
211
316
  this.config.agents.anthropic = {};
212
- this.config.agents.anthropic.model = args;
213
- saveConfig(this.config);
214
- this.agentRunner.setModel(args);
215
- 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 ')}`;
216
344
  }
217
345
  // /stop 命令:中断当前任务
218
346
  if (normalizedContent === '/stop') {
219
- const sessionKey = `${channel}-${channelId}`;
347
+ // 话题使用 session.id 作为队列 key,主会话使用 channel-channelId
348
+ let sessionKey;
349
+ if (threadId) {
350
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
351
+ sessionKey = threadSession.id;
352
+ }
353
+ else {
354
+ sessionKey = `${channel}-${channelId}`;
355
+ }
220
356
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
221
- if (queueLength === 0) {
357
+ const hasActive = this.agentRunner.hasActiveStream(sessionKey);
358
+ if (queueLength === 0 && !hasActive) {
222
359
  return '当前没有正在处理的任务';
223
360
  }
224
361
  await this.agentRunner.interrupt(sessionKey);
@@ -226,19 +363,19 @@ export class CommandHandler {
226
363
  }
227
364
  // /clear 命令:通过 SDK /clear 清空会话历史
228
365
  if (normalizedContent === '/clear') {
229
- const result = await this.ensureSession(channel, channelId);
366
+ const result = await this.ensureSession(channel, channelId, threadId);
230
367
  if ('error' in result)
231
368
  return result.error;
232
369
  const { session } = result;
233
- if (!session.claudeSessionId) {
370
+ if (!session.agentSessionId) {
234
371
  return '❌ 当前会话没有历史记录,无需清空';
235
372
  }
236
373
  const projectPath = path.isAbsolute(session.projectPath)
237
374
  ? session.projectPath
238
375
  : path.resolve(process.cwd(), session.projectPath);
239
- const cleared = await this.agentRunner.clearSession(session.claudeSessionId, projectPath);
376
+ const cleared = await this.agentRunner.clearSession(session.agentSessionId, projectPath);
240
377
  if (cleared) {
241
- await this.sessionManager.updateClaudeSessionIdBySessionId(session.id, '');
378
+ await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
242
379
  this.agentRunner.updateSessionId(session.id, '');
243
380
  return '✅ 已清空当前会话的对话历史';
244
381
  }
@@ -248,20 +385,20 @@ export class CommandHandler {
248
385
  }
249
386
  // /compact 命令:手动压缩会话上下文
250
387
  if (normalizedContent === '/compact') {
251
- const result = await this.ensureSession(channel, channelId);
388
+ const result = await this.ensureSession(channel, channelId, threadId);
252
389
  if ('error' in result)
253
390
  return result.error;
254
391
  const { session } = result;
255
- if (!session.claudeSessionId) {
392
+ if (!session.agentSessionId) {
256
393
  return '❌ 当前会话没有历史记录,无需压缩';
257
394
  }
258
395
  const projectPath = path.isAbsolute(session.projectPath)
259
396
  ? session.projectPath
260
397
  : path.resolve(process.cwd(), session.projectPath);
261
398
  if (sendMessage) {
262
- await sendMessage(channelId, '⏳ 正在压缩会话上下文...');
399
+ await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getThreadSendOpts(session));
263
400
  }
264
- const compacted = await this.agentRunner.compactSession(session.id, session.claudeSessionId, projectPath);
401
+ const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, projectPath);
265
402
  if (compacted) {
266
403
  return '✅ 会话上下文已压缩';
267
404
  }
@@ -269,8 +406,14 @@ export class CommandHandler {
269
406
  return '❌ 会话压缩失败,请稍后重试';
270
407
  }
271
408
  }
272
- // 尝试获取活跃会话(所有命令都尝试获取,但不强制)
273
- let session = await this.sessionManager.getActiveSession(channel, channelId);
409
+ // 尝试获取活跃会话(话题时直接查找话题 session)
410
+ let session;
411
+ if (threadId) {
412
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
413
+ }
414
+ else {
415
+ session = await this.sessionManager.getActiveSession(channel, channelId);
416
+ }
274
417
  // 对于需要创建会话的命令,如果没有会话则创建
275
418
  if (!session && (normalizedContent.startsWith('/new') ||
276
419
  normalizedContent.startsWith('/bind') ||
@@ -286,18 +429,11 @@ export class CommandHandler {
286
429
 
287
430
  提示:发送任意消息或使用 /new 命令创建会话`;
288
431
  }
289
- const sessionKey = `${channel}-${channelId}`;
432
+ const sessionKey = this.getQueueKey(session, channel, channelId);
290
433
  const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey);
291
434
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
292
- let activeStatus = session.isActive ? '✓ 活跃' : '休眠';
293
- if (session.isActive && isCurrentlyProcessing) {
294
- if (queueLength > 0) {
295
- activeStatus += ` [处理中,队列${queueLength}条]`;
296
- }
297
- else {
298
- activeStatus += ' [处理中]';
299
- }
300
- }
435
+ const isThread = !!session.threadId;
436
+ const sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
301
437
  const projectName = this.getProjectName(session.projectPath);
302
438
  const health = await this.sessionManager.getHealthStatus(session.id);
303
439
  const timeSinceSuccess = Date.now() - health.lastSuccessTime;
@@ -306,8 +442,8 @@ export class CommandHandler {
306
442
  `${Math.floor(timeSinceSuccess / 3600000)}小时前`;
307
443
  // 获取会话文件信息并同步 name
308
444
  let sessionTurns = 0;
309
- if (session.claudeSessionId) {
310
- const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.claudeSessionId);
445
+ if (session.agentSessionId) {
446
+ const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId);
311
447
  sessionTurns = fileInfo.turns;
312
448
  if (fileInfo.title && fileInfo.title !== session.name) {
313
449
  await this.sessionManager.renameSession(session.id, fileInfo.title);
@@ -316,10 +452,10 @@ export class CommandHandler {
316
452
  }
317
453
  const lines = [];
318
454
  if (isAdmin) {
319
- lines.push('📊 会话状态:', `渠道: ${channel} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `活跃状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `异常计数: ${health.consecutiveErrors}`, `安全模式: ${health.safeMode ? '是 ⚠️' : '否 ✓'}`, `最后成功: ${timeStr}`, `Claude会话: ${session.claudeSessionId || '(未初始化)'}`, `创建时间: ${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')}`);
320
456
  }
321
457
  else {
322
- lines.push('📊 会话状态:', `会话: ${session.name || '(未命名)'}`, `状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
458
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `会话: ${session.name || '(未命名)'}`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
323
459
  }
324
460
  if (health.safeMode) {
325
461
  lines.push('');
@@ -380,10 +516,17 @@ export class CommandHandler {
380
516
  return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
381
517
  }
382
518
  }
519
+ // 话题中 restart 时保存 rootId 用于重启后回复到话题
520
+ let rootId;
521
+ if (threadId) {
522
+ const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
523
+ rootId = threadSession.metadata?.feishu?.rootId;
524
+ }
383
525
  const restartInfo = {
384
526
  channel,
385
527
  channelId,
386
- timestamp: Date.now()
528
+ timestamp: Date.now(),
529
+ ...(rootId ? { rootId } : {})
387
530
  };
388
531
  fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
389
532
  const { spawn } = await import('child_process');
@@ -508,7 +651,7 @@ export class CommandHandler {
508
651
  }
509
652
  const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath);
510
653
  const cachedEvents = this.messageCache.getEvents(newSession.id);
511
- const hasExistingSession = newSession.claudeSessionId ? '(恢复已有会话)' : '(新建会话)';
654
+ const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
512
655
  let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n ${hasExistingSession}`;
513
656
  if (cachedEvents.length > 0 && sendMessage) {
514
657
  for (const event of cachedEvents) {
@@ -544,7 +687,7 @@ export class CommandHandler {
544
687
  }
545
688
  const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath);
546
689
  const cachedEvents = this.messageCache.getEvents(newSession.id);
547
- const hasExistingSession = newSession.claudeSessionId ? '(恢复已有会话)' : '(新建会话)';
690
+ const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
548
691
  let response = `✓ 已绑定项目目录: ${projectPath}\n ${hasExistingSession}`;
549
692
  if (cachedEvents.length > 0) {
550
693
  response += `\n\n后台任务结果:`;
@@ -585,7 +728,7 @@ export class CommandHandler {
585
728
  const sdkName = sdkSession.customTitle || undefined;
586
729
  if (!sdkName)
587
730
  continue;
588
- const dbSession = currentProjectSessions.find(s => s.claudeSessionId === sdkSession.sessionId);
731
+ const dbSession = currentProjectSessions.find(s => s.agentSessionId === sdkSession.sessionId);
589
732
  if (dbSession && sdkName !== dbSession.name) {
590
733
  await this.sessionManager.renameSession(dbSession.id, sdkName);
591
734
  dbSession.name = sdkName;
@@ -599,19 +742,22 @@ export class CommandHandler {
599
742
  const cliSessions = (isGroup || !isAdmin)
600
743
  ? []
601
744
  : await this.sessionManager.scanCliSessions(session.projectPath);
602
- const dbSessionIds = new Set(currentProjectSessions.map(s => s.claudeSessionId).filter(Boolean));
745
+ const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
603
746
  const lines = [`当前项目 ${path.basename(session.projectPath)} 的会话列表:\n`];
604
747
  const sessionKey = `${channel}-${channelId}`;
605
748
  const isProcessing = this.messageQueue.isProcessing(sessionKey);
606
749
  if (currentProjectSessions.length > 0) {
607
750
  lines.push('【EvolClaw 会话】');
608
- for (const s of currentProjectSessions) {
751
+ for (let i = 0; i < currentProjectSessions.length; i++) {
752
+ const s = currentProjectSessions[i];
609
753
  const prefix = s.isActive ? ' ✓' : ' ';
754
+ const num = `${i + 1}.`;
755
+ const threadTag = s.threadId ? '[话题] ' : '';
610
756
  const name = s.name || '(未命名)';
611
- const uuid = s.claudeSessionId ? `(${s.claudeSessionId.substring(0, 8)})` : '';
757
+ const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
612
758
  const idleTime = formatIdleTime(Date.now() - s.updatedAt);
613
- if (s.claudeSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.claudeSessionId)) {
614
- lines.push(`${prefix} ❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
759
+ if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId)) {
760
+ lines.push(`${prefix} ${num} ${threadTag}❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
615
761
  }
616
762
  else {
617
763
  let status = '[空闲]';
@@ -621,7 +767,7 @@ export class CommandHandler {
621
767
  else if (s.isActive) {
622
768
  status = '[活跃]';
623
769
  }
624
- lines.push(`${prefix} ${name} ${uuid} - ${idleTime} ${status}`);
770
+ lines.push(`${prefix} ${num} ${threadTag}${name} ${uuid} - ${idleTime} ${status}`);
625
771
  }
626
772
  }
627
773
  lines.push('');
@@ -637,20 +783,32 @@ export class CommandHandler {
637
783
  }
638
784
  lines.push('');
639
785
  }
640
- lines.push('使用 /s <name或8位uuid> 切换会话');
786
+ lines.push('使用 /s <序号、name或8位uuid> 切换会话');
641
787
  return lines.join('\n');
642
788
  }
643
789
  // /session 或 /s 命令:切换会话
644
790
  if (normalizedContent.startsWith('/session ')) {
645
791
  const sessionName = normalizedContent.slice(9).trim();
646
792
  if (!sessionName)
647
- return '用法: /s <会话名称或前8位UUID>';
793
+ return '用法: /s <序号、会话名称或前8位UUID>';
648
794
  const sessionKey = `${channel}-${channelId}`;
649
795
  const queueLength = this.messageQueue.getQueueLength(sessionKey);
650
796
  if (queueLength > 0) {
651
797
  return `⚠️ 当前正在处理消息,无法切换会话\n请等待当前任务完成后再试`;
652
798
  }
653
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
+ }
654
812
  if (!targetSession && sessionName.length === 8) {
655
813
  targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
656
814
  }
@@ -673,8 +831,8 @@ export class CommandHandler {
673
831
  if (!targetSession) {
674
832
  return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
675
833
  }
676
- const lastInput = targetSession.claudeSessionId
677
- ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.claudeSessionId)
834
+ const lastInput = targetSession.agentSessionId
835
+ ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId)
678
836
  : null;
679
837
  const lastInputLine = lastInput ? `\n 最后输入: "${lastInput}"` : '';
680
838
  if (!session) {
@@ -687,11 +845,16 @@ export class CommandHandler {
687
845
  if (targetSession.id === session.id) {
688
846
  return `当前已在会话: ${targetSession.name || sessionName}`;
689
847
  }
848
+ // 阻止从主会话切换到话题会话
849
+ if (!session.threadId && targetSession.threadId) {
850
+ return `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用`;
851
+ }
690
852
  const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
691
853
  if (!switched) {
692
854
  return `❌ 切换会话失败`;
693
855
  }
694
- return `✓ 已切换到会话: ${targetSession.name || sessionName}\n 将继续之前的对话历史${lastInputLine}`;
856
+ const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
857
+ return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
695
858
  }
696
859
  // /rename 或 /name 命令:重命名当前会话
697
860
  if (normalizedContent.startsWith('/rename ')) {
@@ -711,9 +874,9 @@ export class CommandHandler {
711
874
  return `❌ 会话名称 "${newName}" 已存在,请使用其他名称`;
712
875
  }
713
876
  // 双写:SDK + 数据库
714
- if (session.claudeSessionId) {
877
+ if (session.agentSessionId) {
715
878
  try {
716
- await sdkRenameSession(session.claudeSessionId, newName, { dir: session.projectPath });
879
+ await sdkRenameSession(session.agentSessionId, newName, { dir: session.projectPath });
717
880
  }
718
881
  catch (error) {
719
882
  logger.warn(`[CommandHandler] SDK renameSession failed (continuing with db update):`, error);
@@ -725,17 +888,59 @@ export class CommandHandler {
725
888
  }
726
889
  return `✓ 已将当前会话重命名为: ${newName}`;
727
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
+ }
728
933
  // /fork 命令:分支当前会话
729
934
  if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
730
935
  const forkName = normalizedContent.slice(5).trim() || undefined;
731
936
  if (!session) {
732
937
  return `❌ 当前没有活跃会话,无法分支`;
733
938
  }
734
- if (!session.claudeSessionId) {
939
+ if (!session.agentSessionId) {
735
940
  return `❌ 当前会话尚未初始化 Claude 对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
736
941
  }
737
942
  try {
738
- const forkResult = await sdkForkSession(session.claudeSessionId, { dir: session.projectPath, title: forkName });
943
+ const forkResult = await sdkForkSession(session.agentSessionId, { dir: session.projectPath, title: forkName });
739
944
  const newSession = await this.sessionManager.createForkedSession(session, forkResult.sessionId, forkName);
740
945
  return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /slist 查看所有会话,/s <名称> 切换回原会话`;
741
946
  }
@@ -757,7 +962,7 @@ export class CommandHandler {
757
962
  const fsPromises = await import('fs/promises');
758
963
  try {
759
964
  const backupDir = await backupClaudeDir(session.projectPath);
760
- if (!session.claudeSessionId) {
965
+ if (!session.agentSessionId) {
761
966
  await this.sessionManager.resetHealthStatus(session.id);
762
967
  return `✓ 修复完成,已退出安全模式
763
968
 
@@ -768,11 +973,11 @@ export class CommandHandler {
768
973
 
769
974
  备份位置:${backupDir}`;
770
975
  }
771
- const healthCheck = await checkSessionFileHealth(session.projectPath, session.claudeSessionId);
976
+ const healthCheck = await checkSessionFileHealth(session.projectPath, session.agentSessionId);
772
977
  if (healthCheck.corrupt) {
773
- const sessionFile = path.join(session.projectPath, '.claude', `${session.claudeSessionId}.jsonl`);
978
+ const sessionFile = path.join(session.projectPath, '.claude', `${session.agentSessionId}.jsonl`);
774
979
  await fsPromises.unlink(sessionFile);
775
- await this.sessionManager.updateClaudeSessionId(session.channel, session.channelId, '');
980
+ await this.sessionManager.updateAgentSessionId(session.channel, session.channelId, '');
776
981
  await this.sessionManager.resetHealthStatus(session.id);
777
982
  return `✓ 修复完成,已退出安全模式
778
983