evolclaw 3.1.11 → 3.3.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 (89) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +27 -2
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -27
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1069 -141
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +28 -0
  9. package/dist/aun/aid/control-aid.js +67 -0
  10. package/dist/aun/aid/identity.js +20 -7
  11. package/dist/aun/aid/store.js +2 -2
  12. package/dist/aun/storage/download.js +1 -1
  13. package/dist/aun/storage/upload.js +13 -1
  14. package/dist/channels/aun.js +538 -325
  15. package/dist/channels/dingtalk.js +77 -140
  16. package/dist/channels/feishu.js +98 -151
  17. package/dist/channels/qqbot.js +75 -138
  18. package/dist/channels/wechat.js +75 -136
  19. package/dist/channels/wecom.js +75 -138
  20. package/dist/cli/agent.js +44 -13
  21. package/dist/cli/index.js +207 -46
  22. package/dist/cli/init-channel.js +38 -148
  23. package/dist/cli/init.js +192 -85
  24. package/dist/cli/model.js +1 -1
  25. package/dist/cli/stats.js +558 -0
  26. package/dist/cli/version.js +87 -0
  27. package/dist/cli/watch-msg.js +5 -2
  28. package/dist/config-store.js +48 -11
  29. package/dist/core/channel-loader.js +84 -82
  30. package/dist/core/command-handler.js +754 -172
  31. package/dist/core/daemon-file-cache.js +216 -0
  32. package/dist/core/evolagent-registry.js +4 -0
  33. package/dist/core/evolagent.js +28 -23
  34. package/dist/core/interaction-router.js +8 -0
  35. package/dist/core/message/command-handler-agent-control.js +215 -0
  36. package/dist/core/message/create-status.js +67 -0
  37. package/dist/core/message/im-renderer.js +35 -13
  38. package/dist/core/message/items-formatter.js +9 -1
  39. package/dist/core/message/message-bridge.js +52 -22
  40. package/dist/core/message/message-log.js +1 -0
  41. package/dist/core/message/message-processor.js +336 -68
  42. package/dist/core/message/message-queue.js +15 -8
  43. package/dist/core/message/pending-hints.js +232 -0
  44. package/dist/core/message/response-depth.js +56 -0
  45. package/dist/core/model/model-catalog.js +1 -1
  46. package/dist/core/model/model-scope.js +40 -7
  47. package/dist/core/permission.js +9 -12
  48. package/dist/core/relation/peer-identity.js +16 -1
  49. package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  51. package/dist/core/session/session-manager.js +27 -13
  52. package/dist/core/session/session-title.js +26 -0
  53. package/dist/core/stats/billing.js +151 -0
  54. package/dist/core/stats/budget.js +93 -0
  55. package/dist/core/stats/db.js +314 -0
  56. package/dist/core/stats/eck-vars.js +84 -0
  57. package/dist/core/stats/index.js +10 -0
  58. package/dist/core/stats/normalizer.js +78 -0
  59. package/dist/core/stats/query.js +760 -0
  60. package/dist/core/stats/writer.js +115 -0
  61. package/dist/core/trigger/manager.js +34 -0
  62. package/dist/core/trigger/parser.js +9 -3
  63. package/dist/core/trigger/scheduler.js +20 -17
  64. package/dist/{agents → eck}/kit-renderer.js +5 -1
  65. package/dist/{agents → eck}/manifest-engine.js +127 -35
  66. package/dist/{agents → eck}/message-renderer.js +26 -1
  67. package/dist/index.js +185 -8
  68. package/dist/ipc.js +22 -0
  69. package/dist/paths.js +7 -3
  70. package/dist/utils/cross-platform.js +23 -5
  71. package/dist/utils/ecweb-pair.js +20 -0
  72. package/dist/utils/stats.js +14 -0
  73. package/kits/docs/evolclaw/INDEX.md +3 -1
  74. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  75. package/kits/docs/evolclaw/fs.md +131 -0
  76. package/kits/docs/evolclaw/group-fs.md +209 -0
  77. package/kits/docs/evolclaw/stats.md +70 -0
  78. package/kits/docs/venues/aun-group.md +29 -6
  79. package/kits/docs/venues/group.md +5 -4
  80. package/kits/eck_manifest.json +12 -0
  81. package/kits/eck_message_manifest.json +30 -3
  82. package/kits/rules/05-venue.md +1 -1
  83. package/kits/templates/message-fragments/inject-default.md +2 -0
  84. package/kits/templates/message-fragments/item.md +1 -1
  85. package/kits/templates/system-fragments/response-depth.md +16 -0
  86. package/package.json +4 -4
  87. package/dist/agents/baseagent-normalize.js +0 -19
  88. package/dist/core/relation/peer-key.js +0 -16
  89. package/dist/utils/channel-helpers.js +0 -46
@@ -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';
@@ -58,6 +59,38 @@ function getEvolclawVersion() {
58
59
  }
59
60
  return _cachedVersion;
60
61
  }
