evolcore 0.0.12 → 0.0.13

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/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
@@ -7,7 +7,7 @@ import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
9
9
  import { DEFAULT_FLUSH_DELAY_SECONDS } from '../types.js';
10
- import { resolvePaths, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
+ import { resolvePaths, agentDir as agentDirPath, resolveRoot, channelStatePath } from '../paths.js';
11
11
  import { saveToUploads, sanitizeFileName, bufferToInboundImage, safeFetch } from '../utils/media-cache.js';
12
12
  import { appendAidEvent } from '../utils/instance-registry.js';
13
13
  import { appendMessageLog, appendMessageLogStrict, buildOutboundEntry, buildInboundEntry, classifyAunPayloadForLog, hasMessageLogOperation } from '../core/message/message-log.js';
@@ -31,15 +31,36 @@ import { isHClassPath } from '../core/protected-paths.js';
31
31
  import { consumeAunCausation, registerAunCausation } from '../core/causation/aun-association.js';
32
32
  import { deriveCausation, normalizeCausation } from '../core/causation/context.js';
33
33
  import { recordCausationSpan } from '../core/causation/audit.js';
34
- import { isExplicitGroupId } from '../aun/group-identity.js';
35
34
  import { refreshAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
36
35
  import { readInstalledEvolcoreVersion } from '../utils/evolcore-version.js';
37
36
  import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
38
37
  import { hasMentionAll, mentionEntryAids, mentionEntryTargets, } from '../core/message/mention-schema.js';
39
38
  import { normalizeAunMentionEntries, } from '../aun/msg/mention-schema.js';
39
+ import { isDeliveryTarget, sameDeliveryTarget } from '../core/message/message-utils.js';
40
40
  export const AUN_HANDOFF_MARKER_FIELD = '_evolcore_handoff_id';
41
41
  const AUN_INTERACTION_CARD_TTL_MS = 24 * 60 * 60 * 1000;
42
+ const AUN_INBOUND_DEDUP_TTL_MS = 7 * 24 * 60 * 60 * 1000;
42
43
  const IMAGE_MIME_TYPE = /^image\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/i;
44
+ function requireEnvelopeDelivery(channelId, envelopeDelivery, nestedDelivery) {
45
+ if (!isDeliveryTarget(envelopeDelivery)) {
46
+ throw Object.assign(new Error(`AUN outbound envelope requires a top-level delivery route for channelId=${channelId}`), {
47
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
48
+ });
49
+ }
50
+ if (nestedDelivery !== undefined) {
51
+ if (!isDeliveryTarget(nestedDelivery) || !sameDeliveryTarget(envelopeDelivery, nestedDelivery)) {
52
+ throw Object.assign(new Error(`AUN outbound envelope has conflicting delivery routes for channelId=${channelId}`), {
53
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
54
+ });
55
+ }
56
+ }
57
+ if (envelopeDelivery.chatType === 'group' && envelopeDelivery.groupId !== channelId) {
58
+ throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
59
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
60
+ });
61
+ }
62
+ return envelopeDelivery;
63
+ }
43
64
  function normalizeImageMimeType(mimeType) {
44
65
  const normalized = mimeType?.split(';', 1)[0].trim().toLowerCase();
45
66
  if (!normalized)
@@ -133,6 +154,8 @@ export function aunOptsToInbound(opts, channel, channelType) {
133
154
  encrypted: opts.encrypted,
134
155
  protectedHeaders: opts.protectedHeaders,
135
156
  messageId: opts.messageId,
157
+ receivedAt: opts.receivedAt,
158
+ gatewaySeq: opts.gatewaySeq,
136
159
  causation: opts.causation,
137
160
  mentions: opts.mentions,
138
161
  mentionAids: opts.mentionAids,
@@ -289,14 +312,36 @@ export class AUNChannel {
289
312
  throw e;
290
313
  }
291
314
  }
292
- /** 判断 channelId 是否为群组 ID
293
- * - 新格式:group.{aidDomain}/{group_no|group_name}
294
- * - 数字群号:{group_no}.{aidDomain}(如 11117.example.com)
295
- * - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
315
+ /** Resolve the authenticated transport route for an ordinary outbound send.
316
+ * The channel id is an address only; it is intentionally never used to infer
317
+ * whether the address is a group.
296
318
  */
297
- /** 判断 channelId 是否群组 ID(public:plugin adapter 闭包需调用) */
298
- isGroupId(id) {
299
- return isExplicitGroupId(id);
319
+ requireDelivery(channelId, context, explicit) {
320
+ const supplied = explicit ?? context?.delivery;
321
+ if (supplied !== undefined && !isDeliveryTarget(supplied)) {
322
+ throw Object.assign(new Error(`AUN outbound send has an invalid delivery route for channelId=${channelId}`), {
323
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
324
+ });
325
+ }
326
+ if (!supplied) {
327
+ throw Object.assign(new Error(`AUN outbound send requires an explicit delivery route for channelId=${channelId}`), {
328
+ code: 'AUN_OUTBOUND_ROUTE_REQUIRED',
329
+ });
330
+ }
331
+ if (supplied.chatType === 'group' && supplied.groupId !== channelId) {
332
+ throw Object.assign(new Error(`AUN group delivery does not match channelId=${channelId}`), {
333
+ code: 'AUN_OUTBOUND_ROUTE_MISMATCH',
334
+ });
335
+ }
336
+ return supplied;
337
+ }
338
+ requirePersistedDelivery(channelId, delivery) {
339
+ // Persisted sends only trust the top-level route. In particular, old
340
+ // outbox entries cannot recover routing from their nested reply context.
341
+ return this.requireDelivery(channelId, undefined, delivery);
342
+ }
343
+ withDelivery(context, delivery) {
344
+ return { ...(context ?? {}), delivery };
300
345
  }
301
346
  getShortAid(aid) {
302
347
  if (!aid)
@@ -318,15 +363,19 @@ export class AUNChannel {
318
363
  const name = cached?.name;
319
364
  return name && name !== short ? `${short}(${name})` : short;
320
365
  }
321
- notifyCardActionFailure(channelId, text, operatorId, threadId) {
366
+ notifyCardActionFailure(channelId, text, operatorId, threadId, delivery) {
322
367
  if (!channelId) {
323
368
  logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
324
369
  return;
325
370
  }
326
- const context = {};
371
+ if (!delivery) {
372
+ logger.warn(`${this.logPrefix()} Card action failure without trusted delivery route: ${text}`);
373
+ return;
374
+ }
375
+ const context = { delivery };
327
376
  if (threadId)
328
377
  context.threadId = threadId;
329
- if (operatorId && this.isGroupId(channelId))
378
+ if (operatorId && delivery.chatType === 'group')
330
379
  context.peerId = operatorId;
331
380
  void this.sendMessage(channelId, text, context).catch((err) => {
332
381
  logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
@@ -336,7 +385,7 @@ export class AUNChannel {
336
385
  logger.info(`${this.logPrefix()} ActionInteraction audit: behavior=${behavior ?? 'reply'} `
337
386
  + `cardId=${cardMessageId} interactionId=${interactionId} result=${result} textLength=${textLength}`);
338
387
  }
339
- extractTextPayload(payload, channelId, senderAid) {
388
+ extractTextPayload(payload, channelId, senderAid, delivery) {
340
389
  if (typeof payload === 'string')
341
390
  return payload;
342
391
  if (payload && typeof payload === 'object') {
@@ -347,6 +396,7 @@ export class AUNChannel {
347
396
  const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
348
397
  : typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
349
398
  const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
399
+ const cardDelivery = cardInfo?.delivery ?? delivery;
350
400
  if (cardInfo) {
351
401
  // compose 按钮由新版客户端在卡片内采集输入;旧版客户端可继续从主编辑器
352
402
  // 采集。两者最终都回传 action_card_reply{behavior:"compose", text}。
@@ -371,25 +421,25 @@ export class AUNChannel {
371
421
  // payloads omit senderAid. A group channelId is only a conversation
372
422
  // identifier and must never be treated as the clicking operator.
373
423
  const cardClickerAid = senderAid
374
- || (channelId && !this.isGroupId(channelId) ? channelId : '')
424
+ || (cardDelivery?.chatType === 'private' && channelId ? channelId : '')
375
425
  || '';
376
426
  if (cardInfo.isCommandCard) {
377
427
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息。
378
428
  // 先完成点击者鉴权,再消费 mapping;否则群成员的一次误点会让发起者
379
429
  // 无法继续操作同一张卡片。
380
- const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
430
+ const chatType = cardDelivery?.chatType ?? 'private';
381
431
  if (this.messageHandler && actionValue.startsWith('/')) {
382
432
  // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
383
433
  // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
384
434
  if (chatType === 'group' && !cardClickerAid) {
385
435
  logger.info(`${this.logPrefix()} CommandCard rejected: missing authenticated clicker mid=${cardMsgId}`);
386
- this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
436
+ this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
387
437
  return '';
388
438
  }
389
439
  if (chatType === 'group' && cardInfo.initiatorAid
390
440
  && cardClickerAid !== cardInfo.initiatorAid) {
391
441
  logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
392
- this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
442
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
393
443
  return '';
394
444
  }
395
445
  // 已通过鉴权并即将进入处理流程;此时才消费 mapping,避免非发起者
@@ -408,28 +458,28 @@ export class AUNChannel {
408
458
  })).catch((err) => {
409
459
  logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
410
460
  const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
411
- this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
461
+ this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId, cardDelivery);
412
462
  });
413
463
  }
414
464
  else if (!this.messageHandler) {
415
- this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
465
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId, cardDelivery);
416
466
  }
417
467
  else {
418
- this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
468
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId, cardDelivery);
419
469
  }
420
470
  }
421
471
  else {
422
472
  // ActionInteraction:后端明确接受后才消费 mapping。撤权、过期或
423
473
  // 重复点击返回 false 时保持 fail-closed,并提示用户卡片已失效。
424
- if (channelId && this.isGroupId(channelId) && !cardClickerAid) {
474
+ if (cardDelivery?.chatType === 'group' && !cardClickerAid) {
425
475
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_unauthenticated', text.length);
426
- this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId);
476
+ this.notifyCardActionFailure(channelId, '⚠️ 无法验证卡片操作者身份', undefined, threadId, cardDelivery);
427
477
  return '';
428
478
  }
429
- if (channelId && this.isGroupId(channelId) && cardInfo.initiatorAid
479
+ if (cardDelivery?.chatType === 'group' && cardInfo.initiatorAid
430
480
  && cardClickerAid !== cardInfo.initiatorAid) {
431
481
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_non_initiator', text.length);
432
- this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
482
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId, cardDelivery);
433
483
  return '';
434
484
  }
435
485
  // Compose reply 入参校验:text 必须非空且未超长。
@@ -439,19 +489,19 @@ export class AUNChannel {
439
489
  const MAX_COMPOSE_TEXT_LENGTH = 64 * 1024; // 64K 字符
440
490
  if (!trimmedText) {
441
491
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_empty', text.length);
442
- this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId);
492
+ this.notifyCardActionFailure(channelId, '⚠️ 输入内容不能为空,请重新输入', cardClickerAid, threadId, cardDelivery);
443
493
  return '';
444
494
  }
445
495
  if (text.length > MAX_COMPOSE_TEXT_LENGTH) {
446
496
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_too_long', text.length);
447
- this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId);
497
+ this.notifyCardActionFailure(channelId, `⚠️ 输入内容过长(${text.length} 字符,限制 ${MAX_COMPOSE_TEXT_LENGTH}),请缩短后重试`, cardClickerAid, threadId, cardDelivery);
448
498
  return '';
449
499
  }
450
500
  }
451
501
  if (this.interactionCallback) {
452
502
  if (this.interactionActionsInFlight.has(cardMsgId)) {
453
503
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'rejected_in_flight', text.length);
454
- this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId);
504
+ this.notifyCardActionFailure(channelId, '⏳ 操作正在处理,请勿重复点击', cardClickerAid, threadId, cardDelivery);
455
505
  return '';
456
506
  }
457
507
  this.interactionActionsInFlight.add(cardMsgId);
@@ -484,7 +534,7 @@ export class AUNChannel {
484
534
  // captured the message id before the callback returned.
485
535
  this.forgetInteractionCard(cardMsgId, cardInfo.requestId);
486
536
  }
487
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId);
537
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', cardClickerAid, threadId, cardDelivery);
488
538
  };
489
539
  try {
490
540
  const result = this.interactionCallback(response);
@@ -493,7 +543,7 @@ export class AUNChannel {
493
543
  this.interactionActionsInFlight.delete(cardMsgId);
494
544
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
495
545
  logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
496
- this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
546
+ this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
497
547
  });
498
548
  }
499
549
  else {
@@ -504,19 +554,19 @@ export class AUNChannel {
504
554
  this.interactionActionsInFlight.delete(cardMsgId);
505
555
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'callback_error', text.length);
506
556
  logger.error(`${this.logPrefix()} ActionInteraction callback failed: request=${cardInfo.requestId}`, err);
507
- this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId);
557
+ this.notifyCardActionFailure(channelId, '❌ 操作失败,请稍后重试', cardClickerAid, threadId, cardDelivery);
508
558
  }
509
559
  }
510
560
  else {
511
561
  this.auditActionCardInteraction(behaviorRaw, cardMsgId, cardInfo.requestId, 'handler_unavailable', text.length);
512
- this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
562
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId, cardDelivery);
513
563
  }
