openyida 2026.9.13 → 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.
Files changed (30) hide show
  1. package/lib/app/canvas-compile.js +37 -4
  2. package/lib/app/create-form/batch.js +54 -6
  3. package/lib/app/create-form.js +89 -11
  4. package/lib/app/display-page-readback.js +63 -5
  5. package/lib/app/get-schema.js +158 -5
  6. package/lib/app/publish.js +127 -5
  7. package/lib/core/agent-capabilities.js +6 -7
  8. package/lib/core/command-manifest.js +2 -2
  9. package/lib/core/locales/en.js +9 -0
  10. package/lib/core/locales/zh.js +9 -0
  11. package/lib/core/utils.js +358 -0
  12. package/package.json +1 -1
  13. package/yida-skills/skills/yida-app/SKILL.md +11 -5
  14. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +5 -3
  15. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +4 -3
  16. package/yida-skills/skills/yida-app/workflow/step-2-design.md +2 -2
  17. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +5 -2
  18. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +18 -14
  19. package/yida-skills/skills/yida-create-form-page/SKILL.md +18 -3
  20. package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +4 -3
  21. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +22 -19
  22. package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
  23. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +1 -1
  24. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
  25. package/yida-skills/skills/yida-integration/SKILL.md +1 -0
  26. package/yida-skills/skills/yida-nav-shell/SKILL.md +4 -4
  27. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  28. package/yida-skills/skills/yida-prd/workflow/step-2-information-architecture.md +2 -2
  29. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
  30. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +17 -18
@@ -24,12 +24,13 @@
24
24
  * 3) 第三方依赖以 `window.<别名>` 形式引用(antd→window.antd、react→window.React …),
25
25
  * 这些 UMD 依赖由画布运行时依据 importedModules 白名单按需注入。
26
26
  *
27
- * 因此本地编译 = Babel 把 JSX/TS 转成 ES5 → 把 import 改写成 window 别名引用、
28
- * 把 export default 改写成画布入口 `YidaComp` → 正则抽出依赖包名。
27
+ * 因此本地编译 = Babel 把 JSX/TS 转成 JS → 把 import 改写成 window 别名引用、
28
+ * 把 export default 改写成画布入口 `YidaComp` → UglifyJS 压缩 → 正则抽出依赖包名。
29
29
  */
30
30
 
31
31
  const Babel = require('@babel/standalone');
32
32
  const globals = require('globals');
33
+ const UglifyJS = require('uglify-js');
33
34
  const {
34
35
  assertNoEmojiInArtifactName,
35
36
  assertNoEmojiInText,
@@ -877,6 +878,36 @@ function assertCanvasRuntimeParseable(runtimeCode, options = {}) {
877
878
  }
878
879
  }
879
880
 
