evolclaw 2.7.3 → 2.8.1
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/dist/agents/claude-runner.js +91 -48
- package/dist/channels/aun-ops.js +275 -0
- package/dist/channels/aun.js +285 -125
- package/dist/cli.js +360 -1
- package/dist/config.js +1 -1
- package/dist/core/agent-registry.js +164 -0
- package/dist/core/command-handler.js +147 -97
- package/dist/core/evolagent-schema.js +72 -0
- package/dist/core/evolagent.js +66 -0
- package/dist/core/message/message-bridge.js +14 -2
- package/dist/core/message/message-processor.js +24 -10
- package/dist/core/message/thought-emitter.js +4 -2
- package/dist/core/session/session-manager.js +22 -3
- package/dist/index.js +8 -12
- package/dist/paths.js +2 -0
- package/dist/templates/prompts.md +1 -0
- package/dist/utils/init-channel.js +91 -221
- package/dist/utils/init.js +18 -42
- package/dist/utils/logger.js +58 -2
- package/evolclaw-install-aun.md +48 -7
- package/package.json +2 -2
package/dist/channels/aun.js
CHANGED
|
@@ -35,16 +35,45 @@ export class AUNChannel {
|
|
|
35
35
|
recallHandler;
|
|
36
36
|
connected = false;
|
|
37
37
|
traceStream = null;
|
|
38
|
-
|
|
38
|
+
traceHourTag = ''; // 当前 trace 文件对应的小时标识 (YYYYMMDD-HH)
|
|
39
39
|
trace(dir, event, data) {
|
|
40
40
|
if (!this.config.aunTrace)
|
|
41
41
|
return;
|
|
42
42
|
this.rotateTraceIfNeeded();
|
|
43
43
|
if (!this.traceStream)
|
|
44
44
|
return;
|
|
45
|
-
|
|
45
|
+
// 自动从 data 推断顶层字段(self_aid / peer_aid / group_id / task_id / chatmode),
|
|
46
|
+
// 便于 jq 过滤:`jq 'select(.task_id == "task-xxx")'`
|
|
47
|
+
const d = (data && typeof data === 'object') ? data : {};
|
|
48
|
+
const payload = d.payload ?? {};
|
|
49
|
+
const topContext = {
|
|
50
|
+
self_aid: this._aid ?? this.config.aid,
|
|
51
|
+
};
|
|
52
|
+
// peer / group 识别
|
|
53
|
+
const peerAid = d.to ?? d.from ?? d.sender_aid ?? payload.to;
|
|
54
|
+
if (peerAid)
|
|
55
|
+
topContext.peer_aid = peerAid;
|
|
56
|
+
const groupId = d.group_id ?? payload.group_id;
|
|
57
|
+
if (groupId)
|
|
58
|
+
topContext.group_id = groupId;
|
|
59
|
+
// task_id / chatmode(message.send / thought.put / status 都可能有)
|
|
60
|
+
const taskId = payload.task_id ?? d.context?.id ?? d.data?.task_id;
|
|
61
|
+
if (taskId)
|
|
62
|
+
topContext.task_id = taskId;
|
|
63
|
+
const chatmode = payload.chatmode;
|
|
64
|
+
if (chatmode)
|
|
65
|
+
topContext.chatmode = chatmode;
|
|
66
|
+
const line = JSON.stringify({ ts: localTimestamp(), dir, event, ...topContext, data });
|
|
46
67
|
this.traceStream.write(line + '\n');
|
|
47
68
|
}
|
|
69
|
+
/** 日志前缀(含 self aid 简称,多实例可识别) */
|
|
70
|
+
logPrefix() {
|
|
71
|
+
const aid = this._aid ?? this.config.aid;
|
|
72
|
+
if (!aid)
|
|
73
|
+
return '[AUN]';
|
|
74
|
+
const short = aid.split('.')[0] || aid;
|
|
75
|
+
return `[AUN ${short}]`;
|
|
76
|
+
}
|
|
48
77
|
/**
|
|
49
78
|
* 统一的 RPC 调用包装:自动记录 OUT 发送、.ok 结果、.error 错误(含 trace + evolclaw.log 失败日志)。
|
|
50
79
|
* 所有 client.call() 都应通过此方法调用,保证 aun-trace 里每个 OUT 调用都有"发+收/错"成对记录。
|
|
@@ -68,22 +97,43 @@ export class AUNChannel {
|
|
|
68
97
|
code: e?.code,
|
|
69
98
|
name: e?.name,
|
|
70
99
|
});
|
|
71
|
-
logger.warn(
|
|
100
|
+
logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
|
|
72
101
|
throw e;
|
|
73
102
|
}
|
|
74
103
|
}
|
|
104
|
+
static AUN_TRACE_RE = /^aun-\d{8}-\d{2}\.log$/;
|
|
105
|
+
static AUN_RETAIN_HOURS = 12;
|
|
75
106
|
rotateTraceIfNeeded() {
|
|
76
107
|
const d = new Date();
|
|
77
|
-
const
|
|
78
|
-
|
|
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)
|
|
79
111
|
return;
|
|
80
112
|
if (this.traceStream) {
|
|
81
113
|
this.traceStream.end();
|
|
82
114
|
this.traceStream = null;
|
|
83
115
|
}
|
|
84
|
-
this.
|
|
85
|
-
const logPath = path.join(resolvePaths().logs, `aun-${
|
|
116
|
+
this.traceHourTag = hourTag;
|
|
117
|
+
const logPath = path.join(resolvePaths().logs, `aun-${hourTag}.log`);
|
|
86
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 { }
|
|
87
137
|
}
|
|
88
138
|
/** 判断 channelId 是否为群组 ID
|
|
89
139
|
* - 新格式:group.{issuer}/{group_no|group_name}
|
|
@@ -104,6 +154,18 @@ export class AUNChannel {
|
|
|
104
154
|
return undefined;
|
|
105
155
|
return trimmed.split('.')[0] || trimmed;
|
|
106
156
|
}
|
|
157
|
+
/** 同步获取对端显示名(仅从缓存,不触发网络请求)。用于日志中补充对端标识。
|
|
158
|
+
* 返回 `shortAid(displayName)` 或 `shortAid`,若 aid 为空返回 '?'
|
|
159
|
+
*/
|
|
160
|
+
peerLabel(aid) {
|
|
161
|
+
if (!aid)
|
|
162
|
+
return '?';
|
|
163
|
+
const bareAid = aid.includes(':') ? aid.substring(0, aid.indexOf(':')) : aid;
|
|
164
|
+
const short = this.getShortAid(bareAid) ?? bareAid;
|
|
165
|
+
const cached = this.peerInfoCache.get(bareAid);
|
|
166
|
+
const name = cached?.name;
|
|
167
|
+
return name && name !== short ? `${short}(${name})` : short;
|
|
168
|
+
}
|
|
107
169
|
extractTextPayload(payload) {
|
|
108
170
|
if (typeof payload === 'string')
|
|
109
171
|
return payload;
|
|
@@ -131,8 +193,41 @@ export class AUNChannel {
|
|
|
131
193
|
.replace(/[ \t]+/g, ' ')
|
|
132
194
|
.trim();
|
|
133
195
|
}
|
|
134
|
-
|
|
135
|
-
const
|
|
196
|
+
extractMentionAids(mentions) {
|
|
197
|
+
const aids = [];
|
|
198
|
+
for (const m of mentions) {
|
|
199
|
+
if (typeof m === 'string')
|
|
200
|
+
aids.push(m);
|
|
201
|
+
else if (m && typeof m === 'object' && typeof m.aid === 'string')
|
|
202
|
+
aids.push(m.aid);
|
|
203
|
+
}
|
|
204
|
+
return aids;
|
|
205
|
+
}
|
|
206
|
+
hasMentionAll(mentions) {
|
|
207
|
+
for (const m of mentions) {
|
|
208
|
+
if (m === 'all')
|
|
209
|
+
return true;
|
|
210
|
+
if (m && typeof m === 'object' && m.scope === 'all')
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
/** 从正文提取 @aid(AID 格式:至少三段域名),用于出站填充 payload.mentions */
|
|
216
|
+
static MENTION_AID_RE = /@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?){2,})/g;
|
|
217
|
+
extractMentionAidsFromText(text) {
|
|
218
|
+
const out = [];
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
for (const m of text.matchAll(AUNChannel.MENTION_AID_RE)) {
|
|
221
|
+
const aid = m[1];
|
|
222
|
+
if (!seen.has(aid)) {
|
|
223
|
+
seen.add(aid);
|
|
224
|
+
out.push(aid);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
buildGroupReplyContext(taskId, senderAid, encrypted) {
|
|
230
|
+
const replyContext = { metadata: { encrypted } };
|
|
136
231
|
if (taskId)
|
|
137
232
|
replyContext.threadId = taskId;
|
|
138
233
|
replyContext.peerId = senderAid;
|
|
@@ -141,7 +236,7 @@ export class AUNChannel {
|
|
|
141
236
|
acknowledgeImmediately(messageId, seq) {
|
|
142
237
|
if (seq != null && this.client) {
|
|
143
238
|
this.client.call('message.ack', { seq }).catch(e => {
|
|
144
|
-
logger.debug(
|
|
239
|
+
logger.debug(`${this.logPrefix()} Immediate ack failed: ${e}`);
|
|
145
240
|
});
|
|
146
241
|
}
|
|
147
242
|
if (messageId)
|
|
@@ -167,6 +262,11 @@ export class AUNChannel {
|
|
|
167
262
|
peerE2ee = new Map();
|
|
168
263
|
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
169
264
|
plaintextRecv = 0;
|
|
265
|
+
sessionModeResolver;
|
|
266
|
+
static PROACTIVE_ALLOW_TYPES = new Set([
|
|
267
|
+
'text', 'quote', 'image', 'video', 'voice', 'file', 'json',
|
|
268
|
+
'merge', 'link', 'location', 'personal_card',
|
|
269
|
+
]);
|
|
170
270
|
// Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
|
|
171
271
|
intentionalDisconnect = false;
|
|
172
272
|
reconnectAttempt = 0;
|
|
@@ -183,7 +283,7 @@ export class AUNChannel {
|
|
|
183
283
|
this.config = config;
|
|
184
284
|
if (config.aunTrace) {
|
|
185
285
|
this.rotateTraceIfNeeded();
|
|
186
|
-
logger.info(
|
|
286
|
+
logger.info(`${this.logPrefix()} Trace logging enabled (hourly rotation, 12h retention): ${resolvePaths().logs}/aun-YYYYMMDD-HH.log`);
|
|
187
287
|
}
|
|
188
288
|
}
|
|
189
289
|
async connect() {
|
|
@@ -216,17 +316,17 @@ export class AUNChannel {
|
|
|
216
316
|
try {
|
|
217
317
|
const discovery = new GatewayDiscovery({});
|
|
218
318
|
gateway = await discovery.discover(wellKnownUrl);
|
|
219
|
-
logger.info(
|
|
319
|
+
logger.info(`${this.logPrefix()} Gateway discovered: ${gateway}`);
|
|
220
320
|
}
|
|
221
321
|
catch (e) {
|
|
222
|
-
logger.warn(
|
|
322
|
+
logger.warn(`${this.logPrefix()} Well-known discovery failed (${e}), no fallback available`);
|
|
223
323
|
}
|
|
224
324
|
}
|
|
225
325
|
if (!gateway) {
|
|
226
|
-
logger.error(
|
|
326
|
+
logger.error(`${this.logPrefix()} Cannot resolve gateway URL from AID`);
|
|
227
327
|
throw new Error('Cannot resolve gateway URL from AID');
|
|
228
328
|
}
|
|
229
|
-
logger.info(
|
|
329
|
+
logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
|
|
230
330
|
// Create client with FileSecretStore (AES-256-GCM)
|
|
231
331
|
// 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
|
|
232
332
|
const rootCaPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
@@ -242,14 +342,14 @@ export class AUNChannel {
|
|
|
242
342
|
this.trace('IN', 'message.received', data);
|
|
243
343
|
const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
|
|
244
344
|
const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
|
|
245
|
-
logger.debug(
|
|
345
|
+
logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
|
|
246
346
|
this.handleIncomingPrivateMessage(data);
|
|
247
347
|
});
|
|
248
348
|
this.client.on('group.message_created', (data) => {
|
|
249
349
|
this.trace('IN', 'group.message_created', data);
|
|
250
350
|
const gid = (data && typeof data === 'object') ? data.group_id ?? '' : '';
|
|
251
351
|
const sender = (data && typeof data === 'object') ? data.sender_aid ?? '' : '';
|
|
252
|
-
logger.debug(
|
|
352
|
+
logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
|
|
253
353
|
this.handleIncomingGroupMessage(data);
|
|
254
354
|
});
|
|
255
355
|
this.client.on('connection.state', (data) => {
|
|
@@ -263,7 +363,7 @@ export class AUNChannel {
|
|
|
263
363
|
if (Array.isArray(ids)) {
|
|
264
364
|
for (const id of ids) {
|
|
265
365
|
if (typeof id === 'string') {
|
|
266
|
-
logger.info(
|
|
366
|
+
logger.info(`${this.logPrefix()} Message recalled: ${id}`);
|
|
267
367
|
this.recallHandler?.(id);
|
|
268
368
|
}
|
|
269
369
|
}
|
|
@@ -273,12 +373,12 @@ export class AUNChannel {
|
|
|
273
373
|
this.client.on('message.undecryptable', (data) => {
|
|
274
374
|
this.trace('IN', 'message.undecryptable', data);
|
|
275
375
|
const d = data;
|
|
276
|
-
logger.warn(
|
|
376
|
+
logger.warn(`${this.logPrefix()} Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
|
|
277
377
|
});
|
|
278
378
|
this.client.on('group.message_undecryptable', (data) => {
|
|
279
379
|
this.trace('IN', 'group.message_undecryptable', data);
|
|
280
380
|
const d = data;
|
|
281
|
-
logger.warn(
|
|
381
|
+
logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
|
|
282
382
|
});
|
|
283
383
|
// Authenticate
|
|
284
384
|
// Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
|
|
@@ -295,7 +395,7 @@ export class AUNChannel {
|
|
|
295
395
|
}
|
|
296
396
|
let accessToken;
|
|
297
397
|
try {
|
|
298
|
-
logger.info(
|
|
398
|
+
logger.info(`${this.logPrefix()} Authenticating as ${aidName}...`);
|
|
299
399
|
this.trace('OUT', 'auth.authenticate', { aid: aidName });
|
|
300
400
|
const auth = await this.client.auth.authenticate(aidName ? { aid: aidName } : undefined);
|
|
301
401
|
this.trace('OUT', 'auth.authenticate.ok', { aid: auth.aid, gateway: auth.gateway, hasToken: !!auth.access_token });
|
|
@@ -303,23 +403,23 @@ export class AUNChannel {
|
|
|
303
403
|
accessToken = auth.access_token;
|
|
304
404
|
const resolvedGateway = auth.gateway || gateway;
|
|
305
405
|
this.client._gatewayUrl = resolvedGateway;
|
|
306
|
-
logger.info(
|
|
406
|
+
logger.info(`${this.logPrefix()} Authenticated as ${auth.aid ?? '?'}, gateway=${resolvedGateway}`);
|
|
307
407
|
}
|
|
308
408
|
catch (e) {
|
|
309
409
|
const errMsg = e.message || String(e);
|
|
310
410
|
const errName = e.constructor?.name || 'Error';
|
|
311
411
|
this.trace('OUT', 'auth.authenticate.error', { error: errMsg, name: errName });
|
|
312
|
-
logger.error(
|
|
412
|
+
logger.error(`${this.logPrefix()} Authentication failed (${errName}): ${errMsg}`);
|
|
313
413
|
if (e.stack)
|
|
314
|
-
logger.debug(
|
|
414
|
+
logger.debug(`${this.logPrefix()} Auth stack: ${e.stack}`);
|
|
315
415
|
// Fallback: try direct token from env/config (legacy)
|
|
316
416
|
accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
|
|
317
417
|
if (!accessToken) {
|
|
318
|
-
logger.error(
|
|
418
|
+
logger.error(`${this.logPrefix()} No accessToken fallback available, scheduling retry`);
|
|
319
419
|
this.scheduleReconnect();
|
|
320
420
|
throw new Error('Authentication failed and no accessToken fallback available');
|
|
321
421
|
}
|
|
322
|
-
logger.warn(
|
|
422
|
+
logger.warn(`${this.logPrefix()} Using accessToken fallback`);
|
|
323
423
|
}
|
|
324
424
|
// Connect (SDK auto_reconnect handles transient failures)
|
|
325
425
|
try {
|
|
@@ -340,16 +440,16 @@ export class AUNChannel {
|
|
|
340
440
|
const cert = clientAny._keystore?.loadCert?.(aidName);
|
|
341
441
|
if (cert) {
|
|
342
442
|
clientAny._identity.cert = cert;
|
|
343
|
-
logger.info(
|
|
443
|
+
logger.info(`${this.logPrefix()} Backfilled identity.cert from keystore for e2ee fingerprint`);
|
|
344
444
|
}
|
|
345
445
|
}
|
|
346
|
-
logger.info(
|
|
446
|
+
logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
|
|
347
447
|
// Send welcome message to owner after first connection
|
|
348
448
|
await this.sendWelcomeMessage();
|
|
349
449
|
}
|
|
350
450
|
catch (e) {
|
|
351
451
|
this.trace('OUT', 'client.connect.error', { error: String(e) });
|
|
352
|
-
logger.error(
|
|
452
|
+
logger.error(`${this.logPrefix()} Connection failed: ${e}`);
|
|
353
453
|
this.scheduleReconnect();
|
|
354
454
|
throw e;
|
|
355
455
|
}
|
|
@@ -358,7 +458,7 @@ export class AUNChannel {
|
|
|
358
458
|
try {
|
|
359
459
|
const owner = this.config.owner;
|
|
360
460
|
if (!owner) {
|
|
361
|
-
logger.info(
|
|
461
|
+
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message`);
|
|
362
462
|
return;
|
|
363
463
|
}
|
|
364
464
|
// Check agent.md initialized field
|
|
@@ -366,25 +466,25 @@ export class AUNChannel {
|
|
|
366
466
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
367
467
|
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
368
468
|
if (!fs.existsSync(agentMdPath)) {
|
|
369
|
-
logger.warn(
|
|
469
|
+
logger.warn(`${this.logPrefix()} agent.md not found, skipping welcome message`);
|
|
370
470
|
return;
|
|
371
471
|
}
|
|
372
472
|
const agentMdContent = fs.readFileSync(agentMdPath, 'utf-8');
|
|
373
473
|
const match = agentMdContent.match(/^---\n([\s\S]*?)\n---/);
|
|
374
474
|
if (!match) {
|
|
375
|
-
logger.warn(
|
|
475
|
+
logger.warn(`${this.logPrefix()} agent.md frontmatter not found`);
|
|
376
476
|
return;
|
|
377
477
|
}
|
|
378
478
|
const frontmatter = match[1];
|
|
379
479
|
const initializedMatch = frontmatter.match(/^initialized:\s*(true|false)/m);
|
|
380
480
|
if (!initializedMatch || initializedMatch[1] === 'true') {
|
|
381
|
-
logger.info(
|
|
481
|
+
logger.info(`${this.logPrefix()} Agent already initialized, skipping welcome message`);
|
|
382
482
|
return;
|
|
383
483
|
}
|
|
384
484
|
// Fetch owner's agent.md to derive name and validate type
|
|
385
485
|
const ownerInfo = await this.fetchPeerInfo(owner);
|
|
386
486
|
if (ownerInfo.type !== null && ownerInfo.type !== 'human') {
|
|
387
|
-
logger.warn(
|
|
487
|
+
logger.warn(`${this.logPrefix()} Owner ${owner} type is "${ownerInfo.type}" (not human). Consider using a human AID as owner.`);
|
|
388
488
|
}
|
|
389
489
|
// Name: prefer existing agent.md name if user has customized it,
|
|
390
490
|
// otherwise generate "{ownerName}的Evol助手 ({aidLabel})" for disambiguation
|
|
@@ -428,14 +528,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
428
528
|
`;
|
|
429
529
|
// Write locally
|
|
430
530
|
fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
|
|
431
|
-
logger.info(
|
|
531
|
+
logger.info(`${this.logPrefix()} Updated agent.md with initialized=true`);
|
|
432
532
|
// Publish to AUN network via auth.uploadAgentMd
|
|
433
533
|
try {
|
|
434
534
|
await this.client.auth.uploadAgentMd(newAgentMd);
|
|
435
|
-
logger.info(
|
|
535
|
+
logger.info(`${this.logPrefix()} Published agent.md to AUN network`);
|
|
436
536
|
}
|
|
437
537
|
catch (e) {
|
|
438
|
-
logger.warn(
|
|
538
|
+
logger.warn(`${this.logPrefix()} Failed to publish agent.md: ${e}`);
|
|
439
539
|
}
|
|
440
540
|
// Send welcome message
|
|
441
541
|
const welcomeText = `🎉 欢迎使用 EvolClaw!
|
|
@@ -463,7 +563,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
463
563
|
// pull if the initial E2EE push still arrives before the cert resolves.
|
|
464
564
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
465
565
|
if (!this.client) {
|
|
466
|
-
logger.warn(
|
|
566
|
+
logger.warn(`${this.logPrefix()} Client disconnected before welcome message could be sent`);
|
|
467
567
|
return;
|
|
468
568
|
}
|
|
469
569
|
await this.callAndTrace('message.send', {
|
|
@@ -472,10 +572,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
472
572
|
encrypt: true,
|
|
473
573
|
persist_required: true,
|
|
474
574
|
});
|
|
475
|
-
logger.info(
|
|
575
|
+
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
476
576
|
}
|
|
477
577
|
catch (e) {
|
|
478
|
-
logger.warn(
|
|
578
|
+
logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
|
|
479
579
|
}
|
|
480
580
|
}
|
|
481
581
|
// ── Event handlers ──────────────────────────────────────────
|
|
@@ -484,7 +584,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
484
584
|
const objectKey = att.object_key;
|
|
485
585
|
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
486
586
|
if (!objectKey) {
|
|
487
|
-
logger.warn(
|
|
587
|
+
logger.warn(`${this.logPrefix()} Attachment missing object_key, skipping`);
|
|
488
588
|
return null;
|
|
489
589
|
}
|
|
490
590
|
let downloadUrl;
|
|
@@ -495,32 +595,32 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
495
595
|
});
|
|
496
596
|
downloadUrl = ticket.download_url || '';
|
|
497
597
|
if (!downloadUrl) {
|
|
498
|
-
logger.warn(
|
|
598
|
+
logger.warn(`${this.logPrefix()} No download_url for attachment: ${filename}`);
|
|
499
599
|
return null;
|
|
500
600
|
}
|
|
501
601
|
}
|
|
502
602
|
catch (e) {
|
|
503
|
-
logger.warn(
|
|
603
|
+
logger.warn(`${this.logPrefix()} create_download_ticket failed for ${filename}: ${e}`);
|
|
504
604
|
return null;
|
|
505
605
|
}
|
|
506
606
|
let buffer;
|
|
507
607
|
try {
|
|
508
608
|
const res = await fetch(downloadUrl);
|
|
509
609
|
if (!res.ok) {
|
|
510
|
-
logger.warn(
|
|
610
|
+
logger.warn(`${this.logPrefix()} Download failed for ${filename}: HTTP ${res.status}`);
|
|
511
611
|
return null;
|
|
512
612
|
}
|
|
513
613
|
buffer = Buffer.from(await res.arrayBuffer());
|
|
514
614
|
}
|
|
515
615
|
catch (e) {
|
|
516
|
-
logger.warn(
|
|
616
|
+
logger.warn(`${this.logPrefix()} Download error for ${filename}: ${e}`);
|
|
517
617
|
return null;
|
|
518
618
|
}
|
|
519
619
|
if (att.sha256) {
|
|
520
620
|
const { createHash } = await import('node:crypto');
|
|
521
621
|
const actual = createHash('sha256').update(buffer).digest('hex');
|
|
522
622
|
if (actual !== att.sha256) {
|
|
523
|
-
logger.warn(
|
|
623
|
+
logger.warn(`${this.logPrefix()} SHA256 mismatch for ${filename}: expected ${att.sha256.slice(0, 8)}… got ${actual.slice(0, 8)}…`);
|
|
524
624
|
return null;
|
|
525
625
|
}
|
|
526
626
|
}
|
|
@@ -529,11 +629,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
529
629
|
: process.cwd();
|
|
530
630
|
try {
|
|
531
631
|
const result = saveToUploads(buffer, filename, projectPath);
|
|
532
|
-
logger.info(
|
|
632
|
+
logger.info(`${this.logPrefix()} Saved attachment: ${result.filePath} (${result.size} bytes)`);
|
|
533
633
|
return result.filePath;
|
|
534
634
|
}
|
|
535
635
|
catch (e) {
|
|
536
|
-
logger.warn(
|
|
636
|
+
logger.warn(`${this.logPrefix()} saveToUploads failed for ${filename}: ${e}`);
|
|
537
637
|
return null;
|
|
538
638
|
}
|
|
539
639
|
}
|
|
@@ -552,19 +652,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
552
652
|
const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
|
|
553
653
|
if (this._aid && fromAid === this._aid && (!msgChatId || !this._chatId || msgChatId !== this._chatId)) {
|
|
554
654
|
this.acknowledgeImmediately(messageId, seq);
|
|
555
|
-
logger.debug(
|
|
655
|
+
logger.debug(`${this.logPrefix()} P2P dropped: echo from self (from=${fromAid} mid=${messageId})`);
|
|
556
656
|
return;
|
|
557
657
|
}
|
|
558
|
-
//
|
|
658
|
+
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
559
659
|
const msgEncrypted = !!(msg.e2ee);
|
|
560
|
-
if (
|
|
561
|
-
|
|
562
|
-
this.peerE2ee.set(fromAid, { ok: true, ts: Date.now() });
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
this.plaintextRecv++;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
660
|
+
if (!msgEncrypted)
|
|
661
|
+
this.plaintextRecv++;
|
|
568
662
|
// Detect @mentions
|
|
569
663
|
const mentions = [];
|
|
570
664
|
if (this._aid && text.includes(`@${this._aid}`)) {
|
|
@@ -600,7 +694,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
600
694
|
const peerInfo = await this.fetchPeerInfo(fromAid);
|
|
601
695
|
const shortAid = this.getShortAid(fromAid);
|
|
602
696
|
const displayName = peerInfo.name || shortAid;
|
|
603
|
-
|
|
697
|
+
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
698
|
+
const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
699
|
+
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))}`);
|
|
700
|
+
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
|
|
701
|
+
const replyContext = { metadata: { encrypted: msgEncrypted } };
|
|
702
|
+
if (taskId)
|
|
703
|
+
replyContext.threadId = taskId;
|
|
604
704
|
this.dispatchMessage({
|
|
605
705
|
channelId: chatId,
|
|
606
706
|
userId: fromAid,
|
|
@@ -612,6 +712,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
612
712
|
mentions,
|
|
613
713
|
peerName: displayName || undefined,
|
|
614
714
|
peerType: peerInfo.type || 'unknown',
|
|
715
|
+
replyContext,
|
|
615
716
|
});
|
|
616
717
|
}
|
|
617
718
|
async handleIncomingGroupMessage(data) {
|
|
@@ -629,37 +730,56 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
629
730
|
const payloadMentions = Array.isArray(payload?.mentions)
|
|
630
731
|
? payload.mentions.filter((m) => typeof m === 'string')
|
|
631
732
|
: [];
|
|
632
|
-
logger.debug(
|
|
733
|
+
logger.debug(`${this.logPrefix()}[DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
633
734
|
if (!groupId || !senderAid) {
|
|
634
735
|
this.acknowledgeImmediately(messageId, seq);
|
|
635
|
-
logger.debug(
|
|
736
|
+
logger.debug(`${this.logPrefix()} Group dropped: missing groupId or senderAid (mid=${messageId})`);
|
|
636
737
|
return;
|
|
637
738
|
}
|
|
638
739
|
if (this._aid && senderAid === this._aid) {
|
|
639
740
|
this.acknowledgeImmediately(messageId, seq);
|
|
640
|
-
logger.debug(
|
|
741
|
+
logger.debug(`${this.logPrefix()} Group dropped: own message (group=${groupId} mid=${messageId})`);
|
|
641
742
|
return;
|
|
642
743
|
}
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
744
|
+
// ── proactive 模式入站白名单 ──
|
|
745
|
+
if (this.sessionModeResolver) {
|
|
746
|
+
const sessionMode = await this.sessionModeResolver(groupId).catch(() => undefined);
|
|
747
|
+
if (sessionMode === 'proactive') {
|
|
748
|
+
const payloadObj = (payload && typeof payload === 'object') ? payload : null;
|
|
749
|
+
const payloadType = payloadObj?.type ?? '';
|
|
750
|
+
if (!AUNChannel.PROACTIVE_ALLOW_TYPES.has(payloadType)) {
|
|
751
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
752
|
+
logger.info(`${this.logPrefix()} Group dropped (proactive deny): type=${payloadType} group=${groupId} sender=${senderAid} mid=${messageId}`);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
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
|
+
}
|
|
651
766
|
}
|
|
652
767
|
}
|
|
768
|
+
// 记录入站消息加密状态,透传到出站 ReplyContext
|
|
769
|
+
const msgEncrypted = !!(msg.e2ee);
|
|
770
|
+
if (!msgEncrypted)
|
|
771
|
+
this.plaintextRecv++;
|
|
653
772
|
// dispatch_mode from server tells agent how to work in this group
|
|
654
773
|
const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
|
|
655
774
|
const mentionedSelf = this._aid
|
|
656
775
|
? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
|
|
657
776
|
: false;
|
|
658
|
-
|
|
777
|
+
// @all 仅认结构化 mentions(payload.mentions),不扫描正文 — 避免引述性 "@all" 误判
|
|
778
|
+
const mentionedAll = payloadMentions.includes('all');
|
|
659
779
|
// In mention mode, only respond when explicitly mentioned; in broadcast mode, respond to all
|
|
660
780
|
if (dispatchMode === 'mention' && !mentionedSelf && !mentionedAll) {
|
|
661
781
|
this.acknowledgeImmediately(messageId, seq);
|
|
662
|
-
logger.
|
|
782
|
+
logger.info(`${this.logPrefix()} Group dropped: unmentioned in mention-mode (group=${groupId} sender=${senderAid} mid=${messageId} textPreview=${JSON.stringify(text.slice(0, 80))})`);
|
|
663
783
|
return;
|
|
664
784
|
}
|
|
665
785
|
const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
|
|
@@ -671,7 +791,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
671
791
|
// Allow through if there's text OR attachments; both-empty messages are silently dropped
|
|
672
792
|
if (!strippedText && !hasAttachments) {
|
|
673
793
|
this.acknowledgeImmediately(messageId, seq);
|
|
674
|
-
logger.debug(
|
|
794
|
+
logger.debug(`${this.logPrefix()} Group dropped: empty text and no attachments (group=${groupId} sender=${senderAid} mid=${messageId})`);
|
|
675
795
|
return;
|
|
676
796
|
}
|
|
677
797
|
const mentions = mentionedAll
|
|
@@ -700,7 +820,19 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
700
820
|
const peerInfo = await this.fetchPeerInfo(senderAid);
|
|
701
821
|
const shortAid = this.getShortAid(senderAid);
|
|
702
822
|
const displayName = peerInfo.name || shortAid;
|
|
703
|
-
|
|
823
|
+
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
824
|
+
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
825
|
+
const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
|
|
826
|
+
const textMentionAll = this.hasExplicitMention(text, 'all');
|
|
827
|
+
const structMentionSelf = this._aid ? payloadMentions.includes(this._aid) : false;
|
|
828
|
+
const structMentionAll = payloadMentions.includes('all');
|
|
829
|
+
const reason = mentionedAll
|
|
830
|
+
? 'mention.all(struct)'
|
|
831
|
+
: mentionedSelf
|
|
832
|
+
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
833
|
+
: `${dispatchMode}.no-mention`;
|
|
834
|
+
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))}`);
|
|
835
|
+
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} text=${finalText.slice(0, 60)}`);
|
|
704
836
|
this.dispatchMessage({
|
|
705
837
|
channelId: groupId,
|
|
706
838
|
userId: senderAid,
|
|
@@ -712,7 +844,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
712
844
|
seq,
|
|
713
845
|
taskId,
|
|
714
846
|
mentions,
|
|
715
|
-
replyContext: this.buildGroupReplyContext(taskId, senderAid),
|
|
847
|
+
replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted),
|
|
716
848
|
});
|
|
717
849
|
}
|
|
718
850
|
dispatchMessage(event) {
|
|
@@ -748,7 +880,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
748
880
|
mentions: mentionObjects,
|
|
749
881
|
replyContext,
|
|
750
882
|
}).catch(err => {
|
|
751
|
-
logger.error(
|
|
883
|
+
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
752
884
|
});
|
|
753
885
|
}
|
|
754
886
|
handleConnectionState(data) {
|
|
@@ -761,14 +893,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
761
893
|
this.lastReconnectLogTime = 0;
|
|
762
894
|
this.lastReconnectLogAttempt = 0;
|
|
763
895
|
this.trace('IN', 'connection.state', data);
|
|
764
|
-
logger.info(
|
|
896
|
+
logger.info(`${this.logPrefix()} Connected`);
|
|
765
897
|
}
|
|
766
898
|
else if (state === 'disconnected') {
|
|
767
899
|
this.connected = false;
|
|
768
900
|
this.trace('IN', 'connection.state', data);
|
|
769
|
-
logger.warn(
|
|
901
|
+
logger.warn(`${this.logPrefix()} Disconnected: ${data.error ?? 'unknown'}`);
|
|
770
902
|
}
|
|
771
903
|
else if (state === 'reconnecting') {
|
|
904
|
+
this.connected = false;
|
|
772
905
|
const attempt = data.attempt ?? 0;
|
|
773
906
|
const now = Date.now();
|
|
774
907
|
// Throttled logging: first attempt, every N attempts, or every M seconds
|
|
@@ -778,14 +911,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
778
911
|
if (isFirst || isStep || isInterval) {
|
|
779
912
|
const suppressed = attempt - this.lastReconnectLogAttempt - 1;
|
|
780
913
|
const suffix = suppressed > 0 ? `, ${suppressed} suppressed since last log` : '';
|
|
781
|
-
logger.info(
|
|
914
|
+
logger.info(`${this.logPrefix()} SDK reconnecting (attempt ${attempt}${suffix})`);
|
|
782
915
|
this.lastReconnectLogTime = now;
|
|
783
916
|
this.lastReconnectLogAttempt = attempt;
|
|
784
917
|
this.trace('IN', 'connection.state', data);
|
|
785
918
|
}
|
|
786
919
|
// Detect runaway SDK reconnect loop: force disconnect and use TS-layer backoff
|
|
787
920
|
if (attempt >= AUNChannel.SDK_RECONNECT_GIVEUP && !this.intentionalDisconnect) {
|
|
788
|
-
logger.warn(
|
|
921
|
+
logger.warn(`${this.logPrefix()} SDK reconnect stuck at attempt ${attempt}, forcing TS-layer reconnect with backoff`);
|
|
789
922
|
this.connected = false;
|
|
790
923
|
if (this.client) {
|
|
791
924
|
this.trace('OUT', 'client.close', { reason: 'sdk_reconnect_stuck' });
|
|
@@ -799,7 +932,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
799
932
|
this.connected = false;
|
|
800
933
|
this.trace('IN', 'connection.state', data);
|
|
801
934
|
const reason = data.reason ?? '';
|
|
802
|
-
logger.error(
|
|
935
|
+
logger.error(`${this.logPrefix()} Terminal failure: ${data.error ?? 'unknown'}${reason ? ` (${reason})` : ''}`);
|
|
803
936
|
if (!this.intentionalDisconnect) {
|
|
804
937
|
this.scheduleReconnect();
|
|
805
938
|
}
|
|
@@ -812,16 +945,19 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
812
945
|
onMessage(handler) {
|
|
813
946
|
this.messageHandler = handler;
|
|
814
947
|
}
|
|
948
|
+
setSessionModeResolver(resolver) {
|
|
949
|
+
this.sessionModeResolver = resolver;
|
|
950
|
+
}
|
|
815
951
|
onRecall(handler) {
|
|
816
952
|
this.recallHandler = handler;
|
|
817
953
|
}
|
|
818
954
|
async sendMessage(channelId, text, context) {
|
|
819
955
|
if (!this.connected || !this.client) {
|
|
820
|
-
logger.warn(
|
|
956
|
+
logger.warn(`${this.logPrefix()} Cannot send: not connected`);
|
|
821
957
|
return;
|
|
822
958
|
}
|
|
823
959
|
if (!text?.trim()) {
|
|
824
|
-
logger.warn(
|
|
960
|
+
logger.warn(`${this.logPrefix()} Attempted to send empty message, skipping`);
|
|
825
961
|
return;
|
|
826
962
|
}
|
|
827
963
|
let finalText = text;
|
|
@@ -837,6 +973,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
837
973
|
}
|
|
838
974
|
}
|
|
839
975
|
const payload = { type: 'text', text: finalText };
|
|
976
|
+
// 出站群消息:从正文提取 @aid 填入结构化 mentions(与 aun CLI 对齐,方便对端按 mentions 过滤)
|
|
977
|
+
if (this.isGroupId(channelId)) {
|
|
978
|
+
const extracted = this.extractMentionAidsFromText(finalText);
|
|
979
|
+
if (extracted.length > 0)
|
|
980
|
+
payload.mentions = extracted;
|
|
981
|
+
}
|
|
840
982
|
if (context?.threadId)
|
|
841
983
|
payload.thread_id = context.threadId;
|
|
842
984
|
if (context?.metadata?.taskId)
|
|
@@ -851,34 +993,42 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
851
993
|
payload.chat_id = channelId;
|
|
852
994
|
}
|
|
853
995
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
854
|
-
const encrypt =
|
|
996
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
997
|
+
? !!(context.metadata.encrypted)
|
|
998
|
+
: this.shouldEncrypt(encryptTarget);
|
|
855
999
|
const params = { payload, encrypt };
|
|
856
1000
|
try {
|
|
857
1001
|
if (isGroup) {
|
|
858
1002
|
params.group_id = channelId;
|
|
859
1003
|
const result = await this.callAndTrace('group.send', params);
|
|
860
1004
|
if (!result || !result.message_id) {
|
|
861
|
-
|
|
1005
|
+
const dispatchStatus = result?.message_dispatch?.status;
|
|
1006
|
+
if (dispatchStatus === 'debounced' || dispatchStatus === 'dispatched') {
|
|
1007
|
+
logger.info(`${this.logPrefix()} group.send ok (${dispatchStatus}): group=${channelId} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
logger.warn(`${this.logPrefix()} group.send returned no message_id: ${JSON.stringify(result)}`);
|
|
1011
|
+
}
|
|
862
1012
|
}
|
|
863
1013
|
else {
|
|
864
|
-
logger.info(
|
|
1014
|
+
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
865
1015
|
}
|
|
866
1016
|
}
|
|
867
1017
|
else {
|
|
868
1018
|
params.to = targetAid;
|
|
869
1019
|
const result = await this.callAndTrace('message.send', params);
|
|
870
1020
|
if (!result || !result.message_id) {
|
|
871
|
-
logger.warn(
|
|
1021
|
+
logger.warn(`${this.logPrefix()} message.send returned no message_id: ${JSON.stringify(result)}`);
|
|
872
1022
|
}
|
|
873
1023
|
else {
|
|
874
|
-
logger.info(
|
|
1024
|
+
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
875
1025
|
}
|
|
876
1026
|
}
|
|
877
1027
|
}
|
|
878
1028
|
catch (e) {
|
|
879
1029
|
if (encrypt && e instanceof E2EEError) {
|
|
880
1030
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
881
|
-
logger.warn(
|
|
1031
|
+
logger.warn(`${this.logPrefix()} E2EE send failed to ${channelId}, retrying plaintext: ${e}`);
|
|
882
1032
|
params.encrypt = false;
|
|
883
1033
|
try {
|
|
884
1034
|
if (isGroup) {
|
|
@@ -886,7 +1036,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
886
1036
|
const result = await this.client.call('group.send', params);
|
|
887
1037
|
this.trace('OUT', 'group.send.fallback.ok', { message_id: result?.message_id });
|
|
888
1038
|
if (!result || !result.message_id) {
|
|
889
|
-
logger.warn(
|
|
1039
|
+
logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
890
1040
|
}
|
|
891
1041
|
}
|
|
892
1042
|
else {
|
|
@@ -894,18 +1044,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
894
1044
|
const result = await this.client.call('message.send', params);
|
|
895
1045
|
this.trace('OUT', 'message.send.fallback.ok', { message_id: result?.message_id });
|
|
896
1046
|
if (!result || !result.message_id) {
|
|
897
|
-
logger.warn(
|
|
1047
|
+
logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
898
1048
|
}
|
|
899
1049
|
}
|
|
900
1050
|
}
|
|
901
1051
|
catch (e2) {
|
|
902
1052
|
this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
|
|
903
|
-
logger.error(
|
|
1053
|
+
logger.error(`${this.logPrefix()} Plaintext fallback also failed to ${channelId}: ${e2}`);
|
|
904
1054
|
}
|
|
905
1055
|
}
|
|
906
1056
|
else {
|
|
907
1057
|
this.trace('OUT', 'send.error', { channelId, error: String(e) });
|
|
908
|
-
logger.error(
|
|
1058
|
+
logger.error(`${this.logPrefix()} Send failed to ${channelId}: ${e}`);
|
|
909
1059
|
}
|
|
910
1060
|
}
|
|
911
1061
|
}
|
|
@@ -917,7 +1067,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
917
1067
|
* selector 使用 context: { type: 'task', id: taskId }
|
|
918
1068
|
* 存储键:group_id/peer_aid + sender_aid + context.type + context.id
|
|
919
1069
|
*/
|
|
920
|
-
async sendThought(channelId, taskId, payload) {
|
|
1070
|
+
async sendThought(channelId, taskId, payload, context) {
|
|
921
1071
|
if (!this.connected || !this.client)
|
|
922
1072
|
return;
|
|
923
1073
|
if (!taskId)
|
|
@@ -925,46 +1075,49 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
925
1075
|
// Multi-instance routing
|
|
926
1076
|
const colonIdx = channelId.indexOf(':');
|
|
927
1077
|
const targetId = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
1078
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1079
|
+
? !!(context.metadata.encrypted)
|
|
1080
|
+
: this.shouldEncrypt(targetId);
|
|
928
1081
|
const params = {
|
|
929
1082
|
context: { type: 'task', id: taskId },
|
|
930
1083
|
payload,
|
|
931
|
-
encrypt
|
|
1084
|
+
encrypt,
|
|
932
1085
|
};
|
|
933
1086
|
try {
|
|
934
1087
|
const stage = payload?.stage ?? 'unknown';
|
|
935
1088
|
if (this.isGroupId(channelId)) {
|
|
936
1089
|
params.group_id = targetId;
|
|
937
1090
|
await this.callAndTrace('group.thought.put', params);
|
|
938
|
-
logger.info(
|
|
1091
|
+
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt}`);
|
|
939
1092
|
}
|
|
940
1093
|
else {
|
|
941
1094
|
params.to = targetId;
|
|
942
1095
|
await this.callAndTrace('message.thought.put', params);
|
|
943
|
-
logger.info(
|
|
1096
|
+
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt}`);
|
|
944
1097
|
}
|
|
945
1098
|
}
|
|
946
1099
|
catch (e) {
|
|
947
1100
|
const err = e;
|
|
948
|
-
logger.debug(
|
|
1101
|
+
logger.debug(`${this.logPrefix()} thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
949
1102
|
}
|
|
950
1103
|
}
|
|
951
1104
|
async sendFile(channelId, filePath, context) {
|
|
952
1105
|
if (!this.connected || !this.client) {
|
|
953
|
-
logger.warn(
|
|
1106
|
+
logger.warn(`${this.logPrefix()} Cannot sendFile: not connected`);
|
|
954
1107
|
return;
|
|
955
1108
|
}
|
|
956
1109
|
const absPath = path.resolve(filePath);
|
|
957
1110
|
if (!fs.existsSync(absPath)) {
|
|
958
|
-
logger.warn(
|
|
1111
|
+
logger.warn(`${this.logPrefix()} sendFile: file not found: ${absPath}`);
|
|
959
1112
|
return;
|
|
960
1113
|
}
|
|
961
1114
|
const stat = fs.statSync(absPath);
|
|
962
1115
|
if (stat.size === 0) {
|
|
963
|
-
logger.warn(
|
|
1116
|
+
logger.warn(`${this.logPrefix()} sendFile: file is empty`);
|
|
964
1117
|
return;
|
|
965
1118
|
}
|
|
966
1119
|
if (stat.size > 10 * 1024 * 1024) {
|
|
967
|
-
logger.warn(
|
|
1120
|
+
logger.warn(`${this.logPrefix()} sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
968
1121
|
return;
|
|
969
1122
|
}
|
|
970
1123
|
const filename = path.basename(absPath);
|
|
@@ -1035,7 +1188,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1035
1188
|
filePayload.chat_id = channelId;
|
|
1036
1189
|
}
|
|
1037
1190
|
const encryptTarget = isGroup ? channelId : fileTargetAid;
|
|
1038
|
-
const encrypt =
|
|
1191
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1192
|
+
? !!(context.metadata.encrypted)
|
|
1193
|
+
: this.shouldEncrypt(encryptTarget);
|
|
1039
1194
|
const params = { payload: filePayload, encrypt };
|
|
1040
1195
|
try {
|
|
1041
1196
|
if (isGroup) {
|
|
@@ -1044,7 +1199,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1044
1199
|
const result = await this.client.call('group.send', params);
|
|
1045
1200
|
this.trace('OUT', 'group.send.file.ok', { message_id: result?.message_id });
|
|
1046
1201
|
if (!result || !result.message_id) {
|
|
1047
|
-
logger.warn(
|
|
1202
|
+
logger.warn(`${this.logPrefix()} group.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
1048
1203
|
}
|
|
1049
1204
|
}
|
|
1050
1205
|
else {
|
|
@@ -1053,7 +1208,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1053
1208
|
const result = await this.client.call('message.send', params);
|
|
1054
1209
|
this.trace('OUT', 'message.send.file.ok', { message_id: result?.message_id });
|
|
1055
1210
|
if (!result || !result.message_id) {
|
|
1056
|
-
logger.warn(
|
|
1211
|
+
logger.warn(`${this.logPrefix()} message.send.file returned no message_id: ${JSON.stringify(result)}`);
|
|
1057
1212
|
}
|
|
1058
1213
|
}
|
|
1059
1214
|
}
|
|
@@ -1064,14 +1219,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1064
1219
|
});
|
|
1065
1220
|
if (encrypt && sendErr instanceof E2EEError) {
|
|
1066
1221
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
1067
|
-
logger.warn(
|
|
1222
|
+
logger.warn(`${this.logPrefix()} E2EE sendFile failed to ${channelId}, retrying plaintext: ${sendErr}`);
|
|
1068
1223
|
params.encrypt = false;
|
|
1069
1224
|
if (isGroup) {
|
|
1070
1225
|
this.trace('OUT', 'group.send.file.fallback', params);
|
|
1071
1226
|
const result = await this.client.call('group.send', params);
|
|
1072
1227
|
this.trace('OUT', 'group.send.file.fallback.ok', { message_id: result?.message_id });
|
|
1073
1228
|
if (!result || !result.message_id) {
|
|
1074
|
-
logger.warn(
|
|
1229
|
+
logger.warn(`${this.logPrefix()} group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
1075
1230
|
}
|
|
1076
1231
|
}
|
|
1077
1232
|
else {
|
|
@@ -1079,7 +1234,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1079
1234
|
const result = await this.client.call('message.send', params);
|
|
1080
1235
|
this.trace('OUT', 'message.send.file.fallback.ok', { message_id: result?.message_id });
|
|
1081
1236
|
if (!result || !result.message_id) {
|
|
1082
|
-
logger.warn(
|
|
1237
|
+
logger.warn(`${this.logPrefix()} message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
|
|
1083
1238
|
}
|
|
1084
1239
|
}
|
|
1085
1240
|
}
|
|
@@ -1087,11 +1242,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1087
1242
|
throw sendErr;
|
|
1088
1243
|
}
|
|
1089
1244
|
}
|
|
1090
|
-
logger.info(
|
|
1245
|
+
logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
1091
1246
|
}
|
|
1092
1247
|
catch (e) {
|
|
1093
1248
|
this.trace('OUT', 'sendFile.error', { channelId, filePath, error: String(e) });
|
|
1094
|
-
logger.error(
|
|
1249
|
+
logger.error(`${this.logPrefix()} sendFile failed for ${channelId}: ${e}`);
|
|
1095
1250
|
}
|
|
1096
1251
|
}
|
|
1097
1252
|
acknowledge(messageId) {
|
|
@@ -1127,20 +1282,22 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1127
1282
|
payload.chat_id = channelId;
|
|
1128
1283
|
}
|
|
1129
1284
|
const encryptTarget = isGroup ? channelId : statusTargetAid;
|
|
1130
|
-
const encrypt =
|
|
1285
|
+
const encrypt = context?.metadata?.encrypted != null
|
|
1286
|
+
? !!(context.metadata.encrypted)
|
|
1287
|
+
: this.shouldEncrypt(encryptTarget);
|
|
1131
1288
|
const params = { payload, encrypt };
|
|
1132
1289
|
const sendWithFallback = (method) => {
|
|
1133
1290
|
this.client.call(method, params).catch(e => {
|
|
1134
1291
|
if (encrypt && e instanceof E2EEError) {
|
|
1135
1292
|
this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
|
|
1136
|
-
logger.warn(
|
|
1293
|
+
logger.warn(`${this.logPrefix()} E2EE status send failed to ${channelId}, retrying plaintext`);
|
|
1137
1294
|
params.encrypt = false;
|
|
1138
1295
|
this.client.call(method, params).catch(e2 => {
|
|
1139
|
-
logger.debug(
|
|
1296
|
+
logger.debug(`${this.logPrefix()} Processing status fallback failed: ${e2}`);
|
|
1140
1297
|
});
|
|
1141
1298
|
}
|
|
1142
1299
|
else {
|
|
1143
|
-
logger.debug(
|
|
1300
|
+
logger.debug(`${this.logPrefix()} Processing status failed: ${e}`);
|
|
1144
1301
|
}
|
|
1145
1302
|
});
|
|
1146
1303
|
};
|
|
@@ -1154,7 +1311,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1154
1311
|
this.trace('OUT', 'message.send.status', params);
|
|
1155
1312
|
sendWithFallback('message.send');
|
|
1156
1313
|
}
|
|
1157
|
-
|
|
1314
|
+
// 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
|
|
1315
|
+
const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
|
|
1316
|
+
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
1317
|
+
logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} encrypt=${encrypt} target=${targetLabel}`);
|
|
1158
1318
|
}
|
|
1159
1319
|
sendCustomPayload(channelId, payload) {
|
|
1160
1320
|
if (!this.client || !this.connected)
|
|
@@ -1184,7 +1344,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1184
1344
|
this.trace('OUT', 'message.send.custom.ok', { message_id: result?.message_id });
|
|
1185
1345
|
}).catch(e => {
|
|
1186
1346
|
this.trace('OUT', 'message.send.custom.error', { error: String(e) });
|
|
1187
|
-
logger.warn(
|
|
1347
|
+
logger.warn(`${this.logPrefix()} Custom payload failed: ${e}`);
|
|
1188
1348
|
});
|
|
1189
1349
|
}
|
|
1190
1350
|
async disconnect() {
|
|
@@ -1205,12 +1365,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1205
1365
|
this.client = null;
|
|
1206
1366
|
}
|
|
1207
1367
|
this.connected = false;
|
|
1208
|
-
logger.info(
|
|
1368
|
+
logger.info(`${this.logPrefix()} Disconnected`);
|
|
1209
1369
|
if (this.traceStream) {
|
|
1210
1370
|
this.traceStream.end();
|
|
1211
1371
|
this.traceStream = null;
|
|
1212
1372
|
}
|
|
1213
|
-
logger.info(
|
|
1373
|
+
logger.info(`${this.logPrefix()} Disconnected`);
|
|
1214
1374
|
}
|
|
1215
1375
|
// ── TS-layer reconnect (fallback when SDK auto_reconnect exhausted) ──
|
|
1216
1376
|
scheduleReconnect() {
|
|
@@ -1220,25 +1380,25 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1220
1380
|
return;
|
|
1221
1381
|
const delays = AUNChannel.RECONNECT_DELAYS;
|
|
1222
1382
|
if (this.reconnectAttempt >= delays.length) {
|
|
1223
|
-
logger.error(
|
|
1383
|
+
logger.error(`${this.logPrefix()} All ${delays.length} reconnect attempts exhausted, giving up`);
|
|
1224
1384
|
this.onChannelDown?.();
|
|
1225
1385
|
return;
|
|
1226
1386
|
}
|
|
1227
1387
|
const delay = delays[this.reconnectAttempt];
|
|
1228
1388
|
this.reconnectAttempt++;
|
|
1229
|
-
logger.info(
|
|
1389
|
+
logger.info(`${this.logPrefix()} Scheduling reconnect #${this.reconnectAttempt}/${delays.length} in ${delay}s`);
|
|
1230
1390
|
this.reconnectTimer = setTimeout(async () => {
|
|
1231
1391
|
this.reconnectTimer = null;
|
|
1232
1392
|
try {
|
|
1233
|
-
logger.info(
|
|
1393
|
+
logger.info(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} starting...`);
|
|
1234
1394
|
this.trace('OUT', 'reconnect.start', { attempt: this.reconnectAttempt });
|
|
1235
1395
|
await this.initClient();
|
|
1236
1396
|
this.trace('OUT', 'reconnect.ok', { attempt: this.reconnectAttempt });
|
|
1237
|
-
logger.info(
|
|
1397
|
+
logger.info(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} succeeded`);
|
|
1238
1398
|
}
|
|
1239
1399
|
catch (err) {
|
|
1240
1400
|
this.trace('OUT', 'reconnect.error', { attempt: this.reconnectAttempt, error: String(err) });
|
|
1241
|
-
logger.error(
|
|
1401
|
+
logger.error(`${this.logPrefix()} Reconnect #${this.reconnectAttempt} failed:`, err);
|
|
1242
1402
|
this.scheduleReconnect();
|
|
1243
1403
|
}
|
|
1244
1404
|
}, delay * 1000);
|
|
@@ -1314,7 +1474,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1314
1474
|
return info;
|
|
1315
1475
|
}
|
|
1316
1476
|
catch (e) {
|
|
1317
|
-
logger.debug(
|
|
1477
|
+
logger.debug(`${this.logPrefix()} fetchPeerInfo failed for ${aid}: ${e}`);
|
|
1318
1478
|
return { type: null }; // no agent.md → unknown
|
|
1319
1479
|
}
|
|
1320
1480
|
}
|
|
@@ -1367,7 +1527,7 @@ export class AUNChannelPlugin {
|
|
|
1367
1527
|
sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
|
|
1368
1528
|
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
1369
1529
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
1370
|
-
putThought: (id, taskId, payload) => channel.sendThought(id, taskId, payload),
|
|
1530
|
+
putThought: (id, taskId, payload, context) => channel.sendThought(id, taskId, payload, context),
|
|
1371
1531
|
_selfAid: () => channel.getStatus().aid,
|
|
1372
1532
|
_selfName: () => channel.getSelfName(),
|
|
1373
1533
|
};
|