openyida 2026.9.2 → 2026.9.3

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 (80) hide show
  1. package/README.md +9 -7
  2. package/lib/app/create-form/api-path.js +16 -2
  3. package/lib/app/create-form/args.js +14 -0
  4. package/lib/app/create-form/nav-icon-service.js +202 -0
  5. package/lib/app/create-form/nav-icon.js +267 -0
  6. package/lib/app/create-form.js +77 -3
  7. package/lib/app/nav-group.js +221 -53
  8. package/lib/app/page-template-guard.js +64 -0
  9. package/lib/app/publish.js +15 -1
  10. package/lib/app/services/canvas-page-schema-builder.js +1 -1
  11. package/lib/asset/ai-image.js +8 -9
  12. package/lib/asset/asset-cmd.js +3 -49
  13. package/lib/asset/asset-resolve.js +8 -10
  14. package/lib/asset/asset-status.js +7 -7
  15. package/lib/core/agent-capabilities.js +43 -1
  16. package/lib/core/cli-error.js +6 -1
  17. package/lib/core/command-contract.js +42 -1
  18. package/lib/core/command-manifest.js +73 -11
  19. package/lib/core/locales/en.js +5 -1
  20. package/lib/core/locales/zh.js +7 -3
  21. package/lib/core/query-data.js +126 -8
  22. package/lib/core/sample.js +51 -10
  23. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +7 -7
  24. package/package.json +1 -1
  25. package/scripts/postinstall.js +5 -5
  26. package/yida-skills/SKILL.md +13 -13
  27. package/yida-skills/references/task-retrospective.md +4 -4
  28. package/yida-skills/skills/yida-app/SKILL.md +9 -8
  29. package/yida-skills/skills/yida-app/references/common-issues.md +1 -1
  30. package/yida-skills/skills/yida-app/workflow/step-2-design.md +42 -15
  31. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +8 -1
  32. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +13 -15
  33. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +4 -2
  34. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +26 -11
  35. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +22 -11
  36. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +8 -8
  37. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +1 -1
  38. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +16 -16
  39. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +1 -1
  40. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +1 -1
  41. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +6 -7
  42. package/yida-skills/skills/yida-canvas-table-form/SKILL.md +2 -2
  43. package/yida-skills/skills/yida-create-app/SKILL.md +13 -7
  44. package/yida-skills/skills/yida-create-form-page/SKILL.md +15 -17
  45. package/yida-skills/skills/yida-create-form-page/references/form-field-properties.md +1 -4
  46. package/yida-skills/skills/yida-custom-page/SKILL.md +5 -5
  47. package/yida-skills/skills/yida-custom-page/references/coding-guide.md +2 -6
  48. package/yida-skills/skills/yida-custom-page/references/design-system.md +4 -4
  49. package/yida-skills/skills/yida-data-management/SKILL.md +6 -4
  50. package/yida-skills/skills/yida-data-management/references/data-format-guide.md +13 -5
  51. package/yida-skills/skills/yida-design/SKILL.md +36 -39
  52. package/yida-skills/skills/yida-design/references/asset-workflow.md +32 -85
  53. package/yida-skills/skills/yida-design/references/page-quality-gates.md +3 -3
  54. package/yida-skills/skills/yida-design/references/style-design-selection.md +4 -4
  55. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +15 -24
  56. package/yida-skills/skills/yida-design/references/theme/theme-token-presets.md +8 -8
  57. package/yida-skills/skills/yida-design/references/visual-decision-engine.md +1 -1
  58. package/yida-skills/skills/yida-design/references/visual-scaffold-recipes.md +2 -2
  59. package/yida-skills/skills/yida-design/sub_skill/page-design/SKILL.md +12 -13
  60. package/yida-skills/skills/yida-design/workflow/output-design.md +40 -26
  61. package/yida-skills/skills/yida-design/workflow/output-prd.md +8 -8
  62. package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +14 -0
  63. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +23 -17
  64. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +3 -3
  65. package/yida-skills/skills/yida-design/workflow/step-5-visual-states.md +5 -5
  66. package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +22 -55
  67. package/yida-skills/skills/yida-nav-group/SKILL.md +3 -1
  68. package/yida-skills/skills/yida-page-config/SKILL.md +3 -1
  69. package/yida-skills/skills/yida-prd/SKILL.md +48 -0
  70. package/yida-skills/skills/yida-prd/references/app/blueprint.md +73 -0
  71. package/yida-skills/skills/yida-prd/references/app/navigation-patterns.md +43 -0
  72. package/yida-skills/skills/yida-prd/references/app/role-journey.md +30 -0
  73. package/yida-skills/skills/yida-prd/workflow/output-prd.md +188 -0
  74. package/yida-skills/skills/yida-prd/workflow/step-1-read-brief.md +15 -0
  75. package/yida-skills/skills/{yida-design/workflow/step-3-information-architecture.md → yida-prd/workflow/step-2-information-architecture.md} +2 -2
  76. package/yida-skills/skills/yida-publish-page/SKILL.md +1 -1
  77. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +49 -0
  78. package/yida-skills/skills-index.json +36 -35
  79. package/yida-skills/skills/yida-design/workflow/step-1-positioning.md +0 -58
  80. package/yida-skills/skills/yida-form-detail/SKILL.md +0 -68