62
+ /**
63
+ * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
64
+ *
65
+ * registerBridge 适配回调用它替代手抄字段——历史上手抄漏掉了
66
+ * sameDevice/sameNetwork/sameEgressIp,proximity 在此被吞,eck-debug 永远 false。
67
+ * 抽成纯函数后可单测锁字段,杜绝再漏。
68
+ */
69
+ export function aunOptsToInbound(opts, channel, channelType) {
70
+ return {
71
+ channel,
72
+ channelType,
73
+ channelId: opts.channelId,
74
+ selfAID: opts.selfAID,
75
+ groupId: opts.groupId,
76
+ content: opts.content,
77
+ chatType: opts.chatType || 'private',
78
+ peerId: opts.peerId || '',
79
+ peerName: opts.peerName,
80
+ peerType: opts.peerType,
81
+ sameDevice: opts.sameDevice,
82
+ sameNetwork: opts.sameNetwork,
83
+ sameEgressIp: opts.sameEgressIp,
84
+ messageId: opts.messageId,
85
+ mentions: opts.mentions,
86
+ mentionAids: opts.mentionAids,
87
+ threadId: opts.threadId,
88
+ replyContext: opts.replyContext,
89
+ source: opts.source,
90
+ images: opts.images,
91
+ dispatchMode: opts.dispatchMode,
92
+ };
93
+ }
61
94
  export class AUNChannel {
62
95
  config;
63
96
  client = null;
@@ -84,14 +117,16 @@ export class AUNChannel {
84
117
  // 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
85
118
  const d = (data && typeof data === 'object') ? data : {};
86
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 : {};
87
122
  const topContext = {
88
123
  self_aid: this._aid ?? this.config.aid,
89
124
  };
90
125
  // peer / group 识别
91
- 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;
92
127
  if (peerAid)
93
128
  topContext.peer_aid = peerAid;
94
- const groupId = d.group_id ?? payload.group_id;
129
+ const groupId = env.group_id ?? d.group_id ?? payload.group_id;
95
130
  if (groupId)
96
131
  topContext.group_id = groupId;
97
132
  // task_id / chatmode(message.send / thought.put / status 都可能有)
@@ -144,6 +179,7 @@ export class AUNChannel {
144
179
  * - 数字群号:{group_no}.{issuer}(如 11117.agentid.pub)
145
180
  * - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
146
181
  */
182
+ /** 判断 channelId 是否群组 ID(public:plugin adapter 闭包需调用) */
147
183
  isGroupId(id) {
148
184
  return (id.startsWith('group.') && id.includes('/'))
149
185
  || /^\d+\./.test(id)
@@ -184,23 +220,19 @@ export class AUNChannel {
184
220
  if (cardInfo) {
185
221
  const actionValue = typeof obj.value === 'string' ? obj.value
186
222
  : typeof obj.action_value === 'string' ? obj.action_value : text;
223
+ // 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
224
+ // payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
225
+ // 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
226
+ const cardClickerAid = senderAid || channelId || '';
187
227
  if (cardInfo.isCommandCard) {
188
228
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息
189
229
  this.cardMessageIdMap.delete(cardMsgId);
190
230
  if (this.messageHandler && actionValue.startsWith('/')) {
191
231
  const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
192
- // 卡片点击者身份:优先 payload.from / payload.sender_aid / payload.user_id,
193
- // fallback 到外层 senderAid,最后用 cardInfo 中记录的原始命令发起者
194
- const cardClickerAid = (typeof obj.from === 'string' && obj.from)
195
- || (typeof obj.sender_aid === 'string' && obj.sender_aid)
196
- || (typeof obj.user_id === 'string' && obj.user_id)
197
- || senderAid
198
- || cardInfo.initiatorAid
199
- || channelId || '';
200
- // Initiator 校验:群聊中仅卡片发起者可操作(与飞书行为对齐)
201
- if (cardInfo.initiatorAid && cardClickerAid
202
- && cardClickerAid !== cardInfo.initiatorAid
203
- && !this.isGroupId(cardClickerAid)) {
232
+ // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
233
+ // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
234
+ if (chatType === 'group' && cardInfo.initiatorAid && cardClickerAid
235
+ && cardClickerAid !== cardInfo.initiatorAid) {
204
236
  logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
205
237
  return '';
206
238
  }
@@ -225,6 +257,7 @@ export class AUNChannel {
225
257
  id: cardInfo.requestId,
226
258
  action: actionValue,
227
259
  values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
260
+ operatorId: cardClickerAid || undefined,
228
261
  });
229
262
  }
230
263
  }
@@ -427,6 +460,7 @@ export class AUNChannel {
427
460
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
428
461
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
429
462
  seenMessages = new Map();
463
+ groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
430
464
  peerInfoCache = new Map();
431
465
  messageSeqMap = new Map(); // messageId → seq (for ack)
432
466
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
@@ -445,6 +479,8 @@ export class AUNChannel {
445
479
  static MENU_REQUEST_TYPES = new Set([
446
480
  'menu.list', 'menu.query', 'menu.options', 'menu.update', 'menu.action',
447
481
  ]);
482
+ /** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
483
+ static INJECT_REQUEST_TYPE = 'observer.inject';
448
484
  // Reconnect state
449
485
  // SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
450
486
  // 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
@@ -591,13 +627,17 @@ export class AUNChannel {
591
627
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
592
628
  this.handleIncomingPrivateMessage(data);
593
629
  });
594
- client.on('group.message_created', (data) => {
595
- this.trace('IN', 'group.message_created', data);
596
- const gid = (data && typeof data === 'object') ? data.group_id ?? '' : '';
597
- const sender = (data && typeof data === 'object') ? data.sender_aid ?? '' : '';
598
- logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
599
- this.handleIncomingGroupMessage(data);
600
- });
630
+ // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
631
+ if (!this.config.pureIdentity) {
632
+ client.on('group.message_created', (data) => {
633
+ this.trace('IN', 'group.message_created', data);
634
+ const env = (data && typeof data === 'object') ? data.envelope ?? {} : {};
635
+ const gid = env.group_id ?? '';
636
+ const sender = env.from ?? '';
637
+ logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
638
+ this.handleIncomingGroupMessage(data);
639
+ });
640
+ }
601
641
  client.on('connection.state', (data) => {
602
642
  // trace is handled inside handleConnectionState with throttling
603
643
  this.handleConnectionState(data);
@@ -624,13 +664,36 @@ export class AUNChannel {
624
664
  client.on('message.undecryptable', (data) => {
625
665
  this.trace('IN', 'message.undecryptable', data);
626
666
  const d = data;
627
- logger.warn(`${this.logPrefix()} Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
628
- });
629
- client.on('group.message_undecryptable', (data) => {
630
- this.trace('IN', 'group.message_undecryptable', data);
631
- const d = data;
632
- logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
667
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
668
+ logger.warn(`${this.logPrefix()} Message undecryptable: from=${env.from} mid=${d.message_id} err=${d._decrypt_error}`);
633
669
  });
670
+ // pureIdentity(控制 AID):不注册 group 解密失败监听
671
+ if (!this.config.pureIdentity) {
672
+ client.on('group.message_undecryptable', (data) => {
673
+ this.trace('IN', 'group.message_undecryptable', data);
674
+ const d = data;
675
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
676
+ logger.warn(`${this.logPrefix()} Group message undecryptable: group=${env.group_id} from=${env.from} mid=${d.message_id} err=${d._decrypt_error}`);
677
+ });
678
+ // 群消息撤回(SDK 0.4.10 在线 push 通道):与私聊 message.recalled 同构,
679
+ // 逐个 message_id 交给 recallHandler → msgBridge.cancel(排队中删除 / 处理中中断)。
680
+ client.on('group.message_recalled', (data) => {
681
+ this.trace('IN', 'group.message_recalled', data);
682
+ if (data && typeof data === 'object') {
683
+ const d = data;
684
+ const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
685
+ const ids = d.message_ids;
686
+ if (Array.isArray(ids)) {
687
+ for (const id of ids) {
688
+ if (typeof id === 'string') {
689
+ logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
690
+ this.recallHandler?.(id);
691
+ }
692
+ }
693
+ }
694
+ }
695
+ });
696
+ }
634
697
  // Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
635
698
  try {
636
699
  logger.info(`${this.logPrefix()} Authenticating as ${aidName}...`);
@@ -676,7 +739,8 @@ export class AUNChannel {
676
739
  this._aid = this.client.aid ?? undefined;
677
740
  const deviceId = this.client._device_id ?? '';
678
741
  this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
679
- this._selfName = this.loadSelfName(aidName);
742
+ // pureIdentity(控制 AID):无 agent.md,跳过自身 agent.md 拉取,省一次 404
743
+ this._selfName = this.config.pureIdentity ? undefined : this.loadSelfName(aidName);
680
744
  if (this._selfName && this.aidStatsCollector)
681
745
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
682
746
  this.connected = true;
@@ -697,7 +761,10 @@ export class AUNChannel {
697
761
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
698
762
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
699
763
  // Send welcome message to owner after first connection
700
- await this.sendWelcomeMessage();
764
+ // pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
765
+ if (!this.config.pureIdentity) {
766
+ await this.sendWelcomeMessage();
767
+ }
701
768
  }
702
769
  catch (e) {
703
770
  this.trace('OUT', 'client.connect.error', { error: String(e) });
@@ -962,12 +1029,23 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
962
1029
  if (!data || typeof data !== 'object')
963
1030
  return;
964
1031
  const msg = data;
965
- const fromAid = msg.from ?? '';
1032
+ // SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1033
+ // message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
1034
+ const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1035
+ const fromAid = env.from ?? '';
966
1036
  const payload = msg.payload ?? '';
967
- const text = this.extractTextPayload(payload, fromAid);
1037
+ const text = this.extractTextPayload(payload, fromAid, fromAid);
968
1038
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
969
1039
  const messageId = msg.message_id ?? '';
970
1040
  const seq = msg.seq;
1041
+ // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1042
+ // forwardInbound 内部排除 self-echo 与 from-owner。
1043
+ // 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
1044
+ // (即便日后 from-owner 排除规则调整,也不会泄漏)。
1045
+ const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
1046
+ if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
1047
+ this.forwardInbound(msg);
1048
+ }
971
1049
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
972
1050
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
973
1051
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -977,7 +1055,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
977
1055
  return;
978
1056
  }
979
1057
  // 记录入站消息加密状态,透传到出站 ReplyContext
980
- const msgEncrypted = !!(msg.e2ee);
1058
+ const msgEncrypted = !!env.encrypted;
981
1059
  if (!msgEncrypted)
982
1060
  this.plaintextRecv++;
983
1061
  // Detect @mentions
@@ -1013,6 +1091,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1013
1091
  });
1014
1092
  return;
1015
1093
  }
1094
+ // observer.inject:owner 插话。鉴权 from∈owners 后,以 target.channel_id 选 agent↔对端 会话,
1095
+ // observer 插话(v0.3):只落盘到 pending-hints,不进 Agent、不回 owner。详见 docs/observer-insert-design.md。
1096
+ if (p2pPayloadType === AUNChannel.INJECT_REQUEST_TYPE) {
1097
+ this.acknowledgeImmediately(messageId, seq);
1098
+ this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type);
1099
+ return;
1100
+ }
1016
1101
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
1017
1102
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
1018
1103
  this.acknowledgeImmediately(messageId, seq);
@@ -1049,13 +1134,19 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1049
1134
  if (!data || typeof data !== 'object')
1050
1135
  return;
1051
1136
  const msg = data;
1052
- const groupId = msg.group_id ?? '';
1053
- const senderAid = msg.sender_aid ?? '';
1137
+ // SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
1138
+ // message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
1139
+ const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
1140
+ const groupId = env.group_id ?? '';
1141
+ const senderAid = env.from ?? '';
1054
1142
  const payload = msg.payload ?? '';
1055
1143
  const text = this.extractTextPayload(payload, groupId, senderAid);
1056
1144
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1057
1145
  const messageId = msg.message_id ?? '';
1058
1146
  const seq = msg.seq;
1147
+ // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1148
+ // forwardInbound 内部排除 self-echo 与 from-owner。
1149
+ this.forwardInbound(msg);
1059
1150
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1060
1151
  const payloadMentions = Array.isArray(payload?.mentions)
1061
1152
  ? payload.mentions.filter((m) => typeof m === 'string')
@@ -1077,7 +1168,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1077
1168
  const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
1078
1169
  if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
1079
1170
  this.acknowledgeImmediately(messageId, seq);
1080
- const msgEncryptedFast = !!(msg.e2ee);
1171
+ const msgEncryptedFast = !!env.encrypted;
1081
1172
  const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1082
1173
  const peerInfo = this.peerInfoCached(senderAid);
1083
1174
  const shortAid = this.getShortAid(senderAid);
@@ -1128,7 +1219,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1128
1219
  }
1129
1220
  }
1130
1221
  // 记录入站消息加密状态,透传到出站 ReplyContext
1131
- const msgEncrypted = !!(msg.e2ee);
1222
+ const msgEncrypted = !!env.encrypted;
1132
1223
  if (!msgEncrypted)
1133
1224
  this.plaintextRecv++;
1134
1225
  // dispatch_mode: 本地设置优先,fallback 到服务器参数
@@ -1216,6 +1307,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1216
1307
  logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1217
1308
  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 });
1218
1309
  this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1310
+ // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1311
+ // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1312
+ const renderMentionAids = Array.from(new Set([
1313
+ ...payloadMentions,
1314
+ ...this.extractMentionAidsFromText(text),
1315
+ ]));
1219
1316
  this.dispatchMessage({
1220
1317
  channelId: groupId,
1221
1318
  groupId,
@@ -1231,7 +1328,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1231
1328
  seq,
1232
1329
  threadId,
1233
1330
  mentions,
1331
+ mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
1234
1332
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1333
+ dispatchMode,
1235
1334
  images: inboundImages.length > 0 ? inboundImages : undefined,
1236
1335
  });
1237
1336
  }
@@ -1301,50 +1400,191 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1301
1400
  messageId: event.messageId,
1302
1401
  threadId: event.threadId,
1303
1402
  mentions: mentionObjects,
1403
+ mentionAids: event.mentionAids,
1304
1404
  replyContext,
1405
+ source: event.source,
1406
+ dispatchMode: event.dispatchMode,
1305
1407
  images: event.images,
1306
1408
  }).catch(err => {
1307
1409
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1308
1410
  });
1309
- // Observer forward: inbound
1310
- this.forwardToOwners('inbound', {
1311
- from: event.userId || event.channelId || '',
1312
- to: this.config.aid,
1313
- seq: event.seq,
1314
- payload: { type: 'text', text: event.text },
1315
- });
1316
1411
  }
1412
+ // ── 观察者模式(Observer Mode) ──────────────────────────────
1413
+ //
1414
+ // observable=true 时,Agent 收发的每条 AUN 消息(原始信封 + 解密后明文
1415
+ // payload)镜像一份给 owners[]。入站在所有过滤之前转发;出站在真实发送
1416
+ // 成功后转发。外层一律明文。详见 docs/observer-mode-design.md。
1417
+ // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
1418
+ // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
1419
+ observerConfigResolver;
1420
+ /** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
1421
+ setObserverConfigResolver(fn) {
1422
+ this.observerConfigResolver = fn;
1423
+ }
1424
+ /** 读取 observable 开关 + owners;无 resolver(如未接入 daemon)时视为关闭。 */
1425
+ getObserverConfig() {
1426
+ return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
1427
+ }
1428
+ /**
1429
+ * 入站转发:到达本 AID 的消息全部转发,排除 self-echo。
1430
+ * 若消息发送方本身是 owner,则不转发给该 owner,但仍转发给其他 owner。
1431
+ * 调用点须在所有过滤逻辑之前,payload 为 SDK 解密后的明文。
1432
+ */
1317
1433
  /**
1318
- * 观察者模式转发:将消息副本以 observer.forward 格式发给所有 owners
1319
- * 仅在 AgentConfig.observable === true 时执行;owners 为空或无法加载配置时静默跳过。
1434
+ * 入站转发:把对端发来的消息原样转发给 owner
1435
+ * data 为 SDK message.received / group.message_created 回调的整个对象,
1436
+ * 不拆解、不重组——SDK 信封结构变化不影响此处。
1320
1437
  */
1321
- forwardToOwners(direction, original) {
1438
+ forwardInbound(data) {
1322
1439
  if (!this.connected || !this.client)
1323
1440
  return;
1324
- const agentConfig = loadAgent(this.config.aid);
1325
- if (!agentConfig?.observable)
1441
+ const { observable, owners } = this.getObserverConfig();
1442
+ if (!observable || owners.length === 0)
1326
1443
  return;
1327
- const owners = agentConfig.owners ?? [];
1328
- if (owners.length === 0)
1444
+ const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
1445
+ const from = env.from ?? '';
1446
+ if (this._aid && from === this._aid)
1447
+ return; // self-echo:已在出站转过
1448
+ // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
1449
+ const recipientOwners = owners.filter(o => o !== from);
1450
+ if (recipientOwners.length === 0)
1329
1451
  return;
1452
+ this.emitForward('inbound', data, recipientOwners);
1453
+ }
1454
+ /**
1455
+ * 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
1456
+ * result 为 SDK message.send / group.send 的 SendResult(已 attach envelope + payload)。
1457
+ * 若对端本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1458
+ */
1459
+ forwardOutbound(result) {
1460
+ if (!this.connected || !this.client)
1461
+ return;
1462
+ const { observable, owners } = this.getObserverConfig();
1463
+ if (!observable || owners.length === 0)
1464
+ return;
1465
+ const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
1466
+ const to = env.to ?? env.group_id ?? '';
1467
+ // 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1468
+ // 但仍转发给其他 owner。
1469
+ const recipientOwners = owners.filter(o => o !== to);
1470
+ if (recipientOwners.length === 0)
1471
+ return;
1472
+ this.emitForward('outbound', result, recipientOwners);
1473
+ }
1474
+ /**
1475
+ * 实际投递 observer.forward 给每个 owner,外层一律明文。
1476
+ * original 为 SDK 给到的原始对象(入站回调 data / 出站 SendResult),整体透传,
1477
+ * 不挑字段、不改字段——SDK 加任何字段都会自动一并转发给 owner。
1478
+ */
1479
+ emitForward(direction, original, owners) {
1330
1480
  const forwardPayload = {
1331
1481
  type: 'observer.forward',
1332
1482
  direction,
1333
1483
  agent_aid: this.config.aid,
1334
- original: {
1335
- from: original.from,
1336
- to: original.to,
1337
- ...(original.seq != null ? { seq: original.seq } : {}),
1338
- timestamp: Date.now(),
1339
- payload: original.payload,
1340
- },
1484
+ original,
1341
1485
  };
1342
1486
  for (const ownerAid of owners) {
1343
- const encrypt = this.shouldEncrypt(ownerAid);
1344
- this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
1487
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
1345
1488
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
1346
1489
  }
1347
1490
  }
1491
+ // ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
1492
+ //
1493
+ // owner 经 message.send 给 agent 自身 AID 发 observer.inject(payload 为对象)。
1494
+ // 鉴权 from∈owners 后,把提示【只落盘】到 agent↔对端 会话的 pending-hints.jsonl
1495
+ // (不 dispatch、不跑 LLM、不回 owner);下一条对端消息到达时由 message-processor
1496
+ // 回放消费、注入渲染层。action=add 加提示 / remove 撤销。
1497
+ // 详见 docs/observer-insert-design.md 第一部分。
1498
+ /** 回 observer.inject.ack 给 owner(明文)。accepted 在成功写盘之后发出。 */
1499
+ emitInjectAck(ownerAid, injectId, data, error) {
1500
+ if (!this.connected || !this.client)
1501
+ return;
1502
+ const ackPayload = { type: 'observer.inject.ack' };
1503
+ if (injectId)
1504
+ ackPayload.id = injectId;
1505
+ if (data)
1506
+ ackPayload.data = data;
1507
+ if (error)
1508
+ ackPayload.error = error;
1509
+ this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: false })
1510
+ .catch(e => logger.debug(`${this.logPrefix()} observer.inject.ack to ${ownerAid} failed: ${e}`));
1511
+ }
1512
+ /** 处理 observer.inject:鉴权 + 校验 + 只落盘到 pending-hints(不触发处理、不回 owner)。 */
1513
+ handleObserverInject(fromAid, payload, displayName, peerType) {
1514
+ void peerType;
1515
+ const { owners } = this.getObserverConfig();
1516
+ const ts = Date.now();
1517
+ const req = parseInjectRequest(payload, fromAid, owners, ts);
1518
+ if (req.kind === 'reject') {
1519
+ logger.warn(`${this.logPrefix()} observer.inject rejected: ${this.getShortAid(fromAid)} ${req.code}`);
1520
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.action }, { code: req.code, message: req.message });
1521
+ return;
1522
+ }
1523
+ const selfAID = this.config.aid;
1524
+ const sessionsDir = resolvePaths().sessionsDir;
1525
+ let ok;
1526
+ if (req.kind === 'remove') {
1527
+ ok = appendHintRemove(sessionsDir, 'aun', req.channelId, selfAID, { targetId: req.targetId, threadId: req.threadId, ts });
1528
+ }
1529
+ else {
1530
+ ok = appendHintAdd(sessionsDir, 'aun', req.channelId, selfAID, { id: req.id, text: req.text, threadId: req.threadId, ownerAid: req.ownerAid, ts });
1531
+ }
1532
+ if (!ok) {
1533
+ this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示落盘失败' });
1534
+ return;
1535
+ }
1536
+ 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)'}`}`);
1537
+ this.emitInjectAck(fromAid, req.injectId, { status: 'accepted', action: req.kind });
1538
+ // 记录到 watch(被观察的 agent↔对端 会话),带 owner-inject 标记区分对端真实消息。
1539
+ // v0.3:只记"提示已添加/已撤销",不触发处理、不产生 agent→owner 回应。
1540
+ const watchText = req.kind === 'remove' ? `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}` : req.text;
1541
+ const synthId = `inject-${req.injectId || ts}`;
1542
+ this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, synthId, watchText);
1543
+ }
1544
+ /**
1545
+ * 把 observer 插话 / 对插话的回应记录到 watch(被观察的 agent↔对端 会话),
1546
+ * 带 source='owner-inject' 标记,与对端真实消息区分。
1547
+ * 写三处:messages.jsonl(watch msg)、appendAidEvent(watch aid 事件流)、aidStatsCollector(统计)。
1548
+ * @param dir 'in'=owner→agent 插话;'out'=agent→owner 对插话的回应
1549
+ * @param peerChannelId 被观察会话的对端(agent↔对端),日志落点 = sessions/aun/<self>/<peerChannelId>/
1550
+ */
1551
+ recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text) {
1552
+ try {
1553
+ const selfAID = this.config.aid;
1554
+ const isGroup = chatType === 'group';
1555
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID);
1556
+ const entry = dir === 'in'
1557
+ ? buildInboundEntry({
1558
+ from: ownerAid, to: selfAID, chatType,
1559
+ groupId: isGroup ? peerChannelId : null, msgId, content: text,
1560
+ permMode: 'owner', source: 'owner-inject',
1561
+ })
1562
+ : buildOutboundEntry({
1563
+ from: selfAID, to: ownerAid, chatType,
1564
+ groupId: isGroup ? peerChannelId : null, msgId, content: text,
1565
+ source: 'owner-inject',
1566
+ });
1567
+ appendMessageLog(chatDir, entry);
1568
+ }
1569
+ catch (e) {
1570
+ logger.debug(`${this.logPrefix()} recordInjectWatch(msg) failed: ${e}`);
1571
+ }
1572
+ // watch aid:事件流 + 统计(标 inject,便于过滤)
1573
+ try {
1574
+ const len = Buffer.byteLength(text, 'utf-8');
1575
+ if (dir === 'in') {
1576
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: ownerAid, msgId, kind: 'text', len, inject: true });
1577
+ this.aidStatsCollector?.recordInbound(this.config.aid, ownerAid, len, text, false, false, 'inject');
1578
+ }
1579
+ else {
1580
+ appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: ownerAid, msgId, kind: 'text', len, inject: true });
1581
+ this.aidStatsCollector?.recordOutbound(this.config.aid, ownerAid, len, text, false, false, 'inject');
1582
+ }
1583
+ }
1584
+ catch (e) {
1585
+ logger.debug(`${this.logPrefix()} recordInjectWatch(aid) failed: ${e}`);
1586
+ }
1587
+ }
1348
1588
  handleEcho(event) {
1349
1589
  const ts = () => {
1350
1590
  const d = new Date();
@@ -1820,12 +2060,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1820
2060
  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 });
