@oj-module/cli 0.2.6 → 0.2.14

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 (169) hide show
  1. package/README.md +1 -1
  2. package/devkit/README.md +70 -24
  3. package/devkit/framework-dev.md +14 -5
  4. package/devkit/manual.md +28 -23
  5. package/devkit/module-dev-and-cdn-playground.md +197 -0
  6. package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
  7. package/package.json +2 -2
  8. package/shell-dist/assets/runtime.js +130 -45
  9. package/shell-dist/index.html +2 -2
  10. package/shell-dist/versions.json +3 -3
  11. package/src/__tests__/build-backend.test.ts +64 -0
  12. package/src/__tests__/versions.test.ts +26 -0
  13. package/src/build.ts +9 -0
  14. package/src/bundle.ts +8 -5
  15. package/src/contract/__tests__/handler-scan.test.ts +29 -0
  16. package/src/contract/__tests__/market-client.integration.test.ts +170 -0
  17. package/src/contract/handler-scan.ts +35 -4
  18. package/src/dev.ts +10 -2
  19. package/src/index.ts +9 -3
  20. package/src/init.ts +24 -0
  21. package/src/lifecycle/__tests__/bundle.test.ts +3 -3
  22. package/src/market/__tests__/bootstrap.test.ts +204 -0
  23. package/src/market/__tests__/build-index.test.ts +154 -0
  24. package/src/market/__tests__/cdn-layout.test.ts +96 -0
  25. package/src/market/__tests__/failure-playbook.test.ts +33 -0
  26. package/src/market/__tests__/fixture.test.ts +48 -0
  27. package/src/market/__tests__/publish.test.ts +109 -0
  28. package/src/market/__tests__/report.test.ts +51 -0
  29. package/src/market/__tests__/setup.test.ts +114 -0
  30. package/src/market/__tests__/verify.test.ts +111 -0
  31. package/src/market/bootstrap.ts +255 -0
  32. package/src/market/build-index.ts +159 -0
  33. package/src/market/cdn-layout.ts +122 -0
  34. package/src/market/command.ts +139 -0
  35. package/src/market/failure-playbook.ts +57 -0
  36. package/src/market/fixture.ts +138 -0
  37. package/src/market/index-builder.ts +76 -0
  38. package/src/market/publish.ts +156 -0
  39. package/src/market/report.ts +67 -0
  40. package/src/market/scan.ts +99 -0
  41. package/src/market/setup.ts +144 -0
  42. package/src/market/trial.ts +121 -0
  43. package/src/market/verify.ts +298 -0
  44. package/src/market/walkthrough.ts +506 -0
  45. package/src/oj.ts +3 -1
  46. package/src/preview-config.ts +177 -0
  47. package/src/preview.ts +106 -56
  48. package/src/sign.ts +21 -6
  49. package/src/usage.ts +22 -2
  50. package/src/vendor.ts +4 -2
  51. package/src/versions.ts +28 -0
  52. package/templates/api/.ojm-api-exempt.json +6 -2
  53. package/templates/api/config.yaml +24 -2
  54. package/templates/api/src/_platform/README.md +31 -0
  55. package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
  56. package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
  57. package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
  58. package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
  59. package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
  60. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
  61. package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
  62. package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
  63. package/templates/api/src/_platform/_lifecycle/uninstall.ts +29 -56
  64. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
  65. package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
  66. package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
  67. package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
  68. package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +25 -22
  69. package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
  70. package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
  71. package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
  72. package/templates/api/src/_platform/_shared/service/infra/oplog.ts +40 -0
  73. package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
  74. package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
  75. package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
  76. package/templates/api/src/_platform/contacts/api.ts +2 -2
  77. package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
  78. package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
  79. package/templates/api/src/_platform/manifest.yaml +4 -1
  80. package/templates/api/src/_platform/market/api.ts +54 -16
  81. package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
  82. package/templates/api/src/_platform/schema.yaml +26 -0
  83. package/templates/api/src/_platform/solutions/api.ts +13 -12
  84. package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
  85. package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
  86. package/templates/api/src/auth/_shared/session.ts +26 -22
  87. package/templates/api/src/auth/api.ts +1 -1
  88. package/templates/api/src/demo/todos/api.ts +8 -8
  89. package/templates/api/src/home/line/api.ts +4 -4
  90. package/templates/api/src/home/pie/api.ts +4 -4
  91. package/templates/api/src/notification/_shared/notify.ts +97 -57
  92. package/templates/api/src/notification/api.ts +1 -1
  93. package/templates/api/src/notification/manifest.yaml +1 -0
  94. package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
  95. package/templates/api/src/notification/schema.yaml +7 -0
  96. package/templates/api/src/notification/seed.sql +5 -4
  97. package/templates/api/src/personal-center/profile/api.ts +63 -0
  98. package/templates/api/src/personal-center/upload/api.ts +1 -1
  99. package/templates/api/src/system/login-methods/api.ts +1 -1
  100. package/templates/api/src/system/oplog/api.ts +3 -3
  101. package/templates/api/src/system/permissions/api.ts +2 -2
  102. package/templates/api/src/system/roles/api.ts +2 -2
  103. package/templates/api/src/system/roles/bindings/api.ts +2 -2
  104. package/templates/api/src/system/settings/api.ts +2 -2
  105. package/templates/api/src/system/users/api.ts +2 -2
  106. package/templates/api/src/system/users/forbidden/api.ts +2 -2
  107. package/templates/api/src/system/users/password/api.ts +2 -2
  108. package/templates/api/src/system/users/roles/api.ts +2 -2
  109. package/templates/api/src/web/user-info/api.ts +4 -5
  110. package/templates/api/tests/auth-session.spec.ts +1 -1
  111. package/templates/api/tests/auth-throttle.spec.ts +1 -1
  112. package/templates/api/tests/fixture.ts +8 -16
  113. package/templates/api/tests/guard.spec.ts +1 -1
  114. package/templates/api/tests/l1-builder.test.ts +91 -0
  115. package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
  116. package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
  117. package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
  118. package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
  119. package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
  120. package/templates/api/tests/query-utils.spec.ts +85 -1
  121. package/templates/api/tests/rbac-audit.spec.ts +1 -1
  122. package/templates/api/tests/rbac-contacts.spec.ts +2 -2
  123. package/templates/api/tests/rbac-guard.spec.ts +1 -1
  124. package/templates/api/tests/rbac-roles.spec.ts +2 -2
  125. package/templates/api/tests/rbac-settings.spec.ts +66 -21
  126. package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
  127. package/templates/api/tests/rbac-users.spec.ts +1 -1
  128. package/templates/api/tsconfig.json +8 -0
  129. package/templates/cdn/README.md +48 -0
  130. package/templates/env.d.ts +2 -0
  131. package/templates/global.d.ts +850 -0
  132. package/templates/init.include.yaml +14 -2
  133. package/templates/logs/task-events.jsonl +0 -0
  134. package/templates/openapi.json +580 -23
  135. package/templates/package.json +2 -1
  136. package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
  137. package/templates/web/src/_platform/__client/api.ts +152 -39
  138. package/templates/web/src/notification/entry.ts +46 -9
  139. package/templates/web/src/notification/locales/en-US.json +19 -0
  140. package/templates/web/src/notification/locales/zh-CN.json +19 -0
  141. package/templates/web/src/notification/pages/center/index.tsx +131 -0
  142. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
  143. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
  144. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
  145. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
  146. package/templates/web/src/oj-market/api/client.ts +5 -105
  147. package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
  148. package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
  149. package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
  150. package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
  151. package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
  152. package/templates/web/src/oj-market/entry.ts +8 -2
  153. package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
  154. package/templates/web/src/oj-market/locales/en-US.json +2 -1
  155. package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
  156. package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
  157. package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
  158. package/templates/web/src/personal-center/__client/api.ts +25 -1
  159. package/templates/web/src/personal-center/entry.ts +5 -0
  160. package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
  161. package/templates/web/src/system/entry.ts +2 -1
  162. package/templates/web.config.ts +13 -10
  163. package/vendor/host-versions.json +3 -3
  164. package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
  165. package/templates/api/src/notifications/api.ts +0 -32
  166. package/templates/api/src/notifications/manifest.yaml +0 -5
  167. package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
  168. package/templates/web/src/notifications/__client/api.ts +0 -80
  169. /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
