evolclaw 2.2.0 → 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 (43) hide show
  1. package/README.md +49 -27
  2. package/data/evolclaw.sample.json +6 -3
  3. package/dist/agents/claude-runner.js +125 -52
  4. package/dist/agents/codex-runner.js +10 -5
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +247 -84
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +132 -50
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +750 -209
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +216 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +386 -105
  16. package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
  17. package/dist/{utils → core/message}/stream-debouncer.js +1 -1
  18. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  19. package/dist/core/permission.js +212 -11
  20. package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
  21. package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
  22. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  23. package/dist/{utils → core/session}/session-file-health.js +1 -1
  24. package/dist/core/{session-manager.js → session/session-manager.js} +57 -11
  25. package/dist/index.js +138 -54
  26. package/dist/{core/ipc-server.js → ipc.js} +36 -1
  27. package/dist/types.js +3 -0
  28. package/dist/utils/cross-platform.js +38 -1
  29. package/dist/utils/error-utils.js +130 -5
  30. package/dist/utils/init-channel.js +649 -0
  31. package/dist/utils/init.js +55 -150
  32. package/dist/utils/logger.js +8 -3
  33. package/dist/utils/media-cache.js +207 -0
  34. package/dist/{core → utils}/stats-collector.js +16 -0
  35. package/package.json +3 -3
  36. package/dist/core/message-bridge.js +0 -187
  37. package/dist/utils/init-feishu.js +0 -263
  38. package/dist/utils/init-wechat.js +0 -172
  39. package/dist/utils/ipc-client.js +0 -36
  40. package/dist/utils/permission-utils.js +0 -71
  41. /package/dist/{utils → core/message}/message-cache.js +0 -0
  42. /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
  43. /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
@@ -1,10 +1,74 @@
1
- import { AUNClient } from '@aun/core-node';
2
- 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';
3
7
  export class AUNChannel {
4
8
  config;
5
9
  client = null;
6
10
  messageHandler;
7
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
+ }
8
72
  _aid;
9
73
  seenMessages = new Map();
10
74
  messageSeqMap = new Map(); // messageId → seq (for ack)
@@ -17,6 +81,11 @@ export class AUNChannel {
17
81
  onChannelDown;
18
82
  constructor(config) {
19
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
+ }
20
89
  }
21
90
  async connect() {
22
91
  this.intentionalDisconnect = false;
@@ -53,21 +122,39 @@ export class AUNChannel {
53
122
  logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
54
123
  // Create client with FileSecretStore (AES-256-GCM)
55
124
  // 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
125
+ const rootCaPath = `${aunPath}/CA/root/root.crt`;
56
126
  this.client = new AUNClient({
57
127
  aun_path: aunPath,
128
+ root_ca_path: rootCaPath,
58
129
  ...(encryptionSeed && { encryption_seed: encryptionSeed }),
59
130
  });
60
131
  // Set gateway URL (internal property, same as Python SDK)
61
132
  this.client._gatewayUrl = gateway;
62
133
  // 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));
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
+ });
66
152
  // Authenticate
67
153
  let accessToken;
68
154
  try {
69
155
  logger.info(`[AUN] Authenticating as ${aidName}...`);
70
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 });
71
158
  accessToken = auth.access_token;
72
159
  const resolvedGateway = auth.gateway || gateway;
73
160
  this.client._gatewayUrl = resolvedGateway;
@@ -107,7 +194,7 @@ export class AUNChannel {
107
194
  const msg = data;
108
195
  const fromAid = msg.from ?? '';
109
196
  const payload = msg.payload ?? '';
110
- const text = typeof payload === 'string' ? payload : (payload ? JSON.stringify(payload) : '');
197
+ const text = this.extractTextPayload(payload);
111
198
  const taskId = msg.task_id;
112
199
  const messageId = msg.message_id ?? '';
113
200
  const seq = msg.seq;
@@ -134,24 +221,42 @@ export class AUNChannel {
134
221
  const groupId = msg.group_id ?? '';
135
222
  const senderAid = msg.sender_aid ?? msg.from ?? '';
136
223
  const payload = msg.payload ?? '';
137
- const text = typeof payload === 'string' ? payload : (payload ? JSON.stringify(payload) : '');
224
+ const text = this.extractTextPayload(payload);
138
225
  const taskId = msg.task_id;
139
226
  const messageId = msg.message_id ?? '';
140
227
  const seq = msg.seq;
141
- // Detect @mentions
142
- const mentions = [];
143
- if (this._aid && text.includes(`@${this._aid}`)) {
144
- mentions.push(this._aid);
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;
145
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] : []);
146
249
  this.dispatchMessage({
147
250
  channelId: groupId,
148
251
  userId: senderAid,
149
- text,
252
+ peerName: this.getShortAid(senderAid),
253
+ text: strippedText,
150
254
  chatType: 'group',
151
255
  messageId,
152
256
  seq,
153
257
  taskId,
154
258
  mentions,
259
+ replyContext: this.buildGroupReplyContext(taskId, senderAid, text),
155
260
  });
156
261
  }
