evolclaw 2.1.1 → 2.2.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 (43) hide show
  1. package/README.md +10 -3
  2. package/data/evolclaw.sample.json +9 -1
  3. package/dist/agents/claude-runner.js +612 -0
  4. package/dist/agents/codex-runner.js +310 -0
  5. package/dist/channels/aun.js +416 -9
  6. package/dist/channels/feishu.js +397 -104
  7. package/dist/channels/wechat.js +84 -2
  8. package/dist/cli.js +427 -126
  9. package/dist/config.js +102 -4
  10. package/dist/core/adapters/claude-session-file-adapter.js +144 -0
  11. package/dist/core/adapters/codex-session-file-adapter.js +196 -0
  12. package/dist/core/agent-loader.js +39 -0
  13. package/dist/core/channel-loader.js +60 -0
  14. package/dist/core/command-handler.js +908 -304
  15. package/dist/core/event-bus.js +32 -0
  16. package/dist/core/ipc-server.js +71 -0
  17. package/dist/core/message-bridge.js +187 -0
  18. package/dist/core/message-processor.js +370 -227
  19. package/dist/core/message-queue.js +153 -29
  20. package/dist/core/permission.js +58 -0
  21. package/dist/core/session-file-adapter.js +7 -0
  22. package/dist/core/session-manager.js +571 -223
  23. package/dist/core/stats-collector.js +86 -0
  24. package/dist/index.js +309 -243
  25. package/dist/paths.js +1 -0
  26. package/dist/utils/error-utils.js +4 -2
  27. package/dist/utils/init-feishu.js +2 -0
  28. package/dist/utils/init-wechat.js +2 -0
  29. package/dist/utils/init.js +285 -53
  30. package/dist/utils/ipc-client.js +36 -0
  31. package/dist/utils/migrate-project.js +122 -0
  32. package/dist/utils/{permission.js → permission-utils.js} +31 -3
  33. package/dist/utils/rich-content-renderer.js +228 -0
  34. package/dist/utils/session-file-health.js +11 -34
  35. package/dist/utils/stream-debouncer.js +122 -0
  36. package/dist/utils/stream-idle-monitor.js +1 -1
  37. package/package.json +3 -1
  38. package/dist/core/agent-runner.js +0 -348
  39. package/dist/core/message-stream.js +0 -59
  40. package/dist/index.js.bak +0 -340
  41. package/dist/utils/markdown-to-feishu.js +0 -94
  42. /package/dist/utils/{platform.js → cross-platform.js} +0 -0
  43. /package/dist/{core → utils}/message-cache.js +0 -0
@@ -1,80 +1,120 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
+ import { hasCompact } from '../agents/claude-runner.js';
3
4
  import { StreamFlusher } from '../utils/stream-flusher.js';
4
5
  import { StreamIdleMonitor } from '../utils/stream-idle-monitor.js';
5
6
  import { logger } from '../utils/logger.js';
6
7
  import { getErrorMessage, classifyError, ErrorType } from '../utils/error-utils.js';
7
- import { isOwner } from '../config.js';
8
+ import { summarizeToolInput } from '../utils/permission-utils.js';
9
+ import { getOwner } from '../config.js';
8
10
  /**
9
11
  * 统一消息处理器
10
12
  * 负责处理来自不同渠道的消息,协调事件流处理
11
13
  */