514
564
  }
515
565
  }
516
566
  else if (this.ownedCardMsgIds.has(cardMsgId)) {
517
567
  // 本 agent 发出的卡片,但 entry 已过期(24h TTL)
518
568
  logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
519
- this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
569
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起', undefined, undefined, delivery);
520
570
  }
521
571
  else {
522
572
  // 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
@@ -751,8 +801,13 @@ export class AUNChannel {
751
801
  return sanitized;
752
802
  return undefined;
753
803
  }
754
- buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName) {
755
- const replyContext = { metadata: { encrypted, chatmode } };
804
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, groupId) {
805
+ if (!groupId)
806
+ throw new Error('buildGroupReplyContext requires groupId');
807
+ const replyContext = {
808
+ metadata: { encrypted, chatmode },
809
+ delivery: { chatType: 'group', groupId },
810
+ };
756
811
  if (threadId)
757
812
  replyContext.threadId = threadId;
758
813
  replyContext.peerId = senderAid;
@@ -766,6 +821,20 @@ export class AUNChannel {
766
821
  }
767
822
  return replyContext;
768
823
  }
824
+ buildPrivateReplyContext(threadId, senderAid, encrypted, messageId, chatmode, topicName, refMessageId) {
825
+ const replyContext = {
826
+ metadata: { encrypted, chatmode, refMessageId },
827
+ delivery: { chatType: 'private' },
828
+ peerId: senderAid,
829
+ };
830
+ if (threadId)
831
+ replyContext.threadId = threadId;
832
+ if (messageId)
833
+ replyContext.replyToMessageId = messageId;
834
+ if (topicName && threadId)
835
+ replyContext.metadata.topicName = topicName;
836
+ return replyContext;
837
+ }
769
838
  claimInboundMessage(kind, messageId, seq) {
770
839
  if (!messageId)
771
840
  return true;
@@ -788,9 +857,72 @@ export class AUNChannel {
788
857
  this.inboundSeenMessages.set(dedupeKey, { seenAt: Date.now(), seq });
789
858
  if (seq !== undefined)
790
859
  this.messageSeqMap.set(messageId, seq);
791
- setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), 5 * 60 * 1000);
860
+ this.appendInboundDedupe(dedupeKey, messageId, kind, seq);
861
+ setTimeout(() => this.inboundSeenMessages.delete(dedupeKey), AUN_INBOUND_DEDUP_TTL_MS);
792
862
  return true;
793
863
  }