157
262
  dispatchMessage(event) {
@@ -169,8 +274,10 @@ export class AUNChannel {
169
274
  if (!this.messageHandler)
170
275
  return;
171
276
  const mentionObjects = event.mentions?.map(aid => ({ userId: aid }));
172
- let replyContext;
173
- if (event.taskId) {
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) {
174
281
  replyContext = { threadId: event.taskId };
175
282
  }
176
283
  this.messageHandler({
@@ -178,6 +285,7 @@ export class AUNChannel {
178
285
  content: event.text || '',
179
286
  chatType: event.chatType,
180
287
  peerId: event.userId || event.channelId || '',
288
+ peerName: event.peerName,
181
289
  messageId: event.messageId,
182
290
  threadId: event.taskId,
183
291
  mentions: mentionObjects,
@@ -230,20 +338,30 @@ export class AUNChannel {
230
338
  finalText = '最终回复\n' + text;
231
339
  }
232
340
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
233
- const params = { payload: finalText, encrypt: true };
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 };
234
349
  if (context?.threadId)
235
350
  params.task_id = context.threadId;
236
351
  try {
237
352
  if (channelId.startsWith('grp_')) {
238
353
  params.group_id = channelId;
354
+ this.trace('OUT', 'group.send', params);
239
355
  await this.client.call('group.send', params);
240
356
  }
241
357
  else {
242
358
  params.to = channelId;
359
+ this.trace('OUT', 'message.send', params);
243
360
  await this.client.call('message.send', params);
244
361
  }
245
362
  }
246
363
  catch (e) {
364
+ this.trace('OUT', 'send.error', { channelId, error: String(e) });
247
365
  logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
248
366
  }
249
367
  }
@@ -261,29 +379,52 @@ export class AUNChannel {
261
379
  this.sentCount.delete(channelId); // 新任务开始,重置计数
262
380
  if (!this.client || !this.connected)
263
381
  return;
264
- const payload = JSON.stringify({
382
+ const payload = {
265
383
  type: 'processing',
266
384
  status,
267
385
  sessionId,
268
386
  timestamp: Math.floor(Date.now() / 1000),
269
- });
387
+ };
270
388
  const params = {
271
- to: channelId, payload,
389
+ payload,
272
390
  encrypt: true, persist: false,
273
391
  };
274
392
  if (context?.threadId)
275
393
  params.task_id = context.threadId;
276
- this.client.call('message.send', params).catch(e => {
277
- logger.debug(`[AUN] Processing status failed: ${e}`);
278
- });
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
+ }
279
408
  }
280
409
  sendCustomPayload(channelId, payload) {
281
410
  if (!this.client || !this.connected)
282
411
  return;
283
- this.client.call('message.send', {
284
- to: channelId, payload,
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,
285
424
  encrypt: true, persist: false,
286
- }).catch(e => {
425
+ };
426
+ this.trace('OUT', 'message.send.custom', sendParams);
427
+ this.client.call('message.send', sendParams).catch(e => {
287
428
  logger.debug(`[AUN] Custom payload failed: ${e}`);
288
429
  });
289
430
  }
@@ -301,6 +442,10 @@ export class AUNChannel {
301
442
  this.client = null;
302
443
  }
303
444
  this.connected = false;
445
+ if (this.traceStream) {
446
+ this.traceStream.end();
447
+ this.traceStream = null;
448
+ }
304
449
  logger.info('[AUN] Disconnected');
305
450
  }
306
451
  // ── TS-layer reconnect (fallback when SDK auto_reconnect exhausted) ──
@@ -367,69 +512,87 @@ export class AUNChannel {
367
512
  export class AUNChannelPlugin {
368
513
  name = 'aun';
369
514
  isEnabled(config) {
370
- return config.channels?.aun?.enabled !== false && !!config.channels?.aun?.aid;
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;
371
590
  }
372
591
  async createChannel(config) {
373
- const aunConfig = config.channels?.aun;
374
- if (!aunConfig?.aid) {
592
+ const instances = await this.createChannels(config);
593
+ if (instances.length === 0) {
375
594
  throw new Error('AUN config missing (aid required, e.g. "mybot.agentid.pub")');
376
595
  }
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
- };
596
+ return instances[0];
434
597
  }
435
598
  }