openyida 2026.9.9 → 2026.9.10
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/lib/app/create-form/args.js +4 -0
- package/lib/app/create-form/batch.js +181 -13
- package/lib/app/create-form.js +45 -14
- package/lib/core/cli-error.js +10 -1
- package/lib/core/command-manifest.js +1 -1
- package/lib/design-plan/init.js +90 -12
- package/lib/design-plan/materialize.js +12 -6
- package/lib/design-plan/normalize.js +88 -1
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/SKILL.md +11 -2
- package/yida-skills/skills/yida-app/workflow/parallel-work.md +3 -1
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +6 -4
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +8 -5
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +4 -0
- package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +3 -1
- package/yida-skills/skills/yida-create-form-page/SKILL.md +52 -1
- package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +18 -2
- package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +3 -1
- package/yida-skills/skills/yida-prd/workflow/plan-business.md +3 -1
- package/yida-skills/skills/yida-requirement-analysis/SKILL.md +9 -8
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +23 -1
|
@@ -143,8 +143,10 @@ function createParseArgs(dependencies) {
|
|
|
143
143
|
if (args.includes('--help') || args.includes('-h')) {
|
|
144
144
|
usage(t('create_form.usage_create_short'));
|
|
145
145
|
hint(t('create_form.usage_update_short'));
|
|
146
|
+
hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
|
|
146
147
|
hint(t('create_form.example_create'));
|
|
147
148
|
hint(t('create_form.example_update'));
|
|
149
|
+
hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
|
|
148
150
|
return {
|
|
149
151
|
mode: 'help',
|
|
150
152
|
help: true,
|
|
@@ -347,8 +349,10 @@ function createParseArgs(dependencies) {
|
|
|
347
349
|
|
|
348
350
|
usage(t('create_form.usage_create_short'));
|
|
349
351
|
hint(t('create_form.usage_update_short'));
|
|
352
|
+
hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
|
|
350
353
|
hint(t('create_form.example_create'));
|
|
351
354
|
hint(t('create_form.example_update'));
|
|
355
|
+
hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
|
|
352
356
|
throwCreateFormError(t('create_form.usage_create_short'), 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
353
357
|
};
|
|
354
358
|
}
|
|
@@ -6,12 +6,50 @@ const crypto = require('crypto');
|
|
|
6
6
|
const { execFile } = require('child_process');
|
|
7
7
|
const { CliError } = require('../../core/cli-error');
|
|
8
8
|
const { t } = require('../../core/i18n');
|
|
9
|
+
const { usage, hint } = require('../../core/chalk');
|
|
10
|
+
const { assertNoEmojiInText, assertNoEmojiInValue } = require('../../core/no-emoji-guard');
|
|
11
|
+
const { normalizeYidaLocale } = require('../../core/yida-i18n');
|
|
12
|
+
const { FORM_NAV_ICONS, resolveFormNavIcon } = require('./nav-icon');
|
|
13
|
+
|
|
14
|
+
const BATCH_USAGE = 'openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]';
|
|
15
|
+
const BATCH_PLAN_EXAMPLE = '{"forms":[{"key":"customer","title":"客户","fieldsFile":"customer-fields.json"},{"key":"order","title":"订单","fieldsFile":"order-fields.json","dependsOn":["customer"]}]}';
|
|
9
16
|
|
|
10
17
|
function invalid(reason) {
|
|
11
18
|
throw new CliError(t('create_form.batch_invalid'), { code: 'FORM_BATCH_INVALID', details: { reason } });
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
function validateStaticDefinition(form, fields) {
|
|
22
|
+
try {
|
|
23
|
+
assertNoEmojiInText(form.title, { artifact: `forms.${form.key}.title` });
|
|
24
|
+
assertNoEmojiInValue(fields, { artifact: `forms.${form.key}.fields` });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
invalid({
|
|
27
|
+
formKey: form.key,
|
|
28
|
+
errorCode: error?.code || 'OPENYIDA_ARTIFACT_EMOJI_FORBIDDEN',
|
|
29
|
+
errorMsg: error?.message || String(error),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (form.icon !== undefined && !resolveFormNavIcon(form.icon, form.title, fields).icon) {
|
|
33
|
+
invalid({
|
|
34
|
+
formKey: form.key,
|
|
35
|
+
errorCode: 'CREATE_FORM_NAV_ICON_INVALID',
|
|
36
|
+
errorMsg: `Unsupported form navigation icon: ${form.icon}`,
|
|
37
|
+
icon: form.icon,
|
|
38
|
+
availableIconCount: FORM_NAV_ICONS.length,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (form.locale !== undefined && !normalizeYidaLocale(form.locale)) {
|
|
42
|
+
invalid({
|
|
43
|
+
formKey: form.key,
|
|
44
|
+
errorCode: 'CREATE_FORM_INVALID_ARGUMENTS',
|
|
45
|
+
errorMsg: `Unsupported locale: ${form.locale}`,
|
|
46
|
+
locale: form.locale,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
function parseArgs(args) {
|
|
52
|
+
if (args.includes('--help') || args.includes('-h')) { return { help: true }; }
|
|
15
53
|
const options = { concurrency: 3, check: false };
|
|
16
54
|
const positional = [];
|
|
17
55
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -25,11 +63,19 @@ function parseArgs(args) {
|
|
|
25
63
|
}
|
|
26
64
|
}
|
|
27
65
|
if (positional.length !== 2 || !Number.isInteger(options.concurrency) || options.concurrency < 1 || options.concurrency > 4) {
|
|
28
|
-
invalid(
|
|
66
|
+
invalid(BATCH_USAGE.replace(/^openyida /, ''));
|
|
29
67
|
}
|
|
30
68
|
return { ...options, appType: positional[0], file: path.resolve(positional[1]) };
|
|
31
69
|
}
|
|
32
70
|
|
|
71
|
+
function showHelp() {
|
|
72
|
+
usage(BATCH_USAGE);
|
|
73
|
+
hint('plan.json: ' + BATCH_PLAN_EXAMPLE);
|
|
74
|
+
hint('fieldsFile is resolved relative to plan.json; use dependsOn and {"$form":"key","field":"label"} for dependencies.');
|
|
75
|
+
hint('Run one authoritative batch. If it continues in background, do not stop it, delete its state, or fall back to create/update/resume.');
|
|
76
|
+
return { success: true, help: true, usage: BATCH_USAGE };
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
// References occupy a complete JSON value; ordinary text is preserved.
|
|
34
80
|
function mapReferences(value, resolve) {
|
|
35
81
|
if (Array.isArray(value)) { return value.map(item => mapReferences(item, resolve)); }
|
|
@@ -42,7 +88,38 @@ function mapReferences(value, resolve) {
|
|
|
42
88
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, mapReferences(item, resolve)]));
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
function
|
|
91
|
+
function normalizeAssociationReferences(value, appType) {
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.map(item => normalizeAssociationReferences(item, appType));
|
|
94
|
+
}
|
|
95
|
+
if (!value || typeof value !== 'object') { return value; }
|
|
96
|
+
const normalized = Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
97
|
+
key,
|
|
98
|
+
normalizeAssociationReferences(item, appType),
|
|
99
|
+
]));
|
|
100
|
+
const association = value.associationForm;
|
|
101
|
+
if (
|
|
102
|
+
value.type === 'AssociationFormField' &&
|
|
103
|
+
association &&
|
|
104
|
+
typeof association === 'object' &&
|
|
105
|
+
!Array.isArray(association) &&
|
|
106
|
+
typeof association.$form === 'string'
|
|
107
|
+
) {
|
|
108
|
+
const reference = { $form: association.$form };
|
|
109
|
+
normalized.associationForm = {
|
|
110
|
+
...(appType ? { appType } : {}),
|
|
111
|
+
formUuid: reference,
|
|
112
|
+
...(typeof association.field === 'string' && association.field.trim() ? {
|
|
113
|
+
mainFieldId: { ...reference, field: association.field },
|
|
114
|
+
mainFieldLabel: association.field,
|
|
115
|
+
mainComponentName: 'TextField',
|
|
116
|
+
} : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadPlan(file, options = {}) {
|
|
46
123
|
const input = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
124
|
if (!Array.isArray(input.forms) || !input.forms.length) { invalid('forms'); }
|
|
48
125
|
const forms = input.forms.map(form => {
|
|
@@ -52,7 +129,9 @@ function loadPlan(file) {
|
|
|
52
129
|
if (Object.keys(form).some(key => !['key', 'title', 'fields', 'fieldsFile', 'dependsOn', 'formUuid', 'icon', 'locale'].includes(key))) { invalid(form.key); }
|
|
53
130
|
if (form.formUuid !== undefined && (typeof form.formUuid !== 'string' || !form.formUuid.startsWith('FORM'))) { invalid(form.key); }
|
|
54
131
|
for (const key of ['icon', 'locale']) { if (form[key] !== undefined && typeof form[key] !== 'string') { invalid(key); } }
|
|
55
|
-
const
|
|
132
|
+
const rawFields = form.fieldsFile === undefined ? form.fields : JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), form.fieldsFile), 'utf8'));
|
|
133
|
+
const fields = normalizeAssociationReferences(rawFields, options.appType);
|
|
134
|
+
validateStaticDefinition(form, fields);
|
|
56
135
|
const dependencies = new Set(form.dependsOn || []);
|
|
57
136
|
mapReferences(fields, key => { dependencies.add(key); return 'FORM-PRECHECK'; });
|
|
58
137
|
return { ...form, fields, dependsOn: [...dependencies] };
|
|
@@ -87,14 +166,75 @@ function parseOutput(stdout) {
|
|
|
87
166
|
return output;
|
|
88
167
|
}
|
|
89
168
|
|
|
90
|
-
function
|
|
169
|
+
function mergeCommandOutput(stdout, stderr, failed) {
|
|
170
|
+
const stdoutOutput = parseOutput(stdout);
|
|
171
|
+
const stderrOutput = parseOutput(stderr);
|
|
172
|
+
if (!failed) { return stdoutOutput || stderrOutput; }
|
|
173
|
+
if (!stdoutOutput) { return stderrOutput; }
|
|
174
|
+
if (!stderrOutput) { return stdoutOutput; }
|
|
175
|
+
return { ...stdoutOutput, ...stderrOutput };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function boundedDiagnostic(value) {
|
|
179
|
+
const text = String(value || '').trim();
|
|
180
|
+
return text.length > 4000 ? text.slice(-4000) : text;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function expectedReadbackFields(fields) {
|
|
184
|
+
const expected = [];
|
|
185
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
186
|
+
if (Array.isArray(field)) {
|
|
187
|
+
expected.push(...expectedReadbackFields(field));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!field || typeof field !== 'object') { continue; }
|
|
191
|
+
if (field.type === 'ColumnContainer') {
|
|
192
|
+
expected.push(...expectedReadbackFields(field.children));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (['Divider', 'RichText', 'PageSection', 'GroupContainer'].includes(field.type)) { continue; }
|
|
196
|
+
if (typeof field.label === 'string' && field.label.trim()) {
|
|
197
|
+
expected.push({ label: field.label.trim(), componentName: field.type });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return expected;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readbackMatchesExpectedFields(schema, fields) {
|
|
204
|
+
if (!schema || typeof schema.formUuid !== 'string' || !Array.isArray(schema.fields)) { return false; }
|
|
205
|
+
const actual = schema.fields.filter(field => field && typeof field === 'object');
|
|
206
|
+
return expectedReadbackFields(fields).every(expected => actual.some(field =>
|
|
207
|
+
field.label === expected.label && (!expected.componentName || field.componentName === expected.componentName)
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readbackMismatch(formUuid, form, schema) {
|
|
212
|
+
const expected = expectedReadbackFields(form.fields);
|
|
213
|
+
const actual = Array.isArray(schema?.fields) ? schema.fields.map(field => ({
|
|
214
|
+
label: field?.label,
|
|
215
|
+
componentName: field?.componentName,
|
|
216
|
+
})) : [];
|
|
217
|
+
return Object.assign(new Error(`Form batch readback mismatch: ${form.key}`), {
|
|
218
|
+
output: {
|
|
219
|
+
success: false,
|
|
220
|
+
errorCode: 'FORM_BATCH_READBACK_MISMATCH',
|
|
221
|
+
errorMsg: `Created form ${form.key} did not contain all expected fields`,
|
|
222
|
+
formUuid,
|
|
223
|
+
details: { formKey: form.key, formUuid, expected, actual },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function execute(args, { execFile: execFileImpl = execFile } = {}) {
|
|
91
229
|
return new Promise((resolve, reject) => {
|
|
92
|
-
|
|
230
|
+
execFileImpl(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
|
|
93
231
|
env: { ...process.env, YIDA_QUIET: '1' }, timeout: 180000, maxBuffer: 8 * 1024 * 1024,
|
|
94
|
-
}, (error, stdout) => {
|
|
95
|
-
const output =
|
|
232
|
+
}, (error, stdout, stderr) => {
|
|
233
|
+
const output = mergeCommandOutput(stdout, stderr, Boolean(error));
|
|
96
234
|
if (error || !output || typeof output !== 'object' || output.success === false) {
|
|
97
|
-
reject(Object.assign(new Error(
|
|
235
|
+
reject(Object.assign(new Error(
|
|
236
|
+
output?.errorMsg || output?.error || output?.message || boundedDiagnostic(stderr) || error?.message || 'Missing command result'
|
|
237
|
+
), { output }));
|
|
98
238
|
} else { resolve(output); }
|
|
99
239
|
});
|
|
100
240
|
});
|
|
@@ -140,7 +280,8 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
140
280
|
|
|
141
281
|
async function run(args, dependencies = {}) {
|
|
142
282
|
const options = parseArgs(args);
|
|
143
|
-
|
|
283
|
+
if (options.help) { return showHelp(); }
|
|
284
|
+
const { forms, groups } = loadPlan(options.file, { appType: options.appType });
|
|
144
285
|
const call = dependencies.execute || execute;
|
|
145
286
|
// Preflight all definitions before the first remote mutation.
|
|
146
287
|
await schedule(forms.map(form => ({ ...form, dependsOn: [] })), options.concurrency, {}, async form => {
|
|
@@ -202,14 +343,28 @@ async function run(args, dependencies = {}) {
|
|
|
202
343
|
}
|
|
203
344
|
state.results[form.key].formUuid = formUuid;
|
|
204
345
|
save(state);
|
|
346
|
+
let resumed = false;
|
|
205
347
|
if (shouldResume) {
|
|
206
348
|
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
349
|
+
resumed = true;
|
|
350
|
+
}
|
|
351
|
+
let schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
352
|
+
if (!readbackMatchesExpectedFields(schema, resolvedFields) && !resumed && !form.formUuid) {
|
|
353
|
+
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
354
|
+
resumed = true;
|
|
355
|
+
schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
356
|
+
}
|
|
357
|
+
if (schema.formUuid !== formUuid || !readbackMatchesExpectedFields(schema, resolvedFields)) {
|
|
358
|
+
throw readbackMismatch(formUuid, { ...form, fields: resolvedFields }, schema);
|
|
207
359
|
}
|
|
208
|
-
const schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
209
|
-
if (schema.formUuid !== formUuid || !Array.isArray(schema.fields)) { invalid(`schema: ${form.key}`); }
|
|
210
360
|
return { formUuid, fields: schema.fields };
|
|
211
361
|
}, () => save(state));
|
|
212
|
-
const
|
|
362
|
+
const success = Object.values(state.results).every(item => item.status === 'success');
|
|
363
|
+
const output = { success, groups, stateFile, results: state.results };
|
|
364
|
+
if (!success) {
|
|
365
|
+
output.errorCode = 'FORM_BATCH_PARTIAL_FAILURE';
|
|
366
|
+
output.nextAction = 'Inspect the saved state and child error, then fix the batch input or recover known formUuid values. Do not fall back to create-form create.';
|
|
367
|
+
}
|
|
213
368
|
console.log(JSON.stringify(output));
|
|
214
369
|
if (!output.success) { process.exitCode = 1; }
|
|
215
370
|
return output;
|
|
@@ -218,4 +373,17 @@ async function run(args, dependencies = {}) {
|
|
|
218
373
|
}
|
|
219
374
|
}
|
|
220
375
|
|
|
221
|
-
module.exports = {
|
|
376
|
+
module.exports = {
|
|
377
|
+
run,
|
|
378
|
+
parseArgs,
|
|
379
|
+
loadPlan,
|
|
380
|
+
mapReferences,
|
|
381
|
+
normalizeAssociationReferences,
|
|
382
|
+
schedule,
|
|
383
|
+
parseOutput,
|
|
384
|
+
mergeCommandOutput,
|
|
385
|
+
execute,
|
|
386
|
+
expectedReadbackFields,
|
|
387
|
+
readbackMatchesExpectedFields,
|
|
388
|
+
validateStaticDefinition,
|
|
389
|
+
};
|
package/lib/app/create-form.js
CHANGED
|
@@ -123,6 +123,29 @@ function createCreateFormError(message, code, details) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function createFormPreflightError(errorObject, appType, formTitle) {
|
|
127
|
+
const error = errorObject && errorObject.isCliError
|
|
128
|
+
? errorObject
|
|
129
|
+
: createCreateFormError(
|
|
130
|
+
errorObject?.message || String(errorObject || 'Create form preflight failed'),
|
|
131
|
+
'CREATE_FORM_PREFLIGHT_FAILED'
|
|
132
|
+
);
|
|
133
|
+
const existing = error.details && typeof error.details === 'object' && !Array.isArray(error.details)
|
|
134
|
+
? error.details
|
|
135
|
+
: {};
|
|
136
|
+
error.details = {
|
|
137
|
+
...existing,
|
|
138
|
+
stage: 'preflight',
|
|
139
|
+
retryable: true,
|
|
140
|
+
retrySafe: true,
|
|
141
|
+
sideEffectState: 'not_started',
|
|
142
|
+
mutationAccepted: false,
|
|
143
|
+
mutationPerformed: false,
|
|
144
|
+
target: { resource: 'form', appType, formTitle },
|
|
145
|
+
};
|
|
146
|
+
return error;
|
|
147
|
+
}
|
|
148
|
+
|
|
126
149
|
const parseArgs = createParseArgs({
|
|
127
150
|
parseOpenOption,
|
|
128
151
|
normalizeYidaLocale,
|
|
@@ -4902,20 +4925,28 @@ async function mainCreate(parsedArgs, authRef) {
|
|
|
4902
4925
|
label('Fields:', fieldsJsonOrFile);
|
|
4903
4926
|
|
|
4904
4927
|
step(2, t('create_form.step_read_fields', 2));
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4928
|
+
let fields;
|
|
4929
|
+
let columns;
|
|
4930
|
+
let validations;
|
|
4931
|
+
let iconResolution;
|
|
4932
|
+
try {
|
|
4933
|
+
assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
|
|
4934
|
+
({ fields, columns, validations } = readFieldsDefinition(fieldsJsonOrFile));
|
|
4935
|
+
assertNoEmojiInFormDefinition(formTitle, fields, validations);
|
|
4936
|
+
validateFormFieldDefinitions(fields);
|
|
4937
|
+
iconResolution = resolveFormNavIcon(parsedArgs.icon, formTitle, fields);
|
|
4938
|
+
if (!iconResolution.icon) {
|
|
4939
|
+
throwCreateFormError(
|
|
4940
|
+
`Unsupported form navigation icon: ${parsedArgs.icon}. Run openyida create-form icons --json for the supported catalog.`,
|
|
4941
|
+
'CREATE_FORM_NAV_ICON_INVALID',
|
|
4942
|
+
{
|
|
4943
|
+
icon: parsedArgs.icon,
|
|
4944
|
+
availableIconCount: FORM_NAV_ICONS.length,
|
|
4945
|
+
}
|
|
4946
|
+
);
|
|
4947
|
+
}
|
|
4948
|
+
} catch (preflightError) {
|
|
4949
|
+
throw createFormPreflightError(preflightError, appType, formTitle);
|
|
4919
4950
|
}
|
|
4920
4951
|
const formIcon = iconResolution.icon;
|
|
4921
4952
|
const fieldCount = countDataFieldDefinitions(fields);
|
package/lib/core/cli-error.js
CHANGED
|
@@ -90,8 +90,17 @@ function shouldUseStructuredErrorOutput(error, args = []) {
|
|
|
90
90
|
&& details.retrySafe === false
|
|
91
91
|
&& details.sideEffectState === 'unknown'
|
|
92
92
|
);
|
|
93
|
+
const deterministicPreflightFailure = !!(
|
|
94
|
+
details
|
|
95
|
+
&& typeof details === 'object'
|
|
96
|
+
&& !Array.isArray(details)
|
|
97
|
+
&& details.stage === 'preflight'
|
|
98
|
+
&& details.retrySafe === true
|
|
99
|
+
&& details.sideEffectState === 'not_started'
|
|
100
|
+
&& details.mutationPerformed === false
|
|
101
|
+
);
|
|
93
102
|
const navigationOrderFailure = /^NAV_ORDER_/.test(error.code || '');
|
|
94
|
-
return ownedPartial || mutationOutcomeUnknown || navigationOrderFailure;
|
|
103
|
+
return ownedPartial || mutationOutcomeUnknown || deterministicPreflightFailure || navigationOrderFailure;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
module.exports = {
|
|
@@ -1556,7 +1556,7 @@ function summarizeLocalizedCommands(commands) {
|
|
|
1556
1556
|
theme_command_ids: ['sample', 'create-app', 'update-app'],
|
|
1557
1557
|
navigation_command_ids: { platform: ['update-app', 'nav-group'], custom: ['update-app', 'update-form-config', 'get-form-config'] },
|
|
1558
1558
|
navigation_policy: 'Before PRD planning, reuse explicit navigation choices. During first-time intake, ask only when unknown: platform-l-shape, platform-top, platform-side, custom, with neutral descriptions and no preselection. For custom navigation also settle side/top/mixed/dock. Store type/source/reason/variant in the brief. Custom navigation requires app and per-page navigation hiding with readback; layout and navigation tone are separate choices.',
|
|
1559
|
-
design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved navigation, custom navigation layout, visual style and page scope before planning or resource creation. Existing business apps only clarify the current change.
|
|
1559
|
+
design_mode_policy: 'Analyze requirements first. First-time builds include new apps and existing apps without business pages. Reuse detailed supplied plans; if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. Confirm unresolved navigation, custom navigation layout, visual style and page scope before planning or resource creation. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false skips unrelated navigation and visual questions. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Confirm the displayed revision before building, and do not materialize or patch again after confirmation.',
|
|
1560
1560
|
product_design_policy: 'yida-requirement-analysis owns shared facts and first-time intake. yida-prd owns business planning; yida-design owns visual design. Reuse the same confirmed brief and supplied details. Prepare business and base visuals concurrently, then bind visuals to settled page tasks. yida-app merges and checks the artifacts before creating resources. Plan uses the compact authoring contract and selected theme context; the CLI reads full templates and renders all artifacts.',
|
|
1561
1561
|
ui_guidance_policy: 'Page implementation consumes yida-prd prd.md for positioning, information architecture, page prototype, native form entry policy, material strategy, business-specific checks, resource creation order, page implementation delivery order, navigation order, and acceptance criteria; it consumes yida-design design.md for app custom theme CSS delivery, themeColor/navTheme, visual states, visualScaffold, surface material, rounded rules, density rules, components, and state styling. prd.md and design.md are the only design sources of truth. Page implementation may extract page-spec.json as a derived implementation handoff from prd.md + design.md; conflicts are resolved by sending business conflicts to yida-prd and visual conflicts to yida-design before regenerating the spec. Core normal forms default to 1-3 business sample records before page implementation, followed by query readback. An explicit opt-out, configuration dictionary, sensitive data, or lack of safely constructible values requires a recorded skip reason. Screenshots, public sharing, data-source deep binding, and fine navigation grouping are optional after explicit user request or PRD acceptance criteria.',
|
|
1562
1562
|
default_nav_order_policy: 'For custom navigation, implement PRD navigation order in the custom shell and verify app/page navigation hiding; skip platform nav-group ordering. For platform navigation: after the primary page is successfully published, perform exactly one navigation order operation. If the PRD names a navigation order, publish without --auto-nav-order and then call openyida nav-group order <appType> <items...>. If the PRD only gives broad groups or is missing navigation order, use openyida publish ... --auto-nav-order and do not call nav-group order or auto-order afterward. Explicit and automatic ordering are mutually exclusive; never generate per-item move loops. The fallback priority is portal/home/workbench entry > business handling > data management > business analytics > system configuration.',
|
package/lib/design-plan/init.js
CHANGED
|
@@ -8,14 +8,49 @@ const { planBase } = require('./parallel');
|
|
|
8
8
|
|
|
9
9
|
const ROOT = path.resolve(__dirname, '../../yida-skills/skills/yida-design/sub_skill/yida-design-plan');
|
|
10
10
|
|
|
11
|
+
function list(value) {
|
|
12
|
+
return Array.isArray(value) ? value : [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function customScene(scene) {
|
|
16
|
+
return !['form', 'process-form', 'report'].includes(scene?.kind);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resourceOnlyScope(brief) {
|
|
20
|
+
if (brief.explicitScope?.allowInferredResources === false) {return true;}
|
|
21
|
+
const scopedForms = list(brief.explicitScope?.forms);
|
|
22
|
+
const confirmedCustomPages = list(brief.pageScenes).filter(customScene);
|
|
23
|
+
return scopedForms.length > 0 && confirmedCustomPages.length === 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scopeEntryMatchesScene(entry, scene) {
|
|
27
|
+
const candidate = typeof entry === 'string' ? entry : entry?.key || entry?.name;
|
|
28
|
+
return candidate && [scene?.key, scene?.name].includes(candidate);
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
function initialize(inputPath, options = {}) {
|
|
12
32
|
const input = path.resolve(inputPath);
|
|
13
33
|
const brief = readJson(input, 'requirement-brief.json');
|
|
14
34
|
const project = brief.projectName;
|
|
35
|
+
if ((project === undefined || project === null || project === '')
|
|
36
|
+
&& typeof brief.meta?.projectName === 'string'
|
|
37
|
+
&& brief.meta.projectName.trim()) {
|
|
38
|
+
throw new CliError('projectName 必须位于 requirement-brief.json 根级;检测到 meta.projectName,请将该字段移动到根级后重试,不要更换项目名', {
|
|
39
|
+
code: 'DESIGN_PLAN_PROJECT_NAME_MISPLACED',
|
|
40
|
+
details: {
|
|
41
|
+
expectedPath: 'projectName',
|
|
42
|
+
actualPath: 'meta.projectName',
|
|
43
|
+
nextAction: 'move_project_name_to_root',
|
|
44
|
+
retryable: true,
|
|
45
|
+
retrySafe: true,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
15
49
|
if (typeof project !== 'string' || !project.trim() || /[/\\]/.test(project) || ['.', '..'].includes(project)) {
|
|
16
50
|
throw new CliError('projectName 必须是单层项目目录名', { code: 'DESIGN_PLAN_INVALID_PROJECT' });
|
|
17
51
|
}
|
|
18
|
-
|
|
52
|
+
const resourceOnly = resourceOnlyScope(brief);
|
|
53
|
+
if ((!resourceOnly && brief.intake?.confirmed !== true) || brief.openQuestions?.length) {
|
|
19
54
|
throw new CliError('请先完成需求确认并记录 intake.confirmed=true', { code: 'DESIGN_PLAN_INTAKE_REQUIRED' });
|
|
20
55
|
}
|
|
21
56
|
if (!['platform-l-shape', 'platform-top', 'platform-side', 'custom'].includes(brief.navigation?.type)
|
|
@@ -38,7 +73,10 @@ function initialize(inputPath, options = {}) {
|
|
|
38
73
|
if ([output, guide, business, visual].some(file => fs.existsSync(file))) {
|
|
39
74
|
throw new CliError('计划或编写上下文已存在,请继续补充现有文件', { code: 'DESIGN_PLAN_ALREADY_EXISTS' });
|
|
40
75
|
}
|
|
41
|
-
const
|
|
76
|
+
const scopedPages = list(brief.explicitScope?.pages);
|
|
77
|
+
const scenes = list(brief.pageScenes)
|
|
78
|
+
.filter(customScene)
|
|
79
|
+
.filter(scene => !resourceOnly || scopedPages.some(entry => scopeEntryMatchesScene(entry, scene)));
|
|
42
80
|
const pages = scenes.map((scene, index) => {
|
|
43
81
|
const source = typeof scene === 'string' ? { name: scene } : scene;
|
|
44
82
|
const { key, ...facts } = source;
|
|
@@ -49,19 +87,49 @@ function initialize(inputPath, options = {}) {
|
|
|
49
87
|
density: '', permissionSummary: '', dataBinding: '', dataSources: [], ...facts };
|
|
50
88
|
});
|
|
51
89
|
const choice = brief.visualSelection || {};
|
|
52
|
-
const app = brief.resourceContext?.app || {};
|
|
90
|
+
const app = brief.resourceContext?.app || brief.resourceContext || {};
|
|
53
91
|
const appConfig = { navigationType: brief.navigation.type };
|
|
54
92
|
for (const key of ['appType', 'corpId', 'baseUrl']) {if (app[key]) {appConfig[key] = app[key];}}
|
|
93
|
+
const businessGoals = Array.isArray(brief.businessGoals)
|
|
94
|
+
? brief.businessGoals
|
|
95
|
+
: (typeof brief.businessGoals === 'string' && brief.businessGoals.trim() ? [brief.businessGoals] : []);
|
|
96
|
+
const modelInputs = (resourceOnly && list(brief.explicitScope?.forms).length > 0
|
|
97
|
+
? list(brief.explicitScope.forms)
|
|
98
|
+
: list(brief.businessObjects)).map(item => {
|
|
99
|
+
if (typeof item !== 'string') {return item;}
|
|
100
|
+
return list(brief.businessObjects).find(candidate => candidate?.name === item) || item;
|
|
101
|
+
});
|
|
102
|
+
const normalizedScope = {
|
|
103
|
+
...brief.explicitScope,
|
|
104
|
+
...(resourceOnly ? {
|
|
105
|
+
allowInferredResources: false,
|
|
106
|
+
pages: scopedPages.filter(entry => scenes.some(scene => scopeEntryMatchesScene(entry, scene))),
|
|
107
|
+
} : {}),
|
|
108
|
+
};
|
|
109
|
+
const rolePermissionSummary = list(brief.targetUsers)
|
|
110
|
+
.map(user => typeof user === 'string' ? `${user}:使用本轮范围内已授权资源` : '')
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
if (resourceOnly && rolePermissionSummary.length === 0) {
|
|
113
|
+
rolePermissionSummary.push('应用成员:使用本轮范围内已授权资源');
|
|
114
|
+
}
|
|
55
115
|
const plan = {
|
|
56
116
|
schemaVersion: '2.0',
|
|
57
|
-
meta: { projectName: project, appName: brief.appName || project, revision: '1', status: 'draft', source: input,
|
|
117
|
+
meta: { projectName: project, appName: brief.appName || brief.projectDisplayName || brief.meta?.displayName || project, revision: '1', status: 'draft', source: input,
|
|
58
118
|
businessDomain: brief.industry || '', experienceTopology: '',
|
|
59
119
|
planState: { planConfirmed: false, presentedRevision: null, confirmedRevision: null } },
|
|
60
|
-
overview: { summary:
|
|
61
|
-
dataModels:
|
|
120
|
+
overview: { summary: businessGoals.join(';'), businessGraph: { relations: [] }, rolePermissionSummary },
|
|
121
|
+
dataModels: modelInputs.map(item => {
|
|
62
122
|
const model = typeof item === 'string' ? { name: item } : item;
|
|
63
123
|
return { ...model, name: model.name, formType: model.formType || '宜搭表单', description: model.description || '', fields: Array.isArray(model.fields) ? model.fields : [] };
|
|
64
|
-
}), businessFlows: [], pages: { customPageDetails: pages }, execution: {
|
|
124
|
+
}), businessFlows: [], pages: { customPageDetails: pages }, execution: {
|
|
125
|
+
appConfig,
|
|
126
|
+
explicitScope: { ...normalizedScope, navigation: brief.navigation },
|
|
127
|
+
...(resourceOnly ? {
|
|
128
|
+
sampleDataPlan: modelInputs
|
|
129
|
+
.filter(item => !String(item?.formType || '').includes('流程'))
|
|
130
|
+
.map(item => ({ form: typeof item === 'string' ? item : item.name, skipReason: '本轮显式窄范围不包含示例数据' })),
|
|
131
|
+
} : {}),
|
|
132
|
+
},
|
|
65
133
|
visualStyle: {
|
|
66
134
|
...(choice.tokens ? { tokens: choice.tokens } : {}),
|
|
67
135
|
...(choice.evidence ? { evidence: choice.evidence } : {}),
|
|
@@ -82,14 +150,23 @@ function initialize(inputPath, options = {}) {
|
|
|
82
150
|
const patterns = readJson(path.join(ROOT, 'templates/page-patterns/index.json')).patterns;
|
|
83
151
|
const context = [
|
|
84
152
|
'# 计划编写上下文', '', `需求事实:${input}`, `计划目录:${outputDir}`, '',
|
|
85
|
-
|
|
153
|
+
`先读 build-plan-compact-schema.md,再读 init 已创建的 business.json;保留 base 并一次补完 facts 后写回。business facts 只能包含 overview、dataModels、businessFlows、pages 和可选 execution,不得写 visualStyle。写完后只执行一次:openyida design-plan materialize ${output} --business-file ${business} --visual-file ${visual} --json。标准首版禁止先调用 --from-preview、preview、--check 或无参数 materialize,也不要用 Glob 或额外 Read 检查产物;成功结果已返回 HTML 路径和 revision。只有视觉选择不完整或存在品牌稿、参考图、逐页特殊视觉要求时才补充视觉。保留页面 ID 与已有需求细节。`, '',
|
|
86
154
|
'## 页面模式', ...patterns.map(item => `- ${item.id}(${item.label}):${item.mustKeep.join(';')}`), '',
|
|
87
155
|
`## 已选风格:${theme.label}`, overview.trim(), '',
|
|
88
156
|
'用户确认的配色优先于模板固定灰阶:surfaceTone=brand-tinted 将浅色页面、填充和边界与主色协调,正文保留中性层级;用户明确只改强调色、保持中性或忠实参考配色时设为 theme。深色表面保留原层级。不要为了模板的无彩限制撤掉用户要求的品牌氛围。', '',
|
|
89
157
|
'完整主题的 token、组件、状态和响应式规则由 CLI 注入 design.md;调整具体组件时按需读取对应模板章节。', '',
|
|
90
158
|
].join('\n');
|
|
91
159
|
const base = planBase(plan);
|
|
92
|
-
const
|
|
160
|
+
const businessFacts = Object.fromEntries(
|
|
161
|
+
['overview', 'dataModels', 'businessFlows', 'pages', 'execution'].map(key => [key, plan[key]]),
|
|
162
|
+
);
|
|
163
|
+
const businessReady = resourceOnly
|
|
164
|
+
&& businessGoals.length > 0
|
|
165
|
+
&& rolePermissionSummary.length > 0
|
|
166
|
+
&& plan.dataModels.length > 0
|
|
167
|
+
&& plan.dataModels.every(model => model.name && list(model.fields).length > 0
|
|
168
|
+
&& model.fields.every(field => field?.name && field?.type && typeof field.required === 'boolean'));
|
|
169
|
+
const businessPart = `${JSON.stringify({ base, ready: businessReady, facts: businessFacts }, null, 2)}\n`;
|
|
93
170
|
// The intake already records an atomic visual selection. Seed it here so a
|
|
94
171
|
// standard first Plan only needs one model planning pass for business facts.
|
|
95
172
|
const selectedVisual = plan.visualStyle.forUser;
|
|
@@ -100,10 +177,11 @@ function initialize(inputPath, options = {}) {
|
|
|
100
177
|
const visualPart = `${JSON.stringify({ base, ready: visualReady, facts: { visualStyle: plan.visualStyle } }, null, 2)}\n`;
|
|
101
178
|
writeFiles([[output, `${JSON.stringify(plan, null, 2)}\n`], [guide, context], [business, businessPart], [visual, visualPart]]);
|
|
102
179
|
return { success: true, input, output, context: guide, contract: path.join(ROOT, 'references/build-plan-compact-schema.md'), draft: true,
|
|
103
|
-
|
|
104
|
-
|
|
180
|
+
materialize: { mode: 'complete_files_once', maxCalls: 1,
|
|
181
|
+
command: `openyida design-plan materialize ${output} --business-file ${business} --visual-file ${visual} --json` },
|
|
182
|
+
parallelTasks: [...(!businessReady ? [{ id: 'business', skill: 'yida-prd', output: business, dependsOn: [] }] : []),
|
|
105
183
|
...(!visualReady ? [{ id: 'visual-selection', skill: 'yida-design', output: visual, dependsOn: [] }] : [])],
|
|
106
|
-
preparedInputs: { visual, visualReady },
|
|
184
|
+
preparedInputs: { business, businessReady, visual, visualReady },
|
|
107
185
|
optionalTasks: [{ id: 'visual-refinement', skill: 'yida-design', input: business, output: visual, dependsOn: ['business'],
|
|
108
186
|
when: 'brand material, reference images, or explicit page-specific visual differences require refinement' }],
|
|
109
187
|
};
|
|
@@ -253,6 +253,7 @@ function buildExecution(plan) {
|
|
|
253
253
|
const models = plan.dataModels || [];
|
|
254
254
|
const { navigationStyle } = visualSelections(plan);
|
|
255
255
|
const execution = plan.execution === undefined ? {} : plan.execution;
|
|
256
|
+
const resourceOnly = execution.explicitScope?.allowInferredResources === false;
|
|
256
257
|
validateBusiness(plan, execution);
|
|
257
258
|
const visualConfig = { navTheme: navigationStyle.tone, logoSource: 'appIcon' };
|
|
258
259
|
const navigation = navigationConfig(plan);
|
|
@@ -272,16 +273,20 @@ function buildExecution(plan) {
|
|
|
272
273
|
];
|
|
273
274
|
const handoff = {
|
|
274
275
|
resourceBlueprint: resources,
|
|
275
|
-
resourceCreationOrder:
|
|
276
|
+
resourceCreationOrder: resourceOnly
|
|
277
|
+
? [...models.map(model => model.name), ...pages.map(page => page.name)]
|
|
278
|
+
: ['应用与主题配置', ...models.map(model => model.name), '初始示例数据', ...pages.map(page => page.name), '发布与导航排序'],
|
|
276
279
|
pageImplementationOrder: pages.map(page => page.pageId || page.name),
|
|
277
280
|
navigationOrder: [],
|
|
278
|
-
navigationFallback: '发布时使用 --auto-nav-order,不再重复执行排序',
|
|
281
|
+
navigationFallback: resourceOnly ? '本轮显式窄范围不执行导航排序' : '发布时使用 --auto-nav-order,不再重复执行排序',
|
|
279
282
|
acceptanceCriteria: [
|
|
280
283
|
...models.map(model => `${model.name}的字段、必填规则与关系符合数据模型`),
|
|
281
284
|
...(plan.businessFlows || []).map(flow => `${flow.name}按触发条件和业务规则执行`),
|
|
282
285
|
...pages.map(page => `${page.name}支持${page.primaryTask || page.positioning}`),
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
...(!resourceOnly ? [
|
|
287
|
+
'应用主题按 design.md 配置,页面消费同一组 token',
|
|
288
|
+
'核心普通表单示例数据写入并抽查,或说明跳过原因',
|
|
289
|
+
] : []),
|
|
285
290
|
],
|
|
286
291
|
...execution,
|
|
287
292
|
interactionStates: { ...interactionStates, ...execution.interactionStates },
|
|
@@ -802,8 +807,9 @@ function prepareArtifacts(sourcePlan, outputDir, themeSeed) {
|
|
|
802
807
|
} finally {timings[name] = Math.round((performance.now() - start) * 100) / 100;}
|
|
803
808
|
};
|
|
804
809
|
const plan = measure('validationAndNormalizationMs', () => {
|
|
805
|
-
|
|
806
|
-
|
|
810
|
+
const normalized = normalizePlan(sourcePlan);
|
|
811
|
+
authoringIssues = validateAuthoring(normalized);
|
|
812
|
+
return normalized;
|
|
807
813
|
});
|
|
808
814
|
const execution = measure('businessValidationMs', () => buildExecution(plan));
|
|
809
815
|
const prd = measure('prdMs', () => renderPrd(plan, execution));
|
|
@@ -28,6 +28,90 @@ function compactSchema(plan) {
|
|
|
28
28
|
return /^2(?:\.|$)/.test(String(plan.schemaVersion || ''));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function normalizeEquivalentAuthoring(plan) {
|
|
32
|
+
const summary = plan.overview?.rolePermissionSummary;
|
|
33
|
+
if (Array.isArray(summary)) {
|
|
34
|
+
plan.overview.rolePermissionSummary = summary.map((entry) => {
|
|
35
|
+
if (typeof entry === 'string') {return entry;}
|
|
36
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {return entry;}
|
|
37
|
+
const role = entry.role || entry.name || '';
|
|
38
|
+
const permission = entry.permissions || entry.permission || entry.scope || entry.description || '';
|
|
39
|
+
return [role, permission].filter(Boolean).join(':');
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const model of list(plan.dataModels)) {
|
|
44
|
+
const fields = list(model.fields);
|
|
45
|
+
for (const field of fields) {
|
|
46
|
+
if (!field?.type && typeof field?.fieldType === 'string') {field.type = field.fieldType;}
|
|
47
|
+
if (field && Object.prototype.hasOwnProperty.call(field, 'fieldType')) {delete field.fieldType;}
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(model.sampleRecords)) {
|
|
50
|
+
model.sampleRecords = model.sampleRecords.map((record) => {
|
|
51
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {return record;}
|
|
52
|
+
const values = record.fields && typeof record.fields === 'object' && !Array.isArray(record.fields)
|
|
53
|
+
? record.fields
|
|
54
|
+
: record;
|
|
55
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => {
|
|
56
|
+
const field = fields.find(item => item?.name === key || item?.key === key);
|
|
57
|
+
return [field?.name || key, value];
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
} else if (
|
|
61
|
+
model.sampleRecords
|
|
62
|
+
&& typeof model.sampleRecords === 'object'
|
|
63
|
+
&& !Array.isArray(model.sampleRecords)
|
|
64
|
+
&& typeof model.sampleRecords.skipReason === 'string'
|
|
65
|
+
&& model.sampleRecords.skipReason.trim()
|
|
66
|
+
) {
|
|
67
|
+
model.skipSampleReason = model.sampleRecords.skipReason.trim();
|
|
68
|
+
delete model.sampleRecords;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const execution = plan.execution = plan.execution || {};
|
|
73
|
+
const scope = execution.explicitScope;
|
|
74
|
+
if (scope?.allowInferredResources === false && scope.sampleData === undefined) {
|
|
75
|
+
execution.sampleDataPlan = list(plan.dataModels)
|
|
76
|
+
.filter(model => !String(model?.formType || '').includes('流程'))
|
|
77
|
+
.map(model => ({ form: model.name, skipReason: '本轮显式窄范围不包含示例数据' }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (typeof execution.appConfig?.hideAppNav === 'boolean') {
|
|
81
|
+
execution.appConfig.hideAppNav = execution.appConfig.hideAppNav ? 'y' : 'n';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const modelNames = list(plan.dataModels).map(model => model?.name).filter(Boolean);
|
|
85
|
+
for (const page of list(plan.pages?.customPageDetails)) {
|
|
86
|
+
if (page.dataBinding && typeof page.dataBinding === 'object' && !Array.isArray(page.dataBinding)) {
|
|
87
|
+
page.dataSources = list(page.dataSources).length > 0 ? page.dataSources : list(page.dataBinding.dataSources);
|
|
88
|
+
page.dataBinding = page.dataBinding.mode || page.dataBinding.type || '';
|
|
89
|
+
}
|
|
90
|
+
page.dataSources = list(page.dataSources).map(source => {
|
|
91
|
+
const candidate = typeof source === 'string' ? source : source?.source || source?.name;
|
|
92
|
+
if (typeof candidate !== 'string') {return candidate;}
|
|
93
|
+
const trimmed = candidate.trim();
|
|
94
|
+
return modelNames.find(name => trimmed === name || trimmed === `${name}表单`) || trimmed;
|
|
95
|
+
});
|
|
96
|
+
if (
|
|
97
|
+
!['form', 'report', 'connector', 'static-empty'].includes(page.dataBinding)
|
|
98
|
+
&& page.dataSources.length > 0
|
|
99
|
+
&& page.dataSources.every(source => modelNames.includes(source))
|
|
100
|
+
) {
|
|
101
|
+
page.dataBinding = 'form';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const navigation = plan.visualStyle?.forUser?.navigationStyle;
|
|
106
|
+
if (navigation) {
|
|
107
|
+
navigation.structure = {
|
|
108
|
+
'platform-l-shape': 'side',
|
|
109
|
+
'platform-side': 'side',
|
|
110
|
+
'platform-top': 'top',
|
|
111
|
+
}[navigation.structure] || navigation.structure;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
31
115
|
function pageType(formType) {
|
|
32
116
|
if (String(formType || '').includes('流程')) {
|
|
33
117
|
return '宜搭流程表单';
|
|
@@ -75,7 +159,9 @@ function ensureOverview(plan) {
|
|
|
75
159
|
if (!list(overview.businessGraph.nodes).length) {
|
|
76
160
|
overview.businessGraph.nodes = deriveGraphNodes(plan.dataModels);
|
|
77
161
|
}
|
|
78
|
-
|
|
162
|
+
const modelNames = new Set(list(plan.dataModels).map(model => model?.name).filter(Boolean));
|
|
163
|
+
overview.businessGraph.relations = list(overview.businessGraph.relations)
|
|
164
|
+
.filter(relation => relation && modelNames.has(relation.from) && modelNames.has(relation.to));
|
|
79
165
|
if (!list(overview.dataModelSummary).length) {
|
|
80
166
|
overview.dataModelSummary = list(plan.dataModels).map(model => `${model.name}:${model.description || '核心业务数据'}`);
|
|
81
167
|
}
|
|
@@ -259,6 +345,7 @@ function normalizePlan(sourcePlan) {
|
|
|
259
345
|
return sourcePlan;
|
|
260
346
|
}
|
|
261
347
|
const plan = clone(sourcePlan);
|
|
348
|
+
normalizeEquivalentAuthoring(plan);
|
|
262
349
|
const themeIndex = readJson(THEME_INDEX_PATH, '主题索引');
|
|
263
350
|
const pagePatternIndex = readJson(PAGE_PATTERN_INDEX_PATH, '页面模式索引');
|
|
264
351
|
ensureOverview(plan);
|
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
|
|
|
7
7
|
|
|
8
8
|
完整应用编排技能。它负责把一次“创建/搭建/补齐应用”的需求拆成资源解析、产品设计、资源落地、页面发布和结果输出。全局 CLI、ID、存储、发布和输出规则以主入口 `SKILL.md` 为准;按步骤执行该步骤所需 `use_skill(...)`。
|
|
9
9
|
|
|
10
|
+
## 模式入口(先按这里路由)
|
|
11
|
+
|
|
12
|
+
Plan 模式只读取 `workflow/step-1-resource-context.md`、`workflow/step-2-design.md` 和精确路径 `workflow/plan/workflow.md`;后者已经包含完整 Plan 入口。禁止用 Glob 查找 Plan 文件,也不要额外读取 `workflow/plan/step-1-understand.md` 或 `workflow/plan/step-2-confirm.md`。Plan 确认恢复后,若 `explicitScope.allowInferredResources=false`,直接读取 `workflow/step-4-forms-processes.md` 实施范围内资源,不再重读 Step 1、调用 list-forms 或做应用设置预检。
|
|
13
|
+
|
|
10
14
|
## 步骤模版(进行时展示给用户看的步骤)
|
|
11
15
|
|
|
12
16
|
完整应用搭建时,直接使用对应模式的步骤名称。
|
|
@@ -36,6 +40,10 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
|
|
|
36
40
|
|
|
37
41
|
已有应用时,直接省略“创建应用”,不另列“复用现有应用”待办,后续步骤重新编号;无需示例数据时跳过对应步骤。步骤状态按真实进度更新,有独立输入的工作可同时进行。
|
|
38
42
|
|
|
43
|
+
用户明确把本轮交付限定为一个或若干具体表单、流程、报表或页面时,按 `explicitScope` 只保留达到该交付所需的步骤;即使需求背景使用“应用/系统”,也不自动补示例数据、自定义工作台、主题设置、导航排序或其他资源。Plan 模式仍生成并确认方案,但确认后只执行该窄范围。
|
|
44
|
+
|
|
45
|
+
若窄范围只要求创建一个普通表单并交付链接,成功的 `create-form create` 结果就是本轮资源回读证据:立即交付其中的真实链接并停止。不要再调用 `get-schema`、`list-forms`、数据管理技能、示例数据、主题或导航命令,除非创建结果明确缺少 ID/链接或用户另外要求这些内容。
|
|
46
|
+
|
|
39
47
|
禁区:待办标题、说明和进度不出现技能名、命令、文件路径、登录账号、内部资源 ID;这些留在工具调用里。用户明确询问技术细节时再解释。
|
|
40
48
|
|
|
41
49
|
## 触发条件
|
|
@@ -60,7 +68,7 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
|
|
|
60
68
|
| 8 | [发布页面并排序导航](workflow/step-8-publish-navigation.md) | 执行 `use_skill("yida-publish-page")`,发布本轮源码到主页面并执行轻量导航排序 | 已发布主页面 URL |
|
|
61
69
|
| 9 | [输出与收尾](workflow/step-9-output-finish.md) | 核对完成条件,按业务语言输出结果 | 2-3 句业务总结 + 一组应用访问入口 |
|
|
62
70
|
|
|
63
|
-
独立工作按 [并行执行](workflow/parallel-work.md)
|
|
71
|
+
独立工作按 [并行执行](workflow/parallel-work.md) 调度,任务分别产出,由主流程汇合。完整应用在计划或主题确认后生成主题 CSS,appType 和 CSS 就绪就同步应用设置;`explicitScope.allowInferredResources=false` 的资源级交付不生成或上传主题、不修改应用设置、不排序导航。
|
|
64
72
|
|
|
65
73
|
## 核心规则
|
|
66
74
|
|
|
@@ -68,11 +76,12 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
|
|
|
68
76
|
2. **显式目标优先**:本轮用户给出的 `appType`、`formUuid`、URL、页面名或流程标识,优先级高于绑定上下文和历史缓存;同级冲突或无法唯一识别时才问用户。
|
|
69
77
|
3. **产品与视觉分工**:`yida-requirement-analysis` 先统一整理用户需求;业务目标、资源蓝图、页面结构、导航顺序和验收标准由 `yida-prd` 写入 `prd.md`;主题 token、布局、材质、圆角、密度、组件和状态规则由 `yida-design` 写入 `design.md`。两份文件校验通过前不得创建资源。
|
|
70
78
|
4. **阶段技能按需加载**:进入应用壳、表单、流程、页面、发布、数据写入等阶段时,才执行对应 `use_skill(...)`。
|
|
71
|
-
5. **真实 ID 和真实数据**:不编造 `appType`、`formUuid`、`fieldId`、`processCode`、`reportId
|
|
79
|
+
5. **真实 ID 和真实数据**:不编造 `appType`、`formUuid`、`fieldId`、`processCode`、`reportId`。无显式窄范围的完整应用默认给核心普通表单写入 1-3 条业务化 seed records 并 query 抽查;`explicitScope.allowInferredResources=false` 时不得增加未点名的 seed records 或页面。
|
|
72
80
|
6. **自定义页面开发技能固定**:完整应用页面源码按 Step 7 执行。
|
|
73
81
|
7. **删除必须确认**:用户要求删除应用时,先展示应用名称、应用 ID 和影响范围,等待明确“确认删除”后才能执行。
|
|
74
82
|
8. **列表页选择**:默认使用普通表单的数据管理页;用户明确要求自定义列表页时才创建 display 页面。
|
|
75
83
|
9. **交付物收口**:Step 2 的三个文件和 Step 9 的 build manifest 都是内部文件,不是用户交付物。表单、流程、报表和页面只在业务总结中概述,不逐项生成用户可见附件;宿主支持交付工具时,final 只交付一次“应用访问入口”组。
|
|
84
|
+
10. **窄范围停止点**:完成 `explicitScope` 中的资源回读与真实链接交付后立即停止;不得为了满足完整应用默认完成条件继续进入被裁剪的 Step 5-8。
|
|
76
85
|
|
|
77
86
|
## 关键决策树
|
|
78
87
|
|
|
@@ -43,9 +43,11 @@
|
|
|
43
43
|
|
|
44
44
|
标准 Plan 首版采用一次收齐:业务任务完成一个 `business.json`,视觉直接复用 init 根据已确认选择生成的 `visual.json`,随后合并物化。这样首版只等待一次模型规划。品牌稿、参考图、页面级特殊视觉或明确的视觉精修要求命中时,才由视觉任务更新 `visual.json`;超大需求需要展示中间进展时才使用 [按模块更新方案](incremental-preview.md),全部完成后再用 `materialize --from-preview` 汇总校验。
|
|
45
45
|
|
|
46
|
+
init 返回的 `materialize.command` 是标准首版唯一生成命令。执行成功后直接使用其 `outputs.html` 和 `revision` 展示确认,不通过 `--from-preview`、preview、Glob 或额外 Read 探测产物;确认后不再物化或 patch。`explicitScope.allowInferredResources=false` 时,完整主题仍可作为 Plan 展示的一部分,但不进入应用设置、导航或发布执行。
|
|
47
|
+
|
|
46
48
|
`design-plan init` 返回 `parallelTasks` 和两个片段文件:
|
|
47
49
|
|
|
48
|
-
- `business.json
|
|
50
|
+
- `business.json`:init 已预填业务骨架,业务任务必须先读后写,保留 base;facts 只能填写 overview、dataModels、businessFlows、pages 和可选 execution,禁止加入 visualStyle。一次补齐所有普通表单的 sampleDataPlan(不造数写 skipReason)和所有自定义页面的 permissionSummary,完成后设 ready=true。可选 meta 仅填写 businessDomain、experienceTopology。
|
|
49
51
|
- `visual.json`:init 根据需求阶段原子保存的 `visualSelection` 写入 facts.visualStyle;主色、方向和导航明暗完整时设 `ready=true`,否则保持未就绪并返回 `visual-selection` 任务,只补充缺失选择。标准页面的完整主题和页面场景规则由 CLI 补齐。只有命中特殊视觉条件时,视觉任务才按业务页面更新项目差异与 `pageApplications`。
|
|
50
52
|
|
|
51
53
|
两个文件的 base 由 CLI 生成,保留原值;任务读取同一份需求、草稿和主题上下文。业务片段完成后保持稳定,变更时通知视觉任务重新核对页面设计。
|
|
@@ -24,6 +24,8 @@ openyida design-plan materialize prd/<项目名>/build-plan.json --from-preview
|
|
|
24
24
|
|
|
25
25
|
CLI 完整校验后一起保存源计划、`prd.md`、`design.md`、`build-plan.html` 和 `app-theme.css`。HTML 使用预置模板,业务内容与 PRD 一致。
|
|
26
26
|
|
|
27
|
+
标准首版必须直接执行 init 返回的 `materialize.command`;不得先试 `--from-preview`、`preview`、无参数 materialize 或 shell 重定向。成功 JSON 已包含 `outputs.html` 和 `revision`,直接用于下一步同一次结构化提问,不再 Glob 目录,也不额外 Read `prd.md`、`design.md` 或 `build-plan.json`。
|
|
28
|
+
|
|
27
29
|
完整文件的职责与版本规则见 [完整文件合并](../parallel-work.md#plan-的-cli-交接)。直接维护源计划时先设 `meta.status=awaiting_confirmation`,再执行 `openyida design-plan materialize prd/<项目名>/build-plan.json --json`;仅做诊断时使用 `openyida design-plan materialize prd/<项目名>/build-plan.json --check --json`。正常生成已经包含完整校验,不先运行一次 --check 再重复生成。
|
|
28
30
|
|
|
29
31
|
HTML 保留“需求总览、数据模型、业务流程、页面规划”四章,完整展示用户需要确认的业务、视觉、数据、顺序和验收内容;整体视觉放在需求总览,逐页视觉放在页面详情。展示范围见 [HTML 内容契约](../../../yida-design/sub_skill/yida-design-plan/assets/README.md#需求总览中的视觉信息),Markdown 供 Agent 执行。
|
|
@@ -35,16 +37,16 @@ HTML 保留“需求总览、数据模型、业务流程、页面规划”四章
|
|
|
35
37
|
按 [用户交互契约](../../../yida-design/references/ask-human-interaction-contract.md) 执行:
|
|
36
38
|
|
|
37
39
|
1. 在会话中展示“当前这版方案”,并用 3–7 条业务摘要说明方案内容。
|
|
38
|
-
2.
|
|
39
|
-
3. 结构化交互成功创建后内部记录 `presentedRevision=meta.revision`。询问“确认并开始搭建”或“继续调整”,提交时由宿主原样回传 revision
|
|
40
|
+
2. 必须实际调用 `ask_human` 创建结构化提问,并通过同一次调用的 `attachments` 携带可打开的 `prd/<项目名>/build-plan.html`;附件对象固定使用 `name: "build-plan.html"`,并将 `revision` 设为当前 `meta.revision`。只输出方案正文或普通 assistant 文本后结束本轮属于未完成,严禁用它替代 `ask_human`;也不得改成项目标题,或先发普通文本附件、再单独提问。
|
|
41
|
+
3. 结构化交互成功创建后内部记录 `presentedRevision=meta.revision`。询问“确认并开始搭建”或“继续调整”,提交时由宿主原样回传 revision,将确认结果绑定到本次展示版本。用户可见版本称为“第 N 版方案”,展示序号与内部 revision 绑定。
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
只有以下条件同时成立才交接;它们由本轮 ask_human 请求和回传在运行时判定,不要求把确认状态写回 workspace 文件:
|
|
42
44
|
|
|
43
45
|
- `meta.status=confirmed`
|
|
44
46
|
- `meta.planState.planConfirmed=true`
|
|
45
47
|
- `meta.revision=presentedRevision=confirmedRevision`
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
收到“确认并开始搭建”且回传 revision 等于展示 revision 后,直接进入同版本资源实施。确认之后严禁再次 materialize、patch、Edit 或 Read 计划来“同步确认状态”;不存在可写 `meta.planState.planConfirmed` 的确认命令。`explicitScope.allowInferredResources=false` 时也不执行主题 CSS、应用设置或导航交接,只创建范围内资源并回读、交付。
|
|
48
50
|
|
|
49
51
|
## 4. 处理调整
|
|
50
52
|
|
|
@@ -13,16 +13,19 @@
|
|
|
13
13
|
|
|
14
14
|
## 执行顺序
|
|
15
15
|
|
|
16
|
-
1.
|
|
16
|
+
1. 直接复用已确认的 `requirement-brief.json`;本文件已经包含规划入口,不再额外读取 `step-1-understand.md` 或 `step-2-confirm.md`。
|
|
17
17
|
2. 初始化计划草稿,CLI 返回编写契约与当前主题的精简上下文:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
openyida design-plan init .cache/openyida/<项目名>/requirement-brief.json --theme-id <已选主题> --json
|
|
21
21
|
```
|
|
22
|
+
`requirement-brief.json` 必须复用 `yida-requirement-analysis` 的输出结构,不要自行发明字段层级;`projectName` 位于 JSON 根级,`businessGoals` 等集合字段保持数组,命令第一次就传入 `visualSelection.themeId`。若 CLI 返回字段路径诊断,按 `expectedPath` 集中修正原文件后最多重试一次,不要通过反复更换项目名规避结构错误。
|
|
22
23
|
|
|
23
|
-
3. 执行 init 返回的 `parallelTasks
|
|
24
|
-
4.
|
|
25
|
-
5.
|
|
24
|
+
3. 执行 init 返回的 `parallelTasks`:普通标准首版只有一个业务规划任务;显式窄范围且表单字段已完整时,CLI 返回 `preparedInputs.businessReady=true` 和空任务列表,直接进入第 4 步,不再 Read/Edit business 或补规划。视觉方向、主题色和导航样式已在需求阶段确定,CLI 将其预填到 `preparedInputs.visual`,并从主题模板确定性补齐页面标准视觉,不再启动“基础视觉 → 等待页面 → 逐页视觉绑定”两段模型任务。
|
|
25
|
+
4. init 已创建并预填 `business.json` 骨架;先 Read 该文件,保留 `base`,一次补完后再 Write/Edit,避免覆盖保护失败。`facts` 只允许 overview、dataModels、businessFlows、pages 和可选 execution,绝不写 `visualStyle`。同一次补齐所有普通表单的 sampleDataPlan(窄范围不造数时写 skipReason)及所有自定义页面的 permissionSummary,然后直接执行 init 返回的 `materialize.command`,只物化一次。标准首版禁止先试 `--from-preview`、`preview`、`--check` 或无参数 materialize;成功 JSON 已返回 HTML 路径和 revision,不再用 Glob、Read 或帮助命令检查产物。只有存在品牌稿、参考图、页面级特殊风格或用户明确要求精修时,才执行 `optionalTasks.visual-refinement` 后再物化。
|
|
26
|
+
5. 读取精确路径 `workflow/plan/step-4-deliver.md`,按其中契约直接展示并确认当前方案。超大需求需要展示中间进展或用户明确要求边生成边查看时,才使用 [按模块更新方案](../incremental-preview.md);普通首版不逐模块预览和重复渲染。
|
|
27
|
+
|
|
28
|
+
明确窄交付时,方案只描述 `explicitScope` 中的资源;确认后裁剪无关步骤,在这些资源回读并交付真实链接后停止。不得因用户使用“应用”一词自行增加 seed records、自定义页面、主题或导航工作。
|
|
26
29
|
|
|
27
30
|
初次编写只读 CLI 返回的紧凑契约、当前主题上下文及共享需求;模板全文由 CLI 读取。具体组件定制、暗色浮层或复杂页面需要额外规则时,再读取对应章节。
|
|
28
31
|
|
|
@@ -42,4 +45,4 @@ build-plan.json(业务和视觉的源事实)
|
|
|
42
45
|
|
|
43
46
|
草稿按已确定的模块更新,最终校验后统一保存三份文档和主题 CSS。调整由对应技能更新源事实,再由编排重新生成并确认,具体命令与版本条件见 [生成与确认](step-4-deliver.md)。
|
|
44
47
|
|
|
45
|
-
用户交互按 [可见表达契约](../../../yida-design/references/ask-human-interaction-contract.md) 执行。当前展示版本确认后,将 `prd.md`、`design.md` 和已生成的 `outputs.theme`
|
|
48
|
+
用户交互按 [可见表达契约](../../../yida-design/references/ask-human-interaction-contract.md) 执行。当前展示版本确认后,将 `prd.md`、`design.md` 和已生成的 `outputs.theme` 交给应用主流程 Step 3。仅完整应用同步主题设置;`explicitScope.allowInferredResources=false` 时忽略主题和导航交接,只创建范围内资源并交付,不重复物化计划。
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
回答齐全后直接保存内部需求记录并进入 2.1;不把“生成需求简报”列为独立任务,不再扩写或展示简报请用户确认。已有确认记录且需求未变化时直接复用。记录粒度、保存和校验规则统一遵守上述需求分析流程。
|
|
14
14
|
|
|
15
|
+
显式搭建方式属于本次任务的粘性输入。若用户在首次消息已选择 Plan,澄清业务模块、页面、导航或风格时不得再次询问模式,也不得在合并回答后改写为 Fast;进入 2.1 前必须以用户最后一次明确选择校验 `intake.designMode`。Fast 同理。
|
|
16
|
+
|
|
15
17
|
执行规划前读取 `constraints.prohibitedActions`。PRD 与 design 必须把禁止项写成实现门禁:`theme-file` 禁止时沿用现有平台主题且不安排主题文件任务;`page-source` 禁止时只允许只读核查与非源码配置;`publish` 禁止时把发布明确标记为跳过。不得为了满足默认九步流程静默删除这些约束。
|
|
16
18
|
|
|
17
19
|
## 2.1 按已确认方式推进
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
- Fast:继续 2.2–2.3;已有详细需求直接作为规划基础。
|
|
22
24
|
- Plan:执行 [Plan 编排](plan/workflow.md),用户确认当前方案后完成主题交接并进入 Step 3。
|
|
23
25
|
|
|
26
|
+
Plan 分支从已加载 `yida-app` 的 Available Files 读取精确路径 `workflow/plan/workflow.md`;不要把当前文件名当目录拼成 `workflow/step-2-design/plan/workflow.md`,也不要用 Glob 猜路径。
|
|
27
|
+
|
|
24
28
|
共享需求只整理一次。创建出的资源 ID 写入执行上下文;用户需求或范围实质变化时再更新需求与相关规划。需求文件与实施文档供内部执行,Plan 的 HTML 用于用户查看和确认方案。
|
|
25
29
|
|
|
26
30
|
## 2.2 同时生成 PRD 和视觉设计
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Step 4:创建或更新表单/流程
|
|
2
2
|
|
|
3
|
-
按 PRD 的依赖创建或复用表单和流程。同一轮需要新建两个及以上普通表单时,把独立表单和关联表单写入同一个 `forms.json`,通过 `dependsOn` / `$form` 表达依赖,并且只调用一次 `openyida create-form batch`;由 CLI 内部完成分组、真实 ID 回读和依赖调度,不逐个 create,也不由模型拆成多次 batch。某页所需表单、流程就绪后即可接入该页;不要因其他页面的资源未完成而阻塞无依赖页面开发。
|
|
3
|
+
按 PRD 的依赖创建或复用表单和流程。同一轮需要新建两个及以上普通表单时,把独立表单和关联表单写入同一个 `forms.json`,通过 `dependsOn` / `$form` 表达依赖,并且只调用一次 `openyida create-form batch`;由 CLI 内部完成分组、真实 ID 回读和依赖调度,不逐个 create,也不由模型拆成多次 batch。调用前确认实际项目根,让 Write 的绝对路径与 Bash 从项目根使用的 `.cache/openyida/<项目名>/forms.json` 指向同一个物理文件,并用 Read 确认任务文件存在;不要用 batch 探测路径,收到 background pending 后也不要重试 batch。某页所需表单、流程就绪后即可接入该页;不要因其他页面的资源未完成而阻塞无依赖页面开发。
|
|
4
|
+
|
|
5
|
+
`explicitScope.allowInferredResources=false` 且本轮只有一个普通表单时走最短路径:Write 一份字段文件后调用一次 `openyida create-form create`。Write 会创建父目录,禁止先用 `ls` 或 Bash 检查缓存目录。成功结果已包含真实 formUuid/链接时,直接交付并停止;不得继续 `get-schema`、写 schema evidence、加载数据管理技能、造示例数据、更新主题或排序导航。只有创建结果缺少本次明确验收所需字段时才做一次针对性回读。
|
|
4
6
|
|
|
5
7
|
拿到真实 `appType` 和已确认的业务契约即可开始本步骤,不等待主题 CSS 上传或应用主题设置回读。主题分支与本步骤并行,按 [主题与业务资源的依赖](parallel-work.md#主题与业务资源的依赖) 汇合。
|
|
6
8
|
|
|
@@ -24,6 +24,7 @@ description: 表单页面创建与更新;支持 19 种业务字段和 Divider
|
|
|
24
24
|
- 不要用此命令操作数据记录(增删改查),应使用 `yida-data-management`
|
|
25
25
|
- 不要用 shell heredoc、`cat`/`echo`/`printf`/`tee` 或重定向生成字段、变更、补丁、规则、数据源 JSON 文件
|
|
26
26
|
- OpenYida CLI 不要加 `2>/dev/null`;失败时保留 stdout/stderr 诊断,遇到 DENIED 或重复失败必须换策略
|
|
27
|
+
- 多表单 batch 契约已在本技能给出时,不要再调用 `create-form batch --help`、`create-form batch --check`,也不要搜索 CLI 安装目录或源码来探测格式;这些调用同样占用本轮唯一一次 batch 调用名额
|
|
27
28
|
- 已有目标表单且用户是改字段/联动/属性时,不要创建新表单;必须走 update/patch/rule/bind-datasource。
|
|
28
29
|
- 不要用 `GroupContainer` / `PageSection` 承载普通业务分组;普通分组必须优先用 `Divider`
|
|
29
30
|
- 严禁为原生表单或 `formDetail` 生成、注入 CSS、JS、HTML 或主题代码;详情页由平台渲染。
|
|
@@ -58,7 +59,57 @@ description: 表单页面创建与更新;支持 19 种业务字段和 Divider
|
|
|
58
59
|
|
|
59
60
|
## 多表单创建
|
|
60
61
|
|
|
61
|
-
同一轮需要新建两个及以上普通表单时,必须按 [并行创建表单](references/batch-forms.md) 把全部表单写入同一个 `forms.json`,并且只调用一次 `openyida create-form batch <appType> <任务文件> --json`。独立表单和关联表单放在同一任务文件中,依赖通过 `dependsOn` / `$form` 表达,由 CLI 在一次 batch 内部完成分组、真实 `formUuid/fieldId` 回读和依赖调度;不要手工拆成多次 batch,也不要逐个调用 `create-form create
|
|
62
|
+
同一轮需要新建两个及以上普通表单时,必须按 [并行创建表单](references/batch-forms.md) 把全部表单写入同一个 `forms.json`,并且只调用一次 `openyida create-form batch <appType> <任务文件> --json`。独立表单和关联表单放在同一任务文件中,依赖通过 `dependsOn` / `$form` 表达,由 CLI 在一次 batch 内部完成分组、真实 `formUuid/fieldId` 回读和依赖调度;不要手工拆成多次 batch,也不要逐个调用 `create-form create`。调用前先确认 CLI 的实际项目根目录,并让 Write 创建的绝对路径与 Bash 使用的任务文件指向同一个物理文件:常见 `<workspace>/project` 布局中应写入 `<workspace>/project/.cache/openyida/<项目名>/forms.json`,再从该项目根传 `.cache/openyida/<项目名>/forms.json`。先用 Read 确认任务文件存在,不要通过试跑 batch 探测路径。batch 返回 background pending 时等待运行时投递完成结果,不得再次调用 batch。只有修改已有表单、恢复已有 `formUuid`,或当前 batch 契约无法表达依赖时,才走明确的非 batch 路径并说明原因。
|
|
63
|
+
|
|
64
|
+
主技能内的最小任务文件契约如下,执行普通批量创建无需再查 help、sample 或 CLI 源码:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"forms": [
|
|
69
|
+
{ "key": "customer", "title": "客户", "fields": [{ "type": "TextField", "label": "客户名称", "required": true }] },
|
|
70
|
+
{ "key": "contact", "title": "联系人", "fields": [{ "type": "TextField", "label": "联系人姓名", "required": true }] },
|
|
71
|
+
{
|
|
72
|
+
"key": "relation",
|
|
73
|
+
"title": "客户联系人关系",
|
|
74
|
+
"dependsOn": ["customer", "contact"],
|
|
75
|
+
"fields": [{
|
|
76
|
+
"type": "AssociationFormField",
|
|
77
|
+
"label": "关联客户",
|
|
78
|
+
"associationForm": {
|
|
79
|
+
"appType": "APP_XXX",
|
|
80
|
+
"formUuid": { "$form": "customer" },
|
|
81
|
+
"formTitle": "客户",
|
|
82
|
+
"mainFieldId": { "$form": "customer", "field": "客户名称" },
|
|
83
|
+
"mainFieldLabel": "客户名称",
|
|
84
|
+
"mainComponentName": "TextField"
|
|
85
|
+
}
|
|
86
|
+
}]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`fieldsFile` 也可替代内联 `fields`,其路径相对 `forms.json` 所在目录;已有完整表单可提供 `formUuid` 回读复用。普通搭建的 batch 项必须省略 `icon`,由 CLI 按标题和字段语义自动选择;只有用户明确给出 `openyida create-form icons --json` 目录中的表单图标名时才设置,应用图标 `xian-*` 绝不是表单图标。`locale` 同样只在用户明确指定时设置。普通多表单创建的执行顺序固定为:确认 `projectRoot` → Write 一个任务文件 → Read 确认文件 → 唯一一次真实 batch → 使用 batch 结果和必要的 compact `get-schema` 回读。
|
|
93
|
+
|
|
94
|
+
最小 `forms.json` 结构如下;`fieldsFile` 相对 `forms.json` 所在目录解析:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"forms": [
|
|
99
|
+
{ "key": "customer", "title": "客户", "fieldsFile": "customer-fields.json" },
|
|
100
|
+
{
|
|
101
|
+
"key": "order",
|
|
102
|
+
"title": "订单",
|
|
103
|
+
"fieldsFile": "order-fields.json",
|
|
104
|
+
"dependsOn": ["customer"]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
关联字段必须把引用放在 `associationForm` 内。推荐使用紧凑写法 `"associationForm": { "$form": "customer", "field": "客户名称" }`;batch 会将其规范化为 `associationForm.formUuid` 和 `associationForm.mainFieldId`。完整写法则分别在 `formUuid` 使用 `{ "$form": "customer" }`、在 `mainFieldId` 使用 `{ "$form": "customer", "field": "客户名称" }`。不要把 `$form` 放在 `AssociationFormField` 顶层,也不要用 `batch --help`、空参数或临时计划探索格式;技能中的结构就是正式契约。
|
|
111
|
+
|
|
112
|
+
批量命令超过前台时限进入后台属于正常行为。此时必须保留原任务和 `<forms.json>.state.json`,等待运行时自动回传结果;禁止调用 `ToolStop`,禁止删除 `.state.json`/`.lock`,禁止改变参数再次调用 batch。若最终返回 `FORM_BATCH_PARTIAL_FAILURE`,在本轮原样保留结构化错误并停止;禁止模型侧再调用 `create-form create`、`update` 或 `resume` 补洞。CLI 会在同一次 batch 内对已取得真实 `formUuid` 的空壳表单执行一次保守恢复。
|
|
62
113
|
|
|
63
114
|
## 官方表单示例范式
|
|
64
115
|
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
openyida create-form batch <appType> .cache/openyida/<项目名>/forms.json --json
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
这条相对路径以 CLI 的实际项目根目录为准。先确认项目根,再让文件工具的写入位置和 Bash 参数落到同一个物理文件;例如项目根是 `<workspace>/project` 时,Write 应创建 `<workspace>/project/.cache/openyida/<项目名>/forms.json`,Bash 从 `<workspace>/project` 执行并传 `.cache/openyida/<项目名>/forms.json`。调用前用 Read 确认文件存在,不要用 batch 试错路径。
|
|
10
|
+
|
|
9
11
|
## 任务文件
|
|
10
12
|
|
|
11
13
|
```json
|
|
@@ -18,7 +20,7 @@ openyida create-form batch <appType> .cache/openyida/<项目名>/forms.json --js
|
|
|
18
20
|
}
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
`fieldsFile` 相对任务文件所在目录,也可使用 `fields`
|
|
23
|
+
`fieldsFile` 相对任务文件所在目录,也可使用 `fields` 直接填写字段定义。普通 batch 项省略 `icon`,由 CLI 自动选择;只有用户明确给出表单图标目录中的合法名称时才设置,禁止写应用图标 `xian-*`。`locale` 也只在用户明确指定时设置。已有完整表单填写 `formUuid`,CLI 回读并复用。
|
|
22
24
|
|
|
23
25
|
客户和商品同时创建。订单的前置表单完成后开始创建;其他独立任务继续执行。依赖环、未知依赖、重复 key 和无效字段会在创建前报错。
|
|
24
26
|
|
|
@@ -26,6 +28,18 @@ openyida create-form batch <appType> .cache/openyida/<项目名>/forms.json --js
|
|
|
26
28
|
|
|
27
29
|
在字段配置中,用以下对象引用同批次的真实 ID,CLI 自动补入依赖:
|
|
28
30
|
|
|
31
|
+
紧凑写法(推荐):
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"type": "AssociationFormField",
|
|
36
|
+
"label": "客户",
|
|
37
|
+
"associationForm": { "$form": "customer", "field": "客户名称" }
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
batch 会在预校验前把紧凑写法规范化为下面的完整结构;前向依赖只做本地结构校验,不要求目标表单预先存在:
|
|
42
|
+
|
|
29
43
|
```json
|
|
30
44
|
{
|
|
31
45
|
"type": "AssociationFormField",
|
|
@@ -41,7 +55,7 @@ openyida create-form batch <appType> .cache/openyida/<项目名>/forms.json --js
|
|
|
41
55
|
}
|
|
42
56
|
```
|
|
43
57
|
|
|
44
|
-
`field` 按字段名称或 ID 精确匹配,重名时使用 ID
|
|
58
|
+
`field` 按字段名称或 ID 精确匹配,重名时使用 ID。关联表单完成且字段回读成功后,才解析引用并创建当前表单。紧凑写法适用于同应用内关联;需要跨应用或更多 `associationForm` 属性时使用完整写法。普通关联字段属性沿用 [关联表单配置](association-form-field.md)。
|
|
45
59
|
|
|
46
60
|
## 检查与恢复
|
|
47
61
|
|
|
@@ -49,7 +63,9 @@ openyida create-form batch <appType> .cache/openyida/<项目名>/forms.json --js
|
|
|
49
63
|
- `--concurrency 1..4` 设置并发上限,默认 3。
|
|
50
64
|
- 结果保存在任务文件旁的 `.state.json`,包含每张表单的 ID、字段和状态。主流程从中汇总资源上下文,再配置导航。
|
|
51
65
|
- 相同任务再次执行时,成功表单回读复用。失败及中断任务保留原状态,其依赖标记为 blocked;独立任务继续。
|
|
66
|
+
- 当前批次返回 `success:false` 时,以返回的 `results`、子命令诊断和 `.state.json` 为准停止本轮创建;不得为补齐失败项改用单条 `create-form create`,也不得把剩余项拆成新的 batch。修正输入或为已知资源补入 `formUuid` 后,后续恢复仍使用原任务文件执行 batch。
|
|
52
67
|
- 恢复失败任务前,核对已创建资源并修复。准备新任务文件时,用 `formUuid` 复用完整表单,仅为确认尚未创建的表单保留创建任务。
|
|
53
68
|
- `.lock` 防止同一任务重复启动。进程异常退出遗留锁时,确认原进程已结束、核对已创建资源后再清理。
|
|
69
|
+
- 首次 batch 返回 background pending 时等待运行时自动投递完成结果;pending 不是失败,也不是重试信号,不得再次调用 batch。
|
|
54
70
|
|
|
55
71
|
已有表单的字段修改仍使用 update/patch 等命令;不同表单可分别更新,同一张表单由一个任务维护。流程审批按 `yida-create-process` 执行,等待其依赖的表单就绪后再配置。
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
用户同时要求先出计划和少提问时采用 Plan,复用已知信息。用户指定的详细计划作为规划基础,补齐实施所需的缺项。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
同一目标、同一次搭建复用已确认方式;该选择在需求澄清、`ask_human` 恢复和 brief 合并中保持粘性,回答未重复提到模式不代表改选。只有用户明确要求改用另一方式时才更新 `intake.designMode`。切换目标时重新判断。已有应用的局部增改、单页美化和主题调整直接处理本次任务,遇到会改变范围的问题再澄清。
|
|
13
13
|
|
|
14
14
|
两种方式共用业务和视觉契约;Plan 额外展示方案并绑定当前版本确认。面向用户的表达与宿主适配见 [用户交互契约](ask-human-interaction-contract.md)。
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
`design-plan init` 从已确认需求建立草稿、主题上下文和业务/视觉片段,返回必要任务与可选视觉精修。标准首版只编写业务事实,视觉片段复用已确认选择,由 CLI 补齐标准页面视觉;视觉选择不完整时返回补齐任务,有特殊视觉要求时执行可选精修。片段写入与合并见 [并行交接](../../../../yida-app/workflow/parallel-work.md#plan-的-cli-交接)。保留生成的项目目录名、页面 ID 与 sceneKey;业务名称使用 `meta.appName`。补齐业务与视觉片段后执行交接命令;直接维护单一计划时使用:
|
|
6
6
|
|
|
7
|
+
init 已预填 `business.json` 结构,业务任务先读后写并保留 base。业务 facts 仅允许 overview、dataModels、businessFlows、pages、execution;visualStyle 只属于 `visual.json`。首次合并前一次补齐每个普通表单的 sampleDataPlan(跳过则写 skipReason)和每个自定义页面的 permissionSummary,避免用多轮 materialize 探测必填字段。
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
openyida design-plan materialize prd/<项目名>/build-plan.json --json
|
|
9
11
|
```
|
|
@@ -108,7 +110,7 @@ CLI 校验源事实,使用预置模板整批生成 `prd.md`、`design.md` 和
|
|
|
108
110
|
| 字段 | 格式 |
|
|
109
111
|
| --- | --- |
|
|
110
112
|
| `appConfig` | 已知真实 `appType/corpId/baseUrl`;`navigationType` 为 platform-l-shape/platform-top/platform-side/custom。hideAppNav/layoutDirection/navTheme/logoSource 由导航与视觉派生 |
|
|
111
|
-
| `explicitScope` |
|
|
113
|
+
| `explicitScope` | 用户明确的页面、表单、流程、报表、导航和交付范围;`allowInferredResources=false` 时不增加未点名的 seed records、自定义页面、主题或导航任务;自定义导航布局保留在 navigation.variant |
|
|
112
114
|
| `resourceBlueprint` | `{name,type,purpose,pageId?}` 数组;type 为 normal-form/process-form/display-page/report,名称与模型、页面对应且唯一 |
|
|
113
115
|
| `resourceCreationOrder` | 覆盖全部资源的有序名称数组,先应用,再被依赖模型,最后依赖它们的页面 |
|
|
114
116
|
| `pageImplementationOrder` | 覆盖全部页面的 pageId 或名称数组 |
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
1. 按“业务目标 → 对象与字段 → 流程与规则 → 用户任务 → 页面”规划。字段、关系、规则、权限和已确认页面范围保留完整。
|
|
6
6
|
2. 维护 overview、dataModels、businessFlows、pages 和业务 execution。导航沿用需求选择;资源按依赖排列,页面任务、数据来源、主操作和验收要求明确。
|
|
7
|
-
3.
|
|
7
|
+
3. init 已创建并预填 `business.json` 骨架。先读取它,保留 base;facts **只能**包含 overview、dataModels、businessFlows、pages 和可选 execution,禁止写 visualStyle。一次补齐全部业务事实并将 ready 设为 true:每个普通表单都有 sampleDataPlan(窄范围不造数写 skipReason),每个自定义页面都有非空 permissionSummary。视觉选择已由 CLI 预填,不等待另一个模型任务。只有命中品牌稿、参考图、页面级特殊视觉或用户明确要求精修时,才将页面任务、区块与 sceneKey 交给可选视觉任务。超大需求需要中间展示时才按 [按模块更新方案](../../yida-app/workflow/incremental-preview.md) 提交;普通首版使用 [完整文件合并](../../yida-app/workflow/parallel-work.md#plan-的-cli-交接)。
|
|
8
8
|
4. CLI 生成完整 PRD 与交接,模板规则、摘要副本和默认设计引用由 CLI 补齐。校验问题按字段路径集中修正。
|
|
9
9
|
|
|
10
10
|
首版只写紧凑契约要求的业务事实和用户指定的差异,不重复写 CLI 可派生的概要、资源顺序副本、通用交互状态与默认验收文案。普通表单先提供 1 条覆盖必填字段的有效示例记录;用户要求多条或业务验收确有需要时再增加。不要先生成完整 PRD Markdown,再将相同内容改写为 JSON。
|
|
11
11
|
|
|
12
|
+
`explicitScope.allowInferredResources=false` 时只规划点名资源:不增加自定义工作台、列表页、seed records、主题设置或导航任务;普通表单的 sampleDataPlan 用 skipReason 明确“本轮窄范围不造数”。
|
|
13
|
+
|
|
12
14
|
页面模式选项由 CLI 返回的 authoring-context 提供。遇到复杂跨页流程或特殊资源时,再按需读取 [业务内容检查](../references/plan-content.md) 与 [统一 PRD 格式](output-prd.md)。
|
|
@@ -25,16 +25,16 @@ description: 识别并读取需求来源,理解和澄清用户需求,输出
|
|
|
25
25
|
| `projectName` | 稳定的项目目录名 |
|
|
26
26
|
| `appName` | 用户可见的应用名称候选 |
|
|
27
27
|
| `industry` / `appType` | 业务领域和应用类型 |
|
|
28
|
-
| `targetUsers` |
|
|
29
|
-
| `businessGoals` |
|
|
30
|
-
| `coreFunctions` |
|
|
31
|
-
| `businessObjects` |
|
|
32
|
-
| `pageScenes` |
|
|
28
|
+
| `targetUsers` | 核心角色和使用场景,字符串数组 |
|
|
29
|
+
| `businessGoals` | 要解决的问题和成功结果,字符串数组;只有一个目标也写数组 |
|
|
30
|
+
| `coreFunctions` | 核心功能与必要辅助功能,数组 |
|
|
31
|
+
| `businessObjects` | 客户、订单、项目、工单等核心对象,数组 |
|
|
32
|
+
| `pageScenes` | 已确认的页面与表单范围,数组;记录稳定 key、name、kind、purpose 及已有细项 |
|
|
33
33
|
| `intake` | 首次搭建判断、来源详细程度、搭建方式和需求确认状态 |
|
|
34
|
-
| `visualSelection` |
|
|
35
|
-
| `navigation` | 应用导航决策:`type
|
|
34
|
+
| `visualSelection` | 已确认风格及其主题、主色、导航明暗映射;Plan 初始化前必须含非空 `themeId` |
|
|
35
|
+
| `navigation` | 应用导航决策:`type` 只能是 `platform-l-shape/platform-top/platform-side/custom`,并记录 `source/reason`;自定义导航增加 `variant: side/top/mixed/dock`;未决时 type 为 null,规划前补齐 |
|
|
36
36
|
| `resourceContext` | 已确认可复用的 app/page/form/process 业务上下文,不写猜测 ID |
|
|
37
|
-
| `explicitScope` |
|
|
37
|
+
| `explicitScope` | 用户明确指定的页面、表单、流程、报表、导航项和本轮交付;明确窄范围时写对应数组及 `allowInferredResources:false`,没有时为 `null` |
|
|
38
38
|
| `brandHints` / `colorHints` | 明确的品牌、参考页面、已有主题、偏好色与避用色 |
|
|
39
39
|
| `constraints` | 组织、设备、权限、交付等约束;用户明确禁止的动作原文写入 `prohibitedActions`,作为后续写操作硬门禁 |
|
|
40
40
|
| `assumptions` / `openQuestions` | 可安全默认的事项与会改变范围的未决问题 |
|
|
@@ -43,6 +43,7 @@ description: 识别并读取需求来源,理解和澄清用户需求,输出
|
|
|
43
43
|
|
|
44
44
|
- brief 必须可解析,已知的项目名、目标、用户、对象、功能、场景及范围准确且无遗漏;不为填满字段补造需求。会改变资源范围、权限或业务对象的问题写入 `openQuestions`,由编排组织确认后再进入规划;不影响范围的待设计细节由 PRD 或视觉设计补齐。
|
|
45
45
|
- 用户指定的资源和范围原样保留在 `explicitScope`;真实 ID 有证据时写入 `resourceContext`。
|
|
46
|
+
- 用户用“应用/系统”描述背景、又明确要求本轮只完成某个表单、流程、报表或页面并交付链接时,后者是执行边界:`explicitScope` 只记录该资源与交付,设置 `allowInferredResources:false`,不得推导示例数据、自定义工作台或其他页面。
|
|
46
47
|
- “不复制/不上传主题”“不修改页面源码”“不发布”等负向要求不得改写成偏好或建议;保留原文,并分别规范化为 `theme-file`、`page-source`、`publish` 等 `constraints.prohibitedActions`。明确的禁止项不因 Fast/Plan 默认流程而失效。
|
|
47
48
|
- 需求文件校验通过后保持不变。后续创建出的真实 ID 写入 schema 或当前任务资源上下文。
|
|
48
49
|
- 用户需求或已确认范围实质变化时,由本技能更新 brief,并交给 `yida-app` 同步业务与视觉规划。
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
首次搭建包括新应用,以及已有应用但尚无业务页面的情况;占位页面按空应用处理。已有业务应用的增改只确认本次变更中的疑问。
|
|
12
12
|
|
|
13
|
+
用户已把本轮限定为一个或若干命名资源且交付条件明确时,这是“显式窄范围”,不是完整首次搭建问卷:只追问会改变这些资源本身或阻塞创建的事实。导航、主题、示例数据、工作台和额外页面不在交付范围时,不询问其偏好;为满足 Plan 输入可记录当前平台值或稳定默认值并标记 `source: "ai_default"`,但下游不得据此执行应用设置。用户已明确 Plan 时也不再询问搭建方式。此类请求中的 Plan 确认卡就是唯一必需的方案确认交互。特别是“随后完成一个表单并交付链接”只允许把该表单写入 `explicitScope.forms/delivery`,`explicitScope.pages` 必须为 `[]`,不得根据“查看、管理、统计”等普通表单能力臆造工作台或自定义列表。
|
|
14
|
+
|
|
13
15
|
## 2. 确认首次搭建的未决事项
|
|
14
16
|
|
|
15
17
|
先分析再提问。用户已明确的信息直接采用;同一次搭建已回答的问题直接复用。首次提问必须在同一轮一次性收集所有尚未明确的搭建方式(Fast / Plan)、业务模块、页面与表单范围、导航归属(平台或自定义)、导航布局和设计风格。不要先问模块、导航和风格,收到回答后再另起一轮补问模式、导航位置或页面。
|
|
16
18
|
|
|
19
|
+
先锁定用户已经明确的搭建方式:用户写明 `Plan`、先出 PRD/方案并确认后搭建时,立即把草稿的 `intake.designMode` 设为 `plan`;用户写明 `Fast` 或直接快速搭建时设为 `fast`。已锁定的搭建方式不再进入提问选项。后续 `ask_human` 只补齐其他未决事项,合并回答时必须保留已有 `intake.designMode`;不能因为回答里没有重复提到模式、结构化问题被合并、上下文压缩或重新生成 brief 而回退到 Fast。只有用户明确说“改用 Fast/Plan”时才能切换,并以最后一次明确选择为准。
|
|
20
|
+
|
|
17
21
|
将整组问题放在一次结构化提问调用中。宿主限制问题数量时,合并为“搭建方式”“导航归属与布局”“业务模块、页面与风格”等复合问题;选项数量或多选能力不足时,用允许自由输入的同组问题列出完整选择。不得因为工具数量限制而拆成多轮。只有回答遗漏、互相冲突或产生新的关键业务疑问时才针对性追问。
|
|
18
22
|
|
|
19
23
|
| 事项 | 何时询问 | 用户可见问题与选项 |
|
|
@@ -48,9 +52,12 @@
|
|
|
48
52
|
|
|
49
53
|
写入 `.cache/openyida/<项目名>/requirement-brief.json`,遵守 [输出契约](../SKILL.md#输出)。
|
|
50
54
|
|
|
55
|
+
`Write` 会创建所需父目录;不要为此先调用 Bash `mkdir`。
|
|
56
|
+
|
|
51
57
|
本步骤是保存答案,不是重新撰写需求文档:
|
|
52
58
|
|
|
53
59
|
- 合并已读取的需求、资源信息和本轮回答,完成必要确认后一次写入;中断恢复可保存未确认草稿,但不得把草稿当作已确认输入。
|
|
60
|
+
- 合并采用保留式更新:未被本轮提问的已确认字段不得用默认值覆盖。尤其 `intake.designMode` 是同一次搭建的粘性选择;若本轮没有询问搭建方式,写回值必须与提问前一致。
|
|
54
61
|
- 简短需求使用简短事实、列表和必要的页面标识,不扩写背景、价值分析、完整字段表、页面区块或验收标准;这些设计工作由后续 PRD 与视觉设计负责。用户已提供的字段、关系、流程和页面细项必须保留,不为精简而丢弃,也不重复改写成多份摘要。
|
|
55
62
|
- 不另建 `brief.md`、简报 HTML 或简报附件,不为保存 JSON 单独安排一次模型扩写或用户确认。用户回答清楚后直接记录,不能再展示整份简报询问“是否确认需求”。
|
|
56
63
|
- 已有确认记录且需求未变化时直接复用;后续只补充已确定的视觉映射或更新用户变更涉及的字段,不因阶段切换重写全文或重新生成页面 key。
|
|
@@ -58,11 +65,26 @@
|
|
|
58
65
|
- `intake` 记录 `firstBuild`、`sourceDetail`(`detailed/brief`)、`designMode`(`fast/plan`)、`confirmed`。未决事项处理完毕后才将 confirmed 设为 true。
|
|
59
66
|
- `navigation` 记录 `type/source/reason`;自定义导航的 `variant` 为 `side/top/mixed/dock`。顶部浮导使用 `top`,默认浮导或用户指定的通栏样式写入 `reason`,PRD 与视觉设计共同沿用;`dock` 表示底部悬浮胶囊。导航明暗由视觉选择记录。
|
|
60
67
|
- 业务模块答案写入 `coreFunctions/businessObjects/explicitScope`;与页面范围合问时,分别保存模块事实与 `pageScenes`,不要只保留模块或页面数量。
|
|
68
|
+
- `targetUsers/businessGoals/coreFunctions/businessObjects/pageScenes` 一律保持数组类型;单个目标也写成单元素数组。Plan 的 `visualSelection.themeId` 在保存 confirmed brief 前补齐;导航 type 使用 `platform-l-shape/platform-top/platform-side/custom` 精确枚举。
|
|
69
|
+
- 用户先用“应用/系统”描述背景、后续又明确“只完成/随后完成一个”具体资源并交付时,以具体资源作为本轮执行边界。`explicitScope` 写对应的 forms/processes/reports/pages/delivery 数组并设置 `allowInferredResources:false`;不把“应用”自动扩展为示例数据、工作台、自定义列表或其他未点名资源。
|
|
61
70
|
- `pageScenes` 使用 `{key,name,kind,purpose}`,key 是稳定场景标识,kind 为 `custom-page/form/process-form/report`。保留用户提供的细项;工作台通常排在首位。
|
|
62
71
|
- `visualSelection` 保存已确认的风格要求;视觉技能将其映射为主题、主色和导航明暗,具体字段见计划编写契约。
|
|
63
72
|
- `resourceContext` 记录已验证的复用资源,`explicitScope` 保留明确范围;来源中的完整业务细节保留在对应事实中。
|
|
64
73
|
- `constraints.prohibitedActions` 贯穿 PRD、design 与实现,不得在下游被“默认主题”“默认发布”或“修复后重试”覆盖。
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
Plan 的视觉选择直接使用下面的对象结构,不另写 `styleDescription/primaryColor/navigationStyle` 等平级别名:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"themeId": "airy-structured-clarity",
|
|
80
|
+
"visualDirection": {"label": "轻盈结构", "description": "清晰、简洁、适合持续业务操作", "source": "requirement"},
|
|
81
|
+
"colorStrategy": {"primaryColor": "#1677FF", "primaryColorName": "专业蓝", "source": "requirement", "usage": "主操作与选中态", "surfaceTone": "brand-tinted"},
|
|
82
|
+
"navigationStyle": {"structure": "side", "tone": "light", "source": "requirement", "selectionReason": "沿用平台侧边导航"}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
用户未指定色值但允许代为设计时,可按所选主题给出一个明确色值并标记 `source: "ai_default"`;不要留到 `design-plan init` 失败后再选择。
|
|
87
|
+
|
|
88
|
+
全部必要回答写回后,直接进入已选 Fast / Plan 的规划流程;进入路由前再次核对 `intake.designMode` 与本轮最后一次明确选择一致。Plan 仍在方案生成后确认当前搭建方案。内部只检查 JSON 可解析、已确认选择完整、页面 key 唯一且稳定、没有影响搭建的未决问题,不另起简报评审。交互工具与面向用户的文案遵守 [用户交互契约](../../yida-design/references/ask-human-interaction-contract.md)。
|
|
67
89
|
|
|
68
90
|
后续创建的资源 ID 写入执行上下文;用户需求发生变化时更新同一份 brief,并通知下游修改受影响的内容。
|