1821
2061
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1822
2062
  this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1823
- // Observer forward: outbound (group)
1824
- this.forwardToOwners('outbound', {
1825
- from: this.config.aid,
1826
- to: channelId,
1827
- payload: { type: 'text', text: finalText },
1828
- });
2063
+ // Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload)
2064
+ this.forwardOutbound(result);
1829
2065
  }
1830
2066
  }
1831
2067
  else {
@@ -1839,12 +2075,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1839
2075
  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 });
1840
2076
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1841
2077
  this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1842
- // Observer forward: outbound (private)
1843
- this.forwardToOwners('outbound', {
1844
- from: this.config.aid,
1845
- to: targetAid,
1846
- payload: { type: 'text', text: finalText },
1847
- });
2078
+ // Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload)
2079
+ this.forwardOutbound(result);
1848
2080
  }
1849
2081
  }
1850
2082
  return true;
@@ -1862,6 +2094,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1862
2094
  if (!result || !result.message_id) {
1863
2095
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
1864
2096
  }
2097
+ this.forwardOutbound(result);
1865
2098
  }
1866
2099
  else {
1867
2100
  this.trace('OUT', 'message.send.fallback', params);
@@ -1870,6 +2103,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1870
2103
  if (!result || !result.message_id) {
1871
2104
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
1872
2105
  }
2106
+ this.forwardOutbound(result);
1873
2107
  }
