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
|
@@ -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 ??
|
|
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
|
|
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(
|
|
177
|
+
async checkSafeMode(session, channelId, adapter, safeModeThreshold, consecutiveErrors) {
|
|
153
178
|
if (safeModeThreshold <= 0)
|
|
154
179
|
return;
|
|
155
|
-
const health = await this.sessionManager.getHealthStatus(
|
|
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(
|
|
158
|
-
logger.warn(`[MessageProcessor] Session ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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}`);
|