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.
@@ -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(DEFAULT_BASE_URL, qrResp.qrcode);
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(DEFAULT_BASE_URL, qrResp.qrcode);
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
- // TS SDK (@eleans/aun-core-node) is bundled as npm dependency — no external deps needed
534
- console.log(' ✓ @eleans/aun-core-node (TS SDK)');
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-node');
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, gatewayPort };
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
+ }