@zhin.js/scaffold-wizard 0.1.8 → 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,19 +61,17 @@ export async function configureTelegramEndpoint(ctx: EndpointConfigureContext):
60
61
  },
61
62
  ]);
62
63
 
63
- const endpointConfig: Record<string, unknown> = {
64
- context: 'telegram',
65
- name: endpointName.trim(),
66
- token: '${TELEGRAM_TOKEN}',
64
+ // schema:polling / webhook 为顶层共享字段;name / token 为 endpoint 级
65
+ const config: Record<string, unknown> = {
67
66
  polling: transport === 'polling',
67
+ endpoints: [{ name: endpointName.trim(), token: '${TELEGRAM_TOKEN}' }],
68
68
  };
69
69
 
70
70
  if (transport === 'webhook') {
71
71
  console.log('');
72
72
  console.log(chalk.yellow(' Webhook 前置条件:'));
73
73
  console.log(chalk.gray(' • Endpoint 进程需绑定可被 Telegram 访问的 HTTPS 域名(有效 TLS 证书)'));
74
- console.log(chalk.gray(' • 在 @BotFather 可配合 setWebhook,或由 Telegraf 在 launch 时注册'));
75
- console.log(chalk.gray(' • 防火墙/反向代理需放行 webhook.port(默认由 Telegraf 监听)'));
74
+ console.log(chalk.gray(' • Webhook 由内置 HTTP Host(zhin runtime start)承接,防火墙/反向代理需放行 http 端口'));
76
75
 
77
76
  const webhook = await inquirer.prompt([
78
77
  {
@@ -89,30 +88,18 @@ export async function configureTelegramEndpoint(ctx: EndpointConfigureContext):
89
88
  type: 'input',
90
89
  name: 'path',
91
90
  message: ' Webhook 路径:',
92
- default: '/telegram-webhook',
93
- },
94
- {
95
- type: 'input',
96
- name: 'port',
97
- message: ' 本地监听端口(Telegraf webhook server):',
98
- default: '8443',
99
- validate: (v: string) => {
100
- const p = parseInt(v, 10);
101
- if (Number.isNaN(p) || p < 1 || p > 65535) return '请输入 1–65535 端口';
102
- return true;
103
- },
91
+ default: '/telegram/webhook',
104
92
  },
105
93
  ]);
106
94
 
107
- endpointConfig.polling = false;
108
- endpointConfig.webhook = {
95
+ config.polling = false;
96
+ config.webhook = {
109
97
  domain: webhook.domain.trim(),
110
98
  path: webhook.path.trim(),
111
- port: parseInt(webhook.port, 10),
112
99
  };
113
100
  }
114
101
 
115
- return endpointConfig;
102
+ return config;
116
103
  }
117
104
 
118
105
  export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -148,10 +135,11 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
148
135
  },
149
136
  ]);
150
137
 
138
+ // schema:app_id / private_key / webhook_secret 为 endpoint 级;webhook_path / poll_interval 为顶层共享字段
151
139
  const endpointConfig: Record<string, unknown> = {
152
- context: 'github',
153
140
  name: endpointName.trim(),
154
141
  };
142
+ const sharedConfig: Record<string, unknown> = {};
155
143
 
