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.
Files changed (54) hide show
  1. package/README.md +59 -30
  2. package/data/evolclaw.sample.json +15 -4
  3. package/dist/agents/claude-runner.js +685 -0
  4. package/dist/agents/codex-runner.js +315 -0
  5. package/dist/agents/gemini-runner.js +425 -0
  6. package/dist/channels/aun.js +580 -10
  7. package/dist/channels/feishu.js +888 -135
  8. package/dist/channels/wechat.js +127 -21
  9. package/dist/cli.js +519 -136
  10. package/dist/config.js +277 -25
  11. package/dist/core/agent-loader.js +39 -0
  12. package/dist/core/channel-loader.js +67 -0
  13. package/dist/core/command-handler.js +1537 -392
  14. package/dist/core/event-bus.js +32 -0
  15. package/dist/core/interaction-router.js +68 -0
  16. package/dist/core/message/message-bridge.js +216 -0
  17. package/dist/core/message/message-processor.js +1028 -0
  18. package/dist/core/message/message-queue.js +240 -0
  19. package/dist/core/message/stream-debouncer.js +122 -0
  20. package/dist/{utils → core/message}/stream-flusher.js +73 -13
  21. package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
  22. package/dist/core/permission.js +259 -0
  23. package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
  24. package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
  25. package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
  26. package/dist/core/session/session-file-adapter.js +7 -0
  27. package/dist/core/session/session-file-health.js +45 -0
  28. package/dist/core/session/session-manager.js +1072 -0
  29. package/dist/index.js +402 -252
  30. package/dist/ipc.js +106 -0
  31. package/dist/paths.js +1 -0
  32. package/dist/types.js +3 -0
  33. package/dist/utils/{platform.js → cross-platform.js} +38 -1
  34. package/dist/utils/error-utils.js +130 -5
  35. package/dist/utils/init-channel.js +649 -0
  36. package/dist/utils/init.js +190 -53
  37. package/dist/utils/logger.js +8 -3
  38. package/dist/utils/media-cache.js +207 -0
  39. package/dist/utils/migrate-project.js +122 -0
  40. package/dist/utils/rich-content-renderer.js +228 -0
  41. package/dist/utils/stats-collector.js +102 -0
  42. package/package.json +4 -2
  43. package/dist/core/agent-runner.js +0 -348
  44. package/dist/core/message-processor.js +0 -604
  45. package/dist/core/message-queue.js +0 -116
  46. package/dist/core/message-stream.js +0 -59
  47. package/dist/core/session-manager.js +0 -664
  48. package/dist/index.js.bak +0 -340
  49. package/dist/utils/init-feishu.js +0 -261
  50. package/dist/utils/init-wechat.js +0 -170
  51. package/dist/utils/markdown-to-feishu.js +0 -94
  52. package/dist/utils/permission.js +0 -43
  53. package/dist/utils/session-file-health.js +0 -68
  54. /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
package/dist/cli.js CHANGED
@@ -4,10 +4,13 @@ import path from 'path';
4
4
  import { spawn, execFile } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
7
+ import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
8
+ import { migrateProject } from './utils/migrate-project.js';
7
9
  import { cmdInit } from './utils/init.js';
8
- import { cmdInitWechat } from './utils/init-wechat.js';
9
- import { cmdInitFeishu } from './utils/init-feishu.js';
10
- import * as platform from './utils/platform.js';
10
+ import { ipcQuery } from './ipc.js';
11
+ import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
12
+ import * as platform from './utils/cross-platform.js';
13
+ import { EventBus } from './core/event-bus.js';
11
14
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
12
15
  process.removeAllListeners('warning');
13
16
  process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
@@ -86,17 +89,19 @@ function countLines(pkgRoot, logDir) {
86
89
  };
87
90
  console.log('\n[launcher] 正在统计代码行数...\n');
88
91
  const core = countDir(path.join(srcDir, 'core'));
92
+ const agents = countDir(path.join(srcDir, 'agents'));
89
93
  const channels = countDir(path.join(srcDir, 'channels'), 'experimental');
90
94
  const utils = countDir(path.join(srcDir, 'utils'));
91
95
  const entry = countFile(path.join(srcDir, 'index.ts'))
92
96
  + countFile(path.join(srcDir, 'config.ts'))
93
97
  + countFile(path.join(srcDir, 'types.ts'))
94
98
  + countFile(path.join(srcDir, 'cli.ts'));
95
- const total = core + channels + utils + entry;
99
+ const total = core + agents + channels + utils + entry;
96
100
  console.log('==================================================');
97
101
  console.log('EvolClaw 代码统计');
98
102
  console.log('==================================================');
99
103
  console.log(`核心模块: ${String(core).padStart(8)} 行`);
104
+ console.log(`Agent 模块: ${String(agents).padStart(8)} 行`);
100
105
  console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
101
106
  console.log(`工具库: ${String(utils).padStart(8)} 行`);
102
107
  console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
@@ -117,7 +122,7 @@ function countLines(pkgRoot, logDir) {
117
122
  }
