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.
@@ -28,8 +28,9 @@
28
28
  },
29
29
  "idleMonitor": {
30
30
  "enabled": true,
31
- "timeout": 120000,
31
+ "timeout": 120,
32
32
  "safeModeThreshold": 3
33
33
  },
34
- "flushDelay": 4000
34
+ "flushDelay": 4,
35
+ "showActivities": "all"
35
36
  }
@@ -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
- if (msg.parent_id && this.client) {
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: { msg_type: msgType, content: msgContent }
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
- await client.im.message.create({
717
- params: { receive_id_type: 'chat_id' },
718
- data: {
719
- receive_id: pendingInfo.channelId,
720
- msg_type: 'text',
721
- content: JSON.stringify({ text: message }),
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
- // 优先使用传入的 claudeSessionId(从数据库恢复),否则使用内存中的
48
- let claudeSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
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
- claudeSessionId = undefined;
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
- // 验证会话文件是否存在且有效(仅在非安全模式且有 claudeSessionId 时)
61
- if (claudeSessionId && !skipResume) {
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, `${claudeSessionId}.jsonl`);
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
- claudeSessionId = undefined;
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, claudeSessionId:', initialClaudeSessionId);
224
- queryStream = createQuery(prompt, claudeSessionId);
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, claudeSessionId) {
253
- logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, claudeSessionId=${claudeSessionId}`);
254
- this.activeSessions.set(sessionId, claudeSessionId);
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, claudeSessionId);
259
+ this.onSessionIdUpdate(sessionId, agentSessionId);
257
260
  }
258
261
  }
259
- runSessionCommand(prompt, claudeSessionId, projectPath) {
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: claudeSessionId,
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, claudeSessionId, projectPath) {
278
+ async compactSession(sessionId, agentSessionId, projectPath) {
276
279
  try {
277
- logger.info(`[AgentRunner] Compacting session: ${claudeSessionId}`);
278
- const stream = this.runSessionCommand('/compact', claudeSessionId, projectPath);
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(claudeSessionId, projectPath) {
298
+ async clearSession(agentSessionId, projectPath) {
296
299
  try {
297
- logger.info(`[AgentRunner] Clearing session via SDK: ${claudeSessionId}`);
298
- const stream = this.runSessionCommand('/clear', claudeSessionId, projectPath);
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
- // 注意:/stop, /clear, /compact, /safe 故意不在此列表中,它们需要进入队列触发中断机制
58
- const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/slist', '/session', '/rename', '/repair', '/fork', '/p ', '/s ', '/name '];
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
- const sessionKey = `${channel}-${channelId}`;
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
- if (queueLength === 0) {
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.claudeSessionId) {
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.claudeSessionId, projectPath);
267
+ const cleared = await this.agentRunner.clearSession(session.agentSessionId, projectPath);
240
268
  if (cleared) {
241
- await this.sessionManager.updateClaudeSessionIdBySessionId(session.id, '');
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.claudeSessionId) {
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.claudeSessionId, projectPath);
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 = await this.sessionManager.getActiveSession(channel, channelId);
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 = `${channel}-${channelId}`;
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
- let activeStatus = session.isActive ? '✓ 活跃' : '休眠';
293
- if (session.isActive && isCurrentlyProcessing) {
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.claudeSessionId) {
310
- const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.claudeSessionId);
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('📊 会话状态:', `渠道: ${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')}`);
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('📊 会话状态:', `会话: ${session.name || '(未命名)'}`, `状态: ${activeStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
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.claudeSessionId ? '(恢复已有会话)' : '(新建会话)';
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.claudeSessionId ? '(恢复已有会话)' : '(新建会话)';
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.claudeSessionId === sdkSession.sessionId);
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.claudeSessionId).filter(Boolean));
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.claudeSessionId ? `(${s.claudeSessionId.substring(0, 8)})` : '';
653
+ const uuid = s.agentSessionId ? `(${s.agentSessionId.substring(0, 8)})` : '';
612
654
  const idleTime = formatIdleTime(Date.now() - s.updatedAt);
613
- if (s.claudeSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.claudeSessionId)) {
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.claudeSessionId
677
- ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.claudeSessionId)
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.claudeSessionId) {
756
+ if (session.agentSessionId) {
715
757
  try {
716
- await sdkRenameSession(session.claudeSessionId, newName, { dir: session.projectPath });
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.claudeSessionId) {
776
+ if (!session.agentSessionId) {
735
777
  return `❌ 当前会话尚未初始化 Claude 对话,无法分支\n\n请先发送一条消息,然后再使用 /fork`;
736
778
  }
737
779
  try {
738
- const forkResult = await sdkForkSession(session.claudeSessionId, { dir: session.projectPath, title: forkName });
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.claudeSessionId) {
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.claudeSessionId);
813
+ const healthCheck = await checkSessionFileHealth(session.projectPath, session.agentSessionId);
772
814
  if (healthCheck.corrupt) {
773
- const sessionFile = path.join(session.projectPath, '.claude', `${session.claudeSessionId}.jsonl`);
815
+ const sessionFile = path.join(session.projectPath, '.claude', `${session.agentSessionId}.jsonl`);
774
816
  await fsPromises.unlink(sessionFile);
775
- await this.sessionManager.updateClaudeSessionId(session.channel, session.channelId, '');
817
+ await this.sessionManager.updateAgentSessionId(session.channel, session.channelId, '');
776
818
  await this.sessionManager.resetHealthStatus(session.id);
777
819
  return `✓ 修复完成,已退出安全模式
778
820