evolclaw 3.2.0 → 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 (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -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/store.js +1 -1
  10. package/dist/aun/storage/download.js +1 -1
  11. package/dist/aun/storage/upload.js +13 -1
  12. package/dist/channels/aun.js +406 -293
  13. package/dist/channels/dingtalk.js +77 -140
  14. package/dist/channels/feishu.js +97 -150
  15. package/dist/channels/qqbot.js +75 -138
  16. package/dist/channels/wechat.js +75 -136
  17. package/dist/channels/wecom.js +75 -138
  18. package/dist/cli/agent.js +8 -5
  19. package/dist/cli/index.js +177 -44
  20. package/dist/cli/init.js +33 -6
  21. package/dist/cli/model.js +1 -1
  22. package/dist/cli/stats.js +558 -0
  23. package/dist/cli/version.js +87 -0
  24. package/dist/cli/watch-msg.js +5 -2
  25. package/dist/config-store.js +12 -6
  26. package/dist/core/channel-loader.js +84 -82
  27. package/dist/core/command-handler.js +473 -114
  28. package/dist/core/evolagent-registry.js +1 -0
  29. package/dist/core/evolagent.js +1 -1
  30. package/dist/core/interaction-router.js +8 -0
  31. package/dist/core/message/command-handler-agent-control.js +63 -1
  32. package/dist/core/message/im-renderer.js +35 -13
  33. package/dist/core/message/items-formatter.js +9 -1
  34. package/dist/core/message/message-bridge.js +49 -21
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/message-processor.js +295 -35
  37. package/dist/core/message/message-queue.js +2 -2
  38. package/dist/core/message/pending-hints.js +232 -0
  39. package/dist/core/message/response-depth.js +56 -0
  40. package/dist/core/model/model-catalog.js +1 -1
  41. package/dist/core/model/model-scope.js +2 -2
  42. package/dist/core/permission.js +9 -12
  43. package/dist/core/relation/peer-identity.js +16 -1
  44. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  45. package/dist/core/session/session-manager.js +27 -13
  46. package/dist/core/session/session-title.js +26 -0
  47. package/dist/core/stats/billing.js +151 -0
  48. package/dist/core/stats/budget.js +93 -0
  49. package/dist/core/stats/db.js +314 -0
  50. package/dist/core/stats/eck-vars.js +84 -0
  51. package/dist/core/stats/index.js +10 -0
  52. package/dist/core/stats/normalizer.js +78 -0
  53. package/dist/core/stats/query.js +760 -0
  54. package/dist/core/stats/writer.js +115 -0
  55. package/dist/core/trigger/manager.js +34 -0
  56. package/dist/core/trigger/parser.js +9 -3
  57. package/dist/core/trigger/scheduler.js +20 -17
  58. package/dist/{agents → eck}/manifest-engine.js +20 -1
  59. package/dist/{agents → eck}/message-renderer.js +24 -1
  60. package/dist/index.js +130 -8
  61. package/dist/ipc.js +17 -1
  62. package/dist/utils/cross-platform.js +23 -5
  63. package/dist/utils/ecweb-pair.js +20 -0
  64. package/dist/utils/stats.js +14 -0
  65. package/kits/docs/evolclaw/INDEX.md +3 -1
  66. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  67. package/kits/docs/evolclaw/fs.md +131 -0
  68. package/kits/docs/evolclaw/group-fs.md +209 -0
  69. package/kits/docs/evolclaw/stats.md +70 -0
  70. package/kits/docs/venues/aun-group.md +29 -6
  71. package/kits/docs/venues/group.md +5 -4
  72. package/kits/eck_manifest.json +12 -0
  73. package/kits/eck_message_manifest.json +30 -3
  74. package/kits/rules/05-venue.md +1 -1
  75. package/kits/templates/message-fragments/inject-default.md +2 -0
  76. package/kits/templates/system-fragments/response-depth.md +16 -0
  77. package/package.json +4 -4
  78. package/dist/agents/baseagent-normalize.js +0 -19
  79. package/dist/core/relation/peer-key.js +0 -16
  80. package/dist/evolclaw-config.js +0 -11
  81. package/dist/utils/channel-helpers.js +0 -46
  82. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  83. /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)
