openyida 2026.8.31 → 2026.9.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +2 -2
  2. package/lib/app/create-app.js +1 -0
  3. package/lib/app/create-form.js +334 -66
  4. package/lib/app/display-page-readback.js +63 -5
  5. package/lib/app/form-action-binding.js +185 -0
  6. package/lib/app/get-schema.js +168 -14
  7. package/lib/app/publish.js +138 -6
  8. package/lib/app/schema-semantic-analysis.js +56 -14
  9. package/lib/auth/token-store.js +13 -1
  10. package/lib/core/command-manifest.js +3 -3
  11. package/lib/core/locales/en.js +20 -0
  12. package/lib/core/locales/zh.js +20 -0
  13. package/lib/core/query-data.js +215 -23
  14. package/lib/core/utils.js +409 -0
  15. package/lib/report/append.js +3 -1
  16. package/lib/report/contract.js +7 -0
  17. package/lib/report/index.js +94 -5
  18. package/lib/report/inspect.js +32 -3
  19. package/lib/report/runtime-probe.js +248 -0
  20. package/lib/report/url.js +13 -0
  21. package/package.json +1 -1
  22. package/yida-skills/references/report-field-config-guide.md +10 -28
  23. package/yida-skills/references/yida-api.md +10 -0
  24. package/yida-skills/skills/yida-app/SKILL.md +5 -3
  25. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +1 -1
  26. package/yida-skills/skills/yida-app/workflow/step-5-seed-records.md +1 -1
  27. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +9 -7
  28. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +5 -2
  29. package/yida-skills/skills/yida-chart/references/echarts-bindding-guide.md +1 -1
  30. package/yida-skills/skills/yida-create-form-page/SKILL.md +2 -2
  31. package/yida-skills/skills/yida-create-form-page/references/advanced-form-modes.md +35 -2
  32. package/yida-skills/skills/yida-data-management/SKILL.md +17 -16
  33. package/yida-skills/skills/yida-data-management/references/data-format-guide.md +1 -1
  34. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  35. package/yida-skills/skills/yida-design/workflow/output-prd.md +6 -4
  36. package/yida-skills/skills/yida-design/workflow/step-3-information-architecture.md +5 -2
  37. package/yida-skills/skills/yida-report/SKILL.md +6 -11
  38. package/yida-skills/skills/yida-report/references/examples.md +11 -18
package/README.md CHANGED
@@ -200,7 +200,7 @@ openyida configure-process APP_XXX FORM_XXX .cache/openyida/process/process.json
200
200
  openyida process preview APP_XXX PROC_INST_XXX --output .cache/openyida/process/process.html
201
201
  openyida data query form APP_XXX FORM_XXX --page 1 --size 20
202
202
  openyida data query form APP_XXX FORM_XXX --dynamic-order '{"dateField_xxx":"-"}'
203
- openyida data create form APP_XXX FORM_XXX --data-file .cache/openyida/data-import/record.json
203
+ openyida data create form APP_XXX FORM_XXX --expect-form-name 客户 --expect-form-type receipt --data-file .cache/openyida/data-import/record.json
204
204
  openyida get-permission APP_XXX FORM_XXX
