openyida 2026.9.8 → 2026.9.9

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 (58) hide show
  1. package/README.md +4 -1
  2. package/lib/app/create-form/args.js +36 -2
  3. package/lib/app/create-form/batch.js +24 -7
  4. package/lib/app/create-form/commands.js +2 -0
  5. package/lib/app/create-form.js +292 -0
  6. package/lib/app/services/canvas-page-schema-builder.js +3 -3
  7. package/lib/core/command-manifest.js +4 -1
  8. package/lib/core/locales/en.js +9 -1
  9. package/lib/core/locales/zh.js +9 -1
  10. package/lib/core/sample.js +13 -1
  11. package/lib/design-plan/materialize.js +6 -10
  12. package/lib/process/configure-process.js +18 -1
  13. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +107 -23
  14. package/lib/samples/openyida-scaffold/canvas-nav/content.jsx +42 -0
  15. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  16. package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +84 -17
  17. package/lib/samples/yida-rechart/trend-combo.canvas.jsx +97 -18
  18. package/package.json +1 -1
  19. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +3 -3
  20. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +2 -2
  21. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +15 -3
  22. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +4 -1
  23. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +2 -0
  24. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +27 -7
  25. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +2 -41
  26. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +93 -65
  27. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +89 -0
  28. package/yida-skills/skills/yida-canvas-custom-page/references/component-library-guide.md +2 -2
  29. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +11 -3
  30. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +6 -2
  31. package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +122 -0
  32. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +102 -0
  33. package/yida-skills/skills/yida-canvas-table-form/SKILL.md +2 -2
  34. package/yida-skills/skills/yida-create-form-page/SKILL.md +17 -3
  35. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +18 -4
  36. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +2 -4
  37. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +2 -2
  38. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +3 -3
  39. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +3 -3
  40. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +4 -4
  41. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +3 -3
  42. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +7 -7
  43. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +2 -2
  44. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +4 -4
  45. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +3 -3
  46. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +5 -5
  47. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +2 -2
  48. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +4 -4
  49. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +2 -2
  50. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +2 -2
  51. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +4 -4
  52. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +4 -4
  53. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +5 -5
  54. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +2 -2
  55. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +3 -3
  56. package/yida-skills/skills/yida-design/workflow/output-design.md +1 -1
  57. package/yida-skills/skills/yida-nav-shell/SKILL.md +7 -2
  58. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +84 -3
package/README.md CHANGED
@@ -174,6 +174,8 @@ openyida update-app APP_XXX --theme-file .cache/openyida/crm/app-theme.css --nav
174
174
  openyida corp-efficiency
175
175
  openyida create-form create APP_XXX "Customer" .cache/openyida/forms/customer-fields.json
176
176
  openyida create-form update APP_XXX FORM_XXX .cache/openyida/forms/customer-changes.json
177
+ openyida create-form update APP_XXX FORM_XXX --data-file .cache/openyida/forms/customer-changes.json
178
+ openyida create-form resume APP_XXX FORM_XXX .cache/openyida/forms/customer-fields.json --json
177
179
  openyida sample openyida-page-template form-fields --output .cache/openyida/forms/customer-fields.json
178
180
  openyida sample openyida-page-template canvas-form-drawer --output project/pages/src/customer-entry.canvas.jsx --var APP_TYPE=APP_XXX --var FORM_UUID=FORM_XXX
179
181
  openyida get-schema APP_XXX FORM_XXX