@@ -215,23 +220,19 @@ export class AUNChannel {
215
220
  if (cardInfo) {
216
221
  const actionValue = typeof obj.value === 'string' ? obj.value
217
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 || '';
218
227
  if (cardInfo.isCommandCard) {
219
228
  // CommandCard:action_value 是完整 slash 命令,构造伪入站消息
220
229
  this.cardMessageIdMap.delete(cardMsgId);
221
230
  if (this.messageHandler && actionValue.startsWith('/')) {
222
231
  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)) {
232
+ // Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
233
+ // 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
234
+ if (chatType === 'group' && cardInfo.initiatorAid && cardClickerAid
235
+ && cardClickerAid !== cardInfo.initiatorAid) {
235
236
  logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
236
237
  return '';
237
238
  }
@@ -256,6 +257,7 @@ export class AUNChannel {
256
257
  id: cardInfo.requestId,
257
258
  action: actionValue,
258
259
  values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
260
+ operatorId: cardClickerAid || undefined,
259
261
  });
260
262
  }
261
263
  }
@@ -477,6 +479,8 @@ export class AUNChannel {
477
479
  static MENU_REQUEST_TYPES = new Set([
478
480
  'menu.list', 'menu.query', 'menu.options', 'menu.update', 'menu.action',
479
481
  ]);
482
+ /** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
483
+ static INJECT_REQUEST_TYPE = 'observer.inject';
480
484
  // Reconnect state
481
485
  // SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
482
486
  // 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
@@ -627,8 +631,9 @@ export class AUNChannel {
627
631
  if (!this.config.pureIdentity) {
628
632
  client.on('group.message_created', (data) => {
629
633
  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 ?? '' : '';
634
+ const env = (data && typeof data === 'object') ? data.envelope ?? {} : {};
635
+ const gid = env.group_id ?? '';
636
+ const sender = env.from ?? '';
632
637
  logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
633
638
  this.handleIncomingGroupMessage(data);
634
639
  });
@@ -659,14 +664,34 @@ export class AUNChannel {
659
664
  client.on('message.undecryptable', (data) => {
660
665
  this.trace('IN', 'message.undecryptable', data);
661
666
  const d = data;
662
- logger.warn(`${this.logPrefix()} Message undecryptable: 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}`);
663
669
  });
664
670
  // pureIdentity(控制 AID):不注册 group 解密失败监听
665
671
  if (!this.config.pureIdentity) {
666
672
  client.on('group.message_undecryptable', (data) => {
667
673
  this.trace('IN', 'group.message_undecryptable', data);
668
674
  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}`);
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
+ }
670
695
  });
671
696
  }
672
697
  // Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
@@ -1004,15 +1029,23 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1004
1029
  if (!data || typeof data !== 'object')
1005
1030
  return;
1006
1031
  const msg = data;
1007
- 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 ?? '';
1008
1036
  const payload = msg.payload ?? '';
1009
- const text = this.extractTextPayload(payload, fromAid);
1037
+ const text = this.extractTextPayload(payload, fromAid, fromAid);
1010
1038
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1011
1039
  const messageId = msg.message_id ?? '';
1012
1040
  const seq = msg.seq;
1013
1041
  // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1014
1042
  // forwardInbound 内部排除 self-echo 与 from-owner。
1015
- this.forwardInbound(fromAid, seq, payload);
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
+ }
1016
1049
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
1017
1050
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
1018
1051
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -1022,7 +1055,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1022
1055
  return;
1023
1056
  }
1024
1057
  // 记录入站消息加密状态,透传到出站 ReplyContext
1025
- const msgEncrypted = !!(msg.e2ee);
1058
+ const msgEncrypted = !!env.encrypted;
1026
1059
  if (!msgEncrypted)
1027
1060
  this.plaintextRecv++;
1028
1061
  // Detect @mentions
@@ -1058,6 +1091,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1058
1091
  });
1059
1092
  return;
1060
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
+ }
1061
1101
  // payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
1062
1102
  if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
1063
1103
  this.acknowledgeImmediately(messageId, seq);
@@ -1094,8 +1134,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1094
1134
  if (!data || typeof data !== 'object')
1095
1135
  return;
1096
1136
  const msg = data;
1097
- const groupId = msg.group_id ?? '';
1098
- 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 ?? '';
1099
1142
  const payload = msg.payload ?? '';
1100
1143
  const text = this.extractTextPayload(payload, groupId, senderAid);
1101
1144
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
@@ -1103,7 +1146,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1103
1146
  const seq = msg.seq;
1104
1147
  // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1105
1148
  // forwardInbound 内部排除 self-echo 与 from-owner。
1106
- this.forwardInbound(senderAid, seq, payload);
1149
+ this.forwardInbound(msg);
1107
1150
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1108
1151
  const payloadMentions = Array.isArray(payload?.mentions)
1109
1152
  ? payload.mentions.filter((m) => typeof m === 'string')
@@ -1125,7 +1168,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1125
1168
  const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
1126
1169
  if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
1127
1170
  this.acknowledgeImmediately(messageId, seq);
1128
- const msgEncryptedFast = !!(msg.e2ee);
1171
+ const msgEncryptedFast = !!env.encrypted;
1129
1172
  const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1130
1173
  const peerInfo = this.peerInfoCached(senderAid);
1131
1174
  const shortAid = this.getShortAid(senderAid);
@@ -1176,7 +1219,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1176
1219
  }
1177
1220
  }
1178
1221
  // 记录入站消息加密状态,透传到出站 ReplyContext
1179
- const msgEncrypted = !!(msg.e2ee);
1222
+ const msgEncrypted = !!env.encrypted;
1180
1223
  if (!msgEncrypted)
1181
1224
  this.plaintextRecv++;
1182
1225
  // dispatch_mode: 本地设置优先,fallback 到服务器参数
@@ -1287,6 +1330,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1287
1330
  mentions,
1288
1331
  mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
1289
1332
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1333
+ dispatchMode,
1290
1334
  images: inboundImages.length > 0 ? inboundImages : undefined,
1291
1335
  });
1292
1336
  }
@@ -1358,6 +1402,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1358
1402
  mentions: mentionObjects,
1359
1403
  mentionAids: event.mentionAids,
1360
1404
  replyContext,
1405
+ source: event.source,
1406
+ dispatchMode: event.dispatchMode,
1361
1407
  images: event.images,
1362
1408
  }).catch(err => {
1363
1409
  logger.error(`${this.logPrefix()} Message handler error:`, err);
@@ -1384,57 +1430,161 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1384
1430
  * 若消息发送方本身是 owner,则不转发给该 owner,但仍转发给其他 owner。
1385
1431
  * 调用点须在所有过滤逻辑之前,payload 为 SDK 解密后的明文。
1386
1432
  */
1387
- forwardInbound(from, seq, payload) {
1433
+ /**
1434
+ * 入站转发:把对端发来的消息原样转发给 owner。
1435
+ * data 为 SDK message.received / group.message_created 回调的整个对象,
1436
+ * 不拆解、不重组——SDK 信封结构变化不影响此处。
1437
+ */
1438
+ forwardInbound(data) {
1388
1439
  if (!this.connected || !this.client)
1389
1440
  return;
1390
1441
  const { observable, owners } = this.getObserverConfig();
1391
1442
  if (!observable || owners.length === 0)
1392
1443
  return;
1444
+ const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
1445
+ const from = env.from ?? '';
1393
1446
  if (this._aid && from === this._aid)
1394
1447
  return; // self-echo:已在出站转过
1395
1448
  // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
1396
1449
  const recipientOwners = owners.filter(o => o !== from);
1397
1450
  if (recipientOwners.length === 0)
1398
1451
  return;
1399
- this.emitForward('inbound', { from, to: this.config.aid, seq, payload }, recipientOwners);
1452
+ this.emitForward('inbound', data, recipientOwners);
1400
1453
  }
1401
1454
  /**
1402
- * 出站转发:Agent 经 AUN 真实发出的消息全部转发。
1403
- * to 为对端(私聊 AID / ID),payload 为实际发送的明文 payload
1404
- * to 本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1455
+ * 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
1456
+ * result SDK message.send / group.send SendResult(已 attach envelope + payload)。
1457
+ * 若对端本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1405
1458
  */
1406
- forwardOutbound(to, payload) {
1459
+ forwardOutbound(result) {
1407
1460
  if (!this.connected || !this.client)
1408
1461
  return;
1409
1462
  const { observable, owners } = this.getObserverConfig();
1410
1463
  if (!observable || owners.length === 0)
1411
1464
  return;
1412
- // 过滤:若 to 本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1465
+ const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
1466
+ const to = env.to ?? env.group_id ?? '';
1467
+ // 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1413
1468
  // 但仍转发给其他 owner。
1414
1469
  const recipientOwners = owners.filter(o => o !== to);
1415
1470
  if (recipientOwners.length === 0)
1416
1471
  return;
1417
- this.emitForward('outbound', { from: this.config.aid, to, payload }, recipientOwners);
1472
+ this.emitForward('outbound', result, recipientOwners);
1418
1473
  }
1419
- /** 实际投递 observer.forward 给每个 owner,外层一律明文。 */
1474
+ /**
1475
+ * 实际投递 observer.forward 给每个 owner,外层一律明文。
1476
+ * original 为 SDK 给到的原始对象(入站回调 data / 出站 SendResult),整体透传,
1477
+ * 不挑字段、不改字段——SDK 加任何字段都会自动一并转发给 owner。
1478
+ */
1420
1479
  emitForward(direction, original, owners) {
1421
1480
  const forwardPayload = {
1422
1481
  type: 'observer.forward',
1423
1482
  direction,
1424
1483
  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
- },
1484
+ original,
1432
1485
  };
1433
1486
  for (const ownerAid of owners) {
1434
1487
  this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
1435
1488
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
1436
1489
  }
1437
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
+ }
1438
1588
  handleEcho(event) {
1439
1589
  const ts = () => {
1440
1590
  const d = new Date();
@@ -1910,8 +2060,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1910
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 });
1911
2061
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1912
2062
  this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1913
- // Observer forward: outbound (group) — 转发实际发出的明文 payload
1914
- this.forwardOutbound(channelId, payload);
2063
+ // Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload
2064
+ this.forwardOutbound(result);
1915
2065
  }
1916
2066
  }
