openxiangda 1.0.83 → 1.0.85

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.
package/lib/cli.js CHANGED
@@ -35,6 +35,8 @@ const { version: CURRENT_VERSION } = require('../package.json');
35
35
 
36
36
  const NPM_PACKAGE_NAME = 'openxiangda';
37
37
  const OFFICIAL_NPM_REGISTRY = 'https://registry.npmjs.org';
38
+ const RUNTIME_UPLOAD_DEFAULT_TIMEOUT_MS = 120000;
39
+ const RUNTIME_UPLOAD_CONCURRENCY = 3;
38
40
 
39
41
  async function main(argv) {
40
42
  const [command, ...rest] = argv;
@@ -114,7 +116,7 @@ Usage:
114
116
  openxiangda permission form-group-list|form-group-create|form-group-bind
115
117
  openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
116
118
  openxiangda resource validate|plan|publish|pull [--profile name] [--json]
117
- openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--no-build] [--no-activate] [--json]
119
+ openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode staged|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
118
120
  openxiangda runtime releases [--profile name] [--json]
119
121
  openxiangda runtime activate <releaseId> [--profile name] [--json]
120
122
  openxiangda inspect app|form|workflow|automation|permissions
@@ -2766,30 +2768,57 @@ async function runtime(args) {
2766
2768
  const buildId = normalizeRuntimeBuildId(flags['build-id'] || createRuntimeBuildId());
2767
2769
  const distDir = path.resolve(process.cwd(), flags.dist || 'dist');
2768
2770
  const assetBaseUrl = buildRuntimeAssetBaseUrl(target.appType, buildId);
2771
+ const uploadMode = normalizeRuntimeUploadMode(flags['upload-mode']);
2772
+ const uploadTimeoutMs = normalizeRuntimeUploadTimeoutMs(flags['upload-timeout-ms']);
2773
+ const traceId = createRuntimeTraceId();
2769
2774
  if (!flags['no-build']) {
2770
2775
  runRuntimeBuild({
2771
2776
  buildId,
2772
2777
  appType: target.appType,
2773
2778
  assetBaseUrl,
2774
2779
  command: flags['build-command'],
2780
+ jsonOutput: Boolean(flags.json),
2775
2781
  });
2776
2782
  }
2777
2783
  const files = collectRuntimeDistFiles(distDir, {
2778
2784
  includeSourceMaps: Boolean(flags['include-sourcemaps']),
2779
2785
  });
2780
2786
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
2787
+ printRuntimeProgress(
2788
+ `runtime release upload: mode=${uploadMode} traceId=${traceId} files=${files.length} size=${formatBytes(totalBytes)} timeout=${uploadTimeoutMs}ms`
2789
+ );
2790
+ const releaseFiles =
2791
+ uploadMode === 'legacy-json'
2792
+ ? files.map(file => ({
2793
+ path: file.path,
2794
+ size: file.size,
2795
+ sha256: file.sha256,
2796
+ contentType: file.contentType,
2797
+ contentBase64: file.buffer.toString('base64'),
2798
+ }))
2799
+ : await uploadRuntimeDistFilesStaged({
2800
+ config,
2801
+ profileName: target.profileName,
2802
+ appType: target.appType,
2803
+ buildId,
2804
+ files,
2805
+ traceId,
2806
+ timeoutMs: uploadTimeoutMs,
2807
+ });
2781
2808
  const data = await requestWithAuth(
2782
2809
  config,
2783
2810
  target.profileName,
2784
2811
  `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`,
2785
2812
  {
2786
2813
  method: 'POST',
2814
+ timeoutMs: uploadTimeoutMs,
2815
+ headers: { 'x-openxiangda-trace-id': traceId },
2787
2816
  body: {
2788
2817
  buildId,
2789
2818
  version: flags.version || readWorkspaceVersion(),
2790
2819
  releaseNotes: flags.notes || flags['release-notes'] || '',
2791
2820
  activate: !flags['no-activate'],
2792
- files,
2821
+ files: releaseFiles,
2793
2822
  },
2794
2823
  }
2795
2824
  );
@@ -2803,6 +2832,8 @@ async function runtime(args) {
2803
2832
  totalBytes,
2804
2833
  assetBaseUrl,
2805
2834
  activated: !flags['no-activate'],
2835
+ uploadMode,
2836
+ traceId,
2806
2837
  };
2807
2838
  if (flags.json) return writeJson(result);
2808
2839
  print(
@@ -2810,6 +2841,8 @@ async function runtime(args) {
2810
2841
  `runtime release 已上传: ${target.appType}`,
2811
2842
  `build: ${buildId}`,
2812
2843
  `files: ${files.length} (${formatBytes(totalBytes)})`,
2844
+ `upload: ${uploadMode}`,
2845
+ `traceId: ${traceId}`,
2813
2846
  `assetBase: ${assetBaseUrl}`,
2814
2847
  `active: ${result.activated ? 'yes' : 'no'}`,
2815
2848
  ].join('\n')
@@ -2822,11 +2855,16 @@ async function runtime(args) {
2822
2855
 
2823
2856
  function runRuntimeBuild(options) {
2824
2857
  const command = options.command || defaultRuntimeBuildCommand();
2825
- print(`构建 React SPA runtime: ${command}`);
2858
+ if (options.jsonOutput) {
2859
+ printRuntimeProgress(`构建 React SPA runtime: ${command}`);
2860
+ } else {
2861
+ print(`构建 React SPA runtime: ${command}`);
2862
+ }
2826
2863
  const result = spawnSync(command, [], {
2827
2864
  cwd: process.cwd(),
2828
2865
  shell: true,
2829
- stdio: 'inherit',
2866
+ stdio: options.jsonOutput ? 'pipe' : 'inherit',
2867
+ encoding: options.jsonOutput ? 'utf8' : undefined,
2830
2868
  env: {
2831
2869
  ...process.env,
2832
2870
  OPENXIANGDA_APP_TYPE: options.appType,
@@ -2835,6 +2873,10 @@ function runRuntimeBuild(options) {
2835
2873
  OPENXIANGDA_RUNTIME_ASSET_BASE: options.assetBaseUrl,
2836
2874
  },
2837
2875
  });
2876
+ if (options.jsonOutput) {
2877
+ if (result.stdout) process.stderr.write(maskText(result.stdout));
2878
+ if (result.stderr) process.stderr.write(maskText(result.stderr));
2879
+ }
2838
2880
  if (result.error) fail(`runtime build 无法启动: ${result.error.message}`);
2839
2881
  if (result.status !== 0) fail(`runtime build 失败: exit ${result.status}`);
2840
2882
  }
@@ -2856,7 +2898,7 @@ function collectRuntimeDistFiles(distDir, options = {}) {
2856
2898
  }
2857
2899
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
2858
2900
  if (totalBytes > 25 * 1024 * 1024) {
2859
- fail(`runtime dist 过大: ${formatBytes(totalBytes)},当前 JSON 发布通道上限为 25MB`);
2901
+ fail(`runtime dist 过大: ${formatBytes(totalBytes)},当前 runtime 发布通道上限为 25MB`);
2860
2902
  }
2861
2903
  return files;
2862
2904
  }
@@ -2878,11 +2920,112 @@ function walkRuntimeDist(rootDir, currentDir, files, options) {
2878
2920
  size: buffer.length,
2879
2921
  sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
2880
2922
  contentType: inferRuntimeContentType(relative),
2881
- contentBase64: buffer.toString('base64'),
2923
+ buffer,
2882
2924
  });
2883
2925
  }
2884
2926
  }
