evolclaw 3.0.0 → 3.1.1

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 (104) hide show
  1. package/README.md +1 -1
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +47 -12
  5. package/dist/agents/codex-runner.js +2 -0
  6. package/dist/agents/gemini-runner.js +9 -9
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/aun/aid/identity.js +28 -0
  9. package/dist/aun/aid/index.js +1 -1
  10. package/dist/aun/aid/lifecycle-log.js +33 -0
  11. package/dist/aun/msg/group.js +3 -1
  12. package/dist/aun/msg/p2p.js +42 -1
  13. package/dist/channels/aun.js +427 -146
  14. package/dist/channels/dingtalk.js +3 -1
  15. package/dist/channels/feishu.js +128 -7
  16. package/dist/channels/qqbot.js +3 -1
  17. package/dist/channels/wechat.js +4 -1
  18. package/dist/channels/wecom.js +3 -1
  19. package/dist/cli/bench.js +1219 -0
  20. package/dist/cli/index.js +418 -40
  21. package/dist/cli/init.js +3 -4
  22. package/dist/cli/link-rules.js +245 -0
  23. package/dist/cli/net-check.js +640 -0
  24. package/dist/cli/watch-msg.js +666 -0
  25. package/dist/config-store.js +82 -5
  26. package/dist/core/channel-loader.js +23 -10
  27. package/dist/core/command-handler.js +127 -99
  28. package/dist/core/evolagent.js +5 -10
  29. package/dist/core/message/im-renderer.js +93 -48
  30. package/dist/core/message/items-formatter.js +11 -4
  31. package/dist/core/message/message-bridge.js +11 -2
  32. package/dist/core/message/message-log.js +8 -1
  33. package/dist/core/message/message-processor.js +194 -127
  34. package/dist/core/message/message-queue.js +10 -3
  35. package/dist/core/permission.js +95 -3
  36. package/dist/core/relation/peer-identity.js +161 -0
  37. package/dist/core/session/session-manager.js +103 -65
  38. package/dist/core/trigger/manager.js +16 -0
  39. package/dist/core/trigger/parser.js +110 -0
  40. package/dist/core/trigger/scheduler.js +7 -1
  41. package/dist/data/error-dict.json +118 -0
  42. package/dist/eck/baseagent-caps.js +18 -0
  43. package/dist/eck/detect.js +47 -0
  44. package/dist/eck/init.js +77 -0
  45. package/dist/eck/rules-loader.js +28 -0
  46. package/dist/index.js +186 -19
  47. package/dist/net-check.js +640 -0
  48. package/dist/paths.js +31 -40
  49. package/dist/utils/aid-lifecycle-log.js +33 -0
  50. package/dist/utils/atomic-write.js +10 -0
  51. package/dist/utils/cross-platform.js +17 -8
  52. package/dist/utils/error-utils.js +27 -15
  53. package/dist/utils/instance-registry.js +6 -5
  54. package/dist/utils/log-writer.js +2 -1
  55. package/dist/utils/logger.js +10 -0
  56. package/dist/utils/npm-ops.js +35 -3
  57. package/dist/utils/process-introspect.js +16 -38
  58. package/dist/utils/stats.js +216 -2
  59. package/dist/watch-msg.js +26 -11
  60. package/evolclaw-install-aun.md +14 -2
  61. package/kits/docs/GUIDE.md +20 -0
  62. package/kits/docs/INDEX.md +52 -0
  63. package/kits/docs/aun/CHEATSHEET.md +17 -0
  64. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  65. package/kits/docs/channels/feishu.md +27 -0
  66. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  67. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  68. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  69. package/kits/docs/eck_templates/runtime.template.md +19 -0
  70. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  71. package/kits/docs/evolclaw/MSG_PRIVATE.md +72 -0
  72. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  73. package/kits/docs/identity/PATH_OPS.md +16 -0
  74. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  75. package/kits/docs/path-registry.md +43 -0
  76. package/kits/eck_manifest.json +95 -0
  77. package/kits/rules/01-overview.md +120 -0
  78. package/kits/rules/02-navigation.md +75 -0
  79. package/kits/rules/03-identity.md +34 -0
  80. package/kits/rules/04-relation.md +49 -0
  81. package/kits/rules/05-venue.md +45 -0
  82. package/kits/rules/06-channel.md +73 -0
  83. package/kits/templates/system-fragments/baseagent.md +2 -0
  84. package/kits/templates/system-fragments/channel.md +10 -0
  85. package/kits/templates/system-fragments/identity.md +12 -0
  86. package/kits/templates/system-fragments/relation.md +9 -0
  87. package/kits/templates/system-fragments/runtime.md +19 -0
  88. package/kits/templates/system-fragments/venue.md +5 -0
  89. package/package.json +7 -5
  90. package/dist/agents/templates.js +0 -122
  91. package/dist/data/prompts.md +0 -137
  92. package/kits/aun/meta.md +0 -25
  93. package/kits/aun/role.md +0 -25
  94. package/kits/templates/group.md +0 -20
  95. package/kits/templates/private.md +0 -9
  96. package/kits/templates/system-fragments/personal-context.md +0 -3
  97. package/kits/templates/system-fragments/self-intro.md +0 -5
  98. package/kits/templates/system-fragments/speaker-intro.md +0 -5
  99. package/kits/templates/system-fragments/venue-intro.md +0 -5
  100. /package/kits/{channels → docs/channels}/aun.md +0 -0
  101. /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
  102. /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
  103. /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
  104. /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
@@ -6,13 +6,17 @@ import os from 'os';
6
6
  import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
9
- import { resolvePaths, getPackageRoot } from '../paths.js';
9
+ import { resolvePaths, getPackageRoot, agentDir as agentDirPath } from '../paths.js';
10
10
  import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
11
11
  import { appendAidEvent } from '../utils/instance-registry.js';
12
+ import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
13
+ import { chatDirPath } from '../core/session/session-fs-store.js';
14
+ import { appendAidLifecycle } from '../aun/aid/identity.js';
12
15
  import { loadAgent, saveAgent } from '../config-store.js';
13
16
  import { getProcessStartTime } from '../utils/process-introspect.js';
14
17
  import * as outbox from '../aun/outbox.js';
15
18
  import { guessMime, formatSize } from '../utils/media-cache.js';
