@zhin.js/scaffold-wizard 1.1.0 → 1.1.2

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 (58) hide show
  1. package/README.md +5 -0
  2. package/lib/adapter.js +1 -1
  3. package/lib/adapter.js.map +1 -1
  4. package/lib/ai.d.ts +1 -7
  5. package/lib/ai.d.ts.map +1 -1
  6. package/lib/ai.js +0 -63
  7. package/lib/ai.js.map +1 -1
  8. package/lib/apply.d.ts +5 -5
  9. package/lib/apply.d.ts.map +1 -1
  10. package/lib/apply.js +13 -23
  11. package/lib/apply.js.map +1 -1
  12. package/lib/database-definitions.d.ts +28 -0
  13. package/lib/database-definitions.d.ts.map +1 -0
  14. package/lib/database-definitions.js +290 -0
  15. package/lib/database-definitions.js.map +1 -0
  16. package/lib/database.d.ts.map +1 -1
  17. package/lib/database.js +69 -51
  18. package/lib/database.js.map +1 -1
  19. package/lib/env.d.ts +6 -0
  20. package/lib/env.d.ts.map +1 -1
  21. package/lib/env.js +51 -0
  22. package/lib/env.js.map +1 -1
  23. package/lib/index.d.ts +8 -6
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +7 -6
  26. package/lib/index.js.map +1 -1
  27. package/lib/optional-peers.d.ts.map +1 -1
  28. package/lib/optional-peers.js +5 -16
  29. package/lib/optional-peers.js.map +1 -1
  30. package/lib/project-config-plan.d.ts +2 -1
  31. package/lib/project-config-plan.d.ts.map +1 -1
  32. package/lib/project-config-plan.js +29 -83
  33. package/lib/project-config-plan.js.map +1 -1
  34. package/lib/project-deps.d.ts +1 -1
  35. package/lib/project-deps.d.ts.map +1 -1
  36. package/lib/project-deps.js +5 -7
  37. package/lib/project-deps.js.map +1 -1
  38. package/lib/types.d.ts +4 -3
  39. package/lib/types.d.ts.map +1 -1
  40. package/lib/types.js +6 -8
  41. package/lib/types.js.map +1 -1
  42. package/lib/zhin-stack-deps.d.ts +8 -6
  43. package/lib/zhin-stack-deps.d.ts.map +1 -1
  44. package/lib/zhin-stack-deps.js +16 -25
  45. package/lib/zhin-stack-deps.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/adapter.ts +1 -1
  48. package/src/ai.ts +1 -70
  49. package/src/apply.ts +17 -23
  50. package/src/database-definitions.ts +336 -0
  51. package/src/database.ts +77 -52
  52. package/src/env.ts +52 -0
  53. package/src/index.ts +18 -6
  54. package/src/optional-peers.ts +4 -14
  55. package/src/project-config-plan.ts +35 -79
  56. package/src/project-deps.ts +5 -6
  57. package/src/types.ts +13 -10
  58. package/src/zhin-stack-deps.ts +16 -27
