openxiangda-devkit-core 2.0.0-alpha.73 → 2.0.0-alpha.76

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.
@@ -38,6 +38,9 @@ export function adminResourcePage(resourceCode, options) {
38
38
  export function adminOperationPage(routeCode, options) {
39
39
  return adminNavigationItem({ kind: 'operation', routeCode }, options);
40
40
  }
41
+ export function adminApplicationTodoCenterPage(options) {
42
+ return adminNavigationItem({ kind: 'application-todo-center' }, options);
43
+ }
41
44
  export function adminWorkflowWorkCenterPage(options) {
42
45
  return adminNavigationItem({ kind: 'workflow-work-center' }, options);
43
46
  }
@@ -121,6 +124,10 @@ export function renderAdminNavigationSuggestion(config) {
121
124
  imports.add('adminOperationPage');
122
125
  expression = `adminOperationPage(${JSON.stringify(item.page.routeCode)}${options ? `, ${options}` : ''})`;
123
126
  break;
127
+ case 'application-todo-center':
128
+ imports.add('adminApplicationTodoCenterPage');
129
+ expression = `adminApplicationTodoCenterPage(${options || ''})`;
130
+ break;
124
131
  case 'workflow-work-center':
125
132
  imports.add('adminWorkflowWorkCenterPage');
126
133
  expression = `adminWorkflowWorkCenterPage(${options || ''})`;
@@ -761,7 +768,7 @@ export function validateAppConfig(value) {
761
768
  !['light', 'standard'].includes(String(backend.resourceProfile))) {
762
769
  diagnostics.push(diagnostic('APP_CONFIG_BACKEND_RESOURCE_PROFILE_UNSUPPORTED', 'backend.resourceProfile 必须是 light 或 standard', 'backend.resourceProfile'));
763
770
  }
764
- validateFrontendRoutes(frontend.routes, diagnostics);
771
+ validateFrontendRoutes(config, diagnostics);
765
772
  validateAdminNavigation(config, diagnostics);
766
773
  validatePerspectives(config.perspectives, object(config.authz).roles, diagnostics);
767
774
  const declaredResourceCodes = new Set((Array.isArray(object(config.data).resources)
@@ -1740,6 +1747,10 @@ export function validateAppConfig(value) {
1740
1747
  const editableParameters = Array.isArray(workflows.editableParameters)
1741
1748
  ? workflows.editableParameters
1742
1749
  : [];
1750
+ const frontendRoutesByCode = new Map((Array.isArray(frontend.routes) ? frontend.routes : []).map(routeValue => {
1751
+ const route = object(routeValue);
1752
+ return [string(route.code), route];
1753
+ }));
1743
1754
  const definitionByKey = new Map();
1744
1755
  const bindingByKey = new Map();
1745
1756
  const providerKeys = new Set();
@@ -1791,6 +1802,9 @@ export function validateAppConfig(value) {
1791
1802
  const version = Number(declaration.version);
1792
1803
  const path = `workflows.definitions[${index}]`;
1793
1804
  const launch = object(declaration.launch);
1805
+ if (Object.keys(declaration).some(key => !['version', 'definition', 'launch', 'detailRouteCode'].includes(key))) {
1806
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DEFINITION_DECLARATION_INVALID', 'Workflow definition declaration 只能声明 version、definition、launch 和 detailRouteCode', path));
1807
+ }
1794
1808
  if (declaration.launch !== undefined &&
1795
1809
  (Object.keys(launch).some(key => key !== 'mode') ||
1796
1810
  ![
@@ -1801,6 +1815,41 @@ export function validateAppConfig(value) {
1801
1815
  ].includes(string(launch.mode)))) {
1802
1816
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_LAUNCH_MODE_INVALID', 'Workflow launch.mode 必须是 standalone、custom-page、hidden-handoff 或 work-center-only', `${path}.launch.mode`));
1803
1817
  }
1818
+ if (declaration.detailRouteCode !== undefined) {
1819
+ const detailRouteCode = object(declaration.detailRouteCode);
1820
+ const desktopCode = string(detailRouteCode.desktop);
1821
+ const mobileCode = string(detailRouteCode.mobile);
1822
+ if (Object.keys(detailRouteCode).some(key => !['desktop', 'mobile'].includes(key)) ||
1823
+ !OPERATION_CODE_PATTERN.test(desktopCode) ||
1824
+ !OPERATION_CODE_PATTERN.test(mobileCode) ||
1825
+ desktopCode === mobileCode) {
1826
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_INVALID', 'detailRouteCode 必须声明两个不同且合法的 desktop/mobile route code', `${path}.detailRouteCode`));
1827
+ }
1828
+ else {
1829
+ for (const [device, code, surface] of [
1830
+ ['desktop', desktopCode, 'admin'],
1831
+ ['mobile', mobileCode, 'user'],
1832
+ ]) {
1833
+ const route = frontendRoutesByCode.get(code);
1834
+ const routePointer = `${path}.detailRouteCode.${device}`;
1835
+ if (!route) {
1836
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_REFERENCE_MISSING', `detailRouteCode.${device} 必须引用已声明 frontend route`, routePointer));
1837
+ continue;
1838
+ }
1839
+ if (string(route.surface) !== surface) {
1840
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_SURFACE_INVALID', `detailRouteCode.${device} 必须引用 ${surface} surface`, routePointer));
1841
+ }
1842
+ const parameters = [
1843
+ ...string(route.path).matchAll(/:([A-Za-z][A-Za-z0-9_]*)/g),
1844
+ ].map(match => match[1]);
1845
+ if (string(route.path).includes('*') ||
1846
+ parameters.length !== 1 ||
1847
+ parameters[0] !== 'instanceId') {
1848
+ diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_DETAIL_ROUTE_PATH_INVALID', `detailRouteCode.${device} 路由必须且只能声明 :instanceId 动态参数`, routePointer));
1849
+ }
1850
+ }
1851
+ }
1852
+ }
1804
1853
  if (!Number.isInteger(version) || version < 1) {
1805
1854
  diagnostics.push(diagnostic('APP_CONFIG_WORKFLOW_VERSION_INVALID', 'Workflow definition version 必须是正整数', `${path}.version`));
1806
1855
  }
@@ -2048,14 +2097,108 @@ function validateBackendOperations(rawOperations, declaredResourceCodes, diagnos
2048
2097
  routes.add(routeKey);
2049
2098
  });
2050
2099
  }
2051
- function validateFrontendRoutes(rawRoutes, diagnostics) {
2100
+ function canonicalFrontendRouteShape(path) {
2101
+ const normalized = path.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
2102
+ return normalized
2103
+ .split('/')
2104
+ .map(segment => segment.startsWith(':') ? ':' : segment.includes('*') ? '*' : segment)
2105
+ .join('/');
2106
+ }
2107
+ function generatedPlatformRouteClaims(config) {
2108
+ const claims = [
2109
+ { owner: 'platform:admin-entry', path: '/' },
2110
+ { owner: 'platform:admin-entry', path: '/admin' },
2111
+ {
2112
+ owner: 'platform:file-preview',
2113
+ path: '/files/:resourceCode/:fileId/preview',
2114
+ },
2115
+ ];
2116
+ const data = object(config.data);
2117
+ const resources = Array.isArray(data.resources) ? data.resources : [];
2118
+ for (const rawResource of resources) {
2119
+ const resource = object(rawResource);
2120
+ const code = string(resource.code);
2121
+ if (!code)
2122
+ continue;
2123
+ const surface = object(resource.surface);
2124
+ const generated = object(surface.generated);
2125
+ const native = !surface.mutationOwner || surface.mutationOwner === 'native';
2126
+ const operations = {
2127
+ list: generated.list === undefined ? true : generated.list === true,
2128
+ detail: generated.detail === undefined ? true : generated.detail === true,
2129
+ create: generated.create === undefined ? native : generated.create === true,
2130
+ update: generated.update === undefined ? native : generated.update === true,
2131
+ };
2132
+ const base = `/admin/resources/${code}`;
2133
+ const candidates = [
2134
+ ['list', base],
2135
+ ['detail', `${base}/:id`],
2136
+ ['create', `${base}/new`],
2137
+ ['update', `${base}/:id/edit`],
2138
+ ];
2139
+ for (const [operation, path] of candidates) {
2140
+ if (!operations[operation])
2141
+ continue;
2142
+ claims.push({ owner: `resource:${code}:${operation}`, path }, { owner: `resource:${code}:${operation}:mobile`, path: `/m${path}` });
2143
+ }
2144
+ }
2145
+ const frontend = object(config.frontend);
2146
+ const admin = object(frontend.admin);
2147
+ const navigation = Array.isArray(admin.navigation) ? admin.navigation : [];
2148
+ const hasTodoCenter = navigation.some(rawGroup => {
2149
+ const group = object(rawGroup);
2150
+ const items = Array.isArray(group.items) ? group.items : [];
2151
+ return items.some(rawItem => string(object(object(rawItem).page).kind) === 'application-todo-center');
2152
+ });
2153
+ if (hasTodoCenter) {
2154
+ claims.push({ owner: 'application:todo-center', path: '/admin/todos' }, { owner: 'application:todo-center:mobile', path: '/m/todos' });
2155
+ }
2156
+ const workflows = object(config.workflows);
2157
+ const definitions = Array.isArray(workflows.definitions)
2158
+ ? workflows.definitions
2159
+ : [];
2160
+ if (definitions.length > 0) {
2161
+ claims.push({ owner: 'workflow:work-center', path: '/admin/work-center' }, { owner: 'workflow:work-center:mobile', path: '/m/work-center' }, { owner: 'workflow:task', path: '/admin/tasks/:taskId' }, { owner: 'workflow:task:mobile', path: '/m/tasks/:taskId' }, { owner: 'workflow:instance', path: '/admin/workflows/:instanceId' }, {
2162
+ owner: 'workflow:instance:mobile',
2163
+ path: '/m/workflows/:instanceId',
2164
+ });
2165
+ const standaloneCodes = new Set();
2166
+ for (const rawDefinition of definitions) {
2167
+ const declaration = object(rawDefinition);
2168
+ if (string(object(declaration.launch).mode) !== 'standalone')
2169
+ continue;
2170
+ const workflowCode = string(object(declaration.definition).code);
2171
+ if (workflowCode)
2172
+ standaloneCodes.add(workflowCode);
2173
+ }
2174
+ for (const workflowCode of standaloneCodes) {
2175
+ claims.push({
2176
+ owner: `workflow:${workflowCode}:launch`,
2177
+ path: `/admin/workflows/${workflowCode}/new`,
2178
+ });
2179
+ }
2180
+ if (standaloneCodes.size > 0) {
2181
+ claims.push({
2182
+ owner: 'workflow:launch:mobile',
2183
+ path: '/m/workflows/:workflowCode/start',
2184
+ });
2185
+ }
2186
+ }
2187
+ return claims;
2188
+ }
2189
+ function validateFrontendRoutes(config, diagnostics) {
2190
+ const frontend = object(config.frontend);
2191
+ const rawRoutes = frontend.routes;
2052
2192
  if (rawRoutes !== undefined && !Array.isArray(rawRoutes)) {
2053
2193
  diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTES_INVALID', 'frontend.routes 必须是数组', 'frontend.routes'));
2054
2194
  return;
2055
2195
  }
2056
2196
  const routes = Array.isArray(rawRoutes) ? rawRoutes : [];
2057
2197
  const codes = new Set();
2058
- const paths = new Set();
2198
+ const pathClaims = new Map();
2199
+ for (const claim of generatedPlatformRouteClaims(config)) {
2200
+ pathClaims.set(canonicalFrontendRouteShape(claim.path), claim);
2201
+ }
2059
2202
  routes.forEach((raw, index) => {
2060
2203
  const route = object(raw);
2061
2204
  const code = string(route.code);
@@ -2103,17 +2246,31 @@ function validateFrontendRoutes(rawRoutes, diagnostics) {
2103
2246
  if (!OPERATION_CODE_PATTERN.test(code) ||
2104
2247
  codes.has(code) ||
2105
2248
  !validAppPath(routePath) ||
2106
- paths.has(routePath) ||
2107
2249
  !string(route.label) ||
2108
2250
  !['admin', 'user'].includes(string(route.surface)) ||
2251
+ (route.surface === 'admin' &&
2252
+ routePath !== '/admin' &&
2253
+ !routePath.startsWith('/admin/')) ||
2254
+ (route.surface === 'user' &&
2255
+ (routePath === '/admin' || routePath.startsWith('/admin/'))) ||
2109
2256
  (route.capability !== undefined &&
2110
2257
  !CAPABILITY_PATTERN.test(string(route.capability))) ||
2111
2258
  accessInvalid ||
2112
2259
  routePolicyInvalid) {
2113
2260
  diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTE_INVALID', '前端 route 必须声明唯一 code/path、label、surface、有效访问表达式和有界标签策略;动态 route 不能固定、恢复或保活', path));
2114
2261
  }
2262
+ const shape = canonicalFrontendRouteShape(routePath);
2263
+ const existing = pathClaims.get(shape);
2264
+ if (routePath && existing) {
2265
+ diagnostics.push(diagnostic('APP_CONFIG_FRONTEND_ROUTE_PATH_CONFLICT', `前端 route path 与 ${existing.owner} 冲突: ${routePath}`, `${path}.path`));
2266
+ }
2267
+ else if (routePath) {
2268
+ pathClaims.set(shape, {
2269
+ owner: `frontend:${code || index}`,
2270
+ path: routePath,
2271
+ });
2272
+ }
2115
2273
  codes.add(code);
2116
- paths.add(routePath);
2117
2274
  });
2118
2275
  const parentByCode = new Map();
2119
2276
  routes.forEach((raw, index) => {
@@ -2239,6 +2396,12 @@ function validateAdminNavigation(config, diagnostics) {
2239
2396
  internalCode = routeCode;
2240
2397
  exists = Boolean(route && route.surface === 'admin' && !/[:*]/.test(route.path));
2241
2398
  }
2399
+ else if (kind === 'application-todo-center') {
2400
+ pageKey = 'application:todo-center';
2401
+ defaultLabel = '待办中心';
2402
+ internalCode = pageKey;
2403
+ exists = true;
2404
+ }
2242
2405
  else if (kind === 'workflow-work-center') {
2243
2406
  pageKey = 'workflow:work-center';
2244
2407
  defaultLabel = '我的审批';
@@ -2255,7 +2418,13 @@ function validateAdminNavigation(config, diagnostics) {
2255
2418
  exists = Boolean(workflow && workflow.mode === 'standalone');
2256
2419
  }
2257
2420
  if (Object.keys(item).some(key => !['page', 'label', 'icon', 'order'].includes(key)) ||
2258
- !['resource', 'operation', 'workflow-work-center', 'workflow-launch'].includes(kind) ||
2421
+ ![
2422
+ 'resource',
2423
+ 'operation',
2424
+ 'application-todo-center',
2425
+ 'workflow-work-center',
2426
+ 'workflow-launch',
2427
+ ].includes(kind) ||
2259
2428
  (item.icon !== undefined &&
2260
2429
  !ADMIN_NAVIGATION_ICONS.has(string(item.icon))) ||
2261
2430
  !validAdminOrder(item.order)) {