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 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 和姓名;应用可显式声明 `:self:read` / `:class:read` 收紧范围。完整示例见 [`docs/dingtalk-school-contact-relations.md`](docs/dingtalk-school-contact-relations.md)。
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": ["pdf", "docx", "xlsx", "pptx", "txt", "csv", "json"],
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
- const forwarded = [...args];
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
- if (!target.environmentId || !target.targetName) return steps;
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 = withManagedReleaseForwardedFlags(
2082
- step.id,
2083
- step.args,
2084
- flags
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 | Read next | First action |
61
- | --- | --- | --- |
62
- | Diagnose, snapshot, compare, explain an error | `openxiangda-inspect` | read-only snapshot/inspect |
63
- | Login, profile, release, CLI/state issue | `openxiangda-core` | resolve explicit profile and workspace |
64
- | Create or bind an app | `openxiangda-app` | inspect local binding, then init/bind |
65
- | Form/schema/data view | `openxiangda-form` | inspect the target form and schema |
66
- | React/classic page | `openxiangda-page` | inspect the target route/page |
67
- | Function, automation, workflow, JS_CODE | `openxiangda-workflow-automation` | inspect the exact resource codes |
68
- | Roles, permission groups, data scope | `openxiangda-permission-settings` | produce the permission matrix |
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 | `openxiangda-architecture-design` plus the relevant domain skill | run the design gate |
71
- | External backend/OpenAPI/AK-SK | `openxiangda-open-api` | describe the exact operation |
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. Keep 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
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
- | 家长 | `SCHOOL_GUARDIAN` | 在家校成员关系中作为 guardian |
37
- | 学生 | `SCHOOL_STUDENT` | 在家校成员关系中作为 student |
38
- | 老师 | `SCHOOL_TEACHER` | 在家校成员关系中作为 teacher |
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
- sdk.context.permissions?.platformRoleCodes?.includes("SCHOOL_HEAD_TEACHER")
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
- class: SchoolContactRelation["class"]
211
- isHeadTeacher: boolean
212
- syncedAt: string
213
- syncState: "current" | "not_synced" | "disabled"
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。