evolclaw 2.0.0 → 2.0.2
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/data/evolclaw.sample.json +31 -26
- package/dist/channels/wechat.js +451 -0
- package/dist/cli.js +196 -146
- package/dist/config.js +32 -17
- package/dist/core/agent-runner.js +27 -41
- package/dist/core/command-handler.js +72 -54
- package/dist/core/message-processor.js +36 -10
- package/dist/core/message-queue.js +9 -3
- package/dist/core/session-manager.js +81 -238
- package/dist/index.js +189 -115
- package/dist/utils/init-feishu.js +261 -0
- package/dist/utils/init-wechat.js +170 -0
- package/dist/utils/init.js +120 -67
- package/dist/utils/stream-flusher.js +3 -2
- package/package.json +9 -7
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ import { spawn, execFileSync, execFile } from 'child_process';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
|
|
6
6
|
import { cmdInit } from './utils/init.js';
|
|
7
|
+
import { cmdInitWechat } from './utils/init-wechat.js';
|
|
8
|
+
import { cmdInitFeishu } from './utils/init-feishu.js';
|
|
7
9
|
const execFileAsync = promisify(execFile);
|
|
8
10
|
// 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
|
|
9
11
|
function cleanEnv() {
|
|
@@ -28,47 +30,29 @@ function isRunning(pidFile) {
|
|
|
28
30
|
return null;
|
|
29
31
|
}
|
|
30
32
|
}
|
|
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
33
|
function rotateLogs(logDir) {
|
|
48
34
|
if (!fs.existsSync(logDir))
|
|
49
35
|
return;
|
|
50
36
|
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
37
|
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
65
38
|
for (const file of fs.readdirSync(logDir)) {
|
|
66
|
-
if (!file.includes('.log.'))
|
|
67
|
-
continue;
|
|
68
39
|
const filePath = path.join(logDir, file);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
fs.
|
|
40
|
+
if (file.endsWith('.log')) {
|
|
41
|
+
// 轮转超大日志
|
|
42
|
+
const stat = fs.statSync(filePath);
|
|
43
|
+
if (stat.size > MAX_SIZE) {
|
|
44
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15);
|
|
45
|
+
const newPath = `${filePath}.${timestamp}`;
|
|
46
|
+
fs.renameSync(filePath, newPath);
|
|
47
|
+
console.log(` Rotated: ${file} -> ${path.basename(newPath)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (file.includes('.log.')) {
|
|
51
|
+
// 清理 7 天前的旧日志
|
|
52
|
+
const stat = fs.statSync(filePath);
|
|
53
|
+
if (stat.mtimeMs < cutoff) {
|
|
54
|
+
fs.unlinkSync(filePath);
|
|
55
|
+
}
|
|
72
56
|
}
|
|
73
57
|
}
|
|
74
58
|
}
|
|
@@ -268,72 +252,61 @@ function cmdStart() {
|
|
|
268
252
|
};
|
|
269
253
|
setTimeout(checkReady, 1000);
|
|
270
254
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
255
|
+
/**
|
|
256
|
+
* 停止进程并等待退出,返回 Promise
|
|
257
|
+
*/
|
|
258
|
+
async function stopAndWait(pidFile) {
|
|
259
|
+
const pid = isRunning(pidFile);
|
|
260
|
+
if (!pid)
|
|
276
261
|
return;
|
|
277
|
-
}
|
|
278
262
|
console.log(`🛑 Stopping EvolClaw (PID: ${pid})...`);
|
|
279
263
|
process.kill(pid);
|
|
280
|
-
|
|
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);
|
|
264
|
+
await new Promise((resolve) => {
|
|
315
265
|
let waited = 0;
|
|
316
|
-
|
|
266
|
+
const check = setInterval(() => {
|
|
267
|
+
waited++;
|
|
317
268
|
try {
|
|
318
269
|
process.kill(pid, 0);
|
|
319
|
-
execFileSync('sleep', ['1']);
|
|
320
|
-
waited++;
|
|
321
270
|
}
|
|
322
271
|
catch {
|
|
323
|
-
|
|
272
|
+
clearInterval(check);
|
|
273
|
+
try {
|
|
274
|
+
fs.unlinkSync(pidFile);
|
|
275
|
+
}
|
|
276
|
+
catch { }
|
|
277
|
+
console.log('✓ EvolClaw stopped');
|
|
278
|
+
resolve();
|
|
279
|
+
return;
|
|
324
280
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
281
|
+
if (waited >= 10) {
|
|
282
|
+
clearInterval(check);
|
|
283
|
+
try {
|
|
284
|
+
process.kill(pid, 9);
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
287
|
+
try {
|
|
288
|
+
fs.unlinkSync(pidFile);
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
console.log('✓ EvolClaw stopped (forced)');
|
|
292
|
+
resolve();
|
|
329
293
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
294
|
+
}, 1000);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
async function cmdStop() {
|
|
298
|
+
const p = resolvePaths();
|
|
299
|
+
const pid = isRunning(p.pid);
|
|
300
|
+
if (!pid) {
|
|
301
|
+
console.log('⚠ EvolClaw is not running');
|
|
302
|
+
return;
|
|
336
303
|
}
|
|
304
|
+
await stopAndWait(p.pid);
|
|
305
|
+
}
|
|
306
|
+
async function cmdRestart() {
|
|
307
|
+
console.log('🔄 Restarting EvolClaw...');
|
|
308
|
+
const p = resolvePaths();
|
|
309
|
+
await stopAndWait(p.pid);
|
|
337
310
|
setTimeout(() => cmdStart(), 1000);
|
|
338
311
|
}
|
|
339
312
|
async function cmdStatus() {
|
|
@@ -375,51 +348,58 @@ async function cmdStatus() {
|
|
|
375
348
|
}
|
|
376
349
|
catch { }
|
|
377
350
|
}
|
|
378
|
-
//
|
|
351
|
+
// Channel configuration status
|
|
379
352
|
if (fs.existsSync(p.config)) {
|
|
380
353
|
console.log('');
|
|
381
|
-
console.log('🔌
|
|
354
|
+
console.log('🔌 Channels:');
|
|
382
355
|
try {
|
|
383
356
|
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
384
|
-
if (config.feishu?.appId && config.feishu?.appSecret) {
|
|
385
|
-
//
|
|
357
|
+
if (config.channels?.feishu?.appId && config.channels?.feishu?.appSecret) {
|
|
358
|
+
// Verify Feishu credentials connectivity
|
|
386
359
|
try {
|
|
387
360
|
const lark = await import('@larksuiteoapi/node-sdk');
|
|
388
|
-
const client = new lark.Client({ appId: config.feishu.appId, appSecret: config.feishu.appSecret });
|
|
361
|
+
const client = new lark.Client({ appId: config.channels.feishu.appId, appSecret: config.channels.feishu.appSecret });
|
|
389
362
|
const res = await client.auth.tenantAccessToken.internal({
|
|
390
|
-
data: { app_id: config.feishu.appId, app_secret: config.feishu.appSecret },
|
|
363
|
+
data: { app_id: config.channels.feishu.appId, app_secret: config.channels.feishu.appSecret },
|
|
391
364
|
});
|
|
392
365
|
if (res.code === 0) {
|
|
393
|
-
console.log(`
|
|
366
|
+
console.log(` Feishu: ✓ Connected (App ID: ${config.channels.feishu.appId.slice(0, 8)}...)`);
|
|
394
367
|
}
|
|
395
368
|
else {
|
|
396
|
-
console.log(`
|
|
369
|
+
console.log(` Feishu: ✗ Connection refused (${res.msg})`);
|
|
397
370
|
}
|
|
398
371
|
}
|
|
399
372
|
catch (e) {
|
|
400
373
|
const msg = e.message || '';
|
|
401
374
|
if (msg.includes('ETIMEDOUT') || msg.includes('ENETUNREACH') || msg.includes('ENOTFOUND')) {
|
|
402
|
-
console.log('
|
|
375
|
+
console.log(' Feishu: ✗ Connection timeout (network unreachable)');
|
|
403
376
|
}
|
|
404
377
|
else {
|
|
405
|
-
console.log(`
|
|
378
|
+
console.log(` Feishu: ✗ Connection failed (${msg.slice(0, 80)})`);
|
|
406
379
|
}
|
|
407
380
|
}
|
|
408
381
|
}
|
|
409
382
|
else {
|
|
410
|
-
console.log('
|
|
383
|
+
console.log(' Feishu: - Not configured');
|
|
384
|
+
}
|
|
385
|
+
if (config.channels?.aun?.domain && config.channels?.aun?.agentName) {
|
|
386
|
+
console.log(` AUN: ✓ Configured (${config.channels.aun.agentName}@${config.channels.aun.domain})`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
console.log(' AUN: - Not configured');
|
|
411
390
|
}
|
|
412
|
-
if (config.
|
|
413
|
-
|
|
391
|
+
if (config.channels?.wechat?.token) {
|
|
392
|
+
const tokenPreview = config.channels.wechat.token.slice(0, 20);
|
|
393
|
+
console.log(` WeChat: ✓ Configured (Token: ${tokenPreview}...)`);
|
|
414
394
|
}
|
|
415
395
|
else {
|
|
416
|
-
console.log('
|
|
396
|
+
console.log(' WeChat: - Not configured');
|
|
417
397
|
}
|
|
418
|
-
if (config.anthropic?.model) {
|
|
419
|
-
console.log(`
|
|
398
|
+
if (config.agents?.anthropic?.model) {
|
|
399
|
+
console.log(` Model: ${config.agents.anthropic.model}`);
|
|
420
400
|
}
|
|
421
401
|
if (config.projects?.defaultPath) {
|
|
422
|
-
console.log(`
|
|
402
|
+
console.log(` Default project: ${config.projects.defaultPath}`);
|
|
423
403
|
}
|
|
424
404
|
}
|
|
425
405
|
catch { }
|
|
@@ -508,16 +488,16 @@ async function cmdRestartMonitor() {
|
|
|
508
488
|
if (started) {
|
|
509
489
|
log('✓ Service restarted successfully');
|
|
510
490
|
archiveSelfHealLog(p, log);
|
|
511
|
-
await
|
|
491
|
+
await notifyChannel(p, pendingInfo, '✅ 服务重启成功!', log);
|
|
512
492
|
cleanupPendingFile(pendingFile, log);
|
|
513
493
|
process.exit(0);
|
|
514
494
|
}
|
|
515
495
|
// 启动失败,进入 self-heal 循环
|
|
516
496
|
log('❌ Service failed to start, entering self-heal loop');
|
|
517
|
-
await
|
|
497
|
+
await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
|
|
518
498
|
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
519
499
|
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
520
|
-
await
|
|
500
|
+
await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
521
501
|
// 调用 claude CLI 修复
|
|
522
502
|
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, log);
|
|
523
503
|
if (!healed) {
|
|
@@ -529,7 +509,7 @@ async function cmdRestartMonitor() {
|
|
|
529
509
|
if (started) {
|
|
530
510
|
log(`✓ Self-heal succeeded on attempt ${attempt}`);
|
|
531
511
|
archiveSelfHealLog(p, log);
|
|
532
|
-
await
|
|
512
|
+
await notifyChannel(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
|
|
533
513
|
cleanupPendingFile(pendingFile, log);
|
|
534
514
|
process.exit(0);
|
|
535
515
|
}
|
|
@@ -537,7 +517,7 @@ async function cmdRestartMonitor() {
|
|
|
537
517
|
}
|
|
538
518
|
// 全部失败
|
|
539
519
|
log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
|
|
540
|
-
await
|
|
520
|
+
await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
|
|
541
521
|
cleanupPendingFile(pendingFile, log);
|
|
542
522
|
process.exit(1);
|
|
543
523
|
}
|
|
@@ -678,35 +658,95 @@ function archiveSelfHealLog(p, log) {
|
|
|
678
658
|
log(`Archived self-heal log to ${archivePath}`);
|
|
679
659
|
}
|
|
680
660
|
/**
|
|
681
|
-
*
|
|
661
|
+
* 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
|
|
662
|
+
* 支持 feishu / wechat,根据 pendingInfo.channel 路由
|
|
682
663
|
*/
|
|
683
|
-
async function
|
|
684
|
-
if (!pendingInfo
|
|
664
|
+
async function notifyChannel(p, pendingInfo, message, log) {
|
|
665
|
+
if (!pendingInfo)
|
|
685
666
|
return;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
667
|
+
const configPath = path.join(p.dataDir, 'evolclaw.json');
|
|
668
|
+
if (!fs.existsSync(configPath))
|
|
669
|
+
return;
|
|
670
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
671
|
+
if (pendingInfo.channel === 'feishu') {
|
|
672
|
+
try {
|
|
673
|
+
if (!config.channels?.feishu?.appId || !config.channels?.feishu?.appSecret)
|
|
674
|
+
return;
|
|
675
|
+
const lark = await import('@larksuiteoapi/node-sdk');
|
|
676
|
+
const client = new lark.Client({
|
|
677
|
+
appId: config.channels.feishu.appId,
|
|
678
|
+
appSecret: config.channels.feishu.appSecret,
|
|
679
|
+
});
|
|
680
|
+
await client.im.message.create({
|
|
681
|
+
params: { receive_id_type: 'chat_id' },
|
|
682
|
+
data: {
|
|
683
|
+
receive_id: pendingInfo.channelId,
|
|
684
|
+
msg_type: 'text',
|
|
685
|
+
content: JSON.stringify({ text: message }),
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
log(`Feishu notification sent: ${message.slice(0, 50)}`);
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
692
|
+
}
|
|
707
693
|
}
|
|
708
|
-
|
|
709
|
-
|
|
694
|
+
else if (pendingInfo.channel === 'wechat') {
|
|
695
|
+
try {
|
|
696
|
+
if (!config.channels?.wechat?.token)
|
|
697
|
+
return;
|
|
698
|
+
const crypto = await import('node:crypto');
|
|
699
|
+
const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
700
|
+
const token = config.channels.wechat.token;
|
|
701
|
+
// 读取缓存的 context_token
|
|
702
|
+
const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
|
|
703
|
+
let contextToken;
|
|
704
|
+
try {
|
|
705
|
+
if (fs.existsSync(syncBufPath)) {
|
|
706
|
+
const tokens = JSON.parse(fs.readFileSync(syncBufPath, 'utf-8'));
|
|
707
|
+
contextToken = tokens[pendingInfo.channelId];
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch { }
|
|
711
|
+
if (!contextToken) {
|
|
712
|
+
log(`WeChat notification skipped: no context_token for ${pendingInfo.channelId}`);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
716
|
+
const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
717
|
+
const body = JSON.stringify({
|
|
718
|
+
msg: {
|
|
719
|
+
from_user_id: '',
|
|
720
|
+
to_user_id: pendingInfo.channelId,
|
|
721
|
+
client_id: `evolclaw-restart:${Date.now()}`,
|
|
722
|
+
message_type: 2,
|
|
723
|
+
message_state: 2,
|
|
724
|
+
item_list: [{ type: 1, text_item: { text: message } }],
|
|
725
|
+
context_token: contextToken,
|
|
726
|
+
},
|
|
727
|
+
base_info: { channel_version: '1.0.0' },
|
|
728
|
+
});
|
|
729
|
+
const res = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
|
|
730
|
+
method: 'POST',
|
|
731
|
+
headers: {
|
|
732
|
+
'Content-Type': 'application/json',
|
|
733
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
734
|
+
'Authorization': `Bearer ${token.trim()}`,
|
|
735
|
+
'X-WECHAT-UIN': wechatUin,
|
|
736
|
+
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
|
|
737
|
+
},
|
|
738
|
+
body,
|
|
739
|
+
});
|
|
740
|
+
if (res.ok) {
|
|
741
|
+
log(`WeChat notification sent: ${message.slice(0, 50)}`);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
log(`WeChat notification failed: HTTP ${res.status}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch (error) {
|
|
748
|
+
log(`WeChat notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
749
|
+
}
|
|
710
750
|
}
|
|
711
751
|
}
|
|
712
752
|
// ==================== Main ====================
|
|
@@ -714,16 +754,24 @@ export async function main(args) {
|
|
|
714
754
|
const cmd = args[0] || 'start';
|
|
715
755
|
switch (cmd) {
|
|
716
756
|
case 'init':
|
|
717
|
-
|
|
757
|
+
if (args[1] === 'wechat') {
|
|
758
|
+
await cmdInitWechat();
|
|
759
|
+
}
|
|
760
|
+
else if (args[1] === 'feishu') {
|
|
761
|
+
await cmdInitFeishu();
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
await cmdInit();
|
|
765
|
+
}
|
|
718
766
|
break;
|
|
719
767
|
case 'start':
|
|
720
768
|
cmdStart();
|
|
721
769
|
break;
|
|
722
770
|
case 'stop':
|
|
723
|
-
cmdStop();
|
|
771
|
+
await cmdStop();
|
|
724
772
|
break;
|
|
725
773
|
case 'restart':
|
|
726
|
-
cmdRestart();
|
|
774
|
+
await cmdRestart();
|
|
727
775
|
break;
|
|
728
776
|
case 'status':
|
|
729
777
|
await cmdStatus();
|
|
@@ -738,12 +786,14 @@ export async function main(args) {
|
|
|
738
786
|
console.log(`Usage: evolclaw {init|start|stop|restart|status|logs}
|
|
739
787
|
|
|
740
788
|
Commands:
|
|
741
|
-
init
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
789
|
+
init 创建配置文件 (${resolvePaths().config})
|
|
790
|
+
init wechat 微信扫码登录并写入配置
|
|
791
|
+
init feishu 飞书扫码登录并写入配置
|
|
792
|
+
start 启动服务 (默认)
|
|
793
|
+
stop 停止服务
|
|
794
|
+
restart 重启服务
|
|
795
|
+
status 查看状态
|
|
796
|
+
logs 查看日志 (tail -f)
|
|
747
797
|
|
|
748
798
|
Environment:
|
|
749
799
|
EVOLCLAW_HOME 数据目录 (默认: ~/.evolclaw)
|
package/dist/config.js
CHANGED
|
@@ -17,16 +17,24 @@ function loadClaudeSettings() {
|
|
|
17
17
|
}
|
|
18
18
|
export function resolveAnthropicConfig(config) {
|
|
19
19
|
const settings = loadClaudeSettings();
|
|
20
|
-
|
|
20
|
+
// 过滤占位符,视为未配置
|
|
21
|
+
const configApiKey = config.agents?.anthropic?.apiKey;
|
|
22
|
+
const isPlaceholderKey = !configApiKey ||
|
|
23
|
+
configApiKey.includes('your-') ||
|
|
24
|
+
configApiKey.includes('placeholder');
|
|
25
|
+
const apiKey = (isPlaceholderKey ? null : configApiKey)
|
|
21
26
|
|| process.env.ANTHROPIC_AUTH_TOKEN
|
|
22
27
|
|| settings.env?.ANTHROPIC_AUTH_TOKEN;
|
|
23
28
|
if (!apiKey) {
|
|
24
|
-
throw new Error('No API key found. Set one of:
|
|
29
|
+
throw new Error('No API key found. Set one of: agents.anthropic.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
|
|
25
30
|
}
|
|
26
|
-
|
|
31
|
+
// baseUrl 也过滤占位符
|
|
32
|
+
const configBaseUrl = config.agents?.anthropic?.baseUrl;
|
|
33
|
+
const isPlaceholderUrl = configBaseUrl?.includes('api.anthropic.com');
|
|
34
|
+
const baseUrl = (isPlaceholderUrl ? null : configBaseUrl)
|
|
27
35
|
|| process.env.ANTHROPIC_BASE_URL
|
|
28
36
|
|| settings.env?.ANTHROPIC_BASE_URL;
|
|
29
|
-
const model = config.anthropic?.model
|
|
37
|
+
const model = config.agents?.anthropic?.model
|
|
30
38
|
|| settings.model
|
|
31
39
|
|| 'sonnet';
|
|
32
40
|
return { apiKey, baseUrl, model };
|
|
@@ -44,35 +52,42 @@ export function saveConfig(config, configPath = resolvePaths().config) {
|
|
|
44
52
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
45
53
|
}
|
|
46
54
|
export function getOwner(config, channel) {
|
|
47
|
-
|
|
55
|
+
const ch = config.channels?.[channel];
|
|
56
|
+
return ch?.owner;
|
|
48
57
|
}
|
|
49
58
|
export function setOwner(config, channel, userId, configPath = resolvePaths().config) {
|
|
50
|
-
if (!config.
|
|
51
|
-
config.
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
if (!config.channels)
|
|
60
|
+
config.channels = {};
|
|
61
|
+
const channels = config.channels;
|
|
62
|
+
if (!channels[channel])
|
|
63
|
+
channels[channel] = {};
|
|
64
|
+
channels[channel].owner = userId;
|
|
54
65
|
saveConfig(config, configPath);
|
|
55
66
|
}
|
|
56
67
|
export function isOwner(config, channel, userId) {
|
|
57
|
-
return config
|
|
68
|
+
return getOwner(config, channel) === userId;
|
|
58
69
|
}
|
|
59
70
|
function validateConfig(config) {
|
|
60
71
|
// anthropic 部分不再强制校验,由 resolveAnthropicConfig() 处理
|
|
61
72
|
// Feishu 配置可选,但如果配置了就要完整
|
|
62
|
-
if (config.feishu) {
|
|
63
|
-
if (!config.feishu.appId || config.feishu.appId.startsWith('YOUR_')) {
|
|
73
|
+
if (config.channels?.feishu) {
|
|
74
|
+
if (!config.channels.feishu.appId || config.channels.feishu.appId.startsWith('YOUR_')) {
|
|
64
75
|
logger.warn('⚠ Feishu appId not configured (Feishu channel will be disabled)');
|
|
65
76
|
}
|
|
66
|
-
if (!config.feishu.appSecret || config.feishu.appSecret.startsWith('YOUR_')) {
|
|
77
|
+
if (!config.channels.feishu.appSecret || config.channels.feishu.appSecret.startsWith('YOUR_')) {
|
|
67
78
|
logger.warn('⚠ Feishu appSecret not configured (Feishu channel will be disabled)');
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
|
-
if (!config.aun?.domain)
|
|
71
|
-
throw new Error('Missing aun.domain');
|
|
72
|
-
if (!config.aun?.agentName)
|
|
73
|
-
throw new Error('Missing aun.agentName');
|
|
81
|
+
if (!config.channels?.aun?.domain)
|
|
82
|
+
throw new Error('Missing channels.aun.domain');
|
|
83
|
+
if (!config.channels?.aun?.agentName)
|
|
84
|
+
throw new Error('Missing channels.aun.agentName');
|
|
74
85
|
if (!config.projects?.defaultPath)
|
|
75
86
|
throw new Error('Missing projects.defaultPath');
|
|
87
|
+
// WeChat 配置可选,但如果启用了就需要 token
|
|
88
|
+
if (config.channels?.wechat?.enabled && !config.channels?.wechat?.token) {
|
|
89
|
+
logger.warn('⚠ WeChat enabled but token not configured (WeChat channel will be disabled)');
|
|
90
|
+
}
|
|
76
91
|
}
|
|
77
92
|
export function ensureDir(dirPath) {
|
|
78
93
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -22,6 +22,15 @@ export class AgentRunner {
|
|
|
22
22
|
this.config = config;
|
|
23
23
|
this.onSessionIdUpdate = onSessionIdUpdate;
|
|
24
24
|
}
|
|
25
|
+
getAgentEnv() {
|
|
26
|
+
return {
|
|
27
|
+
...process.env,
|
|
28
|
+
ANTHROPIC_AUTH_TOKEN: this.apiKey,
|
|
29
|
+
PATH: process.env.PATH,
|
|
30
|
+
DISABLE_AUTOUPDATER: '1',
|
|
31
|
+
...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
|
|
32
|
+
};
|
|
33
|
+
}
|
|
25
34
|
setModel(model) {
|
|
26
35
|
this.model = model;
|
|
27
36
|
}
|
|
@@ -104,8 +113,8 @@ export class AgentRunner {
|
|
|
104
113
|
}
|
|
105
114
|
return {};
|
|
106
115
|
};
|
|
107
|
-
const useSettingSources = this.config?.
|
|
108
|
-
const enableSummaries = this.config?.
|
|
116
|
+
const useSettingSources = this.config?.agents?.anthropic?.useSettingSources !== false;
|
|
117
|
+
const enableSummaries = this.config?.agents?.anthropic?.agentProgressSummaries !== false;
|
|
109
118
|
// 公共 options(新旧模式共用)
|
|
110
119
|
const commonOptions = {
|
|
111
120
|
cwd: projectPath,
|
|
@@ -126,13 +135,7 @@ export class AgentRunner {
|
|
|
126
135
|
logger.debug(`[Claude-stderr] ${msg.trim()}`);
|
|
127
136
|
}
|
|
128
137
|
},
|
|
129
|
-
env:
|
|
130
|
-
...process.env,
|
|
131
|
-
ANTHROPIC_AUTH_TOKEN: this.apiKey,
|
|
132
|
-
PATH: process.env.PATH,
|
|
133
|
-
DISABLE_AUTOUPDATER: '1',
|
|
134
|
-
...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
|
|
135
|
-
}
|
|
138
|
+
env: this.getAgentEnv()
|
|
136
139
|
};
|
|
137
140
|
const createQuery = (promptInput, resumeSessionId) => {
|
|
138
141
|
if (useSettingSources) {
|
|
@@ -252,28 +255,26 @@ export class AgentRunner {
|
|
|
252
255
|
this.onSessionIdUpdate(sessionId, claudeSessionId);
|
|
253
256
|
}
|
|
254
257
|
}
|
|
258
|
+
runSessionCommand(prompt, claudeSessionId, projectPath) {
|
|
259
|
+
return query({
|
|
260
|
+
prompt,
|
|
261
|
+
options: {
|
|
262
|
+
cwd: projectPath,
|
|
263
|
+
model: this.model,
|
|
264
|
+
resume: claudeSessionId,
|
|
265
|
+
maxTurns: 1,
|
|
266
|
+
permissionMode: 'default',
|
|
267
|
+
env: this.getAgentEnv()
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
255
271
|
/**
|
|
256
272
|
* 主动压缩会话上下文
|
|
257
273
|
*/
|
|
258
274
|
async compactSession(sessionId, claudeSessionId, projectPath) {
|
|
259
275
|
try {
|
|
260
276
|
logger.info(`[AgentRunner] Compacting session: ${claudeSessionId}`);
|
|
261
|
-
const stream =
|
|
262
|
-
prompt: '/compact',
|
|
263
|
-
options: {
|
|
264
|
-
cwd: projectPath,
|
|
265
|
-
model: this.model,
|
|
266
|
-
resume: claudeSessionId,
|
|
267
|
-
maxTurns: 1,
|
|
268
|
-
permissionMode: 'default',
|
|
269
|
-
env: {
|
|
270
|
-
...process.env,
|
|
271
|
-
ANTHROPIC_AUTH_TOKEN: this.apiKey,
|
|
272
|
-
DISABLE_AUTOUPDATER: '1',
|
|
273
|
-
...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
+
const stream = this.runSessionCommand('/compact', claudeSessionId, projectPath);
|
|
277
278
|
for await (const event of stream) {
|
|
278
279
|
if (event.type === 'system' && event.subtype === 'compact_boundary') {
|
|
279
280
|
logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
|
|
@@ -293,22 +294,7 @@ export class AgentRunner {
|
|
|
293
294
|
async clearSession(claudeSessionId, projectPath) {
|
|
294
295
|
try {
|
|
295
296
|
logger.info(`[AgentRunner] Clearing session via SDK: ${claudeSessionId}`);
|
|
296
|
-
const stream =
|
|
297
|
-
prompt: '/clear',
|
|
298
|
-
options: {
|
|
299
|
-
cwd: projectPath,
|
|
300
|
-
model: this.model,
|
|
301
|
-
resume: claudeSessionId,
|
|
302
|
-
maxTurns: 1,
|
|
303
|
-
permissionMode: 'default',
|
|
304
|
-
env: {
|
|
305
|
-
...process.env,
|
|
306
|
-
ANTHROPIC_AUTH_TOKEN: this.apiKey,
|
|
307
|
-
DISABLE_AUTOUPDATER: '1',
|
|
308
|
-
...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {})
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
});
|
|
297
|
+
const stream = this.runSessionCommand('/clear', claudeSessionId, projectPath);
|
|
312
298
|
for await (const event of stream) {
|
|
313
299
|
logger.debug(`[AgentRunner] Clear event: type=${event.type}, subtype=${event.subtype || 'none'}`);
|
|
314
300
|
}
|