evolclaw 2.1.2 → 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.
- package/README.md +10 -3
- package/data/evolclaw.sample.json +9 -1
- package/dist/agents/claude-runner.js +612 -0
- package/dist/agents/codex-runner.js +310 -0
- package/dist/channels/aun.js +416 -9
- package/dist/channels/feishu.js +397 -104
- package/dist/channels/wechat.js +84 -2
- package/dist/cli.js +427 -126
- package/dist/config.js +102 -4
- package/dist/core/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/adapters/codex-session-file-adapter.js +196 -0
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +60 -0
- package/dist/core/command-handler.js +908 -304
- package/dist/core/event-bus.js +32 -0
- package/dist/core/ipc-server.js +71 -0
- package/dist/core/message-bridge.js +187 -0
- package/dist/core/message-processor.js +370 -227
- package/dist/core/message-queue.js +153 -29
- package/dist/core/permission.js +58 -0
- package/dist/core/session-file-adapter.js +7 -0
- package/dist/core/session-manager.js +567 -205
- package/dist/core/stats-collector.js +86 -0
- package/dist/index.js +309 -243
- package/dist/paths.js +1 -0
- package/dist/utils/init-feishu.js +2 -0
- package/dist/utils/init-wechat.js +2 -0
- package/dist/utils/init.js +285 -53
- package/dist/utils/ipc-client.js +36 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/{permission.js → permission-utils.js} +31 -3
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/session-file-health.js +11 -34
- package/dist/utils/stream-debouncer.js +122 -0
- package/dist/utils/stream-idle-monitor.js +1 -1
- package/package.json +3 -1
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-stream.js +0 -59
- package/dist/index.js.bak +0 -340
- package/dist/utils/markdown-to-feishu.js +0 -94
- /package/dist/utils/{platform.js → cross-platform.js} +0 -0
- /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 {
|
|
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(
|
|
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 (
|
|
49
|
-
this.
|
|
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
|
|
64
|
-
// 非主人(群聊或单聊):空闲监控静默/简短
|
|
65
|
-
const quietMode = isGroup || !isOwnerUser;
|
|
114
|
+
const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
|
|
66
115
|
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
67
116
|
const shouldSuppress = () => {
|
|
68
|
-
|
|
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(
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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 &&
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
174
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
236
|
+
await adapter.sendText(channelId, `\u26a0\ufe0f 检测到异常(${consecutiveErrors}/${safeModeThreshold})\n\n如果问题持续,系统将自动进入安全模式。建议使用 /status 查看状态。`, sendOpts);
|
|
204
237
|
}
|
|
205
238
|
}
|
|
206
|
-
async _processMessageInternal(message,
|
|
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
|
-
//
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
261
|
-
opts
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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
|
|
292
|
-
|
|
293
|
-
await this.processEventStream(retryStream, session,
|
|
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
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
await adapter.sendText(message.channelId,
|
|
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
|
|
338
|
-
: '\n\n
|
|
339
|
-
await adapter.sendText(message.channelId, hint, this.
|
|
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
|
-
|
|
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, `[
|
|
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.
|
|
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
|
-
//
|
|
403
|
-
const metadata = message.
|
|
404
|
-
? {
|
|
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,
|
|
416
|
-
let hasTextDelta = false;
|
|
558
|
+
async processEventStream(stream, session, flusher, resetTimer, shouldSuppress) {
|
|
417
559
|
let hasReceivedText = false;
|
|
418
|
-
let
|
|
560
|
+
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
419
561
|
try {
|
|
420
562
|
for await (const event of stream) {
|
|
421
|
-
//
|
|
422
|
-
const toolName = event.type === '
|
|
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
|
-
//
|
|
427
|
-
logger.info(`[MessageProcessor] Event: type=${event.type}
|
|
428
|
-
//
|
|
429
|
-
if (event.
|
|
430
|
-
logger.info(`[MessageProcessor]
|
|
431
|
-
|
|
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
|
-
//
|
|
438
|
-
if (event.type === '
|
|
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
|
-
//
|
|
446
|
-
if (event.type === '
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
flusher.addActivity(
|
|
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
|
-
//
|
|
453
|
-
if (event.type === '
|
|
454
|
-
const tools = event.
|
|
455
|
-
const duration = event.
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
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(
|
|
600
|
+
flusher.addActivity(`\u23f3 \u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`);
|
|
463
601
|
}
|
|
464
602
|
}
|
|
465
|
-
//
|
|
466
|
-
if (event.type === '
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
//
|
|
493
|
-
if (event.type === '
|
|
494
|
-
logger.
|
|
495
|
-
if (shouldSuppress()) {
|
|
496
|
-
|
|
497
|
-
flusher.
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
await flusher.flush(true); // isFinal=true 标记最终输出
|
|
661
|
+
await flusher.flush(true);
|
|
505
662
|
}
|
|
506
663
|
continue;
|
|
507
664
|
}
|
|
508
|
-
// === 后台任务:只处理
|
|
509
|
-
if (event.type !== '
|
|
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.
|
|
519
|
-
cost: event.
|
|
675
|
+
duration: event.durationMs,
|
|
676
|
+
cost: event.costUsd
|
|
520
677
|
}
|
|
521
678
|
});
|
|
522
679
|
}
|
|
523
|
-
else if (event.
|
|
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('
|
|
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 = ['...', '
|
|
587
|
-
'
|
|
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
|
|
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
|
|
740
|
+
if (/^[.\s\u2026]+$/.test(filePath))
|
|
598
741
|
return true;
|
|
599
742
|
// 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
|
|
600
743
|
if (/[\\[\]{}*+?|^$]/.test(filePath))
|