@@ -0,0 +1,336 @@
1
+ import type { DatabaseConfig } from './types.js';
2
+
3
+ export type DatabaseDialect = DatabaseConfig['dialect'];
4
+
5
+ export interface DatabaseDriverDefinition {
6
+ readonly package: string;
7
+ readonly version: string;
8
+ }
9
+
10
+ export interface DatabaseDialectDefinition {
11
+ readonly dialect: DatabaseDialect;
12
+ readonly name: string;
13
+ readonly description: string;
14
+ /** Concrete service version covered by the repository's live acceptance tests. */
15
+ readonly verifiedServer: string;
16
+ readonly driver?: DatabaseDriverDefinition;
17
+ readonly schema: Readonly<Record<string, unknown>>;
18
+ }
19
+
20
+ type SchemaIssueKind = 'env_missing' | 'invalid';
21
+
22
+ export interface DatabaseSchemaIssue {
23
+ readonly kind: SchemaIssueKind;
24
+ readonly path: string;
25
+ readonly message: string;
26
+ readonly envKey?: string;
27
+ }
28
+
29
+ const text = (defaultValue?: string, extras: Record<string, unknown> = {}) => ({
30
+ type: 'string',
31
+ minLength: 1,
32
+ ...(defaultValue === undefined ? {} : { default: defaultValue }),
33
+ ...extras,
34
+ });
35
+
36
+ const port = (defaultValue: number) => ({
37
+ type: 'integer',
38
+ minimum: 1,
39
+ maximum: 65535,
40
+ default: defaultValue,
41
+ });
42
+
43
+ export const DATABASE_DIALECT_DEFINITIONS: readonly DatabaseDialectDefinition[] = Object.freeze([
44
+ {
45
+ dialect: 'sqlite',
46
+ name: 'SQLite',
47
+ description: '推荐,零配置,适合单机 Bot',
48
+ verifiedServer: 'Node.js 内置 node:sqlite(Node.js 22.5+)',
49
+ schema: {
50
+ type: 'object',
51
+ additionalProperties: true,
52
+ required: ['dialect'],
53
+ properties: {
54
+ dialect: { const: 'sqlite' },
55
+ filename: text('./data/bot.db'),
56
+ mode: { type: 'string', enum: ['wal', 'delete', 'memory'], default: 'wal' },
57
+ },
58
+ },
59
+ },
60
+ {
61
+ dialect: 'mysql',
62
+ name: 'MySQL',
63
+ description: '关系型数据库',
64
+ verifiedServer: 'MySQL 8.4 LTS',
65
+ driver: { package: 'mysql2', version: '^3.23.2' },
66
+ schema: {
67
+ type: 'object',
68
+ additionalProperties: true,
69
+ required: ['dialect'],
70
+ properties: {
71
+ dialect: { const: 'mysql' },
72
+ host: text('127.0.0.1'),
73
+ port: port(3306),
74
+ user: text('root'),
75
+ password: { type: 'string' },
76
+ database: text('zhin_bot'),
77
+ },
78
+ },
79
+ },
80
+ {
81
+ dialect: 'pg',
82
+ name: 'PostgreSQL',
83
+ description: '关系型数据库,方言名为 pg',
84
+ verifiedServer: 'PostgreSQL 17',
85
+ driver: { package: 'pg', version: '^8.22.0' },
86
+ schema: {
87
+ type: 'object',
88
+ additionalProperties: true,
89
+ required: ['dialect'],
90
+ properties: {
91
+ dialect: { const: 'pg' },
92
+ host: text('127.0.0.1'),
93
+ port: port(5432),
94
+ user: text('postgres'),
95
+ password: { type: 'string' },
96
+ database: text('zhin_bot'),
97
+ },
98
+ },
99
+ },
100
+ {
101
+ dialect: 'mongodb',
102
+ name: 'MongoDB',
103
+ description: '文档数据库',
104
+ verifiedServer: 'MongoDB 8',
105
+ driver: { package: 'mongodb', version: '^7.5.0' },
106
+ schema: {
107
+ type: 'object',
108
+ additionalProperties: true,
109
+ required: ['dialect', 'url', 'dbName'],
110
+ properties: {
111
+ dialect: { const: 'mongodb' },
112
+ url: text('mongodb://127.0.0.1:27017', { pattern: '^mongodb(?:\\+srv)?://' }),
113
+ dbName: text('zhin_bot'),
114
+ },
115
+ },
116
+ },
117
+ {
118
+ dialect: 'redis',
119
+ name: 'Redis',
120
+ description: '键值数据库',
121
+ verifiedServer: 'Redis 8',
122
+ driver: { package: 'redis', version: '^6.1.0' },
123
+ schema: {
124
+ type: 'object',
125
+ additionalProperties: true,
126
+ required: ['dialect'],
127
+ properties: {
128
+ dialect: { const: 'redis' },
129
+ socket: {
130
+ type: 'object',
131
+ additionalProperties: true,
132
+ required: ['host', 'port'],
133
+ properties: {
134
+ host: text('127.0.0.1'),
135
+ port: port(6379),
136
+ },
137
+ },
138
+ password: { type: 'string' },
139
+ database: { type: 'integer', minimum: 0, default: 0 },
140
+ },
141
+ },
142
+ },
143
+ {
144
+ dialect: 'memory',
145
+ name: 'Memory',
146
+ description: '仅进程内,适合测试,不持久化',
147
+ verifiedServer: 'Zhin 内置实现',
148
+ schema: {
149
+ type: 'object',
150
+ additionalProperties: false,
151
+ required: ['dialect'],
152
+ properties: { dialect: { const: 'memory' } },
153
+ },
154
+ },
155
+ ]);
156
+
157
+ const definitions = new Map(DATABASE_DIALECT_DEFINITIONS.map((definition) => [definition.dialect, definition]));
158
+
159
+ export function getDatabaseDialectDefinition(dialect: string): DatabaseDialectDefinition | undefined {
160
+ return definitions.get(dialect as DatabaseDialect);
161
+ }
162
+
163
+ export function databaseChoiceLabel(definition: DatabaseDialectDefinition): string {
164
+ const driver = definition.driver
165
+ ? `;驱动 ${definition.driver.package} ${definition.driver.version}`
166
+ : '';
167
+ return `${definition.name}(${definition.description};当前支持/实机验证 ${definition.verifiedServer}${driver})`;
168
+ }
169
+
170
+ function expandEnvReferences(
171
+ input: string,
172
+ path: string,
173
+ env: Readonly<Record<string, string | undefined>>,
174
+ issues: DatabaseSchemaIssue[],
175
+ ): string {
176
+ let output = '';
177
+ let index = 0;
178
+ while (index < input.length) {
179
+ const start = input.indexOf('${', index);
180
+ if (start < 0) break;
181
+ let cursor = start + 2;
182
+ const first = input[cursor];
183
+ if (first === undefined || !isEnvKeyStart(first)) {
184
+ output += input.slice(index, start + 1);
185
+ index = start + 1;
186
+ continue;
187
+ }
188
+ cursor += 1;
189
+ while (cursor < input.length && isEnvKeyCharacter(input[cursor]!)) cursor += 1;
190
+ const key = input.slice(start + 2, cursor);
191
+ let fallback: string | undefined;
192
+ if (input[cursor] === ':' && (input[cursor + 1] === '-' || input[cursor + 1] === '=')) {
193
+ const close = input.indexOf('}', cursor + 2);
194
+ if (close < 0) {
195
+ output += input.slice(index, start + 1);
196
+ index = start + 1;
197
+ continue;
198
+ }
199
+ fallback = input.slice(cursor + 2, close);
200
+ cursor = close;
201
+ }
202
+ if (input[cursor] !== '}') {
203
+ output += input.slice(index, start + 1);
204
+ index = start + 1;
205
+ continue;
206
+ }
207
+ output += input.slice(index, start);
208
+ const resolved = env[key];
209
+ if ((resolved === undefined || resolved === '') && fallback !== undefined) {
210
+ output += fallback;
211
+ } else if (resolved === undefined) {
212
+ issues.push({
213
+ kind: 'env_missing',
214
+ path,
215
+ envKey: key,
216
+ message: `环境变量 ${key} 未设置(${path})`,
217
+ });
218
+ } else {
219
+ output += resolved;
220
+ }
221
+ index = cursor + 1;
222
+ }
223
+ return output + input.slice(index);
224
+ }
225
+
226
+ function isEnvKeyStart(character: string): boolean {
227
+ return (character >= 'A' && character <= 'Z')
228
+ || (character >= 'a' && character <= 'z')
229
+ || character === '_';
230
+ }
231
+
232
+ function isEnvKeyCharacter(character: string): boolean {
233
+ return isEnvKeyStart(character) || (character >= '0' && character <= '9');
234
+ }
235
+
236
+ function validateValue(
237
+ schema: Record<string, unknown>,
238
+ rawValue: unknown,
239
+ path: string,
240
+ env: Readonly<Record<string, string | undefined>>,
241
+ issues: DatabaseSchemaIssue[],
242
+ ): void {
243
+ let value = rawValue;
244
+ if (typeof value === 'string') {
245
+ const before = issues.length;
246
+ value = expandEnvReferences(value, path, env, issues);
247
+ if (issues.length > before) return;
248
+ }
249
+
250
+ if ('const' in schema && value !== schema.const) {
251
+ issues.push({ kind: 'invalid', path, message: `${path} 必须是 ${JSON.stringify(schema.const)}` });
252
+ return;
253
+ }
254
+ const type = schema.type;
255
+ if (type === 'object') {
256
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
257
+ issues.push({ kind: 'invalid', path, message: `${path} 必须是对象` });
258
+ return;
259
+ }
260
+ const record = value as Record<string, unknown>;
261
+ const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
262
+ if (schema.additionalProperties === false) {
263
+ for (const key of Object.keys(record)) {
264
+ if (!(key in properties)) {
265
+ issues.push({ kind: 'invalid', path: `${path}.${key}`, message: `${path}.${key} 不是受支持的配置项` });
266
+ }
267
+ }
268
+ }
269
+ for (const required of (schema.required ?? []) as string[]) {
270
+ if (!(required in record) || record[required] === undefined || record[required] === null) {
271
+ issues.push({ kind: 'invalid', path: `${path}.${required}`, message: `${path}.${required} 不能为空` });
272
+ }
273
+ }
274
+ for (const [key, propertySchema] of Object.entries(properties)) {
275
+ if (record[key] !== undefined) validateValue(propertySchema, record[key], `${path}.${key}`, env, issues);
276
+ }
277
+ return;
278
+ }
279
+ if (type === 'string') {
280
+ if (typeof value !== 'string') {
281
+ issues.push({ kind: 'invalid', path, message: `${path} 必须是字符串` });
282
+ return;
283
+ }
284
+ if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
285
+ issues.push({ kind: 'invalid', path, message: `${path} 不能为空` });
286
+ }
287
+ if (typeof schema.pattern === 'string' && !new RegExp(schema.pattern, 'u').test(value)) {
288
+ issues.push({ kind: 'invalid', path, message: `${path} 格式无效` });
289
+ }
290
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
291
+ issues.push({ kind: 'invalid', path, message: `${path} 必须是 ${schema.enum.join('、')} 之一` });
292
+ }
293
+ return;
294
+ }
295
+ if (type === 'integer') {
296
+ const numeric = typeof value === 'number'
297
+ ? value
298
+ : typeof value === 'string' && /^\d+$/u.test(value.trim())
299
+ ? Number(value.trim())
300
+ : Number.NaN;
301
+ if (!Number.isInteger(numeric)) {
302
+ issues.push({ kind: 'invalid', path, message: `${path} 必须是整数` });
303
+ return;
304
+ }
305
+ if (typeof schema.minimum === 'number' && numeric < schema.minimum) {
306
+ issues.push({ kind: 'invalid', path, message: `${path} 不能小于 ${schema.minimum}` });
307
+ }
308
+ if (typeof schema.maximum === 'number' && numeric > schema.maximum) {
309
+ issues.push({ kind: 'invalid', path, message: `${path} 不能大于 ${schema.maximum}` });
310
+ }
311
+ }
312
+ }
313
+
314
+ export function validateDatabaseConfig(
315
+ config: unknown,
316
+ env: Readonly<Record<string, string | undefined>> = {},
317
+ ): readonly DatabaseSchemaIssue[] {
318
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
319
+ return [{ kind: 'invalid', path: 'database', message: 'database 必须是对象' }];
320
+ }
321
+ const dialect = (config as Record<string, unknown>).dialect;
322
+ if (typeof dialect !== 'string') {
323
+ return [{ kind: 'invalid', path: 'database.dialect', message: 'database.dialect 不能为空' }];
324
+ }
325
+ const definition = getDatabaseDialectDefinition(dialect);
326
+ if (!definition) {
327
+ return [{
328
+ kind: 'invalid',
329
+ path: 'database.dialect',
330
+ message: `不支持的数据库方言 ${JSON.stringify(dialect)}`,
331
+ }];
332
+ }
333
+ const issues: DatabaseSchemaIssue[] = [];
334
+ validateValue(definition.schema as Record<string, unknown>, config, 'database', env, issues);
335
+ return Object.freeze(issues);
336
+ }
package/src/database.ts CHANGED
@@ -1,5 +1,31 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { DatabaseConfig } from './types.js';
3
+ import {
4
+ DATABASE_DIALECT_DEFINITIONS,
5
+ databaseChoiceLabel,
6
+ getDatabaseDialectDefinition,
7
+ validateDatabaseConfig,
8
+ type DatabaseDialect,
9
+ } from './database-definitions.js';
10
+
11
+ function schemaDefault(dialect: DatabaseDialect, ...path: string[]): unknown {
12
+ let schema = getDatabaseDialectDefinition(dialect)?.schema as Record<string, unknown> | undefined;
13
+ for (const segment of path) {
14
+ const properties = schema?.properties as Record<string, Record<string, unknown>> | undefined;
15
+ schema = properties?.[segment];
16
+ }
17
+ return schema?.default;
18
+ }
19
+
20
+ function parseIntegerInput(input: unknown, minimum: number, maximum = Number.MAX_SAFE_INTEGER): number | string {
21
+ const raw = String(input).trim();
22
+ if (!/^\d+$/u.test(raw)) return `请输入有效的整数 (${minimum}-${maximum})`;
23
+ const value = Number(raw);
24
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
25
+ return `请输入有效的整数 (${minimum}-${maximum})`;
26
+ }
27
+ return value;
28
+ }
3
29
 
