@oj-module/cli 0.2.48 → 0.2.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/devkit/README.md +9 -7
  2. package/devkit/SKILL.md +53 -14
  3. package/devkit/manual.md +203 -3
  4. package/devkit/wecom-daikaifa.md +100 -1
  5. package/devkit/wecom-internal-app.md +643 -0
  6. package/package.json +2 -2
  7. package/shell-dist/assets/runtime.js +2 -2
  8. package/shell-dist/index.html +2 -2
  9. package/shell-dist/versions.json +3 -3
  10. package/src/__tests__/build-backend.test.ts +80 -0
  11. package/src/build.ts +50 -0
  12. package/src/init.ts +5 -4
  13. package/templates/config.yaml +28 -6
  14. package/templates/openapi.json +174 -2
  15. package/templates/src/auth/api/_shared/wecom/admin.ts +87 -7
  16. package/templates/src/auth/api/_shared/wecom/client.ts +42 -6
  17. package/templates/src/auth/api/_shared/wecom/config.ts +256 -46
  18. package/templates/src/auth/api/_shared/wecom/contacts.ts +7 -1
  19. package/templates/src/auth/api/_shared/wecom/events.ts +25 -6
  20. package/templates/src/auth/api/_shared/wecom/login.ts +44 -4
  21. package/templates/src/auth/api/_shared/wecom/replay.ts +44 -3
  22. package/templates/src/auth/api/_shared/wecom/store.ts +61 -12
  23. package/templates/src/auth/api/_shared/wecom/sync.ts +25 -1
  24. package/templates/src/auth/api/migrations/0003__credential_kind.mysql.sql +3 -0
  25. package/templates/src/auth/api/migrations/0003__credential_kind.pg.sql +3 -0
  26. package/templates/src/auth/api/migrations/0003__credential_kind.sql +9 -0
  27. package/templates/src/auth/api/schema.yaml +2 -0
  28. package/templates/src/auth/api/wecom/admin/api.ts +112 -11
  29. package/templates/src/auth/api/wecom/api.ts +151 -59
  30. package/templates/src/auth/api/wecom/sync/api.ts +21 -5
  31. package/templates/src/auth/web/__client/api.schemas.ts +22 -1
  32. package/templates/src/auth/web/__client/api.ts +47 -1
  33. package/templates/src/login/web/locales/en-US.json +2 -0
  34. package/templates/src/login/web/locales/zh-CN.json +2 -0
  35. package/templates/src/login/web/pages/login.tsx +39 -3
  36. package/templates/src/system/web/locales/en-US.json +24 -1
  37. package/templates/src/system/web/locales/zh-CN.json +24 -1
  38. package/templates/src/system/web/pages/departments/sync-bar.tsx +32 -7
  39. package/templates/src/system/web/pages/wecom/index.tsx +164 -46
  40. package/templates/tests/wecom-admin.spec.ts +7 -7
  41. package/templates/tests/wecom-connector.spec.ts +38 -8
  42. package/templates/tests/wecom-corp.spec.ts +523 -0
  43. package/templates/tests/wecom-endpoints.spec.ts +73 -7
  44. package/templates/tests/wecom-org-sync.spec.ts +7 -1
  45. package/vendor/host-versions.json +3 -3
package/devkit/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  面向用 `ojm` 开发前后端一体工程的开发者与 AI agent。本目录是**源文件**,
4
4
  发布时由 cli 的 `prepack`(`packages/cli/scripts/sync-devkit.mjs`)把其中
5
- **ojm 侧的 8 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
5
+ **ojm 侧的 9 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
6
6
  发布,再由 `ojm init` 分发到工程内。
7
7
 
8
8
  整棵树覆盖前后端两侧,但仍是**两套独立分发的 skill**(同名不同物,别混):
9
9
 
10
10
  | 侧 | 本仓位置 | 用户工程里的落点 | 分发链 |
11
11
  |---|---|---|---|
12
- | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 8 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包 → `ojm init` 第 4b 步 |
12
+ | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 9 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包 → `ojm init` 第 4b 步 |
13
13
  | oj API 开发(后端 handler、全局对象、配置、测试) | `oj-api-dev/` 子目录(快照) | `.claude/skills/oj-api-dev/` | **oj 二进制自带**(`bin/devkit/`)→ `ojm init` 第 4 步 |
14
14
 
15
15
  > **`oj-api-dev/` 子目录是快照,不是本仓维护的文档**:它与本机 oj 的
@@ -24,12 +24,13 @@
24
24
  | 文件 | 用途 |
25
25
  |---|---|
26
26
  | `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
27
- | `manual.md` | 模块开发手册(13 章):工程结构(module-root 单树 + 两处产物根;含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读;§1.8 `#` 别名与 TS 投影)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发(含 §11.1.1 引用式方案)、签名与信任;含 §13.1.1 web.route_prefix 自定义根路径 |
27
+ | `manual.md` | 模块开发手册(14 章):工程结构(module-root 单树 + 两处产物根;含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读;§1.8 `#` 别名与 TS 投影)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发(含 §11.1.1 引用式方案)、签名与信任、§13.1.1 web.route_prefix 自定义根路径、**§14 组织架构与通讯录**(内置部门/成员底座 + 共享读面 7 端点 + `scope` 两级门禁 + 归属权四条规则 + 源适配器接点,选人与按部门算人都读它) |
28
28
  | `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
29
29
  | `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路五步跑通(setup → publish → build-index → verify → 安装) |
30
30
  | `sql-builder-missing-capabilities-pr.md` | 构造器能力缺口 PR 参考(**仅框架仓库**):7 项诉求的闭环记录 |
31
31
  | `menu-rules.md` | 方案菜单归组规则专题:单根模块约定、自定义根路径 `web.route_prefix`、`category` 三值语义、admin 重写与视觉归组(手册 §13 的展开版) |
32
- | `wecom-daikaifa.md` | 企业微信代开发集成:**凭据清单**(6 项 `vars` + 可信域名/可信 IP 备案)、首次开通顺序(含 `suite_ticket` 依赖)、**用户 / 开发 / 部署三角色手册**、错误文案速查 |
32
+ | `wecom-daikaifa.md` | 企业微信代开发集成:**凭据清单**(8 项 `vars`,按 Tier A/B/C 分层的来源与必填时序 + 可信域名/可信 IP 备案)、首次开通顺序(含 `suite_ticket` 依赖与引导期死锁的破法)、**用户 / 开发 / 部署三角色手册**、错误文案速查、与自建应用共存的语义变化 |
33
+ | `wecom-internal-app.md` | 企业微信**企业内部应用(自建应用)**集成:5 项 `wecom_corp_*` 凭据 + 2 项复用、回调只填「通讯录同步」一处、「验证并接入」四步自检、可见范围与可信 IP/域名、单企业私有化的边界(自建应用收不到成员变更事件)、共存配置、故障速查 |
33
34
 