12
14
  export class MessageProcessor {
13
- agentRunner;
14
15
  sessionManager;
15
16
  config;
16
17
  messageCache;
18
+ eventBus;
17
19
  commandHandler;
18
20
  channels = new Map();
19
21
  currentFlusher;
20
- currentIsGroup = false;
21
22
  shouldSuppressActivities = false;
23
+ agentMap;
24
+ defaultAgentId;
25
+ interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
26
+ /** 按 agentId 获取 agent,回退到默认 */
27
+ getAgent(agentId) {
28
+ if (agentId && this.agentMap.has(agentId))
29
+ return this.agentMap.get(agentId);
30
+ return this.agentMap.get(this.defaultAgentId) || this.agentMap.values().next().value;
31
+ }
32
+ /** 获取可用 agent 列表 */
33
+ getAvailableAgents() {
34
+ return [...this.agentMap.keys()];
35
+ }
22
36
  /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
23
37
  async isBackgroundSession(session, channel, channelId) {
24
- // 话题会话独立运行,不是后台任务
25
38
  if (session.threadId)
26
39
  return false;
27
- // 主会话:与当前活跃会话比对
28
40
  const active = await this.sessionManager.getActiveSession(channel, channelId);
29
41
  return active ? session.id !== active.id : false;
30
42
  }
31
- constructor(agentRunner, sessionManager, config, messageCache, commandHandler) {
32
- this.agentRunner = agentRunner;
43
+ constructor(agentRunnerOrMap, sessionManager, config, messageCache, eventBus, commandHandler, defaultAgentId) {
33
44
  this.sessionManager = sessionManager;
34
45
  this.config = config;
35
46
  this.messageCache = messageCache;
47
+ this.eventBus = eventBus;
36
48
  this.commandHandler = commandHandler;
49
+ if (agentRunnerOrMap instanceof Map) {
50
+ this.agentMap = agentRunnerOrMap;
51
+ this.defaultAgentId = defaultAgentId || 'claude';
52
+ }
53
+ else {
54
+ // 向后兼容:单个 agentRunner
55
+ this.agentMap = new Map([[agentRunnerOrMap.name, agentRunnerOrMap]]);
56
+ this.defaultAgentId = agentRunnerOrMap.name;
57
+ }
58
+ // 监听中断事件,标记被中断的 session
59
+ this.eventBus.subscribe('message:interrupted', (event) => {
60
+ if ('sessionId' in event && event.sessionId) {
61
+ this.interruptedSessions.set(event.sessionId, event.reason || 'unknown');
62
+ }
63
+ });
37
64
  }
38
65
  /**
39
66
  * 注册渠道适配器
40
67
  */
41
- registerChannel(adapter, options) {
42
- this.channels.set(adapter.name, { adapter, options });
68
+ registerChannel(adapter, policy, options) {
69
+ this.channels.set(adapter.name, { adapter, options, policy });
70
+ }
71
+ /**
72
+ * 获取渠道适配器
73
+ */
74
+ getAdapter(channelName) {
75
+ return this.channels.get(channelName)?.adapter;
76
+ }
77
+ /**
78
+ * 获取渠道信息(含 policy)
79
+ */
80
+ getChannelInfo(channelName) {
81
+ return this.channels.get(channelName);
43
82
  }
44
83
  /**
45
84
  * 处理 compact 开始事件
46
85
  */
47
- handleCompactStart() {
48
- if (this.currentFlusher && !this.currentIsGroup && !this.shouldSuppressActivities) {
49
- this.currentFlusher.addActivity(' 会话压缩中...');
86
+ handleCompactStart(sessionId) {
87
+ if (sessionId) {
88
+ this.eventBus.publish({ type: 'agent:compact-start', sessionId });
89
+ }
90
+ if (this.currentFlusher && !this.shouldSuppressActivities) {
91
+ this.currentFlusher.addActivity('\u23f3 会话压缩中...');
50
92
  }
51
93
  }
52
94
  /**
53
95
  * 处理消息(主入口)
54
96
  */
55
97
  async processMessage(message) {
56
- const isGroup = message.isGroup ?? false;
57
- this.currentIsGroup = isGroup;
58
98
  const idleMs = (this.config.idleMonitor?.timeout ?? 120) * 1000;
59
- const streamKey = `${message.channel}-${message.channelId}`;
60
99
  const channelInfo = this.channels.get(message.channel);
100
+ if (!channelInfo) {
101
+ logger.error(`[MessageProcessor] Unknown channel: ${message.channel}`);
102
+ return;
103
+ }
104
+ const { policy } = channelInfo;
105
+ // 解析会话(唯一的 getOrCreateSession 调用点)
106
+ const { session, absoluteProjectPath } = await this.resolveSession(message);
107
+ const streamKey = session.id;
108
+ const chatType = message.chatType || 'private';
109
+ const identityRole = session.identity?.role || 'anonymous';
110
+ // 按 session.agentId 选择 agent 后端
111
+ const agent = this.getAgent(session.agentId);
61
112
  const monitorEnabled = this.config.idleMonitor?.enabled !== false;
62
113
  const safeModeThreshold = this.config.idleMonitor?.safeModeThreshold ?? 3;
63
- const isOwnerUser = isOwner(this.config, message.channel, message.userId || '');
64
- // 非主人(群聊或单聊):空闲监控静默/简短
65
- const quietMode = isGroup || !isOwnerUser;
114
+ const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
66
115
  // 计算是否抑制中间输出(工具活动 + 流式文本)
67
116
  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;
117
+ return !policy.showMiddleResult(chatType, identityRole);
78
118
  };
79
119
  this.shouldSuppressActivities = shouldSuppress();
80
120
  let monitor;
@@ -83,47 +123,43 @@ export class MessageProcessor {
83
123
  const resetTimer = (eventType, toolName) => {
84
124
  monitor?.recordEvent(eventType || 'unknown', toolName);
85
125
  };
126
+ // Cache background status to avoid async call inside setInterval
127
+ const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
86
128
  const timeoutPromise = new Promise((_, reject) => {
87
129
  rejectFn = reject;
88
130
  if (!monitorEnabled)
89
131
  return;
90
132
  monitor = new StreamIdleMonitor(idleMs);
91
- monitorInterval = setInterval(async () => {
133
+ monitorInterval = setInterval(() => {
92
134
  // Drain all pending levels in one tick
93
135
  let result = monitor.check();
94
136
  while (result) {
95
137
  if (result.action === 'kill') {
96
138
  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) {
139
+ this.eventBus.publish({ type: 'agent:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
140
+ // 后台任务也需要中断(释放资源),但不发送通知
141
+ if (channelInfo && !isBackground) {
142
+ const msg = showIdleMonitor
143
+ ? result.message
144
+ : `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
145
+ channelInfo.adapter.sendText(message.channelId, msg).catch(e => {
106
146
  logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
107
- }
147
+ });
108
148
  }
109
- try {
110
- await this.agentRunner.interrupt(streamKey);
111
- }
112
- catch (e) {
149
+ agent.interrupt(streamKey).catch(e => {
113
150
  logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
114
- }
151
+ });
115
152
  rejectFn(new Error('SDK_TIMEOUT'));
116
153
  return;
117
154
  }
118
155
  else {
119
- // notify or warn: send diagnostic message, task continues(非主人时静默)
156
+ // notify or warn: send diagnostic message, task continues
120
157
  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);
158
+ if (channelInfo && showIdleMonitor && !shouldSuppress()) {
159
+ if (!isBackground) {
160
+ channelInfo.adapter.sendText(message.channelId, result.message).catch(e => {
161
+ logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
162
+ });
127
163
  }
128
164
  }
129
165
  }
@@ -133,25 +169,23 @@ export class MessageProcessor {
133
169
  });
134
170
  try {
135
171
  await Promise.race([
136
- this._processMessageInternal(message, resetTimer, isGroup, shouldSuppress),
172
+ this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress),
137
173
  timeoutPromise
138
174
  ]);
139
175
  }
140
176
  catch (error) {
141
177
  // 超时错误:kill 级别已发送诊断信息,无需再发
142
178
  // 非超时错误走通用处理
143
- // 记录错误到健康状态(仅主人的错误累计触发安全模式)
179
+ // 记录错误到健康状态(复用已有 session)
144
180
  if (channelInfo) {
145
181
  try {
146
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
147
182
  const errorType = classifyError(error);
148
183
  // 上下文过长是可恢复错误,不累计触发安全模式
149
184
  if (errorType === ErrorType.CONTEXT_TOO_LONG) {
150
185
  logger.info(`[MessageProcessor] Context too long error, skipping safe mode accumulation`);
151
186
  }
152
- else if (quietMode) {
153
- // 群聊或非主人的错误只记日志,不累计
154
- logger.info(`[MessageProcessor] Non-owner/group error (user=${message.userId}, group=${isGroup}), skipping safe mode accumulation`);
187
+ else if (!policy.accumulateErrors(chatType, identityRole)) {
188
+ logger.info(`[MessageProcessor] Non-accumulating error (chatType=${chatType}, identity=${identityRole}), skipping safe mode accumulation`);
155
189
  }
156
190
  else {
157
191
  const newCount = await this.sessionManager.recordError(session.id, errorType, error.message);
@@ -169,28 +203,27 @@ export class MessageProcessor {
169
203
  clearInterval(monitorInterval);
170
204
  }
171
205
  }
172
- /** 从 session 提取话题回复选项 */
173
- getThreadSendOpts(session) {
174
- const rootId = session.metadata?.feishu?.rootId;
175
- return rootId ? { replyToMessageId: rootId, replyInThread: true } : undefined;
206
+ /** 从 session 提取渠道预构建的回复上下文 */
207
+ getReplyContext(session) {
208
+ return session.metadata?.replyContext;
176
209
  }
177
210
  /**
178
211
  * 检查是否需要进入安全模式(safeModeThreshold 为 0 时跳过)
179
- * 仅单聊主人会话调用(群聊和非主人已在调用侧过滤)
180
212
  */
181
213
  async checkSafeMode(session, channelId, adapter, safeModeThreshold, consecutiveErrors) {
182
214
  if (safeModeThreshold <= 0)
183
215
  return;
184
216
  const health = await this.sessionManager.getHealthStatus(session.id);
185
- const sendOpts = this.getThreadSendOpts(session);
217
+ const sendOpts = this.getReplyContext(session);
186
218
  const isThread = !!session.threadId;
187
219
  if (consecutiveErrors >= safeModeThreshold && !health.safeMode) {
188
220
  await this.sessionManager.setSafeMode(session.id, true);
189
221
  logger.warn(`[MessageProcessor] Session ${session.id} entered safe mode after ${consecutiveErrors} errors`);
222
+ this.eventBus.publish({ type: 'session:safe-mode-entered', sessionId: session.id, consecutiveErrors });
190
223
  const suggestions = isThread
191
224
  ? `1. /repair - 检查并修复会话(推荐,保留历史)\n2. /clear - 清空会话历史\n3. /status - 查看详细状态`
192
225
  : `1. /repair - 检查并修复会话(推荐,保留历史)\n2. /new [名称] - 创建新会话(清空历史)\n3. /status - 查看详细状态`;
193
- await adapter.sendText(channelId, `⚠️ 安全模式已启用(连续 ${consecutiveErrors} 次异常)
226
+ await adapter.sendText(channelId, `\u26a0\ufe0f 安全模式已启用(连续 ${consecutiveErrors} 次异常)
194
227
 
195
228
  当前限制:
196
229
  - 无法记住之前的对话
@@ -200,10 +233,10 @@ export class MessageProcessor {
200
233
  ${suggestions}`, sendOpts);
201
234
  }
202
235
  else if (safeModeThreshold >= 2 && consecutiveErrors === safeModeThreshold - 1) {
203
- await adapter.sendText(channelId, `⚠️ 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`, sendOpts);
236
+ await adapter.sendText(channelId, `\u26a0\ufe0f 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`, sendOpts);
204
237
  }
205
238
  }
206
- async _processMessageInternal(message, resetTimer, isGroup, shouldSuppress) {
239
+ async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
207
240
  const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
208
241
  const channelInfo = this.channels.get(message.channel);
209
242
  if (!channelInfo) {
@@ -211,21 +244,9 @@ ${suggestions}`, sendOpts);
211
244
  return;
212
245
  }
213
246
  const { adapter, options } = channelInfo;
247
+ const agent = this.getAgent(session.agentId);
248
+ const streamKey = session.id;
214
249
  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
250
  const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
230
251
  // 记录收到消息
231
252
  logger.message({
@@ -234,10 +255,20 @@ ${suggestions}`, sendOpts);
234
255
  dir: 'inbound',
235
256
  status: 'received'
236
257
  });
258
+ this.eventBus.publish({
259
+ type: 'message:received',
260
+ sessionId: session.id,
261
+ channel: message.channel,
262
+ channelId: message.channelId,
263
+ content: message.content,
264
+ timestamp: Date.now()
265
+ });
237
266
  const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
238
- const modeInfo = isBackground ? ' [后台]' : '';
267
+ const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
239
268
  logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
240
269
  // 记录开始处理
270
+ this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
271
+ adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(session));
241
272
  logger.message({
242
273
  msgId: messageId,
243
274
  sessionId: session.id,
@@ -248,18 +279,16 @@ ${suggestions}`, sendOpts);
248
279
  // 创建 StreamFlusher,传入文件标记模式用于自动过滤
249
280
  // 使用动态判断,确保切换项目后不会继续输出
250
281
  let firstReply = true;
251
- const messageIsGroup = isGroup; // 捕获 isGroup 供闭包使用
252
282
  const flusher = new StreamFlusher(async (text, isFinal) => {
253
283
  const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
254
284
  if (!isCurrentlyBackground) {
255
285
  const opts = {};
256
286
  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;
287
+ opts.title = '\u6700\u7ec8\u56de\u590d:';
288
+ // 话题会话:使用 Channel 预构建的 replyContext(确保消息进入话题)
289
+ const replyCtx = session.metadata?.replyContext;
290
+ if (replyCtx) {
291
+ Object.assign(opts, replyCtx);
263
292
  }
264
293
  else if (firstReply && message.messageId) {
265
294
  // 主会话:首条消息引用回复用户原消息
@@ -269,28 +298,73 @@ ${suggestions}`, sendOpts);
269
298
  await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
270
299
  }
271
300
  // 后台任务:静默,不发送输出
272
- }, (this.config.flushDelay || 4) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag);
301
+ }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag);
273
302
  // 保存当前 flusher,用于 compact 事件
274
303
  this.currentFlusher = flusher;
275
304
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
276
- const streamKey = `${message.channel}-${message.channelId}`;
305
+ // 设置权限审批的消息发送回调(指向当前渠道)
306
+ agent.setSendPrompt(async (text) => {
307
+ await adapter.sendText(message.channelId, text, this.getReplyContext(session));
308
+ });
309
+ // 设置 per-session 权限模式
310
+ const permissionMode = session.metadata?.permissionMode || 'default';
311
+ agent.setMode(permissionMode);
312
+ // 标记会话为处理中(实时持久化,重启后可恢复)
313
+ this.sessionManager.markProcessing(session.id);
314
+ // 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
315
+ const prevInterruptReason = this.interruptedSessions.get(session.id);
316
+ this.interruptedSessions.delete(session.id);
317
+ const effectivePrompt = prevInterruptReason === 'new_message' && session.agentSessionId
318
+ ? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
319
+ : message.content;
277
320
  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);
321
+ // 动态构建运行时上下文提示
322
+ const contextParts = [];
323
+ // 1. 当前环境信息
324
+ const peerLabel = session.identity?.role || 'unknown';
325
+ const sessionName = session.name || '默认会话';
326
+ const peerName = message.peerName || session.metadata?.peerName;
327
+ const envParts = [
328
+ `会话通道: ${message.channel}`,
329
+ `当前项目: ${path.basename(absoluteProjectPath)}`,
330
+ ];
331
+ if (session.name)
332
+ envParts.push(`会话名称: ${session.name}`);
333
+ envParts.push(`对端身份: ${peerLabel}`);
334
+ if (peerName)
335
+ envParts.push(`对端名称: ${peerName}`);
336
+ contextParts.push(`[当前环境] ${envParts.join(' | ')}`);
337
+ // 2. 文件发送能力
338
+ const fileChannels = [...this.channels.entries()]
339
+ .filter(([, info]) => info.adapter.sendFile)
340
+ .map(([name]) => name);
341
+ const currentCanSend = fileChannels.includes(message.channel);
342
+ const crossChannels = fileChannels.filter(n => n !== message.channel);
343
+ if (currentCanSend || crossChannels.length > 0) {
344
+ const hints = [];
345
+ if (currentCanSend)
346
+ hints.push(`[SEND_FILE:路径] 发送文件到当前通道`);
347
+ if (crossChannels.length > 0)
348
+ hints.push(`[SEND_FILE:${crossChannels[0]}:路径] 发送文件到指定通道(可用: ${crossChannels.join('/')})`);
349
+ contextParts.push(hints.join(','));
350
+ }
351
+ const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
352
+ const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
353
+ agent.registerStream(streamKey, stream);
354
+ await this.processEventStream(stream, session, flusher, resetTimer, shouldSuppress);
281
355
  }
282
356
  catch (error) {
283
- if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId) {
357
+ if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
284
358
  // 尝试 compact 压缩会话
285
- flusher.addActivity('⚠️ 上下文过长,正在压缩会话...');
359
+ flusher.addActivity('\u26a0\ufe0f 上下文过长,正在压缩会话...');
286
360
  await flusher.flush();
287
- const compacted = await this.agentRunner.compactSession(session.id, session.agentSessionId, absoluteProjectPath);
361
+ const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
288
362
  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);
363
+ // compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
364
+ flusher.addActivity('\u2705 压缩完成,正在重试...');
365
+ const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, options?.systemPromptAppend, this.sessionManager);
366
+ agent.registerStream(streamKey, retryStream);
367
+ await this.processEventStream(retryStream, session, flusher, resetTimer, shouldSuppress);
294
368
  }
295
369
  else {
296
370
  throw new Error('CONTEXT_COMPACT_FAILED');
@@ -300,33 +374,63 @@ ${suggestions}`, sendOpts);
300
374
  throw error;
301
375
  }
302
376
  }
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));
377
+ // 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
378
+ const FILE_MARKER_RE = /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
379
+ const markerPattern = options?.fileMarkerPattern ?? FILE_MARKER_RE;
380
+ const fullText = flusher.getFinalText();
381
+ const fileMatches = [...fullText.matchAll(markerPattern)];
382
+ for (const match of fileMatches) {
383
+ // 兼容旧格式 (1组) 和新格式 (2组)
384
+ const hasChannelGroup = match.length >= 3;
385
+ const targetChannelName = hasChannelGroup ? (match[1] ?? message.channel) : message.channel;
386
+ const filePath = (hasChannelGroup ? match[2] : match[1]).trim();
387
+ if (this.isPlaceholderPath(filePath)) {
388
+ logger.info(`[${adapter.name}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
389
+ continue;
390
+ }
391
+ // 跨通道仅限 owner
392
+ if (targetChannelName !== message.channel && session.identity?.role !== 'owner') {
393
+ await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, this.getReplyContext(session));
394
+ continue;
395
+ }
396
+ const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
397
+ if (!fs.existsSync(resolvedPath)) {
398
+ logger.warn(`[${adapter.name}] File not found: ${resolvedPath}`);
399
+ await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, this.getReplyContext(session));
400
+ continue;
401
+ }
402
+ // 找目标 adapter
403
+ const targetInfo = this.channels.get(targetChannelName);
404
+ if (!targetInfo) {
405
+ await adapter.sendText(message.channelId, `\u274c 通道 ${targetChannelName} 未启用或不存在`, this.getReplyContext(session));
406
+ continue;
407
+ }
408
+ if (!targetInfo.adapter.sendFile) {
409
+ await adapter.sendText(message.channelId, `\u274c 通道 ${targetChannelName} 不支持文件发送`, this.getReplyContext(session));
410
+ continue;
411
+ }
412
+ // 找目标 channelId
413
+ let targetChannelId = message.channelId;
414
+ if (targetChannelName !== message.channel) {
415
+ const ownerPeerId = getOwner(this.config, targetChannelName);
416
+ targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelName, ownerPeerId) ?? '') : '';
417
+ if (!targetChannelId) {
418
+ await adapter.sendText(message.channelId, `\u274c 未找到 ${targetChannelName} 的私聊会话,请先在该通道发送一条消息`, this.getReplyContext(session));
319
419
  continue;
320
420
  }
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));
421
+ }
422
+ logger.info(`[${adapter.name}] Sending file via ${targetChannelName}: ${resolvedPath}`);
423
+ try {
424
+ await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, this.getReplyContext(session));
425
+ this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetChannelName });
426
+ if (targetChannelName !== message.channel) {
427
+ await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetChannelName} 发送`, this.getReplyContext(session));
328
428
  }
329
429
  }
430
+ catch (error) {
431
+ logger.error(`[${adapter.name}] Failed to send file: ${resolvedPath}`, error);
432
+ await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, this.getReplyContext(session));
433
+ }
330
434
  }
331
435
  // Flush 剩余内容(文件标记已在 flush 时自动移除)
332
436
  await flusher.flush(true);
@@ -334,19 +438,31 @@ ${suggestions}`, sendOpts);
334
438
  const healthStatus = await this.sessionManager.getHealthStatus(session.id);