1874
2108
  return true;
1875
2109
  }
@@ -1965,6 +2199,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1965
2199
  const tid = putRes?.thought_id;
1966
2200
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1967
2201
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2202
+ this.forwardOutbound(putRes);
1968
2203
  if (thoughtText) {
1969
2204
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
1970
2205
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
@@ -1976,6 +2211,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1976
2211
  const tid = putRes?.thought_id;
1977
2212
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1978
2213
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2214
+ this.forwardOutbound(putRes);
1979
2215
  if (thoughtText) {
1980
2216
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
1981
2217
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
@@ -2012,12 +2248,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2012
2248
  const result = await this.callAndTrace('group.send', params);
2013
2249
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
2014
2250
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
2251
+ this.forwardOutbound(result);
2015
2252
  return mid;
2016
2253
  }
2017
2254
  else {
2018
2255
  params.to = targetAid;
2019
2256
  const result = await this.callAndTrace('message.send', params);
2020
2257
  logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
2258
+ this.forwardOutbound(result);
2021
2259
  return result?.message_id ?? null;
2022
2260
  }
2023
2261
  }
@@ -2134,11 +2372,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2134
2372
  ? !!(context.metadata.encrypted)
2135
2373
  : this.shouldEncrypt(encryptTarget);
2136
2374
  const params = { payload: filePayload, encrypt };
