evolclaw 3.2.0 → 3.4.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 +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- 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-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- package/dist/core/message/im-renderer.js +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +86 -26
- 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 +334 -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/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- 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_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
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';
|
|
@@ -87,6 +88,7 @@ export function aunOptsToInbound(opts, channel, channelType) {
|
|
|
87
88
|
replyContext: opts.replyContext,
|
|
88
89
|
source: opts.source,
|
|
89
90
|
images: opts.images,
|
|
91
|
+
dispatchMode: opts.dispatchMode,
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
94
|
export class AUNChannel {
|
|
@@ -115,14 +117,16 @@ export class AUNChannel {
|
|
|
115
117
|
// 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
|
|
116
118
|
const d = (data && typeof data === 'object') ? data : {};
|
|
117
119
|
const payload = d.payload ?? {};
|
|
120
|
+
// 入站事件(IN)路由字段在 d.envelope.*(SDK 0.5.*);出站 params(OUT)字段在顶层。两者兼顾。
|
|
121
|
+
const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
|
|
118
122
|
const topContext = {
|
|
119
123
|
self_aid: this._aid ?? this.config.aid,
|
|
120
124
|
};
|
|
121
125
|
// peer / group 识别
|
|
122
|
-
const peerAid = d.to ?? d.from ?? d.sender_aid ?? payload.to;
|
|
126
|
+
const peerAid = env.to ?? env.from ?? d.to ?? d.from ?? d.sender_aid ?? payload.to;
|
|
123
127
|
if (peerAid)
|
|
124
128
|
topContext.peer_aid = peerAid;
|
|
125
|
-
const groupId = d.group_id ?? payload.group_id;
|
|
129
|
+
const groupId = env.group_id ?? d.group_id ?? payload.group_id;
|
|
126
130
|
if (groupId)
|
|
127
131
|
topContext.group_id = groupId;
|
|
128
132
|
// task_id / chatmode(message.send / thought.put / status 都可能有)
|
|
@@ -175,6 +179,7 @@ export class AUNChannel {
|
|
|
175
179
|
* - 数字群号:{group_no}.{issuer}(如 11117.agentid.pub)
|
|
176
180
|
* - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
|
|
177
181
|
*/
|
|
182
|
+
/** 判断 channelId 是否群组 ID(public:plugin adapter 闭包需调用) */
|
|
178
183
|
isGroupId(id) {
|
|
179
184
|
return (id.startsWith('group.') && id.includes('/'))
|
|
180
185
|
|| /^\d+\./.test(id)
|
|
@@ -201,6 +206,20 @@ export class AUNChannel {
|
|
|
201
206
|
const name = cached?.name;
|
|
202
207
|
return name && name !== short ? `${short}(${name})` : short;
|
|
203
208
|
}
|
|
209
|
+
notifyCardActionFailure(channelId, text, operatorId, threadId) {
|
|
210
|
+
if (!channelId) {
|
|
211
|
+
logger.warn(`${this.logPrefix()} Card action failure without channelId: ${text}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const context = {};
|
|
215
|
+
if (threadId)
|
|
216
|
+
context.threadId = threadId;
|
|
217
|
+
if (operatorId && this.isGroupId(channelId))
|
|
218
|
+
context.peerId = operatorId;
|
|
219
|
+
void this.sendMessage(channelId, text, context).catch((err) => {
|
|
220
|
+
logger.error(`${this.logPrefix()} Failed to send card action error:`, err);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
204
223
|
extractTextPayload(payload, channelId, senderAid) {
|
|
205
224
|
if (typeof payload === 'string')
|
|
206
225
|
return payload;
|
|
@@ -215,27 +234,25 @@ export class AUNChannel {
|
|
|
215
234
|
if (cardInfo) {
|
|
216
235
|
const actionValue = typeof obj.value === 'string' ? obj.value
|
|
217
236
|
: typeof obj.action_value === 'string' ? obj.action_value : text;
|
|
237
|
+
const threadId = typeof obj.thread_id === 'string' ? obj.thread_id : undefined;
|
|
238
|
+
// 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
|
|
239
|
+
// payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
|
|
240
|
+
// 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
|
|
241
|
+
const cardClickerAid = senderAid || channelId || '';
|
|
218
242
|
if (cardInfo.isCommandCard) {
|
|
219
243
|
// CommandCard:action_value 是完整 slash 命令,构造伪入站消息
|
|
220
244
|
this.cardMessageIdMap.delete(cardMsgId);
|
|
221
245
|
if (this.messageHandler && actionValue.startsWith('/')) {
|
|
222
246
|
const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|| (typeof obj.user_id === 'string' && obj.user_id)
|
|
228
|
-
|| senderAid
|
|
229
|
-
|| cardInfo.initiatorAid
|
|
230
|
-
|| channelId || '';
|
|
231
|
-
// Initiator 校验:群聊中仅卡片发起者可操作(与飞书行为对齐)
|
|
232
|
-
if (cardInfo.initiatorAid && cardClickerAid
|
|
233
|
-
&& cardClickerAid !== cardInfo.initiatorAid
|
|
234
|
-
&& !this.isGroupId(cardClickerAid)) {
|
|
247
|
+
// Initiator 校验:仅群聊需要(私聊信道一对一,点击者恒为对端 = initiator)。
|
|
248
|
+
// 身份只信认证信封提取的 cardClickerAid,非 payload 自报。
|
|
249
|
+
if (chatType === 'group' && cardInfo.initiatorAid && cardClickerAid
|
|
250
|
+
&& cardClickerAid !== cardInfo.initiatorAid) {
|
|
235
251
|
logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
|
|
252
|
+
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
|
|
236
253
|
return '';
|
|
237
254
|
}
|
|
238
|
-
this.messageHandler({
|
|
255
|
+
Promise.resolve(this.messageHandler({
|
|
239
256
|
channelId: channelId || '',
|
|
240
257
|
chatType,
|
|
241
258
|
content: actionValue,
|
|
@@ -243,8 +260,18 @@ export class AUNChannel {
|
|
|
243
260
|
peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
|
|
244
261
|
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
245
262
|
source: 'card-trigger',
|
|
263
|
+
})).catch((err) => {
|
|
264
|
+
logger.error(`${this.logPrefix()} CommandCard handler failed: action=${actionValue} mid=${cardMsgId}`, err);
|
|
265
|
+
const message = err instanceof Error && err.message ? err.message : String(err || '未知错误');
|
|
266
|
+
this.notifyCardActionFailure(channelId, `❌ 操作失败: ${message}`, cardClickerAid, threadId);
|
|
246
267
|
});
|
|
247
268
|
}
|
|
269
|
+
else if (!this.messageHandler) {
|
|
270
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
|
|
274
|
+
}
|
|
248
275
|
}
|
|
249
276
|
else {
|
|
250
277
|
// ActionInteraction:走 interactionCallback → InteractionRouter
|
|
@@ -256,12 +283,22 @@ export class AUNChannel {
|
|
|
256
283
|
id: cardInfo.requestId,
|
|
257
284
|
action: actionValue,
|
|
258
285
|
values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
|
|
286
|
+
operatorId: cardClickerAid || undefined,
|
|
259
287
|
});
|
|
260
288
|
}
|
|
289
|
+
else {
|
|
290
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
|
|
291
|
+
}
|
|
261
292
|
}
|
|
262
293
|
}
|
|
294
|
+
else if (this.ownedCardMsgIds.has(cardMsgId)) {
|
|
295
|
+
// 本 agent 发出的卡片,但 entry 已过期(20min TTL)
|
|
296
|
+
logger.debug(`${this.logPrefix()} action_card_reply expired: cardMsgId=${cardMsgId}`);
|
|
297
|
+
this.notifyCardActionFailure(channelId, '⚠️ 卡片已失效,请重新发起');
|
|
298
|
+
}
|
|
263
299
|
else {
|
|
264
|
-
|
|
300
|
+
// 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
|
|
301
|
+
logger.debug(`${this.logPrefix()} action_card_reply ignored (not owned): cardMsgId=${cardMsgId}`);
|
|
265
302
|
}
|
|
266
303
|
// 始终返回空字符串,阻止消息分发给 agent
|
|
267
304
|
return '';
|
|
@@ -402,6 +439,13 @@ export class AUNChannel {
|
|
|
402
439
|
.replace(/[ \t]+/g, ' ')
|
|
403
440
|
.trim();
|
|
404
441
|
}
|
|
442
|
+
/** 剥离正文中所有 @aid(用于命令判定 + 命令消息进 agent 前的清理)。 */
|
|
443
|
+
stripAllMentions(text) {
|
|
444
|
+
return text
|
|
445
|
+
.replace(/(^|\s)@[\w.-]+(?=$|\s|[.,!?;:,。!?;:]|[\u4e00-\u9fff])/g, '$1')
|
|
446
|
+
.replace(/[ \t]+/g, ' ')
|
|
447
|
+
.trim();
|
|
448
|
+
}
|
|
405
449
|
extractMentionAids(mentions) {
|
|
406
450
|
const aids = [];
|
|
407
451
|
for (const m of mentions) {
|
|
@@ -468,6 +512,8 @@ export class AUNChannel {
|
|
|
468
512
|
interactionCallback;
|
|
469
513
|
// action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
|
|
470
514
|
cardMessageIdMap = new Map();
|
|
515
|
+
/** 本 agent 曾发出过的卡片 msgId(只增不删,用于区分"过期失效"vs"他人发的卡") */
|
|
516
|
+
ownedCardMsgIds = new Set();
|
|
471
517
|
dispatchModeResolver;
|
|
472
518
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
473
519
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
@@ -477,6 +523,8 @@ export class AUNChannel {
|
|
|
477
523
|
static MENU_REQUEST_TYPES = new Set([
|
|
478
524
|
'menu.list', 'menu.query', 'menu.options', 'menu.update', 'menu.action',
|
|
479
525
|
]);
|
|
526
|
+
/** 观察者插话请求类型(owner → agent.AID)。详见 docs/observer-insert-design.md。 */
|
|
527
|
+
static INJECT_REQUEST_TYPE = 'observer.inject';
|
|
480
528
|
// Reconnect state
|
|
481
529
|
// SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
|
|
482
530
|
// 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
|
|
@@ -504,6 +552,7 @@ export class AUNChannel {
|
|
|
504
552
|
// AID 连接状态(供 status 命令聚合展示)
|
|
505
553
|
aidState;
|
|
506
554
|
aidStatsCollector;
|
|
555
|
+
outboxInFlight = new Set();
|
|
507
556
|
constructor(config) {
|
|
508
557
|
this.config = config;
|
|
509
558
|
this.agentDir = agentDirPath(config.aid);
|
|
@@ -627,8 +676,9 @@ export class AUNChannel {
|
|
|
627
676
|
if (!this.config.pureIdentity) {
|
|
628
677
|
client.on('group.message_created', (data) => {
|
|
629
678
|
this.trace('IN', 'group.message_created', data);
|
|
630
|
-
const
|
|
631
|
-
const
|
|
679
|
+
const env = (data && typeof data === 'object') ? data.envelope ?? {} : {};
|
|
680
|
+
const gid = env.group_id ?? '';
|
|
681
|
+
const sender = env.from ?? '';
|
|
632
682
|
logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
|
|
633
683
|
this.handleIncomingGroupMessage(data);
|
|
634
684
|
});
|
|
@@ -659,14 +709,34 @@ export class AUNChannel {
|
|
|
659
709
|
client.on('message.undecryptable', (data) => {
|
|
660
710
|
this.trace('IN', 'message.undecryptable', data);
|
|
661
711
|
const d = data;
|
|
662
|
-
|
|
712
|
+
const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
|
|
713
|
+
logger.warn(`${this.logPrefix()} Message undecryptable: from=${env.from} mid=${d.message_id} err=${d._decrypt_error}`);
|
|
663
714
|
});
|
|
664
715
|
// pureIdentity(控制 AID):不注册 group 解密失败监听
|
|
665
716
|
if (!this.config.pureIdentity) {
|
|
666
717
|
client.on('group.message_undecryptable', (data) => {
|
|
667
718
|
this.trace('IN', 'group.message_undecryptable', data);
|
|
668
719
|
const d = data;
|
|
669
|
-
|
|
720
|
+
const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
|
|
721
|
+
logger.warn(`${this.logPrefix()} Group message undecryptable: group=${env.group_id} from=${env.from} mid=${d.message_id} err=${d._decrypt_error}`);
|
|
722
|
+
});
|
|
723
|
+
// 群消息撤回(SDK 0.4.10 在线 push 通道):与私聊 message.recalled 同构,
|
|
724
|
+
// 逐个 message_id 交给 recallHandler → msgBridge.cancel(排队中删除 / 处理中中断)。
|
|
725
|
+
client.on('group.message_recalled', (data) => {
|
|
726
|
+
this.trace('IN', 'group.message_recalled', data);
|
|
727
|
+
if (data && typeof data === 'object') {
|
|
728
|
+
const d = data;
|
|
729
|
+
const env = (d.envelope && typeof d.envelope === 'object') ? d.envelope : {};
|
|
730
|
+
const ids = d.message_ids;
|
|
731
|
+
if (Array.isArray(ids)) {
|
|
732
|
+
for (const id of ids) {
|
|
733
|
+
if (typeof id === 'string') {
|
|
734
|
+
logger.info(`${this.logPrefix()} Group message recalled: group=${env.group_id ?? ''} mid=${id}`);
|
|
735
|
+
this.recallHandler?.(id);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
670
740
|
});
|
|
671
741
|
}
|
|
672
742
|
// Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
|
|
@@ -1004,15 +1074,23 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1004
1074
|
if (!data || typeof data !== 'object')
|
|
1005
1075
|
return;
|
|
1006
1076
|
const msg = data;
|
|
1007
|
-
|
|
1077
|
+
// SDK 0.5.* 移除了顶层 from/to/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1078
|
+
// message_id / seq / payload / same_* 等仍是顶层独立字段,不在 envelope 内。
|
|
1079
|
+
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1080
|
+
const fromAid = env.from ?? '';
|
|
1008
1081
|
const payload = msg.payload ?? '';
|
|
1009
|
-
const text = this.extractTextPayload(payload, fromAid);
|
|
1082
|
+
const text = this.extractTextPayload(payload, fromAid, fromAid);
|
|
1010
1083
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1011
1084
|
const messageId = msg.message_id ?? '';
|
|
1012
1085
|
const seq = msg.seq;
|
|
1013
1086
|
// Observer forward (inbound):在所有过滤之前转发原始明文 payload。
|
|
1014
1087
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1015
|
-
|
|
1088
|
+
// 显式排除 observer.inject:它是 owner 对本 agent 的控制消息,不应镜像给观察者
|
|
1089
|
+
// (即便日后 from-owner 排除规则调整,也不会泄漏)。
|
|
1090
|
+
const inboundType = (payload && typeof payload === 'object') ? payload.type : undefined;
|
|
1091
|
+
if (inboundType !== AUNChannel.INJECT_REQUEST_TYPE) {
|
|
1092
|
+
this.forwardInbound(msg);
|
|
1093
|
+
}
|
|
1016
1094
|
// 回声过滤:自己发出的消息会被 gateway fanout 回来,
|
|
1017
1095
|
// 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
|
|
1018
1096
|
const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
|
|
@@ -1022,7 +1100,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1022
1100
|
return;
|
|
1023
1101
|
}
|
|
1024
1102
|
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
1025
|
-
const msgEncrypted = !!
|
|
1103
|
+
const msgEncrypted = !!env.encrypted;
|
|
1026
1104
|
if (!msgEncrypted)
|
|
1027
1105
|
this.plaintextRecv++;
|
|
1028
1106
|
// Detect @mentions
|
|
@@ -1058,6 +1136,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1058
1136
|
});
|
|
1059
1137
|
return;
|
|
1060
1138
|
}
|
|
1139
|
+
// observer.inject:owner 插话。鉴权 from∈owners 后,以 target.channel_id 选 agent↔对端 会话,
|
|
1140
|
+
// observer 插话(v0.3):只落盘到 pending-hints,不进 Agent、不回 owner。详见 docs/observer-insert-design.md。
|
|
1141
|
+
if (p2pPayloadType === AUNChannel.INJECT_REQUEST_TYPE) {
|
|
1142
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1143
|
+
this.handleObserverInject(fromAid, payload, displayName, peerIdentity.type);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1061
1146
|
// payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
|
|
1062
1147
|
if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
|
|
1063
1148
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -1068,10 +1153,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1068
1153
|
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1069
1154
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
1070
1155
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
1071
|
-
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
|
|
1156
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode, isSystemP2P ? 'notify' : 'send');
|
|
1072
1157
|
const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
|
|
1073
1158
|
if (threadId)
|
|
1074
1159
|
replyContext.threadId = threadId;
|
|
1160
|
+
replyContext.peerId = fromAid;
|
|
1161
|
+
if (messageId)
|
|
1162
|
+
replyContext.replyToMessageId = messageId;
|
|
1075
1163
|
this.dispatchMessage({
|
|
1076
1164
|
channelId: chatId,
|
|
1077
1165
|
userId: fromAid,
|
|
@@ -1094,8 +1182,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1094
1182
|
if (!data || typeof data !== 'object')
|
|
1095
1183
|
return;
|
|
1096
1184
|
const msg = data;
|
|
1097
|
-
|
|
1098
|
-
|
|
1185
|
+
// SDK 0.5.* 移除了顶层 from/sender_aid/group_id/encrypted 等别名,统一从 msg.envelope.* 读取。
|
|
1186
|
+
// message_id / seq / payload / same_* / dispatch_mode 等仍是顶层独立字段,不在 envelope 内。
|
|
1187
|
+
const env = (msg.envelope && typeof msg.envelope === 'object') ? msg.envelope : {};
|
|
1188
|
+
const groupId = env.group_id ?? '';
|
|
1189
|
+
const senderAid = env.from ?? '';
|
|
1099
1190
|
const payload = msg.payload ?? '';
|
|
1100
1191
|
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
1101
1192
|
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
@@ -1103,7 +1194,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1103
1194
|
const seq = msg.seq;
|
|
1104
1195
|
// Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
|
|
1105
1196
|
// forwardInbound 内部排除 self-echo 与 from-owner。
|
|
1106
|
-
this.forwardInbound(
|
|
1197
|
+
this.forwardInbound(msg);
|
|
1107
1198
|
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
1108
1199
|
const payloadMentions = Array.isArray(payload?.mentions)
|
|
1109
1200
|
? payload.mentions.filter((m) => typeof m === 'string')
|
|
@@ -1125,7 +1216,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1125
1216
|
const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
1126
1217
|
if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
|
|
1127
1218
|
this.acknowledgeImmediately(messageId, seq);
|
|
1128
|
-
const msgEncryptedFast = !!
|
|
1219
|
+
const msgEncryptedFast = !!env.encrypted;
|
|
1129
1220
|
const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1130
1221
|
const peerInfo = this.peerInfoCached(senderAid);
|
|
1131
1222
|
const shortAid = this.getShortAid(senderAid);
|
|
@@ -1176,7 +1267,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1176
1267
|
}
|
|
1177
1268
|
}
|
|
1178
1269
|
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
1179
|
-
const msgEncrypted = !!
|
|
1270
|
+
const msgEncrypted = !!env.encrypted;
|
|
1180
1271
|
if (!msgEncrypted)
|
|
1181
1272
|
this.plaintextRecv++;
|
|
1182
1273
|
// dispatch_mode: 本地设置优先,fallback 到服务器参数
|
|
@@ -1194,7 +1285,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1194
1285
|
// 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1195
1286
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
1196
1287
|
const hasEvolClawTraceGroup = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
1197
|
-
|
|
1288
|
+
const isEchoMsg = /echo/i.test(firstLineGroup) && !hasEvolClawTraceGroup;
|
|
1289
|
+
// 命令判定:剥离所有 @ 后看是否 / 开头(多 @ 场景如 @a @b /status 也能正确识别)。
|
|
1290
|
+
// echo 消息走独立的 trace 流程,不参与命令语义判定。
|
|
1291
|
+
const isCommandMsg = !isEchoMsg && this.stripAllMentions(text).startsWith('/');
|
|
1292
|
+
if (isEchoMsg) {
|
|
1198
1293
|
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
1199
1294
|
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
1200
1295
|
const echoTs = () => {
|
|
@@ -1219,14 +1314,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1219
1314
|
return;
|
|
1220
1315
|
}
|
|
1221
1316
|
else {
|
|
1222
|
-
// 非 echo
|
|
1223
|
-
|
|
1317
|
+
// 非 echo 消息:mention 过滤
|
|
1318
|
+
// 命令豁免 broadcast:slash 命令在任何 dispatchMode 下都强制走 mention 语义,
|
|
1319
|
+
// 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
|
|
1320
|
+
const enforceMention = dispatchMode === 'mention' || isCommandMsg;
|
|
1321
|
+
if (enforceMention && !mentionedSelf && !mentionedAll) {
|
|
1224
1322
|
this.acknowledgeImmediately(messageId, seq);
|
|
1225
|
-
logger.info(`${this.logPrefix()} Group dropped: unmentioned
|
|
1323
|
+
logger.info(`${this.logPrefix()} Group dropped: unmentioned (group=${groupId} sender=${senderAid} mid=${messageId} mode=${dispatchMode} isCommand=${isCommandMsg} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
1226
1324
|
return;
|
|
1227
1325
|
}
|
|
1228
1326
|
}
|
|
1229
|
-
|
|
1327
|
+
// 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
|
|
1328
|
+
// 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
|
|
1329
|
+
const strippedText = isCommandMsg
|
|
1330
|
+
? this.stripAllMentions(text)
|
|
1331
|
+
: this.stripSelfMentionIfOnly(text, this._aid);
|
|
1230
1332
|
// Detect attachments before the empty-text guard (顶层 + 嵌套)
|
|
1231
1333
|
const rawAttachments = this.collectAllAttachments(payload);
|
|
1232
1334
|
const hasAttachments = rawAttachments.length > 0;
|
|
@@ -1263,7 +1365,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1263
1365
|
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1264
1366
|
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1265
1367
|
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 });
|
|
1266
|
-
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
|
|
1368
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode, payloadType === 'event' ? 'notify' : 'send');
|
|
1267
1369
|
// 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
|
|
1268
1370
|
// 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
|
|
1269
1371
|
const renderMentionAids = Array.from(new Set([
|
|
@@ -1287,6 +1389,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1287
1389
|
mentions,
|
|
1288
1390
|
mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
|
|
1289
1391
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
|
|
1392
|
+
dispatchMode: serverDispatchMode,
|
|
1290
1393
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1291
1394
|
});
|
|
1292
1395
|
}
|
|
@@ -1358,6 +1461,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1358
1461
|
mentions: mentionObjects,
|
|
1359
1462
|
mentionAids: event.mentionAids,
|
|
1360
1463
|
replyContext,
|
|
1464
|
+
source: event.source,
|
|
1465
|
+
dispatchMode: event.dispatchMode,
|
|
1361
1466
|
images: event.images,
|
|
1362
1467
|
}).catch(err => {
|
|
1363
1468
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
@@ -1384,57 +1489,161 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1384
1489
|
* 若消息发送方本身是 owner,则不转发给该 owner,但仍转发给其他 owner。
|
|
1385
1490
|
* 调用点须在所有过滤逻辑之前,payload 为 SDK 解密后的明文。
|
|
1386
1491
|
*/
|
|
1387
|
-
|
|
1492
|
+
/**
|
|
1493
|
+
* 入站转发:把对端发来的消息原样转发给 owner。
|
|
1494
|
+
* data 为 SDK message.received / group.message_created 回调的整个对象,
|
|
1495
|
+
* 不拆解、不重组——SDK 信封结构变化不影响此处。
|
|
1496
|
+
*/
|
|
1497
|
+
forwardInbound(data) {
|
|
1388
1498
|
if (!this.connected || !this.client)
|
|
1389
1499
|
return;
|
|
1390
1500
|
const { observable, owners } = this.getObserverConfig();
|
|
1391
1501
|
if (!observable || owners.length === 0)
|
|
1392
1502
|
return;
|
|
1503
|
+
const env = (data?.envelope && typeof data.envelope === 'object') ? data.envelope : {};
|
|
1504
|
+
const from = env.from ?? '';
|
|
1393
1505
|
if (this._aid && from === this._aid)
|
|
1394
1506
|
return; // self-echo:已在出站转过
|
|
1395
1507
|
// 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
|
|
1396
1508
|
const recipientOwners = owners.filter(o => o !== from);
|
|
1397
1509
|
if (recipientOwners.length === 0)
|
|
1398
1510
|
return;
|
|
1399
|
-
this.emitForward('inbound',
|
|
1511
|
+
this.emitForward('inbound', data, recipientOwners);
|
|
1400
1512
|
}
|
|
1401
1513
|
/**
|
|
1402
|
-
* 出站转发:Agent 经 AUN
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1514
|
+
* 出站转发:Agent 经 AUN 真实发出的消息原样转发给 owner。
|
|
1515
|
+
* result 为 SDK message.send / group.send 的 SendResult(已 attach envelope + payload)。
|
|
1516
|
+
* 若对端本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
|
|
1405
1517
|
*/
|
|
1406
|
-
forwardOutbound(
|
|
1518
|
+
forwardOutbound(result) {
|
|
1407
1519
|
if (!this.connected || !this.client)
|
|
1408
1520
|
return;
|
|
1409
1521
|
const { observable, owners } = this.getObserverConfig();
|
|
1410
1522
|
if (!observable || owners.length === 0)
|
|
1411
1523
|
return;
|
|
1412
|
-
|
|
1524
|
+
const env = (result?.envelope && typeof result.envelope === 'object') ? result.envelope : {};
|
|
1525
|
+
const to = env.to ?? env.group_id ?? '';
|
|
1526
|
+
// 过滤:若对端本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
|
|
1413
1527
|
// 但仍转发给其他 owner。
|
|
1414
1528
|
const recipientOwners = owners.filter(o => o !== to);
|
|
1415
1529
|
if (recipientOwners.length === 0)
|
|
1416
1530
|
return;
|
|
1417
|
-
this.emitForward('outbound',
|
|
1531
|
+
this.emitForward('outbound', result, recipientOwners);
|
|
1418
1532
|
}
|
|
1419
|
-
/**
|
|
1533
|
+
/**
|
|
1534
|
+
* 实际投递 observer.forward 给每个 owner,外层一律明文。
|
|
1535
|
+
* original 为 SDK 给到的原始对象(入站回调 data / 出站 SendResult),整体透传,
|
|
1536
|
+
* 不挑字段、不改字段——SDK 加任何字段都会自动一并转发给 owner。
|
|
1537
|
+
*/
|
|
1420
1538
|
emitForward(direction, original, owners) {
|
|
1421
1539
|
const forwardPayload = {
|
|
1422
1540
|
type: 'observer.forward',
|
|
1423
1541
|
direction,
|
|
1424
1542
|
agent_aid: this.config.aid,
|
|
1425
|
-
original
|
|
1426
|
-
from: original.from,
|
|
1427
|
-
to: original.to,
|
|
1428
|
-
...(original.seq != null ? { seq: original.seq } : {}),
|
|
1429
|
-
timestamp: Date.now(),
|
|
1430
|
-
payload: original.payload,
|
|
1431
|
-
},
|
|
1543
|
+
original,
|
|
1432
1544
|
};
|
|
1433
1545
|
for (const ownerAid of owners) {
|
|
1434
1546
|
this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
|
|
1435
1547
|
.catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
|
|
1436
1548
|
}
|
|
1437
1549
|
}
|
|
1550
|
+
// ── 观察者插话(Observer Insert,v0.3 待用上下文提示) ──────────────
|
|
1551
|
+
//
|
|
1552
|
+
// owner 经 message.send 给 agent 自身 AID 发 observer.inject(payload 为对象)。
|
|
1553
|
+
// 鉴权 from∈owners 后,把提示【只落盘】到 agent↔对端 会话的 pending-hints.jsonl
|
|
1554
|
+
// (不 dispatch、不跑 LLM、不回 owner);下一条对端消息到达时由 message-processor
|
|
1555
|
+
// 回放消费、注入渲染层。action=add 加提示 / remove 撤销。
|
|
1556
|
+
// 详见 docs/observer-insert-design.md 第一部分。
|
|
1557
|
+
/** 回 observer.inject.ack 给 owner(明文)。accepted 在成功写盘之后发出。 */
|
|
1558
|
+
emitInjectAck(ownerAid, injectId, data, error) {
|
|
1559
|
+
if (!this.connected || !this.client)
|
|
1560
|
+
return;
|
|
1561
|
+
const ackPayload = { type: 'observer.inject.ack' };
|
|
1562
|
+
if (injectId)
|
|
1563
|
+
ackPayload.id = injectId;
|
|
1564
|
+
if (data)
|
|
1565
|
+
ackPayload.data = data;
|
|
1566
|
+
if (error)
|
|
1567
|
+
ackPayload.error = error;
|
|
1568
|
+
this.callAndTrace('message.send', { to: ownerAid, payload: ackPayload, encrypt: false })
|
|
1569
|
+
.catch(e => logger.debug(`${this.logPrefix()} observer.inject.ack to ${ownerAid} failed: ${e}`));
|
|
1570
|
+
}
|
|
1571
|
+
/** 处理 observer.inject:鉴权 + 校验 + 只落盘到 pending-hints(不触发处理、不回 owner)。 */
|
|
1572
|
+
handleObserverInject(fromAid, payload, displayName, peerType) {
|
|
1573
|
+
void peerType;
|
|
1574
|
+
const { owners } = this.getObserverConfig();
|
|
1575
|
+
const ts = Date.now();
|
|
1576
|
+
const req = parseInjectRequest(payload, fromAid, owners, ts);
|
|
1577
|
+
if (req.kind === 'reject') {
|
|
1578
|
+
logger.warn(`${this.logPrefix()} observer.inject rejected: ${this.getShortAid(fromAid)} ${req.code}`);
|
|
1579
|
+
this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.action }, { code: req.code, message: req.message });
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const selfAID = this.config.aid;
|
|
1583
|
+
const sessionsDir = resolvePaths().sessionsDir;
|
|
1584
|
+
let ok;
|
|
1585
|
+
if (req.kind === 'remove') {
|
|
1586
|
+
ok = appendHintRemove(sessionsDir, 'aun', req.channelId, selfAID, { targetId: req.targetId, threadId: req.threadId, ts });
|
|
1587
|
+
}
|
|
1588
|
+
else {
|
|
1589
|
+
ok = appendHintAdd(sessionsDir, 'aun', req.channelId, selfAID, { id: req.id, text: req.text, threadId: req.threadId, ownerAid: req.ownerAid, ts });
|
|
1590
|
+
}
|
|
1591
|
+
if (!ok) {
|
|
1592
|
+
this.emitInjectAck(fromAid, req.injectId, { status: 'rejected', action: req.kind }, { code: 'STORE_FAILED', message: '提示落盘失败' });
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
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)'}`}`);
|
|
1596
|
+
this.emitInjectAck(fromAid, req.injectId, { status: 'accepted', action: req.kind });
|
|
1597
|
+
// 记录到 watch(被观察的 agent↔对端 会话),带 owner-inject 标记区分对端真实消息。
|
|
1598
|
+
// v0.3:只记"提示已添加/已撤销",不触发处理、不产生 agent→owner 回应。
|
|
1599
|
+
const watchText = req.kind === 'remove' ? `[撤销提示]${req.targetId ? ` id=${req.targetId}` : '(全部)'}` : req.text;
|
|
1600
|
+
const synthId = `inject-${req.injectId || ts}`;
|
|
1601
|
+
this.recordInjectWatch('in', fromAid, req.channelId, req.chatType, synthId, watchText);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* 把 observer 插话 / 对插话的回应记录到 watch(被观察的 agent↔对端 会话),
|
|
1605
|
+
* 带 source='owner-inject' 标记,与对端真实消息区分。
|
|
1606
|
+
* 写三处:messages.jsonl(watch msg)、appendAidEvent(watch aid 事件流)、aidStatsCollector(统计)。
|
|
1607
|
+
* @param dir 'in'=owner→agent 插话;'out'=agent→owner 对插话的回应
|
|
1608
|
+
* @param peerChannelId 被观察会话的对端(agent↔对端),日志落点 = sessions/aun/<self>/<peerChannelId>/
|
|
1609
|
+
*/
|
|
1610
|
+
recordInjectWatch(dir, ownerAid, peerChannelId, chatType, msgId, text) {
|
|
1611
|
+
try {
|
|
1612
|
+
const selfAID = this.config.aid;
|
|
1613
|
+
const isGroup = chatType === 'group';
|
|
1614
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID);
|
|
1615
|
+
const entry = dir === 'in'
|
|
1616
|
+
? buildInboundEntry({
|
|
1617
|
+
from: ownerAid, to: selfAID, chatType,
|
|
1618
|
+
groupId: isGroup ? peerChannelId : null, msgId, content: text,
|
|
1619
|
+
permMode: 'owner', source: 'owner-inject',
|
|
1620
|
+
})
|
|
1621
|
+
: buildOutboundEntry({
|
|
1622
|
+
from: selfAID, to: ownerAid, chatType,
|
|
1623
|
+
groupId: isGroup ? peerChannelId : null, msgId, content: text,
|
|
1624
|
+
source: 'owner-inject',
|
|
1625
|
+
});
|
|
1626
|
+
appendMessageLog(chatDir, entry);
|
|
1627
|
+
}
|
|
1628
|
+
catch (e) {
|
|
1629
|
+
logger.debug(`${this.logPrefix()} recordInjectWatch(msg) failed: ${e}`);
|
|
1630
|
+
}
|
|
1631
|
+
// watch aid:事件流 + 统计(标 inject,便于过滤)
|
|
1632
|
+
try {
|
|
1633
|
+
const len = Buffer.byteLength(text, 'utf-8');
|
|
1634
|
+
if (dir === 'in') {
|
|
1635
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: ownerAid, msgId, kind: 'text', len, inject: true });
|
|
1636
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, ownerAid, len, text, false, false, 'inject', 'inject');
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: ownerAid, msgId, kind: 'text', len, inject: true });
|
|
1640
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, ownerAid, len, text, false, false, 'inject', 'inject');
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch (e) {
|
|
1644
|
+
logger.debug(`${this.logPrefix()} recordInjectWatch(aid) failed: ${e}`);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1438
1647
|
handleEcho(event) {
|
|
1439
1648
|
const ts = () => {
|
|
1440
1649
|
const d = new Date();
|
|
@@ -1785,6 +1994,219 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1785
1994
|
onRecall(handler) {
|
|
1786
1995
|
this.recallHandler = handler;
|
|
1787
1996
|
}
|
|
1997
|
+
async withOutboxInFlight(entry, run, busyValue) {
|
|
1998
|
+
if (this.outboxInFlight.has(entry.id))
|
|
1999
|
+
return busyValue;
|
|
2000
|
+
this.outboxInFlight.add(entry.id);
|
|
2001
|
+
try {
|
|
2002
|
+
return await run();
|
|
2003
|
+
}
|
|
2004
|
+
finally {
|
|
2005
|
+
this.outboxInFlight.delete(entry.id);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
messageIdFromSendResult(result) {
|
|
2009
|
+
return result?.message?.message_id ?? result?.message_id ?? null;
|
|
2010
|
+
}
|
|
2011
|
+
payloadLogText(payload, contentKind) {
|
|
2012
|
+
if (typeof payload.text === 'string' && payload.text)
|
|
2013
|
+
return payload.text;
|
|
2014
|
+
switch (contentKind ?? payload.type) {
|
|
2015
|
+
case 'image':
|
|
2016
|
+
return payload.alt ? `[image] ${payload.alt}` : '[image]';
|
|
2017
|
+
case 'card':
|
|
2018
|
+
case 'action_card':
|
|
2019
|
+
return payload.title ? `[card] ${payload.title}` : '[card]';
|
|
2020
|
+
case 'file':
|
|
2021
|
+
return payload.filename ? `[file] ${payload.filename}` : '[file]';
|
|
2022
|
+
default:
|
|
2023
|
+
return `[${String(payload.type ?? contentKind ?? 'payload')}]`;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
payloadByteLength(payload, logText) {
|
|
2027
|
+
if (payload.type === 'image' && typeof payload.data_base64 === 'string') {
|
|
2028
|
+
return Buffer.byteLength(payload.data_base64, 'utf-8');
|
|
2029
|
+
}
|
|
2030
|
+
return Buffer.byteLength(logText, 'utf-8');
|
|
2031
|
+
}
|
|
2032
|
+
applyReplyContextToPayload(payload, context) {
|
|
2033
|
+
const finalPayload = { ...payload };
|
|
2034
|
+
if (context?.threadId && !finalPayload.thread_id)
|
|
2035
|
+
finalPayload.thread_id = context.threadId;
|
|
2036
|
+
if (context?.metadata?.taskId && !finalPayload.task_id)
|
|
2037
|
+
finalPayload.task_id = context.metadata.taskId;
|
|
2038
|
+
if (context?.metadata?.chatmode && !finalPayload.chatmode)
|
|
2039
|
+
finalPayload.chatmode = context.metadata.chatmode;
|
|
2040
|
+
return finalPayload;
|
|
2041
|
+
}
|
|
2042
|
+
registerCardPostSend(messageId, action) {
|
|
2043
|
+
if (action.type !== 'register_interaction_card')
|
|
2044
|
+
return;
|
|
2045
|
+
this.cardMessageIdMap.set(messageId, {
|
|
2046
|
+
requestId: action.requestId,
|
|
2047
|
+
isCommandCard: action.isCommandCard,
|
|
2048
|
+
initiatorAid: action.initiatorAid,
|
|
2049
|
+
});
|
|
2050
|
+
this.ownedCardMsgIds.add(messageId);
|
|
2051
|
+
const now = Date.now();
|
|
2052
|
+
const mapTtl = action.expiresAt && action.expiresAt > now
|
|
2053
|
+
? action.expiresAt - now
|
|
2054
|
+
: 20 * 60 * 1000;
|
|
2055
|
+
setTimeout(() => this.cardMessageIdMap.delete(messageId), mapTtl);
|
|
2056
|
+
setTimeout(() => this.ownedCardMsgIds.delete(messageId), 24 * 60 * 60 * 1000);
|
|
2057
|
+
}
|
|
2058
|
+
runPostSend(entry, messageId) {
|
|
2059
|
+
if (!entry.postSend)
|
|
2060
|
+
return;
|
|
2061
|
+
if (entry.postSend.type === 'register_interaction_card') {
|
|
2062
|
+
this.registerCardPostSend(messageId, entry.postSend);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
async sendAunPayload(channelId, payload, context, label) {
|
|
2066
|
+
if (!this.client || !this.connected)
|
|
2067
|
+
return { ok: false };
|
|
2068
|
+
const isGroup = this.isGroupId(channelId);
|
|
2069
|
+
const targetAid = channelId;
|
|
2070
|
+
const encryptTarget = isGroup ? channelId : targetAid;
|
|
2071
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
2072
|
+
? !!(context.metadata.encrypted)
|
|
2073
|
+
: this.shouldEncrypt(encryptTarget);
|
|
2074
|
+
const method = isGroup ? 'group.send' : 'message.send';
|
|
2075
|
+
const params = { payload, encrypt };
|
|
2076
|
+
if (isGroup)
|
|
2077
|
+
params.group_id = channelId;
|
|
2078
|
+
else
|
|
2079
|
+
params.to = targetAid;
|
|
2080
|
+
const callOnce = async (sendParams, fallback) => {
|
|
2081
|
+
const result = fallback
|
|
2082
|
+
? await this.client.call(method, sendParams)
|
|
2083
|
+
: await this.callAndTrace(method, sendParams);
|
|
2084
|
+
const mid = this.messageIdFromSendResult(result);
|
|
2085
|
+
if (!mid) {
|
|
2086
|
+
logger.warn(`${this.logPrefix()} ${method}${fallback ? ' fallback' : ''} (${label}) returned no message_id: ${JSON.stringify(result)}`);
|
|
2087
|
+
return { ok: false, result, encrypt: !!sendParams.encrypt };
|
|
2088
|
+
}
|
|
2089
|
+
return { ok: true, messageId: mid, result, encrypt: !!sendParams.encrypt };
|
|
2090
|
+
};
|
|
2091
|
+
try {
|
|
2092
|
+
return await callOnce(params, false);
|
|
2093
|
+
}
|
|
2094
|
+
catch (e) {
|
|
2095
|
+
if (encrypt && e instanceof E2EEError) {
|
|
2096
|
+
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
2097
|
+
logger.warn(`${this.logPrefix()} E2EE ${label} send failed to ${channelId}, retrying plaintext: ${e}`);
|
|
2098
|
+
const fallbackParams = { ...params, encrypt: false };
|
|
2099
|
+
try {
|
|
2100
|
+
this.trace('OUT', `${method}.${label}.fallback`, fallbackParams);
|
|
2101
|
+
const sent = await callOnce(fallbackParams, true);
|
|
2102
|
+
this.trace('OUT', `${method}.${label}.fallback.${sent.ok ? 'ok' : 'missing_id'}`, { message_id: sent.messageId });
|
|
2103
|
+
return sent;
|
|
2104
|
+
}
|
|
2105
|
+
catch (e2) {
|
|
2106
|
+
this.trace('OUT', `${method}.${label}.fallback.error`, { channelId, error: String(e2) });
|
|
2107
|
+
logger.error(`${this.logPrefix()} Plaintext ${label} fallback also failed to ${channelId}: ${e2}`);
|
|
2108
|
+
return { ok: false };
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
this.trace('OUT', `${method}.${label}.error`, { channelId, error: String(e) });
|
|
2112
|
+
logger.error(`${this.logPrefix()} ${label} send failed to ${channelId}: ${e}`);
|
|
2113
|
+
return { ok: false };
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
recordDurableOutbound(channelId, payload, messageId, encrypt, context, isGroup, contentKind, logText, result) {
|
|
2117
|
+
const kind = contentKind ?? payload.type ?? 'custom';
|
|
2118
|
+
appendAidEvent({
|
|
2119
|
+
ts: Date.now(),
|
|
2120
|
+
iso: new Date().toISOString(),
|
|
2121
|
+
event: 'message_out',
|
|
2122
|
+
aid: this.config.aid,
|
|
2123
|
+
to: channelId,
|
|
2124
|
+
msgId: messageId,
|
|
2125
|
+
kind,
|
|
2126
|
+
len: Buffer.byteLength(logText, 'utf-8'),
|
|
2127
|
+
...(isGroup && { groupId: channelId }),
|
|
2128
|
+
});
|
|
2129
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, this.payloadByteLength(payload, logText), logText, false, encrypt, context?.metadata?.chatmode);
|
|
2130
|
+
const source = context?.metadata?.source ?? 'daemon';
|
|
2131
|
+
this.appendOutboundJsonl(channelId, logText, messageId, encrypt, context, isGroup, 'text', source);
|
|
2132
|
+
this.forwardOutbound(result);
|
|
2133
|
+
}
|
|
2134
|
+
async sendContentPayload(channelId, payload, opts) {
|
|
2135
|
+
const finalPayload = this.applyReplyContextToPayload(payload, opts.context);
|
|
2136
|
+
const logText = opts.logText ?? this.payloadLogText(finalPayload, opts.contentKind);
|
|
2137
|
+
const entry = outbox.enqueue(this.config.aid, {
|
|
2138
|
+
channelId,
|
|
2139
|
+
type: 'payload',
|
|
2140
|
+
contentKind: opts.contentKind,
|
|
2141
|
+
payload: finalPayload,
|
|
2142
|
+
context: opts.context,
|
|
2143
|
+
logText,
|
|
2144
|
+
ttl: opts.ttl,
|
|
2145
|
+
postSend: opts.postSend,
|
|
2146
|
+
});
|
|
2147
|
+
logger.debug(`${this.logPrefix()} Outbox enqueued payload: id=${entry.id} kind=${opts.contentKind} channel=${channelId} text=${logText.slice(0, 40)}`);
|
|
2148
|
+
if (!this.connected || !this.client) {
|
|
2149
|
+
logger.warn(`${this.logPrefix()} Not connected, payload queued in outbox (id=${entry.id}, kind=${opts.contentKind}). Triggering reconnect.`);
|
|
2150
|
+
if (!this.reconnectTimer && !this.client) {
|
|
2151
|
+
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendContentPayload failed: ${e}`));
|
|
2152
|
+
}
|
|
2153
|
+
return { queued: true };
|
|
2154
|
+
}
|
|
2155
|
+
const result = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
|
|
2156
|
+
if (result.ok) {
|
|
2157
|
+
outbox.remove(this.config.aid, entry.id);
|
|
2158
|
+
return { messageId: result.messageId };
|
|
2159
|
+
}
|
|
2160
|
+
return { queued: true };
|
|
2161
|
+
}
|
|
2162
|
+
buildTaskPayloadBase(envelope, context) {
|
|
2163
|
+
const base = {};
|
|
2164
|
+
if (envelope.taskId)
|
|
2165
|
+
base.task_id = envelope.taskId;
|
|
2166
|
+
if (envelope.sessionId)
|
|
2167
|
+
base.session_id = envelope.sessionId;
|
|
2168
|
+
if (envelope.agentName)
|
|
2169
|
+
base.agent_name = envelope.agentName;
|
|
2170
|
+
if (envelope.chatmode)
|
|
2171
|
+
base.chatmode = envelope.chatmode;
|
|
2172
|
+
if (context?.threadId)
|
|
2173
|
+
base.thread_id = context.threadId;
|
|
2174
|
+
if (context?.peerId)
|
|
2175
|
+
base.initiator = context.peerId;
|
|
2176
|
+
if (context?.replyToMessageId)
|
|
2177
|
+
base.ref_message_id = context.replyToMessageId;
|
|
2178
|
+
return base;
|
|
2179
|
+
}
|
|
2180
|
+
activityLogText(raw) {
|
|
2181
|
+
// 兼容两种入参:原始 ThoughtItem(顶层字段),或已构建的 activity payload(字段收在 .item 里)
|
|
2182
|
+
const item = raw?.item && typeof raw.item === 'object' ? raw.item : raw;
|
|
2183
|
+
if (typeof item?.text === 'string' && item.text)
|
|
2184
|
+
return item.text;
|
|
2185
|
+
if (item?.kind === 'tool_call')
|
|
2186
|
+
return `[tool_call] ${item.name ?? ''}`.trim();
|
|
2187
|
+
if (item?.kind === 'tool_result')
|
|
2188
|
+
return `[tool_result] ${item.name ?? ''} ${item.ok === false ? 'failed' : 'ok'}`.trim();
|
|
2189
|
+
if (item?.kind)
|
|
2190
|
+
return `[activity:${item.kind}]`;
|
|
2191
|
+
return '[activity]';
|
|
2192
|
+
}
|
|
2193
|
+
buildActivityPayload(envelope, context, item) {
|
|
2194
|
+
const activityItem = item && typeof item === 'object' && !Array.isArray(item)
|
|
2195
|
+
? { ...item }
|
|
2196
|
+
: { kind: 'unknown', text: String(item ?? '') };
|
|
2197
|
+
return {
|
|
2198
|
+
...this.buildTaskPayloadBase(envelope, context),
|
|
2199
|
+
type: 'activity',
|
|
2200
|
+
item: activityItem,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
async sendReliableStructured(channelId, payload, context, logText) {
|
|
2204
|
+
await this.sendContentPayload(channelId, payload, {
|
|
2205
|
+
contentKind: 'custom',
|
|
2206
|
+
context,
|
|
2207
|
+
logText: logText ?? this.payloadLogText(payload, 'custom'),
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
1788
2210
|
async sendMessage(channelId, text, context) {
|
|
1789
2211
|
if (!text?.trim()) {
|
|
1790
2212
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
@@ -1817,7 +2239,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1817
2239
|
return;
|
|
1818
2240
|
}
|
|
1819
2241
|
// Attempt immediate delivery
|
|
1820
|
-
const ok = await this.deliverTextEntry(entry);
|
|
2242
|
+
const ok = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
1821
2243
|
if (ok) {
|
|
1822
2244
|
outbox.remove(this.config.aid, entry.id);
|
|
1823
2245
|
}
|
|
@@ -1899,19 +2321,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1899
2321
|
if (!mid) {
|
|
1900
2322
|
const dispatchStatus = result?.message_dispatch?.status;
|
|
1901
2323
|
if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
|
|
1902
|
-
logger.
|
|
2324
|
+
logger.warn(`${this.logPrefix()} group.send returned ${dispatchStatus} without message_id; keeping outbox entry: group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1903
2325
|
}
|
|
1904
2326
|
else {
|
|
1905
2327
|
logger.warn(`${this.logPrefix()} group.send returned no message_id: ${JSON.stringify(result)}`);
|
|
1906
2328
|
}
|
|
2329
|
+
return false;
|
|
1907
2330
|
}
|
|
1908
2331
|
else {
|
|
1909
2332
|
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1910
2333
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
1911
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
2334
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
|
|
1912
2335
|
this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
|
|
1913
|
-
// Observer forward: outbound (group) —
|
|
1914
|
-
this.forwardOutbound(
|
|
2336
|
+
// Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload)
|
|
2337
|
+
this.forwardOutbound(result);
|
|
1915
2338
|
}
|
|
1916
2339
|
}
|
|
1917
2340
|
else {
|
|
@@ -1919,14 +2342,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1919
2342
|
const result = await this.callAndTrace('message.send', params);
|
|
1920
2343
|
if (!result || !result.message_id) {
|
|
1921
2344
|
logger.warn(`${this.logPrefix()} message.send returned no message_id: ${JSON.stringify(result)}`);
|
|
2345
|
+
return false;
|
|
1922
2346
|
}
|
|
1923
2347
|
else {
|
|
1924
2348
|
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1925
2349
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
|
|
1926
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
2350
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode, 'send');
|
|
1927
2351
|
this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
|
|
1928
|
-
// Observer forward: outbound (private) —
|
|
1929
|
-
this.forwardOutbound(
|
|
2352
|
+
// Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload)
|
|
2353
|
+
this.forwardOutbound(result);
|
|
1930
2354
|
}
|
|
1931
2355
|
}
|
|
1932
2356
|
return true;
|
|
@@ -1940,20 +2364,30 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1940
2364
|
if (isGroup) {
|
|
1941
2365
|
this.trace('OUT', 'group.send.fallback', params);
|
|
1942
2366
|
const result = await this.client.call('group.send', params);
|
|
1943
|
-
this.
|
|
1944
|
-
|
|
2367
|
+
const mid = this.messageIdFromSendResult(result);
|
|
2368
|
+
this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
|
|
2369
|
+
if (!mid) {
|
|
1945
2370
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2371
|
+
return false;
|
|
1946
2372
|
}
|
|
1947
|
-
this.
|
|
2373
|
+
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 });
|
|
2374
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode);
|
|
2375
|
+
this.appendOutboundJsonl(channelId, finalText, mid, false, context, true, 'text', source);
|
|
2376
|
+
this.forwardOutbound(result);
|
|
1948
2377
|
}
|
|
1949
2378
|
else {
|
|
1950
2379
|
this.trace('OUT', 'message.send.fallback', params);
|
|
1951
2380
|
const result = await this.client.call('message.send', params);
|
|
1952
|
-
|
|
1953
|
-
|
|
2381
|
+
const mid = result?.message_id;
|
|
2382
|
+
this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
|
|
2383
|
+
if (!result || !mid) {
|
|
1954
2384
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2385
|
+
return false;
|
|
1955
2386
|
}
|
|
1956
|
-
this.
|
|
2387
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: mid, kind: 'text', len: finalText.length });
|
|
2388
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, false, context?.metadata?.chatmode);
|
|
2389
|
+
this.appendOutboundJsonl(targetAid, finalText, mid, false, context, false, 'text', source);
|
|
2390
|
+
this.forwardOutbound(result);
|
|
1957
2391
|
}
|
|
1958
2392
|
return true;
|
|
1959
2393
|
}
|
|
@@ -1970,6 +2404,26 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1970
2404
|
}
|
|
1971
2405
|
}
|
|
1972
2406
|
}
|
|
2407
|
+
async deliverPayloadEntry(entry) {
|
|
2408
|
+
const channelId = entry.channelId;
|
|
2409
|
+
const payload = entry.payload;
|
|
2410
|
+
if (!payload) {
|
|
2411
|
+
logger.warn(`${this.logPrefix()} deliverPayloadEntry: missing payload (outbox id=${entry.id})`);
|
|
2412
|
+
return { ok: true };
|
|
2413
|
+
}
|
|
2414
|
+
const contentKind = entry.contentKind;
|
|
2415
|
+
const logText = entry.logText ?? this.payloadLogText(payload, contentKind);
|
|
2416
|
+
const context = entry.context;
|
|
2417
|
+
logger.info(`${this.logPrefix()} deliverPayloadEntry: id=${entry.id} kind=${contentKind ?? payload.type ?? 'payload'} channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} textLen=${logText.length}`);
|
|
2418
|
+
const sent = await this.sendAunPayload(channelId, payload, context, `${contentKind ?? payload.type ?? 'payload'}`);
|
|
2419
|
+
if (!sent.ok || !sent.messageId)
|
|
2420
|
+
return sent;
|
|
2421
|
+
const isGroup = this.isGroupId(channelId);
|
|
2422
|
+
logger.info(`${this.logPrefix()} durable payload sent: kind=${contentKind ?? payload.type ?? 'payload'} target=${isGroup ? channelId : this.peerLabel(channelId)} mid=${sent.messageId} encrypt=${sent.encrypt} text=${logText.slice(0, 60)}`);
|
|
2423
|
+
this.recordDurableOutbound(channelId, payload, sent.messageId, !!sent.encrypt, context, isGroup, contentKind, logText, sent.result);
|
|
2424
|
+
this.runPostSend(entry, sent.messageId);
|
|
2425
|
+
return sent;
|
|
2426
|
+
}
|
|
1973
2427
|
/** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
|
|
1974
2428
|
appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
|
|
1975
2429
|
try {
|
|
@@ -2022,10 +2476,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2022
2476
|
encrypt,
|
|
2023
2477
|
};
|
|
2024
2478
|
try {
|
|
2025
|
-
const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
|
|
2026
|
-
const stage = payload?.stage ?? `items=${itemCount}`;
|
|
2027
|
-
// 提取 thought 文本(只对 kind=text 的 item 写 jsonl,过滤 tool_use/tool_result 等结构化项)
|
|
2028
2479
|
const items = payload?.items;
|
|
2480
|
+
const itemCount = Array.isArray(items) ? items.length : 1;
|
|
2481
|
+
const stage = payload?.stage ?? (payload?.kind ? `kind=${payload.kind}` : `items=${itemCount}`);
|
|
2482
|
+
// 提取 thought 文本:兼容旧 items[] 和新扁平 activity payload。
|
|
2029
2483
|
let thoughtText;
|
|
2030
2484
|
if (Array.isArray(items) && items.length > 0) {
|
|
2031
2485
|
const lastItem = items[items.length - 1];
|
|
@@ -2043,15 +2497,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2043
2497
|
thoughtText = lastItem;
|
|
2044
2498
|
}
|
|
2045
2499
|
}
|
|
2500
|
+
else {
|
|
2501
|
+
thoughtText = this.activityLogText(payload);
|
|
2502
|
+
}
|
|
2046
2503
|
if (this.isGroupId(channelId)) {
|
|
2047
2504
|
params.group_id = targetId;
|
|
2048
2505
|
const putRes = await this.callAndTrace('group.thought.put', params);
|
|
2049
2506
|
const tid = putRes?.thought_id;
|
|
2050
2507
|
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
2051
2508
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
2052
|
-
this.forwardOutbound(
|
|
2509
|
+
this.forwardOutbound(putRes);
|
|
2053
2510
|
if (thoughtText) {
|
|
2054
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
|
|
2511
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive', 'thought');
|
|
2055
2512
|
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
|
|
2056
2513
|
}
|
|
2057
2514
|
}
|
|
@@ -2061,9 +2518,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2061
2518
|
const tid = putRes?.thought_id;
|
|
2062
2519
|
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
2063
2520
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
2064
|
-
this.forwardOutbound(
|
|
2521
|
+
this.forwardOutbound(putRes);
|
|
2065
2522
|
if (thoughtText) {
|
|
2066
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
|
|
2523
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive', 'thought');
|
|
2067
2524
|
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
|
|
2068
2525
|
}
|
|
2069
2526
|
}
|
|
@@ -2098,14 +2555,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2098
2555
|
const result = await this.callAndTrace('group.send', params);
|
|
2099
2556
|
const mid = result?.message?.message_id ?? result?.message_id ?? null;
|
|
2100
2557
|
logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
|
|
2101
|
-
this.forwardOutbound(
|
|
2558
|
+
this.forwardOutbound(result);
|
|
2102
2559
|
return mid;
|
|
2103
2560
|
}
|
|
2104
2561
|
else {
|
|
2105
2562
|
params.to = targetAid;
|
|
2106
2563
|
const result = await this.callAndTrace('message.send', params);
|
|
2107
2564
|
logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
|
|
2108
|
-
this.forwardOutbound(
|
|
2565
|
+
this.forwardOutbound(result);
|
|
2109
2566
|
return result?.message_id ?? null;
|
|
2110
2567
|
}
|
|
2111
2568
|
}
|
|
@@ -2145,7 +2602,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2145
2602
|
}
|
|
2146
2603
|
return;
|
|
2147
2604
|
}
|
|
2148
|
-
const ok = await this.deliverFileEntry(entry);
|
|
2605
|
+
const ok = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
|
|
2149
2606
|
if (ok) {
|
|
2150
2607
|
outbox.remove(this.config.aid, entry.id);
|
|
2151
2608
|
}
|
|
@@ -2215,6 +2672,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2215
2672
|
filePayload.task_id = context.metadata.taskId;
|
|
2216
2673
|
if (context?.metadata?.chatmode)
|
|
2217
2674
|
filePayload.chatmode = context.metadata.chatmode;
|
|
2675
|
+
// file-link-cache: 回带点击请求的 correlationId,客户端用它把异步到达的文件消息对回这次 fetch 点击
|
|
2676
|
+
if (context?.metadata?.correlationId)
|
|
2677
|
+
filePayload.correlation_id = context.metadata.correlationId;
|
|
2218
2678
|
const isGroup = this.isGroupId(channelId);
|
|
2219
2679
|
const fileTargetAid = channelId;
|
|
2220
2680
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
@@ -2222,24 +2682,32 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2222
2682
|
? !!(context.metadata.encrypted)
|
|
2223
2683
|
: this.shouldEncrypt(encryptTarget);
|
|
2224
2684
|
const params = { payload: filePayload, encrypt };
|
|
2685
|
+
let sendResult = null;
|
|
2686
|
+
let sentMid = null;
|
|
2225
2687
|
try {
|
|
2226
2688
|
if (isGroup) {
|
|
2227
2689
|
params.group_id = channelId;
|
|
2228
2690
|
this.trace('OUT', 'group.send.file', params);
|
|
2229
2691
|
const result = await this.client.call('group.send', params);
|
|
2692
|
+
sendResult = result;
|
|
2230
2693
|
const fileMid = result?.message?.message_id ?? result?.message_id;
|
|
2694
|
+
sentMid = fileMid ?? null;
|
|
2231
2695
|
this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
|
|
2232
2696
|
if (!fileMid) {
|
|
2233
2697
|
logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
2698
|
+
return false;
|
|
2234
2699
|
}
|
|
2235
2700
|
}
|
|
2236
2701
|
else {
|
|
2237
2702
|
params.to = fileTargetAid;
|
|
2238
2703
|
this.trace('OUT', 'message.send.file', params);
|
|
2239
2704
|
const result = await this.client.call('message.send', params);
|
|
2240
|
-
|
|
2241
|
-
|
|
2705
|
+
sendResult = result;
|
|
2706
|
+
sentMid = result?.message_id ?? null;
|
|
2707
|
+
this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
|
|
2708
|
+
if (!result || !sentMid) {
|
|
2242
2709
|
logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
2710
|
+
return false;
|
|
2243
2711
|
}
|
|
2244
2712
|
}
|
|
2245
2713
|
}
|
|
@@ -2255,18 +2723,24 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2255
2723
|
if (isGroup) {
|
|
2256
2724
|
this.trace('OUT', 'group.send.file.fallback', params);
|
|
2257
2725
|
const result = await this.client.call('group.send', params);
|
|
2726
|
+
sendResult = result;
|
|
2258
2727
|
const fbMid = result?.message?.message_id ?? result?.message_id;
|
|
2728
|
+
sentMid = fbMid ?? null;
|
|
2259
2729
|
this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
|
|
2260
2730
|
if (!fbMid) {
|
|
2261
2731
|
logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2732
|
+
return false;
|
|
2262
2733
|
}
|
|
2263
2734
|
}
|
|
2264
2735
|
else {
|
|
2265
2736
|
this.trace('OUT', 'message.send.file.fallback', params);
|
|
2266
2737
|
const result = await this.client.call('message.send', params);
|
|
2267
|
-
|
|
2268
|
-
|
|
2738
|
+
sendResult = result;
|
|
2739
|
+
sentMid = result?.message_id ?? null;
|
|
2740
|
+
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
|
|
2741
|
+
if (!result || !sentMid) {
|
|
2269
2742
|
logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2743
|
+
return false;
|
|
2270
2744
|
}
|
|
2271
2745
|
}
|
|
2272
2746
|
}
|
|
@@ -2275,7 +2749,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2275
2749
|
}
|
|
2276
2750
|
}
|
|
2277
2751
|
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
2278
|
-
|
|
2752
|
+
if (sentMid) {
|
|
2753
|
+
const fileText = filePayload.text;
|
|
2754
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: sentMid, kind: 'file', len: fileText.length, ...(isGroup && { groupId: channelId }) });
|
|
2755
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(fileText, 'utf-8'), fileText, false, !!params.encrypt, context?.metadata?.chatmode);
|
|
2756
|
+
const source = context?.metadata?.source ?? 'daemon';
|
|
2757
|
+
this.appendOutboundJsonl(channelId, fileText, sentMid, !!params.encrypt, context, isGroup, 'text', source);
|
|
2758
|
+
}
|
|
2759
|
+
if (sendResult)
|
|
2760
|
+
this.forwardOutbound(sendResult);
|
|
2279
2761
|
return true;
|
|
2280
2762
|
}
|
|
2281
2763
|
catch (e) {
|
|
@@ -2309,10 +2791,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2309
2791
|
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
2310
2792
|
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
2311
2793
|
if (entry.type === 'text') {
|
|
2312
|
-
return this.deliverTextEntry(entry);
|
|
2794
|
+
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
2313
2795
|
}
|
|
2314
2796
|
else if (entry.type === 'file') {
|
|
2315
|
-
return this.deliverFileEntry(entry);
|
|
2797
|
+
return this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
|
|
2798
|
+
}
|
|
2799
|
+
else if (entry.type === 'payload') {
|
|
2800
|
+
const sent = await this.withOutboxInFlight(entry, () => this.deliverPayloadEntry(entry), { ok: false });
|
|
2801
|
+
return sent.ok;
|
|
2316
2802
|
}
|
|
2317
2803
|
return true; // unknown type, discard
|
|
2318
2804
|
});
|
|
@@ -2341,58 +2827,44 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2341
2827
|
progress: 'progress',
|
|
2342
2828
|
};
|
|
2343
2829
|
const statusPayload = {
|
|
2344
|
-
type: 'status',
|
|
2345
|
-
|
|
2830
|
+
type: 'task.status',
|
|
2831
|
+
status: stateMap[status] ?? status,
|
|
2346
2832
|
task_id: taskId,
|
|
2347
2833
|
session_id: sessionId,
|
|
2348
2834
|
severity,
|
|
2835
|
+
terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
|
|
2349
2836
|
...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
|
|
2350
2837
|
};
|
|
2351
2838
|
if (context?.threadId)
|
|
2352
2839
|
statusPayload.thread_id = context.threadId;
|
|
2353
2840
|
if (context?.peerId)
|
|
2354
2841
|
statusPayload.initiator = context.peerId;
|
|
2842
|
+
if (context?.metadata?.chatmode)
|
|
2843
|
+
statusPayload.chatmode = context.metadata.chatmode;
|
|
2355
2844
|
if (context?.replyToMessageId)
|
|
2356
2845
|
statusPayload.ref_message_id = context.replyToMessageId;
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
:
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
2380
|
-
logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
|
|
2381
|
-
const c2 = this.client;
|
|
2382
|
-
if (!c2)
|
|
2383
|
-
return;
|
|
2384
|
-
const fallbackParams = { ...params, encrypt: false };
|
|
2385
|
-
return c2.call(method, fallbackParams).catch(e2 => {
|
|
2386
|
-
logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
|
|
2387
|
-
});
|
|
2388
|
-
}
|
|
2389
|
-
logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
|
|
2390
|
-
});
|
|
2391
|
-
};
|
|
2392
|
-
const method = isGroup ? 'group.send' : 'message.send';
|
|
2393
|
-
sendOne(method, statusPayload, 'status');
|
|
2394
|
-
this.forwardOutbound(channelId, statusPayload);
|
|
2395
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
|
|
2846
|
+
const notifyOptions = { ttlMs: 60_000 };
|
|
2847
|
+
if (this.isGroupId(channelId))
|
|
2848
|
+
notifyOptions.groupId = channelId;
|
|
2849
|
+
else
|
|
2850
|
+
notifyOptions.to = channelId;
|
|
2851
|
+
this.trace('OUT', 'notify.task_status', {
|
|
2852
|
+
method: 'event/app.task.status',
|
|
2853
|
+
params: statusPayload,
|
|
2854
|
+
options: notifyOptions,
|
|
2855
|
+
});
|
|
2856
|
+
const notify = this.client.notify;
|
|
2857
|
+
if (typeof notify !== 'function') {
|
|
2858
|
+
logger.warn(`${this.logPrefix()} task.${status} notify skipped: client.notify unavailable`);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
Promise.resolve(notify.call(this.client, 'event/app.task.status', statusPayload, notifyOptions)).then(() => {
|
|
2862
|
+
this.trace('OUT', 'notify.task_status.ok', { task_id: taskId, status: statusPayload.status });
|
|
2863
|
+
}).catch((e) => {
|
|
2864
|
+
this.trace('OUT', 'notify.task_status.error', { task_id: taskId, status: statusPayload.status, error: String(e) });
|
|
2865
|
+
logger.debug(`${this.logPrefix()} task_status notify failed: ${e}`);
|
|
2866
|
+
}).catch(() => { });
|
|
2867
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
|
|
2396
2868
|
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
2397
2869
|
const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
|
|
2398
2870
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
@@ -2615,251 +3087,290 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2615
3087
|
return undefined; // 不写缓存,下次仍可重试
|
|
2616
3088
|
}
|
|
2617
3089
|
}
|
|
3090
|
+
/**
|
|
3091
|
+
* 查询某成员在群里的角色(经 group.get_admins)。
|
|
3092
|
+
* 仅用于话题创建权限校验(稀有事件),故不缓存:每次查权威源,结果天然最新。
|
|
3093
|
+
* 命中 admins 列表(owner+admin)返回其 role;不在列表(含普通 member / observer)返回 'none';
|
|
3094
|
+
* 未连接 / 异常返回 undefined —— 绝不抛出,由调用方按 fail-closed 处理。
|
|
3095
|
+
*/
|
|
3096
|
+
async getGroupMemberRole(groupId, aid) {
|
|
3097
|
+
if (!groupId || !aid)
|
|
3098
|
+
return undefined;
|
|
3099
|
+
if (!this.client)
|
|
3100
|
+
return undefined;
|
|
3101
|
+
try {
|
|
3102
|
+
const result = await this.callAndTrace('group.get_admins', { group_id: groupId });
|
|
3103
|
+
const admins = Array.isArray(result?.admins) ? result.admins : [];
|
|
3104
|
+
const hit = admins.find((m) => m?.aid === aid);
|
|
3105
|
+
if (hit) {
|
|
3106
|
+
const role = hit.role;
|
|
3107
|
+
return role === 'owner' || role === 'admin' ? role : 'admin';
|
|
3108
|
+
}
|
|
3109
|
+
return 'none'; // 不在 owner/admin 列表 → 普通 member / observer / 非成员
|
|
3110
|
+
}
|
|
3111
|
+
catch {
|
|
3112
|
+
return undefined; // 查询失败,调用方 fail-closed
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
2618
3115
|
}
|
|
2619
3116
|
// Plugin implementation
|
|
2620
3117
|
export class AUNChannelPlugin {
|
|
2621
3118
|
name = 'aun';
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
3119
|
+
async createInstance(inst, ctx) {
|
|
3120
|
+
// AUN aid is the agent's own AID; loader injects it as inst.aid, ctx.agentName is the source of truth.
|
|
3121
|
+
const aid = inst.aid ?? ctx.agentName;
|
|
3122
|
+
if (inst.enabled === false || !aid)
|
|
3123
|
+
return null;
|
|
3124
|
+
const channel = new AUNChannel({
|
|
3125
|
+
aid,
|
|
3126
|
+
keystorePath: inst.keystorePath,
|
|
3127
|
+
gatewayUrl: inst.gatewayUrl,
|
|
3128
|
+
accessToken: inst.accessToken,
|
|
3129
|
+
flushDelay: inst.flushDelay,
|
|
3130
|
+
owner: inst.owner ?? inst.owners?.[0],
|
|
3131
|
+
agentName: ctx.agentName,
|
|
3132
|
+
channelName: inst.name,
|
|
3133
|
+
aunTrace: ctx.debug?.aunTrace,
|
|
3134
|
+
aunSdkLog: ctx.debug?.aunSdkLog,
|
|
3135
|
+
});
|
|
3136
|
+
const mode = resolveShowActivities(inst);
|
|
3137
|
+
const adapter = {
|
|
3138
|
+
channelName: inst.name,
|
|
3139
|
+
channelKey: inst.name, // channelName 实际上就是 channelKey
|
|
3140
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
|
|
3141
|
+
send: async (envelope, payload) => {
|
|
3142
|
+
const replyCtx = envelope.replyContext;
|
|
3143
|
+
const channelId = envelope.channelId;
|
|
3144
|
+
const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
|
|
3145
|
+
switch (payload.kind) {
|
|
3146
|
+
case 'result.text':
|
|
3147
|
+
case 'command.result':
|
|
3148
|
+
case 'command.error': {
|
|
3149
|
+
const sendCtx = { ...(replyCtx ?? {}) };
|
|
3150
|
+
if (payload.kind === 'result.text' && payload.isFinal)
|
|
3151
|
+
sendCtx.title = '✅ 最终回复:';
|
|
3152
|
+
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
case 'system.notice': {
|
|
3156
|
+
await channel.sendReliableStructured(channelId, {
|
|
3157
|
+
type: 'notice',
|
|
3158
|
+
...taskBase(),
|
|
3159
|
+
subtype: payload.subtype,
|
|
3160
|
+
text: payload.text,
|
|
3161
|
+
severity: 'info',
|
|
3162
|
+
}, replyCtx, payload.text);
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
case 'system.error': {
|
|
3166
|
+
await channel.sendReliableStructured(channelId, {
|
|
3167
|
+
type: 'error',
|
|
3168
|
+
...taskBase(),
|
|
3169
|
+
subtype: payload.subtype,
|
|
3170
|
+
message: payload.text,
|
|
3171
|
+
user_message: payload.text,
|
|
3172
|
+
recoverable: payload.recoverable,
|
|
3173
|
+
terminal: !payload.recoverable,
|
|
3174
|
+
}, replyCtx, payload.text);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
case 'result.error': {
|
|
3178
|
+
await channel.sendReliableStructured(channelId, {
|
|
3179
|
+
type: 'error',
|
|
3180
|
+
...taskBase(),
|
|
3181
|
+
reason: payload.reason,
|
|
3182
|
+
message: payload.text,
|
|
3183
|
+
user_message: payload.text,
|
|
3184
|
+
terminal: true,
|
|
3185
|
+
}, replyCtx, payload.text);
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
case 'result.file': {
|
|
3189
|
+
const fileCtx = payload.correlationId
|
|
3190
|
+
? { ...(replyCtx ?? {}), metadata: { ...(replyCtx?.metadata ?? {}), correlationId: payload.correlationId } }
|
|
3191
|
+
: replyCtx;
|
|
3192
|
+
await channel.sendFile(channelId, payload.filePath, fileCtx);
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
case 'result.image': {
|
|
3196
|
+
const buf = payload.data;
|
|
3197
|
+
const b64 = buf.toString('base64');
|
|
3198
|
+
await channel.sendContentPayload(channelId, {
|
|
3199
|
+
type: 'image', alt: payload.alt, data_base64: b64, mime_type: payload.mimeType,
|
|
3200
|
+
}, {
|
|
3201
|
+
contentKind: 'image',
|
|
3202
|
+
context: replyCtx,
|
|
3203
|
+
logText: payload.alt ? `[image] ${payload.alt}` : '[image]',
|
|
3204
|
+
});
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
case 'activity.batch': {
|
|
3208
|
+
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
3209
|
+
for (const item of items) {
|
|
3210
|
+
if (item?.kind === 'progress') {
|
|
3211
|
+
channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, {
|
|
3212
|
+
activityType: 'progress',
|
|
3213
|
+
text: item.text,
|
|
3214
|
+
state: item.state,
|
|
3215
|
+
toolUses: item.tool_uses,
|
|
3216
|
+
durationMs: item.duration_ms,
|
|
3217
|
+
});
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
const aunPayload = channel.buildActivityPayload(envelope, replyCtx, item);
|
|
2692
3221
|
if (envelope.chatmode === 'proactive') {
|
|
2693
|
-
await channel.sendThought(channelId, envelope.taskId, aunPayload,
|
|
3222
|
+
await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
|
|
2694
3223
|
}
|
|
2695
3224
|
else {
|
|
2696
|
-
|
|
2697
|
-
await channel.sendStructured(channelId, aunPayload, ctx);
|
|
3225
|
+
await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
|
|
2698
3226
|
}
|
|
2699
|
-
return;
|
|
2700
3227
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
value: btn.command,
|
|
2754
|
-
style: btn.style ?? 'default',
|
|
2755
|
-
behavior: 'reply',
|
|
2756
|
-
})),
|
|
2757
|
-
};
|
|
2758
|
-
if (card.body)
|
|
2759
|
-
aunCard.description = card.body;
|
|
2760
|
-
if (ctx?.threadId)
|
|
2761
|
-
aunCard.thread_id = ctx.threadId;
|
|
2762
|
-
const msgId = await channel.sendStructured(channelId, aunCard, ctx);
|
|
2763
|
-
if (msgId) {
|
|
2764
|
-
channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
|
|
2765
|
-
setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
else if (payload.fallbackText) {
|
|
2769
|
-
await channel.sendMessage(channelId, payload.fallbackText, ctx);
|
|
2770
|
-
}
|
|
2771
|
-
return;
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
case 'status.progress':
|
|
3231
|
+
channel.sendProcessingStatus(channelId, 'progress', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3232
|
+
return;
|
|
3233
|
+
case 'status.started':
|
|
3234
|
+
channel.sendProcessingStatus(channelId, 'start', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3235
|
+
return;
|
|
3236
|
+
case 'status.queued':
|
|
3237
|
+
channel.sendProcessingStatus(channelId, 'queued', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3238
|
+
return;
|
|
3239
|
+
case 'status.completed':
|
|
3240
|
+
channel.sendProcessingStatus(channelId, 'done', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3241
|
+
return;
|
|
3242
|
+
case 'status.interrupted':
|
|
3243
|
+
channel.sendProcessingStatus(channelId, 'interrupted', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3244
|
+
return;
|
|
3245
|
+
case 'status.error':
|
|
3246
|
+
channel.sendProcessingStatus(channelId, 'error', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3247
|
+
return;
|
|
3248
|
+
case 'status.timeout':
|
|
3249
|
+
channel.sendProcessingStatus(channelId, 'timeout', envelope.sessionId ?? envelope.taskId, envelope.taskId, replyCtx, payload.metadata);
|
|
3250
|
+
return;
|
|
3251
|
+
case 'interaction': {
|
|
3252
|
+
const req = payload.interaction;
|
|
3253
|
+
if (req.kind.kind === 'action') {
|
|
3254
|
+
const action = req.kind;
|
|
3255
|
+
const aunCard = {
|
|
3256
|
+
type: 'action_card',
|
|
3257
|
+
title: action.title,
|
|
3258
|
+
actions: action.buttons.map(btn => ({
|
|
3259
|
+
label: btn.label, value: btn.key, style: btn.style ?? 'default', behavior: 'reply',
|
|
3260
|
+
})),
|
|
3261
|
+
};
|
|
3262
|
+
if (action.body)
|
|
3263
|
+
aunCard.description = action.body;
|
|
3264
|
+
if (req.initiatorId && channel.isGroupId(channelId))
|
|
3265
|
+
aunCard.initiator = req.initiatorId;
|
|
3266
|
+
if (replyCtx?.threadId)
|
|
3267
|
+
aunCard.thread_id = replyCtx.threadId;
|
|
3268
|
+
await channel.sendContentPayload(channelId, aunCard, {
|
|
3269
|
+
contentKind: 'card',
|
|
3270
|
+
context: replyCtx,
|
|
3271
|
+
logText: action.title ? `[card] ${action.title}` : '[card]',
|
|
3272
|
+
postSend: {
|
|
3273
|
+
type: 'register_interaction_card',
|
|
3274
|
+
requestId: req.id,
|
|
3275
|
+
isCommandCard: false,
|
|
3276
|
+
initiatorAid: req.initiatorId,
|
|
3277
|
+
expiresAt: Date.now() + 20 * 60 * 1000,
|
|
3278
|
+
},
|
|
3279
|
+
});
|
|
2772
3280
|
}
|
|
2773
|
-
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
3281
|
+
else if (req.kind.kind === 'command-card') {
|
|
3282
|
+
const card = req.kind;
|
|
3283
|
+
const aunCard = {
|
|
3284
|
+
type: 'action_card',
|
|
3285
|
+
title: card.title,
|
|
3286
|
+
actions: card.buttons.map(btn => ({
|
|
3287
|
+
label: btn.label, value: btn.command, style: btn.style ?? 'default', behavior: 'reply', disabled: btn.disabled || undefined,
|
|
3288
|
+
})),
|
|
3289
|
+
};
|
|
3290
|
+
if (card.body)
|
|
3291
|
+
aunCard.description = card.body;
|
|
3292
|
+
if (replyCtx?.threadId)
|
|
3293
|
+
aunCard.thread_id = replyCtx.threadId;
|
|
3294
|
+
await channel.sendContentPayload(channelId, aunCard, {
|
|
3295
|
+
contentKind: 'card',
|
|
3296
|
+
context: replyCtx,
|
|
3297
|
+
logText: card.title ? `[card] ${card.title}` : '[card]',
|
|
3298
|
+
postSend: {
|
|
3299
|
+
type: 'register_interaction_card',
|
|
3300
|
+
requestId: req.id,
|
|
3301
|
+
isCommandCard: true,
|
|
3302
|
+
initiatorAid: req.initiatorId,
|
|
3303
|
+
expiresAt: Date.now() + 20 * 60 * 1000,
|
|
3304
|
+
},
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
else if (payload.fallbackText) {
|
|
3308
|
+
await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
|
|
2777
3309
|
}
|
|
2778
|
-
|
|
2779
|
-
logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
|
|
3310
|
+
return;
|
|
2780
3311
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
};
|
|
2786
|
-
const policy = {
|
|
2787
|
-
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
2788
|
-
canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
2789
|
-
canCreateSession: (chatType, identity) => true,
|
|
2790
|
-
canDeleteSession: (chatType, identity) => true,
|
|
2791
|
-
canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
2792
|
-
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
2793
|
-
showMiddleResult: (chatType, identity) => {
|
|
2794
|
-
const mode = getChannelShowActivities(config, inst.name);
|
|
2795
|
-
if (mode === 'none')
|
|
2796
|
-
return false;
|
|
2797
|
-
if (mode === 'dm-only')
|
|
2798
|
-
return chatType === 'private';
|
|
2799
|
-
if (mode === 'owner-dm-only')
|
|
2800
|
-
return chatType === 'private' && identity === 'owner';
|
|
2801
|
-
return true;
|
|
2802
|
-
},
|
|
2803
|
-
showIdleMonitor: (chatType, identity) => {
|
|
2804
|
-
const mode = getChannelShowActivities(config, inst.name);
|
|
2805
|
-
if (mode === 'none')
|
|
2806
|
-
return false;
|
|
2807
|
-
if (mode === 'dm-only')
|
|
2808
|
-
return chatType === 'private';
|
|
2809
|
-
if (mode === 'owner-dm-only')
|
|
2810
|
-
return chatType === 'private' && identity === 'owner';
|
|
2811
|
-
return true;
|
|
2812
|
-
},
|
|
2813
|
-
accumulateErrors: (chatType, identity) => true,
|
|
2814
|
-
};
|
|
2815
|
-
const options = {
|
|
2816
|
-
flushDelay: inst.flushDelay ?? 3,
|
|
2817
|
-
fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
|
|
2818
|
-
};
|
|
2819
|
-
result.push({
|
|
2820
|
-
channelType: 'aun',
|
|
2821
|
-
adapter,
|
|
2822
|
-
channel,
|
|
2823
|
-
policy,
|
|
2824
|
-
options,
|
|
2825
|
-
connect: () => channel.connect(),
|
|
2826
|
-
disconnect: () => channel.disconnect(),
|
|
2827
|
-
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
2828
|
-
registerBridge(bridge, channelType) {
|
|
2829
|
-
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
2830
|
-
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
2831
|
-
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
2832
|
-
},
|
|
2833
|
-
registerHooks(ctx) {
|
|
2834
|
-
channel.setEventBus(ctx.eventBus);
|
|
2835
|
-
if (channel.setOnChannelDown) {
|
|
2836
|
-
channel.setOnChannelDown(() => {
|
|
2837
|
-
ctx.eventBus.publish({
|
|
2838
|
-
type: 'channel:error',
|
|
2839
|
-
channel: 'aun',
|
|
2840
|
-
channelName: adapter.channelName,
|
|
2841
|
-
status: 'auth_error',
|
|
2842
|
-
message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
2843
|
-
timestamp: Date.now(),
|
|
2844
|
-
});
|
|
2845
|
-
});
|
|
3312
|
+
case 'custom': {
|
|
3313
|
+
const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
|
|
3314
|
+
channel.sendCustomPayload(channelId, text);
|
|
3315
|
+
return;
|
|
2846
3316
|
}
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3317
|
+
default:
|
|
3318
|
+
logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
|
|
3319
|
+
}
|
|
3320
|
+
},
|
|
3321
|
+
acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
|
|
3322
|
+
onInteraction: (cb) => { channel.interactionCallback = cb; },
|
|
3323
|
+
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
3324
|
+
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
3325
|
+
getGroupName: (groupId) => channel.getGroupName(groupId),
|
|
3326
|
+
getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
|
|
3327
|
+
_selfAid: () => channel.getStatus().aid,
|
|
3328
|
+
_selfName: () => channel.getSelfName(),
|
|
3329
|
+
};
|
|
3330
|
+
const policy = {
|
|
3331
|
+
canSwitchProject: (_, identity) => identity === 'owner' || identity === 'admin',
|
|
3332
|
+
canListProjects: (_, identity) => identity === 'owner' || identity === 'admin',
|
|
3333
|
+
canCreateSession: () => true,
|
|
3334
|
+
canDeleteSession: () => true,
|
|
3335
|
+
canImportCliSession: (_, identity) => identity === 'owner' || identity === 'admin',
|
|
3336
|
+
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
3337
|
+
showMiddleResult: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
|
|
3338
|
+
showIdleMonitor: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
|
|
3339
|
+
accumulateErrors: () => true,
|
|
3340
|
+
};
|
|
3341
|
+
return {
|
|
3342
|
+
channelType: 'aun', adapter, channel,
|
|
3343
|
+
policy,
|
|
3344
|
+
options: { flushDelay: inst.flushDelay ?? 3, fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g },
|
|
3345
|
+
connect: () => channel.connect(),
|
|
3346
|
+
disconnect: () => channel.disconnect(),
|
|
3347
|
+
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|
|
3348
|
+
registerBridge(bridge, channelType) {
|
|
3349
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
3350
|
+
handler(aunOptsToInbound(opts, adapter.channelName, channelType));
|
|
3351
|
+
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
3352
|
+
},
|
|
3353
|
+
registerHooks(hookCtx) {
|
|
3354
|
+
channel.setEventBus(hookCtx.eventBus);
|
|
3355
|
+
if (channel.setOnChannelDown) {
|
|
3356
|
+
channel.setOnChannelDown(() => {
|
|
3357
|
+
hookCtx.eventBus.publish({
|
|
3358
|
+
type: 'channel:error',
|
|
3359
|
+
channel: 'aun',
|
|
3360
|
+
channelName: adapter.channelName,
|
|
3361
|
+
status: 'auth_error',
|
|
3362
|
+
message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
3363
|
+
timestamp: Date.now(),
|
|
2851
3364
|
});
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
}
|
|
2863
|
-
return instances[0];
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
if (typeof channel.setDispatchModeResolver === 'function') {
|
|
3368
|
+
channel.setDispatchModeResolver(async (channelId) => {
|
|
3369
|
+
const session = await hookCtx.sessionManager.getActiveSession(adapter.channelName, channelId);
|
|
3370
|
+
return session?.metadata?.dispatchModeOverride;
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
},
|
|
3374
|
+
};
|
|
2864
3375
|
}
|
|
2865
3376
|
}
|