@oj-module/cli 0.2.66 → 0.2.68

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 (200) hide show
  1. package/bin/.oj-version +1 -0
  2. package/bin/oj +4 -0
  3. package/devkit/README.md +15 -7
  4. package/devkit/SKILL.md +38 -4
  5. package/devkit/framework-dev.md +10 -4
  6. package/devkit/manual.md +146 -24
  7. package/devkit/menu-rules.md +19 -4
  8. package/devkit/module-dev-and-cdn-playground.md +4 -0
  9. package/devkit/publish-walkthrough.md +132 -37
  10. package/devkit/wecom-internal-app.md +8 -3
  11. package/devkit/wecom-multi-corp-links.md +163 -0
  12. package/package.json +2 -2
  13. package/shell-dist/assets/{index.i0kqbxp050.js → index.6alqyx6ae0.js} +21 -1
  14. package/shell-dist/assets/{runtime.1xfsxcajj0.js → runtime.1xzahckt10.js} +4863 -4847
  15. package/shell-dist/index.html +4 -4
  16. package/shell-dist/versions.json +3 -3
  17. package/src/__tests__/cli.test.ts +31 -0
  18. package/src/__tests__/new-scaffold.test.ts +13 -0
  19. package/src/__tests__/publish-keygen.test.ts +157 -0
  20. package/src/__tests__/templates-migration-schema-parity.test.ts +540 -0
  21. package/src/__tests__/templates-schema-columns.test.ts +15 -2
  22. package/src/__tests__/templates-tenant-scope.test.ts +6 -2
  23. package/src/build/module-def.ts +1 -0
  24. package/src/builtin.ts +3 -2
  25. package/src/bundle.ts +8 -3
  26. package/src/cli.ts +21 -5
  27. package/src/dev.ts +15 -10
  28. package/src/init.ts +47 -41
  29. package/src/install.ts +11 -4
  30. package/src/keygen.ts +54 -0
  31. package/src/lifecycle/__tests__/bundle.test.ts +21 -3
  32. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +1 -1
  33. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +2 -2
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +5 -2
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +1 -1
  36. package/src/lifecycle/__tests__/manifest-contract.test.ts +75 -3
  37. package/src/lifecycle/__tests__/smoke.test.ts +2 -2
  38. package/src/manifest-contract.ts +91 -8
  39. package/src/market/bootstrap.ts +26 -2
  40. package/src/market/command.ts +1 -0
  41. package/src/market/publish.ts +8 -1
  42. package/src/market/walkthrough.ts +24 -8
  43. package/src/new.ts +2 -0
  44. package/src/scaffold-guard.ts +227 -0
  45. package/src/site.ts +2 -0
  46. package/src/usage.ts +13 -9
  47. package/src/vendor.ts +5 -1
  48. package/templates/config.yaml +4 -2
  49. package/templates/init.include.yaml +10 -3
  50. package/templates/openapi.json +460 -1
  51. package/templates/src/README.md +37 -0
  52. package/templates/src/app/api/_platform/_lifecycle/compensate.ts +136 -0
  53. package/templates/src/app/api/_platform/_lifecycle/installer.ts +78 -25
  54. package/templates/src/app/api/_platform/_lifecycle/lock.ts +23 -0
  55. package/templates/src/app/api/_platform/_lifecycle/steps/deploy-web.ts +28 -0
  56. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +46 -0
  57. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +100 -36
  58. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +7 -15
  59. package/templates/src/app/api/_platform/_lifecycle/text-edit.ts +71 -19
  60. package/templates/src/app/api/_platform/_lifecycle/types.ts +26 -2
  61. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +93 -14
  62. package/templates/src/app/api/_platform/_shared/service/domain/settings.ts +8 -4
  63. package/templates/src/app/api/_platform/_shared/service/domain/solution-perms.ts +25 -10
  64. package/templates/src/app/api/_platform/manifest.yaml +1 -0
  65. package/templates/src/app/api/_platform/market/api.ts +235 -10
  66. package/templates/src/app/api/_platform/migrations/0001__baseline.mysql.sql +178 -0
  67. package/templates/src/app/api/_platform/migrations/0001__baseline.pg.sql +173 -0
  68. package/templates/src/app/api/_platform/migrations/0001__baseline.sql +188 -0
  69. package/templates/src/app/api/_platform/migrations/0002__market_release.mysql.sql +17 -0
  70. package/templates/src/app/api/_platform/migrations/0002__market_release.pg.sql +16 -0
  71. package/templates/src/app/api/_platform/migrations/0002__market_release.sql +33 -0
  72. package/templates/src/app/api/_platform/schema.yaml +18 -0
  73. package/templates/src/app/api/_platform/seed.mysql.sql +10 -0
  74. package/templates/src/app/api/_platform/seed.pg.sql +10 -0
  75. package/templates/src/app/api/auth/_shared/wecom/client.ts +24 -4
  76. package/templates/src/app/api/home/migrations/0001__create_home_tables.mysql.sql +14 -0
  77. package/templates/src/app/api/home/migrations/0001__create_home_tables.pg.sql +14 -0
  78. package/templates/src/app/api/home/migrations/0002__tenant.pg.sql +4 -0
  79. package/templates/src/app/api/home/seed.mysql.sql +71 -0
  80. package/templates/src/app/api/home/seed.pg.sql +71 -0
  81. package/templates/src/app/api/notification/_shared/notify.ts +6 -6
  82. package/templates/src/app/api/notification/migrations/0001__create_notifications.mysql.sql +11 -0
  83. package/templates/src/app/api/notification/migrations/0001__create_notifications.pg.sql +11 -0
  84. package/templates/src/app/api/notification/migrations/0002__notifications_user.mysql.sql +8 -0
  85. package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.mysql.sql +21 -0
  86. package/templates/src/app/api/notification/migrations/0003__notifications_broadcast.pg.sql +22 -0
  87. package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.mysql.sql +10 -0
  88. package/templates/src/app/api/notification/migrations/0004__notifications_broadcast_hidden.pg.sql +11 -0
  89. package/templates/src/app/api/notification/migrations/0005__tenant.pg.sql +6 -0
  90. package/templates/src/app/api/notification/seed.mysql.sql +8 -0
  91. package/templates/src/app/api/notification/seed.pg.sql +11 -0
  92. package/templates/src/app/api/publish/_shared/identity.ts +69 -0
  93. package/templates/src/app/api/publish/api.ts +349 -0
  94. package/templates/src/app/api/publish/manifest.yaml +8 -0
  95. package/templates/src/app/api/publish/migrations/0001__publish_credential.mysql.sql +23 -0
  96. package/templates/src/app/api/publish/migrations/0001__publish_credential.pg.sql +23 -0
  97. package/templates/src/app/api/publish/migrations/0001__publish_credential.sql +39 -0
  98. package/templates/src/app/api/publish/schema.yaml +33 -0
  99. package/templates/src/app/api/system/login-methods/api.ts +2 -1
  100. package/templates/src/app/web/_platform/__client/api.schemas.ts +15 -0
  101. package/templates/src/app/web/_platform/__client/index.ts +25 -1
  102. package/templates/src/app/web/entry.ts +9 -5
  103. package/templates/src/app/web/home/routes.ts +5 -0
  104. package/templates/src/app/web/login/locales/en-US.json +7 -1
  105. package/templates/src/app/web/login/locales/zh-CN.json +7 -1
  106. package/templates/src/app/web/login/pages/login.tsx +79 -86
  107. package/templates/src/app/web/login/routes.ts +54 -1
  108. package/templates/src/app/web/personal-center/locales/en-US.json +1 -0
  109. package/templates/src/app/web/personal-center/locales/zh-CN.json +1 -0
  110. package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +5 -0
  111. package/templates/src/app/web/system/__client/api.schemas.ts +2 -1
  112. package/templates/src/app/web/system/locales/en-US.json +1 -8
  113. package/templates/src/app/web/system/locales/zh-CN.json +1 -8
  114. package/templates/src/app/web/system/pages/settings/index.tsx +17 -4
  115. package/templates/src/app/web/system/routes.ts +0 -6
  116. package/templates/src/oj-market/web/__tests__/CardGrid.test.tsx +33 -0
  117. package/templates/src/oj-market/web/__tests__/DetailDrawer.test.tsx +69 -0
  118. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +7 -1
  119. package/templates/src/oj-market/web/api/client.ts +3 -0
  120. package/templates/src/oj-market/web/api/defaultClient.ts +5 -0
  121. package/templates/src/oj-market/web/components/CardItem.tsx +44 -1
  122. package/templates/src/oj-market/web/components/DetailDrawer.tsx +17 -1
  123. package/templates/src/oj-market/web/entry.ts +31 -2
  124. package/templates/src/oj-market/web/locales/en-US.json +5 -1
  125. package/templates/src/oj-market/web/locales/zh-CN.json +5 -1
  126. package/templates/src/oj-market/web/publish/__client/api.schemas.ts +61 -0
  127. package/templates/src/oj-market/web/publish/__client/index.ts +164 -0
  128. package/templates/src/oj-market/web/publish/locales/en-US.json +80 -0
  129. package/templates/src/oj-market/web/publish/locales/zh-CN.json +80 -0
  130. package/templates/src/oj-market/web/publish/pages/history.tsx +112 -0
  131. package/templates/src/oj-market/web/publish/pages/index.tsx +196 -0
  132. package/templates/src/oj-market/web/publish/pages/model.ts +52 -0
  133. package/templates/src/oj-market/web/publish/pages/token-modal.tsx +82 -0
  134. package/templates/src/oj-market/web/publish/routes.ts +41 -0
  135. package/templates/src/oj-market/web/types.ts +30 -0
  136. package/templates/tests/lifecycle/compensate.spec.ts +207 -0
  137. package/templates/tests/lifecycle/deploy-smoke.spec.ts +38 -14
  138. package/templates/tests/lifecycle/installer.spec.ts +28 -9
  139. package/templates/tests/lifecycle/market-api.spec.ts +164 -12
  140. package/templates/tests/lifecycle/preflight.spec.ts +68 -0
  141. package/templates/tests/lifecycle/register.spec.ts +22 -0
  142. package/templates/tests/lifecycle/role-title-translate.spec.ts +4 -34
  143. package/templates/tests/lifecycle/text-edit.spec.ts +45 -27
  144. package/templates/tests/lifecycle/uninstall.spec.ts +36 -0
  145. package/templates/tests/publish-credential.spec.ts +354 -0
  146. package/templates/tests/publish-identity.spec.ts +44 -0
  147. package/templates/tests/rbac-solution-perms.spec.ts +48 -3
  148. package/templates/tests/wecom-client-timeout.spec.ts +33 -3
  149. package/templates/tests/wecom-landing.spec.ts +63 -1
  150. package/templates/tsconfig.paths.json +1 -0
  151. package/templates/web.config.ts +8 -4
  152. package/vendor/host-versions.json +3 -3
  153. package/templates/seed.mysql.sql +0 -6
  154. package/templates/seed.pg.sql +0 -6
  155. package/templates/src/app/api/_platform/migrations/0001__create_users.mysql.sql +0 -9
  156. package/templates/src/app/api/_platform/migrations/0001__create_users.pg.sql +0 -9
  157. package/templates/src/app/api/_platform/migrations/0001__create_users.sql +0 -7
  158. package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.mysql.sql +0 -3
  159. package/templates/src/app/api/_platform/migrations/0002__add_users_avatar_base64.sql +0 -2
  160. package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.mysql.sql +0 -5
  161. package/templates/src/app/api/_platform/migrations/0003__users_source_display_name.sql +0 -5
  162. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.mysql.sql +0 -70
  163. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.pg.sql +0 -67
  164. package/templates/src/app/api/_platform/migrations/0004__sys_rbac.sql +0 -67
  165. package/templates/src/app/api/_platform/migrations/0005__users_must_change.mysql.sql +0 -3
  166. package/templates/src/app/api/_platform/migrations/0005__users_must_change.sql +0 -2
  167. package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.mysql.sql +0 -3
  168. package/templates/src/app/api/_platform/migrations/0006__users_is_forbidden.sql +0 -3
  169. package/templates/src/app/api/_platform/migrations/0007__users_email.sql +0 -3
  170. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.mysql.sql +0 -10
  171. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.pg.sql +0 -10
  172. package/templates/src/app/api/_platform/migrations/0008__sys_refresh_token.sql +0 -11
  173. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.mysql.sql +0 -12
  174. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.pg.sql +0 -12
  175. package/templates/src/app/api/_platform/migrations/0009__sys_throttle.sql +0 -13
  176. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.mysql.sql +0 -17
  177. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.pg.sql +0 -16
  178. package/templates/src/app/api/_platform/migrations/0010__sys_solution_route.sql +0 -18
  179. package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.mysql.sql +0 -2
  180. package/templates/src/app/api/_platform/migrations/0011__sys_setting_group_label.sql +0 -4
  181. package/templates/src/app/api/_platform/migrations/0012__market_history.mysql.sql +0 -13
  182. package/templates/src/app/api/_platform/migrations/0012__market_history.pg.sql +0 -12
  183. package/templates/src/app/api/_platform/migrations/0012__market_history.sql +0 -14
  184. package/templates/src/app/api/_platform/migrations/0013__users_profile.sql +0 -4
  185. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.mysql.sql +0 -9
  186. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.pg.sql +0 -7
  187. package/templates/src/app/api/_platform/migrations/0014__reg_source_seq.sql +0 -8
  188. package/templates/src/app/api/_platform/migrations/0015__tenant.mysql.sql +0 -12
  189. package/templates/src/app/api/_platform/migrations/0015__tenant.pg.sql +0 -11
  190. package/templates/src/app/api/_platform/migrations/0015__tenant.sql +0 -44
  191. package/templates/src/app/api/_platform/migrations/0016__org.mysql.sql +0 -41
  192. package/templates/src/app/api/_platform/migrations/0016__org.pg.sql +0 -40
  193. package/templates/src/app/api/_platform/migrations/0016__org.sql +0 -62
  194. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +0 -7
  195. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +0 -4
  196. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +0 -8
  197. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +0 -3
  198. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +0 -3
  199. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +0 -8
  200. package/templates/src/app/web/system/pages/login-methods/index.tsx +0 -30