2375
+ let sendResult = null;
2137
2376
  try {
2138
2377
  if (isGroup) {
2139
2378
  params.group_id = channelId;
2140
2379
  this.trace('OUT', 'group.send.file', params);
2141
2380
  const result = await this.client.call('group.send', params);
2381
+ sendResult = result;
2142
2382
  const fileMid = result?.message?.message_id ?? result?.message_id;
2143
2383
  this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
2144
2384
  if (!fileMid) {
@@ -2149,6 +2389,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2149
2389
  params.to = fileTargetAid;
2150
2390
  this.trace('OUT', 'message.send.file', params);
2151
2391
  const result = await this.client.call('message.send', params);
2392
+ sendResult = result;
2152
2393
  this.trace('OUT', 'message.send.file.ok', { message_id: result?.message_id });
2153
2394
  if (!result || !result.message_id) {
2154
2395
  logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
@@ -2167,6 +2408,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2167
2408
  if (isGroup) {
2168
2409
  this.trace('OUT', 'group.send.file.fallback', params);
2169
2410
  const result = await this.client.call('group.send', params);
2411
+ sendResult = result;
2170
2412
  const fbMid = result?.message?.message_id ?? result?.message_id;
2171
2413
  this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
2172
2414
  if (!fbMid) {
@@ -2176,6 +2418,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2176
2418
  else {
2177
2419
  this.trace('OUT', 'message.send.file.fallback', params);
2178
2420
  const result = await this.client.call('message.send', params);
2421
+ sendResult = result;
2179
2422
  this.trace('OUT', 'message.send.file.fallback.ok', { message_id: result?.message_id });
2180
2423
  if (!result || !result.message_id) {
2181
2424
  logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
@@ -2187,6 +2430,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2187
2430
  }
2188
2431
  }
2189
2432
  logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
2433
+ if (sendResult)
2434
+ this.forwardOutbound(sendResult);
2190
2435
  return true;
2191
2436
  }
2192
2437
  catch (e) {
@@ -2276,7 +2521,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2276
2521
  const c = this.client;
2277
2522
  if (!c) {
2278
2523
  logger.debug(`${this.logPrefix()} ${label} skipped: client gone`);
2279
- return Promise.resolve();
2524
+ return Promise.resolve(null);
2280
2525
  }
2281
2526
  const encrypt = computeEncrypt();
2282
2527
  const params = { payload, encrypt };
@@ -2285,23 +2530,28 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2285
2530
  else
2286
2531
  params.to = statusTargetAid;
2287
2532
  this.trace('OUT', `${method}.task_${label}`, params);
2288
- return c.call(method, params).catch(e => {
2533
+ return c.call(method, params).catch((e) => {
2289
2534
  if (encrypt && e instanceof E2EEError) {
2290
2535
  this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
2291
2536
  logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
2292
2537
  const c2 = this.client;
2293
2538
  if (!c2)
2294
- return;
2539
+ return null;
2295
2540
  const fallbackParams = { ...params, encrypt: false };
2296
- return c2.call(method, fallbackParams).catch(e2 => {
2541
+ return c2.call(method, fallbackParams).catch((e2) => {
2297
2542
  logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
2543
+ return null;
2298
2544
  });
2299
2545
  }
2300
2546
  logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
2547
+ return null;
2301
2548
  });
2302
2549
  };
2303
2550
  const method = isGroup ? 'group.send' : 'message.send';
2304
- sendOne(method, statusPayload, 'status');
2551
+ sendOne(method, statusPayload, 'status').then(result => {
2552
+ if (result)
2553
+ this.forwardOutbound(result);
2554
+ }).catch(() => { });
2305
2555
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
2306
2556
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
2307
2557
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
@@ -2499,267 +2749,230 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2499
2749
  const result = await agentmdSync(aid, { store: this.store ?? undefined });
2500
2750
  return result.content ?? '';
2501
2751
  }
2752
+ /**
2753
+ * 取群显示名(group.get → group.name),进程内缓存。
2754
+ * 走长连接 callAndTrace,失败/未连接返回 undefined —— 绝不抛出阻塞消息处理。
2755
+ */
2756
+ async getGroupName(groupId) {
2757
+ if (!groupId)
2758
+ return undefined;
2759
+ const cached = this.groupNameCache.get(groupId);
2760
+ if (cached !== undefined)
2761
+ return cached || undefined;
2762
+ if (!this.client)
2763
+ return undefined;
2764
+ try {
2765
+ const result = await this.callAndTrace('group.get', { group_id: groupId });
2766
+ const name = result?.group?.name;
2767
+ if (typeof name === 'string' && name) {
2768
+ this.groupNameCache.set(groupId, name);
2769
+ return name;
2770
+ }
2771
+ this.groupNameCache.set(groupId, ''); // 负缓存:避免反复 RPC(空串视为无名)
2772
+ return undefined;
2773
+ }
2774
+ catch {
2775
+ return undefined; // 不写缓存,下次仍可重试
2776
+ }
2777
+ }
2502
2778
  }
2503
2779
  // Plugin implementation
2504
2780
  export class AUNChannelPlugin {
2505
2781
  name = 'aun';
2506
- isEnabled(config) {
2507
- const raw = config.channels?.aun;
2508
- if (!raw)
2509
- return false;
2510
- if (Array.isArray(raw)) {
2511
- return raw.some(inst => inst.enabled !== false && !!inst.aid);
2512
- }
2513
- return raw.enabled !== false && !!raw.aid;
2514
- }
2515
- async createChannels(config) {
2516
- const instances = normalizeChannelInstances(config.channels?.aun, 'aun');
2517
- const result = [];
2518
- for (const inst of instances) {
2519
- if (inst.enabled === false || !inst.aid)
2520
- continue;
2521
- const channel = new AUNChannel({
2522
- aid: inst.aid,
2523
- keystorePath: inst.keystorePath,
2524
- gatewayUrl: inst.gatewayUrl,
2525
- accessToken: inst.accessToken,
2526
- flushDelay: inst.flushDelay,
2527
- owner: inst.owner,
2528
- agentName: inst.agentName,
2529
- channelName: inst.name,
2530
- aunTrace: config.debug?.aunTrace,
2531
- aunSdkLog: config.debug?.aunSdkLog,
2532
- });
2533
- const adapter = {
2534
- channelName: inst.name,
2535
- channelKey: inst.name, // channelName 实际上就是 channelKey
2536
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
2537
- send: async (envelope, payload) => {
2538
- const ctx = envelope.replyContext;
2539
- const channelId = envelope.channelId;
2540
- switch (payload.kind) {
2541
- case 'result.text':
2542
- case 'command.result':
2543
- case 'command.error':
2544
- case 'system.notice':
2545
- case 'system.error':
2546
- case 'result.error': {
2547
- const sendCtx = { ...(ctx ?? {}) };
2548
- if (payload.kind === 'result.text' && payload.isFinal)
2549
- sendCtx.title = '✅ 最终回复:';
2550
- await channel.sendMessage(channelId, payload.text, sendCtx);
2551
- return;
2782
+ async createInstance(inst, ctx) {
2783
+ // AUN aid is the agent's own AID; loader injects it as inst.aid, ctx.agentName is the source of truth.
2784
+ const aid = inst.aid ?? ctx.agentName;
2785
+ if (inst.enabled === false || !aid)
2786
+ return null;
2787
+ const channel = new AUNChannel({
2788
+ aid,
2789
+ keystorePath: inst.keystorePath,
2790
+ gatewayUrl: inst.gatewayUrl,
2791
+ accessToken: inst.accessToken,
2792
+ flushDelay: inst.flushDelay,
2793
+ owner: inst.owner ?? inst.owners?.[0],
2794
+ agentName: ctx.agentName,
2795
+ channelName: inst.name,
2796
+ aunTrace: ctx.debug?.aunTrace,
2797
+ aunSdkLog: ctx.debug?.aunSdkLog,
2798
+ });
2799
+ const mode = resolveShowActivities(inst);
2800
+ const adapter = {
2801
+ channelName: inst.name,
2802
+ channelKey: inst.name, // channelName 实际上就是 channelKey
2803
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
2804
+ send: async (envelope, payload) => {
2805
+ const replyCtx = envelope.replyContext;
2806
+ const channelId = envelope.channelId;
2807
+ switch (payload.kind) {
2808
+ case 'result.text':
2809
+ case 'command.result':
2810
+ case 'command.error':
2811
+ case 'system.notice':
2812
+ case 'system.error':
2813
+ case 'result.error': {
2814
+ const sendCtx = { ...(replyCtx ?? {}) };
2815
+ if (payload.kind === 'result.text' && payload.isFinal)
2816
+ sendCtx.title = '✅ 最终回复:';
2817
+ await channel.sendMessage(channelId, payload.text, sendCtx);
2818
+ return;
2819
+ }
2820
+ case 'result.file':
2821
+ await channel.sendFile(channelId, payload.filePath, replyCtx);
2822
+ return;
2823
+ case 'result.image': {
2824
+ const buf = payload.data;
2825
+ const b64 = buf.toString('base64');
2826
+ await channel.sendStructured(channelId, {
2827
+ type: 'image', alt: payload.alt, data_base64: b64, mime_type: payload.mimeType,
2828
+ }, replyCtx);
2829
+ return;
2830
+ }
2831
+ case 'activity.batch': {
2832
+ const aunPayload = {
2833
+ type: 'thought',
2834
+ items: payload.items,
2835
+ client_context: { task_id: envelope.taskId, chatmode: envelope.chatmode, agent_name: envelope.agentName },
2836
+ };
2837
+ if (replyCtx?.threadId)
2838
+ aunPayload.thread_id = replyCtx.threadId;
2839
+ if (envelope.chatmode === 'proactive') {
2840
+ await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
2552
2841
  }
2553
- case 'result.file':
2554
- await channel.sendFile(channelId, payload.filePath, ctx);
2555
- return;
2556
- case 'result.image': {
2557
- // AUN 支持 image,走 sendStructured 发 type=image payload
2558
- const buf = payload.data;
2559
- const b64 = buf.toString('base64');
2560
- await channel.sendStructured(channelId, {
2561
- type: 'image',
2562
- alt: payload.alt,
2563
- data_base64: b64,
2564
- mime_type: payload.mimeType,
2565
- }, ctx);
2566
- return;
2842
+ else {
2843
+ await channel.sendStructured(channelId, aunPayload, replyCtx);
2567
2844
  }
2568
- case 'activity.batch': {
2569
- const aunPayload = {
2570
- type: 'thought',
2571
- items: payload.items,
2572
- client_context: { task_id: envelope.taskId, chatmode: envelope.chatmode, agent_name: envelope.agentName },
2845
+ return;
2846
+ }
2847
+ case 'status.progress':
2848
+ channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2849
+ return;
2850
+ case 'status.started':
2851
+ channel.sendProcessingStatus(channelId, 'start', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2852
+ return;
2853
+ case 'status.queued':
2854
+ channel.sendProcessingStatus(channelId, 'queued', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2855
+ return;
2856
+ case 'status.completed':
2857
+ channel.sendProcessingStatus(channelId, 'done', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2858
+ return;
2859
+ case 'status.interrupted':
2860
+ channel.sendProcessingStatus(channelId, 'interrupted', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2861
+ return;
2862
+ case 'status.error':
2863
+ channel.sendProcessingStatus(channelId, 'error', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2864
+ return;
2865
+ case 'status.timeout':
2866
+ channel.sendProcessingStatus(channelId, 'timeout', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
2867
+ return;
2868
+ case 'interaction': {
2869
+ const req = payload.interaction;
2870
+ if (req.kind.kind === 'action') {
2871
+ const action = req.kind;
2872
+ const aunCard = {
2873
+ type: 'action_card',
2874
+ title: action.title,
2875
+ actions: action.buttons.map(btn => ({
2876
+ label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
2877
+ })),
2573
2878
  };
2574
- if (ctx?.threadId)
2575
- aunPayload.thread_id = ctx.threadId;
2576
- if (envelope.chatmode === 'proactive') {
2577
- await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
2879
+ if (action.body)
2880
+ aunCard.description = action.body;
2881
+ if (req.initiatorId && channel.isGroupId(channelId))
2882
+ aunCard.initiator = req.initiatorId;
2883
+ if (replyCtx?.threadId)
2884
+ aunCard.thread_id = replyCtx.threadId;
2885
+ const msgId = await channel.sendStructured(channelId, aunCard, replyCtx);
2886
+ if (msgId) {
2887
+ channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: false, initiatorAid: req.initiatorId });
2888
+ setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2578
2889
  }
2579
- else {
2580
- // interactive 模式不发 thought.put,只写入消息历史
2581
- await channel.sendStructured(channelId, aunPayload, ctx);
2582
- }
2583
- return;
2584
2890
  }
2585
- case 'status.progress':
2586
- channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2587
- return;
2588
- case 'status.started':
2589
- channel.sendProcessingStatus(channelId, 'start', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2590
- return;
2591
- case 'status.queued':
2592
- channel.sendProcessingStatus(channelId, 'queued', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2593
- return;
2594
- case 'status.completed':
2595
- channel.sendProcessingStatus(channelId, 'done', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2596
- return;
2597
- case 'status.interrupted':
2598
- channel.sendProcessingStatus(channelId, 'interrupted', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2599
- return;
2600
- case 'status.error':
2601
- channel.sendProcessingStatus(channelId, 'error', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2602
- return;
2603
- case 'status.timeout':
2604
- channel.sendProcessingStatus(channelId, 'timeout', envelope.sessionId ?? envelope.taskId, envelope.taskId, ctx, payload.metadata);
2605
- return;
2606
- case 'interaction': {
2607
- const req = payload.interaction;
2608
- if (req.kind.kind === 'action') {
2609
- const action = req.kind;
2610
- const aunCard = {
2611
- type: 'action_card',
2612
- title: action.title,
2613
- actions: action.buttons.map(btn => ({
2614
- label: btn.label,
2615
- value: btn.key,
2616
- style: btn.style ?? 'default',
2617
- behavior: 'reply',
2618
- })),
2619
- };
2620
- if (action.body)
2621
- aunCard.description = action.body;
2622
- if (ctx?.threadId)
2623
- aunCard.thread_id = ctx.threadId;
2624
- const msgId = await channel.sendStructured(channelId, aunCard, ctx);
2625
- if (msgId) {
2626
- channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: false });
2627
- setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2628
- }
2629
- }
2630
- else if (req.kind.kind === 'command-card') {
2631
- const card = req.kind;
2632
- const aunCard = {
2633
- type: 'action_card',
2634
- title: card.title,
2635
- actions: card.buttons.map(btn => ({
2636
- label: btn.label,
2637
- value: btn.command,
2638
- style: btn.style ?? 'default',
2639
- behavior: 'reply',
2640
- })),
2641
- };
2642
- if (card.body)
2643
- aunCard.description = card.body;
2644
- if (ctx?.threadId)
2645
- aunCard.thread_id = ctx.threadId;
2646
- const msgId = await channel.sendStructured(channelId, aunCard, ctx);
2647
- if (msgId) {
2648
- channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
2649
- setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2650
- }
2651
- }
2652
- else if (payload.fallbackText) {
2653
- await channel.sendMessage(channelId, payload.fallbackText, ctx);
2891
+ else if (req.kind.kind === 'command-card') {
2892
+ const card = req.kind;
2893
+ const aunCard = {
2894
+ type: 'action_card',
2895
+ title: card.title,
2896
+ actions: card.buttons.map(btn => ({
2897
+ label: btn.label, value: btn.command, style: btn.style ?? 'default', behavior: 'reply',
2898
+ })),
2899
+ };
2900
+ if (card.body)
2901
+ aunCard.description = card.body;
2902
+ if (replyCtx?.threadId)
2903
+ aunCard.thread_id = replyCtx.threadId;
2904
+ const msgId = await channel.sendStructured(channelId, aunCard, replyCtx);
2905
+ if (msgId) {
2906
+ channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
2907
+ setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
2654
2908
  }
2655
- return;
2656
2909
  }
2657
- case 'custom': {
2658
- const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
2659
- channel.sendCustomPayload(channelId, text);
2660
- return;
2910
+ else if (payload.fallbackText) {
2911
+ await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
2661
2912
  }
2662
- default:
2663
- logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
2913
+ return;
2664
2914
  }
2665
- }, acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); }, onInteraction: (cb) => { channel.interactionCallback = cb; }, uploadAgentMd: (content) => channel.uploadAgentMd(content),
2666
- downloadAgentMd: (aid) => channel.downloadAgentMd(aid), _selfAid: () => channel.getStatus().aid,
2667
- _selfName: () => channel.getSelfName(),
2668
- };
2669
- const policy = {
2670
- canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
2671
- canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
2672
- canCreateSession: (chatType, identity) => true,
2673
- canDeleteSession: (chatType, identity) => true,
2674
- canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
2675
- messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
2676
- showMiddleResult: (chatType, identity) => {
2677
- const mode = getChannelShowActivities(config, inst.name);
2678
- if (mode === 'none')
2679
- return false;
2680
- if (mode === 'dm-only')
2681
- return chatType === 'private';
2682
- if (mode === 'owner-dm-only')
2683
- return chatType === 'private' && identity === 'owner';
2684
- return true;
2685
- },
2686
- showIdleMonitor: (chatType, identity) => {
2687
- const mode = getChannelShowActivities(config, inst.name);
2688
- if (mode === 'none')
2689
- return false;
2690
- if (mode === 'dm-only')
2691
- return chatType === 'private';
2692
- if (mode === 'owner-dm-only')
2693
- return chatType === 'private' && identity === 'owner';
2694
- return true;
2695
- },
2696
- accumulateErrors: (chatType, identity) => true,
2697
- };
2698
- const options = {
2699
- flushDelay: inst.flushDelay ?? 3,
2700
- fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
2701
- };
2702
- result.push({
2703
- channelType: 'aun',
2704
- adapter,
2705
- channel,
2706
- policy,
2707
- options,
2708
- connect: () => channel.connect(),
2709
- disconnect: () => channel.disconnect(),
2710
- onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
2711
- registerBridge(bridge, channelType) {
2712
- bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
2713
- handler({
2714
- channel: adapter.channelName,
2715
- channelType,
2716
- channelId: opts.channelId,
2717
- selfAID: opts.selfAID,
2718
- groupId: opts.groupId,
2719
- content: opts.content,
2720
- chatType: opts.chatType || 'private',
2721
- peerId: opts.peerId || '',
2722
- peerName: opts.peerName,
2723
- peerType: opts.peerType,
2724
- messageId: opts.messageId,
2725
- mentions: opts.mentions,
2726
- threadId: opts.threadId,
2727
- replyContext: opts.replyContext,
2728
- source: opts.source,
2729
- images: opts.images,
2730
- });
2731
- }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2732
- },
2733
- registerHooks(ctx) {
2734
- channel.setEventBus(ctx.eventBus);
2735
- if (channel.setOnChannelDown) {
2736
- channel.setOnChannelDown(() => {
2737
- ctx.eventBus.publish({
2738
- type: 'channel:error',
2739
- channel: 'aun',
2740
- channelName: adapter.channelName,
2741
- status: 'auth_error',
2742
- message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
2743
- timestamp: Date.now(),
2744
- });
2745
- });
2915
+ case 'custom': {
2916
+ const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
2917
+ channel.sendCustomPayload(channelId, text);
2918
+ return;
2746
2919
  }
2747
- if (typeof channel.setDispatchModeResolver === 'function') {
2748
- channel.setDispatchModeResolver(async (channelId) => {
2749
- const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);
2750
- return session?.metadata?.dispatchMode;
2920
+ default:
2921
+ logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
2922
+ }
2923
+ },
2924
+ acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
2925
+ onInteraction: (cb) => { channel.interactionCallback = cb; },
2926
+ uploadAgentMd: (content) => channel.uploadAgentMd(content),
2927
+ downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
2928
+ getGroupName: (groupId) => channel.getGroupName(groupId),
2929
+ _selfAid: () => channel.getStatus().aid,
2930
+ _selfName: () => channel.getSelfName(),
2931
+ };
2932
+ const policy = {
2933
+ canSwitchProject: (_, identity) => identity === 'owner' || identity === 'admin',
2934
+ canListProjects: (_, identity) => identity === 'owner' || identity === 'admin',
2935
+ canCreateSession: () => true,
2936
+ canDeleteSession: () => true,
2937
+ canImportCliSession: (_, identity) => identity === 'owner' || identity === 'admin',
2938
+ messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
2939
+ showMiddleResult: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
2940
+ showIdleMonitor: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
2941
+ accumulateErrors: () => true,
2942
+ };
2943
+ return {
2944
+ channelType: 'aun', adapter, channel,
2945
+ policy,
2946
+ options: { flushDelay: inst.flushDelay ?? 3, fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g },
2947
+ connect: () => channel.connect(),
2948
+ disconnect: () => channel.disconnect(),
2949
+ onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
2950
+ registerBridge(bridge, channelType) {
2951
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
2952
+ handler(aunOptsToInbound(opts, adapter.channelName, channelType));
2953
+ }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2954
+ },
2955
+ registerHooks(hookCtx) {
2956
+ channel.setEventBus(hookCtx.eventBus);
2957
+ if (channel.setOnChannelDown) {
2958
+ channel.setOnChannelDown(() => {
2959
+ hookCtx.eventBus.publish({
2960
+ type: 'channel:error',
2961
+ channel: 'aun',
2962
+ channelName: adapter.channelName,
2963
+ status: 'auth_error',
2964
+ message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
2965
+ timestamp: Date.now(),
2751
2966
  });
2752
- }
2753
- },
2754
- });
2755
- }
2756
- return result;
2757
- }
2758
- async createChannel(config) {
2759
- const instances = await this.createChannels(config);
2760
- if (instances.length === 0) {
2761
- throw new Error('AUN config missing (aid required, e.g. "mybot.agentid.pub")');
2762
- }
2763
- return instances[0];
2967
+ });
2968
+ }
2969
+ if (typeof channel.setDispatchModeResolver === 'function') {
2970
+ channel.setDispatchModeResolver(async (channelId) => {
2971
+ const session = await hookCtx.sessionManager.getActiveSession(adapter.channelName, channelId);
2972
+ return session?.metadata?.dispatchMode;
2973
+ });
2974
+ }
2975
+ },
2976
+ };
2764
2977
  }
2765
2978
  }