118
123
  if (shouldAppend) {
119
124
  const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
120
- fs.appendFileSync(statsFile, `${now}\t${core}\t${channels}\t${utils}\t${entry}\t${total}\n`);
125
+ fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${entry}\t${total}\n`);
121
126
  }
122
127
  showHistory(statsFile);
123
128
  }
@@ -131,21 +136,30 @@ function showHistory(statsFile) {
131
136
  console.log('\n==================================================');
132
137
  console.log('历史记录(最近 8 次)');
133
138
  console.log('==================================================');
134
- console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
139
+ console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'Agent'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
135
140
  console.log('--------------------------------------------------');
136
141
  let prevTotal = null;
137
142
  for (const line of recent) {
138
143
  const parts = line.split('\t');
139
- if (parts.length < 6)
144
+ // 兼容旧格式(6列: time,core,ch,utils,entry,total)和新格式(7列: +agents)
145
+ let time, c, a, ch, u, e, t;
146
+ if (parts.length >= 7) {
147
+ [time, c, a, ch, u, e, t] = parts;
148
+ }
149
+ else if (parts.length >= 6) {
150
+ [time, c, ch, u, e, t] = parts;
151
+ a = '-';
152
+ }
153
+ else {
140
154
  continue;
141
- const [time, c, ch, u, e, t] = parts;
155
+ }
142
156
  const total = parseInt(t, 10);
143
157
  let diff = '-';
144
158
  if (prevTotal !== null) {
145
159
  const change = total - prevTotal;
146
160
  diff = change >= 0 ? `+${change}` : `${change}`;
147
161
  }
148
- console.log(`${time.padEnd(20)} ${c.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
162
+ console.log(`${time.padEnd(20)} ${c.padStart(6)} ${a.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
149
163
  prevTotal = total;
150
164
  }
151
165
  console.log('==================================================');
@@ -159,6 +173,23 @@ async function cmdStart() {
159
173
  console.log('❌ 配置文件不存在,请先运行 evolclaw init');
160
174
  process.exit(1);
161
175
  }
176
+ // 配置完整性校验
177
+ try {
178
+ const config = loadConfig(p.config);
179
+ const integrity = validateConfigIntegrity(config);
180
+ if (!integrity.valid) {
181
+ console.log(`❌ 配置文件完整性校验失败:`);
182
+ for (const reason of integrity.reasons) {
183
+ console.log(` - ${reason}`);
184
+ }
185
+ console.log(`\n配置文件: ${p.config}`);
186
+ process.exit(1);
187
+ }
188
+ }
189
+ catch (e) {
190
+ console.log(`❌ 配置文件加载失败: ${e.message}`);
191
+ process.exit(1);
192
+ }
162
193
  // 检查 PID 文件
163
194
  const pid = isRunning(p.pid);
164
195
  if (pid) {
@@ -167,8 +198,11 @@ async function cmdStart() {
167
198
  process.exit(1);
168
199
  }
169
200
  // 检查是否有残留进程(PID 文件已丢失但进程还在)
201
+ // 只清理属于当前 EVOLCLAW_HOME 的进程,避免误杀其他实例
170
202
  let hasOrphan = false;
171
- const orphanPids = platform.findProcesses('node.*dist/index.js');
203
+ const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
204
+ const allPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
205
+ const orphanPids = allPids.filter(pid => platform.getProcessEnv(pid, 'EVOLCLAW_HOME') === p.root);
172
206
  if (orphanPids.length > 0) {
173
207
  console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
174
208
  for (const p of orphanPids) {
@@ -197,6 +231,7 @@ async function cmdStart() {
197
231
  stdio: ['ignore', out, err],
198
232
  env: {
199
233
  ...process.env,
234
+ EVOLCLAW_HOME: p.root,
200
235
  LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
201
236
  MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
202
237
  EVENT_LOG: process.env.EVENT_LOG || 'true',
@@ -204,7 +239,7 @@ async function cmdStart() {
204
239
  });
205
240
  fs.writeFileSync(p.pid, String(child.pid));
206
241
  child.unref();
207
- // 等待 ready signal(最多 15 秒)
242
+ // 等待 ready signal(最多 30 秒,AUN sidecar 超时 15s + 其他通道连接)
208
243
  const startTime = Date.now();
209
244
  const checkReady = () => {
210
245
  // ready signal 出现(优先检查,避免 Windows 上 isRunning 误判)
@@ -213,6 +248,41 @@ async function cmdStart() {
213
248
  console.log(`✓ EvolClaw started successfully (PID: ${pid})`);
214
249
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
215
250
  console.log(` Logs: ${p.logs}/`);
251
+ // 从主日志提取渠道连接摘要
252
+ const mainLog = path.join(p.logs, 'evolclaw.log');
253
+ if (fs.existsSync(mainLog)) {
254
+ const logLines = fs.readFileSync(mainLog, 'utf-8').split('\n');
255
+ // 从末尾往前找最近一次启动的摘要
256
+ let channelSummary = '';
257
+ for (let i = logLines.length - 1; i >= 0; i--) {
258
+ if (logLines[i].includes('EvolClaw is running with')) {
259
+ channelSummary = logLines[i];
260
+ break;
261
+ }
262
+ }
263
+ if (channelSummary) {
264
+ const match = channelSummary.match(/running with .+/);
265
+ if (match)
266
+ console.log(` ${match[0]}`);
267
+ }
268
+ // 最近一次启动的失败信息
269
+ let lastReadyIdx = -1;
270
+ for (let i = logLines.length - 1; i >= 0; i--) {
271
+ if (logLines[i].includes('Ready signal written')) {
272
+ lastReadyIdx = i;
273
+ break;
274
+ }
275
+ }
276
+ if (lastReadyIdx > 0) {
277
+ for (let i = Math.max(0, lastReadyIdx - 20); i < lastReadyIdx; i++) {
278
+ const line = logLines[i];
279
+ if (line.includes('failed to connect') || line.includes('Failed to create channel')) {
280
+ const match = line.match(/\[WARN\]\s*(.+)/);
281
+ console.log(` ⚠ ${match ? match[1] : line.trim()}`);
282
+ }
283
+ }
284
+ }
285
+ }
216
286
  console.log('');
217
287
  // 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
218
288
  if (resolveRoot() === getPackageRoot()) {
@@ -221,7 +291,7 @@ async function cmdStart() {
221
291
  return;
222
292
  }
223
293
  // 超时
224
- if (Date.now() - startTime > 15000) {
294
+ if (Date.now() - startTime > 30000) {
225
295
  console.log('❌ Failed to start EvolClaw (ready signal timeout)');
226
296
  console.log('');
227
297
  console.log('📝 Error details (last 10 lines of stdout):');
@@ -302,6 +372,53 @@ async function cmdRestart() {
302
372
  await stopAndWait(p.pid);
303
373
  setTimeout(() => cmdStart(), 1000);
304
374
  }
375
+ function formatTimeAgo(ms) {
376
+ const sec = Math.floor(ms / 1000);
377
+ if (sec < 60)
378
+ return '刚刚';
379
+ const min = Math.floor(sec / 60);
380
+ if (min < 60)
381
+ return `${min}分钟前`;
382
+ const hour = Math.floor(min / 60);
383
+ if (hour < 24)
384
+ return `${hour}小时前`;
385
+ const day = Math.floor(hour / 24);
386
+ return `${day}天前`;
387
+ }
388
+ function showConfigChannels(config) {
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
+ }
407
+ }
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
+ }
417
+ }
418
+ else {
419
+ console.log(' (no channels configured)');
420
+ }
421
+ }
305
422
  async function cmdStatus() {
306
423
  const p = resolvePaths();
307
424
  const pid = isRunning(p.pid);
@@ -315,11 +432,47 @@ async function cmdStatus() {
315
432
  console.log(` Uptime: ${info.uptime}`);
316
433
  if (info.cpu)
317
434
  console.log(` CPU: ${info.cpu}%`);
318
- if (info.memory)
319
- 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
+ }
320
440
  }
321
441
  catch { }
322
442
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
443
+ // Runtime statistics (only when running)
444
+ if (fs.existsSync(p.db)) {
445
+ try {
446
+ const Database = await import('node:sqlite');
447
+ const db = new Database.DatabaseSync(p.db);
448
+ // Get recent active sessions (last 5)
449
+ const recentSessions = db.prepare(`
450
+ SELECT id, project_path, name, channel, chat_type, thread_id, agent_session_id, agent_id, metadata, updated_at
451
+ FROM sessions
452
+ WHERE deleted_at IS NULL
453
+ ORDER BY updated_at DESC
454
+ LIMIT 5
455
+ `).all();
456
+ db.close();
457
+ if (recentSessions.length > 0) {
458
+ console.log('');
459
+ console.log('📋 Recent Active Sessions:');
460
+ for (const s of recentSessions) {
461
+ const projectName = path.basename(s.project_path);
462
+ const sessionType = s.thread_id ? '话题会话' : '主会话';
463
+ const chatType = s.chat_type === 'group' ? '群聊' : '单聊';
464
+ const sessionName = s.name || '默认会话';
465
+ const timeAgo = formatTimeAgo(Date.now() - s.updated_at);
466
+ const meta = s.metadata ? JSON.parse(s.metadata) : {};
467
+ const dot = meta.isActive ? '•' : '○';
468
+ const agentId = s.agent_session_id ? ` [${s.agent_session_id}]` : '';
469
+ const agentType = s.agent_id || 'claude';
470
+ console.log(` ${dot} [${agentType}] ${projectName} / ${sessionName} (${sessionType}, ${chatType})${agentId} - ${timeAgo}`);
471
+ }
472
+ }
473
+ }
474
+ catch { }
475
+ }
323
476
  }
324
477
  else {
325
478
  console.log('⚠ EvolClaw is not running');
@@ -327,16 +480,17 @@ async function cmdStatus() {
327
480
  console.log(` Stale PID file found: ${p.pid}`);
328
481
  }
329
482
  }
483
+ // Session & Project statistics (always show if DB exists)
330
484
  if (fs.existsSync(p.db)) {
331
485
  console.log('');
332
486
  console.log('📦 Sessions & Projects:');
333
487
  try {
334
488
  const Database = await import('node:sqlite');
335
489
  const db = new Database.DatabaseSync(p.db);
336
- const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions').get();
337
- const activeSessions = db.prepare('SELECT count(*) as cnt FROM sessions WHERE is_active=1').get();
338
- const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions').get();
339
- const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions').get();
490
+ const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions WHERE deleted_at IS NULL').get();
491
+ const activeSessions = db.prepare("SELECT count(*) as cnt FROM sessions WHERE json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL").get();
492
+ const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions WHERE deleted_at IS NULL').get();
493
+ const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions WHERE deleted_at IS NULL').get();
340
494
  db.close();
341
495
  console.log(` Total sessions: ${totalSessions.cnt} (active: ${activeSessions.cnt})`);
342
496
  console.log(` Unique chats: ${uniqueChats.cnt}`);
@@ -344,102 +498,59 @@ async function cmdStatus() {
344
498
  }
345
499
  catch { }
346
500
  }
347
- // Channel configuration status
501
+ // Channel status
348
502
  if (fs.existsSync(p.config)) {
349
503
  console.log('');
350
- console.log('🔌 Channels:');
351
- try {
352
- const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
353
- if (config.channels?.feishu?.appId && config.channels?.feishu?.appSecret) {
354
- // Verify Feishu credentials connectivity
355
- try {
356
- const lark = await import('@larksuiteoapi/node-sdk');
357
- const client = new lark.Client({ appId: config.channels.feishu.appId, appSecret: config.channels.feishu.appSecret });
358
- const res = await client.auth.tenantAccessToken.internal({
359
- data: { app_id: config.channels.feishu.appId, app_secret: config.channels.feishu.appSecret },
360
- });
361
- if (res.code === 0) {
362
- console.log(` Feishu: Connected (App ID: ${config.channels.feishu.appId.slice(0, 8)}...)`);
363
- }
364
- else {
365
- console.log(` Feishu: ✗ Connection refused (${res.msg})`);
366
- }
504
+ const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
505
+ if (pid) {
506
+ // Running: query IPC for real-time status
507
+ const status = await ipcQuery(p.socket, { type: 'status' });
508
+ if (status) {
509
+ console.log('🔌 Channels (live):');
510
+ // Group channels by channelType
511
+ const groups = new Map();
512
+ for (const [name, ch] of Object.entries(status.channels)) {
513
+ const type = ch.channelType || name;
514
+ if (!groups.has(type))
515
+ groups.set(type, []);
516
+ groups.get(type).push({ name, ch: ch });
367
517
  }
368
- catch (e) {
369
- const msg = e.message || '';
370
- if (msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH') || msg.includes('ENOTFOUND')) {
371
- console.log(' Feishu: Connection timeout (network unreachable)');
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}`);
372
524
  }
373
525
  else {
374
- console.log(` Feishu: Connection failed (${msg.slice(0, 80)})`);
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(', ')}]`);
375
532
  }
376
533
  }
377
- }
378
- else {
379
- console.log(' Feishu: - Not configured');
380
- }
381
- if (config.channels?.wechat?.token) {
382
- const tokenPreview = config.channels.wechat.token.slice(0, 20);
383
- // Validate token by calling getconfig API
384
- try {
385
- const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
386
- const body = JSON.stringify({ base_info: { channel_version: '1.0.0' } });
387
- const uint32 = (await import('node:crypto')).default.randomBytes(4).readUInt32BE(0);
388
- const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
389
- const res = await fetch(`${baseUrl}/ilink/bot/getconfig`, {
390
- method: 'POST',
391
- headers: {
392
- 'Content-Type': 'application/json',
393
- 'AuthorizationType': 'ilink_bot_token',
394
- 'Authorization': `Bearer ${config.channels.wechat.token.trim()}`,
395
- 'X-WECHAT-UIN': wechatUin,
396
- },
397
- body,
398
- signal: AbortSignal.timeout(10_000),
399
- });
400
- const resp = JSON.parse(await res.text());
401
- const isExpired = resp.errcode === -14 || resp.ret === -14;
402
- if (isExpired) {
403
- console.log(` WeChat: ✗ Token expired (Token: ${tokenPreview}...)`);
404
- console.log(' Run: evolclaw init wechat && evolclaw restart');
405
- }
406
- else {
407
- console.log(` WeChat: ✓ Connected (Token: ${tokenPreview}...)`);
408
- }
409
- }
410
- catch (e) {
411
- const msg = e.message || '';
412
- if (msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH') || msg.includes('ENOTFOUND')) {
413
- console.log(` WeChat: ✗ Connection timeout (Token: ${tokenPreview}...)`);
414
- }
415
- else {
416
- console.log(` WeChat: ✓ Configured (Token: ${tokenPreview}...)`);
417
- }
534
+ if (status.stats) {
535
+ console.log('');
536
+ console.log('📊 Last hour:');
537
+ console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
538
+ if (status.stats.errors > 0)
539
+ console.log(` Errors: ${status.stats.errors}`);
540
+ if (status.stats.completed > 0)
541
+ console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
418
542
  }
419
543
  }
420
544
  else {
421
- console.log(' WeChat: - Not configured');
422
- }
423
- // Check AUN with placeholder detection
424
- const aunDomain = config.channels?.aun?.domain;
425
- const aunAgent = config.channels?.aun?.agentName;
426
- const isAunPlaceholder = !aunDomain || !aunAgent ||
427
- aunDomain.includes('your-') || aunDomain.includes('placeholder') ||
428
- aunAgent.includes('your-') || aunAgent.includes('placeholder');
429
- if (aunDomain && aunAgent && !isAunPlaceholder) {
430
- console.log(` AUN: ✓ Configured (${aunAgent}@${aunDomain})`);
431
- }
432
- else {
433
- console.log(' AUN: - Not configured');
434
- }
435
- if (config.agents?.anthropic?.model) {
436
- console.log(` Model: ${config.agents.anthropic.model}`);
437
- }
438
- if (config.projects?.defaultPath) {
439
- console.log(` Default project: ${config.projects.defaultPath}`);
545
+ // IPC unreachable but PID exists — show config only
546
+ console.log('🔌 Channels (IPC unreachable):');
547
+ showConfigChannels(config);
440
548
  }
441
549
  }
442
- catch { }
550
+ else {
551
+ console.log('🔌 Channel Configuration:');
552
+ showConfigChannels(config);
553
+ }
443
554
  }
444
555
  console.log('');
445
556
  console.log('📁 Log Files:');
@@ -449,9 +560,9 @@ async function cmdStatus() {
449
560
  const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
450
561
  console.log(` Main log: ${mainLog} (${sizeMB} MB)`);
451
562
  console.log('');
452
- console.log('📝 Recent activity (last 10 lines):');
563
+ console.log('📝 Recent activity (last 30 lines):');
453
564
  const content = fs.readFileSync(mainLog, 'utf-8').trim().split('\n');
454
- console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
565
+ console.log(content.slice(-30).map(l => ` ${l}`).join('\n'));
455
566
  }
456
567
  else {
457
568
  console.log(' (no log file yet)');
@@ -483,11 +594,17 @@ async function cmdRestartMonitor() {
483
594
  const p = resolvePaths();
484
595
  const restartLog = path.join(p.logs, 'restart.log');
485
596
  const MAX_HEAL_ATTEMPTS = 3;
486
- const READY_TIMEOUT = 15000; // 15s
597
+ const READY_TIMEOUT = 30000; // 30s(AUN sidecar 10s + Feishu 连接 12s)
598
+ const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
599
+ const eventBus = new EventBus();
487
600
  const log = (msg) => {
488
601
  const line = `[${new Date().toISOString().replace('T', ' ').slice(0, 19)}] ${msg}\n`;
489
602
  fs.appendFileSync(restartLog, line);
490
603
  };
604
+ /** 检查服务是否已经在运行(ready signal 存在 + 进程存活) */
605
+ const isServiceAlive = () => {
606
+ return fs.existsSync(p.readySignal) && isRunning(p.pid) !== null;
607
+ };
491
608
  log('Restart monitor started');
492
609
  // 读取 restart-pending.json 用于后续通知
493
610
  const pendingFile = path.join(p.dataDir, 'restart-pending.json');
@@ -527,40 +644,97 @@ async function cmdRestartMonitor() {
527
644
  if (started) {
528
645
  log('✓ Service restarted successfully');
529
646
  archiveSelfHealLog(p, log);
530
- await notifyChannel(p, pendingInfo, '✅ 服务重启成功!', log);
531
- cleanupPendingFile(pendingFile, log);
647
+ // 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
532
648
  process.exit(0);
533
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
+ }
534
657
  // 启动失败,进入 self-heal 循环
535
658
  log('❌ Service failed to start, entering self-heal loop');
659
+ eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
536
660
  await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
537
661
  for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
662
+ // 前置检查:服务可能已被上一轮 claude 修复并启动
663
+ if (isServiceAlive()) {
664
+ log(`✓ Service already running before attempt ${attempt}, skipping`);
665
+ await sendHealSummary(p, pendingInfo, attempt - 1, log);
666
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt - 1 });
667
+ archiveSelfHealLog(p, log);
668
+ cleanupPendingFile(pendingFile, log);
669
+ process.exit(0);
670
+ }
538
671
  log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
672
+ eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
539
673
  await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
540
- // 调用 claude CLI 修复(递增超时:3/4/5 分钟)
541
- const timeout = (2 + attempt) * 60 * 1000;
542
- const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, timeout, log);
674
+ const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
675
+ // 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
676
+ if (isServiceAlive()) {
677
+ log(`✓ Service is running after attempt ${attempt}`);
678
+ await sendHealSummary(p, pendingInfo, attempt, log);
679
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
680
+ archiveSelfHealLog(p, log);
681
+ cleanupPendingFile(pendingFile, log);
682
+ process.exit(0);
683
+ }
543
684
  if (!healed) {
544
685
  log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
545
686
  continue;
546
687
  }
547
- // 重新启动
688
+ // claude 正常完成但服务没自动启动,尝试 spawn
548
689
  started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
549
690
  if (started) {
550
691
  log(`✓ Self-heal succeeded on attempt ${attempt}`);
692
+ await sendHealSummary(p, pendingInfo, attempt, log);
693
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
551
694
  archiveSelfHealLog(p, log);
552
- await notifyChannel(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
553
695
  cleanupPendingFile(pendingFile, log);
554
696
  process.exit(0);
555
697
  }
556
698
  log(`Attempt ${attempt}: still failing after fix`);
557
699
  }
558
- // 全部失败
700
+ // 全部失败 — 最后再检查一次
701
+ if (isServiceAlive()) {
702
+ log('✓ Service recovered during final check');
703
+ await sendHealSummary(p, pendingInfo, MAX_HEAL_ATTEMPTS, log);
704
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: MAX_HEAL_ATTEMPTS });
705
+ archiveSelfHealLog(p, log);
706
+ cleanupPendingFile(pendingFile, log);
707
+ process.exit(0);
708
+ }
559
709
  log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
710
+ eventBus.publish({ type: 'self-heal:completed', success: false, attempts: MAX_HEAL_ATTEMPTS });
560
711
  await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
561
712
  cleanupPendingFile(pendingFile, log);
562
713
  process.exit(1);
563
714
  }
715
+ /**
716
+ * 发送 self-heal 修复成功小结(从 self-heal.md 提取摘要)
717
+ */
718
+ async function sendHealSummary(p, pendingInfo, attempts, log) {
719
+ let summary = `✅ 自动修复成功(第 ${attempts || 1} 次尝试)`;
720
+ try {
721
+ if (fs.existsSync(p.selfHealLog)) {
722
+ const content = fs.readFileSync(p.selfHealLog, 'utf-8');
723
+ // 提取最后一个 ## 章节的要点
724
+ const sections = content.split(/^## /m).filter(Boolean);
725
+ const last = sections[sections.length - 1];
726
+ if (last) {
727
+ const lines = last.split('\n').filter(l => l.startsWith('- ')).map(l => l.trim());
728
+ if (lines.length > 0) {
729
+ summary += '\n' + lines.join('\n');
730
+ }
731
+ }
732
+ }
733
+ }
734
+ catch { }
735
+ summary += '\n\n⚠️ 修复前进行中的任务已中断,如需继续请重新发送。';
736
+ await notifyChannel(p, pendingInfo, summary, log);
737
+ }
564
738
  function sleep(ms) {
565
739
  return new Promise(resolve => setTimeout(resolve, ms));
566
740
  }
@@ -582,7 +756,13 @@ async function spawnAndWaitReady(p, log, timeout) {
582
756
  fs.unlinkSync(p.readySignal);
583
757
  }
584
758
  catch { }
585
- // 杀掉可能残留的进程
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 { }
586
766
  try {
587
767
  fs.unlinkSync(p.pid);
588
768
  }
@@ -597,6 +777,7 @@ async function spawnAndWaitReady(p, log, timeout) {
597
777
  stdio: ['ignore', out, err],
598
778
  env: {
599
779
  ...process.env,
780
+ EVOLCLAW_HOME: p.root,
600
781
  LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
601
782
  MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
602
783
  EVENT_LOG: process.env.EVENT_LOG || 'true',
@@ -643,16 +824,27 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
643
824
 
644
825
  关键信息:
645
826
  - 项目目录:${projectDir}
646
- - 错误日志:${stdoutLog}(请读取最后 50 行分析错误原因)
647
- - 主日志:${path.join(p.logs, 'evolclaw.log')}(可能包含更多上下文)
827
+ - EVOLCLAW_HOME:${p.root}
828
+ - 错误日志:${stdoutLog}
829
+ - 主日志:${path.join(p.logs, 'evolclaw.log')}(logger 输出在这里,包含 config 校验失败等关键错误)
648
830
  - 修复记录:${selfHealLog}(${selfHealExists})
649
831
 
832
+ ⚠️ 重要诊断技巧:
833
+ - stdout.log 可能是空的(进程秒退时 logger 输出不会到 stdout),一定要同时读 evolclaw.log
834
+ - 必须实际运行进程来复现错误:\`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\`,观察输出和退出码
835
+ - 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
836
+ - 可以运行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js diagnose\` 快速检查配置和数据库
837
+ - 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
838
+ - evolclaw.json 有自动备份机制:运行时 config watch 检测到文件损坏会自动保存内存快照到 \`data/evolclaw-{timestamp}.json\`,同时 \`data/evolclaw.backup.json\` 是最近一次完整配置的备份。如果 evolclaw.json 损坏或缺失,可以从这些备份恢复
839
+
650
840
  请执行以下步骤:
651
- 1. 读取错误日志,分析启动失败的根本原因
652
- 2. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
653
- 3. 修复代码问题
654
- 4. 执行 npm run build 确认编译通过
655
- 5. 将本次修复内容追加到 ${selfHealLog},格式:
841
+ 1. 读取 ${stdoutLog} 和 ${path.join(p.logs, 'evolclaw.log')} 的最后 50 行
842
+ 2. 运行 \`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\` 复现错误(设置 10 秒超时)
843
+ 3. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
844
+ 4. 根据实际复现的错误修复代码
845
+ 5. 执行 npm run build 确认编译通过
846
+ 6. 验证修复:启动服务确认 ready.signal 已写入,然后执行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js stop\` 优雅停止(restart-monitor 会负责最终启动)
847
+ 7. 将本次修复内容追加到 ${selfHealLog},格式:
656
848
  ## 第 ${attempt} 次修复 - {时间}
657
849
  - 错误原因:...
658
850
  - 修复方案:...
@@ -665,6 +857,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
665
857
  '-p', prompt,
666
858
  '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
667
859
  '--output-format', 'text',
860
+ '--no-session-persistence',
668
861
  ], {
669
862
  cwd: projectDir,
670
863
  timeout,
@@ -679,12 +872,19 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
679
872
  return true;
680
873
  }
681
874
  catch (error) {
682
- const msg = error.message || String(error);
683
- log(`Claude CLI error: ${msg.slice(0, 800)}`);
875
+ if (error.killed) {
876
+ log(`Claude CLI timeout after ${timeout / 60000}min (attempt ${attempt})`);
877
+ }
878
+ else {
879
+ log(`Claude CLI error: exit code ${error.code ?? 'unknown'} (attempt ${attempt})`);
880
+ }
684
881
  if (error.stdout)
685
- log(`Stdout: ${String(error.stdout).slice(0, 500)}`);
686
- if (error.stderr)
687
- log(`Stderr: ${String(error.stderr).slice(0, 500)}`);
882
+ log(`Claude output: ${String(error.stdout).slice(0, 500)}`);
883
+ if (error.stderr) {
884
+ const stderr = String(error.stderr).replace(/Warning: no stdin.*\n?/g, '').trim();
885
+ if (stderr)
886
+ log(`Claude stderr: ${stderr.slice(0, 300)}`);
887
+ }
688
888
  return false;
689
889
  }
690
890
  }
@@ -699,6 +899,28 @@ function archiveSelfHealLog(p, log) {
699
899
  fs.renameSync(p.selfHealLog, archivePath);
700
900
  log(`Archived self-heal log to ${archivePath}`);
701
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
+ }
702
924
  /**
703
925
  * 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
704
926
  * 支持 feishu / wechat,根据 pendingInfo.channel 路由
@@ -710,14 +932,20 @@ async function notifyChannel(p, pendingInfo, message, log) {
710
932
  if (!fs.existsSync(configPath))
711
933
  return;
712
934
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
713
- 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') {
714
941
  try {
715
- if (!config.channels?.feishu?.appId || !config.channels?.feishu?.appSecret)
942
+ const inst = resolved.config;
943
+ if (!inst.appId || !inst.appSecret)
716
944
  return;
717
945
  const lark = await import('@larksuiteoapi/node-sdk');
718
946
  const client = new lark.Client({
719
- appId: config.channels.feishu.appId,
720
- appSecret: config.channels.feishu.appSecret,
947
+ appId: inst.appId,
948
+ appSecret: inst.appSecret,
721
949
  });
722
950
  if (pendingInfo.rootId) {
723
951
  await client.im.message.reply({
@@ -745,13 +973,14 @@ async function notifyChannel(p, pendingInfo, message, log) {
745
973
  log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
746
974
  }
747
975
  }
748
- else if (pendingInfo.channel === 'wechat') {
976
+ else if (resolved.type === 'wechat') {
749
977
  try {
750
- if (!config.channels?.wechat?.token)
978
+ const inst = resolved.config;
979
+ if (!inst.token)
751
980
  return;
752
981
  const crypto = await import('node:crypto');
753
- const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
754
- const token = config.channels.wechat.token;
982
+ const baseUrl = (inst.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
983
+ const token = inst.token;
755
984
  // 读取缓存的 context_token
756
985
  const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
757
986
  let contextToken;
@@ -803,6 +1032,144 @@ async function notifyChannel(p, pendingInfo, message, log) {
803
1032
  }
804
1033
  }
805
1034
  }
1035
+ // ==================== Migrate ====================
1036
+ async function cmdMv(oldDir, newDir) {
1037
+ if (!oldDir || !newDir) {
1038
+ console.log('Usage: evolclaw mv <old_directory> <new_directory>');
1039
+ console.log('Example: evolclaw mv ~/projects/old-name ~/projects/new-name');
1040
+ process.exit(1);
1041
+ }
1042
+ const oldAbs = path.resolve(oldDir);
1043
+ const newAbs = path.resolve(newDir);
1044
+ console.log(`迁移项目: ${oldAbs} → ${newAbs}\n`);
1045
+ try {
1046
+ const r = await migrateProject(oldAbs, newAbs);
1047
+ if (r.claudeSessionsMoved)
1048
+ console.log('✓ Claude Code 会话目录已迁移');
1049
+ if (r.claudeHistoryUpdated)
1050
+ console.log('✓ Claude Code history.jsonl 已更新');
1051
+ if (r.codexUpdated > 0)
1052
+ console.log(`✓ Codex 数据库已更新 (${r.codexUpdated} 个会话)`);
1053
+ if (r.directoryMoved)
1054
+ console.log('✓ 项目目录已移动');
1055
+ if (r.evolclawDbUpdated > 0)
1056
+ console.log(`✓ EvolClaw sessions.db 已更新 (${r.evolclawDbUpdated} 个会话)`);
1057
+ if (r.evolclawConfigUpdated)
1058
+ console.log('✓ evolclaw.json projects.list 已更新');
1059
+ console.log('\n迁移完成!');
1060
+ }
1061
+ catch (e) {
1062
+ console.error(`迁移失败: ${e instanceof Error ? e.message : e}`);
1063
+ process.exit(1);
1064
+ }
1065
+ }
1066
+ // ==================== Diagnose ====================
1067
+ async function cmdDiagnose() {
1068
+ const p = resolvePaths();
1069
+ let hasError = false;
1070
+ // 1. 检查数据目录
1071
+ console.log(`[diagnose] EVOLCLAW_HOME = ${p.root}`);
1072
+ if (!fs.existsSync(p.root)) {
1073
+ console.error(`[diagnose] ❌ 数据目录不存在: ${p.root}`);
1074
+ hasError = true;
1075
+ }
1076
+ else {
1077
+ console.log(`[diagnose] ✓ 数据目录存在`);
1078
+ }
1079
+ // 2. 加载并校验配置
1080
+ try {
1081
+ const config = loadConfig();
1082
+ console.log(`[diagnose] ✓ 配置文件加载成功: ${p.config}`);
1083
+ const integrity = validateConfigIntegrity(config);
1084
+ if (!integrity.valid) {
1085
+ console.error(`[diagnose] ❌ 配置完整性校验失败:\n ${integrity.reasons.join('\n ')}`);
1086
+ hasError = true;
1087
+ }
1088
+ else {
1089
+ console.log(`[diagnose] ✓ 配置完整性校验通过`);
1090
+ }
1091
+ // 3. 检查 Anthropic 配置
1092
+ try {
1093
+ const anthropic = resolveAnthropicConfig(config);
1094
+ console.log(`[diagnose] ✓ Anthropic 配置解析成功 (apiKey: ${anthropic.apiKey ? '已设置' : '❌ 未设置'}, model: ${anthropic.model || 'default'})`);
1095
+ }
1096
+ catch (e) {
1097
+ console.error(`[diagnose] ❌ Anthropic 配置解析失败: ${e instanceof Error ? e.message : e}`);
1098
+ hasError = true;
1099
+ }
1100
+ }
1101
+ catch (e) {
1102
+ console.error(`[diagnose] ❌ 配置文件加载失败: ${e instanceof Error ? e.message : e}`);
1103
+ hasError = true;
1104
+ }
1105
+ // 4. 检查数据库
1106
+ try {
1107
+ const { SessionManager } = await import('./core/session/session-manager.js');
1108
+ const eventBus = new EventBus();
1109
+ new SessionManager(p.db, eventBus);
1110
+ console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
1111
+ }
1112
+ catch (e) {
1113
+ console.error(`[diagnose] ❌ 数据库初始化失败: ${e instanceof Error ? e.message : e}`);
1114
+ hasError = true;
1115
+ }
1116
+ // 5. 检查残留进程
1117
+ try {
1118
+ const pid = isRunning(p.pid);
1119
+ if (pid) {
1120
+ console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${pid}`);
1121
+ }
1122
+ else {
1123
+ console.log(`[diagnose] ✓ 无残留进程`);
1124
+ }
1125
+ }
1126
+ catch {
1127
+ console.log(`[diagnose] ✓ 无 PID 文件`);
1128
+ }
1129
+ // 6. 检查关键文件
1130
+ const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
1131
+ if (!fs.existsSync(appMain)) {
1132
+ console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
1133
+ hasError = true;
1134
+ }
1135
+ else {
1136
+ console.log(`[diagnose] ✓ 编译产物存在: ${appMain}`);
1137
+ }
1138
+ if (hasError) {
1139
+ console.error('\n[diagnose] ❌ 诊断发现问题,请修复后重试');
1140
+ process.exit(1);
1141
+ }
1142
+ else {
1143
+ console.log('\n[diagnose] ✓ 所有检查通过');
1144
+ }
1145
+ }
1146
+ async function cmdTui() {
1147
+ const config = loadConfig();
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;
1151
+ if (!aun?.owner || !aun?.aid) {
1152
+ console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
1153
+ process.exit(1);
1154
+ }
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');
1160
+ process.exit(1);
1161
+ }
1162
+ const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
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
+ }
1170
+ const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1171
+ child.on('exit', (code) => process.exit(code ?? 0));
1172
+ }
806
1173
  // ==================== Main ====================
