openxiangda-devkit-core 2.11.0 → 2.13.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.
@@ -2441,7 +2441,7 @@ function validateCapabilityDeclarations(appCode, declarations, diagnostics) {
2441
2441
  diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_CAPABILITY_DECLARATION_INVALID', '显式 capability 必须属于当前应用、code 唯一,并声明 backend/ui kind 与 name', path));
2442
2442
  }
2443
2443
  if (platformCapabilityCodes(appCode).has(code)) {
2444
- diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_PLATFORM_CAPABILITY_RESERVED', '平台 capability 由平台保留 catalog 提供,应用不能重复声明', `${path}.code`));
2444
+ diagnostics.push(diagnostic('APP_CONFIG_AUTHZ_PLATFORM_CAPABILITY_RESERVED', '平台 capability 由平台保留 catalog 提供,应用不能重复声明', `${path}.code`, `删除 capabilities 里的该声明,直接在角色中引用即可,例如 roles: [{ code: 'admin', capabilities: [directoryRead] }],其中 directoryRead = app:${appCode}:directory:read`));
2445
2445
  }
2446
2446
  seen.add(code);
2447
2447
  });
@@ -2649,7 +2649,7 @@ function validateBackendOperations(rawOperations, declaredResources, declaredWor
2649
2649
  Number(ai.timeoutMs) < 100 ||
2650
2650
  Number(ai.timeoutMs) > 30000)) ||
2651
2651
  aiKeys.some(key => !allowedAiKeys.has(key))) {
2652
- diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_AI_INVALID', 'AI 操作必须显式声明名称、说明、风险、现有资源、副作用和有界执行策略;GET 只能只读,写操作必须确认', `${path}.ai`));
2652
+ diagnostics.push(diagnostic('APP_CONFIG_BACKEND_OPERATION_AI_INVALID', 'AI 操作必须显式声明名称、说明、风险、现有资源、副作用和有界执行策略;GET 只能只读,写操作必须确认', `${path}.ai`, '写操作的 ai.sideEffects 至少列一条具体副作用,例如 sideEffects: [\'更新报修单状态为处理中\', \'写入一条派工记录\'];resources 必须引用已声明资源(1-16 个)'));
2653
2653
  }
2654
2654
  }
2655
2655
  codes.add(code);
@@ -3297,6 +3297,13 @@ function validateApplicationAuthentication(config, diagnostics) {
3297
3297
  return [string(route.code), route];
3298
3298
  }));
3299
3299
  const routeCodes = new Set(routeByCode.keys());
