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.
- package/README.md +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → cross-platform.js} +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
package/dist/channels/aun.js
CHANGED
|
@@ -1,28 +1,598 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
this.
|
|
13
|
-
|
|
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(
|
|
19
|
-
if (!this.connected)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
}
|