evolclaw 2.8.3 → 3.0.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/dist/agents/claude-runner.js +102 -38
- package/dist/agents/codex-runner.js +11 -14
- package/dist/agents/gemini-runner.js +10 -12
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -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 +1051 -288
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +858 -847
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +309 -142
- package/dist/core/message/message-queue.js +3 -3
- package/dist/core/permission.js +21 -8
- 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 +704 -775
- 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/{templates → data}/prompts.md +34 -1
- package/dist/index.js +431 -275
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- 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/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
|
@@ -2,32 +2,55 @@ 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
13
|
import { renderPromptSection } from '../../agents/templates.js';
|
|
14
|
+
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
|
+
/**
|
|
16
|
+
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
17
|
+
*
|
|
18
|
+
* 用于所有走 adapter.send 的出站路径:
|
|
19
|
+
* - 任务流内的 IMRenderer 投影(chatmode 由会话决定)
|
|
20
|
+
* - 命令回显(MessageBridge.handleCommand,taskId 用合成 ID `cmd-...`)
|
|
21
|
+
* - 网关层系统通知(src/index.ts,taskId 用 `system-...` / `restart-...` 等便于 events.log 关联)
|
|
22
|
+
*
|
|
23
|
+
* 注意:
|
|
24
|
+
* - chatmode 缺省 `'interactive'`(系统通知 / 命令回显都属于同步交互);
|
|
25
|
+
* - timestamp 可由调用方注入(便于测试),缺省 `Date.now()`。
|
|
26
|
+
*/
|
|
27
|
+
export function buildEnvelope(opts) {
|
|
28
|
+
return {
|
|
29
|
+
taskId: opts.taskId ?? `interaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
30
|
+
channel: opts.channel,
|
|
31
|
+
channelId: opts.channelId,
|
|
32
|
+
agentName: opts.agentName ?? '<unknown>',
|
|
33
|
+
chatmode: opts.chatmode ?? 'interactive',
|
|
34
|
+
replyContext: opts.replyContext,
|
|
35
|
+
timestamp: opts.timestamp ?? Date.now(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
15
38
|
/**
|
|
16
39
|
* 统一消息处理器
|
|
17
40
|
* 负责处理来自不同渠道的消息,协调事件流处理
|
|
18
41
|
*/
|
|
19
42
|
export class MessageProcessor {
|
|
20
43
|
sessionManager;
|
|
21
|
-
|
|
44
|
+
globalSettings;
|
|
22
45
|
messageCache;
|
|
23
46
|
eventBus;
|
|
24
47
|
commandHandler;
|
|
25
48
|
channels = new Map();
|
|
26
49
|
channelTypeMap = new Map(); // channelType → channelName(首个实例)
|
|
27
|
-
|
|
50
|
+
currentRenderer;
|
|
28
51
|
shouldSuppressActivities = false;
|
|
29
52
|
agentMap;
|
|
30
|
-
|
|
53
|
+
primaryRunnerKey;
|
|
31
54
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
32
55
|
interactionRouter;
|
|
33
56
|
messageQueue;
|
|
@@ -38,18 +61,18 @@ export class MessageProcessor {
|
|
|
38
61
|
* - `channel` is used to look up the owning EvolAgent (via registry).
|
|
39
62
|
* - `baseagent` (e.g. 'claude') comes from `session.agentId`.
|
|
40
63
|
*
|
|
41
|
-
* Falls back to `
|
|
64
|
+
* Falls back to `primaryRunnerKey` (a composite key, e.g. `aid::claude`)
|
|
42
65
|
* when no match is found.
|
|
43
66
|
*/
|
|
44
67
|
getAgent(channel, baseagent) {
|
|
45
68
|
if (channel && baseagent) {
|
|
46
|
-
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '
|
|
69
|
+
const evolName = this.agentRegistry?.resolveByChannel(channel)?.name || '<unknown>';
|
|
47
70
|
const key = `${evolName}::${baseagent}`;
|
|
48
71
|
if (this.agentMap.has(key))
|
|
49
72
|
return this.agentMap.get(key);
|
|
50
73
|
}
|
|
51
|
-
if (this.agentMap.has(this.
|
|
52
|
-
return this.agentMap.get(this.
|
|
74
|
+
if (this.agentMap.has(this.primaryRunnerKey))
|
|
75
|
+
return this.agentMap.get(this.primaryRunnerKey);
|
|
53
76
|
return this.agentMap.values().next().value;
|
|
54
77
|
}
|
|
55
78
|
/** 获取可用 agent 列表 */
|
|
@@ -63,23 +86,23 @@ export class MessageProcessor {
|
|
|
63
86
|
const active = await this.sessionManager.getActiveSession(channel, channelId);
|
|
64
87
|
return active ? session.id !== active.id : false;
|
|
65
88
|
}
|
|
66
|
-
constructor(agentRunnerOrMap, sessionManager,
|
|
89
|
+
constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
|
|
67
90
|
this.sessionManager = sessionManager;
|
|
68
|
-
this.
|
|
91
|
+
this.globalSettings = globalSettings;
|
|
69
92
|
this.messageCache = messageCache;
|
|
70
93
|
this.eventBus = eventBus;
|
|
71
94
|
this.commandHandler = commandHandler;
|
|
72
95
|
if (agentRunnerOrMap instanceof Map) {
|
|
73
96
|
this.agentMap = agentRunnerOrMap;
|
|
74
|
-
this.
|
|
97
|
+
this.primaryRunnerKey = primaryRunnerKey || '<unknown>::claude';
|
|
75
98
|
}
|
|
76
99
|
else {
|
|
77
|
-
//
|
|
78
|
-
this.agentMap = new Map([[
|
|
79
|
-
this.
|
|
100
|
+
// 测试 / 单 runner 路径:占位 agent name 用 '<unknown>'
|
|
101
|
+
this.agentMap = new Map([[`<unknown>::${agentRunnerOrMap.name}`, agentRunnerOrMap]]);
|
|
102
|
+
this.primaryRunnerKey = `<unknown>::${agentRunnerOrMap.name}`;
|
|
80
103
|
}
|
|
81
104
|
// 监听中断事件,标记被中断的 session
|
|
82
|
-
this.eventBus.subscribe('
|
|
105
|
+
this.eventBus.subscribe('task:interrupted', (event) => {
|
|
83
106
|
if ('sessionId' in event && event.sessionId) {
|
|
84
107
|
this.interruptedSessions.set(event.sessionId, event.reason || 'unknown');
|
|
85
108
|
}
|
|
@@ -101,7 +124,7 @@ export class MessageProcessor {
|
|
|
101
124
|
const agent = this.agentRegistry.resolveByChannel(channelName);
|
|
102
125
|
if (!agent)
|
|
103
126
|
return null;
|
|
104
|
-
const globalCm = this.config
|
|
127
|
+
const globalCm = this.agentRegistry?.resolveByChannel(channelName)?.config?.chatmode;
|
|
105
128
|
return agent.getContext(channelName, chatType, globalCm);
|
|
106
129
|
}
|
|
107
130
|
/**
|
|
@@ -132,10 +155,10 @@ export class MessageProcessor {
|
|
|
132
155
|
*/
|
|
133
156
|
handleCompactStart(sessionId) {
|
|
134
157
|
if (sessionId) {
|
|
135
|
-
this.eventBus.publish({ type: '
|
|
158
|
+
this.eventBus.publish({ type: 'runner:compact-start', sessionId });
|
|
136
159
|
}
|
|
137
|
-
if (this.
|
|
138
|
-
this.
|
|
160
|
+
if (this.currentRenderer && !this.shouldSuppressActivities) {
|
|
161
|
+
this.currentRenderer.addNotice('\u23f3 会话压缩中...', 'info', 'compact-start', true);
|
|
139
162
|
}
|
|
140
163
|
}
|
|
141
164
|
/**
|
|
@@ -170,7 +193,7 @@ export class MessageProcessor {
|
|
|
170
193
|
* 处理消息(主入口)
|
|
171
194
|
*/
|
|
172
195
|
async processMessage(message) {
|
|
173
|
-
const idleMs = (this.
|
|
196
|
+
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
174
197
|
// 先解析会话,再优先用 session.metadata.channelName 精确定位实例级 adapter
|
|
175
198
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
176
199
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
@@ -184,6 +207,7 @@ export class MessageProcessor {
|
|
|
184
207
|
const streamKey = session.id;
|
|
185
208
|
const chatType = message.chatType || 'private';
|
|
186
209
|
const identityRole = session.identity?.role || 'anonymous';
|
|
210
|
+
const agentNameForMonitor = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
187
211
|
// Resolve agent context from registry (Phase 2 foundation)
|
|
188
212
|
const agentContext = this.getAgentContext(channelKey, chatType);
|
|
189
213
|
if (agentContext) {
|
|
@@ -191,7 +215,7 @@ export class MessageProcessor {
|
|
|
191
215
|
}
|
|
192
216
|
// 按 session.agentId 选择 agent 后端
|
|
193
217
|
const agent = this.getAgent(channelKey, session.agentId);
|
|
194
|
-
const monitorEnabled = this.
|
|
218
|
+
const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
|
|
195
219
|
const showIdleMonitor = policy.showIdleMonitor(chatType, identityRole);
|
|
196
220
|
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
197
221
|
const shouldSuppress = () => {
|
|
@@ -217,13 +241,13 @@ export class MessageProcessor {
|
|
|
217
241
|
while (result) {
|
|
218
242
|
if (result.action === 'kill') {
|
|
219
243
|
logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
220
|
-
this.eventBus.publish({ type: '
|
|
244
|
+
this.eventBus.publish({ type: 'runner:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
|
|
221
245
|
// 后台任务也需要中断(释放资源),但不发送通知
|
|
222
246
|
if (channelInfo && !isBackground) {
|
|
223
247
|
const msg = showIdleMonitor
|
|
224
248
|
? result.message
|
|
225
249
|
: `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
|
|
226
|
-
channelInfo.adapter.
|
|
250
|
+
channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: msg, subtype: 'health' }).catch(e => {
|
|
227
251
|
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
228
252
|
});
|
|
229
253
|
}
|
|
@@ -239,7 +263,7 @@ export class MessageProcessor {
|
|
|
239
263
|
logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
240
264
|
if (channelInfo && showIdleMonitor && !shouldSuppress()) {
|
|
241
265
|
if (!isBackground) {
|
|
242
|
-
channelInfo.adapter.
|
|
266
|
+
channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: result.message, subtype: 'health' }).catch(e => {
|
|
243
267
|
logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
|
|
244
268
|
});
|
|
245
269
|
}
|
|
@@ -302,8 +326,8 @@ export class MessageProcessor {
|
|
|
302
326
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
303
327
|
const channelKey = session.metadata?.channelName || message.channel;
|
|
304
328
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
305
|
-
// Per-method agent name for stats bucketing (agent.name or '
|
|
306
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '
|
|
329
|
+
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
330
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
307
331
|
if (!channelInfo) {
|
|
308
332
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
309
333
|
return;
|
|
@@ -329,8 +353,16 @@ export class MessageProcessor {
|
|
|
329
353
|
metadata: { ...(base?.metadata ?? {}), taskId, chatmode },
|
|
330
354
|
};
|
|
331
355
|
};
|
|
332
|
-
|
|
333
|
-
|
|
356
|
+
const isProactive = session.sessionMode === 'proactive';
|
|
357
|
+
const isAutonomous = session.sessionMode === 'autonomous' || message.triggerMeta?.silent === true;
|
|
358
|
+
const envelope = buildEnvelope({
|
|
359
|
+
taskId,
|
|
360
|
+
channel: message.channel,
|
|
361
|
+
channelId: message.channelId,
|
|
362
|
+
agentName: agentNameForStats,
|
|
363
|
+
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
364
|
+
replyContext: taskReplyContext(),
|
|
365
|
+
});
|
|
334
366
|
try {
|
|
335
367
|
const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
336
368
|
// 记录收到消息
|
|
@@ -360,8 +392,11 @@ export class MessageProcessor {
|
|
|
360
392
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
361
393
|
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
394
|
// 记录开始处理
|
|
363
|
-
this.eventBus.publish({ type: '
|
|
364
|
-
|
|
395
|
+
this.eventBus.publish({ type: 'task:started', sessionId: session.id });
|
|
396
|
+
// 触发器消息不发 processing status(无需通知用户)
|
|
397
|
+
if (message.source !== 'trigger') {
|
|
398
|
+
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
399
|
+
}
|
|
365
400
|
logger.message({
|
|
366
401
|
msgId: messageId,
|
|
367
402
|
sessionId: session.id,
|
|
@@ -369,42 +404,43 @@ export class MessageProcessor {
|
|
|
369
404
|
status: 'processing'
|
|
370
405
|
});
|
|
371
406
|
const startTime = Date.now();
|
|
372
|
-
// 创建
|
|
373
|
-
// 使用动态判断,确保切换项目后不会继续输出
|
|
407
|
+
// 创建 IMRenderer(统一 interactive/proactive 两条路径)
|
|
374
408
|
let firstReply = true;
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
const renderer = new IMRenderer({
|
|
410
|
+
adapter,
|
|
411
|
+
envelope,
|
|
412
|
+
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
413
|
+
suppressActivities: shouldSuppress() || isAutonomous,
|
|
414
|
+
fileMarkerPattern: options?.fileMarkerPattern,
|
|
415
|
+
diagEnabled: this.globalSettings.debug?.flusherDiag,
|
|
416
|
+
send: async (payload) => {
|
|
417
|
+
if (isAutonomous)
|
|
418
|
+
return; // autonomous session: never send to channel
|
|
419
|
+
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
420
|
+
if (isCurrentlyBackground)
|
|
421
|
+
return;
|
|
379
422
|
const opts = {};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const replyCtx = this.getReplyContext(message);
|
|
384
|
-
if (replyCtx) {
|
|
385
|
-
Object.assign(opts, replyCtx);
|
|
423
|
+
const baseReplyCtx = this.getReplyContext(message);
|
|
424
|
+
if (baseReplyCtx) {
|
|
425
|
+
Object.assign(opts, baseReplyCtx);
|
|
386
426
|
}
|
|
387
427
|
else if (firstReply && message.messageId) {
|
|
388
|
-
|
|
389
|
-
if (hasText) {
|
|
428
|
+
if (payload.kind === 'result.text' && payload.text) {
|
|
390
429
|
opts.replyToMessageId = message.messageId;
|
|
391
430
|
firstReply = false;
|
|
392
431
|
}
|
|
393
432
|
}
|
|
433
|
+
if (payload.kind === 'result.text' && payload.isFinal) {
|
|
434
|
+
opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
|
|
435
|
+
}
|
|
394
436
|
opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
this.currentFlusher = flusher;
|
|
437
|
+
const enrichedEnvelope = { ...envelope, replyContext: opts };
|
|
438
|
+
await adapter.send(enrichedEnvelope, payload);
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
this.currentRenderer = renderer;
|
|
401
442
|
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));
|
|
443
|
+
logger.info(`[MessageProcessor] proactive mode: outputs via thought.put task=${taskId}`);
|
|
408
444
|
}
|
|
409
445
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
410
446
|
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
@@ -412,7 +448,7 @@ export class MessageProcessor {
|
|
|
412
448
|
const capturedReplyContext = taskReplyContext();
|
|
413
449
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
414
450
|
agent.setSendPrompt(async (text) => {
|
|
415
|
-
await adapter.
|
|
451
|
+
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text, isFinal: true });
|
|
416
452
|
});
|
|
417
453
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
418
454
|
agent.setPermissionContext?.(session.id, {
|
|
@@ -420,6 +456,11 @@ export class MessageProcessor {
|
|
|
420
456
|
channelId: capturedChannelId,
|
|
421
457
|
replyContext: capturedReplyContext,
|
|
422
458
|
interactionRouter: this.interactionRouter,
|
|
459
|
+
userId: message.peerId || undefined,
|
|
460
|
+
channel: message.channel,
|
|
461
|
+
agentName: agentNameForStats,
|
|
462
|
+
taskId,
|
|
463
|
+
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
423
464
|
interceptNextMessage: this.messageQueue
|
|
424
465
|
? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
|
|
425
466
|
: undefined,
|
|
@@ -442,6 +483,7 @@ export class MessageProcessor {
|
|
|
442
483
|
? `【新消息插入】\n\n${message.content}\n\n【请无视之前中断继续处理】`
|
|
443
484
|
: message.content;
|
|
444
485
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
486
|
+
let effectiveSystemPrompt;
|
|
445
487
|
try {
|
|
446
488
|
// 动态构建运行时上下文提示
|
|
447
489
|
const contextParts = [];
|
|
@@ -465,9 +507,9 @@ export class MessageProcessor {
|
|
|
465
507
|
let currentCanSend = false;
|
|
466
508
|
if (!isProactive) {
|
|
467
509
|
const fileChannelTypes = new Set();
|
|
468
|
-
currentCanSend = !!channelInfo.adapter.
|
|
510
|
+
currentCanSend = !!(channelInfo.adapter.capabilities?.file);
|
|
469
511
|
for (const [, info] of this.channels) {
|
|
470
|
-
if (info.adapter.
|
|
512
|
+
if (info.adapter.capabilities?.file) {
|
|
471
513
|
fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
|
|
472
514
|
}
|
|
473
515
|
}
|
|
@@ -477,10 +519,20 @@ export class MessageProcessor {
|
|
|
477
519
|
const capParts = [];
|
|
478
520
|
if (options?.supportsImages)
|
|
479
521
|
capParts.push('图片输入');
|
|
480
|
-
if (channelInfo.adapter.
|
|
522
|
+
if (channelInfo.adapter.capabilities?.image)
|
|
481
523
|
capParts.push('图片输出');
|
|
482
|
-
if (channelInfo.adapter.
|
|
524
|
+
if (channelInfo.adapter.capabilities?.file)
|
|
483
525
|
capParts.push('文件发送');
|
|
526
|
+
// Personal layer: persona.md + working memory 注入
|
|
527
|
+
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
|
|
528
|
+
if (owningAgent) {
|
|
529
|
+
const persona = owningAgent.getPersona?.();
|
|
530
|
+
if (persona)
|
|
531
|
+
contextParts.push(persona);
|
|
532
|
+
const working = owningAgent.getWorkingMemory?.();
|
|
533
|
+
if (working)
|
|
534
|
+
contextParts.push(`[当前关注]\n${working}`);
|
|
535
|
+
}
|
|
484
536
|
contextParts.push(renderPromptSection('runtime', {
|
|
485
537
|
channel: currentChannelType,
|
|
486
538
|
project: path.basename(absoluteProjectPath),
|
|
@@ -508,7 +560,13 @@ export class MessageProcessor {
|
|
|
508
560
|
if (isProactive) {
|
|
509
561
|
contextParts.push(renderPromptSection('proactive', {}));
|
|
510
562
|
}
|
|
511
|
-
|
|
563
|
+
// 4. 触发器功能提示词(非触发器消息时注入,让 AI 知道可以使用触发器)
|
|
564
|
+
if (message.source !== 'trigger') {
|
|
565
|
+
const triggerSection = renderPromptSection('trigger', {});
|
|
566
|
+
if (triggerSection)
|
|
567
|
+
contextParts.push(triggerSection);
|
|
568
|
+
}
|
|
569
|
+
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
512
570
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
513
571
|
const MAX_RETRIES = 3;
|
|
514
572
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
@@ -518,7 +576,7 @@ export class MessageProcessor {
|
|
|
518
576
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
519
577
|
agent.registerStream(streamKey, stream);
|
|
520
578
|
streamRegistered = true;
|
|
521
|
-
streamResult = await this.processEventStream(stream, session,
|
|
579
|
+
streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
|
|
522
580
|
break; // 成功,跳出重试循环
|
|
523
581
|
}
|
|
524
582
|
catch (retryError) {
|
|
@@ -528,8 +586,8 @@ export class MessageProcessor {
|
|
|
528
586
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
529
587
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
530
588
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
531
|
-
|
|
532
|
-
await
|
|
589
|
+
renderer.addNotice(`⚠️ API 暂时不可用,${delay / 1000}秒后重试 (${attempt}/${MAX_RETRIES})...`, 'warn', 'retry', true);
|
|
590
|
+
await renderer.flush();
|
|
533
591
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
534
592
|
continue;
|
|
535
593
|
}
|
|
@@ -540,15 +598,15 @@ export class MessageProcessor {
|
|
|
540
598
|
catch (error) {
|
|
541
599
|
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && hasCompact(agent)) {
|
|
542
600
|
// 尝试 compact 压缩会话
|
|
543
|
-
|
|
544
|
-
await
|
|
601
|
+
renderer.addNotice('\u26a0\ufe0f 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
602
|
+
await renderer.flush();
|
|
545
603
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
546
604
|
if (compacted) {
|
|
547
605
|
// compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
|
|
548
|
-
|
|
549
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined,
|
|
606
|
+
renderer.addNotice('\u2705 压缩完成,正在重试...', 'info', 'compact-retry', true);
|
|
607
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
550
608
|
agent.registerStream(streamKey, retryStream);
|
|
551
|
-
streamResult = await this.processEventStream(retryStream, session,
|
|
609
|
+
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
552
610
|
}
|
|
553
611
|
else {
|
|
554
612
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -560,12 +618,12 @@ export class MessageProcessor {
|
|
|
560
618
|
}
|
|
561
619
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
562
620
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
563
|
-
// suppressed 模式下
|
|
621
|
+
// suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
|
|
564
622
|
// proactive 模式:agent 主动调用 ctl file 发送文件,跳过标记处理
|
|
565
623
|
if (!isProactive) {
|
|
566
624
|
const FILE_MARKER_RE = /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g;
|
|
567
625
|
const markerPattern = options?.fileMarkerPattern ?? FILE_MARKER_RE;
|
|
568
|
-
const flusherText =
|
|
626
|
+
const flusherText = renderer.getFinalText();
|
|
569
627
|
const fullText = flusherText.length >= (streamResult.fullText?.length || 0) ? flusherText : streamResult.fullText;
|
|
570
628
|
const fileMatches = [...fullText.matchAll(markerPattern)];
|
|
571
629
|
for (const match of fileMatches) {
|
|
@@ -596,22 +654,22 @@ export class MessageProcessor {
|
|
|
596
654
|
&& targetSpec !== currentChannelType;
|
|
597
655
|
// 跨通道仅限 owner
|
|
598
656
|
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
599
|
-
await adapter.
|
|
657
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 跨通道发送仅限管理员`, subtype: 'fatal' });
|
|
600
658
|
continue;
|
|
601
659
|
}
|
|
602
660
|
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
603
661
|
if (!fs.existsSync(resolvedPath)) {
|
|
604
662
|
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
605
|
-
await adapter.
|
|
663
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u26a0\ufe0f 文件未找到: ${filePath}`, subtype: 'fatal' });
|
|
606
664
|
continue;
|
|
607
665
|
}
|
|
608
666
|
// 找目标 adapter
|
|
609
667
|
if (!targetInfo) {
|
|
610
|
-
await adapter.
|
|
668
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 未启用或不存在`, subtype: 'channel_down' });
|
|
611
669
|
continue;
|
|
612
670
|
}
|
|
613
|
-
if (!targetInfo.adapter.
|
|
614
|
-
await adapter.
|
|
671
|
+
if (!targetInfo.adapter.capabilities?.file) {
|
|
672
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 通道 ${targetLabel} 不支持文件发送`, subtype: 'capability' });
|
|
615
673
|
continue;
|
|
616
674
|
}
|
|
617
675
|
// 找目标 channelId
|
|
@@ -619,58 +677,58 @@ export class MessageProcessor {
|
|
|
619
677
|
if (isCrossChannel) {
|
|
620
678
|
const targetAdapterName = targetInfo.adapter.channelName;
|
|
621
679
|
const targetChannelType = targetInfo.options?.channelType || targetAdapterName;
|
|
622
|
-
const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName)
|
|
680
|
+
const ownerPeerId = this.agentRegistry?.getOwner?.(targetAdapterName);
|
|
623
681
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
624
682
|
if (!targetChannelId) {
|
|
625
|
-
await adapter.
|
|
683
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, subtype: 'channel_down' });
|
|
626
684
|
continue;
|
|
627
685
|
}
|
|
628
686
|
}
|
|
629
687
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
630
688
|
try {
|
|
631
|
-
await targetInfo.adapter.
|
|
632
|
-
this.eventBus.publish({ type: '
|
|
689
|
+
await targetInfo.adapter.send(buildEnvelope({ taskId, channel: targetInfo.adapter.channelName, channelId: targetChannelId, agentName: agentNameForStats, replyContext: taskReplyContext() }), { kind: 'result.file', filePath: resolvedPath });
|
|
690
|
+
this.eventBus.publish({ type: 'runner:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
633
691
|
if (isCrossChannel) {
|
|
634
|
-
await adapter.
|
|
692
|
+
await adapter.send(envelope, { kind: 'system.notice', text: `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, subtype: 'health' });
|
|
635
693
|
}
|
|
636
694
|
}
|
|
637
695
|
catch (error) {
|
|
638
696
|
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
639
|
-
await adapter.
|
|
697
|
+
await adapter.send(envelope, { kind: 'system.error', text: `\u274c 文件发送失败: ${filePath}`, subtype: 'fatal' });
|
|
640
698
|
}
|
|
641
699
|
}
|
|
642
700
|
} // end of !isProactive
|
|
643
|
-
// 最终回复文本添加到
|
|
701
|
+
// 最终回复文本添加到 renderer(统一在流结束后处理,避免多 complete 事件重复发送)
|
|
644
702
|
// suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
|
|
645
703
|
// 非 suppressed 且无流式文本:同上
|
|
646
704
|
// 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
|
|
647
|
-
// 但如果
|
|
705
|
+
// 但如果 renderer 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
|
|
648
706
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
649
707
|
// 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
|
|
650
708
|
// 特征:无流式 text + complete.result 匹配已知模式
|
|
651
709
|
// 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
|
|
652
|
-
// Proactive 模式下
|
|
710
|
+
// Proactive 模式下 renderer silent,需要兜底发出以告知用户,否则用户完全无反馈。
|
|
653
711
|
const isSdkFallbackMessage = !!finalReplyText
|
|
654
712
|
&& !streamResult.hasReceivedText
|
|
655
713
|
&& /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
|
|
656
714
|
if (finalReplyText) {
|
|
657
715
|
if (isProactive && isSdkFallbackMessage) {
|
|
658
|
-
// Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent
|
|
716
|
+
// Proactive 模式 + SDK 本地兜底:直接 sendText 绕过 silent renderer
|
|
659
717
|
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
660
718
|
if (!isCurrentlyBackground) {
|
|
661
|
-
await adapter.
|
|
719
|
+
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
|
|
662
720
|
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
663
721
|
}
|
|
664
722
|
}
|
|
665
723
|
else if (shouldSuppress()) {
|
|
666
|
-
|
|
724
|
+
renderer.addText(finalReplyText);
|
|
667
725
|
}
|
|
668
|
-
else if (!streamResult.hasReceivedText || (!
|
|
669
|
-
|
|
726
|
+
else if (!streamResult.hasReceivedText || (!renderer.hasSentContent() && !renderer.hasContent())) {
|
|
727
|
+
renderer.addText(finalReplyText);
|
|
670
728
|
}
|
|
671
729
|
}
|
|
672
730
|
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
673
|
-
await
|
|
731
|
+
await renderer.flush(true);
|
|
674
732
|
// 清理 activeStreams(正常完成)
|
|
675
733
|
agent.cleanupStream(streamKey);
|
|
676
734
|
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
@@ -690,9 +748,14 @@ export class MessageProcessor {
|
|
|
690
748
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
691
749
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
692
750
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
693
|
-
|
|
751
|
+
if (message.source !== 'trigger') {
|
|
752
|
+
adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
753
|
+
}
|
|
754
|
+
if (message.triggerMeta) {
|
|
755
|
+
this.eventBus.publish({ type: 'trigger:failed', triggerId: message.triggerMeta.triggerId, messageId: messageId, error: errorSummary });
|
|
756
|
+
}
|
|
694
757
|
this.eventBus.publish({
|
|
695
|
-
type: '
|
|
758
|
+
type: 'task:error',
|
|
696
759
|
sessionId: session.id,
|
|
697
760
|
error: errorSummary,
|
|
698
761
|
errorType,
|
|
@@ -719,10 +782,30 @@ export class MessageProcessor {
|
|
|
719
782
|
}
|
|
720
783
|
else {
|
|
721
784
|
// 真正的成功
|
|
722
|
-
|
|
785
|
+
const durationMs = Date.now() - startTime;
|
|
786
|
+
if (message.source !== 'trigger') {
|
|
787
|
+
if (interruptReason) {
|
|
788
|
+
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs } }).catch(() => { });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (message.triggerMeta) {
|
|
795
|
+
if (interruptReason) {
|
|
796
|
+
this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, reason: 'interrupted' });
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
800
|
+
}
|
|
801
|
+
// Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
|
|
802
|
+
if (session.sessionMode === 'autonomous') {
|
|
803
|
+
this.sessionManager.unbindSession(session.id).catch(() => { });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
723
806
|
await this.sessionManager.recordSuccess(session.id);
|
|
724
807
|
this.eventBus.publish({
|
|
725
|
-
type: '
|
|
808
|
+
type: 'task:completed',
|
|
726
809
|
sessionId: session.id,
|
|
727
810
|
channel: message.channel,
|
|
728
811
|
channelId: message.channelId,
|
|
@@ -740,12 +823,28 @@ export class MessageProcessor {
|
|
|
740
823
|
status: 'completed',
|
|
741
824
|
duration: Date.now() - startTime
|
|
742
825
|
});
|
|
826
|
+
// 写入消息记录(出方向)
|
|
827
|
+
if (streamResult.lastReplyText || streamResult.fullText) {
|
|
828
|
+
const chatDir = this.sessionManager.getChatDir(session);
|
|
829
|
+
appendMessageLog(chatDir, buildOutboundEntry({
|
|
830
|
+
from: message.selfId || session.selfId || 'self',
|
|
831
|
+
to: message.peerId || message.channelId,
|
|
832
|
+
chatType: (message.chatType || session.chatType || 'private'),
|
|
833
|
+
groupId: session.metadata?.groupId ?? null,
|
|
834
|
+
msgId: `${messageId}_reply`,
|
|
835
|
+
content: streamResult.lastReplyText || streamResult.fullText,
|
|
836
|
+
replyTo: message.messageId ?? null,
|
|
837
|
+
agent: session.agentId || null,
|
|
838
|
+
model: agent.getModel?.() || null,
|
|
839
|
+
durationMs: Date.now() - startTime,
|
|
840
|
+
}));
|
|
841
|
+
}
|
|
743
842
|
}
|
|
744
843
|
const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
745
|
-
if (isFinallyBackground) {
|
|
844
|
+
if (isFinallyBackground && session.sessionMode !== 'autonomous') {
|
|
746
845
|
const projectName = path.basename(session.projectPath);
|
|
747
846
|
const count = this.messageCache.getCount(session.id);
|
|
748
|
-
await adapter.
|
|
847
|
+
await adapter.send(envelope, { kind: 'system.notice', text: `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, subtype: 'background' });
|
|
749
848
|
}
|
|
750
849
|
// 记录发送响应
|
|
751
850
|
logger.message({
|
|
@@ -774,10 +873,12 @@ export class MessageProcessor {
|
|
|
774
873
|
: 'error';
|
|
775
874
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
776
875
|
if (!isUserInterrupt) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
876
|
+
const statusPayload = procStatus === 'timeout'
|
|
877
|
+
? { kind: 'status.timeout' }
|
|
878
|
+
: procStatus === 'interrupted'
|
|
879
|
+
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
880
|
+
: { kind: 'status.error' };
|
|
881
|
+
adapter.send(envelope, statusPayload).catch(() => { });
|
|
781
882
|
}
|
|
782
883
|
// 用户主动中断时降级日志;其余仍按 error 记录
|
|
783
884
|
if (isUserInterrupt) {
|
|
@@ -789,7 +890,7 @@ export class MessageProcessor {
|
|
|
789
890
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
790
891
|
const errorType = prefixErrorType(ERROR_PREFIX.INFRA, errType);
|
|
791
892
|
this.eventBus.publish({
|
|
792
|
-
type: '
|
|
893
|
+
type: 'task:error',
|
|
793
894
|
sessionId: session.id,
|
|
794
895
|
error: errorMsg,
|
|
795
896
|
errorType,
|
|
@@ -808,7 +909,7 @@ export class MessageProcessor {
|
|
|
808
909
|
}
|
|
809
910
|
// 发送用户友好的错误消息(SDK_TIMEOUT 已在 kill 级别发过提示,跳过)
|
|
810
911
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
|
|
811
|
-
// processEventStream 已通过
|
|
912
|
+
// processEventStream 已通过 renderer 发过错误时也跳过
|
|
812
913
|
if (error instanceof Error && error.message === 'SDK_TIMEOUT') {
|
|
813
914
|
logger.info(`[MessageProcessor] SDK_TIMEOUT error, skip sending duplicate message`);
|
|
814
915
|
}
|
|
@@ -816,14 +917,14 @@ export class MessageProcessor {
|
|
|
816
917
|
logger.info(`[MessageProcessor] User interrupt by new_message, skip sending error message`);
|
|
817
918
|
}
|
|
818
919
|
else if (error?._errorAlreadySent) {
|
|
819
|
-
logger.info(`[MessageProcessor] Error already sent via
|
|
920
|
+
logger.info(`[MessageProcessor] Error already sent via renderer, skip sending duplicate message`);
|
|
820
921
|
}
|
|
821
922
|
else {
|
|
822
923
|
const userMessage = getErrorMessage(error, undefined);
|
|
823
924
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
824
925
|
let sendOpts;
|
|
825
926
|
try {
|
|
826
|
-
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.
|
|
927
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId);
|
|
827
928
|
sendOpts = this.getReplyContext(message);
|
|
828
929
|
}
|
|
829
930
|
catch { }
|
|
@@ -832,15 +933,8 @@ export class MessageProcessor {
|
|
|
832
933
|
...(sendOpts ?? {}),
|
|
833
934
|
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
834
935
|
};
|
|
835
|
-
await adapter.
|
|
936
|
+
await adapter.send({ ...envelope, replyContext: sendOpts }, { kind: 'result.text', text: userMessage, isFinal: true });
|
|
836
937
|
// 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
938
|
}
|
|
845
939
|
}
|
|
846
940
|
}
|
|
@@ -852,7 +946,22 @@ export class MessageProcessor {
|
|
|
852
946
|
const metadata = (message.threadId && message.replyContext)
|
|
853
947
|
? { replyContext: message.replyContext }
|
|
854
948
|
: undefined;
|
|
855
|
-
const
|
|
949
|
+
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
950
|
+
// --session silent 触发器:新建独立 autonomous 会话,与原会话历史隔离
|
|
951
|
+
if (message.triggerMeta?.silent) {
|
|
952
|
+
const prevActive = await this.sessionManager.getActiveSession(message.channel, message.channelId);
|
|
953
|
+
const session = await this.sessionManager.createNewSession(message.channel, message.channelId, projectPath, `trigger-${message.triggerMeta.triggerId.slice(0, 8)}`);
|
|
954
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: 'autonomous' });
|
|
955
|
+
session.sessionMode = 'autonomous';
|
|
956
|
+
if (prevActive) {
|
|
957
|
+
await this.sessionManager.switchToSession(message.channel, message.channelId, prevActive.id);
|
|
958
|
+
}
|
|
959
|
+
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
960
|
+
? session.projectPath
|
|
961
|
+
: path.resolve(process.cwd(), session.projectPath);
|
|
962
|
+
return { session, absoluteProjectPath };
|
|
963
|
+
}
|
|
964
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId);
|
|
856
965
|
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
857
966
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
858
967
|
? session.projectPath
|
|
@@ -865,9 +974,9 @@ export class MessageProcessor {
|
|
|
865
974
|
* 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
|
|
866
975
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
867
976
|
*/
|
|
868
|
-
async processEventStream(stream, session,
|
|
977
|
+
async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
|
|
869
978
|
// Per-session agent name for stats bucketing
|
|
870
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '
|
|
979
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
|
|
871
980
|
let hasReceivedText = false;
|
|
872
981
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
873
982
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -906,10 +1015,8 @@ export class MessageProcessor {
|
|
|
906
1015
|
else {
|
|
907
1016
|
logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
908
1017
|
}
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
thoughtEmitter.emit(event).catch(() => { });
|
|
912
|
-
}
|
|
1018
|
+
// IMRenderer 旁路:proactive 模式逐事件投影为 thought(fire-and-forget)
|
|
1019
|
+
renderer.emit(event);
|
|
913
1020
|
// session_id 已在 AgentRunner.transformStream 中处理,此处仅记录
|
|
914
1021
|
if (event.type === 'session_id') {
|
|
915
1022
|
logger.debug(`[MessageProcessor] Session ID updated: ${event.sessionId} for session: ${session.id}`);
|
|
@@ -918,14 +1025,14 @@ export class MessageProcessor {
|
|
|
918
1025
|
// session 状态变更(idle/running/requires_action)
|
|
919
1026
|
if (event.type === 'state_changed') {
|
|
920
1027
|
logger.debug(`[MessageProcessor] Session state: ${event.state} for session: ${session.id}`);
|
|
921
|
-
this.eventBus.publish({ type: '
|
|
1028
|
+
this.eventBus.publish({ type: 'runner:state-changed', sessionId: session.id, state: event.state });
|
|
922
1029
|
continue;
|
|
923
1030
|
}
|
|
924
1031
|
// agent 状态通知(仅事件,不直出给用户)
|
|
925
1032
|
if (event.type === 'status') {
|
|
926
1033
|
logger.debug(`[MessageProcessor] Agent status: ${event.subtype}: ${event.message}`);
|
|
927
1034
|
this.eventBus.publish({
|
|
928
|
-
type: '
|
|
1035
|
+
type: 'runner:status',
|
|
929
1036
|
sessionId: session.id,
|
|
930
1037
|
subtype: event.subtype,
|
|
931
1038
|
message: event.message,
|
|
@@ -942,14 +1049,14 @@ export class MessageProcessor {
|
|
|
942
1049
|
lastReplyText += event.text;
|
|
943
1050
|
this.eventBus.publish({ type: 'message:text', sessionId: session.id, text: event.text, isFinal: false });
|
|
944
1051
|
if (!shouldSuppress()) {
|
|
945
|
-
|
|
1052
|
+
renderer.addText(event.text);
|
|
946
1053
|
}
|
|
947
1054
|
}
|
|
948
1055
|
// compact 完成
|
|
949
1056
|
if (event.type === 'compact') {
|
|
950
|
-
this.eventBus.publish({ type: '
|
|
1057
|
+
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
951
1058
|
if (!shouldSuppress()) {
|
|
952
|
-
|
|
1059
|
+
renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...(压缩前 tokens: ${event.preTokens})`, 'info', 'compact');
|
|
953
1060
|
}
|
|
954
1061
|
}
|
|
955
1062
|
// 子任务进度
|
|
@@ -958,10 +1065,10 @@ export class MessageProcessor {
|
|
|
958
1065
|
const duration = event.durationMs ? `${Math.round(event.durationMs / 1000)}s` : '';
|
|
959
1066
|
const stats = [tools > 0 ? `${tools}\u6b21\u5de5\u5177\u8c03\u7528` : '', duration].filter(Boolean).join(', ');
|
|
960
1067
|
if (event.summary && !shouldSuppress()) {
|
|
961
|
-
|
|
1068
|
+
renderer.addProgress(`\u5b50\u4efb\u52a1: ${event.summary}${stats ? ` (${stats})` : ''}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
|
|
962
1069
|
}
|
|
963
1070
|
else if (stats && !shouldSuppress()) {
|
|
964
|
-
|
|
1071
|
+
renderer.addProgress(`\u5b50\u4efb\u52a1\u8fdb\u884c\u4e2d: ${stats}`, { state: 'processing', toolUses: event.toolUses, durationMs: event.durationMs });
|
|
965
1072
|
}
|
|
966
1073
|
}
|
|
967
1074
|
// 工具调用
|
|
@@ -977,7 +1084,7 @@ export class MessageProcessor {
|
|
|
977
1084
|
});
|
|
978
1085
|
if (!shouldSuppress()) {
|
|
979
1086
|
const desc = summarizeToolInput(event.name, event.input || {});
|
|
980
|
-
|
|
1087
|
+
renderer.addToolCall(event.name, event.input, event.callId, desc);
|
|
981
1088
|
}
|
|
982
1089
|
}
|
|
983
1090
|
// 工具结果
|
|
@@ -996,7 +1103,10 @@ export class MessageProcessor {
|
|
|
996
1103
|
let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
|
|
997
1104
|
// 移除 XML 风格的错误标签
|
|
998
1105
|
errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
|
|
999
|
-
|
|
1106
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId);
|
|
1107
|
+
}
|
|
1108
|
+
else if (!event.isError && !shouldSuppress()) {
|
|
1109
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId);
|
|
1000
1110
|
}
|
|
1001
1111
|
}
|
|
1002
1112
|
// 运行时错误(Codex: turn.failed / item error)
|
|
@@ -1004,7 +1114,7 @@ export class MessageProcessor {
|
|
|
1004
1114
|
logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
|
|
1005
1115
|
if (!hasErrorResult && !shouldSuppress()) {
|
|
1006
1116
|
hasErrorResult = true;
|
|
1007
|
-
|
|
1117
|
+
renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
|
|
1008
1118
|
}
|
|
1009
1119
|
}
|
|
1010
1120
|
// 完成事件
|
|
@@ -1029,12 +1139,12 @@ export class MessageProcessor {
|
|
|
1029
1139
|
const userFriendlyMessage = event.terminalReason
|
|
1030
1140
|
? getErrorMessage(null, event.terminalReason)
|
|
1031
1141
|
: `\u274c ${errorSummary}`;
|
|
1032
|
-
|
|
1142
|
+
renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
|
|
1033
1143
|
}
|
|
1034
1144
|
// 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
|
|
1035
1145
|
// 最终文本留给流结束后的统一 flush(true)
|
|
1036
|
-
if (
|
|
1037
|
-
await
|
|
1146
|
+
if (renderer.hasContent()) {
|
|
1147
|
+
await renderer.flushActivitiesOnly();
|
|
1038
1148
|
}
|
|
1039
1149
|
}
|
|
1040
1150
|
continue;
|
|
@@ -1068,7 +1178,7 @@ export class MessageProcessor {
|
|
|
1068
1178
|
});
|
|
1069
1179
|
// 后台任务完成也纳入统计
|
|
1070
1180
|
this.eventBus.publish({
|
|
1071
|
-
type: '
|
|
1181
|
+
type: 'task:completed',
|
|
1072
1182
|
sessionId: session.id,
|
|
1073
1183
|
channel: session.channel,
|
|
1074
1184
|
channelId: session.channelId,
|
|
@@ -1090,7 +1200,7 @@ export class MessageProcessor {
|
|
|
1090
1200
|
});
|
|
1091
1201
|
// 后台任务失败也纳入统计
|
|
1092
1202
|
this.eventBus.publish({
|
|
1093
|
-
type: '
|
|
1203
|
+
type: 'task:error',
|
|
1094
1204
|
sessionId: session.id,
|
|
1095
1205
|
error: event.errors?.join('; ') || '\u672a\u77e5\u9519\u8bef',
|
|
1096
1206
|
errorType: bgErrorType,
|
|
@@ -1121,13 +1231,13 @@ export class MessageProcessor {
|
|
|
1121
1231
|
logger.error('[MessageProcessor] Stream processing error:', error);
|
|
1122
1232
|
}
|
|
1123
1233
|
if (error instanceof Error && error.message.includes('process exited')) {
|
|
1124
|
-
|
|
1234
|
+
renderer.addNotice('\u274c Claude Code \u8fdb\u7a0b\u5f02\u5e38\u9000\u51fa\uff0c\u8bf7\u91cd\u8bd5', 'warn', 'process-exit', true);
|
|
1125
1235
|
}
|
|
1126
1236
|
// Flush any pending error activities before re-throwing,
|
|
1127
1237
|
// and mark the error so outer catch won't send a duplicate message
|
|
1128
|
-
if (hasErrorResult ||
|
|
1238
|
+
if (hasErrorResult || renderer.hasContent()) {
|
|
1129
1239
|
try {
|
|
1130
|
-
await
|
|
1240
|
+
await renderer.flush(true);
|
|
1131
1241
|
}
|
|
1132
1242
|
catch { }
|
|
1133
1243
|
if (error instanceof Error) {
|
|
@@ -1261,8 +1371,65 @@ export class MessageProcessor {
|
|
|
1261
1371
|
if (/^[.\s\u2026]+$/.test(filePath))
|
|
1262
1372
|
return true;
|
|
1263
1373
|
// 含正则/代码特殊字符(Agent 在说明中引用了代码或正则表达式)
|
|
1264
|
-
if (/[
|
|
1374
|
+
if (/[\[\]{}*+?|^$]/.test(filePath))
|
|
1265
1375
|
return true;
|
|
1266
1376
|
return false;
|
|
1267
1377
|
}
|
|
1268
1378
|
}
|
|
1379
|
+
// ── 出站协议辅助:buildEnvelope / sendInteractionPayload ──
|
|
1380
|
+
// Phase 3 of outbound unification: callers (permission flow, CommandHandler
|
|
1381
|
+
// interaction cards, claude-runner AskUserQuestion / ExitPlanMode) should
|
|
1382
|
+
// produce `{ kind: 'interaction', interaction, fallbackText }` and dispatch
|
|
1383
|
+
// via `adapter.send(envelope, payload)` instead of calling
|
|
1384
|
+
// `adapter.sendInteraction(...)` directly. These helpers centralise the
|
|
1385
|
+
// indirection and provide a backwards-compatible fallback path for adapters
|
|
1386
|
+
// that do not yet implement `send`.
|
|
1387
|
+
/**
|
|
1388
|
+
* Default fallback text for an InteractionRequest. Used when the caller
|
|
1389
|
+
* does not supply one explicitly. Picks the appropriate renderer based on
|
|
1390
|
+
* the interaction kind.
|
|
1391
|
+
*/
|
|
1392
|
+
export function defaultFallbackText(interaction) {
|
|
1393
|
+
const kind = interaction.kind;
|
|
1394
|
+
if (kind.kind === 'command-card') {
|
|
1395
|
+
return renderCommandCardAsText(kind);
|
|
1396
|
+
}
|
|
1397
|
+
if (kind.kind === 'action') {
|
|
1398
|
+
try {
|
|
1399
|
+
return renderActionAsText(interaction);
|
|
1400
|
+
}
|
|
1401
|
+
catch {
|
|
1402
|
+
// ActionInteraction without fallback metadata — produce a minimal hint
|
|
1403
|
+
const action = kind;
|
|
1404
|
+
const lines = [action.title];
|
|
1405
|
+
if (action.body)
|
|
1406
|
+
lines.push(action.body);
|
|
1407
|
+
return lines.join('\n');
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return '';
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Send an interaction payload through the unified `adapter.send` entrypoint.
|
|
1414
|
+
*
|
|
1415
|
+
* Sends an interaction via adapter.send(envelope, { kind: 'interaction', ... }).
|
|
1416
|
+
* Returns 'sent' on success, false on failure.
|
|
1417
|
+
*/
|
|
1418
|
+
export async function sendInteractionPayload(adapter, envelope, interaction, fallbackText, replyCtx) {
|
|
1419
|
+
const text = fallbackText ?? defaultFallbackText(interaction);
|
|
1420
|
+
const payload = {
|
|
1421
|
+
kind: 'interaction',
|
|
1422
|
+
interaction,
|
|
1423
|
+
fallbackText: text || undefined,
|
|
1424
|
+
};
|
|
1425
|
+
try {
|
|
1426
|
+
const enriched = replyCtx
|
|
1427
|
+
? { ...envelope, replyContext: replyCtx }
|
|
1428
|
+
: envelope;
|
|
1429
|
+
await adapter.send(enriched, payload);
|
|
1430
|
+
return 'sent';
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
}
|