19
+ import { PeerIdentityCache } from '../core/relation/peer-identity.js';
16
20
  /**
17
21
  * 构造 connect extra_info:自描述本进程身份。
18
22
  *
@@ -63,8 +67,10 @@ export class AUNChannel {
63
67
  traceWriter = null;
64
68
  eventBus = null;
65
69
  ownerBoundHandler = null;
70
+ queuedHandler = null;
66
71
  pendingEchoMessages = new Map();
67
72
  isEchoSending = false;
73
+ agentDir;
68
74
  trace(dir, event, data) {
69
75
  if (!this.config.aunTrace)
70
76
  return;
@@ -168,7 +174,7 @@ export class AUNChannel {
168
174
  const name = cached?.name;
169
175
  return name && name !== short ? `${short}(${name})` : short;
170
176
  }
171
- extractTextPayload(payload, channelId) {
177
+ extractTextPayload(payload, channelId, senderAid) {
172
178
  if (typeof payload === 'string')
173
179
  return payload;
174
180
  if (payload && typeof payload === 'object') {
@@ -176,21 +182,38 @@ export class AUNChannel {
176
182
  const text = typeof obj.text === 'string' ? obj.text : '';
177
183
  // action_card_reply:卡片交互回复,触发 interactionCallback,不分发给 agent
178
184
  if (obj.type === 'action_card_reply') {
179
- const cardMsgId = typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
185
+ const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
186
+ : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
180
187
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
181
188
  if (cardInfo) {
182
- const actionValue = typeof obj.action_value === 'string' ? obj.action_value : text;
189
+ const actionValue = typeof obj.value === 'string' ? obj.value
190
+ : typeof obj.action_value === 'string' ? obj.action_value : text;
183
191
  if (cardInfo.isCommandCard) {
184
192
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息
185
193
  this.cardMessageIdMap.delete(cardMsgId);
186
194
  if (this.messageHandler && actionValue.startsWith('/')) {
187
195
  const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
196
+ // 卡片点击者身份:优先 payload.from / payload.sender_aid / payload.user_id,
197
+ // 再 fallback 到外层 senderAid,最后用 cardInfo 中记录的原始命令发起者
198
+ const cardClickerAid = (typeof obj.from === 'string' && obj.from)
199
+ || (typeof obj.sender_aid === 'string' && obj.sender_aid)
200
+ || (typeof obj.user_id === 'string' && obj.user_id)
201
+ || senderAid
202
+ || cardInfo.initiatorAid
203
+ || channelId || '';
204
+ // Initiator 校验:群聊中仅卡片发起者可操作(与飞书行为对齐)
205
+ if (cardInfo.initiatorAid && cardClickerAid
206
+ && cardClickerAid !== cardInfo.initiatorAid
207
+ && !this.isGroupId(cardClickerAid)) {
208
+ logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
209
+ return '';
210
+ }
188
211
  this.messageHandler({
189
212
  channelId: channelId || '',
190
213
  chatType,
191
214
  content: actionValue,
192
- peerId: channelId || '',
193
- peerName: typeof obj.action_label === 'string' ? obj.action_label : undefined,
215
+ peerId: cardClickerAid,
216
+ peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
194
217
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
195
218
  source: 'card-trigger',
196
219
  });
@@ -205,7 +228,7 @@ export class AUNChannel {
205
228
  type: 'interaction.response',
206
229
  id: cardInfo.requestId,
207
230
  action: actionValue,
208
- values: { text, action_label: obj.action_label, behavior: obj.behavior },
231
+ values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
209
232
  });
210
233
  }
211
234
  }
@@ -216,23 +239,126 @@ export class AUNChannel {
216
239
  // 始终返回空字符串,阻止消息分发给 agent
217
240
  return '';
218
241
  }
219
- // quote 类型:拼接被引用内容
242
+ // quote 类型:拼接被引用内容(支持 text / image / file attachments)
220
243
  if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
221
244
  const q = obj.quote;
222
245
  const quotedText = typeof q.text === 'string' ? q.text : '';
223
- if (quotedText) {
224
- const sender = typeof q.sender_display === 'string' ? q.sender_display : '';
225
- const prefix = sender ? `${sender}: ` : '';
226
- const quoted = quotedText.split('\n').map(line => `> ${prefix}${line}`).join('\n');
246
+ const sender = typeof q.sender_display === 'string' ? q.sender_display : '';
247
+ const prefix = sender ? `${sender}: ` : '';
248
+ // 构建引用内容:文本 + 附件描述
249
+ const quoteParts = [];
250
+ if (quotedText)
251
+ quoteParts.push(quotedText);
252
+ if (Array.isArray(q.attachments)) {
253
+ for (const att of q.attachments) {
254
+ if (att && typeof att === 'object') {
255
+ const ct = typeof att.content_type === 'string' ? att.content_type : '';
256
+ const fn = typeof att.filename === 'string' ? att.filename : '';
257
+ if (ct.startsWith('image/')) {
258
+ quoteParts.push(fn ? `[图片: ${fn}]` : '[图片]');
259
+ }
260
+ else {
261
+ quoteParts.push(fn ? `[文件: ${fn}]` : '[文件]');
262
+ }
263
+ }
264
+ }
265
+ }
266
+ if (quoteParts.length > 0) {
267
+ const lines = quoteParts.join('\n').split('\n');
268
+ const quoted = lines.map((line, i) => `> ${i === 0 ? prefix : ''}${line}`).join('\n');
227
269
  return text ? `${quoted}\n\n${text}` : quoted;
228
270
  }
229
271
  }
272
+ // merge 类型:合并转发消息,展开子消息为可读文本
273
+ if (obj.type === 'merge') {
274
+ const title = typeof obj.title === 'string' ? obj.title : '合并转发消息';
275
+ const parts = [`以下是转发的合并消息「${title}」:\n---`];
276
+ if (Array.isArray(obj.items)) {
277
+ for (const item of obj.items) {
278
+ if (item && typeof item === 'object') {
279
+ const sender = typeof item.sender_display === 'string' ? item.sender_display : '';
280
+ const itemText = typeof item.text === 'string' ? item.text : '';
281
+ const itemType = typeof item.type === 'string' ? item.type : '';
282
+ // 根据子消息类型构建展示
283
+ const lineParts = [];
284
+ if (itemText)
285
+ lineParts.push(itemText);
286
+ // 子消息附件(image/file)
287
+ if (Array.isArray(item.attachments)) {
288
+ for (const att of item.attachments) {
289
+ if (att && typeof att === 'object') {
290
+ const ct = typeof att.content_type === 'string' ? att.content_type : '';
291
+ const fn = typeof att.filename === 'string' ? att.filename : '';
292
+ if (ct.startsWith('image/') || itemType === 'image') {
293
+ lineParts.push(fn ? `[图片: ${fn}]` : '[图片]');
294
+ }
295
+ else if (ct.startsWith('video/') || itemType === 'video') {
296
+ lineParts.push(fn ? `[视频: ${fn}]` : '[视频]');
297
+ }
298
+ else {
299
+ lineParts.push(fn ? `[文件: ${fn}]` : '[文件]');
300
+ }
301
+ }
302
+ }
303
+ }
304
+ const content = lineParts.join(' ') || `[${itemType || '未知类型'}]`;
305
+ parts.push(sender ? `${sender}: ${content}` : content);
306
+ }
307
+ }
308
+ }
309
+ if (typeof obj.summary === 'string' && obj.summary) {
310
+ parts.push(`\n[摘要] ${obj.summary}`);
311
+ }
312
+ parts.push('---');
313
+ return parts.join('\n');
314
+ }
230
315
  if (typeof obj.text === 'string')
231
316
  return text;
232
317
  return JSON.stringify(payload);
233
318
  }
234
319
  return '';
235
320
  }
321
+ /** 收集 payload 中所有需要下载的 attachments(顶层 + merge.items + quote.quote),按 url 去重 */
322
+ collectAllAttachments(payload) {
323
+ if (!payload || typeof payload !== 'object')
324
+ return [];
325
+ const obj = payload;
326
+ const result = [];
327
+ const seen = new Set();
328
+ const add = (att) => {
329
+ if (!att || typeof att !== 'object')
330
+ return;
331
+ const key = att.url || att.object_key || '';
332
+ if (key && seen.has(key))
333
+ return;
334
+ if (key)
335
+ seen.add(key);
336
+ result.push(att);
337
+ };
338
+ // 顶层 attachments
339
+ if (Array.isArray(obj.attachments)) {
340
+ for (const att of obj.attachments)
341
+ add(att);
342
+ }
343
+ // merge.items 中的子消息 attachments
344
+ if (obj.type === 'merge' && Array.isArray(obj.items)) {
345
+ for (const item of obj.items) {
346
+ if (item && typeof item === 'object' && Array.isArray(item.attachments)) {
347
+ for (const att of item.attachments)
348
+ add(att);
349
+ }
350
+ }
351
+ }
352
+ // quote.quote 中的 attachments
353
+ if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
354
+ const q = obj.quote;
355
+ if (Array.isArray(q.attachments)) {
356
+ for (const att of q.attachments)
357
+ add(att);
358
+ }
359
+ }
360
+ return result;
361
+ }
236
362
  hasExplicitMention(text, target) {
237
363
  const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
238
364
  return new RegExp(`(^|\\s)@${escaped}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`).test(text);
@@ -282,31 +408,24 @@ export class AUNChannel {
282
408
  }
283
409
  return out;
284
410
  }
