evolclaw 2.8.3 → 3.1.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/README.md +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
package/dist/channels/aun.js
CHANGED
|
@@ -4,28 +4,55 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import os from 'os';
|
|
6
6
|
import { logger, localTimestamp } from '../utils/logger.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { LogWriter } from '../utils/log-writer.js';
|
|
8
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
9
|
+
import { resolvePaths, getPackageRoot } from '../paths.js';
|
|
9
10
|
import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
import { appendAidEvent } from '../utils/instance-registry.js';
|
|
12
|
+
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
13
|
+
import { loadAgent, saveAgent } from '../config-store.js';
|
|
14
|
+
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
15
|
+
import * as outbox from '../aun/outbox.js';
|
|
16
|
+
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
17
|
+
/**
|
|
18
|
+
* 构造 connect extra_info:自描述本进程身份。
|
|
19
|
+
*
|
|
20
|
+
* 用途:另一个进程踢掉本连接时,本进程会从 SDK 'gateway.disconnect' 事件的
|
|
21
|
+
* detail.new_extra_info 里看到对方的 extra_info;同时 detail.self_extra_info
|
|
22
|
+
* 是本进程当时连接时上报的内容。把双方信息打到日志便于诊断"谁踢了谁"。
|
|
23
|
+
*
|
|
24
|
+
* 字段需保持稳定(被踢方靠它分辨对方身份)。
|
|
25
|
+
*/
|
|
26
|
+
function buildConnectExtraInfo(opts) {
|
|
27
|
+
const startedAt = getProcessStartTime(process.pid) ?? Date.now();
|
|
28
|
+
return {
|
|
29
|
+
app: 'evolclaw',
|
|
30
|
+
version: getEvolclawVersion(),
|
|
31
|
+
pid: process.pid,
|
|
32
|
+
started_at: startedAt,
|
|
33
|
+
started_at_iso: new Date(startedAt).toISOString(),
|
|
34
|
+
hostname: os.hostname(),
|
|
35
|
+
platform: process.platform,
|
|
36
|
+
node_version: process.version,
|
|
37
|
+
evolclaw_home: process.env.EVOLCLAW_HOME || '',
|
|
38
|
+
launched_by: process.env.EVOLCLAW_LAUNCHED_BY || '',
|
|
39
|
+
aid: opts.aid,
|
|
40
|
+
agent_name: opts.agentName ?? '',
|
|
41
|
+
channel_name: opts.channelName ?? '',
|
|
20
42
|
};
|
|
21
|
-
return map[ext] || 'application/octet-stream';
|
|
22
43
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
let _cachedVersion = null;
|
|
45
|
+
function getEvolclawVersion() {
|
|
46
|
+
if (_cachedVersion !== null)
|
|
47
|
+
return _cachedVersion;
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
50
|
+
_cachedVersion = String(pkg.version ?? '');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
_cachedVersion = '';
|
|
54
|
+
}
|
|
55
|
+
return _cachedVersion;
|
|
29
56
|
}
|
|
30
57
|
export class AUNChannel {
|
|
31
58
|
config;
|
|
@@ -34,13 +61,16 @@ export class AUNChannel {
|
|
|
34
61
|
messageHandler;
|
|
35
62
|
recallHandler;
|
|
36
63
|
connected = false;
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
traceWriter = null;
|
|
65
|
+
eventBus = null;
|
|
66
|
+
ownerBoundHandler = null;
|
|
67
|
+
queuedHandler = null;
|
|
68
|
+
pendingEchoMessages = new Map();
|
|
69
|
+
isEchoSending = false;
|
|
39
70
|
trace(dir, event, data) {
|
|
40
71
|
if (!this.config.aunTrace)
|
|
41
72
|
return;
|
|
42
|
-
this.
|
|
43
|
-
if (!this.traceStream)
|
|
73
|
+
if (!this.traceWriter)
|
|
44
74
|
return;
|
|
45
75
|
// 自动从 data 推断顶层字段(self_aid / peer_aid / group_id / task_id / chatmode),
|
|
46
76
|
// 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
|
|
@@ -64,7 +94,7 @@ export class AUNChannel {
|
|
|
64
94
|
if (chatmode)
|
|
65
95
|
topContext.chatmode = chatmode;
|
|
66
96
|
const line = JSON.stringify({ ts: localTimestamp(), dir, event, ...topContext, data });
|
|
67
|
-
this.
|
|
97
|
+
this.traceWriter.write(line);
|
|
68
98
|
}
|
|
69
99
|
/** 日志前缀(含 self aid 简称,多实例可识别) */
|
|
70
100
|
logPrefix() {
|
|
@@ -109,40 +139,6 @@ export class AUNChannel {
|
|
|
109
139
|
throw e;
|
|
110
140
|
}
|
|
111
141
|
}
|
|
112
|
-
static AUN_TRACE_RE = /^aun-\d{8}-\d{2}\.log$/;
|
|
113
|
-
static AUN_RETAIN_HOURS = 12;
|
|
114
|
-
rotateTraceIfNeeded() {
|
|
115
|
-
const d = new Date();
|
|
116
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
117
|
-
const hourTag = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}`;
|
|
118
|
-
if (this.traceHourTag === hourTag && this.traceStream)
|
|
119
|
-
return;
|
|
120
|
-
if (this.traceStream) {
|
|
121
|
-
this.traceStream.end();
|
|
122
|
-
this.traceStream = null;
|
|
123
|
-
}
|
|
124
|
-
this.traceHourTag = hourTag;
|
|
125
|
-
const logPath = path.join(resolvePaths().logs, `aun-${hourTag}.log`);
|
|
126
|
-
this.traceStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
127
|
-
this.cleanupOldTraceLogs();
|
|
128
|
-
}
|
|
129
|
-
cleanupOldTraceLogs() {
|
|
130
|
-
const logsDir = resolvePaths().logs;
|
|
131
|
-
const cutoff = Date.now() - AUNChannel.AUN_RETAIN_HOURS * 60 * 60 * 1000;
|
|
132
|
-
try {
|
|
133
|
-
for (const name of fs.readdirSync(logsDir)) {
|
|
134
|
-
if (!AUNChannel.AUN_TRACE_RE.test(name))
|
|
135
|
-
continue;
|
|
136
|
-
try {
|
|
137
|
-
const full = path.join(logsDir, name);
|
|
138
|
-
if (fs.statSync(full).mtimeMs < cutoff)
|
|
139
|
-
fs.unlinkSync(full);
|
|
140
|
-
}
|
|
141
|
-
catch { }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
catch { }
|
|
145
|
-
}
|
|
146
142
|
/** 判断 channelId 是否为群组 ID
|
|
147
143
|
* - 新格式:group.{issuer}/{group_no|group_name}
|
|
148
144
|
* - 数字群号:{group_no}.{issuer}(如 11117.agentid.pub)
|
|
@@ -174,17 +170,191 @@ export class AUNChannel {
|
|
|
174
170
|
const name = cached?.name;
|
|
175
171
|
return name && name !== short ? `${short}(${name})` : short;
|
|
176
172
|
}
|
|
177
|
-
extractTextPayload(payload) {
|
|
173
|
+
extractTextPayload(payload, channelId, senderAid) {
|
|
178
174
|
if (typeof payload === 'string')
|
|
179
175
|
return payload;
|
|
180
176
|
if (payload && typeof payload === 'object') {
|
|
181
|
-
const
|
|
182
|
-
|
|
177
|
+
const obj = payload;
|
|
178
|
+
const text = typeof obj.text === 'string' ? obj.text : '';
|
|
179
|
+
// action_card_reply:卡片交互回复,触发 interactionCallback,不分发给 agent
|
|
180
|
+
if (obj.type === 'action_card_reply') {
|
|
181
|
+
const cardMsgId = typeof obj.ref_message_id === 'string' ? obj.ref_message_id
|
|
182
|
+
: typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
|
|
183
|
+
const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
|
|
184
|
+
if (cardInfo) {
|
|
185
|
+
const actionValue = typeof obj.value === 'string' ? obj.value
|
|
186
|
+
: typeof obj.action_value === 'string' ? obj.action_value : text;
|
|
187
|
+
if (cardInfo.isCommandCard) {
|
|
188
|
+
// CommandCard:action_value 是完整 slash 命令,构造伪入站消息
|
|
189
|
+
this.cardMessageIdMap.delete(cardMsgId);
|
|
190
|
+
if (this.messageHandler && actionValue.startsWith('/')) {
|
|
191
|
+
const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
|
|
192
|
+
// 卡片点击者身份:优先 payload.from / payload.sender_aid / payload.user_id,
|
|
193
|
+
// 再 fallback 到外层 senderAid,最后用 cardInfo 中记录的原始命令发起者
|
|
194
|
+
const cardClickerAid = (typeof obj.from === 'string' && obj.from)
|
|
195
|
+
|| (typeof obj.sender_aid === 'string' && obj.sender_aid)
|
|
196
|
+
|| (typeof obj.user_id === 'string' && obj.user_id)
|
|
197
|
+
|| senderAid
|
|
198
|
+
|| cardInfo.initiatorAid
|
|
199
|
+
|| channelId || '';
|
|
200
|
+
// Initiator 校验:群聊中仅卡片发起者可操作(与飞书行为对齐)
|
|
201
|
+
if (cardInfo.initiatorAid && cardClickerAid
|
|
202
|
+
&& cardClickerAid !== cardInfo.initiatorAid
|
|
203
|
+
&& !this.isGroupId(cardClickerAid)) {
|
|
204
|
+
logger.info(`${this.logPrefix()} CommandCard rejected: clicker=${cardClickerAid} initiator=${cardInfo.initiatorAid} mid=${cardMsgId}`);
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
this.messageHandler({
|
|
208
|
+
channelId: channelId || '',
|
|
209
|
+
chatType,
|
|
210
|
+
content: actionValue,
|
|
211
|
+
peerId: cardClickerAid,
|
|
212
|
+
peerName: typeof obj.label === 'string' ? obj.label : typeof obj.action_label === 'string' ? obj.action_label : undefined,
|
|
213
|
+
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
214
|
+
source: 'card-trigger',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// ActionInteraction:走 interactionCallback → InteractionRouter
|
|
220
|
+
// callback 未注册时保留 map entry(TTL 清理),给 router 留重试机会
|
|
221
|
+
if (this.interactionCallback) {
|
|
222
|
+
this.cardMessageIdMap.delete(cardMsgId);
|
|
223
|
+
this.interactionCallback({
|
|
224
|
+
type: 'interaction.response',
|
|
225
|
+
id: cardInfo.requestId,
|
|
226
|
+
action: actionValue,
|
|
227
|
+
values: { text, action_label: obj.label ?? obj.action_label, behavior: obj.behavior },
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
logger.debug(`${this.logPrefix()} action_card_reply dropped: cardMsgId=${cardMsgId} hasCallback=${!!this.interactionCallback}`);
|
|
234
|
+
}
|
|
235
|
+
// 始终返回空字符串,阻止消息分发给 agent
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
// quote 类型:拼接被引用内容(支持 text / image / file attachments)
|
|
239
|
+
if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
|
|
240
|
+
const q = obj.quote;
|
|
241
|
+
const quotedText = typeof q.text === 'string' ? q.text : '';
|
|
242
|
+
const sender = typeof q.sender_display === 'string' ? q.sender_display : '';
|
|
243
|
+
const prefix = sender ? `${sender}: ` : '';
|
|
244
|
+
// 构建引用内容:文本 + 附件描述
|
|
245
|
+
const quoteParts = [];
|
|
246
|
+
if (quotedText)
|
|
247
|
+
quoteParts.push(quotedText);
|
|
248
|
+
if (Array.isArray(q.attachments)) {
|
|
249
|
+
for (const att of q.attachments) {
|
|
250
|
+
if (att && typeof att === 'object') {
|
|
251
|
+
const ct = typeof att.content_type === 'string' ? att.content_type : '';
|
|
252
|
+
const fn = typeof att.filename === 'string' ? att.filename : '';
|
|
253
|
+
if (ct.startsWith('image/')) {
|
|
254
|
+
quoteParts.push(fn ? `[图片: ${fn}]` : '[图片]');
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
quoteParts.push(fn ? `[文件: ${fn}]` : '[文件]');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (quoteParts.length > 0) {
|
|
263
|
+
const lines = quoteParts.join('\n').split('\n');
|
|
264
|
+
const quoted = lines.map((line, i) => `> ${i === 0 ? prefix : ''}${line}`).join('\n');
|
|
265
|
+
return text ? `${quoted}\n\n${text}` : quoted;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// merge 类型:合并转发消息,展开子消息为可读文本
|
|
269
|
+
if (obj.type === 'merge') {
|
|
270
|
+
const title = typeof obj.title === 'string' ? obj.title : '合并转发消息';
|
|
271
|
+
const parts = [`以下是转发的合并消息「${title}」:\n---`];
|
|
272
|
+
if (Array.isArray(obj.items)) {
|
|
273
|
+
for (const item of obj.items) {
|
|
274
|
+
if (item && typeof item === 'object') {
|
|
275
|
+
const sender = typeof item.sender_display === 'string' ? item.sender_display : '';
|
|
276
|
+
const itemText = typeof item.text === 'string' ? item.text : '';
|
|
277
|
+
const itemType = typeof item.type === 'string' ? item.type : '';
|
|
278
|
+
// 根据子消息类型构建展示
|
|
279
|
+
const lineParts = [];
|
|
280
|
+
if (itemText)
|
|
281
|
+
lineParts.push(itemText);
|
|
282
|
+
// 子消息附件(image/file)
|
|
283
|
+
if (Array.isArray(item.attachments)) {
|
|
284
|
+
for (const att of item.attachments) {
|
|
285
|
+
if (att && typeof att === 'object') {
|
|
286
|
+
const ct = typeof att.content_type === 'string' ? att.content_type : '';
|
|
287
|
+
const fn = typeof att.filename === 'string' ? att.filename : '';
|
|
288
|
+
if (ct.startsWith('image/') || itemType === 'image') {
|
|
289
|
+
lineParts.push(fn ? `[图片: ${fn}]` : '[图片]');
|
|
290
|
+
}
|
|
291
|
+
else if (ct.startsWith('video/') || itemType === 'video') {
|
|
292
|
+
lineParts.push(fn ? `[视频: ${fn}]` : '[视频]');
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
lineParts.push(fn ? `[文件: ${fn}]` : '[文件]');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const content = lineParts.join(' ') || `[${itemType || '未知类型'}]`;
|
|
301
|
+
parts.push(sender ? `${sender}: ${content}` : content);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (typeof obj.summary === 'string' && obj.summary) {
|
|
306
|
+
parts.push(`\n[摘要] ${obj.summary}`);
|
|
307
|
+
}
|
|
308
|
+
parts.push('---');
|
|
309
|
+
return parts.join('\n');
|
|
310
|
+
}
|
|
311
|
+
if (typeof obj.text === 'string')
|
|
183
312
|
return text;
|
|
184
313
|
return JSON.stringify(payload);
|
|
185
314
|
}
|
|
186
315
|
return '';
|
|
187
316
|
}
|
|
317
|
+
/** 收集 payload 中所有需要下载的 attachments(顶层 + merge.items + quote.quote),按 url 去重 */
|
|
318
|
+
collectAllAttachments(payload) {
|
|
319
|
+
if (!payload || typeof payload !== 'object')
|
|
320
|
+
return [];
|
|
321
|
+
const obj = payload;
|
|
322
|
+
const result = [];
|
|
323
|
+
const seen = new Set();
|
|
324
|
+
const add = (att) => {
|
|
325
|
+
if (!att || typeof att !== 'object')
|
|
326
|
+
return;
|
|
327
|
+
const key = att.url || att.object_key || '';
|
|
328
|
+
if (key && seen.has(key))
|
|
329
|
+
return;
|
|
330
|
+
if (key)
|
|
331
|
+
seen.add(key);
|
|
332
|
+
result.push(att);
|
|
333
|
+
};
|
|
334
|
+
// 顶层 attachments
|
|
335
|
+
if (Array.isArray(obj.attachments)) {
|
|
336
|
+
for (const att of obj.attachments)
|
|
337
|
+
add(att);
|
|
338
|
+
}
|
|
339
|
+
// merge.items 中的子消息 attachments
|
|
340
|
+
if (obj.type === 'merge' && Array.isArray(obj.items)) {
|
|
341
|
+
for (const item of obj.items) {
|
|
342
|
+
if (item && typeof item === 'object' && Array.isArray(item.attachments)) {
|
|
343
|
+
for (const att of item.attachments)
|
|
344
|
+
add(att);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// quote.quote 中的 attachments
|
|
349
|
+
if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
|
|
350
|
+
const q = obj.quote;
|
|
351
|
+
if (Array.isArray(q.attachments)) {
|
|
352
|
+
for (const att of q.attachments)
|
|
353
|
+
add(att);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
188
358
|
hasExplicitMention(text, target) {
|
|
189
359
|
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
190
360
|
return new RegExp(`(^|\\s)@${escaped}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`).test(text);
|
|
@@ -234,31 +404,24 @@ export class AUNChannel {
|
|
|
234
404
|
}
|
|
235
405
|
return out;
|
|
236
406
|
}
|
|
237
|
-
buildGroupReplyContext(taskId, senderAid, encrypted) {
|
|
407
|
+
buildGroupReplyContext(taskId, senderAid, encrypted, messageId) {
|
|
238
408
|
const replyContext = { metadata: { encrypted } };
|
|
239
409
|
if (taskId)
|
|
240
410
|
replyContext.threadId = taskId;
|
|
241
411
|
replyContext.peerId = senderAid;
|
|
412
|
+
if (messageId)
|
|
413
|
+
replyContext.replyToMessageId = messageId;
|
|
242
414
|
return replyContext;
|
|
243
415
|
}
|
|
244
|
-
acknowledgeImmediately(messageId,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
logger.debug(`${this.logPrefix()} Immediate ack failed: ${e}`);
|
|
248
|
-
});
|
|
249
|
-
}
|
|
416
|
+
acknowledgeImmediately(messageId, _seq) {
|
|
417
|
+
// SDK internally manages seq tracking and ack — do not call message.ack RPC directly,
|
|
418
|
+
// as it corrupts the SDK's seqTracker state and breaks V2 e2ee message pull.
|
|
250
419
|
if (messageId)
|
|
251
420
|
this.messageSeqMap.delete(messageId);
|
|
252
421
|
}
|
|
253
|
-
shouldEncrypt(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return true;
|
|
257
|
-
if (Date.now() - cached.ts > AUNChannel.E2EE_PROBE_TTL) {
|
|
258
|
-
this.peerE2ee.delete(peerId);
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
return cached.ok;
|
|
422
|
+
shouldEncrypt(_peerId) {
|
|
423
|
+
// Default to plaintext; only encrypt when session is explicitly marked encrypted
|
|
424
|
+
return false;
|
|
262
425
|
}
|
|
263
426
|
_aid;
|
|
264
427
|
_selfName; // 本地 agent.md 中的 name,首次 connect 时读取
|
|
@@ -270,36 +433,96 @@ export class AUNChannel {
|
|
|
270
433
|
peerE2ee = new Map();
|
|
271
434
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
272
435
|
plaintextRecv = 0;
|
|
273
|
-
|
|
436
|
+
interactionCallback;
|
|
437
|
+
// action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
|
|
438
|
+
cardMessageIdMap = new Map();
|
|
439
|
+
dispatchModeResolver;
|
|
274
440
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
275
441
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
276
442
|
'merge', 'link', 'location', 'personal_card',
|
|
277
443
|
]);
|
|
278
|
-
// Reconnect state
|
|
444
|
+
// Reconnect state
|
|
445
|
+
// SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
|
|
446
|
+
// 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
|
|
447
|
+
// 2. terminal_failed with kick reason:直接 5min 后重试,不刷屏
|
|
279
448
|
intentionalDisconnect = false;
|
|
280
|
-
reconnectAttempt = 0;
|
|
281
449
|
reconnectTimer = null;
|
|
282
|
-
|
|
450
|
+
reconnectGeneration = 0; // 防止并发 initClient:每次 takeoverReconnect 递增,回调中校验
|
|
283
451
|
onChannelDown;
|
|
284
|
-
//
|
|
452
|
+
// initClient concurrency guard: 防止多个 initClient 并发执行
|
|
453
|
+
initInProgress = false;
|
|
454
|
+
// Flap detection: 连接寿命 < FLAP_WINDOW_MS 累计 FLAP_THRESHOLD 次,判定为持续被踢
|
|
455
|
+
connectedAt = 0;
|
|
456
|
+
flapCount = 0;
|
|
457
|
+
static FLAP_WINDOW_MS = 30_000;
|
|
458
|
+
static FLAP_THRESHOLD = 3;
|
|
459
|
+
// 接管后的统一退避时间(kicked / flap / terminal_failed)
|
|
460
|
+
static TAKEOVER_DELAY_MS = 5 * 60 * 1000; // 5min
|
|
461
|
+
// 一般 terminal_failed(非 kick)的兜底退避
|
|
462
|
+
static FALLBACK_DELAY_MS = 60 * 1000; // 1min
|
|
463
|
+
// SDK reconnect logging throttle(避免 attempt 自增刷屏)
|
|
285
464
|
lastReconnectLogTime = 0;
|
|
286
465
|
lastReconnectLogAttempt = 0;
|
|
287
|
-
static RECONNECT_LOG_INTERVAL = 60_000;
|
|
288
|
-
static RECONNECT_LOG_STEP = 100;
|
|
289
|
-
|
|
466
|
+
static RECONNECT_LOG_INTERVAL = 60_000;
|
|
467
|
+
static RECONNECT_LOG_STEP = 100;
|
|
468
|
+
// AID 连接状态(供 status 命令聚合展示)
|
|
469
|
+
aidState;
|
|
470
|
+
aidStatsCollector;
|
|
290
471
|
constructor(config) {
|
|
291
472
|
this.config = config;
|
|
292
473
|
if (config.aunTrace) {
|
|
293
|
-
this.
|
|
294
|
-
|
|
295
|
-
|
|
474
|
+
this.traceWriter = new LogWriter({
|
|
475
|
+
baseName: 'aun',
|
|
476
|
+
logDir: resolvePaths().logs,
|
|
477
|
+
rotation: 'hourly',
|
|
478
|
+
retention: { hours: 12 },
|
|
479
|
+
});
|
|
480
|
+
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 12h retention): ${this.traceWriter.activePath()}`);
|
|
481
|
+
}
|
|
482
|
+
this.aidState = {
|
|
483
|
+
aid: config.aid,
|
|
484
|
+
agentName: config.agentName ?? '<unknown>',
|
|
485
|
+
channelName: config.channelName ?? 'aun',
|
|
486
|
+
status: 'disabled',
|
|
487
|
+
reconnectCount: 0,
|
|
488
|
+
flapCount: 0,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/** Snapshot of AID connection state for status / IPC aggregation */
|
|
492
|
+
getAidState() {
|
|
493
|
+
return { ...this.aidState, flapCount: this.flapCount };
|
|
494
|
+
}
|
|
495
|
+
setAidStatsCollector(collector) {
|
|
496
|
+
this.aidStatsCollector = collector;
|
|
497
|
+
}
|
|
498
|
+
setAidStatus(status, extra) {
|
|
499
|
+
this.aidState.status = status;
|
|
500
|
+
if (extra)
|
|
501
|
+
Object.assign(this.aidState, extra);
|
|
296
502
|
}
|
|
297
503
|
async connect() {
|
|
298
504
|
this.intentionalDisconnect = false;
|
|
299
|
-
this.
|
|
505
|
+
this.flapCount = 0;
|
|
506
|
+
this.connectedAt = 0;
|
|
507
|
+
this.setAidStatus('reconnecting', { lastAttemptAt: Date.now() });
|
|
300
508
|
await this.initClient();
|
|
509
|
+
this.startOutboxTimer();
|
|
301
510
|
}
|
|
302
511
|
async initClient() {
|
|
512
|
+
// 防止并发 initClient(sendMessage 触发 + timer 触发同时进入)
|
|
513
|
+
if (this.initInProgress) {
|
|
514
|
+
logger.info(`${this.logPrefix()} initClient already in progress, skipping`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this.initInProgress = true;
|
|
518
|
+
try {
|
|
519
|
+
await this._initClientInner();
|
|
520
|
+
}
|
|
521
|
+
finally {
|
|
522
|
+
this.initInProgress = false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async _initClientInner() {
|
|
303
526
|
// Clean up existing client if any
|
|
304
527
|
if (this.client) {
|
|
305
528
|
this.trace('OUT', 'client.close', { reason: 'initClient' });
|
|
@@ -364,6 +587,11 @@ export class AUNChannel {
|
|
|
364
587
|
// trace is handled inside handleConnectionState with throttling
|
|
365
588
|
this.handleConnectionState(data);
|
|
366
589
|
});
|
|
590
|
+
// gateway 被踢/服务端主动断开(含同槽位互踢的 self/new extra_info)
|
|
591
|
+
this.client.on('gateway.disconnect', (data) => {
|
|
592
|
+
this.trace('IN', 'gateway.disconnect', data);
|
|
593
|
+
this.handleGatewayDisconnect(data);
|
|
594
|
+
});
|
|
367
595
|
this.client.on('message.recalled', (data) => {
|
|
368
596
|
this.trace('IN', 'message.recalled', data);
|
|
369
597
|
if (data && typeof data === 'object') {
|
|
@@ -424,6 +652,7 @@ export class AUNChannel {
|
|
|
424
652
|
accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
|
|
425
653
|
if (!accessToken) {
|
|
426
654
|
logger.error(`${this.logPrefix()} No accessToken fallback available, scheduling retry`);
|
|
655
|
+
this.setAidStatus('failed', { lastError: `${errName}: ${errMsg}`.slice(0, 80) });
|
|
427
656
|
this.scheduleReconnect();
|
|
428
657
|
throw new Error('Authentication failed and no accessToken fallback available');
|
|
429
658
|
}
|
|
@@ -431,15 +660,26 @@ export class AUNChannel {
|
|
|
431
660
|
}
|
|
432
661
|
// Connect (SDK auto_reconnect handles transient failures)
|
|
433
662
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
663
|
+
const extraInfo = buildConnectExtraInfo({
|
|
664
|
+
aid: this.config.aid,
|
|
665
|
+
agentName: this.config.agentName,
|
|
666
|
+
channelName: this.config.channelName,
|
|
667
|
+
});
|
|
668
|
+
this.trace('OUT', 'client.connect', { gateway: this.client._gatewayUrl, extra_info: extraInfo });
|
|
669
|
+
await this.client.connect({ access_token: accessToken, gateway: this.client._gatewayUrl, extra_info: extraInfo },
|
|
670
|
+
// max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
|
|
671
|
+
// initial_delay=1s,max_delay=300s(5min 封顶)
|
|
672
|
+
{ auto_reconnect: true, retry: { max_attempts: 0, initial_delay: 1.0, max_delay: 300.0 } });
|
|
436
673
|
this.trace('OUT', 'client.connect.ok', { aid: this.client.aid });
|
|
437
674
|
this._aid = this.client.aid ?? undefined;
|
|
438
675
|
const deviceId = this.client._device_id ?? '';
|
|
439
676
|
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
440
677
|
this._selfName = this.loadSelfName(aidName);
|
|
678
|
+
if (this._selfName && this.aidStatsCollector)
|
|
679
|
+
this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
|
|
441
680
|
this.connected = true;
|
|
442
|
-
this.
|
|
681
|
+
this.connectedAt = Date.now();
|
|
682
|
+
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.client?._gatewayUrl });
|
|
443
683
|
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
444
684
|
// if cert is missing, it falls back to public key SPKI fingerprint which
|
|
445
685
|
// causes peer cert lookup failures. Backfill from keystore if needed.
|
|
@@ -452,43 +692,44 @@ export class AUNChannel {
|
|
|
452
692
|
}
|
|
453
693
|
}
|
|
454
694
|
logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
|
|
695
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
|
|
696
|
+
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
|
|
455
697
|
// Send welcome message to owner after first connection
|
|
456
698
|
await this.sendWelcomeMessage();
|
|
457
699
|
}
|
|
458
700
|
catch (e) {
|
|
459
701
|
this.trace('OUT', 'client.connect.error', { error: String(e) });
|
|
460
702
|
logger.error(`${this.logPrefix()} Connection failed: ${e}`);
|
|
703
|
+
this.setAidStatus('failed', { lastError: String(e).slice(0, 80) });
|
|
461
704
|
this.scheduleReconnect();
|
|
462
705
|
throw e;
|
|
463
706
|
}
|
|
464
707
|
}
|
|
465
708
|
async sendWelcomeMessage() {
|
|
466
709
|
try {
|
|
467
|
-
const owner = this.config.owner;
|
|
468
|
-
if (!owner) {
|
|
469
|
-
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message`);
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
// Check agent.md initialized field
|
|
473
710
|
const aid = this.config.aid;
|
|
474
711
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
712
|
+
// Read initialized + owners from per-agent config.json
|
|
713
|
+
// (config.json 是 owner 的真相来源——auto-bind 后会更新这里,但 this.config 是
|
|
714
|
+
// channel 启动时的快照,不会自动同步)
|
|
715
|
+
const agentConfig = loadAgent(aidName);
|
|
716
|
+
if (!agentConfig) {
|
|
717
|
+
logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
|
|
478
718
|
return;
|
|
479
719
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (!match) {
|
|
483
|
-
logger.warn(`${this.logPrefix()} agent.md frontmatter not found`);
|
|
720
|
+
if (agentConfig.initialized === true) {
|
|
721
|
+
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
484
722
|
return;
|
|
485
723
|
}
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
724
|
+
const owner = agentConfig.owners?.[0] ?? this.config.owner;
|
|
725
|
+
if (!owner) {
|
|
726
|
+
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
|
|
490
727
|
return;
|
|
491
728
|
}
|
|
729
|
+
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
730
|
+
const existingAgentMd = fs.existsSync(agentMdPath) ? fs.readFileSync(agentMdPath, 'utf-8') : '';
|
|
731
|
+
const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
|
|
732
|
+
const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
|
|
492
733
|
// Fetch owner's agent.md to derive name and validate type
|
|
493
734
|
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
494
735
|
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
@@ -497,37 +738,34 @@ export class AUNChannel {
|
|
|
497
738
|
// Name: prefer existing agent.md name if user has customized it,
|
|
498
739
|
// otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
|
|
499
740
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
else {
|
|
505
|
-
ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
|
|
506
|
-
}
|
|
507
|
-
// Check if init wrote a meaningful name (vs just the aid first label default)
|
|
508
|
-
const currentNameMatch = frontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
509
|
-
const currentName = currentNameMatch?.[1]?.trim();
|
|
741
|
+
const ownerDisplayName = (ownerInfo.name || ownerAidClean.split('.')[0]).slice(0, 12);
|
|
742
|
+
const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
743
|
+
const currentName = currentNameMatch?.[1]?.trim().replace(/"$/, '');
|
|
510
744
|
const aidLabel = aidName.split('.')[0];
|
|
511
745
|
let agentDisplayName;
|
|
512
746
|
if (currentName && currentName !== aidLabel) {
|
|
513
|
-
// User or previous init set a custom name — keep it
|
|
514
747
|
agentDisplayName = currentName;
|
|
515
748
|
}
|
|
516
749
|
else {
|
|
517
750
|
agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
|
|
518
751
|
}
|
|
519
|
-
//
|
|
752
|
+
// Preserve user-provided description (from `agent new --description`), fallback to default
|
|
753
|
+
const currentDescMatch = existingFrontmatter.match(/^description:\s*"?([^"\n]*)/m);
|
|
754
|
+
const currentDesc = currentDescMatch?.[1]?.trim().replace(/"$/, '');
|
|
755
|
+
const agentDescription = currentDesc
|
|
756
|
+
? currentDesc
|
|
757
|
+
: 'EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道';
|
|
758
|
+
// Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
|
|
520
759
|
const newAgentMd = `---
|
|
521
760
|
aid: "${aid}"
|
|
522
761
|
name: "${agentDisplayName}"
|
|
523
762
|
type: "codeagent"
|
|
524
763
|
version: "1.0.0"
|
|
525
|
-
description: "
|
|
764
|
+
description: "${agentDescription}"
|
|
526
765
|
tags:
|
|
527
766
|
- evolclaw
|
|
528
767
|
- ai-agent
|
|
529
768
|
- gateway
|
|
530
|
-
initialized: true
|
|
531
769
|
---
|
|
532
770
|
|
|
533
771
|
# ${agentDisplayName}
|
|
@@ -535,8 +773,9 @@ initialized: true
|
|
|
535
773
|
EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
536
774
|
`;
|
|
537
775
|
// Write locally
|
|
776
|
+
fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
|
|
538
777
|
fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
|
|
539
|
-
logger.info(`${this.logPrefix()} Updated agent.md
|
|
778
|
+
logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
|
|
540
779
|
// Publish to AUN network via auth.uploadAgentMd
|
|
541
780
|
try {
|
|
542
781
|
await this.client.auth.uploadAgentMd(newAgentMd);
|
|
@@ -581,6 +820,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
581
820
|
persist_required: true,
|
|
582
821
|
});
|
|
583
822
|
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
823
|
+
// Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
|
|
824
|
+
try {
|
|
825
|
+
const fresh = loadAgent(aidName);
|
|
826
|
+
if (fresh) {
|
|
827
|
+
fresh.initialized = true;
|
|
828
|
+
saveAgent(fresh);
|
|
829
|
+
logger.info(`${this.logPrefix()} Marked ${aidName} as initialized in config.json`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
catch (e) {
|
|
833
|
+
logger.warn(`${this.logPrefix()} Failed to update initialized flag in config.json: ${e}`);
|
|
834
|
+
}
|
|
584
835
|
}
|
|
585
836
|
catch (e) {
|
|
586
837
|
logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
|
|
@@ -590,11 +841,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
590
841
|
async downloadAttachment(att, channelId) {
|
|
591
842
|
const ownerAid = att.owner_aid || this._aid || '';
|
|
592
843
|
const objectKey = att.object_key;
|
|
593
|
-
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
594
844
|
if (!objectKey) {
|
|
595
845
|
logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
|
|
596
846
|
return null;
|
|
597
847
|
}
|
|
848
|
+
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
598
849
|
let downloadUrl;
|
|
599
850
|
try {
|
|
600
851
|
const ticket = await this.callAndTrace('storage.create_download_ticket', {
|
|
@@ -651,7 +902,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
651
902
|
const msg = data;
|
|
652
903
|
const fromAid = msg.from ?? '';
|
|
653
904
|
const payload = msg.payload ?? '';
|
|
654
|
-
const text = this.extractTextPayload(payload);
|
|
905
|
+
const text = this.extractTextPayload(payload, fromAid);
|
|
655
906
|
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
656
907
|
const messageId = msg.message_id ?? '';
|
|
657
908
|
const seq = msg.seq;
|
|
@@ -672,10 +923,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
672
923
|
if (this._aid && text.includes(`@${this._aid}`)) {
|
|
673
924
|
mentions.push(this._aid);
|
|
674
925
|
}
|
|
675
|
-
// Process attachments
|
|
676
|
-
const rawAttachments =
|
|
677
|
-
? payload.attachments
|
|
678
|
-
: [];
|
|
926
|
+
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
927
|
+
const rawAttachments = this.collectAllAttachments(payload);
|
|
679
928
|
let finalText = text;
|
|
680
929
|
if (rawAttachments.length > 0 && this.client) {
|
|
681
930
|
const fileParts = [];
|
|
@@ -695,17 +944,40 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
695
944
|
finalText = parts.join('\n\n');
|
|
696
945
|
}
|
|
697
946
|
}
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
: fromAid;
|
|
947
|
+
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
948
|
+
// device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
|
|
949
|
+
const chatId = fromAid;
|
|
702
950
|
const peerInfo = await this.fetchPeerInfo(fromAid);
|
|
703
951
|
const shortAid = this.getShortAid(fromAid);
|
|
704
952
|
const displayName = peerInfo.name || shortAid;
|
|
705
953
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
706
954
|
const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
707
955
|
logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerInfo.type || 'unknown'} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
956
|
+
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
957
|
+
if (p2pPayloadType === 'action_card_reply')
|
|
958
|
+
return;
|
|
959
|
+
// menu.query:自定义消息快速路径,需要原始 payload JSON 传递给 bridge
|
|
960
|
+
if (p2pPayloadType === 'menu.query') {
|
|
961
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
962
|
+
this.dispatchMessage({
|
|
963
|
+
channelId: chatId, userId: fromAid,
|
|
964
|
+
text: JSON.stringify(payload),
|
|
965
|
+
chatType: 'private', messageId, seq,
|
|
966
|
+
peerName: displayName || undefined,
|
|
967
|
+
peerType: peerInfo.type || undefined,
|
|
968
|
+
});
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
// payload 类型白名单:信号类消息(status / event / thought 等)不进 Agent
|
|
972
|
+
if (p2pPayloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(p2pPayloadType)) {
|
|
973
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
974
|
+
logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
708
977
|
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
|
|
978
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
979
|
+
const isSystemP2P = p2pPayloadType === 'event';
|
|
980
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P);
|
|
709
981
|
const replyContext = { metadata: { encrypted: msgEncrypted } };
|
|
710
982
|
if (taskId)
|
|
711
983
|
replyContext.threadId = taskId;
|
|
@@ -730,7 +1002,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
730
1002
|
const groupId = msg.group_id ?? '';
|
|
731
1003
|
const senderAid = msg.sender_aid ?? '';
|
|
732
1004
|
const payload = msg.payload ?? '';
|
|
733
|
-
const text = this.extractTextPayload(payload);
|
|
1005
|
+
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
734
1006
|
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
735
1007
|
const messageId = msg.message_id ?? '';
|
|
736
1008
|
const seq = msg.seq;
|
|
@@ -749,52 +1021,114 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
749
1021
|
logger.debug(`${this.logPrefix()} Group dropped: own message (group=${groupId} mid=${messageId})`);
|
|
750
1022
|
return;
|
|
751
1023
|
}
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1024
|
+
// 短 echo 快速通道:连通性测试要尽量低延迟,命中后绕过所有 await(后续 mention 过滤)
|
|
1025
|
+
{
|
|
1026
|
+
const firstLineFast = text.split('\n')[0] || '';
|
|
1027
|
+
const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
1028
|
+
if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
|
|
1029
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1030
|
+
const msgEncryptedFast = !!(msg.e2ee);
|
|
1031
|
+
const peerInfo = this.peerInfoCached(senderAid);
|
|
1032
|
+
const shortAid = this.getShortAid(senderAid);
|
|
1033
|
+
const displayName = peerInfo?.name || shortAid;
|
|
1034
|
+
const createdAt = data.created_at;
|
|
1035
|
+
if (!peerInfo)
|
|
1036
|
+
this.prefetchPeerInfo(senderAid);
|
|
1037
|
+
this.handleEcho({
|
|
1038
|
+
channelId: groupId,
|
|
1039
|
+
userId: senderAid,
|
|
1040
|
+
text,
|
|
1041
|
+
chatType: 'group',
|
|
1042
|
+
messageId,
|
|
1043
|
+
peerName: displayName,
|
|
1044
|
+
peerType: peerInfo?.type || 'unknown',
|
|
1045
|
+
seq,
|
|
1046
|
+
replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId),
|
|
1047
|
+
createdAt,
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1053
|
+
{
|
|
1054
|
+
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1055
|
+
if (payloadType === 'action_card_reply')
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// ── payload 类型白名单(所有模式生效) ──
|
|
1059
|
+
// 信号类消息(status / event / thought / task.update 等)不进 Agent
|
|
1060
|
+
{
|
|
1061
|
+
const payloadObj = (payload && typeof payload === 'object') ? payload : null;
|
|
1062
|
+
const payloadType = payloadObj?.type ?? '';
|
|
1063
|
+
// menu.query:自定义消息快速路径
|
|
1064
|
+
if (payloadType === 'menu.query') {
|
|
1065
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1066
|
+
this.dispatchMessage({
|
|
1067
|
+
channelId: groupId, userId: senderAid,
|
|
1068
|
+
text: JSON.stringify(payload),
|
|
1069
|
+
chatType: 'group', messageId, seq, groupId,
|
|
1070
|
+
});
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (payloadType && !AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
|
|
1074
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1075
|
+
logger.info(`${this.logPrefix()} Group dropped (type deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
1076
|
+
return;
|
|
774
1077
|
}
|
|
775
1078
|
}
|
|
776
1079
|
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
777
1080
|
const msgEncrypted = !!(msg.e2ee);
|
|
778
1081
|
if (!msgEncrypted)
|
|
779
1082
|
this.plaintextRecv++;
|
|
780
|
-
// dispatch_mode
|
|
781
|
-
const
|
|
1083
|
+
// dispatch_mode: 本地设置优先,fallback 到服务器参数
|
|
1084
|
+
const serverDispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
|
|
1085
|
+
const localDispatchMode = this.dispatchModeResolver
|
|
1086
|
+
? await this.dispatchModeResolver(groupId).catch(() => undefined)
|
|
1087
|
+
: undefined;
|
|
1088
|
+
const dispatchMode = localDispatchMode || serverDispatchMode;
|
|
782
1089
|
const mentionedSelf = this._aid
|
|
783
1090
|
? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
|
|
784
1091
|
: false;
|
|
785
1092
|
// @all 仅认结构化 mentions(payload.mentions),不扫描正文 — 避免引述性 "@all" 误判
|
|
786
1093
|
const mentionedAll = payloadMentions.includes('all');
|
|
787
|
-
//
|
|
788
|
-
|
|
1094
|
+
// Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
|
|
1095
|
+
// 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1096
|
+
const firstLineGroup = text.split('\n')[0] || '';
|
|
1097
|
+
const hasEvolClawTraceGroup = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
1098
|
+
if (/echo/i.test(firstLineGroup) && !hasEvolClawTraceGroup) {
|
|
1099
|
+
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
1100
|
+
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
1101
|
+
const echoTs = () => {
|
|
1102
|
+
const d = new Date();
|
|
1103
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
1104
|
+
};
|
|
1105
|
+
let echoText = text;
|
|
1106
|
+
echoText += `\n${echoTs()} [EvolClaw.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1107
|
+
this.pendingEchoMessages.set(messageId, {
|
|
1108
|
+
text: echoText,
|
|
1109
|
+
channelId: groupId,
|
|
1110
|
+
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId),
|
|
1111
|
+
receiveTs: Date.now(),
|
|
1112
|
+
});
|
|
1113
|
+
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
1114
|
+
}
|
|
1115
|
+
else if (/echo/i.test(firstLineGroup) && hasEvolClawTraceGroup) {
|
|
1116
|
+
// 回声炸弹:已被任何 EvolClaw 节点 trace 过的 echo,直接丢弃
|
|
789
1117
|
this.acknowledgeImmediately(messageId, seq);
|
|
790
|
-
logger.info(`${this.logPrefix()} Group dropped:
|
|
1118
|
+
logger.info(`${this.logPrefix()} Group dropped: echo bomb (already-traced group=${groupId} sender=${senderAid} mid=${messageId})`);
|
|
791
1119
|
return;
|
|
792
1120
|
}
|
|
1121
|
+
else {
|
|
1122
|
+
// 非 echo 消息:正常 mention 过滤
|
|
1123
|
+
if (dispatchMode === 'mention' && !mentionedSelf && !mentionedAll) {
|
|
1124
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
1125
|
+
logger.info(`${this.logPrefix()} Group dropped: unmentioned in mention-mode (group=${groupId} sender=${senderAid} mid=${messageId} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
793
1129
|
const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
|
|
794
|
-
// Detect attachments before the empty-text guard
|
|
795
|
-
const rawAttachments =
|
|
796
|
-
? payload.attachments
|
|
797
|
-
: [];
|
|
1130
|
+
// Detect attachments before the empty-text guard (顶层 + 嵌套)
|
|
1131
|
+
const rawAttachments = this.collectAllAttachments(payload);
|
|
798
1132
|
const hasAttachments = rawAttachments.length > 0;
|
|
799
1133
|
// Allow through if there's text OR attachments; both-empty messages are silently dropped
|
|
800
1134
|
if (!strippedText && !hasAttachments) {
|
|
@@ -840,9 +1174,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
840
1174
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
841
1175
|
: `${dispatchMode}.no-mention`;
|
|
842
1176
|
logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerInfo.type || 'unknown'} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
1177
|
+
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1178
|
+
if (payloadType === 'action_card_reply')
|
|
1179
|
+
return;
|
|
843
1180
|
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} text=${finalText.slice(0, 60)}`);
|
|
1181
|
+
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 });
|
|
1182
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event');
|
|
844
1183
|
this.dispatchMessage({
|
|
845
1184
|
channelId: groupId,
|
|
1185
|
+
groupId,
|
|
846
1186
|
userId: senderAid,
|
|
847
1187
|
peerName: displayName || undefined,
|
|
848
1188
|
peerType: peerInfo.type || 'unknown',
|
|
@@ -852,7 +1192,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
852
1192
|
seq,
|
|
853
1193
|
taskId,
|
|
854
1194
|
mentions,
|
|
855
|
-
replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted),
|
|
1195
|
+
replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId),
|
|
856
1196
|
});
|
|
857
1197
|
}
|
|
858
1198
|
dispatchMessage(event) {
|
|
@@ -867,6 +1207,35 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
867
1207
|
this.messageSeqMap.set(event.messageId, event.seq);
|
|
868
1208
|
}
|
|
869
1209
|
}
|
|
1210
|
+
// Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
|
|
1211
|
+
// 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1212
|
+
const firstLine = event.text.split('\n')[0] || '';
|
|
1213
|
+
const hasEvolClawTracePrivate = /\[EvolClaw\.(receive|reply|agent)\]/.test(event.text);
|
|
1214
|
+
if (/echo/i.test(firstLine) && firstLine.trim().length <= 10 && !hasEvolClawTracePrivate) {
|
|
1215
|
+
this.handleEcho(event);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
// 回声炸弹:已被任何 EvolClaw 节点 trace 过的 echo,直接丢弃(防止多 agent 间无限回声)
|
|
1219
|
+
if (/echo/i.test(firstLine) && hasEvolClawTracePrivate) {
|
|
1220
|
+
logger.info(`${this.logPrefix()} Dropped: echo bomb (already-traced mid=${event.messageId} chat=${event.chatType})`);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
// 长 echo(>10 字符):存 pending,继续交给 agent 处理
|
|
1224
|
+
if (/echo/i.test(firstLine) && firstLine.trim().length > 10 && !hasEvolClawTracePrivate) {
|
|
1225
|
+
const echoTs = () => {
|
|
1226
|
+
const d = new Date();
|
|
1227
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
1228
|
+
};
|
|
1229
|
+
let echoText = event.text;
|
|
1230
|
+
echoText += `\n${echoTs()} [EvolClaw.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1231
|
+
this.pendingEchoMessages.set(event.messageId, {
|
|
1232
|
+
text: echoText,
|
|
1233
|
+
channelId: event.channelId,
|
|
1234
|
+
context: event.replyContext ? { metadata: event.replyContext.metadata } : undefined,
|
|
1235
|
+
receiveTs: Date.now(),
|
|
1236
|
+
});
|
|
1237
|
+
logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
|
|
1238
|
+
}
|
|
870
1239
|
if (!this.messageHandler)
|
|
871
1240
|
return;
|
|
872
1241
|
const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
|
|
@@ -878,7 +1247,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
878
1247
|
}
|
|
879
1248
|
this.messageHandler({
|
|
880
1249
|
channelId: event.channelId || '',
|
|
1250
|
+
channelType: 'aun',
|
|
881
1251
|
content: event.text || '',
|
|
1252
|
+
selfId: this._aid,
|
|
1253
|
+
groupId: event.groupId,
|
|
882
1254
|
chatType: event.chatType,
|
|
883
1255
|
peerId: event.userId || event.channelId || '',
|
|
884
1256
|
peerName: event.peerName,
|
|
@@ -891,6 +1263,71 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
891
1263
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
892
1264
|
});
|
|
893
1265
|
}
|
|
1266
|
+
handleEcho(event) {
|
|
1267
|
+
const ts = () => {
|
|
1268
|
+
const d = new Date();
|
|
1269
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
1270
|
+
};
|
|
1271
|
+
// 在收到的消息文本末尾追加 trace 行(只追加,不修改原文)
|
|
1272
|
+
let echoText = event.text;
|
|
1273
|
+
echoText += `\n${ts()} [EvolClaw.receive] from=${event.userId}${event.peerName ? `(${event.peerName})` : ''} mid=${event.messageId} chat=${event.chatType} self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1274
|
+
echoText += `\n${ts()} [EvolClaw.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1275
|
+
if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
|
|
1276
|
+
echoText += `\n[TRUNCATED]`;
|
|
1277
|
+
}
|
|
1278
|
+
const replyTarget = event.channelId;
|
|
1279
|
+
// 不传 peerId,避免 sendMessage 在头部追加 @peer
|
|
1280
|
+
const context = { metadata: event.replyContext?.metadata };
|
|
1281
|
+
const sendStart = Date.now();
|
|
1282
|
+
this.isEchoSending = true;
|
|
1283
|
+
this.sendMessage(replyTarget, echoText, context).then(() => {
|
|
1284
|
+
this.isEchoSending = false;
|
|
1285
|
+
const elapsed = Date.now() - sendStart;
|
|
1286
|
+
logger.info(`${this.logPrefix()} Echo reply sent to ${replyTarget} (${elapsed}ms)`);
|
|
1287
|
+
}).catch(e => {
|
|
1288
|
+
this.isEchoSending = false;
|
|
1289
|
+
logger.error(`${this.logPrefix()} Echo reply failed: ${e}`);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* 处理 SDK 'gateway.disconnect' 事件 — 服务端主动断开(含同槽位互踢)。
|
|
1294
|
+
*
|
|
1295
|
+
* 当另一个进程用同 AID + 同 slot 抢连接时,老连接会被踢,detail 字段含:
|
|
1296
|
+
* - self_extra_info: 本进程当时 connect 上报的内容(可证明"是我自己被踢了")
|
|
1297
|
+
* - new_extra_info: 挤掉本连接的新进程上报的内容(指出"谁踢的我")
|
|
1298
|
+
*
|
|
1299
|
+
* 把这些信息打到 logger.warn,便于通过 evolclaw watch / 日志查看。
|
|
1300
|
+
*/
|
|
1301
|
+
handleGatewayDisconnect(data) {
|
|
1302
|
+
if (!data || typeof data !== 'object')
|
|
1303
|
+
return;
|
|
1304
|
+
const d = data;
|
|
1305
|
+
const code = d.code;
|
|
1306
|
+
const reason = d.reason ?? '';
|
|
1307
|
+
const detail = (d.detail && typeof d.detail === 'object' && !Array.isArray(d.detail)) ? d.detail : {};
|
|
1308
|
+
const selfExtra = detail.self_extra_info;
|
|
1309
|
+
const newExtra = detail.new_extra_info;
|
|
1310
|
+
const fmtExtra = (e) => {
|
|
1311
|
+
if (!e || typeof e !== 'object')
|
|
1312
|
+
return '<none>';
|
|
1313
|
+
const obj = e;
|
|
1314
|
+
const parts = [];
|
|
1315
|
+
const keys = ['app', 'version', 'pid', 'started_at_iso', 'hostname', 'launched_by', 'evolclaw_home', 'agent_name', 'channel_name'];
|
|
1316
|
+
for (const k of keys) {
|
|
1317
|
+
if (obj[k] !== undefined && obj[k] !== '')
|
|
1318
|
+
parts.push(`${k}=${String(obj[k])}`);
|
|
1319
|
+
}
|
|
1320
|
+
return parts.length ? parts.join(' ') : JSON.stringify(obj);
|
|
1321
|
+
};
|
|
1322
|
+
if (selfExtra || newExtra) {
|
|
1323
|
+
logger.warn(`${this.logPrefix()} 🥊 Kicked by another connection (code=${code} reason=${reason})`);
|
|
1324
|
+
logger.warn(`${this.logPrefix()} ↳ me : ${fmtExtra(selfExtra)}`);
|
|
1325
|
+
logger.warn(`${this.logPrefix()} ↳ them: ${fmtExtra(newExtra)}`);
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
logger.warn(`${this.logPrefix()} Server-initiated disconnect: code=${code} reason=${reason} detail=${JSON.stringify(detail)}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
894
1331
|
handleConnectionState(data) {
|
|
895
1332
|
if (!data || typeof data !== 'object')
|
|
896
1333
|
return;
|
|
@@ -902,100 +1339,368 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
902
1339
|
logger.info(`[AUN][DIAG-STALE] connection.state event: state=${state} attempt=${data.attempt ?? '-'} | connected_before=${connectedBefore} sdk_state=${sdkState}`);
|
|
903
1340
|
if (state === 'connected') {
|
|
904
1341
|
this.connected = true;
|
|
905
|
-
this.
|
|
1342
|
+
this.connectedAt = Date.now();
|
|
906
1343
|
this.lastReconnectLogTime = 0;
|
|
907
1344
|
this.lastReconnectLogAttempt = 0;
|
|
1345
|
+
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.client?._gatewayUrl });
|
|
908
1346
|
this.trace('IN', 'connection.state', data);
|
|
909
1347
|
logger.info(`${this.logPrefix()} Connected`);
|
|
1348
|
+
// 不在这里清 flapCount —— 短命连接一上来就会触发本分支,
|
|
1349
|
+
// 必须等 disconnected 时根据 lifetime 决定是否清零
|
|
1350
|
+
this.drainOutbox();
|
|
910
1351
|
}
|
|
911
|
-
else if (state === 'disconnected') {
|
|
912
|
-
this.connected
|
|
913
|
-
this.trace('IN', 'connection.state', data);
|
|
914
|
-
logger.warn(`${this.logPrefix()} Disconnected: ${data.error ?? 'unknown'}`);
|
|
915
|
-
}
|
|
916
|
-
else if (state === 'reconnecting') {
|
|
1352
|
+
else if (state === 'disconnected' || state === 'reconnecting') {
|
|
1353
|
+
const wasConnected = this.connected;
|
|
917
1354
|
this.connected = false;
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1355
|
+
this.aidState.reconnectCount++;
|
|
1356
|
+
this.aidState.lastAttemptAt = Date.now();
|
|
1357
|
+
this.setAidStatus('reconnecting');
|
|
1358
|
+
// Flap 检测:仅在从 connected 状态过渡时统计
|
|
1359
|
+
if (wasConnected && this.connectedAt > 0) {
|
|
1360
|
+
const lifetime = Date.now() - this.connectedAt;
|
|
1361
|
+
this.connectedAt = 0;
|
|
1362
|
+
if (lifetime < AUNChannel.FLAP_WINDOW_MS) {
|
|
1363
|
+
this.flapCount++;
|
|
1364
|
+
logger.warn(`${this.logPrefix()} Flap #${this.flapCount}/${AUNChannel.FLAP_THRESHOLD}: connection lived ${lifetime}ms (< ${AUNChannel.FLAP_WINDOW_MS}ms)`);
|
|
1365
|
+
if (this.flapCount >= AUNChannel.FLAP_THRESHOLD && !this.intentionalDisconnect) {
|
|
1366
|
+
logger.error(`${this.logPrefix()} Persistent kick detected (${this.flapCount} flaps), taking over from SDK with ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s backoff`);
|
|
1367
|
+
this.flapCount = 0;
|
|
1368
|
+
this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'flap');
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
// 连接稳定过 ≥ FLAP_WINDOW_MS,重置 flap 计数
|
|
1374
|
+
if (this.flapCount > 0) {
|
|
1375
|
+
logger.info(`${this.logPrefix()} Stable connection (lived ${lifetime}ms), resetting flap counter`);
|
|
1376
|
+
}
|
|
1377
|
+
this.flapCount = 0;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (state === 'disconnected') {
|
|
930
1381
|
this.trace('IN', 'connection.state', data);
|
|
1382
|
+
logger.warn(`${this.logPrefix()} Disconnected: ${data.error ?? 'unknown'}`);
|
|
931
1383
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1384
|
+
else {
|
|
1385
|
+
// reconnecting:节流日志(SDK 自己已经在跑指数退避,不刷屏)
|
|
1386
|
+
const attempt = data.attempt ?? 0;
|
|
1387
|
+
const now = Date.now();
|
|
1388
|
+
const isFirst = attempt <= 1;
|
|
1389
|
+
const isStep = attempt - this.lastReconnectLogAttempt >= AUNChannel.RECONNECT_LOG_STEP;
|
|
1390
|
+
const isInterval = now - this.lastReconnectLogTime >= AUNChannel.RECONNECT_LOG_INTERVAL;
|
|
1391
|
+
if (isFirst || isStep || isInterval) {
|
|
1392
|
+
const suppressed = attempt - this.lastReconnectLogAttempt - 1;
|
|
1393
|
+
const suffix = suppressed > 0 ? `, ${suppressed} suppressed since last log` : '';
|
|
1394
|
+
logger.info(`${this.logPrefix()} SDK reconnecting (attempt ${attempt}${suffix})`);
|
|
1395
|
+
this.lastReconnectLogTime = now;
|
|
1396
|
+
this.lastReconnectLogAttempt = attempt;
|
|
1397
|
+
this.trace('IN', 'connection.state', data);
|
|
940
1398
|
}
|
|
941
|
-
this.scheduleReconnect();
|
|
942
1399
|
}
|
|
943
1400
|
}
|
|
944
1401
|
else if (state === 'terminal_failed') {
|
|
945
1402
|
this.connected = false;
|
|
1403
|
+
this.connectedAt = 0;
|
|
946
1404
|
this.trace('IN', 'connection.state', data);
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1405
|
+
const d = data;
|
|
1406
|
+
const reason = d.reason ?? '';
|
|
1407
|
+
const error = d.error ?? 'unknown';
|
|
1408
|
+
const code = d.code ?? d.detail?.code ?? 0;
|
|
1409
|
+
const detail = (d.detail && typeof d.detail === 'object') ? d.detail : {};
|
|
1410
|
+
if (this.intentionalDisconnect)
|
|
1411
|
+
return;
|
|
1412
|
+
if (this.isKickReason(reason) || code >= 4001) {
|
|
1413
|
+
// @ts-ignore — methods defined below in same class
|
|
1414
|
+
const kickDetail = this.buildKickDetail(code, reason, detail);
|
|
1415
|
+
// @ts-ignore — methods defined below in same class
|
|
1416
|
+
const action = this.classifyKickAction(code);
|
|
1417
|
+
appendAidEvent({
|
|
1418
|
+
ts: Date.now(), iso: new Date().toISOString(),
|
|
1419
|
+
event: 'kicked', aid: this.config.aid,
|
|
1420
|
+
code, reason, action,
|
|
1421
|
+
evictedBy: kickDetail.evictedBy,
|
|
1422
|
+
quotaKind: kickDetail.quotaKind,
|
|
1423
|
+
});
|
|
1424
|
+
appendAidLifecycle({
|
|
1425
|
+
ts: Date.now(), iso: new Date().toISOString(),
|
|
1426
|
+
event: 'kicked', aid: this.config.aid,
|
|
1427
|
+
code, reason, action,
|
|
1428
|
+
evictedBy: kickDetail.evictedBy,
|
|
1429
|
+
newExtra: kickDetail.newExtra,
|
|
1430
|
+
quotaKind: kickDetail.quotaKind,
|
|
1431
|
+
});
|
|
1432
|
+
if (action === 'no_retry') {
|
|
1433
|
+
logger.error(`${this.logPrefix()} Kicked (code=${code}): ${reason} — will NOT retry`);
|
|
1434
|
+
this.setAidStatus('kicked_no_retry', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
|
|
1435
|
+
}
|
|
1436
|
+
else if (action === 'retry_once') {
|
|
1437
|
+
logger.warn(`${this.logPrefix()} Kicked (code=${code}): ${reason} — retrying once after ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
|
|
1438
|
+
this.setAidStatus('kicked', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
|
|
1439
|
+
this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'kicked');
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
logger.warn(`${this.logPrefix()} Kicked (code=${code}): ${reason} — retrying after ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s`);
|
|
1443
|
+
this.setAidStatus('kicked', { lastError: `kicked(${code}): ${reason}`.slice(0, 80), kickDetail });
|
|
1444
|
+
this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'kicked');
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
logger.error(`${this.logPrefix()} Terminal failure: ${error}${reason ? ` (${reason})` : ''}, retrying in ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
|
|
1449
|
+
this.setAidStatus('failed', { lastError: `${error}`.slice(0, 80) });
|
|
1450
|
+
this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'terminal');
|
|
951
1451
|
}
|
|
952
1452
|
}
|
|
953
1453
|
}
|
|
1454
|
+
/** 判断 terminal_failed 的 reason 是否属于"被踢"类 */
|
|
1455
|
+
isKickReason(reason) {
|
|
1456
|
+
if (!reason)
|
|
1457
|
+
return false;
|
|
1458
|
+
const r = reason.toLowerCase();
|
|
1459
|
+
if (r.includes('kicked') || r.includes('kick'))
|
|
1460
|
+
return true;
|
|
1461
|
+
if (/close code 40\d{2}/.test(r))
|
|
1462
|
+
return true;
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* 根据 close code 决定重试策略:
|
|
1467
|
+
* - 'no_retry': 不重试(被挤掉、AID 无效、ACL 拒绝、长连接已存在、配额超限)
|
|
1468
|
+
* - 'retry_once': 重试一次(auth 失败可能 token 刚过期、nonce 无效)
|
|
1469
|
+
* - 'retry_delay': 延迟重试(短连接容量超限、空闲超时)
|
|
1470
|
+
*/
|
|
1471
|
+
classifyKickAction(code) {
|
|
1472
|
+
switch (code) {
|
|
1473
|
+
case 4003: // AID 无效
|
|
1474
|
+
case 4009: // 服务端主动踢
|
|
1475
|
+
case 4011: // ACL 拒绝
|
|
1476
|
+
case 4012: // 长连接已存在(自己另一个实例在线)
|
|
1477
|
+
case 4015: // 被新连接挤掉
|
|
1478
|
+
return 'no_retry';
|
|
1479
|
+
case 4001: // auth 失败(token 可能刚过期)
|
|
1480
|
+
case 4010: // nonce 无效
|
|
1481
|
+
return 'retry_once';
|
|
1482
|
+
case 4008: // auth 超时
|
|
1483
|
+
case 4013: // 短连接容量超限
|
|
1484
|
+
case 4014: // 短连接空闲超时
|
|
1485
|
+
return 'retry_delay';
|
|
1486
|
+
default:
|
|
1487
|
+
return 'retry_delay';
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
buildKickDetail(code, reason, detail) {
|
|
1491
|
+
const evictedByRaw = detail.evicted_by || detail.new_extra_info;
|
|
1492
|
+
let evictedBy;
|
|
1493
|
+
if (evictedByRaw && typeof evictedByRaw === 'object') {
|
|
1494
|
+
evictedBy = {
|
|
1495
|
+
aid: evictedByRaw.aid,
|
|
1496
|
+
deviceId: evictedByRaw.device_id,
|
|
1497
|
+
slotId: evictedByRaw.slot_id,
|
|
1498
|
+
app: evictedByRaw.app,
|
|
1499
|
+
hostname: evictedByRaw.hostname,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
return {
|
|
1503
|
+
code,
|
|
1504
|
+
reason,
|
|
1505
|
+
ts: Date.now(),
|
|
1506
|
+
evictedBy,
|
|
1507
|
+
quotaKind: detail.quota_kind,
|
|
1508
|
+
limit: detail.limit,
|
|
1509
|
+
selfExtra: detail.self_extra_info,
|
|
1510
|
+
newExtra: detail.new_extra_info,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* TS 层接管重连:force close 当前 SDK client,安排 delayMs 后重新 initClient。
|
|
1515
|
+
* 用于 flap / kicked / terminal_failed 三类场景,统一退避路径。
|
|
1516
|
+
*/
|
|
1517
|
+
takeoverReconnect(delayMs, reason) {
|
|
1518
|
+
if (this.intentionalDisconnect)
|
|
1519
|
+
return;
|
|
1520
|
+
// 递增 generation,使任何正在进行的旧 initClient 回调失效
|
|
1521
|
+
const gen = ++this.reconnectGeneration;
|
|
1522
|
+
// 清掉已有 timer,避免叠加
|
|
1523
|
+
if (this.reconnectTimer) {
|
|
1524
|
+
clearTimeout(this.reconnectTimer);
|
|
1525
|
+
this.reconnectTimer = null;
|
|
1526
|
+
}
|
|
1527
|
+
// Force close SDK client,中断它内部的重连循环
|
|
1528
|
+
if (this.client) {
|
|
1529
|
+
this.trace('OUT', 'client.close', { reason: `takeover_${reason}` });
|
|
1530
|
+
this.client.close().catch(() => { });
|
|
1531
|
+
this.client = null;
|
|
1532
|
+
}
|
|
1533
|
+
this.connected = false;
|
|
1534
|
+
const delaySec = Math.round(delayMs / 1000);
|
|
1535
|
+
logger.info(`${this.logPrefix()} Scheduling TS-layer reconnect (${reason}) in ${delaySec}s`);
|
|
1536
|
+
this.trace('OUT', 'reconnect.scheduled', { reason, delayMs, generation: gen });
|
|
1537
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
1538
|
+
this.reconnectTimer = null;
|
|
1539
|
+
// 如果在等待期间又触发了新的 takeoverReconnect,本次作废
|
|
1540
|
+
if (gen !== this.reconnectGeneration) {
|
|
1541
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) cancelled: generation stale (${gen} vs ${this.reconnectGeneration})`);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
try {
|
|
1545
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) starting...`);
|
|
1546
|
+
this.trace('OUT', 'reconnect.start', { reason, generation: gen });
|
|
1547
|
+
await this.initClient();
|
|
1548
|
+
// initClient 完成后再次校验 generation,防止 initClient 期间被新的 takeover 取代
|
|
1549
|
+
if (gen !== this.reconnectGeneration) {
|
|
1550
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) succeeded but generation stale, closing stale client`);
|
|
1551
|
+
if (this.client) {
|
|
1552
|
+
this.client.close().catch(() => { });
|
|
1553
|
+
this.client = null;
|
|
1554
|
+
}
|
|
1555
|
+
this.connected = false;
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
this.trace('OUT', 'reconnect.ok', { reason, generation: gen });
|
|
1559
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) succeeded`);
|
|
1560
|
+
}
|
|
1561
|
+
catch (err) {
|
|
1562
|
+
this.trace('OUT', 'reconnect.error', { reason, error: String(err), generation: gen });
|
|
1563
|
+
logger.error(`${this.logPrefix()} TS-layer reconnect (${reason}) failed: ${err}`);
|
|
1564
|
+
// initClient 内部已经在失败路径触发 scheduleReconnect 了,这里不重复
|
|
1565
|
+
}
|
|
1566
|
+
}, delayMs);
|
|
1567
|
+
}
|
|
954
1568
|
// ── Public API (same interface as before) ───────────────────
|
|
1569
|
+
setEventBus(bus) {
|
|
1570
|
+
// 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
|
|
1571
|
+
if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
1572
|
+
this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
|
|
1573
|
+
}
|
|
1574
|
+
if (this.eventBus && this.queuedHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
1575
|
+
this.eventBus.unsubscribe('task:queued', this.queuedHandler);
|
|
1576
|
+
}
|
|
1577
|
+
this.ownerBoundHandler = null;
|
|
1578
|
+
this.queuedHandler = null;
|
|
1579
|
+
this.eventBus = bus;
|
|
1580
|
+
if (bus && typeof bus.subscribe === 'function') {
|
|
1581
|
+
const handler = (event) => {
|
|
1582
|
+
if (event.channelName !== this.config.channelName)
|
|
1583
|
+
return;
|
|
1584
|
+
// sendWelcomeMessage 内部读 config.json 中最新的 owners[0],并幂等检查 initialized
|
|
1585
|
+
// 自身做 client 健康检查后再发
|
|
1586
|
+
if (!this.client) {
|
|
1587
|
+
logger.info(`${this.logPrefix()} owner-bound event received but client not connected; skip welcome retry`);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
this.sendWelcomeMessage().catch(e => {
|
|
1591
|
+
logger.warn(`${this.logPrefix()} owner-bound welcome retry failed: ${e}`);
|
|
1592
|
+
});
|
|
1593
|
+
};
|
|
1594
|
+
bus.subscribe('channel:owner-bound', handler);
|
|
1595
|
+
this.ownerBoundHandler = handler;
|
|
1596
|
+
const queuedHandler = (event) => {
|
|
1597
|
+
if (event.channel !== this.config.channelName)
|
|
1598
|
+
return;
|
|
1599
|
+
this.sendProcessingStatus(event.channelId, 'queued', '', '', event.replyContext);
|
|
1600
|
+
};
|
|
1601
|
+
bus.subscribe('task:queued', queuedHandler);
|
|
1602
|
+
this.queuedHandler = queuedHandler;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
955
1605
|
onProjectPathRequest(provider) {
|
|
956
1606
|
this.projectPathProvider = provider;
|
|
957
1607
|
}
|
|
958
1608
|
onMessage(handler) {
|
|
959
1609
|
this.messageHandler = handler;
|
|
960
1610
|
}
|
|
961
|
-
|
|
962
|
-
this.
|
|
1611
|
+
setDispatchModeResolver(resolver) {
|
|
1612
|
+
this.dispatchModeResolver = resolver;
|
|
963
1613
|
}
|
|
964
1614
|
onRecall(handler) {
|
|
965
1615
|
this.recallHandler = handler;
|
|
966
1616
|
}
|
|
967
1617
|
async sendMessage(channelId, text, context) {
|
|
968
|
-
// [DIAG-STALE] 进入 sendMessage 时记录 evolclaw connected 标志和 SDK _state,
|
|
969
|
-
// 用于检测两者是否一致:若 connected=true 但 sdk_state != 'connected',即为 stale 状态
|
|
970
|
-
const sdkStateOnEntry = this.client?._state ?? 'no-client';
|
|
971
|
-
if (this.connected !== (sdkStateOnEntry === 'connected')) {
|
|
972
|
-
logger.warn(`[AUN][DIAG-STALE] sendMessage entry MISMATCH: connected=${this.connected} sdk_state=${sdkStateOnEntry} channel=${channelId} text=${text.slice(0, 40)}`);
|
|
973
|
-
}
|
|
974
|
-
else {
|
|
975
|
-
logger.debug(`[AUN][DIAG-STALE] sendMessage entry: connected=${this.connected} sdk_state=${sdkStateOnEntry} channel=${channelId}`);
|
|
976
|
-
}
|
|
977
|
-
if (!this.connected || !this.client) {
|
|
978
|
-
logger.warn(`${this.logPrefix()} Cannot send: not connected`);
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
1618
|
if (!text?.trim()) {
|
|
982
1619
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
983
1620
|
return;
|
|
984
1621
|
}
|
|
1622
|
+
// 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
|
|
1623
|
+
if (!this.isEchoSending) {
|
|
1624
|
+
await this.flushPendingEcho(channelId);
|
|
1625
|
+
}
|
|
985
1626
|
let finalText = text;
|
|
986
|
-
// 多轮工具调用后的最终回复:仅在已有中间消息时添加前缀
|
|
987
1627
|
if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
|
|
988
1628
|
finalText = '最终回复\n' + text;
|
|
989
1629
|
}
|
|
990
1630
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
991
|
-
// 群聊 @ 兜底:提示词已告知 agent 要 @,但如果 agent 没写,系统自动补上
|
|
992
1631
|
if (this.isGroupId(channelId) && context?.peerId) {
|
|
993
1632
|
if (!finalText.includes(`@${context.peerId}`)) {
|
|
994
1633
|
finalText = `@${context.peerId} ` + finalText;
|
|
995
1634
|
}
|
|
996
1635
|
}
|
|
1636
|
+
// Write-ahead: persist to outbox before attempting send
|
|
1637
|
+
const entry = outbox.enqueue(this.config.aid, {
|
|
1638
|
+
channelId,
|
|
1639
|
+
type: 'text',
|
|
1640
|
+
text: finalText,
|
|
1641
|
+
context,
|
|
1642
|
+
});
|
|
1643
|
+
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
1644
|
+
if (!this.connected || !this.client) {
|
|
1645
|
+
logger.warn(`${this.logPrefix()} Not connected, message queued in outbox (id=${entry.id}). Triggering reconnect.`);
|
|
1646
|
+
if (!this.reconnectTimer && !this.client) {
|
|
1647
|
+
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
// Attempt immediate delivery
|
|
1652
|
+
const ok = await this.deliverTextEntry(entry);
|
|
1653
|
+
if (ok) {
|
|
1654
|
+
outbox.remove(this.config.aid, entry.id);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
async flushPendingEcho(channelId) {
|
|
1658
|
+
// 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
|
|
1659
|
+
for (const [key, echo] of this.pendingEchoMessages) {
|
|
1660
|
+
if (echo.channelId === channelId) {
|
|
1661
|
+
this.pendingEchoMessages.delete(key);
|
|
1662
|
+
logger.info(`${this.logPrefix()} [Echo] flushPendingEcho triggered: key=${key} channelId=${channelId} pendingCount=${this.pendingEchoMessages.size}`);
|
|
1663
|
+
const ts = () => {
|
|
1664
|
+
const d = new Date();
|
|
1665
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
1666
|
+
};
|
|
1667
|
+
const agentDuration = Date.now() - echo.receiveTs;
|
|
1668
|
+
let echoText = echo.text;
|
|
1669
|
+
echoText += `\n${ts()} [EvolClaw.agent] duration=${agentDuration}ms`;
|
|
1670
|
+
echoText += `\n${ts()} [EvolClaw.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1671
|
+
if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
|
|
1672
|
+
echoText += `\n[TRUNCATED]`;
|
|
1673
|
+
}
|
|
1674
|
+
// 直接投递 echo trace(不经过 sendMessage 避免 @peer 前缀和递归)
|
|
1675
|
+
if (this.connected && this.client) {
|
|
1676
|
+
const echoEntry = {
|
|
1677
|
+
id: `echo-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`,
|
|
1678
|
+
ts: Date.now(),
|
|
1679
|
+
aid: this.config.aid,
|
|
1680
|
+
channelId,
|
|
1681
|
+
type: 'text',
|
|
1682
|
+
text: echoText,
|
|
1683
|
+
ttl: 300_000,
|
|
1684
|
+
};
|
|
1685
|
+
const ok = await this.deliverTextEntry(echoEntry);
|
|
1686
|
+
if (!ok) {
|
|
1687
|
+
outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
|
|
1688
|
+
}
|
|
1689
|
+
logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
|
|
1693
|
+
logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
|
|
1694
|
+
}
|
|
1695
|
+
break;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
async deliverTextEntry(entry) {
|
|
1700
|
+
const channelId = entry.channelId;
|
|
1701
|
+
const finalText = entry.text;
|
|
1702
|
+
const context = entry.context;
|
|
997
1703
|
const payload = { type: 'text', text: finalText };
|
|
998
|
-
// 出站群消息:从正文提取 @aid 填入结构化 mentions(与 aun CLI 对齐,方便对端按 mentions 过滤)
|
|
999
1704
|
if (this.isGroupId(channelId)) {
|
|
1000
1705
|
const extracted = this.extractMentionAidsFromText(finalText);
|
|
1001
1706
|
if (extracted.length > 0)
|
|
@@ -1007,13 +1712,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1007
1712
|
payload.task_id = context.metadata.taskId;
|
|
1008
1713
|
if (context?.metadata?.chatmode)
|
|
1009
1714
|
payload.chatmode = context.metadata.chatmode;
|
|
1715
|
+
// 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
|
|
1716
|
+
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} textLen=${finalText.length}`);
|
|
1010
1717
|
const isGroup = this.isGroupId(channelId);
|
|
1011
|
-
|
|
1012
|
-
const colonIdx = channelId.indexOf(':');
|
|
1013
|
-
const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
1014
|
-
if (colonIdx > 0) {
|
|
1015
|
-
payload.chat_id = channelId;
|
|
1016
|
-
}
|
|
1718
|
+
const targetAid = channelId;
|
|
1017
1719
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
1018
1720
|
const encrypt = context?.metadata?.encrypted != null
|
|
1019
1721
|
? !!(context.metadata.encrypted)
|
|
@@ -1023,7 +1725,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1023
1725
|
if (isGroup) {
|
|
1024
1726
|
params.group_id = channelId;
|
|
1025
1727
|
const result = await this.callAndTrace('group.send', params);
|
|
1026
|
-
|
|
1728
|
+
const mid = result?.message?.message_id ?? result?.message_id ?? null;
|
|
1729
|
+
if (!mid) {
|
|
1027
1730
|
const dispatchStatus = result?.message_dispatch?.status;
|
|
1028
1731
|
if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
|
|
1029
1732
|
logger.info(`${this.logPrefix()} group.send ok (${dispatchStatus}): group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
@@ -1033,7 +1736,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1033
1736
|
}
|
|
1034
1737
|
}
|
|
1035
1738
|
else {
|
|
1036
|
-
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${
|
|
1739
|
+
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1740
|
+
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 });
|
|
1741
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText);
|
|
1037
1742
|
}
|
|
1038
1743
|
}
|
|
1039
1744
|
else {
|
|
@@ -1044,8 +1749,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1044
1749
|
}
|
|
1045
1750
|
else {
|
|
1046
1751
|
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1752
|
+
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 });
|
|
1753
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText);
|
|
1047
1754
|
}
|
|
1048
1755
|
}
|
|
1756
|
+
return true;
|
|
1049
1757
|
}
|
|
1050
1758
|
catch (e) {
|
|
1051
1759
|
if (encrypt && e instanceof E2EEError) {
|
|
@@ -1056,7 +1764,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1056
1764
|
if (isGroup) {
|
|
1057
1765
|
this.trace('OUT', 'group.send.fallback', params);
|
|
1058
1766
|
const result = await this.client.call('group.send', params);
|
|
1059
|
-
this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message_id });
|
|
1767
|
+
this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message?.message_id ?? result?.message_id });
|
|
1060
1768
|
if (!result || !result.message_id) {
|
|
1061
1769
|
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
1062
1770
|
}
|
|
@@ -1069,15 +1777,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1069
1777
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
1070
1778
|
}
|
|
1071
1779
|
}
|
|
1780
|
+
return true;
|
|
1072
1781
|
}
|
|
1073
1782
|
catch (e2) {
|
|
1074
1783
|
this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
|
|
1075
1784
|
logger.error(`${this.logPrefix()} Plaintext fallback also failed to ${channelId}: ${e2}`);
|
|
1785
|
+
return false;
|
|
1076
1786
|
}
|
|
1077
1787
|
}
|
|
1078
1788
|
else {
|
|
1079
1789
|
this.trace('OUT', 'send.error', { channelId, error: String(e) });
|
|
1080
|
-
logger.error(`${this.logPrefix()} Send failed to ${channelId}: ${e}`);
|
|
1790
|
+
logger.error(`${this.logPrefix()} Send failed to ${channelId} (outbox id=${entry.id}): ${e}`);
|
|
1791
|
+
return false;
|
|
1081
1792
|
}
|
|
1082
1793
|
}
|
|
1083
1794
|
}
|
|
@@ -1094,9 +1805,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1094
1805
|
return;
|
|
1095
1806
|
if (!taskId)
|
|
1096
1807
|
return;
|
|
1097
|
-
//
|
|
1098
|
-
const
|
|
1099
|
-
const targetId = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
1808
|
+
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
1809
|
+
const targetId = channelId;
|
|
1100
1810
|
const encrypt = context?.metadata?.encrypted != null
|
|
1101
1811
|
? !!(context.metadata.encrypted)
|
|
1102
1812
|
: this.shouldEncrypt(targetId);
|
|
@@ -1106,16 +1816,19 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1106
1816
|
encrypt,
|
|
1107
1817
|
};
|
|
1108
1818
|
try {
|
|
1109
|
-
const
|
|
1819
|
+
const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
|
|
1820
|
+
const stage = payload?.stage ?? `items=${itemCount}`;
|
|
1110
1821
|
if (this.isGroupId(channelId)) {
|
|
1111
1822
|
params.group_id = targetId;
|
|
1112
|
-
await this.callAndTrace('group.thought.put', params);
|
|
1113
|
-
|
|
1823
|
+
const putRes = await this.callAndTrace('group.thought.put', params);
|
|
1824
|
+
const tid = putRes?.thought_id;
|
|
1825
|
+
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1114
1826
|
}
|
|
1115
1827
|
else {
|
|
1116
1828
|
params.to = targetId;
|
|
1117
|
-
await this.callAndTrace('message.thought.put', params);
|
|
1118
|
-
|
|
1829
|
+
const putRes = await this.callAndTrace('message.thought.put', params);
|
|
1830
|
+
const tid = putRes?.thought_id;
|
|
1831
|
+
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1119
1832
|
}
|
|
1120
1833
|
}
|
|
1121
1834
|
catch (e) {
|
|
@@ -1123,11 +1836,47 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1123
1836
|
logger.debug(`${this.logPrefix()} thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
1124
1837
|
}
|
|
1125
1838
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1839
|
+
/**
|
|
1840
|
+
* 发送结构化 payload(type='thought' 等)作为消息历史持久化。
|
|
1841
|
+
* 与 sendThought(thought.put)配对:thought.put 用于前端实时渲染(不入消息历史),
|
|
1842
|
+
* sendStructured 用于把同一内容写入消息历史。
|
|
1843
|
+
* 返回服务端分配的 message_id(失败时返回 null)。
|
|
1844
|
+
*/
|
|
1845
|
+
async sendStructured(channelId, payload, context) {
|
|
1846
|
+
if (!this.connected || !this.client)
|
|
1847
|
+
return null;
|
|
1848
|
+
const isGroup = this.isGroupId(channelId);
|
|
1849
|
+
const targetAid = channelId;
|
|
1850
|
+
const encryptTarget = isGroup ? channelId : targetAid;
|
|
1851
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1852
|
+
? !!(context.metadata.encrypted)
|
|
1853
|
+
: this.shouldEncrypt(encryptTarget);
|
|
1854
|
+
const finalPayload = { ...payload };
|
|
1855
|
+
if (context?.threadId && !finalPayload.thread_id)
|
|
1856
|
+
finalPayload.thread_id = context.threadId;
|
|
1857
|
+
const params = { payload: finalPayload, encrypt };
|
|
1858
|
+
try {
|
|
1859
|
+
if (isGroup) {
|
|
1860
|
+
params.group_id = channelId;
|
|
1861
|
+
const result = await this.callAndTrace('group.send', params);
|
|
1862
|
+
const mid = result?.message?.message_id ?? result?.message_id ?? null;
|
|
1863
|
+
logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
|
|
1864
|
+
return mid;
|
|
1865
|
+
}
|
|
1866
|
+
else {
|
|
1867
|
+
params.to = targetAid;
|
|
1868
|
+
const result = await this.callAndTrace('message.send', params);
|
|
1869
|
+
logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
|
|
1870
|
+
return result?.message_id ?? null;
|
|
1871
|
+
}
|
|
1130
1872
|
}
|
|
1873
|
+
catch (e) {
|
|
1874
|
+
const err = e;
|
|
1875
|
+
logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
async sendFile(channelId, filePath, context) {
|
|
1131
1880
|
const absPath = path.resolve(filePath);
|
|
1132
1881
|
if (!fs.existsSync(absPath)) {
|
|
1133
1882
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
@@ -1142,15 +1891,42 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1142
1891
|
logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
1143
1892
|
return;
|
|
1144
1893
|
}
|
|
1894
|
+
// Write-ahead: persist to outbox
|
|
1895
|
+
const entry = outbox.enqueue(this.config.aid, {
|
|
1896
|
+
channelId,
|
|
1897
|
+
type: 'file',
|
|
1898
|
+
filePath: absPath,
|
|
1899
|
+
context,
|
|
1900
|
+
});
|
|
1901
|
+
logger.debug(`${this.logPrefix()} Outbox enqueued file: id=${entry.id} channel=${channelId} file=${absPath}`);
|
|
1902
|
+
if (!this.connected || !this.client) {
|
|
1903
|
+
logger.warn(`${this.logPrefix()} Not connected, file send queued in outbox (id=${entry.id}). Triggering reconnect.`);
|
|
1904
|
+
if (!this.reconnectTimer && !this.client) {
|
|
1905
|
+
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
|
|
1906
|
+
}
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
const ok = await this.deliverFileEntry(entry);
|
|
1910
|
+
if (ok) {
|
|
1911
|
+
outbox.remove(this.config.aid, entry.id);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
async deliverFileEntry(entry) {
|
|
1915
|
+
const channelId = entry.channelId;
|
|
1916
|
+
const absPath = entry.filePath;
|
|
1917
|
+
const context = entry.context;
|
|
1918
|
+
if (!fs.existsSync(absPath)) {
|
|
1919
|
+
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
1920
|
+
return true; // remove from outbox, file no longer exists
|
|
1921
|
+
}
|
|
1145
1922
|
const filename = path.basename(absPath);
|
|
1146
1923
|
const fileData = fs.readFileSync(absPath);
|
|
1924
|
+
const stat = fs.statSync(absPath);
|
|
1147
1925
|
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
1148
1926
|
const contentType = guessMime(filename);
|
|
1149
1927
|
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
1150
1928
|
try {
|
|
1151
|
-
// Upload to storage
|
|
1152
1929
|
if (stat.size <= 64 * 1024) {
|
|
1153
|
-
// Inline upload for small files (≤64KB)
|
|
1154
1930
|
await this.callAndTrace('storage.put_object', {
|
|
1155
1931
|
object_key: objectKey,
|
|
1156
1932
|
content: fileData.toString('base64'),
|
|
@@ -1160,7 +1936,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1160
1936
|
});
|
|
1161
1937
|
}
|
|
1162
1938
|
else {
|
|
1163
|
-
// Ticket upload for large files
|
|
1164
1939
|
const session = await this.callAndTrace('storage.create_upload_session', {
|
|
1165
1940
|
object_key: objectKey,
|
|
1166
1941
|
size_bytes: stat.size,
|
|
@@ -1182,7 +1957,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1182
1957
|
size_bytes: stat.size,
|
|
1183
1958
|
});
|
|
1184
1959
|
}
|
|
1185
|
-
// Send message with attachment
|
|
1186
1960
|
const attachment = {
|
|
1187
1961
|
owner_aid: this._aid || '',
|
|
1188
1962
|
object_key: objectKey,
|
|
@@ -1203,12 +1977,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1203
1977
|
if (context?.metadata?.chatmode)
|
|
1204
1978
|
filePayload.chatmode = context.metadata.chatmode;
|
|
1205
1979
|
const isGroup = this.isGroupId(channelId);
|
|
1206
|
-
|
|
1207
|
-
const fileColonIdx = channelId.indexOf(':');
|
|
1208
|
-
const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
|
|
1209
|
-
if (fileColonIdx > 0) {
|
|
1210
|
-
filePayload.chat_id = channelId;
|
|
1211
|
-
}
|
|
1980
|
+
const fileTargetAid = channelId;
|
|
1212
1981
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
1213
1982
|
const encrypt = context?.metadata?.encrypted != null
|
|
1214
1983
|
? !!(context.metadata.encrypted)
|
|
@@ -1219,8 +1988,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1219
1988
|
params.group_id = channelId;
|
|
1220
1989
|
this.trace('OUT', 'group.send.file', params);
|
|
1221
1990
|
const result = await this.client.call('group.send', params);
|
|
1222
|
-
|
|
1223
|
-
|
|
1991
|
+
const fileMid = result?.message?.message_id ?? result?.message_id;
|
|
1992
|
+
this.trace('OUT', 'group.send.file.ok', { message_id: fileMid });
|
|
1993
|
+
if (!fileMid) {
|
|
1224
1994
|
logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
1225
1995
|
}
|
|
1226
1996
|
}
|
|
@@ -1246,8 +2016,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1246
2016
|
if (isGroup) {
|
|
1247
2017
|
this.trace('OUT', 'group.send.file.fallback', params);
|
|
1248
2018
|
const result = await this.client.call('group.send', params);
|
|
1249
|
-
|
|
1250
|
-
|
|
2019
|
+
const fbMid = result?.message?.message_id ?? result?.message_id;
|
|
2020
|
+
this.trace('OUT', 'group.send.file.fallback.ok', { message_id: fbMid });
|
|
2021
|
+
if (!fbMid) {
|
|
1251
2022
|
logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
1252
2023
|
}
|
|
1253
2024
|
}
|
|
@@ -1265,10 +2036,48 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1265
2036
|
}
|
|
1266
2037
|
}
|
|
1267
2038
|
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
2039
|
+
return true;
|
|
1268
2040
|
}
|
|
1269
2041
|
catch (e) {
|
|
1270
|
-
this.trace('OUT', 'sendFile.error', { channelId, filePath, error: String(e) });
|
|
1271
|
-
logger.error(`${this.logPrefix()} sendFile failed for ${channelId}: ${e}`);
|
|
2042
|
+
this.trace('OUT', 'sendFile.error', { channelId, filePath: absPath, error: String(e) });
|
|
2043
|
+
logger.error(`${this.logPrefix()} sendFile failed for ${channelId} (outbox id=${entry.id}): ${e}`);
|
|
2044
|
+
return false;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
// ── Outbox drain ───────────────────────────────────────────
|
|
2048
|
+
outboxTimer = null;
|
|
2049
|
+
startOutboxTimer() {
|
|
2050
|
+
if (this.outboxTimer)
|
|
2051
|
+
return;
|
|
2052
|
+
this.outboxTimer = setInterval(() => {
|
|
2053
|
+
if (this.connected && this.client && outbox.hasPending(this.config.aid)) {
|
|
2054
|
+
this.drainOutbox();
|
|
2055
|
+
}
|
|
2056
|
+
}, 30_000);
|
|
2057
|
+
}
|
|
2058
|
+
stopOutboxTimer() {
|
|
2059
|
+
if (this.outboxTimer) {
|
|
2060
|
+
clearInterval(this.outboxTimer);
|
|
2061
|
+
this.outboxTimer = null;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
async drainOutbox() {
|
|
2065
|
+
if (!this.connected || !this.client)
|
|
2066
|
+
return;
|
|
2067
|
+
if (!outbox.hasPending(this.config.aid))
|
|
2068
|
+
return;
|
|
2069
|
+
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
2070
|
+
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
2071
|
+
if (entry.type === 'text') {
|
|
2072
|
+
return this.deliverTextEntry(entry);
|
|
2073
|
+
}
|
|
2074
|
+
else if (entry.type === 'file') {
|
|
2075
|
+
return this.deliverFileEntry(entry);
|
|
2076
|
+
}
|
|
2077
|
+
return true; // unknown type, discard
|
|
2078
|
+
});
|
|
2079
|
+
if (result.sent > 0 || result.expired > 0) {
|
|
2080
|
+
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed}`);
|
|
1272
2081
|
}
|
|
1273
2082
|
}
|
|
1274
2083
|
acknowledge(messageId) {
|
|
@@ -1281,62 +2090,72 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1281
2090
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
1282
2091
|
if (!this.client || !this.connected)
|
|
1283
2092
|
return;
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
2093
|
+
const severity = status === 'error' || status === 'timeout' ? 'error' : 'info';
|
|
2094
|
+
const stateMap = {
|
|
2095
|
+
start: 'started',
|
|
2096
|
+
done: 'completed',
|
|
2097
|
+
interrupted: 'interrupted',
|
|
2098
|
+
error: 'error',
|
|
2099
|
+
timeout: 'timeout',
|
|
2100
|
+
queued: 'queued',
|
|
1290
2101
|
};
|
|
1291
|
-
const
|
|
1292
|
-
type: '
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
2102
|
+
const statusPayload = {
|
|
2103
|
+
type: 'status',
|
|
2104
|
+
state: stateMap[status] ?? status,
|
|
2105
|
+
task_id: taskId,
|
|
2106
|
+
session_id: sessionId,
|
|
2107
|
+
severity,
|
|
1296
2108
|
};
|
|
1297
2109
|
if (context?.threadId)
|
|
1298
|
-
|
|
2110
|
+
statusPayload.thread_id = context.threadId;
|
|
2111
|
+
if (context?.peerId)
|
|
2112
|
+
statusPayload.initiator = context.peerId;
|
|
2113
|
+
if (context?.replyToMessageId)
|
|
2114
|
+
statusPayload.ref_message_id = context.replyToMessageId;
|
|
1299
2115
|
const isGroup = this.isGroupId(channelId);
|
|
1300
|
-
//
|
|
1301
|
-
const
|
|
1302
|
-
const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
|
|
1303
|
-
if (statusColonIdx > 0) {
|
|
1304
|
-
payload.chat_id = channelId;
|
|
1305
|
-
}
|
|
2116
|
+
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
2117
|
+
const statusTargetAid = channelId;
|
|
1306
2118
|
const encryptTarget = isGroup ? channelId : statusTargetAid;
|
|
1307
|
-
const
|
|
2119
|
+
const computeEncrypt = () => context?.metadata?.encrypted != null
|
|
1308
2120
|
? !!(context.metadata.encrypted)
|
|
1309
2121
|
: this.shouldEncrypt(encryptTarget);
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
2122
|
+
const sendOne = (method, payload, label) => {
|
|
2123
|
+
const c = this.client;
|
|
2124
|
+
if (!c) {
|
|
2125
|
+
logger.debug(`${this.logPrefix()} ${label} skipped: client gone`);
|
|
2126
|
+
return Promise.resolve();
|
|
2127
|
+
}
|
|
2128
|
+
const encrypt = computeEncrypt();
|
|
2129
|
+
const params = { payload, encrypt };
|
|
2130
|
+
if (isGroup)
|
|
2131
|
+
params.group_id = channelId;
|
|
2132
|
+
else
|
|
2133
|
+
params.to = statusTargetAid;
|
|
2134
|
+
this.trace('OUT', `${method}.task_${label}`, params);
|
|
2135
|
+
return c.call(method, params).catch(e => {
|
|
1313
2136
|
if (encrypt && e instanceof E2EEError) {
|
|
1314
2137
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
1315
|
-
logger.warn(`${this.logPrefix()} E2EE
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2138
|
+
logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
|
|
2139
|
+
const c2 = this.client;
|
|
2140
|
+
if (!c2)
|
|
2141
|
+
return;
|
|
2142
|
+
const fallbackParams = { ...params, encrypt: false };
|
|
2143
|
+
return c2.call(method, fallbackParams).catch(e2 => {
|
|
2144
|
+
logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
|
|
1319
2145
|
});
|
|
1320
2146
|
}
|
|
1321
|
-
|
|
1322
|
-
logger.debug(`${this.logPrefix()} Processing status failed: ${e}`);
|
|
1323
|
-
}
|
|
2147
|
+
logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
|
|
1324
2148
|
});
|
|
1325
2149
|
};
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
sendWithFallback('group.send');
|
|
1330
|
-
}
|
|
1331
|
-
else {
|
|
1332
|
-
params.to = statusTargetAid;
|
|
1333
|
-
this.trace('OUT', 'message.send.status', params);
|
|
1334
|
-
sendWithFallback('message.send');
|
|
1335
|
-
}
|
|
2150
|
+
const method = isGroup ? 'group.send' : 'message.send';
|
|
2151
|
+
sendOne(method, statusPayload, 'status');
|
|
2152
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
|
|
1336
2153
|
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
1337
2154
|
const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
|
|
1338
2155
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
1339
|
-
|
|
2156
|
+
const initiator = statusPayload.initiator ?? '';
|
|
2157
|
+
const refMsgId = statusPayload.ref_message_id ?? '';
|
|
2158
|
+
logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}`);
|
|
1340
2159
|
}
|
|
1341
2160
|
sendCustomPayload(channelId, payload) {
|
|
1342
2161
|
if (!this.client || !this.connected)
|
|
@@ -1351,12 +2170,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1351
2170
|
catch {
|
|
1352
2171
|
payloadObj = { text: payload };
|
|
1353
2172
|
}
|
|
1354
|
-
//
|
|
1355
|
-
const
|
|
1356
|
-
const customTargetAid = customColonIdx > 0 ? channelId.substring(0, customColonIdx) : channelId;
|
|
1357
|
-
if (customColonIdx > 0) {
|
|
1358
|
-
payloadObj.chat_id = channelId;
|
|
1359
|
-
}
|
|
2173
|
+
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
2174
|
+
const customTargetAid = channelId;
|
|
1360
2175
|
const sendParams = {
|
|
1361
2176
|
to: customTargetAid, payload: payloadObj,
|
|
1362
2177
|
encrypt: true,
|
|
@@ -1387,43 +2202,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1387
2202
|
this.client = null;
|
|
1388
2203
|
}
|
|
1389
2204
|
this.connected = false;
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
2205
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
2206
|
+
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
2207
|
+
this.setAidStatus('disabled');
|
|
2208
|
+
if (this.traceWriter) {
|
|
2209
|
+
this.traceWriter.close();
|
|
2210
|
+
this.traceWriter = null;
|
|
1394
2211
|
}
|
|
1395
2212
|
logger.info(`${this.logPrefix()} Disconnected`);
|
|
1396
2213
|
}
|
|
1397
|
-
// ── TS-layer reconnect
|
|
2214
|
+
// ── TS-layer reconnect ─────────────────────────────────────
|
|
2215
|
+
// SDK 内部已经跑无限指数退避(max_attempts=0, max_delay=300s),
|
|
2216
|
+
// TS 层只负责:(1) initClient 失败时安排兜底重试;(2) flap/kicked 接管路径见 takeoverReconnect。
|
|
1398
2217
|
scheduleReconnect() {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
if (this.reconnectTimer)
|
|
1402
|
-
return;
|
|
1403
|
-
const delays = AUNChannel.RECONNECT_DELAYS;
|
|
1404
|
-
if (this.reconnectAttempt >= delays.length) {
|
|
1405
|
-
logger.error(`${this.logPrefix()} All ${delays.length} reconnect attempts exhausted, giving up`);
|
|
1406
|
-
this.onChannelDown?.();
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
const delay = delays[this.reconnectAttempt];
|
|
1410
|
-
this.reconnectAttempt++;
|
|
1411
|
-
logger.info(`${this.logPrefix()} Scheduling reconnect #${this.reconnectAttempt}/${delays.length} in ${delay}s`);
|
|
1412
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
1413
|
-
this.reconnectTimer = null;
|
|
1414
|
-
try {
|
|
1415
|
-
logger.info(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} starting...`);
|
|
1416
|
-
this.trace('OUT', 'reconnect.start', { attempt: this.reconnectAttempt });
|
|
1417
|
-
await this.initClient();
|
|
1418
|
-
this.trace('OUT', 'reconnect.ok', { attempt: this.reconnectAttempt });
|
|
1419
|
-
logger.info(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} succeeded`);
|
|
1420
|
-
}
|
|
1421
|
-
catch (err) {
|
|
1422
|
-
this.trace('OUT', 'reconnect.error', { attempt: this.reconnectAttempt, error: String(err) });
|
|
1423
|
-
logger.error(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} failed:`, err);
|
|
1424
|
-
this.scheduleReconnect();
|
|
1425
|
-
}
|
|
1426
|
-
}, delay * 1000);
|
|
2218
|
+
// initClient 早期失败(auth / connect 阶段)走这里:用 fallback 延迟兜底
|
|
2219
|
+
this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'terminal');
|
|
1427
2220
|
}
|
|
1428
2221
|
/** Manually trigger reconnect (e.g. from /check reconnect command) */
|
|
1429
2222
|
async reconnect() {
|
|
@@ -1433,7 +2226,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1433
2226
|
clearTimeout(this.reconnectTimer);
|
|
1434
2227
|
this.reconnectTimer = null;
|
|
1435
2228
|
}
|
|
1436
|
-
this.
|
|
2229
|
+
this.flapCount = 0;
|
|
1437
2230
|
try {
|
|
1438
2231
|
await this.initClient();
|
|
1439
2232
|
return `重连成功 (${this._aid})`;
|
|
@@ -1443,7 +2236,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1443
2236
|
return `重连失败: ${err},已安排自动重试`;
|
|
1444
2237
|
}
|
|
1445
2238
|
}
|
|
1446
|
-
/** Set callback for when all reconnect attempts are exhausted */
|
|
2239
|
+
/** Set callback for when all reconnect attempts are exhausted (deprecated: 现在无限重试,不会触发) */
|
|
1447
2240
|
setOnChannelDown(callback) {
|
|
1448
2241
|
this.onChannelDown = callback;
|
|
1449
2242
|
}
|
|
@@ -1452,18 +2245,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1452
2245
|
return {
|
|
1453
2246
|
connected: this.connected,
|
|
1454
2247
|
aid: this._aid,
|
|
1455
|
-
|
|
1456
|
-
maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
|
|
2248
|
+
flapCount: this.flapCount,
|
|
1457
2249
|
plaintextRecv: this.plaintextRecv,
|
|
1458
2250
|
};
|
|
1459
2251
|
}
|
|
1460
|
-
/** 读取本地 agent.md 中的 name
|
|
2252
|
+
/** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
|
|
1461
2253
|
loadSelfName(aid) {
|
|
1462
2254
|
try {
|
|
1463
2255
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
1464
2256
|
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
1465
|
-
if (!fs.existsSync(agentMdPath))
|
|
2257
|
+
if (!fs.existsSync(agentMdPath)) {
|
|
2258
|
+
// 异步拉取,不阻塞连接流程
|
|
2259
|
+
this.fetchAndCacheSelfName(aidName);
|
|
1466
2260
|
return undefined;
|
|
2261
|
+
}
|
|
1467
2262
|
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
1468
2263
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1469
2264
|
if (!fmMatch)
|
|
@@ -1475,6 +2270,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1475
2270
|
return undefined;
|
|
1476
2271
|
}
|
|
1477
2272
|
}
|
|
2273
|
+
async fetchAndCacheSelfName(aidName) {
|
|
2274
|
+
try {
|
|
2275
|
+
const { agentmdGet } = await import('../aun/aid/index.js');
|
|
2276
|
+
const content = await agentmdGet(aidName);
|
|
2277
|
+
if (content) {
|
|
2278
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2279
|
+
if (fmMatch) {
|
|
2280
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
2281
|
+
const name = nameMatch?.[1]?.trim();
|
|
2282
|
+
if (name) {
|
|
2283
|
+
this._selfName = name;
|
|
2284
|
+
if (this.aidStatsCollector)
|
|
2285
|
+
this.aidStatsCollector.setSelfName(this.config.aid, name);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
catch {
|
|
2291
|
+
// ignore — name will remain undefined
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
1478
2294
|
getSelfName() {
|
|
1479
2295
|
return this._selfName;
|
|
1480
2296
|
}
|
|
@@ -1500,6 +2316,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1500
2316
|
return { type: null }; // no agent.md → unknown
|
|
1501
2317
|
}
|
|
1502
2318
|
}
|
|
2319
|
+
/** 同步取 peerInfo 缓存,未命中返回 undefined,不发起任何网络请求。 */
|
|
2320
|
+
peerInfoCached(aid) {
|
|
2321
|
+
return this.peerInfoCache.get(aid);
|
|
2322
|
+
}
|
|
2323
|
+
/** 后台预取 peerInfo(下次需要时缓存已就绪),任何错误吞掉。 */
|
|
2324
|
+
prefetchPeerInfo(aid) {
|
|
2325
|
+
if (this.peerInfoCache.has(aid))
|
|
2326
|
+
return;
|
|
2327
|
+
void this.fetchPeerInfo(aid).catch(() => { });
|
|
2328
|
+
}
|
|
1503
2329
|
async uploadAgentMd(content) {
|
|
1504
2330
|
if (!this.client)
|
|
1505
2331
|
throw new Error('not connected');
|
|
@@ -1537,20 +2363,143 @@ export class AUNChannelPlugin {
|
|
|
1537
2363
|
flushDelay: inst.flushDelay,
|
|
1538
2364
|
encryptionSeed: inst.encryptionSeed,
|
|
1539
2365
|
owner: inst.owner,
|
|
2366
|
+
agentName: inst.agentName,
|
|
2367
|
+
channelName: inst.name,
|
|
1540
2368
|
aunTrace: config.debug?.aunTrace,
|
|
1541
2369
|
aunSdkLog: config.debug?.aunSdkLog,
|
|
1542
2370
|
});
|
|
1543
2371
|
const adapter = {
|
|
1544
2372
|
channelName: inst.name,
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
2373
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
|
|
2374
|
+
send: async (envelope, payload) => {
|
|
2375
|
+
const ctx = envelope.replyContext;
|
|
2376
|
+
const channelId = envelope.channelId;
|
|
2377
|
+
switch (payload.kind) {
|
|
2378
|
+
case 'result.text':
|
|
2379
|
+
case 'command.result':
|
|
2380
|
+
case 'command.error':
|
|
2381
|
+
case 'system.notice':
|
|
2382
|
+
case 'system.error':
|
|
2383
|
+
case 'result.error': {
|
|
2384
|
+
const sendCtx = { ...(ctx ?? {}) };
|
|
2385
|
+
if (payload.kind === 'result.text' && payload.isFinal)
|
|
2386
|
+
sendCtx.title = '✅ 最终回复:';
|
|
2387
|
+
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
case 'result.file':
|
|
2391
|
+
await channel.sendFile(channelId, payload.filePath, ctx);
|
|
2392
|
+
return;
|
|
2393
|
+
case 'result.image': {
|
|
2394
|
+
// AUN 支持 image,走 sendStructured 发 type=image payload
|
|
2395
|
+
const buf = payload.data;
|
|
2396
|
+
const b64 = buf.toString('base64');
|
|
2397
|
+
await channel.sendStructured(channelId, {
|
|
2398
|
+
type: 'image',
|
|
2399
|
+
alt: payload.alt,
|
|
2400
|
+
data_base64: b64,
|
|
2401
|
+
mime_type: payload.mimeType,
|
|
2402
|
+
}, ctx);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
case 'activity.batch': {
|
|
2406
|
+
const aunPayload = {
|
|
2407
|
+
type: 'thought',
|
|
2408
|
+
items: payload.items,
|
|
2409
|
+
client_context: { task_id: envelope.taskId, chatmode: envelope.chatmode, agent_name: envelope.agentName },
|
|
2410
|
+
};
|
|
2411
|
+
if (ctx?.threadId)
|
|
2412
|
+
aunPayload.thread_id = ctx.threadId;
|
|
2413
|
+
if (envelope.chatmode === 'proactive') {
|
|
2414
|
+
await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
|
|
2415
|
+
}
|
|
2416
|
+
else {
|
|
2417
|
+
await Promise.all([
|
|
2418
|
+
channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
|
|
2419
|
+
channel.sendStructured(channelId, aunPayload, ctx),
|
|
2420
|
+
]);
|
|
2421
|
+
}
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
case 'status.started':
|
|
2425
|
+
channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
|
|
2426
|
+
return;
|
|
2427
|
+
case 'status.queued':
|
|
2428
|
+
channel.sendProcessingStatus(channelId, 'queued', envelope.taskId, envelope.taskId, ctx);
|
|
2429
|
+
return;
|
|
2430
|
+
case 'status.completed':
|
|
2431
|
+
channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx);
|
|
2432
|
+
return;
|
|
2433
|
+
case 'status.interrupted':
|
|
2434
|
+
channel.sendProcessingStatus(channelId, 'interrupted', envelope.taskId, envelope.taskId, ctx);
|
|
2435
|
+
return;
|
|
2436
|
+
case 'status.error':
|
|
2437
|
+
channel.sendProcessingStatus(channelId, 'error', envelope.taskId, envelope.taskId, ctx);
|
|
2438
|
+
return;
|
|
2439
|
+
case 'status.timeout':
|
|
2440
|
+
channel.sendProcessingStatus(channelId, 'timeout', envelope.taskId, envelope.taskId, ctx);
|
|
2441
|
+
return;
|
|
2442
|
+
case 'interaction': {
|
|
2443
|
+
const req = payload.interaction;
|
|
2444
|
+
if (req.kind.kind === 'action') {
|
|
2445
|
+
const action = req.kind;
|
|
2446
|
+
const aunCard = {
|
|
2447
|
+
type: 'action_card',
|
|
2448
|
+
title: action.title,
|
|
2449
|
+
actions: action.buttons.map(btn => ({
|
|
2450
|
+
label: btn.label,
|
|
2451
|
+
value: btn.key,
|
|
2452
|
+
style: btn.style ?? 'default',
|
|
2453
|
+
behavior: 'reply',
|
|
2454
|
+
})),
|
|
2455
|
+
};
|
|
2456
|
+
if (action.body)
|
|
2457
|
+
aunCard.description = action.body;
|
|
2458
|
+
if (ctx?.threadId)
|
|
2459
|
+
aunCard.thread_id = ctx.threadId;
|
|
2460
|
+
const msgId = await channel.sendStructured(channelId, aunCard, ctx);
|
|
2461
|
+
if (msgId) {
|
|
2462
|
+
channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: false });
|
|
2463
|
+
setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
else if (req.kind.kind === 'command-card') {
|
|
2467
|
+
const card = req.kind;
|
|
2468
|
+
const aunCard = {
|
|
2469
|
+
type: 'action_card',
|
|
2470
|
+
title: card.title,
|
|
2471
|
+
actions: card.buttons.map(btn => ({
|
|
2472
|
+
label: btn.label,
|
|
2473
|
+
value: btn.command,
|
|
2474
|
+
style: btn.style ?? 'default',
|
|
2475
|
+
behavior: 'reply',
|
|
2476
|
+
})),
|
|
2477
|
+
};
|
|
2478
|
+
if (card.body)
|
|
2479
|
+
aunCard.description = card.body;
|
|
2480
|
+
if (ctx?.threadId)
|
|
2481
|
+
aunCard.thread_id = ctx.threadId;
|
|
2482
|
+
const msgId = await channel.sendStructured(channelId, aunCard, ctx);
|
|
2483
|
+
if (msgId) {
|
|
2484
|
+
channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true, initiatorAid: req.initiatorId });
|
|
2485
|
+
setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
else if (payload.fallbackText) {
|
|
2489
|
+
await channel.sendMessage(channelId, payload.fallbackText, ctx);
|
|
2490
|
+
}
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
case 'custom': {
|
|
2494
|
+
const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
|
|
2495
|
+
channel.sendCustomPayload(channelId, text);
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
default:
|
|
2499
|
+
logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
|
|
2500
|
+
}
|
|
2501
|
+
}, acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); }, onInteraction: (cb) => { channel.interactionCallback = cb; }, uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
2502
|
+
downloadAgentMd: (aid) => channel.downloadAgentMd(aid), _selfAid: () => channel.getStatus().aid,
|
|
1554
2503
|
_selfName: () => channel.getSelfName(),
|
|
1555
2504
|
};
|
|
1556
2505
|
const policy = {
|
|
@@ -1595,6 +2544,48 @@ export class AUNChannelPlugin {
|
|
|
1595
2544
|
connect: () => channel.connect(),
|
|
1596
2545
|
disconnect: () => channel.disconnect(),
|
|
1597
2546
|
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
2547
|
+
registerBridge(bridge, channelType) {
|
|
2548
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
2549
|
+
handler({
|
|
2550
|
+
channel: adapter.channelName,
|
|
2551
|
+
channelType,
|
|
2552
|
+
channelId: opts.channelId,
|
|
2553
|
+
selfId: opts.selfId,
|
|
2554
|
+
groupId: opts.groupId,
|
|
2555
|
+
content: opts.content,
|
|
2556
|
+
chatType: opts.chatType || 'private',
|
|
2557
|
+
peerId: opts.peerId || '',
|
|
2558
|
+
peerName: opts.peerName,
|
|
2559
|
+
peerType: opts.peerType,
|
|
2560
|
+
messageId: opts.messageId,
|
|
2561
|
+
mentions: opts.mentions,
|
|
2562
|
+
threadId: opts.threadId,
|
|
2563
|
+
replyContext: opts.replyContext,
|
|
2564
|
+
source: opts.source,
|
|
2565
|
+
});
|
|
2566
|
+
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
2567
|
+
},
|
|
2568
|
+
registerHooks(ctx) {
|
|
2569
|
+
channel.setEventBus(ctx.eventBus);
|
|
2570
|
+
if (channel.setOnChannelDown) {
|
|
2571
|
+
channel.setOnChannelDown(() => {
|
|
2572
|
+
ctx.eventBus.publish({
|
|
2573
|
+
type: 'channel:error',
|
|
2574
|
+
channel: 'aun',
|
|
2575
|
+
channelName: adapter.channelName,
|
|
2576
|
+
status: 'auth_error',
|
|
2577
|
+
message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
2578
|
+
timestamp: Date.now(),
|
|
2579
|
+
});
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
if (typeof channel.setDispatchModeResolver === 'function') {
|
|
2583
|
+
channel.setDispatchModeResolver(async (channelId) => {
|
|
2584
|
+
const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);
|
|
2585
|
+
return session?.metadata?.dispatchMode;
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
1598
2589
|
});
|
|
1599
2590
|
}
|
|
1600
2591
|
return result;
|