openxiangda-devkit-core 2.0.0-alpha.98 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -9
  3. package/dist/app-spec.d.ts +30 -12
  4. package/dist/app-spec.d.ts.map +1 -1
  5. package/dist/app-spec.js +238 -57
  6. package/dist/app-spec.js.map +1 -1
  7. package/dist/application-services.d.ts +625 -42
  8. package/dist/application-services.d.ts.map +1 -1
  9. package/dist/application-services.js +321 -85
  10. package/dist/application-services.js.map +1 -1
  11. package/dist/application-ui-contract.d.ts +5 -0
  12. package/dist/application-ui-contract.d.ts.map +1 -0
  13. package/dist/application-ui-contract.js +149 -0
  14. package/dist/application-ui-contract.js.map +1 -0
  15. package/dist/backend-image-build.js +6 -11
  16. package/dist/backend-image-build.js.map +1 -1
  17. package/dist/command-process.d.ts +14 -0
  18. package/dist/command-process.d.ts.map +1 -0
  19. package/dist/command-process.js +51 -0
  20. package/dist/command-process.js.map +1 -0
  21. package/dist/command-registry.d.ts +44 -4
  22. package/dist/command-registry.d.ts.map +1 -1
  23. package/dist/command-registry.js +10 -6
  24. package/dist/command-registry.js.map +1 -1
  25. package/dist/compiler/ai-catalog.d.ts.map +1 -1
  26. package/dist/compiler/ai-catalog.js +2 -1
  27. package/dist/compiler/ai-catalog.js.map +1 -1
  28. package/dist/compiler/application-model.d.ts +60 -0
  29. package/dist/compiler/application-model.d.ts.map +1 -0
  30. package/dist/compiler/application-model.js +141 -0
  31. package/dist/compiler/application-model.js.map +1 -0
  32. package/dist/compiler/bundle.d.ts.map +1 -1
  33. package/dist/compiler/bundle.js +196 -98
  34. package/dist/compiler/bundle.js.map +1 -1
  35. package/dist/compiler/config.d.ts +36 -10
  36. package/dist/compiler/config.d.ts.map +1 -1
  37. package/dist/compiler/config.js +536 -171
  38. package/dist/compiler/config.js.map +1 -1
  39. package/dist/compiler/field-surface.js +1 -1
  40. package/dist/compiler/field-surface.js.map +1 -1
  41. package/dist/compiler/package-compiler.d.ts.map +1 -1
  42. package/dist/compiler/package-compiler.js +12 -0
  43. package/dist/compiler/package-compiler.js.map +1 -1
  44. package/dist/compiler/permission-review.d.ts +139 -0
  45. package/dist/compiler/permission-review.d.ts.map +1 -0
  46. package/dist/compiler/permission-review.js +131 -0
  47. package/dist/compiler/permission-review.js.map +1 -0
  48. package/dist/configuration-preflight.d.ts +7 -0
  49. package/dist/configuration-preflight.d.ts.map +1 -0
  50. package/dist/configuration-preflight.js +13 -0
  51. package/dist/configuration-preflight.js.map +1 -0
  52. package/dist/control-plane-client.d.ts +56 -61
  53. package/dist/control-plane-client.d.ts.map +1 -1
  54. package/dist/control-plane-client.js +89 -8
  55. package/dist/control-plane-client.js.map +1 -1
  56. package/dist/delivery-cache.d.ts +34 -0
  57. package/dist/delivery-cache.d.ts.map +1 -0
  58. package/dist/delivery-cache.js +186 -0
  59. package/dist/delivery-cache.js.map +1 -0
  60. package/dist/delivery-source.d.ts +17 -0
  61. package/dist/delivery-source.d.ts.map +1 -0
  62. package/dist/delivery-source.js +85 -0
  63. package/dist/delivery-source.js.map +1 -0
  64. package/dist/deployment-observer.d.ts +14 -0
  65. package/dist/deployment-observer.d.ts.map +1 -0
  66. package/dist/deployment-observer.js +68 -0
  67. package/dist/deployment-observer.js.map +1 -0
  68. package/dist/deployment.d.ts +17 -1
  69. package/dist/deployment.d.ts.map +1 -1
  70. package/dist/deployment.js +55 -15
  71. package/dist/deployment.js.map +1 -1
  72. package/dist/design-readiness.d.ts +24 -0
  73. package/dist/design-readiness.d.ts.map +1 -0
  74. package/dist/design-readiness.js +102 -0
  75. package/dist/design-readiness.js.map +1 -0
  76. package/dist/developer-errors.d.ts +11 -0
  77. package/dist/developer-errors.d.ts.map +1 -0
  78. package/dist/developer-errors.js +16 -0
  79. package/dist/developer-errors.js.map +1 -0
  80. package/dist/developer-operations.d.ts +190 -0
  81. package/dist/developer-operations.d.ts.map +1 -0
  82. package/dist/developer-operations.js +65 -0
  83. package/dist/developer-operations.js.map +1 -0
  84. package/dist/development-lifecycle.d.ts +75 -0
  85. package/dist/development-lifecycle.d.ts.map +1 -0
  86. package/dist/development-lifecycle.js +189 -0
  87. package/dist/development-lifecycle.js.map +1 -0
  88. package/dist/documentation.d.ts +111 -0
  89. package/dist/documentation.d.ts.map +1 -0
  90. package/dist/documentation.js +112 -0
  91. package/dist/documentation.js.map +1 -0
  92. package/dist/index.d.ts +10 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +10 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/operation-progress.d.ts +26 -0
  97. package/dist/operation-progress.d.ts.map +1 -0
  98. package/dist/operation-progress.js +92 -0
  99. package/dist/operation-progress.js.map +1 -0
  100. package/dist/optional-backend.d.ts +14 -0
  101. package/dist/optional-backend.d.ts.map +1 -0
  102. package/dist/optional-backend.js +145 -0
  103. package/dist/optional-backend.js.map +1 -0
  104. package/dist/session.d.ts +22 -0
  105. package/dist/session.d.ts.map +1 -1
  106. package/dist/session.js +36 -0
  107. package/dist/session.js.map +1 -1
  108. package/dist/workspace-loader.d.ts.map +1 -1
  109. package/dist/workspace-loader.js +17 -2
  110. package/dist/workspace-loader.js.map +1 -1
  111. package/dist/workspace-operation.d.ts +3 -0
  112. package/dist/workspace-operation.d.ts.map +1 -0
  113. package/dist/workspace-operation.js +45 -0
  114. package/dist/workspace-operation.js.map +1 -0
  115. package/dist/workspace.d.ts.map +1 -1
  116. package/dist/workspace.js +1 -0
  117. package/dist/workspace.js.map +1 -1
  118. package/package.json +18 -4
  119. package/templates/backend/Dockerfile +20 -0
  120. package/templates/backend/package.json +27 -0
  121. package/templates/backend/src/app.module.ts +23 -0
  122. package/templates/backend/src/main.ts +5 -0
  123. package/templates/backend/tsconfig.json +9 -0
@@ -1,3 +1,5 @@
1
+ import { materializeApplicationModules } from './application-model.js';
2
+ import { nativeFieldRequiresCreateInputV2 } from 'openxiangda-contracts/native-compiler';
1
3
  import { SCHEMA_VERSIONS, DATA_FIELD_TYPES, PLATFORM_EVENT_TYPES_V2, nativePlatformCapabilityCatalog, validateDataResource, WORKFLOW_SUMMARY_MAX_FIELDS, WORKFLOW_SUMMARY_TEXT_LONG_MAX_BYTES, isWorkflowSummaryFieldType, } from 'openxiangda-contracts';
2
4
  import { Ajv2020 } from 'ajv/dist/2020.js';
3
5
  import { CronExpressionParser } from 'cron-parser';
