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.
@@ -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 = { success, groups, stateFile, results: state.results };
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.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.';
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
  };
@@ -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 + '/' + appType + '/workbench/' + formUuid;
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: authRef.baseUrl + '/' + appType + '/workbench/' + formUuid,
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 + '/' + appType + '/workbench/' + formUuid;
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 + '/' + appType + '/workbench/' + formUuid;
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
- await saveFormSchema(authRef, appType, formUuid, firstRead.schema, firstRead.version, 4);
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
- await saveFormSchema(authRef, appType, formUuid, firstRead.schema, firstRead.version, 4);
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 + '/' + appType + '/workbench/' + formUuid;
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 + '/' + appType + '/workbench/' + formUuid;
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
- const runtime = builderPath.runtime || {};
481
- const managedRuntime = String(env.OPENYIDA_MANAGED_RUNTIME || '').trim().toLowerCase();
482
- const managedCloudAgent = managedRuntime === 'cloud' ||
483
- runtime.runtime === 'web_sandbox' ||
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: managedCloudAgent ? 'managed_cloud_agent' : 'non_cloud_agent',
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: managedCloudAgent ? 'omit' : 'include',
494
+ admin: injectedAuthUsable ? 'omit' : 'include',
496
495
  },
497
496
  };
498
497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.9.14",
3
+ "version": "2026.9.15",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -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` 结果就是本轮资源回读证据:立即交付其中的真实链接并停止。不要再调用 `get-schema`、`list-forms`、数据管理技能、示例数据、主题或导航命令,除非创建结果明确缺少 ID/链接或用户另外要求这些内容。
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. **窄范围停止点**:完成 `explicitScope` 中的资源回读与真实链接交付后立即停止;不得为了满足完整应用默认完成条件继续进入被裁剪的 Step 5-8。
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
- 标准首版必须直接执行 init 返回的 `materialize.command`;不得先试 `--from-preview`、`preview`、无参数 materialize 或 shell 重定向。成功 JSON 已包含 `outputs.html` 和 `revision`,直接用于下一步同一次结构化提问,不再 Glob 目录,也不额外 Read `prd.md`、`design.md` 或 `build-plan.json`。
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. 必须实际调用 `ask_human` 创建结构化提问,并通过同一次调用的 `attachments` 携带可打开的 `prd/<项目名>/build-plan.html`;附件对象固定使用 `name: "build-plan.html"`,并将 `revision` 设为当前 `meta.revision`。调用必须遵守交互契约中的实际参数模板:一个顶层 `question`、恰好两个顶层 `options`(value 只能是 `confirm_build`、`continue_editing`)、同次调用的 `attachments`、`revision` 和 `submitLabel`。禁止使用 `fields`、`text`、`textarea`,禁止增加“调整说明”或其他条件式输入。只输出方案正文或普通 assistant 文本后结束本轮属于未完成,严禁用它替代 `ask_human`;也不得改成项目标题,或先发普通文本附件、再单独提问。
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
- 收到“确认并开始搭建”且回传 revision 等于展示 revision 后,直接进入同版本资源实施。确认之后严禁再次 materialize、patch、Edit 或 Read 计划来“同步确认状态”;不存在可写 `meta.planState.planConfirmed` 的确认命令。`explicitScope.allowInferredResources=false` 时也不执行主题 CSS、应用设置或导航交接,只创建范围内资源并回读、交付。
49
+ 收到 `confirm_build` 且回传 revision 等于展示 revision 后,以该版本计划作为实施阶段的唯一方案输入并直接创建范围内资源。`explicitScope.allowInferredResources=false` 时,实施范围由 `explicitScope` 决定,完成资源创建、回读和交付。
50
50
 
51
51
  ## 4. 处理调整
52
52
 
53
- 用户选择“继续调整”后保持在 Plan Design,本轮不创建应用、表单、流程或页面。下一轮再单独询问需要修改的内容;不要把调整说明、自由文本或 textarea 塞进最终确认卡。收到具体调整后才执行下面的 patch,物化新 revision 并重新展示最终确认。
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
- 明确窄交付时,方案只描述 `explicitScope` 中的资源;确认后裁剪无关步骤,在这些资源回读并交付真实链接后停止。不得因用户使用“应用”一词自行增加 seed records、自定义页面、主题或导航工作。
29
+ 明确范围的方案以 `explicitScope` 作为完整执行清单。确认后的每个写操作都对应清单中的一个资源或交付项;清单资源全部回读且真实链接完成交付时,本轮达到完成态。
29
30
 
30
31
  初次编写只读 CLI 返回的紧凑契约、当前主题上下文及共享需求;模板全文由 CLI 读取。具体组件定制、暗色浮层或复杂页面需要额外规则时,再读取对应章节。
31
32
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  回答齐全后直接保存内部需求记录并进入 2.1;不把“生成需求简报”列为独立任务,不再扩写或展示简报请用户确认。已有确认记录且需求未变化时直接复用。记录粒度、保存和校验规则统一遵守上述需求分析流程。
14
14
 
15
- 显式搭建方式属于本次任务的粘性输入。若用户在首次消息已选择 Plan,澄清业务模块、页面或风格时不得再次询问模式,也不得在合并回答后改写为 Fast;进入 2.1 前必须以用户最后一次明确选择校验 `intake.designMode`。Fast 同理。
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` 且本轮只有一个普通表单时走最短路径:Write 一份字段文件后调用一次 `openyida create-form create`。Write 会创建父目录,禁止先用 `ls` Bash 检查缓存目录。成功结果已包含真实 formUuid/链接时,直接交付并停止;不得继续 `get-schema`、写 schema evidence、加载数据管理技能、造示例数据、更新主题或排序导航。只有创建结果缺少本次明确验收所需字段时才做一次针对性回读。
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
- [Step 5:写入初始表单数据](step-5-seed-records.md)
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
- - 先写 2-3 句业务交付总结,再给一个名为“应用访问入口”的入口组。
71
- - 调用 `notify_human` 前必须先输出上述正文总结;用户明确要求记录数、资源 ID、发布状态或核验结果时,先在正文写完经 readback 支撑的简洁清单,再调用交付工具。链接卡或平台产物卡不能替代正文总结与显式要求的清单。
72
- - 一次完整应用搭建只产生这一组用户可见交付,不把表单、流程、报表、页面、资源清单或内部文件分别登记为附件、链接卡或下载卡。
73
- - 业务资源只在总结中按能力或数量概述是默认规则,例如“已完成 4 张业务表单、1 条审批流程和 1 个经营看板”;不默认输出资源 ID 表格、资源清单、长列表、appType、formUuid、pageId、reportId。
74
- - 用户或调用方明确要求资源清单、资源 UUID/ID、发布状态或测试数据摘要时,final 必须在业务总结与唯一入口组之间补充一个简洁的“交付清单”。清单只列本轮已通过真实返回值或只读 readback 核验的资源名称、类型和 ID,并同时写明主页面发布状态及 seed records 写入/抽查摘要;不得遗漏已创建或发布的资源,不得用链接卡代替正文清单,也不得编造未知 ID。
75
- - 即使用户没有要求技术清单,业务总结中的资源数量、seed records 数量以及“已写入/已验证/已就绪”等完成状态也必须逐资源来自真实返回值或只读 readback。证据不完整时缩小表述范围并明确未核验项,禁止为了让总结完整而补齐推测数字。
76
- - 新增、修改或发布单个具体页面时,仍只交付当前页面,不扩展成完整应用入口组。
77
- - 完整应用的入口组始终包含“应用工作台” `{base_url}/{appType}/workbench`。
78
- - 主页面在 PRD 中为 `entryMode=standalone`,且 Step 8 回读确认 `isRenderNav=false` 时,入口组额外包含“独立业务入口” `{base_url}/{appType}/custom/{formUuid}`;否则不得输出。
79
- - 先读取 `openyida agent-capabilities --summary-json` 的 `application_entry_policy.entries.admin`:值为 `include` 时,入口组额外包含“应用开发后台” `{base_url}/{appType}/admin`;值为 `omit` 时不得输出。不要根据 Agent 名称或自然语言猜测云端/非云端。
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
- - [ ] final 先写业务总结,再给唯一一组“应用访问入口”;
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
- 批量命令超过前台时限进入后台属于正常行为。此时必须保留原任务和 `<forms.json>.state.json`,等待运行时自动回传结果;禁止调用 `ToolStop`,禁止删除 `.state.json`/`.lock`,禁止改变参数再次调用 batch。若最终返回 `FORM_BATCH_PARTIAL_FAILURE`,在本轮原样保留结构化错误并停止;禁止模型侧再调用 `create-form create`、`update` 或 `resume` 补洞。CLI 会在同一次 batch 内对已取得真实 `formUuid` 的空壳表单执行一次保守恢复。
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
- - 当前批次返回 `success:false` 时,以返回的 `results`、子命令诊断和 `.state.json` 为准停止本轮创建;不得为补齐失败项改用单条 `create-form create`,也不得把剩余项拆成新的 batch。修正输入或为已知资源补入 `formUuid` 后,后续恢复仍使用原任务文件执行 batch。
67
- - 恢复失败任务前,核对已创建资源并修复。准备新任务文件时,用 `formUuid` 复用完整表单,仅为确认尚未创建的表单保留创建任务。
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
- - 首次 batch 返回 background pending 时等待运行时自动投递完成结果;pending 不是失败,也不是重试信号,不得再次调用 batch。
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` 语义问题:只使用一个顶层 `question` 和恰好两个顶层 `options`,附件与版本必须放在同一次调用中。下面先记录内部逻辑身份;`interactionId`、`questionType`、`prompt`、`allowCustom` `writeBackPath` 只用于状态理解,不是可传给工具的参数:
143
+ 最终确认是一个 `single_select` 语义问题。它只有下面这一份可调用 schema;顶层字段集合固定为 `question`、`title`、`options`、`attachments`、`revision`、`submitLabel`。将路径和 `{revision}` 替换为本次 materialize 返回的真实值:
144
144
 
145
- ```json
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
- 最终确认 payload 禁止使用 `fields`、`text`、`textarea`,也禁止增加“调整说明”或其他条件式补充问题;`options` value 集合必须恰好为 `confirm_build`、`continue_editing`。确认与调整内容不能合并收集。
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
- 1. 首次生成计划时创建 `meta.revision`,设置 `meta.status=draft`、`meta.planState.planConfirmed=false`。
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
- 同一目标、同一次搭建复用已确认方式;该选择在需求澄清、`ask_human` 恢复和 brief 合并中保持粘性,回答未重复提到模式不代表改选。只有用户明确要求改用另一方式时才更新 `intake.designMode`。切换目标时重新判断。已有应用的局部增改、单页美化和主题调整直接处理本次任务,遇到会改变范围的问题再澄清。
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
- 先分析再提问。用户已明确的信息直接采用;同一次搭建已回答的问题直接复用。首次提问必须在同一轮一次性收集所有尚未明确的搭建方式(Fast / Plan)、业务模块、页面与表单范围和设计风格。不要先问模块和风格,收到回答后再另起一轮补问模式或页面。导航方式及布局由 AI 按下方规则判断。
17
+ 先分析再提问。用户已明确的信息直接采用;同一次搭建已回答的问题直接复用。首次结构化提问以一次原子交互收集全部未决的搭建方式(Fast / Plan)、业务模块、页面与表单范围和设计风格,回答完成后统一写回。导航方式及布局由 AI 按下方规则判断。
18
18
 
