openxiangda 1.0.119 → 1.0.120

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
@@ -5804,6 +5804,23 @@ function validateResourceItem(kind, item, errors, warnings) {
5804
5804
  if (!credentials.accessKeyId || !credentials.accessKeySecret) {
5805
5805
  warnings.push(`${label}: 未声明凭据;publish 只适用于更新并保留平台已有密文`);
5806
5806
  }
5807
+ const cors = config.cors || item.cors;
5808
+ if (cors?.managed === true) {
5809
+ const allowedOrigins = normalizeManifestStringArray(
5810
+ cors.allowedOrigins || cors.allowedOrigin || cors.origins || cors.origin
5811
+ );
5812
+ if (allowedOrigins.length === 0) {
5813
+ warnings.push(`${label}.cors.allowedOrigins: 未声明时后端将尝试使用当前平台域名`);
5814
+ }
5815
+ const allowedMethods = normalizeManifestStringArray(
5816
+ cors.allowedMethods || cors.allowedMethod || ['PUT', 'GET', 'HEAD']
5817
+ );
5818
+ if (allowedMethods.length === 0) errors.push(`${label}.cors.allowedMethods: 不能为空`);
5819
+ const maxAgeSeconds = Number(cors.maxAgeSeconds || 600);
5820
+ if (!Number.isFinite(maxAgeSeconds) || maxAgeSeconds < 0 || maxAgeSeconds > 86400) {
5821
+ errors.push(`${label}.cors.maxAgeSeconds: 必须在 0 到 86400 秒之间`);
5822
+ }
5823
+ }
5807
5824
  }
5808
5825
  if (kind === 'authConfigs') {
5809
5826
  const config = item.configJson || item.config || item;
@@ -7353,6 +7370,7 @@ async function publishStorageConfigResources(config, target, storageConfigs, res
7353
7370
  code,
7354
7371
  action: existing ? 'update' : 'create',
7355
7372
  id: data?.id,
7373
+ corsStatus: data?.corsStatus,
7356
7374
  });
7357
7375
  }
7358
7376
  }
@@ -8844,6 +8862,7 @@ function normalizeStorageConfigManifest(storageConfig) {
8844
8862
  storageConfig.config ||
8845
8863
  withoutStorageConfigManifestMeta(storageConfig)
8846
8864
  );
8865
+ const cors = normalizeStorageCorsForPublish(configJson.cors || storageConfig.cors);
8847
8866
  const credentials = normalizeStorageCredentialsForPublish(storageConfig);
8848
8867
  return stripUndefinedValues({
8849
8868
  code,
@@ -8863,11 +8882,38 @@ function normalizeStorageConfigManifest(storageConfig) {
8863
8882
  maxFileSizeMb: configJson.maxFileSizeMb,
8864
8883
  allowedExtensions: configJson.allowedExtensions,
8865
8884
  uploadUrlExpiresSeconds: configJson.uploadUrlExpiresSeconds,
8885
+ cors,
8866
8886
  }),
8867
8887
  ...(credentials ? { credentials } : {}),
8868
8888
  });
8869
8889
  }
8870
8890
 