807
1174
  export async function main(args) {
808
1175
  const cmd = args[0] || 'start';
@@ -814,6 +1181,9 @@ export async function main(args) {
814
1181
  else if (args[1] === 'feishu') {
815
1182
  await cmdInitFeishu();
816
1183
  }
1184
+ else if (args[1] === 'aun') {
1185
+ await cmdInitAun();
1186
+ }
817
1187
  else {
818
1188
  await cmdInit();
819
1189
  }
@@ -836,18 +1206,31 @@ export async function main(args) {
836
1206
  case 'restart-monitor':
837
1207
  await cmdRestartMonitor();
838
1208
  break;
1209
+ case 'mv':
1210
+ await cmdMv(args[1], args[2]);
1211
+ break;
1212
+ case 'diagnose':
1213
+ await cmdDiagnose();
1214
+ break;
1215
+ case 'tui':
1216
+ await cmdTui();
1217
+ break;
839
1218
  default:
840
- console.log(`Usage: evolclaw {init|start|stop|restart|status|logs}
1219
+ console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|diagnose|mv}
841
1220
 
842
1221
  Commands:
843
1222
  init 创建配置文件 (${resolvePaths().config})
844
- init wechat 微信扫码登录并写入配置
845
1223
  init feishu 飞书扫码登录并写入配置
1224
+ init wechat 微信扫码登录并写入配置
1225
+ init aun AUN (AgentUnin.Network) 配置
846
1226
  start 启动服务 (默认)
847
1227
  stop 停止服务
848
1228
  restart 重启服务
849
1229
  status 查看状态
850
1230
  logs 查看日志 (tail -f)
1231
+ tui 启动 AUN TUI 客户端
1232
+ diagnose 诊断启动环境(配置、数据库、进程)
1233
+ mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
851
1234
 
852
1235
  Environment:
853
1236
  EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)