2885
2927
 
2928
+ async function uploadRuntimeDistFilesStaged(options) {
2929
+ const files = options.files || [];
2930
+ let completed = 0;
2931
+ const results = await runWithConcurrency(
2932
+ files,
2933
+ RUNTIME_UPLOAD_CONCURRENCY,
2934
+ async file => {
2935
+ const uploaded = await uploadRuntimeDistFile(options, file);
2936
+ completed += 1;
2937
+ printRuntimeProgress(
2938
+ `runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
2939
+ );
2940
+ return uploaded;
2941
+ }
2942
+ );
2943
+ return results.map(file => ({
2944
+ path: file.path,
2945
+ size: file.size,
2946
+ sha256: file.sha256,
2947
+ contentType: file.contentType,
2948
+ }));
2949
+ }
2950
+
2951
+ async function uploadRuntimeDistFile(options, file) {
2952
+ const apiPath = apiPathWithQuery(
2953
+ `/openxiangda-api/v1/apps/${encodeURIComponent(options.appType)}/runtime/releases/files`,
2954
+ {
2955
+ buildId: options.buildId,
2956
+ path: file.path,
2957
+ size: file.size,
2958
+ sha256: file.sha256,
2959
+ contentType: file.contentType,
2960
+ }
2961
+ );
2962
+ return await requestFormWithAuth(
2963
+ options.config,
2964
+ options.profileName,
2965
+ apiPath,
2966
+ () => {
2967
+ const form = new FormData();
2968
+ form.append(
2969
+ 'file',
2970
+ new Blob([file.buffer], { type: file.contentType }),
2971
+ path.basename(file.path)
2972
+ );
2973
+ return form;
2974
+ },
2975
+ {
2976
+ timeoutMs: options.timeoutMs,
2977
+ headers: { 'x-openxiangda-trace-id': options.traceId },
2978
+ }
2979
+ );
2980
+ }
2981
+
2982
+ async function runWithConcurrency(items, concurrency, worker) {
2983
+ const results = new Array(items.length);
2984
+ let nextIndex = 0;
2985
+ let firstError = null;
2986
+ const workers = Array.from(
2987
+ { length: Math.min(concurrency, items.length) },
2988
+ async () => {
2989
+ while (nextIndex < items.length && !firstError) {
2990
+ const index = nextIndex;
2991
+ nextIndex += 1;
2992
+ try {
2993
+ results[index] = await worker(items[index], index);
2994
+ } catch (error) {
2995
+ firstError = error;
2996
+ }
2997
+ }
2998
+ }
2999
+ );
3000
+ await Promise.all(workers);
3001
+ if (firstError) throw firstError;
3002
+ return results;
3003
+ }
3004
+
3005
+ function normalizeRuntimeUploadMode(value) {
3006
+ const normalized = String(value || 'staged').trim().toLowerCase();
3007
+ if (normalized === 'staged' || normalized === 'legacy-json') return normalized;
3008
+ fail(`--upload-mode 只支持 staged 或 legacy-json,当前: ${value}`);
3009
+ }
3010
+
3011
+ function normalizeRuntimeUploadTimeoutMs(value) {
3012
+ const candidate =
3013
+ value === undefined || value === null || value === ''
3014
+ ? process.env.OPENXIANGDA_RUNTIME_UPLOAD_TIMEOUT_MS
3015
+ : value;
3016
+ const parsed = Number(candidate);
3017
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3018
+ return RUNTIME_UPLOAD_DEFAULT_TIMEOUT_MS;
3019
+ }
3020
+
3021
+ function createRuntimeTraceId() {
3022
+ return `oxd-${Date.now().toString(36)}-${crypto.randomBytes(4).toString('hex')}`;
3023
+ }
3024
+
3025
+ function printRuntimeProgress(message) {
3026
+ process.stderr.write(`${maskText(message)}\n`);
3027
+ }
3028
+
2886
3029
  function buildRuntimeAssetBaseUrl(appType, buildId) {
2887
3030
  return `/service/openxiangda-api/v1/apps/${encodeURIComponent(appType)}/runtime/releases/by-build/${encodeURIComponent(buildId)}/files/`;
2888
3031
  }
@@ -3231,6 +3374,7 @@ function ensureResourceBuckets(bound) {
3231
3374
  bound.resources.roles = bound.resources.roles || {};
3232
3375
  bound.resources.connectors = bound.resources.connectors || {};
3233
3376
  bound.resources.dataViews = bound.resources.dataViews || {};
3377
+ bound.resources.authConfigs = bound.resources.authConfigs || {};
3234
3378
  bound.resources.notifications = bound.resources.notifications || {};
3235
3379
  bound.resources.notifications.templates = bound.resources.notifications.templates || {};
3236
3380
  bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
@@ -3441,6 +3585,14 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
3441
3585
  }, keys);
3442
3586
  }
3443
3587
 
3588
+ function saveAuthConfigResource(target, authConfigCode, configId, extra = {}) {
3589
+ const keys = ['configId', 'status'];
3590
+ saveStateResource(target, 'authConfigs', authConfigCode, {
3591
+ ...pickStateFields(extra, keys),
3592
+ configId,
3593
+ }, keys);
3594
+ }
3595
+
3444
3596
  function saveRoleResource(target, roleCode, roleId) {
3445
3597
  saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
3446
3598
  }
@@ -3548,6 +3700,7 @@ const RESOURCE_SPECS = [
3548
3700
  { key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
3549
3701
  { key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
3550
3702
  { key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
3703
+ { key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
3551
3704
  {
3552
3705
  key: 'pagePermissionGroups',
3553
3706
  dir: path.join('permissions', 'page-groups'),
@@ -3747,6 +3900,7 @@ function generateResourceTypes(manifest, outputFile) {
3747
3900
  );
3748
3901
  const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
3749
3902
  const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
3903
+ const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
3750
3904
  const content = [
3751
3905
  '/* eslint-disable */',
3752
3906
  '// Generated by openxiangda resource typegen. Do not edit manually.',
@@ -3766,6 +3920,9 @@ function generateResourceTypes(manifest, outputFile) {
3766
3920
  `export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
3767
3921
  'export type FunctionCode = typeof functionCodes[number]',
3768
3922
  '',
3923
+ `export const authConfigCodes = ${JSON.stringify(authConfigCodes, null, 2)} as const`,
3924
+ 'export type AuthConfigCode = typeof authConfigCodes[number]',
3925
+ '',
3769
3926
  `export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
3770
3927
  '',
3771
3928
  `export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
@@ -3780,6 +3937,7 @@ function generateResourceTypes(manifest, outputFile) {
3780
3937
  pagePermissionGroups: pagePermissionGroupCodes.length,
3781
3938
  dataViews: dataViewCodes.length,
3782
3939
  functions: functionCodes.length,
3940
+ authConfigs: authConfigCodes.length,
3783
3941
  };
3784
3942
  }
3785
3943
 
@@ -3858,6 +4016,32 @@ function validateResourceItem(kind, item, errors, warnings) {
3858
4016
  }
3859
4017
  validateDataViewPerformance(label, definition, errors, warnings, storageMode);
3860
4018
  }
4019
+ if (kind === 'authConfigs') {
4020
+ const config = item.configJson || item.config || item;
4021
+ if (!Array.isArray(config.methods)) {
4022
+ errors.push(`${label}: 缺少 methods`);
4023
+ } else {
4024
+ config.methods.forEach((method, index) => {
4025
+ if (!method?.type) errors.push(`${label}.methods[${index}]: 缺少 type`);
4026
+ if (
4027
+ method?.type === 'phone_code' &&
4028
+ method.enabled !== false &&
4029
+ !(
4030
+ method.provider?.functionCode ||
4031
+ method.providerFunctionCode ||
4032
+ config.providers?.phone_code?.functionCode ||
4033
+ config.providers?.phoneCode?.functionCode
4034
+ )
4035
+ ) {
4036
+ errors.push(`${label}.methods[${index}]: phone_code 缺少 provider.functionCode`);
4037
+ }
4038
+ });
4039
+ }
4040
+ const registrationMode = String(config.registration?.mode || 'reject');
4041
+ if (registrationMode !== 'reject') {
4042
+ warnings.push(`${label}: 已开启非默认注册策略 ${registrationMode},请确认身份匹配键、默认角色和审计字段`);
4043
+ }
4044
+ }
3861
4045
  if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
3862
4046
  if (kind === 'formPermissionGroups') {
3863
4047
  if (!item.name) errors.push(`${label}: 缺少 name`);
@@ -4259,6 +4443,7 @@ async function buildResourcePlan(config, target, manifest) {
4259
4443
  addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
4260
4444
  await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
4261
4445
  addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
4446
+ addPlanActions(actions, 'authConfig', manifest.authConfigs, existing.authConfigs, (item, current) => authConfigEquals(item, current));
4262
4447
  await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
4263
4448
  addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
4264
4449
  addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
@@ -4294,6 +4479,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
4294
4479
  await publishNotificationResources(config, target, manifest.notifications || [], result);
4295
4480
  await publishWorkflowResources(config, target, manifest.workflows || [], result);
4296
4481
  await publishFunctionResources(config, target, manifest.functions || [], result);
4482
+ await publishAuthConfigResources(config, target, manifest.authConfigs || [], result);
4297
4483
  await publishAutomationResources(config, target, manifest.automations || [], result);
4298
4484
  await publishDataViewResources(config, target, manifest.dataViews || [], result);
4299
4485
  await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
@@ -4317,6 +4503,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
4317
4503
  notificationTypeConfigs: new Map(),
4318
4504
  workflows: new Map(),
4319
4505
  functions: new Map(),
4506
+ authConfigs: new Map(),
4320
4507
  automations: new Map(),
4321
4508
  dataViews: new Map(),
4322
4509
  pagePermissionGroups: new Map(),
@@ -4422,6 +4609,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
4422
4609
  });
4423
4610
  }
4424
4611
  }