@@ -33,20 +35,58 @@ export function defineAdminNavigation(groups) {
33
35
  export function adminNavigationGroup(code, label, items, options = {}) {
34
36
  return Object.freeze({ code, label, items: Object.freeze([...items]), ...options });
35
37
  }
36
- export function adminResourcePage(resourceCode, options) {
37
- return adminNavigationItem({ kind: 'resource', resourceCode }, options);
38
+ export function adminResourcePage(resourceCode, options = {}) {
39
+ const { viewCode, ...display } = options;
40
+ return adminNavigationItem({ kind: 'resource', resourceCode, ...(viewCode ? { viewCode } : {}) }, display);
38
41
  }
39
42
  export function adminOperationPage(routeCode, options) {
40
43
  return adminNavigationItem({ kind: 'operation', routeCode }, options);
41
44
  }
45
+ /**
46
+ * @deprecated The Todo Center is a user Surface. Existing declarations are
47
+ * normalized to `frontend.user.applicationTodoCenter = true` and are not
48
+ * retained in the admin navigation contract.
49
+ */
42
50
  export function adminApplicationTodoCenterPage(options) {
43
- return adminNavigationItem({ kind: 'application-todo-center' }, options);
51
+ return Object.freeze({
52
+ page: Object.freeze({ kind: 'application-todo-center' }),
53
+ ...options,
54
+ });
44
55
  }
45
- export function adminWorkflowWorkCenterPage(options) {
46
- return adminNavigationItem({ kind: 'workflow-work-center' }, options);
56
+ function isLegacyApplicationTodoCenterItem(item) {
57
+ return (item.page.kind ===
58
+ 'application-todo-center');
47
59
  }
48
- export function adminWorkflowLaunchPage(workflowCode, options) {
49
- return adminNavigationItem({ kind: 'workflow-launch', workflowCode }, options);
60
+ function normalizeLegacyUserSurfaceAuthoring(declaration) {
61
+ const admin = declaration.frontend.admin;
62
+ const navigation = admin?.navigation;
63
+ if (!navigation?.some(group => group.items.some(isLegacyApplicationTodoCenterItem))) {
64
+ return declaration;
65
+ }
66
+ const normalizedNavigation = navigation
67
+ .map(group => ({
68
+ ...group,
69
+ items: group.items.filter(item => !isLegacyApplicationTodoCenterItem(item)),
70
+ }))
71
+ .filter(group => group.items.length > 0);
72
+ return {
73
+ ...declaration,
74
+ frontend: {
75
+ ...declaration.frontend,
76
+ user: {
77
+ ...declaration.frontend.user,
78
+ applicationTodoCenter: true,
79
+ },
80
+ ...(admin
81
+ ? {
82
+ admin: {
83
+ ...admin,
84
+ navigation: defineAdminNavigation(normalizedNavigation),
85
+ },
86
+ }
87
+ : {}),
88
+ },
89
+ };
50
90
  }
51
91
  /**
52
92
  * Produces a deterministic one-time authoring suggestion for tooling. The
@@ -55,10 +95,18 @@ export function adminWorkflowLaunchPage(workflowCode, options) {
55
95
  export function suggestAdminNavigation(config) {
56
96
  const groups = [];
57
97
  const resources = [...(config.data?.resources || [])]
58
- .filter(resource => resource.surface?.generated?.list !== false)
59
98
  .sort((left, right) => left.code < right.code ? -1 : left.code > right.code ? 1 : 0);
60
- if (resources.length > 0) {
61
- groups.push(adminNavigationGroup('data-management', '数据管理', resources.map(resource => adminResourcePage(resource.code, { label: resource.name })), { icon: 'database', order: 100 }));
99
+ const resourceItems = resources.flatMap(resource => [
100
+ ...(resource.surface?.generated?.list !== false
101
+ ? [adminResourcePage(resource.code, { label: resource.name })]
102
+ : []),
103
+ ...[...(resource.surface?.views || [])]
104
+ .filter(view => view.generated.list !== false)
105
+ .sort((left, right) => left.code.localeCompare(right.code, 'en'))
106
+ .map(view => adminResourcePage(resource.code, { viewCode: view.code, label: view.name })),
107
+ ]);
108
+ if (resourceItems.length > 0) {
109
+ groups.push(adminNavigationGroup('data-management', '数据管理', resourceItems, { icon: 'database', order: 100 }));
62
110
  }
63
111
  const routes = [...(config.frontend.routes || [])]
64
112
  .filter(route => route.surface === 'admin' && !/[:*]/.test(route.path))
@@ -66,34 +114,13 @@ export function suggestAdminNavigation(config) {
66
114
  if (routes.length > 0) {
67
115
  groups.push(adminNavigationGroup('business-operations', '业务操作', routes.map(route => adminOperationPage(route.code, { label: route.label })), { icon: 'operations', order: 200 }));
68
116
  }
69
- const workflowCodes = [
70
- ...new Set((config.workflows?.definitions || []).map(declaration => declaration.definition.code)),
71
- ].sort();
72
- const workflowItems = [];
73
- if (workflowCodes.length > 0) {
74
- workflowItems.push(adminWorkflowWorkCenterPage({ label: '我的审批' }));
75
- }
76
- for (const code of workflowCodes) {
77
- const declarations = (config.workflows?.definitions || []).filter(declaration => declaration.definition.code === code);
78
- const activeVersion = config.workflows?.activations.find(activation => activation.workflowCode === code)?.definitionVersion;
79
- const selected = declarations.find(declaration => declaration.version === activeVersion) ||
80
- [...declarations].sort((left, right) => right.version - left.version)[0];
81
- if (selected?.launch?.mode === 'standalone') {
82
- workflowItems.push(adminWorkflowLaunchPage(code, {
83
- label: `发起${selected.definition.title}`,
84
- }));
85
- }
86
- }
87
- if (workflowItems.length > 0) {
88
- groups.push(adminNavigationGroup('workflow-center', '流程中心', workflowItems, {
89
- icon: 'workflow',
90
- order: 300,
91
- }));
92
- }
93
117
  return defineAdminNavigation(groups);
94
118
  }
95
119
  function renderAdminNavigationOptions(options) {
96
120
  const entries = [
121
+ ...(options.viewCode !== undefined
122
+ ? [`viewCode: ${JSON.stringify(options.viewCode)}`]
123
+ : []),
97
124
  ...(options.label !== undefined
98
125
  ? [`label: ${JSON.stringify(options.label)}`]
99
126
  : []),
@@ -114,7 +141,9 @@ export function renderAdminNavigationSuggestion(config) {
114
141
  const groups = navigation.map(group => {
115
142
  imports.add('adminNavigationGroup');
116
143
  const items = group.items.map(item => {
117
- const options = renderAdminNavigationOptions(item);
144
+ const options = renderAdminNavigationOptions({ ...item,
145
+ ...(item.page.kind === 'resource' && item.page.viewCode ? { viewCode: item.page.viewCode } : {}),
146
+ });
118
147
  let expression;
119
148
  switch (item.page.kind) {
120
149
  case 'resource':
@@ -125,18 +154,6 @@ export function renderAdminNavigationSuggestion(config) {
125
154
  imports.add('adminOperationPage');
126
155
  expression = `adminOperationPage(${JSON.stringify(item.page.routeCode)}${options ? `, ${options}` : ''})`;
127
156
  break;
128
- case 'application-todo-center':
129
- imports.add('adminApplicationTodoCenterPage');
130
- expression = `adminApplicationTodoCenterPage(${options || ''})`;
131
- break;
132
- case 'workflow-work-center':
133
- imports.add('adminWorkflowWorkCenterPage');
134
- expression = `adminWorkflowWorkCenterPage(${options || ''})`;
135
- break;
136
- case 'workflow-launch':
137
- imports.add('adminWorkflowLaunchPage');
138
- expression = `adminWorkflowLaunchPage(${JSON.stringify(item.page.workflowCode)}${options ? `, ${options}` : ''})`;
139
- break;
140
157
  }
141
158
  return ` ${expression},`;
142
159
  });
@@ -604,7 +621,6 @@ export function backendRuntimeRequired(config) {
604
621
  config.events?.subscriptions?.length ||
605
622
  config.events?.timers?.length ||
606
623
  config.events?.dateTriggers?.length ||
607
- config.workflows?.activations?.length ||
608
624
  config.workflows?.providers?.length)));
609
625
  }
610
626
  function object(value) {
@@ -834,7 +850,10 @@ export function validateAppConfig(value) {
834
850
  validateApplicationAuthentication(config, diagnostics);
835
851
  validateFrontendDevicePolicy(config, diagnostics);
836
852
  validateFrontendRoutes(config, diagnostics);
853
+ validateResourceDetailRoutes(config, diagnostics);
854
+ validateFrontendUser(config, diagnostics);
837
855
  validateAnonymousPublicAccess(config, diagnostics);
856
+ validateAdminAccess(config, diagnostics);
838
857
  validateAdminNavigation(config, diagnostics);
839
858
  validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
840
859
  const declaredResources = new Map((Array.isArray(object(config.data).resources)
@@ -855,7 +874,7 @@ export function validateAppConfig(value) {
855
874
  const declaredWorkflowCodes = new Set((Array.isArray(object(config.workflows).activations)
856
875
  ? object(config.workflows).activations
857
876
  : []).map(item => string(object(item).workflowCode)));
858
- validateBackendOperations(backend.operations, declaredResources, declaredWorkflowCodes, diagnostics);
877
+ validateBackendOperations(backend.operations, declaredResources, declaredWorkflowCodes, new Set((Array.isArray(object(config.authz).roles) ? object(config.authz).roles : []).map(role => string(object(role).code))), diagnostics);
859
878
  const eventConfig = object(config.events);
860
879
  const workflowConfig = object(config.workflows);
861
880
  if (backend.enabled === false &&
@@ -866,10 +885,8 @@ export function validateAppConfig(value) {
866
885
  Array.isArray(eventConfig.timers) && eventConfig.timers.length > 0 ||
867
886
  Array.isArray(eventConfig.dateTriggers) &&
868
887
  eventConfig.dateTriggers.length > 0 ||
869
- Array.isArray(workflowConfig.activations) &&
870
- workflowConfig.activations.length > 0 ||
871
888
  Array.isArray(workflowConfig.providers) && workflowConfig.providers.length > 0)) {
872
- diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件或工作流运行时', 'backend.enabled'));
889
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_DISABLED_WITH_DECLARATIONS', 'backend.enabled=false 时不能声明后端操作、Secret、事件消费者或工作流人员提供器', 'backend.enabled'));
873
890
  }
874
891
  const secretNames = new Set();
875
892
  const secretEnvs = new Set();
@@ -1041,6 +1058,22 @@ export function validateAppConfig(value) {
1041
1058
  }
1042
1059
  seenCapabilities.add(capability);
1043
1060
  });
1061
+ const deniedCapabilities = Array.isArray(role.deniedCapabilities)
1062
+ ? role.deniedCapabilities
1063
+ : [];
1064
+ if (role.deniedCapabilities !== undefined &&
1065
+ !Array.isArray(role.deniedCapabilities)) {
1066
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_DENIED_CAPABILITIES_INVALID', 'deniedCapabilities 必须是 capability 数组', `authz.roles[${index}].deniedCapabilities`));
1067
+ }
1068
+ const seenDeniedCapabilities = new Set();
1069
+ deniedCapabilities.forEach((rawCapability, capabilityIndex) => {
1070
+ const capability = string(rawCapability);
1071
+ if (!/^[A-Za-z0-9][A-Za-z0-9:._*-]{0,254}$/.test(capability) ||
1072
+ seenDeniedCapabilities.has(capability)) {
1073
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_ROLE_DENIED_CAPABILITY_INVALID', 'denied capability 必须是非空稳定标识,且同一角色内不能重复', `authz.roles[${index}].deniedCapabilities[${capabilityIndex}]`));
1074
+ }
1075
+ seenDeniedCapabilities.add(capability);
1076
+ });
1044
1077
  });
1045
1078
  const authenticatedUserRoleCode = authz.authenticatedUserRoleCode === undefined
1046
1079
  ? ''
@@ -1972,6 +2005,108 @@ export function validateAppConfig(value) {
1972
2005
  }
1973
2006
  editableParameterCodes.add(code);
1974
2007
  });
2008
+ const backendOperationsByCode = new Map((Array.isArray(backend.operations) ? backend.operations : []).map(raw => {
2009
+ const operation = object(raw);
2010
+ return [string(operation.code), operation];
2011
+ }));
2012
+ const dataResourcesByCode = new Map((Array.isArray(data.resources) ? data.resources : []).map(raw => {
2013
+ const resource = object(raw);
2014
+ return [string(resource.code), resource];
2015
+ }));
2016
+ const validateNamedOperationIntent = (rawIntent, intentKind, definition, path) => {
2017
+ const intent = object(rawIntent);
2018
+ const operationCode = string(intent.operationCode);
2019
+ const operation = backendOperationsByCode.get(operationCode);
2020
+ const inputs = object(intent.inputs);
2021
+ const output = object(intent.output);
2022
+ const subjectResource = dataResourcesByCode.get(definition.subject?.resourceCode);
2023
+ const subjectSchema = object(subjectResource?.schema);
2024
+ const subjectFields = new Set((Array.isArray(subjectSchema.fields) ? subjectSchema.fields : []).map(field => string(object(field).code)));
2025
+ const requestSchema = object(operation?.requestSchema);
2026
+ const requestProperties = object(requestSchema.properties);
2027
+ const requestRequired = new Set(Array.isArray(requestSchema.required)
2028
+ ? requestSchema.required.map(string)
2029
+ : []);
2030
+ const responseProperties = object(object(operation?.responseSchema).properties);
2031
+ const operationWorkflow = object(object(operation?.platformAccess).workflow);
2032
+ const operationWorkflowCodes = Array.isArray(operationWorkflow.codes)
2033
+ ? operationWorkflow.codes.map(string)
2034
+ : [];
2035
+ if (Object.keys(intent).some(key => !['operationCode', 'inputs', 'output'].includes(key)) ||
2036
+ !OPERATION_CODE_PATTERN.test(operationCode) ||
2037
+ !operation ||
2038
+ string(operation.method) !== 'POST' ||
2039
+ !operationWorkflowCodes.includes(definition.code)) {
2040
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_INVALID', '标准 Workflow named-operation 必须引用同一应用中声明目标 Workflow platformAccess 的 POST operation', path));
2041
+ }
2042
+ const inputEntries = Object.entries(inputs);
2043
+ const propertyCodePattern = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
2044
+ const fieldCodes = new Set();
2045
+ const sourceCounts = new Map();
2046
+ let inputsInvalid = !isRecord(intent.inputs) ||
2047
+ inputEntries.length === 0 ||
2048
+ inputEntries.length > 64;
2049
+ for (const [inputCode, rawBinding] of inputEntries) {
2050
+ const binding = object(rawBinding);
2051
+ const source = string(binding.source);
2052
+ const allowedKeys = source === 'field' ? ['source', 'fieldCode'] : ['source'];
2053
+ inputsInvalid ||=
2054
+ !propertyCodePattern.test(inputCode) ||
2055
+ !(inputCode in requestProperties) ||
2056
+ ![
2057
+ 'field',
2058
+ 'idempotency-key',
2059
+ 'current-user-reference',
2060
+ 'subject-id',
2061
+ 'subject-revision',
2062
+ 'requested-at',
2063
+ ].includes(source) ||
2064
+ Object.keys(binding).some(key => !allowedKeys.includes(key));
2065
+ sourceCounts.set(source, (sourceCounts.get(source) || 0) + 1);
2066
+ if (source === 'field') {
2067
+ const fieldCode = string(binding.fieldCode);
2068
+ inputsInvalid ||=
2069
+ !propertyCodePattern.test(fieldCode) ||
2070
+ !subjectFields.has(fieldCode) ||
2071
+ fieldCodes.has(fieldCode);
2072
+ fieldCodes.add(fieldCode);
2073
+ }
2074
+ }
2075
+ inputsInvalid ||=
2076
+ [...requestRequired].some(inputCode => !(inputCode in inputs)) ||
2077
+ (sourceCounts.get('idempotency-key') || 0) !== 1 ||
2078
+ ![...inputEntries].some(([inputCode, rawBinding]) => string(object(rawBinding).source) === 'idempotency-key' &&
2079
+ requestRequired.has(inputCode));
2080
+ if (intentKind === 'create') {
2081
+ inputsInvalid ||=
2082
+ (sourceCounts.get('subject-id') || 0) > 0 ||
2083
+ (sourceCounts.get('subject-revision') || 0) > 0;
2084
+ }
2085
+ else {
2086
+ inputsInvalid ||=
2087
+ (sourceCounts.get('subject-id') || 0) !== 1 ||
2088
+ (sourceCounts.get('subject-revision') || 0) !== 1;
2089
+ }
2090
+ if (inputsInvalid) {
2091
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_INPUT_INVALID', 'named-operation 必须以唯一且完整的顶层 input binding 覆盖请求必填字段、平台幂等键及对应 create/existing 身份来源', `${path}.inputs`));
2092
+ }
2093
+ const subjectIdOutput = string(output.subjectId);
2094
+ const subjectRevisionOutput = string(output.subjectRevision);
2095
+ const processCommandOutput = string(output.processCommand);
2096
+ if (!isRecord(intent.output) ||
2097
+ Object.keys(output).some(key => !['subjectId', 'subjectRevision', 'processCommand'].includes(key)) ||
2098
+ !propertyCodePattern.test(subjectIdOutput) ||
2099
+ !(subjectIdOutput in responseProperties) ||
2100
+ (output.subjectRevision !== undefined &&
2101
+ (!propertyCodePattern.test(subjectRevisionOutput) ||
2102
+ !(subjectRevisionOutput in responseProperties))) ||
2103
+ (output.processCommand !== undefined &&
2104
+ (!propertyCodePattern.test(processCommandOutput) ||
2105
+ !(processCommandOutput in responseProperties)))) {
2106
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_OUTPUT_INVALID', 'named-operation output 必须引用 responseSchema 中的顶层 subjectId、可选 revision 和可选 processCommand 属性', `${path}.output`));
2107
+ }
2108
+ return fieldCodes;
2109
+ };
1975
2110
  definitions.forEach((item, index) => {
1976
2111
  const declaration = object(item);
1977
2112
  const definition = object(declaration.definition);
@@ -1982,7 +2117,7 @@ export function validateAppConfig(value) {
1982
2117
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_DECLARATION_INVALID', 'Workflow definition declaration 只能声明 version、definition、launch 和 detailRouteCode', path));
1983
2118
  }
1984
2119
  if (declaration.launch !== undefined &&
1985
- (Object.keys(launch).some(key => key !== 'mode') ||
2120
+ (Object.keys(launch).some(key => !['mode', 'submission'].includes(key)) ||
1986
2121
  ![
1987
2122
  'standalone',
1988
2123
  'custom-page',
@@ -1991,6 +2126,41 @@ export function validateAppConfig(value) {
1991
2126
  ].includes(string(launch.mode)))) {
1992
2127
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_MODE_INVALID', 'Workflow launch.mode 必须是 standalone、custom-page、hidden-handoff 或 work-center-only', `${path}.launch.mode`));
1993
2128
  }
2129
+ if (launch.submission !== undefined) {
2130
+ const submission = object(launch.submission);
2131
+ const createFields = submission.create
2132
+ ? validateNamedOperationIntent(submission.create, 'create', definition, `${path}.launch.submission.create`)
2133
+ : new Set();
2134
+ const existingFields = submission.existing
2135
+ ? validateNamedOperationIntent(submission.existing, 'existing', definition, `${path}.launch.submission.existing`)
2136
+ : new Set();
2137
+ const contexts = Array.isArray(submission.context)
2138
+ ? submission.context
2139
+ : [];
2140
+ const contextKeys = new Set();
2141
+ let contextInvalid = contexts.length > 16;
2142
+ for (const rawContext of contexts) {
2143
+ const context = object(rawContext);
2144
+ const queryParameter = string(context.queryParameter);
2145
+ const fieldCode = string(context.fieldCode);
2146
+ contextInvalid ||=
2147
+ Object.keys(context).some(key => !['queryParameter', 'fieldCode'].includes(key)) ||
2148
+ !/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(queryParameter) ||
2149
+ contextKeys.has(queryParameter) ||
2150
+ (!createFields.has(fieldCode) && !existingFields.has(fieldCode));
2151
+ contextKeys.add(queryParameter);
2152
+ }
2153
+ if (!isRecord(launch.submission) ||
2154
+ submission.kind !== 'named-operation' ||
2155
+ Object.keys(submission).some(key => !['kind', 'create', 'existing', 'context'].includes(key)) ||
2156
+ (!['standalone', 'hidden-handoff'].includes(string(launch.mode))) ||
2157
+ (!submission.create && !submission.existing)) {
2158
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_NAMED_OPERATION_SUBMISSION_INVALID', 'named-operation submission 只适用于 standalone/hidden-handoff,且必须声明 create 或 existing intent', `${path}.launch.submission`));
2159
+ }
2160
+ if (contextInvalid) {
2161
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_CONTEXT_INVALID', 'Workflow launch context 必须唯一、受限并指向已绑定的 subject form field', `${path}.launch.submission.context`));
2162
+ }
2163
+ }
1994
2164
  if (declaration.detailRouteCode !== undefined) {
1995
2165
  const detailRouteCode = object(declaration.detailRouteCode);
1996
2166
  const desktopCode = string(detailRouteCode.desktop);
@@ -2255,7 +2425,7 @@ function validateCapabilityDeclarations(appCode, declarations, diagnostics) {
2255
2425
  seen.add(code);
2256
2426
  });
2257
2427
  }
2258
- function validateBackendOperations(rawOperations, declaredResources, declaredWorkflowCodes, diagnostics) {
2428
+ function validateBackendOperations(rawOperations, declaredResources, declaredWorkflowCodes, declaredRoleCodes, diagnostics) {
2259
2429
  const declaredResourceCodes = new Set(declaredResources.keys());
2260
2430
  if (rawOperations !== undefined && !Array.isArray(rawOperations)) {
2261
2431
  diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATIONS_INVALID', 'backend.operations 必须是数组', 'backend.operations'));
@@ -2292,10 +2462,18 @@ function validateBackendOperations(rawOperations, declaredResources, declaredWor
2292
2462
  'managedFileCopies',
2293
2463
  'notification',
2294
2464
  'workflow',
2465
+ 'roleAssertions',
2295
2466
  ]);
2296
2467
  let invalid = !isRecord(operation.platformAccess) ||
2297
2468
  Object.keys(access).length === 0 ||
2298
2469
  Object.keys(access).some(key => !allowedAccessKeys.has(key));
2470
+ if (access.roleAssertions !== undefined) {
2471
+ const assertion = object(access.roleAssertions);
2472
+ const roles = Array.isArray(assertion.roleCodes) ? assertion.roleCodes : [];
2473
+ invalid ||= !isRecord(access.roleAssertions) || roles.length < 1 || roles.length > 20 ||
2474
+ new Set(roles).size !== roles.length || roles.some(role => !declaredRoleCodes.has(string(role))) ||
2475
+ Object.keys(assertion).some(key => key !== 'roleCodes');
2476
+ }
2299
2477
  if (access.directory !== undefined) {
2300
2478
  const directory = object(access.directory);
2301
2479
  const fields = Array.isArray(directory.fields) ? directory.fields : [];
@@ -2408,7 +2586,7 @@ function validateBackendOperations(rawOperations, declaredResources, declaredWor
2408
2586
  Object.keys(workflow).some(key => key !== 'codes');
2409
2587
  }
2410
2588
  if (invalid) {
2411
- diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_PLATFORM_ACCESS_INVALID', 'operation.platformAccess 必须只声明有界的发起人目录、托管文件、托管文件复制、标准通知或现有工作流依赖', `${path}.platformAccess`));
2589
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_PLATFORM_ACCESS_INVALID', 'operation.platformAccess 必须只声明有界的发起人目录、托管文件、托管文件复制、标准通知、现有工作流或已声明角色条件依赖', `${path}.platformAccess`));
2412
2590
  }
2413
2591
  }
2414
2592
  if (operation.ai !== undefined) {
@@ -2480,64 +2658,66 @@ function generatedPlatformRouteClaims(config) {
2480
2658
  const code = string(resource.code);
2481
2659
  if (!code)
2482
2660
  continue;
2483
- const surface = object(resource.surface);
2484
- const generated = object(surface.generated);
2485
- const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2486
- const operations = {
2487
- list: generated.list === undefined ? true : generated.list === true,
2488
- detail: generated.detail === undefined ? true : generated.detail === true,
2489
- create: generated.create === undefined ? native : generated.create === true,
2490
- update: generated.update === undefined ? native : generated.update === true,
2491
- };
2492
- const base = `/admin/resources/${code}`;
2493
- const candidates = [
2494
- ['list', base],
2495
- ['detail', `${base}/:id`],
2496
- ['create', `${base}/new`],
2497
- ['update', `${base}/:id/edit`],
2498
- ];
2499
- for (const [operation, path] of candidates) {
2500
- if (!operations[operation])
2501
- continue;
2502
- claims.push({ owner: `resource:${code}:${operation}`, path }, { owner: `resource:${code}:${operation}:mobile`, path: `/m${path}` });
2661
+ const resourceSurface = object(resource.surface);
2662
+ for (const selected of [resourceSurface, ...(Array.isArray(resourceSurface.views) ? resourceSurface.views.map(object) : [])]) {
2663
+ const surface = { ...resourceSurface, ...selected };
2664
+ const viewCode = string(selected.code);
2665
+ const generated = object(surface.generated);
2666
+ const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2667
+ const operations = {
2668
+ list: generated.list === undefined ? true : generated.list === true,
2669
+ detail: generated.detail === undefined ? true : generated.detail === true,
2670
+ create: generated.create === undefined ? native : generated.create === true,
2671
+ update: generated.update === undefined ? native : generated.update === true,
2672
+ };
2673
+ const base = `/admin/resources/${code}${viewCode ? `/views/${viewCode}` : ""}`;
2674
+ const candidates = [
2675
+ ['list', base],
2676
+ ['detail', `${base}/:id`],
2677
+ ['create', `${base}/new`],
2678
+ ['update', `${base}/:id/edit`],
2679
+ ];
2680
+ for (const [operation, path] of candidates) {
2681
+ if (!operations[operation])
2682
+ continue;
2683
+ const owner = `resource:${code}${viewCode ? `:view:${viewCode}` : ''}:${operation}`;
2684
+ claims.push({ owner, path });
2685
+ if (object(surface.mobile).enabled !== false)
2686
+ claims.push({ owner: `${owner}:mobile`, path: `/m${path}` });
2687
+ }
2503
2688
  }
2504
2689
  }
2505
2690
  const frontend = object(config.frontend);
2506
- const admin = object(frontend.admin);
2507
- const navigation = Array.isArray(admin.navigation) ? admin.navigation : [];
2508
- const hasTodoCenter = navigation.some(rawGroup => {
2509
- const group = object(rawGroup);
2510
- const items = Array.isArray(group.items) ? group.items : [];
2511
- return items.some(rawItem => string(object(object(rawItem).page).kind) === 'application-todo-center');
2512
- });
2691
+ const user = object(frontend.user);
2692
+ const hasTodoCenter = user.applicationTodoCenter === true;
2513
2693
  if (hasTodoCenter) {
2514
- claims.push({ owner: 'application:todo-center', path: '/admin/todos' }, { owner: 'application:todo-center:mobile', path: '/m/todos' });
2694
+ claims.push({ owner: 'application:todo-center', path: '/todos' }, { owner: 'application:todo-center:mobile', path: '/m/todos' });
2515
2695
  }
2516
2696
  const workflows = object(config.workflows);
2517
2697
  const definitions = Array.isArray(workflows.definitions)
2518
2698
  ? workflows.definitions
2519
2699
  : [];
2520
2700
  if (definitions.length > 0) {
2521
- claims.push({ owner: 'workflow:work-center', path: '/admin/work-center' }, { owner: 'workflow:work-center:mobile', path: '/m/work-center' }, { owner: 'workflow:task', path: '/tasks/:taskId' }, { owner: 'workflow:task:mobile', path: '/m/tasks/:taskId' }, { owner: 'workflow:instance', path: '/workflows/:instanceId' }, {
2701
+ claims.push({ owner: 'workflow:work-center', path: '/work-center' }, { owner: 'workflow:work-center:mobile', path: '/m/work-center' }, { owner: 'workflow:task', path: '/tasks/:taskId' }, { owner: 'workflow:task:mobile', path: '/m/tasks/:taskId' }, { owner: 'workflow:instance', path: '/workflows/:instanceId' }, {
2522
2702
  owner: 'workflow:instance:mobile',
2523
2703
  path: '/m/workflows/:instanceId',
2524
2704
  });
2525
- const standaloneCodes = new Set();
2705
+ const launchCodes = new Set();
2526
2706
  for (const rawDefinition of definitions) {
2527
2707
  const declaration = object(rawDefinition);
2528
- if (string(object(declaration.launch).mode) !== 'standalone')
2708
+ if (!['standalone', 'hidden-handoff'].includes(string(object(declaration.launch).mode)))
2529
2709
  continue;
2530
2710
  const workflowCode = string(object(declaration.definition).code);
2531
2711
  if (workflowCode)
2532
- standaloneCodes.add(workflowCode);
2712
+ launchCodes.add(workflowCode);
2533
2713
  }
2534
- for (const workflowCode of standaloneCodes) {
2714
+ for (const workflowCode of launchCodes) {
2535
2715
  claims.push({
2536
2716
  owner: `workflow:${workflowCode}:launch`,
2537
- path: `/admin/workflows/${workflowCode}/new`,
2717
+ path: '/workflows/:workflowCode/start',
2538
2718
  });
2539
2719
  }
2540
- if (standaloneCodes.size > 0) {
2720
+ if (launchCodes.size > 0) {
2541
2721
  claims.push({
2542
2722
  owner: 'workflow:launch:mobile',
2543
2723
  path: '/m/workflows/:workflowCode/start',
@@ -2566,6 +2746,68 @@ function validateFrontendDevicePolicy(config, diagnostics) {
2566
2746
  diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_DEVICE_POLICY_INVALID', 'frontend.devicePolicy 必须是 viewport-family,移动最大宽度和桌面最小宽度为 320-1600 的相邻整数', 'frontend.devicePolicy'));
2567
2747
  }
2568
2748
  }
2749
+ function validateResourceDetailRoutes(config, diagnostics) {
2750
+ const frontend = object(config.frontend);
2751
+ const routesByCode = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(rawRoute => {
2752
+ const route = object(rawRoute);
2753
+ return [string(route.code), route];
2754
+ }));
2755
+ const data = object(config.data);
2756
+ const resources = Array.isArray(data.resources) ? data.resources : [];
2757
+ resources.forEach((rawResource, resourceIndex) => {
2758
+ const resource = object(rawResource);
2759
+ if (resource.detailRouteCode === undefined)
2760
+ return;
2761
+ const path = `data.resources[${resourceIndex}].detailRouteCode`;
2762
+ const detailRouteCode = object(resource.detailRouteCode);
2763
+ const desktopCode = string(detailRouteCode.desktop);
2764
+ const mobileCode = string(detailRouteCode.mobile);
2765
+ if (Object.keys(detailRouteCode).some(key => !['desktop', 'mobile'].includes(key)) ||
2766
+ !OPERATION_CODE_PATTERN.test(desktopCode) ||
2767
+ !OPERATION_CODE_PATTERN.test(mobileCode) ||
2768
+ desktopCode === mobileCode) {
2769
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_INVALID', 'detailRouteCode 必须声明两个不同且合法的 desktop/mobile route code', path));
2770
+ return;
2771
+ }
2772
+ const readCapability = string(object(resource.capabilities).read);
2773
+ for (const [device, code] of [
2774
+ ['desktop', desktopCode],
2775
+ ['mobile', mobileCode],
2776
+ ]) {
2777
+ const route = routesByCode.get(code);
2778
+ const routePointer = `${path}.${device}`;
2779
+ if (!route) {
2780
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_REFERENCE_MISSING', `detailRouteCode.${device} 必须引用已声明 frontend route`, routePointer));
2781
+ continue;
2782
+ }
2783
+ if (string(route.surface) !== 'user') {
2784
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_SURFACE_INVALID', `detailRouteCode.${device} 必须引用 user surface`, routePointer));
2785
+ }
2786
+ const routePath = string(route.path);
2787
+ const parameters = [
2788
+ ...routePath.matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g),
2789
+ ].map(match => match[1]);
2790
+ const wrongDeviceFamily = device === 'mobile'
2791
+ ? !routePath.startsWith('/m/')
2792
+ : routePath === '/m' || routePath.startsWith('/m/');
2793
+ if (routePath.includes('*') ||
2794
+ routePath.includes('?') ||
2795
+ routePath.includes('#') ||
2796
+ routePath.split('/').includes('..') ||
2797
+ parameters.length !== 1 ||
2798
+ wrongDeviceFamily) {
2799
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_PATH_INVALID', `detailRouteCode.${device} 路由必须属于对应端且只能声明一个有界动态记录参数`, routePointer));
2800
+ }
2801
+ const access = object(route.access);
2802
+ const allOf = Array.isArray(access.allOf) ? access.allOf.map(string) : [];
2803
+ if (!readCapability ||
2804
+ (string(route.capability) !== readCapability &&
2805
+ !allOf.includes(readCapability))) {
2806
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_DETAIL_ROUTE_CAPABILITY_INVALID', `detailRouteCode.${device} 路由必须要求当前资源的 read capability`, routePointer));
2807
+ }
2808
+ }
2809
+ });
2810
+ }
2569
2811
  function validateFrontendRoutes(config, diagnostics) {
2570
2812
  const frontend = object(config.frontend);
2571
2813
  const rawRoutes = frontend.routes;
@@ -2642,9 +2884,14 @@ function validateFrontendRoutes(config, diagnostics) {
2642
2884
  !['admin', 'user'].includes(string(route.surface)) ||
2643
2885
  (route.surface === 'admin' &&
2644
2886
  routePath !== '/admin' &&
2645
- !routePath.startsWith('/admin/')) ||
2887
+ !routePath.startsWith('/admin/') &&
2888
+ routePath !== '/m/admin' &&
2889
+ !routePath.startsWith('/m/admin/')) ||
2646
2890
  (route.surface === 'user' &&
2647
- (routePath === '/admin' || routePath.startsWith('/admin/'))) ||
2891
+ (routePath === '/admin' ||
2892
+ routePath.startsWith('/admin/') ||
2893
+ routePath === '/m/admin' ||
2894
+ routePath.startsWith('/m/admin/'))) ||
2648
2895
  (route.capability !== undefined &&
2649
2896
  !CAPABILITY_PATTERN.test(string(route.capability))) ||
2650
2897
  accessInvalid ||
@@ -2727,16 +2974,17 @@ function validateAnonymousPublicAccess(config, diagnostics) {
2727
2974
  const declaration = object(field);
2728
2975
  return [string(declaration.code), string(declaration.type)];
2729
2976
  }));
2977
+ const surface = object(resource.surface);
2978
+ const surfaceFields = object(surface.fields);
2730
2979
  const requiredCreateFields = new Set(fields
2731
2980
  .map(object)
2732
2981
  .filter(field => {
2733
2982
  const type = string(field.type);
2734
- return (field.nullable === false &&
2735
- APP_DATA_FIELD_TYPES.has(type) &&
2736
- supportsGeneratedMutation({ type }));
2983
+ const fieldSurface = object(surfaceFields[string(field.code)]);
2984
+ return (APP_DATA_FIELD_TYPES.has(type) &&
2985
+ nativeFieldRequiresCreateInputV2({ type, ...(field.nullable === false ? { nullable: false } : {}) }, { system: fieldSurface.system === true, requiredHint: fieldSurface.requiredHint === true }));
2737
2986
  })
2738
2987
  .map(field => string(field.code)));
2739
- const surface = object(resource.surface);
2740
2988
  const generated = object(surface.generated);
2741
2989
  const nativeCreate = (string(surface.mutationOwner) || 'native') === 'native' &&
2742
2990
  generated.create !== false;
@@ -2803,9 +3051,7 @@ function validateAnonymousPublicAccess(config, diagnostics) {
2803
3051
  fields.some(field => !resourceFields?.has(field)) ||
2804
3052
  new Set(requiredFields).size !== requiredFields.length ||
2805
3053
  requiredFields.some(field => !fields.includes(field)) ||
2806
- (operations.includes('create') &&
2807
- (!resource?.nativeCreate ||
2808
- [...(resource?.requiredCreateFields || [])].some(field => !fields.includes(field) || !requiredFields.includes(field)))) ||
3054
+ (operations.includes('create') && !resource?.nativeCreate) ||
2809
3055
  new Set(ownRecordFields).size !== ownRecordFields.length ||
2810
3056
  ownRecordFields.some(field => !fields.includes(field)) ||
2811
3057
  (operations.includes('draft.read') !== operations.includes('draft.update')) ||
@@ -2826,6 +3072,13 @@ function validateAnonymousPublicAccess(config, diagnostics) {
2826
3072
  if (invalid) {
2827
3073
  diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_PUBLIC_POLICY_INVALID', '匿名公开策略必须绑定唯一静态 user route、已声明资源/字段和有界操作', path));
2828
3074
  }
3075
+ if (operations.includes('create') && resource?.nativeCreate) {
3076
+ const missingFields = [...resource.requiredCreateFields].filter(field => !fields.includes(field));
3077
+ const missingRequired = [...resource.requiredCreateFields].filter(field => !requiredFields.includes(field));
3078
+ if (missingFields.length || missingRequired.length) {
3079
+ diagnostics.push(diagnostic('APP_CONFIG_ANONYMOUS_CREATE_FIELDS_INCOMPLETE', `公开创建必须覆盖资源 ${resourceCode} 的必填字段:${[...new Set([...missingFields, ...missingRequired])].join('、')}`, `${path}.${missingFields.length ? 'fields' : 'requiredFields'}`));
3080
+ }
3081
+ }
2829
3082
  const validationCodes = new Set();
2830
3083
  validations.forEach((rawValidation, validationIndex) => {
2831
3084
  const validation = object(rawValidation);
@@ -2984,14 +3237,50 @@ function validAdminOrder(value) {
2984
3237
  return (value === undefined ||
2985
3238
  (Number.isSafeInteger(value) && Math.abs(Number(value)) <= 10_000));
2986
3239
  }
3240
+ function validateFrontendUser(config, diagnostics) {
3241
+ const frontend = object(config.frontend);
3242
+ if (frontend.user === undefined)
3243
+ return;
3244
+ const user = object(frontend.user);
3245
+ if (!frontend.user ||
3246
+ typeof frontend.user !== 'object' ||
3247
+ Array.isArray(frontend.user) ||
3248
+ Object.keys(user).some(key => key !== 'applicationTodoCenter') ||
3249
+ (user.applicationTodoCenter !== undefined &&
3250
+ typeof user.applicationTodoCenter !== 'boolean')) {
3251
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_USER_INVALID', 'frontend.user 只接受 applicationTodoCenter 布尔值', 'frontend.user'));
3252
+ }
3253
+ }
3254
+ function validateAdminAccess(config, diagnostics) {
3255
+ const accessValue = config.frontend.admin?.access;
3256
+ if (accessValue === undefined)
3257
+ return;
3258
+ const access = object(accessValue);
3259
+ const allOf = Array.isArray(access.allOf) ? access.allOf : [];
3260
+ const anyOf = Array.isArray(access.anyOf) ? access.anyOf : [];
3261
+ const invalid = !accessValue ||
3262
+ typeof accessValue !== 'object' ||
3263
+ Array.isArray(accessValue) ||
3264
+ Object.keys(access).some(key => !['allOf', 'anyOf'].includes(key)) ||
3265
+ (allOf.length === 0 && anyOf.length === 0) ||
3266
+ allOf.length > 50 ||
3267
+ anyOf.length > 50 ||
3268
+ allOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
3269
+ anyOf.some(value => !CAPABILITY_PATTERN.test(string(value))) ||
3270
+ new Set(allOf.map(string)).size !== allOf.length ||
3271
+ new Set(anyOf.map(string)).size !== anyOf.length;
3272
+ if (invalid) {
3273
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ACCESS_INVALID', 'frontend.admin.access 必须包含非空、无重复且有效的 allOf 或 anyOf capability 数组', 'frontend.admin.access'));
3274
+ }
3275
+ }
2987
3276
  function validateAdminNavigation(config, diagnostics) {
2988
3277
  const frontend = object(config.frontend);
2989
3278
  const admin = object(frontend.admin);
2990
3279
  const rawNavigation = admin.navigation;
2991
3280
  if (frontend.admin !== undefined &&
2992
- (Object.keys(admin).some(key => key !== 'navigation') ||
3281
+ (Object.keys(admin).some(key => !['access', 'navigation'].includes(key)) ||
2993
3282
  !Array.isArray(rawNavigation))) {
2994
- diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_INVALID', 'frontend.admin 只接受 navigation 数组', 'frontend.admin.navigation'));
3283
+ diagnostics.push(diagnostic('APP_CONFIG_ADMIN_NAVIGATION_INVALID', 'frontend.admin 只接受 access 和 navigation 数组', 'frontend.admin.navigation'));
2995
3284
  return;
2996
3285
  }
2997
3286
  const groups = Array.isArray(rawNavigation) ? rawNavigation : [];
@@ -3000,24 +3289,6 @@ function validateAdminNavigation(config, diagnostics) {
3000
3289
  }
3001
3290
  const resources = new Map((config.data?.resources || []).map(resource => [resource.code, resource]));
3002
3291
  const routes = new Map((config.frontend.routes || []).map(route => [route.code, route]));
3003
- const workflows = new Map();
3004
- const workflowDefinitionsByCode = new Map();
3005
- for (const declaration of config.workflows?.definitions || []) {
3006
- const declarations = workflowDefinitionsByCode.get(declaration.definition.code) || [];
3007
- declarations.push(declaration);
3008
- workflowDefinitionsByCode.set(declaration.definition.code, declarations);
3009
- }
3010
- for (const [code, declarations] of workflowDefinitionsByCode) {
3011
- const activeVersion = config.workflows?.activations.find(activation => activation.workflowCode === code)?.definitionVersion;
3012
- const selected = declarations.find(declaration => declaration.version === activeVersion) ||
3013
- [...declarations].sort((left, right) => right.version - left.version)[0];
3014
- if (selected) {
3015
- workflows.set(code, {
3016
- title: selected.definition.title,
3017
- mode: selected.launch?.mode || 'work-center-only',
3018
- });
3019
- }
3020
- }
3021
3292
  const groupCodes = new Set();
3022
3293
  const pageKeys = new Set();
3023
3294
  let itemCount = 0;
@@ -3051,14 +3322,15 @@ function validateAdminNavigation(config, diagnostics) {
3051
3322
  let defaultLabel = '';
3052
3323
  let internalCode = '';
3053
3324
  let exists = false;
3054
- let standaloneViolation = false;
3055
3325
  if (kind === 'resource') {
3056
3326
  const resourceCode = string(page.resourceCode);
3057
3327
  const resource = resources.get(resourceCode);
3058
- pageKey = `resource:${resourceCode}:list`;
3059
- defaultLabel = resource?.name || '';
3328
+ const viewCode = string(page.viewCode);
3329
+ const view = resource?.surface?.views?.find((item) => item.code === viewCode);
3330
+ pageKey = `resource:${resourceCode}${viewCode ? `:view:${viewCode}` : ""}:list`;
3331
+ defaultLabel = (viewCode ? view?.name : resource?.name) || '';
3060
3332
  internalCode = resourceCode;
3061
- exists = Boolean(resource?.surface?.generated?.list);
3333
+ exists = Boolean(viewCode ? view?.generated?.list : resource?.surface?.generated?.list);
3062
3334
  }
3063
3335
  else if (kind === 'operation') {
3064
3336
  const routeCode = string(page.routeCode);
@@ -3066,46 +3338,22 @@ function validateAdminNavigation(config, diagnostics) {
3066
3338
  pageKey = `operation:${routeCode}`;
3067
3339
  defaultLabel = route?.label || '';
3068
3340
  internalCode = routeCode;
3069
- exists = Boolean(route && route.surface === 'admin' && !/[:*]/.test(route.path));
3070
- }
3071
- else if (kind === 'application-todo-center') {
3072
- pageKey = 'application:todo-center';
3073
- defaultLabel = '待办中心';
3074
- internalCode = pageKey;
3075
- exists = true;
3076
- }
3077
- else if (kind === 'workflow-work-center') {
3078
- pageKey = 'workflow:work-center';
3079
- defaultLabel = '我的审批';
3080
- internalCode = pageKey;
3081
- exists = workflows.size > 0;
3082
- }
3083
- else if (kind === 'workflow-launch') {
3084
- const workflowCode = string(page.workflowCode);
3085
- const workflow = workflows.get(workflowCode);
3086
- pageKey = `workflow:${workflowCode}:launch`;
3087
- defaultLabel = workflow ? `发起${workflow.title}` : '';
3088
- internalCode = workflowCode;
3089
- standaloneViolation = Boolean(workflow && workflow.mode !== 'standalone');
3090
- exists = Boolean(workflow && workflow.mode === 'standalone');
3341
+ exists = Boolean(route &&
3342
+ route.surface === 'admin' &&
3343
+ !route.path.startsWith('/m/admin') &&
3344
+ !/[:*]/.test(route.path));
3091
3345
  }
3092
3346
  if (Object.keys(item).some(key => !['page', 'label', 'icon', 'order'].includes(key)) ||
3093
3347
  ![
3094
3348
  'resource',
3095
3349
  'operation',
3096
- 'application-todo-center',
3097
- 'workflow-work-center',
3098
- 'workflow-launch',
3099
3350
  ].includes(kind) ||
3100
3351
  (item.icon !== undefined &&
3101
3352
  !ADMIN_NAVIGATION_ICONS.has(string(item.icon))) ||
3102
3353
  !validAdminOrder(item.order)) {
3103
3354
  diagnostics.push(diagnostic('APP_CONFIG_ADMIN_ITEM_INVALID', '导航项必须使用受支持的页面 helper、label、icon 和 order', itemPath));
3104
3355
  }
3105
- if (standaloneViolation) {
3106
- diagnostics.push(diagnostic('APP_CONFIG_ADMIN_WORKFLOW_NOT_STANDALONE', '只有 self-contained standalone Workflow launch page 可以进入菜单', `${itemPath}.page`));
3107
- }
3108
- else if (!exists) {
3356
+ if (!exists) {
3109
3357
  diagnostics.push(diagnostic('APP_CONFIG_ADMIN_PAGE_NOT_FOUND', '导航引用必须指向存在且可进入菜单的 page registry 页面', `${itemPath}.page`));
3110
3358
  }
3111
3359
  if (pageKey && pageKeys.has(pageKey)) {
@@ -3199,6 +3447,12 @@ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
3199
3447
  capability: string(value),
3200
3448
  path: `authz.roles[${index}].capabilities[${capabilityIndex}]`,
3201
3449
  }));
3450
+ (Array.isArray(role.deniedCapabilities)
3451
+ ? role.deniedCapabilities
3452
+ : []).forEach((value, capabilityIndex) => references.push({
3453
+ capability: string(value),
3454
+ path: `authz.roles[${index}].deniedCapabilities[${capabilityIndex}]`,
3455
+ }));
3202
3456
  });
3203
3457
  const backend = object(config.backend);
3204
3458
  (Array.isArray(backend.operations) ? backend.operations : []).forEach((rawOperation, index) => references.push({
@@ -3224,6 +3478,14 @@ function validateCapabilityClosure(appCode, config, declarations, diagnostics) {
3224
3478
  }));
3225
3479
  }
3226
3480
  });
3481
+ const adminAccess = object(object(frontend.admin).access);
3482
+ for (const key of ['allOf', 'anyOf']) {
3483
+ const values = Array.isArray(adminAccess[key]) ? adminAccess[key] : [];
3484
+ values.forEach((value, capabilityIndex) => references.push({
3485
+ capability: string(value),
3486
+ path: `frontend.admin.access.${key}[${capabilityIndex}]`,
3487
+ }));
3488
+ }
3227
3489
  for (const reference of references) {
3228
3490
  if (!catalog.has(reference.capability) ||
3229
3491
  (!reference.capability.startsWith(`app:${appCode}:`) &&
@@ -3274,6 +3536,21 @@ export function resourceCapabilityCodes(appCode, resourceCode) {
3274
3536
  delete: `${prefix}:delete`,
3275
3537
  };
3276
3538
  }
3539
+ function materializeRoleCapabilities(role) {
3540
+ const capabilities = new Set(role.capabilities);
3541
+ for (const denied of role.deniedCapabilities || []) {
3542
+ capabilities.delete(denied);
3543
+ }
3544
+ return [...capabilities].sort();
3545
+ }
3546
+ /** Explicit business permission preset; adding a page never changes grants. */
3547
+ export function resourceRoleCapabilities(appCode, resourceCode, access) {
3548
+ const codes = resourceCapabilityCodes(appCode, resourceCode);
3549
+ const operations = access === 'manage'
3550
+ ? ['read', 'create', 'update', 'delete']
3551
+ : access === 'read' ? ['read'] : access;
3552
+ return [...new Set(operations.map(operation => codes[operation]))].sort();
3553
+ }
3277
3554
  export function currentUserDataPolicy(input) {
3278
3555
  return {
3279
3556
  code: input.code,
@@ -3365,9 +3642,12 @@ export function materializeDataResource(appCode, declaration) {
3365
3642
  physicalPlanForField(definition, field.searchable === true);
3366
3643
  return definition;
3367
3644
  });
3368
- const visible = declaration.fields
3369
- .filter(field => field.list !== false)
3370
- .slice(0, 8);
3645
+ const listCandidates = declaration.fields
3646
+ .filter(field => field.list !== false && (field.hidden ?? field.system) !== true);
3647
+ const visible = declaration.list?.fields
3648
+ ? declaration.list.fields.flatMap(code => listCandidates.filter(field => field.code === code))
3649
+ : listCandidates.slice(0, 8);
3650
+ const defaultSortField = visible.find(field => supportsSort(field.type));
3371
3651
  const searchableFields = declaration.fields
3372
3652
  .filter(field => field.searchable === true)
3373
3653
  .map(field => field.code);
@@ -3390,6 +3670,7 @@ export function materializeDataResource(appCode, declaration) {
3390
3670
  return [capabilities[operation]];
3391
3671
  };
3392
3672
  const surface = {
3673
+ ...(declaration.views?.length ? { views: declaration.views } : {}),
3393
3674
  mutationOwner,
3394
3675
  generated,
3395
3676
  fields: Object.fromEntries(declaration.fields.map(field => [
@@ -3401,6 +3682,9 @@ export function materializeDataResource(appCode, declaration) {
3401
3682
  ...(field.section ? { section: field.section } : {}),
3402
3683
  ...(field.required === true ? { requiredHint: true } : {}),
3403
3684
  ...(field.system !== undefined ? { system: field.system } : {}),
3685
+ ...(field.hidden !== undefined || field.system
3686
+ ? { hidden: field.hidden ?? true }
3687
+ : {}),
3404
3688
  readCapabilities: access(field, 'read'),
3405
3689
  createCapabilities: access(field, 'create'),
3406
3690
  updateCapabilities: access(field, 'update'),
@@ -3434,22 +3718,23 @@ export function materializeDataResource(appCode, declaration) {
3434
3718
  },
3435
3719
  ])),
3436
3720
  list: {
3721
+ fieldOrder: visible.map(field => field.code),
3437
3722
  defaultPageSize: declaration.list?.defaultPageSize || 20,
3438
3723
  ...(searchableFields.length ? { searchableFields } : {}),
3439
3724
  ...(filterFields.length ? { filterFields } : {}),
3440
3725
  ...(declaration.list?.defaultSort
3441
3726
  ? { defaultSort: declaration.list.defaultSort }
3442
- : visible[0]
3443
- ? { defaultSort: { field: visible[0].code, order: 'asc' } }
3727
+ : defaultSortField
3728
+ ? { defaultSort: { field: defaultSortField.code, order: 'asc' } }
3444
3729
  : {}),
3445
3730
  },
3446
3731
  form: {
3447
3732
  layout: declaration.form?.layout || 'flat',
3448
- fieldOrder: declaration.fields.map(field => field.code),
3733
+ fieldOrder: declaration.form?.fields || declaration.fields.map(field => field.code),
3449
3734
  },
3450
3735
  detail: {
3451
3736
  layout: declaration.detail?.layout || 'flat',
3452
- fieldOrder: declaration.fields.map(field => field.code),
3737
+ fieldOrder: declaration.detail?.fields || declaration.fields.map(field => field.code),
3453
3738
  },
3454
3739
  mobile: declaration.mobile || { enabled: true },
3455
3740
  };
@@ -3465,6 +3750,9 @@ export function materializeDataResource(appCode, declaration) {
3465
3750
  ...(declaration.dataPolicyCode
3466
3751
  ? { dataPolicyCode: declaration.dataPolicyCode }
3467
3752
  : {}),
3753
+ ...(declaration.detailRouteCode
3754
+ ? { detailRouteCode: { ...declaration.detailRouteCode } }
3755
+ : {}),
3468
3756
  fieldPolicies: Object.fromEntries(declaration.fields.map(field => [
3469
3757
  field.code,
3470
3758
  {
@@ -3483,6 +3771,7 @@ export function validateAppDeclaration(value) {
3483
3771
  const resources = Array.isArray(data.resources) ? data.resources : [];
3484
3772
  const diagnostics = [];
3485
3773
  const resourceKeys = new Set([
3774
+ 'views',
3486
3775
  'code',
3487
3776
  'name',
3488
3777
  'mutationOwner',
@@ -3494,6 +3783,7 @@ export function validateAppDeclaration(value) {
3494
3783
  'detail',
3495
3784
  'mobile',
3496
3785
  'dataPolicyCode',
3786
+ 'detailRouteCode',
3497
3787
  ]);
3498
3788
  const fieldKeys = new Set([
3499
3789
  'code',
@@ -3516,6 +3806,7 @@ export function validateAppDeclaration(value) {
3516
3806
  'widget',
3517
3807
  'section',
3518
3808
  'system',
3809
+ 'hidden',
3519
3810
  'list',
3520
3811
  'filter',
3521
3812
  'searchable',
@@ -3527,7 +3818,7 @@ export function validateAppDeclaration(value) {
3527
3818
  const resourcePath = `data.resources[${resourceIndex}]`;
3528
3819
  for (const key of Object.keys(resource)) {
3529
3820
  if (!resourceKeys.has(key)) {
3530
- diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields 及可选列表/布局/数据策略'));
3821
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_RESOURCE_PROPERTY_UNSUPPORTED', `${resourcePath}.${key} 不属于单次资源声明合同`, `${resourcePath}.${key}`, '只声明 code、name、fields 及可选列表/布局/数据策略/详情路由'));
3531
3822
  }
3532
3823
  }
3533
3824
  const fields = Array.isArray(resource.fields) ? resource.fields : [];
@@ -3595,12 +3886,6 @@ export function validateAppDeclaration(value) {
3595
3886
  else if (!rangeType && field.rangeBoundary !== undefined) {
3596
3887
  diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_RANGE_BOUNDARY_TYPE_INVALID', 'rangeBoundary 只能用于 date-range 或 datetime-range', `${fieldPath}.rangeBoundary`));
3597
3888
  }
3598
- if (field.indexed === false &&
3599
- (field.filter === true ||
3600
- field.searchable === true ||
3601
- field.sortable === true)) {
3602
- diagnostics.push(diagnostic('APP_CONFIG_DATA_FIELD_INDEX_REQUIRED', '启用筛选、搜索或排序时不能显式关闭索引', `${fieldPath}.indexed`));
3603
- }
3604
3889
  if (['radio', 'checkbox'].includes(widget) &&
3605
3890
  object(field.source).loadMode !== 'all' &&
3606
3891
  field.source !== undefined) {
@@ -3614,6 +3899,7 @@ export function validateAppDeclaration(value) {
3614
3899
  'required',
3615
3900
  'indexed',
3616
3901
  'system',
3902
+ 'hidden',
3617
3903
  'list',
3618
3904
  'filter',
3619
3905
  'searchable',
@@ -3642,6 +3928,27 @@ export function validateAppDeclaration(value) {
3642
3928
  if (code)
3643
3929
  declaredCodes.add(code);
3644
3930
  });
3931
+ for (const view of ['list', 'form', 'detail']) {
3932
+ const selected = object(resource[view]).fields;
3933
+ if (selected === undefined)
3934
+ continue;
3935
+ if (!Array.isArray(selected) || new Set(selected).size !== selected.length ||
3936
+ selected.some(code => typeof code !== 'string' || !declaredCodes.has(code))) {
3937
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_FIELDS_INVALID', '列表/表单/详情 fields 必须是当前资源中不重复的字段列表', `${resourcePath}.${view}.fields`));
3938
+ materializable = false;
3939
+ }
3940
+ }
3941
+ const selectedFormFields = object(resource.form).fields ?? fields.map(raw => object(raw).code);
3942
+ if (Array.isArray(selectedFormFields) && (generated.create ?? mutationOwner === 'native')) {
3943
+ for (const rawField of fields) {
3944
+ const field = object(rawField);
3945
+ if (field.required === true && field.system !== true &&
3946
+ supportsGeneratedMutation({ type: field.type, system: false }) &&
3947
+ object(field.access).create !== false && (field.hidden === true || !selectedFormFields.includes(field.code))) {
3948
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_FORM_REQUIRED_FIELD_MISSING', `标准新增表单缺少必填字段 ${field.label || field.code};请加入表单或关闭标准新增`, `${resourcePath}.form.fields`));
3949
+ }
3950
+ }
3951
+ }
3645
3952
  for (const operation of ['create', 'update']) {
3646
3953
  const enabled = generated[operation] ?? mutationOwner === 'native';
3647
3954
  const writable = fields.some(rawField => {
@@ -3663,6 +3970,19 @@ export function validateAppDeclaration(value) {
3663
3970
  }
3664
3971
  if (appCode && fields.length && materializable) {
3665
3972
  const canonical = materializeDataResource(appCode, rawResource);
3973
+ for (const [viewIndex, view] of (canonical.surface?.views || []).entries()) {
3974
+ for (const [property, values, supports] of [
3975
+ ['searchableFields', view.list?.searchableFields || [], supportsSearch],
3976
+ ['filterFields', view.list?.filterFields || [], supportsFilter],
3977
+ ['defaultSort', view.list?.defaultSort ? [view.list.defaultSort.field] : [], supportsSort],
3978
+ ]) {
3979
+ for (const code of values) {
3980
+ const field = canonical.schema.fields.find(field => field.code === code);
3981
+ if (!field || !supports(field.type))
3982
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_VIEW_QUERY_FIELD_INVALID', '视图查询字段不存在或不支持此查询类型', `${resourcePath}.views[${viewIndex}].list.${property}`));
3983
+ }
3984
+ }
3985
+ }
3666
3986
  diagnostics.push(...validateDataResource(canonical).map(item => ({
3667
3987
  ...item,
3668
3988
  path: `${resourcePath}.${item.path}`,
@@ -3730,24 +4050,69 @@ export function validateAppDeclaration(value) {
3730
4050
  return diagnostics;
3731
4051
  }
3732
4052
  export function defineOpenXiangdaApp(declaration) {
3733
- const declarationDiagnostics = validateAppDeclaration(declaration);
4053
+ const { modules, ...input } = declaration;
4054
+ const projected = materializeApplicationModules(modules || []);
4055
+ if (projected.diagnostics.length)
4056
+ throw new AppConfigValidationError(projected.diagnostics);
4057
+ const normalizedDeclaration = normalizeLegacyUserSurfaceAuthoring({
4058
+ ...input,
4059
+ schemaVersion: declaration.schemaVersion ?? 3,
4060
+ frontend: { root: 'apps/web', ...declaration.frontend },
4061
+ backend: {
4062
+ root: 'apps/server', runtime: 'node', framework: 'nestjs',
4063
+ enabled: Boolean(declaration.backend?.operations?.length || declaration.backend?.secrets?.length ||
4064
+ declaration.events?.subscriptions?.length || declaration.events?.timers?.length ||
4065
+ declaration.events?.dateTriggers?.length ||
4066
+ declaration.workflows?.providers?.length),
4067
+ ...declaration.backend,
4068
+ },
4069
+ platform: { root: 'platform', ...declaration.platform },
4070
+ ...(modules || declaration.data ? {
4071
+ data: { resources: [...(declaration.data?.resources || []), ...projected.resources] },
4072
+ } : {}),
4073
+ });
4074
+ const declarationDiagnostics = validateAppDeclaration(normalizedDeclaration);
3734
4075
  if (declarationDiagnostics.length > 0) {
3735
4076
  throw new AppConfigValidationError(declarationDiagnostics);
3736
4077
  }
3737
- const { data, ...application } = declaration;
3738
- const config = {
4078
+ const { data, ...application } = normalizedDeclaration;
4079
+ const materialized = {
3739
4080
  ...application,
3740
4081
  ...(data
3741
4082
  ? {
3742
4083
  data: {
3743
- resources: data.resources.map(resource => materializeDataResource(declaration.app.code, resource)),
4084
+ resources: data.resources.map(resource => materializeDataResource(normalizedDeclaration.app.code, resource)),
3744
4085
  },
3745
4086
  }
3746
4087
  : {}),
3747
4088
  };
4089
+ const config = materialized.authz
4090
+ ? {
4091
+ ...materialized,
4092
+ authz: {
4093
+ ...materialized.authz,
4094
+ roles: materialized.authz.roles.map(role => ({
4095
+ ...role,
4096
+ capabilities: materializeRoleCapabilities(role),
4097
+ })),
4098
+ },
4099
+ }
4100
+ : materialized;
3748
4101
  const diagnostics = validateAppConfig(config);
3749
4102
  if (diagnostics.length > 0)
3750
4103
  throw new AppConfigValidationError(diagnostics);
3751
- return Object.freeze(config);
4104
+ const sealed = config.authz
4105
+ ? {
4106
+ ...config,
4107
+ authz: {
4108
+ ...config.authz,
4109
+ roles: config.authz.roles.map(role => {
4110
+ const { deniedCapabilities: _deniedCapabilities, ...materializedRole } = role;
4111
+ return materializedRole;
4112
+ }),
4113
+ },
4114
+ }
4115
+ : config;
4116
+ return Object.freeze(sealed);
3752
4117
  }
3753
4118
  //# sourceMappingURL=config.js.map