evolclaw 2.0.7 → 2.1.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.
- package/data/evolclaw.sample.json +3 -2
- package/dist/channels/feishu.js +32 -14
- package/dist/cli.js +20 -8
- package/dist/core/agent-runner.js +24 -21
- package/dist/core/command-handler.js +81 -39
- package/dist/core/message-processor.js +82 -48
- package/dist/core/session-manager.js +161 -113
- package/dist/index.js +13 -12
- package/dist/utils/session-file-health.js +4 -3
- package/package.json +1 -1
package/dist/channels/feishu.js
CHANGED
|
@@ -46,10 +46,6 @@ export class FeishuChannel {
|
|
|
46
46
|
const msg = data.message;
|
|
47
47
|
logger.debug('[Feishu] Received message, message_id:', msg.message_id, 'type:', msg.message_type);
|
|
48
48
|
logger.debug('[Feishu] Full data object:', JSON.stringify(data, null, 2));
|
|
49
|
-
// 诊断:话题消息检测
|
|
50
|
-
if (msg.thread_id) {
|
|
51
|
-
logger.info('[Feishu] Thread message detected, thread_id:', msg.thread_id, 'parent_id:', msg.parent_id, 'root_id:', msg.root_id);
|
|
52
|
-
}
|
|
53
49
|
if (!msg.message_id || this.isDuplicate(msg.message_id)) {
|
|
54
50
|
logger.debug('[Feishu] Duplicate message ignored:', msg.message_id);
|
|
55
51
|
return;
|
|
@@ -58,6 +54,10 @@ export class FeishuChannel {
|
|
|
58
54
|
this.addAckReaction(msg.message_id);
|
|
59
55
|
if (!this.messageHandler)
|
|
60
56
|
return;
|
|
57
|
+
// 话题消息检测日志(去重后)
|
|
58
|
+
if (msg.thread_id) {
|
|
59
|
+
logger.info('[Feishu] Thread message, thread_id:', msg.thread_id, 'root_id:', msg.root_id);
|
|
60
|
+
}
|
|
61
61
|
// 提取 @ 提及列表(排除机器人自身)
|
|
62
62
|
const mentions = (msg.mentions || []).map((m) => ({
|
|
63
63
|
userId: m.id?.open_id || '',
|
|
@@ -74,10 +74,15 @@ export class FeishuChannel {
|
|
|
74
74
|
userName = undefined;
|
|
75
75
|
}
|
|
76
76
|
try {
|
|
77
|
-
//
|
|
77
|
+
// 提取话题信息
|
|
78
|
+
const threadId = msg.thread_id || undefined;
|
|
79
|
+
const rootId = msg.root_id || undefined;
|
|
80
|
+
// 处理引用消息(话题内消息跳过,避免每条都拼接引用前缀)
|
|
78
81
|
let quotedText = '';
|
|
79
82
|
let quotedImages = [];
|
|
80
|
-
|
|
83
|
+
// 话题创建消息检测:DB 中无对应 thread session 时为首条消息
|
|
84
|
+
const isThreadCreating = threadId && !this.hasThreadSession(threadId);
|
|
85
|
+
if (msg.parent_id && (!msg.thread_id || isThreadCreating) && this.client) {
|
|
81
86
|
try {
|
|
82
87
|
const res = await this.client.im.message.get({
|
|
83
88
|
path: { message_id: msg.parent_id }
|
|
@@ -151,7 +156,7 @@ export class FeishuChannel {
|
|
|
151
156
|
// 优先使用 text_without_at_bot(去除机器人 @),否则使用 text
|
|
152
157
|
const content = parsed.text_without_at_bot || parsed.text;
|
|
153
158
|
const finalContent = quotedText + content;
|
|
154
|
-
await this.messageHandler(msg.chat_id, finalContent, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id, mentions.length > 0 ? mentions : undefined);
|
|
159
|
+
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, userId, userName, messageId: msg.message_id, mentions: mentions.length > 0 ? mentions : undefined, threadId, rootId });
|
|
155
160
|
}
|
|
156
161
|
// 处理图片消息
|
|
157
162
|
else if (msg.message_type === 'image') {
|
|
@@ -165,11 +170,11 @@ export class FeishuChannel {
|
|
|
165
170
|
if (imageData) {
|
|
166
171
|
const allImages = [...quotedImages, imageData];
|
|
167
172
|
const prompt = quotedText + '用户发送了一张图片,请分析这张图片的内容。';
|
|
168
|
-
await this.messageHandler(msg.chat_id, prompt, allImages, userId, userName, msg.message_id);
|
|
173
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: allImages, userId, userName, messageId: msg.message_id, threadId, rootId });
|
|
169
174
|
}
|
|
170
175
|
else {
|
|
171
176
|
const prompt = quotedText + '[图片下载失败] 应用可能缺少 im:message 或 im:message:readonly 权限';
|
|
172
|
-
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
177
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, userId, userName, messageId: msg.message_id, threadId, rootId });
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
// 处理文件消息
|
|
@@ -184,11 +189,11 @@ export class FeishuChannel {
|
|
|
184
189
|
const filePath = await this.downloadFile(fileKey, fileName, msg.message_id, projectPath);
|
|
185
190
|
if (filePath) {
|
|
186
191
|
const prompt = quotedText + `用户发送了文件:${fileName}\n文件已保存到:${filePath}\n请使用 Read 工具读取并分析文件内容。`;
|
|
187
|
-
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
192
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, userId, userName, messageId: msg.message_id, threadId, rootId });
|
|
188
193
|
}
|
|
189
194
|
else {
|
|
190
195
|
const prompt = quotedText + '[文件下载失败] 应用可能缺少 im:resource 权限';
|
|
191
|
-
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
196
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, userId, userName, messageId: msg.message_id, threadId, rootId });
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
// 处理富文本消息
|
|
@@ -210,13 +215,13 @@ export class FeishuChannel {
|
|
|
210
215
|
if (title)
|
|
211
216
|
finalContent = `${title}\n${finalContent}`;
|
|
212
217
|
finalContent = quotedText + finalContent;
|
|
213
|
-
await this.messageHandler(msg.chat_id, finalContent, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
218
|
+
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, userId, userName, messageId: msg.message_id, threadId, rootId });
|
|
214
219
|
}
|
|
215
220
|
// 处理其他类型消息
|
|
216
221
|
else {
|
|
217
222
|
logger.debug('[Feishu] Unsupported message type:', msg.message_type);
|
|
218
223
|
const prompt = quotedText + `[不支持的消息类型: ${msg.message_type}]`;
|
|
219
|
-
await this.messageHandler(msg.chat_id, prompt, quotedImages.length > 0 ? quotedImages : undefined, userId, userName, msg.message_id);
|
|
224
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, userId, userName, messageId: msg.message_id, threadId, rootId });
|
|
220
225
|
}
|
|
221
226
|
}
|
|
222
227
|
catch (error) {
|
|
@@ -313,9 +318,13 @@ export class FeishuChannel {
|
|
|
313
318
|
: JSON.stringify({ text: content });
|
|
314
319
|
}
|
|
315
320
|
if (options?.replyToMessageId) {
|
|
321
|
+
const replyData = { msg_type: msgType, content: msgContent };
|
|
322
|
+
if (options.replyInThread) {
|
|
323
|
+
replyData.reply_in_thread = true;
|
|
324
|
+
}
|
|
316
325
|
await this.client.im.message.reply({
|
|
317
326
|
path: { message_id: options.replyToMessageId },
|
|
318
|
-
data:
|
|
327
|
+
data: replyData
|
|
319
328
|
});
|
|
320
329
|
}
|
|
321
330
|
else {
|
|
@@ -366,6 +375,15 @@ export class FeishuChannel {
|
|
|
366
375
|
throw error;
|
|
367
376
|
}
|
|
368
377
|
}
|
|
378
|
+
hasThreadSession(threadId) {
|
|
379
|
+
try {
|
|
380
|
+
const row = this.db.prepare('SELECT 1 FROM sessions WHERE thread_id = ? LIMIT 1').get(threadId);
|
|
381
|
+
return !!row;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
369
387
|
async disconnect() {
|
|
370
388
|
if (this.cleanupInterval) {
|
|
371
389
|
clearInterval(this.cleanupInterval);
|
package/dist/cli.js
CHANGED
|
@@ -713,14 +713,26 @@ async function notifyChannel(p, pendingInfo, message, log) {
|
|
|
713
713
|
appId: config.channels.feishu.appId,
|
|
714
714
|
appSecret: config.channels.feishu.appSecret,
|
|
715
715
|
});
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
716
|
+
if (pendingInfo.rootId) {
|
|
717
|
+
await client.im.message.reply({
|
|
718
|
+
path: { message_id: pendingInfo.rootId },
|
|
719
|
+
data: {
|
|
720
|
+
msg_type: 'text',
|
|
721
|
+
content: JSON.stringify({ text: message }),
|
|
722
|
+
reply_in_thread: true,
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
await client.im.message.create({
|
|
728
|
+
params: { receive_id_type: 'chat_id' },
|
|
729
|
+
data: {
|
|
730
|
+
receive_id: pendingInfo.channelId,
|
|
731
|
+
msg_type: 'text',
|
|
732
|
+
content: JSON.stringify({ text: message }),
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
}
|
|
724
736
|
log(`Feishu notification sent: ${message.slice(0, 50)}`);
|
|
725
737
|
}
|
|
726
738
|
catch (error) {
|
|
@@ -44,24 +44,24 @@ export class AgentRunner {
|
|
|
44
44
|
async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager) {
|
|
45
45
|
ensureDir(projectPath);
|
|
46
46
|
ensureDir(path.join(projectPath, '.claude'));
|
|
47
|
-
// 优先使用传入的
|
|
48
|
-
let
|
|
47
|
+
// 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
|
|
48
|
+
let agentSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
|
|
49
49
|
// 检查是否在安全模式
|
|
50
50
|
let skipResume = false;
|
|
51
51
|
if (sessionManager) {
|
|
52
52
|
const health = await sessionManager.getHealthStatus(sessionId);
|
|
53
53
|
if (health.safeMode) {
|
|
54
54
|
// 安全模式:不使用 resume,每次都是新对话
|
|
55
|
-
|
|
55
|
+
agentSessionId = undefined;
|
|
56
56
|
skipResume = true;
|
|
57
57
|
logger.warn(`[AgentRunner] Safe mode enabled for ${sessionId}, not resuming session`);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
// 验证会话文件是否存在且有效(仅在非安全模式且有
|
|
61
|
-
if (
|
|
60
|
+
// 验证会话文件是否存在且有效(仅在非安全模式且有 agentSessionId 时)
|
|
61
|
+
if (agentSessionId && !skipResume) {
|
|
62
62
|
const homeDir = os.homedir();
|
|
63
63
|
const encodedProjectPath = encodePath(projectPath);
|
|
64
|
-
const sessionFile = path.join(homeDir, '.claude', 'projects', encodedProjectPath, `${
|
|
64
|
+
const sessionFile = path.join(homeDir, '.claude', 'projects', encodedProjectPath, `${agentSessionId}.jsonl`);
|
|
65
65
|
let isValid = false;
|
|
66
66
|
if (fs.existsSync(sessionFile)) {
|
|
67
67
|
try {
|
|
@@ -88,7 +88,7 @@ export class AgentRunner {
|
|
|
88
88
|
}
|
|
89
89
|
if (!isValid) {
|
|
90
90
|
logger.warn(`[AgentRunner] Invalid session file, starting new session`);
|
|
91
|
-
|
|
91
|
+
agentSessionId = undefined;
|
|
92
92
|
this.activeSessions.delete(sessionId);
|
|
93
93
|
if (this.onSessionIdUpdate) {
|
|
94
94
|
this.onSessionIdUpdate(sessionId, '');
|
|
@@ -220,8 +220,8 @@ export class AgentRunner {
|
|
|
220
220
|
queryStream = createQuery(stream);
|
|
221
221
|
}
|
|
222
222
|
else {
|
|
223
|
-
logger.debug('[AgentRunner] Creating query with text only,
|
|
224
|
-
queryStream = createQuery(prompt,
|
|
223
|
+
logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
|
|
224
|
+
queryStream = createQuery(prompt, agentSessionId);
|
|
225
225
|
}
|
|
226
226
|
this.activeStreams.set(sessionId, queryStream);
|
|
227
227
|
return queryStream;
|
|
@@ -243,26 +243,29 @@ export class AgentRunner {
|
|
|
243
243
|
logger.info(`[AgentRunner] Interrupted session: ${sessionId}`);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
hasActiveStream(sessionId) {
|
|
247
|
+
return this.activeStreams.has(sessionId);
|
|
248
|
+
}
|
|
246
249
|
registerStream(key, stream) {
|
|
247
250
|
this.activeStreams.set(key, stream);
|
|
248
251
|
}
|
|
249
252
|
cleanupStream(sessionId) {
|
|
250
253
|
this.activeStreams.delete(sessionId);
|
|
251
254
|
}
|
|
252
|
-
updateSessionId(sessionId,
|
|
253
|
-
logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId},
|
|
254
|
-
this.activeSessions.set(sessionId,
|
|
255
|
+
updateSessionId(sessionId, agentSessionId) {
|
|
256
|
+
logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
257
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
255
258
|
if (this.onSessionIdUpdate) {
|
|
256
|
-
this.onSessionIdUpdate(sessionId,
|
|
259
|
+
this.onSessionIdUpdate(sessionId, agentSessionId);
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
|
-
runSessionCommand(prompt,
|
|
262
|
+
runSessionCommand(prompt, agentSessionId, projectPath) {
|
|
260
263
|
return query({
|
|
261
264
|
prompt,
|
|
262
265
|
options: {
|
|
263
266
|
cwd: projectPath,
|
|
264
267
|
model: this.model,
|
|
265
|
-
resume:
|
|
268
|
+
resume: agentSessionId,
|
|
266
269
|
maxTurns: 1,
|
|
267
270
|
permissionMode: 'default',
|
|
268
271
|
env: this.getAgentEnv()
|
|
@@ -272,10 +275,10 @@ export class AgentRunner {
|
|
|
272
275
|
/**
|
|
273
276
|
* 主动压缩会话上下文
|
|
274
277
|
*/
|
|
275
|
-
async compactSession(sessionId,
|
|
278
|
+
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
276
279
|
try {
|
|
277
|
-
logger.info(`[AgentRunner] Compacting session: ${
|
|
278
|
-
const stream = this.runSessionCommand('/compact',
|
|
280
|
+
logger.info(`[AgentRunner] Compacting session: ${agentSessionId}`);
|
|
281
|
+
const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
|
|
279
282
|
for await (const event of stream) {
|
|
280
283
|
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
281
284
|
logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
|
|
@@ -292,10 +295,10 @@ export class AgentRunner {
|
|
|
292
295
|
/**
|
|
293
296
|
* 通过 SDK /clear 命令清空会话历史
|
|
294
297
|
*/
|
|
295
|
-
async clearSession(
|
|
298
|
+
async clearSession(agentSessionId, projectPath) {
|
|
296
299
|
try {
|
|
297
|
-
logger.info(`[AgentRunner] Clearing session via SDK: ${
|
|
298
|
-
const stream = this.runSessionCommand('/clear',
|
|
300
|
+
logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
|
|
301
|
+
const stream = this.runSessionCommand('/clear', agentSessionId, projectPath);
|
|
299
302
|
for await (const event of stream) {
|
|
300
303
|
logger.debug(`[AgentRunner] Clear event: type=${event.type}, subtype=${event.subtype || 'none'}`);
|
|
301
304
|
}
|
|
@@ -54,8 +54,9 @@ const aliases = {
|
|
|
54
54
|
'/name': '/rename'
|
|
55
55
|
};
|
|
56
56
|
// 命令快速路径前缀(不进入消息队列的命令)
|
|
57
|
-
// 注意:/
|
|
58
|
-
|
|
57
|
+
// 注意:/clear, /compact, /safe 故意不在此列表中,它们需要进入队列触发中断机制
|
|
58
|
+
// /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 '];
|
|
59
60
|
export class CommandHandler {
|
|
60
61
|
sessionManager;
|
|
61
62
|
agentRunner;
|
|
@@ -82,8 +83,19 @@ export class CommandHandler {
|
|
|
82
83
|
getProjectName(projectPath) {
|
|
83
84
|
return this.getConfiguredProjectName(projectPath) || path.basename(projectPath);
|
|
84
85
|
}
|
|
86
|
+
/** 获取消息队列 key:话题用 session.id,主会话用 channel-channelId */
|
|
87
|
+
getQueueKey(session, channel, channelId) {
|
|
88
|
+
if (session?.threadId)
|
|
89
|
+
return session.id;
|
|
90
|
+
return `${channel}-${channelId}`;
|
|
91
|
+
}
|
|
85
92
|
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
86
|
-
async ensureSession(channel, channelId) {
|
|
93
|
+
async ensureSession(channel, channelId, threadId) {
|
|
94
|
+
if (threadId) {
|
|
95
|
+
// 话题会话:按 thread_id 查找
|
|
96
|
+
const session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
97
|
+
return { session };
|
|
98
|
+
}
|
|
87
99
|
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
88
100
|
if (!session) {
|
|
89
101
|
return { error: '❌ 当前没有活跃会话\n使用 /new 创建新会话' };
|
|
@@ -111,7 +123,7 @@ export class CommandHandler {
|
|
|
111
123
|
/**
|
|
112
124
|
* 主命令处理入口
|
|
113
125
|
*/
|
|
114
|
-
async handle(content, channel, channelId, sendMessage, userId) {
|
|
126
|
+
async handle(content, channel, channelId, sendMessage, userId, threadId) {
|
|
115
127
|
// 规范化命令(将别名转换为完整命令)
|
|
116
128
|
let normalizedContent = content;
|
|
117
129
|
for (const [alias, full] of Object.entries(aliases)) {
|
|
@@ -122,6 +134,13 @@ export class CommandHandler {
|
|
|
122
134
|
}
|
|
123
135
|
// 权限检查:区分用户级命令和管理级命令
|
|
124
136
|
const { isOwner: checkOwner } = await import('../config.js');
|
|
137
|
+
// 话题内禁用部分命令
|
|
138
|
+
if (threadId) {
|
|
139
|
+
const threadBlocked = ['/new', '/slist', '/plist', '/bind', '/s', '/session', '/project', '/p', '/fork'];
|
|
140
|
+
const isBlocked = threadBlocked.some(c => normalizedContent === c || normalizedContent.startsWith(c + ' '));
|
|
141
|
+
if (isBlocked)
|
|
142
|
+
return '⚠️ 话题中不支持此命令';
|
|
143
|
+
}
|
|
125
144
|
const isAdmin = !userId || checkOwner(this.config, channel, userId);
|
|
126
145
|
if (normalizedContent.startsWith('/')) {
|
|
127
146
|
const userCommands = ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/s '];
|
|
@@ -216,9 +235,18 @@ export class CommandHandler {
|
|
|
216
235
|
}
|
|
217
236
|
// /stop 命令:中断当前任务
|
|
218
237
|
if (normalizedContent === '/stop') {
|
|
219
|
-
|
|
238
|
+
// 话题使用 session.id 作为队列 key,主会话使用 channel-channelId
|
|
239
|
+
let sessionKey;
|
|
240
|
+
if (threadId) {
|
|
241
|
+
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
242
|
+
sessionKey = threadSession.id;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
sessionKey = `${channel}-${channelId}`;
|
|
246
|
+
}
|
|
220
247
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
221
|
-
|
|
248
|
+
const hasActive = this.agentRunner.hasActiveStream(sessionKey);
|
|
249
|
+
if (queueLength === 0 && !hasActive) {
|
|
222
250
|
return '当前没有正在处理的任务';
|
|
223
251
|
}
|
|
224
252
|
await this.agentRunner.interrupt(sessionKey);
|
|
@@ -226,19 +254,19 @@ export class CommandHandler {
|
|
|
226
254
|
}
|
|
227
255
|
// /clear 命令:通过 SDK /clear 清空会话历史
|
|
228
256
|
if (normalizedContent === '/clear') {
|
|
229
|
-
const result = await this.ensureSession(channel, channelId);
|
|
257
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
230
258
|
if ('error' in result)
|
|
231
259
|
return result.error;
|
|
232
260
|
const { session } = result;
|
|
233
|
-
if (!session.
|
|
261
|
+
if (!session.agentSessionId) {
|
|
234
262
|
return '❌ 当前会话没有历史记录,无需清空';
|
|
235
263
|
}
|
|
236
264
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
237
265
|
? session.projectPath
|
|
238
266
|
: path.resolve(process.cwd(), session.projectPath);
|
|
239
|
-
const cleared = await this.agentRunner.clearSession(session.
|
|
267
|
+
const cleared = await this.agentRunner.clearSession(session.agentSessionId, projectPath);
|
|
240
268
|
if (cleared) {
|
|
241
|
-
await this.sessionManager.
|
|
269
|
+
await this.sessionManager.updateAgentSessionIdBySessionId(session.id, '');
|
|
242
270
|
this.agentRunner.updateSessionId(session.id, '');
|
|
243
271
|
return '✅ 已清空当前会话的对话历史';
|
|
244
272
|
}
|
|
@@ -248,11 +276,11 @@ export class CommandHandler {
|
|
|
248
276
|
}
|
|
249
277
|
// /compact 命令:手动压缩会话上下文
|
|
250
278
|
if (normalizedContent === '/compact') {
|
|
251
|
-
const result = await this.ensureSession(channel, channelId);
|
|
279
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
252
280
|
if ('error' in result)
|
|
253
281
|
return result.error;
|
|
254
282
|
const { session } = result;
|
|
255
|
-
if (!session.
|
|
283
|
+
if (!session.agentSessionId) {
|
|
256
284
|
return '❌ 当前会话没有历史记录,无需压缩';
|
|
257
285
|
}
|
|
258
286
|
const projectPath = path.isAbsolute(session.projectPath)
|
|
@@ -261,7 +289,7 @@ export class CommandHandler {
|
|
|
261
289
|
if (sendMessage) {
|
|
262
290
|
await sendMessage(channelId, '⏳ 正在压缩会话上下文...');
|
|
263
291
|
}
|
|
264
|
-
const compacted = await this.agentRunner.compactSession(session.id, session.
|
|
292
|
+
const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, projectPath);
|
|
265
293
|
if (compacted) {
|
|
266
294
|
return '✅ 会话上下文已压缩';
|
|
267
295
|
}
|
|
@@ -269,8 +297,14 @@ export class CommandHandler {
|
|
|
269
297
|
return '❌ 会话压缩失败,请稍后重试';
|
|
270
298
|
}
|
|
271
299
|
}
|
|
272
|
-
//
|
|
273
|
-
let session
|
|
300
|
+
// 尝试获取活跃会话(话题时直接查找话题 session)
|
|
301
|
+
let session;
|
|
302
|
+
if (threadId) {
|
|
303
|
+
session = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
307
|
+
}
|
|
274
308
|
// 对于需要创建会话的命令,如果没有会话则创建
|
|
275
309
|
if (!session && (normalizedContent.startsWith('/new') ||
|
|
276
310
|
normalizedContent.startsWith('/bind') ||
|
|
@@ -286,11 +320,12 @@ export class CommandHandler {
|
|
|
286
320
|
|
|
287
321
|
提示:发送任意消息或使用 /new 命令创建会话`;
|
|
288
322
|
}
|
|
289
|
-
const sessionKey =
|
|
323
|
+
const sessionKey = this.getQueueKey(session, channel, channelId);
|
|
290
324
|
const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
291
325
|
const queueLength = this.messageQueue.getQueueLength(sessionKey);
|
|
292
|
-
|
|
293
|
-
|
|
326
|
+
const isThread = !!session.threadId;
|
|
327
|
+
let activeStatus = isThread ? '话题' : (session.isActive ? '✓ 活跃' : '休眠');
|
|
328
|
+
if ((isThread || session.isActive) && isCurrentlyProcessing) {
|
|
294
329
|
if (queueLength > 0) {
|
|
295
330
|
activeStatus += ` [处理中,队列${queueLength}条]`;
|
|
296
331
|
}
|
|
@@ -306,8 +341,8 @@ export class CommandHandler {
|
|
|
306
341
|
`${Math.floor(timeSinceSuccess / 3600000)}小时前`;
|
|
307
342
|
// 获取会话文件信息并同步 name
|
|
308
343
|
let sessionTurns = 0;
|
|
309
|
-
if (session.
|
|
310
|
-
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.
|
|
344
|
+
if (session.agentSessionId) {
|
|
345
|
+
const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId);
|
|
311
346
|
sessionTurns = fileInfo.turns;
|
|
312
347
|
if (fileInfo.title && fileInfo.title !== session.name) {
|
|
313
348
|
await this.sessionManager.renameSession(session.id, fileInfo.title);
|
|
@@ -316,10 +351,10 @@ export class CommandHandler {
|
|
|
316
351
|
}
|
|
317
352
|
const lines = [];
|
|
318
353
|
if (isAdmin) {
|
|
319
|
-
lines.push('
|
|
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')}`);
|
|
320
355
|
}
|
|
321
356
|
else {
|
|
322
|
-
lines.push('
|
|
357
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `会话: ${session.name || '(未命名)'}`, `状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
323
358
|
}
|
|
324
359
|
if (health.safeMode) {
|
|
325
360
|
lines.push('');
|
|
@@ -380,10 +415,17 @@ export class CommandHandler {
|
|
|
380
415
|
return sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。';
|
|
381
416
|
}
|
|
382
417
|
}
|
|
418
|
+
// 话题中 restart 时保存 rootId 用于重启后回复到话题
|
|
419
|
+
let rootId;
|
|
420
|
+
if (threadId) {
|
|
421
|
+
const threadSession = await this.sessionManager.getOrCreateSession(channel, channelId, this.config.projects?.defaultPath || process.cwd(), threadId);
|
|
422
|
+
rootId = threadSession.metadata?.feishu?.rootId;
|
|
423
|
+
}
|
|
383
424
|
const restartInfo = {
|
|
384
425
|
channel,
|
|
385
426
|
channelId,
|
|
386
|
-
timestamp: Date.now()
|
|
427
|
+
timestamp: Date.now(),
|
|
428
|
+
...(rootId ? { rootId } : {})
|
|
387
429
|
};
|
|
388
430
|
fs.writeFileSync(path.join(resolvePaths().dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
|
|
389
431
|
const { spawn } = await import('child_process');
|
|
@@ -508,7 +550,7 @@ export class CommandHandler {
|
|
|
508
550
|
}
|
|
509
551
|
const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath);
|
|
510
552
|
const cachedEvents = this.messageCache.getEvents(newSession.id);
|
|
511
|
-
const hasExistingSession = newSession.
|
|
553
|
+
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
512
554
|
let response = `✓ 已切换到项目: ${projectName}\n 路径: ${projectPath}\n ${hasExistingSession}`;
|
|
513
555
|
if (cachedEvents.length > 0 && sendMessage) {
|
|
514
556
|
for (const event of cachedEvents) {
|
|
@@ -544,7 +586,7 @@ export class CommandHandler {
|
|
|
544
586
|
}
|
|
545
587
|
const newSession = await this.sessionManager.switchProject(channel, channelId, projectPath);
|
|
546
588
|
const cachedEvents = this.messageCache.getEvents(newSession.id);
|
|
547
|
-
const hasExistingSession = newSession.
|
|
589
|
+
const hasExistingSession = newSession.agentSessionId ? '(恢复已有会话)' : '(新建会话)';
|
|
548
590
|
let response = `✓ 已绑定项目目录: ${projectPath}\n ${hasExistingSession}`;
|
|
549
591
|
if (cachedEvents.length > 0) {
|
|
550
592
|
response += `\n\n后台任务结果:`;
|
|
@@ -585,7 +627,7 @@ export class CommandHandler {
|
|
|
585
627
|
const sdkName = sdkSession.customTitle || undefined;
|
|
586
628
|
if (!sdkName)
|
|
587
629
|
continue;
|
|
588
|
-
const dbSession = currentProjectSessions.find(s => s.
|
|
630
|
+
const dbSession = currentProjectSessions.find(s => s.agentSessionId === sdkSession.sessionId);
|
|
589
631
|
if (dbSession && sdkName !== dbSession.name) {
|
|
590
632
|
await this.sessionManager.renameSession(dbSession.id, sdkName);
|
|
591
633
|
dbSession.name = sdkName;
|
|
@@ -599,7 +641,7 @@ export class CommandHandler {
|
|
|
599
641
|
const cliSessions = (isGroup || !isAdmin)
|
|
600
642
|
? []
|
|
601
643
|
: await this.sessionManager.scanCliSessions(session.projectPath);
|
|
602
|
-
const dbSessionIds = new Set(currentProjectSessions.map(s => s.
|
|
644
|
+
const dbSessionIds = new Set(currentProjectSessions.map(s => s.agentSessionId).filter(Boolean));
|
|
603
645
|
const lines = [`当前项目 ${path.basename(session.projectPath)} 的会话列表:\n`];
|
|
604
646
|
const sessionKey = `${channel}-${channelId}`;
|
|
605
647
|
const isProcessing = this.messageQueue.isProcessing(sessionKey);
|
|
@@ -608,9 +650,9 @@ export class CommandHandler {
|
|
|
608
650
|
for (const s of currentProjectSessions) {
|
|
609
651
|
const prefix = s.isActive ? ' ✓' : ' ';
|
|
610
652
|
const name = s.name || '(未命名)';
|
|
611
|
-
const uuid = s.
|
|
653
|
+
const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
|
|
612
654
|
const idleTime = formatIdleTime(Date.now() - s.updatedAt);
|
|
613
|
-
if (s.
|
|
655
|
+
if (s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId)) {
|
|
614
656
|
lines.push(`${prefix} ❌ ${name} ${uuid} - ${idleTime} [会话文件缺失]`);
|
|
615
657
|
}
|
|
616
658
|
else {
|
|
@@ -673,8 +715,8 @@ export class CommandHandler {
|
|
|
673
715
|
if (!targetSession) {
|
|
674
716
|
return `❌ 会话不存在: ${sessionName}\n使用 /slist 查看可用会话`;
|
|
675
717
|
}
|
|
676
|
-
const lastInput = targetSession.
|
|
677
|
-
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.
|
|
718
|
+
const lastInput = targetSession.agentSessionId
|
|
719
|
+
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId)
|
|
678
720
|
: null;
|
|
679
721
|
const lastInputLine = lastInput ? `\n 最后输入: "${lastInput}"` : '';
|
|
680
722
|
if (!session) {
|
|
@@ -711,9 +753,9 @@ export class CommandHandler {
|
|
|
711
753
|
return `❌ 会话名称 "${newName}" 已存在,请使用其他名称`;
|
|
712
754
|
}
|
|
713
755
|
// 双写:SDK + 数据库
|
|
714
|
-
if (session.
|
|
756
|
+
if (session.agentSessionId) {
|
|
715
757
|
try {
|
|
716
|
-
await sdkRenameSession(session.
|
|
758
|
+
await sdkRenameSession(session.agentSessionId, newName, { dir: session.projectPath });
|
|
717
759
|
}
|
|
718
760
|
catch (error) {
|
|
719
761
|
logger.warn(`[CommandHandler] SDK renameSession failed (continuing with db update):`, error);
|
|
@@ -731,11 +773,11 @@ export class CommandHandler {
|
|
|
731
773
|
if (!session) {
|
|
732
774
|
return `❌ 当前没有活跃会话,无法分支`;
|
|
733
775
|
}
|
|
734
|
-
if (!session.
|
|
776
|
+
if (!session.agentSessionId) {
|
|
735
777
|
return `❌ 当前会话尚未初始化 Claude 对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
|
|
736
778
|
}
|
|
737
779
|
try {
|
|
738
|
-
const forkResult = await sdkForkSession(session.
|
|
780
|
+
const forkResult = await sdkForkSession(session.agentSessionId, { dir: session.projectPath, title: forkName });
|
|
739
781
|
const newSession = await this.sessionManager.createForkedSession(session, forkResult.sessionId, forkName);
|
|
740
782
|
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /slist 查看所有会话,/s <名称> 切换回原会话`;
|
|
741
783
|
}
|
|
@@ -757,7 +799,7 @@ export class CommandHandler {
|
|
|
757
799
|
const fsPromises = await import('fs/promises');
|
|
758
800
|
try {
|
|
759
801
|
const backupDir = await backupClaudeDir(session.projectPath);
|
|
760
|
-
if (!session.
|
|
802
|
+
if (!session.agentSessionId) {
|
|
761
803
|
await this.sessionManager.resetHealthStatus(session.id);
|
|
762
804
|
return `✓ 修复完成,已退出安全模式
|
|
763
805
|
|
|
@@ -768,11 +810,11 @@ export class CommandHandler {
|
|
|
768
810
|
|
|
769
811
|
备份位置:${backupDir}`;
|
|
770
812
|
}
|
|
771
|
-
const healthCheck = await checkSessionFileHealth(session.projectPath, session.
|
|
813
|
+
const healthCheck = await checkSessionFileHealth(session.projectPath, session.agentSessionId);
|
|
772
814
|
if (healthCheck.corrupt) {
|
|
773
|
-
const sessionFile = path.join(session.projectPath, '.claude', `${session.
|
|
815
|
+
const sessionFile = path.join(session.projectPath, '.claude', `${session.agentSessionId}.jsonl`);
|
|
774
816
|
await fsPromises.unlink(sessionFile);
|
|
775
|
-
await this.sessionManager.
|
|
817
|
+
await this.sessionManager.updateAgentSessionId(session.channel, session.channelId, '');
|
|
776
818
|
await this.sessionManager.resetHealthStatus(session.id);
|
|
777
819
|
return `✓ 修复完成,已退出安全模式
|
|
778
820
|
|