@zhin.js/scaffold-wizard 0.1.9 → 0.2.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.
@@ -1,5 +1,6 @@
1
1
  import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
+ import { IlinkQrLoginError, loginWithIlinkQr } from './weixin-ilink-login.js';
3
4
 
4
5
  /** 平台适配器文档索引 */
5
6
  export const ADAPTERS_DOCS_URL = 'https://zhin.js.org/adapters/';
@@ -60,10 +61,10 @@ export async function configureTelegramEndpoint(ctx: EndpointConfigureContext):
60
61
  },
61
62
  ]);
62
63
 
63
- const endpointConfig: Record<string, unknown> = {
64
- name: endpointName.trim(),
65
- token: '${TELEGRAM_TOKEN}',
64
+ // schema:polling / webhook 为顶层共享字段;name / token 为 endpoint 级
65
+ const config: Record<string, unknown> = {
66
66
  polling: transport === 'polling',
67
+ endpoints: [{ name: endpointName.trim(), token: '${TELEGRAM_TOKEN}' }],
67
68
  };
68
69
 
69
70
  if (transport === 'webhook') {
@@ -91,14 +92,14 @@ export async function configureTelegramEndpoint(ctx: EndpointConfigureContext):
91
92
  },
92
93
  ]);
93
94
 
94
- endpointConfig.polling = false;
95
- endpointConfig.webhook = {
95
+ config.polling = false;
96
+ config.webhook = {
96
97
  domain: webhook.domain.trim(),
97
98
  path: webhook.path.trim(),
98
99
  };
99
100
  }
100
101
 
101
- return endpointConfig;
102
+ return config;
102
103
  }
103
104
 
104
105
  export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -134,9 +135,11 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
134
135
  },
135
136
  ]);
136
137
 
138
+ // schema:app_id / private_key / webhook_secret 为 endpoint 级;webhook_path / poll_interval 为顶层共享字段
137
139
  const endpointConfig: Record<string, unknown> = {
138
140
  name: endpointName.trim(),
139
141
  };
142
+ const sharedConfig: Record<string, unknown> = {};
140
143
 
141
144
  if (authMode === 'github_app') {
142
145
  console.log(chalk.gray(' 在 github.com/settings/apps 创建 App,记录 App ID 并下载 .pem 私钥'));
@@ -206,7 +209,7 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
206
209
 
207
210
  ctx.envVars.GITHUB_WEBHOOK_SECRET = webhook.webhook_secret;
208
211
  endpointConfig.webhook_secret = '${GITHUB_WEBHOOK_SECRET}';
209
- endpointConfig.webhook_path = webhook.webhook_path.trim();
212
+ sharedConfig.webhook_path = webhook.webhook_path.trim();
210
213
  } else {
211
214
  const { poll_interval } = await inquirer.prompt([
212
215
  {
@@ -221,7 +224,7 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
221
224
  },
222
225
  },
223
226
  ]);
224
- endpointConfig.poll_interval = parseInt(poll_interval, 10);
227
+ sharedConfig.poll_interval = parseInt(poll_interval, 10);
225
228
  }
226
229
 
227
230
  const { enableMcp } = await inquirer.prompt([
@@ -245,7 +248,7 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
245
248
  ctx.envVars.GITHUB_PERSONAL_ACCESS_TOKEN = pat;
246
249
  }
247
250
 
248
- return endpointConfig;
251
+ return { ...sharedConfig, endpoints: [endpointConfig] };
249
252
  }
250
253
 
251
254
  export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -274,8 +277,8 @@ export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promi
274
277
  },
275
278
  ]);
276
279
 
280
+ // schema:connection 为顶层共享字段;name / url / path / access_token 为 endpoint 级
277
281
  const endpointConfig: Record<string, unknown> = {
278
- connection,
279
282
  name: endpointName.trim(),
280
283
  };
281
284
 
@@ -317,7 +320,7 @@ export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promi
317
320
  endpointConfig.access_token = '${ONEBOT11_ACCESS_TOKEN}';
318
321
  }
319
322
 
320
- return endpointConfig;
323
+ return { connection, endpoints: [endpointConfig] };
321
324
  }
322
325
 
323
326
  export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -367,12 +370,15 @@ export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Rec
367
370
  },
368
371
  ]);
369
372
 
