worsoft-frontend-codegen-local-mcp 0.1.57 → 0.1.60

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.
@@ -45,3 +45,4 @@ export function putObj(obj?: object) {
45
45
  data: obj,
46
46
  });
47
47
  }
48
+ {{EXTRA_API_FUNCTIONS}}
@@ -47,3 +47,4 @@ export function putObj(obj?: object) {
47
47
  }
48
48
 
49
49
  {{DICT_API_FUNCTIONS}}
50
+ {{EXTRA_API_FUNCTIONS}}
@@ -45,3 +45,4 @@ export function putObj(obj?: object) {
45
45
  data: obj,
46
46
  });
47
47
  }
48
+ {{EXTRA_API_FUNCTIONS}}
package/mcp_server.js CHANGED
@@ -5,7 +5,7 @@ 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.57';
8
+ const SERVER_VERSION = '0.1.60';
9
9
  const PROTOCOL_VERSION = '2024-11-05';
10
10
  const TOOL_NAME = 'worsoft_codegen_local_generate_frontend';
11
11
  const STYLE_CATALOG_PATH = path.join(__dirname, 'assets', 'style-catalog.json');
@@ -312,11 +312,30 @@ const TOOL_SCHEMA = {
312
312
  type: 'string',
313
313
  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.',
314
314
  },
315
- targetI18nKey: {
316
- type: 'string',
317
- 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.',
318
- },
319
- writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
315
+ targetI18nKey: {
316
+ type: 'string',
317
+ 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.',
318
+ },
319
+ extraApis: {
320
+ type: 'array',
321
+ description: 'Additional API methods parsed from the API document and belonging to the current targetApiModule. Each method is rendered to api.ts with a purpose comment for later task-split usage.',
322
+ items: {
323
+ type: 'object',
324
+ properties: {
325
+ functionName: { type: 'string', description: 'Exported frontend API function name, for example getCurrentCountByTradeDictId.' },
326
+ description: { type: 'string', description: 'API purpose comment. It must exactly match the API document interface title text after removing the section number and "接口N:" prefix; do not summarize or rewrite it.' },
327
+ url: { type: 'string', description: 'Request URL. Absolute backend path is preferred, for example /admin/xxx/count.' },
328
+ method: { type: 'string', enum: ['get', 'post', 'put', 'delete'], description: 'HTTP method.' },
329
+ requestType: { type: 'string', enum: ['params', 'data'], description: 'Whether arguments are sent as query params or request body.' },
330
+ targetApiModule: { type: 'string', description: 'Optional target api module. If provided, it must equal the current targetApiModule.' },
331
+ source: { type: 'string', description: 'Source note from API doc or PRD.' },
332
+ usedBy: { type: 'string', description: 'Optional downstream usage hint for task-split.' },
333
+ },
334
+ required: ['functionName', 'description', 'url', 'method'],
335
+ additionalProperties: false,
336
+ },
337
+ },
338
+ writeToDisk: { type: 'boolean', default: true, description: 'Whether to write generated files.' },
320
339
  overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing files. If false, existing files are skipped.' },
