evolclaw 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) 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 +7 -9
  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 +4 -1
  13. package/dist/channels/aun.js +353 -125
  14. package/dist/channels/dingtalk.js +2 -1
  15. package/dist/channels/feishu.js +118 -5
  16. package/dist/channels/qqbot.js +2 -1
  17. package/dist/channels/wechat.js +3 -1
  18. package/dist/channels/wecom.js +2 -1
  19. package/dist/cli/bench.js +1219 -0
  20. package/dist/cli/index.js +279 -19
  21. package/dist/cli/link-rules.js +245 -0
  22. package/dist/cli/net-check.js +640 -0
  23. package/dist/cli/watch-msg.js +589 -0
  24. package/dist/config-store.js +37 -5
  25. package/dist/core/channel-loader.js +23 -10
  26. package/dist/core/command-handler.js +46 -22
  27. package/dist/core/evolagent.js +5 -10
  28. package/dist/core/message/im-renderer.js +50 -44
  29. package/dist/core/message/items-formatter.js +11 -4
  30. package/dist/core/message/message-bridge.js +7 -2
  31. package/dist/core/message/message-log.js +2 -0
  32. package/dist/core/message/message-processor.js +150 -99
  33. package/dist/core/message/message-queue.js +10 -3
  34. package/dist/core/permission.js +95 -3
  35. package/dist/core/session/session-manager.js +98 -64
  36. package/dist/core/trigger/scheduler.js +1 -1
  37. package/dist/data/error-dict.json +118 -0
  38. package/dist/eck/baseagent-caps.js +18 -0
  39. package/dist/eck/detect.js +47 -0
  40. package/dist/eck/init.js +77 -0
  41. package/dist/eck/rules-loader.js +28 -0
  42. package/dist/index.js +137 -16
  43. package/dist/net-check.js +640 -0
  44. package/dist/paths.js +31 -40
  45. package/dist/utils/aid-lifecycle-log.js +33 -0
  46. package/dist/utils/atomic-write.js +10 -0
  47. package/dist/utils/cross-platform.js +17 -8
  48. package/dist/utils/error-utils.js +10 -2
  49. package/dist/utils/instance-registry.js +6 -5
  50. package/dist/utils/log-writer.js +2 -1
  51. package/dist/utils/logger.js +10 -0
  52. package/dist/utils/npm-ops.js +35 -3
  53. package/dist/utils/process-introspect.js +16 -38
  54. package/dist/watch-msg.js +26 -11
  55. package/evolclaw-install-aun.md +14 -2
  56. package/kits/docs/GUIDE.md +20 -0
  57. package/kits/docs/INDEX.md +52 -0
  58. package/kits/docs/aun/CHEATSHEET.md +17 -0
  59. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  60. package/kits/docs/channels/feishu.md +27 -0
  61. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  62. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  63. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  64. package/kits/docs/eck_templates/runtime.template.md +19 -0
  65. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  66. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  67. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  68. package/kits/docs/identity/PATH_OPS.md +16 -0
  69. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  70. package/kits/docs/path-registry.md +43 -0
  71. package/kits/eck_manifest.json +95 -0
  72. package/kits/rules/01-overview.md +120 -0
  73. package/kits/rules/02-navigation.md +75 -0
  74. package/kits/rules/03-identity.md +34 -0
  75. package/kits/rules/04-relation.md +49 -0
  76. package/kits/rules/05-venue.md +45 -0
  77. package/kits/rules/06-channel.md +43 -0
  78. package/kits/templates/system-fragments/baseagent.md +2 -0
  79. package/kits/templates/system-fragments/channel.md +10 -0
  80. package/kits/templates/system-fragments/identity.md +12 -0
  81. package/kits/templates/system-fragments/relation.md +9 -0
  82. package/kits/templates/system-fragments/runtime.md +19 -0
  83. package/kits/templates/system-fragments/venue.md +5 -0
  84. package/package.json +7 -5
  85. package/dist/agents/templates.js +0 -122
  86. package/dist/data/prompts.md +0 -137
  87. package/kits/aun/meta.md +0 -25
  88. package/kits/aun/role.md +0 -25
  89. package/kits/templates/group.md +0 -20
  90. package/kits/templates/private.md +0 -9
  91. package/kits/templates/system-fragments/personal-context.md +0 -3
  92. package/kits/templates/system-fragments/self-intro.md +0 -5
  93. package/kits/templates/system-fragments/speaker-intro.md +0 -5
  94. package/kits/templates/system-fragments/venue-intro.md +0 -5
  95. /package/kits/{channels → docs/channels}/aun.md +0 -0
  96. /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
  97. /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
  98. /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
  99. /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
@@ -9,6 +9,7 @@ import { normalizeChannelInstances, getChannelShowActivities } from '../utils/ch
9
9
  import { resolvePaths, getPackageRoot } from '../paths.js';
10
10
  import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
11
11
  import { appendAidEvent } from '../utils/instance-registry.js';
12
+ import { appendAidLifecycle } from '../aun/aid/identity.js';
12
13
  import { loadAgent, saveAgent } from '../config-store.js';
