openxiangda 1.0.263 → 1.0.265
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 +31 -22
- package/openxiangda-skills/SKILL.md +18 -13
- package/openxiangda-skills/references/pages/page-sdk.md +2 -2
- package/openxiangda-skills/references/school-contact-relations.md +118 -45
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- package/package.json +1 -1
- package/packages/sdk/dist/runtime/index.cjs +42 -42
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +7 -7
- package/packages/sdk/dist/runtime/react.cjs +46 -46
- package/packages/sdk/dist/runtime/react.d.mts +39 -4
- package/packages/sdk/dist/runtime/react.d.ts +39 -4
- package/packages/sdk/dist/runtime/react.mjs +5 -5
- package/templates/openxiangda-react-spa/AGENTS.md +7 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +20 -19
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<!-- OpenXiangda-Policy-Version: 7 -->
|
|
2
|
+
|
|
2
3
|
# OpenXiangda
|
|
3
4
|
|
|
4
5
|
OpenXiangda is a lightweight CLI and skill package for private low-code platforms.
|
|
@@ -361,7 +362,7 @@ The dev host also calls the backend runtime route resolver for the current `/vie
|
|
|
361
362
|
Built-in route defaults come from the `openxiangda` SDK: `StandardFormPage`, `FormSubmitTemplate`, `FormDetailTemplate`, `ProcessDetailTemplate`, and `DataManagementList`. Workspaces can override whole built-in pages from `src/runtime/builtin-overrides.tsx`; exact `formUuid` entries win before the `*` fallback.
|
|
362
363
|
|
|
363
364
|
```tsx
|
|
364
|
-
import type { BuiltinRouteOverrides } from "openxiangda/runtime"
|
|
365
|
+
import type { BuiltinRouteOverrides } from "openxiangda/runtime";
|
|
365
366
|
|
|
366
367
|
export const runtimeRouteOverrides: BuiltinRouteOverrides = {
|
|
367
368
|
"form-submit": {
|
|
@@ -371,7 +372,7 @@ export const runtimeRouteOverrides: BuiltinRouteOverrides = {
|
|
|
371
372
|
"form-detail": {},
|
|
372
373
|
"process-detail": {},
|
|
373
374
|
"data-manage-list": {},
|
|
374
|
-
}
|
|
375
|
+
};
|
|
375
376
|
```
|
|
376
377
|
|
|
377
378
|
AI / Playwright verification should use real user identity, not a bypass mode. The backend exposes `POST /openxiangda-api/v1/apps/:appType/verification-login-links` for platform admins to create a short-lived, single-use `loginUrl` for a target user and redirect URI. Playwright can open the returned URL directly; the backend writes cookies and redirects back to the local or tenant `/view/...` page. Test users can be created with `POST /openxiangda-api/v1/apps/:appType/test-users`; they use the `__ox_ai_test__:<appType>:<key>` ID prefix and are intended to be used through verification login links.
|
|
@@ -379,13 +380,13 @@ AI / Playwright verification should use real user identity, not a bypass mode. T
|
|
|
379
380
|
An independent production user shell can call `resolveBrowserRuntimeRoute()` from `openxiangda/runtime` first. It posts the current `/view/...` path to `/openxiangda-api/v1/apps/:appType/runtime/routes/resolve`; the backend classifies the route as `custom-page`, `builtin-route`, `legacy-fallback`, or `not-found`. Code pages return backend-authorized bootstrap data directly, so the shell can pass that to `mountBrowserPageRuntime()` or mount with `createBrowserPageContext()` without reusing the legacy view workbench logic. Built-in routes can be rendered with `BuiltinRouteRenderer`, which loads form schema through the same `/service` proxy and keeps backend permission checks authoritative.
|
|
380
381
|
|
|
381
382
|
```ts
|
|
382
|
-
import { resolveBrowserRuntimeRoute } from "openxiangda/runtime"
|
|
383
|
+
import { resolveBrowserRuntimeRoute } from "openxiangda/runtime";
|
|
383
384
|
|
|
384
385
|
const route = await resolveBrowserRuntimeRoute({
|
|
385
386
|
appType: "APP_XXXX",
|
|
386
387
|
path: window.location.pathname,
|
|
387
388
|
search: window.location.search,
|
|
388
|
-
})
|
|
389
|
+
});
|
|
389
390
|
```
|
|
390
391
|
|
|
391
392
|
Local workspace state is authoritative. If the current folder has no `.openxiangda/state.json` app binding, create a new app instead of searching the platform for a similar app name:
|
|
@@ -415,16 +416,16 @@ openxiangda form export customer --mode package --profile dev --output ./exports
|
|
|
415
416
|
|
|
416
417
|
运行时页面读取当前用户信息时,优先使用 `sdk.user.getCurrent<PageUserRecord>()`。用户对象会返回常规组织成员关系 `departments`,也会返回系统维护的所属单位字段 `affiliatedDepartmentId` / `affiliatedDepartment`。`departments` 表示用户真实所在的部门、班级、专业等成员关系;`affiliatedDepartment` 表示业务上用于统计、筛选和展示的归属单位,通常是学院、单位或在源单位缺失时可用的具体部门节点,不用于替代权限部门成员关系。
|
|
417
418
|
|
|
418
|
-
钉钉家校通讯录的家长、学生、教师身份和明确监护关系使用 `sdk.organization.schoolContact.*`(页面)或 `ctx.organization.schoolContact.*`(App Function)。已登录非游客用户无需给应用角色额外绑定权限,默认可查询当前租户全部关系,返回平台用户 ID、手机号、钉钉 userid
|
|
419
|
+
钉钉家校通讯录的家长、学生、教师身份和明确监护关系使用 `sdk.organization.schoolContact.*`(页面)或 `ctx.organization.schoolContact.*`(App Function)。已登录非游客用户无需给应用角色额外绑定权限,默认可查询当前租户全部关系,返回平台用户 ID、手机号、钉钉 userid 和姓名;`teachers.list` 还返回 `teacher.managedClasses`、`class.headTeachers` 和同步/人工 `source`,用于班主任双向查询。应用可显式声明 `:self:read` / `:class:read` 收紧范围。完整示例见 [`docs/dingtalk-school-contact-relations.md`](docs/dingtalk-school-contact-relations.md)。
|
|
419
420
|
|
|
420
421
|
```ts
|
|
421
|
-
import type { PageUserRecord } from "openxiangda/runtime"
|
|
422
|
+
import type { PageUserRecord } from "openxiangda/runtime";
|
|
422
423
|
|
|
423
|
-
const currentUser = await sdk.user.getCurrent<PageUserRecord>()
|
|
424
|
-
const user = currentUser.result
|
|
424
|
+
const currentUser = await sdk.user.getCurrent<PageUserRecord>();
|
|
425
|
+
const user = currentUser.result;
|
|
425
426
|
|
|
426
|
-
const affiliatedDepartmentName = user?.affiliatedDepartment?.name
|
|
427
|
-
const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
|
|
427
|
+
const affiliatedDepartmentName = user?.affiliatedDepartment?.name;
|
|
428
|
+
const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId;
|
|
428
429
|
```
|
|
429
430
|
|
|
430
431
|
工程化资源放在工作区 `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")` 调用平台运行时接口,第三方密钥只保存在后端连接器配置中。
|
|
@@ -460,7 +461,15 @@ React SPA 新应用的无需登录访问统一使用 `/view/:appType/public/*`
|
|
|
460
461
|
"text/csv",
|
|
461
462
|
"application/json"
|
|
462
463
|
],
|
|
463
|
-
"allowedExtensions": [
|
|
464
|
+
"allowedExtensions": [
|
|
465
|
+
"pdf",
|
|
466
|
+
"docx",
|
|
467
|
+
"xlsx",
|
|
468
|
+
"pptx",
|
|
469
|
+
"txt",
|
|
470
|
+
"csv",
|
|
471
|
+
"json"
|
|
472
|
+
],
|
|
464
473
|
"maxSizeBytes": 31457280,
|
|
465
474
|
"visibility": "private",
|
|
466
475
|
"pathPrefix": "public/register/attachments/"
|
|
@@ -477,10 +486,10 @@ React SPA 新应用的无需登录访问统一使用 `/view/:appType/public/*`
|
|
|
477
486
|
公开页面里的图片和附件分两类处理。真正可以对公网长期公开的资产,例如仪器封面图、公开说明书、门户 banner,应上传到平台公开文件区,默认 bucket 为 `public-assets`,上传后保存的 `url` / `previewUrl` / `downloadUrl` 会是 `/file/public/...`,未登录浏览器可以直接读取并被浏览器/CDN 缓存。仍可能包含隐私、审批材料、订单报告、结算单、维保现场照片的附件不要放入公开区,应继续保存在私有 bucket,并通过登录态或受控文件票据访问。
|
|
478
487
|
|
|
479
488
|
```ts
|
|
480
|
-
import { createFormRuntimeApi } from "openxiangda"
|
|
489
|
+
import { createFormRuntimeApi } from "openxiangda";
|
|
481
490
|
|
|
482
|
-
const api = createFormRuntimeApi({ baseUrl: "/service" })
|
|
483
|
-
const file = await api.uploadPublicFile(imageFile, "public-assets")
|
|
491
|
+
const api = createFormRuntimeApi({ baseUrl: "/service" });
|
|
492
|
+
const file = await api.uploadPublicFile(imageFile, "public-assets");
|
|
484
493
|
|
|
485
494
|
// file.url === "/service/file/public/public-assets/..."
|
|
486
495
|
// file.visibility === "public"
|
|
@@ -503,7 +512,7 @@ import {
|
|
|
503
512
|
这些独立 API 必须在模板默认提供的 `OpenXiangdaProvider` 和 `OpenXiangdaPageProvider` 内使用。它们会从 PageSdk 获取当前 `appType`,并统一处理 preview ticket、metadata 和二进制文件响应。默认覆盖 PDF、常见媒体、文本、DOCX 和 XLSX,更多 DOC/XLS/PPT、ODF 格式由部署侧 ONLYOFFICE 配置决定。只有在需要复制、分享或新窗口打开预览页时,才通过 PageSdk `sdk.createFileAccessTicket(..., "preview", { appType })` 获取并打开 `previewPageUrl`;不要直接打开文件内容流 `previewUrl`。
|
|
504
513
|
|
|
505
514
|
```tsx
|
|
506
|
-
import { ImageField } from "openxiangda"
|
|
515
|
+
import { ImageField } from "openxiangda";
|
|
507
516
|
|
|
508
517
|
<ImageField
|
|
509
518
|
fieldId="photos"
|
|
@@ -514,9 +523,9 @@ import { ImageField } from "openxiangda"
|
|
|
514
523
|
enabled: true,
|
|
515
524
|
skipBelowBytes: 200 * 1024,
|
|
516
525
|
thumb: { maxWidth: 320, maxHeight: 320, quality: 0.72 },
|
|
517
|
-
preview: { maxWidth: 1280, maxHeight: 1280, quality: 0.82 }
|
|
526
|
+
preview: { maxWidth: 1280, maxHeight: 1280, quality: 0.82 },
|
|
518
527
|
}}
|
|
519
|
-
|
|
528
|
+
/>;
|
|
520
529
|
```
|
|
521
530
|
|
|
522
531
|
压缩默认 `format: "source"`,会尽量沿用原图片格式。如果显式输出 `webp`、`png` 或 `jpeg`,对应 OSS storage resource 的 `allowedExtensions` 也要包含该扩展名。
|
|
@@ -602,15 +611,15 @@ openxiangda organization account-reset-password user-alice --profile dev --body-
|
|
|
602
611
|
页面 SDK 和 App Function 使用同一套后端权限:
|
|
603
612
|
|
|
604
613
|
```ts
|
|
605
|
-
const capabilities = await sdk.organization.capabilities()
|
|
614
|
+
const capabilities = await sdk.organization.capabilities();
|
|
606
615
|
if (!capabilities.result?.canManage) {
|
|
607
|
-
throw new Error("当前账号没有组织账号管理权限")
|
|
616
|
+
throw new Error("当前账号没有组织账号管理权限");
|
|
608
617
|
}
|
|
609
618
|
|
|
610
619
|
await sdk.organization.departments.create({
|
|
611
620
|
name: "销售部",
|
|
612
621
|
parentId: "dept-root",
|
|
613
|
-
})
|
|
622
|
+
});
|
|
614
623
|
|
|
615
624
|
await sdk.organization.accounts.create({
|
|
616
625
|
id: "user-alice",
|
|
@@ -618,12 +627,12 @@ await sdk.organization.accounts.create({
|
|
|
618
627
|
password: initialPassword,
|
|
619
628
|
name: "Alice",
|
|
620
629
|
departmentIds: ["dept-sales"],
|
|
621
|
-
})
|
|
630
|
+
});
|
|
622
631
|
|
|
623
632
|
// App Function / trusted JS uses ctx.organization with the same permission check.
|
|
624
633
|
await ctx.organization.accounts.resetPassword("user-alice", {
|
|
625
634
|
newPassword,
|
|
626
|
-
})
|
|
635
|
+
});
|
|
627
636
|
```
|
|
628
637
|
|
|
629
638
|
应用登录能力通过 auth resource 和 runtime SDK 提供:登录配置放在 `src/resources/auth/<code>.json`,默认 React SPA 模板已包含 `/view/:appType/login`,自定义页面可使用 `createAuthClient({ appType, servicePrefix })` 或 `LoginPage` / `useAuth` from `openxiangda/runtime/react`。钉钉登录默认使用 `dingtalkFlow="auto"`:只有明确位于钉钉容器且 JSAPI 可用时才走免登码,普通浏览器会请求应用级 OAuth 地址并跳转钉钉认证页;自定义页可调用 `getDingTalkOAuthUrl({ returnUrl })` 接入相同能力。手机号验证码、CAS/SSO 或其他外部登录可以由 App Function provider 校验外部凭证,但 provider 只能返回 `phone` / `email` / `externalId` / `unionId` 等身份声明;平台后端按 auth resource 策略执行账号匹配、绑定、创建或拒绝,并由平台统一签发 token/cookie。默认注册策略是拒绝,开启自动注册或白名单注册前必须确认身份匹配键、默认角色、验证码 TTL/频率/失败次数、审计字段和错误文案策略。
|
|
@@ -4,6 +4,7 @@ description: "Use OpenXiangda for private low-code platform work: app workspaces
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<!-- OpenXiangda-Policy-Version: 7 -->
|
|
7
|
+
|
|
7
8
|
# OpenXiangda
|
|
8
9
|
|
|
9
10
|
OpenXiangda connects an AI coding workspace to the private low-code platform through a normal-user profile and `/openxiangda-api/v1`. External backends using AK/SK are a separate `openxiangda-open-api` flow.
|
|
@@ -57,18 +58,18 @@ content/source/parent verification and never overwrites immutable storage.
|
|
|
57
58
|
|
|
58
59
|
## Decide the track
|
|
59
60
|
|
|
60
|
-
| Intent
|
|
61
|
-
|
|
|
62
|
-
| Diagnose, snapshot, compare, explain an error
|
|
63
|
-
| Login, profile, release, CLI/state issue
|
|
64
|
-
| Create or bind an app
|
|
65
|
-
| Form/schema/data view
|
|
66
|
-
| React/classic page
|
|
67
|
-
| Function, automation, workflow, JS_CODE
|
|
68
|
-
| Roles, permission groups, data scope
|
|
61
|
+
| Intent | Read next | First action |
|
|
62
|
+
| ------------------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
63
|
+
| Diagnose, snapshot, compare, explain an error | `openxiangda-inspect` | read-only snapshot/inspect |
|
|
64
|
+
| Login, profile, release, CLI/state issue | `openxiangda-core` | resolve explicit profile and workspace |
|
|
65
|
+
| Create or bind an app | `openxiangda-app` | inspect local binding, then init/bind |
|
|
66
|
+
| Form/schema/data view | `openxiangda-form` | inspect the target form and schema |
|
|
67
|
+
| React/classic page | `openxiangda-page` | inspect the target route/page |
|
|
68
|
+
| Function, automation, workflow, JS_CODE | `openxiangda-workflow-automation` | inspect the exact resource codes |
|
|
69
|
+
| Roles, permission groups, data scope | `openxiangda-permission-settings` | produce the permission matrix |
|
|
69
70
|
| DingTalk school-contact guardian/student relationships | `openxiangda-page` plus `references/school-contact-relations.md` | use the platform relationship SDK, defaulting to unrestricted tenant scope |
|
|
70
|
-
| New app architecture, auth, public access
|
|
71
|
-
| External backend/OpenAPI/AK-SK
|
|
71
|
+
| New app architecture, auth, public access | `openxiangda-architecture-design` plus the relevant domain skill | run the design gate |
|
|
72
|
+
| External backend/OpenAPI/AK-SK | `openxiangda-open-api` | describe the exact operation |
|
|
72
73
|
|
|
73
74
|
## Scope before work
|
|
74
75
|
|
|
@@ -191,8 +192,12 @@ The lease is app-level promotion ownership, while worktree ownership prevents tw
|
|
|
191
192
|
- Use platform roles, permission groups, public grants, and backend Function checks for authorization; frontend hiding is presentation only.
|
|
192
193
|
- Keep tokens and secrets out of project files and chat.
|
|
193
194
|
- Use official public SDK entrypoints and platform form/file components.
|
|
194
|
-
- Read school-contact guardian/student relationships through
|
|
195
|
-
`sdk.organization.schoolContact` or the trusted App Function bridge.
|
|
195
|
+
- Read school-contact guardian/student and teacher/class relationships through
|
|
196
|
+
`sdk.organization.schoolContact` or the trusted App Function bridge. Use
|
|
197
|
+
`SCHOOL_HEAD_TEACHER` only for global identity and `teachers.list` for the
|
|
198
|
+
exact class relationship; consume `teacher.managedClasses` and
|
|
199
|
+
`class.headTeachers` for the two relationship directions and inspect
|
|
200
|
+
`source` when synced/manual provenance matters. Keep the
|
|
196
201
|
authenticated non-guest default tenant-wide scope without adding an app-role
|
|
197
202
|
permission binding unless the product explicitly requires self/class
|
|
198
203
|
restrictions; never infer relations from department membership.
|
|
@@ -19,7 +19,7 @@ Guidelines:
|
|
|
19
19
|
- Use `sdk.export.create` and `sdk.export.get` for asynchronous XLSX exports. Simple pages may send a declarative workbook definition. Complex or reusable exports should send only a published App Function `definitionCode`, the current query snapshot, export scope, and stable selected row IDs. The server executes `structured_export_provider_v1` with fresh user permissions; never upload executable rendering code from the browser. See `docs/structured-export-v1.md`.
|
|
20
20
|
- Use `AttachmentPreviewList`, `ImagePreviewGrid`, or `useFilePreview` from `openxiangda/runtime/react` for in-page attachment previews in a custom React SPA page. They use the current PageSdk context and enforce the platform capability and ticket contracts. Use `sdk.createFileAccessTicket(bucketName, objectName, fileName, "preview", { appType })` only when the page needs a shareable or new-window preview link. `appType` defaults to the current page context. Open `response.result.previewPageUrl`; do not use `previewUrl` or `/service/file/preview-by-ticket/:ticket` as the page entry.
|
|
21
21
|
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for intentional organization pages. Read-only list/detail pages need `app:organization:read` or `app:organization:manage`; writes and password operations need `app:organization:manage`. Do not call legacy `/user` or `/department` endpoints from pages.
|
|
22
|
-
- Use `sdk.organization.schoolContact.*` for synchronized guardian/student relationships. Authenticated non-guest users default to all relationships in the current tenant without an app-role permission binding. Use the optional `self:read` or `class:read` permission only when the product explicitly needs a narrower data scope. The response intentionally includes platform user ID, mobile, DingTalk userid, and name. Read `references/school-contact-relations.md` before building a family or class page.
|
|
22
|
+
- Use `sdk.organization.schoolContact.*` for synchronized guardian/student and teacher/class relationships. `SCHOOL_HEAD_TEACHER` identifies a user who currently heads at least one class; use `teachers.list({ classId, isHeadTeacher: true })` to resolve the exact class. Authenticated non-guest users default to all relationships in the current tenant without an app-role permission binding. Use the optional `self:read` or `class:read` permission only when the product explicitly needs a narrower data scope. The response intentionally includes platform user ID, mobile, DingTalk userid, and name. Read `references/school-contact-relations.md` before building a family or class page.
|
|
23
23
|
- Synchronized school-contact users also receive platform identity codes: `SCHOOL_GUARDIAN`, `SCHOOL_STUDENT`, and `SCHOOL_TEACHER`. Page permission groups select them with `platformRoleCodes` (additive to app `roles`), and runtime permissions expose them in `permissions.platformRoleCodes`. App Functions can use a `platform_roles` invocation audience and trusted `ctx.operator.platformRoleCodes`; these roles are maintained only by the daily organization sync.
|
|
24
24
|
- Use `sdk.workCenter.listItems({ boxType })` and `sdk.workCenter.getStats()` for current-user work center lists and counts. `boxType` is one of `todo`, `done`, `cc`, or `initiated`. This is the end-user task/handled/cc/initiated surface; do not build todo pages by reading workflow operation logs, automation logs, or raw process-task tables.
|
|
25
25
|
- `sdk.form.create` returns identifiers and generated serial number values only. Treat `formInstId` / `formInstanceId` as the required success contract; `serialNumber` / `serialNumbers` are present only when the form has `SerialNumberField`. Do not expect the save response to contain the full row, formula results, or other server-generated field values. Call `sdk.form.getDetail` explicitly after create when those values are needed.
|
|
@@ -253,7 +253,7 @@ await sdk.organization.accounts.resetPassword("alice", {
|
|
|
253
253
|
|
|
254
254
|
Use `ctx.organization.departments.*` and `ctx.organization.accounts.*` inside App Functions or trusted JS nodes for the same capability. Runtime service principals must resolve a real operator/audit actor; permission is checked against that user, not against the runtime service account. Keep password changes on dedicated endpoints: update account metadata with `update`, reset another user's password with `resetPassword`, and change the current user's password with `changeMyPassword({ oldPassword, newPassword })`.
|
|
255
255
|
|
|
256
|
-
For synchronized school-contact relationships, use `sdk.organization.schoolContact.relations.list`, `children.list`, `guardians.list`, or `myFamily.get`; App Functions use the same paths under `ctx.organization.schoolContact`. Do not infer a family relationship from class membership or persist a second relationship master inside application forms. See `references/school-contact-relations.md` for permission codes, filters, response fields, and examples.
|
|
256
|
+
For synchronized school-contact relationships, use `sdk.organization.schoolContact.relations.list`, `teachers.list`, `children.list`, `guardians.list`, or `myFamily.get`; App Functions use the same paths under `ctx.organization.schoolContact`. Do not infer a family relationship from class membership, infer a specific head-teacher class from the global role, or persist a second relationship master inside application forms. See `references/school-contact-relations.md` for permission codes, filters, response fields, and examples.
|
|
257
257
|
|
|
258
258
|
Work center:
|
|
259
259
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 家校通讯录关系
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
家校关系由平台组织中心统一同步和维护。同步成员和人工成员都直接位于班级部门下,通过 `guardian`、`student`、`teacher` 和班主任标记区分,不再依赖“家长/学生/老师”子部门。应用不得直连钉钉,也不得根据班级成员自行推断家长和学生关系。
|
|
4
4
|
|
|
5
5
|
## 权限与默认范围
|
|
6
6
|
|
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
```json
|
|
12
12
|
{
|
|
13
|
-
"apiPermissionCodes": [
|
|
14
|
-
"app:organization:school-contact:read"
|
|
15
|
-
]
|
|
13
|
+
"apiPermissionCodes": ["app:organization:school-contact:read"]
|
|
16
14
|
}
|
|
17
15
|
```
|
|
18
16
|
|
|
@@ -29,13 +27,14 @@
|
|
|
29
27
|
|
|
30
28
|
### 平台内置身份角色
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
每次成功的钉钉家校通讯录全量同步都会对账并维护四种平台层系统身份角色:
|
|
33
31
|
|
|
34
|
-
| 展示名 | 稳定角色编码
|
|
35
|
-
|
|
|
36
|
-
| 家长
|
|
37
|
-
| 学生
|
|
38
|
-
| 老师
|
|
32
|
+
| 展示名 | 稳定角色编码 | 含义 |
|
|
33
|
+
| ------ | --------------------- | --------------------------------------------- |
|
|
34
|
+
| 家长 | `SCHOOL_GUARDIAN` | 在家校成员关系中作为 guardian |
|
|
35
|
+
| 学生 | `SCHOOL_STUDENT` | 在家校成员关系中作为 student |
|
|
36
|
+
| 老师 | `SCHOOL_TEACHER` | 在家校成员关系中作为 teacher |
|
|
37
|
+
| 班主任 | `SCHOOL_HEAD_TEACHER` | 至少一个 teacher 班级成员带 `is_adviser=true` |
|
|
39
38
|
|
|
40
39
|
身份角色可组合,一个平台用户可以同时是老师和家长;角色由同步维护,不能在平台角色管理中手工分配、修改、删除,也不会被当前平台角色切换隐藏。应用权限组把这些编码填入 `platformRoleCodes`,与应用角色 `roles` 是并列的 OR 条件;两个数组都为空仍表示不限制。
|
|
41
40
|
|
|
@@ -43,9 +42,15 @@
|
|
|
43
42
|
|
|
44
43
|
```ts
|
|
45
44
|
const isGuardian =
|
|
46
|
-
sdk.context.permissions?.platformRoleCodes?.includes("SCHOOL_GUARDIAN")
|
|
45
|
+
sdk.context.permissions?.platformRoleCodes?.includes("SCHOOL_GUARDIAN");
|
|
46
|
+
|
|
47
|
+
const isHeadTeacher = sdk.context.permissions?.platformRoleCodes?.includes(
|
|
48
|
+
"SCHOOL_HEAD_TEACHER",
|
|
49
|
+
);
|
|
47
50
|
```
|
|
48
51
|
|
|
52
|
+
`SCHOOL_HEAD_TEACHER` 只说明当前用户至少担任一个班的班主任。班主任是班级维度的关系;要判断具体班级,必须使用下方 `teachers.list` 返回的 `isHeadTeacher`,不能用全局角色推断。
|
|
53
|
+
|
|
49
54
|
App Function 的调用受众可以声明平台身份角色,服务端按真实操作者的同步结果校验:
|
|
50
55
|
|
|
51
56
|
```json
|
|
@@ -69,9 +74,9 @@ const response = await sdk.organization.schoolContact.relations.list({
|
|
|
69
74
|
classId,
|
|
70
75
|
page: 1,
|
|
71
76
|
pageSize: 20,
|
|
72
|
-
})
|
|
77
|
+
});
|
|
73
78
|
|
|
74
|
-
const relations = response.result?.items || []
|
|
79
|
+
const relations = response.result?.items || [];
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
常用方法:
|
|
@@ -80,16 +85,31 @@ const relations = response.result?.items || []
|
|
|
80
85
|
await sdk.organization.schoolContact.children.list(guardianUserId, {
|
|
81
86
|
page: 1,
|
|
82
87
|
pageSize: 20,
|
|
83
|
-
})
|
|
88
|
+
});
|
|
84
89
|
|
|
85
90
|
await sdk.organization.schoolContact.guardians.list(studentUserId, {
|
|
86
91
|
page: 1,
|
|
87
92
|
pageSize: 20,
|
|
88
|
-
})
|
|
93
|
+
});
|
|
89
94
|
|
|
90
|
-
await sdk.organization.schoolContact.myFamily.get({ pageSize: 50 })
|
|
95
|
+
await sdk.organization.schoolContact.myFamily.get({ pageSize: 50 });
|
|
96
|
+
|
|
97
|
+
await sdk.organization.schoolContact.teachers.list({
|
|
98
|
+
classId,
|
|
99
|
+
isHeadTeacher: true,
|
|
100
|
+
page: 1,
|
|
101
|
+
pageSize: 20,
|
|
102
|
+
});
|
|
91
103
|
```
|
|
92
104
|
|
|
105
|
+
`teachers.list` 的每个结果同时提供两个方向的班主任信息:
|
|
106
|
+
|
|
107
|
+
- `teacher.managedClasses`:该老师当前作为班主任管理的全部班级。
|
|
108
|
+
- `class.headTeachers`:该班级当前的全部班主任。
|
|
109
|
+
- `source`:班级身份来源,值为 `dingtalk_school_contact` 或 `manual`。
|
|
110
|
+
|
|
111
|
+
这两个字段可直接用于“我管理的班级”和“班级班主任”场景,不要在应用中自行维护第二份映射。
|
|
112
|
+
|
|
93
113
|
关系列表支持以下服务端过滤条件:
|
|
94
114
|
|
|
95
115
|
- `userId`、`guardianUserId`、`studentUserId`:平台用户 ID。
|
|
@@ -100,29 +120,53 @@ await sdk.organization.schoolContact.myFamily.get({ pageSize: 50 })
|
|
|
100
120
|
- `role`:`guardian` 或 `student`,用于限定通用人员过滤作用在哪一端。
|
|
101
121
|
- `relationCode`、`page`、`pageSize`;`pageSize` 最大为 100。
|
|
102
122
|
|
|
123
|
+
老师班级成员列表 `teachers.list` 支持:
|
|
124
|
+
|
|
125
|
+
- `userId`:老师的平台用户 ID。
|
|
126
|
+
- `dingtalkUserId`:老师的钉钉 userid。
|
|
127
|
+
- `mobile`:精确手机号。
|
|
128
|
+
- `name`:老师姓名。
|
|
129
|
+
- `classId`:平台家校班级 ID 或钉钉教育 `class_id`。
|
|
130
|
+
- `isHeadTeacher`:布尔值;`true` 只返回班主任,`false` 只返回普通任课老师。
|
|
131
|
+
- `page`、`pageSize`;`pageSize` 最大为 100。
|
|
132
|
+
|
|
103
133
|
## App Function
|
|
104
134
|
|
|
105
135
|
App Function、Automation 或 Workflow 中需要复用关系时,调用对应的受控后端桥接:
|
|
106
136
|
|
|
107
137
|
```ts
|
|
108
|
-
export default async function(ctx, input) {
|
|
138
|
+
export default async function (ctx, input) {
|
|
109
139
|
const result = await ctx.organization.schoolContact.relations.list({
|
|
110
140
|
guardianUserId: input.guardianUserId,
|
|
111
141
|
page: 1,
|
|
112
142
|
pageSize: 20,
|
|
113
|
-
})
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return result.items;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
114
148
|
|
|
115
|
-
|
|
149
|
+
查询某位老师担任班主任的班级:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
export default async function (ctx, input) {
|
|
153
|
+
return await ctx.organization.schoolContact.teachers.list({
|
|
154
|
+
userId: input.teacherUserId,
|
|
155
|
+
isHeadTeacher: true,
|
|
156
|
+
page: 1,
|
|
157
|
+
pageSize: 100,
|
|
158
|
+
});
|
|
116
159
|
}
|
|
117
160
|
```
|
|
118
161
|
|
|
119
162
|
对应方法为:
|
|
120
163
|
|
|
121
164
|
```ts
|
|
122
|
-
ctx.organization.schoolContact.relations.list(params)
|
|
123
|
-
ctx.organization.schoolContact.
|
|
124
|
-
ctx.organization.schoolContact.
|
|
125
|
-
ctx.organization.schoolContact.
|
|
165
|
+
ctx.organization.schoolContact.relations.list(params);
|
|
166
|
+
ctx.organization.schoolContact.teachers.list(params);
|
|
167
|
+
ctx.organization.schoolContact.children.list(userId, params);
|
|
168
|
+
ctx.organization.schoolContact.guardians.list(userId, params);
|
|
169
|
+
ctx.organization.schoolContact.myFamily.get(params);
|
|
126
170
|
```
|
|
127
171
|
|
|
128
172
|
运行时使用平台提供的真实操作者做权限校验。不要信任页面 input 中伪造的租户、角色或当前用户 ID。
|
|
@@ -131,36 +175,65 @@ ctx.organization.schoolContact.myFamily.get(params)
|
|
|
131
175
|
|
|
132
176
|
```ts
|
|
133
177
|
interface SchoolContactRelation {
|
|
134
|
-
relationId: string
|
|
135
|
-
relationCode: string | null
|
|
136
|
-
relationName: string | null
|
|
178
|
+
relationId: string;
|
|
179
|
+
relationCode: string | null;
|
|
180
|
+
relationName: string | null;
|
|
137
181
|
guardian: {
|
|
138
|
-
userId: string
|
|
139
|
-
dingtalkUserId: string
|
|
140
|
-
name: string
|
|
141
|
-
mobile: string | null
|
|
142
|
-
}
|
|
182
|
+
userId: string;
|
|
183
|
+
dingtalkUserId: string | null;
|
|
184
|
+
name: string;
|
|
185
|
+
mobile: string | null;
|
|
186
|
+
};
|
|
143
187
|
student: {
|
|
144
|
-
userId: string
|
|
145
|
-
dingtalkUserId: string
|
|
146
|
-
name: string
|
|
147
|
-
mobile: string | null
|
|
148
|
-
}
|
|
188
|
+
userId: string;
|
|
189
|
+
dingtalkUserId: string | null;
|
|
190
|
+
name: string;
|
|
191
|
+
mobile: string | null;
|
|
192
|
+
};
|
|
149
193
|
class: {
|
|
150
|
-
id: string
|
|
151
|
-
dingtalkClassId: string
|
|
152
|
-
name: string
|
|
153
|
-
campusName: string | null
|
|
154
|
-
periodName: string | null
|
|
155
|
-
gradeName: string | null
|
|
156
|
-
}
|
|
157
|
-
syncedAt: string
|
|
158
|
-
syncState: "current" | "not_synced" | "disabled"
|
|
194
|
+
id: string;
|
|
195
|
+
dingtalkClassId: string;
|
|
196
|
+
name: string;
|
|
197
|
+
campusName: string | null;
|
|
198
|
+
periodName: string | null;
|
|
199
|
+
gradeName: string | null;
|
|
200
|
+
};
|
|
201
|
+
syncedAt: string;
|
|
202
|
+
syncState: "current" | "not_synced" | "disabled";
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
老师班级成员的返回项为:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
interface SchoolContactTeacherMembership {
|
|
210
|
+
membershipId: string;
|
|
211
|
+
teacher: {
|
|
212
|
+
userId: string;
|
|
213
|
+
dingtalkUserId: string | null;
|
|
214
|
+
name: string;
|
|
215
|
+
mobile: string | null;
|
|
216
|
+
managedClasses: SchoolContactRelation["class"][];
|
|
217
|
+
};
|
|
218
|
+
class: SchoolContactRelation["class"] & {
|
|
219
|
+
headTeachers: Array<{
|
|
220
|
+
userId: string;
|
|
221
|
+
dingtalkUserId: string | null;
|
|
222
|
+
name: string;
|
|
223
|
+
mobile: string | null;
|
|
224
|
+
}>;
|
|
225
|
+
};
|
|
226
|
+
isHeadTeacher: boolean;
|
|
227
|
+
source: "dingtalk_school_contact" | "manual";
|
|
228
|
+
syncedAt: string;
|
|
229
|
+
syncState: "current" | "not_synced" | "disabled";
|
|
159
230
|
}
|
|
160
231
|
```
|
|
161
232
|
|
|
162
233
|
手机号可能为空。`sync.state=disabled` 表示管理员关闭了后续同步,现有数据仍保留但可能过期;业务页面在对时效敏感时应展示 `lastSuccessfulSyncAt`。
|
|
163
234
|
|
|
235
|
+
平台管理员人工添加的班级身份也会由 `teachers.list` 返回,并标记 `source=manual`。人工家长/学生身份会维护对应平台身份和班级归属,但不会自动生成监护关系;只有明确存在的家庭关系才会出现在 `relations`、`children`、`guardians` 和 `myFamily`。
|
|
236
|
+
|
|
164
237
|
## 持久化边界
|
|
165
238
|
|
|
166
239
|
- 平台关系是当前事实,不要复制全量关系到应用表单或 Data View。
|
|
@@ -36,7 +36,7 @@ openxiangda workspace publish --profile <name> --page <pageCode>
|
|
|
36
36
|
- ✅ For external APIs use `src/resources/connectors/<code>.json` + `sdk.connector.invoke()`; for joined read-only lists use row data views + `sdk.dataView.query`; for stable dashboard metrics use aggregate data views + `sdk.dataView.stats`.
|
|
37
37
|
- ✅ Choose data view `storageMode` deliberately: `materialized` for refreshed reports/lists that tolerate delay, `live` for bounded real-time multi-form reads.
|
|
38
38
|
- ✅ For reusable backend orchestration use App Function (`src/functions/<functionCode>/index.ts` + `src/resources/functions/<functionCode>.json`) and call `sdk.function.invoke(code, { input })`; keep page components out of multi-table business orchestration.
|
|
39
|
-
- ✅
|
|
39
|
+
- ✅ 家长、学生、教师、班主任身份或班级/监护关系页面使用 `sdk.organization.schoolContact.*`;先读 `references/school-contact-relations.md`。用 `SCHOOL_HEAD_TEACHER` 判断全局身份,用 `teachers.list({ classId, isHeadTeacher: true })` 判断具体班主任班级,并直接读取 `teacher.managedClasses` / `class.headTeachers`;默认使用已登录用户的租户内全部关系,仅在业务要求时声明 self/class 收紧范围,并保持服务端分页。
|
|
40
40
|
- ✅ Filters, search bars, modal forms, drawers, and inline edits use platform components for platform data fields, otherwise `antd` / `antd-mobile` controls.
|
|
41
41
|
- ❌ Single-file giant pages. Split per `references/best-practices.md`.
|
|
42
42
|
- ❌ Hardcoded `/view/...&isRenderNav=false` URLs scattered through page code; use the runtime navigation helper.
|
|
@@ -132,7 +132,7 @@ Read these references only when editing page code:
|
|
|
132
132
|
- All visible page copy must be end-user-facing business text. Do not put developer explanations, implementation notes, schema descriptions, or "this module is generated by..." text into sections, cards, alerts, empty states, tooltips, or helper copy.
|
|
133
133
|
- Store live `pageId`, `routeKey`, and `legacyFormUuid` under the current profile only.
|
|
134
134
|
- Use `openxiangda/runtime` for platform data access instead of hardcoding backend URLs in page code.
|
|
135
|
-
- 家校关系必须通过 `sdk.organization.schoolContact` / `ctx.organization.schoolContact`
|
|
135
|
+
- 家校关系必须通过 `sdk.organization.schoolContact` / `ctx.organization.schoolContact` 读取;班主任班级使用 `teachers.list` 的 `isHeadTeacher`,不要用全局角色推断具体班级,也不要直连钉钉、查询系统表或用同班成员推断亲属关系。
|
|
136
136
|
- For reminders, alerts, and business messages, declare `src/resources/notifications/` first and call `sdk.notification`; do not hardcode notification API URLs.
|
|
137
137
|
- For backend business logic shared by pages, automations, or workflows, declare an App Function and call `sdk.function.invoke`; do not duplicate the same multi-form query/connector/notification orchestration in page code.
|
|
138
138
|
- Before hand-writing mature UI behavior, consult `references/component-guide.md` and use established libraries: platform components for platform data fields, antd/antd-mobile for controls and overlays, ECharts for charts, GSAP for complex animation timelines, and maintained packages such as dnd-kit for drag/drop. Do not rebuild mature controls with raw DOM/native inputs.
|