evolclaw 2.0.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.
package/dist/cli.js ADDED
@@ -0,0 +1,759 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn, execFileSync, execFile } from 'child_process';
4
+ import { promisify } from 'util';
5
+ import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
6
+ import { cmdInit } from './utils/init.js';
7
+ const execFileAsync = promisify(execFile);
8
+ // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
9
+ function cleanEnv() {
10
+ for (const key of [
11
+ 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT',
12
+ 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
13
+ 'CLAUDE_CONFIG_DIR', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL'
14
+ ]) {
15
+ delete process.env[key];
16
+ }
17
+ }
18
+ function isRunning(pidFile) {
19
+ if (!fs.existsSync(pidFile))
20
+ return null;
21
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
22
+ try {
23
+ process.kill(pid, 0);
24
+ return pid;
25
+ }
26
+ catch {
27
+ fs.unlinkSync(pidFile);
28
+ return null;
29
+ }
30
+ }
31
+ function killAllInstances() {
32
+ try {
33
+ const output = execFileSync('pgrep', ['-f', 'node.*dist/index.js'], { encoding: 'utf-8' }).trim();
34
+ if (output) {
35
+ const pids = output.split('\n');
36
+ console.log(` Found ${pids.length} running instance(s), stopping them...`);
37
+ for (const pid of pids) {
38
+ try {
39
+ process.kill(parseInt(pid, 10));
40
+ }
41
+ catch { }
42
+ }
43
+ }
44
+ }
45
+ catch { }
46
+ }
47
+ function rotateLogs(logDir) {
48
+ if (!fs.existsSync(logDir))
49
+ return;
50
+ const MAX_SIZE = 10 * 1024 * 1024; // 10MB
51
+ for (const file of fs.readdirSync(logDir)) {
52
+ if (!file.endsWith('.log'))
53
+ continue;
54
+ const filePath = path.join(logDir, file);
55
+ const stat = fs.statSync(filePath);
56
+ if (stat.size > MAX_SIZE) {
57
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
58
+ const newPath = `${filePath}.${timestamp}`;
59
+ fs.renameSync(filePath, newPath);
60
+ console.log(` Rotated: ${file} -> ${path.basename(newPath)}`);
61
+ }
62
+ }
63
+ // 清理 7 天前的旧日志
64
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
65
+ for (const file of fs.readdirSync(logDir)) {
66
+ if (!file.includes('.log.'))
67
+ continue;
68
+ const filePath = path.join(logDir, file);
69
+ const stat = fs.statSync(filePath);
70
+ if (stat.mtimeMs < cutoff) {
71
+ fs.unlinkSync(filePath);
72
+ }
73
+ }
74
+ }
75
+ function countLines(pkgRoot, logDir) {
76
+ const srcDir = path.join(pkgRoot, 'src');
77
+ const statsFile = path.join(logDir, 'line-stats.log');
78
+ const countDir = (dir, exclude) => {
79
+ if (!fs.existsSync(dir))
80
+ return 0;
81
+ let total = 0;
82
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
83
+ if (exclude && entry.name === exclude)
84
+ continue;
85
+ const full = path.join(dir, entry.name);
86
+ if (entry.isDirectory()) {
87
+ total += countDir(full);
88
+ }
89
+ else if (entry.name.endsWith('.ts')) {
90
+ total += fs.readFileSync(full, 'utf-8').split('\n').length;
91
+ }
92
+ }
93
+ return total;
94
+ };
95
+ const countFile = (filePath) => {
96
+ if (!fs.existsSync(filePath))
97
+ return 0;
98
+ return fs.readFileSync(filePath, 'utf-8').split('\n').length;
99
+ };
100
+ console.log('\n[launcher] 正在统计代码行数...\n');
101
+ const core = countDir(path.join(srcDir, 'core'));
102
+ const channels = countDir(path.join(srcDir, 'channels'), 'experimental');
103
+ const utils = countDir(path.join(srcDir, 'utils'));
104
+ const entry = countFile(path.join(srcDir, 'index.ts'))
105
+ + countFile(path.join(srcDir, 'config.ts'))
106
+ + countFile(path.join(srcDir, 'types.ts'))
107
+ + countFile(path.join(srcDir, 'cli.ts'));
108
+ const total = core + channels + utils + entry;
109
+ console.log('==================================================');
110
+ console.log('EvolClaw 代码统计');
111
+ console.log('==================================================');
112
+ console.log(`核心模块: ${String(core).padStart(8)} 行`);
113
+ console.log(`渠道适配: ${String(channels).padStart(8)} 行`);
114
+ console.log(`工具库: ${String(utils).padStart(8)} 行`);
115
+ console.log(`入口与配置: ${String(entry).padStart(8)} 行`);
116
+ console.log('--------------------------------------------------');
117
+ console.log(`总计: ${String(total).padStart(8)} 行`);
118
+ console.log('==================================================');
119
+ // 追加历史记录(仅在数据变化时)
120
+ let shouldAppend = true;
121
+ if (fs.existsSync(statsFile)) {
122
+ const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
123
+ if (lines.length > 0) {
124
+ const lastLine = lines[lines.length - 1];
125
+ const lastTotal = lastLine.split('\t').pop();
126
+ if (lastTotal === String(total)) {
127
+ shouldAppend = false;
128
+ }
129
+ }
130
+ }
131
+ if (shouldAppend) {
132
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
133
+ fs.appendFileSync(statsFile, `${now}\t${core}\t${channels}\t${utils}\t${entry}\t${total}\n`);
134
+ }
135
+ showHistory(statsFile);
136
+ }
137
+ function showHistory(statsFile) {
138
+ if (!fs.existsSync(statsFile))
139
+ return;
140
+ const lines = fs.readFileSync(statsFile, 'utf-8').trim().split('\n');
141
+ if (lines.length < 2)
142
+ return;
143
+ const recent = lines.slice(-8);
144
+ console.log('\n==================================================');
145
+ console.log('历史记录(最近 8 次)');
146
+ console.log('==================================================');
147
+ console.log(`${'时间'.padEnd(20)} ${'核心'.padStart(6)} ${'渠道'.padStart(6)} ${'工具'.padStart(6)} ${'入口'.padStart(6)} ${'总计'.padStart(6)} ${'变化'.padStart(8)}`);
148
+ console.log('--------------------------------------------------');
149
+ let prevTotal = null;
150
+ for (const line of recent) {
151
+ const parts = line.split('\t');
152
+ if (parts.length < 6)
153
+ continue;
154
+ const [time, c, ch, u, e, t] = parts;
155
+ const total = parseInt(t, 10);
156
+ let diff = '-';
157
+ if (prevTotal !== null) {
158
+ const change = total - prevTotal;
159
+ diff = change >= 0 ? `+${change}` : `${change}`;
160
+ }
161
+ console.log(`${time.padEnd(20)} ${c.padStart(6)} ${ch.padStart(6)} ${u.padStart(6)} ${e.padStart(6)} ${t.padStart(6)} ${diff.padStart(8)}`);
162
+ prevTotal = total;
163
+ }
164
+ console.log('==================================================');
165
+ }
166
+ // ==================== Commands ====================
167
+ function cmdStart() {
168
+ const p = resolvePaths();
169
+ ensureDataDirs();
170
+ // 检查配置文件
171
+ if (!fs.existsSync(p.config)) {
172
+ console.log('❌ 配置文件不存在,请先运行 evolclaw init');
173
+ process.exit(1);
174
+ }
175
+ // 检查 PID 文件
176
+ const pid = isRunning(p.pid);
177
+ if (pid) {
178
+ console.log(`❌ EvolClaw is already running (PID: ${pid})`);
179
+ console.log(' 使用 evolclaw restart 重启,或 evolclaw stop 先停止');
180
+ process.exit(1);
181
+ }
182
+ // 检查是否有残留进程(PID 文件已丢失但进程还在)
183
+ let hasOrphan = false;
184
+ try {
185
+ const output = execFileSync('pgrep', ['-f', 'node.*dist/index.js'], { encoding: 'utf-8' }).trim();
186
+ if (output) {
187
+ const pids = output.split('\n');
188
+ console.log(`⚠ 发现 ${pids.length} 个残留进程,正在清理...`);
189
+ for (const p of pids) {
190
+ try {
191
+ process.kill(parseInt(p, 10));
192
+ }
193
+ catch { }
194
+ }
195
+ hasOrphan = true;
196
+ }
197
+ }
198
+ catch { }
199
+ // 如果清理了残留进程,等待它们退出
200
+ if (hasOrphan) {
201
+ execFileSync('sleep', ['2']);
202
+ }
203
+ console.log('🚀 Starting EvolClaw...');
204
+ rotateLogs(p.logs);
205
+ cleanEnv();
206
+ // 删除旧的 ready signal
207
+ try {
208
+ fs.unlinkSync(p.readySignal);
209
+ }
210
+ catch { }
211
+ const stdoutLog = path.join(p.logs, 'stdout.log');
212
+ const out = fs.openSync(stdoutLog, 'a');
213
+ const err = fs.openSync(stdoutLog, 'a');
214
+ const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
215
+ const child = spawn('node', [appMain], {
216
+ detached: true,
217
+ stdio: ['ignore', out, err],
218
+ env: {
219
+ ...process.env,
220
+ LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
221
+ MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
222
+ EVENT_LOG: process.env.EVENT_LOG || 'true',
223
+ }
224
+ });
225
+ fs.writeFileSync(p.pid, String(child.pid));
226
+ child.unref();
227
+ // 等待 ready signal(最多 15 秒)
228
+ const startTime = Date.now();
229
+ const checkReady = () => {
230
+ // 进程已退出
231
+ if (!isRunning(p.pid)) {
232
+ console.log('❌ Failed to start EvolClaw');
233
+ console.log('');
234
+ console.log('📝 Error details (last 10 lines of stdout):');
235
+ if (fs.existsSync(stdoutLog)) {
236
+ const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
237
+ console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
238
+ }
239
+ process.exit(1);
240
+ return;
241
+ }
242
+ // ready signal 出现
243
+ if (fs.existsSync(p.readySignal)) {
244
+ const pid = isRunning(p.pid);
245
+ console.log(`✓ EvolClaw started successfully (PID: ${pid})`);
246
+ console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
247
+ console.log(` Logs: ${p.logs}/`);
248
+ console.log('');
249
+ // 代码统计仅在开发环境显示(EVOLCLAW_HOME 指向包目录)
250
+ if (resolveRoot() === getPackageRoot()) {
251
+ countLines(getPackageRoot(), p.logs);
252
+ }
253
+ return;
254
+ }
255
+ // 超时
256
+ if (Date.now() - startTime > 15000) {
257
+ console.log('❌ Failed to start EvolClaw (ready signal timeout)');
258
+ console.log('');
259
+ console.log('📝 Error details (last 10 lines of stdout):');
260
+ if (fs.existsSync(stdoutLog)) {
261
+ const content = fs.readFileSync(stdoutLog, 'utf-8').trim().split('\n');
262
+ console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
263
+ }
264
+ process.exit(1);
265
+ return;
266
+ }
267
+ setTimeout(checkReady, 500);
268
+ };
269
+ setTimeout(checkReady, 1000);
270
+ }
271
+ function cmdStop() {
272
+ const p = resolvePaths();
273
+ const pid = isRunning(p.pid);
274
+ if (!pid) {
275
+ console.log('⚠ EvolClaw is not running');
276
+ return;
277
+ }
278
+ console.log(`🛑 Stopping EvolClaw (PID: ${pid})...`);
279
+ process.kill(pid);
280
+ let waited = 0;
281
+ const check = setInterval(() => {
282
+ waited++;
283
+ try {
284
+ process.kill(pid, 0);
285
+ }
286
+ catch {
287
+ clearInterval(check);
288
+ try {
289
+ fs.unlinkSync(p.pid);
290
+ }
291
+ catch { }
292
+ console.log('✓ EvolClaw stopped');
293
+ return;
294
+ }
295
+ if (waited >= 10) {
296
+ clearInterval(check);
297
+ try {
298
+ process.kill(pid, 9);
299
+ }
300
+ catch { }
301
+ try {
302
+ fs.unlinkSync(p.pid);
303
+ }
304
+ catch { }
305
+ console.log('✓ EvolClaw stopped (forced)');
306
+ }
307
+ }, 1000);
308
+ }
309
+ function cmdRestart() {
310
+ console.log('🔄 Restarting EvolClaw...');
311
+ const p = resolvePaths();
312
+ const pid = isRunning(p.pid);
313
+ if (pid) {
314
+ process.kill(pid);
315
+ let waited = 0;
316
+ while (waited < 10) {
317
+ try {
318
+ process.kill(pid, 0);
319
+ execFileSync('sleep', ['1']);
320
+ waited++;
321
+ }
322
+ catch {
323
+ break;
324
+ }
325
+ }
326
+ if (waited >= 10) {
327
+ try {
328
+ process.kill(pid, 9);
329
+ }
330
+ catch { }
331
+ }
332
+ try {
333
+ fs.unlinkSync(p.pid);
334
+ }
335
+ catch { }
336
+ }
337
+ setTimeout(() => cmdStart(), 1000);
338
+ }
339
+ async function cmdStatus() {
340
+ const p = resolvePaths();
341
+ const pid = isRunning(p.pid);
342
+ if (pid) {
343
+ console.log(`✓ EvolClaw is running (PID: ${pid})`);
344
+ console.log('');
345
+ console.log('📊 Process Info:');
346
+ try {
347
+ const uptime = execFileSync('ps', ['-p', String(pid), '-o', 'etime='], { encoding: 'utf-8' }).trim();
348
+ const cpu = execFileSync('ps', ['-p', String(pid), '-o', '%cpu='], { encoding: 'utf-8' }).trim();
349
+ const mem = execFileSync('ps', ['-p', String(pid), '-o', 'rss='], { encoding: 'utf-8' }).trim();
350
+ console.log(` Uptime: ${uptime}`);
351
+ console.log(` CPU: ${cpu}%`);
352
+ console.log(` Memory: ${mem} KB`);
353
+ }
354
+ catch { }
355
+ console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
356
+ }
357
+ else {
358
+ console.log('⚠ EvolClaw is not running');
359
+ if (fs.existsSync(p.pid)) {
360
+ console.log(` Stale PID file found: ${p.pid}`);
361
+ }
362
+ }
363
+ if (fs.existsSync(p.db)) {
364
+ console.log('');
365
+ console.log('📦 Sessions & Projects:');
366
+ try {
367
+ const output = execFileSync('sqlite3', [p.db,
368
+ 'SELECT count(*) FROM sessions; SELECT count(*) FROM sessions WHERE is_active=1; SELECT count(DISTINCT channel_id) FROM sessions; SELECT count(DISTINCT project_path) FROM sessions;'
369
+ ], { encoding: 'utf-8' }).trim().split('\n');
370
+ if (output.length >= 4) {
371
+ console.log(` 会话总数: ${output[0]} (活跃: ${output[1]})`);
372
+ console.log(` 独立会话: ${output[2]} 个`);
373
+ console.log(` 涉及项目: ${output[3]} 个`);
374
+ }
375
+ }
376
+ catch { }
377
+ }
378
+ // 渠道配置状态
379
+ if (fs.existsSync(p.config)) {
380
+ console.log('');
381
+ console.log('🔌 渠道配置:');
382
+ try {
383
+ const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
384
+ if (config.feishu?.appId && config.feishu?.appSecret) {
385
+ // 验证飞书凭证连通性
386
+ try {
387
+ const lark = await import('@larksuiteoapi/node-sdk');
388
+ const client = new lark.Client({ appId: config.feishu.appId, appSecret: config.feishu.appSecret });
389
+ const res = await client.auth.tenantAccessToken.internal({
390
+ data: { app_id: config.feishu.appId, app_secret: config.feishu.appSecret },
391
+ });
392
+ if (res.code === 0) {
393
+ console.log(` 飞书: ✓ 已连接 (App ID: ${config.feishu.appId.slice(0, 8)}...)`);
394
+ }
395
+ else {
396
+ console.log(` 飞书: ✗ 连接拒绝 (${res.msg})`);
397
+ }
398
+ }
399
+ catch (e) {
400
+ const msg = e.message || '';
401
+ if (msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH') || msg.includes('ENOTFOUND')) {
402
+ console.log(' 飞书: ✗ 连接超时(网络不可达)');
403
+ }
404
+ else {
405
+ console.log(` 飞书: ✗ 连接失败 (${msg.slice(0, 80)})`);
406
+ }
407
+ }
408
+ }
409
+ else {
410
+ console.log(' 飞书: - 未配置');
411
+ }
412
+ if (config.aun?.domain && config.aun?.agentName) {
413
+ console.log(` AUN: ✓ 已配置 (${config.aun.agentName}@${config.aun.domain})`);
414
+ }
415
+ else {
416
+ console.log(' AUN: - 未配置');
417
+ }
418
+ if (config.anthropic?.model) {
419
+ console.log(` 模型: ${config.anthropic.model}`);
420
+ }
421
+ if (config.projects?.defaultPath) {
422
+ console.log(` 默认项目: ${config.projects.defaultPath}`);
423
+ }
424
+ }
425
+ catch { }
426
+ }
427
+ console.log('');
428
+ console.log('📁 Log Files:');
429
+ const mainLog = path.join(p.logs, 'evolclaw.log');
430
+ if (fs.existsSync(mainLog)) {
431
+ const stat = fs.statSync(mainLog);
432
+ const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
433
+ console.log(` Main log: ${mainLog} (${sizeMB} MB)`);
434
+ console.log('');
435
+ console.log('📝 Recent activity (last 10 lines):');
436
+ const content = fs.readFileSync(mainLog, 'utf-8').trim().split('\n');
437
+ console.log(content.slice(-10).map(l => ` ${l}`).join('\n'));
438
+ }
439
+ else {
440
+ console.log(' (no log file yet)');
441
+ }
442
+ }
443
+ function cmdLogs() {
444
+ const p = resolvePaths();
445
+ const mainLog = path.join(p.logs, 'evolclaw.log');
446
+ if (!fs.existsSync(mainLog)) {
447
+ console.log(`❌ Log file not found: ${mainLog}`);
448
+ process.exit(1);
449
+ }
450
+ const child = spawn('tail', ['-f', mainLog], { stdio: 'inherit' });
451
+ child.on('exit', (code) => process.exit(code || 0));
452
+ }
453
+ /**
454
+ * restart-monitor: 内部命令,由 /restart 命令调用
455
+ * 支持 self-heal:启动失败时调用 claude CLI 自动修复,最多重试 3 次
456
+ */
457
+ async function cmdRestartMonitor() {
458
+ const p = resolvePaths();
459
+ const restartLog = path.join(p.logs, 'restart.log');
460
+ const MAX_HEAL_ATTEMPTS = 3;
461
+ const READY_TIMEOUT = 15000; // 15s
462
+ const log = (msg) => {
463
+ const line = `[${new Date().toISOString().replace('T', ' ').slice(0, 19)}] ${msg}\n`;
464
+ fs.appendFileSync(restartLog, line);
465
+ };
466
+ log('Restart monitor started');
467
+ // 读取 restart-pending.json 用于后续通知
468
+ const pendingFile = path.join(p.dataDir, 'restart-pending.json');
469
+ let pendingInfo = null;
470
+ try {
471
+ if (fs.existsSync(pendingFile)) {
472
+ pendingInfo = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
473
+ }
474
+ }
475
+ catch { }
476
+ // 等待旧进程退出
477
+ if (fs.existsSync(p.pid)) {
478
+ const oldPid = parseInt(fs.readFileSync(p.pid, 'utf-8').trim(), 10);
479
+ log(`Monitoring process PID: ${oldPid}`);
480
+ await new Promise((resolve) => {
481
+ let waited = 0;
482
+ const interval = setInterval(() => {
483
+ waited++;
484
+ try {
485
+ process.kill(oldPid, 0);
486
+ }
487
+ catch {
488
+ clearInterval(interval);
489
+ log(`Process ${oldPid} has exited`);
490
+ resolve();
491
+ return;
492
+ }
493
+ if (waited >= 30) {
494
+ clearInterval(interval);
495
+ log('ERROR: Process still running after 30s, force killing');
496
+ try {
497
+ process.kill(oldPid, 9);
498
+ }
499
+ catch { }
500
+ resolve();
501
+ }
502
+ }, 1000);
503
+ });
504
+ await sleep(3000);
505
+ }
506
+ // 启动并检测 ready signal
507
+ let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
508
+ if (started) {
509
+ log('✓ Service restarted successfully');
510
+ archiveSelfHealLog(p, log);
511
+ await notifyFeishu(p, pendingInfo, '✅ 服务重启成功!', log);
512
+ cleanupPendingFile(pendingFile, log);
513
+ process.exit(0);
514
+ }
515
+ // 启动失败,进入 self-heal 循环
516
+ log('❌ Service failed to start, entering self-heal loop');
517
+ await notifyFeishu(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
518
+ for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
519
+ log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
520
+ await notifyFeishu(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
521
+ // 调用 claude CLI 修复
522
+ const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, log);
523
+ if (!healed) {
524
+ log(`Self-heal attempt ${attempt} failed (claude invocation error)`);
525
+ continue;
526
+ }
527
+ // 重新启动
528
+ started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
529
+ if (started) {
530
+ log(`✓ Self-heal succeeded on attempt ${attempt}`);
531
+ archiveSelfHealLog(p, log);
532
+ await notifyFeishu(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
533
+ cleanupPendingFile(pendingFile, log);
534
+ process.exit(0);
535
+ }
536
+ log(`Attempt ${attempt}: still failing after fix`);
537
+ }
538
+ // 全部失败
539
+ log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
540
+ await notifyFeishu(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
541
+ cleanupPendingFile(pendingFile, log);
542
+ process.exit(1);
543
+ }
544
+ function sleep(ms) {
545
+ return new Promise(resolve => setTimeout(resolve, ms));
546
+ }
547
+ function cleanupPendingFile(filePath, log) {
548
+ try {
549
+ if (fs.existsSync(filePath)) {
550
+ fs.unlinkSync(filePath);
551
+ log('Cleaned up restart-pending.json');
552
+ }
553
+ }
554
+ catch { }
555
+ }
556
+ /**
557
+ * 启动新进程并等待 ready.signal
558
+ */
559
+ async function spawnAndWaitReady(p, log, timeout) {
560
+ // 删除旧的 ready signal
561
+ try {
562
+ fs.unlinkSync(p.readySignal);
563
+ }
564
+ catch { }
565
+ // 杀掉可能残留的进程
566
+ try {
567
+ fs.unlinkSync(p.pid);
568
+ }
569
+ catch { }
570
+ cleanEnv();
571
+ const stdoutLog = path.join(p.logs, 'stdout.log');
572
+ const out = fs.openSync(stdoutLog, 'a');
573
+ const err = fs.openSync(stdoutLog, 'a');
574
+ const appMain = path.join(getPackageRoot(), 'dist', 'index.js');
575
+ const child = spawn('node', [appMain], {
576
+ detached: true,
577
+ stdio: ['ignore', out, err],
578
+ env: {
579
+ ...process.env,
580
+ LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
581
+ MESSAGE_LOG: process.env.MESSAGE_LOG || 'true',
582
+ EVENT_LOG: process.env.EVENT_LOG || 'true',
583
+ }
584
+ });
585
+ fs.writeFileSync(p.pid, String(child.pid));
586
+ child.unref();
587
+ log(`Spawned new process PID: ${child.pid}, waiting for ready signal...`);
588
+ // 轮询等待 ready.signal 出现
589
+ const start = Date.now();
590
+ while (Date.now() - start < timeout) {
591
+ await sleep(500);
592
+ // 进程已退出则提前失败
593
+ if (!isRunning(p.pid)) {
594
+ log('Process exited before ready signal');
595
+ return false;
596
+ }
597
+ if (fs.existsSync(p.readySignal)) {
598
+ log('Ready signal detected');
599
+ return true;
600
+ }
601
+ }
602
+ log(`Ready signal not received within ${timeout / 1000}s`);
603
+ // 超时后杀掉进程
604
+ const pid = isRunning(p.pid);
605
+ if (pid) {
606
+ try {
607
+ process.kill(pid);
608
+ }
609
+ catch { }
610
+ try {
611
+ fs.unlinkSync(p.pid);
612
+ }
613
+ catch { }
614
+ }
615
+ return false;
616
+ }
617
+ /**
618
+ * 调用 claude CLI 进行自动修复
619
+ */
620
+ async function invokeClaude(p, attempt, maxAttempts, log) {
621
+ const projectDir = getPackageRoot();
622
+ const selfHealLog = p.selfHealLog;
623
+ const stdoutLog = path.join(p.logs, 'stdout.log');
624
+ const selfHealExists = fs.existsSync(selfHealLog) ? '存在,请先阅读之前的修复记录' : '不存在(首次修复)';
625
+ const prompt = `EvolClaw 服务启动失败,需要你诊断并修复。这是第 ${attempt}/${maxAttempts} 次自动修复尝试。
626
+
627
+ 关键信息:
628
+ - 项目目录:${projectDir}
629
+ - 错误日志:${stdoutLog}(请读取最后 50 行分析错误原因)
630
+ - 主日志:${path.join(p.logs, 'evolclaw.log')}(可能包含更多上下文)
631
+ - 修复记录:${selfHealLog}(${selfHealExists})
632
+
633
+ 请执行以下步骤:
634
+ 1. 读取错误日志,分析启动失败的根本原因
635
+ 2. 如果 ${selfHealLog} 存在,先阅读之前的修复记录,避免重复尝试已失败的方案
636
+ 3. 修复代码问题
637
+ 4. 执行 npm run build 确认编译通过
638
+ 5. 将本次修复内容追加到 ${selfHealLog},格式:
639
+ ## 第 ${attempt} 次修复 - {时间}
640
+ - 错误原因:...
641
+ - 修复方案:...
642
+ - 修改文件:...
643
+
644
+ 注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
645
+ try {
646
+ log(`Invoking claude CLI (attempt ${attempt})...`);
647
+ const { stdout, stderr } = await execFileAsync('claude', [
648
+ '-p', prompt,
649
+ '--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
650
+ '--output-format', 'text',
651
+ ], {
652
+ cwd: projectDir,
653
+ timeout: 5 * 60 * 1000, // 5 分钟超时
654
+ env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
655
+ maxBuffer: 10 * 1024 * 1024,
656
+ });
657
+ if (stdout)
658
+ log(`Claude output: ${stdout.slice(0, 500)}`);
659
+ if (stderr)
660
+ log(`Claude stderr: ${stderr.slice(0, 200)}`);
661
+ log(`Claude CLI completed (attempt ${attempt})`);
662
+ return true;
663
+ }
664
+ catch (error) {
665
+ log(`Claude CLI error: ${error.message?.slice(0, 300) || error}`);
666
+ return false;
667
+ }
668
+ }
669
+ /**
670
+ * 归档 self-heal.md
671
+ */
672
+ function archiveSelfHealLog(p, log) {
673
+ if (!fs.existsSync(p.selfHealLog))
674
+ return;
675
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
676
+ const archivePath = path.join(p.logs, `self-heal-${timestamp}.md`);
677
+ fs.renameSync(p.selfHealLog, archivePath);
678
+ log(`Archived self-heal log to ${archivePath}`);
679
+ }
680
+ /**
681
+ * 通过 Feishu API 发送通知(轻量级,不依赖 FeishuChannel)
682
+ */
683
+ async function notifyFeishu(p, pendingInfo, message, log) {
684
+ if (!pendingInfo || pendingInfo.channel !== 'feishu')
685
+ return;
686
+ try {
687
+ const configPath = path.join(p.dataDir, 'evolclaw.json');
688
+ if (!fs.existsSync(configPath))
689
+ return;
690
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
691
+ if (!config.feishu?.appId || !config.feishu?.appSecret)
692
+ return;
693
+ const lark = await import('@larksuiteoapi/node-sdk');
694
+ const client = new lark.Client({
695
+ appId: config.feishu.appId,
696
+ appSecret: config.feishu.appSecret,
697
+ });
698
+ await client.im.message.create({
699
+ params: { receive_id_type: 'chat_id' },
700
+ data: {
701
+ receive_id: pendingInfo.channelId,
702
+ msg_type: 'text',
703
+ content: JSON.stringify({ text: message }),
704
+ },
705
+ });
706
+ log(`Feishu notification sent: ${message.slice(0, 50)}`);
707
+ }
708
+ catch (error) {
709
+ log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
710
+ }
711
+ }
712
+ // ==================== Main ====================
713
+ export async function main(args) {
714
+ const cmd = args[0] || 'start';
715
+ switch (cmd) {
716
+ case 'init':
717
+ await cmdInit();
718
+ break;
719
+ case 'start':
720
+ cmdStart();
721
+ break;
722
+ case 'stop':
723
+ cmdStop();
724
+ break;
725
+ case 'restart':
726
+ cmdRestart();
727
+ break;
728
+ case 'status':
729
+ await cmdStatus();
730
+ break;
731
+ case 'logs':
732
+ cmdLogs();
733
+ break;
734
+ case 'restart-monitor':
735
+ await cmdRestartMonitor();
736
+ break;
737
+ default:
738
+ console.log(`Usage: evolclaw {init|start|stop|restart|status|logs}
739
+
740
+ Commands:
741
+ init 创建配置文件 (${resolvePaths().config})
742
+ start 启动服务 (默认)
743
+ stop 停止服务
744
+ restart 重启服务
745
+ status 查看状态
746
+ logs 查看日志 (tail -f)
747
+
748
+ Environment:
749
+ EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)
750
+ LOG_LEVEL 日志级别 (默认: INFO)
751
+ MESSAGE_LOG 消息日志 (默认: true)
752
+ EVENT_LOG 事件日志 (默认: true)`);
753
+ process.exit(1);
754
+ }
755
+ }
756
+ // 直接运行时自动执行(node dist/cli.js ...)
757
+ if (import.meta.url === `file://${process.argv[1]}`) {
758
+ main(process.argv.slice(2));
759
+ }