scaffold-engine 0.1.0
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/README.md +117 -0
- package/engine.project.example.json +23 -0
- package/package.json +49 -0
- package/scripts/postinstall.cjs +42 -0
- package/specs/catalogs/action-templates.yaml +189 -0
- package/specs/catalogs/child-templates.yaml +54 -0
- package/specs/catalogs/field-fragments.yaml +203 -0
- package/specs/catalogs/object-catalog.yaml +35 -0
- package/specs/catalogs/object-name-suggestions.yaml +30 -0
- package/specs/catalogs/object-templates.yaml +45 -0
- package/specs/catalogs/pattern-catalog.yaml +48 -0
- package/specs/catalogs/status-templates.yaml +16 -0
- package/specs/projects/crm-pilot/customer.yaml +122 -0
- package/specs/projects/crm-pilot/lead.from-nl.yaml +76 -0
- package/specs/projects/crm-pilot/lead.yaml +82 -0
- package/specs/projects/generated-from-nl/crm-customer.yaml +158 -0
- package/specs/projects/generated-from-nl/crm-lead.yaml +76 -0
- package/specs/projects/generated-from-nl/crm-opportunity.yaml +78 -0
- package/specs/projects/generated-from-nl/crm-quote.yaml +78 -0
- package/specs/projects/generated-from-nl/custom-documentLines.yaml +125 -0
- package/specs/projects/generated-from-nl/custom-treeEntity.yaml +78 -0
- package/specs/projects/generated-from-nl/erp-material-pattern-test.yaml +79 -0
- package/specs/projects/generated-from-nl/erp-material.yaml +78 -0
- package/specs/projects/generated-from-nl/hr-orgUnit.yaml +100 -0
- package/specs/projects/pattern-examples/document-lines-demo.yaml +125 -0
- package/specs/projects/pattern-examples/tree-entity-demo.yaml +79 -0
- package/specs/rules/business-model.schema.json +262 -0
- package/specs/rules/extension-boundaries.json +26 -0
- package/specs/rules/requirement-draft.schema.json +75 -0
- package/specs/rules/spec-governance.json +29 -0
- package/specs/templates/crm/customer.template.yaml +121 -0
- package/specs/templates/crm/lead.template.yaml +82 -0
- package/tools/analyze-requirement.cjs +950 -0
- package/tools/cli.cjs +59 -0
- package/tools/create-draft.cjs +18 -0
- package/tools/engine.cjs +47 -0
- package/tools/generate-draft.cjs +33 -0
- package/tools/generate-module.cjs +1218 -0
- package/tools/init-project.cjs +194 -0
- package/tools/lib/draft-toolkit.cjs +357 -0
- package/tools/lib/model-toolkit.cjs +482 -0
- package/tools/lib/pattern-renderers.cjs +166 -0
- package/tools/lib/renderers/detail-page-renderer.cjs +327 -0
- package/tools/lib/renderers/form-page-renderer.cjs +553 -0
- package/tools/lib/renderers/list-page-renderer.cjs +371 -0
- package/tools/lib/runtime-config.cjs +154 -0
- package/tools/patch-draft.cjs +57 -0
- package/tools/prompts/business-model-prompt.md +58 -0
- package/tools/run-requirement.cjs +672 -0
- package/tools/validate-draft.cjs +32 -0
- package/tools/validate-model.cjs +140 -0
- package/tools/verify-patterns.cjs +67 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const YAML = require('yaml');
|
|
6
|
+
const {
|
|
7
|
+
loadRuntimeConfig,
|
|
8
|
+
readJson,
|
|
9
|
+
resolveScaffoldPath,
|
|
10
|
+
snakeCase,
|
|
11
|
+
validateModelFile,
|
|
12
|
+
} = require('./lib/model-toolkit.cjs');
|
|
13
|
+
const { resolveCatalogPath, resolveRulePath } = require('./lib/runtime-config.cjs');
|
|
14
|
+
|
|
15
|
+
if (require.main === module) {
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const options = parseArgs(args);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const requirementText = readRequirementText(options);
|
|
21
|
+
const result = analyzeRequirement(requirementText, { configPath: options.configPath });
|
|
22
|
+
|
|
23
|
+
if (options.writeSpec && result.suggestedModel) {
|
|
24
|
+
const specPath = writeSuggestedSpec(result.suggestedModel, options.writeSpec);
|
|
25
|
+
result.generatedSpecPath = specPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.format === 'json') {
|
|
29
|
+
console.log(JSON.stringify(result, null, 2));
|
|
30
|
+
} else {
|
|
31
|
+
console.log(renderMarkdown(result));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.decision === 'not_extendable') {
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
if (result.decision === 'need_more_info') {
|
|
38
|
+
process.exit(3);
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`\n❌ 需求分析失败: ${error.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
const options = {
|
|
48
|
+
file: '',
|
|
49
|
+
format: 'markdown',
|
|
50
|
+
writeSpec: '',
|
|
51
|
+
configPath: '',
|
|
52
|
+
};
|
|
53
|
+
const textParts = [];
|
|
54
|
+
|
|
55
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
56
|
+
const item = argv[index];
|
|
57
|
+
if (item === '--file') {
|
|
58
|
+
options.file = argv[index + 1] || '';
|
|
59
|
+
index += 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (item === '--format') {
|
|
63
|
+
options.format = argv[index + 1] || 'markdown';
|
|
64
|
+
index += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (item === '--write-spec') {
|
|
68
|
+
options.writeSpec = argv[index + 1] || '';
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (item === '--config') {
|
|
73
|
+
options.configPath = argv[index + 1] || '';
|
|
74
|
+
index += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
textParts.push(item);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
options.text = textParts.join(' ').trim();
|
|
81
|
+
return options;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readRequirementText(options) {
|
|
85
|
+
if (options.file) {
|
|
86
|
+
return fs.readFileSync(path.resolve(options.file), 'utf8').trim();
|
|
87
|
+
}
|
|
88
|
+
if (options.text) {
|
|
89
|
+
return options.text;
|
|
90
|
+
}
|
|
91
|
+
throw new Error('请提供需求文本,例如: node ./tools/analyze-requirement.cjs "做一个线索管理,支持新建、编辑、列表、详情"');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function analyzeRequirement(requirementText, options = {}) {
|
|
95
|
+
const runtimeConfig = loadRuntimeConfig(options);
|
|
96
|
+
const boundaries = readJson(resolveRulePath(runtimeConfig, 'extensionBoundaries'));
|
|
97
|
+
const resources = loadCatalogResources(runtimeConfig);
|
|
98
|
+
const { objects: objectCatalog, patterns: patternCatalog } = resources;
|
|
99
|
+
const matchedObject = detectObject(requirementText, objectCatalog);
|
|
100
|
+
const matchedPattern = detectPattern(requirementText, patternCatalog);
|
|
101
|
+
const suggestedName = !matchedObject && matchedPattern
|
|
102
|
+
? detectSuggestedObjectName(requirementText, resources.nameSuggestions, matchedPattern.id)
|
|
103
|
+
: null;
|
|
104
|
+
const requestedActions = detectActions(requirementText);
|
|
105
|
+
const missingInfo = [];
|
|
106
|
+
const reasons = [];
|
|
107
|
+
|
|
108
|
+
if (!matchedObject) {
|
|
109
|
+
const seedModel = matchedPattern ? buildPatternSeedModel(matchedPattern, requestedActions, resources, suggestedName) : null;
|
|
110
|
+
return finalizeAnalysisResult({
|
|
111
|
+
decision: 'need_more_info',
|
|
112
|
+
nextAction: 'clarify',
|
|
113
|
+
requirement: requirementText,
|
|
114
|
+
reason: [
|
|
115
|
+
matchedPattern
|
|
116
|
+
? suggestedName
|
|
117
|
+
? `已识别出结构模式 ${matchedPattern.label}(${matchedPattern.id}),并推断出候选对象 ${suggestedName.label}(${suggestedName.domain}/${suggestedName.object})`
|
|
118
|
+
: `已识别出结构模式 ${matchedPattern.label}(${matchedPattern.id}),但还缺少稳定的业务对象`
|
|
119
|
+
: '未识别出稳定的业务对象,当前工具需要至少明确一个对象名,例如“客户”“线索”“商机”',
|
|
120
|
+
],
|
|
121
|
+
missingInfo: matchedPattern
|
|
122
|
+
? suggestedName
|
|
123
|
+
? ['对象的最小字段集']
|
|
124
|
+
: ['业务对象名称', '对象的最小字段集']
|
|
125
|
+
: ['业务对象名称', '对象的最小字段集'],
|
|
126
|
+
inferred: {
|
|
127
|
+
domain: suggestedName ? suggestedName.domain : undefined,
|
|
128
|
+
object: suggestedName ? suggestedName.object : undefined,
|
|
129
|
+
label: suggestedName ? suggestedName.label : undefined,
|
|
130
|
+
pattern: matchedPattern ? matchedPattern.id : undefined,
|
|
131
|
+
actions: requestedActions,
|
|
132
|
+
},
|
|
133
|
+
suggestedModel: seedModel || undefined,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const domain = matchedObject.domain;
|
|
138
|
+
const object = matchedObject.object;
|
|
139
|
+
const label = matchedObject.label;
|
|
140
|
+
const pattern = matchedObject.patternRef || '';
|
|
141
|
+
const table = `${snakeCase(domain)}_${snakeCase(object)}`;
|
|
142
|
+
const actions = requestedActions.length > 0
|
|
143
|
+
? requestedActions
|
|
144
|
+
: Array.isArray(matchedObject.defaultActions) ? matchedObject.defaultActions : [];
|
|
145
|
+
const inferredAppTemplate = inferAppTemplateByActions(actions, resources.actionTemplates);
|
|
146
|
+
const statusFlow = alignStatusFlowActions(
|
|
147
|
+
matchedObject.defaultStatusFlow || inferredAppTemplate.statusFlow,
|
|
148
|
+
actions
|
|
149
|
+
);
|
|
150
|
+
const responseContract = matchedObject.defaultResponseContract || inferredAppTemplate.responseContract;
|
|
151
|
+
const permissionProfile = matchedObject.defaultPermissionProfile || inferredAppTemplate.permissionProfile;
|
|
152
|
+
const validationRules = Array.isArray(matchedObject.defaultValidationRules) && matchedObject.defaultValidationRules.length > 0
|
|
153
|
+
? matchedObject.defaultValidationRules
|
|
154
|
+
: inferredAppTemplate.validationRules;
|
|
155
|
+
const errorCodes = Array.isArray(matchedObject.defaultErrorCodes) && matchedObject.defaultErrorCodes.length > 0
|
|
156
|
+
? matchedObject.defaultErrorCodes
|
|
157
|
+
: inferredAppTemplate.errorCodes;
|
|
158
|
+
|
|
159
|
+
reasons.push(`识别对象为 ${label}(${domain}/${object})`);
|
|
160
|
+
if (matchedPattern && matchedPattern.id === pattern) {
|
|
161
|
+
reasons.push(`结构模式识别为 ${matchedPattern.label}(${matchedPattern.id}),与对象模板一致`);
|
|
162
|
+
} else if (matchedPattern && matchedPattern.id !== pattern) {
|
|
163
|
+
reasons.push(`结构模式初步识别为 ${matchedPattern.label}(${matchedPattern.id}),最终回退采用对象模板模式 ${pattern}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (requestedActions.length === 0 && actions.length > 0) {
|
|
167
|
+
reasons.push(`未显式识别动作,已回退使用对象模板默认动作: ${actions.join(', ')}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (actions.length === 0) {
|
|
171
|
+
missingInfo.push('至少一个业务动作,例如新建、编辑、列表、详情');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ((boundaries.protectedDomains || []).map(toLower).includes(toLower(domain))) {
|
|
175
|
+
return finalizeAnalysisResult({
|
|
176
|
+
decision: 'not_extendable',
|
|
177
|
+
nextAction: 'reject',
|
|
178
|
+
requirement: requirementText,
|
|
179
|
+
reason: [`domain "${domain}" 属于受保护域,当前框架不允许把业务扩展直接落到系统域上`],
|
|
180
|
+
missingInfo: [],
|
|
181
|
+
inferred: {
|
|
182
|
+
domain,
|
|
183
|
+
object,
|
|
184
|
+
label,
|
|
185
|
+
pattern,
|
|
186
|
+
actions,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ((boundaries.protectedMetaObjects || []).map(toLower).includes(toLower(object))) {
|
|
192
|
+
return finalizeAnalysisResult({
|
|
193
|
+
decision: 'not_extendable',
|
|
194
|
+
nextAction: 'reject',
|
|
195
|
+
requirement: requirementText,
|
|
196
|
+
reason: [`object "${object}" 属于受保护对象,当前框架不允许直接覆盖系统对象`],
|
|
197
|
+
missingInfo: [],
|
|
198
|
+
inferred: {
|
|
199
|
+
domain,
|
|
200
|
+
object,
|
|
201
|
+
label,
|
|
202
|
+
actions,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const model = buildSuggestedModel({
|
|
208
|
+
domain,
|
|
209
|
+
object,
|
|
210
|
+
label,
|
|
211
|
+
pattern,
|
|
212
|
+
table,
|
|
213
|
+
fields: matchedObject.fields,
|
|
214
|
+
actions,
|
|
215
|
+
statusFlow,
|
|
216
|
+
responseContract,
|
|
217
|
+
permissionProfile,
|
|
218
|
+
validationRules,
|
|
219
|
+
errorCodes,
|
|
220
|
+
detailSections: matchedObject.detailSections,
|
|
221
|
+
children: matchedObject.children,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const validation = validateInlineModel(model);
|
|
225
|
+
if (validation.boundaryErrors.length > 0) {
|
|
226
|
+
return finalizeAnalysisResult({
|
|
227
|
+
decision: 'not_extendable',
|
|
228
|
+
nextAction: 'reject',
|
|
229
|
+
requirement: requirementText,
|
|
230
|
+
reason: validation.boundaryErrors,
|
|
231
|
+
missingInfo,
|
|
232
|
+
inferred: {
|
|
233
|
+
domain,
|
|
234
|
+
object,
|
|
235
|
+
label,
|
|
236
|
+
pattern,
|
|
237
|
+
actions,
|
|
238
|
+
},
|
|
239
|
+
suggestedModel: model,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (validation.schemaErrors.length > 0) {
|
|
244
|
+
return finalizeAnalysisResult({
|
|
245
|
+
decision: 'need_more_info',
|
|
246
|
+
nextAction: 'clarify',
|
|
247
|
+
requirement: requirementText,
|
|
248
|
+
reason: validation.schemaErrors,
|
|
249
|
+
missingInfo,
|
|
250
|
+
inferred: {
|
|
251
|
+
domain,
|
|
252
|
+
object,
|
|
253
|
+
label,
|
|
254
|
+
pattern,
|
|
255
|
+
actions,
|
|
256
|
+
},
|
|
257
|
+
suggestedModel: model,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (missingInfo.length > 0) {
|
|
262
|
+
return finalizeAnalysisResult({
|
|
263
|
+
decision: 'need_more_info',
|
|
264
|
+
nextAction: 'clarify',
|
|
265
|
+
requirement: requirementText,
|
|
266
|
+
reason: reasons,
|
|
267
|
+
missingInfo,
|
|
268
|
+
inferred: {
|
|
269
|
+
domain,
|
|
270
|
+
object,
|
|
271
|
+
label,
|
|
272
|
+
pattern,
|
|
273
|
+
actions,
|
|
274
|
+
},
|
|
275
|
+
suggestedModel: model,
|
|
276
|
+
warnings: validation.warnings,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
reasons.push('已能映射为当前 scaffold 业务模型规范');
|
|
281
|
+
reasons.push('未触碰系统保留 domain/object/table 边界');
|
|
282
|
+
|
|
283
|
+
return finalizeAnalysisResult({
|
|
284
|
+
decision: 'extendable',
|
|
285
|
+
nextAction: 'generate',
|
|
286
|
+
requirement: requirementText,
|
|
287
|
+
reason: reasons,
|
|
288
|
+
missingInfo: [],
|
|
289
|
+
inferred: {
|
|
290
|
+
domain,
|
|
291
|
+
object,
|
|
292
|
+
label,
|
|
293
|
+
pattern,
|
|
294
|
+
actions,
|
|
295
|
+
},
|
|
296
|
+
suggestedModel: model,
|
|
297
|
+
warnings: validation.warnings,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildAnalysisMeta(decision, nextAction, requirement) {
|
|
302
|
+
return {
|
|
303
|
+
contractVersion: '1.0.0',
|
|
304
|
+
stage: 'analyze',
|
|
305
|
+
status: decision === 'extendable' ? 'passed' : decision === 'need_more_info' ? 'incomplete' : 'failed',
|
|
306
|
+
nextAction,
|
|
307
|
+
requirement,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function finalizeAnalysisResult(result) {
|
|
312
|
+
const inferred = result.inferred || {};
|
|
313
|
+
const suggestedModel = result.suggestedModel;
|
|
314
|
+
return {
|
|
315
|
+
...buildAnalysisMeta(result.decision, result.nextAction, result.requirement),
|
|
316
|
+
...result,
|
|
317
|
+
candidate: {
|
|
318
|
+
domain: inferred.domain || '',
|
|
319
|
+
object: inferred.object || '',
|
|
320
|
+
label: inferred.label || '',
|
|
321
|
+
pattern: inferred.pattern || '',
|
|
322
|
+
},
|
|
323
|
+
draftSummary: suggestedModel ? buildDraftSummary(suggestedModel) : undefined,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildDraftSummary(model) {
|
|
328
|
+
const fields = Array.isArray(model?.data?.fields) ? model.data.fields : [];
|
|
329
|
+
const children = Array.isArray(model?.data?.children) ? model.data.children : [];
|
|
330
|
+
const actions = Array.isArray(model?.app?.actions) ? model.app.actions : [];
|
|
331
|
+
return {
|
|
332
|
+
rootFieldCount: fields.length,
|
|
333
|
+
childTableCount: children.length,
|
|
334
|
+
actionCount: actions.length,
|
|
335
|
+
hasStatusFlow: Boolean(model?.app?.statusFlow),
|
|
336
|
+
hasPermissionProfile: Boolean(model?.app?.permissionProfile),
|
|
337
|
+
hasValidationRules: Array.isArray(model?.app?.validationRules) && model.app.validationRules.length > 0,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function loadCatalogResources(runtimeConfig) {
|
|
342
|
+
const catalogPath = resolveCatalogPath(runtimeConfig, 'objectCatalog');
|
|
343
|
+
const patternPath = resolveCatalogPath(runtimeConfig, 'patternCatalog');
|
|
344
|
+
const fragmentPath = resolveCatalogPath(runtimeConfig, 'fieldFragments');
|
|
345
|
+
const templatePath = resolveCatalogPath(runtimeConfig, 'objectTemplates');
|
|
346
|
+
const suggestionPath = resolveCatalogPath(runtimeConfig, 'objectNameSuggestions');
|
|
347
|
+
const actionTemplatePath = resolveCatalogPath(runtimeConfig, 'actionTemplates');
|
|
348
|
+
const statusTemplatePath = resolveCatalogPath(runtimeConfig, 'statusTemplates');
|
|
349
|
+
const childTemplatePath = resolveCatalogPath(runtimeConfig, 'childTemplates');
|
|
350
|
+
const patternDoc = YAML.parse(fs.readFileSync(patternPath, 'utf8')) || {};
|
|
351
|
+
const catalog = YAML.parse(fs.readFileSync(catalogPath, 'utf8')) || {};
|
|
352
|
+
const fragmentDoc = YAML.parse(fs.readFileSync(fragmentPath, 'utf8')) || {};
|
|
353
|
+
const templateDoc = YAML.parse(fs.readFileSync(templatePath, 'utf8')) || {};
|
|
354
|
+
const suggestionDoc = YAML.parse(fs.readFileSync(suggestionPath, 'utf8')) || {};
|
|
355
|
+
const actionTemplateDoc = YAML.parse(fs.readFileSync(actionTemplatePath, 'utf8')) || {};
|
|
356
|
+
const statusTemplateDoc = YAML.parse(fs.readFileSync(statusTemplatePath, 'utf8')) || {};
|
|
357
|
+
const childTemplateDoc = YAML.parse(fs.readFileSync(childTemplatePath, 'utf8')) || {};
|
|
358
|
+
const patterns = patternDoc.patterns || {};
|
|
359
|
+
const fragments = fragmentDoc.fragments || {};
|
|
360
|
+
const templates = templateDoc.templates || {};
|
|
361
|
+
const suggestions = Array.isArray(suggestionDoc.suggestions) ? suggestionDoc.suggestions : [];
|
|
362
|
+
const actionTemplates = actionTemplateDoc.templates || {};
|
|
363
|
+
const statusTemplates = statusTemplateDoc.templates || {};
|
|
364
|
+
const childTemplates = childTemplateDoc.templates || {};
|
|
365
|
+
const objects = Array.isArray(catalog.objects) ? catalog.objects : [];
|
|
366
|
+
return {
|
|
367
|
+
patterns: Object.entries(patterns).map(([id, item]) => ({
|
|
368
|
+
id,
|
|
369
|
+
label: item.label || id,
|
|
370
|
+
keywords: Array.isArray(item.detectKeywords) ? item.detectKeywords : [],
|
|
371
|
+
defaultUi: item.defaultUi || {},
|
|
372
|
+
defaultActionTemplateRef: item.defaultActionTemplateRef || '',
|
|
373
|
+
defaultChildTemplateRefs: Array.isArray(item.defaultChildTemplateRefs) ? item.defaultChildTemplateRefs : [],
|
|
374
|
+
seedFieldRefs: Array.isArray(item.seedFieldRefs) ? item.seedFieldRefs : [],
|
|
375
|
+
seedChildTemplateRefs: Array.isArray(item.seedChildTemplateRefs) ? item.seedChildTemplateRefs : [],
|
|
376
|
+
})),
|
|
377
|
+
objects: objects.map((item) => resolveCatalogObject(item, patterns, templates, fragments, actionTemplates, statusTemplates, childTemplates)),
|
|
378
|
+
nameSuggestions: suggestions.map((item) => ({
|
|
379
|
+
keywords: Array.isArray(item.keywords) ? item.keywords : [],
|
|
380
|
+
patternRef: item.patternRef || '',
|
|
381
|
+
domain: item.domain || 'custom',
|
|
382
|
+
object: item.object || '',
|
|
383
|
+
label: item.label || '',
|
|
384
|
+
})),
|
|
385
|
+
fragments,
|
|
386
|
+
actionTemplates,
|
|
387
|
+
statusTemplates,
|
|
388
|
+
childTemplates,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function resolveCatalogObject(item, patterns, templates, fragments, actionTemplates, statusTemplates, childTemplates) {
|
|
393
|
+
const template = item.objectTemplateRef ? templates[item.objectTemplateRef] : null;
|
|
394
|
+
if (item.objectTemplateRef && !template) {
|
|
395
|
+
throw new Error(`对象词典 ${item.domain}/${item.object} 引用了不存在的对象模板: ${item.objectTemplateRef}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const patternRef = item.patternRef || template?.patternRef || '';
|
|
399
|
+
const pattern = patternRef ? patterns[patternRef] : null;
|
|
400
|
+
if (patternRef && !pattern) {
|
|
401
|
+
throw new Error(`对象词典 ${item.domain}/${item.object} 引用了不存在的模式模板: ${patternRef}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const merged = mergeCatalogPattern(pattern || {}, template || {}, item, patternRef);
|
|
405
|
+
const appTemplate = resolveDefaultAppTemplate(merged, actionTemplates);
|
|
406
|
+
const children = expandChildTemplates(merged, childTemplates);
|
|
407
|
+
return {
|
|
408
|
+
...merged,
|
|
409
|
+
...appTemplate,
|
|
410
|
+
children,
|
|
411
|
+
fields: expandCatalogFields(merged, fragments, statusTemplates),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function mergeCatalogPattern(pattern, template, item, patternRef) {
|
|
416
|
+
const patternUi = pattern.defaultUi || {};
|
|
417
|
+
return {
|
|
418
|
+
...pattern,
|
|
419
|
+
...template,
|
|
420
|
+
...item,
|
|
421
|
+
patternRef,
|
|
422
|
+
detailSections: item.detailSections || template.detailSections || patternUi.detailSections || ['basicInfo'],
|
|
423
|
+
actionTemplateRef: item.actionTemplateRef || template.actionTemplateRef || pattern.defaultActionTemplateRef || '',
|
|
424
|
+
fieldRefs: uniqueList([...(template.fieldRefs || []), ...(item.fieldRefs || [])]),
|
|
425
|
+
fields: [...(template.fields || []), ...(item.fields || [])],
|
|
426
|
+
childTemplateRefs: uniqueList([
|
|
427
|
+
...(pattern.defaultChildTemplateRefs || []),
|
|
428
|
+
...(template.childTemplateRefs || []),
|
|
429
|
+
...(item.childTemplateRefs || []),
|
|
430
|
+
]),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function expandChildTemplates(item, childTemplates) {
|
|
435
|
+
const resolved = [];
|
|
436
|
+
for (const ref of item.childTemplateRefs || []) {
|
|
437
|
+
const template = childTemplates[ref];
|
|
438
|
+
if (!template) {
|
|
439
|
+
throw new Error(`对象词典 ${item.domain}/${item.object} 引用了不存在的子表模板: ${ref}`);
|
|
440
|
+
}
|
|
441
|
+
resolved.push(cloneField(template));
|
|
442
|
+
}
|
|
443
|
+
for (const child of item.children || []) {
|
|
444
|
+
resolved.push(cloneField(child));
|
|
445
|
+
}
|
|
446
|
+
return resolved;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function expandCatalogFields(item, fragments, statusTemplates) {
|
|
450
|
+
const resolved = [];
|
|
451
|
+
|
|
452
|
+
for (const ref of item.fieldRefs || []) {
|
|
453
|
+
const field = fragments[ref];
|
|
454
|
+
if (!field) {
|
|
455
|
+
throw new Error(`对象词典 ${item.domain}/${item.object} 引用了不存在的字段片段: ${ref}`);
|
|
456
|
+
}
|
|
457
|
+
resolved.push(resolveFieldFragment(field, statusTemplates));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (const field of item.fields || []) {
|
|
461
|
+
if (field.fragmentRef) {
|
|
462
|
+
const base = fragments[field.fragmentRef];
|
|
463
|
+
if (!base) {
|
|
464
|
+
throw new Error(`对象词典 ${item.domain}/${item.object} 引用了不存在的字段片段: ${field.fragmentRef}`);
|
|
465
|
+
}
|
|
466
|
+
resolved.push({
|
|
467
|
+
...resolveFieldFragment(base, statusTemplates),
|
|
468
|
+
...omit(field, ['fragmentRef']),
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
resolved.push(resolveFieldFragment(field, statusTemplates));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return resolved;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function resolveFieldFragment(field, statusTemplates) {
|
|
479
|
+
const next = cloneField(field);
|
|
480
|
+
if (next.statusTemplateRef) {
|
|
481
|
+
const template = statusTemplates[next.statusTemplateRef];
|
|
482
|
+
if (!template) {
|
|
483
|
+
throw new Error(`字段片段 ${next.name || next.label || 'unknown'} 引用了不存在的状态模板: ${next.statusTemplateRef}`);
|
|
484
|
+
}
|
|
485
|
+
next.options = Array.isArray(template.options) ? [...template.options] : [];
|
|
486
|
+
next.default = template.default;
|
|
487
|
+
delete next.statusTemplateRef;
|
|
488
|
+
}
|
|
489
|
+
return next;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function resolveDefaultAppTemplate(item, actionTemplates) {
|
|
493
|
+
if (!item.actionTemplateRef) {
|
|
494
|
+
return {
|
|
495
|
+
defaultActions: [],
|
|
496
|
+
defaultStatusFlow: null,
|
|
497
|
+
defaultResponseContract: null,
|
|
498
|
+
defaultPermissionProfile: null,
|
|
499
|
+
defaultValidationRules: [],
|
|
500
|
+
defaultErrorCodes: [],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const template = actionTemplates[item.actionTemplateRef];
|
|
504
|
+
if (!template) {
|
|
505
|
+
throw new Error(`对象词典 ${item.domain}/${item.object} 引用了不存在的动作模板: ${item.actionTemplateRef}`);
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
defaultActions: Array.isArray(template.actions) ? [...template.actions] : [],
|
|
509
|
+
defaultStatusFlow: template.statusFlow ? cloneField(template.statusFlow) : null,
|
|
510
|
+
defaultResponseContract: template.responseContract ? cloneField(template.responseContract) : null,
|
|
511
|
+
defaultPermissionProfile: template.permissionProfile ? cloneField(template.permissionProfile) : null,
|
|
512
|
+
defaultValidationRules: Array.isArray(template.validationRules) ? template.validationRules.map((item) => cloneField(item)) : [],
|
|
513
|
+
defaultErrorCodes: Array.isArray(template.errorCodes) ? template.errorCodes.map((item) => cloneField(item)) : [],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function cloneField(field) {
|
|
518
|
+
return JSON.parse(JSON.stringify(field));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function omit(object, keys) {
|
|
522
|
+
const next = { ...object };
|
|
523
|
+
for (const key of keys) {
|
|
524
|
+
delete next[key];
|
|
525
|
+
}
|
|
526
|
+
return next;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function uniqueList(items) {
|
|
530
|
+
return Array.from(new Set(items.filter(Boolean)));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function detectObject(text, catalog) {
|
|
534
|
+
const normalized = normalizeText(text);
|
|
535
|
+
let bestMatch = null;
|
|
536
|
+
|
|
537
|
+
for (const item of catalog) {
|
|
538
|
+
let score = 0;
|
|
539
|
+
for (const keyword of item.keywords) {
|
|
540
|
+
if (normalized.includes(normalizeText(keyword))) {
|
|
541
|
+
score += keyword.length;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
545
|
+
bestMatch = { score, item };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return bestMatch && bestMatch.score > 0 ? bestMatch.item : null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function detectPattern(text, catalog) {
|
|
553
|
+
const normalized = normalizeText(text);
|
|
554
|
+
let bestMatch = null;
|
|
555
|
+
|
|
556
|
+
for (const item of catalog) {
|
|
557
|
+
let score = 0;
|
|
558
|
+
for (const keyword of item.keywords || []) {
|
|
559
|
+
if (normalized.includes(normalizeText(keyword))) {
|
|
560
|
+
score += keyword.length;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
564
|
+
bestMatch = { score, item };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return bestMatch && bestMatch.score > 0 ? bestMatch.item : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function detectSuggestedObjectName(text, suggestions, patternRef) {
|
|
572
|
+
const normalized = normalizeText(text);
|
|
573
|
+
let bestMatch = null;
|
|
574
|
+
|
|
575
|
+
for (const item of suggestions || []) {
|
|
576
|
+
if (item.patternRef && patternRef && item.patternRef !== patternRef) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
let score = 0;
|
|
580
|
+
for (const keyword of item.keywords || []) {
|
|
581
|
+
if (normalized.includes(normalizeText(keyword))) {
|
|
582
|
+
score += keyword.length;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
586
|
+
bestMatch = { score, item };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return bestMatch && bestMatch.score > 0 ? bestMatch.item : null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function buildPatternSeedModel(pattern, requestedActions, resources, suggestedName) {
|
|
594
|
+
const fields = (pattern.seedFieldRefs || []).map((ref) => {
|
|
595
|
+
const field = resources.fragments[ref];
|
|
596
|
+
if (!field) {
|
|
597
|
+
throw new Error(`模式 ${pattern.id} 引用了不存在的种子字段片段: ${ref}`);
|
|
598
|
+
}
|
|
599
|
+
return resolveFieldFragment(field, resources.statusTemplates);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const children = (pattern.seedChildTemplateRefs || []).map((ref) => {
|
|
603
|
+
const child = resources.childTemplates[ref];
|
|
604
|
+
if (!child) {
|
|
605
|
+
throw new Error(`模式 ${pattern.id} 引用了不存在的种子子表模板: ${ref}`);
|
|
606
|
+
}
|
|
607
|
+
return cloneField(child);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const actions = requestedActions.length > 0
|
|
611
|
+
? requestedActions
|
|
612
|
+
: resolveDefaultActionsFromPattern(pattern, resources.actionTemplates);
|
|
613
|
+
const inferredAppTemplate = inferAppTemplateByActions(actions, resources.actionTemplates);
|
|
614
|
+
const statusFlow = alignStatusFlowActions(
|
|
615
|
+
resolveDefaultStatusFlowFromPattern(pattern, resources.actionTemplates) || inferredAppTemplate.statusFlow,
|
|
616
|
+
actions
|
|
617
|
+
);
|
|
618
|
+
const responseContract = resolveDefaultResponseContractFromPattern(pattern, resources.actionTemplates) || inferredAppTemplate.responseContract;
|
|
619
|
+
const permissionProfile = resolveDefaultPermissionProfileFromPattern(pattern, resources.actionTemplates) || inferredAppTemplate.permissionProfile;
|
|
620
|
+
const validationRules = resolveDefaultValidationRulesFromPattern(pattern, resources.actionTemplates).length > 0
|
|
621
|
+
? resolveDefaultValidationRulesFromPattern(pattern, resources.actionTemplates)
|
|
622
|
+
: inferredAppTemplate.validationRules;
|
|
623
|
+
const errorCodes = resolveDefaultErrorCodesFromPattern(pattern, resources.actionTemplates).length > 0
|
|
624
|
+
? resolveDefaultErrorCodesFromPattern(pattern, resources.actionTemplates)
|
|
625
|
+
: inferredAppTemplate.errorCodes;
|
|
626
|
+
|
|
627
|
+
return buildSuggestedModel({
|
|
628
|
+
domain: suggestedName?.domain || 'custom',
|
|
629
|
+
object: suggestedName?.object || `${pattern.id}Sample`,
|
|
630
|
+
label: suggestedName?.label || `${pattern.label}示例`,
|
|
631
|
+
pattern: pattern.id,
|
|
632
|
+
table: suggestedName
|
|
633
|
+
? `${snakeCase(suggestedName.domain)}_${snakeCase(suggestedName.object)}`
|
|
634
|
+
: `custom_${snakeCase(pattern.id)}_sample`,
|
|
635
|
+
fields,
|
|
636
|
+
actions,
|
|
637
|
+
statusFlow,
|
|
638
|
+
responseContract,
|
|
639
|
+
permissionProfile,
|
|
640
|
+
validationRules,
|
|
641
|
+
errorCodes,
|
|
642
|
+
detailSections: pattern.defaultUi?.detailSections || ['basicInfo'],
|
|
643
|
+
children,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function resolveDefaultActionsFromPattern(pattern, actionTemplates) {
|
|
648
|
+
const template = pattern.defaultActionTemplateRef ? actionTemplates[pattern.defaultActionTemplateRef] : null;
|
|
649
|
+
return Array.isArray(template?.actions) ? [...template.actions] : [];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function resolveDefaultStatusFlowFromPattern(pattern, actionTemplates) {
|
|
653
|
+
const template = pattern.defaultActionTemplateRef ? actionTemplates[pattern.defaultActionTemplateRef] : null;
|
|
654
|
+
return template?.statusFlow ? cloneField(template.statusFlow) : null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function resolveDefaultErrorCodesFromPattern(pattern, actionTemplates) {
|
|
658
|
+
const template = pattern.defaultActionTemplateRef ? actionTemplates[pattern.defaultActionTemplateRef] : null;
|
|
659
|
+
return Array.isArray(template?.errorCodes) ? template.errorCodes.map((item) => cloneField(item)) : [];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function resolveDefaultResponseContractFromPattern(pattern, actionTemplates) {
|
|
663
|
+
const template = pattern.defaultActionTemplateRef ? actionTemplates[pattern.defaultActionTemplateRef] : null;
|
|
664
|
+
return template?.responseContract ? cloneField(template.responseContract) : null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function resolveDefaultPermissionProfileFromPattern(pattern, actionTemplates) {
|
|
668
|
+
const template = pattern.defaultActionTemplateRef ? actionTemplates[pattern.defaultActionTemplateRef] : null;
|
|
669
|
+
return template?.permissionProfile ? cloneField(template.permissionProfile) : null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function resolveDefaultValidationRulesFromPattern(pattern, actionTemplates) {
|
|
673
|
+
const template = pattern.defaultActionTemplateRef ? actionTemplates[pattern.defaultActionTemplateRef] : null;
|
|
674
|
+
return Array.isArray(template?.validationRules) ? template.validationRules.map((item) => cloneField(item)) : [];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function inferAppTemplateByActions(actions, actionTemplates) {
|
|
678
|
+
const set = new Set(actions || []);
|
|
679
|
+
if (set.has('submit') || set.has('approve') || set.has('reject')) {
|
|
680
|
+
return getActionTemplate('crudWithSubmitApprove', actionTemplates);
|
|
681
|
+
}
|
|
682
|
+
if (set.has('activate') || set.has('deactivate')) {
|
|
683
|
+
return getActionTemplate('crudWithToggle', actionTemplates);
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
statusFlow: null,
|
|
687
|
+
responseContract: null,
|
|
688
|
+
permissionProfile: null,
|
|
689
|
+
validationRules: [],
|
|
690
|
+
errorCodes: [],
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getActionTemplate(templateRef, actionTemplates) {
|
|
695
|
+
const template = actionTemplates[templateRef];
|
|
696
|
+
if (!template) {
|
|
697
|
+
return {
|
|
698
|
+
statusFlow: null,
|
|
699
|
+
responseContract: null,
|
|
700
|
+
permissionProfile: null,
|
|
701
|
+
validationRules: [],
|
|
702
|
+
errorCodes: [],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
statusFlow: template.statusFlow ? cloneField(template.statusFlow) : null,
|
|
707
|
+
responseContract: template.responseContract ? cloneField(template.responseContract) : null,
|
|
708
|
+
permissionProfile: template.permissionProfile ? cloneField(template.permissionProfile) : null,
|
|
709
|
+
validationRules: Array.isArray(template.validationRules) ? template.validationRules.map((item) => cloneField(item)) : [],
|
|
710
|
+
errorCodes: Array.isArray(template.errorCodes) ? template.errorCodes.map((item) => cloneField(item)) : [],
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function detectActions(text) {
|
|
715
|
+
const actionRules = [
|
|
716
|
+
{ action: 'create', patterns: ['新建', '创建', '新增'] },
|
|
717
|
+
{ action: 'update', patterns: ['编辑', '修改', '更新'] },
|
|
718
|
+
{ action: 'list', patterns: ['列表', '查询', '搜索', '筛选'] },
|
|
719
|
+
{ action: 'detail', patterns: ['详情', '查看'] },
|
|
720
|
+
{ action: 'activate', patterns: ['启用'] },
|
|
721
|
+
{ action: 'deactivate', patterns: ['停用', '禁用'] },
|
|
722
|
+
{ action: 'submit', patterns: ['提交', '提交审批', '送审'] },
|
|
723
|
+
{ action: 'approve', patterns: ['审批通过', '审核通过', '批准', '审批'] },
|
|
724
|
+
{ action: 'reject', patterns: ['驳回', '拒绝', '退回'] },
|
|
725
|
+
{ action: 'close', patterns: ['关闭', '关闭单据', '结束'] },
|
|
726
|
+
{ action: 'archive', patterns: ['归档', '存档'] },
|
|
727
|
+
{ action: 'convert', patterns: ['转化', '转换'] },
|
|
728
|
+
];
|
|
729
|
+
|
|
730
|
+
const actions = [];
|
|
731
|
+
for (const rule of actionRules) {
|
|
732
|
+
if (rule.patterns.some((pattern) => text.includes(pattern))) {
|
|
733
|
+
actions.push(rule.action);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return actions;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function buildSuggestedModel({ domain, object, label, pattern, table, fields, actions, statusFlow, responseContract, permissionProfile, validationRules, errorCodes, detailSections, children }) {
|
|
740
|
+
return {
|
|
741
|
+
meta: {
|
|
742
|
+
domain,
|
|
743
|
+
object,
|
|
744
|
+
label,
|
|
745
|
+
...(pattern ? { pattern } : {}),
|
|
746
|
+
version: '0.1.0',
|
|
747
|
+
description: `由 requirement:analyze 从自然语言需求推导出的候选业务模型。`,
|
|
748
|
+
},
|
|
749
|
+
data: {
|
|
750
|
+
table,
|
|
751
|
+
fields,
|
|
752
|
+
...(children && children.length > 0 ? {
|
|
753
|
+
children: children.map((child) => ({
|
|
754
|
+
name: child.name,
|
|
755
|
+
label: child.label,
|
|
756
|
+
table: buildChildTableName(domain, object, child),
|
|
757
|
+
foreignKey: child.foreignKey,
|
|
758
|
+
fields: child.fields || [],
|
|
759
|
+
})),
|
|
760
|
+
} : {}),
|
|
761
|
+
},
|
|
762
|
+
app: {
|
|
763
|
+
actions,
|
|
764
|
+
defaultStatus: fields.some((item) => item.name === 'status') ? String(fields.find((item) => item.name === 'status').default || '') : undefined,
|
|
765
|
+
...(statusFlow ? { statusFlow } : {}),
|
|
766
|
+
...(responseContract ? { responseContract } : {}),
|
|
767
|
+
...(permissionProfile ? { permissionProfile } : {}),
|
|
768
|
+
...(validationRules && validationRules.length > 0 ? { validationRules } : {}),
|
|
769
|
+
...(errorCodes && errorCodes.length > 0 ? { errorCodes } : {}),
|
|
770
|
+
},
|
|
771
|
+
ui: {
|
|
772
|
+
list: {
|
|
773
|
+
columns: buildListColumns(fields),
|
|
774
|
+
filters: buildListFilters(fields),
|
|
775
|
+
},
|
|
776
|
+
form: {
|
|
777
|
+
groups: [
|
|
778
|
+
{
|
|
779
|
+
title: '基本信息',
|
|
780
|
+
fields: fields.map((item) => item.name),
|
|
781
|
+
},
|
|
782
|
+
...buildChildFormGroups(children),
|
|
783
|
+
],
|
|
784
|
+
},
|
|
785
|
+
detail: {
|
|
786
|
+
sections: buildDetailSections(detailSections, children),
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function alignStatusFlowActions(statusFlow, actions) {
|
|
793
|
+
if (!statusFlow) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
const allowed = new Set(actions || []);
|
|
797
|
+
return {
|
|
798
|
+
...cloneField(statusFlow),
|
|
799
|
+
transitions: (statusFlow.transitions || []).filter((item) => allowed.has(item.action)),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildChildTableName(domain, object, child) {
|
|
804
|
+
if (child.table) {
|
|
805
|
+
return child.table;
|
|
806
|
+
}
|
|
807
|
+
const suffix = child.tableSuffix || snakeCase(child.name || 'detail');
|
|
808
|
+
return `${snakeCase(domain)}_${snakeCase(object)}_${snakeCase(suffix)}`;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function buildChildFormGroups(children) {
|
|
812
|
+
return (children || []).map((child) => ({
|
|
813
|
+
title: child.formGroupTitle || child.label || child.name,
|
|
814
|
+
childTable: child.name,
|
|
815
|
+
}));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function buildDetailSections(detailSections, children) {
|
|
819
|
+
const base = detailSections && detailSections.length > 0 ? [...detailSections] : ['basicInfo'];
|
|
820
|
+
for (const child of children || []) {
|
|
821
|
+
const section = child.detailSection || child.name;
|
|
822
|
+
if (!base.includes(section)) {
|
|
823
|
+
base.push(section);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return base;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function buildListColumns(fields) {
|
|
830
|
+
const preferred = fields
|
|
831
|
+
.filter((item) => ['leadCode', 'customerCode', 'opportunityCode', 'quoteCode', 'materialCode', 'documentNo', 'documentTitle', 'bizDate', 'categoryCode', 'leadName', 'customerName', 'opportunityName', 'quoteName', 'materialName', 'categoryName', 'parentName', 'source', 'stage', 'materialType', 'materialGroup', 'ownerName', 'sortOrder', 'status'].includes(item.name))
|
|
832
|
+
.map((item) => item.name);
|
|
833
|
+
const unique = Array.from(new Set([...preferred, 'createdAt']));
|
|
834
|
+
return unique;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function buildListFilters(fields) {
|
|
838
|
+
return fields
|
|
839
|
+
.filter((item) => ['leadCode', 'customerCode', 'opportunityCode', 'quoteCode', 'materialCode', 'documentNo', 'documentTitle', 'bizDate', 'categoryCode', 'leadName', 'customerName', 'opportunityName', 'quoteName', 'materialName', 'categoryName', 'parentName', 'source', 'stage', 'materialType', 'materialGroup', 'status'].includes(item.name))
|
|
840
|
+
.map((item) => item.name);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function validateInlineModel(model) {
|
|
844
|
+
const tempPath = path.join(
|
|
845
|
+
os.tmpdir(),
|
|
846
|
+
`scaffold-requirement-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.yaml`
|
|
847
|
+
);
|
|
848
|
+
fs.writeFileSync(tempPath, YAML.stringify(model), 'utf8');
|
|
849
|
+
try {
|
|
850
|
+
return validateModelFile(tempPath);
|
|
851
|
+
} finally {
|
|
852
|
+
if (fs.existsSync(tempPath)) {
|
|
853
|
+
fs.rmSync(tempPath, { force: true });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function renderMarkdown(result) {
|
|
859
|
+
const lines = [];
|
|
860
|
+
lines.push(`需求: ${result.requirement}`);
|
|
861
|
+
lines.push('');
|
|
862
|
+
lines.push(`decision: ${result.decision}`);
|
|
863
|
+
lines.push(`nextAction: ${result.nextAction}`);
|
|
864
|
+
|
|
865
|
+
if (result.inferred) {
|
|
866
|
+
lines.push('');
|
|
867
|
+
lines.push('识别结果:');
|
|
868
|
+
lines.push(`- domain: ${result.inferred.domain || '-'}`);
|
|
869
|
+
lines.push(`- object: ${result.inferred.object || '-'}`);
|
|
870
|
+
lines.push(`- label: ${result.inferred.label || '-'}`);
|
|
871
|
+
lines.push(`- pattern: ${result.inferred.pattern || '-'}`);
|
|
872
|
+
lines.push(`- actions: ${(result.inferred.actions || []).join(', ') || '-'}`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (result.reason && result.reason.length > 0) {
|
|
876
|
+
lines.push('');
|
|
877
|
+
lines.push('原因:');
|
|
878
|
+
for (const item of result.reason) {
|
|
879
|
+
lines.push(`- ${item}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (result.missingInfo && result.missingInfo.length > 0) {
|
|
884
|
+
lines.push('');
|
|
885
|
+
lines.push('缺失信息:');
|
|
886
|
+
for (const item of result.missingInfo) {
|
|
887
|
+
lines.push(`- ${item}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
892
|
+
lines.push('');
|
|
893
|
+
lines.push('提示:');
|
|
894
|
+
for (const item of result.warnings) {
|
|
895
|
+
lines.push(`- ${item}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (result.generatedSpecPath) {
|
|
900
|
+
lines.push('');
|
|
901
|
+
lines.push(`已写出候选模型: ${result.generatedSpecPath}`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (result.suggestedModel) {
|
|
905
|
+
lines.push('');
|
|
906
|
+
lines.push('候选模型:');
|
|
907
|
+
lines.push('```yaml');
|
|
908
|
+
lines.push(YAML.stringify(result.suggestedModel).trimEnd());
|
|
909
|
+
lines.push('```');
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return `${lines.join('\n')}\n`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function normalizeText(value) {
|
|
916
|
+
return String(value).trim().toLowerCase().replace(/\s+/g, '');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function toLower(value) {
|
|
920
|
+
return String(value).toLowerCase();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function writeSuggestedSpec(model, filePath) {
|
|
924
|
+
const specPath = resolveOutputPath(filePath);
|
|
925
|
+
fs.mkdirSync(path.dirname(specPath), { recursive: true });
|
|
926
|
+
fs.writeFileSync(specPath, YAML.stringify(model), 'utf8');
|
|
927
|
+
return specPath;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function resolveOutputPath(filePath) {
|
|
931
|
+
if (path.isAbsolute(filePath)) {
|
|
932
|
+
return filePath;
|
|
933
|
+
}
|
|
934
|
+
const normalized = String(filePath).replace(/\\/g, '/');
|
|
935
|
+
if (normalized.startsWith('scaffold/')) {
|
|
936
|
+
return path.resolve(process.cwd(), normalized.slice('scaffold/'.length));
|
|
937
|
+
}
|
|
938
|
+
if (normalized.startsWith('specs/')) {
|
|
939
|
+
return resolveScaffoldPath(...normalized.split('/'));
|
|
940
|
+
}
|
|
941
|
+
return path.resolve(filePath);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
module.exports = {
|
|
945
|
+
analyzeRequirement,
|
|
946
|
+
parseArgs,
|
|
947
|
+
readRequirementText,
|
|
948
|
+
renderMarkdown,
|
|
949
|
+
writeSuggestedSpec,
|
|
950
|
+
};
|