34
35
  后端 handler 本身的写法(`json` 信封、`db` 查询、`manifest.yaml`、`oj test`)、
35
36
  站点挂载(`mounts`)、密钥信封(`secrets`)、vars、cors、schema 校验、broker/tasks/oidc/ws 等
@@ -71,7 +72,7 @@
71
72
  ```sh
72
73
  # 本 devkit(ojm 模块开发):拷入工程的 Claude Code skill 目录
73
74
  mkdir -p .claude/skills/ojm-module-dev
74
- cp SKILL.md manual.md README.md wecom-daikaifa.md .claude/skills/ojm-module-dev/
75
+ cp SKILL.md manual.md README.md wecom-daikaifa.md wecom-internal-app.md .claude/skills/ojm-module-dev/
75
76
 
76
77
  # 后端 API 开发另装 oj 自带的 devkit(独立 skill):
77
78
  # 权威源随 oj 二进制分发——本机安装在 ~/.local/share/oj/devkit/,
@@ -91,7 +92,8 @@ cp ~/.local/share/oj/devkit/SKILL.md ~/.local/share/oj/devkit/api-manual.md .cla
91
92
  `api-manual.md` + 有则拷的 `sql-builder-guide.md` / `oj-test-guide.md` +
92
93
  `global.d.ts` → `.claude/skills/oj-api-dev/`;
93
94
  - **第 4b 步**:从 cli 包内置 `devkit/`(prepack 由本目录顶层同步)拷
94
- `SKILL.md` / `manual.md` / `README.md` / `wecom-daikaifa.md` → `.claude/skills/ojm-module-dev/`。
95
+ `SKILL.md` / `manual.md` / `README.md` / `wecom-daikaifa.md` / `wecom-internal-app.md`
96
+ → `.claude/skills/ojm-module-dev/`。
95
97
 
96
98
  ```mermaid
97
99
  flowchart LR
@@ -110,6 +112,6 @@ flowchart LR
110
112
  框架贡献者要把本仓快照同步到新版:从 `~/.local/share/oj/devkit/` 整目录覆盖
111
113
  `docs/devkit/oj-api-dev/`(10 个文件,保持逐字节一致以便对账)。
112
114
 
113
- > 本目录顶层 8 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
115
+ > 本目录顶层 9 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
114
116
  > 其它文档。`oj-api-dev/` 子目录是 oj 侧快照,同样自包含(其自身 README 即索引),
115
117
  > 但不随 cli 包发货、仅本仓参考。维护者视角的内容来源整理在 `framework-dev.md`(仅本仓)。
package/devkit/SKILL.md CHANGED
@@ -17,7 +17,8 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
17
17
 
18
18
  1. **读章**:建模块 → 手册 §2;加接口(handler .schema → oj openapi → 生成 → 对接)→ §3;
19
19
  接管通知/上传/菜单 → §4;换布局或加插槽 → §5;排查 → §8;
20
- **接通企业微信代开发(企微登录 / 多企业租户 / 代开发后台)→ `wecom-daikaifa.md`**。
20
+ **组织架构 / 通讯录(选人、按部门算收件人、接 LDAP 等新来源)→ §14**;
21
+ **接通企业微信(代开发 / 企业内部应用 / 两套共存)→ `wecom-daikaifa.md` + `wecom-internal-app.md`**。
21
22
  2. **建目录**:一个目录 = 一个模块——前端一侧 `src/<模块名>/web/`(`entry.ts` 必备,
22
23
  `pages/`、`locales/` 按需),后端一侧 `src/<模块名>/api/`(目录镜像路由的 `api.ts`,
23
24
  动词挂 `.schema`)。两侧可只存在一侧。
@@ -73,6 +74,12 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
73
74
  **拿用户/角色等信息:读走 `useUserStore` / `useAuthStore` / `useAccess`(`AccessControl` 门禁),
74
75
  写走契约 client(`ojm api` 生成模块自有 `__client`,或复用 runtime 导出的 `api/rbac` / `api/user`),
75
76
  绝不 import 框架 `#src/*`**——设计动机与红线见手册 §2.8。
77
+ - **组织架构/通讯录是内置底座,别自己搭**:部门与成员是 `_platform` 的两张租户表
78
+ (`sys_department` / `sys_dept_member`),外部来源(企微 / LDAP)只是往这套模型灌数据的**适配器**。
79
+ 要「谁在哪个部门」用共享读面 `/api/_platform/org/*` 或后端直调
80
+ `#/_platform/_shared/service/domain/org-read`(`resolveReadFace` 一次做完守卫);要「某部门含下级的所有人」
81
+ 用 `org/resolve`。**别自己建部门表、别自己调企微通讯录接口、别拉全树在 JS 里递归**。
82
+ 跨模块别名须在自己模块 `manifest.yaml` 的 `deps` 里声明 `_platform: ^0.1.0`。详见手册 §14。
76
83
  - **后端方法名是 `del`,不是 `delete`**(写成 `delete` 返回 405)。
77
84
  - **加一张新表是三件套,不是只写 `schema.yaml`**:`src/<模块>/api/migrations/{seq:04}__*.sql`(DDL 的真正定义(实际 SQL),
78
85
  DDL 只进 migrations)+ `schema.yaml`(声明,与 migration 逐字段一致)+ `manifest.yaml` 的 `tables:`
@@ -94,21 +101,41 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
94
101
  | 改 `schema.yaml` / `migrations/` / `config.yaml` | **必须重启** |
95
102
  | 前端 `src/<模块>/web/**` | 模块重建 + 浏览器自动刷新 |
96
103
 
97
- ## 企业微信代开发(接通真实套件)
104
+ ## 企业微信(接通真实套件 / 自建应用)
98
105
 
99
- 模板已内置企微登录(`src/auth/api/wecom/` + 登录页「企业微信」页签 + `/system/wecom` 代开发后台)。
100
- 接真实套件的**凭据清单、开通顺序、用户/开发/部署三视角、错误文案速查**全在
101
- `wecom-daikaifa.md`(同目录),这里只列最容易踩的四条:
106
+ 模板已内置企微登录(`src/auth/api/wecom/` + 登录页「企业微信」页签 + `/system/wecom` 后台)。
107
+ 两种来源、两本手册,**按客户形态选**:
102
108
 
