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,371 @@
|
|
|
1
|
+
function renderAdminListPage(context, helpers) {
|
|
2
|
+
const patternMode = helpers.getPatternId(context) || 'default';
|
|
3
|
+
return renderPatternAdminListPage(context, patternMode, helpers);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function renderPatternAdminListPage(context, patternMode, helpers) {
|
|
7
|
+
const pageResultType = `${context.objectPascal}PageResult`;
|
|
8
|
+
const filtersType = `${context.objectPascal}Filters`;
|
|
9
|
+
const searchField = helpers.getPrimarySearchField(context);
|
|
10
|
+
const filterFields = ((context.ui.list && context.ui.list.filters) || [])
|
|
11
|
+
.map((fieldName) => context.fields.find((item) => item.name === fieldName))
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
const pendingFiltersInitial = filterFields.map((field) => ` ${field.name}: "",`).join('\n');
|
|
14
|
+
const filtersInitial = filterFields.map((field) => ` ${field.name}: "",`).join('\n');
|
|
15
|
+
const loadArgs = filterFields.map((field) => ` filters.${field.name},`).join('\n');
|
|
16
|
+
const applyFilterAssignments = filterFields
|
|
17
|
+
.filter((field) => field.name !== searchField)
|
|
18
|
+
.map((field) => ` ${field.name}: pendingFilters.${field.name},`)
|
|
19
|
+
.join('\n');
|
|
20
|
+
const clearFilters = filterFields.map((field) => ` ${field.name}: "",`).join('\n');
|
|
21
|
+
const columns = renderAdminListColumns(context, helpers);
|
|
22
|
+
const filterContent = renderAdminFilterContent(context, searchField, helpers);
|
|
23
|
+
const filterCountFields = filterFields.filter((field) => field.name !== searchField).map((field) => `filters.${field.name}`);
|
|
24
|
+
const headerIcon = helpers.getAdminHeaderIcon(context);
|
|
25
|
+
const headerTitle = helpers.getPatternListTitle(context);
|
|
26
|
+
const primaryActionLabel = helpers.getPatternCreateLabel(context);
|
|
27
|
+
const searchPlaceholder = helpers.getPatternSearchPlaceholder(context, helpers.getFieldLabel(context, searchField));
|
|
28
|
+
|
|
29
|
+
return `import { useCallback, useEffect, useMemo, useState } from "react";
|
|
30
|
+
import { useNavigate } from "react-router";
|
|
31
|
+
import { Eye, Pencil, ${headerIcon} } from "lucide-react";
|
|
32
|
+
import { toast } from "sonner";
|
|
33
|
+
import type { ${context.objectPascal}Vo } from "@scaffold/api/client";
|
|
34
|
+
import { ListReport, type ListReportColumn } from "@/components/admin-ui/list-report";
|
|
35
|
+
import { Button } from "@/components/ui/button";
|
|
36
|
+
import { Input } from "@/components/ui/input";
|
|
37
|
+
import { Label } from "@/components/ui/label";
|
|
38
|
+
import {
|
|
39
|
+
Select,
|
|
40
|
+
SelectContent,
|
|
41
|
+
SelectItem,
|
|
42
|
+
SelectTrigger,
|
|
43
|
+
SelectValue,
|
|
44
|
+
} from "@/components/ui/select";
|
|
45
|
+
import { cn } from "@/lib/utils";
|
|
46
|
+
import {
|
|
47
|
+
create${context.objectPascal}Api,
|
|
48
|
+
formatDateTime,
|
|
49
|
+
getOptionLabel,
|
|
50
|
+
${context.fields.some((field) => field.name === 'status') ? `get${context.objectPascal}StatusMeta,\n ` : ''}${context.fields.filter((field) => field.type === 'enum').map((field) => `${field.name.toUpperCase()}_OPTIONS`).join(',\n ')}${context.fields.filter((field) => field.type === 'enum').length ? ',\n ' : ''}type ${pageResultType},
|
|
51
|
+
type ${filtersType},
|
|
52
|
+
} from "./${context.objectKebab}-ui";
|
|
53
|
+
|
|
54
|
+
export function ListPage() {
|
|
55
|
+
const navigate = useNavigate();
|
|
56
|
+
const ${context.objectCamel}Api = useMemo(() => create${context.objectPascal}Api(), []);
|
|
57
|
+
const [showFilter, setShowFilter] = useState(false);
|
|
58
|
+
const [loading, setLoading] = useState(false);
|
|
59
|
+
const [filters, setFilters] = useState<${filtersType}>({
|
|
60
|
+
${filtersInitial}
|
|
61
|
+
});
|
|
62
|
+
const [pendingFilters, setPendingFilters] = useState({
|
|
63
|
+
${pendingFiltersInitial}
|
|
64
|
+
});
|
|
65
|
+
const [pagination, setPagination] = useState({
|
|
66
|
+
pageIndex: 0,
|
|
67
|
+
pageSize: 10,
|
|
68
|
+
});
|
|
69
|
+
const [listState, setListState] = useState({
|
|
70
|
+
rows: [] as ${context.objectPascal}Vo[],
|
|
71
|
+
total: 0,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const load${context.objectPascal}s = useCallback(async () => {
|
|
75
|
+
setLoading(true);
|
|
76
|
+
try {
|
|
77
|
+
const response = await ${context.objectCamel}Api.page(
|
|
78
|
+
String(pagination.pageIndex + 1),
|
|
79
|
+
String(pagination.pageSize),
|
|
80
|
+
${loadArgs}
|
|
81
|
+
) as ${pageResultType};
|
|
82
|
+
|
|
83
|
+
setListState({
|
|
84
|
+
rows: response.records ?? [],
|
|
85
|
+
total: response.total ?? 0,
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : "加载${context.label}列表失败";
|
|
89
|
+
toast.error(message);
|
|
90
|
+
} finally {
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
}, [${context.objectCamel}Api, pagination.pageIndex, pagination.pageSize, ${filterFields.map((field) => `filters.${field.name}`).join(', ')}]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
void load${context.objectPascal}s();
|
|
97
|
+
}, [load${context.objectPascal}s]);
|
|
98
|
+
|
|
99
|
+
const columns: ListReportColumn<${context.objectPascal}Vo>[] = [
|
|
100
|
+
${helpers.indent(columns, 4)}
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const handleApplyFilters = () => {
|
|
104
|
+
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
105
|
+
setFilters((prev) => ({
|
|
106
|
+
...prev,
|
|
107
|
+
${applyFilterAssignments || ' ...prev,'}
|
|
108
|
+
}));
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleClearFilters = () => {
|
|
112
|
+
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
113
|
+
setPendingFilters({
|
|
114
|
+
${clearFilters}
|
|
115
|
+
});
|
|
116
|
+
setFilters({
|
|
117
|
+
${clearFilters}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const filterCount = [${filterCountFields.join(', ')}].filter(Boolean).length;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="h-full">
|
|
125
|
+
<ListReport<${context.objectPascal}Vo>
|
|
126
|
+
header={{
|
|
127
|
+
title: "${headerTitle}",
|
|
128
|
+
subtitle: "${helpers.getPatternListSubtitle(context)}",
|
|
129
|
+
tag: "${context.domainUpper} MVP",
|
|
130
|
+
icon: <${headerIcon} className="size-7" />,
|
|
131
|
+
}}
|
|
132
|
+
data={listState.rows}
|
|
133
|
+
columns={columns}
|
|
134
|
+
totalCount={listState.total}
|
|
135
|
+
loading={loading}
|
|
136
|
+
manualPagination={true}
|
|
137
|
+
primaryAction={{
|
|
138
|
+
id: "create",
|
|
139
|
+
label: "${primaryActionLabel}",
|
|
140
|
+
onClick: () => navigate("/${context.routePath}/create"),
|
|
141
|
+
}}
|
|
142
|
+
searchPlaceholder="${searchPlaceholder}"
|
|
143
|
+
onSearch={(value) => {
|
|
144
|
+
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
145
|
+
setFilters((prev) => ({
|
|
146
|
+
...prev,
|
|
147
|
+
${searchField}: value.trim(),
|
|
148
|
+
}));
|
|
149
|
+
}}
|
|
150
|
+
showFilter={showFilter}
|
|
151
|
+
onFilterToggle={() => setShowFilter((prev) => !prev)}
|
|
152
|
+
filterCount={filterCount}
|
|
153
|
+
onFilterClear={handleClearFilters}
|
|
154
|
+
onFilterApply={handleApplyFilters}
|
|
155
|
+
onRefresh={() => void load${context.objectPascal}s()}
|
|
156
|
+
pageIndex={pagination.pageIndex}
|
|
157
|
+
pageSize={pagination.pageSize}
|
|
158
|
+
onPaginationChange={(pageIndex, pageSize) => {
|
|
159
|
+
setPagination({ pageIndex, pageSize });
|
|
160
|
+
}}
|
|
161
|
+
getRowId={(row) => String(row.id)}
|
|
162
|
+
onRowClick={(row) => navigate("/${context.routePath}/" + row.id)}
|
|
163
|
+
filterContent={
|
|
164
|
+
${helpers.indent(filterContent, 10)}
|
|
165
|
+
}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default ListPage;
|
|
172
|
+
`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderAdminListColumns(context, helpers) {
|
|
176
|
+
const titleField = helpers.getPrimaryTitleField(context);
|
|
177
|
+
const codeField = helpers.findFieldByPriority(context, helpers.getPatternFieldPriority(context, 'code'))
|
|
178
|
+
|| helpers.findFieldByPriority(context, ['leadCode', 'customerCode', 'opportunityCode', 'quoteCode', 'contractCode', 'materialCode']);
|
|
179
|
+
const secondaryField = helpers.findFieldByPriority(context, helpers.getPatternFieldPriority(context, 'secondary'));
|
|
180
|
+
const ownerField = context.fields.find((field) => field.name === 'ownerName') || secondaryField;
|
|
181
|
+
const columns = [];
|
|
182
|
+
|
|
183
|
+
if (codeField) {
|
|
184
|
+
columns.push(`{
|
|
185
|
+
id: "${codeField.name}",
|
|
186
|
+
header: "${codeField.label || codeField.name}",
|
|
187
|
+
sortable: true,
|
|
188
|
+
accessorKey: "${codeField.name}",
|
|
189
|
+
cell: (row) => (
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
onClick={(event) => {
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
navigate("/${context.routePath}/" + row.id);
|
|
195
|
+
}}
|
|
196
|
+
className="text-sm font-medium text-primary hover:underline"
|
|
197
|
+
>
|
|
198
|
+
{row.${codeField.name} || "-"}
|
|
199
|
+
</button>
|
|
200
|
+
),
|
|
201
|
+
}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const titleMeta = context.fields.find((field) => field.name === titleField);
|
|
205
|
+
if (titleMeta) {
|
|
206
|
+
columns.push(`{
|
|
207
|
+
id: "${titleMeta.name}",
|
|
208
|
+
header: "${titleMeta.label || titleMeta.name}",
|
|
209
|
+
sortable: true,
|
|
210
|
+
accessorKey: "${titleMeta.name}",
|
|
211
|
+
cell: (row) => (
|
|
212
|
+
<div>
|
|
213
|
+
<p className="text-sm font-medium">{row.${titleMeta.name}}</p>
|
|
214
|
+
<p className="text-xs text-muted-foreground">{${helpers.renderDisplayValue(ownerField || titleMeta, `row.${ownerField ? ownerField.name : titleMeta.name}`, `"${ownerField && ownerField.name === 'ownerName' ? '未分配负责人' : '-'}"` )}}</p>
|
|
215
|
+
</div>
|
|
216
|
+
),
|
|
217
|
+
}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const fieldName of (context.ui.list.columns || [])) {
|
|
221
|
+
const field = context.fields.find((item) => item.name === fieldName);
|
|
222
|
+
if (!field || field.name === codeField?.name || field.name === titleMeta?.name) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
columns.push(renderSimpleListColumn(context, field, helpers));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
columns.push(`{
|
|
229
|
+
id: "actions",
|
|
230
|
+
header: "操作",
|
|
231
|
+
align: "center",
|
|
232
|
+
cell: (row) => (
|
|
233
|
+
<div className="flex justify-center gap-1" onClick={(event) => event.stopPropagation()}>
|
|
234
|
+
<Button
|
|
235
|
+
type="button"
|
|
236
|
+
variant="ghost"
|
|
237
|
+
size="icon"
|
|
238
|
+
className="size-8 text-muted-foreground hover:bg-primary/10 hover:text-primary"
|
|
239
|
+
title="查看详情"
|
|
240
|
+
onClick={() => navigate("/${context.routePath}/" + row.id)}
|
|
241
|
+
>
|
|
242
|
+
<Eye className="size-4" />
|
|
243
|
+
</Button>
|
|
244
|
+
<Button
|
|
245
|
+
type="button"
|
|
246
|
+
variant="ghost"
|
|
247
|
+
size="icon"
|
|
248
|
+
className="size-8 text-muted-foreground hover:bg-primary/10 hover:text-primary"
|
|
249
|
+
title="编辑${context.label}"
|
|
250
|
+
onClick={() => navigate("/${context.routePath}/" + row.id + "/edit")}
|
|
251
|
+
>
|
|
252
|
+
<Pencil className="size-4" />
|
|
253
|
+
</Button>
|
|
254
|
+
</div>
|
|
255
|
+
),
|
|
256
|
+
}`);
|
|
257
|
+
|
|
258
|
+
return columns.join(',\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderSimpleListColumn(context, field, helpers) {
|
|
262
|
+
if (field.name === 'createdAt' || field.name === 'updatedAt') {
|
|
263
|
+
return `{
|
|
264
|
+
id: "${field.name}",
|
|
265
|
+
header: "${field.label || (field.name === 'createdAt' ? '创建时间' : '更新时间')}",
|
|
266
|
+
cell: (row) => (
|
|
267
|
+
<span className="text-sm text-muted-foreground">{formatDateTime(row.${field.name})}</span>
|
|
268
|
+
),
|
|
269
|
+
}`;
|
|
270
|
+
}
|
|
271
|
+
if (field.type === 'enum') {
|
|
272
|
+
if (field.name === 'status') {
|
|
273
|
+
return `{
|
|
274
|
+
id: "${field.name}",
|
|
275
|
+
header: "${field.label || field.name}",
|
|
276
|
+
align: "center",
|
|
277
|
+
accessorKey: "${field.name}",
|
|
278
|
+
cell: (row) => {
|
|
279
|
+
const meta = get${context.objectPascal}StatusMeta(row.${field.name});
|
|
280
|
+
return (
|
|
281
|
+
<div className="flex justify-center">
|
|
282
|
+
<span
|
|
283
|
+
className={cn(
|
|
284
|
+
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
|
|
285
|
+
meta.badgeClassName
|
|
286
|
+
)}
|
|
287
|
+
>
|
|
288
|
+
<span className={cn("size-1.5 rounded-full", meta.dotClassName)} />
|
|
289
|
+
{meta.label}
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
},
|
|
294
|
+
}`;
|
|
295
|
+
}
|
|
296
|
+
return `{
|
|
297
|
+
id: "${field.name}",
|
|
298
|
+
header: "${field.label || field.name}",
|
|
299
|
+
align: "center",
|
|
300
|
+
cell: (row) => (
|
|
301
|
+
<span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
|
|
302
|
+
{getOptionLabel(${field.name.toUpperCase()}_OPTIONS, row.${field.name})}
|
|
303
|
+
</span>
|
|
304
|
+
),
|
|
305
|
+
}`;
|
|
306
|
+
}
|
|
307
|
+
return `{
|
|
308
|
+
id: "${field.name}",
|
|
309
|
+
header: "${field.label || field.name}",
|
|
310
|
+
cell: (row) => <span className="text-sm text-muted-foreground">{${helpers.renderDisplayValue(field, `row.${field.name}`)}}</span>,
|
|
311
|
+
}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function renderAdminFilterContent(context, searchField, helpers) {
|
|
315
|
+
const filterFields = ((context.ui.list && context.ui.list.filters) || [])
|
|
316
|
+
.filter((fieldName) => fieldName !== searchField)
|
|
317
|
+
.map((fieldName) => context.fields.find((item) => item.name === fieldName))
|
|
318
|
+
.filter(Boolean);
|
|
319
|
+
if (filterFields.length === 0) {
|
|
320
|
+
return `<div className="text-sm text-muted-foreground">当前没有额外筛选项</div>`;
|
|
321
|
+
}
|
|
322
|
+
return `<div className="grid gap-4 md:grid-cols-${Math.min(filterFields.length, 3)}">
|
|
323
|
+
${helpers.indent(filterFields.map((field) => renderFilterField(field)).join('\n'), 2)}
|
|
324
|
+
</div>`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderFilterField(field) {
|
|
328
|
+
if (field.type === 'enum') {
|
|
329
|
+
return `<div className="space-y-2">
|
|
330
|
+
<Label className="text-xs text-muted-foreground">${field.label || field.name}</Label>
|
|
331
|
+
<Select
|
|
332
|
+
value={pendingFilters.${field.name} || "__all__"}
|
|
333
|
+
onValueChange={(value) =>
|
|
334
|
+
setPendingFilters((prev) => ({
|
|
335
|
+
...prev,
|
|
336
|
+
${field.name}: value === "__all__" ? "" : value,
|
|
337
|
+
}))
|
|
338
|
+
}
|
|
339
|
+
>
|
|
340
|
+
<SelectTrigger className="w-full">
|
|
341
|
+
<SelectValue placeholder="全部${field.label || field.name}" />
|
|
342
|
+
</SelectTrigger>
|
|
343
|
+
<SelectContent>
|
|
344
|
+
<SelectItem value="__all__">全部${field.label || field.name}</SelectItem>
|
|
345
|
+
{${field.name.toUpperCase()}_OPTIONS.map((item) => (
|
|
346
|
+
<SelectItem key={item.value} value={item.value}>
|
|
347
|
+
{item.label}
|
|
348
|
+
</SelectItem>
|
|
349
|
+
))}
|
|
350
|
+
</SelectContent>
|
|
351
|
+
</Select>
|
|
352
|
+
</div>`;
|
|
353
|
+
}
|
|
354
|
+
return `<div className="space-y-2">
|
|
355
|
+
<Label className="text-xs text-muted-foreground">${field.label || field.name}</Label>
|
|
356
|
+
<Input
|
|
357
|
+
value={pendingFilters.${field.name}}
|
|
358
|
+
onChange={(event) =>
|
|
359
|
+
setPendingFilters((prev) => ({
|
|
360
|
+
...prev,
|
|
361
|
+
${field.name}: event.target.value,
|
|
362
|
+
}))
|
|
363
|
+
}
|
|
364
|
+
placeholder="请输入${field.label || field.name}"
|
|
365
|
+
/>
|
|
366
|
+
</div>`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
renderAdminListPage,
|
|
371
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SCAFFOLD_ROOT = path.resolve(__dirname, '..', '..');
|
|
5
|
+
|
|
6
|
+
function buildDefaultRuntimeConfig() {
|
|
7
|
+
return {
|
|
8
|
+
scaffoldRoot: SCAFFOLD_ROOT,
|
|
9
|
+
projectRoot: path.resolve(SCAFFOLD_ROOT, '..'),
|
|
10
|
+
catalogs: {
|
|
11
|
+
rootDir: path.join(SCAFFOLD_ROOT, 'specs', 'catalogs'),
|
|
12
|
+
objectCatalog: 'object-catalog.yaml',
|
|
13
|
+
patternCatalog: 'pattern-catalog.yaml',
|
|
14
|
+
fieldFragments: 'field-fragments.yaml',
|
|
15
|
+
objectTemplates: 'object-templates.yaml',
|
|
16
|
+
objectNameSuggestions: 'object-name-suggestions.yaml',
|
|
17
|
+
actionTemplates: 'action-templates.yaml',
|
|
18
|
+
statusTemplates: 'status-templates.yaml',
|
|
19
|
+
childTemplates: 'child-templates.yaml'
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
rootDir: path.join(SCAFFOLD_ROOT, 'specs', 'rules'),
|
|
23
|
+
businessModelSchema: 'business-model.schema.json',
|
|
24
|
+
extensionBoundaries: 'extension-boundaries.json',
|
|
25
|
+
specGovernance: 'spec-governance.json',
|
|
26
|
+
requirementDraftSchema: 'requirement-draft.schema.json'
|
|
27
|
+
},
|
|
28
|
+
drafts: {
|
|
29
|
+
rootDir: path.join(SCAFFOLD_ROOT, '.sandbox', 'drafts')
|
|
30
|
+
},
|
|
31
|
+
generatedSpecs: {
|
|
32
|
+
rootDir: path.join(SCAFFOLD_ROOT, 'specs', 'projects', 'generated-from-nl')
|
|
33
|
+
},
|
|
34
|
+
targets: {
|
|
35
|
+
adapter: 'scaffold-default',
|
|
36
|
+
apiSrcDir: path.join(SCAFFOLD_ROOT, 'packages', 'api', 'src'),
|
|
37
|
+
adminSrcDir: path.join(SCAFFOLD_ROOT, 'packages', 'admin', 'src'),
|
|
38
|
+
toolCwd: SCAFFOLD_ROOT,
|
|
39
|
+
pnpmCwd: path.resolve(SCAFFOLD_ROOT, '..'),
|
|
40
|
+
apiPackageDir: path.join(SCAFFOLD_ROOT, 'packages', 'api')
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadRuntimeConfig(options = {}) {
|
|
46
|
+
const explicit = options.runtimeConfig || {};
|
|
47
|
+
const configPath = options.configPath || process.env.SCAFFOLD_RUNTIME_CONFIG || '';
|
|
48
|
+
const fileConfig = configPath ? readJson(path.resolve(configPath)) : {};
|
|
49
|
+
const merged = deepMerge(buildDefaultRuntimeConfig(), fileConfig);
|
|
50
|
+
return normalizeRuntimeConfig(deepMerge(merged, explicit));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeRuntimeConfig(config) {
|
|
54
|
+
const scaffoldRoot = path.resolve(config.scaffoldRoot || SCAFFOLD_ROOT);
|
|
55
|
+
const projectRoot = path.resolve(config.projectRoot || path.resolve(scaffoldRoot, '..'));
|
|
56
|
+
return {
|
|
57
|
+
scaffoldRoot,
|
|
58
|
+
projectRoot,
|
|
59
|
+
catalogs: normalizeNamedRoot(config.catalogs, path.join(scaffoldRoot, 'specs', 'catalogs')),
|
|
60
|
+
rules: normalizeNamedRoot(config.rules, path.join(scaffoldRoot, 'specs', 'rules')),
|
|
61
|
+
drafts: {
|
|
62
|
+
rootDir: path.resolve(config.drafts?.rootDir || path.join(scaffoldRoot, '.sandbox', 'drafts'))
|
|
63
|
+
},
|
|
64
|
+
generatedSpecs: {
|
|
65
|
+
rootDir: path.resolve(config.generatedSpecs?.rootDir || path.join(scaffoldRoot, 'specs', 'projects', 'generated-from-nl'))
|
|
66
|
+
},
|
|
67
|
+
targets: {
|
|
68
|
+
adapter: config.targets?.adapter || 'scaffold-default',
|
|
69
|
+
apiSrcDir: path.resolve(config.targets?.apiSrcDir || path.join(scaffoldRoot, 'packages', 'api', 'src')),
|
|
70
|
+
adminSrcDir: path.resolve(config.targets?.adminSrcDir || path.join(scaffoldRoot, 'packages', 'admin', 'src')),
|
|
71
|
+
toolCwd: path.resolve(config.targets?.toolCwd || scaffoldRoot),
|
|
72
|
+
pnpmCwd: path.resolve(config.targets?.pnpmCwd || projectRoot),
|
|
73
|
+
apiPackageDir: path.resolve(config.targets?.apiPackageDir || path.join(scaffoldRoot, 'packages', 'api'))
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeNamedRoot(section = {}, defaultRootDir) {
|
|
79
|
+
return {
|
|
80
|
+
...section,
|
|
81
|
+
rootDir: path.resolve(section.rootDir || defaultRootDir)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveCatalogPath(runtimeConfig, key) {
|
|
86
|
+
return resolveNamedPath(runtimeConfig.catalogs, key);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveRulePath(runtimeConfig, key) {
|
|
90
|
+
return resolveNamedPath(runtimeConfig.rules, key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveGeneratedSpecPath(runtimeConfig, fileName) {
|
|
94
|
+
return path.join(runtimeConfig.generatedSpecs.rootDir, fileName);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveDraftRoot(runtimeConfig) {
|
|
98
|
+
return runtimeConfig.drafts.rootDir;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveNamedPath(section, key) {
|
|
102
|
+
const value = section[key];
|
|
103
|
+
if (!value) {
|
|
104
|
+
throw new Error(`runtime config 缺少路径项: ${key}`);
|
|
105
|
+
}
|
|
106
|
+
return path.resolve(section.rootDir, value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function deepMerge(base, patch) {
|
|
110
|
+
if (Array.isArray(patch)) {
|
|
111
|
+
return patch.map((item) => cloneValue(item));
|
|
112
|
+
}
|
|
113
|
+
if (!isObject(base) || !isObject(patch)) {
|
|
114
|
+
return cloneValue(patch);
|
|
115
|
+
}
|
|
116
|
+
const next = { ...base };
|
|
117
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
next[key] = value.map((item) => cloneValue(item));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (isObject(value) && isObject(base[key])) {
|
|
123
|
+
next[key] = deepMerge(base[key], value);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
next[key] = cloneValue(value);
|
|
127
|
+
}
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cloneValue(value) {
|
|
132
|
+
if (value === undefined) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return JSON.parse(JSON.stringify(value));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isObject(value) {
|
|
139
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readJson(filePath) {
|
|
143
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
SCAFFOLD_ROOT,
|
|
148
|
+
buildDefaultRuntimeConfig,
|
|
149
|
+
loadRuntimeConfig,
|
|
150
|
+
resolveCatalogPath,
|
|
151
|
+
resolveDraftRoot,
|
|
152
|
+
resolveGeneratedSpecPath,
|
|
153
|
+
resolveRulePath
|
|
154
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const YAML = require('yaml');
|
|
5
|
+
const { getDefaultDraftRoot, loadDraft, patchDraft, saveDraft } = require('./lib/draft-toolkit.cjs');
|
|
6
|
+
|
|
7
|
+
const options = parseArgs(process.argv.slice(2));
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
if (!options.draft || !options.patch) {
|
|
11
|
+
throw new Error('请提供 --draft <draft-path|draft-id> 和 --patch <patch.yaml|json>');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const draft = loadDraft(options.draft, { draftRoot: getDefaultDraftRoot() });
|
|
15
|
+
const patch = readStructuredFile(options.patch);
|
|
16
|
+
const nextDraft = patchDraft(draft, patch);
|
|
17
|
+
const savedDraft = saveDraft(nextDraft, {
|
|
18
|
+
draftRoot: draft.storage?.draftRoot || getDefaultDraftRoot(),
|
|
19
|
+
fileName: draft.storage?.fileName,
|
|
20
|
+
});
|
|
21
|
+
console.log(JSON.stringify(savedDraft, null, 2));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(`\n❌ Draft patch 失败: ${error.message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const options = {
|
|
29
|
+
draft: '',
|
|
30
|
+
patch: '',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const item = argv[index];
|
|
35
|
+
if (item === '--draft') {
|
|
36
|
+
options.draft = argv[index + 1] || '';
|
|
37
|
+
index += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (item === '--patch') {
|
|
41
|
+
options.patch = argv[index + 1] || '';
|
|
42
|
+
index += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readStructuredFile(filePath) {
|
|
51
|
+
const absolutePath = path.resolve(filePath);
|
|
52
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
53
|
+
if (absolutePath.endsWith('.yaml') || absolutePath.endsWith('.yml')) {
|
|
54
|
+
return YAML.parse(content);
|
|
55
|
+
}
|
|
56
|
+
return JSON.parse(content);
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# 业务模型生成 Prompt
|
|
2
|
+
|
|
3
|
+
目标:根据业务需求说明,输出一个**严格符合 scaffold 业务模型规范**的 YAML 文件。
|
|
4
|
+
|
|
5
|
+
输出要求:
|
|
6
|
+
|
|
7
|
+
1. 只输出 YAML,不要解释。
|
|
8
|
+
2. 顶层必须包含 `meta`、`data`、`app`、`ui`。
|
|
9
|
+
3. `meta.object` 使用英文小写业务对象名,例如 `customer`。
|
|
10
|
+
4. `data.table` 使用下划线命名,例如 `crm_customer`。
|
|
11
|
+
5. 如果存在子表,必须放到 `data.children`,并声明 `foreignKey`。
|
|
12
|
+
6. `app.actions` 只允许使用:
|
|
13
|
+
- `create`
|
|
14
|
+
- `update`
|
|
15
|
+
- `detail`
|
|
16
|
+
- `list`
|
|
17
|
+
- `activate`
|
|
18
|
+
- `deactivate`
|
|
19
|
+
- `submit`
|
|
20
|
+
- `approve`
|
|
21
|
+
- `reject`
|
|
22
|
+
- `close`
|
|
23
|
+
- `archive`
|
|
24
|
+
- `convert`
|
|
25
|
+
7. 如存在 `status` 字段,优先补充 `app.defaultStatus`,必要时补充 `app.statusFlow`。
|
|
26
|
+
8. 如存在稳定业务动作,允许补充 `app.errorCodes`。
|
|
27
|
+
9. `ui.list.columns`、`ui.list.filters`、`ui.form.groups.fields` 中引用的字段,必须都存在于 `data.fields`。
|
|
28
|
+
10. 不允许修改系统对象:
|
|
29
|
+
- `sys_user`
|
|
30
|
+
- `sys_role`
|
|
31
|
+
- `sys_menu`
|
|
32
|
+
|
|
33
|
+
建议输出结构:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
meta:
|
|
37
|
+
domain: crm
|
|
38
|
+
object: customer
|
|
39
|
+
label: 客户
|
|
40
|
+
version: 0.1.0
|
|
41
|
+
|
|
42
|
+
data:
|
|
43
|
+
table: crm_customer
|
|
44
|
+
fields: []
|
|
45
|
+
children: []
|
|
46
|
+
|
|
47
|
+
app:
|
|
48
|
+
actions: [create, update, detail, list]
|
|
49
|
+
|
|
50
|
+
ui:
|
|
51
|
+
list:
|
|
52
|
+
columns: []
|
|
53
|
+
filters: []
|
|
54
|
+
form:
|
|
55
|
+
groups: []
|
|
56
|
+
detail:
|
|
57
|
+
sections: []
|
|
58
|
+
```
|