openxiangda 1.0.86 → 1.0.88
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 +30 -0
- package/lib/cli.js +391 -0
- package/openxiangda-skills/SKILL.md +5 -1
- package/openxiangda-skills/references/architecture-design.md +29 -0
- package/openxiangda-skills/references/openxiangda-api.md +109 -3
- package/openxiangda-skills/references/pages/page-sdk.md +35 -0
- package/openxiangda-skills/references/permissions-settings.md +39 -2
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +72 -4
- package/package.json +1 -1
- package/packages/sdk/dist/runtime/index.cjs +3624 -3394
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +4 -1001
- package/packages/sdk/dist/runtime/index.d.ts +4 -1001
- package/packages/sdk/dist/runtime/index.mjs +3083 -2855
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +2826 -121
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +1400 -2
- package/packages/sdk/dist/runtime/react.d.ts +1400 -2
- package/packages/sdk/dist/runtime/react.mjs +2782 -89
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +29 -0
- package/templates/openxiangda-react-spa/src/app/router.tsx +21 -1
- package/templates/openxiangda-react-spa/src/pages/public/PublicRegisterPage.tsx +15 -0
- package/templates/openxiangda-react-spa/src/resources/permissions/page-groups/public-visitor.json +8 -0
- package/templates/openxiangda-react-spa/src/resources/public-access/public-register.json +14 -0
- package/templates/openxiangda-react-spa/src/resources/routes/public-register.json +8 -0
- package/templates/openxiangda-react-spa/tsconfig.app.json +1 -1
- package/templates/openxiangda-react-spa/vite.config.ts +1 -1
- package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.mts +0 -328
- package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.ts +0 -328
package/README.md
CHANGED
|
@@ -174,6 +174,36 @@ const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
|
|
|
174
174
|
|
|
175
175
|
工程化资源放在工作区 `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")` 调用平台运行时接口,第三方密钥只保存在后端连接器配置中。
|
|
176
176
|
|
|
177
|
+
React SPA 新应用的无需登录访问统一使用 `/view/:appType/public/*`。应用在 `src/resources/routes/*.json` 声明公开路由,在 `src/resources/public-access/*.json` 声明公开策略、外部角色和可访问资源 grant,页面用 `PublicAccessGate` 或 `createPublicAccessClient` 创建 scoped public session。公开 guest 的 form、dataView、function、connector 默认全部拒绝,只有 policy `grants` 明确列出的资源可访问,并且仍受对应表单权限组、dataView 权限组等后端权限控制。
|
|
178
|
+
|
|
179
|
+
`mode: "ticket"` 的公开策略默认按单次 ticket 使用;只有明确配置 `ticketConfig.singleUse: false` 时才允许复用。新 public session 只返回 bearer token,SDK 会把 token 注入后续 runtime/bootstrap、dataView、function、connector 请求,不依赖认证 cookie。
|
|
180
|
+
|
|
181
|
+
旧 `?publicAccess=guest` 和表单 `settings/forms/*.json` 里的 `publicAccess` 只用于旧 `sy-lowcode-view` 兼容。新 React SPA 应用不要再设计或生成这种链接。
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"code": "public_register",
|
|
186
|
+
"mode": "guest",
|
|
187
|
+
"routeCode": "public.register",
|
|
188
|
+
"pathPattern": "/view/:appType/public/register",
|
|
189
|
+
"externalRoleCodes": ["external_visitor"],
|
|
190
|
+
"grants": {
|
|
191
|
+
"forms": ["registration_form"],
|
|
192
|
+
"dataViews": ["public_registration_lookup"],
|
|
193
|
+
"functions": ["submit_public_registration"],
|
|
194
|
+
"connectors": ["sms.sendCode"]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { PublicAccessGate } from "openxiangda/runtime/react"
|
|
201
|
+
|
|
202
|
+
<PublicAccessGate policyCode="public_register" routeCode="public.register">
|
|
203
|
+
<PublicRegisterPage />
|
|
204
|
+
</PublicAccessGate>
|
|
205
|
+
```
|
|
206
|
+
|
|
177
207
|
多表只读查询和固定口径统计优先声明 `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`,不需要刷新。
|
|
178
208
|
|
|
179
209
|
后端业务逻辑优先声明为 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 中直接运行时接口要求调用者具备应用自动化管理权限,自动化/流程内部调用走服务端受控上下文。
|
package/lib/cli.js
CHANGED
|
@@ -3376,6 +3376,8 @@ function ensureResourceBuckets(bound) {
|
|
|
3376
3376
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
3377
3377
|
bound.resources.dataViews = bound.resources.dataViews || {};
|
|
3378
3378
|
bound.resources.authConfigs = bound.resources.authConfigs || {};
|
|
3379
|
+
bound.resources.routes = bound.resources.routes || {};
|
|
3380
|
+
bound.resources.publicAccessPolicies = bound.resources.publicAccessPolicies || {};
|
|
3379
3381
|
bound.resources.notifications = bound.resources.notifications || {};
|
|
3380
3382
|
bound.resources.notifications.templates = bound.resources.notifications.templates || {};
|
|
3381
3383
|
bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
|
|
@@ -3586,6 +3588,22 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
|
|
|
3586
3588
|
}, keys);
|
|
3587
3589
|
}
|
|
3588
3590
|
|
|
3591
|
+
function saveRouteResource(target, routeCode, routeId, extra = {}) {
|
|
3592
|
+
const keys = ['routeId', 'pathPattern', 'publicAccess', 'publicPolicyCode'];
|
|
3593
|
+
saveStateResource(target, 'routes', routeCode, {
|
|
3594
|
+
...pickStateFields(extra, keys),
|
|
3595
|
+
routeId,
|
|
3596
|
+
}, keys);
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
function savePublicAccessPolicyResource(target, policyCode, policyId, extra = {}) {
|
|
3600
|
+
const keys = ['policyId', 'mode', 'routeCode', 'pathPattern'];
|
|
3601
|
+
saveStateResource(target, 'publicAccessPolicies', policyCode, {
|
|
3602
|
+
...pickStateFields(extra, keys),
|
|
3603
|
+
policyId,
|
|
3604
|
+
}, keys);
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3589
3607
|
function saveAuthConfigResource(target, authConfigCode, configId, extra = {}) {
|
|
3590
3608
|
const keys = ['configId', 'status'];
|
|
3591
3609
|
saveStateResource(target, 'authConfigs', authConfigCode, {
|
|
@@ -3702,6 +3720,13 @@ const RESOURCE_SPECS = [
|
|
|
3702
3720
|
{ key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
|
|
3703
3721
|
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
3704
3722
|
{ key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
|
|
3723
|
+
{ key: 'routes', dir: 'routes', topFiles: ['routes.json'], pluralKeys: ['routes'] },
|
|
3724
|
+
{
|
|
3725
|
+
key: 'publicAccessPolicies',
|
|
3726
|
+
dir: 'public-access',
|
|
3727
|
+
topFiles: ['public-access.json'],
|
|
3728
|
+
pluralKeys: ['publicAccessPolicies', 'publicAccess', 'policies'],
|
|
3729
|
+
},
|
|
3705
3730
|
{
|
|
3706
3731
|
key: 'pagePermissionGroups',
|
|
3707
3732
|
dir: path.join('permissions', 'page-groups'),
|
|
@@ -3883,6 +3908,12 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3883
3908
|
routeCode: item.routeCode || null,
|
|
3884
3909
|
path: item.path || null,
|
|
3885
3910
|
}));
|
|
3911
|
+
const routes = (manifest.routes || []).map(item => ({
|
|
3912
|
+
code: item.code,
|
|
3913
|
+
pathPattern: item.pathPattern || item.path || null,
|
|
3914
|
+
publicAccess: item.publicAccess || 'none',
|
|
3915
|
+
publicPolicyCode: item.publicPolicyCode || item.policyCode || null,
|
|
3916
|
+
}));
|
|
3886
3917
|
const pagePermissionGroups = (manifest.pagePermissionGroups || []).map(item => ({
|
|
3887
3918
|
code: item.code,
|
|
3888
3919
|
menuCodes: normalizePermissionCodeArray(item.menuCodes),
|
|
@@ -3892,6 +3923,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3892
3923
|
const routeCodes = unique(
|
|
3893
3924
|
[
|
|
3894
3925
|
...menus.map(item => item.routeCode),
|
|
3926
|
+
...routes.map(item => item.code),
|
|
3895
3927
|
...pagePermissionGroups.flatMap(item => item.routeCodes),
|
|
3896
3928
|
].filter(Boolean)
|
|
3897
3929
|
);
|
|
@@ -3902,6 +3934,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3902
3934
|
const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
|
|
3903
3935
|
const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
|
|
3904
3936
|
const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
|
|
3937
|
+
const publicAccessPolicyCodes = unique((manifest.publicAccessPolicies || []).map(item => item.code).filter(Boolean));
|
|
3905
3938
|
const content = [
|
|
3906
3939
|
'/* eslint-disable */',
|
|
3907
3940
|
'// Generated by openxiangda resource typegen. Do not edit manually.',
|
|
@@ -3924,8 +3957,13 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3924
3957
|
`export const authConfigCodes = ${JSON.stringify(authConfigCodes, null, 2)} as const`,
|
|
3925
3958
|
'export type AuthConfigCode = typeof authConfigCodes[number]',
|
|
3926
3959
|
'',
|
|
3960
|
+
`export const publicAccessPolicyCodes = ${JSON.stringify(publicAccessPolicyCodes, null, 2)} as const`,
|
|
3961
|
+
'export type PublicAccessPolicyCode = typeof publicAccessPolicyCodes[number]',
|
|
3962
|
+
'',
|
|
3927
3963
|
`export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
|
|
3928
3964
|
'',
|
|
3965
|
+
`export const runtimeRoutes = ${JSON.stringify(routes, null, 2)} as const`,
|
|
3966
|
+
'',
|
|
3929
3967
|
`export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
|
|
3930
3968
|
'',
|
|
3931
3969
|
].join('\n');
|
|
@@ -3939,6 +3977,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3939
3977
|
dataViews: dataViewCodes.length,
|
|
3940
3978
|
functions: functionCodes.length,
|
|
3941
3979
|
authConfigs: authConfigCodes.length,
|
|
3980
|
+
publicAccessPolicies: publicAccessPolicyCodes.length,
|
|
3942
3981
|
};
|
|
3943
3982
|
}
|
|
3944
3983
|
|
|
@@ -4043,6 +4082,29 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
4043
4082
|
warnings.push(`${label}: 已开启非默认注册策略 ${registrationMode},请确认身份匹配键、默认角色和审计字段`);
|
|
4044
4083
|
}
|
|
4045
4084
|
}
|
|
4085
|
+
if (kind === 'routes') {
|
|
4086
|
+
if (!item.pathPattern && !item.path) errors.push(`${label}: 缺少 pathPattern`);
|
|
4087
|
+
const mode = item.publicAccess === undefined ? 'none' : String(item.publicAccess);
|
|
4088
|
+
if (!['none', 'guest', 'ticket'].includes(mode)) {
|
|
4089
|
+
errors.push(`${label}: publicAccess 只能是 none、guest 或 ticket`);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
if (kind === 'publicAccessPolicies') {
|
|
4093
|
+
if (!item.name) warnings.push(`${label}: 未声明 name,将使用 code`);
|
|
4094
|
+
const mode = item.mode === undefined ? 'guest' : String(item.mode);
|
|
4095
|
+
if (!['guest', 'ticket'].includes(mode)) {
|
|
4096
|
+
errors.push(`${label}: mode 只能是 guest 或 ticket`);
|
|
4097
|
+
}
|
|
4098
|
+
if (!item.routeCode && !item.pathPattern && !item.path) {
|
|
4099
|
+
warnings.push(`${label}: 未声明 routeCode/pathPattern,建议绑定到 /view/:appType/public/* 路由`);
|
|
4100
|
+
}
|
|
4101
|
+
if (!item.grants || typeof item.grants !== 'object' || Array.isArray(item.grants)) {
|
|
4102
|
+
errors.push(`${label}: 缺少 grants object;未显式 grant 的 form/dataView/function/connector 默认拒绝`);
|
|
4103
|
+
}
|
|
4104
|
+
if (!Array.isArray(item.externalRoleCodes || item.externalRoles || item.roles || [])) {
|
|
4105
|
+
errors.push(`${label}: externalRoleCodes 必须是数组`);
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4046
4108
|
if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
4047
4109
|
if (kind === 'formPermissionGroups') {
|
|
4048
4110
|
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
@@ -4445,6 +4507,8 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
4445
4507
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
4446
4508
|
addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
|
|
4447
4509
|
addPlanActions(actions, 'authConfig', manifest.authConfigs, existing.authConfigs, (item, current) => authConfigEquals(item, current));
|
|
4510
|
+
addPlanActions(actions, 'route', manifest.routes, existing.routes, (item, current) => routeEquals(item, current));
|
|
4511
|
+
addPlanActions(actions, 'publicAccessPolicy', manifest.publicAccessPolicies, existing.publicAccessPolicies, (item, current) => publicAccessPolicyEquals(target.bound, item, current));
|
|
4448
4512
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
4449
4513
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
4450
4514
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
@@ -4481,8 +4545,10 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
4481
4545
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
4482
4546
|
await publishFunctionResources(config, target, manifest.functions || [], result);
|
|
4483
4547
|
await publishAuthConfigResources(config, target, manifest.authConfigs || [], result);
|
|
4548
|
+
await publishRouteResources(config, target, manifest.routes || [], result);
|
|
4484
4549
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
4485
4550
|
await publishDataViewResources(config, target, manifest.dataViews || [], result);
|
|
4551
|
+
await publishPublicAccessPolicyResources(config, target, manifest.publicAccessPolicies || [], result);
|
|
4486
4552
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
4487
4553
|
await publishFormPermissionGroupResources(config, target, manifest.formPermissionGroups || [], result);
|
|
4488
4554
|
if (options.prune) {
|
|
@@ -4505,6 +4571,8 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4505
4571
|
workflows: new Map(),
|
|
4506
4572
|
functions: new Map(),
|
|
4507
4573
|
authConfigs: new Map(),
|
|
4574
|
+
routes: new Map(),
|
|
4575
|
+
publicAccessPolicies: new Map(),
|
|
4508
4576
|
automations: new Map(),
|
|
4509
4577
|
dataViews: new Map(),
|
|
4510
4578
|
pagePermissionGroups: new Map(),
|
|
@@ -4635,6 +4703,28 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4635
4703
|
});
|
|
4636
4704
|
}
|
|
4637
4705
|
}
|
|
4706
|
+
if ((manifest.routes || []).length > 0) {
|
|
4707
|
+
const data = await requestWithAuth(
|
|
4708
|
+
config,
|
|
4709
|
+
target.profileName,
|
|
4710
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`, {
|
|
4711
|
+
page: 1,
|
|
4712
|
+
pageSize: 1000,
|
|
4713
|
+
})
|
|
4714
|
+
);
|
|
4715
|
+
indexByCode(maps.routes, normalizeItems(data), item => item.code || item.resourceCode);
|
|
4716
|
+
}
|
|
4717
|
+
if ((manifest.publicAccessPolicies || []).length > 0) {
|
|
4718
|
+
const data = await requestWithAuth(
|
|
4719
|
+
config,
|
|
4720
|
+
target.profileName,
|
|
4721
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`, {
|
|
4722
|
+
page: 1,
|
|
4723
|
+
pageSize: 1000,
|
|
4724
|
+
})
|
|
4725
|
+
);
|
|
4726
|
+
indexByCode(maps.publicAccessPolicies, normalizeItems(data), item => item.code || item.resourceCode);
|
|
4727
|
+
}
|
|
4638
4728
|
if ((manifest.dataViews || []).length > 0) {
|
|
4639
4729
|
const data = await requestWithAuth(
|
|
4640
4730
|
config,
|
|
@@ -5362,6 +5452,74 @@ async function publishDataViewResources(config, target, dataViews, result) {
|
|
|
5362
5452
|
}
|
|
5363
5453
|
}
|
|
5364
5454
|
|
|
5455
|
+
async function publishRouteResources(config, target, routes, result) {
|
|
5456
|
+
for (const route of routes) {
|
|
5457
|
+
const code = route.code || route.resourceCode;
|
|
5458
|
+
const existing = await findExistingRoute(config, target, code);
|
|
5459
|
+
const body = normalizeRouteManifest(route);
|
|
5460
|
+
const data = existing
|
|
5461
|
+
? await requestWithAuth(
|
|
5462
|
+
config,
|
|
5463
|
+
target.profileName,
|
|
5464
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes/${encodeURIComponent(code)}`,
|
|
5465
|
+
{ method: 'PUT', body }
|
|
5466
|
+
)
|
|
5467
|
+
: await requestWithAuth(
|
|
5468
|
+
config,
|
|
5469
|
+
target.profileName,
|
|
5470
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`,
|
|
5471
|
+
{ method: 'POST', body }
|
|
5472
|
+
);
|
|
5473
|
+
if (data?.id) {
|
|
5474
|
+
saveRouteResource(target, code, data.id, {
|
|
5475
|
+
pathPattern: data.pathPattern,
|
|
5476
|
+
publicAccess: data.publicAccess,
|
|
5477
|
+
publicPolicyCode: data.publicPolicyCode,
|
|
5478
|
+
});
|
|
5479
|
+
}
|
|
5480
|
+
result.published.push({
|
|
5481
|
+
kind: 'route',
|
|
5482
|
+
code,
|
|
5483
|
+
action: existing ? 'update' : 'create',
|
|
5484
|
+
id: data?.id,
|
|
5485
|
+
});
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5489
|
+
async function publishPublicAccessPolicyResources(config, target, policies, result) {
|
|
5490
|
+
for (const policy of policies) {
|
|
5491
|
+
const code = policy.code || policy.resourceCode;
|
|
5492
|
+
const existing = await findExistingPublicAccessPolicy(config, target, code);
|
|
5493
|
+
const body = normalizePublicAccessPolicyManifest(target.bound, policy);
|
|
5494
|
+
const data = existing
|
|
5495
|
+
? await requestWithAuth(
|
|
5496
|
+
config,
|
|
5497
|
+
target.profileName,
|
|
5498
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(code)}`,
|
|
5499
|
+
{ method: 'PUT', body }
|
|
5500
|
+
)
|
|
5501
|
+
: await requestWithAuth(
|
|
5502
|
+
config,
|
|
5503
|
+
target.profileName,
|
|
5504
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`,
|
|
5505
|
+
{ method: 'POST', body }
|
|
5506
|
+
);
|
|
5507
|
+
if (data?.id) {
|
|
5508
|
+
savePublicAccessPolicyResource(target, code, data.id, {
|
|
5509
|
+
mode: data.mode,
|
|
5510
|
+
routeCode: data.routeCode,
|
|
5511
|
+
pathPattern: data.pathPattern,
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
result.published.push({
|
|
5515
|
+
kind: 'publicAccessPolicy',
|
|
5516
|
+
code,
|
|
5517
|
+
action: existing ? 'update' : 'create',
|
|
5518
|
+
id: data?.id,
|
|
5519
|
+
});
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5365
5523
|
async function findExistingFunction(config, target, code) {
|
|
5366
5524
|
const stateId = target.bound.resources?.functions?.[code]?.functionId;
|
|
5367
5525
|
const byCode = await requestOptionalWithAuth(
|
|
@@ -5374,6 +5532,30 @@ async function findExistingFunction(config, target, code) {
|
|
|
5374
5532
|
return null;
|
|
5375
5533
|
}
|
|
5376
5534
|
|
|
5535
|
+
async function findExistingRoute(config, target, code) {
|
|
5536
|
+
const stateId = target.bound.resources?.routes?.[code]?.routeId;
|
|
5537
|
+
const byCode = await requestOptionalWithAuth(
|
|
5538
|
+
config,
|
|
5539
|
+
target.profileName,
|
|
5540
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes/${encodeURIComponent(code)}`
|
|
5541
|
+
);
|
|
5542
|
+
if (byCode?.id) return byCode;
|
|
5543
|
+
if (stateId) return { id: stateId, code };
|
|
5544
|
+
return null;
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
async function findExistingPublicAccessPolicy(config, target, code) {
|
|
5548
|
+
const stateId = target.bound.resources?.publicAccessPolicies?.[code]?.policyId;
|
|
5549
|
+
const byCode = await requestOptionalWithAuth(
|
|
5550
|
+
config,
|
|
5551
|
+
target.profileName,
|
|
5552
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(code)}`
|
|
5553
|
+
);
|
|
5554
|
+
if (byCode?.id) return byCode;
|
|
5555
|
+
if (stateId) return { id: stateId, code };
|
|
5556
|
+
return null;
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5377
5559
|
async function findExistingAuthConfig(config, target, code) {
|
|
5378
5560
|
const stateId = target.bound.resources?.authConfigs?.[code]?.configId;
|
|
5379
5561
|
const byCode = await requestOptionalWithAuth(
|
|
@@ -5537,7 +5719,14 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
5537
5719
|
await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
|
|
5538
5720
|
await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
|
|
5539
5721
|
await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
|
|
5722
|
+
await pruneRoutes(config, target, desiredCodes(manifest.routes), result);
|
|
5540
5723
|
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
5724
|
+
await prunePublicAccessPolicies(
|
|
5725
|
+
config,
|
|
5726
|
+
target,
|
|
5727
|
+
desiredCodes(manifest.publicAccessPolicies),
|
|
5728
|
+
result
|
|
5729
|
+
);
|
|
5541
5730
|
await prunePagePermissionGroups(
|
|
5542
5731
|
config,
|
|
5543
5732
|
target,
|
|
@@ -5755,6 +5944,52 @@ async function pruneAuthConfigs(config, target, desired, result) {
|
|
|
5755
5944
|
}
|
|
5756
5945
|
}
|
|
5757
5946
|
|
|
5947
|
+
async function pruneRoutes(config, target, desired, result) {
|
|
5948
|
+
const data = await requestWithAuth(
|
|
5949
|
+
config,
|
|
5950
|
+
target.profileName,
|
|
5951
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`, {
|
|
5952
|
+
page: 1,
|
|
5953
|
+
pageSize: 1000,
|
|
5954
|
+
})
|
|
5955
|
+
);
|
|
5956
|
+
for (const route of normalizeItems(data)) {
|
|
5957
|
+
const code = route.code || route.resourceCode;
|
|
5958
|
+
if (!code || desired.has(code)) continue;
|
|
5959
|
+
await pruneOne(config, target, result, 'route', code, route.id, async () => {
|
|
5960
|
+
await requestWithAuth(
|
|
5961
|
+
config,
|
|
5962
|
+
target.profileName,
|
|
5963
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes/${encodeURIComponent(code)}`,
|
|
5964
|
+
{ method: 'DELETE' }
|
|
5965
|
+
);
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
}
|
|
5969
|
+
|
|
5970
|
+
async function prunePublicAccessPolicies(config, target, desired, result) {
|
|
5971
|
+
const data = await requestWithAuth(
|
|
5972
|
+
config,
|
|
5973
|
+
target.profileName,
|
|
5974
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`, {
|
|
5975
|
+
page: 1,
|
|
5976
|
+
pageSize: 1000,
|
|
5977
|
+
})
|
|
5978
|
+
);
|
|
5979
|
+
for (const policy of normalizeItems(data)) {
|
|
5980
|
+
const code = policy.code || policy.resourceCode;
|
|
5981
|
+
if (!code || desired.has(code)) continue;
|
|
5982
|
+
await pruneOne(config, target, result, 'publicAccessPolicy', code, policy.id, async () => {
|
|
5983
|
+
await requestWithAuth(
|
|
5984
|
+
config,
|
|
5985
|
+
target.profileName,
|
|
5986
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies/${encodeURIComponent(code)}`,
|
|
5987
|
+
{ method: 'DELETE' }
|
|
5988
|
+
);
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5758
5993
|
async function prunePagePermissionGroups(config, target, desired, result) {
|
|
5759
5994
|
const data = await requestWithAuth(
|
|
5760
5995
|
config,
|
|
@@ -5831,6 +6066,8 @@ function removeStateResource(target, kind, code) {
|
|
|
5831
6066
|
automation: 'automations',
|
|
5832
6067
|
function: 'functions',
|
|
5833
6068
|
authConfig: 'authConfigs',
|
|
6069
|
+
route: 'routes',
|
|
6070
|
+
publicAccessPolicy: 'publicAccessPolicies',
|
|
5834
6071
|
dataView: 'dataViews',
|
|
5835
6072
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
5836
6073
|
formPermissionGroup: 'formPermissionGroups',
|
|
@@ -6049,6 +6286,36 @@ async function pullResources(config, target) {
|
|
|
6049
6286
|
written.push(path.relative(process.cwd(), filePath));
|
|
6050
6287
|
}
|
|
6051
6288
|
|
|
6289
|
+
const routes = await requestWithAuth(
|
|
6290
|
+
config,
|
|
6291
|
+
target.profileName,
|
|
6292
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/routes`, {
|
|
6293
|
+
page: 1,
|
|
6294
|
+
pageSize: 1000,
|
|
6295
|
+
})
|
|
6296
|
+
);
|
|
6297
|
+
for (const route of normalizeItems(routes)) {
|
|
6298
|
+
const code = route.code || route.resourceCode || route.id;
|
|
6299
|
+
const filePath = path.join(baseDir, 'routes', `${code}.json`);
|
|
6300
|
+
writeResourceJsonFile(filePath, toPulledRoute(route));
|
|
6301
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
const publicAccessPolicies = await requestWithAuth(
|
|
6305
|
+
config,
|
|
6306
|
+
target.profileName,
|
|
6307
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/public-access/policies`, {
|
|
6308
|
+
page: 1,
|
|
6309
|
+
pageSize: 1000,
|
|
6310
|
+
})
|
|
6311
|
+
);
|
|
6312
|
+
for (const policy of normalizeItems(publicAccessPolicies)) {
|
|
6313
|
+
const code = policy.code || policy.resourceCode || policy.id;
|
|
6314
|
+
const filePath = path.join(baseDir, 'public-access', `${code}.json`);
|
|
6315
|
+
writeResourceJsonFile(filePath, toPulledPublicAccessPolicy(policy, pullLookups));
|
|
6316
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
6317
|
+
}
|
|
6318
|
+
|
|
6052
6319
|
const dataViews = await requestWithAuth(
|
|
6053
6320
|
config,
|
|
6054
6321
|
target.profileName,
|
|
@@ -6264,6 +6531,48 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
|
|
|
6264
6531
|
});
|
|
6265
6532
|
}
|
|
6266
6533
|
|
|
6534
|
+
function toPulledRoute(route) {
|
|
6535
|
+
return stripUndefinedValues({
|
|
6536
|
+
code: route.code || route.resourceCode,
|
|
6537
|
+
title: route.title || route.name,
|
|
6538
|
+
kind: route.kind || 'page',
|
|
6539
|
+
pathPattern: route.pathPattern || route.path,
|
|
6540
|
+
menuCode: route.menuCode || undefined,
|
|
6541
|
+
publicAccess: route.publicAccess || 'none',
|
|
6542
|
+
publicPolicyCode: route.publicPolicyCode || route.policyCode || undefined,
|
|
6543
|
+
meta: route.metaJson || route.meta || undefined,
|
|
6544
|
+
});
|
|
6545
|
+
}
|
|
6546
|
+
|
|
6547
|
+
function toPulledPublicAccessPolicy(policy, lookups) {
|
|
6548
|
+
return stripUndefinedValues({
|
|
6549
|
+
code: policy.code || policy.resourceCode,
|
|
6550
|
+
name: policy.name,
|
|
6551
|
+
description: policy.description || '',
|
|
6552
|
+
enabled: policy.enabled !== false,
|
|
6553
|
+
mode: policy.mode || 'guest',
|
|
6554
|
+
routeCode: policy.routeCode || undefined,
|
|
6555
|
+
pathPattern: policy.pathPattern || policy.path || undefined,
|
|
6556
|
+
externalRoleCodes: policy.externalRoleCodes || policy.externalRoles || [],
|
|
6557
|
+
grants: rewritePublicAccessGrantsForManifest(policy.grantsJson || policy.grants || {}, lookups),
|
|
6558
|
+
ticketConfig: policy.ticketConfigJson || policy.ticketConfig || undefined,
|
|
6559
|
+
rateLimit: policy.rateLimitJson || policy.rateLimit || undefined,
|
|
6560
|
+
expiresAt: policy.expiresAt || undefined,
|
|
6561
|
+
});
|
|
6562
|
+
}
|
|
6563
|
+
|
|
6564
|
+
function rewritePublicAccessGrantsForManifest(grants, lookups) {
|
|
6565
|
+
const formCodes = normalizePermissionCodeArray(grants.forms).map(formUuid =>
|
|
6566
|
+
lookups.formCodeByUuid.get(formUuid) || formUuid
|
|
6567
|
+
);
|
|
6568
|
+
return stripUndefinedValues({
|
|
6569
|
+
formCodes,
|
|
6570
|
+
dataViews: normalizePermissionCodeArray(grants.dataViews),
|
|
6571
|
+
functions: normalizePermissionCodeArray(grants.functions),
|
|
6572
|
+
connectors: normalizePermissionCodeArray(grants.connectors),
|
|
6573
|
+
});
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6267
6576
|
function toPulledAuthConfig(authConfig) {
|
|
6268
6577
|
return stripUndefinedValues({
|
|
6269
6578
|
code: authConfig.code || authConfig.resourceCode,
|
|
@@ -6454,6 +6763,54 @@ function normalizeDataViewManifest(bound, dataView) {
|
|
|
6454
6763
|
});
|
|
6455
6764
|
}
|
|
6456
6765
|
|
|
6766
|
+
function normalizeRouteManifest(route) {
|
|
6767
|
+
const code = route.code || route.resourceCode;
|
|
6768
|
+
return stripUndefinedValues({
|
|
6769
|
+
code,
|
|
6770
|
+
title: route.title || route.name || code,
|
|
6771
|
+
kind: route.kind || 'page',
|
|
6772
|
+
pathPattern: route.pathPattern || route.path,
|
|
6773
|
+
menuCode: route.menuCode,
|
|
6774
|
+
publicAccess: route.publicAccess || 'none',
|
|
6775
|
+
publicPolicyCode: route.publicPolicyCode || route.policyCode,
|
|
6776
|
+
meta: route.meta || route.metaJson,
|
|
6777
|
+
});
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6780
|
+
function normalizePublicAccessPolicyManifest(bound, policy) {
|
|
6781
|
+
const code = policy.code || policy.resourceCode;
|
|
6782
|
+
return stripUndefinedValues({
|
|
6783
|
+
code,
|
|
6784
|
+
name: policy.name || policy.title || code,
|
|
6785
|
+
description: policy.description || '',
|
|
6786
|
+
enabled: policy.enabled !== false,
|
|
6787
|
+
mode: policy.mode || 'guest',
|
|
6788
|
+
routeCode: policy.routeCode,
|
|
6789
|
+
pathPattern: policy.pathPattern || policy.path,
|
|
6790
|
+
externalRoleCodes: normalizePermissionCodeArray(
|
|
6791
|
+
policy.externalRoleCodes || policy.externalRoles || policy.roles
|
|
6792
|
+
),
|
|
6793
|
+
grants: normalizePublicAccessGrants(bound, policy.grants || {}),
|
|
6794
|
+
ticketConfig: policy.ticketConfig,
|
|
6795
|
+
rateLimit: policy.rateLimit,
|
|
6796
|
+
expiresAt: policy.expiresAt,
|
|
6797
|
+
});
|
|
6798
|
+
}
|
|
6799
|
+
|
|
6800
|
+
function normalizePublicAccessGrants(bound, grants = {}) {
|
|
6801
|
+
const formEntries = [
|
|
6802
|
+
...normalizePermissionCodeArray(grants.forms),
|
|
6803
|
+
...normalizePermissionCodeArray(grants.formCodes),
|
|
6804
|
+
...normalizePermissionCodeArray(grants.formUuids),
|
|
6805
|
+
];
|
|
6806
|
+
return stripUndefinedValues({
|
|
6807
|
+
forms: unique(formEntries.map(code => resolveOptionalFormUuid(bound, code)).filter(Boolean)),
|
|
6808
|
+
dataViews: normalizePermissionCodeArray(grants.dataViews),
|
|
6809
|
+
functions: normalizePermissionCodeArray(grants.functions),
|
|
6810
|
+
connectors: normalizePermissionCodeArray(grants.connectors),
|
|
6811
|
+
});
|
|
6812
|
+
}
|
|
6813
|
+
|
|
6457
6814
|
function normalizeAuthConfigManifest(authConfig) {
|
|
6458
6815
|
const code = authConfig.code || authConfig.resourceCode || 'default';
|
|
6459
6816
|
const configJson = clonePlainJson(
|
|
@@ -6969,6 +7326,40 @@ function authConfigEquals(desired, existing) {
|
|
|
6969
7326
|
);
|
|
6970
7327
|
}
|
|
6971
7328
|
|
|
7329
|
+
function routeEquals(desired, existing) {
|
|
7330
|
+
if (!existing) return false;
|
|
7331
|
+
const expected = normalizeRouteManifest(desired);
|
|
7332
|
+
return (
|
|
7333
|
+
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
7334
|
+
String(existing.title || existing.name || '') === String(expected.title || '') &&
|
|
7335
|
+
String(existing.kind || 'page') === String(expected.kind || 'page') &&
|
|
7336
|
+
String(existing.pathPattern || existing.path || '') === String(expected.pathPattern || '') &&
|
|
7337
|
+
String(existing.menuCode || '') === String(expected.menuCode || '') &&
|
|
7338
|
+
String(existing.publicAccess || 'none') === String(expected.publicAccess || 'none') &&
|
|
7339
|
+
String(existing.publicPolicyCode || existing.policyCode || '') === String(expected.publicPolicyCode || '') &&
|
|
7340
|
+
jsonEqualsForPlan(existing.metaJson || existing.meta || {}, expected.meta || {}, desired.__dir)
|
|
7341
|
+
);
|
|
7342
|
+
}
|
|
7343
|
+
|
|
7344
|
+
function publicAccessPolicyEquals(bound, desired, existing) {
|
|
7345
|
+
if (!existing) return false;
|
|
7346
|
+
const expected = normalizePublicAccessPolicyManifest(bound, desired);
|
|
7347
|
+
return (
|
|
7348
|
+
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
7349
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
7350
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
7351
|
+
Boolean(existing.enabled) === Boolean(expected.enabled) &&
|
|
7352
|
+
String(existing.mode || 'guest') === String(expected.mode || 'guest') &&
|
|
7353
|
+
String(existing.routeCode || '') === String(expected.routeCode || '') &&
|
|
7354
|
+
String(existing.pathPattern || '') === String(expected.pathPattern || '') &&
|
|
7355
|
+
stringSetEquals(existing.externalRoleCodes || [], expected.externalRoleCodes || []) &&
|
|
7356
|
+
jsonEqualsForPlan(existing.grantsJson || existing.grants || {}, expected.grants || {}, desired.__dir) &&
|
|
7357
|
+
jsonEqualsForPlan(existing.ticketConfigJson || existing.ticketConfig || {}, expected.ticketConfig || {}, desired.__dir) &&
|
|
7358
|
+
jsonEqualsForPlan(existing.rateLimitJson || existing.rateLimit || {}, expected.rateLimit || {}, desired.__dir) &&
|
|
7359
|
+
String(existing.expiresAt || '') === String(expected.expiresAt || '')
|
|
7360
|
+
);
|
|
7361
|
+
}
|
|
7362
|
+
|
|
6972
7363
|
function dataViewEquals(bound, desired, existing) {
|
|
6973
7364
|
if (!existing) return false;
|
|
6974
7365
|
const expected = normalizeDataViewManifest(bound, desired);
|
|
@@ -29,7 +29,8 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
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
31
|
| 应用登录 / Auth SDK / 手机号验证码 / CAS / 钉钉免登 | `openxiangda-architecture-design` + `openxiangda-page` | design security gate first, then declare `src/resources/auth/<code>.json` and use `createAuthClient` / `LoginPage` |
|
|
32
|
-
| 角色 / 权限组 / 字段权限 / 数据范围
|
|
32
|
+
| 角色 / 权限组 / 字段权限 / 数据范围 | `openxiangda-permission-settings` | `openxiangda permission ...` / `openxiangda settings ...` |
|
|
33
|
+
| 外部人员无需登录访问 / 公开报名 / 公开查询 / public page | `openxiangda-architecture-design` + `openxiangda-permission-settings` + `openxiangda-page` | 先确认公开范围、外部角色、ticket、grants;再声明 `routes` + `public-access` 并用 `PublicAccessGate` |
|
|
33
34
|
| 看应用结构 / 快照 / 对比 / 诊断 / 排查 / 报错 | `openxiangda-inspect` | `openxiangda app snapshot <APP_XXX> --profile <name> --json` |
|
|
34
35
|
| 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
|
|
35
36
|
| 多表只读联表 / 固定口径统计 / 强实时复杂查询 / 看板指标 | `openxiangda-form` (data view) | declare `src/resources/data-views/<code>.json` with `storageMode` → `resource publish` |
|
|
@@ -44,6 +45,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
44
45
|
- ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
|
|
45
46
|
- ✅ 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
47
|
- ✅ 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.
|
|
48
|
+
- ✅ For public/no-login access requirements, run the public access design gate before coding: public routes, external role codes, guest vs ticket, form/dataView/function/connector grants, and backend permission groups must be confirmed. New React SPA apps use `/view/:appType/public/*`, `src/resources/routes/`, `src/resources/public-access/`, and `PublicAccessGate`.
|
|
47
49
|
- ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
|
|
48
50
|
- ✅ 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.
|
|
49
51
|
- ✅ 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.
|
|
@@ -58,6 +60,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
58
60
|
- ❌ Running a full publish after editing one file. Default to `--changed --dry-run` → `--changed`, or targeted `--page` / `--form`.
|
|
59
61
|
- ❌ Storing tokens, AK, SK, or third-party API secrets in project files. Shared env (`APP_OSS_*`, feedback robot) goes to `~/.openxiangda/.env`.
|
|
60
62
|
- ❌ Raw native HTML form controls in AI-authored workspace code (`<input>`, `<select>`, `<textarea>`, file inputs, hand-written pickers, hand-written upload controls). They are allowed only inside OpenXiangda SDK/platform component internals.
|
|
63
|
+
- ❌ Using `?publicAccess=guest` or form `publicAccess` settings for new React SPA apps. Those are legacy `sy-lowcode-view` compatibility only.
|
|
61
64
|
|
|
62
65
|
## Platform Routing
|
|
63
66
|
|
|
@@ -145,6 +148,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
145
148
|
- 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.
|
|
146
149
|
- 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
150
|
- 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.
|
|
151
|
+
- For external/no-login pages in React SPA apps, declare `src/resources/routes/<code>.json` and `src/resources/public-access/<code>.json`, put the page under `/view/:appType/public/*`, and use `PublicAccessGate` or `createPublicAccessClient`. Public guest access to forms, dataViews, functions, and connectors is denied unless policy `grants` explicitly names the resource; backend permission groups still apply.
|
|
148
152
|
- 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.
|
|
149
153
|
- 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.
|
|
150
154
|
- Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` for diagnosis before changing an existing app.
|
|
@@ -25,6 +25,7 @@ This design flow is for applications built on OpenXiangda. The output is not a g
|
|
|
25
25
|
- automations, JS_CODE V2 nodes, and App Functions
|
|
26
26
|
- notifications and connectors
|
|
27
27
|
- application login/auth methods, registration policy, identity matching, and provider boundaries
|
|
28
|
+
- public access routes, external roles, ticket strategy, and explicit grants
|
|
28
29
|
- publish and acceptance steps
|
|
29
30
|
|
|
30
31
|
The design is complete only when another agent can implement it without deciding major architecture tradeoffs.
|
|
@@ -170,6 +171,25 @@ Ask these before generating any login/auth design:
|
|
|
170
171
|
|
|
171
172
|
Default: registration is rejected; provider functions return identity assertions only; platform auth service owns account creation, binding, permission assignment, cookies, and tokens.
|
|
172
173
|
|
|
174
|
+
### Public Access
|
|
175
|
+
|
|
176
|
+
Ask these before designing any page or data that external people can access without normal login:
|
|
177
|
+
|
|
178
|
+
- Which exact routes are public? Use `/view/:appType/public/*` for new React SPA apps.
|
|
179
|
+
- What can anonymous/external users do: view a page only, submit a form, query a dataView, invoke a function, or call a connector-backed action?
|
|
180
|
+
- Which virtual external role codes should represent these visitors, for example `external_visitor`, `external_applicant`, or `external_ticket_holder`?
|
|
181
|
+
- Is ordinary guest mode enough, or does the link need `mode: "ticket"` with expiry/single-use behavior?
|
|
182
|
+
- Which forms/dataViews/functions/connectors are explicitly granted? Everything not listed in `grants` is denied.
|
|
183
|
+
- Which backend permission groups must also include the external role codes?
|
|
184
|
+
- Is the page safe for search/share, or should the URL contain a generated ticket?
|
|
185
|
+
|
|
186
|
+
Recommended default if the user is unsure:
|
|
187
|
+
|
|
188
|
+
- Public marketing/info pages: `mode: "guest"`, no data grants.
|
|
189
|
+
- Public form submission: `mode: "guest"` plus one form grant and a submit permission group for `external_visitor`.
|
|
190
|
+
- Sensitive lookup or status query: `mode: "ticket"` plus one dataView/function grant, short TTL, and a read-only permission group.
|
|
191
|
+
- Never use old `?publicAccess=guest` for new React SPA apps; it is legacy `sy-lowcode-view` compatibility only.
|
|
192
|
+
|
|
173
193
|
### UI Design
|
|
174
194
|
|
|
175
195
|
Ask whether to use Product Design or another design skill for:
|
|
@@ -238,6 +258,15 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
238
258
|
- 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
259
|
- New-user registration must be explicit, with default roles and identity conflict behavior documented.
|
|
240
260
|
|
|
261
|
+
### Public Access
|
|
262
|
+
|
|
263
|
+
- Route resources live under `src/resources/routes/` and use `publicAccess: "guest"` or `"ticket"` only for `/view/:appType/public/*` paths.
|
|
264
|
+
- Public policies live under `src/resources/public-access/`.
|
|
265
|
+
- Policy `externalRoleCodes` are virtual role codes carried by the scoped public token; use the same codes in page/form/dataView permission groups.
|
|
266
|
+
- Policy `grants` must list every form/dataView/function/connector the public page can access. Do not rely on broad guest permissions.
|
|
267
|
+
- Use `PublicAccessGate` in React routes or `createPublicAccessClient` for custom bootstrapping.
|
|
268
|
+
- Old `?publicAccess=guest` and form `publicAccess` settings are legacy compatibility and should not appear in new-app designs.
|
|
269
|
+
|
|
241
270
|
## Final Document Template
|
|
242
271
|
|
|
243
272
|
```markdown
|