335
439
  if (healthStatus.safeMode) {
336
440
  const hint = session.threadId
337
- ? '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /clear 清空会话'
338
- : '\n\n⚠️ 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /new 新建会话';
339
- await adapter.sendText(message.channelId, hint, this.getThreadSendOpts(session));
441
+ ? '\n\n\u26a0\ufe0f 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /clear 清空会话'
442
+ : '\n\n\u26a0\ufe0f 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /new 新建会话';
443
+ await adapter.sendText(message.channelId, hint, this.getReplyContext(session));
340
444
  }
341
445
  // 清理 activeStreams(正常完成)
342
- this.agentRunner.cleanupStream(streamKey);
343
- // 记录成功响应(重置错误计数)
446
+ agent.cleanupStream(streamKey);
447
+ // 清除处理中状态 + 记录成功响应
448
+ this.sessionManager.clearProcessing(session.id);
449
+ // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
450
+ const interruptReason = this.interruptedSessions.get(session.id);
451
+ adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(session));
344
452
  await this.sessionManager.recordSuccess(session.id);
453
+ this.eventBus.publish({
454
+ type: 'message:completed',
455
+ sessionId: session.id,
456
+ channel: message.channel,
457
+ channelId: message.channelId,
458
+ durationMs: Date.now() - startTime,
459
+ timestamp: Date.now()
460
+ });
345
461
  const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
