openxiangda 1.0.132 → 1.0.134
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 +46 -1
- package/lib/cli.js +287 -8
- package/openxiangda-skills/SKILL.md +7 -5
- package/openxiangda-skills/references/connector-resources.md +1 -1
- package/openxiangda-skills/references/notifications.md +86 -0
- package/openxiangda-skills/references/openxiangda-api.md +149 -0
- package/openxiangda-skills/references/pages/page-sdk.md +37 -0
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +16 -1
- package/openxiangda-skills/references/workflow-v3.md +1 -1
- package/package.json +1 -1
- package/packages/sdk/dist/{ProcessPreview-B2-umzSY.d.mts → ProcessPreview-Dfc4-wIq.d.mts} +2 -3
- package/packages/sdk/dist/{ProcessPreview-B2-umzSY.d.ts → ProcessPreview-Dfc4-wIq.d.ts} +2 -3
- package/packages/sdk/dist/components/index.d.mts +35 -36
- package/packages/sdk/dist/components/index.d.ts +35 -36
- package/packages/sdk/dist/runtime/index.cjs +168 -2
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -4
- package/packages/sdk/dist/runtime/index.d.ts +3 -4
- package/packages/sdk/dist/runtime/index.mjs +168 -2
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +168 -2
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +227 -5
- package/packages/sdk/dist/runtime/react.d.ts +227 -5
- package/packages/sdk/dist/runtime/react.mjs +168 -2
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/dist/workflow/index.cjs +70 -12
- package/packages/sdk/dist/workflow/index.cjs.map +1 -1
- package/packages/sdk/dist/workflow/index.mjs +70 -12
- package/packages/sdk/dist/workflow/index.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -108,6 +108,9 @@ openxiangda auth-config methods --json
|
|
|
108
108
|
openxiangda function invoke submit_public_registration --body-json '{"input":{}}'
|
|
109
109
|
openxiangda connector invoke sms.sendCode --body-json '{"body":{"phone":"13800000000"}}'
|
|
110
110
|
openxiangda notification preview public_register_notice --body-json '{"payload":{"title":"测试"}}'
|
|
111
|
+
openxiangda organization capabilities --profile dev --json
|
|
112
|
+
openxiangda organization department-list --profile dev --json
|
|
113
|
+
openxiangda organization account-list --profile dev --keyword alice --json
|
|
111
114
|
openxiangda permission snapshot --form-codes customer,orders --json
|
|
112
115
|
openxiangda permission audit --json
|
|
113
116
|
```
|
|
@@ -306,7 +309,49 @@ const PublicAccessError = ({ error }: { error: { message?: string } }) => (
|
|
|
306
309
|
|
|
307
310
|
多表只读查询和固定口径统计优先声明 `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`,不需要刷新。
|
|
308
311
|
|
|
309
|
-
后端业务逻辑优先声明为 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.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.roleCodes` 显式声明允许的当前应用角色。自动化/流程内部调用走服务端受控上下文。
|
|
312
|
+
后端业务逻辑优先声明为 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.roleCodes` 显式声明允许的当前应用角色。自动化/流程内部调用走服务端受控上下文。
|
|
313
|
+
|
|
314
|
+
平台部门和账号管理走 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 })`。
|
|
315
|
+
|
|
316
|
+
CLI 写操作必须加 `--force`:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
openxiangda organization capabilities --profile dev --json
|
|
320
|
+
openxiangda organization department-list --profile dev --json
|
|
321
|
+
openxiangda organization department-create --profile dev --body-json '{"name":"销售部","parentId":"dept-root"}' --force
|
|
322
|
+
openxiangda organization department-update dept-sales --profile dev --body-json '{"name":"华东销售部"}' --force
|
|
323
|
+
openxiangda organization account-list --profile dev --keyword alice --page 1 --page-size 20 --json
|
|
324
|
+
openxiangda organization account-create --profile dev --body-json '{"id":"user-alice","username":"alice","password":"P@ssw0rd","name":"Alice","departmentIds":["dept-sales"],"affiliatedDepartmentId":"dept-sales"}' --force
|
|
325
|
+
openxiangda organization account-update user-alice --profile dev --body-json '{"name":"Alice Chen","departmentIds":["dept-sales"]}' --force
|
|
326
|
+
openxiangda organization account-reset-password user-alice --profile dev --body-json '{"newPassword":"NewP@ssw0rd"}' --force
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
页面 SDK 和 App Function 使用同一套后端权限:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
const capabilities = await sdk.organization.capabilities()
|
|
333
|
+
if (!capabilities.result?.canManage) {
|
|
334
|
+
throw new Error("当前账号没有组织账号管理权限")
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await sdk.organization.departments.create({
|
|
338
|
+
name: "销售部",
|
|
339
|
+
parentId: "dept-root",
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await sdk.organization.accounts.create({
|
|
343
|
+
id: "user-alice",
|
|
344
|
+
username: "alice",
|
|
345
|
+
password: initialPassword,
|
|
346
|
+
name: "Alice",
|
|
347
|
+
departmentIds: ["dept-sales"],
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// App Function / trusted JS uses ctx.organization with the same permission check.
|
|
351
|
+
await ctx.organization.accounts.resetPassword("user-alice", {
|
|
352
|
+
newPassword,
|
|
353
|
+
})
|
|
354
|
+
```
|
|
310
355
|
|
|
311
356
|
应用登录能力通过 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/频率/失败次数、审计字段和错误文案策略。
|
|
312
357
|
|
package/lib/cli.js
CHANGED
|
@@ -86,6 +86,7 @@ async function main(argv) {
|
|
|
86
86
|
if (command === 'function') return appFunction(rest);
|
|
87
87
|
if (command === 'connector') return connector(rest);
|
|
88
88
|
if (command === 'notification') return notification(rest);
|
|
89
|
+
if (command === 'organization') return organization(rest);
|
|
89
90
|
if (command === 'permission') return permission(rest);
|
|
90
91
|
if (command === 'settings') return settings(rest);
|
|
91
92
|
if (command === 'resource') return resource(rest);
|
|
@@ -150,6 +151,7 @@ Usage:
|
|
|
150
151
|
openxiangda function list|get|create|update|upsert|delete|invoke [--json-file file]
|
|
151
152
|
openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [--json-file file]
|
|
152
153
|
openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send
|
|
154
|
+
openxiangda organization capabilities|department-list|department-get|department-create|department-update|account-list|account-get|account-create|account-update|account-reset-password
|
|
153
155
|
openxiangda permission role-list|role-create|role-update|role-delete|role-bind|snapshot|audit
|
|
154
156
|
openxiangda permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind
|
|
155
157
|
openxiangda permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind
|
|
@@ -2955,10 +2957,13 @@ async function notification(args) {
|
|
|
2955
2957
|
const { flags, positional } = parseArgs(rest);
|
|
2956
2958
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2957
2959
|
print([
|
|
2958
|
-
'用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send',
|
|
2960
|
+
'用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send|capabilities|dingding-preview|dingding-send',
|
|
2959
2961
|
' preview <notificationType|templateCode> [--form-code code|--form-uuid FORM_XXX] --body-json \'{"payload":{"title":"测试"}}\'',
|
|
2960
2962
|
' send <notificationType> --body-json \'{"recipientId":"USER_ID","payload":{"title":"测试"}}\' --force',
|
|
2961
2963
|
' batch-send <notificationType> --body-json \'{"recipients":[{"recipientId":"USER_ID","payload":{}}]}\' --force',
|
|
2964
|
+
' capabilities --json',
|
|
2965
|
+
' dingding-preview <notificationType> [--template-code code|--template-id id] --body-json \'{"payload":{"title":"测试"}}\'',
|
|
2966
|
+
' dingding-send <notificationType> --body-json \'{"recipientId":"USER_ID","payload":{"title":"测试"}}\' --force',
|
|
2962
2967
|
].join('\n'));
|
|
2963
2968
|
return;
|
|
2964
2969
|
}
|
|
@@ -3053,6 +3058,27 @@ async function notification(args) {
|
|
|
3053
3058
|
path: apiPathWithPostDeleteAction(`${appPrefix}/type-configs/${encodeURIComponent(code)}`),
|
|
3054
3059
|
});
|
|
3055
3060
|
}
|
|
3061
|
+
if (subcommand === 'capabilities') {
|
|
3062
|
+
return runDirectRequest(config, target, flags, {
|
|
3063
|
+
method: 'GET',
|
|
3064
|
+
path: `${appPrefix}/dingtalk/capabilities`,
|
|
3065
|
+
strictEnvelope: true,
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
if (subcommand === 'dingding-preview' || subcommand === 'dingding-send') {
|
|
3069
|
+
if (subcommand === 'dingding-send' && !flags.force) {
|
|
3070
|
+
fail('openxiangda notification dingding-send 是发送动作,必须加 --force');
|
|
3071
|
+
}
|
|
3072
|
+
const code = positional[0] || flags.code;
|
|
3073
|
+
const rawBody = readDirectJsonBody(flags, `notification ${subcommand}`, { optional: true });
|
|
3074
|
+
const body = buildDingTalkNotificationActionBody(target, code, rawBody, flags);
|
|
3075
|
+
return runDirectRequest(config, target, flags, {
|
|
3076
|
+
method: 'POST',
|
|
3077
|
+
path: `${appPrefix}/dingtalk/${subcommand === 'dingding-send' ? 'send' : 'preview'}`,
|
|
3078
|
+
body,
|
|
3079
|
+
strictEnvelope: true,
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3056
3082
|
if (['preview', 'send', 'batch-send'].includes(subcommand)) {
|
|
3057
3083
|
if ((subcommand === 'send' || subcommand === 'batch-send') && !flags.force) {
|
|
3058
3084
|
fail(`openxiangda notification ${subcommand} 是发送动作,必须加 --force`);
|
|
@@ -3077,7 +3103,170 @@ async function notification(args) {
|
|
|
3077
3103
|
});
|
|
3078
3104
|
}
|
|
3079
3105
|
|
|
3080
|
-
fail('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send');
|
|
3106
|
+
fail('用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send|capabilities|dingding-preview|dingding-send');
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
async function organization(args) {
|
|
3110
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
3111
|
+
const { flags, positional } = parseArgs(rest);
|
|
3112
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
3113
|
+
print([
|
|
3114
|
+
'用法: openxiangda organization capabilities|department-list|department-get|department-create|department-update|account-list|account-get|account-create|account-update|account-reset-password',
|
|
3115
|
+
' capabilities --json',
|
|
3116
|
+
' department-list --json',
|
|
3117
|
+
' department-get <departmentId> --json',
|
|
3118
|
+
' department-create --body-json \'{"name":"销售部"}\' --force',
|
|
3119
|
+
' department-update <departmentId> --body-json \'{"name":"华东销售"}\' --force',
|
|
3120
|
+
' account-list [--keyword text|--department-ids id1,id2] --json',
|
|
3121
|
+
' account-get <userId> --json',
|
|
3122
|
+
' account-create --body-json \'{"username":"u1","password":"P@ssw0rd","name":"张三"}\' --force',
|
|
3123
|
+
' account-update <userId> --body-json \'{"name":"张三"}\' --force',
|
|
3124
|
+
' account-reset-password <userId> --body-json \'{"newPassword":"P@ssw0rd"}\' --force',
|
|
3125
|
+
].join('\n'));
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
const config = loadConfig();
|
|
3130
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
3131
|
+
const appPrefix = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/organization`;
|
|
3132
|
+
|
|
3133
|
+
if (subcommand === 'capabilities') {
|
|
3134
|
+
return runDirectRequest(config, target, flags, {
|
|
3135
|
+
method: 'GET',
|
|
3136
|
+
path: `${appPrefix}/capabilities`,
|
|
3137
|
+
strictEnvelope: true,
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
if (subcommand === 'department-list') {
|
|
3142
|
+
return runDirectRequest(config, target, flags, {
|
|
3143
|
+
method: 'GET',
|
|
3144
|
+
path: `${appPrefix}/departments`,
|
|
3145
|
+
strictEnvelope: true,
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
if (subcommand === 'department-get') {
|
|
3150
|
+
const departmentId = positional[0] || flags['department-id'] || flags.id;
|
|
3151
|
+
if (!departmentId) fail('用法: openxiangda organization department-get <departmentId>');
|
|
3152
|
+
return runDirectRequest(config, target, flags, {
|
|
3153
|
+
method: 'GET',
|
|
3154
|
+
path: `${appPrefix}/departments/${encodeURIComponent(departmentId)}`,
|
|
3155
|
+
strictEnvelope: true,
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
if (subcommand === 'department-create') {
|
|
3160
|
+
if (!flags.force) fail('openxiangda organization department-create 是写操作,必须加 --force');
|
|
3161
|
+
const body = readDirectJsonBody(flags, 'organization department-create');
|
|
3162
|
+
return runDirectRequest(config, target, flags, {
|
|
3163
|
+
method: 'POST',
|
|
3164
|
+
path: `${appPrefix}/departments`,
|
|
3165
|
+
body,
|
|
3166
|
+
strictEnvelope: true,
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
if (subcommand === 'department-update') {
|
|
3171
|
+
if (!flags.force) fail('openxiangda organization department-update 是写操作,必须加 --force');
|
|
3172
|
+
const departmentId = positional[0] || flags['department-id'] || flags.id;
|
|
3173
|
+
if (!departmentId) fail('用法: openxiangda organization department-update <departmentId> --body-json ... --force');
|
|
3174
|
+
const body = readDirectJsonBody(flags, 'organization department-update');
|
|
3175
|
+
return runDirectRequest(config, target, flags, {
|
|
3176
|
+
method: 'POST',
|
|
3177
|
+
path: `${appPrefix}/departments/${encodeURIComponent(departmentId)}`,
|
|
3178
|
+
body,
|
|
3179
|
+
strictEnvelope: true,
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
if (subcommand === 'account-list') {
|
|
3184
|
+
return runDirectRequest(config, target, flags, {
|
|
3185
|
+
method: 'GET',
|
|
3186
|
+
path: apiPathWithQuery(`${appPrefix}/accounts`, {
|
|
3187
|
+
ids: flags.ids,
|
|
3188
|
+
departmentIds: flags['department-ids'],
|
|
3189
|
+
keyword: flags.keyword,
|
|
3190
|
+
name: flags.name,
|
|
3191
|
+
username: flags.username,
|
|
3192
|
+
phone: flags.phone,
|
|
3193
|
+
email: flags.email,
|
|
3194
|
+
jobNumber: flags['job-number'],
|
|
3195
|
+
page: flags.page,
|
|
3196
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
3197
|
+
}),
|
|
3198
|
+
strictEnvelope: true,
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
if (subcommand === 'account-get') {
|
|
3203
|
+
const userId = positional[0] || flags['user-id'] || flags.id;
|
|
3204
|
+
if (!userId) fail('用法: openxiangda organization account-get <userId>');
|
|
3205
|
+
return runDirectRequest(config, target, flags, {
|
|
3206
|
+
method: 'GET',
|
|
3207
|
+
path: `${appPrefix}/accounts/${encodeURIComponent(userId)}`,
|
|
3208
|
+
strictEnvelope: true,
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
if (subcommand === 'account-create') {
|
|
3213
|
+
if (!flags.force) fail('openxiangda organization account-create 是写操作,必须加 --force');
|
|
3214
|
+
const body = readDirectJsonBody(flags, 'organization account-create');
|
|
3215
|
+
return runDirectRequest(config, target, flags, {
|
|
3216
|
+
method: 'POST',
|
|
3217
|
+
path: `${appPrefix}/accounts`,
|
|
3218
|
+
body,
|
|
3219
|
+
strictEnvelope: true,
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
if (subcommand === 'account-update') {
|
|
3224
|
+
if (!flags.force) fail('openxiangda organization account-update 是写操作,必须加 --force');
|
|
3225
|
+
const userId = positional[0] || flags['user-id'] || flags.id;
|
|
3226
|
+
if (!userId) fail('用法: openxiangda organization account-update <userId> --body-json ... --force');
|
|
3227
|
+
const body = readDirectJsonBody(flags, 'organization account-update');
|
|
3228
|
+
if (Object.prototype.hasOwnProperty.call(body || {}, 'password')) {
|
|
3229
|
+
fail('account-update 不允许包含 password,请使用 account-reset-password');
|
|
3230
|
+
}
|
|
3231
|
+
return runDirectRequest(config, target, flags, {
|
|
3232
|
+
method: 'POST',
|
|
3233
|
+
path: `${appPrefix}/accounts/${encodeURIComponent(userId)}`,
|
|
3234
|
+
body,
|
|
3235
|
+
strictEnvelope: true,
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
if (subcommand === 'account-reset-password') {
|
|
3240
|
+
if (!flags.force) fail('openxiangda organization account-reset-password 是密码写操作,必须加 --force');
|
|
3241
|
+
const userId = positional[0] || flags['user-id'] || flags.id;
|
|
3242
|
+
if (!userId) fail('用法: openxiangda organization account-reset-password <userId> --body-json ... --force');
|
|
3243
|
+
const body = readDirectJsonBody(flags, 'organization account-reset-password');
|
|
3244
|
+
if (!body.newPassword) fail('account-reset-password body 缺少 newPassword');
|
|
3245
|
+
return runDirectRequest(config, target, flags, {
|
|
3246
|
+
method: 'POST',
|
|
3247
|
+
path: `${appPrefix}/accounts/${encodeURIComponent(userId)}/password/reset`,
|
|
3248
|
+
body,
|
|
3249
|
+
strictEnvelope: true,
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
fail('用法: openxiangda organization capabilities|department-list|department-get|department-create|department-update|account-list|account-get|account-create|account-update|account-reset-password');
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
function buildDingTalkNotificationActionBody(target, code, rawBody = {}, flags = {}) {
|
|
3257
|
+
const body = { ...(rawBody || {}) };
|
|
3258
|
+
if (flags['template-code'] && !body.templateCode) body.templateCode = flags['template-code'];
|
|
3259
|
+
if (flags['template-id'] && !body.templateId) body.templateId = flags['template-id'];
|
|
3260
|
+
if (code && !body.notificationType && !body.templateCode && !body.templateId && !body.code) {
|
|
3261
|
+
body.notificationType = code;
|
|
3262
|
+
}
|
|
3263
|
+
const formUuid =
|
|
3264
|
+
body.formUuid ||
|
|
3265
|
+
flags['form-uuid'] ||
|
|
3266
|
+
resolveOptionalFormUuid(target.bound, body.formCode || flags['form-code']);
|
|
3267
|
+
if (formUuid) body.formUuid = formUuid;
|
|
3268
|
+
delete body.formCode;
|
|
3269
|
+
return body;
|
|
3081
3270
|
}
|
|
3082
3271
|
|
|
3083
3272
|
async function normalizeNotificationPreviewBody(config, target, code, rawBody = {}, flags = {}) {
|
|
@@ -4570,6 +4759,7 @@ async function commands(args) {
|
|
|
4570
4759
|
'function list|get|create|update|upsert|delete|invoke',
|
|
4571
4760
|
'connector list|get|create|update|upsert|delete|invoke|download-test',
|
|
4572
4761
|
'notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send',
|
|
4762
|
+
'organization capabilities|department-list|department-get|department-create|department-update|account-list|account-get|account-create|account-update|account-reset-password',
|
|
4573
4763
|
'permission role-list|role-create|role-update|role-delete|role-bind|role-users|role-add-users|snapshot|audit',
|
|
4574
4764
|
'permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind',
|
|
4575
4765
|
'permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind|form-summary|menu-permissions',
|
|
@@ -6382,6 +6572,13 @@ function validateNotificationChannels(label, channelsConfig, errors) {
|
|
|
6382
6572
|
errors.push(`${label}: 不支持的通知渠道 ${channel}`);
|
|
6383
6573
|
}
|
|
6384
6574
|
validateNoSensitiveNotificationConfig(`${label}.channelsConfig.${channel}`, config, errors);
|
|
6575
|
+
if (channel === 'dingding') {
|
|
6576
|
+
validateDingTalkNotificationConfig(
|
|
6577
|
+
`${label}.channelsConfig.${channel}`,
|
|
6578
|
+
config,
|
|
6579
|
+
errors
|
|
6580
|
+
);
|
|
6581
|
+
}
|
|
6385
6582
|
}
|
|
6386
6583
|
}
|
|
6387
6584
|
|
|
@@ -6396,6 +6593,62 @@ function validateNoSensitiveNotificationConfig(label, value, errors) {
|
|
|
6396
6593
|
}
|
|
6397
6594
|
}
|
|
6398
6595
|
|
|
6596
|
+
function validateDingTalkNotificationConfig(label, value, errors) {
|
|
6597
|
+
const config = value?.config || {};
|
|
6598
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) return;
|
|
6599
|
+
if (
|
|
6600
|
+
config.deliveryMode !== undefined &&
|
|
6601
|
+
!['card_preferred', 'card_only', 'work_notice_only'].includes(
|
|
6602
|
+
String(config.deliveryMode)
|
|
6603
|
+
)
|
|
6604
|
+
) {
|
|
6605
|
+
errors.push(`${label}.config.deliveryMode: 只能是 card_preferred、card_only 或 work_notice_only`);
|
|
6606
|
+
}
|
|
6607
|
+
const card = config.card || {};
|
|
6608
|
+
if (card && typeof card === 'object' && !Array.isArray(card)) {
|
|
6609
|
+
if (
|
|
6610
|
+
card.mode !== undefined &&
|
|
6611
|
+
!['standard', 'custom'].includes(String(card.mode))
|
|
6612
|
+
) {
|
|
6613
|
+
errors.push(`${label}.config.card.mode: 只能是 standard 或 custom`);
|
|
6614
|
+
}
|
|
6615
|
+
if (
|
|
6616
|
+
card.mode === 'custom' &&
|
|
6617
|
+
card.paramMap !== undefined &&
|
|
6618
|
+
(!card.paramMap ||
|
|
6619
|
+
typeof card.paramMap !== 'object' ||
|
|
6620
|
+
Array.isArray(card.paramMap))
|
|
6621
|
+
) {
|
|
6622
|
+
errors.push(`${label}.config.card.paramMap: custom 卡片 paramMap 必须是对象`);
|
|
6623
|
+
}
|
|
6624
|
+
if (
|
|
6625
|
+
card.fieldConfigs !== undefined &&
|
|
6626
|
+
!Array.isArray(card.fieldConfigs)
|
|
6627
|
+
) {
|
|
6628
|
+
errors.push(`${label}.config.card.fieldConfigs: 必须是数组`);
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
function resolveNotificationChannelEnvReferences(value) {
|
|
6634
|
+
if (!value || typeof value !== 'object') return value;
|
|
6635
|
+
if (Array.isArray(value)) {
|
|
6636
|
+
return value.map(item => resolveNotificationChannelEnvReferences(item));
|
|
6637
|
+
}
|
|
6638
|
+
const next = {};
|
|
6639
|
+
for (const [key, item] of Object.entries(value)) {
|
|
6640
|
+
if (
|
|
6641
|
+
['cardTemplateId', 'dingTalkTemplateId'].includes(key) &&
|
|
6642
|
+
isEnvReference(item)
|
|
6643
|
+
) {
|
|
6644
|
+
next[key] = resolveEnvReference(item, `DingTalk ${key}`);
|
|
6645
|
+
} else {
|
|
6646
|
+
next[key] = resolveNotificationChannelEnvReferences(item);
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
return next;
|
|
6650
|
+
}
|
|
6651
|
+
|
|
6399
6652
|
function resourceLabel(kind, item) {
|
|
6400
6653
|
return `${kind}:${item.code || item.__source || item.__index || '?'}`;
|
|
6401
6654
|
}
|
|
@@ -7634,13 +7887,13 @@ async function publishAutomationResources(config, target, automations, result, o
|
|
|
7634
7887
|
|
|
7635
7888
|
async function publishFunctionResources(config, target, functions, result, options = {}) {
|
|
7636
7889
|
for (const functionItem of functions) {
|
|
7637
|
-
const code = functionItem.code || functionItem.functionCode;
|
|
7890
|
+
const code = functionItem.code || functionItem.functionCode || functionItem.resourceCode;
|
|
7638
7891
|
if (shouldSkipNoopResource(options, 'function', code)) {
|
|
7639
7892
|
recordNoopResource(result, 'function', code, options);
|
|
7640
7893
|
continue;
|
|
7641
7894
|
}
|
|
7642
7895
|
const existing = await findExistingFunction(config, target, code);
|
|
7643
|
-
|
|
7896
|
+
let definitionJson = await resolveManifestJson(
|
|
7644
7897
|
config,
|
|
7645
7898
|
target.profileName,
|
|
7646
7899
|
functionItem,
|
|
@@ -7648,6 +7901,7 @@ async function publishFunctionResources(config, target, functions, result, optio
|
|
|
7648
7901
|
'definitionFile'
|
|
7649
7902
|
);
|
|
7650
7903
|
applyResourceBindingsToRuntimeDefinition(target.bound, functionItem, definitionJson);
|
|
7904
|
+
definitionJson = normalizeFunctionDefinitionJson(functionItem, definitionJson);
|
|
7651
7905
|
const body = stripUndefinedValues({
|
|
7652
7906
|
code,
|
|
7653
7907
|
name: functionItem.name || definitionJson.name || code,
|
|
@@ -9189,7 +9443,9 @@ function normalizeNotificationTemplateManifest(bound, template) {
|
|
|
9189
9443
|
name: template.name || template.code,
|
|
9190
9444
|
content: template.content || '',
|
|
9191
9445
|
description: template.description || '',
|
|
9192
|
-
channelsConfig:
|
|
9446
|
+
channelsConfig: resolveNotificationChannelEnvReferences(
|
|
9447
|
+
template.channelsConfig
|
|
9448
|
+
),
|
|
9193
9449
|
level,
|
|
9194
9450
|
formUuid,
|
|
9195
9451
|
priority: template.priority,
|
|
@@ -9970,21 +10226,44 @@ function automationEquals(target, desired, existing) {
|
|
|
9970
10226
|
|
|
9971
10227
|
function functionEquals(target, desired, existing) {
|
|
9972
10228
|
if (!existing) return false;
|
|
9973
|
-
|
|
10229
|
+
let desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
|
|
9974
10230
|
applyResourceBindingsToRuntimeDefinition(target.bound, desired, desiredDefinition);
|
|
10231
|
+
desiredDefinition = normalizeFunctionDefinitionJson(desired, desiredDefinition);
|
|
9975
10232
|
const desiredStatus = desired.status === undefined ? 'active' : String(desired.status || 'active');
|
|
10233
|
+
const existingResourceBindings =
|
|
10234
|
+
existing.resourceBindings ??
|
|
10235
|
+
existing.resourceBindingsJson ??
|
|
10236
|
+
existing.definitionJson?.resourceBindings ??
|
|
10237
|
+
{};
|
|
9976
10238
|
return (
|
|
9977
|
-
String(existing.code || existing.functionCode || existing.resourceCode || '') === String(desired.code || desired.functionCode || '') &&
|
|
10239
|
+
String(existing.code || existing.functionCode || existing.resourceCode || '') === String(desired.code || desired.functionCode || desired.resourceCode || '') &&
|
|
9978
10240
|
String(existing.name || '') === String(desired.name || desiredDefinition.name || desired.code || '') &&
|
|
9979
10241
|
String(existing.description || '') === String(
|
|
9980
10242
|
desired.description !== undefined ? desired.description : desiredDefinition.description || ''
|
|
9981
10243
|
) &&
|
|
9982
10244
|
jsonEqualsForPlan(existing.definitionJson, desiredDefinition, desired.__dir) &&
|
|
9983
|
-
jsonEqualsForPlan(
|
|
10245
|
+
jsonEqualsForPlan(existingResourceBindings, desiredDefinition.resourceBindings || {}, desired.__dir) &&
|
|
9984
10246
|
String(existing.status || 'active') === desiredStatus
|
|
9985
10247
|
);
|
|
9986
10248
|
}
|
|
9987
10249
|
|
|
10250
|
+
function normalizeFunctionDefinitionJson(functionItem, definitionJson) {
|
|
10251
|
+
const definition = clonePlainJson(definitionJson || {});
|
|
10252
|
+
const functionCode =
|
|
10253
|
+
definition.functionCode ||
|
|
10254
|
+
functionItem.functionCode ||
|
|
10255
|
+
functionItem.resourceCode ||
|
|
10256
|
+
functionItem.code;
|
|
10257
|
+
return stripUndefinedValues({
|
|
10258
|
+
...definition,
|
|
10259
|
+
kind: definition.kind || 'app_function',
|
|
10260
|
+
version: definition.version || 'function_v1',
|
|
10261
|
+
...(functionCode ? { functionCode: String(functionCode) } : {}),
|
|
10262
|
+
runtimeMode: definition.runtimeMode || 'trusted_node',
|
|
10263
|
+
sourceType: definition.sourceType || 'file_snapshot',
|
|
10264
|
+
});
|
|
10265
|
+
}
|
|
10266
|
+
|
|
9988
10267
|
function authConfigEquals(desired, existing) {
|
|
9989
10268
|
if (!existing) return false;
|
|
9990
10269
|
const expected = normalizeAuthConfigManifest(desired);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: openxiangda
|
|
3
|
-
description: "Use OpenXiangda for any private low-code platform or sy-lowcode-app-workspace work: architecture design, app creation, forms, pages, resources, workflows, automations, JS_CODE, permissions, data views, connectors, notifications, publishing, deployment, diagnosis, feedback, profile/login state, and CLI commands. Trigger on OpenXiangda, 享搭, 私有化低代码, 低代码平台, .openxiangda/state.json, app-workspace.config.ts, or openxiangda CLI usage."
|
|
3
|
+
description: "Use OpenXiangda for any private low-code platform or sy-lowcode-app-workspace work: architecture design, app creation, forms, pages, resources, workflows, automations, JS_CODE, permissions, organization/account management, data views, connectors, notifications, publishing, deployment, diagnosis, feedback, profile/login state, and CLI commands. Trigger on OpenXiangda, 享搭, 私有化低代码, 低代码平台, .openxiangda/state.json, app-workspace.config.ts, or openxiangda CLI usage."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# OpenXiangda
|
|
@@ -30,6 +30,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
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
|
+
| 平台部门 / 平台账号 / 重置密码 / 组织账号管理 | `openxiangda-architecture-design` + `openxiangda-page` | check `openxiangda organization capabilities --json`; use `sdk.organization` / `ctx.organization` only after `app:organization:manage` is granted |
|
|
33
34
|
| 外部人员无需登录访问 / 公开报名 / 公开查询 / public page | `openxiangda-architecture-design` + `openxiangda-permission-settings` + `openxiangda-page` | 先确认公开范围、外部角色、ticket、grants;再声明 `routes` + `public-access`,并用 `OpenXiangdaProvider` + `OpenXiangdaPageProvider` + `PublicAccessGate` |
|
|
34
35
|
| 看应用结构 / 快照 / 对比 / 诊断 / 排查 / 报错 | `openxiangda-inspect` | `openxiangda app snapshot <APP_XXX> --profile <name> --json` |
|
|
35
36
|
| 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
|
|
@@ -48,7 +49,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
48
49
|
- ✅ 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.
|
|
49
50
|
- ✅ 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, backend permission groups, and visible loading/error fallback states must be confirmed. New React SPA apps use `/view/:appType/public/*`, `src/resources/routes/`, `src/resources/public-access/`, and `PublicAccessGate`; route children that use Page SDK hooks must be inside `OpenXiangdaProvider` + `OpenXiangdaPageProvider`.
|
|
50
51
|
- ✅ Public/no-login validation must inspect the JSON envelope, not only HTTP status. HTTP 200 with `code: "PUBLIC_GRANT_DENIED"` is a denied request; success requires `code` `200`, `"200"`, or compatible `0`, and no `success: false`.
|
|
51
|
-
- ✅ Use first-class resource CLI commands for discovery, diagnosis, and small live fixes: `route`, `public-access`, `auth-config`, `function`, `connector`, `notification`, `data-view`, `menu`, and `permission`. For multi-resource formal development, still prefer `src/resources/**` + `openxiangda resource validate|plan|publish`. If a direct CLI write is intentionally used, add `--write-manifest` when the repository should stay the source of truth.
|
|
52
|
+
- ✅ Use first-class resource CLI commands for discovery, diagnosis, and small live fixes: `route`, `public-access`, `auth-config`, `function`, `connector`, `notification`, `organization`, `data-view`, `menu`, and `permission`. For multi-resource formal development, still prefer `src/resources/**` + `openxiangda resource validate|plan|publish`. If a direct CLI write is intentionally used, add `--write-manifest` when the repository should stay the source of truth.
|
|
52
53
|
- ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
|
|
53
54
|
- ✅ In React SPA apps, keep imports on public package entrypoints such as `openxiangda`, `openxiangda/runtime`, and `openxiangda/runtime/react`. The default template already groups large vendor chunks; if a bundle is too large, use route-level lazy loading and Vite `manualChunks`, not `openxiangda/packages/sdk/dist/...` internal paths.
|
|
54
55
|
- ✅ For image thumbnails or lightweight previews, use `imageCompression` on `ImageField` or `AttachmentField`. Keep the original `url` for the source file and use `thumbUrl` / `previewUrl` / `variants` for smaller images.
|
|
@@ -154,7 +155,8 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
154
155
|
- 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.
|
|
155
156
|
- 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.
|
|
156
157
|
- 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.
|
|
157
|
-
- 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.queryOne/queryMany/getById/createOne/updateOne/updateById`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, 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.
|
|
158
|
+
- 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.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.
|
|
159
|
+
- 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 })`.
|
|
158
160
|
- 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.
|
|
159
161
|
- 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`. The route tree must also include `OpenXiangdaPageProvider` inside `OpenXiangdaProvider` before any `usePageSdk()` / `usePageContext()` / `useDataSource()` call; otherwise pages throw `usePageSdkStore 必须在 PageProvider 内使用`. Always provide `fallback` and `errorFallback` for public routes so slow networks, missing tickets, or expired tickets do not render blank pages. Public guest access to forms, dataViews, functions, and connectors is denied unless policy `grants` explicitly names the resource; backend permission groups still apply.
|
|
160
162
|
- When verifying public access, parse the response body and reject string business errors such as `PUBLIC_GRANT_DENIED` even if the HTTP status is 200. Do not use `response.ok` alone as the success condition.
|
|
@@ -194,7 +196,7 @@ Load these references on demand instead of memorizing them. Subskills cite the s
|
|
|
194
196
|
|
|
195
197
|
Core CLI / state:
|
|
196
198
|
|
|
197
|
-
- `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields.
|
|
199
|
+
- `references/openxiangda-api.md` — `/openxiangda-api/v1` request and response fields, including app-scoped organization APIs.
|
|
198
200
|
- `references/architecture-design.md` — OpenXiangda architecture design gate, anti-pattern rejection, question gates, final design template, and black-box demo scenario.
|
|
199
201
|
- `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
|
|
200
202
|
- `references/connector-resources.md` — `src/resources` manifests, connector schema, App Function boundary, and platform runtime calls.
|
|
@@ -212,7 +214,7 @@ Form authoring:
|
|
|
212
214
|
Page authoring:
|
|
213
215
|
|
|
214
216
|
- `references/pages/workspace-structure.md` — `src/pages/<pageCode>/` layout and config.
|
|
215
|
-
- `references/pages/page-sdk.md` — `openxiangda/runtime` data, connector, data view, notification, and App Function APIs.
|
|
217
|
+
- `references/pages/page-sdk.md` — `openxiangda/runtime` data, connector, data view, notification, organization, and App Function APIs.
|
|
216
218
|
- `references/pages/publish-flow.md` — workspace publish steps for code pages.
|
|
217
219
|
|
|
218
220
|
Workflow / automation / permissions:
|
|
@@ -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`, 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. 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. 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.
|
|
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
|
|