evolclaw 2.8.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
|
@@ -2,32 +2,56 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { hasCompact } from '../../agents/claude-runner.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { appendMessageLog, buildOutboundEntry } from './message-log.js';
|
|
6
|
+
import { IMRenderer } from './im-renderer.js';
|
|
7
7
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
9
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
12
|
-
import { getOwner } from '../../config.js';
|
|
13
12
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
14
|
-
import {
|
|
13
|
+
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
14
|
+
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
15
|
+
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
16
|
+
/**
|
|
17
|
+
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
18
|
+
*
|
|
19
|
+
* 用于所有走 adapter.send 的出站路径:
|
|
20
|
+
* - 任务流内的 IMRenderer 投影(chatmode 由会话决定)
|
|
21
|
+
* - 命令回显(MessageBridge.handleCommand,taskId 用合成 ID `cmd-...`)
|
|
22
|
+
* - 网关层系统通知(src/index.ts,taskId 用 `system-...` / `restart-...` 等便于 events.log 关联)
|
|
23
|
+
*
|
|
24
|
+
* 注意:
|
|
25
|
+
* - chatmode 缺省 `'interactive'`(系统通知 / 命令回显都属于同步交互);
|
|
26
|
+
* - timestamp 可由调用方注入(便于测试),缺省 `Date.now()`。
|
|
27
|
+
*/
|
|
28
|
+
export function buildEnvelope(opts) {
|
|
29
|
+
return {
|
|
30
|
+
taskId: opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
31
|
+
channel: opts.channel,
|
|
32
|
+
channelId: opts.channelId,
|
|
33
|
+
agentName: opts.agentName ?? '<unknown>',
|
|
34
|
+
chatmode: opts.chatmode ?? 'interactive',
|
|
35
|
+
replyContext: opts.replyContext,
|
|
36
|
+
timestamp: opts.timestamp ?? Date.now(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
15
39
|
/**
|
|
16
40
|
* 统一消息处理器
|
|
17
41
|
* 负责处理来自不同渠道的消息,协调事件流处理
|
|
18
42
|
*/
|
|
19
43
|
export class MessageProcessor {
|
|
20
44
|
sessionManager;
|
|
21
|
-
|
|
45
|
+
globalSettings;
|
|
22
46
|
messageCache;
|
|
23
47
|
eventBus;
|
|
24
48
|
commandHandler;
|
|
25
49
|
channels = new Map();
|
|
26
50
|
channelTypeMap = new Map(); // channelType → channelName(首个实例)
|
|
27
|
-
|
|
51
|
+
currentRenderer;
|
|
28
52
|
shouldSuppressActivities = false;
|
|
29
53
|
agentMap;
|
|
30
|
-
|
|
54
|
+
primaryRunnerKey;
|
|
31
55
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
32
56
|
interactionRouter;
|
|
33
57
|
messageQueue;
|
|
@@ -38,18 +62,18 @@ export class MessageProcessor {
|
|
|
38
62
|
* - `channel` is used to look up the owning EvolAgent (via registry).
|
|
39
63
|
* - `baseagent` (e.g. 'claude') comes from `session.agentId`.
|
|
40
64
|
*
|
|
41
|
-
* Falls back to `
|
|
65
|
+
* Falls back to `primaryRunnerKey` (a composite key, e.g. `aid::claude`)
|
|
42
66
|
* when no match is found.
|
|
43
67
|
*/
|
|
44
68
|
getAgent(channel, baseagent) {
|
|
45
69
|
if (channel && baseagent) {
|
|
46
|
-
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '
|
|
70
|
+
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
|
|
47
71
|
const key = `${evolName}::${baseagent}`;
|
|
48
72
|
if (this.agentMap.has(key))
|
|
49
73
|
return this.agentMap.get(key);
|
|
50
74
|
}
|
|
51
|
-
if (this.agentMap.has(this.
|
|
52
|
-
return this.agentMap.get(this.
|
|
75
|
+
if (this.agentMap.has(this.primaryRunnerKey))
|
|
76
|
+
return this.agentMap.get(this.primaryRunnerKey);
|
|
53
77
|
return this.agentMap.values().next().value;
|
|
54
78
|
}
|
|
55
79
|
/** 获取可用 agent 列表 */
|
|
@@ -63,23 +87,23 @@ export class MessageProcessor {
|
|
|
63
87
|
const active = await this.sessionManager.getActiveSession(channel, channelId);
|
|
64
88
|
return active ? session.id !== active.id : false;
|
|
65
89
|
}
|
|
66
|
-
constructor(agentRunnerOrMap, sessionManager,
|
|
90
|
+
constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
|
|
67
91
|
this.sessionManager = sessionManager;
|
|
68
|
-
this.
|
|
92
|
+
this.globalSettings = globalSettings;
|
|
69
93
|
this.messageCache = messageCache;
|
|
70
94
|
this.eventBus = eventBus;
|
|
71
95
|
this.commandHandler = commandHandler;
|
|
72
96
|
if (agentRunnerOrMap instanceof Map) {
|
|
73
97
|
this.agentMap = agentRunnerOrMap;
|
|
74
|
-
this.
|
|
98
|
+
this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
|
|
75
99
|
}
|
|
76
100
|
else {
|
|
77
|
-
//
|
|
78
|
-
this.agentMap = new Map([[
|
|
79
|
-
this.
|
|
101
|
+
// 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
|
|
102
|
+
this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
|
|
103
|
+
this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
|
|
80
104
|
}
|
|
81
105
|
// 监听中断事件,标记被中断的 session
|
|
82
|
-
this.eventBus.subscribe('
|
|
106
|
+
this.eventBus.subscribe('task:interrupted', (event) => {
|
|
83
107
|
if ('sessionId' in event && event.sessionId) {
|
|
84
108
|
this.interruptedSessions.set(event.sessionId, event.reason || 'unknown');
|
|
85
109
|
}
|
|
@@ -101,7 +125,8 @@ export class MessageProcessor {
|
|
|
101
125
|
const agent = this.agentRegistry.resolveByChannel(channelName);
|
|
102
126
|
if (!agent)
|
|
103
127
|
return null;
|
|
104
|
-
|
|
128
|
+
// chatmode 解析优先级:agent.config.chatmode > globalSettings.chatmode
|
|
129
|
+
const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
|
|
105
130
|
return agent.getContext(channelName, chatType, globalCm);
|
|
106
131
|
}
|
|
107
132
|
/**
|
|
@@ -132,10 +157,10 @@ export class MessageProcessor {
|
|
|
132
157
|
*/
|
|
133
158
|
handleCompactStart(sessionId) {
|
|
134
159
|
if (sessionId) {
|
|
135
|
-
this.eventBus.publish({ type: '
|
|
160
|
+
this.eventBus.publish({ type: 'runner:compact-start', sessionId });
|
|
136
161
|
}
|
|
137
|
-
if (this.
|
|
138
|
-
this.
|
|
162
|
+
if (this.currentRenderer && !this.shouldSuppressActivities) {
|
|
163
|
+
this.currentRenderer.addNotice('\u23f3 会话压缩中...', 'info', 'compact-start', true);
|
|
139
164
|
}
|
|
140
165
|
}
|
|
141
166
|
/**
|
|
@@ -159,7 +184,7 @@ export class MessageProcessor {
|
|
|
159
184
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
160
185
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
161
186
|
'/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
162
|
-
'/aid', '/agentmd',
|
|
187
|
+
'/aid', '/agentmd', '/upgrade',
|
|
163
188
|
];
|
|
164
189
|
/** 判断消息内容是否为已知命令 */
|
|
165
190
|
isKnownCommand(content) {
|
|
@@ -170,7 +195,7 @@ export class MessageProcessor {
|
|
|
170
195
|
* 处理消息(主入口)
|
|
171
196
|
*/
|
|
172
197
|
async processMessage(message) {
|
|
173
|
-
const idleMs = (this.
|
|
198
|
+
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
174
199
|
// 先解析会话,再优先用 session.metadata.channelName 精确定位实例级 adapter
|
|
175
200
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
176
201
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
@@ -184,6 +209,7 @@ export class MessageProcessor {
|
|
|
184
209
|
const streamKey = session.id;
|
|
185
210
|
const chatType = message.chatType || 'private';
|
|
186
211
|
const identityRole = session.identity?.role || 'anonymous';
|
|
212
|
+
const agentNameForMonitor = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
187
213
|
// Resolve agent context from registry (Phase 2 foundation)
|
|
188
214
|
const agentContext = this.getAgentContext(channelKey, chatType);
|
|
189
215
|
if (agentContext) {
|
|
@@ -191,7 +217,7 @@ export class MessageProcessor {
|
|
|
191
217
|
}
|
|
192
218
|
// 按 session.agentId 选择 agent 后端
|
|
193
219
|
const agent = this.getAgent(channelKey, session.agentId);
|
|
194
|
-
const monitorEnabled = this.
|
|
220
|
+
const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
|
|
195
221
|
const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
|
|
196
222
|
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
197
223
|
const shouldSuppress = () => {
|
|
@@ -217,13 +243,13 @@ export class MessageProcessor {
|
|
|
217
243
|
while (result) {
|
|
218
244
|
if (result.action === 'kill') {
|
|
219
245
|
logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
220
|
-
this.eventBus.publish({ type: '
|
|
246
|
+
this.eventBus.publish({ type: 'runner:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
|
|
221
247
|
// 后台任务也需要中断(释放资源),但不发送通知
|
|
222
248
|
if (channelInfo && !isBackground) {
|
|
223
249
|
const msg = showIdleMonitor
|
|
224
250
|
? result.message
|
|
225
251
|
: `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
|
|
226
|
-
channelInfo.adapter.
|
|
252
|
+
channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: msg, subtype: 'health' }).catch(e => {
|
|
227
253
|
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
228
254
|
});
|
|
229
255
|
}
|
|
@@ -239,7 +265,7 @@ export class MessageProcessor {
|
|
|
239
265
|
logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
240
266
|
if (channelInfo && showIdleMonitor && !shouldSuppress()) {
|
|
241
267
|
if (!isBackground) {
|
|
242
|
-
channelInfo.adapter.
|
|
268
|
+
channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: result.message, subtype: 'health' }).catch(e => {
|
|
243
269
|
logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
|
|
244
270
|
});
|
|
245
271
|
}
|
|
@@ -302,8 +328,8 @@ export class MessageProcessor {
|
|
|
302
328
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
303
329
|
const channelKey = session.metadata?.channelName || message.channel;
|
|
304
330
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
305
|
-
// Per-method agent name for stats bucketing (agent.name or '
|
|
306
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '
|
|
331
|
+
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
332
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
307
333
|
if (!channelInfo) {
|
|
308
334
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
309
335
|
return;
|
|
@@ -321,6 +347,8 @@ export class MessageProcessor {
|
|
|
321
347
|
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
322
348
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
323
349
|
const chatmode = session.sessionMode ?? 'interactive';
|
|
350
|
+
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
351
|
+
logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}`);
|
|
324
352
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
325
353
|
const taskReplyContext = () => {
|
|
326
354
|
const base = this.getReplyContext(message);
|
|
@@ -329,8 +357,16 @@ export class MessageProcessor {
|
|
|
329
357
|
metadata: { ...(base?.metadata ?? {}), taskId, chatmode },
|
|
330
358
|
};
|
|
331
359
|
};
|
|
332
|
-
|
|
333
|
-
|
|
360
|
+
const isProactive = session.sessionMode === 'proactive';
|
|
361
|
+
const isAutonomous = session.sessionMode === 'autonomous' || message.triggerMeta?.silent === true;
|
|
362
|
+
const envelope = buildEnvelope({
|
|
363
|
+
taskId,
|
|
364
|
+
channel: message.channel,
|
|
365
|
+
channelId: message.channelId,
|
|
366
|
+
agentName: agentNameForStats,
|
|
367
|
+
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
368
|
+
replyContext: taskReplyContext(),
|
|
369
|
+
});
|
|
334
370
|
try {
|
|
335
371
|
const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
336
372
|
// 记录收到消息
|
|
@@ -360,8 +396,11 @@ export class MessageProcessor {
|
|
|
360
396
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
361
397
|
logger.info(`[MessageProcessor] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
362
398
|
// 记录开始处理
|
|
363
|
-
this.eventBus.publish({ type: '
|
|
364
|
-
|
|
399
|
+
this.eventBus.publish({ type: 'task:started', sessionId: session.id });
|
|
400
|
+
// 触发器消息不发 processing status(无需通知用户)
|
|
401
|
+
if (message.source !== 'trigger') {
|
|
402
|
+
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
403
|
+
}
|
|
365
404
|
logger.message({
|
|
366
405
|
msgId: messageId,
|
|
367
406
|
sessionId: session.id,
|
|
@@ -369,42 +408,47 @@ export class MessageProcessor {
|
|
|
369
408
|
status: 'processing'
|
|
370
409
|
});
|
|
371
410
|
const startTime = Date.now();
|
|
372
|
-
// 创建
|
|
373
|
-
// 使用动态判断,确保切换项目后不会继续输出
|
|
411
|
+
// 创建 IMRenderer(统一 interactive/proactive 两条路径)
|
|
374
412
|
let firstReply = true;
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
413
|
+
const renderer = new IMRenderer({
|
|
414
|
+
adapter,
|
|
415
|
+
envelope,
|
|
416
|
+
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
417
|
+
suppressActivities: shouldSuppress() || isAutonomous,
|
|
418
|
+
fileMarkerPattern: options?.fileMarkerPattern,
|
|
419
|
+
diagEnabled: this.globalSettings.debug?.flusherDiag,
|
|
420
|
+
send: async (payload) => {
|
|
421
|
+
if (isAutonomous)
|
|
422
|
+
return; // autonomous session: never send to channel
|
|
423
|
+
// proactive 模式:activity.batch 是 thought 协议内容,只发给支持 thought 的 channel
|
|
424
|
+
// (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
|
|
425
|
+
if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
|
|
426
|
+
return;
|
|
427
|
+
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
428
|
+
if (isCurrentlyBackground)
|
|
429
|
+
return;
|
|
379
430
|
const opts = {};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const replyCtx = this.getReplyContext(message);
|
|
384
|
-
if (replyCtx) {
|
|
385
|
-
Object.assign(opts, replyCtx);
|
|
431
|
+
const baseReplyCtx = this.getReplyContext(message);
|
|
432
|
+
if (baseReplyCtx) {
|
|
433
|
+
Object.assign(opts, baseReplyCtx);
|
|
386
434
|
}
|
|
387
435
|
else if (firstReply && message.messageId) {
|
|
388
|
-
|
|
389
|
-
if (hasText) {
|
|
436
|
+
if (payload.kind === 'result.text' && payload.text) {
|
|
390
437
|
opts.replyToMessageId = message.messageId;
|
|
391
438
|
firstReply = false;
|
|
392
439
|
}
|
|
393
440
|
}
|
|
441
|
+
if (payload.kind === 'result.text' && payload.isFinal) {
|
|
442
|
+
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
443
|
+
}
|
|
394
444
|
opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
this.currentFlusher = flusher;
|
|
445
|
+
const enrichedEnvelope = { ...envelope, replyContext: opts };
|
|
446
|
+
await adapter.send(enrichedEnvelope, payload);
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
this.currentRenderer = renderer;
|
|
401
450
|
if (isProactive) {
|
|
402
|
-
logger.info(`[MessageProcessor] proactive mode:
|
|
403
|
-
}
|
|
404
|
-
// Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
|
|
405
|
-
// selector: context = { type: 'task', id: taskId }
|
|
406
|
-
if (isProactive && adapter.putThought) {
|
|
407
|
-
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode, this.getReplyContext(message));
|
|
451
|
+
logger.info(`[MessageProcessor] proactive mode: outputs via thought.put task=${taskId}`);
|
|
408
452
|
}
|
|
409
453
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
410
454
|
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
@@ -412,7 +456,7 @@ export class MessageProcessor {
|
|
|
412
456
|
const capturedReplyContext = taskReplyContext();
|
|
413
457
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
414
458
|
agent.setSendPrompt(async (text) => {
|
|
415
|
-
await adapter.
|
|
459
|
+
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text, isFinal: true });
|
|
416
460
|
});
|
|
417
461
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
418
462
|
agent.setPermissionContext?.(session.id, {
|
|
@@ -420,6 +464,11 @@ export class MessageProcessor {
|
|
|
420
464
|
channelId: capturedChannelId,
|
|
421
465
|
replyContext: capturedReplyContext,
|
|
422
466
|
interactionRouter: this.interactionRouter,
|
|
467
|
+
userId: message.peerId || undefined,
|
|
468
|
+
channel: message.channel,
|
|
469
|
+
agentName: agentNameForStats,
|
|
470
|
+
taskId,
|
|
471
|
+
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
423
472
|
interceptNextMessage: this.messageQueue
|
|
424
473
|
? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
|
|
425
474
|
: undefined,
|
|
@@ -442,73 +491,77 @@ export class MessageProcessor {
|
|
|
442
491
|
? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
|
|
443
492
|
: message.content;
|
|
444
493
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
494
|
+
let effectiveSystemPrompt;
|
|
445
495
|
try {
|
|
446
496
|
// 动态构建运行时上下文提示
|
|
447
497
|
const contextParts = [];
|
|
448
498
|
const currentChannelType = options?.channelType || message.channel;
|
|
449
|
-
//
|
|
450
|
-
const peerName = message.peerName || session.metadata?.peerName;
|
|
451
|
-
const peerType = message.peerType;
|
|
452
|
-
const peerId = message.peerId;
|
|
499
|
+
// 提取 self 信息
|
|
453
500
|
const adapterAny = channelInfo.adapter;
|
|
454
501
|
const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
455
502
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
return `${name} (${id})`;
|
|
459
|
-
return name || id || undefined;
|
|
460
|
-
};
|
|
461
|
-
const selfIdentity = formatIdentity(selfName, selfAid);
|
|
462
|
-
const peerIdentity = formatIdentity(peerName, peerId);
|
|
463
|
-
// 文件发送能力(按 channelType 去重)
|
|
464
|
-
let crossChannelTypes = [];
|
|
503
|
+
const peerName = message.peerName || session.metadata?.peerName;
|
|
504
|
+
// 文件发送能力
|
|
465
505
|
let currentCanSend = false;
|
|
466
506
|
if (!isProactive) {
|
|
467
|
-
|
|
468
|
-
currentCanSend = !!channelInfo.adapter.sendFile;
|
|
469
|
-
for (const [, info] of this.channels) {
|
|
470
|
-
if (info.adapter.sendFile) {
|
|
471
|
-
fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
|
|
507
|
+
currentCanSend = !!(channelInfo.adapter.capabilities?.file);
|
|
475
508
|
}
|
|
476
509
|
// 通道能力
|
|
477
510
|
const capParts = [];
|
|
478
511
|
if (options?.supportsImages)
|
|
479
512
|
capParts.push('图片输入');
|
|
480
|
-
if (channelInfo.adapter.
|
|
513
|
+
if (channelInfo.adapter.capabilities?.image)
|
|
481
514
|
capParts.push('图片输出');
|
|
482
|
-
if (channelInfo.adapter.
|
|
515
|
+
if (channelInfo.adapter.capabilities?.file)
|
|
483
516
|
capParts.push('文件发送');
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
517
|
+
// Personal layer
|
|
518
|
+
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
|
|
519
|
+
const persona = owningAgent?.getPersona?.() || undefined;
|
|
520
|
+
const working = owningAgent?.getWorkingMemory?.() || undefined;
|
|
521
|
+
if (persona)
|
|
522
|
+
contextParts.push(persona);
|
|
523
|
+
if (working)
|
|
524
|
+
contextParts.push(`[当前关注]\n${working}`);
|
|
525
|
+
// 计算 peerKey: <channel>#<urlEncode(peerId)>
|
|
526
|
+
const peerIdRaw = message.peerId;
|
|
527
|
+
const peerKey = (currentChannelType && peerIdRaw)
|
|
528
|
+
? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
|
|
529
|
+
: undefined;
|
|
530
|
+
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
531
|
+
// Kit renderer: 组装上下文
|
|
532
|
+
const kitCtx = {
|
|
533
|
+
vars: {
|
|
534
|
+
EVOLCLAW_HOME: resolveRoot(),
|
|
535
|
+
PACKAGE_ROOT: getPackageRoot(),
|
|
536
|
+
CURRENT_PROJECT: absoluteProjectPath,
|
|
537
|
+
selfAid: selfAid || undefined,
|
|
538
|
+
selfName: selfName || undefined,
|
|
539
|
+
hasPersona: !!persona,
|
|
540
|
+
hasWorkingMemory: !!working,
|
|
541
|
+
peerId: peerIdRaw || undefined,
|
|
542
|
+
peerKey,
|
|
543
|
+
peerName: peerName || undefined,
|
|
544
|
+
peerRole: session.identity?.role || 'unknown',
|
|
545
|
+
groupId: session.metadata?.groupId || undefined,
|
|
546
|
+
scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
|
|
547
|
+
chatType: session.chatType || null,
|
|
548
|
+
channel: currentChannelType || null,
|
|
549
|
+
venueUid: undefined,
|
|
550
|
+
project: path.basename(absoluteProjectPath),
|
|
551
|
+
sessionName: session.name || undefined,
|
|
552
|
+
sessionMode: isProactive ? 'proactive' : 'interactive',
|
|
553
|
+
readonly: session.metadata?.permissionMode === 'readonly',
|
|
554
|
+
canSendFile: !isProactive && currentCanSend,
|
|
555
|
+
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
556
|
+
baseAgent: normalizedBaseagent.canonical,
|
|
557
|
+
baseAgentName: normalizedBaseagent.displayName,
|
|
558
|
+
},
|
|
559
|
+
sessionId: session.id,
|
|
560
|
+
};
|
|
561
|
+
const kitContext = renderKitSections(kitCtx);
|
|
562
|
+
if (kitContext)
|
|
563
|
+
contextParts.push(kitContext);
|
|
564
|
+
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
512
565
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
513
566
|
const MAX_RETRIES = 3;
|
|
514
567
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -518,7 +571,7 @@ export class MessageProcessor {
|
|
|
518
571
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
519
572
|
agent.registerStream(streamKey, stream);
|
|
520
573
|
streamRegistered = true;
|
|
521
|
-
streamResult = await this.processEventStream(stream, session,
|
|
574
|
+
streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
|
|
522
575
|
break; // 成功,跳出重试循环
|
|
523
576
|
}
|
|
524
577
|
catch (retryError) {
|
|
@@ -528,8 +581,8 @@ export class MessageProcessor {
|
|
|
528
581
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
529
582
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
530
583
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
531
|
-
|
|
532
|
-
await
|
|
584
|
+
renderer.addNotice(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
|
|
585
|
+
await renderer.flush();
|
|
533
586
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
534
587
|
continue;
|
|
535
588
|
}
|
|
@@ -540,15 +593,15 @@ export class MessageProcessor {
|
|
|
540
593
|
catch (error) {
|
|
541
594
|
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
|
|
542
595
|
// 尝试 compact 压缩会话
|
|
543
|
-
|
|
544
|
-
await
|
|
596
|
+
renderer.addNotice('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
597
|
+
await renderer.flush();
|
|
545
598
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
546
599
|
if (compacted) {
|
|
547
600
|
// compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
|
|
548
|
-
|
|
549
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined,
|
|
601
|
+
renderer.addNotice('\u2705 压缩完成,正在重试...', 'info', 'compact-retry', true);
|
|
602
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
550
603
|
agent.registerStream(streamKey, retryStream);
|
|
551
|
-
streamResult = await this.processEventStream(retryStream, session,
|
|
604
|
+
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
552
605
|
}
|
|
553
606
|
else {
|
|
554
607
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -558,14 +611,43 @@ export class MessageProcessor {
|
|
|
558
611
|
throw error;
|
|
559
612
|
}
|
|
560
613
|
}
|
|
614
|
+
// prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
|
|
615
|
+
// 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
|
|
616
|
+
const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
617
|
+
const errorsText = streamResult.errors?.join(' ') || '';
|
|
618
|
+
const isPromptTooLong = streamResult.isError && session.agentSessionId && hasCompact(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
619
|
+
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
620
|
+
contextTooLongPattern.test(errorsText) ||
|
|
621
|
+
contextTooLongPattern.test(streamResult.fullText));
|
|
622
|
+
if (isPromptTooLong) {
|
|
623
|
+
renderer.addNotice('⚠️ 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
624
|
+
await renderer.flush();
|
|
625
|
+
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
626
|
+
if (compacted) {
|
|
627
|
+
renderer.addNotice('✅ 压缩完成,正在重试...', 'info', 'compact-retry', true);
|
|
628
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
629
|
+
agent.registerStream(streamKey, retryStream);
|
|
630
|
+
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else if (streamResult.isError && !isPromptTooLong && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
637
|
+
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
638
|
+
contextTooLongPattern.test(errorsText) ||
|
|
639
|
+
contextTooLongPattern.test(streamResult.fullText))) {
|
|
640
|
+
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
641
|
+
renderer.addNotice('⚠️ 上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
|
|
642
|
+
}
|
|
561
643
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
562
644
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
563
|
-
// suppressed 模式下
|
|
645
|
+
// suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
|
|
564
646
|
// proactive 模式:agent 主动调用 ctl file 发送文件,跳过标记处理
|
|
565
647
|
if (!isProactive) {
|
|
566
648
|
const FILE_MARKER_RE = /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
|
|
567
649
|
const markerPattern = options?.fileMarkerPattern ?? FILE_MARKER_RE;
|
|
568
|
-
const flusherText =
|
|
650
|
+
const flusherText = renderer.getFinalText();
|
|
569
651
|
const fullText = flusherText.length >= (streamResult.fullText?.length || 0) ? flusherText : streamResult.fullText;
|
|
570
652
|
const fileMatches = [...fullText.matchAll(markerPattern)];
|
|
571
653
|
for (const match of fileMatches) {
|
|
@@ -596,22 +678,22 @@ export class MessageProcessor {
|
|
|
596
678
|
&& targetSpec !== currentChannelType;
|
|
597
679
|
// 跨通道仅限 owner
|
|
598
680
|
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
599
|
-
await adapter.
|
|
681
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 跨通道发送仅限管理员`, subtype: 'fatal' });
|
|
600
682
|
continue;
|
|
601
683
|
}
|
|
602
684
|
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
603
685
|
if (!fs.existsSync(resolvedPath)) {
|
|
604
686
|
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
605
|
-
await adapter.
|
|
687
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u26a0\ufe0f 文件未找到: ${filePath}`, subtype: 'fatal' });
|
|
606
688
|
continue;
|
|
607
689
|
}
|
|
608
690
|
// 找目标 adapter
|
|
609
691
|
if (!targetInfo) {
|
|
610
|
-
await adapter.
|
|
692
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 未启用或不存在`, subtype: 'channel_down' });
|
|
611
693
|
continue;
|
|
612
694
|
}
|
|
613
|
-
if (!targetInfo.adapter.
|
|
614
|
-
await adapter.
|
|
695
|
+
if (!targetInfo.adapter.capabilities?.file) {
|
|
696
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 不支持文件发送`, subtype: 'capability' });
|
|
615
697
|
continue;
|
|
616
698
|
}
|
|
617
699
|
// 找目标 channelId
|
|
@@ -619,64 +701,56 @@ export class MessageProcessor {
|
|
|
619
701
|
if (isCrossChannel) {
|
|
620
702
|
const targetAdapterName = targetInfo.adapter.channelName;
|
|
621
703
|
const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
|
|
622
|
-
const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName)
|
|
704
|
+
const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName);
|
|
623
705
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
624
706
|
if (!targetChannelId) {
|
|
625
|
-
await adapter.
|
|
707
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, subtype: 'channel_down' });
|
|
626
708
|
continue;
|
|
627
709
|
}
|
|
628
710
|
}
|
|
629
711
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
630
712
|
try {
|
|
631
|
-
await targetInfo.adapter.
|
|
632
|
-
this.eventBus.publish({ type: '
|
|
713
|
+
await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: taskReplyContext() }), { kind: 'result.file', filePath: resolvedPath });
|
|
714
|
+
this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
633
715
|
if (isCrossChannel) {
|
|
634
|
-
await adapter.
|
|
716
|
+
await adapter.send(envelope, { kind: 'system.notice', text: `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, subtype: 'health' });
|
|
635
717
|
}
|
|
636
718
|
}
|
|
637
719
|
catch (error) {
|
|
638
720
|
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
639
|
-
await adapter.
|
|
721
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 文件发送失败: ${filePath}`, subtype: 'fatal' });
|
|
640
722
|
}
|
|
641
723
|
}
|
|
642
724
|
} // end of !isProactive
|
|
643
|
-
//
|
|
644
|
-
// suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
|
|
645
|
-
// 非 suppressed 且无流式文本:同上
|
|
646
|
-
// 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
|
|
647
|
-
// 但如果 flusher 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
|
|
725
|
+
// 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
|
|
648
726
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
649
|
-
// 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
|
|
650
|
-
// 特征:无流式 text + complete.result 匹配已知模式
|
|
651
|
-
// 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
|
|
652
|
-
// Proactive 模式下 flusher silent,需要兜底发出以告知用户,否则用户完全无反馈。
|
|
653
|
-
const isSdkFallbackMessage = !!finalReplyText
|
|
654
|
-
&& !streamResult.hasReceivedText
|
|
655
|
-
&& /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
|
|
656
727
|
if (finalReplyText) {
|
|
657
|
-
if (isProactive &&
|
|
658
|
-
// Proactive 模式 + SDK
|
|
728
|
+
if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
|
|
729
|
+
// Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
|
|
659
730
|
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
660
731
|
if (!isCurrentlyBackground) {
|
|
661
|
-
await adapter.
|
|
732
|
+
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
|
|
662
733
|
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
663
734
|
}
|
|
664
735
|
}
|
|
665
|
-
else if (shouldSuppress()) {
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
else if (!streamResult.hasReceivedText || (!flusher.hasSentContent() && !flusher.hasContent())) {
|
|
669
|
-
flusher.addText(finalReplyText);
|
|
736
|
+
else if (shouldSuppress() || !streamResult.hasReceivedText) {
|
|
737
|
+
renderer.addText(finalReplyText);
|
|
670
738
|
}
|
|
671
739
|
}
|
|
672
|
-
//
|
|
673
|
-
await flusher.flush(true);
|
|
674
|
-
// 清理 activeStreams(正常完成)
|
|
740
|
+
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
675
741
|
agent.cleanupStream(streamKey);
|
|
676
742
|
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
677
|
-
// 清除处理中状态
|
|
678
743
|
this.sessionManager.clearProcessing(session.id);
|
|
679
744
|
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
745
|
+
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
746
|
+
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
747
|
+
if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
|
|
748
|
+
logger.info(`[MessageProcessor] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
752
|
+
await renderer.flush(true);
|
|
753
|
+
}
|
|
680
754
|
// 更新 EvolAgent.lastActivity
|
|
681
755
|
if (this.agentRegistry) {
|
|
682
756
|
const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
|
|
@@ -690,9 +764,14 @@ export class MessageProcessor {
|
|
|
690
764
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
691
765
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
692
766
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
693
|
-
|
|
767
|
+
if (message.source !== 'trigger') {
|
|
768
|
+
adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
769
|
+
}
|
|
770
|
+
if (message.triggerMeta) {
|
|
771
|
+
this.eventBus.publish({ type: 'trigger:failed', triggerId: message.triggerMeta.triggerId, messageId: messageId, error: errorSummary });
|
|
772
|
+
}
|
|
694
773
|
this.eventBus.publish({
|
|
695
|
-
type: '
|
|
774
|
+
type: 'task:error',
|
|
696
775
|
sessionId: session.id,
|
|
697
776
|
error: errorSummary,
|
|
698
777
|
errorType,
|
|
@@ -719,10 +798,30 @@ export class MessageProcessor {
|
|
|
719
798
|
}
|
|
720
799
|
else {
|
|
721
800
|
// 真正的成功
|
|
722
|
-
|
|
801
|
+
const durationMs = Date.now() - startTime;
|
|
802
|
+
if (message.source !== 'trigger') {
|
|
803
|
+
if (interruptReason) {
|
|
804
|
+
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (message.triggerMeta) {
|
|
811
|
+
if (interruptReason) {
|
|
812
|
+
this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, reason: 'interrupted' });
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
816
|
+
}
|
|
817
|
+
// Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
|
|
818
|
+
if (session.sessionMode === 'autonomous') {
|
|
819
|
+
this.sessionManager.unbindSession(session.id).catch(() => { });
|
|
820
|
+
}
|
|
821
|
+
}
|
|
723
822
|
await this.sessionManager.recordSuccess(session.id);
|
|
724
823
|
this.eventBus.publish({
|
|
725
|
-
type: '
|
|
824
|
+
type: 'task:completed',
|
|
726
825
|
sessionId: session.id,
|
|
727
826
|
channel: message.channel,
|
|
728
827
|
channelId: message.channelId,
|
|
@@ -740,12 +839,30 @@ export class MessageProcessor {
|
|
|
740
839
|
status: 'completed',
|
|
741
840
|
duration: Date.now() - startTime
|
|
742
841
|
});
|
|
842
|
+
// 写入消息记录(出方向)
|
|
843
|
+
if (streamResult.lastReplyText || streamResult.fullText) {
|
|
844
|
+
const chatDir = this.sessionManager.getChatDir(session);
|
|
845
|
+
appendMessageLog(chatDir, buildOutboundEntry({
|
|
846
|
+
from: message.selfId || session.selfId || 'self',
|
|
847
|
+
to: message.peerId || message.channelId,
|
|
848
|
+
chatType: (message.chatType || session.chatType || 'private'),
|
|
849
|
+
groupId: session.metadata?.groupId ?? null,
|
|
850
|
+
msgId: `${messageId}_reply`,
|
|
851
|
+
content: streamResult.lastReplyText || streamResult.fullText,
|
|
852
|
+
replyTo: message.messageId ?? null,
|
|
853
|
+
agent: session.agentId || null,
|
|
854
|
+
model: agent.getModel?.() || null,
|
|
855
|
+
durationMs: Date.now() - startTime,
|
|
856
|
+
numTurns: streamResult.numTurns,
|
|
857
|
+
usage: streamResult.usage,
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
743
860
|
}
|
|
744
861
|
const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
745
|
-
if (isFinallyBackground) {
|
|
862
|
+
if (isFinallyBackground && session.sessionMode !== 'autonomous') {
|
|
746
863
|
const projectName = path.basename(session.projectPath);
|
|
747
864
|
const count = this.messageCache.getCount(session.id);
|
|
748
|
-
await adapter.
|
|
865
|
+
await adapter.send(envelope, { kind: 'system.notice', text: `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, subtype: 'background' });
|
|
749
866
|
}
|
|
750
867
|
// 记录发送响应
|
|
751
868
|
logger.message({
|
|
@@ -774,10 +891,12 @@ export class MessageProcessor {
|
|
|
774
891
|
: 'error';
|
|
775
892
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
776
893
|
if (!isUserInterrupt) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
894
|
+
const statusPayload = procStatus === 'timeout'
|
|
895
|
+
? { kind: 'status.timeout' }
|
|
896
|
+
: procStatus === 'interrupted'
|
|
897
|
+
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
898
|
+
: { kind: 'status.error' };
|
|
899
|
+
adapter.send(envelope, statusPayload).catch(() => { });
|
|
781
900
|
}
|
|
782
901
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
783
902
|
if (isUserInterrupt) {
|
|
@@ -789,7 +908,7 @@ export class MessageProcessor {
|
|
|
789
908
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
790
909
|
const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
|
|
791
910
|
this.eventBus.publish({
|
|
792
|
-
type: '
|
|
911
|
+
type: 'task:error',
|
|
793
912
|
sessionId: session.id,
|
|
794
913
|
error: errorMsg,
|
|
795
914
|
errorType,
|
|
@@ -808,7 +927,7 @@ export class MessageProcessor {
|
|
|
808
927
|
}
|
|
809
928
|
// 发送用户友好的错误消息(SDK_TIMEOUT 已在 kill 级别发过提示,跳过)
|
|
810
929
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
|
|
811
|
-
// processEventStream 已通过
|
|
930
|
+
// processEventStream 已通过 renderer 发过错误时也跳过
|
|
812
931
|
if (error instanceof Error && error.message === 'SDK_TIMEOUT') {
|
|
813
932
|
logger.info(`[MessageProcessor] SDK_TIMEOUT error, skip sending duplicate message`);
|
|
814
933
|
}
|
|
@@ -816,14 +935,14 @@ export class MessageProcessor {
|
|
|
816
935
|
logger.info(`[MessageProcessor] User interrupt by new_message, skip sending error message`);
|
|
817
936
|
}
|
|
818
937
|
else if (error?._errorAlreadySent) {
|
|
819
|
-
logger.info(`[MessageProcessor] Error already sent via
|
|
938
|
+
logger.info(`[MessageProcessor] Error already sent via renderer, skip sending duplicate message`);
|
|
820
939
|
}
|
|
821
940
|
else {
|
|
822
941
|
const userMessage = getErrorMessage(error, undefined);
|
|
823
942
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
824
943
|
let sendOpts;
|
|
825
944
|
try {
|
|
826
|
-
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.
|
|
945
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId);
|
|
827
946
|
sendOpts = this.getReplyContext(message);
|
|
828
947
|
}
|
|
829
948
|
catch { }
|
|
@@ -832,15 +951,8 @@ export class MessageProcessor {
|
|
|
832
951
|
...(sendOpts ?? {}),
|
|
833
952
|
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
834
953
|
};
|
|
835
|
-
await adapter.
|
|
954
|
+
await adapter.send({ ...envelope, replyContext: sendOpts }, { kind: 'result.text', text: userMessage, isFinal: true });
|
|
836
955
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
837
|
-
if (thoughtEmitter) {
|
|
838
|
-
const thoughtErrorType = errType === ErrorType.CONTEXT_TOO_LONG ? 'context_too_long' :
|
|
839
|
-
errType === ErrorType.AUTH_ERROR ? 'auth' :
|
|
840
|
-
(errType === ErrorType.SDK_TIMEOUT || errType === ErrorType.STREAM_ERROR) ? 'network' :
|
|
841
|
-
'unknown';
|
|
842
|
-
thoughtEmitter.emit({ type: 'error', error: userMessage, errorType: thoughtErrorType }).catch(() => { });
|
|
843
|
-
}
|
|
844
956
|
}
|
|
845
957
|
}
|
|
846
958
|
}
|
|
@@ -852,7 +964,29 @@ export class MessageProcessor {
|
|
|
852
964
|
const metadata = (message.threadId && message.replyContext)
|
|
853
965
|
? { replyContext: message.replyContext }
|
|
854
966
|
: undefined;
|
|
855
|
-
const
|
|
967
|
+
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
968
|
+
// --session silent 触发器:新建独立 autonomous 会话,与原会话历史隔离
|
|
969
|
+
if (message.triggerMeta?.silent) {
|
|
970
|
+
const prevActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
|
|
971
|
+
const session = await this.sessionManager.createNewSession(message.channel, message.channelId, projectPath, `trigger-${message.triggerMeta.triggerId.slice(0, 8)}`);
|
|
972
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: 'autonomous' });
|
|
973
|
+
session.sessionMode = 'autonomous';
|
|
974
|
+
if (prevActive) {
|
|
975
|
+
await this.sessionManager.switchToSession(message.channel, message.channelId, prevActive.id);
|
|
976
|
+
}
|
|
977
|
+
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
978
|
+
? session.projectPath
|
|
979
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
980
|
+
return { session, absoluteProjectPath };
|
|
981
|
+
}
|
|
982
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
|
|
983
|
+
// 兜底纠正:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
984
|
+
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
985
|
+
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
986
|
+
logger.info(`[MessageProcessor] proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive (peerType=${message.peerType})`);
|
|
987
|
+
session.sessionMode = 'proactive';
|
|
988
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
989
|
+
}
|
|
856
990
|
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
857
991
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
858
992
|
? session.projectPath
|
|
@@ -865,14 +999,16 @@ export class MessageProcessor {
|
|
|
865
999
|
* 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
|
|
866
1000
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
867
1001
|
*/
|
|
868
|
-
async processEventStream(stream, session,
|
|
1002
|
+
async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
|
|
869
1003
|
// Per-session agent name for stats bucketing
|
|
870
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '
|
|
1004
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
|
|
871
1005
|
let hasReceivedText = false;
|
|
872
1006
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
873
1007
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
874
1008
|
// 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
|
|
875
1009
|
let lastReplyText = '';
|
|
1010
|
+
// callId → description 映射,用于 tool_result 回显描述
|
|
1011
|
+
const toolDescByCallId = new Map();
|
|
876
1012
|
try {
|
|
877
1013
|
for await (const event of stream) {
|
|
878
1014
|
// 每收到事件重置空闲超时
|
|
@@ -906,10 +1042,8 @@ export class MessageProcessor {
|
|
|
906
1042
|
else {
|
|
907
1043
|
logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
908
1044
|
}
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
thoughtEmitter.emit(event).catch(() => { });
|
|
912
|
-
}
|
|
1045
|
+
// IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
|
|
1046
|
+
renderer.emit(event);
|
|
913
1047
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
914
1048
|
if (event.type === 'session_id') {
|
|
915
1049
|
logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -918,14 +1052,14 @@ export class MessageProcessor {
|
|
|
918
1052
|
// session 状态变更(idle/running/requires_action)
|
|
919
1053
|
if (event.type === 'state_changed') {
|
|
920
1054
|
logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
|
|
921
|
-
this.eventBus.publish({ type: '
|
|
1055
|
+
this.eventBus.publish({ type: 'runner:state-changed', sessionId: session.id, state: event.state });
|
|
922
1056
|
continue;
|
|
923
1057
|
}
|
|
924
1058
|
// agent 状态通知(仅事件,不直出给用户)
|
|
925
1059
|
if (event.type === 'status') {
|
|
926
1060
|
logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
|
|
927
1061
|
this.eventBus.publish({
|
|
928
|
-
type: '
|
|
1062
|
+
type: 'runner:status',
|
|
929
1063
|
sessionId: session.id,
|
|
930
1064
|
subtype: event.subtype,
|
|
931
1065
|
message: event.message,
|
|
@@ -942,14 +1076,14 @@ export class MessageProcessor {
|
|
|
942
1076
|
lastReplyText += event.text;
|
|
943
1077
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
|
|
944
1078
|
if (!shouldSuppress()) {
|
|
945
|
-
|
|
1079
|
+
renderer.addText(event.text);
|
|
946
1080
|
}
|
|
947
1081
|
}
|
|
948
1082
|
// compact 完成
|
|
949
1083
|
if (event.type === 'compact') {
|
|
950
|
-
this.eventBus.publish({ type: '
|
|
1084
|
+
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
951
1085
|
if (!shouldSuppress()) {
|
|
952
|
-
|
|
1086
|
+
renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`, 'info', 'compact');
|
|
953
1087
|
}
|
|
954
1088
|
}
|
|
955
1089
|
// 子任务进度
|
|
@@ -958,15 +1092,19 @@ export class MessageProcessor {
|
|
|
958
1092
|
const duration = event.durationMs ? `${Math.round(event.durationMs / 1000)}s` : '';
|
|
959
1093
|
const stats = [tools > 0 ? `${tools}\u6b21\u5de5\u5177\u8c03\u7528` : '', duration].filter(Boolean).join(', ');
|
|
960
1094
|
if (event.summary && !shouldSuppress()) {
|
|
961
|
-
|
|
1095
|
+
renderer.addProgress(`\u5b50\u4efb\u52a1: ${event.summary}${stats ? ` (${stats})` : ''}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
|
|
962
1096
|
}
|
|
963
1097
|
else if (stats && !shouldSuppress()) {
|
|
964
|
-
|
|
1098
|
+
renderer.addProgress(`\u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
|
|
965
1099
|
}
|
|
966
1100
|
}
|
|
967
1101
|
// 工具调用
|
|
968
1102
|
if (event.type === 'tool_use') {
|
|
969
|
-
//
|
|
1103
|
+
// 工具调用意味着当前 turn 结束,flush 已累积的文本作为独立消息
|
|
1104
|
+
if (renderer.hasTextPending()) {
|
|
1105
|
+
await renderer.flushText();
|
|
1106
|
+
}
|
|
1107
|
+
// 重置最后回复追踪
|
|
970
1108
|
lastReplyText = '';
|
|
971
1109
|
this.eventBus.publish({
|
|
972
1110
|
type: 'tool:use',
|
|
@@ -977,7 +1115,10 @@ export class MessageProcessor {
|
|
|
977
1115
|
});
|
|
978
1116
|
if (!shouldSuppress()) {
|
|
979
1117
|
const desc = summarizeToolInput(event.name, event.input || {});
|
|
980
|
-
|
|
1118
|
+
if (event.callId) {
|
|
1119
|
+
toolDescByCallId.set(event.callId, desc);
|
|
1120
|
+
}
|
|
1121
|
+
renderer.addToolCall(event.name, event.input, event.callId, desc);
|
|
981
1122
|
}
|
|
982
1123
|
}
|
|
983
1124
|
// 工具结果
|
|
@@ -991,50 +1132,63 @@ export class MessageProcessor {
|
|
|
991
1132
|
agentName: agentNameForStats,
|
|
992
1133
|
timestamp: Date.now()
|
|
993
1134
|
});
|
|
1135
|
+
// 从 tool_use 阶段缓存的描述中回溯
|
|
1136
|
+
const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
|
|
994
1137
|
if (event.isError && !shouldSuppress()) {
|
|
995
1138
|
hasErrorResult = true;
|
|
996
1139
|
let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
|
|
997
1140
|
// 移除 XML 风格的错误标签
|
|
998
1141
|
errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
|
|
999
|
-
|
|
1142
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, undefined, cachedDesc);
|
|
1143
|
+
}
|
|
1144
|
+
else if (!event.isError && !shouldSuppress()) {
|
|
1145
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, undefined, cachedDesc);
|
|
1000
1146
|
}
|
|
1001
1147
|
}
|
|
1002
1148
|
// 运行时错误(Codex: turn.failed / item error)
|
|
1003
1149
|
if (event.type === 'error') {
|
|
1004
1150
|
logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
|
|
1005
|
-
|
|
1151
|
+
// 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
|
|
1152
|
+
lastReplyText += event.error || '';
|
|
1153
|
+
// 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
|
|
1154
|
+
const isContextError = /prompt is too long|input is too long|上下文过长/i.test(event.error || '');
|
|
1155
|
+
if (!isContextError && !hasErrorResult && !shouldSuppress()) {
|
|
1006
1156
|
hasErrorResult = true;
|
|
1007
|
-
|
|
1157
|
+
renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
|
|
1008
1158
|
}
|
|
1009
1159
|
}
|
|
1010
1160
|
// 完成事件
|
|
1011
1161
|
// SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
|
|
1012
1162
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
1013
1163
|
if (event.type === 'complete') {
|
|
1014
|
-
logger.
|
|
1164
|
+
logger.info(`[MessageProcessor] complete event: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
1015
1165
|
// 自动回填会话名称
|
|
1016
1166
|
if (event.sessionTitle && session.name === '默认会话') {
|
|
1017
1167
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
1018
1168
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1019
1169
|
}
|
|
1020
1170
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1021
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
|
|
1171
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1022
1172
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
1023
1173
|
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
1174
|
+
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
1024
1175
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
1025
1176
|
const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
|
|
1026
|
-
|
|
1177
|
+
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
1178
|
+
|| /prompt is too long|input is too long|上下文过长/i.test(event.errors?.join(' ') || '')
|
|
1179
|
+
|| /prompt is too long|input is too long|上下文过长/i.test(lastReplyText);
|
|
1180
|
+
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1027
1181
|
const errorSummary = event.errors?.join('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
|
|
1028
1182
|
// 使用 terminalReason 提供更友好的错误提示
|
|
1029
1183
|
const userFriendlyMessage = event.terminalReason
|
|
1030
1184
|
? getErrorMessage(null, event.terminalReason)
|
|
1031
1185
|
: `\u274c ${errorSummary}`;
|
|
1032
|
-
|
|
1186
|
+
renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
|
|
1033
1187
|
}
|
|
1034
1188
|
// 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
|
|
1035
1189
|
// 最终文本留给流结束后的统一 flush(true)
|
|
1036
|
-
if (
|
|
1037
|
-
await
|
|
1190
|
+
if (renderer.hasContent()) {
|
|
1191
|
+
await renderer.flushActivitiesOnly();
|
|
1038
1192
|
}
|
|
1039
1193
|
}
|
|
1040
1194
|
continue;
|
|
@@ -1055,7 +1209,7 @@ export class MessageProcessor {
|
|
|
1055
1209
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1056
1210
|
}
|
|
1057
1211
|
// 记录完成状态
|
|
1058
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
|
|
1212
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1059
1213
|
if (event.subtype === 'success') {
|
|
1060
1214
|
this.messageCache.addEvent(session.id, {
|
|
1061
1215
|
type: 'completed',
|
|
@@ -1068,7 +1222,7 @@ export class MessageProcessor {
|
|
|
1068
1222
|
});
|
|
1069
1223
|
// 后台任务完成也纳入统计
|
|
1070
1224
|
this.eventBus.publish({
|
|
1071
|
-
type: '
|
|
1225
|
+
type: 'task:completed',
|
|
1072
1226
|
sessionId: session.id,
|
|
1073
1227
|
channel: session.channel,
|
|
1074
1228
|
channelId: session.channelId,
|
|
@@ -1090,7 +1244,7 @@ export class MessageProcessor {
|
|
|
1090
1244
|
});
|
|
1091
1245
|
// 后台任务失败也纳入统计
|
|
1092
1246
|
this.eventBus.publish({
|
|
1093
|
-
type: '
|
|
1247
|
+
type: 'task:error',
|
|
1094
1248
|
sessionId: session.id,
|
|
1095
1249
|
error: event.errors?.join('; ') || '\u672a\u77e5\u9519\u8bef',
|
|
1096
1250
|
errorType: bgErrorType,
|
|
@@ -1102,9 +1256,16 @@ export class MessageProcessor {
|
|
|
1102
1256
|
catch (error) {
|
|
1103
1257
|
// User interrupt (AbortError) is expected, log at info level
|
|
1104
1258
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
1105
|
-
const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop';
|
|
1259
|
+
const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop' || catchInterruptReason === 'recalled';
|
|
1106
1260
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
1107
1261
|
logger.info('[MessageProcessor] Stream interrupted (AbortError)');
|
|
1262
|
+
// User-initiated interrupt: skip flush — new task takes over the channel,
|
|
1263
|
+
// flushing here would send a spurious "最终回复" before the new task's output
|
|
1264
|
+
if (catchIsUserInterrupt) {
|
|
1265
|
+
completeResult.isError = false;
|
|
1266
|
+
completeResult.hasReceivedText = hasReceivedText;
|
|
1267
|
+
return completeResult;
|
|
1268
|
+
}
|
|
1108
1269
|
}
|
|
1109
1270
|
else if (catchIsUserInterrupt) {
|
|
1110
1271
|
// SDK telemetry noise after user-initiated interrupt — not a real error
|
|
@@ -1121,13 +1282,13 @@ export class MessageProcessor {
|
|
|
1121
1282
|
logger.error('[MessageProcessor] Stream processing error:', error);
|
|
1122
1283
|
}
|
|
1123
1284
|
if (error instanceof Error && error.message.includes('process exited')) {
|
|
1124
|
-
|
|
1285
|
+
renderer.addNotice('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5', 'warn', 'process-exit', true);
|
|
1125
1286
|
}
|
|
1126
1287
|
// Flush any pending error activities before re-throwing,
|
|
1127
1288
|
// and mark the error so outer catch won't send a duplicate message
|
|
1128
|
-
if (hasErrorResult ||
|
|
1289
|
+
if (hasErrorResult || renderer.hasContent()) {
|
|
1129
1290
|
try {
|
|
1130
|
-
await
|
|
1291
|
+
await renderer.flush(true);
|
|
1131
1292
|
}
|
|
1132
1293
|
catch { }
|
|
1133
1294
|
if (error instanceof Error) {
|
|
@@ -1261,8 +1422,65 @@ export class MessageProcessor {
|
|
|
1261
1422
|
if (/^[.\s\u2026]+$/.test(filePath))
|
|
1262
1423
|
return true;
|
|
1263
1424
|
// 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
|
|
1264
|
-
if (/[
|
|
1425
|
+
if (/[\[\]{}*+?|^$]/.test(filePath))
|
|
1265
1426
|
return true;
|
|
1266
1427
|
return false;
|
|
1267
1428
|
}
|
|
1268
1429
|
}
|
|
1430
|
+
// ── 出站协议辅助:buildEnvelope / sendInteractionPayload ──
|
|
1431
|
+
// Phase 3 of outbound unification: callers (permission flow, CommandHandler
|
|
1432
|
+
// interaction cards, claude-runner AskUserQuestion / ExitPlanMode) should
|
|
1433
|
+
// produce `{ kind: 'interaction', interaction, fallbackText }` and dispatch
|
|
1434
|
+
// via `adapter.send(envelope, payload)` instead of calling
|
|
1435
|
+
// `adapter.sendInteraction(...)` directly. These helpers centralise the
|
|
1436
|
+
// indirection and provide a backwards-compatible fallback path for adapters
|
|
1437
|
+
// that do not yet implement `send`.
|
|
1438
|
+
/**
|
|
1439
|
+
* Default fallback text for an InteractionRequest. Used when the caller
|
|
1440
|
+
* does not supply one explicitly. Picks the appropriate renderer based on
|
|
1441
|
+
* the interaction kind.
|
|
1442
|
+
*/
|
|
1443
|
+
export function defaultFallbackText(interaction) {
|
|
1444
|
+
const kind = interaction.kind;
|
|
1445
|
+
if (kind.kind === 'command-card') {
|
|
1446
|
+
return renderCommandCardAsText(kind);
|
|
1447
|
+
}
|
|
1448
|
+
if (kind.kind === 'action') {
|
|
1449
|
+
try {
|
|
1450
|
+
return renderActionAsText(interaction);
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
// ActionInteraction without fallback metadata — produce a minimal hint
|
|
1454
|
+
const action = kind;
|
|
1455
|
+
const lines = [action.title];
|
|
1456
|
+
if (action.body)
|
|
1457
|
+
lines.push(action.body);
|
|
1458
|
+
return lines.join('\n');
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return '';
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Send an interaction payload through the unified `adapter.send` entrypoint.
|
|
1465
|
+
*
|
|
1466
|
+
* Sends an interaction via adapter.send(envelope, { kind: 'interaction', ... }).
|
|
1467
|
+
* Returns 'sent' on success, false on failure.
|
|
1468
|
+
*/
|
|
1469
|
+
export async function sendInteractionPayload(adapter, envelope, interaction, fallbackText, replyCtx) {
|
|
1470
|
+
const text = fallbackText ?? defaultFallbackText(interaction);
|
|
1471
|
+
const payload = {
|
|
1472
|
+
kind: 'interaction',
|
|
1473
|
+
interaction,
|
|
1474
|
+
fallbackText: text || undefined,
|
|
1475
|
+
};
|
|
1476
|
+
try {
|
|
1477
|
+
const enriched = replyCtx
|
|
1478
|
+
? { ...envelope, replyContext: replyCtx }
|
|
1479
|
+
: envelope;
|
|
1480
|
+
await adapter.send(enriched, payload);
|
|
1481
|
+
return 'sent';
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
}
|