openxiangda 1.0.118 → 1.0.119

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
@@ -1458,6 +1458,7 @@ async function workspace(args) {
1458
1458
  roles: {},
1459
1459
  connectors: {},
1460
1460
  dataViews: {},
1461
+ storageConfigs: {},
1461
1462
  pagePermissionGroups: {},
1462
1463
  formPermissionGroups: {},
1463
1464
  formSettings: {},
@@ -4909,6 +4910,7 @@ function ensureResourceBuckets(bound) {
4909
4910
  bound.resources.roles = bound.resources.roles || {};
4910
4911
  bound.resources.connectors = bound.resources.connectors || {};
4911
4912
  bound.resources.dataViews = bound.resources.dataViews || {};
4913
+ bound.resources.storageConfigs = bound.resources.storageConfigs || {};
4912
4914
  bound.resources.authConfigs = bound.resources.authConfigs || {};
4913
4915
  bound.resources.routes = bound.resources.routes || {};
4914
4916
  bound.resources.publicAccessPolicies = bound.resources.publicAccessPolicies || {};
@@ -5122,6 +5124,14 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
5122
5124
  }, keys);
5123
5125
  }
5124
5126
 
5127
+ function saveStorageConfigResource(target, storageCode, storageConfigId, extra = {}) {
5128
+ const keys = ['storageConfigId', 'provider', 'status'];
5129
+ saveStateResource(target, 'storageConfigs', storageCode, {
5130
+ ...pickStateFields(extra, keys),
5131
+ storageConfigId,
5132
+ }, keys);
5133
+ }
5134
+
5125
5135
  function saveRouteResource(target, routeCode, routeId, extra = {}) {
5126
5136
  const keys = ['routeId', 'pathPattern', 'publicAccess', 'publicPolicyCode'];
5127
5137
  saveStateResource(target, 'routes', routeCode, {
@@ -5253,6 +5263,7 @@ const RESOURCE_SPECS = [
5253
5263
  { key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
5254
5264
  { key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
5255
5265
  { key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
5266
+ { key: 'storageConfigs', dir: 'storage', topFiles: ['storage.json'], pluralKeys: ['storageConfigs', 'storage'] },
5256
5267
  { key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
5257
5268
  { key: 'routes', dir: 'routes', topFiles: ['routes.json'], pluralKeys: ['routes'] },
5258
5269
  {
@@ -5305,6 +5316,15 @@ const RESOURCE_TYPE_ALIASES = new Map([
5305
5316
  ['dataview', 'dataViews'],
5306
5317
  ['dataviews', 'dataViews'],
5307
5318
  ['data-view-resource', 'dataViews'],
5319
+ ['storage', 'storageConfigs'],
5320
+ ['storages', 'storageConfigs'],
5321
+ ['storage-config', 'storageConfigs'],
5322
+ ['storage-configs', 'storageConfigs'],
5323
+ ['storageconfig', 'storageConfigs'],
5324
+ ['storageconfigs', 'storageConfigs'],
5325
+ ['oss', 'storageConfigs'],
5326
+ ['oss-config', 'storageConfigs'],
5327
+ ['oss-configs', 'storageConfigs'],
5308
5328
  ['auth', 'authConfigs'],
5309
5329
  ['auth-config', 'authConfigs'],
5310
5330
  ['auth-configs', 'authConfigs'],
@@ -5369,7 +5389,7 @@ function getResourceTypeFilters(positional = [], flags = {}) {
5369
5389
  if (!key) {
5370
5390
  fail(
5371
5391
  `未知资源类型: ${rawType}。常用类型包括 workflow, notification, route, menu, role, ` +
5372
- `page-permission-group, form-permission-group, public-access。`
5392
+ `storage, page-permission-group, form-permission-group, public-access。`
5373
5393
  );
5374
5394
  }
5375
5395
  filters.add(key);
@@ -5636,6 +5656,7 @@ function generateResourceTypes(manifest, outputFile) {
5636
5656
  );
5637
5657
  const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
5638
5658
  const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
5659
+ const storageConfigCodes = unique((manifest.storageConfigs || []).map(item => item.code).filter(Boolean));
5639
5660
  const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
5640
5661
  const publicAccessPolicyCodes = unique((manifest.publicAccessPolicies || []).map(item => item.code).filter(Boolean));
5641
5662
  const content = [
@@ -5654,6 +5675,9 @@ function generateResourceTypes(manifest, outputFile) {
5654
5675
  `export const dataViewCodes = ${JSON.stringify(dataViewCodes, null, 2)} as const`,
5655
5676
  'export type DataViewCode = typeof dataViewCodes[number]',
5656
5677
  '',
5678
+ `export const storageConfigCodes = ${JSON.stringify(storageConfigCodes, null, 2)} as const`,
5679
+ 'export type StorageConfigCode = typeof storageConfigCodes[number]',
5680
+ '',
5657
5681
  `export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
5658
5682
  'export type FunctionCode = typeof functionCodes[number]',
5659
5683
  '',
@@ -5678,6 +5702,7 @@ function generateResourceTypes(manifest, outputFile) {
5678
5702
  routeCodes: routeCodes.length,
5679
5703
  pagePermissionGroups: pagePermissionGroupCodes.length,
5680
5704
  dataViews: dataViewCodes.length,
5705
+ storageConfigs: storageConfigCodes.length,
5681
5706
  functions: functionCodes.length,
5682
5707
  authConfigs: authConfigCodes.length,
5683
5708
  publicAccessPolicies: publicAccessPolicyCodes.length,
@@ -5759,6 +5784,27 @@ function validateResourceItem(kind, item, errors, warnings) {
5759
5784
  }
5760
5785
  validateDataViewPerformance(label, definition, errors, warnings, storageMode);
5761
5786
  }
5787
+ if (kind === 'storageConfigs') {
5788
+ const config = item.configJson || item.config || item;
5789
+ const provider = String(item.provider || config.provider || 'oss').toLowerCase();
5790
+ if (provider !== 'oss') errors.push(`${label}: 当前仅支持 provider=oss`);
5791
+ if (!item.name && !config.name) warnings.push(`${label}: 未声明 name,将使用 code`);
5792
+ if (!config.region) errors.push(`${label}: 缺少 OSS region`);
5793
+ if (!config.bucket) errors.push(`${label}: 缺少 OSS bucket`);
5794
+ const status = item.status === undefined ? 'active' : String(item.status);
5795
+ if (!['active', 'disabled'].includes(status)) {
5796
+ errors.push(`${label}: status 只能是 active 或 disabled`);
5797
+ }
5798
+ const credentials = item.credentials || item.secret || item.secretJson || {};
5799
+ for (const key of ['accessKeyId', 'accessKeySecret']) {
5800
+ if (credentials[key] !== undefined && !isEnvReference(credentials[key])) {
5801
+ errors.push(`${label}.credentials.${key}: OSS 凭据必须使用环境变量引用`);
5802
+ }
5803
+ }
5804
+ if (!credentials.accessKeyId || !credentials.accessKeySecret) {
5805
+ warnings.push(`${label}: 未声明凭据;publish 只适用于更新并保留平台已有密文`);
5806
+ }
5807
+ }
5762
5808
  if (kind === 'authConfigs') {
5763
5809
  const config = item.configJson || item.config || item;
5764
5810
  if (!Array.isArray(config.methods)) {
@@ -6214,6 +6260,7 @@ async function buildResourcePlan(config, target, manifest) {
6214
6260
  addPlanActions(actions, 'publicAccessPolicy', manifest.publicAccessPolicies, existing.publicAccessPolicies, (item, current) => publicAccessPolicyEquals(target.bound, item, current));
6215
6261
  await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
6216
6262
  addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
6263
+ addPlanActions(actions, 'storageConfig', manifest.storageConfigs, existing.storageConfigs, (item, current) => storageConfigEquals(item, current));
6217
6264
  addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
6218
6265
  addPlanActions(actions, 'formPermissionGroup', manifest.formPermissionGroups, existing.formPermissionGroups, (item, current) => formPermissionGroupEquals(target.bound, item, current));
6219
6266
  for (const item of manifest.formSettings || []) {
@@ -6287,6 +6334,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
6287
6334
  await publishRouteResources(config, target, manifest.routes || [], result, publishOptions);
6288
6335
  await publishAutomationResources(config, target, manifest.automations || [], result, publishOptions);
6289
6336
  await publishDataViewResources(config, target, manifest.dataViews || [], result, publishOptions);
6337
+ await publishStorageConfigResources(config, target, manifest.storageConfigs || [], result, publishOptions);
6290
6338
  await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result, publishOptions);
6291
6339
  await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result, publishOptions);
6292
6340
  await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result, publishOptions);
@@ -6314,6 +6362,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
6314
6362
  publicAccessPolicies: new Map(),
6315
6363
  automations: new Map(),
6316
6364
  dataViews: new Map(),
6365
+ storageConfigs: new Map(),
6317
6366
  pagePermissionGroups: new Map(),
6318
6367
  formPermissionGroups: new Map(),
6319
6368
  };
@@ -6495,6 +6544,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
6495
6544
  });
6496
6545
  }
6497
6546
  }
6547
+ if ((manifest.storageConfigs || []).length > 0) {
6548
+ const data = await requestWithAuth(
6549
+ config,
6550
+ target.profileName,
6551
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`, {
6552
+ page: 1,
6553
+ pageSize: 1000,
6554
+ })
6555
+ );
6556
+ indexByCode(maps.storageConfigs, normalizeItems(data), item => item.code || item.resourceCode);
6557
+ for (const item of manifest.storageConfigs || []) {
6558
+ const code = item.code || item.resourceCode;
6559
+ const existing = maps.storageConfigs.get(code);
6560
+ if (!existing) continue;
6561
+ const detail = await requestOptionalWithAuth(
6562
+ config,
6563
+ target.profileName,
6564
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`
6565
+ );
6566
+ maps.storageConfigs.set(code, {
6567
+ ...existing,
6568
+ ...(detail || {}),
6569
+ });
6570
+ }
6571
+ }
6498
6572
  if ((manifest.pagePermissionGroups || []).length > 0) {
6499
6573
  const data = await requestWithAuth(
6500
6574
  config,
@@ -7246,6 +7320,43 @@ async function publishDataViewResources(config, target, dataViews, result, optio
7246
7320
  }
7247
7321
  }
7248
7322
 
7323
+ async function publishStorageConfigResources(config, target, storageConfigs, result, options = {}) {
7324
+ for (const storageItem of storageConfigs) {
7325
+ const code = storageItem.code || storageItem.resourceCode;
7326
+ if (shouldSkipNoopResource(options, 'storageConfig', code)) {
7327
+ recordNoopResource(result, 'storageConfig', code, options);
7328
+ continue;
7329
+ }
7330
+ const existing = await findExistingStorageConfig(config, target, code);
7331
+ const body = normalizeStorageConfigManifest(storageItem);
7332
+ const data = existing
7333
+ ? await requestWithAuth(
7334
+ config,
7335
+ target.profileName,
7336
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`,
7337
+ { method: 'PUT', body }
7338
+ )
7339
+ : await requestWithAuth(
7340
+ config,
7341
+ target.profileName,
7342
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`,
7343
+ { method: 'POST', body }
7344
+ );
7345
+ if (data?.id) {
7346
+ saveStorageConfigResource(target, data.code || code, data.id, {
7347
+ provider: data.provider || body.provider,
7348
+ status: data.status || body.status,
7349
+ });
7350
+ }
7351
+ result.published.push({
7352
+ kind: 'storageConfig',
7353
+ code,
7354
+ action: existing ? 'update' : 'create',
7355
+ id: data?.id,
7356
+ });
7357
+ }
7358
+ }
7359
+
7249
7360
  async function publishDataViewPermissionGroups(config, target, dataViewCode, permissionGroups) {
7250
7361
  await requestWithAuth(
7251
7362
  config,
@@ -7398,6 +7509,18 @@ async function findExistingDataView(config, target, code) {
7398
7509
  return null;
7399
7510
  }
7400
7511
 
7512
+ async function findExistingStorageConfig(config, target, code) {
7513
+ const stateId = target.bound.resources?.storageConfigs?.[code]?.storageConfigId;
7514
+ const byCode = await requestOptionalWithAuth(
7515
+ config,
7516
+ target.profileName,
7517
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`
7518
+ );
7519
+ if (byCode?.id) return byCode;
7520
+ if (stateId) return { id: stateId, code };
7521
+ return null;
7522
+ }
7523
+
7401
7524
  async function findExistingAutomation(config, target, code) {
7402
7525
  const stateId = target.bound.resources?.automations?.[code]?.automationId;
7403
7526
  if (stateId) {
@@ -7545,6 +7668,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
7545
7668
  await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
7546
7669
  await pruneRoutes(config, target, desiredCodes(manifest.routes), result);
7547
7670
  await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
7671
+ await pruneStorageConfigs(config, target, desiredCodes(manifest.storageConfigs), result);
7548
7672
  await prunePublicAccessPolicies(
7549
7673
  config,
7550
7674
  target,
@@ -7722,6 +7846,29 @@ async function pruneDataViews(config, target, desired, result) {
7722
7846
  }
7723
7847
  }
7724
7848
 
7849
+ async function pruneStorageConfigs(config, target, desired, result) {
7850
+ const data = await requestWithAuth(
7851
+ config,
7852
+ target.profileName,
7853
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`, {
7854
+ page: 1,
7855
+ pageSize: 1000,
7856
+ })
7857
+ );
7858
+ for (const item of normalizeItems(data)) {
7859
+ const code = item.code || item.resourceCode;
7860
+ if (!code || desired.has(code)) continue;
7861
+ await pruneOne(config, target, result, 'storageConfig', code, item.id, async () => {
7862
+ await requestWithAuth(
7863
+ config,
7864
+ target.profileName,
7865
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`,
7866
+ { method: 'DELETE' }
7867
+ );
7868
+ });
7869
+ }
7870
+ }
7871
+
7725
7872
  async function pruneFunctions(config, target, desired, result) {
7726
7873
  const data = await requestWithAuth(
7727
7874
  config,
@@ -7893,6 +8040,7 @@ function removeStateResource(target, kind, code) {
7893
8040
  route: 'routes',
7894
8041
  publicAccessPolicy: 'publicAccessPolicies',
7895
8042
  dataView: 'dataViews',
8043
+ storageConfig: 'storageConfigs',
7896
8044
  pagePermissionGroup: 'pagePermissionGroups',
7897
8045
  formPermissionGroup: 'formPermissionGroups',
7898
8046
  };
@@ -8165,6 +8313,26 @@ async function pullResources(config, target) {
8165
8313
  written.push(path.relative(process.cwd(), filePath));
8166
8314
  }
8167
8315
 
8316
+ const storageConfigs = await requestWithAuth(
8317
+ config,
8318
+ target.profileName,
8319
+ apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`, {
8320
+ page: 1,
8321
+ pageSize: 1000,
8322
+ })
8323
+ );
8324
+ for (const storageConfig of normalizeItems(storageConfigs)) {
8325
+ const code = storageConfig.code || storageConfig.resourceCode || storageConfig.id;
8326
+ const detail = await requestWithAuth(
8327
+ config,
8328
+ target.profileName,
8329
+ `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`
8330
+ );
8331
+ const filePath = path.join(baseDir, 'storage', `${code}.json`);
8332
+ writeResourceJsonFile(filePath, toPulledStorageConfig(detail));
8333
+ written.push(path.relative(process.cwd(), filePath));
8334
+ }
8335
+
8168
8336
  const pageGroups = await requestWithAuth(
8169
8337
  config,
8170
8338
  target.profileName,
@@ -8407,6 +8575,18 @@ function toPulledAuthConfig(authConfig) {
8407
8575
  });
8408
8576
  }
8409
8577
 
8578
+ function toPulledStorageConfig(storageConfig) {
8579
+ return stripUndefinedValues({
8580
+ code: storageConfig.code || storageConfig.resourceCode,
8581
+ name: storageConfig.name,
8582
+ description: storageConfig.description || '',
8583
+ provider: storageConfig.provider || 'oss',
8584
+ status: storageConfig.status || 'active',
8585
+ configJson: storageConfig.configJson || storageConfig.config || {},
8586
+ credentialStatus: storageConfig.credentials || storageConfig.credentialStatus || undefined,
8587
+ });
8588
+ }
8589
+
8410
8590
  function rewriteDataViewDefinitionForManifest(definition, lookups) {
8411
8591
  rewriteDataViewSourceForManifest(definition.base, lookups);
8412
8592
  for (const join of definition.joins || []) {
@@ -8657,6 +8837,106 @@ function normalizeAuthConfigManifest(authConfig) {
8657
8837
  });
8658
8838
  }
8659
8839
 
8840
+ function normalizeStorageConfigManifest(storageConfig) {
8841
+ const code = storageConfig.code || storageConfig.resourceCode;
8842
+ const configJson = clonePlainJson(
8843
+ storageConfig.configJson ||
8844
+ storageConfig.config ||
8845
+ withoutStorageConfigManifestMeta(storageConfig)
8846
+ );
8847
+ const credentials = normalizeStorageCredentialsForPublish(storageConfig);
8848
+ return stripUndefinedValues({
8849
+ code,
8850
+ name: storageConfig.name || configJson.name || code,
8851
+ description:
8852
+ storageConfig.description !== undefined
8853
+ ? storageConfig.description
8854
+ : configJson.description || '',
8855
+ provider: storageConfig.provider || configJson.provider || 'oss',
8856
+ status: storageConfig.status || configJson.status || 'active',
8857
+ configJson: stripUndefinedValues({
8858
+ region: configJson.region,
8859
+ bucket: configJson.bucket,
8860
+ endpoint: configJson.endpoint,
8861
+ publicBaseUrl: configJson.publicBaseUrl,
8862
+ pathPrefix: configJson.pathPrefix,
8863
+ maxFileSizeMb: configJson.maxFileSizeMb,
8864
+ allowedExtensions: configJson.allowedExtensions,
8865
+ uploadUrlExpiresSeconds: configJson.uploadUrlExpiresSeconds,
8866
+ }),
8867
+ ...(credentials ? { credentials } : {}),
8868
+ });
8869
+ }
8870
+
8871
+ function withoutStorageConfigManifestMeta(storageConfig) {
8872
+ const next = withoutResourceMeta(storageConfig);
8873
+ delete next.code;
8874
+ delete next.resourceCode;
8875
+ delete next.name;
8876
+ delete next.description;
8877
+ delete next.provider;
8878
+ delete next.status;
8879
+ delete next.config;
8880
+ delete next.configJson;
8881
+ delete next.credentials;
8882
+ delete next.secret;
8883
+ delete next.secretJson;
8884
+ delete next.credentialStatus;
8885
+ return next;
8886
+ }
8887
+
8888
+ function normalizeStorageCredentialsForPublish(storageConfig) {
8889
+ const source =
8890
+ storageConfig.credentials || storageConfig.secret || storageConfig.secretJson || {};
8891
+ const hasAccessKeyId = source.accessKeyId !== undefined;
8892
+ const hasAccessKeySecret = source.accessKeySecret !== undefined;
8893
+ if (!hasAccessKeyId && !hasAccessKeySecret) return null;
8894
+ return {
8895
+ accessKeyId: resolveEnvReference(source.accessKeyId, 'OSS accessKeyId'),
8896
+ accessKeySecret: resolveEnvReference(
8897
+ source.accessKeySecret,
8898
+ 'OSS accessKeySecret'
8899
+ ),
8900
+ };
8901
+ }
8902
+
8903
+ let cachedManifestEnv = null;
8904
+
8905
+ function getManifestEnv() {
8906
+ if (!cachedManifestEnv) {
8907
+ cachedManifestEnv = {
8908
+ ...loadGlobalEnv(),
8909
+ ...process.env,
8910
+ };
8911
+ }
8912
+ return cachedManifestEnv;
8913
+ }
8914
+
8915
+ function isEnvReference(value) {
8916
+ return Boolean(parseEnvReferenceName(value));
8917
+ }
8918
+
8919
+ function resolveEnvReference(value, label) {
8920
+ const envName = parseEnvReferenceName(value);
8921
+ if (!envName) fail(`${label} 必须使用环境变量引用`);
8922
+ const env = getManifestEnv();
8923
+ const resolved = env[envName];
8924
+ if (resolved === undefined || resolved === '') {
8925
+ fail(`${label} 引用的环境变量未设置: ${envName}`);
8926
+ }
8927
+ return resolved;
8928
+ }
8929
+
8930
+ function parseEnvReferenceName(value) {
8931
+ const text = String(value || '').trim();
8932
+ const match =
8933
+ text.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/) ||
8934
+ text.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/) ||
8935
+ text.match(/^env:([A-Za-z_][A-Za-z0-9_]*)$/) ||
8936
+ text.match(/^\{\{env\.([A-Za-z_][A-Za-z0-9_]*)\}\}$/);
8937
+ return match?.[1];
8938
+ }
8939
+
8660
8940
  function withoutAuthConfigManifestMeta(authConfig) {
8661
8941
  const next = withoutResourceMeta(authConfig);
8662
8942
  delete next.code;
@@ -9188,6 +9468,24 @@ function authConfigEquals(desired, existing) {
9188
9468
  );
9189
9469
  }
9190
9470
 
9471
+ function storageConfigEquals(desired, existing) {
9472
+ if (!existing) return false;
9473
+ const expected = normalizeStorageConfigManifest({
9474
+ ...desired,
9475
+ credentials: undefined,
9476
+ secret: undefined,
9477
+ secretJson: undefined,
9478
+ });
9479
+ return (
9480
+ String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
9481
+ String(existing.name || '') === String(expected.name || '') &&
9482
+ String(existing.description || '') === String(expected.description || '') &&
9483
+ String(existing.provider || 'oss') === String(expected.provider || 'oss') &&
9484
+ String(existing.status || 'active') === String(expected.status || 'active') &&
9485
+ jsonEqualsForPlan(existing.configJson || existing.config || {}, expected.configJson || {}, desired.__dir)
9486
+ );
9487
+ }
9488
+
9191
9489
  function routeEquals(desired, existing) {
9192
9490
  if (!existing) return false;
9193
9491
  const expected = normalizeRouteManifest(desired);
@@ -57,6 +57,7 @@ function initWorkspace(options = {}) {
57
57
  roles: {},
58
58
  connectors: {},
59
59
  dataViews: {},
60
+ storageConfigs: {},
60
61
  notifications: {
61
62
  templates: {},
62
63
  typeConfigs: {},
@@ -59,7 +59,7 @@ AI 在实现成熟交互前必须先判断是否已有可靠组件或库:
59
59
  | --- | --- | --- | --- |
60
60
  | 选择人员 | `UserSelectField` | 直接接入组织架构 | 人员列表、搜索、按部门筛选、层级树 |
61
61
  | 选择部门 | `DepartmentSelectField` | 直接接入组织架构 | 部门树、搜索、多选 |
62
- | 上传附件 | `AttachmentField` | 接入平台存储 | 上传、预览、下载、鉴权 URL |
62
+ | 上传附件 | `AttachmentField` | 接入平台存储或声明式 OSS 存储 | 上传、预览、下载、鉴权 URL 或真实 OSS URL |
63
63
  | 上传图片 | `ImageField` | 接入平台存储 | 上传、压缩、缩略图、预览 |
64
64
  | 富文本编辑 | `EditorField` | 含图片上传集成 | 完整工具栏、平台图床、粘贴清洗 |
65
65
  | 数字签名 | `DigitalSignatureField` | 平台级签名 | 签名采集、验证、归档 |
@@ -75,12 +75,13 @@ import {
75
75
  AttachmentField,
76
76
  } from 'openxiangda';
77
77
 
78
- <UserSelectField name="owner" label="负责人" multiple={false} />
79
- <DepartmentSelectField name="dept" label="所属部门" />
80
- <AttachmentField name="files" label="附件" maxCount={5} />
81
- ```
82
-
83
- > ✅ 一句话原则:**这些场景看到了,直接抄上表,不要自己写。**
78
+ <UserSelectField name="owner" label="负责人" multiple={false} />
79
+ <DepartmentSelectField name="dept" label="所属部门" />
80
+ <AttachmentField name="files" label="附件" maxCount={5} />
81
+ <AttachmentField name="ossFiles" label="OSS 附件" uploadProvider="oss" storageCode="evaluate_oss" />
82
+ ```
83
+
84
+ > ✅ 一句话原则:**这些场景看到了,直接抄上表,不要自己写。**
84
85
 
85
86
  ---
86
87
 
@@ -75,6 +75,7 @@ Rules:
75
75
  - For `CascadeDateField`, store a date range and document whether the value is inclusive.
76
76
  - For `UserSelectField` and `DepartmentSelectField`, do not invent user or department IDs. Use runtime/platform selection.
77
77
  - For `AttachmentField` and `ImageField`, do not hardcode OSS URLs in defaults unless the file has already been uploaded by platform tooling.
78
+ - For custom OSS attachment upload, first declare a `src/resources/storage/<code>.json` resource, then set `uploadProvider: "oss"` and `storageCode: "<code>"` on `AttachmentField`. Never put OSS AK/SK values in schema, page code, or committed resource files.
78
79
  - For `SubFormField`, use `columns` for child fields and keep child `fieldId` values stable within the subform.
79
80
  - `TextAreaField` is accepted by workspace tools and normalized to runtime `TextareaField`; prefer `TextAreaField` in OpenXiangda docs for readability.
80
81
 
@@ -43,7 +43,7 @@
43
43
  | 人员选择(单) | `UserSelectField` (单选) | `{"label": "张三", "value": "user_001"}` | `value.label` | `value.value` 作为 userId |
44
44
  | 人员选择(多) | `UserSelectField` (多选) | `[{"label":"张三","value":"user_001"},{"label":"李四","value":"user_002"}]` | `items.map(i => i.label).join(', ')` | `items.map(i => i.value)` 作为 userId 列表 |
45
45
  | 部门选择 | `DepartmentSelectField` | 单选 `{"label": "研发部", "value": "dept_001"}`;多选 `[{...}, ...]` | `value.label` 或 `items.map(i => i.label)` | `value.value` / `items.map(i => i.value)` |
46
- | 附件 | `AttachmentField` | `[{"name":"文件.pdf","url":"https://...","size":1024,"type":"application/pdf"}]` | 文件名+下载链接列表 | 通过 `url` 下载 |
46
+ | 附件 | `AttachmentField` | `[{"name":"文件.pdf","url":"https://...","size":1024,"type":"application/pdf","provider":"platform或oss","storageCode":"evaluate_oss"}]` | 文件名+下载链接列表 | 通过 `url` 下载;OSS 附件保存真实 OSS URL |
47
47
  | 图片 | `ImageField` | `[{"name":"图片.png","url":"https://...","thumbUrl":"https://..."}]` | 缩略图网格 | 通过 `url` 预览 |
48
48
  | 子表单 | `SubFormField` | `[{字段A: ..., 字段B: ...}, {...}]` (对象数组) | 渲染为子行表格 | 遍历每行,按字段 code 取值 |
49
49
  | 位置 | `LocationField` | `{"address":"杭州市xxx","lng":120.12,"lat":30.27}` | `value.address` | `value.lng`/`value.lat` 用于地图 |
@@ -244,7 +244,7 @@ const filter = {
244
244
  3. **提交表单时必须组装完整 `{label, value}` 结构**:仅传 value 会导致历史展示丢失中文名。
245
245
  4. **多选字段始终是数组**,即便只有一项也写成 `[{label, value}]`;空值用 `[]`,不要用 `null`。
246
246
  5. **`UserSelectField` / `DepartmentSelectField` 与普通 `SelectField` 的格式完全一致**,唯一区别是 `value` 语义为 userId / departmentId。
247
- 6. **附件 / 图片字段始终是数组**:单文件场景也是 `[{name, url, ...}]`,没有则 `[]`。
247
+ 6. **附件 / 图片字段始终是数组**:单文件场景也是 `[{name, url, ...}]`,没有则 `[]`。使用 OSS 自定义上传时,附件项会额外带 `provider: "oss"`、`storageCode`、`publicUrl` 等元信息,但仍以 `url` 作为主访问地址。
248
248
  7. **子表单 `SubFormField` 是对象数组**,每个对象代表一行,键为子字段 code。读取时遍历数组,按字段 code 访问;空子表单为 `[]`。
249
249
  8. **不要尝试"翻译"或"规范化" label/value**:数据库存的就是最终展示格式,前后端皆然。任何二次翻译都是 bug 来源。
250
250
  9. **流程表单的数据格式与普通表单一致**:`formData` 结构与普通表单一致,流程节点信息走另外的字段,不影响业务字段结构。
@@ -50,6 +50,7 @@ openxiangda notification template-upsert --json-file src/resources/notifications
50
50
  openxiangda notification type-upsert --json-file src/resources/notifications/register.json
51
51
  openxiangda notification preview public_register_notice --body-json '{"payload":{"title":"测试"}}'
52
52
 
53
+ openxiangda resource plan storage --profile <name>
53
54
  openxiangda data-view upsert --json-file src/resources/data-views/public_lookup.json
54
55
  openxiangda menu update public_register --json-file src/resources/menus/public_register.json --write-manifest
55
56
  openxiangda permission audit --json
@@ -138,6 +139,44 @@ await auth.phoneCodeLogin({ phone, code, challengeId: sent.challengeId });
138
139
 
139
140
  设计前必须确认启用方式、注册策略、身份匹配键、provider 边界、默认权限、安全参数、第三方配置归属。
140
141
 
142
+ ## 1. Storage — `src/resources/storage/<code>.json`
143
+
144
+ 用于给附件字段声明自定义 OSS 上传目标。manifest 里只能写环境变量引用,不能写真实 AK/SK;发布时 CLI 解析环境变量,后端密文保存。
145
+
146
+ ```json
147
+ {
148
+ "code": "evaluate_oss",
149
+ "name": "Evaluate OSS",
150
+ "provider": "oss",
151
+ "status": "active",
152
+ "configJson": {
153
+ "region": "oss-cn-hangzhou",
154
+ "bucket": "evaluate-oss",
155
+ "publicBaseUrl": "https://evaluate-oss.oss-cn-hangzhou.aliyuncs.com",
156
+ "pathPrefix": "openxiangda/{{appType}}/{{yyyy}}/{{MM}}/{{dd}}",
157
+ "maxFileSizeMb": 20,
158
+ "allowedExtensions": ["txt", "pdf", "png"]
159
+ },
160
+ "credentials": {
161
+ "accessKeyId": "${APP_OSS_ACCESS_KEY_ID}",
162
+ "accessKeySecret": "${APP_OSS_ACCESS_KEY_SECRET}"
163
+ }
164
+ }
165
+ ```
166
+
167
+ 表单字段引用:
168
+
169
+ ```tsx
170
+ <AttachmentField
171
+ name="attachments"
172
+ label="附件"
173
+ uploadProvider="oss"
174
+ storageCode="evaluate_oss"
175
+ />
176
+ ```
177
+
178
+ 默认附件不配置 `uploadProvider`/`storageCode` 时仍走平台上传。OSS 附件保存真实 OSS URL;预览、下载、删除对象都通过 storage 配置对应的运行时接口处理。
179
+
141
180
  ## 1. Public Route — `src/resources/routes/<code>.json`
142
181
 
143
182
  新 React SPA 公开页使用 `/view/:appType/public/*`,不要再使用旧 `?publicAccess=guest`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.118",
3
+ "version": "1.0.119",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -252,11 +252,16 @@ interface RuntimeRequestConfig {
252
252
  headers?: Record<string, string>;
253
253
  responseType?: 'json' | 'blob';
254
254
  }
255
+ interface RuntimeUploadOptions {
256
+ uploadProvider?: 'platform' | 'oss';
257
+ storageCode?: string;
258
+ appType?: string;
259
+ }
255
260
  interface FormRuntimeApi {
256
261
  request: <T = any>(config: RuntimeRequestConfig) => Promise<RuntimeResponse<T> | Blob>;
257
- uploadFile: (file: File, bucketName?: string, onProgress?: (percent: number) => void) => Promise<AttachmentItem>;
262
+ uploadFile: (file: File, bucketName?: string, onProgress?: (percent: number) => void, options?: RuntimeUploadOptions) => Promise<AttachmentItem>;
258
263
  uploadPublicFile: (file: File, bucketName?: string, onProgress?: (percent: number) => void) => Promise<AttachmentItem>;
259
- deleteFile: (objectName: string, bucketName?: string) => Promise<{
264
+ deleteFile: (objectName: string, bucketName?: string, options?: RuntimeUploadOptions) => Promise<{
260
265
  success: boolean;
261
266
  }>;
262
267
  createDownloadTicket: (bucketName: string, objectName: string, fileName?: string) => Promise<any>;
@@ -571,6 +576,9 @@ interface AttachmentItem {
571
576
  id: string;
572
577
  uid?: string;
573
578
  status?: 'uploading' | 'done' | 'error';
579
+ provider?: 'platform' | 'oss';
580
+ storageCode?: string;
581
+ appType?: string;
574
582
  objectName?: string;
575
583
  bucketName?: string;
576
584
  originalName?: string;
@@ -593,6 +601,8 @@ interface AttachmentFieldProps extends BaseFieldProps {
593
601
  maxSize?: number;
594
602
  uploadAction?: string;
595
603
  bucketName?: string;
604
+ uploadProvider?: 'platform' | 'oss';
605
+ storageCode?: string;
596
606
  multiple?: boolean;
597
607
  allowedTypes?: string[];
598
608
  showPreview?: boolean;
@@ -1095,4 +1105,4 @@ interface ProcessPreviewProps {
1095
1105
  }
1096
1106
  declare const ProcessPreview: React__default.FC<ProcessPreviewProps>;
1097
1107
 
1098
- export { type FormRuntimeConfig as $, type AddressFieldProps as A, type BaseFieldProps as B, type CascadeDateFieldProps as C, type DataFilter as D, type DepartmentTreeNode as E, type DigitalSignatureFieldProps as F, type DigitalSignatureValue as G, type EditorChoiceOption as H, type EditorFieldProps as I, type EditorToolbarAction as J, type FieldBehavior as K, type FieldDefinition as L, type FieldLayoutNode as M, type FieldValueSyncConfig as N, type FormAppearanceConfig as O, type FormDataDeleteParams as P, type FormDataQueryParams as Q, type FormEffect as R, type FormEffectAction as S, type FormEffectCondition as T, type FormEffectConditionOperator as U, type FormEngineConfig as V, type FormEngineMode as W, type FormInstanceData as X, type FormLayoutNode as Y, type FormRuntimeApi as Z, type FormRuntimeApiConfig as _, type AddressValue as a, type TextFieldProps as a$, type FormSchema as a0, FormSection as a1, type FormSectionProps as a2, type FormSubmitBehavior as a3, type FormTemplateConfig as a4, type GridLayoutCell as a5, type GridLayoutNode as a6, type ImageFieldProps as a7, type InitiatorSelectCandidate as a8, type InitiatorSelectRequirement as a9, type ProcessStatus as aA, type ProcessTask as aB, type RadioFieldProps as aC, type ResubmitParams as aD, type ReturnParams as aE, type ReturnPolicy as aF, type ReturnableNode as aG, type ReturnableNodeResult as aH, type RuntimeDataQueryParams as aI, type RuntimeDataQueryResult as aJ, type RuntimeRequestConfig as aK, type RuntimeResponse as aL, type SaveTaskParams as aM, type SectionLayoutNode as aN, type SelectFieldProps as aO, type SerialNumberFieldProps as aP, type SignaturePoint as aQ, type StandardFormPageMode as aR, type StatusMeta as aS, type StepLayoutItem as aT, type StepsLayoutNode as aU, type SubFormColumn as aV, type SubFormFieldProps as aW, type TabLayoutItem as aX, type TabsLayoutNode as aY, type TaskStatus as aZ, type TextAreaFieldProps as a_, type InitiatorSelectScope as aa, type InitiatorSelectedApprovers as ab, type JSONFieldProps as ac, type LayoutVisibleWhen as ad, type LinkedFormOptionConfig as ae, type LocationFieldProps as af, type LocationValue as ag, type LowcodePageMeta as ah, type LowcodePageNode as ai, type LowcodePageNodeType as aj, type LowcodePageSchema as ak, type MultiSelectFieldProps as al, type NumberFieldProps as am, type OptionItem as an, type OptionSourceConfig as ao, type OptionSourceType as ap, type PeopleShortcutConfig as aq, type PeopleShortcutType as ar, type PreviewParams as as, type ProcessAction as at, type ProcessBasicInfo as au, type ProcessDefinition as av, type ProcessNodeType as aw, ProcessPreview as ax, type ProcessPreviewProps as ay, type ProcessRoute as az, type ApprovalActionType as b, type TextShortcutConfig as b0, type TextShortcutType as b1, type TransferParams as b2, type UserDisplayFormat as b3, type UserItem as b4, type UserSelectFieldProps as b5, type ValidationPreset as b6, type ValidationRule as b7, type ViewPermissionQueryParams as b8, type ViewPermissionSummary as b9, type WithdrawParams as ba, 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 AttachmentItem as k, type BaseLayoutNode as l, type CascadeSelectFieldProps as m, type ChangeRecord as n, type ChangeRecordListResponse as o, type ChangeRecordQueryParams as p, type CheckboxFieldProps as q, type DataLinkageCondition as r, type DataLinkageConfig as s, type DateFieldProps as t, type DateRangeRestriction as u, type DateRestrictionConfig as v, type DateShortcutConfig as w, type DateShortcutType as x, type DefaultValueLinkageConfig as y, type DepartmentSelectFieldProps as z };
1108
+ export { type FormRuntimeConfig as $, type AddressFieldProps as A, type BaseFieldProps as B, type CascadeDateFieldProps as C, type DataFilter as D, type DepartmentTreeNode as E, type DigitalSignatureFieldProps as F, type DigitalSignatureValue as G, type EditorChoiceOption as H, type EditorFieldProps as I, type EditorToolbarAction as J, type FieldBehavior as K, type FieldDefinition as L, type FieldLayoutNode as M, type FieldValueSyncConfig as N, type FormAppearanceConfig as O, type FormDataDeleteParams as P, type FormDataQueryParams as Q, type FormEffect as R, type FormEffectAction as S, type FormEffectCondition as T, type FormEffectConditionOperator as U, type FormEngineConfig as V, type FormEngineMode as W, type FormInstanceData as X, type FormLayoutNode as Y, type FormRuntimeApi as Z, type FormRuntimeApiConfig as _, type AddressValue as a, type TextAreaFieldProps as a$, type FormSchema as a0, FormSection as a1, type FormSectionProps as a2, type FormSubmitBehavior as a3, type FormTemplateConfig as a4, type GridLayoutCell as a5, type GridLayoutNode as a6, type ImageFieldProps as a7, type InitiatorSelectCandidate as a8, type InitiatorSelectRequirement as a9, type ProcessStatus as aA, type ProcessTask as aB, type RadioFieldProps as aC, type ResubmitParams as aD, type ReturnParams as aE, type ReturnPolicy as aF, type ReturnableNode as aG, type ReturnableNodeResult as aH, type RuntimeDataQueryParams as aI, type RuntimeDataQueryResult as aJ, type RuntimeRequestConfig as aK, type RuntimeResponse as aL, type RuntimeUploadOptions as aM, type SaveTaskParams as aN, type SectionLayoutNode as aO, type SelectFieldProps as aP, type SerialNumberFieldProps as aQ, type SignaturePoint as aR, type StandardFormPageMode as aS, type StatusMeta as aT, type StepLayoutItem as aU, type StepsLayoutNode as aV, type SubFormColumn as aW, type SubFormFieldProps as aX, type TabLayoutItem as aY, type TabsLayoutNode as aZ, type TaskStatus as a_, type InitiatorSelectScope as aa, type InitiatorSelectedApprovers as ab, type JSONFieldProps as ac, type LayoutVisibleWhen as ad, type LinkedFormOptionConfig as ae, type LocationFieldProps as af, type LocationValue as ag, type LowcodePageMeta as ah, type LowcodePageNode as ai, type LowcodePageNodeType as aj, type LowcodePageSchema as ak, type MultiSelectFieldProps as al, type NumberFieldProps as am, type OptionItem as an, type OptionSourceConfig as ao, type OptionSourceType as ap, type PeopleShortcutConfig as aq, type PeopleShortcutType as ar, type PreviewParams as as, type ProcessAction as at, type ProcessBasicInfo as au, type ProcessDefinition as av, type ProcessNodeType as aw, ProcessPreview as ax, type ProcessPreviewProps as ay, type ProcessRoute as az, type ApprovalActionType as b, type TextFieldProps as b0, type TextShortcutConfig as b1, type TextShortcutType as b2, type TransferParams as b3, type UserDisplayFormat as b4, type UserItem as b5, type UserSelectFieldProps as b6, type ValidationPreset as b7, type ValidationRule as b8, type ViewPermissionQueryParams as b9, type ViewPermissionSummary as ba, type WithdrawParams as bb, 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 AttachmentItem as k, type BaseLayoutNode as l, type CascadeSelectFieldProps as m, type ChangeRecord as n, type ChangeRecordListResponse as o, type ChangeRecordQueryParams as p, type CheckboxFieldProps as q, type DataLinkageCondition as r, type DataLinkageConfig as s, type DateFieldProps as t, type DateRangeRestriction as u, type DateRestrictionConfig as v, type DateShortcutConfig as w, type DateShortcutType as x, type DefaultValueLinkageConfig as y, type DepartmentSelectFieldProps as z };