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
@@ -3,6 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { resolvePaths } from '../paths.js';
5
5
  import { logger } from '../utils/logger.js';
6
+ import { sanitizeFileName, saveToUploads, safeFetch } from '../utils/media-cache.js';
6
7
  const CHANNEL_VERSION = '1.0.0';
7
8
  const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
8
9
  const DEFAULT_API_TIMEOUT_MS = 15_000;
@@ -61,10 +62,7 @@ async function downloadMedia(cdnMedia, hexKey) {
61
62
  if (!cdnMedia.encrypt_query_param)
62
63
  throw new Error('No encrypt_query_param');
63
64
  const url = `${CDN_BASE_URL}/download?encrypted_query_param=${encodeURIComponent(cdnMedia.encrypt_query_param)}`;
64
- const res = await fetch(url);
65
- if (!res.ok)
66
- throw new Error(`CDN download failed: ${res.status}`);
67
- const encrypted = Buffer.from(await res.arrayBuffer());
65
+ const encrypted = await safeFetch(url);
68
66
  if (!aesKeyBase64)
69
67
  return encrypted; // 无 key = 明文
70
68
  return decryptAesEcb(encrypted, parseAesKey(aesKeyBase64));
@@ -142,6 +140,7 @@ export class WechatChannel {
142
140
  // Session expired 状态
143
141
  sessionPausedUntil = 0;
144
142
  onSessionExpired;
143
+ eventBus;
145
144
  // Project path resolver(用于保存文件到 uploads 目录)
146
145
  projectPathResolver;
147
146
  constructor(config) {
@@ -158,6 +157,10 @@ export class WechatChannel {
158
157
  onSessionExpiredNotify(handler) {
159
158
  this.onSessionExpired = handler;
160
159
  }
160
+ /** 注册事件总线(推荐,替代 onSessionExpiredNotify) */
161
+ setEventBus(bus) {
162
+ this.eventBus = bus;
163
+ }
161
164
  /** 当前是否处于 session 暂停状态 */
162
165
  isSessionPaused() {
163
166
  return Date.now() < this.sessionPausedUntil;
@@ -385,9 +388,19 @@ export class WechatChannel {
385
388
  const pauseMin = SESSION_PAUSE_DURATION_MS / 60_000;
386
389
  this.sessionPausedUntil = Date.now() + SESSION_PAUSE_DURATION_MS;
387
390
  logger.error(`[WeChat] Session still expired, pausing for ${pauseMin}min`);
388
- // 通知用户(通过其他渠道)
389
- if (this.onSessionExpired) {
390
- this.onSessionExpired(`⚠️ 微信 token 已过期,通道暂停 ${pauseMin} 分钟后自动重试。\n如需立即恢复,请运行: evolclaw init wechat`);
391
+ // 通知用户(通过事件总线或回调)
392
+ const authMsg = `⚠️ 微信 token 已过期,通道暂停 ${pauseMin} 分钟后自动重试。\n如需立即恢复,请运行: evolclaw init wechat`;
393
+ if (this.eventBus) {
394
+ this.eventBus.publish({
395
+ type: 'channel:health',
396
+ channel: 'wechat',
397
+ status: 'auth_error',
398
+ message: authMsg,
399
+ timestamp: Date.now(),
400
+ });
401
+ }
402
+ else if (this.onSessionExpired) {
403
+ this.onSessionExpired(authMsg);
391
404
  }
392
405
  await this.sleep(SESSION_PAUSE_DURATION_MS, signal);
393
406
  if (signal.aborted)
@@ -527,14 +540,14 @@ export class WechatChannel {
527
540
  }
528
541
  else if (item.type === MSG_ITEM_FILE && item.file_item?.media) {
529
542
  const buf = await downloadMedia(item.file_item.media);
530
- const fileName = this.sanitizeFileName(item.file_item.file_name || `file_${Date.now()}`);
531
- const savePath = await this.saveToUploads(buf, fileName, channelId);
543
+ const fileName = sanitizeFileName(item.file_item.file_name || `file_${Date.now()}`);
544
+ const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
532
545
  prompts.push(`用户发送了文件:${fileName}\n文件已保存到:${savePath}\n请使用 Read 工具读取并分析文件内容。`);
533
546
  }
534
547
  else if (item.type === MSG_ITEM_VIDEO && item.video_item?.media) {
535
548
  const buf = await downloadMedia(item.video_item.media);
536
549
  const fileName = `video_${Date.now()}.mp4`;
537
- const savePath = await this.saveToUploads(buf, fileName, channelId);
550
+ const savePath = await this.saveToUploadsLocal(buf, fileName, channelId);
538
551
  prompts.push(`用户发送了视频:${fileName}\n文件已保存到:${savePath}`);
539
552
  }
540
553
  }
@@ -544,19 +557,12 @@ export class WechatChannel {
544
557
  }
545
558
  return { prompt: prompts.join('\n\n'), images };
546
559
  }
547
- async saveToUploads(buf, fileName, channelId) {
560
+ async saveToUploadsLocal(buf, fileName, channelId) {
548
561
  const projectPath = this.projectPathResolver
549
562
  ? await this.projectPathResolver(channelId)
550
563
  : process.cwd();
551
- const uploadsDir = path.join(projectPath, '.evolclaw', 'uploads');
552
- fs.mkdirSync(uploadsDir, { recursive: true });
553
- const savePath = path.join(uploadsDir, fileName);
554
- fs.writeFileSync(savePath, buf);
555
- return savePath;
556
- }
557
- /** 清理文件名:移除路径穿越字符,只保留 basename */
558
- sanitizeFileName(name) {
559
- return path.basename(name).replace(/[<>:"|?*\x00-\x1f]/g, '_') || `file_${Date.now()}`;
564
+ const { filePath } = saveToUploads(buf, fileName, projectPath);
565
+ return filePath;
560
566
  }
561
567
  // ── Media Upload (Outbound) ──────────────────────────────────────────
562
568
  async getUploadUrl(params) {
@@ -701,66 +707,84 @@ export class WechatChannel {
701
707
  });
702
708
  }
703
709
  }
710
+ import { normalizeChannelInstances } from '../config.js';
704
711
  export class WechatChannelPlugin {
705
712
  name = 'wechat';
706
713
  isEnabled(config) {
707
- return config.channels?.wechat?.enabled === true && !!config.channels?.wechat?.token;
714
+ const raw = config.channels?.wechat;
715
+ if (!raw)
716
+ return false;
717
+ if (Array.isArray(raw)) {
718
+ return raw.some(inst => inst.enabled !== false && !!inst.token);
719
+ }
720
+ return raw.enabled === true && !!raw.token;
721
+ }
722
+ async createChannels(config) {
723
+ const instances = normalizeChannelInstances(config.channels?.wechat, 'wechat');
724
+ const result = [];
725
+ for (const inst of instances) {
726
+ if (inst.enabled === false || !inst.token)
727
+ continue;
728
+ const channel = new WechatChannel({
729
+ baseUrl: inst.baseUrl || 'https://ilinkai.weixin.qq.com',
730
+ token: inst.token,
731
+ });
732
+ const adapter = {
733
+ channelName: inst.name,
734
+ sendText: (id, text) => channel.sendMessage(id, text),
735
+ sendFile: (id, filePath) => channel.sendFile(id, filePath),
736
+ };
737
+ const policy = {
738
+ canSwitchProject: (chatType, identity) => identity === 'owner',
739
+ canListProjects: (chatType, identity) => identity === 'owner',
740
+ canCreateSession: (chatType, identity) => true,
741
+ canDeleteSession: (chatType, identity) => true,
742
+ canImportCliSession: (chatType, identity) => identity === 'owner',
743
+ messagePrefix: (chatType, peerName) => '',
744
+ showMiddleResult: (chatType, identity) => {
745
+ const mode = inst.showActivities ?? config.showActivities ?? 'all';
746
+ if (mode === 'none')
747
+ return false;
748
+ if (mode === 'dm-only')
749
+ return chatType === 'private';
750
+ if (mode === 'owner-dm-only')
751
+ return chatType === 'private' && identity === 'owner';
752
+ return true;
753
+ },
754
+ showIdleMonitor: (chatType, identity) => {
755
+ const mode = inst.showActivities ?? config.showActivities ?? 'all';
756
+ if (mode === 'none')
757
+ return false;
758
+ if (mode === 'dm-only')
759
+ return chatType === 'private';
760
+ if (mode === 'owner-dm-only')
761
+ return chatType === 'private' && identity === 'owner';
762
+ return true;
763
+ },
764
+ accumulateErrors: (chatType, identity) => true,
765
+ };
766
+ const options = {
767
+ fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
768
+ flushDelay: inst.flushDelay ?? 3, // WeChat 默认 3s
769
+ };
770
+ result.push({
771
+ channelType: 'wechat',
772
+ adapter,
773
+ channel,
774
+ policy,
775
+ options,
776
+ connect: () => channel.connect(),
777
+ disconnect: () => channel.disconnect(),
778
+ onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
779
+ });
780
+ }
781
+ return result;
708
782
  }
709
783
  async createChannel(config) {
710
- const wechatConfig = config.channels?.wechat;
711
- if (!wechatConfig?.token) {
784
+ const instances = await this.createChannels(config);
785
+ if (instances.length === 0) {
712
786
  throw new Error('WeChat config missing');
713
787
  }
714
- const channel = new WechatChannel({
715
- baseUrl: wechatConfig.baseUrl || 'https://ilinkai.weixin.qq.com',
716
- token: wechatConfig.token,
717
- });
718
- const adapter = {
719
- name: 'wechat',
720
- sendText: (id, text) => channel.sendMessage(id, text),
721
- sendFile: (id, filePath) => channel.sendFile(id, filePath),
722
- };
723
- const policy = {
724
- canSwitchProject: (chatType, identity) => identity === 'owner',
725
- canListProjects: (chatType, identity) => identity === 'owner',
726
- canCreateSession: (chatType, identity) => true,
727
- canDeleteSession: (chatType, identity) => true,
728
- canImportCliSession: (chatType, identity) => identity === 'owner',
729
- messagePrefix: (chatType, peerName) => '',
730
- showMiddleResult: (chatType, identity) => {
731
- const mode = wechatConfig.showActivities ?? config.showActivities ?? 'all';
732
- if (mode === 'none')
733
- return false;
734
- if (mode === 'dm-only')
735
- return chatType === 'private';
736
- if (mode === 'owner-dm-only')
737
- return chatType === 'private' && identity === 'owner';
738
- return true;
739
- },
740
- showIdleMonitor: (chatType, identity) => {
741
- const mode = wechatConfig.showActivities ?? config.showActivities ?? 'all';
742
- if (mode === 'none')
743
- return false;
744
- if (mode === 'dm-only')
745
- return chatType === 'private';
746
- if (mode === 'owner-dm-only')
747
- return chatType === 'private' && identity === 'owner';
748
- return true;
749
- },
750
- accumulateErrors: (chatType, identity) => true,
751
- };
752
- const options = {
753
- fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
754
- flushDelay: wechatConfig.flushDelay ?? 3, // WeChat 默认 3s
755
- };
756
- return {
757
- adapter,
758
- channel,
759
- policy,
760
- options,
761
- connect: () => channel.connect(),
762
- disconnect: () => channel.disconnect(),
763
- onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
764
- };
788
+ return instances[0];
765
789
  }
766
790
  }
package/dist/cli.js CHANGED
@@ -6,11 +6,9 @@ import { promisify } from 'util';
6
6
  import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
7
7
  import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
8
8
  import { migrateProject } from './utils/migrate-project.js';
9
- import readline from 'readline';
10
- import { cmdInit, cmdInitAun, checkAunEnvironment } from './utils/init.js';
11
- import { ipcQuery } from './utils/ipc-client.js';
12
- import { cmdInitWechat } from './utils/init-wechat.js';
13
- import { cmdInitFeishu } from './utils/init-feishu.js';
9
+ import { cmdInit } from './utils/init.js';
10
+ import { ipcQuery } from './ipc.js';
11
+ import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
14
12
  import * as platform from './utils/cross-platform.js';
15
13
  import { EventBus } from './core/event-bus.js';
16
14
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
@@ -200,9 +198,11 @@ async function cmdStart() {
200
198
  process.exit(1);
201
199
  }
202
200
  // 检查是否有残留进程(PID 文件已丢失但进程还在)
201
+ // 只清理属于当前 EVOLCLAW_HOME 的进程,避免误杀其他实例
203
202
  let hasOrphan = false;
204
203
  const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
205
- const orphanPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
204
+ const allPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
205
+ const orphanPids = allPids.filter(pid => platform.getProcessEnv(pid, 'EVOLCLAW_HOME') === p.root);
206
206
  if (orphanPids.length > 0) {
207
207
  console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
208
208
  for (const p of orphanPids) {
@@ -231,6 +231,7 @@ async function cmdStart() {
231
231
  stdio: ['ignore', out, err],
232
232
  env: {
233
233
  ...process.env,
234
+ EVOLCLAW_HOME: p.root,
234
235
  LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
235
236
  MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
236
237
  EVENT_LOG: process.env.EVENT_LOG || 'true',
@@ -385,27 +386,37 @@ function formatTimeAgo(ms) {
385
386
  return `${day}天前`;
386
387
  }
387
388
  function showConfigChannels(config) {
388
- // Feishu
389
- if (config.channels?.feishu?.appId) {
390
- console.log(` feishu: Configured (App ID: ${config.channels.feishu.appId.slice(0, 8)}...)`);
391
- }
392
- else {
393
- console.log(' feishu: - Not configured');
394
- }
395
- // WeChat
396
- if (config.channels?.wechat?.token) {
397
- console.log(` wechat: Configured (Token: ${config.channels.wechat.token.slice(0, 20)}...)`);
398
- }
399
- else {
400
- console.log(' wechat: - Not configured');
389
+ const groups = [];
390
+ const channelChecks = [
391
+ { type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
392
+ { type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
393
+ { type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
394
+ ];
395
+ for (const { type, isValid } of channelChecks) {
396
+ const raw = config.channels?.[type];
397
+ if (!raw)
398
+ continue;
399
+ if (Array.isArray(raw)) {
400
+ const names = raw.filter(isValid).map((inst) => inst.name || type);
401
+ if (names.length > 0)
402
+ groups.push({ type, instances: names });
403
+ }
404
+ else if (isValid(raw)) {
405
+ groups.push({ type, instances: [raw.name || type] });
406
+ }
401
407
  }
402
- // AUN
403
- const aunAid = config.channels?.aun?.aid;
404
- if (aunAid && !aunAid.includes('your-') && !aunAid.includes('placeholder')) {
405
- console.log(` aun: Configured (${aunAid})`);
408
+ if (groups.length > 0) {
409
+ for (const g of groups) {
410
+ if (g.instances.length === 1) {
411
+ console.log(` ${g.instances[0]}: Configured`);
412
+ }
413
+ else {
414
+ console.log(` ${g.type}: [${g.instances.join(', ')}]`);
415
+ }
416
+ }
406
417
  }
407
418
  else {
408
- console.log(' aun: - Not configured');
419
+ console.log(' (no channels configured)');
409
420
  }
410
421
  }
411
422
  async function cmdStatus() {
@@ -421,8 +432,11 @@ async function cmdStatus() {
421
432
  console.log(` Uptime: ${info.uptime}`);
422
433
  if (info.cpu)
423
434
  console.log(` CPU: ${info.cpu}%`);
424
- if (info.memory)
425
- console.log(` Memory: ${info.memory} KB`);
435
+ if (info.memory) {
436
+ const memKB = parseInt(info.memory, 10);
437
+ const memStr = memKB >= 1024 ? `${(memKB / 1024).toFixed(0)} MB` : `${memKB} KB`;
438
+ console.log(` Memory: ${memStr}`);
439
+ }
426
440
  }
427
441
  catch { }
428
442
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
@@ -493,13 +507,29 @@ async function cmdStatus() {
493
507
  const status = await ipcQuery(p.socket, { type: 'status' });
494
508
  if (status) {
495
509
  console.log('🔌 Channels (live):');
510
+ // Group channels by channelType
511
+ const groups = new Map();
496
512
  for (const [name, ch] of Object.entries(status.channels)) {
497
- const label = ch.connected
498
- ? '✓ Connected'
499
- : ch.reconnectAttempt
500
- ? `⏳ Reconnecting (${ch.reconnectAttempt}/${ch.maxAttempts})`
501
- : '✗ Disconnected';
502
- console.log(` ${name}: ${label}`);
513
+ const type = ch.channelType || name;
514
+ if (!groups.has(type))
515
+ groups.set(type, []);
516
+ groups.get(type).push({ name, ch: ch });
517
+ }
518
+ for (const [type, instances] of groups) {
519
+ if (instances.length === 1) {
520
+ // Single instance: show instance name directly
521
+ const { name, ch } = instances[0];
522
+ const label = ch.connected ? '✓ Connected' : ch.reconnectAttempt ? `⏳ Reconnecting (${ch.reconnectAttempt}/${ch.maxAttempts})` : '✗ Disconnected';
523
+ console.log(` ${name}: ${label}`);
524
+ }
525
+ else {
526
+ // Multi-instance: feishu [name1 ✓, name2 ✗]
527
+ const parts = instances.map(({ name, ch }) => {
528
+ const icon = ch.connected ? '✓' : ch.reconnectAttempt ? '⏳' : '✗';
529
+ return `${name} ${icon}`;
530
+ });
531
+ console.log(` ${type}: [${parts.join(', ')}]`);
532
+ }
503
533
  }
504
534
  if (status.stats) {
505
535
  console.log('');
@@ -530,9 +560,9 @@ async function cmdStatus() {
530
560
  const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
531
561
  console.log(` Main log: ${mainLog} (${sizeMB} MB)`);
532
562
  console.log('');
533
- console.log('📝 Recent activity (last 10 lines):');
563
+ console.log('📝 Recent activity (last 30 lines):');
534
564
  const content = fs.readFileSync(mainLog, 'utf-8').trim().split('\n');
535
- console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
565
+ console.log(content.slice(-30).map(l => ` ${l}`).join('\n'));
536
566
  }
537
567
  else {
538
568
  console.log(' (no log file yet)');
@@ -617,6 +647,13 @@ async function cmdRestartMonitor() {
617
647
  // 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
618
648
  process.exit(0);
619
649
  }
650
+ // 启动失败 — 测试环境下跳过 self-heal(避免 claude -p 污染会话列表、误杀生产进程)
651
+ if (p.root.startsWith('/tmp/') || process.env.EVOLCLAW_TEST === '1') {
652
+ log('❌ Service failed to start (test environment detected, skipping self-heal)');
653
+ await notifyChannel(p, pendingInfo, '❌ 服务启动失败(测试环境,已跳过自动修复)', log);
654
+ cleanupPendingFile(pendingFile, log);
655
+ process.exit(1);
656
+ }
620
657
  // 启动失败,进入 self-heal 循环
621
658
  log('❌ Service failed to start, entering self-heal loop');
622
659
  eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
@@ -719,7 +756,13 @@ async function spawnAndWaitReady(p, log, timeout) {
719
756
  fs.unlinkSync(p.readySignal);
720
757
  }
721
758
  catch { }
722
- // 杀掉可能残留的进程
759
+ // 杀掉可能残留的进程(先读 PID 再删文件,避免数据库锁)
760
+ try {
761
+ const stalePid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
762
+ if (!isNaN(stalePid))
763
+ platform.killProcess(stalePid, true);
764
+ }
765
+ catch { }
723
766
  try {
724
767
  fs.unlinkSync(p.pid);
725
768
  }
@@ -734,6 +777,7 @@ async function spawnAndWaitReady(p, log, timeout) {
734
777
  stdio: ['ignore', out, err],
735
778
  env: {
736
779
  ...process.env,
780
+ EVOLCLAW_HOME: p.root,
737
781
  LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
738
782
  MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
739
783
  EVENT_LOG: process.env.EVENT_LOG || 'true',
@@ -813,6 +857,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
813
857
  '-p', prompt,
814
858
  '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
815
859
  '--output-format', 'text',
860
+ '--no-session-persistence',
816
861
  ], {
817
862
  cwd: projectDir,
818
863
  timeout,
@@ -854,6 +899,28 @@ function archiveSelfHealLog(p, log) {
854
899
  fs.renameSync(p.selfHealLog, archivePath);
855
900
  log(`Archived self-heal log to ${archivePath}`);
856
901
  }
902
+ /**
903
+ * Resolve a channel instance name to its type and config object.
904
+ * Searches across all channel types (feishu, wechat, aun) for a matching instance.
905
+ */
906
+ function resolveInstanceConfig(config, instanceName) {
907
+ for (const type of ['feishu', 'wechat', 'aun']) {
908
+ const raw = config.channels?.[type];
909
+ if (!raw)
910
+ continue;
911
+ if (Array.isArray(raw)) {
912
+ const inst = raw.find((i) => i.name === instanceName);
913
+ if (inst)
914
+ return { type, config: inst };
915
+ }
916
+ else {
917
+ const name = raw.name || type;
918
+ if (name === instanceName)
919
+ return { type, config: raw };
920
+ }
921
+ }
922
+ return null;
923
+ }
857
924
  /**
858
925
  * 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
859
926
  * 支持 feishu / wechat,根据 pendingInfo.channel 路由
@@ -865,14 +932,20 @@ async function notifyChannel(p, pendingInfo, message, log) {
865
932
  if (!fs.existsSync(configPath))
866
933
  return;
867
934
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
868
- if (pendingInfo.channel === 'feishu') {
935
+ const resolved = resolveInstanceConfig(config, pendingInfo.channel);
936
+ if (!resolved) {
937
+ log(`Channel instance "${pendingInfo.channel}" not found in config`);
938
+ return;
939
+ }
940
+ if (resolved.type === 'feishu') {
869
941
  try {
870
- if (!config.channels?.feishu?.appId || !config.channels?.feishu?.appSecret)
942
+ const inst = resolved.config;
943
+ if (!inst.appId || !inst.appSecret)
871
944
  return;
872
945
  const lark = await import('@larksuiteoapi/node-sdk');
873
946
  const client = new lark.Client({
874
- appId: config.channels.feishu.appId,
875
- appSecret: config.channels.feishu.appSecret,
947
+ appId: inst.appId,
948
+ appSecret: inst.appSecret,
876
949
  });
877
950
  if (pendingInfo.rootId) {
878
951
  await client.im.message.reply({
@@ -900,13 +973,14 @@ async function notifyChannel(p, pendingInfo, message, log) {
900
973
  log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
901
974
  }
902
975
  }
903
- else if (pendingInfo.channel === 'wechat') {
976
+ else if (resolved.type === 'wechat') {
904
977
  try {
905
- if (!config.channels?.wechat?.token)
978
+ const inst = resolved.config;
979
+ if (!inst.token)
906
980
  return;
907
981
  const crypto = await import('node:crypto');
908
- const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
909
- const token = config.channels.wechat.token;
982
+ const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
983
+ const token = inst.token;
910
984
  // 读取缓存的 context_token
911
985
  const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
912
986
  let contextToken;
@@ -1030,7 +1104,7 @@ async function cmdDiagnose() {
1030
1104
  }
1031
1105
  // 4. 检查数据库
1032
1106
  try {
1033
- const { SessionManager } = await import('./core/session-manager.js');
1107
+ const { SessionManager } = await import('./core/session/session-manager.js');
1034
1108
  const eventBus = new EventBus();
1035
1109
  new SessionManager(p.db, eventBus);
1036
1110
  console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
@@ -1071,20 +1145,28 @@ async function cmdDiagnose() {
1071
1145
  }
1072
1146
  async function cmdTui() {
1073
1147
  const config = loadConfig();
1074
- const aun = config.channels?.aun;
1148
+ // Find the first AUN instance (TUI connects to one AUN instance)
1149
+ const aunResolved = resolveInstanceConfig(config, 'aun');
1150
+ const aun = aunResolved?.type === 'aun' ? aunResolved.config : null;
1075
1151
  if (!aun?.owner || !aun?.aid) {
1076
1152
  console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
1077
1153
  process.exit(1);
1078
1154
  }
1079
- // Check Python + aun_core, interactive install if missing
1080
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1081
- const ready = await checkAunEnvironment(rl);
1082
- rl.close();
1083
- if (!ready) {
1155
+ // TUI requires Python + aun_core (independent of init aun which is now pure TS)
1156
+ const pythonCheck = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1157
+ if (!platform.commandExists(pythonCheck)) {
1158
+ console.error(`[tui] Python 未找到 (${pythonCheck})`);
1159
+ console.error(' → TUI 依赖 Python 和 aun-core: pip3 install aun-core');
1084
1160
  process.exit(1);
1085
1161
  }
1086
1162
  const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1087
1163
  const cliScript = path.join(getPackageRoot(), 'aun', 'aun_cli.py');
1164
+ if (!fs.existsSync(cliScript)) {
1165
+ console.error(`[tui] aun_cli.py 不存在: ${cliScript}`);
1166
+ console.error(' → TUI 需要 AUN CLI 工具,请确认源码目录包含 aun/aun_cli.py');
1167
+ console.error(' → 安装: pip3 install aun-core && 从源码仓库获取 aun_cli.py');
1168
+ process.exit(1);
1169
+ }
1088
1170
  const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1089
1171
  child.on('exit', (code) => process.exit(code ?? 0));
1090
1172
  }