4612
+ if ((manifest.authConfigs || []).length > 0) {
4613
+ const data = await requestWithAuth(
4614
+ config,
4615
+ target.profileName,
4616
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
4617
+ page: 1,
4618
+ pageSize: 1000,
4619
+ })
4620
+ );
4621
+ indexByCode(maps.authConfigs, normalizeItems(data), item => item.code || item.resourceCode);
4622
+ for (const item of manifest.authConfigs || []) {
4623
+ const code = item.code || item.resourceCode;
4624
+ const existing = maps.authConfigs.get(code);
4625
+ if (!existing) continue;
4626
+ const detail = await requestOptionalWithAuth(
4627
+ config,
4628
+ target.profileName,
4629
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
4630
+ );
4631
+ maps.authConfigs.set(code, {
4632
+ ...existing,
4633
+ ...(detail || {}),
4634
+ });
4635
+ }
4636
+ }
4425
4637
  if ((manifest.dataViews || []).length > 0) {
4426
4638
  const data = await requestWithAuth(
4427
4639
  config,
@@ -5084,6 +5296,38 @@ async function publishFunctionResources(config, target, functions, result) {
5084
5296
  }
5085
5297
  }
5086
5298
 
5299
+ async function publishAuthConfigResources(config, target, authConfigs, result) {
5300
+ for (const authItem of authConfigs) {
5301
+ const code = authItem.code || authItem.resourceCode || 'default';
5302
+ const existing = await findExistingAuthConfig(config, target, code);
5303
+ const body = normalizeAuthConfigManifest(authItem);
5304
+ const data = existing
5305
+ ? await requestWithAuth(
5306
+ config,
5307
+ target.profileName,
5308
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
5309
+ { method: 'PUT', body }
5310
+ )
5311
+ : await requestWithAuth(
5312
+ config,
5313
+ target.profileName,
5314
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`,
5315
+ { method: 'POST', body }
5316
+ );
5317
+ if (data?.id) {
5318
+ saveAuthConfigResource(target, data.code || code, data.id, {
5319
+ status: data.status,
5320
+ });
5321
+ }
5322
+ result.published.push({
5323
+ kind: 'authConfig',
5324
+ code,
5325
+ action: existing ? 'update' : 'create',
5326
+ id: data?.id,
5327
+ });
5328
+ }
5329
+ }
5330
+
5087
5331
  async function publishDataViewResources(config, target, dataViews, result) {
5088
5332
  for (const dataViewItem of dataViews) {
5089
5333
  const existing = await findExistingDataView(config, target, dataViewItem.code);
@@ -5129,6 +5373,18 @@ async function findExistingFunction(config, target, code) {
5129
5373
  return null;
5130
5374
  }
5131
5375
 
5376
+ async function findExistingAuthConfig(config, target, code) {
5377
+ const stateId = target.bound.resources?.authConfigs?.[code]?.configId;
5378
+ const byCode = await requestOptionalWithAuth(
5379
+ config,
5380
+ target.profileName,
5381
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
5382
+ );
5383
+ if (byCode?.id) return byCode;
5384
+ if (stateId) return { id: stateId, code };
5385
+ return null;
5386
+ }
5387
+
5132
5388
  async function findExistingDataView(config, target, code) {
5133
5389
  const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
5134
5390
  const byCode = await requestOptionalWithAuth(
@@ -5279,6 +5535,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
5279
5535
  await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
5280
5536
  await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
5281
5537
  await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
5538
+ await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
5282
5539
  await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
5283
5540
  await prunePagePermissionGroups(
5284
5541
  config,
@@ -5474,6 +5731,29 @@ async function pruneFunctions(config, target, desired, result) {
5474
5731
  }
5475
5732
  }
5476
5733
 
5734
+ async function pruneAuthConfigs(config, target, desired, result) {
5735
+ const data = await requestWithAuth(
5736
+ config,
5737
+ target.profileName,
5738
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
5739
+ page: 1,
5740
+ pageSize: 1000,
5741
+ })
5742
+ );
5743
+ for (const item of normalizeItems(data)) {
5744
+ const code = item.code || item.resourceCode;
5745
+ if (!code || desired.has(code)) continue;
5746
+ await pruneOne(config, target, result, 'authConfig', code, item.id, async () => {
5747
+ await requestWithAuth(
5748
+ config,
5749
+ target.profileName,
5750
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
5751
+ { method: 'DELETE' }
5752
+ );
5753
+ });
5754
+ }
5755
+ }
5756
+
5477
5757
  async function prunePagePermissionGroups(config, target, desired, result) {
5478
5758
  const data = await requestWithAuth(
5479
5759
  config,
@@ -5549,6 +5829,7 @@ function removeStateResource(target, kind, code) {
5549
5829
  workflow: 'workflows',
5550
5830
  automation: 'automations',
5551
5831
  function: 'functions',
5832
+ authConfig: 'authConfigs',
5552
5833
  dataView: 'dataViews',
5553
5834
  pagePermissionGroup: 'pagePermissionGroups',
5554
5835
  formPermissionGroup: 'formPermissionGroups',
@@ -5747,6 +6028,26 @@ async function pullResources(config, target) {
5747
6028
  written.push(path.relative(process.cwd(), filePath));
5748
6029
  }
5749
6030
 
6031
+ const authConfigs = await requestWithAuth(
6032
+ config,
6033
+ target.profileName,
6034
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
6035
+ page: 1,
6036
+ pageSize: 1000,
6037
+ })
6038
+ );
6039
+ for (const item of normalizeItems(authConfigs)) {
6040
+ const code = item.code || item.resourceCode || item.id;
6041
+ const detail = await requestWithAuth(
6042
+ config,
6043
+ target.profileName,
6044
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
6045
+ );
6046
+ const filePath = path.join(baseDir, 'auth', `${code}.json`);
6047
+ writeResourceJsonFile(filePath, toPulledAuthConfig(detail));
6048
+ written.push(path.relative(process.cwd(), filePath));
6049
+ }
6050
+
5750
6051
  const dataViews = await requestWithAuth(
5751
6052
  config,
5752
6053
  target.profileName,
@@ -5962,6 +6263,16 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
5962
6263
  });
5963
6264
  }
5964
6265
 
6266
+ function toPulledAuthConfig(authConfig) {
6267
+ return stripUndefinedValues({
6268
+ code: authConfig.code || authConfig.resourceCode,
6269
+ name: authConfig.name,
6270
+ description: authConfig.description || '',
6271
+ status: authConfig.status || 'active',
6272
+ configJson: authConfig.configJson || authConfig.config || {},
6273
+ });
6274
+ }
6275
+
5965
6276
  function rewriteDataViewDefinitionForManifest(definition, lookups) {
5966
6277
  rewriteDataViewSourceForManifest(definition.base, lookups);
5967
6278
  for (const join of definition.joins || []) {
@@ -6142,6 +6453,40 @@ function normalizeDataViewManifest(bound, dataView) {
6142
6453
  });
6143
6454
  }
6144
6455
 
6456
+ function normalizeAuthConfigManifest(authConfig) {
6457
+ const code = authConfig.code || authConfig.resourceCode || 'default';
6458
+ const configJson = clonePlainJson(
6459
+ authConfig.configJson || authConfig.config || withoutAuthConfigManifestMeta(authConfig)
6460
+ );
6461
+ return stripUndefinedValues({
6462
+ code,
6463
+ name: authConfig.name || configJson.name || code,
6464
+ description:
6465
+ authConfig.description !== undefined
6466
+ ? authConfig.description
6467
+ : configJson.description || '',
6468
+ status: authConfig.status || configJson.status || 'active',
6469
+ configJson: {
6470
+ ...configJson,
6471
+ methods: Array.isArray(configJson.methods) ? configJson.methods : [],
6472
+ registration: configJson.registration || { mode: 'reject' },
6473
+ binding: configJson.binding || { mode: 'auto' },
6474
+ },
6475
+ });
6476
+ }
6477
+
6478
+ function withoutAuthConfigManifestMeta(authConfig) {
6479
+ const next = withoutResourceMeta(authConfig);
6480
+ delete next.code;
6481
+ delete next.resourceCode;
6482
+ delete next.name;
6483
+ delete next.description;
6484
+ delete next.status;
6485
+ delete next.config;
6486
+ delete next.configJson;
6487
+ return next;
6488
+ }
6489
+
6145
6490
  function withoutDataViewManifestMeta(dataView) {
6146
6491
  const next = withoutResourceMeta(dataView);
6147
6492
  delete next.permissionGroups;
@@ -6611,6 +6956,18 @@ function functionEquals(target, desired, existing) {
6611
6956
  );
6612
6957
  }
6613
6958
 
6959
+ function authConfigEquals(desired, existing) {
6960
+ if (!existing) return false;
6961
+ const expected = normalizeAuthConfigManifest(desired);
6962
+ return (
6963
+ String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
6964
+ String(existing.name || '') === String(expected.name || '') &&
6965
+ String(existing.description || '') === String(expected.description || '') &&
6966
+ String(existing.status || 'active') === String(expected.status || 'active') &&
6967
+ jsonEqualsForPlan(existing.configJson || {}, expected.configJson || {}, desired.__dir)
6968
+ );
6969
+ }
6970
+
6614
6971
  function dataViewEquals(bound, desired, existing) {
6615
6972
  if (!existing) return false;
6616
6973
  const expected = normalizeDataViewManifest(bound, desired);
@@ -7087,7 +7444,7 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
7087
7444
  }
7088
7445
  }
7089
7446
 
7090
- async function requestFormWithAuth(config, profileName, apiPath, formDataFactory) {
7447
+ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory, options = {}) {
7091
7448
  const resolved = getProfile(config, profileName);
7092
7449
  const profile = resolved.profile;
7093
7450
  if (!profile.token?.accessToken) {
@@ -7099,7 +7456,8 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
7099
7456
  profile.baseUrl,
7100
7457
  apiPath,
7101
7458
  formDataFactory(),
7102
- profile.token.accessToken
7459
+ profile.token.accessToken,
7460
+ options
7103
7461
  );
7104
7462
  return unwrapApi(payload);
7105
7463
  } catch (error) {
@@ -7111,25 +7469,41 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
7111
7469
  profile.baseUrl,
7112
7470
  apiPath,
7113
7471
  formDataFactory(),
7114
- profile.token.accessToken
7472
+ profile.token.accessToken,
7473
+ options
7115
7474
  );
7116
7475
  return unwrapApi(payload);
7117
7476
  }
7118
7477
  }
7119
7478
 
7120
- async function requestForm(baseUrl, apiPath, formData, accessToken) {
7479
+ async function requestForm(baseUrl, apiPath, formData, accessToken, options = {}) {
7121
7480
  if (typeof fetch !== 'function') {
7122
7481
  throw new Error('当前 Node.js 版本不支持 fetch,请使用 Node.js 18 或更高版本');
7123
7482
  }
7124
7483
  const url = `${baseUrl.replace(/\/+$/, '')}${apiPath}`;
7125
- const response = await fetch(url, {
7126
- method: 'POST',
7127
- headers: {
7128
- accept: 'application/json',
7129
- authorization: `Bearer ${accessToken}`,
7130
- },
7131
- body: formData,
7132
- });
7484
+ const timeoutMs = normalizeRuntimeUploadTimeoutMs(options.timeoutMs);
7485
+ const controller = new AbortController();
7486
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
7487
+ let response;
7488
+ try {
7489
+ response = await fetch(url, {
7490
+ method: 'POST',
7491
+ headers: {
7492
+ accept: 'application/json',
7493
+ authorization: `Bearer ${accessToken}`,
7494
+ ...(options.headers || {}),
7495
+ },
7496
+ body: formData,
7497
+ signal: controller.signal,
7498
+ });
7499
+ } catch (error) {
7500
+ if (isAbortError(error)) {
7501
+ throw new Error(maskText(`HTTP form request timed out after ${timeoutMs}ms: ${apiPath}`));
7502
+ }
7503
+ throw error;
7504
+ } finally {
7505
+ clearTimeout(timer);
7506
+ }
7133
7507
  const text = await response.text();
7134
7508
  let payload = null;
7135
7509
  if (text) {
@@ -7146,6 +7520,13 @@ async function requestForm(baseUrl, apiPath, formData, accessToken) {
7146
7520
  return payload;
7147
7521
  }
7148
7522
 
7523
+ function isAbortError(error) {
7524
+ return (
7525
+ error?.name === 'AbortError' ||
7526
+ String(error?.message || '').toLowerCase().includes('aborted')
7527
+ );
7528
+ }
7529
+
7149
7530
  async function refreshProfile(config, profileName) {
7150
7531
  const { profile } = getProfile(config, profileName);
7151
7532
  if (!profile.token?.refreshToken) {