openxiangda 1.0.118 → 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 +356 -1
- package/lib/workspace-init.js +1 -0
- package/openxiangda-skills/references/component-guide.md +10 -7
- package/openxiangda-skills/references/forms/component-registry.md +1 -0
- package/openxiangda-skills/references/openxiangda-api.md +68 -0
- package/openxiangda-skills/references/platform-data-model.md +2 -2
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +49 -0
- package/package.json +1 -1
- package/packages/sdk/dist/{ProcessPreview-Cw7X8odD.d.mts → ProcessPreview-CFmuu427.d.mts} +13 -3
- package/packages/sdk/dist/{ProcessPreview-Cw7X8odD.d.ts → ProcessPreview-CFmuu427.d.ts} +13 -3
- package/packages/sdk/dist/components/index.cjs +134 -11
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +2 -2
- package/packages/sdk/dist/components/index.d.ts +2 -2
- package/packages/sdk/dist/components/index.mjs +134 -11
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/index.cjs +134 -11
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +134 -11
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +1 -1
- package/packages/sdk/dist/runtime/react.d.ts +1 -1
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +1 -0
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,44 @@ 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
|
+
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
|
+
}
|
|
5824
|
+
}
|
|
5762
5825
|
if (kind === 'authConfigs') {
|
|
5763
5826
|
const config = item.configJson || item.config || item;
|
|
5764
5827
|
if (!Array.isArray(config.methods)) {
|
|
@@ -6214,6 +6277,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
6214
6277
|
addPlanActions(actions, 'publicAccessPolicy', manifest.publicAccessPolicies, existing.publicAccessPolicies, (item, current) => publicAccessPolicyEquals(target.bound, item, current));
|
|
6215
6278
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
6216
6279
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
6280
|
+
addPlanActions(actions, 'storageConfig', manifest.storageConfigs, existing.storageConfigs, (item, current) => storageConfigEquals(item, current));
|
|
6217
6281
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
6218
6282
|
addPlanActions(actions, 'formPermissionGroup', manifest.formPermissionGroups, existing.formPermissionGroups, (item, current) => formPermissionGroupEquals(target.bound, item, current));
|
|
6219
6283
|
for (const item of manifest.formSettings || []) {
|
|
@@ -6287,6 +6351,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
6287
6351
|
await publishRouteResources(config, target, manifest.routes || [], result, publishOptions);
|
|
6288
6352
|
await publishAutomationResources(config, target, manifest.automations || [], result, publishOptions);
|
|
6289
6353
|
await publishDataViewResources(config, target, manifest.dataViews || [], result, publishOptions);
|
|
6354
|
+
await publishStorageConfigResources(config, target, manifest.storageConfigs || [], result, publishOptions);
|
|
6290
6355
|
await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result, publishOptions);
|
|
6291
6356
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result, publishOptions);
|
|
6292
6357
|
await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result, publishOptions);
|
|
@@ -6314,6 +6379,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
6314
6379
|
publicAccessPolicies: new Map(),
|
|
6315
6380
|
automations: new Map(),
|
|
6316
6381
|
dataViews: new Map(),
|
|
6382
|
+
storageConfigs: new Map(),
|
|
6317
6383
|
pagePermissionGroups: new Map(),
|
|
6318
6384
|
formPermissionGroups: new Map(),
|
|
6319
6385
|
};
|
|
@@ -6495,6 +6561,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
6495
6561
|
});
|
|
6496
6562
|
}
|
|
6497
6563
|
}
|
|
6564
|
+
if ((manifest.storageConfigs || []).length > 0) {
|
|
6565
|
+
const data = await requestWithAuth(
|
|
6566
|
+
config,
|
|
6567
|
+
target.profileName,
|
|
6568
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`, {
|
|
6569
|
+
page: 1,
|
|
6570
|
+
pageSize: 1000,
|
|
6571
|
+
})
|
|
6572
|
+
);
|
|
6573
|
+
indexByCode(maps.storageConfigs, normalizeItems(data), item => item.code || item.resourceCode);
|
|
6574
|
+
for (const item of manifest.storageConfigs || []) {
|
|
6575
|
+
const code = item.code || item.resourceCode;
|
|
6576
|
+
const existing = maps.storageConfigs.get(code);
|
|
6577
|
+
if (!existing) continue;
|
|
6578
|
+
const detail = await requestOptionalWithAuth(
|
|
6579
|
+
config,
|
|
6580
|
+
target.profileName,
|
|
6581
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`
|
|
6582
|
+
);
|
|
6583
|
+
maps.storageConfigs.set(code, {
|
|
6584
|
+
...existing,
|
|
6585
|
+
...(detail || {}),
|
|
6586
|
+
});
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6498
6589
|
if ((manifest.pagePermissionGroups || []).length > 0) {
|
|
6499
6590
|
const data = await requestWithAuth(
|
|
6500
6591
|
config,
|
|
@@ -7246,6 +7337,44 @@ async function publishDataViewResources(config, target, dataViews, result, optio
|
|
|
7246
7337
|
}
|
|
7247
7338
|
}
|
|
7248
7339
|
|
|
7340
|
+
async function publishStorageConfigResources(config, target, storageConfigs, result, options = {}) {
|
|
7341
|
+
for (const storageItem of storageConfigs) {
|
|
7342
|
+
const code = storageItem.code || storageItem.resourceCode;
|
|
7343
|
+
if (shouldSkipNoopResource(options, 'storageConfig', code)) {
|
|
7344
|
+
recordNoopResource(result, 'storageConfig', code, options);
|
|
7345
|
+
continue;
|
|
7346
|
+
}
|
|
7347
|
+
const existing = await findExistingStorageConfig(config, target, code);
|
|
7348
|
+
const body = normalizeStorageConfigManifest(storageItem);
|
|
7349
|
+
const data = existing
|
|
7350
|
+
? await requestWithAuth(
|
|
7351
|
+
config,
|
|
7352
|
+
target.profileName,
|
|
7353
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`,
|
|
7354
|
+
{ method: 'PUT', body }
|
|
7355
|
+
)
|
|
7356
|
+
: await requestWithAuth(
|
|
7357
|
+
config,
|
|
7358
|
+
target.profileName,
|
|
7359
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`,
|
|
7360
|
+
{ method: 'POST', body }
|
|
7361
|
+
);
|
|
7362
|
+
if (data?.id) {
|
|
7363
|
+
saveStorageConfigResource(target, data.code || code, data.id, {
|
|
7364
|
+
provider: data.provider || body.provider,
|
|
7365
|
+
status: data.status || body.status,
|
|
7366
|
+
});
|
|
7367
|
+
}
|
|
7368
|
+
result.published.push({
|
|
7369
|
+
kind: 'storageConfig',
|
|
7370
|
+
code,
|
|
7371
|
+
action: existing ? 'update' : 'create',
|
|
7372
|
+
id: data?.id,
|
|
7373
|
+
corsStatus: data?.corsStatus,
|
|
7374
|
+
});
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
|
|
7249
7378
|
async function publishDataViewPermissionGroups(config, target, dataViewCode, permissionGroups) {
|
|
7250
7379
|
await requestWithAuth(
|
|
7251
7380
|
config,
|
|
@@ -7398,6 +7527,18 @@ async function findExistingDataView(config, target, code) {
|
|
|
7398
7527
|
return null;
|
|
7399
7528
|
}
|
|
7400
7529
|
|
|
7530
|
+
async function findExistingStorageConfig(config, target, code) {
|
|
7531
|
+
const stateId = target.bound.resources?.storageConfigs?.[code]?.storageConfigId;
|
|
7532
|
+
const byCode = await requestOptionalWithAuth(
|
|
7533
|
+
config,
|
|
7534
|
+
target.profileName,
|
|
7535
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`
|
|
7536
|
+
);
|
|
7537
|
+
if (byCode?.id) return byCode;
|
|
7538
|
+
if (stateId) return { id: stateId, code };
|
|
7539
|
+
return null;
|
|
7540
|
+
}
|
|
7541
|
+
|
|
7401
7542
|
async function findExistingAutomation(config, target, code) {
|
|
7402
7543
|
const stateId = target.bound.resources?.automations?.[code]?.automationId;
|
|
7403
7544
|
if (stateId) {
|
|
@@ -7545,6 +7686,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
7545
7686
|
await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
|
|
7546
7687
|
await pruneRoutes(config, target, desiredCodes(manifest.routes), result);
|
|
7547
7688
|
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
7689
|
+
await pruneStorageConfigs(config, target, desiredCodes(manifest.storageConfigs), result);
|
|
7548
7690
|
await prunePublicAccessPolicies(
|
|
7549
7691
|
config,
|
|
7550
7692
|
target,
|
|
@@ -7722,6 +7864,29 @@ async function pruneDataViews(config, target, desired, result) {
|
|
|
7722
7864
|
}
|
|
7723
7865
|
}
|
|
7724
7866
|
|
|
7867
|
+
async function pruneStorageConfigs(config, target, desired, result) {
|
|
7868
|
+
const data = await requestWithAuth(
|
|
7869
|
+
config,
|
|
7870
|
+
target.profileName,
|
|
7871
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`, {
|
|
7872
|
+
page: 1,
|
|
7873
|
+
pageSize: 1000,
|
|
7874
|
+
})
|
|
7875
|
+
);
|
|
7876
|
+
for (const item of normalizeItems(data)) {
|
|
7877
|
+
const code = item.code || item.resourceCode;
|
|
7878
|
+
if (!code || desired.has(code)) continue;
|
|
7879
|
+
await pruneOne(config, target, result, 'storageConfig', code, item.id, async () => {
|
|
7880
|
+
await requestWithAuth(
|
|
7881
|
+
config,
|
|
7882
|
+
target.profileName,
|
|
7883
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`,
|
|
7884
|
+
{ method: 'DELETE' }
|
|
7885
|
+
);
|
|
7886
|
+
});
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
|
|
7725
7890
|
async function pruneFunctions(config, target, desired, result) {
|
|
7726
7891
|
const data = await requestWithAuth(
|
|
7727
7892
|
config,
|
|
@@ -7893,6 +8058,7 @@ function removeStateResource(target, kind, code) {
|
|
|
7893
8058
|
route: 'routes',
|
|
7894
8059
|
publicAccessPolicy: 'publicAccessPolicies',
|
|
7895
8060
|
dataView: 'dataViews',
|
|
8061
|
+
storageConfig: 'storageConfigs',
|
|
7896
8062
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
7897
8063
|
formPermissionGroup: 'formPermissionGroups',
|
|
7898
8064
|
};
|
|
@@ -8165,6 +8331,26 @@ async function pullResources(config, target) {
|
|
|
8165
8331
|
written.push(path.relative(process.cwd(), filePath));
|
|
8166
8332
|
}
|
|
8167
8333
|
|
|
8334
|
+
const storageConfigs = await requestWithAuth(
|
|
8335
|
+
config,
|
|
8336
|
+
target.profileName,
|
|
8337
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs`, {
|
|
8338
|
+
page: 1,
|
|
8339
|
+
pageSize: 1000,
|
|
8340
|
+
})
|
|
8341
|
+
);
|
|
8342
|
+
for (const storageConfig of normalizeItems(storageConfigs)) {
|
|
8343
|
+
const code = storageConfig.code || storageConfig.resourceCode || storageConfig.id;
|
|
8344
|
+
const detail = await requestWithAuth(
|
|
8345
|
+
config,
|
|
8346
|
+
target.profileName,
|
|
8347
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/storage-configs/${encodeURIComponent(code)}`
|
|
8348
|
+
);
|
|
8349
|
+
const filePath = path.join(baseDir, 'storage', `${code}.json`);
|
|
8350
|
+
writeResourceJsonFile(filePath, toPulledStorageConfig(detail));
|
|
8351
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
8352
|
+
}
|
|
8353
|
+
|
|
8168
8354
|
const pageGroups = await requestWithAuth(
|
|
8169
8355
|
config,
|
|
8170
8356
|
target.profileName,
|
|
@@ -8407,6 +8593,18 @@ function toPulledAuthConfig(authConfig) {
|
|
|
8407
8593
|
});
|
|
8408
8594
|
}
|
|
8409
8595
|
|
|
8596
|
+
function toPulledStorageConfig(storageConfig) {
|
|
8597
|
+
return stripUndefinedValues({
|
|
8598
|
+
code: storageConfig.code || storageConfig.resourceCode,
|
|
8599
|
+
name: storageConfig.name,
|
|
8600
|
+
description: storageConfig.description || '',
|
|
8601
|
+
provider: storageConfig.provider || 'oss',
|
|
8602
|
+
status: storageConfig.status || 'active',
|
|
8603
|
+
configJson: storageConfig.configJson || storageConfig.config || {},
|
|
8604
|
+
credentialStatus: storageConfig.credentials || storageConfig.credentialStatus || undefined,
|
|
8605
|
+
});
|
|
8606
|
+
}
|
|
8607
|
+
|
|
8410
8608
|
function rewriteDataViewDefinitionForManifest(definition, lookups) {
|
|
8411
8609
|
rewriteDataViewSourceForManifest(definition.base, lookups);
|
|
8412
8610
|
for (const join of definition.joins || []) {
|
|
@@ -8657,6 +8855,135 @@ function normalizeAuthConfigManifest(authConfig) {
|
|
|
8657
8855
|
});
|
|
8658
8856
|
}
|
|
8659
8857
|
|
|
8858
|
+
function normalizeStorageConfigManifest(storageConfig) {
|
|
8859
|
+
const code = storageConfig.code || storageConfig.resourceCode;
|
|
8860
|
+
const configJson = clonePlainJson(
|
|
8861
|
+
storageConfig.configJson ||
|
|
8862
|
+
storageConfig.config ||
|
|
8863
|
+
withoutStorageConfigManifestMeta(storageConfig)
|
|
8864
|
+
);
|
|
8865
|
+
const cors = normalizeStorageCorsForPublish(configJson.cors || storageConfig.cors);
|
|
8866
|
+
const credentials = normalizeStorageCredentialsForPublish(storageConfig);
|
|
8867
|
+
return stripUndefinedValues({
|
|
8868
|
+
code,
|
|
8869
|
+
name: storageConfig.name || configJson.name || code,
|
|
8870
|
+
description:
|
|
8871
|
+
storageConfig.description !== undefined
|
|
8872
|
+
? storageConfig.description
|
|
8873
|
+
: configJson.description || '',
|
|
8874
|
+
provider: storageConfig.provider || configJson.provider || 'oss',
|
|
8875
|
+
status: storageConfig.status || configJson.status || 'active',
|
|
8876
|
+
configJson: stripUndefinedValues({
|
|
8877
|
+
region: configJson.region,
|
|
8878
|
+
bucket: configJson.bucket,
|
|
8879
|
+
endpoint: configJson.endpoint,
|
|
8880
|
+
publicBaseUrl: configJson.publicBaseUrl,
|
|
8881
|
+
pathPrefix: configJson.pathPrefix,
|
|
8882
|
+
maxFileSizeMb: configJson.maxFileSizeMb,
|
|
8883
|
+
allowedExtensions: configJson.allowedExtensions,
|
|
8884
|
+
uploadUrlExpiresSeconds: configJson.uploadUrlExpiresSeconds,
|
|
8885
|
+
cors,
|
|
8886
|
+
}),
|
|
8887
|
+
...(credentials ? { credentials } : {}),
|
|
8888
|
+
});
|
|
8889
|
+
}
|
|
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
|
+
|
|
8917
|
+
function withoutStorageConfigManifestMeta(storageConfig) {
|
|
8918
|
+
const next = withoutResourceMeta(storageConfig);
|
|
8919
|
+
delete next.code;
|
|
8920
|
+
delete next.resourceCode;
|
|
8921
|
+
delete next.name;
|
|
8922
|
+
delete next.description;
|
|
8923
|
+
delete next.provider;
|
|
8924
|
+
delete next.status;
|
|
8925
|
+
delete next.cors;
|
|
8926
|
+
delete next.config;
|
|
8927
|
+
delete next.configJson;
|
|
8928
|
+
delete next.credentials;
|
|
8929
|
+
delete next.secret;
|
|
8930
|
+
delete next.secretJson;
|
|
8931
|
+
delete next.credentialStatus;
|
|
8932
|
+
return next;
|
|
8933
|
+
}
|
|
8934
|
+
|
|
8935
|
+
function normalizeStorageCredentialsForPublish(storageConfig) {
|
|
8936
|
+
const source =
|
|
8937
|
+
storageConfig.credentials || storageConfig.secret || storageConfig.secretJson || {};
|
|
8938
|
+
const hasAccessKeyId = source.accessKeyId !== undefined;
|
|
8939
|
+
const hasAccessKeySecret = source.accessKeySecret !== undefined;
|
|
8940
|
+
if (!hasAccessKeyId && !hasAccessKeySecret) return null;
|
|
8941
|
+
return {
|
|
8942
|
+
accessKeyId: resolveEnvReference(source.accessKeyId, 'OSS accessKeyId'),
|
|
8943
|
+
accessKeySecret: resolveEnvReference(
|
|
8944
|
+
source.accessKeySecret,
|
|
8945
|
+
'OSS accessKeySecret'
|
|
8946
|
+
),
|
|
8947
|
+
};
|
|
8948
|
+
}
|
|
8949
|
+
|
|
8950
|
+
let cachedManifestEnv = null;
|
|
8951
|
+
|
|
8952
|
+
function getManifestEnv() {
|
|
8953
|
+
if (!cachedManifestEnv) {
|
|
8954
|
+
cachedManifestEnv = {
|
|
8955
|
+
...loadGlobalEnv(),
|
|
8956
|
+
...process.env,
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
8959
|
+
return cachedManifestEnv;
|
|
8960
|
+
}
|
|
8961
|
+
|
|
8962
|
+
function isEnvReference(value) {
|
|
8963
|
+
return Boolean(parseEnvReferenceName(value));
|
|
8964
|
+
}
|
|
8965
|
+
|
|
8966
|
+
function resolveEnvReference(value, label) {
|
|
8967
|
+
const envName = parseEnvReferenceName(value);
|
|
8968
|
+
if (!envName) fail(`${label} 必须使用环境变量引用`);
|
|
8969
|
+
const env = getManifestEnv();
|
|
8970
|
+
const resolved = env[envName];
|
|
8971
|
+
if (resolved === undefined || resolved === '') {
|
|
8972
|
+
fail(`${label} 引用的环境变量未设置: ${envName}`);
|
|
8973
|
+
}
|
|
8974
|
+
return resolved;
|
|
8975
|
+
}
|
|
8976
|
+
|
|
8977
|
+
function parseEnvReferenceName(value) {
|
|
8978
|
+
const text = String(value || '').trim();
|
|
8979
|
+
const match =
|
|
8980
|
+
text.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/) ||
|
|
8981
|
+
text.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/) ||
|
|
8982
|
+
text.match(/^env:([A-Za-z_][A-Za-z0-9_]*)$/) ||
|
|
8983
|
+
text.match(/^\{\{env\.([A-Za-z_][A-Za-z0-9_]*)\}\}$/);
|
|
8984
|
+
return match?.[1];
|
|
8985
|
+
}
|
|
8986
|
+
|
|
8660
8987
|
function withoutAuthConfigManifestMeta(authConfig) {
|
|
8661
8988
|
const next = withoutResourceMeta(authConfig);
|
|
8662
8989
|
delete next.code;
|
|
@@ -8934,6 +9261,16 @@ function normalizePermissionCodeArray(value) {
|
|
|
8934
9261
|
);
|
|
8935
9262
|
}
|
|
8936
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
|
+
|
|
8937
9274
|
function saveResourceEntry(target, bucket, code, extra = {}) {
|
|
8938
9275
|
saveStateResource(target, bucket, code, pickStateFields(extra, ['formUuid']), [
|
|
8939
9276
|
'formUuid',
|
|
@@ -9188,6 +9525,24 @@ function authConfigEquals(desired, existing) {
|
|
|
9188
9525
|
);
|
|
9189
9526
|
}
|
|
9190
9527
|
|
|
9528
|
+
function storageConfigEquals(desired, existing) {
|
|
9529
|
+
if (!existing) return false;
|
|
9530
|
+
const expected = normalizeStorageConfigManifest({
|
|
9531
|
+
...desired,
|
|
9532
|
+
credentials: undefined,
|
|
9533
|
+
secret: undefined,
|
|
9534
|
+
secretJson: undefined,
|
|
9535
|
+
});
|
|
9536
|
+
return (
|
|
9537
|
+
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
9538
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
9539
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
9540
|
+
String(existing.provider || 'oss') === String(expected.provider || 'oss') &&
|
|
9541
|
+
String(existing.status || 'active') === String(expected.status || 'active') &&
|
|
9542
|
+
jsonEqualsForPlan(existing.configJson || existing.config || {}, expected.configJson || {}, desired.__dir)
|
|
9543
|
+
);
|
|
9544
|
+
}
|
|
9545
|
+
|
|
9191
9546
|
function routeEquals(desired, existing) {
|
|
9192
9547
|
if (!existing) return false;
|
|
9193
9548
|
const expected = normalizeRouteManifest(desired);
|
package/lib/workspace-init.js
CHANGED
|
@@ -59,7 +59,7 @@ AI 在实现成熟交互前必须先判断是否已有可靠组件或库:
|
|
|
59
59
|
| --- | --- | --- | --- |
|
|
60
60
|
| 选择人员 | `UserSelectField` | 直接接入组织架构 | 人员列表、搜索、按部门筛选、层级树 |
|
|
61
61
|
| 选择部门 | `DepartmentSelectField` | 直接接入组织架构 | 部门树、搜索、多选 |
|
|
62
|
-
| 上传附件 | `AttachmentField` |
|
|
62
|
+
| 上传附件 | `AttachmentField` | 接入平台存储或声明式 OSS 存储 | 上传、预览、下载、鉴权 URL 或真实 OSS URL |
|
|
63
63
|
| 上传图片 | `ImageField` | 接入平台存储 | 上传、压缩、缩略图、预览 |
|
|
64
64
|
| 富文本编辑 | `EditorField` | 含图片上传集成 | 完整工具栏、平台图床、粘贴清洗 |
|
|
65
65
|
| 数字签名 | `DigitalSignatureField` | 平台级签名 | 签名采集、验证、归档 |
|
|
@@ -75,12 +75,15 @@ 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
|
+
OSS 浏览器直传所需的 CORS 规则在 `src/resources/storage/<code>.json` 的 `configJson.cors.managed` 中声明,不要把 bucket 配置写进组件 props。
|
|
85
|
+
|
|
86
|
+
> ✅ 一句话原则:**这些场景看到了,直接抄上表,不要自己写。**
|
|
84
87
|
|
|
85
88
|
---
|
|
86
89
|
|
|
@@ -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, 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.
|
|
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
|
|
|
@@ -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`.
|
|
@@ -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` 结构与普通表单一致,流程节点信息走另外的字段,不影响业务字段结构。
|