346
462
  if (isFinallyBackground) {
347
463
  const projectName = path.basename(session.projectPath);
348
464
  const count = this.messageCache.getCount(session.id);
349
- await adapter.sendText(message.channelId, `[后台-${projectName}] 任务完成 (${count}条消息已缓存)`);
465
+ await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`);
350
466
  }
351
467
  const duration = Date.now() - startTime;
352
468
  // 记录处理完成
@@ -366,7 +482,31 @@ ${suggestions}`, sendOpts);
366
482
  });
367
483
  }
368
484
  catch (error) {
485
+ // 清理流和处理中状态(异常时也要清除)
486
+ agent.cleanupStream(streamKey);
487
+ try {
488
+ this.sessionManager.clearProcessing(session.id);
489
+ }
490
+ catch { }
491
+ // 注意:不在此处清除 interruptedSessions,由下一条消息的 prompt 包装逻辑消费
492
+ // 区分超时 / 中断 / 错误
493
+ const errType = classifyError(error);
494
+ const procStatus = errType === ErrorType.SDK_TIMEOUT ? 'timeout'
495
+ : errType === ErrorType.STREAM_ERROR ? 'interrupted'
496
+ : 'error';
497
+ try {
498
+ adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(session));
499
+ }
500
+ catch { }
369
501
  logger.error(`[${message.channel}] Error:`, error);
