@zhin.js/scaffold-wizard 0.1.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/LICENSE +21 -0
- package/README.md +67 -0
- package/lib/adapter-configurers.d.ts +19 -0
- package/lib/adapter-configurers.d.ts.map +1 -0
- package/lib/adapter-configurers.js +502 -0
- package/lib/adapter-configurers.js.map +1 -0
- package/lib/adapter.d.ts +37 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +458 -0
- package/lib/adapter.js.map +1 -0
- package/lib/ai.d.ts +88 -0
- package/lib/ai.d.ts.map +1 -0
- package/lib/ai.js +440 -0
- package/lib/ai.js.map +1 -0
- package/lib/apply.d.ts +14 -0
- package/lib/apply.d.ts.map +1 -0
- package/lib/apply.js +86 -0
- package/lib/apply.js.map +1 -0
- package/lib/database.d.ts +3 -0
- package/lib/database.d.ts.map +1 -0
- package/lib/database.js +260 -0
- package/lib/database.js.map +1 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -0
- package/lib/project-deps.d.ts +18 -0
- package/lib/project-deps.d.ts.map +1 -0
- package/lib/project-deps.js +49 -0
- package/lib/project-deps.js.map +1 -0
- package/lib/types.d.ts +22 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/package.json +48 -0
- package/src/adapter-configurers.ts +550 -0
- package/src/adapter.ts +524 -0
- package/src/ai.ts +497 -0
- package/src/apply.ts +99 -0
- package/src/database.ts +267 -0
- package/src/index.ts +51 -0
- package/src/project-deps.ts +49 -0
- package/src/types.ts +32 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import {
|
|
4
|
+
ADAPTERS_DOCS_URL,
|
|
5
|
+
adapterDocsUrl,
|
|
6
|
+
configureDiscordBot,
|
|
7
|
+
configureGitHubBot,
|
|
8
|
+
configureOneBot11Bot,
|
|
9
|
+
configureQQBot,
|
|
10
|
+
configureSlackBot,
|
|
11
|
+
configureTelegramBot,
|
|
12
|
+
} from './adapter-configurers.js';
|
|
13
|
+
|
|
14
|
+
export interface AdapterSetupResult {
|
|
15
|
+
packages: string[];
|
|
16
|
+
plugins: string[];
|
|
17
|
+
bots: Array<Record<string, any>>;
|
|
18
|
+
envVars: Record<string, string>;
|
|
19
|
+
/** 所选适配器需要 database 插件(如 GitHub 订阅/OAuth 表) */
|
|
20
|
+
requiresDatabase?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 适配器定义
|
|
24
|
+
interface AdapterDefinition {
|
|
25
|
+
name: string;
|
|
26
|
+
value: string;
|
|
27
|
+
package: string;
|
|
28
|
+
plugin: string;
|
|
29
|
+
extraDeps?: Record<string, string>;
|
|
30
|
+
needsHttp: boolean;
|
|
31
|
+
description?: string;
|
|
32
|
+
setupHint?: string;
|
|
33
|
+
/** 文档:zhin.js.org/adapters/<slug>;索引见 ADAPTERS_DOCS_URL */
|
|
34
|
+
docUrl?: string;
|
|
35
|
+
requiresDatabase?: boolean;
|
|
36
|
+
configure?: (ctx: import('./adapter-configurers.js').BotConfigureContext) => Promise<Record<string, unknown>>;
|
|
37
|
+
fields: AdapterField[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface AdapterField {
|
|
41
|
+
key: string;
|
|
42
|
+
message: string;
|
|
43
|
+
type?: 'input' | 'password' | 'select';
|
|
44
|
+
default?: string;
|
|
45
|
+
required?: boolean;
|
|
46
|
+
envKey?: string; // 如果是敏感信息,对应的环境变量名
|
|
47
|
+
choices?: { name: string; value: string }[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ADAPTERS: AdapterDefinition[] = [
|
|
51
|
+
{
|
|
52
|
+
name: 'Sandbox (调试沙盒,默认)',
|
|
53
|
+
value: 'sandbox',
|
|
54
|
+
package: '@zhin.js/adapter-sandbox',
|
|
55
|
+
plugin: '@zhin.js/adapter-sandbox',
|
|
56
|
+
needsHttp: true,
|
|
57
|
+
description: '终端 + Remote Console 沙盒页,本地开发首选',
|
|
58
|
+
setupHint: '无需额外凭据;pnpm dev 后可在终端或 Console 沙盒页直接对话。',
|
|
59
|
+
fields: [],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'ICQQ (QQ)',
|
|
63
|
+
value: 'icqq',
|
|
64
|
+
package: '@zhin.js/adapter-icqq',
|
|
65
|
+
plugin: '@zhin.js/adapter-icqq',
|
|
66
|
+
needsHttp: false,
|
|
67
|
+
description: '非官方 QQ 协议,需本地登录',
|
|
68
|
+
setupHint: '先安装 icqq CLI 并登录:pnpm dlx icqq login <QQ号>,登录态保存在 ~/.icqq/。',
|
|
69
|
+
docUrl: adapterDocsUrl('icqq'),
|
|
70
|
+
fields: [
|
|
71
|
+
{ key: 'name', message: 'QQ 号(与 icqq login 时一致):', required: true, envKey: 'ICQQ_ACCOUNT' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'QQ 官方',
|
|
76
|
+
value: 'qq',
|
|
77
|
+
package: '@zhin.js/adapter-qq',
|
|
78
|
+
plugin: '@zhin.js/adapter-qq',
|
|
79
|
+
needsHttp: false,
|
|
80
|
+
description: 'QQ 开放平台官方 Bot',
|
|
81
|
+
setupHint: '在 q.qq.com/bot 创建机器人,获取 AppID / Token / Secret;mode 选 public 或 private。',
|
|
82
|
+
docUrl: adapterDocsUrl('qq'),
|
|
83
|
+
configure: configureQQBot,
|
|
84
|
+
fields: [],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'KOOK',
|
|
88
|
+
value: 'kook',
|
|
89
|
+
package: '@zhin.js/adapter-kook',
|
|
90
|
+
plugin: '@zhin.js/adapter-kook',
|
|
91
|
+
needsHttp: true,
|
|
92
|
+
description: 'KOOK 语音平台 Bot',
|
|
93
|
+
setupHint: '在 KOOK 开发者中心创建 Bot 并复制 Token;需配置 Webhook 回调地址(公网或内网穿透)。',
|
|
94
|
+
fields: [
|
|
95
|
+
{ key: 'token', message: 'KOOK Bot Token:', required: true, type: 'password', envKey: 'KOOK_TOKEN' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'Discord',
|
|
100
|
+
value: 'discord',
|
|
101
|
+
package: '@zhin.js/adapter-discord',
|
|
102
|
+
plugin: '@zhin.js/adapter-discord',
|
|
103
|
+
needsHttp: true,
|
|
104
|
+
description: 'Discord(Gateway 或 Interactions)',
|
|
105
|
+
setupHint: 'Developer Portal 创建 Bot;Gateway 用 Token,Interactions 需 Application ID + Public Key。',
|
|
106
|
+
docUrl: adapterDocsUrl('discord'),
|
|
107
|
+
configure: configureDiscordBot,
|
|
108
|
+
fields: [],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'Telegram',
|
|
112
|
+
value: 'telegram',
|
|
113
|
+
package: '@zhin.js/adapter-telegram',
|
|
114
|
+
plugin: '@zhin.js/adapter-telegram',
|
|
115
|
+
needsHttp: false,
|
|
116
|
+
description: 'Telegram Bot(polling / webhook 可选)',
|
|
117
|
+
setupHint: '@BotFather 获取 Token;polling 适合本地,webhook 需 HTTPS 公网域名。',
|
|
118
|
+
docUrl: adapterDocsUrl('telegram'),
|
|
119
|
+
configure: configureTelegramBot,
|
|
120
|
+
fields: [],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'Slack',
|
|
124
|
+
value: 'slack',
|
|
125
|
+
package: '@zhin.js/adapter-slack',
|
|
126
|
+
plugin: '@zhin.js/adapter-slack',
|
|
127
|
+
needsHttp: false,
|
|
128
|
+
description: 'Slack(Socket Mode 或 HTTP Events)',
|
|
129
|
+
setupHint: 'Socket Mode 无需公网;HTTP 模式需配置 Event Subscriptions URL。',
|
|
130
|
+
docUrl: adapterDocsUrl('slack'),
|
|
131
|
+
configure: configureSlackBot,
|
|
132
|
+
fields: [],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: '钉钉',
|
|
136
|
+
value: 'dingtalk',
|
|
137
|
+
package: '@zhin.js/adapter-dingtalk',
|
|
138
|
+
plugin: '@zhin.js/adapter-dingtalk',
|
|
139
|
+
needsHttp: true,
|
|
140
|
+
description: '钉钉企业内部应用 / 机器人',
|
|
141
|
+
setupHint: '在钉钉开放平台创建应用,配置机器人并设置 HTTP 回调 URL(需公网可达)。',
|
|
142
|
+
fields: [
|
|
143
|
+
{ key: 'appKey', message: 'App Key:', required: true, envKey: 'DINGTALK_APP_KEY' },
|
|
144
|
+
{ key: 'appSecret', message: 'App Secret:', required: true, type: 'password', envKey: 'DINGTALK_APP_SECRET' },
|
|
145
|
+
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/dingtalk/webhook' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: '飞书',
|
|
150
|
+
value: 'lark',
|
|
151
|
+
package: '@zhin.js/adapter-lark',
|
|
152
|
+
plugin: '@zhin.js/adapter-lark',
|
|
153
|
+
needsHttp: true,
|
|
154
|
+
description: '飞书 / Lark 企业应用',
|
|
155
|
+
setupHint: '在飞书开放平台创建企业自建应用,启用机器人并配置事件订阅 URL(需公网可达)。',
|
|
156
|
+
fields: [
|
|
157
|
+
{ key: 'appId', message: 'App ID:', required: true, envKey: 'LARK_APP_ID' },
|
|
158
|
+
{ key: 'appSecret', message: 'App Secret:', required: true, type: 'password', envKey: 'LARK_APP_SECRET' },
|
|
159
|
+
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/lark/webhook' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'OneBot v11',
|
|
164
|
+
value: 'onebot11',
|
|
165
|
+
package: '@zhin.js/adapter-onebot11',
|
|
166
|
+
plugin: '@zhin.js/adapter-onebot11',
|
|
167
|
+
needsHttp: false,
|
|
168
|
+
description: 'OneBot v11(ws 正向 / wss 反向)',
|
|
169
|
+
setupHint: 'connection: ws 连 OneBot 实现;connection: wss 反向连接(需 host-router)。',
|
|
170
|
+
docUrl: adapterDocsUrl('onebot11'),
|
|
171
|
+
configure: configureOneBot11Bot,
|
|
172
|
+
fields: [],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: '微信公众号',
|
|
176
|
+
value: 'wechat-mp',
|
|
177
|
+
package: '@zhin.js/adapter-wechat-mp',
|
|
178
|
+
plugin: '@zhin.js/adapter-wechat-mp',
|
|
179
|
+
needsHttp: true,
|
|
180
|
+
description: '微信公众平台(服务号/订阅号)',
|
|
181
|
+
setupHint: '在微信公众平台配置服务器 URL 与 Token;需备案域名且公网 HTTPS 可达。',
|
|
182
|
+
fields: [
|
|
183
|
+
{ key: 'appId', message: 'App ID:', required: true, envKey: 'WECHAT_APP_ID' },
|
|
184
|
+
{ key: 'appSecret', message: 'App Secret:', required: true, type: 'password', envKey: 'WECHAT_APP_SECRET' },
|
|
185
|
+
{ key: 'token', message: '验证 Token:', required: true, envKey: 'WECHAT_TOKEN' },
|
|
186
|
+
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/wechat/webhook' },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'Email',
|
|
191
|
+
value: 'email',
|
|
192
|
+
package: '@zhin.js/adapter-email',
|
|
193
|
+
plugin: '@zhin.js/adapter-email',
|
|
194
|
+
needsHttp: false,
|
|
195
|
+
description: 'SMTP 发信 + IMAP 收信',
|
|
196
|
+
setupHint: '使用邮箱 SMTP/IMAP 凭据;QQ/163 等需开启授权码而非登录密码。',
|
|
197
|
+
fields: [
|
|
198
|
+
{ key: 'smtpHost', message: 'SMTP 服务器:', required: true, default: 'smtp.qq.com' },
|
|
199
|
+
{ key: 'smtpPort', message: 'SMTP 端口:', default: '465' },
|
|
200
|
+
{ key: 'imapHost', message: 'IMAP 服务器:', required: true, default: 'imap.qq.com' },
|
|
201
|
+
{ key: 'imapPort', message: 'IMAP 端口:', default: '993' },
|
|
202
|
+
{ key: 'user', message: '邮箱地址:', required: true, envKey: 'EMAIL_USER' },
|
|
203
|
+
{ key: 'password', message: '邮箱密码/授权码:', required: true, type: 'password', envKey: 'EMAIL_PASSWORD' },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'GitHub',
|
|
208
|
+
value: 'github',
|
|
209
|
+
package: '@zhin.js/adapter-github',
|
|
210
|
+
plugin: '@zhin.js/adapter-github',
|
|
211
|
+
needsHttp: true,
|
|
212
|
+
description: 'GitHub Issue/PR 聊天 + App 认证',
|
|
213
|
+
setupHint: 'GitHub App 或 gh CLI;Webhook 用 /pub/github/webhook;需 database 存订阅与 OAuth。',
|
|
214
|
+
docUrl: adapterDocsUrl('github'),
|
|
215
|
+
requiresDatabase: true,
|
|
216
|
+
configure: configureGitHubBot,
|
|
217
|
+
fields: [],
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 适配器选择与 Bot 配置引导向导
|
|
223
|
+
*/
|
|
224
|
+
export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log(chalk.blue('🔌 配置聊天适配器'));
|
|
227
|
+
console.log(chalk.gray(' Sandbox 适合本地调试;生产环境请额外勾选目标 IM 平台。'));
|
|
228
|
+
console.log(chalk.gray(' 带 Webhook 的适配器需要 Bot 可被公网访问(或使用内网穿透)。'));
|
|
229
|
+
|
|
230
|
+
const { selectedAdapters } = await inquirer.prompt([
|
|
231
|
+
{
|
|
232
|
+
type: 'checkbox',
|
|
233
|
+
name: 'selectedAdapters',
|
|
234
|
+
message: '选择聊天平台适配器(空格选择,回车确认):',
|
|
235
|
+
choices: ADAPTERS.map(a => ({
|
|
236
|
+
name: a.description ? `${a.name} — ${a.description}` : a.name,
|
|
237
|
+
value: a.value,
|
|
238
|
+
checked: a.value === 'sandbox',
|
|
239
|
+
disabled: a.value === 'sandbox' ? '默认必选' : false,
|
|
240
|
+
})),
|
|
241
|
+
validate: (input: string[]) => {
|
|
242
|
+
if (input.length === 0) return '至少选择一个适配器';
|
|
243
|
+
return true;
|
|
244
|
+
},
|
|
245
|
+
pageSize: 14,
|
|
246
|
+
}
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
const adapterValues: string[] = selectedAdapters.includes('sandbox')
|
|
250
|
+
? selectedAdapters
|
|
251
|
+
: ['sandbox', ...selectedAdapters];
|
|
252
|
+
|
|
253
|
+
const httpAdapters = adapterValues
|
|
254
|
+
.map(v => ADAPTERS.find(a => a.value === v))
|
|
255
|
+
.filter((a): a is AdapterDefinition => !!a && a.needsHttp && a.value !== 'sandbox');
|
|
256
|
+
|
|
257
|
+
if (httpAdapters.length > 0) {
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(chalk.yellow(' ⚠ 以下适配器通常需要公网 HTTP/Webhook:'));
|
|
260
|
+
for (const adapter of httpAdapters) {
|
|
261
|
+
console.log(chalk.gray(` • ${adapter.name.split(' (')[0]}`));
|
|
262
|
+
}
|
|
263
|
+
console.log(chalk.gray(' 开发时可先用 Sandbox;上线前请确保 http 端口(默认 8086)可从平台回调。'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result: AdapterSetupResult = {
|
|
267
|
+
packages: [],
|
|
268
|
+
plugins: [],
|
|
269
|
+
bots: [],
|
|
270
|
+
envVars: {},
|
|
271
|
+
requiresDatabase: false,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
for (const adapterValue of adapterValues) {
|
|
275
|
+
const adapterDef = ADAPTERS.find(a => a.value === adapterValue);
|
|
276
|
+
if (!adapterDef) continue;
|
|
277
|
+
|
|
278
|
+
result.packages.push(adapterDef.package);
|
|
279
|
+
if (adapterDef.extraDeps) {
|
|
280
|
+
for (const [pkg, ver] of Object.entries(adapterDef.extraDeps)) {
|
|
281
|
+
result.packages.push(`${pkg}@${ver}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
result.plugins.push(adapterDef.plugin);
|
|
285
|
+
if (adapterDef.requiresDatabase) {
|
|
286
|
+
result.requiresDatabase = true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (adapterValue === 'sandbox') continue;
|
|
290
|
+
|
|
291
|
+
const hasConfig = adapterDef.configure || adapterDef.fields.length > 0;
|
|
292
|
+
if (!hasConfig) continue;
|
|
293
|
+
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log(chalk.yellow(` 📝 配置 ${adapterDef.name.split(' (')[0]} Bot`));
|
|
296
|
+
if (adapterDef.setupHint) {
|
|
297
|
+
console.log(chalk.gray(` ${adapterDef.setupHint}`));
|
|
298
|
+
}
|
|
299
|
+
if (adapterDef.docUrl && !adapterDef.configure) {
|
|
300
|
+
console.log(chalk.gray(` 文档: ${adapterDef.docUrl}`));
|
|
301
|
+
}
|
|
302
|
+
console.log(chalk.gray(` 索引: ${ADAPTERS_DOCS_URL}`));
|
|
303
|
+
|
|
304
|
+
if (adapterDef.configure) {
|
|
305
|
+
let markedDb = false;
|
|
306
|
+
const botConfig = await adapterDef.configure({
|
|
307
|
+
envVars: result.envVars,
|
|
308
|
+
markRequiresDatabase: () => {
|
|
309
|
+
markedDb = true;
|
|
310
|
+
result.requiresDatabase = true;
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
if (markedDb) result.requiresDatabase = true;
|
|
314
|
+
result.bots.push(botConfig);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const botConfig: Record<string, any> = {
|
|
319
|
+
context: adapterDef.value,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
for (const field of adapterDef.fields) {
|
|
323
|
+
const promptConfig: any = {
|
|
324
|
+
type: field.type || 'input',
|
|
325
|
+
name: 'value',
|
|
326
|
+
message: ` ${field.message}`,
|
|
327
|
+
default: field.default,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (field.type === 'select' && field.choices) {
|
|
331
|
+
promptConfig.choices = field.choices;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (field.required) {
|
|
335
|
+
promptConfig.validate = (input: string) => {
|
|
336
|
+
if (!input || !input.trim()) return `${field.message.replace(':', '')} 不能为空`;
|
|
337
|
+
return true;
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const { value } = await inquirer.prompt([promptConfig]);
|
|
342
|
+
|
|
343
|
+
if (field.envKey) {
|
|
344
|
+
// 敏感信息存 .env,配置引用环境变量
|
|
345
|
+
result.envVars[field.envKey] = value || '';
|
|
346
|
+
botConfig[field.key] = `\${${field.envKey}}`;
|
|
347
|
+
} else {
|
|
348
|
+
botConfig[field.key] = value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
result.bots.push(botConfig);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 获取适配器配置需要写入 .env 的环境变量文本
|
|
360
|
+
*/
|
|
361
|
+
export function generateAdapterEnvVars(result: AdapterSetupResult): string {
|
|
362
|
+
const entries = Object.entries(result.envVars);
|
|
363
|
+
if (entries.length === 0) return '';
|
|
364
|
+
|
|
365
|
+
const lines: string[] = ['', '# 适配器配置'];
|
|
366
|
+
for (const [key, value] of entries) {
|
|
367
|
+
lines.push(`${key}=${value}`);
|
|
368
|
+
}
|
|
369
|
+
return lines.join('\n');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 将 bot 配置项格式化为 YAML 行
|
|
374
|
+
*/
|
|
375
|
+
function appendYamlValue(lines: string[], key: string, value: unknown, indent: number): void {
|
|
376
|
+
const pad = ' '.repeat(indent);
|
|
377
|
+
if (value === undefined || value === null) return;
|
|
378
|
+
|
|
379
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
380
|
+
lines.push(`${pad}${key}:`);
|
|
381
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
382
|
+
appendYamlValue(lines, subKey, subValue, indent + 1);
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
lines.push(`${pad}${key}:`);
|
|
389
|
+
for (const item of value) {
|
|
390
|
+
if (typeof item === 'string') {
|
|
391
|
+
lines.push(`${pad} - "${item}"`);
|
|
392
|
+
} else {
|
|
393
|
+
lines.push(`${pad} - ${item}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
400
|
+
lines.push(`${pad}${key}: ${value}`);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
lines.push(`${pad}${key}: ${value}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* 生成 YAML 格式的 bots 配置段
|
|
409
|
+
*/
|
|
410
|
+
export function generateBotsConfigYaml(result: AdapterSetupResult): string {
|
|
411
|
+
if (result.bots.length === 0) return '';
|
|
412
|
+
|
|
413
|
+
const lines: string[] = ['', 'bots:'];
|
|
414
|
+
for (const bot of result.bots) {
|
|
415
|
+
const { context, ...config } = bot;
|
|
416
|
+
lines.push(` - context: ${context}`);
|
|
417
|
+
for (const [key, value] of Object.entries(config)) {
|
|
418
|
+
appendYamlValue(lines, key, value, 2);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return lines.join('\n');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* 生成 TOML 格式的 bots 配置段
|
|
427
|
+
*/
|
|
428
|
+
export function generateBotsConfigToml(result: AdapterSetupResult): string {
|
|
429
|
+
if (result.bots.length === 0) return '';
|
|
430
|
+
|
|
431
|
+
const lines: string[] = [''];
|
|
432
|
+
for (const bot of result.bots) {
|
|
433
|
+
lines.push('[[bots]]');
|
|
434
|
+
for (const [key, value] of Object.entries(bot)) {
|
|
435
|
+
if (typeof value === 'string') {
|
|
436
|
+
lines.push(`${key} = "${value.replace(/"/g, '\\"')}"`);
|
|
437
|
+
} else {
|
|
438
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
lines.push('');
|
|
442
|
+
}
|
|
443
|
+
return lines.join('\n');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* 生成 JSON 格式的 bots 配置段
|
|
448
|
+
*/
|
|
449
|
+
export function generateBotsConfigJSON(result: AdapterSetupResult): string {
|
|
450
|
+
if (result.bots.length === 0) return '';
|
|
451
|
+
return ` "bots": ${JSON.stringify(result.bots, null, 4).replace(/^/gm, ' ').trimStart()},`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 获取适配器配置后的简要说明(用于初始化完成提示)
|
|
456
|
+
*/
|
|
457
|
+
export function getAdapterSetupNotes(result: AdapterSetupResult): string[] {
|
|
458
|
+
const notes: string[] = [];
|
|
459
|
+
for (const bot of result.bots) {
|
|
460
|
+
const context = bot.context as string;
|
|
461
|
+
if (context === 'telegram' && bot.polling === false) {
|
|
462
|
+
notes.push('Telegram Webhook: 确保 HTTPS 域名可从公网访问,且 webhook.port 已放行');
|
|
463
|
+
}
|
|
464
|
+
if (context === 'telegram' && bot.polling !== false) {
|
|
465
|
+
notes.push('Telegram polling: 本地开发可直接 pnpm dev,无需公网');
|
|
466
|
+
}
|
|
467
|
+
if (context === 'github') {
|
|
468
|
+
if (bot.webhook_secret) {
|
|
469
|
+
notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/pub/github/webhook');
|
|
470
|
+
} else {
|
|
471
|
+
notes.push('GitHub polling: 无 Webhook 时将轮询 Events API(默认 60s)');
|
|
472
|
+
}
|
|
473
|
+
if (bot.app_id) {
|
|
474
|
+
notes.push('GitHub App: 私钥路径需存在,且 App 已安装到目标仓库');
|
|
475
|
+
} else {
|
|
476
|
+
notes.push('GitHub gh CLI: 运行 gh auth login 完成认证');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
for (const plugin of result.plugins) {
|
|
482
|
+
const value = plugin.replace('@zhin.js/adapter-', '');
|
|
483
|
+
const def = ADAPTERS.find(a => a.value === value);
|
|
484
|
+
if (def?.setupHint && value !== 'sandbox' && !notes.some(n => n.startsWith(def.name.split(' (')[0]))) {
|
|
485
|
+
notes.push(`${def.name.split(' (')[0]}: ${def.setupHint}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (result.requiresDatabase) {
|
|
490
|
+
notes.push('已选适配器需要 database:脚手架将自动配置 SQLite');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return notes;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 获取适配器所需的额外依赖包
|
|
498
|
+
*/
|
|
499
|
+
export function getAdapterDependencies(result: AdapterSetupResult): Record<string, string> {
|
|
500
|
+
const deps: Record<string, string> = {};
|
|
501
|
+
|
|
502
|
+
for (const pkg of result.packages) {
|
|
503
|
+
if (pkg.includes('@')) {
|
|
504
|
+
// 处理 package@version 格式
|
|
505
|
+
const atIdx = pkg.lastIndexOf('@');
|
|
506
|
+
if (atIdx > 0) {
|
|
507
|
+
const name = pkg.substring(0, atIdx);
|
|
508
|
+
const version = pkg.substring(atIdx + 1);
|
|
509
|
+
if (name.startsWith('@')) {
|
|
510
|
+
// 如 @icqqjs/icqq@latest
|
|
511
|
+
deps[name] = version;
|
|
512
|
+
} else {
|
|
513
|
+
deps[name] = version;
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
deps[pkg] = 'latest';
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
deps[pkg] = 'latest';
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return deps;
|
|
524
|
+
}
|