openxiangda 1.0.84 → 1.0.86
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 +2 -0
- package/lib/cli.js +305 -10
- package/openxiangda-skills/SKILL.md +4 -1
- package/openxiangda-skills/references/architecture-design.md +38 -4
- package/openxiangda-skills/references/connector-resources.md +3 -0
- package/openxiangda-skills/references/pages/page-sdk.md +39 -0
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +81 -0
- package/package.json +2 -1
- package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.mts +328 -0
- package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.ts +328 -0
- package/packages/sdk/dist/runtime/index.cjs +4250 -3641
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +3845 -3219
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +645 -38
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +2 -162
- package/packages/sdk/dist/runtime/react.d.ts +2 -162
- package/packages/sdk/dist/runtime/react.mjs +667 -39
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/src/app/router.tsx +2 -1
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
|
|
|
178
178
|
|
|
179
179
|
后端业务逻辑优先声明为 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 中直接运行时接口要求调用者具备应用自动化管理权限,自动化/流程内部调用走服务端受控上下文。
|
|
180
180
|
|
|
181
|
+
应用登录能力通过 auth resource 和 runtime SDK 提供:登录配置放在 `src/resources/auth/<code>.json`,默认 React SPA 模板已包含 `/view/:appType/login`,自定义页面可使用 `createAuthClient({ appType, servicePrefix })` 或 `LoginPage` / `useAuth` from `openxiangda/runtime/react`。手机号验证码、CAS/SSO 或其他外部登录可以由 App Function provider 校验外部凭证,但 provider 只能返回 `phone` / `email` / `externalId` / `unionId` 等身份声明;平台后端按 auth resource 策略执行账号匹配、绑定、创建或拒绝,并由平台统一签发 token/cookie。默认注册策略是拒绝,开启自动注册或白名单注册前必须确认身份匹配键、默认角色、验证码 TTL/频率/失败次数、审计字段和错误文案策略。
|
|
182
|
+
|
|
181
183
|
常用数据视图命令:
|
|
182
184
|
|
|
183
185
|
```bash
|
package/lib/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const { builtinModules } = require('module');
|
|
5
6
|
const { spawnSync } = require('child_process');
|
|
6
7
|
const { pathToFileURL } = require('url');
|
|
7
8
|
const esbuild = require('esbuild');
|
|
@@ -3374,6 +3375,7 @@ function ensureResourceBuckets(bound) {
|
|
|
3374
3375
|
bound.resources.roles = bound.resources.roles || {};
|
|
3375
3376
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
3376
3377
|
bound.resources.dataViews = bound.resources.dataViews || {};
|
|
3378
|
+
bound.resources.authConfigs = bound.resources.authConfigs || {};
|
|
3377
3379
|
bound.resources.notifications = bound.resources.notifications || {};
|
|
3378
3380
|
bound.resources.notifications.templates = bound.resources.notifications.templates || {};
|
|
3379
3381
|
bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
|
|
@@ -3584,6 +3586,14 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
|
|
|
3584
3586
|
}, keys);
|
|
3585
3587
|
}
|
|
3586
3588
|
|
|
3589
|
+
function saveAuthConfigResource(target, authConfigCode, configId, extra = {}) {
|
|
3590
|
+
const keys = ['configId', 'status'];
|
|
3591
|
+
saveStateResource(target, 'authConfigs', authConfigCode, {
|
|
3592
|
+
...pickStateFields(extra, keys),
|
|
3593
|
+
configId,
|
|
3594
|
+
}, keys);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3587
3597
|
function saveRoleResource(target, roleCode, roleId) {
|
|
3588
3598
|
saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
|
|
3589
3599
|
}
|
|
@@ -3691,6 +3701,7 @@ const RESOURCE_SPECS = [
|
|
|
3691
3701
|
{ key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
|
|
3692
3702
|
{ key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
|
|
3693
3703
|
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
3704
|
+
{ key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
|
|
3694
3705
|
{
|
|
3695
3706
|
key: 'pagePermissionGroups',
|
|
3696
3707
|
dir: path.join('permissions', 'page-groups'),
|
|
@@ -3890,6 +3901,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3890
3901
|
);
|
|
3891
3902
|
const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
|
|
3892
3903
|
const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
|
|
3904
|
+
const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
|
|
3893
3905
|
const content = [
|
|
3894
3906
|
'/* eslint-disable */',
|
|
3895
3907
|
'// Generated by openxiangda resource typegen. Do not edit manually.',
|
|
@@ -3909,6 +3921,9 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3909
3921
|
`export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
|
|
3910
3922
|
'export type FunctionCode = typeof functionCodes[number]',
|
|
3911
3923
|
'',
|
|
3924
|
+
`export const authConfigCodes = ${JSON.stringify(authConfigCodes, null, 2)} as const`,
|
|
3925
|
+
'export type AuthConfigCode = typeof authConfigCodes[number]',
|
|
3926
|
+
'',
|
|
3912
3927
|
`export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
|
|
3913
3928
|
'',
|
|
3914
3929
|
`export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
|
|
@@ -3923,6 +3938,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3923
3938
|
pagePermissionGroups: pagePermissionGroupCodes.length,
|
|
3924
3939
|
dataViews: dataViewCodes.length,
|
|
3925
3940
|
functions: functionCodes.length,
|
|
3941
|
+
authConfigs: authConfigCodes.length,
|
|
3926
3942
|
};
|
|
3927
3943
|
}
|
|
3928
3944
|
|
|
@@ -4001,6 +4017,32 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
4001
4017
|
}
|
|
4002
4018
|
validateDataViewPerformance(label, definition, errors, warnings, storageMode);
|
|
4003
4019
|
}
|
|
4020
|
+
if (kind === 'authConfigs') {
|
|
4021
|
+
const config = item.configJson || item.config || item;
|
|
4022
|
+
if (!Array.isArray(config.methods)) {
|
|
4023
|
+
errors.push(`${label}: 缺少 methods`);
|
|
4024
|
+
} else {
|
|
4025
|
+
config.methods.forEach((method, index) => {
|
|
4026
|
+
if (!method?.type) errors.push(`${label}.methods[${index}]: 缺少 type`);
|
|
4027
|
+
if (
|
|
4028
|
+
method?.type === 'phone_code' &&
|
|
4029
|
+
method.enabled !== false &&
|
|
4030
|
+
!(
|
|
4031
|
+
method.provider?.functionCode ||
|
|
4032
|
+
method.providerFunctionCode ||
|
|
4033
|
+
config.providers?.phone_code?.functionCode ||
|
|
4034
|
+
config.providers?.phoneCode?.functionCode
|
|
4035
|
+
)
|
|
4036
|
+
) {
|
|
4037
|
+
errors.push(`${label}.methods[${index}]: phone_code 缺少 provider.functionCode`);
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
const registrationMode = String(config.registration?.mode || 'reject');
|
|
4042
|
+
if (registrationMode !== 'reject') {
|
|
4043
|
+
warnings.push(`${label}: 已开启非默认注册策略 ${registrationMode},请确认身份匹配键、默认角色和审计字段`);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4004
4046
|
if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
4005
4047
|
if (kind === 'formPermissionGroups') {
|
|
4006
4048
|
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
@@ -4402,6 +4444,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
4402
4444
|
addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
|
|
4403
4445
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
4404
4446
|
addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
|
|
4447
|
+
addPlanActions(actions, 'authConfig', manifest.authConfigs, existing.authConfigs, (item, current) => authConfigEquals(item, current));
|
|
4405
4448
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
4406
4449
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
4407
4450
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
@@ -4437,6 +4480,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
4437
4480
|
await publishNotificationResources(config, target, manifest.notifications || [], result);
|
|
4438
4481
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
4439
4482
|
await publishFunctionResources(config, target, manifest.functions || [], result);
|
|
4483
|
+
await publishAuthConfigResources(config, target, manifest.authConfigs || [], result);
|
|
4440
4484
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
4441
4485
|
await publishDataViewResources(config, target, manifest.dataViews || [], result);
|
|
4442
4486
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
@@ -4460,6 +4504,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4460
4504
|
notificationTypeConfigs: new Map(),
|
|
4461
4505
|
workflows: new Map(),
|
|
4462
4506
|
functions: new Map(),
|
|
4507
|
+
authConfigs: new Map(),
|
|
4463
4508
|
automations: new Map(),
|
|
4464
4509
|
dataViews: new Map(),
|
|
4465
4510
|
pagePermissionGroups: new Map(),
|
|
@@ -4565,6 +4610,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4565
4610
|
});
|
|
4566
4611
|
}
|
|
4567
4612
|
}
|
|
4613
|
+
if ((manifest.authConfigs || []).length > 0) {
|
|
4614
|
+
const data = await requestWithAuth(
|
|
4615
|
+
config,
|
|
4616
|
+
target.profileName,
|
|
4617
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
|
|
4618
|
+
page: 1,
|
|
4619
|
+
pageSize: 1000,
|
|
4620
|
+
})
|
|
4621
|
+
);
|
|
4622
|
+
indexByCode(maps.authConfigs, normalizeItems(data), item => item.code || item.resourceCode);
|
|
4623
|
+
for (const item of manifest.authConfigs || []) {
|
|
4624
|
+
const code = item.code || item.resourceCode;
|
|
4625
|
+
const existing = maps.authConfigs.get(code);
|
|
4626
|
+
if (!existing) continue;
|
|
4627
|
+
const detail = await requestOptionalWithAuth(
|
|
4628
|
+
config,
|
|
4629
|
+
target.profileName,
|
|
4630
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
|
|
4631
|
+
);
|
|
4632
|
+
maps.authConfigs.set(code, {
|
|
4633
|
+
...existing,
|
|
4634
|
+
...(detail || {}),
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4568
4638
|
if ((manifest.dataViews || []).length > 0) {
|
|
4569
4639
|
const data = await requestWithAuth(
|
|
4570
4640
|
config,
|
|
@@ -5227,6 +5297,38 @@ async function publishFunctionResources(config, target, functions, result) {
|
|
|
5227
5297
|
}
|
|
5228
5298
|
}
|
|
5229
5299
|
|
|
5300
|
+
async function publishAuthConfigResources(config, target, authConfigs, result) {
|
|
5301
|
+
for (const authItem of authConfigs) {
|
|
5302
|
+
const code = authItem.code || authItem.resourceCode || 'default';
|
|
5303
|
+
const existing = await findExistingAuthConfig(config, target, code);
|
|
5304
|
+
const body = normalizeAuthConfigManifest(authItem);
|
|
5305
|
+
const data = existing
|
|
5306
|
+
? await requestWithAuth(
|
|
5307
|
+
config,
|
|
5308
|
+
target.profileName,
|
|
5309
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
|
|
5310
|
+
{ method: 'PUT', body }
|
|
5311
|
+
)
|
|
5312
|
+
: await requestWithAuth(
|
|
5313
|
+
config,
|
|
5314
|
+
target.profileName,
|
|
5315
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`,
|
|
5316
|
+
{ method: 'POST', body }
|
|
5317
|
+
);
|
|
5318
|
+
if (data?.id) {
|
|
5319
|
+
saveAuthConfigResource(target, data.code || code, data.id, {
|
|
5320
|
+
status: data.status,
|
|
5321
|
+
});
|
|
5322
|
+
}
|
|
5323
|
+
result.published.push({
|
|
5324
|
+
kind: 'authConfig',
|
|
5325
|
+
code,
|
|
5326
|
+
action: existing ? 'update' : 'create',
|
|
5327
|
+
id: data?.id,
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
|
|
5230
5332
|
async function publishDataViewResources(config, target, dataViews, result) {
|
|
5231
5333
|
for (const dataViewItem of dataViews) {
|
|
5232
5334
|
const existing = await findExistingDataView(config, target, dataViewItem.code);
|
|
@@ -5272,6 +5374,18 @@ async function findExistingFunction(config, target, code) {
|
|
|
5272
5374
|
return null;
|
|
5273
5375
|
}
|
|
5274
5376
|
|
|
5377
|
+
async function findExistingAuthConfig(config, target, code) {
|
|
5378
|
+
const stateId = target.bound.resources?.authConfigs?.[code]?.configId;
|
|
5379
|
+
const byCode = await requestOptionalWithAuth(
|
|
5380
|
+
config,
|
|
5381
|
+
target.profileName,
|
|
5382
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
|
|
5383
|
+
);
|
|
5384
|
+
if (byCode?.id) return byCode;
|
|
5385
|
+
if (stateId) return { id: stateId, code };
|
|
5386
|
+
return null;
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5275
5389
|
async function findExistingDataView(config, target, code) {
|
|
5276
5390
|
const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
|
|
5277
5391
|
const byCode = await requestOptionalWithAuth(
|
|
@@ -5422,6 +5536,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
5422
5536
|
await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
|
|
5423
5537
|
await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
|
|
5424
5538
|
await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
|
|
5539
|
+
await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
|
|
5425
5540
|
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
5426
5541
|
await prunePagePermissionGroups(
|
|
5427
5542
|
config,
|
|
@@ -5617,6 +5732,29 @@ async function pruneFunctions(config, target, desired, result) {
|
|
|
5617
5732
|
}
|
|
5618
5733
|
}
|
|
5619
5734
|
|
|
5735
|
+
async function pruneAuthConfigs(config, target, desired, result) {
|
|
5736
|
+
const data = await requestWithAuth(
|
|
5737
|
+
config,
|
|
5738
|
+
target.profileName,
|
|
5739
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
|
|
5740
|
+
page: 1,
|
|
5741
|
+
pageSize: 1000,
|
|
5742
|
+
})
|
|
5743
|
+
);
|
|
5744
|
+
for (const item of normalizeItems(data)) {
|
|
5745
|
+
const code = item.code || item.resourceCode;
|
|
5746
|
+
if (!code || desired.has(code)) continue;
|
|
5747
|
+
await pruneOne(config, target, result, 'authConfig', code, item.id, async () => {
|
|
5748
|
+
await requestWithAuth(
|
|
5749
|
+
config,
|
|
5750
|
+
target.profileName,
|
|
5751
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
|
|
5752
|
+
{ method: 'DELETE' }
|
|
5753
|
+
);
|
|
5754
|
+
});
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
|
|
5620
5758
|
async function prunePagePermissionGroups(config, target, desired, result) {
|
|
5621
5759
|
const data = await requestWithAuth(
|
|
5622
5760
|
config,
|
|
@@ -5692,6 +5830,7 @@ function removeStateResource(target, kind, code) {
|
|
|
5692
5830
|
workflow: 'workflows',
|
|
5693
5831
|
automation: 'automations',
|
|
5694
5832
|
function: 'functions',
|
|
5833
|
+
authConfig: 'authConfigs',
|
|
5695
5834
|
dataView: 'dataViews',
|
|
5696
5835
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
5697
5836
|
formPermissionGroup: 'formPermissionGroups',
|
|
@@ -5890,6 +6029,26 @@ async function pullResources(config, target) {
|
|
|
5890
6029
|
written.push(path.relative(process.cwd(), filePath));
|
|
5891
6030
|
}
|
|
5892
6031
|
|
|
6032
|
+
const authConfigs = await requestWithAuth(
|
|
6033
|
+
config,
|
|
6034
|
+
target.profileName,
|
|
6035
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
|
|
6036
|
+
page: 1,
|
|
6037
|
+
pageSize: 1000,
|
|
6038
|
+
})
|
|
6039
|
+
);
|
|
6040
|
+
for (const item of normalizeItems(authConfigs)) {
|
|
6041
|
+
const code = item.code || item.resourceCode || item.id;
|
|
6042
|
+
const detail = await requestWithAuth(
|
|
6043
|
+
config,
|
|
6044
|
+
target.profileName,
|
|
6045
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
|
|
6046
|
+
);
|
|
6047
|
+
const filePath = path.join(baseDir, 'auth', `${code}.json`);
|
|
6048
|
+
writeResourceJsonFile(filePath, toPulledAuthConfig(detail));
|
|
6049
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
6050
|
+
}
|
|
6051
|
+
|
|
5893
6052
|
const dataViews = await requestWithAuth(
|
|
5894
6053
|
config,
|
|
5895
6054
|
target.profileName,
|
|
@@ -6105,6 +6264,16 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
|
|
|
6105
6264
|
});
|
|
6106
6265
|
}
|
|
6107
6266
|
|
|
6267
|
+
function toPulledAuthConfig(authConfig) {
|
|
6268
|
+
return stripUndefinedValues({
|
|
6269
|
+
code: authConfig.code || authConfig.resourceCode,
|
|
6270
|
+
name: authConfig.name,
|
|
6271
|
+
description: authConfig.description || '',
|
|
6272
|
+
status: authConfig.status || 'active',
|
|
6273
|
+
configJson: authConfig.configJson || authConfig.config || {},
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
6276
|
+
|
|
6108
6277
|
function rewriteDataViewDefinitionForManifest(definition, lookups) {
|
|
6109
6278
|
rewriteDataViewSourceForManifest(definition.base, lookups);
|
|
6110
6279
|
for (const join of definition.joins || []) {
|
|
@@ -6285,6 +6454,40 @@ function normalizeDataViewManifest(bound, dataView) {
|
|
|
6285
6454
|
});
|
|
6286
6455
|
}
|
|
6287
6456
|
|
|
6457
|
+
function normalizeAuthConfigManifest(authConfig) {
|
|
6458
|
+
const code = authConfig.code || authConfig.resourceCode || 'default';
|
|
6459
|
+
const configJson = clonePlainJson(
|
|
6460
|
+
authConfig.configJson || authConfig.config || withoutAuthConfigManifestMeta(authConfig)
|
|
6461
|
+
);
|
|
6462
|
+
return stripUndefinedValues({
|
|
6463
|
+
code,
|
|
6464
|
+
name: authConfig.name || configJson.name || code,
|
|
6465
|
+
description:
|
|
6466
|
+
authConfig.description !== undefined
|
|
6467
|
+
? authConfig.description
|
|
6468
|
+
: configJson.description || '',
|
|
6469
|
+
status: authConfig.status || configJson.status || 'active',
|
|
6470
|
+
configJson: {
|
|
6471
|
+
...configJson,
|
|
6472
|
+
methods: Array.isArray(configJson.methods) ? configJson.methods : [],
|
|
6473
|
+
registration: configJson.registration || { mode: 'reject' },
|
|
6474
|
+
binding: configJson.binding || { mode: 'auto' },
|
|
6475
|
+
},
|
|
6476
|
+
});
|
|
6477
|
+
}
|
|
6478
|
+
|
|
6479
|
+
function withoutAuthConfigManifestMeta(authConfig) {
|
|
6480
|
+
const next = withoutResourceMeta(authConfig);
|
|
6481
|
+
delete next.code;
|
|
6482
|
+
delete next.resourceCode;
|
|
6483
|
+
delete next.name;
|
|
6484
|
+
delete next.description;
|
|
6485
|
+
delete next.status;
|
|
6486
|
+
delete next.config;
|
|
6487
|
+
delete next.configJson;
|
|
6488
|
+
return next;
|
|
6489
|
+
}
|
|
6490
|
+
|
|
6288
6491
|
function withoutDataViewManifestMeta(dataView) {
|
|
6289
6492
|
const next = withoutResourceMeta(dataView);
|
|
6290
6493
|
delete next.permissionGroups;
|
|
@@ -6754,6 +6957,18 @@ function functionEquals(target, desired, existing) {
|
|
|
6754
6957
|
);
|
|
6755
6958
|
}
|
|
6756
6959
|
|
|
6960
|
+
function authConfigEquals(desired, existing) {
|
|
6961
|
+
if (!existing) return false;
|
|
6962
|
+
const expected = normalizeAuthConfigManifest(desired);
|
|
6963
|
+
return (
|
|
6964
|
+
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
6965
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
6966
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
6967
|
+
String(existing.status || 'active') === String(expected.status || 'active') &&
|
|
6968
|
+
jsonEqualsForPlan(existing.configJson || {}, expected.configJson || {}, desired.__dir)
|
|
6969
|
+
);
|
|
6970
|
+
}
|
|
6971
|
+
|
|
6757
6972
|
function dataViewEquals(bound, desired, existing) {
|
|
6758
6973
|
if (!existing) return false;
|
|
6759
6974
|
const expected = normalizeDataViewManifest(bound, desired);
|
|
@@ -6893,10 +7108,18 @@ function resolveJsCodeBundlePathForPlan(localPath) {
|
|
|
6893
7108
|
const extension = path.extname(localPath).toLowerCase();
|
|
6894
7109
|
if (extension && extension !== '.ts' && extension !== '.tsx') return null;
|
|
6895
7110
|
const normalized = localPath.replace(/\\/g, '/');
|
|
6896
|
-
const
|
|
7111
|
+
const jsCodeMatched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
|
|
7112
|
+
const automationMatched = normalized.match(/src\/automations\/([^/]+)\/index\.tsx?$/);
|
|
7113
|
+
const functionMatched = normalized.match(/src\/functions\/([^/]+)\/index\.tsx?$/);
|
|
7114
|
+
const matched = jsCodeMatched || automationMatched || functionMatched;
|
|
6897
7115
|
if (!matched) return null;
|
|
7116
|
+
const sourceKind = functionMatched
|
|
7117
|
+
? 'functions'
|
|
7118
|
+
: automationMatched
|
|
7119
|
+
? 'automations'
|
|
7120
|
+
: 'js-code-nodes';
|
|
6898
7121
|
const workspaceRoot = findWorkspaceRoot(localPath);
|
|
6899
|
-
return path.join(workspaceRoot, 'dist',
|
|
7122
|
+
return path.join(workspaceRoot, 'dist', sourceKind, matched[1], 'index.cjs');
|
|
6900
7123
|
}
|
|
6901
7124
|
|
|
6902
7125
|
function sortObjectForStableJson(value) {
|
|
@@ -7058,7 +7281,7 @@ async function uploadJsCodeSnapshotFile(
|
|
|
7058
7281
|
const resolvedLocalPath = fs.existsSync(baseResolvedPath)
|
|
7059
7282
|
? baseResolvedPath
|
|
7060
7283
|
: cwdResolvedPath;
|
|
7061
|
-
const bundlePath = resolveJsCodeBundlePath(resolvedLocalPath, scriptCode);
|
|
7284
|
+
const bundlePath = await resolveJsCodeBundlePath(resolvedLocalPath, scriptCode);
|
|
7062
7285
|
if (!fs.existsSync(bundlePath)) {
|
|
7063
7286
|
fail(`JS_CODE bundle不存在: ${bundlePath}`);
|
|
7064
7287
|
}
|
|
@@ -7094,14 +7317,14 @@ async function uploadJsCodeSnapshotFile(
|
|
|
7094
7317
|
return sourceFile;
|
|
7095
7318
|
}
|
|
7096
7319
|
|
|
7097
|
-
function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
7320
|
+
async function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
7098
7321
|
const extension = path.extname(localPath).toLowerCase();
|
|
7099
7322
|
if (['.js', '.cjs', '.mjs'].includes(extension)) {
|
|
7100
7323
|
fail(
|
|
7101
7324
|
`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}`
|
|
7102
7325
|
);
|
|
7103
7326
|
}
|
|
7104
|
-
if (
|
|
7327
|
+
if (!['.ts', '.tsx'].includes(extension)) {
|
|
7105
7328
|
fail(
|
|
7106
7329
|
`JS_CODE V2 sourceFile.localPath 必须指向 TypeScript 源文件 src/js-code-nodes/<scriptCode>/index.ts、src/automations/<scriptCode>/index.ts 或 src/functions/<functionCode>/index.ts: ${localPath}`
|
|
7107
7330
|
);
|
|
@@ -7125,16 +7348,88 @@ function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
7125
7348
|
: 'js-code-nodes';
|
|
7126
7349
|
|
|
7127
7350
|
const workspaceRoot = findWorkspaceRoot(localPath);
|
|
7128
|
-
const result =
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7351
|
+
const result = runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind);
|
|
7352
|
+
if (result.status !== 0 && sourceKind === 'functions' && isUnsupportedFunctionsBuildScript(result)) {
|
|
7353
|
+
warn(
|
|
7354
|
+
`当前工作区 scripts/build-js-code.mjs 不支持 App Function source=functions,已使用 openxiangda 内置构建器兼容构建 ${resolvedScriptCode}`
|
|
7355
|
+
);
|
|
7356
|
+
await buildFunctionSourceWithBundledEsbuild(localPath, workspaceRoot, resolvedScriptCode);
|
|
7357
|
+
} else if (result.status !== 0) {
|
|
7358
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7359
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7133
7360
|
fail(`JS_CODE bundle构建失败: ${resolvedScriptCode}`);
|
|
7361
|
+
} else {
|
|
7362
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
7363
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
7134
7364
|
}
|
|
7135
7365
|
return path.join(workspaceRoot, 'dist', sourceKind, resolvedScriptCode, 'index.cjs');
|
|
7136
7366
|
}
|
|
7137
7367
|
|
|
7368
|
+
function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind) {
|
|
7369
|
+
return spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
|
|
7370
|
+
cwd: workspaceRoot,
|
|
7371
|
+
encoding: 'utf8',
|
|
7372
|
+
});
|
|
7373
|
+
}
|
|
7374
|
+
|
|
7375
|
+
function isUnsupportedFunctionsBuildScript(result) {
|
|
7376
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
7377
|
+
return (
|
|
7378
|
+
output.includes('unsupported source: functions') ||
|
|
7379
|
+
output.includes('Expected js-code-nodes or automations')
|
|
7380
|
+
);
|
|
7381
|
+
}
|
|
7382
|
+
|
|
7383
|
+
async function buildFunctionSourceWithBundledEsbuild(localPath, workspaceRoot, functionCode) {
|
|
7384
|
+
const outfile = path.join(workspaceRoot, 'dist', 'functions', functionCode, 'index.cjs');
|
|
7385
|
+
fs.mkdirSync(path.dirname(outfile), { recursive: true });
|
|
7386
|
+
const external = Array.from(
|
|
7387
|
+
new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)])
|
|
7388
|
+
);
|
|
7389
|
+
try {
|
|
7390
|
+
await esbuild.build({
|
|
7391
|
+
entryPoints: [localPath],
|
|
7392
|
+
outfile,
|
|
7393
|
+
bundle: true,
|
|
7394
|
+
platform: 'node',
|
|
7395
|
+
format: 'cjs',
|
|
7396
|
+
target: ['node20'],
|
|
7397
|
+
sourcemap: true,
|
|
7398
|
+
logLevel: 'silent',
|
|
7399
|
+
external,
|
|
7400
|
+
plugins: [
|
|
7401
|
+
{
|
|
7402
|
+
name: 'openxiangda-workspace-alias',
|
|
7403
|
+
setup(build) {
|
|
7404
|
+
build.onResolve({ filter: /^@\// }, args => ({
|
|
7405
|
+
path: resolveWorkspaceAliasPath(workspaceRoot, args.path),
|
|
7406
|
+
}));
|
|
7407
|
+
},
|
|
7408
|
+
},
|
|
7409
|
+
],
|
|
7410
|
+
});
|
|
7411
|
+
} catch (error) {
|
|
7412
|
+
fail(`App Function 内置构建失败: ${functionCode}: ${error.message}`);
|
|
7413
|
+
}
|
|
7414
|
+
print(`built functions/${functionCode} -> ${path.relative(workspaceRoot, outfile)}`);
|
|
7415
|
+
}
|
|
7416
|
+
|
|
7417
|
+
function resolveWorkspaceAliasPath(workspaceRoot, importPath) {
|
|
7418
|
+
const raw = path.join(workspaceRoot, 'src', importPath.replace(/^@\//, ''));
|
|
7419
|
+
const candidates = [
|
|
7420
|
+
raw,
|
|
7421
|
+
`${raw}.ts`,
|
|
7422
|
+
`${raw}.tsx`,
|
|
7423
|
+
`${raw}.js`,
|
|
7424
|
+
`${raw}.mjs`,
|
|
7425
|
+
`${raw}.cjs`,
|
|
7426
|
+
path.join(raw, 'index.ts'),
|
|
7427
|
+
path.join(raw, 'index.tsx'),
|
|
7428
|
+
path.join(raw, 'index.js'),
|
|
7429
|
+
];
|
|
7430
|
+
return candidates.find(candidate => fs.existsSync(candidate)) || raw;
|
|
7431
|
+
}
|
|
7432
|
+
|
|
7138
7433
|
function findWorkspaceRoot(startPath) {
|
|
7139
7434
|
if (!fs.existsSync(startPath)) {
|
|
7140
7435
|
return process.cwd();
|
|
@@ -28,6 +28,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
28
28
|
| 审批流程 / workflow / 流程节点 / JS_CODE | `openxiangda-workflow-automation` | `openxiangda workflow validate / create / publish` |
|
|
29
29
|
| 自动化 / 定时任务 / 提交触发 / cron | `openxiangda-workflow-automation` | `openxiangda automation validate / create / publish / enable` |
|
|
30
30
|
| App Function / function_call / 可复用后端逻辑 | `openxiangda-workflow-automation` | declare `src/resources/functions/<code>.json` + `src/functions/<code>/index.ts` |
|
|
31
|
+
| 应用登录 / Auth SDK / 手机号验证码 / CAS / 钉钉免登 | `openxiangda-architecture-design` + `openxiangda-page` | design security gate first, then declare `src/resources/auth/<code>.json` and use `createAuthClient` / `LoginPage` |
|
|
31
32
|
| 角色 / 权限组 / 字段权限 / 数据范围 / 公开访问 | `openxiangda-permission-settings` | `openxiangda permission ...` / `openxiangda settings ...` |
|
|
32
33
|
| 看应用结构 / 快照 / 对比 / 诊断 / 排查 / 报错 | `openxiangda-inspect` | `openxiangda app snapshot <APP_XXX> --profile <name> --json` |
|
|
33
34
|
| 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
|
|
@@ -42,6 +43,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
42
43
|
- ✅ Each profile (dev / prod / ...) has its own `appType` and resource IDs; never copy a `formUuid` / `pageId` / `workflowId` / `automationId` across profiles.
|
|
43
44
|
- ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
|
|
44
45
|
- ✅ For non-trivial app requirements, run the architecture design gate first: forms, fields, pages, permissions, data views, status/workflow, automation, notifications, and development tasks must be decided before coding.
|
|
46
|
+
- ✅ For app login/auth requirements, run the auth security gate before coding: enabled methods, registration policy, identity matching keys, provider boundary, default roles, rate limits, audit fields, and third-party ownership must be confirmed.
|
|
45
47
|
- ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
|
|
46
48
|
- ✅ List pages, linked options, remote selectors, and report drill-downs must use paginated server-side queries with explicit searchable fields. Do not fetch a large page and filter in local state.
|
|
47
49
|
- ✅ Treat workflow forms as an exception. Ordinary ticket/order/task/asset lifecycles use status fields, state machines, action logs, permissions, and automations; workflows are for real approval tasks.
|
|
@@ -142,6 +144,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
142
144
|
- 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.
|
|
143
145
|
- 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.
|
|
144
146
|
- 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.
|
|
147
|
+
- For application login, declare auth resources in `src/resources/auth/<code>.json` and publish them with `openxiangda resource publish`. App Function auth providers may validate external credentials and return only an identity assertion; they must never issue tokens, set cookies, or write platform user/binding tables directly. The platform auth service decides create/bind/reject and issues tokens.
|
|
145
148
|
- 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.
|
|
146
149
|
- 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.
|
|
147
150
|
- Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` for diagnosis before changing an existing app.
|
|
@@ -181,7 +184,7 @@ Core CLI / state:
|
|
|
181
184
|
- `references/architecture-design.md` — OpenXiangda architecture design gate, anti-pattern rejection, question gates, final design template, and black-box demo scenario.
|
|
182
185
|
- `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
|
|
183
186
|
- `references/connector-resources.md` — `src/resources` manifests, connector schema, App Function boundary, and platform runtime calls.
|
|
184
|
-
- `references/resource-manifest-cheatsheet.md` — 复制即用的 connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
|
|
187
|
+
- `references/resource-manifest-cheatsheet.md` — 复制即用的 auth / connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
|
|
185
188
|
- `references/data-views.md` — `src/resources/data-views` materialized/live data view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
|
|
186
189
|
- `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
|
|
187
190
|
- `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.
|
|
@@ -24,6 +24,7 @@ This design flow is for applications built on OpenXiangda. The output is not a g
|
|
|
24
24
|
- status machines or workflows
|
|
25
25
|
- automations, JS_CODE V2 nodes, and App Functions
|
|
26
26
|
- notifications and connectors
|
|
27
|
+
- application login/auth methods, registration policy, identity matching, and provider boundaries
|
|
27
28
|
- publish and acceptance steps
|
|
28
29
|
|
|
29
30
|
The design is complete only when another agent can implement it without deciding major architecture tradeoffs.
|
|
@@ -72,6 +73,8 @@ Do not insult the person. Criticize the proposal and explain the technical conse
|
|
|
72
73
|
| Materialized view for hard realtime data | Users see stale data | `storageMode: "live"` only for bounded realtime joins, or direct source queries/App Function |
|
|
73
74
|
| Live view for unbounded heavy joins | Slow runtime queries | Materialized view with scheduled refresh and indexes |
|
|
74
75
|
| JS_CODE for reusable business service | Logic gets duplicated in graph nodes | App Function for shared backend logic; JS_CODE only for node-local trigger logic |
|
|
76
|
+
| Auth provider issues tokens or writes users directly | Breaks platform account, binding, permission, and audit boundaries | Provider returns identity assertion only; platform creates/binds/rejects and issues token |
|
|
77
|
+
| Phone-code login auto-registers by default | Allows uncontrolled account creation from weak identity proof | Default registration is reject; enable auto-create/whitelist only after explicit confirmation |
|
|
75
78
|
| Raw native form controls | Inconsistent with platform runtime and validation | OpenXiangda platform components first, Ant Design wrappers second |
|
|
76
79
|
| Multiple form permission groups without stable local codes | CLI state and platform `resourceCode` cannot reliably distinguish groups on the same form | Give every group a unique `code`, for example `ticket_reporter_view` and `ticket_repairer_view` |
|
|
77
80
|
| Assuming resource publish means workflow is active | Workflow resources may exist as drafts until explicitly published | Verify `workflow list` shows `isPublished: true`; run `workflow publish <workflowCode>` when needed |
|
|
@@ -153,6 +156,20 @@ Default: reusable calculations and validations become App Functions; one-off bac
|
|
|
153
156
|
- Are templates reusable resources?
|
|
154
157
|
- Are external credentials needed? If yes, use connectors/resources, never page source secrets.
|
|
155
158
|
|
|
159
|
+
### Login And Auth
|
|
160
|
+
|
|
161
|
+
Ask these before generating any login/auth design:
|
|
162
|
+
|
|
163
|
+
- Which methods are enabled: password, DingTalk in-app free login, CAS/SSO, phone code, guest?
|
|
164
|
+
- What is the phone registration policy: reject, auto-create, bind existing only, whitelist, or manual approval?
|
|
165
|
+
- What are the identity match keys and priorities: `phone`, `email`, `externalId`, `unionId`, `jobNumber`, `username`?
|
|
166
|
+
- Which App Function provider validates each external credential, and what identity assertion fields must it return?
|
|
167
|
+
- Which default app roles/page groups/data scopes should new users receive, if registration is enabled?
|
|
168
|
+
- What are the security parameters: code TTL, send frequency, failed attempts, IP/device limits, audit fields, and failure message disclosure?
|
|
169
|
+
- Is CAS/DingTalk configured by the platform tenant, or delegated to an app-level provider function?
|
|
170
|
+
|
|
171
|
+
Default: registration is rejected; provider functions return identity assertions only; platform auth service owns account creation, binding, permission assignment, cookies, and tokens.
|
|
172
|
+
|
|
156
173
|
### UI Design
|
|
157
174
|
|
|
158
175
|
Ask whether to use Product Design or another design skill for:
|
|
@@ -213,6 +230,14 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
213
230
|
- After publishing workflow resources, verify `workflow list` and record whether `isPublished` is true.
|
|
214
231
|
- In React SPA and classic workspaces, JS_CODE/App Function TypeScript lives under `src/js-code-nodes`, `src/automations`, or `src/functions`; build with `pnpm build-js-code`.
|
|
215
232
|
|
|
233
|
+
### Auth
|
|
234
|
+
|
|
235
|
+
- Auth resources use stable local codes under `src/resources/auth/`.
|
|
236
|
+
- Use `/view/:appType/login` or `LoginPage` from `openxiangda/runtime/react` for default React login.
|
|
237
|
+
- Use `createAuthClient({ appType, servicePrefix })` for custom React login pages.
|
|
238
|
+
- Phone-code providers are App Functions. They validate send/verify events and return identity assertions; they do not issue tokens or mutate platform users.
|
|
239
|
+
- New-user registration must be explicit, with default roles and identity conflict behavior documented.
|
|
240
|
+
|
|
216
241
|
## Final Document Template
|
|
217
242
|
|
|
218
243
|
```markdown
|
|
@@ -262,17 +287,26 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
262
287
|
- JS_CODE V2 nodes:
|
|
263
288
|
- App Functions:
|
|
264
289
|
|
|
265
|
-
## 8.
|
|
290
|
+
## 8. Login And Auth
|
|
291
|
+
|
|
292
|
+
- Enabled methods:
|
|
293
|
+
- Registration policy:
|
|
294
|
+
- Identity matching keys:
|
|
295
|
+
- Provider functions:
|
|
296
|
+
- Default roles/permissions:
|
|
297
|
+
- Security parameters:
|
|
298
|
+
|
|
299
|
+
## 9. Notifications And Connectors
|
|
266
300
|
|
|
267
301
|
| Scenario | Trigger | Channel | Recipients | Template/resource | Notes |
|
|
268
302
|
|---|---|---|---|---|---|
|
|
269
303
|
|
|
270
|
-
##
|
|
304
|
+
## 10. Development Task Table
|
|
271
305
|
|
|
272
306
|
| Phase | Task | Resources/files | Validation | Publish step |
|
|
273
307
|
|---|---|---|---|---|
|
|
274
308
|
|
|
275
|
-
##
|
|
309
|
+
## 11. Acceptance Criteria
|
|
276
310
|
|
|
277
311
|
- Functional checks:
|
|
278
312
|
- Permission checks:
|
|
@@ -281,7 +315,7 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
281
315
|
- Notification/automation checks:
|
|
282
316
|
- Workflow checks: real approval flows show `isPublished: true`.
|
|
283
317
|
|
|
284
|
-
##
|
|
318
|
+
## 12. Open Questions And Confirmed Assumptions
|
|
285
319
|
|
|
286
320
|
- Confirmed:
|
|
287
321
|
- Still open:
|
|
@@ -12,6 +12,7 @@ Common folders:
|
|
|
12
12
|
- `automations`
|
|
13
13
|
- `data-views`
|
|
14
14
|
- `functions`
|
|
15
|
+
- `auth`
|
|
15
16
|
- `permissions/page-groups`
|
|
16
17
|
- `permissions/form-groups`
|
|
17
18
|
- `settings/forms`
|
|
@@ -33,6 +34,8 @@ Data view manifests live under `src/resources/data-views/` and define read-only
|
|
|
33
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`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; they do not expose raw SQL or Redis in the current MVP. Use JS_CODE V2 only for node-local workflow/automation scripts.
|
|
35
36
|
|
|
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
|
+
|
|
36
39
|
```json
|
|
37
40
|
{
|
|
38
41
|
"code": "crm",
|