evolclaw 2.0.1 → 2.0.3
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 +205 -149
- 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 +98 -69
- package/dist/utils/stream-flusher.js +3 -2
- package/package.json +8 -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() {
|
|
@@ -368,58 +341,71 @@ async function cmdStatus() {
|
|
|
368
341
|
'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
342
|
], { encoding: 'utf-8' }).trim().split('\n');
|
|
370
343
|
if (output.length >= 4) {
|
|
371
|
-
console.log(`
|
|
372
|
-
console.log(`
|
|
373
|
-
console.log(`
|
|
344
|
+
console.log(` Total sessions: ${output[0]} (active: ${output[1]})`);
|
|
345
|
+
console.log(` Unique chats: ${output[2]}`);
|
|
346
|
+
console.log(` Projects: ${output[3]}`);
|
|
374
347
|
}
|
|
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?.wechat?.token) {
|
|
386
|
+
const tokenPreview = config.channels.wechat.token.slice(0, 20);
|
|
387
|
+
console.log(` WeChat: ✓ Configured (Token: ${tokenPreview}...)`);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
console.log(' WeChat: - Not configured');
|
|
411
391
|
}
|
|
412
|
-
|
|
413
|
-
|
|
392
|
+
// Check AUN with placeholder detection
|
|
393
|
+
const aunDomain = config.channels?.aun?.domain;
|
|
394
|
+
const aunAgent = config.channels?.aun?.agentName;
|
|
395
|
+
const isAunPlaceholder = !aunDomain || !aunAgent ||
|
|
396
|
+
aunDomain.includes('your-') || aunDomain.includes('placeholder') ||
|
|
397
|
+
aunAgent.includes('your-') || aunAgent.includes('placeholder');
|
|
398
|
+
if (aunDomain && aunAgent && !isAunPlaceholder) {
|
|
399
|
+
console.log(` AUN: ✓ Configured (${aunAgent}@${aunDomain})`);
|
|
414
400
|
}
|
|
415
401
|
else {
|
|
416
|
-
console.log(' AUN: -
|
|
402
|
+
console.log(' AUN: - Not configured');
|
|
417
403
|
}
|
|
418
|
-
if (config.anthropic?.model) {
|
|
419
|
-
console.log(`
|
|
404
|
+
if (config.agents?.anthropic?.model) {
|
|
405
|
+
console.log(` Model: ${config.agents.anthropic.model}`);
|
|
420
406
|
}
|
|
421
407
|
if (config.projects?.defaultPath) {
|
|
422
|
-
console.log(`
|
|
408
|
+
console.log(` Default project: ${config.projects.defaultPath}`);
|
|
423
409
|
}
|
|
424
410
|
}
|
|
425
411
|
catch { }
|
|
@@ -508,16 +494,16 @@ async function cmdRestartMonitor() {
|
|
|
508
494
|
if (started) {
|
|
509
495
|
log('✓ Service restarted successfully');
|
|
510
496
|
archiveSelfHealLog(p, log);
|
|
511
|
-
await
|
|
497
|
+
await notifyChannel(p, pendingInfo, '✅ 服务重启成功!', log);
|
|
512
498
|
cleanupPendingFile(pendingFile, log);
|
|
513
499
|
process.exit(0);
|
|
514
500
|
}
|
|
515
501
|
// 启动失败,进入 self-heal 循环
|
|
516
502
|
log('❌ Service failed to start, entering self-heal loop');
|
|
517
|
-
await
|
|
503
|
+
await notifyChannel(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
|
|
518
504
|
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
519
505
|
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
520
|
-
await
|
|
506
|
+
await notifyChannel(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
521
507
|
// 调用 claude CLI 修复
|
|
522
508
|
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, log);
|
|
523
509
|
if (!healed) {
|
|
@@ -529,7 +515,7 @@ async function cmdRestartMonitor() {
|
|
|
529
515
|
if (started) {
|
|
530
516
|
log(`✓ Self-heal succeeded on attempt ${attempt}`);
|
|
531
517
|
archiveSelfHealLog(p, log);
|
|
532
|
-
await
|
|
518
|
+
await notifyChannel(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
|
|
533
519
|
cleanupPendingFile(pendingFile, log);
|
|
534
520
|
process.exit(0);
|
|
535
521
|
}
|
|
@@ -537,7 +523,7 @@ async function cmdRestartMonitor() {
|
|
|
537
523
|
}
|
|
538
524
|
// 全部失败
|
|
539
525
|
log(`❌ All ${MAX_HEAL_ATTEMPTS} self-heal attempts failed`);
|
|
540
|
-
await
|
|
526
|
+
await notifyChannel(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
|
|
541
527
|
cleanupPendingFile(pendingFile, log);
|
|
542
528
|
process.exit(1);
|
|
543
529
|
}
|
|
@@ -678,35 +664,95 @@ function archiveSelfHealLog(p, log) {
|
|
|
678
664
|
log(`Archived self-heal log to ${archivePath}`);
|
|
679
665
|
}
|
|
680
666
|
/**
|
|
681
|
-
*
|
|
667
|
+
* 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
|
|
668
|
+
* 支持 feishu / wechat,根据 pendingInfo.channel 路由
|
|
682
669
|
*/
|
|
683
|
-
async function
|
|
684
|
-
if (!pendingInfo
|
|
670
|
+
async function notifyChannel(p, pendingInfo, message, log) {
|
|
671
|
+
if (!pendingInfo)
|
|
685
672
|
return;
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
673
|
+
const configPath = path.join(p.dataDir, 'evolclaw.json');
|
|
674
|
+
if (!fs.existsSync(configPath))
|
|
675
|
+
return;
|
|
676
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
677
|
+
if (pendingInfo.channel === 'feishu') {
|
|
678
|
+
try {
|
|
679
|
+
if (!config.channels?.feishu?.appId || !config.channels?.feishu?.appSecret)
|
|
680
|
+
return;
|
|
681
|
+
const lark = await import('@larksuiteoapi/node-sdk');
|
|
682
|
+
const client = new lark.Client({
|
|
683
|
+
appId: config.channels.feishu.appId,
|
|
684
|
+
appSecret: config.channels.feishu.appSecret,
|
|
685
|
+
});
|
|
686
|
+
await client.im.message.create({
|
|
687
|
+
params: { receive_id_type: 'chat_id' },
|
|
688
|
+
data: {
|
|
689
|
+
receive_id: pendingInfo.channelId,
|
|
690
|
+
msg_type: 'text',
|
|
691
|
+
content: JSON.stringify({ text: message }),
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
log(`Feishu notification sent: ${message.slice(0, 50)}`);
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
698
|
+
}
|
|
707
699
|
}
|
|
708
|
-
|
|
709
|
-
|
|
700
|
+
else if (pendingInfo.channel === 'wechat') {
|
|
701
|
+
try {
|
|
702
|
+
if (!config.channels?.wechat?.token)
|
|
703
|
+
return;
|
|
704
|
+
const crypto = await import('node:crypto');
|
|
705
|
+
const baseUrl = (config.channels.wechat.baseUrl || 'https://ilinkai.weixin.qq.com').replace(/\/$/, '');
|
|
706
|
+
const token = config.channels.wechat.token;
|
|
707
|
+
// 读取缓存的 context_token
|
|
708
|
+
const syncBufPath = path.join(p.dataDir, 'wechat-context-tokens.json');
|
|
709
|
+
let contextToken;
|
|
710
|
+
try {
|
|
711
|
+
if (fs.existsSync(syncBufPath)) {
|
|
712
|
+
const tokens = JSON.parse(fs.readFileSync(syncBufPath, 'utf-8'));
|
|
713
|
+
contextToken = tokens[pendingInfo.channelId];
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
catch { }
|
|
717
|
+
if (!contextToken) {
|
|
718
|
+
log(`WeChat notification skipped: no context_token for ${pendingInfo.channelId}`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
722
|
+
const wechatUin = Buffer.from(String(uint32), 'utf-8').toString('base64');
|
|
723
|
+
const body = JSON.stringify({
|
|
724
|
+
msg: {
|
|
725
|
+
from_user_id: '',
|
|
726
|
+
to_user_id: pendingInfo.channelId,
|
|
727
|
+
client_id: `evolclaw-restart:${Date.now()}`,
|
|
728
|
+
message_type: 2,
|
|
729
|
+
message_state: 2,
|
|
730
|
+
item_list: [{ type: 1, text_item: { text: message } }],
|
|
731
|
+
context_token: contextToken,
|
|
732
|
+
},
|
|
733
|
+
base_info: { channel_version: '1.0.0' },
|
|
734
|
+
});
|
|
735
|
+
const res = await fetch(`${baseUrl}/ilink/bot/sendmessage`, {
|
|
736
|
+
method: 'POST',
|
|
737
|
+
headers: {
|
|
738
|
+
'Content-Type': 'application/json',
|
|
739
|
+
'AuthorizationType': 'ilink_bot_token',
|
|
740
|
+
'Authorization': `Bearer ${token.trim()}`,
|
|
741
|
+
'X-WECHAT-UIN': wechatUin,
|
|
742
|
+
'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
|
|
743
|
+
},
|
|
744
|
+
body,
|
|
745
|
+
});
|
|
746
|
+
if (res.ok) {
|
|
747
|
+
log(`WeChat notification sent: ${message.slice(0, 50)}`);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
log(`WeChat notification failed: HTTP ${res.status}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
log(`WeChat notification failed: ${error.message?.slice(0, 200) || error}`);
|
|
755
|
+
}
|
|
710
756
|
}
|
|
711
757
|
}
|
|
712
758
|
// ==================== Main ====================
|
|
@@ -714,16 +760,24 @@ export async function main(args) {
|
|
|
714
760
|
const cmd = args[0] || 'start';
|
|
715
761
|
switch (cmd) {
|
|
716
762
|
case 'init':
|
|
717
|
-
|
|
763
|
+
if (args[1] === 'wechat') {
|
|
764
|
+
await cmdInitWechat();
|
|
765
|
+
}
|
|
766
|
+
else if (args[1] === 'feishu') {
|
|
767
|
+
await cmdInitFeishu();
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
await cmdInit();
|
|
771
|
+
}
|
|
718
772
|
break;
|
|
719
773
|
case 'start':
|
|
720
774
|
cmdStart();
|
|
721
775
|
break;
|
|
722
776
|
case 'stop':
|
|
723
|
-
cmdStop();
|
|
777
|
+
await cmdStop();
|
|
724
778
|
break;
|
|
725
779
|
case 'restart':
|
|
726
|
-
cmdRestart();
|
|
780
|
+
await cmdRestart();
|
|
727
781
|
break;
|
|
728
782
|
case 'status':
|
|
729
783
|
await cmdStatus();
|
|
@@ -738,12 +792,14 @@ export async function main(args) {
|
|
|
738
792
|
console.log(`Usage: evolclaw {init|start|stop|restart|status|logs}
|
|
739
793
|
|
|
740
794
|
Commands:
|
|
741
|
-
init
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
795
|
+
init 创建配置文件 (${resolvePaths().config})
|
|
796
|
+
init wechat 微信扫码登录并写入配置
|
|
797
|
+
init feishu 飞书扫码登录并写入配置
|
|
798
|
+
start 启动服务 (默认)
|
|
799
|
+
stop 停止服务
|
|
800
|
+
restart 重启服务
|
|
801
|
+
status 查看状态
|
|
802
|
+
logs 查看日志 (tail -f)
|
|
747
803
|
|
|
748
804
|
Environment:
|
|
749
805
|
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)) {
|