1917
2067
  else {
@@ -1925,8 +2075,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1925
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 });
1926
2076
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1927
2077
  this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1928
- // Observer forward: outbound (private) — 转发实际发出的明文 payload
1929
- this.forwardOutbound(targetAid, payload);
2078
+ // Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload
2079
+ this.forwardOutbound(result);
1930
2080
  }
1931
2081
  }
1932
2082
  return true;
@@ -1944,7 +2094,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1944
2094
  if (!result || !result.message_id) {
1945
2095
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
1946
2096
  }
1947
- this.forwardOutbound(channelId, payload);
2097
+ this.forwardOutbound(result);
1948
2098
  }
1949
2099
  else {
1950
2100
  this.trace('OUT', 'message.send.fallback', params);
@@ -1953,7 +2103,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1953
2103
  if (!result || !result.message_id) {
1954
2104
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
1955
2105
  }
1956
- this.forwardOutbound(targetAid, payload);
2106
+ this.forwardOutbound(result);
1957
2107
  }
1958
2108
  return true;
1959
2109
  }
@@ -2049,7 +2199,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2049
2199
  const tid = putRes?.thought_id;
2050
2200
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
2051
2201
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2052
- this.forwardOutbound(channelId, payload);
2202
+ this.forwardOutbound(putRes);
2053
2203
  if (thoughtText) {
2054
2204
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
2055
2205
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
@@ -2061,7 +2211,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2061
2211
  const tid = putRes?.thought_id;
2062
2212
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
2063
2213
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2064
- this.forwardOutbound(channelId, payload);
2214
+ this.forwardOutbound(putRes);
2065
2215
  if (thoughtText) {
2066
2216
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
2067
2217
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
@@ -2098,14 +2248,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2098
2248
  const result = await this.callAndTrace('group.send', params);
2099
2249
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
2100
2250
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
2101
- this.forwardOutbound(channelId, finalPayload);
2251
+ this.forwardOutbound(result);
2102
2252
  return mid;
2103
2253
  }
2104
2254
  else {
2105
2255
  params.to = targetAid;
2106
2256
  const result = await this.callAndTrace('message.send', params);
2107
2257
  logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
2108
- this.forwardOutbound(targetAid, finalPayload);
2258
+ this.forwardOutbound(result);
2109
2259
  return result?.message_id ?? null;
2110
2260
  }
2111
2261
  }