205
205
  ```
206
206
 
@@ -430,7 +430,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
430
430
 
431
431
  | Command | Description |
432
432
  |---------|-------------|
433
- | `openyida data <query\|get\|create\|update> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --confirm [--json]` | Unified data management (form/process/task/subform) |
433
+ | `openyida data <query\|get\|create\|update> <resource> ... \| delete form <appType> <formUuid> --inst-id <id> --expect-form-name <name> --expect-form-type receipt --confirm [--json]` | Unified data management (form/process/task/subform) |
434
434
  | `openyida task-center <type> [options]` | Global task center (todo/processed/cc etc.) |
435
435
  | `openyida basic-info <overview\|commodity\|grant\|capacity\|quota\|abs-path\|dataflow\|i18n\|domain>` | Query organization basic info, capacity, quotas, and domain settings |
436
436
  | `openyida read-dingtalk-doc <docUrl> [--output <file>] [--json]` | Fetch Markdown content from a DingTalk document |
@@ -269,6 +269,7 @@ function buildCreateAppPayload(params, auth, contentLocale, openExclusive, openP
269
269
  openPhysicColumn,
270
270
  openIsolationDatabase: 'n',
271
271
  openExclusiveUnit: 'n',
272
+ createWithModernTheme: 'y',
272
273
  group: 'ALL',
273
274
  fromBuilderAi: 'y',
274
275
  builderAiSource: resolveBuilderAiSource(),
@@ -84,6 +84,13 @@ const { dispatchCreateFormCommand } = require('./create-form/commands');
84
84
  const { buildApiPath } = require('./create-form/api-path');
85
85
  const { deepMerge, splitJsonPointer } = require('./create-form/schema-patch');
86
86
  const { ensureYidaGlobalThemeAction } = require('./form-theme-action');
87
+ const {
88
+ actionRefName,
89
+ buildDesignerEventBinding,
90
+ extractExportedActionFunctionNames,
91
+ inspectActionBindings,
92
+ syncDesignerActionCatalog,
93
+ } = require('./form-action-binding');
87
94
  const { FORM_RULES_BLOCK_END, FORM_RULES_BLOCK_START } = require('./create-form/rule-builder');
88
95
  const { SMART_VALIDATION_BLOCK_END, SMART_VALIDATION_BLOCK_START } = require('./create-form/validation-builder');
89
96
  const { createFieldNormalizers } = require('./create-form/field-normalizers');
@@ -2678,6 +2685,116 @@ function compileActionSource(source) {
2678
2685
  return compiledResult.compiled;
2679
2686
  }
2680
2687
 
2688
+ function bindFieldAction(schema, formContainer, operation) {
2689
+ if (!formContainer || !formContainer.children) {
2690
+ throw new Error('未找到 FormContainer');
2691
+ }
2692
+ const fieldKey = operation.fieldId || operation.field || operation.label;
2693
+ const eventName = operation.event || 'onChange';
2694
+ const actionName = operation.name || operation.actionName;
2695
+ if (!actionName) {
2696
+ throw new Error('bind-field-action 必须提供 name/actionName');
2697
+ }
2698
+ if (!isValidActionIdentifier(actionName)) {
2699
+ throw new Error('动作名称不是合法的 JavaScript 标识符: ' + actionName);
2700
+ }
2701
+ const found = findFieldByIdOrLabelDeep(formContainer.children, fieldKey);
2702
+ if (!found) {
2703
+ throw new Error('未找到字段: ' + fieldKey);
2704
+ }
2705
+ found.field.props = found.field.props || {};
2706
+ const existingActionName = actionRefName(found.field.props[eventName]);
2707
+ if (existingActionName && existingActionName !== actionName && operation.replaceExisting !== true) {
2708
+ throw createCreateFormError(
2709
+ t('create_form.action_event_conflict', fieldKey, eventName, existingActionName),
2710
+ 'FORM_ACTION_EVENT_CONFLICT',
2711
+ {
2712
+ status: 'SEMANTIC_FAILURE',
2713
+ retryable: false,
2714
+ sideEffectState: 'none',
2715
+ nextStep: 'review_existing_field_action',
2716
+ field: fieldKey,
2717
+ event: eventName,
2718
+ existingActionName,
2719
+ requestedActionName: actionName,
2720
+ }
2721
+ );
2722
+ }
2723
+
2724
+ found.field.props[eventName] = buildDesignerEventBinding(
2725
+ actionName,
2726
+ operation.params || {},
2727
+ found.field.props[eventName]
2728
+ );
2729
+
2730
+ const actions = ensureActionModule(schema);
2731
+ const fieldId = found.field.props.fieldId || found.field.id || String(fieldKey || '');
2732
+ const relatedEventId = operation.relatedEventId || ((found.field.id || fieldId) + ':' + eventName);
2733
+ syncDesignerActionCatalog(actions);
2734
+
2735
+ return {
2736
+ action: operation.action === 'field-action' ? 'field-action' : 'bind-field-action',
2737
+ field: fieldKey,
2738
+ fieldId,
2739
+ event: eventName,
2740
+ actionName,
2741
+ relatedEventId,
2742
+ replacedActionName: existingActionName && existingActionName !== actionName ? existingActionName : '',
2743
+ };
2744
+ }
2745
+
2746
+ function applyAtomicFieldAction(schema, formContainer, operation) {
2747
+ const actionName = operation.name || operation.actionName;
2748
+ if (!actionName) {
2749
+ throw new Error('field-action 必须提供 name/actionName');
2750
+ }
2751
+ if (!isValidActionIdentifier(actionName)) {
2752
+ throw new Error('动作名称不是合法的 JavaScript 标识符: ' + actionName);
2753
+ }
2754
+
2755
+ const actions = ensureActionModule(schema);
2756
+ const source = readOptionalSource(operation, 'source', 'sourceFile');
2757
+ if (source !== undefined) {
2758
+ if (!extractExportedActionFunctionNames(source).includes(actionName)) {
2759
+ throw createCreateFormError(
2760
+ t('create_form.action_source_export_missing', actionName),
2761
+ 'FORM_ACTION_FUNCTION_MISSING',
2762
+ {
2763
+ status: 'SEMANTIC_FAILURE',
2764
+ retryable: false,
2765
+ sideEffectState: 'none',
2766
+ nextStep: 'export_named_action_function',
2767
+ actionName,
2768
+ }
2769
+ );
2770
+ }
2771
+ const blockStart = '/* openyida:field-action:start:' + actionName + ' */';
2772
+ const blockEnd = '/* openyida:field-action:end:' + actionName + ' */';
2773
+ actions.module.source = upsertGeneratedSourceBlockWithBounds(
2774
+ actions.module.source || '',
2775
+ source,
2776
+ blockStart,
2777
+ blockEnd
2778
+ );
2779
+ actions.module.compiled = compileActionSource(actions.module.source);
2780
+ actions.type = actions.type || 'FUNCTION';
2781
+ } else if (!extractExportedActionFunctionNames(actions.module.source || '').includes(actionName)) {
2782
+ throw createCreateFormError(
2783
+ t('create_form.action_source_missing', actionName),
2784
+ 'FORM_ACTION_FUNCTION_MISSING',
2785
+ {
2786
+ status: 'SEMANTIC_FAILURE',
2787
+ retryable: false,
2788
+ sideEffectState: 'none',
2789
+ nextStep: 'provide_action_source',
2790
+ actionName,
2791
+ }
2792
+ );
2793
+ }
2794
+
2795
+ return bindFieldAction(schema, formContainer, Object.assign({}, operation, { action: 'field-action' }));
2796
+ }
2797
+
2681
2798
  function readOptionalSource(operation, key, fileKey) {
2682
2799
  if (operation[fileKey]) {
2683
2800
  const sourcePath = path.resolve(operation[fileKey]);
@@ -2773,6 +2890,11 @@ function applySchemaPatchOperations(schema, operations) {
2773
2890
  return;
2774
2891
  }
2775
2892
 
2893
+ if (action === 'field-action') {
2894
+ applied.push(applyAtomicFieldAction(schema, formContainer, operation));
2895
+ return;
2896
+ }
2897
+
2776
2898
  if (['bind-datasource', 'data-source', 'datasource', 'select-datasource'].includes(String(action).toLowerCase())) {
2777
2899
  if (!formContainer || !formContainer.children) {
2778
2900
  throw new Error('未找到 FormContainer');
@@ -2802,42 +2924,7 @@ function applySchemaPatchOperations(schema, operations) {
2802
2924
  }
2803
2925
 
2804
2926
  if (action === 'bind-field-action') {
2805
- if (!formContainer || !formContainer.children) {
2806
- throw new Error('未找到 FormContainer');
2807
- }
2808
- const fieldKey = operation.fieldId || operation.field || operation.label;
2809
- const eventName = operation.event || 'onChange';
2810
- const actionName = operation.name || operation.actionName;
2811
- if (!actionName) {
2812
- throw new Error('bind-field-action 必须提供 name/actionName');
2813
- }
2814
- const found = findFieldByIdOrLabelDeep(formContainer.children, fieldKey);
2815
- if (!found) {
2816
- throw new Error('未找到字段: ' + fieldKey);
2817
- }
2818
- found.field.props = found.field.props || {};
2819
- found.field.props[eventName] = {
2820
- name: actionName,
2821
- id: actionName,
2822
- params: operation.params || {},
2823
- type: 'actionRef',
2824
- };
2825
-
2826
- const actions = ensureActionModule(schema);
2827
- const relatedEventId = operation.relatedEventId || (found.field.id + ':' + eventName);
2828
- const existing = actions.list.find(function (item) {
2829
- return item.relatedEventId === relatedEventId && item.id === actionName;
2830
- });
2831
- if (!existing) {
2832
- actions.list.push({
2833
- relatedEventId,
2834
- name: actionName,
2835
- id: actionName,
2836
- type: 'componentEvent',
2837
- params: operation.params || {},
2838
- });
2839
- }
2840
- applied.push({ action: 'bind-field-action', field: fieldKey, event: eventName, actionName });
2927
+ applied.push(bindFieldAction(schema, formContainer, operation));
2841
2928
  return;
2842
2929
  }
2843
2930
 
@@ -3246,18 +3333,71 @@ function upsertActionListEntry(actions, entry) {
3246
3333
  actions.list.push(entry);
3247
3334
  }
3248
3335
 
3336
+ function removeObsoleteGeneratedRuleCatalogEntries(actions, activeActionNames) {
3337
+ // Only prune OpenYida-owned rule wrappers; unknown user/platform entries must survive catalog sync.
3338
+ const activeNames = new Set(activeActionNames || []);
3339
+ actions.list = (actions.list || []).filter(function (item) {
3340
+ const names = [item && item.id, item && item.name].filter(Boolean).map(String);
3341
+ const generatedNames = names.filter(function (name) {
3342
+ return name.startsWith('openyidaRuleChange_');
3343
+ });
3344
+ return generatedNames.length === 0 || generatedNames.some(function (name) {
3345
+ return activeNames.has(name);
3346
+ });
3347
+ });
3348
+ }
3349
+
3350
+ function previousRuleActionName(source, wrapperName) {
3351
+ const start = String(source || '').indexOf('export function ' + wrapperName + '(');
3352
+ if (start === -1) {return '';}
3353
+ const end = String(source || '').indexOf('return this.openyidaApplyRules', start);
3354
+ const wrapperSource = String(source || '').slice(start, end === -1 ? undefined : end);
3355
+ const match = wrapperSource.match(/typeof this\.([A-Za-z_$][A-Za-z0-9_$]*) === ["']function["']/);
3356
+ return match ? match[1] : '';
3357
+ }
3358
+
3359
+ function reconcileGeneratedRuleBindings(value, nextSourceFieldIds, actionsSource) {
3360
+ if (!value) {return;}
3361
+ if (Array.isArray(value)) {
3362
+ value.forEach(function (item) {
3363
+ reconcileGeneratedRuleBindings(item, nextSourceFieldIds, actionsSource);
3364
+ });
3365
+ return;
3366
+ }
3367
+ if (typeof value !== 'object') {return;}
3368
+
3369
+ const props = value.props || {};
3370
+ const fieldId = String(props.fieldId || '');
3371
+ const existingActionName = actionRefName(props.onChange);
3372
+ if (fieldId && existingActionName.startsWith('openyidaRuleChange_') && !nextSourceFieldIds.has(fieldId)) {
3373
+ const previousActionName = previousRuleActionName(actionsSource, existingActionName);
3374
+ if (previousActionName) {
3375
+ props.onChange = buildDesignerEventBinding(previousActionName, {}, props.onChange);
3376
+ } else {
3377
+ delete props.onChange;
3378
+ }
3379
+ }
3380
+
3381
+ Object.values(value).forEach(function (child) {
3382
+ if (child !== props) {
3383
+ reconcileGeneratedRuleBindings(child, nextSourceFieldIds, actionsSource);
3384
+ }
3385
+ });
3386
+ }
3387
+
3249
3388
  function buildFormRulesActionSource(rules, fieldMap, bindings) {
3250
3389
  const rulesJson = JSON.stringify(rules, null, 2);
3251
3390
  const fieldMapJson = JSON.stringify(fieldMap, null, 2);
3252
3391
  const wrapperSource = bindings.map(function (binding) {
3253
3392
  const previousCall = binding.previousActionName && isValidActionIdentifier(binding.previousActionName)
3254
- ? ' if (typeof ' + binding.previousActionName + ' === "function") {\n ' + binding.previousActionName + '.call(this, event);\n }\n'
3393
+ ? ' if (typeof this.' + binding.previousActionName + ' === "function") {\n this.' + binding.previousActionName + '(event);\n }\n'
3255
3394
  : '';
3256
- return 'export function ' + binding.wrapperName + '(event) {\n' +
3395
+ const title = binding.label ? '/** @title ' + binding.label + '值发生变化 */\n' : '';
3396
+ return title + 'export function ' + binding.wrapperName + '(event) {\n' +
3257
3397
  ' event = event || {};\n' +
3258
3398
  ' event.__openyidaSourceFieldId = "' + binding.fieldId + '";\n' +
3259
3399
  previousCall +
3260
- ' return openyidaApplyRules.call(this, event);\n' +
3400
+ ' return this.openyidaApplyRules(event);\n' +
3261
3401
  '}';
3262
3402
  }).join('\n\n');
3263
3403
 
@@ -3405,10 +3545,10 @@ export function openyidaApplyRules(event) {
3405
3545
  }
3406
3546
 
3407
3547
  export function openyidaRulesDidMount(event) {
3408
- if (typeof didMount === 'function') {
3409
- didMount.call(this, event);
3548
+ if (typeof this.didMount === 'function') {
3549
+ this.didMount(event);
3410
3550
  }
3411
- return openyidaApplyRules.call(this, event || {});
3551
+ return this.openyidaApplyRules(event || {});
3412
3552
  }
3413
3553
 
3414
3554
  ${wrapperSource}
@@ -3434,6 +3574,8 @@ function applyFormRules(schema, rawRules) {
3434
3574
  }
3435
3575
  });
3436
3576
 
3577
+ reconcileGeneratedRuleBindings(formContainer.children, sourceFieldIds, actions.module.source || '');
3578
+
3437
3579
  const bindings = [];
3438
3580
  sourceFieldIds.forEach(function (fieldId) {
3439
3581
  const found = findFieldByIdOrLabelDeep(formContainer.children, fieldId);
@@ -3443,32 +3585,18 @@ function applyFormRules(schema, rawRules) {
3443
3585
  found.field.props = found.field.props || {};
3444
3586
  const eventName = 'onChange';
3445
3587
  const existingAction = found.field.props[eventName];
3446
- const previousActionName = existingAction &&
3447
- existingAction.type === 'actionRef' &&
3448
- existingAction.name &&
3449
- !String(existingAction.name).startsWith('openyidaRuleChange_')
3450
- ? String(existingAction.name)
3451
- : '';
3588
+ const existingActionName = actionRefName(existingAction);
3452
3589
  const wrapperName = 'openyidaRuleChange_' + sanitizeActionName(fieldId);
3590
+ const previousActionName = existingActionName.startsWith('openyidaRuleChange_')
3591
+ ? previousRuleActionName(actions.module.source || '', existingActionName)
3592
+ : existingActionName;
3453
3593
  const relatedEventId = (found.field.id || fieldId) + ':' + eventName;
3454
3594
 
3455
- found.field.props[eventName] = {
3456
- name: wrapperName,
3457
- id: wrapperName,
3458
- params: {},
3459
- type: 'actionRef',
3460
- };
3461
-
3462
- actions.list = actions.list.filter(function (item) {
3463
- return !(item.relatedEventId === relatedEventId && String(item.id || '').startsWith('openyidaRuleChange_'));
3464
- });
3465
- upsertActionListEntry(actions, {
3466
- relatedEventId,
3467
- name: wrapperName,
3468
- id: wrapperName,
3469
- type: 'componentEvent',
3470
- params: {},
3471
- });
3595
+ found.field.props[eventName] = buildDesignerEventBinding(
3596
+ wrapperName,
3597
+ {},
3598
+ found.field.props[eventName]
3599
+ );
3472
3600
 
3473
3601
  bindings.push({
3474
3602
  fieldId,
@@ -3516,6 +3644,11 @@ function applyFormRules(schema, rawRules) {
3516
3644
  actions.module.source = upsertGeneratedSourceBlock(actions.module.source || '', generatedSource);
3517
3645
  actions.module.compiled = compileActionSource(actions.module.source);
3518
3646
  actions.type = actions.type || 'FUNCTION';
3647
+ removeObsoleteGeneratedRuleCatalogEntries(actions, bindings.map(function (binding) {
3648
+ return binding.wrapperName;
3649
+ }));
3650
+ // Merge source exports after the rich lifecycle/component entries above so their metadata is preserved.
3651
+ syncDesignerActionCatalog(actions);
3519
3652
 
3520
3653
  return {
3521
3654
  rules: normalized.rules,
@@ -5332,7 +5465,22 @@ async function mainRule(parsedArgs, authRef) {
5332
5465
  fillSerialNumberFormulas(formContainer.children, corpId, appType, formUuid);
5333
5466
  }
5334
5467
 
5468
+ const actionBindingExpectations = applied.bindings.map(function (binding) {
5469
+ return {
5470
+ fieldId: binding.fieldId,
5471
+ event: binding.eventName,
5472
+ actionName: binding.wrapperName,
5473
+ relatedEventId: binding.relatedEventId,
5474
+ };
5475
+ });
5476
+ assertFormActionBindings(schema, actionBindingExpectations);
5335
5477
  await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
5478
+ const actionReadback = await readBackFormActionBindings(
5479
+ authRef,
5480
+ appType,
5481
+ formUuid,
5482
+ actionBindingExpectations
5483
+ );
5336
5484
 
5337
5485
  const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5338
5486
  result(true, '表单联动规则保存成功', [
@@ -5347,6 +5495,7 @@ async function mainRule(parsedArgs, authRef) {
5347
5495
  formUuid,
5348
5496
  appType,
5349
5497
  rulesApplied: applied.rules.length,
5498
+ readbackVerified: actionReadback.verified,
5350
5499
  rules: applied.rules.map(function (rule) {
5351
5500
  return {
5352
5501
  type: rule.type,
@@ -5358,11 +5507,16 @@ async function mainRule(parsedArgs, authRef) {
5358
5507
  };
5359
5508
  }),
5360
5509
  eventBindings: applied.bindings.map(function (binding) {
5510
+ const readback = actionReadback.bindings.find(function (item) {
5511
+ return item.fieldId === binding.fieldId && item.event === binding.eventName;
5512
+ });
5361
5513
  return {
5362
5514
  fieldId: binding.fieldId,
5363
5515
  label: binding.label,
5364
5516
  event: binding.eventName,
5365
5517
  actionName: binding.wrapperName,
5518
+ designerBindingFound: readback && readback.designerBindingFound,
5519
+ verified: readback && readback.verified,
5366
5520
  resolved: getFieldEvidenceFromLookup(applied.fieldLookup, binding.fieldId),
5367
5521
  };
5368
5522
  }),
@@ -5376,6 +5530,97 @@ async function mainRule(parsedArgs, authRef) {
5376
5530
 
5377
5531
  // ── patch 模式主流程 ──────────────────────────────────
5378
5532
 
5533
+ function actionBindingExpectationsFromOperations(appliedOperations) {
5534
+ return (appliedOperations || []).filter(function (operation) {
5535
+ return operation.action === 'field-action' || operation.action === 'bind-field-action';
5536
+ }).map(function (operation) {
5537
+ return {
5538
+ fieldId: operation.fieldId,
5539
+ event: operation.event,
5540
+ actionName: operation.actionName,
5541
+ relatedEventId: operation.relatedEventId,
5542
+ };
5543
+ });
5544
+ }
5545
+
5546
+ function assertFormActionBindings(schema, expectations, options) {
5547
+ const inspection = inspectActionBindings(schema, expectations);
5548
+ if (inspection.verified) {
5549
+ return inspection;
5550
+ }
5551
+ const readback = options && options.readback === true;
5552
+ throw createCreateFormError(
5553
+ t(readback ? 'create_form.action_readback_mismatch' : 'create_form.action_binding_incomplete'),
5554
+ readback ? 'FORM_ACTION_BINDING_READBACK_MISMATCH' : 'FORM_ACTION_BINDING_INCOMPLETE',
5555
+ {
5556
+ status: 'SEMANTIC_FAILURE',
5557
+ retryable: false,
5558
+ retrySafe: false,
5559
+ sideEffectState: readback ? 'committed' : 'none',
5560
+ mutationAccepted: readback,
5561
+ readbackVerified: false,
5562
+ readbackAllowed: true,
5563
+ nextStep: readback ? 'inspect_existing_form_action_binding' : 'fix_action_binding_patch',
5564
+ bindings: inspection.bindings,
5565
+ }
5566
+ );
5567
+ }
5568
+
5569
+ async function readBackFormActionBindings(authRef, appType, formUuid, expectations) {
5570
+ const schemaResult = await requestWithAutoLogin(function (auth) {
5571
+ return sendGetRequest(
5572
+ auth.baseUrl,
5573
+ buildApiPath(appType, 'getFormSchema', { prefix: '_view', namespace: 'alibaba' }),
5574
+ { formUuid: formUuid, schemaVersion: 'V5' }
5575
+ );
5576
+ }, authRef);
5577
+
5578
+ if (!schemaResult || schemaResult.success === false || schemaResult.__needLogin) {
5579
+ const errorMsg = schemaResult ? schemaResult.errorMsg || t('common.unknown_error') : t('common.request_failed');
5580
+ throw createCreateFormError(
5581
+ t('create_form.action_readback_failed', errorMsg),
5582
+ 'FORM_ACTION_BINDING_READBACK_FAILED',
5583
+ {
5584
+ status: 'READBACK_UNVERIFIED',
5585
+ retryable: true,
5586
+ retrySafe: false,
5587
+ sideEffectState: 'committed',
5588
+ mutationAccepted: true,
5589
+ readbackVerified: false,
5590
+ readbackAllowed: true,
5591
+ nextStep: 'retry_readback_only',
5592
+ target: { appType, formUuid },
5593
+ result: sanitizeFailureResult(schemaResult),
5594
+ }
5595
+ );
5596
+ }
5597
+
5598
+ let schema;
5599
+ try {
5600
+ schema = schemaResult.content
5601
+ ? (typeof schemaResult.content === 'string' ? JSON.parse(schemaResult.content) : schemaResult.content)
5602
+ : schemaResult;
5603
+ } catch (parseError) {
5604
+ throw createCreateFormError(
5605
+ t('create_form.action_readback_failed', parseError.message),
5606
+ 'FORM_ACTION_BINDING_READBACK_FAILED',
5607
+ {
5608
+ status: 'READBACK_UNVERIFIED',
5609
+ retryable: true,
5610
+ retrySafe: false,
5611
+ sideEffectState: 'committed',
5612
+ mutationAccepted: true,
5613
+ readbackVerified: false,
5614
+ readbackAllowed: true,
5615
+ nextStep: 'retry_readback_only',
5616
+ target: { appType, formUuid },
5617
+ }
5618
+ );
5619
+ }
5620
+
5621
+ return assertFormActionBindings(schema, expectations, { readback: true });
5622
+ }
5623
+
5379
5624
  async function mainPatch(parsedArgs, authRef) {
5380
5625
  const { appType, formUuid, patchJsonOrFile } = parsedArgs;
5381
5626
 
@@ -5429,6 +5674,9 @@ async function mainPatch(parsedArgs, authRef) {
5429
5674
  try {
5430
5675
  appliedOperations = applySchemaPatchOperations(schema, operations);
5431
5676
  } catch (patchError) {
5677
+ if (patchError && patchError.isCliError) {
5678
+ throw patchError;
5679
+ }
5432
5680
  fail('Schema 补丁应用失败: ' + patchError.message);
5433
5681
  console.log(JSON.stringify({
5434
5682
  success: false,
@@ -5441,6 +5689,10 @@ async function mainPatch(parsedArgs, authRef) {
5441
5689
  formUuid,
5442
5690
  });
5443
5691
  }
5692
+ const actionBindingExpectations = actionBindingExpectationsFromOperations(appliedOperations);
5693
+ const localActionInspection = actionBindingExpectations.length > 0
5694
+ ? assertFormActionBindings(schema, actionBindingExpectations)
5695
+ : null;
5444
5696
  success('已应用 ' + appliedOperations.length + ' 个补丁操作');
5445
5697
  appliedOperations.forEach(function (operation, index) {
5446
5698
  listItem((index + 1) + '. ' + operation.action + (operation.field ? ' ' + operation.field : '') + (operation.path ? ' ' + operation.path : ''));
@@ -5455,6 +5707,9 @@ async function mainPatch(parsedArgs, authRef) {
5455
5707
  }
5456
5708
 
5457
5709
  await saveFormSchema(authRef, appType, formUuid, schema, version, 4);
5710
+ const actionReadback = actionBindingExpectations.length > 0
5711
+ ? await readBackFormActionBindings(authRef, appType, formUuid, actionBindingExpectations)
5712
+ : null;
5458
5713
 
5459
5714
  const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
5460
5715
  result(true, 'Schema 补丁保存成功', [
@@ -5464,7 +5719,16 @@ async function mainPatch(parsedArgs, authRef) {
5464
5719
  ]);
5465
5720
 
5466
5721
  console.log(JSON.stringify(withBrowserHandoff(
5467
- { success: true, formUuid, appType, operationsApplied: appliedOperations.length, operations: appliedOperations, url: formUrl },
5722
+ {
5723
+ success: true,
5724
+ formUuid,
5725
+ appType,
5726
+ operationsApplied: appliedOperations.length,
5727
+ operations: appliedOperations,
5728
+ readbackVerified: actionReadback ? actionReadback.verified : undefined,
5729
+ eventBindings: actionReadback ? actionReadback.bindings : (localActionInspection ? localActionInspection.bindings : undefined),
5730
+ url: formUrl,
5731
+ },
5468
5732
  formUrl,
5469
5733
  { stage: 'patch_form_success', title: formUuid },
5470
5734
  parsedArgs.browserOpenMode
@@ -5811,5 +6075,9 @@ module.exports = {
5811
6075
  ensureYidaGlobalThemeAction,
5812
6076
  applyDefaultFormDetailStyle,
5813
6077
  applyChangesToSchema,
6078
+ applyFormRules,
6079
+ applySchemaPatchOperations,
6080
+ actionBindingExpectationsFromOperations,
6081
+ assertFormActionBindings,
5814
6082
  },
5815
6083
  };
@@ -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,