worsoft-frontend-codegen-local-mcp 0.1.9 → 0.1.11

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 (2) hide show
  1. package/mcp_server.js +1814 -1816
  2. package/package.json +1 -1
package/mcp_server.js CHANGED
@@ -1,1818 +1,1816 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
-
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
7
  const SERVER_NAME = 'worsoft-codegen-local';
8
- const SERVER_VERSION = '0.1.9';
9
- const PROTOCOL_VERSION = '2024-11-05';
10
- const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
- const TEMPLATE_LIBRARY_ROOT = path.resolve(__dirname, '..', 'template');
12
- const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
13
- const DEFAULT_DESIGN_FILE = path.resolve(__dirname, '..', 'sql', 'SQL 璁捐璇存槑.md');
14
- const STYLE_CATALOG = loadStyleCatalog();
15
- const DEFAULT_DICT_REGISTRY_KEYS = {
16
- add_start_stop: 'COMMON_STATUS',
17
- trade_standard_type: 'TRADE_STANDARD_TYPE',
18
- trade_score_standard: 'TRADE_SCORE_STANDARD',
19
- };
20
- const DEFAULT_CRUD_SCHEMA_TEMPLATE = `export interface FieldMeta {
21
- show: boolean;
22
- alwaysHide: boolean;
23
- smart: boolean;
24
- labelKey: string;
25
- label?: string;
26
- width: string;
27
- dictType?: string;
28
- }
29
-
30
- export interface FieldConfig {
31
- key: string;
32
- labelKey?: string;
33
- label?: string;
34
- width?: string;
35
- show?: boolean;
36
- alwaysHide?: boolean;
37
- smart?: boolean;
38
- dictType?: string;
39
- }
40
-
41
- export interface CrudSchemaDefinition {
42
- master: FieldConfig[];
43
- children?: Record<string, FieldConfig[]>;
44
- }
45
-
46
- export interface CrudSchema {
47
- master: Record<string, FieldMeta>;
48
- children: Record<string, Record<string, FieldMeta>>;
49
- filterTypes: Record<string, number>;
50
- childFilterTypes: Record<string, Record<string, number>>;
51
- allDictTypes: string[];
52
- }
53
-
54
- const DEFAULT_WIDTH = '120';
55
-
56
- export const field = (labelKey: string, width = DEFAULT_WIDTH): FieldMeta => ({
57
- show: true,
58
- alwaysHide: false,
59
- smart: false,
60
- labelKey,
61
- width,
62
- });
63
-
64
- export const dictField = (labelKey: string, dictType: string, width = DEFAULT_WIDTH): FieldMeta => ({
65
- ...field(labelKey, width),
66
- dictType,
67
- });
68
-
69
- export const buildFilterTypes = (fields: Record<string, FieldMeta>) =>
70
- Object.fromEntries(Object.entries(fields).filter(([, item]) => item.dictType).map(([key]) => [key, 30]));
71
-
72
- export const collectDictTypes = (...groups: Array<Record<string, FieldMeta>>) =>
73
- Array.from(
74
- new Set(
75
- groups.flatMap((group) =>
76
- Object.values(group)
77
- .map((item) => item.dictType)
78
- .filter(Boolean) as string[]
79
- )
80
- )
81
- );
82
-
83
- const normalizeField = (item: FieldConfig): FieldMeta => ({
84
- show: item.show ?? true,
85
- alwaysHide: item.alwaysHide ?? false,
86
- smart: item.smart ?? false,
87
- labelKey: item.labelKey ?? item.label ?? item.key,
88
- ...(item.label ? { label: item.label } : {}),
89
- width: item.width ?? DEFAULT_WIDTH,
90
- ...(item.dictType ? { dictType: item.dictType } : {}),
91
- });
92
-
93
- const toFieldMap = (fields: FieldConfig[]) =>
94
- Object.fromEntries(fields.map((item) => [item.key, normalizeField(item)])) as Record<string, FieldMeta>;
95
-
96
- const buildSchema = (
97
- master: Record<string, FieldMeta>,
98
- children: Record<string, Record<string, FieldMeta>> = {}
99
- ): CrudSchema => ({
100
- master,
101
- children,
102
- filterTypes: buildFilterTypes(master),
103
- childFilterTypes: Object.fromEntries(
104
- Object.entries(children).map(([key, fields]) => [key, buildFilterTypes(fields)])
105
- ),
106
- allDictTypes: collectDictTypes(master, ...Object.values(children)),
107
- });
108
-
109
- export function createCrudSchema(master: Record<string, FieldMeta>, children?: Record<string, Record<string, FieldMeta>>): CrudSchema;
110
- export function createCrudSchema(definition: CrudSchemaDefinition): CrudSchema;
111
- export function createCrudSchema(
112
- masterOrDefinition: Record<string, FieldMeta> | CrudSchemaDefinition,
113
- children: Record<string, Record<string, FieldMeta>> = {}
114
- ): CrudSchema {
115
- if ('master' in masterOrDefinition && Array.isArray(masterOrDefinition.master)) {
116
- const master = toFieldMap(masterOrDefinition.master);
117
- const childGroups = Object.fromEntries(
118
- Object.entries(masterOrDefinition.children ?? {}).map(([key, fields]) => [key, toFieldMap(fields)])
119
- ) as Record<string, Record<string, FieldMeta>>;
120
- return buildSchema(master, childGroups);
121
- }
122
-
123
- return buildSchema(masterOrDefinition as Record<string, FieldMeta>, children);
124
- }
125
- `;
126
-
127
- const TOOL_SCHEMA = {
128
- type: 'object',
129
- properties: {
130
- designFile: { type: 'string', description: 'Absolute or relative Markdown design file path. Defaults to ../sql/SQL 璁捐璇存槑.md when omitted.' },
131
- tableName: { type: 'string', description: 'Target main table name from the design file.' },
132
- style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Style id from assets/style-catalog.json.' },
133
- children: {
134
- type: 'array',
135
- description: 'Optional direct child-table relations for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form.',
136
- items: {
137
- type: 'object',
138
- properties: {
139
- childTableName: { type: 'string', description: 'Child table name.' },
140
- mainField: { type: 'string', description: 'Main table relation field.' },
141
- childField: { type: 'string', description: 'Child table relation field.' },
142
- relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
143
- },
144
- required: ['childTableName', 'mainField', 'childField'],
145
- additionalProperties: false,
146
- },
147
- },
148
- childTableName: { type: 'string', description: 'Child table name. Required when style=master_child_jump.' },
149
- mainField: { type: 'string', description: 'Main table relation field. Required when style=master_child_jump.' },
150
- childField: { type: 'string', description: 'Child table relation field. Required when style=master_child_jump.' },
151
- relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
152
- frontendPath: { type: 'string', description: 'Absolute frontend output root path.' },
153
- moduleName: { type: 'string', description: 'Relative frontend module path, for example admin/test.' },
154
- writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
155
- overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
156
- },
157
- required: ['tableName', 'style', 'frontendPath'],
158
- additionalProperties: false,
159
- };
160
-
161
- function loadStyleCatalog() {
162
- return JSON.parse(fs.readFileSync(STYLE_CATALOG_PATH, 'utf8'));
163
- }
164
-
165
- function writeMessage(payload) {
166
- process.stdout.write(JSON.stringify(payload) + '\n');
167
- }
168
-
169
- function successResponse(id, result) {
170
- return { jsonrpc: '2.0', id, result };
171
- }
172
-
173
- function errorResponse(id, code, message) {
174
- return { jsonrpc: '2.0', id, error: { code, message } };
175
- }
176
-
177
- function toolTextResult(text) {
178
- return { content: [{ type: 'text', text }], isError: false };
179
- }
180
-
181
- function toCamelCase(value) {
182
- return value.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
183
- }
184
-
185
- function toPascalCase(value) {
186
- const camel = toCamelCase(value);
187
- return camel.charAt(0).toUpperCase() + camel.slice(1);
188
- }
189
-
190
- function normalizeModuleName(moduleName) {
191
- if (!moduleName) {
192
- return 'admin/test';
193
- }
194
- return moduleName.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
195
- }
196
-
197
- function toConstantCase(value) {
198
- return String(value || '')
199
- .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
200
- .replace(/[^a-zA-Z0-9]+/g, '_')
201
- .replace(/^_+|_+$/g, '')
202
- .toUpperCase();
203
- }
204
-
205
- function parseDictRegistryEntries(fileContent) {
206
- const blockMatch = String(fileContent || '').match(/export const DictRegistry\s*=\s*{([\s\S]*?)}\s*as const;/);
207
- if (!blockMatch) return [];
208
-
209
- const entries = [];
210
- const entryRegex = /^\s*([A-Z0-9_]+)\s*:\s*['"]([^'"]+)['"]\s*,?\s*$/gm;
211
- let match = entryRegex.exec(blockMatch[1]);
212
- while (match) {
213
- entries.push({ key: match[1], value: match[2] });
214
- match = entryRegex.exec(blockMatch[1]);
215
- }
216
- return entries;
217
- }
218
-
219
- function renderDictRegistryContent(entries) {
220
- const lines = entries.map((entry) => ` ${entry.key}: '${entry.value}',`);
221
- return [
222
- 'export const DictRegistry = {',
223
- ...lines,
224
- '} as const;',
225
- '',
226
- 'export type DictRegistryKey = keyof typeof DictRegistry;',
227
- 'export type DictType = (typeof DictRegistry)[DictRegistryKey];',
228
- '',
229
- ].join('\n');
230
- }
231
-
232
- function isPlainObject(value) {
233
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
234
- }
235
-
236
- function parseExportDefaultObject(fileContent) {
237
- const source = String(fileContent || '').replace(/^\uFEFF/, '').trim();
238
- if (!source) return null;
239
- try {
240
- const executable = source.replace(/^export\s+default/, 'return');
241
- return new Function(executable)();
242
- } catch (error) {
243
- return null;
244
- }
245
- }
246
-
247
- function deepMergeMissing(target, source) {
248
- if (!isPlainObject(source)) {
249
- return target === undefined ? source : target;
250
- }
251
-
252
- const result = isPlainObject(target) ? { ...target } : {};
253
- for (const [key, value] of Object.entries(source)) {
254
- if (result[key] === undefined) {
255
- result[key] = value;
256
- continue;
257
- }
258
- if (isPlainObject(result[key]) && isPlainObject(value)) {
259
- result[key] = deepMergeMissing(result[key], value);
260
- }
261
- }
262
- return result;
263
- }
264
-
265
- function escapeTsString(value) {
266
- return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
267
- }
268
-
269
- function renderTsLiteral(value, indentLevel = 0) {
270
- const indent = ' '.repeat(indentLevel);
271
- const childIndent = ' '.repeat(indentLevel + 1);
272
-
273
- if (typeof value === 'string') {
274
- return `'${escapeTsString(value)}'`;
275
- }
276
- if (typeof value === 'number' || typeof value === 'boolean') {
277
- return String(value);
278
- }
279
- if (Array.isArray(value)) {
280
- if (!value.length) return '[]';
281
- return ['[', ...value.map((item) => `${childIndent}${renderTsLiteral(item, indentLevel + 1)},`), `${indent}]`].join('\n');
282
- }
283
- if (isPlainObject(value)) {
284
- const entries = Object.entries(value);
285
- if (!entries.length) return '{}';
286
- return [
287
- '{',
288
- ...entries.map(([key, item]) => {
289
- const renderedKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${escapeTsString(key)}'`;
290
- return `${childIndent}${renderedKey}: ${renderTsLiteral(item, indentLevel + 1)},`;
291
- }),
292
- `${indent}}`,
293
- ].join('\n');
294
- }
295
- return 'null';
296
- }
297
-
298
- function renderExportDefaultContent(objectValue) {
299
- return `export default ${renderTsLiteral(objectValue, 0)};\n`;
300
- }
301
-
302
- function getPreferredDictRegistryKey(dictType) {
303
- return DEFAULT_DICT_REGISTRY_KEYS[dictType] || toConstantCase(dictType) || 'DICT_TYPE';
304
- }
305
-
306
- function buildUniqueDictRegistryKey(dictType, usedKeys, existingByKey) {
307
- const preferred = getPreferredDictRegistryKey(dictType);
308
- let candidate = preferred;
309
- let index = 2;
310
-
311
- while (usedKeys.has(candidate) && existingByKey.get(candidate) !== dictType) {
312
- candidate = `${preferred}_${index}`;
313
- index += 1;
314
- }
315
-
316
- return candidate;
317
- }
318
-
319
- function buildI18nNamespaceSegments(model) {
320
- return [...model.moduleName.split('/').filter(Boolean), model.functionName];
321
- }
322
-
323
- function buildI18nNamespace(model) {
324
- return buildI18nNamespaceSegments(model).join('.');
325
- }
326
-
327
- function removeFeatureCommonLocaleSections(localeObject, model) {
328
- if (!isPlainObject(localeObject)) {
329
- return localeObject;
330
- }
331
-
332
- const segments = buildI18nNamespaceSegments(model);
333
- let cursor = localeObject;
334
- for (let index = 0; index < segments.length; index += 1) {
335
- const segment = segments[index];
336
- if (!isPlainObject(cursor[segment])) {
337
- return localeObject;
338
- }
339
- if (index === segments.length - 1) {
340
- delete cursor[segment].placeholders;
341
- delete cursor[segment].actions;
342
- delete cursor[segment].messages;
343
- return localeObject;
344
- }
345
- cursor = cursor[segment];
346
- }
347
-
348
- return localeObject;
349
- }
350
-
351
- function buildFieldLabelKey(model, field) {
352
- return `${buildI18nNamespace(model)}.fields.${field.attrName}`;
353
- }
354
-
355
- function buildChildFieldLabelKey(model, childModel, field) {
356
- return `${buildI18nNamespace(model)}.children.${childModel.listName}.fields.${field.attrName}`;
357
- }
358
-
359
- function buildChildSectionTitleKey(model, childModel) {
360
- return `${buildI18nNamespace(model)}.children.${childModel.listName}.title`;
361
- }
362
-
363
- function buildLocaleLeaf(model) {
364
- const leaf = {
365
- title: model.tableComment,
366
- fields: Object.fromEntries(model.visibleFields.map((field) => [field.attrName, stripDictAnnotation(field.comment)])),
367
- };
368
-
369
- if (model.children.length) {
370
- leaf.children = Object.fromEntries(
371
- model.children.map((childModel) => [
372
- childModel.listName,
373
- {
374
- title: childModel.tableComment,
375
- fields: Object.fromEntries(childModel.visibleFields.map((field) => [field.attrName, stripDictAnnotation(field.comment)])),
376
- },
377
- ])
378
- );
379
- }
380
-
381
- return leaf;
382
- }
383
-
384
- function buildZhCnLocaleObject(model) {
385
- const root = {};
386
- const segments = buildI18nNamespaceSegments(model);
387
- let cursor = root;
388
-
389
- for (let index = 0; index < segments.length - 1; index += 1) {
390
- const segment = segments[index];
391
- cursor[segment] = cursor[segment] || {};
392
- cursor = cursor[segment];
393
- }
394
-
395
- cursor[segments[segments.length - 1]] = buildLocaleLeaf(model);
396
- return root;
397
- }
398
-
399
- function prepareZhCnLocaleFile(model) {
400
- const localePath = path.join(model.frontendPath, 'src', 'i18n', 'biz', ...model.moduleName.split('/'), `${model.functionName}.zh-cn.ts`);
401
- const exists = fs.existsSync(localePath);
402
- const currentContent = exists ? readUtf8File(localePath) : '';
403
- const currentObject = exists ? parseExportDefaultObject(currentContent) : null;
404
- const generatedObject = buildZhCnLocaleObject(model);
405
- const isCompatible = !exists || isPlainObject(currentObject);
406
- const sanitizedCurrentObject = isCompatible ? removeFeatureCommonLocaleSections(currentObject || {}, model) : null;
407
- const mergedObject = isCompatible ? deepMergeMissing(sanitizedCurrentObject || {}, generatedObject) : null;
408
-
409
- return {
410
- path: localePath,
411
- frontendPath: model.frontendPath,
412
- exists,
413
- isCompatible,
414
- namespace: buildI18nNamespace(model),
415
- content: mergedObject ? renderExportDefaultContent(mergedObject) : '',
416
- needsWrite: !exists || (isCompatible && renderExportDefaultContent(currentObject || {}) !== renderExportDefaultContent(mergedObject)),
417
- };
418
- }
419
-
420
- function prepareDictRegistry(frontendPath, dictTypes) {
421
- const registryPath = path.join(frontendPath, 'src', 'enums', 'dict-registry.ts');
422
- const exists = fs.existsSync(registryPath);
423
- const existingEntries = exists ? parseDictRegistryEntries(readUtf8File(registryPath)) : [];
424
- const entries = existingEntries.map((entry) => ({ ...entry }));
425
- const keyByValue = new Map(entries.map((entry) => [entry.value, entry.key]));
426
- const existingByKey = new Map(entries.map((entry) => [entry.key, entry.value]));
427
- const usedKeys = new Set(entries.map((entry) => entry.key));
428
- let changed = !exists;
429
-
430
- for (const dictType of dictTypes) {
431
- if (!dictType || keyByValue.has(dictType)) continue;
432
- const key = buildUniqueDictRegistryKey(dictType, usedKeys, existingByKey);
433
- entries.push({ key, value: dictType });
434
- keyByValue.set(dictType, key);
435
- existingByKey.set(key, dictType);
436
- usedKeys.add(key);
437
- changed = true;
438
- }
439
-
440
- return {
441
- path: registryPath,
442
- entries,
443
- keyByValue,
444
- needsWrite: changed,
445
- };
446
- }
447
-
448
- function ensureCrudSchemaSupportFile(frontendPath) {
449
- const schemaPath = path.join(frontendPath, 'src', 'utils', 'crudSchema.ts');
450
- const exists = fs.existsSync(schemaPath);
451
- const currentContent = exists ? readUtf8File(schemaPath) : '';
452
- const hasCoreShape =
453
- currentContent.includes('export interface CrudSchemaDefinition') &&
454
- currentContent.includes('export function createCrudSchema') &&
455
- currentContent.includes('export interface FieldConfig');
456
- const supportsLabelKey = currentContent.includes('labelKey');
457
- const shouldUpgradeLegacy = exists && hasCoreShape && !supportsLabelKey;
458
-
459
- return {
460
- path: schemaPath,
461
- content: DEFAULT_CRUD_SCHEMA_TEMPLATE,
462
- exists,
463
- isCompatible: hasCoreShape && supportsLabelKey,
464
- needsWrite: !exists || shouldUpgradeLegacy,
465
- };
466
- }
467
-
468
- function ensureDirectory(filePath) {
469
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
470
- }
471
-
472
- function writeSupportFile(filePath, content) {
473
- ensureDirectory(filePath);
474
- fs.writeFileSync(filePath, content, 'utf8');
475
- }
476
-
477
- function prepareSharedSupport(frontendPath, dictTypes) {
478
- const normalizedDictTypes = [...new Set((dictTypes || []).filter(Boolean))];
479
- const dictRegistry = prepareDictRegistry(frontendPath, normalizedDictTypes);
480
- const crudSchema = ensureCrudSchemaSupportFile(frontendPath);
481
- return {
482
- dictRegistry,
483
- crudSchema,
484
- };
485
- }
486
-
487
- function maybeWriteSharedSupport(sharedSupport, writeToDisk) {
488
- if (!writeToDisk) return;
489
-
490
- if (sharedSupport.crudSchema.needsWrite) {
491
- writeSupportFile(sharedSupport.crudSchema.path, sharedSupport.crudSchema.content);
492
- }
493
-
494
- if (sharedSupport.dictRegistry.needsWrite) {
495
- writeSupportFile(sharedSupport.dictRegistry.path, renderDictRegistryContent(sharedSupport.dictRegistry.entries));
496
- }
497
- }
498
-
499
- function buildSupportNote(sharedSupport, localeZhSupport) {
500
- const notes = [];
501
-
502
- if (sharedSupport.crudSchema.exists && !sharedSupport.crudSchema.isCompatible) {
503
- notes.push(
504
- 'Detected an existing src/utils/crudSchema.ts that does not match the expected helper signature. ' +
505
- 'MCP preserved the existing file and did not overwrite it. Generated pages now depend on that file being manually aligned.'
506
- );
507
- }
508
-
509
- if (localeZhSupport.exists && !localeZhSupport.isCompatible) {
510
- notes.push(
511
- `Detected an existing ${path.relative(localeZhSupport.frontendPath, localeZhSupport.path).replace(/\\/g, '/')} that MCP could not parse. ` +
512
- 'The file was preserved and not updated, so new Chinese i18n keys may need to be merged manually.'
513
- );
514
- }
515
-
516
- return notes.length ? notes.join(' ') : 'Runtime template rendering completed.';
517
- }
518
-
519
- function getDictRegistryReference(dictType, keyByValue) {
520
- if (!dictType) return '';
521
- const key = keyByValue.get(dictType) || getPreferredDictRegistryKey(dictType);
522
- return `DictRegistry.${key}`;
523
- }
524
-
525
- function isAuditField(fieldName) {
526
- return [
527
- 'id',
528
- 'tenant_id',
529
- 'version',
530
- 'create_time',
531
- 'update_time',
532
- 'create_user_id',
533
- 'update_user_id',
534
- 'create_user',
535
- 'update_user',
536
- 'create_pos_id',
537
- 'create_pos',
538
- 'create_dpt_id',
539
- 'create_dpt',
540
- 'create_ogn_id',
541
- 'create_ogn',
542
- 'create_psm_full_id',
543
- 'create_psm_full_name',
544
- 'update_psm_full_id',
545
- 'update_psm_full_name',
546
- 'del_flag',
547
- ].includes(fieldName);
548
- }
549
-
550
- function findDictType(comment) {
551
- return extractDictType(comment);
552
- const match = comment.match(/(?:瀛楀吀|dict)[锛?\s]*([a-zA-Z0-9_]+)/i);
553
- }
554
-
555
- function mapFieldType(field) {
556
- if (field.dictType) return 'select';
557
- if (field.sqlType === 'DATETIME' || field.sqlType === 'TIMESTAMP') return 'datetime';
558
- if (field.sqlType === 'DATE') return 'date';
559
- if (['INT', 'BIGINT', 'DECIMAL', 'NUMERIC'].includes(field.sqlType)) return 'number';
560
- if (field.sqlType === 'TEXT') return 'textarea';
561
- if (field.sqlType === 'VARCHAR' && field.length && Number(field.length) > 64) return 'textarea';
562
- return 'text';
563
- }
564
-
565
- function escapeForRegex(value) {
566
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
567
- }
568
-
569
- function stripBom(text) {
570
- return text.replace(/^\uFEFF/, '');
571
- }
572
-
573
- function readUtf8File(filePath) {
574
- return stripBom(fs.readFileSync(filePath, 'utf8'));
575
- }
576
-
577
- function normalizeDictType(value) {
578
- const normalized = String(value || '').trim();
579
- if (!normalized || normalized === '-' || normalized === '/') {
580
- return null;
581
- }
582
- return normalized.replace(/^['"`]+|['"`]+$/g, '');
583
- }
584
-
585
- function stripDictAnnotation(label) {
586
- const text = String(label || '').trim();
587
- if (!text) {
588
- return '';
589
- }
590
- return text
591
- .replace(/\s*[锛?]\s*(?:瀛楀吀|dict)(?:绫诲瀷|type)?\s*[:锛?]?\s*[a-zA-Z0-9_-]+\s*[锛?]\s*/gi, '')
592
- .replace(/\s+/g, ' ')
593
- .trim();
594
- }
595
-
596
- function extractDictType(text) {
597
- const normalized = String(text || '').trim();
598
- if (!normalized) {
599
- return null;
600
- }
601
-
602
- const patterns = [
603
- /(?:瀛楀吀|dict)(?:绫诲瀷|type)?\s*[:锛?]\s*([a-zA-Z0-9_-]+)/i,
604
- /(?:瀛楀吀|dict)(?:绫诲瀷|type)?\s*[锛?]\s*([a-zA-Z0-9_-]+)\s*[锛?]/i,
605
- /[锛?]\s*(?:瀛楀吀|dict)(?:绫诲瀷|type)?\s*[:锛?]?\s*([a-zA-Z0-9_-]+)\s*[锛?]/i,
606
- ];
607
-
608
- for (const pattern of patterns) {
609
- const match = normalized.match(pattern);
610
- if (match && match[1]) {
611
- return normalizeDictType(match[1]);
612
- }
613
- }
614
-
615
- return null;
616
- }
617
-
618
- function resolveDictType(comment, explicitDictType) {
619
- return normalizeDictType(explicitDictType) || extractDictType(comment);
620
- }
621
-
622
- function detectDictType(comment) {
623
- return extractDictType(comment);
624
- return findDictType(comment) || ((comment.match(/(?:瀛楀吀|dict)\s*[:锛歕(锛圿?\s*([a-zA-Z0-9_]+)/i) || [])[1] ?? null);
625
- }
626
-
627
- function splitLength(value) {
628
- const normalized = String(value || '').trim();
629
- if (!normalized || normalized === '-' || normalized === '/') {
630
- return { length: '', scale: '' };
631
- }
632
- const parts = normalized.split(',').map((item) => item.trim()).filter(Boolean);
633
- return { length: parts[0] || '', scale: parts[1] || '' };
634
- }
635
-
636
- function normalizeDefaultValue(value) {
637
- const normalized = String(value || '').trim();
638
- if (!normalized || normalized === '-' || normalized === '/') {
639
- return '';
640
- }
641
- return normalized;
642
- }
643
-
644
- function parseRequiredFlag(value) {
645
- const normalized = String(value || '').trim().toLowerCase();
646
- return ['鏄?, 'y', 'yes', '1', 'true', '蹇呭~'].includes(normalized);
647
- }
648
-
649
- function parseMarkdownRow(line) {
650
- const trimmed = line.trim();
651
- if (!trimmed.startsWith('|')) return [];
652
- return trimmed
653
- .slice(1, trimmed.endsWith('|') ? -1 : undefined)
654
- .split('|')
655
- .map((cell) => cell.trim());
656
- }
657
-
658
- function isMarkdownSeparatorRow(cells) {
659
- return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
660
- }
661
-
662
- function findHeaderIndex(headers, patterns) {
663
- return headers.findIndex((header) => patterns.some((pattern) => pattern.test(header)));
664
- }
665
-
666
- function findPrimaryKeyFromText(text, fields) {
667
- const quotedMatch = text.match(/PRIMARY\s+KEY\s*\(\s*`?([a-zA-Z0-9_]+)`?\s*\)/i);
668
- if (quotedMatch) {
669
- return quotedMatch[1];
670
- }
671
-
672
- const commentPk = fields.find((field) => /涓婚敭/i.test(field.comment));
673
- if (commentPk) {
674
- return commentPk.fieldName;
675
- }
676
-
677
- const idField = fields.find((field) => field.fieldName === 'id');
678
- if (idField) {
679
- return idField.fieldName;
680
- }
681
-
682
- return fields[0] ? fields[0].fieldName : null;
683
- }
684
-
685
- function parseMarkdownTableSection(tableName, tableComment, sectionLines) {
686
- const firstTableLineIndex = sectionLines.findIndex((line) => line.trim().startsWith('|'));
687
- if (firstTableLineIndex < 0) {
688
- throw new Error('Could not find markdown field table for ' + tableName);
689
- }
690
-
691
- const tableLines = [];
692
- for (let index = firstTableLineIndex; index < sectionLines.length; index += 1) {
693
- const line = sectionLines[index].trim();
694
- if (!line.startsWith('|')) break;
695
- tableLines.push(line);
696
- }
697
-
698
- const rows = tableLines.map(parseMarkdownRow).filter((cells) => cells.length > 0);
699
- if (rows.length < 3) {
700
- throw new Error('Markdown field table is incomplete for ' + tableName);
701
- }
702
-
703
- const headers = rows[0];
704
- const dataRows = rows.slice(1).filter((cells) => !isMarkdownSeparatorRow(cells));
705
- const fieldNameIndex = findHeaderIndex(headers, [/瀛楁鍚?, /鍒楀悕/i, /field/i]);
706
- const sqlTypeIndex = findHeaderIndex(headers, [/绫诲瀷/, /鏁版嵁绫诲瀷/, /type/i]);
707
- const lengthIndex = findHeaderIndex(headers, [/闀垮害/, /绮惧害/, /length/i]);
708
- const requiredIndex = findHeaderIndex(headers, [/蹇呭~/, /蹇呰緭/, /required/i]);
709
- const defaultIndex = findHeaderIndex(headers, [/榛樿/, /default/i]);
710
- const commentIndex = findHeaderIndex(headers, [/璇存槑/, /澶囨敞/, /comment/i]);
711
-
712
- const dictTypeIndex = findHeaderIndex(headers, [/瀛楀吀/, /dict/i, /dict_?type/i]);
713
-
714
- if (fieldNameIndex < 0 || sqlTypeIndex < 0) {
715
- throw new Error('Markdown field table headers are missing required columns for ' + tableName);
716
- }
717
-
718
- const fields = [];
719
- for (const row of dataRows) {
720
- const fieldName = String(row[fieldNameIndex] || '').trim();
721
- if (!fieldName || fieldName === '-') continue;
722
-
723
- const sqlType = String(row[sqlTypeIndex] || '').trim().toUpperCase();
724
- if (!sqlType) continue;
725
-
726
- const lengthRaw = lengthIndex >= 0 ? row[lengthIndex] : '';
727
- const comment = String(commentIndex >= 0 ? row[commentIndex] : '').trim() || fieldName;
728
- const explicitDictType = dictTypeIndex >= 0 ? row[dictTypeIndex] : '';
729
- const { length, scale } = splitLength(lengthRaw);
730
-
731
- fields.push({
732
- fieldName,
733
- attrName: toCamelCase(fieldName),
734
- sqlType,
735
- length,
736
- scale,
737
- comment,
738
- dictType: resolveDictType(comment, explicitDictType),
739
- notNull: requiredIndex >= 0 ? parseRequiredFlag(row[requiredIndex]) : false,
740
- defaultValue: defaultIndex >= 0 ? normalizeDefaultValue(row[defaultIndex]) : '',
741
- });
742
- }
743
-
744
- if (!fields.length) {
745
- throw new Error('No fields were parsed from markdown table ' + tableName);
746
- }
747
-
748
- const sectionText = sectionLines.join('\n');
749
- const pkName = findPrimaryKeyFromText(sectionText, fields);
750
- const pkField = fields.find((field) => field.fieldName === pkName) || fields[0];
751
- return { pkField, fields, tableComment };
752
- }
753
-
754
- function parseMarkdownDesignTables(markdownText) {
755
- const lines = stripBom(markdownText).replace(/\r\n?/g, '\n').split('\n');
756
- const tables = new Map();
757
-
758
- for (let index = 0; index < lines.length; index += 1) {
759
- const heading = lines[index].trim();
760
- const headingMatch = heading.match(/^###\s+\S+\s+([a-zA-Z0-9_]+)\s*[\(锛圿([^)\n锛塢+)[\)锛塢/);
761
- if (!headingMatch) continue;
762
-
763
- const tableName = headingMatch[1];
764
- const tableComment = headingMatch[2].trim();
765
- const sectionLines = [];
766
- let nextIndex = index + 1;
767
- while (nextIndex < lines.length && !/^(##|###)\s+/.test(lines[nextIndex])) {
768
- sectionLines.push(lines[nextIndex]);
769
- nextIndex += 1;
770
- }
771
-
772
- tables.set(tableName, parseMarkdownTableSection(tableName, tableComment, sectionLines));
773
- index = nextIndex - 1;
774
- }
775
-
776
- return tables;
777
- }
778
-
779
- function extractIdentifiersFromLine(line) {
780
- return [...String(line || '').matchAll(/`?([a-zA-Z][a-zA-Z0-9_]*)`?/g)].map((match) => match[1]);
781
- }
782
-
783
- function stripMarkdownSyntax(text) {
784
- return String(text || '')
785
- .replace(/\*\*/g, '')
786
- .replace(/`/g, '')
787
- .replace(/\r\n?/g, '\n');
788
- }
789
-
790
- function parseMarkdownRelations(markdownText) {
791
- const text = stripMarkdownSyntax(markdownText);
792
- const blocks = [];
793
- const headingRegex = /^(###|####)\s+(.+)$/gm;
794
- let current = null;
795
- let match = headingRegex.exec(text);
796
-
797
- while (match) {
798
- if (current) {
799
- current.body = text.slice(current.start, match.index);
800
- blocks.push(current);
801
- }
802
-
803
- current = {
804
- title: match[2].trim(),
805
- start: headingRegex.lastIndex,
806
- body: '',
807
- };
808
- match = headingRegex.exec(text);
809
- }
810
-
811
- if (current) {
812
- current.body = text.slice(current.start);
813
- blocks.push(current);
814
- }
815
-
816
- const relations = [];
817
- for (const block of blocks) {
818
- const lines = block.body.split('\n').map((line) => line.trim()).filter(Boolean);
819
- const mainLine = lines.find((line) => /(涓昏〃|鐖惰〃)\s*[:锛歖/.test(line));
820
- const childLine = lines.find((line) => /(浠庤〃|瀛愯〃)\s*[:锛歖/.test(line));
821
- const fieldLine = lines.find((line) => /鍏宠仈瀛楁\s*[:锛歖/.test(line));
822
- if (!mainLine || !childLine || !fieldLine) continue;
823
-
824
- const mainIdentifiers = extractIdentifiersFromLine(mainLine);
825
- const childIdentifiers = extractIdentifiersFromLine(childLine);
826
- const fieldIdentifiers = extractIdentifiersFromLine(fieldLine);
827
- if (!mainIdentifiers.length || !childIdentifiers.length || fieldIdentifiers.length < 2) continue;
828
-
829
- const relationTypeLine = lines.find((line) => /鍏崇郴绫诲瀷\s*[:锛歖/.test(line));
830
- relations.push({
831
- title: block.title,
832
- mainTableName: mainIdentifiers[0],
833
- childTableName: childIdentifiers[0],
834
- childField: fieldIdentifiers[0],
835
- mainField: fieldIdentifiers[1],
836
- relationType: relationTypeLine ? relationTypeLine.split(/[:锛歖/).slice(1).join(':').trim() : '',
837
- });
838
- }
839
-
840
- return relations;
841
- }
842
-
843
- function parseMarkdownDesignFile(markdownText) {
844
- return {
845
- tables: parseMarkdownDesignTables(markdownText),
846
- relations: [],
847
- };
848
- }
849
-
850
- function parseTableFromMarkdownDesign(designDoc, tableName) {
851
- const parsed = designDoc.tables.get(tableName);
852
- if (!parsed) {
853
- throw new Error('Could not find table definition for ' + tableName + ' in Markdown design file');
854
- }
855
- return parsed;
856
- }
857
-
858
- function buildQuestionMarkSegmentRegex(segment) {
859
- return new RegExp('^' + escapeForRegex(segment).replace(/\\\?/g, '.') + '$', 'i');
860
- }
861
-
862
- function tryResolveGarbledPath(resolvedPath) {
863
- if (!resolvedPath.includes('?')) {
864
- return null;
865
- }
866
-
867
- const parsed = path.parse(resolvedPath);
868
- const segments = resolvedPath.slice(parsed.root.length).split(path.sep).filter(Boolean);
869
- let currentPath = parsed.root;
870
-
871
- for (const segment of segments) {
872
- const exactPath = path.join(currentPath, segment);
873
- if (fs.existsSync(exactPath)) {
874
- currentPath = exactPath;
875
- continue;
876
- }
877
-
878
- if (!segment.includes('?') || !fs.existsSync(currentPath) || !fs.statSync(currentPath).isDirectory()) {
879
- return null;
880
- }
881
-
882
- const matcher = buildQuestionMarkSegmentRegex(segment);
883
- const matches = fs.readdirSync(currentPath).filter((name) => matcher.test(name));
884
- if (matches.length !== 1) {
885
- throw new Error(
886
- 'Source file path could not be resolved uniquely from garbled input: ' +
887
- resolvedPath +
888
- '. Matched entries: ' +
889
- (matches.length ? matches.join(', ') : 'none')
890
- );
891
- }
892
-
893
- currentPath = path.join(currentPath, matches[0]);
894
- }
895
-
896
- return fs.existsSync(currentPath) ? currentPath : null;
897
- }
898
-
899
- function resolveSourcePath(inputPath) {
900
- const resolvedPath = path.resolve(inputPath);
901
- if (fs.existsSync(resolvedPath)) {
902
- return resolvedPath;
903
- }
904
-
905
- const recoveredPath = tryResolveGarbledPath(resolvedPath);
906
- if (recoveredPath) {
907
- return recoveredPath;
908
- }
909
-
910
- throw new Error('Source file does not exist: ' + resolvedPath);
911
- }
912
-
913
- function formatRelationCandidate(relation) {
914
- return {
915
- childTableName: relation.childTableName,
916
- mainField: relation.mainField,
917
- childField: relation.childField,
918
- relationType: relation.relationType || '',
919
- };
920
- }
921
-
922
- function getRelationCandidates(designDoc, tableName) {
923
- return designDoc.relations
924
- .filter((relation) => relation.mainTableName === tableName)
925
- .map(formatRelationCandidate);
926
- }
927
-
928
- function buildRetryArguments(safeArgs, sourceFile, childTableName) {
929
- const retryArguments = {
930
- tableName: safeArgs.tableName,
931
- style: safeArgs.style,
932
- frontendPath: safeArgs.frontendPath,
933
- moduleName: safeArgs.moduleName,
934
- writeToDisk: safeArgs.writeToDisk,
935
- overwrite: safeArgs.overwrite,
936
- };
937
-
938
- if (sourceFile) {
939
- retryArguments.designFile = sourceFile;
940
- }
941
-
942
- if (safeArgs.children && safeArgs.children.length) {
943
- retryArguments.children = safeArgs.children.map((relation) => ({
944
- childTableName: relation.childTableName,
945
- mainField: relation.mainField,
946
- childField: relation.childField,
947
- relationType: relation.relationType || '',
948
- }));
949
- }
950
-
951
- if (childTableName) {
952
- retryArguments.childTableName = childTableName;
953
- }
954
-
955
- if (safeArgs.mainField) {
956
- retryArguments.mainField = safeArgs.mainField;
957
- }
958
-
959
- if (safeArgs.childField) {
960
- retryArguments.childField = safeArgs.childField;
961
- }
962
-
963
- if (safeArgs.relationType) {
964
- retryArguments.relationType = safeArgs.relationType;
965
- }
966
-
967
- return retryArguments;
968
- }
969
-
970
- function buildRelationCorrectionEntry(safeArgs, sourceFile, candidates, selectedChildTableName) {
971
- return {
972
- field: 'childTableName',
973
- title: '涓诲瓙琛ㄥ叧鑱斾慨姝e叆鍙?,
974
- tableName: safeArgs.tableName,
975
- style: safeArgs.style,
976
- designFile: sourceFile,
977
- description: candidates.length
978
- ? 'If the current child relation is not the one you want, pass childTableName to choose a candidate from the design file.'
979
- : 'No child relation was found. Check the 涓讳粠琛ㄥ叧鑱旇鏄?section in the design file.',
980
- currentValue: selectedChildTableName || '',
981
- options: candidates.map((item) => item.childTableName),
982
- candidates,
983
- example: candidates.length ? buildRetryArguments(safeArgs, sourceFile, candidates[0].childTableName) : buildRetryArguments(safeArgs, sourceFile, selectedChildTableName),
984
- retryArguments: candidates.map((item) => buildRetryArguments(safeArgs, sourceFile, item.childTableName)),
985
- };
986
- }
987
-
988
- function createRelationResolutionError(message, safeArgs, sourceFile, candidates, selectedChildTableName, status) {
989
- const error = new Error(message);
990
- error.details = {
991
- type: 'relation_resolution',
992
- status,
993
- tableName: safeArgs.tableName,
994
- designFile: sourceFile,
995
- message,
996
- candidates,
997
- correctionEntry: buildRelationCorrectionEntry(safeArgs, sourceFile, candidates, selectedChildTableName),
998
- };
999
- return error;
1000
- }
1001
-
1002
- function loadSourceDocument(safeArgs) {
1003
- const inputPath = safeArgs.designFile || DEFAULT_DESIGN_FILE;
1004
- const sourcePath = resolveSourcePath(inputPath);
1005
- const text = readUtf8File(sourcePath);
1006
- return {
1007
- path: sourcePath,
1008
- text,
1009
- designDoc: parseMarkdownDesignFile(text),
1010
- };
1011
- }
1012
-
1013
- function normalizeChildrenInput(inputChildren) {
1014
- if (inputChildren === undefined || inputChildren === null) {
1015
- return [];
1016
- }
1017
- if (!Array.isArray(inputChildren)) {
1018
- throw new Error('children must be an array');
1019
- }
1020
- return inputChildren.map((item, index) => {
1021
- if (!item || typeof item !== 'object') {
1022
- throw new Error('children[' + index + '] must be an object');
1023
- }
1024
- const childTableName = item.childTableName ? String(item.childTableName) : '';
1025
- const mainField = item.mainField ? String(item.mainField) : '';
1026
- const childField = item.childField ? String(item.childField) : '';
1027
- const relationType = item.relationType ? String(item.relationType) : '';
1028
- const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !({ childTableName, mainField, childField })[field]);
1029
- if (missingFields.length) {
1030
- throw new Error('children[' + index + '] is missing required fields: ' + missingFields.join(', '));
1031
- }
1032
- return { childTableName, mainField, childField, relationType };
1033
- });
1034
- }
1035
-
1036
- function resolveMarkdownChildRelations(sourceDocument, safeArgs) {
1037
- if (safeArgs.children && safeArgs.children.length) {
1038
- return safeArgs.children.map((relation) => ({
1039
- mainTableName: safeArgs.tableName,
1040
- childTableName: relation.childTableName,
1041
- mainField: relation.mainField,
1042
- childField: relation.childField,
1043
- relationType: relation.relationType || '',
1044
- }));
1045
- }
1046
-
1047
- const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !safeArgs[field]);
1048
- if (missingFields.length) {
1049
- const message = 'master_child_jump requires either children[] or legacy relation fields. Missing: ' + missingFields.join(', ') + '.';
1050
- const error = new Error(message);
1051
- error.details = {
1052
- type: 'relation_input_required',
1053
- status: 'missing_required_relation_input',
1054
- tableName: safeArgs.tableName,
1055
- designFile: sourceDocument.path,
1056
- message,
1057
- requiredFields: ['children[] or childTableName/mainField/childField'],
1058
- correctionEntry: {
1059
- fields: ['children', 'childTableName', 'mainField', 'childField'],
1060
- example: {
1061
- ...buildRetryArguments(safeArgs, sourceDocument.path, safeArgs.childTableName || '<child_table_name>'),
1062
- children: [
1063
- {
1064
- childTableName: safeArgs.childTableName || '<child_table_name>',
1065
- mainField: safeArgs.mainField || '<main_field>',
1066
- childField: safeArgs.childField || '<child_field>',
1067
- relationType: safeArgs.relationType || '1:N',
1068
- },
1069
- ],
1070
- mainField: safeArgs.mainField || '<main_field>',
1071
- childField: safeArgs.childField || '<child_field>',
1072
- },
1073
- },
1074
- };
1075
- throw error;
1076
- }
1077
-
1078
- return [
1079
- {
1080
- mainTableName: safeArgs.tableName,
1081
- childTableName: safeArgs.childTableName,
1082
- mainField: safeArgs.mainField,
1083
- childField: safeArgs.childField,
1084
- relationType: safeArgs.relationType || '',
1085
- },
1086
- ];
1087
- }
1088
-
1089
- function getStylePreset(styleId) {
1090
- const preset = STYLE_CATALOG[styleId];
1091
- if (!preset) throw new Error('Unsupported style: ' + styleId);
1092
- return preset;
1093
- }
1094
-
1095
- function resolveSharedTemplates(stylePreset) {
1096
- const selected = {};
1097
- for (const [kind, templateName] of Object.entries(stylePreset.templateFiles || {})) {
1098
- const templatePath = path.join(TEMPLATE_LIBRARY_ROOT, templateName);
1099
- selected[kind] = { name: templateName, path: templatePath, exists: fs.existsSync(templatePath) };
1100
- }
1101
- return selected;
1102
- }
1103
-
1104
- function hasRuntimeSupport(stylePreset) {
1105
- return Boolean(stylePreset.runtime && stylePreset.runtime.supported && stylePreset.runtime.templateDir);
1106
- }
1107
-
1108
- function normalizeFields(parsed) {
1109
- return parsed.fields.map((field) => ({ ...field, formType: mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1110
- }
1111
-
1112
- function ensureFieldExists(fields, fieldName, tableName, role) {
1113
- const field = fields.find((item) => item.fieldName === fieldName);
1114
- if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
1115
- return field;
1116
- }
1117
-
1118
- function buildChildModels(sourceDocument, safeArgs, mainParsed) {
1119
- if (safeArgs.style !== 'master_child_jump') return [];
1120
-
1121
- const relations = resolveMarkdownChildRelations(sourceDocument, safeArgs);
1122
- return relations.map((relation) => {
1123
- const childParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, relation.childTableName);
1124
- const childFields = normalizeFields(childParsed);
1125
- const mainRelationField = ensureFieldExists(mainParsed.fields, relation.mainField, safeArgs.tableName, 'Main relation');
1126
- const childRelationField = ensureFieldExists(childParsed.fields, relation.childField, relation.childTableName, 'Child relation');
1127
- const childVisibleFields = childFields.filter(
1128
- (field) => field.fieldName !== childParsed.pkField.fieldName && !field.isAudit && field.fieldName !== relation.childField
1129
- );
1130
-
1131
- return {
1132
- tableName: relation.childTableName,
1133
- tableComment: childParsed.tableComment,
1134
- className: toPascalCase(relation.childTableName),
1135
- functionName: toCamelCase(relation.childTableName),
1136
- listName: toCamelCase(relation.childTableName) + 'List',
1137
- pk: childParsed.pkField,
1138
- fields: childFields,
1139
- visibleFields: childVisibleFields,
1140
- mainField: mainRelationField,
1141
- childField: childRelationField,
1142
- relationType: relation.relationType,
1143
- };
1144
- });
1145
- }
1146
-
1147
- function buildModel(safeArgs) {
1148
- const sourceDocument = loadSourceDocument(safeArgs);
1149
- const mainParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, safeArgs.tableName);
1150
- const fields = normalizeFields(mainParsed);
1151
- const visibleFields = fields.filter((field) => field.fieldName !== mainParsed.pkField.fieldName && !field.isAudit);
1152
- const gridFields = visibleFields.slice(0, 8);
1153
- const children = buildChildModels(sourceDocument, safeArgs, mainParsed);
1154
- const childDictTypes = children.flatMap((child) => child.visibleFields.map((field) => field.dictType).filter(Boolean));
1155
- const dictTypes = [...new Set([...visibleFields.map((field) => field.dictType).filter(Boolean), ...childDictTypes])];
1156
-
1157
- return {
1158
- sourceFile: sourceDocument.path,
1159
- designFile: sourceDocument.path,
1160
- tableName: safeArgs.tableName,
1161
- tableComment: mainParsed.tableComment,
1162
- className: toPascalCase(safeArgs.tableName),
1163
- functionName: toCamelCase(safeArgs.tableName),
1164
- moduleName: normalizeModuleName(safeArgs.moduleName),
1165
- pk: mainParsed.pkField,
1166
- fields,
1167
- visibleFields,
1168
- gridFields,
1169
- dictTypes,
1170
- frontendPath: path.resolve(safeArgs.frontendPath),
1171
- style: safeArgs.style,
1172
- children,
1173
- };
1174
- }
1175
-
1176
- function renderTemplate(templateText, replacements) {
1177
- let output = templateText;
1178
- for (const [key, value] of Object.entries(replacements)) {
1179
- output = output.split('{{' + key + '}}').join(String(value));
1180
- }
1181
- return output;
1182
- }
1183
-
1184
- function renderFormField(field) {
1185
- const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1186
- const prop = field.attrName;
1187
-
1188
- if (field.formType === 'select') {
1189
- return [
1190
- ' <el-col :span="12" class="mb20">',
1191
- ` <el-form-item label="${label}" prop="${prop}">`,
1192
- ` <el-select v-model="form.${prop}" placeholder="璇烽€夋嫨${label}" style="width: 100%">`,
1193
- ` <el-option v-for="item in ${field.dictType}" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1194
- ' </el-select>',
1195
- ' </el-form-item>',
1196
- ' </el-col>',
1197
- ].join('\n');
1198
- }
1199
-
1200
- if (field.formType === 'number') {
1201
- const max = field.comment.includes('%') || field.comment.includes('姣斾緥') ? ' :max="100"' : '';
1202
- const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1203
- return [
1204
- ' <el-col :span="12" class="mb20">',
1205
- ` <el-form-item label="${label}" prop="${prop}">`,
1206
- ` <el-input-number v-model="form.${prop}" :min="0"${max}${precision} placeholder="璇疯緭鍏?{label}" style="width: 100%" />`,
1207
- ' </el-form-item>',
1208
- ' </el-col>',
1209
- ].join('\n');
1210
- }
1211
-
1212
- if (field.formType === 'datetime' || field.formType === 'date') {
1213
- const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
1214
- const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
1215
- return [
1216
- ' <el-col :span="12" class="mb20">',
1217
- ` <el-form-item label="${label}" prop="${prop}">`,
1218
- ` <el-date-picker type="${pickerType}" placeholder="璇烽€夋嫨${label}" v-model="form.${prop}" :value-format="${formatName}" style="width: 100%"></el-date-picker>`,
1219
- ' </el-form-item>',
1220
- ' </el-col>',
1221
- ].join('\n');
1222
- }
1223
-
1224
- if (field.formType === 'textarea') {
1225
- return [
1226
- ' <el-col :span="24" class="mb20">',
1227
- ` <el-form-item label="${label}" prop="${prop}">`,
1228
- ` <el-input type="textarea" v-model="form.${prop}" placeholder="璇疯緭鍏?{label}" />`,
1229
- ' </el-form-item>',
1230
- ' </el-col>',
1231
- ].join('\n');
1232
- }
1233
-
1234
- return [
1235
- ' <el-col :span="12" class="mb20">',
1236
- ` <el-form-item label="${label}" prop="${prop}">`,
1237
- ` <el-input v-model="form.${prop}" placeholder="璇疯緭鍏?{label}" />`,
1238
- ' </el-form-item>',
1239
- ' </el-col>',
1240
- ].join('\n');
1241
- }
1242
-
1243
- function renderTableColumn(field) {
1244
- const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1245
- if (field.dictType) {
1246
- return [
1247
- ` <el-table-column prop="${field.attrName}" label="${label}" show-overflow-tooltip>`,
1248
- ' <template #default="scope">',
1249
- ` <dict-tag :options="${field.dictType}" :value="scope.row.${field.attrName}" />`,
1250
- ' </template>',
1251
- ' </el-table-column>',
1252
- ].join('\n');
1253
- }
1254
- return ` <el-table-column prop="${field.attrName}" label="${label}" show-overflow-tooltip />`;
1255
- }
1256
-
1257
- function renderChildTableColumn(field, childListName) {
1258
- const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1259
- const rules = field.notNull ? ` :rules="[{ required: true, trigger: 'blur' }]"` : '';
1260
-
1261
- let control = ` <el-input v-model="row.${field.attrName}" />`;
1262
- if (field.formType === 'select' && field.dictType) {
1263
- control = [
1264
- ` <el-select v-model="row.${field.attrName}" placeholder="璇烽€夋嫨${label}" style="width: 100%">`,
1265
- ` <el-option v-for="item in ${field.dictType}" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1266
- ' </el-select>',
1267
- ].join('\n');
1268
- } else if (field.formType === 'number') {
1269
- const max = field.comment.includes('%') || field.comment.includes('姣斾緥') ? ' :max="100"' : '';
1270
- const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1271
- control = ` <el-input-number v-model="row.${field.attrName}" :min="0"${max}${precision} style="width: 100%" />`;
1272
- }
1273
-
1274
- return [
1275
- ` <el-table-column label="${label}" prop="${field.attrName}">`,
1276
- ' <template #default="{ row, $index }">',
1277
- ` <el-form-item :prop="\`${childListName}.\${$index}.${field.attrName}\`"${rules}>`,
1278
- control,
1279
- ' </el-form-item>',
1280
- ' </template>',
1281
- ' </el-table-column>',
1282
- ].join('\n');
1283
- }
1284
-
1285
- function renderOptionField(field) {
1286
- const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1287
- return ` ${field.attrName}: { show:true, alwaysHide:false, smart:false, label: '${label}', width: '120' },`;
1288
- }
1289
-
1290
- function renderFilterType(field) {
1291
- if (!field.dictType) return null;
1292
- return ` ${field.attrName}: 30,`;
1293
- }
1294
-
1295
- function renderDefaultLine(field) {
1296
- if (field.formType === 'number') return ` ${field.attrName}: 0,`;
1297
- return ` ${field.attrName}: '',`;
1298
- }
1299
-
1300
- function renderFormDefaults(model) {
1301
- const lines = [` ${model.pk.attrName}: '',`];
1302
- for (const field of model.visibleFields) lines.push(renderDefaultLine(field));
1303
- return lines.join('\n');
1304
- }
1305
-
1306
- function renderChildTempDefaults(childModel) {
1307
- if (!childModel) return '';
1308
- return childModel.fields.map(renderDefaultLine).join('\n');
1309
- }
1310
-
1311
- function renderChildListDefaultLine(childModel) {
1312
- return ` ${childModel.listName}: [],`;
1313
- }
1314
-
1315
- function renderChildTempDeclaration(childModel) {
1316
- return [
1317
- `const childTemp${childModel.className} = reactive({`,
1318
- renderChildTempDefaults(childModel),
1319
- '});',
1320
- ].join('\n');
1321
- }
1322
-
1323
- function renderChildSection(childModel, childCount) {
1324
- const title = childModel.tableComment.replace(/'/g, "\\'");
1325
- const deleteExpression =
1326
- childCount > 1
1327
- ? `deleteChild(obj, '${childModel.pk.attrName}', '${childModel.tableName}')`
1328
- : `deleteChild(obj, '${childModel.pk.attrName}')`;
1329
-
1330
- return [
1331
- ' <el-col :span="24" class="mb20">',
1332
- ` <div class="mb10" style="font-weight: 600;">${title}</div>`,
1333
- ` <sc-form-table v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" @delete="(obj) => ${deleteExpression}" :placeholder="t('common.noData')">`,
1334
- childModel.visibleFields.map((field) => renderChildTableColumn(field, childModel.listName)).join('\n'),
1335
- ' </sc-form-table>',
1336
- ' </el-col>',
1337
- ].join('\n');
1338
- }
1339
-
1340
- function renderChildFormListDefaults(children) {
1341
- if (!children.length) return '';
1342
- return children.map(renderChildListDefaultLine).join('\n');
1343
- }
1344
-
1345
- function renderChildTempDeclarations(children) {
1346
- if (!children.length) return '';
1347
- return children.map(renderChildTempDeclaration).join('\n\n');
1348
- }
1349
-
1350
- function renderChildResetListLines(children) {
1351
- if (!children.length) return '';
1352
- return children.map((childModel) => ` form.${childModel.listName} = [];`).join('\n');
1353
- }
1354
-
1355
- function renderDictImportBlock(dictTypes) {
1356
- if (!dictTypes.length) return '';
1357
- return [
1358
- "import { useDict } from '/@/hooks/dict';",
1359
- `const { ${dictTypes.join(', ')} } = useDict(${dictTypes.map((item) => `'${item}'`).join(', ')});`,
1360
- ].join('\n');
1361
- }
1362
-
1363
- function getDefaultOptionFieldWidthV2(field) {
1364
- if (field.formType === 'textarea') return '180';
1365
- return '120';
1366
- }
1367
-
1368
- function renderOptionFieldV2(field, labelKey, dictRegistryRefs, indent = ' ') {
1369
- const parts = [`key: '${field.attrName}'`, `labelKey: '${labelKey}'`];
1370
- const width = getDefaultOptionFieldWidthV2(field);
1371
-
1372
- if (width !== '120') {
1373
- parts.push(`width: '${width}'`);
1374
- }
1375
-
1376
- if (field.dictType) {
1377
- parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1378
- }
1379
-
1380
- return `${indent}{ ${parts.join(', ')} },`;
1381
- }
1382
-
1383
- function renderChildOptionGroupV2(model, childModel, dictRegistryRefs) {
1384
- return [
1385
- ` ${childModel.listName}: [`,
1386
- childModel.visibleFields
1387
- .map((field) => renderOptionFieldV2(field, buildChildFieldLabelKey(model, childModel, field), dictRegistryRefs, ' '))
1388
- .join('\n'),
1389
- ' ],',
1390
- ].join('\n');
1391
- }
1392
-
1393
- function renderTextMaxlengthAttrV2(field) {
1394
- if (!field.length) return '';
1395
- return ` :maxlength="${field.length}"`;
1396
- }
1397
-
1398
- function renderTextareaMaxlengthAttrsV2(field) {
1399
- if (!field.length) return '';
1400
- return ` :maxlength="${field.length}" show-word-limit`;
1401
- }
1402
-
1403
- function renderFormFieldV2(field) {
1404
- const prop = field.attrName;
1405
- const labelExpr = `getMasterFieldLabel('${prop}')`;
1406
- const dictExpr = `getMasterFieldMeta('${prop}')?.dictType`;
1407
- const visibilityExpr = `isMasterFieldVisible('${prop}')`;
1408
-
1409
- if (field.formType === 'select') {
1410
- return [
1411
- ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1412
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1413
- ` <el-select v-model="form.${prop}" :placeholder="selectPlaceholder(${labelExpr})" style="width: 100%">`,
1414
- ` <el-option v-for="item in getDictOptions(${dictExpr})" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1415
- ' </el-select>',
1416
- ' </el-form-item>',
1417
- ' </el-col>',
1418
- ].join('\n');
1419
- }
1420
-
1421
- if (field.formType === 'number') {
1422
- const max = field.comment.includes('%') || field.comment.includes('濮f柧绶?) ? ' :max="100"' : '';
1423
- const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1424
- return [
1425
- ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1426
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1427
- ` <el-input-number v-model="form.${prop}" :min="0"${max}${precision} :placeholder="inputPlaceholder(${labelExpr})" style="width: 100%" />`,
1428
- ' </el-form-item>',
1429
- ' </el-col>',
1430
- ].join('\n');
1431
- }
1432
-
1433
- if (field.formType === 'datetime' || field.formType === 'date') {
1434
- const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
1435
- const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
1436
- return [
1437
- ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1438
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1439
- ` <el-date-picker type="${pickerType}" :placeholder="selectPlaceholder(${labelExpr})" v-model="form.${prop}" :value-format="${formatName}" style="width: 100%"></el-date-picker>`,
1440
- ' </el-form-item>',
1441
- ' </el-col>',
1442
- ].join('\n');
1443
- }
1444
-
1445
- if (field.formType === 'textarea') {
1446
- return [
1447
- ` <el-col v-if="${visibilityExpr}" :span="24" class="mb20">`,
1448
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1449
- ` <el-input type="textarea" v-model="form.${prop}" :placeholder="inputPlaceholder(${labelExpr})"${renderTextareaMaxlengthAttrsV2(field)} />`,
1450
- ' </el-form-item>',
1451
- ' </el-col>',
1452
- ].join('\n');
1453
- }
1454
-
1455
- return [
1456
- ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1457
- ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1458
- ` <el-input v-model="form.${prop}" :placeholder="inputPlaceholder(${labelExpr})"${renderTextMaxlengthAttrV2(field)} />`,
1459
- ' </el-form-item>',
1460
- ' </el-col>',
1461
- ].join('\n');
1462
- }
1463
-
1464
- function renderTableColumnV2(field, dictRegistryRefs) {
1465
- const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1466
- const parts = [`prop: '${field.attrName}'`, `label: '${label}'`];
1467
- const width = getDefaultOptionFieldWidthV2(field);
1468
-
1469
- if (width !== '120') {
1470
- parts.push(`width: '${width}'`);
1471
- }
1472
-
1473
- if (field.dictType) {
1474
- parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1475
- }
1476
-
1477
- return ` { ${parts.join(', ')} },`;
1478
- }
1479
-
1480
- function renderChildTableColumnV2(field, childListName) {
1481
- const rules = field.notNull ? ` :rules="[{ required: true, trigger: 'blur' }]"` : '';
1482
- const labelExpr = `getChildFieldLabel('${childListName}', '${field.attrName}')`;
1483
- const dictExpr = `getChildFieldMeta('${childListName}', '${field.attrName}')?.dictType`;
1484
-
1485
- let control = ` <el-input v-model="row.${field.attrName}"${renderTextMaxlengthAttrV2(field)} />`;
1486
- if (field.formType === 'select' && field.dictType) {
1487
- control = [
1488
- ` <el-select v-model="row.${field.attrName}" :placeholder="selectPlaceholder(${labelExpr})" style="width: 100%">`,
1489
- ` <el-option v-for="item in getDictOptions(${dictExpr})" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1490
- ' </el-select>',
1491
- ].join('\n');
1492
- } else if (field.formType === 'number') {
1493
- const max = field.comment.includes('%') || field.comment.includes('濮f柧绶?) ? ' :max="100"' : '';
1494
- const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1495
- control = ` <el-input-number v-model="row.${field.attrName}" :min="0"${max}${precision} style="width: 100%" />`;
1496
- } else if (field.formType === 'datetime' || field.formType === 'date') {
1497
- const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
1498
- const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
1499
- control = ` <el-date-picker type="${pickerType}" v-model="row.${field.attrName}" :value-format="${formatName}" :placeholder="selectPlaceholder(${labelExpr})" style="width: 100%"></el-date-picker>`;
1500
- } else if (field.formType === 'textarea') {
1501
- control = ` <el-input type="textarea" v-model="row.${field.attrName}"${renderTextareaMaxlengthAttrsV2(field)} />`;
1502
- }
1503
-
1504
- return [
1505
- ` <el-table-column :label="${labelExpr}" prop="${field.attrName}">`,
1506
- ' <template #default="{ row, $index }">',
1507
- ` <el-form-item :prop="\`${childListName}.\${$index}.${field.attrName}\`"${rules}>`,
1508
- control,
1509
- ' </el-form-item>',
1510
- ' </template>',
1511
- ' </el-table-column>',
1512
- ].join('\n');
1513
- }
1514
-
1515
- function renderChildSectionV2(childModel, childCount) {
1516
- const deleteExpression =
1517
- childCount > 1
1518
- ? `deleteChild(obj, '${childModel.pk.attrName}', '${childModel.tableName}')`
1519
- : `deleteChild(obj, '${childModel.pk.attrName}')`;
1520
-
1521
- return [
1522
- ' <el-col :span="24" class="mb20">',
1523
- ` <div class="mb10" style="font-weight: 600;">{{ childSectionTitle('${childModel.listName}') }}</div>`,
1524
- ` <sc-form-table v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" @delete="(obj) => ${deleteExpression}" :placeholder="t('common.noData')">`,
1525
- childModel.visibleFields.map((field) => renderChildTableColumnV2(field, childModel.listName)).join('\n'),
1526
- ' </sc-form-table>',
1527
- ' </el-col>',
1528
- ].join('\n');
1529
- }
1530
-
1531
- function renderValidationRule(field) {
1532
- if (!field.notNull) return null;
1533
- const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1534
- return ` ${field.attrName}: [{ required: true, message: '${label}涓嶈兘涓虹┖', trigger: 'blur' }],`;
1535
- }
1536
-
1537
- function renderFormRules(visibleFields) {
1538
- const lines = visibleFields.map(renderValidationRule).filter(Boolean);
1539
- return lines.join('\n');
1540
- }
1541
-
1542
- function renderFormRulesV2(visibleFields) {
1543
- const lines = visibleFields
1544
- .filter((field) => field.notNull)
1545
- .map((field) => ` ${field.attrName}: [{ required: true, message: fieldRequiredMessage('${field.attrName}'), trigger: 'blur' }],`);
1546
- return lines.join('\n');
1547
- }
1548
-
1549
- function buildReplacements(model, sharedSupport) {
1550
- const menuBaseId = Date.now();
1551
- const apiModulePath = `${model.moduleName}/${model.functionName}`;
1552
- const routePath = `${model.moduleName}/${model.functionName}`;
1553
- const permissionPrefix = `${model.moduleName}/${model.functionName}`.replace(/\//g, '_');
1554
- const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
1555
- const i18nNamespace = buildI18nNamespace(model);
1556
-
1557
- return {
1558
- TABLE_NAME: model.tableName,
1559
- TABLE_COMMENT: model.tableComment,
1560
- CLASS_NAME: model.className,
1561
- FUNCTION_NAME: model.functionName,
1562
- PK_ATTR: model.pk.attrName,
1563
- API_MODULE_PATH: apiModulePath,
1564
- API_PATH: apiModulePath,
1565
- VIEW_MODULE_PATH: routePath,
1566
- MENU_ROUTE_PATH: routePath,
1567
- I18N_NAMESPACE: i18nNamespace,
1568
- PERMISSION_PREFIX: permissionPrefix,
1569
- MENU_BASE_ID: menuBaseId,
1570
- MENU_BASE_ID_PLUS_1: menuBaseId + 1,
1571
- MENU_BASE_ID_PLUS_2: menuBaseId + 2,
1572
- MENU_BASE_ID_PLUS_3: menuBaseId + 3,
1573
- MENU_BASE_ID_PLUS_4: menuBaseId + 4,
1574
- MENU_BASE_ID_PLUS_5: menuBaseId + 5,
1575
- GENERATED_AT: new Date().toISOString(),
1576
- FORM_FIELDS: model.visibleFields.map(renderFormFieldV2).join('\n'),
1577
- TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumnV2(field, dictRegistryRefs)).join('\n'),
1578
- FORM_DEFAULTS: renderFormDefaults(model),
1579
- DICT_REGISTRY_IMPORT_BLOCK: model.dictTypes.length ? "import { DictRegistry } from '/@/enums/dict-registry';" : '',
1580
- MASTER_OPTION_FIELDS: model.visibleFields.map((field) => renderOptionFieldV2(field, buildFieldLabelKey(model, field), dictRegistryRefs)).join('\n'),
1581
- CHILD_OPTION_GROUPS: model.children.map((childModel) => renderChildOptionGroupV2(model, childModel, dictRegistryRefs)).join('\n'),
1582
- FORM_RULES: renderFormRulesV2(model.visibleFields),
1583
- CHILD_FORM_LIST_DEFAULTS: renderChildFormListDefaults(model.children),
1584
- CHILD_TEMP_DECLARATIONS: renderChildTempDeclarations(model.children),
1585
- CHILD_RESET_LISTS: renderChildResetListLines(model.children),
1586
- CHILD_SECTIONS: model.children.map((childModel) => renderChildSectionV2(childModel, model.children.length)).join('\n'),
1587
- };
1588
- }
1589
-
1590
- function renderFiles(model, stylePreset, sharedSupport, localeZhSupport) {
1591
- if (!hasRuntimeSupport(stylePreset)) throw new Error('Runtime templates are not implemented for style: ' + model.style);
1592
-
1593
- const runtime = stylePreset.runtime;
1594
- const templateDir = path.resolve(__dirname, runtime.templateDir);
1595
- const replacements = buildReplacements(model, sharedSupport);
1596
- const formTemplate = fs.readFileSync(path.join(templateDir, runtime.files.form), 'utf8');
1597
- const listTemplate = fs.readFileSync(path.join(templateDir, runtime.files.list), 'utf8');
1598
- const optionsTemplate = fs.readFileSync(path.join(templateDir, runtime.files.options), 'utf8');
1599
- const apiTemplate = fs.readFileSync(path.join(templateDir, runtime.files.api), 'utf8');
1600
- const menuSqlTemplate = runtime.files.menuSql ? fs.readFileSync(path.join(templateDir, runtime.files.menuSql), 'utf8') : null;
1601
-
1602
- const viewRoot = path.join(model.frontendPath, 'src', 'views', ...model.moduleName.split('/'), model.functionName);
1603
- const apiRoot = path.join(model.frontendPath, 'src', 'api', ...model.moduleName.split('/'));
1604
- const menuRoot = path.join(model.frontendPath, 'menu');
1605
-
1606
- const files = [
1607
- { type: 'form', path: path.join(viewRoot, 'form.vue'), content: renderTemplate(formTemplate, replacements) },
1608
- { type: 'list', path: path.join(viewRoot, 'index.vue'), content: renderTemplate(listTemplate, replacements) },
1609
- { type: 'options', path: path.join(viewRoot, 'options.ts'), content: renderTemplate(optionsTemplate, replacements) },
1610
- { type: 'api', path: path.join(apiRoot, `${model.functionName}.ts`), content: renderTemplate(apiTemplate, replacements) },
1611
- {
1612
- type: 'i18nZh',
1613
- path: localeZhSupport.path,
1614
- content: localeZhSupport.content,
1615
- canWrite: localeZhSupport.isCompatible,
1616
- needsWrite: localeZhSupport.needsWrite,
1617
- },
1618
- ];
1619
-
1620
- if (menuSqlTemplate) {
1621
- files.push({ type: 'menuSql', path: path.join(menuRoot, `${model.functionName}_menu.sql`), content: renderTemplate(menuSqlTemplate, replacements) });
1622
- }
1623
- return files;
1624
- }
1625
-
1626
- function ensureArguments(input) {
1627
- if (!input || typeof input !== 'object') throw new Error('Arguments must be an object');
1628
- for (const key of TOOL_SCHEMA.required) {
1629
- if (!input[key]) throw new Error(key + ' is required');
1630
- }
1631
-
1632
- const style = String(input.style);
1633
- getStylePreset(style);
1634
-
1635
- return {
1636
- designFile: input.designFile ? String(input.designFile) : null,
1637
- tableName: String(input.tableName),
1638
- style,
1639
- children: normalizeChildrenInput(input.children),
1640
- childTableName: input.childTableName ? String(input.childTableName) : null,
1641
- mainField: input.mainField ? String(input.mainField) : null,
1642
- childField: input.childField ? String(input.childField) : null,
1643
- relationType: input.relationType ? String(input.relationType) : '',
1644
- frontendPath: String(input.frontendPath),
1645
- moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
1646
- writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
1647
- overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
1648
- };
1649
- }
1650
-
1651
- function maybeWriteFiles(files, writeToDisk, overwrite) {
1652
- if (!writeToDisk) return;
1653
- for (const file of files) {
1654
- if (file.canWrite === false) {
1655
- file.status = 'skipped';
1656
- continue;
1657
- }
1658
- if (file.needsWrite === false) {
1659
- file.status = 'unchanged';
1660
- continue;
1661
- }
1662
- if (!overwrite && fs.existsSync(file.path)) {
1663
- file.status = 'skipped';
1664
- continue;
1665
- }
1666
- fs.mkdirSync(path.dirname(file.path), { recursive: true });
1667
- fs.writeFileSync(file.path, file.content, 'utf8');
1668
- file.status = 'success';
1669
- }
1670
- }
1671
-
1672
- function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, note) {
1673
- const relations = model.children.map((childModel) => ({
1674
- childTableName: childModel.tableName,
1675
- childTableComment: childModel.tableComment,
1676
- mainField: childModel.mainField.fieldName,
1677
- childField: childModel.childField.fieldName,
1678
- childListName: childModel.listName,
1679
- relationType: childModel.relationType || '',
1680
- }));
1681
- const selectedList = relations.map((relation) => formatRelationCandidate(relation));
1682
-
1683
- return {
1684
- mode: 'local-template',
1685
- style: safeArgs.style,
1686
- styleLabel: stylePreset.label,
1687
- runtimeSupported: hasRuntimeSupport(stylePreset),
1688
- sourceFile: model.sourceFile,
1689
- designFile: model.designFile,
1690
- tableName: model.tableName,
1691
- tableComment: model.tableComment,
1692
- moduleName: model.moduleName,
1693
- writeToDisk: safeArgs.writeToDisk,
1694
- selectedTemplates: sharedTemplates,
1695
- files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
1696
- relation: relations.length === 1 ? relations[0] : null,
1697
- relations,
1698
- relationResolution: model.children.length
1699
- ? {
1700
- status: 'provided',
1701
- tableName: safeArgs.tableName,
1702
- designFile: model.designFile,
1703
- source: 'arguments',
1704
- message:
1705
- model.children.length > 1
1706
- ? 'Direct child relations were provided by the caller. MCP skipped design-doc relation inference and generated a single main form with multiple child tables.'
1707
- : 'The relation was provided by the caller. MCP skipped design-doc relation inference and generated files directly.',
1708
- selected: selectedList.length === 1 ? selectedList[0] : null,
1709
- selectedList,
1710
- correctionEntry: {
1711
- fields: ['children', 'childTableName', 'mainField', 'childField'],
1712
- example: buildRetryArguments(safeArgs, model.designFile, model.children[0].tableName),
1713
- },
1714
- }
1715
- : null,
1716
- summary: {
1717
- totalFields: model.fields.length,
1718
- visibleFields: model.visibleFields.length,
1719
- dictFields: model.visibleFields.filter((field) => field.dictType).map((field) => field.attrName),
1720
- skippedAuditFields: model.fields.filter((field) => field.isAudit).map((field) => field.fieldName),
1721
- childCount: model.children.length,
1722
- childTables: model.children.map((childModel) => childModel.tableName),
1723
- childVisibleFields: model.children.reduce((sum, childModel) => sum + childModel.visibleFields.length, 0),
1724
- },
1725
- note,
1726
- };
1727
- }
1728
-
1729
- async function handleToolCall(argumentsObject) {
1730
- const safeArgs = ensureArguments(argumentsObject);
1731
- const stylePreset = getStylePreset(safeArgs.style);
1732
- const model = buildModel(safeArgs);
1733
- const sharedTemplates = resolveSharedTemplates(stylePreset);
1734
- const sharedSupport = prepareSharedSupport(model.frontendPath, model.dictTypes);
1735
- const localeZhSupport = prepareZhCnLocaleFile(model);
1736
-
1737
- if (!hasRuntimeSupport(stylePreset)) {
1738
- const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, [], 'Style mapping is declared, but runtime template rendering is not implemented yet for this style.');
1739
- return toolTextResult(JSON.stringify(manifest, null, 2));
1740
- }
1741
-
1742
- const files = renderFiles(model, stylePreset, sharedSupport, localeZhSupport);
1743
- maybeWriteFiles(files, safeArgs.writeToDisk, safeArgs.overwrite);
1744
- maybeWriteSharedSupport(sharedSupport, safeArgs.writeToDisk);
1745
- const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, buildSupportNote(sharedSupport, localeZhSupport));
1746
- return toolTextResult(JSON.stringify(manifest, null, 2));
1747
- }
1748
-
1749
- async function onMessage(message) {
1750
- const { id, method, params } = message;
1751
- const isNotification = id === undefined || id === null;
1752
-
1753
- if (method === 'initialize') {
1754
- writeMessage(successResponse(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: SERVER_NAME, version: SERVER_VERSION } }));
1755
- return;
1756
- }
1757
-
1758
- if (method === 'notifications/initialized') return;
1759
-
1760
- // Silently ignore all other notifications (no id means no response expected).
1761
- if (isNotification) return;
1762
-
1763
- if (method === 'ping') {
1764
- writeMessage(successResponse(id, {}));
1765
- return;
1766
- }
1767
-
1768
- if (method === 'tools/list') {
1769
- writeMessage(successResponse(id, { tools: [{ name: TOOL_NAME, description: 'Generate Worsoft frontend files and menu SQL from a local Markdown design file with style-based local templates. In master_child_jump mode, the caller must provide either children[] or childTableName/mainField/childField.', inputSchema: TOOL_SCHEMA }] }));
1770
- return;
1771
- }
1772
-
1773
- if (method === 'tools/call') {
1774
- if (!params || params.name !== TOOL_NAME) {
1775
- writeMessage(errorResponse(id, -32602, 'Unknown tool'));
1776
- return;
1777
- }
1778
- try {
1779
- const result = await handleToolCall(params.arguments || {});
1780
- writeMessage(successResponse(id, result));
1781
- } catch (error) {
1782
- const errorText = error && error.details ? JSON.stringify(error.details, null, 2) : String(error.message || error);
1783
- writeMessage(successResponse(id, { content: [{ type: 'text', text: errorText }], isError: true }));
1784
- }
1785
- return;
1786
- }
1787
-
1788
- writeMessage(errorResponse(id, -32601, 'Method not found'));
1789
- }
1790
-
1791
- function start() {
1792
- process.stdin.setEncoding('utf8');
1793
- let buffer = '';
1794
- process.stdin.on('data', (chunk) => {
1795
- buffer += chunk;
1796
- let newlineIndex = buffer.indexOf('\n');
1797
- while (newlineIndex >= 0) {
1798
- const raw = buffer.slice(0, newlineIndex).trim();
1799
- buffer = buffer.slice(newlineIndex + 1);
1800
- if (raw) {
1801
- try {
1802
- const message = JSON.parse(raw);
1803
- Promise.resolve(onMessage(message)).catch((error) => {
1804
- if (message && Object.prototype.hasOwnProperty.call(message, 'id')) {
1805
- writeMessage(errorResponse(message.id, -32603, error.message));
1806
- }
1807
- });
1808
- } catch (error) {
1809
- writeMessage(errorResponse(null, -32700, 'Parse error: ' + error.message));
1810
- }
1811
- }
1812
- newlineIndex = buffer.indexOf('\n');
1813
- }
1814
- });
1815
- }
1816
-
1817
- start();
8
+ const SERVER_VERSION = '0.1.11';
9
+ const PROTOCOL_VERSION = '2024-11-05';
10
+ const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
+ const TEMPLATE_LIBRARY_ROOT = path.resolve(__dirname, '..', 'template');
12
+ const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
13
+ const DEFAULT_DESIGN_FILE = path.resolve(__dirname, '..', 'sql', 'SQL 闁荤姳鐒﹀畷姗€顢橀崫銉﹀珰閻庢稒蓱椤?md');
14
+ const STYLE_CATALOG = loadStyleCatalog();
15
+ const DEFAULT_DICT_REGISTRY_KEYS = {
16
+ add_start_stop: 'COMMON_STATUS',
17
+ trade_standard_type: 'TRADE_STANDARD_TYPE',
18
+ trade_score_standard: 'TRADE_SCORE_STANDARD',
19
+ };
20
+ const DEFAULT_CRUD_SCHEMA_TEMPLATE = `export interface FieldMeta {
21
+ show: boolean;
22
+ alwaysHide: boolean;
23
+ smart: boolean;
24
+ labelKey: string;
25
+ label?: string;
26
+ width: string;
27
+ dictType?: string;
28
+ }
29
+
30
+ export interface FieldConfig {
31
+ key: string;
32
+ labelKey?: string;
33
+ label?: string;
34
+ width?: string;
35
+ show?: boolean;
36
+ alwaysHide?: boolean;
37
+ smart?: boolean;
38
+ dictType?: string;
39
+ }
40
+
41
+ export interface CrudSchemaDefinition {
42
+ master: FieldConfig[];
43
+ children?: Record<string, FieldConfig[]>;
44
+ }
45
+
46
+ export interface CrudSchema {
47
+ master: Record<string, FieldMeta>;
48
+ children: Record<string, Record<string, FieldMeta>>;
49
+ filterTypes: Record<string, number>;
50
+ childFilterTypes: Record<string, Record<string, number>>;
51
+ allDictTypes: string[];
52
+ }
53
+
54
+ const DEFAULT_WIDTH = '120';
55
+
56
+ export const field = (labelKey: string, width = DEFAULT_WIDTH): FieldMeta => ({
57
+ show: true,
58
+ alwaysHide: false,
59
+ smart: false,
60
+ labelKey,
61
+ width,
62
+ });
63
+
64
+ export const dictField = (labelKey: string, dictType: string, width = DEFAULT_WIDTH): FieldMeta => ({
65
+ ...field(labelKey, width),
66
+ dictType,
67
+ });
68
+
69
+ export const buildFilterTypes = (fields: Record<string, FieldMeta>) =>
70
+ Object.fromEntries(Object.entries(fields).filter(([, item]) => item.dictType).map(([key]) => [key, 30]));
71
+
72
+ export const collectDictTypes = (...groups: Array<Record<string, FieldMeta>>) =>
73
+ Array.from(
74
+ new Set(
75
+ groups.flatMap((group) =>
76
+ Object.values(group)
77
+ .map((item) => item.dictType)
78
+ .filter(Boolean) as string[]
79
+ )
80
+ )
81
+ );
82
+
83
+ const normalizeField = (item: FieldConfig): FieldMeta => ({
84
+ show: item.show ?? true,
85
+ alwaysHide: item.alwaysHide ?? false,
86
+ smart: item.smart ?? false,
87
+ labelKey: item.labelKey ?? item.label ?? item.key,
88
+ ...(item.label ? { label: item.label } : {}),
89
+ width: item.width ?? DEFAULT_WIDTH,
90
+ ...(item.dictType ? { dictType: item.dictType } : {}),
91
+ });
92
+
93
+ const toFieldMap = (fields: FieldConfig[]) =>
94
+ Object.fromEntries(fields.map((item) => [item.key, normalizeField(item)])) as Record<string, FieldMeta>;
95
+
96
+ const buildSchema = (
97
+ master: Record<string, FieldMeta>,
98
+ children: Record<string, Record<string, FieldMeta>> = {}
99
+ ): CrudSchema => ({
100
+ master,
101
+ children,
102
+ filterTypes: buildFilterTypes(master),
103
+ childFilterTypes: Object.fromEntries(
104
+ Object.entries(children).map(([key, fields]) => [key, buildFilterTypes(fields)])
105
+ ),
106
+ allDictTypes: collectDictTypes(master, ...Object.values(children)),
107
+ });
108
+
109
+ export function createCrudSchema(master: Record<string, FieldMeta>, children?: Record<string, Record<string, FieldMeta>>): CrudSchema;
110
+ export function createCrudSchema(definition: CrudSchemaDefinition): CrudSchema;
111
+ export function createCrudSchema(
112
+ masterOrDefinition: Record<string, FieldMeta> | CrudSchemaDefinition,
113
+ children: Record<string, Record<string, FieldMeta>> = {}
114
+ ): CrudSchema {
115
+ if ('master' in masterOrDefinition && Array.isArray(masterOrDefinition.master)) {
116
+ const master = toFieldMap(masterOrDefinition.master);
117
+ const childGroups = Object.fromEntries(
118
+ Object.entries(masterOrDefinition.children ?? {}).map(([key, fields]) => [key, toFieldMap(fields)])
119
+ ) as Record<string, Record<string, FieldMeta>>;
120
+ return buildSchema(master, childGroups);
121
+ }
122
+
123
+ return buildSchema(masterOrDefinition as Record<string, FieldMeta>, children);
124
+ }
125
+ `;
126
+
127
+ const TOOL_SCHEMA = {
128
+ type: 'object',
129
+ properties: {
130
+ designFile: { type: 'string', description: 'Absolute or relative Markdown design file path. Defaults to ../sql/SQL 闁荤姳鐒﹀畷姗€顢橀崫銉﹀珰閻庢稒蓱椤?md when omitted.' },
131
+ tableName: { type: 'string', description: 'Target main table name from the design file.' },
132
+ style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Style id from assets/style-catalog.json.' },
133
+ children: {
134
+ type: 'array',
135
+ description: 'Optional direct child-table relations for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form.',
136
+ items: {
137
+ type: 'object',
138
+ properties: {
139
+ childTableName: { type: 'string', description: 'Child table name.' },
140
+ mainField: { type: 'string', description: 'Main table relation field.' },
141
+ childField: { type: 'string', description: 'Child table relation field.' },
142
+ relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
143
+ },
144
+ required: ['childTableName', 'mainField', 'childField'],
145
+ additionalProperties: false,
146
+ },
147
+ },
148
+ childTableName: { type: 'string', description: 'Child table name. Required when style=master_child_jump.' },
149
+ mainField: { type: 'string', description: 'Main table relation field. Required when style=master_child_jump.' },
150
+ childField: { type: 'string', description: 'Child table relation field. Required when style=master_child_jump.' },
151
+ relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
152
+ frontendPath: { type: 'string', description: 'Absolute frontend output root path.' },
153
+ moduleName: { type: 'string', description: 'Relative frontend module path, for example admin/test.' },
154
+ writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
155
+ overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
156
+ },
157
+ required: ['tableName', 'style', 'frontendPath'],
158
+ additionalProperties: false,
159
+ };
160
+
161
+ function loadStyleCatalog() {
162
+ return JSON.parse(fs.readFileSync(STYLE_CATALOG_PATH, 'utf8'));
163
+ }
164
+
165
+ function writeMessage(payload) {
166
+ process.stdout.write(JSON.stringify(payload) + '\n');
167
+ }
168
+
169
+ function successResponse(id, result) {
170
+ return { jsonrpc: '2.0', id, result };
171
+ }
172
+
173
+ function errorResponse(id, code, message) {
174
+ return { jsonrpc: '2.0', id, error: { code, message } };
175
+ }
176
+
177
+ function toolTextResult(text) {
178
+ return { content: [{ type: 'text', text }], isError: false };
179
+ }
180
+
181
+ function toCamelCase(value) {
182
+ return value.replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
183
+ }
184
+
185
+ function toPascalCase(value) {
186
+ const camel = toCamelCase(value);
187
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
188
+ }
189
+
190
+ function normalizeModuleName(moduleName) {
191
+ if (!moduleName) {
192
+ return 'admin/test';
193
+ }
194
+ return moduleName.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
195
+ }
196
+
197
+ function toConstantCase(value) {
198
+ return String(value || '')
199
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
200
+ .replace(/[^a-zA-Z0-9]+/g, '_')
201
+ .replace(/^_+|_+$/g, '')
202
+ .toUpperCase();
203
+ }
204
+
205
+ function parseDictRegistryEntries(fileContent) {
206
+ const blockMatch = String(fileContent || '').match(/export const DictRegistry\s*=\s*{([\s\S]*?)}\s*as const;/);
207
+ if (!blockMatch) return [];
208
+
209
+ const entries = [];
210
+ const entryRegex = /^\s*([A-Z0-9_]+)\s*:\s*['"]([^'"]+)['"]\s*,?\s*$/gm;
211
+ let match = entryRegex.exec(blockMatch[1]);
212
+ while (match) {
213
+ entries.push({ key: match[1], value: match[2] });
214
+ match = entryRegex.exec(blockMatch[1]);
215
+ }
216
+ return entries;
217
+ }
218
+
219
+ function renderDictRegistryContent(entries) {
220
+ const lines = entries.map((entry) => ` ${entry.key}: '${entry.value}',`);
221
+ return [
222
+ 'export const DictRegistry = {',
223
+ ...lines,
224
+ '} as const;',
225
+ '',
226
+ 'export type DictRegistryKey = keyof typeof DictRegistry;',
227
+ 'export type DictType = (typeof DictRegistry)[DictRegistryKey];',
228
+ '',
229
+ ].join('\n');
230
+ }
231
+
232
+ function isPlainObject(value) {
233
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
234
+ }
235
+
236
+ function parseExportDefaultObject(fileContent) {
237
+ const source = String(fileContent || '').replace(/^\uFEFF/, '').trim();
238
+ if (!source) return null;
239
+ try {
240
+ const executable = source.replace(/^export\s+default/, 'return');
241
+ return new Function(executable)();
242
+ } catch (error) {
243
+ return null;
244
+ }
245
+ }
246
+
247
+ function deepMergeMissing(target, source) {
248
+ if (!isPlainObject(source)) {
249
+ return target === undefined ? source : target;
250
+ }
251
+
252
+ const result = isPlainObject(target) ? { ...target } : {};
253
+ for (const [key, value] of Object.entries(source)) {
254
+ if (result[key] === undefined) {
255
+ result[key] = value;
256
+ continue;
257
+ }
258
+ if (isPlainObject(result[key]) && isPlainObject(value)) {
259
+ result[key] = deepMergeMissing(result[key], value);
260
+ }
261
+ }
262
+ return result;
263
+ }
264
+
265
+ function escapeTsString(value) {
266
+ return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
267
+ }
268
+
269
+ function renderTsLiteral(value, indentLevel = 0) {
270
+ const indent = ' '.repeat(indentLevel);
271
+ const childIndent = ' '.repeat(indentLevel + 1);
272
+
273
+ if (typeof value === 'string') {
274
+ return `'${escapeTsString(value)}'`;
275
+ }
276
+ if (typeof value === 'number' || typeof value === 'boolean') {
277
+ return String(value);
278
+ }
279
+ if (Array.isArray(value)) {
280
+ if (!value.length) return '[]';
281
+ return ['[', ...value.map((item) => `${childIndent}${renderTsLiteral(item, indentLevel + 1)},`), `${indent}]`].join('\n');
282
+ }
283
+ if (isPlainObject(value)) {
284
+ const entries = Object.entries(value);
285
+ if (!entries.length) return '{}';
286
+ return [
287
+ '{',
288
+ ...entries.map(([key, item]) => {
289
+ const renderedKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${escapeTsString(key)}'`;
290
+ return `${childIndent}${renderedKey}: ${renderTsLiteral(item, indentLevel + 1)},`;
291
+ }),
292
+ `${indent}}`,
293
+ ].join('\n');
294
+ }
295
+ return 'null';
296
+ }
297
+
298
+ function renderExportDefaultContent(objectValue) {
299
+ return `export default ${renderTsLiteral(objectValue, 0)};\n`;
300
+ }
301
+
302
+ function getPreferredDictRegistryKey(dictType) {
303
+ return DEFAULT_DICT_REGISTRY_KEYS[dictType] || toConstantCase(dictType) || 'DICT_TYPE';
304
+ }
305
+
306
+ function buildUniqueDictRegistryKey(dictType, usedKeys, existingByKey) {
307
+ const preferred = getPreferredDictRegistryKey(dictType);
308
+ let candidate = preferred;
309
+ let index = 2;
310
+
311
+ while (usedKeys.has(candidate) && existingByKey.get(candidate) !== dictType) {
312
+ candidate = `${preferred}_${index}`;
313
+ index += 1;
314
+ }
315
+
316
+ return candidate;
317
+ }
318
+
319
+ function buildI18nNamespaceSegments(model) {
320
+ return [...model.moduleName.split('/').filter(Boolean), model.functionName];
321
+ }
322
+
323
+ function buildI18nNamespace(model) {
324
+ return buildI18nNamespaceSegments(model).join('.');
325
+ }
326
+
327
+ function removeFeatureCommonLocaleSections(localeObject, model) {
328
+ if (!isPlainObject(localeObject)) {
329
+ return localeObject;
330
+ }
331
+
332
+ const segments = buildI18nNamespaceSegments(model);
333
+ let cursor = localeObject;
334
+ for (let index = 0; index < segments.length; index += 1) {
335
+ const segment = segments[index];
336
+ if (!isPlainObject(cursor[segment])) {
337
+ return localeObject;
338
+ }
339
+ if (index === segments.length - 1) {
340
+ delete cursor[segment].placeholders;
341
+ delete cursor[segment].actions;
342
+ delete cursor[segment].messages;
343
+ return localeObject;
344
+ }
345
+ cursor = cursor[segment];
346
+ }
347
+
348
+ return localeObject;
349
+ }
350
+
351
+ function buildFieldLabelKey(model, field) {
352
+ return `${buildI18nNamespace(model)}.fields.${field.attrName}`;
353
+ }
354
+
355
+ function buildChildFieldLabelKey(model, childModel, field) {
356
+ return `${buildI18nNamespace(model)}.children.${childModel.listName}.fields.${field.attrName}`;
357
+ }
358
+
359
+ function buildChildSectionTitleKey(model, childModel) {
360
+ return `${buildI18nNamespace(model)}.children.${childModel.listName}.title`;
361
+ }
362
+
363
+ function buildLocaleLeaf(model) {
364
+ const leaf = {
365
+ title: model.tableComment,
366
+ fields: Object.fromEntries(model.visibleFields.map((field) => [field.attrName, stripDictAnnotation(field.comment)])),
367
+ };
368
+
369
+ if (model.children.length) {
370
+ leaf.children = Object.fromEntries(
371
+ model.children.map((childModel) => [
372
+ childModel.listName,
373
+ {
374
+ title: childModel.tableComment,
375
+ fields: Object.fromEntries(childModel.visibleFields.map((field) => [field.attrName, stripDictAnnotation(field.comment)])),
376
+ },
377
+ ])
378
+ );
379
+ }
380
+
381
+ return leaf;
382
+ }
383
+
384
+ function buildZhCnLocaleObject(model) {
385
+ const root = {};
386
+ const segments = buildI18nNamespaceSegments(model);
387
+ let cursor = root;
388
+
389
+ for (let index = 0; index < segments.length - 1; index += 1) {
390
+ const segment = segments[index];
391
+ cursor[segment] = cursor[segment] || {};
392
+ cursor = cursor[segment];
393
+ }
394
+
395
+ cursor[segments[segments.length - 1]] = buildLocaleLeaf(model);
396
+ return root;
397
+ }
398
+
399
+ function prepareZhCnLocaleFile(model) {
400
+ const localePath = path.join(model.frontendPath, 'src', 'i18n', 'biz', ...model.moduleName.split('/'), `${model.functionName}.zh-cn.ts`);
401
+ const exists = fs.existsSync(localePath);
402
+ const currentContent = exists ? readUtf8File(localePath) : '';
403
+ const currentObject = exists ? parseExportDefaultObject(currentContent) : null;
404
+ const generatedObject = buildZhCnLocaleObject(model);
405
+ const isCompatible = !exists || isPlainObject(currentObject);
406
+ const sanitizedCurrentObject = isCompatible ? removeFeatureCommonLocaleSections(currentObject || {}, model) : null;
407
+ const mergedObject = isCompatible ? deepMergeMissing(sanitizedCurrentObject || {}, generatedObject) : null;
408
+
409
+ return {
410
+ path: localePath,
411
+ frontendPath: model.frontendPath,
412
+ exists,
413
+ isCompatible,
414
+ namespace: buildI18nNamespace(model),
415
+ content: mergedObject ? renderExportDefaultContent(mergedObject) : '',
416
+ needsWrite: !exists || (isCompatible && renderExportDefaultContent(currentObject || {}) !== renderExportDefaultContent(mergedObject)),
417
+ };
418
+ }
419
+
420
+ function prepareDictRegistry(frontendPath, dictTypes) {
421
+ const registryPath = path.join(frontendPath, 'src', 'enums', 'dict-registry.ts');
422
+ const exists = fs.existsSync(registryPath);
423
+ const existingEntries = exists ? parseDictRegistryEntries(readUtf8File(registryPath)) : [];
424
+ const entries = existingEntries.map((entry) => ({ ...entry }));
425
+ const keyByValue = new Map(entries.map((entry) => [entry.value, entry.key]));
426
+ const existingByKey = new Map(entries.map((entry) => [entry.key, entry.value]));
427
+ const usedKeys = new Set(entries.map((entry) => entry.key));
428
+ let changed = !exists;
429
+
430
+ for (const dictType of dictTypes) {
431
+ if (!dictType || keyByValue.has(dictType)) continue;
432
+ const key = buildUniqueDictRegistryKey(dictType, usedKeys, existingByKey);
433
+ entries.push({ key, value: dictType });
434
+ keyByValue.set(dictType, key);
435
+ existingByKey.set(key, dictType);
436
+ usedKeys.add(key);
437
+ changed = true;
438
+ }
439
+
440
+ return {
441
+ path: registryPath,
442
+ entries,
443
+ keyByValue,
444
+ needsWrite: changed,
445
+ };
446
+ }
447
+
448
+ function ensureCrudSchemaSupportFile(frontendPath) {
449
+ const schemaPath = path.join(frontendPath, 'src', 'utils', 'crudSchema.ts');
450
+ const exists = fs.existsSync(schemaPath);
451
+ const currentContent = exists ? readUtf8File(schemaPath) : '';
452
+ const hasCoreShape =
453
+ currentContent.includes('export interface CrudSchemaDefinition') &&
454
+ currentContent.includes('export function createCrudSchema') &&
455
+ currentContent.includes('export interface FieldConfig');
456
+ const supportsLabelKey = currentContent.includes('labelKey');
457
+ const shouldUpgradeLegacy = exists && hasCoreShape && !supportsLabelKey;
458
+
459
+ return {
460
+ path: schemaPath,
461
+ content: DEFAULT_CRUD_SCHEMA_TEMPLATE,
462
+ exists,
463
+ isCompatible: hasCoreShape && supportsLabelKey,
464
+ needsWrite: !exists || shouldUpgradeLegacy,
465
+ };
466
+ }
467
+
468
+ function ensureDirectory(filePath) {
469
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
470
+ }
471
+
472
+ function writeSupportFile(filePath, content) {
473
+ ensureDirectory(filePath);
474
+ fs.writeFileSync(filePath, content, 'utf8');
475
+ }
476
+
477
+ function prepareSharedSupport(frontendPath, dictTypes) {
478
+ const normalizedDictTypes = [...new Set((dictTypes || []).filter(Boolean))];
479
+ const dictRegistry = prepareDictRegistry(frontendPath, normalizedDictTypes);
480
+ const crudSchema = ensureCrudSchemaSupportFile(frontendPath);
481
+ return {
482
+ dictRegistry,
483
+ crudSchema,
484
+ };
485
+ }
486
+
487
+ function maybeWriteSharedSupport(sharedSupport, writeToDisk) {
488
+ if (!writeToDisk) return;
489
+
490
+ if (sharedSupport.crudSchema.needsWrite) {
491
+ writeSupportFile(sharedSupport.crudSchema.path, sharedSupport.crudSchema.content);
492
+ }
493
+
494
+ if (sharedSupport.dictRegistry.needsWrite) {
495
+ writeSupportFile(sharedSupport.dictRegistry.path, renderDictRegistryContent(sharedSupport.dictRegistry.entries));
496
+ }
497
+ }
498
+
499
+ function buildSupportNote(sharedSupport, localeZhSupport) {
500
+ const notes = [];
501
+
502
+ if (sharedSupport.crudSchema.exists && !sharedSupport.crudSchema.isCompatible) {
503
+ notes.push(
504
+ 'Detected an existing src/utils/crudSchema.ts that does not match the expected helper signature. ' +
505
+ 'MCP preserved the existing file and did not overwrite it. Generated pages now depend on that file being manually aligned.'
506
+ );
507
+ }
508
+
509
+ if (localeZhSupport.exists && !localeZhSupport.isCompatible) {
510
+ notes.push(
511
+ `Detected an existing ${path.relative(localeZhSupport.frontendPath, localeZhSupport.path).replace(/\\/g, '/')} that MCP could not parse. ` +
512
+ 'The file was preserved and not updated, so new Chinese i18n keys may need to be merged manually.'
513
+ );
514
+ }
515
+
516
+ return notes.length ? notes.join(' ') : 'Runtime template rendering completed.';
517
+ }
518
+
519
+ function getDictRegistryReference(dictType, keyByValue) {
520
+ if (!dictType) return '';
521
+ const key = keyByValue.get(dictType) || getPreferredDictRegistryKey(dictType);
522
+ return `DictRegistry.${key}`;
523
+ }
524
+
525
+ function isAuditField(fieldName) {
526
+ return [
527
+ 'id',
528
+ 'tenant_id',
529
+ 'version',
530
+ 'create_time',
531
+ 'update_time',
532
+ 'create_user_id',
533
+ 'update_user_id',
534
+ 'create_user',
535
+ 'update_user',
536
+ 'create_pos_id',
537
+ 'create_pos',
538
+ 'create_dpt_id',
539
+ 'create_dpt',
540
+ 'create_ogn_id',
541
+ 'create_ogn',
542
+ 'create_psm_full_id',
543
+ 'create_psm_full_name',
544
+ 'update_psm_full_id',
545
+ 'update_psm_full_name',
546
+ 'del_flag',
547
+ ].includes(fieldName);
548
+ }
549
+
550
+ function findDictType(comment) {
551
+ return extractDictType(comment);
552
+ const match = comment.match(/(?:闁诲孩绋掗〃鍛村触閳х憢dict)[闂?\s]*([a-zA-Z0-9_]+)/i);
553
+ }
554
+
555
+ function mapFieldType(field) {
556
+ if (field.dictType) return 'select';
557
+ if (field.sqlType === 'DATETIME' || field.sqlType === 'TIMESTAMP') return 'datetime';
558
+ if (field.sqlType === 'DATE') return 'date';
559
+ if (['INT', 'BIGINT', 'DECIMAL', 'NUMERIC'].includes(field.sqlType)) return 'number';
560
+ if (field.sqlType === 'TEXT') return 'textarea';
561
+ if (field.sqlType === 'VARCHAR' && field.length && Number(field.length) > 64) return 'textarea';
562
+ return 'text';
563
+ }
564
+
565
+ function escapeForRegex(value) {
566
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
567
+ }
568
+
569
+ function stripBom(text) {
570
+ return text.replace(/^\uFEFF/, '');
571
+ }
572
+
573
+ function readUtf8File(filePath) {
574
+ return stripBom(fs.readFileSync(filePath, 'utf8'));
575
+ }
576
+
577
+ function normalizeDictType(value) {
578
+ const normalized = String(value || '').trim();
579
+ if (!normalized || normalized === '-' || normalized === '/') {
580
+ return null;
581
+ }
582
+ return normalized.replace(/^['"`]+|['"`]+$/g, '');
583
+ }
584
+
585
+ function stripDictAnnotation(label) {
586
+ const text = String(label || '').trim();
587
+ if (!text) {
588
+ return '';
589
+ }
590
+ return text
591
+ .replace(/\s*[\((][^()()]*?(?:字典|dict)(?:[_\s-]*type)?[^()()]*?[\))]\s*/gi, '')
592
+ .replace(/\s+/g, ' ')
593
+ .trim();
594
+ }
595
+
596
+ function extractDictType(text) {
597
+ const normalized = String(text || '').trim();
598
+ if (!normalized) {
599
+ return null;
600
+ }
601
+
602
+ const patterns = [
603
+ /(?:鐎涙鍚€|dict)(?:[_\s-]*type)?\s*[:閿涙瓥\s*([a-zA-Z0-9_-]+)/i,
604
+ /[\(閿涘溈\s*(?:鐎涙鍚€|dict)(?:[_\s-]*type)?\s*[:閿涙瓥?\s*([a-zA-Z0-9_-]+)\s*[\)閿涘、/i,
605
+ ];
606
+
607
+ for (const pattern of patterns) {
608
+ const match = normalized.match(pattern);
609
+ if (match && match[1]) {
610
+ return normalizeDictType(match[1]);
611
+ }
612
+ }
613
+
614
+ return null;
615
+ }
616
+
617
+ function resolveDictType(comment, explicitDictType) {
618
+ return normalizeDictType(explicitDictType) || extractDictType(comment);
619
+ }
620
+
621
+ function detectDictType(comment) {
622
+ return extractDictType(comment);
623
+ }
624
+
625
+ function splitLength(value) {
626
+ const normalized = String(value || '').trim();
627
+ if (!normalized || normalized === '-' || normalized === '/') {
628
+ return { length: '', scale: '' };
629
+ }
630
+ const parts = normalized.split(',').map((item) => item.trim()).filter(Boolean);
631
+ return { length: parts[0] || '', scale: parts[1] || '' };
632
+ }
633
+
634
+ function normalizeDefaultValue(value) {
635
+ const normalized = String(value || '').trim();
636
+ if (!normalized || normalized === '-' || normalized === '/') {
637
+ return '';
638
+ }
639
+ return normalized;
640
+ }
641
+
642
+ function parseRequiredFlag(value) {
643
+ const normalized = String(value || '').trim().toLowerCase();
644
+ return ['闂?, 'y', 'yes', '1', 'true', '闂婎偄娲ら幊搴ㄦ晲?].includes(normalized);
645
+ }
646
+
647
+ function parseMarkdownRow(line) {
648
+ const trimmed = line.trim();
649
+ if (!trimmed.startsWith('|')) return [];
650
+ return trimmed
651
+ .slice(1, trimmed.endsWith('|') ? -1 : undefined)
652
+ .split('|')
653
+ .map((cell) => cell.trim());
654
+ }
655
+
656
+ function isMarkdownSeparatorRow(cells) {
657
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, '')));
658
+ }
659
+
660
+ function findHeaderIndex(headers, patterns) {
661
+ return headers.findIndex((header) => patterns.some((pattern) => pattern.test(header)));
662
+ }
663
+
664
+ function findPrimaryKeyFromText(text, fields) {
665
+ const quotedMatch = text.match(/PRIMARY\s+KEY\s*\(\s*`?([a-zA-Z0-9_]+)`?\s*\)/i);
666
+ if (quotedMatch) {
667
+ return quotedMatch[1];
668
+ }
669
+
670
+ const commentPk = fields.find((field) => /婵炴垶鎸搁…鐑藉极?i.test(field.comment));
671
+ if (commentPk) {
672
+ return commentPk.fieldName;
673
+ }
674
+
675
+ const idField = fields.find((field) => field.fieldName === 'id');
676
+ if (idField) {
677
+ return idField.fieldName;
678
+ }
679
+
680
+ return fields[0] ? fields[0].fieldName : null;
681
+ }
682
+
683
+ function parseMarkdownTableSection(tableName, tableComment, sectionLines) {
684
+ const firstTableLineIndex = sectionLines.findIndex((line) => line.trim().startsWith('|'));
685
+ if (firstTableLineIndex < 0) {
686
+ throw new Error('Could not find markdown field table for ' + tableName);
687
+ }
688
+
689
+ const tableLines = [];
690
+ for (let index = firstTableLineIndex; index < sectionLines.length; index += 1) {
691
+ const line = sectionLines[index].trim();
692
+ if (!line.startsWith('|')) break;
693
+ tableLines.push(line);
694
+ }
695
+
696
+ const rows = tableLines.map(parseMarkdownRow).filter((cells) => cells.length > 0);
697
+ if (rows.length < 3) {
698
+ throw new Error('Markdown field table is incomplete for ' + tableName);
699
+ }
700
+
701
+ const headers = rows[0];
702
+ const dataRows = rows.slice(1).filter((cells) => !isMarkdownSeparatorRow(cells));
703
+ const fieldNameIndex = findHeaderIndex(headers, [/闁诲孩绋掗〃鍡涱敊瀹€鍕Е?, /闂佸憡甯楅〃鍛村箖?i, /field/i]);
704
+ const sqlTypeIndex = findHeaderIndex(headers, [/缂備緡鍋夐褔鎮?, /闂佽桨鑳舵晶妤€鐣垫担铏瑰暗閻犲洩灏欓埀?, /type/i]);
705
+ const lengthIndex = findHeaderIndex(headers, [/闂傚倵鍋撻柛顭戝枛椤?, /缂備緡鍠楅崕宕団偓?, /length/i]);
706
+ const requiredIndex = findHeaderIndex(headers, [/闂婎偄娲ら幊搴ㄦ晲?, /闂婎偄娲ら幊鎾舵?, /required/i]);
707
+ const defaultIndex = findHeaderIndex(headers, [/婵帗绋掗…鍫ヮ敇?, /default/i]);
708
+ const commentIndex = findHeaderIndex(headers, [/闁荤姴娲ら悺銊ノ?, /婵犮垼娉涘ú锕傚极?, /comment/i]);
709
+
710
+ const dictTypeIndex = findHeaderIndex(headers, [/闁诲孩绋掗〃鍛村触閳?, /dict/i, /dict_?type/i]);
711
+
712
+ if (fieldNameIndex < 0 || sqlTypeIndex < 0) {
713
+ throw new Error('Markdown field table headers are missing required columns for ' + tableName);
714
+ }
715
+
716
+ const fields = [];
717
+ for (const row of dataRows) {
718
+ const fieldName = String(row[fieldNameIndex] || '').trim();
719
+ if (!fieldName || fieldName === '-') continue;
720
+
721
+ const sqlType = String(row[sqlTypeIndex] || '').trim().toUpperCase();
722
+ if (!sqlType) continue;
723
+
724
+ const lengthRaw = lengthIndex >= 0 ? row[lengthIndex] : '';
725
+ const comment = String(commentIndex >= 0 ? row[commentIndex] : '').trim() || fieldName;
726
+ const explicitDictType = dictTypeIndex >= 0 ? row[dictTypeIndex] : '';
727
+ const { length, scale } = splitLength(lengthRaw);
728
+
729
+ fields.push({
730
+ fieldName,
731
+ attrName: toCamelCase(fieldName),
732
+ sqlType,
733
+ length,
734
+ scale,
735
+ comment,
736
+ dictType: resolveDictType(comment, explicitDictType),
737
+ notNull: requiredIndex >= 0 ? parseRequiredFlag(row[requiredIndex]) : false,
738
+ defaultValue: defaultIndex >= 0 ? normalizeDefaultValue(row[defaultIndex]) : '',
739
+ });
740
+ }
741
+
742
+ if (!fields.length) {
743
+ throw new Error('No fields were parsed from markdown table ' + tableName);
744
+ }
745
+
746
+ const sectionText = sectionLines.join('\n');
747
+ const pkName = findPrimaryKeyFromText(sectionText, fields);
748
+ const pkField = fields.find((field) => field.fieldName === pkName) || fields[0];
749
+ return { pkField, fields, tableComment };
750
+ }
751
+
752
+ function parseMarkdownDesignTables(markdownText) {
753
+ const lines = stripBom(markdownText).replace(/\r\n?/g, '\n').split('\n');
754
+ const tables = new Map();
755
+
756
+ for (let index = 0; index < lines.length; index += 1) {
757
+ const heading = lines[index].trim();
758
+ const headingMatch = heading.match(/^###\s+\S+\s+([a-zA-Z0-9_]+)\s*[\(闂佹寧绋戝┃?[^)\n闂佹寧绋戦妴?)[\)闂佹寧绋戦妴?);
759
+ if (!headingMatch) continue;
760
+
761
+ const tableName = headingMatch[1];
762
+ const tableComment = headingMatch[2].trim();
763
+ const sectionLines = [];
764
+ let nextIndex = index + 1;
765
+ while (nextIndex < lines.length && !/^(##|###)\s+/.test(lines[nextIndex])) {
766
+ sectionLines.push(lines[nextIndex]);
767
+ nextIndex += 1;
768
+ }
769
+
770
+ tables.set(tableName, parseMarkdownTableSection(tableName, tableComment, sectionLines));
771
+ index = nextIndex - 1;
772
+ }
773
+
774
+ return tables;
775
+ }
776
+
777
+ function extractIdentifiersFromLine(line) {
778
+ return [...String(line || '').matchAll(/`?([a-zA-Z][a-zA-Z0-9_]*)`?/g)].map((match) => match[1]);
779
+ }
780
+
781
+ function stripMarkdownSyntax(text) {
782
+ return String(text || '')
783
+ .replace(/\*\*/g, '')
784
+ .replace(/`/g, '')
785
+ .replace(/\r\n?/g, '\n');
786
+ }
787
+
788
+ function parseMarkdownRelations(markdownText) {
789
+ const text = stripMarkdownSyntax(markdownText);
790
+ const blocks = [];
791
+ const headingRegex = /^(###|####)\s+(.+)$/gm;
792
+ let current = null;
793
+ let match = headingRegex.exec(text);
794
+
795
+ while (match) {
796
+ if (current) {
797
+ current.body = text.slice(current.start, match.index);
798
+ blocks.push(current);
799
+ }
800
+
801
+ current = {
802
+ title: match[2].trim(),
803
+ start: headingRegex.lastIndex,
804
+ body: '',
805
+ };
806
+ match = headingRegex.exec(text);
807
+ }
808
+
809
+ if (current) {
810
+ current.body = text.slice(current.start);
811
+ blocks.push(current);
812
+ }
813
+
814
+ const relations = [];
815
+ for (const block of blocks) {
816
+ const lines = block.body.split('\n').map((line) => line.trim()).filter(Boolean);
817
+ const mainLine = lines.find((line) => /(婵炴垶鎸诲Σ鎺楀Υ閸庡嫰鏌i弽銊ュ姸闁?\s*[:闂佹寧绋掗悺?.test(line));
818
+ const childLine = lines.find((line) => /(婵炲濮村锕傚Υ閸庡嫰鎮楀☉娆忓闁?\s*[:闂佹寧绋掗悺?.test(line));
819
+ const fieldLine = lines.find((line) => /闂佺绻愰悿鍥ㄧ閸懇鍋撳☉娆樻畷妞ゆ柨顒竤*[:闂佹寧绋掗悺?.test(line));
820
+ if (!mainLine || !childLine || !fieldLine) continue;
821
+
822
+ const mainIdentifiers = extractIdentifiersFromLine(mainLine);
823
+ const childIdentifiers = extractIdentifiersFromLine(childLine);
824
+ const fieldIdentifiers = extractIdentifiersFromLine(fieldLine);
825
+ if (!mainIdentifiers.length || !childIdentifiers.length || fieldIdentifiers.length < 2) continue;
826
+
827
+ const relationTypeLine = lines.find((line) => /闂佺绻愮壕顓㈡焾鐎靛摜灏甸悹鍥皺閳ь剛锕*[:闂佹寧绋掗悺?.test(line));
828
+ relations.push({
829
+ title: block.title,
830
+ mainTableName: mainIdentifiers[0],
831
+ childTableName: childIdentifiers[0],
832
+ childField: fieldIdentifiers[0],
833
+ mainField: fieldIdentifiers[1],
834
+ relationType: relationTypeLine ? relationTypeLine.split(/[:闂佹寧绋掗悺?).slice(1).join(':').trim() : '',
835
+ });
836
+ }
837
+
838
+ return relations;
839
+ }
840
+
841
+ function parseMarkdownDesignFile(markdownText) {
842
+ return {
843
+ tables: parseMarkdownDesignTables(markdownText),
844
+ relations: [],
845
+ };
846
+ }
847
+
848
+ function parseTableFromMarkdownDesign(designDoc, tableName) {
849
+ const parsed = designDoc.tables.get(tableName);
850
+ if (!parsed) {
851
+ throw new Error('Could not find table definition for ' + tableName + ' in Markdown design file');
852
+ }
853
+ return parsed;
854
+ }
855
+
856
+ function buildQuestionMarkSegmentRegex(segment) {
857
+ return new RegExp('^' + escapeForRegex(segment).replace(/\\\?/g, '.') + '$', 'i');
858
+ }
859
+
860
+ function tryResolveGarbledPath(resolvedPath) {
861
+ if (!resolvedPath.includes('?')) {
862
+ return null;
863
+ }
864
+
865
+ const parsed = path.parse(resolvedPath);
866
+ const segments = resolvedPath.slice(parsed.root.length).split(path.sep).filter(Boolean);
867
+ let currentPath = parsed.root;
868
+
869
+ for (const segment of segments) {
870
+ const exactPath = path.join(currentPath, segment);
871
+ if (fs.existsSync(exactPath)) {
872
+ currentPath = exactPath;
873
+ continue;
874
+ }
875
+
876
+ if (!segment.includes('?') || !fs.existsSync(currentPath) || !fs.statSync(currentPath).isDirectory()) {
877
+ return null;
878
+ }
879
+
880
+ const matcher = buildQuestionMarkSegmentRegex(segment);
881
+ const matches = fs.readdirSync(currentPath).filter((name) => matcher.test(name));
882
+ if (matches.length !== 1) {
883
+ throw new Error(
884
+ 'Source file path could not be resolved uniquely from garbled input: ' +
885
+ resolvedPath +
886
+ '. Matched entries: ' +
887
+ (matches.length ? matches.join(', ') : 'none')
888
+ );
889
+ }
890
+
891
+ currentPath = path.join(currentPath, matches[0]);
892
+ }
893
+
894
+ return fs.existsSync(currentPath) ? currentPath : null;
895
+ }
896
+
897
+ function resolveSourcePath(inputPath) {
898
+ const resolvedPath = path.resolve(inputPath);
899
+ if (fs.existsSync(resolvedPath)) {
900
+ return resolvedPath;
901
+ }
902
+
903
+ const recoveredPath = tryResolveGarbledPath(resolvedPath);
904
+ if (recoveredPath) {
905
+ return recoveredPath;
906
+ }
907
+
908
+ throw new Error('Source file does not exist: ' + resolvedPath);
909
+ }
910
+
911
+ function formatRelationCandidate(relation) {
912
+ return {
913
+ childTableName: relation.childTableName,
914
+ mainField: relation.mainField,
915
+ childField: relation.childField,
916
+ relationType: relation.relationType || '',
917
+ };
918
+ }
919
+
920
+ function getRelationCandidates(designDoc, tableName) {
921
+ return designDoc.relations
922
+ .filter((relation) => relation.mainTableName === tableName)
923
+ .map(formatRelationCandidate);
924
+ }
925
+
926
+ function buildRetryArguments(safeArgs, sourceFile, childTableName) {
927
+ const retryArguments = {
928
+ tableName: safeArgs.tableName,
929
+ style: safeArgs.style,
930
+ frontendPath: safeArgs.frontendPath,
931
+ moduleName: safeArgs.moduleName,
932
+ writeToDisk: safeArgs.writeToDisk,
933
+ overwrite: safeArgs.overwrite,
934
+ };
935
+
936
+ if (sourceFile) {
937
+ retryArguments.designFile = sourceFile;
938
+ }
939
+
940
+ if (safeArgs.children && safeArgs.children.length) {
941
+ retryArguments.children = safeArgs.children.map((relation) => ({
942
+ childTableName: relation.childTableName,
943
+ mainField: relation.mainField,
944
+ childField: relation.childField,
945
+ relationType: relation.relationType || '',
946
+ }));
947
+ }
948
+
949
+ if (childTableName) {
950
+ retryArguments.childTableName = childTableName;
951
+ }
952
+
953
+ if (safeArgs.mainField) {
954
+ retryArguments.mainField = safeArgs.mainField;
955
+ }
956
+
957
+ if (safeArgs.childField) {
958
+ retryArguments.childField = safeArgs.childField;
959
+ }
960
+
961
+ if (safeArgs.relationType) {
962
+ retryArguments.relationType = safeArgs.relationType;
963
+ }
964
+
965
+ return retryArguments;
966
+ }
967
+
968
+ function buildRelationCorrectionEntry(safeArgs, sourceFile, candidates, selectedChildTableName) {
969
+ return {
970
+ field: 'childTableName',
971
+ title: '婵炴垶鎹侀褔鎮哄▎鎴炲仒闁靛鍎辫ぐ鐘绘煠鏉堛劍鐓i柟渚垮妼椤垽鏁愰崨顓炴辈闂?,
972
+ tableName: safeArgs.tableName,
973
+ style: safeArgs.style,
974
+ designFile: sourceFile,
975
+ description: candidates.length
976
+ ? 'If the current child relation is not the one you want, pass childTableName to choose a candidate from the design file.'
977
+ : 'No child relation was found. Check the 婵炴垶鎹侀濠勫垝閻樺灚鍋橀柕濞垮劚瑜扮娀鏌ゆ潏銊︻梿妞ゆ洍鏅犲?section in the design file.',
978
+ currentValue: selectedChildTableName || '',
979
+ options: candidates.map((item) => item.childTableName),
980
+ candidates,
981
+ example: candidates.length ? buildRetryArguments(safeArgs, sourceFile, candidates[0].childTableName) : buildRetryArguments(safeArgs, sourceFile, selectedChildTableName),
982
+ retryArguments: candidates.map((item) => buildRetryArguments(safeArgs, sourceFile, item.childTableName)),
983
+ };
984
+ }
985
+
986
+ function createRelationResolutionError(message, safeArgs, sourceFile, candidates, selectedChildTableName, status) {
987
+ const error = new Error(message);
988
+ error.details = {
989
+ type: 'relation_resolution',
990
+ status,
991
+ tableName: safeArgs.tableName,
992
+ designFile: sourceFile,
993
+ message,
994
+ candidates,
995
+ correctionEntry: buildRelationCorrectionEntry(safeArgs, sourceFile, candidates, selectedChildTableName),
996
+ };
997
+ return error;
998
+ }
999
+
1000
+ function loadSourceDocument(safeArgs) {
1001
+ const inputPath = safeArgs.designFile || DEFAULT_DESIGN_FILE;
1002
+ const sourcePath = resolveSourcePath(inputPath);
1003
+ const text = readUtf8File(sourcePath);
1004
+ return {
1005
+ path: sourcePath,
1006
+ text,
1007
+ designDoc: parseMarkdownDesignFile(text),
1008
+ };
1009
+ }
1010
+
1011
+ function normalizeChildrenInput(inputChildren) {
1012
+ if (inputChildren === undefined || inputChildren === null) {
1013
+ return [];
1014
+ }
1015
+ if (!Array.isArray(inputChildren)) {
1016
+ throw new Error('children must be an array');
1017
+ }
1018
+ return inputChildren.map((item, index) => {
1019
+ if (!item || typeof item !== 'object') {
1020
+ throw new Error('children[' + index + '] must be an object');
1021
+ }
1022
+ const childTableName = item.childTableName ? String(item.childTableName) : '';
1023
+ const mainField = item.mainField ? String(item.mainField) : '';
1024
+ const childField = item.childField ? String(item.childField) : '';
1025
+ const relationType = item.relationType ? String(item.relationType) : '';
1026
+ const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !({ childTableName, mainField, childField })[field]);
1027
+ if (missingFields.length) {
1028
+ throw new Error('children[' + index + '] is missing required fields: ' + missingFields.join(', '));
1029
+ }
1030
+ return { childTableName, mainField, childField, relationType };
1031
+ });
1032
+ }
1033
+
1034
+ function resolveMarkdownChildRelations(sourceDocument, safeArgs) {
1035
+ if (safeArgs.children && safeArgs.children.length) {
1036
+ return safeArgs.children.map((relation) => ({
1037
+ mainTableName: safeArgs.tableName,
1038
+ childTableName: relation.childTableName,
1039
+ mainField: relation.mainField,
1040
+ childField: relation.childField,
1041
+ relationType: relation.relationType || '',
1042
+ }));
1043
+ }
1044
+
1045
+ const missingFields = ['childTableName', 'mainField', 'childField'].filter((field) => !safeArgs[field]);
1046
+ if (missingFields.length) {
1047
+ const message = 'master_child_jump requires either children[] or legacy relation fields. Missing: ' + missingFields.join(', ') + '.';
1048
+ const error = new Error(message);
1049
+ error.details = {
1050
+ type: 'relation_input_required',
1051
+ status: 'missing_required_relation_input',
1052
+ tableName: safeArgs.tableName,
1053
+ designFile: sourceDocument.path,
1054
+ message,
1055
+ requiredFields: ['children[] or childTableName/mainField/childField'],
1056
+ correctionEntry: {
1057
+ fields: ['children', 'childTableName', 'mainField', 'childField'],
1058
+ example: {
1059
+ ...buildRetryArguments(safeArgs, sourceDocument.path, safeArgs.childTableName || '<child_table_name>'),
1060
+ children: [
1061
+ {
1062
+ childTableName: safeArgs.childTableName || '<child_table_name>',
1063
+ mainField: safeArgs.mainField || '<main_field>',
1064
+ childField: safeArgs.childField || '<child_field>',
1065
+ relationType: safeArgs.relationType || '1:N',
1066
+ },
1067
+ ],
1068
+ mainField: safeArgs.mainField || '<main_field>',
1069
+ childField: safeArgs.childField || '<child_field>',
1070
+ },
1071
+ },
1072
+ };
1073
+ throw error;
1074
+ }
1075
+
1076
+ return [
1077
+ {
1078
+ mainTableName: safeArgs.tableName,
1079
+ childTableName: safeArgs.childTableName,
1080
+ mainField: safeArgs.mainField,
1081
+ childField: safeArgs.childField,
1082
+ relationType: safeArgs.relationType || '',
1083
+ },
1084
+ ];
1085
+ }
1086
+
1087
+ function getStylePreset(styleId) {
1088
+ const preset = STYLE_CATALOG[styleId];
1089
+ if (!preset) throw new Error('Unsupported style: ' + styleId);
1090
+ return preset;
1091
+ }
1092
+
1093
+ function resolveSharedTemplates(stylePreset) {
1094
+ const selected = {};
1095
+ for (const [kind, templateName] of Object.entries(stylePreset.templateFiles || {})) {
1096
+ const templatePath = path.join(TEMPLATE_LIBRARY_ROOT, templateName);
1097
+ selected[kind] = { name: templateName, path: templatePath, exists: fs.existsSync(templatePath) };
1098
+ }
1099
+ return selected;
1100
+ }
1101
+
1102
+ function hasRuntimeSupport(stylePreset) {
1103
+ return Boolean(stylePreset.runtime && stylePreset.runtime.supported && stylePreset.runtime.templateDir);
1104
+ }
1105
+
1106
+ function normalizeFields(parsed) {
1107
+ return parsed.fields.map((field) => ({ ...field, formType: mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1108
+ }
1109
+
1110
+ function ensureFieldExists(fields, fieldName, tableName, role) {
1111
+ const field = fields.find((item) => item.fieldName === fieldName);
1112
+ if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
1113
+ return field;
1114
+ }
1115
+
1116
+ function buildChildModels(sourceDocument, safeArgs, mainParsed) {
1117
+ if (safeArgs.style !== 'master_child_jump') return [];
1118
+
1119
+ const relations = resolveMarkdownChildRelations(sourceDocument, safeArgs);
1120
+ return relations.map((relation) => {
1121
+ const childParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, relation.childTableName);
1122
+ const childFields = normalizeFields(childParsed);
1123
+ const mainRelationField = ensureFieldExists(mainParsed.fields, relation.mainField, safeArgs.tableName, 'Main relation');
1124
+ const childRelationField = ensureFieldExists(childParsed.fields, relation.childField, relation.childTableName, 'Child relation');
1125
+ const childVisibleFields = childFields.filter(
1126
+ (field) => field.fieldName !== childParsed.pkField.fieldName && !field.isAudit && field.fieldName !== relation.childField
1127
+ );
1128
+
1129
+ return {
1130
+ tableName: relation.childTableName,
1131
+ tableComment: childParsed.tableComment,
1132
+ className: toPascalCase(relation.childTableName),
1133
+ functionName: toCamelCase(relation.childTableName),
1134
+ listName: toCamelCase(relation.childTableName) + 'List',
1135
+ pk: childParsed.pkField,
1136
+ fields: childFields,
1137
+ visibleFields: childVisibleFields,
1138
+ mainField: mainRelationField,
1139
+ childField: childRelationField,
1140
+ relationType: relation.relationType,
1141
+ };
1142
+ });
1143
+ }
1144
+
1145
+ function buildModel(safeArgs) {
1146
+ const sourceDocument = loadSourceDocument(safeArgs);
1147
+ const mainParsed = parseTableFromMarkdownDesign(sourceDocument.designDoc, safeArgs.tableName);
1148
+ const fields = normalizeFields(mainParsed);
1149
+ const visibleFields = fields.filter((field) => field.fieldName !== mainParsed.pkField.fieldName && !field.isAudit);
1150
+ const gridFields = visibleFields.slice(0, 8);
1151
+ const children = buildChildModels(sourceDocument, safeArgs, mainParsed);
1152
+ const childDictTypes = children.flatMap((child) => child.visibleFields.map((field) => field.dictType).filter(Boolean));
1153
+ const dictTypes = [...new Set([...visibleFields.map((field) => field.dictType).filter(Boolean), ...childDictTypes])];
1154
+
1155
+ return {
1156
+ sourceFile: sourceDocument.path,
1157
+ designFile: sourceDocument.path,
1158
+ tableName: safeArgs.tableName,
1159
+ tableComment: mainParsed.tableComment,
1160
+ className: toPascalCase(safeArgs.tableName),
1161
+ functionName: toCamelCase(safeArgs.tableName),
1162
+ moduleName: normalizeModuleName(safeArgs.moduleName),
1163
+ pk: mainParsed.pkField,
1164
+ fields,
1165
+ visibleFields,
1166
+ gridFields,
1167
+ dictTypes,
1168
+ frontendPath: path.resolve(safeArgs.frontendPath),
1169
+ style: safeArgs.style,
1170
+ children,
1171
+ };
1172
+ }
1173
+
1174
+ function renderTemplate(templateText, replacements) {
1175
+ let output = templateText;
1176
+ for (const [key, value] of Object.entries(replacements)) {
1177
+ output = output.split('{{' + key + '}}').join(String(value));
1178
+ }
1179
+ return output;
1180
+ }
1181
+
1182
+ function renderFormField(field) {
1183
+ const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1184
+ const prop = field.attrName;
1185
+
1186
+ if (field.formType === 'select') {
1187
+ return [
1188
+ ' <el-col :span="12" class="mb20">',
1189
+ ` <el-form-item label="${label}" prop="${prop}">`,
1190
+ ` <el-select v-model="form.${prop}" placeholder="闁荤姴娲ㄩ崗姗€鍩€椤掆偓椤︽壆鈧?{label}" style="width: 100%">`,
1191
+ ` <el-option v-for="item in ${field.dictType}" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1192
+ ' </el-select>',
1193
+ ' </el-form-item>',
1194
+ ' </el-col>',
1195
+ ].join('\n');
1196
+ }
1197
+
1198
+ if (field.formType === 'number') {
1199
+ const max = field.comment.includes('%') || field.comment.includes('濠殿噯绲鹃弻褏娆?) ? ' :max="100"' : '';
1200
+ const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1201
+ return [
1202
+ ' <el-col :span="12" class="mb20">',
1203
+ ` <el-form-item label="${label}" prop="${prop}">`,
1204
+ ` <el-input-number v-model="form.${prop}" :min="0"${max}${precision} placeholder="闁荤姴娲ㄩ弻澶屾椤撱垹绀?{label}" style="width: 100%" />`,
1205
+ ' </el-form-item>',
1206
+ ' </el-col>',
1207
+ ].join('\n');
1208
+ }
1209
+
1210
+ if (field.formType === 'datetime' || field.formType === 'date') {
1211
+ const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
1212
+ const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
1213
+ return [
1214
+ ' <el-col :span="12" class="mb20">',
1215
+ ` <el-form-item label="${label}" prop="${prop}">`,
1216
+ ` <el-date-picker type="${pickerType}" placeholder="闁荤姴娲ㄩ崗姗€鍩€椤掆偓椤︽壆鈧?{label}" v-model="form.${prop}" :value-format="${formatName}" style="width: 100%"></el-date-picker>`,
1217
+ ' </el-form-item>',
1218
+ ' </el-col>',
1219
+ ].join('\n');
1220
+ }
1221
+
1222
+ if (field.formType === 'textarea') {
1223
+ return [
1224
+ ' <el-col :span="24" class="mb20">',
1225
+ ` <el-form-item label="${label}" prop="${prop}">`,
1226
+ ` <el-input type="textarea" v-model="form.${prop}" placeholder="闁荤姴娲ㄩ弻澶屾椤撱垹绀?{label}" />`,
1227
+ ' </el-form-item>',
1228
+ ' </el-col>',
1229
+ ].join('\n');
1230
+ }
1231
+
1232
+ return [
1233
+ ' <el-col :span="12" class="mb20">',
1234
+ ` <el-form-item label="${label}" prop="${prop}">`,
1235
+ ` <el-input v-model="form.${prop}" placeholder="闁荤姴娲ㄩ弻澶屾椤撱垹绀?{label}" />`,
1236
+ ' </el-form-item>',
1237
+ ' </el-col>',
1238
+ ].join('\n');
1239
+ }
1240
+
1241
+ function renderTableColumn(field) {
1242
+ const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1243
+ if (field.dictType) {
1244
+ return [
1245
+ ` <el-table-column prop="${field.attrName}" label="${label}" show-overflow-tooltip>`,
1246
+ ' <template #default="scope">',
1247
+ ` <dict-tag :options="${field.dictType}" :value="scope.row.${field.attrName}" />`,
1248
+ ' </template>',
1249
+ ' </el-table-column>',
1250
+ ].join('\n');
1251
+ }
1252
+ return ` <el-table-column prop="${field.attrName}" label="${label}" show-overflow-tooltip />`;
1253
+ }
1254
+
1255
+ function renderChildTableColumn(field, childListName) {
1256
+ const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1257
+ const rules = field.notNull ? ` :rules="[{ required: true, trigger: 'blur' }]"` : '';
1258
+
1259
+ let control = ` <el-input v-model="row.${field.attrName}" />`;
1260
+ if (field.formType === 'select' && field.dictType) {
1261
+ control = [
1262
+ ` <el-select v-model="row.${field.attrName}" placeholder="闁荤姴娲ㄩ崗姗€鍩€椤掆偓椤︽壆鈧?{label}" style="width: 100%">`,
1263
+ ` <el-option v-for="item in ${field.dictType}" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1264
+ ' </el-select>',
1265
+ ].join('\n');
1266
+ } else if (field.formType === 'number') {
1267
+ const max = field.comment.includes('%') || field.comment.includes('濠殿噯绲鹃弻褏娆?) ? ' :max="100"' : '';
1268
+ const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1269
+ control = ` <el-input-number v-model="row.${field.attrName}" :min="0"${max}${precision} style="width: 100%" />`;
1270
+ }
1271
+
1272
+ return [
1273
+ ` <el-table-column label="${label}" prop="${field.attrName}">`,
1274
+ ' <template #default="{ row, $index }">',
1275
+ ` <el-form-item :prop="\`${childListName}.\${$index}.${field.attrName}\`"${rules}>`,
1276
+ control,
1277
+ ' </el-form-item>',
1278
+ ' </template>',
1279
+ ' </el-table-column>',
1280
+ ].join('\n');
1281
+ }
1282
+
1283
+ function renderOptionField(field) {
1284
+ const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1285
+ return ` ${field.attrName}: { show:true, alwaysHide:false, smart:false, label: '${label}', width: '120' },`;
1286
+ }
1287
+
1288
+ function renderFilterType(field) {
1289
+ if (!field.dictType) return null;
1290
+ return ` ${field.attrName}: 30,`;
1291
+ }
1292
+
1293
+ function renderDefaultLine(field) {
1294
+ if (field.formType === 'number') return ` ${field.attrName}: 0,`;
1295
+ return ` ${field.attrName}: '',`;
1296
+ }
1297
+
1298
+ function renderFormDefaults(model) {
1299
+ const lines = [` ${model.pk.attrName}: '',`];
1300
+ for (const field of model.visibleFields) lines.push(renderDefaultLine(field));
1301
+ return lines.join('\n');
1302
+ }
1303
+
1304
+ function renderChildTempDefaults(childModel) {
1305
+ if (!childModel) return '';
1306
+ return childModel.fields.map(renderDefaultLine).join('\n');
1307
+ }
1308
+
1309
+ function renderChildListDefaultLine(childModel) {
1310
+ return ` ${childModel.listName}: [],`;
1311
+ }
1312
+
1313
+ function renderChildTempDeclaration(childModel) {
1314
+ return [
1315
+ `const childTemp${childModel.className} = reactive({`,
1316
+ renderChildTempDefaults(childModel),
1317
+ '});',
1318
+ ].join('\n');
1319
+ }
1320
+
1321
+ function renderChildSection(childModel, childCount) {
1322
+ const title = childModel.tableComment.replace(/'/g, "\\'");
1323
+ const deleteExpression =
1324
+ childCount > 1
1325
+ ? `deleteChild(obj, '${childModel.pk.attrName}', '${childModel.tableName}')`
1326
+ : `deleteChild(obj, '${childModel.pk.attrName}')`;
1327
+
1328
+ return [
1329
+ ' <el-col :span="24" class="mb20">',
1330
+ ` <div class="mb10" style="font-weight: 600;">${title}</div>`,
1331
+ ` <sc-form-table v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" @delete="(obj) => ${deleteExpression}" :placeholder="t('common.noData')">`,
1332
+ childModel.visibleFields.map((field) => renderChildTableColumn(field, childModel.listName)).join('\n'),
1333
+ ' </sc-form-table>',
1334
+ ' </el-col>',
1335
+ ].join('\n');
1336
+ }
1337
+
1338
+ function renderChildFormListDefaults(children) {
1339
+ if (!children.length) return '';
1340
+ return children.map(renderChildListDefaultLine).join('\n');
1341
+ }
1342
+
1343
+ function renderChildTempDeclarations(children) {
1344
+ if (!children.length) return '';
1345
+ return children.map(renderChildTempDeclaration).join('\n\n');
1346
+ }
1347
+
1348
+ function renderChildResetListLines(children) {
1349
+ if (!children.length) return '';
1350
+ return children.map((childModel) => ` form.${childModel.listName} = [];`).join('\n');
1351
+ }
1352
+
1353
+ function renderDictImportBlock(dictTypes) {
1354
+ if (!dictTypes.length) return '';
1355
+ return [
1356
+ "import { useDict } from '/@/hooks/dict';",
1357
+ `const { ${dictTypes.join(', ')} } = useDict(${dictTypes.map((item) => `'${item}'`).join(', ')});`,
1358
+ ].join('\n');
1359
+ }
1360
+
1361
+ function getDefaultOptionFieldWidthV2(field) {
1362
+ if (field.formType === 'textarea') return '180';
1363
+ return '120';
1364
+ }
1365
+
1366
+ function renderOptionFieldV2(field, labelKey, dictRegistryRefs, indent = ' ') {
1367
+ const parts = [`key: '${field.attrName}'`, `labelKey: '${labelKey}'`];
1368
+ const width = getDefaultOptionFieldWidthV2(field);
1369
+
1370
+ if (width !== '120') {
1371
+ parts.push(`width: '${width}'`);
1372
+ }
1373
+
1374
+ if (field.dictType) {
1375
+ parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1376
+ }
1377
+
1378
+ return `${indent}{ ${parts.join(', ')} },`;
1379
+ }
1380
+
1381
+ function renderChildOptionGroupV2(model, childModel, dictRegistryRefs) {
1382
+ return [
1383
+ ` ${childModel.listName}: [`,
1384
+ childModel.visibleFields
1385
+ .map((field) => renderOptionFieldV2(field, buildChildFieldLabelKey(model, childModel, field), dictRegistryRefs, ' '))
1386
+ .join('\n'),
1387
+ ' ],',
1388
+ ].join('\n');
1389
+ }
1390
+
1391
+ function renderTextMaxlengthAttrV2(field) {
1392
+ if (!field.length) return '';
1393
+ return ` :maxlength="${field.length}"`;
1394
+ }
1395
+
1396
+ function renderTextareaMaxlengthAttrsV2(field) {
1397
+ if (!field.length) return '';
1398
+ return ` :maxlength="${field.length}" show-word-limit`;
1399
+ }
1400
+
1401
+ function renderFormFieldV2(field) {
1402
+ const prop = field.attrName;
1403
+ const labelExpr = `getMasterFieldLabel('${prop}')`;
1404
+ const dictExpr = `getMasterFieldMeta('${prop}')?.dictType`;
1405
+ const visibilityExpr = `isMasterFieldVisible('${prop}')`;
1406
+
1407
+ if (field.formType === 'select') {
1408
+ return [
1409
+ ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1410
+ ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1411
+ ` <el-select v-model="form.${prop}" :placeholder="selectPlaceholder(${labelExpr})" style="width: 100%">`,
1412
+ ` <el-option v-for="item in getDictOptions(${dictExpr})" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1413
+ ' </el-select>',
1414
+ ' </el-form-item>',
1415
+ ' </el-col>',
1416
+ ].join('\n');
1417
+ }
1418
+
1419
+ if (field.formType === 'number') {
1420
+ const max = field.comment.includes('%') || field.comment.includes('婵犳鍣徊楣冨蓟瑜忓▎?) ? ' :max="100"' : '';
1421
+ const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1422
+ return [
1423
+ ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1424
+ ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1425
+ ` <el-input-number v-model="form.${prop}" :min="0"${max}${precision} :placeholder="inputPlaceholder(${labelExpr})" style="width: 100%" />`,
1426
+ ' </el-form-item>',
1427
+ ' </el-col>',
1428
+ ].join('\n');
1429
+ }
1430
+
1431
+ if (field.formType === 'datetime' || field.formType === 'date') {
1432
+ const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
1433
+ const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
1434
+ return [
1435
+ ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1436
+ ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1437
+ ` <el-date-picker type="${pickerType}" :placeholder="selectPlaceholder(${labelExpr})" v-model="form.${prop}" :value-format="${formatName}" style="width: 100%"></el-date-picker>`,
1438
+ ' </el-form-item>',
1439
+ ' </el-col>',
1440
+ ].join('\n');
1441
+ }
1442
+
1443
+ if (field.formType === 'textarea') {
1444
+ return [
1445
+ ` <el-col v-if="${visibilityExpr}" :span="24" class="mb20">`,
1446
+ ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1447
+ ` <el-input type="textarea" v-model="form.${prop}" :placeholder="inputPlaceholder(${labelExpr})"${renderTextareaMaxlengthAttrsV2(field)} />`,
1448
+ ' </el-form-item>',
1449
+ ' </el-col>',
1450
+ ].join('\n');
1451
+ }
1452
+
1453
+ return [
1454
+ ` <el-col v-if="${visibilityExpr}" :span="12" class="mb20">`,
1455
+ ` <el-form-item :label="${labelExpr}" prop="${prop}">`,
1456
+ ` <el-input v-model="form.${prop}" :placeholder="inputPlaceholder(${labelExpr})"${renderTextMaxlengthAttrV2(field)} />`,
1457
+ ' </el-form-item>',
1458
+ ' </el-col>',
1459
+ ].join('\n');
1460
+ }
1461
+
1462
+ function renderTableColumnV2(field, dictRegistryRefs) {
1463
+ const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1464
+ const parts = [`prop: '${field.attrName}'`, `label: '${label}'`];
1465
+ const width = getDefaultOptionFieldWidthV2(field);
1466
+
1467
+ if (width !== '120') {
1468
+ parts.push(`width: '${width}'`);
1469
+ }
1470
+
1471
+ if (field.dictType) {
1472
+ parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1473
+ }
1474
+
1475
+ return ` { ${parts.join(', ')} },`;
1476
+ }
1477
+
1478
+ function renderChildTableColumnV2(field, childListName) {
1479
+ const rules = field.notNull ? ` :rules="[{ required: true, trigger: 'blur' }]"` : '';
1480
+ const labelExpr = `getChildFieldLabel('${childListName}', '${field.attrName}')`;
1481
+ const dictExpr = `getChildFieldMeta('${childListName}', '${field.attrName}')?.dictType`;
1482
+
1483
+ let control = ` <el-input v-model="row.${field.attrName}"${renderTextMaxlengthAttrV2(field)} />`;
1484
+ if (field.formType === 'select' && field.dictType) {
1485
+ control = [
1486
+ ` <el-select v-model="row.${field.attrName}" :placeholder="selectPlaceholder(${labelExpr})" style="width: 100%">`,
1487
+ ` <el-option v-for="item in getDictOptions(${dictExpr})" :key="item.value" :label="item.label" :value="Number(item.value)" />`,
1488
+ ' </el-select>',
1489
+ ].join('\n');
1490
+ } else if (field.formType === 'number') {
1491
+ const max = field.comment.includes('%') || field.comment.includes('婵犳鍣徊楣冨蓟瑜忓▎?) ? ' :max="100"' : '';
1492
+ const precision = field.sqlType === 'DECIMAL' && field.scale ? ` :precision="${field.scale}" :step="0.01"` : '';
1493
+ control = ` <el-input-number v-model="row.${field.attrName}" :min="0"${max}${precision} style="width: 100%" />`;
1494
+ } else if (field.formType === 'datetime' || field.formType === 'date') {
1495
+ const pickerType = field.formType === 'datetime' ? 'datetime' : 'date';
1496
+ const formatName = field.formType === 'datetime' ? 'dateTimeStr' : 'dateStr';
1497
+ control = ` <el-date-picker type="${pickerType}" v-model="row.${field.attrName}" :value-format="${formatName}" :placeholder="selectPlaceholder(${labelExpr})" style="width: 100%"></el-date-picker>`;
1498
+ } else if (field.formType === 'textarea') {
1499
+ control = ` <el-input type="textarea" v-model="row.${field.attrName}"${renderTextareaMaxlengthAttrsV2(field)} />`;
1500
+ }
1501
+
1502
+ return [
1503
+ ` <el-table-column :label="${labelExpr}" prop="${field.attrName}">`,
1504
+ ' <template #default="{ row, $index }">',
1505
+ ` <el-form-item :prop="\`${childListName}.\${$index}.${field.attrName}\`"${rules}>`,
1506
+ control,
1507
+ ' </el-form-item>',
1508
+ ' </template>',
1509
+ ' </el-table-column>',
1510
+ ].join('\n');
1511
+ }
1512
+
1513
+ function renderChildSectionV2(childModel, childCount) {
1514
+ const deleteExpression =
1515
+ childCount > 1
1516
+ ? `deleteChild(obj, '${childModel.pk.attrName}', '${childModel.tableName}')`
1517
+ : `deleteChild(obj, '${childModel.pk.attrName}')`;
1518
+
1519
+ return [
1520
+ ' <el-col :span="24" class="mb20">',
1521
+ ` <div class="mb10" style="font-weight: 600;">{{ childSectionTitle('${childModel.listName}') }}</div>`,
1522
+ ` <sc-form-table v-model="form.${childModel.listName}" :addTemplate="childTemp${childModel.className}" @delete="(obj) => ${deleteExpression}" :placeholder="t('common.noData')">`,
1523
+ childModel.visibleFields.map((field) => renderChildTableColumnV2(field, childModel.listName)).join('\n'),
1524
+ ' </sc-form-table>',
1525
+ ' </el-col>',
1526
+ ].join('\n');
1527
+ }
1528
+
1529
+ function renderValidationRule(field) {
1530
+ if (!field.notNull) return null;
1531
+ const label = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1532
+ return ` ${field.attrName}: [{ required: true, message: '${label}婵炴垶鎸哥粔鐑藉礂濡崵鈻旈柧蹇撳帨閺?, trigger: 'blur' }],`;
1533
+ }
1534
+
1535
+ function renderFormRules(visibleFields) {
1536
+ const lines = visibleFields.map(renderValidationRule).filter(Boolean);
1537
+ return lines.join('\n');
1538
+ }
1539
+
1540
+ function renderFormRulesV2(visibleFields) {
1541
+ const lines = visibleFields
1542
+ .filter((field) => field.notNull)
1543
+ .map((field) => ` ${field.attrName}: [{ required: true, message: fieldRequiredMessage('${field.attrName}'), trigger: 'blur' }],`);
1544
+ return lines.join('\n');
1545
+ }
1546
+
1547
+ function buildReplacements(model, sharedSupport) {
1548
+ const menuBaseId = Date.now();
1549
+ const apiModulePath = `${model.moduleName}/${model.functionName}`;
1550
+ const routePath = `${model.moduleName}/${model.functionName}`;
1551
+ const permissionPrefix = `${model.moduleName}/${model.functionName}`.replace(/\//g, '_');
1552
+ const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
1553
+ const i18nNamespace = buildI18nNamespace(model);
1554
+
1555
+ return {
1556
+ TABLE_NAME: model.tableName,
1557
+ TABLE_COMMENT: model.tableComment,
1558
+ CLASS_NAME: model.className,
1559
+ FUNCTION_NAME: model.functionName,
1560
+ PK_ATTR: model.pk.attrName,
1561
+ API_MODULE_PATH: apiModulePath,
1562
+ API_PATH: apiModulePath,
1563
+ VIEW_MODULE_PATH: routePath,
1564
+ MENU_ROUTE_PATH: routePath,
1565
+ I18N_NAMESPACE: i18nNamespace,
1566
+ PERMISSION_PREFIX: permissionPrefix,
1567
+ MENU_BASE_ID: menuBaseId,
1568
+ MENU_BASE_ID_PLUS_1: menuBaseId + 1,
1569
+ MENU_BASE_ID_PLUS_2: menuBaseId + 2,
1570
+ MENU_BASE_ID_PLUS_3: menuBaseId + 3,
1571
+ MENU_BASE_ID_PLUS_4: menuBaseId + 4,
1572
+ MENU_BASE_ID_PLUS_5: menuBaseId + 5,
1573
+ GENERATED_AT: new Date().toISOString(),
1574
+ FORM_FIELDS: model.visibleFields.map(renderFormFieldV2).join('\n'),
1575
+ TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumnV2(field, dictRegistryRefs)).join('\n'),
1576
+ FORM_DEFAULTS: renderFormDefaults(model),
1577
+ DICT_REGISTRY_IMPORT_BLOCK: model.dictTypes.length ? "import { DictRegistry } from '/@/enums/dict-registry';" : '',
1578
+ MASTER_OPTION_FIELDS: model.visibleFields.map((field) => renderOptionFieldV2(field, buildFieldLabelKey(model, field), dictRegistryRefs)).join('\n'),
1579
+ CHILD_OPTION_GROUPS: model.children.map((childModel) => renderChildOptionGroupV2(model, childModel, dictRegistryRefs)).join('\n'),
1580
+ FORM_RULES: renderFormRulesV2(model.visibleFields),
1581
+ CHILD_FORM_LIST_DEFAULTS: renderChildFormListDefaults(model.children),
1582
+ CHILD_TEMP_DECLARATIONS: renderChildTempDeclarations(model.children),
1583
+ CHILD_RESET_LISTS: renderChildResetListLines(model.children),
1584
+ CHILD_SECTIONS: model.children.map((childModel) => renderChildSectionV2(childModel, model.children.length)).join('\n'),
1585
+ };
1586
+ }
1587
+
1588
+ function renderFiles(model, stylePreset, sharedSupport, localeZhSupport) {
1589
+ if (!hasRuntimeSupport(stylePreset)) throw new Error('Runtime templates are not implemented for style: ' + model.style);
1590
+
1591
+ const runtime = stylePreset.runtime;
1592
+ const templateDir = path.resolve(__dirname, runtime.templateDir);
1593
+ const replacements = buildReplacements(model, sharedSupport);
1594
+ const formTemplate = fs.readFileSync(path.join(templateDir, runtime.files.form), 'utf8');
1595
+ const listTemplate = fs.readFileSync(path.join(templateDir, runtime.files.list), 'utf8');
1596
+ const optionsTemplate = fs.readFileSync(path.join(templateDir, runtime.files.options), 'utf8');
1597
+ const apiTemplate = fs.readFileSync(path.join(templateDir, runtime.files.api), 'utf8');
1598
+ const menuSqlTemplate = runtime.files.menuSql ? fs.readFileSync(path.join(templateDir, runtime.files.menuSql), 'utf8') : null;
1599
+
1600
+ const viewRoot = path.join(model.frontendPath, 'src', 'views', ...model.moduleName.split('/'), model.functionName);
1601
+ const apiRoot = path.join(model.frontendPath, 'src', 'api', ...model.moduleName.split('/'));
1602
+ const menuRoot = path.join(model.frontendPath, 'menu');
1603
+
1604
+ const files = [
1605
+ { type: 'form', path: path.join(viewRoot, 'form.vue'), content: renderTemplate(formTemplate, replacements) },
1606
+ { type: 'list', path: path.join(viewRoot, 'index.vue'), content: renderTemplate(listTemplate, replacements) },
1607
+ { type: 'options', path: path.join(viewRoot, 'options.ts'), content: renderTemplate(optionsTemplate, replacements) },
1608
+ { type: 'api', path: path.join(apiRoot, `${model.functionName}.ts`), content: renderTemplate(apiTemplate, replacements) },
1609
+ {
1610
+ type: 'i18nZh',
1611
+ path: localeZhSupport.path,
1612
+ content: localeZhSupport.content,
1613
+ canWrite: localeZhSupport.isCompatible,
1614
+ needsWrite: localeZhSupport.needsWrite,
1615
+ },
1616
+ ];
1617
+
1618
+ if (menuSqlTemplate) {
1619
+ files.push({ type: 'menuSql', path: path.join(menuRoot, `${model.functionName}_menu.sql`), content: renderTemplate(menuSqlTemplate, replacements) });
1620
+ }
1621
+ return files;
1622
+ }
1623
+
1624
+ function ensureArguments(input) {
1625
+ if (!input || typeof input !== 'object') throw new Error('Arguments must be an object');
1626
+ for (const key of TOOL_SCHEMA.required) {
1627
+ if (!input[key]) throw new Error(key + ' is required');
1628
+ }
1629
+
1630
+ const style = String(input.style);
1631
+ getStylePreset(style);
1632
+
1633
+ return {
1634
+ designFile: input.designFile ? String(input.designFile) : null,
1635
+ tableName: String(input.tableName),
1636
+ style,
1637
+ children: normalizeChildrenInput(input.children),
1638
+ childTableName: input.childTableName ? String(input.childTableName) : null,
1639
+ mainField: input.mainField ? String(input.mainField) : null,
1640
+ childField: input.childField ? String(input.childField) : null,
1641
+ relationType: input.relationType ? String(input.relationType) : '',
1642
+ frontendPath: String(input.frontendPath),
1643
+ moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
1644
+ writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
1645
+ overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
1646
+ };
1647
+ }
1648
+
1649
+ function maybeWriteFiles(files, writeToDisk, overwrite) {
1650
+ if (!writeToDisk) return;
1651
+ for (const file of files) {
1652
+ if (file.canWrite === false) {
1653
+ file.status = 'skipped';
1654
+ continue;
1655
+ }
1656
+ if (file.needsWrite === false) {
1657
+ file.status = 'unchanged';
1658
+ continue;
1659
+ }
1660
+ if (!overwrite && fs.existsSync(file.path)) {
1661
+ file.status = 'skipped';
1662
+ continue;
1663
+ }
1664
+ fs.mkdirSync(path.dirname(file.path), { recursive: true });
1665
+ fs.writeFileSync(file.path, file.content, 'utf8');
1666
+ file.status = 'success';
1667
+ }
1668
+ }
1669
+
1670
+ function buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, note) {
1671
+ const relations = model.children.map((childModel) => ({
1672
+ childTableName: childModel.tableName,
1673
+ childTableComment: childModel.tableComment,
1674
+ mainField: childModel.mainField.fieldName,
1675
+ childField: childModel.childField.fieldName,
1676
+ childListName: childModel.listName,
1677
+ relationType: childModel.relationType || '',
1678
+ }));
1679
+ const selectedList = relations.map((relation) => formatRelationCandidate(relation));
1680
+
1681
+ return {
1682
+ mode: 'local-template',
1683
+ style: safeArgs.style,
1684
+ styleLabel: stylePreset.label,
1685
+ runtimeSupported: hasRuntimeSupport(stylePreset),
1686
+ sourceFile: model.sourceFile,
1687
+ designFile: model.designFile,
1688
+ tableName: model.tableName,
1689
+ tableComment: model.tableComment,
1690
+ moduleName: model.moduleName,
1691
+ writeToDisk: safeArgs.writeToDisk,
1692
+ selectedTemplates: sharedTemplates,
1693
+ files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
1694
+ relation: relations.length === 1 ? relations[0] : null,
1695
+ relations,
1696
+ relationResolution: model.children.length
1697
+ ? {
1698
+ status: 'provided',
1699
+ tableName: safeArgs.tableName,
1700
+ designFile: model.designFile,
1701
+ source: 'arguments',
1702
+ message:
1703
+ model.children.length > 1
1704
+ ? 'Direct child relations were provided by the caller. MCP skipped design-doc relation inference and generated a single main form with multiple child tables.'
1705
+ : 'The relation was provided by the caller. MCP skipped design-doc relation inference and generated files directly.',
1706
+ selected: selectedList.length === 1 ? selectedList[0] : null,
1707
+ selectedList,
1708
+ correctionEntry: {
1709
+ fields: ['children', 'childTableName', 'mainField', 'childField'],
1710
+ example: buildRetryArguments(safeArgs, model.designFile, model.children[0].tableName),
1711
+ },
1712
+ }
1713
+ : null,
1714
+ summary: {
1715
+ totalFields: model.fields.length,
1716
+ visibleFields: model.visibleFields.length,
1717
+ dictFields: model.visibleFields.filter((field) => field.dictType).map((field) => field.attrName),
1718
+ skippedAuditFields: model.fields.filter((field) => field.isAudit).map((field) => field.fieldName),
1719
+ childCount: model.children.length,
1720
+ childTables: model.children.map((childModel) => childModel.tableName),
1721
+ childVisibleFields: model.children.reduce((sum, childModel) => sum + childModel.visibleFields.length, 0),
1722
+ },
1723
+ note,
1724
+ };
1725
+ }
1726
+
1727
+ async function handleToolCall(argumentsObject) {
1728
+ const safeArgs = ensureArguments(argumentsObject);
1729
+ const stylePreset = getStylePreset(safeArgs.style);
1730
+ const model = buildModel(safeArgs);
1731
+ const sharedTemplates = resolveSharedTemplates(stylePreset);
1732
+ const sharedSupport = prepareSharedSupport(model.frontendPath, model.dictTypes);
1733
+ const localeZhSupport = prepareZhCnLocaleFile(model);
1734
+
1735
+ if (!hasRuntimeSupport(stylePreset)) {
1736
+ const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, [], 'Style mapping is declared, but runtime template rendering is not implemented yet for this style.');
1737
+ return toolTextResult(JSON.stringify(manifest, null, 2));
1738
+ }
1739
+
1740
+ const files = renderFiles(model, stylePreset, sharedSupport, localeZhSupport);
1741
+ maybeWriteFiles(files, safeArgs.writeToDisk, safeArgs.overwrite);
1742
+ maybeWriteSharedSupport(sharedSupport, safeArgs.writeToDisk);
1743
+ const manifest = buildManifest(model, safeArgs, stylePreset, sharedTemplates, files, buildSupportNote(sharedSupport, localeZhSupport));
1744
+ return toolTextResult(JSON.stringify(manifest, null, 2));
1745
+ }
1746
+
1747
+ async function onMessage(message) {
1748
+ const { id, method, params } = message;
1749
+ const isNotification = id === undefined || id === null;
1750
+
1751
+ if (method === 'initialize') {
1752
+ writeMessage(successResponse(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: { name: SERVER_NAME, version: SERVER_VERSION } }));
1753
+ return;
1754
+ }
1755
+
1756
+ if (method === 'notifications/initialized') return;
1757
+
1758
+ // Silently ignore all other notifications (no id means no response expected).
1759
+ if (isNotification) return;
1760
+
1761
+ if (method === 'ping') {
1762
+ writeMessage(successResponse(id, {}));
1763
+ return;
1764
+ }
1765
+
1766
+ if (method === 'tools/list') {
1767
+ writeMessage(successResponse(id, { tools: [{ name: TOOL_NAME, description: 'Generate Worsoft frontend files and menu SQL from a local Markdown design file with style-based local templates. In master_child_jump mode, the caller must provide either children[] or childTableName/mainField/childField.', inputSchema: TOOL_SCHEMA }] }));
1768
+ return;
1769
+ }
1770
+
1771
+ if (method === 'tools/call') {
1772
+ if (!params || params.name !== TOOL_NAME) {
1773
+ writeMessage(errorResponse(id, -32602, 'Unknown tool'));
1774
+ return;
1775
+ }
1776
+ try {
1777
+ const result = await handleToolCall(params.arguments || {});
1778
+ writeMessage(successResponse(id, result));
1779
+ } catch (error) {
1780
+ const errorText = error && error.details ? JSON.stringify(error.details, null, 2) : String(error.message || error);
1781
+ writeMessage(successResponse(id, { content: [{ type: 'text', text: errorText }], isError: true }));
1782
+ }
1783
+ return;
1784
+ }
1785
+
1786
+ writeMessage(errorResponse(id, -32601, 'Method not found'));
1787
+ }
1788
+
1789
+ function start() {
1790
+ process.stdin.setEncoding('utf8');
1791
+ let buffer = '';
1792
+ process.stdin.on('data', (chunk) => {
1793
+ buffer += chunk;
1794
+ let newlineIndex = buffer.indexOf('\n');
1795
+ while (newlineIndex >= 0) {
1796
+ const raw = buffer.slice(0, newlineIndex).trim();
1797
+ buffer = buffer.slice(newlineIndex + 1);
1798
+ if (raw) {
1799
+ try {
1800
+ const message = JSON.parse(raw);
1801
+ Promise.resolve(onMessage(message)).catch((error) => {
1802
+ if (message && Object.prototype.hasOwnProperty.call(message, 'id')) {
1803
+ writeMessage(errorResponse(message.id, -32603, error.message));
1804
+ }
1805
+ });
1806
+ } catch (error) {
1807
+ writeMessage(errorResponse(null, -32700, 'Parse error: ' + error.message));
1808
+ }
1809
+ }
1810
+ newlineIndex = buffer.indexOf('\n');
1811
+ }
1812
+ });
1813
+ }
1814
+
1815
+ start();
1818
1816