@@ -2222,11 +2372,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2222
2372
  ? !!(context.metadata.encrypted)
2223
2373
  : this.shouldEncrypt(encryptTarget);
2224
2374
  const params = { payload: filePayload, encrypt };
2375
+ let sendResult = null;
2225
2376
  try {
2226
2377
  if (isGroup) {
2227
2378
  params.group_id = channelId;
2228
2379
  this.trace('OUT', 'group.send.file', params);
2229
2380
  const result = await this.client.call('group.send', params);
2381
+ sendResult = result;
2230
2382
  const fileMid = result?.message?.message_id ?? result?.message_id;
2231
2383
  this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
2232
2384
  if (!fileMid) {
@@ -2237,6 +2389,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2237
2389
  params.to = fileTargetAid;
2238
2390
  this.trace('OUT', 'message.send.file', params);
2239
2391
  const result = await this.client.call('message.send', params);
2392
+ sendResult = result;
2240
2393
  this.trace('OUT', 'message.send.file.ok', { message_id: result?.message_id });
2241
2394
  if (!result || !result.message_id) {
2242
2395
  logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
@@ -2255,6 +2408,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2255
2408
  if (isGroup) {
2256
2409
  this.trace('OUT', 'group.send.file.fallback', params);
2257
2410
  const result = await this.client.call('group.send', params);
2411
+ sendResult = result;
2258
2412
  const fbMid = result?.message?.message_id ?? result?.message_id;
2259
2413
  this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
2260
2414
  if (!fbMid) {
@@ -2264,6 +2418,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2264
2418
  else {
2265
2419
  this.trace('OUT', 'message.send.file.fallback', params);
2266
2420
  const result = await this.client.call('message.send', params);
2421
+ sendResult = result;
2267
2422
  this.trace('OUT', 'message.send.file.fallback.ok', { message_id: result?.message_id });
2268
2423
  if (!result || !result.message_id) {
2269
2424
  logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
@@ -2275,7 +2430,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2275
2430
  }
2276
2431
  }
2277
2432
  logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
2278
- this.forwardOutbound(channelId, filePayload);
2433
+ if (sendResult)
2434
+ this.forwardOutbound(sendResult);
2279
2435
  return true;
2280
2436
  }
2281
2437
  catch (e) {
@@ -2365,7 +2521,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2365
2521
  const c = this.client;
2366
2522
  if (!c) {
2367
2523
  logger.debug(`${this.logPrefix()} ${label} skipped: client gone`);
2368
- return Promise.resolve();
2524
+ return Promise.resolve(null);
2369
2525
  }
2370
2526
  const encrypt = computeEncrypt();
2371
2527
  const params = { payload, encrypt };
@@ -2374,24 +2530,28 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2374
2530
  else
2375
2531
  params.to = statusTargetAid;
2376
2532
  this.trace('OUT', `${method}.task_${label}`, params);
2377
- return c.call(method, params).catch(e => {
2533
+ return c.call(method, params).catch((e) => {
2378
2534
  if (encrypt && e instanceof E2EEError) {
2379
2535
  this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
2380
2536
  logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
2381
2537
  const c2 = this.client;
2382
2538
  if (!c2)
2383
- return;
2539
+ return null;
2384
2540
  const fallbackParams = { ...params, encrypt: false };
2385
- return c2.call(method, fallbackParams).catch(e2 => {
2541
+ return c2.call(method, fallbackParams).catch((e2) => {
2386
2542
  logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
2543
+ return null;
2387
2544
  });
2388
2545
  }
2389
2546
  logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
2547
+ return null;
2390
2548
  });
2391
2549
  };
2392
2550
  const method = isGroup ? 'group.send' : 'message.send';
2393
- sendOne(method, statusPayload, 'status');
2394
- this.forwardOutbound(channelId, statusPayload);
2551
+ sendOne(method, statusPayload, 'status').then(result => {
2552
+ if (result)
2553
+ this.forwardOutbound(result);
2554
+ }).catch(() => { });
2395
2555
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
2396
2556
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
2397
2557
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
@@ -2619,247 +2779,200 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2619
2779
  // Plugin implementation
2620
2780
  export class AUNChannelPlugin {
2621
2781
  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;
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);
2668
2841
  }
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;
2842
+ else {
2843
+ await channel.sendStructured(channelId, aunPayload, replyCtx);
2683
2844
  }
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 },
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
+ })),
2689
2878
  };
2690
- if (ctx?.threadId)
2691
- aunPayload.thread_id = ctx.threadId;
2692
- if (envelope.chatmode === 'proactive') {
2693
- await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
2694
- }
2695
- else {
2696
- // interactive 模式不发 thought.put,只写入消息历史
2697
- await channel.sendStructured(channelId, 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);
2698
2889
  }
2699
- return;
2700
2890
  }
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);
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);
2770
2908
  }
2771
- return;
2772
2909
  }
2773
- case 'custom': {
2774
- const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
2775
- channel.sendCustomPayload(channelId, text);
2776
- return;
2910
+ else if (payload.fallbackText) {
2911
+ await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
2777
2912
  }
2778
- default:
2779
- logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
2913
+ return;
2780
2914
  }
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
- });
2915
+ case 'custom': {
2916
+ const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
2917
+ channel.sendCustomPayload(channelId, text);
2918
+ return;
2846
2919
  }
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;
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(),
2851
2966
  });
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];
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
+ };
2864
2977
  }
2865
2978
  }