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.
Files changed (48) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +2 -6
  3. package/assets/.env.template +4 -0
  4. package/assets/config.json.template +6 -0
  5. package/assets/wechat-group-qr.jpeg +0 -0
  6. package/dist/agents/claude-runner.js +1 -1
  7. package/dist/agents/codex-runner.js +75 -19
  8. package/dist/agents/gemini-runner.js +0 -2
  9. package/dist/agents/kit-renderer.js +85 -22
  10. package/dist/aun/aid/agentmd.js +67 -74
  11. package/dist/aun/aid/client.js +22 -7
  12. package/dist/aun/aid/identity.js +314 -28
  13. package/dist/aun/aid/index.js +2 -2
  14. package/dist/aun/rpc/connection.js +8 -10
  15. package/dist/channels/aun.js +53 -41
  16. package/dist/cli/agent.js +28 -28
  17. package/dist/cli/bench.js +8 -14
  18. package/dist/cli/help.js +23 -0
  19. package/dist/cli/index.js +398 -73
  20. package/dist/cli/init-channel.js +2 -3
  21. package/dist/cli/init.js +13 -6
  22. package/dist/cli/link-rules.js +2 -1
  23. package/dist/cli/net-check.js +10 -11
  24. package/dist/core/command-handler.js +621 -541
  25. package/dist/core/evolagent.js +31 -0
  26. package/dist/core/message/im-renderer.js +10 -0
  27. package/dist/core/message/message-bridge.js +123 -24
  28. package/dist/core/message/message-processor.js +61 -31
  29. package/dist/core/relation/peer-identity.js +64 -21
  30. package/dist/core/session/session-manager.js +191 -44
  31. package/dist/core/trigger/manager.js +37 -0
  32. package/dist/index.js +4 -1
  33. package/dist/paths.js +87 -16
  34. package/dist/utils/npm-ops.js +18 -11
  35. package/kits/eck_manifest.json +9 -9
  36. package/kits/rules/02-navigation.md +1 -0
  37. package/kits/rules/05-venue.md +2 -2
  38. package/kits/rules/06-channel.md +2 -18
  39. package/kits/templates/system-fragments/baseagent.md +8 -2
  40. package/kits/templates/system-fragments/channel.md +20 -8
  41. package/kits/templates/system-fragments/identity.md +5 -6
  42. package/kits/templates/system-fragments/relation.md +10 -5
  43. package/kits/templates/system-fragments/session.md +20 -0
  44. package/kits/templates/system-fragments/venue.md +5 -3
  45. package/package.json +4 -2
  46. package/dist/net-check.js +0 -640
  47. package/dist/watch-msg.js +0 -544
  48. package/kits/templates/system-fragments/runtime.md +0 -19
@@ -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
- /** 自定义消息快速路径:拦截 menu.query 等自定义 payload,返回 true 表示已处理 */
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
- if (parsed.type === 'menu.query') {
222
- if (parsed.cmd && (parsed.mode === 'query' || parsed.mode === 'update')) {
223
- // exec 模式:查询状态或执行命令
224
- const result = await this.cmdHandler.execMenu(parsed.cmd, parsed.mode, channel, msg.channelId, msg.peerId);
225
- const base = { type: 'menu.response', cmd: parsed.cmd };
226
- const response = JSON.stringify('error' in result ? { ...base, error: result.error } : { ...base, data: result.data });
227
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
228
- }
229
- else if (parsed.cmd) {
230
- // 动态子菜单查询
231
- const items = await this.cmdHandler.getSubMenuItems(parsed.cmd, channel, msg.channelId, msg.peerId);
232
- const response = JSON.stringify({ type: 'menu.response', cmd: parsed.cmd, items: items ?? [] });
233
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
234
- }
235
- else {
236
- // 全量菜单
237
- const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
238
- const items = this.cmdHandler.getMenuItems(identity.role, msg.chatType || 'private');
239
- const response = JSON.stringify({ type: 'menu.response', items });
240
- await this.sendCustomResponse(adapter, channel, msg.channelId, response, sendReply);
241
- }
242
- return true;
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
- return false;
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
- async isBackgroundSession(session, channel, channelId) {
98
+ isBackgroundSession(session, _channel, _channelId) {
84
99
  if (session.threadId)
85
100
  return false;
86
- const active = await this.sessionManager.getActiveSession(channel, channelId);
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', '/agentmd', '/upgrade',
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 = await this.isBackgroundSession(session, message.channel, message.channelId);
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 = await this.isBackgroundSession(session, message.channel, message.channelId);
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 = await this.isBackgroundSession(session, message.channel, message.channelId);
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
- sessionMode: isProactive ? 'proactive' : 'interactive',
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 && hasCompact(agent)) {
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 && hasCompact(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
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('上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
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 = await this.isBackgroundSession(session, message.channel, message.channelId);
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 = await this.isBackgroundSession(session, message.channel, message.channelId);
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, undefined, undefined, undefined, undefined, message.peerType);
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 = await this.isBackgroundSession(session, session.channel, session.channelId);
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
- ? getErrorMessage(null, event.terminalReason, false)
1178
- : errorSummary;
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. 从对端的 agent.md 确定身份(human / agent
6
- * 2. 缓存到关系层文件(30天时效)
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: Date.now(),
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, // 验签失败 → 当做 agent(安全策略)
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
- * 完整流程:检查缓存需要刷新则下载 agent.md 更新缓存
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. 下载并验签 agent.md(SDK 自动验签)
145
+ // 2. 标准流程:checkAgentMd fetchAgentMd(如果有变化)
135
146
  try {
136
- logger.debug(`[PeerIdentityCache] Fetching agent.md: ${channel}#${peerId}`);
137
- const result = await aunClient.fetchAgentMd(peerId);
138
- const agentMd = result.content;
139
- // 3. 更新缓存
140
- return this.updateFromAgentMd(channel, peerId, agentDir, agentMd, Date.now());
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
- // 验签失败或下载失败 标记为 unknown,当做 agent
144
- logger.warn(`[PeerIdentityCache] Failed to fetch agent.md: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
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
  }