evolclaw 3.2.0 → 3.4.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 (95) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -4
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -31
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1152 -140
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +58 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/outbox.js +14 -2
  11. package/dist/aun/storage/download.js +1 -1
  12. package/dist/aun/storage/upload.js +13 -1
  13. package/dist/channels/aun.js +869 -358
  14. package/dist/channels/dingtalk.js +77 -140
  15. package/dist/channels/feishu.js +125 -154
  16. package/dist/channels/qqbot.js +75 -138
  17. package/dist/channels/wechat.js +75 -136
  18. package/dist/channels/wecom.js +75 -138
  19. package/dist/cli/agent-command.js +591 -0
  20. package/dist/cli/agent.js +23 -8
  21. package/dist/cli/aun-commands.js +1444 -0
  22. package/dist/cli/ctl-command.js +78 -0
  23. package/dist/cli/daemon-commands.js +2707 -0
  24. package/dist/cli/index.js +23 -4905
  25. package/dist/cli/init.js +33 -6
  26. package/dist/cli/model.js +1 -1
  27. package/dist/cli/restart-monitor.js +539 -0
  28. package/dist/cli/stats.js +558 -0
  29. package/dist/cli/version.js +87 -0
  30. package/dist/cli/watch-logs.js +33 -0
  31. package/dist/cli/watch-msg.js +5 -2
  32. package/dist/config-store.js +12 -6
  33. package/dist/core/channel-loader.js +88 -83
  34. package/dist/core/command/command-handler.js +1189 -0
  35. package/dist/core/command/menu-handler.js +1478 -0
  36. package/dist/core/command/slash-gate.js +142 -0
  37. package/dist/core/command/slash-handler.js +2090 -0
  38. package/dist/core/evolagent-registry.js +82 -0
  39. package/dist/core/evolagent.js +17 -1
  40. package/dist/core/interaction-router.js +8 -0
  41. package/dist/core/message/command-handler-agent-control.js +63 -1
  42. package/dist/core/message/im-renderer.js +91 -51
  43. package/dist/core/message/items-formatter.js +9 -1
  44. package/dist/core/message/message-bridge.js +73 -24
  45. package/dist/core/message/message-log.js +1 -0
  46. package/dist/core/message/message-processor.js +432 -94
  47. package/dist/core/message/message-queue.js +70 -2
  48. package/dist/core/message/pending-hints.js +232 -0
  49. package/dist/core/model/model-catalog.js +1 -1
  50. package/dist/core/model/model-scope.js +2 -2
  51. package/dist/core/permission.js +25 -12
  52. package/dist/core/relation/peer-identity.js +16 -1
  53. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  54. package/dist/core/session/session-manager.js +86 -26
  55. package/dist/core/session/session-title.js +26 -0
  56. package/dist/core/stats/billing.js +151 -0
  57. package/dist/core/stats/budget.js +93 -0
  58. package/dist/core/stats/db.js +334 -0
  59. package/dist/core/stats/eck-vars.js +84 -0
  60. package/dist/core/stats/index.js +10 -0
  61. package/dist/core/stats/normalizer.js +78 -0
  62. package/dist/core/stats/query.js +760 -0
  63. package/dist/core/stats/writer.js +115 -0
  64. package/dist/core/trigger/manager.js +34 -0
  65. package/dist/core/trigger/parser.js +9 -3
  66. package/dist/core/trigger/scheduler.js +20 -17
  67. package/dist/data/error-dict.json +7 -0
  68. package/dist/{agents → eck}/manifest-engine.js +20 -1
  69. package/dist/{agents → eck}/message-renderer.js +24 -1
  70. package/dist/index.js +174 -9
  71. package/dist/ipc.js +116 -1
  72. package/dist/utils/cross-platform.js +58 -5
  73. package/dist/utils/ecweb-launch.js +49 -0
  74. package/dist/utils/ecweb-pair.js +20 -0
  75. package/dist/utils/error-utils.js +18 -5
  76. package/dist/utils/npm-ops.js +38 -8
  77. package/dist/utils/stats.js +77 -6
  78. package/kits/docs/evolclaw/INDEX.md +3 -1
  79. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  80. package/kits/docs/evolclaw/fs.md +131 -0
  81. package/kits/docs/evolclaw/group-fs.md +209 -0
  82. package/kits/docs/evolclaw/stats.md +70 -0
  83. package/kits/docs/venues/aun-group.md +29 -6
  84. package/kits/docs/venues/group.md +5 -4
  85. package/kits/eck_message_manifest.json +30 -3
  86. package/kits/rules/05-venue.md +1 -1
  87. package/kits/templates/message-fragments/inject-default.md +2 -0
  88. package/package.json +5 -6
  89. package/dist/agents/baseagent-normalize.js +0 -19
  90. package/dist/core/command-handler.js +0 -3876
  91. package/dist/core/relation/peer-key.js +0 -16
  92. package/dist/evolclaw-config.js +0 -11
  93. package/dist/utils/channel-helpers.js +0 -46
  94. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  95. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -5,12 +5,13 @@ import path from 'path';
5
5
  import os from 'os';
6
6
  import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
- import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
8
+ import { resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
9
9
  import { resolvePaths, getPackageRoot, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
10
  import { saveToUploads, sanitizeFileName, bufferToInboundImage } from '../utils/media-cache.js';
11
11
  import { appendAidEvent } from '../utils/instance-registry.js';
12
- import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
12
+ import { appendMessageLog, buildOutboundEntry, buildInboundEntry } from '../core/message/message-log.js';
13
13
  import { chatDirPath } from '../core/session/session-fs-store.js';
14
+ import { appendHintAdd, appendHintRemove, parseInjectRequest } from '../core/message/pending-hints.js';
14
15
  import { appendAidLifecycle } from '../aun/aid/identity.js';
15
16
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
16
17
  import { loadAgent, saveAgent } from '../config-store.js';
@@ -87,6 +88,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
87
88
  replyContext: opts.replyContext,
88
89
  source: opts.source,
89
90
  images: opts.images,
91
+ dispatchMode: opts.dispatchMode,
90
92
  };
91
93
  }
