evolclaw 2.1.2 → 2.2.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 (42) hide show
  1. package/README.md +10 -3
  2. package/data/evolclaw.sample.json +9 -1
  3. package/dist/agents/claude-runner.js +612 -0
  4. package/dist/agents/codex-runner.js +310 -0
  5. package/dist/channels/aun.js +416 -9
  6. package/dist/channels/feishu.js +397 -104
  7. package/dist/channels/wechat.js +84 -2
  8. package/dist/cli.js +427 -126
  9. package/dist/config.js +102 -4
  10. package/dist/core/adapters/claude-session-file-adapter.js +144 -0
  11. package/dist/core/adapters/codex-session-file-adapter.js +196 -0
  12. package/dist/core/agent-loader.js +39 -0
  13. package/dist/core/channel-loader.js +60 -0
  14. package/dist/core/command-handler.js +908 -304
  15. package/dist/core/event-bus.js +32 -0
  16. package/dist/core/ipc-server.js +71 -0
  17. package/dist/core/message-bridge.js +187 -0
  18. package/dist/core/message-processor.js +370 -227
  19. package/dist/core/message-queue.js +153 -29
  20. package/dist/core/permission.js +58 -0
  21. package/dist/core/session-file-adapter.js +7 -0
  22. package/dist/core/session-manager.js +567 -205
  23. package/dist/core/stats-collector.js +86 -0
  24. package/dist/index.js +309 -243
  25. package/dist/paths.js +1 -0
  26. package/dist/utils/init-feishu.js +2 -0
  27. package/dist/utils/init-wechat.js +2 -0
  28. package/dist/utils/init.js +285 -53
  29. package/dist/utils/ipc-client.js +36 -0
  30. package/dist/utils/migrate-project.js +122 -0
  31. package/dist/utils/{permission.js → permission-utils.js} +31 -3
  32. package/dist/utils/rich-content-renderer.js +228 -0
  33. package/dist/utils/session-file-health.js +11 -34
  34. package/dist/utils/stream-debouncer.js +122 -0
  35. package/dist/utils/stream-idle-monitor.js +1 -1
  36. package/package.json +3 -1
  37. package/dist/core/agent-runner.js +0 -348
  38. package/dist/core/message-stream.js +0 -59
  39. package/dist/index.js.bak +0 -340
  40. package/dist/utils/markdown-to-feishu.js +0 -94
  41. /package/dist/utils/{platform.js → cross-platform.js} +0 -0
  42. /package/dist/{core → utils}/message-cache.js +0 -0
@@ -1,28 +1,435 @@
1
+ import { AUNClient } from '@aun/core-node';
1
2
  import { logger } from '../utils/logger.js';