103
- - **六项 `vars` 任一为空 → 企微登录整体 400**(`wecom_suite_id` / `wecom_suite_secret` /
104
- `wecom_callback_token` / `wecom_encoding_aes_key` / `wecom_enc_key` / `wecom_base_url`)。
105
- `wecom_base_url` 必须以 `/api/auth/wecom` 结尾,它派生 `<base>/event`(指令回调 URL)与 `<base>/callback`。
109
+ | 客户形态 | 读 | 凭据 |
110
+ |---|---|---|
111
+ | 多家企业(SaaS,扫码即装) | `wecom-daikaifa.md`(同目录) | 代开发套件 8 项 `vars`(Tier A/B/C) |
112
+ | 单企业私有化 | `wecom-internal-app.md`(同目录) | 自建应用 5 项 `wecom_corp_*` + 2 项复用 |
113
+ | 两者同时 | 两本都读,共存看 `wecom-daikaifa.md` §10 | 两套都填 + `wecom_allow_coexist: 'true'` |
114
+
115
+ 两本手册都有**凭据清单、开通顺序、用户/开发/部署三视角、错误文案速查**,接企微时先读它们。
116
+ 这里只列最容易踩的五条:
117
+
118
+ - **凭据填齐 = 启用,没有 `wecom_mode`**。只看「哪套变量填了」:`wecom_suite_id` + `wecom_suite_secret`
119
+ = 代开发;`wecom_corp_id` + `wecom_corp_secret` + `wecom_corp_agent_id` = 自建。两套都齐必须显式
120
+ `wecom_allow_coexist: 'true'`,否则首个请求 400(配置事故要看得见,不允许静默择一)。
121
+ `wecom_enc_key` / `wecom_base_url` 两种来源都要;`wecom_base_url` 必须以 `/api/auth/wecom` 结尾,
122
+ 它派生 `<base>/event`(回调 URL)与 `<base>/callback`(网页授权回跳)。
106
123
  - **`suite_ticket` 是被企微推来的**(每 10 分钟一次、30 分钟过期),所以「回调 URL 打通」必须
107
- 早于「生成开通链接」,否则 503 且看起来像代码坏了。
108
- - **新增企业不改配置、不重启**:企业管理员确认授权 → `create_auth` → `wecom_app_profile`。
124
+ 早于「生成开通链接」,否则 503 且看起来像代码坏了。自建模式没有 `suite_ticket`。
125
+ - **新增企业不改配置、不重启**(代开发):企业管理员确认授权 → `create_auth` → `wecom_app_profile`。
109
126
  同企业测试+正式两个应用是两行(主键含 `agent_id`),此时登录链接必须带 `&agent=`。
127
+ **自建模式反过来**:一套部署一家企业,档案行由 `/system/wecom` 点「验证并接入」写入;
128
+ 换过 Secret 必须重新点一次。
129
+ - **自建应用收不到成员变更事件**(官方 90970):要事件增量,只能让企业管理员另配
130
+ 「通讯录同步 → 接收事件服务器」,且该 URL 必须在企业主体域名下——这就是「SaaS 多企业只能走代开发」的由来。
131
+ 一个 `/event` 端点吃两套回调,靠逐个 Token 试签分辨,两组凭据不能串用。
110
132
  - **整页跳转拼 API base**:`window.location.assign(resolveApiBase() + wecomAuthorizePath(corp))`;
111
133
  拼站点 base 会被 SPA 兜底页吞掉(页面原地重载、一个请求都不发,控制台无报错)。
134
+ - **通讯录同步是「组织架构底座 + 企微适配器」,不是企微专属功能**:模板的
135
+ `src/auth/api/wecom/{sync,contacts}.ts` 只负责「取快照 → 喂给领域层」,模型与读面在 `_platform`
136
+ (手册 §14)。要按部门算人、要选人、要给组织架构接 LDAP,一律读 §14,不要在企微代码里加分支。
137
+ 同步/事件增量与回调解密是**两条独立纪律**:回调必须回 `success`,而事件处理失败**也照样回 `success`**
138
+ (重推补不回已判死的失败,兜底是下一轮全量同步)。
112
139
 
113
140
  ## 新模块 checklist
114
141
 
@@ -172,14 +199,26 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
172
199
  | 限速/节流/计数真实请求下永不累积(测试全绿) | 模块级 `Map`/`let` 不跨请求存活(每请求重建模块) | 状态落库(模板迁移 0009 先例)或 oj 持久面 |
173
200
  | `ojm build` 报 `introspect api.ts: jwt not configured` | 模块级语句碰运行时全局,introspect 空沙箱炸 | 移进 handler 函数体内(manual §1.7-4) |
174
201
  | 页签怎么点都关不掉、无报错提示 | `beforeClose` 守卫 fail-safe 否决(守卫抛错按取消处理) | 先查控制台 `[tabs] beforeClose` 报错;确认弹窗逻辑与 `resolve(true/false)` 收口(manual §6.4) |
202
+ | 组织架构读面 403「该部门不在可见范围内」 | `scope` 默认 `self`,只看得到自己那条部门路径 | 换 `scope=all`(需权限点 `platform.org.read.all`),或改问自己所在部门(manual §14.2) |
203
+ | 组织架构读面 400「只展开自己所在部门」 | `includeSub` 用在了祖先/兄弟部门上(self 档只允许展开自己所在部门) | 收窄到「我所在部门」,或申请 `scope=all`(manual §14.2) |
204
+ | `oj build` 报 `#/_platform/...` 找不到模块 | 跨模块别名没在 `manifest.yaml` 的 `deps` 里声明 | 加 `_platform: ^0.1.0`(manual §14.2) |
205
+ | 同步一轮之后本地手建的部门消失了 | 适配器漏了 `origin` 条件(同步与回收都必须只动自己的行) | `markSeen` / `reconcileHidden` 带 `origin`;回收只在「本轮完整零跳过」时执行(manual §14.5) |
206
+ | 组织架构里有这个人,通讯录搜不到 | 他是同步建档(`activated=0`,从没登录过本站),通讯录只出活人 | 正确行为;让他登录一次,或走组织架构读面(manual §14.6) |
175
207
 
176
208
  ## 手册
177
209
 
