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.
Files changed (30) hide show
  1. package/README.md +46 -1
  2. package/lib/cli.js +287 -8
  3. package/openxiangda-skills/SKILL.md +7 -5
  4. package/openxiangda-skills/references/connector-resources.md +1 -1
  5. package/openxiangda-skills/references/notifications.md +86 -0
  6. package/openxiangda-skills/references/openxiangda-api.md +149 -0
  7. package/openxiangda-skills/references/pages/page-sdk.md +37 -0
  8. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +16 -1
  9. package/openxiangda-skills/references/workflow-v3.md +1 -1
  10. package/package.json +1 -1
  11. package/packages/sdk/dist/{ProcessPreview-B2-umzSY.d.mts → ProcessPreview-Dfc4-wIq.d.mts} +2 -3
  12. package/packages/sdk/dist/{ProcessPreview-B2-umzSY.d.ts → ProcessPreview-Dfc4-wIq.d.ts} +2 -3
  13. package/packages/sdk/dist/components/index.d.mts +35 -36
  14. package/packages/sdk/dist/components/index.d.ts +35 -36
  15. package/packages/sdk/dist/runtime/index.cjs +168 -2
  16. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  17. package/packages/sdk/dist/runtime/index.d.mts +3 -4
  18. package/packages/sdk/dist/runtime/index.d.ts +3 -4
  19. package/packages/sdk/dist/runtime/index.mjs +168 -2
  20. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  21. package/packages/sdk/dist/runtime/react.cjs +168 -2
  22. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  23. package/packages/sdk/dist/runtime/react.d.mts +227 -5
  24. package/packages/sdk/dist/runtime/react.d.ts +227 -5
  25. package/packages/sdk/dist/runtime/react.mjs +168 -2
  26. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  27. package/packages/sdk/dist/workflow/index.cjs +70 -12
  28. package/packages/sdk/dist/workflow/index.cjs.map +1 -1
  29. package/packages/sdk/dist/workflow/index.mjs +70 -12
  30. 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
- const definitionJson = await resolveManifestJson(
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: template.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
- const desiredDefinition = resolveManifestPlainJson(desired, 'definitionJson', 'definitionFile');
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(existing.resourceBindings || {}, desiredDefinition.resourceBindings || {}, desired.__dir) &&
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