92
94
  export class AUNChannel {
@@ -115,14 +117,16 @@ export class AUNChannel {
115
117
  // 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
116
118
  const d = (data && typeof data === 'object') ? data : {};
117
119
  const payload = d.payload ?? {};
120
+ // 入站事件(IN)路由字段在 d.envelope.*(SDK 0.5.*);出站 params(OUT)字段在顶层。两者兼顾。
121
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
118
122
  const topContext = {
119
123
  self_aid: this._aid ?? this.config.aid,
120
124
  };
121
125
  // peer / group 识别
122
- const peerAid = d.to ?? d.from ?? d.sender_aid ?? payload.to;
126
+ const peerAid = env.to ?? env.from ?? d.to ?? d.from ?? d.sender_aid ?? payload.to;
123
127
  if (peerAid)
124
128
  topContext.peer_aid = peerAid;
125
- const groupId = d.group_id ?? payload.group_id;
129
+ const groupId = env.group_id ?? d.group_id ?? payload.group_id;
126
130
  if (groupId)
127
131
  topContext.group_id = groupId;
128
132
  // task_id / chatmode(message.send / thought.put / status 都可能有)
@@ -175,6 +179,7 @@ export class AUNChannel {
175
179
  * - 数字群号:{group_no}.{issuer}(如 11117.agentid.pub)
176
180
  * - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
177
181
  */
182
+ /** 判断 channelId 是否群组 ID(public:plugin adapter 闭包需调用) */
178
183
  isGroupId(id) {
179
184
  return (id.startsWith('group.') && id.includes('/'))
180
185
  || /^\d+\./.test(id)
@@ -201,6 +206,20 @@ export class AUNChannel {
201
206
  const name = cached?.name;
202
207
  return name && name !== short ? `${short}(${name})` : short;
203
208
  }
209
+ notifyCardActionFailure(channelId, text, operatorId, threadId) {
210
+ if (!channelId) {
211
+ logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
212
+ return;
213
+ }
214
+ const context = {};
215
+ if (threadId)
216
+ context.threadId = threadId;
217
+ if (operatorId && this.isGroupId(channelId))
218
+ context.peerId = operatorId;
219
+ void this.sendMessage(channelId, text, context).catch((err) => {
220
+ logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
221
+ });
222
+ }
204
223
  extractTextPayload(payload, channelId, senderAid) {
205
224
  if (typeof payload === 'string')
206
225
  return payload;
@@ -215,27 +234,25 @@ export class AUNChannel {
215
234
  if (cardInfo) {
216
235
  const actionValue = typeof obj.value === 'string' ? obj.value
217
236
  : typeof obj.action_value === 'string' ? obj.action_value : text;
237
+ const threadId = typeof obj.thread_id === 'string' ? obj.thread_id : undefined;
238
+ // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
239
+ // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
240
+ // 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
241
+ const cardClickerAid = senderAid || channelId || '';
218
242
  if (cardInfo.isCommandCard) {
219
243
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息
220
244
  this.cardMessageIdMap.delete(cardMsgId);
221
245
  if (this.messageHandler && actionValue.startsWith('/')) {
222
246
  const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
223
- // 卡片点击者身份:优先 payload.from / payload.sender_aid / payload.user_id,
224
- // fallback 到外层 senderAid,最后用 cardInfo 中记录的原始命令发起者
225
- const cardClickerAid = (typeof obj.from === 'string' && obj.from)
226
- || (typeof obj.sender_aid === 'string' && obj.sender_aid)
227
- || (typeof obj.user_id === 'string' && obj.user_id)
228
- || senderAid
229
- || cardInfo.initiatorAid
230
- || channelId || '';
231
- // Initiator 校验:群聊中仅卡片发起者可操作(与飞书行为对齐)
232
- if (cardInfo.initiatorAid && cardClickerAid
233
- && cardClickerAid !== cardInfo.initiatorAid
234
- && !this.isGroupId(cardClickerAid)) {
247
+ // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
248
+ // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
249
+ if (chatType === 'group' && cardInfo.initiatorAid && cardClickerAid
250
+ && cardClickerAid !== cardInfo.initiatorAid) {
235
251
  logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
252
+ this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
236
253
  return '';
237
254
  }
238
- this.messageHandler({
255
+ Promise.resolve(this.messageHandler({
239
256
  channelId: channelId || '',
240
257
  chatType,
241
258
  content: actionValue,
@@ -243,8 +260,18 @@ export class AUNChannel {
243
260
  peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
244
261
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
245
262
  source: 'card-trigger',
263
+ })).catch((err) => {
264
+ logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
265
+ const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
266
+ this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
246
267
  });
247
268
  }
269
+ else if (!this.messageHandler) {
270
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
271
+ }
272
+ else {
273
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
274
+ }
248
275
  }
249
276
  else {
250
277
  // ActionInteraction:走 interactionCallback → InteractionRouter
@@ -256,12 +283,22 @@ export class AUNChannel {
256
283
  id: cardInfo.requestId,
257
284
  action: actionValue,
258
285
  values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
286
+ operatorId: cardClickerAid || undefined,
259
287
  });
260
288
  }
289
+ else {
290
+ this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
291
+ }
261
292
  }
262
293
  }
294
+ else if (this.ownedCardMsgIds.has(cardMsgId)) {
295
+ // 本 agent 发出的卡片,但 entry 已过期(20min TTL)
296
+ logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
297
+ this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
298
+ }
263
299
  else {
264
- logger.debug(`${this.logPrefix()} action_card_reply dropped: cardMsgId=${cardMsgId} hasCallback=${!!this.interactionCallback}`);
300
+ // 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
301
+ logger.debug(`${this.logPrefix()} action_card_reply ignored (not owned): cardMsgId=${cardMsgId}`);
265
302
  }
266
303
  // 始终返回空字符串,阻止消息分发给 agent
267
304
  return '';
@@ -402,6 +439,13 @@ export class AUNChannel {
402
439
  .replace(/[ \t]+/g, ' ')
403
440
  .trim();
404
441
  }
442
+ /** 剥离正文中所有 @aid(用于命令判定 + 命令消息进 agent 前的清理)。 */
443
+ stripAllMentions(text) {
444
+ return text
445
+ .replace(/(^|\s)@[\w.-]+(?=$|\s|[.,!?;:,。!?;:]|[\u4e00-\u9fff])/g, '$1')
446
+ .replace(/[ \t]+/g, ' ')
447
+ .trim();
448
+ }
405
449
  extractMentionAids(mentions) {
406
450
  const aids = [];
407
451
  for (const m of mentions) {
@@ -468,6 +512,8 @@ export class AUNChannel {
468
512
  interactionCallback;
469
513
  // action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
470
514
  cardMessageIdMap = new Map();
515
+ /** 本 agent 曾发出过的卡片 msgId(只增不删,用于区分"过期失效"vs"他人发的卡") */
516
+ ownedCardMsgIds = new Set();
471
517
  dispatchModeResolver;
472
518
  static PROACTIVE_ALLOW_TYPES = new Set([
473
519
  'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
@@ -477,6 +523,8 @@ export class AUNChannel {
477
523
  static MENU_REQUEST_TYPES = new Set([
478
524
  'menu.list', 'menu.query', 'menu.options', 'menu.update', 'menu.action',
479
525
  ]);
526
+ /** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
527
+ static INJECT_REQUEST_TYPE = 'observer.inject';
480
528
  // Reconnect state
481
529
  // SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
482
530
  // 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
@@ -504,6 +552,7 @@ export class AUNChannel {
504
552
  // AID 连接状态(供 status 命令聚合展示)
505
553
  aidState;
506
554
  aidStatsCollector;
555
+ outboxInFlight = new Set();
507
556
  constructor(config) {
508
557
  this.config = config;
509
558
  this.agentDir = agentDirPath(config.aid);
@@ -627,8 +676,9 @@ export class AUNChannel {
627
676
  if (!this.config.pureIdentity) {
628
677
  client.on('group.message_created', (data) => {
629
678
  this.trace('IN', 'group.message_created', data);
630
- const gid = (data && typeof data === 'object') ? data.group_id ?? '' : '';
631
- const sender = (data && typeof data === 'object') ? data.sender_aid ?? '' : '';
679
+ const env = (data && typeof data === 'object') ? data.envelope ?? {} : {};
680
+ const gid = env.group_id ?? '';
681
+ const sender = env.from ?? '';
632
682
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
633
683
  this.handleIncomingGroupMessage(data);
634
684
  });
@@ -659,14 +709,34 @@ export class AUNChannel {
659
709
  client.on('message.undecryptable', (data) => {
660
710
  this.trace('IN', 'message.undecryptable', data);
661
711
  const d = data;
662
- logger.warn(`${this.logPrefix()} Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
712
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
713
+ logger.warn(`${this.logPrefix()} Message undecryptable: from=${env.from} mid=${d.message_id} err=${d._decrypt_error}`);
663
714
  });
664
715
  // pureIdentity(控制 AID):不注册 group 解密失败监听
665
716
  if (!this.config.pureIdentity) {
666
717
  client.on('group.message_undecryptable', (data) => {
667
718
  this.trace('IN', 'group.message_undecryptable', data);
668
719
  const d = data;
669
- logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
720
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
721
+ logger.warn(`${this.logPrefix()} Group message undecryptable: group=${env.group_id} from=${env.from} mid=${d.message_id} err=${d._decrypt_error}`);
722
+ });
723
+ // 群消息撤回(SDK 0.4.10 在线 push 通道):与私聊 message.recalled 同构,
724
+ // 逐个 message_id 交给 recallHandler → msgBridge.cancel(排队中删除 / 处理中中断)。
725
+ client.on('group.message_recalled', (data) => {
726
+ this.trace('IN', 'group.message_recalled', data);
727
+ if (data && typeof data === 'object') {
728
+ const d = data;
729
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
730
+ const ids = d.message_ids;
731
+ if (Array.isArray(ids)) {
732
+ for (const id of ids) {
733
+ if (typeof id === 'string') {
734
+ logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
735
+ this.recallHandler?.(id);
736
+ }
737
+ }
738
+ }
739
+ }
670
740
  });
671
741
  }
672
742
  // Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
@@ -1004,15 +1074,23 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1004
1074
  if (!data || typeof data !== 'object')
1005
1075
  return;
1006
1076
  const msg = data;
1007
- const fromAid = msg.from ?? '';
1077
+ // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1078
+ // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1079
+ const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1080
+ const fromAid = env.from ?? '';
1008
1081
  const payload = msg.payload ?? '';
1009
- const text = this.extractTextPayload(payload, fromAid);
1082
+ const text = this.extractTextPayload(payload, fromAid, fromAid);
1010
1083
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1011
1084
  const messageId = msg.message_id ?? '';
1012
1085
  const seq = msg.seq;
1013
1086
  // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1014
1087
  // forwardInbound 内部排除 self-echo 与 from-owner。
1015
- this.forwardInbound(fromAid, seq, payload);
1088
+ // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
1089
+ // (即便日后 from-owner 排除规则调整,也不会泄漏)。
1090
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1091
+ if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
1092
+ this.forwardInbound(msg);
1093
+ }
1016
1094
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
1017
1095
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
1018
1096
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -1022,7 +1100,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1022
1100
  return;
1023
1101
  }
1024
1102
  // 记录入站消息加密状态,透传到出站 ReplyContext
1025
- const msgEncrypted = !!(msg.e2ee);
1103
+ const msgEncrypted = !!env.encrypted;
1026
1104
  if (!msgEncrypted)
1027
1105
  this.plaintextRecv++;
1028
1106
  // Detect @mentions
@@ -1058,6 +1136,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1058
1136
  });
1059
1137
  return;
1060
1138
  }
1139
+ // observer.inject:owner 插话。鉴权 from∈owners 后,以 target.channel_id 选 agent↔对端 会话,
1140
+ // observer 插话(v0.3):只落盘到 pending-hints,不进 Agent、不回 owner。详见 docs/observer-insert-design.md。
1141
+ if (p2pPayloadType === AUNChannel.INJECT_REQUEST_TYPE) {
1142
+ this.acknowledgeImmediately(messageId, seq);
1143
+ this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type);
1144
+ return;
1145
+ }
1061
1146
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
1062
1147
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
1063
1148
  this.acknowledgeImmediately(messageId, seq);
@@ -1068,10 +1153,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1068
1153
  logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1069
1154
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
1070
1155
  const isSystemP2P = p2pPayloadType === 'event';
1071
- this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
1156
+ this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
1072
1157
  const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
1073
1158
  if (threadId)
1074
1159
  replyContext.threadId = threadId;
1160
+ replyContext.peerId = fromAid;
1161
+ if (messageId)
1162
+ replyContext.replyToMessageId = messageId;
1075
1163
  this.dispatchMessage({
1076
1164
  channelId: chatId,
1077
1165
  userId: fromAid,
@@ -1094,8 +1182,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1094
1182
  if (!data || typeof data !== 'object')
1095
1183
  return;
1096
1184
  const msg = data;
1097
- const groupId = msg.group_id ?? '';
1098
- const senderAid = msg.sender_aid ?? '';
1185
+ // SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1186
+ // message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
1187
+ const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1188
+ const groupId = env.group_id ?? '';
1189
+ const senderAid = env.from ?? '';
1099
1190
  const payload = msg.payload ?? '';
1100
1191
  const text = this.extractTextPayload(payload, groupId, senderAid);
1101
1192
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
@@ -1103,7 +1194,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1103
1194
  const seq = msg.seq;
1104
1195
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1105
1196
  // forwardInbound 内部排除 self-echo 与 from-owner。
1106
- this.forwardInbound(senderAid, seq, payload);
1197
+ this.forwardInbound(msg);
1107
1198
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1108
1199
  const payloadMentions = Array.isArray(payload?.mentions)
1109
1200
  ? payload.mentions.filter((m) => typeof m === 'string')
@@ -1125,7 +1216,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1125
1216
  const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
1126
1217
  if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
1127
1218
  this.acknowledgeImmediately(messageId, seq);
1128
- const msgEncryptedFast = !!(msg.e2ee);
1219
+ const msgEncryptedFast = !!env.encrypted;
1129
1220
  const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1130
1221
  const peerInfo = this.peerInfoCached(senderAid);
1131
1222
  const shortAid = this.getShortAid(senderAid);
@@ -1176,7 +1267,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1176
1267
  }
1177
1268
  }
1178
1269
  // 记录入站消息加密状态,透传到出站 ReplyContext
1179
- const msgEncrypted = !!(msg.e2ee);
1270
+ const msgEncrypted = !!env.encrypted;
1180
1271
  if (!msgEncrypted)
1181
1272
  this.plaintextRecv++;
1182
1273
  // dispatch_mode: 本地设置优先,fallback 到服务器参数
@@ -1194,7 +1285,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1194
1285
  // 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
1195
1286
  const firstLineGroup = text.split('\n')[0] || '';
1196
1287
  const hasEvolClawTraceGroup = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
1197
- if (/echo/i.test(firstLineGroup) && !hasEvolClawTraceGroup) {
1288
+ const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolClawTraceGroup;
1289
+ // 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
1290
+ // echo 消息走独立的 trace 流程,不参与命令语义判定。
1291
+ const isCommandMsg = !isEchoMsg && this.stripAllMentions(text).startsWith('/');
1292
+ if (isEchoMsg) {
1198
1293
  // 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
1199
1294
  // >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
1200
1295
  const echoTs = () => {
@@ -1219,14 +1314,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1219
1314
  return;
1220
1315
  }
1221
1316
  else {
1222
- // 非 echo 消息:正常 mention 过滤
1223
- if (dispatchMode === 'mention' && !mentionedSelf && !mentionedAll) {
1317
+ // 非 echo 消息:mention 过滤
1318
+ // 命令豁免 broadcast:slash 命令在任何 dispatchMode 下都强制走 mention 语义,
1319
+ // 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
1320
+ const enforceMention = dispatchMode === 'mention' || isCommandMsg;
1321
+ if (enforceMention && !mentionedSelf && !mentionedAll) {
1224
1322
  this.acknowledgeImmediately(messageId, seq);
1225
- logger.info(`${this.logPrefix()} Group dropped: unmentioned in mention-mode (group=${groupId} sender=${senderAid} mid=${messageId} textPreview=${JSON.stringify(text.slice(0, 80))})`);
1323
+ logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mode=${dispatchMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
1226
1324
  return;
1227
1325
  }
1228
1326
  }
1229
- const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
1327
+ // 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
1328
+ // 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
1329
+ const strippedText = isCommandMsg
1330
+ ? this.stripAllMentions(text)
1331
+ : this.stripSelfMentionIfOnly(text, this._aid);
1230
1332
  // Detect attachments before the empty-text guard (顶层 + 嵌套)
1231
1333
  const rawAttachments = this.collectAllAttachments(payload);
1232
1334
  const hasAttachments = rawAttachments.length > 0;
@@ -1263,7 +1365,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1263
1365
  const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1264
1366
  logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1265
1367
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: senderAid, msgId: messageId, kind: 'text', len: finalText.length, groupId });
1266
- this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1368
+ this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
1267
1369
  // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1268
1370
  // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1269
1371
  const renderMentionAids = Array.from(new Set([
@@ -1287,6 +1389,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1287
1389
  mentions,
1288
1390
  mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
1289
1391
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1392
+ dispatchMode: serverDispatchMode,
1290
1393
  images: inboundImages.length > 0 ? inboundImages : undefined,
1291
1394
  });
1292
1395
  }
@@ -1358,6 +1461,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1358
1461
  mentions: mentionObjects,
1359
1462
  mentionAids: event.mentionAids,
1360
1463
  replyContext,
1464
+ source: event.source,
1465
+ dispatchMode: event.dispatchMode,
1361
1466
  images: event.images,
1362
1467
  }).catch(err => {
1363
1468
  logger.error(`${this.logPrefix()} Message handler error:`, err);
@@ -1384,57 +1489,161 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1384
1489
  * 若消息发送方本身是 owner,则不转发给该 owner,但仍转发给其他 owner。
1385
1490
  * 调用点须在所有过滤逻辑之前,payload 为 SDK 解密后的明文。
1386
1491
  */
1387
- forwardInbound(from, seq, payload) {
1492
+ /**
1493
+ * 入站转发:把对端发来的消息原样转发给 owner。
1494
+ * data 为 SDK message.received / group.message_created 回调的整个对象,
1495
+ * 不拆解、不重组——SDK 信封结构变化不影响此处。
1496
+ */
1497
+ forwardInbound(data) {
1388
1498
  if (!this.connected || !this.client)
1389
1499
  return;
1390
1500
  const { observable, owners } = this.getObserverConfig();
1391
1501
  if (!observable || owners.length === 0)
1392
1502
  return;
1503
+ const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
1504
+ const from = env.from ?? '';
1393
1505
  if (this._aid && from === this._aid)
1394
1506
  return; // self-echo:已在出站转过
1395
1507
  // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
1396
1508
  const recipientOwners = owners.filter(o => o !== from);
1397
1509
  if (recipientOwners.length === 0)
1398
1510
  return;
1399
- this.emitForward('inbound', { from, to: this.config.aid, seq, payload }, recipientOwners);
1511
+ this.emitForward('inbound', data, recipientOwners);
1400
1512
  }
1401
1513
  /**
1402
- * 出站转发:Agent 经 AUN 真实发出的消息全部转发。
1403
- * to 为对端(私聊 AID / ID),payload 为实际发送的明文 payload
1404
- * to 本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1514
+ * 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
1515
+ * result SDK message.send / group.send SendResult(已 attach envelope + payload)。
1516
+ * 若对端本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1405
1517
  */
1406
- forwardOutbound(to, payload) {
1518
+ forwardOutbound(result) {
1407
1519
  if (!this.connected || !this.client)
1408
1520
  return;
1409
1521
  const { observable, owners } = this.getObserverConfig();
1410
1522
  if (!observable || owners.length === 0)
1411
1523
  return;
1412
- // 过滤:若 to 本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1524
+ const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
1525
+ const to = env.to ?? env.group_id ?? '';
1526
+ // 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1413
1527
  // 但仍转发给其他 owner。
1414
1528
  const recipientOwners = owners.filter(o => o !== to);
1415
1529
  if (recipientOwners.length === 0)
1416
1530
  return;
1417
- this.emitForward('outbound', { from: this.config.aid, to, payload }, recipientOwners);
1531
+ this.emitForward('outbound', result, recipientOwners);
1418
1532
  }
1419
- /** 实际投递 observer.forward 给每个 owner,外层一律明文。 */
1533
+ /**
1534
+ * 实际投递 observer.forward 给每个 owner,外层一律明文。
1535
+ * original 为 SDK 给到的原始对象(入站回调 data / 出站 SendResult),整体透传,
1536
+ * 不挑字段、不改字段——SDK 加任何字段都会自动一并转发给 owner。
1537
+ */
1420
1538
  emitForward(direction, original, owners) {
1421
1539
  const forwardPayload = {
1422
1540
  type: 'observer.forward',
1423
1541
  direction,
1424
1542
  agent_aid: this.config.aid,
1425
- original: {
1426
- from: original.from,
1427
- to: original.to,
1428
- ...(original.seq != null ? { seq: original.seq } : {}),
1429
- timestamp: Date.now(),
1430
- payload: original.payload,
1431
- },
1543
+ original,
1432
1544
  };
1433
1545
  for (const ownerAid of owners) {
1434
1546
  this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
1435
1547
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
1436
1548
  }
1437
1549
  }
1550
+ // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
1551
+ //
1552
+ // owner 经 message.send 给 agent 自身 AID 发 observer.inject(payload 为对象)。
1553
+ // 鉴权 from∈owners 后,把提示【只落盘】到 agent↔对端 会话的 pending-hints.jsonl
1554
+ // (不 dispatch、不跑 LLM、不回 owner);下一条对端消息到达时由 message-processor
1555
+ // 回放消费、注入渲染层。action=add 加提示 / remove 撤销。
1556
+ // 详见 docs/observer-insert-design.md 第一部分。
1557
+ /** 回 observer.inject.ack 给 owner(明文)。accepted 在成功写盘之后发出。 */
1558
+ emitInjectAck(ownerAid, injectId, data, error) {
1559
+ if (!this.connected || !this.client)
1560
+ return;
1561
+ const ackPayload = { type: 'observer.inject.ack' };
1562
+ if (injectId)
1563
+ ackPayload.id = injectId;
1564
+ if (data)
1565
+ ackPayload.data = data;
1566
+ if (error)
1567
+ ackPayload.error = error;
1568
+ this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: false })
1569
+ .catch(e => logger.debug(`${this.logPrefix()} observer.inject.ack to ${ownerAid} failed: ${e}`));
1570
+ }
1571
+ /** 处理 observer.inject:鉴权 + 校验 + 只落盘到 pending-hints(不触发处理、不回 owner)。 */
1572
+ handleObserverInject(fromAid, payload, displayName, peerType) {
1573
+ void peerType;
1574
+ const { owners } = this.getObserverConfig();
1575
+ const ts = Date.now();
1576
+ const req = parseInjectRequest(payload, fromAid, owners, ts);
1577
+ if (req.kind === 'reject') {
1578
+ logger.warn(`${this.logPrefix()} observer.inject rejected: ${this.getShortAid(fromAid)} ${req.code}`);
1579
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.action }, { code: req.code, message: req.message });
1580
+ return;
1581
+ }
1582
+ const selfAID = this.config.aid;
1583
+ const sessionsDir = resolvePaths().sessionsDir;
1584
+ let ok;
1585
+ if (req.kind === 'remove') {
1586
+ ok = appendHintRemove(sessionsDir, 'aun', req.channelId, selfAID, { targetId: req.targetId, threadId: req.threadId, ts });
1587
+ }
1588
+ else {
1589
+ ok = appendHintAdd(sessionsDir, 'aun', req.channelId, selfAID, { id: req.id, text: req.text, threadId: req.threadId, ownerAid: req.ownerAid, ts });
1590
+ }
1591
+ if (!ok) {
1592
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示落盘失败' });
1593
+ return;
1594
+ }
1595
+ logger.info(`${this.logPrefix()} observer.inject ${req.kind} stored: from=${this.getShortAid(fromAid)}(${displayName}) target=${req.channelId} chatType=${req.chatType} thread=${req.threadId ?? 'main'}${req.kind === 'add' ? ` textLen=${req.text.length}` : `${req.targetId ? ` targetId=${req.targetId}` : ' (clear-all)'}`}`);
1596
+ this.emitInjectAck(fromAid, req.injectId, { status: 'accepted', action: req.kind });
1597
+ // 记录到 watch(被观察的 agent↔对端 会话),带 owner-inject 标记区分对端真实消息。
1598
+ // v0.3:只记"提示已添加/已撤销",不触发处理、不产生 agent→owner 回应。
1599
+ const watchText = req.kind === 'remove' ? `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}` : req.text;
1600
+ const synthId = `inject-${req.injectId || ts}`;
1601
+ this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, synthId, watchText);
1602
+ }
1603
+ /**
1604
+ * 把 observer 插话 / 对插话的回应记录到 watch(被观察的 agent↔对端 会话),
1605
+ * 带 source='owner-inject' 标记,与对端真实消息区分。
1606
+ * 写三处:messages.jsonl(watch msg)、appendAidEvent(watch aid 事件流)、aidStatsCollector(统计)。
1607
+ * @param dir 'in'=owner→agent 插话;'out'=agent→owner 对插话的回应
1608
+ * @param peerChannelId 被观察会话的对端(agent↔对端),日志落点 = sessions/aun/<self>/<peerChannelId>/
1609
+ */
1610
+ recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text) {
1611
+ try {
1612
+ const selfAID = this.config.aid;
1613
+ const isGroup = chatType === 'group';
1614
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID);
1615
+ const entry = dir === 'in'
1616
+ ? buildInboundEntry({
1617
+ from: ownerAid, to: selfAID, chatType,
1618
+ groupId: isGroup ? peerChannelId : null, msgId, content: text,
1619
+ permMode: 'owner', source: 'owner-inject',
1620
+ })
1621
+ : buildOutboundEntry({
1622
+ from: selfAID, to: ownerAid, chatType,
1623
+ groupId: isGroup ? peerChannelId : null, msgId, content: text,
1624
+ source: 'owner-inject',
1625
+ });
1626
+ appendMessageLog(chatDir, entry);
1627
+ }
1628
+ catch (e) {
1629
+ logger.debug(`${this.logPrefix()} recordInjectWatch(msg) failed: ${e}`);
1630
+ }
1631
+ // watch aid:事件流 + 统计(标 inject,便于过滤)
1632
+ try {
1633
+ const len = Buffer.byteLength(text, 'utf-8');
1634
+ if (dir === 'in') {
1635
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: ownerAid, msgId, kind: 'text', len, inject: true });
1636
+ this.aidStatsCollector?.recordInbound(this.config.aid, ownerAid, len, text, false, false, 'inject', 'inject');
1637
+ }
1638
+ else {
1639
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: ownerAid, msgId, kind: 'text', len, inject: true });
1640
+ this.aidStatsCollector?.recordOutbound(this.config.aid, ownerAid, len, text, false, false, 'inject', 'inject');
1641
+ }
1642
+ }
1643
+ catch (e) {
1644
+ logger.debug(`${this.logPrefix()} recordInjectWatch(aid) failed: ${e}`);
1645
+ }
1646
+ }
1438
1647
  handleEcho(event) {
1439
1648
  const ts = () => {
1440
1649
  const d = new Date();
@@ -1785,6 +1994,219 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1785
1994
  onRecall(handler) {
1786
1995
  this.recallHandler = handler;
1787
1996
  }
1997
+ async withOutboxInFlight(entry, run, busyValue) {
1998
+ if (this.outboxInFlight.has(entry.id))
1999
+ return busyValue;
2000
+ this.outboxInFlight.add(entry.id);
2001
+ try {
2002
+ return await run();
2003
+ }
2004
+ finally {
2005
+ this.outboxInFlight.delete(entry.id);
2006
+ }
2007
+ }
2008
+ messageIdFromSendResult(result) {
2009
+ return result?.message?.message_id ?? result?.message_id ?? null;
2010
+ }
2011
+ payloadLogText(payload, contentKind) {
2012
+ if (typeof payload.text === 'string' && payload.text)
2013
+ return payload.text;
2014
+ switch (contentKind ?? payload.type) {
2015
+ case 'image':
2016
+ return payload.alt ? `[image] ${payload.alt}` : '[image]';
2017
+ case 'card':
2018
+ case 'action_card':
2019
+ return payload.title ? `[card] ${payload.title}` : '[card]';
2020
+ case 'file':
2021
+ return payload.filename ? `[file] ${payload.filename}` : '[file]';
2022
+ default:
2023
+ return `[${String(payload.type ?? contentKind ?? 'payload')}]`;
2024
+ }
2025
+ }
2026
+ payloadByteLength(payload, logText) {
2027
+ if (payload.type === 'image' && typeof payload.data_base64 === 'string') {
2028
+ return Buffer.byteLength(payload.data_base64, 'utf-8');
2029
+ }
2030
+ return Buffer.byteLength(logText, 'utf-8');
2031
+ }
2032
+ applyReplyContextToPayload(payload, context) {
2033
+ const finalPayload = { ...payload };
2034
+ if (context?.threadId && !finalPayload.thread_id)
2035
+ finalPayload.thread_id = context.threadId;
2036
+ if (context?.metadata?.taskId && !finalPayload.task_id)
2037
+ finalPayload.task_id = context.metadata.taskId;
2038
+ if (context?.metadata?.chatmode && !finalPayload.chatmode)
2039
+ finalPayload.chatmode = context.metadata.chatmode;
2040
+ return finalPayload;
2041
+ }
2042
+ registerCardPostSend(messageId, action) {
2043
+ if (action.type !== 'register_interaction_card')
2044
+ return;
2045
+ this.cardMessageIdMap.set(messageId, {
2046
+ requestId: action.requestId,
2047
+ isCommandCard: action.isCommandCard,
2048
+ initiatorAid: action.initiatorAid,
2049
+ });
2050
+ this.ownedCardMsgIds.add(messageId);
2051
+ const now = Date.now();
2052
+ const mapTtl = action.expiresAt && action.expiresAt > now
2053
+ ? action.expiresAt - now
2054
+ : 20 * 60 * 1000;
2055
+ setTimeout(() => this.cardMessageIdMap.delete(messageId), mapTtl);
2056
+ setTimeout(() => this.ownedCardMsgIds.delete(messageId), 24 * 60 * 60 * 1000);
2057
+ }
2058
+ runPostSend(entry, messageId) {
2059
+ if (!entry.postSend)
2060
+ return;
2061
+ if (entry.postSend.type === 'register_interaction_card') {
2062
+ this.registerCardPostSend(messageId, entry.postSend);
2063
+ }
2064
+ }
2065
+ async sendAunPayload(channelId, payload, context, label) {
2066
+ if (!this.client || !this.connected)
2067
+ return { ok: false };
2068
+ const isGroup = this.isGroupId(channelId);
2069
+ const targetAid = channelId;
2070
+ const encryptTarget = isGroup ? channelId : targetAid;
2071
+ const encrypt = context?.metadata?.encrypted != null
2072
+ ? !!(context.metadata.encrypted)
2073
+ : this.shouldEncrypt(encryptTarget);
2074
+ const method = isGroup ? 'group.send' : 'message.send';
2075
+ const params = { payload, encrypt };
2076
+ if (isGroup)
2077
+ params.group_id = channelId;
2078
+ else
2079
+ params.to = targetAid;
2080
+ const callOnce = async (sendParams, fallback) => {
2081
+ const result = fallback
2082
+ ? await this.client.call(method, sendParams)
2083
+ : await this.callAndTrace(method, sendParams);
2084
+ const mid = this.messageIdFromSendResult(result);
2085
+ if (!mid) {
2086
+ logger.warn(`${this.logPrefix()} ${method}${fallback ? ' fallback' : ''} (${label}) returned no message_id: ${JSON.stringify(result)}`);
2087
+ return { ok: false, result, encrypt: !!sendParams.encrypt };
2088
+ }
2089
+ return { ok: true, messageId: mid, result, encrypt: !!sendParams.encrypt };
2090
+ };
2091
+ try {
2092
+ return await callOnce(params, false);
2093
+ }
2094
+ catch (e) {
2095
+ if (encrypt && e instanceof E2EEError) {
2096
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
2097
+ logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
2098
+ const fallbackParams = { ...params, encrypt: false };
2099
+ try {
2100
+ this.trace('OUT', `${method}.${label}.fallback`, fallbackParams);
2101
+ const sent = await callOnce(fallbackParams, true);
2102
+ this.trace('OUT', `${method}.${label}.fallback.${sent.ok ? 'ok' : 'missing_id'}`, { message_id: sent.messageId });
2103
+ return sent;
2104
+ }
2105
+ catch (e2) {
2106
+ this.trace('OUT', `${method}.${label}.fallback.error`, { channelId, error: String(e2) });
2107
+ logger.error(`${this.logPrefix()} Plaintext ${label} fallback also failed to ${channelId}: ${e2}`);
2108
+ return { ok: false };
2109
+ }
2110
+ }
2111
+ this.trace('OUT', `${method}.${label}.error`, { channelId, error: String(e) });
2112
+ logger.error(`${this.logPrefix()} ${label} send failed to ${channelId}: ${e}`);
2113
+ return { ok: false };
2114
+ }
2115
+ }
2116
+ recordDurableOutbound(channelId, payload, messageId, encrypt, context, isGroup, contentKind, logText, result) {
2117
+ const kind = contentKind ?? payload.type ?? 'custom';
2118
+ appendAidEvent({
2119
+ ts: Date.now(),
2120
+ iso: new Date().toISOString(),
2121
+ event: 'message_out',
2122
+ aid: this.config.aid,
2123
+ to: channelId,
2124
+ msgId: messageId,
2125
+ kind,
2126
+ len: Buffer.byteLength(logText, 'utf-8'),
2127
+ ...(isGroup && { groupId: channelId }),
2128
+ });
2129
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, this.payloadByteLength(payload, logText), logText, false, encrypt, context?.metadata?.chatmode);
2130
+ const source = context?.metadata?.source ?? 'daemon';
2131
+ this.appendOutboundJsonl(channelId, logText, messageId, encrypt, context, isGroup, 'text', source);
2132
+ this.forwardOutbound(result);
2133
+ }
2134
+ async sendContentPayload(channelId, payload, opts) {
2135
+ const finalPayload = this.applyReplyContextToPayload(payload, opts.context);
2136
+ const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
2137
+ const entry = outbox.enqueue(this.config.aid, {
2138
+ channelId,
2139
+ type: 'payload',
2140
+ contentKind: opts.contentKind,
2141
+ payload: finalPayload,
2142
+ context: opts.context,
2143
+ logText,
2144
+ ttl: opts.ttl,
2145
+ postSend: opts.postSend,
2146
+ });
2147
+ logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
2148
+ if (!this.connected || !this.client) {
2149
+ logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
2150
+ if (!this.reconnectTimer && !this.client) {
2151
+ this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
2152
+ }
2153
+ return { queued: true };
2154
+ }
2155
+ const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
2156
+ if (result.ok) {
2157
+ outbox.remove(this.config.aid, entry.id);
2158
+ return { messageId: result.messageId };
2159
+ }
2160
+ return { queued: true };
2161
+ }
2162
+ buildTaskPayloadBase(envelope, context) {
2163
+ const base = {};
2164
+ if (envelope.taskId)
2165
+ base.task_id = envelope.taskId;
2166
+ if (envelope.sessionId)
2167
+ base.session_id = envelope.sessionId;
2168
+ if (envelope.agentName)
2169
+ base.agent_name = envelope.agentName;
2170
+ if (envelope.chatmode)
2171
+ base.chatmode = envelope.chatmode;
2172
+ if (context?.threadId)
2173
+ base.thread_id = context.threadId;
2174
+ if (context?.peerId)
2175
+ base.initiator = context.peerId;
2176
+ if (context?.replyToMessageId)
2177
+ base.ref_message_id = context.replyToMessageId;
2178
+ return base;
2179
+ }
2180
+ activityLogText(raw) {
2181
+ // 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
2182
+ const item = raw?.item && typeof raw.item === 'object' ? raw.item : raw;
2183
+ if (typeof item?.text === 'string' && item.text)
2184
+ return item.text;
2185
+ if (item?.kind === 'tool_call')
2186
+ return `[tool_call] ${item.name ?? ''}`.trim();
2187
+ if (item?.kind === 'tool_result')
2188
+ return `[tool_result] ${item.name ?? ''} ${item.ok === false ? 'failed' : 'ok'}`.trim();
2189
+ if (item?.kind)
2190
+ return `[activity:${item.kind}]`;
2191
+ return '[activity]';
2192
+ }
2193
+ buildActivityPayload(envelope, context, item) {
2194
+ const activityItem = item && typeof item === 'object' && !Array.isArray(item)
2195
+ ? { ...item }
2196
+ : { kind: 'unknown', text: String(item ?? '') };
2197
+ return {
2198
+ ...this.buildTaskPayloadBase(envelope, context),
2199
+ type: 'activity',
2200
+ item: activityItem,
2201
+ };
2202
+ }
2203
+ async sendReliableStructured(channelId, payload, context, logText) {
2204
+ await this.sendContentPayload(channelId, payload, {
2205
+ contentKind: 'custom',
2206
+ context,
2207
+ logText: logText ?? this.payloadLogText(payload, 'custom'),
2208
+ });
2209
+ }
1788
2210
  async sendMessage(channelId, text, context) {
1789
2211
  if (!text?.trim()) {
1790
2212
  logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
@@ -1817,7 +2239,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1817
2239
  return;
1818
2240
  }
1819
2241
  // Attempt immediate delivery
1820
- const ok = await this.deliverTextEntry(entry);
2242
+ const ok = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
1821
2243
  if (ok) {
1822
2244
  outbox.remove(this.config.aid, entry.id);
1823
2245
  }
@@ -1899,19 +2321,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1899
2321
  if (!mid) {
1900
2322
  const dispatchStatus = result?.message_dispatch?.status;
1901
2323
  if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
1902
- logger.info(`${this.logPrefix()} group.send ok (${dispatchStatus}): group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
2324
+ logger.warn(`${this.logPrefix()} group.send returned ${dispatchStatus} without message_id; keeping outbox entry: group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1903
2325
  }
1904
2326
  else {
1905
2327
  logger.warn(`${this.logPrefix()} group.send returned no message_id: ${JSON.stringify(result)}`);
1906
2328
  }
2329
+ return false;
1907
2330
  }
1908
2331
  else {
1909
2332
  logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1910
2333
  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 });
1911
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
2334
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
1912
2335
  this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1913
- // Observer forward: outbound (group) — 转发实际发出的明文 payload
1914
- this.forwardOutbound(channelId, payload);
2336
+ // Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload
2337
+ this.forwardOutbound(result);
1915
2338
  }
1916
2339
  }
1917
2340
  else {
@@ -1919,14 +2342,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1919
2342
  const result = await this.callAndTrace('message.send', params);
1920
2343
  if (!result || !result.message_id) {
1921
2344
  logger.warn(`${this.logPrefix()} message.send returned no message_id: ${JSON.stringify(result)}`);
2345
+ return false;
1922
2346
  }
1923
2347
  else {
1924
2348
  logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1925
2349
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
1926
- this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
2350
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
1927
2351
  this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1928
- // Observer forward: outbound (private) — 转发实际发出的明文 payload
1929
- this.forwardOutbound(targetAid, payload);
2352
+ // Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload
2353
+ this.forwardOutbound(result);
1930
2354
  }
1931
2355
  }
1932
2356
  return true;
@@ -1940,20 +2364,30 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1940
2364
  if (isGroup) {
1941
2365
  this.trace('OUT', 'group.send.fallback', params);
1942
2366
  const result = await this.client.call('group.send', params);
1943
- this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message?.message_id ?? result?.message_id });
1944
- if (!result || !result.message_id) {
2367
+ const mid = this.messageIdFromSendResult(result);
2368
+ this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
2369
+ if (!mid) {
1945
2370
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
2371
+ return false;
1946
2372
  }
1947
- this.forwardOutbound(channelId, payload);
2373
+ 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 });
2374
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode);
2375
+ this.appendOutboundJsonl(channelId, finalText, mid, false, context, true, 'text', source);
2376
+ this.forwardOutbound(result);
1948
2377
  }
