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/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() {
@@ -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(` 会话总数: ${output[0]} (活跃: ${output[1]})`);
372
- console.log(` 独立会话: ${output[2]} 个`);
373
- console.log(` 涉及项目: ${output[3]} 个`);
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(` 飞书:已连接 (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?.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
- if (config.aun?.domain && config.aun?.agentName) {
413
- console.log(` AUN: 已配置 (${config.aun.agentName}@${config.aun.domain})`);
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(` 模型: ${config.anthropic.model}`);
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(` 默认项目: ${config.projects.defaultPath}`);
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 notifyFeishu(p, pendingInfo, '✅ 服务重启成功!', log);
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 notifyFeishu(p, pendingInfo, '⚠️ 服务启动失败,正在尝试自动修复...', log);
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 notifyFeishu(p, pendingInfo, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
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 notifyFeishu(p, pendingInfo, `✅ 自愈成功!(第 ${attempt} 次修复后恢复)`, log);
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 notifyFeishu(p, pendingInfo, `❌ ${MAX_HEAL_ATTEMPTS} 次自动修复均失败,需要人工介入。\n修复记录:${p.selfHealLog}`, log);
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
- * 通过 Feishu API 发送通知(轻量级,不依赖 FeishuChannel)
667
+ * 通过对应渠道 API 发送通知(轻量级,不依赖 Channel 实例)
668
+ * 支持 feishu / wechat,根据 pendingInfo.channel 路由
682
669
  */
683
- async function notifyFeishu(p, pendingInfo, message, log) {
684
- if (!pendingInfo || pendingInfo.channel !== 'feishu')
670
+ async function notifyChannel(p, pendingInfo, message, log) {
671
+ if (!pendingInfo)
685
672
  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)}`);
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
- catch (error) {
709
- log(`Feishu notification failed: ${error.message?.slice(0, 200) || error}`);
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
- await cmdInit();
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 创建配置文件 (${resolvePaths().config})
742
- start 启动服务 (默认)
743
- stop 停止服务
744
- restart 重启服务
745
- status 查看状态
746
- logs 查看日志 (tail -f)
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
- 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)) {