2
3
  export class AUNChannel {
3
4
  config;
5
+ client = null;
4
6
  messageHandler;
5
7
  connected = false;
8
+ _aid;
9
+ seenMessages = new Map();
10
+ messageSeqMap = new Map(); // messageId → seq (for ack)
11
+ sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
12
+ // Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
13
+ intentionalDisconnect = false;
14
+ reconnectAttempt = 0;
15
+ reconnectTimer = null;
16
+ static RECONNECT_DELAYS = [60, 120, 300, 600]; // seconds
17
+ onChannelDown;
6
18
  constructor(config) {
7
19
  this.config = config;
8
20
  }
9
21
  async connect() {
10
- // TODO: 集成真实的 AUN SDK
11
- // 当前为占位符实现,确保接口一致性
12
- this.connected = true;
13
- logger.info(`[AUN] Connected as ${this.config.agentName}@${this.config.domain}`);
22
+ this.intentionalDisconnect = false;
23
+ this.reconnectAttempt = 0;
24
+ await this.initClient();
14
25
  }
26
+ async initClient() {
27
+ // Clean up existing client if any
28
+ if (this.client) {
29
+ try {
30
+ await this.client.close();
31
+ }
32
+ catch { /* ignore */ }
33
+ this.client = null;
34
+ }
35
+ this.connected = false;
36
+ const aunPath = this.config.keystorePath || `${process.env.HOME || '~'}/.aun`;
37
+ const aidName = this.config.aid;
38
+ const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
39
+ // Gateway URL: 旧配置 gatewayUrl 优先,否则从 AID 推导
40
+ let gateway = this.config.gatewayUrl || '';
41
+ if (!gateway) {
42
+ const parts = aidName.split('.');
43
+ if (parts.length >= 3) {
44
+ const domain = parts.slice(1).join('.'); // alice.agentid.pub → agentid.pub
45
+ const port = this.config.gatewayPort || 443;
46
+ gateway = `wss://gateway.${domain}:${port}/aun`;
47
+ }
48
+ }
49
+ if (!gateway) {
50
+ logger.error('[AUN] Cannot derive gateway URL from AID');
51
+ return;
52
+ }
53
+ logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
54
+ // Create client with FileSecretStore (AES-256-GCM)
55
+ // 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
56
+ this.client = new AUNClient({
57
+ aun_path: aunPath,
58
+ ...(encryptionSeed && { encryption_seed: encryptionSeed }),
59
+ });
60
+ // Set gateway URL (internal property, same as Python SDK)
61
+ this.client._gatewayUrl = gateway;
62
+ // Register event handlers before connecting
63
+ this.client.on('message.received', (data) => this.handleIncomingPrivateMessage(data));
64
+ this.client.on('group.message_created', (data) => this.handleIncomingGroupMessage(data));
65
+ this.client.on('connection.state', (data) => this.handleConnectionState(data));
66
+ // Authenticate
67
+ let accessToken;
68
+ try {
69
+ logger.info(`[AUN] Authenticating as ${aidName}...`);
70
+ const auth = await this.client.auth.authenticate(aidName ? { aid: aidName } : undefined);
71
+ accessToken = auth.access_token;
72
+ const resolvedGateway = auth.gateway || gateway;
73
+ this.client._gatewayUrl = resolvedGateway;
74
+ logger.info(`[AUN] Authenticated as ${auth.aid ?? '?'}, gateway=${resolvedGateway}`);
75
+ }
76
+ catch (e) {
77
+ const errMsg = e.message || String(e);
78
+ const errName = e.constructor?.name || 'Error';
79
+ logger.error(`[AUN] Authentication failed (${errName}): ${errMsg}`);
80
+ if (e.stack)
81
+ logger.debug(`[AUN] Auth stack: ${e.stack}`);
82
+ // Fallback: try direct token from env/config (legacy)
83
+ accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
84
+ if (!accessToken) {
85
+ logger.error(`[AUN] No accessToken fallback available, AUN channel disabled`);
86
+ return;
87
+ }
88
+ logger.warn(`[AUN] Using accessToken fallback`);
89
+ }
90
+ // Connect (SDK auto_reconnect handles transient failures)
91
+ try {
92
+ 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 } });
93
+ this._aid = this.client.aid ?? undefined;
94
+ this.connected = true;
95
+ this.reconnectAttempt = 0;
96
+ logger.info(`[AUN] Connected as ${this._aid}`);
97
+ }
98
+ catch (e) {
99
+ logger.error(`[AUN] Connection failed: ${e}`);
100
+ return;
101
+ }
102
+ }
103
+ // ── Event handlers ──────────────────────────────────────────
104
+ async handleIncomingPrivateMessage(data) {
105
+ if (!data || typeof data !== 'object')
106
+ return;
107
+ const msg = data;
108
+ const fromAid = msg.from ?? '';
109
+ const payload = msg.payload ?? '';
110
+ const text = typeof payload === 'string' ? payload : (payload ? JSON.stringify(payload) : '');
111
+ const taskId = msg.task_id;
112
+ const messageId = msg.message_id ?? '';
113
+ const seq = msg.seq;
114
+ // Detect @mentions
115
+ const mentions = [];
116
+ if (this._aid && text.includes(`@${this._aid}`)) {
117
+ mentions.push(this._aid);
118
+ }
119
+ this.dispatchMessage({
120
+ channelId: fromAid,
121
+ userId: fromAid,
122
+ text,
123
+ chatType: 'private',
124
+ messageId,
125
+ seq,
126
+ taskId,
127
+ mentions,
128
+ });
129
+ }
130
+ async handleIncomingGroupMessage(data) {
131
+ if (!data || typeof data !== 'object')
132
+ return;
133
+ const msg = data;
134
+ const groupId = msg.group_id ?? '';
135
+ const senderAid = msg.sender_aid ?? msg.from ?? '';
136
+ const payload = msg.payload ?? '';
137
+ const text = typeof payload === 'string' ? payload : (payload ? JSON.stringify(payload) : '');
138
+ const taskId = msg.task_id;
139
+ const messageId = msg.message_id ?? '';
140
+ const seq = msg.seq;
141
+ // Detect @mentions
142
+ const mentions = [];
143
+ if (this._aid && text.includes(`@${this._aid}`)) {
144
+ mentions.push(this._aid);
145
+ }
146
+ this.dispatchMessage({
147
+ channelId: groupId,
148
+ userId: senderAid,
149
+ text,
150
+ chatType: 'group',
151
+ messageId,
152
+ seq,
153
+ taskId,
154
+ mentions,
155
+ });
156
+ }
157
+ dispatchMessage(event) {
158
+ // Dedup
159
+ if (event.messageId) {
160
+ if (this.seenMessages.has(event.messageId))
161
+ return;
162
+ this.seenMessages.set(event.messageId, Date.now());
163
+ setTimeout(() => this.seenMessages.delete(event.messageId), 5 * 60 * 1000);
164
+ // Track seq for acknowledge
165
+ if (event.seq != null) {
166
+ this.messageSeqMap.set(event.messageId, event.seq);
167
+ }
168
+ }
169
+ if (!this.messageHandler)
170
+ return;
171
+ const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
172
+ let replyContext;
173
+ if (event.taskId) {
174
+ replyContext = { threadId: event.taskId };
175
+ }
176
+ this.messageHandler({
177
+ channelId: event.channelId || '',
178
+ content: event.text || '',
179
+ chatType: event.chatType,
180
+ peerId: event.userId || event.channelId || '',
181
+ messageId: event.messageId,
182
+ threadId: event.taskId,
183
+ mentions: mentionObjects,
184
+ replyContext,
185
+ }).catch(err => {
186
+ logger.error('[AUN] Message handler error:', err);
187
+ });
188
+ }
189
+ handleConnectionState(data) {
190
+ if (!data || typeof data !== 'object')
191
+ return;
192
+ const state = data.state ?? '';
193
+ if (state === 'connected') {
194
+ this.connected = true;
195
+ this.reconnectAttempt = 0;
196
+ logger.info('[AUN] Connected');
197
+ }
198
+ else if (state === 'disconnected') {
199
+ this.connected = false;
200
+ logger.warn(`[AUN] Disconnected: ${data.error ?? 'unknown'}`);
201
+ }
202
+ else if (state === 'reconnecting') {
203
+ logger.info(`[AUN] SDK reconnecting (attempt ${data.attempt})`);
204
+ }
205
+ else if (state === 'terminal_failed') {
206
+ this.connected = false;
207
+ logger.error(`[AUN] Terminal failure: ${data.error ?? 'unknown'}`);
208
+ // SDK auto_reconnect exhausted; fall back to TS-layer reconnect
209
+ if (!this.intentionalDisconnect) {
210
+ this.scheduleReconnect();
211
+ }
212
+ }
213
+ }
214
+ // ── Public API (same interface as before) ───────────────────
15
215
  onMessage(handler) {
16
216
  this.messageHandler = handler;
17
217
  }
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)}...`);
218
+ async sendMessage(channelId, text, context) {
219
+ if (!this.connected || !this.client) {
220
+ logger.warn('[AUN] Cannot send: not connected');
221
+ return;
222
+ }
223
+ if (!text?.trim()) {
224
+ logger.warn('[AUN] Attempted to send empty message, skipping');
225
+ return;
226
+ }
227
+ let finalText = text;
228
+ // 多轮工具调用后的最终回复:仅在已有中间消息时添加前缀
229
+ if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
230
+ finalText = '最终回复\n' + text;
231
+ }
232
+ this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
233
+ const params = { payload: finalText, encrypt: true };
234
+ if (context?.threadId)
235
+ params.task_id = context.threadId;
236
+ try {
237
+ if (channelId.startsWith('grp_')) {
238
+ params.group_id = channelId;
239
+ await this.client.call('group.send', params);
240
+ }
241
+ else {
242
+ params.to = channelId;
243
+ await this.client.call('message.send', params);
244
+ }
245
+ }
246
+ catch (e) {
247
+ logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
248
+ }
249
+ }
250
+ acknowledge(messageId) {
251
+ const seq = this.messageSeqMap.get(messageId);
252
+ if (seq != null && this.client) {
253
+ this.client.call('message.ack', { seq }).catch(e => {
254
+ logger.debug(`[AUN] Ack failed: ${e}`);
255
+ });
256
+ this.messageSeqMap.delete(messageId);
257
+ }
258
+ }
259
+ sendProcessingStatus(channelId, status, sessionId, context) {
260
+ if (status === 'start')
261
+ this.sentCount.delete(channelId); // 新任务开始,重置计数
262
+ if (!this.client || !this.connected)
263
+ return;
264
+ const payload = JSON.stringify({
265
+ type: 'processing',
266
+ status,
267
+ sessionId,
268
+ timestamp: Math.floor(Date.now() / 1000),
269
+ });
270
+ const params = {
271
+ to: channelId, payload,
272
+ encrypt: true, persist: false,
273
+ };
274
+ if (context?.threadId)
275
+ params.task_id = context.threadId;
276
+ this.client.call('message.send', params).catch(e => {
277
+ logger.debug(`[AUN] Processing status failed: ${e}`);
278
+ });
279
+ }
280
+ sendCustomPayload(channelId, payload) {
281
+ if (!this.client || !this.connected)
282
+ return;
283
+ this.client.call('message.send', {
284
+ to: channelId, payload,
285
+ encrypt: true, persist: false,
286
+ }).catch(e => {
287
+ logger.debug(`[AUN] Custom payload failed: ${e}`);
288
+ });
23
289
  }
24
290
  async disconnect() {
291
+ this.intentionalDisconnect = true;
292
+ if (this.reconnectTimer) {
293
+ clearTimeout(this.reconnectTimer);
294
+ this.reconnectTimer = null;
295
+ }
296
+ if (this.client) {
297
+ try {
298
+ await this.client.close();
299
+ }
300
+ catch { /* ignore */ }
301
+ this.client = null;
302
+ }
25
303
  this.connected = false;
26
304
  logger.info('[AUN] Disconnected');
27
305
  }
306
+ // ── TS-layer reconnect (fallback when SDK auto_reconnect exhausted) ──
307
+ scheduleReconnect() {
308
+ if (this.intentionalDisconnect)
309
+ return;
310
+ if (this.reconnectTimer)
311
+ return;
312
+ const delays = AUNChannel.RECONNECT_DELAYS;
313
+ if (this.reconnectAttempt >= delays.length) {
314
+ logger.error(`[AUN] All ${delays.length} reconnect attempts exhausted, giving up`);
315
+ this.onChannelDown?.();
316
+ return;
317
+ }
318
+ const delay = delays[this.reconnectAttempt];
319
+ this.reconnectAttempt++;
320
+ logger.info(`[AUN] Scheduling reconnect #${this.reconnectAttempt}/${delays.length} in ${delay}s`);
321
+ this.reconnectTimer = setTimeout(async () => {
322
+ this.reconnectTimer = null;
323
+ try {
324
+ logger.info(`[AUN] Reconnect #${this.reconnectAttempt} starting...`);
325
+ await this.initClient();
326
+ logger.info(`[AUN] Reconnect #${this.reconnectAttempt} succeeded`);
327
+ }
328
+ catch (err) {
329
+ logger.error(`[AUN] Reconnect #${this.reconnectAttempt} failed:`, err);
330
+ this.scheduleReconnect();
331
+ }
332
+ }, delay * 1000);
333
+ }
334
+ /** Manually trigger reconnect (e.g. from /check reconnect command) */
335
+ async reconnect() {
336
+ if (this.connected)
337
+ return '已连接,无需重连';
338
+ if (this.reconnectTimer) {
339
+ clearTimeout(this.reconnectTimer);
340
+ this.reconnectTimer = null;
341
+ }
342
+ this.reconnectAttempt = 0;
343
+ try {
344
+ await this.initClient();
345
+ return `重连成功 (${this._aid})`;
346
+ }
347
+ catch (err) {
348
+ this.scheduleReconnect();
349
+ return `重连失败: ${err},已安排自动重试`;
350
+ }
351
+ }
352
+ /** Set callback for when all reconnect attempts are exhausted */
353
+ setOnChannelDown(callback) {
354
+ this.onChannelDown = callback;
355
+ }
356
+ /** Get current connection status */
357
+ getStatus() {
358
+ return {
359
+ connected: this.connected,
360
+ aid: this._aid,
361
+ reconnectAttempt: this.reconnectAttempt,
362
+ maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
363
+ };
364
+ }
365
+ }
366
+ // Plugin implementation
367
+ export class AUNChannelPlugin {
368
+ name = 'aun';
369
+ isEnabled(config) {
370
+ return config.channels?.aun?.enabled !== false && !!config.channels?.aun?.aid;
371
+ }
372
+ async createChannel(config) {
373
+ const aunConfig = config.channels?.aun;
374
+ if (!aunConfig?.aid) {
375
+ throw new Error('AUN config missing (aid required, e.g. "mybot.agentid.pub")');
376
+ }
377
+ const channel = new AUNChannel({
378
+ aid: aunConfig.aid,
379
+ keystorePath: aunConfig.keystorePath,
380
+ gatewayPort: aunConfig.gatewayPort,
381
+ gatewayUrl: aunConfig.gatewayUrl,
382
+ accessToken: aunConfig.accessToken,
383
+ flushDelay: aunConfig.flushDelay,
384
+ encryptionSeed: aunConfig.encryptionSeed,
385
+ });
386
+ const adapter = {
387
+ name: 'aun',
388
+ sendText: (id, text, context) => channel.sendMessage(id, text, context),
389
+ acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
390
+ sendProcessingStatus: (id, status, sessionId, context) => channel.sendProcessingStatus(id, status, sessionId, context),
391
+ sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
392
+ };
393
+ const policy = {
394
+ canSwitchProject: (chatType, identity) => identity === 'owner',
395
+ canListProjects: (chatType, identity) => identity === 'owner',
396
+ canCreateSession: (chatType, identity) => true,
397
+ canDeleteSession: (chatType, identity) => true,
398
+ canImportCliSession: (chatType, identity) => identity === 'owner',
399
+ messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
400
+ showMiddleResult: (chatType, identity) => {
401
+ const mode = aunConfig.showActivities ?? config.showActivities ?? 'all';
402
+ if (mode === 'none')
403
+ return false;
404
+ if (mode === 'dm-only')
405
+ return chatType === 'private';
406
+ if (mode === 'owner-dm-only')
407
+ return chatType === 'private' && identity === 'owner';
408
+ return true;
409
+ },
410
+ showIdleMonitor: (chatType, identity) => {
411
+ const mode = aunConfig.showActivities ?? config.showActivities ?? 'all';
412
+ if (mode === 'none')
413
+ return false;
414
+ if (mode === 'dm-only')
415
+ return chatType === 'private';
416
+ if (mode === 'owner-dm-only')
417
+ return chatType === 'private' && identity === 'owner';
418
+ return true;
419
+ },
420
+ accumulateErrors: (chatType, identity) => true,
421
+ };
422
+ const options = {
423
+ flushDelay: aunConfig.flushDelay ?? 3,
424
+ fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
425
+ };
426
+ return {
427
+ adapter,
428
+ channel,
429
+ policy,
430
+ options,
431
+ connect: () => channel.connect(),
432
+ disconnect: () => channel.disconnect(),
433
+ };
434
+ }
28
435
  }