worsoft-frontend-codegen-local-mcp 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mcp_server.js CHANGED
@@ -5,57 +5,57 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
7
  const SERVER_NAME = 'worsoft-codegen-local';
8
- const SERVER_VERSION = '0.1.39';
8
+ const SERVER_VERSION = '0.1.41';
9
9
  const PROTOCOL_VERSION = '2024-11-05';
10
10
  const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
- const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
11
+ const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
12
12
  const STYLE_CATALOG = loadStyleCatalog();
13
13
  const DEFAULT_DICT_REGISTRY_KEYS = {
14
14
  add_start_stop: 'COMMON_STATUS',
15
15
  trade_standard_type: 'TRADE_STANDARD_TYPE',
16
16
  trade_score_standard: 'TRADE_SCORE_STANDARD',
17
17
  };
18
- const DEFAULT_CRUD_SCHEMA_TEMPLATE = `export interface FieldMeta {
19
- show: boolean;
20
- listShow: boolean;
21
- alwaysHide: boolean;
22
- smart: boolean;
23
- queryType?: number;
24
- labelKey: string;
25
- label?: string;
26
- width: string;
27
- dictType?: string;
28
- }
18
+ const DEFAULT_CRUD_SCHEMA_TEMPLATE = `export interface FieldMeta {
19
+ show: boolean;
20
+ listShow: boolean;
21
+ alwaysHide: boolean;
22
+ smart: boolean;
23
+ queryType?: number;
24
+ labelKey: string;
25
+ label?: string;
26
+ width: string;
27
+ dictType?: string;
28
+ }
29
29
 
30
30
  export interface FieldConfig {
31
31
  key: string;
32
32
  labelKey?: string;
33
33
  label?: string;
34
34
  width?: string;
35
- show?: boolean;
36
- listShow?: boolean;
37
- alwaysHide?: boolean;
38
- smart?: boolean;
39
- queryType?: number;
40
- dictType?: string;
41
- }
35
+ show?: boolean;
36
+ listShow?: boolean;
37
+ alwaysHide?: boolean;
38
+ smart?: boolean;
39
+ queryType?: number;
40
+ dictType?: string;
41
+ }
42
42
 
43
43
  export interface CrudSchemaDefinition {
44
44
  master: FieldConfig[];
45
45
  children?: Record<string, FieldConfig[]>;
46
46
  }
47
47
 
48
- export interface CrudSchema {
49
- master: Record<string, FieldMeta>;
50
- children: Record<string, Record<string, FieldMeta>>;
51
- smartNames: string[];
52
- filterTypes: Record<string, number>;
53
- childFilterTypes: Record<string, Record<string, number>>;
54
- allDictTypes: string[];
55
- }
56
-
57
- const DEFAULT_WIDTH = '120';
58
- const DEFAULT_DICT_QUERY_TYPE = 30;
48
+ export interface CrudSchema {
49
+ master: Record<string, FieldMeta>;
50
+ children: Record<string, Record<string, FieldMeta>>;
51
+ smartNames: string[];
52
+ filterTypes: Record<string, number>;
53
+ childFilterTypes: Record<string, Record<string, number>>;
54
+ allDictTypes: string[];
55
+ }
56
+
57
+ const DEFAULT_WIDTH = '120';
58
+ const DEFAULT_DICT_QUERY_TYPE = 30;
59
59
 
60
60
  export const field = (labelKey: string, width = DEFAULT_WIDTH): FieldMeta => ({
61
61
  show: true,
@@ -71,17 +71,17 @@ export const dictField = (labelKey: string, dictType: string, width = DEFAULT_WI
71
71
  dictType,
72
72
  });
73
73
 
74
- export const buildFilterTypes = (fields: Record<string, FieldMeta>) =>
75
- Object.fromEntries(
76
- Object.entries(fields)
77
- .filter(([, item]) => typeof item.queryType === 'number')
78
- .map(([key, item]) => [key, item.queryType as number])
79
- );
80
-
81
- export const buildSmartNames = (fields: Record<string, FieldMeta>) =>
82
- Object.entries(fields)
83
- .filter(([, item]) => item.smart)
84
- .map(([key]) => key);
74
+ export const buildFilterTypes = (fields: Record<string, FieldMeta>) =>
75
+ Object.fromEntries(
76
+ Object.entries(fields)
77
+ .filter(([, item]) => typeof item.queryType === 'number')
78
+ .map(([key, item]) => [key, item.queryType as number])
79
+ );
80
+
81
+ export const buildSmartNames = (fields: Record<string, FieldMeta>) =>
82
+ Object.entries(fields)
83
+ .filter(([, item]) => item.smart)
84
+ .map(([key]) => key);
85
85
 
86
86
  export const collectDictTypes = (...groups: Array<Record<string, FieldMeta>>) =>
87
87
  Array.from(
@@ -94,19 +94,19 @@ export const collectDictTypes = (...groups: Array<Record<string, FieldMeta>>) =>
94
94
  )
95
95
  );
96
96
 