502
+ const errorMsg = error instanceof Error ? error.message : String(error);
503
+ const errorType = errType;
504
+ this.eventBus.publish({
505
+ type: 'message:error',
506
+ sessionId: message.channelId,
507
+ error: errorMsg,
508
+ errorType: String(errorType)
509
+ });
370
510
  // 记录处理失败
371
511
  logger.message({
372
512
  msgId: messageId,
@@ -388,7 +528,7 @@ ${suggestions}`, sendOpts);
388
528
  let sendOpts;
389
529
  try {
390
530
  const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
391
- sendOpts = this.getThreadSendOpts(session);
531
+ sendOpts = this.getReplyContext(session);
392
532
  }
393
533
  catch { }
394
534
  await adapter.sendText(message.channelId, userMessage, sendOpts);
@@ -399,131 +539,148 @@ ${suggestions}`, sendOpts);
399
539
  * 解析会话和项目路径
400
540
  */
401
541
  async resolveSession(message) {
402
- // 话题会话:传入 rootId metadata(首条消息的 messageId 作为 rootId)
403
- const metadata = message.threadId && message.messageId
404
- ? { feishu: { rootId: message.messageId } }
542
+ // 话题会话:使用 Channel 预构建的 replyContext
543
+ const metadata = message.replyContext
544
+ ? { replyContext: message.replyContext }
405
545
  : undefined;
406
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata);
546
+ const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata, undefined, message.peerId);
407
547
  const absoluteProjectPath = path.isAbsolute(session.projectPath)
