openxiangda 1.0.143 → 1.0.145
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 +1 -1
- package/lib/cli.js +263 -18
- package/lib/design-gates.js +3 -0
- package/lib/sdd.js +53 -5
- package/openxiangda-skills/SKILL.md +1 -1
- package/openxiangda-skills/references/connector-resources.md +1 -1
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +11 -3
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -365,7 +365,7 @@ const PublicAccessError = ({ error }: { error: { message?: string } }) => (
|
|
|
365
365
|
|
|
366
366
|
多表只读查询和固定口径统计优先声明 `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`,不需要刷新。
|
|
367
367
|
|
|
368
|
-
后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。运行时页面调用会在 `ctx.operator`、`ctx.currentUser`、`ctx.permissions` 中注入可信的 `roleCodes`、`currentRoleCode`、`hasFullAccess`、`isAppAdmin`、`isPlatformAdmin`,敏感动作必须读取这些服务端上下文做授权,不要信任页面 input 里传入的角色字段。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,授权边界是函数 manifest 的 `resources.forms` 绑定和函数调用授权,不是页面用户对目标表单的直接提交入口;报名、签到、福利选择等内部多表写入应走 App Function,不要为了写内部表单给普通用户开放原始 submit 权限。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json`
|
|
368
|
+
后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。运行时页面调用会在 `ctx.operator`、`ctx.currentUser`、`ctx.permissions` 中注入可信的 `roleCodes`、`currentRoleCode`、`hasFullAccess`、`isAppAdmin`、`isPlatformAdmin`,敏感动作必须读取这些服务端上下文做授权,不要信任页面 input 里传入的角色字段。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,授权边界是函数 manifest 的 `resources.forms` 绑定和函数调用授权,不是页面用户对目标表单的直接提交入口;报名、签到、福利选择等内部多表写入应走 App Function,不要为了写内部表单给普通用户开放原始 submit 权限。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。直接运行时接口默认要求调用者具备应用自动化管理权限;普通页面调用必须在函数 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles` 或 `scope_policy`,`roleCodes` 只用于当前应用角色精确匹配,不支持 `"*"` / `"all-app-roles"`。自动化/流程内部调用走服务端受控上下文。内部业务表单需要关闭原始写入接口时,在表单 settings 中设置 `runtimeWrite.mode="function_only"`。
|
|
369
369
|
|
|
370
370
|
平台部门和账号管理走 app-scoped organization 能力。调用者必须在目标应用拥有 `app:organization:manage`;平台管理员天然可用,普通应用角色需要显式授权。Runtime service principal 不会直接放行,`ctx.organization` 会按真实操作人 / audit actor 校验权限。新接口只提供创建、更新、查询和密码专用操作,不提供删除;`account-update` 不能携带 `password`,重置他人密码必须走 `account-reset-password`,当前用户改密用 SDK / `ctx.organization.accounts.changeMyPassword({ oldPassword, newPassword })`。
|
|
371
371
|
|
package/lib/cli.js
CHANGED
|
@@ -542,7 +542,7 @@ async function sdd(args) {
|
|
|
542
542
|
'说明:',
|
|
543
543
|
' - SDD 记录保存在 openspec/,不依赖官方 OpenSpec CLI。',
|
|
544
544
|
' - 新工作区使用 openxiangda-sdd-v2:proposal/specs/design/tasks/evidence 给人读,coverage.json/release.json 给 CLI 校验。',
|
|
545
|
-
' - 高风险发布需要 approved change 的 coverage 覆盖本次 forms/pages/resources/runtime 目标和高风险文件。',
|
|
545
|
+
' - 高风险发布需要 approved change 的 coverage 覆盖本次 forms/pages/functions/automations/workflows/JS_CODE/resources/runtime 目标和高风险文件。',
|
|
546
546
|
].join('\n'));
|
|
547
547
|
return;
|
|
548
548
|
}
|
|
@@ -624,17 +624,22 @@ async function sdd(args) {
|
|
|
624
624
|
|
|
625
625
|
if (subcommand === 'verify') {
|
|
626
626
|
const changeId = positional[0] || flags.change;
|
|
627
|
-
const
|
|
628
|
-
changed
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
627
|
+
const hasExplicitReleaseScope = Boolean(
|
|
628
|
+
flags.changed || flags['changed-only'] || flags.since || flags.all || flags['no-changed']
|
|
629
|
+
);
|
|
630
|
+
const releasePlan = hasExplicitReleaseScope
|
|
631
|
+
? buildWorkspaceReleasePlan({
|
|
632
|
+
changed: Boolean(flags.changed || flags['changed-only'] || flags.since) && flags.changed !== false,
|
|
633
|
+
since: flags.since,
|
|
634
|
+
profile: flags.profile || '<profile>',
|
|
635
|
+
skipSdd: true,
|
|
636
|
+
})
|
|
637
|
+
: null;
|
|
633
638
|
const result = verifySddChange({
|
|
634
639
|
cwd: process.cwd(),
|
|
635
640
|
changeId,
|
|
636
641
|
releasePlan,
|
|
637
|
-
writeMetadata:
|
|
642
|
+
writeMetadata: Boolean(releasePlan),
|
|
638
643
|
});
|
|
639
644
|
if (flags.json) {
|
|
640
645
|
writeJson(result);
|
|
@@ -901,7 +906,7 @@ function printDoctorReport(result) {
|
|
|
901
906
|
}
|
|
902
907
|
if (result.release) {
|
|
903
908
|
lines.push(
|
|
904
|
-
`release plan: forms=${result.release.targets.forms.length}, pages=${result.release.targets.pages.length}, resources=${result.release.targets.resources ? 'yes' : 'no'}, runtime=${result.release.targets.runtime ? 'yes' : 'no'}`
|
|
909
|
+
`release plan: forms=${result.release.targets.forms.length}, pages=${result.release.targets.pages.length}, functions=${(result.release.targets.functions || []).length}, resources=${result.release.targets.resources ? 'yes' : 'no'}, runtime=${result.release.targets.runtime ? 'yes' : 'no'}`
|
|
905
910
|
);
|
|
906
911
|
}
|
|
907
912
|
if (result.sdd) {
|
|
@@ -7009,6 +7014,7 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
7009
7014
|
if (!item.definitionJson && !item.definitionFile) {
|
|
7010
7015
|
errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
|
|
7011
7016
|
}
|
|
7017
|
+
validateFunctionRuntimeInvoke(label, item, errors, warnings);
|
|
7012
7018
|
}
|
|
7013
7019
|
if (kind === 'dataViews') {
|
|
7014
7020
|
const definition = item.definition || item;
|
|
@@ -7217,17 +7223,183 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
7217
7223
|
if (!item.formCode && !item.formUuid && !item.code) {
|
|
7218
7224
|
errors.push(`${label}: 缺少 formCode、formUuid 或 code`);
|
|
7219
7225
|
}
|
|
7226
|
+
validateFormRuntimeWriteSettings(label, item, errors);
|
|
7220
7227
|
if (
|
|
7221
7228
|
item.settings === undefined &&
|
|
7222
7229
|
item.indexes === undefined &&
|
|
7223
7230
|
item.dataManagement === undefined &&
|
|
7224
|
-
item.publicAccess === undefined
|
|
7231
|
+
item.publicAccess === undefined &&
|
|
7232
|
+
item.runtimeWrite === undefined
|
|
7225
7233
|
) {
|
|
7226
|
-
warnings.push(`${label}: 未声明 settings/indexes/dataManagement/publicAccess`);
|
|
7234
|
+
warnings.push(`${label}: 未声明 settings/indexes/dataManagement/publicAccess/runtimeWrite`);
|
|
7235
|
+
}
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
7238
|
+
|
|
7239
|
+
const FUNCTION_RUNTIME_INVOKE_AUDIENCE_TYPES = new Set([
|
|
7240
|
+
'authenticated',
|
|
7241
|
+
'app_roles',
|
|
7242
|
+
'page_permission_group',
|
|
7243
|
+
'scope_policy',
|
|
7244
|
+
]);
|
|
7245
|
+
|
|
7246
|
+
const FUNCTION_RUNTIME_INVOKE_MAGIC_ROLE_CODES = new Set([
|
|
7247
|
+
'*',
|
|
7248
|
+
'all-app-roles',
|
|
7249
|
+
'all_app_roles',
|
|
7250
|
+
'authenticated',
|
|
7251
|
+
'logged_in',
|
|
7252
|
+
'login',
|
|
7253
|
+
]);
|
|
7254
|
+
|
|
7255
|
+
function validateFunctionRuntimeInvoke(label, item, errors, warnings) {
|
|
7256
|
+
const definition = item.definitionJson || {};
|
|
7257
|
+
const runtimeInvoke = item.runtimeInvoke || definition.runtimeInvoke;
|
|
7258
|
+
if (!runtimeInvoke) return;
|
|
7259
|
+
if (typeof runtimeInvoke !== 'object' || Array.isArray(runtimeInvoke)) {
|
|
7260
|
+
errors.push(`${label}: runtimeInvoke 必须是对象`);
|
|
7261
|
+
return;
|
|
7262
|
+
}
|
|
7263
|
+
if (
|
|
7264
|
+
runtimeInvoke.enabled !== undefined &&
|
|
7265
|
+
typeof runtimeInvoke.enabled !== 'boolean'
|
|
7266
|
+
) {
|
|
7267
|
+
errors.push(`${label}: runtimeInvoke.enabled 必须是 boolean`);
|
|
7268
|
+
}
|
|
7269
|
+
|
|
7270
|
+
const roleCodeValues = [
|
|
7271
|
+
runtimeInvoke.roleCodes,
|
|
7272
|
+
runtimeInvoke.roles,
|
|
7273
|
+
runtimeInvoke.allowedRoleCodes,
|
|
7274
|
+
definition.invokeRoleCodes,
|
|
7275
|
+
];
|
|
7276
|
+
let hasRoleGrant = false;
|
|
7277
|
+
for (const value of roleCodeValues) {
|
|
7278
|
+
if (value === undefined) continue;
|
|
7279
|
+
if (!Array.isArray(value)) {
|
|
7280
|
+
errors.push(`${label}: runtimeInvoke roleCodes/roles/allowedRoleCodes 必须是数组`);
|
|
7281
|
+
continue;
|
|
7282
|
+
}
|
|
7283
|
+
if (value.length > 0) hasRoleGrant = true;
|
|
7284
|
+
for (const roleCode of value) {
|
|
7285
|
+
const normalized = String(roleCode || '').trim();
|
|
7286
|
+
if (!normalized) continue;
|
|
7287
|
+
if (FUNCTION_RUNTIME_INVOKE_MAGIC_ROLE_CODES.has(normalized.toLowerCase())) {
|
|
7288
|
+
errors.push(
|
|
7289
|
+
`${label}: runtimeInvoke.roleCodes 不支持魔法值 "${normalized}";请使用 audience.type`
|
|
7290
|
+
);
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
}
|
|
7294
|
+
|
|
7295
|
+
const audiences = [
|
|
7296
|
+
runtimeInvoke.audience,
|
|
7297
|
+
runtimeInvoke.audiences,
|
|
7298
|
+
definition.invokeAudience,
|
|
7299
|
+
definition.invokeAudiences,
|
|
7300
|
+
].filter(value => value !== undefined);
|
|
7301
|
+
for (const audience of audiences) {
|
|
7302
|
+
validateFunctionRuntimeInvokeAudience(label, audience, errors);
|
|
7303
|
+
}
|
|
7304
|
+
|
|
7305
|
+
const hasAudience =
|
|
7306
|
+
audiences.length > 0 ||
|
|
7307
|
+
runtimeInvoke.allowAuthenticated === true ||
|
|
7308
|
+
runtimeInvoke.authenticated === true ||
|
|
7309
|
+
runtimeInvoke.allowLoggedIn === true ||
|
|
7310
|
+
runtimeInvoke.scopePolicyCode ||
|
|
7311
|
+
runtimeInvoke.policyCode ||
|
|
7312
|
+
runtimeInvoke.dataScopePolicyCode ||
|
|
7313
|
+
runtimeInvoke.pagePermissionGroupCodes ||
|
|
7314
|
+
runtimeInvoke.pagePermissionGroups ||
|
|
7315
|
+
runtimeInvoke.pageGroups;
|
|
7316
|
+
|
|
7317
|
+
if (!hasAudience && !hasRoleGrant) {
|
|
7318
|
+
warnings.push(
|
|
7319
|
+
`${label}: runtimeInvoke 未声明 audience 或有效 roleCodes,页面运行时调用仍会要求 app:automation:manage`
|
|
7320
|
+
);
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7323
|
+
|
|
7324
|
+
function validateFunctionRuntimeInvokeAudience(label, value, errors) {
|
|
7325
|
+
if (Array.isArray(value)) {
|
|
7326
|
+
value.forEach(item => validateFunctionRuntimeInvokeAudience(label, item, errors));
|
|
7327
|
+
return;
|
|
7328
|
+
}
|
|
7329
|
+
if (typeof value === 'string') {
|
|
7330
|
+
const type = normalizeFunctionRuntimeInvokeAudienceType(value);
|
|
7331
|
+
if (!FUNCTION_RUNTIME_INVOKE_AUDIENCE_TYPES.has(type)) {
|
|
7332
|
+
errors.push(`${label}: 不支持的 runtimeInvoke.audience 类型 "${value}"`);
|
|
7333
|
+
}
|
|
7334
|
+
return;
|
|
7335
|
+
}
|
|
7336
|
+
if (!value || typeof value !== 'object') {
|
|
7337
|
+
errors.push(`${label}: runtimeInvoke.audience 必须是字符串、对象或数组`);
|
|
7338
|
+
return;
|
|
7339
|
+
}
|
|
7340
|
+
const type = normalizeFunctionRuntimeInvokeAudienceType(value.type);
|
|
7341
|
+
if (!FUNCTION_RUNTIME_INVOKE_AUDIENCE_TYPES.has(type)) {
|
|
7342
|
+
errors.push(`${label}: 不支持的 runtimeInvoke.audience.type "${value.type || ''}"`);
|
|
7343
|
+
return;
|
|
7344
|
+
}
|
|
7345
|
+
if (type === 'app_roles') {
|
|
7346
|
+
const roleCodes = normalizePermissionCodeArray(value.roleCodes || value.roles);
|
|
7347
|
+
if (!roleCodes.length) {
|
|
7348
|
+
errors.push(`${label}: audience.type=app_roles 时必须声明 roleCodes`);
|
|
7349
|
+
}
|
|
7350
|
+
for (const roleCode of roleCodes) {
|
|
7351
|
+
if (FUNCTION_RUNTIME_INVOKE_MAGIC_ROLE_CODES.has(roleCode.toLowerCase())) {
|
|
7352
|
+
errors.push(
|
|
7353
|
+
`${label}: audience.roleCodes 不支持魔法值 "${roleCode}";请改用 authenticated/page_permission_group/scope_policy`
|
|
7354
|
+
);
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
if (type === 'scope_policy') {
|
|
7359
|
+
const policyCode = String(
|
|
7360
|
+
value.policyCode || value.scopePolicyCode || value.dataScopePolicyCode || ''
|
|
7361
|
+
).trim();
|
|
7362
|
+
if (!policyCode) {
|
|
7363
|
+
errors.push(`${label}: audience.type=scope_policy 时必须声明 policyCode`);
|
|
7227
7364
|
}
|
|
7228
7365
|
}
|
|
7229
7366
|
}
|
|
7230
7367
|
|
|
7368
|
+
function normalizeFunctionRuntimeInvokeAudienceType(value) {
|
|
7369
|
+
const type = String(value || '')
|
|
7370
|
+
.trim()
|
|
7371
|
+
.replace(/[-\s]+/g, '_')
|
|
7372
|
+
.toLowerCase();
|
|
7373
|
+
if (['logged_in', 'login', 'all_authenticated'].includes(type)) {
|
|
7374
|
+
return 'authenticated';
|
|
7375
|
+
}
|
|
7376
|
+
if (['app_role', 'roles', 'role_codes'].includes(type)) {
|
|
7377
|
+
return 'app_roles';
|
|
7378
|
+
}
|
|
7379
|
+
if (['page_permission_groups', 'page_group', 'page_groups'].includes(type)) {
|
|
7380
|
+
return 'page_permission_group';
|
|
7381
|
+
}
|
|
7382
|
+
if (['data_scope_policy', 'business_scope_policy'].includes(type)) {
|
|
7383
|
+
return 'scope_policy';
|
|
7384
|
+
}
|
|
7385
|
+
return type;
|
|
7386
|
+
}
|
|
7387
|
+
|
|
7388
|
+
function validateFormRuntimeWriteSettings(label, item, errors) {
|
|
7389
|
+
const runtimeWrite = item.runtimeWrite || item.settings?.runtimeWrite;
|
|
7390
|
+
if (runtimeWrite === undefined) return;
|
|
7391
|
+
if (!runtimeWrite || typeof runtimeWrite !== 'object' || Array.isArray(runtimeWrite)) {
|
|
7392
|
+
errors.push(`${label}: runtimeWrite 必须是对象`);
|
|
7393
|
+
return;
|
|
7394
|
+
}
|
|
7395
|
+
const mode = String(runtimeWrite.mode || '').trim();
|
|
7396
|
+
if (!['permission_group', 'function_only', 'public_form'].includes(mode)) {
|
|
7397
|
+
errors.push(
|
|
7398
|
+
`${label}: runtimeWrite.mode 只能是 permission_group、function_only 或 public_form`
|
|
7399
|
+
);
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
|
|
7231
7403
|
function validateScopePermissionResources(manifest, errors, warnings) {
|
|
7232
7404
|
const policyCodes = new Set(
|
|
7233
7405
|
(manifest.dataScopePolicies || [])
|
|
@@ -8507,12 +8679,16 @@ async function publishFormSettingsResources(config, target, settingsItems, resul
|
|
|
8507
8679
|
const code = item.code || item.formCode || item.formUuid;
|
|
8508
8680
|
const formUuid = resolveManifestFormUuid(target.bound, item, { fallbackToCode: true });
|
|
8509
8681
|
if (!formUuid) fail(`表单设置 ${code} 无法解析 formUuid`);
|
|
8510
|
-
if (item.settings !== undefined) {
|
|
8682
|
+
if (item.settings !== undefined || item.runtimeWrite !== undefined) {
|
|
8683
|
+
const settings = {
|
|
8684
|
+
...(item.settings || {}),
|
|
8685
|
+
...(item.runtimeWrite !== undefined ? { runtimeWrite: item.runtimeWrite } : {}),
|
|
8686
|
+
};
|
|
8511
8687
|
await requestWithAuth(
|
|
8512
8688
|
config,
|
|
8513
8689
|
target.profileName,
|
|
8514
8690
|
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/settings`,
|
|
8515
|
-
{ method: 'POST', body: { settings
|
|
8691
|
+
{ method: 'POST', body: { settings } }
|
|
8516
8692
|
);
|
|
8517
8693
|
}
|
|
8518
8694
|
if (item.indexes !== undefined) {
|
|
@@ -11551,6 +11727,9 @@ function normalizeFunctionDefinitionJson(functionItem, definitionJson) {
|
|
|
11551
11727
|
kind: definition.kind || 'app_function',
|
|
11552
11728
|
version: definition.version || 'function_v1',
|
|
11553
11729
|
...(functionCode ? { functionCode: String(functionCode) } : {}),
|
|
11730
|
+
...(functionItem.runtimeInvoke !== undefined && definition.runtimeInvoke === undefined
|
|
11731
|
+
? { runtimeInvoke: functionItem.runtimeInvoke }
|
|
11732
|
+
: {}),
|
|
11554
11733
|
runtimeMode: definition.runtimeMode || 'trusted_node',
|
|
11555
11734
|
sourceType: definition.sourceType || 'file_snapshot',
|
|
11556
11735
|
});
|
|
@@ -11613,7 +11792,11 @@ function publicAccessPolicyEquals(bound, desired, existing) {
|
|
|
11613
11792
|
String(existing.routeCode || '') === String(expected.routeCode || '') &&
|
|
11614
11793
|
String(existing.pathPattern || '') === String(expected.pathPattern || '') &&
|
|
11615
11794
|
stringSetEquals(existing.externalRoleCodes || [], expected.externalRoleCodes || []) &&
|
|
11616
|
-
jsonEqualsForPlan(
|
|
11795
|
+
jsonEqualsForPlan(
|
|
11796
|
+
normalizePublicAccessGrants(bound, existing.grantsJson || existing.grants || {}),
|
|
11797
|
+
expected.grants || {},
|
|
11798
|
+
desired.__dir
|
|
11799
|
+
) &&
|
|
11617
11800
|
jsonEqualsForPlan(existing.ticketConfigJson || existing.ticketConfig || {}, expected.ticketConfig || {}, desired.__dir) &&
|
|
11618
11801
|
jsonEqualsForPlan(existing.rateLimitJson || existing.rateLimit || {}, expected.rateLimit || {}, desired.__dir) &&
|
|
11619
11802
|
String(existing.expiresAt || '') === String(expected.expiresAt || '')
|
|
@@ -12315,12 +12498,35 @@ function getAllWorkspaceSourceFiles() {
|
|
|
12315
12498
|
return files.sort();
|
|
12316
12499
|
}
|
|
12317
12500
|
|
|
12501
|
+
function addCodeResourceTargetFromPath(targets, file, prefix, key) {
|
|
12502
|
+
if (!file.startsWith(prefix)) return false;
|
|
12503
|
+
const rest = file.slice(prefix.length);
|
|
12504
|
+
const first = rest.split('/').filter(Boolean)[0] || '';
|
|
12505
|
+
const code = first.replace(/\.json$/i, '');
|
|
12506
|
+
if (code) targets[key].add(code);
|
|
12507
|
+
return true;
|
|
12508
|
+
}
|
|
12509
|
+
|
|
12510
|
+
function isRuntimeSourceFile(file) {
|
|
12511
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|css|scss|sass|less|html|json|svg|png|jpe?g|gif|webp|avif|ico)$/i.test(file);
|
|
12512
|
+
}
|
|
12513
|
+
|
|
12318
12514
|
function classifyWorkspaceFiles(files, runtimeMode) {
|
|
12319
12515
|
const forms = new Set();
|
|
12320
12516
|
const pages = new Set();
|
|
12517
|
+
const functions = new Set();
|
|
12518
|
+
const automations = new Set();
|
|
12519
|
+
const workflows = new Set();
|
|
12520
|
+
const jsCodeNodes = new Set();
|
|
12321
12521
|
let resources = false;
|
|
12322
12522
|
let runtime = false;
|
|
12323
12523
|
const other = [];
|
|
12524
|
+
const codeResourceTargets = {
|
|
12525
|
+
functions,
|
|
12526
|
+
automations,
|
|
12527
|
+
workflows,
|
|
12528
|
+
jsCodeNodes,
|
|
12529
|
+
};
|
|
12324
12530
|
|
|
12325
12531
|
for (const file of files) {
|
|
12326
12532
|
const formMatch = file.match(/^src\/forms\/([^/]+)/);
|
|
@@ -12337,6 +12543,30 @@ function classifyWorkspaceFiles(files, runtimeMode) {
|
|
|
12337
12543
|
}
|
|
12338
12544
|
continue;
|
|
12339
12545
|
}
|
|
12546
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/functions/', 'functions')) {
|
|
12547
|
+
continue;
|
|
12548
|
+
}
|
|
12549
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/automations/', 'automations')) {
|
|
12550
|
+
continue;
|
|
12551
|
+
}
|
|
12552
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/workflows/', 'workflows')) {
|
|
12553
|
+
continue;
|
|
12554
|
+
}
|
|
12555
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/js-code-nodes/', 'jsCodeNodes')) {
|
|
12556
|
+
continue;
|
|
12557
|
+
}
|
|
12558
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/functions/', 'functions')) {
|
|
12559
|
+
continue;
|
|
12560
|
+
}
|
|
12561
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/automations/', 'automations')) {
|
|
12562
|
+
continue;
|
|
12563
|
+
}
|
|
12564
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/workflows/', 'workflows')) {
|
|
12565
|
+
continue;
|
|
12566
|
+
}
|
|
12567
|
+
if (addCodeResourceTargetFromPath(codeResourceTargets, file, 'src/resources/js-code-nodes/', 'jsCodeNodes')) {
|
|
12568
|
+
continue;
|
|
12569
|
+
}
|
|
12340
12570
|
if (file.startsWith('src/resources/')) {
|
|
12341
12571
|
resources = true;
|
|
12342
12572
|
continue;
|
|
@@ -12345,7 +12575,8 @@ function classifyWorkspaceFiles(files, runtimeMode) {
|
|
|
12345
12575
|
file === 'package.json' ||
|
|
12346
12576
|
file === 'vite.config.ts' ||
|
|
12347
12577
|
file === 'index.html' ||
|
|
12348
|
-
file.startsWith('src/')
|
|
12578
|
+
file.startsWith('src/runtime/') ||
|
|
12579
|
+
(file.startsWith('src/') && isRuntimeSourceFile(file))
|
|
12349
12580
|
) {
|
|
12350
12581
|
runtime = runtimeMode === 'react-spa' || runtime;
|
|
12351
12582
|
continue;
|
|
@@ -12356,6 +12587,10 @@ function classifyWorkspaceFiles(files, runtimeMode) {
|
|
|
12356
12587
|
return {
|
|
12357
12588
|
forms: Array.from(forms).sort(),
|
|
12358
12589
|
pages: Array.from(pages).sort(),
|
|
12590
|
+
functions: Array.from(functions).sort(),
|
|
12591
|
+
automations: Array.from(automations).sort(),
|
|
12592
|
+
workflows: Array.from(workflows).sort(),
|
|
12593
|
+
jsCodeNodes: Array.from(jsCodeNodes).sort(),
|
|
12359
12594
|
resources,
|
|
12360
12595
|
runtime,
|
|
12361
12596
|
other,
|
|
@@ -12443,11 +12678,17 @@ function scanWorkspaceReliabilityIssues(files) {
|
|
|
12443
12678
|
function buildWorkspaceReleaseCommands(targets, runtimeMode, profile) {
|
|
12444
12679
|
const profileArg = profile || '<profile>';
|
|
12445
12680
|
const commands = [];
|
|
12681
|
+
const hasResourceTargets =
|
|
12682
|
+
Boolean(targets.resources) ||
|
|
12683
|
+
(targets.functions || []).length > 0 ||
|
|
12684
|
+
(targets.automations || []).length > 0 ||
|
|
12685
|
+
(targets.workflows || []).length > 0 ||
|
|
12686
|
+
(targets.jsCodeNodes || []).length > 0;
|
|
12446
12687
|
if (runtimeMode === 'react-spa') {
|
|
12447
12688
|
for (const formCode of targets.forms) {
|
|
12448
12689
|
commands.push(`openxiangda workspace publish --profile ${profileArg} --form ${formCode}`);
|
|
12449
12690
|
}
|
|
12450
|
-
if (
|
|
12691
|
+
if (hasResourceTargets) {
|
|
12451
12692
|
commands.push(`openxiangda resource publish --profile ${profileArg}`);
|
|
12452
12693
|
}
|
|
12453
12694
|
if (targets.runtime) {
|
|
@@ -12463,10 +12704,10 @@ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile) {
|
|
|
12463
12704
|
if (onlyTargets.length > 0) {
|
|
12464
12705
|
commands.push(
|
|
12465
12706
|
`openxiangda workspace publish --profile ${profileArg} --only ${onlyTargets.join(',')}${
|
|
12466
|
-
|
|
12707
|
+
hasResourceTargets ? ' --resources' : ''
|
|
12467
12708
|
}`
|
|
12468
12709
|
);
|
|
12469
|
-
} else if (
|
|
12710
|
+
} else if (hasResourceTargets) {
|
|
12470
12711
|
commands.push(`openxiangda resource publish --profile ${profileArg}`);
|
|
12471
12712
|
}
|
|
12472
12713
|
return commands;
|
|
@@ -12556,6 +12797,10 @@ function printWorkspaceReleasePlan(plan) {
|
|
|
12556
12797
|
`compatibility: apiContracts=${plan.workspace.compatibility.apiContracts}, legacyFallbacks=${plan.workspace.compatibility.legacyFallbacks}`,
|
|
12557
12798
|
`forms: ${plan.targets.forms.length ? plan.targets.forms.join(', ') : '(none)'}`,
|
|
12558
12799
|
`pages: ${plan.targets.pages.length ? plan.targets.pages.join(', ') : '(none)'}`,
|
|
12800
|
+
`functions: ${(plan.targets.functions || []).length ? plan.targets.functions.join(', ') : '(none)'}`,
|
|
12801
|
+
`automations: ${(plan.targets.automations || []).length ? plan.targets.automations.join(', ') : '(none)'}`,
|
|
12802
|
+
`workflows: ${(plan.targets.workflows || []).length ? plan.targets.workflows.join(', ') : '(none)'}`,
|
|
12803
|
+
`jsCodeNodes: ${(plan.targets.jsCodeNodes || []).length ? plan.targets.jsCodeNodes.join(', ') : '(none)'}`,
|
|
12559
12804
|
`resources: ${plan.targets.resources ? 'yes' : 'no'}`,
|
|
12560
12805
|
`runtime deploy: ${plan.targets.runtime ? 'yes' : 'no'}`,
|
|
12561
12806
|
];
|
package/lib/design-gates.js
CHANGED
|
@@ -522,6 +522,9 @@ const RESOURCE_EXPLAINS = {
|
|
|
522
522
|
version: 'function_v1',
|
|
523
523
|
runtimeMode: 'trusted_node',
|
|
524
524
|
sourceType: 'inline',
|
|
525
|
+
runtimeInvoke: {
|
|
526
|
+
audience: { type: 'authenticated' },
|
|
527
|
+
},
|
|
525
528
|
code: 'module.exports = async () => ({ ok: true })',
|
|
526
529
|
},
|
|
527
530
|
status: 'active',
|
package/lib/sdd.js
CHANGED
|
@@ -10,6 +10,7 @@ const RELEASE_FILE = 'release.json';
|
|
|
10
10
|
const BYPASS_LOG_FILE = path.join('changes', 'bypass-log.md');
|
|
11
11
|
const SDD_SCHEMA_VERSION = 'openxiangda-sdd-v2';
|
|
12
12
|
const LEGACY_SDD_SCHEMA_VERSION = 'openxiangda-sdd-v1';
|
|
13
|
+
const CODE_RESOURCE_TARGETS = ['functions', 'automations', 'workflows', 'jsCodeNodes'];
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SPEC_CONTENT = `# Application Specification
|
|
15
16
|
|
|
@@ -310,6 +311,17 @@ function normalizeAffected(input = {}) {
|
|
|
310
311
|
};
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
function extractCodeFromPath(file, prefix) {
|
|
315
|
+
if (!file.startsWith(prefix)) return null;
|
|
316
|
+
const rest = file.slice(prefix.length);
|
|
317
|
+
const first = rest.split('/').filter(Boolean)[0] || '';
|
|
318
|
+
return first.replace(/\.json$/i, '') || null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function coversCodeTarget(normalized, key, code) {
|
|
322
|
+
return Boolean(normalized.resources || normalized.backend || includesAll(normalized[key], [code]));
|
|
323
|
+
}
|
|
324
|
+
|
|
313
325
|
function createChangeMetadata(changeId, options = {}) {
|
|
314
326
|
return {
|
|
315
327
|
schemaVersion: SDD_SCHEMA_VERSION,
|
|
@@ -501,6 +513,10 @@ function createReleaseManifest(meta) {
|
|
|
501
513
|
targets: {
|
|
502
514
|
forms: meta.affected.forms,
|
|
503
515
|
pages: meta.affected.pages,
|
|
516
|
+
functions: meta.affected.functions,
|
|
517
|
+
automations: meta.affected.automations,
|
|
518
|
+
workflows: meta.affected.workflows,
|
|
519
|
+
jsCodeNodes: meta.affected.jsCodeNodes,
|
|
504
520
|
resources: meta.affected.resources,
|
|
505
521
|
runtime: meta.affected.runtime,
|
|
506
522
|
other: [],
|
|
@@ -613,6 +629,7 @@ function hasHighRiskTargets(targets = {}) {
|
|
|
613
629
|
return (
|
|
614
630
|
(targets.forms || []).length > 0 ||
|
|
615
631
|
(targets.pages || []).length > 0 ||
|
|
632
|
+
CODE_RESOURCE_TARGETS.some(key => (targets[key] || []).length > 0) ||
|
|
616
633
|
Boolean(targets.resources) ||
|
|
617
634
|
Boolean(targets.runtime)
|
|
618
635
|
);
|
|
@@ -647,6 +664,12 @@ function affectedCoversTargets(affected, targets = {}) {
|
|
|
647
664
|
if (!includesAll(normalized.pages, targets.pages || [])) {
|
|
648
665
|
missing.push(`pages:${(targets.pages || []).filter(item => !normalized.pages.includes(item)).join(',')}`);
|
|
649
666
|
}
|
|
667
|
+
for (const key of CODE_RESOURCE_TARGETS) {
|
|
668
|
+
const required = targets[key] || [];
|
|
669
|
+
if (required.length === 0) continue;
|
|
670
|
+
if (normalized.resources || normalized.backend || includesAll(normalized[key], required)) continue;
|
|
671
|
+
missing.push(`${key}:${required.filter(item => !normalized[key].includes(item)).join(',')}`);
|
|
672
|
+
}
|
|
650
673
|
if (targets.resources && !normalized.resources && !normalized.backend) missing.push('resources');
|
|
651
674
|
if (targets.runtime && !normalized.runtime) missing.push('runtime');
|
|
652
675
|
return {
|
|
@@ -668,6 +691,18 @@ function affectedCoversHighRiskFiles(affected, files = []) {
|
|
|
668
691
|
} else if (file.startsWith('src/pages/')) {
|
|
669
692
|
const pageCode = file.split('/')[2];
|
|
670
693
|
if (!normalized.runtime && !includesAll(normalized.pages, [pageCode])) missing.push(file);
|
|
694
|
+
} else if (file.startsWith('src/resources/functions/')) {
|
|
695
|
+
const code = extractCodeFromPath(file, 'src/resources/functions/');
|
|
696
|
+
if (code && !coversCodeTarget(normalized, 'functions', code)) missing.push(file);
|
|
697
|
+
} else if (file.startsWith('src/resources/automations/')) {
|
|
698
|
+
const code = extractCodeFromPath(file, 'src/resources/automations/');
|
|
699
|
+
if (code && !coversCodeTarget(normalized, 'automations', code)) missing.push(file);
|
|
700
|
+
} else if (file.startsWith('src/resources/workflows/')) {
|
|
701
|
+
const code = extractCodeFromPath(file, 'src/resources/workflows/');
|
|
702
|
+
if (code && !coversCodeTarget(normalized, 'workflows', code)) missing.push(file);
|
|
703
|
+
} else if (file.startsWith('src/resources/js-code-nodes/')) {
|
|
704
|
+
const code = extractCodeFromPath(file, 'src/resources/js-code-nodes/');
|
|
705
|
+
if (code && !coversCodeTarget(normalized, 'jsCodeNodes', code)) missing.push(file);
|
|
671
706
|
} else if (file.startsWith('src/resources/') && !normalized.resources) {
|
|
672
707
|
missing.push(file);
|
|
673
708
|
} else if (file.startsWith('src/functions/')) {
|
|
@@ -1116,8 +1151,8 @@ function validateDeltaSpecs(changeDir) {
|
|
|
1116
1151
|
};
|
|
1117
1152
|
}
|
|
1118
1153
|
|
|
1119
|
-
function
|
|
1120
|
-
|
|
1154
|
+
function buildUpdatedReleaseManifest(loaded, releasePlan) {
|
|
1155
|
+
return {
|
|
1121
1156
|
schemaVersion: SDD_SCHEMA_VERSION,
|
|
1122
1157
|
changeId: loaded.changeId,
|
|
1123
1158
|
...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
|
|
@@ -1126,6 +1161,10 @@ function updateReleaseManifest(loaded, releasePlan) {
|
|
|
1126
1161
|
commands: releasePlan?.commands || loaded.release?.commands || [],
|
|
1127
1162
|
verifiedAt: nowIso(),
|
|
1128
1163
|
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function updateReleaseManifest(loaded, releasePlan) {
|
|
1167
|
+
const release = buildUpdatedReleaseManifest(loaded, releasePlan);
|
|
1129
1168
|
writeJsonFile(loaded.releaseFile, release);
|
|
1130
1169
|
return release;
|
|
1131
1170
|
}
|
|
@@ -1210,11 +1249,12 @@ function verifySddChange(options = {}) {
|
|
|
1210
1249
|
}
|
|
1211
1250
|
const release =
|
|
1212
1251
|
options.writeMetadata && options.releasePlan && loaded.release && !loaded.release.__invalid
|
|
1213
|
-
?
|
|
1252
|
+
? buildUpdatedReleaseManifest(loaded, options.releasePlan)
|
|
1214
1253
|
: loaded.release;
|
|
1215
1254
|
const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
|
|
1216
1255
|
errors.push(...releaseValidation.errors);
|
|
1217
1256
|
warnings.push(...releaseValidation.warnings);
|
|
1257
|
+
const passed = errors.length === 0;
|
|
1218
1258
|
const verifiedMeta = {
|
|
1219
1259
|
...loaded.meta,
|
|
1220
1260
|
release: {
|
|
@@ -1224,7 +1264,14 @@ function verifySddChange(options = {}) {
|
|
|
1224
1264
|
},
|
|
1225
1265
|
updatedAt: nowIso(),
|
|
1226
1266
|
};
|
|
1227
|
-
|
|
1267
|
+
let metadataWritten = false;
|
|
1268
|
+
if (options.writeMetadata && passed) {
|
|
1269
|
+
if (options.releasePlan && loaded.release && !loaded.release.__invalid) {
|
|
1270
|
+
updateReleaseManifest(loaded, options.releasePlan);
|
|
1271
|
+
}
|
|
1272
|
+
writeJsonFile(loaded.metaFile, verifiedMeta);
|
|
1273
|
+
metadataWritten = true;
|
|
1274
|
+
}
|
|
1228
1275
|
return {
|
|
1229
1276
|
schemaVersion: loaded.meta.schemaVersion || loaded.governance.schemaVersion || LEGACY_SDD_SCHEMA_VERSION,
|
|
1230
1277
|
change: verifiedMeta,
|
|
@@ -1234,7 +1281,8 @@ function verifySddChange(options = {}) {
|
|
|
1234
1281
|
errors,
|
|
1235
1282
|
warnings,
|
|
1236
1283
|
blockingErrors: errors,
|
|
1237
|
-
passed
|
|
1284
|
+
passed,
|
|
1285
|
+
metadataWritten,
|
|
1238
1286
|
targets,
|
|
1239
1287
|
highRiskFiles,
|
|
1240
1288
|
};
|
|
@@ -162,7 +162,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
162
162
|
- 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.
|
|
163
163
|
- 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.
|
|
164
164
|
- 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.
|
|
165
|
-
- 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
|
|
165
|
+
- 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. Direct runtime invocation defaults to app automation management permission; ordinary page callers must use `definitionJson.runtimeInvoke.audience` (`authenticated`, `page_permission_group`, `app_roles`, or `scope_policy`). `roleCodes` remains only for current app-role grants; never use `"*"` or `"all-app-roles"` there. Current App Function MVP exposes controlled helpers such as `ctx.resources`, `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, `ctx.organization`, and `ctx.platform.api`; it does not expose raw SQL or Redis. Runtime functions also receive trusted role context on `ctx.operator.roleCodes`, `ctx.operator.currentRoleCode`, `ctx.operator.hasFullAccess`, `ctx.currentUser`, and `ctx.permissions`; use these for server-side business authorization. Do not trust role codes sent in page input for sensitive actions. App Function form writes are trusted backend writes governed by declared `resources.forms` and function invocation authorization; do not grant normal users raw submit permission to internal forms just to let `ctx.form.createOne/updateOne/updateById` run. For internal function-only forms, publish form settings with `runtimeWrite.mode="function_only"` to close raw write endpoints.
|
|
166
166
|
- For app-managed platform departments/accounts, use `sdk.organization.departments.*`, `sdk.organization.accounts.*`, `ctx.organization.departments.*`, or `ctx.organization.accounts.*`. The real current user/audit actor must hold `app:organization:manage` on the app; runtime service principals do not bypass this permission. Do not call legacy `/user` or `/department` write endpoints from apps. Do not include `password` in account updates; reset target accounts with `resetPassword`, and change the current user's password with `changeMyPassword({ oldPassword, newPassword })`.
|
|
167
167
|
- For account, role, data-scope, organization-account, RBAC, or query-param authorization designs, read `references/permission-design-patterns.md` and choose a permission mode before writing forms/pages/resources. Formal backends should use platform roles, page permission groups, form permission groups, public-access grants, and App Function role/scope checks; frontend hiding is display only.
|
|
168
168
|
- When a role such as 校区管理员 will create app roles, assign role members, grant role permissions, maintain permission groups, or manage organization accounts, its role manifest must declare the needed `apiPermissionCodes` such as `app:role:manage`, `app:page-permission-group:manage`, `app:form-permission-group:manage`, and `app:organization:manage`. If that administrator creates another administrator role, the new role must also declare its own `apiPermissionCodes`; permissions are not inherited from the creator.
|
|
@@ -32,7 +32,7 @@ Notification manifests live under `src/resources/notifications/` and contain `te
|
|
|
32
32
|
|
|
33
33
|
Data view manifests live under `src/resources/data-views/` and define read-only joined or aggregate query resources. Use `storageMode: "materialized"` for refreshed lists/reports where lag is acceptable, and `storageMode: "live"` for bounded real-time query shapes. Use row views with `sdk.dataView.query` and aggregate views with `sdk.dataView.stats`. Before generating one, confirm freshness tolerance, query bounds, and indexes for materialized filters/sort fields/dimensions/date buckets. Do not use them for single-form CRUD, simple linkedForm selects, writes, write-back, or ad-hoc BI. See `data-views.md` before generating one.
|
|
34
34
|
|
|
35
|
-
App Function manifests live under `src/resources/functions/`, with source in `src/functions/<functionCode>/index.ts`. Use them for reusable server-side logic that pages, automations, and workflows can share through `sdk.function.invoke` or `function_call` nodes. App Functions expose controlled runtime helpers such as `ctx.resources`, `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, `ctx.organization`, and `ctx.platform.api`; they do not expose raw SQL or Redis in the current MVP. Runtime page invocations also expose trusted role context on `ctx.operator.roleCodes`, `ctx.operator.currentRoleCode`, `ctx.operator.hasFullAccess`, `ctx.currentUser`, and `ctx.permissions`; use that context for server-side authorization, and never trust page-submitted role codes for sensitive actions. App Function form writes are trusted backend writes governed by declared `resources.forms` and function invocation authorization, not by direct page-user submit access to the target form. App Function organization writes require `app:organization:manage` on the real operator/audit actor. Use JS_CODE V2 only for node-local workflow/automation scripts.
|
|
35
|
+
App Function manifests live under `src/resources/functions/`, with source in `src/functions/<functionCode>/index.ts`. Use them for reusable server-side logic that pages, automations, and workflows can share through `sdk.function.invoke` or `function_call` nodes. App Functions expose controlled runtime helpers such as `ctx.resources`, `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, `ctx.organization`, and `ctx.platform.api`; they do not expose raw SQL or Redis in the current MVP. Runtime page invocations also expose trusted role context on `ctx.operator.roleCodes`, `ctx.operator.currentRoleCode`, `ctx.operator.hasFullAccess`, `ctx.currentUser`, and `ctx.permissions`; use that context for server-side authorization, and never trust page-submitted role codes for sensitive actions. Page-call grants should use `definitionJson.runtimeInvoke.audience` (`authenticated`, `page_permission_group`, `app_roles`, or `scope_policy`); `roleCodes` is only for current app-role grants and does not support `"*"` or `"all-app-roles"`. App Function form writes are trusted backend writes governed by declared `resources.forms` and function invocation authorization, not by direct page-user submit access to the target form. For internal function-only forms, publish form settings with `runtimeWrite.mode="function_only"` so raw write endpoints are closed. App Function organization writes require `app:organization:manage` on the real operator/audit actor. Use JS_CODE V2 only for node-local workflow/automation scripts.
|
|
36
36
|
|
|
37
37
|
Auth manifests live under `src/resources/auth/`. Use them to enable app-level login methods and bind phone-code/CAS/custom providers to App Functions. Auth provider functions are called only by the platform auth flow. They validate external credentials and return identity assertions such as `phone`, `email`, `externalId`, or `unionId`; they must not issue tokens, set cookies, or mutate platform user/binding tables.
|
|
38
38
|
|
|
@@ -115,7 +115,7 @@ App Function source should prefer `export default async function(ctx, input) {}`
|
|
|
115
115
|
The second argument is the `input` passed by `sdk.function.invoke`, and the
|
|
116
116
|
same value is also available as `ctx.input` for compatibility.
|
|
117
117
|
|
|
118
|
-
Use App Functions when the logic must run server-side and be reusable by pages, automations, or workflows. Runtime invocation requires app automation management permission by default. To let ordinary app users call a function from a page, declare
|
|
118
|
+
Use App Functions when the logic must run server-side and be reusable by pages, automations, or workflows. Runtime invocation requires app automation management permission by default. To let ordinary app users call a function from a page, declare `definitionJson.runtimeInvoke.audience` with `authenticated`, `page_permission_group`, `app_roles`, or `scope_policy`; use `roleCodes` only for current app-role grants. Do not use `"*"` or `"all-app-roles"` as role codes. Inside the function, authorize sensitive actions with trusted runtime context such as `ctx.operator.roleCodes`, `ctx.operator.currentRoleCode`, `ctx.operator.hasFullAccess`, `ctx.currentUser`, or `ctx.permissions`; do not trust role codes sent in the page input. When the function writes forms with `ctx.form.createOne/updateOne/updateById`, treat the write as a trusted backend operation over declared `resources.forms`; do not open direct submit permission on internal forms just for that page action.
|
|
119
119
|
|
|
120
120
|
Organization management:
|
|
121
121
|
|
|
@@ -571,7 +571,10 @@ JS_CODE 调用:`ctx.notification.sendByType({ ... })`。允许的 channels:`
|
|
|
571
571
|
"runtimeMode": "trusted_node",
|
|
572
572
|
"timeout": 30000,
|
|
573
573
|
"runtimeInvoke": {
|
|
574
|
-
"
|
|
574
|
+
"audience": {
|
|
575
|
+
"type": "page_permission_group",
|
|
576
|
+
"resourceCodes": ["member_portal_pages"]
|
|
577
|
+
}
|
|
575
578
|
},
|
|
576
579
|
"sourceFile": {
|
|
577
580
|
"localPath": "src/functions/reservation_reminder_summary/index.ts"
|
|
@@ -643,7 +646,7 @@ const result = await sdk.function.invoke("reservation_reminder_summary", {
|
|
|
643
646
|
}
|
|
644
647
|
```
|
|
645
648
|
|
|
646
|
-
适用边界:可复用后端业务逻辑、跨页面/自动化/流程共享的查询编排、连接器调用、通知编排、受控平台 API 调用。App Function 支持 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.api` 等受控 helper,当前 MVP 不暴露原始 SQL/Redis。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,权限边界是 function manifest 的 `resources.forms` 和函数调用授权,不是页面用户对目标表单的直接提交权限;内部业务表单不要为了函数写入而开放普通用户原始 submit
|
|
649
|
+
适用边界:可复用后端业务逻辑、跨页面/自动化/流程共享的查询编排、连接器调用、通知编排、受控平台 API 调用。App Function 支持 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.api` 等受控 helper,当前 MVP 不暴露原始 SQL/Redis。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,权限边界是 function manifest 的 `resources.forms` 和函数调用授权,不是页面用户对目标表单的直接提交权限;内部业务表单不要为了函数写入而开放普通用户原始 submit。运行时接口默认需要应用自动化管理权限;普通用户页面要调用时,用 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles` 或 `scope_policy`,不要把 `"*"`、`"all-app-roles"` 写进 `roleCodes`。自动化/流程内部调用走服务端上下文,不受页面运行时授权限制。若表单只能由函数/流程写入,在 `src/resources/settings/forms/<formCode>.json` 设置 `runtimeWrite.mode="function_only"` 关闭原始写入接口。
|
|
647
650
|
|
|
648
651
|
## 5. Workflow — `src/resources/workflows/<code>/workflow.json`(manifest)+ `src/workflows/<code>/workflow.ts`(代码优先)
|
|
649
652
|
|
|
@@ -831,7 +834,10 @@ permission codes.
|
|
|
831
834
|
"code": "customer",
|
|
832
835
|
"settings": {
|
|
833
836
|
"submitButtonText": "提交",
|
|
834
|
-
"successMessage": "提交成功"
|
|
837
|
+
"successMessage": "提交成功",
|
|
838
|
+
"runtimeWrite": {
|
|
839
|
+
"mode": "permission_group"
|
|
840
|
+
}
|
|
835
841
|
},
|
|
836
842
|
"indexes": [
|
|
837
843
|
{ "fields": ["customerCode"], "unique": true },
|
|
@@ -841,6 +847,8 @@ permission codes.
|
|
|
841
847
|
}
|
|
842
848
|
```
|
|
843
849
|
|
|
850
|
+
`runtimeWrite.mode` 可选 `permission_group`、`function_only`、`public_form`。内部业务表单需要强制走 App Function / Workflow 时使用 `function_only`;这会关闭页面原始写入接口,但不影响 `ctx.form.createOne/updateOne/updateById` 等受信后端写入。
|
|
851
|
+
|
|
844
852
|
`publicAccess` 表单 setting 只用于旧 `sy-lowcode-view` 兼容。新 React SPA 公开页面必须使用 `src/resources/routes/` + `src/resources/public-access/` + `OpenXiangdaProvider` + `OpenXiangdaPageProvider` + `PublicAccessGate`。
|
|
845
853
|
|
|
846
854
|
## 12. Menu — `src/resources/menus/<code>.json`
|
|
@@ -103,7 +103,7 @@ Use `workflow pull` to inspect the live definition. Use `workflow list --json` a
|
|
|
103
103
|
|
|
104
104
|
JS_CODE is the backend execution escape hatch for workflow and automation. Use it when the logic must run on the server after a backend trigger, such as a fixed cron schedule, a form date-field schedule, a form submit/update/delete/field-change event, or a workflow approval/process event. It is appropriate for cross-form data queries, create/update/batch update operations, process termination, platform API calls, external HTTP calls, and complex orchestration that the frontend cannot handle reliably.
|
|
105
105
|
|
|
106
|
-
App Function is the reusable backend execution model. Use it when the logic should be called by custom pages, multiple automations, workflows, or the runtime API. Source lives in `src/functions/<functionCode>/index.ts`; manifest lives in `src/resources/functions/<functionCode>.json`. Call it from pages with `sdk.function.invoke(code, { input })`, from graph definitions with `function_call`, or from the runtime endpoint `/:appType/v1/functions/:code/invoke.json
|
|
106
|
+
App Function is the reusable backend execution model. Use it when the logic should be called by custom pages, multiple automations, workflows, or the runtime API. Source lives in `src/functions/<functionCode>/index.ts`; manifest lives in `src/resources/functions/<functionCode>.json`. Call it from pages with `sdk.function.invoke(code, { input })`, from graph definitions with `function_call`, or from the runtime endpoint `/:appType/v1/functions/:code/invoke.json`. Direct runtime invocation defaults to app automation management permission; ordinary page callers must declare `definitionJson.runtimeInvoke.audience` (`authenticated`, `page_permission_group`, `app_roles`, or `scope_policy`). Prefer `export default async function(ctx, input) {}` for App Function source; the second argument is the invoke input and the same value is available as `ctx.input`. Current MVP exposes controlled helpers only and does not expose raw SQL or Redis. `ctx.form.createOne/updateOne/updateById` are trusted backend writes governed by declared `resources.forms` and function invocation authorization; keep internal forms closed to direct user submit unless the business explicitly needs raw form submission. For internal function-only forms, set form settings `runtimeWrite.mode="function_only"` instead of relying only on missing submit permission groups.
|
|
107
107
|
|
|
108
108
|
For new AI-authored automations, prefer code-first `automation_code_ts` resources instead of visual v3 graph definitions. Put the source in `src/automations/<resourceCode>/index.ts`, define `definition.code.json` with `kind: "automation_code_ts"`, and provide `preview.json` for read-only frontend display. Use `ctx.logger.debug/info/warn/error(message, data?)` at every important step; OpenXiangda can inspect logs with `automation executions`, `automation logs`, and `automation diagnose`.
|
|
109
109
|
|