4
30
  // 配置数据库选项
5
31
  export async function configureDatabaseOptions(): Promise<DatabaseConfig> {
@@ -8,31 +34,42 @@ export async function configureDatabaseOptions(): Promise<DatabaseConfig> {
8
34
  type: 'select',
9
35
  name: 'dialect',
10
36
  message: '选择数据库类型:',
11
- choices: [
12
- { name: 'SQLite (推荐, 零配置)', value: 'sqlite' },
13
- { name: 'MySQL', value: 'mysql' },
14
- { name: 'PostgreSQL', value: 'pg' },
15
- { name: 'MongoDB', value: 'mongodb' },
16
- { name: 'Redis', value: 'redis' }
17
- ],
37
+ choices: DATABASE_DIALECT_DEFINITIONS.map((definition) => ({
38
+ name: databaseChoiceLabel(definition),
39
+ value: definition.dialect,
40
+ })),
18
41
  default: 'sqlite'
19
42
  }
20
43
  ]);
21
44
 
45
+ let config: DatabaseConfig;
22
46
  switch (dialect) {
23
47
  case 'sqlite':
24
- return await configureSQLite();
48
+ config = await configureSQLite();
49
+ break;
25
50
  case 'mysql':
26
- return await configureMySQL();
51
+ config = await configureMySQL();
52
+ break;
27
53
  case 'pg':
28
- return await configurePostgreSQL();
54
+ config = await configurePostgreSQL();
55
+ break;
29
56
  case 'mongodb':
30
- return await configureMongoDB();
57
+ config = await configureMongoDB();
58
+ break;
31
59
  case 'redis':
32
- return await configureRedis();
60
+ config = await configureRedis();
61
+ break;
62
+ case 'memory':
63
+ config = { dialect: 'memory' };
64
+ break;
33
65
  default:
34
66
  throw new Error(`未支持的数据库类型: ${dialect}`);
35
67
  }