408
548
  ? session.projectPath
409
549
  : path.resolve(process.cwd(), session.projectPath);
410
550
  return { session, absoluteProjectPath };
411
551
  }
412
552
  /**
413
- * 处理事件流
553
+ * 处理标准事件流(AgentEvent)
554
+ *
555
+ * 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
556
+ * SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
414
557
  */
415
- async processEventStream(stream, session, channelId, adapter, options, flusher, isBackground, resetTimer, shouldSuppress) {
416
- let hasTextDelta = false;
558
+ async processEventStream(stream, session, flusher, resetTimer, shouldSuppress) {
417
559
  let hasReceivedText = false;
418
- let lastSessionId;
560
+ let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
419
561
  try {
420
562
  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;
563
+ // 每收到事件重置空闲超时
564
+ const toolName = event.type === 'tool_use' ? event.name : undefined;
425
565
  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;
566
+ // 记录所有事件类型
567
+ logger.info(`[MessageProcessor] Event: type=${event.type}`);
568
+ // session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
569
+ if (event.type === 'session_id') {
570
+ logger.info(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
571
+ continue;
433
572
  }
434
573
  const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
435
574
  // === 前台任务:正常处理所有事件 ===
436
575
  if (!isCurrentlyBackground) {
437
- // 流式文本事件(抑制时跳过,只累积到 allText)
438
- if (event.type === 'text_delta' && event.text) {
439
- hasTextDelta = true;
576
+ // 流式文本
577
+ if (event.type === 'text') {
440
578
  hasReceivedText = true;
579
+ this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
441
580
  if (!shouldSuppress()) {
442
581
  flusher.addText(event.text);
443
582
  }
444
583
  }
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})`);
584
+ // compact 完成
585
+ if (event.type === 'compact') {
586
+ this.eventBus.publish({ type: 'agent:compact-complete', sessionId: session.id, preTokens: event.preTokens });
587
+ if (!shouldSuppress()) {
588
+ flusher.addActivity(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`);
450
589
  }
451
590
  }
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})` : ''}`);
591
+ // 子任务进度
592
+ if (event.type === 'task_progress') {
593
+ const tools = event.toolUses ?? 0;
594
+ const duration = event.durationMs ? `${Math.round(event.durationMs / 1000)}s` : '';
595
+ const stats = [tools > 0 ? `${tools}\u6b21\u5de5\u5177\u8c03\u7528` : '', duration].filter(Boolean).join(', ');
596
+ if (event.summary && !shouldSuppress()) {
597
+ flusher.addActivity(`\u23f3 \u5b50\u4efb\u52a1: ${event.summary}${stats ? ` (${stats})` : ''}`);
460
598
  }
461
599
  else if (stats && !shouldSuppress()) {
462
- flusher.addActivity(`⏳ 子任务进行中: ${stats}`);
600
+ flusher.addActivity(`\u23f3 \u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`);
463
601
  }
464
602
  }
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
- }
603
+ // 工具调用
604
+ if (event.type === 'tool_use') {
605
+ this.eventBus.publish({
606
+ type: 'tool:use',
607
+ sessionId: session.id,
608
+ toolName: event.name,
609
+ input: event.input,
610
+ timestamp: Date.now()
611
+ });
612
+ if (!shouldSuppress()) {
613
+ const desc = summarizeToolInput(event.name, event.input || {});
614
+ flusher.addActivity(`\ud83d\udd27 ${event.name}${desc ? ': ' + desc : ''}`);
481
615
  }
482
616
  }