285
- buildGroupReplyContext(taskId, senderAid, encrypted) {
286
- const replyContext = { metadata: { encrypted } };
411
+ buildGroupReplyContext(taskId, senderAid, encrypted, messageId, chatmode) {
412
+ const replyContext = { metadata: { encrypted, chatmode } };
287
413
  if (taskId)
288
414
  replyContext.threadId = taskId;
289
415
  replyContext.peerId = senderAid;
416
+ if (messageId)
417
+ replyContext.replyToMessageId = messageId;
290
418
  return replyContext;
291
419
  }
292
- acknowledgeImmediately(messageId, seq) {
293
- if (seq != null && this.client) {
294
- this.client.call('message.ack', { seq }).catch(e => {
295
- logger.debug(`${this.logPrefix()} Immediate ack failed: ${e}`);
296
- });
297
- }
420
+ acknowledgeImmediately(messageId, _seq) {
421
+ // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
422
+ // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
298
423
  if (messageId)
299
424
  this.messageSeqMap.delete(messageId);
300
425
  }
301
- shouldEncrypt(peerId) {
302
- const cached = this.peerE2ee.get(peerId);
303
- if (!cached)
304
- return true;
305
- if (Date.now() - cached.ts > AUNChannel.E2EE_PROBE_TTL) {
306
- this.peerE2ee.delete(peerId);
307
- return true;
308
- }
309
- return cached.ok;
426
+ shouldEncrypt(_peerId) {
427
+ // Default to plaintext; only encrypt when session is explicitly marked encrypted
428
+ return false;
310
429
  }
311
430
  _aid;
312
431
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
@@ -318,7 +437,6 @@ export class AUNChannel {
318
437
  peerE2ee = new Map();
319
438
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
320
439
  plaintextRecv = 0;
321
- sessionModeResolver;
322
440
  interactionCallback;
323
441
  // action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
324
442
  cardMessageIdMap = new Map();
@@ -356,6 +474,7 @@ export class AUNChannel {
356
474
  aidStatsCollector;
357
475
  constructor(config) {
358
476
  this.config = config;
477
+ this.agentDir = agentDirPath(config.aid);
359
478
  if (config.aunTrace) {
360
479
  this.traceWriter = new LogWriter({
361
480
  baseName: 'aun',
@@ -579,6 +698,7 @@ export class AUNChannel {
579
698
  }
580
699
  logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
581
700
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
701
+ appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
582
702
  // Send welcome message to owner after first connection
583
703
  await this.sendWelcomeMessage();
584
704
  }
@@ -625,7 +745,7 @@ export class AUNChannel {
625
745
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
626
746
  const ownerDisplayName = (ownerInfo.name || ownerAidClean.split('.')[0]).slice(0, 12);
627
747
  const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
628
- const currentName = currentNameMatch?.[1]?.trim();
748
+ const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
629
749
  const aidLabel = aidName.split('.')[0];
630
750
  let agentDisplayName;
631
751
  if (currentName && currentName !== aidLabel) {
@@ -634,13 +754,19 @@ export class AUNChannel {
634
754
  else {
635
755
  agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
636
756
  }
757
+ // Preserve user-provided description (from `agent new --description`), fallback to default
758
+ const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
759
+ const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
760
+ const agentDescription = currentDesc
761
+ ? currentDesc
762
+ : 'EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道';
637
763
  // Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
638
764
  const newAgentMd = `---
639
765
  aid: "${aid}"
640
766
  name: "${agentDisplayName}"
641
767
  type: "codeagent"
642
768
  version: "1.0.0"
643
- description: "EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道"
769
+ description: "${agentDescription}"
644
770
  tags:
645
771
  - evolclaw
646
772
  - ai-agent
@@ -720,11 +846,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
720
846
  async downloadAttachment(att, channelId) {
721
847
  const ownerAid = att.owner_aid || this._aid || '';
722
848
  const objectKey = att.object_key;
723
- const filename = att.filename || objectKey.split('/').pop() || 'unknown';
724
849
  if (!objectKey) {
725
850
  logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
726
851
  return null;
727
852
  }
853
+ const filename = att.filename || objectKey.split('/').pop() || 'unknown';
728
854
  let downloadUrl;
729
855
  try {
730
856
  const ticket = await this.callAndTrace('storage.create_download_ticket', {
@@ -802,10 +928,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
802
928
  if (this._aid && text.includes(`@${this._aid}`)) {
803
929
  mentions.push(this._aid);
804
930
  }
805
- // Process attachments
806
- const rawAttachments = Array.isArray(payload?.attachments)
807
- ? payload.attachments
808
- : [];
931
+ // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
932
+ const rawAttachments = this.collectAllAttachments(payload);
809
933
  let finalText = text;
810
934
  if (rawAttachments.length > 0 && this.client) {
811
935
  const fileParts = [];
@@ -828,20 +952,40 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
828
952
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
829
953
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
830
954
  const chatId = fromAid;
831
- const peerInfo = await this.fetchPeerInfo(fromAid);
955
+ // 解析对端身份(30天缓存)
956
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, this.agentDir, this.client, false);
832
957
  const shortAid = this.getShortAid(fromAid);
833
- const displayName = peerInfo.name || shortAid;
958
+ const displayName = peerIdentity.name || shortAid;
834
959
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
835
960
  const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
836
- logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerInfo.type || 'unknown'} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
961
+ logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
837
962
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
838
963
  if (p2pPayloadType === 'action_card_reply')
839
964
  return;
840
- logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
965
+ // menu.query:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
966
+ if (p2pPayloadType === 'menu.query') {
967
+ this.acknowledgeImmediately(messageId, seq);
968
+ this.dispatchMessage({
969
+ channelId: chatId, userId: fromAid,
970
+ text: JSON.stringify(payload),
971
+ chatType: 'private', messageId, seq,
972
+ peerName: displayName || undefined,
973
+ peerType: peerIdentity.type,
974
+ });
975
+ return;
976
+ }
977
+ // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
978
+ if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
979
+ this.acknowledgeImmediately(messageId, seq);
980
+ logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
981
+ return;
982
+ }
983
+ const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
984
+ logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
841
985
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
842
986
  const isSystemP2P = p2pPayloadType === 'event';
843
- this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P);
844
- const replyContext = { metadata: { encrypted: msgEncrypted } };
987
+ this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
988
+ const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
845
989
  if (taskId)
846
990
  replyContext.threadId = taskId;
847
991
  this.dispatchMessage({
@@ -854,7 +998,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
854
998
  taskId,
855
999
  mentions,
856
1000
  peerName: displayName || undefined,
857
- peerType: peerInfo.type || 'unknown',
1001
+ peerType: peerIdentity.type,
858
1002
  replyContext,
859
1003
  });
860
1004
  }
@@ -865,7 +1009,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
865
1009
  const groupId = msg.group_id ?? '';
866
1010
  const senderAid = msg.sender_aid ?? '';
867
1011
  const payload = msg.payload ?? '';
868
- const text = this.extractTextPayload(payload, groupId);
1012
+ const text = this.extractTextPayload(payload, groupId, senderAid);
869
1013
  const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
870
1014
  const messageId = msg.message_id ?? '';
871
1015
  const seq = msg.seq;
@@ -884,13 +1028,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
884
1028
  logger.debug(`${this.logPrefix()} Group dropped: own message (group=${groupId} mid=${messageId})`);
885
1029
  return;
886
1030
  }
887
- // 短 echo 快速通道:连通性测试要尽量低延迟,命中后绕过所有 await(sessionModeResolver / 后续 mention 过滤)
1031
+ // 短 echo 快速通道:连通性测试要尽量低延迟,命中后绕过所有 await(后续 mention 过滤)
888
1032
  {
889
1033
  const firstLineFast = text.split('\n')[0] || '';
890
1034
  const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
891
1035
  if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
892
1036
  this.acknowledgeImmediately(messageId, seq);
893
1037
  const msgEncryptedFast = !!(msg.e2ee);
1038
+ const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
894
1039
  const peerInfo = this.peerInfoCached(senderAid);
895
1040
  const shortAid = this.getShortAid(senderAid);
896
1041
  const displayName = peerInfo?.name || shortAid;
@@ -906,25 +1051,37 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
906
1051
  peerName: displayName,
907
1052
  peerType: peerInfo?.type || 'unknown',
908
1053
  seq,
909
- replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast),
1054
+ replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId, msgChatmodeFast),
910
1055
  createdAt,
911
1056
  });
912
1057
  return;
913
1058
  }
914
1059
  }
915
- // ── proactive 模式 payload 类型白名单 ──
916
- // 仅做类型层面的防噪(task.update / status.ping 等信号类消息不进 Agent);
917
- // mention 过滤统一交给后面的 dispatchMode 一段处理(避免双层语义)。
918
- if (this.sessionModeResolver) {
919
- const sessionMode = await this.sessionModeResolver(groupId).catch(() => undefined);
920
- if (sessionMode === 'proactive') {
921
- const payloadObj = (payload && typeof payload === 'object') ? payload : null;
922
- const payloadType = payloadObj?.type ?? '';
923
- if (!AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
924
- this.acknowledgeImmediately(messageId, seq);
925
- logger.info(`${this.logPrefix()} Group dropped (proactive deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
926
- return;
927
- }
1060
+ // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1061
+ {
1062
+ const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1063
+ if (payloadType === 'action_card_reply')
1064
+ return;
1065
+ }
1066
+ // ── payload 类型白名单(所有模式生效) ──
1067
+ // 信号类消息(status / event / thought / task.update 等)不进 Agent
1068
+ {
1069
+ const payloadObj = (payload && typeof payload === 'object') ? payload : null;
1070
+ const payloadType = payloadObj?.type ?? '';
1071
+ // menu.query:自定义消息快速路径
1072
+ if (payloadType === 'menu.query') {
1073
+ this.acknowledgeImmediately(messageId, seq);
1074
+ this.dispatchMessage({
1075
+ channelId: groupId, userId: senderAid,
1076
+ text: JSON.stringify(payload),
1077
+ chatType: 'group', messageId, seq, groupId,
1078
+ });
1079
+ return;
1080
+ }
1081
+ if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
1082
+ this.acknowledgeImmediately(messageId, seq);
1083
+ logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
1084
+ return;
928
1085
  }
929
1086
  }
930
1087
  // 记录入站消息加密状态,透传到出站 ReplyContext
@@ -955,16 +1112,17 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
955
1112
  };
956
1113
  let echoText = text;
957
1114
  echoText += `\n${echoTs()} [EvolClaw.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
1115
+ const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
958
1116
  this.pendingEchoMessages.set(messageId, {
959
1117
  text: echoText,
960
1118
  channelId: groupId,
961
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted),
1119
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho),
962
1120
  receiveTs: Date.now(),
963
1121
  });
964
1122
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
965
1123
  }
966
1124
  else if (/echo/i.test(firstLineGroup) && hasEvolClawTraceGroup) {
967
- // 回声炸弹:已被 trace 过的 echo,直接丢弃
1125
+ // 回声炸弹:已被任何 EvolClaw 节点 trace 过的 echo,直接丢弃
968
1126
  this.acknowledgeImmediately(messageId, seq);
969
1127
  logger.info(`${this.logPrefix()} Group dropped: echo bomb (already-traced group=${groupId} sender=${senderAid} mid=${messageId})`);
970
1128
  return;
@@ -978,10 +1136,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
978
1136
  }
979
1137
  }
980
1138
  const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
981
- // Detect attachments before the empty-text guard
982
- const rawAttachments = Array.isArray(payload?.attachments)
983
- ? payload.attachments
984
- : [];
1139
+ // Detect attachments before the empty-text guard (顶层 + 嵌套)
1140
+ const rawAttachments = this.collectAllAttachments(payload);
985
1141
  const hasAttachments = rawAttachments.length > 0;
986
1142
  // Allow through if there's text OR attachments; both-empty messages are silently dropped
987
1143
  if (!strippedText && !hasAttachments) {
@@ -1012,9 +1168,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1012
1168
  finalText = parts.join('\n\n');
1013
1169
  }
1014
1170
  }
1015
- const peerInfo = await this.fetchPeerInfo(senderAid);
1171
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, this.agentDir, this.client, false);
1016
1172
  const shortAid = this.getShortAid(senderAid);
1017
- const displayName = peerInfo.name || shortAid;
1173
+ const displayName = peerIdentity.name || shortAid;
1018
1174
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
1019
1175
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1020
1176
  const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
@@ -1026,26 +1182,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1026
1182
  : mentionedSelf
1027
1183
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
1028
1184
  : `${dispatchMode}.no-mention`;
1029
- logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerInfo.type || 'unknown'} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1185
+ logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1030
1186
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1031
1187
  if (payloadType === 'action_card_reply')
1032
1188
  return;
1033
- logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} text=${finalText.slice(0, 60)}`);
1189
+ const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1190
+ logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1034
1191
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: senderAid, msgId: messageId, kind: 'text', len: finalText.length, groupId });
1035
- this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event');
1192
+ this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1036
1193
  this.dispatchMessage({
1037
1194
  channelId: groupId,
1038
1195
  groupId,
1039
1196
  userId: senderAid,
1040
1197
  peerName: displayName || undefined,
1041
- peerType: peerInfo.type || 'unknown',
1198
+ peerType: peerIdentity.type,
1042
1199
  text: finalText,
1043
1200
  chatType: 'group',
1044
1201
  messageId,
1045
1202
  seq,
1046
1203
  taskId,
1047
1204
  mentions,
1048
- replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted),
1205
+ replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId, msgChatmode),
1049
1206
  });
1050
1207
  }
1051
1208
  dispatchMessage(event) {
@@ -1068,7 +1225,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1068
1225
  this.handleEcho(event);
1069
1226
  return;
1070
1227
  }
1071
- // 回声炸弹:已被本系统 trace 过的 echo,直接丢弃
1228
+ // 回声炸弹:已被任何 EvolClaw 节点 trace 过的 echo,直接丢弃(防止多 agent 间无限回声)
1072
1229
  if (/echo/i.test(firstLine) && hasEvolClawTracePrivate) {
1073
1230
  logger.info(`${this.logPrefix()} Dropped: echo bomb (already-traced mid=${event.messageId} chat=${event.chatType})`);
1074
1231
  return;
@@ -1258,16 +1415,46 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1258
1415
  const d = data;
1259
1416
  const reason = d.reason ?? '';
1260
1417
  const error = d.error ?? 'unknown';
1418
+ const code = d.code ?? d.detail?.code ?? 0;
1419
+ const detail = (d.detail && typeof d.detail === 'object') ? d.detail : {};
1261
1420
  if (this.intentionalDisconnect)
1262
1421
  return;
1263
- // 被踢类(server kicked / close code 4001-4011)→ ERROR + 5min 长退避
1264
- if (this.isKickReason(reason)) {
1265
- logger.error(`${this.logPrefix()} Kicked by server: ${reason} (${error}), backing off ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s`);
1266
- this.setAidStatus('kicked', { lastError: `kicked: ${error}`.slice(0, 80) });
1267
- this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'kicked');
1422
+ if (this.isKickReason(reason) || code >= 4001) {
1423
+ // @ts-ignore — methods defined below in same class
1424
+ const kickDetail = this.buildKickDetail(code, reason, detail);
1425
+ // @ts-ignore methods defined below in same class
1426
+ const action = this.classifyKickAction(code);
1427
+ appendAidEvent({
1428
+ ts: Date.now(), iso: new Date().toISOString(),
1429
+ event: 'kicked', aid: this.config.aid,
1430
+ code, reason, action,
1431
+ evictedBy: kickDetail.evictedBy,
1432
+ quotaKind: kickDetail.quotaKind,
1433
+ });
1434
+ appendAidLifecycle({
1435
+ ts: Date.now(), iso: new Date().toISOString(),
1436
+ event: 'kicked', aid: this.config.aid,
1437
+ code, reason, action,
1438
+ evictedBy: kickDetail.evictedBy,
1439
+ newExtra: kickDetail.newExtra,
1440
+ quotaKind: kickDetail.quotaKind,
1441
+ });
1442
+ if (action === 'no_retry') {
1443
+ logger.error(`${this.logPrefix()} Kicked (code=${code}): ${reason} — will NOT retry`);
1444
+ this.setAidStatus('kicked_no_retry', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
1445
+ }
1446
+ else if (action === 'retry_once') {
1447
+ logger.warn(`${this.logPrefix()} Kicked (code=${code}): ${reason} — retrying once after ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
1448
+ this.setAidStatus('kicked', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
1449
+ this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'kicked');
1450
+ }
1451
+ else {
1452
+ logger.warn(`${this.logPrefix()} Kicked (code=${code}): ${reason} — retrying after ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s`);
1453
+ this.setAidStatus('kicked', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
1454
+ this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'kicked');
1455
+ }
1268
1456
  }
1269
1457
  else {
1270
- // 其他 terminal failure(含 max_attempts_exhausted 兜底)→ 1min 后再试
1271
1458
  logger.error(`${this.logPrefix()} Terminal failure: ${error}${reason ? ` (${reason})` : ''}, retrying in ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
1272
1459
  this.setAidStatus('failed', { lastError: `${error}`.slice(0, 80) });
1273
1460
  this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'terminal');
@@ -1281,11 +1468,58 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1281
1468
  const r = reason.toLowerCase();
1282
1469
  if (r.includes('kicked') || r.includes('kick'))
1283
1470
  return true;
1284
- // close code 4001/4003/4008/4009/4010/4011 都是 SDK _NO_RECONNECT_CODES
1285
- if (/close code 40(0[13]|0[89]|1[01])/.test(r))
1471
+ if (/close code 40\d{2}/.test(r))
1286
1472
  return true;
1287
1473
  return false;
1288
1474
  }
1475
+ /**
1476
+ * 根据 close code 决定重试策略:
1477
+ * - 'no_retry': 不重试(被挤掉、AID 无效、ACL 拒绝、长连接已存在、配额超限)
1478
+ * - 'retry_once': 重试一次(auth 失败可能 token 刚过期、nonce 无效)
1479
+ * - 'retry_delay': 延迟重试(短连接容量超限、空闲超时)
1480
+ */
1481
+ classifyKickAction(code) {
1482
+ switch (code) {
1483
+ case 4003: // AID 无效
1484
+ case 4009: // 服务端主动踢
1485
+ case 4011: // ACL 拒绝
1486
+ case 4012: // 长连接已存在(自己另一个实例在线)
1487
+ case 4015: // 被新连接挤掉
1488
+ return 'no_retry';
1489
+ case 4001: // auth 失败(token 可能刚过期)
1490
+ case 4010: // nonce 无效
1491
+ return 'retry_once';
1492
+ case 4008: // auth 超时
1493
+ case 4013: // 短连接容量超限
1494
+ case 4014: // 短连接空闲超时
1495
+ return 'retry_delay';
1496
+ default:
1497
+ return 'retry_delay';
1498
+ }
1499
+ }
1500
+ buildKickDetail(code, reason, detail) {
1501
+ const evictedByRaw = detail.evicted_by || detail.new_extra_info;
1502
+ let evictedBy;
1503
+ if (evictedByRaw && typeof evictedByRaw === 'object') {
1504
+ evictedBy = {
1505
+ aid: evictedByRaw.aid,
1506
+ deviceId: evictedByRaw.device_id,
1507
+ slotId: evictedByRaw.slot_id,
1508
+ app: evictedByRaw.app,
1509
+ hostname: evictedByRaw.hostname,
1510
+ };
1511
+ }
1512
+ return {
1513
+ code,
1514
+ reason,
1515
+ ts: Date.now(),
1516
+ evictedBy,
1517
+ quotaKind: detail.quota_kind,
1518
+ limit: detail.limit,
1519
+ selfExtra: detail.self_extra_info,
1520
+ newExtra: detail.new_extra_info,
1521
+ };
1522
+ }
1289
1523
  /**
1290
1524
  * TS 层接管重连:force close 当前 SDK client,安排 delayMs 后重新 initClient。
1291
1525
  * 用于 flap / kicked / terminal_failed 三类场景,统一退避路径。
@@ -1347,7 +1581,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1347
1581
  if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
1348
1582
  this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
1349
1583
  }
1584
+ if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
1585
+ this.eventBus.unsubscribe('task:queued', this.queuedHandler);
1586
+ }
1350
1587
  this.ownerBoundHandler = null;
1588
+ this.queuedHandler = null;
1351
1589
  this.eventBus = bus;
1352
1590
  if (bus && typeof bus.subscribe === 'function') {
1353
1591
  const handler = (event) => {
@@ -1365,6 +1603,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1365
1603
  };
1366
1604
  bus.subscribe('channel:owner-bound', handler);
1367
1605
  this.ownerBoundHandler = handler;
1606
+ const queuedHandler = (event) => {
1607
+ if (event.channel !== this.config.channelName)
1608
+ return;
1609
+ this.sendProcessingStatus(event.channelId, 'queued', '', '', event.replyContext);
1610
+ };
1611
+ bus.subscribe('task:queued', queuedHandler);
1612
+ this.queuedHandler = queuedHandler;
1368
1613
  }
1369
1614
  }
1370
1615
  onProjectPathRequest(provider) {
@@ -1373,9 +1618,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1373
1618
  onMessage(handler) {
1374
1619
  this.messageHandler = handler;
1375
1620
  }
1376
- setSessionModeResolver(resolver) {
1377
- this.sessionModeResolver = resolver;
1378
- }
1379
1621
  setDispatchModeResolver(resolver) {
1380
1622
  this.dispatchModeResolver = resolver;
1381
1623
  }
@@ -1392,9 +1634,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1392
1634
  await this.flushPendingEcho(channelId);
1393
1635
  }
1394
1636
  let finalText = text;
1395
- if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
1396
- finalText = '最终回复\n' + text;
1397
- }
1398
1637
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
1399
1638
  if (this.isGroupId(channelId) && context?.peerId) {
1400
1639
  if (!finalText.includes(`@${context.peerId}`)) {
@@ -1480,6 +1719,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1480
1719
  payload.task_id = context.metadata.taskId;
1481
1720
  if (context?.metadata?.chatmode)
1482
1721
  payload.chatmode = context.metadata.chatmode;
1722
+ // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
1723
+ logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} textLen=${finalText.length}`);
1483
1724
  const isGroup = this.isGroupId(channelId);
1484
1725
  const targetAid = channelId;
1485
1726
  const encryptTarget = isGroup ? channelId : targetAid;
@@ -1491,7 +1732,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1491
1732
  if (isGroup) {
1492
1733
  params.group_id = channelId;
1493
1734
  const result = await this.callAndTrace('group.send', params);
1494
- if (!result || !result.message_id) {
1735
+ const mid = result?.message?.message_id ?? result?.message_id ?? null;
1736
+ if (!mid) {
1495
1737
  const dispatchStatus = result?.message_dispatch?.status;
1496
1738
  if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
1497
1739
  logger.info(`${this.logPrefix()} group.send ok (${dispatchStatus}): group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
@@ -1501,9 +1743,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1501
1743
  }
1502
1744
  }
1503
1745
  else {
1504
- logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1505
- appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: result.message_id, kind: 'text', len: finalText.length, groupId: channelId });
1506
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText);
1746
+ logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1747
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
1748
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1749
+ this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true);
1507
1750
  }
1508
1751
  }
1509
1752
  else {
@@ -1515,7 +1758,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1515
1758
  else {
1516
1759
  logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1517
1760
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
1518
- this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText);
1761
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1762
+ this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false);
1519
1763
  }
1520
1764
  }
1521
1765
  return true;
@@ -1529,7 +1773,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1529
1773
  if (isGroup) {
1530
1774
  this.trace('OUT', 'group.send.fallback', params);
1531
1775
  const result = await this.client.call('group.send', params);
1532
- this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message_id });
1776
+ this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message?.message_id ?? result?.message_id });
1533
1777
  if (!result || !result.message_id) {
1534
1778
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
1535
1779
  }
@@ -1557,6 +1801,33 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1557
1801
  }
1558
1802
  }
1559
1803
  }
1804
+ /** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
1805
+ appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text') {
1806
+ try {
1807
+ const sessionsDir = resolvePaths().sessionsDir;
1808
+ const selfId = this.config.aid;
1809
+ const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfId);
1810
+ const chatmode = context?.metadata?.chatmode;
1811
+ appendMessageLog(chatDir, buildOutboundEntry({
1812
+ from: selfId,
1813
+ to: channelId,
1814
+ chatType: isGroup ? 'group' : 'private',
1815
+ groupId: isGroup ? channelId : null,
1816
+ msgId,
1817
+ content: text,
1818
+ replyTo: null,
1819
+ agent: null,
1820
+ model: null,
1821
+ durationMs: null,
1822
+ encrypt,
1823
+ chatmode,
1824
+ msgType,
1825
+ }));
1826
+ }
1827
+ catch (e) {
1828
+ logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
1829
+ }
1830
+ }
1560
1831
  /**
1561
1832
  * 发送 thought 内容(Proactive 模式可观测)
1562
1833
  * 群聊:调用 group.thought.put
@@ -1583,15 +1854,28 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1583
1854
  try {
1584
1855
  const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
1585
1856
  const stage = payload?.stage ?? `items=${itemCount}`;
1857
+ // 提取 thought 文本(取最后一项的 text 或 content 字段)
1858
+ const items = payload?.items;
1859
+ let thoughtText;
1860
+ if (Array.isArray(items) && items.length > 0) {
1861
+ const lastItem = items[items.length - 1];
1862
+ thoughtText = lastItem?.text || lastItem?.content || (typeof lastItem === 'string' ? lastItem : undefined);
1863
+ }
1586
1864
  if (this.isGroupId(channelId)) {
1587
1865
  params.group_id = targetId;
1588
- await this.callAndTrace('group.thought.put', params);
1589
- logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt}`);
1866
+ const putRes = await this.callAndTrace('group.thought.put', params);
1867
+ const tid = putRes?.thought_id;
1868
+ logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1869
+ this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1870
+ // thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
1590
1871
  }
1591
1872
  else {
1592
1873
  params.to = targetId;
1593
- await this.callAndTrace('message.thought.put', params);
1594
- logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt}`);
1874
+ const putRes = await this.callAndTrace('message.thought.put', params);
1875
+ const tid = putRes?.thought_id;
1876
+ logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1877
+ this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1878
+ // thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
1595
1879
  }
1596
1880
  }
1597
1881
  catch (e) {
@@ -1622,8 +1906,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1622
1906
  if (isGroup) {
1623
1907
  params.group_id = channelId;
1624
1908
  const result = await this.callAndTrace('group.send', params);
1625
- logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${result?.message_id} encrypt=${encrypt}`);
1626
- return result?.message_id ?? null;
1909
+ const mid = result?.message?.message_id ?? result?.message_id ?? null;
1910
+ logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
1911
+ return mid;
1627
1912
  }
1628
1913
  else {
1629
1914
  params.to = targetAid;
@@ -1750,8 +2035,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1750
2035
  params.group_id = channelId;
1751
2036
  this.trace('OUT', 'group.send.file', params);
1752
2037
  const result = await this.client.call('group.send', params);
1753
- this.trace('OUT', 'group.send.file.ok', { message_id: result?.message_id });
1754
- if (!result || !result.message_id) {
2038
+ const fileMid = result?.message?.message_id ?? result?.message_id;
2039
+ this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
2040
+ if (!fileMid) {
1755
2041
  logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
1756
2042
  }
1757
2043
  }
@@ -1777,8 +2063,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1777
2063
  if (isGroup) {
1778
2064
  this.trace('OUT', 'group.send.file.fallback', params);
1779
2065
  const result = await this.client.call('group.send', params);
1780
- this.trace('OUT', 'group.send.file.fallback.ok', { message_id: result?.message_id });
1781
- if (!result || !result.message_id) {
2066
+ const fbMid = result?.message?.message_id ?? result?.message_id;
2067
+ this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
2068
+ if (!fbMid) {
1782
2069
  logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
1783
2070
  }
1784
2071
  }
@@ -1845,35 +2132,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1845
2132
  // to avoid duplicate "已送达" at the sender CLI
1846
2133
  this.messageSeqMap.delete(messageId);
1847
2134
  }
1848
- sendProcessingStatus(channelId, status, sessionId, taskId, context) {
2135
+ sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
1849
2136
  if (status === 'start')
1850
2137
  this.sentCount.delete(channelId); // 新任务开始,重置计数
1851
2138
  if (!this.client || !this.connected)
1852
2139
  return;
1853
- // 旧路 payload(type='event', event='task.*')—— 前后兼容保留,未来废弃
1854
- const eventMap = {
1855
- start: 'task.started',
1856
- done: 'task.completed',
1857
- interrupted: 'task.interrupted',
1858
- error: 'task.error',
1859
- timeout: 'task.timeout',
1860
- };
1861
2140
  const severity = status === 'error' || status === 'timeout' ? 'error' : 'info';
1862
- const eventPayload = {
1863
- type: 'event',
1864
- event: eventMap[status] ?? `task.${status}`,
1865
- data: { task_id: taskId, session_id: sessionId },
1866
- severity,
1867
- };
1868
- if (context?.threadId)
1869
- eventPayload.thread_id = context.threadId;
1870
- // 新路 payload(type='status')—— 结构化任务状态,下游直接读字段不用解析 event 字符串
1871
2141
  const stateMap = {
1872
2142
  start: 'started',
1873
2143
  done: 'completed',
1874
2144
  interrupted: 'interrupted',
1875
2145
  error: 'error',
1876
2146
  timeout: 'timeout',
2147
+ queued: 'queued',
2148
+ progress: 'progress',
1877
2149
  };
1878
2150
  const statusPayload = {
1879
2151
  type: 'status',
@@ -1881,15 +2153,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1881
2153
  task_id: taskId,
1882
2154
  session_id: sessionId,
1883
2155
  severity,
2156
+ ...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
1884
2157
  };
1885
2158
  if (context?.threadId)
1886
2159
  statusPayload.thread_id = context.threadId;
2160
+ if (context?.peerId)
2161
+ statusPayload.initiator = context.peerId;
2162
+ if (context?.replyToMessageId)
2163
+ statusPayload.ref_message_id = context.replyToMessageId;
1887
2164
  const isGroup = this.isGroupId(channelId);
1888
2165
  // 私聊 channelId = 对端 AID(不含 device_id)
1889
2166
  const statusTargetAid = channelId;
1890
2167
  const encryptTarget = isGroup ? channelId : statusTargetAid;
1891
- // 计算 encrypt 标志(每次调用读最新 peerE2ee 状态,
1892
- // 这样第二条 send 能受益于第一条触发的 peerE2ee 标记)
1893
2168
  const computeEncrypt = () => context?.metadata?.encrypted != null
1894
2169
  ? !!(context.metadata.encrypted)
1895
2170
  : this.shouldEncrypt(encryptTarget);
@@ -1922,17 +2197,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1922
2197
  });
1923
2198
  };
1924
2199
  const method = isGroup ? 'group.send' : 'message.send';
1925
- // 串行:等第一条完成(或失败更新 peerE2ee 标记)后再发第二条,
1926
- // 保证两路 payload 到达顺序(event 在前,status 在后)+ 第二条能用最新 encrypt 状态
1927
- sendOne(method, eventPayload, 'event')
1928
- .then(() => sendOne(method, statusPayload, 'status'));
1929
- // 统计为系统消息(按两条独立消息分别记账)
1930
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(eventPayload).length, undefined, true);
2200
+ sendOne(method, statusPayload, 'status');
1931
2201
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
1932
2202
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
1933
2203
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
1934
2204
  const chatmode = context?.metadata?.chatmode ?? '?';
1935
- logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel}`);
2205
+ const initiator = statusPayload.initiator ?? '';
2206
+ const refMsgId = statusPayload.ref_message_id ?? '';
2207
+ const metaStr = statusPayload.metadata ? ` meta=${JSON.stringify(statusPayload.metadata)}` : '';
2208
+ logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`);
1936
2209
  }
1937
2210
  sendCustomPayload(channelId, payload) {
1938
2211
  if (!this.client || !this.connected)
@@ -1980,6 +2253,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1980
2253
  }
1981
2254
  this.connected = false;
1982
2255
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
2256
+ appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
1983
2257
  this.setAidStatus('disabled');
1984
2258
  if (this.traceWriter) {
1985
2259
  this.traceWriter.close();
@@ -2159,7 +2433,7 @@ export class AUNChannelPlugin {
2159
2433
  case 'result.error': {
2160
2434
  const sendCtx = { ...(ctx ?? {}) };
2161
2435
  if (payload.kind === 'result.text' && payload.isFinal)
2162
- sendCtx.title = ' 最终回复:';
2436
+ sendCtx.title = ' 最终回复:';
2163
2437
  await channel.sendMessage(channelId, payload.text, sendCtx);
2164
2438
  return;
2165
2439
  }
@@ -2186,16 +2460,26 @@ export class AUNChannelPlugin {
2186
2460
  };
2187
2461
  if (ctx?.threadId)
2188
2462
  aunPayload.thread_id = ctx.threadId;
2189
- // 双发:thought.put(前端实时渲染) + message.send(消息历史持久化)
2190
- await Promise.all([
2191
- channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
2192
- channel.sendStructured(channelId, aunPayload, ctx),
2193
- ]);
2463
+ if (envelope.chatmode === 'proactive') {
2464
+ await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
2465
+ }
2466
+ else {
2467
+ await Promise.all([
2468
+ channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
2469
+ channel.sendStructured(channelId, aunPayload, ctx),
2470
+ ]);
2471
+ }
2194
2472
  return;
2195
2473
  }
2474
+ case 'status.progress':
2475
+ channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2476
+ return;
2196
2477
  case 'status.started':
2197
2478
  channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
2198
2479
  return;
2480
+ case 'status.queued':
2481
+ channel.sendProcessingStatus(channelId, 'queued', envelope.taskId, envelope.taskId, ctx);
2482
+ return;
2199
2483
  case 'status.completed':
2200
2484
  channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx);
2201
2485
  return;
@@ -2215,7 +2499,6 @@ export class AUNChannelPlugin {
2215
2499
  const aunCard = {
2216
2500
  type: 'action_card',
2217
2501
  title: action.title,
2218
- description: action.body,
2219
2502
  actions: action.buttons.map(btn => ({
2220
2503
  label: btn.label,
2221
2504
  value: btn.key,
@@ -2223,6 +2506,8 @@ export class AUNChannelPlugin {
2223
2506
  behavior: 'reply',
2224
2507
  })),
2225
2508
  };
2509
+ if (action.body)
2510
+ aunCard.description = action.body;
2226
2511
  if (ctx?.threadId)
2227
2512
  aunCard.thread_id = ctx.threadId;
2228
2513
  const msgId = await channel.sendStructured(channelId, aunCard, ctx);
@@ -2236,7 +2521,6 @@ export class AUNChannelPlugin {
2236
2521
  const aunCard = {
2237
2522
  type: 'action_card',
2238
2523
  title: card.title,
2239
- description: card.body,
2240
2524
  actions: card.buttons.map(btn => ({
2241
2525
  label: btn.label,
2242
2526
  value: btn.command,
@@ -2244,11 +2528,13 @@ export class AUNChannelPlugin {
2244
2528
  behavior: 'reply',
2245
2529
  })),
2246
2530
  };
2531
+ if (card.body)
2532
+ aunCard.description = card.body;
2247
2533
  if (ctx?.threadId)
2248
2534
  aunCard.thread_id = ctx.threadId;
2249
2535
  const msgId = await channel.sendStructured(channelId, aunCard, ctx);
2250
2536
  if (msgId) {
2251
- channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true });
2537
+ channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
2252
2538
  setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2253
2539
  }
2254
2540
  }
@@ -2323,6 +2609,7 @@ export class AUNChannelPlugin {
2323
2609
  chatType: opts.chatType || 'private',
2324
2610
  peerId: opts.peerId || '',
2325
2611
  peerName: opts.peerName,
2612
+ peerType: opts.peerType,
2326
2613
  messageId: opts.messageId,
2327
2614
  mentions: opts.mentions,
2328
2615
  threadId: opts.threadId,
@@ -2345,12 +2632,6 @@ export class AUNChannelPlugin {
2345
2632
  });
2346
2633
  });
2347
2634
  }
2348
- if (typeof channel.setSessionModeResolver === 'function') {
2349
- channel.setSessionModeResolver(async (channelId) => {
2350
- const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);
2351
- return session?.sessionMode;
2352
- });
2353
- }
2354
2635
  if (typeof channel.setDispatchModeResolver === 'function') {
2355
2636
  channel.setDispatchModeResolver(async (channelId) => {
2356
2637
  const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);