openxiangda 1.0.136 → 1.0.138

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 (28) hide show
  1. package/lib/cli.js +687 -5
  2. package/lib/design-gates.js +16 -3
  3. package/openxiangda-skills/references/best-practices.md +18 -7
  4. package/openxiangda-skills/references/component-guide.md +7 -1
  5. package/openxiangda-skills/references/permission-design-patterns.md +95 -5
  6. package/openxiangda-skills/references/permissions-settings.md +165 -0
  7. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +87 -0
  8. package/package.json +1 -1
  9. package/packages/sdk/dist/{ProcessPreview-Ci8_UsbN.d.mts → ProcessPreview-CZJP8n3e.d.mts} +6 -0
  10. package/packages/sdk/dist/{ProcessPreview-Ci8_UsbN.d.ts → ProcessPreview-CZJP8n3e.d.ts} +6 -0
  11. package/packages/sdk/dist/components/index.cjs +143 -16
  12. package/packages/sdk/dist/components/index.cjs.map +1 -1
  13. package/packages/sdk/dist/components/index.d.mts +8 -2
  14. package/packages/sdk/dist/components/index.d.ts +8 -2
  15. package/packages/sdk/dist/components/index.mjs +143 -16
  16. package/packages/sdk/dist/components/index.mjs.map +1 -1
  17. package/packages/sdk/dist/runtime/index.cjs +169 -25
  18. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  19. package/packages/sdk/dist/runtime/index.d.mts +1 -1
  20. package/packages/sdk/dist/runtime/index.d.ts +1 -1
  21. package/packages/sdk/dist/runtime/index.mjs +169 -25
  22. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  23. package/packages/sdk/dist/runtime/react.cjs +26 -9
  24. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  25. package/packages/sdk/dist/runtime/react.d.mts +13 -3
  26. package/packages/sdk/dist/runtime/react.d.ts +13 -3
  27. package/packages/sdk/dist/runtime/react.mjs +26 -9
  28. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