@@ -0,0 +1 @@
1
+ v0.1.123
package/bin/oj ADDED
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: package/bin/oj
3
+ size: 126034368 bytes
4
+ sha256: 6774d629abecdd96a84bed71ed220ce85ae46bbbe9a818819eca87a5b5d50be6
package/devkit/README.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # oj-module DevKit——模块开发手册 + agent skill
2
2
 
3
+ > **你在哪读这份文件?** 两种视图,内容同一份:
4
+ > - **业务工程里**(`.claude/skills/ojm-module-dev/`):文件是随 `@oj-module/cli` 分发的**副本**,
5
+ > 直接读 `SKILL.md` / `manual.md` / 两本企微手册 / `publish-walkthrough.md` 即可闭环;
6
+ > 下文出现的 `docs/devkit/`、`packages/cli/` 是**框架仓库路径**,只用来解释文件怎么来的,
7
+ > 你本地没有也不影响使用。
8
+ > - **框架仓库里**(`docs/devkit/`):这里才是源文件,改文档只改这一处。
9
+
3
10
  面向用 `ojm` 开发前后端一体工程的开发者与 AI agent。本目录是**源文件**,
4
11
  发布时由 cli 的 `prepack`(`packages/cli/scripts/sync-devkit.mjs`)把其中
5
- **ojm 侧的 10 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
12
+ **ojm 侧的 11 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
6
13
  发布,再由 `ojm init` 分发到工程内。
7
14
 
8
15
  整棵树覆盖前后端两侧,但仍是**两套独立分发的 skill**(同名不同物,别混):
9
16
 
10
17
  | 侧 | 本仓位置 | 用户工程里的落点 | 分发链 |
11
18
  |---|---|---|---|
12
- | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 10 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包(全部 10 个)→ `ojm init` 第 4b 步(只分发其中 6 个,见下) |
19
+ | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 11 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包(全部 11 个)→ `ojm init` 第 4b 步(只分发其中 6 个,见下) |
13
20
  | oj API 开发(后端 handler、全局对象、配置、测试) | `oj-api-dev/` 子目录(快照) | `.claude/skills/oj-api-dev/` | **oj 二进制自带**(`bin/devkit/`)→ `ojm init` 第 4 步 / `ojm vendor` 装·升后同步(0.2.57 起共用 `syncOjDevkit`,全量 `.md` **覆盖**——文档始终与已装 oj 版本一致) |
14
21
 
15
22
  > **`oj-api-dev/` 子目录是快照,不是本仓维护的文档**:它与本机 oj 的
@@ -25,19 +32,20 @@
25
32
  |---|---|
26
33
  | `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
27
34
  | `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