178
- `manual.md`(同目录)共 13 章:1 工程结构(module-root 单树 / 两处产物根 / **§1.7 handler 四红线** /
210
+ `manual.md`(同目录)共 14 章:1 工程结构(module-root 单树 / 两处产物根 / **§1.7 handler 四红线** /
179
211
  **§1.8 `#` 别名与 TS 投影**——oj 运行时实测钉出,后端 handler 必读)/ 2 写前端模块 /
180
212
  3 契约与代码生成 / 4 接管框架内置功能(provider)/ 5 布局与插槽 / 6 i18n 与样式 / 7 构建与发布 /
181
213
  8 排查 / 9 参考 / 10 CLI 子命令参考(ojm 原生命令 vs oj 透传命令的差异区分)/
182
- 11 方案包打包与分发 / 12 签名与信任 / 13 方案菜单归组规则。
214
+ 11 方案包打包与分发 / 12 签名与信任 / 13 方案菜单归组规则 /
215
+ 14 组织架构与通讯录(共享读面 7 端点、`scope` 两级门禁、归属权与「转为本地维护」、
216
+ 源适配器接点与三条禁令——选人 / 按部门算人 / 接 LDAP 都读它)。
217
+
218
+ 另有 `wecom-daikaifa.md`(同目录):企业微信**代开发**集成的凭据清单与三角色手册——凭据从哪取、
219
+ 怎么填、按什么顺序开通、用户/开发/部署各自怎么做、错误文案怎么查。**接代开发时先读它**。
183
220
 
184
- 另有 `wecom-daikaifa.md`(同目录):企业微信代开发集成的凭据清单与三角色手册——凭据从哪取、
185
- 怎么填、按什么顺序开通、用户/开发/部署各自怎么做、错误文案怎么查。**接企微时先读它**。
221
+ 另有 `wecom-internal-app.md`(同目录):企业微信**企业内部应用(自建应用)**的配置与使用手册——
222
+ 五项 `wecom_corp_*` 从哪取、回调为什么只填「通讯录同步」一处、「验证并接入」的四步自检、
223
+ 可见范围 / 可信 IP / 可信域名的处置、单企业私有化的边界(自建应用收不到成员变更事件)、
224
+ 以及和代开发共存时的配置与页面差异。**接自建应用时先读它**。
package/devkit/manual.md CHANGED
@@ -1569,7 +1569,7 @@ export default async (path: string, html: string) => {
1569
1569
  | 限速/节流/去重窗口在真实请求下永不累积(测试却全绿) | 模块级 `Map`/`let` 不跨请求存活——oj 每请求重建模块(§1.7-3) | 状态落库(模板 0009 先例)或走 oj 持久面 |
1570
1570
  | `ojm build` 报 `introspect api.ts: Error: jwt not configured`(dev 模式正常) | 模块级语句触碰运行时全局,introspect 空沙箱里炸(§1.7-4) | 移进 handler 函数体内按需执行 |
1571
1571
  | 点登录页「企业微信」后页面只闪一下、Network 里一个 auth 请求都没有 | 整页跳转拼成了**站点 base**(根部署下得到 `/auth/wecom/login`,被 SPA 兜底页吞掉) | 改用 `resolveApiBase()` 拼后端端点:`window.location.assign(resolveApiBase() + wecomAuthorizePath(corp))` |
1572
- | 企微登录恒 400 `企业微信未配置:config vars 缺少 …` | 部署常量没填全(六项任一为空即整体拒服务) | 见 `wecom-daikaifa.md` §2.1 的凭据表与缺项后果 |
1572
+ | 企微登录恒 400 `企业微信未配置:config vars 缺少 …` | 部署常量没填全(该来源的凭据任一为空即整体拒服务) | 代开发见 `wecom-daikaifa.md` §2.1,企业内部应用见 `wecom-internal-app.md` §2.1 的凭据表与缺项后果 |
1573
1573
 
1574
1574
  ---
1575
1575
 
@@ -1597,9 +1597,12 @@ export default async (path: string, html: string) => {
1597
1597
  写 handler 遇到 `json` / `db` / `kv` / `jwt` / 租户 / 测试的问题查它;
1598
1598
  站点挂载(`mounts`)、密钥信封(`secrets`)、配置变量(`vars`)、跨域(`cors`)、
1599
1599
  schema 校验(`schema_validation`)、`broker` / `tasks` / `oidc` / `ws` 等 oj 侧能力也都在这套手册里。
1600
- - **企业微信代开发对接**:本目录 `wecom-daikaifa.md`——六项部署常量从哪取/怎么填、
1600
+ - **企业微信代开发对接**:本目录 `wecom-daikaifa.md`——八项部署常量(Tier A/B/C)从哪取/怎么填、
1601
1601
  可信域名与可信 IP 备案、首次开通顺序(`suite_ticket` 依赖回调先通)、
1602
- 用户 / 开发 / 部署三种角色各自的操作、按错误文案索引的故障速查。接企微时先读它。
1602
+ 用户 / 开发 / 部署三种角色各自的操作、按错误文案索引的故障速查、与自建应用共存的语义变化。
1603
+ - **企业微信企业内部应用(自建)对接**:本目录 `wecom-internal-app.md`——五项 `wecom_corp_*` 从哪取、
1604
+ 回调只填「通讯录同步 → 接收事件服务器」一处、「验证并接入」的四步自检、可见范围决定同步范围、
1605
+ 单企业私有化的边界(自建应用收不到成员变更事件)、与代开发共存的配置。接企微时按客户形态选一本。
1603
1606
  - **框架团队内部**:本目录 `framework-dev.md`(改 `packages/runtime`、`packages/cli` 用,
1604
1607
  业务工程不适用)。
1605
1608
 
@@ -2126,6 +2129,202 @@ const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
2126
2129
 
2127
2130
  ---
2128
2131
 
2132
+ ## §14 组织架构与通讯录(直接用现成底座,别自己搭)
2133
+
2134
+ 站点内置一套组织架构底座:**部门 / 成员 / 归属权**由 `_platform` 内核负责,外部通讯录
2135
+ (企业微信,将来 LDAP)只是往这套底座灌数据的**适配器**。模块的活儿只有两种:
2136
+ **读**(谁在哪个部门)或**算**(某部门含下级一共有谁)。
2137
+
2138
+ 命中任一条就来本章,别另起一套:
2139
+
2140
+ | 需求 | 该用 | 别做 |
2141
+ |---|---|---|
2142
+ | 页面要选人 / 选部门 | 共享读面 `/api/_platform/org/*`(§14.2) | 自己查 `users` 全表、前端自己拼树 |
2143
+ | 通知 / 审批要「某部门及其下级的所有人」 | `POST /api/_platform/org/resolve`(或后端直调 `resolveUserIds`) | 前端拉全树、在 JS 里递归 |
2144
+ | 只是「找个人」 | 通讯录 `searchContacts`(§14.6) | 拿组织架构树当通讯录用 |
2145
+ | 要给组织架构接一个新来源(LDAP / 钉钉 / HR) | 领域层适配器接点(§14.5) | 另建一套表;或把外部字段写进 `sys_department` |
2146
+
2147
+ ### 14.1 数据在哪:两张租户表 + `users` 加三列
2148
+
2149
+ | 表 | 关键列 | 语义 |
2150
+ |---|---|---|
2151
+ | `sys_department` | `id` / `parent_id` / `name` / `name_en` / `order_no` | 结构:**`parent_id` 是本地 id,不是外部 id** |
2152
+ | | `origin` / `origin_id` | 归属权 + 外部主键。唯一约束是 `(tenant_id, origin, origin_id)` |
2153
+ | | `sync_visible` / `deleted` / `seen_job_id` | 同步态:可见性只由同步轮次维护;软删只由外部源的删除事件置;`seen_job_id` 是墓碑回收的正面证据 |
2154
+ | `sys_dept_member` | `user_id` / `dept_id` / `order_no` / `is_leader` / `is_main` | 多对多。**全是本地 id,与来源无关**——接任何适配器都不用动这张表 |
2155
+ | `users` | `activated` / `position` / `external_status` | 见下方第二段 |
2156
+
2157
+ 两条必须记住的语义:
2158
+
2159
+ 1. **`parent_id` 是本地 id**。本地手建的部门可以和同步来的部门混挂在一棵树上,外部重构层级
2160
+ 也不会打乱你本地的结构。
2161
+ 2. **`origin` 是归属权,不是「来源标签」**。`local` = 本站管理员全权 CRUD;
2162
+ `wecom` / `ldap` / `sso` = 对应适配器管辖。**任何同步与回收只作用于自己 `origin` 的行**——
2163
+ 这是「本地建的部门不会被第一轮同步抹掉」的根本机制,也是适配器可插拔与本地 CRUD 能共存的唯一原因。
2164
+
2165
+ `users` 那三列的用途别搞混:`activated` 是「**这个号被真人用过**」的证据(管理员建号 / 各来源登录
2166
+ 建档置 1,**同步建档默认 0**);`position` 是职务;`external_status` 是外部源的在职状态快照
2167
+ (**绝不自动改写 `is_forbidden`**,否则会和本站管理员的手动启停互相覆盖)。
2168
+
2169
+ ### 14.2 后端复用:共享读面(推荐直调领域函数)
2170
+
2171
+ 7 个端点,全在 `_platform` 模块下(前缀 `/api/_platform`):
2172
+
2173
+ | 端点 | 出什么 | 关键参数 |
2174
+ |---|---|---|
2175
+ | `GET /org/tree` | 部门**平表**(前端按 `parentId` 组装)+ 成员数 | `scope` |
2176
+ | `GET /org/departments` | 部门平铺分页(大企业别一次拉全树) | `scope`、`parent`(`0` = 只取根层)、`keyword`、`page`、`size`≤200 |
2177
+ | `GET /org/members` | 成员分页 + 部门名路径 | `scope`、`dept`、`includeSub`、`keyword`、`page`、`size` |
2178
+ | `GET /org/user` | 某人的部门路径 / 主部门 / 职务 | `scope`、`uid`(不给即自己) |
2179
+ | `GET /org/picker` | 人 + 部门混合(选人组件一次拿全) | `keyword`(**必填且 ≥2**)、`limit`≤50 |
2180
+ | `POST /org/resolve` | 部门(含子部门)→ `userIds`(去重升序) | body `deptIds` / `userIds` / `includeSub` |
2181
+ | `GET /org/is-member` | 某人是否在某部门(含子部门)下 | `dept` 必填、`uid`(不给即自己) |
2182
+
2183
+ **两级门禁**(默认最小集,要全量请申请权限点):
2184
+
2185
+ - `scope=self`(默认):只看「我在哪」——我所在部门 + 它的祖先链,以及这些部门里的直属成员。
2186
+ **不含兄弟部门**:组织架构含层级 + 人名,比通讯录敏感得多,且同步会把「从未登录本站、
2187
+ 无从同意」的全员灌进来。
2188
+ - `scope=all`:全量,需权限点 `platform.org.read.all`(`superadmin` / `admin` 天然通过)。
2189
+
2190
+ 三条硬限制(踩了只会觉得「接口坏了」):
2191
+
2192
+ - 限速 60 次/分/人(键 `org:<uid>`);批量场景用 `resolve` / 分页,别循环打单条。
2193
+ - `keyword` 一律 ≥2 字:读面是**枚举面**,单字能把整份名册拖走——与通讯录同一条红线。
2194
+ - `includeSub` 在 `self` 档**只对「我所在部门」生效**:对祖先部门请求展开会 **400**。
2195
+ 这是刻意的——祖先一展开就是整棵子树,等于绕过自身路径限制;宁可报错也不静默截断成
2196
+ 「看着对其实少人」的结果。
2197
+
2198
+ 字段口径:读面**一律不出** `origin` / `is_leader` / `external_status`,且**返回字段与 `scope` 无关**。
2199
+ 消费方不用按档位分支解析——归属与在职是管理语义,不归消费面。
2200
+
2201
+ **后端直调**(同进程内,不走 HTTP,省一次序列化与鉴权):
2202
+
2203
+ ```ts
2204
+ import { resolveReadFace, resolveUserIds } from "#/_platform/_shared/service/domain/org-read";
2205
+
2206
+ async function post() {
2207
+ await respond(async () => {
2208
+ // 登录态 → 租户自洽 → 限速 → scope 解析 → all 档验权限 → 算可见集,一次做完
2209
+ const reader = await resolveReadFace((http.query as Record<string, unknown>).scope);
2210
+ const userIds = await resolveUserIds(reader, { deptIds: [42], includeSub: true });
2211
+ return { userIds };
2212
+ });
2213
+ }
2214
+ ```
2215
+
2216
+ `resolveReadFace` 是那 7 个端点**共用的守卫入口**——自己写 handler 也用它,别重抄一遍守卫
2217
+ (重抄必然长歪;它内部的顺序是「先租户自洽再限速」,伪造租户头的人不该消耗别人的限速窗口)。
2218
+
2219
+ > ⚠️ 跨模块别名要在**自己模块** `manifest.yaml` 的 `deps` 里声明 `_platform: ^0.1.0`,
2220
+ > 否则 `oj build` 构建期直接报错(§1.8)。这是「跨模块依赖」的门禁,不是可选的注释。
2221
+
2222
+ ### 14.3 前端怎么用:给自己的模块出端点,别借别人的 client
2223
+
2224
+ 前端模块能 import 的只有三类(§2.6),**别人的 `__client` 与 `#/_platform` 都不在其中**。
2225
+ 所以前端消费组织架构的唯一正路是「借壳」:
2226
+
2227
+ 1. 在**自己模块**的 handler 里调 `resolveReadFace` + 需要的那几个读函数(§14.2);
2228
+ 2. 跑 `oj openapi` + `ojm api`,生成本模块的 `src/<模块>/web/__client/api.ts`;
2229
+ 3. 页面用本模块 client 调。
2230
+
2231
+ 多一层 handler 换来三件事:接口形状由**你自己的契约**决定(只出页面要的字段)、框架内部实现不进你的
2232
+ bundle、契约变更走你自己的版本号。仓库里 `src/system/web/pages/departments` 的组织架构页就是这条路的
2233
+ 另一个方向(它消费的是管理端 `/api/system/departments`,见 §14.4)。
2234
+
2235
+ ### 14.4 管理端 CRUD:只有「管组织架构」的模块才该碰
2236
+
2237
+ 管理端在 `system` 模块下,一律 `assertSystemAdmin` 门禁:
2238
+
2239
+ | 端点 | 用途 |
2240
+ |---|---|
2241
+ | `GET /api/system/departments` | 部门平表 + 成员数(**含**不可见与软删壳,管理员要看全量) |
2242
+ | `POST` / `PUT` / `DELETE /api/system/departments` | 增 / 改 / 删(本地部门才可删;有子部门时**拒绝**而不是级联删) |
2243
+ | `PUT /api/system/departments/move` | 改父节点 |
2244
+ | `POST /api/system/departments/detach?id=` | **「转为本地维护」**:`origin` 改为 `local`,此后同步不再改写这一行 |
2245
+ | `GET` / `POST` / `PUT` / `DELETE /api/system/departments/members?id=` | 部门成员的增删查改 |
2246
+
2247
+ 四条归属规则(写在领域层,绕不过去):
2248
+
2249
+ 1. **本地的行**(`origin='local'`):全权 CRUD。
2250
+ 2. **外部源的行**:结构字段(名称 / 层级 / 删除)**不可本地改**——改了下一轮同步就被覆盖,
2251
+ 所以直接拒。加人/移人不受限:「谁在这个部门」本站始终可调。
2252
+ 3. **要接管某一行**就用「转为本地维护」(`detach`);反向操作(转回外部源)**不提供**——
2253
+ 转回去后下一次同步立刻按源口径改写,语义不可控。
2254
+ 4. **删本地部门是软删壳还是真删**:本地删除走真删(有子部门则拒绝);`deleted=1` 只由外部源的
2255
+ 删除事件置,两者别混用——「看不见」和「没了」不是一回事。
2256
+
2257
+ ### 14.5 自己写一个源适配器(企微是现成范本)
2258
+
2259
+ 领域层**完全不认企微**:所有写路径都以「源」为参数。新适配器只需实现「取快照 → 喂给这几个函数」,
2260
+ 领域层与读面一行都不用改。
2261
+
2262
+ | 接点(`#/_platform/_shared/service/domain/...`) | 用途 | 纪律 |
2263
+ |---|---|---|
2264
+ | `org::upsertDepartment({ origin, originId, parentId, name, orderNo })` | 按 `(origin, origin_id)` 幂等写部门,返回本地 id | 只写结构字段;**不动** `sync_visible` / `deleted` / `origin` |
2265
+ | `org::upsertMember({ userId, deptId, ... })` | 按 `(user_id, dept_id)` 幂等写成员关联 | |
2266
+ | `org::markSeen(origin, deptIds, jobId, deps)` | 标记「本轮同步命中」,并把 `sync_visible` 拉回 1 | 必带 `origin`,否则会误伤本地行 |
2267
+ | `org::reconcileHidden(origin, jobId, deps)` | 墓碑回收:上一轮见过、这一轮没见 → 置不可见 | **只在调用方确认本轮完整(零跳过)后调用** |
2268
+ | `org::markDeleted(origin, originId, deps)` | 外部源侧的删除事件 → `deleted=1`,**级联整棵子树** | 这是 `deleted=1` 的唯一写入点 |
2269
+ | `users::upsertSourceUser({ username, displayName, source, corpId })` | 外部源建档,返回 uid | `activated=0`(目录档);已「转为本地维护」的行一个字段都不改 |
2270
+
2271
+ **一次全量同步的顺序**(照抄即可):
2272
+
2273
+ ```
2274
+ 抢锁(本地租约,可抢回崩溃残留)
2275
+ → 遍历部门:upsertDepartment → markSeen
2276
+ → 遍历成员:upsertSourceUser → applyMemberDetail(写姓名/职务/在职 + 部门关联)
2277
+ → 只有「本轮完整且零跳过」才 reconcileHidden
2278
+ → 放锁,记录统计
2279
+ ```
2280
+
2281
+ 三条禁令(每一条都对应一次真实事故形状):
2282
+
2283
+ 1. **墓碑回收前必须确认本轮完整**。上游抖一下就被读成「这些部门被删了」,表现是组织架构大面积消失。
2284
+ 权限类错误(接口无权限)要**计数并跳过**、限流类要退避重试,别混进「完整」的判据里。
2285
+ 2. **事件增量是加速器,不是权威**。事件处理**任何失败都不回非 `success`**——上游拿不到 `success`
2286
+ 会重推三次,而重推补不回一次已经判死的失败,只会让企业管理员在后台看到「应用安装失败」;
2287
+ 真正的兜底永远是下一轮全量同步。这条与「回调必须回 `success`」是一体两面。
2288
+ 3. **别复用别家的任务表**。企微那对 `wecom_sync_job` / `wecom_sync_lock` 带 `corp_id` / `agent_id` 列,
2289
+ LDAP 直接复用会被这两个字段绑死——**先另建 `ldap_sync_*` 把模型跑通**,两个源都成立再谈泛化
2290
+ (泛化要改已发布的迁移,代价远大于两张小表)。
2291
+
2292
+ **接 LDAP 前必须先回答的问题**:oj 的 `axis("ldap")` 有没有等价 `ldapsearch` 的遍历能力。
2293
+ 现状 `auth/.../ldap-login` 只有**认证**、没有目录同步;没有 search 就只能裸连或自带 client。
2294
+ 这是开工第一天的第一个问题,别边写边猜。
2295
+
2296
+ **验收**(这组断言能同时证明「适配器隔离」与「本地 CRUD 保底」):
2297
+ LDAP 侧改名 / 删人后,`origin='ldap'` 的行跟着变,而 `origin='wecom'` 的行与本地手建的行
2298
+ **一行不动**;本地手建的部门没有被同步覆盖。
2299
+
2300
+ ### 14.6 通讯录 ≠ 组织架构
2301
+
2302
+ | | 通讯录 `searchContacts` / `contactDetail` | 组织架构读面 `/org/*` |
2303
+ |---|---|---|
2304
+ | 形状 | 扁平(id / username / 显示名;详情才给 email / 头像) | 层级(部门树 + 成员归属 + 部门路径) |
2305
+ | 谁可见 | 登录即可,`keyword` ≥2、限速 | 登录 + `scope` 门禁(默认只看自己那条路径) |
2306
+ | **只出活人** | ✅ 只出 `activated=1` | 出全部可见成员(含同步灌进来的目录档) |
2307
+ | 典型用途 | 「找个人」「选人」 | 「算收件人」「按部门做访问控制」 |
2308
+
2309
+ 通讯录那条「只出 `activated=1`」不是优化而是**红线**:同步会把从未登录本站的全员建档,
2310
+ 若通讯录也出这批号,任何登录账号都能用 `keyword`(60 次/分)把整份企业名册遍历走。
2311
+ 所以「同步来的号不出现在通讯录里」是**正确行为**,不是 bug——要全员可见,让那个人先登录一次,
2312
+ 或者走组织架构读面(有 `scope` 门禁)。
2313
+
2314
+ ### 14.7 症状 → 病根 → 药方
2315
+
2316
+ | 症状 | 病根 | 药方 |
2317
+ |---|---|---|
2318
+ | 读面 403「该部门不在可见范围内」 | `scope` 是默认的 `self`,那部门不在你自己那条路径上 | 换成 `scope=all`(需 `platform.org.read.all`),或改问自己所在部门 |
2319
+ | 读面 400「只展开自己所在部门」 | `includeSub` 撞上了祖先部门或兄弟部门 | 只在「我所在部门」上展开;要全量就申请权限点 |
2320
+ | `oj build` 报 `#/_platform/...` 找不到模块 | `manifest.yaml` 的 `deps` 没声明 `_platform` | 加 `_platform: ^0.1.0` |
2321
+ | 同步后本地手建的部门「不见了」 | 适配器漏了 `origin` 条件(`markSeen` / `reconcileHidden` 都必带) | 带上 `origin`;回收只在完整轮次执行 |
2322
+ | 管理员改不动某个部门(400) | 该行 `origin` 不是 `local` | 先「转为本地维护」(`POST /api/system/departments/detach?id=`) |
2323
+ | 组织架构页有人、通讯录搜不到他 | 那人是同步建档(`activated=0`),从没登录过本站 | 正确行为,见 §14.6 |
2324
+ | 同步进度条一开就 429 | 限速按「单片 12 秒打满 → 天然 5 次/分」配平(给 10 次余量),自己并发打就超了 | start/cancel 串行驱动;status 不限速,可 1~2 秒轮询 |
2325
+
2326
+ ---
2327
+
2129
2328
  ## 附录:章节目录
2130
2329
 
2131
2330
  | 章 | 内容 | 什么时候读 |
@@ -2143,3 +2342,4 @@ const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
2143
2342
  | §11 方案包:打包与分发 | build --bundle、Form A/B、分发源、索引规范、引用式方案(api 段省略 tables/deps,引用包内原生 manifest) | 发布方案包 |
2144
2343
  | §12 签名与信任 | keygen/sign/install 发布流程、信任库、TOFU、bootstrap key | 首次签名发布;管理信任 |
2145
2344
  | §13 方案菜单归组规则 | 单根模块约定(根路由=第一级菜单)、自定义根路径 `web.route_prefix`(构建写 modules.json、运行时只改根段、getModuleRoutePrefix 单点消费)、category 三值(普通/hidden/admin)、admin 真重写 `/system/<模块 root>/*` + 视觉归组、menu.main 统一、模块内路径助手、开发者 DO/DON'T | 写方案要让菜单按模块分组、admin 归系统设置 |
2345
+ | §14 组织架构与通讯录 | 内置底座(两张租户表 + `users` 三列)、共享读面 7 端点与两级 `scope` 门禁、前端怎么「借壳」出自己端点、归属权四条规则与管理端 CRUD、源适配器接点与三条禁令(企微为范本,LDAP 前置)、通讯录与组织架构的分工 | 页面要选人/选部门、通知要按部门算收件人、要接新的组织数据来源 |
@@ -17,10 +17,17 @@
17
17
  | 企业管理员(客户的 IT) | §2.5 §2.6 §6.8 §7.6-B §8 | 开四项通讯录权限、选对授权模式、读懂同步页面上的卡片 |
18
18
  | 企业员工(用户) | §5 | 怎么进、要不要注册、进不去怎么办 |
19
19
 
20
+ > 本文件只讲**代开发**。要把同一套部署接成一家企业的**企业内部应用(自建应用)**,
21
+ > 或让两种来源同时服务,读同目录 `wecom-internal-app.md`;两者共存的配置与页面差异见本文 **§10**。
22
+
20
23
  ---
21
24
 
22
25
  ## 1. 这套东西在做什么
23
26
 
27
+ > **术语先分清**:代开发应用在客户企微后台里**也显示为一个「自建应用」**,区别在于它的权限与回调
28
+ > 都由服务商的模板决定、由企业管理员扫码授权装配,而不是客户自己建。客户口中的「自建应用」到底指
29
+ > 哪一种,第一次对接时必须问清——两者本文只讲前者,后者见 `wecom-internal-app.md`。
30
+
24
31
  一句话:**企业管理员在自己企业里「授权安装」一次,之后该企业员工就能用企微身份直接登录
25
32
  本站,不需要注册**。本站同时服务多个企业,靠租户列隔离数据。
26
33
 
@@ -86,7 +93,8 @@
86
93
  `get_customized_auth_url`,该接口只认 `provider_access_token`,而它必须由服务商 corpid + provider_secret 换得。
87
94
  缺它**不影响登录**(login 走 suite 流程),但 `/system/wecom` 点「生成开通链接」会 400 点名缺这两项。
88
95
 
89
- Tier A 任一为空 → 服务启动即 400 拒绝(宁可不登录,也不明文存 `permanent_code`)。
96
+ Tier A 任一为空 → 首个企微相关请求即 400 拒绝(配置按请求读取,不是启动期校验;宁可不登录,
97
+ 也不明文存 `permanent_code`)。
90
98
  Tier B 为空时服务仍可启动并响应 echostr,但 suite 流程会 400——先配 Tier A 把回调验证跑通、
91
99
  创建模版、拿到 suite 凭据,再补全 Tier B 即可,无需鸡生蛋。Tier C 可与 Tier B 同一阶段补全。
92
100
 
@@ -330,6 +338,11 @@ pnpm dev
330
338
 
331
339
  ### 6.3 数据面:三张共享表 + 五张租户表
332
340
 
341
+ > **模型归 `_platform`,企微只是适配器**:`sys_department` / `sys_dept_member` 与共享读面
342
+ > `/api/_platform/org/*` 属于**组织架构底座**——不接企微的部署也有它,接 LDAP 也只是换一个喂数据的源。
343
+ > 要选人、要按部门(含下级)算收件人、要给组织架构加新来源,读 `manual.md` 的 **§14**;
344
+ > 本节只讲企微这一半的落库事实与坑。
345
+
333
346
  | 表 | 是否租户表 | 存什么 | 为什么这样放 |
334
347
  |---|---|---|---|
335
348
  | `wecom_app_profile` | **共享**(`tenant: false`) | 企业 × 应用 → 密文 `permanent_code`、`corp_name`、`env`、`status`、`auth_mode` | 它是「corp → 租户」映射的**权威表**;登录前还没确定租户,必须是部署级 |
@@ -674,3 +687,89 @@ grep -h "wecom change_contact\|wecom event ignored\|callback decrypt failed" log
674
687
  [ ] /system/departments 顶部状态条有「更新于 X」;树有名字、人挂在正确部门
675
688
  [ ] db.sqlite 与 wecom_enc_key 各自有备份
676
689
  ```
690
+
691
+ ---
692
+
693
+ ## 10. 与「企业内部应用(自建)」共存
694
+
695
+ 本部署可以同时接两种来源:**代开发**(服务商模板,扫码即装、可服务多家企业)与
696
+ **企业内部应用 / 自建应用**(企业自建,一套部署一家企业)。自建侧的五项凭据、接入流程、
697
+ 硬约束在同目录 `wecom-internal-app.md`;本节只讲「两者同时存在时」本文件哪些行为会变。
698
+
699
+ ### 10.1 什么时候需要共存
700
+
701
+ | 场景 | 说明 |
702
+ |---|---|
703
+ | 本站自己用自建应用 + 子公司 / 客户走代开发 | 最常见。两套档案的租户键都是 `corp_id`,天然隔离 |
704
+ | 迁移期:同一家既有代开发应用、又刚建了自建应用 | 需要清单里能分辨、事件能选对密钥、同步能带 agent |
705
+ | 只是从代开发迁到自建 | 不需要长期共存,按 §10.6 迁完清变量即可 |
706
+
707
+ ### 10.2 三行配置
708
+
709
+ ```yaml
710
+ # 代开发那套(不变,见 §2.1)
711
+ wecom_suite_id: 'ww…'
712
+ wecom_suite_secret: '…'
713
+ wecom_callback_token: '…'
714
+ wecom_encoding_aes_key: '…'
715
+
716
+ # 自建那套(见 wecom-internal-app.md §2.1)
717
+ wecom_corp_id: 'ww…'
718
+ wecom_corp_secret: '…'
719
+ wecom_corp_agent_id: '1000002'
720
+
721
+ # 两套凭据都填齐时必须显式开,否则首个请求 400
722
+ wecom_allow_coexist: 'true' # 取值 1 / true / yes
723
+ ```
724
+
725
+ 不开就报:`企业微信未配置:config vars 缺少 wecom_allow_coexist: 'true'(检测到代开发与自建应用两套凭据同时配置;确认要共存请显式开启)`。
726
+ 这是故意的:从代开发迁到自建却忘了清旧变量,属于配置事故,应当被看见而不是被静默择一。
727
+
728
+ 「启用哪几种来源」的判定**没有** `wecom_mode` 这类变量,只看「哪套凭据填齐了」——
729
+ 变量填了没有是客观事实,不会与运行期能力漂移。
730
+
731
+ ### 10.3 页面与清单的变化
732
+
733
+ - `/system/wecom` 两张卡片按需显示:「开通企业」(代开发,生成开通链接)与
734
+ 「企业内部应用(自建)」(验证并接入)。两者都在时顶部多一条「本部署同时启用」提示。
735
+ - 已开通企业清单新增**来源**列(代开发 / 企业内部应用);「授权模式」列对自建行显示 `—`
736
+ (自建没有「管理员授权 / 成员授权」之分,同步闸门恒通过)。
737
+ - 部署配置自检未通过时,页面顶部红条直接说出缺什么,而不是整页失败。
738
+
739
+ ### 10.4 两组回调凭据:同一个 URL,不同源,不能互相顶替
740
+
741
+ | 位置 | 用哪组凭据 | 谁填 |
742
+ |---|---|---|
743
+ | 代开发「指令回调 URL」(本文 §3) | `wecom_callback_token` / `wecom_encoding_aes_key` | 服务商在模板详情页填 |
744
+ | 自建「通讯录同步 → 接收事件服务器」 | `wecom_corp_callback_token` / `wecom_corp_encoding_aes_key` | 企业管理员在自己后台填 |
745
+
746
+ 两个地址**都是** `<wecom_base_url>/event`,服务端逐个 Token 试签分辨(SHA1 一次,成本可忽略),
747
+ 命中哪套就用哪套钥匙解密。所以:
748
+
749
+ - 不需要为自建应用再开一条路由,匿名路径白名单也不用加;
750
+ - 但两组凭据**不能串用**:把代开发那组填进自建的位置,只会在保存 URL 时验签失败;
751
+ - 排障时日志会带 `kind`:`kind=suite` / `kind=corp` 告诉你这条报文是哪一侧来的,别再靠猜。
752
+
753
+ ### 10.5 共存下代开发的语义变化(三处,都是「别误伤邻居」)
754
+
755
+ | 动作 | 现在只影响 | 原因 |
756
+ |---|---|---|
757
+ | 企业取消授权(`cancel_auth`) | 只停 `credential_kind='suite'` 的行;该企业仍有其它活跃应用时**不吊销会话** | 代开发取消授权不该把同一家企业的自建应用一起停掉 |
758
+ | 授权模式复核(`change_auth`) | 只遍历 suite 行 | 自建行没有 `auth_mode` 概念 |
759
+ | 组织架构同步 | 状态视图会回传 `agentId`,前端 start / cancel / status 原样带上 | 同一 `corp_id` 有两行时,不指定 agent 后端只能 400 |
760
+
761
+ 同一企业双应用的登录链接**要带 agent**(`/auth/wecom/login?corp=ww…&agent=…`);只带 `corp`
762
+ 会 400 `该企业开通了多个应用,登录链接必须带 agent 参数`。不带 `corp` 时按「本站自己那家企业」
763
+ 解析:**配了 `wecom_corp_*` 就把候选范围限定在那家企业**(agent 取配置里的),没配才从已装档案里解析。
764
+ 所以共存部署里「配了 corp 但还没点验证并接入」时,本站 `/login` 会 400
765
+ `本次部署配置的企业内部应用尚未接入:请在后台点「验证并接入」`——这是提示不是缺陷。
766
+
767
+ ### 10.6 迁移:代开发 → 自建(或反过来)
768
+
769
+ 1. 备好自建应用三项(+ 可选通讯录回调两项),填进 `vars`,重启;
770
+ 2. `/system/wecom` 点「验证并接入」→ 清单多一行 `credential_kind=corp`;
771
+ 3. 用一个普通员工验证登录(账号仍是 `wecom:<userid>`,数据不丢);
772
+ 4. 确认无误后清空 `wecom_suite_*` / `wecom_provider_*` / `wecom_callback_token` /
773
+ `wecom_encoding_aes_key`,去掉 `wecom_allow_coexist`,重启;
774
+ 5. 清单里代开发那几行**停用而不删**,留作回滚。
775
+