13
14
  import { getProcessStartTime } from '../utils/process-introspect.js';
14
15
  import * as outbox from '../aun/outbox.js';
@@ -63,6 +64,7 @@ export class AUNChannel {
63
64
  traceWriter = null;
64
65
  eventBus = null;
65
66
  ownerBoundHandler = null;
67
+ queuedHandler = null;
66
68
  pendingEchoMessages = new Map();
67
69
  isEchoSending = false;
68
70
  trace(dir, event, data) {
@@ -168,7 +170,7 @@ export class AUNChannel {
168
170
  const name = cached?.name;
169
171
  return name && name !== short ? `${short}(${name})` : short;
170
172
  }
171
- extractTextPayload(payload, channelId) {
173
+ extractTextPayload(payload, channelId, senderAid) {
172
174
  if (typeof payload === 'string')
173
175
  return payload;
174
176
  if (payload && typeof payload === 'object') {
@@ -176,21 +178,38 @@ export class AUNChannel {
176
178
  const text = typeof obj.text === 'string' ? obj.text : '';
177
179
  // action_card_reply:卡片交互回复,触发 interactionCallback,不分发给 agent
178
180
  if (obj.type === 'action_card_reply') {
179
- const cardMsgId = typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
181
+ const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
182
+ : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
180
183
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
181
184
  if (cardInfo) {
182
- const actionValue = typeof obj.action_value === 'string' ? obj.action_value : text;
185
+ const actionValue = typeof obj.value === 'string' ? obj.value
186
+ : typeof obj.action_value === 'string' ? obj.action_value : text;
183
187
  if (cardInfo.isCommandCard) {
184
188
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息
185
189
  this.cardMessageIdMap.delete(cardMsgId);
186
190
  if (this.messageHandler && actionValue.startsWith('/')) {
187
191
  const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
192
+ // 卡片点击者身份:优先 payload.from / payload.sender_aid / payload.user_id,
193
+ // 再 fallback 到外层 senderAid,最后用 cardInfo 中记录的原始命令发起者
194
+ const cardClickerAid = (typeof obj.from === 'string' && obj.from)
195
+ || (typeof obj.sender_aid === 'string' && obj.sender_aid)
196
+ || (typeof obj.user_id === 'string' && obj.user_id)
197
+ || senderAid
198
+ || cardInfo.initiatorAid
199
+ || channelId || '';
200
+ // Initiator 校验:群聊中仅卡片发起者可操作(与飞书行为对齐)
201
+ if (cardInfo.initiatorAid && cardClickerAid
202
+ && cardClickerAid !== cardInfo.initiatorAid
203
+ && !this.isGroupId(cardClickerAid)) {
204
+ logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
205
+ return '';
206
+ }
188
207
  this.messageHandler({
189
208
  channelId: channelId || '',
190
209
  chatType,
191
210
  content: actionValue,
192
- peerId: channelId || '',
193
- peerName: typeof obj.action_label === 'string' ? obj.action_label : undefined,
211
+ peerId: cardClickerAid,
212
+ peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
194
213
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
195
214
  source: 'card-trigger',
196
215
  });
@@ -205,7 +224,7 @@ export class AUNChannel {
205
224
  type: 'interaction.response',
206
225
  id: cardInfo.requestId,
207
226
  action: actionValue,
208
- values: { text, action_label: obj.action_label, behavior: obj.behavior },
227
+ values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
209
228
  });
210
229
  }
211
230
  }
@@ -216,23 +235,126 @@ export class AUNChannel {
216
235
  // 始终返回空字符串,阻止消息分发给 agent
217
236
  return '';
218
237
  }
219
- // quote 类型:拼接被引用内容
238
+ // quote 类型:拼接被引用内容(支持 text / image / file attachments)
220
239
  if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
221
240
  const q = obj.quote;
222
241
  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');
242
+ const sender = typeof q.sender_display === 'string' ? q.sender_display : '';
243
+ const prefix = sender ? `${sender}: ` : '';
244
+ // 构建引用内容:文本 + 附件描述
245
+ const quoteParts = [];
246
+ if (quotedText)
247
+ quoteParts.push(quotedText);
248
+ if (Array.isArray(q.attachments)) {
249
+ for (const att of q.attachments) {
250
+ if (att && typeof att === 'object') {
251
+ const ct = typeof att.content_type === 'string' ? att.content_type : '';
252
+ const fn = typeof att.filename === 'string' ? att.filename : '';
253
+ if (ct.startsWith('image/')) {
254
+ quoteParts.push(fn ? `[图片: ${fn}]` : '[图片]');
255
+ }
256
+ else {
257
+ quoteParts.push(fn ? `[文件: ${fn}]` : '[文件]');
258
+ }
259
+ }
260
+ }
261
+ }
262
+ if (quoteParts.length > 0) {
263
+ const lines = quoteParts.join('\n').split('\n');
264
+ const quoted = lines.map((line, i) => `> ${i === 0 ? prefix : ''}${line}`).join('\n');
227
265
  return text ? `${quoted}\n\n${text}` : quoted;
228
266
  }
229
267
  }
268
+ // merge 类型:合并转发消息,展开子消息为可读文本
269
+ if (obj.type === 'merge') {
270
+ const title = typeof obj.title === 'string' ? obj.title : '合并转发消息';
271
+ const parts = [`以下是转发的合并消息「${title}」:\n---`];
272
+ if (Array.isArray(obj.items)) {
273
+ for (const item of obj.items) {
274
+ if (item && typeof item === 'object') {
275
+ const sender = typeof item.sender_display === 'string' ? item.sender_display : '';
276
+ const itemText = typeof item.text === 'string' ? item.text : '';
277
+ const itemType = typeof item.type === 'string' ? item.type : '';
278
+ // 根据子消息类型构建展示
279
+ const lineParts = [];
280
+ if (itemText)
281
+ lineParts.push(itemText);
282
+ // 子消息附件(image/file)
283
+ if (Array.isArray(item.attachments)) {
284
+ for (const att of item.attachments) {
285
+ if (att && typeof att === 'object') {
286
+ const ct = typeof att.content_type === 'string' ? att.content_type : '';
287
+ const fn = typeof att.filename === 'string' ? att.filename : '';
288
+ if (ct.startsWith('image/') || itemType === 'image') {
289
+ lineParts.push(fn ? `[图片: ${fn}]` : '[图片]');
290
+ }
291
+ else if (ct.startsWith('video/') || itemType === 'video') {
292
+ lineParts.push(fn ? `[视频: ${fn}]` : '[视频]');
293
+ }
294
+ else {
295
+ lineParts.push(fn ? `[文件: ${fn}]` : '[文件]');
296
+ }
297
+ }
298
+ }
299
+ }
300
+ const content = lineParts.join(' ') || `[${itemType || '未知类型'}]`;
301
+ parts.push(sender ? `${sender}: ${content}` : content);
302
+ }
303
+ }
304
+ }
305
+ if (typeof obj.summary === 'string' && obj.summary) {
306
+ parts.push(`\n[摘要] ${obj.summary}`);
307
+ }
308
+ parts.push('---');
309
+ return parts.join('\n');
310
+ }
230
311
  if (typeof obj.text === 'string')
231
312
  return text;
232
313
  return JSON.stringify(payload);
233
314
  }
234
315
  return '';
235
316
  }
317
+ /** 收集 payload 中所有需要下载的 attachments(顶层 + merge.items + quote.quote),按 url 去重 */
318
+ collectAllAttachments(payload) {
319
+ if (!payload || typeof payload !== 'object')
320
+ return [];
321
+ const obj = payload;
322
+ const result = [];
323
+ const seen = new Set();
324
+ const add = (att) => {
325
+ if (!att || typeof att !== 'object')
326
+ return;
327
+ const key = att.url || att.object_key || '';
328
+ if (key && seen.has(key))
329
+ return;
330
+ if (key)
331
+ seen.add(key);
332
+ result.push(att);
333
+ };
334
+ // 顶层 attachments
335
+ if (Array.isArray(obj.attachments)) {
336
+ for (const att of obj.attachments)
337
+ add(att);
338
+ }
339
+ // merge.items 中的子消息 attachments
340
+ if (obj.type === 'merge' && Array.isArray(obj.items)) {
341
+ for (const item of obj.items) {
342
+ if (item && typeof item === 'object' && Array.isArray(item.attachments)) {
343
+ for (const att of item.attachments)
344
+ add(att);
345
+ }
346
+ }
347
+ }
348
+ // quote.quote 中的 attachments
349
+ if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
350
+ const q = obj.quote;
351
+ if (Array.isArray(q.attachments)) {
352
+ for (const att of q.attachments)
353
+ add(att);
354
+ }
355
+ }
356
+ return result;
357
+ }
236
358
  hasExplicitMention(text, target) {
237
359
  const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
238
360
  return new RegExp(`(^|\\s)@${escaped}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`).test(text);
@@ -282,31 +404,24 @@ export class AUNChannel {
282
404
  }
283
405
  return out;
284
406
  }
285
- buildGroupReplyContext(taskId, senderAid, encrypted) {
407
+ buildGroupReplyContext(taskId, senderAid, encrypted, messageId) {
286
408
  const replyContext = { metadata: { encrypted } };
287
409
  if (taskId)
288
410
  replyContext.threadId = taskId;
289
411
  replyContext.peerId = senderAid;
412
+ if (messageId)
413
+ replyContext.replyToMessageId = messageId;
290
414
  return replyContext;
291
415
  }
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
- }
416
+ acknowledgeImmediately(messageId, _seq) {
417
+ // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
418
+ // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
298
419
  if (messageId)
299
420
  this.messageSeqMap.delete(messageId);
300
421
  }
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;
422
+ shouldEncrypt(_peerId) {
423
+ // Default to plaintext; only encrypt when session is explicitly marked encrypted
424
+ return false;
310
425
  }
311
426
  _aid;
312
427
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
@@ -318,7 +433,6 @@ export class AUNChannel {
318
433
  peerE2ee = new Map();
319
434
  static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
320
435
  plaintextRecv = 0;
321
- sessionModeResolver;
322
436
  interactionCallback;
323
437
  // action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
324
438
  cardMessageIdMap = new Map();
@@ -579,6 +693,7 @@ export class AUNChannel {
579
693
  }
580
694
  logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
581
695
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
696
+ appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
582
697
  // Send welcome message to owner after first connection
583
698
  await this.sendWelcomeMessage();
584
699
  }
@@ -625,7 +740,7 @@ export class AUNChannel {
625
740
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
626
741
  const ownerDisplayName = (ownerInfo.name || ownerAidClean.split('.')[0]).slice(0, 12);
627
742
  const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
628
- const currentName = currentNameMatch?.[1]?.trim();
743
+ const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
629
744
  const aidLabel = aidName.split('.')[0];
630
745
  let agentDisplayName;
631
746
  if (currentName && currentName !== aidLabel) {
@@ -634,13 +749,19 @@ export class AUNChannel {
634
749
  else {
635
750
  agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
636
751
  }
752
+ // Preserve user-provided description (from `agent new --description`), fallback to default
753
+ const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
754
+ const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
755
+ const agentDescription = currentDesc
756
+ ? currentDesc
757
+ : 'EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道';
637
758
  // Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
638
759
  const newAgentMd = `---
639
760
  aid: "${aid}"
640
761
  name: "${agentDisplayName}"
641
762
  type: "codeagent"
642
763
  version: "1.0.0"
643
- description: "EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道"
764
+ description: "${agentDescription}"
644
765
  tags:
645
766
  - evolclaw
646
767
  - ai-agent
@@ -720,11 +841,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
720
841
  async downloadAttachment(att, channelId) {
721
842
  const ownerAid = att.owner_aid || this._aid || '';
722
843
  const objectKey = att.object_key;
723
- const filename = att.filename || objectKey.split('/').pop() || 'unknown';
724
844
  if (!objectKey) {
725
845
  logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
726
846
  return null;
727
847
  }
848
+ const filename = att.filename || objectKey.split('/').pop() || 'unknown';
728
849
  let downloadUrl;
729
850
  try {
730
851
  const ticket = await this.callAndTrace('storage.create_download_ticket', {
@@ -802,10 +923,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
802
923
  if (this._aid && text.includes(`@${this._aid}`)) {
803
924
  mentions.push(this._aid);
804
925
  }
805
- // Process attachments
806
- const rawAttachments = Array.isArray(payload?.attachments)
807
- ? payload.attachments
808
- : [];
926
+ // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
927
+ const rawAttachments = this.collectAllAttachments(payload);
809
928
  let finalText = text;
810
929
  if (rawAttachments.length > 0 && this.client) {
811
930
  const fileParts = [];
@@ -837,6 +956,24 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
837
956
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
838
957
  if (p2pPayloadType === 'action_card_reply')
839
958
  return;
959
+ // menu.query:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
960
+ if (p2pPayloadType === 'menu.query') {
961
+ this.acknowledgeImmediately(messageId, seq);
962
+ this.dispatchMessage({
963
+ channelId: chatId, userId: fromAid,
964
+ text: JSON.stringify(payload),
965
+ chatType: 'private', messageId, seq,
966
+ peerName: displayName || undefined,
967
+ peerType: peerInfo.type || undefined,
968
+ });
969
+ return;
970
+ }
971
+ // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
972
+ if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
973
+ this.acknowledgeImmediately(messageId, seq);
974
+ logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
975
+ return;
976
+ }
840
977
  logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
841
978
  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
979
  const isSystemP2P = p2pPayloadType === 'event';
@@ -865,7 +1002,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
865
1002
  const groupId = msg.group_id ?? '';
866
1003
  const senderAid = msg.sender_aid ?? '';
867
1004
  const payload = msg.payload ?? '';
868
- const text = this.extractTextPayload(payload, groupId);
1005
+ const text = this.extractTextPayload(payload, groupId, senderAid);
869
1006
  const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
870
1007
  const messageId = msg.message_id ?? '';
871
1008
  const seq = msg.seq;
@@ -884,7 +1021,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
884
1021
  logger.debug(`${this.logPrefix()} Group dropped: own message (group=${groupId} mid=${messageId})`);
885
1022
  return;
886
1023
  }
887
- // 短 echo 快速通道:连通性测试要尽量低延迟,命中后绕过所有 await(sessionModeResolver / 后续 mention 过滤)
1024
+ // 短 echo 快速通道:连通性测试要尽量低延迟,命中后绕过所有 await(后续 mention 过滤)
888
1025
  {
889
1026
  const firstLineFast = text.split('\n')[0] || '';
890
1027
  const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
@@ -906,25 +1043,37 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
906
1043
  peerName: displayName,
907
1044
  peerType: peerInfo?.type || 'unknown',
908
1045
  seq,
909
- replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast),
1046
+ replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId),
910
1047
  createdAt,
911
1048
  });
912
1049
  return;
913
1050
  }
914
1051
  }
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
- }
1052
+ // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1053
+ {
1054
+ const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1055
+ if (payloadType === 'action_card_reply')
1056
+ return;
1057
+ }
1058
+ // ── payload 类型白名单(所有模式生效) ──
1059
+ // 信号类消息(status / event / thought / task.update 等)不进 Agent
1060
+ {
1061
+ const payloadObj = (payload && typeof payload === 'object') ? payload : null;
1062
+ const payloadType = payloadObj?.type ?? '';
1063
+ // menu.query:自定义消息快速路径
1064
+ if (payloadType === 'menu.query') {
1065
+ this.acknowledgeImmediately(messageId, seq);
1066
+ this.dispatchMessage({
1067
+ channelId: groupId, userId: senderAid,
1068
+ text: JSON.stringify(payload),
1069
+ chatType: 'group', messageId, seq, groupId,
1070
+ });
1071
+ return;
1072
+ }
1073
+ if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
1074
+ this.acknowledgeImmediately(messageId, seq);
1075
+ logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
1076
+ return;
928
1077
  }
929
1078
  }
930
1079
  // 记录入站消息加密状态,透传到出站 ReplyContext
@@ -958,13 +1107,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
958
1107
  this.pendingEchoMessages.set(messageId, {
959
1108
  text: echoText,
960
1109
  channelId: groupId,
961
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted),
1110
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId),
962
1111
  receiveTs: Date.now(),
963
1112
  });
964
1113
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
965
1114
  }
966
1115
  else if (/echo/i.test(firstLineGroup) && hasEvolClawTraceGroup) {
967
- // 回声炸弹:已被 trace 过的 echo,直接丢弃
1116
+ // 回声炸弹:已被任何 EvolClaw 节点 trace 过的 echo,直接丢弃
968
1117
  this.acknowledgeImmediately(messageId, seq);
969
1118
  logger.info(`${this.logPrefix()} Group dropped: echo bomb (already-traced group=${groupId} sender=${senderAid} mid=${messageId})`);
970
1119
  return;
@@ -978,10 +1127,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
978
1127
  }
979
1128
  }
980
1129
  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
- : [];
1130
+ // Detect attachments before the empty-text guard (顶层 + 嵌套)
1131
+ const rawAttachments = this.collectAllAttachments(payload);
985
1132
  const hasAttachments = rawAttachments.length > 0;
986
1133
  // Allow through if there's text OR attachments; both-empty messages are silently dropped
987
1134
  if (!strippedText && !hasAttachments) {
@@ -1045,7 +1192,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1045
1192
  seq,
1046
1193
  taskId,
1047
1194
  mentions,
1048
- replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted),
1195
+ replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId),
1049
1196
  });
1050
1197
  }
1051
1198
  dispatchMessage(event) {
@@ -1068,7 +1215,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1068
1215
  this.handleEcho(event);
1069
1216
  return;
1070
1217
  }
1071
- // 回声炸弹:已被本系统 trace 过的 echo,直接丢弃
1218
+ // 回声炸弹:已被任何 EvolClaw 节点 trace 过的 echo,直接丢弃(防止多 agent 间无限回声)
1072
1219
  if (/echo/i.test(firstLine) && hasEvolClawTracePrivate) {
1073
1220
  logger.info(`${this.logPrefix()} Dropped: echo bomb (already-traced mid=${event.messageId} chat=${event.chatType})`);
1074
1221
  return;
@@ -1258,16 +1405,46 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1258
1405
  const d = data;
1259
1406
  const reason = d.reason ?? '';
1260
1407
  const error = d.error ?? 'unknown';
1408
+ const code = d.code ?? d.detail?.code ?? 0;
1409
+ const detail = (d.detail && typeof d.detail === 'object') ? d.detail : {};
1261
1410
  if (this.intentionalDisconnect)
1262
1411
  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');
1412
+ if (this.isKickReason(reason) || code >= 4001) {
1413
+ // @ts-ignore — methods defined below in same class
1414
+ const kickDetail = this.buildKickDetail(code, reason, detail);
1415
+ // @ts-ignore methods defined below in same class
1416
+ const action = this.classifyKickAction(code);
1417
+ appendAidEvent({
1418
+ ts: Date.now(), iso: new Date().toISOString(),
1419
+ event: 'kicked', aid: this.config.aid,
1420
+ code, reason, action,
1421
+ evictedBy: kickDetail.evictedBy,
1422
+ quotaKind: kickDetail.quotaKind,
1423
+ });
1424
+ appendAidLifecycle({
1425
+ ts: Date.now(), iso: new Date().toISOString(),
1426
+ event: 'kicked', aid: this.config.aid,
1427
+ code, reason, action,
1428
+ evictedBy: kickDetail.evictedBy,
1429
+ newExtra: kickDetail.newExtra,
1430
+ quotaKind: kickDetail.quotaKind,
1431
+ });
1432
+ if (action === 'no_retry') {
1433
+ logger.error(`${this.logPrefix()} Kicked (code=${code}): ${reason} — will NOT retry`);
1434
+ this.setAidStatus('kicked_no_retry', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
1435
+ }
1436
+ else if (action === 'retry_once') {
1437
+ logger.warn(`${this.logPrefix()} Kicked (code=${code}): ${reason} — retrying once after ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
1438
+ this.setAidStatus('kicked', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
1439
+ this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'kicked');
1440
+ }
1441
+ else {
1442
+ logger.warn(`${this.logPrefix()} Kicked (code=${code}): ${reason} — retrying after ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s`);
1443
+ this.setAidStatus('kicked', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
1444
+ this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'kicked');
1445
+ }
1268
1446
  }
1269
1447
  else {
1270
- // 其他 terminal failure(含 max_attempts_exhausted 兜底)→ 1min 后再试
1271
1448
  logger.error(`${this.logPrefix()} Terminal failure: ${error}${reason ? ` (${reason})` : ''}, retrying in ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
1272
1449
  this.setAidStatus('failed', { lastError: `${error}`.slice(0, 80) });
1273
1450
  this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'terminal');
@@ -1281,11 +1458,58 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1281
1458
  const r = reason.toLowerCase();
1282
1459
  if (r.includes('kicked') || r.includes('kick'))
1283
1460
  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))
1461
+ if (/close code 40\d{2}/.test(r))
1286
1462
  return true;
1287
1463
  return false;
1288
1464
  }
1465
+ /**
1466
+ * 根据 close code 决定重试策略:
1467
+ * - 'no_retry': 不重试(被挤掉、AID 无效、ACL 拒绝、长连接已存在、配额超限)
1468
+ * - 'retry_once': 重试一次(auth 失败可能 token 刚过期、nonce 无效)
1469
+ * - 'retry_delay': 延迟重试(短连接容量超限、空闲超时)
1470
+ */
1471
+ classifyKickAction(code) {
1472
+ switch (code) {
1473
+ case 4003: // AID 无效
1474
+ case 4009: // 服务端主动踢
1475
+ case 4011: // ACL 拒绝
1476
+ case 4012: // 长连接已存在(自己另一个实例在线)
1477
+ case 4015: // 被新连接挤掉
1478
+ return 'no_retry';
1479
+ case 4001: // auth 失败(token 可能刚过期)
1480
+ case 4010: // nonce 无效
1481
+ return 'retry_once';
1482
+ case 4008: // auth 超时
1483
+ case 4013: // 短连接容量超限
1484
+ case 4014: // 短连接空闲超时
1485
+ return 'retry_delay';
1486
+ default:
1487
+ return 'retry_delay';
1488
+ }
1489
+ }
1490
+ buildKickDetail(code, reason, detail) {
1491
+ const evictedByRaw = detail.evicted_by || detail.new_extra_info;
1492
+ let evictedBy;
1493
+ if (evictedByRaw && typeof evictedByRaw === 'object') {
1494
+ evictedBy = {
1495
+ aid: evictedByRaw.aid,
1496
+ deviceId: evictedByRaw.device_id,
1497
+ slotId: evictedByRaw.slot_id,
1498
+ app: evictedByRaw.app,
1499
+ hostname: evictedByRaw.hostname,
1500
+ };
1501
+ }
1502
+ return {
1503
+ code,
1504
+ reason,
1505
+ ts: Date.now(),
1506
+ evictedBy,
1507
+ quotaKind: detail.quota_kind,
1508
+ limit: detail.limit,
1509
+ selfExtra: detail.self_extra_info,
1510
+ newExtra: detail.new_extra_info,
1511
+ };
1512
+ }
1289
1513
  /**
1290
1514
  * TS 层接管重连:force close 当前 SDK client,安排 delayMs 后重新 initClient。
1291
1515
  * 用于 flap / kicked / terminal_failed 三类场景,统一退避路径。
@@ -1347,7 +1571,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1347
1571
  if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
1348
1572
  this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
1349
1573
  }
1574
+ if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
1575
+ this.eventBus.unsubscribe('task:queued', this.queuedHandler);
1576
+ }
1350
1577
  this.ownerBoundHandler = null;
1578
+ this.queuedHandler = null;
1351
1579
  this.eventBus = bus;
1352
1580
  if (bus && typeof bus.subscribe === 'function') {
1353
1581
  const handler = (event) => {
@@ -1365,6 +1593,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1365
1593
  };
1366
1594
  bus.subscribe('channel:owner-bound', handler);
1367
1595
  this.ownerBoundHandler = handler;
1596
+ const queuedHandler = (event) => {
1597
+ if (event.channel !== this.config.channelName)
1598
+ return;
1599
+ this.sendProcessingStatus(event.channelId, 'queued', '', '', event.replyContext);
1600
+ };
1601
+ bus.subscribe('task:queued', queuedHandler);
1602
+ this.queuedHandler = queuedHandler;
1368
1603
  }
1369
1604
  }
1370
1605
  onProjectPathRequest(provider) {
@@ -1373,9 +1608,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1373
1608
  onMessage(handler) {
1374
1609
  this.messageHandler = handler;
1375
1610
  }
1376
- setSessionModeResolver(resolver) {
1377
- this.sessionModeResolver = resolver;
1378
- }
1379
1611
  setDispatchModeResolver(resolver) {
1380
1612
  this.dispatchModeResolver = resolver;
1381
1613
  }
@@ -1480,6 +1712,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1480
1712
  payload.task_id = context.metadata.taskId;
1481
1713
  if (context?.metadata?.chatmode)
1482
1714
  payload.chatmode = context.metadata.chatmode;
1715
+ // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
1716
+ 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
1717
  const isGroup = this.isGroupId(channelId);
1484
1718
  const targetAid = channelId;
1485
1719
  const encryptTarget = isGroup ? channelId : targetAid;
@@ -1491,7 +1725,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1491
1725
  if (isGroup) {
1492
1726
  params.group_id = channelId;
1493
1727
  const result = await this.callAndTrace('group.send', params);
1494
- if (!result || !result.message_id) {
1728
+ const mid = result?.message?.message_id ?? result?.message_id ?? null;
1729
+ if (!mid) {
1495
1730
  const dispatchStatus = result?.message_dispatch?.status;
1496
1731
  if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
1497
1732
  logger.info(`${this.logPrefix()} group.send ok (${dispatchStatus}): group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
@@ -1501,8 +1736,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1501
1736
  }
1502
1737
  }
1503
1738
  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 });
1739
+ logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1740
+ 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 });
1506
1741
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText);
1507
1742
  }
