@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.
- package/README.md +4 -0
- package/lib/adapter-configurers.d.ts +10 -0
- package/lib/adapter-configurers.d.ts.map +1 -1
- package/lib/adapter-configurers.js +436 -71
- package/lib/adapter-configurers.js.map +1 -1
- package/lib/adapter.d.ts +55 -9
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +206 -110
- package/lib/adapter.js.map +1 -1
- package/lib/apply.d.ts +22 -1
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +156 -23
- package/lib/apply.js.map +1 -1
- package/lib/index.d.ts +4 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -3
- package/lib/index.js.map +1 -1
- package/lib/project-config-plan.d.ts +2 -0
- package/lib/project-config-plan.d.ts.map +1 -1
- package/lib/project-config-plan.js +54 -15
- package/lib/project-config-plan.js.map +1 -1
- package/lib/weixin-ilink-login.d.ts +33 -0
- package/lib/weixin-ilink-login.d.ts.map +1 -0
- package/lib/weixin-ilink-login.js +90 -0
- package/lib/weixin-ilink-login.js.map +1 -0
- package/lib/zhin-stack-deps.d.ts +9 -1
- package/lib/zhin-stack-deps.d.ts.map +1 -1
- package/lib/zhin-stack-deps.js +26 -10
- package/lib/zhin-stack-deps.js.map +1 -1
- package/package.json +3 -1
- package/src/adapter-configurers.ts +464 -70
- package/src/adapter.ts +224 -112
- package/src/apply.ts +170 -21
- package/src/index.ts +10 -4
- package/src/project-config-plan.ts +55 -15
- package/src/weixin-ilink-login.ts +151 -0
- package/src/zhin-stack-deps.ts +26 -10
|
@@ -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
|
-
|
|
64
|
-
|
|
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(' •
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
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(' •
|
|
205
|
-
console.log(chalk.gray(' • GitHub App Webhook URL 填 https://<你的域名>/
|
|
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: '/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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: '
|
|
382
|
-
{ name: '
|
|
360
|
+
{ name: 'websocket — WebSocket 网关(默认,无需公网)', value: 'websocket' },
|
|
361
|
+
{ name: 'webhook — HTTP 回调(需公网可达的内置 HTTP Host)', value: 'webhook' },
|
|
383
362
|
],
|
|
384
|
-
default: '
|
|
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
|
-
|
|
440
|
-
|
|
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
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
446
|
+
config.applicationId = interactions.applicationId.trim();
|
|
447
|
+
config.publicKey = interactions.publicKey.trim();
|
|
448
|
+
config.interactionsPath = interactions.interactionsPath.trim();
|
|
471
449
|
}
|
|
472
450
|
|
|
473
|
-
return
|
|
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
|
|
538
|
-
|
|
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: '
|
|
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.
|
|
592
|
+
endpointConfig.http_url = http.http_url.trim();
|
|
593
|
+
endpointConfig.post_path = http.post_path.trim();
|
|
547
594
|
}
|
|
548
595
|
|
|
549
|
-
|
|
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
|
}
|