evolclaw 2.8.0 → 2.8.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.
@@ -10,13 +10,13 @@ import readline from 'readline';
10
10
  import path from 'path';
11
11
  import os from 'os';
12
12
  import crypto from 'crypto';
13
- import { fileURLToPath } from 'url';
14
- import { execFile, execFileSync } from 'child_process';
13
+ import { execFile } from 'child_process';
15
14
  import { promisify } from 'util';
16
15
  import { resolvePaths } from '../paths.js';
17
16
  import { normalizeChannelInstances } from '../config.js';
18
17
  import { selectInstance } from './init.js';
19
18
  import { isWindows } from './cross-platform.js';
19
+ import { AUN_CORE_SDK_PKG, MIN_AUN_CORE_SDK, resolveAunCoreSdkPkg, isAunSdkVersionOk, isValidAid, aidCreate, agentmdPut, buildInitialAgentMd, } from '../channels/aun-ops.js';
20
20
  const execFileAsync = promisify(execFile);
21
21
  export async function npmInstallGlobal(pkg) {
22
22
  const npmCmd = isWindows ? 'npm.cmd' : 'npm';
@@ -580,91 +580,9 @@ export async function cmdInitWechat() {
580
580
  process.exit(1);
581
581
  }
582
582
  // ==================== AUN ====================
583
- // 最低 @agentunion/fastaun 版本要求
584
- const MIN_AUN_CORE_SDK = [0, 2, 14];
585
- const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
586
- function compareVersion(a, min) {
587
- const parts = a.split('.').map(n => parseInt(n, 10));
588
- if (parts.length < 3 || parts.some(isNaN))
589
- return false;
590
- if (parts[0] !== min[0])
591
- return parts[0] > min[0];
592
- if (parts[1] !== min[1])
593
- return parts[1] > min[1];
594
- return parts[2] >= min[2];
595
- }
596
- export function resolveAunCoreSdkPkg() {
597
- const pkgName = AUN_CORE_SDK_PKG;
598
- // Strategy 1: walk up node_modules from this file (no require.resolve — avoids ESM exports issues)
599
- try {
600
- let dir = path.dirname(fileURLToPath(import.meta.url));
601
- while (true) {
602
- const candidate = path.join(dir, 'node_modules', pkgName, 'package.json');
603
- if (fs.existsSync(candidate)) {
604
- const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
605
- if (data.name === pkgName)
606
- return { version: data.version, path: candidate };
607
- }
608
- const parent = path.dirname(dir);
609
- if (parent === dir)
610
- break;
611
- dir = parent;
612
- }
613
- }
614
- catch { /* fall through */ }
615
- // Strategy 2: npm root -g fallback (globally installed SDK)
616
- try {
617
- const npmCmd = isWindows ? 'npm.cmd' : 'npm';
618
- const globalRoot = execFileSync(npmCmd, ['root', '-g'], {
619
- encoding: 'utf-8', timeout: 10000, shell: isWindows,
620
- }).trim();
621
- const pkgPath = path.join(globalRoot, pkgName, 'package.json');
622
- if (fs.existsSync(pkgPath)) {
623
- const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
624
- return { version: data.version, path: pkgPath };
625
- }
626
- }
627
- catch { /* not found */ }
628
- return null;
629
- }
630
- /**
631
- * Download AUN CA root certificate to ~/.aun/CA/root/root.crt.
632
- * Idempotent: skips if file already exists. Returns true if a cert is on disk
633
- * after the call (either pre-existing or freshly downloaded).
634
- *
635
- * Must be called BEFORE constructing any AUNClient that needs to verify
636
- * gateway-issued certificates (e.g. for uploadAgentMd) — the SDK loads trusted
637
- * roots from disk at client construction time and won't pick up later writes.
638
- */
639
- export async function downloadCaRoot(aunPath, gatewayUrl, indent = '') {
640
- const caDir = path.join(aunPath, 'CA', 'root');
641
- const caCertPath = path.join(caDir, 'root.crt');
642
- if (fs.existsSync(caCertPath))
643
- return true;
644
- if (!gatewayUrl)
645
- return false;
646
- try {
647
- fs.mkdirSync(caDir, { recursive: true });
648
- const gwHttp = gatewayUrl.replace(/^wss?:/, 'https:').replace(/\/aun$/, '');
649
- const resp = await fetch(`${gwHttp}/pki/chain`, { redirect: 'follow' });
650
- if (!resp.ok) {
651
- console.warn(`${indent}⚠ CA 根证书下载失败: HTTP ${resp.status}`);
652
- return false;
653
- }
654
- const body = await resp.text();
655
- if (!body.includes('BEGIN CERTIFICATE')) {
656
- console.warn(`${indent}⚠ CA 根证书响应内容无效,跳过写入`);
657
- return false;
658
- }
659
- fs.writeFileSync(caCertPath, body);
660
- console.log(`${indent}✓ CA 根证书已下载`);
661
- return true;
662
- }
663
- catch (e) {
664
- console.warn(`${indent}⚠ CA 根证书下载失败: ${e},可稍后手动下载`);
665
- return false;
666
- }
667
- }
583
+ //
584
+ // AUN 原子操作(aidCreate, agentmdPut, downloadCaRoot, isValidAid, ...)
585
+ // 已迁移至 src/channels/aun-ops.ts。本节仅保留交互式 UI 编排。
668
586
  export async function checkAunEnvironment(rl) {
669
587
  console.log('\n🔍 AUN 环境检查...\n');
670
588
  const minVer = MIN_AUN_CORE_SDK.join('.');
@@ -688,7 +606,7 @@ export async function checkAunEnvironment(rl) {
688
606
  console.log('');
689
607
  return true;
690
608
  }
691
- if (compareVersion(installed.version, MIN_AUN_CORE_SDK)) {
609
+ if (isAunSdkVersionOk(installed.version)) {
692
610
  console.log(` ✓ ${AUN_CORE_SDK_PKG} v${installed.version}`);
693
611
  console.log('');
694
612
  return true;
@@ -711,95 +629,8 @@ export async function checkAunEnvironment(rl) {
711
629
  console.log('');
712
630
  return true;
713
631
  }
714
- export function isValidAid(name) {
715
- const labels = name.split('.');
716
- return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
717
- }
718
- /**
719
- * Non-interactive AID creation + agent.md publish.
720
- * Reuses the same logic as `evolclaw init --non-interactive --channel aun`.
721
- *
722
- * Returns the created AID string, or throws on failure.
723
- */
724
- export async function createAidSilent(opts) {
725
- const aunPath = path.join(os.homedir(), '.aun');
726
- const aidDir = path.join(aunPath, 'AIDs', opts.aid);
727
- // Skip creation if AID already exists locally
728
- if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
729
- return { aid: opts.aid, alreadyExisted: true };
730
- }
731
- const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
732
- let client = new AUNClient({ aun_path: aunPath });
733
- const result = await client.auth.createAid({ aid: opts.aid });
734
- // Download CA root cert (if not already present)
735
- const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '');
736
- // Rebuild client with CA cert + AID identity for uploadAgentMd
737
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
738
- if (caDownloaded && fs.existsSync(caCertPath)) {
739
- try {
740
- await client.close();
741
- }
742
- catch { /* ignore */ }
743
- client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: opts.aid });
744
- }
745
- // Set gateway URL for uploadAgentMd
746
- let gatewayUrl = result.gateway || '';
747
- if (!gatewayUrl) {
748
- try {
749
- const discovery = new GatewayDiscovery({});
750
- gatewayUrl = await discovery.discover(`https://${opts.aid}/.well-known/aun-gateway`);
751
- }
752
- catch { /* fall through */ }
753
- }
754
- if (gatewayUrl) {
755
- client._gatewayUrl = gatewayUrl;
756
- }
757
- // Write initial agent.md (initialized: false, name = aid first label)
758
- const agentName = opts.aid.split('.')[0];
759
- const agentMdContent = `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
760
- const agentMdPath = path.join(aidDir, 'agent.md');
761
- try {
762
- await client.auth.uploadAgentMd(agentMdContent);
763
- }
764
- catch (e) {
765
- // Non-fatal: first connection will auto-retry
766
- }
767
- fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
768
- try {
769
- await client.close();
770
- }
771
- catch { /* ignore */ }
772
- if (!fs.existsSync(agentMdPath)) {
773
- throw new Error(`agent.md write verification failed: ${agentMdPath}`);
774
- }
775
- return { aid: opts.aid, alreadyExisted: false };
776
- }
777
- /**
778
- * Append a new AUN instance to the config's channels.aun array and save.
779
- * Handles upgrade from single-object to array format.
780
- */
781
- export function appendAunInstance(config, inst) {
782
- if (!config.channels)
783
- config.channels = {};
784
- const newInst = {
785
- name: inst.name,
786
- enabled: inst.enabled ?? true,
787
- aid: inst.aid,
788
- ...(inst.owner && { owner: inst.owner }),
789
- };
790
- if (Array.isArray(config.channels.aun)) {
791
- config.channels.aun.push(newInst);
792
- }
793
- else if (config.channels.aun) {
794
- const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
795
- config.channels.aun = [oldInst, newInst];
796
- }
797
- else {
798
- config.channels.aun = [newInst];
799
- }
800
- const p = resolvePaths();
801
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
802
- }
632
+ // isValidAid, createAidSilent → 已迁移至 src/channels/aun-ops.ts
633
+ // appendAunInstance 已迁移至 src/channels/aun-ops.ts
803
634
  export async function setupAunAid(rl, _config) {
804
635
  let aid = '';
805
636
  // Outer loop: allows retrying with a different AID
@@ -829,64 +660,35 @@ export async function setupAunAid(rl, _config) {
829
660
  console.log(' 已跳过 AID 创建(启动时可能连接失败)');
830
661
  break;
831
662
  }
832
- // Create AID using TS SDK directly
663
+ // Create AID + agent.md via atomic ops
833
664
  console.log(' 正在创建 AID...');
834
665
  let failed = false;
835
666
  try {
836
- const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
837
- let client = new AUNClient({ aun_path: aunPath });
838
- const result = await client.auth.createAid({ aid });
667
+ const result = await aidCreate(aid);
839
668
  console.log(` ✓ AID ${result.aid} 创建成功`);
840
- // 下载 CA 根证书(如果本地不存在),从 SDK 返回的实际网关 URL 派生
841
- const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '', ' ');
842
- // 重建 client:传 root_ca_path 以验证 server cert,传 aid 以加载身份
843
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
844
- if (caDownloaded && fs.existsSync(caCertPath)) {
845
- try {
846
- await client.close();
847
- }
848
- catch { /* ignore */ }
849
- client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid });
850
- }
851
- // 设置 gateway URL(从 createAid 返回值或 well-known 自动发现)
852
- let gatewayUrl = result.gateway || '';
853
- if (!gatewayUrl) {
854
- try {
855
- const discovery = new GatewayDiscovery({});
856
- gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
857
- }
858
- catch { /* fall through */ }
859
- }
860
- if (gatewayUrl) {
861
- client._gatewayUrl = gatewayUrl;
862
- }
863
- // Collect agent.md info and publish
669
+ // Collect agent.md type and upload
864
670
  const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
865
671
  const agentType = typeInput === 'human' ? 'human' : 'ai';
866
- const agentName = aid.split('.')[0];
867
- const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
868
- const agentMdPath = path.join(aidDir, 'agent.md');
672
+ const content = buildInitialAgentMd({ aid, type: agentType });
869
673
  try {
870
- await client.auth.uploadAgentMd(agentMdContent);
871
- console.log(' ✓ agent.md 已发布');
674
+ await agentmdPut(content, { aid, client: result.client });
675
+ console.log(' ✓ agent.md 已发布并写入本地');
872
676
  }
873
677
  catch (e) {
874
678
  console.log(` ⚠ agent.md 发布失败(首次连接将自动重试): ${String(e.message || e).slice(0, 100)}`);
875
- }
876
- fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
877
- if (!fs.existsSync(agentMdPath)) {
679
+ // Still write local copy as fallback
878
680
  try {
879
- await client.close();
681
+ fs.mkdirSync(aidDir, { recursive: true });
682
+ fs.writeFileSync(path.join(aidDir, 'agent.md'), content, 'utf-8');
683
+ console.log(' ✓ agent.md 已写入本地');
684
+ }
685
+ catch (we) {
686
+ console.log(` ✗ agent.md 本地写入失败: ${String(we.message || we).slice(0, 100)}`);
687
+ failed = true;
880
688
  }
881
- catch { /* ignore */ }
882
- console.log(` ✗ agent.md 本地写入校验失败: ${agentMdPath}`);
883
- failed = true;
884
- }
885
- else {
886
- console.log(' ✓ agent.md 已写入本地');
887
689
  }
888
690
  try {
889
- await client.close();
691
+ await result.client.close();
890
692
  }
891
693
  catch { /* ignore */ }
892
694
  }
@@ -1610,3 +1412,71 @@ export async function cmdInitWecom() {
1610
1412
  console.log(` 配置已写入: ${p.config}`);
1611
1413
  console.log(`\n现在可以启动服务: evolclaw restart`);
1612
1414
  }
1415
+ export function getChannelCredentialCollector(type) {
1416
+ switch (type) {
1417
+ case 'feishu':
1418
+ return async () => {
1419
+ const result = await runFeishuQrFlow();
1420
+ if (!result)
1421
+ return null;
1422
+ return { appId: result.appId, appSecret: result.appSecret, enabled: true };
1423
+ };
1424
+ case 'wechat':
1425
+ return async () => {
1426
+ const result = await runWechatQrFlow();
1427
+ if (!result)
1428
+ return null;
1429
+ return { baseUrl: result.baseUrl, token: result.token, enabled: true };
1430
+ };
1431
+ case 'aun':
1432
+ return async () => {
1433
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1434
+ try {
1435
+ if (!await checkAunEnvironment(rl))
1436
+ return null;
1437
+ const result = await setupAunAid(rl, {});
1438
+ if (!result)
1439
+ return null;
1440
+ return { aid: result.aid, owner: result.owner, enabled: true };
1441
+ }
1442
+ finally {
1443
+ rl.close();
1444
+ }
1445
+ };
1446
+ case 'dingtalk':
1447
+ return async () => {
1448
+ const result = await runDingtalkQrFlowSimple();
1449
+ if (!result)
1450
+ return null;
1451
+ return { clientId: result.clientId, clientSecret: result.clientSecret, enabled: true };
1452
+ };
1453
+ case 'qqbot':
1454
+ return async () => {
1455
+ const result = await runQQBotBindFlowSimple();
1456
+ if (!result)
1457
+ return null;
1458
+ return { appId: result.appId, clientSecret: result.clientSecret, enabled: true };
1459
+ };
1460
+ case 'wecom':
1461
+ return async () => {
1462
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1463
+ const ask = (q) => new Promise(r => rl.question(q, r));
1464
+ try {
1465
+ console.log('企业微信 AI Bot 配置\n');
1466
+ console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
1467
+ const botId = (await ask(' Bot ID: ')).trim();
1468
+ if (!botId)
1469
+ return null;
1470
+ const secret = (await ask(' Secret: ')).trim();
1471
+ if (!secret)
1472
+ return null;
1473
+ return { botId, secret, enabled: true };
1474
+ }
1475
+ finally {
1476
+ rl.close();
1477
+ }
1478
+ };
1479
+ default:
1480
+ return null;
1481
+ }
1482
+ }
@@ -396,7 +396,7 @@ export async function cmdInit(options) {
396
396
  // 非交互式模式
397
397
  if (options?.nonInteractive) {
398
398
  const config = JSON.parse(fs.readFileSync(sampleSrc, 'utf-8'));
399
- const defaultPath = options.defaultPath || path.join(os.homedir(), 'evolclaw-project');
399
+ const defaultPath = options.defaultPath || path.join(os.homedir(), 'projects', 'default');
400
400
  if (!fs.existsSync(defaultPath))
401
401
  fs.mkdirSync(defaultPath, { recursive: true });
402
402
  config.projects.defaultPath = defaultPath;
@@ -405,56 +405,32 @@ export async function cmdInit(options) {
405
405
  throw new Error('--aun-aid is required for AUN channel (e.g. --aun-aid mybot.agentid.pub)');
406
406
  }
407
407
  if (options.channel === 'aun' && options.aunAid) {
408
- // 自动安装 AUN SDK
409
- const { resolveAunCoreSdkPkg, npmInstallGlobal, downloadCaRoot } = await import('./init-channel.js');
410
- if (!resolveAunCoreSdkPkg()) {
411
- console.log('正在安装 @agentunion/fastaun...');
412
- await npmInstallGlobal('@agentunion/fastaun@latest');
413
- }
414
- // 创建 AID(如果本地不存在)
408
+ const { ensureAunSdk, aidCreate, agentmdPut, buildInitialAgentMd } = await import('../channels/aun-ops.js');
409
+ await ensureAunSdk();
415
410
  const aunPath = path.join(os.homedir(), '.aun');
416
411
  const aidDir = path.join(aunPath, 'AIDs', options.aunAid);
417
412
  if (!fs.existsSync(path.join(aidDir, 'private'))) {
418
- const { AUNClient } = await import('@agentunion/fastaun');
419
- let client = new AUNClient({ aun_path: aunPath });
420
- // SDK 通过 well-known 自动发现网关
421
- const result = await client.auth.createAid({ aid: options.aunAid });
422
- // 下载 CA 根证书(如果本地不存在),从 SDK 返回的实际网关 URL 派生
423
- const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '');
424
- // 关键:SDK 默认 rootCaPath=null,只读取包内 bundled certs。
425
- // 必须显式传 root_ca_path 指向刚下载的 root.crt,uploadAgentMd 才能验证 server cert。
426
- // 同时传 aid,否则新 client 不知道该加载哪个身份,uploadAgentMd 会报
427
- // "no local identity found, call auth.createAid() first"。
428
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
429
- if (caDownloaded && fs.existsSync(caCertPath)) {
413
+ const result = await aidCreate(options.aunAid);
414
+ try {
415
+ const content = buildInitialAgentMd({ aid: options.aunAid });
430
416
  try {
431
- await client.close();
417
+ await agentmdPut(content, { aid: options.aunAid, client: result.client });
418
+ }
419
+ catch (e) {
420
+ console.warn(`⚠ agent.md 网络发布失败(首次连接将自动重试): ${String(e?.message || e).slice(0, 100)}`);
421
+ fs.mkdirSync(aidDir, { recursive: true });
422
+ fs.writeFileSync(path.join(aidDir, 'agent.md'), content, 'utf-8');
423
+ }
424
+ if (!fs.existsSync(path.join(aidDir, 'agent.md'))) {
425
+ throw new Error(`agent.md 写入校验失败: ${path.join(aidDir, 'agent.md')}`);
432
426
  }
433
- catch { }
434
- client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: options.aunAid });
435
- }
436
- // 写入初始 agent.md(initialized: false)
437
- const agentName = options.aunAid.split('.')[0];
438
- const agentMd = `---\naid: "${options.aunAid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
439
- const agentMdPath = path.join(aidDir, 'agent.md');
440
- try {
441
- await client.auth.uploadAgentMd(agentMd);
442
- }
443
- catch (e) {
444
- console.warn(`⚠ agent.md 网络发布失败(首次连接将自动重试): ${String(e?.message || e).slice(0, 100)}`);
445
427
  }
446
- fs.writeFileSync(agentMdPath, agentMd, 'utf-8');
447
- if (!fs.existsSync(agentMdPath)) {
428
+ finally {
448
429
  try {
449
- await client.close();
430
+ await result.client.close();
450
431
  }
451
432
  catch { }
452
- throw new Error(`agent.md 写入校验失败: ${agentMdPath}`);
453
- }
454
- try {
455
- await client.close();
456
433
  }
457
- catch { }
458
434
  }
459
435
  config.channels.aun = {
460
436
  enabled: true,
@@ -483,7 +459,7 @@ export async function cmdInit(options) {
483
459
  }
484
460
  console.log('📝 交互式配置\n');
485
461
  // 通用配置
486
- const defaultSuggestion = path.join(os.homedir(), 'evolclaw-project');
462
+ const defaultSuggestion = path.join(os.homedir(), 'projects', 'default');
487
463
  let defaultPath = (await ask(rl, ` 默认项目路径 [${defaultSuggestion}]: `)).trim();
488
464
  if (!defaultPath)
489
465
  defaultPath = defaultSuggestion;
@@ -4,6 +4,9 @@ import { resolvePaths } from '../paths.js';
4
4
  const LOG_DIR = resolvePaths().logs;
5
5
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
6
6
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
7
+ const HOUR_MS = 60 * 60 * 1000;
8
+ const RETAIN_HOURS = 12;
9
+ const LOG_FILE_RE = /^evolclaw-\d{8}-\d{2}\.log$/;
7
10
  const config = {
8
11
  messageLog: process.env.MESSAGE_LOG === 'true',
9
12
  eventLog: process.env.EVENT_LOG === 'true'
@@ -11,8 +14,60 @@ const config = {
11
14
  if (!fs.existsSync(LOG_DIR)) {
12
15
  fs.mkdirSync(LOG_DIR, { recursive: true });
13
16
  }
17
+ /** 获取当前小时标识 YYYYMMDD-HH */
18
+ function currentHourTag() {
19
+ const d = new Date();
20
+ const pad = (n) => String(n).padStart(2, '0');
21
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}`;
22
+ }
23
+ /** 清理超过 RETAIN_HOURS 的旧日志文件 */
24
+ function cleanupOldLogs() {
25
+ const cutoff = Date.now() - RETAIN_HOURS * HOUR_MS;
26
+ try {
27
+ for (const name of fs.readdirSync(LOG_DIR)) {
28
+ if (!LOG_FILE_RE.test(name))
29
+ continue;
30
+ try {
31
+ const full = path.join(LOG_DIR, name);
32
+ if (fs.statSync(full).mtimeMs < cutoff)
33
+ fs.unlinkSync(full);
34
+ }
35
+ catch { }
36
+ }
37
+ }
38
+ catch { }
39
+ }
40
+ let mainHourTag = currentHourTag();
41
+ let mainStream = fs.createWriteStream(path.join(LOG_DIR, `evolclaw-${mainHourTag}.log`), { flags: 'a' });
42
+ // 同时保留 evolclaw.log 软链接指向当前文件,方便 tail -f
43
+ function updateSymlink() {
44
+ const link = path.join(LOG_DIR, 'evolclaw.log');
45
+ const target = `evolclaw-${mainHourTag}.log`;
46
+ try {
47
+ fs.unlinkSync(link);
48
+ }
49
+ catch { }
50
+ try {
51
+ fs.symlinkSync(target, link);
52
+ }
53
+ catch { }
54
+ }
55
+ updateSymlink();
56
+ // 启动时清理一次,之后每小时清理
57
+ cleanupOldLogs();
58
+ const cleanupTimer = setInterval(cleanupOldLogs, HOUR_MS);
59
+ cleanupTimer.unref?.();
60
+ function rotateMainIfNeeded() {
61
+ const tag = currentHourTag();
62
+ if (tag === mainHourTag)
63
+ return;
64
+ mainStream.end();
65
+ mainHourTag = tag;
66
+ mainStream = fs.createWriteStream(path.join(LOG_DIR, `evolclaw-${mainHourTag}.log`), { flags: 'a' });
67
+ updateSymlink();
68
+ cleanupOldLogs();
69
+ }
14
70
  const streams = {
15
- main: fs.createWriteStream(path.join(LOG_DIR, 'evolclaw.log'), { flags: 'a' }),
16
71
  message: config.messageLog ? fs.createWriteStream(path.join(LOG_DIR, 'messages.log'), { flags: 'a' }) : null,
17
72
  event: config.eventLog ? fs.createWriteStream(path.join(LOG_DIR, 'events.log'), { flags: 'a' }) : null
18
73
  };
@@ -33,9 +88,10 @@ export function localTimestamp() {
33
88
  function log(level, ...args) {
34
89
  if (!shouldLog(level))
35
90
  return;
91
+ rotateMainIfNeeded();
36
92
  const timestamp = localTimestamp();
37
93
  const msg = `[${timestamp}] [${level}] ${args.join(' ')}`;
38
- write(streams.main, msg);
94
+ mainStream.write(msg + '\n');
39
95
  }
40
96
  /**
41
97
  * 设置日志级别(config 加载后调用,覆盖环境变量默认值)
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Reload Hooks
3
+ *
4
+ * Extracted from index.ts main() for testability. Builds the ReloadHooks
5
+ * implementation used by EvolAgentRegistry.reload() to drain/disconnect/start
6
+ * channels during a hot reload.
7
+ */
8
+ import { logger } from './logger.js';
9
+ export function buildReloadHooks(deps) {
10
+ const { channelLoader, channelInstances, registerChannelInstance, messageQueue } = deps;
11
+ const drainDelayMs = deps.drainDelayMs ?? 500;
12
+ const drainTimeoutMs = deps.drainTimeoutMs ?? 30000;
13
+ return {
14
+ async drainChannel(channelName) {
15
+ logger.info(`[Reload] Draining channel: ${channelName}`);
16
+ if (messageQueue) {
17
+ // Real drain: poll until empty or timeout
18
+ const pollMs = 100;
19
+ const start = Date.now();
20
+ while (messageQueue.isChannelProcessing(channelName)) {
21
+ if (Date.now() - start > drainTimeoutMs) {
22
+ logger.warn(`[Reload] Drain timeout (${drainTimeoutMs}ms) for channel: ${channelName}, proceeding anyway`);
23
+ return;
24
+ }
25
+ await new Promise(r => setTimeout(r, pollMs));
26
+ }
27
+ logger.info(`[Reload] Drain complete: ${channelName}`);
28
+ }
29
+ else if (drainDelayMs > 0) {
30
+ await new Promise(r => setTimeout(r, drainDelayMs));
31
+ }
32
+ },
33
+ async disconnectChannel(channelName) {
34
+ const inst = channelInstances.find(i => i.adapter.channelName === channelName);
35
+ if (!inst) {
36
+ logger.warn(`[Reload] Channel ${channelName} not found, skipping disconnect`);
37
+ return;
38
+ }
39
+ try {
40
+ await inst.disconnect();
41
+ const idx = channelInstances.indexOf(inst);
42
+ if (idx >= 0)
43
+ channelInstances.splice(idx, 1);
44
+ logger.info(`[Reload] Disconnected channel: ${channelName}`);
45
+ }
46
+ catch (e) {
47
+ logger.error(`[Reload] Failed to disconnect ${channelName}: ${e}`);
48
+ throw e;
49
+ }
50
+ },
51
+ async startChannel(agent, channelName) {
52
+ const channels = agent.config.channels;
53
+ let channelType = null;
54
+ for (const [type, raw] of Object.entries(channels)) {
55
+ const instances = Array.isArray(raw) ? raw : [raw];
56
+ for (const inst of instances) {
57
+ const name = inst.name ?? type;
58
+ if (name === channelName) {
59
+ channelType = type;
60
+ break;
61
+ }
62
+ }
63
+ if (channelType)
64
+ break;
65
+ }
66
+ if (!channelType) {
67
+ const msg = `[Reload] Channel ${channelName} not found in agent ${agent.name} config`;
68
+ logger.error(msg);
69
+ throw new Error(msg);
70
+ }
71
+ const partialConfig = {
72
+ agents: agent.config.agents,
73
+ channels: { [channelType]: channels[channelType] },
74
+ projects: agent.config.projects,
75
+ };
76
+ const newInstances = await channelLoader.createAll(partialConfig);
77
+ const newInst = newInstances.find(i => i.adapter.channelName === channelName);
78
+ if (!newInst) {
79
+ throw new Error(`[Reload] Failed to create instance ${channelName}`);
80
+ }
81
+ registerChannelInstance(newInst);
82
+ await newInst.connect();
83
+ channelInstances.push(newInst);
84
+ logger.info(`[Reload] Started channel: ${channelName}`);
85
+ },
86
+ };
87
+ }
@@ -226,3 +226,36 @@ export async function closeBrowser() {
226
226
  browserInstance = null;
227
227
  }
228
228
  }
229
+ // ── Markdown → Plain Text ───────────────────────────────────────────────────
230
+ // 用于不渲染 markdown 的渠道(微信、QQ)将 Agent 输出降级为纯文本。
231
+ export function markdownToPlainText(text) {
232
+ let result = text;
233
+ // Code blocks: strip fences, keep content
234
+ result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
235
+ // Images: remove entirely
236
+ result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
237
+ // Links: keep display text only
238
+ result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
239
+ // Tables: remove separator rows
240
+ result = result.replace(/^\|[\s:|-]+\|$/gm, '');
241
+ result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split('|').map(cell => cell.trim()).join(' '));
242
+ // Bold/italic
243
+ result = result.replace(/\*\*(.+?)\*\*/g, '$1');
244
+ result = result.replace(/\*(.+?)\*/g, '$1');
245
+ result = result.replace(/__(.+?)__/g, '$1');
246
+ result = result.replace(/_(.+?)_/g, '$1');
247
+ // Strikethrough
248
+ result = result.replace(/~~(.+?)~~/g, '$1');
249
+ // Inline code
250
+ result = result.replace(/`([^`]+)`/g, '$1');
251
+ // Headers
252
+ result = result.replace(/^#{1,6}\s+/gm, '');
253
+ // Blockquotes
254
+ result = result.replace(/^>\s?/gm, '');
255
+ // Horizontal rules
256
+ result = result.replace(/^[-*_]{3,}$/gm, '');
257
+ // List markers
258
+ result = result.replace(/^(\s*)[-*+]\s/gm, '$1');
259
+ result = result.replace(/^(\s*)\d+\.\s/gm, '$1');
260
+ return result.trim();
261
+ }