@@ -413,7 +415,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
413
415
  | `openyida create-form create <appType> "<formTitle>" <fieldsJsonFile> [--icon auto\|<iconName>] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a form page |
414
416
  | `openyida create-form icons [--json]` | List available form navigation icons |
415
417
  | `openyida create-form validate-fields <fieldsJsonOrFile> [--json]` | Validate form field JSON locally |
416
- | `openyida create-form update <appType> ... [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
418
+ | `openyida create-form update <appType> <formUuid> (<changesJsonOrFile> \| --data-file <changesJsonOrFile>) [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
419
+ | `openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> [--json]` | Update a form page |
417
420
  | `openyida create-form patch <appType> <formUuid> <patchJsonOrFile> [--open\|--no-open]` | Update a form page |
418
421
  | `openyida create-form rule <appType> <formUuid> <rulesJsonOrFile> [--open\|--no-open]` | Update a form page |
419
422
  | `openyida create-form validation <appType> <formUuid> <validationsJsonOrFile> [--open\|--no-open]` | Update a form page |
@@ -89,6 +89,7 @@ function createParseArgs(dependencies) {
89
89
  contentLocale: null,
90
90
  browserOpenMode: openOption.mode,
91
91
  };
92
+ let dataFileInput;
92
93
 
93
94
  const args = [...rawArgs];
94
95
 
@@ -111,6 +112,14 @@ function createParseArgs(dependencies) {
111
112
  i--;
112
113
  } else if (args[i] === '--icon') {
113
114
  throwCreateFormError('--icon requires a value', 'CREATE_FORM_INVALID_ARGUMENTS');
115
+ } else if (args[i] === '--data-file') {
116
+ const next = args[i + 1];
117
+ if (dataFileInput !== undefined || !next || next.startsWith('--')) {
118
+ throwCreateFormError('--data-file requires exactly one value', 'CREATE_FORM_INVALID_ARGUMENTS');
119
+ }
120
+ dataFileInput = next;
121
+ args.splice(i, 2);
122
+ i--;
114
123
  } else if ((args[i] === '--locale' || args[i] === '--content-locale' || args[i] === '--lang') && i + 1 < args.length) {
115
124
  options.contentLocale = args[i + 1];
116
125
  if (!normalizeYidaLocale(options.contentLocale)) {
@@ -145,6 +154,10 @@ function createParseArgs(dependencies) {
145
154
 
146
155
  const mode = args[0];
147
156
 
157
+ if (dataFileInput !== undefined && mode !== 'update') {
158
+ throwCreateFormError('--data-file is only supported by create-form update', 'CREATE_FORM_INVALID_ARGUMENTS');
159
+ }
160
+
148
161
  if (mode === 'icons' || mode === 'list-icons') {
149
162
  return {
150
163
  mode: 'icons',
@@ -182,7 +195,12 @@ function createParseArgs(dependencies) {
182
195
  }
183
196
 
184
197
  if (mode === 'update') {
185
- if (args.length < 4) {
198
+ const positionalInput = args[3];
199
+ if (args.length > 4 || (positionalInput && dataFileInput !== undefined)) {
200
+ throwCreateFormError('Use either <changesJsonOrFile> or --data-file, not both', 'CREATE_FORM_INVALID_ARGUMENTS');
201
+ }
202
+ const changesJsonOrFile = dataFileInput !== undefined ? dataFileInput : positionalInput;
203
+ if (args.length < 3 || !changesJsonOrFile) {
186
204
  usage(t('create_form.usage_update'), t('create_form.example_update'));
187
205
  throwCreateFormError(t('create_form.usage_update'), 'CREATE_FORM_INVALID_ARGUMENTS');
188
206
  }
@@ -190,7 +208,23 @@ function createParseArgs(dependencies) {
190
208
  mode: 'update',
191
209
  appType: args[1],
192
210
  formUuid: args[2],
193
- changesJsonOrFile: args[3],
211
+ changesJsonOrFile,
212
+ ...options
213
+ };
214
+ }
215
+
216
+ if (mode === 'resume') {
217
+ if (args.length !== 4) {
218
+ throwCreateFormError(
219
+ 'openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> --json',
220
+ 'CREATE_FORM_INVALID_ARGUMENTS'
221
+ );
222
+ }
223
+ return {
224
+ mode: 'resume',
225
+ appType: args[1],
226
+ formUuid: args[2],
227
+ fieldsJsonOrFile: args[3],
194
228
  ...options
195
229
  };
196
230
  }
@@ -101,7 +101,10 @@ function execute(args) {
101
101
  }
102
102
 
103
103
  async function schedule(forms, concurrency, results, worker, save) {
104
- const pending = new Set(forms.filter(form => !results[form.key] || results[form.key].status === 'blocked').map(form => form.key));
104
+ const pending = new Set(forms.filter(form => {
105
+ const result = results[form.key];
106
+ return !result || result.status === 'blocked' || (result.status === 'failed' && result.formUuid);
107
+ }).map(form => form.key));
105
108
  const active = new Map();
106
109
  try {
107
110
  while (pending.size || active.size) {
@@ -114,7 +117,7 @@ async function schedule(forms, concurrency, results, worker, save) {
114
117
  }
115
118
  if (active.size >= concurrency || deps.some(status => status !== 'success')) { continue; }
116
119
  pending.delete(form.key);
117
- results[form.key] = { status: 'running' };
120
+ results[form.key] = { ...results[form.key], status: 'running' };
118
121
  save(); // Record intent before the request; an interrupted create is never retried automatically.
119
122
  const task = Promise.resolve().then(() => worker(form)).then(output => {
120
123
  results[form.key] = { ...output, status: 'success' };
@@ -178,16 +181,30 @@ async function run(args, dependencies = {}) {
178
181
  return matches[0].fieldId;
179
182
  };
180
183
  await schedule(forms, options.concurrency, state.results, async form => {
181
- let formUuid = form.formUuid;
184
+ let formUuid = form.formUuid || state.results[form.key]?.formUuid;
185
+ let shouldResume = Boolean(!form.formUuid && formUuid);
186
+ const resolvedFields = mapReferences(form.fields, resolve);
182
187
  if (!formUuid) {
183
- const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(mapReferences(form.fields, resolve)), '--no-open'];
188
+ const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(resolvedFields), '--no-open'];
184
189
  for (const key of ['icon', 'locale']) { if (form[key]) { argv.push(`--${key}`, form[key]); } }
185
- const created = await call(argv);
186
- if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
187
- formUuid = created.formUuid;
190
+ try {
191
+ const created = await call(argv);
192
+ if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
193
+ formUuid = created.formUuid;
194
+ } catch (error) {
195
+ const createdFormUuid = error.output?.formUuid || error.output?.details?.formUuid;
196
+ if (typeof createdFormUuid !== 'string' || !createdFormUuid.startsWith('FORM')) {
197
+ throw error;
198
+ }
199
+ formUuid = createdFormUuid;
200
+ shouldResume = true;
201
+ }
188
202
  }
189
203
  state.results[form.key].formUuid = formUuid;
190
204
  save(state);
205
+ if (shouldResume) {
206
+ await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
207
+ }
191
208
  const schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
192
209
  if (schema.formUuid !== formUuid || !Array.isArray(schema.fields)) { invalid(`schema: ${form.key}`); }
193
210
  return { formUuid, fields: schema.fields };
@@ -6,6 +6,8 @@ function dispatchCreateFormCommand(parsedArgs, authContext, handlers) {
6
6
  switch (parsedArgs.mode) {
7
7
  case 'update':
8
8
  return handlers.update(...args);
9
+ case 'resume':
10
+ return handlers.resume(...args);
9
11
  case 'patch':
10
12
  return handlers.patch(...args);
11
13
  case 'rule':
@@ -5911,6 +5911,297 @@ async function mainValidation(parsedArgs, authRef) {
5911
5911
 
5912
5912
  // ── update 模式主流程 ─────────────────────────────────
5913
5913
 
5914
+ function collectResumeFieldEvidence(fields, output) {
5915
+ const evidence = output || [];
5916
+ (fields || []).forEach(function (field) {
5917
+ if (!field || typeof field !== 'object') {
5918
+ return;
5919
+ }
5920
+ const labelText = extractLabelText(field);
5921
+ if (labelText && !isFormPresentationComponent(field.componentName)) {
5922
+ evidence.push({
5923
+ label: labelText,
5924
+ componentName: field.componentName,
5925
+ fieldId: field.props && field.props.fieldId ? field.props.fieldId : null,
5926
+ });
5927
+ }
5928
+ if (Array.isArray(field.children)) {
5929
+ collectResumeFieldEvidence(field.children.flat(), evidence);
5930
+ }
5931
+ });
5932
+ return evidence;
5933
+ }
5934
+
5935
+ function desiredResumeFieldIdentity(field) {
5936
+ const labelValue = field && field.label;
5937
+ const label = typeof labelValue === 'string'
5938
+ ? labelValue
5939
+ : labelValue && (labelValue.zh_CN || labelValue.ja_JP || labelValue.en_US || labelValue.pureEn_US);
5940
+ return {
5941
+ label: String(label || '').trim(),
5942
+ componentName: normalizeFormDefinitionType(field),
5943
+ };
5944
+ }
5945
+
5946
+ function collectResumeDesiredFields(fields, options) {
5947
+ const output = [];
5948
+ const verifyNestedFields = Boolean(options && options.verifyNestedFields);
5949
+
5950
+ function visit(value) {
5951
+ if (Array.isArray(value)) {
5952
+ value.forEach(visit);
5953
+ return;
5954
+ }
5955
+ if (!value || typeof value !== 'object') {
5956
+ return;
5957
+ }
5958
+ const componentName = normalizeFormDefinitionType(value);
5959
+ if (isSupportedBusinessFieldType(componentName)) {
5960
+ output.push(value);
5961
+ if (!verifyNestedFields) {
5962
+ return;
5963
+ }
5964
+ }
5965
+ if (Array.isArray(value.children)) {
5966
+ value.children.forEach(visit);
5967
+ }
5968
+ }
5969
+
5970
+ visit(fields);
5971
+ return output;
5972
+ }
5973
+
5974
+ function isRecoverableBlankResumeSchema(schema) {
5975
+ const allowedShellComponents = new Set(['Page', 'RootHeader', 'RootContent', 'RootFooter', 'FooterYida']);
5976
+ const componentsTree = schema && schema.pages && schema.pages[0] && schema.pages[0].componentsTree;
5977
+ if (!Array.isArray(componentsTree) || componentsTree.length === 0) {
5978
+ return true;
5979
+ }
5980
+ let safe = true;
5981
+ function visit(node) {
5982
+ if (!node || typeof node !== 'object' || !safe) {
5983
+ return;
5984
+ }
5985
+ if (node.componentName && !allowedShellComponents.has(node.componentName)) {
5986
+ safe = false;
5987
+ return;
5988
+ }
5989
+ if (Array.isArray(node.children)) {
5990
+ node.children.forEach(visit);
5991
+ }
5992
+ }
5993
+ componentsTree.forEach(visit);
5994
+ return safe;
5995
+ }
5996
+
5997
+ function buildResumeChanges(existingFields, desiredFields) {
5998
+ const existingByLabel = new Map();
5999
+ existingFields.forEach(function (field) {
6000
+ const bucket = existingByLabel.get(field.label) || [];
6001
+ bucket.push(field);
6002
+ existingByLabel.set(field.label, bucket);
6003
+ });
6004
+
6005
+ const desiredLabels = new Set();
6006
+ const missing = [];
6007
+ const existing = [];
6008
+ const conflicts = [];
6009
+ desiredFields.forEach(function (field) {
6010
+ const identity = desiredResumeFieldIdentity(field);
6011
+ if (!identity.label || desiredLabels.has(identity.label)) {
6012
+ conflicts.push({
6013
+ code: identity.label ? 'CREATE_FORM_RESUME_DUPLICATE_DESIRED_FIELD' : 'CREATE_FORM_RESUME_FIELD_LABEL_REQUIRED',
6014
+ label: identity.label || null,
6015
+ });
6016
+ return;
6017
+ }
6018
+ desiredLabels.add(identity.label);
6019
+ const matches = existingByLabel.get(identity.label) || [];
6020
+ if (matches.length === 0) {
6021
+ missing.push(field);
6022
+ return;
6023
+ }
6024
+ if (matches.length !== 1 || matches[0].componentName !== identity.componentName) {
6025
+ conflicts.push({
6026
+ code: 'CREATE_FORM_RESUME_FIELD_CONFLICT',
6027
+ label: identity.label,
6028
+ requestedType: identity.componentName,
6029
+ observedTypes: matches.map(function (item) { return item.componentName; }),
6030
+ });
6031
+ return;
6032
+ }
6033
+ existing.push({
6034
+ label: identity.label,
6035
+ componentName: identity.componentName,
6036
+ fieldId: matches[0].fieldId,
6037
+ });
6038
+ });
6039
+ return { missing, existing, conflicts };
6040
+ }
6041
+
6042
+ async function readResumeSchema(authRef, appType, formUuid) {
6043
+ const schemaResult = await requestWithAutoLogin(function (auth) {
6044
+ return sendGetRequest(
6045
+ auth.baseUrl,
6046
+ buildApiPath(appType, 'getFormSchema', { prefix: '_view', namespace: 'alibaba' }),
6047
+ { formUuid: formUuid, schemaVersion: 'V5' }
6048
+ );
6049
+ }, authRef);
6050
+ if (!schemaResult || schemaResult.success === false || schemaResult.__needLogin || schemaResult.__csrfExpired) {
6051
+ throwCreateFormError(t('create_form.resume_readback_failed'), 'CREATE_FORM_RESUME_READBACK_FAILED', {
6052
+ appType,
6053
+ formUuid,
6054
+ result: sanitizeFailureResult(schemaResult),
6055
+ });
6056
+ }
6057
+ if (schemaResult.appType && schemaResult.appType !== appType) {
6058
+ throwCreateFormError(t('create_form.resume_ownership_unverified'), 'CREATE_FORM_RESUME_OWNERSHIP_UNVERIFIED', {
6059
+ appType,
6060
+ formUuid,
6061
+ });
6062
+ }
6063
+ if (schemaResult.formUuid && schemaResult.formUuid !== formUuid) {
6064
+ throwCreateFormError(t('create_form.resume_identity_mismatch'), 'CREATE_FORM_RESUME_OWNERSHIP_UNVERIFIED', {
6065
+ appType,
6066
+ formUuid,
6067
+ });
6068
+ }
6069
+ const schema = schemaResult.content
6070
+ ? (typeof schemaResult.content === 'string' ? JSON.parse(schemaResult.content) : schemaResult.content)
6071
+ : schemaResult.pages ? schemaResult : null;
6072
+ if (!schema || !Array.isArray(schema.pages) || schema.pages.length === 0) {
6073
+ throwCreateFormError(t('create_form.resume_schema_invalid'), 'CREATE_FORM_RESUME_SCHEMA_INVALID', {
6074
+ appType,
6075
+ formUuid,
6076
+ });
6077
+ }
6078
+ return { schemaResult, schema, version: extractSchemaServerRevision(schemaResult) };
6079
+ }
6080
+
6081
+ async function mainResume(parsedArgs, authRef) {
6082
+ const { appType, formUuid, fieldsJsonOrFile } = parsedArgs;
6083
+ assertNoEmojiInDefinitionFileName(fieldsJsonOrFile);
6084
+ const { fields, validations } = readFieldsDefinition(fieldsJsonOrFile);
6085
+ assertNoEmojiInFormDefinition(formUuid, fields, [], 'create-form resume input');
6086
+ validateFormFieldDefinitions(fields);
6087
+
6088
+ const firstRead = await readResumeSchema(authRef, appType, formUuid);
6089
+ const root = firstRead.schema.pages[0].componentsTree && firstRead.schema.pages[0].componentsTree[0];
6090
+ const formContainer = root ? findFormContainer(root) : null;
6091
+ const completedStages = ['read_target', 'verify_ownership'];
6092
+ let recoveredBlankShell = false;
6093
+ let plan;
6094
+
6095
+ if (!formContainer) {
6096
+ if (!isRecoverableBlankResumeSchema(firstRead.schema)) {
6097
+ throwCreateFormError(t('create_form.resume_container_invalid'), 'CREATE_FORM_RESUME_SCHEMA_INVALID', {
6098
+ appType,
6099
+ formUuid,
6100
+ retryable: false,
6101
+ remoteWrites: 0,
6102
+ });
6103
+ }
6104
+ const corpId = resolveCorpId(authRef.authData);
6105
+ const formTitle = firstRead.schemaResult.formTitle || firstRead.schemaResult.title || formUuid;
6106
+ firstRead.schema = buildFormSchema(formTitle, fields, formUuid, corpId, appType, 'single', 'default', 'top');
6107
+ const resumeValidationRules = collectSmartValidationRulesFromFields(fields).concat(validations || []);
6108
+ if (resumeValidationRules.length > 0) {
6109
+ applySmartValidations(firstRead.schema, resumeValidationRules);
6110
+ }
6111
+ await saveFormSchema(authRef, appType, formUuid, firstRead.schema, firstRead.version, 4);
6112
+ completedStages.push('rebuild_blank_schema', 'save_schema');
6113
+ recoveredBlankShell = true;
6114
+ plan = {
6115
+ existing: [],
6116
+ missing: collectResumeDesiredFields(fields, { verifyNestedFields: true }),
6117
+ conflicts: [],
6118
+ };
6119
+ } else if (!Array.isArray(formContainer.children)) {
6120
+ throwCreateFormError(t('create_form.resume_container_invalid'), 'CREATE_FORM_RESUME_SCHEMA_INVALID', {
6121
+ appType,
6122
+ formUuid,
6123
+ retryable: false,
6124
+ remoteWrites: 0,
6125
+ });
6126
+ } else {
6127
+ plan = buildResumeChanges(
6128
+ collectResumeFieldEvidence(formContainer.children),
6129
+ collectResumeDesiredFields(fields)
6130
+ );
6131
+ if (plan.conflicts.length > 0) {
6132
+ throwCreateFormError(t('create_form.resume_field_conflict'), 'CREATE_FORM_RESUME_CONFLICT', {
6133
+ appType,
6134
+ formUuid,
6135
+ retryable: false,
6136
+ remoteWrites: 0,
6137
+ diagnostics: plan.conflicts,
6138
+ });
6139
+ }
6140
+ completedStages.push('compare_fields');
6141
+ }
6142
+
6143
+ if (!recoveredBlankShell && plan.missing.length > 0) {
6144
+ const applied = applyChangesToSchema(
6145
+ firstRead.schema,
6146
+ plan.missing.map(function (field) { return { action: 'add', field }; }),
6147
+ { verbose: true }
6148
+ );
6149
+ if ((applied.diagnostics || []).length > 0) {
6150
+ throwCreateFormError(t('create_form.resume_field_resolution_failed'), 'CREATE_FORM_RESUME_CONFLICT', {
6151
+ appType,
6152
+ formUuid,
6153
+ retryable: false,
6154
+ remoteWrites: 0,
6155
+ diagnostics: applied.diagnostics,
6156
+ });
6157
+ }
6158
+ const corpId = resolveCorpId(authRef.authData);
6159
+ const updatedContainer = findFormContainer(firstRead.schema.pages[0].componentsTree[0]);
6160
+ fillSerialNumberFormulas(updatedContainer.children, corpId, appType, formUuid);
6161
+ await saveFormSchema(authRef, appType, formUuid, firstRead.schema, firstRead.version, 4);
6162
+ completedStages.push('add_missing_fields', 'save_schema');
6163
+ }
6164
+
6165
+ const finalRead = await readResumeSchema(authRef, appType, formUuid);
6166
+ const finalContainer = findFormContainer(finalRead.schema.pages[0].componentsTree[0]);
6167
+ const finalPlan = buildResumeChanges(
6168
+ collectResumeFieldEvidence(finalContainer && finalContainer.children),
6169
+ collectResumeDesiredFields(fields, { verifyNestedFields: true })
6170
+ );
6171
+ if (finalPlan.missing.length > 0 || finalPlan.conflicts.length > 0) {
6172
+ throwCreateFormError(t('create_form.resume_readback_mismatch'), 'CREATE_FORM_RESUME_READBACK_MISMATCH', {
6173
+ appType,
6174
+ formUuid,
6175
+ retryable: false,
6176
+ resultUnknown: true,
6177
+ completedStages,
6178
+ missingFieldCount: finalPlan.missing.length,
6179
+ diagnostics: finalPlan.conflicts,
6180
+ });
6181
+ }
6182
+ completedStages.push('verify_final_schema');
6183
+ const formUrl = authRef.baseUrl + '/' + appType + '/workbench/' + formUuid;
6184
+ const output = {
6185
+ success: true,
6186
+ appType,
6187
+ formUuid,
6188
+ completedStages,
6189
+ requestedFieldCount: fields.length,
6190
+ existingFieldCount: plan.existing.length,
6191
+ addedFieldCount: plan.missing.length,
6192
+ finalFieldCount: collectResumeFieldEvidence(finalContainer.children).length,
6193
+ recoveredBlankShell,
6194
+ url: formUrl,
6195
+ };
6196
+ console.log(JSON.stringify(withBrowserHandoff(
6197
+ output,
6198
+ formUrl,
6199
+ { stage: 'resume_form_success', title: formUuid },
6200
+ parsedArgs.browserOpenMode
6201
+ )));
6202
+ return output;
6203
+ }
6204
+
5914
6205
  async function mainUpdate(parsedArgs, authRef) {
5915
6206
  const { appType, formUuid, changesJsonOrFile } = parsedArgs;
5916
6207
 
@@ -6102,6 +6393,7 @@ async function run(args) {
6102
6393
  {
6103
6394
  create: mainCreate,
6104
6395
  update: mainUpdate,
6396
+ resume: mainResume,
6105
6397
  patch: mainPatch,
6106
6398
  rule: mainRule,
6107
6399
  validation: mainValidation,
@@ -530,8 +530,8 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
530
530
  componentName: 'Page',
531
531
  id: nextNodeId(),
532
532
  props: {
533
- contentBgColor: 'var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)))',
534
- pageStyle: { backgroundColor: 'var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)))' },
533
+ contentBgColor: 'var(--pod-page-bg-color, var(--color-white, #fff))',
534
+ pageStyle: { backgroundColor: 'var(--pod-page-bg-color, var(--color-white, #fff))' },
535
535
  contentMargin: '0',
536
536
  contentPadding: '0',
537
537
  showTitle: false,
@@ -539,7 +539,7 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
539
539
  templateVersion: '1.0.0',
540
540
  contentMarginMobile: '0',
541
541
  className: 'page_' + nextSuffix(),
542
- contentBgColorMobile: 'var(--oyd-page-background, var(--pod-page-bg-color, var(--color-white, #fff)))',
542
+ contentBgColorMobile: 'var(--pod-page-bg-color, var(--color-white, #fff))',
543
543
  },
544
544
  condition: true,
545
545
  css: [
@@ -449,6 +449,7 @@ const COMMAND_SIDE_EFFECTS = new Map([
449
449
  'create-form.bind-datasource',
450
450
  'create-form.create',
451
451
  'create-form.patch',
452
+ 'create-form.resume',
452
453
  'create-form.rule',
453
454
  'create-form.update',
454
455
  'create-form.validation',
@@ -689,6 +690,7 @@ const COMMAND_PERMISSIONS = new Map([
689
690
  'create-form.create',
690
691
  'create-form.batch',
691
692
  'create-form.patch',
693
+ 'create-form.resume',
692
694
  'create-form.rule',
693
695
  'create-form.update',
694
696
  'create-form.validation',
@@ -1223,7 +1225,8 @@ const COMMAND_GROUPS = [
1223
1225
  command('create-form.validate-fields', ['create-form', 'validate-fields'], 'create-form validate-fields <fieldsJsonOrFile> [--json]', 'help.cmd_validate_form', {
1224
1226
  requiresLogin: false,
1225
1227
  }),
1226
- command('create-form.update', ['create-form', 'update'], 'create-form update <appType> ... [--locale zh_CN|en_US|ja_JP] [--open|--no-open]', 'help.cmd_update_form'),
1228
+ command('create-form.update', ['create-form', 'update'], 'create-form update <appType> <formUuid> (<changesJsonOrFile> | --data-file <changesJsonOrFile>) [--locale zh_CN|en_US|ja_JP] [--open|--no-open]', 'help.cmd_update_form'),
1229
+ command('create-form.resume', ['create-form', 'resume'], 'create-form resume <appType> <formUuid> <fieldsJsonOrFile> [--json]', 'help.cmd_update_form'),
1227
1230
  command('create-form.patch', ['create-form', 'patch'], 'create-form patch <appType> <formUuid> <patchJsonOrFile> [--open|--no-open]', 'help.cmd_update_form'),
1228
1231
  command('create-form.rule', ['create-form', 'rule'], 'create-form rule <appType> <formUuid> <rulesJsonOrFile> [--open|--no-open]', 'help.cmd_update_form'),
1229
1232
  command('create-form.validation', ['create-form', 'validation'], 'create-form validation <appType> <formUuid> <validationsJsonOrFile> [--open|--no-open]', 'help.cmd_update_form'),
@@ -827,7 +827,15 @@ Examples:
827
827
  example_update: 'Example: openyida create-form update "APP_XXX" "FORM-YYY" \'[{"action":"add","field":{"type":"TextField","label":"Note"}}]\'',
828
828
  usage_label: 'Usage:',
829
829
  usage_create_short: ' create: openyida create-form create <appType> <formTitle> <fieldsJsonFile>',
830
- usage_update_short: ' update: openyida create-form update <appType> <formUuid> <changesJsonOrFile>',
830
+ usage_update_short: ' update: openyida create-form update <appType> <formUuid> <changesJsonOrFile> or --data-file <changesJsonOrFile>',
831
+ resume_readback_failed: 'Unable to read the target form before resume.',
832
+ resume_ownership_unverified: 'The target form does not belong to the requested app; resume stopped.',
833
+ resume_identity_mismatch: 'The target form identity does not match the readback; resume stopped.',
834
+ resume_schema_invalid: 'The target form schema is unavailable.',
835
+ resume_container_invalid: 'The target form container is unavailable.',
836
+ resume_field_conflict: 'Existing fields conflict with the requested definition; resume stopped.',
837
+ resume_field_resolution_failed: 'Resume field resolution failed.',
838
+ resume_readback_mismatch: 'Resume readback did not confirm all requested fields.',
831
839
  example_label: '\nExamples:',
832
840
  fields_file_not_found: ' ❌ Fields definition file not found: ',
833
841
  fields_format_invalid: 'Invalid fields definition format',
@@ -798,7 +798,15 @@ openyida - 宜搭命令行工具
798
798
  example_update: '示例:openyida create-form update "APP_XXX" "FORM-YYY" \'[{"action":"add","field":{"type":"TextField","label":"备注"}}]\'',
799
799
  usage_label: '用法:',
800
800
  usage_create_short: ' 创建: openyida create-form create <appType> <formTitle> <fieldsJsonFile>',
801
- usage_update_short: ' 更新: openyida create-form update <appType> <formUuid> <changesJsonOrFile>',
801
+ usage_update_short: ' 更新: openyida create-form update <appType> <formUuid> <changesJsonOrFile> 或 --data-file <changesJsonOrFile>',
802
+ resume_readback_failed: '恢复前无法回读目标表单。',
803
+ resume_ownership_unverified: '目标表单不属于指定应用,已停止恢复。',
804
+ resume_identity_mismatch: '目标表单身份与回读结果不一致,已停止恢复。',
805
+ resume_schema_invalid: '目标表单 Schema 不可用。',
806
+ resume_container_invalid: '目标表单容器不可用。',
807
+ resume_field_conflict: '已有字段与请求定义冲突,恢复已停止。',
808
+ resume_field_resolution_failed: '恢复字段解析失败。',
809
+ resume_readback_mismatch: '恢复后的回读未确认全部请求字段。',
802
810
  example_label: '\n示例:',
803
811
  fields_file_not_found: ' ❌ 字段定义文件不存在: ',
804
812
  fields_format_invalid: '字段定义格式不正确',
@@ -51,12 +51,14 @@ const SAMPLES = {
51
51
  'canvas-form-drawer': 'canvas-form-drawer.canvas.jsx',
52
52
  'form-open-container': 'canvas-form-drawer.canvas.jsx',
53
53
  'canvas-dialog': 'canvas-dialog.canvas.jsx',
54
+ 'canvas-theme': 'canvas-form-drawer.canvas.jsx',
54
55
  'canvas-nav-side': 'canvas-nav/side.jsx',
55
56
  'canvas-nav-top': 'canvas-nav/top.jsx',
56
57
  'canvas-nav-mixed': 'canvas-nav/mixed.jsx',
57
58
  'canvas-nav-dock': 'canvas-nav/dock.jsx',
58
59
  'canvas-nav-tabs': 'canvas-nav/tabs.jsx',
59
60
  'canvas-nav-data': 'canvas-nav/data.jsx',
61
+ 'canvas-nav-content': 'canvas-nav/content.jsx',
60
62
  },
61
63
  'yida-design': {
62
64
  'app-theme': 'app-custom-theme-template.css',
@@ -91,12 +93,15 @@ const SAMPLE_OUTPUT_FILENAMES = {
91
93
  'openyida-page-template/canvas-nav-dock': 'canvas-nav-dock.jsx',
92
94
  'openyida-page-template/canvas-nav-tabs': 'canvas-nav-tabs.jsx',
93
95
  'openyida-page-template/canvas-nav-data': 'canvas-nav-data.jsx',
96
+ 'openyida-page-template/canvas-nav-content': 'canvas-nav-content.jsx',
94
97
  'openyida-page-template/form-open-container': 'form-open-container.jsx',
95
98
  'openyida-page-template/canvas-dialog': 'canvas-dialog.jsx',
99
+ 'openyida-page-template/canvas-theme': 'canvas-theme.jsx',
96
100
  'yida-design/app-theme': 'app-theme.css',
97
101
  };
98
102
 
99
103
  const SAMPLE_HINTS = {
104
+ 'openyida-page-template/canvas-nav-content': '合并 CanvasNavigationContent;顶部导航传 navigation,工作台传 children,原生页传 iframeSrc 和 title;宿主已分配高度时显式传 height。',
100
105
  'openyida-page-template/canvas-nav-data': '向 loadCanvasNavigation 传入 PRD 规划的 items,通常工作台在首位;接口只过滤不可见入口,保留规划顺序与分组。',
101
106
  'openyida-page-template/form-open-container': '将 import、抽屉组件和辅助函数合并到现有 Canvas 页面;表单使用 useYidaFormOpen,其他内容使用 CanvasDrawer。',
102
107
  'yida-design/app-theme': '已复制应用主题模板;请由 yida-design 按 design.md 定点修改需要变化的 token。',
@@ -251,7 +256,7 @@ async function run(args) {
251
256
  const defaultOutputPath = path.join(process.cwd(), '.cache', 'samples', outputFilename);
252
257
  const { outputPath, variables, designFile } = parseOptions(rest, defaultOutputPath);
253
258
  const isNavSample = skill === 'openyida-page-template' && name.startsWith('canvas-nav-');
254
- const prefixPath = isNavSample && !['canvas-nav-tabs', 'canvas-nav-data'].includes(name) ? path.join(path.dirname(sourcePath), 'shared.jsx') : null;
259
+ const prefixPath = isNavSample && !['canvas-nav-tabs', 'canvas-nav-data', 'canvas-nav-content'].includes(name) ? path.join(path.dirname(sourcePath), 'shared.jsx') : null;
255
260
  const sidebarPath = isNavSample && ['canvas-nav-side', 'canvas-nav-mixed'].includes(name) ? path.join(path.dirname(sourcePath), 'sidebar.jsx') : null;
256
261
  if ([sourcePath, prefixPath, sidebarPath, designFile].filter(Boolean).some(file => path.resolve(outputPath) === path.resolve(file))) {
257
262
  throw new CliError('输出路径不能覆盖模板或 design.md', { code: 'SAMPLE_OUTPUT_CONFLICT' });
@@ -289,6 +294,13 @@ async function run(args) {
289
294
  } else if (prefixPath) {
290
295
  const content = [prefixPath, sidebarPath, sourcePath].filter(Boolean).map(file => fs.readFileSync(file, 'utf8')).join('\n');
291
296
  fs.writeFileSync(outputPath, applyTemplateVariables(content, variables), 'utf8');
297
+ } else if (sampleKey === 'openyida-page-template/canvas-theme') {
298
+ const sourceContent = fs.readFileSync(sourcePath, 'utf8');
299
+ const fragment = sourceContent.match(/\/\/ @openyida-canvas-theme:start\r?\n([\s\S]*?)\/\/ @openyida-canvas-theme:end/);
300
+ if (!fragment) {
301
+ throw new CliError('Canvas theme fragment missing', { code: 'SAMPLE_FRAGMENT_MISSING' });
302
+ }
303
+ fs.writeFileSync(outputPath, "import React from 'react';\n\n" + fragment[1], 'utf8');
292
304
  } else if (sampleKey === 'openyida-page-template/form-open-container') {
293
305
  const sourceContent = fs.readFileSync(sourcePath, 'utf8');
294
306
  const fragment = sourceContent.match(/\/\/ @openyida-form-drawer:start\r?\n([\s\S]*?)\/\/ @openyida-form-drawer:end/);
@@ -491,8 +491,7 @@ function deriveSurfacePalette(primaryColor, surfaceTone, template) {
491
491
  const darkSurface = surface && [1, 3, 5].every(index => parseInt(surface.slice(index, index + 2), 16) < 128);
492
492
  if (surfaceTone !== 'brand-tinted' || darkSurface) {return {};}
493
493
  return {
494
- // 品牌浅底属于 Shell;原生页面保留独立表面,Canvas 透明时透出 Shell。
495
- // 不覆盖上方按导航归属生成的 --oyd-page-background。
494
+ // 品牌氛围属于 Shell;原生页面和 Canvas 共用页面底色,不受导航归属影响。
496
495
  '--pod-shell-bg-color-light': mixHex(primaryColor, [255, 255, 255], 0.94),
497
496
  '--pod-card-bg-color': 'var(--color-white, #fff)',
498
497
  '--color-fill1-1': mixHex(primaryColor, [255, 255, 255], 0.97),
@@ -684,12 +683,9 @@ function renderDesign(plan) {
684
683
  for (const [token, value] of Object.entries(derived)) {
685
684
  output = replaceTokenValue(output, token, value);
686
685
  }
687
- // 页面背景先遵循导航形态;项目显式 token 覆盖在下方应用,始终优先。
688
- // 深色设计保留其明确的暗色画布,普通平台导航默认跟随应用页面背景。
689
- if (navigationConfig(plan).hideAppNav === 'y' && !selected.themeId.startsWith('dark-')) {
690
- output = replaceTokenValue(output, '--oyd-page-background', 'transparent');
691
- } else if (!selected.themeId.startsWith('dark-')) {
692
- output = replaceTokenValue(output, '--oyd-page-background', 'var(--pod-page-bg-color, var(--color-white, #fff))');
686
+ // 页面底色统一使用平台 token;导航形态不改变底色,项目显式设计仍优先。
687
+ if (!selected.themeId.startsWith('dark-')) {
688
+ output = replaceTokenValue(output, '--pod-page-bg-color', 'var(--color-white, #fff)');
693
689
  }
694
690
  const explicitTokens = visual.tokens || {};
695
691
  if (typeof explicitTokens !== 'object' || Array.isArray(explicitTokens)) {
@@ -718,9 +714,9 @@ function renderDesign(plan) {
718
714
  output = injectAfterFrontmatter(output, [
719
715
  '## 项目配色适配(优先于模板默认配色)', '',
720
716
  `用户选择的 ${text(color.primaryColorName, primaryColor)} 用于整体浅色氛围。Shell 浅色背景、填充和边界已按同一主色派生,项目显式 token 覆盖优先;最终取值以本文 tokens 为准。`,
721
- '背景分层:--pod-shell-bg-color-light 承载 Shell 浅色氛围;--pod-page-bg-color 保留原生表单/数据管理的独立底色(浅色主题通常为白色);--oyd-page-background 仅控制自定义页。隐藏应用导航时自定义页默认透明,显示平台导航时默认跟随原生页面底色;品牌浅底不得覆盖这项导航默认值。仅有明确的页面背景设计时覆盖 --oyd-page-background,不要因此改写 --pod-page-bg-color。',
717
+ '背景分层:Shell 承载外层氛围;原生页面与自定义页统一消费 --pod-page-bg-color,卡片与面板消费 --pod-card-bg-color。导航隐藏不自动改为透明;渐变、纹理、图片属于页面局部装饰层。明确的应用背景设计通过 app-theme.css 配置平台 token。',
722
718
  '模板中“固定无彩画布”“中性灰必须 R=G=B”“禁止主题色进入背景”等默认限制在本项目不适用;保留布局、圆角、阴影与组件结构。不要为了保留模板灰底,把用户选择的浅彩导航改白。',
723
- '页面背景消费 --oyd-page-background,卡片消费 --pod-card-bg-color(回退 --color-white),边界消费 --pod-card-border;近白页面与白卡必须保留可见细边框,不能只靠阴影分层。导航、表头、嵌套区域与选中态消费同一组品牌/填充 token。',
719
+ '页面背景消费 --pod-page-bg-color,卡片消费 --pod-card-bg-color(回退 --color-white),边界消费 --pod-card-border;近白页面与白卡必须保留可见细边框,不能只靠阴影分层。导航、表头、嵌套区域与选中态消费同一组品牌/填充 token。',
724
720
  '正文与标题使用 --color-text1-4,表头与 placeholder 使用 --color-text1-10,辅助说明使用 --color-text1-3;保持中性文字明度层级,不把整页文字染成主色。成功、警告、错误沿用应用语义色。',
725
721
  'antd 的主色、容器、浮层、文字、边框、填充及语义色都须映射应用 token;浅彩氛围不能仅出现在导航和主按钮。页面实现前先读本节,再读取下面的结构与组件规则。', '',
726
722
  ].join('\n'));