156
144
  if (authMode === 'github_app') {
157
145
  console.log(chalk.gray(' 在 github.com/settings/apps 创建 App,记录 App ID 并下载 .pem 私钥'));
@@ -186,7 +174,7 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
186
174
  message: ' 事件接收方式:',
187
175
  choices: [
188
176
  {
189
- name: 'Webhook 实时推送(需 @zhin.js/host-router + 公网 URL)',
177
+ name: 'Webhook 实时推送(由内置 HTTP Host 承接,需公网 URL)',
190
178
  value: 'webhook',
191
179
  },
192
180
  {
@@ -201,9 +189,8 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
201
189
  if (eventsMode === 'webhook') {
202
190
  console.log('');
203
191
  console.log(chalk.yellow(' Webhook 前置条件:'));
204
- console.log(chalk.gray(' • plugins 需包含 @zhin.js/host-router(脚手架已默认写入)'));
205
- console.log(chalk.gray(' • GitHub App Webhook URL 填 https://<你的域名>/pub/github/webhook'));
206
- console.log(chalk.gray(' • /pub/* 路径绕过 Bearer 认证,供 GitHub 回调'));
192
+ console.log(chalk.gray(' • Webhook 由内置 HTTP Host(zhin runtime start 的 http 配置)承接'));
193
+ console.log(chalk.gray(' • GitHub App Webhook URL 填 https://<你的域名>/github/webhook'));
207
194
 
208
195
  const webhook = await inquirer.prompt([
209
196
  {
@@ -216,13 +203,13 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
216
203
  type: 'input',
217
204
  name: 'webhook_path',
218
205
  message: ' Webhook 路径:',
219
- default: '/pub/github/webhook',
206
+ default: '/github/webhook',
220
207
  },
221
208
  ]);
222
209
 
223
210
  ctx.envVars.GITHUB_WEBHOOK_SECRET = webhook.webhook_secret;
224
211
  endpointConfig.webhook_secret = '${GITHUB_WEBHOOK_SECRET}';
225
- endpointConfig.webhook_path = webhook.webhook_path.trim();
212
+ sharedConfig.webhook_path = webhook.webhook_path.trim();
226
213
  } else {
227
214
  const { poll_interval } = await inquirer.prompt([
228
215
  {
@@ -237,7 +224,7 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
237
224
  },
238
225
  },
239
226
  ]);
240
- endpointConfig.poll_interval = parseInt(poll_interval, 10);
227
+ sharedConfig.poll_interval = parseInt(poll_interval, 10);
241
228
  }
242
229
 
243
230
  const { enableMcp } = await inquirer.prompt([
@@ -261,7 +248,7 @@ export async function configureGitHubEndpoint(ctx: EndpointConfigureContext): Pr
261
248
  ctx.envVars.GITHUB_PERSONAL_ACCESS_TOKEN = pat;
262
249
  }
263
250
 
264
- return endpointConfig;
251
+ return { ...sharedConfig, endpoints: [endpointConfig] };
265
252
  }
266
253
 
267
254
  export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -274,7 +261,7 @@ export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promi
274
261
  message: ' 连接方式:',
275
262
  choices: [
276
263
  { name: '正向 WebSocket (ws) — 本 Endpoint 连接 OneBot 实现', value: 'ws' },
277
- { name: '反向 WebSocket (wss) — OneBot 实现连本 Bot(需 host-router)', value: 'wss' },
264
+ { name: '反向 WebSocket (wss) — OneBot 实现连本 Bot(由内置 HTTP Host 承接)', value: 'wss' },
278
265
  ],
279
266
  default: 'ws',
280
267
  },
@@ -290,9 +277,8 @@ export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promi
290
277
  },
291
278
  ]);
292
279
 
280
+ // schema:connection 为顶层共享字段;name / url / path / access_token 为 endpoint 级
293
281
  const endpointConfig: Record<string, unknown> = {
294
- context: 'onebot11',
295
- connection,
296
282
  name: endpointName.trim(),
297
283
  };
298
284
 
@@ -308,7 +294,7 @@ export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promi
308
294
  ]);
309
295
  endpointConfig.url = url.trim();
310
296
  } else {
311
- console.log(chalk.gray(' 反向 WS 需启用 @zhin.js/host-router,OneBot 实现配置连接到此 Bot'));
297
+ console.log(chalk.gray(' 反向 WS 由内置 HTTP Host 承接,OneBot 实现配置连接到此 Bot'));
312
298
  const { path: wsPath } = await inquirer.prompt([
313
299
  {
314
300
  type: 'input',
@@ -334,7 +320,7 @@ export async function configureOneBot11Bot(ctx: EndpointConfigureContext): Promi
334
320
  endpointConfig.access_token = '${ONEBOT11_ACCESS_TOKEN}';
335
321
  }
336
322
 
337
- return endpointConfig;
323
+ return { connection, endpoints: [endpointConfig] };
338
324
  }
339
325
 
340
326
  export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -354,12 +340,6 @@ export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Rec
354
340
  message: ' App ID(QQ 开放平台):',
355
341
  validate: (v: string) => (v.trim() ? true : 'App ID 不能为空'),
356
342
  },
357
- {
358
- type: 'password',
359
- name: 'token',
360
- message: ' Token:',
361
- validate: (v: string) => (v.trim() ? true : 'Token 不能为空'),
362
- },
363
343
  {
364
344
  type: 'password',
365
345
  name: 'secret',
@@ -369,7 +349,6 @@ export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Rec
369
349
  ]);
370
350
 
371
351
  ctx.envVars.QQ_APP_ID = credentials.appID.trim();
372
- ctx.envVars.QQ_TOKEN = credentials.token;
373
352
  ctx.envVars.QQ_SECRET = credentials.secret;
374
353
 
375
354
  const { mode, sandbox } = await inquirer.prompt([
@@ -378,10 +357,10 @@ export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Rec
378
357
  name: 'mode',
379
358
  message: ' 消息接收模式 (mode):',
380
359
  choices: [
381
- { name: 'public — 公域消息', value: 'public' },
382
- { name: 'private — 私域消息', value: 'private' },
360
+ { name: 'websocket — WebSocket 网关(默认,无需公网)', value: 'websocket' },
361
+ { name: 'webhook — HTTP 回调(需公网可达的内置 HTTP Host)', value: 'webhook' },
383
362
  ],
384
- default: 'public',
363
+ default: 'websocket',
385
364
  },
386
365
  {
387
366
  type: 'confirm',
@@ -391,15 +370,15 @@ export async function configureQQBot(ctx: EndpointConfigureContext): Promise<Rec
391
370
  },
392
371
  ]);
