evolclaw 3.1.10 → 3.2.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.
@@ -7,7 +7,7 @@ import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
9
9
  import { resolvePaths, getPackageRoot, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
10
- import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
10
+ import { saveToUploads, sanitizeFileName, bufferToInboundImage } from '../utils/media-cache.js';
11
11
  import { appendAidEvent } from '../utils/instance-registry.js';
12
12
  import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
13
13
  import { chatDirPath } from '../core/session/session-fs-store.js';
@@ -58,6 +58,37 @@ function getEvolclawVersion() {
58
58
  }
59
59
  return _cachedVersion;
60
60
  }
61
+ /**
62
+ * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
63
+ *
64
+ * registerBridge 适配回调用它替代手抄字段——历史上手抄漏掉了
65
+ * sameDevice/sameNetwork/sameEgressIp,proximity 在此被吞,eck-debug 永远 false。
66
+ * 抽成纯函数后可单测锁字段,杜绝再漏。
67
+ */
68
+ export function aunOptsToInbound(opts, channel, channelType) {
69
+ return {
70
+ channel,
71
+ channelType,
72
+ channelId: opts.channelId,
73
+ selfAID: opts.selfAID,
74
+ groupId: opts.groupId,
75
+ content: opts.content,
76
+ chatType: opts.chatType || 'private',
77
+ peerId: opts.peerId || '',
78
+ peerName: opts.peerName,
79
+ peerType: opts.peerType,
80
+ sameDevice: opts.sameDevice,
81
+ sameNetwork: opts.sameNetwork,
82
+ sameEgressIp: opts.sameEgressIp,
83
+ messageId: opts.messageId,
84
+ mentions: opts.mentions,
85
+ mentionAids: opts.mentionAids,
86
+ threadId: opts.threadId,
87
+ replyContext: opts.replyContext,
88
+ source: opts.source,
89
+ images: opts.images,
90
+ };
91
+ }
61
92
  export class AUNChannel {
62
93
  config;
63
94
  client = null;
@@ -427,6 +458,7 @@ export class AUNChannel {
427
458
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
428
459
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
429
460
  seenMessages = new Map();
461
+ groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
430
462
  peerInfoCache = new Map();
431
463
  messageSeqMap = new Map(); // messageId → seq (for ack)
432
464
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
@@ -591,13 +623,16 @@ export class AUNChannel {
591
623
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
592
624
  this.handleIncomingPrivateMessage(data);
593
625
  });
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
- });
626
+ // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
627
+ if (!this.config.pureIdentity) {
628
+ client.on('group.message_created', (data) => {
629
+ 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 ?? '' : '';
632
+ logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
633
+ this.handleIncomingGroupMessage(data);
634
+ });
635
+ }
601
636
  client.on('connection.state', (data) => {
602
637
  // trace is handled inside handleConnectionState with throttling
603
638
  this.handleConnectionState(data);
@@ -626,11 +661,14 @@ export class AUNChannel {
626
661
  const d = data;
627
662
  logger.warn(`${this.logPrefix()} Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
628
663
  });
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}`);
633
- });
664
+ // pureIdentity(控制 AID):不注册 group 解密失败监听
665
+ if (!this.config.pureIdentity) {
666
+ client.on('group.message_undecryptable', (data) => {
667
+ this.trace('IN', 'group.message_undecryptable', data);
668
+ 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}`);
670
+ });
671
+ }
634
672
  // Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
635
673
  try {
636
674
  logger.info(`${this.logPrefix()} Authenticating as ${aidName}...`);
@@ -676,7 +714,8 @@ export class AUNChannel {
676
714
  this._aid = this.client.aid ?? undefined;
677
715
  const deviceId = this.client._device_id ?? '';
678
716
  this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
679
- this._selfName = this.loadSelfName(aidName);
717
+ // pureIdentity(控制 AID):无 agent.md,跳过自身 agent.md 拉取,省一次 404
718
+ this._selfName = this.config.pureIdentity ? undefined : this.loadSelfName(aidName);
680
719
  if (this._selfName && this.aidStatsCollector)
681
720
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
682
721
  this.connected = true;
@@ -697,7 +736,10 @@ export class AUNChannel {
697
736
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
698
737
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
699
738
  // Send welcome message to owner after first connection
700
- await this.sendWelcomeMessage();
739
+ // pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
740
+ if (!this.config.pureIdentity) {
741
+ await this.sendWelcomeMessage();
742
+ }
701
743
  }
702
744
  catch (e) {
703
745
  this.trace('OUT', 'client.connect.error', { error: String(e) });
@@ -848,43 +890,55 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
848
890
  }
849
891
  // ── Event handlers ──────────────────────────────────────────
850
892
  /**
851
- * 判断附件是否为图片,返回 MIME 类型(非图片返回空)。
852
- * 多重检测:附件元数据字段 → 文件名后缀 → 文件 magic bytes。
893
+ * 统一处理入站附件:下载 图片识别+base64 注入 → 拼接文本。
894
+ *
895
+ * - 图片:base64 注入视觉通道(不再追加 [文件: …] 文本行,避免冗余)
896
+ * - 非图片:拼 [文件: name → path],并提示用 Read 工具读取
897
+ *
898
+ * @param baseText 已解析的正文(私聊 text / 群聊 strippedText)
899
+ * @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
900
+ * @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
853
901
  */
854
- detectImageMime(att, filePath) {
855
- const extToMime = {
856
- '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
857
- '.gif': 'image/gif', '.webp': 'image/webp',
858
- };
859
- // 1. 附件元数据字段(content_type / mime_type / mimeType)
860
- const metaCt = (att?.content_type || att?.mime_type || att?.mimeType || '');
861
- if (typeof metaCt === 'string' && metaCt.startsWith('image/'))
862
- return metaCt;
863
- // 2. 文件名后缀
864
- const name = (att?.filename || att?.object_key || filePath || '').toLowerCase();
865
- for (const [ext, mime] of Object.entries(extToMime)) {
866
- if (name.endsWith(ext))
867
- return mime;
868
- }
869
- // 3. magic bytes
870
- try {
871
- const { openSync, readSync, closeSync } = require('node:fs');
872
- const fd = openSync(filePath, 'r');
873
- const head = Buffer.alloc(12);
874
- readSync(fd, head, 0, 12, 0);
875
- closeSync(fd);
876
- if (head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47)
877
- return 'image/png';
878
- if (head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
879
- return 'image/jpeg';
880
- if (head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46)
881
- return 'image/gif';
882
- if (head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46 &&
883
- head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50)
884
- return 'image/webp';
885
- }
886
- catch { /* not readable, skip */ }
887
- return '';
902
+ async processAttachments(payload, baseText, channelId, preCollected) {
903
+ const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
904
+ const images = [];
905
+ let finalText = baseText;
906
+ if (rawAttachments.length === 0 || !this.client) {
907
+ return { finalText, images };
908
+ }
909
+ const fileParts = [];
910
+ for (const att of rawAttachments) {
911
+ const filePath = await this.downloadAttachment(att, channelId);
912
+ if (!filePath)
913
+ continue;
914
+ const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
915
+ let img = null;
916
+ try {
917
+ const { readFileSync } = await import('node:fs');
918
+ img = await bufferToInboundImage(readFileSync(filePath), {
919
+ contentType: att.content_type, mimeType: att.mime_type, filename: name,
920
+ });
921
+ }
922
+ catch { /* read failed, treat as non-image file */ }
923
+ if (img) {
924
+ images.push(img);
925
+ // 图片已注入视觉通道,不追加 [文件: …] 文本行
926
+ }
927
+ else {
928
+ fileParts.push(`[文件: ${name} ${filePath}]`);
929
+ }
930
+ }
931
+ const parts = [];
932
+ if (baseText)
933
+ parts.push(baseText);
934
+ if (fileParts.length > 0) {
935
+ parts.push(...fileParts);
936
+ parts.push('请使用 Read 工具读取文件内容。');
937
+ }
938
+ if (parts.length > 0)
939
+ finalText = parts.join('\n\n');
940
+ logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
941
+ return { finalText, images };
888
942
  }
889
943
  async downloadAttachment(att, channelId) {
890
944
  const ownerAid = att.owner_aid || this._aid || '';
@@ -956,6 +1010,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
956
1010
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
957
1011
  const messageId = msg.message_id ?? '';
958
1012
  const seq = msg.seq;
1013
+ // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1014
+ // forwardInbound 内部排除 self-echo 与 from-owner。
1015
+ this.forwardInbound(fromAid, seq, payload);
959
1016
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
960
1017
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
961
1018
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -974,37 +1031,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
974
1031
  mentions.push(this._aid);
975
1032
  }
976
1033
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
977
- const rawAttachments = this.collectAllAttachments(payload);
978
- let finalText = text;
979
- const inboundImages = [];
980
- if (rawAttachments.length > 0 && this.client) {
981
- const fileParts = [];
982
- for (const att of rawAttachments) {
983
- const filePath = await this.downloadAttachment(att, fromAid);
984
- if (filePath) {
985
- const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
986
- const mime = this.detectImageMime(att, filePath);
987
- if (mime) {
988
- try {
989
- const { readFileSync } = await import('node:fs');
990
- inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
991
- }
992
- catch { /* fallback to file path */ }
993
- }
994
- fileParts.push(`[文件: ${name} → ${filePath}]`);
995
- }
996
- }
997
- if (fileParts.length > 0) {
998
- const parts = [];
999
- if (text)
1000
- parts.push(text);
1001
- parts.push(...fileParts);
1002
- if (inboundImages.length === 0)
1003
- parts.push('请使用 Read 工具读取文件内容。');
1004
- finalText = parts.join('\n\n');
1005
- }
1006
- logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
1007
- }
1034
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
1008
1035
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
1009
1036
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
1010
1037
  const chatId = fromAid;
@@ -1074,6 +1101,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1074
1101
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1075
1102
  const messageId = msg.message_id ?? '';
1076
1103
  const seq = msg.seq;
1104
+ // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1105
+ // forwardInbound 内部排除 self-echo 与 from-owner。
1106
+ this.forwardInbound(senderAid, seq, payload);
1077
1107
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1078
1108
  const payloadMentions = Array.isArray(payload?.mentions)
1079
1109
  ? payload.mentions.filter((m) => typeof m === 'string')
@@ -1210,35 +1240,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1210
1240
  ? ['all']
1211
1241
  : mentionedSelf && this._aid ? [this._aid] : [];
1212
1242
  // Process attachments
1213
- let finalText = strippedText;
1214
- const inboundImages = [];
1215
- if (hasAttachments && this.client) {
1216
- const fileParts = [];
1217
- for (const att of rawAttachments) {
1218
- const filePath = await this.downloadAttachment(att, groupId);
1219
- if (filePath) {
1220
- const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
1221
- const mime = this.detectImageMime(att, filePath);
1222
- if (mime) {
1223
- try {
1224
- const { readFileSync } = await import('node:fs');
1225
- inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
1226
- }
1227
- catch { /* fallback to file path */ }
1228
- }
1229
- fileParts.push(`[文件: ${name} → ${filePath}]`);
1230
- }
1231
- }
1232
- if (fileParts.length > 0) {
1233
- const parts = [];
1234
- if (strippedText)
1235
- parts.push(strippedText);
1236
- parts.push(...fileParts);
1237
- if (inboundImages.length === 0)
1238
- parts.push('请使用 Read 工具读取文件内容。');
1239
- finalText = parts.join('\n\n');
1240
- }
1241
- }
1243
+ const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
1242
1244
  const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1243
1245
  const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
1244
1246
  const shortAid = this.getShortAid(senderAid);
@@ -1262,6 +1264,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1262
1264
  logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1263
1265
  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 });
1264
1266
  this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1267
+ // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1268
+ // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1269
+ const renderMentionAids = Array.from(new Set([
1270
+ ...payloadMentions,
1271
+ ...this.extractMentionAidsFromText(text),
1272
+ ]));
1265
1273
  this.dispatchMessage({
1266
1274
  channelId: groupId,
1267
1275
  groupId,
@@ -1277,6 +1285,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1277
1285
  seq,
1278
1286
  threadId,
1279
1287
  mentions,
1288
+ mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
1280
1289
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1281
1290
  images: inboundImages.length > 0 ? inboundImages : undefined,
1282
1291
  });
@@ -1347,32 +1356,68 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1347
1356
  messageId: event.messageId,
1348
1357
  threadId: event.threadId,
1349
1358
  mentions: mentionObjects,
1359
+ mentionAids: event.mentionAids,
1350
1360
  replyContext,
1351
1361
  images: event.images,
1352
1362
  }).catch(err => {
1353
1363
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1354
1364
  });
1355
- // Observer forward: inbound
1356
- this.forwardToOwners('inbound', {
1357
- from: event.userId || event.channelId || '',
1358
- to: this.config.aid,
1359
- seq: event.seq,
1360
- payload: { type: 'text', text: event.text },
1361
- });
1365
+ }
1366
+ // ── 观察者模式(Observer Mode) ──────────────────────────────
1367
+ //
1368
+ // observable=true 时,Agent 收发的每条 AUN 消息(原始信封 + 解密后明文
1369
+ // payload)镜像一份给 owners[]。入站在所有过滤之前转发;出站在真实发送
1370
+ // 成功后转发。外层一律明文。详见 docs/observer-mode-design.md。
1371
+ // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
1372
+ // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
1373
+ observerConfigResolver;
1374
+ /** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
1375
+ setObserverConfigResolver(fn) {
1376
+ this.observerConfigResolver = fn;
1377
+ }
1378
+ /** 读取 observable 开关 + owners;无 resolver(如未接入 daemon)时视为关闭。 */
1379
+ getObserverConfig() {
1380
+ return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
1381
+ }
1382
+ /**
1383
+ * 入站转发:到达本 AID 的消息全部转发,排除 self-echo。
1384
+ * 若消息发送方本身是 owner,则不转发给该 owner,但仍转发给其他 owner。
1385
+ * 调用点须在所有过滤逻辑之前,payload 为 SDK 解密后的明文。
1386
+ */
1387
+ forwardInbound(from, seq, payload) {
1388
+ if (!this.connected || !this.client)
1389
+ return;
1390
+ const { observable, owners } = this.getObserverConfig();
1391
+ if (!observable || owners.length === 0)
1392
+ return;
1393
+ if (this._aid && from === this._aid)
1394
+ return; // self-echo:已在出站转过
1395
+ // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
1396
+ const recipientOwners = owners.filter(o => o !== from);
1397
+ if (recipientOwners.length === 0)
1398
+ return;
1399
+ this.emitForward('inbound', { from, to: this.config.aid, seq, payload }, recipientOwners);
1362
1400
  }
1363
1401
  /**
1364
- * 观察者模式转发:将消息副本以 observer.forward 格式发给所有 owners。
1365
- * 仅在 AgentConfig.observable === true 时执行;owners 为空或无法加载配置时静默跳过。
1402
+ * 出站转发:Agent AUN 真实发出的消息全部转发。
1403
+ * to 为对端(私聊 AID / ID),payload 为实际发送的明文 payload。
1404
+ * 若 to 本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1366
1405
  */
1367
- forwardToOwners(direction, original) {
1406
+ forwardOutbound(to, payload) {
1368
1407
  if (!this.connected || !this.client)
1369
1408
  return;
1370
- const agentConfig = loadAgent(this.config.aid);
1371
- if (!agentConfig?.observable)
1409
+ const { observable, owners } = this.getObserverConfig();
1410
+ if (!observable || owners.length === 0)
1372
1411
  return;
1373
- const owners = agentConfig.owners ?? [];
1374
- if (owners.length === 0)
1412
+ // 过滤:若 to 本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1413
+ // 但仍转发给其他 owner。
1414
+ const recipientOwners = owners.filter(o => o !== to);
1415
+ if (recipientOwners.length === 0)
1375
1416
  return;
1417
+ this.emitForward('outbound', { from: this.config.aid, to, payload }, recipientOwners);
1418
+ }
1419
+ /** 实际投递 observer.forward 给每个 owner,外层一律明文。 */
1420
+ emitForward(direction, original, owners) {
1376
1421
  const forwardPayload = {
1377
1422
  type: 'observer.forward',
1378
1423
  direction,
@@ -1386,8 +1431,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1386
1431
  },
1387
1432
  };
1388
1433
  for (const ownerAid of owners) {
1389
- const encrypt = this.shouldEncrypt(ownerAid);
1390
- this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
1434
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
1391
1435
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
1392
1436
  }
1393
1437
  }
@@ -1866,12 +1910,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1866
1910
  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 });
1867
1911
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1868
1912
  this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1869
- // Observer forward: outbound (group)
1870
- this.forwardToOwners('outbound', {
1871
- from: this.config.aid,
1872
- to: channelId,
1873
- payload: { type: 'text', text: finalText },
1874
- });
1913
+ // Observer forward: outbound (group) — 转发实际发出的明文 payload
1914
+ this.forwardOutbound(channelId, payload);
1875
1915
  }
1876
1916
  }
1877
1917
  else {
@@ -1885,12 +1925,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1885
1925
  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 });
1886
1926
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1887
1927
  this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1888
- // Observer forward: outbound (private)
1889
- this.forwardToOwners('outbound', {
1890
- from: this.config.aid,
1891
- to: targetAid,
1892
- payload: { type: 'text', text: finalText },
1893
- });
1928
+ // Observer forward: outbound (private) — 转发实际发出的明文 payload
1929
+ this.forwardOutbound(targetAid, payload);
1894
1930
  }
1895
1931
  }
1896
1932
  return true;
@@ -1908,6 +1944,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1908
1944
  if (!result || !result.message_id) {
1909
1945
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
1910
1946
  }
1947
+ this.forwardOutbound(channelId, payload);
1911
1948
  }
1912
1949
  else {
1913
1950
  this.trace('OUT', 'message.send.fallback', params);
@@ -1916,6 +1953,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1916
1953
  if (!result || !result.message_id) {
1917
1954
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
1918
1955
  }
1956
+ this.forwardOutbound(targetAid, payload);
1919
1957
  }
1920
1958
  return true;
1921
1959
  }
@@ -2011,6 +2049,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2011
2049
  const tid = putRes?.thought_id;
2012
2050
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
2013
2051
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2052
+ this.forwardOutbound(channelId, payload);
2014
2053
  if (thoughtText) {
2015
2054
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
2016
2055
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
@@ -2022,6 +2061,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2022
2061
  const tid = putRes?.thought_id;
2023
2062
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
2024
2063
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2064
+ this.forwardOutbound(channelId, payload);
2025
2065
  if (thoughtText) {
2026
2066
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
2027
2067
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
@@ -2058,12 +2098,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2058
2098
  const result = await this.callAndTrace('group.send', params);
2059
2099
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
2060
2100
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
2101
+ this.forwardOutbound(channelId, finalPayload);
2061
2102
  return mid;
2062
2103
  }
2063
2104
  else {
2064
2105
  params.to = targetAid;
2065
2106
  const result = await this.callAndTrace('message.send', params);
2066
2107
  logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
2108
+ this.forwardOutbound(targetAid, finalPayload);
2067
2109
  return result?.message_id ?? null;
2068
2110
  }
2069
2111
  }
@@ -2233,6 +2275,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2233
2275
  }
2234
2276
  }
2235
2277
  logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
2278
+ this.forwardOutbound(channelId, filePayload);
2236
2279
  return true;
2237
2280
  }
2238
2281
  catch (e) {
@@ -2348,6 +2391,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2348
2391
  };
2349
2392
  const method = isGroup ? 'group.send' : 'message.send';
2350
2393
  sendOne(method, statusPayload, 'status');
2394
+ this.forwardOutbound(channelId, statusPayload);
2351
2395
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
2352
2396
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
2353
2397
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
@@ -2545,6 +2589,32 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2545
2589
  const result = await agentmdSync(aid, { store: this.store ?? undefined });
2546
2590
  return result.content ?? '';
2547
2591
  }
2592
+ /**
2593
+ * 取群显示名(group.get → group.name),进程内缓存。
2594
+ * 走长连接 callAndTrace,失败/未连接返回 undefined —— 绝不抛出阻塞消息处理。
2595
+ */
2596
+ async getGroupName(groupId) {
2597
+ if (!groupId)
2598
+ return undefined;
2599
+ const cached = this.groupNameCache.get(groupId);
2600
+ if (cached !== undefined)
2601
+ return cached || undefined;
2602
+ if (!this.client)
2603
+ return undefined;
2604
+ try {
2605
+ const result = await this.callAndTrace('group.get', { group_id: groupId });
2606
+ const name = result?.group?.name;
2607
+ if (typeof name === 'string' && name) {
2608
+ this.groupNameCache.set(groupId, name);
2609
+ return name;
2610
+ }
2611
+ this.groupNameCache.set(groupId, ''); // 负缓存:避免反复 RPC(空串视为无名)
2612
+ return undefined;
2613
+ }
2614
+ catch {
2615
+ return undefined; // 不写缓存,下次仍可重试
2616
+ }
2617
+ }
2548
2618
  }
2549
2619
  // Plugin implementation
2550
2620
  export class AUNChannelPlugin {
@@ -2709,7 +2779,8 @@ export class AUNChannelPlugin {
2709
2779
  logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
2710
2780
  }
2711
2781
  }, acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); }, onInteraction: (cb) => { channel.interactionCallback = cb; }, uploadAgentMd: (content) => channel.uploadAgentMd(content),
2712
- downloadAgentMd: (aid) => channel.downloadAgentMd(aid), _selfAid: () => channel.getStatus().aid,
2782
+ downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
2783
+ getGroupName: (groupId) => channel.getGroupName(groupId), _selfAid: () => channel.getStatus().aid,
2713
2784
  _selfName: () => channel.getSelfName(),
2714
2785
  };
2715
2786
  const policy = {
@@ -2756,24 +2827,7 @@ export class AUNChannelPlugin {
2756
2827
  onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
2757
2828
  registerBridge(bridge, channelType) {
2758
2829
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
2759
- handler({
2760
- channel: adapter.channelName,
2761
- channelType,
2762
- channelId: opts.channelId,
2763
- selfAID: opts.selfAID,
2764
- groupId: opts.groupId,
2765
- content: opts.content,
2766
- chatType: opts.chatType || 'private',
2767
- peerId: opts.peerId || '',
2768
- peerName: opts.peerName,
2769
- peerType: opts.peerType,
2770
- messageId: opts.messageId,
2771
- mentions: opts.mentions,
2772
- threadId: opts.threadId,
2773
- replyContext: opts.replyContext,
2774
- source: opts.source,
2775
- images: opts.images,
2776
- });
2830
+ handler(aunOptsToInbound(opts, adapter.channelName, channelType));
2777
2831
  }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2778
2832
  },
2779
2833
  registerHooks(ctx) {
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import imageType from 'image-type';
4
- import { sanitizeFileName, saveToUploads, validateImage } from '../utils/media-cache.js';
4
+ import { sanitizeFileName, saveToUploads, bufferToInboundImage } from '../utils/media-cache.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { hasRichContent, renderAllRichContent, checkDependencies } from '../utils/rich-content-renderer.js';
7
7
  import { formatItemsAsText } from '../core/message/items-formatter.js';
@@ -207,7 +207,7 @@ export class FeishuChannel {
207
207
  // 清理残留的 mention 占位符(@_user_N 代表机器人)
208
208
  content = content.replace(/@_user_\d+/g, '').trim();
209
209
  const finalContent = quotedText + content;
210
- await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, mentions: mentions.length > 0 ? mentions : undefined, threadId, rootId, chatType });
210
+ await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, mentions: mentions.length > 0 ? mentions : undefined, mentionAids: mentions.length > 0 ? mentions.map((m) => m.userId) : undefined, threadId, rootId, chatType });
211
211
  }
212
212
  // 处理图片消息
213
213
  else if (msg.message_type === 'image') {
@@ -789,18 +789,14 @@ export class FeishuChannel {
789
789
  logger.warn('[Feishu] Empty response from image download');
790
790
  return null;
791
791
  }
792
- // 统一图片验证(类型白名单 + 大小限制)
793
- const result = await validateImage(buffer);
794
- if (result.mime === null) {
795
- logger.warn(`[Feishu] Image validation failed: ${result.reason}`);
792
+ // 统一图片识别 + base64 注入(magic bytes → 元数据 → 后缀)
793
+ const img = await bufferToInboundImage(buffer);
794
+ if (!img) {
795
+ logger.warn('[Feishu] Image validation failed (not a supported image)');
796
796
  return null;
797
797
  }
798
- const base64Data = buffer.toString('base64');
799
- logger.debug('[Feishu] Image downloaded successfully, type:', result.mime, 'size:', base64Data.length);
800
- return {
801
- data: base64Data,
802
- mimeType: result.mime
803
- };
798
+ logger.debug('[Feishu] Image downloaded successfully, type:', img.mimeType, 'size:', img.data.length);
799
+ return img;
804
800
  }
805
801
  logger.error('[Feishu] Image download failed: no valid method');
806
802
  return null;
@@ -1523,12 +1519,12 @@ export class FeishuChannelPlugin {
1523
1519
  disconnect: () => channel.disconnect(),
1524
1520
  onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
1525
1521
  registerBridge(bridge, channelType) {
1526
- bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1522
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
1527
1523
  await handler({
1528
1524
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1529
1525
  selfAID: inst.agentName,
1530
1526
  chatType: chatType || 'private',
1531
- peerId: peerId || '', peerName, messageId, mentions, threadId,
1527
+ peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
1532
1528
  replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1533
1529
  source,
1534
1530
  });