- | `publish-walkthrough.md` | 方案包发布全流程(**新手照做版**):构建打包 → keygen 签名 → `market publish` 落 CDN → build-index → verify/trial 验证 → install + TOFU,含多人各自独立发包纪律与失败速查(手册 §11/§12 的过程版) |
35
+ | `publish-walkthrough.md` | 方案包发布全流程(**新手照做版**):构建打包 → **拿发布身份(「发布管理」申请凭证 → `ojm keygen --credential`;无站点才用离线 `--id`)** → 签名 → `market publish` 落 CDN → build-index → verify/trial 验证 → install + TOFU,含多人各自独立发包纪律与失败速查(手册 §11/§12 的过程版) |
29
36
  | `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
30
37
  | `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路五步跑通(setup → publish → build-index → verify → 安装) |
31
38
  | `sql-builder-missing-capabilities-pr.md` | 构造器能力缺口 PR 参考(**仅框架仓库**):7 项诉求的闭环记录 |
32
39
  | `menu-rules.md` | 方案菜单归组规则专题:单根模块约定、自定义根路径 `web.route_prefix`、`category` 三值语义、admin 重写与视觉归组(手册 §13 的展开版) |
33
40
  | `wecom-daikaifa.md` | 企业微信代开发集成:**凭据清单**(8 项 `vars`,按 Tier A/B/C 分层的来源与必填时序 + 可信域名/可信 IP 备案)、首次开通顺序(含 `suite_ticket` 依赖与引导期死锁的破法)、**用户 / 开发 / 部署三角色手册**、错误文案速查、与自建应用共存的语义变化 |
34
41
  | `wecom-internal-app.md` | 企业微信**企业内部应用(自建应用)**集成:5 项 `wecom_corp_*` 凭据 + 2 项复用、回调只填「通讯录同步」一处、「验证并接入」四步自检、可见范围与可信 IP/域名、单企业私有化的边界(自建应用收不到成员变更事件)、共存配置、故障速查 |
42
+ | `wecom-multi-corp-links.md` | **平台运维 / 服务商运营**:一套部署服务 N 家企业的**专属登录链接运维清单**(方案一,运维零代码改动)——前置检查、逐企业接入 6 步、链接格式与参数、员工四入口、逐企业验收打勾、故障速查、日常运维;含企微工作台内早跳透传 `corp`/`agent` 的修复说明 |
35
43
 
36
- > 上表 9 项 + 本文件 `README.md` = 顶层 10 个文件,全部随 cli 包发货。
44
+ > 上表 10 项 + 本文件 `README.md` = 顶层 11 个文件,全部随 cli 包发货。
37
45
  > `ojm init` 第 4b 步**只分发其中 6 个**给业务工程:`SKILL.md` / `manual.md` / `README.md` /
38
46
  > `wecom-daikaifa.md` / `wecom-internal-app.md` / `publish-walkthrough.md`。
39
- > 其余 4 个(`framework-dev.md`、`menu-rules.md`、`module-dev-and-cdn-playground.md`、
40
- > `sql-builder-missing-capabilities-pr.md`)是框架贡献者或专题材料,只在 cli 包内可取。
47
+ > 其余 5 个(`framework-dev.md`、`menu-rules.md`、`module-dev-and-cdn-playground.md`、
48
+ > `sql-builder-missing-capabilities-pr.md`、`wecom-multi-corp-links.md`)是框架贡献者或专题材料,只在 cli 包内可取。
41
49
 
42
50
  后端 handler 本身的写法(`json` 信封、`db` 查询、`manifest.yaml`、`oj test`)、
43
51
  站点挂载(`mounts`)、密钥信封(`secrets`)、vars、cors、schema 校验、broker/tasks/oidc/ws 等
@@ -121,6 +129,6 @@ flowchart LR
121
129
  框架贡献者要把本仓快照同步到新版:从 `~/.local/share/oj/devkit/` 整目录覆盖
122
130
  `docs/devkit/oj-api-dev/`(oj v0.1.116 起 12 个文件,保持逐字节一致以便对账)。
123
131
 
124
- > 本目录顶层 10 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
132
+ > 本目录顶层 11 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
125
133
  > 其它文档。`oj-api-dev/` 子目录是 oj 侧快照,同样自包含(其自身 README 即索引),
126
134
  > 但不随 cli 包发货、仅本仓参考。维护者视角的内容来源整理在 `framework-dev.md`(仅本仓)。
package/devkit/SKILL.md CHANGED
@@ -19,7 +19,8 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
19
19
  接管通知/上传/菜单 → §4;换布局或加插槽 → §5;排查 → §8;
20
20
  **组织架构 / 通讯录(选人、按部门算收件人、接 LDAP 等新来源)→ §14**;
21
21
  **接通企业微信(代开发 / 企业内部应用 / 两套共存)→ `wecom-daikaifa.md` + `wecom-internal-app.md`**;
22
- **把方案包发出去(打包/签名/CDN/安装一条链,新手照敲)→ `publish-walkthrough.md`**。
22
+ **把方案包发出去(打包/签名/CDN/安装一条链,新手照敲)→ `publish-walkthrough.md`**
23
+ (发布身份走站点凭证见其 §2.1)。
23
24
  2. **建目录**:一个目录 = 一个模块——前端一侧 `src/<模块名>/web/`(`entry.ts` 必备,
24
25
  `pages/`、`locales/` 按需),后端一侧 `src/<模块名>/api/`(目录镜像路由的 `api.ts`,
25
26
  动词挂 `.schema`)。两侧可只存在一侧。
@@ -50,7 +51,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
50
51
  历史布局漏登记 = 模块安静不加载;module-root 下登记是可选的,但**排序仍靠它**。
51
52
  - **`home` / `login` 模块必须保留**:shell 预构建把 `/` 重定向到 `/home`,且宿主只消费
52
53
  模块路由、不挂内置登录页。删掉它们 → 登录回跳落错误边界、`/login` 无路由可跳。
53
- - **布局级路由 handle 必写 `layout: "container"`**:不写页面会裸渲染(无侧边栏/页签),
54
+ - **布局级路由 handle 必写 `layout: "container"`**(`category: "admin"` 除外,未声明时框架默认 `container`):不写页面会裸渲染(无侧边栏/页签),
54
55
  且 `keepAlive` 失效。`"container" | "parent" | "none" | 自定义名`。
55
56
  - **注册时机**:一切 `ctx.register.*` 必须在 `lifecycle.onInit` 内、且在 onInit 返回前完成。
