openxiangda 1.0.264 → 1.0.266
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 +35 -22
- package/lib/application-environments.js +15 -1
- package/lib/cli.js +57 -8
- package/openxiangda-skills/SKILL.md +16 -13
- package/openxiangda-skills/references/school-contact-relations.md +78 -60
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +1 -1
- package/package.json +1 -1
- package/packages/sdk/dist/{ProcessPreview-DSUIJi5V.d.mts → ProcessPreview-Cyk6uv-w.d.mts} +3 -2
- package/packages/sdk/dist/{ProcessPreview-DSUIJi5V.d.ts → ProcessPreview-Cyk6uv-w.d.ts} +3 -2
- package/packages/sdk/dist/components/index.d.mts +42 -41
- package/packages/sdk/dist/components/index.d.ts +42 -41
- package/packages/sdk/dist/{dataManagementApi-p_HOhQnA.d.mts → dataManagementApi-4fSaCA5t.d.mts} +2 -2
- package/packages/sdk/dist/{dataManagementApi-Ro9itwm8.d.ts → dataManagementApi-CE8Zyj3a.d.ts} +2 -2
- package/packages/sdk/dist/runtime/index.d.mts +5 -4
- package/packages/sdk/dist/runtime/index.d.ts +5 -4
- package/packages/sdk/dist/runtime/react.d.mts +17 -10
- package/packages/sdk/dist/runtime/react.d.ts +17 -10
- package/templates/openxiangda-react-spa/AGENTS.md +9 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +21 -20
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.
|
|
@@ -106,6 +107,10 @@ openxiangda resource plan function --only customer_get,customer_save --profile d
|
|
|
106
107
|
openxiangda sdd bundle mainline-release --changes add-customer-page,fix-customer-api
|
|
107
108
|
# verify, wait for the lease, stage exact children, atomically finalize, and release
|
|
108
109
|
openxiangda release publish --change mainline-release --profile dev
|
|
110
|
+
# 仅限平台已核验的旧式多历史 lineage、精确非删除恢复
|
|
111
|
+
openxiangda release publish --change mainline-release --profile dev \
|
|
112
|
+
--adopt-online-baseline \
|
|
113
|
+
--adoption-reason "已合入主线的目标来自多次历史发布,无法对应单一 Git 基线"
|
|
109
114
|
openxiangda release status --change mainline-release --watch
|
|
110
115
|
openxiangda release explain --change mainline-release --profile dev
|
|
111
116
|
# 环境托管应用一次性登记(已有应用通常作为 preproduction)
|
|
@@ -361,7 +366,7 @@ The dev host also calls the backend runtime route resolver for the current `/vie
|
|
|
361
366
|
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
367
|
|
|
363
368
|
```tsx
|
|
364
|
-
import type { BuiltinRouteOverrides } from "openxiangda/runtime"
|
|
369
|
+
import type { BuiltinRouteOverrides } from "openxiangda/runtime";
|
|
365
370
|
|
|
366
371
|
export const runtimeRouteOverrides: BuiltinRouteOverrides = {
|
|
367
372
|
"form-submit": {
|
|
@@ -371,7 +376,7 @@ export const runtimeRouteOverrides: BuiltinRouteOverrides = {
|
|
|
371
376
|
"form-detail": {},
|
|
372
377
|
"process-detail": {},
|
|
373
378
|
"data-manage-list": {},
|
|
374
|
-
}
|
|
379
|
+
};
|
|
375
380
|
```
|
|
376
381
|
|
|
377
382
|
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 +384,13 @@ AI / Playwright verification should use real user identity, not a bypass mode. T
|
|
|
379
384
|
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
385
|
|
|
381
386
|
```ts
|
|
382
|
-
import { resolveBrowserRuntimeRoute } from "openxiangda/runtime"
|
|
387
|
+
import { resolveBrowserRuntimeRoute } from "openxiangda/runtime";
|
|
383
388
|
|
|
384
389
|
const route = await resolveBrowserRuntimeRoute({
|
|
385
390
|
appType: "APP_XXXX",
|
|
386
391
|
path: window.location.pathname,
|
|
387
392
|
search: window.location.search,
|
|
388
|
-
})
|
|
393
|
+
});
|
|
389
394
|
```
|
|
390
395
|
|
|
391
396
|
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 +420,16 @@ openxiangda form export customer --mode package --profile dev --output ./exports
|
|
|
415
420
|
|
|
416
421
|
运行时页面读取当前用户信息时,优先使用 `sdk.user.getCurrent<PageUserRecord>()`。用户对象会返回常规组织成员关系 `departments`,也会返回系统维护的所属单位字段 `affiliatedDepartmentId` / `affiliatedDepartment`。`departments` 表示用户真实所在的部门、班级、专业等成员关系;`affiliatedDepartment` 表示业务上用于统计、筛选和展示的归属单位,通常是学院、单位或在源单位缺失时可用的具体部门节点,不用于替代权限部门成员关系。
|
|
417
422
|
|
|
418
|
-
钉钉家校通讯录的家长、学生、教师身份和明确监护关系使用 `sdk.organization.schoolContact.*`(页面)或 `ctx.organization.schoolContact.*`(App Function)。已登录非游客用户无需给应用角色额外绑定权限,默认可查询当前租户全部关系,返回平台用户 ID、手机号、钉钉 userid
|
|
423
|
+
钉钉家校通讯录的家长、学生、教师身份和明确监护关系使用 `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
424
|
|
|
420
425
|
```ts
|
|
421
|
-
import type { PageUserRecord } from "openxiangda/runtime"
|
|
426
|
+
import type { PageUserRecord } from "openxiangda/runtime";
|
|
422
427
|
|
|
423
|
-
const currentUser = await sdk.user.getCurrent<PageUserRecord>()
|
|
424
|
-
const user = currentUser.result
|
|
428
|
+
const currentUser = await sdk.user.getCurrent<PageUserRecord>();
|
|
429
|
+
const user = currentUser.result;
|
|
425
430
|
|
|
426
|
-
const affiliatedDepartmentName = user?.affiliatedDepartment?.name
|
|
427
|
-
const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
|
|
431
|
+
const affiliatedDepartmentName = user?.affiliatedDepartment?.name;
|
|
432
|
+
const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId;
|
|
428
433
|
```
|
|
429
434
|
|
|
430
435
|
工程化资源放在工作区 `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 +465,15 @@ React SPA 新应用的无需登录访问统一使用 `/view/:appType/public/*`
|
|
|
460
465
|
"text/csv",
|
|
461
466
|
"application/json"
|
|
462
467
|
],
|
|
463
|
-
"allowedExtensions": [
|
|
468
|
+
"allowedExtensions": [
|
|
469
|
+
"pdf",
|
|
470
|
+
"docx",
|
|
471
|
+
"xlsx",
|
|
472
|
+
"pptx",
|
|
473
|
+
"txt",
|
|
474
|
+
"csv",
|
|
475
|
+
"json"
|
|
476
|
+
],
|
|
464
477
|
"maxSizeBytes": 31457280,
|
|
465
478
|
"visibility": "private",
|
|
466
479
|
"pathPrefix": "public/register/attachments/"
|
|
@@ -477,10 +490,10 @@ React SPA 新应用的无需登录访问统一使用 `/view/:appType/public/*`
|
|
|
477
490
|
公开页面里的图片和附件分两类处理。真正可以对公网长期公开的资产,例如仪器封面图、公开说明书、门户 banner,应上传到平台公开文件区,默认 bucket 为 `public-assets`,上传后保存的 `url` / `previewUrl` / `downloadUrl` 会是 `/file/public/...`,未登录浏览器可以直接读取并被浏览器/CDN 缓存。仍可能包含隐私、审批材料、订单报告、结算单、维保现场照片的附件不要放入公开区,应继续保存在私有 bucket,并通过登录态或受控文件票据访问。
|
|
478
491
|
|
|
479
492
|
```ts
|
|
480
|
-
import { createFormRuntimeApi } from "openxiangda"
|
|
493
|
+
import { createFormRuntimeApi } from "openxiangda";
|
|
481
494
|
|
|
482
|
-
const api = createFormRuntimeApi({ baseUrl: "/service" })
|
|
483
|
-
const file = await api.uploadPublicFile(imageFile, "public-assets")
|
|
495
|
+
const api = createFormRuntimeApi({ baseUrl: "/service" });
|
|
496
|
+
const file = await api.uploadPublicFile(imageFile, "public-assets");
|
|
484
497
|
|
|
485
498
|
// file.url === "/service/file/public/public-assets/..."
|
|
486
499
|
// file.visibility === "public"
|
|
@@ -503,7 +516,7 @@ import {
|
|
|
503
516
|
这些独立 API 必须在模板默认提供的 `OpenXiangdaProvider` 和 `OpenXiangdaPageProvider` 内使用。它们会从 PageSdk 获取当前 `appType`,并统一处理 preview ticket、metadata 和二进制文件响应。默认覆盖 PDF、常见媒体、文本、DOCX 和 XLSX,更多 DOC/XLS/PPT、ODF 格式由部署侧 ONLYOFFICE 配置决定。只有在需要复制、分享或新窗口打开预览页时,才通过 PageSdk `sdk.createFileAccessTicket(..., "preview", { appType })` 获取并打开 `previewPageUrl`;不要直接打开文件内容流 `previewUrl`。
|
|
504
517
|
|
|
505
518
|
```tsx
|
|
506
|
-
import { ImageField } from "openxiangda"
|
|
519
|
+
import { ImageField } from "openxiangda";
|
|
507
520
|
|
|
508
521
|
<ImageField
|
|
509
522
|
fieldId="photos"
|
|
@@ -514,9 +527,9 @@ import { ImageField } from "openxiangda"
|
|
|
514
527
|
enabled: true,
|
|
515
528
|
skipBelowBytes: 200 * 1024,
|
|
516
529
|
thumb: { maxWidth: 320, maxHeight: 320, quality: 0.72 },
|
|
517
|
-
preview: { maxWidth: 1280, maxHeight: 1280, quality: 0.82 }
|
|
530
|
+
preview: { maxWidth: 1280, maxHeight: 1280, quality: 0.82 },
|
|
518
531
|
}}
|
|
519
|
-
|
|
532
|
+
/>;
|
|
520
533
|
```
|
|
521
534
|
|
|
522
535
|
压缩默认 `format: "source"`,会尽量沿用原图片格式。如果显式输出 `webp`、`png` 或 `jpeg`,对应 OSS storage resource 的 `allowedExtensions` 也要包含该扩展名。
|
|
@@ -602,15 +615,15 @@ openxiangda organization account-reset-password user-alice --profile dev --body-
|
|
|
602
615
|
页面 SDK 和 App Function 使用同一套后端权限:
|
|
603
616
|
|
|
604
617
|
```ts
|
|
605
|
-
const capabilities = await sdk.organization.capabilities()
|
|
618
|
+
const capabilities = await sdk.organization.capabilities();
|
|
606
619
|
if (!capabilities.result?.canManage) {
|
|
607
|
-
throw new Error("当前账号没有组织账号管理权限")
|
|
620
|
+
throw new Error("当前账号没有组织账号管理权限");
|
|
608
621
|
}
|
|
609
622
|
|
|
610
623
|
await sdk.organization.departments.create({
|
|
611
624
|
name: "销售部",
|
|
612
625
|
parentId: "dept-root",
|
|
613
|
-
})
|
|
626
|
+
});
|
|
614
627
|
|
|
615
628
|
await sdk.organization.accounts.create({
|
|
616
629
|
id: "user-alice",
|
|
@@ -618,12 +631,12 @@ await sdk.organization.accounts.create({
|
|
|
618
631
|
password: initialPassword,
|
|
619
632
|
name: "Alice",
|
|
620
633
|
departmentIds: ["dept-sales"],
|
|
621
|
-
})
|
|
634
|
+
});
|
|
622
635
|
|
|
623
636
|
// App Function / trusted JS uses ctx.organization with the same permission check.
|
|
624
637
|
await ctx.organization.accounts.resetPassword("user-alice", {
|
|
625
638
|
newPassword,
|
|
626
|
-
})
|
|
639
|
+
});
|
|
627
640
|
```
|
|
628
641
|
|
|
629
642
|
应用登录能力通过 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/频率/失败次数、审计字段和错误文案策略。
|
|
@@ -1025,7 +1025,7 @@ function releaseExecutionPath(changeId, deploymentId, cwd = process.cwd()) {
|
|
|
1025
1025
|
}
|
|
1026
1026
|
|
|
1027
1027
|
function withManagedReleaseForwardedFlags(stepId, args = [], flags = {}) {
|
|
1028
|
-
|
|
1028
|
+
let forwarded = [...args];
|
|
1029
1029
|
if (stepId === 'runtime-stage' && flags['allow-runtime-rollback']) {
|
|
1030
1030
|
forwarded.push('--allow-runtime-rollback');
|
|
1031
1031
|
const rollbackReason = String(flags.reason || '').trim();
|
|
@@ -1058,6 +1058,19 @@ function withManagedReleaseForwardedFlags(stepId, args = [], flags = {}) {
|
|
|
1058
1058
|
forwarded.push('--reason', replacementReason);
|
|
1059
1059
|
}
|
|
1060
1060
|
}
|
|
1061
|
+
forwarded = withOnlineBaselineAdoptionFlags(forwarded, flags);
|
|
1062
|
+
return forwarded;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function withOnlineBaselineAdoptionFlags(args = [], flags = {}) {
|
|
1066
|
+
const forwarded = [...args];
|
|
1067
|
+
const isExactResourcePublish =
|
|
1068
|
+
forwarded[0] === 'resource' &&
|
|
1069
|
+
forwarded[1] === 'publish' &&
|
|
1070
|
+
(forwarded.includes('--only') || forwarded.includes('--code'));
|
|
1071
|
+
if (!isExactResourcePublish) {
|
|
1072
|
+
return forwarded;
|
|
1073
|
+
}
|
|
1061
1074
|
if (flags['adopt-online-baseline']) {
|
|
1062
1075
|
forwarded.push('--adopt-online-baseline');
|
|
1063
1076
|
const adoptionReason = String(
|
|
@@ -1129,6 +1142,7 @@ module.exports = {
|
|
|
1129
1142
|
saveCandidate,
|
|
1130
1143
|
sha256Canonical,
|
|
1131
1144
|
withManagedReleaseForwardedFlags,
|
|
1145
|
+
withOnlineBaselineAdoptionFlags,
|
|
1132
1146
|
withReleaseClientSessionArgs,
|
|
1133
1147
|
writePrivateJsonAtomic,
|
|
1134
1148
|
};
|
package/lib/cli.js
CHANGED
|
@@ -163,6 +163,7 @@ const {
|
|
|
163
163
|
saveCandidate,
|
|
164
164
|
sha256Canonical,
|
|
165
165
|
withManagedReleaseForwardedFlags,
|
|
166
|
+
withOnlineBaselineAdoptionFlags,
|
|
166
167
|
withReleaseClientSessionArgs,
|
|
167
168
|
} = require('./application-environments');
|
|
168
169
|
const { startDeveloperCenter } = require('./developer-center');
|
|
@@ -336,7 +337,7 @@ Usage:
|
|
|
336
337
|
openxiangda status <runId> [--environment preproduction|production] [--json]
|
|
337
338
|
openxiangda retry <runId> [--environment preproduction|production] [--json]
|
|
338
339
|
openxiangda rollback <preproduction|production> --to <appReleaseId> [--json]
|
|
339
|
-
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
|
|
340
|
+
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--adopt-online-baseline --adoption-reason text] [--watch] [--json]
|
|
340
341
|
openxiangda release ship|candidate|deploy|reconcile|test|fail|promote|rollback [--candidate id] [--deployment id] [--environment target] [--replace-manifest] [--allow-runtime-rollback --reason text] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]
|
|
341
342
|
openxiangda task status --change <id> [--profile name] [--watch] [--json]
|
|
342
343
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
@@ -2075,14 +2076,18 @@ async function endReleaseWithoutLocalLease(
|
|
|
2075
2076
|
}
|
|
2076
2077
|
|
|
2077
2078
|
function decorateEnvironmentReleaseSteps(steps, target, flags = {}) {
|
|
2078
|
-
|
|
2079
|
+
const managedEnvironment = Boolean(
|
|
2080
|
+
target.environmentId && target.targetName
|
|
2081
|
+
);
|
|
2079
2082
|
const deploymentId = readStringFlag(flags, 'deployment-id');
|
|
2080
2083
|
for (const step of steps) {
|
|
2081
|
-
step.args =
|
|
2082
|
-
step.id,
|
|
2083
|
-
step.args,
|
|
2084
|
-
|
|
2085
|
-
|
|
2084
|
+
step.args = managedEnvironment
|
|
2085
|
+
? withManagedReleaseForwardedFlags(step.id, step.args, flags)
|
|
2086
|
+
: withOnlineBaselineAdoptionFlags(step.args, flags);
|
|
2087
|
+
if (!managedEnvironment) {
|
|
2088
|
+
step.command = commandFromArgs(step.args);
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2086
2091
|
step.args.push('--environment', target.targetName);
|
|
2087
2092
|
if (deploymentId) step.args.push('--deployment-id', deploymentId);
|
|
2088
2093
|
if (step.id === 'runtime-stage' && flags.__verifiedCandidate) {
|
|
@@ -2132,6 +2137,43 @@ function decorateEnvironmentReleaseSteps(steps, target, flags = {}) {
|
|
|
2132
2137
|
return steps;
|
|
2133
2138
|
}
|
|
2134
2139
|
|
|
2140
|
+
function assertReleasePublishAdoptionFlags(flags = {}) {
|
|
2141
|
+
const adoptionRequested = Boolean(flags['adopt-online-baseline']);
|
|
2142
|
+
const adoptionReasonSupplied = Object.prototype.hasOwnProperty.call(
|
|
2143
|
+
flags,
|
|
2144
|
+
'adoption-reason'
|
|
2145
|
+
);
|
|
2146
|
+
const adoptionReason = String(flags['adoption-reason'] || '').trim();
|
|
2147
|
+
if (!adoptionRequested && adoptionReasonSupplied) {
|
|
2148
|
+
const error = new Error(
|
|
2149
|
+
'RELEASE_PUBLISH_ADOPTION_FLAG_REQUIRED: --adoption-reason 必须与 --adopt-online-baseline 一起使用;命令已在任何发布租约或资源写入前终止'
|
|
2150
|
+
);
|
|
2151
|
+
error.code = 'RELEASE_PUBLISH_ADOPTION_FLAG_REQUIRED';
|
|
2152
|
+
throw error;
|
|
2153
|
+
}
|
|
2154
|
+
if (adoptionRequested && adoptionReason.length < 8) {
|
|
2155
|
+
const error = new Error(
|
|
2156
|
+
'RELEASE_PUBLISH_ADOPTION_REASON_REQUIRED: --adopt-online-baseline 必须提供至少 8 个字符的 --adoption-reason;命令已在任何发布租约或资源写入前终止'
|
|
2157
|
+
);
|
|
2158
|
+
error.code = 'RELEASE_PUBLISH_ADOPTION_REASON_REQUIRED';
|
|
2159
|
+
throw error;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function assertReleasePublishAdoptionScope(steps = [], flags = {}) {
|
|
2164
|
+
if (!flags['adopt-online-baseline']) return;
|
|
2165
|
+
const adoptionSteps = steps.filter(step =>
|
|
2166
|
+
step.args?.includes('--adopt-online-baseline')
|
|
2167
|
+
);
|
|
2168
|
+
if (adoptionSteps.length === 0) {
|
|
2169
|
+
const error = new Error(
|
|
2170
|
+
'RELEASE_PUBLISH_ADOPTION_SCOPE_REQUIRED: 当前发布计划没有可精确采纳线上基线的 resource publish --only/--code 阶段;命令已在任何发布租约或资源写入前终止'
|
|
2171
|
+
);
|
|
2172
|
+
error.code = 'RELEASE_PUBLISH_ADOPTION_SCOPE_REQUIRED';
|
|
2173
|
+
throw error;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2135
2177
|
function changeIdFromStep(step) {
|
|
2136
2178
|
const index = step.args.indexOf('--change');
|
|
2137
2179
|
return index >= 0 ? step.args[index + 1] : 'change';
|
|
@@ -2145,6 +2187,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2145
2187
|
if (!changeId) {
|
|
2146
2188
|
fail('用法: openxiangda release publish --change <id> --profile <name>');
|
|
2147
2189
|
}
|
|
2190
|
+
assertReleasePublishAdoptionFlags(flags);
|
|
2148
2191
|
assertManagedRuntimeRollbackFlags(flags);
|
|
2149
2192
|
const releaseCandidate = flags.__verifiedCandidate
|
|
2150
2193
|
? assertLocalCandidate(flags.__verifiedCandidate)
|
|
@@ -2171,6 +2214,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
2171
2214
|
changeId
|
|
2172
2215
|
);
|
|
2173
2216
|
decorateEnvironmentReleaseSteps(steps, target, flags);
|
|
2217
|
+
assertReleasePublishAdoptionScope(steps, flags);
|
|
2174
2218
|
const releaseScope = getSddChangeScope({
|
|
2175
2219
|
cwd: process.cwd(),
|
|
2176
2220
|
configText: readWorkspaceConfigText(),
|
|
@@ -5308,7 +5352,7 @@ async function release(args) {
|
|
|
5308
5352
|
const { flags, positional } = parseArgs(rest);
|
|
5309
5353
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
5310
5354
|
print([
|
|
5311
|
-
'用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
5355
|
+
'用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--adopt-online-baseline --adoption-reason text] [--json]',
|
|
5312
5356
|
' openxiangda release ship|candidate|deploy|reconcile|test|fail|promote|rollback [--candidate id] [--deployment id] [--environment target] [--replace-manifest] [--allow-runtime-rollback --reason text] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]',
|
|
5313
5357
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
5314
5358
|
' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--break-glass-adopt-verified-root --reason text] [--force-activate-without-validation] [--profile name] [--json]',
|
|
@@ -5321,6 +5365,7 @@ async function release(args) {
|
|
|
5321
5365
|
' openxiangda release ship --change <id> --profile <name> --confirm-production',
|
|
5322
5366
|
' openxiangda release fail --deployment <id> --message "UAT 未通过" --code UAT_FAILED --details-json <JSON|file> --environment preproduction --profile <name>',
|
|
5323
5367
|
' openxiangda release publish --change <id> --profile <name>',
|
|
5368
|
+
' openxiangda release publish --change <id> --profile <name> --adopt-online-baseline --adoption-reason "已审计目标来自多次历史发布"',
|
|
5324
5369
|
' openxiangda release begin --change <id> --profile <name>',
|
|
5325
5370
|
' openxiangda resource publish function --only <code> --change <id> --profile <name>',
|
|
5326
5371
|
' openxiangda runtime deploy --change <id> --profile <name>',
|
|
@@ -5339,6 +5384,7 @@ async function release(args) {
|
|
|
5339
5384
|
' - release fail 只允许显式选择 preproduction target,并会先核对 deployment 的 targetEnvironmentId;message 必填,code/details 会一并写入平台审计。',
|
|
5340
5385
|
' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
|
|
5341
5386
|
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
5387
|
+
' - 旧式非环境托管 publish 也会把线上基线采纳意图仅透传给精确的 resource publish --only/--code 阶段;无精确资源阶段、原因不足或单独提供 reason 均在获取租约前 fail-closed。',
|
|
5342
5388
|
' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
|
|
5343
5389
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
5344
5390
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
@@ -31065,6 +31111,8 @@ function buildWorkspacePublishEnv(
|
|
|
31065
31111
|
}
|
|
31066
31112
|
|
|
31067
31113
|
module.exports = {
|
|
31114
|
+
assertReleasePublishAdoptionFlags,
|
|
31115
|
+
assertReleasePublishAdoptionScope,
|
|
31068
31116
|
assertRuntimeReleaseReusable,
|
|
31069
31117
|
buildOpenXiangdaUpdateInstallPlan,
|
|
31070
31118
|
buildResourceManifestSddTargets,
|
|
@@ -31081,4 +31129,5 @@ module.exports = {
|
|
|
31081
31129
|
resolveDataViewSourceFormCodes,
|
|
31082
31130
|
satisfiedFormCodesFromContext,
|
|
31083
31131
|
stagedDataViewFormReleaseDependencies,
|
|
31132
|
+
decorateEnvironmentReleaseSteps,
|
|
31084
31133
|
};
|
|
@@ -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
|
|
|
@@ -160,7 +161,7 @@ When the sealed Runtime source intentionally does not descend from the active Ru
|
|
|
160
161
|
|
|
161
162
|
`resource plan` and publish dry-runs are strictly GET/HEAD-only. `READ_ONLY_AUTH_REQUIRED` means the access token expired; run `openxiangda auth refresh --profile <name>` or log in again before retrying. Never add an automatic refresh POST inside a plan.
|
|
162
163
|
|
|
163
|
-
`release publish` is the default promotion entrypoint only for legacy unmanaged workspaces. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. Environment-managed applications use the two-phase `release ship`; candidate/deploy/test/fail/promote, `release begin`, and child commands remain recovery/diagnostic primitives.
|
|
164
|
+
`release publish` is the default promotion entrypoint only for legacy unmanaged workspaces. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. When the same audited historical-lineage condition applies, legacy `release publish` accepts `--adopt-online-baseline --adoption-reason "..."` and forwards the pair only to exact `resource publish --only/--code` stages; missing reasons or plans without an exact resource stage fail before lease acquisition. Environment-managed applications use the two-phase `release ship`; candidate/deploy/test/fail/promote, `release begin`, and child commands remain recovery/diagnostic primitives.
|
|
164
165
|
|
|
165
166
|
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head` and `runtime releases` are compact by default; use `--full` only when the complete manifest is required.
|
|
166
167
|
|
|
@@ -194,7 +195,9 @@ The lease is app-level promotion ownership, while worktree ownership prevents tw
|
|
|
194
195
|
- Read school-contact guardian/student and teacher/class relationships through
|
|
195
196
|
`sdk.organization.schoolContact` or the trusted App Function bridge. Use
|
|
196
197
|
`SCHOOL_HEAD_TEACHER` only for global identity and `teachers.list` for the
|
|
197
|
-
exact class relationship.
|
|
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
|
|
198
201
|
authenticated non-guest default tenant-wide scope without adding an app-role
|
|
199
202
|
permission binding unless the product explicitly requires self/class
|
|
200
203
|
restrictions; never infer relations from department membership.
|
|
@@ -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
|
|
|
@@ -31,11 +29,11 @@
|
|
|
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 |
|
|
39
37
|
| 班主任 | `SCHOOL_HEAD_TEACHER` | 至少一个 teacher 班级成员带 `is_adviser=true` |
|
|
40
38
|
|
|
41
39
|
身份角色可组合,一个平台用户可以同时是老师和家长;角色由同步维护,不能在平台角色管理中手工分配、修改、删除,也不会被当前平台角色切换隐藏。应用权限组把这些编码填入 `platformRoleCodes`,与应用角色 `roles` 是并列的 OR 条件;两个数组都为空仍表示不限制。
|
|
@@ -44,10 +42,11 @@
|
|
|
44
42
|
|
|
45
43
|
```ts
|
|
46
44
|
const isGuardian =
|
|
47
|
-
sdk.context.permissions?.platformRoleCodes?.includes("SCHOOL_GUARDIAN")
|
|
45
|
+
sdk.context.permissions?.platformRoleCodes?.includes("SCHOOL_GUARDIAN");
|
|
48
46
|
|
|
49
|
-
const isHeadTeacher =
|
|
50
|
-
|
|
47
|
+
const isHeadTeacher = sdk.context.permissions?.platformRoleCodes?.includes(
|
|
48
|
+
"SCHOOL_HEAD_TEACHER",
|
|
49
|
+
);
|
|
51
50
|
```
|
|
52
51
|
|
|
53
52
|
`SCHOOL_HEAD_TEACHER` 只说明当前用户至少担任一个班的班主任。班主任是班级维度的关系;要判断具体班级,必须使用下方 `teachers.list` 返回的 `isHeadTeacher`,不能用全局角色推断。
|
|
@@ -75,9 +74,9 @@ const response = await sdk.organization.schoolContact.relations.list({
|
|
|
75
74
|
classId,
|
|
76
75
|
page: 1,
|
|
77
76
|
pageSize: 20,
|
|
78
|
-
})
|
|
77
|
+
});
|
|
79
78
|
|
|
80
|
-
const relations = response.result?.items || []
|
|
79
|
+
const relations = response.result?.items || [];
|
|
81
80
|
```
|
|
82
81
|
|
|
83
82
|
常用方法:
|
|
@@ -86,23 +85,31 @@ const relations = response.result?.items || []
|
|
|
86
85
|
await sdk.organization.schoolContact.children.list(guardianUserId, {
|
|
87
86
|
page: 1,
|
|
88
87
|
pageSize: 20,
|
|
89
|
-
})
|
|
88
|
+
});
|
|
90
89
|
|
|
91
90
|
await sdk.organization.schoolContact.guardians.list(studentUserId, {
|
|
92
91
|
page: 1,
|
|
93
92
|
pageSize: 20,
|
|
94
|
-
})
|
|
93
|
+
});
|
|
95
94
|
|
|
96
|
-
await sdk.organization.schoolContact.myFamily.get({ pageSize: 50 })
|
|
95
|
+
await sdk.organization.schoolContact.myFamily.get({ pageSize: 50 });
|
|
97
96
|
|
|
98
97
|
await sdk.organization.schoolContact.teachers.list({
|
|
99
98
|
classId,
|
|
100
99
|
isHeadTeacher: true,
|
|
101
100
|
page: 1,
|
|
102
101
|
pageSize: 20,
|
|
103
|
-
})
|
|
102
|
+
});
|
|
104
103
|
```
|
|
105
104
|
|
|
105
|
+
`teachers.list` 的每个结果同时提供两个方向的班主任信息:
|
|
106
|
+
|
|
107
|
+
- `teacher.managedClasses`:该老师当前作为班主任管理的全部班级。
|
|
108
|
+
- `class.headTeachers`:该班级当前的全部班主任。
|
|
109
|
+
- `source`:班级身份来源,值为 `dingtalk_school_contact` 或 `manual`。
|
|
110
|
+
|
|
111
|
+
这两个字段可直接用于“我管理的班级”和“班级班主任”场景,不要在应用中自行维护第二份映射。
|
|
112
|
+
|
|
106
113
|
关系列表支持以下服务端过滤条件:
|
|
107
114
|
|
|
108
115
|
- `userId`、`guardianUserId`、`studentUserId`:平台用户 ID。
|
|
@@ -128,38 +135,38 @@ await sdk.organization.schoolContact.teachers.list({
|
|
|
128
135
|
App Function、Automation 或 Workflow 中需要复用关系时,调用对应的受控后端桥接:
|
|
129
136
|
|
|
130
137
|
```ts
|
|
131
|
-
export default async function(ctx, input) {
|
|
138
|
+
export default async function (ctx, input) {
|
|
132
139
|
const result = await ctx.organization.schoolContact.relations.list({
|
|
133
140
|
guardianUserId: input.guardianUserId,
|
|
134
141
|
page: 1,
|
|
135
142
|
pageSize: 20,
|
|
136
|
-
})
|
|
143
|
+
});
|
|
137
144
|
|
|
138
|
-
return result.items
|
|
145
|
+
return result.items;
|
|
139
146
|
}
|
|
140
147
|
```
|
|
141
148
|
|
|
142
149
|
查询某位老师担任班主任的班级:
|
|
143
150
|
|
|
144
151
|
```ts
|
|
145
|
-
export default async function(ctx, input) {
|
|
152
|
+
export default async function (ctx, input) {
|
|
146
153
|
return await ctx.organization.schoolContact.teachers.list({
|
|
147
154
|
userId: input.teacherUserId,
|
|
148
155
|
isHeadTeacher: true,
|
|
149
156
|
page: 1,
|
|
150
157
|
pageSize: 100,
|
|
151
|
-
})
|
|
158
|
+
});
|
|
152
159
|
}
|
|
153
160
|
```
|
|
154
161
|
|
|
155
162
|
对应方法为:
|
|
156
163
|
|
|
157
164
|
```ts
|
|
158
|
-
ctx.organization.schoolContact.relations.list(params)
|
|
159
|
-
ctx.organization.schoolContact.teachers.list(params)
|
|
160
|
-
ctx.organization.schoolContact.children.list(userId, params)
|
|
161
|
-
ctx.organization.schoolContact.guardians.list(userId, params)
|
|
162
|
-
ctx.organization.schoolContact.myFamily.get(params)
|
|
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);
|
|
163
170
|
```
|
|
164
171
|
|
|
165
172
|
运行时使用平台提供的真实操作者做权限校验。不要信任页面 input 中伪造的租户、角色或当前用户 ID。
|
|
@@ -168,31 +175,31 @@ ctx.organization.schoolContact.myFamily.get(params)
|
|
|
168
175
|
|
|
169
176
|
```ts
|
|
170
177
|
interface SchoolContactRelation {
|
|
171
|
-
relationId: string
|
|
172
|
-
relationCode: string | null
|
|
173
|
-
relationName: string | null
|
|
178
|
+
relationId: string;
|
|
179
|
+
relationCode: string | null;
|
|
180
|
+
relationName: string | null;
|
|
174
181
|
guardian: {
|
|
175
|
-
userId: string
|
|
176
|
-
dingtalkUserId: string
|
|
177
|
-
name: string
|
|
178
|
-
mobile: string | null
|
|
179
|
-
}
|
|
182
|
+
userId: string;
|
|
183
|
+
dingtalkUserId: string | null;
|
|
184
|
+
name: string;
|
|
185
|
+
mobile: string | null;
|
|
186
|
+
};
|
|
180
187
|
student: {
|
|
181
|
-
userId: string
|
|
182
|
-
dingtalkUserId: string
|
|
183
|
-
name: string
|
|
184
|
-
mobile: string | null
|
|
185
|
-
}
|
|
188
|
+
userId: string;
|
|
189
|
+
dingtalkUserId: string | null;
|
|
190
|
+
name: string;
|
|
191
|
+
mobile: string | null;
|
|
192
|
+
};
|
|
186
193
|
class: {
|
|
187
|
-
id: string
|
|
188
|
-
dingtalkClassId: string
|
|
189
|
-
name: string
|
|
190
|
-
campusName: string | null
|
|
191
|
-
periodName: string | null
|
|
192
|
-
gradeName: string | null
|
|
193
|
-
}
|
|
194
|
-
syncedAt: string
|
|
195
|
-
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";
|
|
196
203
|
}
|
|
197
204
|
```
|
|
198
205
|
|
|
@@ -200,22 +207,33 @@ interface SchoolContactRelation {
|
|
|
200
207
|
|
|
201
208
|
```ts
|
|
202
209
|
interface SchoolContactTeacherMembership {
|
|
203
|
-
membershipId: string
|
|
210
|
+
membershipId: string;
|
|
204
211
|
teacher: {
|
|
205
|
-
userId: string
|
|
206
|
-
dingtalkUserId: string
|
|
207
|
-
name: string
|
|
208
|
-
mobile: string | null
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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";
|
|
214
230
|
}
|
|
215
231
|
```
|
|
216
232
|
|
|
217
233
|
手机号可能为空。`sync.state=disabled` 表示管理员关闭了后续同步,现有数据仍保留但可能过期;业务页面在对时效敏感时应展示 `lastSuccessfulSyncAt`。
|
|
218
234
|
|
|
235
|
+
平台管理员人工添加的班级身份也会由 `teachers.list` 返回,并标记 `source=manual`。人工家长/学生身份会维护对应平台身份和班级归属,但不会自动生成监护关系;只有明确存在的家庭关系才会出现在 `relations`、`children`、`guardians` 和 `myFamily`。
|
|
236
|
+
|
|
219
237
|
## 持久化边界
|
|
220
238
|
|
|
221
239
|
- 平台关系是当前事实,不要复制全量关系到应用表单或 Data View。
|