package/README.md CHANGED
@@ -27,7 +27,7 @@ ojm dev [port] # 启动开发服务器(默认 5174):宿主代理 + 本
27
27
  ojm build # 构建模块产物与 dist/modules.json
28
28
  ojm init [dir] [--yes] # 脚手架:模板工程 + 证书签发 + 联网下载 oj 到 bin/
29
29
  ojm vendor [tag] [--force] # 下载/重装 oj vendor(经 npm 包 @oj-bin/oj,tag 缺省取最新版本,形如 v0.1.13)
30
- ojm preview [port] [--oj-static] # 生产形态预览:oj migrate + release + 静态兜底
30
+ ojm preview [port] # 生产形态预览:oj migrate → oj serve 单服务(API + 静态 SPA 同进程)
31
31
  ojm info # 输出报障所需的版本矩阵与后端观测
32
32
  ojm api [dir] # 契约产物生成(client/openapi/mock stub)
33
33
  ojm merge <out> <in...> # 合并多份 modules.json
package/devkit/README.md CHANGED
@@ -1,31 +1,67 @@
1
1
  # oj-module DevKit——模块开发手册 + agent skill
2
2
 
3
3
  面向用 `ojm` 开发前后端一体工程的开发者与 AI agent。本目录是**源文件**,
4
- 发布时由 cli 的 `prepack`(`packages/cli/scripts/sync-devkit.mjs`)整目录同步进
5
- `packages/cli/devkit/`,随 `@oj-module/cli` 发布,再由 `ojm init` 分发到工程内。
4
+ 发布时由 cli 的 `prepack`(`packages/cli/scripts/sync-devkit.mjs`)把其中
5
+ **ojm 侧的 5 个文件**同步进 `packages/cli/devkit/`,随 `@oj-module/cli`
6
+ 发布,再由 `ojm init` 分发到工程内。
6
7
 
7
- > 两个 `devkit` 别混:**本目录**(ojm 模块开发)进 cli 包;`bin/devkit/` 是 **oj 自带**
8
- > devkit(`oj-api-dev` skill)在工程里的落点,由 `ojm vendor` / `ojm init` 装 oj 时一并落盘。
8
+ 整棵树覆盖前后端两侧,但仍是**两套独立分发的 skill**(同名不同物,别混):
9
9
 
10
- 它补的是 `oj-api-dev`(oj 后端 API 开发)之外的另一半:**前端模块、契约、
11
- 前后端对接**。
10
+ | 侧 | 本仓位置 | 用户工程里的落点 | 分发链 |
11
+ |---|---|---|---|
12
+ | ojm 模块开发(前端模块、契约、前后端对接) | 本目录顶层 5 个文件 | `.claude/skills/ojm-module-dev/` | `@oj-module/cli` 包 → `ojm init` 第 4b 步 |
13
+ | oj API 开发(后端 handler、全局对象、配置、测试) | `oj-api-dev/` 子目录(快照) | `.claude/skills/oj-api-dev/` | **oj 二进制自带**(`bin/devkit/`)→ `ojm init` 第 4 步 |
14
+
15
+ > **`oj-api-dev/` 子目录是快照,不是本仓维护的文档**:它与本机 oj 的
16
+ > `~/.local/share/oj/devkit/`(oj release 下载)逐字节一致,改它没有意义——
17
+ > 下次刷新即被覆盖。权威源在 oj 侧;它进本仓只为给框架贡献者一个
18
+ > 可 grep 的后端手册副本(后端细节以 oj 发布版为准)。
19
+ > 该子目录**不随 cli 包发货**(用户工程从 oj 二进制拿),`sync-devkit.mjs`
20
+ > 只同步顶层文件。
21
+
22
+ ## ojm 侧文件(随 cli 包发货)
23
+
24
+ | 文件 | 用途 |
25
+ |---|---|
26
+ | `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
27
+ | `manual.md` | 模块开发手册(12 章):工程结构(含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发、签名与信任 |
28
+ | `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
29
+ | `module-dev-and-cdn-playground.md` | 教学文档:开发态 ≠ 分发态、「怎么安装自己的模块 / 会不会重复」、本地 CDN 市场链路五步跑通(setup → publish → build-index → verify → 安装) |
30
+ | `sql-builder-missing-capabilities-pr.md` | 构造器能力缺口 PR 参考(**仅框架仓库**):7 项诉求的闭环记录 |
31
+
32
+ 后端 handler 本身的写法(`json` 信封、`db` 查询、`manifest.yaml`、`oj test`)、
33
+ 站点挂载(`mounts`)、密钥信封(`secrets`)、vars、cors、schema 校验、broker/tasks/oidc/ws 等
34
+ 属于 **`oj-api-dev` skill** 的范围——`ojm init` 已把它装到工程的
35
+ `.claude/skills/oj-api-dev/`(权威源随 oj 二进制分发,随 oj 版本更新,
36
+ 本仓副本见上表)。
12
37
 
13
38
  > **oj v0.1.58 起站点挂载迁移**:`server.api_prefix` / `app_path` / `app_prefix` /
14
39
  > `static_sites` / `app_spa_fallback` 这五个站点键已被删除,改由顶层 **`mounts:`** 扁平表
15
40
  > 承载(一行=一个 URL 前缀+一个目录,`api`/`web` 恰好其一)。旧键出现即被 `oj serve` fail-fast 拒绝。
16
41
  > `ojm init` 模板已是 `mounts:`;oj 侧挂载、密钥信封、vars、cors、schema 校验等完整用法见
17
- > `oj-api-dev` skill 手册(`ojm init` 装机在 `.claude/skills/oj-api-dev/`)。
42
+ > `oj-api-dev` skill 手册。
43
+
44
+ ## oj-api-dev 侧文件(快照,仅本仓参考)
18
45
 