56
57
  拖到 `onActivate` 或某个异步回调里注册不保证生效。`onInit` 签名是 **async**(返回
@@ -140,14 +141,42 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
140
141
  同步/事件增量与回调解密是**两条独立纪律**:回调必须回 `success`,而事件处理失败**也照样回 `success`**
141
142
  (重推补不回已判死的失败,兜底是下一轮全量同步)。
142
143
 
144
+ ## 发布身份(keygen 从哪来)
145
+
146
+ 签名前的第一件事是拿到一对 Ed25519 密钥。**两条路,二选一**:
147
+
148
+ | 路径 | 命令 | 何时用 |
149
+ |---|---|---|
150
+ | 站点凭证(推荐) | `ojm keygen --credential <token>` | 站点有「方案市场 → 发布管理」页;key_id 由站点按产品名+账号下发(信任仍需首装核对) |
151
+ | 离线自建(旁路) | `ojm keygen --id <publisher>.<key-name>` | 只在站点没有「发布管理」页(纯自建/离线 CDN)或 CI 临时密钥时用 |
152
+
153
+ 站点有「方案市场 → 发布管理」页时**不要自建 key_id**:站点侧没有这条身份记录,产品归属与申请人无从追溯,
154
+ 能不能装只能靠超管为陌生指纹手工放行。
155
+
156
+ - 凭证是**一次性兑换令牌**:站点「方案市场 → 发布管理 → 申请发布凭证」填产品名 → 明文只显示一次 →
157
+ 本机 `ojm keygen --credential <token>`(或 `OJM_PUBLISH_CREDENTIAL=<token> ojm keygen`,
158
+ 免得令牌进 shell 历史)。兑换成功即作废,失败(已用/过期/撤销/网络不通)**不落任何密钥文件**。
159
+ - 站点地址优先级:`--site-url` > `OJM_SITE_URL` > `http://127.0.0.1:9778`;兑换端点匿名可达,
160
+ **不需要 `--user/--pass`**。`--credential` 与 `--id` 互斥(同时给 exit 2)。
161
+ - 下发的 key_id 形如 `<产品名>.<编码后用户名>`(例:`demo-solution.wecom-everpan`),CLI 不做推导;
162
+ 它同时是 `~/.ojm/keys/<key_id>/` 的目录名——**不要拿它拼路径或改名**,否则签名侧找不到私钥。
163
+ 凭证只解决「身份命名与归属」,**不预置信任**:公钥在本地生成,站点兑换时拿不到,首装照样
164
+ `needs_trust`,仍要核对指纹。
165
+ - 两条路的私钥纪律完全一致:只活在 `~/.ojm/keys/<id>/`、不入仓不入包、一次发布固定一把。
166
+ 签名/发布/安装全流程见 `publish-walkthrough.md`(§2 凭证、§2.1 离线旁路、§3 签名起)。
167
+
143
168
  ## 新模块 checklist
144
169
 
145
170
  - [ ] `src/<模块>/web/entry.ts` 默认导出 `defineModule({ name, version, routes, i18n?, lifecycle? })`
146
171
  - [ ] `web.config.ts` 的 `modules` 里已登记(`enabled` 缺省即启用;module-root 下扫描已能捡到,登记用于排序/覆盖)
147
172
  - [ ] 路由 `handle.layout` 选对了(有页面用 `"container"`;无页面模块 `routes: []`)
148
173
  - [ ] 菜单标题用命名空间语法 `"<模块名>:menu.<key>"`,且 `locales/<lang>.json` 里有这个 key
149
- - [ ] manifest 声明了 `roles[]` → 每个 `title_key` 用 `"<模块名>:role.<slug>"` 形状(ns 必须 = 模块名),
150
- 且双语 locale 里都有对应嵌套 key `{"role":{…}}`——安装时解析成 `sys_role.title`,缺失则存裸 key(不失败)
174
+ - [ ] 角色/权限声明写在方案契约 `solution.yaml`,**不是** `api/manifest.yaml`(oj 原生七键白名单,
175
+ 摆错位置安装直接拒「白名单外键」);细则见 manual.md §11.5
176
+ - [ ] 声明了 `roles[]` → 每个 `title_key` 用 `"<模块名>:role.<slug>"` 形状(ns 必须 = 模块名);
177
+ 声明了 `permissions[]` → 每个 `title_key` 用 `"<模块名>:perm.<slug>"`(必填)。
178
+ 双语 locale 里都要有对应嵌套 key(`{"role":{…}}` / `{"perm":{…}}`)——安装时按站点语言
179
+ 解析落库(`sys_role.title` / `sys_permission.title_key`),缺失则存裸 key(不失败)
151
180
  - [ ] 有接口 → handler 动词挂了 `.schema`(入参)+ 平级 `.response`(出参,oj v0.1.72+)→ `oj openapi` + `ojm api` 跑过 → onInit 里 `create<Module>Client(ctx)`
152
181
  - [ ] 接管了某功能 → provider 方法数与类型定义**一个不差**
153
182
  - [ ] 有后端 → handler 四红线过一遍(manual §1.7:respond 收口 / import 深度 / 无模块级状态 / 无模块级运行时全局)
@@ -203,6 +232,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
203
232
  | dev 日志 route error(Cannot find module)/ 端点 404 | handler 相对 import 多一层——按**目录**解析 | 改用 `#/<模块名>/…` 别名(与深度无关);存量代码按 manual §1.7 深度表改 |
204
233
  | 限速/节流/计数真实请求下永不累积(测试全绿) | 模块级 `Map`/`let` 不跨请求存活(每请求重建模块) | 状态落库(模板迁移 0009 先例)或 oj 持久面 |
205
234
  | `ojm build` 报 `introspect api.ts: jwt not configured` | 模块级语句碰运行时全局,introspect 空沙箱炸 | 移进 handler 函数体内(manual §1.7-4) |
235
+ | `ojm build` 报「框架默认模块与上游模板不一致」并列出文件 | 内置模块自治门禁:`src/app`、`src/oj-market` 被本地改过/删过/多出文件(CLI 升级后旧版默认模块也算漂移) | 需求提交上游;确认放弃本地修改则 `ojm init --yes` 恢复(会丢弃这两个目录的本地改动,先备份)。无绕过开关(manual §1.9) |
206
236
  | `ojm build` 报「密封值解不开」/「本机没有能解开的私钥」 | 出包预检 fail-closed:合并产物兜底 + tracked 非 jwt 红线(manual §10.1) | jwt 坏了删 `config.local.yaml` 的 `auth.jwt_secret` 行重跑 `ojm dev`;非 jwt 取配对私钥或 local 覆盖该字段 |
207
237
  | 页签怎么点都关不掉、无报错提示 | `beforeClose` 守卫 fail-safe 否决(守卫抛错按取消处理) | 先查控制台 `[tabs] beforeClose` 报错;确认弹窗逻辑与 `resolve(true/false)` 收口(manual §6.4) |
208
238
  | 组织架构读面 403「该部门不在可见范围内」 | `scope` 默认 `self`,只看得到自己那条部门路径 | 换 `scope=all`(需权限点 `platform.org.read.all`),或改问自己所在部门(manual §14.2) |
@@ -229,3 +259,7 @@ description: 用 ojm 开发前后端一体工程时使用——新增/修改前
229
259
  五项 `wecom_corp_*` 从哪取、回调为什么只填「通讯录同步」一处、「验证并接入」的四步自检、
230
260
  可见范围 / 可信 IP / 可信域名的处置、单企业私有化的边界(自建应用收不到成员变更事件)、
231
261
  以及和代开发共存时的配置与页面差异。**接自建应用时先读它**。
262
+
263
+ 另有 `publish-walkthrough.md`(同目录):从「代码写完」到「装进站点」的一条命令链——
264
+ 构建/打包 → 拿发布身份(凭证或离线)→ 签名 → 落 CDN → 重建索引 → verify/trial → install/TOFU,
265
+ 每步带期望输出与通过判据。**第一次发包照着它敲**。
@@ -15,9 +15,14 @@
15
15
  | 框架开发完整指南(契约层/运行时/宿主/工具链四章) | `docs/prd/framework-development-guide.md` |
16
16
  | 端到端可重复验证流程 | `docs/prd/framework-verification-playbook.md` |
17
17
  | 某特性的设计与评审记录 | `docs/prd/<日期>-<特性>-design.md` |
18
- | 各阶段实战记录(骨架→注入→认证→CRUD→release) | `apps/playground-oj/docs/phase*.md` |
18
+ | 各阶段实战记录(骨架→注入→认证→CRUD→发布) | `docs/prd/framework-verification-playbook.md` 附录 A 基线表(playground 已下线) |
19
19
  | 后端 handler 写法 | `docs/devkit/oj-api-dev/api-manual.md`(`oj-api-dev` skill 的本仓快照;用户工程侧在 `.claude/skills/oj-api-dev/`,随 oj 二进制分发) |
20
20
 
21
+ > **路径口径**:上表除 `CLAUDE.md` 与 `docs/devkit/oj-api-dev/` 外,都是**框架仓库内**的路径,
22
+ > 不随 `@oj-module/cli` 发货。本文件只对「能进框架仓库的人」有用——如果你是在 cli 包或
23
+ > 业务工程里读到这里,说明没有仓库上下文,那些链接按设计不可达,**不是坏链**:业务开发
24
+ > 请回 `SKILL.md` + `manual.md`。
25
+
21
26
  > ⚠️ 那两份 guide 有**已知陈旧处**,以源码为准:
22
27
  > dev 端口实际是 **3333**(guide 写的 5173 已过时;注意与 `ojm dev` 的前端 devServer
23
28
  > **5174** 不是一回事——前者是本仓 vite,后者是业务工程 devServer);包版本以
@@ -192,9 +197,10 @@ runtime → cli → registry 复核;`--dry-run` 只演练)。下面的手动
192
197
 
193
198
  ## 附:本 devkit 内容来源(维护者看)
194
199
 
195
- `docs/devkit/` 是**自包含**的交付文档(顶层 6 个文件,含
196
- `sql-builder-missing-capabilities-pr.md` 这份构造器能力缺口 PR 参考),业务工程拿到即可读懂,不依赖工程仓库里的其它文档。
197
- 另有 `oj-api-dev/` 子目录是 oj 侧 devkit 的快照(10 文件,仅本仓参考、不发货),
200
+ `docs/devkit/` 是**自包含**的交付文档(顶层 11 个文件,全部随 cli 包发货,其中 6 个随
201
+ `ojm init` 进业务工程;含 `sql-builder-missing-capabilities-pr.md` 这份构造器能力缺口 PR 参考),
202
+ 业务工程拿到即可读懂,不依赖工程仓库里的其它文档。
203
+ 另有 `oj-api-dev/` 子目录是 oj 侧 devkit 的快照(oj v0.1.116 起 12 文件,仅本仓参考、不发货),
198
204
  结构说明见 `README.md`。
199
205
  下面的来源仅用于维护时追溯与同步,不参与交付:
200
206
 
package/devkit/manual.md CHANGED
@@ -12,7 +12,8 @@
12
12
 
13
13
  ### 1.1 脚手架产物
14
14
 
15
- `ojm init <目录>` 生成(幂等补缺,重跑只补缺失文件,**永不覆盖已有文件**)。
15
+ `ojm init <目录>` 生成(幂等补缺,重跑只补缺失文件,**已有文件永不覆盖**;`--yes` 是唯一
16
+ 例外,见 §1.9 恢复语义)。
16
17
  模板拷贝按框架内置的 **`init.include.yaml` 白名单**执行——未收录的路径一律不拷
17
18
  (demo-only 演示模块、模板自运行产物都在清单外)。该清单是**框架侧文件**,`ojm init`
18
19
  从 cli 包内读取,**不在你的工程里**——你在工程里找不到它,也改不到它。
@@ -35,12 +36,12 @@
35
36
  ├── env.d.ts # ImportMeta.env 类型补丁
36
37
  ├── types/oj-modules.d.ts # `declare module "#*"` 别名兜底(非模块文件的 ambient 声明)
37
38
  ├── .ojm-api-exempt.json
38
- ├── seed.mysql.sql / seed.pg.sql
39
39
  ├── vitest.config.ts # 后端 BDD(include tests/**/*.spec.ts)
40
40
  ├── tests/ # 后端 BDD 测试(node:sqlite 内存库 + 真实迁移建表)
41
41
  ├── src/
42
42
  │ ├── tsconfig.json # 后端 typecheck 工程(extends 上两者,exclude */web)
43
- │ ├── .ojm-builtin.json # 框架自带方案名单(init 派生,可手改/删除;打包排除 + info 折叠,见 §11)
43
+ │ ├── README.md # src 分区说明(内置模块自治契约的人话版,动手前必读)
44
+ │ ├── .ojm-builtin.json # 框架自带方案名单(init 派生,可手改/删除;打包排除 + info 折叠 + build 门禁开关,见 §11)
44
45
  │ ├── styles.css
45
46
  │ ├── app/ # 整个应用(后端模块 + 前端聚合模块)
46
47
  │ │ ├── api/ # 后端模块(模块名 = URL 前缀,各自仍是独立 oj 模块)
@@ -60,7 +61,9 @@
60
61
  │ │ ├── auth/ # 仅 __client(无页面)
61
62
  │ │ ├── _platform/ # 仅 __client(oj-market 引用)
62
63
  │ │ └── web/ # 仅 __client
63
- │ ├── oj-market/web/ # 解决方案市场(独立模块,删目录即下线)
64
+ │ ├── oj-market/web/ # 解决方案市场(独立模块,enabled: false 下线;单根 /oj-market 挂三个子页)
65
+ │ │ ├── entry.ts # routes 聚合:/oj-market 根(方案清单 index)+ 发布管理/发布历史子路由
66
+ │ │ └── publish/ # 发布管理 + 发布历史(/oj-market/publish、/oj-market/history:routes + pages + locales + __client)
64
67
  ├── dist/api/ # 后端产物(release 根:<name>-<ver>/ + manifests.yaml + versions.json)—构建后才有
65
68
  ├── dist/web/ # 站点产物(shell + modules/ + modules.json)—构建后才有
66
69
  ├── bin/ # oj 二进制 + plugins/ + devkit/(不入库)
@@ -198,11 +201,25 @@ src/<模块>/api/<路径...>/api.ts → /api/<模块>/<路径...>
198
201
  | 文件 | 何时生效 |
199
202
  |---|---|
200
203
  | `migrations/*.sql` | 启动(dev `migrate_on_start: auto`);release 默认 `verify`,账本落后则**拒绝启动** |
201
- | `seed.sql` | **每次启动都重放**(sqlite 默认库)——必须幂等,用 `INSERT OR IGNORE` |
204
+ | `seed.sql` / `seed.<方言>.sql` | **每次启动都重放**——方言件(`.sqlite` / `.mysql` / `.pg`)存在即胜出、`seed.sql` 兜底、绝不双跑(oj ≥ 0.1.121);必须幂等 |
202
205
  | `schema.yaml` | 声明;加可空列 / 加表 reconcile 能收敛,但**改类型与改名必须走 migrations** |
203
206
 
204
207
  > `seed.sql` 按 `;` 朴素切分,**注释里不许出现分号字面量**,否则语法错。
205
208
 
209
+ > **方言 seed 放好即生效(oj ≥ 0.1.121,已是 `ojm` 最低版本门禁)**:方言件按模块存放
210
+ > (`src/app/api/<模块>/seed.mysql.sql` / `seed.pg.sql`,与基础件同表同条数),启动时按当前库
211
+ > 方言自动选——方言件胜出、`seed.sql` 兜底、**绝不双跑**;成功日志打 `file=` / `dialect=`,
212
+ > 模块只有别的方言件或文件名拼错(如 `seed.postgres.sql`)会 warn,`oj check` 的 seed 检查会报错。
213
+ > 更早的 oj 只认字面 `seed.sql`(会把 sqlite 写法原样下发 mysql/pg → 1064),已低于门禁,
214
+ > `ojm vendor` / `ojm info` 会直接拒绝。工程根**不再**有方言 seed 副本(历史上那份只含部分
215
+ > 设置、且 mysql 侧裸写 `key`,已删除)。
216
+
217
+ > **框架自带 `_platform` 模块的迁移已压平为单条基线**(`0001__baseline.sql` 及
218
+ > `.mysql.sql` / `.pg.sql` 两个方言版):新工程按基线一次建表,不再有 0002…0018 的增量链。
219
+ > 老开发库的迁移账本里记着旧序号,**基线不会被重放**——若库结构落后(例如缺新列),
220
+ > 直接删 `api/db.sqlite` 重启即可(dev 自动重跑迁移 + seed),本项目**不提供兼容 shim**。
221
+ > 基线必须三方言同步:只写 sqlite 版会让同一模块在 mysql/pg 部署上建表失败。
222
+
206
223
  ### 1.7 handler 四红线(oj 运行时实测钉出,2026-10-16 RBAC 阶段)
207
224
 
208
225
  以下四条都是真机冒烟(`ojm init` + `ojm dev`/`build`)实测钉出的 oj 运行时行为,