8891
+ function normalizeStorageCorsForPublish(cors) {
8892
+ if (cors === undefined || cors === null || cors === '') return undefined;
8893
+ const managed =
8894
+ cors === true ||
8895
+ cors.managed === true ||
8896
+ String(cors.managed || '').toLowerCase() === 'true';
8897
+ if (!managed) return { managed: false };
8898
+ return stripUndefinedValues({
8899
+ managed: true,
8900
+ allowedOrigins: normalizeManifestStringArray(
8901
+ cors.allowedOrigins || cors.allowedOrigin || cors.origins || cors.origin
8902
+ ),
8903
+ allowedMethods: normalizeManifestStringArray(
8904
+ cors.allowedMethods || cors.allowedMethod || ['PUT', 'GET', 'HEAD'],
8905
+ { uppercase: true }
8906
+ ),
8907
+ allowedHeaders: normalizeManifestStringArray(
8908
+ cors.allowedHeaders || cors.allowedHeader || ['content-type', 'x-oss-*']
8909
+ ),
8910
+ exposeHeaders: normalizeManifestStringArray(
8911
+ cors.exposeHeaders || cors.exposeHeader || ['ETag', 'x-oss-request-id']
8912
+ ),
8913
+ maxAgeSeconds: Number(cors.maxAgeSeconds || 600),
8914
+ });
8915
+ }
8916
+
8871
8917
  function withoutStorageConfigManifestMeta(storageConfig) {
8872
8918
  const next = withoutResourceMeta(storageConfig);
8873
8919
  delete next.code;
@@ -8876,6 +8922,7 @@ function withoutStorageConfigManifestMeta(storageConfig) {
8876
8922
  delete next.description;
8877
8923
  delete next.provider;
8878
8924
  delete next.status;
8925
+ delete next.cors;
8879
8926
  delete next.config;
8880
8927
  delete next.configJson;
8881
8928
  delete next.credentials;
@@ -9214,6 +9261,16 @@ function normalizePermissionCodeArray(value) {
9214
9261
  );
9215
9262
  }
9216
9263
 
9264
+ function normalizeManifestStringArray(value, options = {}) {
9265
+ const source = Array.isArray(value) ? value : value === undefined || value === null ? [] : [value];
9266
+ const items = source
9267
+ .flatMap(item => String(item || '').split(','))
9268
+ .map(item => item.trim())
9269
+ .filter(Boolean)
9270
+ .map(item => options.uppercase ? item.toUpperCase() : item);
9271
+ return unique(items);
9272
+ }
9273
+
9217
9274
  function saveResourceEntry(target, bucket, code, extra = {}) {
9218
9275
  saveStateResource(target, bucket, code, pickStateFields(extra, ['formUuid']), [
9219
9276
  'formUuid',
@@ -81,6 +81,8 @@ import {
81
81
  <AttachmentField name="ossFiles" label="OSS 附件" uploadProvider="oss" storageCode="evaluate_oss" />
82
82
  ```
83
83
 
84
+ OSS 浏览器直传所需的 CORS 规则在 `src/resources/storage/<code>.json` 的 `configJson.cors.managed` 中声明,不要把 bucket 配置写进组件 props。
85
+
84
86
  > ✅ 一句话原则:**这些场景看到了,直接抄上表,不要自己写。**
85
87
 
86
88
  ---
@@ -75,7 +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
+ - For custom OSS attachment upload, first declare a `src/resources/storage/<code>.json` resource, optionally enable `configJson.cors.managed` for browser direct-upload CORS, then set `uploadProvider: "oss"` and `storageCode: "<code>"` on `AttachmentField`. Never put OSS AK/SK values in schema, page code, or committed resource files.
79
79
  - For `SubFormField`, use `columns` for child fields and keep child `fieldId` values stable within the subform.
80
80
  - `TextAreaField` is accepted by workspace tools and normalized to runtime `TextareaField`; prefer `TextAreaField` in OpenXiangda docs for readability.
81
81
 
@@ -643,3 +643,71 @@ Requires Bearer token. Returns runtime bootstrap information for an existing cod
643
643
  ### GET `/apps/:appType/snapshot`
644
644
 
645
645
  Requires Bearer token. Returns app metadata, forms, menus, code page definitions, code page releases, and permission hints.
646
+
647
+ ## Storage Configs
648
+
649
+ Storage config APIs manage application-level custom upload targets. The first provider is Aliyun OSS.
650
+
651
+ ### GET `/apps/:appType/storage-configs`
652
+
653
+ Requires Bearer token and storage-config manage permission. Lists storage configs.
654
+
655
+ ### POST `/apps/:appType/storage-configs`
656
+
657
+ Requires Bearer token and storage-config manage permission. Creates or upserts a storage config.
658
+
659
+ Body:
660
+
661
+ ```json
662
+ {
663
+ "code": "evaluate_oss",
664
+ "name": "Evaluate OSS",
665
+ "provider": "oss",
666
+ "status": "active",
667
+ "configJson": {
668
+ "region": "oss-cn-hangzhou",
669
+ "bucket": "evaluate-oss",
670
+ "publicBaseUrl": "https://evaluate-oss.oss-cn-hangzhou.aliyuncs.com",
671
+ "pathPrefix": "openxiangda/{{appType}}/{{yyyy}}/{{MM}}/{{dd}}",
672
+ "allowedExtensions": ["txt", "pdf", "png"],
673
+ "cors": {
674
+ "managed": true,
675
+ "allowedOrigins": ["https://yida.wisejob.cn"],
676
+ "allowedMethods": ["PUT", "GET", "HEAD"],
677
+ "allowedHeaders": ["content-type", "x-oss-*"],
678
+ "exposeHeaders": ["ETag", "x-oss-request-id"],
679
+ "maxAgeSeconds": 600
680
+ }
681
+ },
682
+ "credentials": {
683
+ "accessKeyId": "resolved at publish time",
684
+ "accessKeySecret": "resolved at publish time"
685
+ }
686
+ }
687
+ ```
688
+
689
+ When `configJson.cors.managed` is true, the backend attempts to merge the required OSS bucket CORS rule after saving the storage config. The response may include `corsStatus`; `applied: false` means the storage config was saved but the OSS credentials could not manage bucket CORS.
690
+
691
+ ### GET `/apps/:appType/storage-configs/:code`
692
+
693
+ Requires Bearer token and storage-config manage permission. Returns the storage config with credential status only, never plaintext AK/SK.
694
+
695
+ ### PUT `/apps/:appType/storage-configs/:code`
696
+
697
+ Requires Bearer token and storage-config manage permission. Updates the storage config. Omit `credentials` to keep the existing encrypted credentials.
698
+
699
+ ### DELETE `/apps/:appType/storage-configs/:code`
700
+
701
+ Requires Bearer token and storage-config manage permission. Deletes the storage config metadata; it does not delete existing OSS objects.
702
+
703
+ ### POST `/apps/:appType/storage-configs/:code/cors/apply`
704
+
705
+ Requires Bearer token and storage-config manage permission. Re-applies the declared managed CORS rule for an existing config.
706
+
707
+ ### POST `/apps/:appType/storage-configs/:code/uploads/initiate`
708
+
709
+ Requires Bearer token. Creates a short-lived signed OSS upload URL for browser direct upload.
710
+
711
+ ### POST `/apps/:appType/storage-configs/:code/objects/delete`
712
+
713
+ Requires Bearer token. Deletes an OSS object under the configured `pathPrefix`.
@@ -155,7 +155,15 @@ await auth.phoneCodeLogin({ phone, code, challengeId: sent.challengeId });
155
155
  "publicBaseUrl": "https://evaluate-oss.oss-cn-hangzhou.aliyuncs.com",
156
156
  "pathPrefix": "openxiangda/{{appType}}/{{yyyy}}/{{MM}}/{{dd}}",
157
157
  "maxFileSizeMb": 20,
158
- "allowedExtensions": ["txt", "pdf", "png"]
158
+ "allowedExtensions": ["txt", "pdf", "png"],
159
+ "cors": {
160
+ "managed": true,
161
+ "allowedOrigins": ["https://yida.wisejob.cn"],
162
+ "allowedMethods": ["PUT", "GET", "HEAD"],
163
+ "allowedHeaders": ["content-type", "x-oss-*"],
164
+ "exposeHeaders": ["ETag", "x-oss-request-id"],
165
+ "maxAgeSeconds": 600
166
+ }
159
167
  },
160
168
  "credentials": {
161
169
  "accessKeyId": "${APP_OSS_ACCESS_KEY_ID}",
@@ -177,6 +185,8 @@ await auth.phoneCodeLogin({ phone, code, challengeId: sent.challengeId });
177
185
 
178
186
  默认附件不配置 `uploadProvider`/`storageCode` 时仍走平台上传。OSS 附件保存真实 OSS URL;预览、下载、删除对象都通过 storage 配置对应的运行时接口处理。
179
187
 
188
+ `configJson.cors.managed: true` 会在发布 storage 配置时顺便设置 OSS bucket CORS。后端会读取现有规则并合并,不覆盖其他系统规则;该操作需要 OSS 凭据具备 bucket CORS 管理权限。未声明 `cors.managed` 时不会修改 bucket 级配置。
189
+
180
190
  ## 1. Public Route — `src/resources/routes/<code>.json`
181
191
 
182
192
  新 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.119",
3
+ "version": "1.0.120",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {