@zhin.js/scaffold-wizard 0.1.7 → 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.
Files changed (41) hide show
  1. package/README.md +4 -0
  2. package/lib/adapter-configurers.d.ts.map +1 -1
  3. package/lib/adapter-configurers.js +14 -53
  4. package/lib/adapter-configurers.js.map +1 -1
  5. package/lib/adapter.d.ts +17 -9
  6. package/lib/adapter.d.ts.map +1 -1
  7. package/lib/adapter.js +77 -94
  8. package/lib/adapter.js.map +1 -1
  9. package/lib/apply.d.ts +22 -1
  10. package/lib/apply.d.ts.map +1 -1
  11. package/lib/apply.js +156 -23
  12. package/lib/apply.js.map +1 -1
  13. package/lib/index.d.ts +4 -4
  14. package/lib/index.d.ts.map +1 -1
  15. package/lib/index.js +3 -3
  16. package/lib/index.js.map +1 -1
  17. package/lib/optional-peers.d.ts +1 -1
  18. package/lib/optional-peers.d.ts.map +1 -1
  19. package/lib/optional-peers.js +1 -1
  20. package/lib/optional-peers.js.map +1 -1
  21. package/lib/project-config-plan.d.ts +2 -0
  22. package/lib/project-config-plan.d.ts.map +1 -1
  23. package/lib/project-config-plan.js +66 -17
  24. package/lib/project-config-plan.js.map +1 -1
  25. package/lib/upgrade-l4.d.ts +1 -2
  26. package/lib/upgrade-l4.d.ts.map +1 -1
  27. package/lib/upgrade-l4.js +1 -1
  28. package/lib/upgrade-l4.js.map +1 -1
  29. package/lib/zhin-stack-deps.d.ts +10 -2
  30. package/lib/zhin-stack-deps.d.ts.map +1 -1
  31. package/lib/zhin-stack-deps.js +27 -11
  32. package/lib/zhin-stack-deps.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/adapter-configurers.ts +14 -52
  35. package/src/adapter.ts +91 -99
  36. package/src/apply.ts +170 -21
  37. package/src/index.ts +9 -4
  38. package/src/optional-peers.ts +1 -5
  39. package/src/project-config-plan.ts +67 -17
  40. package/src/upgrade-l4.ts +2 -10
  41. package/src/zhin-stack-deps.ts +27 -16
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
- endpoints: Array<Record<string, any>>;
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 后可在终端或 Console 沙盒页直接对话。',
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 反向连接(需 host-router)。',
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 用 /pub/github/webhook;需 database 存订阅与 OAuth。',
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 端口(默认 8086)可从平台回调。'));
274
+ console.log(chalk.gray(' 开发时可先用 Sandbox;上线前请确保 http 端口(默认 8068)可从平台回调。'));
265
275
  }
266
276
 
267
277
  const result: AdapterSetupResult = {
268
278
  packages: [],
269
279
  plugins: [],
270
- endpoints: [],
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') continue;
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) continue;
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 endpointConfig = await adapterDef.configure({
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.endpoints.push(endpointConfig);
338
+ result.instances.push({ package: adapterDef.package, instanceKey: adapterDef.value, config: instanceConfig });
316
339
  continue;
317
340
  }
318
341
 
319
- const endpointConfig: Record<string, any> = {
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
- endpointConfig[field.key] = `\${${field.envKey}}`;
368
+ fieldValues[field.key] = `\${${field.envKey}}`;
348
369
  } else {
349
- endpointConfig[field.key] = value;
370
+ fieldValues[field.key] = value;
350
371
  }
351
372
  }
352
373
 
353
- result.endpoints.push(endpointConfig);
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,91 +429,24 @@ export function generateAdapterEnvVars(result: AdapterSetupResult): string {
371
429
  }
372
430
 
