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.
- package/dist/agents/templates.js +122 -0
- package/dist/channels/aun-ops.js +275 -0
- package/dist/channels/aun.js +206 -103
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/cli.js +676 -20
- package/dist/config.js +94 -22
- package/dist/core/agent-registry.js +450 -0
- package/dist/core/command-handler.js +422 -255
- package/dist/core/evolagent-registry.js +503 -0
- package/dist/core/evolagent-schema.js +72 -0
- package/dist/core/evolagent.js +315 -0
- package/dist/core/message/message-bridge.js +23 -3
- package/dist/core/message/message-processor.js +56 -11
- package/dist/core/message/message-queue.js +59 -4
- package/dist/core/reload-hooks.js +87 -0
- package/dist/index.js +119 -20
- package/dist/ipc.js +47 -0
- package/dist/paths.js +2 -0
- package/dist/types.js +2 -0
- package/dist/utils/init-channel.js +91 -221
- package/dist/utils/init.js +18 -42
- package/dist/utils/logger.js +58 -2
- package/dist/utils/reload-hooks.js +87 -0
- package/dist/utils/rich-content-renderer.js +33 -0
- package/dist/utils/stats-collector.js +15 -10
- package/evolclaw-install-aun.md +48 -7
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
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 (
|
|
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
|
-
|
|
715
|
-
|
|
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
|
|
663
|
+
// Create AID + agent.md via atomic ops
|
|
833
664
|
console.log(' 正在创建 AID...');
|
|
834
665
|
let failed = false;
|
|
835
666
|
try {
|
|
836
|
-
const
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/utils/init.js
CHANGED
|
@@ -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(), '
|
|
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
|
-
|
|
409
|
-
|
|
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
|
|
419
|
-
|
|
420
|
-
|
|
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.
|
|
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
|
-
|
|
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(), '
|
|
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;
|
package/dist/utils/logger.js
CHANGED
|
@@ -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(
|
|
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
|
+
}
|