881
+ /**
882
+ * 压缩画布运行态代码,并显式保留装配器读取的 YidaComp 入口名。
883
+ */
884
+ function minifyCanvasRuntime(runtimeCode, options = {}) {
885
+ let result;
886
+ try {
887
+ result = UglifyJS.minify(runtimeCode, {
888
+ compress: true,
889
+ mangle: {
890
+ reserved: ['YidaComp'],
891
+ },
892
+ });
893
+ } catch (error) {
894
+ result = { error };
895
+ }
896
+ if (!result || result.error || typeof result.code !== 'string' || !result.code) {
897
+ const detail = result && result.error && result.error.message
898
+ ? result.error.message
899
+ : '压缩结果为空';
900
+ throw new CliError(`Code Canvas runtimeCode 压缩失败: ${detail}`, {
901
+ code: 'OPENYIDA_CANVAS_MINIFY_FAILED',
902
+ details: {
903
+ stage: 'canvas_minify',
904
+ sourcePath: options.sourcePath || '',
905
+ },
906
+ });
907
+ }
908
+ return result.code;
909
+ }
910
+
880
911
  /**
881
912
  * 本地编译 Code Canvas 源码。
882
913
  * @param {string} source 原始 React/JSX/TSX 源码
@@ -984,11 +1015,12 @@ function compileCanvasLocal(source, options = {}) {
984
1015
  configFile: false,
985
1016
  });
986
1017
 
987
- const runtimeCode = stage2.code || '';
988
- assertNoEmojiInText(runtimeCode, {
1018
+ const unminifiedRuntimeCode = stage2.code || '';
1019
+ assertNoEmojiInText(unminifiedRuntimeCode, {
989
1020
  artifact: options.sourcePath ? options.sourcePath + ' runtime' : 'Code Canvas runtime',
990
1021
  code: 'OPENYIDA_CANVAS_SOURCE_EMOJI_FORBIDDEN',
991
1022
  });
1023
+ const runtimeCode = minifyCanvasRuntime(unminifiedRuntimeCode, options);
992
1024
  assertCanvasRuntimeParseable(runtimeCode, options);
993
1025
  assertDependencyManifestConsistent(runtimeCode, importedModules, options);
994
1026
  return {
@@ -1059,6 +1091,7 @@ module.exports = {
1059
1091
  resolveWindowAlias,
1060
1092
  shouldAllowUnsupportedBareImports,
1061
1093
  assertCanvasRuntimeParseable,
1094
+ minifyCanvasRuntime,
1062
1095
  findBareDependencyGlobalIssues,
1063
1096
  findSelfReferentialDependencyBindingIssues,
1064
1097
  findDependencyManifestIssues,
@@ -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
@@ -23,10 +23,30 @@ function fingerprint(value) {
23
23
  return crypto.createHash('sha256').update(normalized).digest('hex');
24
24
  }
25
25
 
26
+ function rawFingerprint(value) {
27
+ if (typeof value !== 'string' || !value) {
28
+ return '';
29
+ }
30
+ return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
31
+ }
32
+
33
+ function positiveSize(value) {
34
+ const size = Number(value);
35
+ return Number.isFinite(size) && size > 0 ? size : 0;
36
+ }
37
+
38
+ function sha256(value) {
39
+ const digest = String(value || '').toLowerCase();
40
+ return /^[a-f0-9]{64}$/.test(digest) ? digest : '';
41
+ }
42
+
26
43
  function createEmptyDisplayInfo() {
27
44
  return {
28
45
  hasYidaCodeCanvas: false,
46
+ hasCodeBundle: false,
29
47
  hasNativeJsx: false,
48
+ codeBundleCount: 0,
49
+ bundleIds: [],
30
50
  runtimeCodeBytes: 0,
31
51
  sourceCodeBytes: 0,
32
52
  compiledCodeBytes: 0,
@@ -34,6 +54,8 @@ function createEmptyDisplayInfo() {
34
54
  componentCount: 0,
35
55
  canvasRuntimeCode: '',
36
56
  canvasSourceCode: '',
57
+ canvasRuntimeSha256: '',
58
+ canvasSourceSha256: '',
37
59
  nativeCompiledCode: '',
38
60
  nativeSourceCode: '',
39
61
  };
@@ -79,12 +101,37 @@ function traverseDisplayNodes(node, info) {
79
101
 
80
102
  if (node.componentName === 'YidaCodeCanvas') {
81
103
  const props = node.props || {};
104
+ const codeBundle = props.codeBundle && typeof props.codeBundle === 'object'
105
+ ? props.codeBundle
106
+ : null;
82
107
  info.hasYidaCodeCanvas = true;
83
108
  info.componentCount++;
84
- info.canvasRuntimeCode = props.runtimeCode || '';
85
- info.canvasSourceCode = props.code || '';
109
+ if (typeof props.runtimeCode === 'string' && props.runtimeCode) {
110
+ info.canvasRuntimeCode = props.runtimeCode;
111
+ }
112
+ if (typeof props.code === 'string' && props.code) {
113
+ info.canvasSourceCode = props.code;
114
+ }
86
115
  info.runtimeCodeBytes += codeBytes(props.runtimeCode);
87
116
  info.sourceCodeBytes += codeBytes(props.code);
117
+ if (codeBundle) {
118
+ const bundleId = String(codeBundle.bundleId || '');
119
+ const runtime = codeBundle.runtime && typeof codeBundle.runtime === 'object'
120
+ ? codeBundle.runtime
121
+ : {};
122
+ const source = codeBundle.source && typeof codeBundle.source === 'object'
123
+ ? codeBundle.source
124
+ : {};
125
+ info.hasCodeBundle = true;
126
+ info.codeBundleCount++;
127
+ if (bundleId && !info.bundleIds.includes(bundleId)) {
128
+ info.bundleIds.push(bundleId);
129
+ }
130
+ info.runtimeCodeBytes += positiveSize(runtime.size);
131
+ info.sourceCodeBytes += positiveSize(source.size);
132
+ info.canvasRuntimeSha256 = sha256(runtime.sha256) || info.canvasRuntimeSha256;
133
+ info.canvasSourceSha256 = sha256(source.sha256) || info.canvasSourceSha256;
134
+ }
88
135
  addImportedModules(info.importedModules, props.importedModules);
89
136
  } else if (node.componentName === 'Jsx') {
90
137
  info.hasNativeJsx = true;
@@ -133,7 +180,9 @@ function hasExpectedDisplayComponent(info, publishMode) {
133
180
  return false;
134
181
  }
135
182
  if (publishMode === 'canvas') {
136
- return info.hasYidaCodeCanvas && info.runtimeCodeBytes > 0;
183
+ return info.hasYidaCodeCanvas && (
184
+ info.runtimeCodeBytes > 0 || !!info.canvasRuntimeSha256
185
+ );
137
186
  }
138
187
  return info.hasNativeJsx && info.compiledCodeBytes > 0;
139
188
  }
@@ -144,7 +193,10 @@ function summarizeDisplayPublishInfo(info) {
144
193
  }
145
194
  return {
146
195
  hasYidaCodeCanvas: info.hasYidaCodeCanvas,
196
+ hasCodeBundle: info.hasCodeBundle,
147
197
  hasNativeJsx: info.hasNativeJsx,
198
+ codeBundleCount: info.codeBundleCount,
199
+ bundleIds: info.bundleIds.slice(),
148
200
  runtimeCodeBytes: info.runtimeCodeBytes,
149
201
  sourceCodeBytes: info.sourceCodeBytes,
150
202
  compiledCodeBytes: info.compiledCodeBytes,
@@ -159,8 +211,13 @@ function verifyPublishedContentMatch(readbackSchema, expectedSchemaContent, publ
159
211
  const displayComponentPresent = hasExpectedDisplayComponent(readbackInfo, publishMode);
160
212
  const readbackArtifact = getPublishArtifact(readbackInfo, publishMode);
161
213
  const expectedArtifact = getPublishArtifact(expectedInfo, publishMode);
162
- const readbackFingerprint = fingerprint(readbackArtifact);
163
- const expectedFingerprint = fingerprint(expectedArtifact);
214
+ const codeBundleReadback = publishMode === 'canvas' && readbackInfo && readbackInfo.hasCodeBundle;
215
+ const readbackFingerprint = codeBundleReadback
216
+ ? readbackInfo.canvasRuntimeSha256
217
+ : fingerprint(readbackArtifact);
218
+ const expectedFingerprint = codeBundleReadback
219
+ ? rawFingerprint(expectedArtifact)
220
+ : fingerprint(expectedArtifact);
164
221
 
165
222
  return {
166
223
  readbackInfo,
@@ -180,6 +237,7 @@ function verifyPublishedContentMatch(readbackSchema, expectedSchemaContent, publ
180
237
  module.exports = {
181
238
  extractDisplayPublishInfo,
182
239
  fingerprint,
240
+ rawFingerprint,
183
241
  hasExpectedDisplayComponent,
184
242
  parseSchemaContent,
185
243
  summarizeDisplayPublishInfo,