evolclaw 2.1.2 → 2.3.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.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -1,604 +0,0 @@
1
- import path from 'path';
2
- import fs from 'fs';
3
- import { StreamFlusher } from '../utils/stream-flusher.js';
4
- import { StreamIdleMonitor } from '../utils/stream-idle-monitor.js';
5
- import { logger } from '../utils/logger.js';
6
- import { getErrorMessage, classifyError, ErrorType } from '../utils/error-utils.js';
7
- import { isOwner } from '../config.js';
8
- /**
9
- * 统一消息处理器
10
- * 负责处理来自不同渠道的消息,协调事件流处理
11
- */
12
- export class MessageProcessor {
13
- agentRunner;
14
- sessionManager;
15
- config;
16
- messageCache;
17
- commandHandler;
18
- channels = new Map();
19
- currentFlusher;
20
- currentIsGroup = false;
21
- shouldSuppressActivities = false;
22
- /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
23
- async isBackgroundSession(session, channel, channelId) {
24
- // 话题会话独立运行,不是后台任务
25
- if (session.threadId)
26
- return false;
27
- // 主会话:与当前活跃会话比对
28
- const active = await this.sessionManager.getActiveSession(channel, channelId);
29
- return active ? session.id !== active.id : false;
30
- }
31
- constructor(agentRunner, sessionManager, config, messageCache, commandHandler) {
32
- this.agentRunner = agentRunner;
33
- this.sessionManager = sessionManager;
34
- this.config = config;
35
- this.messageCache = messageCache;
36
- this.commandHandler = commandHandler;
37
- }
38
- /**
39
- * 注册渠道适配器
40
- */
41
- registerChannel(adapter, options) {
42
- this.channels.set(adapter.name, { adapter, options });
43
- }
44
- /**
45
- * 处理 compact 开始事件
46
- */
47
- handleCompactStart() {
48
- if (this.currentFlusher && !this.currentIsGroup && !this.shouldSuppressActivities) {
49
- this.currentFlusher.addActivity('⏳ 会话压缩中...');
50
- }
51
- }
52
- /**
53
- * 处理消息(主入口)
54
- */
55
- async processMessage(message) {
56
- const isGroup = message.isGroup ?? false;
57
- this.currentIsGroup = isGroup;
58
- const idleMs = (this.config.idleMonitor?.timeout ?? 120) * 1000;
59
- const streamKey = `${message.channel}-${message.channelId}`;
60
- const channelInfo = this.channels.get(message.channel);
61
- const monitorEnabled = this.config.idleMonitor?.enabled !== false;
62
- const safeModeThreshold = this.config.idleMonitor?.safeModeThreshold ?? 3;
63
- const isOwnerUser = isOwner(this.config, message.channel, message.userId || '');
64
- // 非主人(群聊或单聊):空闲监控静默/简短
65
- const quietMode = isGroup || !isOwnerUser;
66
- // 计算是否抑制中间输出(工具活动 + 流式文本)
67
- const shouldSuppress = () => {
68
- const mode = this.config.showActivities ?? 'all';
69
- if (mode === 'all')
70
- return false;
71
- if (mode === 'dm-only')
72
- return isGroup;
73
- if (mode === 'owner-dm-only')
74
- return isGroup || !isOwnerUser;
75
- if (mode === 'none')
76
- return true;
77
- return false;
78
- };
79
- this.shouldSuppressActivities = shouldSuppress();
80
- let monitor;
81
- let monitorInterval;
82
- let rejectFn;
83
- const resetTimer = (eventType, toolName) => {
84
- monitor?.recordEvent(eventType || 'unknown', toolName);
85
- };
86
- const timeoutPromise = new Promise((_, reject) => {
87
- rejectFn = reject;
88
- if (!monitorEnabled)
89
- return;
90
- monitor = new StreamIdleMonitor(idleMs);
91
- monitorInterval = setInterval(async () => {
92
- // Drain all pending levels in one tick
93
- let result = monitor.check();
94
- while (result) {
95
- if (result.action === 'kill') {
96
- logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
97
- // 先发送诊断信息,让用户知道发生了什么
98
- if (channelInfo) {
99
- try {
100
- const msg = quietMode
101
- ? `⚠️ 任务超时(${result.idleSec}秒无响应),已自动中断`
102
- : result.message;
103
- await channelInfo.adapter.sendText(message.channelId, msg);
104
- }
105
- catch (e) {
106
- logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
107
- }
108
- }
109
- try {
110
- await this.agentRunner.interrupt(streamKey);
111
- }
112
- catch (e) {
113
- logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
114
- }
115
- rejectFn(new Error('SDK_TIMEOUT'));
116
- return;
117
- }
118
- else {
119
- // notify or warn: send diagnostic message, task continues(非主人时静默)
120
- logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
121
- if (channelInfo && !quietMode && !shouldSuppress()) {
122
- try {
123
- await channelInfo.adapter.sendText(message.channelId, result.message);
124
- }
125
- catch (e) {
126
- logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
127
- }
128
- }
129
- }
130
- result = monitor.check();
131
- }
132
- }, 30000);
133
- });
134
- try {
135
- await Promise.race([
136
- this._processMessageInternal(message, resetTimer, isGroup, shouldSuppress),
137
- timeoutPromise
138
- ]);
139
- }
140
- catch (error) {
141
- // 超时错误:kill 级别已发送诊断信息,无需再发
142
- // 非超时错误走通用处理
143
- // 记录错误到健康状态(仅主人的错误累计触发安全模式)
144
- if (channelInfo) {
145
- try {
146
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
147
- const errorType = classifyError(error);
148
- // 上下文过长是可恢复错误,不累计触发安全模式
149
- if (errorType === ErrorType.CONTEXT_TOO_LONG) {
150
- logger.info(`[MessageProcessor] Context too long error, skipping safe mode accumulation`);
151
- }
152
- else if (quietMode) {
153
- // 群聊或非主人的错误只记日志,不累计
154
- logger.info(`[MessageProcessor] Non-owner/group error (user=${message.userId}, group=${isGroup}), skipping safe mode accumulation`);
155
- }
156
- else {
157
- const newCount = await this.sessionManager.recordError(session.id, errorType, error.message);
158
- await this.checkSafeMode(session, message.channelId, channelInfo.adapter, safeModeThreshold, newCount);
159
- }
160
- }
161
- catch (statusError) {
162
- logger.error('[MessageProcessor] Failed to update health status:', statusError);
163
- }
164
- }
165
- throw error;
166
- }
167
- finally {
168
- if (monitorInterval)
169
- clearInterval(monitorInterval);
170
- }
171
- }
172
- /** 从 session 提取话题回复选项 */
173
- getThreadSendOpts(session) {
174
- const rootId = session.metadata?.feishu?.rootId;
175
- return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
176
- }
177
- /**
178
- * 检查是否需要进入安全模式(safeModeThreshold 为 0 时跳过)
179
- * 仅单聊主人会话调用(群聊和非主人已在调用侧过滤)
180
- */
181
- async checkSafeMode(session, channelId, adapter, safeModeThreshold, consecutiveErrors) {
182
- if (safeModeThreshold <= 0)
183
- return;
184
- const health = await this.sessionManager.getHealthStatus(session.id);
185
- const sendOpts = this.getThreadSendOpts(session);
186
- const isThread = !!session.threadId;
187
- if (consecutiveErrors >= safeModeThreshold && !health.safeMode) {
188
- await this.sessionManager.setSafeMode(session.id, true);
189
- logger.warn(`[MessageProcessor] Session ${session.id} entered safe mode after ${consecutiveErrors} errors`);
190
- const suggestions = isThread
191
- ? `1. /repair - 检查并修复会话(推荐,保留历史)\n2. /clear - 清空会话历史\n3. /status - 查看详细状态`
192
- : `1. /repair - 检查并修复会话(推荐,保留历史)\n2. /new [名称] - 创建新会话(清空历史)\n3. /status - 查看详细状态`;
193
- await adapter.sendText(channelId, `⚠️ 安全模式已启用(连续 ${consecutiveErrors} 次异常)
194
-
195
- 当前限制:
196
- - 无法记住之前的对话
197
- - 每次提问需要提供完整上下文
198
-
199
- 建议操作:
200
- ${suggestions}`, sendOpts);
201
- }
202
- else if (safeModeThreshold >= 2 && consecutiveErrors === safeModeThreshold - 1) {
203
- await adapter.sendText(channelId, `⚠️ 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`, sendOpts);
204
- }
205
- }
206
- async _processMessageInternal(message, resetTimer, isGroup, shouldSuppress) {
207
- const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
208
- const channelInfo = this.channels.get(message.channel);
209
- if (!channelInfo) {
210
- logger.error(`[MessageProcessor] Unknown channel: ${message.channel}`);
211
- return;
212
- }
213
- const { adapter, options } = channelInfo;
214
- try {
215
- // 检查是否为命令
216
- if (this.commandHandler) {
217
- const cmdResult = await this.commandHandler(message.content, message.channel, message.channelId, message.userId, message.threadId);
218
- if (cmdResult) {
219
- // 话题消息:通过 rootId 回复到话题内
220
- const session = message.threadId ? await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId) : undefined;
221
- const rootId = session?.metadata?.feishu?.rootId;
222
- const sendOpts = rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
223
- await adapter.sendText(message.channelId, cmdResult, sendOpts);
224
- return;
225
- }
226
- }
227
- // 解析会话和项目路径
228
- const { session, absoluteProjectPath } = await this.resolveSession(message);
229
- const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
230
- // 记录收到消息
231
- logger.message({
232
- msgId: messageId,
233
- sessionId: session.id,
234
- dir: 'inbound',
235
- status: 'received'
236
- });
237
- const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
238
- const modeInfo = isBackground ? ' [后台]' : '';
239
- logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
240
- // 记录开始处理
241
- logger.message({
242
- msgId: messageId,
243
- sessionId: session.id,
244
- dir: 'inbound',
245
- status: 'processing'
246
- });
247
- const startTime = Date.now();
248
- // 创建 StreamFlusher,传入文件标记模式用于自动过滤
249
- // 使用动态判断,确保切换项目后不会继续输出
250
- let firstReply = true;
251
- const messageIsGroup = isGroup; // 捕获 isGroup 供闭包使用
252
- const flusher = new StreamFlusher(async (text, isFinal) => {
253
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
254
- if (!isCurrentlyBackground) {
255
- const opts = {};
256
- if (isFinal)
257
- opts.title = '最终回复:';
258
- // 话题会话:所有回复指向 rootId + reply_in_thread(确保消息进入话题)
259
- const rootId = session.metadata?.feishu?.rootId;
260
- if (rootId) {
261
- opts.replyToMessageId = rootId;
262
- opts.replyInThread = true;
263
- }
264
- else if (firstReply && message.messageId) {
265
- // 主会话:首条消息引用回复用户原消息
266
- opts.replyToMessageId = message.messageId;
267
- firstReply = false;
268
- }
269
- await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
270
- }
271
- // 后台任务:静默,不发送输出
272
- }, (this.config.flushDelay || 4) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag);
273
- // 保存当前 flusher,用于 compact 事件
274
- this.currentFlusher = flusher;
275
- // 调用 AgentRunner(含上下文过长自动 compact 重试)
276
- const streamKey = `${message.channel}-${message.channelId}`;
277
- try {
278
- const stream = await this.agentRunner.runQuery(session.id, message.content, absoluteProjectPath, session.agentSessionId, message.images, options?.systemPromptAppend, this.sessionManager);
279
- this.agentRunner.registerStream(streamKey, stream);
280
- await this.processEventStream(stream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress);
281
- }
282
- catch (error) {
283
- if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId) {
284
- // 尝试 compact 压缩会话
285
- flusher.addActivity('⚠️ 上下文过长,正在压缩会话...');
286
- await flusher.flush();
287
- const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, absoluteProjectPath);
288
- if (compacted) {
289
- // compact 成功,带 resume 重试
290
- flusher.addActivity('✅ 压缩完成,正在重试...');
291
- const retryStream = await this.agentRunner.runQuery(session.id, message.content, absoluteProjectPath, session.agentSessionId, message.images, options?.systemPromptAppend, this.sessionManager);
292
- this.agentRunner.registerStream(streamKey, retryStream);
293
- await this.processEventStream(retryStream, session, message.channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress);
294
- }
295
- else {
296
- throw new Error('CONTEXT_COMPACT_FAILED');
297
- }
298
- }
299
- else {
300
- throw error;
301
- }
302
- }
303
- // 处理文件标记(Feishu 专用)- 提取并发送文件
304
- if (options?.fileMarkerPattern && adapter.sendFile) {
305
- const fullText = flusher.getFinalText();
306
- const fileMatches = [...fullText.matchAll(options.fileMarkerPattern)];
307
- for (const match of fileMatches) {
308
- const filePath = match[1].trim();
309
- // 占位符/示例检测:静默跳过,不打扰用户
310
- if (this.isPlaceholderPath(filePath)) {
311
- logger.info(`[${adapter.name}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
312
- continue;
313
- }
314
- const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
315
- // 文件存在性检查:真实路径但文件不存在,告知用户
316
- if (!fs.existsSync(resolvedPath)) {
317
- logger.warn(`[${adapter.name}] File not found: ${resolvedPath}`);
318
- await adapter.sendText(message.channelId, `⚠️ 文件未找到: ${filePath}`, this.getThreadSendOpts(session));
319
- continue;
320
- }
321
- logger.info(`[${adapter.name}] Sending file: ${resolvedPath}`);
322
- try {
323
- await adapter.sendFile(message.channelId, resolvedPath);
324
- }
325
- catch (error) {
326
- logger.error(`[${adapter.name}] Failed to send file: ${resolvedPath}`, error);
327
- await adapter.sendText(message.channelId, `❌ 文件发送失败: ${filePath}`, this.getThreadSendOpts(session));
328
- }
329
- }
330
- }
331
- // Flush 剩余内容(文件标记已在 flush 时自动移除)
332
- await flusher.flush(true);
333
- // 安全模式尾部提示:如果当前会话处于安全模式,追加提醒
334
- const healthStatus = await this.sessionManager.getHealthStatus(session.id);
335
- if (healthStatus.safeMode) {
336
- const hint = session.threadId
337
- ? '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /clear 清空会话'
338
- : '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /new 新建会话';
339
- await adapter.sendText(message.channelId, hint, this.getThreadSendOpts(session));
340
- }
341
- // 清理 activeStreams(正常完成)
342
- this.agentRunner.cleanupStream(streamKey);
343
- // 记录成功响应(重置错误计数)
344
- await this.sessionManager.recordSuccess(session.id);
345
- const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
346
- if (isFinallyBackground) {
347
- const projectName = path.basename(session.projectPath);
348
- const count = this.messageCache.getCount(session.id);
349
- await adapter.sendText(message.channelId, `[后台-${projectName}] ✓ 任务完成 (${count}条消息已缓存)`);
350
- }
351
- const duration = Date.now() - startTime;
352
- // 记录处理完成
353
- logger.message({
354
- msgId: messageId,
355
- sessionId: session.id,
356
- dir: 'inbound',
357
- status: 'completed',
358
- duration
359
- });
360
- // 记录发送响应
361
- logger.message({
362
- msgId: `${messageId}_reply`,
363
- sessionId: session.id,
364
- dir: 'outbound',
365
- status: 'sent'
366
- });
367
- }
368
- catch (error) {
369
- logger.error(`[${message.channel}] Error:`, error);
370
- // 记录处理失败
371
- logger.message({
372
- msgId: messageId,
373
- sessionId: message.channelId,
374
- dir: 'inbound',
375
- status: 'failed',
376
- error: error instanceof Error ? error.message : String(error)
377
- });
378
- if (error instanceof Error) {
379
- logger.error(`[${message.channel}] Error stack:`, error.stack);
380
- }
381
- // 发送用户友好的错误消息(SDK_TIMEOUT 已在 kill 级别发过提示,跳过)
382
- if (error instanceof Error && error.message === 'SDK_TIMEOUT') {
383
- logger.info(`[MessageProcessor] SDK_TIMEOUT error, skip sending duplicate message`);
384
- }
385
- else {
386
- const userMessage = getErrorMessage(error);
387
- // 获取 session 用于话题回复(如果 resolveSession 已执行)
388
- let sendOpts;
389
- try {
390
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
391
- sendOpts = this.getThreadSendOpts(session);
392
- }
393
- catch { }
394
- await adapter.sendText(message.channelId, userMessage, sendOpts);
395
- }
396
- }
397
- }
398
- /**
399
- * 解析会话和项目路径
400
- */
401
- async resolveSession(message) {
402
- // 话题会话:传入 rootId metadata(首条消息的 messageId 作为 rootId)
403
- const metadata = message.threadId && message.messageId
404
- ? { feishu: { rootId: message.messageId } }
405
- : undefined;
406
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata);
407
- const absoluteProjectPath = path.isAbsolute(session.projectPath)
408
- ? session.projectPath
409
- : path.resolve(process.cwd(), session.projectPath);
410
- return { session, absoluteProjectPath };
411
- }
412
- /**
413
- * 处理事件流
414
- */
415
- async processEventStream(stream, session, channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress) {
416
- let hasTextDelta = false;
417
- let hasReceivedText = false;
418
- let lastSessionId;
419
- try {
420
- for await (const event of stream) {
421
- // 每收到事件重置空闲超时,传入事件类型和工具名
422
- const toolName = event.type === 'assistant'
423
- ? event.message?.content?.find((c) => c.type === 'tool_use')?.name
424
- : undefined;
425
- resetTimer(event.type, toolName);
426
- // 记录所有事件类型(INFO级别,便于诊断)
427
- logger.info(`[MessageProcessor] Event: type=${event.type}, subtype=${event.subtype || 'none'}`);
428
- // 提取 session_id(只在首次或变化时更新)
429
- if (event.session_id && event.session_id !== lastSessionId) {
430
- logger.info(`[MessageProcessor] Extracted session_id: ${event.session_id} for session: ${session.id}`);
431
- this.agentRunner.updateSessionId(session.id, event.session_id);
432
- lastSessionId = event.session_id;
433
- }
434
- const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
435
- // === 前台任务:正常处理所有事件 ===
436
- if (!isCurrentlyBackground) {
437
- // 流式文本事件(抑制时跳过,只累积到 allText)
438
- if (event.type === 'text_delta' && event.text) {
439
- hasTextDelta = true;
440
- hasReceivedText = true;
441
- if (!shouldSuppress()) {
442
- flusher.addText(event.text);
443
- }
444
- }
445
- // 系统事件:compact_boundary(群聊时静默)
446
- if (event.type === 'system' && event.subtype === 'compact_boundary') {
447
- if (!this.currentIsGroup && !shouldSuppress()) {
448
- const preTokens = event.compact_metadata?.pre_tokens || 0;
449
- flusher.addActivity(`💡 会话压缩完成,继续执行...(压缩前 tokens: ${preTokens})`);
450
- }
451
- }
452
- // 系统事件:task_progress(子任务进度)
453
- if (event.type === 'system' && event.subtype === 'task_progress') {
454
- const tools = event.tool_uses ?? 0;
455
- const duration = event.duration_ms ? `${Math.round(event.duration_ms / 1000)}s` : '';
456
- const summary = event.summary;
457
- const stats = [tools > 0 ? `${tools}次工具调用` : '', duration].filter(Boolean).join(', ');
458
- if (summary && !shouldSuppress()) {
459
- flusher.addActivity(`⏳ 子任务: ${summary}${stats ? ` (${stats})` : ''}`);
460
- }
461
- else if (stats && !shouldSuppress()) {
462
- flusher.addActivity(`⏳ 子任务进行中: ${stats}`);
463
- }
464
- }
465
- // Assistant 事件:提取工具调用和文本内容
466
- if (event.type === 'assistant' && event.message?.content) {
467
- for (const content of event.message.content) {
468
- if (content.type === 'tool_use') {
469
- if (!shouldSuppress()) {
470
- const desc = this.formatToolDescription(content);
471
- flusher.addActivity(`🔧 ${content.name}${desc ? ': ' + desc : ''}`);
472
- }
473
- }
474
- else if (content.type === 'text' && content.text && !hasTextDelta) {
475
- // 仅在没有 text_delta 事件时从 assistant 事件提取文本,避免重复
476
- hasReceivedText = true;
477
- if (!shouldSuppress()) {
478
- flusher.addTextBlock(content.text);
479
- }
480
- }
481
- }
482
- }
483
- // 工具结果事件:显示失败信息(包括权限拒绝、执行失败等所有场景)
484
- if (event.type === 'tool_result') {
485
- logger.debug(`[MessageProcessor] tool_result: is_error=${event.is_error}, error=${event.error}, content=${typeof event.content}`);
486
- if (event.is_error && !shouldSuppress()) {
487
- const toolName = event.tool_name || '工具';
488
- const errorMsg = event.error || (typeof event.content === 'string' ? event.content : JSON.stringify(event.content)) || '执行失败';
489
- flusher.addActivity(`⚠️ ${toolName}: ${errorMsg}`);
490
- }
491
- }
492
- // Result 事件:最终输出
493
- if (event.type === 'result' && event.result) {
494
- logger.debug(`[MessageProcessor] result event: hasReceivedText=${hasReceivedText}, shouldSuppress=${shouldSuppress()}, result="${event.result}"`);
495
- if (shouldSuppress()) {
496
- // 抑制模式:直接发送 result(跳过中间输出)
497
- flusher.addText(event.result);
498
- }
499
- else if (!hasReceivedText) {
500
- // 非抑制模式 + 无流式文本:使用 result 作为兜底
501
- flusher.addText(event.result);
502
- }
503
- // 非抑制模式 + 有流式文本:已通过 text_delta 累积,无需再添加
504
- await flusher.flush(true); // isFinal=true 标记最终输出
505
- }
506
- continue;
507
- }
508
- // === 后台任务:只处理 result 事件,仅缓存不发送 ===
509
- if (event.type !== 'result') {
510
- continue;
511
- }
512
- if (event.subtype === 'success') {
513
- this.messageCache.addEvent(session.id, {
514
- type: 'completed',
515
- message: event.result,
516
- timestamp: Date.now(),
517
- metadata: {
518
- duration: event.duration_ms,
519
- cost: event.total_cost_usd
520
- }
521
- });
522
- }
523
- else if (event.is_error === true) {
524
- this.messageCache.addEvent(session.id, {
525
- type: 'error',
526
- message: event.errors?.join('\n') || '未知错误',
527
- timestamp: Date.now(),
528
- metadata: {
529
- errorType: event.subtype
530
- }
531
- });
532
- }
533
- }
534
- }
535
- catch (error) {
536
- // 捕获 SDK 进程崩溃或流迭代错误
537
- logger.error('[MessageProcessor] Stream processing error:', error);
538
- if (error instanceof Error && error.message.includes('process exited')) {
539
- flusher.addActivity('❌ Claude Code 进程异常退出,请重试');
540
- }
541
- throw error; // 重新抛出,让外层处理
542
- }
543
- }
544
- /**
545
- * 格式化工具描述(通用)
546
- */
547
- formatToolDescription(toolUse) {
548
- const input = toolUse.input || {};
549
- return (input.description ||
550
- input.file_path ||
551
- input.pattern ||
552
- (typeof input.command === 'string' ? input.command.substring(0, 80) : undefined) ||
553
- (typeof input.prompt === 'string' ? input.prompt.substring(0, 80) : undefined) ||
554
- (typeof input.query === 'string' ? input.query.substring(0, 80) : undefined) ||
555
- '');
556
- }
557
- /**
558
- * 解析文件路径,支持相对路径和绝对路径
559
- * 优先在项目根目录查找,兜底尝试 .openclaw/workspace/
560
- */
561
- resolveFilePath(filePath, projectPath) {
562
- if (path.isAbsolute(filePath)) {
563
- return filePath;
564
- }
565
- // 优先在项目根目录查找
566
- const rootPath = path.join(projectPath, filePath);
567
- if (fs.existsSync(rootPath)) {
568
- return rootPath;
569
- }
570
- // 兜底:尝试 .openclaw/workspace/
571
- const workspacePath = path.join(projectPath, '.openclaw', 'workspace', filePath);
572
- if (fs.existsSync(workspacePath)) {
573
- return workspacePath;
574
- }
575
- // 都找不到,返回项目根目录路径
576
- return rootPath;
577
- }
578
- /**
579
- * 判断文件路径是否为占位符/示例文本
580
- * 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
581
- */
582
- isPlaceholderPath(filePath) {
583
- if (!filePath)
584
- return true;
585
- // 精确占位符
586
- const exactPlaceholders = ['...', '…', 'path', 'file', 'file_path', 'filepath',
587
- '路径', '文件路径', '文件', 'filename', 'xxx'];
588
- if (exactPlaceholders.includes(filePath.toLowerCase()))
589
- return true;
590
- // 示例路径前缀
591
- if (/^(\/path\/to\/|\.\/path\/to\/|example\/|示例|\/example)/i.test(filePath))
592
- return true;
593
- // 含模板变量
594
- if (/\$\{.+\}|\{\{.+\}\}|<.+>/.test(filePath))
595
- return true;
596
- // 纯标点/特殊字符(非路径字符)
597
- if (/^[.\s…]+$/.test(filePath))
598
- return true;
599
- // 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
600
- if (/[\\[\]{}*+?|^$]/.test(filePath))
601
- return true;
602
- return false;
603
- }
604
- }