openxiangda 1.0.77 → 1.0.79
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/README.md +3 -1
- package/lib/cli.js +330 -11
- package/openxiangda-skills/SKILL.md +11 -9
- package/openxiangda-skills/references/automation-v3.md +17 -1
- package/openxiangda-skills/references/connector-resources.md +4 -1
- package/openxiangda-skills/references/data-views.md +23 -9
- package/openxiangda-skills/references/pages/page-sdk.md +15 -0
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +92 -19
- package/openxiangda-skills/references/workflow-v3.md +17 -1
- package/openxiangda-skills/skills/openxiangda-app/SKILL.md +7 -5
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +7 -4
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +9 -9
- package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +2 -2
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +19 -16
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +4 -4
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +27 -8
- package/package.json +1 -1
- package/packages/sdk/dist/runtime/index.cjs +20 -0
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +18 -1
- package/packages/sdk/dist/runtime/index.d.ts +18 -1
- package/packages/sdk/dist/runtime/index.mjs +20 -0
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +8 -2
- package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +5 -1
package/README.md
CHANGED
|
@@ -170,7 +170,9 @@ const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
|
|
|
170
170
|
|
|
171
171
|
工程化资源放在工作区 `src/resources/` 下,由 `openxiangda resource validate|plan|publish|pull` 管理。`workspace publish` 会先构建并注册 workspace 表单/页面,再执行非破坏性资源 upsert,这样菜单、权限组、流程和表单设置可以解析最新的 profile-local ID。需要删除平台中 manifest 未声明的资源时,显式传 `--prune`。连接器页面运行时通过 `sdk.connector.invoke()` / `sdk.connector.call("connector.api")` 调用平台运行时接口,第三方密钥只保存在后端连接器配置中。
|
|
172
172
|
|
|
173
|
-
多表只读查询和固定口径统计优先声明 `src/resources/data-views/*.json`
|
|
173
|
+
多表只读查询和固定口径统计优先声明 `src/resources/data-views/*.json` 数据视图,而不是在页面里手写多次单表查询再拼数据。默认 `storageMode: "materialized"` 会创建 PostgreSQL materialized view,适合读多写少和可接受刷新延迟的列表/报表;`storageMode: "live"` 每次查询实时编译逻辑视图,适合强实时但数据量可控的复杂查询。`viewType: "aggregate"` 是统计聚合视图,适合按客户、状态、月份等维度聚合 count/sum/avg/min/max。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`;页面通过 `sdk.dataView.query(code, params)` 查询行级视图,通过 `sdk.dataView.stats(code, params)` 查询聚合视图,也可以用 `sdk.dataSource.run()` 路由 `dataView.query` / `dataView.stats`。materialized 模式应为常用筛选、排序、统计维度和时间桶声明 `indexes`,并确认用户能接受的刷新延迟;live 模式忽略 `indexes`,不需要刷新。
|
|
174
|
+
|
|
175
|
+
后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。当前 MVP 中直接运行时接口要求调用者具备应用自动化管理权限,自动化/流程内部调用走服务端受控上下文。
|
|
174
176
|
|
|
175
177
|
常用数据视图命令:
|
|
176
178
|
|
package/lib/cli.js
CHANGED
|
@@ -1098,6 +1098,7 @@ async function workspace(args) {
|
|
|
1098
1098
|
pages: {},
|
|
1099
1099
|
workflows: {},
|
|
1100
1100
|
automations: {},
|
|
1101
|
+
functions: {},
|
|
1101
1102
|
menus: {},
|
|
1102
1103
|
roles: {},
|
|
1103
1104
|
connectors: {},
|
|
@@ -3129,6 +3130,7 @@ function ensureResourceBuckets(bound) {
|
|
|
3129
3130
|
bound.resources.pages = bound.resources.pages || {};
|
|
3130
3131
|
bound.resources.workflows = bound.resources.workflows || {};
|
|
3131
3132
|
bound.resources.automations = bound.resources.automations || {};
|
|
3133
|
+
bound.resources.functions = bound.resources.functions || {};
|
|
3132
3134
|
bound.resources.menus = bound.resources.menus || {};
|
|
3133
3135
|
bound.resources.roles = bound.resources.roles || {};
|
|
3134
3136
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
@@ -3327,8 +3329,16 @@ function saveAutomationResource(target, automationCode, automationId, extra = {}
|
|
|
3327
3329
|
}, keys);
|
|
3328
3330
|
}
|
|
3329
3331
|
|
|
3332
|
+
function saveFunctionResource(target, functionCode, functionId, extra = {}) {
|
|
3333
|
+
const keys = ['functionId', 'status'];
|
|
3334
|
+
saveStateResource(target, 'functions', functionCode, {
|
|
3335
|
+
...pickStateFields(extra, keys),
|
|
3336
|
+
functionId,
|
|
3337
|
+
}, keys);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3330
3340
|
function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
|
|
3331
|
-
const keys = ['dataViewId', 'materializedViewName', 'status'];
|
|
3341
|
+
const keys = ['dataViewId', 'materializedViewName', 'status', 'storageMode'];
|
|
3332
3342
|
saveStateResource(target, 'dataViews', dataViewCode, {
|
|
3333
3343
|
...pickStateFields(extra, keys),
|
|
3334
3344
|
dataViewId,
|
|
@@ -3440,6 +3450,7 @@ const RESOURCE_SPECS = [
|
|
|
3440
3450
|
{ key: 'menus', dir: 'menus', topFiles: ['menus.json'], pluralKeys: ['menus'] },
|
|
3441
3451
|
{ key: 'workflows', dir: 'workflows', topFiles: ['workflows.json'], pluralKeys: ['workflows'] },
|
|
3442
3452
|
{ key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
|
|
3453
|
+
{ key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
|
|
3443
3454
|
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
3444
3455
|
{
|
|
3445
3456
|
key: 'pagePermissionGroups',
|
|
@@ -3631,6 +3642,8 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3631
3642
|
const pagePermissionGroupCodes = unique(
|
|
3632
3643
|
pagePermissionGroups.map(item => item.code).filter(Boolean)
|
|
3633
3644
|
);
|
|
3645
|
+
const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
|
|
3646
|
+
const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
|
|
3634
3647
|
const content = [
|
|
3635
3648
|
'/* eslint-disable */',
|
|
3636
3649
|
'// Generated by openxiangda resource typegen. Do not edit manually.',
|
|
@@ -3644,6 +3657,12 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3644
3657
|
`export const pagePermissionGroupCodes = ${JSON.stringify(pagePermissionGroupCodes, null, 2)} as const`,
|
|
3645
3658
|
'export type PagePermissionGroupCode = typeof pagePermissionGroupCodes[number]',
|
|
3646
3659
|
'',
|
|
3660
|
+
`export const dataViewCodes = ${JSON.stringify(dataViewCodes, null, 2)} as const`,
|
|
3661
|
+
'export type DataViewCode = typeof dataViewCodes[number]',
|
|
3662
|
+
'',
|
|
3663
|
+
`export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
|
|
3664
|
+
'export type FunctionCode = typeof functionCodes[number]',
|
|
3665
|
+
'',
|
|
3647
3666
|
`export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
|
|
3648
3667
|
'',
|
|
3649
3668
|
`export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
|
|
@@ -3656,6 +3675,8 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3656
3675
|
menuCodes: menuCodes.length,
|
|
3657
3676
|
routeCodes: routeCodes.length,
|
|
3658
3677
|
pagePermissionGroups: pagePermissionGroupCodes.length,
|
|
3678
|
+
dataViews: dataViewCodes.length,
|
|
3679
|
+
functions: functionCodes.length,
|
|
3659
3680
|
};
|
|
3660
3681
|
}
|
|
3661
3682
|
|
|
@@ -3702,9 +3723,20 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
3702
3723
|
errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
|
|
3703
3724
|
}
|
|
3704
3725
|
}
|
|
3726
|
+
if (kind === 'functions') {
|
|
3727
|
+
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
3728
|
+
if (!item.definitionJson && !item.definitionFile) {
|
|
3729
|
+
errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3705
3732
|
if (kind === 'dataViews') {
|
|
3706
3733
|
const definition = item.definition || item;
|
|
3707
3734
|
const viewType = String(definition.viewType || 'row').toLowerCase();
|
|
3735
|
+
const storageMode = normalizeDataViewStorageMode(
|
|
3736
|
+
item.storageMode || definition.storageMode || 'materialized',
|
|
3737
|
+
label,
|
|
3738
|
+
errors
|
|
3739
|
+
);
|
|
3708
3740
|
if (!item.name && !definition.name) errors.push(`${label}: 缺少 name`);
|
|
3709
3741
|
if (!definition.base) errors.push(`${label}: 缺少 base`);
|
|
3710
3742
|
if (viewType === 'aggregate') {
|
|
@@ -3721,7 +3753,7 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
3721
3753
|
} else {
|
|
3722
3754
|
errors.push(`${label}: 不支持的 viewType: ${definition.viewType}`);
|
|
3723
3755
|
}
|
|
3724
|
-
validateDataViewPerformance(label, definition, errors, warnings);
|
|
3756
|
+
validateDataViewPerformance(label, definition, errors, warnings, storageMode);
|
|
3725
3757
|
}
|
|
3726
3758
|
if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
3727
3759
|
if (kind === 'formPermissionGroups') {
|
|
@@ -3743,7 +3775,7 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
3743
3775
|
}
|
|
3744
3776
|
}
|
|
3745
3777
|
|
|
3746
|
-
function validateDataViewPerformance(label, definition, errors, warnings) {
|
|
3778
|
+
function validateDataViewPerformance(label, definition, errors, warnings, storageMode = 'materialized') {
|
|
3747
3779
|
const viewType = String(definition.viewType || 'row').toLowerCase();
|
|
3748
3780
|
if (viewType !== 'row' && viewType !== 'aggregate') return;
|
|
3749
3781
|
const outputAliases = collectDataViewOutputAliases(definition, viewType);
|
|
@@ -3760,6 +3792,13 @@ function validateDataViewPerformance(label, definition, errors, warnings) {
|
|
|
3760
3792
|
}
|
|
3761
3793
|
}
|
|
3762
3794
|
|
|
3795
|
+
if (storageMode === 'live') {
|
|
3796
|
+
if (indexes.length) {
|
|
3797
|
+
warnings.push(`${label}: live 数据视图不会创建物化索引,indexes 仅在 materialized 模式生效`);
|
|
3798
|
+
}
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3763
3802
|
if (!indexes.length) {
|
|
3764
3803
|
warnings.push(
|
|
3765
3804
|
viewType === 'aggregate'
|
|
@@ -3794,6 +3833,13 @@ function validateDataViewPerformance(label, definition, errors, warnings) {
|
|
|
3794
3833
|
}
|
|
3795
3834
|
}
|
|
3796
3835
|
|
|
3836
|
+
function normalizeDataViewStorageMode(value, label, errors) {
|
|
3837
|
+
const storageMode = String(value || 'materialized').trim().toLowerCase();
|
|
3838
|
+
if (storageMode === 'materialized' || storageMode === 'live') return storageMode;
|
|
3839
|
+
errors.push(`${label}: 不支持的 storageMode: ${value}`);
|
|
3840
|
+
return 'materialized';
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3797
3843
|
function collectDataViewOutputAliases(definition, viewType) {
|
|
3798
3844
|
if (viewType === 'aggregate') {
|
|
3799
3845
|
return [
|
|
@@ -4109,6 +4155,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
4109
4155
|
addPlanActions(actions, 'connector', manifest.connectors, existing.connectors, connectorEquals);
|
|
4110
4156
|
addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
|
|
4111
4157
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
4158
|
+
addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
|
|
4112
4159
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
4113
4160
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
4114
4161
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
@@ -4143,6 +4190,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
4143
4190
|
await publishConnectorResources(config, target, manifest.connectors || [], result);
|
|
4144
4191
|
await publishNotificationResources(config, target, manifest.notifications || [], result);
|
|
4145
4192
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
4193
|
+
await publishFunctionResources(config, target, manifest.functions || [], result);
|
|
4146
4194
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
4147
4195
|
await publishDataViewResources(config, target, manifest.dataViews || [], result);
|
|
4148
4196
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
@@ -4165,6 +4213,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4165
4213
|
notificationTemplates: new Map(),
|
|
4166
4214
|
notificationTypeConfigs: new Map(),
|
|
4167
4215
|
workflows: new Map(),
|
|
4216
|
+
functions: new Map(),
|
|
4168
4217
|
automations: new Map(),
|
|
4169
4218
|
dataViews: new Map(),
|
|
4170
4219
|
pagePermissionGroups: new Map(),
|
|
@@ -4245,6 +4294,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4245
4294
|
);
|
|
4246
4295
|
indexByCode(maps.automations, normalizeItems(data), item => item.resourceCode);
|
|
4247
4296
|
}
|
|
4297
|
+
if ((manifest.functions || []).length > 0) {
|
|
4298
|
+
const data = await requestWithAuth(
|
|
4299
|
+
config,
|
|
4300
|
+
target.profileName,
|
|
4301
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`, {
|
|
4302
|
+
page: 1,
|
|
4303
|
+
pageSize: 1000,
|
|
4304
|
+
})
|
|
4305
|
+
);
|
|
4306
|
+
indexByCode(maps.functions, normalizeItems(data), item => item.code || item.functionCode || item.resourceCode);
|
|
4307
|
+
for (const item of manifest.functions || []) {
|
|
4308
|
+
const code = item.code || item.functionCode || item.resourceCode;
|
|
4309
|
+
const existing = maps.functions.get(code);
|
|
4310
|
+
if (!existing) continue;
|
|
4311
|
+
const detail = await requestOptionalWithAuth(
|
|
4312
|
+
config,
|
|
4313
|
+
target.profileName,
|
|
4314
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`
|
|
4315
|
+
);
|
|
4316
|
+
maps.functions.set(code, {
|
|
4317
|
+
...existing,
|
|
4318
|
+
...(detail || {}),
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4248
4322
|
if ((manifest.dataViews || []).length > 0) {
|
|
4249
4323
|
const data = await requestWithAuth(
|
|
4250
4324
|
config,
|
|
@@ -4432,7 +4506,16 @@ async function requestOptionalWithAuth(config, profileName, apiPath, options = {
|
|
|
4432
4506
|
try {
|
|
4433
4507
|
return await requestWithAuth(config, profileName, apiPath, options);
|
|
4434
4508
|
} catch (error) {
|
|
4435
|
-
|
|
4509
|
+
const status = Number(error?.status || error?.statusCode || error?.response?.status);
|
|
4510
|
+
const apiCode = Number(error?.apiCode);
|
|
4511
|
+
const message = String(error?.message || '');
|
|
4512
|
+
if (
|
|
4513
|
+
status === 404 ||
|
|
4514
|
+
apiCode === 404 ||
|
|
4515
|
+
message.includes('HTTP 404') ||
|
|
4516
|
+
message.includes('不存在') ||
|
|
4517
|
+
/not\s*found/i.test(message)
|
|
4518
|
+
) {
|
|
4436
4519
|
return null;
|
|
4437
4520
|
}
|
|
4438
4521
|
throw error;
|
|
@@ -4782,6 +4865,7 @@ async function publishAutomationResources(config, target, automations, result) {
|
|
|
4782
4865
|
'definitionJson',
|
|
4783
4866
|
'definitionFile'
|
|
4784
4867
|
);
|
|
4868
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, automationItem, definitionJson);
|
|
4785
4869
|
const viewJson =
|
|
4786
4870
|
(await resolveManifestJson(
|
|
4787
4871
|
config,
|
|
@@ -4845,6 +4929,58 @@ async function publishAutomationResources(config, target, automations, result) {
|
|
|
4845
4929
|
}
|
|
4846
4930
|
}
|
|
4847
4931
|
|
|
4932
|
+
async function publishFunctionResources(config, target, functions, result) {
|
|
4933
|
+
for (const functionItem of functions) {
|
|
4934
|
+
const code = functionItem.code || functionItem.functionCode;
|
|
4935
|
+
const existing = await findExistingFunction(config, target, code);
|
|
4936
|
+
const definitionJson = await resolveManifestJson(
|
|
4937
|
+
config,
|
|
4938
|
+
target.profileName,
|
|
4939
|
+
functionItem,
|
|
4940
|
+
'definitionJson',
|
|
4941
|
+
'definitionFile'
|
|
4942
|
+
);
|
|
4943
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, functionItem, definitionJson);
|
|
4944
|
+
const body = stripUndefinedValues({
|
|
4945
|
+
code,
|
|
4946
|
+
name: functionItem.name || definitionJson.name || code,
|
|
4947
|
+
description:
|
|
4948
|
+
functionItem.description !== undefined
|
|
4949
|
+
? functionItem.description
|
|
4950
|
+
: definitionJson.description || '',
|
|
4951
|
+
definitionJson,
|
|
4952
|
+
resourceBindings: definitionJson.resourceBindings,
|
|
4953
|
+
inputSchema: functionItem.inputSchema || definitionJson.inputSchema,
|
|
4954
|
+
outputSchema: functionItem.outputSchema || definitionJson.outputSchema,
|
|
4955
|
+
status: functionItem.status,
|
|
4956
|
+
});
|
|
4957
|
+
const data = existing
|
|
4958
|
+
? await requestWithAuth(
|
|
4959
|
+
config,
|
|
4960
|
+
target.profileName,
|
|
4961
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`,
|
|
4962
|
+
{ method: 'PUT', body }
|
|
4963
|
+
)
|
|
4964
|
+
: await requestWithAuth(
|
|
4965
|
+
config,
|
|
4966
|
+
target.profileName,
|
|
4967
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`,
|
|
4968
|
+
{ method: 'POST', body }
|
|
4969
|
+
);
|
|
4970
|
+
if (data?.id) {
|
|
4971
|
+
saveFunctionResource(target, code, data.id, {
|
|
4972
|
+
status: data.status,
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
result.published.push({
|
|
4976
|
+
kind: 'function',
|
|
4977
|
+
code,
|
|
4978
|
+
action: existing ? 'update' : 'create',
|
|
4979
|
+
id: data?.id,
|
|
4980
|
+
});
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4848
4984
|
async function publishDataViewResources(config, target, dataViews, result) {
|
|
4849
4985
|
for (const dataViewItem of dataViews) {
|
|
4850
4986
|
const existing = await findExistingDataView(config, target, dataViewItem.code);
|
|
@@ -4866,6 +5002,7 @@ async function publishDataViewResources(config, target, dataViews, result) {
|
|
|
4866
5002
|
saveDataViewResource(target, dataViewItem.code, data.id, {
|
|
4867
5003
|
materializedViewName: data.materializedViewName,
|
|
4868
5004
|
status: data.status,
|
|
5005
|
+
storageMode: data.storageMode,
|
|
4869
5006
|
});
|
|
4870
5007
|
}
|
|
4871
5008
|
result.published.push({
|
|
@@ -4877,6 +5014,18 @@ async function publishDataViewResources(config, target, dataViews, result) {
|
|
|
4877
5014
|
}
|
|
4878
5015
|
}
|
|
4879
5016
|
|
|
5017
|
+
async function findExistingFunction(config, target, code) {
|
|
5018
|
+
const stateId = target.bound.resources?.functions?.[code]?.functionId;
|
|
5019
|
+
const byCode = await requestOptionalWithAuth(
|
|
5020
|
+
config,
|
|
5021
|
+
target.profileName,
|
|
5022
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`
|
|
5023
|
+
);
|
|
5024
|
+
if (byCode?.id) return byCode;
|
|
5025
|
+
if (stateId) return { id: stateId, code };
|
|
5026
|
+
return null;
|
|
5027
|
+
}
|
|
5028
|
+
|
|
4880
5029
|
async function findExistingDataView(config, target, code) {
|
|
4881
5030
|
const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
|
|
4882
5031
|
const byCode = await requestOptionalWithAuth(
|
|
@@ -5026,6 +5175,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
5026
5175
|
await pruneMenus(config, target, desiredCodes(manifest.menus), result);
|
|
5027
5176
|
await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
|
|
5028
5177
|
await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
|
|
5178
|
+
await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
|
|
5029
5179
|
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
5030
5180
|
await prunePagePermissionGroups(
|
|
5031
5181
|
config,
|
|
@@ -5198,6 +5348,29 @@ async function pruneDataViews(config, target, desired, result) {
|
|
|
5198
5348
|
}
|
|
5199
5349
|
}
|
|
5200
5350
|
|
|
5351
|
+
async function pruneFunctions(config, target, desired, result) {
|
|
5352
|
+
const data = await requestWithAuth(
|
|
5353
|
+
config,
|
|
5354
|
+
target.profileName,
|
|
5355
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`, {
|
|
5356
|
+
page: 1,
|
|
5357
|
+
pageSize: 1000,
|
|
5358
|
+
})
|
|
5359
|
+
);
|
|
5360
|
+
for (const fn of normalizeItems(data)) {
|
|
5361
|
+
const code = fn.code || fn.functionCode || fn.resourceCode;
|
|
5362
|
+
if (!code || desired.has(code)) continue;
|
|
5363
|
+
await pruneOne(config, target, result, 'function', code, fn.id, async () => {
|
|
5364
|
+
await requestWithAuth(
|
|
5365
|
+
config,
|
|
5366
|
+
target.profileName,
|
|
5367
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`,
|
|
5368
|
+
{ method: 'DELETE' }
|
|
5369
|
+
);
|
|
5370
|
+
});
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5201
5374
|
async function prunePagePermissionGroups(config, target, desired, result) {
|
|
5202
5375
|
const data = await requestWithAuth(
|
|
5203
5376
|
config,
|
|
@@ -5272,6 +5445,7 @@ function removeStateResource(target, kind, code) {
|
|
|
5272
5445
|
menu: 'menus',
|
|
5273
5446
|
workflow: 'workflows',
|
|
5274
5447
|
automation: 'automations',
|
|
5448
|
+
function: 'functions',
|
|
5275
5449
|
dataView: 'dataViews',
|
|
5276
5450
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
5277
5451
|
formPermissionGroup: 'formPermissionGroups',
|
|
@@ -5441,6 +5615,35 @@ async function pullResources(config, target) {
|
|
|
5441
5615
|
written.push(path.relative(process.cwd(), filePath));
|
|
5442
5616
|
}
|
|
5443
5617
|
|
|
5618
|
+
const functions = await requestWithAuth(
|
|
5619
|
+
config,
|
|
5620
|
+
target.profileName,
|
|
5621
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`, {
|
|
5622
|
+
page: 1,
|
|
5623
|
+
pageSize: 1000,
|
|
5624
|
+
})
|
|
5625
|
+
);
|
|
5626
|
+
for (const fn of normalizeItems(functions)) {
|
|
5627
|
+
const code = fn.code || fn.functionCode || fn.resourceCode || fn.id;
|
|
5628
|
+
const detail = await requestWithAuth(
|
|
5629
|
+
config,
|
|
5630
|
+
target.profileName,
|
|
5631
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`
|
|
5632
|
+
);
|
|
5633
|
+
const filePath = path.join(baseDir, 'functions', `${code}.json`);
|
|
5634
|
+
writeResourceJsonFile(filePath, {
|
|
5635
|
+
code,
|
|
5636
|
+
name: detail.name,
|
|
5637
|
+
description: detail.description || '',
|
|
5638
|
+
definitionJson: detail.definitionJson,
|
|
5639
|
+
resourceBindings: detail.resourceBindings || undefined,
|
|
5640
|
+
inputSchema: detail.inputSchema || undefined,
|
|
5641
|
+
outputSchema: detail.outputSchema || undefined,
|
|
5642
|
+
status: detail.status || undefined,
|
|
5643
|
+
});
|
|
5644
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
5645
|
+
}
|
|
5646
|
+
|
|
5444
5647
|
const dataViews = await requestWithAuth(
|
|
5445
5648
|
config,
|
|
5446
5649
|
target.profileName,
|
|
@@ -5649,6 +5852,7 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
|
|
|
5649
5852
|
code: dataView.code || dataView.resourceCode,
|
|
5650
5853
|
name: dataView.name,
|
|
5651
5854
|
description: dataView.description || '',
|
|
5855
|
+
storageMode: dataView.storageMode || 'materialized',
|
|
5652
5856
|
definition,
|
|
5653
5857
|
refreshConfig: dataView.refreshConfig || undefined,
|
|
5654
5858
|
permissionGroups: groups.length > 0 ? groups : undefined,
|
|
@@ -5818,12 +6022,15 @@ function normalizeDataViewManifest(bound, dataView) {
|
|
|
5818
6022
|
definition.description = dataView.description;
|
|
5819
6023
|
}
|
|
5820
6024
|
const normalizedDefinition = normalizeDataViewDefinitionFormRefs(bound, definition, dataView);
|
|
6025
|
+
const storageMode = String(dataView.storageMode || normalizedDefinition.storageMode || 'materialized').toLowerCase();
|
|
6026
|
+
delete normalizedDefinition.storageMode;
|
|
5821
6027
|
return stripUndefinedValues({
|
|
5822
6028
|
code: dataView.code || dataView.resourceCode || normalizedDefinition.code,
|
|
5823
6029
|
name: dataView.name || normalizedDefinition.name || dataView.code || normalizedDefinition.code,
|
|
5824
6030
|
description: dataView.description !== undefined
|
|
5825
|
-
|
|
5826
|
-
|
|
6031
|
+
? dataView.description
|
|
6032
|
+
: normalizedDefinition.description || '',
|
|
6033
|
+
storageMode,
|
|
5827
6034
|
definition: normalizedDefinition,
|
|
5828
6035
|
refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
|
|
5829
6036
|
permissionGroups: Array.isArray(dataView.permissionGroups)
|
|
@@ -5836,6 +6043,7 @@ function withoutDataViewManifestMeta(dataView) {
|
|
|
5836
6043
|
const next = withoutResourceMeta(dataView);
|
|
5837
6044
|
delete next.permissionGroups;
|
|
5838
6045
|
delete next.refreshConfig;
|
|
6046
|
+
delete next.storageMode;
|
|
5839
6047
|
return next;
|
|
5840
6048
|
}
|
|
5841
6049
|
|
|
@@ -5872,6 +6080,93 @@ function resolveDataViewSourceFormUuid(bound, source, dataView, label) {
|
|
|
5872
6080
|
delete source.form;
|
|
5873
6081
|
}
|
|
5874
6082
|
|
|
6083
|
+
function applyResourceBindingsToRuntimeDefinition(bound, item, definitionJson) {
|
|
6084
|
+
if (!definitionJson || typeof definitionJson !== 'object') return definitionJson;
|
|
6085
|
+
const topBindings = resolveRuntimeResourceBindings(
|
|
6086
|
+
bound,
|
|
6087
|
+
item.resources || definitionJson.resources,
|
|
6088
|
+
item
|
|
6089
|
+
);
|
|
6090
|
+
if (!isEmptyResourceBindings(topBindings)) {
|
|
6091
|
+
definitionJson.resourceBindings = mergeResourceBindings(
|
|
6092
|
+
definitionJson.resourceBindings,
|
|
6093
|
+
topBindings
|
|
6094
|
+
);
|
|
6095
|
+
}
|
|
6096
|
+
applyNestedRuntimeResourceBindings(bound, definitionJson, item);
|
|
6097
|
+
return definitionJson;
|
|
6098
|
+
}
|
|
6099
|
+
|
|
6100
|
+
function applyNestedRuntimeResourceBindings(bound, value, owner) {
|
|
6101
|
+
if (!value || typeof value !== 'object') return;
|
|
6102
|
+
if (Array.isArray(value)) {
|
|
6103
|
+
for (const item of value) applyNestedRuntimeResourceBindings(bound, item, owner);
|
|
6104
|
+
return;
|
|
6105
|
+
}
|
|
6106
|
+
if (value.resources && typeof value.resources === 'object') {
|
|
6107
|
+
const bindings = resolveRuntimeResourceBindings(bound, value.resources, owner);
|
|
6108
|
+
if (!isEmptyResourceBindings(bindings)) {
|
|
6109
|
+
value.resourceBindings = mergeResourceBindings(value.resourceBindings, bindings);
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
for (const child of Object.values(value)) {
|
|
6113
|
+
if (child && typeof child === 'object') {
|
|
6114
|
+
applyNestedRuntimeResourceBindings(bound, child, owner);
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
|
|
6119
|
+
function resolveRuntimeResourceBindings(bound, resources, item) {
|
|
6120
|
+
if (!resources || typeof resources !== 'object') return {};
|
|
6121
|
+
const label = resourceLabel('resource', item);
|
|
6122
|
+
const forms = {};
|
|
6123
|
+
for (const formCode of normalizeResourceCodeList(resources.forms)) {
|
|
6124
|
+
const formUuid = bound.resources?.forms?.[formCode]?.formUuid;
|
|
6125
|
+
if (!formUuid) {
|
|
6126
|
+
fail(`${label}: resources.forms 未绑定: ${formCode}`);
|
|
6127
|
+
}
|
|
6128
|
+
forms[formCode] = formUuid;
|
|
6129
|
+
}
|
|
6130
|
+
const dataViews = {};
|
|
6131
|
+
for (const dataViewCode of normalizeResourceCodeList(resources.dataViews)) {
|
|
6132
|
+
dataViews[dataViewCode] = dataViewCode;
|
|
6133
|
+
}
|
|
6134
|
+
return stripEmptyResourceBindings({ forms, dataViews });
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
function normalizeResourceCodeList(value) {
|
|
6138
|
+
if (!value) return [];
|
|
6139
|
+
if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean);
|
|
6140
|
+
if (typeof value === 'object') return Object.keys(value).map(item => String(item || '').trim()).filter(Boolean);
|
|
6141
|
+
return [String(value).trim()].filter(Boolean);
|
|
6142
|
+
}
|
|
6143
|
+
|
|
6144
|
+
function mergeResourceBindings(left, right) {
|
|
6145
|
+
return stripEmptyResourceBindings({
|
|
6146
|
+
...(left || {}),
|
|
6147
|
+
...(right || {}),
|
|
6148
|
+
forms: {
|
|
6149
|
+
...((left && left.forms) || {}),
|
|
6150
|
+
...((right && right.forms) || {}),
|
|
6151
|
+
},
|
|
6152
|
+
dataViews: {
|
|
6153
|
+
...((left && left.dataViews) || {}),
|
|
6154
|
+
...((right && right.dataViews) || {}),
|
|
6155
|
+
},
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
function stripEmptyResourceBindings(bindings) {
|
|
6160
|
+
const next = { ...(bindings || {}) };
|
|
6161
|
+
if (!next.forms || Object.keys(next.forms).length === 0) delete next.forms;
|
|
6162
|
+
if (!next.dataViews || Object.keys(next.dataViews).length === 0) delete next.dataViews;
|
|
6163
|
+
return next;
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
function isEmptyResourceBindings(bindings) {
|
|
6167
|
+
return Object.keys(stripEmptyResourceBindings(bindings)).length === 0;
|
|
6168
|
+
}
|
|
6169
|
+
|
|
5875
6170
|
async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
|
|
5876
6171
|
let value = item[objectKey];
|
|
5877
6172
|
if (value === undefined && item[fileKey]) {
|
|
@@ -6166,6 +6461,7 @@ function workflowEquals(bound, desired, existing) {
|
|
|
6166
6461
|
function automationEquals(target, desired, existing) {
|
|
6167
6462
|
if (!existing) return false;
|
|
6168
6463
|
const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
|
|
6464
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, desired, desiredDefinition);
|
|
6169
6465
|
const desiredView = resolveManifestPlainJson(desired, 'viewJson', 'viewFile', true);
|
|
6170
6466
|
const automationPayload = resolveAutomationManifestPayload(target, desired);
|
|
6171
6467
|
const desiredTags =
|
|
@@ -6195,6 +6491,23 @@ function automationEquals(target, desired, existing) {
|
|
|
6195
6491
|
);
|
|
6196
6492
|
}
|
|
6197
6493
|
|
|
6494
|
+
function functionEquals(target, desired, existing) {
|
|
6495
|
+
if (!existing) return false;
|
|
6496
|
+
const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
|
|
6497
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, desired, desiredDefinition);
|
|
6498
|
+
const desiredStatus = desired.status === undefined ? 'active' : String(desired.status || 'active');
|
|
6499
|
+
return (
|
|
6500
|
+
String(existing.code || existing.functionCode || existing.resourceCode || '') === String(desired.code || desired.functionCode || '') &&
|
|
6501
|
+
String(existing.name || '') === String(desired.name || desiredDefinition.name || desired.code || '') &&
|
|
6502
|
+
String(existing.description || '') === String(
|
|
6503
|
+
desired.description !== undefined ? desired.description : desiredDefinition.description || ''
|
|
6504
|
+
) &&
|
|
6505
|
+
jsonEqualsForPlan(existing.definitionJson, desiredDefinition, desired.__dir) &&
|
|
6506
|
+
jsonEqualsForPlan(existing.resourceBindings || {}, desiredDefinition.resourceBindings || {}, desired.__dir) &&
|
|
6507
|
+
String(existing.status || 'active') === desiredStatus
|
|
6508
|
+
);
|
|
6509
|
+
}
|
|
6510
|
+
|
|
6198
6511
|
function dataViewEquals(bound, desired, existing) {
|
|
6199
6512
|
if (!existing) return false;
|
|
6200
6513
|
const expected = normalizeDataViewManifest(bound, desired);
|
|
@@ -6205,6 +6518,7 @@ function dataViewEquals(bound, desired, existing) {
|
|
|
6205
6518
|
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
6206
6519
|
String(existing.name || '') === String(expected.name || '') &&
|
|
6207
6520
|
String(existing.description || '') === String(expected.description || '') &&
|
|
6521
|
+
String(existing.storageMode || 'materialized') === String(expected.storageMode || 'materialized') &&
|
|
6208
6522
|
jsonEqualsForPlan(existing.definition || {}, expected.definition || {}, desired.__dir) &&
|
|
6209
6523
|
jsonEqualsForPlan(existing.refreshConfig || {}, expected.refreshConfig || {}, desired.__dir) &&
|
|
6210
6524
|
permissionGroupsEqual
|
|
@@ -6538,26 +6852,31 @@ function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
6538
6852
|
const extension = path.extname(localPath).toLowerCase();
|
|
6539
6853
|
if (['.js', '.cjs', '.mjs'].includes(extension)) {
|
|
6540
6854
|
fail(
|
|
6541
|
-
`JS_CODE V2 要求 AI 源码使用 TypeScript,请把 sourceFile.localPath 指向 src/js-code-nodes/<scriptCode>/index.ts,而不是已构建的 ${extension} 文件: ${localPath}`
|
|
6855
|
+
`JS_CODE V2 要求 AI 源码使用 TypeScript,请把 sourceFile.localPath 指向 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts,而不是已构建的 ${extension} 文件: ${localPath}`
|
|
6542
6856
|
);
|
|
6543
6857
|
}
|
|
6544
6858
|
if (extension !== '.ts') {
|
|
6545
6859
|
fail(
|
|
6546
|
-
`JS_CODE V2 sourceFile.localPath 必须指向 TypeScript 源文件 src/js-code-nodes/<scriptCode>/index.ts: ${localPath}`
|
|
6860
|
+
`JS_CODE V2 sourceFile.localPath 必须指向 TypeScript 源文件 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts: ${localPath}`
|
|
6547
6861
|
);
|
|
6548
6862
|
}
|
|
6549
6863
|
|
|
6550
6864
|
const normalized = localPath.replace(/\\/g, '/');
|
|
6551
6865
|
const jsCodeMatched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
|
|
6552
6866
|
const automationMatched = normalized.match(/src\/automations\/([^/]+)\/index\.tsx?$/);
|
|
6553
|
-
const
|
|
6867
|
+
const functionMatched = normalized.match(/src\/functions\/([^/]+)\/index\.tsx?$/);
|
|
6868
|
+
const matched = jsCodeMatched || automationMatched || functionMatched;
|
|
6554
6869
|
const resolvedScriptCode = scriptCode || matched?.[1];
|
|
6555
6870
|
if (!resolvedScriptCode) {
|
|
6556
6871
|
fail(
|
|
6557
|
-
`无法从 sourceFile.localPath 推断 scriptCode,请使用 src/js-code-nodes/<scriptCode>/index.ts 或 src/
|
|
6872
|
+
`无法从 sourceFile.localPath 推断 scriptCode,请使用 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts: ${localPath}`
|
|
6558
6873
|
);
|
|
6559
6874
|
}
|
|
6560
|
-
const sourceKind =
|
|
6875
|
+
const sourceKind = functionMatched
|
|
6876
|
+
? 'functions'
|
|
6877
|
+
: automationMatched
|
|
6878
|
+
? 'automations'
|
|
6879
|
+
: 'js-code-nodes';
|
|
6561
6880
|
|
|
6562
6881
|
const workspaceRoot = findWorkspaceRoot(localPath);
|
|
6563
6882
|
const result = spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
|