19
46
  | 文件 | 用途 |
20
47
  |---|---|
21
- | `SKILL.md` | Claude Code 等 agent 的 skill 入口:工作流、红线、新模块 checklist、陷阱速查,按章节号引用手册 |
22
- | `manual.md` | 模块开发手册(12 章):工程结构(含 §1.7 handler 四红线——oj 运行时实测钉出,后端必读)、写模块、契约与代码生成(含 §3.10 旧 contract.ts 工程迁移指南)、provider 接管(覆盖框架内置功能)、布局、i18n、构建发布、排查、CLI 子命令参考(ojm 原生命令 vs oj 透传命令)、方案包打包与分发、签名与信任 |
23
- | `framework-dev.md` | 框架贡献者路线(**仅框架仓库**):改 `packages/runtime`、`packages/cli` 时的入口与红线 |
48
+ | `SKILL.md` | oj 侧 agent skill 入口(工作流、红线、checklist、陷阱速查) |
49
+ | `api-manual.md` | 后端开发手册(13 章):模块约定、全局对象 API、鉴权租户、测试、配置、构建发布 |
50
+ | `scenarios.md` | 场景速查(照抄就能跑) |
51
+ | `sql-builder-guide.md` | SQL 构造器专题指引(v0.1.82) |
52
+ | `oj-test-guide.md` | `oj test` L1 测试专题指引 |
53
+ | `global.d.ts` / `oj-modules.d.ts` | handler 全局对象 TS 类型 + `#` 导入别名 ambient 兜底 |
54
+ | `oidc-implementation.md` / `oidc-integration.md` | OIDC 演示走读与集成说明 |
55
+ | `README.md` | oj 侧发布索引 + **v0.1.66→v0.1.82 逐版落地记录**(查「某个能力哪版有的」看这里) |
56
+
57
+ > 注意 `oj-api-dev/README.md` 里「由仓库 `docs/devkit/` 经 `cargo xtask build`
58
+ > 归置到 `bin/devkit/`」说的是**上游 oj 仓**的结构,不是本仓——本仓的
59
+ > `docs/devkit/` 是 ojm 侧 devkit(你正在读的这个目录)。
24
60
 
25
61
  ## 安装(业务工程)
26
62
 