1949
2378
  else {
1950
2379
  this.trace('OUT', 'message.send.fallback', params);
1951
2380
  const result = await this.client.call('message.send', params);
1952
- this.trace('OUT', 'message.send.fallback.ok', { message_id: result?.message_id });
1953
- if (!result || !result.message_id) {
2381
+ const mid = result?.message_id;
2382
+ this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
2383
+ if (!result || !mid) {
1954
2384
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
2385
+ return false;
1955
2386
  }
1956
- this.forwardOutbound(targetAid, payload);
2387
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: mid, kind: 'text', len: finalText.length });
2388
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode);
2389
+ this.appendOutboundJsonl(targetAid, finalText, mid, false, context, false, 'text', source);
2390
+ this.forwardOutbound(result);
1957
2391
  }
1958
2392
  return true;
1959
2393
  }
@@ -1970,6 +2404,26 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1970
2404
  }
1971
2405
  }
1972
2406
  }
2407
+ async deliverPayloadEntry(entry) {
2408
+ const channelId = entry.channelId;
2409
+ const payload = entry.payload;
2410
+ if (!payload) {
2411
+ logger.warn(`${this.logPrefix()} deliverPayloadEntry: missing payload (outbox id=${entry.id})`);
2412
+ return { ok: true };
2413
+ }
2414
+ const contentKind = entry.contentKind;
2415
+ const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
2416
+ const context = entry.context;
2417
+ 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}`);
2418
+ const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
2419
+ if (!sent.ok || !sent.messageId)
2420
+ return sent;
2421
+ const isGroup = this.isGroupId(channelId);
2422
+ logger.info(`${this.logPrefix()} durable payload sent: kind=${contentKind ?? payload.type ?? 'payload'} target=${isGroup ? channelId : this.peerLabel(channelId)} mid=${sent.messageId} encrypt=${sent.encrypt} text=${logText.slice(0, 60)}`);
2423
+ this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result);
2424
+ this.runPostSend(entry, sent.messageId);
2425
+ return sent;
2426
+ }
1973
2427
  /** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
1974
2428
  appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
1975
2429
  try {
@@ -2022,10 +2476,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2022
2476
  encrypt,
2023
2477
  };
2024
2478
  try {
2025
- const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
2026
- const stage = payload?.stage ?? `items=${itemCount}`;
2027
- // 提取 thought 文本(只对 kind=text 的 item 写 jsonl,过滤 tool_use/tool_result 等结构化项)
2028
2479
  const items = payload?.items;
2480
+ const itemCount = Array.isArray(items) ? items.length : 1;
2481
+ const stage = payload?.stage ?? (payload?.kind ? `kind=${payload.kind}` : `items=${itemCount}`);
2482
+ // 提取 thought 文本:兼容旧 items[] 和新扁平 activity payload。
2029
2483
  let thoughtText;
2030
2484
  if (Array.isArray(items) && items.length > 0) {
2031
2485
  const lastItem = items[items.length - 1];
@@ -2043,15 +2497,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2043
2497
  thoughtText = lastItem;
2044
2498
  }
2045
2499
  }
2500
+ else {
2501
+ thoughtText = this.activityLogText(payload);
2502
+ }
2046
2503
  if (this.isGroupId(channelId)) {
2047
2504
  params.group_id = targetId;
2048
2505
  const putRes = await this.callAndTrace('group.thought.put', params);
2049
2506
  const tid = putRes?.thought_id;
2050
2507
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
2051
2508
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2052
- this.forwardOutbound(channelId, payload);
2509
+ this.forwardOutbound(putRes);
2053
2510
  if (thoughtText) {
2054
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
2511
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive', 'thought');
2055
2512
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
2056
2513
  }
2057
2514
  }
@@ -2061,9 +2518,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2061
2518
  const tid = putRes?.thought_id;
2062
2519
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
2063
2520
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2064
- this.forwardOutbound(channelId, payload);
2521
+ this.forwardOutbound(putRes);
2065
2522
  if (thoughtText) {
2066
- this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
2523
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive', 'thought');
2067
2524
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
2068
2525
  }
2069
2526
  }
@@ -2098,14 +2555,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2098
2555
  const result = await this.callAndTrace('group.send', params);
2099
2556
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
2100
2557
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
2101
- this.forwardOutbound(channelId, finalPayload);
2558
+ this.forwardOutbound(result);
2102
2559
  return mid;
2103
2560
  }
2104
2561
  else {
2105
2562
  params.to = targetAid;
2106
2563
  const result = await this.callAndTrace('message.send', params);
2107
2564
  logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
2108
- this.forwardOutbound(targetAid, finalPayload);
2565
+ this.forwardOutbound(result);
2109
2566
  return result?.message_id ?? null;
2110
2567
  }
2111
2568
  }
@@ -2145,7 +2602,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2145
2602
  }
2146
2603
  return;
2147
2604
  }
2148
- const ok = await this.deliverFileEntry(entry);
2605
+ const ok = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
2149
2606
  if (ok) {
2150
2607
  outbox.remove(this.config.aid, entry.id);
2151
2608
  }
@@ -2215,6 +2672,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2215
2672
  filePayload.task_id = context.metadata.taskId;
2216
2673
  if (context?.metadata?.chatmode)
2217
2674
  filePayload.chatmode = context.metadata.chatmode;
2675
+ // file-link-cache: 回带点击请求的 correlationId,客户端用它把异步到达的文件消息对回这次 fetch 点击
2676
+ if (context?.metadata?.correlationId)
2677
+ filePayload.correlation_id = context.metadata.correlationId;
2218
2678
  const isGroup = this.isGroupId(channelId);
2219
2679
  const fileTargetAid = channelId;
2220
2680
  const encryptTarget = isGroup ? channelId : fileTargetAid;
@@ -2222,24 +2682,32 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2222
2682
  ? !!(context.metadata.encrypted)
2223
2683
  : this.shouldEncrypt(encryptTarget);
2224
2684
  const params = { payload: filePayload, encrypt };
2685
+ let sendResult = null;
2686
+ let sentMid = null;
2225
2687
  try {
2226
2688
  if (isGroup) {
2227
2689
  params.group_id = channelId;
2228
2690
  this.trace('OUT', 'group.send.file', params);
2229
2691
  const result = await this.client.call('group.send', params);
2692
+ sendResult = result;
2230
2693
  const fileMid = result?.message?.message_id ?? result?.message_id;
2694
+ sentMid = fileMid ?? null;
2231
2695
  this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
2232
2696
  if (!fileMid) {
2233
2697
  logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
2698
+ return false;
2234
2699
  }
2235
2700
  }
2236
2701
  else {
2237
2702
  params.to = fileTargetAid;
2238
2703
  this.trace('OUT', 'message.send.file', params);
2239
2704
  const result = await this.client.call('message.send', params);
2240
- this.trace('OUT', 'message.send.file.ok', { message_id: result?.message_id });
2241
- if (!result || !result.message_id) {
2705
+ sendResult = result;
2706
+ sentMid = result?.message_id ?? null;
2707
+ this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
2708
+ if (!result || !sentMid) {
2242
2709
  logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
2710
+ return false;
2243
2711
  }
2244
2712
  }
2245
2713
  }
@@ -2255,18 +2723,24 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2255
2723
  if (isGroup) {
2256
2724
  this.trace('OUT', 'group.send.file.fallback', params);
2257
2725
  const result = await this.client.call('group.send', params);
2726
+ sendResult = result;
2258
2727
  const fbMid = result?.message?.message_id ?? result?.message_id;
2728
+ sentMid = fbMid ?? null;
2259
2729
  this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
2260
2730
  if (!fbMid) {
2261
2731
  logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
2732
+ return false;
2262
2733
  }
2263
2734
  }
2264
2735
  else {
2265
2736
  this.trace('OUT', 'message.send.file.fallback', params);
2266
2737
  const result = await this.client.call('message.send', params);
2267
- this.trace('OUT', 'message.send.file.fallback.ok', { message_id: result?.message_id });
2268
- if (!result || !result.message_id) {
2738
+ sendResult = result;
2739
+ sentMid = result?.message_id ?? null;
2740
+ this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
2741
+ if (!result || !sentMid) {
2269
2742
  logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
2743
+ return false;
2270
2744
  }
2271
2745
  }
2272
2746
  }
@@ -2275,7 +2749,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2275
2749
  }
2276
2750
  }
2277
2751
  logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
2278
- this.forwardOutbound(channelId, filePayload);
2752
+ if (sentMid) {
2753
+ const fileText = filePayload.text;
2754
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
2755
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode);
2756
+ const source = context?.metadata?.source ?? 'daemon';
2757
+ this.appendOutboundJsonl(channelId, fileText, sentMid, !!params.encrypt, context, isGroup, 'text', source);
2758
+ }
2759
+ if (sendResult)
2760
+ this.forwardOutbound(sendResult);
2279
2761
  return true;
2280
2762
  }
2281
2763
  catch (e) {
@@ -2309,10 +2791,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2309
2791
  logger.info(`${this.logPrefix()} Draining outbox...`);
2310
2792
  const result = await outbox.drain(this.config.aid, async (entry) => {
2311
2793
  if (entry.type === 'text') {
2312
- return this.deliverTextEntry(entry);
2794
+ return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
2313
2795
  }
2314
2796
  else if (entry.type === 'file') {
2315
- return this.deliverFileEntry(entry);
2797
+ return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
2798
+ }
2799
+ else if (entry.type === 'payload') {
2800
+ const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
2801
+ return sent.ok;
2316
2802
  }
2317
2803
  return true; // unknown type, discard
2318
2804
  });
@@ -2341,58 +2827,44 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2341
2827
  progress: 'progress',
2342
2828
  };
2343
2829
  const statusPayload = {
2344
- type: 'status',
2345
- state: stateMap[status] ?? status,
2830
+ type: 'task.status',
2831
+ status: stateMap[status] ?? status,
2346
2832
  task_id: taskId,
2347
2833
  session_id: sessionId,
2348
2834
  severity,
2835
+ terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
2349
2836
  ...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
2350
2837
  };
2351
2838
  if (context?.threadId)
2352
2839
  statusPayload.thread_id = context.threadId;
2353
2840
  if (context?.peerId)
2354
2841
  statusPayload.initiator = context.peerId;
2842
+ if (context?.metadata?.chatmode)
2843
+ statusPayload.chatmode = context.metadata.chatmode;
2355
2844
  if (context?.replyToMessageId)
2356
2845
  statusPayload.ref_message_id = context.replyToMessageId;
2357
- const isGroup = this.isGroupId(channelId);
2358
- // 私聊 channelId = 对端 AID(不含 device_id)
2359
- const statusTargetAid = channelId;
2360
- const encryptTarget = isGroup ? channelId : statusTargetAid;
2361
- const computeEncrypt = () => context?.metadata?.encrypted != null
2362
- ? !!(context.metadata.encrypted)
2363
- : this.shouldEncrypt(encryptTarget);
2364
- const sendOne = (method, payload, label) => {
2365
- const c = this.client;
2366
- if (!c) {
2367
- logger.debug(`${this.logPrefix()} ${label} skipped: client gone`);
2368
- return Promise.resolve();
2369
- }
2370
- const encrypt = computeEncrypt();
2371
- const params = { payload, encrypt };
2372
- if (isGroup)
2373
- params.group_id = channelId;
2374
- else
2375
- params.to = statusTargetAid;
2376
- this.trace('OUT', `${method}.task_${label}`, params);
2377
- return c.call(method, params).catch(e => {
2378
- if (encrypt && e instanceof E2EEError) {
2379
- this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
2380
- logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
2381
- const c2 = this.client;
2382
- if (!c2)
2383
- return;
2384
- const fallbackParams = { ...params, encrypt: false };
2385
- return c2.call(method, fallbackParams).catch(e2 => {
2386
- logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
2387
- });
2388
- }
2389
- logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
2390
- });
2391
- };
2392
- const method = isGroup ? 'group.send' : 'message.send';
2393
- sendOne(method, statusPayload, 'status');
2394
- this.forwardOutbound(channelId, statusPayload);
2395
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
2846
+ const notifyOptions = { ttlMs: 60_000 };
2847
+ if (this.isGroupId(channelId))
2848
+ notifyOptions.groupId = channelId;
2849
+ else
2850
+ notifyOptions.to = channelId;
2851
+ this.trace('OUT', 'notify.task_status', {
2852
+ method: 'event/app.task.status',
2853
+ params: statusPayload,
2854
+ options: notifyOptions,
2855
+ });
2856
+ const notify = this.client.notify;
2857
+ if (typeof notify !== 'function') {
2858
+ logger.warn(`${this.logPrefix()} task.${status} notify skipped: client.notify unavailable`);
2859
+ return;
2860
+ }
2861
+ Promise.resolve(notify.call(this.client, 'event/app.task.status', statusPayload, notifyOptions)).then(() => {
2862
+ this.trace('OUT', 'notify.task_status.ok', { task_id: taskId, status: statusPayload.status });
2863
+ }).catch((e) => {
2864
+ this.trace('OUT', 'notify.task_status.error', { task_id: taskId, status: statusPayload.status, error: String(e) });
2865
+ logger.debug(`${this.logPrefix()} task_status notify failed: ${e}`);
2866
+ }).catch(() => { });
2867
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
2396
2868
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
2397
2869
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
2398
2870
  const chatmode = context?.metadata?.chatmode ?? '?';
@@ -2615,251 +3087,290 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2615
3087
  return undefined; // 不写缓存,下次仍可重试
2616
3088
  }
2617
3089
  }
3090
+ /**
3091
+ * 查询某成员在群里的角色(经 group.get_admins)。
3092
+ * 仅用于话题创建权限校验(稀有事件),故不缓存:每次查权威源,结果天然最新。
3093
+ * 命中 admins 列表(owner+admin)返回其 role;不在列表(含普通 member / observer)返回 'none';
3094
+ * 未连接 / 异常返回 undefined —— 绝不抛出,由调用方按 fail-closed 处理。
3095
+ */
3096
+ async getGroupMemberRole(groupId, aid) {
3097
+ if (!groupId || !aid)
3098
+ return undefined;
3099
+ if (!this.client)
3100
+ return undefined;
3101
+ try {
3102
+ const result = await this.callAndTrace('group.get_admins', { group_id: groupId });
3103
+ const admins = Array.isArray(result?.admins) ? result.admins : [];
3104
+ const hit = admins.find((m) => m?.aid === aid);
3105
+ if (hit) {
3106
+ const role = hit.role;
3107
+ return role === 'owner' || role === 'admin' ? role : 'admin';
3108
+ }
3109
+ return 'none'; // 不在 owner/admin 列表 → 普通 member / observer / 非成员
3110
+ }
3111
+ catch {
3112
+ return undefined; // 查询失败,调用方 fail-closed
3113
+ }
3114
+ }
2618
3115
  }
2619
3116
  // Plugin implementation
2620
3117
  export class AUNChannelPlugin {
2621
3118
  name = 'aun';
2622
- isEnabled(config) {
2623
- const raw = config.channels?.aun;
2624
- if (!raw)
2625
- return false;
2626
- if (Array.isArray(raw)) {
2627
- return raw.some(inst => inst.enabled !== false && !!inst.aid);
2628
- }
2629
- return raw.enabled !== false && !!raw.aid;
2630
- }
2631
- async createChannels(config) {
2632
- const instances = normalizeChannelInstances(config.channels?.aun, 'aun');
2633
- const result = [];
2634
- for (const inst of instances) {
2635
- if (inst.enabled === false || !inst.aid)
2636
- continue;
2637
- const channel = new AUNChannel({
2638
- aid: inst.aid,
2639
- keystorePath: inst.keystorePath,
2640
- gatewayUrl: inst.gatewayUrl,
2641
- accessToken: inst.accessToken,
2642
- flushDelay: inst.flushDelay,
2643
- owner: inst.owner,
2644
- agentName: inst.agentName,
2645
- channelName: inst.name,
2646
- aunTrace: config.debug?.aunTrace,
2647
- aunSdkLog: config.debug?.aunSdkLog,
2648
- });
2649
- const adapter = {
2650
- channelName: inst.name,
2651
- channelKey: inst.name, // channelName 实际上就是 channelKey
2652
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
2653
- send: async (envelope, payload) => {
2654
- const ctx = envelope.replyContext;
2655
- const channelId = envelope.channelId;
2656
- switch (payload.kind) {
2657
- case 'result.text':
2658
- case 'command.result':
2659
- case 'command.error':
2660
- case 'system.notice':
2661
- case 'system.error':
2662
- case 'result.error': {
2663
- const sendCtx = { ...(ctx ?? {}) };
2664
- if (payload.kind === 'result.text' && payload.isFinal)
2665
- sendCtx.title = '✅ 最终回复:';
2666
- await channel.sendMessage(channelId, payload.text, sendCtx);
2667
- return;
2668
- }
2669
- case 'result.file':
2670
- await channel.sendFile(channelId, payload.filePath, ctx);
2671
- return;
2672
- case 'result.image': {
2673
- // AUN 支持 image,走 sendStructured 发 type=image payload
2674
- const buf = payload.data;
2675
- const b64 = buf.toString('base64');
2676
- await channel.sendStructured(channelId, {
2677
- type: 'image',
2678
- alt: payload.alt,
2679
- data_base64: b64,
2680
- mime_type: payload.mimeType,
2681
- }, ctx);
2682
- return;
2683
- }
2684
- case 'activity.batch': {
2685
- const aunPayload = {
2686
- type: 'thought',
2687
- items: payload.items,
2688
- client_context: { task_id: envelope.taskId, chatmode: envelope.chatmode, agent_name: envelope.agentName },
2689
- };
2690
- if (ctx?.threadId)
2691
- aunPayload.thread_id = ctx.threadId;
3119
+ async createInstance(inst, ctx) {
3120
+ // AUN aid is the agent's own AID; loader injects it as inst.aid, ctx.agentName is the source of truth.
3121
+ const aid = inst.aid ?? ctx.agentName;
3122
+ if (inst.enabled === false || !aid)
3123
+ return null;
3124
+ const channel = new AUNChannel({
3125
+ aid,
3126
+ keystorePath: inst.keystorePath,
3127
+ gatewayUrl: inst.gatewayUrl,
3128
+ accessToken: inst.accessToken,
3129
+ flushDelay: inst.flushDelay,
3130
+ owner: inst.owner ?? inst.owners?.[0],
3131
+ agentName: ctx.agentName,
3132
+ channelName: inst.name,
3133
+ aunTrace: ctx.debug?.aunTrace,
3134
+ aunSdkLog: ctx.debug?.aunSdkLog,
3135
+ });
3136
+ const mode = resolveShowActivities(inst);
3137
+ const adapter = {
3138
+ channelName: inst.name,
3139
+ channelKey: inst.name, // channelName 实际上就是 channelKey
3140
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
3141
+ send: async (envelope, payload) => {
3142
+ const replyCtx = envelope.replyContext;
3143
+ const channelId = envelope.channelId;
3144
+ const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
3145
+ switch (payload.kind) {
3146
+ case 'result.text':
3147
+ case 'command.result':
3148
+ case 'command.error': {
3149
+ const sendCtx = { ...(replyCtx ?? {}) };
3150
+ if (payload.kind === 'result.text' && payload.isFinal)
3151
+ sendCtx.title = '✅ 最终回复:';
3152
+ await channel.sendMessage(channelId, payload.text, sendCtx);
3153
+ return;
3154
+ }
3155
+ case 'system.notice': {
3156
+ await channel.sendReliableStructured(channelId, {
3157
+ type: 'notice',
3158
+ ...taskBase(),
3159
+ subtype: payload.subtype,
3160
+ text: payload.text,
3161
+ severity: 'info',
3162
+ }, replyCtx, payload.text);
3163
+ return;
3164
+ }
3165
+ case 'system.error': {
3166
+ await channel.sendReliableStructured(channelId, {
3167
+ type: 'error',
3168
+ ...taskBase(),
3169
+ subtype: payload.subtype,
3170
+ message: payload.text,
3171
+ user_message: payload.text,
3172
+ recoverable: payload.recoverable,
3173
+ terminal: !payload.recoverable,
3174
+ }, replyCtx, payload.text);
3175
+ return;
3176
+ }
3177
+ case 'result.error': {
3178
+ await channel.sendReliableStructured(channelId, {
3179
+ type: 'error',
3180
+ ...taskBase(),
3181
+ reason: payload.reason,
3182
+ message: payload.text,
3183
+ user_message: payload.text,
3184
+ terminal: true,
3185
+ }, replyCtx, payload.text);
3186
+ return;
3187
+ }
3188
+ case 'result.file': {
3189
+ const fileCtx = payload.correlationId
3190
+ ? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
3191
+ : replyCtx;
3192
+ await channel.sendFile(channelId, payload.filePath, fileCtx);
3193
+ return;
3194
+ }
3195
+ case 'result.image': {
3196
+ const buf = payload.data;
3197
+ const b64 = buf.toString('base64');
3198
+ await channel.sendContentPayload(channelId, {
3199
+ type: 'image', alt: payload.alt, data_base64: b64, mime_type: payload.mimeType,
3200
+ }, {
3201
+ contentKind: 'image',
3202
+ context: replyCtx,
3203
+ logText: payload.alt ? `[image] ${payload.alt}` : '[image]',
3204
+ });
3205
+ return;
3206
+ }
3207
+ case 'activity.batch': {
3208
+ const items = Array.isArray(payload.items) ? payload.items : [];
3209
+ for (const item of items) {
3210
+ if (item?.kind === 'progress') {
3211
+ channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, {
3212
+ activityType: 'progress',
3213
+ text: item.text,
3214
+ state: item.state,
3215
+ toolUses: item.tool_uses,
3216
+ durationMs: item.duration_ms,
3217
+ });
3218
+ continue;
3219
+ }
3220
+ const aunPayload = channel.buildActivityPayload(envelope, replyCtx, item);
2692
3221
  if (envelope.chatmode === 'proactive') {
2693
- await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
3222
+ await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
2694
3223
  }
2695
3224
  else {
2696
- // interactive 模式不发 thought.put,只写入消息历史
2697
- await channel.sendStructured(channelId, aunPayload, ctx);
3225
+ await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
2698
3226
  }
2699
- return;
2700
3227
  }
2701
- case 'status.progress':
2702
- channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2703
- return;
2704
- case 'status.started':
2705
- channel.sendProcessingStatus(channelId, 'start', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2706
- return;
2707
- case 'status.queued':
2708
- channel.sendProcessingStatus(channelId, 'queued', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2709
- return;
2710
- case 'status.completed':
2711
- channel.sendProcessingStatus(channelId, 'done', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2712
- return;
2713
- case 'status.interrupted':
2714
- channel.sendProcessingStatus(channelId, 'interrupted', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2715
- return;
2716
- case 'status.error':
2717
- channel.sendProcessingStatus(channelId, 'error', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2718
- return;
2719
- case 'status.timeout':
2720
- channel.sendProcessingStatus(channelId, 'timeout', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2721
- return;
2722
- case 'interaction': {
2723
- const req = payload.interaction;
2724
- if (req.kind.kind === 'action') {
2725
- const action = req.kind;
2726
- const aunCard = {
2727
- type: 'action_card',
2728
- title: action.title,
2729
- actions: action.buttons.map(btn => ({
2730
- label: btn.label,
2731
- value: btn.key,
2732
- style: btn.style ?? 'default',
2733
- behavior: 'reply',
2734
- })),
2735
- };
2736
- if (action.body)
2737
- aunCard.description = action.body;
2738
- if (ctx?.threadId)
2739
- aunCard.thread_id = ctx.threadId;
2740
- const msgId = await channel.sendStructured(channelId, aunCard, ctx);
2741
- if (msgId) {
2742
- channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: false });
2743
- setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2744
- }
2745
- }
2746
- else if (req.kind.kind === 'command-card') {
2747
- const card = req.kind;
2748
- const aunCard = {
2749
- type: 'action_card',
2750
- title: card.title,
2751
- actions: card.buttons.map(btn => ({
2752
- label: btn.label,
2753
- value: btn.command,
2754
- style: btn.style ?? 'default',
2755
- behavior: 'reply',
2756
- })),
2757
- };
2758
- if (card.body)
2759
- aunCard.description = card.body;
2760
- if (ctx?.threadId)
2761
- aunCard.thread_id = ctx.threadId;
2762
- const msgId = await channel.sendStructured(channelId, aunCard, ctx);
2763
- if (msgId) {
2764
- channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
2765
- setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2766
- }
2767
- }
2768
- else if (payload.fallbackText) {
2769
- await channel.sendMessage(channelId, payload.fallbackText, ctx);
2770
- }
2771
- return;
3228
+ return;
3229
+ }
3230
+ case 'status.progress':
3231
+ channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3232
+ return;
3233
+ case 'status.started':
3234
+ channel.sendProcessingStatus(channelId, 'start', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3235
+ return;
3236
+ case 'status.queued':
3237
+ channel.sendProcessingStatus(channelId, 'queued', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3238
+ return;
3239
+ case 'status.completed':
3240
+ channel.sendProcessingStatus(channelId, 'done', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3241
+ return;
3242
+ case 'status.interrupted':
3243
+ channel.sendProcessingStatus(channelId, 'interrupted', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3244
+ return;
3245
+ case 'status.error':
3246
+ channel.sendProcessingStatus(channelId, 'error', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3247
+ return;
3248
+ case 'status.timeout':
3249
+ channel.sendProcessingStatus(channelId, 'timeout', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
3250
+ return;
3251
+ case 'interaction': {
3252
+ const req = payload.interaction;
3253
+ if (req.kind.kind === 'action') {
3254
+ const action = req.kind;
3255
+ const aunCard = {
3256
+ type: 'action_card',
3257
+ title: action.title,
3258
+ actions: action.buttons.map(btn => ({
3259
+ label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
3260
+ })),
3261
+ };
3262
+ if (action.body)
3263
+ aunCard.description = action.body;
3264
+ if (req.initiatorId && channel.isGroupId(channelId))
3265
+ aunCard.initiator = req.initiatorId;
3266
+ if (replyCtx?.threadId)
3267
+ aunCard.thread_id = replyCtx.threadId;
3268
+ await channel.sendContentPayload(channelId, aunCard, {
3269
+ contentKind: 'card',
3270
+ context: replyCtx,
3271
+ logText: action.title ? `[card] ${action.title}` : '[card]',
3272
+ postSend: {
3273
+ type: 'register_interaction_card',
3274
+ requestId: req.id,
3275
+ isCommandCard: false,
3276
+ initiatorAid: req.initiatorId,
3277
+ expiresAt: Date.now() + 20 * 60 * 1000,
3278
+ },
3279
+ });
2772
3280
  }
2773
- case 'custom': {
2774
- const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
2775
- channel.sendCustomPayload(channelId, text);
2776
- return;
3281
+ else if (req.kind.kind === 'command-card') {
3282
+ const card = req.kind;
3283
+ const aunCard = {
3284
+ type: 'action_card',
3285
+ title: card.title,
3286
+ actions: card.buttons.map(btn => ({
3287
+ label: btn.label, value: btn.command, style: btn.style ?? 'default', behavior: 'reply', disabled: btn.disabled || undefined,
3288
+ })),
3289
+ };
3290
+ if (card.body)
3291
+ aunCard.description = card.body;
3292
+ if (replyCtx?.threadId)
3293
+ aunCard.thread_id = replyCtx.threadId;
3294
+ await channel.sendContentPayload(channelId, aunCard, {
3295
+ contentKind: 'card',
3296
+ context: replyCtx,
3297
+ logText: card.title ? `[card] ${card.title}` : '[card]',
3298
+ postSend: {
3299
+ type: 'register_interaction_card',
3300
+ requestId: req.id,
3301
+ isCommandCard: true,
3302
+ initiatorAid: req.initiatorId,
3303
+ expiresAt: Date.now() + 20 * 60 * 1000,
3304
+ },
3305
+ });
3306
+ }
3307
+ else if (payload.fallbackText) {
3308
+ await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
2777
3309
  }
2778
- default:
2779
- logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
3310
+ return;
2780
3311
  }
2781
- }, acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); }, onInteraction: (cb) => { channel.interactionCallback = cb; }, uploadAgentMd: (content) => channel.uploadAgentMd(content),
2782
- downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
2783
- getGroupName: (groupId) => channel.getGroupName(groupId), _selfAid: () => channel.getStatus().aid,
2784
- _selfName: () => channel.getSelfName(),
2785
- };
2786
- const policy = {
2787
- canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
2788
- canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
2789
- canCreateSession: (chatType, identity) => true,
2790
- canDeleteSession: (chatType, identity) => true,
2791
- canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
2792
- messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
2793
- showMiddleResult: (chatType, identity) => {
2794
- const mode = getChannelShowActivities(config, inst.name);
2795
- if (mode === 'none')
2796
- return false;
2797
- if (mode === 'dm-only')
2798
- return chatType === 'private';
2799
- if (mode === 'owner-dm-only')
2800
- return chatType === 'private' && identity === 'owner';
2801
- return true;
2802
- },
2803
- showIdleMonitor: (chatType, identity) => {
2804
- const mode = getChannelShowActivities(config, inst.name);
2805
- if (mode === 'none')
2806
- return false;
2807
- if (mode === 'dm-only')
2808
- return chatType === 'private';
2809
- if (mode === 'owner-dm-only')
2810
- return chatType === 'private' && identity === 'owner';
2811
- return true;
2812
- },
2813
- accumulateErrors: (chatType, identity) => true,
2814
- };
2815
- const options = {
2816
- flushDelay: inst.flushDelay ?? 3,
2817
- fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
2818
- };
2819
- result.push({
2820
- channelType: 'aun',
2821
- adapter,
2822
- channel,
2823
- policy,
2824
- options,
2825
- connect: () => channel.connect(),
2826
- disconnect: () => channel.disconnect(),
2827
- onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
2828
- registerBridge(bridge, channelType) {
2829
- bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
2830
- handler(aunOptsToInbound(opts, adapter.channelName, channelType));
2831
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2832
- },
2833
- registerHooks(ctx) {
2834
- channel.setEventBus(ctx.eventBus);
2835
- if (channel.setOnChannelDown) {
2836
- channel.setOnChannelDown(() => {
2837
- ctx.eventBus.publish({
2838
- type: 'channel:error',
2839
- channel: 'aun',
2840
- channelName: adapter.channelName,
2841
- status: 'auth_error',
2842
- message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
2843
- timestamp: Date.now(),
2844
- });
2845
- });
3312
+ case 'custom': {
3313
+ const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
3314
+ channel.sendCustomPayload(channelId, text);
3315
+ return;
2846
3316
  }
2847
- if (typeof channel.setDispatchModeResolver === 'function') {
2848
- channel.setDispatchModeResolver(async (channelId) => {
2849
- const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);
2850
- return session?.metadata?.dispatchMode;
3317
+ default:
3318
+ logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
3319
+ }
3320
+ },
3321
+ acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
3322
+ onInteraction: (cb) => { channel.interactionCallback = cb; },
3323
+ uploadAgentMd: (content) => channel.uploadAgentMd(content),
3324
+ downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
3325
+ getGroupName: (groupId) => channel.getGroupName(groupId),
3326
+ getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
3327
+ _selfAid: () => channel.getStatus().aid,
3328
+ _selfName: () => channel.getSelfName(),
3329
+ };
3330
+ const policy = {
3331
+ canSwitchProject: (_, identity) => identity === 'owner' || identity === 'admin',
3332
+ canListProjects: (_, identity) => identity === 'owner' || identity === 'admin',
3333
+ canCreateSession: () => true,
3334
+ canDeleteSession: () => true,
3335
+ canImportCliSession: (_, identity) => identity === 'owner' || identity === 'admin',
3336
+ messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
3337
+ showMiddleResult: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
3338
+ showIdleMonitor: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
3339
+ accumulateErrors: () => true,
3340
+ };
3341
+ return {
3342
+ channelType: 'aun', adapter, channel,
3343
+ policy,
3344
+ options: { flushDelay: inst.flushDelay ?? 3, fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g },
3345
+ connect: () => channel.connect(),
3346
+ disconnect: () => channel.disconnect(),
3347
+ onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
3348
+ registerBridge(bridge, channelType) {
3349
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
3350
+ handler(aunOptsToInbound(opts, adapter.channelName, channelType));
3351
+ }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
3352
+ },
3353
+ registerHooks(hookCtx) {
3354
+ channel.setEventBus(hookCtx.eventBus);
3355
+ if (channel.setOnChannelDown) {
3356
+ channel.setOnChannelDown(() => {
3357
+ hookCtx.eventBus.publish({
3358
+ type: 'channel:error',
3359
+ channel: 'aun',
3360
+ channelName: adapter.channelName,
3361
+ status: 'auth_error',
3362
+ message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
3363
+ timestamp: Date.now(),
2851
3364
  });
2852
- }
2853
- },
2854
- });
2855
- }
2856
- return result;
2857
- }
2858
- async createChannel(config) {
2859
- const instances = await this.createChannels(config);
2860
- if (instances.length === 0) {
2861
- throw new Error('AUN config missing (aid required, e.g. "mybot.agentid.pub")');
2862
- }
2863
- return instances[0];
3365
+ });
3366
+ }
3367
+ if (typeof channel.setDispatchModeResolver === 'function') {
3368
+ channel.setDispatchModeResolver(async (channelId) => {
3369
+ const session = await hookCtx.sessionManager.getActiveSession(adapter.channelName, channelId);
3370
+ return session?.metadata?.dispatchModeOverride;
3371
+ });
3372
+ }
3373
+ },
3374
+ };
2864
3375
  }
2865
3376
  }