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