@oj-module/cli 0.2.47 → 0.2.49

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 (101) hide show
  1. package/devkit/README.md +1 -1
  2. package/devkit/SKILL.md +23 -4
  3. package/devkit/framework-dev.md +2 -2
  4. package/devkit/manual.md +221 -14
  5. package/devkit/module-dev-and-cdn-playground.md +2 -2
  6. package/devkit/wecom-daikaifa.md +242 -14
  7. package/package.json +2 -2
  8. package/shell-dist/assets/runtime.js +2 -2
  9. package/shell-dist/index.html +2 -2
  10. package/shell-dist/versions.json +3 -3
  11. package/src/__tests__/build-backend.test.ts +80 -0
  12. package/src/__tests__/templates-tenant-scope.test.ts +7 -2
  13. package/src/build.ts +50 -0
  14. package/src/init.ts +1 -1
  15. package/src/market/__tests__/bootstrap.test.ts +1 -1
  16. package/src/market/__tests__/setup.test.ts +7 -7
  17. package/src/market/bootstrap.ts +1 -1
  18. package/templates/README.md +1 -1
  19. package/templates/init.include.yaml +12 -0
  20. package/templates/openapi.json +2863 -549
  21. package/templates/src/_platform/api/_shared/service/domain/contacts.ts +7 -0
  22. package/templates/src/_platform/api/_shared/service/domain/org-read.ts +451 -0
  23. package/templates/src/_platform/api/_shared/service/domain/org.ts +566 -0
  24. package/templates/src/_platform/api/_shared/service/domain/users.ts +140 -2
  25. package/templates/src/_platform/api/_shared/service/infra/query.ts +2 -1
  26. package/templates/src/_platform/api/fixtures/l1-test-user.sql +1 -1
  27. package/templates/src/_platform/api/manifest.yaml +3 -0
  28. package/templates/src/_platform/api/migrations/0016__org.mysql.sql +41 -0
  29. package/templates/src/_platform/api/migrations/0016__org.pg.sql +40 -0
  30. package/templates/src/_platform/api/migrations/0016__org.sql +62 -0
  31. package/templates/src/_platform/api/org/departments/api.ts +51 -0
  32. package/templates/src/_platform/api/org/is-member/api.ts +33 -0
  33. package/templates/src/_platform/api/org/members/api.ts +55 -0
  34. package/templates/src/_platform/api/org/picker/api.ts +59 -0
  35. package/templates/src/_platform/api/org/resolve/api.ts +38 -0
  36. package/templates/src/_platform/api/org/tree/api.ts +37 -0
  37. package/templates/src/_platform/api/org/user/api.ts +46 -0
  38. package/templates/src/_platform/api/schema.yaml +48 -0
  39. package/templates/src/_platform/api/seed.mysql.sql +10 -5
  40. package/templates/src/_platform/api/seed.pg.sql +10 -5
  41. package/templates/src/_platform/api/seed.sql +13 -5
  42. package/templates/src/_platform/web/__client/api.schemas.ts +126 -0
  43. package/templates/src/_platform/web/__client/api.ts +170 -1
  44. package/templates/src/auth/api/_shared/ldap-login.ts +2 -0
  45. package/templates/src/auth/api/_shared/session.ts +5 -1
  46. package/templates/src/auth/api/_shared/wecom/admin.ts +3 -1
  47. package/templates/src/auth/api/_shared/wecom/client.ts +280 -32
  48. package/templates/src/auth/api/_shared/wecom/config.ts +12 -1
  49. package/templates/src/auth/api/_shared/wecom/contacts.ts +263 -0
  50. package/templates/src/auth/api/_shared/wecom/crypt.ts +17 -7
  51. package/templates/src/auth/api/_shared/wecom/events.ts +111 -26
  52. package/templates/src/auth/api/_shared/wecom/login.ts +188 -5
  53. package/templates/src/auth/api/_shared/wecom/payload.ts +116 -0
  54. package/templates/src/auth/api/_shared/wecom/replay.ts +44 -3
  55. package/templates/src/auth/api/_shared/wecom/store.ts +65 -0
  56. package/templates/src/auth/api/_shared/wecom/sync.ts +501 -0
  57. package/templates/src/auth/api/manifest.yaml +3 -0
  58. package/templates/src/auth/api/migrations/0002__org_sync.mysql.sql +28 -0
  59. package/templates/src/auth/api/migrations/0002__org_sync.pg.sql +27 -0
  60. package/templates/src/auth/api/migrations/0002__org_sync.sql +41 -0
  61. package/templates/src/auth/api/schema.yaml +31 -0
  62. package/templates/src/auth/api/wecom/admin/api.ts +3 -1
  63. package/templates/src/auth/api/wecom/api.ts +155 -35
  64. package/templates/src/auth/api/wecom/sync/api.ts +199 -0
  65. package/templates/src/auth/web/__client/api.schemas.ts +97 -0
  66. package/templates/src/auth/web/__client/api.ts +73 -1
  67. package/templates/src/login/web/locales/en-US.json +2 -0
  68. package/templates/src/login/web/locales/zh-CN.json +2 -0
  69. package/templates/src/login/web/pages/login.tsx +39 -3
  70. package/templates/src/notification/api/seed.sql +1 -1
  71. package/templates/src/system/api/departments/api.ts +142 -0
  72. package/templates/src/system/api/departments/detach/api.ts +34 -0
  73. package/templates/src/system/api/departments/members/api.ts +142 -0
  74. package/templates/src/system/api/departments/move/api.ts +39 -0
  75. package/templates/src/system/api/users/api.ts +45 -5
  76. package/templates/src/system/api/users/profile/api.ts +44 -0
  77. package/templates/src/system/web/__client/api.schemas.ts +143 -0
  78. package/templates/src/system/web/__client/api.ts +293 -1
  79. package/templates/src/system/web/entry.ts +6 -0
  80. package/templates/src/system/web/locales/en-US.json +112 -6
  81. package/templates/src/system/web/locales/zh-CN.json +112 -6
  82. package/templates/src/system/web/pages/departments/index.tsx +346 -0
  83. package/templates/src/system/web/pages/departments/sync-bar.tsx +257 -0
  84. package/templates/src/system/web/pages/departments/tree.ts +66 -0
  85. package/templates/src/system/web/pages/errors.ts +1 -1
  86. package/templates/src/system/web/pages/users/index.tsx +49 -7
  87. package/templates/src/system/web/pages/wecom/index.tsx +14 -0
  88. package/templates/tests/l1-builder.test.ts +1 -1
  89. package/templates/tests/org-departments.spec.ts +311 -0
  90. package/templates/tests/org-read.spec.ts +469 -0
  91. package/templates/tests/rbac-contacts.spec.ts +13 -3
  92. package/templates/tests/rbac-seed.spec.ts +11 -0
  93. package/templates/tests/seed-role-accounts.spec.ts +6 -2
  94. package/templates/tests/wecom-admin.spec.ts +1 -0
  95. package/templates/tests/wecom-connector.spec.ts +146 -10
  96. package/templates/tests/wecom-crypt.spec.ts +12 -3
  97. package/templates/tests/wecom-endpoints.spec.ts +247 -19
  98. package/templates/tests/wecom-org-sync.spec.ts +598 -0
  99. package/templates/tests/wecom-sync.spec.ts +286 -0
  100. package/vendor/host-versions.json +3 -3
  101. package/templates/src/auth/api/_shared/wecom/xml.ts +0 -36
package/devkit/README.md CHANGED
@@ -24,7 +24,7 @@
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 项诉求的闭环记录 |
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`**。
21
22
  2. **建目录**:一个目录 = 一个模块——前端一侧 `src/<模块名>/web/`(`entry.ts` 必备,
22
23
  `pages/`、`locales/` 按需),后端一侧 `src/<模块名>/api/`(目录镜像路由的 `api.ts`,
23
24
  动词挂 `.schema`)。两侧可只存在一侧。
@@ -39,7 +40,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
39
40
  以扫描 `src/*/web/entry.ts` 为真源,登记项排前面、其余按扫描顺序追加;历史布局则必须登记)。
40
41
  **要「不构建」写 `enabled: false`——注释掉无效。**
41
42
  5. **验证**:`pnpm dev`(前端 devServer **5174** + oj 后端 **9778**,端口以工程根 `config.yaml` 为准)
42
- → 登录 `admin / 123456` → 按 §8.1 五步清单核对。
43
+ → 登录 `admin / aA123456` → 按 §8.1 五步清单核对。
43
44
 
44
45
  ## 红线(不可违反)
45
46
 
@@ -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:`
@@ -109,6 +116,11 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
109
116
  同企业测试+正式两个应用是两行(主键含 `agent_id`),此时登录链接必须带 `&agent=`。
110
117
  - **整页跳转拼 API base**:`window.location.assign(resolveApiBase() + wecomAuthorizePath(corp))`;
111
118
  拼站点 base 会被 SPA 兜底页吞掉(页面原地重载、一个请求都不发,控制台无报错)。
119
+ - **通讯录同步是「组织架构底座 + 企微适配器」,不是企微专属功能**:模板的
120
+ `src/auth/api/wecom/{sync,contacts}.ts` 只负责「取快照 → 喂给领域层」,模型与读面在 `_platform`
121
+ (手册 §14)。要按部门算人、要选人、要给组织架构接 LDAP,一律读 §14,不要在企微代码里加分支。
122
+ 同步/事件增量与回调解密是**两条独立纪律**:回调必须回 `success`,而事件处理失败**也照样回 `success`**
123
+ (重推补不回已判死的失败,兜底是下一轮全量同步)。
112
124
 
113
125
  ## 新模块 checklist
114
126
 
@@ -172,14 +184,21 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
172
184
  | 限速/节流/计数真实请求下永不累积(测试全绿) | 模块级 `Map`/`let` 不跨请求存活(每请求重建模块) | 状态落库(模板迁移 0009 先例)或 oj 持久面 |
173
185
  | `ojm build` 报 `introspect api.ts: jwt not configured` | 模块级语句碰运行时全局,introspect 空沙箱炸 | 移进 handler 函数体内(manual §1.7-4) |
174
186
  | 页签怎么点都关不掉、无报错提示 | `beforeClose` 守卫 fail-safe 否决(守卫抛错按取消处理) | 先查控制台 `[tabs] beforeClose` 报错;确认弹窗逻辑与 `resolve(true/false)` 收口(manual §6.4) |
187
+ | 组织架构读面 403「该部门不在可见范围内」 | `scope` 默认 `self`,只看得到自己那条部门路径 | 换 `scope=all`(需权限点 `platform.org.read.all`),或改问自己所在部门(manual §14.2) |
188
+ | 组织架构读面 400「只展开自己所在部门」 | `includeSub` 用在了祖先/兄弟部门上(self 档只允许展开自己所在部门) | 收窄到「我所在部门」,或申请 `scope=all`(manual §14.2) |
189
+ | `oj build` 报 `#/_platform/...` 找不到模块 | 跨模块别名没在 `manifest.yaml` 的 `deps` 里声明 | 加 `_platform: ^0.1.0`(manual §14.2) |
190
+ | 同步一轮之后本地手建的部门消失了 | 适配器漏了 `origin` 条件(同步与回收都必须只动自己的行) | `markSeen` / `reconcileHidden` 带 `origin`;回收只在「本轮完整零跳过」时执行(manual §14.5) |
191
+ | 组织架构里有这个人,通讯录搜不到 | 他是同步建档(`activated=0`,从没登录过本站),通讯录只出活人 | 正确行为;让他登录一次,或走组织架构读面(manual §14.6) |
175
192
 
176
193
  ## 手册
177
194
 
178
- `manual.md`(同目录)共 13 章:1 工程结构(module-root 单树 / 两处产物根 / **§1.7 handler 四红线** /
195
+ `manual.md`(同目录)共 14 章:1 工程结构(module-root 单树 / 两处产物根 / **§1.7 handler 四红线** /
179
196
  **§1.8 `#` 别名与 TS 投影**——oj 运行时实测钉出,后端 handler 必读)/ 2 写前端模块 /
180
197
  3 契约与代码生成 / 4 接管框架内置功能(provider)/ 5 布局与插槽 / 6 i18n 与样式 / 7 构建与发布 /
181
198
  8 排查 / 9 参考 / 10 CLI 子命令参考(ojm 原生命令 vs oj 透传命令的差异区分)/
182
- 11 方案包打包与分发 / 12 签名与信任 / 13 方案菜单归组规则。
199
+ 11 方案包打包与分发 / 12 签名与信任 / 13 方案菜单归组规则 /
200
+ 14 组织架构与通讯录(共享读面 7 端点、`scope` 两级门禁、归属权与「转为本地维护」、
201
+ 源适配器接点与三条禁令——选人 / 按部门算人 / 接 LDAP 都读它)。
183
202
 
184
203
  另有 `wecom-daikaifa.md`(同目录):企业微信代开发集成的凭据清单与三角色手册——凭据从哪取、
185
204
  怎么填、按什么顺序开通、用户/开发/部署各自怎么做、错误文案怎么查。**接企微时先读它**。
@@ -21,7 +21,7 @@
21
21
  > ⚠️ 那两份 guide 有**已知陈旧处**,以源码为准:
22
22
  > dev 端口实际是 **3333**(guide 写的 5173 已过时;注意与 `ojm dev` 的前端 devServer
23
23
  > **5174** 不是一回事——前者是本仓 vite,后者是业务工程 devServer);包版本以
24
- > `packages/*/package.json` 为准(撰写时实际 **0.2.20**,仅供参考,以当前 package.json 为准)。
24
+ > `packages/*/package.json` 为准(撰写时实际 **0.2.47**,仅供参考,以当前 package.json 为准)。
25
25
 
26
26
  ## §2 两包结构与依赖方向
27
27
 
@@ -139,7 +139,7 @@ pnpm test tests/shell # 宿主全量
139
139
  **一键发布**:`node scripts/publish-packages.mjs`(lockstep 门禁 → 构建 → 守卫测试 →
140
140
  runtime → cli → registry 复核;`--dry-run` 只演练)。下面的手动步骤是它的展开参考。
141
141
 
142
- - **两包**同版本号(`@oj-module/runtime` + `@oj-module/cli`,当前 `0.2.20`),后续发版保持
142
+ - **两包**同版本号(`@oj-module/runtime` + `@oj-module/cli`,当前 `0.2.47`),后续发版保持
143
143
  lockstep;`@plane/kit` / `@plane/editor` 不在发布集内(脚本里已注释)。
144
144
  - **必须 `pnpm publish`**——用 `npm publish` 会漏出 `workspace:*` / `catalog:` 字面量。
145
145
  - **顺序:先 `runtime` 后 `cli`**。`runtime` 未公开时,新 `cli` 的精确依赖会悬空,
package/devkit/manual.md CHANGED
@@ -13,8 +13,9 @@
13
13
  ### 1.1 脚手架产物
14
14
 
15
15
  `ojm init <目录>` 生成(幂等补缺,重跑只补缺失文件,**永不覆盖已有文件**)。
16
- 模板拷贝按 `packages/cli/templates/init.include.yaml` **白名单**执行——未收录的
17
- 路径一律不拷(demo-only 演示模块、模板自运行产物都在清单外)。
16
+ 模板拷贝按框架内置的 **`init.include.yaml` 白名单**执行——未收录的路径一律不拷
17
+ (demo-only 演示模块、模板自运行产物都在清单外)。该清单是**框架侧文件**,`ojm init`
18
+ 从 cli 包内读取,**不在你的工程里**——你在工程里找不到它,也改不到它。
18
19
 
19
20
  布局是 **module-root:一个目录 = 一个模块**,前后端同处 `src/<name>/`,各占一侧
20
21
  (`api/` 与 `web/` 可以只存在一侧,两侧皆无则不是模块):
@@ -60,7 +61,7 @@
60
61
  - **`config.yaml` 在工程根**,不是 `api/config.yaml`。oj 把「项目根」锚定在 **config
61
62
  所在目录**,所有 `fs.*` 相对路径、`db.default`、`platform.root`、`mounts[].web`
62
63
  都相对它解析;证书与密钥落在同级的 `config/`(`ojm` 侧由 `resolveConfigPath()` /
63
- `resolveCertDir()` 单点派生,见 `packages/cli/src/layout.ts`)。
64
+ `resolveCertDir()` 单点派生,你不需要自己拼路径)。
64
65
  - **产物有两个根**:后端 `api/dist`(oj 扁平 release,`<name>-<ver>/` 与
65
66
  `manifests.yaml` 同层),站点 `web/dist`(`modules/` + `modules.json`)。
66
67
  `config.yaml` 上移到工程根后,两者不再同名 `dist`。
@@ -75,7 +76,7 @@
75
76
  > **历史布局仍被识别**:`web/src/<name>`(uni-dev)与 `web/<name>`(框架根仓)照旧可用,
76
77
  > 只是 `ojm init` 不再产出。两套源码树**互斥**——同时存在时 `ojm` 直接报错,而不是静默选一个。
77
78
 
78
- 登录账号:**账号名=内置角色名**一一对应,口令均 `123456`,且全部**免首登改密**(must_change=0,纯开发便利,勿用于生产):
79
+ 登录账号:**账号名=内置角色名**一一对应,口令均 `aA123456`,且全部**免首登改密**(must_change=0,纯开发便利,勿用于生产):
79
80
 
80
81
  | 账号 | 角色 | 能干什么 |
81
82
  |---|---|---|
@@ -86,6 +87,13 @@
86
87
 
87
88
  > 旧脚手架库(admin 绑 superadmin / must_change=1)**无需手工迁移**:seed 每次启动幂等重放,
88
89
  > 自动收敛为账号名=角色名并清零 must_change(`DELETE` 旧绑定 + `UPDATE`,见 `_platform/api/seed.sql`)。
90
+ >
91
+ > ⚠️ **但口令不会被 seed 改**:建号语句是 `INSERT OR IGNORE`,已存在的账号行原样保留。
92
+ > 口令在 2026-11-01 由 `123456` 改为 `aA123456`(bcrypt cost=10、`$2y$` 前缀),
93
+ > **更早建好的 dev 库里仍是旧口令**——拿 `aA123456` 登不上时先怀疑这个,别去查登录链路。
94
+ > 要统一:删 `db.sqlite` 重跑 `oj migrate`(seed 随服务启动重放),或直接
95
+ > `UPDATE users SET password_hash = '<aA123456 的哈希>' WHERE username IN ('superadmin','admin','editor','viewer');`。
96
+ > 哈希现成值从 `src/_platform/api/seed.sql` 里 grep `$2y$10$` 即可。
89
97
 
90
98
  端口:前端 devServer **5174**,oj 后端 **9778**
91
99
  (以工程根 `config.yaml` 的 `server.port` 为准)。
@@ -343,7 +351,7 @@ export default defineModule({
343
351
  `defineModule` 字段:`name`(= 目录名)、`description`、`version`、
344
352
  `peerRuntime`、`routes`、`i18n?`、`lifecycle?`。
345
353
 
346
- > 页面组件用**静态 import 或 `lazy()` 都行**:模板(`packages/cli/templates/src/*/web/entry.ts`)
354
+ > 页面组件用**静态 import 或 `lazy()` 都行**:模板自带模块(`src/home/web/entry.ts` 等)
347
355
  > 用静态 import,多页面模块可对非首屏页面用 `lazy(() => import("./pages/x"))` 做代码分割。
348
356
  > 单个页面就用静态 import,少一层间接。
349
357
 
@@ -398,10 +406,11 @@ export default defineModule({
398
406
  可服务的同源路径)。直接 `import "vditor/dist/..."` 其它深路径会命中前缀 external 但无
399
407
  importmap 键,构建期报错——需要新深路径请联系框架方在共享表登记。
400
408
 
401
- ### 2.7 本版本共享依赖钉版(0.2.20 快照)
409
+ ### 2.7 本版本共享依赖钉版(0.2.47 快照)
402
410
 
403
- 宿主经 importmap 提供以下单例(模块 `devDependencies` 须对齐,供 `tsc` 用);完整 68 项矩阵以
404
- `pnpm exec ojm info` 或 `packages/cli/shell-dist/versions.json`(构建期生成)为准。
411
+ 宿主经 importmap 提供以下单例(模块 `devDependencies` 须对齐,供 `tsc` 用);完整矩阵以
412
+ `pnpm exec ojm info` 为准——**别抄本节表格当真相**,本表是撰写时的快照,升级 cli 后
413
+ 以 `ojm info` 输出为准。
405
414
 
406
415
  | 包 | 钉版 |
407
416
  | --- | --- |
@@ -427,7 +436,8 @@ importmap 键,构建期报错——需要新深路径请联系框架方在共
427
436
  | `pinyin-pro` | 3.29.3 |
428
437
  | `vditor` | 4.0.0 |
429
438
 
430
- 框架包版本:`@oj-module/runtime` / `@oj-module/cli` 为 `0.2.20`(**双包 lockstep**);
439
+ 框架包版本:`@oj-module/runtime` / `@oj-module/cli` 为 `0.2.47`(**双包 lockstep**,
440
+ 以 `packages/*/package.json` 或 `ojm info` 为准——本表是撰写时快照);
431
441
  `@plane/kit` / `@plane/editor` 是独立版本线(当前 `0.1.20`,随宿主 importmap 提供,
432
442
  不参与 lockstep)。`oj` 二进制最低版本门禁 `MIN_OJ_VERSION = 0.1.85`——这个版本带来
433
443
  `--module-root`(`src/<name>/{api,web}` 布局的源码树根,与 `-d/--api-path` **互斥**);
@@ -988,7 +998,7 @@ export default {
988
998
  - **归属校验返回 403**:跨用户 `del` / `read` 他人按人项,原语返回 `false`,端点 `json.fail(403, "forbidden")`——
989
999
  与「按人隔离」对称(A 推给 B 后 A 不可见、B 不可删/读 A 的)。
990
1000
 
991
- **验证**:`pnpm dev` → `admin / 123456` 登录 → 右上角铃铛有蓝点 → 点开 →
1001
+ **验证**:`pnpm dev` → `admin / aA123456` 登录 → 右上角铃铛有蓝点 → 点开 →
992
1002
  点任意一条蓝点消失 → 「全部已读」全清 → 「清空」列表变空。
993
1003
 
994
1004
  **加接口的通用流水线**:
@@ -1569,11 +1579,11 @@ export default async (path: string, html: string) => {
1569
1579
 
1570
1580
  | 想抄什么 | 抄这里 |
1571
1581
  |---|---|
1572
- | 带页面的模块(路由 / i18n / order) | `src/home/web/entry.ts`、`src/demo/web/entry.ts`(模板 `packages/cli/templates/src/*/web/`) |
1582
+ | 带页面的模块(路由 / i18n / order) | `src/home/web/entry.ts`、`src/demo/web/entry.ts` |
1573
1583
  | 接管通知 provider(四方法全量) | `src/notification/web/entry.ts` |
1574
1584
  | 上传 provider(action + headers 方法) | `src/personal-center/web/entry.ts` |
1575
1585
  | 无页面模块(`routes: []`) | 同 `src/notification/web/entry.ts` |
1576
- | handler `.schema` 入参 + 平级 `.response` 出参写法(GET + POST、参数段) | `src/notification/api/api.ts`、模板 `packages/cli/templates/src/notification/api/api.ts` |
1586
+ | handler `.schema` 入参 + 平级 `.response` 出参写法(GET + POST、参数段) | `src/notification/api/api.ts` |
1577
1587
  | handler 写法(嵌套组列表 / 写入 / 403 归属) | `src/notification/api/api.ts`(模块根单文件,`read`/`read-all`/`clear` 子组) |
1578
1588
  | 统一异常出口 `respond()`(service 抛 `Error & {status}` → 信封 `json.fail`) | 模板 `src/_platform/api/_shared/respond.ts` + 各 handler 用法(§1.7-1) |
1579
1589
  | 生成物长什么样(含工厂) | `src/notification/web/__client/api.ts` |
@@ -1632,8 +1642,8 @@ export default async (path: string, html: string) => {
1632
1642
  产物见 §1.1(工程根 `config.yaml` + `config/` 证书 + `src/<模块>/{api,web}` 单树 + `bin/`、
1633
1643
  `web.config.ts`、两份 tsconfig 等)。`tsconfig.paths.json` 与 `package.json` /
1634
1644
  `pnpm-workspace.yaml` / `.gitignore` 由 init **现场生成**(不进白名单,模板目录里那份只服务模板自身)。
1635
- 模板拷贝按 `packages/cli/templates/init.include.yaml` 白名单——demo-only 演示模块
1636
- 与模板自运行产物不在清单内,不会进工程(§1.1)。
1645
+ 模板拷贝按框架内置 `init.include.yaml` 白名单——demo-only 演示模块
1646
+ 与模板自运行产物不在清单内,不会进工程(清单是框架侧文件,工程里看不到,见 §1.1)。
1637
1647
  `bin/oj` 缺失时自动经 npm 包 `@oj-bin/oj` 下载(与 `ojm vendor` 同款,受
1638
1648
  `MIN_OJ_VERSION` 门禁约束);下载失败即报错——可先 `ojm vendor`,或把本机 oj
1639
1649
  预置进 `bin/` 后重跑(`bin/oj` 已在则跳过下载,init 幂等)。
@@ -2116,6 +2126,202 @@ const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
2116
2126
 
2117
2127
  ---
2118
2128
 
2129
+ ## §14 组织架构与通讯录(直接用现成底座,别自己搭)
2130
+
2131
+ 站点内置一套组织架构底座:**部门 / 成员 / 归属权**由 `_platform` 内核负责,外部通讯录
2132
+ (企业微信,将来 LDAP)只是往这套底座灌数据的**适配器**。模块的活儿只有两种:
2133
+ **读**(谁在哪个部门)或**算**(某部门含下级一共有谁)。
2134
+
2135
+ 命中任一条就来本章,别另起一套:
2136
+
2137
+ | 需求 | 该用 | 别做 |
2138
+ |---|---|---|
2139
+ | 页面要选人 / 选部门 | 共享读面 `/api/_platform/org/*`(§14.2) | 自己查 `users` 全表、前端自己拼树 |
2140
+ | 通知 / 审批要「某部门及其下级的所有人」 | `POST /api/_platform/org/resolve`(或后端直调 `resolveUserIds`) | 前端拉全树、在 JS 里递归 |
2141
+ | 只是「找个人」 | 通讯录 `searchContacts`(§14.6) | 拿组织架构树当通讯录用 |
2142
+ | 要给组织架构接一个新来源(LDAP / 钉钉 / HR) | 领域层适配器接点(§14.5) | 另建一套表;或把外部字段写进 `sys_department` |
2143
+
2144
+ ### 14.1 数据在哪:两张租户表 + `users` 加三列
2145
+
2146
+ | 表 | 关键列 | 语义 |
2147
+ |---|---|---|
2148
+ | `sys_department` | `id` / `parent_id` / `name` / `name_en` / `order_no` | 结构:**`parent_id` 是本地 id,不是外部 id** |
2149
+ | | `origin` / `origin_id` | 归属权 + 外部主键。唯一约束是 `(tenant_id, origin, origin_id)` |
2150
+ | | `sync_visible` / `deleted` / `seen_job_id` | 同步态:可见性只由同步轮次维护;软删只由外部源的删除事件置;`seen_job_id` 是墓碑回收的正面证据 |
2151
+ | `sys_dept_member` | `user_id` / `dept_id` / `order_no` / `is_leader` / `is_main` | 多对多。**全是本地 id,与来源无关**——接任何适配器都不用动这张表 |
2152
+ | `users` | `activated` / `position` / `external_status` | 见下方第二段 |
2153
+
2154
+ 两条必须记住的语义:
2155
+
2156
+ 1. **`parent_id` 是本地 id**。本地手建的部门可以和同步来的部门混挂在一棵树上,外部重构层级
2157
+ 也不会打乱你本地的结构。
2158
+ 2. **`origin` 是归属权,不是「来源标签」**。`local` = 本站管理员全权 CRUD;
2159
+ `wecom` / `ldap` / `sso` = 对应适配器管辖。**任何同步与回收只作用于自己 `origin` 的行**——
2160
+ 这是「本地建的部门不会被第一轮同步抹掉」的根本机制,也是适配器可插拔与本地 CRUD 能共存的唯一原因。
2161
+
2162
+ `users` 那三列的用途别搞混:`activated` 是「**这个号被真人用过**」的证据(管理员建号 / 各来源登录
2163
+ 建档置 1,**同步建档默认 0**);`position` 是职务;`external_status` 是外部源的在职状态快照
2164
+ (**绝不自动改写 `is_forbidden`**,否则会和本站管理员的手动启停互相覆盖)。
2165
+
2166
+ ### 14.2 后端复用:共享读面(推荐直调领域函数)
2167
+
2168
+ 7 个端点,全在 `_platform` 模块下(前缀 `/api/_platform`):
2169
+
2170
+ | 端点 | 出什么 | 关键参数 |
2171
+ |---|---|---|
2172
+ | `GET /org/tree` | 部门**平表**(前端按 `parentId` 组装)+ 成员数 | `scope` |
2173
+ | `GET /org/departments` | 部门平铺分页(大企业别一次拉全树) | `scope`、`parent`(`0` = 只取根层)、`keyword`、`page`、`size`≤200 |
2174
+ | `GET /org/members` | 成员分页 + 部门名路径 | `scope`、`dept`、`includeSub`、`keyword`、`page`、`size` |
2175
+ | `GET /org/user` | 某人的部门路径 / 主部门 / 职务 | `scope`、`uid`(不给即自己) |
2176
+ | `GET /org/picker` | 人 + 部门混合(选人组件一次拿全) | `keyword`(**必填且 ≥2**)、`limit`≤50 |
2177
+ | `POST /org/resolve` | 部门(含子部门)→ `userIds`(去重升序) | body `deptIds` / `userIds` / `includeSub` |
2178
+ | `GET /org/is-member` | 某人是否在某部门(含子部门)下 | `dept` 必填、`uid`(不给即自己) |
2179
+
2180
+ **两级门禁**(默认最小集,要全量请申请权限点):
2181
+
2182
+ - `scope=self`(默认):只看「我在哪」——我所在部门 + 它的祖先链,以及这些部门里的直属成员。
2183
+ **不含兄弟部门**:组织架构含层级 + 人名,比通讯录敏感得多,且同步会把「从未登录本站、
2184
+ 无从同意」的全员灌进来。
2185
+ - `scope=all`:全量,需权限点 `platform.org.read.all`(`superadmin` / `admin` 天然通过)。
2186
+
2187
+ 三条硬限制(踩了只会觉得「接口坏了」):
2188
+
2189
+ - 限速 60 次/分/人(键 `org:<uid>`);批量场景用 `resolve` / 分页,别循环打单条。
2190
+ - `keyword` 一律 ≥2 字:读面是**枚举面**,单字能把整份名册拖走——与通讯录同一条红线。
2191
+ - `includeSub` 在 `self` 档**只对「我所在部门」生效**:对祖先部门请求展开会 **400**。
2192
+ 这是刻意的——祖先一展开就是整棵子树,等于绕过自身路径限制;宁可报错也不静默截断成
2193
+ 「看着对其实少人」的结果。
2194
+
2195
+ 字段口径:读面**一律不出** `origin` / `is_leader` / `external_status`,且**返回字段与 `scope` 无关**。
2196
+ 消费方不用按档位分支解析——归属与在职是管理语义,不归消费面。
2197
+
2198
+ **后端直调**(同进程内,不走 HTTP,省一次序列化与鉴权):
2199
+
2200
+ ```ts
2201
+ import { resolveReadFace, resolveUserIds } from "#/_platform/_shared/service/domain/org-read";
2202
+
2203
+ async function post() {
2204
+ await respond(async () => {
2205
+ // 登录态 → 租户自洽 → 限速 → scope 解析 → all 档验权限 → 算可见集,一次做完
2206
+ const reader = await resolveReadFace((http.query as Record<string, unknown>).scope);
2207
+ const userIds = await resolveUserIds(reader, { deptIds: [42], includeSub: true });
2208
+ return { userIds };
2209
+ });
2210
+ }
2211
+ ```
2212
+
2213
+ `resolveReadFace` 是那 7 个端点**共用的守卫入口**——自己写 handler 也用它,别重抄一遍守卫
2214
+ (重抄必然长歪;它内部的顺序是「先租户自洽再限速」,伪造租户头的人不该消耗别人的限速窗口)。
2215
+
2216
+ > ⚠️ 跨模块别名要在**自己模块** `manifest.yaml` 的 `deps` 里声明 `_platform: ^0.1.0`,
2217
+ > 否则 `oj build` 构建期直接报错(§1.8)。这是「跨模块依赖」的门禁,不是可选的注释。
2218
+
2219
+ ### 14.3 前端怎么用:给自己的模块出端点,别借别人的 client
2220
+
2221
+ 前端模块能 import 的只有三类(§2.6),**别人的 `__client` 与 `#/_platform` 都不在其中**。
2222
+ 所以前端消费组织架构的唯一正路是「借壳」:
2223
+
2224
+ 1. 在**自己模块**的 handler 里调 `resolveReadFace` + 需要的那几个读函数(§14.2);
2225
+ 2. 跑 `oj openapi` + `ojm api`,生成本模块的 `src/<模块>/web/__client/api.ts`;
2226
+ 3. 页面用本模块 client 调。
2227
+
2228
+ 多一层 handler 换来三件事:接口形状由**你自己的契约**决定(只出页面要的字段)、框架内部实现不进你的
2229
+ bundle、契约变更走你自己的版本号。仓库里 `src/system/web/pages/departments` 的组织架构页就是这条路的
2230
+ 另一个方向(它消费的是管理端 `/api/system/departments`,见 §14.4)。
2231
+
2232
+ ### 14.4 管理端 CRUD:只有「管组织架构」的模块才该碰
2233
+
2234
+ 管理端在 `system` 模块下,一律 `assertSystemAdmin` 门禁:
2235
+
2236
+ | 端点 | 用途 |
2237
+ |---|---|
2238
+ | `GET /api/system/departments` | 部门平表 + 成员数(**含**不可见与软删壳,管理员要看全量) |
2239
+ | `POST` / `PUT` / `DELETE /api/system/departments` | 增 / 改 / 删(本地部门才可删;有子部门时**拒绝**而不是级联删) |
2240
+ | `PUT /api/system/departments/move` | 改父节点 |
2241
+ | `POST /api/system/departments/detach?id=` | **「转为本地维护」**:`origin` 改为 `local`,此后同步不再改写这一行 |
2242
+ | `GET` / `POST` / `PUT` / `DELETE /api/system/departments/members?id=` | 部门成员的增删查改 |
2243
+
2244
+ 四条归属规则(写在领域层,绕不过去):
2245
+
2246
+ 1. **本地的行**(`origin='local'`):全权 CRUD。
2247
+ 2. **外部源的行**:结构字段(名称 / 层级 / 删除)**不可本地改**——改了下一轮同步就被覆盖,
2248
+ 所以直接拒。加人/移人不受限:「谁在这个部门」本站始终可调。
2249
+ 3. **要接管某一行**就用「转为本地维护」(`detach`);反向操作(转回外部源)**不提供**——
2250
+ 转回去后下一次同步立刻按源口径改写,语义不可控。
2251
+ 4. **删本地部门是软删壳还是真删**:本地删除走真删(有子部门则拒绝);`deleted=1` 只由外部源的
2252
+ 删除事件置,两者别混用——「看不见」和「没了」不是一回事。
2253
+
2254
+ ### 14.5 自己写一个源适配器(企微是现成范本)
2255
+
2256
+ 领域层**完全不认企微**:所有写路径都以「源」为参数。新适配器只需实现「取快照 → 喂给这几个函数」,
2257
+ 领域层与读面一行都不用改。
2258
+
2259
+ | 接点(`#/_platform/_shared/service/domain/...`) | 用途 | 纪律 |
2260
+ |---|---|---|
2261
+ | `org::upsertDepartment({ origin, originId, parentId, name, orderNo })` | 按 `(origin, origin_id)` 幂等写部门,返回本地 id | 只写结构字段;**不动** `sync_visible` / `deleted` / `origin` |
2262
+ | `org::upsertMember({ userId, deptId, ... })` | 按 `(user_id, dept_id)` 幂等写成员关联 | |
2263
+ | `org::markSeen(origin, deptIds, jobId, deps)` | 标记「本轮同步命中」,并把 `sync_visible` 拉回 1 | 必带 `origin`,否则会误伤本地行 |
2264
+ | `org::reconcileHidden(origin, jobId, deps)` | 墓碑回收:上一轮见过、这一轮没见 → 置不可见 | **只在调用方确认本轮完整(零跳过)后调用** |
2265
+ | `org::markDeleted(origin, originId, deps)` | 外部源侧的删除事件 → `deleted=1`,**级联整棵子树** | 这是 `deleted=1` 的唯一写入点 |
2266
+ | `users::upsertSourceUser({ username, displayName, source, corpId })` | 外部源建档,返回 uid | `activated=0`(目录档);已「转为本地维护」的行一个字段都不改 |
2267
+
2268
+ **一次全量同步的顺序**(照抄即可):
2269
+
2270
+ ```
2271
+ 抢锁(本地租约,可抢回崩溃残留)
2272
+ → 遍历部门:upsertDepartment → markSeen
2273
+ → 遍历成员:upsertSourceUser → applyMemberDetail(写姓名/职务/在职 + 部门关联)
2274
+ → 只有「本轮完整且零跳过」才 reconcileHidden
2275
+ → 放锁,记录统计
2276
+ ```
2277
+
2278
+ 三条禁令(每一条都对应一次真实事故形状):
2279
+
2280
+ 1. **墓碑回收前必须确认本轮完整**。上游抖一下就被读成「这些部门被删了」,表现是组织架构大面积消失。
2281
+ 权限类错误(接口无权限)要**计数并跳过**、限流类要退避重试,别混进「完整」的判据里。
2282
+ 2. **事件增量是加速器,不是权威**。事件处理**任何失败都不回非 `success`**——上游拿不到 `success`
2283
+ 会重推三次,而重推补不回一次已经判死的失败,只会让企业管理员在后台看到「应用安装失败」;
2284
+ 真正的兜底永远是下一轮全量同步。这条与「回调必须回 `success`」是一体两面。
2285
+ 3. **别复用别家的任务表**。企微那对 `wecom_sync_job` / `wecom_sync_lock` 带 `corp_id` / `agent_id` 列,
2286
+ LDAP 直接复用会被这两个字段绑死——**先另建 `ldap_sync_*` 把模型跑通**,两个源都成立再谈泛化
2287
+ (泛化要改已发布的迁移,代价远大于两张小表)。
2288
+
2289
+ **接 LDAP 前必须先回答的问题**:oj 的 `axis("ldap")` 有没有等价 `ldapsearch` 的遍历能力。
2290
+ 现状 `auth/.../ldap-login` 只有**认证**、没有目录同步;没有 search 就只能裸连或自带 client。
2291
+ 这是开工第一天的第一个问题,别边写边猜。
2292
+
2293
+ **验收**(这组断言能同时证明「适配器隔离」与「本地 CRUD 保底」):
2294
+ LDAP 侧改名 / 删人后,`origin='ldap'` 的行跟着变,而 `origin='wecom'` 的行与本地手建的行
2295
+ **一行不动**;本地手建的部门没有被同步覆盖。
2296
+
2297
+ ### 14.6 通讯录 ≠ 组织架构
2298
+
2299
+ | | 通讯录 `searchContacts` / `contactDetail` | 组织架构读面 `/org/*` |
2300
+ |---|---|---|
2301
+ | 形状 | 扁平(id / username / 显示名;详情才给 email / 头像) | 层级(部门树 + 成员归属 + 部门路径) |
2302
+ | 谁可见 | 登录即可,`keyword` ≥2、限速 | 登录 + `scope` 门禁(默认只看自己那条路径) |
2303
+ | **只出活人** | ✅ 只出 `activated=1` | 出全部可见成员(含同步灌进来的目录档) |
2304
+ | 典型用途 | 「找个人」「选人」 | 「算收件人」「按部门做访问控制」 |
2305
+
2306
+ 通讯录那条「只出 `activated=1`」不是优化而是**红线**:同步会把从未登录本站的全员建档,
2307
+ 若通讯录也出这批号,任何登录账号都能用 `keyword`(60 次/分)把整份企业名册遍历走。
2308
+ 所以「同步来的号不出现在通讯录里」是**正确行为**,不是 bug——要全员可见,让那个人先登录一次,
2309
+ 或者走组织架构读面(有 `scope` 门禁)。
2310
+
2311
+ ### 14.7 症状 → 病根 → 药方
2312
+
2313
+ | 症状 | 病根 | 药方 |
2314
+ |---|---|---|
2315
+ | 读面 403「该部门不在可见范围内」 | `scope` 是默认的 `self`,那部门不在你自己那条路径上 | 换成 `scope=all`(需 `platform.org.read.all`),或改问自己所在部门 |
2316
+ | 读面 400「只展开自己所在部门」 | `includeSub` 撞上了祖先部门或兄弟部门 | 只在「我所在部门」上展开;要全量就申请权限点 |
2317
+ | `oj build` 报 `#/_platform/...` 找不到模块 | `manifest.yaml` 的 `deps` 没声明 `_platform` | 加 `_platform: ^0.1.0` |
2318
+ | 同步后本地手建的部门「不见了」 | 适配器漏了 `origin` 条件(`markSeen` / `reconcileHidden` 都必带) | 带上 `origin`;回收只在完整轮次执行 |
2319
+ | 管理员改不动某个部门(400) | 该行 `origin` 不是 `local` | 先「转为本地维护」(`POST /api/system/departments/detach?id=`) |
2320
+ | 组织架构页有人、通讯录搜不到他 | 那人是同步建档(`activated=0`),从没登录过本站 | 正确行为,见 §14.6 |
2321
+ | 同步进度条一开就 429 | 限速按「单片 12 秒打满 → 天然 5 次/分」配平(给 10 次余量),自己并发打就超了 | start/cancel 串行驱动;status 不限速,可 1~2 秒轮询 |
2322
+
2323
+ ---
2324
+
2119
2325
  ## 附录:章节目录
2120
2326
 
2121
2327
  | 章 | 内容 | 什么时候读 |
@@ -2133,3 +2339,4 @@ const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
2133
2339
  | §11 方案包:打包与分发 | build --bundle、Form A/B、分发源、索引规范、引用式方案(api 段省略 tables/deps,引用包内原生 manifest) | 发布方案包 |
2134
2340
  | §12 签名与信任 | keygen/sign/install 发布流程、信任库、TOFU、bootstrap key | 首次签名发布;管理信任 |
2135
2341
  | §13 方案菜单归组规则 | 单根模块约定(根路由=第一级菜单)、自定义根路径 `web.route_prefix`(构建写 modules.json、运行时只改根段、getModuleRoutePrefix 单点消费)、category 三值(普通/hidden/admin)、admin 真重写 `/system/<模块 root>/*` + 视觉归组、menu.main 统一、模块内路径助手、开发者 DO/DON'T | 写方案要让菜单按模块分组、admin 归系统设置 |
2342
+ | §14 组织架构与通讯录 | 内置底座(两张租户表 + `users` 三列)、共享读面 7 端点与两级 `scope` 门禁、前端怎么「借壳」出自己端点、归属权四条规则与管理端 CRUD、源适配器接点与三条禁令(企微为范本,LDAP 前置)、通讯录与组织架构的分工 | 页面要选人/选部门、通知要按部门算收件人、要接新的组织数据来源 |
@@ -76,7 +76,7 @@ ojm dev
76
76
 
77
77
  # 1) 【免 setup】脚手架 seed 的 framework.market_cdn_url 缺省即 /cdn(本站点自供,
78
78
  # dev 形态 ojm 已把 /cdn 反代给 oj 子进程)。只有指向外部真 CDN 时才需要:
79
- # ojm market setup --user superadmin --pass 123456 --new-pass <你的口令>
79
+ # ojm market setup --user superadmin --pass aA123456 --new-pass <你的口令>
80
80
 
81
81
  # 2) 发包:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级签名
82
82
  # --rebuild-index 顺带重建索引
@@ -165,7 +165,7 @@ node "$CLI_PKG/bin/ojm.mjs" market verify --rebuild-index
165
165
  node "$CLI_PKG/bin/ojm.mjs" build # 生成 api/dist + web/dist(含 api/dist/versions.json)
166
166
  # config:platform.root 改成 api/dist(安装目标恒为 release 部署,root 必须与 --api-path 同源)
167
167
  (bin/oj serve -c config.yaml -b /api --api-path api/dist --console-log &)
168
- node "$CLI_PKG/bin/ojm.mjs" market setup --site-url http://127.0.0.1:9778 --user superadmin --pass 123456 --new-pass <新口令>
168
+ node "$CLI_PKG/bin/ojm.mjs" market setup --site-url http://127.0.0.1:9778 --user superadmin --pass aA123456 --new-pass <新口令>
169
169
  # 市场页点安装 → 首次停在「待信任」→ 核对指纹 → ojm trust confirm --task <id> → 重试 → done
170
170
  ```
171
171