openyida 2026.10.12-beta.2 → 2026.10.13

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 (167) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +16 -27
  3. package/bin/yida.js +7 -32
  4. package/lib/app/app-theme.js +289 -0
  5. package/lib/app/check-prd-completeness.js +4 -15
  6. package/lib/app/create-form/batch.js +9 -30
  7. package/lib/app/create-form.js +50 -3
  8. package/lib/app/custom-theme.js +3 -13
  9. package/lib/app/get-schema.js +1 -6
  10. package/lib/app/nav-group-create.js +69 -0
  11. package/lib/app/nav-group.js +8 -28
  12. package/lib/app/theme-from-design.js +28 -16
  13. package/lib/app/theme-source.js +54 -0
  14. package/lib/app/update-app.js +1 -13
  15. package/lib/app/update-form-config.js +80 -71
  16. package/lib/auth/token-auth.js +6 -57
  17. package/lib/auth/token-store.js +2 -26
  18. package/lib/core/command-manifest.js +168 -175
  19. package/lib/core/locales/en.js +23 -47
  20. package/lib/core/locales/zh.js +23 -47
  21. package/lib/core/sample.js +21 -2
  22. package/lib/core/theme-widget.js +115 -0
  23. package/lib/core/update.js +1 -3
  24. package/lib/core/utils.js +19 -29
  25. package/lib/design/document.js +8 -3
  26. package/lib/design/palette-contrast.js +16 -3
  27. package/lib/design-plan/confirmation.js +1 -1
  28. package/lib/design-plan/init.js +21 -6
  29. package/lib/design-plan/materialize.js +30 -9
  30. package/lib/design-plan/patch.js +4 -2
  31. package/lib/design-plan/themes.js +6 -1
  32. package/lib/design-plan/visual-policy.js +70 -8
  33. package/lib/page-config/save-share-config.js +5 -16
  34. package/package.json +1 -10
  35. package/scripts/postinstall.js +3 -1
  36. package/yida-skills/SKILL.md +3 -27
  37. package/yida-skills/skills/yida-app/workflow/parallel-work.md +1 -1
  38. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +3 -3
  39. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +1 -1
  40. package/yida-skills/skills/yida-app/workflow/step-2-design.md +3 -1
  41. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +1 -1
  42. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +6 -0
  43. package/yida-skills/skills/yida-create-form-page/SKILL.md +5 -1
  44. package/yida-skills/skills/yida-custom-page/SKILL.md +6 -0
  45. package/yida-skills/skills/yida-dashboard/SKILL.md +2 -2
  46. package/yida-skills/skills/yida-dashboard/references/theme-presets.md +20 -245
  47. package/yida-skills/skills/yida-design/SKILL.md +22 -10
  48. package/yida-skills/skills/yida-design/references/application-style-library.md +19 -6
  49. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +13 -7
  50. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +19 -9
  51. package/yida-skills/skills/yida-design/references/navigation-decision.md +14 -1
  52. package/yida-skills/skills/yida-design/references/page-quality-gates.md +16 -0
  53. package/yida-skills/skills/yida-design/references/responsive-layout.md +54 -0
  54. package/yida-skills/skills/yida-design/references/theme/theme-token-presets.md +5 -24
  55. package/yida-skills/skills/yida-design/references/theme-preview-images.json +19 -0
  56. package/yida-skills/skills/yida-design/references/theme-selection.md +80 -9
  57. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +324 -303
  58. package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +2 -0
  59. package/yida-skills/skills/yida-design/sub_skill/ui-improve/SKILL.md +86 -0
  60. package/yida-skills/skills/yida-design/sub_skill/ui-improve/references/application-coverage.md +44 -0
  61. package/yida-skills/skills/yida-design/sub_skill/ui-improve/references/backup-and-restore.md +65 -0
  62. package/yida-skills/skills/yida-design/sub_skill/ui-improve/references/component-replacement.md +18 -0
  63. package/yida-skills/skills/yida-design/sub_skill/ui-improve/references/confirmation.md +26 -0
  64. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +5 -1
  65. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/build-plan-template.html +1186 -1186
  66. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/icons/form-normal.svg +4 -4
  67. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/icons/form-process.svg +6 -6
  68. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/icons/graph-fit.svg +3 -3
  69. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/icons/graph-relations.svg +7 -7
  70. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/icons/graph-zoom-in.svg +4 -4
  71. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/assets/icons/graph-zoom-out.svg +4 -4
  72. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/visual-theme-selection.md +29 -22
  73. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +1166 -1166
  74. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/app_theme.css +2 -2
  75. package/yida-skills/skills/yida-design/templates/design-themes/app-editorial/design.md +4 -2
  76. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/app_theme.css +2 -2
  77. package/yida-skills/skills/yida-design/templates/design-themes/app-executive/design.md +4 -2
  78. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/app_theme.css +2 -2
  79. package/yida-skills/skills/yida-design/templates/design-themes/app-finance/design.md +4 -2
  80. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/app_theme.css +2 -2
  81. package/yida-skills/skills/yida-design/templates/design-themes/app-glass/design.md +4 -2
  82. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/app_theme.css +2 -2
  83. package/yida-skills/skills/yida-design/templates/design-themes/app-graphite/design.md +4 -2
  84. package/yida-skills/skills/yida-design/templates/design-themes/app-line/app_theme.css +2 -2
  85. package/yida-skills/skills/yida-design/templates/design-themes/app-line/design.md +4 -2
  86. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/app_theme.css +2 -2
  87. package/yida-skills/skills/yida-design/templates/design-themes/app-neon/design.md +4 -2
  88. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/app_theme.css +2 -2
  89. package/yida-skills/skills/yida-design/templates/design-themes/app-nordic/design.md +4 -2
  90. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/app_theme.css +2 -2
  91. package/yida-skills/skills/yida-design/templates/design-themes/app-paper/design.md +4 -2
  92. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/app_theme.css +71 -67
  93. package/yida-skills/skills/yida-design/templates/design-themes/app-platinum/design.md +108 -69
  94. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/app_theme.css +2 -2
  95. package/yida-skills/skills/yida-design/templates/design-themes/app-plum/design.md +4 -2
  96. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/app_theme.css +229 -40
  97. package/yida-skills/skills/yida-design/templates/design-themes/app-pop/design.md +54 -44
  98. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/app_theme.css +107 -0
  99. package/yida-skills/skills/yida-design/templates/design-themes/app-sage/design.md +43 -1
  100. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/app_theme.css +16 -16
  101. package/yida-skills/skills/yida-design/templates/design-themes/app-teal/design.md +98 -17
  102. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/app_theme.css +23 -23
  103. package/yida-skills/skills/yida-design/templates/design-themes/app-terminal/design.md +98 -25
  104. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/app_theme.css +2 -2
  105. package/yida-skills/skills/yida-design/templates/design-themes/app-ticket/design.md +4 -2
  106. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/app_theme.css +2 -2
  107. package/yida-skills/skills/yida-design/templates/design-themes/app-wire/design.md +4 -2
  108. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/app_theme.css +15 -4
  109. package/yida-skills/skills/yida-design/templates/design-themes/dark-inset-hairline/design.md +22 -10
  110. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/app_theme.css +21 -10
  111. package/yida-skills/skills/yida-design/templates/design-themes/dark-rail-fine-lines/design.md +24 -20
  112. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/app_theme.css +2 -2
  113. package/yida-skills/skills/yida-design/templates/design-themes/free-creative/design.md +4 -2
  114. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/app_theme.css +160 -18
  115. package/yida-skills/skills/yida-design/templates/design-themes/hairline-soft-blocks/design.md +57 -36
  116. package/yida-skills/skills/yida-design/templates/design-themes/index.json +73 -16
  117. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/app_theme.css +163 -29
  118. package/yida-skills/skills/yida-design/templates/design-themes/ink-glow-soft-panels/design.md +85 -74
  119. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/app_theme.css +170 -27
  120. package/yida-skills/skills/yida-design/templates/design-themes/inset-frame-pixel-rhythm/design.md +50 -29
  121. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/app_theme.css +155 -36
  122. package/yida-skills/skills/yida-design/templates/design-themes/outlined-texture-duotone/design.md +46 -26
  123. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/app_theme.css +161 -29
  124. package/yida-skills/skills/yida-design/templates/design-themes/soft-inset-surfaces/design.md +70 -36
  125. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/app_theme.css +119 -8
  126. package/yida-skills/skills/yida-design/templates/design-themes/soft-outline-rhythm/design.md +31 -23
  127. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/app_theme.css +166 -27
  128. package/yida-skills/skills/yida-design/templates/design-themes/soft-rail-bold-band/design.md +50 -30
  129. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/app_theme.css +152 -15
  130. package/yida-skills/skills/yida-design/templates/design-themes/warm-canvas-contrast-panels/design.md +38 -20
  131. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/app_theme.css +1 -1
  132. package/yida-skills/skills/yida-design/templates/design-themes/warm-rail-muted-panels/design.md +3 -1
  133. package/yida-skills/skills/yida-design/templates/theme-selector.html +83 -0
  134. package/yida-skills/skills/yida-design/workflow/output-design.md +7 -4
  135. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +2 -0
  136. package/yida-skills/skills/yida-design/workflow/step-5-visual-states.md +2 -0
  137. package/yida-skills/skills/yida-document-markdown/SKILL.md +0 -6
  138. package/yida-skills/skills/yida-nav-group/SKILL.md +6 -2
  139. package/yida-skills/skills/yida-nav-shell/SKILL.md +2 -0
  140. package/yida-skills/skills/yida-prd/SKILL.md +2 -0
  141. package/yida-skills/skills/yida-publish-page/SKILL.md +4 -0
  142. package/yida-skills/skills/yida-requirement-analysis/references/handoff.md +4 -2
  143. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +2 -0
  144. package/yida-skills/skills/yida-tingji/SKILL.md +0 -4
  145. package/yida-skills/skills-index.json +11 -3
  146. package/lib/agent/PROTOCOL.md +0 -182
  147. package/lib/agent/ask-human.js +0 -151
  148. package/lib/agent/cmd.js +0 -405
  149. package/lib/agent/command-receipt.js +0 -84
  150. package/lib/agent/connection-store.js +0 -148
  151. package/lib/agent/diagnostic-log.js +0 -133
  152. package/lib/agent/frozen-bundle.js +0 -286
  153. package/lib/agent/managed-command-map.js +0 -36
  154. package/lib/agent/managed-context.js +0 -40
  155. package/lib/agent/managed-run.js +0 -85
  156. package/lib/agent/mutation-receipt.js +0 -82
  157. package/lib/agent/private-directory.js +0 -63
  158. package/lib/agent/provider-preflight.js +0 -257
  159. package/lib/agent/providers.js +0 -4
  160. package/lib/agent/runtime-catalog.json +0 -45
  161. package/lib/agent/runtime-manifest.js +0 -96
  162. package/lib/agent/runtime-package.js +0 -155
  163. package/lib/agent/stdio.js +0 -321
  164. package/lib/auth/profile-lock.js +0 -89
  165. package/lib/core/managed-policy.js +0 -81
  166. package/yida-skills/references/local-agent-diagnostics.md +0 -154
  167. package/yida-skills/references/local-agent-preflight.md +0 -56
@@ -862,6 +862,16 @@ function buildFieldComponent(field) {
862
862
  props.mode = 'multiple';
863
863
  }
864
864
 
865
+ const explicitDefault = field.value ?? field.defaultValue ?? field.props?.value
866
+ ?? field.props?.complexValue?.value;
867
+ const checkedValues = dataSource.filter(item => item.defaultChecked).map(item => item.value);
868
+ const selected = explicitDefault !== undefined
869
+ ? explicitDefault
870
+ : ((componentName === 'CheckboxField' || componentName === 'MultiSelectField')
871
+ ? (checkedValues.length ? checkedValues : '') : (checkedValues[0] ?? ''));
872
+ props.value = selected;
873
+ props.complexValue.value = selected;
874
+
865
875
  if (field.remoteDataSource || field.searchDataSource || field.dataSourceConfig || field.dataSourceUrl || field.searchConfig) {
866
876
  applySelectDataSourceConfig(props, field.remoteDataSource || field.searchDataSource || field.dataSourceConfig || {
867
877
  url: field.dataSourceUrl,
@@ -4809,6 +4819,9 @@ function buildCreateFormPostCreateFailurePayload(context) {
4809
4819
  errorCode: errorObject && errorObject.code ? errorObject.code : (context.errorCode || 'CREATE_FORM_POST_CREATE_FAILED'),
4810
4820
  retryAdvice: t('create_form.create_post_failure_retry_advice', context.appType, context.formTitle),
4811
4821
  };
4822
+ for (const key of ['remoteSaveState', 'missingFieldCount', 'diagnostics']) {
4823
+ if (errorObject?.details?.[key] !== undefined) { payload[key] = errorObject.details[key]; }
4824
+ }
4812
4825
  if (context.fieldCount !== undefined) {
4813
4826
  payload.fieldCount = context.fieldCount;
4814
4827
  }
@@ -5057,8 +5070,12 @@ async function mainCreate(parsedArgs, authRef) {
5057
5070
  formUuid,
5058
5071
  fieldCount,
5059
5072
  });
5073
+ postCreateStage = 'verifySavedSchema';
5074
+ await verifyCreatedFormSchema(authRef, appType, formUuid, fields);
5060
5075
  postCreateStage = 'updateFormNavigationIcon';
5061
5076
  await updateCreatedFormNavigationIcon(authRef, appType, formUuid, formIcon);
5077
+ postCreateStage = 'verifyFinalSchema';
5078
+ await verifyCreatedFormSchema(authRef, appType, formUuid, fields);
5062
5079
  } catch (err) {
5063
5080
  emitCreateFormPostCreateFailure({
5064
5081
  appType,
@@ -5164,7 +5181,9 @@ async function createFormForLegacyProcessQuiet(context, input) {
5164
5181
  applySmartValidations(schema, createValidationRules);
5165
5182
  }
5166
5183
  const saveResult = await saveFormSchema(authRef, appType, formUuid, schema, serverRevision, 4);
5184
+ await verifyCreatedFormSchema(authRef, appType, formUuid, fields);
5167
5185
  const navIconResult = await updateCreatedFormNavigationIcon(authRef, appType, formUuid, formIcon);
5186
+ await verifyCreatedFormSchema(authRef, appType, formUuid, fields);
5168
5187
  const { appUrl, formUrl } = buildFormDeliveryUrls(authRef.baseUrl, appType, formUuid);
5169
5188
  return {
5170
5189
  success: true,
@@ -6154,13 +6173,34 @@ async function readResumeSchema(authRef, appType, formUuid) {
6154
6173
  return { schemaResult, schema, version: extractSchemaServerRevision(schemaResult) };
6155
6174
  }
6156
6175
 
6176
+ async function verifyCreatedFormSchema(authRef, appType, formUuid, fields) {
6177
+ const readback = await readResumeSchema(authRef, appType, formUuid);
6178
+ const root = readback.schema.pages[0].componentsTree?.[0];
6179
+ const container = root ? findFormContainer(root) : null;
6180
+ const verified = buildResumeChanges(
6181
+ collectResumeFieldEvidence(container?.children),
6182
+ collectResumeDesiredFields(fields, { verifyNestedFields: true })
6183
+ );
6184
+ if (verified.missing.length || verified.conflicts.length) {
6185
+ throwCreateFormError(t('create_form.create_readback_mismatch'),
6186
+ 'CREATE_FORM_CREATE_READBACK_MISMATCH', {
6187
+ appType, formUuid, retryable: false,
6188
+ remoteSaveState: verified.existing.length ? 'partial' : 'not_saved',
6189
+ missingFieldCount: verified.missing.length, diagnostics: verified.conflicts,
6190
+ });
6191
+ }
6192
+ }
6193
+
6157
6194
  function isRetryableResumeSaveServerFailure(errorObject) {
6158
6195
  const status = Number(errorObject?.details?.result?.__httpStatus);
6196
+ const businessStatus = Number(errorObject?.details?.result?.errorCode
6197
+ ?? errorObject?.details?.result?.code);
6159
6198
  return errorObject?.code === 'CREATE_FORM_SAVE_SCHEMA_FAILED' &&
6160
- Number.isInteger(status) && status >= 500 && status <= 599;
6199
+ ((Number.isInteger(status) && status >= 500 && status <= 599)
6200
+ || (Number.isInteger(businessStatus) && businessStatus >= 500 && businessStatus <= 599));
6161
6201
  }
6162
6202
 
6163
- async function saveResumeSchemaWithSafeRetry(authRef, appType, formUuid, schema, version, fields) {
6203
+ async function saveResumeSchemaWithSafeRetry(authRef, appType, formUuid, schema, version, fields, attemptedFields = fields) {
6164
6204
  const save = (value, revision) => saveFormSchema(
6165
6205
  authRef, appType, formUuid, value, revision, 4, { deferFailureOutput: true }
6166
6206
  );
@@ -6170,6 +6210,7 @@ async function saveResumeSchemaWithSafeRetry(authRef, appType, formUuid, schema,
6170
6210
  } catch (originalError) {
6171
6211
  if (!isRetryableResumeSaveServerFailure(originalError)) { throw originalError; }
6172
6212
 
6213
+ originalError.details = { ...originalError.details, remoteSaveState: 'unknown' };
6173
6214
  let readback;
6174
6215
  try { readback = await readResumeSchema(authRef, appType, formUuid); } catch (_) { throw originalError; }
6175
6216
  const root = readback.schema.pages[0].componentsTree && readback.schema.pages[0].componentsTree[0];
@@ -6182,6 +6223,12 @@ async function saveResumeSchemaWithSafeRetry(authRef, appType, formUuid, schema,
6182
6223
  );
6183
6224
  if (verified.conflicts.length > 0) { throw originalError; }
6184
6225
  if (verified.missing.length === 0) { return 'save_schema_recovered_by_readback'; }
6226
+ const attempted = buildResumeChanges(evidence, collectResumeDesiredFields(attemptedFields, { verifyNestedFields: true }));
6227
+ originalError.details.remoteSaveState = attempted.existing.length ? 'partial' : 'not_saved';
6228
+ // A business 500 can be a duplicate-key INSERT. Readback provides facts,
6229
+ // but never licenses another write on that ambiguous platform path.
6230
+ const businessStatus = Number(originalError.details.result?.errorCode ?? originalError.details.result?.code);
6231
+ if (businessStatus >= 500 && businessStatus <= 599) { throw originalError; }
6185
6232
  const retryPlan = buildResumeChanges(evidence, collectResumeDesiredFields(fields));
6186
6233
  if (retryPlan.conflicts.length > 0 || retryPlan.missing.length === 0) { throw originalError; }
6187
6234
  const applied = applyChangesToSchema(
@@ -6291,7 +6338,7 @@ async function mainResume(parsedArgs, authRef) {
6291
6338
  const updatedContainer = findFormContainer(firstRead.schema.pages[0].componentsTree[0]);
6292
6339
  fillSerialNumberFormulas(updatedContainer.children, corpId, appType, formUuid);
6293
6340
  const saveRecoveryStage = await saveResumeSchemaWithSafeRetry(
6294
- authRef, appType, formUuid, firstRead.schema, firstRead.version, fields
6341
+ authRef, appType, formUuid, firstRead.schema, firstRead.version, fields, plan.missing
6295
6342
  );
6296
6343
  completedStages.push('add_missing_fields', 'save_schema');
6297
6344
  if (saveRecoveryStage) { completedStages.push(saveRecoveryStage); }
@@ -2,8 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { resolveBearerAuthHeaders } = require('../core/utils');
6
- const { readManagedContext, managedError } = require('../agent/managed-context');
5
+ const { getAccessToken } = require('../auth/token-auth');
7
6
  const { CliError } = require('../core/cli-error');
8
7
  const { t } = require('../core/i18n');
9
8
  const { findCssStructureIssue } = require('../core/css-structure');
@@ -261,13 +260,6 @@ function buildCustomThemeStyle(uploadResult) {
261
260
  }
262
261
 
263
262
  async function uploadCustomThemeFile(appType, filePath, authRef, options = {}) {
264
- const managed = readManagedContext(process.env);
265
- if (managed && appType !== managed.appType) {
266
- throw managedError('MANAGED_APP_MISMATCH', 'managed_app_mismatch');
267
- }
268
- if (managed && new URL(authRef.baseUrl).origin !== new URL(managed.baseUrl).origin) {
269
- throw managedError('MANAGED_OVERRIDE_FORBIDDEN', 'managed_override_forbidden');
270
- }
271
263
  const file = readThemeCssFile(filePath);
272
264
  const fetchImpl = options.fetchImpl || globalThis.fetch;
273
265
  const FormDataImpl = options.FormDataImpl || globalThis.FormData;
@@ -276,7 +268,7 @@ async function uploadCustomThemeFile(appType, filePath, authRef, options = {}) {
276
268
  throw new Error('当前 Node.js 环境缺少 fetch/FormData/Blob,请使用 Node.js 18+');
277
269
  }
278
270
 
279
- const auth = await resolveBearerAuthHeaders({ projectRoot: authRef && authRef.projectRoot });
271
+ const accessToken = await getAccessToken({ projectRoot: authRef && authRef.projectRoot });
280
272
  const endpoint = new URL('/query/app/customTheme/upload.json', authRef.baseUrl);
281
273
  if (authRef.csrfToken) {
282
274
  endpoint.searchParams.set('_csrf_token', authRef.csrfToken);
@@ -287,11 +279,9 @@ async function uploadCustomThemeFile(appType, filePath, authRef, options = {}) {
287
279
 
288
280
  const response = await fetchImpl(endpoint, {
289
281
  method: 'POST',
290
- // Never forward a multipart request carrying credentials to another URL.
291
- redirect: 'error',
292
282
  headers: {
293
283
  Accept: 'application/json, text/plain, */*',
294
- ...auth.headers,
284
+ Authorization: `Bearer ${accessToken}`,
295
285
  Origin: authRef.baseUrl,
296
286
  Referer: `${authRef.baseUrl}/`,
297
287
  'x-requested-with': 'XMLHttpRequest',
@@ -78,12 +78,7 @@ function extractFieldSummary(schemaResult) {
78
78
  const complexDefault = props.complexValue && typeof props.complexValue === 'object'
79
79
  ? props.complexValue.value
80
80
  : undefined;
81
- // Custom text defaults are rendered from props.value; complexValue may
82
- // retain an obsolete empty value after a targeted field update.
83
- const customText = ['TextField', 'TextareaField'].includes(node.componentName) && props.valueType === 'custom';
84
- const defaultValue = firstPresent(customText
85
- ? [props.value, complexDefault, props.defaultValue, props.default]
86
- : [complexDefault, props.defaultValue, props.value, props.default]);
81
+ const defaultValue = firstPresent([complexDefault, props.defaultValue, props.value, props.default]);
87
82
  const validationRules = Array.isArray(props.validation) ? props.validation : [];
88
83
  const hasRequiredSignal = Object.prototype.hasOwnProperty.call(props, 'required')
89
84
  || validationRules.some(rule => rule && (rule.type === 'required' || rule.required === true));
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../core/cli-error');
4
+ const { resolveFormNavIcon } = require('./create-form/nav-icon');
5
+ const { buildFormNavigationIconUpdatePayload } = require('./create-form/nav-icon-service');
6
+
7
+ /** Create a group, then persist a semantic icon using the platform navigation protocol. */
8
+ async function createGroupWithIcon(appType, name, flags, authRef, dependencies) {
9
+ const { fetchNavigationList, postNavAction, resolveNode, buildI18nTitle,
10
+ getNodeName, normalizeNavNode, rootNavUuid } = dependencies;
11
+ if (!name) {throw new Error('Group name is required');}
12
+ const selection = resolveFormNavIcon(flags.icon, name, []);
13
+ if (!selection.icon) {
14
+ throw new CliError('Invalid navigation icon. Use auto or a supported page navigation icon name.', {
15
+ code: 'NAV_GROUP_ICON_INVALID', details: { icon: flags.icon, retrySafe: true },
16
+ });
17
+ }
18
+ const list = await fetchNavigationList(appType, authRef);
19
+ const parent = resolveNode(list, flags.parent || rootNavUuid, { groupOnly: true });
20
+ if (parent.navUuid !== rootNavUuid && parent.parentNavUuid !== rootNavUuid) {
21
+ throw new Error('New groups can only be created at root or under a top-level group');
22
+ }
23
+
24
+ const beforeIds = new Set(list.map((node) => node.navUuid));
25
+ let created;
26
+ let stage = 'create';
27
+ try {
28
+ // Do not replay a creation on an uncertain response: it can duplicate the group.
29
+ await postNavAction(appType, 'save', {
30
+ parentNavUuid: parent.navUuid,
31
+ title: JSON.stringify(buildI18nTitle(name, flags.en)),
32
+ }, authRef, { oneShot: true });
33
+ stage = 'resolve';
34
+ const updatedList = await fetchNavigationList(appType, authRef);
35
+ const matches = updatedList.filter((node) => node.navType === 'NAV' &&
36
+ !beforeIds.has(node.navUuid) && getNodeName(node) === name &&
37
+ (node.parentNavUuid || rootNavUuid) === parent.navUuid);
38
+ if (matches.length !== 1) {
39
+ throw new Error('Created group could not be resolved uniquely.');
40
+ }
41
+ created = matches[0];
42
+ stage = 'icon';
43
+ await postNavAction(appType, 'update',
44
+ buildFormNavigationIconUpdatePayload(created, selection.icon), authRef, { oneShot: true });
45
+ stage = 'readback';
46
+ const observed = await fetchNavigationList(appType, authRef);
47
+ const verified = resolveNode(observed, created.navUuid, { groupOnly: true });
48
+ if (verified.icon !== selection.icon) {
49
+ throw new Error('Group navigation icon readback did not match the requested icon.');
50
+ }
51
+ return {
52
+ success: true, appType, action: 'create', group: normalizeNavNode(verified),
53
+ iconSource: selection.source, readbackVerified: true,
54
+ };
55
+ } catch (error) {
56
+ throw new CliError(error.message, {
57
+ code: 'NAV_GROUP_CREATE_INCOMPLETE',
58
+ details: {
59
+ appType, stage, name, parentNavUuid: parent.navUuid,
60
+ navUuid: created ? created.navUuid : null, expectedIcon: selection.icon,
61
+ partial: stage !== 'create', sideEffectState: created ? 'committed' : 'unknown',
62
+ retrySafe: false, readbackVerified: false, readbackAllowed: true,
63
+ nextStep: `openyida nav-group list ${appType} --flat`,
64
+ },
65
+ });
66
+ }
67
+ }
68
+
69
+ module.exports = { createGroupWithIcon };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const querystring = require('querystring');
4
+ const { createGroupWithIcon } = require('./nav-group-create');
4
5
  const {
5
6
  httpGet,
6
7
  httpPost,
@@ -44,6 +45,7 @@ function normalizeNavNode(node) {
44
45
  formUuid: node.formUuid || null,
45
46
  relateFormUuid: node.relateFormUuid || null,
46
47
  formType: node.formType || '',
48
+ icon: node.icon || null,
47
49
  hidden: node.hidden === 'y',
48
50
  mobileHidden: node.mobileHidden === 'y',
49
51
  listOrder: node.listOrder,
@@ -734,34 +736,12 @@ async function listGroups(appType, flags, authRef) {
734
736
  };
735
737
  }
736
738
 
739
+ /** Reuse the page icon catalog and preserve the created navigation node when setting its icon. */
737
740
  async function createGroup(appType, name, flags, authRef) {
738
- if (!name) {
739
- throw new Error('Group name is required');
740
- }
741
-
742
- const list = await fetchNavigationList(appType, authRef);
743
- const parent = resolveNode(list, flags.parent || ROOT_NAV_UUID, { groupOnly: true });
744
- if (parent.navUuid !== ROOT_NAV_UUID && parent.parentNavUuid !== ROOT_NAV_UUID) {
745
- throw new Error('New groups can only be created at root or under a top-level group');
746
- }
747
-
748
- const title = buildI18nTitle(name, flags.en);
749
- await postNavAction(appType, 'save', {
750
- parentNavUuid: parent.navUuid,
751
- title: JSON.stringify(title),
752
- }, authRef);
753
-
754
- const updatedList = await fetchNavigationList(appType, authRef);
755
- const created = updatedList
756
- .filter((node) => node.navType === 'NAV' && getNodeName(node) === name)
757
- .sort((a, b) => (b.gmtCreate || 0) - (a.gmtCreate || 0))[0];
758
-
759
- return {
760
- success: true,
761
- appType,
762
- action: 'create',
763
- group: created ? normalizeNavNode(created) : { name, parentNavUuid: parent.navUuid },
764
- };
741
+ return createGroupWithIcon(appType, name, flags, authRef, {
742
+ fetchNavigationList, postNavAction, resolveNode, buildI18nTitle,
743
+ getNodeName, normalizeNavNode, rootNavUuid: ROOT_NAV_UUID,
744
+ });
765
745
  }
766
746
 
767
747
  async function renameGroup(appType, value, name, flags, authRef) {
@@ -899,7 +879,7 @@ function printUsage() {
899
879
  console.error(`
900
880
  Usage:
901
881
  openyida nav-group list <appType> [--flat] [--include-system] [--raw]
902
- openyida nav-group create <appType> "<groupName>" [--parent <groupNavUuid|groupName>] [--en "<EnglishName>"]
882
+ openyida nav-group create <appType> "<groupName>" [--parent <groupNavUuid|groupName>] [--en "<EnglishName>"] [--icon auto|<iconName>]
903
883
  openyida nav-group rename <appType> <groupNavUuid|groupName> "<newName>" [--en "<EnglishName>"]
904
884
  openyida nav-group delete <appType> <groupNavUuid|groupName> [--force]
905
885
  openyida nav-group move <appType> <navUuid|formUuid|name> --to <groupNavUuid|groupName|root> [--before <node>] [--after <node>]
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { CliError } = require('../core/cli-error');
6
- const { REQUIRED_BRAND_SCALE_TOKENS, validateThemeCssContent, extractThemeColor } = require('./custom-theme');
6
+ const { validateThemeCssContent, extractThemeColor } = require('./custom-theme');
7
7
  const { parseDesignDocument, extractDesignTokens } = require('../design/document');
8
8
  const { normalizeThemeScopes } = require('./theme-scope');
9
9
  const DECLARATIONS = /^[ \t]*(--[\w-]+)\s*:\s*([^;]+);/gm;
@@ -25,23 +25,22 @@ function readDesignTokens(markdown) {
25
25
  }
26
26
 
27
27
  function tokensFromMetadata(metadata) {
28
- const tokens = extractDesignTokens(metadata, { strict: false });
29
- const missing = REQUIRED_BRAND_SCALE_TOKENS.filter(name => !tokens[name]);
30
- if (missing.length) {
31
- throw new CliError(`design.md 缺少品牌 token:${missing.join(', ')}`, { code: 'DESIGN_THEME_TOKENS_REQUIRED' });
32
- }
33
- return tokens;
28
+ // A design may override only part of the paired CSS. Validate the completed
29
+ // stylesheet after merging instead of requiring duplicate declarations here.
30
+ return extractDesignTokens(metadata, { strict: false });
34
31
  }
35
32
 
36
- function resolveDesignTokens(metadata) {
33
+ function resolveDesignTokens(metadata, options = {}) {
37
34
  const tokens = tokensFromMetadata(metadata);
35
+ if (options.paired) {return tokens;}
38
36
  for (const [alias, source] of Object.entries({
39
37
  '--color-brand-1': '--color-brand1-10', '--color-brand-2': '--color-brand1-1',
40
38
  '--color-brand-3': '--color-brand1-6', '--color-brand-4': '--color-brand1-9',
41
39
  })) {
42
- tokens[alias] = tokens[alias] || tokens[source];
40
+ if (tokens[source]) {tokens[alias] = tokens[alias] || tokens[source];}
43
41
  }
44
- tokens['--color-group'] = tokens['--color-group'] || [6, 1, 5, 2, 9, 3].map(n => tokens[`--color-brand1-${n}`]).join(', ');
42
+ const group = [6, 1, 5, 2, 9, 3].map(n => tokens[`--color-brand1-${n}`]);
43
+ if (group.every(Boolean)) {tokens['--color-group'] = tokens['--color-group'] || group.join(', ');}
45
44
  return tokens;
46
45
  }
47
46
 
@@ -108,10 +107,20 @@ function applyNavigationShape(css, tokens) {
108
107
  return block ? `${css.trimEnd()}\n\n${block}\n` : css;
109
108
  }
110
109
 
111
- function applyDesignTokens(template, markdown, previousMarkdown) {
110
+ // Template baselines can contain brand expressions; they are comparison data,
111
+ // never CSS output. Completed project tokens still use the normal validator.
112
+ function authoredTokens(node, result = {}) {
113
+ for (const [name, value] of Object.entries(node || {})) {
114
+ if (name.startsWith('--')) {result[name] = String(value);}
115
+ else if (value && typeof value === 'object') {authoredTokens(value, result);}
116
+ }
117
+ return result;
118
+ }
119
+
120
+ function applyDesignTokens(template, markdown, previousMarkdown, options = {}) {
112
121
  const metadata = designMetadata(markdown);
113
122
  const previousMetadata = previousMarkdown === markdown ? metadata : (previousMarkdown ? designMetadata(previousMarkdown) : null);
114
- const tokens = resolveDesignTokens(metadata);
123
+ const tokens = resolveDesignTokens(metadata, options);
115
124
  // Bundled CSS is an authoring template. Fill only unresolved declarations from
116
125
  // the completed design before validating and rewriting CSS scopes.
117
126
  template = template.replace(/^([ \t]*)(--[\w-]+)(\s*:\s*)([^;\n]+);/gm,
@@ -124,11 +133,14 @@ function applyDesignTokens(template, markdown, previousMarkdown) {
124
133
  });
125
134
  // Reject damaged templates or existing stylesheets before the scope rewrite.
126
135
  validateThemeCssContent(template);
127
- const previous = previousMetadata === metadata ? tokens : (previousMetadata ? resolveDesignTokens(previousMetadata) : null);
136
+ const previous = previousMetadata === metadata ? tokens : (previousMetadata
137
+ ? (options.paired ? authoredTokens(previousMetadata.tokens) : resolveDesignTokens(previousMetadata)) : null);
128
138
  navigationTone(metadata); // Validate platform metadata; it does not select a second palette.
129
- template = normalizeThemeScopes(template, navigationTone(previousMetadata || metadata));
130
- template = applyNavigationShape(template, tokens);
131
- if (previous) {template = restoreRemovedNavigation(template, tokens, previous);}
139
+ if (!options.paired) {
140
+ template = normalizeThemeScopes(template, navigationTone(previousMetadata || metadata));
141
+ template = applyNavigationShape(template, tokens);
142
+ if (previous) {template = restoreRemovedNavigation(template, tokens, previous);}
143
+ }
132
144
  const changed = Object.fromEntries(Object.entries(tokens).filter(([name, value]) => !previous || previous[name] !== value));
133
145
  if (!Object.keys(changed).length) {
134
146
  validateThemeCssContent(template);
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { CliError } = require('../core/cli-error');
6
+ const { t } = require('../core/i18n');
7
+ const { parseDesignDocument } = require('../design/document');
8
+ const { DESIGN_SKILL_ROOT, loadThemeIndex } = require('../design-plan/themes');
9
+ const { applyDesignTokens } = require('./theme-from-design');
10
+ const SOURCE = /^\/\* openyida-theme-source: ([a-z0-9-]+) \*\/\r?\n/m;
11
+
12
+ function sourceId(markdown) {
13
+ const metadata = parseDesignDocument(markdown, { legacyTokenValues: true }).metadata;
14
+ return metadata.themeProfile?.sourceThemeId ?? metadata.themeId;
15
+ }
16
+
17
+ // Resolve only catalog paths, never a project-supplied CSS path or theme directory.
18
+ function themeSource(markdown) {
19
+ const id = sourceId(markdown);
20
+ const theme = id && loadThemeIndex().themes.find(item => item.themeId === id);
21
+ if (id !== undefined && !theme) {
22
+ throw new CliError(t('design_document.invalid', 'themeProfile.sourceThemeId', 'APPLICATION_STYLE_UNKNOWN'), {
23
+ code: 'APPLICATION_STYLE_UNKNOWN', details: { themeId: id },
24
+ });
25
+ }
26
+ const relative = theme && theme.mode !== 'creative' ? theme.cssTemplatePath : 'references/theme/app-custom-theme-template.css';
27
+ const file = path.resolve(DESIGN_SKILL_ROOT, relative);
28
+ if (!file.startsWith(DESIGN_SKILL_ROOT + path.sep)) {throw new Error('Invalid theme CSS path');}
29
+ return { id: id || 'legacy', css: fs.readFileSync(file, 'utf8'), paired: theme?.mode === 'template',
30
+ design: theme?.mode === 'template' ? fs.readFileSync(path.resolve(DESIGN_SKILL_ROOT, theme.templatePath), 'utf8') : undefined };
31
+ }
32
+
33
+ function generateThemeCss(markdown, existingCss, previousMarkdown) {
34
+ const source = themeSource(markdown);
35
+ const marker = existingCss?.match(SOURCE)?.[1];
36
+ // An exported paired stylesheet is also a valid first-generation input.
37
+ // Unknown historical CSS cannot safely be relabeled as a different template.
38
+ const previousId = marker || (previousMarkdown && sourceId(previousMarkdown))
39
+ || (existingCss?.startsWith(source.css.trimEnd()) ? source.id : 'legacy');
40
+ if (existingCss !== undefined && previousId !== source.id) {
41
+ throw new CliError(t('theme_source.conflict'), {
42
+ code: 'THEME_SOURCE_CONFLICT', details: { previousThemeId: previousId, themeId: source.id },
43
+ });
44
+ }
45
+ const input = existingCss ?? source.css;
46
+ // The paired CSS wins when its design has a different authored default.
47
+ // Only project deltas and unresolved brand expressions are instantiated.
48
+ const css = applyDesignTokens(input, markdown, previousMarkdown ?? source.design, { paired: source.paired });
49
+ // Keep historical token-only files byte-compatible. Named and creative outputs
50
+ // retain identity even when the file is renamed or its token snapshot is lost.
51
+ return source.id === 'legacy' ? css : `/* openyida-theme-source: ${source.id} */\n${css.replace(SOURCE, '')}`;
52
+ }
53
+
54
+ module.exports = { generateThemeCss, sourceId, themeSource };
@@ -763,22 +763,11 @@ async function run(args) {
763
763
  const postData = querystring.stringify(postDataObj);
764
764
  return httpPost(
765
765
  auth.baseUrl,
766
- `/${params.appType}/query/app/updateAppName.json?_api=Form.updateAppName&_mock=false&_stamp=${Date.now()}`,
766
+ `/query/app/updateAppName.json?_api=Form.updateAppName&_mock=false&_stamp=${Date.now()}`,
767
767
  postData
768
768
  );
769
769
  }, authRef);
770
770
 
771
- // Verify the persisted resource before exposing success or a mutation receipt.
772
- let nameVerification;
773
- if (response && response.success && params.name) {
774
- const savedApp = await fetchCurrentApp(params.appType, authRef);
775
- const savedName = pickText(savedApp.appName, '');
776
- if (savedName !== params.name) {
777
- throwCommandError(t('update_app.name_not_persisted'), { code: 'APP_NAME_NOT_PERSISTED' });
778
- }
779
- nameVerification = { verified: true, name: savedName };
780
- }
781
-
782
771
  // 输出结果
783
772
  if (response && response.success) {
784
773
  const details = [['appType', params.appType]];
@@ -789,7 +778,6 @@ async function run(args) {
789
778
  success: true,
790
779
  appType: params.appType,
791
780
  themeVerification: response.themeVerification,
792
- nameVerification,
793
781
  navigationVerification: response.navigationVerification,
794
782
  ...(params.disableCustomTheme ? { runtimeVerification: { verified: false } } : {}),
795
783
  updatedFields: {