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.
Files changed (25) hide show
  1. package/README.md +3 -1
  2. package/lib/cli.js +330 -11
  3. package/openxiangda-skills/SKILL.md +11 -9
  4. package/openxiangda-skills/references/automation-v3.md +17 -1
  5. package/openxiangda-skills/references/connector-resources.md +4 -1
  6. package/openxiangda-skills/references/data-views.md +23 -9
  7. package/openxiangda-skills/references/pages/page-sdk.md +15 -0
  8. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +92 -19
  9. package/openxiangda-skills/references/workflow-v3.md +17 -1
  10. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +7 -5
  11. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +7 -4
  12. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +9 -9
  13. package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +2 -2
  14. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +19 -16
  15. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +4 -4
  16. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +27 -8
  17. package/package.json +1 -1
  18. package/packages/sdk/dist/runtime/index.cjs +20 -0
  19. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  20. package/packages/sdk/dist/runtime/index.d.mts +18 -1
  21. package/packages/sdk/dist/runtime/index.d.ts +18 -1
  22. package/packages/sdk/dist/runtime/index.mjs +20 -0
  23. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  24. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +8 -2
  25. 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` 数据视图,而不是在页面里手写多次单表查询再拼数据。默认数据视图是行级联表视图,适合工单+客户、订单+商品、项目+成员、报表列表、跨页面复用查询等读多写少场景;`viewType: "aggregate"` 是统计聚合视图,适合按客户、状态、月份等维度预聚合 count/sum/avg/min/max。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`,平台创建 PostgreSQL materialized view;页面通过 `sdk.dataView.query(code, params)` 查询行级视图,通过 `sdk.dataView.stats(code, params)` 查询聚合视图,也可以用 `sdk.dataSource.run()` 路由 `dataView.query` / `dataView.stats`。发布前应为常用筛选、排序、统计维度和时间桶声明 `indexes`,并确认用户能接受的刷新延迟;默认不要设置低于 5 分钟的定时刷新。数据视图只读,刷新后才反映源表变化,不适合单表 CRUD、写回源表、强实时状态、临时 BI 查询或简单 linkedForm 下拉。
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
- if (Number(error?.apiCode) === 404 || String(error?.message || '').includes('HTTP 404')) {
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
- ? dataView.description
5826
- : normalizedDefinition.description || '',
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 matched = jsCodeMatched || automationMatched;
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/automations/<scriptCode>/index.ts: ${localPath}`
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 = automationMatched ? 'automations' : 'js-code-nodes';
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], {