evolclaw 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +10 -3
  2. package/data/evolclaw.sample.json +9 -1
  3. package/dist/agents/claude-runner.js +612 -0
  4. package/dist/agents/codex-runner.js +310 -0
  5. package/dist/channels/aun.js +416 -9
  6. package/dist/channels/feishu.js +397 -104
  7. package/dist/channels/wechat.js +84 -2
  8. package/dist/cli.js +427 -126
  9. package/dist/config.js +102 -4
  10. package/dist/core/adapters/claude-session-file-adapter.js +144 -0
  11. package/dist/core/adapters/codex-session-file-adapter.js +196 -0
  12. package/dist/core/agent-loader.js +39 -0
  13. package/dist/core/channel-loader.js +60 -0
  14. package/dist/core/command-handler.js +908 -304
  15. package/dist/core/event-bus.js +32 -0
  16. package/dist/core/ipc-server.js +71 -0
  17. package/dist/core/message-bridge.js +187 -0
  18. package/dist/core/message-processor.js +370 -227
  19. package/dist/core/message-queue.js +153 -29
  20. package/dist/core/permission.js +58 -0
  21. package/dist/core/session-file-adapter.js +7 -0
  22. package/dist/core/session-manager.js +567 -205
  23. package/dist/core/stats-collector.js +86 -0
  24. package/dist/index.js +309 -243
  25. package/dist/paths.js +1 -0
  26. package/dist/utils/init-feishu.js +2 -0
  27. package/dist/utils/init-wechat.js +2 -0
  28. package/dist/utils/init.js +285 -53
  29. package/dist/utils/ipc-client.js +36 -0
  30. package/dist/utils/migrate-project.js +122 -0
  31. package/dist/utils/{permission.js → permission-utils.js} +31 -3
  32. package/dist/utils/rich-content-renderer.js +228 -0
  33. package/dist/utils/session-file-health.js +11 -34
  34. package/dist/utils/stream-debouncer.js +122 -0
  35. package/dist/utils/stream-idle-monitor.js +1 -1
  36. package/package.json +3 -1
  37. package/dist/core/agent-runner.js +0 -348
  38. package/dist/core/message-stream.js +0 -59
  39. package/dist/index.js.bak +0 -340
  40. package/dist/utils/markdown-to-feishu.js +0 -94
  41. /package/dist/utils/{platform.js → cross-platform.js} +0 -0
  42. /package/dist/{core → utils}/message-cache.js +0 -0
package/dist/cli.js CHANGED
@@ -4,10 +4,15 @@ 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 { cmdInit } from './utils/init.js';
7
+ import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
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';
8
12
  import { cmdInitWechat } from './utils/init-wechat.js';
9
13
  import { cmdInitFeishu } from './utils/init-feishu.js';
10
- import * as platform from './utils/platform.js';
14
+ import * as platform from './utils/cross-platform.js';
15
+ import { EventBus } from './core/event-bus.js';
11
16
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
12
17
  process.removeAllListeners('warning');
13
18
  process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
@@ -86,17 +91,19 @@ function countLines(pkgRoot, logDir) {
86
91
  };
87
92
  console.log('\n[launcher] 正在统计代码行数...\n');
88
93
  const core = countDir(path.join(srcDir, 'core'));
94
+ const agents = countDir(path.join(srcDir, 'agents'));
89
95
  const channels = countDir(path.join(srcDir, 'channels'), 'experimental');
90
96
  const utils = countDir(path.join(srcDir, 'utils'));
91
97
  const entry = countFile(path.join(srcDir, 'index.ts'))
92
98
  + countFile(path.join(srcDir, 'config.ts'))
93
99
  + countFile(path.join(srcDir, 'types.ts'))
94
100
  + countFile(path.join(srcDir, 'cli.ts'));
95
- const total = core + channels + utils + entry;
101
+ const total = core + agents + channels + utils + entry;
96
102
  console.log('==================================================');
97
103
  console.log('EvolClaw 代码统计');
98
104
  console.log('==================================================');
99
105
  console.log(`核心模块: ${String(core).padStart(8)} 行`);
106
+ console.log(`Agent 模块: ${String(agents).padStart(8)} 行`);
100
107
  console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
101
108
  console.log(`工具库: ${String(utils).padStart(8)} 行`);
102
109
  console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