483
- // 工具结果事件:显示失败信息(包括权限拒绝、执行失败等所有场景)
617
+ // 工具结果
484
618
  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}`);
619
+ logger.debug(`[MessageProcessor] tool_result: name=${event.name}, is_error=${event.isError}`);
620
+ this.eventBus.publish({
621
+ type: 'tool:result',
622
+ sessionId: session.id,
623
+ toolName: event.name,
624
+ isError: event.isError,
625
+ content: event.result,
626
+ timestamp: Date.now()
627
+ });
628
+ if (event.isError && !shouldSuppress()) {
629
+ hasErrorResult = true;
630
+ let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
631
+ // 移除 XML 风格的错误标签
632
+ errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
633
+ flusher.addActivity(`\u26a0\ufe0f ${event.name || '\u5de5\u5177'}: ${errorMsg}`);
490
634
  }
491
635
  }
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);
636
+ // 运行时错误(Codex: turn.failed / item error)
637
+ if (event.type === 'error') {
638
+ logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
639
+ if (!hasErrorResult && !shouldSuppress()) {
640
+ hasErrorResult = true;
641
+ flusher.addActivity(`\u26a0\ufe0f ${event.error}`);
498
642
  }
499
- else if (!hasReceivedText) {
500
- // 非抑制模式 + 无流式文本:使用 result 作为兜底
501
- flusher.addText(event.result);
643
+ }
644
+ // 完成事件
645
+ if (event.type === 'complete') {
646
+ logger.debug(`[MessageProcessor] complete event: hasReceivedText=${hasReceivedText}, isError=${event.isError}, shouldSuppress=${shouldSuppress()}`);
647
+ // 失败且无前置错误输出:显示 errors 摘要
648
+ if (event.isError && !hasErrorResult && !shouldSuppress()) {
649
+ const errorSummary = event.errors?.join('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
650
+ flusher.addActivity(`\u26a0\ufe0f ${errorSummary}`);
651
+ }
652
+ // 成功结果文本:suppressed 模式下总是添加,否则仅在无流式文本时添加
653
+ if (event.result) {
654
+ if (shouldSuppress()) {
655
+ flusher.addText(event.result);
656
+ }
657
+ else if (!hasReceivedText) {
658
+ flusher.addText(event.result);
659
+ }
502
660
  }
503
- // 非抑制模式 + 有流式文本:已通过 text_delta 累积,无需再添加
504
- await flusher.flush(true); // isFinal=true 标记最终输出
661
+ await flusher.flush(true);
505
662
  }
506
663
  continue;
507
664
  }
508
- // === 后台任务:只处理 result 事件,仅缓存不发送 ===
509
- if (event.type !== 'result') {
665
+ // === 后台任务:只处理 complete 事件,仅缓存不发送 ===
666
+ if (event.type !== 'complete') {
510
667
  continue;
511
668
  }
512
669
  if (event.subtype === 'success') {
513
670
  this.messageCache.addEvent(session.id, {
514
671
  type: 'completed',
515
- message: event.result,
672
+ message: event.result || '',
516
673
  timestamp: Date.now(),
517
674
  metadata: {
518
- duration: event.duration_ms,
519
- cost: event.total_cost_usd
675
+ duration: event.durationMs,
676
+ cost: event.costUsd
520
677
  }
521
678
  });
522
679
  }
523
- else if (event.is_error === true) {
680
+ else if (event.isError === true) {
524
681
  this.messageCache.addEvent(session.id, {
525
682
  type: 'error',
526
- message: event.errors?.join('\n') || '未知错误',
683
+ message: event.errors?.join('\n') || '\u672a\u77e5\u9519\u8bef',
527
684
  timestamp: Date.now(),
528
685
  metadata: {
529
686
  errorType: event.subtype
@@ -533,27 +690,13 @@ ${suggestions}`, sendOpts);
533
690
  }
