evolclaw 2.2.0 → 2.4.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 +283 -95
  7. package/dist/channels/feishu.js +556 -96
  8. package/dist/channels/wechat.js +98 -74
  9. package/dist/cli.js +232 -57
  10. package/dist/config.js +185 -31
  11. package/dist/core/channel-loader.js +11 -4
  12. package/dist/core/command-handler.js +803 -247
  13. package/dist/core/interaction-router.js +68 -0
  14. package/dist/core/message/message-bridge.js +217 -0
  15. package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
  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} +61 -11
  25. package/dist/index.js +140 -57
  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
package/dist/cli.js CHANGED
@@ -7,10 +7,9 @@ import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './pat
7
7
  import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
8
8
  import { migrateProject } from './utils/migrate-project.js';
9
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';
10
+ import { cmdInit } from './utils/init.js';
11
+ import { ipcQuery } from './ipc.js';
12
+ import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
14
13
  import * as platform from './utils/cross-platform.js';
15
14
  import { EventBus } from './core/event-bus.js';
16
15
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
@@ -200,9 +199,11 @@ async function cmdStart() {
200
199
  process.exit(1);
201
200
  }
202
201
  // 检查是否有残留进程(PID 文件已丢失但进程还在)
202
+ // 只清理属于当前 EVOLCLAW_HOME 的进程,避免误杀其他实例
203
203
  let hasOrphan = false;
204
204
  const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