321
340
  writeSupportFiles: {
322
341
  type: 'boolean',
@@ -1398,15 +1417,89 @@ function hasRuntimeSupport(stylePreset) {
1398
1417
  return Boolean(stylePreset.runtime && stylePreset.runtime.supported && stylePreset.runtime.templateDir);
1399
1418
  }
1400
1419
 
1401
- function normalizeFields(parsed) {
1402
- return parsed.fields.map((field) => ({ ...field, formType: field.formType || mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1403
- }
1404
-
1405
- function ensureFieldExists(fields, fieldName, tableName, role) {
1406
- const field = fields.find((item) => item.fieldName === fieldName);
1407
- if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
1408
- return field;
1409
- }
1420
+ function normalizeFields(parsed) {
1421
+ return parsed.fields.map((field) => ({ ...field, formType: field.formType || mapFieldType(field), isAudit: isAuditField(field.fieldName) }));
1422
+ }
1423
+
1424
+ function sanitizeIdentifier(value, label) {
1425
+ const name = String(value || '').trim();
1426
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
1427
+ throw new Error(label + ' must be a valid JavaScript identifier');
1428
+ }
1429
+ return name;
1430
+ }
1431
+
1432
+ function normalizeExtraApis(inputExtraApis, currentTargetApiModule) {
1433
+ if (inputExtraApis === undefined || inputExtraApis === null) return [];
1434
+ if (!Array.isArray(inputExtraApis)) {
1435
+ throw new Error('extraApis must be an array');
1436
+ }
1437
+
1438
+ const seen = new Set();
1439
+ return inputExtraApis.map((item, index) => {
1440
+ if (!item || typeof item !== 'object') {
1441
+ throw new Error('extraApis[' + index + '] must be an object');
1442
+ }
1443
+ const functionName = sanitizeIdentifier(item.functionName, 'extraApis[' + index + '].functionName');
1444
+ if (seen.has(functionName)) {
1445
+ throw new Error('Duplicate extra API functionName found: ' + functionName);
1446
+ }
1447
+ seen.add(functionName);
1448
+
1449
+ const method = String(item.method || '').trim().toLowerCase();
1450
+ if (!['get', 'post', 'put', 'delete'].includes(method)) {
1451
+ throw new Error('extraApis[' + index + '].method must be one of get, post, put, delete');
1452
+ }
1453
+ const url = normalizeExtraApiUrl(String(item.url || '').trim(), currentTargetApiModule);
1454
+ if (!url) {
1455
+ throw new Error('extraApis[' + index + '].url is required');
1456
+ }
1457
+ const description = String(item.description || '').trim();
1458
+ if (!description) {
1459
+ throw new Error('extraApis[' + index + '].description is required');
1460
+ }
1461
+ const targetApiModule = item.targetApiModule ? normalizeModuleName(String(item.targetApiModule)) : '';
1462
+ if (targetApiModule && currentTargetApiModule && targetApiModule !== currentTargetApiModule) {
1463
+ throw new Error(
1464
+ 'extraApis[' +
1465
+ index +
1466
+ '].targetApiModule must equal current targetApiModule. Cross-module extra API generation is blocked to avoid overwriting other API files.'
1467
+ );
1468
+ }
1469
+ const requestType = item.requestType ? String(item.requestType).trim() : method === 'get' || method === 'delete' ? 'params' : 'data';
1470
+ if (!['params', 'data'].includes(requestType)) {
1471
+ throw new Error('extraApis[' + index + '].requestType must be params or data');
1472
+ }
1473
+
1474
+ return {
1475
+ functionName,
1476
+ description,
1477
+ url,
1478
+ method,
1479
+ requestType,
1480
+ targetApiModule,
1481
+ source: item.source ? String(item.source).trim() : '',
1482
+ usedBy: item.usedBy ? String(item.usedBy).trim() : '',
1483
+ };
1484
+ });
1485
+ }
1486
+
1487
+ function normalizeExtraApiUrl(rawUrl, currentTargetApiModule) {
1488
+ if (!rawUrl) return '';
1489
+ if (/^https?:\/\//i.test(rawUrl)) return rawUrl;
1490
+ let url = rawUrl.startsWith('/') ? rawUrl : '/' + rawUrl;
1491
+ const moduleRoot = String(currentTargetApiModule || '').split('/')[0] || '';
1492
+ if (moduleRoot && !url.startsWith('/' + moduleRoot + '/')) {
1493
+ url = '/' + moduleRoot + url;
1494
+ }
1495
+ return url;
1496
+ }
1497
+
1498
+ function ensureFieldExists(fields, fieldName, tableName, role) {
1499
+ const field = fields.find((item) => item.fieldName === fieldName);
1500
+ if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
1501
+ return field;
1502
+ }
1410
1503
 
1411
1504
  function detectStatusField(fields, explicitName) {
1412
1505
  if (explicitName) {
@@ -1518,9 +1611,10 @@ function buildMultiLevelDictModel(safeArgs) {
1518
1611
  targetI18nKey: resolvedTargets.targetI18nKey,
1519
1612
  frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1520
1613
  style: safeArgs.style,
1521
- levels: builtLevels,
1522
- modules: allModules,
1523
- dictTypes,
1614
+ levels: builtLevels,
1615
+ modules: allModules,
1616
+ extraApis: normalizeExtraApis(safeArgs.extraApis, resolvedTargets.targetApiModule),
1617
+ dictTypes,
1524
1618
  pk: parentModule.pk,
1525
1619
  fields: parentModule.fields,
1526
1620
  optionFields: parentModule.optionFields,
@@ -1591,11 +1685,11 @@ function buildModel(safeArgs) {
1591
1685
 
1592
1686
  const derivedFunctionName = toCamelCase(safeArgs.tableName);
1593
1687
  const apiPath = safeArgs.apiPath || derivedFunctionName;
1594
- const resolvedTargets = resolveGenerationTargets({
1595
- moduleName: safeArgs.moduleName,
1596
- functionName: derivedFunctionName,
1597
- apiPath,
1598
- targetViewDir: safeArgs.targetViewDir,
1688
+ const resolvedTargets = resolveGenerationTargets({
1689
+ moduleName: safeArgs.moduleName,
1690
+ functionName: derivedFunctionName,
1691
+ apiPath,
1692
+ targetViewDir: safeArgs.targetViewDir,
1599
1693
  targetApiModule: safeArgs.targetApiModule,
1600
1694
  targetI18nKey: safeArgs.targetI18nKey,
1601
1695
  });
@@ -1622,9 +1716,10 @@ function buildModel(safeArgs) {
1622
1716
  dictTypes,
1623
1717
  frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1624
1718
  style: safeArgs.style,
1719
+ extraApis: normalizeExtraApis(safeArgs.extraApis, resolvedTargets.targetApiModule),
1625
1720
  children,
1626
1721
  };
1627
- }
1722
+ }
1628
1723
 
1629
1724
  function renderTemplate(templateText, replacements) {
1630
1725
  let output = templateText;
@@ -2228,9 +2323,10 @@ function renderMultiLevelApiTs(model) {
2228
2323
  "import request from '/@/utils/request';",
2229
2324
  '',
2230
2325
  model.modules.map(renderMultiLevelApiFunctions).join('\n\n'),
2231
- '',
2232
- ].join('\n');
2233
- }
2326
+ renderExtraApiFunctions(model),
2327
+ '',
2328
+ ].join('\n');
2329
+ }
2234
2330
 
2235
2331
  function renderMultiLevelFormField(field) {
2236
2332
  const labelExpr = `getFieldLabel('${field.attrName}')`;
@@ -2968,6 +3064,39 @@ function renderBusinessStatusHelpers(model) {
2968
3064
  ].join('\n');
2969
3065
  }
2970
3066
 
3067
+ function sanitizeComment(value) {
3068
+ return String(value || '').replace(/\*\//g, '* /').replace(/\r?\n/g, ' ').trim();
3069
+ }
3070
+
3071
+ function renderExtraApiFunctions(model) {
3072
+ if (!Array.isArray(model.extraApis) || !model.extraApis.length) return '';
3073
+ return model.extraApis
3074
+ .map((api) => {
3075
+ const requestField = api.requestType === 'data' ? 'data' : 'params';
3076
+ const lines = [
3077
+ '',
3078
+ `// 额外接口:${sanitizeComment(api.description)}`,
3079
+ ];
3080
+ if (api.usedBy) {
3081
+ lines.push(`// 使用场景:${sanitizeComment(api.usedBy)}`);
3082
+ }
3083
+ if (api.source) {
3084
+ lines.push(`// 来源说明:${sanitizeComment(api.source)}`);
3085
+ }
3086
+ lines.push(
3087
+ `export function ${api.functionName}(payload?: any) {`,
3088
+ ' return request({',
3089
+ ` url: '${api.url}',`,
3090
+ ` method: '${api.method}',`,
3091
+ ` ${requestField}: payload,`,
3092
+ ' });',
3093
+ '}'
3094
+ );
3095
+ return lines.join('\n');
3096
+ })
3097
+ .join('\n');
3098
+ }
3099
+
2971
3100
  function buildReplacements(model, sharedSupport) {
2972
3101
  const menuBaseId = Date.now();
2973
3102
  const apiModulePath = model.targetApiModule || `${model.moduleName}/${model.functionName}`;
@@ -2998,6 +3127,7 @@ function buildReplacements(model, sharedSupport) {
2998
3127
  BUSINESS_STATUS_IMPORTS: renderBusinessStatusImports(model),
2999
3128
  BUSINESS_EDIT_IF: hasBusinessBillStateEditControl(model) ? ' v-if="showEditAction(row)"' : '',
3000
3129
  BUSINESS_STATUS_HELPERS: renderBusinessStatusHelpers(model),
3130
+ EXTRA_API_FUNCTIONS: renderExtraApiFunctions(model),
3001
3131
  CUSTOM_QUERY_FIELDS_EXPR: model.pageType === 'dict' ? '[]' : 'queryableDictOptions.value',
3002
3132
  SHOW_RIGHT_TOOLS: model.pageType === 'dict' ? 'false' : 'true',
3003
3133
  MENU_BASE_ID: menuBaseId,
@@ -3097,8 +3227,9 @@ function ensureArguments(input) {
3097
3227
  moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
3098
3228
  targetViewDir: input.targetViewDir ? String(input.targetViewDir) : '',
3099
3229
  targetApiModule: input.targetApiModule ? String(input.targetApiModule) : '',
3100
- targetI18nKey: input.targetI18nKey ? String(input.targetI18nKey) : '',
3101
- writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
3230
+ targetI18nKey: input.targetI18nKey ? String(input.targetI18nKey) : '',
3231
+ extraApis: input.extraApis === undefined ? [] : input.extraApis,
3232
+ writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
3102
3233
  overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
3103
3234
  writeSupportFiles: input.writeSupportFiles === undefined ? true : Boolean(input.writeSupportFiles),
3104
3235
  mergeI18nZh: input.mergeI18nZh === undefined ? true : Boolean(input.mergeI18nZh),
@@ -3163,11 +3294,12 @@ function buildManifest(model, safeArgs, stylePreset, files, note) {
3163
3294
  disableApi: buildApiRoutePath(model.moduleName, moduleModel.disableApi).replace(/^\/+/, ''),
3164
3295
  })),
3165
3296
  })),
3166
- summary: {
3167
- totalLevels: model.levels.length,
3168
- totalModules: model.modules.length,
3169
- dictFields: model.modules.flatMap((moduleModel) => moduleModel.optionFields.filter((field) => field.dictType).map((field) => `${moduleModel.key}.${field.attrName}`)),
3170
- },
3297
+ summary: {
3298
+ totalLevels: model.levels.length,
3299
+ totalModules: model.modules.length,
3300
+ extraApis: model.extraApis.map((api) => ({ functionName: api.functionName, description: api.description, url: api.url, method: api.method })),
3301
+ dictFields: model.modules.flatMap((moduleModel) => moduleModel.optionFields.filter((field) => field.dictType).map((field) => `${moduleModel.key}.${field.attrName}`)),
3302
+ },
3171
3303
  note,
3172
3304
  };
3173
3305
  }
@@ -3228,8 +3360,9 @@ function buildManifest(model, safeArgs, stylePreset, files, note) {
3228
3360
  listVisibleFields: model.listFields.length,
3229
3361
  dictFields: model.optionFields.filter((field) => field.dictType).map((field) => field.attrName),
3230
3362
  skippedAuditFields: model.fields.filter((field) => field.isAudit).map((field) => field.fieldName),
3231
- childCount: model.children.length,
3232
- childTables: model.children.map((childModel) => childModel.tableName),
3363
+ childCount: model.children.length,
3364
+ extraApis: model.extraApis.map((api) => ({ functionName: api.functionName, description: api.description, url: api.url, method: api.method })),
3365
+ childTables: model.children.map((childModel) => childModel.tableName),
3233
3366
  childPayloadFields: model.children.map((childModel) => ({ childTableName: childModel.tableName, payloadField: childModel.payloadField || childModel.listName })),
3234
3367
  childVisibleFields: model.children.reduce((sum, childModel) => sum + childModel.visibleFields.length, 0),
3235
3368
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worsoft-frontend-codegen-local-mcp",
3
- "version": "0.1.57",
3
+ "version": "0.1.60",
4
4
  "description": "Worsoft frontend local-template code generation MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "worsoft <sw@worsoft.vip>",