@@ -117,7 +124,7 @@ function countLines(pkgRoot, logDir) {
117
124
  }
118
125
  if (shouldAppend) {
119
126
  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`);
127
+ fs.appendFileSync(statsFile, `${now}\t${core}\t${agents}\t${channels}\t${utils}\t${entry}\t${total}\n`);
121
128
  }
122
129
  showHistory(statsFile);
123
130
  }
@@ -131,21 +138,30 @@ function showHistory(statsFile) {
131
138
  console.log('\n==================================================');
132
139
  console.log('历史记录(最近 8 次)');
133
140
  console.log('==================================================');
134
- console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
141
+ console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'Agent'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
135
142
  console.log('--------------------------------------------------');
136
143
  let prevTotal = null;
137
144
  for (const line of recent) {
138
145
  const parts = line.split('\t');
139
- if (parts.length < 6)
146
+ // 兼容旧格式(6列: time,core,ch,utils,entry,total)和新格式(7列: +agents)
147
+ let time, c, a, ch, u, e, t;
148
+ if (parts.length >= 7) {
149
+ [time, c, a, ch, u, e, t] = parts;
150
+ }
151
+ else if (parts.length >= 6) {
152
+ [time, c, ch, u, e, t] = parts;
153
+ a = '-';
154
+ }
155
+ else {
140
156
  continue;
141
- const [time, c, ch, u, e, t] = parts;
157
+ }
142
158
  const total = parseInt(t, 10);
143
159
  let diff = '-';
144
160
  if (prevTotal !== null) {
145
161
  const change = total - prevTotal;
146
162
  diff = change >= 0 ? `+${change}` : `${change}`;
147
163
  }
148
- console.log(`${time.padEnd(20)} ${c.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
164
+ 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
165
  prevTotal = total;
150
166
  }
151
167
  console.log('==================================================');
@@ -159,6 +175,23 @@ async function cmdStart() {
159
175
  console.log('❌ 配置文件不存在,请先运行 evolclaw init');
160
176
  process.exit(1);
161
177
  }
178
+ // 配置完整性校验
179
+ try {
180
+ const config = loadConfig(p.config);
181
+ const integrity = validateConfigIntegrity(config);
182
+ if (!integrity.valid) {
183
+ console.log(`❌ 配置文件完整性校验失败:`);
184
+ for (const reason of integrity.reasons) {
185
+ console.log(` - ${reason}`);
186
+ }
187
+ console.log(`\n配置文件: ${p.config}`);
188
+ process.exit(1);
189
+ }
190
+ }
191
+ catch (e) {
192
+ console.log(`❌ 配置文件加载失败: ${e.message}`);
193
+ process.exit(1);
194
+ }
162
195
  // 检查 PID 文件
163
196
  const pid = isRunning(p.pid);
164
197
  if (pid) {
@@ -168,7 +201,8 @@ async function cmdStart() {
168
201
  }
169
202
  // 检查是否有残留进程(PID 文件已丢失但进程还在)
170
203
  let hasOrphan = false;
171
- const orphanPids = platform.findProcesses('node.*dist/index.js');
204
+ const evolclawMain = path.join(getPackageRoot(), 'dist', 'index.js');
205
+ const orphanPids = platform.findProcesses(evolclawMain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
172
206
  if (orphanPids.length > 0) {
173
207
  console.log(`⚠ 发现 ${orphanPids.length} 个残留进程,正在清理...`);
174
208
  for (const p of orphanPids) {
@@ -204,7 +238,7 @@ async function cmdStart() {
204
238
  });
205
239
  fs.writeFileSync(p.pid, String(child.pid));
206
240
  child.unref();
207
- // 等待 ready signal(最多 15 秒)
241
+ // 等待 ready signal(最多 30 秒,AUN sidecar 超时 15s + 其他通道连接)
208
242
  const startTime = Date.now();
209
243
  const checkReady = () => {
210
244
  // ready signal 出现(优先检查,避免 Windows 上 isRunning 误判)
@@ -213,6 +247,41 @@ async function cmdStart() {
213
247
  console.log(`✓ EvolClaw started successfully (PID: ${pid})`);
214
248
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
215
249
  console.log(` Logs: ${p.logs}/`);
250
+ // 从主日志提取渠道连接摘要
251
+ const mainLog = path.join(p.logs, 'evolclaw.log');
252
+ if (fs.existsSync(mainLog)) {
253
+ const logLines = fs.readFileSync(mainLog, 'utf-8').split('\n');
254
+ // 从末尾往前找最近一次启动的摘要
255
+ let channelSummary = '';
256
+ for (let i = logLines.length - 1; i >= 0; i--) {
257
+ if (logLines[i].includes('EvolClaw is running with')) {
258
+ channelSummary = logLines[i];
259
+ break;
260
+ }
261
+ }
262
+ if (channelSummary) {
263
+ const match = channelSummary.match(/running with .+/);
264
+ if (match)
265
+ console.log(` ${match[0]}`);
266
+ }
267
+ // 最近一次启动的失败信息
268
+ let lastReadyIdx = -1;
269
+ for (let i = logLines.length - 1; i >= 0; i--) {
270
+ if (logLines[i].includes('Ready signal written')) {
271
+ lastReadyIdx = i;
272
+ break;
273
+ }
274
+ }
275
+ if (lastReadyIdx > 0) {
276
+ for (let i = Math.max(0, lastReadyIdx - 20); i < lastReadyIdx; i++) {
277
+ const line = logLines[i];
278
+ if (line.includes('failed to connect') || line.includes('Failed to create channel')) {
279
+ const match = line.match(/\[WARN\]\s*(.+)/);
280
+ console.log(` ⚠ ${match ? match[1] : line.trim()}`);
281
+ }
282
+ }
283
+ }
284
+ }
216
285
  console.log('');
217
286
  // 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
218
287
  if (resolveRoot() === getPackageRoot()) {
@@ -221,7 +290,7 @@ async function cmdStart() {
221
290
  return;
222
291
  }
223
292
  // 超时
224
- if (Date.now() - startTime > 15000) {
293
+ if (Date.now() - startTime > 30000) {
225
294
  console.log('❌ Failed to start EvolClaw (ready signal timeout)');
226
295
  console.log('');
227
296
  console.log('📝 Error details (last 10 lines of stdout):');
@@ -302,6 +371,43 @@ async function cmdRestart() {
302
371
  await stopAndWait(p.pid);
303
372
  setTimeout(() => cmdStart(), 1000);
304
373
  }
374
+ function formatTimeAgo(ms) {
375
+ const sec = Math.floor(ms / 1000);
376
+ if (sec < 60)
377
+ return '刚刚';
378
+ const min = Math.floor(sec / 60);
379
+ if (min < 60)
380
+ return `${min}分钟前`;
381
+ const hour = Math.floor(min / 60);
382
+ if (hour < 24)
383
+ return `${hour}小时前`;
384
+ const day = Math.floor(hour / 24);
385
+ return `${day}天前`;
386
+ }
387
+ 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');
401
+ }
402
+ // AUN
403
+ const aunAid = config.channels?.aun?.aid;
404
+ if (aunAid && !aunAid.includes('your-') && !aunAid.includes('placeholder')) {
405
+ console.log(` aun: Configured (${aunAid})`);
406
+ }
407
+ else {
408
+ console.log(' aun: - Not configured');
409
+ }
410
+ }
305
411
  async function cmdStatus() {
306
412
  const p = resolvePaths();
307
413
  const pid = isRunning(p.pid);
@@ -320,6 +426,39 @@ async function cmdStatus() {
320
426
  }
321
427
  catch { }
322
428
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
429
+ // Runtime statistics (only when running)
430
+ if (fs.existsSync(p.db)) {
431
+ try {
432
+ const Database = await import('node:sqlite');
433
+ const db = new Database.DatabaseSync(p.db);
434
+ // Get recent active sessions (last 5)
435
+ const recentSessions = db.prepare(`
436
+ SELECT id, project_path, name, channel, chat_type, thread_id, agent_session_id, agent_id, metadata, updated_at
437
+ FROM sessions
438
+ WHERE deleted_at IS NULL
439
+ ORDER BY updated_at DESC
440
+ LIMIT 5
441
+ `).all();
442
+ db.close();
443
+ if (recentSessions.length > 0) {
444
+ console.log('');
445
+ console.log('📋 Recent Active Sessions:');
446
+ for (const s of recentSessions) {
447
+ const projectName = path.basename(s.project_path);
448
+ const sessionType = s.thread_id ? '话题会话' : '主会话';
449
+ const chatType = s.chat_type === 'group' ? '群聊' : '单聊';
450
+ const sessionName = s.name || '默认会话';
451
+ const timeAgo = formatTimeAgo(Date.now() - s.updated_at);
452
+ const meta = s.metadata ? JSON.parse(s.metadata) : {};
453
+ const dot = meta.isActive ? '•' : '○';
454
+ const agentId = s.agent_session_id ? ` [${s.agent_session_id}]` : '';
455
+ const agentType = s.agent_id || 'claude';
456
+ console.log(` ${dot} [${agentType}] ${projectName} / ${sessionName} (${sessionType}, ${chatType})${agentId} - ${timeAgo}`);
457
+ }
458
+ }
459
+ }
460
+ catch { }
461
+ }
323
462
  }
324
463
  else {
325
464
  console.log('⚠ EvolClaw is not running');
@@ -327,16 +466,17 @@ async function cmdStatus() {
327
466
  console.log(` Stale PID file found: ${p.pid}`);
328
467
  }
329
468
  }
469
+ // Session & Project statistics (always show if DB exists)
330
470
  if (fs.existsSync(p.db)) {
331
471
  console.log('');
332
472
  console.log('📦 Sessions & Projects:');
333
473
  try {
334
474
  const Database = await import('node:sqlite');
335
475
  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();
476
+ const totalSessions = db.prepare('SELECT count(*) as cnt FROM sessions WHERE deleted_at IS NULL').get();
477
+ const activeSessions = db.prepare("SELECT count(*) as cnt FROM sessions WHERE json_extract(metadata, '$.isActive') = true AND deleted_at IS NULL").get();
478
+ const uniqueChats = db.prepare('SELECT count(DISTINCT channel_id) as cnt FROM sessions WHERE deleted_at IS NULL').get();
479
+ const projects = db.prepare('SELECT count(DISTINCT project_path) as cnt FROM sessions WHERE deleted_at IS NULL').get();
340
480
  db.close();
341
481
  console.log(` Total sessions: ${totalSessions.cnt} (active: ${activeSessions.cnt})`);
342
482
  console.log(` Unique chats: ${uniqueChats.cnt}`);
@@ -344,102 +484,43 @@ async function cmdStatus() {
344
484
  }
345
485
  catch { }
346
486
  }
347
- // Channel configuration status
487
+ // Channel status
348
488
  if (fs.existsSync(p.config)) {
349
489
  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
- }
490
+ const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
491
+ if (pid) {
492
+ // Running: query IPC for real-time status
493
+ const status = await ipcQuery(p.socket, { type: 'status' });
494
+ if (status) {
495
+ console.log('🔌 Channels (live):');
496
+ 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}`);
367
503
  }
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)');
372
- }
373
- else {
374
- console.log(` Feishu: Connection failed (${msg.slice(0, 80)})`);
375
- }
504
+ if (status.stats) {
505
+ console.log('');
506
+ console.log('📊 Last hour:');
507
+ console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
508
+ if (status.stats.errors > 0)
509
+ console.log(` Errors: ${status.stats.errors}`);
510
+ if (status.stats.completed > 0)
511
+ console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
376
512
  }
377
513
  }
378
514
  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
- }
418
- }
419
- }
420
- 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}`);
515
+ // IPC unreachable but PID exists — show config only
516
+ console.log('🔌 Channels (IPC unreachable):');
517
+ showConfigChannels(config);
440
518
  }
441
519
  }
442
- catch { }
520
+ else {
521
+ console.log('🔌 Channel Configuration:');
522
+ showConfigChannels(config);
523
+ }
443
524
  }
444
525
  console.log('');
445
526
  console.log('📁 Log Files:');
@@ -483,11 +564,17 @@ async function cmdRestartMonitor() {
483
564
  const p = resolvePaths();
484
565
  const restartLog = path.join(p.logs, 'restart.log');
485
566
  const MAX_HEAL_ATTEMPTS = 3;
486
- const READY_TIMEOUT = 15000; // 15s
567
+ const READY_TIMEOUT = 30000; // 30s(AUN sidecar 10s + Feishu 连接 12s)
568
+ const HEAL_TIMEOUT = 30 * 60 * 1000; // 30 分钟,让 claude 自然结束
569
+ const eventBus = new EventBus();
487
570
  const log = (msg) => {
488
571
  const line = `[${new Date().toISOString().replace('T', ' ').slice(0, 19)}] ${msg}\n`;
489
572
  fs.appendFileSync(restartLog, line);
490
573
  };
574
+ /** 检查服务是否已经在运行(ready signal 存在 + 进程存活) */
575
+ const isServiceAlive = () => {
576
+ return fs.existsSync(p.readySignal) && isRunning(p.pid) !== null;
577
+ };
491
578
  log('Restart monitor started');
492
579
  // 读取 restart-pending.json 用于后续通知
493
580
  const pendingFile = path.join(p.dataDir, 'restart-pending.json');
@@ -527,40 +614,90 @@ async function cmdRestartMonitor() {
527
614
  if (started) {
528
615
  log('✓ Service restarted successfully');
529
616
  archiveSelfHealLog(p, log);
530
- await notifyChannel(p, pendingInfo, '✅ 服务重启成功!', log);
531
- cleanupPendingFile(pendingFile, log);
617
+ // 通知由新进程自行发送(channel-agnostic),此处不再调用 notifyChannel
532
618
  process.exit(0);
533
619
  }
534
620
  // 启动失败,进入 self-heal 循环
535
621
  log('❌ Service failed to start, entering self-heal loop');
622
+ eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
536
623
  await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
537
624
  for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
625
+ // 前置检查:服务可能已被上一轮 claude 修复并启动
626
+ if (isServiceAlive()) {
627
+ log(`✓ Service already running before attempt ${attempt}, skipping`);
628
+ await sendHealSummary(p, pendingInfo, attempt - 1, log);
629
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt - 1 });
630
+ archiveSelfHealLog(p, log);
631
+ cleanupPendingFile(pendingFile, log);
632
+ process.exit(0);
633
+ }
538
634
  log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
635
+ eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
539
636
  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);
637
+ const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
638
+ // 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
639
+ if (isServiceAlive()) {
640
+ log(`✓ Service is running after attempt ${attempt}`);
641
+ await sendHealSummary(p, pendingInfo, attempt, log);
642
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
643
+ archiveSelfHealLog(p, log);
644
+ cleanupPendingFile(pendingFile, log);
645
+ process.exit(0);
646
+ }
543
647
  if (!healed) {
544
648
  log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
545
649
  continue;
546
650
  }
547
- // 重新启动
651
+ // claude 正常完成但服务没自动启动,尝试 spawn
548
652
  started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
549
653
  if (started) {
550
654
  log(`✓ Self-heal succeeded on attempt ${attempt}`);
655
+ await sendHealSummary(p, pendingInfo, attempt, log);
656
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: attempt });
551
657
  archiveSelfHealLog(p, log);
552
- await notifyChannel(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
553
658
  cleanupPendingFile(pendingFile, log);
554
659
  process.exit(0);
555
660
  }
556
661
  log(`Attempt ${attempt}: still failing after fix`);
557
662
  }
558
- // 全部失败
663
+ // 全部失败 — 最后再检查一次
664
+ if (isServiceAlive()) {
665
+ log('✓ Service recovered during final check');
666
+ await sendHealSummary(p, pendingInfo, MAX_HEAL_ATTEMPTS, log);
667
+ eventBus.publish({ type: 'self-heal:completed', success: true, attempts: MAX_HEAL_ATTEMPTS });
668
+ archiveSelfHealLog(p, log);
669
+ cleanupPendingFile(pendingFile, log);
670
+ process.exit(0);
671
+ }
559
672
  log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
673
+ eventBus.publish({ type: 'self-heal:completed', success: false, attempts: MAX_HEAL_ATTEMPTS });
560
674
  await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
561
675
  cleanupPendingFile(pendingFile, log);
562
676
  process.exit(1);
563
677
  }
678
+ /**
679
+ * 发送 self-heal 修复成功小结(从 self-heal.md 提取摘要)
680
+ */
681
+ async function sendHealSummary(p, pendingInfo, attempts, log) {
682
+ let summary = `✅ 自动修复成功(第 ${attempts || 1} 次尝试)`;
683
+ try {
684
+ if (fs.existsSync(p.selfHealLog)) {
685
+ const content = fs.readFileSync(p.selfHealLog, 'utf-8');
686
+ // 提取最后一个 ## 章节的要点
687
+ const sections = content.split(/^## /m).filter(Boolean);
688
+ const last = sections[sections.length - 1];
689
+ if (last) {
690
+ const lines = last.split('\n').filter(l => l.startsWith('- ')).map(l => l.trim());
691
+ if (lines.length > 0) {
692
+ summary += '\n' + lines.join('\n');
693
+ }
694
+ }
695
+ }
696
+ }
697
+ catch { }
698
+ summary += '\n\n⚠️ 修复前进行中的任务已中断,如需继续请重新发送。';
699
+ await notifyChannel(p, pendingInfo, summary, log);
700
+ }
564
701
  function sleep(ms) {
565
702
  return new Promise(resolve => setTimeout(resolve, ms));
566
703
  }
@@ -643,16 +780,27 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
643
780
 
644
781
  关键信息:
645
782
  - 项目目录:${projectDir}
646
- - 错误日志:${stdoutLog}(请读取最后 50 行分析错误原因)
647
- - 主日志:${path.join(p.logs, 'evolclaw.log')}(可能包含更多上下文)
783
+ - EVOLCLAW_HOME:${p.root}
784
+ - 错误日志:${stdoutLog}
785
+ - 主日志:${path.join(p.logs, 'evolclaw.log')}(logger 输出在这里,包含 config 校验失败等关键错误)
648
786
  - 修复记录:${selfHealLog}(${selfHealExists})
649
787
 
788
+ ⚠️ 重要诊断技巧:
789
+ - stdout.log 可能是空的(进程秒退时 logger 输出不会到 stdout),一定要同时读 evolclaw.log
790
+ - 必须实际运行进程来复现错误:\`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\`,观察输出和退出码
791
+ - 检查是否有旧进程仍在运行:\`ps aux | grep 'node.*dist/index.js' | grep -v grep\`,旧进程可能占用端口或锁文件
792
+ - 可以运行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js diagnose\` 快速检查配置和数据库
793
+ - 如果进程无任何输出就 exit(1),说明是 process.exit(1) 被显式调用,搜索源码中所有 process.exit(1) 位置
794
+ - evolclaw.json 有自动备份机制:运行时 config watch 检测到文件损坏会自动保存内存快照到 \`data/evolclaw-{timestamp}.json\`,同时 \`data/evolclaw.backup.json\` 是最近一次完整配置的备份。如果 evolclaw.json 损坏或缺失,可以从这些备份恢复
795
+
650
796
  请执行以下步骤:
651
- 1. 读取错误日志,分析启动失败的根本原因
652
- 2. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
653
- 3. 修复代码问题
654
- 4. 执行 npm run build 确认编译通过
655
- 5. 将本次修复内容追加到 ${selfHealLog},格式:
797
+ 1. 读取 ${stdoutLog} 和 ${path.join(p.logs, 'evolclaw.log')} 的最后 50 行
798
+ 2. 运行 \`EVOLCLAW_HOME=${p.root} node dist/index.js 2>&1\` 复现错误(设置 10 秒超时)
799
+ 3. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
800
+ 4. 根据实际复现的错误修复代码
801
+ 5. 执行 npm run build 确认编译通过
802
+ 6. 验证修复:启动服务确认 ready.signal 已写入,然后执行 \`EVOLCLAW_HOME=${p.root} node dist/cli.js stop\` 优雅停止(restart-monitor 会负责最终启动)
803
+ 7. 将本次修复内容追加到 ${selfHealLog},格式:
656
804
  ## 第 ${attempt} 次修复 - {时间}
657
805
  - 错误原因:...
658
806
  - 修复方案:...
@@ -679,12 +827,19 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
679
827
  return true;
680
828
  }
681
829
  catch (error) {
682
- const msg = error.message || String(error);
683
- log(`Claude CLI error: ${msg.slice(0, 800)}`);
830
+ if (error.killed) {
831
+ log(`Claude CLI timeout after ${timeout / 60000}min (attempt ${attempt})`);
832
+ }
833
+ else {
834
+ log(`Claude CLI error: exit code ${error.code ?? 'unknown'} (attempt ${attempt})`);
835
+ }
684
836
  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)}`);
837
+ log(`Claude output: ${String(error.stdout).slice(0, 500)}`);
838
+ if (error.stderr) {
839
+ const stderr = String(error.stderr).replace(/Warning: no stdin.*\n?/g, '').trim();
840
+ if (stderr)
841
+ log(`Claude stderr: ${stderr.slice(0, 300)}`);
842
+ }
688
843
  return false;
689
844
  }
