openyida 2026.9.14 → 2026.9.15
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/batch.js +54 -6
- package/lib/app/create-form.js +89 -11
- package/lib/core/agent-capabilities.js +6 -7
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/SKILL.md +11 -5
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +4 -4
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +4 -3
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +1 -1
- package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +5 -2
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +18 -14
- package/yida-skills/skills/yida-create-form-page/SKILL.md +17 -3
- package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +4 -3
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +13 -44
- package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +6 -4
|
@@ -225,6 +225,25 @@ function readbackMismatch(formUuid, form, schema) {
|
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
function buildPartialFailureRecovery(results) {
|
|
229
|
+
const items = Object.values(results || {});
|
|
230
|
+
const hasUnknownWrite = items.some(item =>
|
|
231
|
+
item && (item.status === 'running' || (item.status === 'failed' && !item.formUuid))
|
|
232
|
+
);
|
|
233
|
+
const recoveryAction = hasUnknownWrite
|
|
234
|
+
? 'inspect_unknown_write_then_reconcile'
|
|
235
|
+
: 'rerun_unchanged_plan';
|
|
236
|
+
return { recoveryAction, nextAction: recoveryAction };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildDeliveryUrls(baseUrl, appType, formUuid) {
|
|
240
|
+
const normalizedBaseUrl = String(baseUrl || '').replace(/\/+$/, '');
|
|
241
|
+
if (!normalizedBaseUrl || !appType || !formUuid) { return {}; }
|
|
242
|
+
const appUrl = `${normalizedBaseUrl}/${appType}/workbench`;
|
|
243
|
+
const formUrl = `${appUrl}/${formUuid}`;
|
|
244
|
+
return { url: formUrl, formUrl, appUrl };
|
|
245
|
+
}
|
|
246
|
+
|
|
228
247
|
function execute(args, { execFile: execFileImpl = execFile } = {}) {
|
|
229
248
|
return new Promise((resolve, reject) => {
|
|
230
249
|
execFileImpl(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
|
|
@@ -317,13 +336,15 @@ async function run(args, dependencies = {}) {
|
|
|
317
336
|
try {
|
|
318
337
|
const state = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { fingerprint, appType: options.appType, results: {} };
|
|
319
338
|
if (state.fingerprint !== fingerprint) { invalid('state belongs to a different plan; reconcile existing resources before preparing a new batch'); }
|
|
320
|
-
await call(['login', '--check-only', '--json']);
|
|
339
|
+
const loginStatus = await call(['login', '--check-only', '--json']);
|
|
340
|
+
const baseUrl = loginStatus?.base_url || loginStatus?.baseUrl || '';
|
|
321
341
|
// Read back completed resources before their IDs can be used by dependent forms.
|
|
322
342
|
for (const form of forms.filter(item => state.results[item.key]?.status === 'success')) {
|
|
323
343
|
const item = state.results[form.key];
|
|
324
344
|
const schema = await call(['get-schema', options.appType, item.formUuid, '--field-map-json']);
|
|
325
345
|
if (schema.formUuid !== item.formUuid || !Array.isArray(schema.fields)) { invalid(`schema: ${form.key}`); }
|
|
326
346
|
item.fields = schema.fields;
|
|
347
|
+
Object.assign(item, buildDeliveryUrls(baseUrl, options.appType, item.formUuid));
|
|
327
348
|
}
|
|
328
349
|
const resolve = (key, field) => {
|
|
329
350
|
const item = state.results[key];
|
|
@@ -336,6 +357,7 @@ async function run(args, dependencies = {}) {
|
|
|
336
357
|
await schedule(forms, options.concurrency, state.results, async form => {
|
|
337
358
|
let formUuid = form.formUuid || state.results[form.key]?.formUuid;
|
|
338
359
|
let shouldResume = Boolean(!form.formUuid && formUuid);
|
|
360
|
+
let delivery = buildDeliveryUrls(baseUrl, options.appType, formUuid);
|
|
339
361
|
const resolvedFields = mapReferences(form.fields, resolve);
|
|
340
362
|
if (!formUuid) {
|
|
341
363
|
const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(resolvedFields), '--no-open'];
|
|
@@ -344,6 +366,12 @@ async function run(args, dependencies = {}) {
|
|
|
344
366
|
const created = await call(argv);
|
|
345
367
|
if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
|
|
346
368
|
formUuid = created.formUuid;
|
|
369
|
+
delivery = {
|
|
370
|
+
...buildDeliveryUrls(baseUrl, options.appType, formUuid),
|
|
371
|
+
...(created.url ? { url: created.url } : {}),
|
|
372
|
+
...(created.formUrl ? { formUrl: created.formUrl } : {}),
|
|
373
|
+
...(created.appUrl ? { appUrl: created.appUrl } : {}),
|
|
374
|
+
};
|
|
347
375
|
} catch (error) {
|
|
348
376
|
const createdFormUuid = error.output?.formUuid || error.output?.details?.formUuid;
|
|
349
377
|
if (typeof createdFormUuid !== 'string' || !createdFormUuid.startsWith('FORM')) {
|
|
@@ -357,25 +385,43 @@ async function run(args, dependencies = {}) {
|
|
|
357
385
|
save(state);
|
|
358
386
|
let resumed = false;
|
|
359
387
|
if (shouldResume) {
|
|
360
|
-
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
388
|
+
const resumeOutput = await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
389
|
+
delivery = {
|
|
390
|
+
...buildDeliveryUrls(baseUrl, options.appType, formUuid),
|
|
391
|
+
...(resumeOutput.url ? { url: resumeOutput.url } : {}),
|
|
392
|
+
...(resumeOutput.formUrl ? { formUrl: resumeOutput.formUrl } : {}),
|
|
393
|
+
...(resumeOutput.appUrl ? { appUrl: resumeOutput.appUrl } : {}),
|
|
394
|
+
};
|
|
361
395
|
resumed = true;
|
|
362
396
|
}
|
|
363
397
|
let schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
364
398
|
if (!readbackMatchesExpectedFields(schema, resolvedFields) && !resumed && !form.formUuid) {
|
|
365
|
-
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
399
|
+
const resumeOutput = await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
400
|
+
delivery = {
|
|
401
|
+
...buildDeliveryUrls(baseUrl, options.appType, formUuid),
|
|
402
|
+
...(resumeOutput.url ? { url: resumeOutput.url } : {}),
|
|
403
|
+
...(resumeOutput.formUrl ? { formUrl: resumeOutput.formUrl } : {}),
|
|
404
|
+
...(resumeOutput.appUrl ? { appUrl: resumeOutput.appUrl } : {}),
|
|
405
|
+
};
|
|
366
406
|
resumed = true;
|
|
367
407
|
schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
368
408
|
}
|
|
369
409
|
if (schema.formUuid !== formUuid || !readbackMatchesExpectedFields(schema, resolvedFields)) {
|
|
370
410
|
throw readbackMismatch(formUuid, { ...form, fields: resolvedFields }, schema);
|
|
371
411
|
}
|
|
372
|
-
return { formUuid, fields: schema.fields };
|
|
412
|
+
return { formUuid, fields: schema.fields, ...delivery };
|
|
373
413
|
}, () => save(state));
|
|
374
414
|
const success = Object.values(state.results).every(item => item.status === 'success');
|
|
375
|
-
const output = {
|
|
415
|
+
const output = {
|
|
416
|
+
success,
|
|
417
|
+
groups,
|
|
418
|
+
stateFile,
|
|
419
|
+
results: state.results,
|
|
420
|
+
...((baseUrl && options.appType) ? { appUrl: `${String(baseUrl).replace(/\/+$/, '')}/${options.appType}/workbench` } : {}),
|
|
421
|
+
};
|
|
376
422
|
if (!success) {
|
|
377
423
|
output.errorCode = 'FORM_BATCH_PARTIAL_FAILURE';
|
|
378
|
-
output
|
|
424
|
+
Object.assign(output, buildPartialFailureRecovery(state.results));
|
|
379
425
|
}
|
|
380
426
|
console.log(JSON.stringify(output));
|
|
381
427
|
if (!output.success) { process.exitCode = 1; }
|
|
@@ -397,5 +443,7 @@ module.exports = {
|
|
|
397
443
|
execute,
|
|
398
444
|
expectedReadbackFields,
|
|
399
445
|
readbackMatchesExpectedFields,
|
|
446
|
+
buildPartialFailureRecovery,
|
|
447
|
+
buildDeliveryUrls,
|
|
400
448
|
validateStaticDefinition,
|
|
401
449
|
};
|
package/lib/app/create-form.js
CHANGED
|
@@ -4858,7 +4858,10 @@ async function saveFormSchema(authRef, appType, formUuid, schema, version, stepO
|
|
|
4858
4858
|
formUuid,
|
|
4859
4859
|
result: sanitizeFailureResult(saveResult),
|
|
4860
4860
|
});
|
|
4861
|
-
if (failureContext) {
|
|
4861
|
+
if (failureContext && failureContext.deferFailureOutput) {
|
|
4862
|
+
// Resume may safely resolve an HTTP 5xx by exact readback. Defer the
|
|
4863
|
+
// failure payload so a recovered command emits one authoritative result.
|
|
4864
|
+
} else if (failureContext) {
|
|
4862
4865
|
emitCreateFormPostCreateFailure(Object.assign({}, failureContext, {
|
|
4863
4866
|
stage: 'saveFormSchema',
|
|
4864
4867
|
error: saveError,
|
|
@@ -4879,6 +4882,13 @@ async function saveFormSchema(authRef, appType, formUuid, schema, version, stepO
|
|
|
4879
4882
|
|
|
4880
4883
|
// ── create 模式主流程 ─────────────────────────────────
|
|
4881
4884
|
|
|
4885
|
+
function buildFormDeliveryUrls(baseUrl, appType, formUuid) {
|
|
4886
|
+
return {
|
|
4887
|
+
appUrl: baseUrl + '/' + appType + '/workbench',
|
|
4888
|
+
formUrl: baseUrl + '/' + appType + '/workbench/' + formUuid,
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4882
4892
|
async function mainValidateFields(parsedArgs) {
|
|
4883
4893
|
const { fieldsJsonOrFile } = parsedArgs;
|
|
4884
4894
|
assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
|
|
@@ -5034,13 +5044,13 @@ async function mainCreate(parsedArgs, authRef) {
|
|
|
5034
5044
|
}
|
|
5035
5045
|
|
|
5036
5046
|
// 输出结果
|
|
5037
|
-
const formUrl = authRef.baseUrl
|
|
5047
|
+
const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
|
|
5038
5048
|
result(true, t('create_form.create_success'), [
|
|
5039
5049
|
['Form UUID', formUuid],
|
|
5040
5050
|
['URL', formUrl],
|
|
5041
5051
|
]);
|
|
5042
5052
|
console.log(JSON.stringify(withBrowserHandoff(
|
|
5043
|
-
{ success: true, formUuid, formTitle, appType, fieldCount, icon: formIcon, iconSource: iconResolution.source, url: formUrl },
|
|
5053
|
+
{ success: true, formUuid, formTitle, appType, fieldCount, icon: formIcon, iconSource: iconResolution.source, url: formUrl, formUrl, appUrl },
|
|
5044
5054
|
formUrl,
|
|
5045
5055
|
{ stage: 'create_form_success', title: formTitle },
|
|
5046
5056
|
parsedArgs.browserOpenMode
|
|
@@ -5127,6 +5137,7 @@ async function createFormForLegacyProcessQuiet(context, input) {
|
|
|
5127
5137
|
}
|
|
5128
5138
|
const saveResult = await saveFormSchema(authRef, appType, formUuid, schema, serverRevision, 4);
|
|
5129
5139
|
const navIconResult = await updateCreatedFormNavigationIcon(authRef, appType, formUuid, formIcon);
|
|
5140
|
+
const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
|
|
5130
5141
|
return {
|
|
5131
5142
|
success: true,
|
|
5132
5143
|
appType,
|
|
@@ -5138,7 +5149,9 @@ async function createFormForLegacyProcessQuiet(context, input) {
|
|
|
5138
5149
|
navIconResult,
|
|
5139
5150
|
icon: formIcon,
|
|
5140
5151
|
iconSource: iconResolution.source,
|
|
5141
|
-
url:
|
|
5152
|
+
url: formUrl,
|
|
5153
|
+
formUrl,
|
|
5154
|
+
appUrl,
|
|
5142
5155
|
};
|
|
5143
5156
|
}
|
|
5144
5157
|
|
|
@@ -5319,7 +5332,7 @@ async function mainAddOption(parsedArgs, authRef) {
|
|
|
5319
5332
|
// Step 4: 保存 Schema
|
|
5320
5333
|
await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
|
|
5321
5334
|
|
|
5322
|
-
const formUrl = authRef.baseUrl
|
|
5335
|
+
const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
|
|
5323
5336
|
result(true, '选项追加成功', [
|
|
5324
5337
|
['Form UUID', formUuid],
|
|
5325
5338
|
['Field', fieldLabel],
|
|
@@ -5339,6 +5352,8 @@ async function mainAddOption(parsedArgs, authRef) {
|
|
|
5339
5352
|
skipped: skippedOptions,
|
|
5340
5353
|
totalOptions: existingDataSource.length,
|
|
5341
5354
|
url: formUrl,
|
|
5355
|
+
formUrl,
|
|
5356
|
+
appUrl,
|
|
5342
5357
|
}));
|
|
5343
5358
|
}
|
|
5344
5359
|
|
|
@@ -5438,7 +5453,7 @@ async function mainBindDataSource(parsedArgs, authRef) {
|
|
|
5438
5453
|
|
|
5439
5454
|
await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
|
|
5440
5455
|
|
|
5441
|
-
const formUrl = authRef.baseUrl
|
|
5456
|
+
const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
|
|
5442
5457
|
result(true, '字段数据源保存成功', [
|
|
5443
5458
|
['Form UUID', formUuid],
|
|
5444
5459
|
['Field', fieldLabel],
|
|
@@ -5459,6 +5474,8 @@ async function mainBindDataSource(parsedArgs, authRef) {
|
|
|
5459
5474
|
options: normalized.options.length,
|
|
5460
5475
|
filterLocal: targetComponent.props.filterLocal,
|
|
5461
5476
|
pageUrl: formUrl,
|
|
5477
|
+
formUrl,
|
|
5478
|
+
appUrl,
|
|
5462
5479
|
},
|
|
5463
5480
|
formUrl,
|
|
5464
5481
|
{ stage: 'bind_datasource_success', title: formUuid },
|
|
@@ -6109,6 +6126,59 @@ async function readResumeSchema(authRef, appType, formUuid) {
|
|
|
6109
6126
|
return { schemaResult, schema, version: extractSchemaServerRevision(schemaResult) };
|
|
6110
6127
|
}
|
|
6111
6128
|
|
|
6129
|
+
function isRetryableResumeSaveServerFailure(errorObject) {
|
|
6130
|
+
const status = Number(errorObject?.details?.result?.__httpStatus);
|
|
6131
|
+
return errorObject?.code === 'CREATE_FORM_SAVE_SCHEMA_FAILED' &&
|
|
6132
|
+
Number.isInteger(status) && status >= 500 && status <= 599;
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
async function saveResumeSchemaWithSafeRetry(authRef, appType, formUuid, schema, version, fields) {
|
|
6136
|
+
const save = (value, revision) => saveFormSchema(
|
|
6137
|
+
authRef, appType, formUuid, value, revision, 4, { deferFailureOutput: true }
|
|
6138
|
+
);
|
|
6139
|
+
try {
|
|
6140
|
+
await save(schema, version);
|
|
6141
|
+
return '';
|
|
6142
|
+
} catch (originalError) {
|
|
6143
|
+
if (!isRetryableResumeSaveServerFailure(originalError)) { throw originalError; }
|
|
6144
|
+
|
|
6145
|
+
let readback;
|
|
6146
|
+
try { readback = await readResumeSchema(authRef, appType, formUuid); } catch (_) { throw originalError; }
|
|
6147
|
+
const root = readback.schema.pages[0].componentsTree && readback.schema.pages[0].componentsTree[0];
|
|
6148
|
+
const container = root ? findFormContainer(root) : null;
|
|
6149
|
+
if (!container) { throw originalError; }
|
|
6150
|
+
const evidence = collectResumeFieldEvidence(container.children);
|
|
6151
|
+
const verified = buildResumeChanges(
|
|
6152
|
+
evidence,
|
|
6153
|
+
collectResumeDesiredFields(fields, { verifyNestedFields: true })
|
|
6154
|
+
);
|
|
6155
|
+
if (verified.conflicts.length > 0) { throw originalError; }
|
|
6156
|
+
if (verified.missing.length === 0) { return 'save_schema_recovered_by_readback'; }
|
|
6157
|
+
const retryPlan = buildResumeChanges(evidence, collectResumeDesiredFields(fields));
|
|
6158
|
+
if (retryPlan.conflicts.length > 0 || retryPlan.missing.length === 0) { throw originalError; }
|
|
6159
|
+
const applied = applyChangesToSchema(
|
|
6160
|
+
readback.schema,
|
|
6161
|
+
retryPlan.missing.map(function (field) { return { action: 'add', field }; }),
|
|
6162
|
+
{ verbose: true }
|
|
6163
|
+
);
|
|
6164
|
+
if ((applied.diagnostics || []).length > 0) { throw originalError; }
|
|
6165
|
+
const updatedContainer = findFormContainer(readback.schema.pages[0].componentsTree[0]);
|
|
6166
|
+
fillSerialNumberFormulas(updatedContainer.children, resolveCorpId(authRef.authData), appType, formUuid);
|
|
6167
|
+
|
|
6168
|
+
try {
|
|
6169
|
+
await save(readback.schema, readback.version);
|
|
6170
|
+
} catch (retryError) {
|
|
6171
|
+
originalError.details = Object.assign({}, originalError.details, {
|
|
6172
|
+
safeRetryAttempted: true,
|
|
6173
|
+
retryErrorCode: retryError?.code || 'CREATE_FORM_SAVE_SCHEMA_FAILED',
|
|
6174
|
+
retryResult: sanitizeFailureResult(retryError?.details?.result),
|
|
6175
|
+
});
|
|
6176
|
+
throw originalError;
|
|
6177
|
+
}
|
|
6178
|
+
return 'save_schema_retried';
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
|
|
6112
6182
|
async function mainResume(parsedArgs, authRef) {
|
|
6113
6183
|
const { appType, formUuid, fieldsJsonOrFile } = parsedArgs;
|
|
6114
6184
|
assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
|
|
@@ -6139,8 +6209,11 @@ async function mainResume(parsedArgs, authRef) {
|
|
|
6139
6209
|
if (resumeValidationRules.length > 0) {
|
|
6140
6210
|
applySmartValidations(firstRead.schema, resumeValidationRules);
|
|
6141
6211
|
}
|
|
6142
|
-
|
|
6212
|
+
const saveRecoveryStage = await saveResumeSchemaWithSafeRetry(
|
|
6213
|
+
authRef, appType, formUuid, firstRead.schema, firstRead.version, fields
|
|
6214
|
+
);
|
|
6143
6215
|
completedStages.push('rebuild_blank_schema', 'save_schema');
|
|
6216
|
+
if (saveRecoveryStage) { completedStages.push(saveRecoveryStage); }
|
|
6144
6217
|
recoveredBlankShell = true;
|
|
6145
6218
|
plan = {
|
|
6146
6219
|
existing: [],
|
|
@@ -6189,8 +6262,11 @@ async function mainResume(parsedArgs, authRef) {
|
|
|
6189
6262
|
const corpId = resolveCorpId(authRef.authData);
|
|
6190
6263
|
const updatedContainer = findFormContainer(firstRead.schema.pages[0].componentsTree[0]);
|
|
6191
6264
|
fillSerialNumberFormulas(updatedContainer.children, corpId, appType, formUuid);
|
|
6192
|
-
|
|
6265
|
+
const saveRecoveryStage = await saveResumeSchemaWithSafeRetry(
|
|
6266
|
+
authRef, appType, formUuid, firstRead.schema, firstRead.version, fields
|
|
6267
|
+
);
|
|
6193
6268
|
completedStages.push('add_missing_fields', 'save_schema');
|
|
6269
|
+
if (saveRecoveryStage) { completedStages.push(saveRecoveryStage); }
|
|
6194
6270
|
}
|
|
6195
6271
|
|
|
6196
6272
|
const finalRead = await readResumeSchema(authRef, appType, formUuid);
|
|
@@ -6211,7 +6287,7 @@ async function mainResume(parsedArgs, authRef) {
|
|
|
6211
6287
|
});
|
|
6212
6288
|
}
|
|
6213
6289
|
completedStages.push('verify_final_schema');
|
|
6214
|
-
const formUrl = authRef.baseUrl
|
|
6290
|
+
const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
|
|
6215
6291
|
const output = {
|
|
6216
6292
|
success: true,
|
|
6217
6293
|
appType,
|
|
@@ -6223,6 +6299,8 @@ async function mainResume(parsedArgs, authRef) {
|
|
|
6223
6299
|
finalFieldCount: collectResumeFieldEvidence(finalContainer.children).length,
|
|
6224
6300
|
recoveredBlankShell,
|
|
6225
6301
|
url: formUrl,
|
|
6302
|
+
formUrl,
|
|
6303
|
+
appUrl,
|
|
6226
6304
|
};
|
|
6227
6305
|
console.log(JSON.stringify(withBrowserHandoff(
|
|
6228
6306
|
output,
|
|
@@ -6384,14 +6462,14 @@ async function mainUpdate(parsedArgs, authRef) {
|
|
|
6384
6462
|
await saveFormSchema(authRef, appType, formUuid, schema, version, 6);
|
|
6385
6463
|
|
|
6386
6464
|
// 输出结果
|
|
6387
|
-
const formUrl = authRef.baseUrl
|
|
6465
|
+
const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
|
|
6388
6466
|
result(true, t('create_form.update_success'), [
|
|
6389
6467
|
['Form UUID', formUuid],
|
|
6390
6468
|
['URL', formUrl],
|
|
6391
6469
|
['Changes', String(appliedChanges.length)],
|
|
6392
6470
|
]);
|
|
6393
6471
|
console.log(JSON.stringify(withBrowserHandoff(
|
|
6394
|
-
{ success: true, formUuid, appType, changesApplied: appliedChanges.length, changes: appliedChanges, url: formUrl },
|
|
6472
|
+
{ success: true, formUuid, appType, changesApplied: appliedChanges.length, changes: appliedChanges, url: formUrl, formUrl, appUrl },
|
|
6395
6473
|
formUrl,
|
|
6396
6474
|
{ stage: 'update_form_success', title: formUuid },
|
|
6397
6475
|
parsedArgs.browserOpenMode
|
|
@@ -477,22 +477,21 @@ function buildFullAppArtifactRoute(manifest) {
|
|
|
477
477
|
}
|
|
478
478
|
|
|
479
479
|
function buildApplicationEntryPolicy(builderPath, env = process.env) {
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
runtime.tool === 'mulerun';
|
|
480
|
+
void env;
|
|
481
|
+
const auth = builderPath.auth || {};
|
|
482
|
+
const injectedAuthUsable = auth.auth_runtime === 'env_token_bootstrap' &&
|
|
483
|
+
auth.can_auto_use === true;
|
|
485
484
|
|
|
486
485
|
return {
|
|
487
486
|
schema_version: 1,
|
|
488
|
-
environment:
|
|
487
|
+
environment: injectedAuthUsable ? 'managed_cloud_agent' : 'non_cloud_agent',
|
|
489
488
|
delivery_unit: 'single_application_entry_group',
|
|
490
489
|
resource_delivery: 'summary_only',
|
|
491
490
|
internal_artifact_delivery: 'never',
|
|
492
491
|
entries: {
|
|
493
492
|
workbench: 'always',
|
|
494
493
|
custom: 'when_entry_mode_standalone_and_is_render_nav_false_readback',
|
|
495
|
-
admin:
|
|
494
|
+
admin: injectedAuthUsable ? 'omit' : 'include',
|
|
496
495
|
},
|
|
497
496
|
};
|
|
498
497
|
}
|
package/package.json
CHANGED
|
@@ -9,11 +9,17 @@ description: 宜搭完整应用开发编排技能。对普通 OpenYida 应用做
|
|
|
9
9
|
|
|
10
10
|
## 模式入口(先按这里路由)
|
|
11
11
|
|
|
12
|
+
`intake.designMode` 是整轮搭建工作流的模式状态,随 intake、需求记录、PRD/视觉设计、方案确认和实施阶段持续传递。它的初始值来自用户的明确选择,后续也只由用户新的明确模式选择更新;其余未决 intake 字段按当前模式的稳定默认策略补齐。Plan 依次产出需求事实、`build-plan.html` / `build-plan.json` 和带 revision 的最终确认;与当前 revision 匹配的 `confirm_build` 是进入真实资源实施阶段的状态转换。在此转换发生前,工作范围保持在需求、设计、方案产物和只读资源检查。
|
|
13
|
+
|
|
14
|
+
`continue_editing` 使当前计划沿既有 lineage 继续演进。根据变更粒度使用 `design-plan patch`,或编辑当前计划源后 materialize,产出单调递增的新 revision 并再次展示最终确认卡。最近一次获确认的 revision 是后续实施阶段的唯一方案输入。
|
|
15
|
+
|
|
12
16
|
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
17
|
|
|
14
|
-
##
|
|
18
|
+
## 执行步骤(进行时展示给用户)
|
|
19
|
+
|
|
20
|
+
先从已经确认的 `execution.explicitScope` 生成本轮步骤,再开始资源实施。步骤只对应范围内尚未完成的资源和交付;资源成功回读后从剩余步骤中移除。`allowInferredResources=true` 表示完整应用交付,可使用下面的完整应用模版;`allowInferredResources=false` 表示边界已闭合,使用“需求识别与分析 / 设计功能和页面 / 生成PRD方案&确认(Plan)+ 范围内资源步骤 + 检查范围并交付”的精简步骤。
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
完整应用搭建时使用对应模式的步骤名称。
|
|
17
23
|
|
|
18
24
|
**Plan 模式**
|
|
19
25
|
|
|
@@ -38,11 +44,11 @@ Plan 模式只读取 `workflow/step-1-resource-context.md`、`workflow/step-2-de
|
|
|
38
44
|
7. 发布页面与配置导航
|
|
39
45
|
8. 检查功能并交付
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
已有应用时省略“创建应用”,后续步骤重新编号。步骤状态按真实进度更新,有独立输入的工作可同时进行。
|
|
42
48
|
|
|
43
49
|
用户明确把本轮交付限定为一个或若干具体表单、流程、报表或页面时,按 `explicitScope` 只保留达到该交付所需的步骤;即使需求背景使用“应用/系统”,也不自动补示例数据、自定义工作台、主题设置、导航排序或其他资源。Plan 模式仍生成并确认方案,但确认后只执行该窄范围。
|
|
44
50
|
|
|
45
|
-
若窄范围只要求创建一个普通表单并交付链接,成功的 `create-form create`
|
|
51
|
+
若窄范围只要求创建一个普通表单并交付链接,成功的 `create-form create` 结果就是本轮资源回读证据。`url` 是兼容字段,与 `formUrl` 表示同一表单入口;`appUrl` 表示应用工作台入口。按用户要求的入口层级选择权威字段,资源保存成功且证据齐全后完成当前范围;证据缺失时执行一次针对性只读回查。普通表单保存成功后进入可用状态,自定义展示页遵循页面发布生命周期。
|
|
46
52
|
|
|
47
53
|
禁区:待办标题、说明和进度不出现技能名、命令、文件路径、登录账号、内部资源 ID;这些留在工具调用里。用户明确询问技术细节时再解释。
|
|
48
54
|
|
|
@@ -81,7 +87,7 @@ Plan 模式只读取 `workflow/step-1-resource-context.md`、`workflow/step-2-de
|
|
|
81
87
|
7. **删除必须确认**:用户要求删除应用时,先展示应用名称、应用 ID 和影响范围,等待明确“确认删除”后才能执行。
|
|
82
88
|
8. **列表页选择**:默认使用普通表单的数据管理页;用户明确要求自定义列表页时才创建 display 页面。
|
|
83
89
|
9. **交付物收口**:Step 2 的三个文件和 Step 9 的 build manifest 都是内部文件,不是用户交付物。表单、流程、报表和页面只在业务总结中概述,不逐项生成用户可见附件;宿主支持交付工具时,final 只交付一次“应用访问入口”组。
|
|
84
|
-
10.
|
|
90
|
+
10. **范围完成点**:以 `explicitScope` 的资源集合为任务队列;集合清空且真实链接已交付时,本轮完成。完整应用默认完成条件只在 `allowInferredResources=true` 时生效。
|
|
85
91
|
|
|
86
92
|
## 关键决策树
|
|
87
93
|
|
|
@@ -24,7 +24,7 @@ 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
|
-
|
|
27
|
+
标准首版的生成入口是 init 返回的 `materialize.command`。成功 JSON 中的 `outputs.html` 和 `revision` 直接构成下一步结构化确认的附件与版本输入。按模块更新过的草稿使用本节单列的 `--from-preview` 命令;诊断使用下方 `--check` 命令。
|
|
28
28
|
|
|
29
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 再重复生成。
|
|
30
30
|
|
|
@@ -37,7 +37,7 @@ HTML 保留“需求总览、数据模型、业务流程、页面规划”四章
|
|
|
37
37
|
按 [用户交互契约](../../../yida-design/references/ask-human-interaction-contract.md) 执行:
|
|
38
38
|
|
|
39
39
|
1. 在会话中展示“当前这版方案”,并用 3–7 条业务摘要说明方案内容。
|
|
40
|
-
2.
|
|
40
|
+
2. 实际调用 `ask_human` 创建结构化提问。调用对象严格采用交互契约中的唯一 payload schema;`attachments` 携带 `name: "build-plan.html"`、`path: "prd/<项目名>/build-plan.html"`,`revision` 使用当前 `meta.revision`,`options` 固定为 `confirm_build` 和 `continue_editing`。一次成功调用同时建立方案展示、版本绑定和最终选择。
|
|
41
41
|
3. 结构化交互成功创建后内部记录 `presentedRevision=meta.revision`。询问“确认并开始搭建”或“继续调整”,提交时由宿主原样回传 revision,将确认结果绑定到本次展示版本。用户可见版本称为“第 N 版方案”,展示序号与内部 revision 绑定。
|
|
42
42
|
|
|
43
43
|
只有以下条件同时成立才交接;它们由本轮 ask_human 请求和回传在运行时判定,不要求把确认状态写回 workspace 文件:
|
|
@@ -46,11 +46,11 @@ HTML 保留“需求总览、数据模型、业务流程、页面规划”四章
|
|
|
46
46
|
- `meta.planState.planConfirmed=true`
|
|
47
47
|
- `meta.revision=presentedRevision=confirmedRevision`
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
收到 `confirm_build` 且回传 revision 等于展示 revision 后,以该版本计划作为实施阶段的唯一方案输入并直接创建范围内资源。`explicitScope.allowInferredResources=false` 时,实施范围由 `explicitScope` 决定,完成资源创建、回读和交付。
|
|
50
50
|
|
|
51
51
|
## 4. 处理调整
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
`continue_editing` 把工作流从 `awaiting_confirmation` 转为 `editing`。下一次交互收集变更内容;收到变更后更新当前计划源,物化新 revision,并重新进入最终确认。
|
|
54
54
|
|
|
55
55
|
按字段更新源事实并重新生成,例如同时调整品牌色和圆角:
|
|
56
56
|
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
- 规划功能、页面和配色:设计功能和页面
|
|
8
8
|
- 生成方案并等待用户确认:生成PRD方案&确认
|
|
9
|
-
-
|
|
10
|
-
-
|
|
9
|
+
- 方案确认后:读取同一 revision 的 `execution.explicitScope`,为其中非空的资源集合生成对应实施步骤,最后增加“检查范围并交付”
|
|
10
|
+
- `allowInferredResources=true` 的完整应用:按 `yida-app` 完整应用模版生成后续步骤
|
|
11
|
+
- `allowInferredResources=false` 的闭合范围:后续步骤总集等于确认方案中的资源步骤与交付步骤;每完成并回读一个资源,就从剩余集合中移除
|
|
11
12
|
|
|
12
13
|
沿用已有待办,不把下面的内部执行顺序另建成一份任务列表。
|
|
13
14
|
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
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
27
|
5. 读取精确路径 `workflow/plan/step-4-deliver.md`,按其中契约直接展示并确认当前方案。超大需求需要展示中间进展或用户明确要求边生成边查看时,才使用 [按模块更新方案](../incremental-preview.md);普通首版不逐模块预览和重复渲染。
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
明确范围的方案以 `explicitScope` 作为完整执行清单。确认后的每个写操作都对应清单中的一个资源或交付项;清单资源全部回读且真实链接完成交付时,本轮达到完成态。
|
|
29
30
|
|
|
30
31
|
初次编写只读 CLI 返回的紧凑契约、当前主题上下文及共享需求;模板全文由 CLI 读取。具体组件定制、暗色浮层或复杂页面需要额外规则时,再读取对应章节。
|
|
31
32
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
回答齐全后直接保存内部需求记录并进入 2.1;不把“生成需求简报”列为独立任务,不再扩写或展示简报请用户确认。已有确认记录且需求未变化时直接复用。记录粒度、保存和校验规则统一遵守上述需求分析流程。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`intake.designMode` 是本次搭建的权威路由输入。进入 intake 和每次 `ask_human` 恢复后都读取该状态:未初始化时收集明确模式选择,已初始化时按问题声明的写回字段合并回答,新的明确模式选择负责更新模式状态。进入 2.1 时直接按合并后的当前状态路由。
|
|
16
16
|
|
|
17
17
|
执行规划前读取 `constraints.prohibitedActions`。PRD 与 design 必须把禁止项写成实现门禁:`theme-file` 禁止时沿用现有平台主题且不安排主题文件任务;`page-source` 禁止时只允许只读核查与非源码配置;`publish` 禁止时把发布明确标记为跳过。不得为了满足默认九步流程静默删除这些约束。
|
|
18
18
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
`explicitScope.allowInferredResources=false`
|
|
5
|
+
`explicitScope.allowInferredResources=false` 且本轮只有一个普通表单时走最短路径:以 Write 创建字段文件及其父目录,随后调用一次 `openyida create-form create`。成功结果中的 `url`/`formUrl` 是表单入口,`appUrl` 是应用工作台入口。将该表单标记为已完成并计算 `remainingScope`;为空时按用户要求的入口层级交付并完成本轮范围。普通表单保存成功后进入可用状态,自定义展示页遵循页面发布生命周期;验收证据缺失时执行一次针对性只读回查。
|
|
6
6
|
|
|
7
7
|
拿到真实 `appType` 和已确认的业务契约即可开始本步骤,不等待主题 CSS 上传或应用主题设置回读。主题分支与本步骤并行,按 [主题与业务资源的依赖](parallel-work.md#主题与业务资源的依赖) 汇合。
|
|
8
8
|
|
|
@@ -91,4 +91,7 @@ PRD 导航类型为自定义导航时,对本轮创建或复用的每个表单
|
|
|
91
91
|
|
|
92
92
|
## 下一步
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
- `remainingScope` 为空:→ [Step 9:按确认范围输出与收尾](step-9-output-finish.md)
|
|
95
|
+
- `remainingScope` 包含 seed records:→ [Step 5:写入初始表单数据](step-5-seed-records.md)
|
|
96
|
+
- `remainingScope` 包含自定义页面:→ [Step 6:创建或复用主页面](step-6-main-page.md)
|
|
97
|
+
- 其他资源:继续执行本步骤中与该资源类型对应的能力,直到资源集合完成
|
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
主入口与页面入口必须取自本轮 CLI 成功结果返回的 `appUrl`、`workbenchUrl` 或 `url`,并在后续只读回查中保持一致。不得由模型根据 `appType` 自行拼接或猜测链接;下文 URL 规则仅用于校验服务端/CLI 返回值,不是缺失链接时的生成规则。若成功结果没有权威 URL,必须明确交付失败并执行只读回查,不能交付空卡片或用模板补齐。
|
|
18
18
|
|
|
19
|
+
入口层级由用户的交付目标决定:应用访问、应用入口或应用工作台使用成功结果的 `appUrl`;具体表单、流程、报表或页面入口使用对应资源结果的 `url` / `workbenchUrl`。同一结果同时提供两种入口时,优先满足用户明确点名的层级。
|
|
20
|
+
|
|
19
21
|
## 完成条件核对
|
|
20
22
|
|
|
23
|
+
`execution.explicitScope.allowInferredResources=false` 时走范围完成路径:逐项核对清单内资源的成功结果或只读回读、用户要求的入口层级和交付状态;`remainingScope` 为空且真实链接已交付即为完成。下方完整应用检查表仅适用于 `allowInferredResources=true`。
|
|
24
|
+
|
|
21
25
|
完整应用默认完成需要同时满足:
|
|
22
26
|
|
|
23
27
|
1. 主页面发布成功;
|
|
@@ -67,17 +71,17 @@
|
|
|
67
71
|
|
|
68
72
|
## 结果输出格式
|
|
69
73
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
- 用户或调用方明确要求资源清单、资源 UUID/ID
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
- 主页面在 PRD 中为 `entryMode=standalone`,且 Step 8 回读确认 `isRenderNav=false`
|
|
79
|
-
-
|
|
80
|
-
-
|
|
74
|
+
- 准备 2-3 句业务交付总结,并给一个名为“应用访问入口”的入口组。
|
|
75
|
+
- `notify_human` 是终态交付动作,其终态 artifact 的可见 `description` 是本轮业务总结、readback 事实和必要上下文的统一载体;用户要求的记录数、资源 ID、发布状态或核验结果也写入这里。调用成功即完成本轮交付。
|
|
76
|
+
- 一次完整应用 run 交付一组用户可见的“应用访问入口”。同一应用的后续交付从已有资源和已验证 URL 生成当前 run 的入口组;资源变更由用户本轮明确要求的变更范围驱动。
|
|
77
|
+
- 业务资源在总结中按能力或数量概述,例如“已完成 4 张业务表单、1 条审批流程和 1 个经营看板”。
|
|
78
|
+
- 用户或调用方明确要求资源清单、资源 UUID/ID、发布状态或测试数据摘要时,终态 artifact 的 `description` 包含简洁的“交付清单”。清单是本轮真实返回值和只读 readback 的投影,覆盖已创建或发布资源的名称、类型、ID、主页面发布状态及 seed records 写入/抽查摘要;未知信息标记为未核验。
|
|
79
|
+
- 业务总结中的资源数量、seed records 数量和完成状态与逐资源真实返回值/readback 一一对应;证据不完整的资源标记为未核验。
|
|
80
|
+
- 新增、修改或发布单个具体页面时,交付当前页面并保持单页范围。
|
|
81
|
+
- 完整应用的入口组包含“应用工作台” `{base_url}/{appType}/workbench`。
|
|
82
|
+
- 主页面在 PRD 中为 `entryMode=standalone`,且 Step 8 回读确认 `isRenderNav=false` 时,入口组包含“独立业务入口” `{base_url}/{appType}/custom/{formUuid}`。
|
|
83
|
+
- “应用开发后台”的入口谓词是 `application_entry_policy.entries.admin=include`;命中时加入 `{base_url}/{appType}/admin`,值为 `omit` 时入口组保持已有入口。该策略的事实源是 `openyida agent-capabilities --summary-json`,其中 include 由已注入可用鉴权 `builder_path.auth.auth_runtime=env_token_bootstrap && can_auto_use=true` 推导。
|
|
84
|
+
- 工作台、符合条件的独立业务入口和开发后台共同组成同一个应用入口组。
|
|
81
85
|
- 不把 `g.alicdn.com` 的 `index.css`、`index.js`、`index.html`、`locales/*.json`、构建产物 URL、CDN 资源 URL 或中间文件链接当成最终结果展示。
|
|
82
86
|
- 调用方或评测要求结构化结果时,额外输出顶层 `skillsUsed`,只填写本轮实际读取并使用的 `yida-*` 子技能名;不得把计划使用或未加载的技能写入。
|
|
83
87
|
|
|
@@ -132,10 +136,10 @@
|
|
|
132
136
|
|
|
133
137
|
## Checklist
|
|
134
138
|
|
|
135
|
-
- [ ]
|
|
136
|
-
- [ ] `notify_human`
|
|
139
|
+
- [ ] 唯一一组“应用访问入口”的终态 artifact `description` 已包含业务总结;
|
|
140
|
+
- [ ] `notify_human` 作为最后一个动作调用,用户显式要求的事实清单和资源 ID 已进入可见 `description`;
|
|
137
141
|
- [ ] final 中每个资源数量和数据完成声明都有对应资源自己的成功返回值/readback;未把一张表单的记录数套用到其他表单;
|
|
138
|
-
- [ ] 用户或调用方显式要求资源清单/UUID
|
|
142
|
+
- [ ] 用户或调用方显式要求资源清单/UUID/发布状态/测试数据摘要时,终态 artifact `description` 已完整列出经核验的交付清单;
|
|
139
143
|
- [ ] 已写入轻量 build-manifest 并运行页面/资源数量完整性风险检查;未通过时没有声称“已按 PRD 完成搭建”;
|
|
140
144
|
- [ ] 已按 `constraints.prohibitedActions` 调整完成条件;跳过写操作时没有伪造已换肤、已修改源码或已发布;
|
|
141
145
|
- [ ] 未把内部文件或每个业务资源分别交付;
|
|
@@ -110,7 +110,15 @@ description: 表单页面创建与更新;支持 19 种业务字段和 Divider
|
|
|
110
110
|
|
|
111
111
|
关联字段必须把引用放在 `associationForm` 内。推荐使用紧凑写法 `"associationForm": { "$form": "customer", "field": "客户名称" }`;batch 会将其规范化为 `associationForm.formUuid` 和 `associationForm.mainFieldId`。完整写法则分别在 `formUuid` 使用 `{ "$form": "customer" }`、在 `mainFieldId` 使用 `{ "$form": "customer", "field": "客户名称" }`。不要把 `$form` 放在 `AssociationFormField` 顶层,也不要用 `batch --help`、空参数或临时计划探索格式;技能中的结构就是正式契约。
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Batch 以任务文件指纹和 `<forms.json>.state.json` 共同标识一次批量操作,恢复动作由结果中的 `recoveryAction` 唯一决定:
|
|
114
|
+
|
|
115
|
+
| 当前结果 | 状态事实 | 下一动作 |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| background pending | 原任务仍在执行,task/state/lock 保持为同一操作 | 保持当前执行单元,后续只接收该任务的最终结果 |
|
|
118
|
+
| `FORM_BATCH_PARTIAL_FAILURE` + `rerun_unchanged_plan` | 已知 `formUuid` 已记录在 state,原任务指纹可安全恢复 | 后续使用原任务文件和原参数重新执行同一 batch,由 CLI 从 state 复用已知资源 |
|
|
119
|
+
| `FORM_BATCH_PARTIAL_FAILURE` + `inspect_unknown_write_then_reconcile` | 至少一个远端写结果缺少资源 ID,当前指纹进入待核对状态 | 先回读远端资源;核对完成后建立新的 reconcile 任务,用已确认 `formUuid` 表示已有表单,仅保留确定尚未创建的任务 |
|
|
120
|
+
|
|
121
|
+
CLI 在同一次 batch 内对已取得真实 `formUuid` 的空壳表单执行一次保守恢复。每个结果状态只沿表中对应的下一动作推进。
|
|
114
122
|
|
|
115
123
|
## 官方表单示例范式
|
|
116
124
|
|
|
@@ -179,9 +187,11 @@ openyida create-form create <appType> <formTitle> <fieldsJsonOrFile> [--layout d
|
|
|
179
187
|
输出:
|
|
180
188
|
|
|
181
189
|
```json
|
|
182
|
-
{"success":true,"formUuid":"FORM-XXX","formTitle":"用户信息表","appType":"APP_xxx","fieldCount":4,"icon":"name-card","iconSource":"auto","url":"{base_url}/APP_xxx/workbench/FORM-XXX"}
|
|
190
|
+
{"success":true,"formUuid":"FORM-XXX","formTitle":"用户信息表","appType":"APP_xxx","fieldCount":4,"icon":"name-card","iconSource":"auto","url":"{base_url}/APP_xxx/workbench/FORM-XXX","formUrl":"{base_url}/APP_xxx/workbench/FORM-XXX","appUrl":"{base_url}/APP_xxx/workbench"}
|
|
183
191
|
```
|
|
184
192
|
|
|
193
|
+
`url` 是兼容字段,与 `formUrl` 表示同一表单入口;`appUrl` 表示应用工作台入口。普通表单保存成功后进入可用状态,自定义展示页使用 `publish-page` 生命周期。终态交付按用户请求的层级选择对应权威入口和 `resourceType`/`resourceId`:应用级使用 `appUrl`、`app_home`、`appType`,表单级使用 `formUrl`、`form`、`formUuid`。
|
|
194
|
+
|
|
185
195
|
完整应用模式下的下一步:
|
|
186
196
|
|
|
187
197
|
1. 将 `formUuid`、字段摘要和字段 JSON 路径写入 `.cache/<项目名>-schema.json`。
|
|
@@ -221,12 +231,16 @@ openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> --json
|
|
|
221
231
|
该命令先回读目标表单并核对字段,只添加可唯一判定的缺失字段,保存后再次回读;同名异类型、重复
|
|
222
232
|
目标字段、归属不匹配或回读不确定时均停止且不写入。它不会新建替代表单,也不会覆盖已有字段。
|
|
223
233
|
|
|
234
|
+
`resume` 的保存端 HTTP 5xx 恢复流程固定为:精确回读目标表单;目标字段已存在时收口为成功,目标字段缺失且无冲突时绑定最新服务端 revision 执行一次保存,再做最终回读。成功 JSON 提供真实 `formUuid`、表单入口 `url`/`formUrl` 和应用工作台入口 `appUrl`,据此完成终态交付;用户要求资源 ID 时,将已验证的 `formUuid` 写入终态 artifact 的可见 `description`。
|
|
235
|
+
|
|
224
236
|
输出:
|
|
225
237
|
|
|
226
238
|
```json
|
|
227
|
-
{"success":true,"formUuid":"FORM-YYY","appType":"APP_XXX","changesApplied":1,"changes":[{"action":"update","label":"备注","changedProps":"required","resolved":{"label":"备注","fieldId":"textField_xxx","componentName":"TextField"},"updatedProps":{"required":true}}],"url":"{base_url}/APP_XXX/workbench/FORM-YYY"}
|
|
239
|
+
{"success":true,"formUuid":"FORM-YYY","appType":"APP_XXX","changesApplied":1,"changes":[{"action":"update","label":"备注","changedProps":"required","resolved":{"label":"备注","fieldId":"textField_xxx","componentName":"TextField"},"updatedProps":{"required":true}}],"url":"{base_url}/APP_XXX/workbench/FORM-YYY","formUrl":"{base_url}/APP_XXX/workbench/FORM-YYY","appUrl":"{base_url}/APP_XXX/workbench"}
|
|
228
240
|
```
|
|
229
241
|
|
|
242
|
+
`resume` 的终态交付沿用相同入口映射:应用级交付选择 `appUrl` / `app_home`,表单级交付选择 `formUrl` / `form`;`url` 继续兼容表单入口。
|
|
243
|
+
|
|
230
244
|
常见 compact changes:
|
|
231
245
|
|
|
232
246
|
```json
|
|
@@ -63,9 +63,10 @@ batch 会在预校验前把紧凑写法规范化为下面的完整结构;前
|
|
|
63
63
|
- `--concurrency 1..4` 设置并发上限,默认 3。
|
|
64
64
|
- 结果保存在任务文件旁的 `.state.json`,包含每张表单的 ID、字段和状态。主流程从中汇总资源上下文,再配置导航。
|
|
65
65
|
- 相同任务再次执行时,成功表单回读复用。失败及中断任务保留原状态,其依赖标记为 blocked;独立任务继续。
|
|
66
|
-
-
|
|
67
|
-
-
|
|
66
|
+
- `success:false` 时,`results`、子命令诊断、`.state.json` 和 `recoveryAction` 共同描述本轮终态及下一步。
|
|
67
|
+
- `rerun_unchanged_plan` 表示原任务指纹可安全恢复:后续使用原任务文件和原参数执行同一 batch,CLI 从 state 读取已知 ID 并复用成功表单。
|
|
68
|
+
- `inspect_unknown_write_then_reconcile` 表示当前指纹进入待核对状态:先回读远端资源;核对完成后建立新的 reconcile 任务,用已确认 `formUuid` 表示已有表单,仅保留确定尚未创建的任务。
|
|
68
69
|
- `.lock` 防止同一任务重复启动。进程异常退出遗留锁时,确认原进程已结束、核对已创建资源后再清理。
|
|
69
|
-
-
|
|
70
|
+
- background pending 表示原 batch 仍在执行;task/state/lock 继续代表同一执行单元,后续只接受该任务的最终结果。
|
|
70
71
|
|
|
71
72
|
已有表单的字段修改仍使用 update/patch 等命令;不同表单可分别更新,同一张表单由一个任务维护。流程审批按 `yida-create-process` 执行,等待其依赖的表单就绪后再配置。
|
|
@@ -140,41 +140,9 @@ Plan Design 完成当前版本后,按以下顺序与用户交互:
|
|
|
140
140
|
|
|
141
141
|
`build-plan.html` 不承载对话控件或确认按钮;用户在会话中完成确认。
|
|
142
142
|
|
|
143
|
-
最终确认是一个 `single_select`
|
|
143
|
+
最终确认是一个 `single_select` 语义问题。它只有下面这一份可调用 schema;顶层字段集合固定为 `question`、`title`、`options`、`attachments`、`revision`、`submitLabel`。将路径和 `{revision}` 替换为本次 materialize 返回的真实值:
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
"interactionId": "plan_confirm_r{revision}",
|
|
148
|
-
"questionType": "confirm",
|
|
149
|
-
"title": "确认整体方案",
|
|
150
|
-
"prompt": "是否按当前这版方案开始搭建?",
|
|
151
|
-
"options": [
|
|
152
|
-
{
|
|
153
|
-
"value": "confirm_build",
|
|
154
|
-
"label": "确认并开始搭建",
|
|
155
|
-
"description": "按当前方案创建应用、表单、流程和页面。"
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
"value": "continue_editing",
|
|
159
|
-
"label": "继续调整",
|
|
160
|
-
"description": "继续完善当前方案,确认后再开始搭建。"
|
|
161
|
-
}
|
|
162
|
-
],
|
|
163
|
-
"allowCustom": false,
|
|
164
|
-
"writeBackPath": "meta.planState",
|
|
165
|
-
"attachments": [
|
|
166
|
-
{
|
|
167
|
-
"name": "当前搭建方案",
|
|
168
|
-
"path": "prd/<项目名>/build-plan.html"
|
|
169
|
-
}
|
|
170
|
-
],
|
|
171
|
-
"revision": "{revision}"
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
#### 实际 `ask_human` 调用参数
|
|
176
|
-
|
|
177
|
-
模型必须直接复制下面的可调用结构,并将路径、`{revision}` 替换为本次 materialize 返回的真实值:
|
|
145
|
+
#### 最终确认 `ask_human` payload
|
|
178
146
|
|
|
179
147
|
```json
|
|
180
148
|
{
|
|
@@ -203,9 +171,7 @@ Plan Design 完成当前版本后,按以下顺序与用户交互:
|
|
|
203
171
|
}
|
|
204
172
|
```
|
|
205
173
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
用户选择 `continue_editing` 后保持在 Plan Design,本次确认恢复不创建任何应用资源;在下一轮用新的问题单独收集要修改的内容,完成 patch/materialize、生成新 revision 后,再展示同样结构的最终确认。用户选择 `confirm_build` 且回传 revision 匹配时,才进入资源实施。
|
|
174
|
+
`options` 固定为 `confirm_build`、`continue_editing` 两项。最终确认只收集当前 revision 的去向;调整内容由进入 editing 状态后的下一次交互收集。内部交互身份使用 `plan_confirm_r{revision}`,回答写入 `meta.planState`。
|
|
209
175
|
|
|
210
176
|
## 版本与确认状态
|
|
211
177
|
|
|
@@ -225,11 +191,14 @@ Plan Design 完成当前版本后,按以下顺序与用户交互:
|
|
|
225
191
|
}
|
|
226
192
|
```
|
|
227
193
|
|
|
228
|
-
|
|
194
|
+
状态转换规则:
|
|
195
|
+
|
|
196
|
+
| 当前状态 | 事件 | 下一状态与产出 |
|
|
197
|
+
| --- | --- | --- |
|
|
198
|
+
| 初始 | 首次生成计划 | 创建 `meta.revision`,进入 `draft` |
|
|
199
|
+
| `draft` / `editing` | materialize 成功并展示计划 | 进入 `awaiting_confirmation`,`presentedRevision=meta.revision` |
|
|
200
|
+
| `awaiting_confirmation` | `confirm_build` 且回传 revision 匹配 | 进入 `confirmed`,记录 `planConfirmed=true`、`confirmedRevision`、交互 ID 和确认时间,并以该 revision 进入资源实施 |
|
|
201
|
+
| `awaiting_confirmation` | `continue_editing` | 进入 `editing`;下一次交互收集变更,生成新 revision 后重新 materialize 和确认 |
|
|
202
|
+
| 任意非终态 | 用户取消或关闭交互 | 进入 `stopped`,保留当前计划产物 |
|
|
229
203
|
|
|
230
|
-
|
|
231
|
-
2. 每次修改影响搭建计划事实时生成新的 `meta.revision`,并清空旧确认信息。
|
|
232
|
-
3. 计划展示完成后设置 `meta.status=awaiting_confirmation` 和 `presentedRevision=meta.revision`。
|
|
233
|
-
4. 用户在最终确认交互中选择“确认并开始搭建”时,设置 `meta.status=confirmed`、`planConfirmed=true`、`confirmedRevision=meta.revision`,同时记录交互 ID 和确认时间。
|
|
234
|
-
5. 只有 `meta.status=confirmed`、`planConfirmed=true` 且 `meta.revision=presentedRevision=confirmedRevision` 时,Plan Design 才能返回 `yida-app` Step 3。
|
|
235
|
-
6. 用户选择“继续调整”时保持在 Plan Design,本轮不创建应用资源;下一轮单独收集调整内容,生成新 revision 并重新确认。用户取消或关闭交互时停止执行,不创建应用资源。
|
|
204
|
+
资源实施的进入条件是 `meta.status=confirmed`、`planConfirmed=true` 且 `meta.revision=presentedRevision=confirmedRevision`。
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
用户同时要求先出计划和少提问时采用 Plan,复用已知信息。用户指定的详细计划作为规划基础,补齐实施所需的缺项。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
同一目标、同一次搭建以 `intake.designMode` 作为权威模式状态。需求澄清、`ask_human` 恢复和 brief 合并都携带该状态;用户新的明确模式选择触发状态转换,其他回答只更新各自声明的字段。新目标重新初始化模式判断。已有应用的局部增改、单页美化和主题调整直接处理本次任务,遇到会改变范围的问题再澄清。
|
|
13
13
|
|
|
14
14
|
两种方式共用业务和视觉契约;Plan 额外展示方案并绑定当前版本确认。面向用户的表达与宿主适配见 [用户交互契约](ask-human-interaction-contract.md)。
|
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
## 2. 确认首次搭建的未决事项
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
先分析再提问。用户已明确的信息直接采用;同一次搭建已回答的问题直接复用。首次结构化提问以一次原子交互收集全部未决的搭建方式(Fast / Plan)、业务模块、页面与表单范围和设计风格,回答完成后统一写回。导航方式及布局由 AI 按下方规则判断。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
`intake.designMode` 是搭建方式的权威状态。初始化时把用户明确选择的 Plan / Fast 意图分别映射为 `plan` / `fast`;状态未确定时才把搭建方式纳入提问,用户明确选择后完成初始化。后续结构化回答按各问题声明的写回字段合并,搭建方式只在用户给出新的明确模式选择时发生状态转换,并以该选择作为后续路由依据。
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
`intake.designMode` 与其他 intake 字段分开演进:搭建方式保持当前明确选择,尚未确定的模块、页面、风格等字段由该模式的稳定默认策略补齐。Plan 的 intake 完成后进入方案生成;与当前 revision 匹配的最终确认使工作流进入实施阶段。每次合并 `ask_human` 回答或重建 brief 都按这一状态转换规则写回。
|
|
22
|
+
|
|
23
|
+
将整组问题放在一次结构化提问调用中。宿主限制问题数量时,合并为“搭建方式”“业务模块、页面与风格”等复合问题;选项数量或多选能力不足时,用允许自由输入的同组问题列出完整选择。回答遗漏、互相冲突或产生新的关键业务疑问时,再开启一次针对性补充交互。
|
|
22
24
|
|
|
23
25
|
| 事项 | 何时询问 | 用户可见问题与选项 |
|
|
24
26
|
| --- | --- | --- |
|
|
@@ -63,7 +65,7 @@ AI 在已确定的导航方式内,结合业务模块数量、层级、切换
|
|
|
63
65
|
本步骤是保存答案,不是重新撰写需求文档:
|
|
64
66
|
|
|
65
67
|
- 合并已读取的需求、资源信息和本轮回答,完成必要确认后一次写入;中断恢复可保存未确认草稿,但不得把草稿当作已确认输入。
|
|
66
|
-
-
|
|
68
|
+
- 合并采用字段级保留式更新:本轮问题声明的写回字段使用新回答,其余已确认字段沿用现值;`intake.designMode` 按上面的模式状态转换规则写回。
|
|
67
69
|
- 简短需求使用简短事实、列表和必要的页面标识,不扩写背景、价值分析、完整字段表、页面区块或验收标准;这些设计工作由后续 PRD 与视觉设计负责。用户已提供的字段、关系、流程和页面细项必须保留,不为精简而丢弃,也不重复改写成多份摘要。
|
|
68
70
|
- 不另建 `brief.md`、简报 HTML 或简报附件,不为保存 JSON 单独安排一次模型扩写或用户确认。用户回答清楚后直接记录,不能再展示整份简报询问“是否确认需求”。
|
|
69
71
|
- 已有确认记录且需求未变化时直接复用;后续只补充已确定的视觉映射或更新用户变更涉及的字段,不因阶段切换重写全文或重新生成页面 key。
|