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/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
- const stat = fs.statSync(filePath);
70
- if (stat.mtimeMs < cutoff) {
71
- fs.unlinkSync(filePath);
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
- function cmdStop() {
272
- const p = resolvePaths();
273
- const pid = isRunning(p.pid);
274
- if (!pid) {
275
- console.log('⚠ EvolClaw is not running');
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
- 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);
264
+ await new Promise((resolve) => {
315
265
  let waited = 0;
316
- while (waited < 10) {
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
- break;
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
- if (waited >= 10) {
327
- try {
328
- process.kill(pid, 9);
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
- catch { }
331
- }
332
- try {
333
- fs.unlinkSync(p.pid);
334
- }
335
- catch { }
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(` 飞书:已连接 (App ID: ${config.feishu.appId.slice(0, 8)}...)`);
366
+ console.log(` Feishu:Connected (App ID: ${config.channels.feishu.appId.slice(0, 8)}...)`);
394
367
  }
395
368
  else {
396
- console.log(` 飞书:连接拒绝 (${res.msg})`);
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(` 飞书:连接失败 (${msg.slice(0, 80)})`);
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.aun?.domain && config.aun?.agentName) {
413
- console.log(` AUN: 已配置 (${config.aun.agentName}@${config.aun.domain})`);
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(' AUN: - 未配置');
396
+ console.log(' WeChat: - Not configured');
417
397
  }
418
- if (config.anthropic?.model) {
419
- console.log(` 模型: ${config.anthropic.model}`);
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(` 默认项目: ${config.projects.defaultPath}`);
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 notifyFeishu(p, pendingInfo, '✅ 服务重启成功!', log);
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 notifyFeishu(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
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 notifyFeishu(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
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 notifyFeishu(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
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 notifyFeishu(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
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
- * 通过 Feishu API 发送通知(轻量级,不依赖 FeishuChannel)
661
+ * 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
662
+ * 支持 feishu / wechat,根据 pendingInfo.channel 路由
682
663
  */
683
- async function notifyFeishu(p, pendingInfo, message, log) {
684
- if (!pendingInfo || pendingInfo.channel !== 'feishu')
664
+ async function notifyChannel(p, pendingInfo, message, log) {
665
+ if (!pendingInfo)
685
666
  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)}`);
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
- catch (error) {
709
- log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
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
- await cmdInit();
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 创建配置文件 (${resolvePaths().config})
742
- start 启动服务 (默认)
743
- stop 停止服务
744
- restart 重启服务
745
- status 查看状态
746
- logs 查看日志 (tail -f)
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
- const apiKey = config.anthropic?.apiKey
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: config.anthropic.apiKey, env ANTHROPIC_AUTH_TOKEN, or ~/.claude/settings.json env.ANTHROPIC_AUTH_TOKEN');
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
- const baseUrl = config.anthropic?.baseUrl
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
- return config.owners?.[channel];
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.owners) {
51
- config.owners = {};
52
- }
53
- config.owners[channel] = userId;
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.owners?.[channel] === userId;
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?.sdk?.useSettingSources !== false;
108
- const enableSummaries = this.config?.sdk?.agentProgressSummaries !== false;
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 = query({
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 = query({
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
  }