@zhin.js/scaffold-wizard 0.1.9 → 0.2.1
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 +2 -1
- package/lib/adapter-configurers.d.ts +10 -0
- package/lib/adapter-configurers.d.ts.map +1 -1
- package/lib/adapter-configurers.js +428 -24
- package/lib/adapter-configurers.js.map +1 -1
- package/lib/adapter.d.ts +43 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +163 -33
- package/lib/adapter.js.map +1 -1
- package/lib/ai.d.ts +6 -0
- package/lib/ai.d.ts.map +1 -1
- package/lib/ai.js +20 -4
- package/lib/ai.js.map +1 -1
- package/lib/apply.d.ts +11 -6
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +84 -15
- package/lib/apply.js.map +1 -1
- package/lib/env.d.ts +14 -0
- package/lib/env.d.ts.map +1 -0
- package/lib/env.js +89 -0
- package/lib/env.js.map +1 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/lib/optional-peers.d.ts.map +1 -1
- package/lib/optional-peers.js +11 -0
- package/lib/optional-peers.js.map +1 -1
- package/lib/project-config-plan.d.ts +0 -1
- package/lib/project-config-plan.d.ts.map +1 -1
- package/lib/project-config-plan.js +0 -8
- package/lib/project-config-plan.js.map +1 -1
- package/lib/project-deps.d.ts +2 -1
- package/lib/project-deps.d.ts.map +1 -1
- package/lib/project-deps.js +13 -4
- package/lib/project-deps.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 +4 -3
- package/lib/zhin-stack-deps.d.ts.map +1 -1
- package/lib/zhin-stack-deps.js +4 -20
- package/lib/zhin-stack-deps.js.map +1 -1
- package/package.json +3 -1
- package/src/adapter-configurers.ts +456 -24
- package/src/adapter.ts +171 -34
- package/src/ai.ts +21 -4
- package/src/apply.ts +98 -14
- package/src/env.ts +113 -0
- package/src/index.ts +12 -2
- package/src/optional-peers.ts +9 -0
- package/src/project-config-plan.ts +0 -9
- package/src/project-deps.ts +13 -4
- package/src/stack-versions.generated.json +36 -38
- package/src/weixin-ilink-login.ts +151 -0
- package/src/zhin-stack-deps.ts +4 -22
package/src/adapter.ts
CHANGED
|
@@ -5,12 +5,18 @@ import {
|
|
|
5
5
|
adapterDocsUrl,
|
|
6
6
|
configureDiscordEndpoint,
|
|
7
7
|
configureGitHubEndpoint,
|
|
8
|
+
configureMilkyBot,
|
|
9
|
+
configureNapcatBot,
|
|
8
10
|
configureOneBot11Bot,
|
|
11
|
+
configureOneBot12Bot,
|
|
9
12
|
configureQQBot,
|
|
13
|
+
configureSatoriBot,
|
|
10
14
|
configureSlackEndpoint,
|
|
11
15
|
configureTelegramEndpoint,
|
|
16
|
+
configureWeixinIlinkBot,
|
|
12
17
|
} from './adapter-configurers.js';
|
|
13
18
|
import { ZHIN_STACK_VERSIONS } from './zhin-stack-deps.js';
|
|
19
|
+
import { formatEnvValue } from './env.js';
|
|
14
20
|
|
|
15
21
|
/** 一个待挂载的子插件实例(package.json zhin.plugins 清单 + zhin.config plugins.<instanceKey> 配置) */
|
|
16
22
|
export interface AdapterPluginInstance {
|
|
@@ -32,7 +38,7 @@ export interface AdapterSetupResult {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
// 适配器定义
|
|
35
|
-
interface AdapterDefinition {
|
|
41
|
+
export interface AdapterDefinition {
|
|
36
42
|
name: string;
|
|
37
43
|
value: string;
|
|
38
44
|
package: string;
|
|
@@ -48,7 +54,7 @@ interface AdapterDefinition {
|
|
|
48
54
|
fields: AdapterField[];
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
interface AdapterField {
|
|
57
|
+
export interface AdapterField {
|
|
52
58
|
key: string;
|
|
53
59
|
message: string;
|
|
54
60
|
type?: 'input' | 'password' | 'select';
|
|
@@ -56,9 +62,11 @@ interface AdapterField {
|
|
|
56
62
|
required?: boolean;
|
|
57
63
|
envKey?: string; // 如果是敏感信息,对应的环境变量名
|
|
58
64
|
choices?: { name: string; value: string }[];
|
|
65
|
+
/** 字段归属:endpoint 级(默认,进 endpoints[i])或 shared(顶层共享字段) */
|
|
66
|
+
scope?: 'endpoint' | 'shared';
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
const ADAPTERS: AdapterDefinition[] = [
|
|
69
|
+
export const ADAPTERS: AdapterDefinition[] = [
|
|
62
70
|
{
|
|
63
71
|
name: 'Sandbox (调试沙盒,默认)',
|
|
64
72
|
value: 'sandbox',
|
|
@@ -80,6 +88,8 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
80
88
|
docUrl: adapterDocsUrl('icqq'),
|
|
81
89
|
fields: [
|
|
82
90
|
{ key: 'name', message: 'QQ 号(与 icqq login 时一致):', required: true, envKey: 'ICQQ_ACCOUNT' },
|
|
91
|
+
// schema 顶层 required: master(所有 endpoint 共享)
|
|
92
|
+
{ key: 'master', message: '主人 QQ 号(master,/approve 等管理命令):', required: true, scope: 'shared' },
|
|
83
93
|
],
|
|
84
94
|
},
|
|
85
95
|
{
|
|
@@ -153,6 +163,8 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
153
163
|
fields: [
|
|
154
164
|
{ key: 'appKey', message: 'App Key:', required: true, envKey: 'DINGTALK_APP_KEY' },
|
|
155
165
|
{ key: 'appSecret', message: 'App Secret:', required: true, type: 'password', envKey: 'DINGTALK_APP_SECRET' },
|
|
166
|
+
// schema endpoints[i] required 含 robotCode(主动发送 /robot/send 需要)
|
|
167
|
+
{ key: 'robotCode', message: 'Robot Code:', required: true, envKey: 'DINGTALK_ROBOT_CODE' },
|
|
156
168
|
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/dingtalk/webhook' },
|
|
157
169
|
],
|
|
158
170
|
},
|
|
@@ -167,7 +179,24 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
167
179
|
fields: [
|
|
168
180
|
{ key: 'appId', message: 'App ID:', required: true, envKey: 'LARK_APP_ID' },
|
|
169
181
|
{ key: 'appSecret', message: 'App Secret:', required: true, type: 'password', envKey: 'LARK_APP_SECRET' },
|
|
170
|
-
|
|
182
|
+
// schema 顶层共享字段(endpoints[i] 不含 webhookPath)
|
|
183
|
+
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/lark/webhook', scope: 'shared' },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'LINE',
|
|
188
|
+
value: 'line',
|
|
189
|
+
package: '@zhin.js/adapter-line',
|
|
190
|
+
plugin: '@zhin.js/adapter-line',
|
|
191
|
+
needsHttp: true,
|
|
192
|
+
description: 'LINE Messaging API',
|
|
193
|
+
setupHint: '在 LINE Developers Console 创建 Messaging API Channel;Webhook URL 需 HTTPS 公网可达,并关闭 Auto-reply。',
|
|
194
|
+
docUrl: adapterDocsUrl('line'),
|
|
195
|
+
fields: [
|
|
196
|
+
{ key: 'channelSecret', message: 'Channel Secret:', required: true, type: 'password', envKey: 'LINE_CHANNEL_SECRET' },
|
|
197
|
+
{ key: 'channelAccessToken', message: 'Channel Access Token:', required: true, type: 'password', envKey: 'LINE_CHANNEL_ACCESS_TOKEN' },
|
|
198
|
+
// schema 顶层共享字段(endpoints[i] 不含 webhookPath)
|
|
199
|
+
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/line/webhook', scope: 'shared' },
|
|
171
200
|
],
|
|
172
201
|
},
|
|
173
202
|
{
|
|
@@ -182,6 +211,54 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
182
211
|
configure: configureOneBot11Bot,
|
|
183
212
|
fields: [],
|
|
184
213
|
},
|
|
214
|
+
{
|
|
215
|
+
name: 'OneBot v12',
|
|
216
|
+
value: 'onebot12',
|
|
217
|
+
package: '@zhin.js/adapter-onebot12',
|
|
218
|
+
plugin: '@zhin.js/adapter-onebot12',
|
|
219
|
+
needsHttp: false,
|
|
220
|
+
description: 'OneBot v12(ws / webhook / wss)',
|
|
221
|
+
setupHint: 'connection: ws 连 OneBot 实现;webhook / wss 由内置 HTTP Host 承接。',
|
|
222
|
+
docUrl: adapterDocsUrl('onebot12'),
|
|
223
|
+
configure: configureOneBot12Bot,
|
|
224
|
+
fields: [],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'NapCat',
|
|
228
|
+
value: 'napcat',
|
|
229
|
+
package: '@zhin.js/adapter-napcat',
|
|
230
|
+
plugin: '@zhin.js/adapter-napcat',
|
|
231
|
+
needsHttp: false,
|
|
232
|
+
description: 'NapCat(OneBot 11 + NapCat 扩展)',
|
|
233
|
+
setupHint: 'connection: ws 连 NapCat(默认 ws://127.0.0.1:3001);wss / http 由内置 HTTP Host 承接。',
|
|
234
|
+
docUrl: adapterDocsUrl('napcat'),
|
|
235
|
+
configure: configureNapcatBot,
|
|
236
|
+
fields: [],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'Milky',
|
|
240
|
+
value: 'milky',
|
|
241
|
+
package: '@zhin.js/adapter-milky',
|
|
242
|
+
plugin: '@zhin.js/adapter-milky',
|
|
243
|
+
needsHttp: false,
|
|
244
|
+
description: 'Milky 协议(ws / sse / webhook / wss)',
|
|
245
|
+
setupHint: 'connection: ws 连协议端 ws(s)://baseUrl/event;webhook / wss 由内置 HTTP Host 承接。',
|
|
246
|
+
docUrl: adapterDocsUrl('milky'),
|
|
247
|
+
configure: configureMilkyBot,
|
|
248
|
+
fields: [],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'Satori',
|
|
252
|
+
value: 'satori',
|
|
253
|
+
package: '@zhin.js/adapter-satori',
|
|
254
|
+
plugin: '@zhin.js/adapter-satori',
|
|
255
|
+
needsHttp: false,
|
|
256
|
+
description: 'Satori 聊天协议(ws / webhook)',
|
|
257
|
+
setupHint: 'connection: ws 连 Satori SDK(默认 http://127.0.0.1:5140);webhook 由内置 HTTP Host 承接。',
|
|
258
|
+
docUrl: adapterDocsUrl('satori'),
|
|
259
|
+
configure: configureSatoriBot,
|
|
260
|
+
fields: [],
|
|
261
|
+
},
|
|
185
262
|
{
|
|
186
263
|
name: '微信公众号',
|
|
187
264
|
value: 'wechat-mp',
|
|
@@ -194,7 +271,26 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
194
271
|
{ key: 'appId', message: 'App ID:', required: true, envKey: 'WECHAT_APP_ID' },
|
|
195
272
|
{ key: 'appSecret', message: 'App Secret:', required: true, type: 'password', envKey: 'WECHAT_APP_SECRET' },
|
|
196
273
|
{ key: 'token', message: '验证 Token:', required: true, envKey: 'WECHAT_TOKEN' },
|
|
197
|
-
|
|
274
|
+
// schema 顶层共享字段名为 path(endpoints[i] 不含此项)
|
|
275
|
+
{ key: 'webhookPath', message: 'Webhook 路径:', default: '/wechat/webhook', scope: 'shared' },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: '企业微信',
|
|
280
|
+
value: 'wecom',
|
|
281
|
+
package: '@zhin.js/adapter-wecom',
|
|
282
|
+
plugin: '@zhin.js/adapter-wecom',
|
|
283
|
+
needsHttp: true,
|
|
284
|
+
description: '企业微信自建应用',
|
|
285
|
+
setupHint: '管理后台创建自建应用并配置「接收消息」URL(需公网可达);Token / EncodingAESKey 与后台一致。',
|
|
286
|
+
docUrl: adapterDocsUrl('wecom'),
|
|
287
|
+
fields: [
|
|
288
|
+
{ key: 'corpId', message: 'Corp ID:', required: true, envKey: 'WECOM_CORP_ID' },
|
|
289
|
+
{ key: 'agentSecret', message: '应用 Secret:', required: true, type: 'password', envKey: 'WECOM_AGENT_SECRET' },
|
|
290
|
+
{ key: 'token', message: '回调 Token:', required: true, type: 'password', envKey: 'WECOM_TOKEN' },
|
|
291
|
+
{ key: 'encodingAESKey', message: 'EncodingAESKey(43 字符):', required: true, type: 'password', envKey: 'WECOM_AES_KEY' },
|
|
292
|
+
// schema 顶层共享字段(endpoints[i] 不含 webhookPath)
|
|
293
|
+
{ key: 'webhookPath', message: '回调路径:', default: '/wecom/callback', scope: 'shared' },
|
|
198
294
|
],
|
|
199
295
|
},
|
|
200
296
|
{
|
|
@@ -227,6 +323,18 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
227
323
|
configure: configureGitHubEndpoint,
|
|
228
324
|
fields: [],
|
|
229
325
|
},
|
|
326
|
+
{
|
|
327
|
+
name: '微信 iLink(个人微信)',
|
|
328
|
+
value: 'weixin-ilink',
|
|
329
|
+
package: '@zhin.js/adapter-weixin-ilink',
|
|
330
|
+
plugin: '@zhin.js/adapter-weixin-ilink',
|
|
331
|
+
needsHttp: false,
|
|
332
|
+
description: '个人微信(iLink / ClawBot 灰度入口,长轮询,仅私聊)',
|
|
333
|
+
setupHint: '需最新版微信 + ClawBot 灰度资格;默认扫码绑定(bot_token 写入 .env),也可手动输入或放 data/weixin-ilink/<name>.json 侧车凭证文件。',
|
|
334
|
+
docUrl: adapterDocsUrl('weixin-ilink'),
|
|
335
|
+
configure: configureWeixinIlinkBot,
|
|
336
|
+
fields: [],
|
|
337
|
+
},
|
|
230
338
|
];
|
|
231
339
|
|
|
232
340
|
/**
|
|
@@ -253,7 +361,7 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
253
361
|
if (input.length === 0) return '至少选择一个适配器';
|
|
254
362
|
return true;
|
|
255
363
|
},
|
|
256
|
-
pageSize:
|
|
364
|
+
pageSize: 22,
|
|
257
365
|
}
|
|
258
366
|
]);
|
|
259
367
|
|
|
@@ -374,7 +482,7 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
374
482
|
result.instances.push({
|
|
375
483
|
package: adapterDef.package,
|
|
376
484
|
instanceKey: adapterDef.value,
|
|
377
|
-
config: buildFieldBasedInstanceConfig(adapterDef
|
|
485
|
+
config: buildFieldBasedInstanceConfig(adapterDef, fieldValues),
|
|
378
486
|
});
|
|
379
487
|
}
|
|
380
488
|
|
|
@@ -382,36 +490,56 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
382
490
|
}
|
|
383
491
|
|
|
384
492
|
/**
|
|
385
|
-
* 字段式适配器(无自定义 configure)的实例配置整形,对齐各插件 schema.json
|
|
386
|
-
* -
|
|
387
|
-
* -
|
|
388
|
-
* -
|
|
493
|
+
* 字段式适配器(无自定义 configure)的实例配置整形,对齐各插件 schema.json 新格式:
|
|
494
|
+
* - endpoint 级字段(name + 凭据等,字段默认 scope: 'endpoint')进 `endpoints: [{ name, ... }]`
|
|
495
|
+
* - scope: 'shared' 的字段(如 lark/wechat-mp 的 webhookPath)留顶层
|
|
496
|
+
* - wechat-mp:顶层字段名为 path(向导沿用 webhookPath 提问)
|
|
497
|
+
* - email:smtp/imap 平铺字段 → endpoint 内 smtp/imap 嵌套对象
|
|
498
|
+
* - 未单独询问 name 的适配器用 `<adapter>-bot` 兜底
|
|
389
499
|
*/
|
|
390
|
-
function buildFieldBasedInstanceConfig(
|
|
391
|
-
if (
|
|
392
|
-
const { webhookPath, ...rest } = values;
|
|
393
|
-
return { ...rest, path: webhookPath ?? '/wechat/webhook' };
|
|
394
|
-
}
|
|
395
|
-
if (adapterValue === 'email') {
|
|
500
|
+
export function buildFieldBasedInstanceConfig(adapter: AdapterDefinition, values: Record<string, any>): Record<string, unknown> {
|
|
501
|
+
if (adapter.value === 'email') {
|
|
396
502
|
const user = values.user ?? '';
|
|
397
503
|
const password = values.password ?? '';
|
|
398
504
|
return {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
505
|
+
endpoints: [{
|
|
506
|
+
name: 'email-bot',
|
|
507
|
+
smtp: {
|
|
508
|
+
host: values.smtpHost,
|
|
509
|
+
port: Number(values.smtpPort) || 465,
|
|
510
|
+
secure: true,
|
|
511
|
+
auth: { user, pass: password },
|
|
512
|
+
},
|
|
513
|
+
imap: {
|
|
514
|
+
host: values.imapHost,
|
|
515
|
+
port: Number(values.imapPort) || 993,
|
|
516
|
+
tls: true,
|
|
517
|
+
user,
|
|
518
|
+
password,
|
|
519
|
+
},
|
|
520
|
+
}],
|
|
412
521
|
};
|
|
413
522
|
}
|
|
414
|
-
|
|
523
|
+
|
|
524
|
+
const endpoint: Record<string, unknown> = {};
|
|
525
|
+
const shared: Record<string, unknown> = {};
|
|
526
|
+
for (const field of adapter.fields) {
|
|
527
|
+
const value = values[field.key];
|
|
528
|
+
if (value === undefined) continue;
|
|
529
|
+
if (field.scope === 'shared') {
|
|
530
|
+
shared[field.key] = value;
|
|
531
|
+
} else {
|
|
532
|
+
endpoint[field.key] = value;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (adapter.value === 'wechat-mp' && shared.webhookPath !== undefined) {
|
|
536
|
+
shared.path = shared.webhookPath;
|
|
537
|
+
delete shared.webhookPath;
|
|
538
|
+
}
|
|
539
|
+
if (!endpoint.name) {
|
|
540
|
+
endpoint.name = `${adapter.value}-bot`;
|
|
541
|
+
}
|
|
542
|
+
return { ...shared, endpoints: [endpoint] };
|
|
415
543
|
}
|
|
416
544
|
|
|
417
545
|
/**
|
|
@@ -423,7 +551,7 @@ export function generateAdapterEnvVars(result: AdapterSetupResult): string {
|
|
|
423
551
|
|
|
424
552
|
const lines: string[] = ['', '# 适配器配置'];
|
|
425
553
|
for (const [key, value] of entries) {
|
|
426
|
-
lines.push(`${key}=${value}`);
|
|
554
|
+
lines.push(`${key}=${formatEnvValue(value)}`);
|
|
427
555
|
}
|
|
428
556
|
return lines.join('\n');
|
|
429
557
|
}
|
|
@@ -464,12 +592,14 @@ export function getAdapterSetupNotes(result: AdapterSetupResult): string[] {
|
|
|
464
592
|
notes.push('Telegram polling: 本地开发可直接 pnpm dev,无需公网');
|
|
465
593
|
}
|
|
466
594
|
if (context === 'github') {
|
|
467
|
-
|
|
595
|
+
const endpoints = Array.isArray(entry.endpoints) ? entry.endpoints as Array<Record<string, unknown>> : [];
|
|
596
|
+
const firstEndpoint = endpoints[0] ?? {};
|
|
597
|
+
if (firstEndpoint.webhook_secret) {
|
|
468
598
|
notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/github/webhook');
|
|
469
599
|
} else {
|
|
470
600
|
notes.push('GitHub polling: 无 Webhook 时将轮询 Events API(默认 60s)');
|
|
471
601
|
}
|
|
472
|
-
if (
|
|
602
|
+
if (firstEndpoint.app_id) {
|
|
473
603
|
notes.push('GitHub App: 私钥路径需存在,且 App 已安装到目标仓库');
|
|
474
604
|
} else {
|
|
475
605
|
notes.push('GitHub gh CLI: 运行 gh auth login 完成认证');
|
|
@@ -521,3 +651,10 @@ export function getAdapterDependencies(result: AdapterSetupResult): Record<strin
|
|
|
521
651
|
|
|
522
652
|
return deps;
|
|
523
653
|
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 按 npm 包名查找已知适配器定义(如 @zhin.js/adapter-telegram)
|
|
657
|
+
*/
|
|
658
|
+
export function findAdapterByPackage(packageName: string): AdapterDefinition | undefined {
|
|
659
|
+
return ADAPTERS.find(a => a.package === packageName || a.plugin === packageName);
|
|
660
|
+
}
|
package/src/ai.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { formatEnvValue } from './env.js';
|
|
3
4
|
|
|
4
5
|
export interface AISetupConfig {
|
|
5
6
|
enabled: boolean;
|
|
@@ -103,6 +104,16 @@ export function providerSdkFor(name: string): string {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* openai-compatible sdk 在 runtime 无 baseUrl 无法建 provider
|
|
109
|
+
* (packages/im/ai sdk-provider-adapter 直接拒绝),这些 provider 的 baseUrl 必填,
|
|
110
|
+
* 向导预填官方地址。
|
|
111
|
+
*/
|
|
112
|
+
export const PROVIDER_DEFAULT_BASE_URLS: Readonly<Record<string, string>> = Object.freeze({
|
|
113
|
+
zhipu: 'https://open.bigmodel.cn/api/paas/v4',
|
|
114
|
+
moonshot: 'https://api.moonshot.cn/v1',
|
|
115
|
+
});
|
|
116
|
+
|
|
106
117
|
function defaultModelForProvider(driver: string): string {
|
|
107
118
|
const entry = PROVIDERS.find(p => p.value === driver);
|
|
108
119
|
return entry?.defaultModel ?? '';
|
|
@@ -192,7 +203,7 @@ export const RECOMMENDED_AI_DEFAULTS = {
|
|
|
192
203
|
export async function configureAI(): Promise<AISetupConfig> {
|
|
193
204
|
console.log('');
|
|
194
205
|
console.log(chalk.blue('🤖 配置 AI 智能体'));
|
|
195
|
-
console.log(chalk.gray(' 启用后将自动写入 package.json:@zhin.js/agent、zod、ai、所选 @ai-sdk
|
|
206
|
+
console.log(chalk.gray(' 启用后将自动写入 package.json:@zhin.js/agent、zod、ai、所选 @ai-sdk/*(启用 memory MCP 时另加 @modelcontextprotocol/sdk)。'));
|
|
196
207
|
console.log(chalk.gray(' 会话持久化默认开启,将自动配置 SQLite 数据库(若尚未选择)。'));
|
|
197
208
|
|
|
198
209
|
const { enableAI } = await inquirer.prompt([
|
|
@@ -253,6 +264,7 @@ export async function configureAI(): Promise<AISetupConfig> {
|
|
|
253
264
|
models: [ollamaConfig.model.trim()],
|
|
254
265
|
};
|
|
255
266
|
} else {
|
|
267
|
+
const defaultBaseUrl = PROVIDER_DEFAULT_BASE_URLS[provider];
|
|
256
268
|
const cloudConfig = await inquirer.prompt([
|
|
257
269
|
{
|
|
258
270
|
type: 'input',
|
|
@@ -272,8 +284,13 @@ export async function configureAI(): Promise<AISetupConfig> {
|
|
|
272
284
|
{
|
|
273
285
|
type: 'input',
|
|
274
286
|
name: 'baseUrl',
|
|
275
|
-
message:
|
|
276
|
-
|
|
287
|
+
message: defaultBaseUrl
|
|
288
|
+
? 'API 地址(该提供商 sdk 为 openai-compatible,baseUrl 必填):'
|
|
289
|
+
: '自定义 API 地址(留空使用官方默认):',
|
|
290
|
+
default: defaultBaseUrl ?? '',
|
|
291
|
+
validate: defaultBaseUrl
|
|
292
|
+
? (input: string) => (input.trim() ? true : 'baseUrl 不能为空(openai-compatible 无默认地址)')
|
|
293
|
+
: undefined,
|
|
277
294
|
}
|
|
278
295
|
]);
|
|
279
296
|
|
|
@@ -389,7 +406,7 @@ export function generateAIEnvVars(config: AISetupConfig): string {
|
|
|
389
406
|
for (const [, providerConfig] of Object.entries(config.providers)) {
|
|
390
407
|
const realKey = (providerConfig as any).__envApiKey;
|
|
391
408
|
if (realKey) {
|
|
392
|
-
envVars.push(`AI_API_KEY=${realKey}`);
|
|
409
|
+
envVars.push(`AI_API_KEY=${formatEnvValue(realKey)}`);
|
|
393
410
|
}
|
|
394
411
|
}
|
|
395
412
|
|
package/src/apply.ts
CHANGED
|
@@ -10,21 +10,14 @@ import {
|
|
|
10
10
|
getAdapterDependencies,
|
|
11
11
|
} from './adapter.js';
|
|
12
12
|
import { generateAIEnvVars, materializeAIConfig } from './ai.js';
|
|
13
|
+
import { generateDatabaseEnvVars, mergeEnvText } from './env.js';
|
|
13
14
|
import { ensureDatabaseForAdapters, ensureDatabaseForAI, getAIDependencies } from './project-deps.js';
|
|
14
|
-
import { DEFAULT_CREATE_BOT_HTTP_PORT } from './zhin-stack-deps.js';
|
|
15
|
+
import { DEFAULT_CREATE_BOT_HTTP_PORT, ZHIN_STACK_VERSIONS } from './zhin-stack-deps.js';
|
|
15
16
|
import { packageToInstanceKey } from './project-config-plan.js';
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated Plugin Runtime 由 CLI composition root 装配 Console/HTTP Host,
|
|
19
|
-
* 不再需要在 plugins 清单中声明 host 插件;保留导出仅为兼容。
|
|
20
|
-
*/
|
|
21
|
-
export const CONSOLE_HOST_PLUGINS = [
|
|
22
|
-
'@zhin.js/host-router',
|
|
23
|
-
'@zhin.js/host-api',
|
|
24
|
-
] as const;
|
|
25
|
-
|
|
26
18
|
export function applyDatabaseToConfig(config: Record<string, unknown>, database: DatabaseConfig): void {
|
|
27
|
-
|
|
19
|
+
// 物化为可落盘对象:非 SQLite 连接参数引用 .env(${VAR}),明文密码不进配置文件
|
|
20
|
+
config.database = materializeDatabaseConfig(database);
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
/** 将 legacy 数组形式 plugins 归一化为新 runtime 的 instanceKey 映射 */
|
|
@@ -46,13 +39,63 @@ export function normalizePluginsMap(value: unknown): Record<string, unknown> {
|
|
|
46
39
|
export function applyAdaptersToConfig(config: Record<string, unknown>, result: AdapterSetupResult): void {
|
|
47
40
|
const plugins = normalizePluginsMap(config.plugins);
|
|
48
41
|
for (const instance of result.instances) {
|
|
49
|
-
plugins[instance.instanceKey] =
|
|
42
|
+
plugins[instance.instanceKey] = mergeAdapterInstanceConfig(
|
|
43
|
+
instance.instanceKey,
|
|
44
|
+
plugins[instance.instanceKey],
|
|
45
|
+
instance.config,
|
|
46
|
+
);
|
|
50
47
|
}
|
|
51
48
|
config.plugins = plugins;
|
|
52
49
|
// legacy endpoints 列表不被 runtime 配置 schema 接受(additionalProperties: false)
|
|
53
50
|
delete config.endpoints;
|
|
54
51
|
}
|
|
55
52
|
|
|
53
|
+
function endpointName(endpoint: unknown): string | undefined {
|
|
54
|
+
if (!endpoint || typeof endpoint !== 'object' || Array.isArray(endpoint)) return undefined;
|
|
55
|
+
const name = (endpoint as Record<string, unknown>).name;
|
|
56
|
+
return typeof name === 'string' ? name : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 重跑向导时按实例合并配置而非整体覆盖:保留现有 endpoints 中 name 不冲突的项
|
|
61
|
+
* 与其他实例级键;被向导结果覆盖的 endpoints / 键在覆盖前打印提示。
|
|
62
|
+
*/
|
|
63
|
+
function mergeAdapterInstanceConfig(
|
|
64
|
+
instanceKey: string,
|
|
65
|
+
existing: unknown,
|
|
66
|
+
next: Record<string, unknown>,
|
|
67
|
+
): Record<string, unknown> {
|
|
68
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) return next;
|
|
69
|
+
const current = existing as Record<string, unknown>;
|
|
70
|
+
const dropped: string[] = [];
|
|
71
|
+
|
|
72
|
+
const merged: Record<string, unknown> = { ...current, ...next };
|
|
73
|
+
|
|
74
|
+
const currentEndpoints = Array.isArray(current.endpoints) ? current.endpoints : [];
|
|
75
|
+
const nextEndpoints = Array.isArray(next.endpoints) ? next.endpoints : [];
|
|
76
|
+
if (currentEndpoints.length > 0 && nextEndpoints.length > 0) {
|
|
77
|
+
const nextNames = new Set(nextEndpoints.map(endpointName));
|
|
78
|
+
const kept = currentEndpoints.filter((endpoint) => {
|
|
79
|
+
const name = endpointName(endpoint);
|
|
80
|
+
if (name != null && nextNames.has(name)) {
|
|
81
|
+
dropped.push(`endpoint "${name}"`);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
merged.endpoints = [...nextEndpoints, ...kept];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const key of Object.keys(next)) {
|
|
90
|
+
if (key !== 'endpoints' && key in current) dropped.push(`键 "${key}"`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (dropped.length > 0) {
|
|
94
|
+
console.warn(`[zhin setup] plugins.${instanceKey} 的 ${dropped.join('、')} 已被向导结果覆盖`);
|
|
95
|
+
}
|
|
96
|
+
return merged;
|
|
97
|
+
}
|
|
98
|
+
|
|
56
99
|
export function applyAIToConfig(config: Record<string, unknown>, ai: AISetupConfig): void {
|
|
57
100
|
config.ai = materializeAIConfig(ai);
|
|
58
101
|
}
|
|
@@ -90,14 +133,17 @@ export async function appendWizardEnvVars(
|
|
|
90
133
|
projectDir: string,
|
|
91
134
|
adapters?: AdapterSetupResult,
|
|
92
135
|
ai?: AISetupConfig,
|
|
136
|
+
database?: DatabaseConfig,
|
|
93
137
|
): Promise<void> {
|
|
94
138
|
const extra =
|
|
139
|
+
(database ? generateDatabaseEnvVars(database) : '') +
|
|
95
140
|
(adapters ? generateAdapterEnvVars(adapters) : '') +
|
|
96
141
|
(ai ? generateAIEnvVars(ai) : '');
|
|
97
|
-
if (extra) {
|
|
142
|
+
if (extra.trim()) {
|
|
98
143
|
const envPath = path.join(projectDir, '.env');
|
|
99
144
|
const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf-8') : '';
|
|
100
|
-
|
|
145
|
+
// 按 KEY 覆盖而非追加:重复运行向导幂等,不产生重复行
|
|
146
|
+
await fs.writeFile(envPath, mergeEnvText(existing, extra));
|
|
101
147
|
}
|
|
102
148
|
|
|
103
149
|
// 新 Plugin Runtime 通过 package.json zhin.plugins 清单挂载子插件,与依赖一起落盘
|
|
@@ -112,9 +158,47 @@ export function collectWizardDependencies(
|
|
|
112
158
|
return {
|
|
113
159
|
...(options.adapters ? getAdapterDependencies(options.adapters) : {}),
|
|
114
160
|
...getAIDependencies(options.ai),
|
|
161
|
+
// tools/ 约定需要 @zhin.js/tool(与 create-zhin 路径对齐)
|
|
162
|
+
...(options.ai?.enabled ? { '@zhin.js/tool': ZHIN_STACK_VERSIONS['@zhin.js/tool'] } : {}),
|
|
115
163
|
};
|
|
116
164
|
}
|
|
117
165
|
|
|
166
|
+
/** 向导结果需要合并进 package.json zhin.features 的条目(与 create-zhin 路径对齐) */
|
|
167
|
+
export function collectWizardFeatures(
|
|
168
|
+
options: Pick<InitOptions, 'ai'>,
|
|
169
|
+
): Array<{ package: string; api: string }> {
|
|
170
|
+
return options.ai?.enabled ? [{ package: '@zhin.js/tool', api: '^1.0.0' }] : [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 把 features 清单条目合并进项目 package.json 的 zhin.features(zhin setup 路径) */
|
|
174
|
+
export async function mergeFeaturesIntoPackageJson(
|
|
175
|
+
projectDir: string,
|
|
176
|
+
entries: Array<{ package: string; api: string }>,
|
|
177
|
+
): Promise<boolean> {
|
|
178
|
+
if (entries.length === 0) return false;
|
|
179
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
180
|
+
if (!(await fs.pathExists(pkgPath))) return false;
|
|
181
|
+
|
|
182
|
+
const pkg = await fs.readJson(pkgPath);
|
|
183
|
+
const zhin = pkg.zhin && typeof pkg.zhin === 'object' && !Array.isArray(pkg.zhin)
|
|
184
|
+
? pkg.zhin
|
|
185
|
+
: { protocol: 1, type: 'plugin', entry: './plugin.ts' };
|
|
186
|
+
const features: Array<{ package: string; api?: string }> = Array.isArray(zhin.features)
|
|
187
|
+
? [...zhin.features]
|
|
188
|
+
: [];
|
|
189
|
+
let changed = false;
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
if (features.some((item) => item?.package === entry.package)) continue;
|
|
192
|
+
features.push(entry);
|
|
193
|
+
changed = true;
|
|
194
|
+
}
|
|
195
|
+
if (!changed && pkg.zhin) return false;
|
|
196
|
+
zhin.features = features;
|
|
197
|
+
pkg.zhin = zhin;
|
|
198
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
118
202
|
export async function mergeDependenciesIntoPackageJson(
|
|
119
203
|
projectDir: string,
|
|
120
204
|
deps: Record<string, string>,
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { DatabaseConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dotenv 值格式化:含 `#` / 空白 / 引号 / 反斜杠 / 换行 时用双引号包裹并转义,
|
|
5
|
+
* 避免未转义值截断注释或破坏行解析(所有 .env 生成点统一走这里)。
|
|
6
|
+
*/
|
|
7
|
+
export function formatEnvValue(value: unknown): string {
|
|
8
|
+
const raw = value == null ? '' : String(value);
|
|
9
|
+
if (raw !== '' && !/[#"'\\\s]/.test(raw)) return raw;
|
|
10
|
+
return `"${raw
|
|
11
|
+
.replace(/\\/g, '\\\\')
|
|
12
|
+
.replace(/"/g, '\\"')
|
|
13
|
+
.replace(/\r/g, '\\r')
|
|
14
|
+
.replace(/\n/g, '\\n')}"`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ENV_KEY_LINE = /^([A-Za-z_][A-Za-z0-9_]*)=/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 幂等合并 .env 内容:extra 中出现的 KEY 覆盖 existing 同名行(原位替换),
|
|
21
|
+
* 新 KEY 连同其段落注释追加到文末。重复执行结果不变。
|
|
22
|
+
*/
|
|
23
|
+
export function mergeEnvText(existing: string, extra: string): string {
|
|
24
|
+
const entries: Array<{ comment?: string; key: string; line: string }> = [];
|
|
25
|
+
let pendingComment: string | undefined;
|
|
26
|
+
for (const rawLine of extra.split('\n')) {
|
|
27
|
+
const line = rawLine.trim();
|
|
28
|
+
if (!line) continue;
|
|
29
|
+
if (line.startsWith('#')) {
|
|
30
|
+
pendingComment = line;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const match = ENV_KEY_LINE.exec(line);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
entries.push({ comment: pendingComment, key: match[1], line });
|
|
36
|
+
pendingComment = undefined;
|
|
37
|
+
}
|
|
38
|
+
const out = existing.split('\n');
|
|
39
|
+
if (entries.length === 0) {
|
|
40
|
+
return existing.replace(/\s*$/, '') + (existing.trim() ? '\n' : '');
|
|
41
|
+
}
|
|
42
|
+
const byKey = new Map(entries.map((entry) => [entry.key, entry]));
|
|
43
|
+
const consumed = new Set<string>();
|
|
44
|
+
for (let index = 0; index < out.length; index += 1) {
|
|
45
|
+
const match = ENV_KEY_LINE.exec(out[index].trim());
|
|
46
|
+
if (match && byKey.has(match[1])) {
|
|
47
|
+
out[index] = byKey.get(match[1])!.line;
|
|
48
|
+
consumed.add(match[1]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const fresh = entries.filter((entry) => !consumed.has(entry.key));
|
|
52
|
+
if (fresh.length > 0) {
|
|
53
|
+
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
|
|
54
|
+
if (out.length > 0) out.push('');
|
|
55
|
+
let lastComment: string | undefined;
|
|
56
|
+
for (const entry of fresh) {
|
|
57
|
+
if (entry.comment && entry.comment !== lastComment) out.push(entry.comment);
|
|
58
|
+
lastComment = entry.comment;
|
|
59
|
+
out.push(entry.line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out.join('\n').replace(/\s*$/, '') + '\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 生成数据库连接环境变量(连接参数落 .env,配置文件只引用 ${VAR})。 */
|
|
66
|
+
export function generateDatabaseEnvVars(config: DatabaseConfig): string {
|
|
67
|
+
const envVars: string[] = [];
|
|
68
|
+
|
|
69
|
+
switch (config.dialect) {
|
|
70
|
+
case 'mysql':
|
|
71
|
+
envVars.push(
|
|
72
|
+
'# MySQL 数据库配置',
|
|
73
|
+
`DB_HOST=${formatEnvValue(config.host || 'localhost')}`,
|
|
74
|
+
`DB_PORT=${formatEnvValue(config.port || 3306)}`,
|
|
75
|
+
`DB_USER=${formatEnvValue(config.user || 'root')}`,
|
|
76
|
+
`DB_PASSWORD=${formatEnvValue(config.password || '')}`,
|
|
77
|
+
`DB_DATABASE=${formatEnvValue(config.database || 'zhin_bot')}`
|
|
78
|
+
);
|
|
79
|
+
break;
|
|
80
|
+
case 'pg':
|
|
81
|
+
envVars.push(
|
|
82
|
+
'# PostgreSQL 数据库配置',
|
|
83
|
+
`DB_HOST=${formatEnvValue(config.host || 'localhost')}`,
|
|
84
|
+
`DB_PORT=${formatEnvValue(config.port || 5432)}`,
|
|
85
|
+
`DB_USER=${formatEnvValue(config.user || 'postgres')}`,
|
|
86
|
+
`DB_PASSWORD=${formatEnvValue(config.password || '')}`,
|
|
87
|
+
`DB_DATABASE=${formatEnvValue(config.database || 'zhin_bot')}`
|
|
88
|
+
);
|
|
89
|
+
break;
|
|
90
|
+
case 'mongodb':
|
|
91
|
+
envVars.push(
|
|
92
|
+
'# MongoDB 数据库配置',
|
|
93
|
+
`DB_URL=${formatEnvValue(config.url || 'mongodb://localhost:27017')}`,
|
|
94
|
+
`DB_NAME=${formatEnvValue(config.dbName || 'zhin_bot')}`
|
|
95
|
+
);
|
|
96
|
+
break;
|
|
97
|
+
case 'redis':
|
|
98
|
+
envVars.push(
|
|
99
|
+
'# Redis 数据库配置',
|
|
100
|
+
`REDIS_HOST=${formatEnvValue(config.socket?.host || 'localhost')}`,
|
|
101
|
+
`REDIS_PORT=${formatEnvValue(config.socket?.port || 6379)}`,
|
|
102
|
+
`REDIS_PASSWORD=${formatEnvValue(config.password || '')}`,
|
|
103
|
+
`REDIS_DB=${formatEnvValue(config.database ?? 0)}`
|
|
104
|
+
);
|
|
105
|
+
break;
|
|
106
|
+
case 'sqlite':
|
|
107
|
+
default:
|
|
108
|
+
// SQLite 不需要额外的环境变量
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return envVars.length > 0 ? `\n\n${envVars.join('\n')}` : '';
|
|
113
|
+
}
|