openxiangda 1.0.127 → 1.0.129

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 (29) hide show
  1. package/README.md +1 -1
  2. package/lib/cli.js +376 -34
  3. package/package.json +1 -1
  4. package/packages/sdk/dist/{ProcessPreview-Kexdosgu.d.mts → ProcessPreview-B2-umzSY.d.mts} +12 -6
  5. package/packages/sdk/dist/{ProcessPreview-Kexdosgu.d.ts → ProcessPreview-B2-umzSY.d.ts} +12 -6
  6. package/packages/sdk/dist/components/index.cjs +95 -16
  7. package/packages/sdk/dist/components/index.cjs.map +1 -1
  8. package/packages/sdk/dist/components/index.d.mts +4 -3
  9. package/packages/sdk/dist/components/index.d.ts +4 -3
  10. package/packages/sdk/dist/components/index.mjs +95 -16
  11. package/packages/sdk/dist/components/index.mjs.map +1 -1
  12. package/packages/sdk/dist/runtime/index.cjs +131 -16
  13. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  14. package/packages/sdk/dist/runtime/index.d.mts +2 -2
  15. package/packages/sdk/dist/runtime/index.d.ts +2 -2
  16. package/packages/sdk/dist/runtime/index.mjs +131 -16
  17. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  18. package/packages/sdk/dist/runtime/react.cjs +34 -0
  19. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  20. package/packages/sdk/dist/runtime/react.d.mts +63 -2
  21. package/packages/sdk/dist/runtime/react.d.ts +63 -2
  22. package/packages/sdk/dist/runtime/react.mjs +34 -0
  23. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  24. package/templates/openxiangda-react-spa/src/app/navigation.ts +8 -1
  25. package/templates/openxiangda-react-spa/src/app/router.tsx +6 -0
  26. package/templates/openxiangda-react-spa/src/pages/admin/LoginLogPage.tsx +425 -0
  27. package/templates/openxiangda-react-spa/src/resources/menus/menus.json +9 -0
  28. package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/app-admin.json +3 -3
  29. package/templates/openxiangda-react-spa/src/resources/roles/roles.json +6 -0
package/README.md CHANGED
@@ -44,7 +44,7 @@ openxiangda app snapshot APP_XXXX --profile dev --json
44
44
 
45
45
  User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only profile-specific resource IDs.
46
46
 
47
- React SPA workspaces publish their frontend with `openxiangda runtime deploy`. The default upload mode is staged multipart: each `dist/` file is uploaded separately, then the CLI submits a release manifest and activates it unless `--no-activate` is passed. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result. Use `--upload-timeout-ms <ms>` or `OPENXIANGDA_RUNTIME_UPLOAD_TIMEOUT_MS` to tune runtime upload/finalize timeouts. Use `--upload-mode legacy-json` only as a compatibility fallback for older platforms.
47
+ React SPA workspaces publish their frontend with `openxiangda runtime deploy`. The default upload mode is `auto`: it tries staged multipart first, then falls back to built-in OSS direct upload if the staged file API returns HTTP 403. Use `--upload-mode oss-direct` to force built-in OSS direct upload, or `--upload-mode legacy-json` only as a compatibility fallback for older platforms. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result. Use `--upload-timeout-ms <ms>` or `OPENXIANGDA_RUNTIME_UPLOAD_TIMEOUT_MS` to tune runtime upload/finalize timeouts.
48
48
 
49
49
  React SPA templates include stable Vite manual chunk grouping for React, antd, ECharts, editor dependencies, and OpenXiangda runtime/component entrypoints. The SDK keeps large UI dependencies such as `antd-mobile` and `dayjs` external so the application bundler can split them by route. Applications should import from public package entrypoints such as `openxiangda`, `openxiangda/runtime`, and `openxiangda/runtime/react`; do not import `openxiangda/packages/sdk/dist/...` internals to chase bundle size.
50
50
 
package/lib/cli.js CHANGED
@@ -155,7 +155,7 @@ Usage:
155
155
  openxiangda permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind
156
156
  openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
157
157
  openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json] [--dry-run]
158
- openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode staged|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
158
+ openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode auto|staged|oss-direct|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
159
159
  openxiangda runtime releases [--profile name] [--json]
160
160
  openxiangda runtime activate <releaseId> [--profile name] [--json]
161
161
  openxiangda inspect app|form|workflow|automation|permissions
@@ -3863,6 +3863,7 @@ async function runtime(args) {
3863
3863
  ' openxiangda runtime releases --profile <name>',
3864
3864
  '说明:',
3865
3865
  ' - deploy 上传当前工作区 dist/ 并激活 React SPA runtime release。',
3866
+ ' - 默认 --upload-mode auto;multipart 上传遇到 403 时自动切换到阿里 OSS 直传。',
3866
3867
  ' - 如果平台服务较旧不支持分片上传,可临时使用 --upload-mode legacy-json。',
3867
3868
  ' - runtime 只发布前端产物;表单、流程、通知等资源仍走 resource publish。',
3868
3869
  ].join('\n'));