373
431
  /**
374
- * 将 Endpoint 配置项格式化为 YAML 行
375
- */
376
- function appendYamlValue(lines: string[], key: string, value: unknown, indent: number): void {
377
- const pad = ' '.repeat(indent);
378
- if (value === undefined || value === null) return;
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 配置段
432
+ * 汇总适配器实例为 zhin.config `plugins.<instanceKey>` 配置映射(新 Plugin Runtime 格式)
434
433
  */
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} = "${value.replace(/"/g, '\\"')}"`);
444
- } else {
445
- lines.push(`${key} = ${JSON.stringify(value)}`);
446
- }
447
- }
448
- lines.push('');
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;
449
438
  }
450
- return lines.join('\n');
439
+ return plugins;
451
440
  }
452
441
 
453
442
  /**
454
- * 生成 JSON 格式的 endpoints 配置段
443
+ * 汇总适配器实例为 package.json `zhin.plugins` 清单条目
455
444
  */
456
- export function generateEndpointsConfigJSON(result: AdapterSetupResult): string {
457
- if (result.endpoints.length === 0) return '';
458
- return ` "endpoints": ${JSON.stringify(result.endpoints, null, 4).replace(/^/gm, ' ').trimStart()},`;
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
+ }));
459
450
  }
460
451
 
461
452
  /**
@@ -463,17 +454,18 @@ export function generateEndpointsConfigJSON(result: AdapterSetupResult): string
463
454
  */
464
455
  export function getAdapterSetupNotes(result: AdapterSetupResult): string[] {
465
456
  const notes: string[] = [];
466
- for (const entry of result.endpoints) {
467
- const context = entry.context as string;
457
+ for (const instance of result.instances) {
458
+ const context = instance.instanceKey;
459
+ const entry = instance.config;
468
460
  if (context === 'telegram' && entry.polling === false) {
469
- notes.push('Telegram Webhook: 确保 HTTPS 域名可从公网访问,且 webhook.port 已放行');
461
+ notes.push('Telegram Webhook: 确保 HTTPS 域名可从公网访问,且 http 端口已放行');
470
462
  }
471
463
  if (context === 'telegram' && entry.polling !== false) {
472
464
  notes.push('Telegram polling: 本地开发可直接 pnpm dev,无需公网');
473
465
  }
474
466
  if (context === 'github') {
475
467
  if (entry.webhook_secret) {
476
- notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/pub/github/webhook');
468
+ notes.push('GitHub Webhook: App 设置 URL 为 https://<域名>/github/webhook');
477
469
  } else {
478
470
  notes.push('GitHub polling: 无 Webhook 时将轮询 Events API(默认 60s)');
479
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 { generateAdapterEnvVars, getAdapterDependencies } from './adapter.js';
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
- /** Host 插件(Webhook / Console 必需) */
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
- if (database.dialect === 'sqlite') {
17
- config.inbox = { enabled: true };
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 = Array.isArray(config.plugins) ? [...config.plugins as string[]] : [];
23
- for (const plugin of result.plugins) {
24
- if (!plugins.includes(plugin)) plugins.push(plugin);
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
- const endpoints = Array.isArray(config.endpoints) ? [...config.endpoints as Record<string, unknown>[]] : [];
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 (!extra) return;
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
- const envPath = path.join(projectDir, '.env');
53
- const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf-8') : '';
54
- await fs.writeFile(envPath, existing.replace(/\s*$/, '') + extra + '\n');
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
- generateEndpointsConfigYaml,
9
- generateEndpointsConfigJSON,
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';
@@ -1,10 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
- import type { PackageJsonLike } from './project-deps.js';
4
- import {
5
- findMissingPackageDependencies,
6
- findUnresolvedPackageInstalls,
7
- } from './project-deps.js';
3
+ import { findMissingPackageDependencies, findUnresolvedPackageInstalls, type PackageJsonLike } from './project-deps.js';
8
4
 
9
5
  export const SPEECH_PACKAGE = '@zhin.js/speech';
10
6
  export const HTML_RENDERER_PACKAGE = '@zhin.js/html-renderer';