690
845
  }
@@ -803,6 +958,136 @@ async function notifyChannel(p, pendingInfo, message, log) {
803
958
  }
804
959
  }
805
960
  }
961
+ // ==================== Migrate ====================
962
+ async function cmdMv(oldDir, newDir) {
963
+ if (!oldDir || !newDir) {
964
+ console.log('Usage: evolclaw mv <old_directory> <new_directory>');
965
+ console.log('Example: evolclaw mv ~/projects/old-name ~/projects/new-name');
966
+ process.exit(1);
967
+ }
968
+ const oldAbs = path.resolve(oldDir);
969
+ const newAbs = path.resolve(newDir);
970
+ console.log(`迁移项目: ${oldAbs} → ${newAbs}\n`);
971
+ try {
972
+ const r = await migrateProject(oldAbs, newAbs);
973
+ if (r.claudeSessionsMoved)
974
+ console.log('✓ Claude Code 会话目录已迁移');
975
+ if (r.claudeHistoryUpdated)
976
+ console.log('✓ Claude Code history.jsonl 已更新');
977
+ if (r.codexUpdated > 0)
978
+ console.log(`✓ Codex 数据库已更新 (${r.codexUpdated} 个会话)`);
979
+ if (r.directoryMoved)
980
+ console.log('✓ 项目目录已移动');
981
+ if (r.evolclawDbUpdated > 0)
982
+ console.log(`✓ EvolClaw sessions.db 已更新 (${r.evolclawDbUpdated} 个会话)`);
983
+ if (r.evolclawConfigUpdated)
984
+ console.log('✓ evolclaw.json projects.list 已更新');
985
+ console.log('\n迁移完成!');
986
+ }
987
+ catch (e) {
988
+ console.error(`迁移失败: ${e instanceof Error ? e.message : e}`);
989
+ process.exit(1);
990
+ }
991
+ }
992
+ // ==================== Diagnose ====================
993
+ async function cmdDiagnose() {
994
+ const p = resolvePaths();
995
+ let hasError = false;
996
+ // 1. 检查数据目录
997
+ console.log(`[diagnose] EVOLCLAW_HOME = ${p.root}`);
998
+ if (!fs.existsSync(p.root)) {
999
+ console.error(`[diagnose] ❌ 数据目录不存在: ${p.root}`);
1000
+ hasError = true;
1001
+ }
1002
+ else {
1003
+ console.log(`[diagnose] ✓ 数据目录存在`);
1004
+ }
1005
+ // 2. 加载并校验配置
1006
+ try {
1007
+ const config = loadConfig();
1008
+ console.log(`[diagnose] ✓ 配置文件加载成功: ${p.config}`);
1009
+ const integrity = validateConfigIntegrity(config);
1010
+ if (!integrity.valid) {
1011
+ console.error(`[diagnose] ❌ 配置完整性校验失败:\n ${integrity.reasons.join('\n ')}`);
1012
+ hasError = true;
1013
+ }
1014
+ else {
1015
+ console.log(`[diagnose] ✓ 配置完整性校验通过`);
1016
+ }
1017
+ // 3. 检查 Anthropic 配置
1018
+ try {
1019
+ const anthropic = resolveAnthropicConfig(config);
1020
+ console.log(`[diagnose] ✓ Anthropic 配置解析成功 (apiKey: ${anthropic.apiKey ? '已设置' : '❌ 未设置'}, model: ${anthropic.model || 'default'})`);
1021
+ }
1022
+ catch (e) {
1023
+ console.error(`[diagnose] ❌ Anthropic 配置解析失败: ${e instanceof Error ? e.message : e}`);
1024
+ hasError = true;
1025
+ }
1026
+ }
1027
+ catch (e) {
1028
+ console.error(`[diagnose] ❌ 配置文件加载失败: ${e instanceof Error ? e.message : e}`);
1029
+ hasError = true;
1030
+ }
1031
+ // 4. 检查数据库
1032
+ try {
1033
+ const { SessionManager } = await import('./core/session-manager.js');
1034
+ const eventBus = new EventBus();
1035
+ new SessionManager(p.db, eventBus);
1036
+ console.log(`[diagnose] ✓ 数据库初始化成功: ${p.db}`);
1037
+ }
1038
+ catch (e) {
1039
+ console.error(`[diagnose] ❌ 数据库初始化失败: ${e instanceof Error ? e.message : e}`);
1040
+ hasError = true;
1041
+ }
1042
+ // 5. 检查残留进程
1043
+ try {
1044
+ const pid = isRunning(p.pid);
1045
+ if (pid) {
1046
+ console.log(`[diagnose] ⚠️ 已有进程运行中: PID ${pid}`);
1047
+ }
1048
+ else {
1049
+ console.log(`[diagnose] ✓ 无残留进程`);
1050
+ }
1051
+ }
1052
+ catch {
1053
+ console.log(`[diagnose] ✓ 无 PID 文件`);
1054
+ }
1055
+ // 6. 检查关键文件
1056
+ const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
1057
+ if (!fs.existsSync(appMain)) {
1058
+ console.error(`[diagnose] ❌ 编译产物不存在: ${appMain}`);
1059
+ hasError = true;
1060
+ }
1061
+ else {
1062
+ console.log(`[diagnose] ✓ 编译产物存在: ${appMain}`);
1063
+ }
1064
+ if (hasError) {
1065
+ console.error('\n[diagnose] ❌ 诊断发现问题,请修复后重试');
1066
+ process.exit(1);
1067
+ }
1068
+ else {
1069
+ console.log('\n[diagnose] ✓ 所有检查通过');
1070
+ }
1071
+ }
1072
+ async function cmdTui() {
1073
+ const config = loadConfig();
1074
+ const aun = config.channels?.aun;
1075
+ if (!aun?.owner || !aun?.aid) {
1076
+ console.error('[tui] AUN 未配置,请先运行: evolclaw init aun');
1077
+ process.exit(1);
1078
+ }
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) {
1084
+ process.exit(1);
1085
+ }
1086
+ const pythonBin = aun.pythonBin || process.env.AUN_PYTHON || 'python3';
1087
+ const cliScript = path.join(getPackageRoot(), 'aun', 'aun_cli.py');
1088
+ const child = spawn(pythonBin, [cliScript, '-a', aun.owner, '-t', aun.aid], { stdio: 'inherit' });
1089
+ child.on('exit', (code) => process.exit(code ?? 0));
1090
+ }
806
1091
  // ==================== Main ====================