package/lib/cli.js CHANGED
@@ -93,6 +93,7 @@ async function main(argv) {
93
93
  if (command === 'workflow') return workflow(rest);
94
94
  if (command === 'automation') return automation(rest);
95
95
  if (command === 'data-view') return dataView(rest);
96
+ if (command === 'scope') return scope(rest);
96
97
  if (command === 'route') return route(rest);
97
98
  if (command === 'public-access') return publicAccess(rest);
98
99
  if (command === 'auth-config') return authConfig(rest);
@@ -162,6 +163,7 @@ Usage:
162
163
  openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
163
164
  openxiangda automation publish|enable|disable <automationCode|automationId>
164
165
  openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats <dataViewCode> [--profile name] [--json]
166
+ openxiangda scope dimension-list|dimension-upsert|grant-source-list|grant-source-upsert|policy-list|policy-upsert|sync|explain [code] [--json-file file]
165
167
  openxiangda route list|get|create|update|upsert|delete [--json-file file] [--dry-run] [--write-manifest]
166
168
  openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [--json-file file]
167
169
  openxiangda auth-config list|get|create|update|upsert|delete|methods [--json-file file]
@@ -1860,6 +1862,9 @@ async function workspace(args) {
1860
1862
  connectors: {},
1861
1863
  dataViews: {},
1862
1864
  storageConfigs: {},
1865
+ scopeDimensions: {},
1866
+ scopeGrantSources: {},
1867
+ dataScopePolicies: {},
1863
1868
  pagePermissionGroups: {},
1864
1869
  formPermissionGroups: {},
1865
1870
  formSettings: {},
@@ -3101,6 +3106,112 @@ async function dataView(args) {
3101
3106
  fail('用法: openxiangda data-view list|get|create|update|upsert|delete|status|refresh|query|stats');
3102
3107
  }
3103
3108
 
3109
+ async function scope(args) {
3110
+ const { subcommand, rest } = parseSubcommandArgs(args);
3111
+ const { flags, positional } = parseArgs(rest);
3112
+ if (wantsSubcommandHelp(subcommand, flags)) {
3113
+ print('用法: openxiangda scope dimension-list|dimension-upsert|grant-source-list|grant-source-upsert|policy-list|policy-upsert|sync|explain [code] [--json-file file] [--dry-run] [--write-manifest]');
3114
+ return;
3115
+ }
3116
+ const config = loadConfig();
3117
+ const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
3118
+ const basePath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope`;
3119
+
3120
+ if (subcommand === 'dimension-list') {
3121
+ return runDirectRequest(config, target, flags, {
3122
+ method: 'GET',
3123
+ path: `${basePath}/dimensions`,
3124
+ });
3125
+ }
3126
+
3127
+ if (subcommand === 'dimension-upsert') {
3128
+ return upsertScopeResource(config, target, flags, positional, {
3129
+ commandName: 'scope dimension-upsert',
3130
+ resourceType: 'scope-dimension',
3131
+ path: `${basePath}/dimensions`,
3132
+ normalizeBody: body => normalizeScopeDimensionManifest(body, target.bound),
3133
+ });
3134
+ }
3135
+
3136
+ if (subcommand === 'grant-source-list') {
3137
+ return runDirectRequest(config, target, flags, {
3138
+ method: 'GET',
3139
+ path: `${basePath}/grant-sources`,
3140
+ });
3141
+ }
3142
+
3143
+ if (subcommand === 'grant-source-upsert') {
3144
+ return upsertScopeResource(config, target, flags, positional, {
3145
+ commandName: 'scope grant-source-upsert',
3146
+ resourceType: 'scope-grant-source',
3147
+ path: `${basePath}/grant-sources`,
3148
+ normalizeBody: body => normalizeScopeGrantSourceManifest(body, target.bound),
3149
+ });
3150
+ }
3151
+
3152
+ if (subcommand === 'policy-list') {
3153
+ return runDirectRequest(config, target, flags, {
3154
+ method: 'GET',
3155
+ path: `${basePath}/policies`,
3156
+ });
3157
+ }
3158
+
3159
+ if (subcommand === 'policy-upsert') {
3160
+ return upsertScopeResource(config, target, flags, positional, {
3161
+ commandName: 'scope policy-upsert',
3162
+ resourceType: 'data-scope-policy',
3163
+ path: `${basePath}/policies`,
3164
+ normalizeBody: normalizeDataScopePolicyManifest,
3165
+ });
3166
+ }
3167
+
3168
+ if (subcommand === 'sync') {
3169
+ const [code] = positional;
3170
+ if (!code) fail('用法: openxiangda scope sync <grantSourceCode> [--dry-run] [--json]');
3171
+ const body = {
3172
+ ...readDirectJsonBody(flags, 'scope sync', { optional: true }),
3173
+ ...(flags['dry-run'] ? { dryRun: true } : {}),
3174
+ };
3175
+ return runDirectRequest(config, target, flags, {
3176
+ method: 'POST',
3177
+ path: `${basePath}/grant-sources/${encodeURIComponent(code)}/sync`,
3178
+ body,
3179
+ });
3180
+ }
3181
+
3182
+ if (subcommand === 'explain') {
3183
+ const [code] = positional;
3184
+ if (!code) fail('用法: openxiangda scope explain <policyCode> [--json-file file] [--json]');
3185
+ const body = readDirectJsonBody(flags, 'scope explain', { optional: true });
3186
+ return runDirectRequest(config, target, flags, {
3187
+ method: 'POST',
3188
+ path: `${basePath}/policies/${encodeURIComponent(code)}/explain`,
3189
+ body,
3190
+ });
3191
+ }
3192
+
3193
+ fail('用法: openxiangda scope dimension-list|dimension-upsert|grant-source-list|grant-source-upsert|policy-list|policy-upsert|sync|explain');
3194
+ }
3195
+
3196
+ async function upsertScopeResource(config, target, flags, positional, spec) {
3197
+ const rawBody = readDirectJsonBody(flags, spec.commandName);
3198
+ const explicitCode = positional[0] || flags.code;
3199
+ if (explicitCode && !rawBody.code && !rawBody.resourceCode) rawBody.code = explicitCode;
3200
+ const code = explicitCode || rawBody.code || rawBody.resourceCode;
3201
+ if (!code) fail(`openxiangda ${spec.commandName} 缺少资源 code`);
3202
+ const body = spec.normalizeBody(rawBody);
3203
+ const data = await runDirectRequest(config, target, flags, {
3204
+ method: 'POST',
3205
+ path: `${spec.path}/${encodeURIComponent(code)}`,
3206
+ body,
3207
+ }, { returnData: true });
3208
+ if (!flags['dry-run']) {
3209
+ saveScopeResource(target, resourceTypeToBucket(spec.resourceType), code, data);
3210
+ if (flags['write-manifest']) writeDirectManifest(spec.resourceType, code, { ...rawBody, code });
3211
+ }
3212
+ return outputDirectResult({ action: 'upsert', data }, flags);
3213
+ }
3214
+
3104
3215
  async function route(args) {
3105
3216
  const { subcommand, rest } = parseSubcommandArgs(args);
3106
3217
  const { flags, positional } = parseArgs(rest);
@@ -5579,6 +5690,9 @@ function directManifestDir(resourceType) {
5579
5690
  role: path.join('src', 'resources', 'roles'),
5580
5691
  'page-permission-group': path.join('src', 'resources', 'permissions', 'page-groups'),
5581
5692
  'form-permission-group': path.join('src', 'resources', 'permissions', 'form-groups'),
5693
+ 'scope-dimension': path.join('src', 'resources', 'permissions', 'scope-dimensions'),
5694
+ 'scope-grant-source': path.join('src', 'resources', 'permissions', 'scope-grant-sources'),
5695
+ 'data-scope-policy': path.join('src', 'resources', 'permissions', 'data-scope-policies'),
5582
5696
  };
5583
5697
  return dirs[resourceType];
5584
5698
  }
@@ -5738,6 +5852,8 @@ async function buildPermissionAudit(config, target, flags = {}) {
5738
5852
  const manifest = loadWorkspaceResources();
5739
5853
  const validation = validateWorkspaceResources(manifest);
5740
5854
  const roleCodes = new Set((manifest.roles || []).map(item => item.code || item.resourceCode).filter(Boolean));
5855
+ const policyCodes = new Set((manifest.dataScopePolicies || []).map(item => item.code || item.resourceCode).filter(Boolean));
5856
+ const scopePolicyRefs = [];
5741
5857
  const warnings = [...validation.warnings];
5742
5858
  const errors = [...validation.errors];
5743
5859
  for (const group of manifest.pagePermissionGroups || []) {
@@ -5751,6 +5867,31 @@ async function buildPermissionAudit(config, target, flags = {}) {
5751
5867
  }
5752
5868
  const formUuid = resolveManifestFormUuid(target.bound, group);
5753
5869
  if (!formUuid) errors.push(`form permission group ${group.code} 缺少 formCode/formUuid`);
5870
+ if (group.dataPermission?.type === 'scope_policy') {
5871
+ const policyCode = getScopePolicyCode(group.dataPermission);
5872
+ if (policyCode) scopePolicyRefs.push(policyCode);
5873
+ if (policyCode && policyCodes.size > 0 && !policyCodes.has(policyCode)) {
5874
+ warnings.push(`form permission group ${group.code} 引用未声明 data-scope-policy: ${policyCode}`);
5875
+ }
5876
+ }
5877
+ }
5878
+ for (const dataView of manifest.dataViews || []) {
5879
+ for (const group of dataView.permissionGroups || []) {
5880
+ for (const roleCode of group.roles || []) {
5881
+ if (roleCodes.size > 0 && !roleCodes.has(roleCode)) {
5882
+ warnings.push(`data view ${dataView.code} permission group ${group.code || group.name || '?'} 引用未声明角色: ${roleCode}`);
5883
+ }
5884
+ }
5885
+ if (group.dataPermission?.type !== 'scope_policy') continue;
5886
+ const policyCode = getScopePolicyCode(group.dataPermission);
5887
+ if (policyCode) scopePolicyRefs.push(policyCode);
5888
+ if (policyCode && policyCodes.size > 0 && !policyCodes.has(policyCode)) {
5889
+ warnings.push(`data view ${dataView.code} permission group ${group.code || group.name || '?'} 引用未声明 data-scope-policy: ${policyCode}`);
5890
+ }
5891
+ }
5892
+ }
5893
+ if (scopePolicyRefs.length > 0 && (manifest.scopeGrantSources || []).length === 0) {
5894
+ warnings.push('权限组已引用 scope_policy,但本工作区未声明 scopeGrantSources;请确认授权来源由其他工作区或平台后台维护');
5754
5895
  }
5755
5896
  for (const policy of manifest.publicAccessPolicies || []) {
5756
5897
  const grants = normalizePublicAccessPolicyManifest(target.bound, policy).grants || {};
@@ -5770,6 +5911,12 @@ async function buildPermissionAudit(config, target, flags = {}) {
5770
5911
  ),
5771
5912
  errors,
5772
5913
  warnings,
5914
+ scope: {
5915
+ dimensions: (manifest.scopeDimensions || []).length,
5916
+ grantSources: (manifest.scopeGrantSources || []).length,
5917
+ policies: (manifest.dataScopePolicies || []).length,
5918
+ referencedPolicies: unique(scopePolicyRefs),
5919
+ },
5773
5920
  };
5774
5921
  if (flags.live) {
5775
5922
  result.live = await requestWithAuth(
@@ -5806,6 +5953,9 @@ function ensureResourceBuckets(bound) {
5806
5953
  bound.resources.authConfigs = bound.resources.authConfigs || {};
5807
5954
  bound.resources.routes = bound.resources.routes || {};
5808
5955
  bound.resources.publicAccessPolicies = bound.resources.publicAccessPolicies || {};
5956
+ bound.resources.scopeDimensions = bound.resources.scopeDimensions || {};
5957
+ bound.resources.scopeGrantSources = bound.resources.scopeGrantSources || {};
5958
+ bound.resources.dataScopePolicies = bound.resources.dataScopePolicies || {};
5809
5959
  bound.resources.notifications = bound.resources.notifications || {};
5810
5960
  bound.resources.notifications.templates = bound.resources.notifications.templates || {};
5811
5961
  bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
@@ -6056,6 +6206,24 @@ function saveStorageConfigResource(target, storageCode, storageConfigId, extra =
6056
6206
  }, keys);
6057
6207
  }
6058
6208
 
6209
+ function saveScopeResource(target, bucket, code, data = {}) {
6210
+ if (!bucket || !code) return;
6211
+ const keys = ['id', 'resourceCode', 'name', 'updatedAt'];
6212
+ saveStateResource(target, bucket, code, {
6213
+ id: data?.id,
6214
+ resourceCode: data?.resourceCode || data?.code || code,
6215
+ name: data?.name,
6216
+ }, keys);
6217
+ }
6218
+
6219
+ function resourceTypeToBucket(resourceType) {
6220
+ return {
6221
+ 'scope-dimension': 'scopeDimensions',
6222
+ 'scope-grant-source': 'scopeGrantSources',
6223
+ 'data-scope-policy': 'dataScopePolicies',
6224
+ }[resourceType];
6225
+ }
6226
+
6059
6227
  function saveRouteResource(target, routeCode, routeId, extra = {}) {
6060
6228
  const keys = ['routeId', 'pathPattern', 'publicAccess', 'publicPolicyCode'];
6061
6229
  saveStateResource(target, 'routes', routeCode, {
@@ -6208,6 +6376,24 @@ const RESOURCE_SPECS = [
6208
6376
  topFiles: [path.join('permissions', 'form-groups.json')],
6209
6377
  pluralKeys: ['formPermissionGroups', 'formGroups'],
6210
6378
  },
6379
+ {
6380
+ key: 'scopeDimensions',
6381
+ dir: path.join('permissions', 'scope-dimensions'),
6382
+ topFiles: [path.join('permissions', 'scope-dimensions.json')],
6383
+ pluralKeys: ['scopeDimensions', 'dimensions'],
6384
+ },
6385
+ {
6386
+ key: 'scopeGrantSources',
6387
+ dir: path.join('permissions', 'scope-grant-sources'),
6388
+ topFiles: [path.join('permissions', 'scope-grant-sources.json')],
6389
+ pluralKeys: ['scopeGrantSources', 'grantSources', 'sources'],
6390
+ },
6391
+ {
6392
+ key: 'dataScopePolicies',
6393
+ dir: path.join('permissions', 'data-scope-policies'),
6394
+ topFiles: [path.join('permissions', 'data-scope-policies.json')],
6395
+ pluralKeys: ['dataScopePolicies', 'scopePolicies', 'policies'],
6396
+ },
6211
6397
  {
6212
6398
  key: 'formSettings',
6213
6399
  dir: path.join('settings', 'forms'),
@@ -6274,6 +6460,20 @@ const RESOURCE_TYPE_ALIASES = new Map([
6274
6460
  ['form-groups', 'formPermissionGroups'],
6275
6461
  ['formpermissiongroup', 'formPermissionGroups'],
6276
6462
  ['formpermissiongroups', 'formPermissionGroups'],
6463
+ ['scope-dimension', 'scopeDimensions'],
6464
+ ['scope-dimensions', 'scopeDimensions'],
6465
+ ['scopedimension', 'scopeDimensions'],
6466
+ ['scopedimensions', 'scopeDimensions'],
6467
+ ['scope-grant-source', 'scopeGrantSources'],
6468
+ ['scope-grant-sources', 'scopeGrantSources'],
6469
+ ['scopegrantsource', 'scopeGrantSources'],
6470
+ ['scopegrantsources', 'scopeGrantSources'],
6471
+ ['data-scope-policy', 'dataScopePolicies'],
6472
+ ['data-scope-policies', 'dataScopePolicies'],
6473
+ ['scope-policy', 'dataScopePolicies'],
6474
+ ['scope-policies', 'dataScopePolicies'],
6475
+ ['datascopepolicy', 'dataScopePolicies'],
6476
+ ['datascopepolicies', 'dataScopePolicies'],
6277
6477
  ['form-setting', 'formSettings'],
6278
6478
  ['form-settings', 'formSettings'],
6279
6479
  ['settings-form', 'formSettings'],
@@ -6283,8 +6483,8 @@ const RESOURCE_TYPE_ALIASES = new Map([
6283
6483
  ]);
6284
6484
 
6285
6485
  const RESOURCE_TYPE_ALIAS_GROUPS = new Map([
6286
- ['permission', ['pagePermissionGroups', 'formPermissionGroups']],
6287
- ['permissions', ['pagePermissionGroups', 'formPermissionGroups']],
6486
+ ['permission', ['pagePermissionGroups', 'formPermissionGroups', 'scopeDimensions', 'scopeGrantSources', 'dataScopePolicies']],
6487
+ ['permissions', ['pagePermissionGroups', 'formPermissionGroups', 'scopeDimensions', 'scopeGrantSources', 'dataScopePolicies']],
6288
6488
  ]);
6289
6489
 
6290
6490
  function getResourceTypeFilters(positional = [], flags = {}) {
@@ -6313,7 +6513,7 @@ function getResourceTypeFilters(positional = [], flags = {}) {
6313
6513
  if (!key) {
6314
6514
  fail(
6315
6515
  `未知资源类型: ${rawType}。常用类型包括 workflow, notification, route, menu, role, ` +
6316
- `storage, page-permission-group, form-permission-group, public-access。`
6516
+ `storage, page-permission-group, form-permission-group, scope-policy, public-access。`
6317
6517
  );
6318
6518
  }
6319
6519
  filters.add(key);
@@ -6523,6 +6723,7 @@ function validateWorkspaceResources(manifest) {
6523
6723
  }
6524
6724
  }
6525
6725
  validateNotificationReferences(manifest.notifications || [], errors);
6726
+ validateScopePermissionResources(manifest, errors, warnings);
6526
6727
  return {
6527
6728
  valid: errors.length === 0,
6528
6729
  errors,
@@ -6610,6 +6811,9 @@ function generateResourceTypes(manifest, outputFile) {
6610
6811
  const storageConfigCodes = unique((manifest.storageConfigs || []).map(item => item.code).filter(Boolean));
6611
6812
  const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
6612
6813
  const publicAccessPolicyCodes = unique((manifest.publicAccessPolicies || []).map(item => item.code).filter(Boolean));
6814
+ const scopeDimensionCodes = unique((manifest.scopeDimensions || []).map(item => item.code).filter(Boolean));
6815
+ const scopeGrantSourceCodes = unique((manifest.scopeGrantSources || []).map(item => item.code).filter(Boolean));
6816
+ const dataScopePolicyCodes = unique((manifest.dataScopePolicies || []).map(item => item.code).filter(Boolean));
6613
6817
  const content = [
6614
6818
  '/* eslint-disable */',
6615
6819
  '// Generated by openxiangda resource typegen. Do not edit manually.',
@@ -6638,6 +6842,15 @@ function generateResourceTypes(manifest, outputFile) {
6638
6842
  `export const publicAccessPolicyCodes = ${JSON.stringify(publicAccessPolicyCodes, null, 2)} as const`,
6639
6843
  'export type PublicAccessPolicyCode = typeof publicAccessPolicyCodes[number]',
6640
6844
  '',
6845
+ `export const scopeDimensionCodes = ${JSON.stringify(scopeDimensionCodes, null, 2)} as const`,
6846
+ 'export type ScopeDimensionCode = typeof scopeDimensionCodes[number]',
6847
+ '',
6848
+ `export const scopeGrantSourceCodes = ${JSON.stringify(scopeGrantSourceCodes, null, 2)} as const`,
6849
+ 'export type ScopeGrantSourceCode = typeof scopeGrantSourceCodes[number]',
6850
+ '',
6851
+ `export const dataScopePolicyCodes = ${JSON.stringify(dataScopePolicyCodes, null, 2)} as const`,
6852
+ 'export type DataScopePolicyCode = typeof dataScopePolicyCodes[number]',
6853
+ '',
6641
6854
  `export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
6642
6855
  '',
6643
6856
  `export const runtimeRoutes = ${JSON.stringify(routes, null, 2)} as const`,
@@ -6657,6 +6870,9 @@ function generateResourceTypes(manifest, outputFile) {
6657
6870
  functions: functionCodes.length,
6658
6871
  authConfigs: authConfigCodes.length,
6659
6872
  publicAccessPolicies: publicAccessPolicyCodes.length,
6873
+ scopeDimensions: scopeDimensionCodes.length,
6874
+ scopeGrantSources: scopeGrantSourceCodes.length,
6875
+ dataScopePolicies: dataScopePolicyCodes.length,
6660
6876
  };
6661
6877
  }
6662
6878
 
@@ -6747,6 +6963,21 @@ function validateResourceItem(kind, item, errors, warnings) {
6747
6963
  } else {
6748
6964
  errors.push(`${label}: 不支持的 viewType: ${definition.viewType}`);
6749
6965
  }
6966
+ for (const [index, group] of (item.permissionGroups || []).entries()) {
6967
+ const actions = group.actions || group.operations;
6968
+ if (actions !== undefined) {
6969
+ if (!Array.isArray(actions)) {
6970
+ errors.push(`${label}.permissionGroups[${index}]: actions/operations 必须是数组`);
6971
+ } else {
6972
+ for (const action of actions) {
6973
+ const normalizedAction = normalizePermissionActionValue(action);
6974
+ if (!DATA_VIEW_PERMISSION_ACTIONS.has(normalizedAction)) {
6975
+ errors.push(`${label}.permissionGroups[${index}]: 不支持的数据视图 action "${action}"`);
6976
+ }
6977
+ }
6978
+ }
6979
+ }
6980
+ }
6750
6981
  validateDataViewPerformance(label, definition, errors, warnings, storageMode);
6751
6982
  }
6752
6983
  if (kind === 'storageConfigs') {
@@ -6836,10 +7067,74 @@ function validateResourceItem(kind, item, errors, warnings) {
6836
7067
  errors.push(`${label}: externalRoleCodes 必须是数组`);
6837
7068
  }
6838
7069
  }
7070
+ if (kind === 'scopeDimensions') {
7071
+ if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
7072
+ if (item.hierarchyMode && !['flat', 'self_parent'].includes(String(item.hierarchyMode))) {
7073
+ errors.push(`${label}: hierarchyMode 只能是 flat 或 self_parent`);
7074
+ }
7075
+ if ((item.sourceFormUuid || item.formUuid) && !item.sourceValueField && !item.valueField) {
7076
+ errors.push(`${label}: 使用表单维护维度时缺少 sourceValueField`);
7077
+ }
7078
+ }
7079
+ if (kind === 'scopeGrantSources') {
7080
+ if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
7081
+ if (!item.sourceFormUuid && !item.formUuid) errors.push(`${label}: 缺少 sourceFormUuid`);
7082
+ const subjectMappings = item.subjectMappings || item.subjects;
7083
+ const dimensionMappings = item.dimensionMappings || item.dimensions;
7084
+ if (!Array.isArray(subjectMappings) || subjectMappings.length === 0) {
7085
+ errors.push(`${label}: 缺少 subjectMappings`);
7086
+ }
7087
+ if (!Array.isArray(dimensionMappings) || dimensionMappings.length === 0) {
7088
+ errors.push(`${label}: 缺少 dimensionMappings`);
7089
+ }
7090
+ }
7091
+ if (kind === 'dataScopePolicies') {
7092
+ if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
7093
+ const rules = item.rules || item.dimensions;
7094
+ if (!Array.isArray(rules) || rules.length === 0) {
7095
+ errors.push(`${label}: 缺少 rules`);
7096
+ } else {
7097
+ rules.forEach((rule, index) => {
7098
+ const ruleLabel = `${label}.rules[${index}]`;
7099
+ const dimensionCode = rule.dimensionCode || rule.dimension;
7100
+ const targetField = rule.field || rule.targetField;
7101
+ if (!dimensionCode) errors.push(`${ruleLabel}: 缺少 dimensionCode`);
7102
+ if (!targetField) errors.push(`${ruleLabel}: 缺少 field/targetField`);
7103
+ if (rule.valuePath && !isSafeScopePolicyValuePath(rule.valuePath)) {
7104
+ errors.push(`${ruleLabel}: valuePath 只能包含字母、数字、下划线、中划线、点或 >`);
7105
+ }
7106
+ if (isScopePolicyMultiValueComponent(rule.componentType)) {
7107
+ warnings.push(`${ruleLabel}: 多值选择/人员/部门字段不适合直接作为 scope_policy 目标字段,建议派生隐藏 scalar key 字段`);
7108
+ } else if (
7109
+ !rule.valuePath &&
7110
+ !rule.componentType &&
7111
+ looksLikeJsonScopeTargetField(targetField)
7112
+ ) {
7113
+ warnings.push(`${ruleLabel}: 目标字段看起来可能是选择/人员/部门类字段;若底层是 JSONB,请声明 valuePath:"value" 或 componentType,优先使用隐藏 scalar key`);
7114
+ }
7115
+ });
7116
+ }
7117
+ }
6839
7118
  if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
6840
7119
  if (kind === 'formPermissionGroups') {
6841
7120
  if (!item.name) errors.push(`${label}: 缺少 name`);
6842
7121
  if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
7122
+ const actions = item.actions || item.operations;
7123
+ if (actions !== undefined) {
7124
+ if (!Array.isArray(actions)) {
7125
+ errors.push(`${label}: actions/operations 必须是数组`);
7126
+ } else {
7127
+ for (const action of actions) {
7128
+ const normalizedAction = normalizePermissionActionValue(action);
7129
+ if (!FORM_PERMISSION_ACTIONS.has(normalizedAction)) {
7130
+ errors.push(`${label}: 不支持的表单 action "${action}"`);
7131
+ }
7132
+ }
7133
+ }
7134
+ }
7135
+ if (item.dataPermission?.type === 'scope_policy' && !getScopePolicyCode(item.dataPermission)) {
7136
+ errors.push(`${label}: dataPermission.type=scope_policy 时必须声明 policyCode`);
7137
+ }
6843
7138
  }
6844
7139
  if (kind === 'formSettings') {
6845
7140
  if (!item.formCode && !item.formUuid && !item.code) {
@@ -6856,6 +7151,129 @@ function validateResourceItem(kind, item, errors, warnings) {
6856
7151
  }
6857
7152
  }
6858
7153
 
7154
+ function validateScopePermissionResources(manifest, errors, warnings) {
7155
+ const policyCodes = new Set(
7156
+ (manifest.dataScopePolicies || [])
7157
+ .map(item => item.code || item.resourceCode)
7158
+ .filter(Boolean)
7159
+ .map(String)
7160
+ );
7161
+ const scopePolicyRefs = [];
7162
+ const inspectPermissionGroup = (kind, item) => {
7163
+ const groups = kind === 'dataViews'
7164
+ ? (Array.isArray(item.permissionGroups) ? item.permissionGroups : [])
7165
+ : [item];
7166
+ for (const group of groups) {
7167
+ const actions = kind === 'dataViews'
7168
+ ? normalizeDataViewPermissionGroupActions(group)
7169
+ : normalizeFormPermissionGroupActions(group);
7170
+ const dataPermission = group?.dataPermission;
7171
+ if (
7172
+ kind === 'formPermissionGroups' &&
7173
+ actions.includes('create') &&
7174
+ (!dataPermission || dataPermission.type !== 'scope_policy')
7175
+ ) {
7176
+ warnings.push(`${resourceLabel(kind, item)}: 声明了 create,但未声明 create 对应的 scope_policy;若新增数据必须落在用户业务范围内,请补充 dataPermission.type=scope_policy`);
7177
+ }
7178
+ if (!dataPermission) continue;
7179
+ if (dataPermission.type === 'scope_policy') {
7180
+ if (kind === 'formPermissionGroups') {
7181
+ const missing = ['view', 'edit', 'delete', 'export']
7182
+ .filter(action => !actions.includes(action));
7183
+ if (missing.length) {
7184
+ warnings.push(`${resourceLabel(kind, item)}: 使用 scope_policy 但 action 覆盖不完整,缺少 ${missing.join(', ')};请确认每个角色的 action matrix`);
7185
+ }
7186
+ }
7187
+ if (kind === 'dataViews') {
7188
+ const missing = ['query', 'stats', 'export']
7189
+ .filter(action => !actions.includes(action));
7190
+ if (missing.length) {
7191
+ warnings.push(`${resourceLabel(kind, item)}: Data View 使用 scope_policy 但 action 覆盖不完整,缺少 ${missing.join(', ')}`);
7192
+ }
7193
+ }
7194
+ const policyCode = getScopePolicyCode(dataPermission);
7195
+ if (!policyCode) {
7196
+ errors.push(`${resourceLabel(kind, item)}: scope_policy 缺少 policyCode`);
7197
+ continue;
7198
+ }
7199
+ scopePolicyRefs.push(policyCode);
7200
+ if (policyCodes.size > 0 && !policyCodes.has(policyCode)) {
7201
+ warnings.push(`${resourceLabel(kind, item)}: scope_policy 引用 ${policyCode},但 src/resources 中未声明同名 data-scope-policy`);
7202
+ }
7203
+ } else if (containsBusinessScopeLikeField(dataPermission)) {
7204
+ warnings.push(`${resourceLabel(kind, item)}: 检测到疑似业务范围字段条件,业务对象数量超过少量枚举时优先改为 dataPermission.type=scope_policy`);
7205
+ }
7206
+ }
7207
+ };
7208
+ for (const group of manifest.formPermissionGroups || []) {
7209
+ inspectPermissionGroup('formPermissionGroups', group);
7210
+ }
7211
+ for (const view of manifest.dataViews || []) {
7212
+ inspectPermissionGroup('dataViews', view);
7213
+ }
7214
+ const formGroupCount = (manifest.formPermissionGroups || []).length;
7215
+ const pageGroupCount = (manifest.pagePermissionGroups || []).length;
7216
+ if (formGroupCount >= 30) {
7217
+ warnings.push(`formPermissionGroups 当前 ${formGroupCount} 个;如这些组按客户/项目/区域/学院/班级/门店拆分,建议收敛为业务授权表 + scope_policy`);
7218
+ }
7219
+ if (pageGroupCount >= 30) {
7220
+ warnings.push(`pagePermissionGroups 当前 ${pageGroupCount} 个;如这些组按业务对象拆分,建议只保留入口角色,数据范围交给 scope_policy`);
7221
+ }
7222
+ if (scopePolicyRefs.length > 0 && (manifest.scopeGrantSources || []).length === 0) {
7223
+ warnings.push('已引用 scope_policy,但未声明 scopeGrantSources;请确认授权来源由平台后台或其他工作区维护');
7224
+ }
7225
+ }
7226
+
7227
+ function getScopePolicyCode(dataPermission = {}) {
7228
+ return String(
7229
+ dataPermission.policyCode ||
7230
+ dataPermission.scopePolicyCode ||
7231
+ dataPermission.code ||
7232
+ ''
7233
+ ).trim();
7234
+ }
7235
+
7236
+ function isSafeScopePolicyValuePath(value) {
7237
+ return /^[A-Za-z0-9_.>-]+$/.test(String(value || ''));
7238
+ }
7239
+
7240
+ function isScopePolicyMultiValueComponent(componentType) {
7241
+ const normalized = String(componentType || '').trim().toLowerCase();
7242
+ return [
7243
+ 'multiselect',
7244
+ 'multiselectfield',
7245
+ 'checkbox',
7246
+ 'checkboxfield',
7247
+ 'cascadeselect',
7248
+ 'cascadeselectfield',
7249
+ ].includes(normalized);
7250
+ }
7251
+
7252
+ function looksLikeJsonScopeTargetField(field) {
7253
+ return /(select|option|user|employee|member|dept|department|linked|association|人员|用户|部门|选择)/i.test(String(field || ''));
7254
+ }
7255
+
7256
+ function containsBusinessScopeLikeField(value) {
7257
+ const fields = [];
7258
+ const visit = item => {
7259
+ if (!item || typeof item !== 'object') return;
7260
+ if (Array.isArray(item)) {
7261
+ item.forEach(visit);
7262
+ return;
7263
+ }
7264
+ if (item.field || item.key || item.targetField) {
7265
+ fields.push(String(item.field || item.key || item.targetField));
7266
+ }
7267
+ visit(item.condition);
7268
+ visit(item.conditions);
7269
+ visit(item.rules);
7270
+ };
7271
+ visit(value);
7272
+ return fields.some(field =>
7273
+ /(scope|customer|client|project|region|area|store|shop|campus|college|school|class|班级|学院|校区|区域|门店|客户|项目)/i.test(field)
7274
+ );
7275
+ }
7276
+
6859
7277
  function validateDataViewPerformance(label, definition, errors, warnings, storageMode = 'materialized') {
6860
7278
  const viewType = String(definition.viewType || 'row').toLowerCase();
6861
7279
  if (viewType !== 'row' && viewType !== 'aggregate') return;
@@ -7306,6 +7724,9 @@ async function buildResourcePlan(config, target, manifest) {
7306
7724
  await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
7307
7725
  addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
7308
7726
  addPlanActions(actions, 'storageConfig', manifest.storageConfigs, existing.storageConfigs, (item, current) => storageConfigEquals(item, current));
7727
+ addPlanActions(actions, 'scopeDimension', manifest.scopeDimensions, existing.scopeDimensions, (item, current) => scopeDimensionEquals(target.bound, item, current));
7728
+ addPlanActions(actions, 'scopeGrantSource', manifest.scopeGrantSources, existing.scopeGrantSources, (item, current) => scopeGrantSourceEquals(target.bound, item, current));
7729
+ addPlanActions(actions, 'dataScopePolicy', manifest.dataScopePolicies, existing.dataScopePolicies, dataScopePolicyEquals);
7309
7730
  addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
7310
7731
  addPlanActions(actions, 'formPermissionGroup', manifest.formPermissionGroups, existing.formPermissionGroups, (item, current) => formPermissionGroupEquals(target.bound, item, current));
7311
7732
  for (const item of manifest.formSettings || []) {
@@ -7378,6 +7799,9 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
7378
7799
  await publishAuthConfigResources(config, target, manifest.authConfigs || [], result, publishOptions);
7379
7800
  await publishRouteResources(config, target, manifest.routes || [], result, publishOptions);
7380
7801
  await publishAutomationResources(config, target, manifest.automations || [], result, publishOptions);
7802
+ await publishScopeDimensionResources(config, target, manifest.scopeDimensions || [], result, publishOptions);
7803
+ await publishScopeGrantSourceResources(config, target, manifest.scopeGrantSources || [], result, publishOptions);
7804
+ await publishDataScopePolicyResources(config, target, manifest.dataScopePolicies || [], result, publishOptions);
7381
7805
  await publishDataViewResources(config, target, manifest.dataViews || [], result, publishOptions);
7382
7806
  await publishStorageConfigResources(config, target, manifest.storageConfigs || [], result, publishOptions);
7383
7807
  await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result, publishOptions);
@@ -7408,6 +7832,9 @@ async function fetchExistingResourceMaps(config, target, manifest) {
7408
7832
  automations: new Map(),
7409
7833
  dataViews: new Map(),
7410
7834
  storageConfigs: new Map(),
7835
+ scopeDimensions: new Map(),
7836
+ scopeGrantSources: new Map(),
7837
+ dataScopePolicies: new Map(),
7411
7838
  pagePermissionGroups: new Map(),
7412
7839
  formPermissionGroups: new Map(),
7413
7840
  };
@@ -7620,6 +8047,30 @@ async function fetchExistingResourceMaps(config, target, manifest) {
7620
8047
  });
7621
8048
  }
7622
8049
  }
8050
+ if ((manifest.scopeDimensions || []).length > 0) {
8051
+ const data = await requestWithAuth(
8052
+ config,
8053
+ target.profileName,
8054
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/dimensions`
8055
+ );
8056
+ indexByCode(maps.scopeDimensions, normalizeItems(data), item => item.code || item.resourceCode);
8057
+ }
8058
+ if ((manifest.scopeGrantSources || []).length > 0) {
8059
+ const data = await requestWithAuth(
8060
+ config,
8061
+ target.profileName,
8062
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/grant-sources`
8063
+ );
8064
+ indexByCode(maps.scopeGrantSources, normalizeItems(data), item => item.code || item.resourceCode);
8065
+ }
8066
+ if ((manifest.dataScopePolicies || []).length > 0) {
8067
+ const data = await requestWithAuth(
8068
+ config,
8069
+ target.profileName,
8070
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/policies`
8071
+ );
8072
+ indexByCode(maps.dataScopePolicies, normalizeItems(data), item => item.code || item.resourceCode);
8073
+ }
7623
8074
  if ((manifest.pagePermissionGroups || []).length > 0) {
7624
8075
  const data = await requestWithAuth(
7625
8076
  config,
@@ -8454,6 +8905,102 @@ async function publishAuthConfigResources(config, target, authConfigs, result, o
8454
8905
  }
8455
8906
  }
8456
8907
 
8908
+ async function publishScopeDimensionResources(config, target, dimensions, result, options = {}) {
8909
+ for (const item of dimensions) {
8910
+ const code = item.code || item.resourceCode;
8911
+ if (shouldSkipNoopResource(options, 'scopeDimension', code)) {
8912
+ recordNoopResource(result, 'scopeDimension', code, options);
8913
+ continue;
8914
+ }
8915
+ const existing = await findExistingScopeResource(config, target, 'dimensions', code);
8916
+ const body = normalizeScopeDimensionManifest(item, target.bound);
8917
+ const data = await requestWithAuth(
8918
+ config,
8919
+ target.profileName,
8920
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/dimensions/${encodeURIComponent(code)}`,
8921
+ { method: 'POST', body }
8922
+ );
8923
+ saveScopeResource(target, 'scopeDimensions', code, data);
8924
+ result.published.push({
8925
+ kind: 'scopeDimension',
8926
+ code,
8927
+ action: existing ? 'update' : 'create',
8928
+ id: data?.id,
8929
+ });
8930
+ }
8931
+ }
8932
+
8933
+ async function publishScopeGrantSourceResources(config, target, sources, result, options = {}) {
8934
+ for (const item of sources) {
8935
+ const code = item.code || item.resourceCode;
8936
+ if (shouldSkipNoopResource(options, 'scopeGrantSource', code)) {
8937
+ recordNoopResource(result, 'scopeGrantSource', code, options);
8938
+ continue;
8939
+ }
8940
+ const existing = await findExistingScopeResource(config, target, 'grant-sources', code);
8941
+ const body = normalizeScopeGrantSourceManifest(item, target.bound);
8942
+ const data = await requestWithAuth(
8943
+ config,
8944
+ target.profileName,
8945
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/grant-sources/${encodeURIComponent(code)}`,
8946
+ { method: 'POST', body }
8947
+ );
8948
+ saveScopeResource(target, 'scopeGrantSources', code, data);
8949
+ const published = {
8950
+ kind: 'scopeGrantSource',
8951
+ code,
8952
+ action: existing ? 'update' : 'create',
8953
+ id: data?.id,
8954
+ };
8955
+ if (item.sync === true) {
8956
+ published.sync = await requestWithAuth(
8957
+ config,
8958
+ target.profileName,
8959
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/grant-sources/${encodeURIComponent(code)}/sync`,
8960
+ { method: 'POST', body: { dryRun: false } }
8961
+ );
8962
+ }
8963
+ result.published.push(published);
8964
+ }
8965
+ }
8966
+
8967
+ async function publishDataScopePolicyResources(config, target, policies, result, options = {}) {
8968
+ for (const item of policies) {
8969
+ const code = item.code || item.resourceCode;
8970
+ if (shouldSkipNoopResource(options, 'dataScopePolicy', code)) {
8971
+ recordNoopResource(result, 'dataScopePolicy', code, options);
8972
+ continue;
8973
+ }
8974
+ const existing = await findExistingScopeResource(config, target, 'policies', code);
8975
+ const body = normalizeDataScopePolicyManifest(item);
8976
+ const data = await requestWithAuth(
8977
+ config,
8978
+ target.profileName,
8979
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/policies/${encodeURIComponent(code)}`,
8980
+ { method: 'POST', body }
8981
+ );
8982
+ saveScopeResource(target, 'dataScopePolicies', code, data);
8983
+ result.published.push({
8984
+ kind: 'dataScopePolicy',
8985
+ code,
8986
+ action: existing ? 'update' : 'create',
8987
+ id: data?.id,
8988
+ });
8989
+ }
8990
+ }
8991
+
8992
+ async function findExistingScopeResource(config, target, segment, code) {
8993
+ if (!code) return null;
8994
+ const data = await requestWithAuth(
8995
+ config,
8996
+ target.profileName,
8997
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/scope/${segment}`
8998
+ );
8999
+ return normalizeItems(data).find(item =>
9000
+ String(item.code || item.resourceCode || '') === String(code)
9001
+ );
9002
+ }
9003
+
8457
9004
  async function publishDataViewResources(config, target, dataViews, result, options = {}) {
8458
9005
  for (const dataViewItem of dataViews) {
8459
9006
  if (shouldSkipNoopResource(options, 'dataView', dataViewItem.code)) {
@@ -9977,11 +10524,20 @@ function normalizeDataViewManifest(bound, dataView) {
9977
10524
  definition: normalizedDefinition,
9978
10525
  refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
9979
10526
  permissionGroups: Array.isArray(dataView.permissionGroups)
9980
- ? dataView.permissionGroups.map(group => stripUndefinedValues(withoutResourceMeta(group)))
10527
+ ? dataView.permissionGroups.map(group => normalizeDataViewPermissionGroupManifest(group))
9981
10528
  : undefined,
9982
10529
  });
9983
10530
  }
9984
10531
 
10532
+ function normalizeDataViewPermissionGroupManifest(group = {}) {
10533
+ const body = stripUndefinedValues({
10534
+ ...withoutResourceMeta(group),
10535
+ operations: normalizeDataViewPermissionGroupActions(group),
10536
+ });
10537
+ delete body.actions;
10538
+ return body;
10539
+ }
10540
+
9985
10541
  function normalizeRouteManifest(route) {
9986
10542
  const code = route.code || route.resourceCode;
9987
10543
  return stripUndefinedValues({
@@ -10016,6 +10572,60 @@ function normalizePublicAccessPolicyManifest(bound, policy) {
10016
10572
  });
10017
10573
  }
10018
10574
 
10575
+ function normalizeScopeDimensionManifest(dimension, bound) {
10576
+ const code = dimension.code || dimension.resourceCode;
10577
+ const sourceFormUuid =
10578
+ dimension.sourceFormUuid ||
10579
+ dimension.formUuid ||
10580
+ resolveOptionalFormUuid(bound, dimension.sourceFormCode || dimension.formCode);
10581
+ return stripUndefinedValues({
10582
+ code,
10583
+ name: dimension.name || dimension.title || code,
10584
+ valueType: dimension.valueType || 'string',
10585
+ hierarchyMode: dimension.hierarchyMode || 'flat',
10586
+ sourceFormUuid,
10587
+ sourceValueField: dimension.sourceValueField || dimension.valueField,
10588
+ sourceLabelField: dimension.sourceLabelField || dimension.labelField,
10589
+ sourceParentField: dimension.sourceParentField || dimension.parentField,
10590
+ enabledField: dimension.enabledField,
10591
+ enabled: dimension.enabled !== false,
10592
+ configJson: dimension.configJson || dimension.config,
10593
+ });
10594
+ }
10595
+
10596
+ function normalizeScopeGrantSourceManifest(source, bound) {
10597
+ const code = source.code || source.resourceCode;
10598
+ const sourceFormUuid =
10599
+ source.sourceFormUuid ||
10600
+ source.formUuid ||
10601
+ resolveOptionalFormUuid(bound, source.sourceFormCode || source.formCode);
10602
+ return stripUndefinedValues({
10603
+ code,
10604
+ name: source.name || source.title || code,
10605
+ sourceFormUuid,
10606
+ subjectMappings: source.subjectMappings || source.subjects || [],
10607
+ dimensionMappings: source.dimensionMappings || source.dimensions || [],
10608
+ operationField: source.operationField,
10609
+ enabledField: source.enabledField,
10610
+ effectiveFromField: source.effectiveFromField,
10611
+ effectiveToField: source.effectiveToField,
10612
+ filterJson: source.filterJson || source.filter,
10613
+ enabled: source.enabled !== false,
10614
+ });
10615
+ }
10616
+
10617
+ function normalizeDataScopePolicyManifest(policy) {
10618
+ const code = policy.code || policy.resourceCode;
10619
+ return stripUndefinedValues({
10620
+ code,
10621
+ name: policy.name || policy.title || code,
10622
+ matchMode: policy.matchMode === 'OR' ? 'OR' : 'AND',
10623
+ rules: policy.rules || policy.dimensions || [],
10624
+ configJson: policy.configJson || policy.config,
10625
+ enabled: policy.enabled !== false,
10626
+ });
10627
+ }
10628
+
10019
10629
  function normalizePublicAccessGrants(bound, grants = {}) {
10020
10630
  const formEntries = [
10021
10631
  ...normalizePermissionCodeArray(grants.forms),
@@ -10579,6 +11189,30 @@ function formPermissionGroupEquals(bound, desired, existing) {
10579
11189
  return stableStringifyForPlan(current, process.cwd()) === stableStringifyForPlan(expected, desired.__dir || process.cwd());
10580
11190
  }
10581
11191
 
11192
+ function scopeDimensionEquals(bound, desired, existing) {
11193
+ if (!existing) return false;
11194
+ const expected = normalizeScopeDimensionManifest(desired, bound);
11195
+ return scopeResourceEquals(expected, existing, desired.__dir);
11196
+ }
11197
+
11198
+ function scopeGrantSourceEquals(bound, desired, existing) {
11199
+ if (!existing) return false;
11200
+ const expected = normalizeScopeGrantSourceManifest(desired, bound);
11201
+ return scopeResourceEquals(expected, existing, desired.__dir);
11202
+ }
11203
+
11204
+ function dataScopePolicyEquals(desired, existing) {
11205
+ if (!existing) return false;
11206
+ const expected = normalizeDataScopePolicyManifest(desired);
11207
+ return scopeResourceEquals(expected, existing, desired.__dir);
11208
+ }
11209
+
11210
+ function scopeResourceEquals(expected, existing, baseDir) {
11211
+ const current = pickObjectKeys(existing, Object.keys(expected));
11212
+ return stableStringifyForPlan(current, process.cwd()) ===
11213
+ stableStringifyForPlan(expected, baseDir || process.cwd());
11214
+ }
11215
+
10582
11216
  function normalizeFormPermissionGroupManifest(bound, group) {
10583
11217
  const formUuid = resolveManifestFormUuid(bound, group);
10584
11218
  const body = {
@@ -10588,10 +11222,12 @@ function normalizeFormPermissionGroupManifest(bound, group) {
10588
11222
  name: group.name || group.code,
10589
11223
  type: group.type || 'view',
10590
11224
  roles: group.roles || [],
11225
+ operations: normalizeFormPermissionGroupActions(group),
10591
11226
  };
10592
11227
  delete body.code;
10593
11228
  delete body.formCode;
10594
11229
  delete body.form;
11230
+ delete body.actions;
10595
11231
  return stripUndefinedValues(body);
10596
11232
  }
10597
11233
 
@@ -10613,6 +11249,52 @@ function sortStringValues(value = []) {
10613
11249
  return [...(value || [])].map(item => String(item)).sort();
10614
11250
  }
10615
11251
 
11252
+ const FORM_PERMISSION_ACTIONS = new Set([
11253
+ 'view',
11254
+ 'create',
11255
+ 'edit',
11256
+ 'delete',
11257
+ 'export',
11258
+ 'import',
11259
+ 'change_records',
11260
+ 'workflow',
11261
+ ]);
11262
+
11263
+ const DATA_VIEW_PERMISSION_ACTIONS = new Set([
11264
+ 'query',
11265
+ 'stats',
11266
+ 'export',
11267
+ 'refresh',
11268
+ ]);
11269
+
11270
+ function normalizePermissionActionValue(action) {
11271
+ const value = String(action || '').trim();
11272
+ if (!value) return '';
11273
+ if (value === 'submit') return 'create';
11274
+ return value;
11275
+ }
11276
+
11277
+ function normalizePermissionActionArray(value, allowed, fallback = []) {
11278
+ const source = Array.isArray(value) ? value : fallback;
11279
+ return unique(source.map(normalizePermissionActionValue).filter(action => allowed.has(action)));
11280
+ }
11281
+
11282
+ function normalizeFormPermissionGroupActions(group = {}) {
11283
+ if (group.actions !== undefined || group.operations !== undefined) {
11284
+ return normalizePermissionActionArray(group.actions || group.operations, FORM_PERMISSION_ACTIONS);
11285
+ }
11286
+ return group.type === 'submit'
11287
+ ? ['create']
11288
+ : ['view', 'edit', 'delete', 'change_records', 'workflow'];
11289
+ }
11290
+
11291
+ function normalizeDataViewPermissionGroupActions(group = {}) {
11292
+ if (group.actions !== undefined || group.operations !== undefined) {
11293
+ return normalizePermissionActionArray(group.actions || group.operations, DATA_VIEW_PERMISSION_ACTIONS);
11294
+ }
11295
+ return ['query'];
11296
+ }
11297
+
10616
11298
  function pickObjectKeys(source = {}, keys = []) {
10617
11299
  return keys.reduce((acc, key) => {
10618
11300
  acc[key] = source[key];
@@ -10834,7 +11516,7 @@ function normalizeDataViewPermissionGroupsForPlan(groups = []) {
10834
11516
  code,
10835
11517
  name: group.name || code || '默认权限组',
10836
11518
  roles: Array.isArray(group.roles) ? sortStringValues(group.roles) : [],
10837
- operations: Array.isArray(group.operations) ? sortStringValues(group.operations) : ['query'],
11519
+ operations: sortStringValues(normalizeDataViewPermissionGroupActions(group)),
10838
11520
  fieldPermissions: normalizeFieldPermissionsForPlan(group.fieldPermissions),
10839
11521
  dataPermission: group.dataPermission || undefined,
10840
11522
  });