3300
+ // 生成式用户标准面的路由码(仅参与登录默认路由解析,不占用声明码空间)。
3301
+ const declaredResources = (Array.isArray(object(config.data).resources)
3302
+ ? object(config.data).resources
3303
+ : []);
3304
+ const userSurfaceResourceCodes = new Set(declaredResources
3305
+ .filter(raw => object(object(raw).userSurface))
3306
+ .map(raw => string(object(raw).code)));
3300
3307
  const claimedShapes = new Map(generatedPlatformRouteClaims(config).map(claim => [
3301
3308
  canonicalFrontendRouteShape(claim.path),
3302
3309
  claim,
@@ -3309,7 +3316,21 @@ function validateApplicationAuthentication(config, diagnostics) {
3309
3316
  const routeCode = string(surface.routeCode);
3310
3317
  const routePath = string(surface.path);
3311
3318
  const defaultRouteCode = string(surface.defaultRouteCode);
3312
- const defaultRoute = routeByCode.get(defaultRouteCode);
3319
+ const generatedUserRoute = /^user:([a-z][a-z0-9-]{0,62}):(records|submit)$/.exec(defaultRouteCode);
3320
+ const defaultRoute = generatedUserRoute
3321
+ ? userSurfaceResourceCodes.has(generatedUserRoute[1])
3322
+ ? {
3323
+ surface: 'user',
3324
+ path: generatedUserRoute[2] === 'records'
3325
+ ? device === 'mobile'
3326
+ ? `/m/my/${generatedUserRoute[1]}`
3327
+ : `/my/${generatedUserRoute[1]}`
3328
+ : device === 'mobile'
3329
+ ? `/m/my/${generatedUserRoute[1]}/submit`
3330
+ : `/my/${generatedUserRoute[1]}/submit`,
3331
+ }
3332
+ : undefined
3333
+ : routeByCode.get(defaultRouteCode);
3313
3334
  const shape = canonicalFrontendRouteShape(routePath);
3314
3335
  const expectedPath = device === 'mobile' ? '/m/login' : '/login';
3315
3336
  const invalid = Object.keys(surface).some(key => !['routeCode', 'path', 'defaultRouteCode'].includes(key)) ||
@@ -3730,6 +3751,12 @@ export function resourceReadPolicy(input) {
3730
3751
  }
3731
3752
  export function materializeDataResource(appCode, declaration) {
3732
3753
  const capabilities = resourceCapabilityCodes(appCode, declaration.code);
3754
+ // audit.read: true 是声明糖:绑定本资源的 read 能力。
3755
+ const auditRead = declaration.audit === undefined || declaration.audit.read === false
3756
+ ? false
3757
+ : declaration.audit.read === true
3758
+ ? [capabilities.read]
3759
+ : [...declaration.audit.read];
3733
3760
  const mutationOwner = declaration.mutationOwner || 'native';
3734
3761
  const nativeMutations = mutationOwner === 'native';
3735
3762
  const generated = {
@@ -3876,6 +3903,7 @@ export function materializeDataResource(appCode, declaration) {
3876
3903
  ...(declaration.dataPolicyCode
3877
3904
  ? { dataPolicyCode: declaration.dataPolicyCode }
3878
3905
  : {}),
3906
+ ...(declaration.userSurface ? { userSurface: { ...declaration.userSurface } } : {}),
3879
3907
  ...(declaration.detailRouteCode
3880
3908
  ? { detailRouteCode: { ...declaration.detailRouteCode } }
3881
3909
  : {}),
@@ -3890,7 +3918,7 @@ export function materializeDataResource(appCode, declaration) {
3890
3918
  },
3891
3919
  ])),
3892
3920
  ...(declaration.audit === undefined ? {} : Object.fromEntries(DATA_AUDIT_METADATA_FIELDS.map(code => [code, {
3893
- read: declaration.audit.read === false ? [] : [...declaration.audit.read],
3921
+ read: auditRead === false ? [] : [...auditRead],
3894
3922
  }]))),
3895
3923
  },
3896
3924
  };
@@ -3915,6 +3943,7 @@ export function validateAppDeclaration(value) {
3915
3943
  'mobile',
3916
3944
  'dataPolicyCode',
3917
3945
  'detailRouteCode',
3946
+ 'userSurface',
3918
3947
  'audit',
3919
3948
  ]);
3920
3949
  const fieldKeys = new Set([
@@ -3960,10 +3989,21 @@ export function validateAppDeclaration(value) {
3960
3989
  const read = audit.read;
3961
3990
  if (!resource.audit || typeof resource.audit !== 'object' || Array.isArray(resource.audit) ||
3962
3991
  Object.keys(audit).some(key => key !== 'read') ||
3963
- (read !== false && (!Array.isArray(read) || read.length < 1 || read.length > 20 ||
3992
+ (read !== false && read !== true && (!Array.isArray(read) || read.length < 1 || read.length > 20 ||
3964
3993
  read.some(value => typeof value !== 'string' || !value.trim()) || new Set(read).size !== read.length))) {
3965
3994
  auditValid = false;
3966
- diagnostics.push(diagnostic('APP_CONFIG_DATA_AUDIT_READ_INVALID', 'audit.read 必须为 false 或不重复的非空 capability 数组(最多 20 项)', `${resourcePath}.audit`));
3995
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_AUDIT_READ_INVALID', 'audit.read 必须为 true(绑定资源读能力)、false 或不重复的非空 capability 数组(最多 20 项)', `${resourcePath}.audit`, `audit: { read: true } 会把审计读取绑定到本资源的 read 能力;需要更细粒度时写能力数组,例如 audit: { read: ['app:${appCode}:data:<resource>:read'] }`));
3996
+ }
3997
+ }
3998
+ const userSurface = resource.userSurface;
3999
+ if (userSurface !== undefined) {
4000
+ const surface = object(userSurface);
4001
+ const keys = Object.keys(surface).filter(key => !['home', 'listLabel', 'submitLabel'].includes(key));
4002
+ const labels = [surface.listLabel, surface.submitLabel];
4003
+ if (keys.length ||
4004
+ (surface.home !== undefined && typeof surface.home !== 'boolean') ||
4005
+ labels.some(label => label !== undefined && (typeof label !== 'string' || !label.trim() || label.length > 64))) {
4006
+ diagnostics.push(diagnostic('APP_CONFIG_DATA_USER_SURFACE_INVALID', 'userSurface 只接受 home 布尔与 listLabel/submitLabel(1-64 字符)', `${resourcePath}.userSurface`, `例如 userSurface: { home: true, listLabel: '我的领用', submitLabel: '新建领用' };直接在 CRUD 视图上写 user: true 即可自动生成`));
3967
4007
  }
3968
4008
  }
3969
4009
  const mutationOwner = string(resource.mutationOwner) || 'native';
@@ -4215,6 +4255,23 @@ export function defineOpenXiangdaApp(declaration) {
4215
4255
  data: { resources: [...(declaration.data?.resources || []), ...projected.resources] },
4216
4256
  } : {}),
4217
4257
  });
4258
+ // 生成式用户标准面:登录默认路由仍是模板占位时,指向首页资源的标准页。
4259
+ const homeResourceCode = (normalizedDeclaration.data?.resources || [])
4260
+ .filter(resource => resource.userSurface)
4261
+ .sort((left, right) => left.code.localeCompare(right.code))
4262
+ .find(resource => resource.userSurface?.home === true)?.code
4263
+ || (normalizedDeclaration.data?.resources || [])
4264
+ .filter(resource => resource.userSurface)
4265
+ .sort((left, right) => left.code.localeCompare(right.code))[0]?.code;
4266
+ if (homeResourceCode && normalizedDeclaration.frontend.authentication) {
4267
+ const surfaces = normalizedDeclaration.frontend.authentication.surfaces;
4268
+ if (surfaces.desktop.defaultRouteCode === 'application-home') {
4269
+ surfaces.desktop.defaultRouteCode = `user:${homeResourceCode}:records`;
4270
+ }
4271
+ if (surfaces.mobile.defaultRouteCode === 'application-home-mobile') {
4272
+ surfaces.mobile.defaultRouteCode = `user:${homeResourceCode}:records`;
4273
+ }
4274
+ }
4218
4275
  const declarationDiagnostics = validateAppDeclaration(normalizedDeclaration);
4219
4276
  if (declarationDiagnostics.length > 0) {
4220
4277
  throw new AppConfigValidationError(declarationDiagnostics);