evolclaw 2.8.2 → 3.0.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/dist/agents/claude-runner.js +105 -30
- package/dist/agents/codex-runner.js +15 -7
- package/dist/agents/gemini-runner.js +14 -5
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -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 +1064 -279
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/baseagent-loader.js +48 -0
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +1090 -838
- package/dist/core/evolagent-registry.js +191 -360
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +326 -145
- package/dist/core/message/message-queue.js +5 -5
- package/dist/core/permission.js +21 -8
- 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 +704 -775
- 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/{templates → data}/prompts.md +34 -1
- package/dist/index.js +437 -273
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -576
- package/dist/core/agent-loader.js +0 -39
- 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/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/channels/aun.js
CHANGED
|
@@ -4,28 +4,54 @@ 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 { loadAgent, saveAgent } from '../config-store.js';
|
|
13
|
+
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
14
|
+
import * as outbox from '../aun/outbox.js';
|
|
15
|
+
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
16
|
+
/**
|
|
17
|
+
* 构造 connect extra_info:自描述本进程身份。
|
|
18
|
+
*
|
|
19
|
+
* 用途:另一个进程踢掉本连接时,本进程会从 SDK 'gateway.disconnect' 事件的
|
|
20
|
+
* detail.new_extra_info 里看到对方的 extra_info;同时 detail.self_extra_info
|
|
21
|
+
* 是本进程当时连接时上报的内容。把双方信息打到日志便于诊断"谁踢了谁"。
|
|
22
|
+
*
|
|
23
|
+
* 字段需保持稳定(被踢方靠它分辨对方身份)。
|
|
24
|
+
*/
|
|
25
|
+
function buildConnectExtraInfo(opts) {
|
|
26
|
+
const startedAt = getProcessStartTime(process.pid) ?? Date.now();
|
|
27
|
+
return {
|
|
28
|
+
app: 'evolclaw',
|
|
29
|
+
version: getEvolclawVersion(),
|
|
30
|
+
pid: process.pid,
|
|
31
|
+
started_at: startedAt,
|
|
32
|
+
started_at_iso: new Date(startedAt).toISOString(),
|
|
33
|
+
hostname: os.hostname(),
|
|
34
|
+
platform: process.platform,
|
|
35
|
+
node_version: process.version,
|
|
36
|
+
evolclaw_home: process.env.EVOLCLAW_HOME || '',
|
|
37
|
+
launched_by: process.env.EVOLCLAW_LAUNCHED_BY || '',
|
|
38
|
+
aid: opts.aid,
|
|
39
|
+
agent_name: opts.agentName ?? '',
|
|
40
|
+
channel_name: opts.channelName ?? '',
|
|
20
41
|
};
|
|
21
|
-
return map[ext] || 'application/octet-stream';
|
|
22
42
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
let _cachedVersion = null;
|
|
44
|
+
function getEvolclawVersion() {
|
|
45
|
+
if (_cachedVersion !== null)
|
|
46
|
+
return _cachedVersion;
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
|
|
49
|
+
_cachedVersion = String(pkg.version ?? '');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
_cachedVersion = '';
|
|
53
|
+
}
|
|
54
|
+
return _cachedVersion;
|
|
29
55
|
}
|
|
30
56
|
export class AUNChannel {
|
|
31
57
|
config;
|
|
@@ -34,13 +60,15 @@ export class AUNChannel {
|
|
|
34
60
|
messageHandler;
|
|
35
61
|
recallHandler;
|
|
36
62
|
connected = false;
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
traceWriter = null;
|
|
64
|
+
eventBus = null;
|
|
65
|
+
ownerBoundHandler = null;
|
|
66
|
+
pendingEchoMessages = new Map();
|
|
67
|
+
isEchoSending = false;
|
|
39
68
|
trace(dir, event, data) {
|
|
40
69
|
if (!this.config.aunTrace)
|
|
41
70
|
return;
|
|
42
|
-
this.
|
|
43
|
-
if (!this.traceStream)
|
|
71
|
+
if (!this.traceWriter)
|
|
44
72
|
return;
|
|
45
73
|
// 自动从 data 推断顶层字段(self_aid / peer_aid / group_id / task_id / chatmode),
|
|
46
74
|
// 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
|
|
@@ -64,7 +92,7 @@ export class AUNChannel {
|
|
|
64
92
|
if (chatmode)
|
|
65
93
|
topContext.chatmode = chatmode;
|
|
66
94
|
const line = JSON.stringify({ ts: localTimestamp(), dir, event, ...topContext, data });
|
|
67
|
-
this.
|
|
95
|
+
this.traceWriter.write(line);
|
|
68
96
|
}
|
|
69
97
|
/** 日志前缀(含 self aid 简称,多实例可识别) */
|
|
70
98
|
logPrefix() {
|
|
@@ -80,6 +108,11 @@ export class AUNChannel {
|
|
|
80
108
|
*/
|
|
81
109
|
async callAndTrace(method, params, opts) {
|
|
82
110
|
this.trace('OUT', method, params);
|
|
111
|
+
// [DIAG-STALE] 记录调用瞬间 SDK 内部 _state,证明是否在 reconnecting 中误发
|
|
112
|
+
const sdkStateBefore = this.client?._state ?? 'no-client';
|
|
113
|
+
if (sdkStateBefore !== 'connected') {
|
|
114
|
+
logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} on non-connected SDK: sdk_state=${sdkStateBefore} evolclaw_connected=${this.connected}`);
|
|
115
|
+
}
|
|
83
116
|
try {
|
|
84
117
|
const result = await this.client.call(method, params);
|
|
85
118
|
if (!opts?.silentOk) {
|
|
@@ -97,44 +130,13 @@ export class AUNChannel {
|
|
|
97
130
|
code: e?.code,
|
|
98
131
|
name: e?.name,
|
|
99
132
|
});
|
|
133
|
+
// [DIAG-STALE] 失败时再记录一次 SDK _state,看错误类型是否为 ConnectionError
|
|
134
|
+
const sdkStateAfter = this.client?._state ?? 'no-client';
|
|
135
|
+
logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} FAILED: err_name=${e?.name ?? '?'} err_code=${e?.code ?? '?'} sdk_state_before=${sdkStateBefore} sdk_state_after=${sdkStateAfter} evolclaw_connected=${this.connected}`);
|
|
100
136
|
logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
|
|
101
137
|
throw e;
|
|
102
138
|
}
|
|
103
139
|
}
|
|
104
|
-
static AUN_TRACE_RE = /^aun-\d{8}-\d{2}\.log$/;
|
|
105
|
-
static AUN_RETAIN_HOURS = 12;
|
|
106
|
-
rotateTraceIfNeeded() {
|
|
107
|
-
const d = new Date();
|
|
108
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
109
|
-
const hourTag = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}`;
|
|
110
|
-
if (this.traceHourTag === hourTag && this.traceStream)
|
|
111
|
-
return;
|
|
112
|
-
if (this.traceStream) {
|
|
113
|
-
this.traceStream.end();
|
|
114
|
-
this.traceStream = null;
|
|
115
|
-
}
|
|
116
|
-
this.traceHourTag = hourTag;
|
|
117
|
-
const logPath = path.join(resolvePaths().logs, `aun-${hourTag}.log`);
|
|
118
|
-
this.traceStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
119
|
-
this.cleanupOldTraceLogs();
|
|
120
|
-
}
|
|
121
|
-
cleanupOldTraceLogs() {
|
|
122
|
-
const logsDir = resolvePaths().logs;
|
|
123
|
-
const cutoff = Date.now() - AUNChannel.AUN_RETAIN_HOURS * 60 * 60 * 1000;
|
|
124
|
-
try {
|
|
125
|
-
for (const name of fs.readdirSync(logsDir)) {
|
|
126
|
-
if (!AUNChannel.AUN_TRACE_RE.test(name))
|
|
127
|
-
continue;
|
|
128
|
-
try {
|
|
129
|
-
const full = path.join(logsDir, name);
|
|
130
|
-
if (fs.statSync(full).mtimeMs < cutoff)
|
|
131
|
-
fs.unlinkSync(full);
|
|
132
|
-
}
|
|
133
|
-
catch { }
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch { }
|
|
137
|
-
}
|
|
138
140
|
/** 判断 channelId 是否为群组 ID
|
|
139
141
|
* - 新格式:group.{issuer}/{group_no|group_name}
|
|
140
142
|
* - 数字群号:{group_no}.{issuer}(如 11117.agentid.pub)
|
|
@@ -166,12 +168,66 @@ export class AUNChannel {
|
|
|
166
168
|
const name = cached?.name;
|
|
167
169
|
return name && name !== short ? `${short}(${name})` : short;
|
|
168
170
|
}
|
|
169
|
-
extractTextPayload(payload) {
|
|
171
|
+
extractTextPayload(payload, channelId) {
|
|
170
172
|
if (typeof payload === 'string')
|
|
171
173
|
return payload;
|
|
172
174
|
if (payload && typeof payload === 'object') {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
+
const obj = payload;
|
|
176
|
+
const text = typeof obj.text === 'string' ? obj.text : '';
|
|
177
|
+
// action_card_reply:卡片交互回复,触发 interactionCallback,不分发给 agent
|
|
178
|
+
if (obj.type === 'action_card_reply') {
|
|
179
|
+
const cardMsgId = typeof obj.card_message_id === 'string' ? obj.card_message_id : '';
|
|
180
|
+
const cardInfo = cardMsgId ? this.cardMessageIdMap.get(cardMsgId) : undefined;
|
|
181
|
+
if (cardInfo) {
|
|
182
|
+
const actionValue = typeof obj.action_value === 'string' ? obj.action_value : text;
|
|
183
|
+
if (cardInfo.isCommandCard) {
|
|
184
|
+
// CommandCard:action_value 是完整 slash 命令,构造伪入站消息
|
|
185
|
+
this.cardMessageIdMap.delete(cardMsgId);
|
|
186
|
+
if (this.messageHandler && actionValue.startsWith('/')) {
|
|
187
|
+
const chatType = channelId ? (this.isGroupId(channelId) ? 'group' : 'private') : 'private';
|
|
188
|
+
this.messageHandler({
|
|
189
|
+
channelId: channelId || '',
|
|
190
|
+
chatType,
|
|
191
|
+
content: actionValue,
|
|
192
|
+
peerId: channelId || '',
|
|
193
|
+
peerName: typeof obj.action_label === 'string' ? obj.action_label : undefined,
|
|
194
|
+
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
195
|
+
source: 'card-trigger',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// ActionInteraction:走 interactionCallback → InteractionRouter
|
|
201
|
+
// callback 未注册时保留 map entry(TTL 清理),给 router 留重试机会
|
|
202
|
+
if (this.interactionCallback) {
|
|
203
|
+
this.cardMessageIdMap.delete(cardMsgId);
|
|
204
|
+
this.interactionCallback({
|
|
205
|
+
type: 'interaction.response',
|
|
206
|
+
id: cardInfo.requestId,
|
|
207
|
+
action: actionValue,
|
|
208
|
+
values: { text, action_label: obj.action_label, behavior: obj.behavior },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
logger.debug(`${this.logPrefix()} action_card_reply dropped: cardMsgId=${cardMsgId} hasCallback=${!!this.interactionCallback}`);
|
|
215
|
+
}
|
|
216
|
+
// 始终返回空字符串,阻止消息分发给 agent
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
// quote 类型:拼接被引用内容
|
|
220
|
+
if (obj.type === 'quote' && obj.quote && typeof obj.quote === 'object') {
|
|
221
|
+
const q = obj.quote;
|
|
222
|
+
const quotedText = typeof q.text === 'string' ? q.text : '';
|
|
223
|
+
if (quotedText) {
|
|
224
|
+
const sender = typeof q.sender_display === 'string' ? q.sender_display : '';
|
|
225
|
+
const prefix = sender ? `${sender}: ` : '';
|
|
226
|
+
const quoted = quotedText.split('\n').map(line => `> ${prefix}${line}`).join('\n');
|
|
227
|
+
return text ? `${quoted}\n\n${text}` : quoted;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (typeof obj.text === 'string')
|
|
175
231
|
return text;
|
|
176
232
|
return JSON.stringify(payload);
|
|
177
233
|
}
|
|
@@ -263,35 +319,96 @@ export class AUNChannel {
|
|
|
263
319
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
264
320
|
plaintextRecv = 0;
|
|
265
321
|
sessionModeResolver;
|
|
322
|
+
interactionCallback;
|
|
323
|
+
// action_card message_id → { requestId, isCommandCard }(用于关联 action_card_reply)
|
|
324
|
+
cardMessageIdMap = new Map();
|
|
325
|
+
dispatchModeResolver;
|
|
266
326
|
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
267
327
|
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
268
328
|
'merge', 'link', 'location', 'personal_card',
|
|
269
329
|
]);
|
|
270
|
-
// Reconnect state
|
|
330
|
+
// Reconnect state
|
|
331
|
+
// SDK 自己跑无限指数退避(1s → 5min);TS 层只在 SDK 够不到的两类场景下接管:
|
|
332
|
+
// 1. flap:短命 connected 反复出现(SDK 不记忆跨轮 base delay,会从 1s 重新开始)
|
|
333
|
+
// 2. terminal_failed with kick reason:直接 5min 后重试,不刷屏
|
|
271
334
|
intentionalDisconnect = false;
|
|
272
|
-
reconnectAttempt = 0;
|
|
273
335
|
reconnectTimer = null;
|
|
274
|
-
|
|
336
|
+
reconnectGeneration = 0; // 防止并发 initClient:每次 takeoverReconnect 递增,回调中校验
|
|
275
337
|
onChannelDown;
|
|
276
|
-
//
|
|
338
|
+
// initClient concurrency guard: 防止多个 initClient 并发执行
|
|
339
|
+
initInProgress = false;
|
|
340
|
+
// Flap detection: 连接寿命 < FLAP_WINDOW_MS 累计 FLAP_THRESHOLD 次,判定为持续被踢
|
|
341
|
+
connectedAt = 0;
|
|
342
|
+
flapCount = 0;
|
|
343
|
+
static FLAP_WINDOW_MS = 30_000;
|
|
344
|
+
static FLAP_THRESHOLD = 3;
|
|
345
|
+
// 接管后的统一退避时间(kicked / flap / terminal_failed)
|
|
346
|
+
static TAKEOVER_DELAY_MS = 5 * 60 * 1000; // 5min
|
|
347
|
+
// 一般 terminal_failed(非 kick)的兜底退避
|
|
348
|
+
static FALLBACK_DELAY_MS = 60 * 1000; // 1min
|
|
349
|
+
// SDK reconnect logging throttle(避免 attempt 自增刷屏)
|
|
277
350
|
lastReconnectLogTime = 0;
|
|
278
351
|
lastReconnectLogAttempt = 0;
|
|
279
|
-
static RECONNECT_LOG_INTERVAL = 60_000;
|
|
280
|
-
static RECONNECT_LOG_STEP = 100;
|
|
281
|
-
|
|
352
|
+
static RECONNECT_LOG_INTERVAL = 60_000;
|
|
353
|
+
static RECONNECT_LOG_STEP = 100;
|
|
354
|
+
// AID 连接状态(供 status 命令聚合展示)
|
|
355
|
+
aidState;
|
|
356
|
+
aidStatsCollector;
|
|
282
357
|
constructor(config) {
|
|
283
358
|
this.config = config;
|
|
284
359
|
if (config.aunTrace) {
|
|
285
|
-
this.
|
|
286
|
-
|
|
287
|
-
|
|
360
|
+
this.traceWriter = new LogWriter({
|
|
361
|
+
baseName: 'aun',
|
|
362
|
+
logDir: resolvePaths().logs,
|
|
363
|
+
rotation: 'hourly',
|
|
364
|
+
retention: { hours: 12 },
|
|
365
|
+
});
|
|
366
|
+
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 12h retention): ${this.traceWriter.activePath()}`);
|
|
367
|
+
}
|
|
368
|
+
this.aidState = {
|
|
369
|
+
aid: config.aid,
|
|
370
|
+
agentName: config.agentName ?? '<unknown>',
|
|
371
|
+
channelName: config.channelName ?? 'aun',
|
|
372
|
+
status: 'disabled',
|
|
373
|
+
reconnectCount: 0,
|
|
374
|
+
flapCount: 0,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** Snapshot of AID connection state for status / IPC aggregation */
|
|
378
|
+
getAidState() {
|
|
379
|
+
return { ...this.aidState, flapCount: this.flapCount };
|
|
380
|
+
}
|
|
381
|
+
setAidStatsCollector(collector) {
|
|
382
|
+
this.aidStatsCollector = collector;
|
|
383
|
+
}
|
|
384
|
+
setAidStatus(status, extra) {
|
|
385
|
+
this.aidState.status = status;
|
|
386
|
+
if (extra)
|
|
387
|
+
Object.assign(this.aidState, extra);
|
|
288
388
|
}
|
|
289
389
|
async connect() {
|
|
290
390
|
this.intentionalDisconnect = false;
|
|
291
|
-
this.
|
|
391
|
+
this.flapCount = 0;
|
|
392
|
+
this.connectedAt = 0;
|
|
393
|
+
this.setAidStatus('reconnecting', { lastAttemptAt: Date.now() });
|
|
292
394
|
await this.initClient();
|
|
395
|
+
this.startOutboxTimer();
|
|
293
396
|
}
|
|
294
397
|
async initClient() {
|
|
398
|
+
// 防止并发 initClient(sendMessage 触发 + timer 触发同时进入)
|
|
399
|
+
if (this.initInProgress) {
|
|
400
|
+
logger.info(`${this.logPrefix()} initClient already in progress, skipping`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.initInProgress = true;
|
|
404
|
+
try {
|
|
405
|
+
await this._initClientInner();
|
|
406
|
+
}
|
|
407
|
+
finally {
|
|
408
|
+
this.initInProgress = false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async _initClientInner() {
|
|
295
412
|
// Clean up existing client if any
|
|
296
413
|
if (this.client) {
|
|
297
414
|
this.trace('OUT', 'client.close', { reason: 'initClient' });
|
|
@@ -356,6 +473,11 @@ export class AUNChannel {
|
|
|
356
473
|
// trace is handled inside handleConnectionState with throttling
|
|
357
474
|
this.handleConnectionState(data);
|
|
358
475
|
});
|
|
476
|
+
// gateway 被踢/服务端主动断开(含同槽位互踢的 self/new extra_info)
|
|
477
|
+
this.client.on('gateway.disconnect', (data) => {
|
|
478
|
+
this.trace('IN', 'gateway.disconnect', data);
|
|
479
|
+
this.handleGatewayDisconnect(data);
|
|
480
|
+
});
|
|
359
481
|
this.client.on('message.recalled', (data) => {
|
|
360
482
|
this.trace('IN', 'message.recalled', data);
|
|
361
483
|
if (data && typeof data === 'object') {
|
|
@@ -416,6 +538,7 @@ export class AUNChannel {
|
|
|
416
538
|
accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
|
|
417
539
|
if (!accessToken) {
|
|
418
540
|
logger.error(`${this.logPrefix()} No accessToken fallback available, scheduling retry`);
|
|
541
|
+
this.setAidStatus('failed', { lastError: `${errName}: ${errMsg}`.slice(0, 80) });
|
|
419
542
|
this.scheduleReconnect();
|
|
420
543
|
throw new Error('Authentication failed and no accessToken fallback available');
|
|
421
544
|
}
|
|
@@ -423,15 +546,26 @@ export class AUNChannel {
|
|
|
423
546
|
}
|
|
424
547
|
// Connect (SDK auto_reconnect handles transient failures)
|
|
425
548
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
549
|
+
const extraInfo = buildConnectExtraInfo({
|
|
550
|
+
aid: this.config.aid,
|
|
551
|
+
agentName: this.config.agentName,
|
|
552
|
+
channelName: this.config.channelName,
|
|
553
|
+
});
|
|
554
|
+
this.trace('OUT', 'client.connect', { gateway: this.client._gatewayUrl, extra_info: extraInfo });
|
|
555
|
+
await this.client.connect({ access_token: accessToken, gateway: this.client._gatewayUrl, extra_info: extraInfo },
|
|
556
|
+
// max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
|
|
557
|
+
// initial_delay=1s,max_delay=300s(5min 封顶)
|
|
558
|
+
{ auto_reconnect: true, retry: { max_attempts: 0, initial_delay: 1.0, max_delay: 300.0 } });
|
|
428
559
|
this.trace('OUT', 'client.connect.ok', { aid: this.client.aid });
|
|
429
560
|
this._aid = this.client.aid ?? undefined;
|
|
430
561
|
const deviceId = this.client._device_id ?? '';
|
|
431
562
|
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
432
563
|
this._selfName = this.loadSelfName(aidName);
|
|
564
|
+
if (this._selfName && this.aidStatsCollector)
|
|
565
|
+
this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
|
|
433
566
|
this.connected = true;
|
|
434
|
-
this.
|
|
567
|
+
this.connectedAt = Date.now();
|
|
568
|
+
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.client?._gatewayUrl });
|
|
435
569
|
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
436
570
|
// if cert is missing, it falls back to public key SPKI fingerprint which
|
|
437
571
|
// causes peer cert lookup failures. Backfill from keystore if needed.
|
|
@@ -444,43 +578,43 @@ export class AUNChannel {
|
|
|
444
578
|
}
|
|
445
579
|
}
|
|
446
580
|
logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
|
|
581
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.client._gatewayUrl });
|
|
447
582
|
// Send welcome message to owner after first connection
|
|
448
583
|
await this.sendWelcomeMessage();
|
|
449
584
|
}
|
|
450
585
|
catch (e) {
|
|
451
586
|
this.trace('OUT', 'client.connect.error', { error: String(e) });
|
|
452
587
|
logger.error(`${this.logPrefix()} Connection failed: ${e}`);
|
|
588
|
+
this.setAidStatus('failed', { lastError: String(e).slice(0, 80) });
|
|
453
589
|
this.scheduleReconnect();
|
|
454
590
|
throw e;
|
|
455
591
|
}
|
|
456
592
|
}
|
|
457
593
|
async sendWelcomeMessage() {
|
|
458
594
|
try {
|
|
459
|
-
const owner = this.config.owner;
|
|
460
|
-
if (!owner) {
|
|
461
|
-
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message`);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
// Check agent.md initialized field
|
|
465
595
|
const aid = this.config.aid;
|
|
466
596
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
597
|
+
// Read initialized + owners from per-agent config.json
|
|
598
|
+
// (config.json 是 owner 的真相来源——auto-bind 后会更新这里,但 this.config 是
|
|
599
|
+
// channel 启动时的快照,不会自动同步)
|
|
600
|
+
const agentConfig = loadAgent(aidName);
|
|
601
|
+
if (!agentConfig) {
|
|
602
|
+
logger.warn(`${this.logPrefix()} agent config not found for ${aidName}, skipping welcome message`);
|
|
470
603
|
return;
|
|
471
604
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (!match) {
|
|
475
|
-
logger.warn(`${this.logPrefix()} agent.md frontmatter not found`);
|
|
605
|
+
if (agentConfig.initialized === true) {
|
|
606
|
+
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
476
607
|
return;
|
|
477
608
|
}
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
609
|
+
const owner = agentConfig.owners?.[0] ?? this.config.owner;
|
|
610
|
+
if (!owner) {
|
|
611
|
+
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
|
|
482
612
|
return;
|
|
483
613
|
}
|
|
614
|
+
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
615
|
+
const existingAgentMd = fs.existsSync(agentMdPath) ? fs.readFileSync(agentMdPath, 'utf-8') : '';
|
|
616
|
+
const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
|
|
617
|
+
const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
|
|
484
618
|
// Fetch owner's agent.md to derive name and validate type
|
|
485
619
|
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
486
620
|
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
@@ -489,26 +623,18 @@ export class AUNChannel {
|
|
|
489
623
|
// Name: prefer existing agent.md name if user has customized it,
|
|
490
624
|
// otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
|
|
491
625
|
const ownerAidClean = owner.startsWith('@') ? owner.slice(1) : owner;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
ownerDisplayName = ownerInfo.name.slice(0, 12);
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
ownerDisplayName = ownerAidClean.split('.')[0].slice(0, 12);
|
|
498
|
-
}
|
|
499
|
-
// Check if init wrote a meaningful name (vs just the aid first label default)
|
|
500
|
-
const currentNameMatch = frontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
626
|
+
const ownerDisplayName = (ownerInfo.name || ownerAidClean.split('.')[0]).slice(0, 12);
|
|
627
|
+
const currentNameMatch = existingFrontmatter.match(/^name:\s*"?([^"\n]+)/m);
|
|
501
628
|
const currentName = currentNameMatch?.[1]?.trim();
|
|
502
629
|
const aidLabel = aidName.split('.')[0];
|
|
503
630
|
let agentDisplayName;
|
|
504
631
|
if (currentName && currentName !== aidLabel) {
|
|
505
|
-
// User or previous init set a custom name — keep it
|
|
506
632
|
agentDisplayName = currentName;
|
|
507
633
|
}
|
|
508
634
|
else {
|
|
509
635
|
agentDisplayName = `${ownerDisplayName}的Evol助手 (${aidLabel})`;
|
|
510
636
|
}
|
|
511
|
-
// Generate new agent.md
|
|
637
|
+
// Generate new agent.md (no `initialized` frontmatter — that's now in config.json)
|
|
512
638
|
const newAgentMd = `---
|
|
513
639
|
aid: "${aid}"
|
|
514
640
|
name: "${agentDisplayName}"
|
|
@@ -519,7 +645,6 @@ tags:
|
|
|
519
645
|
- evolclaw
|
|
520
646
|
- ai-agent
|
|
521
647
|
- gateway
|
|
522
|
-
initialized: true
|
|
523
648
|
---
|
|
524
649
|
|
|
525
650
|
# ${agentDisplayName}
|
|
@@ -527,8 +652,9 @@ initialized: true
|
|
|
527
652
|
EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
528
653
|
`;
|
|
529
654
|
// Write locally
|
|
655
|
+
fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
|
|
530
656
|
fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
|
|
531
|
-
logger.info(`${this.logPrefix()} Updated agent.md
|
|
657
|
+
logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
|
|
532
658
|
// Publish to AUN network via auth.uploadAgentMd
|
|
533
659
|
try {
|
|
534
660
|
await this.client.auth.uploadAgentMd(newAgentMd);
|
|
@@ -573,6 +699,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
573
699
|
persist_required: true,
|
|
574
700
|
});
|
|
575
701
|
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
702
|
+
// Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
|
|
703
|
+
try {
|
|
704
|
+
const fresh = loadAgent(aidName);
|
|
705
|
+
if (fresh) {
|
|
706
|
+
fresh.initialized = true;
|
|
707
|
+
saveAgent(fresh);
|
|
708
|
+
logger.info(`${this.logPrefix()} Marked ${aidName} as initialized in config.json`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
logger.warn(`${this.logPrefix()} Failed to update initialized flag in config.json: ${e}`);
|
|
713
|
+
}
|
|
576
714
|
}
|
|
577
715
|
catch (e) {
|
|
578
716
|
logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
|
|
@@ -643,7 +781,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
643
781
|
const msg = data;
|
|
644
782
|
const fromAid = msg.from ?? '';
|
|
645
783
|
const payload = msg.payload ?? '';
|
|
646
|
-
const text = this.extractTextPayload(payload);
|
|
784
|
+
const text = this.extractTextPayload(payload, fromAid);
|
|
647
785
|
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
648
786
|
const messageId = msg.message_id ?? '';
|
|
649
787
|
const seq = msg.seq;
|
|
@@ -687,17 +825,22 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
687
825
|
finalText = parts.join('\n\n');
|
|
688
826
|
}
|
|
689
827
|
}
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
: fromAid;
|
|
828
|
+
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
829
|
+
// device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
|
|
830
|
+
const chatId = fromAid;
|
|
694
831
|
const peerInfo = await this.fetchPeerInfo(fromAid);
|
|
695
832
|
const shortAid = this.getShortAid(fromAid);
|
|
696
833
|
const displayName = peerInfo.name || shortAid;
|
|
697
834
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
698
835
|
const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
699
836
|
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))}`);
|
|
837
|
+
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
838
|
+
if (p2pPayloadType === 'action_card_reply')
|
|
839
|
+
return;
|
|
700
840
|
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
|
|
841
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
842
|
+
const isSystemP2P = p2pPayloadType === 'event';
|
|
843
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P);
|
|
701
844
|
const replyContext = { metadata: { encrypted: msgEncrypted } };
|
|
702
845
|
if (taskId)
|
|
703
846
|
replyContext.threadId = taskId;
|
|
@@ -722,7 +865,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
722
865
|
const groupId = msg.group_id ?? '';
|
|
723
866
|
const senderAid = msg.sender_aid ?? '';
|
|
724
867
|
const payload = msg.payload ?? '';
|
|
725
|
-
const text = this.extractTextPayload(payload);
|
|
868
|
+
const text = this.extractTextPayload(payload, groupId);
|
|
726
869
|
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
727
870
|
const messageId = msg.message_id ?? '';
|
|
728
871
|
const seq = msg.seq;
|
|
@@ -741,7 +884,37 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
741
884
|
logger.debug(`${this.logPrefix()} Group dropped: own message (group=${groupId} mid=${messageId})`);
|
|
742
885
|
return;
|
|
743
886
|
}
|
|
744
|
-
//
|
|
887
|
+
// 短 echo 快速通道:连通性测试要尽量低延迟,命中后绕过所有 await(sessionModeResolver / 后续 mention 过滤)
|
|
888
|
+
{
|
|
889
|
+
const firstLineFast = text.split('\n')[0] || '';
|
|
890
|
+
const hasEvolClawTrace = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
891
|
+
if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
|
|
892
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
893
|
+
const msgEncryptedFast = !!(msg.e2ee);
|
|
894
|
+
const peerInfo = this.peerInfoCached(senderAid);
|
|
895
|
+
const shortAid = this.getShortAid(senderAid);
|
|
896
|
+
const displayName = peerInfo?.name || shortAid;
|
|
897
|
+
const createdAt = data.created_at;
|
|
898
|
+
if (!peerInfo)
|
|
899
|
+
this.prefetchPeerInfo(senderAid);
|
|
900
|
+
this.handleEcho({
|
|
901
|
+
channelId: groupId,
|
|
902
|
+
userId: senderAid,
|
|
903
|
+
text,
|
|
904
|
+
chatType: 'group',
|
|
905
|
+
messageId,
|
|
906
|
+
peerName: displayName,
|
|
907
|
+
peerType: peerInfo?.type || 'unknown',
|
|
908
|
+
seq,
|
|
909
|
+
replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast),
|
|
910
|
+
createdAt,
|
|
911
|
+
});
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// ── proactive 模式 payload 类型白名单 ──
|
|
916
|
+
// 仅做类型层面的防噪(task.update / status.ping 等信号类消息不进 Agent);
|
|
917
|
+
// mention 过滤统一交给后面的 dispatchMode 一段处理(避免双层语义)。
|
|
745
918
|
if (this.sessionModeResolver) {
|
|
746
919
|
const sessionMode = await this.sessionModeResolver(groupId).catch(() => undefined);
|
|
747
920
|
if (sessionMode === 'proactive') {
|
|
@@ -752,36 +925,58 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
752
925
|
logger.info(`${this.logPrefix()} Group dropped (proactive deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
753
926
|
return;
|
|
754
927
|
}
|
|
755
|
-
const rawText = typeof payloadObj?.text === 'string' ? payloadObj.text : '';
|
|
756
|
-
const rawMentions = Array.isArray(payloadObj?.mentions) ? payloadObj.mentions : [];
|
|
757
|
-
const mentionAids = this.extractMentionAids(rawMentions);
|
|
758
|
-
const mentionsSelf = !!this._aid && (this.hasExplicitMention(rawText, this._aid) || mentionAids.includes(this._aid));
|
|
759
|
-
// @all 仅认结构化 mentions(payload.mentions),不扫描正文 — 避免引述性 "@all" 误判
|
|
760
|
-
const mentionsAll = this.hasMentionAll(rawMentions);
|
|
761
|
-
if (!mentionsSelf && !mentionsAll) {
|
|
762
|
-
this.acknowledgeImmediately(messageId, seq);
|
|
763
|
-
logger.info(`${this.logPrefix()} Group dropped (proactive whitelist): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId} textPreview=${JSON.stringify(rawText.slice(0, 80))}`);
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
928
|
}
|
|
767
929
|
}
|
|
768
930
|
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
769
931
|
const msgEncrypted = !!(msg.e2ee);
|
|
770
932
|
if (!msgEncrypted)
|
|
771
933
|
this.plaintextRecv++;
|
|
772
|
-
// dispatch_mode
|
|
773
|
-
const
|
|
934
|
+
// dispatch_mode: 本地设置优先,fallback 到服务器参数
|
|
935
|
+
const serverDispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
|
|
936
|
+
const localDispatchMode = this.dispatchModeResolver
|
|
937
|
+
? await this.dispatchModeResolver(groupId).catch(() => undefined)
|
|
938
|
+
: undefined;
|
|
939
|
+
const dispatchMode = localDispatchMode || serverDispatchMode;
|
|
774
940
|
const mentionedSelf = this._aid
|
|
775
941
|
? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
|
|
776
942
|
: false;
|
|
777
943
|
// @all 仅认结构化 mentions(payload.mentions),不扫描正文 — 避免引述性 "@all" 误判
|
|
778
944
|
const mentionedAll = payloadMentions.includes('all');
|
|
779
|
-
//
|
|
780
|
-
|
|
945
|
+
// Echo 机制优先于 mention 过滤:消息第一行包含 echo 时触发
|
|
946
|
+
// 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
947
|
+
const firstLineGroup = text.split('\n')[0] || '';
|
|
948
|
+
const hasEvolClawTraceGroup = /\[EvolClaw\.(receive|reply|agent)\]/.test(text);
|
|
949
|
+
if (/echo/i.test(firstLineGroup) && !hasEvolClawTraceGroup) {
|
|
950
|
+
// 短 echo(≤10 字符)已在前面的快速通道命中并 return,这里只处理长 echo
|
|
951
|
+
// >10 字符:追加 trace,存 pending echo,跳过 mention 过滤继续走 Agent 流程
|
|
952
|
+
const echoTs = () => {
|
|
953
|
+
const d = new Date();
|
|
954
|
+
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')}`;
|
|
955
|
+
};
|
|
956
|
+
let echoText = text;
|
|
957
|
+
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'}`;
|
|
958
|
+
this.pendingEchoMessages.set(messageId, {
|
|
959
|
+
text: echoText,
|
|
960
|
+
channelId: groupId,
|
|
961
|
+
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted),
|
|
962
|
+
receiveTs: Date.now(),
|
|
963
|
+
});
|
|
964
|
+
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
965
|
+
}
|
|
966
|
+
else if (/echo/i.test(firstLineGroup) && hasEvolClawTraceGroup) {
|
|
967
|
+
// 回声炸弹:已被 trace 过的 echo,直接丢弃
|
|
781
968
|
this.acknowledgeImmediately(messageId, seq);
|
|
782
|
-
logger.info(`${this.logPrefix()} Group dropped:
|
|
969
|
+
logger.info(`${this.logPrefix()} Group dropped: echo bomb (already-traced group=${groupId} sender=${senderAid} mid=${messageId})`);
|
|
783
970
|
return;
|
|
784
971
|
}
|
|
972
|
+
else {
|
|
973
|
+
// 非 echo 消息:正常 mention 过滤
|
|
974
|
+
if (dispatchMode === 'mention' && !mentionedSelf && !mentionedAll) {
|
|
975
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
976
|
+
logger.info(`${this.logPrefix()} Group dropped: unmentioned in mention-mode (group=${groupId} sender=${senderAid} mid=${messageId} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
785
980
|
const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
|
|
786
981
|
// Detect attachments before the empty-text guard
|
|
787
982
|
const rawAttachments = Array.isArray(payload?.attachments)
|
|
@@ -832,9 +1027,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
832
1027
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
833
1028
|
: `${dispatchMode}.no-mention`;
|
|
834
1029
|
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))}`);
|
|
1030
|
+
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1031
|
+
if (payloadType === 'action_card_reply')
|
|
1032
|
+
return;
|
|
835
1033
|
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} text=${finalText.slice(0, 60)}`);
|
|
1034
|
+
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 });
|
|
1035
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event');
|
|
836
1036
|
this.dispatchMessage({
|
|
837
1037
|
channelId: groupId,
|
|
1038
|
+
groupId,
|
|
838
1039
|
userId: senderAid,
|
|
839
1040
|
peerName: displayName || undefined,
|
|
840
1041
|
peerType: peerInfo.type || 'unknown',
|
|
@@ -859,6 +1060,35 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
859
1060
|
this.messageSeqMap.set(event.messageId, event.seq);
|
|
860
1061
|
}
|
|
861
1062
|
}
|
|
1063
|
+
// Echo 机制:消息第一行包含 "echo"(不区分大小写)且原始内容 ≤10 字符时,直接回声
|
|
1064
|
+
// 包含 [EvolClaw.xxx] trace 说明已被本系统处理过,是回声的回声,丢弃防止链式爆炸
|
|
1065
|
+
const firstLine = event.text.split('\n')[0] || '';
|
|
1066
|
+
const hasEvolClawTracePrivate = /\[EvolClaw\.(receive|reply|agent)\]/.test(event.text);
|
|
1067
|
+
if (/echo/i.test(firstLine) && firstLine.trim().length <= 10 && !hasEvolClawTracePrivate) {
|
|
1068
|
+
this.handleEcho(event);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
// 回声炸弹:已被本系统 trace 过的 echo,直接丢弃
|
|
1072
|
+
if (/echo/i.test(firstLine) && hasEvolClawTracePrivate) {
|
|
1073
|
+
logger.info(`${this.logPrefix()} Dropped: echo bomb (already-traced mid=${event.messageId} chat=${event.chatType})`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
// 长 echo(>10 字符):存 pending,继续交给 agent 处理
|
|
1077
|
+
if (/echo/i.test(firstLine) && firstLine.trim().length > 10 && !hasEvolClawTracePrivate) {
|
|
1078
|
+
const echoTs = () => {
|
|
1079
|
+
const d = new Date();
|
|
1080
|
+
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')}`;
|
|
1081
|
+
};
|
|
1082
|
+
let echoText = event.text;
|
|
1083
|
+
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'}`;
|
|
1084
|
+
this.pendingEchoMessages.set(event.messageId, {
|
|
1085
|
+
text: echoText,
|
|
1086
|
+
channelId: event.channelId,
|
|
1087
|
+
context: event.replyContext ? { metadata: event.replyContext.metadata } : undefined,
|
|
1088
|
+
receiveTs: Date.now(),
|
|
1089
|
+
});
|
|
1090
|
+
logger.info(`${this.logPrefix()} [Echo] long echo stored: mid=${event.messageId} channelId=${event.channelId}`);
|
|
1091
|
+
}
|
|
862
1092
|
if (!this.messageHandler)
|
|
863
1093
|
return;
|
|
864
1094
|
const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
|
|
@@ -870,7 +1100,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
870
1100
|
}
|
|
871
1101
|
this.messageHandler({
|
|
872
1102
|
channelId: event.channelId || '',
|
|
1103
|
+
channelType: 'aun',
|
|
873
1104
|
content: event.text || '',
|
|
1105
|
+
selfId: this._aid,
|
|
1106
|
+
groupId: event.groupId,
|
|
874
1107
|
chatType: event.chatType,
|
|
875
1108
|
peerId: event.userId || event.channelId || '',
|
|
876
1109
|
peerName: event.peerName,
|
|
@@ -883,62 +1116,257 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
883
1116
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
884
1117
|
});
|
|
885
1118
|
}
|
|
1119
|
+
handleEcho(event) {
|
|
1120
|
+
const ts = () => {
|
|
1121
|
+
const d = new Date();
|
|
1122
|
+
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')}`;
|
|
1123
|
+
};
|
|
1124
|
+
// 在收到的消息文本末尾追加 trace 行(只追加,不修改原文)
|
|
1125
|
+
let echoText = event.text;
|
|
1126
|
+
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'}`;
|
|
1127
|
+
echoText += `\n${ts()} [EvolClaw.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1128
|
+
if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
|
|
1129
|
+
echoText += `\n[TRUNCATED]`;
|
|
1130
|
+
}
|
|
1131
|
+
const replyTarget = event.channelId;
|
|
1132
|
+
// 不传 peerId,避免 sendMessage 在头部追加 @peer
|
|
1133
|
+
const context = { metadata: event.replyContext?.metadata };
|
|
1134
|
+
const sendStart = Date.now();
|
|
1135
|
+
this.isEchoSending = true;
|
|
1136
|
+
this.sendMessage(replyTarget, echoText, context).then(() => {
|
|
1137
|
+
this.isEchoSending = false;
|
|
1138
|
+
const elapsed = Date.now() - sendStart;
|
|
1139
|
+
logger.info(`${this.logPrefix()} Echo reply sent to ${replyTarget} (${elapsed}ms)`);
|
|
1140
|
+
}).catch(e => {
|
|
1141
|
+
this.isEchoSending = false;
|
|
1142
|
+
logger.error(`${this.logPrefix()} Echo reply failed: ${e}`);
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* 处理 SDK 'gateway.disconnect' 事件 — 服务端主动断开(含同槽位互踢)。
|
|
1147
|
+
*
|
|
1148
|
+
* 当另一个进程用同 AID + 同 slot 抢连接时,老连接会被踢,detail 字段含:
|
|
1149
|
+
* - self_extra_info: 本进程当时 connect 上报的内容(可证明"是我自己被踢了")
|
|
1150
|
+
* - new_extra_info: 挤掉本连接的新进程上报的内容(指出"谁踢的我")
|
|
1151
|
+
*
|
|
1152
|
+
* 把这些信息打到 logger.warn,便于通过 evolclaw watch / 日志查看。
|
|
1153
|
+
*/
|
|
1154
|
+
handleGatewayDisconnect(data) {
|
|
1155
|
+
if (!data || typeof data !== 'object')
|
|
1156
|
+
return;
|
|
1157
|
+
const d = data;
|
|
1158
|
+
const code = d.code;
|
|
1159
|
+
const reason = d.reason ?? '';
|
|
1160
|
+
const detail = (d.detail && typeof d.detail === 'object' && !Array.isArray(d.detail)) ? d.detail : {};
|
|
1161
|
+
const selfExtra = detail.self_extra_info;
|
|
1162
|
+
const newExtra = detail.new_extra_info;
|
|
1163
|
+
const fmtExtra = (e) => {
|
|
1164
|
+
if (!e || typeof e !== 'object')
|
|
1165
|
+
return '<none>';
|
|
1166
|
+
const obj = e;
|
|
1167
|
+
const parts = [];
|
|
1168
|
+
const keys = ['app', 'version', 'pid', 'started_at_iso', 'hostname', 'launched_by', 'evolclaw_home', 'agent_name', 'channel_name'];
|
|
1169
|
+
for (const k of keys) {
|
|
1170
|
+
if (obj[k] !== undefined && obj[k] !== '')
|
|
1171
|
+
parts.push(`${k}=${String(obj[k])}`);
|
|
1172
|
+
}
|
|
1173
|
+
return parts.length ? parts.join(' ') : JSON.stringify(obj);
|
|
1174
|
+
};
|
|
1175
|
+
if (selfExtra || newExtra) {
|
|
1176
|
+
logger.warn(`${this.logPrefix()} 🥊 Kicked by another connection (code=${code} reason=${reason})`);
|
|
1177
|
+
logger.warn(`${this.logPrefix()} ↳ me : ${fmtExtra(selfExtra)}`);
|
|
1178
|
+
logger.warn(`${this.logPrefix()} ↳ them: ${fmtExtra(newExtra)}`);
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
logger.warn(`${this.logPrefix()} Server-initiated disconnect: code=${code} reason=${reason} detail=${JSON.stringify(detail)}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
886
1184
|
handleConnectionState(data) {
|
|
887
1185
|
if (!data || typeof data !== 'object')
|
|
888
1186
|
return;
|
|
889
1187
|
const state = data.state ?? '';
|
|
1188
|
+
// [DIAG-STALE] 记录状态切换瞬间 evolclaw 的 connected 标志和 SDK 的内部 _state,
|
|
1189
|
+
// 用于证明"reconnecting 时 connected 保持 true,导致 sendMessage 误放行"的假设
|
|
1190
|
+
const sdkState = this.client?._state ?? 'no-client';
|
|
1191
|
+
const connectedBefore = this.connected;
|
|
1192
|
+
logger.info(`[AUN][DIAG-STALE] connection.state event: state=${state} attempt=${data.attempt ?? '-'} | connected_before=${connectedBefore} sdk_state=${sdkState}`);
|
|
890
1193
|
if (state === 'connected') {
|
|
891
1194
|
this.connected = true;
|
|
892
|
-
this.
|
|
1195
|
+
this.connectedAt = Date.now();
|
|
893
1196
|
this.lastReconnectLogTime = 0;
|
|
894
1197
|
this.lastReconnectLogAttempt = 0;
|
|
1198
|
+
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.client?._gatewayUrl });
|
|
895
1199
|
this.trace('IN', 'connection.state', data);
|
|
896
1200
|
logger.info(`${this.logPrefix()} Connected`);
|
|
1201
|
+
// 不在这里清 flapCount —— 短命连接一上来就会触发本分支,
|
|
1202
|
+
// 必须等 disconnected 时根据 lifetime 决定是否清零
|
|
1203
|
+
this.drainOutbox();
|
|
897
1204
|
}
|
|
898
|
-
else if (state === 'disconnected') {
|
|
1205
|
+
else if (state === 'disconnected' || state === 'reconnecting') {
|
|
1206
|
+
const wasConnected = this.connected;
|
|
899
1207
|
this.connected = false;
|
|
900
|
-
this.
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
this.
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1208
|
+
this.aidState.reconnectCount++;
|
|
1209
|
+
this.aidState.lastAttemptAt = Date.now();
|
|
1210
|
+
this.setAidStatus('reconnecting');
|
|
1211
|
+
// Flap 检测:仅在从 connected 状态过渡时统计
|
|
1212
|
+
if (wasConnected && this.connectedAt > 0) {
|
|
1213
|
+
const lifetime = Date.now() - this.connectedAt;
|
|
1214
|
+
this.connectedAt = 0;
|
|
1215
|
+
if (lifetime < AUNChannel.FLAP_WINDOW_MS) {
|
|
1216
|
+
this.flapCount++;
|
|
1217
|
+
logger.warn(`${this.logPrefix()} Flap #${this.flapCount}/${AUNChannel.FLAP_THRESHOLD}: connection lived ${lifetime}ms (< ${AUNChannel.FLAP_WINDOW_MS}ms)`);
|
|
1218
|
+
if (this.flapCount >= AUNChannel.FLAP_THRESHOLD && !this.intentionalDisconnect) {
|
|
1219
|
+
logger.error(`${this.logPrefix()} Persistent kick detected (${this.flapCount} flaps), taking over from SDK with ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s backoff`);
|
|
1220
|
+
this.flapCount = 0;
|
|
1221
|
+
this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'flap');
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
// 连接稳定过 ≥ FLAP_WINDOW_MS,重置 flap 计数
|
|
1227
|
+
if (this.flapCount > 0) {
|
|
1228
|
+
logger.info(`${this.logPrefix()} Stable connection (lived ${lifetime}ms), resetting flap counter`);
|
|
1229
|
+
}
|
|
1230
|
+
this.flapCount = 0;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (state === 'disconnected') {
|
|
917
1234
|
this.trace('IN', 'connection.state', data);
|
|
1235
|
+
logger.warn(`${this.logPrefix()} Disconnected: ${data.error ?? 'unknown'}`);
|
|
918
1236
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1237
|
+
else {
|
|
1238
|
+
// reconnecting:节流日志(SDK 自己已经在跑指数退避,不刷屏)
|
|
1239
|
+
const attempt = data.attempt ?? 0;
|
|
1240
|
+
const now = Date.now();
|
|
1241
|
+
const isFirst = attempt <= 1;
|
|
1242
|
+
const isStep = attempt - this.lastReconnectLogAttempt >= AUNChannel.RECONNECT_LOG_STEP;
|
|
1243
|
+
const isInterval = now - this.lastReconnectLogTime >= AUNChannel.RECONNECT_LOG_INTERVAL;
|
|
1244
|
+
if (isFirst || isStep || isInterval) {
|
|
1245
|
+
const suppressed = attempt - this.lastReconnectLogAttempt - 1;
|
|
1246
|
+
const suffix = suppressed > 0 ? `, ${suppressed} suppressed since last log` : '';
|
|
1247
|
+
logger.info(`${this.logPrefix()} SDK reconnecting (attempt ${attempt}${suffix})`);
|
|
1248
|
+
this.lastReconnectLogTime = now;
|
|
1249
|
+
this.lastReconnectLogAttempt = attempt;
|
|
1250
|
+
this.trace('IN', 'connection.state', data);
|
|
927
1251
|
}
|
|
928
|
-
this.scheduleReconnect();
|
|
929
1252
|
}
|
|
930
1253
|
}
|
|
931
1254
|
else if (state === 'terminal_failed') {
|
|
932
1255
|
this.connected = false;
|
|
1256
|
+
this.connectedAt = 0;
|
|
933
1257
|
this.trace('IN', 'connection.state', data);
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1258
|
+
const d = data;
|
|
1259
|
+
const reason = d.reason ?? '';
|
|
1260
|
+
const error = d.error ?? 'unknown';
|
|
1261
|
+
if (this.intentionalDisconnect)
|
|
1262
|
+
return;
|
|
1263
|
+
// 被踢类(server kicked / close code 4001-4011)→ ERROR + 5min 长退避
|
|
1264
|
+
if (this.isKickReason(reason)) {
|
|
1265
|
+
logger.error(`${this.logPrefix()} Kicked by server: ${reason} (${error}), backing off ${AUNChannel.TAKEOVER_DELAY_MS / 1000}s`);
|
|
1266
|
+
this.setAidStatus('kicked', { lastError: `kicked: ${error}`.slice(0, 80) });
|
|
1267
|
+
this.takeoverReconnect(AUNChannel.TAKEOVER_DELAY_MS, 'kicked');
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
// 其他 terminal failure(含 max_attempts_exhausted 兜底)→ 1min 后再试
|
|
1271
|
+
logger.error(`${this.logPrefix()} Terminal failure: ${error}${reason ? ` (${reason})` : ''}, retrying in ${AUNChannel.FALLBACK_DELAY_MS / 1000}s`);
|
|
1272
|
+
this.setAidStatus('failed', { lastError: `${error}`.slice(0, 80) });
|
|
1273
|
+
this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'terminal');
|
|
938
1274
|
}
|
|
939
1275
|
}
|
|
940
1276
|
}
|
|
1277
|
+
/** 判断 terminal_failed 的 reason 是否属于"被踢"类 */
|
|
1278
|
+
isKickReason(reason) {
|
|
1279
|
+
if (!reason)
|
|
1280
|
+
return false;
|
|
1281
|
+
const r = reason.toLowerCase();
|
|
1282
|
+
if (r.includes('kicked') || r.includes('kick'))
|
|
1283
|
+
return true;
|
|
1284
|
+
// close code 4001/4003/4008/4009/4010/4011 都是 SDK _NO_RECONNECT_CODES
|
|
1285
|
+
if (/close code 40(0[13]|0[89]|1[01])/.test(r))
|
|
1286
|
+
return true;
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* TS 层接管重连:force close 当前 SDK client,安排 delayMs 后重新 initClient。
|
|
1291
|
+
* 用于 flap / kicked / terminal_failed 三类场景,统一退避路径。
|
|
1292
|
+
*/
|
|
1293
|
+
takeoverReconnect(delayMs, reason) {
|
|
1294
|
+
if (this.intentionalDisconnect)
|
|
1295
|
+
return;
|
|
1296
|
+
// 递增 generation,使任何正在进行的旧 initClient 回调失效
|
|
1297
|
+
const gen = ++this.reconnectGeneration;
|
|
1298
|
+
// 清掉已有 timer,避免叠加
|
|
1299
|
+
if (this.reconnectTimer) {
|
|
1300
|
+
clearTimeout(this.reconnectTimer);
|
|
1301
|
+
this.reconnectTimer = null;
|
|
1302
|
+
}
|
|
1303
|
+
// Force close SDK client,中断它内部的重连循环
|
|
1304
|
+
if (this.client) {
|
|
1305
|
+
this.trace('OUT', 'client.close', { reason: `takeover_${reason}` });
|
|
1306
|
+
this.client.close().catch(() => { });
|
|
1307
|
+
this.client = null;
|
|
1308
|
+
}
|
|
1309
|
+
this.connected = false;
|
|
1310
|
+
const delaySec = Math.round(delayMs / 1000);
|
|
1311
|
+
logger.info(`${this.logPrefix()} Scheduling TS-layer reconnect (${reason}) in ${delaySec}s`);
|
|
1312
|
+
this.trace('OUT', 'reconnect.scheduled', { reason, delayMs, generation: gen });
|
|
1313
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
1314
|
+
this.reconnectTimer = null;
|
|
1315
|
+
// 如果在等待期间又触发了新的 takeoverReconnect,本次作废
|
|
1316
|
+
if (gen !== this.reconnectGeneration) {
|
|
1317
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) cancelled: generation stale (${gen} vs ${this.reconnectGeneration})`);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) starting...`);
|
|
1322
|
+
this.trace('OUT', 'reconnect.start', { reason, generation: gen });
|
|
1323
|
+
await this.initClient();
|
|
1324
|
+
// initClient 完成后再次校验 generation,防止 initClient 期间被新的 takeover 取代
|
|
1325
|
+
if (gen !== this.reconnectGeneration) {
|
|
1326
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) succeeded but generation stale, closing stale client`);
|
|
1327
|
+
if (this.client) {
|
|
1328
|
+
this.client.close().catch(() => { });
|
|
1329
|
+
this.client = null;
|
|
1330
|
+
}
|
|
1331
|
+
this.connected = false;
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
this.trace('OUT', 'reconnect.ok', { reason, generation: gen });
|
|
1335
|
+
logger.info(`${this.logPrefix()} TS-layer reconnect (${reason}) succeeded`);
|
|
1336
|
+
}
|
|
1337
|
+
catch (err) {
|
|
1338
|
+
this.trace('OUT', 'reconnect.error', { reason, error: String(err), generation: gen });
|
|
1339
|
+
logger.error(`${this.logPrefix()} TS-layer reconnect (${reason}) failed: ${err}`);
|
|
1340
|
+
// initClient 内部已经在失败路径触发 scheduleReconnect 了,这里不重复
|
|
1341
|
+
}
|
|
1342
|
+
}, delayMs);
|
|
1343
|
+
}
|
|
941
1344
|
// ── Public API (same interface as before) ───────────────────
|
|
1345
|
+
setEventBus(bus) {
|
|
1346
|
+
// 重新订阅前先解掉旧的——避免 reload/重连后 listener 累积
|
|
1347
|
+
if (this.eventBus && this.ownerBoundHandler && typeof this.eventBus.unsubscribe === 'function') {
|
|
1348
|
+
this.eventBus.unsubscribe('channel:owner-bound', this.ownerBoundHandler);
|
|
1349
|
+
}
|
|
1350
|
+
this.ownerBoundHandler = null;
|
|
1351
|
+
this.eventBus = bus;
|
|
1352
|
+
if (bus && typeof bus.subscribe === 'function') {
|
|
1353
|
+
const handler = (event) => {
|
|
1354
|
+
if (event.channelName !== this.config.channelName)
|
|
1355
|
+
return;
|
|
1356
|
+
// sendWelcomeMessage 内部读 config.json 中最新的 owners[0],并幂等检查 initialized
|
|
1357
|
+
// 自身做 client 健康检查后再发
|
|
1358
|
+
if (!this.client) {
|
|
1359
|
+
logger.info(`${this.logPrefix()} owner-bound event received but client not connected; skip welcome retry`);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
this.sendWelcomeMessage().catch(e => {
|
|
1363
|
+
logger.warn(`${this.logPrefix()} owner-bound welcome retry failed: ${e}`);
|
|
1364
|
+
});
|
|
1365
|
+
};
|
|
1366
|
+
bus.subscribe('channel:owner-bound', handler);
|
|
1367
|
+
this.ownerBoundHandler = handler;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
942
1370
|
onProjectPathRequest(provider) {
|
|
943
1371
|
this.projectPathProvider = provider;
|
|
944
1372
|
}
|
|
@@ -948,32 +1376,99 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
948
1376
|
setSessionModeResolver(resolver) {
|
|
949
1377
|
this.sessionModeResolver = resolver;
|
|
950
1378
|
}
|
|
1379
|
+
setDispatchModeResolver(resolver) {
|
|
1380
|
+
this.dispatchModeResolver = resolver;
|
|
1381
|
+
}
|
|
951
1382
|
onRecall(handler) {
|
|
952
1383
|
this.recallHandler = handler;
|
|
953
1384
|
}
|
|
954
1385
|
async sendMessage(channelId, text, context) {
|
|
955
|
-
if (!this.connected || !this.client) {
|
|
956
|
-
logger.warn(`${this.logPrefix()} Cannot send: not connected`);
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
1386
|
if (!text?.trim()) {
|
|
960
1387
|
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
961
1388
|
return;
|
|
962
1389
|
}
|
|
1390
|
+
// 长 echo: agent 首次回复前先发 echo trace(echo 自身发送时跳过)
|
|
1391
|
+
if (!this.isEchoSending) {
|
|
1392
|
+
await this.flushPendingEcho(channelId);
|
|
1393
|
+
}
|
|
963
1394
|
let finalText = text;
|
|
964
|
-
// 多轮工具调用后的最终回复:仅在已有中间消息时添加前缀
|
|
965
1395
|
if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
|
|
966
1396
|
finalText = '最终回复\n' + text;
|
|
967
1397
|
}
|
|
968
1398
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
969
|
-
// 群聊 @ 兜底:提示词已告知 agent 要 @,但如果 agent 没写,系统自动补上
|
|
970
1399
|
if (this.isGroupId(channelId) && context?.peerId) {
|
|
971
1400
|
if (!finalText.includes(`@${context.peerId}`)) {
|
|
972
1401
|
finalText = `@${context.peerId} ` + finalText;
|
|
973
1402
|
}
|
|
974
1403
|
}
|
|
1404
|
+
// Write-ahead: persist to outbox before attempting send
|
|
1405
|
+
const entry = outbox.enqueue(this.config.aid, {
|
|
1406
|
+
channelId,
|
|
1407
|
+
type: 'text',
|
|
1408
|
+
text: finalText,
|
|
1409
|
+
context,
|
|
1410
|
+
});
|
|
1411
|
+
logger.debug(`${this.logPrefix()} Outbox enqueued: id=${entry.id} channel=${channelId} text=${finalText.slice(0, 40)}`);
|
|
1412
|
+
if (!this.connected || !this.client) {
|
|
1413
|
+
logger.warn(`${this.logPrefix()} Not connected, message queued in outbox (id=${entry.id}). Triggering reconnect.`);
|
|
1414
|
+
if (!this.reconnectTimer && !this.client) {
|
|
1415
|
+
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendMessage failed: ${e}`));
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
// Attempt immediate delivery
|
|
1420
|
+
const ok = await this.deliverTextEntry(entry);
|
|
1421
|
+
if (ok) {
|
|
1422
|
+
outbox.remove(this.config.aid, entry.id);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async flushPendingEcho(channelId) {
|
|
1426
|
+
// 查找该 channelId 是否有 pending echo(长 echo 等待 agent 首次回复)
|
|
1427
|
+
for (const [key, echo] of this.pendingEchoMessages) {
|
|
1428
|
+
if (echo.channelId === channelId) {
|
|
1429
|
+
this.pendingEchoMessages.delete(key);
|
|
1430
|
+
logger.info(`${this.logPrefix()} [Echo] flushPendingEcho triggered: key=${key} channelId=${channelId} pendingCount=${this.pendingEchoMessages.size}`);
|
|
1431
|
+
const ts = () => {
|
|
1432
|
+
const d = new Date();
|
|
1433
|
+
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')}`;
|
|
1434
|
+
};
|
|
1435
|
+
const agentDuration = Date.now() - echo.receiveTs;
|
|
1436
|
+
let echoText = echo.text;
|
|
1437
|
+
echoText += `\n${ts()} [EvolClaw.agent] duration=${agentDuration}ms`;
|
|
1438
|
+
echoText += `\n${ts()} [EvolClaw.reply] echo回声发出 conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1439
|
+
if (Buffer.byteLength(echoText, 'utf-8') > 4096) {
|
|
1440
|
+
echoText += `\n[TRUNCATED]`;
|
|
1441
|
+
}
|
|
1442
|
+
// 直接投递 echo trace(不经过 sendMessage 避免 @peer 前缀和递归)
|
|
1443
|
+
if (this.connected && this.client) {
|
|
1444
|
+
const echoEntry = {
|
|
1445
|
+
id: `echo-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`,
|
|
1446
|
+
ts: Date.now(),
|
|
1447
|
+
aid: this.config.aid,
|
|
1448
|
+
channelId,
|
|
1449
|
+
type: 'text',
|
|
1450
|
+
text: echoText,
|
|
1451
|
+
ttl: 300_000,
|
|
1452
|
+
};
|
|
1453
|
+
const ok = await this.deliverTextEntry(echoEntry);
|
|
1454
|
+
if (!ok) {
|
|
1455
|
+
outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
|
|
1456
|
+
}
|
|
1457
|
+
logger.info(`${this.logPrefix()} [Echo] long echo trace delivered=${ok} to ${channelId} (agent ${agentDuration}ms)`);
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
outbox.enqueue(this.config.aid, { channelId, type: 'text', text: echoText });
|
|
1461
|
+
logger.warn(`${this.logPrefix()} [Echo] not connected, echo trace queued in outbox`);
|
|
1462
|
+
}
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
async deliverTextEntry(entry) {
|
|
1468
|
+
const channelId = entry.channelId;
|
|
1469
|
+
const finalText = entry.text;
|
|
1470
|
+
const context = entry.context;
|
|
975
1471
|
const payload = { type: 'text', text: finalText };
|
|
976
|
-
// 出站群消息:从正文提取 @aid 填入结构化 mentions(与 aun CLI 对齐,方便对端按 mentions 过滤)
|
|
977
1472
|
if (this.isGroupId(channelId)) {
|
|
978
1473
|
const extracted = this.extractMentionAidsFromText(finalText);
|
|
979
1474
|
if (extracted.length > 0)
|
|
@@ -986,12 +1481,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
986
1481
|
if (context?.metadata?.chatmode)
|
|
987
1482
|
payload.chatmode = context.metadata.chatmode;
|
|
988
1483
|
const isGroup = this.isGroupId(channelId);
|
|
989
|
-
|
|
990
|
-
const colonIdx = channelId.indexOf(':');
|
|
991
|
-
const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
992
|
-
if (colonIdx > 0) {
|
|
993
|
-
payload.chat_id = channelId;
|
|
994
|
-
}
|
|
1484
|
+
const targetAid = channelId;
|
|
995
1485
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
996
1486
|
const encrypt = context?.metadata?.encrypted != null
|
|
997
1487
|
? !!(context.metadata.encrypted)
|
|
@@ -1012,6 +1502,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1012
1502
|
}
|
|
1013
1503
|
else {
|
|
1014
1504
|
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1505
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: result.message_id, kind: 'text', len: finalText.length, groupId: channelId });
|
|
1506
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText);
|
|
1015
1507
|
}
|
|
1016
1508
|
}
|
|
1017
1509
|
else {
|
|
@@ -1022,8 +1514,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1022
1514
|
}
|
|
1023
1515
|
else {
|
|
1024
1516
|
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1517
|
+
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 });
|
|
1518
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText);
|
|
1025
1519
|
}
|
|
1026
1520
|
}
|
|
1521
|
+
return true;
|
|
1027
1522
|
}
|
|
1028
1523
|
catch (e) {
|
|
1029
1524
|
if (encrypt && e instanceof E2EEError) {
|
|
@@ -1047,15 +1542,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1047
1542
|
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
1048
1543
|
}
|
|
1049
1544
|
}
|
|
1545
|
+
return true;
|
|
1050
1546
|
}
|
|
1051
1547
|
catch (e2) {
|
|
1052
1548
|
this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
|
|
1053
1549
|
logger.error(`${this.logPrefix()} Plaintext fallback also failed to ${channelId}: ${e2}`);
|
|
1550
|
+
return false;
|
|
1054
1551
|
}
|
|
1055
1552
|
}
|
|
1056
1553
|
else {
|
|
1057
1554
|
this.trace('OUT', 'send.error', { channelId, error: String(e) });
|
|
1058
|
-
logger.error(`${this.logPrefix()} Send failed to ${channelId}: ${e}`);
|
|
1555
|
+
logger.error(`${this.logPrefix()} Send failed to ${channelId} (outbox id=${entry.id}): ${e}`);
|
|
1556
|
+
return false;
|
|
1059
1557
|
}
|
|
1060
1558
|
}
|
|
1061
1559
|
}
|
|
@@ -1072,9 +1570,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1072
1570
|
return;
|
|
1073
1571
|
if (!taskId)
|
|
1074
1572
|
return;
|
|
1075
|
-
//
|
|
1076
|
-
const
|
|
1077
|
-
const targetId = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
1573
|
+
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
1574
|
+
const targetId = channelId;
|
|
1078
1575
|
const encrypt = context?.metadata?.encrypted != null
|
|
1079
1576
|
? !!(context.metadata.encrypted)
|
|
1080
1577
|
: this.shouldEncrypt(targetId);
|
|
@@ -1084,7 +1581,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1084
1581
|
encrypt,
|
|
1085
1582
|
};
|
|
1086
1583
|
try {
|
|
1087
|
-
const
|
|
1584
|
+
const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
|
|
1585
|
+
const stage = payload?.stage ?? `items=${itemCount}`;
|
|
1088
1586
|
if (this.isGroupId(channelId)) {
|
|
1089
1587
|
params.group_id = targetId;
|
|
1090
1588
|
await this.callAndTrace('group.thought.put', params);
|
|
@@ -1101,11 +1599,46 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1101
1599
|
logger.debug(`${this.logPrefix()} thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
1102
1600
|
}
|
|
1103
1601
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1602
|
+
/**
|
|
1603
|
+
* 发送结构化 payload(type='thought' 等)作为消息历史持久化。
|
|
1604
|
+
* 与 sendThought(thought.put)配对:thought.put 用于前端实时渲染(不入消息历史),
|
|
1605
|
+
* sendStructured 用于把同一内容写入消息历史。
|
|
1606
|
+
* 返回服务端分配的 message_id(失败时返回 null)。
|
|
1607
|
+
*/
|
|
1608
|
+
async sendStructured(channelId, payload, context) {
|
|
1609
|
+
if (!this.connected || !this.client)
|
|
1610
|
+
return null;
|
|
1611
|
+
const isGroup = this.isGroupId(channelId);
|
|
1612
|
+
const targetAid = channelId;
|
|
1613
|
+
const encryptTarget = isGroup ? channelId : targetAid;
|
|
1614
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1615
|
+
? !!(context.metadata.encrypted)
|
|
1616
|
+
: this.shouldEncrypt(encryptTarget);
|
|
1617
|
+
const finalPayload = { ...payload };
|
|
1618
|
+
if (context?.threadId && !finalPayload.thread_id)
|
|
1619
|
+
finalPayload.thread_id = context.threadId;
|
|
1620
|
+
const params = { payload: finalPayload, encrypt };
|
|
1621
|
+
try {
|
|
1622
|
+
if (isGroup) {
|
|
1623
|
+
params.group_id = channelId;
|
|
1624
|
+
const result = await this.callAndTrace('group.send', params);
|
|
1625
|
+
logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${result?.message_id} encrypt=${encrypt}`);
|
|
1626
|
+
return result?.message_id ?? null;
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1629
|
+
params.to = targetAid;
|
|
1630
|
+
const result = await this.callAndTrace('message.send', params);
|
|
1631
|
+
logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
|
|
1632
|
+
return result?.message_id ?? null;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
catch (e) {
|
|
1636
|
+
const err = e;
|
|
1637
|
+
logger.warn(`${this.logPrefix()} sendStructured failed (${payload.type}) to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
1638
|
+
return null;
|
|
1108
1639
|
}
|
|
1640
|
+
}
|
|
1641
|
+
async sendFile(channelId, filePath, context) {
|
|
1109
1642
|
const absPath = path.resolve(filePath);
|
|
1110
1643
|
if (!fs.existsSync(absPath)) {
|
|
1111
1644
|
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
@@ -1120,15 +1653,42 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1120
1653
|
logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
1121
1654
|
return;
|
|
1122
1655
|
}
|
|
1656
|
+
// Write-ahead: persist to outbox
|
|
1657
|
+
const entry = outbox.enqueue(this.config.aid, {
|
|
1658
|
+
channelId,
|
|
1659
|
+
type: 'file',
|
|
1660
|
+
filePath: absPath,
|
|
1661
|
+
context,
|
|
1662
|
+
});
|
|
1663
|
+
logger.debug(`${this.logPrefix()} Outbox enqueued file: id=${entry.id} channel=${channelId} file=${absPath}`);
|
|
1664
|
+
if (!this.connected || !this.client) {
|
|
1665
|
+
logger.warn(`${this.logPrefix()} Not connected, file send queued in outbox (id=${entry.id}). Triggering reconnect.`);
|
|
1666
|
+
if (!this.reconnectTimer && !this.client) {
|
|
1667
|
+
this.initClient().catch(e => logger.error(`${this.logPrefix()} Reconnect from sendFile failed: ${e}`));
|
|
1668
|
+
}
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const ok = await this.deliverFileEntry(entry);
|
|
1672
|
+
if (ok) {
|
|
1673
|
+
outbox.remove(this.config.aid, entry.id);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async deliverFileEntry(entry) {
|
|
1677
|
+
const channelId = entry.channelId;
|
|
1678
|
+
const absPath = entry.filePath;
|
|
1679
|
+
const context = entry.context;
|
|
1680
|
+
if (!fs.existsSync(absPath)) {
|
|
1681
|
+
logger.warn(`${this.logPrefix()} deliverFileEntry: file gone: ${absPath}`);
|
|
1682
|
+
return true; // remove from outbox, file no longer exists
|
|
1683
|
+
}
|
|
1123
1684
|
const filename = path.basename(absPath);
|
|
1124
1685
|
const fileData = fs.readFileSync(absPath);
|
|
1686
|
+
const stat = fs.statSync(absPath);
|
|
1125
1687
|
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
1126
1688
|
const contentType = guessMime(filename);
|
|
1127
1689
|
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
1128
1690
|
try {
|
|
1129
|
-
// Upload to storage
|
|
1130
1691
|
if (stat.size <= 64 * 1024) {
|
|
1131
|
-
// Inline upload for small files (≤64KB)
|
|
1132
1692
|
await this.callAndTrace('storage.put_object', {
|
|
1133
1693
|
object_key: objectKey,
|
|
1134
1694
|
content: fileData.toString('base64'),
|
|
@@ -1138,7 +1698,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1138
1698
|
});
|
|
1139
1699
|
}
|
|
1140
1700
|
else {
|
|
1141
|
-
// Ticket upload for large files
|
|
1142
1701
|
const session = await this.callAndTrace('storage.create_upload_session', {
|
|
1143
1702
|
object_key: objectKey,
|
|
1144
1703
|
size_bytes: stat.size,
|
|
@@ -1160,7 +1719,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1160
1719
|
size_bytes: stat.size,
|
|
1161
1720
|
});
|
|
1162
1721
|
}
|
|
1163
|
-
// Send message with attachment
|
|
1164
1722
|
const attachment = {
|
|
1165
1723
|
owner_aid: this._aid || '',
|
|
1166
1724
|
object_key: objectKey,
|
|
@@ -1181,12 +1739,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1181
1739
|
if (context?.metadata?.chatmode)
|
|
1182
1740
|
filePayload.chatmode = context.metadata.chatmode;
|
|
1183
1741
|
const isGroup = this.isGroupId(channelId);
|
|
1184
|
-
|
|
1185
|
-
const fileColonIdx = channelId.indexOf(':');
|
|
1186
|
-
const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
|
|
1187
|
-
if (fileColonIdx > 0) {
|
|
1188
|
-
filePayload.chat_id = channelId;
|
|
1189
|
-
}
|
|
1742
|
+
const fileTargetAid = channelId;
|
|
1190
1743
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
1191
1744
|
const encrypt = context?.metadata?.encrypted != null
|
|
1192
1745
|
? !!(context.metadata.encrypted)
|
|
@@ -1243,10 +1796,48 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1243
1796
|
}
|
|
1244
1797
|
}
|
|
1245
1798
|
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
1799
|
+
return true;
|
|
1246
1800
|
}
|
|
1247
1801
|
catch (e) {
|
|
1248
|
-
this.trace('OUT', 'sendFile.error', { channelId, filePath, error: String(e) });
|
|
1249
|
-
logger.error(`${this.logPrefix()} sendFile failed for ${channelId}: ${e}`);
|
|
1802
|
+
this.trace('OUT', 'sendFile.error', { channelId, filePath: absPath, error: String(e) });
|
|
1803
|
+
logger.error(`${this.logPrefix()} sendFile failed for ${channelId} (outbox id=${entry.id}): ${e}`);
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
// ── Outbox drain ───────────────────────────────────────────
|
|
1808
|
+
outboxTimer = null;
|
|
1809
|
+
startOutboxTimer() {
|
|
1810
|
+
if (this.outboxTimer)
|
|
1811
|
+
return;
|
|
1812
|
+
this.outboxTimer = setInterval(() => {
|
|
1813
|
+
if (this.connected && this.client && outbox.hasPending(this.config.aid)) {
|
|
1814
|
+
this.drainOutbox();
|
|
1815
|
+
}
|
|
1816
|
+
}, 30_000);
|
|
1817
|
+
}
|
|
1818
|
+
stopOutboxTimer() {
|
|
1819
|
+
if (this.outboxTimer) {
|
|
1820
|
+
clearInterval(this.outboxTimer);
|
|
1821
|
+
this.outboxTimer = null;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
async drainOutbox() {
|
|
1825
|
+
if (!this.connected || !this.client)
|
|
1826
|
+
return;
|
|
1827
|
+
if (!outbox.hasPending(this.config.aid))
|
|
1828
|
+
return;
|
|
1829
|
+
logger.info(`${this.logPrefix()} Draining outbox...`);
|
|
1830
|
+
const result = await outbox.drain(this.config.aid, async (entry) => {
|
|
1831
|
+
if (entry.type === 'text') {
|
|
1832
|
+
return this.deliverTextEntry(entry);
|
|
1833
|
+
}
|
|
1834
|
+
else if (entry.type === 'file') {
|
|
1835
|
+
return this.deliverFileEntry(entry);
|
|
1836
|
+
}
|
|
1837
|
+
return true; // unknown type, discard
|
|
1838
|
+
});
|
|
1839
|
+
if (result.sent > 0 || result.expired > 0) {
|
|
1840
|
+
logger.info(`${this.logPrefix()} Outbox drained: sent=${result.sent} expired=${result.expired} failed=${result.failed}`);
|
|
1250
1841
|
}
|
|
1251
1842
|
}
|
|
1252
1843
|
acknowledge(messageId) {
|
|
@@ -1259,6 +1850,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1259
1850
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
1260
1851
|
if (!this.client || !this.connected)
|
|
1261
1852
|
return;
|
|
1853
|
+
// 旧路 payload(type='event', event='task.*')—— 前后兼容保留,未来废弃
|
|
1262
1854
|
const eventMap = {
|
|
1263
1855
|
start: 'task.started',
|
|
1264
1856
|
done: 'task.completed',
|
|
@@ -1266,55 +1858,81 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1266
1858
|
error: 'task.error',
|
|
1267
1859
|
timeout: 'task.timeout',
|
|
1268
1860
|
};
|
|
1269
|
-
const
|
|
1861
|
+
const severity = status === 'error' || status === 'timeout' ? 'error' : 'info';
|
|
1862
|
+
const eventPayload = {
|
|
1270
1863
|
type: 'event',
|
|
1271
1864
|
event: eventMap[status] ?? `task.${status}`,
|
|
1272
1865
|
data: { task_id: taskId, session_id: sessionId },
|
|
1273
|
-
severity
|
|
1866
|
+
severity,
|
|
1274
1867
|
};
|
|
1275
1868
|
if (context?.threadId)
|
|
1276
|
-
|
|
1869
|
+
eventPayload.thread_id = context.threadId;
|
|
1870
|
+
// 新路 payload(type='status')—— 结构化任务状态,下游直接读字段不用解析 event 字符串
|
|
1871
|
+
const stateMap = {
|
|
1872
|
+
start: 'started',
|
|
1873
|
+
done: 'completed',
|
|
1874
|
+
interrupted: 'interrupted',
|
|
1875
|
+
error: 'error',
|
|
1876
|
+
timeout: 'timeout',
|
|
1877
|
+
};
|
|
1878
|
+
const statusPayload = {
|
|
1879
|
+
type: 'status',
|
|
1880
|
+
state: stateMap[status] ?? status,
|
|
1881
|
+
task_id: taskId,
|
|
1882
|
+
session_id: sessionId,
|
|
1883
|
+
severity,
|
|
1884
|
+
};
|
|
1885
|
+
if (context?.threadId)
|
|
1886
|
+
statusPayload.thread_id = context.threadId;
|
|
1277
1887
|
const isGroup = this.isGroupId(channelId);
|
|
1278
|
-
//
|
|
1279
|
-
const
|
|
1280
|
-
const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
|
|
1281
|
-
if (statusColonIdx > 0) {
|
|
1282
|
-
payload.chat_id = channelId;
|
|
1283
|
-
}
|
|
1888
|
+
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
1889
|
+
const statusTargetAid = channelId;
|
|
1284
1890
|
const encryptTarget = isGroup ? channelId : statusTargetAid;
|
|
1285
|
-
|
|
1891
|
+
// 计算 encrypt 标志(每次调用读最新 peerE2ee 状态,
|
|
1892
|
+
// 这样第二条 send 能受益于第一条触发的 peerE2ee 标记)
|
|
1893
|
+
const computeEncrypt = () => context?.metadata?.encrypted != null
|
|
1286
1894
|
? !!(context.metadata.encrypted)
|
|
1287
1895
|
: this.shouldEncrypt(encryptTarget);
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1896
|
+
const sendOne = (method, payload, label) => {
|
|
1897
|
+
const c = this.client;
|
|
1898
|
+
if (!c) {
|
|
1899
|
+
logger.debug(`${this.logPrefix()} ${label} skipped: client gone`);
|
|
1900
|
+
return Promise.resolve();
|
|
1901
|
+
}
|
|
1902
|
+
const encrypt = computeEncrypt();
|
|
1903
|
+
const params = { payload, encrypt };
|
|
1904
|
+
if (isGroup)
|
|
1905
|
+
params.group_id = channelId;
|
|
1906
|
+
else
|
|
1907
|
+
params.to = statusTargetAid;
|
|
1908
|
+
this.trace('OUT', `${method}.task_${label}`, params);
|
|
1909
|
+
return c.call(method, params).catch(e => {
|
|
1291
1910
|
if (encrypt && e instanceof E2EEError) {
|
|
1292
1911
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
1293
|
-
logger.warn(`${this.logPrefix()} E2EE
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1912
|
+
logger.warn(`${this.logPrefix()} E2EE task_${label} send failed to ${channelId}, retrying plaintext`);
|
|
1913
|
+
const c2 = this.client;
|
|
1914
|
+
if (!c2)
|
|
1915
|
+
return;
|
|
1916
|
+
const fallbackParams = { ...params, encrypt: false };
|
|
1917
|
+
return c2.call(method, fallbackParams).catch(e2 => {
|
|
1918
|
+
logger.debug(`${this.logPrefix()} task_${label} fallback failed: ${e2}`);
|
|
1297
1919
|
});
|
|
1298
1920
|
}
|
|
1299
|
-
|
|
1300
|
-
logger.debug(`${this.logPrefix()} Processing status failed: ${e}`);
|
|
1301
|
-
}
|
|
1921
|
+
logger.debug(`${this.logPrefix()} task_${label} failed: ${e}`);
|
|
1302
1922
|
});
|
|
1303
1923
|
};
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
sendWithFallback('message.send');
|
|
1313
|
-
}
|
|
1924
|
+
const method = isGroup ? 'group.send' : 'message.send';
|
|
1925
|
+
// 串行:等第一条完成(或失败更新 peerE2ee 标记)后再发第二条,
|
|
1926
|
+
// 保证两路 payload 到达顺序(event 在前,status 在后)+ 第二条能用最新 encrypt 状态
|
|
1927
|
+
sendOne(method, eventPayload, 'event')
|
|
1928
|
+
.then(() => sendOne(method, statusPayload, 'status'));
|
|
1929
|
+
// 统计为系统消息(按两条独立消息分别记账)
|
|
1930
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(eventPayload).length, undefined, true);
|
|
1931
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
|
|
1314
1932
|
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
1315
1933
|
const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
|
|
1316
1934
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
1317
|
-
logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode}
|
|
1935
|
+
logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel}`);
|
|
1318
1936
|
}
|
|
1319
1937
|
sendCustomPayload(channelId, payload) {
|
|
1320
1938
|
if (!this.client || !this.connected)
|
|
@@ -1329,12 +1947,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1329
1947
|
catch {
|
|
1330
1948
|
payloadObj = { text: payload };
|
|
1331
1949
|
}
|
|
1332
|
-
//
|
|
1333
|
-
const
|
|
1334
|
-
const customTargetAid = customColonIdx > 0 ? channelId.substring(0, customColonIdx) : channelId;
|
|
1335
|
-
if (customColonIdx > 0) {
|
|
1336
|
-
payloadObj.chat_id = channelId;
|
|
1337
|
-
}
|
|
1950
|
+
// 私聊 channelId = 对端 AID(不含 device_id)
|
|
1951
|
+
const customTargetAid = channelId;
|
|
1338
1952
|
const sendParams = {
|
|
1339
1953
|
to: customTargetAid, payload: payloadObj,
|
|
1340
1954
|
encrypt: true,
|
|
@@ -1365,43 +1979,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1365
1979
|
this.client = null;
|
|
1366
1980
|
}
|
|
1367
1981
|
this.connected = false;
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
this.
|
|
1982
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
1983
|
+
this.setAidStatus('disabled');
|
|
1984
|
+
if (this.traceWriter) {
|
|
1985
|
+
this.traceWriter.close();
|
|
1986
|
+
this.traceWriter = null;
|
|
1372
1987
|
}
|
|
1373
1988
|
logger.info(`${this.logPrefix()} Disconnected`);
|
|
1374
1989
|
}
|
|
1375
|
-
// ── TS-layer reconnect
|
|
1990
|
+
// ── TS-layer reconnect ─────────────────────────────────────
|
|
1991
|
+
// SDK 内部已经跑无限指数退避(max_attempts=0, max_delay=300s),
|
|
1992
|
+
// TS 层只负责:(1) initClient 失败时安排兜底重试;(2) flap/kicked 接管路径见 takeoverReconnect。
|
|
1376
1993
|
scheduleReconnect() {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
if (this.reconnectTimer)
|
|
1380
|
-
return;
|
|
1381
|
-
const delays = AUNChannel.RECONNECT_DELAYS;
|
|
1382
|
-
if (this.reconnectAttempt >= delays.length) {
|
|
1383
|
-
logger.error(`${this.logPrefix()} All ${delays.length} reconnect attempts exhausted, giving up`);
|
|
1384
|
-
this.onChannelDown?.();
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
const delay = delays[this.reconnectAttempt];
|
|
1388
|
-
this.reconnectAttempt++;
|
|
1389
|
-
logger.info(`${this.logPrefix()} Scheduling reconnect #${this.reconnectAttempt}/${delays.length} in ${delay}s`);
|
|
1390
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
1391
|
-
this.reconnectTimer = null;
|
|
1392
|
-
try {
|
|
1393
|
-
logger.info(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} starting...`);
|
|
1394
|
-
this.trace('OUT', 'reconnect.start', { attempt: this.reconnectAttempt });
|
|
1395
|
-
await this.initClient();
|
|
1396
|
-
this.trace('OUT', 'reconnect.ok', { attempt: this.reconnectAttempt });
|
|
1397
|
-
logger.info(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} succeeded`);
|
|
1398
|
-
}
|
|
1399
|
-
catch (err) {
|
|
1400
|
-
this.trace('OUT', 'reconnect.error', { attempt: this.reconnectAttempt, error: String(err) });
|
|
1401
|
-
logger.error(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} failed:`, err);
|
|
1402
|
-
this.scheduleReconnect();
|
|
1403
|
-
}
|
|
1404
|
-
}, delay * 1000);
|
|
1994
|
+
// initClient 早期失败(auth / connect 阶段)走这里:用 fallback 延迟兜底
|
|
1995
|
+
this.takeoverReconnect(AUNChannel.FALLBACK_DELAY_MS, 'terminal');
|
|
1405
1996
|
}
|
|
1406
1997
|
/** Manually trigger reconnect (e.g. from /check reconnect command) */
|
|
1407
1998
|
async reconnect() {
|
|
@@ -1411,7 +2002,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1411
2002
|
clearTimeout(this.reconnectTimer);
|
|
1412
2003
|
this.reconnectTimer = null;
|
|
1413
2004
|
}
|
|
1414
|
-
this.
|
|
2005
|
+
this.flapCount = 0;
|
|
1415
2006
|
try {
|
|
1416
2007
|
await this.initClient();
|
|
1417
2008
|
return `重连成功 (${this._aid})`;
|
|
@@ -1421,7 +2012,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1421
2012
|
return `重连失败: ${err},已安排自动重试`;
|
|
1422
2013
|
}
|
|
1423
2014
|
}
|
|
1424
|
-
/** Set callback for when all reconnect attempts are exhausted */
|
|
2015
|
+
/** Set callback for when all reconnect attempts are exhausted (deprecated: 现在无限重试,不会触发) */
|
|
1425
2016
|
setOnChannelDown(callback) {
|
|
1426
2017
|
this.onChannelDown = callback;
|
|
1427
2018
|
}
|
|
@@ -1430,18 +2021,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1430
2021
|
return {
|
|
1431
2022
|
connected: this.connected,
|
|
1432
2023
|
aid: this._aid,
|
|
1433
|
-
|
|
1434
|
-
maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
|
|
2024
|
+
flapCount: this.flapCount,
|
|
1435
2025
|
plaintextRecv: this.plaintextRecv,
|
|
1436
2026
|
};
|
|
1437
2027
|
}
|
|
1438
|
-
/** 读取本地 agent.md 中的 name
|
|
2028
|
+
/** 读取本地 agent.md 中的 name(用于身份上下文展示),若本地不存在则尝试远程拉取 */
|
|
1439
2029
|
loadSelfName(aid) {
|
|
1440
2030
|
try {
|
|
1441
2031
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
1442
2032
|
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
1443
|
-
if (!fs.existsSync(agentMdPath))
|
|
2033
|
+
if (!fs.existsSync(agentMdPath)) {
|
|
2034
|
+
// 异步拉取,不阻塞连接流程
|
|
2035
|
+
this.fetchAndCacheSelfName(aidName);
|
|
1444
2036
|
return undefined;
|
|
2037
|
+
}
|
|
1445
2038
|
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
1446
2039
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1447
2040
|
if (!fmMatch)
|
|
@@ -1453,6 +2046,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1453
2046
|
return undefined;
|
|
1454
2047
|
}
|
|
1455
2048
|
}
|
|
2049
|
+
async fetchAndCacheSelfName(aidName) {
|
|
2050
|
+
try {
|
|
2051
|
+
const { agentmdGet } = await import('../aun/aid/index.js');
|
|
2052
|
+
const content = await agentmdGet(aidName);
|
|
2053
|
+
if (content) {
|
|
2054
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2055
|
+
if (fmMatch) {
|
|
2056
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
2057
|
+
const name = nameMatch?.[1]?.trim();
|
|
2058
|
+
if (name) {
|
|
2059
|
+
this._selfName = name;
|
|
2060
|
+
if (this.aidStatsCollector)
|
|
2061
|
+
this.aidStatsCollector.setSelfName(this.config.aid, name);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
catch {
|
|
2067
|
+
// ignore — name will remain undefined
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
1456
2070
|
getSelfName() {
|
|
1457
2071
|
return this._selfName;
|
|
1458
2072
|
}
|
|
@@ -1478,6 +2092,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1478
2092
|
return { type: null }; // no agent.md → unknown
|
|
1479
2093
|
}
|
|
1480
2094
|
}
|
|
2095
|
+
/** 同步取 peerInfo 缓存,未命中返回 undefined,不发起任何网络请求。 */
|
|
2096
|
+
peerInfoCached(aid) {
|
|
2097
|
+
return this.peerInfoCache.get(aid);
|
|
2098
|
+
}
|
|
2099
|
+
/** 后台预取 peerInfo(下次需要时缓存已就绪),任何错误吞掉。 */
|
|
2100
|
+
prefetchPeerInfo(aid) {
|
|
2101
|
+
if (this.peerInfoCache.has(aid))
|
|
2102
|
+
return;
|
|
2103
|
+
void this.fetchPeerInfo(aid).catch(() => { });
|
|
2104
|
+
}
|
|
1481
2105
|
async uploadAgentMd(content) {
|
|
1482
2106
|
if (!this.client)
|
|
1483
2107
|
throw new Error('not connected');
|
|
@@ -1515,20 +2139,134 @@ export class AUNChannelPlugin {
|
|
|
1515
2139
|
flushDelay: inst.flushDelay,
|
|
1516
2140
|
encryptionSeed: inst.encryptionSeed,
|
|
1517
2141
|
owner: inst.owner,
|
|
2142
|
+
agentName: inst.agentName,
|
|
2143
|
+
channelName: inst.name,
|
|
1518
2144
|
aunTrace: config.debug?.aunTrace,
|
|
1519
2145
|
aunSdkLog: config.debug?.aunSdkLog,
|
|
1520
2146
|
});
|
|
1521
2147
|
const adapter = {
|
|
1522
2148
|
channelName: inst.name,
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2149
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
|
|
2150
|
+
send: async (envelope, payload) => {
|
|
2151
|
+
const ctx = envelope.replyContext;
|
|
2152
|
+
const channelId = envelope.channelId;
|
|
2153
|
+
switch (payload.kind) {
|
|
2154
|
+
case 'result.text':
|
|
2155
|
+
case 'command.result':
|
|
2156
|
+
case 'command.error':
|
|
2157
|
+
case 'system.notice':
|
|
2158
|
+
case 'system.error':
|
|
2159
|
+
case 'result.error': {
|
|
2160
|
+
const sendCtx = { ...(ctx ?? {}) };
|
|
2161
|
+
if (payload.kind === 'result.text' && payload.isFinal)
|
|
2162
|
+
sendCtx.title = '✓ 最终回复:';
|
|
2163
|
+
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
case 'result.file':
|
|
2167
|
+
await channel.sendFile(channelId, payload.filePath, ctx);
|
|
2168
|
+
return;
|
|
2169
|
+
case 'result.image': {
|
|
2170
|
+
// AUN 支持 image,走 sendStructured 发 type=image payload
|
|
2171
|
+
const buf = payload.data;
|
|
2172
|
+
const b64 = buf.toString('base64');
|
|
2173
|
+
await channel.sendStructured(channelId, {
|
|
2174
|
+
type: 'image',
|
|
2175
|
+
alt: payload.alt,
|
|
2176
|
+
data_base64: b64,
|
|
2177
|
+
mime_type: payload.mimeType,
|
|
2178
|
+
}, ctx);
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
case 'activity.batch': {
|
|
2182
|
+
const aunPayload = {
|
|
2183
|
+
type: 'thought',
|
|
2184
|
+
items: payload.items,
|
|
2185
|
+
client_context: { task_id: envelope.taskId, chatmode: envelope.chatmode, agent_name: envelope.agentName },
|
|
2186
|
+
};
|
|
2187
|
+
if (ctx?.threadId)
|
|
2188
|
+
aunPayload.thread_id = ctx.threadId;
|
|
2189
|
+
// 双发:thought.put(前端实时渲染) + message.send(消息历史持久化)
|
|
2190
|
+
await Promise.all([
|
|
2191
|
+
channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
|
|
2192
|
+
channel.sendStructured(channelId, aunPayload, ctx),
|
|
2193
|
+
]);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
case 'status.started':
|
|
2197
|
+
channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
|
|
2198
|
+
return;
|
|
2199
|
+
case 'status.completed':
|
|
2200
|
+
channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx);
|
|
2201
|
+
return;
|
|
2202
|
+
case 'status.interrupted':
|
|
2203
|
+
channel.sendProcessingStatus(channelId, 'interrupted', envelope.taskId, envelope.taskId, ctx);
|
|
2204
|
+
return;
|
|
2205
|
+
case 'status.error':
|
|
2206
|
+
channel.sendProcessingStatus(channelId, 'error', envelope.taskId, envelope.taskId, ctx);
|
|
2207
|
+
return;
|
|
2208
|
+
case 'status.timeout':
|
|
2209
|
+
channel.sendProcessingStatus(channelId, 'timeout', envelope.taskId, envelope.taskId, ctx);
|
|
2210
|
+
return;
|
|
2211
|
+
case 'interaction': {
|
|
2212
|
+
const req = payload.interaction;
|
|
2213
|
+
if (req.kind.kind === 'action') {
|
|
2214
|
+
const action = req.kind;
|
|
2215
|
+
const aunCard = {
|
|
2216
|
+
type: 'action_card',
|
|
2217
|
+
title: action.title,
|
|
2218
|
+
description: action.body,
|
|
2219
|
+
actions: action.buttons.map(btn => ({
|
|
2220
|
+
label: btn.label,
|
|
2221
|
+
value: btn.key,
|
|
2222
|
+
style: btn.style ?? 'default',
|
|
2223
|
+
behavior: 'reply',
|
|
2224
|
+
})),
|
|
2225
|
+
};
|
|
2226
|
+
if (ctx?.threadId)
|
|
2227
|
+
aunCard.thread_id = ctx.threadId;
|
|
2228
|
+
const msgId = await channel.sendStructured(channelId, aunCard, ctx);
|
|
2229
|
+
if (msgId) {
|
|
2230
|
+
channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: false });
|
|
2231
|
+
setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
else if (req.kind.kind === 'command-card') {
|
|
2235
|
+
const card = req.kind;
|
|
2236
|
+
const aunCard = {
|
|
2237
|
+
type: 'action_card',
|
|
2238
|
+
title: card.title,
|
|
2239
|
+
description: card.body,
|
|
2240
|
+
actions: card.buttons.map(btn => ({
|
|
2241
|
+
label: btn.label,
|
|
2242
|
+
value: btn.command,
|
|
2243
|
+
style: btn.style ?? 'default',
|
|
2244
|
+
behavior: 'reply',
|
|
2245
|
+
})),
|
|
2246
|
+
};
|
|
2247
|
+
if (ctx?.threadId)
|
|
2248
|
+
aunCard.thread_id = ctx.threadId;
|
|
2249
|
+
const msgId = await channel.sendStructured(channelId, aunCard, ctx);
|
|
2250
|
+
if (msgId) {
|
|
2251
|
+
channel.cardMessageIdMap.set(msgId, { requestId: req.id, isCommandCard: true });
|
|
2252
|
+
setTimeout(() => channel.cardMessageIdMap.delete(msgId), 20 * 60 * 1000);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
else if (payload.fallbackText) {
|
|
2256
|
+
await channel.sendMessage(channelId, payload.fallbackText, ctx);
|
|
2257
|
+
}
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
case 'custom': {
|
|
2261
|
+
const text = typeof payload.payload === 'string' ? payload.payload : JSON.stringify(payload.payload);
|
|
2262
|
+
channel.sendCustomPayload(channelId, text);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
default:
|
|
2266
|
+
logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
|
|
2267
|
+
}
|
|
2268
|
+
}, acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); }, onInteraction: (cb) => { channel.interactionCallback = cb; }, uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
2269
|
+
downloadAgentMd: (aid) => channel.downloadAgentMd(aid), _selfAid: () => channel.getStatus().aid,
|
|
1532
2270
|
_selfName: () => channel.getSelfName(),
|
|
1533
2271
|
};
|
|
1534
2272
|
const policy = {
|
|
@@ -1573,6 +2311,53 @@ export class AUNChannelPlugin {
|
|
|
1573
2311
|
connect: () => channel.connect(),
|
|
1574
2312
|
disconnect: () => channel.disconnect(),
|
|
1575
2313
|
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
2314
|
+
registerBridge(bridge, channelType) {
|
|
2315
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
|
|
2316
|
+
handler({
|
|
2317
|
+
channel: adapter.channelName,
|
|
2318
|
+
channelType,
|
|
2319
|
+
channelId: opts.channelId,
|
|
2320
|
+
selfId: opts.selfId,
|
|
2321
|
+
groupId: opts.groupId,
|
|
2322
|
+
content: opts.content,
|
|
2323
|
+
chatType: opts.chatType || 'private',
|
|
2324
|
+
peerId: opts.peerId || '',
|
|
2325
|
+
peerName: opts.peerName,
|
|
2326
|
+
messageId: opts.messageId,
|
|
2327
|
+
mentions: opts.mentions,
|
|
2328
|
+
threadId: opts.threadId,
|
|
2329
|
+
replyContext: opts.replyContext,
|
|
2330
|
+
source: opts.source,
|
|
2331
|
+
});
|
|
2332
|
+
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
2333
|
+
},
|
|
2334
|
+
registerHooks(ctx) {
|
|
2335
|
+
channel.setEventBus(ctx.eventBus);
|
|
2336
|
+
if (channel.setOnChannelDown) {
|
|
2337
|
+
channel.setOnChannelDown(() => {
|
|
2338
|
+
ctx.eventBus.publish({
|
|
2339
|
+
type: 'channel:error',
|
|
2340
|
+
channel: 'aun',
|
|
2341
|
+
channelName: adapter.channelName,
|
|
2342
|
+
status: 'auth_error',
|
|
2343
|
+
message: `⚠️ AUN 渠道 ${adapter.channelName} 断连,自动重试已用尽。\n使用 /check rty aun 手动重连`,
|
|
2344
|
+
timestamp: Date.now(),
|
|
2345
|
+
});
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
if (typeof channel.setSessionModeResolver === 'function') {
|
|
2349
|
+
channel.setSessionModeResolver(async (channelId) => {
|
|
2350
|
+
const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);
|
|
2351
|
+
return session?.sessionMode;
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
if (typeof channel.setDispatchModeResolver === 'function') {
|
|
2355
|
+
channel.setDispatchModeResolver(async (channelId) => {
|
|
2356
|
+
const session = await ctx.sessionManager.getActiveSession(adapter.channelName, channelId);
|
|
2357
|
+
return session?.metadata?.dispatchMode;
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
},
|
|
1576
2361
|
});
|
|
1577
2362
|
}
|
|
1578
2363
|
return result;
|