@@ -82,6 +82,12 @@ const { createParseArgs } = require('./create-form/args');
82
82
  const { createDefinitionReaders } = require('./create-form/definition-reader');
83
83
  const { dispatchCreateFormCommand } = require('./create-form/commands');
84
84
  const { buildApiPath } = require('./create-form/api-path');
85
+ const {
86
+ FORM_NAV_ICON_GROUPS,
87
+ FORM_NAV_ICONS,
88
+ resolveFormNavIcon,
89
+ } = require('./create-form/nav-icon');
90
+ const { createFormNavIconService } = require('./create-form/nav-icon-service');
85
91
  const { deepMerge, splitJsonPointer } = require('./create-form/schema-patch');
86
92
  const {
87
93
  actionRefName,
@@ -4647,11 +4653,13 @@ function applyChangesToSchema(schema, changes, options) {
4647
4653
 
4648
4654
  // ── 发送 POST 请求(支持 302 自动重登录) ────────────
4649
4655
 
4650
- function sendPostRequest(baseUrl, requestPath, extraParams, formUuid, authRef) {
4656
+ function sendPostRequest(baseUrl, requestPath, extraParams, formUuid, authRef, requestOptions) {
4657
+ requestOptions = requestOptions || {};
4651
4658
  const authParams = authRef && authRef.csrfToken ? { _csrf_token: authRef.csrfToken } : {};
4652
4659
  const postData = querystring.stringify(Object.assign(authParams, extraParams));
4660
+ const refererAppType = requestOptions.appType || extraParams.appType || '';
4653
4661
  const referer = formUuid
4654
- ? `${baseUrl}/alibaba/web/${extraParams.appType || ''}/design/pageDesigner?formUuid=${formUuid}`
4662
+ ? requestOptions.referer || `${baseUrl}/alibaba/web/${refererAppType}/design/pageDesigner?formUuid=${formUuid}`
4655
4663
  : baseUrl + '/';
4656
4664
  return httpPost(baseUrl, requestPath, postData, { referer });
4657
4665
  }
@@ -4678,6 +4686,19 @@ function sendCreateFormRequest(authRef, appType, formTitle) {
4678
4686
  );
4679
4687
  }
4680
4688
 
4689
+ const {
4690
+ resolveFormNavigationNode,
4691
+ updateCreatedFormNavigationIcon,
4692
+ } = createFormNavIconService({
4693
+ buildApiPath,
4694
+ requestWithAutoLogin,
4695
+ sendGetRequest,
4696
+ sendPostRequest,
4697
+ throwCreateFormError,
4698
+ sanitizeFailureResult,
4699
+ candidateLimit: FIELD_RESOLUTION_CANDIDATE_LIMIT,
4700
+ });
4701
+
4681
4702
  // ── 登录态辅助:从 authData 中提取 corpId ──────────
4682
4703
 
4683
4704
  function resolveCorpId(authData) {
@@ -4853,6 +4874,25 @@ async function mainValidateFields(parsedArgs) {
4853
4874
  return output;
4854
4875
  }
4855
4876
 
4877
+ async function mainListIcons(parsedArgs) {
4878
+ const output = {
4879
+ success: true,
4880
+ count: FORM_NAV_ICONS.length,
4881
+ default: 'auto',
4882
+ groups: FORM_NAV_ICON_GROUPS.map(function (group) {
4883
+ return {
4884
+ key: group.key,
4885
+ label: group.label,
4886
+ icons: group.items.map(function (item) {
4887
+ return { name: item[0], label: item[1] };
4888
+ }),
4889
+ };
4890
+ }),
4891
+ };
4892
+ console.log(JSON.stringify(output, null, parsedArgs.json ? 2 : 0));
4893
+ return output;
4894
+ }
4895
+
4856
4896
  async function mainCreate(parsedArgs, authRef) {
4857
4897
  const { appType, formTitle, fieldsJsonOrFile, layout, theme, labelAlign } = parsedArgs;
4858
4898
 
@@ -4866,9 +4906,22 @@ async function mainCreate(parsedArgs, authRef) {
4866
4906
  const { fields, columns, validations } = readFieldsDefinition(fieldsJsonOrFile);
4867
4907
  assertNoEmojiInFormDefinition(formTitle, fields, validations);
4868
4908
  validateFormFieldDefinitions(fields);
4909
+ const iconResolution = resolveFormNavIcon(parsedArgs.icon, formTitle, fields);
4910
+ if (!iconResolution.icon) {
4911
+ throwCreateFormError(
4912
+ `Unsupported form navigation icon: ${parsedArgs.icon}. Run openyida create-form icons --json for the supported catalog.`,
4913
+ 'CREATE_FORM_NAV_ICON_INVALID',
4914
+ {
4915
+ icon: parsedArgs.icon,
4916
+ availableIconCount: FORM_NAV_ICONS.length,
4917
+ }
4918
+ );
4919
+ }
4920
+ const formIcon = iconResolution.icon;
4869
4921
  const fieldCount = countDataFieldDefinitions(fields);
4870
4922
  success(t('create_form.fields_loaded', fieldCount));
4871
4923
  label('Columns:', String(columns));
4924
+ label('Icon:', `${formIcon} (${iconResolution.source})`);
4872
4925
  fields.forEach(function (field, index) {
4873
4926
  listItem((index + 1) + '. ' + normalizeFormDefinitionType(field) + ': ' + getDefinitionDisplayName(field));
4874
4927
  });
@@ -4935,6 +4988,8 @@ async function mainCreate(parsedArgs, authRef) {
4935
4988
  formUuid,
4936
4989
  fieldCount,
4937
4990
  });
4991
+ postCreateStage = 'updateFormNavigationIcon';
4992
+ await updateCreatedFormNavigationIcon(authRef, appType, formUuid, formIcon);
4938
4993
  } catch (err) {
4939
4994
  emitCreateFormPostCreateFailure({
4940
4995
  appType,
@@ -4954,7 +5009,7 @@ async function mainCreate(parsedArgs, authRef) {
4954
5009
  ['URL', formUrl],
4955
5010
  ]);
4956
5011
  console.log(JSON.stringify(withBrowserHandoff(
4957
- { success: true, formUuid, formTitle, appType, fieldCount, url: formUrl },
5012
+ { success: true, formUuid, formTitle, appType, fieldCount, icon: formIcon, iconSource: iconResolution.source, url: formUrl },
4958
5013
  formUrl,
4959
5014
  { stage: 'create_form_success', title: formTitle },
4960
5015
  parsedArgs.browserOpenMode
@@ -4991,6 +5046,7 @@ async function createFormForLegacyProcessQuiet(context, input) {
4991
5046
  layout: input.layout || 'single',
4992
5047
  theme: input.theme || 'default',
4993
5048
  labelAlign: input.labelAlign || 'top',
5049
+ icon: input.icon || 'auto',
4994
5050
  contentLocale: input.contentLocale || null,
4995
5051
  browserOpenMode: 'off',
4996
5052
  };
@@ -5000,6 +5056,15 @@ async function createFormForLegacyProcessQuiet(context, input) {
5000
5056
  const { fields, validations } = readFieldsDefinition(fieldsJsonOrFile);
5001
5057
  assertNoEmojiInFormDefinition(formTitle, fields, validations, 'legacy create-form input');
5002
5058
  validateFormFieldDefinitions(fields);
5059
+ const iconResolution = resolveFormNavIcon(parsedArgs.icon, formTitle, fields);
5060
+ if (!iconResolution.icon) {
5061
+ throwCreateFormError(
5062
+ `Unsupported form navigation icon: ${parsedArgs.icon}. Run openyida create-form icons --json for the supported catalog.`,
5063
+ 'CREATE_FORM_NAV_ICON_INVALID',
5064
+ { icon: parsedArgs.icon, availableIconCount: FORM_NAV_ICONS.length }
5065
+ );
5066
+ }
5067
+ const formIcon = iconResolution.icon;
5003
5068
  const fieldCount = countDataFieldDefinitions(fields);
5004
5069
  const createResult = await sendCreateFormRequest(authRef, appType, formTitle);
5005
5070
 
@@ -5030,6 +5095,7 @@ async function createFormForLegacyProcessQuiet(context, input) {
5030
5095
  applySmartValidations(schema, createValidationRules);
5031
5096
  }
5032
5097
  const saveResult = await saveFormSchema(authRef, appType, formUuid, schema, serverRevision, 4);
5098
+ const navIconResult = await updateCreatedFormNavigationIcon(authRef, appType, formUuid, formIcon);
5033
5099
  return {
5034
5100
  success: true,
5035
5101
  appType,
@@ -5038,6 +5104,9 @@ async function createFormForLegacyProcessQuiet(context, input) {
5038
5104
  fieldCount,
5039
5105
  createResult,
5040
5106
  saveResult,
5107
+ navIconResult,
5108
+ icon: formIcon,
5109
+ iconSource: iconResolution.source,
5041
5110
  url: authRef.baseUrl + '/' + appType + '/workbench/' + formUuid,
5042
5111
  };
5043
5112
  }
@@ -6017,6 +6086,9 @@ async function run(args) {
6017
6086
  if (parsedArgs.mode === 'validate-fields') {
6018
6087
  return mainValidateFields(parsedArgs);
6019
6088
  }
6089
+ if (parsedArgs.mode === 'icons') {
6090
+ return mainListIcons(parsedArgs);
6091
+ }
6020
6092
 
6021
6093
  step(1, t('common.step_login', 1));
6022
6094
  const authRef = createAuthRef();
@@ -6058,6 +6130,8 @@ module.exports = {
6058
6130
  normalizeFormDefinitionType,
6059
6131
  validateFormFieldDefinitions,
6060
6132
  collectFormFieldValidationDiagnostics,
6133
+ resolveFormNavigationNode,
6134
+ updateCreatedFormNavigationIcon,
6061
6135
  applyChangesToSchema,
6062
6136
  applyFormRules,
6063
6137
  applySchemaPatchOperations,
@@ -7,6 +7,7 @@ const {
7
7
  requestWithAutoLogin,
8
8
  } = require('../core/utils');
9
9
  const { createAuthRef, isAuthRefReady } = require('../core/yida-client');
10
+ const { CliError } = require('../core/cli-error');
10
11
  const { parseYidaI18n } = require('../core/yida-i18n');
11
12
 
12
13
  const ROOT_NAV_UUID = 'NAV-SYSTEM-PARENT-UUID';
@@ -422,6 +423,92 @@ function autoOrderRootNodes(list) {
422
423
  };
423
424
  }
424
425
 
426
+ function snapshotNavigationRoots(roots) {
427
+ const snapshot = [];
428
+
429
+ const visit = (nodes, parentNavUuid) => {
430
+ (nodes || []).forEach((node, siblingIndex) => {
431
+ snapshot.push({
432
+ navUuid: node.navUuid,
433
+ parentNavUuid,
434
+ siblingIndex,
435
+ navType: node.navType || '',
436
+ });
437
+ visit(node.children || [], node.navUuid);
438
+ });
439
+ };
440
+
441
+ visit(roots || [], ROOT_NAV_UUID);
442
+ return snapshot;
443
+ }
444
+
445
+ function buildNavigationSnapshot(list) {
446
+ return snapshotNavigationRoots(buildMutableForest(list).roots);
447
+ }
448
+
449
+ function navigationSnapshotsEqual(left, right) {
450
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
451
+ return false;
452
+ }
453
+
454
+ return left.every((item, index) => {
455
+ const other = right[index];
456
+ return other &&
457
+ item.navUuid === other.navUuid &&
458
+ item.parentNavUuid === other.parentNavUuid &&
459
+ item.siblingIndex === other.siblingIndex &&
460
+ item.navType === other.navType;
461
+ });
462
+ }
463
+
464
+ function findFirstNavigationDifference(actual, expected) {
465
+ const actualItems = Array.isArray(actual) ? actual : [];
466
+ const expectedItems = Array.isArray(expected) ? expected : [];
467
+ const length = Math.max(actualItems.length, expectedItems.length);
468
+
469
+ for (let index = 0; index < length; index++) {
470
+ const actualItem = actualItems[index] || null;
471
+ const expectedItem = expectedItems[index] || null;
472
+ if (!actualItem || !expectedItem ||
473
+ actualItem.navUuid !== expectedItem.navUuid ||
474
+ actualItem.parentNavUuid !== expectedItem.parentNavUuid ||
475
+ actualItem.siblingIndex !== expectedItem.siblingIndex ||
476
+ actualItem.navType !== expectedItem.navType) {
477
+ return {
478
+ index,
479
+ actual: actualItem,
480
+ expected: expectedItem,
481
+ };
482
+ }
483
+ }
484
+
485
+ return null;
486
+ }
487
+
488
+ function summarizeNavigationOrderDiff(code, beforeOrder, expectedOrder, observedOrder) {
489
+ const hasObservedOrder = Array.isArray(observedOrder);
490
+ return {
491
+ beforeCount: Array.isArray(beforeOrder) ? beforeOrder.length : 0,
492
+ expectedCount: Array.isArray(expectedOrder) ? expectedOrder.length : 0,
493
+ observedCount: hasObservedOrder ? observedOrder.length : null,
494
+ firstPlannedChange: findFirstNavigationDifference(beforeOrder, expectedOrder),
495
+ ...(code === 'NAV_ORDER_READBACK_MISMATCH' && hasObservedOrder
496
+ ? { firstReadbackMismatch: findFirstNavigationDifference(observedOrder, expectedOrder) }
497
+ : {}),
498
+ };
499
+ }
500
+
501
+ function buildNavigationOrderPlan(list, ordered) {
502
+ const beforeOrder = buildNavigationSnapshot(list);
503
+ const expectedOrder = snapshotNavigationRoots(ordered.roots);
504
+ return {
505
+ beforeOrder,
506
+ expectedOrder,
507
+ targetNavUuids: ordered.orderedNavUuids.slice(),
508
+ changed: !navigationSnapshotsEqual(beforeOrder, expectedOrder),
509
+ };
510
+ }
511
+
425
512
  function countChildren(list, navUuid) {
426
513
  return (list || []).filter((node) => node.parentNavUuid === navUuid).length;
427
514
  }
@@ -455,7 +542,7 @@ async function fetchNavigationList(appType, authRef) {
455
542
  return Array.isArray(result.content) ? result.content : [];
456
543
  }
457
544
 
458
- async function postNavAction(appType, action, payload, authRef) {
545
+ async function postNavAction(appType, action, payload, authRef, options = {}) {
459
546
  const endpoints = {
460
547
  save: 'saveFormNavigation.json',
461
548
  update: 'updateFormNavigation.json',
@@ -475,21 +562,143 @@ async function postNavAction(appType, action, payload, authRef) {
475
562
  throw new Error(`Unknown nav action: ${action}`);
476
563
  }
477
564
 
478
- const result = await requestWithAutoLogin((auth) => {
565
+ const requestFn = (auth) => {
479
566
  const path = `/dingtalk/web/${appType}/query/formnav/${endpoint}` +
480
567
  `?_api=${encodeURIComponent(apiNames[action])}` +
481
568
  '&_mock=false' +
482
569
  `&_stamp=${Date.now()}`;
483
570
  return httpPost(auth.baseUrl, path, querystring.stringify(payload));
484
- }, authRef);
571
+ };
572
+ const result = options.oneShot === true
573
+ ? await requestFn(authRef)
574
+ : await requestWithAutoLogin(requestFn, authRef);
485
575
 
486
- if (!result || result.success === false) {
576
+ if (!result || result.success === false || result.__needLogin || result.__csrfExpired) {
487
577
  throw new Error(result ? result.errorMsg || `Nav action failed: ${action}` : `Nav action failed: ${action}`);
488
578
  }
489
579
 
490
580
  return result;
491
581
  }
492
582
 
583
+ function buildNavigationOrderSuccess(appType, action, ordered, observedList, plan, recoveredByReadback) {
584
+ const orderedNodes = ordered.orderedNavUuids.map((navUuid) => normalizeNavNode(
585
+ resolveNode(observedList, navUuid, { excludeSystem: true })
586
+ ));
587
+ return {
588
+ success: true,
589
+ appType,
590
+ action,
591
+ targetParentNavUuid: ROOT_NAV_UUID,
592
+ changed: plan.changed,
593
+ alreadyApplied: !plan.changed,
594
+ mutationPerformed: plan.changed,
595
+ readbackVerified: true,
596
+ recoveredByReadback: recoveredByReadback === true,
597
+ sideEffectState: plan.changed ? 'committed' : 'none',
598
+ orderedNavUuids: ordered.orderedNavUuids,
599
+ orderedNodes,
600
+ skipped: !plan.changed,
601
+ };
602
+ }
603
+
604
+ function buildNavigationOrderError(code, appType, action, plan, observedOrder, cause) {
605
+ const sideEffectState = code === 'NAV_ORDER_NOT_APPLIED' ? 'none' : 'unknown';
606
+ const retrySafe = code === 'NAV_ORDER_NOT_APPLIED';
607
+ const readbackVerified = code === 'NAV_ORDER_NOT_APPLIED';
608
+ const status = code === 'NAV_ORDER_NOT_APPLIED'
609
+ ? 'NOT_APPLIED'
610
+ : code === 'NAV_ORDER_READBACK_MISMATCH'
611
+ ? 'SEMANTIC_FAILURE'
612
+ : 'RESULT_UNKNOWN';
613
+ return new CliError(code, {
614
+ code,
615
+ details: {
616
+ target: {
617
+ type: 'navigationOrder',
618
+ appType,
619
+ action,
620
+ navUuids: plan.targetNavUuids,
621
+ },
622
+ changed: plan.changed,
623
+ ...(code === 'NAV_ORDER_NOT_APPLIED' ? { mutationPerformed: false } : {}),
624
+ readbackVerified,
625
+ retryable: retrySafe,
626
+ retrySafe,
627
+ sideEffectState,
628
+ status,
629
+ readbackAllowed: true,
630
+ orderDiff: summarizeNavigationOrderDiff(
631
+ code,
632
+ plan.beforeOrder,
633
+ plan.expectedOrder,
634
+ observedOrder
635
+ ),
636
+ cause: cause && cause.message ? cause.message : undefined,
637
+ nextStep: `openyida nav-group list ${appType} --flat`,
638
+ },
639
+ });
640
+ }
641
+
642
+ async function applyNavigationOrder(appType, action, list, ordered, authRef, dependencies = {}) {
643
+ const fetchList = dependencies.fetchNavigationList || fetchNavigationList;
644
+ const postAction = dependencies.postNavAction || postNavAction;
645
+ const plan = buildNavigationOrderPlan(list, ordered);
646
+
647
+ if (!plan.changed) {
648
+ return buildNavigationOrderSuccess(appType, action, ordered, list, plan, false);
649
+ }
650
+
651
+ const firstNode = resolveNode(list, ordered.orderedNavUuids[0], { excludeSystem: true });
652
+ let mutationError = null;
653
+ try {
654
+ await postAction(appType, 'updateOrderNew', {
655
+ currentId: firstNode.id,
656
+ parentNavUuid: ROOT_NAV_UUID,
657
+ navType: firstNode.navType,
658
+ ids: ordered.ids.join(','),
659
+ }, authRef, { oneShot: true });
660
+ } catch (error) {
661
+ mutationError = error;
662
+ }
663
+
664
+ let updatedList;
665
+ try {
666
+ updatedList = await fetchList(appType, authRef);
667
+ } catch (readbackError) {
668
+ throw buildNavigationOrderError(
669
+ 'NAV_ORDER_RESULT_UNKNOWN',
670
+ appType,
671
+ action,
672
+ plan,
673
+ null,
674
+ readbackError
675
+ );
676
+ }
677
+
678
+ const observedOrder = buildNavigationSnapshot(updatedList);
679
+ if (navigationSnapshotsEqual(observedOrder, plan.expectedOrder)) {
680
+ return buildNavigationOrderSuccess(appType, action, ordered, updatedList, plan, !!mutationError);
681
+ }
682
+ if (navigationSnapshotsEqual(observedOrder, plan.beforeOrder)) {
683
+ throw buildNavigationOrderError(
684
+ 'NAV_ORDER_NOT_APPLIED',
685
+ appType,
686
+ action,
687
+ plan,
688
+ observedOrder,
689
+ mutationError
690
+ );
691
+ }
692
+ throw buildNavigationOrderError(
693
+ 'NAV_ORDER_READBACK_MISMATCH',
694
+ appType,
695
+ action,
696
+ plan,
697
+ observedOrder,
698
+ mutationError
699
+ );
700
+ }
701
+
493
702
  function normalizeListOutput(list, options = {}) {
494
703
  const includeSystem = options.includeSystem === true;
495
704
  return (list || [])
@@ -647,62 +856,17 @@ async function orderRootNodes(appType, values, authRef) {
647
856
 
648
857
  const list = await fetchNavigationList(appType, authRef);
649
858
  const ordered = reorderRootNodes(list, values);
650
- const firstNode = resolveNode(list, ordered.orderedNavUuids[0], { excludeSystem: true });
651
-
652
- await postNavAction(appType, 'updateOrderNew', {
653
- currentId: firstNode.id,
654
- parentNavUuid: ROOT_NAV_UUID,
655
- navType: firstNode.navType,
656
- ids: ordered.ids.join(','),
657
- }, authRef);
658
-
659
- const updatedList = await fetchNavigationList(appType, authRef);
660
- const orderedNodes = ordered.orderedNavUuids.map((navUuid) => normalizeNavNode(
661
- resolveNode(updatedList, navUuid, { excludeSystem: true })
662
- ));
663
- return {
664
- success: true,
665
- appType,
666
- action: 'order',
667
- targetParentNavUuid: ROOT_NAV_UUID,
668
- orderedNavUuids: ordered.orderedNavUuids,
669
- orderedNodes,
670
- };
859
+ return applyNavigationOrder(appType, 'order', list, ordered, authRef);
671
860
  }
672
861
 
673
862
  async function autoOrderNavigation(appType, authRef) {
674
863
  const list = await fetchNavigationList(appType, authRef);
675
864
  const ordered = autoOrderRootNodes(list);
676
865
  if (!ordered.orderedNavUuids.length) {
677
- return {
678
- success: true,
679
- appType,
680
- action: 'auto-order',
681
- targetParentNavUuid: ROOT_NAV_UUID,
682
- orderedNavUuids: [],
683
- orderedNodes: [],
684
- skipped: true,
685
- };
866
+ const plan = buildNavigationOrderPlan(list, ordered);
867
+ return buildNavigationOrderSuccess(appType, 'auto-order', ordered, list, plan, false);
686
868
  }
687
-
688
- const firstNode = resolveNode(list, ordered.orderedNavUuids[0], { excludeSystem: true });
689
- await postNavAction(appType, 'updateOrderNew', {
690
- currentId: firstNode.id,
691
- parentNavUuid: ROOT_NAV_UUID,
692
- navType: firstNode.navType,
693
- ids: ordered.ids.join(','),
694
- }, authRef);
695
-
696
- const updatedList = await fetchNavigationList(appType, authRef);
697
- const updatedOrdered = autoOrderRootNodes(updatedList);
698
- return {
699
- success: true,
700
- appType,
701
- action: 'auto-order',
702
- targetParentNavUuid: ROOT_NAV_UUID,
703
- orderedNavUuids: updatedOrdered.orderedNavUuids,
704
- orderedNodes: updatedOrdered.orderedNodes,
705
- };
869
+ return applyNavigationOrder(appType, 'auto-order', list, ordered, authRef);
706
870
  }
707
871
 
708
872
  async function setVisibility(appType, value, hidden, authRef) {
@@ -790,8 +954,11 @@ async function run(args) {
790
954
 
791
955
  module.exports = {
792
956
  ROOT_NAV_UUID,
957
+ applyNavigationOrder,
793
958
  autoOrderNavigation,
794
959
  autoOrderRootNodes,
960
+ buildNavigationOrderPlan,
961
+ buildNavigationSnapshot,
795
962
  buildI18nTitle,
796
963
  buildNavigationTree,
797
964
  compareNavigationNodes,
@@ -800,6 +967,7 @@ module.exports = {
800
967
  getNavigationPriority,
801
968
  moveNodeInTree,
802
969
  normalizeNavNode,
970
+ navigationSnapshotsEqual,
803
971
  orderRootNodes,
804
972
  parseArgs,
805
973
  reorderRootNodes,
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const { CliError } = require('../core/cli-error');
4
+
5
+ const TEMPLATE_RESIDUE_PATTERNS = [
6
+ {
7
+ pattern: /@openyida-page-template-base/,
8
+ residue: 'template-marker',
9
+ },
10
+ {
11
+ pattern: /\bSAMPLE_ROWS\b/,
12
+ residue: 'sample-data',
13
+ },
14
+ {
15
+ pattern: /\{\{(?:APP_TYPE|FORM_UUID)\}\}/,
16
+ residue: 'template-variable',
17
+ },
18
+ ];
19
+
20
+ function findPageTemplateResidues(sourceCode) {
21
+ const source = String(sourceCode || '');
22
+ const lines = source.split('\n');
23
+ const issues = [];
24
+
25
+ TEMPLATE_RESIDUE_PATTERNS.forEach(({ pattern, residue }) => {
26
+ lines.forEach((line, index) => {
27
+ if (pattern.test(line)) {
28
+ issues.push({
29
+ line: index + 1,
30
+ residue,
31
+ });
32
+ }
33
+ });
34
+ });
35
+
36
+ return issues;
37
+ }
38
+
39
+ function assertPageTemplateCustomized(sourceCode, sourcePath) {
40
+ const issues = findPageTemplateResidues(sourceCode);
41
+ if (issues.length === 0) {
42
+ return;
43
+ }
44
+
45
+ throw new CliError('内置页面模板尚未完成业务化改写:请先替换示例数据、模板变量和占位文案,再发布。', {
46
+ code: 'OPENYIDA_PAGE_TEMPLATE_NOT_CUSTOMIZED',
47
+ details: {
48
+ sourcePath,
49
+ issues,
50
+ retryable: false,
51
+ retrySafe: true,
52
+ sideEffectState: 'none',
53
+ nextAction: {
54
+ type: 'edit_source_then_publish',
55
+ sourcePath,
56
+ },
57
+ },
58
+ });
59
+ }
60
+
61
+ module.exports = {
62
+ assertPageTemplateCustomized,
63
+ findPageTemplateResidues,
64
+ };
@@ -28,12 +28,13 @@ const { createAuthRef, isTokenAuthRef } = require('../core/yida-client');
28
28
  const { requireSchemaServerRevision } = require('../core/server-revision');
29
29
  const { t } = require('../core/i18n');
30
30
  const { banner, step, label, success, fail, warn, info, error, result, usage, hint } = require('../core/chalk');
31
- const { CliError } = require('../core/cli-error');
31
+ const { CliError, isCliError, toErrorPayload } = require('../core/cli-error');
32
32
  const { compileSource } = require('./page-compiler');
33
33
  const { compileCanvas } = require('./canvas-compile');
34
34
  const { runLintCheck } = require('./page-linter');
35
35
  const { buildPageFile, shouldBuildPageSource } = require('./page-compat');
36
36
  const { warnLargePageSource } = require('./page-size-guard');
37
+ const { assertPageTemplateCustomized } = require('./page-template-guard');
37
38
  const { fetchFormPageList } = require('./form-navigation');
38
39
  const { parseOpenOption, withBrowserHandoff } = require('../core/browser-handoff');
39
40
  const { assertNoEmojiInValue } = require('../core/no-emoji-guard');
@@ -540,6 +541,7 @@ async function runMain(argv) {
540
541
  sourcePath = initialSource.absolutePath;
541
542
  const initialSourceCode = initialSource.source;
542
543
  warnLargePageSource(initialSourceCode, sourcePath);
544
+ assertPageTemplateCustomized(initialSourceCode, sourcePath);
543
545
 
544
546
  let sourceCode = initialSourceCode;
545
547
  let compiledCode = '';
@@ -723,6 +725,18 @@ async function runMain(argv) {
723
725
  success(t('publish.nav_order_ok', orderedNames || '-'));
724
726
  } catch (navOrderError) {
725
727
  navOrderWarning = navOrderError && navOrderError.message ? navOrderError.message : t('common.unknown_error');
728
+ navOrderResult = isCliError(navOrderError)
729
+ ? toErrorPayload(navOrderError)
730
+ : {
731
+ success: false,
732
+ errorCode: 'NAV_ORDER_RESULT_UNKNOWN',
733
+ errorMsg: navOrderWarning,
734
+ retryable: false,
735
+ retrySafe: false,
736
+ sideEffectState: 'unknown',
737
+ readbackVerified: false,
738
+ nextStep: `openyida nav-group list ${appType} --flat`,
739
+ };
726
740
  warn(t('publish.nav_order_failed', navOrderWarning));
727
741
  }
728
742
  }
@@ -361,7 +361,7 @@ function buildCanvasPageSchemaObject(sourceCode, runtimeCode, importedModules, f
361
361
  contentBgColorMobile: 'var(--pod-page-bg-color, var(--color-white, #fff))',
362
362
  },
363
363
  condition: true,
364
- css: 'body{background-color:var(--pod-page-bg-color,var(--color-white,#fff))}.vc-page-yida-page{--yida-form-content-padding:0;--yida-form-content-margin:0;--yida-layout-padding:0}.vc-deep-container-entry.vc-rootcontent{padding:0!important;margin-top:0!important;margin-right:0!important;margin-bottom:0!important;margin-left:0!important}',
364
+ css: '.vc-page-yida-page{--yida-form-content-padding:0;--yida-form-content-margin:0;--yida-layout-padding:0}.vc-deep-container-entry.vc-rootcontent{padding:0!important;margin-top:0!important;margin-right:0!important;margin-bottom:0!important;margin-left:0!important}',
365
365
  methods: {
366
366
  __initMethods__: {
367
367
  type: 'js',
@@ -11,8 +11,8 @@
11
11
  * 由 skill 文档引导它:能自己生成就生成,否则去免费素材库搜真实图片;
12
12
  * 3. 提供推荐的免费/可商用素材库清单与使用纪律,供 skill / agent 参考。
13
13
  *
14
- * 无论走哪条路,产出的候选 URL 都必须先经过 url-verify 校验真实可达,
15
- * 再(有 CDN 时)转存 CDN,绝不把未经校验或编造的 URL 直接写进页面。
14
+ * 无论走哪条路,候选素材都交给 asset resolve 统一解析;有 CDN 时再转存,
15
+ * 绝不把编造的 URL 直接写进页面。
16
16
  *
17
17
  * 用法:
18
18
  * const { detectImageGenerator, getFreeStockLibraries, getMaterialSourcingGuidance } = require('./ai-image');
@@ -25,7 +25,7 @@
25
25
  * 推荐的免费 / 可商用图片素材库
26
26
  * 说明:这些站点提供免费可商用(多为 CC0 / 自有免费授权)的高质量图片,
27
27
  * 适合官网 Hero、产品图、场景配图。agent 应优先从这里取真实图片 URL,
28
- * 取到后必须用 `openyida asset verify-url` 校验可达且确为图片。
28
+ * 取到后交给 `openyida asset resolve` 统一处理。
29
29
  */
30
30
  const FREE_STOCK_LIBRARIES = [
31
31
  {
@@ -34,7 +34,7 @@ const FREE_STOCK_LIBRARIES = [
34
34
  license: 'Unsplash License(免费可商用,无需署名)',
35
35
  bestFor: '高质量摄影级 Hero 大图、生活方式/场景图',
36
36
  directImageHint:
37
- 'images.unsplash.com/photo-... 形式的直链可直接用于 <img>;页面链接需先取到直链再校验。',
37
+ 'images.unsplash.com/photo-... 形式的直链可直接用于 <img>;页面链接需先取得图片直链。',
38
38
  },
39
39
  {
40
40
  name: 'Pexels',
@@ -82,7 +82,7 @@ function detectImageGenerator(options = {}) {
82
82
  reason:
83
83
  'openyida 无内置文生图能力(`openyida ai` 仅支持文生文/识图)。' +
84
84
  '图片素材应由智能体自行生成,或到免费素材库(Unsplash/Pexels 等)检索真实图片,' +
85
- '再经 `openyida asset verify-url` 校验、(有 CDN 时)转存 CDN 后回填 spec。',
85
+ '再交给 `openyida asset resolve` 解析、(有 CDN 时)转存 CDN 后回填 spec。',
86
86
  delegateToAgent: true,
87
87
  };
88
88
  }
@@ -105,13 +105,12 @@ function getMaterialSourcingGuidance() {
105
105
  '1. 判断页面是否需要真实图片(官网 Hero / 产品图 / 场景图通常需要)。',
106
106
  '2. 优先:若智能体自身具备图片生成能力,按品牌调性生成图片并保存到本地。',
107
107
  '3. 备选:到免费可商用素材库(见 libraries)检索与主题匹配的真实图片,拿到图片直链。',
108
- '4. 校验:对每个候选 URL 运行 `openyida asset verify-url <url>`,只保留 ok=true 的。',
109
- '5. 转存:若已配置 CDN(`openyida asset status` 显示 cdn-upload),用 `openyida asset resolve --upload-assets` 把本地/外链图片转存为稳定 CDN URL 并回填 spec.assets。',
110
- '6. 兜底:若既无生成能力又找不到可校验素材,交付「标注素材缺口」的低保真草稿,绝不编造图片 URL 或声称已上传。',
108
+ '4. 解析:把本地文件或外链交给 `openyida asset resolve` 统一处理并回填 spec.assets;已配置 CDN 时转存为稳定 URL。',
109
+ '5. 兜底:若既无生成能力又找不到可用素材,交付「标注素材缺口」的低保真草稿,绝不编造图片 URL 或声称已上传。',
111
110
  ],
112
111
  libraries: getFreeStockLibraries(),
113
112
  rules: [
114
- '绝不编造图片 URL;未经 verify-url 校验的 URL 不得写进页面。',
113
+ '绝不编造图片 URL;素材经 `openyida asset resolve` 统一处理后再写进页面。',
115
114
  '无 CDN 时不得声称「已上传」或「已是最终版」。',
116
115
  '素材授权需为免费可商用;如不确定授权,改用文字排版/数据图示/插画(如 unDraw)替代。',
117
116
  ],