@@ -268,6 +285,35 @@ oj 的 `#` 导入与目录深度无关(后端解析规则见 `oj-api-dev` 手
268
285
  - `#` 与 Node `package.json#imports` 共用命名空间:工程里若出现 `#` 开头的 imports 键,
269
286
  `oj build` 直接失败(避免两套解析器分叉)。
270
287
 
288
+ ### 1.9 内置模块自治契约(build 门禁)
289
+
290
+ **`src/app/`(框架内置页面与后端)和 `src/oj-market/` 是上游自治的,下游不许改。**
291
+ `ojm build`(含 `--bundle`)与 `ojm market publish --real` 在任何产物写入前,把这两个目录
292
+ 逐文件与**当前 cli 包内模板**比对(上游模板即基线;比较前行尾归一,git autocrlf 不误报):
293
+
294
+ | 漂移 | 判定 |
295
+ | --- | --- |
296
+ | 改了内容 | `modified` — 拦 |
297
+ | 删了白名单文件 | `missing` — 拦 |
298
+ | 目录里多出文件 | `extra` — 拦 |
299
+
300
+ 被拦时构建直接失败、**零产物**,报错点名全部差异文件,并给出唯一正路:
301
+
302
+ - 功能需求/缺陷 → **提交上游 issue**,等新版本下发;
303
+ - 确认放弃本地修改 → `ojm init --yes` 恢复上游版本(见下)。
304
+
305
+ 没有绕过开关。这不是安全边界——删掉 `src/.ojm-builtin.json`(门禁开关)会整体退回旧行为,
306
+ 但没人替你兜底升级时的功能漂移;`enabled: false` 是下线某个自带能力的正确姿势(不是删目录)。
307
+
308
+ **`ojm init --yes`(恢复/升级模式)**:日常重跑 init 永不覆盖;`--yes` 额外把 `src/app`、
309
+ `src/oj-market` 恢复成当前 CLI 携带的上游版本(覆盖白名单文件、删除目录内多出文件、重写
310
+ `.ojm-builtin.json` 为上游名单)。**会丢弃这两个目录里的本地修改,执行前先备份**;下游自定义
311
+ 模块(`src/<你的模块>/`)与工程配置(config.yaml / web.config.ts / package.json 等)不受影响。
312
+
313
+ **CLI 升级后必须先同步**:新版 CLI 的模板就是新基线,旧版默认模块(即使一行没改)与新模板
314
+ 不一致即被拦——升级 cli 后先跑一次 `ojm init --yes` 再 build。dev 不拦,本地随便试验;
315
+ 发布产物必须过门禁。
316
+
271
317
  ---
272
318
 
273
319
  ## §2 写前端模块
@@ -379,7 +425,7 @@ export default defineModule({
379
425
  | `icon` | `createElement(XxxOutlined)` |
380
426
  | `keepAlive` | 页面缓存(多 tab 开启时有效;与 layout 解绑;相对/绝对声明均可,key 取路由绝对路径) |
381
427
  | `hideInMenu` | 不在菜单显示 |
382
- | `roles` / `permissions` | 路由级权限(页面级 / 按钮级) |
428
+ | `roles` / `permissions` | 路由级权限(页面级 / 按钮级);可绑定的角色/权限点目录由方案契约声明供给(§11.5) |
383
429
  | `iframeLink` | 内嵌 iframe 的外链地址 |
384
430
 
385
431
  > 另有**模块级** `config.requiredRoles` / `config.requiredPermissions`——它在路由
@@ -1135,13 +1181,11 @@ export default {
1135
1181
  | `container` | `ContainerLayout` | **有页面的业务路由必写**。完整外壳:顶栏 + 侧边/顶部导航 + 页签(keepAlive)+ 主内容 + 可选页脚 + 回到顶部 + 水印 |
1136
1182
  | `parent` | `ParentLayout` | 嵌套父级,自身只渲染 `<Outlet/>`(不挂任何 chrome),用于「菜单分组 / 多级路由」的父节点 |
1137
1183
  | `fullscreen` | `FullscreenLayout` | 全屏外壳:占满视口 + 品牌区(logo + 应用标题)+ 右上角工具(布局预览/主题/语言)+ 横幅 + 页脚,**无侧边栏/页签**。登录页即用它 |
1138
- | `none` | 裸 `<Outlet/>` | 不挂任何外壳,页面裸渲染。**不写 `layout` 时运行时回落为此值** |
1184
+ | `none` | 裸 `<Outlet/>` | 不挂任何外壳,页面裸渲染。普通路由不写 `layout` 时回落为此值;`category: "admin"` 未声明时默认 `container` |
1139
1185
 
1140
1186
  > 红线(见 SKILL.md):有页面的路由 handle 忘写 `layout: "container"`,页面会**裸奔**
1141
- > (无侧边栏/页签/顶栏),且 `keepAlive` 失效。
1142
- >
1143
- > 迁移说明:类型定义里 `handle.layout` 当前标了默认 `"container"` 仅为向后兼容,
1144
- > **目标态是改为 `"none"`**——不要依赖默认值,每个路由都显式写 `layout`。
1187
+ > (无侧边栏/页签/顶栏),且 `keepAlive` 失效。唯一例外是 `category: "admin"`:未声明时默认
1188
+ > `container`,需要其他布局时仍可显式覆盖。
1145
1189
 
1146
1190
  ### 5.2 路由如何选布局
1147
1191
 
@@ -1919,9 +1963,9 @@ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登
1919
1963
  一个脚手架可放多份用户方案(`ojm new` 逐个生成,各自一份 `src/<name>/solution.yaml`),
1920
1964
  `ojm build --bundle` 无参时全部打包、逐个校验独立出包;框架自带方案自动排除(名单见
1921
1965
  `src/.ojm-builtin.json`,`ojm info` 默认折叠自带模块)。
1922
- **从开发到发布的完整过程视角(新手照做版,含签名/私钥纪律与多人各自发包纪律)见本目录**
1923
- [`publish-walkthrough.md`](./publish-walkthrough.md);仓内深度过程视角另见
1924
- [`docs/guides/solution-dev-build-publish.md`](../guides/solution-dev-build-publish.md)(不随 cli 包发货)。
1966
+ **从开发到发布的完整过程视角(新手照做版,含签名/私钥纪律与多人各自发包纪律)见本目录
1967
+ [`publish-walkthrough.md`](./publish-walkthrough.md)**——那份文件同样随包分发、自包含,
1968
+ 发布链路只读它 + 本章即可闭环。
1925
1969
 
1926
1970
  ### 11.1 打包:`ojm build --bundle`
1927
1971
 
@@ -2021,6 +2065,14 @@ flowchart TD
2021
2065
  ```
2022
2066
 
2023
2067
  - **needs_trust 确认后重试同一安装**即可续跑;一切消费面须把它当**非成功态**渲染。
2068
+ - **`partial` 不都是失败**:oj 0.1.122+ 首装 11 段全 `ok` 也可以终态 `partial`(`restart_pending`
2069
+ 正常语义——新模块的路由/模块注册要宿主重启,message 明示待重启段);重启宿主后 retry 同一条
2070
+ install → `done`,冒烟三项(探活 / integrity / 注册一致性)全过。partial 仅在 `reloading` 段
2071
+ 报 `unchanged` 时才是真失败(对着 dev 装的典型症状)。
2072
+ - **preflight 含匿名面检查(`anonymous_residual`)**:实际匿名可达的端点必须在方案契约
2073
+ `api.anonymous` 里声明(`ojm new --api` 骨架已自带 ping 的声明);漏声明的包拒装且不可自动重试。
2074
+ - **preflight 含 RBAC 声明校验**:`permissions[]` / `roles[]` 的命名前缀、`title_key`
2075
+ 必填、悬空引用双端拒绝——形状细则与注册/管理面见 §11.5。
2024
2076
 
2025
2077
  ### 11.4 生命周期命令与站点寻址约定
2026
2078
 
@@ -2042,6 +2094,61 @@ flowchart TD
2042
2094
  > `enable`/`disable`/`uninstall` 失败多半是「站点地址/凭据没给对」或「站点没这个方案」——
2043
2095
  > 先 `ojm info` 贴出版本矩阵与模块清单,确认目标站点与方案名无误再重试。
2044
2096
 
2097
+ ### 11.5 方案自管理角色/权限(声明 → 安装注册 → 管理面 → 多语言)
2098
+
2099
+ 方案可以自带角色与权限点目录,装进站点后由管理员在「系统管理 → 角色权限」页分配——
2100
+ **声明属方案契约(`solution.yaml`),不是 `api/manifest.yaml`**(后者是 oj 原生七键清单,
2101
+ 写方案级字段安装期被白名单拒装;见 §1.2)。
2102
+
2103
+ **声明面(`solution.yaml`,两段都可选)**:
2104
+
2105
+ ```yaml
2106
+ permissions: # code 强制 <name>. 前缀;title_key 必填 <name>:perm.*;包内唯一
2107
+ - code: wiki.doc.create
2108
+ title_key: wiki:perm.doc_create
2109
+ - code: wiki.doc.read # 无角色引用也合法(系统预置点,管理员在管理面自行分配)
2110
+ title_key: wiki:perm.doc_read
2111
+ roles: # name 强制 <name>.<role>;title_key 必填 <name>:role.*;
2112
+ - name: wiki.editor # permissions ⊆ permissions[].code(悬空引用双端拒绝)
2113
+ title_key: wiki:role.editor
2114
+ permissions: [wiki.doc.create, wiki.doc.read]
2115
+ ```
2116
+
2117
+ **安装注册(registering 段,唯一出口 `upsertSolutionPerms`)**:
2118
+
2119
+ - **title 安装期解析**:`title` 由 `title_key` 经包内
2120
+ `web/modules/<name>/locales/<站点默认语言>.json` 解析落库(站点默认语言读 `sys_setting`
2121
+ `site.language`,缺省 zh-CN);ns ≠ 本方案名不越界读;任何解析失败保留原文、**绝不拒装**。
2122
+ - **title_key 随行落库(展示锚)**:`sys_role.title_key` / `sys_permission.title_key` 存原始 key,
2123
+ 展示页按当前语言实时翻译、`title`/`code` 兜底。
2124
+ - **升级即重解析**:新版本 locale 文案改进随升级覆写元数据(title/title_key);
2125
+ 存量站点的权限点补 title_key 也是同一条路——**重装同版本方案即触发覆写**。
2126
+ - **只加法**:声明绑定 insert-if-absent(管理员撤过的绑定不复活);声明消失 →
2127
+ 行保留标 `deprecated`(装机告警清单);**重声明复位 `deprecated=0`**;
2128
+ 卸载双维级联清理。除该出口外不得直写 `sys_role`/`sys_permission`。
2129
+ - **落库不产生任何授权**:声明权限点只建目录行,判权仍走角色绑定——别以为把 code 写进
2130
+ manifest 就等于拿到权限。
2131
+
2132
+ **管理面(`/api/system/*`,需系统管理员——superadmin 或 admin,其余角色 403;`custom`/`wiki.*` 命名空间互斥)**:
2133
+
2134
+ | 端点 | 作用 | 红线 |
2135
+ |---|---|---|
2136
+ | `GET /api/system/roles` | 角色清单(module 归属 / title+title_key / holders / 已绑权限) | `superadmin` 行前端展示层隐藏(后端原样返回) |
2137
+ | `POST /api/system/roles` | 建自定义角色 | 机器名强制 `custom.<slug>`;未知权限码 400 |
2138
+ | `PUT /api/system/roles/bindings?name=` | 整集替换角色↔权限绑定 | builtin 角色硬拒 403;写后按持有者失效判权缓存(撤销即强退) |
2139
+ | `DELETE /api/system/roles?name=` | 删角色 | builtin 硬拒 403;有持有者 409(`holders` 明细) |
2140
+ | `GET /api/system/permissions` | 权限点目录(含 deprecated 标记,UI 标灰) | 判权链不过滤 deprecated |
2141
+ | `PUT /api/system/users/roles?uid=` | 整集替换用户角色(`sys_user_role` 唯一权威源) | 写后失效该用户判权缓存 |
2142
+
2143
+ **展示与多语言(「系统管理 → 角色权限」页,`/system/roles`)**:方案角色按当前语言实时渲染
2144
+ `t(title_key, { defaultValue: title })`;builtin 角色回退框架词典 `system:rolesPage.roleTitle.<name>`;
2145
+ custom 角色原样显示 title;权限点 tab 同理,title_key 不命中回退裸 code。
2146
+ `title_key` 引用的键须在方案 `web/locales/{zh-CN,en-US}.json` 双语落齐(打包校验项)。
2147
+
2148
+ > 与前端路由权限的关系:路由级 `handle.roles` / `permissions` 与模块级
2149
+ > `config.requiredRoles` / `requiredPermissions`(§2.4)是**消费端**过滤;本节是**供给端**
2150
+ > 目录与管理面。判权本身在服务端按 `sys_user_role` → `sys_role_permission` 链路裁决。
2151
+
2045
2152
  ---
2046
2153
 
2047
2154
  ## §12 签名与信任
@@ -2075,16 +2182,26 @@ flowchart LR
2075
2182
  ### 12.1 发布流程(作者侧,三步)
2076
2183
 
2077
2184
  ```bash
2078
- # ① 一次性:生成 Ed25519 密钥对(私钥 ~/.ojm/keys/<id>/ 永不出目录——--out 指进工程树须 --yes)
2079
- ojm keygen --id <publisher>.<key-name>
2185
+ # ① 一次性:拿发布身份。站点有「方案市场 → 发布管理」页时,用页面签发的一次性凭证兑换(key_id 由站点算好下发,
2186
+ # CLI 不推导;兑换即作废;失败不落任何密钥文件;与 --id 互斥,同时给 exit 2):
2187
+ ojm keygen --credential <token> [--site-url <站点>]
2188
+ OJM_PUBLISH_CREDENTIAL=<token> ojm keygen
2189
+
2190
+ # ①' 旁路:只在没有「发布管理」页(纯自建/离线 CDN)或 CI 临时密钥时自建 key_id——
2191
+ # 站点侧没有这条身份记录,产品归属与申请人无从追溯,只能靠超管为陌生指纹手工放行:
2192
+ ojm keygen --id <publisher>.<key-name> # 私钥 ~/.ojm/keys/<id>/ 永不出目录;--out 指进工程树须 --yes
2080
2193
 
2081
- # ② 每次发布:签名(产物 = 同目录 <tgz>.sig;--check 离线自检,不必靠装失败发现签错)
2082
- ojm sign dist/<name>-<version>.tgz --id <publisher>.<key-name> --check
2194
+ # ② 每次发布:签名(--id 填上一步拿到的 key_id;产物 = 同目录 <tgz>.sig;--check 离线自检)
2195
+ ojm sign dist/<name>-<version>.tgz --id <key_id> --check
2083
2196
 
2084
2197
  # ③ 安装/分发:.sig 自动探测随包上传
2085
2198
  ojm install dist/<name>-<version>.tgz --site-url <站点> --user superadmin --pass …
2086
2199
  ```
2087
2200
 
2201
+ 凭证通道的站点地址:`--site-url` > `OJM_SITE_URL` > `http://127.0.0.1:9778`;兑换端点匿名可达,
2202
+ 不需要 `--user/--pass`。key_id 形如 `<产品名>.<编码后用户名>`(例:`demo-solution.wecom-everpan`),
2203
+ 由站点按申请时的产品名与账号算好。
2204
+
2088
2205
  宿主验签六码(失败文案双语,`--locale` 切换):`sig_missing`(缺签)/ `sig_alg`(算法
2089
2206
  不受接受)/ `sig_key`(key_id 命名空间违约)/ `tampered`(验签不过)/ `unknown_key`
2090
2207
  (信任库无此 key → **needs_trust 第五终态**)/ `sig_format`(.sig 结构违约)。
@@ -2093,8 +2210,9 @@ ojm install dist/<name>-<version>.tgz --site-url <站点> --user superadmin --pa
2093
2210
 
2094
2211
  **TOFU = Trust On First Use(首次使用即信任)**:陌生发布方的第一个包,安装器无法判断
2095
2212
  该不该信,于是把公钥指纹摆出来让人核对——核对通过才入库,之后同一 key_id 直接放行。
2096
- 信任的建立因此有两条路:**第一方**走模板内置公钥免核对(§12.3),**第三方**走本节的
2097
- 人工核对。
2213
+ 信任的建立因此有两条路:**模板内置的 oj-market 方案**用内置公钥免核对(§12.3),其余来源
2214
+ (含你自己 keygen 的身份、站点凭证下发的身份)一律走本节的人工核对——凭证只定 key_id 归属,
2215
+ 站点在兑换时拿不到你的公钥,无法代为建立信任。
2098
2216
 
2099
2217
  - 信任库 = `.solutions/trusted-keys.yaml`(key_id → 公钥 + added_at;首次绑定不可改,
2100
2218
  轮换 = 新增 key_id)。
@@ -2148,7 +2266,7 @@ export default defineModule({
2148
2266
  handle: { title: "wiki:menu.main", layout: "container" },
2149
2267
  children: [
2150
2268
  { path: "space", handle: { title: "wiki:menu.space", layout: "parent" } },
2151
- { path: "admin", handle: { title: "wiki:menu.admin", category: "admin", layout: "container" } },
2269
+ { path: "admin", handle: { title: "wiki:menu.admin", category: "admin" } },
2152
2270
  { path: "status/detail", handle: { title: "wiki:menu.statusDetail", category: "hidden" } },
2153
2271
  ],
2154
2272
  }],
@@ -2200,7 +2318,11 @@ const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
2200
2318
 
2201
2319
  - 省略 / `""` / 旧 `"main"` → 普通可见菜单项。**`"main"` 不再产生合成父菜单**(扁平 main 归组已取消),新代码直接省略。
2202
2320
  - `"hidden"` → 有效路由但**不进菜单**(等价于 `hideInMenu: true`),用于详情页等。
2203
- - `"admin"` → 路由层被 `rewriteAdminPaths` 真重写为 `/system/<模块 root path>/*`(运行时 URL 真正归到系统设置),菜单层再被 `groupSolutionMenus` 收进 `/system` 子组 `solgroup:admin:<模块>`。
2321
+ - `"admin"` → 路由层被 `rewriteAdminPaths` 真重写为 `/system/<模块 root path>/*`(运行时 URL 真正归到系统设置),菜单层再被 `groupSolutionMenus` 收进 `/system` 子组 `solgroup:admin:<模块>`;未声明 `layout` 时默认 `container`,显式配置优先。
2322
+
2323
+ 下游主动导航时调用公开纯函数 `resolveRoutePath(route)` 获取最终 path,不复制重写规则。相对 admin path 会从 `getRoutes()` 生成的绝对 `id` 补全模块根;普通路由 path 原样返回,已重写 path 不重复添加 `/system`。
2324
+
2325
+ admin 子树内的后代路由自身不声明 `category`(继承自父级),调用时追加祖先链 `resolveRoutePath(child, moduleRoot, ..., parent)`(链从模块根到直接父级):重写结果为 `/system + 原绝对路径`(与 admin 嵌套深度无关),原绝对路径优先取已生成的 `id`(index 路由含尾斜杠),无 id 按链拼接;链上无 admin 祖先时仅拼接绝对路径,已重写 id 幂等。
2204
2326
 
2205
2327
  > `/system` 是框架侧断言,方案只标 `category: "admin"`,挂载点交给框架;宿主缺 `/system` 时告警一次。
2206
2328
 
@@ -2216,7 +2338,7 @@ const paths = createWikiPaths(getModuleRoutePrefix("wiki"));
2216
2338
 
2217
2339
  - 在 `entry.ts` 路由 handle 直接声明归组/菜单元数据(**真源**):
2218
2340
  `{ title: "wiki:menu.space", layout: "parent" }`、
2219
- `{ title: "wiki:menu.admin", category: "admin", layout: "container" }`
2341
+ `{ title: "wiki:menu.admin", category: "admin" }`
2220
2342
  - 模块只设一个根路由,根 `title` 用 `<模块>:menu.main`;`manifest.yaml` 的 `routes[]` 不必手写(构建期从 handle 派生覆盖)。
2221
2343
  - 在 `locales/{zh-CN,en-US}.json` 里补嵌套的 `menu.main`。
2222
2344
  - `admin` 路由统一 `category: "admin"`;模块内路径走 `paths.ts` 助手,`currentActiveMenu` 写绝对路径且从助手派生。
@@ -2440,7 +2562,7 @@ LDAP 侧改名 / 删人后,`origin='ldap'` 的行跟着变,而 `origin='weco
2440
2562
  | §8 排查 | 五步排查法、症状速查表 | 任何「没生效」 |
2441
2563
  | §9 参考 | 可抄的工程内文件、oj-api-dev 手册指引 | 找范本 |
2442
2564
  | §10 CLI 子命令参考 | ojm 原生命令 vs oj 透传命令;`market` 子命令(本地模拟 CDN) | 查命令用法;`ojm <cmd> --help` 看单命令帮助 |
2443
- | §11 方案包:打包与分发 | build --bundle、Form A/B、分发源、索引规范、引用式方案(api 段省略 tables/deps,引用包内原生 manifest) | 发布方案包 |
2565
+ | §11 方案包:打包与分发 | build --bundle、Form A/B、分发源、索引规范、引用式方案(api 段省略 tables/deps,引用包内原生 manifest)、**方案自管理角色/权限(声明/注册/管理面/多语言 §11.5)** | 发布方案包;方案要自带角色/权限目录 |
2444
2566
  | §12 签名与信任 | keygen/sign/install 发布流程、信任库、TOFU、bootstrap key | 首次签名发布;管理信任 |
2445
2567
  | §13 方案菜单归组规则 | 单根模块约定(根路由=第一级菜单)、自定义根路径 `web.route_prefix`(构建写 modules.json、运行时只改根段、getModuleRoutePrefix 单点消费)、category 三值(普通/hidden/admin)、admin 真重写 `/system/<模块 root>/*` + 视觉归组、menu.main 统一、模块内路径助手、开发者 DO/DON'T | 写方案要让菜单按模块分组、admin 归系统设置 |
2446
2568
  | §14 组织架构与通讯录 | 内置底座(两张租户表 + `users` 三列)、共享读面 7 端点与两级 `scope` 门禁、前端怎么「借壳」出自己端点、归属权四条规则与管理端 CRUD、源适配器接点与三条禁令(企微为范本,LDAP 前置)、通讯录与组织架构的分工 | 页面要选人/选部门、通知要按部门算收件人、要接新的组织数据来源 |
@@ -53,8 +53,8 @@ export default defineModule({
53
53
  { path: "space", handle: { title: "wiki:menu.space", layout: "parent" } },
54
54
  { path: "status", handle: { title: "wiki:menu.status", layout: "parent" } },
55
55
  { path: "collections", handle: { title: "wiki:menu.collections", layout: "parent" } },
56
- // admin 页面:标 category:"admin",路径会被真重写为 /system/wiki/...(§2)
57
- { path: "admin", handle: { title: "wiki:menu.admin", category: "admin", layout: "container" } },
56
+ // admin 页面:标 category:"admin",默认 container,路径真重写为 /system/wiki/...(§2)
57
+ { path: "admin", handle: { title: "wiki:menu.admin", category: "admin" } },
58
58
  // 仅可达、不进菜单的页面:category:"hidden"
59
59
  { path: "status/detail", handle: { title: "wiki:menu.statusDetail", category: "hidden" } },
60
60
  ],
@@ -67,7 +67,7 @@ export default defineModule({
67
67
 
68
68
  - 模块**只有一个根路由**,它渲染成侧栏第一级菜单项。根路由下的页面是其 `children`,按 `category` 决定是否在菜单出现(见 §1)。
69
69
  - 根路由的 `handle.title` 用 `<模块名>:menu.main`(这是「模块标题」与「该模块在 /system 下的管理子组标题」共用的同一个 key,见 §4)。
70
- - `layout` 语义:`container` = 全站外壳(顶栏/侧栏/页签/页脚);`parent` = 仅 Outlet 包裹(分组路由,无额外外壳);深嵌套分组路由必须用 `parent`,否则会叠加出双层外壳(双击 chrome)。根路由与 admin 路由保留 `container`。
70
+ - `layout` 语义:`container` = 全站外壳(顶栏/侧栏/页签/页脚);`parent` = 仅 Outlet 包裹(分组路由,无额外外壳);深嵌套分组路由必须用 `parent`,否则会叠加出双层外壳(双击 chrome)。根路由保留 `container`;admin 路由未声明 `layout` 时默认 `container`,显式配置仍优先。
71
71
  - `module` 由 module-loader 在 `getRoutes()` 注册时按 `definition.name` 自动注入,**你不用写**;旧 bundle 也带标。
72
72
  - 不要为了归组父菜单去手写一个额外的「分组根路由」——第一级菜单就是模块根路由本身。
73
73
 
@@ -113,6 +113,21 @@ export default defineModule({
113
113
  "/wiki/admin" (category:"admin") ──► hoist + 重写 ──► "/system/wiki/admin"
114
114
  ```
115
115
 
116
+ 下游需要主动导航时,从公开入口调用 `resolveRoutePath(route)`,不要复制这套映射规则。它接收单条 route 并返回最终 path:admin 添加 `/system`,普通路由原样返回,已重写 path 幂等;相对 admin path 使用 `getRoutes()` 生成的绝对 `id` 补全模块根。
117
+
118
+ admin 子树内的后代路由(自身不声明 `category`,继承自父级)追加祖先链参数,链从模块根到直接父级:
119
+
120
+ ```ts
121
+ import { resolveRoutePath } from "@oj-module/runtime";
122
+
123
+ const target = resolveRoutePath(route);
124
+ if (target)
125
+ navigate(target);
126
+
127
+ // admin 子树后代
128
+ const childTarget = resolveRoutePath(childRoute, moduleRootRoute, adminParentRoute);
129
+ ```
130
+
116
131
  > 真重写 vs 旧视觉归组:旧版只在菜单层把 admin 挪到 /system,路由 URL 仍是 `/wiki/admin`,
117
132
  > 深链与 SEO 都错位。新版在路由层就改掉路径,URL 与菜单一致。
118
133
 
@@ -341,7 +356,7 @@ headless chromium 核对「`/` 重定向到 `/oj/<首个模块路由>`」与「
341
356
 
342
357
  - 在 `entry.ts` 路由 handle 直接声明归组/菜单元数据(**真源**):
343
358
  `{ title: "wiki:menu.space", layout: "parent" }`、
344
- `{ title: "wiki:menu.admin", category: "admin", layout: "container" }`
359
+ `{ title: "wiki:menu.admin", category: "admin" }`
345
360
  - 模块只设**一个根路由**,其余页面作其 `children`;根路由 `title` 用 `<模块>:menu.main`。
346
361
  - `manifest.yaml` 的 `routes[]` **不必手写**:构建期从 entry.ts handle 自动派生并覆盖;漂移时警告一次并以 entry.ts 为准。
347
362
  - 在 `locales/{zh-CN,en-US}.json` 里补嵌套的 `menu.main`(根菜单标题 + 系统设置管理子组标题共用)。
@@ -57,6 +57,10 @@
57
57
  > 解析数据源是包内 `web/modules/<name>/locales/`——`build --bundle` 自动从源码复制双语,
58
58
  > 作者零动作。key 缺失/文件缺失 → title 存裸 key,**安装永不因此失败**;改 locale 文案后重新
59
59
  > 打包升级,角色名即随升级更新。
60
+ >
61
+ > **权限点标题同理落库(0.2.68 起)**:`permissions[].title_key`(契约必填)随行落库
62
+ > `sys_permission.title_key`(此前只校验不落库、展示页回退裸 code);升级元数据覆写与
63
+ > deprecated 重声明复位与角色同纪律。声明面/管理面/多语言全景见 devkit 手册 §11.5。
60
64
 
61
65
  ### 3.2 但同名再装会被直接拒
62
66