68
+ const issues = validateDatabaseConfig(config);
69
+ if (issues.length > 0) {
70
+ throw new TypeError(`数据库配置不符合 Schema:${issues.map((issue) => issue.message).join(';')}`);
71
+ }
72
+ return config;
36
73
  }
37
74
 
38
75
  // SQLite 配置
@@ -42,7 +79,7 @@ async function configureSQLite(): Promise<DatabaseConfig> {
42
79
  type: 'input',
43
80
  name: 'filename',
44
81
  message: 'SQLite 数据库文件路径:',
45
- default: './data/bot.db',
82
+ default: schemaDefault('sqlite', 'filename'),
46
83
  validate: (input: string) => {
47
84
  if (!input.trim()) return '文件路径不能为空';
48
85
  return true;
@@ -57,7 +94,7 @@ async function configureSQLite(): Promise<DatabaseConfig> {
57
94
  { name: 'DELETE (默认)', value: 'delete' },
58
95
  { name: 'MEMORY (内存数据库)', value: 'memory' }
59
96
  ],
60
- default: 'wal'
97
+ default: schemaDefault('sqlite', 'mode')
61
98
  }
62
99
  ]);
63
100
 
@@ -75,26 +112,23 @@ async function configureMySQL(): Promise<DatabaseConfig> {
75
112
  type: 'input',
76
113
  name: 'host',
77
114
  message: 'MySQL 主机地址:',
78
- default: 'localhost'
115
+ default: schemaDefault('mysql', 'host')
79
116
  },