807
1092
  export async function main(args) {
808
1093
  const cmd = args[0] || 'start';
@@ -814,6 +1099,9 @@ export async function main(args) {
814
1099
  else if (args[1] === 'feishu') {
815
1100
  await cmdInitFeishu();
816
1101
  }
1102
+ else if (args[1] === 'aun') {
1103
+ await cmdInitAun();
1104
+ }
817
1105
  else {
818
1106
  await cmdInit();
819
1107
  }
@@ -836,18 +1124,31 @@ export async function main(args) {
836
1124
  case 'restart-monitor':
837
1125
  await cmdRestartMonitor();
838
1126
  break;
1127
+ case 'mv':
1128
+ await cmdMv(args[1], args[2]);
1129
+ break;
1130
+ case 'diagnose':
1131
+ await cmdDiagnose();
1132
+ break;
1133
+ case 'tui':
1134
+ await cmdTui();
1135
+ break;
839
1136
  default:
840
- console.log(`Usage: evolclaw {init|start|stop|restart|status|logs}
1137
+ console.log(`Usage: evolclaw {init|start|stop|restart|status|logs|tui|diagnose|mv}
841
1138
 
842
1139
  Commands:
843
1140
  init 创建配置文件 (${resolvePaths().config})
844
- init wechat 微信扫码登录并写入配置
845
1141
  init feishu 飞书扫码登录并写入配置
1142
+ init wechat 微信扫码登录并写入配置
1143
+ init aun AUN (AgentUnin.Network) 配置
846
1144
  start 启动服务 (默认)
847
1145
  stop 停止服务
848
1146
  restart 重启服务
849
1147
  status 查看状态
850
1148
  logs 查看日志 (tail -f)
1149
+ tui 启动 AUN TUI 客户端
1150
+ diagnose 诊断启动环境(配置、数据库、进程)
1151
+ mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
851
1152
 
852
1153
  Environment:
853
1154
  EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)