evolclaw 2.1.2 → 2.3.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.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
@@ -1,28 +1,598 @@
1
- import { logger } from '../utils/logger.js';
1
+ import { AUNClient } from '@eleans/aun-core-node';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { logger, localTimestamp } from '../utils/logger.js';
5
+ import { normalizeChannelInstances } from '../config.js';
6
+ import { resolvePaths } from '../paths.js';
2
7
  export class AUNChannel {
3
8
  config;
9
+ client = null;
4
10
  messageHandler;
5
11
  connected = false;
12
+ traceStream = null;
13
+ trace(dir, event, data) {
14
+ if (!this.traceStream)
15
+ return;
16
+ const line = JSON.stringify({ ts: localTimestamp(), dir, event, data });
17
+ this.traceStream.write(line + '\n');
18
+ }
19
+ getShortAid(aid) {
20
+ if (!aid)
21
+ return undefined;
22
+ const trimmed = aid.trim();
23
+ if (!trimmed)
24
+ return undefined;
25
+ return trimmed.split('.')[0] || trimmed;
26
+ }
27
+ extractTextPayload(payload) {
28
+ if (typeof payload === 'string')
29
+ return payload;
30
+ if (payload && typeof payload === 'object') {
31
+ const text = payload.text;
32
+ if (typeof text === 'string')
33
+ return text;
34
+ return JSON.stringify(payload);
35
+ }
36
+ return '';
37
+ }
38
+ hasExplicitMention(text, target) {
39
+ const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40
+ return new RegExp(`(^|\\s)@${escaped}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`).test(text);
41
+ }
42
+ stripTriggerMentions(text, selfAid) {
43
+ let result = text;
44
+ if (selfAid) {
45
+ const escapedAid = selfAid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
+ result = result.replace(new RegExp(`(^|\\s)@${escapedAid}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`, 'g'), '$1');
47
+ }
48
+ result = result.replace(/(^|\s)@all(?=$|\s|[.,!?;:,。!?;:]|[\u4e00-\u9fff])/gi, '$1');
49
+ return result.replace(/[ \t]+/g, ' ').trim();
50
+ }
51
+ buildGroupReplyContext(taskId, senderAid, text) {
52
+ const replyContext = {};
53
+ if (taskId)
54
+ replyContext.threadId = taskId;
55
+ if (this.hasExplicitMention(text, 'all')) {
56
+ replyContext.mentionUserIds = ['all'];
57
+ }
58
+ else {
59
+ replyContext.mentionUserIds = [senderAid];
60
+ }
61
+ return replyContext;
62
+ }
63
+ acknowledgeImmediately(messageId, seq) {
64
+ if (seq != null && this.client) {
65
+ this.client.call('message.ack', { seq }).catch(e => {
66
+ logger.debug(`[AUN] Immediate ack failed: ${e}`);
67
+ });
68
+ }
69
+ if (messageId)
70
+ this.messageSeqMap.delete(messageId);
71
+ }
72
+ _aid;
73
+ seenMessages = new Map();
74
+ messageSeqMap = new Map(); // messageId → seq (for ack)
75
+ sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
76
+ // Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
77
+ intentionalDisconnect = false;
78
+ reconnectAttempt = 0;
79
+ reconnectTimer = null;
80
+ static RECONNECT_DELAYS = [60, 120, 300, 600]; // seconds
81
+ onChannelDown;
6
82
  constructor(config) {
7
83
  this.config = config;
84
+ if (config.aunTrace) {
85
+ const logPath = path.join(resolvePaths().logs, 'aun-trace.log');
86
+ this.traceStream = fs.createWriteStream(logPath, { flags: 'a' });
87
+ logger.info(`[AUN] Trace logging enabled: ${logPath}`);
88
+ }
8
89
  }
9
90
  async connect() {
10
- // TODO: 集成真实的 AUN SDK
11
- // 当前为占位符实现,确保接口一致性
12
- this.connected = true;
13
- logger.info(`[AUN] Connected as ${this.config.agentName}@${this.config.domain}`);
91
+ this.intentionalDisconnect = false;
92
+ this.reconnectAttempt = 0;
93
+ await this.initClient();
94
+ }
95
+ async initClient() {
96
+ // Clean up existing client if any
97
+ if (this.client) {
98
+ try {
99
+ await this.client.close();
100
+ }
101
+ catch { /* ignore */ }
102
+ this.client = null;
103
+ }
104
+ this.connected = false;
105
+ const aunPath = this.config.keystorePath || `${process.env.HOME || '~'}/.aun`;
106
+ const aidName = this.config.aid;
107
+ const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
108
+ // Gateway URL: 旧配置 gatewayUrl 优先,否则从 AID 推导
109
+ let gateway = this.config.gatewayUrl || '';
110
+ if (!gateway) {
111
+ const parts = aidName.split('.');
112
+ if (parts.length >= 3) {
113
+ const domain = parts.slice(1).join('.'); // alice.agentid.pub → agentid.pub
114
+ const port = this.config.gatewayPort || 443;
115
+ gateway = `wss://gateway.${domain}:${port}/aun`;
116
+ }
117
+ }
118
+ if (!gateway) {
119
+ logger.error('[AUN] Cannot derive gateway URL from AID');
120
+ return;
121
+ }
122
+ logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
123
+ // Create client with FileSecretStore (AES-256-GCM)
124
+ // 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
125
+ const rootCaPath = `${aunPath}/CA/root/root.crt`;
126
+ this.client = new AUNClient({
127
+ aun_path: aunPath,
128
+ root_ca_path: rootCaPath,
129
+ ...(encryptionSeed && { encryption_seed: encryptionSeed }),
130
+ });
131
+ // Set gateway URL (internal property, same as Python SDK)
132
+ this.client._gatewayUrl = gateway;
133
+ // Register event handlers before connecting
134
+ this.client.on('message.received', (data) => {
135
+ this.trace('IN', 'message.received', data);
136
+ const kind = (data && typeof data === 'object') ? data.kind ?? '' : '';
137
+ const keys = (data && typeof data === 'object') ? Object.keys(data).join(',') : typeof data;
138
+ logger.info(`[AUN][DIAG] message.received: kind=${kind} keys=${keys}`);
139
+ this.handleIncomingPrivateMessage(data);
140
+ });
141
+ this.client.on('group.message_created', (data) => {
142
+ this.trace('IN', 'group.message_created', data);
143
+ const gid = (data && typeof data === 'object') ? data.group_id ?? '' : '';
144
+ const sender = (data && typeof data === 'object') ? data.sender_aid ?? '' : '';
145
+ logger.info(`[AUN][DIAG] group.message_created: group_id=${gid} sender=${sender}`);
146
+ this.handleIncomingGroupMessage(data);
147
+ });
148
+ this.client.on('connection.state', (data) => {
149
+ this.trace('IN', 'connection.state', data);
150
+ this.handleConnectionState(data);
151
+ });
152
+ // Authenticate
153
+ let accessToken;
154
+ try {
155
+ logger.info(`[AUN] Authenticating as ${aidName}...`);
156
+ const auth = await this.client.auth.authenticate(aidName ? { aid: aidName } : undefined);
157
+ this.trace('IN', 'auth.result', { aid: auth.aid, gateway: auth.gateway, hasToken: !!auth.access_token });
158
+ accessToken = auth.access_token;
159
+ const resolvedGateway = auth.gateway || gateway;
160
+ this.client._gatewayUrl = resolvedGateway;
161
+ logger.info(`[AUN] Authenticated as ${auth.aid ?? '?'}, gateway=${resolvedGateway}`);
162
+ }
163
+ catch (e) {
164
+ const errMsg = e.message || String(e);
165
+ const errName = e.constructor?.name || 'Error';
166
+ logger.error(`[AUN] Authentication failed (${errName}): ${errMsg}`);
167
+ if (e.stack)
168
+ logger.debug(`[AUN] Auth stack: ${e.stack}`);
169
+ // Fallback: try direct token from env/config (legacy)
170
+ accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
171
+ if (!accessToken) {
172
+ logger.error(`[AUN] No accessToken fallback available, AUN channel disabled`);
173
+ return;
174
+ }
175
+ logger.warn(`[AUN] Using accessToken fallback`);
176
+ }
177
+ // Connect (SDK auto_reconnect handles transient failures)
178
+ try {
179
+ await this.client.connect({ access_token: accessToken, gateway: this.client._gatewayUrl }, { auto_reconnect: true, retry: { max_attempts: 5, initial_delay: 1.0, max_delay: 30.0 } });
180
+ this._aid = this.client.aid ?? undefined;
181
+ this.connected = true;
182
+ this.reconnectAttempt = 0;
183
+ logger.info(`[AUN] Connected as ${this._aid}`);
184
+ }
185
+ catch (e) {
186
+ logger.error(`[AUN] Connection failed: ${e}`);
187
+ return;
188
+ }
189
+ }
190
+ // ── Event handlers ──────────────────────────────────────────
191
+ async handleIncomingPrivateMessage(data) {
192
+ if (!data || typeof data !== 'object')
193
+ return;
194
+ const msg = data;
195
+ const fromAid = msg.from ?? '';
196
+ const payload = msg.payload ?? '';
197
+ const text = this.extractTextPayload(payload);
198
+ const taskId = msg.task_id;
199
+ const messageId = msg.message_id ?? '';
200
+ const seq = msg.seq;
201
+ // Detect @mentions
202
+ const mentions = [];
203
+ if (this._aid && text.includes(`@${this._aid}`)) {
204
+ mentions.push(this._aid);
205
+ }
206
+ this.dispatchMessage({
207
+ channelId: fromAid,
208
+ userId: fromAid,
209
+ text,
210
+ chatType: 'private',
211
+ messageId,
212
+ seq,
213
+ taskId,
214
+ mentions,
215
+ });
216
+ }
217
+ async handleIncomingGroupMessage(data) {
218
+ if (!data || typeof data !== 'object')
219
+ return;
220
+ const msg = data;
221
+ const groupId = msg.group_id ?? '';
222
+ const senderAid = msg.sender_aid ?? msg.from ?? '';
223
+ const payload = msg.payload ?? '';
224
+ const text = this.extractTextPayload(payload);
225
+ const taskId = msg.task_id;
226
+ const messageId = msg.message_id ?? '';
227
+ const seq = msg.seq;
228
+ logger.info(`[AUN][DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
229
+ if (!groupId || !senderAid) {
230
+ this.acknowledgeImmediately(messageId, seq);
231
+ return;
232
+ }
233
+ if (this._aid && senderAid === this._aid) {
234
+ this.acknowledgeImmediately(messageId, seq);
235
+ return;
236
+ }
237
+ const mentionedSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
238
+ const mentionedAll = this.hasExplicitMention(text, 'all');
239
+ if (!mentionedSelf && !mentionedAll) {
240
+ this.acknowledgeImmediately(messageId, seq);
241
+ return;
242
+ }
243
+ const strippedText = this.stripTriggerMentions(text, this._aid);
244
+ if (!strippedText) {
245
+ this.acknowledgeImmediately(messageId, seq);
246
+ return;
247
+ }
248
+ const mentions = mentionedAll ? ['all'] : (this._aid ? [this._aid] : []);
249
+ this.dispatchMessage({
250
+ channelId: groupId,
251
+ userId: senderAid,
252
+ peerName: this.getShortAid(senderAid),
253
+ text: strippedText,
254
+ chatType: 'group',
255
+ messageId,
256
+ seq,
257
+ taskId,
258
+ mentions,
259
+ replyContext: this.buildGroupReplyContext(taskId, senderAid, text),
260
+ });
261
+ }
262
+ dispatchMessage(event) {
263
+ // Dedup
264
+ if (event.messageId) {
265
+ if (this.seenMessages.has(event.messageId))
266
+ return;
267
+ this.seenMessages.set(event.messageId, Date.now());
268
+ setTimeout(() => this.seenMessages.delete(event.messageId), 5 * 60 * 1000);
269
+ // Track seq for acknowledge
270
+ if (event.seq != null) {
271
+ this.messageSeqMap.set(event.messageId, event.seq);
272
+ }
273
+ }
274
+ if (!this.messageHandler)
275
+ return;
276
+ const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
277
+ // Use caller-supplied replyContext (group path builds mentionUserIds);
278
+ // fall back to simple threadId-only context for private messages
279
+ let replyContext = event.replyContext;
280
+ if (!replyContext && event.taskId) {
281
+ replyContext = { threadId: event.taskId };
282
+ }
283
+ this.messageHandler({
284
+ channelId: event.channelId || '',
285
+ content: event.text || '',
286
+ chatType: event.chatType,
287
+ peerId: event.userId || event.channelId || '',
288
+ peerName: event.peerName,
289
+ messageId: event.messageId,
290
+ threadId: event.taskId,
291
+ mentions: mentionObjects,
292
+ replyContext,
293
+ }).catch(err => {
294
+ logger.error('[AUN] Message handler error:', err);
295
+ });
14
296
  }
297
+ handleConnectionState(data) {
298
+ if (!data || typeof data !== 'object')
299
+ return;
300
+ const state = data.state ?? '';
301
+ if (state === 'connected') {
302
+ this.connected = true;
303
+ this.reconnectAttempt = 0;
304
+ logger.info('[AUN] Connected');
305
+ }
306
+ else if (state === 'disconnected') {
307
+ this.connected = false;
308
+ logger.warn(`[AUN] Disconnected: ${data.error ?? 'unknown'}`);
309
+ }
310
+ else if (state === 'reconnecting') {
311
+ logger.info(`[AUN] SDK reconnecting (attempt ${data.attempt})`);
312
+ }
313
+ else if (state === 'terminal_failed') {
314
+ this.connected = false;
315
+ logger.error(`[AUN] Terminal failure: ${data.error ?? 'unknown'}`);
316
+ // SDK auto_reconnect exhausted; fall back to TS-layer reconnect
317
+ if (!this.intentionalDisconnect) {
318
+ this.scheduleReconnect();
319
+ }
320
+ }
321
+ }
322
+ // ── Public API (same interface as before) ───────────────────
15
323
  onMessage(handler) {
16
324
  this.messageHandler = handler;
17
325
  }
18
- async sendMessage(sessionId, content) {
19
- if (!this.connected)
20
- throw new Error('AUN not connected');
21
- // TODO: 实现真实的消息发送
22
- logger.debug(`[AUN] Send to ${sessionId}: ${content.slice(0, 50)}...`);
326
+ async sendMessage(channelId, text, context) {
327
+ if (!this.connected || !this.client) {
328
+ logger.warn('[AUN] Cannot send: not connected');
329
+ return;
330
+ }
331
+ if (!text?.trim()) {
332
+ logger.warn('[AUN] Attempted to send empty message, skipping');
333
+ return;
334
+ }
335
+ let finalText = text;
336
+ // 多轮工具调用后的最终回复:仅在已有中间消息时添加前缀
337
+ if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
338
+ finalText = '最终回复\n' + text;
339
+ }
340
+ this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
341
+ // Render outbound mentions for group sends
342
+ if (channelId.startsWith('grp_') && context?.mentionUserIds?.length) {
343
+ const mentionPrefix = context.mentionUserIds.includes('all')
344
+ ? '@all '
345
+ : context.mentionUserIds.map(id => `@${id}`).join(' ') + ' ';
346
+ finalText = mentionPrefix + finalText;
347
+ }
348
+ const params = { payload: { text: finalText }, encrypt: true };
349
+ if (context?.threadId)
350
+ params.task_id = context.threadId;
351
+ try {
352
+ if (channelId.startsWith('grp_')) {
353
+ params.group_id = channelId;
354
+ this.trace('OUT', 'group.send', params);
355
+ await this.client.call('group.send', params);
356
+ }
357
+ else {
358
+ params.to = channelId;
359
+ this.trace('OUT', 'message.send', params);
360
+ await this.client.call('message.send', params);
361
+ }
362
+ }
363
+ catch (e) {
364
+ this.trace('OUT', 'send.error', { channelId, error: String(e) });
365
+ logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
366
+ }
367
+ }
368
+ acknowledge(messageId) {
369
+ const seq = this.messageSeqMap.get(messageId);
370
+ if (seq != null && this.client) {
371
+ this.client.call('message.ack', { seq }).catch(e => {
372
+ logger.debug(`[AUN] Ack failed: ${e}`);
373
+ });
374
+ this.messageSeqMap.delete(messageId);
375
+ }
376
+ }
377
+ sendProcessingStatus(channelId, status, sessionId, context) {
378
+ if (status === 'start')
379
+ this.sentCount.delete(channelId); // 新任务开始,重置计数
380
+ if (!this.client || !this.connected)
381
+ return;
382
+ const payload = {
383
+ type: 'processing',
384
+ status,
385
+ sessionId,
386
+ timestamp: Math.floor(Date.now() / 1000),
387
+ };
388
+ const params = {
389
+ payload,
390
+ encrypt: true, persist: false,
391
+ };
392
+ if (context?.threadId)
393
+ params.task_id = context.threadId;
394
+ if (channelId.startsWith('grp_')) {
395
+ params.group_id = channelId;
396
+ this.trace('OUT', 'group.send.status', params);
397
+ this.client.call('group.send', params).catch(e => {
398
+ logger.debug(`[AUN] Processing status failed: ${e}`);
399
+ });
400
+ }
401
+ else {
402
+ params.to = channelId;
403
+ this.trace('OUT', 'message.send.status', params);
404
+ this.client.call('message.send', params).catch(e => {
405
+ logger.debug(`[AUN] Processing status failed: ${e}`);
406
+ });
407
+ }
408
+ }
409
+ sendCustomPayload(channelId, payload) {
410
+ if (!this.client || !this.connected)
411
+ return;
412
+ // SDK 0.3.0 E2EE requires payload to be an object
413
+ let payloadObj;
414
+ try {
415
+ const parsed = JSON.parse(payload);
416
+ payloadObj = (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
417
+ ? parsed : { text: payload };
418
+ }
419
+ catch {
420
+ payloadObj = { text: payload };
421
+ }
422
+ const sendParams = {
423
+ to: channelId, payload: payloadObj,
424
+ encrypt: true, persist: false,
425
+ };
426
+ this.trace('OUT', 'message.send.custom', sendParams);
427
+ this.client.call('message.send', sendParams).catch(e => {
428
+ logger.debug(`[AUN] Custom payload failed: ${e}`);
429
+ });
23
430
  }
24
431
  async disconnect() {
432
+ this.intentionalDisconnect = true;
433
+ if (this.reconnectTimer) {
434
+ clearTimeout(this.reconnectTimer);
435
+ this.reconnectTimer = null;
436
+ }
437
+ if (this.client) {
438
+ try {
439
+ await this.client.close();
440
+ }
441
+ catch { /* ignore */ }
442
+ this.client = null;
443
+ }
25
444
  this.connected = false;
445
+ if (this.traceStream) {
446
+ this.traceStream.end();
447
+ this.traceStream = null;
448
+ }
26
449
  logger.info('[AUN] Disconnected');
27
450
  }
451
+ // ── TS-layer reconnect (fallback when SDK auto_reconnect exhausted) ──
452
+ scheduleReconnect() {
453
+ if (this.intentionalDisconnect)
454
+ return;
455
+ if (this.reconnectTimer)
456
+ return;
457
+ const delays = AUNChannel.RECONNECT_DELAYS;
458
+ if (this.reconnectAttempt >= delays.length) {
459
+ logger.error(`[AUN] All ${delays.length} reconnect attempts exhausted, giving up`);
460
+ this.onChannelDown?.();
461
+ return;
462
+ }
463
+ const delay = delays[this.reconnectAttempt];
464
+ this.reconnectAttempt++;
465
+ logger.info(`[AUN] Scheduling reconnect #${this.reconnectAttempt}/${delays.length} in ${delay}s`);
466
+ this.reconnectTimer = setTimeout(async () => {
467
+ this.reconnectTimer = null;
468
+ try {
469
+ logger.info(`[AUN] Reconnect #${this.reconnectAttempt} starting...`);
470
+ await this.initClient();
471
+ logger.info(`[AUN] Reconnect #${this.reconnectAttempt} succeeded`);
472
+ }
473
+ catch (err) {
474
+ logger.error(`[AUN] Reconnect #${this.reconnectAttempt} failed:`, err);
475
+ this.scheduleReconnect();
476
+ }
477
+ }, delay * 1000);
478
+ }
479
+ /** Manually trigger reconnect (e.g. from /check reconnect command) */
480
+ async reconnect() {
481
+ if (this.connected)
482
+ return '已连接,无需重连';
483
+ if (this.reconnectTimer) {
484
+ clearTimeout(this.reconnectTimer);
485
+ this.reconnectTimer = null;
486
+ }
487
+ this.reconnectAttempt = 0;
488
+ try {
489
+ await this.initClient();
490
+ return `重连成功 (${this._aid})`;
491
+ }
492
+ catch (err) {
493
+ this.scheduleReconnect();
494
+ return `重连失败: ${err},已安排自动重试`;
495
+ }
496
+ }
497
+ /** Set callback for when all reconnect attempts are exhausted */
498
+ setOnChannelDown(callback) {
499
+ this.onChannelDown = callback;
500
+ }
501
+ /** Get current connection status */
502
+ getStatus() {
503
+ return {
504
+ connected: this.connected,
505
+ aid: this._aid,
506
+ reconnectAttempt: this.reconnectAttempt,
507
+ maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
508
+ };
509
+ }
510
+ }
511
+ // Plugin implementation
512
+ export class AUNChannelPlugin {
513
+ name = 'aun';
514
+ isEnabled(config) {
515
+ const raw = config.channels?.aun;
516
+ if (!raw)
517
+ return false;
518
+ if (Array.isArray(raw)) {
519
+ return raw.some(inst => inst.enabled !== false && !!inst.aid);
520
+ }
521
+ return raw.enabled !== false && !!raw.aid;
522
+ }
523
+ async createChannels(config) {
524
+ const instances = normalizeChannelInstances(config.channels?.aun, 'aun');
525
+ const result = [];
526
+ for (const inst of instances) {
527
+ if (inst.enabled === false || !inst.aid)
528
+ continue;
529
+ const channel = new AUNChannel({
530
+ aid: inst.aid,
531
+ keystorePath: inst.keystorePath,
532
+ gatewayPort: inst.gatewayPort,
533
+ gatewayUrl: inst.gatewayUrl,
534
+ accessToken: inst.accessToken,
535
+ flushDelay: inst.flushDelay,
536
+ encryptionSeed: inst.encryptionSeed,
537
+ aunTrace: config.debug?.aunTrace,
538
+ });
539
+ const adapter = {
540
+ channelName: inst.name,
541
+ sendText: (id, text, context) => channel.sendMessage(id, text, context),
542
+ acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
543
+ sendProcessingStatus: (id, status, sessionId, context) => channel.sendProcessingStatus(id, status, sessionId, context),
544
+ sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
545
+ };
546
+ const policy = {
547
+ canSwitchProject: (chatType, identity) => identity === 'owner',
548
+ canListProjects: (chatType, identity) => identity === 'owner',
549
+ canCreateSession: (chatType, identity) => true,
550
+ canDeleteSession: (chatType, identity) => true,
551
+ canImportCliSession: (chatType, identity) => identity === 'owner',
552
+ messagePrefix: () => '',
553
+ showMiddleResult: (chatType, identity) => {
554
+ const mode = inst.showActivities ?? config.showActivities ?? 'all';
555
+ if (mode === 'none')
556
+ return false;
557
+ if (mode === 'dm-only')
558
+ return chatType === 'private';
559
+ if (mode === 'owner-dm-only')
560
+ return chatType === 'private' && identity === 'owner';
561
+ return true;
562
+ },
563
+ showIdleMonitor: (chatType, identity) => {
564
+ const mode = inst.showActivities ?? config.showActivities ?? 'all';
565
+ if (mode === 'none')
566
+ return false;
567
+ if (mode === 'dm-only')
568
+ return chatType === 'private';
569
+ if (mode === 'owner-dm-only')
570
+ return chatType === 'private' && identity === 'owner';
571
+ return true;
572
+ },
573
+ accumulateErrors: (chatType, identity) => true,
574
+ };
575
+ const options = {
576
+ flushDelay: inst.flushDelay ?? 3,
577
+ fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
578
+ };
579
+ result.push({
580
+ channelType: 'aun',
581
+ adapter,
582
+ channel,
583
+ policy,
584
+ options,
585
+ connect: () => channel.connect(),
586
+ disconnect: () => channel.disconnect(),
587
+ });
588
+ }
589
+ return result;
590
+ }
591
+ async createChannel(config) {
592
+ const instances = await this.createChannels(config);
593
+ if (instances.length === 0) {
594
+ throw new Error('AUN config missing (aid required, e.g. "mybot.agentid.pub")');
595
+ }
596
+ return instances[0];
597
+ }
28
598
  }