373
+ // schema:mode / sandbox 为顶层共享字段;name / appid / secret 为 endpoint 级
370
374
  return {
371
- name: credentials.name.trim(),
372
- appid: '${QQ_APP_ID}',
373
- secret: '${QQ_SECRET}',
374
375
  mode,
375
376
  sandbox,
377
+ endpoints: [{
378
+ name: credentials.name.trim(),
379
+ appid: '${QQ_APP_ID}',
380
+ secret: '${QQ_SECRET}',
381
+ }],
376
382
  };
377
383
  }
378
384
 
@@ -409,10 +415,10 @@ export async function configureDiscordEndpoint(ctx: EndpointConfigureContext): P
409
415
  ]);
410
416
  ctx.envVars.DISCORD_TOKEN = token;
411
417
 
412
- const endpointConfig: Record<string, unknown> = {
418
+ // schema:connection / applicationId / publicKey / interactionsPath 为顶层共享字段;name / token 为 endpoint 级
419
+ const config: Record<string, unknown> = {
413
420
  connection,
414
- name: endpointName.trim(),
415
- token: '${DISCORD_TOKEN}',
421
+ endpoints: [{ name: endpointName.trim(), token: '${DISCORD_TOKEN}' }],
416
422
  };
417
423
 
418
424
  if (connection === 'interactions') {
@@ -437,12 +443,12 @@ export async function configureDiscordEndpoint(ctx: EndpointConfigureContext): P
437
443
  default: '/discord/interactions',
438
444
  },
439
445
  ]);
440
- endpointConfig.applicationId = interactions.applicationId.trim();
441
- endpointConfig.publicKey = interactions.publicKey.trim();
442
- endpointConfig.interactionsPath = interactions.interactionsPath.trim();
446
+ config.applicationId = interactions.applicationId.trim();
447
+ config.publicKey = interactions.publicKey.trim();
448
+ config.interactionsPath = interactions.interactionsPath.trim();
443
449
  }
444
450
 
445
- return endpointConfig;
451
+ return config;
446
452
  }
447
453
 