1508
1743
  }
@@ -1529,7 +1764,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1529
1764
  if (isGroup) {
1530
1765
  this.trace('OUT', 'group.send.fallback', params);
1531
1766
  const result = await this.client.call('group.send', params);
1532
- this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message_id });
1767
+ this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message?.message_id ?? result?.message_id });
1533
1768
  if (!result || !result.message_id) {
1534
1769
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
1535
1770
  }
@@ -1585,13 +1820,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1585
1820
  const stage = payload?.stage ?? `items=${itemCount}`;
1586
1821
  if (this.isGroupId(channelId)) {
1587
1822
  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}`);
1823
+ const putRes = await this.callAndTrace('group.thought.put', params);
1824
+ const tid = putRes?.thought_id;
1825
+ logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1590
1826
  }
1591
1827
  else {
1592
1828
  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}`);
1829
+ const putRes = await this.callAndTrace('message.thought.put', params);
1830
+ const tid = putRes?.thought_id;
1831
+ logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1595
1832
  }
1596
1833
  }
1597
1834
  catch (e) {
@@ -1622,8 +1859,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1622
1859
  if (isGroup) {
1623
1860
  params.group_id = channelId;
1624
1861
  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;
1862
+ const mid = result?.message?.message_id ?? result?.message_id ?? null;
1863
+ logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
1864
+ return mid;
1627
1865
  }
1628
1866
  else {
1629
1867
  params.to = targetAid;
@@ -1750,8 +1988,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1750
1988
  params.group_id = channelId;
1751
1989
  this.trace('OUT', 'group.send.file', params);
1752
1990
  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) {
1991
+ const fileMid = result?.message?.message_id ?? result?.message_id;
1992
+ this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
1993
+ if (!fileMid) {
1755
1994
  logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
1756
1995
  }
1757
1996
  }
@@ -1777,8 +2016,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1777
2016
  if (isGroup) {
1778
2017
  this.trace('OUT', 'group.send.file.fallback', params);
1779
2018
  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) {
2019
+ const fbMid = result?.message?.message_id ?? result?.message_id;
2020
+ this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
2021
+ if (!fbMid) {
1782
2022
  logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
1783
2023
  }
1784
2024
  }
@@ -1850,30 +2090,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1850
2090
  this.sentCount.delete(channelId); // 新任务开始,重置计数
1851
2091
  if (!this.client || !this.connected)
1852
2092
  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
2093
  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
2094
  const stateMap = {
1872
2095
  start: 'started',
1873
2096
  done: 'completed',
1874
2097
  interrupted: 'interrupted',
1875
2098
  error: 'error',
1876
2099
  timeout: 'timeout',
2100
+ queued: 'queued',
1877
2101
  };
1878
2102
  const statusPayload = {
1879
2103
  type: 'status',
@@ -1884,12 +2108,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1884
2108
  };
1885
2109
  if (context?.threadId)
1886
2110
  statusPayload.thread_id = context.threadId;
2111
+ if (context?.peerId)
2112
+ statusPayload.initiator = context.peerId;
2113
+ if (context?.replyToMessageId)
2114
+ statusPayload.ref_message_id = context.replyToMessageId;
1887
2115
  const isGroup = this.isGroupId(channelId);
1888
2116
  // 私聊 channelId = 对端 AID(不含 device_id)
1889
2117
  const statusTargetAid = channelId;
1890
2118
  const encryptTarget = isGroup ? channelId : statusTargetAid;
1891
- // 计算 encrypt 标志(每次调用读最新 peerE2ee 状态,
1892
- // 这样第二条 send 能受益于第一条触发的 peerE2ee 标记)
1893
2119
  const computeEncrypt = () => context?.metadata?.encrypted != null
1894
2120
  ? !!(context.metadata.encrypted)
1895
2121
  : this.shouldEncrypt(encryptTarget);
@@ -1922,17 +2148,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1922
2148
  });
