evolclaw 3.3.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 +36 -0
- package/README.md +7 -3
- package/dist/agents/claude-runner.js +23 -27
- package/dist/agents/codex-runner.js +90 -6
- package/dist/agents/runner-types.js +30 -0
- package/dist/aun/outbox.js +14 -2
- package/dist/channels/aun.js +506 -108
- package/dist/channels/feishu.js +29 -5
- package/dist/cli/agent-command.js +591 -0
- package/dist/cli/agent.js +15 -3
- 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 +12 -5027
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/core/channel-loader.js +4 -1
- 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 +81 -0
- package/dist/core/evolagent.js +16 -0
- package/dist/core/message/im-renderer.js +67 -49
- package/dist/core/message/message-bridge.js +30 -9
- package/dist/core/message/message-processor.js +200 -122
- package/dist/core/message/message-queue.js +68 -0
- package/dist/core/permission.js +16 -0
- package/dist/core/session/session-manager.js +59 -13
- package/dist/core/stats/db.js +20 -0
- package/dist/core/stats/writer.js +3 -3
- package/dist/data/error-dict.json +7 -0
- package/dist/index.js +49 -6
- package/dist/ipc.js +99 -0
- package/dist/utils/cross-platform.js +35 -0
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +63 -6
- package/kits/eck_manifest.json +0 -12
- package/package.json +2 -3
- package/dist/core/command-handler.js +0 -4235
- package/dist/core/message/response-depth.js +0 -56
- package/kits/templates/system-fragments/response-depth.md +0 -16
package/dist/channels/aun.js
CHANGED
|
@@ -206,6 +206,20 @@ export class AUNChannel {
|
|
|
206
206
|
const name = cached?.name;
|
|
207
207
|
return name && name !== short ? `${short}(${name})` : short;
|
|
208
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
|
+
}
|
|
209
223
|
extractTextPayload(payload, channelId, senderAid) {
|
|
210
224
|
if (typeof payload === 'string')
|
|
211
225
|
return payload;
|
|
@@ -220,6 +234,7 @@ export class AUNChannel {
|
|
|
220
234
|
if (cardInfo) {
|
|
221
235
|
const actionValue = typeof obj.value === 'string' ? obj.value
|
|
222
236
|
: typeof obj.action_value === 'string' ? obj.action_value : text;
|
|
237
|
+
const threadId = typeof obj.thread_id === 'string' ? obj.thread_id : undefined;
|
|
223
238
|
// 卡片点击者身份:只信认证信封(senderAid 参数,由调用方从 msg.from / msg.sender_aid 提取)。
|
|
224
239
|
// payload 自报字段(from / sender_aid / user_id)不可信,可被客户端伪造,不读取。
|
|
225
240
|
// 两类卡片共用:CommandCard → 伪入站消息的 peerId,ActionInteraction → operatorId。
|
|
@@ -234,9 +249,10 @@ export class AUNChannel {
|
|
|
234
249
|
if (chatType === 'group' && cardInfo.initiatorAid && cardClickerAid
|
|
235
250
|
&& cardClickerAid !== cardInfo.initiatorAid) {
|
|
236
251
|
logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
|
|
252
|
+
this.notifyCardActionFailure(channelId, '⚠️ 仅卡片发起者可操作', cardClickerAid, threadId);
|
|
237
253
|
return '';
|
|
238
254
|
}
|
|
239
|
-
this.messageHandler({
|
|
255
|
+
Promise.resolve(this.messageHandler({
|
|
240
256
|
channelId: channelId || '',
|
|
241
257
|
chatType,
|
|
242
258
|
content: actionValue,
|
|
@@ -244,8 +260,18 @@ export class AUNChannel {
|
|
|
244
260
|
peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
|
|
245
261
|
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
246
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);
|
|
247
267
|
});
|
|
248
268
|
}
|
|
269
|
+
else if (!this.messageHandler) {
|
|
270
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:命令处理器未就绪', cardClickerAid, threadId);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:无效的卡片命令', cardClickerAid, threadId);
|
|
274
|
+
}
|
|
249
275
|
}
|
|
250
276
|
else {
|
|
251
277
|
// ActionInteraction:走 interactionCallback → InteractionRouter
|
|
@@ -260,10 +286,19 @@ export class AUNChannel {
|
|
|
260
286
|
operatorId: cardClickerAid || undefined,
|
|
261
287
|
});
|
|
262
288
|
}
|
|
289
|
+
else {
|
|
290
|
+
this.notifyCardActionFailure(channelId, '❌ 操作失败:交互处理器未就绪', cardClickerAid, threadId);
|
|
291
|
+
}
|
|
263
292
|
}
|
|
264
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
|
+
}
|
|
265
299
|
else {
|
|
266
|
-
|
|
300
|
+
// 非本 agent 发出的卡片(broadcast 模式下其他 agent 的卡)→ 静默忽略
|
|
301
|
+
logger.debug(`${this.logPrefix()} action_card_reply ignored (not owned): cardMsgId=${cardMsgId}`);
|
|
267
302
|
}
|
|
268
303
|
// 始终返回空字符串,阻止消息分发给 agent
|
|
269
304
|
return '';
|
|
@@ -404,6 +439,13 @@ export class AUNChannel {
|
|
|
404
439
|
.replace(/[ \t]+/g, ' ')
|
|
405
440
|
.trim();
|
|
406
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
|
+
}
|
|
407
449
|
extractMentionAids(mentions) {
|
|
408
450
|
const aids = [];
|
|
409
451
|
for (const m of mentions) {
|
|
@@ -470,6 +512,8 @@ export class AUNChannel {
|
|
|
470
512
|
interactionCallback;
|
|
471
513
|
// action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
|
|
472
514
|
cardMessageIdMap = new Map();
|
|
515
|
+
/** 本 agent 曾发出过的卡片 msgId(只增不删,用于区分"过期失效"vs"他人发的卡") */
|
|
516
|
+
ownedCardMsgIds = new Set();
|
|
473
517
|
dispatchModeResolver;
|
|
474
518
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
475
519
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
@@ -508,6 +552,7 @@ export class AUNChannel {
|
|
|
508
552
|
// AID 连接状态(供 status 命令聚合展示)
|
|
509
553
|
aidState;
|
|
510
554
|
aidStatsCollector;
|
|
555
|
+
outboxInFlight = new Set();
|
|
511
556
|
constructor(config) {
|
|
512
557
|
this.config = config;
|
|
513
558
|
this.agentDir = agentDirPath(config.aid);
|
|
@@ -1108,10 +1153,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1108
1153
|
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1109
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 });
|
|
1110
1155
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
1111
|
-
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');
|
|
1112
1157
|
const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
|
|
1113
1158
|
if (threadId)
|
|
1114
1159
|
replyContext.threadId = threadId;
|
|
1160
|
+
replyContext.peerId = fromAid;
|
|
1161
|
+
if (messageId)
|
|
1162
|
+
replyContext.replyToMessageId = messageId;
|
|
1115
1163
|
this.dispatchMessage({
|
|
1116
1164
|
channelId: chatId,
|
|
1117
1165
|
userId: fromAid,
|
|
@@ -1237,7 +1285,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1237
1285
|
// 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1238
1286
|
const firstLineGroup = text.split('\n')[0] || '';
|
|
1239
1287
|
const hasEvolClawTraceGroup = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
1240
|
-
|
|
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) {
|
|
1241
1293
|
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
1242
1294
|
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
1243
1295
|
const echoTs = () => {
|
|
@@ -1262,14 +1314,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1262
1314
|
return;
|
|
1263
1315
|
}
|
|
1264
1316
|
else {
|
|
1265
|
-
// 非 echo
|
|
1266
|
-
|
|
1317
|
+
// 非 echo 消息:mention 过滤
|
|
1318
|
+
// 命令豁免 broadcast:slash 命令在任何 dispatchMode 下都强制走 mention 语义,
|
|
1319
|
+
// 即必须 @ 本 agent(或 @all)才处理,避免广播群里一条命令被全部 agent 各自执行。
|
|
1320
|
+
const enforceMention = dispatchMode === 'mention' || isCommandMsg;
|
|
1321
|
+
if (enforceMention && !mentionedSelf && !mentionedAll) {
|
|
1267
1322
|
this.acknowledgeImmediately(messageId, seq);
|
|
1268
|
-
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))})`);
|
|
1269
1324
|
return;
|
|
1270
1325
|
}
|
|
1271
1326
|
}
|
|
1272
|
-
|
|
1327
|
+
// 命令消息:剥离所有 @(多 agent 被 @ 时各自拿到干净的 /status 各自执行);
|
|
1328
|
+
// 普通消息:仅在唯一 @ 是自己时剥离,保留其他 @ 供 agent 感知。
|
|
1329
|
+
const strippedText = isCommandMsg
|
|
1330
|
+
? this.stripAllMentions(text)
|
|
1331
|
+
: this.stripSelfMentionIfOnly(text, this._aid);
|
|
1273
1332
|
// Detect attachments before the empty-text guard (顶层 + 嵌套)
|
|
1274
1333
|
const rawAttachments = this.collectAllAttachments(payload);
|
|
1275
1334
|
const hasAttachments = rawAttachments.length > 0;
|
|
@@ -1306,7 +1365,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1306
1365
|
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1307
1366
|
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1308
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 });
|
|
1309
|
-
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');
|
|
1310
1369
|
// 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
|
|
1311
1370
|
// 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
|
|
1312
1371
|
const renderMentionAids = Array.from(new Set([
|
|
@@ -1330,7 +1389,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1330
1389
|
mentions,
|
|
1331
1390
|
mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
|
|
1332
1391
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
|
|
1333
|
-
dispatchMode,
|
|
1392
|
+
dispatchMode: serverDispatchMode,
|
|
1334
1393
|
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1335
1394
|
});
|
|
1336
1395
|
}
|
|
@@ -1574,11 +1633,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1574
1633
|
const len = Buffer.byteLength(text, 'utf-8');
|
|
1575
1634
|
if (dir === 'in') {
|
|
1576
1635
|
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');
|
|
1636
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, ownerAid, len, text, false, false, 'inject', 'inject');
|
|
1578
1637
|
}
|
|
1579
1638
|
else {
|
|
1580
1639
|
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');
|
|
1640
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, ownerAid, len, text, false, false, 'inject', 'inject');
|
|
1582
1641
|
}
|
|
1583
1642
|
}
|
|
1584
1643
|
catch (e) {
|
|
@@ -1935,6 +1994,219 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1935
1994
|
onRecall(handler) {
|
|
1936
1995
|
this.recallHandler = handler;
|
|
1937
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
|
+
}
|
|
1938
2210
|
async sendMessage(channelId, text, context) {
|
|
1939
2211
|
if (!text?.trim()) {
|
|
1940
2212
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
@@ -1967,7 +2239,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1967
2239
|
return;
|
|
1968
2240
|
}
|
|
1969
2241
|
// Attempt immediate delivery
|
|
1970
|
-
const ok = await this.deliverTextEntry(entry);
|
|
2242
|
+
const ok = await this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
1971
2243
|
if (ok) {
|
|
1972
2244
|
outbox.remove(this.config.aid, entry.id);
|
|
1973
2245
|
}
|
|
@@ -2049,16 +2321,17 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2049
2321
|
if (!mid) {
|
|
2050
2322
|
const dispatchStatus = result?.message_dispatch?.status;
|
|
2051
2323
|
if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
|
|
2052
|
-
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)}`);
|
|
2053
2325
|
}
|
|
2054
2326
|
else {
|
|
2055
2327
|
logger.warn(`${this.logPrefix()} group.send returned no message_id: ${JSON.stringify(result)}`);
|
|
2056
2328
|
}
|
|
2329
|
+
return false;
|
|
2057
2330
|
}
|
|
2058
2331
|
else {
|
|
2059
2332
|
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
2060
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 });
|
|
2061
|
-
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');
|
|
2062
2335
|
this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
|
|
2063
2336
|
// Observer forward: outbound (group) — 原样转发 SDK SendResult(含 envelope + payload)
|
|
2064
2337
|
this.forwardOutbound(result);
|
|
@@ -2069,11 +2342,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2069
2342
|
const result = await this.callAndTrace('message.send', params);
|
|
2070
2343
|
if (!result || !result.message_id) {
|
|
2071
2344
|
logger.warn(`${this.logPrefix()} message.send returned no message_id: ${JSON.stringify(result)}`);
|
|
2345
|
+
return false;
|
|
2072
2346
|
}
|
|
2073
2347
|
else {
|
|
2074
2348
|
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
2075
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 });
|
|
2076
|
-
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');
|
|
2077
2351
|
this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
|
|
2078
2352
|
// Observer forward: outbound (private) — 原样转发 SDK SendResult(含 envelope + payload)
|
|
2079
2353
|
this.forwardOutbound(result);
|
|
@@ -2090,19 +2364,29 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2090
2364
|
if (isGroup) {
|
|
2091
2365
|
this.trace('OUT', 'group.send.fallback', params);
|
|
2092
2366
|
const result = await this.client.call('group.send', params);
|
|
2093
|
-
this.
|
|
2094
|
-
|
|
2367
|
+
const mid = this.messageIdFromSendResult(result);
|
|
2368
|
+
this.trace('OUT', 'group.send.fallback.ok', { message_id: mid });
|
|
2369
|
+
if (!mid) {
|
|
2095
2370
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2371
|
+
return false;
|
|
2096
2372
|
}
|
|
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);
|
|
2097
2376
|
this.forwardOutbound(result);
|
|
2098
2377
|
}
|
|
2099
2378
|
else {
|
|
2100
2379
|
this.trace('OUT', 'message.send.fallback', params);
|
|
2101
2380
|
const result = await this.client.call('message.send', params);
|
|
2102
|
-
|
|
2103
|
-
|
|
2381
|
+
const mid = result?.message_id;
|
|
2382
|
+
this.trace('OUT', 'message.send.fallback.ok', { message_id: mid });
|
|
2383
|
+
if (!result || !mid) {
|
|
2104
2384
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2385
|
+
return false;
|
|
2105
2386
|
}
|
|
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);
|
|
2106
2390
|
this.forwardOutbound(result);
|
|
2107
2391
|
}
|
|
2108
2392
|
return true;
|
|
@@ -2120,6 +2404,26 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2120
2404
|
}
|
|
2121
2405
|
}
|
|
2122
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
|
+
}
|
|
2123
2427
|
/** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
|
|
2124
2428
|
appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
|
|
2125
2429
|
try {
|
|
@@ -2172,10 +2476,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2172
2476
|
encrypt,
|
|
2173
2477
|
};
|
|
2174
2478
|
try {
|
|
2175
|
-
const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
|
|
2176
|
-
const stage = payload?.stage ?? `items=${itemCount}`;
|
|
2177
|
-
// 提取 thought 文本(只对 kind=text 的 item 写 jsonl,过滤 tool_use/tool_result 等结构化项)
|
|
2178
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。
|
|
2179
2483
|
let thoughtText;
|
|
2180
2484
|
if (Array.isArray(items) && items.length > 0) {
|
|
2181
2485
|
const lastItem = items[items.length - 1];
|
|
@@ -2193,6 +2497,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2193
2497
|
thoughtText = lastItem;
|
|
2194
2498
|
}
|
|
2195
2499
|
}
|
|
2500
|
+
else {
|
|
2501
|
+
thoughtText = this.activityLogText(payload);
|
|
2502
|
+
}
|
|
2196
2503
|
if (this.isGroupId(channelId)) {
|
|
2197
2504
|
params.group_id = targetId;
|
|
2198
2505
|
const putRes = await this.callAndTrace('group.thought.put', params);
|
|
@@ -2201,7 +2508,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2201
2508
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
2202
2509
|
this.forwardOutbound(putRes);
|
|
2203
2510
|
if (thoughtText) {
|
|
2204
|
-
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');
|
|
2205
2512
|
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
|
|
2206
2513
|
}
|
|
2207
2514
|
}
|
|
@@ -2213,7 +2520,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2213
2520
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
2214
2521
|
this.forwardOutbound(putRes);
|
|
2215
2522
|
if (thoughtText) {
|
|
2216
|
-
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');
|
|
2217
2524
|
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
|
|
2218
2525
|
}
|
|
2219
2526
|
}
|
|
@@ -2295,7 +2602,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2295
2602
|
}
|
|
2296
2603
|
return;
|
|
2297
2604
|
}
|
|
2298
|
-
const ok = await this.deliverFileEntry(entry);
|
|
2605
|
+
const ok = await this.withOutboxInFlight(entry, () => this.deliverFileEntry(entry), false);
|
|
2299
2606
|
if (ok) {
|
|
2300
2607
|
outbox.remove(this.config.aid, entry.id);
|
|
2301
2608
|
}
|
|
@@ -2365,6 +2672,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2365
2672
|
filePayload.task_id = context.metadata.taskId;
|
|
2366
2673
|
if (context?.metadata?.chatmode)
|
|
2367
2674
|
filePayload.chatmode = context.metadata.chatmode;
|
|
2675
|
+
// file-link-cache: 回带点击请求的 correlationId,客户端用它把异步到达的文件消息对回这次 fetch 点击
|
|
2676
|
+
if (context?.metadata?.correlationId)
|
|
2677
|
+
filePayload.correlation_id = context.metadata.correlationId;
|
|
2368
2678
|
const isGroup = this.isGroupId(channelId);
|
|
2369
2679
|
const fileTargetAid = channelId;
|
|
2370
2680
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
@@ -2373,6 +2683,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2373
2683
|
: this.shouldEncrypt(encryptTarget);
|
|
2374
2684
|
const params = { payload: filePayload, encrypt };
|
|
2375
2685
|
let sendResult = null;
|
|
2686
|
+
let sentMid = null;
|
|
2376
2687
|
try {
|
|
2377
2688
|
if (isGroup) {
|
|
2378
2689
|
params.group_id = channelId;
|
|
@@ -2380,9 +2691,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2380
2691
|
const result = await this.client.call('group.send', params);
|
|
2381
2692
|
sendResult = result;
|
|
2382
2693
|
const fileMid = result?.message?.message_id ?? result?.message_id;
|
|
2694
|
+
sentMid = fileMid ?? null;
|
|
2383
2695
|
this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
|
|
2384
2696
|
if (!fileMid) {
|
|
2385
2697
|
logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
2698
|
+
return false;
|
|
2386
2699
|
}
|
|
2387
2700
|
}
|
|
2388
2701
|
else {
|
|
@@ -2390,9 +2703,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2390
2703
|
this.trace('OUT', 'message.send.file', params);
|
|
2391
2704
|
const result = await this.client.call('message.send', params);
|
|
2392
2705
|
sendResult = result;
|
|
2393
|
-
|
|
2394
|
-
|
|
2706
|
+
sentMid = result?.message_id ?? null;
|
|
2707
|
+
this.trace('OUT', 'message.send.file.ok', { message_id: sentMid });
|
|
2708
|
+
if (!result || !sentMid) {
|
|
2395
2709
|
logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
2710
|
+
return false;
|
|
2396
2711
|
}
|
|
2397
2712
|
}
|
|
2398
2713
|
}
|
|
@@ -2410,18 +2725,22 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2410
2725
|
const result = await this.client.call('group.send', params);
|
|
2411
2726
|
sendResult = result;
|
|
2412
2727
|
const fbMid = result?.message?.message_id ?? result?.message_id;
|
|
2728
|
+
sentMid = fbMid ?? null;
|
|
2413
2729
|
this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
|
|
2414
2730
|
if (!fbMid) {
|
|
2415
2731
|
logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2732
|
+
return false;
|
|
2416
2733
|
}
|
|
2417
2734
|
}
|
|
2418
2735
|
else {
|
|
2419
2736
|
this.trace('OUT', 'message.send.file.fallback', params);
|
|
2420
2737
|
const result = await this.client.call('message.send', params);
|
|
2421
2738
|
sendResult = result;
|
|
2422
|
-
|
|
2423
|
-
|
|
2739
|
+
sentMid = result?.message_id ?? null;
|
|
2740
|
+
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: sentMid });
|
|
2741
|
+
if (!result || !sentMid) {
|
|
2424
2742
|
logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
2743
|
+
return false;
|
|
2425
2744
|
}
|
|
2426
2745
|
}
|
|
2427
2746
|
}
|
|
@@ -2430,6 +2749,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2430
2749
|
}
|
|
2431
2750
|
}
|
|
2432
2751
|
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
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
|
+
}
|
|
2433
2759
|
if (sendResult)
|
|
2434
2760
|
this.forwardOutbound(sendResult);
|
|
2435
2761
|
return true;
|
|
@@ -2465,10 +2791,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2465
2791
|
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
2466
2792
|
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
2467
2793
|
if (entry.type === 'text') {
|
|
2468
|
-
return this.deliverTextEntry(entry);
|
|
2794
|
+
return this.withOutboxInFlight(entry, () => this.deliverTextEntry(entry), false);
|
|
2469
2795
|
}
|
|
2470
2796
|
else if (entry.type === 'file') {
|
|
2471
|
-
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;
|
|
2472
2802
|
}
|
|
2473
2803
|
return true; // unknown type, discard
|
|
2474
2804
|
});
|
|
@@ -2497,62 +2827,44 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2497
2827
|
progress: 'progress',
|
|
2498
2828
|
};
|
|
2499
2829
|
const statusPayload = {
|
|
2500
|
-
type: 'status',
|
|
2501
|
-
|
|
2830
|
+
type: 'task.status',
|
|
2831
|
+
status: stateMap[status] ?? status,
|
|
2502
2832
|
task_id: taskId,
|
|
2503
2833
|
session_id: sessionId,
|
|
2504
2834
|
severity,
|
|
2835
|
+
terminal: ['done', 'interrupted', 'error', 'timeout'].includes(status),
|
|
2505
2836
|
...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
|
|
2506
2837
|
};
|
|
2507
2838
|
if (context?.threadId)
|
|
2508
2839
|
statusPayload.thread_id = context.threadId;
|
|
2509
2840
|
if (context?.peerId)
|
|
2510
2841
|
statusPayload.initiator = context.peerId;
|
|
2842
|
+
if (context?.metadata?.chatmode)
|
|
2843
|
+
statusPayload.chatmode = context.metadata.chatmode;
|
|
2511
2844
|
if (context?.replyToMessageId)
|
|
2512
2845
|
statusPayload.ref_message_id = context.replyToMessageId;
|
|
2513
|
-
const
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
:
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
return c.call(method, params).catch((e) => {
|
|
2534
|
-
if (encrypt && e instanceof E2EEError) {
|
|
2535
|
-
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
2536
|
-
logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
|
|
2537
|
-
const c2 = this.client;
|
|
2538
|
-
if (!c2)
|
|
2539
|
-
return null;
|
|
2540
|
-
const fallbackParams = { ...params, encrypt: false };
|
|
2541
|
-
return c2.call(method, fallbackParams).catch((e2) => {
|
|
2542
|
-
logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
|
|
2543
|
-
return null;
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
|
|
2547
|
-
return null;
|
|
2548
|
-
});
|
|
2549
|
-
};
|
|
2550
|
-
const method = isGroup ? 'group.send' : 'message.send';
|
|
2551
|
-
sendOne(method, statusPayload, 'status').then(result => {
|
|
2552
|
-
if (result)
|
|
2553
|
-
this.forwardOutbound(result);
|
|
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}`);
|
|
2554
2866
|
}).catch(() => { });
|
|
2555
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
|
|
2867
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true, undefined, undefined, 'notify');
|
|
2556
2868
|
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
2557
2869
|
const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
|
|
2558
2870
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
@@ -2775,6 +3087,31 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2775
3087
|
return undefined; // 不写缓存,下次仍可重试
|
|
2776
3088
|
}
|
|
2777
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
|
+
}
|
|
2778
3115
|
}
|
|
2779
3116
|
// Plugin implementation
|
|
2780
3117
|
export class AUNChannelPlugin {
|
|
@@ -2804,43 +3141,89 @@ export class AUNChannelPlugin {
|
|
|
2804
3141
|
send: async (envelope, payload) => {
|
|
2805
3142
|
const replyCtx = envelope.replyContext;
|
|
2806
3143
|
const channelId = envelope.channelId;
|
|
3144
|
+
const taskBase = () => channel.buildTaskPayloadBase(envelope, replyCtx);
|
|
2807
3145
|
switch (payload.kind) {
|
|
2808
3146
|
case 'result.text':
|
|
2809
3147
|
case 'command.result':
|
|
2810
|
-
case 'command.error':
|
|
2811
|
-
case 'system.notice':
|
|
2812
|
-
case 'system.error':
|
|
2813
|
-
case 'result.error': {
|
|
3148
|
+
case 'command.error': {
|
|
2814
3149
|
const sendCtx = { ...(replyCtx ?? {}) };
|
|
2815
3150
|
if (payload.kind === 'result.text' && payload.isFinal)
|
|
2816
3151
|
sendCtx.title = '✅ 最终回复:';
|
|
2817
3152
|
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
2818
3153
|
return;
|
|
2819
3154
|
}
|
|
2820
|
-
case '
|
|
2821
|
-
await channel.
|
|
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);
|
|
2822
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
|
+
}
|
|
2823
3195
|
case 'result.image': {
|
|
2824
3196
|
const buf = payload.data;
|
|
2825
3197
|
const b64 = buf.toString('base64');
|
|
2826
|
-
await channel.
|
|
3198
|
+
await channel.sendContentPayload(channelId, {
|
|
2827
3199
|
type: 'image', alt: payload.alt, data_base64: b64, mime_type: payload.mimeType,
|
|
2828
|
-
},
|
|
3200
|
+
}, {
|
|
3201
|
+
contentKind: 'image',
|
|
3202
|
+
context: replyCtx,
|
|
3203
|
+
logText: payload.alt ? `[image] ${payload.alt}` : '[image]',
|
|
3204
|
+
});
|
|
2829
3205
|
return;
|
|
2830
3206
|
}
|
|
2831
3207
|
case 'activity.batch': {
|
|
2832
|
-
const
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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);
|
|
3221
|
+
if (envelope.chatmode === 'proactive') {
|
|
3222
|
+
await channel.sendThought(channelId, envelope.taskId, aunPayload, replyCtx);
|
|
3223
|
+
}
|
|
3224
|
+
else {
|
|
3225
|
+
await channel.sendReliableStructured(channelId, aunPayload, replyCtx, channel.activityLogText(item));
|
|
3226
|
+
}
|
|
2844
3227
|
}
|
|
2845
3228
|
return;
|
|
2846
3229
|
}
|
|
@@ -2882,11 +3265,18 @@ export class AUNChannelPlugin {
|
|
|
2882
3265
|
aunCard.initiator = req.initiatorId;
|
|
2883
3266
|
if (replyCtx?.threadId)
|
|
2884
3267
|
aunCard.thread_id = replyCtx.threadId;
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
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
|
+
});
|
|
2890
3280
|
}
|
|
2891
3281
|
else if (req.kind.kind === 'command-card') {
|
|
2892
3282
|
const card = req.kind;
|
|
@@ -2894,18 +3284,25 @@ export class AUNChannelPlugin {
|
|
|
2894
3284
|
type: 'action_card',
|
|
2895
3285
|
title: card.title,
|
|
2896
3286
|
actions: card.buttons.map(btn => ({
|
|
2897
|
-
label: btn.label, value: btn.command, style: btn.style ?? 'default', behavior: 'reply',
|
|
3287
|
+
label: btn.label, value: btn.command, style: btn.style ?? 'default', behavior: 'reply', disabled: btn.disabled || undefined,
|
|
2898
3288
|
})),
|
|
2899
3289
|
};
|
|
2900
3290
|
if (card.body)
|
|
2901
3291
|
aunCard.description = card.body;
|
|
2902
3292
|
if (replyCtx?.threadId)
|
|
2903
3293
|
aunCard.thread_id = replyCtx.threadId;
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
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
|
+
});
|
|
2909
3306
|
}
|
|
2910
3307
|
else if (payload.fallbackText) {
|
|
2911
3308
|
await channel.sendMessage(channelId, payload.fallbackText, replyCtx);
|
|
@@ -2926,6 +3323,7 @@ export class AUNChannelPlugin {
|
|
|
2926
3323
|
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
2927
3324
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
2928
3325
|
getGroupName: (groupId) => channel.getGroupName(groupId),
|
|
3326
|
+
getGroupMemberRole: (groupId, aid) => channel.getGroupMemberRole(groupId, aid),
|
|
2929
3327
|
_selfAid: () => channel.getStatus().aid,
|
|
2930
3328
|
_selfName: () => channel.getSelfName(),
|
|
2931
3329
|
};
|
|
@@ -2969,7 +3367,7 @@ export class AUNChannelPlugin {
|
|
|
2969
3367
|
if (typeof channel.setDispatchModeResolver === 'function') {
|
|
2970
3368
|
channel.setDispatchModeResolver(async (channelId) => {
|
|
2971
3369
|
const session = await hookCtx.sessionManager.getActiveSession(adapter.channelName, channelId);
|
|
2972
|
-
return session?.metadata?.
|
|
3370
|
+
return session?.metadata?.dispatchModeOverride;
|
|
2973
3371
|
});
|
|
2974
3372
|
}
|
|
2975
3373
|
},
|