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.
- package/README.md +9 -4
- package/data/evolclaw.sample.json +3 -2
- package/dist/channels/feishu.js +39 -15
- package/dist/cli.js +33 -15
- package/dist/config.js +4 -1
- package/dist/core/agent-runner.js +56 -21
- package/dist/core/command-handler.js +271 -66
- package/dist/core/message-processor.js +117 -59
- package/dist/core/session-manager.js +156 -130
- package/dist/index.js +18 -14
- package/dist/index.js.bak +340 -0
- package/dist/utils/session-file-health.js +4 -3
- package/dist/utils/stream-flusher.js +42 -32
- package/package.json +1 -1
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
import { renameSession as sdkRenameSession, forkSession as sdkForkSession, listSessions as sdkListSessions } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import {
|
|
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
|
-
// 注意:/
|
|
58
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
376
|
+
const cleared = await this.agentRunner.clearSession(session.agentSessionId, projectPath);
|
|
240
377
|
if (cleared) {
|
|
241
|
-
await this.sessionManager.
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
293
|
-
|
|
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.
|
|
310
|
-
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.
|
|
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('
|
|
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('
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (
|
|
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.
|
|
757
|
+
const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
|
|
612
758
|
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
613
|
-
if (s.
|
|
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
|
|
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
|
|
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.
|
|
677
|
-
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.
|
|
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
|
-
|
|
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.
|
|
877
|
+
if (session.agentSessionId) {
|
|
715
878
|
try {
|
|
716
|
-
await sdkRenameSession(session.
|
|
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.
|
|
939
|
+
if (!session.agentSessionId) {
|
|
735
940
|
return `❌ 当前会话尚未初始化 Claude 对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
|
|
736
941
|
}
|
|
737
942
|
try {
|
|
738
|
-
const forkResult = await sdkForkSession(session.
|
|
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.
|
|
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.
|
|
976
|
+
const healthCheck = await checkSessionFileHealth(session.projectPath, session.agentSessionId);
|
|
772
977
|
if (healthCheck.corrupt) {
|
|
773
|
-
const sessionFile = path.join(session.projectPath, '.claude', `${session.
|
|
978
|
+
const sessionFile = path.join(session.projectPath, '.claude', `${session.agentSessionId}.jsonl`);
|
|
774
979
|
await fsPromises.unlink(sessionFile);
|
|
775
|
-
await this.sessionManager.
|
|
980
|
+
await this.sessionManager.updateAgentSessionId(session.channel, session.channelId, '');
|
|
776
981
|
await this.sessionManager.resetHealthStatus(session.id);
|
|
777
982
|
return `✓ 修复完成,已退出安全模式
|
|
778
983
|
|