1923
2149
  };
1924
2150
  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);
2151
+ sendOne(method, statusPayload, 'status');
1931
2152
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
1932
2153
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
1933
2154
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
1934
2155
  const chatmode = context?.metadata?.chatmode ?? '?';
1935
- logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel}`);
2156
+ const initiator = statusPayload.initiator ?? '';
2157
+ const refMsgId = statusPayload.ref_message_id ?? '';
2158
+ logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}`);
1936
2159
  }
1937
2160
  sendCustomPayload(channelId, payload) {
1938
2161
  if (!this.client || !this.connected)
@@ -1980,6 +2203,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1980
2203
  }
1981
2204
  this.connected = false;
1982
2205
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
2206
+ appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
1983
2207
  this.setAidStatus('disabled');
1984
2208
  if (this.traceWriter) {
1985
2209
  this.traceWriter.close();
@@ -2159,7 +2383,7 @@ export class AUNChannelPlugin {
2159
2383
  case 'result.error': {
2160
2384
  const sendCtx = { ...(ctx ?? {}) };
2161
2385
  if (payload.kind === 'result.text' && payload.isFinal)
2162
- sendCtx.title = ' 最终回复:';
2386
+ sendCtx.title = ' 最终回复:';
2163
2387
  await channel.sendMessage(channelId, payload.text, sendCtx);
2164
2388
  return;
2165
2389
  }
@@ -2186,16 +2410,23 @@ export class AUNChannelPlugin {
2186
2410
  };
2187
2411
  if (ctx?.threadId)
2188
2412
  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
- ]);
2413
+ if (envelope.chatmode === 'proactive') {
2414
+ await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
2415
+ }
2416
+ else {
2417
+ await Promise.all([
2418
+ channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
2419
+ channel.sendStructured(channelId, aunPayload, ctx),
2420
+ ]);
2421
+ }
2194
2422
  return;
