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.
- package/CHANGELOG.md +38 -0
- package/README.md +26 -4
- package/dist/agents/kit-renderer.js +5 -1
- package/dist/agents/manifest-engine.js +108 -35
- package/dist/agents/message-renderer.js +2 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/channels/aun.js +212 -158
- package/dist/channels/feishu.js +10 -14
- package/dist/channels/wechat.js +8 -2
- package/dist/cli/agent.js +38 -10
- package/dist/cli/index.js +50 -8
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +162 -82
- package/dist/config-store.js +38 -7
- package/dist/core/cache/file-cache.js +216 -0
- package/dist/core/command-handler.js +291 -68
- package/dist/core/evolagent-registry.js +3 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/message/command-handler-agent-control.js +153 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/message-bridge.js +5 -3
- package/dist/core/message/message-processor.js +44 -36
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/model/model-scope.js +39 -6
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/evolclaw-config.js +11 -0
- package/dist/index.js +57 -2
- package/dist/ipc.js +6 -0
- package/dist/paths.js +7 -3
- package/dist/utils/media-cache.js +40 -1
- package/dist/utils/npm-ops.js +13 -3
- package/kits/templates/message-fragments/item.md +1 -1
- package/package.json +1 -1
package/dist/channels/aun.js
CHANGED
|
@@ -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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
852
|
-
*
|
|
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
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
*
|
|
1365
|
-
*
|
|
1402
|
+
* 出站转发:Agent 经 AUN 真实发出的消息全部转发。
|
|
1403
|
+
* to 为对端(私聊 AID / 群 ID),payload 为实际发送的明文 payload。
|
|
1404
|
+
* 若 to 本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
|
|
1366
1405
|
*/
|
|
1367
|
-
|
|
1406
|
+
forwardOutbound(to, payload) {
|
|
1368
1407
|
if (!this.connected || !this.client)
|
|
1369
1408
|
return;
|
|
1370
|
-
const
|
|
1371
|
-
if (!
|
|
1409
|
+
const { observable, owners } = this.getObserverConfig();
|
|
1410
|
+
if (!observable || owners.length === 0)
|
|
1372
1411
|
return;
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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),
|
|
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) {
|
package/dist/channels/feishu.js
CHANGED
|
@@ -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,
|
|
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
|
|
794
|
-
if (
|
|
795
|
-
logger.warn(
|
|
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
|
-
|
|
799
|
-
|
|
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
|
});
|