@@ -3901,10 +3902,42 @@ async function runtime(args) {
3901
3902
  if (subcommand === 'deploy') {
3902
3903
  const buildId = normalizeRuntimeBuildId(flags['build-id'] || createRuntimeBuildId());
3903
3904
  const distDir = path.resolve(process.cwd(), flags.dist || 'dist');
3904
- const assetBaseUrl = buildRuntimeAssetBaseUrl(target.appType, buildId);
3905
3905
  const uploadMode = normalizeRuntimeUploadMode(flags['upload-mode']);
3906
3906
  const uploadTimeoutMs = normalizeRuntimeUploadTimeoutMs(flags['upload-timeout-ms']);
3907
3907
  const traceId = createRuntimeTraceId();
3908
+ const gatewayAssetBaseUrl = buildRuntimeAssetBaseUrl(target.appType, buildId);
3909
+ let assetBaseUrl = gatewayAssetBaseUrl;
3910
+ let effectiveUploadMode = uploadMode;
3911
+ if (uploadMode === 'auto' || uploadMode === 'oss-direct') {
3912
+ try {
3913
+ const plan = await createRuntimeOssUploadBasePlan({
3914
+ config,
3915
+ profileName: target.profileName,
3916
+ appType: target.appType,
3917
+ buildId,
3918
+ traceId,
3919
+ timeoutMs: uploadTimeoutMs,
3920
+ });
3921
+ if (plan?.assetBaseUrl) {
3922
+ assetBaseUrl = plan.assetBaseUrl;
3923
+ if (uploadMode === 'auto') {
3924
+ effectiveUploadMode = 'oss-direct';
3925
+ printRuntimeProgress(
3926
+ `runtime builtin OSS available; using OSS direct upload traceId=${traceId}`
3927
+ );
3928
+ }
3929
+ }
3930
+ } catch (error) {
3931
+ if (uploadMode === 'oss-direct') {
3932
+ throw error;
3933
+ }
3934
+ effectiveUploadMode = 'auto';
3935
+ assetBaseUrl = gatewayAssetBaseUrl;
3936
+ printRuntimeProgress(
3937
+ `runtime builtin OSS preflight unavailable; using staged upload first traceId=${traceId}`
3938
+ );
3939
+ }
3940
+ }
3908
3941
  if (!flags['no-build']) {
3909
3942
  runRuntimeBuild({
3910
3943
  buildId,
@@ -3919,26 +3952,58 @@ async function runtime(args) {
3919
3952
  });
3920
3953
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
3921
3954
  printRuntimeProgress(
3922
- `runtime release upload: mode=${uploadMode} traceId=${traceId} files=${files.length} size=${formatBytes(totalBytes)} timeout=${uploadTimeoutMs}ms`
3923
- );
3924
- const releaseFiles =
3925
- uploadMode === 'legacy-json'
3926
- ? files.map(file => ({
3927
- path: file.path,
3928
- size: file.size,
3929
- sha256: file.sha256,
3930
- contentType: file.contentType,
3931
- contentBase64: file.buffer.toString('base64'),
3932
- }))
3933
- : await uploadRuntimeDistFilesStaged({
3934
- config,
3935
- profileName: target.profileName,
3936
- appType: target.appType,
3937
- buildId,
3938
- files,
3939
- traceId,
3940
- timeoutMs: uploadTimeoutMs,
3941
- });
3955
+ `runtime release upload: mode=${effectiveUploadMode} traceId=${traceId} files=${files.length} size=${formatBytes(totalBytes)} timeout=${uploadTimeoutMs}ms`
3956
+ );
3957
+ let releaseFiles;
3958
+ if (uploadMode === 'legacy-json') {
3959
+ releaseFiles = files.map(file => ({
3960
+ path: file.path,
3961
+ size: file.size,
3962
+ sha256: file.sha256,
3963
+ contentType: file.contentType,
3964
+ contentBase64: file.buffer.toString('base64'),
3965
+ }));
3966
+ effectiveUploadMode = 'legacy-json';
3967
+ } else if (effectiveUploadMode === 'oss-direct') {
3968
+ releaseFiles = await uploadRuntimeDistFilesOssDirect({
3969
+ config,
3970
+ profileName: target.profileName,
3971
+ appType: target.appType,
3972
+ buildId,
3973
+ files,
3974
+ traceId,
3975
+ timeoutMs: uploadTimeoutMs,
3976
+ });
3977
+ } else {
3978
+ try {
3979
+ releaseFiles = await uploadRuntimeDistFilesStaged({
3980
+ config,
3981
+ profileName: target.profileName,
3982
+ appType: target.appType,
3983
+ buildId,
3984
+ files,
3985
+ traceId,
3986
+ timeoutMs: uploadTimeoutMs,
3987
+ });
3988
+ } catch (error) {
3989
+ if (uploadMode !== 'auto' || !isRuntimeStagedUploadBlocked(error)) {
3990
+ throw error;
3991
+ }
3992
+ effectiveUploadMode = 'oss-direct';
3993
+ printRuntimeProgress(
3994
+ `runtime staged upload blocked by HTTP 403; fallback to OSS direct upload traceId=${traceId}`
3995
+ );
3996
+ releaseFiles = await uploadRuntimeDistFilesOssDirect({
3997
+ config,
3998
+ profileName: target.profileName,
3999
+ appType: target.appType,
4000
+ buildId,
4001
+ files,
4002
+ traceId,
4003
+ timeoutMs: uploadTimeoutMs,
4004
+ });
4005
+ }
4006
+ }
3942
4007
  const data = await requestWithAuth(
3943
4008
  config,
3944
4009
  target.profileName,
@@ -3957,6 +4022,7 @@ async function runtime(args) {
3957
4022
  }
3958
4023
  );
3959
4024
  saveRuntimeReleaseState(target, data);
4025
+ const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
3960
4026
  const result = {
3961
4027
  appType: target.appType,
3962
4028
  buildId,
@@ -3964,9 +4030,9 @@ async function runtime(args) {
3964
4030
  distDir,
3965
4031
  fileCount: files.length,
3966
4032
  totalBytes,
3967
- assetBaseUrl,
4033
+ assetBaseUrl: releaseAssetBaseUrl,
3968
4034
  activated: !flags['no-activate'],
3969
- uploadMode,
4035
+ uploadMode: effectiveUploadMode,
3970
4036
  traceId,
3971
4037
  };
3972
4038
  if (flags.json) return writeJson(result);
@@ -3975,9 +4041,9 @@ async function runtime(args) {
3975
4041
  `runtime release 已上传: ${target.appType}`,
3976
4042
  `build: ${buildId}`,
3977
4043
  `files: ${files.length} (${formatBytes(totalBytes)})`,
3978
- `upload: ${uploadMode}`,
4044
+ `upload: ${effectiveUploadMode}`,
3979
4045
  `traceId: ${traceId}`,
3980
- `assetBase: ${assetBaseUrl}`,
4046
+ `assetBase: ${releaseAssetBaseUrl}`,
3981
4047
  `active: ${result.activated ? 'yes' : 'no'}`,
3982
4048
  ].join('\n')
3983
4049
  );
@@ -3987,6 +4053,24 @@ async function runtime(args) {
3987
4053
  fail('用法: openxiangda runtime deploy|releases|activate [--profile name]');
3988
4054
  }
3989
4055
 
4056
+ async function createRuntimeOssUploadBasePlan(options) {
4057
+ return await requestWithAuth(
4058
+ options.config,
4059
+ options.profileName,
4060
+ `/openxiangda-api/v1/apps/${encodeURIComponent(options.appType)}/runtime/releases/oss-upload-plan`,
4061
+ {
4062
+ method: 'POST',
4063
+ timeoutMs: options.timeoutMs,
4064
+ headers: { 'x-openxiangda-trace-id': options.traceId },
4065
+ body: {
4066
+ buildId: options.buildId,
4067
+ planOnly: true,
4068
+ files: [],
4069
+ },
4070
+ }
4071
+ );
4072
+ }
4073
+
3990
4074
  function runRuntimeBuild(options) {
3991
4075
  const command = options.command || defaultRuntimeBuildCommand();
3992
4076
  if (options.jsonOutput) {
@@ -4082,6 +4166,95 @@ async function uploadRuntimeDistFilesStaged(options) {
4082
4166
  }));
4083
4167
  }
4084
4168
 
4169
+ async function uploadRuntimeDistFilesOssDirect(options) {
4170
+ const files = options.files || [];
4171
+ const plan = await requestWithAuth(
4172
+ options.config,
4173
+ options.profileName,
4174
+ `/openxiangda-api/v1/apps/${encodeURIComponent(options.appType)}/runtime/releases/oss-upload-plan`,
4175
+ {
4176
+ method: 'POST',
4177
+ timeoutMs: options.timeoutMs,
4178
+ headers: { 'x-openxiangda-trace-id': options.traceId },
4179
+ body: {
4180
+ buildId: options.buildId,
4181
+ files: files.map(file => ({
4182
+ path: file.path,
4183
+ size: file.size,
4184
+ sha256: file.sha256,
4185
+ contentType: file.contentType,
4186
+ })),
4187
+ },
4188
+ }
4189
+ );
4190
+ const uploadFiles = Array.isArray(plan?.files) ? plan.files : [];
4191
+ const uploadByPath = new Map(uploadFiles.map(file => [file.path, file]));
4192
+ let completed = 0;
4193
+ const results = await runWithConcurrency(
4194
+ files,
4195
+ RUNTIME_UPLOAD_CONCURRENCY,
4196
+ async file => {
4197
+ const uploadInfo = uploadByPath.get(file.path);
4198
+ if (!uploadInfo?.uploadUrl) {
4199
+ fail(`runtime OSS 上传计划缺少文件: ${file.path}`);
4200
+ }
4201
+ const uploaded = await uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo);
4202
+ completed += 1;
4203
+ printRuntimeProgress(
4204
+ `runtime OSS file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
4205
+ );
4206
+ return uploaded;
4207
+ }
4208
+ );
4209
+ return results.map(file => ({
4210
+ path: file.path,
4211
+ size: file.size,
4212
+ sha256: file.sha256,
4213
+ contentType: file.contentType,
4214
+ storageProvider: 'oss',
4215
+ objectName: file.objectName,
4216
+ bucketName: file.bucketName,
4217
+ }));
4218
+ }
4219
+
4220
+ async function uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo) {
4221
+ const timeoutMs = normalizeRuntimeUploadTimeoutMs(options.timeoutMs);
4222
+ const controller = new AbortController();
4223
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4224
+ let response;
4225
+ try {
4226
+ response = await fetch(uploadInfo.uploadUrl, {
4227
+ method: String(uploadInfo.uploadMethod || 'PUT').toUpperCase(),
4228
+ headers: uploadInfo.headers || {},
4229
+ body: file.buffer,
4230
+ signal: controller.signal,
4231
+ });
4232
+ } catch (error) {
4233
+ if (isAbortError(error)) {
4234
+ throw new Error(`runtime OSS 文件上传超时: ${file.path}, timeout=${timeoutMs}ms`);
4235
+ }
4236
+ throw new Error(formatFetchError(error, `runtime OSS upload ${file.path}`));
4237
+ } finally {
4238
+ clearTimeout(timer);
4239
+ }
4240
+ if (!response.ok) {
4241
+ const text = await response.text().catch(() => '');
4242
+ throw new Error(
4243
+ `runtime OSS 文件上传失败: ${file.path}, HTTP ${response.status}: ${
4244
+ text || response.statusText || 'request failed'
4245
+ }`
4246
+ );
4247
+ }
4248
+ return {
4249
+ path: file.path,
4250
+ size: file.size,
4251
+ sha256: file.sha256,
4252
+ contentType: file.contentType,
4253
+ objectName: uploadInfo.objectName,
4254
+ bucketName: uploadInfo.bucketName,
4255
+ };
4256
+ }
4257
+
4085
4258
  async function uploadRuntimeDistFile(options, file) {
4086
4259
  const apiPath = apiPathWithQuery(
4087
4260
  `/openxiangda-api/v1/apps/${encodeURIComponent(options.appType)}/runtime/releases/files`,
@@ -4137,9 +4310,20 @@ async function runWithConcurrency(items, concurrency, worker) {
4137
4310
  }
4138
4311
 
4139
4312
  function normalizeRuntimeUploadMode(value) {
4140
- const normalized = String(value || 'staged').trim().toLowerCase();
4141
- if (normalized === 'staged' || normalized === 'legacy-json') return normalized;
4142
- fail(`--upload-mode 只支持 staged 或 legacy-json,当前: ${value}`);
4313
+ const normalized = String(value || 'auto').trim().toLowerCase();
4314
+ if (
4315
+ normalized === 'auto' ||
4316
+ normalized === 'staged' ||
4317
+ normalized === 'oss-direct' ||
4318
+ normalized === 'legacy-json'
4319
+ ) {
4320
+ return normalized;
4321
+ }
4322
+ fail(`--upload-mode 只支持 auto、staged、oss-direct 或 legacy-json,当前: ${value}`);
4323
+ }
4324
+
4325
+ function isRuntimeStagedUploadBlocked(error) {
4326
+ return /^HTTP 403\b/.test(String(error?.message || ''));
4143
4327
  }
4144
4328
 
4145
4329
  function normalizeRuntimeUploadTimeoutMs(value) {
@@ -5764,7 +5948,21 @@ function validateResourceItem(kind, item, errors, warnings) {
5764
5948
  return;
5765
5949
  }
5766
5950
 
5767
- if (kind === 'roles' && !item.name) errors.push(`${label}: 缺少 name`);
5951
+ if (kind === 'roles') {
5952
+ if (!item.name) errors.push(`${label}: 缺少 name`);
5953
+ if (
5954
+ item.apiPermissionCodes !== undefined &&
5955
+ !Array.isArray(item.apiPermissionCodes)
5956
+ ) {
5957
+ errors.push(`${label}: apiPermissionCodes 必须是数组`);
5958
+ }
5959
+ if (
5960
+ item.apiPermissionMode !== undefined &&
5961
+ !['merge', 'replace'].includes(String(item.apiPermissionMode))
5962
+ ) {
5963
+ errors.push(`${label}: apiPermissionMode 只能是 merge 或 replace`);
5964
+ }
5965
+ }
5768
5966
  if (kind === 'menus' && !item.name) errors.push(`${label}: 缺少 name`);
5769
5967
  if (kind === 'workflows') {
5770
5968
  if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
@@ -6421,6 +6619,12 @@ async function fetchExistingResourceMaps(config, target, manifest) {
6421
6619
  })
6422
6620
  );
6423
6621
  indexByCode(maps.roles, normalizeItems(data), item => item.code);
6622
+ await hydrateRoleApiPermissions(
6623
+ config,
6624
+ target,
6625
+ maps.roles,
6626
+ manifest.roles || []
6627
+ );
6424
6628
  }
6425
6629
  if ((manifest.menus || []).length > 0) {
6426
6630
  const data = await requestWithAuth(
@@ -6640,6 +6844,118 @@ async function fetchExistingResourceMaps(config, target, manifest) {
6640
6844
  return maps;
6641
6845
  }
6642
6846
 
6847
+ function roleDeclaresApiPermissions(role) {
6848
+ if (!Array.isArray(role?.apiPermissionCodes)) return false;
6849
+ if (normalizePermissionCodeArray(role.apiPermissionCodes).length > 0) {
6850
+ return true;
6851
+ }
6852
+ return String(role?.apiPermissionMode || '').trim().toLowerCase() === 'replace';
6853
+ }
6854
+
6855
+ function normalizeApiPermissionMode(role) {
6856
+ const mode = String(role?.apiPermissionMode || 'merge').trim().toLowerCase();
6857
+ if (mode === 'merge' || mode === 'replace') return mode;
6858
+ fail(`${resourceLabel('role', role)}: apiPermissionMode 只能是 merge 或 replace`);
6859
+ }
6860
+
6861
+ function sameStringSet(left, right) {
6862
+ const a = normalizePermissionCodeArray(left);
6863
+ const b = normalizePermissionCodeArray(right);
6864
+ if (a.length !== b.length) return false;
6865
+ const bSet = new Set(b);
6866
+ return a.every(item => bSet.has(item));
6867
+ }
6868
+
6869
+ function isSubsetStringSet(subset, source) {
6870
+ const sourceSet = new Set(normalizePermissionCodeArray(source));
6871
+ return normalizePermissionCodeArray(subset).every(item => sourceSet.has(item));
6872
+ }
6873
+
6874
+ function normalizeApiPermissionRecords(data) {
6875
+ return flattenItems(normalizeItems(data)).filter(item => item?.code);
6876
+ }
6877
+
6878
+ async function fetchRoleApiPermissions(config, target, roleId) {
6879
+ if (!roleId) return [];
6880
+ const data = await requestWithAuth(
6881
+ config,
6882
+ target.profileName,
6883
+ `/permission/api/role/${encodeURIComponent(roleId)}`
6884
+ );
6885
+ return normalizeApiPermissionRecords(data);
6886
+ }
6887
+
6888
+ async function hydrateRoleApiPermissions(config, target, roleMap, desiredRoles) {
6889
+ const roles = (desiredRoles || []).filter(roleDeclaresApiPermissions);
6890
+ if (roles.length === 0) return;
6891
+ for (const role of roles) {
6892
+ const code = role.code || role.resourceCode;
6893
+ const existing = roleMap.get(code);
6894
+ if (!existing?.id) continue;
6895
+ const permissions = await fetchRoleApiPermissions(config, target, existing.id);
6896
+ roleMap.set(code, {
6897
+ ...existing,
6898
+ apiPermissionCodes: normalizePermissionCodeArray(
6899
+ permissions.map(item => item.code)
6900
+ ),
6901
+ });
6902
+ }
6903
+ }
6904
+
6905
+ async function fetchAppApiPermissionMap(config, target) {
6906
+ const data = await requestWithAuth(
6907
+ config,
6908
+ target.profileName,
6909
+ apiPathWithQuery('/permission/api', {
6910
+ scope: 'app',
6911
+ appType: target.appType,
6912
+ page: 1,
6913
+ limit: 1000,
6914
+ })
6915
+ );
6916
+ const map = new Map();
6917
+ for (const item of normalizeApiPermissionRecords(data)) {
6918
+ map.set(String(item.code), item);
6919
+ }
6920
+ return map;
6921
+ }
6922
+
6923
+ async function syncRoleApiPermissions(config, target, role, roleId) {
6924
+ if (!roleDeclaresApiPermissions(role) || !roleId) return null;
6925
+ const desiredCodes = normalizePermissionCodeArray(role.apiPermissionCodes);
6926
+ const mode = normalizeApiPermissionMode(role);
6927
+ const currentPermissions = await fetchRoleApiPermissions(config, target, roleId);
6928
+ const currentCodes = normalizePermissionCodeArray(
6929
+ currentPermissions.map(item => item.code)
6930
+ );
6931
+ const nextCodes =
6932
+ mode === 'replace' ? desiredCodes : unique([...currentCodes, ...desiredCodes]);
6933
+ if (sameStringSet(currentCodes, nextCodes)) {
6934
+ return { action: 'noop', mode, codes: desiredCodes };
6935
+ }
6936
+
6937
+ const permissionMap = await fetchAppApiPermissionMap(config, target);
6938
+ const currentByCode = new Map(
6939
+ currentPermissions.map(item => [String(item.code), item])
6940
+ );
6941
+ const permissionIds = nextCodes.map(code => {
6942
+ const permission = currentByCode.get(code) || permissionMap.get(code);
6943
+ if (!permission?.id) {
6944
+ fail(
6945
+ `${resourceLabel('role', role)}: 未找到接口权限点 ${code},请确认平台已 seed 该权限`
6946
+ );
6947
+ }
6948
+ return permission.id;
6949
+ });
6950
+
6951
+ await requestWithAuth(config, target.profileName, '/permission/api/assign', {
6952
+ method: 'POST',
6953
+ body: { roleId, permissionIds },
6954
+ });
6955
+
6956
+ return { action: 'update', mode, codes: desiredCodes };
6957
+ }
6958
+
6643
6959
  function addPlanActions(actions, kind, desiredItems = [], existingMap, equals) {
6644
6960
  for (const item of desiredItems) {
6645
6961
  const code = item.code || item.resourceCode || item.methodName;
@@ -6821,7 +7137,19 @@ async function publishRoleResources(config, target, roles, result, options = {})
6821
7137
  { method: 'POST', body: { userIds: role.userIds } }
6822
7138
  );
6823
7139
  }
6824
- result.published.push({ kind: 'role', code: role.code, action: existing ? 'update' : 'create', id: data?.id });
7140
+ const apiPermissions = await syncRoleApiPermissions(
7141
+ config,
7142
+ target,
7143
+ role,
7144
+ data?.id
7145
+ );
7146
+ result.published.push({
7147
+ kind: 'role',
7148
+ code: role.code,
7149
+ action: existing ? 'update' : 'create',
7150
+ id: data?.id,
7151
+ ...(apiPermissions ? { apiPermissions } : {}),
7152
+ });
6825
7153
  }
6826
7154
  }
6827
7155
 
@@ -8124,10 +8452,17 @@ async function pullResources(config, target) {
8124
8452
  );
8125
8453
  for (const role of normalizeItems(roles)) {
8126
8454
  const filePath = path.join(baseDir, 'roles', `${role.code}.json`);
8455
+ const apiPermissions = role.id
8456
+ ? await fetchRoleApiPermissions(config, target, role.id)
8457
+ : [];
8458
+ const apiPermissionCodes = normalizePermissionCodeArray(
8459
+ apiPermissions.map(item => item.code)
8460
+ );
8127
8461
  writeResourceJsonFile(filePath, {
8128
8462
  code: role.code,
8129
8463
  name: role.name,
8130
8464
  description: role.description || '',
8465
+ ...(apiPermissionCodes.length > 0 ? { apiPermissionCodes } : {}),
8131
8466
  });
8132
8467
  written.push(path.relative(process.cwd(), filePath));
8133
8468
  }
@@ -9313,10 +9648,17 @@ function withoutResourceMeta(value) {
9313
9648
  }
9314
9649
 
9315
9650
  function roleEquals(desired, existing) {
9316
- return (
9651
+ const metadataEquals =
9317
9652
  String(existing.name || '') === String(desired.name || desired.code || '') &&
9318
- String(existing.description || '') === String(desired.description || '')
9319
- );
9653
+ String(existing.description || '') === String(desired.description || '');
9654
+ if (!metadataEquals) return false;
9655
+ if (!roleDeclaresApiPermissions(desired)) return true;
9656
+
9657
+ const desiredCodes = normalizePermissionCodeArray(desired.apiPermissionCodes);
9658
+ const existingCodes = normalizePermissionCodeArray(existing.apiPermissionCodes);
9659
+ return normalizeApiPermissionMode(desired) === 'replace'
9660
+ ? sameStringSet(desiredCodes, existingCodes)
9661
+ : isSubsetStringSet(desiredCodes, existingCodes);
9320
9662
  }
9321
9663
 
9322
9664
  function menuEquals(bound, desired, existing) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.127",
3
+ "version": "1.0.129",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -252,10 +252,13 @@ interface RuntimeRequestConfig {
252
252
  headers?: Record<string, string>;
253
253
  responseType?: 'json' | 'blob';
254
254
  }
255
+ type RuntimeUploadProvider = 'platform' | 'oss' | 'builtin-oss';
255
256
  interface RuntimeUploadOptions {
256
- uploadProvider?: 'platform' | 'oss';
257
+ uploadProvider?: RuntimeUploadProvider;
258
+ storageScope?: 'app' | 'platform';
257
259
  storageCode?: string;
258
260
  appType?: string;
261
+ uploadPurpose?: 'attachment' | 'image';
259
262
  imageCompression?: ImageCompressionConfig;
260
263
  }
261
264
  interface FormRuntimeApi {
@@ -297,6 +300,7 @@ interface FormEngineConfig {
297
300
  mode: FormEngineMode;
298
301
  formUuid: string;
299
302
  appType: string;
303
+ defaultUploadProvider?: RuntimeUploadProvider;
300
304
  formInstanceId?: string;
301
305
  submitBehavior?: FormSubmitBehavior;
302
306
  permissions?: {
@@ -592,6 +596,8 @@ interface AttachmentItem {
592
596
  uid?: string;
593
597
  status?: 'uploading' | 'done' | 'error';
594
598
  provider?: 'platform' | 'oss';
599
+ uploadProvider?: RuntimeUploadProvider;
600
+ storageScope?: 'app' | 'platform';
595
601
  storageCode?: string;
596
602
  appType?: string;
597
603
  objectName?: string;
@@ -642,8 +648,8 @@ interface AttachmentFieldProps extends BaseFieldProps {
642
648
  maxSize?: number;
643
649
  uploadAction?: string;
644
650
  bucketName?: string;
645
- /** 自定义上传提供方;设置为 oss 时需同时配置 storageCode。 */
646
- uploadProvider?: 'platform' | 'oss';
651
+ /** 自定义上传提供方;oss 需配置 storageCode,builtin-oss 使用平台内置 OSS。 */
652
+ uploadProvider?: RuntimeUploadProvider;
647
653
  /** src/resources/storage/<code>.json 中声明的存储 code。 */
648
654
  storageCode?: string;
649
655
  multiple?: boolean;
@@ -664,8 +670,8 @@ interface ImageFieldProps extends BaseFieldProps {
664
670
  accept?: string;
665
671
  uploadAction?: string;
666
672
  bucketName?: string;
667
- /** 自定义上传提供方;设置为 oss 时需同时配置 storageCode。 */
668
- uploadProvider?: 'platform' | 'oss';
673
+ /** 自定义上传提供方;oss 需配置 storageCode,builtin-oss 使用平台内置 OSS。 */
674
+ uploadProvider?: RuntimeUploadProvider;
669
675
  /** src/resources/storage/<code>.json 中声明的存储 code。 */
670
676
  storageCode?: string;
671
677
  multiple?: boolean;
@@ -1156,4 +1162,4 @@ interface ProcessPreviewProps {
1156
1162
  }
1157
1163
  declare const ProcessPreview: React__default.FC<ProcessPreviewProps>;
1158
1164
 
1159
- export { type FormRuntimeApiConfig as $, type AddressFieldProps as A, type BaseFieldProps as B, type CascadeDateFieldProps as C, type DataFilter as D, type DepartmentSelectFieldProps as E, type DepartmentTreeNode as F, type DigitalSignatureFieldProps as G, type DigitalSignatureValue as H, type EditorChoiceOption as I, type EditorFieldProps as J, type EditorToolbarAction as K, type FieldBehavior as L, type FieldDefinition as M, type FieldLayoutNode as N, type FieldValueSyncConfig as O, type FormAppearanceConfig as P, type FormDataDeleteParams as Q, type FormDataQueryParams as R, type FormEffect as S, type FormEffectAction as T, type FormEffectCondition as U, type FormEffectConditionOperator as V, type FormEngineConfig as W, type FormEngineMode as X, type FormInstanceData as Y, type FormLayoutNode as Z, type FormRuntimeApi as _, type AddressValue as a, type SubFormFieldProps as a$, type FormRuntimeConfig as a0, type FormSchema as a1, FormSection as a2, type FormSectionProps as a3, type FormSubmitBehavior as a4, type FormTemplateConfig as a5, type GridLayoutCell as a6, type GridLayoutNode as a7, type ImageCompressionConfig as a8, type ImageCompressionVariantConfig as a9, type ProcessNodeType as aA, ProcessPreview as aB, type ProcessPreviewProps as aC, type ProcessRoute as aD, type ProcessStatus as aE, type ProcessTask as aF, type RadioFieldProps as aG, type ResubmitParams as aH, type ReturnParams as aI, type ReturnPolicy as aJ, type ReturnableNode as aK, type ReturnableNodeResult as aL, type RuntimeDataQueryParams as aM, type RuntimeDataQueryResult as aN, type RuntimeRequestConfig as aO, type RuntimeResponse as aP, type RuntimeUploadOptions as aQ, type SaveTaskParams as aR, type SectionLayoutNode as aS, type SelectFieldProps as aT, type SerialNumberFieldProps as aU, type SignaturePoint as aV, type StandardFormPageMode as aW, type StatusMeta as aX, type StepLayoutItem as aY, type StepsLayoutNode as aZ, type SubFormColumn as a_, type ImageFieldProps as aa, type ImageVariant as ab, type InitiatorSelectCandidate as ac, type InitiatorSelectRequirement as ad, type InitiatorSelectScope as ae, type InitiatorSelectedApprovers as af, type JSONFieldProps as ag, type LayoutVisibleWhen as ah, type LinkedFormOptionConfig as ai, type LocationFieldProps as aj, type LocationValue as ak, type LowcodePageMeta as al, type LowcodePageNode as am, type LowcodePageNodeType as an, type LowcodePageSchema as ao, type MultiSelectFieldProps as ap, type NumberFieldProps as aq, type OptionItem as ar, type OptionSourceConfig as as, type OptionSourceType as at, type PeopleShortcutConfig as au, type PeopleShortcutType as av, type PreviewParams as aw, type ProcessAction as ax, type ProcessBasicInfo as ay, type ProcessDefinition as az, type ApprovalActionType as b, type TabLayoutItem as b0, type TabsLayoutNode as b1, type TaskStatus as b2, type TextAreaFieldProps as b3, type TextFieldProps as b4, type TextShortcutConfig as b5, type TextShortcutType as b6, type TransferParams as b7, type UserDisplayFormat as b8, type UserItem as b9, type UserSelectFieldProps as ba, type ValidationPreset as bb, type ValidationRule as bc, type ViewPermissionQueryParams as bd, type ViewPermissionSummary as be, type WithdrawParams as bf, type ApprovalPermission as c, ApprovalTimeline as d, type ApprovalTimelineProps as e, type ApproveParams as f, type AssociationFormConfig as g, type AssociationFormFieldProps as h, type AssociationValue as i, type AttachmentFieldProps as j, type AttachmentImageVariants as k, type AttachmentItem as l, type BaseLayoutNode as m, type CascadeSelectFieldProps as n, type ChangeRecord as o, type ChangeRecordListResponse as p, type ChangeRecordQueryParams as q, type CheckboxFieldProps as r, type DataLinkageCondition as s, type DataLinkageConfig as t, type DateFieldProps as u, type DateRangeRestriction as v, type DateRestrictionConfig as w, type DateShortcutConfig as x, type DateShortcutType as y, type DefaultValueLinkageConfig as z };
1165
+ export { type FormRuntimeApiConfig as $, type AddressFieldProps as A, type BaseFieldProps as B, type CascadeDateFieldProps as C, type DataFilter as D, type DepartmentSelectFieldProps as E, type DepartmentTreeNode as F, type DigitalSignatureFieldProps as G, type DigitalSignatureValue as H, type EditorChoiceOption as I, type EditorFieldProps as J, type EditorToolbarAction as K, type FieldBehavior as L, type FieldDefinition as M, type FieldLayoutNode as N, type FieldValueSyncConfig as O, type FormAppearanceConfig as P, type FormDataDeleteParams as Q, type FormDataQueryParams as R, type FormEffect as S, type FormEffectAction as T, type FormEffectCondition as U, type FormEffectConditionOperator as V, type FormEngineConfig as W, type FormEngineMode as X, type FormInstanceData as Y, type FormLayoutNode as Z, type FormRuntimeApi as _, type AddressValue as a, type SubFormColumn as a$, type FormRuntimeConfig as a0, type FormSchema as a1, FormSection as a2, type FormSectionProps as a3, type FormSubmitBehavior as a4, type FormTemplateConfig as a5, type GridLayoutCell as a6, type GridLayoutNode as a7, type ImageCompressionConfig as a8, type ImageCompressionVariantConfig as a9, type ProcessNodeType as aA, ProcessPreview as aB, type ProcessPreviewProps as aC, type ProcessRoute as aD, type ProcessStatus as aE, type ProcessTask as aF, type RadioFieldProps as aG, type ResubmitParams as aH, type ReturnParams as aI, type ReturnPolicy as aJ, type ReturnableNode as aK, type ReturnableNodeResult as aL, type RuntimeDataQueryParams as aM, type RuntimeDataQueryResult as aN, type RuntimeRequestConfig as aO, type RuntimeResponse as aP, type RuntimeUploadOptions as aQ, type RuntimeUploadProvider as aR, type SaveTaskParams as aS, type SectionLayoutNode as aT, type SelectFieldProps as aU, type SerialNumberFieldProps as aV, type SignaturePoint as aW, type StandardFormPageMode as aX, type StatusMeta as aY, type StepLayoutItem as aZ, type StepsLayoutNode as a_, type ImageFieldProps as aa, type ImageVariant as ab, type InitiatorSelectCandidate as ac, type InitiatorSelectRequirement as ad, type InitiatorSelectScope as ae, type InitiatorSelectedApprovers as af, type JSONFieldProps as ag, type LayoutVisibleWhen as ah, type LinkedFormOptionConfig as ai, type LocationFieldProps as aj, type LocationValue as ak, type LowcodePageMeta as al, type LowcodePageNode as am, type LowcodePageNodeType as an, type LowcodePageSchema as ao, type MultiSelectFieldProps as ap, type NumberFieldProps as aq, type OptionItem as ar, type OptionSourceConfig as as, type OptionSourceType as at, type PeopleShortcutConfig as au, type PeopleShortcutType as av, type PreviewParams as aw, type ProcessAction as ax, type ProcessBasicInfo as ay, type ProcessDefinition as az, type ApprovalActionType as b, type SubFormFieldProps as b0, type TabLayoutItem as b1, type TabsLayoutNode as b2, type TaskStatus as b3, type TextAreaFieldProps as b4, type TextFieldProps as b5, type TextShortcutConfig as b6, type TextShortcutType as b7, type TransferParams as b8, type UserDisplayFormat as b9, type UserItem as ba, type UserSelectFieldProps as bb, type ValidationPreset as bc, type ValidationRule as bd, type ViewPermissionQueryParams as be, type ViewPermissionSummary as bf, type WithdrawParams as bg, type ApprovalPermission as c, ApprovalTimeline as d, type ApprovalTimelineProps as e, type ApproveParams as f, type AssociationFormConfig as g, type AssociationFormFieldProps as h, type AssociationValue as i, type AttachmentFieldProps as j, type AttachmentImageVariants as k, type AttachmentItem as l, type BaseLayoutNode as m, type CascadeSelectFieldProps as n, type ChangeRecord as o, type ChangeRecordListResponse as p, type ChangeRecordQueryParams as q, type CheckboxFieldProps as r, type DataLinkageCondition as s, type DataLinkageConfig as t, type DateFieldProps as u, type DateRangeRestriction as v, type DateRestrictionConfig as w, type DateShortcutConfig as x, type DateShortcutType as y, type DefaultValueLinkageConfig as z };