27
- **方式一(推荐):`ojm init` 自动分发。** `ojm init` 会把本 devkit 自动拷进工程的
28
- `.claude/skills/ojm-module-dev/`(与 oj 后端 devkit `oj-api-dev` 一并安装,
63
+ **方式一(推荐):`ojm init` 自动分发。** `ojm init` 把两侧 skill 都装进工程的
64
+ `.claude/skills/`(ojm 侧来自 cli 包;oj 侧来自 oj 二进制的 `bin/devkit/`,
29
65
  已存在则幂等跳过)。无需手动拷贝。
30
66
 
31
67
  **方式二:手动拷贝**(旧工程或不走 init 时):
@@ -35,33 +71,43 @@
35
71
  mkdir -p .claude/skills/ojm-module-dev
36
72
  cp SKILL.md manual.md README.md .claude/skills/ojm-module-dev/
37
73
 
38
- # 后端 API 开发另装 oj 自带的 devkit(独立 skill,不在此目录内):
74
+ # 后端 API 开发另装 oj 自带的 devkit(独立 skill):
39
75
  # 权威源随 oj 二进制分发——本机安装在 ~/.local/share/oj/devkit/,
40
76
  # 工程内在 bin/devkit/(ojm vendor / init 安装 oj 时一并落盘)
41
77
  mkdir -p .claude/skills/oj-api-dev
42
78
  cp ~/.local/share/oj/devkit/SKILL.md ~/.local/share/oj/devkit/api-manual.md .claude/skills/oj-api-dev/
43
79
  ```
44
80
 
45
- 安装后 Claude Code 里 `/ojm-module-dev` 触发,或直接说「用 ojm-module-dev 加个 xxx 模块」。
81
+ 安装后 Claude Code 里 `/ojm-module-dev` 或 `/oj-api-dev` 触发,
82
+ 或直接说「用 ojm-module-dev 加个 xxx 模块」。
46
83
 
47
84
  ## 与 `ojm init` 的关系
48
85
 
49
- `ojm init` 已接入本 devkit 分发(`packages/cli/src/init.ts` 第 4b 步):从 cli 包内置的
50
- `devkit/`(prepack 由 `docs/devkit/` 同步而来)拷 `SKILL.md` / `manual.md` / `README.md`
51
- 到 `.claude/skills/ojm-module-dev/`。发布链路细节见 `framework-dev.md` §7。
86
+ 两条分发链都接在 `packages/cli/src/init.ts`:
87
+
88
+ - **第 4 步**:从用户 `bin/devkit/`(oj release 下载)拷 `SKILL.md` /
89
+ `api-manual.md` + 有则拷的 `sql-builder-guide.md` / `oj-test-guide.md` +
90
+ `global.d.ts` → `.claude/skills/oj-api-dev/`;
91
+ - **第 4b 步**:从 cli 包内置 `devkit/`(prepack 由本目录顶层同步)拷
92
+ `SKILL.md` / `manual.md` / `README.md` → `.claude/skills/ojm-module-dev/`。
52
93
 
53
94
  ```mermaid
54
95
  flowchart LR
55
- SRC["docs/devkit/(源文件,只在此处维护)"] -->|"cli prepack:sync-devkit.mjs"| PKG["packages/cli/devkit/(发布副本,随 @oj-module/cli)"]
96
+ SRC["docs/devkit/ 顶层(ojm 侧源文件,只在此处维护)"] -->|"cli prepack:sync-devkit.mjs(只同步顶层,不含 oj-api-dev/)"| PKG["packages/cli/devkit/(发布副本,随 @oj-module/cli)"]
56
97
  PKG -->|"ojm init 第 4b 步(幂等跳过已存在)"| PROJ[".claude/skills/ojm-module-dev/"]
57
- OJBIN["bin/devkit/(oj 自带)"] -->|"ojm init 第 4 步"| API[".claude/skills/oj-api-dev/"]
98
+ OJBIN["bin/devkit/(oj 二进制自带)"] -->|"ojm init 第 4 步"| API[".claude/skills/oj-api-dev/"]
99
+ OJBIN -.->|"本仓快照刷新:整目录覆盖(保持逐字节一致)"| SNAP["docs/devkit/oj-api-dev/(仅本仓参考,不发货)"]
58
100
  ```
59
101
 
60
102
  ## 更新
61
103
 
62
- 手册与 skill 随 `@oj-module/cli` 版本一起发布;升级 cli 后重跑 `ojm init` 即可刷新
63
- (幂等跳过已存在的文件——若想强制覆盖旧拷贝,先删 `.claude/skills/ojm-module-dev/` 再 init)。
64
- 源文件只在仓库根 `docs/devkit/` 一处维护。
104
+ - **ojm 侧**:手册与 skill 随 `@oj-module/cli` 版本一起发布;升级 cli 后重跑
105
+ `ojm init` 即可刷新(幂等跳过已存在的文件——若想强制覆盖旧拷贝,先删
106
+ `.claude/skills/ojm-module-dev/` 再 init)。源文件只在仓库 `docs/devkit/` 顶层一处维护。
107
+ - **oj 侧**:手册与 skill 随 oj 版本发布;升级 oj 后用新 `bin/devkit/` 覆盖旧拷贝。
108
+ 框架贡献者要把本仓快照同步到新版:从 `~/.local/share/oj/devkit/` 整目录覆盖
109
+ `docs/devkit/oj-api-dev/`(10 个文件,保持逐字节一致以便对账)。
65
110
 
66
- > 本目录(devkit)是**自包含**的:所有内容都在这 4 个文件里,不需要工程仓库里的
67
- > 其它文档即可读懂。维护者视角的内容来源整理在 `framework-dev.md`(仅本仓)。
111
+ > 本目录顶层 5 个文件是**自包含**的:业务工程拿到即可读懂,不需要工程仓库里的
112
+ > 其它文档。`oj-api-dev/` 子目录是 oj 侧快照,同样自包含(其自身 README 即索引),
113
+ > 但不随 cli 包发货、仅本仓参考。维护者视角的内容来源整理在 `framework-dev.md`(仅本仓)。
@@ -15,12 +15,12 @@
15
15
  | 端到端可重复验证流程 | `docs/prd/framework-verification-playbook.md` |
16
16
  | 某特性的设计与评审记录 | `docs/prd/<日期>-<特性>-design.md` |
17
17
  | 各阶段实战记录(骨架→注入→认证→CRUD→release) | `apps/playground-oj/docs/phase*.md` |
18
- | 后端 handler 写法 | `bin/devkit/api-manual.md`(`oj-api-dev` skill) |
18
+ | 后端 handler 写法 | `docs/devkit/oj-api-dev/api-manual.md`(`oj-api-dev` skill 的本仓快照;用户工程侧在 `.claude/skills/oj-api-dev/`,随 oj 二进制分发) |
19
19
 
20
20
  > ⚠️ 那两份 guide 有**已知陈旧处**,以源码为准:
21
21
  > dev 端口实际是 **3333**(guide 写的 5173 已过时;注意与 `ojm dev` 的前端 devServer
22
22
  > **5174** 不是一回事——前者是本仓 vite,后者是业务工程 devServer);包版本以
23
- > `packages/*/package.json` 为准(guide 内部版本号不一致,实际 **0.2.5**)。
23
+ > `packages/*/package.json` 为准(撰写时实际 **0.2.8**,仅供参考,以当前 package.json 为准)。
24
24
 
25
25
  ## §2 两包结构与依赖方向
26
26
 
@@ -133,7 +133,7 @@ pnpm test tests/shell # 宿主全量
133
133
  **一键发布**:`node scripts/publish-packages.mjs`(lockstep 门禁 → 构建 → 守卫测试 →
134
134
  runtime → cli → registry 复核;`--dry-run` 只演练)。下面的手动步骤是它的展开参考。
135
135
 
136
- - **两包**同版本号(`@oj-module/runtime` + `@oj-module/cli`,当前 `0.2.5`),后续发版保持
136
+ - **两包**同版本号(`@oj-module/runtime` + `@oj-module/cli`,当前 `0.2.8`),后续发版保持
137
137
  lockstep;`@plane/kit` / `@plane/editor` 不在发布集内(脚本里已注释)。
138
138
  - **必须 `pnpm publish`**——用 `npm publish` 会漏出 `workspace:*` / `catalog:` 字面量。
139
139
  - **顺序:先 `runtime` 后 `cli`**。`runtime` 未公开时,新 `cli` 的精确依赖会悬空,
@@ -167,7 +167,13 @@ runtime → cli → registry 复核;`--dry-run` 只演练)。下面的手动
167
167
  `README.md`,已存在则幂等跳过)。
168
168
 
169
169
  `packages/cli/devkit/` 是发布副本、由 prepack 生成,已加进 `.gitignore` 不入库;
170
- 源仍是仓库根 `docs/devkit/`,改文档只动那里一处。
170
+ 源仍是仓库 `docs/devkit/` 顶层,改文档只动那里一处。
171
+
172
+ > `docs/devkit/oj-api-dev/` 是 **oj 侧 devkit 的快照**(10 个文件,与
173
+ > `~/.local/share/oj/devkit/` 逐字节一致),仅供本仓 grep 后端手册,
174
+ > **不随 cli 包发货**(`sync-devkit.mjs` 只同步顶层文件)——用户工程的
175
+ > `oj-api-dev` skill 由 oj 二进制分发。升级本机 oj 后要同步快照:
176
+ > 从 `~/.local/share/oj/devkit/` 整目录覆盖该子目录。
171
177
 
172
178
  > 历史记录:此处原为「待对接(未做)」,上述两步落地后即完成。
173
179
 
@@ -175,7 +181,10 @@ runtime → cli → registry 复核;`--dry-run` 只演练)。下面的手动
175
181
 
176
182
  ## 附:本 devkit 内容来源(维护者看)
177
183
 
178
- `docs/devkit/` 是**自包含**的交付文档,业务工程拿到这 4 个文件即可读懂,不依赖工程仓库里的其它文档。
184
+ `docs/devkit/` 是**自包含**的交付文档(顶层 5 个文件,含
185
+ `sql-builder-missing-capabilities-pr.md` 这份构造器能力缺口 PR 参考),业务工程拿到即可读懂,不依赖工程仓库里的其它文档。
186
+ 另有 `oj-api-dev/` 子目录是 oj 侧 devkit 的快照(10 文件,仅本仓参考、不发货),
187
+ 结构说明见 `README.md`。
179
188
  下面的来源仅用于维护时追溯与同步,不参与交付:
180
189
 
181
190
  | 源(本仓其它位置) | 贡献到 devkit 的内容 |
package/devkit/manual.md CHANGED
@@ -100,7 +100,9 @@ api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
100
100
  | `api/src/<模块>/manifest.yaml` | `tables: [表名]` | **与 `schema.yaml` 双向一致**(约定),只写一边启动即失败 |
101
101
 
102
102
  > 少了 migration → 表根本建不出来;少了 `schema.yaml` / `manifest.tables`
103
- > → 归属校验失败;两边列定义漂移 → reconcile 报差异(`oj schema diff` 可见)。
103
+ > → 启动归属校验失败,且任何经 `db.table("<表名>")` 访问该表的代码都会被 oj 的 DB 构造器以
104
+ > `unknown table` 拒绝(2026-10-19 演练实踩:仅由迁移建表、未声明的 `sys_rate_window` /
105
+ > `sys_login_throttle` 导致登录恒 500);两边列定义漂移 → reconcile 报差异(`oj schema diff` 可见)。
104
106
 
105
107
  ### 1.6 迁移与 seed 的应用时机
106
108
 
@@ -214,7 +216,7 @@ export default defineModule({
214
216
  path: "/books",
215
217
  handle: {
216
218
  layout: "container", // ⚠️ 布局级路由必填(见 §5.1)
217
- order: 5, // 菜单排序
219
+ order: 5, // 菜单排序(≥0 主区升序;<0 入侧边栏底部固定区)
218
220
  title: "books:menu.books", // 命名空间语法 "<模块名>:<key>"
219
221
  icon: createElement(BookOutlined),
220
222
  },
@@ -251,7 +253,7 @@ export default defineModule({
251
253
  | 字段 | 说明 |
252
254
  |---|---|
253
255
  | `layout` | `"container"`(完整壳)\| `"parent"`(嵌套父级)\| `"fullscreen"`(全屏)\| `"none"`(默认,裸渲染)\| 自定义名(§5) |
254
- | `order` | 菜单排序值 |
256
+ | `order` | 菜单排序值(双段序轴):`≥0` 主菜单区升序;`<0` 底部固定区(pin 在侧边栏底部,区内仍按 order 升序);未声明按登记下标兜底;同 order 按根 path 字母序 |
255
257
  | `title` | 菜单标题,用 `"<模块名>:menu.<key>"` |
256
258
  | `icon` | `createElement(XxxOutlined)` |
257
259
  | `keepAlive` | 页面缓存(**必须挂在 `layout: "container"` 下才生效**) |
@@ -297,7 +299,7 @@ export default defineModule({
297
299
  可服务的同源路径)。直接 `import "vditor/dist/..."` 其它深路径会命中前缀 external 但无
298
300
  importmap 键,构建期报错——需要新深路径请联系框架方在共享表登记。
299
301
 
300
- ### 2.7 本版本共享依赖钉版(0.2.5 快照)
302
+ ### 2.7 本版本共享依赖钉版(0.2.8 快照)
301
303
 
302
304
  宿主经 importmap 提供以下单例(模块 `devDependencies` 须对齐,供 `tsc` 用);完整 68 项矩阵以
303
305
  `pnpm exec ojm info` 或 `packages/cli/shell-dist/versions.json`(构建期生成)为准。
@@ -326,7 +328,7 @@ importmap 键,构建期报错——需要新深路径请联系框架方在共
326
328
  | `pinyin-pro` | 3.29.3 |
327
329
  | `vditor` | 4.0.0 |
328
330
 
329
- 框架包版本:`@oj-module/runtime` / `@oj-module/cli` 为 `0.2.5`(**双包 lockstep**);
331
+ 框架包版本:`@oj-module/runtime` / `@oj-module/cli` 为 `0.2.8`(**双包 lockstep**);
330
332
  `@plane/kit` / `@plane/editor` 是独立版本线(当前 `0.1.20`,随宿主 importmap 提供,
331
333
  不参与 lockstep)。`oj` 二进制最低版本门禁 `MIN_OJ_VERSION = 0.1.73`——契约流水线要求
332
334
  oj ≥ 0.1.73:平级 `.response` 出参声明位自 v0.1.72 起、`oj openapi` 的 `del`→`delete`
@@ -913,7 +915,8 @@ ctx.register.routesApi({
913
915
  ```ts
914
916
  export default {
915
917
  async get() {
916
- const rows = await db.query("SELECT path, title, icon FROM menus ORDER BY sort", []);
918
+ // 表须按 §1.5 三件套在 schema.yaml + manifest.yaml 声明;db.table() 构造器要求表已声明
919
+ const rows = await db.table("menus").select(["path", "title", "icon"]).orderBy([{ field: "sort" }]).all();
917
920
  json.ok(rows.map(r => ({
918
921
  path: String(r.path),
919
922
  handle: { layout: "container", title: String(r.title) },
@@ -1294,7 +1297,7 @@ pnpm dev # 开发:devServer + oj 后端(api/src 保存即热更
1294
1297
  # Ctrl+C(SIGINT)/ kill(SIGTERM)终止 dev 时,前端与后端 oj 一起终止:
1295
1298
  # 先关 SSE 通道 → 停 oj 子进程 → 打印「后端 oj 已随前端一起终止」,不留 oj 孤儿占端口
1296
1299
  pnpm build # = ojm build:oj build(生成 routes.js)+ 前端全站合并到 web/dist
1297
- pnpm preview # = ojm preview:oj migrate(verify 闸)→ 启动 serve + 静态后备
1300
+ pnpm preview # = ojm preview:oj migrate(verify 闸)→ oj serve 单服务(API + 静态 SPA 同进程)
1298
1301
  pnpm typecheck # = tsc --noEmit
1299
1302
  pnpm exec ojm info # 版本矩阵 + 模块清单(报障用)
1300
1303
  pnpm exec ojm vendor # 下载/重装 oj 二进制
@@ -1354,10 +1357,11 @@ export default async (path: string, html: string) => {
1354
1357
  顺序是 transform → dev 的 reload 脚本注入,`ojm dev` 照旧热更。
1355
1358
  - 返回 `string` 或 `{ html, cacheControl? }`;**未配置时输出与历史逐字节一致**,抛错/坏返回只
1356
1359
  打日志并回退原文(不会 500),非 HTML 资产完全不受影响。
1357
- - 配置的模块缺失或 default 不是函数 → `ojm dev`/`ojm preview` **启动即报错**(不留半个 oj 进程)。
1358
- - **`preview --oj-static` 例外**:该模式静态由 oj 二进制直出(`--app-path`),不经 ojm 静态层,
1359
- 本钩子不生效;生产要在 oj 托管侧做同样的事,用 oj 的 `server.html_meta`(构建期 JSON)或
1360
- `server.html_meta_handler`(动态 handler)——见 oj devkit 手册「静态站点与 per-route meta」。
1360
+ - 配置的模块缺失或 default 不是函数 → `ojm dev` **启动即报错**(不留半个 oj 进程)。
1361
+ - **单服务 preview 例外**:preview 静态由 oj 二进制直出(web 挂载),不经 ojm 静态层,本钩子
1362
+ 不生效(启动检测到配置即 WARN,不加载模块);逐路由 meta 用 oj 的 `server.html_meta`
1363
+ (构建期 JSON)或 `server.html_meta_handler`(动态 handler)——见 oj devkit 手册
1364
+ 「静态站点与 per-route meta」。
1361
1365
 
1362
1366
  > release 的 `server.migrate_on_start` 默认 `verify`:迁移账本落后会**拒绝启动**,
1363
1367
  > 先跑 `ojm migrate -d api/dist`(透传自动补 `-c api/config.yaml`)。
@@ -1398,7 +1402,7 @@ export default async (path: string, html: string) => {
1398
1402
  | 豁免写了 `//` 注释后全部失效 | 严格 JSON 解析失败、静默回退 | 注释写进 `_comment` 字段 |
1399
1403
  | 新建后端模块目录后 API 404 | 目录镜像路由未热更 | **重启** `ojm dev` |
1400
1404
  | DELETE 返回 405 | 方法名写成了 `delete` | 改成 `del` |
1401
- | IM 预览/爬虫只有默认标题(页面 JS 改了没用) | 服务端没做 HTML 注入 | 配 `htmlTransform`(见 §7;`preview --oj-static` 模式下要改到 oj 侧 `server.html_meta_handler`) |
1405
+ | IM 预览/爬虫只有默认标题(页面 JS 改了没用) | 服务端没做 HTML 注入 | 配 `htmlTransform`(见 §7;单服务 preview 要改到 oj 侧 `server.html_meta`/`server.html_meta_handler`) |
1402
1406
  | 配了 `htmlTransform` 但没生效 | 路径写错 / default 不是函数会**启动报错**;能起来却没效果 → 检查该请求是否命中静态层(`/api` 前缀与带扩展名的资源都不过钩子) | 见 §7 的生效范围 |
1403
1407
  | 上传报 401 | `headers` 写成固定对象,token 过期 | 写成方法,用时现取(§4.5) |
1404
1408
  | 后端菜单用了我的布局名,页面却没壳 | 注册该布局的模块没加载 | 看控制台 `[layout] 未知名布局` 提醒 |
@@ -1468,7 +1472,7 @@ export default async (path: string, html: string) => {
1468
1472
 
1469
1473
  > **为什么只有这 5 个走透传?** oj 还有 `serve` / `build` / `secret`(`keygen`/`seal` 等)
1470
1474
  > 子命令——但它们被 `ojm` **内部调用并附带额外逻辑**(如 `ojm build` 在 `oj build`
1471
- > 之后还要做前端全站合并;`ojm preview` 在 `oj migrate` 之后起 `oj serve` + 静态后备),
1475
+ > 之后还要做前端全站合并;`ojm preview` 在 `oj migrate` 之后起 `oj serve` 单服务),
1472
1476
  > 所以不单独透传给用户,避免绕开 ojm 的工程化处理。需要原汁原味跑 oj 其它子命令时,
1473
1477
  > 直接 `./bin/oj <sub> ...`(记得手动 `-c api/config.yaml`)。
1474
1478
 
@@ -1516,21 +1520,22 @@ export default async (path: string, html: string) => {
1516
1520
  > release 下只有 `routes.js` 里的路由存在,目录镜像路由不生效——所以参数路由要先
1517
1521
  > `ojm build` 再测(见 §7)。
1518
1522
 
1519
- #### `ojm preview [port] [--oj-static]`
1523
+ #### `ojm preview [port]`
1520
1524
 
1521
- 生产形态预览(设计 §6):
1525
+ 生产形态预览(设计 §6,2026-10-19 单服务化):
1522
1526
 
1523
1527
  1. 启动前 **fail-fast 四查**(缺 oj 二进制 / `config.yaml` / `web/dist/index.html` /
1524
1528
  `api/dist/manifests.yaml` 任一即报错并给补救指引,绝不留下 oj 孤儿);
1525
1529
  2. `oj migrate`(应用待执行迁移,非零退出即退、不起 server);
1526
- 3. `oj serve`(release/js,仅 API);
1527
- 4. ojm 静态层后备(SPA 回退,无 SSE 注入、不设 no-store)。
1528
-
1529
- - `port`:preview 端口(缺省 **4173**)。
1530
- - `--oj-static`:静态改由 oj `--app-path` 直出(真 exercise oj 静态层;history 深链接 404 为已知限制,
1531
- ojm 本口仍提供 SPA 后备 + `/api` 反代)。
1532
- - 支持 `web.config.ts` 的 `htmlTransform`(按路由注入 `<title>`/`og:*`,见 §7),模块缺失或
1533
- default 非函数会**启动即报错**。
1530
+ 3. 生成派生 config `api/.ojm-preview.yaml`(原 config 副本 + upsert `/` web 挂载 +
1531
+ `spa: true`;显式 `port` 时覆盖 `server.port`);
1532
+ 4. `oj serve`(release/js,**单进程同时出 API 与静态**,SPA 深链回落)。
1533
+
1534
+ - `port`:preview 端口(缺省用 config 的 `server.port`,默认 9778);显式传参经派生 config 覆盖。
1535
+ - ojm 不再自建 http server、不反代、不传 `--app-path`——与「oj serve + web 挂载」的生产公式同形。
1536
+ - 派生 config 为临时产物,退出三路清理(SIGINT / 进程退出兜底 / 编程式 stop),模板 .gitignore 已收录。
1537
+ - `web.config.ts` 的 `htmlTransform` 在单服务 preview **不生效**(启动 WARN 指向 oj
1538
+ `server.html_meta` / `server.html_meta_handler`)。
1534
1539
 
1535
1540
  #### `ojm api [dir] [--check] [--docs] [--exempt <path>]`
1536
1541
 
@@ -0,0 +1,197 @@
1
+ # 开发态模块与本地 CDN 演练场
2
+
3
+ > 一篇写给「第一次在本工程开发模块」的人的教学文档。读完之后你应该能回答三个问题:
4
+ > **① 开发中的模块要不要「安装」?② 要的话装到哪、会不会重复?③ 怎么在本地把市场链路完整跑一遍?**
5
+ > 适用版本:`@oj-module/cli` 0.2.12+(`ojm market` 子命令),运行期 `oj` ≥ 0.1.73。
6
+
7
+ ## 1. 先建立心智模型:开发态 ≠ 分发态
8
+
9
+ 整个体系里存在**两套互不相干**的模块生命周期,混在一起是绝大多数困惑的来源:
10
+
11
+ | | 开发态 | 分发态 |
12
+ |---|---|---|
13
+ | 模块在哪 | 工程内源码:`web/src/A` + `api/src/A` | 一个 `.tgz` 包(`ojm build --bundle` 产出) |
14
+ | 谁登记它 | `web.config.ts` 的 `modules` 列表 → `ojm build` 生成 `modules.json` | 安装器往目标站点 upsert |
15
+ | 跑起来靠 | `ojm dev` 直跑源码,改一行刷新即见 | 安装器十阶段(下载→验签→解压→迁移→注册→热更→冒烟) |
16
+ | 典型场景 | 你在写它 | 别人(或另一个站点)装它 |
17
+
18
+ 一句话:**开发态没有「安装」这个动作**。`ojm dev` 起的站点直接扫描 `api/src` 产生路由、读 `modules.json` 装配菜单——你写的模块 A 此刻「已经在跑」,只是还没打过包。
19
+
20
+ ## 2. 常见问题:我开发了模块 A,怎么「安装」我自己?
21
+
22
+ ### 2.1 正确答案:装到别处,不是装进你正在开发的工程
23
+
24
+ 「安装自己」听起来矛盾,是因为安装这个动作的语义是**把一个包的分发产物并入一个宿主站点**。你正在开发的工程就是 A 的家,再装一遍相当于把家具搬进它已经在的屋子。
25
+
26
+ 真正想验证「A 这个包能不能装、安装流程对不对」时,有三个去处,按推荐顺序:
27
+
28
+ 1. **一次性隔离站点**:`ojm market trial --fixture <name>@<version>`(随机端口 + 一次性 DB + 一次性密钥 + 隔离信任库/CDN),跑完即毁,不碰你的工程、DB、信任库。这是唯一「装了也不心疼」的地方,也是最快的「这包能不能装」判据(实测 11 段全 ok 约 12 秒)。
29
+ 2. **另一个工程**:`ojm install dist/A-0.1.0.tgz`(file 源,multipart 直传)。
30
+ 3. **市场链路**:`ojm market publish` 落 CDN → 市场页点安装(§4 全套)。
31
+
32
+ ### 2.2 偏要在本工程试怎么办
33
+
34
+ 先 `ojm uninstall A` 再装,且升级走安装器的重装路径、不要手工覆盖同版本文件。理由见 §3.3 的「遮蔽」。
35
+
36
+ ## 3. 常见问题:装完之后路由、菜单会重复吗?
37
+
38
+ **不会重复——注册面全是 upsert(按键覆盖),不是 insert(追加)。** 但有两个坑,分开说。
39
+
40
+ ### 3.1 upsert 语义:同键只有一份
41
+
42
+ | 面 | 键 | 重复安装时发生什么 |
43
+ |---|---|---|
44
+ | 路由 `sys_solution_route` | `path` | 同 path 覆写元数据、下架标记复位 |
45
+ | 角色 / 权限 | 模块 + code | 按键 upsert;**本次未声明**的条目标记下架(行保留、不删) |
46
+ | 系统设置 `sys_setting` | `key` | 按 key upsert,但**管理员改过的值永不复活** |
47
+ | `modules.json` | 模块名 | 按模块名登记,唯一条目 |
48
+
49
+ 所以「菜单出现两条 A」这种事不会发生——菜单条目按模块名唯一。
50
+
51
+ ### 3.2 但同名再装会被直接拒
52
+
53
+ 安装前置校验有**方案名唯一**约束:同名方案已装 → 拒装并报占用者版本(`方案名已被已装方案「A@x.y.z」占用`)。只有升级 / 重装路径放行。这是第一道防线:从入口就不给你制造重复的机会。
54
+
55
+ ### 3.3 真正的坑:两个来源抢同一个 name/path(遮蔽)
56
+
57
+ 注意 §3.1 的 upsert 是**按 name/path** 而不是按来源。于是存在这种状态:
58
+
59
+ > 你源码里的 A(`api/src/A`,路由由 oj 扫描源码产生)和某次安装产物 `dist/A-0.1.0/` 同时存在。
60
+
61
+ 表现是:路由与模块条目各自只有一份(不重复),但**哪一份生效取决于挂载命中顺序**——症状为「我改了源码却不生效」或「行为怎么都不对」。这是本主题下最难查的一类问题:没有报错、没有重复,只是「被另一个自己顶替了」。
62
+
63
+ **规避方式**:别让两个来源同时在场。日常开发不装;试装去隔离站点;本工程试过之后 `ojm uninstall A` 清干净再回到源码直跑。
64
+
65
+ ## 4. 本地 CDN 演练场:把市场链路完整跑一遍
66
+
67
+ 模板自带一个**本地模拟 CDN**(工程根 `cdn/` + `api/config.yaml` 的 `/cdn` 挂载),配上 `ojm market` 四条命令,就能在本地复刻「发包 → 上架 → 索引 → 安装」全链路。不需要外部服务。
68
+
69
+ ### 4.1 全流程(五步)
70
+
71
+ ```bash
72
+ # 0) 起站点(dev 或 serve 均可;市场地址必须指向 oj 子进程的端口,见 §5.1)
73
+ ojm dev
74
+
75
+ # 1) 打开市场地址:把 framework.market_cdn_url 指向 http://127.0.0.1:9778/cdn
76
+ # 站点管理员首次登录处于改密悬挂期时会自动改密换全权 token
77
+ ojm market setup --user admin --pass 123456 --new-pass <你的口令>
78
+
79
+ # 2) 发包:合成 fixture(或 --real 走 ojm build --bundle)→ 确定性 tgz → 包级签名
80
+ # --rebuild-index 顺带重建索引
81
+ ojm market publish --fixture demo-solution@0.1.0 --id <发布方id> --rebuild-index
82
+
83
+ # 3) 离线验包:不起站点、不装、不要凭据
84
+ ojm market verify --rebuild-index
85
+
86
+ # 4) 浏览器打开市场页 → 看到卡片 → 点安装
87
+ # 首次遇陌生发布方会停在「待信任」→ ojm trust confirm --task <id> → 重试即成
88
+ ```
89
+
90
+ 每条命令的边界:
91
+
92
+ | 命令 | 做什么 | **不**做什么 |
93
+ |---|---|---|
94
+ | `market setup` | 写系统设置 `framework.market_cdn_url` | 不改写 `api/config.yaml`(挂载由模板自带) |
95
+ | `market publish` | 落 `<name>-<version>.tgz` + 包级 `.tgz.sig` | **不碰索引**(索引是共享单文件,多人发布会互相覆盖) |
96
+ | `market build-index` | 扫 `cdn/*.tgz` 提取包内 manifest,全量重建两份索引并签名 | 不落包 |
97
+ | `market verify` | 完整性 / 包级签 / 索引签 / manifest 结构四类校验 + 漂移检出 | 不起站点、不装、不访问任何远端 |
98
+
99
+ ### 4.2 索引为什么是「扫描重建」而不是「增量合并」
100
+
101
+ 因为索引签名对不上多人协作:如果 publish 时读-改-写共享索引,后发者会把前者的签名覆盖掉,且存在竞态。而索引签名在运行期只是**完整性层**(防目录被改),不是信任锚——那就干脆把它降级成派生产物:**谁都能重建,重建结果只取决于目录里有什么**。这也是 `publish` 与 `build-index` 必须分成两个命令的原因。
102
+
103
+ ### 4.3 信任模型(30 秒版)
104
+
105
+ - **包级签名**(`<tgz>.sig`,Ed25519):信任锚。安装时逐字节验,不过即拒(fail-closed)。
106
+ - **索引签名**:完整性层。索引被改能发现,但**不背书内容安全**。
107
+ - **TOFU**:首次遇到未登记的发布方 key,安装停在待信任状态,管理员确认后写入信任库;首次绑定不可改。
108
+ - `market verify` 的结论恒带一句边界声明:**校验通过只说明来源与完整性,不构成内容安全性背书**——这不是免责套话,是信任模型的准确描述。
109
+
110
+ ## 5. 两个已经踩过的坑(别再踩)
111
+
112
+ ### 5.1 dev 形态下,`/cdn` 不在 dev server 上
113
+
114
+ `ojm dev` 的 Node 服务在 **5174**,只把 `/api` 反代给 oj 子进程;`/cdn` 是 **oj 的挂载**,只在 oj 子进程(`config.yaml` 的 `server.port`,默认 9778)上成立。所以市场地址**必须写绝对 URL** 且端口取自 config——写成相对 `/cdn` 的话,dev 下取到的是 index.html,报出来是难懂的 YAML 解析错,不是干净 404。`market setup` 已按 config 推导,别手改。
115
+
116
+ ### 5.2 CDN 里摆了 `.tgz.sig` 侧车,不等于能装
117
+
118
+ 安装器的验签段只认 `manifests.yaml` 里的 `archive.signature`。侧车文件在目录里摆着**不够**,索引不带过去就是 `sig_missing` 硬拒。`market build-index` 已负责把侧车读进索引——所以规范动作永远是 **publish → build-index**,两个都跑。
119
+
120
+ > 附注:曾有一条「包内 `api/<mod>/manifest.yaml` 的 desc 必须逐字等于方案 summary」的校验,已于 2026-10-21 移除——那是构建期映射被当成安装期契约,而 desc 只是模块描述文本、非安全边界。现存相关校验只有:`summary` 必填、`desc` 必填非空。
121
+
122
+ ## 6. 实战:发一个真方案,并躲开七个坑
123
+
124
+ 上面的 fixture 路径能秒出可装包,但真实交付要么写方案工程、要么至少知道真构建长什么样。下面是
125
+ 2026-10-21 一次**完整演练**(宿主 `ojm init` → 方案工程 → 打包签名 → 发布 → 安装 → TOFU → 装成)
126
+ 沉淀下来的路径与坑。演练两轮全绿(换全新宿主零手工补丁复跑同样通过)。
127
+
128
+ ### 6.1 方案工程最小形态
129
+
130
+ ```text
131
+ solution-demo/
132
+ ├── manifest.yaml # 方案契约清单(Form A)
133
+ ├── api/src/<name>/
134
+ │ ├── manifest.yaml # oj 原生清单(name/desc/version/tables)
135
+ │ ├── schema.yaml # 表结构真源(必须与 manifest.tables 对齐)
136
+ │ ├── routes.js
137
+ │ ├── migrations/0001__init.sql
138
+ │ └── ping/api.ts # handler 必须是 .ts
139
+ └── web/src/<name>/locales/{zh-CN,en-US}.json # 引用 i18n key 时必需
140
+ ```
141
+
142
+ ```bash
143
+ <host>/bin/oj build -d api/src -o api/dist # 后端构建(只用宿主已装的 oj)
144
+ node "$CLI_PKG/bin/ojm.mjs" build --bundle # 打包(构建即校验)
145
+ node "$CLI_PKG/bin/ojm.mjs" keygen --id demo.publisher --out ./keys --yes
146
+ node "$CLI_PKG/bin/ojm.mjs" sign dist/<name>-<version>.tgz --id demo.publisher --key ./keys/private.pem --check
147
+ ```
148
+
149
+ ### 6.2 发布与安装(release 形态)
150
+
151
+ ```bash
152
+ cd <host>
153
+ node "$CLI_PKG/bin/ojm.mjs" market publish --real ../solution-demo --id demo.publisher --key ../keys/private.pem --rebuild-index
154
+ node "$CLI_PKG/bin/ojm.mjs" market verify --rebuild-index
155
+
156
+ node "$CLI_PKG/bin/ojm.mjs" build # 生成 api/dist + web/dist(含 api/dist/versions.json)
157
+ # config:platform.root 改成 dist(安装目标恒为 release 部署)
158
+ (cd api && ../bin/oj serve -c config.yaml -b /api --api-path dist --console-log &)
159
+ node "$CLI_PKG/bin/ojm.mjs" market setup --url http://127.0.0.1:9778 --user admin --pass 123456 --new-pass <新口令>
160
+ # 市场页点安装 → 首次停在「待信任」→ 核对指纹 → ojm trust confirm --task <id> → 重试 → done
161
+ ```
162
+
163
+ ### 6.3 七个坑(按撞见顺序)
164
+
165
+ | # | 现象 | 根因 / 正确做法 |
166
+ |---|---|---|
167
+ | ① | `oj build` 报 `tables 声明 "x" 不在 schema.yaml` | oj 用 `schema.yaml` 做表结构真源,`manifest.tables` 只是引用面;补 `schema.yaml`(`tables: {<t>: {pk, columns}}`),与迁移语句对齐 |
168
+ | ② | `oj build` 成功但 `routes.js` 是空表、dist 里没有 handler | **只编译 `.ts` handler**;写成 `ping/api.js` 会被静默丢弃 → 改 `api.ts` |
169
+ | ③ | `build --bundle` 报 `缺少 web/src/<name>/locales/…` | manifest 一旦引用 `title_key`/`label_key` 就必须双语 locale(后端方案也要);且 key 必须**嵌套** `{"menu":{"main":"…"}}`,平铺 `"menu.main"` 查不到 |
170
+ | ④ | 安装 POST 返回 200,但 `market/task` 恒 404 | 宿主 config 缺 `fs:` 段 → 安装器落盘被 jail 拒、失败发生在任务文件写入前;0.2.13 起模板自带,旧工程手补 |
171
+ | ⑤ | 安装卡 `preflight`:`无法读取宿主 runtime 版本` | 安装器读 `api/dist/versions.json`;0.2.13 起 `ojm build` 双落点(旧版手抄 `web/dist/versions.json`) |
172
+ | ⑥ | 安装卡 `reloading`:`NotCapable: module '_platform' is not platform-privileged` | 宿主 config 的 `platform.modules` 未列 `_platform`(0.2.13 起模板自带) |
173
+ | ⑦ | 安装卡 `reloading`:`reloadConfig 返回 unchanged` / 卡 `smoke`:探活 404 | 起站形态不对:`ojm preview` 服务派生 config(安装器热更写的是 `api/config.yaml`,被监视文件没变);dev(`--api-path src`)装到 dist 的模块不被发现。**安装必须对真实 config 以 `--api-path dist` 起站** |
174
+
175
+ ### 6.4 装成之后怎么确认
176
+
177
+ ```bash
178
+ curl -s http://127.0.0.1:9778/api/<name>/ping # {"code":0,...pong...}
179
+ cat api/dist/manifests.yaml | grep <name> # <name>: <version>
180
+ ls api/dist/<name>-<version>/ # 安装产物目录
181
+ ```
182
+
183
+ ## 7. 一页速查
184
+
185
+ ```text
186
+ 写模块 → 源码直跑(ojm dev),不要装
187
+ 验证能装 → ojm market trial(隔离站点,12s),或 ojm install <tgz> 到另一个工程
188
+ 重复? → 不会。注册面全 upsert;同名再装被前置校验直接拒
189
+ 市场链路 → setup → publish → build-index → verify → 市场页安装(release 形态:--api-path dist)
190
+ 绝对 URL → 市场地址端口取自 config(9778),不是 dev server 的 5174
191
+ 信任 → 包级签=锚,索引签=完整性,TOFU 管陌生发布方,verify 不背书内容安全
192
+ ```
193
+
194
+ ## 8. 延伸阅读
195
+
196
+ - 方案打包与分发全貌、签名与信任:本 skill 的 `manual.md` §11–§12
197
+ - 后端 handler 写法、站点挂载(`mounts`)等:`oj-api-dev` skill 手册