448
454
  export async function configureSlackEndpoint(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -486,11 +492,11 @@ export async function configureSlackEndpoint(ctx: EndpointConfigureContext): Pro
486
492
  ctx.envVars.SLACK_BOT_TOKEN = token;
487
493
  ctx.envVars.SLACK_SIGNING_SECRET = signingSecret;
488
494
 
495
+ // schema:socketMode 为顶层共享字段;name / token / signingSecret / appToken 为 endpoint 级
489
496
  const endpointConfig: Record<string, unknown> = {
490
497
  name: endpointName.trim(),
491
498
  token: '${SLACK_BOT_TOKEN}',
492
499
  signingSecret: '${SLACK_SIGNING_SECRET}',
493
- socketMode: socketMode === 'true',
494
500
  };
495
501
 
496
502
  if (socketMode === 'true') {
@@ -508,5 +514,431 @@ export async function configureSlackEndpoint(ctx: EndpointConfigureContext): Pro
508
514
  console.log(chalk.yellow(' HTTP 模式由内置 HTTP Host 承接,需公网可达并配置 Slack Event Subscriptions URL'));
509
515
  }
510
516
 
511
- return endpointConfig;
517
+ return { socketMode: socketMode === 'true', endpoints: [endpointConfig] };
518
+ }
519
+
520
+ export async function configureNapcatBot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
521
+ console.log(chalk.gray(' 文档: ' + adapterDocsUrl('napcat')));
522
+
523
+ const { connection } = await inquirer.prompt([
524
+ {
525
+ type: 'select',
526
+ name: 'connection',
527
+ message: ' 连接方式:',
528
+ choices: [
529
+ { name: '正向 WebSocket (ws) — 本 Bot 连接 NapCat', value: 'ws' },
530
+ { name: '反向 WebSocket (wss) — NapCat 连本 Bot(由内置 HTTP Host 承接)', value: 'wss' },
531
+ { name: 'HTTP (http) — POST 上报入站 + HTTP API 出站(由内置 HTTP Host 承接)', value: 'http' },
532
+ ],
533
+ default: 'ws',
534
+ },
535
+ ]);
536
+
537
+ const { endpointName } = await inquirer.prompt([
538
+ {
539
+ type: 'input',
540
+ name: 'endpointName',
541
+ message: ' Endpoint 标识名称:',
542
+ default: 'napcat-bot',
543
+ validate: (v: string) => (v.trim() ? true : '名称不能为空'),
544
+ },
545
+ ]);
546
+
547
+ // schema:connection 为顶层共享字段;name / url / path / http_url / post_path / access_token 为 endpoint 级
548
+ const endpointConfig: Record<string, unknown> = {
549
+ name: endpointName.trim(),
550
+ };
551
+
552
+ if (connection === 'ws') {
553
+ const { url } = await inquirer.prompt([
554
+ {
555
+ type: 'input',
556
+ name: 'url',
557
+ message: ' NapCat WS 地址:',
558
+ default: 'ws://127.0.0.1:3001',
559
+ validate: (v: string) => (v.trim() ? true : '地址不能为空'),
560
+ },
561
+ ]);
562
+ endpointConfig.url = url.trim();
563
+ } else if (connection === 'wss') {
564
+ console.log(chalk.gray(' 反向 WS 由内置 HTTP Host 承接,NapCat 配置连接到此 Bot'));
565
+ const { path: wsPath } = await inquirer.prompt([
566
+ {
567
+ type: 'input',
568
+ name: 'path',
569
+ message: ' 反向 WS 路径:',
570
+ default: '/napcat/ws',
571
+ validate: (v: string) => (v.trim() ? true : '路径不能为空'),
572
+ },
573
+ ]);
574
+ endpointConfig.path = wsPath.trim();
575
+ } else {
576
+ const http = await inquirer.prompt([
577
+ {
578
+ type: 'input',
579
+ name: 'http_url',
580
+ message: ' NapCat HTTP API 地址(出站):',
581
+ default: 'http://127.0.0.1:3000',
582
+ validate: (v: string) => (v.trim() ? true : '地址不能为空'),
583
+ },
584
+ {
585
+ type: 'input',
586
+ name: 'post_path',
587
+ message: ' HTTP POST 上报路径(入站):',
588
+ default: '/napcat/post',
589
+ validate: (v: string) => (v.trim() ? true : '路径不能为空'),
590
+ },
591
+ ]);
592
+ endpointConfig.http_url = http.http_url.trim();
593
+ endpointConfig.post_path = http.post_path.trim();
594
+ }
595
+
596
+ const { accessToken } = await inquirer.prompt([
597
+ {
598
+ type: 'password',
599
+ name: 'accessToken',
600
+ message: ' access_token(无则留空):',
601
+ default: '',
602
+ },
603
+ ]);
604
+ if (accessToken?.trim()) {
605
+ ctx.envVars.NAPCAT_TOKEN = accessToken.trim();
606
+ endpointConfig.access_token = '${NAPCAT_TOKEN}';
607
+ }
608
+
609
+ return { connection, endpoints: [endpointConfig] };
610
+ }
611
+
612
+ export async function configureOneBot12Bot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
613
+ console.log(chalk.gray(' 文档: ' + adapterDocsUrl('onebot12')));
614
+
615
+ const { connection } = await inquirer.prompt([
616
+ {
617
+ type: 'select',
618
+ name: 'connection',
619
+ message: ' 连接方式:',
620
+ choices: [
621
+ { name: '正向 WebSocket (ws) — 本 Bot 连接 OneBot 实现', value: 'ws' },
622
+ { name: 'HTTP Webhook (webhook) — POST 入站 + api_url HTTP 出站(由内置 HTTP Host 承接)', value: 'webhook' },
623
+ { name: '反向 WebSocket (wss) — OneBot 实现连本 Bot(由内置 HTTP Host 承接)', value: 'wss' },
624
+ ],
625
+ default: 'ws',
626
+ },
627
+ ]);
628
+
629
+ const { endpointName } = await inquirer.prompt([
630
+ {
631
+ type: 'input',
632
+ name: 'endpointName',
633
+ message: ' Endpoint 标识名称:',
634
+ default: 'onebot12-bot',
635
+ validate: (v: string) => (v.trim() ? true : '名称不能为空'),
636
+ },
637
+ ]);
638
+
639
+ // schema:connection 为顶层共享字段;name / url / path / api_url / access_token 为 endpoint 级
640
+ const endpointConfig: Record<string, unknown> = {
641
+ name: endpointName.trim(),
642
+ };
643
+
644
+ if (connection === 'ws') {
645
+ const { url } = await inquirer.prompt([
646
+ {
647
+ type: 'input',
648
+ name: 'url',
649
+ message: ' OneBot WS 地址:',
650
+ default: 'ws://127.0.0.1:6700',
651
+ validate: (v: string) => (v.trim() ? true : '地址不能为空'),
652
+ },
653
+ ]);
654
+ endpointConfig.url = url.trim();
655
+ } else if (connection === 'webhook') {
656
+ const webhook = await inquirer.prompt([
657
+ {
658
+ type: 'input',
659
+ name: 'path',
660
+ message: ' Webhook POST 路径(入站):',
661
+ default: '/onebot12/webhook',
662
+ validate: (v: string) => (v.trim() ? true : '路径不能为空'),
663
+ },
664
+ {
665
+ type: 'input',
666
+ name: 'api_url',
667
+ message: ' HTTP action 地址(出站):',
668
+ default: 'http://127.0.0.1:6700',
669
+ validate: (v: string) => (v.trim() ? true : '地址不能为空'),
670
+ },
671
+ ]);
672
+ endpointConfig.path = webhook.path.trim();
673
+ endpointConfig.api_url = webhook.api_url.trim();
674
+ } else {
675
+ console.log(chalk.gray(' 反向 WS 由内置 HTTP Host 承接,OneBot 实现配置连接到此 Bot'));
676
+ const { path: wsPath } = await inquirer.prompt([
677
+ {
678
+ type: 'input',
679
+ name: 'path',
680
+ message: ' 反向 WS 路径:',
681
+ default: '/onebot12/ws',
682
+ validate: (v: string) => (v.trim() ? true : '路径不能为空'),
683
+ },
684
+ ]);
685
+ endpointConfig.path = wsPath.trim();
686
+ }
687
+
688
+ const { accessToken } = await inquirer.prompt([
689
+ {
690
+ type: 'password',
691
+ name: 'accessToken',
692
+ message: ' access_token(无则留空):',
693
+ default: '',
694
+ },
695
+ ]);
696
+ if (accessToken?.trim()) {
697
+ ctx.envVars.ONEBOT12_ACCESS_TOKEN = accessToken.trim();
698
+ endpointConfig.access_token = '${ONEBOT12_ACCESS_TOKEN}';
699
+ }
700
+
701
+ return { connection, endpoints: [endpointConfig] };
702
+ }
703
+
704
+ export async function configureMilkyBot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
705
+ console.log(chalk.gray(' 文档: ' + adapterDocsUrl('milky')));
706
+
707
+ const { connection } = await inquirer.prompt([
708
+ {
709
+ type: 'select',
710
+ name: 'connection',
711
+ message: ' 连接方式:',
712
+ choices: [
713
+ { name: '正向 WebSocket (ws) — 本 Bot 连协议端 ws(s)://baseUrl/event', value: 'ws' },
714
+ { name: 'SSE (sse) — HTTP GET baseUrl/event(text/event-stream)', value: 'sse' },
715
+ { name: 'Webhook (webhook) — POST 入站 + baseUrl HTTP API 出站(由内置 HTTP Host 承接)', value: 'webhook' },
716
+ { name: '反向 WebSocket (wss) — 由内置 HTTP Host 承接', value: 'wss' },
717
+ ],
718
+ default: 'ws',
719
+ },
720
+ ]);
721
+
722
+ const { endpointName, baseUrl } = await inquirer.prompt([
723
+ {
724
+ type: 'input',
725
+ name: 'endpointName',
726
+ message: ' Endpoint 标识名称:',
727
+ default: 'milky-bot',
728
+ validate: (v: string) => (v.trim() ? true : '名称不能为空'),
729
+ },
730
+ {
731
+ type: 'input',
732
+ name: 'baseUrl',
733
+ message: ' Milky HTTP API baseUrl:',
734
+ default: 'http://127.0.0.1:8080',
735
+ validate: (v: string) => (v.trim() ? true : 'baseUrl 不能为空'),
736
+ },
737
+ ]);
738
+
739
+ // schema:connection 为顶层共享字段;name / baseUrl / path / access_token 为 endpoint 级
740
+ const endpointConfig: Record<string, unknown> = {
741
+ name: endpointName.trim(),
742
+ baseUrl: baseUrl.trim(),
743
+ };
744
+
745
+ if (connection === 'webhook' || connection === 'wss') {
746
+ const { path: hookPath } = await inquirer.prompt([
747
+ {
748
+ type: 'input',
749
+ name: 'path',
750
+ message: ` ${connection === 'webhook' ? 'Webhook POST' : '反向 WS'} 路径:`,
751
+ default: connection === 'webhook' ? '/milky/webhook' : '/milky/ws',
752
+ validate: (v: string) => (v.trim() ? true : '路径不能为空'),
753
+ },
754
+ ]);
755
+ endpointConfig.path = hookPath.trim();
756
+ }
757
+
758
+ const { accessToken } = await inquirer.prompt([
759
+ {
760
+ type: 'password',
761
+ name: 'accessToken',
762
+ message: ' access_token(无则留空):',
763
+ default: '',
764
+ },
765
+ ]);
766
+ if (accessToken?.trim()) {
767
+ ctx.envVars.MILKY_ACCESS_TOKEN = accessToken.trim();
768
+ endpointConfig.access_token = '${MILKY_ACCESS_TOKEN}';
769
+ }
770
+
771
+ return { connection, endpoints: [endpointConfig] };
772
+ }
773
+
774
+ export async function configureSatoriBot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
775
+ console.log(chalk.gray(' 文档: ' + adapterDocsUrl('satori')));
776
+
777
+ const { connection } = await inquirer.prompt([
778
+ {
779
+ type: 'select',
780
+ name: 'connection',
781
+ message: ' 连接方式:',
782
+ choices: [
783
+ { name: '正向 WebSocket (ws) — 本 Bot 连 Satori SDK', value: 'ws' },
784
+ { name: 'Webhook (webhook) — SDK POST 事件到 path(由内置 HTTP Host 承接)', value: 'webhook' },
785
+ ],
786
+ default: 'ws',
787
+ },
788
+ ]);
789
+
790
+ const { endpointName, baseUrl } = await inquirer.prompt([
791
+ {
792
+ type: 'input',
793
+ name: 'endpointName',
794
+ message: ' Endpoint 标识名称:',
795
+ default: 'satori-bot',
796
+ validate: (v: string) => (v.trim() ? true : '名称不能为空'),
797
+ },
798
+ {
799
+ type: 'input',
800
+ name: 'baseUrl',
801
+ message: ' Satori SDK baseUrl:',
802
+ default: 'http://127.0.0.1:5140',
803
+ validate: (v: string) => (v.trim() ? true : 'baseUrl 不能为空'),
804
+ },
805
+ ]);
806
+
807
+ // schema:connection 为顶层共享字段;name / baseUrl / token / path 为 endpoint 级
808
+ const endpointConfig: Record<string, unknown> = {
809
+ name: endpointName.trim(),
810
+ baseUrl: baseUrl.trim(),
811
+ };
812
+
813
+ if (connection === 'webhook') {
814
+ const { path: hookPath } = await inquirer.prompt([
815
+ {
816
+ type: 'input',
817
+ name: 'path',
818
+ message: ' Webhook POST 路径:',
819
+ default: '/satori/webhook',
820
+ validate: (v: string) => (v.trim() ? true : '路径不能为空'),
821
+ },
822
+ ]);
823
+ endpointConfig.path = hookPath.trim();
824
+ }
825
+
826
+ const { token } = await inquirer.prompt([
827
+ {
828
+ type: 'password',
829
+ name: 'token',
830
+ message: ' Bearer Token(无则留空):',
831
+ default: '',
832
+ },
833
+ ]);
834
+ if (token?.trim()) {
835
+ ctx.envVars.SATORI_TOKEN = token.trim();
836
+ endpointConfig.token = '${SATORI_TOKEN}';
837
+ }
838
+
839
+ return { connection, endpoints: [endpointConfig] };
840
+ }
841
+
842
+ /**
843
+ * 微信 iLink(个人微信 / ClawBot)配置:默认走扫码绑定 —— 终端展示二维码,
844
+ * 微信扫码确认后 bot_token 写入 .env(`WEIXIN_ILINK_TOKEN`),
845
+ * zhin.config.yml 只留 `${WEIXIN_ILINK_TOKEN}` 引用;扫码失败可降级手动输入 token。
846
+ */
847
+ export async function configureWeixinIlinkBot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
848
+ console.log(chalk.gray(' 文档: ' + adapterDocsUrl('weixin-ilink')));
849
+ console.log(chalk.gray(' 需最新版微信 + ClawBot 灰度资格(仅私聊)'));
850
+
851
+ const { endpointName } = await inquirer.prompt([
852
+ {
853
+ type: 'input',
854
+ name: 'endpointName',
855
+ message: ' Endpoint 标识名称:',
856
+ default: 'weixin-bot',
857
+ validate: (v: string) => (v.trim() ? true : '名称不能为空'),
858
+ },
859
+ ]);
860
+
861
+ const { method } = await inquirer.prompt([
862
+ {
863
+ type: 'select',
864
+ name: 'method',
865
+ message: ' 登录方式:',
866
+ choices: [
867
+ { name: '扫码绑定(微信 ClawBot 入口扫码,自动获取 bot_token)', value: 'qr' },
868
+ { name: '手动输入 bot_token', value: 'manual' },
869
+ ],
870
+ default: 'qr',
871
+ },
872
+ ]);
873
+
874
+ if (method === 'qr') {
875
+ const scanned = await tryQrLogin(ctx);
876
+ if (scanned) {
877
+ return {
878
+ endpoints: [{ name: endpointName.trim(), botToken: '${WEIXIN_ILINK_TOKEN}' }],
879
+ };
880
+ }
881
+ console.log(chalk.yellow(' 扫码未完成,降级为手动输入 bot_token'));
882
+ }
883
+
884
+ const { token } = await inquirer.prompt([
885
+ {
886
+ type: 'password',
887
+ name: 'token',
888
+ message: ' iLink Bot Token:',
889
+ validate: (v: string) => (v.trim() ? true : 'Token 不能为空'),
890
+ },
891
+ ]);
892
+ ctx.envVars.WEIXIN_ILINK_TOKEN = token.trim();
893
+ return {
894
+ endpoints: [{ name: endpointName.trim(), botToken: '${WEIXIN_ILINK_TOKEN}' }],
895
+ };
896
+ }
897
+
898
+ /** 扫码尝试:成功写入 envVars 并返回 true;用户放弃/过期/超时返回 false。 */
899
+ async function tryQrLogin(ctx: EndpointConfigureContext): Promise<boolean> {
900
+ // 动态加载 qrcode:扫码路径才需要,失败则降级为纯链接展示
901
+ let renderQr: ((text: string) => Promise<string>) | undefined;
902
+ try {
903
+ const qrcode = await import('qrcode');
904
+ renderQr = (text) => qrcode.toString(text, { type: 'terminal', small: true });
905
+ } catch {
906
+ renderQr = undefined;
907
+ }
908
+
909
+ for (;;) {
910
+ try {
911
+ const creds = await loginWithIlinkQr({
912
+ onQrCode: async (url) => {
913
+ console.log('');
914
+ if (renderQr) {
915
+ console.log(await renderQr(url));
916
+ }
917
+ console.log(chalk.cyan(' 请用微信扫描上方二维码(或打开链接完成绑定):'));
918
+ console.log(chalk.cyan(` ${url}`));
919
+ console.log('');
920
+ },
921
+ onStatus: (status) => {
922
+ if (status === 'scaned') {
923
+ console.log(chalk.green(' 已扫码,请在微信中确认…'));
924
+ }
925
+ },
926
+ });
927
+ ctx.envVars.WEIXIN_ILINK_TOKEN = creds.botToken;
928
+ console.log(chalk.green(' 绑定成功,bot_token 已写入 .env(WEIXIN_ILINK_TOKEN)'));
929
+ return true;
930
+ } catch (error) {
931
+ const message = error instanceof Error ? error.message : String(error);
932
+ console.log(chalk.yellow(` 扫码登录失败:${message}`));
933
+ const { retry } = await inquirer.prompt([
934
+ {
935
+ type: 'confirm',
936
+ name: 'retry',
937
+ message: ' 重新获取二维码再试一次?(选否则手动输入 token)',
938
+ default: error instanceof IlinkQrLoginError && error.reason === 'expired',
939
+ },
940
+ ]);
941
+ if (!retry) return false;
942
+ }
943
+ }
512
944
  }