864
+ inboundDedupeFile() {
865
+ const channelName = this.config.channelName ?? 'aun';
866
+ return channelStatePath(channelName, 'dedup', 'aun', `${encodeURIComponent(this.config.aid)}.jsonl`);
867
+ }
868
+ loadInboundDedupe() {
869
+ const file = this.inboundDedupeFile();
870
+ const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
871
+ let lines;
872
+ try {
873
+ lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
874
+ }
875
+ catch {
876
+ return;
877
+ }
878
+ let needsRewrite = false;
879
+ for (const line of lines) {
880
+ try {
881
+ const entry = JSON.parse(line);
882
+ if (typeof entry.key !== 'string' || typeof entry.seenAt !== 'number' || !Number.isFinite(entry.seenAt) || entry.seenAt <= cutoff) {
883
+ needsRewrite = true;
884
+ continue;
885
+ }
886
+ this.inboundSeenMessages.set(entry.key, {
887
+ seenAt: entry.seenAt,
888
+ seq: typeof entry.seq === 'number' && Number.isFinite(entry.seq) ? entry.seq : undefined,
889
+ });
890
+ }
891
+ catch {
892
+ needsRewrite = true;
893
+ }
894
+ }
895
+ if (needsRewrite)
896
+ this.rewriteInboundDedupe();
897
+ }
898
+ appendInboundDedupe(key, messageId, kind, seq) {
899
+ try {
900
+ const file = this.inboundDedupeFile();
901
+ fs.mkdirSync(path.dirname(file), { recursive: true });
902
+ fs.appendFileSync(file, JSON.stringify({ key, messageId, kind, seenAt: Date.now(), seq }) + '\n');
903
+ }
904
+ catch (error) {
905
+ logger.warn(`${this.logPrefix()} Failed to persist inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
906
+ }
907
+ }
908
+ rewriteInboundDedupe() {
909
+ const cutoff = Date.now() - AUN_INBOUND_DEDUP_TTL_MS;
910
+ for (const [key, value] of this.inboundSeenMessages) {
911
+ if (value.seenAt <= cutoff)
912
+ this.inboundSeenMessages.delete(key);
913
+ }
914
+ try {
915
+ const file = this.inboundDedupeFile();
916
+ fs.mkdirSync(path.dirname(file), { recursive: true });
917
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
918
+ const lines = [...this.inboundSeenMessages].map(([key, value]) => JSON.stringify({ key, seenAt: value.seenAt, seq: value.seq }));
919
+ fs.writeFileSync(tmp, lines.length > 0 ? `${lines.join('\n')}\n` : '');
920
+ fs.renameSync(tmp, file);
921
+ }
922
+ catch (error) {
923
+ logger.warn(`${this.logPrefix()} Failed to compact inbound dedup: ${error instanceof Error ? error.message : String(error)}`);
924
+ }
925
+ }
794
926
  acknowledgeImmediately(messageId, _seq) {
795
927
  // SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
796
928
  // as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
@@ -875,6 +1007,7 @@ export class AUNChannel {
875
1007
  constructor(config) {
876
1008
  this.config = config;
877
1009
  this.agentDir = agentDirPath(config.aid);
1010
+ this.loadInboundDedupe();
878
1011
  if (config.aunTrace) {
879
1012
  this.traceWriter = new LogWriter({
880
1013
  baseName: 'aun',
@@ -972,7 +1105,7 @@ export class AUNChannel {
972
1105
  // gateway metadata cache when well-known is temporarily unavailable.
973
1106
  const configuredGateway = this.config.gatewayUrl || '';
974
1107
  logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${configuredGateway || '<sdk-discovery>'}, aun_path=${aunPath}`);
975
- // 构造 AIDStore(slot=evolcore daemon,与 cli/netcheck 共享 evolcore 隔离键)
1108
+ // 构造 AIDStoredaemon 使用独立 slot,CLI/netcheck 不能触碰业务入站游标。
976
1109
  // encryptionSeed / rootCaPath 由 getAidStore 内部注入
977
1110
  const store = await getAidStore({
978
1111
  slotId: SLOT.daemon,
@@ -1025,7 +1158,7 @@ export class AUNChannel {
1025
1158
  for (const id of ids) {
1026
1159
  if (typeof id === 'string') {
1027
1160
  logger.info(`${this.logPrefix()} Message recalled: ${id}`);
1028
- this.recallHandler?.(id);
1161
+ this.recallHandler?.(id, this._aid || this.config.aid);
1029
1162
  }
1030
1163
  }
1031
1164
  }
@@ -1057,7 +1190,7 @@ export class AUNChannel {
1057
1190
  for (const id of ids) {
1058
1191
  if (typeof id === 'string') {
1059
1192
  logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
1060
- this.recallHandler?.(id);
1193
+ this.recallHandler?.(id, this._aid || this.config.aid);
1061
1194
  }
1062
1195
  }
1063
1196
  }
@@ -1095,7 +1228,7 @@ export class AUNChannel {
1095
1228
  });
1096
1229
  this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
1097
1230
  await client.connect({
1098
- // connection_kind 默认 long;slot 已由 AID 携带(evolcore daemon)
1231
+ // connection_kind 默认 long;slot 已由 AID 携带(evolcore-daemon)
1099
1232
  // extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
1100
1233
  extra_info: extraInfo,
1101
1234
  // max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
@@ -1155,7 +1288,9 @@ export class AUNChannel {
1155
1288
  }
1156
1289
  const operationId = postBootstrapWelcomeOperationId(aidName);
1157
1290
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aidName);
1158
- if (hasMessageLogOperation(chatDir, operationId) || outbox.findByDedupeKey(aidName, operationId)) {
1291
+ const prepared = outbox.findByDedupeKey(aidName, operationId);
1292
+ if (hasMessageLogOperation(chatDir, operationId)
1293
+ || (prepared?.delivery?.chatType === 'private' && prepared.channelId === owner)) {
1159
1294
  return true;
1160
1295
  }
1161
1296
  const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
@@ -1211,6 +1346,11 @@ export class AUNChannel {
1211
1346
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', owner, aid);
1212
1347
  return hasMessageLogOperation(chatDir, operationId);
1213
1348
  }
1349
+ if (entry.delivery?.chatType !== 'private') {
1350
+ logger.warn(`${this.logPrefix()} Discarding unrouted legacy post-bootstrap welcome: id=${entry.id}`);
1351
+ outbox.remove(aid, entry.id);
1352
+ return false;
1353
+ }
1214
1354
  const agentConfig = loadAgent(aid);
1215
1355
  if (!agentConfig || normalizeAgentLifecycle(agentConfig).lifecycle !== 'active') {
1216
1356
  logger.info(`${this.logPrefix()} Post-bootstrap welcome prepared; waiting for lifecycle=active`);
@@ -1234,7 +1374,7 @@ export class AUNChannel {
1234
1374
  * @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
1235
1375
  * @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
1236
1376
  */
1237
- async processAttachments(payload, baseText, channelId, preCollected) {
1377
+ async processAttachments(payload, baseText, channelId, preCollected, delivery) {
1238
1378
  const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
1239
1379
  const images = [];
1240
1380
  let finalText = baseText;
@@ -1243,7 +1383,7 @@ export class AUNChannel {
1243
1383
  }
1244
1384
  const fileParts = [];
1245
1385
  for (const att of rawAttachments) {
1246
- const filePath = await this.downloadAttachment(att, channelId);
1386
+ const filePath = await this.downloadAttachment(att, channelId, delivery);
1247
1387
  if (!filePath)
1248
1388
  continue;
1249
1389
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
@@ -1275,8 +1415,11 @@ export class AUNChannel {
1275
1415
  logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
1276
1416
  return { finalText, images };
1277
1417
  }
1278
- async downloadAttachment(att, channelId) {
1279
- const ownerAid = att.owner_aid || (!this.isGroupId(channelId) ? channelId : '') || this._aid || '';
1418
+ async downloadAttachment(att, channelId, delivery) {
1419
+ const ownerAid = att.owner_aid
1420
+ || (delivery?.chatType === 'private' ? channelId : '')
1421
+ || this._aid
1422
+ || '';
1280
1423
  const objectKey = att.object_key;
1281
1424
  if (!objectKey) {
1282
1425
  logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
@@ -1448,6 +1591,7 @@ export class AUNChannel {
1448
1591
  if (!data || typeof data !== 'object')
1449
1592
  return;
1450
1593
  const msg = data;
1594
+ const receivedAt = Date.now();
1451
1595
  // Claim before any payload extraction or command parsing. Retransmits may
1452
1596
  // carry a new seq, but the application identity remains message_id.
1453
1597
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
@@ -1467,7 +1611,7 @@ export class AUNChannel {
1467
1611
  ? payload._evolcore_causation_ref
1468
1612
  : undefined;
1469
1613
  const logDescriptor = classifyAunPayloadForLog(payload);
1470
- const text = this.extractTextPayload(payload, fromAid, fromAid);
1614
+ const text = this.extractTextPayload(payload, fromAid, fromAid, { chatType: 'private' });
1471
1615
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1472
1616
  const refMessageId = typeof payload === 'object' && payload !== null && typeof payload.ref_message_id === 'string'
1473
1617
  ? payload.ref_message_id
@@ -1513,7 +1657,7 @@ export class AUNChannel {
1513
1657
  mentions.push({ aid: this._aid });
1514
1658
  }
1515
1659
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
1516
- const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
1660
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid, undefined, { chatType: 'private' });
1517
1661
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
1518
1662
  // device_id 仅 SDK 内部多实例去重用,evolcore session 层面跨端共享会话
1519
1663
  const chatId = fromAid;
@@ -1529,7 +1673,7 @@ export class AUNChannel {
1529
1673
  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))}`);
1530
1674
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1531
1675
  if (p2pPayloadType === 'action_card_reply') {
1532
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1676
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1533
1677
  return;
1534
1678
  }
1535
1679
  // menu.* 与联系人本人状态查询:需要原始 payload JSON 传递给 bridge。
@@ -1537,18 +1681,18 @@ export class AUNChannel {
1537
1681
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_QUERY_TYPE
1538
1682
  || p2pPayloadType === AUNChannel.CONTACT_REQUEST_SUBMIT_TYPE) {
1539
1683
  this.acknowledgeImmediately(messageId, seq);
1540
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1684
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1541
1685
  this.dispatchMessage({
1542
1686
  channelId: chatId, userId: fromAid,
1543
1687
  text: JSON.stringify(payload),
1544
- chatType: 'private', messageId, seq,
1688
+ chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1545
1689
  peerName: displayName || undefined,
1546
1690
  peerType: peerIdentity.type,
1547
1691
  encrypted: msgEncrypted,
1548
1692
  protectedHeaders,
1549
1693
  mentions,
1550
1694
  mentionAids: mentionEntryAids(mentions),
1551
- replyContext: this.buildGroupReplyContext(threadId, fromAid, msgEncrypted, messageId),
1695
+ replyContext: this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId),
1552
1696
  msgType: logDescriptor.msgType,
1553
1697
  payloadType: logDescriptor.payloadType,
1554
1698
  payloadSummary: logDescriptor.payloadSummary,
@@ -1568,11 +1712,11 @@ export class AUNChannel {
1568
1712
  // 透传 encrypted:bind.response 须与请求的加密/明文对称(见 onMessage 处理)。
1569
1713
  if (p2pPayloadType === AUNChannel.BIND_REQUEST_TYPE) {
1570
1714
  this.acknowledgeImmediately(messageId, seq);
1571
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1715
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1572
1716
  this.dispatchMessage({
1573
1717
  channelId: chatId, userId: fromAid,
1574
1718
  text: JSON.stringify(payload),
1575
- chatType: 'private', messageId, seq,
1719
+ chatType: 'private', messageId, seq, receivedAt, gatewaySeq: seq,
1576
1720
  peerName: displayName || undefined,
1577
1721
  peerType: peerIdentity.type,
1578
1722
  encrypted: msgEncrypted,
@@ -1588,7 +1732,7 @@ export class AUNChannel {
1588
1732
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
1589
1733
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
1590
1734
  this.acknowledgeImmediately(messageId, seq);
1591
- this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId);
1735
+ this.appendInboundPayloadJsonl(chatId, fromAid, messageId, msgEncrypted, false, logDescriptor, threadId, receivedAt, seq);
1592
1736
  logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
1593
1737
  return;
1594
1738
  }
@@ -1597,24 +1741,15 @@ export class AUNChannel {
1597
1741
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
1598
1742
  const isSystemP2P = p2pPayloadType === 'event';
1599
1743
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
1600
- const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode, refMessageId } };
1601
- if (threadId)
1602
- replyContext.threadId = threadId;
1603
- replyContext.peerId = fromAid;
1604
- if (messageId)
1605
- replyContext.replyToMessageId = messageId;
1606
- // 协议 v2.4 §6.4/§12.1: 话题会话创建时传入 topicName
1607
- if (topicName && threadId) {
1608
- if (!replyContext.metadata)
1609
- replyContext.metadata = {};
1610
- replyContext.metadata.topicName = topicName;
1611
- }
1744
+ const replyContext = this.buildPrivateReplyContext(threadId, fromAid, msgEncrypted, messageId, msgChatmode, topicName, refMessageId);
1612
1745
  this.dispatchMessage({
1613
1746
  channelId: chatId,
1614
1747
  userId: fromAid,
1615
1748
  text: finalText,
1616
1749
  chatType: 'private',
1617
1750
  messageId,
1751
+ receivedAt,
1752
+ gatewaySeq: seq,
1618
1753
  causation,
1619
1754
  seq,
1620
1755
  threadId,
@@ -1637,6 +1772,7 @@ export class AUNChannel {
1637
1772
  if (!data || typeof data !== 'object')
1638
1773
  return;
1639
1774
  const msg = data;
1775
+ const receivedAt = Date.now();
1640
1776
  // Deduplicate at the adapter boundary, before mention/slash routing.
1641
1777
  const messageId = typeof msg.message_id === 'string' ? msg.message_id : '';
1642
1778
  const seq = typeof msg.seq === 'number' ? msg.seq : undefined;
@@ -1654,7 +1790,7 @@ export class AUNChannel {
1654
1790
  return;
1655
1791
  const payloadMentionAids = mentionEntryAids(payloadMentionEntries);
1656
1792
  const logDescriptor = classifyAunPayloadForLog(payload);
1657
- const text = this.extractTextPayload(payload, groupId, senderAid);
1793
+ const text = this.extractTextPayload(payload, groupId, senderAid, { chatType: 'group', groupId });
1658
1794
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1659
1795
  const topicName = (typeof payload === 'object' && payload !== null)
1660
1796
  ? this.sanitizeTopicName(payload.topicName)
@@ -1710,7 +1846,7 @@ export class AUNChannel {
1710
1846
  peerName: displayName,
1711
1847
  peerType: peerInfo?.type || 'unknown',
1712
1848
  seq,
1713
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName),
1849
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncryptedFast, messageId, msgChatmodeFast, topicName, groupId),
1714
1850
  createdAt,
1715
1851
  });
1716
1852
  return;
@@ -1720,7 +1856,7 @@ export class AUNChannel {
1720
1856
  {
1721
1857
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1722
1858
  if (payloadType === 'action_card_reply') {
1723
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1859
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
1724
1860
  return;
1725
1861
  }
1726
1862
  }
@@ -1733,18 +1869,18 @@ export class AUNChannel {
1733
1869
  if (payloadType.startsWith('menu.')) {
1734
1870
  const menuEncrypted = !!env.encrypted;
1735
1871
  this.acknowledgeImmediately(messageId, seq);
1736
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId);
1872
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, menuEncrypted, true, logDescriptor, threadId, receivedAt, seq);
1737
1873
  this.dispatchMessage({
1738
1874
  channelId: groupId, userId: senderAid,
1739
1875
  text: JSON.stringify(payload),
1740
- chatType: 'group', messageId, seq, groupId,
1876
+ chatType: 'group', messageId, seq, receivedAt, gatewaySeq: seq, groupId,
1741
1877
  encrypted: menuEncrypted,
1742
1878
  protectedHeaders,
1743
1879
  mentions: payloadMentionEntries,
1744
1880
  mentionAids: payloadMentionAids,
1745
1881
  isMentioned: hasMentionAll(payloadMentionEntries)
1746
1882
  || (!!this._aid && mentionEntryTargets(payloadMentionEntries).includes(this._aid)),
1747
- replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName),
1883
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, menuEncrypted, messageId, payloadObj?.chatmode, topicName, groupId),
1748
1884
  msgType: logDescriptor.msgType,
1749
1885
  payloadType: logDescriptor.payloadType,
1750
1886
  payloadSummary: logDescriptor.payloadSummary,
@@ -1754,7 +1890,7 @@ export class AUNChannel {
1754
1890
  }
1755
1891
  if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
1756
1892
  this.acknowledgeImmediately(messageId, seq);
1757
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId);
1893
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, !!env.encrypted, true, logDescriptor, threadId, receivedAt, seq);
1758
1894
  logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
1759
1895
  return;
1760
1896
  }
@@ -1795,7 +1931,7 @@ export class AUNChannel {
1795
1931
  this.pendingEchoMessages.set(messageId, {
1796
1932
  text: echoText,
1797
1933
  channelId: groupId,
1798
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined),
1934
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho, undefined, groupId),
1799
1935
  receiveTs: Date.now(),
1800
1936
  });
1801
1937
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -1817,7 +1953,7 @@ export class AUNChannel {
1817
1953
  // TODO: Phase 6 实施 selective-response 模式后,移除此过滤,只保留 isMentioned 标记
1818
1954
  if (enforceMention && !isMentioned && !isOwnHandoff) {
1819
1955
  this.acknowledgeImmediately(messageId, seq);
1820
- this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId);
1956
+ this.appendInboundPayloadJsonl(groupId, senderAid, messageId, msgEncrypted, true, logDescriptor, threadId, receivedAt, seq);
1821
1957
  logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mentionMode=${mentionMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
1822
1958
  return;
1823
1959
  }
@@ -1838,7 +1974,7 @@ export class AUNChannel {
1838
1974
  }
1839
1975
  const mentions = payloadMentionEntries;
1840
1976
  // Process attachments
1841
- const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
1977
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments, { chatType: 'group', groupId });
1842
1978
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1843
1979
  const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
1844
1980
  const shortAid = this.getShortAid(senderAid);
@@ -1882,6 +2018,8 @@ export class AUNChannel {
1882
2018
  text: finalText,
1883
2019
  chatType: 'group',
1884
2020
  messageId,
2021
+ receivedAt,
2022
+ gatewaySeq: seq,
1885
2023
  seq,
1886
2024
  threadId,
1887
2025
  mentions,
@@ -1889,7 +2027,7 @@ export class AUNChannel {
1889
2027
  isMentioned: isOwnHandoff || mentionedSelf || mentionedAll,
1890
2028
  source: isOwnHandoff ? 'handoff' : undefined,
1891
2029
  encrypted: msgEncrypted,
1892
- replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName),
2030
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode, topicName, groupId),
1893
2031
  mentionMode: serverMentionMode,
1894
2032
  images: inboundImages.length > 0 ? inboundImages : undefined,
1895
2033
  msgType: logDescriptor.msgType,
@@ -1899,6 +2037,19 @@ export class AUNChannel {
1899
2037
  });
1900
2038
  }
1901
2039
  dispatchMessage(event) {
2040
+ const authenticatedDelivery = event.chatType === 'group'
2041
+ ? { chatType: 'group', groupId: event.groupId ?? event.channelId }
2042
+ : { chatType: 'private' };
2043
+ const suppliedDelivery = event.replyContext?.delivery;
2044
+ const suppliedMatches = suppliedDelivery?.chatType === 'private'
2045
+ ? authenticatedDelivery.chatType === 'private'
2046
+ : suppliedDelivery?.chatType === 'group'
2047
+ && authenticatedDelivery.chatType === 'group'
2048
+ && suppliedDelivery.groupId === authenticatedDelivery.groupId;
2049
+ const delivery = suppliedDelivery && suppliedMatches ? suppliedDelivery : authenticatedDelivery;
2050
+ if (suppliedDelivery && delivery !== suppliedDelivery) {
2051
+ logger.warn(`${this.logPrefix()} Ignoring mismatched inbound reply route: channel=${event.channelId} chatType=${event.chatType}`);
2052
+ }
1902
2053
  // Inbound events are claimed in handleIncomingPrivateMessage / GroupMessage
1903
2054
  // before parsing. Keeping dedupe there prevents duplicate slash commands
1904
2055
  // from reaching routing and mention policy code.
@@ -1926,19 +2077,24 @@ export class AUNChannel {
1926
2077
  this.pendingEchoMessages.set(event.messageId, {
1927
2078
  text: echoText,
1928
2079
  channelId: event.channelId,
1929
- context: event.replyContext ? { metadata: event.replyContext.metadata } : undefined,
2080
+ context: {
2081
+ ...(event.replyContext?.metadata ? { metadata: event.replyContext.metadata } : {}),
2082
+ delivery,
2083
+ },
1930
2084
  receiveTs: Date.now(),
1931
2085
  });
1932
2086
  logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
1933
2087
  }
1934
2088
  if (!this.messageHandler)
1935
2089
  return;
1936
- // Use caller-supplied replyContext (group path builds mentionUserIds);
1937
- // fall back to simple threadId-only context for private messages
1938
- let replyContext = event.replyContext;
1939
- if (!replyContext && event.threadId) {
1940
- replyContext = { threadId: event.threadId };
1941
- }
2090
+ // Preserve the caller-supplied context (group paths add mention fields),
2091
+ // but always attach the route derived from this authenticated event when
2092
+ // a synthetic/test event omitted it. Never infer the route from the AID.
2093
+ const replyContext = {
2094
+ ...(event.replyContext ?? {}),
2095
+ delivery,
2096
+ ...(event.threadId && !event.replyContext?.threadId ? { threadId: event.threadId } : {}),
2097
+ };
1942
2098
  this.messageHandler({
1943
2099
  channelId: event.channelId || '',
1944
2100
  channelType: 'aun',
@@ -1955,6 +2111,8 @@ export class AUNChannel {
1955
2111
  encrypted: event.encrypted,
1956
2112
  protectedHeaders: event.protectedHeaders,
1957
2113
  messageId: event.messageId,
2114
+ receivedAt: event.receivedAt,
2115
+ gatewaySeq: event.gatewaySeq ?? event.seq,
1958
2116
  causation: event.causation,
1959
2117
  threadId: event.threadId,
1960
2118
  mentions: event.mentions,
@@ -1972,7 +2130,7 @@ export class AUNChannel {
1972
2130
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1973
2131
  });
1974
2132
  }
1975
- appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId) {
2133
+ appendInboundPayloadJsonl(channelId, fromAid, msgId, encrypt, isGroup, descriptor, threadId, receivedAt, gatewaySeq) {
1976
2134
  try {
1977
2135
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
1978
2136
  appendMessageLog(chatDir, buildInboundEntry({
@@ -1987,6 +2145,9 @@ export class AUNChannel {
1987
2145
  msgType: descriptor.msgType,
1988
2146
  payloadType: descriptor.payloadType,
1989
2147
  payloadSummary: descriptor.payloadSummary,
2148
+ timestamp: receivedAt,
2149
+ receivedAt,
2150
+ gatewaySeq,
1990
2151
  }));
1991
2152
  }
1992
2153
  catch (e) {
@@ -2190,7 +2351,14 @@ export class AUNChannel {
2190
2351
  }
2191
2352
  const replyTarget = event.channelId;
2192
2353
  // 不传 peerId,避免 sendMessage 在头部追加 @peer
2193
- const context = { threadId: event.threadId, metadata: event.replyContext?.metadata };
2354
+ const authenticatedDelivery = event.chatType === 'group'
2355
+ ? { chatType: 'group', groupId: event.channelId }
2356
+ : { chatType: 'private' };
2357
+ const context = {
2358
+ threadId: event.threadId,
2359
+ metadata: event.replyContext?.metadata,
2360
+ delivery: authenticatedDelivery,
2361
+ };
2194
2362
  const sendStart = Date.now();
2195
2363
  this.isEchoSending = true;
2196
2364
  this.sendMessage(replyTarget, echoText, context).then(() => {
@@ -2611,14 +2779,18 @@ export class AUNChannel {
2611
2779
  const timer = setTimeout(() => this.invalidatedInteractions.delete(interactionId), 24 * 60 * 60 * 1000);
2612
2780
  timer.unref?.();
2613
2781
  }
2614
- async recallInteractionCard(messageId, channelId) {
2782
+ async recallInteractionCard(messageId, channelId, delivery) {
2615
2783
  if (!this.client || !this.connected)
2616
2784
  return false;
2617
2785
  try {
2618
- const isGroup = channelId ? this.isGroupId(channelId) : false;
2786
+ if (!channelId || !delivery) {
2787
+ logger.warn(`${this.logPrefix()} Cannot recall interaction card without trusted delivery route: mid=${messageId}`);
2788
+ return false;
2789
+ }
2790
+ const isGroup = delivery.chatType === 'group';
2619
2791
  const method = isGroup ? 'group.recall' : 'message.recall';
2620
2792
  const params = isGroup
2621
- ? { group_id: channelId, message_ids: [messageId] }
2793
+ ? { group_id: delivery.groupId, message_ids: [messageId] }
2622
2794
  : { message_ids: [messageId] };
2623
2795
  await this.callAndTrace(method, params, { silentError: true });
2624
2796
  return true;
@@ -2641,9 +2813,10 @@ export class AUNChannel {
2641
2813
  }
2642
2814
  const messageIds = [...(this.interactionCardMessageIds.get(interactionId) ?? [])];
2643
2815
  for (const messageId of messageIds) {
2644
- const channelId = this.cardMessageIdMap.get(messageId)?.channelId;
2816
+ const card = this.cardMessageIdMap.get(messageId);
2817
+ const channelId = card?.channelId;
2645
2818
  this.forgetInteractionCard(messageId, interactionId);
2646
- await this.recallInteractionCard(messageId, channelId);
2819
+ await this.recallInteractionCard(messageId, channelId, card?.delivery);
2647
2820
  }
2648
2821
  }
2649
2822
  forgetInteractionCard(messageId, interactionId) {
@@ -2653,7 +2826,7 @@ export class AUNChannel {
2653
2826
  if (ids && ids.size === 0)
2654
2827
  this.interactionCardMessageIds.delete(interactionId);
2655
2828
  }
2656
- registerCardPostSend(messageId, action, channelId) {
2829
+ registerCardPostSend(messageId, action, channelId, delivery) {
2657
2830
  if (action.type !== 'register_interaction_card')
2658
2831
  return;
2659
2832
  this.cardMessageIdMap.set(messageId, {
@@ -2661,6 +2834,7 @@ export class AUNChannel {
2661
2834
  isCommandCard: action.isCommandCard,
2662
2835
  initiatorAid: action.initiatorAid,
2663
2836
  channelId,
2837
+ delivery: delivery ?? action.delivery,
2664
2838
  });
2665
2839
  this.ownedCardMsgIds.add(messageId);
2666
2840
  let messageIds = this.interactionCardMessageIds.get(action.requestId);
@@ -2686,17 +2860,18 @@ export class AUNChannel {
2686
2860
  const invalidationReason = this.invalidatedInteractions.get(action.requestId);
2687
2861
  if (invalidationReason) {
2688
2862
  this.cardMessageIdMap.delete(messageId);
2689
- void this.recallInteractionCard(messageId, channelId);
2863
+ void this.recallInteractionCard(messageId, channelId, delivery ?? action.delivery);
2690
2864
  }
2691
2865
  }
2692
2866
  runPostSend(entry, messageId) {
2693
2867
  if (!entry.postSend)
2694
2868
  return;
2695
2869
  if (entry.postSend.type === 'register_interaction_card') {
2696
- this.registerCardPostSend(messageId, entry.postSend, entry.channelId);
2870
+ this.registerCardPostSend(messageId, entry.postSend, entry.channelId, entry.delivery);
2697
2871
  }
2698
2872
  }
2699
2873
  async sendAunPayload(channelId, payload, context, label) {
2874
+ const delivery = this.requireDelivery(channelId, context);
2700
2875
  let normalizedPayload;
2701
2876
  try {
2702
2877
  normalizedPayload = this.normalizeAunPayloadMentions(payload);
@@ -2709,7 +2884,7 @@ export class AUNChannel {
2709
2884
  }
2710
2885
  if (!this.client || !this.connected)
2711
2886
  return { ok: false };
2712
- const isGroup = this.isGroupId(channelId);
2887
+ const isGroup = delivery.chatType === 'group';
2713
2888
  const targetAid = channelId;
2714
2889
  const encryptTarget = isGroup ? channelId : targetAid;
2715
2890
  const encrypt = context?.metadata?.encrypted != null
@@ -2727,7 +2902,7 @@ export class AUNChannel {
2727
2902
  const truncatedPayload = this.truncatePayloadIfNeeded(payloadWithCausationRef, label);
2728
2903
  const params = { payload: truncatedPayload, encrypt };
2729
2904
  if (isGroup)
2730
- params.group_id = channelId;
2905
+ params.group_id = delivery.groupId;
2731
2906
  else
2732
2907
  params.to = targetAid;
2733
2908
  const callOnce = async (sendParams, fallback) => {
@@ -2797,6 +2972,8 @@ export class AUNChannel {
2797
2972
  this.forwardOutbound(result);
2798
2973
  }
2799
2974
  async sendContentPayload(channelId, payload, opts) {
2975
+ const delivery = this.requireDelivery(channelId, opts.context);
2976
+ const context = this.withDelivery(opts.context, delivery);
2800
2977
  // Validate the caller's raw mentions before any early-return policy (such
2801
2978
  // as a cancelled interaction) can hide a malformed wire payload.
2802
2979
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
@@ -2812,14 +2989,15 @@ export class AUNChannel {
2812
2989
  // Validate the caller's raw mentions before deep-cleaning: that helper
2813
2990
  // intentionally removes undefined array elements, which would otherwise
2814
2991
  // turn malformed `mentions: [undefined]` into an apparently valid `[]`.
2815
- const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, opts.context));
2992
+ const finalPayload = this.normalizeAunPayloadMentions(this.applyReplyContextToPayload(validatedPayload, context));
2816
2993
  const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
2817
2994
  const entry = outbox.enqueue(this.config.aid, {
2818
2995
  channelId,
2996
+ delivery,
2819
2997
  type: 'payload',
2820
2998
  contentKind: opts.contentKind,
2821
2999
  payload: finalPayload,
2822
- context: opts.context,
3000
+ context,
2823
3001
  logText,
2824
3002
  ttl: opts.ttl,
2825
3003
  postSend: opts.postSend,
@@ -2966,13 +3144,15 @@ export class AUNChannel {
2966
3144
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
2967
3145
  return;
2968
3146
  }
3147
+ const delivery = this.requireDelivery(channelId, context);
3148
+ const routedContext = this.withDelivery(context, delivery);
2969
3149
  // 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
2970
3150
  if (!this.isEchoSending) {
2971
- await this.flushPendingEcho(channelId);
3151
+ await this.flushPendingEcho(channelId, delivery);
2972
3152
  }
2973
3153
  let finalText = text;
2974
3154
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
2975
- if (this.isGroupId(channelId)) {
3155
+ if (delivery.chatType === 'group') {
2976
3156
  const requestedMentions = Array.from(new Set((context?.mentionUserIds ?? [])
2977
3157
  .filter((id) => typeof id === 'string' && id.trim().length > 0)
2978
3158
  .map(id => id.trim())));
@@ -2995,8 +3175,8 @@ export class AUNChannel {
2995
3175
  }
2996
3176
  }
2997
3177
  }
2998
- const operationId = typeof context?.metadata?.operationId === 'string'
2999
- ? context.metadata.operationId
3178
+ const operationId = typeof routedContext.metadata?.operationId === 'string'
3179
+ ? routedContext.metadata.operationId
3000
3180
  : undefined;
3001
3181
  if (operationId) {
3002
3182
  const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', channelId, this.config.aid);
@@ -3006,13 +3186,14 @@ export class AUNChannel {
3006
3186
  // Write-ahead: persist to outbox before attempting send
3007
3187
  const entry = outbox.enqueue(this.config.aid, {
3008
3188
  channelId,
3189
+ delivery,
3009
3190
  dedupeKey: operationId,
3010
- critical: context?.metadata?.criticalDelivery === true,
3191
+ critical: routedContext.metadata?.criticalDelivery === true,
3011
3192
  type: 'text',
3012
3193
  text: finalText,
3013
- context,
3014
- ttl: typeof context?.metadata?.outboxTtl === 'number'
3015
- ? context.metadata.outboxTtl
3194
+ context: routedContext,
3195
+ ttl: typeof routedContext.metadata?.outboxTtl === 'number'
3196
+ ? routedContext.metadata.outboxTtl
3016
3197
  : undefined,
3017
3198
  });
3018
3199
  logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
@@ -3036,8 +3217,8 @@ export class AUNChannel {
3036
3217
  }
3037
3218
  /** Daemon-side transport for `ec msg send` running inside an agent task. */
3038
3219
  async sendDaemonMsg(args) {
3039
- if (!args.to || this.isGroupId(args.to)) {
3040
- return { ok: false, error: 'aun-msg-send only supports private AID targets', code: 'UNSUPPORTED_TARGET' };
3220
+ if (!args.to) {
3221
+ return { ok: false, error: 'aun-msg-send requires a private target', code: 'UNSUPPORTED_TARGET' };
3041
3222
  }
3042
3223
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3043
3224
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
@@ -3161,8 +3342,8 @@ export class AUNChannel {
3161
3342
  }
3162
3343
  /** Daemon-side transport for `ec group send` running inside an agent task. */
3163
3344
  async sendDaemonGroupMsg(args) {
3164
- if (!args.groupId || !this.isGroupId(args.groupId)) {
3165
- return { ok: false, error: 'aun-msg-send requires a valid group target', code: 'UNSUPPORTED_TARGET' };
3345
+ if (!args.groupId) {
3346
+ return { ok: false, error: 'aun group send requires a group target', code: 'UNSUPPORTED_TARGET' };
3166
3347
  }
3167
3348
  if (!args.payload || typeof args.payload !== 'object' || Array.isArray(args.payload)) {
3168
3349
  return { ok: false, error: 'payload must be an object', code: 'INVALID_PAYLOAD' };
@@ -3246,7 +3427,7 @@ export class AUNChannel {
3246
3427
  return { ok: false, error: e?.message || String(e), code: e?.code };
3247
3428
  }
3248
3429
  }
3249
- async flushPendingEcho(channelId) {
3430
+ async flushPendingEcho(channelId, delivery) {
3250
3431
  // 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
3251
3432
  for (const [key, echo] of this.pendingEchoMessages) {
3252
3433
  if (echo.channelId === channelId) {
@@ -3270,18 +3451,32 @@ export class AUNChannel {
3270
3451
  ts: Date.now(),
3271
3452
  aid: this.config.aid,
3272
3453
  channelId,
3454
+ delivery,
3273
3455
  type: 'text',
3274
3456
  text: echoText,
3457
+ context: this.withDelivery(echo.context, delivery),
3275
3458
  ttl: 300_000,
3276
3459
  };
3277
3460
  const ok = await this.deliverTextEntry(echoEntry);
3278
3461
  if (!ok) {
3279
- outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
3462
+ outbox.enqueue(this.config.aid, {
3463
+ channelId,
3464
+ delivery,
3465
+ type: 'text',
3466
+ text: echoText,
3467
+ context: this.withDelivery(echo.context, delivery),
3468
+ });
3280
3469
  }
3281
3470
  logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
3282
3471
  }
3283
3472
  else {
3284
- outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
3473
+ outbox.enqueue(this.config.aid, {
3474
+ channelId,
3475
+ delivery,
3476
+ type: 'text',
3477
+ text: echoText,
3478
+ context: this.withDelivery(echo.context, delivery),
3479
+ });
3285
3480
  logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
3286
3481
  }
3287
3482
  break;
@@ -3291,7 +3486,8 @@ export class AUNChannel {
3291
3486
  async deliverTextEntry(entry) {
3292
3487
  const channelId = entry.channelId;
3293
3488
  const finalText = entry.text;
3294
- const context = entry.context;
3489
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3490
+ const context = this.withDelivery(entry.context, delivery);
3295
3491
  const operationId = entry.dedupeKey
3296
3492
  || (typeof context?.metadata?.operationId === 'string' ? context.metadata.operationId : undefined);
3297
3493
  if (operationId) {
@@ -3309,7 +3505,7 @@ export class AUNChannel {
3309
3505
  // 从 context.metadata.source 读取 source,默认为 'daemon'
3310
3506
  const source = context?.metadata?.source ?? 'daemon';
3311
3507
  const payload = { type: 'text', text: finalText };
3312
- if (this.isGroupId(channelId)) {
3508
+ if (delivery.chatType === 'group') {
3313
3509
  const extracted = this.extractMentionEntriesFromText(finalText);
3314
3510
  if (extracted.length > 0)
3315
3511
  payload.mentions = extracted;
@@ -3323,7 +3519,7 @@ export class AUNChannel {
3323
3519
  if (context?.metadata?.chatmode)
3324
3520
  payload.chatmode = context.metadata.chatmode;
3325
3521
  const causation = normalizeCausation(context?.metadata?.causation);
3326
- const causationRef = !this.isGroupId(channelId) && causation
3522
+ const causationRef = delivery.chatType !== 'group' && causation
3327
3523
  ? `caus_${crypto.randomUUID().replace(/-/g, '')}`
3328
3524
  : undefined;
3329
3525
  if (causationRef && causation) {
@@ -3332,7 +3528,7 @@ export class AUNChannel {
3332
3528
  }
3333
3529
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
3334
3530
  logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
3335
- const isGroup = this.isGroupId(channelId);
3531
+ const isGroup = delivery.chatType === 'group';
3336
3532
  const targetAid = channelId;
3337
3533
  if (operationId && entry.deliveryReceipt) {
3338
3534
  this.appendOutboundJsonl(channelId, {
@@ -3506,7 +3702,8 @@ export class AUNChannel {
3506
3702
  }
3507
3703
  const contentKind = entry.contentKind;
3508
3704
  const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
3509
- const context = entry.context;
3705
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
3706
+ const context = this.withDelivery(entry.context, delivery);
3510
3707
  logger.info(`${this.logPrefix()} deliverPayloadEntry: id=${entry.id} kind=${contentKind ?? payload.type ?? 'payload'} channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} textLen=${logText.length}`);
3511
3708
  const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
3512
3709
  if (sent.discarded) {
@@ -3515,7 +3712,7 @@ export class AUNChannel {
3515
3712
  }
3516
3713
  if (!sent.ok || !sent.messageId)
3517
3714
  return sent;
3518
- const isGroup = this.isGroupId(channelId);
3715
+ const isGroup = delivery.chatType === 'group';
3519
3716
  const statsContext = isGroup ? await this.groupStatsContext(channelId) : undefined;
3520
3717
  this.logAunSendAccepted(isGroup ? 'group.send' : 'message.send', isGroup ? channelId : this.peerLabel(channelId), sent.messageId, !!sent.encrypt, sent.result, logText);
3521
3718
  this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result, statsContext);
@@ -3577,6 +3774,8 @@ export class AUNChannel {
3577
3774
  async sendThought(channelId, taskId, payload, context) {
3578
3775
  if (!taskId)
3579
3776
  return;
3777
+ const delivery = this.requireDelivery(channelId, context);
3778
+ context = this.withDelivery(context, delivery);
3580
3779
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
3581
3780
  const finalPayload = this.normalizeAunPayloadMentions(this.stripUndefinedDeep(validatedPayload));
3582
3781
  if (!this.connected || !this.client)
@@ -3621,8 +3820,8 @@ export class AUNChannel {
3621
3820
  else {
3622
3821
  thoughtText = this.activityLogText(finalPayload);
3623
3822
  }
3624
- if (this.isGroupId(channelId)) {
3625
- params.group_id = targetId;
3823
+ if (delivery.chatType === 'group') {
3824
+ params.group_id = delivery.groupId;
3626
3825
  const putRes = await this.callAndTrace('group.thought.put', params);
3627
3826
  const tid = putRes?.thought_id;
3628
3827
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
@@ -3652,13 +3851,15 @@ export class AUNChannel {
3652
3851
  }
3653
3852
  /** 发送结构化 AUN payload;transient 协议载荷不写本地 messages.jsonl。 */
3654
3853
  async sendStructured(channelId, payload, context) {
3854
+ const delivery = this.requireDelivery(channelId, context);
3855
+ context = this.withDelivery(context, delivery);
3655
3856
  // Validate before checking connection state so malformed producer payloads
3656
3857
  // are reported deterministically instead of being hidden by a disconnect.
3657
3858
  const validatedPayload = this.normalizeAunPayloadMentions(payload);
3658
3859
  const finalPayload = this.stripUndefinedDeep(validatedPayload);
3659
3860
  if (!this.connected || !this.client)
3660
3861
  return null;
3661
- const isGroup = this.isGroupId(channelId);
3862
+ const isGroup = delivery.chatType === 'group';
3662
3863
  const targetAid = channelId;
3663
3864
  const encryptTarget = isGroup ? channelId : targetAid;
3664
3865
  const encrypt = context?.metadata?.encrypted != null
@@ -3684,7 +3885,7 @@ export class AUNChannel {
3684
3885
  }
3685
3886
  try {
3686
3887
  if (isGroup) {
3687
- params.group_id = channelId;
3888
+ params.group_id = delivery.groupId;
3688
3889
  const result = await this.callAndTrace('group.send', params);
3689
3890
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
3690
3891
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
@@ -3708,6 +3909,8 @@ export class AUNChannel {
3708
3909
  }
3709
3910
  }
3710
3911
  async sendFile(channelId, filePath, context) {
3912
+ const delivery = this.requireDelivery(channelId, context);
3913
+ context = this.withDelivery(context, delivery);
3711
3914
  const absPath = path.resolve(filePath);
3712
3915
  if (!fs.existsSync(absPath)) {
3713
3916
  logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
@@ -3725,6 +3928,7 @@ export class AUNChannel {
3725
3928
  // Write-ahead: persist to outbox
3726
3929
  const entry = outbox.enqueue(this.config.aid, {
3727
3930
  channelId,
3931
+ delivery,
3728
3932
  type: 'file',
3729
3933
  filePath: absPath,
3730
3934
  context,
@@ -3743,6 +3947,8 @@ export class AUNChannel {
3743
3947
  }
3744
3948
  }
3745
3949
  async sendImage(channelId, data, mimeType, alt, context) {
3950
+ const delivery = this.requireDelivery(channelId, context);
3951
+ context = this.withDelivery(context, delivery);
3746
3952
  if (!Buffer.isBuffer(data)) {
3747
3953
  throw new Error('result.image data must be a Buffer');
3748
3954
  }
@@ -3764,6 +3970,7 @@ export class AUNChannel {
3764
3970
  const logText = alt ? `[image] ${alt}` : '[image]';
3765
3971
  const entry = outbox.enqueue(this.config.aid, {
3766
3972
  channelId,
3973
+ delivery,
3767
3974
  type: 'image',
3768
3975
  contentKind: 'image',
3769
3976
  image: { dataBase64: data.toString('base64'), mimeType: normalizedMimeType, alt },
@@ -3822,6 +4029,9 @@ export class AUNChannel {
3822
4029
  logger.warn(`${this.logPrefix()} deliverImageEntry: missing image data (outbox id=${entry.id})`);
3823
4030
  return true;
3824
4031
  }
4032
+ const delivery = this.requirePersistedDelivery(entry.channelId, entry.delivery);
4033
+ entry.delivery = delivery;
4034
+ entry.context = this.withDelivery(entry.context, delivery);
3825
4035
  const data = Buffer.from(image.dataBase64, 'base64');
3826
4036
  if (data.length === 0) {
3827
4037
  logger.warn(`${this.logPrefix()} deliverImageEntry: invalid image data (outbox id=${entry.id})`);
@@ -3839,6 +4049,7 @@ export class AUNChannel {
3839
4049
  if (!outbox.replace(this.config.aid, entry)) {
3840
4050
  payloadEntry = outbox.enqueue(this.config.aid, {
3841
4051
  channelId: entry.channelId,
4052
+ delivery,
3842
4053
  type: 'payload',
3843
4054
  contentKind: 'image',
3844
4055
  payload: entry.payload,
@@ -3856,7 +4067,8 @@ export class AUNChannel {
3856
4067
  async deliverFileEntry(entry) {
3857
4068
  const channelId = entry.channelId;
3858
4069
  const absPath = entry.filePath;
3859
- const context = entry.context;
4070
+ const delivery = this.requirePersistedDelivery(channelId, entry.delivery);
4071
+ const context = this.withDelivery(entry.context, delivery);
3860
4072
  if (!fs.existsSync(absPath)) {
3861
4073
  logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
3862
4074
  return true; // remove from outbox, file no longer exists
@@ -3914,7 +4126,7 @@ export class AUNChannel {
3914
4126
  attachment,
3915
4127
  context,
3916
4128
  });
3917
- const isGroup = this.isGroupId(channelId);
4129
+ const isGroup = delivery.chatType === 'group';
3918
4130
  const fileTargetAid = channelId;
3919
4131
  const encryptTarget = isGroup ? channelId : fileTargetAid;
3920
4132
  const encrypt = context?.metadata?.encrypted != null
@@ -3925,7 +4137,7 @@ export class AUNChannel {
3925
4137
  let sentMid = null;
3926
4138
  try {
3927
4139
  if (isGroup) {
3928
- params.group_id = channelId;
4140
+ params.group_id = delivery.groupId;
3929
4141
  this.trace('OUT', 'group.send.file', params);
3930
4142
  const result = await this.client.call('group.send', params);
3931
4143
  sendResult = result;
@@ -4033,6 +4245,10 @@ export class AUNChannel {
4033
4245
  return;
4034
4246
  logger.info(`${this.logPrefix()} Draining outbox...`);
4035
4247
  const result = await outbox.drain(this.config.aid, async (entry) => {
4248
+ if (!isDeliveryTarget(entry.delivery)) {
4249
+ logger.warn(`${this.logPrefix()} Discarding legacy/malformed outbox entry without a trusted delivery route: id=${entry.id} channel=${entry.channelId}`);
4250
+ return true;
4251
+ }
4036
4252
  if (entry.type === 'text') {
4037
4253
  return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
4038
4254
  }
@@ -4058,11 +4274,23 @@ export class AUNChannel {
4058
4274
  this.messageSeqMap.delete(messageId);
4059
4275
  }
4060
4276
  sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
4277
+ let delivery;
4278
+ try {
4279
+ delivery = this.requireDelivery(channelId, context);
4280
+ }
4281
+ catch (error) {
4282
+ logger.warn(`${this.logPrefix()} task.${status} notify skipped without trusted delivery route: channel=${channelId} task=${taskId} error=${error instanceof Error ? error.message : String(error)}`);
4283
+ return;
4284
+ }
4285
+ context = this.withDelivery(context, delivery);
4061
4286
  if (status === 'start')
4062
4287
  this.sentCount.delete(channelId); // 新任务开始,重置计数
4063
4288
  if (!this.client || !this.connected)
4064
4289
  return;
4065
- const severity = status === 'error' || status === 'timeout' ? 'error' : 'info';
4290
+ const requestedSeverity = extraMeta?.severity;
4291
+ const severity = requestedSeverity === 'warn'
4292
+ ? 'warn'
4293
+ : status === 'error' || status === 'timeout' ? 'error' : 'info';
4066
4294
  const stateMap = {
4067
4295
  start: 'started',
4068
4296
  done: 'completed',
@@ -4081,7 +4309,10 @@ export class AUNChannel {
4081
4309
  severity,
4082
4310
  terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
4083
4311
  };
4084
- const metadata = extraMeta ? this.stripUndefinedDeep(extraMeta) : undefined;
4312
+ const metadataInput = extraMeta ? { ...extraMeta } : undefined;
4313
+ if (metadataInput)
4314
+ delete metadataInput.severity;
4315
+ const metadata = metadataInput ? this.stripUndefinedDeep(metadataInput) : undefined;
4085
4316
  if (metadata && Object.keys(metadata).length > 0)
4086
4317
  statusPayload.metadata = metadata;
4087
4318
  if (context?.threadId)
@@ -4093,8 +4324,8 @@ export class AUNChannel {
4093
4324
  if (context?.replyToMessageId)
4094
4325
  statusPayload.ref_message_id = context.replyToMessageId;
4095
4326
  const notifyOptions = { ttlMs: 60_000 };
4096
- if (this.isGroupId(channelId))
4097
- notifyOptions.groupId = channelId;
4327
+ if (delivery.chatType === 'group')
4328
+ notifyOptions.groupId = delivery.groupId;
4098
4329
  else
4099
4330
  notifyOptions.to = channelId;
4100
4331
  this.trace('OUT', 'notify.task_status', {
@@ -4115,7 +4346,7 @@ export class AUNChannel {
4115
4346
  }).catch(() => { });
4116
4347
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
4117
4348
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
4118
- const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
4349
+ const targetLabel = delivery.chatType === 'group' ? delivery.groupId : this.peerLabel(channelId);
4119
4350
  const chatmode = context?.metadata?.chatmode ?? '?';
4120
4351
  const initiator = statusPayload.initiator ?? '';
4121
4352
  const refMsgId = statusPayload.ref_message_id ?? '';
@@ -4127,8 +4358,6 @@ export class AUNChannel {
4127
4358
  logger.info(logTaskStatus);
4128
4359
  }
4129
4360
  async sendCustomPayload(channelId, payload, context) {
4130
- if (!this.client || !this.connected)
4131
- return;
4132
4361
  // SDK 0.3.0 E2EE requires payload to be an object
4133
4362
  let payloadObj;
4134
4363
  try {
@@ -4362,21 +4591,25 @@ export class AUNChannelPlugin {
4362
4591
  channelKey: inst.name, // channelName 实际上就是 channelKey
4363
4592
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true, authenticatedApproval: true },
4364
4593
  send: async (envelope, payload) => {
4594
+ const channelId = envelope?.channelId;
4595
+ const delivery = requireEnvelopeDelivery(channelId, envelope?.delivery, envelope?.replyContext?.delivery);
4365
4596
  const parentCausation = normalizeCausation(envelope.causation ?? envelope.replyContext?.metadata?.causation);
4366
4597
  const outboundCausation = parentCausation ? deriveCausation(parentCausation) : undefined;
4367
4598
  const replyCtx = outboundCausation
4368
4599
  ? {
4369
4600
  ...(envelope.replyContext ?? {}),
4601
+ delivery,
4370
4602
  metadata: { ...(envelope.replyContext?.metadata ?? {}), causation: outboundCausation },
4371
4603
  }
4372
- : envelope.replyContext;
4604
+ : (envelope.replyContext || envelope.delivery)
4605
+ ? { ...(envelope.replyContext ?? {}), delivery }
4606
+ : undefined;
4373
4607
  if (outboundCausation) {
4374
4608
  recordCausationSpan(outboundCausation, 'message.outbound', {
4375
4609
  status: 'started',
4376
4610
  refs: { taskId: envelope.taskId, sessionId: envelope.sessionId },
4377
4611
  });
4378
4612
  }
4379
- const channelId = envelope.channelId;
4380
4613
  const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
4381
4614
  switch (payload.kind) {
4382
4615
  case 'result.text':
@@ -4505,7 +4738,7 @@ export class AUNChannelPlugin {
4505
4738
  aunCard.text = action.body;
4506
4739
  setIfDefined(aunCard, 'format', action.bodyFormat);
4507
4740
  }
4508
- if (req.initiatorId && channel.isGroupId(channelId))
4741
+ if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
4509
4742
  aunCard.initiator = req.initiatorId;
4510
4743
  if (replyCtx?.threadId)
4511
4744
  aunCard.thread_id = replyCtx.threadId;
@@ -4518,6 +4751,7 @@ export class AUNChannelPlugin {
4518
4751
  requestId: req.id,
4519
4752
  isCommandCard: false,
4520
4753
  initiatorAid: req.initiatorId,
4754
+ delivery: replyCtx?.delivery,
4521
4755
  expiresAt: Date.now() + cardTtlMs,
4522
4756
  },
4523
4757
  });
@@ -4542,7 +4776,7 @@ export class AUNChannelPlugin {
4542
4776
  aunCard.text = card.body;
4543
4777
  setIfDefined(aunCard, 'format', card.bodyFormat);
4544
4778
  }
4545
- if (req.initiatorId && channel.isGroupId(channelId))
4779
+ if (req.initiatorId && replyCtx?.delivery?.chatType === 'group')
4546
4780
  aunCard.initiator = req.initiatorId;
4547
4781
  if (replyCtx?.threadId)
4548
4782
  aunCard.thread_id = replyCtx.threadId;
@@ -4555,6 +4789,7 @@ export class AUNChannelPlugin {
4555
4789
  requestId: req.id,
4556
4790
  isCommandCard: true,
4557
4791
  initiatorAid: req.initiatorId,
4792
+ delivery: replyCtx?.delivery,
4558
4793
  expiresAt: Date.now() + cardTtlMs,
4559
4794
  },
4560
4795
  });