@zhin.js/scaffold-wizard 0.1.8 → 0.1.9
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.map +1 -1
- package/lib/adapter-configurers.js +14 -53
- package/lib/adapter-configurers.js.map +1 -1
- package/lib/adapter.d.ts +17 -9
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +77 -104
- 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/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 +1 -1
- package/src/adapter-configurers.ts +14 -52
- package/src/adapter.ts +91 -108
- package/src/apply.ts +170 -21
- package/src/index.ts +9 -4
- package/src/project-config-plan.ts +55 -15
- package/src/zhin-stack-deps.ts +26 -10
package/src/adapter.ts
CHANGED
|
@@ -12,10 +12,20 @@ import {
|
|
|
12
12
|
} from './adapter-configurers.js';
|
|
13
13
|
import { ZHIN_STACK_VERSIONS } from './zhin-stack-deps.js';
|
|
14
14
|
|
|
15
|
+
/** 一个待挂载的子插件实例(package.json zhin.plugins 清单 + zhin.config plugins.<instanceKey> 配置) */
|
|
16
|
+
export interface AdapterPluginInstance {
|
|
17
|
+
/** 适配器/插件包名,如 @zhin.js/adapter-sandbox */
|
|
18
|
+
package: string;
|
|
19
|
+
/** 运行时实例键,如 sandbox / telegram;同名包多实例时需区分 */
|
|
20
|
+
instanceKey: string;
|
|
21
|
+
/** 写入 zhin.config plugins.<instanceKey> 的配置(对齐该插件 schema.json) */
|
|
22
|
+
config: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
export interface AdapterSetupResult {
|
|
16
26
|
packages: string[];
|
|
17
27
|
plugins: string[];
|
|
18
|
-
|
|
28
|
+
instances: AdapterPluginInstance[];
|
|
19
29
|
envVars: Record<string, string>;
|
|
20
30
|
/** 所选适配器需要 database 插件(如 GitHub 订阅/OAuth 表) */
|
|
21
31
|
requiresDatabase?: boolean;
|
|
@@ -56,7 +66,7 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
56
66
|
plugin: '@zhin.js/adapter-sandbox',
|
|
57
67
|
needsHttp: true,
|
|
58
68
|
description: '终端 + Remote Console 沙盒页,本地开发首选',
|
|
59
|
-
setupHint: '无需额外凭据;pnpm dev
|
|
69
|
+
setupHint: '无需额外凭据;pnpm dev(zhin runtime start)后可在 Console 沙盒页直接对话。',
|
|
60
70
|
fields: [],
|
|
61
71
|
},
|
|
62
72
|
{
|
|
@@ -167,7 +177,7 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
167
177
|
plugin: '@zhin.js/adapter-onebot11',
|
|
168
178
|
needsHttp: false,
|
|
169
179
|
description: 'OneBot v11(ws 正向 / wss 反向)',
|
|
170
|
-
setupHint: 'connection: ws 连 OneBot 实现;connection: wss
|
|
180
|
+
setupHint: 'connection: ws 连 OneBot 实现;connection: wss 反向连接(由内置 HTTP Host 承接)。',
|
|
171
181
|
docUrl: adapterDocsUrl('onebot11'),
|
|
172
182
|
configure: configureOneBot11Bot,
|
|
173
183
|
fields: [],
|
|
@@ -211,7 +221,7 @@ const ADAPTERS: AdapterDefinition[] = [
|
|
|
211
221
|
plugin: '@zhin.js/adapter-github',
|
|
212
222
|
needsHttp: true,
|
|
213
223
|
description: 'GitHub Issue/PR 聊天 + App 认证',
|
|
214
|
-
setupHint: 'GitHub App 或 gh CLI;Webhook
|
|
224
|
+
setupHint: 'GitHub App 或 gh CLI;Webhook 由内置 HTTP Host 承接(默认 /github/webhook);需 database 存订阅与 OAuth。',
|
|
215
225
|
docUrl: adapterDocsUrl('github'),
|
|
216
226
|
requiresDatabase: true,
|
|
217
227
|
configure: configureGitHubEndpoint,
|
|
@@ -261,13 +271,13 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
261
271
|
for (const adapter of httpAdapters) {
|
|
262
272
|
console.log(chalk.gray(` • ${adapter.name.split(' (')[0]}`));
|
|
263
273
|
}
|
|
264
|
-
console.log(chalk.gray(' 开发时可先用 Sandbox;上线前请确保 http 端口(默认
|
|
274
|
+
console.log(chalk.gray(' 开发时可先用 Sandbox;上线前请确保 http 端口(默认 8068)可从平台回调。'));
|
|
265
275
|
}
|
|
266
276
|
|
|
267
277
|
const result: AdapterSetupResult = {
|
|
268
278
|
packages: [],
|
|
269
279
|
plugins: [],
|
|
270
|
-
|
|
280
|
+
instances: [],
|
|
271
281
|
envVars: {},
|
|
272
282
|
requiresDatabase: false,
|
|
273
283
|
};
|
|
@@ -287,10 +297,23 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
287
297
|
result.requiresDatabase = true;
|
|
288
298
|
}
|
|
289
299
|
|
|
290
|
-
if (adapterValue === 'sandbox')
|
|
300
|
+
if (adapterValue === 'sandbox') {
|
|
301
|
+
// 对齐 examples/test-bot:Console 沙盒页与本地调试共用的默认 Endpoint
|
|
302
|
+
result.instances.push({
|
|
303
|
+
package: adapterDef.package,
|
|
304
|
+
instanceKey: 'sandbox',
|
|
305
|
+
config: {
|
|
306
|
+
endpoints: [{ context: 'sandbox', name: 'sandbox-bot', owner: 'sandbox-user' }],
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
291
311
|
|
|
292
312
|
const hasConfig = adapterDef.configure || adapterDef.fields.length > 0;
|
|
293
|
-
if (!hasConfig)
|
|
313
|
+
if (!hasConfig) {
|
|
314
|
+
result.instances.push({ package: adapterDef.package, instanceKey: adapterDef.value, config: {} });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
294
317
|
|
|
295
318
|
console.log('');
|
|
296
319
|
console.log(chalk.yellow(` 📝 配置 ${adapterDef.name.split(' (')[0]} Bot`));
|
|
@@ -304,7 +327,7 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
304
327
|
|
|
305
328
|
if (adapterDef.configure) {
|
|
306
329
|
let markedDb = false;
|
|
307
|
-
const
|
|
330
|
+
const instanceConfig = await adapterDef.configure({
|
|
308
331
|
envVars: result.envVars,
|
|
309
332
|
markRequiresDatabase: () => {
|
|
310
333
|
markedDb = true;
|
|
@@ -312,13 +335,11 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
312
335
|
},
|
|
313
336
|
});
|
|
314
337
|
if (markedDb) result.requiresDatabase = true;
|
|
315
|
-
result.
|
|
338
|
+
result.instances.push({ package: adapterDef.package, instanceKey: adapterDef.value, config: instanceConfig });
|
|
316
339
|
continue;
|
|
317
340
|
}
|
|
318
341
|
|
|
319
|
-
const
|
|
320
|
-
context: adapterDef.value,
|
|
321
|
-
};
|
|
342
|
+
const fieldValues: Record<string, any> = {};
|
|
322
343
|
|
|
323
344
|
for (const field of adapterDef.fields) {
|
|
324
345
|
const promptConfig: any = {
|
|
@@ -344,18 +365,55 @@ export async function configureAdapters(): Promise<AdapterSetupResult> {
|
|
|
344
365
|
if (field.envKey) {
|
|
345
366
|
// 敏感信息存 .env,配置引用环境变量
|
|
346
367
|
result.envVars[field.envKey] = value || '';
|
|
347
|
-
|
|
368
|
+
fieldValues[field.key] = `\${${field.envKey}}`;
|
|
348
369
|
} else {
|
|
349
|
-
|
|
370
|
+
fieldValues[field.key] = value;
|
|
350
371
|
}
|
|
351
372
|
}
|
|
352
373
|
|
|
353
|
-
result.
|
|
374
|
+
result.instances.push({
|
|
375
|
+
package: adapterDef.package,
|
|
376
|
+
instanceKey: adapterDef.value,
|
|
377
|
+
config: buildFieldBasedInstanceConfig(adapterDef.value, fieldValues),
|
|
378
|
+
});
|
|
354
379
|
}
|
|
355
380
|
|
|
356
381
|
return result;
|
|
357
382
|
}
|
|
358
383
|
|
|
384
|
+
/**
|
|
385
|
+
* 字段式适配器(无自定义 configure)的实例配置整形,对齐各插件 schema.json:
|
|
386
|
+
* - wechat-mp:webhookPath → path
|
|
387
|
+
* - email:smtp/imap 平铺字段 → smtp/imap 嵌套对象
|
|
388
|
+
* - 其余:字段直传
|
|
389
|
+
*/
|
|
390
|
+
function buildFieldBasedInstanceConfig(adapterValue: string, values: Record<string, any>): Record<string, unknown> {
|
|
391
|
+
if (adapterValue === 'wechat-mp') {
|
|
392
|
+
const { webhookPath, ...rest } = values;
|
|
393
|
+
return { ...rest, path: webhookPath ?? '/wechat/webhook' };
|
|
394
|
+
}
|
|
395
|
+
if (adapterValue === 'email') {
|
|
396
|
+
const user = values.user ?? '';
|
|
397
|
+
const password = values.password ?? '';
|
|
398
|
+
return {
|
|
399
|
+
smtp: {
|
|
400
|
+
host: values.smtpHost,
|
|
401
|
+
port: Number(values.smtpPort) || 465,
|
|
402
|
+
secure: true,
|
|
403
|
+
auth: { user, pass: password },
|
|
404
|
+
},
|
|
405
|
+
imap: {
|
|
406
|
+
host: values.imapHost,
|
|
407
|
+
port: Number(values.imapPort) || 993,
|
|
408
|
+
tls: true,
|
|
409
|
+
user,
|
|
410
|
+
password,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return values;
|
|
415
|
+
}
|
|
416
|
+
|
|
359
417
|
/**
|
|
360
418
|
* 获取适配器配置需要写入 .env 的环境变量文本
|
|
361
419
|
*/
|
|
@@ -371,100 +429,24 @@ export function generateAdapterEnvVars(result: AdapterSetupResult): string {
|
|
|
371
429
|
}
|
|
372
430
|
|
|
373
431
|
/**
|
|
374
|
-
*
|
|
432
|
+
* 汇总适配器实例为 zhin.config `plugins.<instanceKey>` 配置映射(新 Plugin Runtime 格式)
|
|
375
433
|
*/
|
|
376
|
-
function
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
381
|
-
lines.push(`${pad}${key}:`);
|
|
382
|
-
for (const [subKey, subValue] of Object.entries(value)) {
|
|
383
|
-
appendYamlValue(lines, subKey, subValue, indent + 1);
|
|
384
|
-
}
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (Array.isArray(value)) {
|
|
389
|
-
lines.push(`${pad}${key}:`);
|
|
390
|
-
for (const item of value) {
|
|
391
|
-
if (typeof item === 'string') {
|
|
392
|
-
lines.push(`${pad} - "${item}"`);
|
|
393
|
-
} else {
|
|
394
|
-
lines.push(`${pad} - ${item}`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
401
|
-
lines.push(`${pad}${key}: ${value}`);
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
lines.push(`${pad}${key}: ${value}`);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* 生成 YAML 格式的 endpoints 配置段
|
|
410
|
-
*/
|
|
411
|
-
export function generateEndpointsConfigYaml(result: AdapterSetupResult): string {
|
|
412
|
-
if (result.endpoints.length === 0) {
|
|
413
|
-
return `
|
|
414
|
-
|
|
415
|
-
# Sandbox:Remote Console 打开「沙盒」页并通过 WebSocket 连接时自动创建 Endpoint
|
|
416
|
-
endpoints: []
|
|
417
|
-
`;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const lines: string[] = ['', 'endpoints:'];
|
|
421
|
-
for (const entry of result.endpoints) {
|
|
422
|
-
const { context, ...config } = entry;
|
|
423
|
-
lines.push(` - context: ${context}`);
|
|
424
|
-
for (const [key, value] of Object.entries(config)) {
|
|
425
|
-
appendYamlValue(lines, key, value, 2);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return lines.join('\n');
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* 生成 TOML 格式的 endpoints 配置段
|
|
434
|
-
*/
|
|
435
|
-
export function generateEndpointsConfigToml(result: AdapterSetupResult): string {
|
|
436
|
-
if (result.endpoints.length === 0) return '';
|
|
437
|
-
|
|
438
|
-
const lines: string[] = [''];
|
|
439
|
-
for (const entry of result.endpoints) {
|
|
440
|
-
lines.push('[[endpoints]]');
|
|
441
|
-
for (const [key, value] of Object.entries(entry)) {
|
|
442
|
-
if (typeof value === 'string') {
|
|
443
|
-
lines.push(`${key} = "${escapeTomlString(value)}"`);
|
|
444
|
-
} else {
|
|
445
|
-
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
lines.push('');
|
|
449
|
-
}
|
|
450
|
-
return lines.join('\n');
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function escapeTomlString(value: string): string {
|
|
454
|
-
let out = '';
|
|
455
|
-
for (const ch of value) {
|
|
456
|
-
if (ch === '"' || ch === '\\') out += `\\${ch}`;
|
|
457
|
-
else out += ch;
|
|
434
|
+
export function collectAdapterPluginConfigs(result: AdapterSetupResult): Record<string, Record<string, unknown>> {
|
|
435
|
+
const plugins: Record<string, Record<string, unknown>> = {};
|
|
436
|
+
for (const instance of result.instances) {
|
|
437
|
+
plugins[instance.instanceKey] = instance.config;
|
|
458
438
|
}
|
|
459
|
-
return
|
|
439
|
+
return plugins;
|
|
460
440
|
}
|
|
461
441
|
|
|
462
442
|
/**
|
|
463
|
-
*
|
|
443
|
+
* 汇总适配器实例为 package.json `zhin.plugins` 清单条目
|
|
464
444
|
*/
|
|
465
|
-
export function
|
|
466
|
-
|
|
467
|
-
|
|
445
|
+
export function collectAdapterPluginManifest(result: AdapterSetupResult): Array<{ package: string; instanceKey: string }> {
|
|
446
|
+
return result.instances.map((instance) => ({
|
|
447
|
+
package: instance.package,
|
|
448
|
+
instanceKey: instance.instanceKey,
|
|
449
|
+
}));
|
|
468
450
|
}
|
|
469
451
|
|
|
470
452
|
/**
|
|
@@ -472,17 +454,18 @@ export function generateEndpointsConfigJSON(result: AdapterSetupResult): string
|
|
|
472
454
|
*/
|
|
473
455
|
export function getAdapterSetupNotes(result: AdapterSetupResult): string[] {
|
|
474
456
|
const notes: string[] = [];
|
|
475
|
-
for (const
|
|
476
|
-
const context =
|
|
457
|
+
for (const instance of result.instances) {
|
|
458
|
+
const context = instance.instanceKey;
|
|
459
|
+
const entry = instance.config;
|
|
477
460
|
if (context === 'telegram' && entry.polling === false) {
|
|
478
|
-
notes.push('Telegram Webhook: 确保 HTTPS 域名可从公网访问,且
|
|
461
|
+
notes.push('Telegram Webhook: 确保 HTTPS 域名可从公网访问,且 http 端口已放行');
|
|
479
462
|
}
|
|
480
463
|
if (context === 'telegram' && entry.polling !== false) {
|
|
481
464
|
notes.push('Telegram polling: 本地开发可直接 pnpm dev,无需公网');
|
|
482
465
|
}
|
|
483
466
|
if (context === 'github') {
|
|
484
467
|
if (entry.webhook_secret) {
|
|
485
|
-
notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/
|
|
468
|
+
notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/github/webhook');
|
|
486
469
|
} else {
|
|
487
470
|
notes.push('GitHub polling: 无 Webhook 时将轮询 Events API(默认 60s)');
|
|
488
471
|
}
|
package/src/apply.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { stringify as stringifyToml } from 'smol-toml';
|
|
3
5
|
import type { AdapterSetupResult, AISetupConfig, DatabaseConfig, InitOptions } from './types.js';
|
|
4
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
collectAdapterPluginConfigs,
|
|
8
|
+
collectAdapterPluginManifest,
|
|
9
|
+
generateAdapterEnvVars,
|
|
10
|
+
getAdapterDependencies,
|
|
11
|
+
} from './adapter.js';
|
|
5
12
|
import { generateAIEnvVars, materializeAIConfig } from './ai.js';
|
|
6
13
|
import { ensureDatabaseForAdapters, ensureDatabaseForAI, getAIDependencies } from './project-deps.js';
|
|
14
|
+
import { DEFAULT_CREATE_BOT_HTTP_PORT } from './zhin-stack-deps.js';
|
|
15
|
+
import { packageToInstanceKey } from './project-config-plan.js';
|
|
7
16
|
|
|
8
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Plugin Runtime 由 CLI composition root 装配 Console/HTTP Host,
|
|
19
|
+
* 不再需要在 plugins 清单中声明 host 插件;保留导出仅为兼容。
|
|
20
|
+
*/
|
|
9
21
|
export const CONSOLE_HOST_PLUGINS = [
|
|
10
22
|
'@zhin.js/host-router',
|
|
11
23
|
'@zhin.js/host-api',
|
|
@@ -13,32 +25,67 @@ export const CONSOLE_HOST_PLUGINS = [
|
|
|
13
25
|
|
|
14
26
|
export function applyDatabaseToConfig(config: Record<string, unknown>, database: DatabaseConfig): void {
|
|
15
27
|
config.database = database;
|
|
16
|
-
|
|
17
|
-
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 将 legacy 数组形式 plugins 归一化为新 runtime 的 instanceKey 映射 */
|
|
31
|
+
export function normalizePluginsMap(value: unknown): Record<string, unknown> {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
const map: Record<string, unknown> = {};
|
|
34
|
+
for (const entry of value) {
|
|
35
|
+
if (typeof entry !== 'string') continue;
|
|
36
|
+
map[packageToInstanceKey(entry)] ??= {};
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
18
39
|
}
|
|
40
|
+
if (value && typeof value === 'object') {
|
|
41
|
+
return { ...(value as Record<string, unknown>) };
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
export function applyAdaptersToConfig(config: Record<string, unknown>, result: AdapterSetupResult): void {
|
|
22
|
-
const plugins =
|
|
23
|
-
for (const
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (result.plugins.some((p: string) => p !== '@zhin.js/adapter-sandbox')) {
|
|
27
|
-
for (const hostPlugin of CONSOLE_HOST_PLUGINS) {
|
|
28
|
-
if (!plugins.includes(hostPlugin)) plugins.push(hostPlugin);
|
|
29
|
-
}
|
|
47
|
+
const plugins = normalizePluginsMap(config.plugins);
|
|
48
|
+
for (const instance of result.instances) {
|
|
49
|
+
plugins[instance.instanceKey] = instance.config;
|
|
30
50
|
}
|
|
31
51
|
config.plugins = plugins;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
endpoints.push(...result.endpoints);
|
|
35
|
-
config.endpoints = endpoints;
|
|
52
|
+
// legacy endpoints 列表不被 runtime 配置 schema 接受(additionalProperties: false)
|
|
53
|
+
delete config.endpoints;
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
export function applyAIToConfig(config: Record<string, unknown>, ai: AISetupConfig): void {
|
|
39
57
|
config.ai = materializeAIConfig(ai);
|
|
40
58
|
}
|
|
41
59
|
|
|
60
|
+
/** 把 zhin.plugins 清单条目合并进项目 package.json(zhin setup 路径) */
|
|
61
|
+
export async function mergePluginManifestIntoPackageJson(
|
|
62
|
+
projectDir: string,
|
|
63
|
+
entries: Array<{ package: string; instanceKey: string }>,
|
|
64
|
+
): Promise<boolean> {
|
|
65
|
+
if (entries.length === 0) return false;
|
|
66
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
67
|
+
if (!(await fs.pathExists(pkgPath))) return false;
|
|
68
|
+
|
|
69
|
+
const pkg = await fs.readJson(pkgPath);
|
|
70
|
+
const zhin = pkg.zhin && typeof pkg.zhin === 'object' && !Array.isArray(pkg.zhin)
|
|
71
|
+
? pkg.zhin
|
|
72
|
+
: { protocol: 1, type: 'plugin', entry: './plugin.ts' };
|
|
73
|
+
const manifest: Array<{ package: string; instanceKey: string }> = Array.isArray(zhin.plugins)
|
|
74
|
+
? [...zhin.plugins]
|
|
75
|
+
: [];
|
|
76
|
+
let changed = false;
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (manifest.some((item) => item?.instanceKey === entry.instanceKey)) continue;
|
|
79
|
+
manifest.push({ package: entry.package, instanceKey: entry.instanceKey });
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
if (!changed && pkg.zhin) return false;
|
|
83
|
+
zhin.plugins = manifest;
|
|
84
|
+
pkg.zhin = zhin;
|
|
85
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
42
89
|
export async function appendWizardEnvVars(
|
|
43
90
|
projectDir: string,
|
|
44
91
|
adapters?: AdapterSetupResult,
|
|
@@ -47,11 +94,16 @@ export async function appendWizardEnvVars(
|
|
|
47
94
|
const extra =
|
|
48
95
|
(adapters ? generateAdapterEnvVars(adapters) : '') +
|
|
49
96
|
(ai ? generateAIEnvVars(ai) : '');
|
|
50
|
-
if (
|
|
97
|
+
if (extra) {
|
|
98
|
+
const envPath = path.join(projectDir, '.env');
|
|
99
|
+
const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf-8') : '';
|
|
100
|
+
await fs.writeFile(envPath, existing.replace(/\s*$/, '') + extra + '\n');
|
|
101
|
+
}
|
|
51
102
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
103
|
+
// 新 Plugin Runtime 通过 package.json zhin.plugins 清单挂载子插件,与依赖一起落盘
|
|
104
|
+
if (adapters) {
|
|
105
|
+
await mergePluginManifestIntoPackageJson(projectDir, collectAdapterPluginManifest(adapters));
|
|
106
|
+
}
|
|
55
107
|
}
|
|
56
108
|
|
|
57
109
|
export function collectWizardDependencies(
|
|
@@ -95,5 +147,102 @@ export function finalizeWizardOptions(options: InitOptions): void {
|
|
|
95
147
|
export function applyWizardOptionsToConfig(config: Record<string, unknown>, options: InitOptions): void {
|
|
96
148
|
if (options.database) applyDatabaseToConfig(config, options.database);
|
|
97
149
|
if (options.adapters) applyAdaptersToConfig(config, options.adapters);
|
|
98
|
-
if (options.ai) applyAIToConfig(config, options.ai);
|
|
150
|
+
if (options.ai?.enabled) applyAIToConfig(config, options.ai);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 数据库配置物化为可落盘对象;非 SQLite 连接参数引用 .env(${VAR} 由 runtime 展开)
|
|
155
|
+
*/
|
|
156
|
+
export function materializeDatabaseConfig(config: DatabaseConfig): Record<string, unknown> {
|
|
157
|
+
switch (config.dialect) {
|
|
158
|
+
case 'mysql':
|
|
159
|
+
return {
|
|
160
|
+
dialect: 'mysql',
|
|
161
|
+
host: '${DB_HOST}',
|
|
162
|
+
port: '${DB_PORT}',
|
|
163
|
+
user: '${DB_USER}',
|
|
164
|
+
password: '${DB_PASSWORD}',
|
|
165
|
+
database: '${DB_DATABASE}',
|
|
166
|
+
};
|
|
167
|
+
case 'pg':
|
|
168
|
+
return {
|
|
169
|
+
dialect: 'pg',
|
|
170
|
+
host: '${DB_HOST}',
|
|
171
|
+
port: '${DB_PORT}',
|
|
172
|
+
user: '${DB_USER}',
|
|
173
|
+
password: '${DB_PASSWORD}',
|
|
174
|
+
database: '${DB_DATABASE}',
|
|
175
|
+
};
|
|
176
|
+
case 'mongodb':
|
|
177
|
+
return { dialect: 'mongodb', url: '${DB_URL}', dbName: '${DB_NAME}' };
|
|
178
|
+
case 'redis':
|
|
179
|
+
return {
|
|
180
|
+
dialect: 'redis',
|
|
181
|
+
socket: { host: '${REDIS_HOST}', port: '${REDIS_PORT}' },
|
|
182
|
+
password: '${REDIS_PASSWORD}',
|
|
183
|
+
database: '${REDIS_DB}',
|
|
184
|
+
};
|
|
185
|
+
case 'sqlite':
|
|
186
|
+
default: {
|
|
187
|
+
const sqlite: Record<string, unknown> = { dialect: 'sqlite' };
|
|
188
|
+
if (config.filename) sqlite.filename = config.filename;
|
|
189
|
+
if (config.mode) sqlite.mode = config.mode;
|
|
190
|
+
return sqlite;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 生活助手模板的额外 AI 配置(知识库 + compaction + 执行审批) */
|
|
196
|
+
function applyLifeAssistantAiExtras(ai: Record<string, unknown>): Record<string, unknown> {
|
|
197
|
+
const agent = ai.agent && typeof ai.agent === 'object' && !Array.isArray(ai.agent)
|
|
198
|
+
? { ...(ai.agent as Record<string, unknown>) }
|
|
199
|
+
: {};
|
|
200
|
+
return {
|
|
201
|
+
...ai,
|
|
202
|
+
knowledge: { baseDir: 'knowledge' },
|
|
203
|
+
agent: {
|
|
204
|
+
...agent,
|
|
205
|
+
execSecurity: 'allowlist',
|
|
206
|
+
execApprovalMode: 'ask',
|
|
207
|
+
compaction: { enabled: true, auto: true, keepRecentTokens: 20000 },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 构建新 Plugin Runtime 配置文档(顶层 http/database/ai + plugins.<instanceKey>)。
|
|
214
|
+
* 顶层键与 packages/im/runtime/src/config-composer.ts 的 effectiveSchema 对齐。
|
|
215
|
+
*/
|
|
216
|
+
export function buildRuntimeConfigDocument(options: InitOptions): Record<string, unknown> {
|
|
217
|
+
const doc: Record<string, unknown> = {};
|
|
218
|
+
|
|
219
|
+
if (options.database) {
|
|
220
|
+
doc.database = materializeDatabaseConfig(options.database);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
doc.http = {
|
|
224
|
+
token: '${HTTP_TOKEN}',
|
|
225
|
+
port: DEFAULT_CREATE_BOT_HTTP_PORT,
|
|
226
|
+
base: '/api',
|
|
227
|
+
corsOrigins: ['https://console.zhin.dev'],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (options.ai?.enabled) {
|
|
231
|
+
const ai = materializeAIConfig(options.ai);
|
|
232
|
+
doc.ai = options.template === 'life-assistant' ? applyLifeAssistantAiExtras(ai) : ai;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
doc.plugins = options.adapters ? collectAdapterPluginConfigs(options.adapters) : {};
|
|
236
|
+
|
|
237
|
+
return doc;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 序列化配置文档为 zhin.config.yml / json / toml 文本 */
|
|
241
|
+
export function serializeRuntimeConfig(
|
|
242
|
+
doc: Record<string, unknown>,
|
|
243
|
+
format: 'yaml' | 'json' | 'toml',
|
|
244
|
+
): string {
|
|
245
|
+
if (format === 'json') return `${JSON.stringify(doc, null, 2)}\n`;
|
|
246
|
+
if (format === 'toml') return stringifyToml(doc);
|
|
247
|
+
return yaml.stringify(doc);
|
|
99
248
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
export type { InitOptions, DatabaseConfig } from './types.js';
|
|
2
2
|
export { DATABASE_PACKAGES } from './types.js';
|
|
3
3
|
|
|
4
|
-
export type { AdapterSetupResult } from './adapter.js';
|
|
4
|
+
export type { AdapterSetupResult, AdapterPluginInstance } from './adapter.js';
|
|
5
5
|
export {
|
|
6
6
|
configureAdapters,
|
|
7
7
|
generateAdapterEnvVars,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
generateEndpointsConfigToml,
|
|
8
|
+
collectAdapterPluginConfigs,
|
|
9
|
+
collectAdapterPluginManifest,
|
|
11
10
|
getAdapterDependencies,
|
|
12
11
|
getAdapterSetupNotes,
|
|
13
12
|
} from './adapter.js';
|
|
@@ -89,6 +88,7 @@ export {
|
|
|
89
88
|
migrateAiLegacyConfig,
|
|
90
89
|
diagnoseConsoleConfig,
|
|
91
90
|
applyConsoleConfigFixes,
|
|
91
|
+
packageToInstanceKey,
|
|
92
92
|
} from './project-config-plan.js';
|
|
93
93
|
export type {
|
|
94
94
|
LoadedProjectConfig,
|
|
@@ -106,6 +106,11 @@ export {
|
|
|
106
106
|
appendWizardEnvVars,
|
|
107
107
|
collectWizardDependencies,
|
|
108
108
|
mergeDependenciesIntoPackageJson,
|
|
109
|
+
mergePluginManifestIntoPackageJson,
|
|
109
110
|
finalizeWizardOptions,
|
|
110
111
|
applyWizardOptionsToConfig,
|
|
112
|
+
normalizePluginsMap,
|
|
113
|
+
materializeDatabaseConfig,
|
|
114
|
+
buildRuntimeConfigDocument,
|
|
115
|
+
serializeRuntimeConfig,
|
|
111
116
|
} from './apply.js';
|