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.
@@ -18,6 +18,14 @@ export class MessageProcessor {
18
18
  channels = new Map();
19
19
  currentFlusher;
20
20
  currentIsGroup = false;
21
+ shouldSuppressActivities = false;
22
+ /** 话题 session 永远不是后台任务;主 session 与当前活跃 session 比对 */
23
+ async isBackgroundSession(session, channel, channelId) {
24
+ if (session.threadId)
25
+ return false;
26
+ const active = await this.sessionManager.getActiveSession(channel, channelId);
27
+ return active ? session.id !== active.id : false;
28
+ }
21
29
  constructor(agentRunner, sessionManager, config, messageCache, commandHandler) {
22
30
  this.agentRunner = agentRunner;
23
31
  this.sessionManager = sessionManager;
@@ -35,7 +43,7 @@ export class MessageProcessor {
35
43
  * 处理 compact 开始事件
36
44
  */
37
45
  handleCompactStart() {
38
- if (this.currentFlusher && !this.currentIsGroup) {
46
+ if (this.currentFlusher && !this.currentIsGroup && !this.shouldSuppressActivities) {
39
47
  this.currentFlusher.addActivity('⏳ 会话压缩中...');
40
48
  }
41
49
  }
@@ -45,7 +53,7 @@ export class MessageProcessor {
45
53
  async processMessage(message) {
46
54
  const isGroup = message.isGroup ?? false;
47
55
  this.currentIsGroup = isGroup;
48
- const idleMs = this.config.idleMonitor?.timeout ?? 120000;
56
+ const idleMs = (this.config.idleMonitor?.timeout ?? 120) * 1000;
49
57
  const streamKey = `${message.channel}-${message.channelId}`;
50
58
  const channelInfo = this.channels.get(message.channel);
51
59
  const monitorEnabled = this.config.idleMonitor?.enabled !== false;
@@ -53,6 +61,18 @@ export class MessageProcessor {
53
61
  const isOwnerUser = isOwner(this.config, message.channel, message.userId || '');
54
62
  // 非主人(群聊或单聊):空闲监控静默/简短
55
63
  const quietMode = isGroup || !isOwnerUser;
64
+ // 计算是否抑制活动输出
65
+ const shouldSuppress = () => {
66
+ const mode = this.config.showActivities ?? 'all';
67
+ if (mode === 'all')
68
+ return false;
69
+ if (mode === 'dm-only')
70
+ return isGroup;
71
+ if (mode === 'owner-dm-only')
72
+ return isGroup || !isOwnerUser;
73
+ return false;
74
+ };
75
+ this.shouldSuppressActivities = shouldSuppress();
56
76
  let monitor;
57
77
  let monitorInterval;
58
78
  let rejectFn;
@@ -94,7 +114,7 @@ export class MessageProcessor {
94
114
  else {
95
115
  // notify or warn: send diagnostic message, task continues(非主人时静默)
96
116
  logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
97
- if (channelInfo && !quietMode) {
117
+ if (channelInfo && !quietMode && !shouldSuppress()) {
98
118
  try {
99
119
  await channelInfo.adapter.sendText(message.channelId, result.message);
100
120
  }
@@ -109,7 +129,7 @@ export class MessageProcessor {
109
129
  });
110
130
  try {
111
131
  await Promise.race([
112
- this._processMessageInternal(message, resetTimer, isGroup),
132
+ this._processMessageInternal(message, resetTimer, isGroup, shouldSuppress),
113
133
  timeoutPromise
114
134
  ]);
115
135
  }
@@ -119,7 +139,7 @@ export class MessageProcessor {
119
139
  // 记录错误到健康状态(仅主人的错误累计触发安全模式)
120
140
  if (channelInfo) {
121
141
  try {
122
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd());
142
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
123
143
  const errorType = classifyError(error);
124
144
  // 上下文过长是可恢复错误,不累计触发安全模式
125
145
  if (errorType === ErrorType.CONTEXT_TOO_LONG) {
@@ -131,7 +151,7 @@ export class MessageProcessor {
131
151
  }
132
152
  else {
133
153
  const newCount = await this.sessionManager.recordError(session.id, errorType, error.message);
134
- await this.checkSafeMode(session.id, message.channelId, channelInfo.adapter, safeModeThreshold, newCount);
154
+ await this.checkSafeMode(session, message.channelId, channelInfo.adapter, safeModeThreshold, newCount);
135
155
  }
136
156
  }
137
157
  catch (statusError) {
@@ -145,17 +165,27 @@ export class MessageProcessor {
145
165
  clearInterval(monitorInterval);
146
166
  }
147
167
  }
168
+ /** 从 session 提取话题回复选项 */
169
+ getThreadSendOpts(session) {
170
+ const rootId = session.metadata?.feishu?.rootId;
171
+ return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
172
+ }
148
173
  /**
149
174
  * 检查是否需要进入安全模式(safeModeThreshold 为 0 时跳过)
150
175
  * 仅单聊主人会话调用(群聊和非主人已在调用侧过滤)
151
176
  */
152
- async checkSafeMode(sessionId, channelId, adapter, safeModeThreshold, consecutiveErrors) {
177
+ async checkSafeMode(session, channelId, adapter, safeModeThreshold, consecutiveErrors) {
153
178
  if (safeModeThreshold <= 0)
154
179
  return;
155
- const health = await this.sessionManager.getHealthStatus(sessionId);
180
+ const health = await this.sessionManager.getHealthStatus(session.id);
181
+ const sendOpts = this.getThreadSendOpts(session);
182
+ const isThread = !!session.threadId;
156
183
  if (consecutiveErrors >= safeModeThreshold && !health.safeMode) {
157
- await this.sessionManager.setSafeMode(sessionId, true);
158
- logger.warn(`[MessageProcessor] Session ${sessionId} entered safe mode after ${consecutiveErrors} errors`);
184
+ await this.sessionManager.setSafeMode(session.id, true);
185
+ logger.warn(`[MessageProcessor] Session ${session.id} entered safe mode after ${consecutiveErrors} errors`);
186
+ const suggestions = isThread
187
+ ? `1. /repair - 检查并修复会话(推荐,保留历史)\n2. /clear - 清空会话历史\n3. /status - 查看详细状态`
188
+ : `1. /repair - 检查并修复会话(推荐,保留历史)\n2. /new [名称] - 创建新会话(清空历史)\n3. /status - 查看详细状态`;
159
189
  await adapter.sendText(channelId, `⚠️ 安全模式已启用(连续 ${consecutiveErrors} 次异常)
160
190
 
161
191
  当前限制:
@@ -163,16 +193,13 @@ export class MessageProcessor {
163
193
  - 每次提问需要提供完整上下文
164
194
 
165
195
  建议操作:
166
- 1. /repair - 检查并修复会话(推荐,保留历史)
167
- 2. /new [名称] - 创建新会话(清空历史)
168
- 3. /status - 查看详细状态`);
196
+ ${suggestions}`, sendOpts);
169
197
  }
170
198
  else if (safeModeThreshold >= 2 && consecutiveErrors === safeModeThreshold - 1) {
171
- // 阈值前一次错误,发送警告
172
- await adapter.sendText(channelId, `⚠️ 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`);
199
+ await adapter.sendText(channelId, `⚠️ 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`, sendOpts);
173
200
  }
174
201
  }
175
- async _processMessageInternal(message, resetTimer, isGroup) {
202
+ async _processMessageInternal(message, resetTimer, isGroup, shouldSuppress) {
176
203
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
177
204
  const channelInfo = this.channels.get(message.channel);
178
205
  if (!channelInfo) {
@@ -183,17 +210,19 @@ export class MessageProcessor {
183
210
  try {
184
211
  // 检查是否为命令
185
212
  if (this.commandHandler) {
186
- const cmdResult = await this.commandHandler(message.content, message.channel, message.channelId, message.userId);
213
+ const cmdResult = await this.commandHandler(message.content, message.channel, message.channelId, message.userId, message.threadId);
187
214
  if (cmdResult) {
188
- await adapter.sendText(message.channelId, cmdResult);
215
+ // 话题消息:通过 rootId 回复到话题内
216
+ const session = message.threadId ? await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId) : undefined;
217
+ const rootId = session?.metadata?.feishu?.rootId;
218
+ const sendOpts = rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
219
+ await adapter.sendText(message.channelId, cmdResult, sendOpts);
189
220
  return;
190
221
  }
191
222
  }
192
223
  // 解析会话和项目路径
193
224
  const { session, absoluteProjectPath } = await this.resolveSession(message);
194
- // 判断是否是后台任务
195
- const activeSession = await this.sessionManager.getActiveSession(message.channel, message.channelId);
196
- const isBackground = activeSession ? session.id !== activeSession.id : false;
225
+ const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
197
226
  // 记录收到消息
198
227
  logger.message({
199
228
  msgId: messageId,
@@ -217,43 +246,47 @@ export class MessageProcessor {
217
246
  let firstReply = true;
218
247
  const messageIsGroup = isGroup; // 捕获 isGroup 供闭包使用
219
248
  const flusher = new StreamFlusher(async (text, isFinal) => {
220
- // 动态判断是否是后台任务
221
- const currentActiveSession = await this.sessionManager.getActiveSession(message.channel, message.channelId);
222
- const isCurrentlyBackground = currentActiveSession ? session.id !== currentActiveSession.id : false;
249
+ const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
223
250
  if (!isCurrentlyBackground) {
224
251
  const opts = {};
225
252
  if (isFinal)
226
253
  opts.title = '最终回复:';
227
- // 首条消息引用回复用户原消息
228
- if (firstReply && message.messageId) {
254
+ // 话题会话:所有回复指向 rootId + reply_in_thread(确保消息进入话题)
255
+ const rootId = session.metadata?.feishu?.rootId;
256
+ if (rootId) {
257
+ opts.replyToMessageId = rootId;
258
+ opts.replyInThread = true;
259
+ }
260
+ else if (firstReply && message.messageId) {
261
+ // 主会话:首条消息引用回复用户原消息
229
262
  opts.replyToMessageId = message.messageId;
230
263
  firstReply = false;
231
264
  }
232
265
  await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
233
266
  }
234
267
  // 后台任务:静默,不发送输出
235
- }, this.config.flushDelay ?? 4000, options?.fileMarkerPattern);
268
+ }, this.config.flushDelay ? this.config.flushDelay * 1000 : 4000, options?.fileMarkerPattern);
236
269
  // 保存当前 flusher,用于 compact 事件
237
270
  this.currentFlusher = flusher;
238
271
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
239
272
  const streamKey = `${message.channel}-${message.channelId}`;
240
273
  try {
241
- const stream = await this.agentRunner.runQuery(session.id, message.content, absoluteProjectPath, session.claudeSessionId, message.images, options?.systemPromptAppend, this.sessionManager);
274
+ const stream = await this.agentRunner.runQuery(session.id, message.content, absoluteProjectPath, session.agentSessionId, message.images, options?.systemPromptAppend, this.sessionManager);
242
275
  this.agentRunner.registerStream(streamKey, stream);
243
- await this.processEventStream(stream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer);
276
+ await this.processEventStream(stream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress);
244
277
  }
245
278
  catch (error) {
246
- if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.claudeSessionId) {
279
+ if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId) {
247
280
  // 尝试 compact 压缩会话
248
281
  flusher.addActivity('⚠️ 上下文过长,正在压缩会话...');
249
282
  await flusher.flush();
250
- const compacted = await this.agentRunner.compactSession(session.id, session.claudeSessionId, absoluteProjectPath);
283
+ const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, absoluteProjectPath);
251
284
  if (compacted) {
252
285
  // compact 成功,带 resume 重试
253
286
  flusher.addActivity('✅ 压缩完成,正在重试...');
254
- const retryStream = await this.agentRunner.runQuery(session.id, message.content, absoluteProjectPath, session.claudeSessionId, message.images, options?.systemPromptAppend, this.sessionManager);
287
+ const retryStream = await this.agentRunner.runQuery(session.id, message.content, absoluteProjectPath, session.agentSessionId, message.images, options?.systemPromptAppend, this.sessionManager);
255
288
  this.agentRunner.registerStream(streamKey, retryStream);
256
- await this.processEventStream(retryStream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer);
289
+ await this.processEventStream(retryStream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress);
257
290
  }
258
291
  else {
259
292
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -296,15 +329,16 @@ export class MessageProcessor {
296
329
  // 安全模式尾部提示:如果当前会话处于安全模式,追加提醒
297
330
  const healthStatus = await this.sessionManager.getHealthStatus(session.id);
298
331
  if (healthStatus.safeMode) {
299
- await adapter.sendText(message.channelId, '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /new 新建会话');
332
+ const hint = session.threadId
333
+ ? '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /clear 清空会话'
334
+ : '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /new 新建会话';
335
+ await adapter.sendText(message.channelId, hint, this.getThreadSendOpts(session));
300
336
  }
301
337
  // 清理 activeStreams(正常完成)
302
338
  this.agentRunner.cleanupStream(streamKey);
303
339
  // 记录成功响应(重置错误计数)
304
340
  await this.sessionManager.recordSuccess(session.id);
305
- // 动态判断是否是后台任务,决定是否发送通知
306
- const currentActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
307
- const isFinallyBackground = currentActive ? session.id !== currentActive.id : false;
341
+ const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
308
342
  if (isFinallyBackground) {
309
343
  const projectName = path.basename(session.projectPath);
310
344
  const count = this.messageCache.getCount(session.id);
@@ -354,7 +388,7 @@ export class MessageProcessor {
354
388
  * 解析会话和项目路径
355
389
  */
356
390
  async resolveSession(message) {
357
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd());
391
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
358
392
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
359
393
  ? session.projectPath
360
394
  : path.resolve(process.cwd(), session.projectPath);
@@ -363,7 +397,7 @@ export class MessageProcessor {
363
397
  /**
364
398
  * 处理事件流
365
399
  */
366
- async processEventStream(stream, session, channelId, adapter, options, flusher, isBackground, resetTimer) {
400
+ async processEventStream(stream, session, channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress) {
367
401
  let hasTextDelta = false;
368
402
  let hasReceivedText = false;
369
403
  let lastSessionId;
@@ -382,9 +416,7 @@ export class MessageProcessor {
382
416
  this.agentRunner.updateSessionId(session.id, event.session_id);
383
417
  lastSessionId = event.session_id;
384
418
  }
385
- // 动态判断当前是否是后台任务
386
- const currentActive = await this.sessionManager.getActiveSession(session.channel, session.channelId);
387
- const isCurrentlyBackground = currentActive ? session.id !== currentActive.id : false;
419
+ const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
388
420
  // === 前台任务:正常处理所有事件 ===
389
421
  if (!isCurrentlyBackground) {
390
422
  // 流式文本事件
@@ -395,7 +427,7 @@ export class MessageProcessor {
395
427
  }
396
428
  // 系统事件:compact_boundary(群聊时静默)
397
429
  if (event.type === 'system' && event.subtype === 'compact_boundary') {
398
- if (!this.currentIsGroup) {
430
+ if (!this.currentIsGroup && !shouldSuppress()) {
399
431
  const preTokens = event.compact_metadata?.pre_tokens || 0;
400
432
  flusher.addActivity(`💡 会话压缩完成,继续执行...(压缩前 tokens: ${preTokens})`);
401
433
  }
@@ -406,10 +438,10 @@ export class MessageProcessor {
406
438
  const duration = event.duration_ms ? `${Math.round(event.duration_ms / 1000)}s` : '';
407
439
  const summary = event.summary;
408
440
  const stats = [tools > 0 ? `${tools}次工具调用` : '', duration].filter(Boolean).join(', ');
409
- if (summary) {
441
+ if (summary && !shouldSuppress()) {
410
442
  flusher.addActivity(`⏳ 子任务: ${summary}${stats ? ` (${stats})` : ''}`);
411
443
  }
412
- else if (stats) {
444
+ else if (stats && !shouldSuppress()) {
413
445
  flusher.addActivity(`⏳ 子任务进行中: ${stats}`);
414
446
  }
415
447
  }
@@ -417,8 +449,10 @@ export class MessageProcessor {
417
449
  if (event.type === 'assistant' && event.message?.content) {
418
450
  for (const content of event.message.content) {
419
451
  if (content.type === 'tool_use') {
420
- const desc = this.formatToolDescription(content);
421
- flusher.addActivity(`🔧 ${content.name}${desc ? ': ' + desc : ''}`);
452
+ if (!shouldSuppress()) {
453
+ const desc = this.formatToolDescription(content);
454
+ flusher.addActivity(`🔧 ${content.name}${desc ? ': ' + desc : ''}`);
455
+ }
422
456
  }
423
457
  else if (content.type === 'text' && content.text && !hasTextDelta) {
424
458
  // 仅在没有 text_delta 事件时从 assistant 事件提取文本,避免重复
@@ -430,7 +464,7 @@ export class MessageProcessor {
430
464
  // 工具结果事件:显示失败信息(包括权限拒绝、执行失败等所有场景)
431
465
  if (event.type === 'tool_result') {
432
466
  logger.debug(`[MessageProcessor] tool_result: is_error=${event.is_error}, error=${event.error}, content=${typeof event.content}`);
433
- if (event.is_error) {
467
+ if (event.is_error && !shouldSuppress()) {
434
468
  const toolName = event.tool_name || '工具';
435
469
  const errorMsg = event.error || (typeof event.content === 'string' ? event.content : JSON.stringify(event.content)) || '执行失败';
436
470
  flusher.addActivity(`⚠️ ${toolName}: ${errorMsg}`);