evolclaw 2.4.0 → 2.5.0
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/README.md +33 -14
- package/dist/agents/claude-runner.js +224 -23
- package/dist/agents/codex-runner.js +2 -8
- package/dist/agents/gemini-runner.js +1 -8
- package/dist/channels/aun.js +438 -53
- package/dist/channels/dingtalk.js +506 -0
- package/dist/channels/feishu.js +31 -231
- package/dist/channels/qqbot.js +391 -0
- package/dist/channels/wechat.js +36 -38
- package/dist/channels/wecom.js +549 -0
- package/dist/cli.js +69 -9
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +462 -112
- package/dist/core/message/message-bridge.js +8 -5
- package/dist/core/message/message-processor.js +146 -54
- package/dist/core/message/message-queue.js +48 -0
- package/dist/core/message/stream-flusher.js +2 -2
- package/dist/core/session/session-manager.js +21 -3
- package/dist/index.js +48 -13
- package/dist/ipc.js +14 -4
- package/dist/templates/skills.md +64 -0
- package/dist/utils/error-dict.js +63 -0
- package/dist/utils/error-utils.js +156 -56
- package/dist/utils/format.js +32 -0
- package/dist/utils/init-channel.js +734 -8
- package/dist/utils/init.js +33 -2
- package/dist/utils/media-cache.js +2 -0
- package/dist/utils/stats-collector.js +0 -8
- package/package.json +9 -3
|
@@ -9,9 +9,31 @@ import fs from 'fs';
|
|
|
9
9
|
import readline from 'readline';
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import os from 'os';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
import { execFile } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
12
16
|
import { resolvePaths } from '../paths.js';
|
|
13
17
|
import { normalizeChannelInstances } from '../config.js';
|
|
14
18
|
import { selectInstance } from './init.js';
|
|
19
|
+
import { isWindows } from './cross-platform.js';
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
async function npmInstallGlobal(pkg) {
|
|
22
|
+
try {
|
|
23
|
+
await execFileAsync('npm', ['install', '-g', pkg], { timeout: 180000 });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
|
|
27
|
+
if (isWindows) {
|
|
28
|
+
throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
|
|
29
|
+
}
|
|
30
|
+
await execFileAsync('sudo', ['npm', 'install', '-g', pkg], { timeout: 180000 });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
function ask(rl, question) {
|
|
16
38
|
return new Promise(resolve => rl.question(question, resolve));
|
|
17
39
|
}
|
|
@@ -362,8 +384,9 @@ export async function runWechatQrFlow() {
|
|
|
362
384
|
console.log('请用微信扫描上方二维码...\n');
|
|
363
385
|
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
364
386
|
let scannedPrinted = false;
|
|
387
|
+
let currentPollUrl = DEFAULT_BASE_URL;
|
|
365
388
|
while (Date.now() < deadline) {
|
|
366
|
-
const status = await pollQRStatus(
|
|
389
|
+
const status = await pollQRStatus(currentPollUrl, qrResp.qrcode);
|
|
367
390
|
switch (status.status) {
|
|
368
391
|
case 'wait':
|
|
369
392
|
process.stdout.write('.');
|
|
@@ -374,6 +397,11 @@ export async function runWechatQrFlow() {
|
|
|
374
397
|
scannedPrinted = true;
|
|
375
398
|
}
|
|
376
399
|
break;
|
|
400
|
+
case 'scaned_but_redirect':
|
|
401
|
+
if (status.redirect_host) {
|
|
402
|
+
currentPollUrl = `https://${status.redirect_host}`;
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
377
405
|
case 'expired':
|
|
378
406
|
console.error('\n二维码已过期');
|
|
379
407
|
return null;
|
|
@@ -440,8 +468,9 @@ export async function cmdInitWechat() {
|
|
|
440
468
|
console.log('请用微信扫描上方二维码...\n');
|
|
441
469
|
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
442
470
|
let scannedPrinted = false;
|
|
471
|
+
let currentPollUrl = DEFAULT_BASE_URL;
|
|
443
472
|
while (Date.now() < deadline) {
|
|
444
|
-
const status = await pollQRStatus(
|
|
473
|
+
const status = await pollQRStatus(currentPollUrl, qrResp.qrcode);
|
|
445
474
|
switch (status.status) {
|
|
446
475
|
case 'wait':
|
|
447
476
|
process.stdout.write('.');
|
|
@@ -452,6 +481,11 @@ export async function cmdInitWechat() {
|
|
|
452
481
|
scannedPrinted = true;
|
|
453
482
|
}
|
|
454
483
|
break;
|
|
484
|
+
case 'scaned_but_redirect':
|
|
485
|
+
if (status.redirect_host) {
|
|
486
|
+
currentPollUrl = `https://${status.redirect_host}`;
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
455
489
|
case 'expired':
|
|
456
490
|
console.log('\n二维码已过期,请重新运行 evolclaw init wechat');
|
|
457
491
|
process.exit(1);
|
|
@@ -528,10 +562,88 @@ export async function cmdInitWechat() {
|
|
|
528
562
|
process.exit(1);
|
|
529
563
|
}
|
|
530
564
|
// ==================== AUN ====================
|
|
565
|
+
// 最低 @eleans/aun-core-sdk 版本要求
|
|
566
|
+
const MIN_AUN_CORE_SDK = [0, 2, 9];
|
|
567
|
+
const AUN_CORE_SDK_PKG = '@eleans/aun-core-sdk';
|
|
568
|
+
function compareVersion(a, min) {
|
|
569
|
+
const parts = a.split('.').map(n => parseInt(n, 10));
|
|
570
|
+
if (parts.length < 3 || parts.some(isNaN))
|
|
571
|
+
return false;
|
|
572
|
+
if (parts[0] !== min[0])
|
|
573
|
+
return parts[0] > min[0];
|
|
574
|
+
if (parts[1] !== min[1])
|
|
575
|
+
return parts[1] > min[1];
|
|
576
|
+
return parts[2] >= min[2];
|
|
577
|
+
}
|
|
578
|
+
function resolveAunCoreSdkPkg() {
|
|
579
|
+
try {
|
|
580
|
+
const esmRequire = createRequire(import.meta.url);
|
|
581
|
+
const entry = esmRequire.resolve(AUN_CORE_SDK_PKG);
|
|
582
|
+
const pkgPath = path.join(path.dirname(entry), 'package.json');
|
|
583
|
+
if (!fs.existsSync(pkgPath)) {
|
|
584
|
+
// 向上回溯查找 package.json
|
|
585
|
+
let dir = path.dirname(entry);
|
|
586
|
+
for (let i = 0; i < 5; i++) {
|
|
587
|
+
const candidate = path.join(dir, 'package.json');
|
|
588
|
+
if (fs.existsSync(candidate)) {
|
|
589
|
+
const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
590
|
+
if (data.name === AUN_CORE_SDK_PKG)
|
|
591
|
+
return { version: data.version, path: candidate };
|
|
592
|
+
}
|
|
593
|
+
dir = path.dirname(dir);
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
598
|
+
return { version: data.version, path: pkgPath };
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
531
604
|
export async function checkAunEnvironment(rl) {
|
|
532
605
|
console.log('\n🔍 AUN 环境检查...\n');
|
|
533
|
-
|
|
534
|
-
|
|
606
|
+
const minVer = MIN_AUN_CORE_SDK.join('.');
|
|
607
|
+
const installed = resolveAunCoreSdkPkg();
|
|
608
|
+
if (!installed) {
|
|
609
|
+
console.log(` ✗ ${AUN_CORE_SDK_PKG} 未安装`);
|
|
610
|
+
const answer = (await ask(rl, ` → 是否安装 ${AUN_CORE_SDK_PKG}@latest?[Y/n] `)).trim().toLowerCase();
|
|
611
|
+
if (answer === 'n' || answer === 'no') {
|
|
612
|
+
console.log(' 已取消');
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
console.log(` 正在安装 ${AUN_CORE_SDK_PKG}...`);
|
|
616
|
+
try {
|
|
617
|
+
await npmInstallGlobal(`${AUN_CORE_SDK_PKG}@latest`);
|
|
618
|
+
console.log(` ✓ ${AUN_CORE_SDK_PKG} 安装完成`);
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
console.log(` ✗ 安装失败: ${e.message?.slice(0, 200) || e}`);
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
console.log('');
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
if (compareVersion(installed.version, MIN_AUN_CORE_SDK)) {
|
|
628
|
+
console.log(` ✓ ${AUN_CORE_SDK_PKG} v${installed.version}`);
|
|
629
|
+
console.log('');
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
console.log(` ✗ ${AUN_CORE_SDK_PKG} v${installed.version} — 需要 >= ${minVer}`);
|
|
633
|
+
const answer = (await ask(rl, ` → 是否升级 ${AUN_CORE_SDK_PKG}?[Y/n] `)).trim().toLowerCase();
|
|
634
|
+
if (answer === 'n' || answer === 'no') {
|
|
635
|
+
console.log(' 已取消');
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
console.log(` 正在升级 ${AUN_CORE_SDK_PKG}...`);
|
|
639
|
+
try {
|
|
640
|
+
await npmInstallGlobal(`${AUN_CORE_SDK_PKG}@latest`);
|
|
641
|
+
console.log(` ✓ ${AUN_CORE_SDK_PKG} 升级完成`);
|
|
642
|
+
}
|
|
643
|
+
catch (e) {
|
|
644
|
+
console.log(` ✗ 升级失败: ${e.message?.slice(0, 200) || e}`);
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
535
647
|
console.log('');
|
|
536
648
|
return true;
|
|
537
649
|
}
|
|
@@ -541,7 +653,7 @@ function isValidAid(name) {
|
|
|
541
653
|
}
|
|
542
654
|
export async function setupAunAid(rl, _config) {
|
|
543
655
|
let aid = '';
|
|
544
|
-
let gatewayPort;
|
|
656
|
+
let gatewayPort; // only used locally for AID creation, not written to config
|
|
545
657
|
// Outer loop: allows retrying with a different AID
|
|
546
658
|
while (true) {
|
|
547
659
|
// Ask AID with format validation
|
|
@@ -579,7 +691,7 @@ export async function setupAunAid(rl, _config) {
|
|
|
579
691
|
console.log(' 正在创建 AID...');
|
|
580
692
|
let failed = false;
|
|
581
693
|
try {
|
|
582
|
-
const { AUNClient } = await import('@eleans/aun-core-
|
|
694
|
+
const { AUNClient } = await import('@eleans/aun-core-sdk');
|
|
583
695
|
const client = new AUNClient({ aun_path: aunPath });
|
|
584
696
|
// Set gateway URL from AID domain + port
|
|
585
697
|
const domain = aid.split('.').slice(1).join('.');
|
|
@@ -587,6 +699,19 @@ export async function setupAunAid(rl, _config) {
|
|
|
587
699
|
client._gatewayUrl = `wss://gateway.${domain}:${port}/aun`;
|
|
588
700
|
const result = await client.auth.createAid({ aid });
|
|
589
701
|
console.log(` ✓ AID ${result.aid} 创建成功`);
|
|
702
|
+
// Collect agent.md info and publish
|
|
703
|
+
const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
|
|
704
|
+
const agentType = typeInput === 'human' ? 'human' : 'ai';
|
|
705
|
+
const agentName = aid.split('.')[0];
|
|
706
|
+
const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\n---\n`;
|
|
707
|
+
try {
|
|
708
|
+
await client.auth.uploadAgentMd(agentMdContent);
|
|
709
|
+
console.log(' ✓ agent.md 已发布');
|
|
710
|
+
fs.writeFileSync(path.join(aidDir, 'agent.md'), agentMdContent, 'utf-8');
|
|
711
|
+
}
|
|
712
|
+
catch (e) {
|
|
713
|
+
console.log(` ⚠ agent.md 发布失败(可稍后用 /agentmd put 重试): ${String(e.message || e).slice(0, 100)}`);
|
|
714
|
+
}
|
|
590
715
|
try {
|
|
591
716
|
await client.close();
|
|
592
717
|
}
|
|
@@ -607,7 +732,7 @@ export async function setupAunAid(rl, _config) {
|
|
|
607
732
|
break;
|
|
608
733
|
// default: retry with new AID
|
|
609
734
|
}
|
|
610
|
-
return { aid
|
|
735
|
+
return { aid };
|
|
611
736
|
}
|
|
612
737
|
export async function cmdInitAun() {
|
|
613
738
|
const p = resolvePaths();
|
|
@@ -636,7 +761,6 @@ export async function cmdInitAun() {
|
|
|
636
761
|
config.channels.aun = {
|
|
637
762
|
enabled: true,
|
|
638
763
|
aid: result.aid,
|
|
639
|
-
...(result.gatewayPort && { gatewayPort: result.gatewayPort }),
|
|
640
764
|
};
|
|
641
765
|
if (!config.channels.defaultChannel)
|
|
642
766
|
config.channels.defaultChannel = 'aun';
|
|
@@ -647,3 +771,605 @@ export async function cmdInitAun() {
|
|
|
647
771
|
rl.close();
|
|
648
772
|
}
|
|
649
773
|
}
|
|
774
|
+
// ==================== DingTalk ====================
|
|
775
|
+
const DINGTALK_BASE_URL = 'https://oapi.dingtalk.com';
|
|
776
|
+
const DINGTALK_SOURCE = 'openClaw';
|
|
777
|
+
async function dingtalkApiPost(path, payload) {
|
|
778
|
+
const res = await fetch(`${DINGTALK_BASE_URL}${path}`, {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
headers: { 'Content-Type': 'application/json' },
|
|
781
|
+
body: JSON.stringify(payload),
|
|
782
|
+
});
|
|
783
|
+
if (!res.ok)
|
|
784
|
+
throw new Error(`DingTalk API ${path} failed: ${res.status}`);
|
|
785
|
+
const data = await res.json();
|
|
786
|
+
if (data.errcode && data.errcode !== 0) {
|
|
787
|
+
throw new Error(`DingTalk API ${path}: ${data.errmsg || 'unknown error'} (errcode=${data.errcode})`);
|
|
788
|
+
}
|
|
789
|
+
return data;
|
|
790
|
+
}
|
|
791
|
+
async function runDingtalkQrFlow() {
|
|
792
|
+
// Step 1: init → nonce
|
|
793
|
+
const initData = await dingtalkApiPost('/app/registration/init', { source: DINGTALK_SOURCE });
|
|
794
|
+
const nonce = initData.nonce?.trim();
|
|
795
|
+
if (!nonce)
|
|
796
|
+
throw new Error('DingTalk init: 未返回 nonce');
|
|
797
|
+
// Step 2: begin → device_code + verification_uri_complete
|
|
798
|
+
const beginData = await dingtalkApiPost('/app/registration/begin', { nonce });
|
|
799
|
+
const deviceCode = beginData.device_code?.trim();
|
|
800
|
+
const verificationUri = beginData.verification_uri_complete?.trim();
|
|
801
|
+
if (!deviceCode)
|
|
802
|
+
throw new Error('DingTalk begin: 未返回 device_code');
|
|
803
|
+
if (!verificationUri)
|
|
804
|
+
throw new Error('DingTalk begin: 未返回 verification_uri_complete');
|
|
805
|
+
// Display QR code
|
|
806
|
+
try {
|
|
807
|
+
const qrterm = await import('qrcode-terminal');
|
|
808
|
+
await new Promise(resolve => {
|
|
809
|
+
qrterm.default.generate(verificationUri, { small: true }, (qr) => {
|
|
810
|
+
console.log(qr);
|
|
811
|
+
resolve();
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
console.log(`请在浏览器中打开此链接扫码: ${verificationUri}\n`);
|
|
817
|
+
}
|
|
818
|
+
console.log('请用钉钉扫描上方二维码...\n');
|
|
819
|
+
console.log('提示: 扫码页面标注 "OpenClaw" 是钉钉生态接入桥,可放心使用。\n');
|
|
820
|
+
console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
|
|
821
|
+
let userAction = null;
|
|
822
|
+
const setupKeyListener = () => {
|
|
823
|
+
if (!process.stdin.isTTY)
|
|
824
|
+
return () => { };
|
|
825
|
+
process.stdin.setRawMode(true);
|
|
826
|
+
process.stdin.resume();
|
|
827
|
+
process.stdin.setEncoding('utf8');
|
|
828
|
+
const handler = (key) => {
|
|
829
|
+
if (key === 'q' || key === '\u0003')
|
|
830
|
+
userAction = QUIT;
|
|
831
|
+
if (key === 's')
|
|
832
|
+
userAction = SKIP;
|
|
833
|
+
};
|
|
834
|
+
process.stdin.on('data', handler);
|
|
835
|
+
return () => {
|
|
836
|
+
process.stdin.removeListener('data', handler);
|
|
837
|
+
process.stdin.setRawMode(false);
|
|
838
|
+
process.stdin.pause();
|
|
839
|
+
};
|
|
840
|
+
};
|
|
841
|
+
const cleanup = setupKeyListener();
|
|
842
|
+
const pollInterval = Math.max(Number(beginData.interval ?? 3), 2);
|
|
843
|
+
const expiresIn = Number(beginData.expires_in ?? 7200);
|
|
844
|
+
const startedAt = Date.now();
|
|
845
|
+
try {
|
|
846
|
+
while (Date.now() - startedAt < expiresIn * 1000) {
|
|
847
|
+
if (userAction === QUIT)
|
|
848
|
+
return QUIT;
|
|
849
|
+
if (userAction === SKIP)
|
|
850
|
+
return SKIP;
|
|
851
|
+
await new Promise(r => setTimeout(r, pollInterval * 1000));
|
|
852
|
+
const pollData = await dingtalkApiPost('/app/registration/poll', { device_code: deviceCode });
|
|
853
|
+
const status = (pollData.status || '').trim().toUpperCase();
|
|
854
|
+
if (status === 'SUCCESS') {
|
|
855
|
+
if (!pollData.client_id || !pollData.client_secret) {
|
|
856
|
+
throw new Error('授权成功但未返回凭据');
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
clientId: pollData.client_id.trim(),
|
|
860
|
+
clientSecret: pollData.client_secret.trim(),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (status === 'WAITING') {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (status === 'EXPIRED') {
|
|
867
|
+
throw new Error('扫码会话已过期');
|
|
868
|
+
}
|
|
869
|
+
if (status === 'FAIL') {
|
|
870
|
+
throw new Error(`授权失败: ${pollData.fail_reason || '未知原因'}`);
|
|
871
|
+
}
|
|
872
|
+
// Unknown status — keep polling
|
|
873
|
+
}
|
|
874
|
+
throw new Error('等待扫码结果超时');
|
|
875
|
+
}
|
|
876
|
+
finally {
|
|
877
|
+
cleanup();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
export async function runDingtalkQrFlowSimple() {
|
|
881
|
+
try {
|
|
882
|
+
const result = await runDingtalkQrFlow();
|
|
883
|
+
if (result === QUIT || result === SKIP)
|
|
884
|
+
return null;
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
export async function cmdInitDingtalk() {
|
|
893
|
+
const p = resolvePaths();
|
|
894
|
+
if (!fs.existsSync(p.config)) {
|
|
895
|
+
console.log('❌ 配置文件不存在,请先运行 evolclaw init');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
899
|
+
// Normalize existing instances and filter out placeholders
|
|
900
|
+
const allInstances = normalizeChannelInstances(config.channels?.dingtalk, 'dingtalk');
|
|
901
|
+
const validInstances = [];
|
|
902
|
+
for (let i = 0; i < allInstances.length; i++) {
|
|
903
|
+
const inst = allInstances[i];
|
|
904
|
+
if (!inst.clientId || !inst.clientSecret)
|
|
905
|
+
continue;
|
|
906
|
+
if (inst.clientId.includes('your-') || inst.clientId.includes('placeholder'))
|
|
907
|
+
continue;
|
|
908
|
+
if (inst.clientSecret.includes('your-') || inst.clientSecret.includes('placeholder'))
|
|
909
|
+
continue;
|
|
910
|
+
validInstances.push({ ...inst, originalIndex: i });
|
|
911
|
+
}
|
|
912
|
+
let choice = null;
|
|
913
|
+
if (validInstances.length > 0) {
|
|
914
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
915
|
+
try {
|
|
916
|
+
choice = await selectInstance(rl, 'dingtalk', validInstances);
|
|
917
|
+
if (choice === null)
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
finally {
|
|
921
|
+
rl.close();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
console.log('正在获取钉钉登录二维码...\n');
|
|
925
|
+
let result;
|
|
926
|
+
try {
|
|
927
|
+
const flowResult = await runDingtalkQrFlow();
|
|
928
|
+
if (flowResult === QUIT) {
|
|
929
|
+
console.log('已退出');
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (flowResult === SKIP) {
|
|
933
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
934
|
+
try {
|
|
935
|
+
console.log('\n手动输入模式:\n');
|
|
936
|
+
let clientId = '';
|
|
937
|
+
while (!clientId) {
|
|
938
|
+
clientId = (await ask(rl, ' 钉钉 Client ID (AppKey): ')).trim();
|
|
939
|
+
if (!clientId)
|
|
940
|
+
console.log(' ⚠ 不能为空');
|
|
941
|
+
}
|
|
942
|
+
let clientSecret = '';
|
|
943
|
+
while (!clientSecret) {
|
|
944
|
+
clientSecret = (await ask(rl, ' 钉钉 Client Secret (AppSecret): ')).trim();
|
|
945
|
+
if (!clientSecret)
|
|
946
|
+
console.log(' ⚠ 不能为空');
|
|
947
|
+
}
|
|
948
|
+
result = { clientId, clientSecret };
|
|
949
|
+
}
|
|
950
|
+
finally {
|
|
951
|
+
rl.close();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
result = flowResult;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
console.error(`\n登录失败: ${error instanceof Error ? error.message : error}`);
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
// Write config to the correct slot
|
|
963
|
+
if (!config.channels)
|
|
964
|
+
config.channels = {};
|
|
965
|
+
if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.dingtalk)) {
|
|
966
|
+
const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
|
|
967
|
+
config.channels.dingtalk[idx].clientId = result.clientId;
|
|
968
|
+
config.channels.dingtalk[idx].clientSecret = result.clientSecret;
|
|
969
|
+
config.channels.dingtalk[idx].enabled = true;
|
|
970
|
+
}
|
|
971
|
+
else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.dingtalk)) {
|
|
972
|
+
config.channels.dingtalk = config.channels.dingtalk || {};
|
|
973
|
+
config.channels.dingtalk.clientId = result.clientId;
|
|
974
|
+
config.channels.dingtalk.clientSecret = result.clientSecret;
|
|
975
|
+
config.channels.dingtalk.enabled = true;
|
|
976
|
+
}
|
|
977
|
+
else if (choice && choice.action === 'add') {
|
|
978
|
+
const newInst = {
|
|
979
|
+
name: choice.name,
|
|
980
|
+
clientId: result.clientId,
|
|
981
|
+
clientSecret: result.clientSecret,
|
|
982
|
+
enabled: true,
|
|
983
|
+
};
|
|
984
|
+
if (Array.isArray(config.channels.dingtalk)) {
|
|
985
|
+
config.channels.dingtalk.push(newInst);
|
|
986
|
+
}
|
|
987
|
+
else if (config.channels.dingtalk) {
|
|
988
|
+
const oldInst = { ...config.channels.dingtalk, name: config.channels.dingtalk.name || 'dingtalk' };
|
|
989
|
+
config.channels.dingtalk = [oldInst, newInst];
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
config.channels.dingtalk = [newInst];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
config.channels.dingtalk = {
|
|
997
|
+
clientId: result.clientId,
|
|
998
|
+
clientSecret: result.clientSecret,
|
|
999
|
+
enabled: true,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (!config.channels.defaultChannel)
|
|
1003
|
+
config.channels.defaultChannel = 'dingtalk';
|
|
1004
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
1005
|
+
console.log(`\n✅ 钉钉连接成功!`);
|
|
1006
|
+
console.log(` Client ID: ${result.clientId}`);
|
|
1007
|
+
if (choice) {
|
|
1008
|
+
console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
|
|
1009
|
+
}
|
|
1010
|
+
console.log(` 配置已写入: ${p.config}`);
|
|
1011
|
+
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
1012
|
+
}
|
|
1013
|
+
// ==================== QQBot ====================
|
|
1014
|
+
const QQBOT_PORTAL_HOST = 'q.qq.com';
|
|
1015
|
+
const QQBOT_CREATE_PATH = '/lite/create_bind_task';
|
|
1016
|
+
const QQBOT_POLL_PATH = '/lite/poll_bind_result';
|
|
1017
|
+
const QQBOT_QR_TEMPLATE = 'https://q.qq.com/qqbot/openclaw/connect.html?task_id={task_id}&_wv=2&source=hermes';
|
|
1018
|
+
const QQBOT_POLL_INTERVAL_MS = 2000;
|
|
1019
|
+
const QQBOT_POLL_TIMEOUT_MS = 600_000; // 10 minutes
|
|
1020
|
+
function qqbotApiHeaders() {
|
|
1021
|
+
return {
|
|
1022
|
+
'Content-Type': 'application/json',
|
|
1023
|
+
'Accept': 'application/json', // Required — without it, q.qq.com returns anti-bot HTML
|
|
1024
|
+
'User-Agent': 'EvolClaw/QQBotInit',
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function generateBindKey() {
|
|
1028
|
+
const keyBuffer = crypto.randomBytes(32);
|
|
1029
|
+
return { keyBase64: keyBuffer.toString('base64'), keyBuffer };
|
|
1030
|
+
}
|
|
1031
|
+
function decryptSecret(encryptedBase64, keyBuffer) {
|
|
1032
|
+
const raw = Buffer.from(encryptedBase64, 'base64');
|
|
1033
|
+
const iv = raw.subarray(0, 12);
|
|
1034
|
+
const authTag = raw.subarray(raw.length - 16);
|
|
1035
|
+
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
1036
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
|
|
1037
|
+
decipher.setAuthTag(authTag);
|
|
1038
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1039
|
+
return decrypted.toString('utf-8');
|
|
1040
|
+
}
|
|
1041
|
+
async function runQQBotBindFlow() {
|
|
1042
|
+
// Step 1: Generate AES key and create bind task
|
|
1043
|
+
const { keyBase64, keyBuffer } = generateBindKey();
|
|
1044
|
+
const createRes = await fetch(`https://${QQBOT_PORTAL_HOST}${QQBOT_CREATE_PATH}`, {
|
|
1045
|
+
method: 'POST',
|
|
1046
|
+
headers: qqbotApiHeaders(),
|
|
1047
|
+
body: JSON.stringify({ key: keyBase64 }),
|
|
1048
|
+
});
|
|
1049
|
+
if (!createRes.ok)
|
|
1050
|
+
throw new Error(`create_bind_task failed: ${createRes.status}`);
|
|
1051
|
+
const createData = await createRes.json();
|
|
1052
|
+
if (createData.retcode !== 0) {
|
|
1053
|
+
throw new Error(`create_bind_task: ${createData.msg || 'unknown error'}`);
|
|
1054
|
+
}
|
|
1055
|
+
const taskId = createData.data?.task_id;
|
|
1056
|
+
if (!taskId)
|
|
1057
|
+
throw new Error('create_bind_task: 未返回 task_id');
|
|
1058
|
+
// Step 2: Build QR URL and display
|
|
1059
|
+
const qrUrl = QQBOT_QR_TEMPLATE.replace('{task_id}', encodeURIComponent(taskId));
|
|
1060
|
+
try {
|
|
1061
|
+
const qrterm = await import('qrcode-terminal');
|
|
1062
|
+
await new Promise(resolve => {
|
|
1063
|
+
qrterm.default.generate(qrUrl, { small: true }, (qr) => {
|
|
1064
|
+
console.log(qr);
|
|
1065
|
+
resolve();
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
console.log(`请在浏览器中打开此链接扫码: ${qrUrl}\n`);
|
|
1071
|
+
}
|
|
1072
|
+
console.log('请用 QQ 扫描上方二维码绑定机器人...\n');
|
|
1073
|
+
console.log('按 q 退出 | 按 s 跳过扫码手动输入\n');
|
|
1074
|
+
let userAction = null;
|
|
1075
|
+
const setupKeyListener = () => {
|
|
1076
|
+
if (!process.stdin.isTTY)
|
|
1077
|
+
return () => { };
|
|
1078
|
+
process.stdin.setRawMode(true);
|
|
1079
|
+
process.stdin.resume();
|
|
1080
|
+
process.stdin.setEncoding('utf8');
|
|
1081
|
+
const handler = (key) => {
|
|
1082
|
+
if (key === 'q' || key === '\u0003')
|
|
1083
|
+
userAction = QUIT;
|
|
1084
|
+
if (key === 's')
|
|
1085
|
+
userAction = SKIP;
|
|
1086
|
+
};
|
|
1087
|
+
process.stdin.on('data', handler);
|
|
1088
|
+
return () => {
|
|
1089
|
+
process.stdin.removeListener('data', handler);
|
|
1090
|
+
process.stdin.setRawMode(false);
|
|
1091
|
+
process.stdin.pause();
|
|
1092
|
+
};
|
|
1093
|
+
};
|
|
1094
|
+
const cleanup = setupKeyListener();
|
|
1095
|
+
const startedAt = Date.now();
|
|
1096
|
+
try {
|
|
1097
|
+
// Step 3: Poll for bind result
|
|
1098
|
+
while (Date.now() - startedAt < QQBOT_POLL_TIMEOUT_MS) {
|
|
1099
|
+
if (userAction === QUIT)
|
|
1100
|
+
return QUIT;
|
|
1101
|
+
if (userAction === SKIP)
|
|
1102
|
+
return SKIP;
|
|
1103
|
+
await new Promise(r => setTimeout(r, QQBOT_POLL_INTERVAL_MS));
|
|
1104
|
+
const pollRes = await fetch(`https://${QQBOT_PORTAL_HOST}${QQBOT_POLL_PATH}`, {
|
|
1105
|
+
method: 'POST',
|
|
1106
|
+
headers: qqbotApiHeaders(),
|
|
1107
|
+
body: JSON.stringify({ task_id: taskId }),
|
|
1108
|
+
});
|
|
1109
|
+
if (!pollRes.ok)
|
|
1110
|
+
continue; // transient error, keep polling
|
|
1111
|
+
const pollData = await pollRes.json();
|
|
1112
|
+
if (pollData.retcode !== 0)
|
|
1113
|
+
continue;
|
|
1114
|
+
const status = pollData.data?.status ?? 0 /* QQBotBindStatus.NONE */;
|
|
1115
|
+
if (status === 2 /* QQBotBindStatus.COMPLETED */) {
|
|
1116
|
+
const botAppId = pollData.data?.bot_appid;
|
|
1117
|
+
const encryptedSecret = pollData.data?.bot_encrypt_secret;
|
|
1118
|
+
if (!botAppId || !encryptedSecret) {
|
|
1119
|
+
throw new Error('绑定成功但未返回完整凭据');
|
|
1120
|
+
}
|
|
1121
|
+
// Step 4: Decrypt the secret
|
|
1122
|
+
const clientSecret = decryptSecret(encryptedSecret, keyBuffer);
|
|
1123
|
+
return { appId: botAppId, clientSecret };
|
|
1124
|
+
}
|
|
1125
|
+
if (status === 3 /* QQBotBindStatus.EXPIRED */) {
|
|
1126
|
+
throw new Error('二维码已过期');
|
|
1127
|
+
}
|
|
1128
|
+
// NONE or PENDING — keep polling
|
|
1129
|
+
}
|
|
1130
|
+
throw new Error('等待扫码结果超时');
|
|
1131
|
+
}
|
|
1132
|
+
finally {
|
|
1133
|
+
cleanup();
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
export async function runQQBotBindFlowSimple() {
|
|
1137
|
+
try {
|
|
1138
|
+
const result = await runQQBotBindFlow();
|
|
1139
|
+
if (result === QUIT || result === SKIP)
|
|
1140
|
+
return null;
|
|
1141
|
+
return result;
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
export async function cmdInitQQBot() {
|
|
1149
|
+
const p = resolvePaths();
|
|
1150
|
+
if (!fs.existsSync(p.config)) {
|
|
1151
|
+
console.log('❌ 配置文件不存在,请先运行 evolclaw init');
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
1155
|
+
// Normalize existing instances and filter out placeholders
|
|
1156
|
+
const allInstances = normalizeChannelInstances(config.channels?.qqbot, 'qqbot');
|
|
1157
|
+
const validInstances = [];
|
|
1158
|
+
for (let i = 0; i < allInstances.length; i++) {
|
|
1159
|
+
const inst = allInstances[i];
|
|
1160
|
+
if (!inst.appId || !inst.clientSecret)
|
|
1161
|
+
continue;
|
|
1162
|
+
if (inst.appId.includes('your-') || inst.appId.includes('placeholder'))
|
|
1163
|
+
continue;
|
|
1164
|
+
if (inst.clientSecret.includes('your-') || inst.clientSecret.includes('placeholder'))
|
|
1165
|
+
continue;
|
|
1166
|
+
validInstances.push({ ...inst, originalIndex: i });
|
|
1167
|
+
}
|
|
1168
|
+
let choice = null;
|
|
1169
|
+
if (validInstances.length > 0) {
|
|
1170
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1171
|
+
try {
|
|
1172
|
+
choice = await selectInstance(rl, 'qqbot', validInstances);
|
|
1173
|
+
if (choice === null)
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
finally {
|
|
1177
|
+
rl.close();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
console.log('正在创建 QQ 机器人绑定任务...\n');
|
|
1181
|
+
let result;
|
|
1182
|
+
try {
|
|
1183
|
+
const flowResult = await runQQBotBindFlow();
|
|
1184
|
+
if (flowResult === QUIT) {
|
|
1185
|
+
console.log('已退出');
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
if (flowResult === SKIP) {
|
|
1189
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1190
|
+
try {
|
|
1191
|
+
console.log('\n手动输入模式:\n');
|
|
1192
|
+
let appId = '';
|
|
1193
|
+
while (!appId) {
|
|
1194
|
+
appId = (await ask(rl, ' QQ 机器人 App ID: ')).trim();
|
|
1195
|
+
if (!appId)
|
|
1196
|
+
console.log(' ⚠ 不能为空');
|
|
1197
|
+
}
|
|
1198
|
+
let clientSecret = '';
|
|
1199
|
+
while (!clientSecret) {
|
|
1200
|
+
clientSecret = (await ask(rl, ' QQ 机器人 Client Secret: ')).trim();
|
|
1201
|
+
if (!clientSecret)
|
|
1202
|
+
console.log(' ⚠ 不能为空');
|
|
1203
|
+
}
|
|
1204
|
+
result = { appId, clientSecret };
|
|
1205
|
+
}
|
|
1206
|
+
finally {
|
|
1207
|
+
rl.close();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
result = flowResult;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
console.error(`\n绑定失败: ${error instanceof Error ? error.message : error}`);
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
// Write config to the correct slot
|
|
1219
|
+
if (!config.channels)
|
|
1220
|
+
config.channels = {};
|
|
1221
|
+
if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.qqbot)) {
|
|
1222
|
+
const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
|
|
1223
|
+
config.channels.qqbot[idx].appId = result.appId;
|
|
1224
|
+
config.channels.qqbot[idx].clientSecret = result.clientSecret;
|
|
1225
|
+
config.channels.qqbot[idx].enabled = true;
|
|
1226
|
+
}
|
|
1227
|
+
else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.qqbot)) {
|
|
1228
|
+
config.channels.qqbot = config.channels.qqbot || {};
|
|
1229
|
+
config.channels.qqbot.appId = result.appId;
|
|
1230
|
+
config.channels.qqbot.clientSecret = result.clientSecret;
|
|
1231
|
+
config.channels.qqbot.enabled = true;
|
|
1232
|
+
}
|
|
1233
|
+
else if (choice && choice.action === 'add') {
|
|
1234
|
+
const newInst = {
|
|
1235
|
+
name: choice.name,
|
|
1236
|
+
appId: result.appId,
|
|
1237
|
+
clientSecret: result.clientSecret,
|
|
1238
|
+
enabled: true,
|
|
1239
|
+
};
|
|
1240
|
+
if (Array.isArray(config.channels.qqbot)) {
|
|
1241
|
+
config.channels.qqbot.push(newInst);
|
|
1242
|
+
}
|
|
1243
|
+
else if (config.channels.qqbot) {
|
|
1244
|
+
const oldInst = { ...config.channels.qqbot, name: config.channels.qqbot.name || 'qqbot' };
|
|
1245
|
+
config.channels.qqbot = [oldInst, newInst];
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
config.channels.qqbot = [newInst];
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
config.channels.qqbot = {
|
|
1253
|
+
appId: result.appId,
|
|
1254
|
+
clientSecret: result.clientSecret,
|
|
1255
|
+
enabled: true,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (!config.channels.defaultChannel)
|
|
1259
|
+
config.channels.defaultChannel = 'qqbot';
|
|
1260
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
1261
|
+
console.log(`\n✅ QQ 机器人绑定成功!`);
|
|
1262
|
+
console.log(` App ID: ${result.appId}`);
|
|
1263
|
+
if (choice) {
|
|
1264
|
+
console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
|
|
1265
|
+
}
|
|
1266
|
+
console.log(` 配置已写入: ${p.config}`);
|
|
1267
|
+
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
1268
|
+
}
|
|
1269
|
+
// ==================== WeCom (企业微信) ====================
|
|
1270
|
+
export async function cmdInitWecom() {
|
|
1271
|
+
const p = resolvePaths();
|
|
1272
|
+
if (!fs.existsSync(p.config)) {
|
|
1273
|
+
console.log('❌ 配置文件不存在,请先运行 evolclaw init');
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const config = JSON.parse(fs.readFileSync(p.config, 'utf-8'));
|
|
1277
|
+
// Normalize existing instances and filter out placeholders
|
|
1278
|
+
const allInstances = normalizeChannelInstances(config.channels?.wecom, 'wecom');
|
|
1279
|
+
const validInstances = [];
|
|
1280
|
+
for (let i = 0; i < allInstances.length; i++) {
|
|
1281
|
+
const inst = allInstances[i];
|
|
1282
|
+
if (!inst.botId || !inst.secret)
|
|
1283
|
+
continue;
|
|
1284
|
+
if (inst.botId.includes('your-') || inst.botId.includes('placeholder'))
|
|
1285
|
+
continue;
|
|
1286
|
+
if (inst.secret.includes('your-') || inst.secret.includes('placeholder'))
|
|
1287
|
+
continue;
|
|
1288
|
+
validInstances.push({ ...inst, originalIndex: i });
|
|
1289
|
+
}
|
|
1290
|
+
let choice = null;
|
|
1291
|
+
if (validInstances.length > 0) {
|
|
1292
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1293
|
+
try {
|
|
1294
|
+
choice = await selectInstance(rl, 'wecom', validInstances);
|
|
1295
|
+
if (choice === null)
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
finally {
|
|
1299
|
+
rl.close();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// WeCom uses manual input only (no QR flow)
|
|
1303
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1304
|
+
let result;
|
|
1305
|
+
try {
|
|
1306
|
+
console.log('企业微信 AI Bot 配置\n');
|
|
1307
|
+
console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
|
|
1308
|
+
let botId = '';
|
|
1309
|
+
while (!botId) {
|
|
1310
|
+
botId = (await ask(rl, ' Bot ID: ')).trim();
|
|
1311
|
+
if (!botId)
|
|
1312
|
+
console.log(' ⚠ 不能为空');
|
|
1313
|
+
}
|
|
1314
|
+
let secret = '';
|
|
1315
|
+
while (!secret) {
|
|
1316
|
+
secret = (await ask(rl, ' Secret: ')).trim();
|
|
1317
|
+
if (!secret)
|
|
1318
|
+
console.log(' ⚠ 不能为空');
|
|
1319
|
+
}
|
|
1320
|
+
result = { botId, secret };
|
|
1321
|
+
}
|
|
1322
|
+
finally {
|
|
1323
|
+
rl.close();
|
|
1324
|
+
}
|
|
1325
|
+
// Write config to the correct slot
|
|
1326
|
+
if (!config.channels)
|
|
1327
|
+
config.channels = {};
|
|
1328
|
+
if (choice && choice.action === 'overwrite' && Array.isArray(config.channels.wecom)) {
|
|
1329
|
+
const idx = validInstances[choice.index]?.originalIndex ?? choice.index;
|
|
1330
|
+
config.channels.wecom[idx].botId = result.botId;
|
|
1331
|
+
config.channels.wecom[idx].secret = result.secret;
|
|
1332
|
+
config.channels.wecom[idx].enabled = true;
|
|
1333
|
+
}
|
|
1334
|
+
else if (choice && choice.action === 'overwrite' && !Array.isArray(config.channels.wecom)) {
|
|
1335
|
+
config.channels.wecom = config.channels.wecom || {};
|
|
1336
|
+
config.channels.wecom.botId = result.botId;
|
|
1337
|
+
config.channels.wecom.secret = result.secret;
|
|
1338
|
+
config.channels.wecom.enabled = true;
|
|
1339
|
+
}
|
|
1340
|
+
else if (choice && choice.action === 'add') {
|
|
1341
|
+
const newInst = {
|
|
1342
|
+
name: choice.name,
|
|
1343
|
+
botId: result.botId,
|
|
1344
|
+
secret: result.secret,
|
|
1345
|
+
enabled: true,
|
|
1346
|
+
};
|
|
1347
|
+
if (Array.isArray(config.channels.wecom)) {
|
|
1348
|
+
config.channels.wecom.push(newInst);
|
|
1349
|
+
}
|
|
1350
|
+
else if (config.channels.wecom) {
|
|
1351
|
+
const oldInst = { ...config.channels.wecom, name: config.channels.wecom.name || 'wecom' };
|
|
1352
|
+
config.channels.wecom = [oldInst, newInst];
|
|
1353
|
+
}
|
|
1354
|
+
else {
|
|
1355
|
+
config.channels.wecom = [newInst];
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
config.channels.wecom = {
|
|
1360
|
+
botId: result.botId,
|
|
1361
|
+
secret: result.secret,
|
|
1362
|
+
enabled: true,
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (!config.channels.defaultChannel)
|
|
1366
|
+
config.channels.defaultChannel = 'wecom';
|
|
1367
|
+
fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
|
|
1368
|
+
console.log(`\n✅ 企业微信 AI Bot 配置成功!`);
|
|
1369
|
+
console.log(` Bot ID: ${result.botId}`);
|
|
1370
|
+
if (choice) {
|
|
1371
|
+
console.log(` 实例: ${choice.name} (${choice.action === 'add' ? '新增' : '覆盖'})`);
|
|
1372
|
+
}
|
|
1373
|
+
console.log(` 配置已写入: ${p.config}`);
|
|
1374
|
+
console.log(`\n现在可以启动服务: evolclaw restart`);
|
|
1375
|
+
}
|