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

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.59';
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,78 @@ 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 = String(item.url || '').trim();
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: url.startsWith('/') ? url : '/' + 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 ensureFieldExists(fields, fieldName, tableName, role) {
1488
+ const field = fields.find((item) => item.fieldName === fieldName);
1489
+ if (!field) throw new Error(role + ' field "' + fieldName + '" was not found on table ' + tableName);
1490
+ return field;
1491
+ }
1410
1492
 
1411
1493
  function detectStatusField(fields, explicitName) {
1412
1494
  if (explicitName) {
@@ -1518,9 +1600,10 @@ function buildMultiLevelDictModel(safeArgs) {
1518
1600
  targetI18nKey: resolvedTargets.targetI18nKey,
1519
1601
  frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1520
1602
  style: safeArgs.style,
1521
- levels: builtLevels,
1522
- modules: allModules,
1523
- dictTypes,
1603
+ levels: builtLevels,
1604
+ modules: allModules,
1605
+ extraApis: normalizeExtraApis(safeArgs.extraApis, resolvedTargets.targetApiModule),
1606
+ dictTypes,
1524
1607
  pk: parentModule.pk,
1525
1608
  fields: parentModule.fields,
1526
1609
  optionFields: parentModule.optionFields,
@@ -1591,11 +1674,11 @@ function buildModel(safeArgs) {
1591
1674
 
1592
1675
  const derivedFunctionName = toCamelCase(safeArgs.tableName);
1593
1676
  const apiPath = safeArgs.apiPath || derivedFunctionName;
1594
- const resolvedTargets = resolveGenerationTargets({
1595
- moduleName: safeArgs.moduleName,
1596
- functionName: derivedFunctionName,
1597
- apiPath,
1598
- targetViewDir: safeArgs.targetViewDir,
1677
+ const resolvedTargets = resolveGenerationTargets({
1678
+ moduleName: safeArgs.moduleName,
1679
+ functionName: derivedFunctionName,
1680
+ apiPath,
1681
+ targetViewDir: safeArgs.targetViewDir,
1599
1682
  targetApiModule: safeArgs.targetApiModule,
1600
1683
  targetI18nKey: safeArgs.targetI18nKey,
1601
1684
  });
@@ -1622,9 +1705,10 @@ function buildModel(safeArgs) {
1622
1705
  dictTypes,
1623
1706
  frontendPath: normalizeFrontendRootPath(safeArgs.frontendPath),
1624
1707
  style: safeArgs.style,
1708
+ extraApis: normalizeExtraApis(safeArgs.extraApis, resolvedTargets.targetApiModule),
1625
1709
  children,
1626
1710
  };
1627
- }
1711
+ }
1628
1712
 
1629
1713
  function renderTemplate(templateText, replacements) {
1630
1714
  let output = templateText;
@@ -2228,9 +2312,10 @@ function renderMultiLevelApiTs(model) {
2228
2312
  "import request from '/@/utils/request';",
2229
2313
  '',
2230
2314
  model.modules.map(renderMultiLevelApiFunctions).join('\n\n'),
2231
- '',
2232
- ].join('\n');
2233
- }
2315
+ renderExtraApiFunctions(model),
2316
+ '',
2317
+ ].join('\n');
2318
+ }
2234
2319
 
2235
2320
  function renderMultiLevelFormField(field) {
2236
2321
  const labelExpr = `getFieldLabel('${field.attrName}')`;
@@ -2968,6 +3053,39 @@ function renderBusinessStatusHelpers(model) {
2968
3053
  ].join('\n');
2969
3054
  }
2970
3055
 
3056
+ function sanitizeComment(value) {
3057
+ return String(value || '').replace(/\*\//g, '* /').replace(/\r?\n/g, ' ').trim();
3058
+ }
3059
+
3060
+ function renderExtraApiFunctions(model) {
3061
+ if (!Array.isArray(model.extraApis) || !model.extraApis.length) return '';
3062
+ return model.extraApis
3063
+ .map((api) => {
3064
+ const requestField = api.requestType === 'data' ? 'data' : 'params';
3065
+ const lines = [
3066
+ '',
3067
+ `// 额外接口:${sanitizeComment(api.description)}`,
3068
+ ];
3069
+ if (api.usedBy) {
3070
+ lines.push(`// 使用场景:${sanitizeComment(api.usedBy)}`);
3071
+ }
3072
+ if (api.source) {
3073
+ lines.push(`// 来源说明:${sanitizeComment(api.source)}`);
3074
+ }
3075
+ lines.push(
3076
+ `export function ${api.functionName}(payload?: any) {`,
3077
+ ' return request({',
3078
+ ` url: '${api.url}',`,
3079
+ ` method: '${api.method}',`,
3080
+ ` ${requestField}: payload,`,
3081
+ ' });',
3082
+ '}'
3083
+ );
3084
+ return lines.join('\n');
3085
+ })
3086
+ .join('\n');
3087
+ }
3088
+
2971
3089
  function buildReplacements(model, sharedSupport) {
2972
3090
  const menuBaseId = Date.now();
2973
3091
  const apiModulePath = model.targetApiModule || `${model.moduleName}/${model.functionName}`;
@@ -2998,6 +3116,7 @@ function buildReplacements(model, sharedSupport) {
2998
3116
  BUSINESS_STATUS_IMPORTS: renderBusinessStatusImports(model),
2999
3117
  BUSINESS_EDIT_IF: hasBusinessBillStateEditControl(model) ? ' v-if="showEditAction(row)"' : '',
3000
3118
  BUSINESS_STATUS_HELPERS: renderBusinessStatusHelpers(model),
3119
+ EXTRA_API_FUNCTIONS: renderExtraApiFunctions(model),
3001
3120
  CUSTOM_QUERY_FIELDS_EXPR: model.pageType === 'dict' ? '[]' : 'queryableDictOptions.value',
3002
3121
  SHOW_RIGHT_TOOLS: model.pageType === 'dict' ? 'false' : 'true',
3003
3122
  MENU_BASE_ID: menuBaseId,
@@ -3097,8 +3216,9 @@ function ensureArguments(input) {
3097
3216
  moduleName: input.moduleName ? String(input.moduleName) : 'admin/test',
3098
3217
  targetViewDir: input.targetViewDir ? String(input.targetViewDir) : '',
3099
3218
  targetApiModule: input.targetApiModule ? String(input.targetApiModule) : '',
3100
- targetI18nKey: input.targetI18nKey ? String(input.targetI18nKey) : '',
3101
- writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
3219
+ targetI18nKey: input.targetI18nKey ? String(input.targetI18nKey) : '',
3220
+ extraApis: input.extraApis === undefined ? [] : input.extraApis,
3221
+ writeToDisk: input.writeToDisk === undefined ? true : Boolean(input.writeToDisk),
3102
3222
  overwrite: input.overwrite === undefined ? true : Boolean(input.overwrite),
3103
3223
  writeSupportFiles: input.writeSupportFiles === undefined ? true : Boolean(input.writeSupportFiles),
3104
3224
  mergeI18nZh: input.mergeI18nZh === undefined ? true : Boolean(input.mergeI18nZh),
@@ -3163,11 +3283,12 @@ function buildManifest(model, safeArgs, stylePreset, files, note) {
3163
3283
  disableApi: buildApiRoutePath(model.moduleName, moduleModel.disableApi).replace(/^\/+/, ''),
3164
3284
  })),
3165
3285
  })),
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
- },
3286
+ summary: {
3287
+ totalLevels: model.levels.length,
3288
+ totalModules: model.modules.length,
3289
+ extraApis: model.extraApis.map((api) => ({ functionName: api.functionName, description: api.description, url: api.url, method: api.method })),
3290
+ dictFields: model.modules.flatMap((moduleModel) => moduleModel.optionFields.filter((field) => field.dictType).map((field) => `${moduleModel.key}.${field.attrName}`)),
3291
+ },
3171
3292
  note,
3172
3293
  };
3173
3294
  }
@@ -3228,8 +3349,9 @@ function buildManifest(model, safeArgs, stylePreset, files, note) {
3228
3349
  listVisibleFields: model.listFields.length,
3229
3350
  dictFields: model.optionFields.filter((field) => field.dictType).map((field) => field.attrName),
3230
3351
  skippedAuditFields: model.fields.filter((field) => field.isAudit).map((field) => field.fieldName),
3231
- childCount: model.children.length,
3232
- childTables: model.children.map((childModel) => childModel.tableName),
3352
+ childCount: model.children.length,
3353
+ extraApis: model.extraApis.map((api) => ({ functionName: api.functionName, description: api.description, url: api.url, method: api.method })),
3354
+ childTables: model.children.map((childModel) => childModel.tableName),
3233
3355
  childPayloadFields: model.children.map((childModel) => ({ childTableName: childModel.tableName, payloadField: childModel.payloadField || childModel.listName })),
3234
3356
  childVisibleFields: model.children.reduce((sum, childModel) => sum + childModel.visibleFields.length, 0),
3235
3357
  },
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.59",
4
4
  "description": "Worsoft frontend local-template code generation MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "worsoft <sw@worsoft.vip>",