2195
2423
  }
2196
2424
  case 'status.started':
2197
2425
  channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
2198
2426
  return;
2427
+ case 'status.queued':
2428
+ channel.sendProcessingStatus(channelId, 'queued', envelope.taskId, envelope.taskId, ctx);
2429
+ return;
2199
2430
  case 'status.completed':
2200
2431
  channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx);
2201
2432
  return;
@@ -2215,7 +2446,6 @@ export class AUNChannelPlugin {
2215
2446
  const aunCard = {
2216
2447
  type: 'action_card',
2217
2448
  title: action.title,
2218
- description: action.body,
2219
2449
  actions: action.buttons.map(btn => ({
2220
2450
  label: btn.label,
2221
2451
  value: btn.key,
@@ -2223,6 +2453,8 @@ export class AUNChannelPlugin {
2223
2453
  behavior: 'reply',
2224
2454
  })),
2225
2455
  };
2456
+ if (action.body)
2457
+ aunCard.description = action.body;
2226
2458
  if (ctx?.threadId)
2227
2459
  aunCard.thread_id = ctx.threadId;
2228
2460
  const msgId = await channel.sendStructured(channelId, aunCard, ctx);
@@ -2236,7 +2468,6 @@ export class AUNChannelPlugin {
2236
2468
  const aunCard = {
2237
2469
  type: 'action_card',
2238
2470
  title: card.title,
2239
- description: card.body,
2240
2471
  actions: card.buttons.map(btn => ({
2241
2472
  label: btn.label,
2242
2473
  value: btn.command,
@@ -2244,11 +2475,13 @@ export class AUNChannelPlugin {
2244
2475
  behavior: 'reply',
2245
2476
  })),
2246
2477
  };
2478
+ if (card.body)
2479
+ aunCard.description = card.body;
2247
2480
  if (ctx?.threadId)
2248
2481
  aunCard.thread_id = ctx.threadId;
2249
2482
  const msgId = await channel.sendStructured(channelId, aunCard, ctx);
2250
2483
  if (msgId) {
2251
- channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true });
2484
+ channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
2252
2485
  setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2253
2486
  }
2254
2487
  }
@@ -2323,6 +2556,7 @@ export class AUNChannelPlugin {
2323
2556
  chatType: opts.chatType || 'private',
2324
2557
  peerId: opts.peerId || '',
2325
2558
  peerName: opts.peerName,
2559
+ peerType: opts.peerType,
2326
2560
  messageId: opts.messageId,
2327
2561
  mentions: opts.mentions,
2328
2562
  threadId: opts.threadId,
@@ -2345,12 +2579,6 @@ export class AUNChannelPlugin {
2345
2579
  });
2346
2580
  });
2347
2581
  }
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
2582
  if (typeof channel.setDispatchModeResolver === 'function') {
2355
2583
  channel.setDispatchModeResolver(async (channelId) => {
2356
2584
  const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);