393
372
 
373
+ // schema:mode / sandbox 为顶层共享字段;name / appid / secret 为 endpoint 级
394
374
  return {
395
- context: 'qq',
396
- name: credentials.name.trim(),
397
- appID: '${QQ_APP_ID}',
398
- token: '${QQ_TOKEN}',
399
- secret: '${QQ_SECRET}',
400
375
  mode,
401
- platform: 'qq',
402
376
  sandbox,
377
+ endpoints: [{
378
+ name: credentials.name.trim(),
379
+ appid: '${QQ_APP_ID}',
380
+ secret: '${QQ_SECRET}',
381
+ }],
403
382
  };
404
383
  }
405
384
 
@@ -436,15 +415,14 @@ export async function configureDiscordEndpoint(ctx: EndpointConfigureContext): P
436
415
  ]);
437
416
  ctx.envVars.DISCORD_TOKEN = token;
438
417
 
439
- const endpointConfig: Record<string, unknown> = {
440
- context: 'discord',
418
+ // schema:connection / applicationId / publicKey / interactionsPath 为顶层共享字段;name / token 为 endpoint 级
419
+ const config: Record<string, unknown> = {
441
420
  connection,
442
- name: endpointName.trim(),
443
- token: '${DISCORD_TOKEN}',
421
+ endpoints: [{ name: endpointName.trim(), token: '${DISCORD_TOKEN}' }],
444
422
  };
445
423
 
446
424
  if (connection === 'interactions') {
447
- console.log(chalk.gray(' Interactions 模式需 @zhin.js/host-router 与 Developer Portal 中的公钥'));
425
+ console.log(chalk.gray(' Interactions 模式由内置 HTTP Host 承接,需 Developer Portal 中的公钥'));
448
426
  const interactions = await inquirer.prompt([
449
427
  {
450
428
  type: 'input',
@@ -465,12 +443,12 @@ export async function configureDiscordEndpoint(ctx: EndpointConfigureContext): P
465
443
  default: '/discord/interactions',
466
444
  },
467
445
  ]);
468
- endpointConfig.applicationId = interactions.applicationId.trim();
469
- endpointConfig.publicKey = interactions.publicKey.trim();
470
- endpointConfig.interactionsPath = interactions.interactionsPath.trim();
446
+ config.applicationId = interactions.applicationId.trim();
447
+ config.publicKey = interactions.publicKey.trim();
448
+ config.interactionsPath = interactions.interactionsPath.trim();
471
449
  }
472
450
 
473
- return endpointConfig;
451
+ return config;
474
452
  }
475
453
 
476
454
  export async function configureSlackEndpoint(ctx: EndpointConfigureContext): Promise<Record<string, unknown>> {
@@ -514,12 +492,11 @@ export async function configureSlackEndpoint(ctx: EndpointConfigureContext): Pro
514
492
  ctx.envVars.SLACK_BOT_TOKEN = token;
515
493
  ctx.envVars.SLACK_SIGNING_SECRET = signingSecret;
516
494
 
495
+ // schema:socketMode 为顶层共享字段;name / token / signingSecret / appToken 为 endpoint 级
517
496
  const endpointConfig: Record<string, unknown> = {
518
- context: 'slack',
519
497
  name: endpointName.trim(),
520
498
  token: '${SLACK_BOT_TOKEN}',
521
499
  signingSecret: '${SLACK_SIGNING_SECRET}',
522
- socketMode: socketMode === 'true',
523
500
  };
524
501
 
525
502
  if (socketMode === 'true') {
@@ -534,17 +511,434 @@ export async function configureSlackEndpoint(ctx: EndpointConfigureContext): Pro
534
511
  ctx.envVars.SLACK_APP_TOKEN = appToken;
535
512
  endpointConfig.appToken = '${SLACK_APP_TOKEN}';
536
513
  } else {
537
- console.log(chalk.yellow(' HTTP 模式需公网可达并配置 Slack Event Subscriptions URL'));
538
- const { port } = await inquirer.prompt([
514
+ console.log(chalk.yellow(' HTTP 模式由内置 HTTP Host 承接,需公网可达并配置 Slack Event Subscriptions URL'));
515
+ }
516
+
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([
539
577
  {
540
578
  type: 'input',
541
- name: 'port',
542
- message: ' 监听端口:',
543
- default: '3000',
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 : '路径不能为空'),
544
590
  },
545
591
  ]);
546
- endpointConfig.port = parseInt(port, 10);
592
+ endpointConfig.http_url = http.http_url.trim();
593
+ endpointConfig.post_path = http.post_path.trim();
547
594
  }
548
595
 
549
- return endpointConfig;
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
+ }
550
944
  }