evolclaw 3.1.2 → 3.1.4
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/CHANGELOG.md +38 -0
- package/README.md +2 -6
- package/assets/.env.template +4 -0
- package/assets/config.json.template +6 -0
- package/assets/wechat-group-qr.jpeg +0 -0
- package/dist/agents/claude-runner.js +1 -1
- package/dist/agents/codex-runner.js +75 -19
- package/dist/agents/gemini-runner.js +0 -2
- package/dist/agents/kit-renderer.js +85 -22
- package/dist/aun/aid/agentmd.js +67 -74
- package/dist/aun/aid/client.js +22 -7
- package/dist/aun/aid/identity.js +314 -28
- package/dist/aun/aid/index.js +2 -2
- package/dist/aun/rpc/connection.js +8 -10
- package/dist/channels/aun.js +53 -41
- package/dist/cli/agent.js +28 -28
- package/dist/cli/bench.js +8 -14
- package/dist/cli/help.js +23 -0
- package/dist/cli/index.js +398 -73
- package/dist/cli/init-channel.js +2 -3
- package/dist/cli/init.js +13 -6
- package/dist/cli/link-rules.js +2 -1
- package/dist/cli/net-check.js +10 -11
- package/dist/core/command-handler.js +621 -541
- package/dist/core/evolagent.js +31 -0
- package/dist/core/message/im-renderer.js +10 -0
- package/dist/core/message/message-bridge.js +123 -24
- package/dist/core/message/message-processor.js +61 -31
- package/dist/core/relation/peer-identity.js +64 -21
- package/dist/core/session/session-manager.js +191 -44
- package/dist/core/trigger/manager.js +37 -0
- package/dist/index.js +4 -1
- package/dist/paths.js +87 -16
- package/dist/utils/npm-ops.js +18 -11
- package/kits/eck_manifest.json +9 -9
- package/kits/rules/02-navigation.md +1 -0
- package/kits/rules/05-venue.md +2 -2
- package/kits/rules/06-channel.md +2 -18
- package/kits/templates/system-fragments/baseagent.md +8 -2
- package/kits/templates/system-fragments/channel.md +20 -8
- package/kits/templates/system-fragments/identity.md +5 -6
- package/kits/templates/system-fragments/relation.md +10 -5
- package/kits/templates/system-fragments/session.md +20 -0
- package/kits/templates/system-fragments/venue.md +5 -3
- package/package.json +4 -2
- package/dist/net-check.js +0 -640
- package/dist/watch-msg.js +0 -544
- package/kits/templates/system-fragments/runtime.md +0 -19
package/dist/core/evolagent.js
CHANGED
|
@@ -159,6 +159,15 @@ export class EvolAgent {
|
|
|
159
159
|
this.persist();
|
|
160
160
|
}
|
|
161
161
|
// ── Baseagent 字段写入 ────────────────────────────────────────────────
|
|
162
|
+
/** 切换当前活跃 baseagent(写顶层 active_baseagent)。 */
|
|
163
|
+
setActiveBaseagent(value) {
|
|
164
|
+
if (value === undefined)
|
|
165
|
+
delete this.rawAgent.active_baseagent;
|
|
166
|
+
else
|
|
167
|
+
this.rawAgent.active_baseagent = value;
|
|
168
|
+
this.merged.active_baseagent = value;
|
|
169
|
+
this.persist();
|
|
170
|
+
}
|
|
162
171
|
setBaseagentModel(value) {
|
|
163
172
|
const ba = this.baseagent;
|
|
164
173
|
if (!this.rawAgent.baseagents)
|
|
@@ -182,6 +191,28 @@ export class EvolAgent {
|
|
|
182
191
|
block[fieldName] = value;
|
|
183
192
|
this.persist();
|
|
184
193
|
}
|
|
194
|
+
/** 设置私聊 chatmode(群聊/非 human 强制 proactive,无可写入项)。 */
|
|
195
|
+
setChatmodePrivate(value) {
|
|
196
|
+
if (!this.rawAgent.chatmode)
|
|
197
|
+
this.rawAgent.chatmode = {};
|
|
198
|
+
if (value === undefined)
|
|
199
|
+
delete this.rawAgent.chatmode.private;
|
|
200
|
+
else
|
|
201
|
+
this.rawAgent.chatmode.private = value;
|
|
202
|
+
if (!this.merged.chatmode)
|
|
203
|
+
this.merged.chatmode = {};
|
|
204
|
+
this.merged.chatmode.private = value;
|
|
205
|
+
this.persist();
|
|
206
|
+
}
|
|
207
|
+
/** 设置群聊 dispatch 默认值(mention | broadcast)。 */
|
|
208
|
+
setDispatch(value) {
|
|
209
|
+
if (value === undefined)
|
|
210
|
+
delete this.rawAgent.dispatch;
|
|
211
|
+
else
|
|
212
|
+
this.rawAgent.dispatch = value;
|
|
213
|
+
this.merged.dispatch = value;
|
|
214
|
+
this.persist();
|
|
215
|
+
}
|
|
185
216
|
// ── Projects ──────────────────────────────────────────────────────────
|
|
186
217
|
getProjects() {
|
|
187
218
|
const list = this.merged.projects?.list;
|
|
@@ -142,6 +142,16 @@ export class IMRenderer {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
/** 清除上下文过长错误文本(从 buffer + allText 中移除) */
|
|
146
|
+
stripContextError(pattern) {
|
|
147
|
+
this.textBuffer = this.textBuffer.replace(pattern, '').trim();
|
|
148
|
+
this.allText = this.allText.replace(pattern, '').trim();
|
|
149
|
+
for (const item of this.itemsQueue) {
|
|
150
|
+
if (item.kind === 'text') {
|
|
151
|
+
item.text = item.text.replace(pattern, '');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
145
155
|
// ── 文本/活动注入(替代 StreamFlusher.addText/addActivity)──
|
|
146
156
|
/** 添加文本片段(流式 text) */
|
|
147
157
|
addText(text, outputTokens, turn) {
|
|
@@ -207,7 +207,28 @@ export class MessageBridge {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
|
-
|
|
210
|
+
// ── Menu Protocol ──
|
|
211
|
+
static MENU_NAME_MAP = {
|
|
212
|
+
pwd: '/pwd',
|
|
213
|
+
session: '/session',
|
|
214
|
+
baseagent: '/baseagent',
|
|
215
|
+
model: '/model',
|
|
216
|
+
effort: '/effort',
|
|
217
|
+
chatmode: '/chatmode',
|
|
218
|
+
dispatch: '/dispatch',
|
|
219
|
+
permission: '/perm',
|
|
220
|
+
activity: '/activity',
|
|
221
|
+
system: '/system',
|
|
222
|
+
};
|
|
223
|
+
resolveCmd(name, cmd) {
|
|
224
|
+
if (cmd)
|
|
225
|
+
return cmd;
|
|
226
|
+
const mapped = MessageBridge.MENU_NAME_MAP[name];
|
|
227
|
+
if (!mapped)
|
|
228
|
+
throw { code: 'UNKNOWN_NAME', message: `未知操作: ${name}` };
|
|
229
|
+
return mapped;
|
|
230
|
+
}
|
|
231
|
+
/** 自定义消息快速路径:拦截 menu.* 协议 */
|
|
211
232
|
async handleCustomPayload(content, channel, msg, sendReply, adapter) {
|
|
212
233
|
let parsed;
|
|
213
234
|
try {
|
|
@@ -218,30 +239,108 @@ export class MessageBridge {
|
|
|
218
239
|
}
|
|
219
240
|
if (!parsed || typeof parsed !== 'object' || !parsed.type)
|
|
220
241
|
return false;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
switch (parsed.type) {
|
|
243
|
+
case 'menu.list':
|
|
244
|
+
await this.handleMenuList(parsed, channel, msg, adapter, sendReply);
|
|
245
|
+
return true;
|
|
246
|
+
case 'menu.query':
|
|
247
|
+
await this.handleMenuQuery(parsed, channel, msg, adapter, sendReply);
|
|
248
|
+
return true;
|
|
249
|
+
case 'menu.options':
|
|
250
|
+
await this.handleMenuOptions(parsed, channel, msg, adapter, sendReply);
|
|
251
|
+
return true;
|
|
252
|
+
case 'menu.update':
|
|
253
|
+
await this.handleMenuUpdate(parsed, channel, msg, adapter, sendReply);
|
|
254
|
+
return true;
|
|
255
|
+
case 'menu.action':
|
|
256
|
+
await this.handleMenuAction(parsed, channel, msg, adapter, sendReply);
|
|
257
|
+
return true;
|
|
258
|
+
default:
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async handleMenuList(req, channel, msg, adapter, sendReply) {
|
|
263
|
+
const { id } = req;
|
|
264
|
+
try {
|
|
265
|
+
const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
|
|
266
|
+
const data = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
|
|
267
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, data }, sendReply);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, {
|
|
271
|
+
type: 'menu.response', id,
|
|
272
|
+
error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
|
|
273
|
+
}, sendReply);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async handleMenuQuery(req, channel, msg, adapter, sendReply) {
|
|
277
|
+
const { id, name, cmd } = req;
|
|
278
|
+
try {
|
|
279
|
+
const resolvedCmd = this.resolveCmd(name, cmd);
|
|
280
|
+
const result = await this.cmdHandler.execMenuQuery(resolvedCmd, channel, msg.channelId, msg.peerId);
|
|
281
|
+
if ('error' in result)
|
|
282
|
+
throw { code: result.code || 'EXEC_FAILED', message: result.error };
|
|
283
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, {
|
|
287
|
+
type: 'menu.response', id, name,
|
|
288
|
+
error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
|
|
289
|
+
}, sendReply);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async handleMenuOptions(req, channel, msg, adapter, sendReply) {
|
|
293
|
+
const { id, name, cmd } = req;
|
|
294
|
+
try {
|
|
295
|
+
const resolvedCmd = this.resolveCmd(name, cmd);
|
|
296
|
+
const data = await this.cmdHandler.getSubMenuItems(resolvedCmd, channel, msg.channelId, msg.peerId) ?? [];
|
|
297
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data }, sendReply);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, {
|
|
301
|
+
type: 'menu.response', id, name,
|
|
302
|
+
error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
|
|
303
|
+
}, sendReply);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async handleMenuUpdate(req, channel, msg, adapter, sendReply) {
|
|
307
|
+
const { id, name, cmd, value } = req;
|
|
308
|
+
try {
|
|
309
|
+
if (!value)
|
|
310
|
+
throw { code: 'MISSING_VALUE', message: '缺少 value 参数' };
|
|
311
|
+
const resolvedCmd = this.resolveCmd(name, cmd);
|
|
312
|
+
const result = await this.cmdHandler.execMenuUpdate(resolvedCmd, value, channel, msg.channelId, msg.peerId);
|
|
313
|
+
if ('error' in result)
|
|
314
|
+
throw { code: result.code || 'EXEC_FAILED', message: result.error };
|
|
315
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, {
|
|
319
|
+
type: 'menu.response', id, name,
|
|
320
|
+
error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
|
|
321
|
+
}, sendReply);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async handleMenuAction(req, channel, msg, adapter, sendReply) {
|
|
325
|
+
const { id, name, cmd, action, args } = req;
|
|
326
|
+
try {
|
|
327
|
+
if (!action)
|
|
328
|
+
throw { code: 'MISSING_VALUE', message: '缺少 action 参数' };
|
|
329
|
+
const resolvedCmd = this.resolveCmd(name, cmd);
|
|
330
|
+
const result = await this.cmdHandler.execMenuAction(resolvedCmd, action, args, channel, msg.channelId, msg.peerId);
|
|
331
|
+
if ('error' in result)
|
|
332
|
+
throw { code: result.code || 'EXEC_FAILED', message: result.error };
|
|
333
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, { type: 'menu.response', id, name, data: result.data }, sendReply);
|
|
243
334
|
}
|
|
244
|
-
|
|
335
|
+
catch (err) {
|
|
336
|
+
await this.sendMenuResponse(adapter, channel, msg.channelId, {
|
|
337
|
+
type: 'menu.response', id, name,
|
|
338
|
+
error: { code: err?.code || 'INTERNAL', message: err?.message || String(err) }
|
|
339
|
+
}, sendReply);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async sendMenuResponse(adapter, channel, channelId, response, sendReply) {
|
|
343
|
+
await this.sendCustomResponse(adapter, channel, channelId, JSON.stringify(response), sendReply);
|
|
245
344
|
}
|
|
246
345
|
/** menu.query 响应:优先走 adapter.send(custom),降级 sendReply */
|
|
247
346
|
async sendCustomResponse(adapter, channel, channelId, response, sendReply) {
|
|
@@ -12,6 +12,21 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
|
12
12
|
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
13
13
|
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
14
14
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
|
+
function getContextTooLongHint(agent) {
|
|
16
|
+
if (canCompactAgent(agent)) {
|
|
17
|
+
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
18
|
+
}
|
|
19
|
+
return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
|
|
20
|
+
}
|
|
21
|
+
function getContextCompactFailedHint(agent) {
|
|
22
|
+
if (canCompactAgent(agent)) {
|
|
23
|
+
return '上下文过长,自动压缩失败,请手动输入 /compact 重试';
|
|
24
|
+
}
|
|
25
|
+
return '上下文过长,请精简提问,或使用 /new 新建会话后继续';
|
|
26
|
+
}
|
|
27
|
+
function canCompactAgent(agent) {
|
|
28
|
+
return hasCompact(agent) && agent.capabilities?.compact !== false;
|
|
29
|
+
}
|
|
15
30
|
/**
|
|
16
31
|
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
17
32
|
*
|
|
@@ -80,10 +95,11 @@ export class MessageProcessor {
|
|
|
80
95
|
return [...this.agentMap.keys()];
|
|
81
96
|
}
|
|
82
97
|
/** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
|
|
83
|
-
|
|
98
|
+
isBackgroundSession(session, _channel, _channelId) {
|
|
84
99
|
if (session.threadId)
|
|
85
100
|
return false;
|
|
86
|
-
|
|
101
|
+
// 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
|
|
102
|
+
const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfId);
|
|
87
103
|
return active ? session.id !== active.id : false;
|
|
88
104
|
}
|
|
89
105
|
constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
|
|
@@ -183,7 +199,7 @@ export class MessageProcessor {
|
|
|
183
199
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
184
200
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
185
201
|
'/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
186
|
-
'/aid', '/
|
|
202
|
+
'/aid', '/upgrade', '/evolagent',
|
|
187
203
|
];
|
|
188
204
|
/** 判断消息内容是否为已知命令 */
|
|
189
205
|
isKnownCommand(content) {
|
|
@@ -230,7 +246,7 @@ export class MessageProcessor {
|
|
|
230
246
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
231
247
|
};
|
|
232
248
|
// Cache background status to avoid async call inside setInterval
|
|
233
|
-
const isBackground =
|
|
249
|
+
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
234
250
|
const timeoutPromise = new Promise((_, reject) => {
|
|
235
251
|
rejectFn = reject;
|
|
236
252
|
if (!monitorEnabled)
|
|
@@ -367,7 +383,7 @@ export class MessageProcessor {
|
|
|
367
383
|
replyContext: taskReplyContext(),
|
|
368
384
|
});
|
|
369
385
|
try {
|
|
370
|
-
const isBackground =
|
|
386
|
+
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
371
387
|
// 记录收到消息
|
|
372
388
|
logger.message({
|
|
373
389
|
msgId: messageId,
|
|
@@ -424,7 +440,7 @@ export class MessageProcessor {
|
|
|
424
440
|
// (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
|
|
425
441
|
if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
|
|
426
442
|
return;
|
|
427
|
-
const isCurrentlyBackground =
|
|
443
|
+
const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
428
444
|
if (isCurrentlyBackground)
|
|
429
445
|
return;
|
|
430
446
|
const opts = {};
|
|
@@ -501,18 +517,13 @@ export class MessageProcessor {
|
|
|
501
517
|
const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
502
518
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
503
519
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
504
|
-
// 文件发送能力
|
|
505
|
-
let currentCanSend = false;
|
|
506
|
-
if (!isProactive) {
|
|
507
|
-
currentCanSend = !!(channelInfo.adapter.capabilities?.file);
|
|
508
|
-
}
|
|
509
520
|
// 通道能力
|
|
510
521
|
const capParts = [];
|
|
511
522
|
if (options?.supportsImages)
|
|
512
523
|
capParts.push('图片输入');
|
|
513
524
|
if (channelInfo.adapter.capabilities?.image)
|
|
514
525
|
capParts.push('图片输出');
|
|
515
|
-
if (channelInfo.adapter.capabilities?.file)
|
|
526
|
+
if (!isProactive && channelInfo.adapter.capabilities?.file)
|
|
516
527
|
capParts.push('文件发送');
|
|
517
528
|
// Personal layer
|
|
518
529
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
|
|
@@ -528,6 +539,7 @@ export class MessageProcessor {
|
|
|
528
539
|
? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
|
|
529
540
|
: undefined;
|
|
530
541
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
542
|
+
const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
531
543
|
// Kit renderer: 组装上下文
|
|
532
544
|
const kitCtx = {
|
|
533
545
|
vars: {
|
|
@@ -542,19 +554,23 @@ export class MessageProcessor {
|
|
|
542
554
|
peerKey,
|
|
543
555
|
peerName: peerName || undefined,
|
|
544
556
|
peerRole: session.identity?.role || 'unknown',
|
|
557
|
+
peerType: message.peerType || undefined,
|
|
545
558
|
groupId: session.metadata?.groupId || undefined,
|
|
546
|
-
scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
|
|
547
559
|
chatType: session.chatType || null,
|
|
548
560
|
channel: currentChannelType || null,
|
|
549
561
|
venueUid: undefined,
|
|
562
|
+
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
550
563
|
project: path.basename(absoluteProjectPath),
|
|
564
|
+
sessionId: session.id,
|
|
551
565
|
sessionName: session.name || undefined,
|
|
552
|
-
|
|
566
|
+
sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
|
|
567
|
+
threadId: session.threadId || undefined,
|
|
568
|
+
chatMode: isProactive ? 'proactive' : 'interactive',
|
|
553
569
|
readonly: session.metadata?.permissionMode === 'readonly',
|
|
554
|
-
canSendFile: !isProactive && currentCanSend,
|
|
555
|
-
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
556
570
|
baseAgent: normalizedBaseagent.canonical,
|
|
557
571
|
baseAgentName: normalizedBaseagent.displayName,
|
|
572
|
+
baseAgentModel: agentModel || undefined,
|
|
573
|
+
agentSessionId: session.agentSessionId || undefined,
|
|
558
574
|
},
|
|
559
575
|
sessionId: session.id,
|
|
560
576
|
};
|
|
@@ -571,7 +587,7 @@ export class MessageProcessor {
|
|
|
571
587
|
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
572
588
|
agent.registerStream(streamKey, stream);
|
|
573
589
|
streamRegistered = true;
|
|
574
|
-
streamResult = await this.processEventStream(stream, session, renderer, resetTimer, shouldSuppress);
|
|
590
|
+
streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
575
591
|
break; // 成功,跳出重试循环
|
|
576
592
|
}
|
|
577
593
|
catch (retryError) {
|
|
@@ -591,7 +607,7 @@ export class MessageProcessor {
|
|
|
591
607
|
}
|
|
592
608
|
}
|
|
593
609
|
catch (error) {
|
|
594
|
-
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId &&
|
|
610
|
+
if (classifyError(error) === ErrorType.CONTEXT_TOO_LONG && session.agentSessionId && canCompactAgent(agent)) {
|
|
595
611
|
// 尝试 compact 压缩会话
|
|
596
612
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
597
613
|
await renderer.flush();
|
|
@@ -601,7 +617,7 @@ export class MessageProcessor {
|
|
|
601
617
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
602
618
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
603
619
|
agent.registerStream(streamKey, retryStream);
|
|
604
|
-
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
620
|
+
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
605
621
|
}
|
|
606
622
|
else {
|
|
607
623
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -615,7 +631,7 @@ export class MessageProcessor {
|
|
|
615
631
|
// 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
|
|
616
632
|
const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
617
633
|
const errorsText = streamResult.errors?.join(' ') || '';
|
|
618
|
-
const isPromptTooLong = streamResult.isError && session.agentSessionId &&
|
|
634
|
+
const isPromptTooLong = streamResult.isError && session.agentSessionId && canCompactAgent(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
619
635
|
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
620
636
|
contextTooLongPattern.test(errorsText) ||
|
|
621
637
|
contextTooLongPattern.test(streamResult.fullText));
|
|
@@ -627,7 +643,17 @@ export class MessageProcessor {
|
|
|
627
643
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
628
644
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
629
645
|
agent.registerStream(streamKey, retryStream);
|
|
630
|
-
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
646
|
+
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
647
|
+
// 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
|
|
648
|
+
const retryErrorsText = streamResult.errors?.join(' ') || '';
|
|
649
|
+
const retryStillTooLong = streamResult.isError && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
650
|
+
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
651
|
+
contextTooLongPattern.test(retryErrorsText) ||
|
|
652
|
+
contextTooLongPattern.test(streamResult.fullText));
|
|
653
|
+
if (retryStillTooLong) {
|
|
654
|
+
renderer.stripContextError(contextTooLongPattern);
|
|
655
|
+
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
656
|
+
}
|
|
631
657
|
}
|
|
632
658
|
else {
|
|
633
659
|
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
@@ -638,7 +664,7 @@ export class MessageProcessor {
|
|
|
638
664
|
contextTooLongPattern.test(errorsText) ||
|
|
639
665
|
contextTooLongPattern.test(streamResult.fullText))) {
|
|
640
666
|
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
641
|
-
renderer.addNotice(
|
|
667
|
+
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
642
668
|
}
|
|
643
669
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
644
670
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
@@ -727,7 +753,7 @@ export class MessageProcessor {
|
|
|
727
753
|
if (finalReplyText) {
|
|
728
754
|
if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
|
|
729
755
|
// Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
|
|
730
|
-
const isCurrentlyBackground =
|
|
756
|
+
const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
731
757
|
if (!isCurrentlyBackground) {
|
|
732
758
|
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
|
|
733
759
|
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
@@ -843,7 +869,7 @@ export class MessageProcessor {
|
|
|
843
869
|
// 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
|
|
844
870
|
// 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
|
|
845
871
|
}
|
|
846
|
-
const isFinallyBackground =
|
|
872
|
+
const isFinallyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
847
873
|
if (isFinallyBackground && session.sessionMode !== 'autonomous') {
|
|
848
874
|
const projectName = path.basename(session.projectPath);
|
|
849
875
|
const count = this.messageCache.getCount(session.id);
|
|
@@ -927,7 +953,7 @@ export class MessageProcessor {
|
|
|
927
953
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
928
954
|
let sendOpts;
|
|
929
955
|
try {
|
|
930
|
-
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId);
|
|
956
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
|
|
931
957
|
sendOpts = this.getReplyContext(message);
|
|
932
958
|
}
|
|
933
959
|
catch { }
|
|
@@ -964,7 +990,7 @@ export class MessageProcessor {
|
|
|
964
990
|
: path.resolve(process.cwd(), session.projectPath);
|
|
965
991
|
return { session, absoluteProjectPath };
|
|
966
992
|
}
|
|
967
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId,
|
|
993
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfId, message.channelType, message.peerType);
|
|
968
994
|
// 兜底纠正1:群聊强制 proactive
|
|
969
995
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
970
996
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
@@ -990,7 +1016,7 @@ export class MessageProcessor {
|
|
|
990
1016
|
* 此方法只消费标准 AgentEvent 类型,不引用任何 SDK 特有事件。
|
|
991
1017
|
* SDK 事件 → AgentEvent 的转换在 AgentRunner.transformStream() 中完成。
|
|
992
1018
|
*/
|
|
993
|
-
async processEventStream(stream, session, renderer, resetTimer, shouldSuppress) {
|
|
1019
|
+
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
|
|
994
1020
|
// Per-session agent name for stats bucketing
|
|
995
1021
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelName || session.channel)?.name ?? '<unknown>';
|
|
996
1022
|
let hasReceivedText = false;
|
|
@@ -1058,7 +1084,7 @@ export class MessageProcessor {
|
|
|
1058
1084
|
});
|
|
1059
1085
|
continue;
|
|
1060
1086
|
}
|
|
1061
|
-
const isCurrentlyBackground =
|
|
1087
|
+
const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
|
|
1062
1088
|
// === 前台任务:正常处理所有事件 ===
|
|
1063
1089
|
if (!isCurrentlyBackground) {
|
|
1064
1090
|
// 流式文本
|
|
@@ -1173,9 +1199,13 @@ export class MessageProcessor {
|
|
|
1173
1199
|
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1174
1200
|
const errorSummary = event.errors?.join('; ') || '任务执行失败';
|
|
1175
1201
|
// 使用 terminalReason 提供更友好的错误提示(不带 emoji,由 formatter 统一加)
|
|
1176
|
-
const userFriendlyMessage = event.terminalReason
|
|
1177
|
-
?
|
|
1178
|
-
:
|
|
1202
|
+
const userFriendlyMessage = event.terminalReason === 'prompt_too_long'
|
|
1203
|
+
? getContextTooLongHint(agent)
|
|
1204
|
+
: event.terminalReason === 'context_compact_failed'
|
|
1205
|
+
? getContextCompactFailedHint(agent)
|
|
1206
|
+
: event.terminalReason
|
|
1207
|
+
? getErrorMessage(null, event.terminalReason, false)
|
|
1208
|
+
: errorSummary;
|
|
1179
1209
|
renderer.addNotice(userFriendlyMessage, 'warn', 'task-error', true);
|
|
1180
1210
|
}
|
|
1181
1211
|
// 中间 complete:flush 掉已有 activities(不带 isFinal),让中间结果及时显示
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* PeerIdentityCache - 对端身份缓存管理
|
|
3
3
|
*
|
|
4
4
|
* 职责:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
5
|
+
* 1. 通过 agentmdSync 标准流程获取对端 agent.md(check → fetch if changed)
|
|
6
|
+
* 2. 仅在 agent.md 内容变化时重写 peer-identity.json
|
|
7
7
|
* 3. 支持入站和出站消息的身份查询
|
|
8
8
|
*
|
|
9
|
-
* 信源:对端的 agent.md(通过 AUN SDK
|
|
9
|
+
* 信源:对端的 agent.md(通过 AUN SDK checkAgentMd + fetchAgentMd)
|
|
10
10
|
* 判定规则:type !== 'human' → agent
|
|
11
11
|
* 缓存位置:$AGENT_DIR/relations/<channel>#<urlEncode(peerId)>/peer-identity.json
|
|
12
12
|
*/
|
|
@@ -14,6 +14,7 @@ import * as fs from 'fs';
|
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as crypto from 'crypto';
|
|
16
16
|
import { logger } from '../../utils/logger.js';
|
|
17
|
+
import { agentMdPath } from '../../paths.js';
|
|
17
18
|
/**
|
|
18
19
|
* 对端身份缓存管理器
|
|
19
20
|
*/
|
|
@@ -54,29 +55,26 @@ export class PeerIdentityCache {
|
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* 从 agent.md 更新身份信息
|
|
57
|
-
* @param agentMd 已验签的 agent.md 内容
|
|
58
58
|
*/
|
|
59
59
|
static updateFromAgentMd(channel, peerId, agentDir, agentMd, verifiedAt) {
|
|
60
|
-
// 解析 type 和 name
|
|
61
60
|
const typeMatch = agentMd.match(/^type:\s*["']?(\w+)["']?/m);
|
|
62
61
|
const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
63
62
|
const type = typeMatch?.[1] || 'unknown';
|
|
64
63
|
const isAgent = type !== 'human';
|
|
65
64
|
const name = nameMatch?.[1]?.trim();
|
|
66
|
-
// 计算 hash
|
|
67
65
|
const agentMdHash = 'sha256:' + crypto.createHash('sha256').update(agentMd, 'utf-8').digest('hex');
|
|
68
|
-
|
|
66
|
+
const now = Date.now();
|
|
69
67
|
const identity = {
|
|
70
68
|
aid: peerId,
|
|
71
69
|
type,
|
|
72
70
|
isAgent,
|
|
73
71
|
name,
|
|
74
72
|
agentMdHash,
|
|
73
|
+
agentMdUpdatedAt: now,
|
|
75
74
|
verifiedAt,
|
|
76
|
-
lastCheckedAt:
|
|
75
|
+
lastCheckedAt: now,
|
|
77
76
|
source: 'agentmd',
|
|
78
77
|
};
|
|
79
|
-
// 写入文件
|
|
80
78
|
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
81
79
|
try {
|
|
82
80
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -88,6 +86,18 @@ export class PeerIdentityCache {
|
|
|
88
86
|
}
|
|
89
87
|
return identity;
|
|
90
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* 仅更新 lastCheckedAt(内容未变时的轻量操作)
|
|
91
|
+
*/
|
|
92
|
+
static touchLastChecked(channel, peerId, agentDir, cached) {
|
|
93
|
+
const updated = { ...cached, lastCheckedAt: Date.now() };
|
|
94
|
+
const filePath = this.getFilePath(channel, peerId, agentDir);
|
|
95
|
+
try {
|
|
96
|
+
fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
catch { /* ignore */ }
|
|
99
|
+
return updated;
|
|
100
|
+
}
|
|
91
101
|
/**
|
|
92
102
|
* 标记为 unknown(验签失败或无 agent.md)
|
|
93
103
|
*/
|
|
@@ -95,8 +105,9 @@ export class PeerIdentityCache {
|
|
|
95
105
|
const identity = {
|
|
96
106
|
aid: peerId,
|
|
97
107
|
type: 'unknown',
|
|
98
|
-
isAgent: true,
|
|
108
|
+
isAgent: true,
|
|
99
109
|
agentMdHash: '',
|
|
110
|
+
agentMdUpdatedAt: 0,
|
|
100
111
|
verifiedAt: 0,
|
|
101
112
|
lastCheckedAt: Date.now(),
|
|
102
113
|
source: 'unknown',
|
|
@@ -113,17 +124,17 @@ export class PeerIdentityCache {
|
|
|
113
124
|
return identity;
|
|
114
125
|
}
|
|
115
126
|
/**
|
|
116
|
-
*
|
|
127
|
+
* 完整流程:缓存检查 → agentmdSync(check+fetch)→ 按 changed 决定是否重写
|
|
117
128
|
*
|
|
118
129
|
* @param channel 渠道类型(如 'aun')
|
|
119
130
|
* @param peerId 对端 ID(AUN 是 AID)
|
|
120
131
|
* @param agentDir agent 数据根目录
|
|
121
|
-
* @param aunClient AUN SDK client(需要有 fetchAgentMd 方法)
|
|
132
|
+
* @param aunClient AUN SDK client(需要有 checkAgentMd / fetchAgentMd 方法)
|
|
122
133
|
* @param forceRefresh 强制刷新(忽略缓存时效)
|
|
123
134
|
* @returns PeerIdentity
|
|
124
135
|
*/
|
|
125
136
|
static async resolve(channel, peerId, agentDir, aunClient, forceRefresh = false) {
|
|
126
|
-
// 1.
|
|
137
|
+
// 1. 缓存检查
|
|
127
138
|
if (!forceRefresh && !this.needsRefresh(channel, peerId, agentDir)) {
|
|
128
139
|
const cached = this.get(channel, peerId, agentDir);
|
|
129
140
|
if (cached) {
|
|
@@ -131,17 +142,49 @@ export class PeerIdentityCache {
|
|
|
131
142
|
return cached;
|
|
132
143
|
}
|
|
133
144
|
}
|
|
134
|
-
// 2.
|
|
145
|
+
// 2. 标准流程:checkAgentMd → fetchAgentMd(如果有变化)
|
|
135
146
|
try {
|
|
136
|
-
logger.debug(`[PeerIdentityCache]
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channel}#${peerId}`);
|
|
148
|
+
const state = await aunClient.checkAgentMd(peerId, 30);
|
|
149
|
+
let content;
|
|
150
|
+
if (state.in_sync && state.local_found) {
|
|
151
|
+
// 本地已是最新,读本地文件
|
|
152
|
+
const localPath = agentMdPath(peerId);
|
|
153
|
+
try {
|
|
154
|
+
content = fs.readFileSync(localPath, 'utf-8');
|
|
155
|
+
}
|
|
156
|
+
catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
if (!content) {
|
|
159
|
+
// 需要下载(不同步或本地不存在)
|
|
160
|
+
const info = await aunClient.fetchAgentMd(peerId);
|
|
161
|
+
content = info.content;
|
|
162
|
+
}
|
|
163
|
+
// 3. 比较 hash,仅在变化时重写 peer-identity.json
|
|
164
|
+
const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
165
|
+
const cached = this.get(channel, peerId, agentDir);
|
|
166
|
+
if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
|
|
167
|
+
return this.touchLastChecked(channel, peerId, agentDir, cached);
|
|
168
|
+
}
|
|
169
|
+
return this.updateFromAgentMd(channel, peerId, agentDir, content, Date.now());
|
|
141
170
|
}
|
|
142
171
|
catch (err) {
|
|
143
|
-
//
|
|
144
|
-
|
|
172
|
+
// 4. 网络失败,fallback 本地文件
|
|
173
|
+
const localPath = agentMdPath(peerId);
|
|
174
|
+
try {
|
|
175
|
+
if (fs.existsSync(localPath)) {
|
|
176
|
+
const localContent = fs.readFileSync(localPath, 'utf-8');
|
|
177
|
+
logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
|
|
178
|
+
const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
|
|
179
|
+
const cached = this.get(channel, peerId, agentDir);
|
|
180
|
+
if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
|
|
181
|
+
return this.touchLastChecked(channel, peerId, agentDir, cached);
|
|
182
|
+
}
|
|
183
|
+
return this.updateFromAgentMd(channel, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { /* ignore fs errors */ }
|
|
187
|
+
logger.warn(`[PeerIdentityCache] Failed to resolve: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
|
|
145
188
|
return this.markUnknown(channel, peerId, agentDir);
|
|
146
189
|
}
|
|
147
190
|
}
|