openxiangda 1.0.77 → 1.0.78
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 +320 -10
- 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 +88 -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,
|
|
@@ -4782,6 +4856,7 @@ async function publishAutomationResources(config, target, automations, result) {
|
|
|
4782
4856
|
'definitionJson',
|
|
4783
4857
|
'definitionFile'
|
|
4784
4858
|
);
|
|
4859
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, automationItem, definitionJson);
|
|
4785
4860
|
const viewJson =
|
|
4786
4861
|
(await resolveManifestJson(
|
|
4787
4862
|
config,
|
|
@@ -4845,6 +4920,58 @@ async function publishAutomationResources(config, target, automations, result) {
|
|
|
4845
4920
|
}
|
|
4846
4921
|
}
|
|
4847
4922
|
|
|
4923
|
+
async function publishFunctionResources(config, target, functions, result) {
|
|
4924
|
+
for (const functionItem of functions) {
|
|
4925
|
+
const code = functionItem.code || functionItem.functionCode;
|
|
4926
|
+
const existing = await findExistingFunction(config, target, code);
|
|
4927
|
+
const definitionJson = await resolveManifestJson(
|
|
4928
|
+
config,
|
|
4929
|
+
target.profileName,
|
|
4930
|
+
functionItem,
|
|
4931
|
+
'definitionJson',
|
|
4932
|
+
'definitionFile'
|
|
4933
|
+
);
|
|
4934
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, functionItem, definitionJson);
|
|
4935
|
+
const body = stripUndefinedValues({
|
|
4936
|
+
code,
|
|
4937
|
+
name: functionItem.name || definitionJson.name || code,
|
|
4938
|
+
description:
|
|
4939
|
+
functionItem.description !== undefined
|
|
4940
|
+
? functionItem.description
|
|
4941
|
+
: definitionJson.description || '',
|
|
4942
|
+
definitionJson,
|
|
4943
|
+
resourceBindings: definitionJson.resourceBindings,
|
|
4944
|
+
inputSchema: functionItem.inputSchema || definitionJson.inputSchema,
|
|
4945
|
+
outputSchema: functionItem.outputSchema || definitionJson.outputSchema,
|
|
4946
|
+
status: functionItem.status,
|
|
4947
|
+
});
|
|
4948
|
+
const data = existing
|
|
4949
|
+
? await requestWithAuth(
|
|
4950
|
+
config,
|
|
4951
|
+
target.profileName,
|
|
4952
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`,
|
|
4953
|
+
{ method: 'PUT', body }
|
|
4954
|
+
)
|
|
4955
|
+
: await requestWithAuth(
|
|
4956
|
+
config,
|
|
4957
|
+
target.profileName,
|
|
4958
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`,
|
|
4959
|
+
{ method: 'POST', body }
|
|
4960
|
+
);
|
|
4961
|
+
if (data?.id) {
|
|
4962
|
+
saveFunctionResource(target, code, data.id, {
|
|
4963
|
+
status: data.status,
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4966
|
+
result.published.push({
|
|
4967
|
+
kind: 'function',
|
|
4968
|
+
code,
|
|
4969
|
+
action: existing ? 'update' : 'create',
|
|
4970
|
+
id: data?.id,
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4848
4975
|
async function publishDataViewResources(config, target, dataViews, result) {
|
|
4849
4976
|
for (const dataViewItem of dataViews) {
|
|
4850
4977
|
const existing = await findExistingDataView(config, target, dataViewItem.code);
|
|
@@ -4866,6 +4993,7 @@ async function publishDataViewResources(config, target, dataViews, result) {
|
|
|
4866
4993
|
saveDataViewResource(target, dataViewItem.code, data.id, {
|
|
4867
4994
|
materializedViewName: data.materializedViewName,
|
|
4868
4995
|
status: data.status,
|
|
4996
|
+
storageMode: data.storageMode,
|
|
4869
4997
|
});
|
|
4870
4998
|
}
|
|
4871
4999
|
result.published.push({
|
|
@@ -4877,6 +5005,18 @@ async function publishDataViewResources(config, target, dataViews, result) {
|
|
|
4877
5005
|
}
|
|
4878
5006
|
}
|
|
4879
5007
|
|
|
5008
|
+
async function findExistingFunction(config, target, code) {
|
|
5009
|
+
const stateId = target.bound.resources?.functions?.[code]?.functionId;
|
|
5010
|
+
const byCode = await requestOptionalWithAuth(
|
|
5011
|
+
config,
|
|
5012
|
+
target.profileName,
|
|
5013
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`
|
|
5014
|
+
);
|
|
5015
|
+
if (byCode?.id) return byCode;
|
|
5016
|
+
if (stateId) return { id: stateId, code };
|
|
5017
|
+
return null;
|
|
5018
|
+
}
|
|
5019
|
+
|
|
4880
5020
|
async function findExistingDataView(config, target, code) {
|
|
4881
5021
|
const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
|
|
4882
5022
|
const byCode = await requestOptionalWithAuth(
|
|
@@ -5026,6 +5166,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
5026
5166
|
await pruneMenus(config, target, desiredCodes(manifest.menus), result);
|
|
5027
5167
|
await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
|
|
5028
5168
|
await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
|
|
5169
|
+
await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
|
|
5029
5170
|
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
5030
5171
|
await prunePagePermissionGroups(
|
|
5031
5172
|
config,
|
|
@@ -5198,6 +5339,29 @@ async function pruneDataViews(config, target, desired, result) {
|
|
|
5198
5339
|
}
|
|
5199
5340
|
}
|
|
5200
5341
|
|
|
5342
|
+
async function pruneFunctions(config, target, desired, result) {
|
|
5343
|
+
const data = await requestWithAuth(
|
|
5344
|
+
config,
|
|
5345
|
+
target.profileName,
|
|
5346
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`, {
|
|
5347
|
+
page: 1,
|
|
5348
|
+
pageSize: 1000,
|
|
5349
|
+
})
|
|
5350
|
+
);
|
|
5351
|
+
for (const fn of normalizeItems(data)) {
|
|
5352
|
+
const code = fn.code || fn.functionCode || fn.resourceCode;
|
|
5353
|
+
if (!code || desired.has(code)) continue;
|
|
5354
|
+
await pruneOne(config, target, result, 'function', code, fn.id, async () => {
|
|
5355
|
+
await requestWithAuth(
|
|
5356
|
+
config,
|
|
5357
|
+
target.profileName,
|
|
5358
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`,
|
|
5359
|
+
{ method: 'DELETE' }
|
|
5360
|
+
);
|
|
5361
|
+
});
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5201
5365
|
async function prunePagePermissionGroups(config, target, desired, result) {
|
|
5202
5366
|
const data = await requestWithAuth(
|
|
5203
5367
|
config,
|
|
@@ -5272,6 +5436,7 @@ function removeStateResource(target, kind, code) {
|
|
|
5272
5436
|
menu: 'menus',
|
|
5273
5437
|
workflow: 'workflows',
|
|
5274
5438
|
automation: 'automations',
|
|
5439
|
+
function: 'functions',
|
|
5275
5440
|
dataView: 'dataViews',
|
|
5276
5441
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
5277
5442
|
formPermissionGroup: 'formPermissionGroups',
|
|
@@ -5441,6 +5606,35 @@ async function pullResources(config, target) {
|
|
|
5441
5606
|
written.push(path.relative(process.cwd(), filePath));
|
|
5442
5607
|
}
|
|
5443
5608
|
|
|
5609
|
+
const functions = await requestWithAuth(
|
|
5610
|
+
config,
|
|
5611
|
+
target.profileName,
|
|
5612
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`, {
|
|
5613
|
+
page: 1,
|
|
5614
|
+
pageSize: 1000,
|
|
5615
|
+
})
|
|
5616
|
+
);
|
|
5617
|
+
for (const fn of normalizeItems(functions)) {
|
|
5618
|
+
const code = fn.code || fn.functionCode || fn.resourceCode || fn.id;
|
|
5619
|
+
const detail = await requestWithAuth(
|
|
5620
|
+
config,
|
|
5621
|
+
target.profileName,
|
|
5622
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions/${encodeURIComponent(code)}`
|
|
5623
|
+
);
|
|
5624
|
+
const filePath = path.join(baseDir, 'functions', `${code}.json`);
|
|
5625
|
+
writeResourceJsonFile(filePath, {
|
|
5626
|
+
code,
|
|
5627
|
+
name: detail.name,
|
|
5628
|
+
description: detail.description || '',
|
|
5629
|
+
definitionJson: detail.definitionJson,
|
|
5630
|
+
resourceBindings: detail.resourceBindings || undefined,
|
|
5631
|
+
inputSchema: detail.inputSchema || undefined,
|
|
5632
|
+
outputSchema: detail.outputSchema || undefined,
|
|
5633
|
+
status: detail.status || undefined,
|
|
5634
|
+
});
|
|
5635
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
5636
|
+
}
|
|
5637
|
+
|
|
5444
5638
|
const dataViews = await requestWithAuth(
|
|
5445
5639
|
config,
|
|
5446
5640
|
target.profileName,
|
|
@@ -5649,6 +5843,7 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
|
|
|
5649
5843
|
code: dataView.code || dataView.resourceCode,
|
|
5650
5844
|
name: dataView.name,
|
|
5651
5845
|
description: dataView.description || '',
|
|
5846
|
+
storageMode: dataView.storageMode || 'materialized',
|
|
5652
5847
|
definition,
|
|
5653
5848
|
refreshConfig: dataView.refreshConfig || undefined,
|
|
5654
5849
|
permissionGroups: groups.length > 0 ? groups : undefined,
|
|
@@ -5818,12 +6013,15 @@ function normalizeDataViewManifest(bound, dataView) {
|
|
|
5818
6013
|
definition.description = dataView.description;
|
|
5819
6014
|
}
|
|
5820
6015
|
const normalizedDefinition = normalizeDataViewDefinitionFormRefs(bound, definition, dataView);
|
|
6016
|
+
const storageMode = String(dataView.storageMode || normalizedDefinition.storageMode || 'materialized').toLowerCase();
|
|
6017
|
+
delete normalizedDefinition.storageMode;
|
|
5821
6018
|
return stripUndefinedValues({
|
|
5822
6019
|
code: dataView.code || dataView.resourceCode || normalizedDefinition.code,
|
|
5823
6020
|
name: dataView.name || normalizedDefinition.name || dataView.code || normalizedDefinition.code,
|
|
5824
6021
|
description: dataView.description !== undefined
|
|
5825
|
-
|
|
5826
|
-
|
|
6022
|
+
? dataView.description
|
|
6023
|
+
: normalizedDefinition.description || '',
|
|
6024
|
+
storageMode,
|
|
5827
6025
|
definition: normalizedDefinition,
|
|
5828
6026
|
refreshConfig: dataView.refreshConfig || dataView.refresh || normalizedDefinition.refresh,
|
|
5829
6027
|
permissionGroups: Array.isArray(dataView.permissionGroups)
|
|
@@ -5836,6 +6034,7 @@ function withoutDataViewManifestMeta(dataView) {
|
|
|
5836
6034
|
const next = withoutResourceMeta(dataView);
|
|
5837
6035
|
delete next.permissionGroups;
|
|
5838
6036
|
delete next.refreshConfig;
|
|
6037
|
+
delete next.storageMode;
|
|
5839
6038
|
return next;
|
|
5840
6039
|
}
|
|
5841
6040
|
|
|
@@ -5872,6 +6071,93 @@ function resolveDataViewSourceFormUuid(bound, source, dataView, label) {
|
|
|
5872
6071
|
delete source.form;
|
|
5873
6072
|
}
|
|
5874
6073
|
|
|
6074
|
+
function applyResourceBindingsToRuntimeDefinition(bound, item, definitionJson) {
|
|
6075
|
+
if (!definitionJson || typeof definitionJson !== 'object') return definitionJson;
|
|
6076
|
+
const topBindings = resolveRuntimeResourceBindings(
|
|
6077
|
+
bound,
|
|
6078
|
+
item.resources || definitionJson.resources,
|
|
6079
|
+
item
|
|
6080
|
+
);
|
|
6081
|
+
if (!isEmptyResourceBindings(topBindings)) {
|
|
6082
|
+
definitionJson.resourceBindings = mergeResourceBindings(
|
|
6083
|
+
definitionJson.resourceBindings,
|
|
6084
|
+
topBindings
|
|
6085
|
+
);
|
|
6086
|
+
}
|
|
6087
|
+
applyNestedRuntimeResourceBindings(bound, definitionJson, item);
|
|
6088
|
+
return definitionJson;
|
|
6089
|
+
}
|
|
6090
|
+
|
|
6091
|
+
function applyNestedRuntimeResourceBindings(bound, value, owner) {
|
|
6092
|
+
if (!value || typeof value !== 'object') return;
|
|
6093
|
+
if (Array.isArray(value)) {
|
|
6094
|
+
for (const item of value) applyNestedRuntimeResourceBindings(bound, item, owner);
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
if (value.resources && typeof value.resources === 'object') {
|
|
6098
|
+
const bindings = resolveRuntimeResourceBindings(bound, value.resources, owner);
|
|
6099
|
+
if (!isEmptyResourceBindings(bindings)) {
|
|
6100
|
+
value.resourceBindings = mergeResourceBindings(value.resourceBindings, bindings);
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
for (const child of Object.values(value)) {
|
|
6104
|
+
if (child && typeof child === 'object') {
|
|
6105
|
+
applyNestedRuntimeResourceBindings(bound, child, owner);
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
|
|
6110
|
+
function resolveRuntimeResourceBindings(bound, resources, item) {
|
|
6111
|
+
if (!resources || typeof resources !== 'object') return {};
|
|
6112
|
+
const label = resourceLabel('resource', item);
|
|
6113
|
+
const forms = {};
|
|
6114
|
+
for (const formCode of normalizeResourceCodeList(resources.forms)) {
|
|
6115
|
+
const formUuid = bound.resources?.forms?.[formCode]?.formUuid;
|
|
6116
|
+
if (!formUuid) {
|
|
6117
|
+
fail(`${label}: resources.forms 未绑定: ${formCode}`);
|
|
6118
|
+
}
|
|
6119
|
+
forms[formCode] = formUuid;
|
|
6120
|
+
}
|
|
6121
|
+
const dataViews = {};
|
|
6122
|
+
for (const dataViewCode of normalizeResourceCodeList(resources.dataViews)) {
|
|
6123
|
+
dataViews[dataViewCode] = dataViewCode;
|
|
6124
|
+
}
|
|
6125
|
+
return stripEmptyResourceBindings({ forms, dataViews });
|
|
6126
|
+
}
|
|
6127
|
+
|
|
6128
|
+
function normalizeResourceCodeList(value) {
|
|
6129
|
+
if (!value) return [];
|
|
6130
|
+
if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean);
|
|
6131
|
+
if (typeof value === 'object') return Object.keys(value).map(item => String(item || '').trim()).filter(Boolean);
|
|
6132
|
+
return [String(value).trim()].filter(Boolean);
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
function mergeResourceBindings(left, right) {
|
|
6136
|
+
return stripEmptyResourceBindings({
|
|
6137
|
+
...(left || {}),
|
|
6138
|
+
...(right || {}),
|
|
6139
|
+
forms: {
|
|
6140
|
+
...((left && left.forms) || {}),
|
|
6141
|
+
...((right && right.forms) || {}),
|
|
6142
|
+
},
|
|
6143
|
+
dataViews: {
|
|
6144
|
+
...((left && left.dataViews) || {}),
|
|
6145
|
+
...((right && right.dataViews) || {}),
|
|
6146
|
+
},
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
function stripEmptyResourceBindings(bindings) {
|
|
6151
|
+
const next = { ...(bindings || {}) };
|
|
6152
|
+
if (!next.forms || Object.keys(next.forms).length === 0) delete next.forms;
|
|
6153
|
+
if (!next.dataViews || Object.keys(next.dataViews).length === 0) delete next.dataViews;
|
|
6154
|
+
return next;
|
|
6155
|
+
}
|
|
6156
|
+
|
|
6157
|
+
function isEmptyResourceBindings(bindings) {
|
|
6158
|
+
return Object.keys(stripEmptyResourceBindings(bindings)).length === 0;
|
|
6159
|
+
}
|
|
6160
|
+
|
|
5875
6161
|
async function resolveManifestJson(config, profileName, item, objectKey, fileKey, optional = false) {
|
|
5876
6162
|
let value = item[objectKey];
|
|
5877
6163
|
if (value === undefined && item[fileKey]) {
|
|
@@ -6166,6 +6452,7 @@ function workflowEquals(bound, desired, existing) {
|
|
|
6166
6452
|
function automationEquals(target, desired, existing) {
|
|
6167
6453
|
if (!existing) return false;
|
|
6168
6454
|
const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
|
|
6455
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, desired, desiredDefinition);
|
|
6169
6456
|
const desiredView = resolveManifestPlainJson(desired, 'viewJson', 'viewFile', true);
|
|
6170
6457
|
const automationPayload = resolveAutomationManifestPayload(target, desired);
|
|
6171
6458
|
const desiredTags =
|
|
@@ -6195,6 +6482,23 @@ function automationEquals(target, desired, existing) {
|
|
|
6195
6482
|
);
|
|
6196
6483
|
}
|
|
6197
6484
|
|
|
6485
|
+
function functionEquals(target, desired, existing) {
|
|
6486
|
+
if (!existing) return false;
|
|
6487
|
+
const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
|
|
6488
|
+
applyResourceBindingsToRuntimeDefinition(target.bound, desired, desiredDefinition);
|
|
6489
|
+
const desiredStatus = desired.status === undefined ? 'active' : String(desired.status || 'active');
|
|
6490
|
+
return (
|
|
6491
|
+
String(existing.code || existing.functionCode || existing.resourceCode || '') === String(desired.code || desired.functionCode || '') &&
|
|
6492
|
+
String(existing.name || '') === String(desired.name || desiredDefinition.name || desired.code || '') &&
|
|
6493
|
+
String(existing.description || '') === String(
|
|
6494
|
+
desired.description !== undefined ? desired.description : desiredDefinition.description || ''
|
|
6495
|
+
) &&
|
|
6496
|
+
jsonEqualsForPlan(existing.definitionJson, desiredDefinition, desired.__dir) &&
|
|
6497
|
+
jsonEqualsForPlan(existing.resourceBindings || {}, desiredDefinition.resourceBindings || {}, desired.__dir) &&
|
|
6498
|
+
String(existing.status || 'active') === desiredStatus
|
|
6499
|
+
);
|
|
6500
|
+
}
|
|
6501
|
+
|
|
6198
6502
|
function dataViewEquals(bound, desired, existing) {
|
|
6199
6503
|
if (!existing) return false;
|
|
6200
6504
|
const expected = normalizeDataViewManifest(bound, desired);
|
|
@@ -6205,6 +6509,7 @@ function dataViewEquals(bound, desired, existing) {
|
|
|
6205
6509
|
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
6206
6510
|
String(existing.name || '') === String(expected.name || '') &&
|
|
6207
6511
|
String(existing.description || '') === String(expected.description || '') &&
|
|
6512
|
+
String(existing.storageMode || 'materialized') === String(expected.storageMode || 'materialized') &&
|
|
6208
6513
|
jsonEqualsForPlan(existing.definition || {}, expected.definition || {}, desired.__dir) &&
|
|
6209
6514
|
jsonEqualsForPlan(existing.refreshConfig || {}, expected.refreshConfig || {}, desired.__dir) &&
|
|
6210
6515
|
permissionGroupsEqual
|
|
@@ -6538,26 +6843,31 @@ function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
6538
6843
|
const extension = path.extname(localPath).toLowerCase();
|
|
6539
6844
|
if (['.js', '.cjs', '.mjs'].includes(extension)) {
|
|
6540
6845
|
fail(
|
|
6541
|
-
`JS_CODE V2 要求 AI 源码使用 TypeScript,请把 sourceFile.localPath 指向 src/js-code-nodes/<scriptCode>/index.ts,而不是已构建的 ${extension} 文件: ${localPath}`
|
|
6846
|
+
`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
6847
|
);
|
|
6543
6848
|
}
|
|
6544
6849
|
if (extension !== '.ts') {
|
|
6545
6850
|
fail(
|
|
6546
|
-
`JS_CODE V2 sourceFile.localPath 必须指向 TypeScript 源文件 src/js-code-nodes/<scriptCode>/index.ts: ${localPath}`
|
|
6851
|
+
`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
6852
|
);
|
|
6548
6853
|
}
|
|
6549
6854
|
|
|
6550
6855
|
const normalized = localPath.replace(/\\/g, '/');
|
|
6551
6856
|
const jsCodeMatched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
|
|
6552
6857
|
const automationMatched = normalized.match(/src\/automations\/([^/]+)\/index\.tsx?$/);
|
|
6553
|
-
const
|
|
6858
|
+
const functionMatched = normalized.match(/src\/functions\/([^/]+)\/index\.tsx?$/);
|
|
6859
|
+
const matched = jsCodeMatched || automationMatched || functionMatched;
|
|
6554
6860
|
const resolvedScriptCode = scriptCode || matched?.[1];
|
|
6555
6861
|
if (!resolvedScriptCode) {
|
|
6556
6862
|
fail(
|
|
6557
|
-
`无法从 sourceFile.localPath 推断 scriptCode,请使用 src/js-code-nodes/<scriptCode>/index.ts 或 src/
|
|
6863
|
+
`无法从 sourceFile.localPath 推断 scriptCode,请使用 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts: ${localPath}`
|
|
6558
6864
|
);
|
|
6559
6865
|
}
|
|
6560
|
-
const sourceKind =
|
|
6866
|
+
const sourceKind = functionMatched
|
|
6867
|
+
? 'functions'
|
|
6868
|
+
: automationMatched
|
|
6869
|
+
? 'automations'
|
|
6870
|
+
: 'js-code-nodes';
|
|
6561
6871
|
|
|
6562
6872
|
const workspaceRoot = findWorkspaceRoot(localPath);
|
|
6563
6873
|
const result = spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
|
|
@@ -26,10 +26,11 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
26
26
|
| 创建 / 修改自定义代码页 / portal / dashboard | `openxiangda-page` | edit `src/pages/<code>/` → `workspace publish --page <code>` |
|
|
27
27
|
| 审批流程 / workflow / 流程节点 / JS_CODE | `openxiangda-workflow-automation` | `openxiangda workflow validate / create / publish` |
|
|
28
28
|
| 自动化 / 定时任务 / 提交触发 / cron | `openxiangda-workflow-automation` | `openxiangda automation validate / create / publish / enable` |
|
|
29
|
+
| App Function / function_call / 可复用后端逻辑 | `openxiangda-workflow-automation` | declare `src/resources/functions/<code>.json` + `src/functions/<code>/index.ts` |
|
|
29
30
|
| 角色 / 权限组 / 字段权限 / 数据范围 / 公开访问 | `openxiangda-permission-settings` | `openxiangda permission ...` / `openxiangda settings ...` |
|
|
30
31
|
| 看应用结构 / 快照 / 对比 / 诊断 / 排查 / 报错 | `openxiangda-inspect` | `openxiangda app snapshot <APP_XXX> --profile <name> --json` |
|
|
31
32
|
| 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
|
|
32
|
-
| 多表只读联表 / 固定口径统计 / 看板指标 | `openxiangda-form` (data view) | declare `src/resources/data-views/<code>.json` → `resource publish` |
|
|
33
|
+
| 多表只读联表 / 固定口径统计 / 强实时复杂查询 / 看板指标 | `openxiangda-form` (data view) | declare `src/resources/data-views/<code>.json` with `storageMode` → `resource publish` |
|
|
33
34
|
| 调外部 / 第三方 API / 钉钉 / 自建系统 | `openxiangda-page` (connector) | declare `src/resources/connectors/<code>.json` → `sdk.connector.invoke` |
|
|
34
35
|
|
|
35
36
|
### Hard rules — always
|
|
@@ -133,14 +134,15 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
133
134
|
- All visible copy in forms and pages must be written for end users. Do not put implementation notes, developer explanations, schema descriptions, or "this area is generated by..." text into page sections, cards, labels, tips, or empty states.
|
|
134
135
|
- Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
|
|
135
136
|
- Put engineering-managed resources in `src/resources/` and use `openxiangda resource validate|plan|publish|pull`. `workspace publish` publishes workspace forms/pages first, then runs non-destructive resource upsert. Only pass `--prune` when the user explicitly wants local manifests to delete platform-side extras.
|
|
136
|
-
- For repeated read-only multi-form queries or predefined dashboard metrics, create a data view manifest in `src/resources/data-views/`. Use row views plus `sdk.dataView.query` for joined lists/lookups; use `viewType: "aggregate"` plus `sdk.dataView.stats` for count/sum/avg/min/max statistics.
|
|
137
|
+
- For repeated read-only multi-form queries or predefined dashboard metrics, create a data view manifest in `src/resources/data-views/`. Use `storageMode: "materialized"` for refreshed lists/reports that tolerate delay; use `storageMode: "live"` for bounded real-time joins where source changes must appear immediately. Use row views plus `sdk.dataView.query` for joined lists/lookups; use `viewType: "aggregate"` plus `sdk.dataView.stats` for count/sum/avg/min/max statistics. Add `indexes` only for materialized views. Do not use data views for single-form CRUD, simple linkedForm selects, writes, write-back, unbounded realtime joins, or ad-hoc BI. Read `references/data-views.md` before designing one.
|
|
137
138
|
- For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
|
|
139
|
+
- For reusable backend logic shared by pages, automations, and workflows, create an App Function in `src/resources/functions/<functionCode>.json` plus `src/functions/<functionCode>/index.ts`. Call it from pages with `sdk.function.invoke`, from automation/workflow graphs with `function_call`, or from the runtime endpoint when the caller has app automation management permission. Current App Function MVP exposes controlled helpers such as `ctx.resources`, `ctx.form`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; it does not expose raw SQL or Redis.
|
|
138
140
|
- Before scaffolding a real app, complex page, data management page, portal shell, status lifecycle, role governance, or automation, read `references/best-practices.md` and pick the architecture first. Prefer copying the relevant pattern from `examples/best-practices/` into `src/` instead of generating a single large page file.
|
|
139
141
|
- Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
|
|
140
142
|
- Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` for diagnosis before changing an existing app.
|
|
141
143
|
- Run write commands that update `.openxiangda/state.json` sequentially within the same workspace. Read-only commands can run in parallel.
|
|
142
144
|
- JS_CODE is backend-executed workflow/automation logic, not frontend page code. Use it when logic must run after a backend trigger such as fixed cron schedules, form date-field schedules, form submit/update/delete/field-change events, or workflow approval/process events.
|
|
143
|
-
- Use JS_CODE for cross-form data queries, create/update/batch update operations, process termination, platform API calls, external HTTP calls, and
|
|
145
|
+
- Use JS_CODE for node-local cross-form data queries, create/update/batch update operations, process termination, platform API calls, external HTTP calls, and backend trigger orchestration. For logic that pages, automations, and workflows should reuse through a stable backend entry, prefer App Function. Do not use JS_CODE for simple UI interactions, ordinary form validation, or display-only page behavior.
|
|
144
146
|
- For workflow/automation JS_CODE nodes, prefer V2 `runtimeMode: "trusted_node"`. AI-authored source must be TypeScript under `sy-lowcode-app-workspace/src/js-code-nodes/<scriptCode>/index.ts`. `pnpm build-js-code --script <scriptCode>` runs TypeScript validation before bundling, and `sourceFile.localPath` should point to the TS source; the CLI builds, uploads, and replaces it with snapshot metadata during validate/create.
|
|
145
147
|
|
|
146
148
|
## Subskills
|
|
@@ -169,9 +171,9 @@ Core CLI / state:
|
|
|
169
171
|
|
|
170
172
|
- `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields.
|
|
171
173
|
- `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
|
|
172
|
-
- `references/connector-resources.md` — `src/resources` manifests, connector schema, and
|
|
173
|
-
- `references/resource-manifest-cheatsheet.md` — 复制即用的 connector / data-view / notification / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
|
|
174
|
-
- `references/data-views.md` — `src/resources/data-views` materialized view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
|
|
174
|
+
- `references/connector-resources.md` — `src/resources` manifests, connector schema, App Function boundary, and platform runtime calls.
|
|
175
|
+
- `references/resource-manifest-cheatsheet.md` — 复制即用的 connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
|
|
176
|
+
- `references/data-views.md` — `src/resources/data-views` materialized/live data view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
|
|
175
177
|
- `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
|
|
176
178
|
- `references/best-practices.md` — initialized examples for modular pages, state lifecycles, role governance, permission isolation, high-performance queries, portal shells, workflow boundaries, and automation patterns.
|
|
177
179
|
|
|
@@ -184,13 +186,13 @@ Form authoring:
|
|
|
184
186
|
Page authoring:
|
|
185
187
|
|
|
186
188
|
- `references/pages/workspace-structure.md` — `src/pages/<pageCode>/` layout and config.
|
|
187
|
-
- `references/pages/page-sdk.md` — `openxiangda/runtime` data and
|
|
189
|
+
- `references/pages/page-sdk.md` — `openxiangda/runtime` data, connector, data view, notification, and App Function APIs.
|
|
188
190
|
- `references/pages/publish-flow.md` — workspace publish steps for code pages.
|
|
189
191
|
|
|
190
192
|
Workflow / automation / permissions:
|
|
191
193
|
|
|
192
|
-
- `references/workflow-v3.md` — workflow v3 definitions, JS_CODE nodes, trusted_node mode.
|
|
193
|
-
- `references/automation-v3.md` — automation triggers, conditions, and publishing.
|
|
194
|
+
- `references/workflow-v3.md` — workflow v3 definitions, JS_CODE nodes, App Function `function_call`, and trusted_node mode.
|
|
195
|
+
- `references/automation-v3.md` — automation triggers, conditions, App Function `function_call`, and publishing.
|
|
194
196
|
- `references/permissions-settings.md` — roles, page/form permission groups, settings.
|
|
195
197
|
|
|
196
198
|
Platform domain knowledge (read these first when generating forms or pages so the output matches the live platform behavior):
|