534
691
  }
535
692
  catch (error) {
536
- // 捕获 SDK 进程崩溃或流迭代错误
537
693
  logger.error('[MessageProcessor] Stream processing error:', error);
538
694
  if (error instanceof Error && error.message.includes('process exited')) {
539
- flusher.addActivity(' Claude Code 进程异常退出,请重试');
695
+ flusher.addActivity('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5');
540
696
  }
541
- throw error; // 重新抛出,让外层处理
697
+ throw error;
542
698
  }
543
699
  }
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
700
  /**
558
701
  * 解析文件路径,支持相对路径和绝对路径
559
702
  * 优先在项目根目录查找,兜底尝试 .openclaw/workspace/
@@ -583,18 +726,18 @@ ${suggestions}`, sendOpts);
583
726
  if (!filePath)
584
727
  return true;
585
728
  // 精确占位符
586
- const exactPlaceholders = ['...', '', 'path', 'file', 'file_path', 'filepath',
587
- '路径', '文件路径', '文件', 'filename', 'xxx'];
729
+ const exactPlaceholders = ['...', '\u2026', 'path', 'file', 'file_path', 'filepath',
730
+ '\u8def\u5f84', '\u6587\u4ef6\u8def\u5f84', '\u6587\u4ef6', 'filename', 'xxx'];
588
731
  if (exactPlaceholders.includes(filePath.toLowerCase()))
589
732
  return true;
590
733
  // 示例路径前缀
591
- if (/^(\/path\/to\/|\.\/path\/to\/|example\/|示例|\/example)/i.test(filePath))
734
+ if (/^(\/path\/to\/|\.\/path\/to\/|example\/|\u793a\u4f8b|\/example)/i.test(filePath))
592
735
  return true;
593
736
  // 含模板变量
594
737
  if (/\$\{.+\}|\{\{.+\}\}|<.+>/.test(filePath))
595
738
  return true;
596
739
  // 纯标点/特殊字符(非路径字符)
597
- if (/^[.\s]+$/.test(filePath))
740
+ if (/^[.\s\u2026]+$/.test(filePath))
598
741
  return true;
599
742
  // 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
600
743
  if (/[\\[\]{}*+?|^$]/.test(filePath))