205
- const orphanPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
205
+ const allPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
206
+ const orphanPids = allPids.filter(pid => platform.getProcessEnv(pid, 'EVOLCLAW_HOME') === p.root);
206
207
  if (orphanPids.length > 0) {
207
208
  console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
208
209
  for (const p of orphanPids) {
@@ -231,6 +232,7 @@ async function cmdStart() {
231
232
  stdio: ['ignore', out, err],
232
233
  env: {
233
234
  ...process.env,
235
+ EVOLCLAW_HOME: p.root,
234
236
  LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
235
237
  MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
236
238
  EVENT_LOG: process.env.EVENT_LOG || 'true',
@@ -385,27 +387,37 @@ function formatTimeAgo(ms) {
385
387
  return `${day}天前`;
386
388
  }
387
389
  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');
390
+ const groups = [];
391
+ const channelChecks = [
392
+ { type: 'feishu', isValid: (inst) => !!inst.appId && inst.enabled !== false },
393
+ { type: 'wechat', isValid: (inst) => !!inst.token && inst.enabled !== false },
394
+ { type: 'aun', isValid: (inst) => !!inst.aid && inst.enabled !== false && !inst.aid.includes('your-') && !inst.aid.includes('placeholder') },
395
+ ];
396
+ for (const { type, isValid } of channelChecks) {
397
+ const raw = config.channels?.[type];
398
+ if (!raw)
399
+ continue;
400
+ if (Array.isArray(raw)) {
401
+ const names = raw.filter(isValid).map((inst) => inst.name || type);
402
+ if (names.length > 0)
403
+ groups.push({ type, instances: names });
404
+ }
405
+ else if (isValid(raw)) {
406
+ groups.push({ type, instances: [raw.name || type] });
407
+ }
401
408
  }
402
- // AUN
403
- const aunAid = config.channels?.aun?.aid;
404
- if (aunAid && !aunAid.includes('your-') && !aunAid.includes('placeholder')) {
405
- console.log(` aun: Configured (${aunAid})`);
409
+ if (groups.length > 0) {
410
+ for (const g of groups) {
411
+ if (g.instances.length === 1) {
412
+ console.log(` ${g.instances[0]}: Configured`);
413
+ }
414
+ else {
415
+ console.log(` ${g.type}: [${g.instances.join(', ')}]`);
416
+ }
417
+ }
406
418
  }
407
419
  else {
408
- console.log(' aun: - Not configured');
420
+ console.log(' (no channels configured)');
409
421
  }
410
422
  }
411
423
  async function cmdStatus() {
@@ -421,8 +433,11 @@ async function cmdStatus() {
421
433
  console.log(` Uptime: ${info.uptime}`);
422
434
  if (info.cpu)
423
435
  console.log(` CPU: ${info.cpu}%`);
424
- if (info.memory)
425
- console.log(` Memory: ${info.memory} KB`);
436
+ if (info.memory) {
437
+ const memKB = parseInt(info.memory, 10);
438
+ const memStr = memKB >= 1024 ? `${(memKB / 1024).toFixed(0)} MB` : `${memKB} KB`;
439
+ console.log(` Memory: ${memStr}`);
440
+ }
426
441
  }
427
442
  catch { }
428
443
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
@@ -493,13 +508,29 @@ async function cmdStatus() {
493
508
  const status = await ipcQuery(p.socket, { type: 'status' });
494
509
  if (status) {
495
510
  console.log('🔌 Channels (live):');
511
+ // Group channels by channelType
512
+ const groups = new Map();
496
513
  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}`);
514
+ const type = ch.channelType || name;
515
+ if (!groups.has(type))
516
+ groups.set(type, []);
517
+ groups.get(type).push({ name, ch: ch });
518
+ }
519
+ for (const [type, instances] of groups) {
520
+ if (instances.length === 1) {
521
+ // Single instance: show instance name directly
522
+ const { name, ch } = instances[0];
523
+ const label = ch.connected ? '✓ Connected' : ch.reconnectAttempt ? `⏳ Reconnecting (${ch.reconnectAttempt}/${ch.maxAttempts})` : '✗ Disconnected';
524
+ console.log(` ${name}: ${label}`);
525
+ }
526
+ else {
527
+ // Multi-instance: feishu [name1 ✓, name2 ✗]
528
+ const parts = instances.map(({ name, ch }) => {
529
+ const icon = ch.connected ? '✓' : ch.reconnectAttempt ? '⏳' : '✗';
530
+ return `${name} ${icon}`;
531
+ });
532
+ console.log(` ${type}: [${parts.join(', ')}]`);
533
+ }
503
534
  }
504
535
  if (status.stats) {
505
536
  console.log('');
@@ -530,30 +561,119 @@ async function cmdStatus() {
530
561
  const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
531
562
  console.log(` Main log: ${mainLog} (${sizeMB} MB)`);
532
563
  console.log('');
533
- console.log('📝 Recent activity (last 10 lines):');
564
+ console.log('📝 Recent activity (last 30 lines):');
534
565
  const content = fs.readFileSync(mainLog, 'utf-8').trim().split('\n');
535
- console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
566
+ console.log(content.slice(-30).map(l => ` ${l}`).join('\n'));
536
567
  }
537
568
  else {
538
569
  console.log(' (no log file yet)');
539
570
  }
540
571
  }
541
- function cmdLogs() {
572
+ // Log line pattern: [timestamp] [LEVEL] [Module?] message
573
+ const LOG_RE = /^(\[[^\]]+\]) (\[(?:INFO|WARN|ERROR|DEBUG)\]) ((?:\[[^\]]+\] )*)(.*)$/;
574
+ const MAX_MSG = 200; // truncate long messages
575
+ function makeColors(enabled) {
576
+ const e = (code) => enabled ? code : '';
577
+ return {
578
+ reset: e('\x1b[0m'), dim: e('\x1b[2m'), bold: e('\x1b[1m'),
579
+ red: e('\x1b[31m'), yellow: e('\x1b[33m'), cyan: e('\x1b[36m'),
580
+ magenta: e('\x1b[35m'), gray: e('\x1b[90m'),
581
+ };
582
+ }
583
+ function renderLogLine(line, opts) {
584
+ const m = line.match(LOG_RE);
585
+ if (!m)
586
+ return line; // passthrough non-standard lines (stack traces etc.)
587
+ const [, ts, levelTag, modulePart, msg] = m;
588
+ const level = levelTag.slice(1, -1); // strip brackets
589
+ // Level filter
590
+ if (opts.level) {
591
+ const want = opts.level.toUpperCase();
592
+ if (want === 'ERROR' && level !== 'ERROR')
593
+ return null;
594
+ if (want === 'WARN' && level !== 'WARN' && level !== 'ERROR')
595
+ return null;
596
+ }
597
+ // Module filter (case-insensitive substring match)
598
+ if (opts.module) {
599
+ const mod = modulePart.toLowerCase();
600
+ if (!mod.includes(opts.module.toLowerCase()))
601
+ return null;
602
+ }
603
+ // Truncate long messages (always, regardless of color)
604
+ const truncated = msg.length > MAX_MSG ? msg.slice(0, MAX_MSG) + '…' : msg;
605
+ const C = makeColors(opts.color);
606
+ // Color by level
607
+ const levelColor = level === 'ERROR' ? C.red : level === 'WARN' ? C.yellow : level === 'DEBUG' ? C.gray : '';
608
+ // Highlight user messages: [channel] channelId: text
609
+ const isUserMsg = modulePart && /^\S+: .+$/.test(truncated);
610
+ const renderedMsg = isUserMsg
611
+ ? C.cyan + truncated + C.reset
612
+ : levelColor + truncated + C.reset;
613
+ return (C.dim + ts + C.reset + ' ' +
614
+ levelColor + C.bold + levelTag + C.reset + ' ' +
615
+ C.magenta + modulePart.trimEnd() + C.reset +
616
+ (modulePart ? ' ' : '') +
617
+ renderedMsg);
618
+ }
619
+ function cmdLogs(args) {
620
+ const raw = args.includes('--raw');
621
+ const noColor = args.includes('--no-color');
622
+ const levelIdx = args.indexOf('--level');
623
+ const moduleIdx = args.indexOf('--module');
624
+ const level = levelIdx !== -1 ? args[levelIdx + 1] : undefined;
625
+ const module = moduleIdx !== -1 ? args[moduleIdx + 1] : undefined;
542
626
  const p = resolvePaths();
543
627
  const mainLog = path.join(p.logs, 'evolclaw.log');
544
628
  if (!fs.existsSync(mainLog)) {
545
629
  console.log(`❌ Log file not found: ${mainLog}`);
546
630
  process.exit(1);
547
631
  }
632
+ if (raw) {
633
+ // Raw mode: plain tail -f, no rendering at all
634
+ if (platform.isWindows) {
635
+ const tail = platform.tailFile(mainLog);
636
+ platform.onShutdown(() => tail.abort());
637
+ }
638
+ else {
639
+ const child = spawn('tail', ['-f', '-n', '50', mainLog], { stdio: 'inherit' });
640
+ child.on('exit', (code) => process.exit(code || 0));
641
+ }
642
+ return;
643
+ }
644
+ // Rendered mode: always filter+truncate, color depends on TTY
645
+ const useColor = !noColor && !!process.stdout.isTTY;
646
+ const opts = { level, module, color: useColor };
647
+ function processLine(line) {
648
+ const rendered = renderLogLine(line, opts);
649
+ if (rendered !== null)
650
+ process.stdout.write(rendered + '\n');
651
+ }
548
652
  if (platform.isWindows) {
549
- // Windows: use fs.watch for live tail
550
- const tail = platform.tailFile(mainLog);
551
- platform.onShutdown(() => tail.abort());
653
+ // Windows: read existing content + watch
654
+ const existing = fs.readFileSync(mainLog, 'utf-8').split('\n').slice(-50);
655
+ existing.forEach(processLine);
656
+ let size = fs.statSync(mainLog).size;
657
+ const watcher = fs.watch(mainLog, () => {
658
+ const newSize = fs.statSync(mainLog).size;
659
+ if (newSize <= size)
660
+ return;
661
+ const buf = Buffer.alloc(newSize - size);
662
+ const fd = fs.openSync(mainLog, 'r');
663
+ fs.readSync(fd, buf, 0, buf.length, size);
664
+ fs.closeSync(fd);
665
+ size = newSize;
666
+ buf.toString().split('\n').forEach(l => l && processLine(l));
667
+ });
668
+ platform.onShutdown(() => watcher.close());
552
669
  }
553
670
  else {
554
- // Unix: use tail -f
555
- const child = spawn('tail', ['-f', mainLog], { stdio: 'inherit' });
671
+ // Unix: spawn tail -f, pipe through renderer
672
+ const child = spawn('tail', ['-f', '-n', '50', mainLog]);
673
+ const rl = readline.createInterface({ input: child.stdout });
674
+ rl.on('line', processLine);
556
675
  child.on('exit', (code) => process.exit(code || 0));
676
+ platform.onShutdown(() => { child.kill(); });
557
677
  }
558
678
  }
559
679
  /**
@@ -617,6 +737,13 @@ async function cmdRestartMonitor() {
617
737
  // 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
618
738
  process.exit(0);
619
739
  }
740
+ // 启动失败 — 测试环境下跳过 self-heal(避免 claude -p 污染会话列表、误杀生产进程)
741
+ if (p.root.startsWith('/tmp/') || process.env.EVOLCLAW_TEST === '1') {
742
+ log('❌ Service failed to start (test environment detected, skipping self-heal)');
743
+ await notifyChannel(p, pendingInfo, '❌ 服务启动失败(测试环境,已跳过自动修复)', log);
744
+ cleanupPendingFile(pendingFile, log);
745
+ process.exit(1);
746
+ }
620
747
  // 启动失败,进入 self-heal 循环
621
748
  log('❌ Service failed to start, entering self-heal loop');
622
749
  eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
@@ -719,7 +846,13 @@ async function spawnAndWaitReady(p, log, timeout) {
719
846
  fs.unlinkSync(p.readySignal);
720
847
  }
721
848
  catch { }
722
- // 杀掉可能残留的进程
849
+ // 杀掉可能残留的进程(先读 PID 再删文件,避免数据库锁)
850
+ try {
851
+ const stalePid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
852
+ if (!isNaN(stalePid))
853
+ platform.killProcess(stalePid, true);
854
+ }
855
+ catch { }
723
856
  try {
724
857
  fs.unlinkSync(p.pid);
725
858
  }
@@ -734,6 +867,7 @@ async function spawnAndWaitReady(p, log, timeout) {
734
867
  stdio: ['ignore', out, err],
735
868
  env: {
736
869
  ...process.env,
870
+ EVOLCLAW_HOME: p.root,
737
871
  LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
738
872
  MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
739
873
  EVENT_LOG: process.env.EVENT_LOG || 'true',
@@ -813,6 +947,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
813
947
  '-p', prompt,
814
948
  '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
815
949
  '--output-format', 'text',
950
+ '--no-session-persistence',
816
951
  ], {
817
952
  cwd: projectDir,
818
953
  timeout,
@@ -854,6 +989,28 @@ function archiveSelfHealLog(p, log) {
854
989
  fs.renameSync(p.selfHealLog, archivePath);
855
990
  log(`Archived self-heal log to ${archivePath}`);
856
991
  }
992
+ /**
993
+ * Resolve a channel instance name to its type and config object.
994
+ * Searches across all channel types (feishu, wechat, aun) for a matching instance.
995
+ */
996
+ function resolveInstanceConfig(config, instanceName) {
997
+ for (const type of ['feishu', 'wechat', 'aun']) {
998
+ const raw = config.channels?.[type];
999
+ if (!raw)
1000
+ continue;
1001
+ if (Array.isArray(raw)) {
1002
+ const inst = raw.find((i) => i.name === instanceName);
1003
+ if (inst)
1004
+ return { type, config: inst };
1005
+ }
1006
+ else {
1007
+ const name = raw.name || type;
1008
+ if (name === instanceName)
1009
+ return { type, config: raw };
1010
+ }
1011
+ }
1012
+ return null;
1013
+ }
857
1014
  /**
858
1015
  * 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
859
1016
  * 支持 feishu / wechat,根据 pendingInfo.channel 路由
@@ -865,14 +1022,20 @@ async function notifyChannel(p, pendingInfo, message, log) {
865
1022
  if (!fs.existsSync(configPath))
866
1023
  return;
867
1024
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
868
- if (pendingInfo.channel === 'feishu') {
1025
+ const resolved = resolveInstanceConfig(config, pendingInfo.channel);
1026
+ if (!resolved) {
1027
+ log(`Channel instance "${pendingInfo.channel}" not found in config`);
1028
+ return;
1029
+ }
1030
+ if (resolved.type === 'feishu') {
869
1031
  try {
870
- if (!config.channels?.feishu?.appId || !config.channels?.feishu?.appSecret)
1032
+ const inst = resolved.config;
1033
+ if (!inst.appId || !inst.appSecret)
871
1034
  return;
872
1035
  const lark = await import('@larksuiteoapi/node-sdk');
873
1036
  const client = new lark.Client({
874
- appId: config.channels.feishu.appId,
875
- appSecret: config.channels.feishu.appSecret,
1037
+ appId: inst.appId,
1038
+ appSecret: inst.appSecret,
876
1039
  });
877
1040
  if (pendingInfo.rootId) {
878
1041
  await client.im.message.reply({
@@ -900,13 +1063,14 @@ async function notifyChannel(p, pendingInfo, message, log) {
900
1063
  log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
901
1064
  }
902
1065
  }
903
- else if (pendingInfo.channel === 'wechat') {
1066
+ else if (resolved.type === 'wechat') {
904
1067
  try {
905
- if (!config.channels?.wechat?.token)
1068
+ const inst = resolved.config;
1069
+ if (!inst.token)
906
1070
  return;
907
1071
  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;
1072
+ const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
1073
+ const token = inst.token;
910
1074
  // 读取缓存的 context_token
911
1075
  const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
912
1076
  let contextToken;
@@ -1030,7 +1194,7 @@ async function cmdDiagnose() {
1030
1194
  }
1031
1195
  // 4. 检查数据库
1032
1196
  try {
1033
- const { SessionManager } = await import('./core/session-manager.js');
1197
+ const { SessionManager } = await import('./core/session/session-manager.js');
1034
1198
  const eventBus = new EventBus();
1035
1199
  new SessionManager(p.db, eventBus);
1036
1200
  console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
@@ -1071,20 +1235,28 @@ async function cmdDiagnose() {
1071
1235
  }
1072
1236
  async function cmdTui() {
1073
1237
  const config = loadConfig();
1074
- const aun = config.channels?.aun;
1238
+ // Find the first AUN instance (TUI connects to one AUN instance)
1239
+ const aunResolved = resolveInstanceConfig(config, 'aun');
1240
+ const aun = aunResolved?.type === 'aun' ? aunResolved.config : null;
1075
1241
  if (!aun?.owner || !aun?.aid) {
1076
1242
  console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
1077
1243
  process.exit(1);
1078
1244
  }
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) {
1245
+ // TUI requires Python + aun_core (independent of init aun which is now pure TS)
1246
+ const pythonCheck = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1247
+ if (!platform.commandExists(pythonCheck)) {
1248
+ console.error(`[tui] Python 未找到 (${pythonCheck})`);
1249
+ console.error(' → TUI 依赖 Python 和 aun-core: pip3 install aun-core');
1084
1250
  process.exit(1);
1085
1251
  }
1086
1252
  const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1087
1253
  const cliScript = path.join(getPackageRoot(), 'aun', 'aun_cli.py');
1254
+ if (!fs.existsSync(cliScript)) {
1255
+ console.error(`[tui] aun_cli.py 不存在: ${cliScript}`);
1256
+ console.error(' → TUI 需要 AUN CLI 工具,请确认源码目录包含 aun/aun_cli.py');
1257
+ console.error(' → 安装: pip3 install aun-core && 从源码仓库获取 aun_cli.py');
1258
+ process.exit(1);
1259
+ }
1088
1260
  const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1089
1261
  child.on('exit', (code) => process.exit(code ?? 0));
1090
1262
  }
@@ -1119,7 +1291,7 @@ export async function main(args) {
1119
1291
  await cmdStatus();
1120
1292
  break;
1121
1293
  case 'logs':
1122
- cmdLogs();
1294
+ cmdLogs(args.slice(1));
1123
1295
  break;
1124
1296
  case 'restart-monitor':
1125
1297
  await cmdRestartMonitor();
@@ -1145,7 +1317,10 @@ Commands:
1145
1317
  stop 停止服务
1146
1318
  restart 重启服务
1147
1319
  status 查看状态
1148
- logs 查看日志 (tail -f)
1320
+ logs 查看日志 (tail -f, 着色渲染)
1321
+ --level error|warn 只显示指定级别及以上
1322
+ --module <name> 只显示指定模块(如 feishu、AgentRunner)
1323
+ --raw 原始输出,不着色
1149
1324
  tui 启动 AUN TUI 客户端
1150
1325
  diagnose 诊断启动环境(配置、数据库、进程)
1151
1326
  mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)