80
117
  {
81
118
  type: 'input',
82
119
  name: 'port',
83
120
  message: 'MySQL 端口:',
84
- default: '3306',
121
+ default: String(schemaDefault('mysql', 'port')),
85
122
  validate: (input: string) => {
86
- const port = parseInt(input);
87
- if (isNaN(port) || port < 1 || port > 65535) {
88
- return '请输入有效的端口号 (1-65535)';
89
- }
90
- return true;
123
+ const value = parseIntegerInput(input, 1, 65535);
124
+ return typeof value === 'number' ? true : value;
91
125
  }
92
126
  },
93
127
  {
94
128
  type: 'input',
95
129
  name: 'user',
96
130
  message: 'MySQL 用户名:',
97
- default: 'root'
131
+ default: schemaDefault('mysql', 'user')
98
132
  },
99
133
  {
100
134
  type: 'password',
@@ -105,7 +139,7 @@ async function configureMySQL(): Promise<DatabaseConfig> {
105
139
  type: 'input',
106
140
  name: 'database',
107
141
  message: 'MySQL 数据库名:',
108
- default: 'zhin_bot',
142
+ default: schemaDefault('mysql', 'database'),
109
143
  validate: (input: string) => {
110
144
  if (!input.trim()) return '数据库名不能为空';
111
145
  return true;
@@ -116,7 +150,7 @@ async function configureMySQL(): Promise<DatabaseConfig> {
116
150
  return {
117
151
  dialect: 'mysql',
118
152
  host: config.host,
119
- port: parseInt(config.port),
153
+ port: Number(config.port),
120
154
  user: config.user,
121
155
  password: config.password,
122
156
  database: config.database
@@ -130,26 +164,23 @@ async function configurePostgreSQL(): Promise<DatabaseConfig> {
130
164
  type: 'input',
131
165
  name: 'host',
132
166
  message: 'PostgreSQL 主机地址:',
133
- default: 'localhost'
167
+ default: schemaDefault('pg', 'host')
134
168
  },
135
169
  {
136
170
  type: 'input',
137
171
  name: 'port',
138
172
  message: 'PostgreSQL 端口:',
139
- default: '5432',
173
+ default: String(schemaDefault('pg', 'port')),
140
174
  validate: (input: string) => {
141
- const port = parseInt(input);
142
- if (isNaN(port) || port < 1 || port > 65535) {
143
- return '请输入有效的端口号 (1-65535)';
144
- }
145
- return true;
175
+ const value = parseIntegerInput(input, 1, 65535);
176
+ return typeof value === 'number' ? true : value;
146
177
  }
147
178
  },
148
179
  {
149
180
  type: 'input',
150
181
  name: 'user',
151
182
  message: 'PostgreSQL 用户名:',
152
- default: 'postgres'
183
+ default: schemaDefault('pg', 'user')
153
184
  },
154
185
  {
155
186
  type: 'password',
@@ -160,7 +191,7 @@ async function configurePostgreSQL(): Promise<DatabaseConfig> {
160
191
  type: 'input',
161
192
  name: 'database',
162
193
  message: 'PostgreSQL 数据库名:',
163
- default: 'zhin_bot',
194
+ default: schemaDefault('pg', 'database'),
164
195
  validate: (input: string) => {
165
196
  if (!input.trim()) return '数据库名不能为空';
166
197
  return true;
@@ -171,7 +202,7 @@ async function configurePostgreSQL(): Promise<DatabaseConfig> {
171
202
  return {
172
203
  dialect: 'pg',
173
204
  host: config.host,
174
- port: parseInt(config.port),
205
+ port: Number(config.port),
175
206
  user: config.user,
176
207
  password: config.password,
177
208
  database: config.database
@@ -185,7 +216,7 @@ async function configureMongoDB(): Promise<DatabaseConfig> {
185
216
  type: 'input',
186
217
  name: 'url',
187
218
  message: 'MongoDB 连接字符串:',
188
- default: 'mongodb://localhost:27017',
219
+ default: schemaDefault('mongodb', 'url'),
189
220
  validate: (input: string) => {
190
221
  if (!input.trim()) return '连接字符串不能为空';
191
222
  if (!input.startsWith('mongodb://') && !input.startsWith('mongodb+srv://')) {
@@ -198,7 +229,7 @@ async function configureMongoDB(): Promise<DatabaseConfig> {
198
229
  type: 'input',
199
230
  name: 'dbName',
200
231
  message: 'MongoDB 数据库名:',
201
- default: 'zhin_bot',
232
+ default: schemaDefault('mongodb', 'dbName'),
202
233
  validate: (input: string) => {
203
234
  if (!input.trim()) return '数据库名不能为空';
204
235
  return true;
@@ -220,19 +251,16 @@ async function configureRedis(): Promise<DatabaseConfig> {
220
251
  type: 'input',
221
252
  name: 'host',
222
253
  message: 'Redis 主机地址:',
223
- default: 'localhost'
254
+ default: schemaDefault('redis', 'socket', 'host')
224
255
  },
225
256
  {
226
257
  type: 'input',
227
258
  name: 'port',
228
259
  message: 'Redis 端口:',
229
- default: '6379',
260
+ default: String(schemaDefault('redis', 'socket', 'port')),
230
261
  validate: (input: string) => {
231
- const port = parseInt(input);
232
- if (isNaN(port) || port < 1 || port > 65535) {
233
- return '请输入有效的端口号 (1-65535)';
234
- }
235
- return true;
262
+ const value = parseIntegerInput(input, 1, 65535);
263
+ return typeof value === 'number' ? true : value;
236
264
  }
237
265
  },
238
266
  {
@@ -244,13 +272,10 @@ async function configureRedis(): Promise<DatabaseConfig> {
244
272
  type: 'input',
245
273
  name: 'db',
246
274
  message: 'Redis 数据库索引:',
247
- default: '0',
275
+ default: String(schemaDefault('redis', 'database')),
248
276
  validate: (input: string) => {
249
- const db = parseInt(input);
250
- if (isNaN(db) || db < 0) {
251
- return '请输入有效的数据库索引 (>= 0)';
252
- }
253
- return true;
277
+ const value = parseIntegerInput(input, 0);
278
+ return typeof value === 'number' ? true : '请输入有效的数据库索引 (>= 0)';
254
279
  }
255
280
  }
256
281
  ]);
@@ -259,9 +284,9 @@ async function configureRedis(): Promise<DatabaseConfig> {
259
284
  dialect: 'redis',
260
285
  socket: {
261
286
  host: config.host,
262
- port: parseInt(config.port)
287
+ port: Number(config.port)
263
288
  },
264
289
  password: config.password || undefined,
265
- database: parseInt(config.db)
290
+ database: Number(config.db)
266
291
  };
267
- }
292
+ }
package/src/env.ts CHANGED
@@ -104,6 +104,7 @@ export function generateDatabaseEnvVars(config: DatabaseConfig): string {
104
104
  );
105
105
  break;
106
106
  case 'sqlite':
107
+ case 'memory':
107
108
  default:
108
109
  // SQLite 不需要额外的环境变量
109
110
  break;
@@ -111,3 +112,54 @@ export function generateDatabaseEnvVars(config: DatabaseConfig): string {
111
112
 
112
113
  return envVars.length > 0 ? `\n\n${envVars.join('\n')}` : '';
113
114
  }
115
+
116
+ /**
117
+ * Generate a version-control-safe database section for `.env.example`.
118
+ * User-entered passwords and credential-bearing connection URLs are never
119
+ * copied from the interactive wizard.
120
+ */
121
+ export function generateDatabaseEnvExample(config: DatabaseConfig): string {
122
+ let lines: string[] = [];
123
+ switch (config.dialect) {
124
+ case 'mysql':
125
+ lines = [
126
+ '# MySQL 数据库配置',
127
+ 'DB_HOST=127.0.0.1',
128
+ 'DB_PORT=3306',
129
+ 'DB_USER=root',
130
+ 'DB_PASSWORD=change-me',
131
+ 'DB_DATABASE=zhin_bot',
132
+ ];
133
+ break;
134
+ case 'pg':
135
+ lines = [
136
+ '# PostgreSQL 数据库配置',
137
+ 'DB_HOST=127.0.0.1',
138
+ 'DB_PORT=5432',
139
+ 'DB_USER=postgres',
140
+ 'DB_PASSWORD=change-me',
141
+ 'DB_DATABASE=zhin_bot',
142
+ ];
143
+ break;
144
+ case 'mongodb':
145
+ lines = [
146
+ '# MongoDB 数据库配置',
147
+ 'DB_URL=mongodb://127.0.0.1:27017',
148
+ 'DB_NAME=zhin_bot',
149
+ ];
150
+ break;
151
+ case 'redis':
152
+ lines = [
153
+ '# Redis 数据库配置',
154
+ 'REDIS_HOST=127.0.0.1',
155
+ 'REDIS_PORT=6379',
156
+ 'REDIS_PASSWORD=',
157
+ 'REDIS_DB=0',
158
+ ];
159
+ break;
160
+ case 'sqlite':
161
+ case 'memory':
162
+ return '';
163
+ }
164
+ return `\n\n${lines.join('\n')}`;
165
+ }