97
- const normalizeField = (item: FieldConfig): FieldMeta => ({
98
- show: item.show ?? true,
99
- listShow: item.listShow ?? item.show ?? true,
100
- alwaysHide: item.alwaysHide ?? false,
101
- smart: item.smart ?? false,
102
- ...(typeof item.queryType === 'number'
103
- ? { queryType: item.queryType }
104
- : item.dictType && (item.listShow ?? item.show ?? true)
105
- ? { queryType: DEFAULT_DICT_QUERY_TYPE }
106
- : {}),
107
- labelKey: item.labelKey ?? item.label ?? item.key,
108
- ...(item.label ? { label: item.label } : {}),
109
- width: item.width ?? DEFAULT_WIDTH,
97
+ const normalizeField = (item: FieldConfig): FieldMeta => ({
98
+ show: item.show ?? true,
99
+ listShow: item.listShow ?? item.show ?? true,
100
+ alwaysHide: item.alwaysHide ?? false,
101
+ smart: item.smart ?? false,
102
+ ...(typeof item.queryType === 'number'
103
+ ? { queryType: item.queryType }
104
+ : item.dictType && (item.listShow ?? item.show ?? true)
105
+ ? { queryType: DEFAULT_DICT_QUERY_TYPE }
106
+ : {}),
107
+ labelKey: item.labelKey ?? item.label ?? item.key,
108
+ ...(item.label ? { label: item.label } : {}),
109
+ width: item.width ?? DEFAULT_WIDTH,
110
110
  ...(item.dictType ? { dictType: item.dictType } : {}),
111
111
  });
112
112
 
@@ -116,13 +116,13 @@ const toFieldMap = (fields: FieldConfig[]) =>
116
116
  const buildSchema = (
117
117
  master: Record<string, FieldMeta>,
118
118
  children: Record<string, Record<string, FieldMeta>> = {}
119
- ): CrudSchema => ({
120
- master,
121
- children,
122
- smartNames: buildSmartNames(master),
123
- filterTypes: buildFilterTypes(master),
124
- childFilterTypes: Object.fromEntries(
125
- Object.entries(children).map(([key, fields]) => [key, buildFilterTypes(fields)])
119
+ ): CrudSchema => ({
120
+ master,
121
+ children,
122
+ smartNames: buildSmartNames(master),
123
+ filterTypes: buildFilterTypes(master),
124
+ childFilterTypes: Object.fromEntries(
125
+ Object.entries(children).map(([key, fields]) => [key, buildFilterTypes(fields)])
126
126
  ),
127
127
  allDictTypes: collectDictTypes(master, ...Object.values(children)),
128
128
  });
@@ -148,19 +148,19 @@ export function createCrudSchema(
148
148
  const TOOL_SCHEMA = {
149
149
  type: 'object',
150
150
  properties: {
151
- featureTitle: { type: 'string', description: 'Feature title from pre-parsed structured metadata.' },
152
- tableName: { type: 'string', description: 'Canonical main table name from PRD-aligned structured metadata.' },
153
- tableComment: { type: 'string', description: 'Canonical main table comment or feature label from PRD-aligned structured metadata.' },
154
- apiPath: { type: 'string', description: 'Backend API base path from pre-parsed structured metadata, for example iwmEmpOutsourcePerson.' },
155
- pageType: {
156
- type: 'string',
157
- enum: ['business', 'dict', 'non_standard'],
158
- description: 'Structured page type from parseResult. MCP consumes this value but does not derive it. Dict pages are restricted to dialog-based templates.',
159
- },
160
- style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Final style id from parseResult or translated mcpPayload. MCP validates it but does not infer it.' },
161
- fields: {
162
- type: 'array',
163
- description: 'Structured main-table field metadata already translated by the caller. MCP only consumes these low-level generation parameters.',
151
+ featureTitle: { type: 'string', description: 'Feature title from pre-parsed structured metadata.' },
152
+ tableName: { type: 'string', description: 'Canonical main table name from PRD-aligned structured metadata.' },
153
+ tableComment: { type: 'string', description: 'Canonical main table comment or feature label from PRD-aligned structured metadata.' },
154
+ apiPath: { type: 'string', description: 'Backend API base path from pre-parsed structured metadata, for example iwmEmpOutsourcePerson.' },
155
+ pageType: {
156
+ type: 'string',
157
+ enum: ['business', 'dict', 'non_standard'],
158
+ description: 'Structured page type from parseResult. MCP consumes this value but does not derive it. Dict pages are restricted to dialog-based templates.',
159
+ },
160
+ style: { type: 'string', enum: Object.keys(STYLE_CATALOG), description: 'Final style id from parseResult or translated mcpPayload. MCP validates it but does not infer it.' },
161
+ fields: {
162
+ type: 'array',
163
+ description: 'Structured main-table field metadata already translated by the caller. MCP only consumes these low-level generation parameters.',
164
164
  items: {
165
165
  type: 'object',
166
166
  properties: {
@@ -170,17 +170,17 @@ const TOOL_SCHEMA = {
170
170
  length: { type: ['string', 'number'], description: 'Field length or precision string, for example 30 or 10,2.' },
171
171
  scale: { type: ['string', 'number'], description: 'Optional decimal scale.' },
172
172
  required: { type: ['boolean', 'string'], description: 'Whether the field is required.' },
173
- readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
174
- show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
175
- listShow: { type: ['boolean', 'string'], description: 'Whether the field is shown on the list page.' },
176
- smart: { type: ['boolean', 'string'], description: 'Whether the field participates in smart keyword search. This must come from PRD, not inferred from type.' },
177
- queryType: { type: ['string', 'number'], description: 'Structured query type for list filtering, for example 20 for date ranges or 30 for dictionary filters.' },
178
- dictType: { type: 'string', description: 'Dictionary type code from structured metadata.' },
173
+ readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
174
+ show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
175
+ listShow: { type: ['boolean', 'string'], description: 'Whether the field is shown on the list page.' },
176
+ smart: { type: ['boolean', 'string'], description: 'Whether the field participates in smart keyword search. This must come from PRD, not inferred from type.' },
177
+ queryType: { type: ['string', 'number'], description: 'Structured query type for list filtering, for example 20 for date ranges or 30 for dictionary filters.' },
178
+ dictType: { type: 'string', description: 'Dictionary type code from structured metadata.' },
179
179
  defaultValue: { type: ['string', 'number', 'boolean'], description: 'Optional default value.' },
180
- description: { type: 'string', description: 'Field description or notes.' },
181
- componentType: { type: 'string', description: 'Explicit component type from PRD structured metadata, for example text, textarea, select, number or datetime.' },
182
- formType: { type: 'string', description: 'Explicit form control type translated by the caller. MCP consumes this value first and only falls back to legacy heuristics when omitted.' },
183
- sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
180
+ description: { type: 'string', description: 'Field description or notes.' },
181
+ componentType: { type: 'string', description: 'Explicit component type from PRD structured metadata, for example text, textarea, select, number or datetime.' },
182
+ formType: { type: 'string', description: 'Explicit form control type translated by the caller. MCP consumes this value first and only falls back to legacy heuristics when omitted.' },
183
+ sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
184
184
  primary: { type: ['boolean', 'string'], description: 'Whether the field is the primary key.' },
185
185
  },
186
186
  required: ['fieldName', 'label', 'type'],
@@ -189,7 +189,7 @@ const TOOL_SCHEMA = {
189
189
  },
190
190
  children: {
191
191
  type: 'array',
192
- description: 'Optional direct child-table structures for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form. The caller must determine semantic truth before passing them in.',
192
+ description: 'Optional direct child-table structures for master_child_jump. When provided, MCP renders all listed direct child tables on the same main form. The caller must determine semantic truth before passing them in.',
193
193
  items: {
194
194
  type: 'object',
195
195
  properties: {
@@ -197,7 +197,7 @@ const TOOL_SCHEMA = {
197
197
  childTableComment: { type: 'string', description: 'Child table comment or display label.' },
198
198
  mainField: { type: 'string', description: 'Main table relation field.' },
199
199
  childField: { type: 'string', description: 'Child table relation field.' },
200
- payloadField: { type: 'string', description: 'Backend payload list field name from structured metadata, for example certificateList.' },
200
+ payloadField: { type: 'string', description: 'Backend payload list field name from structured metadata, for example certificateList.' },
201
201
  relationType: { type: 'string', description: 'Optional relation type label, for example 1:N.' },
202
202
  fields: {
203
203
  type: 'array',
@@ -211,17 +211,17 @@ const TOOL_SCHEMA = {
211
211
  length: { type: ['string', 'number'], description: 'Field length or precision string.' },
212
212
  scale: { type: ['string', 'number'], description: 'Optional decimal scale.' },
213
213
  required: { type: ['boolean', 'string'], description: 'Whether the field is required.' },
214
- readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
215
- show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
216
- listShow: { type: ['boolean', 'string'], description: 'Whether the field is shown on the list page.' },
217
- smart: { type: ['boolean', 'string'], description: 'Whether the field participates in smart keyword search. This must come from PRD, not inferred from type.' },
218
- queryType: { type: ['string', 'number'], description: 'Structured query type for list filtering, for example 20 for date ranges or 30 for dictionary filters.' },
219
- dictType: { type: 'string', description: 'Dictionary type code from structured metadata.' },
214
+ readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
215
+ show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
216
+ listShow: { type: ['boolean', 'string'], description: 'Whether the field is shown on the list page.' },
217
+ smart: { type: ['boolean', 'string'], description: 'Whether the field participates in smart keyword search. This must come from PRD, not inferred from type.' },
218
+ queryType: { type: ['string', 'number'], description: 'Structured query type for list filtering, for example 20 for date ranges or 30 for dictionary filters.' },
219
+ dictType: { type: 'string', description: 'Dictionary type code from structured metadata.' },
220
220
  defaultValue: { type: ['string', 'number', 'boolean'], description: 'Optional default value.' },
221
- description: { type: 'string', description: 'Field description or notes.' },
222
- componentType: { type: 'string', description: 'Explicit component type from PRD structured metadata.' },
223
- formType: { type: 'string', description: 'Explicit form control type translated by the caller.' },
224
- sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
221
+ description: { type: 'string', description: 'Field description or notes.' },
222
+ componentType: { type: 'string', description: 'Explicit component type from PRD structured metadata.' },
223
+ formType: { type: 'string', description: 'Explicit form control type translated by the caller.' },
224
+ sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
225
225
  primary: { type: ['boolean', 'string'], description: 'Whether the field is the primary key.' },
226
226
  },
227
227
  required: ['fieldName', 'label', 'type'],
@@ -247,8 +247,8 @@ const TOOL_SCHEMA = {
247
247
  items: {
248
248
  type: 'object',
249
249
  properties: {
250
- tableName: { type: 'string', description: 'Canonical module table name from PRD-aligned structured metadata.' },
251
- tableComment: { type: 'string', description: 'Canonical module display label from PRD-aligned structured metadata.' },
250
+ tableName: { type: 'string', description: 'Canonical module table name from PRD-aligned structured metadata.' },
251
+ tableComment: { type: 'string', description: 'Canonical module display label from PRD-aligned structured metadata.' },
252
252
  apiPath: { type: 'string', description: 'Backend API base path for this module.' },
253
253
  primaryKey: { type: 'string', description: 'Primary key field name. Defaults to id when omitted.' },
254
254
  queryParentField: { type: 'string', description: 'Direct parent foreign key field used for page queries.' },
@@ -268,17 +268,17 @@ const TOOL_SCHEMA = {
268
268
  length: { type: ['string', 'number'], description: 'Field length or precision string.' },
269
269
  scale: { type: ['string', 'number'], description: 'Optional decimal scale.' },
270
270
  required: { type: ['boolean', 'string'], description: 'Whether the field is required.' },
271
- readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
272
- show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
273
- listShow: { type: ['boolean', 'string'], description: 'Whether the field is shown on the list page.' },
274
- smart: { type: ['boolean', 'string'], description: 'Whether the field participates in smart keyword search. This must come from PRD, not inferred from type.' },
275
- queryType: { type: ['string', 'number'], description: 'Structured query type for list filtering, for example 20 for date ranges or 30 for dictionary filters.' },
276
- dictType: { type: 'string', description: 'Dictionary type code from structured metadata.' },
271
+ readonly: { type: ['boolean', 'string'], description: 'Whether the field is readonly on the page.' },
272
+ show: { type: ['boolean', 'string'], description: 'Whether the field is shown on the page.' },
273
+ listShow: { type: ['boolean', 'string'], description: 'Whether the field is shown on the list page.' },
274
+ smart: { type: ['boolean', 'string'], description: 'Whether the field participates in smart keyword search. This must come from PRD, not inferred from type.' },
275
+ queryType: { type: ['string', 'number'], description: 'Structured query type for list filtering, for example 20 for date ranges or 30 for dictionary filters.' },
276
+ dictType: { type: 'string', description: 'Dictionary type code from structured metadata.' },
277
277
  defaultValue: { type: ['string', 'number', 'boolean'], description: 'Optional default value.' },
278
- description: { type: 'string', description: 'Field description or notes.' },
279
- componentType: { type: 'string', description: 'Explicit component type from PRD structured metadata.' },
280
- formType: { type: 'string', description: 'Explicit form control type translated by the caller.' },
281
- sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
278
+ description: { type: 'string', description: 'Field description or notes.' },
279
+ componentType: { type: 'string', description: 'Explicit component type from PRD structured metadata.' },
280
+ formType: { type: 'string', description: 'Explicit form control type translated by the caller.' },
281
+ sourceKind: { type: 'string', enum: ['entity', 'display', 'virtual', 'common'], description: 'Field source kind.' },
282
282
  primary: { type: ['boolean', 'string'], description: 'Whether the field is the primary key.' }
283
283
  },
284
284
  required: ['fieldName', 'label', 'type'],
@@ -294,34 +294,34 @@ const TOOL_SCHEMA = {
294
294
  required: ['levelIndex', 'modules'],
295
295
  additionalProperties: false
296
296
  }
297
- },
298
- frontendPath: { type: 'string', description: 'Absolute frontend output root path.' },
299
- moduleName: { type: 'string', description: 'Relative frontend module path, for example admin/test.' },
300
- targetViewDir: {
301
- type: 'string',
302
- description: 'Explicit relative target view directory under src/views, for example admin/iwmSysTrade. When provided, MCP writes directly to this directory instead of deriving the feature folder from tableName.',
303
- },
304
- targetApiModule: {
305
- type: 'string',
306
- description: 'Explicit relative api module path under src/api without extension, for example admin/iwmSysTrade. When provided, MCP writes the api file directly to this target path.',
307
- },
308
- targetI18nKey: {
309
- type: 'string',
310
- description: 'Explicit zh-cn i18n namespace, for example admin.iwmSysTrade. When provided, MCP writes zh-cn content to the matching file path and namespace instead of deriving them from moduleName and functionName.',
311
- },
312
- writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
313
- overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
314
- writeSupportFiles: {
315
- type: 'boolean',
316
- default: true,
317
- description: 'Whether to write project support files such as src/enums/dict-registry.ts and src/utils/crudSchema.ts.'
318
- },
319
- mergeI18nZh: {
320
- type: 'boolean',
321
- default: true,
322
- description: 'Whether to merge generated zh-cn content into an existing i18n file. If false, MCP renders zh-cn.ts from current metadata only.'
323
- },
324
- },
297
+ },
298
+ frontendPath: { type: 'string', description: 'Absolute frontend output root path.' },
299
+ moduleName: { type: 'string', description: 'Relative frontend module path, for example admin/test.' },
300
+ targetViewDir: {
301
+ type: 'string',
302
+ description: 'Explicit relative target view directory under src/views, for example admin/iwmSysTrade. When provided, MCP writes directly to this directory instead of deriving the feature folder from tableName.',
303
+ },
304
+ targetApiModule: {
305
+ type: 'string',
306
+ description: 'Explicit relative api module path under src/api without extension, for example admin/iwmSysTrade. When provided, MCP writes the api file directly to this target path.',
307
+ },
308
+ targetI18nKey: {
309
+ type: 'string',
310
+ description: 'Explicit zh-cn i18n namespace, for example admin.iwmSysTrade. When provided, MCP writes zh-cn content to the matching file path and namespace instead of deriving them from moduleName and functionName.',
311
+ },
312
+ writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
313
+ overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
314
+ writeSupportFiles: {
315
+ type: 'boolean',
316
+ default: true,
317
+ description: 'Whether to write project support files such as src/enums/dict-registry.ts and src/utils/crudSchema.ts.'
318
+ },
319
+ mergeI18nZh: {
320
+ type: 'boolean',
321
+ default: true,
322
+ description: 'Whether to merge generated zh-cn content into an existing i18n file. If false, MCP renders zh-cn.ts from current metadata only.'
323
+ },
324
+ },
325
325
  required: ['tableName', 'style', 'frontendPath'],
326
326
  additionalProperties: false,
327
327
  };
@@ -362,9 +362,9 @@ function normalizeModuleName(moduleName) {
362
362
  return moduleName.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
363
363
  }
364
364
 
365
- function normalizeModulePathForFeature(moduleName, functionName, apiPath) {
366
- const normalized = normalizeModuleName(moduleName);
367
- const segments = normalized.split('/').filter(Boolean);
365
+ function normalizeModulePathForFeature(moduleName, functionName, apiPath) {
366
+ const normalized = normalizeModuleName(moduleName);
367
+ const segments = normalized.split('/').filter(Boolean);
368
368
  if (segments.length <= 1) {
369
369
  return normalized;
370
370
  }
@@ -379,73 +379,73 @@ function normalizeModulePathForFeature(moduleName, functionName, apiPath) {
379
379
  while (segments.length > 1 && duplicateNames.has(segments[segments.length - 1])) {
380
380
  segments.pop();
381
381
  }
382
-
383
- return segments.join('/');
384
- }
385
-
386
- function normalizeExplicitTargetPath(targetPath, label) {
387
- if (!targetPath) return '';
388
- const normalized = normalizeModuleName(String(targetPath));
389
- const segments = normalized.split('/').filter(Boolean);
390
- if (segments.length < 2) {
391
- throw new Error(`${label} must include parent path and feature name, for example admin/exampleFeature`);
392
- }
393
- return normalized;
394
- }
395
-
396
- function normalizeTargetI18nKey(targetI18nKey) {
397
- if (!targetI18nKey) return '';
398
- const normalized = String(targetI18nKey).trim().replace(/^\.|\.$/g, '');
399
- const segments = normalized.split('.').filter(Boolean);
400
- if (segments.length < 2) {
401
- throw new Error('targetI18nKey must include parent namespace and feature name, for example admin.exampleFeature');
402
- }
403
- return segments.join('.');
404
- }
405
-
406
- function resolveGenerationTargets({ moduleName, functionName, apiPath, targetViewDir, targetApiModule, targetI18nKey }) {
407
- const explicitViewDir = normalizeExplicitTargetPath(targetViewDir, 'targetViewDir');
408
- const explicitApiModule = normalizeExplicitTargetPath(targetApiModule, 'targetApiModule');
409
- const explicitI18nKey = normalizeTargetI18nKey(targetI18nKey);
410
-
411
- const fallbackModuleName = normalizeModulePathForFeature(moduleName, functionName, apiPath);
412
- const fallbackViewDir = [fallbackModuleName, functionName].filter(Boolean).join('/');
413
- const fallbackApiModule = [fallbackModuleName, functionName].filter(Boolean).join('/');
414
- const fallbackI18nKey = [fallbackModuleName, functionName].filter(Boolean).join('.').replace(/\//g, '.');
415
-
416
- const finalViewDir = explicitViewDir || fallbackViewDir;
417
- const viewSegments = finalViewDir.split('/').filter(Boolean);
418
- const finalFunctionName = viewSegments[viewSegments.length - 1];
419
- const finalModuleName = viewSegments.slice(0, -1).join('/');
420
- const finalApiModule = explicitApiModule || [finalModuleName, finalFunctionName].filter(Boolean).join('/');
421
- const finalI18nKey = explicitI18nKey || [finalModuleName, finalFunctionName].filter(Boolean).join('.');
422
-
423
- return {
424
- moduleName: finalModuleName,
425
- functionName: finalFunctionName,
426
- targetViewDir: finalViewDir,
427
- targetApiModule: finalApiModule,
428
- targetI18nKey: finalI18nKey,
429
- };
430
- }
431
-
432
- function normalizeFrontendRootPath(frontendPath) {
433
- const resolvedPath = path.resolve(String(frontendPath || ''));
434
- const normalizedPath = resolvedPath.replace(/[\\/]+$/, '');
435
- const baseName = path.basename(normalizedPath).toLowerCase();
436
- if (baseName === 'src') {
437
- return path.dirname(normalizedPath);
438
- }
439
- return normalizedPath;
440
- }
441
-
442
- function buildViewRoot(model) {
443
- return path.join(model.frontendPath, 'src', 'views', ...model.targetViewDir.split('/').filter(Boolean));
444
- }
445
-
446
- function buildApiFilePath(model) {
447
- return path.join(model.frontendPath, 'src', 'api', ...model.targetApiModule.split('/').filter(Boolean)) + '.ts';
448
- }
382
+
383
+ return segments.join('/');
384
+ }
385
+
386
+ function normalizeExplicitTargetPath(targetPath, label) {
387
+ if (!targetPath) return '';
388
+ const normalized = normalizeModuleName(String(targetPath));
389
+ const segments = normalized.split('/').filter(Boolean);
390
+ if (segments.length < 2) {
391
+ throw new Error(`${label} must include parent path and feature name, for example admin/exampleFeature`);
392
+ }
393
+ return normalized;
394
+ }
395
+
396
+ function normalizeTargetI18nKey(targetI18nKey) {
397
+ if (!targetI18nKey) return '';
398
+ const normalized = String(targetI18nKey).trim().replace(/^\.|\.$/g, '');
399
+ const segments = normalized.split('.').filter(Boolean);
400
+ if (segments.length < 2) {
401
+ throw new Error('targetI18nKey must include parent namespace and feature name, for example admin.exampleFeature');
402
+ }
403
+ return segments.join('.');
404
+ }
405
+
406
+ function resolveGenerationTargets({ moduleName, functionName, apiPath, targetViewDir, targetApiModule, targetI18nKey }) {
407
+ const explicitViewDir = normalizeExplicitTargetPath(targetViewDir, 'targetViewDir');
408
+ const explicitApiModule = normalizeExplicitTargetPath(targetApiModule, 'targetApiModule');
409
+ const explicitI18nKey = normalizeTargetI18nKey(targetI18nKey);
410
+
411
+ const fallbackModuleName = normalizeModulePathForFeature(moduleName, functionName, apiPath);
412
+ const fallbackViewDir = [fallbackModuleName, functionName].filter(Boolean).join('/');
413
+ const fallbackApiModule = [fallbackModuleName, functionName].filter(Boolean).join('/');
414
+ const fallbackI18nKey = [fallbackModuleName, functionName].filter(Boolean).join('.').replace(/\//g, '.');
415
+
416
+ const finalViewDir = explicitViewDir || fallbackViewDir;
417
+ const viewSegments = finalViewDir.split('/').filter(Boolean);
418
+ const finalFunctionName = viewSegments[viewSegments.length - 1];
419
+ const finalModuleName = viewSegments.slice(0, -1).join('/');
420
+ const finalApiModule = explicitApiModule || [finalModuleName, finalFunctionName].filter(Boolean).join('/');
421
+ const finalI18nKey = explicitI18nKey || [finalModuleName, finalFunctionName].filter(Boolean).join('.');
422
+
423
+ return {
424
+ moduleName: finalModuleName,
425
+ functionName: finalFunctionName,
426
+ targetViewDir: finalViewDir,
427
+ targetApiModule: finalApiModule,
428
+ targetI18nKey: finalI18nKey,
429
+ };
430
+ }
431
+
432
+ function normalizeFrontendRootPath(frontendPath) {
433
+ const resolvedPath = path.resolve(String(frontendPath || ''));
434
+ const normalizedPath = resolvedPath.replace(/[\\/]+$/, '');
435
+ const baseName = path.basename(normalizedPath).toLowerCase();
436
+ if (baseName === 'src') {
437
+ return path.dirname(normalizedPath);
438
+ }
439
+ return normalizedPath;
440
+ }
441
+
442
+ function buildViewRoot(model) {
443
+ return path.join(model.frontendPath, 'src', 'views', ...model.targetViewDir.split('/').filter(Boolean));
444
+ }
445
+
446
+ function buildApiFilePath(model) {
447
+ return path.join(model.frontendPath, 'src', 'api', ...model.targetApiModule.split('/').filter(Boolean)) + '.ts';
448
+ }
449
449
 
450
450
  function toConstantCase(value) {
451
451
  return String(value || '')
@@ -569,12 +569,12 @@ function buildUniqueDictRegistryKey(dictType, usedKeys, existingByKey) {
569
569
  return candidate;
570
570
  }
571
571
 
572
- function buildI18nNamespaceSegments(model) {
573
- if (model.targetI18nKey) {
574
- return model.targetI18nKey.split('.').filter(Boolean);
575
- }
576
- return [...model.moduleName.split('/').filter(Boolean), model.functionName];
577
- }
572
+ function buildI18nNamespaceSegments(model) {
573
+ if (model.targetI18nKey) {
574
+ return model.targetI18nKey.split('.').filter(Boolean);
575
+ }
576
+ return [...model.moduleName.split('/').filter(Boolean), model.functionName];
577
+ }
578
578
 
579
579
  function buildI18nNamespace(model) {
580
580
  return buildI18nNamespaceSegments(model).join('.');
@@ -675,43 +675,43 @@ function buildZhCnLocaleObject(model) {
675
675
  return root;
676
676
  }
677
677
 
678
- function prepareZhCnLocaleFile(model, mergeExisting) {
679
- const localeSegments = buildI18nNamespaceSegments(model);
680
- const localePath = path.join(model.frontendPath, 'src', 'i18n', 'biz', ...localeSegments) + '.zh-cn.ts';
681
- const exists = fs.existsSync(localePath);
682
- const currentContent = exists ? readUtf8File(localePath) : '';
683
- const generatedObject = buildZhCnLocaleObject(model);
684
- const generatedContent = renderExportDefaultContent(generatedObject);
685
-
686
- if (!mergeExisting) {
687
- return {
688
- path: localePath,
689
- frontendPath: model.frontendPath,
690
- exists,
691
- isCompatible: true,
692
- namespace: buildI18nNamespace(model),
693
- content: generatedContent,
694
- needsWrite: !exists || currentContent !== generatedContent,
695
- mergeMode: 'replace',
696
- };
697
- }
698
-
699
- const currentObject = exists ? parseExportDefaultObject(currentContent) : null;
700
- const isCompatible = !exists || isPlainObject(currentObject);
701
- const sanitizedCurrentObject = isCompatible ? removeFeatureCommonLocaleSections(currentObject || {}, model) : null;
702
- const mergedObject = isCompatible ? deepMergeMissing(sanitizedCurrentObject || {}, generatedObject) : null;
703
-
704
- return {
705
- path: localePath,
706
- frontendPath: model.frontendPath,
707
- exists,
708
- isCompatible,
709
- namespace: buildI18nNamespace(model),
710
- content: mergedObject ? renderExportDefaultContent(mergedObject) : '',
711
- needsWrite: !exists || (isCompatible && renderExportDefaultContent(currentObject || {}) !== renderExportDefaultContent(mergedObject)),
712
- mergeMode: 'merge',
713
- };
714
- }
678
+ function prepareZhCnLocaleFile(model, mergeExisting) {
679
+ const localeSegments = buildI18nNamespaceSegments(model);
680
+ const localePath = path.join(model.frontendPath, 'src', 'i18n', 'biz', ...localeSegments) + '.zh-cn.ts';
681
+ const exists = fs.existsSync(localePath);
682
+ const currentContent = exists ? readUtf8File(localePath) : '';
683
+ const generatedObject = buildZhCnLocaleObject(model);
684
+ const generatedContent = renderExportDefaultContent(generatedObject);
685
+
686
+ if (!mergeExisting) {
687
+ return {
688
+ path: localePath,
689
+ frontendPath: model.frontendPath,
690
+ exists,
691
+ isCompatible: true,
692
+ namespace: buildI18nNamespace(model),
693
+ content: generatedContent,
694
+ needsWrite: !exists || currentContent !== generatedContent,
695
+ mergeMode: 'replace',
696
+ };
697
+ }
698
+
699
+ const currentObject = exists ? parseExportDefaultObject(currentContent) : null;
700
+ const isCompatible = !exists || isPlainObject(currentObject);
701
+ const sanitizedCurrentObject = isCompatible ? removeFeatureCommonLocaleSections(currentObject || {}, model) : null;
702
+ const mergedObject = isCompatible ? deepMergeMissing(sanitizedCurrentObject || {}, generatedObject) : null;
703
+
704
+ return {
705
+ path: localePath,
706
+ frontendPath: model.frontendPath,
707
+ exists,
708
+ isCompatible,
709
+ namespace: buildI18nNamespace(model),
710
+ content: mergedObject ? renderExportDefaultContent(mergedObject) : '',
711
+ needsWrite: !exists || (isCompatible && renderExportDefaultContent(currentObject || {}) !== renderExportDefaultContent(mergedObject)),
712
+ mergeMode: 'merge',
713
+ };
714
+ }
715
715
 
716
716
  function prepareDictRegistry(frontendPath, dictTypes) {
717
717
  const registryPath = path.join(frontendPath, 'src', 'enums', 'dict-registry.ts');
@@ -765,46 +765,46 @@ function ensureDirectory(filePath) {
765
765
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
766
766
  }
767
767
 
768
- function writeSupportFile(filePath, content) {
769
- ensureDirectory(filePath);
770
- fs.writeFileSync(filePath, content, 'utf8');
771
- }
772
-
773
- function buildVirtualDictRegistry(dictTypes) {
774
- const normalizedDictTypes = [...new Set((dictTypes || []).filter(Boolean))];
775
- const entries = normalizedDictTypes.map((dictType) => ({ key: getPreferredDictRegistryKey(dictType), value: dictType }));
776
- return {
777
- path: '',
778
- entries,
779
- keyByValue: new Map(entries.map((entry) => [entry.value, entry.key])),
780
- needsWrite: false,
781
- writeEnabled: false,
782
- };
783
- }
784
-
785
- function prepareSharedSupport(frontendPath, dictTypes, writeSupportFiles) {
786
- const normalizedDictTypes = [...new Set((dictTypes || []).filter(Boolean))];
787
- const dictRegistry = writeSupportFiles ? prepareDictRegistry(frontendPath, normalizedDictTypes) : buildVirtualDictRegistry(normalizedDictTypes);
788
- const crudSchemaPath = path.join(frontendPath, 'src', 'utils', 'crudSchema.ts');
789
- const crudSchema = writeSupportFiles
790
- ? ensureCrudSchemaSupportFile(frontendPath)
791
- : {
792
- path: crudSchemaPath,
793
- content: DEFAULT_CRUD_SCHEMA_TEMPLATE,
794
- exists: fs.existsSync(crudSchemaPath),
795
- isCompatible: true,
796
- needsWrite: false,
797
- writeEnabled: false,
798
- };
799
- return {
800
- dictRegistry,
801
- crudSchema,
802
- writeEnabled: Boolean(writeSupportFiles),
803
- };
804
- }
805
-
806
- function maybeWriteSharedSupport(sharedSupport, writeToDisk) {
807
- if (!writeToDisk || !sharedSupport.writeEnabled) return;
768
+ function writeSupportFile(filePath, content) {
769
+ ensureDirectory(filePath);
770
+ fs.writeFileSync(filePath, content, 'utf8');
771
+ }
772
+
773
+ function buildVirtualDictRegistry(dictTypes) {
774
+ const normalizedDictTypes = [...new Set((dictTypes || []).filter(Boolean))];
775
+ const entries = normalizedDictTypes.map((dictType) => ({ key: getPreferredDictRegistryKey(dictType), value: dictType }));
776
+ return {
777
+ path: '',
778
+ entries,
779
+ keyByValue: new Map(entries.map((entry) => [entry.value, entry.key])),
780
+ needsWrite: false,
781
+ writeEnabled: false,
782
+ };
783
+ }
784
+
785
+ function prepareSharedSupport(frontendPath, dictTypes, writeSupportFiles) {
786
+ const normalizedDictTypes = [...new Set((dictTypes || []).filter(Boolean))];
787
+ const dictRegistry = writeSupportFiles ? prepareDictRegistry(frontendPath, normalizedDictTypes) : buildVirtualDictRegistry(normalizedDictTypes);
788
+ const crudSchemaPath = path.join(frontendPath, 'src', 'utils', 'crudSchema.ts');
789
+ const crudSchema = writeSupportFiles
790
+ ? ensureCrudSchemaSupportFile(frontendPath)
791
+ : {
792
+ path: crudSchemaPath,
793
+ content: DEFAULT_CRUD_SCHEMA_TEMPLATE,
794
+ exists: fs.existsSync(crudSchemaPath),
795
+ isCompatible: true,
796
+ needsWrite: false,
797
+ writeEnabled: false,
798
+ };
799
+ return {
800
+ dictRegistry,
801
+ crudSchema,
802
+ writeEnabled: Boolean(writeSupportFiles),
803
+ };
804
+ }
805
+
806
+ function maybeWriteSharedSupport(sharedSupport, writeToDisk) {
807
+ if (!writeToDisk || !sharedSupport.writeEnabled) return;
808
808
 
809
809
  if (sharedSupport.crudSchema.needsWrite) {
810
810
  writeSupportFile(sharedSupport.crudSchema.path, sharedSupport.crudSchema.content);
@@ -815,35 +815,35 @@ function maybeWriteSharedSupport(sharedSupport, writeToDisk) {
815
815
  }
816
816
  }
817
817
 
818
- function buildSupportNote(sharedSupport, localeZhSupport) {
819
- const notes = [];
820
-
821
- if (!sharedSupport.writeEnabled) {
822
- notes.push(
823
- 'Shared support file writing is disabled. Generated code still references src/utils/crudSchema.ts and may reference src/enums/dict-registry.ts, so those helpers must already exist in the target project.'
824
- );
825
- }
826
-
827
- if (sharedSupport.crudSchema.exists && !sharedSupport.crudSchema.isCompatible) {
828
- notes.push(
829
- 'Detected an existing src/utils/crudSchema.ts that does not match the expected helper signature. ' +
830
- 'MCP preserved the existing file and did not overwrite it. Generated pages now depend on that file being manually aligned.'
818
+ function buildSupportNote(sharedSupport, localeZhSupport) {
819
+ const notes = [];
820
+
821
+ if (!sharedSupport.writeEnabled) {
822
+ notes.push(
823
+ 'Shared support file writing is disabled. Generated code still references src/utils/crudSchema.ts and may reference src/enums/dict-registry.ts, so those helpers must already exist in the target project.'
824
+ );
825
+ }
826
+
827
+ if (sharedSupport.crudSchema.exists && !sharedSupport.crudSchema.isCompatible) {
828
+ notes.push(
829
+ 'Detected an existing src/utils/crudSchema.ts that does not match the expected helper signature. ' +
830
+ 'MCP preserved the existing file and did not overwrite it. Generated pages now depend on that file being manually aligned.'
831
831
  );
832
832
  }
833
833
 
834
- if (localeZhSupport.exists && !localeZhSupport.isCompatible) {
835
- notes.push(
836
- `Detected an existing ${path.relative(localeZhSupport.frontendPath, localeZhSupport.path).replace(/\\/g, '/')} that MCP could not parse. ` +
837
- 'The file was preserved and not updated, so new Chinese i18n keys may need to be merged manually.'
838
- );
839
- }
840
-
841
- if (localeZhSupport.mergeMode === 'replace') {
842
- notes.push('zh-cn.ts merge is disabled. MCP rendered the Chinese locale file from current metadata only.');
843
- }
844
-
845
- return notes.length ? notes.join(' ') : 'Runtime template rendering completed.';
846
- }
834
+ if (localeZhSupport.exists && !localeZhSupport.isCompatible) {
835
+ notes.push(
836
+ `Detected an existing ${path.relative(localeZhSupport.frontendPath, localeZhSupport.path).replace(/\\/g, '/')} that MCP could not parse. ` +
837
+ 'The file was preserved and not updated, so new Chinese i18n keys may need to be merged manually.'
838
+ );
839
+ }
840
+
841
+ if (localeZhSupport.mergeMode === 'replace') {
842
+ notes.push('zh-cn.ts merge is disabled. MCP rendered the Chinese locale file from current metadata only.');
843
+ }
844
+
845
+ return notes.length ? notes.join(' ') : 'Runtime template rendering completed.';
846
+ }
847
847
 
848
848
  function getDictRegistryReference(dictType, keyByValue) {
849
849
  if (!dictType) return '';
@@ -861,34 +861,34 @@ function findDictType(comment) {
861
861
  return extractDictType(comment);
862
862
  }
863
863
 
864
- function mapFieldType(field) {
865
- if (field.dictType) return 'select';
866
- if (field.sqlType === 'DATETIME' || field.sqlType === 'TIMESTAMP') return 'datetime';
867
- if (field.sqlType === 'DATE') return 'date';
868
- if (['INT', 'BIGINT', 'DECIMAL', 'NUMERIC'].includes(field.sqlType)) return 'number';
869
- if (field.sqlType === 'TEXT') return 'textarea';
870
- if (field.sqlType === 'VARCHAR' && field.length && Number(field.length) > 64) return 'textarea';
871
- return 'text';
872
- }
873
-
874
- function normalizeStructuredFormType(value) {
875
- const normalized = String(value || '').trim().toLowerCase();
876
- if (!normalized) return '';
877
- if (normalized === 'date') return 'date';
878
- if (normalized === 'datetime') return 'datetime';
879
- if (normalized === 'microme-operator') return 'number';
880
- if (normalized === 'upload' || normalized === 'picker') {
881
- throw new Error(
882
- 'Explicit component/form type "' +
883
- normalized +
884
- '" is not yet supported by worsoft-codegen-local templates. Please keep it in parseResult for downstream handling or extend MCP template support first.'
885
- );
886
- }
887
- if (['text', 'select', 'textarea', 'number'].includes(normalized)) {
888
- return normalized;
889
- }
890
- throw new Error('Unsupported explicit component/form type: ' + normalized);
891
- }
864
+ function mapFieldType(field) {
865
+ if (field.dictType) return 'select';
866
+ if (field.sqlType === 'DATETIME' || field.sqlType === 'TIMESTAMP') return 'datetime';
867
+ if (field.sqlType === 'DATE') return 'date';
868
+ if (['INT', 'BIGINT', 'DECIMAL', 'NUMERIC'].includes(field.sqlType)) return 'number';
869
+ if (field.sqlType === 'TEXT') return 'textarea';
870
+ if (field.sqlType === 'VARCHAR' && field.length && Number(field.length) > 64) return 'textarea';
871
+ return 'text';
872
+ }
873
+
874
+ function normalizeStructuredFormType(value) {
875
+ const normalized = String(value || '').trim().toLowerCase();
876
+ if (!normalized) return '';
877
+ if (normalized === 'date') return 'date';
878
+ if (normalized === 'datetime') return 'datetime';
879
+ if (normalized === 'microme-operator') return 'number';
880
+ if (normalized === 'upload' || normalized === 'picker') {
881
+ throw new Error(
882
+ 'Explicit component/form type "' +
883
+ normalized +
884
+ '" is not yet supported by worsoft-codegen-local templates. Please keep it in parseResult for downstream handling or extend MCP template support first.'
885
+ );
886
+ }
887
+ if (['text', 'select', 'textarea', 'number'].includes(normalized)) {
888
+ return normalized;
889
+ }
890
+ throw new Error('Unsupported explicit component/form type: ' + normalized);
891
+ }
892
892
 
893
893
  function escapeForRegex(value) {
894
894
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -1000,6 +1000,28 @@ function normalizeApiPath(value) {
1000
1000
  .replace(/\/+$/, '');
1001
1001
  }
1002
1002
 
1003
+ function buildApiRoutePath(moduleName, apiPath) {
1004
+ const normalizedApiPath = normalizeApiPath(apiPath);
1005
+ if (!normalizedApiPath) return '';
1006
+
1007
+ const normalizedModuleName = normalizeModuleName(moduleName || '')
1008
+ .split('/')
1009
+ .filter(Boolean);
1010
+ const apiSegments = normalizedApiPath.split('/').filter(Boolean);
1011
+
1012
+ if (!normalizedModuleName.length) {
1013
+ return '/' + apiSegments.join('/');
1014
+ }
1015
+
1016
+ const modulePrefix = normalizedModuleName.join('/');
1017
+ const currentApiPath = apiSegments.join('/');
1018
+ if (currentApiPath === modulePrefix || currentApiPath.startsWith(modulePrefix + '/')) {
1019
+ return '/' + currentApiPath;
1020
+ }
1021
+
1022
+ return '/' + [...normalizedModuleName, ...apiSegments].join('/');
1023
+ }
1024
+
1003
1025
  function normalizeStructuredSqlType(value) {
1004
1026
  const normalized = String(value || '').trim();
1005
1027
  const upper = normalized.replace(/\s+/g, '').toUpperCase();
@@ -1023,7 +1045,7 @@ function normalizeStructuredLengthAndScale(lengthValue, scaleValue) {
1023
1045
  };
1024
1046
  }
1025
1047
 
1026
- function normalizeStructuredField(inputField, index, contextLabel) {
1048
+ function normalizeStructuredField(inputField, index, contextLabel) {
1027
1049
  if (!inputField || typeof inputField !== 'object') {
1028
1050
  throw new Error(contextLabel + '[' + index + '] must be an object');
1029
1051
  }
@@ -1042,42 +1064,42 @@ function normalizeStructuredField(inputField, index, contextLabel) {
1042
1064
  throw new Error(contextLabel + '[' + index + '] is missing required field: type');
1043
1065
  }
1044
1066
 
1045
- const { length, scale } = normalizeStructuredLengthAndScale(inputField.length, inputField.scale);
1046
- const explicitFormType = normalizeStructuredFormType(inputField.formType || inputField.componentType);
1047
- const explicitQueryType =
1048
- inputField.queryType === undefined || inputField.queryType === null || inputField.queryType === ''
1049
- ? undefined
1050
- : Number.parseInt(String(inputField.queryType), 10);
1051
- const listShow = parseBooleanLike(inputField.listShow, parseBooleanLike(inputField.show, true));
1052
-
1053
- return {
1054
- fieldName,
1055
- attrName: toCamelCase(fieldName),
1067
+ const { length, scale } = normalizeStructuredLengthAndScale(inputField.length, inputField.scale);
1068
+ const explicitFormType = normalizeStructuredFormType(inputField.formType || inputField.componentType);
1069
+ const explicitQueryType =
1070
+ inputField.queryType === undefined || inputField.queryType === null || inputField.queryType === ''
1071
+ ? undefined
1072
+ : Number.parseInt(String(inputField.queryType), 10);
1073
+ const listShow = parseBooleanLike(inputField.listShow, parseBooleanLike(inputField.show, true));
1074
+
1075
+ return {
1076
+ fieldName,
1077
+ attrName: toCamelCase(fieldName),
1056
1078
  sqlType: type,
1057
1079
  length,
1058
1080
  scale,
1059
1081
  comment: label,
1060
1082
  label,
1061
- description: String(inputField.description || '').trim(),
1062
- formType: explicitFormType,
1063
- dictType: normalizeDictType(inputField.dictType),
1064
- notNull: parseBooleanLike(inputField.required, false),
1065
- defaultValue: normalizeDefaultValue(inputField.defaultValue),
1066
- readonly: parseBooleanLike(inputField.readonly, false),
1067
- show: parseBooleanLike(inputField.show, true),
1068
- listShow,
1069
- smart: parseBooleanLike(inputField.smart, false),
1070
- queryType: Number.isNaN(explicitQueryType)
1071
- ? undefined
1072
- : explicitQueryType !== undefined
1073
- ? explicitQueryType
1074
- : normalizeDictType(inputField.dictType) && listShow
1075
- ? 30
1076
- : undefined,
1077
- sourceKind: normalizeStructuredSourceKind(inputField.sourceKind),
1078
- primary: parseBooleanLike(inputField.primary, fieldName === 'id'),
1079
- };
1080
- }
1083
+ description: String(inputField.description || '').trim(),
1084
+ formType: explicitFormType,
1085
+ dictType: normalizeDictType(inputField.dictType),
1086
+ notNull: parseBooleanLike(inputField.required, false),
1087
+ defaultValue: normalizeDefaultValue(inputField.defaultValue),
1088
+ readonly: parseBooleanLike(inputField.readonly, false),
1089
+ show: parseBooleanLike(inputField.show, true),
1090
+ listShow,
1091
+ smart: parseBooleanLike(inputField.smart, false),
1092
+ queryType: Number.isNaN(explicitQueryType)
1093
+ ? undefined
1094
+ : explicitQueryType !== undefined
1095
+ ? explicitQueryType
1096
+ : normalizeDictType(inputField.dictType) && listShow
1097
+ ? 30
1098
+ : undefined,
1099
+ sourceKind: normalizeStructuredSourceKind(inputField.sourceKind),
1100
+ primary: parseBooleanLike(inputField.primary, fieldName === 'id'),
1101
+ };
1102
+ }
1081
1103
 
1082
1104
  function normalizeStructuredFieldArray(inputFields, contextLabel) {
1083
1105
  if (!Array.isArray(inputFields) || !inputFields.length) {
@@ -1114,14 +1136,14 @@ function buildRetryArguments(safeArgs) {
1114
1136
  tableName: safeArgs.tableName,
1115
1137
  ...(safeArgs.tableComment ? { tableComment: safeArgs.tableComment } : {}),
1116
1138
  style: safeArgs.style,
1117
- frontendPath: safeArgs.frontendPath,
1118
- moduleName: safeArgs.moduleName,
1119
- writeToDisk: safeArgs.writeToDisk,
1120
- overwrite: safeArgs.overwrite,
1121
- writeSupportFiles: safeArgs.writeSupportFiles,
1122
- mergeI18nZh: safeArgs.mergeI18nZh,
1123
- fields: safeArgs.fields,
1124
- };
1139
+ frontendPath: safeArgs.frontendPath,
1140
+ moduleName: safeArgs.moduleName,
1141
+ writeToDisk: safeArgs.writeToDisk,
1142
+ overwrite: safeArgs.overwrite,
1143
+ writeSupportFiles: safeArgs.writeSupportFiles,
1144
+ mergeI18nZh: safeArgs.mergeI18nZh,
1145
+ fields: safeArgs.fields,
1146
+ };
1125
1147
 
1126
1148
  if (safeArgs.children && safeArgs.children.length) {
1127
1149
  retryArguments.children = safeArgs.children;
@@ -1244,53 +1266,53 @@ function normalizeLevelsInput(inputLevels) {
1244
1266
  return levels;
1245
1267
  }
1246
1268
 
1247
- function getStylePreset(styleId) {
1248
- const preset = STYLE_CATALOG[styleId];
1249
- if (!preset) throw new Error('Unsupported style: ' + styleId);
1250
- return preset;
1251
- }
1252
-
1253
- function normalizePageTypeInput(pageType) {
1254
- if (pageType === undefined || pageType === null || pageType === '') return '';
1255
- const normalized = String(pageType).trim();
1256
- if (['business', 'dict', 'non_standard'].includes(normalized)) return normalized;
1257
- throw new Error(`Unsupported pageType: ${normalized}. Allowed values are dict, business, non_standard.`);
1258
- }
1259
-
1260
- function rejectSemanticStageInputs(input) {
1261
- const forbiddenKeys = [
1262
- 'parseResult',
1263
- 'prdFile',
1264
- 'apiDocFile',
1265
- 'fieldMappings',
1266
- 'dictionaryMeta',
1267
- 'listQueryMeta',
1268
- 'fieldUiMeta',
1269
- ];
1270
- const present = forbiddenKeys.filter((key) => Object.prototype.hasOwnProperty.call(input, key));
1271
- if (present.length) {
1272
- throw new Error(`worsoft_codegen_local_generate_frontend only accepts translated low-level generation parameters. Unsupported semantic-stage keys: ${present.join(', ')}`);
1273
- }
1274
- }
1275
-
1276
- function validatePageTypeAndStyle(pageType, style) {
1277
- if (!pageType) return;
1278
- if (pageType === 'non_standard') {
1279
- throw new Error('non_standard pages are not supported by worsoft_codegen_local_generate_frontend');
1280
- }
1281
- if (pageType !== 'dict') return;
1282
- if (style === 'single_table_jump' || style === 'master_child_jump') {
1283
- throw new Error(`Dict pages must use dialog-based styles. pageType=dict does not support style=${style}`);
1284
- }
1285
- }
1286
-
1287
- function hasRuntimeSupport(stylePreset) {
1288
- return Boolean(stylePreset.runtime && stylePreset.runtime.supported && stylePreset.runtime.templateDir);
1289
- }
1290
-
1291
- function normalizeFields(parsed) {
1292
- return parsed.fields.map((field) => ({ ...field, formType: field.formType || mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1293
- }
1269
+ function getStylePreset(styleId) {
1270
+ const preset = STYLE_CATALOG[styleId];
1271
+ if (!preset) throw new Error('Unsupported style: ' + styleId);
1272
+ return preset;
1273
+ }
1274
+
1275
+ function normalizePageTypeInput(pageType) {
1276
+ if (pageType === undefined || pageType === null || pageType === '') return '';
1277
+ const normalized = String(pageType).trim();
1278
+ if (['business', 'dict', 'non_standard'].includes(normalized)) return normalized;
1279
+ throw new Error(`Unsupported pageType: ${normalized}. Allowed values are dict, business, non_standard.`);
1280
+ }
1281
+
1282
+ function rejectSemanticStageInputs(input) {
1283
+ const forbiddenKeys = [
1284
+ 'parseResult',
1285
+ 'prdFile',
1286
+ 'apiDocFile',
1287
+ 'fieldMappings',
1288
+ 'dictionaryMeta',
1289
+ 'listQueryMeta',
1290
+ 'fieldUiMeta',
1291
+ ];
1292
+ const present = forbiddenKeys.filter((key) => Object.prototype.hasOwnProperty.call(input, key));
1293
+ if (present.length) {
1294
+ throw new Error(`worsoft_codegen_local_generate_frontend only accepts translated low-level generation parameters. Unsupported semantic-stage keys: ${present.join(', ')}`);
1295
+ }
1296
+ }
1297
+
1298
+ function validatePageTypeAndStyle(pageType, style) {
1299
+ if (!pageType) return;
1300
+ if (pageType === 'non_standard') {
1301
+ throw new Error('non_standard pages are not supported by worsoft_codegen_local_generate_frontend');
1302
+ }
1303
+ if (pageType !== 'dict') return;
1304
+ if (style === 'single_table_jump' || style === 'master_child_jump') {
1305
+ throw new Error(`Dict pages must use dialog-based styles. pageType=dict does not support style=${style}`);
1306
+ }
1307
+ }
1308
+
1309
+ function hasRuntimeSupport(stylePreset) {
1310
+ return Boolean(stylePreset.runtime && stylePreset.runtime.supported && stylePreset.runtime.templateDir);
1311
+ }
1312
+
1313
+ function normalizeFields(parsed) {
1314
+ return parsed.fields.map((field) => ({ ...field, formType: field.formType || mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1315
+ }
1294
1316
 
1295
1317
  function ensureFieldExists(fields, fieldName, tableName, role) {
1296
1318
  const field = fields.find((item) => item.fieldName === fieldName);
@@ -1378,33 +1400,36 @@ function buildMultiLevelDictModel(safeArgs) {
1378
1400
  throw new Error('multi_level_dict requires level 1 with at least one parent module');
1379
1401
  }
1380
1402
 
1381
- const parentModule = parentLevel.modules[0];
1382
- const derivedFunctionName = toCamelCase(safeArgs.tableName || parentModule.tableName);
1383
- const resolvedTargets = resolveGenerationTargets({
1384
- moduleName: safeArgs.moduleName,
1385
- functionName: derivedFunctionName,
1386
- apiPath: safeArgs.apiPath || parentModule.apiPath,
1387
- targetViewDir: safeArgs.targetViewDir,
1388
- targetApiModule: safeArgs.targetApiModule,
1389
- targetI18nKey: safeArgs.targetI18nKey,
1390
- });
1391
- const allModules = builtLevels.flatMap((level) => level.modules);
1392
- const dictTypes = [...new Set(allModules.flatMap((module) => module.optionFields.map((field) => field.dictType).filter(Boolean)))];
1393
-
1394
- return {
1395
- featureTitle: safeArgs.featureTitle || safeArgs.tableComment || parentModule.tableComment,
1396
- tableName: safeArgs.tableName,
1397
- tableComment: safeArgs.tableComment || parentModule.tableComment,
1398
- apiPath: safeArgs.apiPath || parentModule.apiPath,
1399
- pageType: safeArgs.pageType || 'dict',
1400
- className: toPascalCase(safeArgs.tableName || parentModule.tableName),
1401
- functionName: resolvedTargets.functionName,
1402
- moduleName: resolvedTargets.moduleName,
1403
- targetViewDir: resolvedTargets.targetViewDir,
1404
- targetApiModule: resolvedTargets.targetApiModule,
1405
- targetI18nKey: resolvedTargets.targetI18nKey,
1406
- frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1407
- style: safeArgs.style,
1403
+ const parentModule = parentLevel.modules[0];
1404
+ const derivedFunctionName = toCamelCase(safeArgs.tableName || parentModule.tableName);
1405
+ const resolvedTargets = resolveGenerationTargets({
1406
+ moduleName: safeArgs.moduleName,
1407
+ functionName: derivedFunctionName,
1408
+ apiPath: safeArgs.apiPath || parentModule.apiPath,
1409
+ targetViewDir: safeArgs.targetViewDir,
1410
+ targetApiModule: safeArgs.targetApiModule,
1411
+ targetI18nKey: safeArgs.targetI18nKey,
1412
+ });
1413
+ const allModules = builtLevels.flatMap((level) => level.modules);
1414
+ const dictTypes = [...new Set(allModules.flatMap((module) => module.optionFields.map((field) => field.dictType).filter(Boolean)))];
1415
+ allModules.forEach((moduleModel) => {
1416
+ moduleModel.moduleName = resolvedTargets.moduleName;
1417
+ });
1418
+
1419
+ return {
1420
+ featureTitle: safeArgs.featureTitle || safeArgs.tableComment || parentModule.tableComment,
1421
+ tableName: safeArgs.tableName,
1422
+ tableComment: safeArgs.tableComment || parentModule.tableComment,
1423
+ apiPath: safeArgs.apiPath || parentModule.apiPath,
1424
+ pageType: safeArgs.pageType || 'dict',
1425
+ className: toPascalCase(safeArgs.tableName || parentModule.tableName),
1426
+ functionName: resolvedTargets.functionName,
1427
+ moduleName: resolvedTargets.moduleName,
1428
+ targetViewDir: resolvedTargets.targetViewDir,
1429
+ targetApiModule: resolvedTargets.targetApiModule,
1430
+ targetI18nKey: resolvedTargets.targetI18nKey,
1431
+ frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1432
+ style: safeArgs.style,
1408
1433
  levels: builtLevels,
1409
1434
  modules: allModules,
1410
1435
  dictTypes,
@@ -1475,36 +1500,36 @@ function buildModel(safeArgs) {
1475
1500
  const childDictTypes = children.flatMap((child) => child.optionFields.map((field) => field.dictType).filter(Boolean));
1476
1501
  const dictTypes = [...new Set([...optionFields.map((field) => field.dictType).filter(Boolean), ...childDictTypes])];
1477
1502
 
1478
- const derivedFunctionName = toCamelCase(safeArgs.tableName);
1479
- const apiPath = safeArgs.apiPath || derivedFunctionName;
1480
- const resolvedTargets = resolveGenerationTargets({
1481
- moduleName: safeArgs.moduleName,
1482
- functionName: derivedFunctionName,
1483
- apiPath,
1484
- targetViewDir: safeArgs.targetViewDir,
1485
- targetApiModule: safeArgs.targetApiModule,
1486
- targetI18nKey: safeArgs.targetI18nKey,
1487
- });
1488
- return {
1489
- featureTitle: safeArgs.featureTitle || safeArgs.tableComment || safeArgs.tableName,
1490
- tableName: safeArgs.tableName,
1491
- tableComment: safeArgs.tableComment || safeArgs.featureTitle || safeArgs.tableName,
1492
- apiPath,
1493
- pageType: safeArgs.pageType || '',
1494
- className: toPascalCase(safeArgs.tableName),
1495
- functionName: resolvedTargets.functionName,
1496
- moduleName: resolvedTargets.moduleName,
1497
- targetViewDir: resolvedTargets.targetViewDir,
1498
- targetApiModule: resolvedTargets.targetApiModule,
1499
- targetI18nKey: resolvedTargets.targetI18nKey,
1500
- pk: pkField,
1503
+ const derivedFunctionName = toCamelCase(safeArgs.tableName);
1504
+ const apiPath = safeArgs.apiPath || derivedFunctionName;
1505
+ const resolvedTargets = resolveGenerationTargets({
1506
+ moduleName: safeArgs.moduleName,
1507
+ functionName: derivedFunctionName,
1508
+ apiPath,
1509
+ targetViewDir: safeArgs.targetViewDir,
1510
+ targetApiModule: safeArgs.targetApiModule,
1511
+ targetI18nKey: safeArgs.targetI18nKey,
1512
+ });
1513
+ return {
1514
+ featureTitle: safeArgs.featureTitle || safeArgs.tableComment || safeArgs.tableName,
1515
+ tableName: safeArgs.tableName,
1516
+ tableComment: safeArgs.tableComment || safeArgs.featureTitle || safeArgs.tableName,
1517
+ apiPath,
1518
+ pageType: safeArgs.pageType || '',
1519
+ className: toPascalCase(safeArgs.tableName),
1520
+ functionName: resolvedTargets.functionName,
1521
+ moduleName: resolvedTargets.moduleName,
1522
+ targetViewDir: resolvedTargets.targetViewDir,
1523
+ targetApiModule: resolvedTargets.targetApiModule,
1524
+ targetI18nKey: resolvedTargets.targetI18nKey,
1525
+ pk: pkField,
1501
1526
  fields,
1502
1527
  optionFields,
1503
1528
  visibleFields,
1504
1529
  listFields,
1505
1530
  gridFields,
1506
1531
  dictTypes,
1507
- frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1532
+ frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1508
1533
  style: safeArgs.style,
1509
1534
  children,
1510
1535
  };
@@ -1648,6 +1673,8 @@ function renderFormRulesV2(fields) {
1648
1673
  function renderFormDefaults(model) {
1649
1674
  const lines = [` ${model.pk.attrName}: '',`];
1650
1675
  for (const field of model.fields.filter((item) => item.fieldName !== model.pk.fieldName && !item.isAudit)) lines.push(renderDefaultLine(field));
1676
+ lines.push(` version: 1,`);
1677
+ lines.push(` tenantId: Local.getTenant(),`);
1651
1678
  return lines.join('\n');
1652
1679
  }
1653
1680
 
@@ -1763,27 +1790,27 @@ function getDefaultOptionFieldWidthV2(field) {
1763
1790
 
1764
1791
  return '100';
1765
1792
  }
1766
- function renderOptionFieldV2(field, labelKey, dictRegistryRefs, indent = ' ') {
1767
- const fallbackLabel = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1768
- const parts = [`key: '${field.attrName}'`, `labelKey: '${labelKey}'`, `label: '${fallbackLabel}'`];
1769
- const width = getDefaultOptionFieldWidthV2(field);
1793
+ function renderOptionFieldV2(field, labelKey, dictRegistryRefs, indent = ' ') {
1794
+ const fallbackLabel = stripDictAnnotation(field.comment).replace(/'/g, "\\'");
1795
+ const parts = [`key: '${field.attrName}'`, `labelKey: '${labelKey}'`, `label: '${fallbackLabel}'`];
1796
+ const width = getDefaultOptionFieldWidthV2(field);
1770
1797
 
1771
1798
  if (width !== '120') {
1772
1799
  parts.push(`width: '${width}'`);
1773
1800
  }
1774
-
1775
- if (field.dictType) {
1776
- parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1777
- }
1778
- if (field.smart) {
1779
- parts.push('smart: true');
1780
- }
1781
- if (typeof field.queryType === 'number') {
1782
- parts.push(`queryType: ${field.queryType}`);
1783
- }
1784
- if (field.show === false) {
1785
- parts.push('show: false');
1786
- }
1801
+
1802
+ if (field.dictType) {
1803
+ parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1804
+ }
1805
+ if (field.smart) {
1806
+ parts.push('smart: true');
1807
+ }
1808
+ if (typeof field.queryType === 'number') {
1809
+ parts.push(`queryType: ${field.queryType}`);
1810
+ }
1811
+ if (field.show === false) {
1812
+ parts.push('show: false');
1813
+ }
1787
1814
  if (field.listShow === false) {
1788
1815
  parts.push('listShow: false');
1789
1816
  }
@@ -1904,28 +1931,31 @@ function renderMultiLevelOptionField(field, model, moduleModel, dictRegistryRefs
1904
1931
  `labelKey: '${buildMultiLevelFieldLabelKey(model, moduleModel, field)}'`,
1905
1932
  ];
1906
1933
  const width = getDefaultOptionFieldWidthV2(field);
1907
- if (width) parts.push(`width: '${width}'`);
1908
- if (field.show === false) parts.push('show: false');
1909
- if (field.listShow === false) parts.push('listShow: false');
1910
- if (field.smart) parts.push('smart: true');
1911
- if (typeof field.queryType === 'number') parts.push(`queryType: ${field.queryType}`);
1912
- if (field.dictType) parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1913
- return `${indent}{ ${parts.join(', ')} },`;
1914
- }
1934
+ if (width) parts.push(`width: '${width}'`);
1935
+ if (field.show === false) parts.push('show: false');
1936
+ if (field.listShow === false) parts.push('listShow: false');
1937
+ if (field.smart) parts.push('smart: true');
1938
+ if (typeof field.queryType === 'number') parts.push(`queryType: ${field.queryType}`);
1939
+ if (field.dictType) parts.push(`dictType: ${getDictRegistryReference(field.dictType, dictRegistryRefs)}`);
1940
+ return `${indent}{ ${parts.join(', ')} },`;
1941
+ }
1915
1942
 
1916
1943
  function renderMultiLevelModuleDefinition(model, moduleModel, dictRegistryRefs) {
1944
+ const moduleApiPath = buildApiRoutePath(model.moduleName, moduleModel.apiPath);
1945
+ const moduleEnableApi = buildApiRoutePath(model.moduleName, moduleModel.enableApi);
1946
+ const moduleDisableApi = buildApiRoutePath(model.moduleName, moduleModel.disableApi);
1917
1947
  const lines = [
1918
- ` ${moduleModel.key}: {`,
1919
- ` key: '${moduleModel.key}',`,
1920
- ` titleKey: '${buildMultiLevelModuleTitleKey(model, moduleModel)}',`,
1921
- ` apiPath: '${moduleModel.apiPath}',`,
1922
- ` primaryKey: '${moduleModel.pk.attrName}',`,
1923
- ];
1948
+ ` ${moduleModel.key}: {`,
1949
+ ` key: '${moduleModel.key}',`,
1950
+ ` titleKey: '${buildMultiLevelModuleTitleKey(model, moduleModel)}',`,
1951
+ ` apiPath: '${moduleApiPath}',`,
1952
+ ` primaryKey: '${moduleModel.pk.attrName}',`,
1953
+ ];
1924
1954
  if (moduleModel.queryParentField) lines.push(` queryParentField: '${moduleModel.queryParentField}',`);
1925
1955
  if (moduleModel.statusField) lines.push(` statusField: '${moduleModel.statusField}',`);
1926
1956
  if (moduleModel.statusDictType) lines.push(` statusDictType: ${getDictRegistryReference(moduleModel.statusDictType, dictRegistryRefs)},`);
1927
- lines.push(` enableApi: '${moduleModel.enableApi.startsWith('/') ? moduleModel.enableApi : '/' + moduleModel.enableApi}',`);
1928
- lines.push(` disableApi: '${moduleModel.disableApi.startsWith('/') ? moduleModel.disableApi : '/' + moduleModel.disableApi}',`);
1957
+ lines.push(` enableApi: '${moduleEnableApi}',`);
1958
+ lines.push(` disableApi: '${moduleDisableApi}',`);
1929
1959
  lines.push(' fields: [');
1930
1960
  lines.push(moduleModel.optionFields.map((field) => renderMultiLevelOptionField(field, model, moduleModel, dictRegistryRefs)).join('\n'));
1931
1961
  lines.push(' ],');
@@ -1989,9 +2019,9 @@ function renderMultiLevelOptionsTs(model, dictRegistryRefs) {
1989
2019
 
1990
2020
  function renderMultiLevelApiFunctions(moduleModel) {
1991
2021
  const pkAttr = moduleModel.pk.attrName;
1992
- const basePath = moduleModel.apiPath.startsWith('/') ? moduleModel.apiPath : '/' + moduleModel.apiPath;
1993
- const enablePath = moduleModel.enableApi.startsWith('/') ? moduleModel.enableApi : '/' + moduleModel.enableApi;
1994
- const disablePath = moduleModel.disableApi.startsWith('/') ? moduleModel.disableApi : '/' + moduleModel.disableApi;
2022
+ const basePath = buildApiRoutePath(moduleModel.moduleName, moduleModel.apiPath);
2023
+ const enablePath = buildApiRoutePath(moduleModel.moduleName, moduleModel.enableApi);
2024
+ const disablePath = buildApiRoutePath(moduleModel.moduleName, moduleModel.disableApi);
1995
2025
  return [
1996
2026
  `export function fetch${moduleModel.className}List(query?: any) {`,
1997
2027
  ' return request({',
@@ -2052,13 +2082,13 @@ function renderMultiLevelApiFunctions(moduleModel) {
2052
2082
  }
2053
2083
 
2054
2084
  function renderMultiLevelApiTs(model) {
2055
- return [
2056
- "import request from '/@/utils/request';",
2057
- '',
2058
- model.modules.map(renderMultiLevelApiFunctions).join('\n\n'),
2059
- '',
2060
- ].join('\n');
2061
- }
2085
+ return [
2086
+ "import request from '/@/utils/request';",
2087
+ '',
2088
+ model.modules.map(renderMultiLevelApiFunctions).join('\n\n'),
2089
+ '',
2090
+ ].join('\n');
2091
+ }
2062
2092
 
2063
2093
  function renderMultiLevelFormField(field) {
2064
2094
  const labelExpr = `getFieldLabel('${field.attrName}')`;
@@ -2143,6 +2173,8 @@ function renderMultiLevelFormVue(model, moduleModel) {
2143
2173
  const defaultLines = [
2144
2174
  ` ${moduleModel.pk.attrName}: '',`,
2145
2175
  ...moduleModel.optionFields.map((field) => renderDefaultLine(field)),
2176
+ ` version: 1,`,
2177
+ ` tenantId: Local.getTenant(),`,
2146
2178
  ].join('\n');
2147
2179
  const rules = renderFormRulesV2(moduleModel.visibleFields);
2148
2180
  return `<template>
@@ -2163,6 +2195,7 @@ ${moduleModel.visibleFields.map(renderMultiLevelFormField).join('\n')}
2163
2195
 
2164
2196
  <script setup lang="ts" name="${componentName}">
2165
2197
  import { useMessage } from '/@/hooks/message';
2198
+ import { Local } from '/@/utils/storage';
2166
2199
  import { useDict } from '/@/hooks/dict';
2167
2200
  import { useCrudPageMeta } from '/@/hooks/useCrudPageMeta';
2168
2201
  import { useI18n } from 'vue-i18n';
@@ -2551,15 +2584,15 @@ function renderMultiLevelMenuSql(model) {
2551
2584
  ].join('\n');
2552
2585
  }
2553
2586
 
2554
- function renderMultiLevelFiles(model, sharedSupport, localeZhSupport) {
2555
- const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
2556
- const viewRoot = buildViewRoot(model);
2557
- const apiFilePath = buildApiFilePath(model);
2558
- const menuRoot = path.join(model.frontendPath, 'menu');
2559
- const files = [
2560
- { type: 'list', path: path.join(viewRoot, 'index.vue'), content: renderMultiLevelIndexVue(model) },
2561
- { type: 'options', path: path.join(viewRoot, 'options.ts'), content: renderMultiLevelOptionsTs(model, dictRegistryRefs) },
2562
- { type: 'api', path: apiFilePath, content: renderMultiLevelApiTs(model) },
2587
+ function renderMultiLevelFiles(model, sharedSupport, localeZhSupport) {
2588
+ const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
2589
+ const viewRoot = buildViewRoot(model);
2590
+ const apiFilePath = buildApiFilePath(model);
2591
+ const menuRoot = path.join(model.frontendPath, 'menu');
2592
+ const files = [
2593
+ { type: 'list', path: path.join(viewRoot, 'index.vue'), content: renderMultiLevelIndexVue(model) },
2594
+ { type: 'options', path: path.join(viewRoot, 'options.ts'), content: renderMultiLevelOptionsTs(model, dictRegistryRefs) },
2595
+ { type: 'api', path: apiFilePath, content: renderMultiLevelApiTs(model) },
2563
2596
  {
2564
2597
  type: 'i18nZh',
2565
2598
  path: localeZhSupport.path,
@@ -2583,7 +2616,8 @@ function renderMultiLevelFiles(model, sharedSupport, localeZhSupport) {
2583
2616
 
2584
2617
  function buildReplacements(model, sharedSupport) {
2585
2618
  const menuBaseId = Date.now();
2586
- const apiModulePath = model.apiPath || `${model.moduleName}/${model.functionName}`;
2619
+ const apiModulePath = model.targetApiModule || `${model.moduleName}/${model.functionName}`;
2620
+ const apiRoutePath = buildApiRoutePath(model.moduleName, model.apiPath || model.functionName).replace(/^\/+/, '');
2587
2621
  const routePath = `${model.moduleName}/${model.functionName}`;
2588
2622
  const permissionPrefix = `${model.moduleName}/${model.functionName}`.replace(/\//g, '_');
2589
2623
  const dictRegistryRefs = sharedSupport.dictRegistry.keyByValue;
@@ -2597,7 +2631,7 @@ function buildReplacements(model, sharedSupport) {
2597
2631
  FUNCTION_NAME: model.functionName,
2598
2632
  PK_ATTR: model.pk.attrName,
2599
2633
  API_MODULE_PATH: apiModulePath,
2600
- API_PATH: apiModulePath,
2634
+ API_PATH: apiRoutePath,
2601
2635
  VIEW_MODULE_PATH: routePath,
2602
2636
  MENU_ROUTE_PATH: routePath,
2603
2637
  I18N_NAMESPACE: i18nNamespace,
@@ -2610,7 +2644,7 @@ function buildReplacements(model, sharedSupport) {
2610
2644
  MENU_BASE_ID_PLUS_5: menuBaseId + 5,
2611
2645
  GENERATED_AT: new Date().toISOString(),
2612
2646
  FORM_FIELDS: model.visibleFields.map(renderFormFieldV2).join('\n'),
2613
- TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumn(field, dictRegistryRefs)).join('\n'),
2647
+ TABLE_COLUMNS: model.gridFields.map((field) => renderTableColumn(field, dictRegistryRefs)).join('\n'),
2614
2648
  FORM_DEFAULTS: renderFormDefaults(model),
2615
2649
  DICT_REGISTRY_IMPORT_BLOCK: model.dictTypes.length ? "import { DictRegistry } from '/@/enums/dict-registry';" : '',
2616
2650
  MASTER_OPTION_FIELDS: model.optionFields.map((field) => renderOptionFieldV2(field, buildFieldLabelKey(model, field), dictRegistryRefs)).join('\n'),
@@ -2619,7 +2653,7 @@ function buildReplacements(model, sharedSupport) {
2619
2653
  CHILD_FORM_LIST_DEFAULTS: renderChildFormListDefaults(model.children),
2620
2654
  CHILD_TEMP_DECLARATIONS: renderChildTempDeclarations(model.children),
2621
2655
  CHILD_RESET_LISTS: renderChildResetListLines(model.children),
2622
- CHILD_SECTIONS: model.children.map((childModel) => renderChildSection(childModel, model.children.length)).join('\n'),
2656
+ CHILD_SECTIONS: model.children.map((childModel) => renderChildSection(childModel, model.children.length)).join('\n'),
2623
2657
  };
2624
2658
  }
2625
2659
 
@@ -2638,15 +2672,15 @@ function renderFiles(model, stylePreset, sharedSupport, localeZhSupport) {
2638
2672
  const apiTemplate = fs.readFileSync(path.join(templateDir, runtime.files.api), 'utf8');
2639
2673
  const menuSqlTemplate = runtime.files.menuSql ? fs.readFileSync(path.join(templateDir, runtime.files.menuSql), 'utf8') : null;
2640
2674
 
2641
- const viewRoot = buildViewRoot(model);
2642
- const apiFilePath = buildApiFilePath(model);
2643
- const menuRoot = path.join(model.frontendPath, 'menu');
2675
+ const viewRoot = buildViewRoot(model);
2676
+ const apiFilePath = buildApiFilePath(model);
2677
+ const menuRoot = path.join(model.frontendPath, 'menu');
2644
2678
 
2645
2679
  const files = [
2646
2680
  { type: 'form', path: path.join(viewRoot, 'form.vue'), content: renderTemplate(formTemplate, replacements) },
2647
2681
  { type: 'list', path: path.join(viewRoot, 'index.vue'), content: renderTemplate(listTemplate, replacements) },
2648
2682
  { type: 'options', path: path.join(viewRoot, 'options.ts'), content: renderTemplate(optionsTemplate, replacements) },
2649
- { type: 'api', path: apiFilePath, content: renderTemplate(apiTemplate, replacements) },
2683
+ { type: 'api', path: apiFilePath, content: renderTemplate(apiTemplate, replacements) },
2650
2684
  {
2651
2685
  type: 'i18nZh',
2652
2686
  path: localeZhSupport.path,
@@ -2662,18 +2696,18 @@ function renderFiles(model, stylePreset, sharedSupport, localeZhSupport) {
2662
2696
  return files;
2663
2697
  }
2664
2698
 
2665
- function ensureArguments(input) {
2666
- if (!input || typeof input !== 'object') throw new Error('Arguments must be an object');
2667
- rejectSemanticStageInputs(input);
2668
- for (const key of TOOL_SCHEMA.required) {
2669
- if (input[key] === undefined || input[key] === null || input[key] === '') throw new Error(key + ' is required');
2670
- }
2699
+ function ensureArguments(input) {
2700
+ if (!input || typeof input !== 'object') throw new Error('Arguments must be an object');
2701
+ rejectSemanticStageInputs(input);
2702
+ for (const key of TOOL_SCHEMA.required) {
2703
+ if (input[key] === undefined || input[key] === null || input[key] === '') throw new Error(key + ' is required');
2704
+ }
2671
2705
 
2672
- const style = String(input.style);
2673
- const pageType = normalizePageTypeInput(input.pageType);
2674
- getStylePreset(style);
2675
- validatePageTypeAndStyle(pageType, style);
2676
- const isMultiLevelDict = style === 'multi_level_dict';
2706
+ const style = String(input.style);
2707
+ const pageType = normalizePageTypeInput(input.pageType);
2708
+ getStylePreset(style);
2709
+ validatePageTypeAndStyle(pageType, style);
2710
+ const isMultiLevelDict = style === 'multi_level_dict';
2677
2711
  const fields = isMultiLevelDict ? [] : normalizeStructuredFieldArray(input.fields, 'fields');
2678
2712
  const levels = isMultiLevelDict ? normalizeLevelsInput(input.levels) : [];
2679
2713
 
@@ -2685,27 +2719,27 @@ function ensureArguments(input) {
2685
2719
  throw new Error('fields must be a non-empty array');
2686
2720
  }
2687
2721
 
2688
- return {
2689
- featureTitle: input.featureTitle ? String(input.featureTitle) : '',
2690
- tableName: String(input.tableName),
2691
- tableComment: input.tableComment ? String(input.tableComment) : '',
2692
- apiPath: normalizeApiPath(input.apiPath),
2693
- pageType,
2694
- style,
2722
+ return {
2723
+ featureTitle: input.featureTitle ? String(input.featureTitle) : '',
2724
+ tableName: String(input.tableName),
2725
+ tableComment: input.tableComment ? String(input.tableComment) : '',
2726
+ apiPath: normalizeApiPath(input.apiPath),
2727
+ pageType,
2728
+ style,
2695
2729
  fields,
2696
2730
  levels,
2697
- children: normalizeChildrenInput(input.children),
2698
- frontendPath: String(input.frontendPath),
2699
- moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
2700
- targetViewDir: input.targetViewDir ? String(input.targetViewDir) : '',
2701
- targetApiModule: input.targetApiModule ? String(input.targetApiModule) : '',
2702
- targetI18nKey: input.targetI18nKey ? String(input.targetI18nKey) : '',
2703
- writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
2704
- overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
2705
- writeSupportFiles: input.writeSupportFiles === undefined ? true : Boolean(input.writeSupportFiles),
2706
- mergeI18nZh: input.mergeI18nZh === undefined ? true : Boolean(input.mergeI18nZh),
2707
- };
2708
- }
2731
+ children: normalizeChildrenInput(input.children),
2732
+ frontendPath: String(input.frontendPath),
2733
+ moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
2734
+ targetViewDir: input.targetViewDir ? String(input.targetViewDir) : '',
2735
+ targetApiModule: input.targetApiModule ? String(input.targetApiModule) : '',
2736
+ targetI18nKey: input.targetI18nKey ? String(input.targetI18nKey) : '',
2737
+ writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
2738
+ overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
2739
+ writeSupportFiles: input.writeSupportFiles === undefined ? true : Boolean(input.writeSupportFiles),
2740
+ mergeI18nZh: input.mergeI18nZh === undefined ? true : Boolean(input.mergeI18nZh),
2741
+ };
2742
+ }
2709
2743
 
2710
2744
  function maybeWriteFiles(files, writeToDisk, overwrite) {
2711
2745
  if (!writeToDisk) return;
@@ -2728,43 +2762,43 @@ function maybeWriteFiles(files, writeToDisk, overwrite) {
2728
2762
  }
2729
2763
  }
2730
2764
 
2731
- function buildManifest(model, safeArgs, stylePreset, files, note) {
2732
- if (model.style === 'multi_level_dict') {
2733
- return {
2734
- mode: 'local-template',
2735
- style: safeArgs.style,
2736
- pageType: model.pageType || '',
2737
- styleLabel: stylePreset.label,
2765
+ function buildManifest(model, safeArgs, stylePreset, files, note) {
2766
+ if (model.style === 'multi_level_dict') {
2767
+ return {
2768
+ mode: 'local-template',
2769
+ style: safeArgs.style,
2770
+ pageType: model.pageType || '',
2771
+ styleLabel: stylePreset.label,
2738
2772
  runtimeSupported: hasRuntimeSupport(stylePreset),
2739
- tableName: model.tableName,
2740
- tableComment: model.tableComment,
2741
- apiPath: model.apiPath,
2742
- moduleName: model.moduleName,
2743
- targetViewDir: model.targetViewDir,
2744
- targetApiModule: model.targetApiModule,
2745
- targetI18nKey: model.targetI18nKey,
2746
- writeToDisk: safeArgs.writeToDisk,
2747
- sideEffects: {
2748
- writeSupportFiles: safeArgs.writeSupportFiles,
2749
- mergeI18nZh: safeArgs.mergeI18nZh,
2750
- },
2751
- files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
2752
- levels: model.levels.map((level) => ({
2753
- levelIndex: level.levelIndex,
2754
- position: level.position,
2755
- modules: level.modules.map((moduleModel) => ({
2756
- key: moduleModel.key,
2757
- tableName: moduleModel.tableName,
2758
- tableComment: moduleModel.tableComment,
2759
- apiPath: moduleModel.apiPath,
2760
- primaryKey: moduleModel.pk.fieldName,
2761
- queryParentField: moduleModel.queryParentField,
2762
- statusField: moduleModel.statusField,
2763
- statusDictType: moduleModel.statusDictType,
2764
- enableApi: moduleModel.enableApi,
2765
- disableApi: moduleModel.disableApi,
2773
+ tableName: model.tableName,
2774
+ tableComment: model.tableComment,
2775
+ apiPath: buildApiRoutePath(model.moduleName, model.apiPath).replace(/^\/+/, ''),
2776
+ moduleName: model.moduleName,
2777
+ targetViewDir: model.targetViewDir,
2778
+ targetApiModule: model.targetApiModule,
2779
+ targetI18nKey: model.targetI18nKey,
2780
+ writeToDisk: safeArgs.writeToDisk,
2781
+ sideEffects: {
2782
+ writeSupportFiles: safeArgs.writeSupportFiles,
2783
+ mergeI18nZh: safeArgs.mergeI18nZh,
2784
+ },
2785
+ files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
2786
+ levels: model.levels.map((level) => ({
2787
+ levelIndex: level.levelIndex,
2788
+ position: level.position,
2789
+ modules: level.modules.map((moduleModel) => ({
2790
+ key: moduleModel.key,
2791
+ tableName: moduleModel.tableName,
2792
+ tableComment: moduleModel.tableComment,
2793
+ apiPath: buildApiRoutePath(model.moduleName, moduleModel.apiPath).replace(/^\/+/, ''),
2794
+ primaryKey: moduleModel.pk.fieldName,
2795
+ queryParentField: moduleModel.queryParentField,
2796
+ statusField: moduleModel.statusField,
2797
+ statusDictType: moduleModel.statusDictType,
2798
+ enableApi: buildApiRoutePath(model.moduleName, moduleModel.enableApi).replace(/^\/+/, ''),
2799
+ disableApi: buildApiRoutePath(model.moduleName, moduleModel.disableApi).replace(/^\/+/, ''),
2800
+ })),
2766
2801
  })),
2767
- })),
2768
2802
  summary: {
2769
2803
  totalLevels: model.levels.length,
2770
2804
  totalModules: model.modules.length,
@@ -2786,36 +2820,36 @@ function buildManifest(model, safeArgs, stylePreset, files, note) {
2786
2820
  }));
2787
2821
  const selectedList = relations.map((relation) => formatRelationCandidate(relation));
2788
2822
 
2789
- return {
2790
- mode: 'local-template',
2791
- style: safeArgs.style,
2792
- pageType: model.pageType || '',
2793
- styleLabel: stylePreset.label,
2823
+ return {
2824
+ mode: 'local-template',
2825
+ style: safeArgs.style,
2826
+ pageType: model.pageType || '',
2827
+ styleLabel: stylePreset.label,
2794
2828
  runtimeSupported: hasRuntimeSupport(stylePreset),
2795
- tableName: model.tableName,
2796
- tableComment: model.tableComment,
2797
- apiPath: model.apiPath,
2798
- moduleName: model.moduleName,
2799
- targetViewDir: model.targetViewDir,
2800
- targetApiModule: model.targetApiModule,
2801
- targetI18nKey: model.targetI18nKey,
2802
- writeToDisk: safeArgs.writeToDisk,
2803
- sideEffects: {
2804
- writeSupportFiles: safeArgs.writeSupportFiles,
2805
- mergeI18nZh: safeArgs.mergeI18nZh,
2806
- },
2807
- files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
2829
+ tableName: model.tableName,
2830
+ tableComment: model.tableComment,
2831
+ apiPath: buildApiRoutePath(model.moduleName, model.apiPath).replace(/^\/+/, ''),
2832
+ moduleName: model.moduleName,
2833
+ targetViewDir: model.targetViewDir,
2834
+ targetApiModule: model.targetApiModule,
2835
+ targetI18nKey: model.targetI18nKey,
2836
+ writeToDisk: safeArgs.writeToDisk,
2837
+ sideEffects: {
2838
+ writeSupportFiles: safeArgs.writeSupportFiles,
2839
+ mergeI18nZh: safeArgs.mergeI18nZh,
2840
+ },
2841
+ files: files.map((file) => ({ type: file.type, path: file.path, bytes: Buffer.byteLength(file.content, 'utf8'), status: file.status || (safeArgs.writeToDisk ? 'success' : 'rendered') })),
2808
2842
  relation: relations.length === 1 ? relations[0] : null,
2809
2843
  relations,
2810
2844
  relationResolution: model.children.length
2811
2845
  ? {
2812
2846
  status: 'structured_input',
2813
- tableName: safeArgs.tableName,
2814
- source: 'arguments',
2815
- message:
2816
- model.children.length > 1
2817
- ? 'Direct child relations were provided by the caller as structured metadata. MCP generated a single main form with multiple child tables.'
2818
- : 'The relation was provided by the caller as structured metadata. MCP generated files directly.',
2847
+ tableName: safeArgs.tableName,
2848
+ source: 'arguments',
2849
+ message:
2850
+ model.children.length > 1
2851
+ ? 'Direct child relations were provided by the caller as structured metadata. MCP generated a single main form with multiple child tables.'
2852
+ : 'The relation was provided by the caller as structured metadata. MCP generated files directly.',
2819
2853
  selected: selectedList.length === 1 ? selectedList[0] : null,
2820
2854
  selectedList,
2821
2855
  correctionEntry: {
@@ -2851,24 +2885,24 @@ function buildManifest(model, safeArgs, stylePreset, files, note) {
2851
2885
  };
2852
2886
  }
2853
2887
 
2854
- async function handleToolCall(argumentsObject) {
2855
- const safeArgs = ensureArguments(argumentsObject);
2856
- const stylePreset = getStylePreset(safeArgs.style);
2857
- const model = buildModel(safeArgs);
2858
- const sharedSupport = prepareSharedSupport(model.frontendPath, model.dictTypes, safeArgs.writeSupportFiles);
2859
- const localeZhSupport = prepareZhCnLocaleFile(model, safeArgs.mergeI18nZh);
2860
-
2861
- if (!hasRuntimeSupport(stylePreset)) {
2862
- const manifest = buildManifest(model, safeArgs, stylePreset, [], 'Style mapping is declared, but runtime template rendering is not implemented yet for this style.');
2863
- return toolTextResult(JSON.stringify(manifest, null, 2));
2864
- }
2865
-
2866
- const files = renderFiles(model, stylePreset, sharedSupport, localeZhSupport);
2867
- maybeWriteFiles(files, safeArgs.writeToDisk, safeArgs.overwrite);
2868
- maybeWriteSharedSupport(sharedSupport, safeArgs.writeToDisk);
2869
- const manifest = buildManifest(model, safeArgs, stylePreset, files, buildSupportNote(sharedSupport, localeZhSupport));
2870
- return toolTextResult(JSON.stringify(manifest, null, 2));
2871
- }
2888
+ async function handleToolCall(argumentsObject) {
2889
+ const safeArgs = ensureArguments(argumentsObject);
2890
+ const stylePreset = getStylePreset(safeArgs.style);
2891
+ const model = buildModel(safeArgs);
2892
+ const sharedSupport = prepareSharedSupport(model.frontendPath, model.dictTypes, safeArgs.writeSupportFiles);
2893
+ const localeZhSupport = prepareZhCnLocaleFile(model, safeArgs.mergeI18nZh);
2894
+
2895
+ if (!hasRuntimeSupport(stylePreset)) {
2896
+ const manifest = buildManifest(model, safeArgs, stylePreset, [], 'Style mapping is declared, but runtime template rendering is not implemented yet for this style.');
2897
+ return toolTextResult(JSON.stringify(manifest, null, 2));
2898
+ }
2899
+
2900
+ const files = renderFiles(model, stylePreset, sharedSupport, localeZhSupport);
2901
+ maybeWriteFiles(files, safeArgs.writeToDisk, safeArgs.overwrite);
2902
+ maybeWriteSharedSupport(sharedSupport, safeArgs.writeToDisk);
2903
+ const manifest = buildManifest(model, safeArgs, stylePreset, files, buildSupportNote(sharedSupport, localeZhSupport));
2904
+ return toolTextResult(JSON.stringify(manifest, null, 2));
2905
+ }
2872
2906
 
2873
2907
  async function onMessage(message) {
2874
2908
  const { id, method, params } = message;
@@ -2894,11 +2928,11 @@ async function onMessage(message) {
2894
2928
  successResponse(id, {
2895
2929
  tools: [
2896
2930
  {
2897
- name: TOOL_NAME,
2898
- description:
2899
- 'Generate Worsoft frontend files and menu SQL from structured feature metadata. In master_child_jump mode, the caller must provide children[] with explicit payloadField and child field metadata. In multi_level_dict mode, the caller must provide levels[] with explicit hierarchy metadata.',
2900
- inputSchema: TOOL_SCHEMA,
2901
- },
2931
+ name: TOOL_NAME,
2932
+ description:
2933
+ 'Generate Worsoft frontend files and menu SQL from structured feature metadata. In master_child_jump mode, the caller must provide children[] with explicit payloadField and child field metadata. In multi_level_dict mode, the caller must provide levels[] with explicit hierarchy metadata.',
2934
+ inputSchema: TOOL_SCHEMA,
2935
+ },
2902
2936
  ],
2903
2937
  })
2904
2938
  );