19
- 先锁定用户已经明确的搭建方式:用户写明 `Plan`、先出 PRD/方案并确认后搭建时,立即把草稿的 `intake.designMode` 设为 `plan`;用户写明 `Fast` 或直接快速搭建时设为 `fast`。已锁定的搭建方式不再进入提问选项。后续 `ask_human` 只补齐其他未决事项,合并回答时必须保留已有 `intake.designMode`;不能因为回答里没有重复提到模式、结构化问题被合并、上下文压缩或重新生成 brief 而回退到 Fast。只有用户明确说“改用 Fast/Plan”时才能切换,并以最后一次明确选择为准。
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
- - 合并采用保留式更新:未被本轮提问的已确认字段不得用默认值覆盖。尤其 `intake.designMode` 是同一次搭建的粘性选择;若本轮没有询问搭建方式,写回值必须与提问前一致。
68
+ - 合并采用字段级保留式更新:本轮问题声明的写回字段使用新回答,其余已确认字段沿用现值;`intake.designMode` 按上面的模式状态转换规则写回。
67
69
  - 简短需求使用简短事实、列表和必要的页面标识,不扩写背景、价值分析、完整字段表、页面区块或验收标准;这些设计工作由后续 PRD 与视觉设计负责。用户已提供的字段、关系、流程和页面细项必须保留,不为精简而丢弃,也不重复改写成多份摘要。
68
70
  - 不另建 `brief.md`、简报 HTML 或简报附件,不为保存 JSON 单独安排一次模型扩写或用户确认。用户回答清楚后直接记录,不能再展示整份简报询问“是否确认需求”。
69
71
  - 已有确认记录且需求未变化时直接复用;后续只补充已确定的视觉映射或更新用户变更涉及的字段,不因阶段切换重写全文或重新生成页面 key。