@oj-module/cli 0.2.18 → 0.2.23

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 (345) hide show
  1. package/devkit/README.md +4 -4
  2. package/devkit/SKILL.md +41 -37
  3. package/devkit/framework-dev.md +6 -5
  4. package/devkit/manual.md +352 -168
  5. package/devkit/menu-rules.md +3 -3
  6. package/devkit/module-dev-and-cdn-playground.md +31 -22
  7. package/devkit/sql-builder-missing-capabilities-pr.md +11 -6
  8. package/package.json +2 -2
  9. package/shell-dist/assets/runtime.js +59 -34
  10. package/shell-dist/index.html +2 -2
  11. package/shell-dist/versions.json +3 -3
  12. package/src/__tests__/build-backend.test.ts +54 -0
  13. package/src/__tests__/init-packagejson.test.ts +29 -0
  14. package/src/__tests__/layout.test.ts +176 -0
  15. package/src/__tests__/module-list.test.ts +66 -0
  16. package/src/__tests__/oj-caps.test.ts +114 -0
  17. package/src/alias-paths.ts +102 -0
  18. package/src/args.ts +54 -4
  19. package/src/build.ts +41 -9
  20. package/src/builtin.ts +93 -0
  21. package/src/bundle.ts +254 -38
  22. package/src/config.ts +82 -9
  23. package/src/contract/__tests__/market-client.integration.test.ts +14 -14
  24. package/src/contract/check.ts +56 -28
  25. package/src/contract/emit-stub.ts +21 -9
  26. package/src/contract/handler-scan.ts +19 -10
  27. package/src/contract/run.ts +14 -4
  28. package/src/dev.ts +80 -9
  29. package/src/index.ts +27 -7
  30. package/src/info.ts +69 -10
  31. package/src/init.ts +114 -28
  32. package/src/layout.ts +282 -24
  33. package/src/lifecycle/__tests__/args.test.ts +20 -0
  34. package/src/lifecycle/__tests__/builtin.test.ts +97 -0
  35. package/src/lifecycle/__tests__/bundle.test.ts +184 -6
  36. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  37. package/src/lifecycle/__tests__/new.test.ts +129 -0
  38. package/src/lifecycle/__tests__/smoke.test.ts +2 -1
  39. package/src/manifest-contract.ts +16 -3
  40. package/src/market/__tests__/bootstrap.test.ts +17 -9
  41. package/src/market/bootstrap.ts +30 -17
  42. package/src/market/publish.ts +10 -2
  43. package/src/new.ts +208 -0
  44. package/src/oj-caps.ts +133 -0
  45. package/src/oj.ts +27 -4
  46. package/src/preview-config.ts +1 -1
  47. package/src/preview.ts +2 -1
  48. package/src/site.ts +2 -2
  49. package/src/usage.ts +24 -12
  50. package/src/vendor.ts +5 -1
  51. package/templates/{api/config.yaml → config.yaml} +13 -8
  52. package/templates/global.d.ts +7 -6
  53. package/templates/init.include.yaml +41 -36
  54. package/templates/package.json +3 -1
  55. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/installer.ts +4 -0
  56. package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/smoke.ts +21 -2
  57. package/templates/{api/src/_platform → src/_platform/api}/fixtures/l1-test-user.sql +4 -5
  58. package/templates/{api/src/_platform → src/_platform/api}/market/api.ts +25 -15
  59. package/templates/src/_platform/api/seed.mysql.sql +16 -0
  60. package/templates/src/_platform/api/seed.pg.sql +16 -0
  61. package/templates/{api/src/_platform → src/_platform/api}/seed.sql +16 -5
  62. package/templates/{api/src/_platform → src/_platform/api}/solutions/api.ts +7 -3
  63. package/templates/{api/src/auth → src/auth/api}/_shared/login-throttle.ts +2 -2
  64. package/templates/{api/src/auth → src/auth/api}/_shared/session.ts +2 -2
  65. package/templates/{api/src/auth → src/auth/api}/api.ts +1 -1
  66. package/templates/{api/src/auth → src/auth/api}/ldap-login/api.ts +1 -1
  67. package/templates/src/auth/api/manifest.yaml +6 -0
  68. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/InstallDialog.test.tsx +53 -1
  69. package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/marketClient.fake.ts +6 -0
  70. package/templates/{web/src/oj-market → src/oj-market/web}/api/client.ts +5 -0
  71. package/templates/{web/src/oj-market → src/oj-market/web}/api/context.ts +2 -2
  72. package/templates/{web/src/oj-market → src/oj-market/web}/api/defaultClient.ts +7 -1
  73. package/templates/src/oj-market/web/components/InstallDialog.tsx +194 -0
  74. package/templates/{web/src/oj-market → src/oj-market/web}/components/RollbackDialog.tsx +7 -3
  75. package/templates/{web/src/oj-market → src/oj-market/web}/components/UninstallConfirm.tsx +18 -7
  76. package/templates/{web/src/oj-market → src/oj-market/web}/hooks/useMarket.ts +29 -12
  77. package/templates/{web/src/oj-market → src/oj-market/web}/locales/en-US.json +10 -1
  78. package/templates/{web/src/oj-market → src/oj-market/web}/locales/zh-CN.json +10 -1
  79. package/templates/{web/src/oj-market → src/oj-market/web}/types.ts +15 -2
  80. package/templates/{api/src/system → src/system/api}/login-methods/api.ts +2 -2
  81. package/templates/src/system/api/manifest.yaml +6 -0
  82. package/templates/{api/src/system → src/system/api}/oplog/api.ts +4 -4
  83. package/templates/{api/src/system → src/system/api}/permissions/api.ts +3 -3
  84. package/templates/{api/src/system → src/system/api}/roles/api.ts +3 -3
  85. package/templates/{api/src/system → src/system/api}/roles/bindings/api.ts +3 -3
  86. package/templates/{api/src/system → src/system/api}/settings/api.ts +3 -3
  87. package/templates/{api/src/system → src/system/api}/users/api.ts +3 -3
  88. package/templates/{api/src/system → src/system/api}/users/forbidden/api.ts +3 -3
  89. package/templates/{api/src/system → src/system/api}/users/password/api.ts +3 -3
  90. package/templates/{api/src/system → src/system/api}/users/roles/api.ts +3 -3
  91. package/templates/{web/src/system → src/system/web}/locales/en-US.json +1 -1
  92. package/templates/{web/src/system → src/system/web}/locales/zh-CN.json +1 -1
  93. package/templates/src/tsconfig.json +16 -0
  94. package/templates/{api/tests → tests}/auth-ldap.spec.ts +2 -2
  95. package/templates/{api/tests → tests}/auth-session.spec.ts +3 -3
  96. package/templates/{api/tests → tests}/auth-throttle.spec.ts +1 -1
  97. package/templates/tests/fixture.ts +434 -0
  98. package/templates/{api/tests → tests}/guard.spec.ts +1 -1
  99. package/templates/{api/tests → tests}/l1-builder.test.ts +2 -2
  100. package/templates/{api/tests → tests}/lifecycle/atomic-state.spec.ts +3 -3
  101. package/templates/{api/tests → tests}/lifecycle/bootstrap.spec.ts +8 -4
  102. package/templates/{api/tests → tests}/lifecycle/deploy-smoke.spec.ts +31 -8
  103. package/templates/{api/tests → tests}/lifecycle/installer.spec.ts +15 -15
  104. package/templates/{api/tests → tests}/lifecycle/market-api.spec.ts +30 -29
  105. package/templates/{api/tests → tests}/lifecycle/migrate.spec.ts +6 -6
  106. package/templates/{api/tests → tests}/lifecycle/preflight.spec.ts +4 -4
  107. package/templates/{api/tests → tests}/lifecycle/register.spec.ts +1 -1
  108. package/templates/{api/tests → tests}/lifecycle/semver.spec.ts +1 -1
  109. package/templates/{api/tests → tests}/lifecycle/sha384.spec.ts +1 -1
  110. package/templates/{api/tests → tests}/lifecycle/sig-fixtures.spec.ts +1 -1
  111. package/templates/{api/tests → tests}/lifecycle/solutions-api.spec.ts +1 -1
  112. package/templates/{api/tests → tests}/lifecycle/steps-fs.spec.ts +12 -12
  113. package/templates/{api/tests → tests}/lifecycle/text-edit.spec.ts +1 -1
  114. package/templates/{api/tests → tests}/lifecycle/tgz-guard.spec.ts +1 -1
  115. package/templates/{api/tests → tests}/lifecycle/trust.spec.ts +5 -5
  116. package/templates/{api/tests → tests}/lifecycle/uninstall.spec.ts +7 -7
  117. package/templates/{api/tests → tests}/lifecycle/verify-checks.spec.ts +9 -6
  118. package/templates/{api/tests → tests}/lifecycle/verify-report.spec.ts +7 -5
  119. package/templates/{api/tests → tests}/query-utils.spec.ts +1 -1
  120. package/templates/{api/tests → tests}/rbac-audit.spec.ts +1 -1
  121. package/templates/{api/tests → tests}/rbac-contacts.spec.ts +2 -2
  122. package/templates/{api/tests → tests}/rbac-guard.spec.ts +1 -1
  123. package/templates/{api/tests → tests}/rbac-roles.spec.ts +2 -2
  124. package/templates/{api/tests → tests}/rbac-seed.spec.ts +13 -11
  125. package/templates/{api/tests → tests}/rbac-settings.spec.ts +2 -2
  126. package/templates/{api/tests → tests}/rbac-solution-perms.spec.ts +1 -1
  127. package/templates/{api/tests → tests}/rbac-users.spec.ts +3 -3
  128. package/templates/tests/seed-role-accounts.spec.ts +87 -0
  129. package/templates/tsconfig.json +5 -1
  130. package/templates/tsconfig.paths.json +19 -0
  131. package/templates/types/oj-modules.d.ts +17 -0
  132. package/templates/vitest.config.ts +22 -0
  133. package/templates/web.config.ts +23 -13
  134. package/vendor/host-versions.json +3 -3
  135. package/templates/api/src/_platform/seed.mysql.sql +0 -8
  136. package/templates/api/src/_platform/seed.pg.sql +0 -8
  137. package/templates/api/src/auth/manifest.yaml +0 -5
  138. package/templates/api/src/system/manifest.yaml +0 -5
  139. package/templates/api/tests/fixture.ts +0 -58
  140. package/templates/api/tsconfig.json +0 -8
  141. package/templates/api/vitest.config.ts +0 -12
  142. package/templates/logs/task-events.jsonl +0 -0
  143. package/templates/web/src/oj-market/components/InstallDialog.tsx +0 -112
  144. /package/templates/{api/.ojm-api-exempt.json → .ojm-api-exempt.json} +0 -0
  145. /package/templates/{api/seed.mysql.sql → seed.mysql.sql} +0 -0
  146. /package/templates/{api/seed.pg.sql → seed.pg.sql} +0 -0
  147. /package/templates/{api/src/_platform → src/_platform/api}/README.md +0 -0
  148. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/atomic.ts +0 -0
  149. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/history.ts +0 -0
  150. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/lock.ts +0 -0
  151. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/semver.ts +0 -0
  152. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/sha384.ts +0 -0
  153. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/state.ts +0 -0
  154. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/deploy-web.ts +0 -0
  155. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/download.ts +0 -0
  156. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/extract.ts +0 -0
  157. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/migrate.ts +0 -0
  158. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/preflight.ts +0 -0
  159. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/register.ts +0 -0
  160. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/reload.ts +0 -0
  161. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/steps/verify.ts +0 -0
  162. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tasks.ts +0 -0
  163. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/text-edit.ts +0 -0
  164. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/tgz-guard.ts +0 -0
  165. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/trust.ts +0 -0
  166. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/types.ts +0 -0
  167. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/uninstall.ts +0 -0
  168. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-checks.ts +0 -0
  169. /package/templates/{api/src/_platform → src/_platform/api}/_lifecycle/verify-report.ts +0 -0
  170. /package/templates/{api/src/_platform → src/_platform/api}/_shared/respond.ts +0 -0
  171. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/contacts.ts +0 -0
  172. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/roles.ts +0 -0
  173. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/settings.ts +0 -0
  174. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/solution-perms.ts +0 -0
  175. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/domain/users.ts +0 -0
  176. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/access.ts +0 -0
  177. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/audit.ts +0 -0
  178. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/guard.ts +0 -0
  179. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/oplog.ts +0 -0
  180. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/query.ts +0 -0
  181. /package/templates/{api/src/_platform → src/_platform/api}/_shared/service/infra/rate.ts +0 -0
  182. /package/templates/{api/src/_platform → src/_platform/api}/_shared/site-facade.ts +0 -0
  183. /package/templates/{api/src/_platform → src/_platform/api}/contacts/api.ts +0 -0
  184. /package/templates/{api/src/_platform → src/_platform/api}/contacts/detail/api.ts +0 -0
  185. /package/templates/{api/src/_platform → src/_platform/api}/manifest.yaml +0 -0
  186. /package/templates/{api/src/_platform → src/_platform/api}/market/bootstrap.ts +0 -0
  187. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.mysql.sql +0 -0
  188. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.pg.sql +0 -0
  189. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0001__create_users.sql +0 -0
  190. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  191. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0002__add_users_avatar_base64.sql +0 -0
  192. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  193. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0003__users_source_display_name.sql +0 -0
  194. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.mysql.sql +0 -0
  195. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.pg.sql +0 -0
  196. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0004__sys_rbac.sql +0 -0
  197. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.mysql.sql +0 -0
  198. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0005__users_must_change.sql +0 -0
  199. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  200. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0006__users_is_forbidden.sql +0 -0
  201. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0007__users_email.sql +0 -0
  202. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  203. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  204. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0008__sys_refresh_token.sql +0 -0
  205. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.mysql.sql +0 -0
  206. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.pg.sql +0 -0
  207. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0009__sys_throttle.sql +0 -0
  208. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  209. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.pg.sql +0 -0
  210. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0010__sys_solution_route.sql +0 -0
  211. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  212. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0011__sys_setting_group_label.sql +0 -0
  213. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.mysql.sql +0 -0
  214. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.pg.sql +0 -0
  215. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0012__market_history.sql +0 -0
  216. /package/templates/{api/src/_platform → src/_platform/api}/migrations/0013__users_profile.sql +0 -0
  217. /package/templates/{api/src/_platform → src/_platform/api}/schema.yaml +0 -0
  218. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.schemas.ts +0 -0
  219. /package/templates/{web/src/_platform → src/_platform/web}/__client/api.ts +0 -0
  220. /package/templates/{web/src/about → src/about/web}/entry.ts +0 -0
  221. /package/templates/{web/src/about → src/about/web}/locales/en-US.json +0 -0
  222. /package/templates/{web/src/about → src/about/web}/locales/zh-CN.json +0 -0
  223. /package/templates/{web/src/about → src/about/web}/pages/constants.ts +0 -0
  224. /package/templates/{web/src/about → src/about/web}/pages/index.tsx +0 -0
  225. /package/templates/{web/src/access → src/access/web}/entry.ts +0 -0
  226. /package/templates/{web/src/access → src/access/web}/locales/en-US.json +0 -0
  227. /package/templates/{web/src/access → src/access/web}/locales/zh-CN.json +0 -0
  228. /package/templates/{web/src/access → src/access/web}/pages/access-mode/index.tsx +0 -0
  229. /package/templates/{web/src/access → src/access/web}/pages/admin-visible/index.tsx +0 -0
  230. /package/templates/{web/src/access → src/access/web}/pages/button-control/index.tsx +0 -0
  231. /package/templates/{web/src/access → src/access/web}/pages/common-visible/index.tsx +0 -0
  232. /package/templates/{web/src/access → src/access/web}/pages/page-control/index.tsx +0 -0
  233. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap-login.ts +0 -0
  234. /package/templates/{api/src/auth → src/auth/api}/_shared/ldap.ts +0 -0
  235. /package/templates/{web/src/auth → src/auth/web}/__client/api.schemas.ts +0 -0
  236. /package/templates/{web/src/auth → src/auth/web}/__client/api.ts +0 -0
  237. /package/templates/{api/src/demo → src/demo/api}/manifest.yaml +0 -0
  238. /package/templates/{api/src/demo → src/demo/api}/migrations/0001__create_todos.sql +0 -0
  239. /package/templates/{api/src/demo → src/demo/api}/schema.yaml +0 -0
  240. /package/templates/{api/src/demo → src/demo/api}/seed.sql +0 -0
  241. /package/templates/{api/src/demo → src/demo/api}/todos/api.ts +0 -0
  242. /package/templates/{web/src/demo → src/demo/web}/__client/api.schemas.ts +0 -0
  243. /package/templates/{web/src/demo → src/demo/web}/__client/api.ts +0 -0
  244. /package/templates/{web/src/demo → src/demo/web}/entry.ts +0 -0
  245. /package/templates/{web/src/demo → src/demo/web}/locales/en-US.json +0 -0
  246. /package/templates/{web/src/demo → src/demo/web}/locales/zh-CN.json +0 -0
  247. /package/templates/{web/src/demo → src/demo/web}/pages/index.tsx +0 -0
  248. /package/templates/{web/src/exception → src/exception/web}/entry.ts +0 -0
  249. /package/templates/{web/src/exception → src/exception/web}/locales/en-US.json +0 -0
  250. /package/templates/{web/src/exception → src/exception/web}/locales/zh-CN.json +0 -0
  251. /package/templates/{web/src/exception → src/exception/web}/pages/403/index.tsx +0 -0
  252. /package/templates/{web/src/exception → src/exception/web}/pages/404/index.tsx +0 -0
  253. /package/templates/{web/src/exception → src/exception/web}/pages/500/index.tsx +0 -0
  254. /package/templates/{web/src/exception → src/exception/web}/pages/unknown-component/index.tsx +0 -0
  255. /package/templates/{api/src/home → src/home/api}/line/api.ts +0 -0
  256. /package/templates/{api/src/home → src/home/api}/manifest.yaml +0 -0
  257. /package/templates/{api/src/home → src/home/api}/migrations/0001__create_home_tables.sql +0 -0
  258. /package/templates/{api/src/home → src/home/api}/pie/api.ts +0 -0
  259. /package/templates/{api/src/home → src/home/api}/schema.yaml +0 -0
  260. /package/templates/{api/src/home → src/home/api}/seed.sql +0 -0
  261. /package/templates/{web/src/home → src/home/web}/__client/api.schemas.ts +0 -0
  262. /package/templates/{web/src/home → src/home/web}/__client/api.ts +0 -0
  263. /package/templates/{web/src/home → src/home/web}/entry.ts +0 -0
  264. /package/templates/{web/src/home → src/home/web}/locales/en-US.json +0 -0
  265. /package/templates/{web/src/home → src/home/web}/locales/zh-CN.json +0 -0
  266. /package/templates/{web/src/home → src/home/web}/pages/components/bar-chart.tsx +0 -0
  267. /package/templates/{web/src/home → src/home/web}/pages/components/card-list.tsx +0 -0
  268. /package/templates/{web/src/home → src/home/web}/pages/components/line-chart.tsx +0 -0
  269. /package/templates/{web/src/home → src/home/web}/pages/components/pie-chart.tsx +0 -0
  270. /package/templates/{web/src/home → src/home/web}/pages/index.tsx +0 -0
  271. /package/templates/{web/src/login → src/login/web}/entry.ts +0 -0
  272. /package/templates/{web/src/login → src/login/web}/locales/en-US.json +0 -0
  273. /package/templates/{web/src/login → src/login/web}/locales/zh-CN.json +0 -0
  274. /package/templates/{web/src/login → src/login/web}/pages/change-password.tsx +0 -0
  275. /package/templates/{web/src/login → src/login/web}/pages/login.tsx +0 -0
  276. /package/templates/{api/src/notification → src/notification/api}/_shared/notify.ts +0 -0
  277. /package/templates/{api/src/notification → src/notification/api}/api.ts +0 -0
  278. /package/templates/{api/src/notification → src/notification/api}/del/api.ts +0 -0
  279. /package/templates/{api/src/notification → src/notification/api}/manifest.yaml +0 -0
  280. /package/templates/{api/src/notification → src/notification/api}/migrations/0001__create_notifications.sql +0 -0
  281. /package/templates/{api/src/notification → src/notification/api}/migrations/0002__notifications_user.sql +0 -0
  282. /package/templates/{api/src/notification → src/notification/api}/migrations/0003__notifications_broadcast.sql +0 -0
  283. /package/templates/{api/src/notification → src/notification/api}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  284. /package/templates/{api/src/notification → src/notification/api}/schema.yaml +0 -0
  285. /package/templates/{api/src/notification → src/notification/api}/seed.sql +0 -0
  286. /package/templates/{web/src/notification → src/notification/web}/__client/api.schemas.ts +0 -0
  287. /package/templates/{web/src/notification → src/notification/web}/__client/api.ts +0 -0
  288. /package/templates/{web/src/notification → src/notification/web}/entry.ts +0 -0
  289. /package/templates/{web/src/notification → src/notification/web}/locales/en-US.json +0 -0
  290. /package/templates/{web/src/notification → src/notification/web}/locales/zh-CN.json +0 -0
  291. /package/templates/{web/src/notification → src/notification/web}/pages/center/index.tsx +0 -0
  292. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/CardGrid.test.tsx +0 -0
  293. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/HistoryDrawer.test.tsx +0 -0
  294. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/RollbackDialog.test.tsx +0 -0
  295. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/UninstallConfirm.test.tsx +0 -0
  296. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/poll.test.ts +0 -0
  297. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/render.tsx +0 -0
  298. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/runtime-stub.ts +0 -0
  299. /package/templates/{web/src/oj-market → src/oj-market/web}/__tests__/semver.test.ts +0 -0
  300. /package/templates/{web/src/oj-market → src/oj-market/web}/api/MarketClientProvider.tsx +0 -0
  301. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardGrid.tsx +0 -0
  302. /package/templates/{web/src/oj-market → src/oj-market/web}/components/CardItem.tsx +0 -0
  303. /package/templates/{web/src/oj-market → src/oj-market/web}/components/DetailDrawer.tsx +0 -0
  304. /package/templates/{web/src/oj-market → src/oj-market/web}/components/HistoryDrawer.tsx +0 -0
  305. /package/templates/{web/src/oj-market → src/oj-market/web}/entry.ts +0 -0
  306. /package/templates/{web/src/oj-market → src/oj-market/web}/pages/MarketApp.tsx +0 -0
  307. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/poll.ts +0 -0
  308. /package/templates/{web/src/oj-market → src/oj-market/web}/utils/semver.ts +0 -0
  309. /package/templates/{web/src/outside → src/outside/web}/entry.ts +0 -0
  310. /package/templates/{web/src/outside → src/outside/web}/locales/en-US.json +0 -0
  311. /package/templates/{web/src/outside → src/outside/web}/locales/zh-CN.json +0 -0
  312. /package/templates/{api/src/personal-center → src/personal-center/api}/manifest.yaml +0 -0
  313. /package/templates/{api/src/personal-center → src/personal-center/api}/profile/api.ts +0 -0
  314. /package/templates/{api/src/personal-center → src/personal-center/api}/upload/api.ts +0 -0
  315. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.schemas.ts +0 -0
  316. /package/templates/{web/src/personal-center → src/personal-center/web}/__client/api.ts +0 -0
  317. /package/templates/{web/src/personal-center → src/personal-center/web}/entry.ts +0 -0
  318. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/en-US.json +0 -0
  319. /package/templates/{web/src/personal-center → src/personal-center/web}/locales/zh-CN.json +0 -0
  320. /package/templates/{web/src/personal-center → src/personal-center/web}/pages/my-profile/index.tsx +0 -0
  321. /package/templates/{web/src/route-nest → src/route-nest/web}/entry.ts +0 -0
  322. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/en-US.json +0 -0
  323. /package/templates/{web/src/route-nest → src/route-nest/web}/locales/zh-CN.json +0 -0
  324. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-1/index.tsx +0 -0
  325. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu1/menu1-2/index.tsx +0 -0
  326. /package/templates/{web/src/route-nest → src/route-nest/web}/pages/menu2/index.tsx +0 -0
  327. /package/templates/{web/src → src}/styles.css +0 -0
  328. /package/templates/{web/src/system → src/system/web}/__client/api.schemas.ts +0 -0
  329. /package/templates/{web/src/system → src/system/web}/__client/api.ts +0 -0
  330. /package/templates/{web/src/system → src/system/web}/entry.ts +0 -0
  331. /package/templates/{web/src/system → src/system/web}/pages/errors.ts +0 -0
  332. /package/templates/{web/src/system → src/system/web}/pages/forbidden.tsx +0 -0
  333. /package/templates/{web/src/system → src/system/web}/pages/login-methods/index.tsx +0 -0
  334. /package/templates/{web/src/system → src/system/web}/pages/oplog/index.tsx +0 -0
  335. /package/templates/{web/src/system → src/system/web}/pages/roles/index.tsx +0 -0
  336. /package/templates/{web/src/system → src/system/web}/pages/settings/index.tsx +0 -0
  337. /package/templates/{web/src/system → src/system/web}/pages/users/index.tsx +0 -0
  338. /package/templates/{web/src/system → src/system/web}/pages/users/roles.tsx +0 -0
  339. /package/templates/{api/src/web → src/web/api}/get-async-routes/api.ts +0 -0
  340. /package/templates/{api/src/web → src/web/api}/hello/api.ts +0 -0
  341. /package/templates/{api/src/web → src/web/api}/manifest.yaml +0 -0
  342. /package/templates/{api/src/web → src/web/api}/user-info/api.ts +0 -0
  343. /package/templates/{web/src → src/web}/web/__client/api.schemas.ts +0 -0
  344. /package/templates/{web/src → src/web}/web/__client/api.ts +0 -0
  345. /package/templates/{api/tests → tests}/rbac-tables.spec.ts +0 -0
package/devkit/manual.md CHANGED
@@ -14,69 +14,137 @@
14
14
 
15
15
  `ojm init <目录>` 生成(幂等补缺,重跑只补缺失文件,**永不覆盖已有文件**)。
16
16
  模板拷贝按 `packages/cli/templates/init.include.yaml` **白名单**执行——未收录的
17
- 路径一律不拷(demo-only 演示模块、模板自运行产物都在清单外):
17
+ 路径一律不拷(demo-only 演示模块、模板自运行产物都在清单外)。
18
+
19
+ 布局是 **module-root:一个目录 = 一个模块**,前后端同处 `src/<name>/`,各占一侧
20
+ (`api/` 与 `web/` 可以只存在一侧,两侧皆无则不是模块):
18
21
 
19
22
  ```
20
23
  <工程根>/
21
24
  ├── README.md
22
25
  ├── package.json # scripts: dev / build / preview / info / typecheck;devDeps 钉死真实版本
23
26
  ├── pnpm-workspace.yaml # 构建审批(esbuild)+ redocly 钉版(init 生成,非模板拷贝)
24
- ├── web.config.ts # 前端模块清单(name + entry)
27
+ ├── config.yaml # oj 服务配置(mounts / db / auth / fs / platform / secrets)
28
+ ├── config/ # 现场签发:证书三件套(private/public.pem + cert.jws)+ secrets 密钥对
29
+ ├── web.config.ts # 前端模块清单(可选覆盖;真源是扫描 src/<name>/web/entry.ts)
25
30
  ├── openapi.json # 契约唯一事实源(oj openapi 产物,模板随附,ojm api --check 开箱即过)
26
- ├── tsconfig.json
31
+ ├── tsconfig.json # 前端 typecheck 工程(include src,exclude src/*/api)
32
+ ├── tsconfig.paths.json # `#/<模块名>/*` 的 TS paths 投影(ojm 生成,勿手改,见 §1.8)
27
33
  ├── global.d.ts # 后端 oj 全局对象(db/json/http…)类型声明
28
34
  ├── env.d.ts # ImportMeta.env 类型补丁
29
- ├── .gitignore
30
- ├── api/
31
- │ ├── config.yaml # oj 服务配置(mounts / db / auth / secrets)
32
- │ ├── config/ # 现场签发:证书三件套(private/public.pem + cert.jws)+ secrets 密钥对
33
- │ ├── .ojm-api-exempt.json
34
- │ ├── seed.mysql.sql / seed.pg.sql
35
- │ ├── vitest.config.ts
36
- │ ├── tests/ # 后端 BDD 测试(node:sqlite 内存库 + 真实迁移建表)
37
- │ └── src/
38
- │ ├── _platform/ # 框架级:users 表 / rbac / 市场编排 / 生命周期
39
- │ ├── auth/ # login / refresh / logout / ldap-login
40
- │ ├── notification/ # 通知互动(表 + push + del)
41
- │ ├── notifications/ # root 级 /api/notifications(通知铃的后备实现)
42
- │ ├── personal-center/ # 头像上传
43
- │ ├── system/ # 用户 / 角色 / 权限 / 设置 / oplog / login-methods
44
- │ └── web/ # hello / user-info / get-async-routes
45
- ├── web/
46
- │ └── src/
47
- │ ├── home/ demo/ login/ # 带页面模块(home/login 不可少,见 SKILL 红线)
48
- │ ├── personal-center/ notification/ system/ # 其余默认前端模块
49
- │ ├── _platform/ auth/ notifications/ web/ # 仅 __client 生成物(无页面)
50
- │ └── styles.css
35
+ ├── types/oj-modules.d.ts # `declare module "#*"` 别名兜底(非模块文件的 ambient 声明)
36
+ ├── .ojm-api-exempt.json
37
+ ├── seed.mysql.sql / seed.pg.sql
38
+ ├── vitest.config.ts # 后端 BDD(include tests/**/*.spec.ts)
39
+ ├── tests/ # 后端 BDD 测试(node:sqlite 内存库 + 真实迁移建表)
40
+ ├── src/
41
+ │ ├── tsconfig.json # 后端 typecheck 工程(extends 上两者,exclude */web)
42
+ │ ├── .ojm-builtin.json # 框架自带方案名单(init 派生,可手改/删除;打包排除 + info 折叠,见 §11)
43
+ │ ├── styles.css
44
+ │ ├── _platform/ # 框架级:users 表 / rbac / 市场编排 / 生命周期
45
+ │ │ ├── api/ # + _shared/(respond、service/、site-facade)
46
+ │ │ └── web/ # 仅 __client 生成物(无页面)
47
+ │ ├── auth/api/ # login / refresh / logout / ldap-login
48
+ │ ├── system/{api,web}/ # 用户 / 角色 / 权限 / 设置 / oplog / login-methods
49
+ │ ├── notification/{api,web}/ # 通知互动(表 + push + del)
50
+ │ ├── personal-center/{api,web}/ # 头像上传
51
+ │ └── web/{api,web}/ # root 级 /api/web(hello / user-info / get-async-routes 在 api 侧)
52
+ ├── api/dist/ # 后端产物(release 根:<name>-<ver>/ + manifests.yaml + versions.json)—构建后才有
53
+ ├── web/dist/ # 站点产物(shell + modules/ + modules.json)—构建后才有
51
54
  ├── bin/ # oj 二进制 + plugins/ + devkit/(不入库)
52
55
  └── .claude/skills/ # ojm-module-dev(本 devkit)+ oj-api-dev(随 oj 二进制)
53
56
  ```
54
57
 
55
- > 模板里还有一批 demo-only 模块(`api/src/{demo,home}`、`web/src/{about,access,
56
- > exception,outside,route-nest,oj-market}`)仅供模板自演示,**不进脚手架**;
57
- > 其 `web.config.ts` 条目带 `// #region demo-only` 标记,init 拷贝时整段剥离。
58
+ 几条容易踩的命名事实:
59
+
60
+ - **`config.yaml` 在工程根**,不是 `api/config.yaml`。oj 把「项目根」锚定在 **config
61
+ 所在目录**,所有 `fs.*` 相对路径、`db.default`、`platform.root`、`mounts[].web`
62
+ 都相对它解析;证书与密钥落在同级的 `config/`(`ojm` 侧由 `resolveConfigPath()` /
63
+ `resolveCertDir()` 单点派生,见 `packages/cli/src/layout.ts`)。
64
+ - **产物有两个根**:后端 `api/dist`(oj 扁平 release,`<name>-<ver>/` 与
65
+ `manifests.yaml` 同层),站点 `web/dist`(`modules/` + `modules.json`)。
66
+ `config.yaml` 上移到工程根后,两者不再同名 `dist`。
67
+ - **`src/` 下每个目录都得是一个模块**:脚本 / 共享库请放在 `src/` 之外,否则
68
+ `ojm dev/build` 会以「既无 `api/manifest.yaml` 也无 `web/entry.ts`」直接报错。
69
+
70
+ > 模板里还有一批 demo-only 模块(`src/{demo,home}/api`、`src/{about,access,exception,
71
+ > outside,route-nest}/web`)仅供模板自演示,**不进脚手架**;其 `web.config.ts` 条目带
72
+ > `// #region demo-only` 标记,init 拷贝时整段剥离。新布局的清单是「扫描 + 覆盖」并集,
73
+ > 注释掉不会让模块消失——要「不构建」必须写 `enabled: false`。
74
+
75
+ > **历史布局仍被识别**:`web/src/<name>`(uni-dev)与 `web/<name>`(框架根仓)照旧可用,
76
+ > 只是 `ojm init` 不再产出。两套源码树**互斥**——同时存在时 `ojm` 直接报错,而不是静默选一个。
77
+
78
+ 登录账号:**账号名=内置角色名**一一对应,口令均 `123456`,且全部**免首登改密**(must_change=0,纯开发便利,勿用于生产):
79
+
80
+ | 账号 | 角色 | 能干什么 |
81
+ |---|---|---|
82
+ | `superadmin` | superadmin | 全部能力(含安全组设置、成员资格管理) |
83
+ | `admin` | admin | 日常管理(不含 superadmin 专属项) |
84
+ | `editor` | editor | 页面门编辑级 |
85
+ | `viewer` | viewer | 页面门访客级 |
86
+
87
+ > 旧脚手架库(admin 绑 superadmin / must_change=1)**无需手工迁移**:seed 每次启动幂等重放,
88
+ > 自动收敛为账号名=角色名并清零 must_change(`DELETE` 旧绑定 + `UPDATE`,见 `_platform/api/seed.sql`)。
58
89
 
59
- 登录账号 `admin / 123456`。端口:前端 devServer **5174**,oj 后端 **9778**
60
- (以 `api/config.yaml` 的 `server.port` 为准)。
90
+ 端口:前端 devServer **5174**,oj 后端 **9778**
91
+ (以工程根 `config.yaml` 的 `server.port` 为准)。
61
92
 
62
93
  ### 1.2 两个模块体系,同名不同物
63
94
 
64
- 前后端各有自己的"模块",目录名一致是**约定**(契约会强制校验:apiPrefix 必须字面等于后端目录名):
95
+ 前后端各有自己的"模块",在 module-root 布局里它们**同处一个目录的两侧**,目录名一致是
96
+ **约定**(线上前缀取 `manifest.yaml` 的 `name`,而生成的 client 以目录名作认领身份——
97
+ 两者对齐才不会串,见 §3.2):
65
98
 
66
- | | 前端模块 | 后端模块 |
99
+ | | 前端一侧 | 后端一侧 |
67
100
  |---|---|---|
68
- | 位置 | `web/src/<模块>/` | `api/src/<模块>/` |
69
- | 入口 | `entry.ts`(`defineModule`) | `manifest.yaml` |
70
- | 加接口 | 消费生成的 client | 写 `api.ts` handler |
71
- | 注册处 | `web.config.ts` | 目录存在即注册(无需清单) |
101
+ | 位置 | `src/<模块>/web/` | `src/<模块>/api/` |
102
+ | 入口 | `web/entry.ts`(`defineModule`) | `api/manifest.yaml` |
103
+ | 加接口 | 消费生成的 client | 写 `api/<路径...>/api.ts` handler |
104
+ | 注册处 | `web.config.ts`(可选覆盖,扫描为真源) | 目录存在即注册(无需清单) |
105
+
106
+ 历史布局下两侧分居 `web/src/<模块>/` 与 `api/src/<模块>/`——**同一个名字、两个目录**,
107
+ 是布局迁移时最容易对不上的地方。
108
+
109
+ #### manifest.yaml 与 solution.yaml:两种身份,别混淆
110
+
111
+ 工程里会出现两个名字里带 "manifest" 的文件,用途**完全不同**,混用会出构建/运行期怪问题:
112
+
113
+ | 文件 | 层级 | 身份 | 谁读它 | 写什么 |
114
+ |---|---|---|---|---|
115
+ | `src/<模块>/api/manifest.yaml` | **模块级**(每个后端模块一份) | oj **原生七键**清单(`name`/`desc`/`version`/`tables`/…) | **oj 运行时**——按 `manifest.yaml` 枚举发现并校验 api 后端模块 | 模块名、版本、表清单(与 `schema.yaml` 双向一致,§1.5) |
116
+ | `solution.yaml`(`src/<模块>/` 或工程根) | **方案级**(整个方案一份) | 方案包契约(身份 / 路由 / api 段 / web 段 / 权限 / …) | **`ojm build --bundle`**——打包入口与契约校验 | 方案名、版本、`api` 段(模块+前缀+tables+deps+anonymous)、`web` 段(entry/css/integrity)、`routes`、`health` |
117
+
118
+ 关键区别与红线:
119
+
120
+ - **模块级 `manifest.yaml` 是 oj 发现 api 后端的唯一真源**:`ojm dev/build` 递归扫描
121
+ `src/<模块>/api/manifest.yaml` 认领后端模块;oj 启动也用它做强校验(`name ==` 目录名、版本同源)。
122
+ 它**只能有 oj 原生七键**,写任何方案级字段(`routes` / `permissions` / …)都会被拒启。
123
+ - **方案级契约以前也叫 `manifest.yaml`(工程根)**,但它会和上面的「oj 按 `manifest.yaml` 枚举 api 模块」
124
+ 撞名:**工程根再放一份 `manifest.yaml` 当 bundle 契约,会被 oj 的搜索枚举误当成 api 模块**,污染后端发现。
125
+ 所以解决方案统一改名为 **`solution.yaml`**,`ojm build --bundle` 优先认它(`manifest.yaml` 仅作旧方案回退)——
126
+ **后端按 `manifest.yaml` 枚举 api、打包按 `solution.yaml` 取契约**,两条枚举互不干扰。
127
+ **位置**:单模块方案推荐放 `src/<模块>/solution.yaml`(契约与所打包模块同位);`ojm build --bundle`
128
+ 也兼容工程根 `solution.yaml`,以及两处的 `manifest.yaml` 回退。**模块级契约只认 `src/<模块>/` 根下、
129
+ 绝不进 `<模块>/api/`**——否则照样会和 oj 的 api 枚举撞车。
130
+ - 进包后文件名固定回到 `manifest.yaml`(安装侧只认这个名字):`ojm build --bundle` 把工程根或
131
+ `src/<模块>/` 下的 `solution.yaml`(或旧 `manifest.yaml`)拷贝进 tgz 内的 `<name>-<version>/manifest.yaml`,安装器/市场仍按老名字消费。
132
+
133
+ > 一句话:**`manifest.yaml` 是给 oj 认模块用的(模块级、七键、oj 枚举);`solution.yaml` 是给 `ojm build --bundle`
134
+ > 打包用的(方案级、工程根)。两者文件名不同、层级不同、读者不同,别写错层。**
72
135
 
73
136
  ### 1.3 后端路由 = 目录镜像
74
137
 
75
138
  ```
76
- api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
139
+ src/<模块>/api/<路径...>/api.ts → /api/<模块>/<路径...>
77
140
  ```
78
141
 
79
- `api/src/web/hello/api.ts` → `GET|POST /api/web/hello`。
142
+ `src/web/api/hello/api.ts` → `GET|POST /api/web/hello`。
143
+
144
+ 注意中间的 `api` 是**布局分隔符,不是路由段**(`src/web/api/hello` ↔ `/api/web/hello`,
145
+ 不是 `/api/web/api/hello`)——`ojm` 的契约扫描按「模块的 api 侧目录」逐模块进行,
146
+ 不会把 `src` 整棵当扫描根(历史布局同理:`api/src/web/hello/api.ts`)。
147
+
80
148
  `api.ts` 默认导出一个对象,**方法名就是 HTTP 方法名**:
81
149
  `get/post/put/patch/del`(注意是 `del`,**不是 `delete`**——写成 `delete` 返回 405)。
82
150
 
@@ -84,10 +152,10 @@ api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
84
152
 
85
153
  | 改动 | 生效方式 |
86
154
  |---|---|
87
- | 已有 `api.ts` 的**文件内容** | 保存即热更 |
88
- | **新增/删除**后端模块目录 | **必须重启 `ojm dev`** |
155
+ | 已有 `api/**/api.ts` 的**文件内容** | 保存即热更 |
156
+ | **新增/删除**后端模块目录(`src/<模块>/api`) | **必须重启 `ojm dev`** |
89
157
  | 改 `schema.yaml` / `migrations/` / `config.yaml` | **必须重启** |
90
- | 前端 `web/src/**` | 模块重建 + 浏览器自动刷新 |
158
+ | 前端 `src/<模块>/web/**` | 模块重建 + 浏览器自动刷新 |
91
159
 
92
160
  ### 1.5 建一张新表:三个文件,缺一不可
93
161
 
@@ -95,9 +163,9 @@ api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
95
163
 
96
164
  | 文件 | 角色 | 规矩 |
97
165
  |---|---|---|
98
- | `api/src/<模块>/migrations/{seq:04}__{desc}.sql` | **DDL 的真正定义(实际 SQL)** | **DDL 只能写在 migrations 里**。序号连续且唯一,`CREATE TABLE IF NOT EXISTS` |
99
- | `api/src/<模块>/schema.yaml` | 声明式表结构(供归属图与列白名单使用) | 列名、null 性、默认值要与 migration **逐字段一致** |
100
- | `api/src/<模块>/manifest.yaml` | `tables: [表名]` | **与 `schema.yaml` 双向一致**(约定),只写一边启动即失败 |
166
+ | `src/<模块>/api/migrations/{seq:04}__{desc}.sql` | **DDL 的真正定义(实际 SQL)** | **DDL 只能写在 migrations 里**。序号连续且唯一,`CREATE TABLE IF NOT EXISTS` |
167
+ | `src/<模块>/api/schema.yaml` | 声明式表结构(供归属图与列白名单使用) | 列名、null 性、默认值要与 migration **逐字段一致** |
168
+ | `src/<模块>/api/manifest.yaml` | `tables: [表名]` | **与 `schema.yaml` 双向一致**(约定),只写一边启动即失败 |
101
169
 
102
170
  > 少了 migration → 表根本建不出来;少了 `schema.yaml` / `manifest.tables`
103
171
  > → 启动归属校验失败,且任何经 `db.table("<表名>")` 访问该表的代码都会被 oj 的 DB 构造器以
@@ -126,18 +194,21 @@ api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
126
194
  `_platform/_shared/respond.ts` 的 `respond(fn)`——service 抛 `Error & {status?, ...extra}`,
127
195
  handler 写 `await respond(async () => { ...; return data; })` 即自动映射
128
196
  (extra = error 的 own 可枚举属性除去 `status`/`message`,进信封 `data`)。
129
- 2. **相对 import 按「目录」解析,不是按文件层级。** 路由加载器以**含 `api.ts` 的目录**
130
- 为基准解析相对路径——多写一层 `../` 在 dev 日志里是 route error(模块像没注册),
131
- vitest 直引 service 永远发现不了。正确深度:
197
+ 2. **跨模块引用优先用 `#` 别名;相对 import 按「目录」解析,不是按文件层级。**
198
+ 路由加载器以**含 `api.ts` 的目录**为基准解析相对路径——多写一层 `../` 在 dev 日志里
199
+ 是 route error(模块像没注册),vitest 直引 service 永远发现不了。新代码一律用 §1.8
200
+ 的别名(与嵌套深度无关);存量相对 import 的正确深度(module-root 布局,基准 = 含
201
+ `api.ts` 的目录):
132
202
 
133
203
  | 文件位置 | 到 `_platform/_shared` 的写法 |
134
204
  |---|---|
135
- | `api/src/system/<x>/api.ts` | `../../_platform/_shared/...` |
136
- | `api/src/system/<x>/<y>/api.ts` | `../../../_platform/_shared/...` |
137
- | `api/src/auth/api.ts` | `../_platform/_shared/...` |
138
- | `api/src/auth/<z>/api.ts` | `../../_platform/_shared/...` |
139
- | `api/src/_platform/<x>/api.ts` | `../_shared/...` |
140
- | `api/src/_platform/<x>/<y>/api.ts` | `../../_shared/...` |
205
+ | `src/system/api/<x>/api.ts` | `../../../_platform/api/_shared/...` |
206
+ | `src/system/api/<x>/<y>/api.ts` | `../../../../_platform/api/_shared/...` |
207
+ | `src/auth/api/<z>/api.ts` | `../../../_platform/api/_shared/...` |
208
+ | `src/_platform/api/<x>/api.ts` | `../_shared/...` |
209
+ | `src/_platform/api/<x>/<y>/api.ts` | `../../_shared/...` |
210
+
211
+ > 历史布局把 `src/<m>/api` 换成 `api/src/<m>` 后同构,深度不变。
141
212
  3. **模块级可变状态不跨请求存活。** oj **每请求重建 handler 模块实例**(实测:模块级
142
213
  计数器连打三次恒为初值)——`let` / `Map` / `Set` 级状态一律视为**单请求作用域**。
143
214
  需要跨请求的状态(限速、节流、去重窗口、判权缓存)**必须落库**或走 oj 持久面。
@@ -151,6 +222,30 @@ api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
151
222
  顶部只许 import 与纯常量声明;一切运行时全局的取用移进 handler 函数体内按需执行
152
223
  (无状态构造是纯闭包,每请求新建零成本)。
153
224
 
225
+ ### 1.8 `#` 别名与 TS 投影(`tsconfig.paths.json`)
226
+
227
+ oj 的 `#` 导入与目录深度无关(后端解析规则见 `oj-api-dev` 手册「导入解析」):
228
+
229
+ | 写法 | 锚点 | module-root 下实际位置 |
230
+ |---|---|---|
231
+ | `#x` | **本模块根** | `src/<本模块>/api/x` |
232
+ | `#/<模块名>/x` | **目标模块根** | `src/<模块名>/api/x` |
233
+
234
+ - 模板后端一律写跨模块别名(如 `import { respond } from "#/_platform/_shared/respond"`),
235
+ 不再数 `../`;目录搬动不用改引用。
236
+ - 运行期由 oj loader 解析;编辑器与 `tsc` 靠两份**静态投影**:
237
+ - `tsconfig.paths.json`:`ojm init` 生成,`ojm build` / `ojm dev` 按
238
+ `src/*/api/manifest.yaml` **现场重写**(模块增删自动跟随,**勿手改**)。
239
+ - `types/oj-modules.d.ts`:非模块文件的 `declare module "#*"`,兜底那些静态定位不到的
240
+ `#` 导入(降级 `any`,而不是 TS2307 一片红)。
241
+ - `tsconfig.paths.json` 每条规则的替换值只允许一个 `*`(`./src/*/api/*` 会被 TS5062 拒掉),
242
+ 所以按模块逐条枚举:`"#/<名>/*": ["./src/<名>/api/*"]`。`#x`(本模块根)每个模块各不相同、
243
+ 无法枚举,由上面的 ambient 兜底;想要真实类型就写 `#/<本模块名>/x`,它命中同一条 paths。
244
+ - **跨模块别名需在 `manifest.yaml` 的 `deps` 里声明依赖**,缺目标模块则 `oj build` 在构建期
245
+ 直接报错(与表归属校验同一套归属图);模块名写错的别名同样在构建期被拦下。
246
+ - `#` 与 Node `package.json#imports` 共用命名空间:工程里若出现 `#` 开头的 imports 键,
247
+ `oj build` 直接失败(避免两套解析器分叉)。
248
+
154
249
  ---
155
250
 
156
251
  ## §2 写前端模块
@@ -158,20 +253,23 @@ api/src/<模块>/<路径...>/api.ts → /api/<模块>/<路径...>
158
253
  ### 2.1 目录
159
254
 
160
255
  ```
161
- web/src/<模块名>/
256
+ src/<模块名>/web/
162
257
  ├── entry.ts # 元数据唯一来源(没有 package.json / meta.json)
163
258
  ├── pages/ # 页面组件(按需)
164
259
  ├── locales/ # zh-CN.json / en-US.json(按需)
165
260
  └── __client/ # ojm api 生成物(勿手改)
166
261
  ```
167
262
 
168
- ### 2.2 两步缺一不可
263
+ ### 2.2 两件事:建档 + (可选)登记
169
264
 
170
- 1. **有文件**:`web/src/<模块>/entry.ts` 默认导出 `defineModule({...})`;
171
- 2. **去登记**:工程根 `web.config.ts` 的 `modules` 数组加一项。
265
+ 1. **有文件**:`src/<模块>/web/entry.ts` 默认导出 `defineModule({...})`;
266
+ 2. **登记**:`web.config.ts` 的 `modules` 数组加一项——module-root 布局里这份清单退化为
267
+ **可选覆盖**:`ojm build/dev` 以扫描 `src/*/web/entry.ts` 为真源(字典序),登记项按声明
268
+ 顺序排前、其余按扫描顺序追加,**忘登记不会漏构建**;登记了不存在的前端入口才报错。
269
+ 要「不构建某个模块」得显式写 `enabled: false`(注释掉无效——扫描仍会捡到它)。
172
270
 
173
- > 模板真出过这个 bug:模块文件都在,就是忘了登记,结果模块安静地没加载、
174
- > 按钮安静地灰着,**全程没有一个报错**。写完模块第一件事就是检查 `web.config.ts`。
271
+ > 历史布局(`web/src/<模块>/`)没有扫描,登记是**必需**的。模板真出过这个 bug:模块文件都在,
272
+ > 就是忘了登记,结果模块安静地没加载、按钮安静地灰着,**全程没有一个报错**。
175
273
 
176
274
  **建档顺序很重要**(有接口的模块):先写 handler 的 `.schema` → 跑 `oj openapi` + `ojm api` → 再写 `entry.ts`。
177
275
  生成前 `import { createXxxClient } from "./__client/api"` 必然 ts 报红,
@@ -185,11 +283,11 @@ export default {
185
283
  modules: [
186
284
  // home 必须保留且尽量靠前:shell 预构建把 "/" → VITE_BASE_HOME_PATH(=/home)
187
285
  // 重定向,缺 /home 路由会让登录回跳 / 点 logo 落错误边界。
188
- { name: "home", entry: "web/src/home/entry.ts", enabled: true },
286
+ { name: "home", entry: "src/home/web/entry.ts", enabled: true },
189
287
  // login 必须保留:shell 宿主只消费模块路由、不挂 runtime 内置 baseRoutes,
190
288
  // 缺它 `/login` 无路由可跳(登出会落空)。
191
- { name: "login", entry: "web/src/login/entry.ts", enabled: true },
192
- { name: "books", entry: "web/src/books/entry.ts", enabled: true },
289
+ { name: "login", entry: "src/login/web/entry.ts", enabled: true },
290
+ { name: "books", entry: "src/books/web/entry.ts", enabled: true },
193
291
  ],
194
292
  };
195
293
  ```
@@ -245,7 +343,7 @@ export default defineModule({
245
343
  `defineModule` 字段:`name`(= 目录名)、`description`、`version`、
246
344
  `peerRuntime`、`routes`、`i18n?`、`lifecycle?`。
247
345
 
248
- > 页面组件用**静态 import 或 `lazy()` 都行**:模板(`templates/web/src/*/entry.ts`)
346
+ > 页面组件用**静态 import 或 `lazy()` 都行**:模板(`packages/cli/templates/src/*/web/entry.ts`)
249
347
  > 用静态 import,多页面模块可对非首屏页面用 `lazy(() => import("./pages/x"))` 做代码分割。
250
348
  > 单个页面就用静态 import,少一层间接。
251
349
 
@@ -300,7 +398,7 @@ export default defineModule({
300
398
  可服务的同源路径)。直接 `import "vditor/dist/..."` 其它深路径会命中前缀 external 但无
301
399
  importmap 键,构建期报错——需要新深路径请联系框架方在共享表登记。
302
400
 
303
- ### 2.7 本版本共享依赖钉版(0.2.8 快照)
401
+ ### 2.7 本版本共享依赖钉版(0.2.20 快照)
304
402
 
305
403
  宿主经 importmap 提供以下单例(模块 `devDependencies` 须对齐,供 `tsc` 用);完整 68 项矩阵以
306
404
  `pnpm exec ojm info` 或 `packages/cli/shell-dist/versions.json`(构建期生成)为准。
@@ -329,11 +427,12 @@ importmap 键,构建期报错——需要新深路径请联系框架方在共
329
427
  | `pinyin-pro` | 3.29.3 |
330
428
  | `vditor` | 4.0.0 |
331
429
 
332
- 框架包版本:`@oj-module/runtime` / `@oj-module/cli` 为 `0.2.8`(**双包 lockstep**);
430
+ 框架包版本:`@oj-module/runtime` / `@oj-module/cli` 为 `0.2.20`(**双包 lockstep**);
333
431
  `@plane/kit` / `@plane/editor` 是独立版本线(当前 `0.1.20`,随宿主 importmap 提供,
334
- 不参与 lockstep)。`oj` 二进制最低版本门禁 `MIN_OJ_VERSION = 0.1.73`——契约流水线要求
335
- oj ≥ 0.1.73:平级 `.response` 出参声明位自 v0.1.72 起、`oj openapi` 的 `del`→`delete`
336
- 操作键归一自 v0.1.73 起(低版本 oj 生成 `openapi.json` 会丢出参类型或产出非法
432
+ 不参与 lockstep)。`oj` 二进制最低版本门禁 `MIN_OJ_VERSION = 0.1.85`——这个版本带来
433
+ `--module-root`(`src/<name>/{api,web}` 布局的源码树根,与 `-d/--api-path` **互斥**);
434
+ 契约流水线另要求 oj ≥ 0.1.73:平级 `.response` 出参声明位自 v0.1.72 起、`oj openapi` 的
435
+ `del`→`delete` 操作键归一自 v0.1.73 起(低版本 oj 生成 `openapi.json` 会丢出参类型或产出非法
337
436
  `"del"` 键,`ojm api --check` 必漂移)。
338
437
 
339
438
  > **`@oj-module/runtime` 到底导出哪些东西?** 权威清单随包发布在
@@ -367,7 +466,7 @@ const roles = useUserStore(s => s.roles);
367
466
 
368
467
  **写(改密/改角色/改设置)→ 走契约 API client,不碰框架内部**
369
468
 
370
- - 框架已把 `auth/*`、`system/*`、`web/*` 等端点经 OpenAPI 契约定义,并从 `@oj-module/runtime` 导出类型化 fetch(如 `api/rbac` 的 `fetchChangePassword`、`fetchRbacUpdateUserRoles`,`api/user` 的 `fetchUserInfo`)。要调框架内置写接口,直接复用这些导出。**但更推荐模块 own 自己的契约**:在**自己模块的 handler 动词**上写 `.schema`,跑 `oj openapi` + `ojm api` 生成 `web/src/<模块>/__client/api.ts`,模块经自己的 client 调框架端点——接口由契约(versioned OpenAPI)对齐,而非把框架源文件打进来。
469
+ - 框架已把 `auth/*`、`system/*`、`web/*` 等端点经 OpenAPI 契约定义,并从 `@oj-module/runtime` 导出类型化 fetch(如 `api/rbac` 的 `fetchChangePassword`、`fetchRbacUpdateUserRoles`,`api/user` 的 `fetchUserInfo`)。要调框架内置写接口,直接复用这些导出。**但更推荐模块 own 自己的契约**:在**自己模块的 handler 动词**上写 `.schema`,跑 `oj openapi` + `ojm api` 生成 `src/<模块>/web/__client/api.ts`,模块经自己的 client 调框架端点——接口由契约(versioned OpenAPI)对齐,而非把框架源文件打进来。
371
470
  - 底层都走同一个 `request` 单例 + 契约 URL;模块侧请求被 `defineModule` 的 `ctx` 收敛到登记过的 `apiPrefix`,越界 / `../` 穿越 / 逐请求 prefix 覆盖均被拒(请求边界见 §2.6 + `tests/runtime/scoped-request.test.ts`)。
372
471
 
373
472
  **场景:模块记录自己的操作日志(写端)**
@@ -415,13 +514,13 @@ await fetchRbacOplogPost({
415
514
  oj handler .schema(入参,手写)+ .response(出参,平级)——唯一权威源
416
515
  │ oj 内省(含组级 .schema / .response 继承合并)
417
516
  ▼
418
- oj openapi -c config -d api/src -b /api -o openapi.json → OpenAPI 3.1
517
+ oj openapi -c config.yaml --module-root src -b /api -o openapi.json → OpenAPI 3.1
419
518
  │
420
519
  ▼
421
520
  ojm api → irFromOpenapi → emitZodFromJsonSchema / emitClient
422
521
  │
423
522
  ▼
424
- web/<模块>/__client/{api.ts, api.schemas.ts}(+ stub / mock)
523
+ src/<模块>/web/__client/{api.ts, api.schemas.ts}(+ stub / mock)
425
524
  ```
426
525
 
427
526
  `openapi.json` 即权威文档源,`ojm api --docs` 直接聚合它渲染文档站。`.schema` / `.response`
@@ -440,16 +539,20 @@ web/<模块>/__client/{api.ts, api.schemas.ts}(+ stub / mock)
440
539
 
441
540
  | 工程形态 | 权威源 | 产物流向 |
442
541
  |---|---|---|
443
- | **uni-dev**(前后端一体,推荐) | 后端 `api/src/<模块>/**/api.ts` 上的 `.schema` | `web/src/<模块>/__client/{api.ts, api.schemas.ts}`(由 `ojm api` 消费 `openapi.json` 生成);stub → `api/src/<模块>/<路径>/api.ts`(未实现 handler 时补骨架) |
542
+ | **module-root**(`ojm init` 产物,推荐) | 后端 `src/<模块>/api/**/api.ts` 上的 `.schema` | `src/<模块>/web/__client/{api.ts, api.schemas.ts}`(由 `ojm api` 消费 `openapi.json` 生成);stub → `src/<模块>/api/<路径>/api.ts`(未实现 handler 时补骨架) |
543
+ | **uni-dev**(历史布局) | 后端 `api/src/<模块>/**/api.ts` 上的 `.schema` | `web/src/<模块>/__client/…`;stub → `api/src/<模块>/<路径>/api.ts` |
444
544
  | 纯前端 | 仅消费他人 `openapi.json` | `__client/` 全部落前端侧,无 stub |
445
545
 
446
546
  `openapi.json` 是项目级单文件、含全部模块,由 `oj openapi` 产出,ojm 不再自产 `routes.json` /
447
547
  `openapi.yaml`。
448
548
 
449
- **硬约束**:模块目录名即 API 前缀来源——`api/src/order/**` → 线上前缀 `/order`(与旧 `apiPrefix`
450
- 字面相等约束同源,只是现在由目录名推导,不再手写字段)。不符即人话报错。
549
+ **硬约束**:线上前缀来自模块 `manifest.yaml` 的 `name`(0.1.85 起与目录名脱钩,目录名只用于
550
+ 定位)——`name: order` 的全部 handler 挂在 `/order` 下,即 `src/order/api/**` → `/order`
551
+ (历史布局 `api/src/order/**` 同一条约束)。**约定仍让两者保持一致**:`ojm api` 生成的 client
552
+ 以目录名作 `MODULE_PREFIX`(跨模块认领护栏的对比基准),只改一边会让 dev 控制台报「正在认领
553
+ 别人的 client」。与旧 `apiPrefix` 字面相等约束同源,只是不再手写字段。
451
554
 
452
- > 工程里有两个 "web":前端 `web/`,与后端 `api/src/web/`(提供 user-info 等内置接口)。
555
+ > 工程里有两个 "web":前端侧 `src/web/web/`,与后端 `src/web/api/`(提供 user-info 等内置接口)。
453
556
  > 豁免清单里的 `"modules": ["web"]` 指的是**后者**。
454
557
 
455
558
  ### 3.3 handler `.schema`(入参)+ `.response`(出参)写法(JSON Schema 子集)
@@ -461,7 +564,7 @@ web/<模块>/__client/{api.ts, api.schemas.ts}(+ stub / mock)
461
564
  `defineApi` 的 `data`):
462
565
 
463
566
  ```ts
464
- // api/src/order/list/api.ts(oj v0.1.72+ 写法,.schema / .response 手写,勿由工具生成)
567
+ // src/order/api/list/api.ts(oj v0.1.72+ 写法,.schema / .response 手写,勿由工具生成)
465
568
  function get() {
466
569
  // ...业务逻辑,json.ok(...) / json.fail(...)
467
570
  }
@@ -516,7 +619,7 @@ minLength / maxLength / pattern / enum / nullable / minItems / maxItems / defaul
516
619
  **上传文件(multipart/form-data)**:当前 `ojm api` 的 openapi 流水线**不生成** form 通道的
517
620
  前端 client(文件部件是二进制,不进 JSON Schema)。惯例走 antd `Upload` **直连**后端
518
621
  (不经 JSON client),handler 用 `http.files` / `http.file(i)` 读字节,仅平级声明
519
- `.response`(见 `api/src/personal-center/upload/api.ts` 范本)。
622
+ `.response`(见 `src/personal-center/api/upload/api.ts` 范本)。
520
623
 
521
624
  **不要手写 `content-type`**:运行时(ky)见到 `FormData` 会自己补 `multipart/form-data;
522
625
  boundary=…`,手写反而丢 boundary。
@@ -526,7 +629,7 @@ boundary=…`,手写反而丢 boundary。
526
629
 
527
630
  ### 3.4 生成物使用契约
528
631
 
529
- 生成的 `web/src/<模块>/__client/api.ts` 导出:
632
+ 生成的 `src/<模块>/web/__client/api.ts` 导出:
530
633
 
531
634
  - 裸请求函数(`fetchNotifications`、`markRead`…)——**直接 import 调用**;
532
635
  - `API_PREFIX` 常量——契约前缀的唯一真源;
@@ -548,7 +651,7 @@ const booksClient = createBooksClient(ctx);
548
651
  - 重复构造幂等安全;`onInit` 每模块只跑一次(HMR / StrictMode 不重复执行)。
549
652
 
550
653
  > **生成 ≠ 使用。** `ojm api` 为 **openapi.json 里出现的每个模块**(`tags[0]`)生成
551
- > `web/src/<模块>/__client/`,**不看有没有人 import**。所以「handler 在、`__client/`
654
+ > `src/<模块>/web/__client/`,**不看有没有人 import**。所以「handler 在、`__client/`
552
655
  > 目录却不在」只有一种解释:那个模块的 `__client/` 从没生成过(模板里的
553
656
  > personal-center 上传走 antd Upload 直连,不经过 JSON client,故没人 import 它的 `__client`)。
554
657
  > 你在自己工程跑一次 `ojm api`,它就会出现。
@@ -566,7 +669,7 @@ const booksClient = createBooksClient(ctx);
566
669
  `__client/api.ts`、`__client/api.schemas.ts` **重跑即覆盖**(eslint 已忽略这些路径)。
567
670
  改需求 = 改 handler 的 `.schema`(或路由),重跑 `oj openapi` + `ojm api`。
568
671
 
569
- **唯一例外:stub。** `api/src/**/api.ts` 的 stub 文件头带指纹
672
+ **唯一例外:stub。** `src/<模块>/api/**/api.ts` 的 stub 文件头带指纹
570
673
  (`// ojm-api:stub <name> sha256:...`)。人碰过的 stub,工具**永不写永不删**——
571
674
  实现 handler 就是在 stub 里填业务逻辑(`.schema` 由开发者手写,工具不注入)。
572
675
 
@@ -580,8 +683,8 @@ PascalCase(operationId) + `Query` / `Params` / `Body` / `Data`
580
683
  ### 3.6 命令
581
684
 
582
685
  ```bash
583
- # 1) oj 产出 OpenAPI 规范(base 从 config 的 api 挂载前缀读;dev 用 -d api/src 内省)
584
- oj openapi -c api/config.yaml -d api/src -b /api -o openapi.json
686
+ # 1) oj 产出 OpenAPI 规范(base 缺省读 config 的 api 挂载前缀;dev 用 --module-root src 内省源码树)
687
+ oj openapi -c config.yaml --module-root src -b /api -o openapi.json
585
688
  # 2) ojm 消费生成前端 __client(按 tags[0] 分组到各模块)
586
689
  pnpm exec ojm api
587
690
  # 3) 校验同步 / route 双向对账(只读)
@@ -590,7 +693,8 @@ pnpm exec ojm api --check
590
693
  pnpm exec ojm api --docs
591
694
  ```
592
695
 
593
- > `oj openapi` 是 **oj 透传命令**(ojm 自动补 `-c api/config.yaml`),参数照 oj 原版语义。
696
+ > `oj openapi` 是 **oj 透传命令**(ojm 自动补 `-c config.yaml`),参数照 oj 原版语义。
697
+ > 历史布局把 `--module-root src` 换成 `-d api/src`(两者互斥,都是「源码树根」)。
594
698
  > 找不到 `openapi.json` 时 `ojm api` 直接报错指引先跑 `oj openapi`(旧 `contract.ts` 不兼容读取)。
595
699
 
596
700
  `--check` 对四件事(只读,绝不写盘、绝不修文件):
@@ -624,7 +728,7 @@ flowchart TD
624
728
  前端 DEV 期 zod 校验、OpenAPI 文档——前端 client 类型则来自 `oj openapi` 产出的 `openapi.json`。
625
729
 
626
730
  ```ts
627
- // api/src/order/list/api.ts(.schema 由开发者手写,oj 运行期据此校验入参)
731
+ // src/order/api/list/api.ts(.schema 由开发者手写,oj 运行期据此校验入参)
628
732
  function get(): void {
629
733
  json.ok({ /* TODO */ });
630
734
  }
@@ -638,24 +742,26 @@ get.schema = {
638
742
  export default { get };
639
743
  ```
640
744
 
641
- 开关在 `api/config.yaml` 的 `server.schema_validation`(默认 `true`,置 `false` 为逃生门)。
745
+ 开关在 `config.yaml` 的 `server.schema_validation`(默认 `true`,置 `false` 为逃生门)。
642
746
  白名单裁剪规则与降级(无 `format`/`oneOf`、Rust regex 不支持零宽断言、params/query 只允许扁平
643
747
  标量等)在 `oj openapi` 内省期或 ojm 生成期 fail-fast / 告警,详情见 §3.3 与 `oj-api-dev` skill
644
748
  手册(`.claude/skills/oj-api-dev/`)。
645
749
 
646
- ### 3.8 豁免清单 `api/.ojm-api-exempt.json`
750
+ ### 3.8 豁免清单 `.ojm-api-exempt.json`
647
751
 
648
752
  把"确认无害"的对账差异从 error 降级为 skip(**只降级,绝不引入新错误**):
649
753
 
650
754
  ```jsonc
651
755
  {
652
756
  "_comment": "说明文字写这里——本文件是严格 JSON,写 // 注释会解析失败、豁免静默全失效!",
653
- "modules": ["web"], // 整模块跳过(值 = api/src/<模块> 目录名)
757
+ "modules": ["web"], // 整模块跳过(值 = 模块目录名,module-root 下即 src/<名>/api 的 <名>)
654
758
  "paths": ["/auth/*"] // /xxx/* 一层通配;其余为精确前缀
655
759
  }
656
760
  ```
657
761
 
658
- 文件缺失或损坏时静默按空豁免处理。cli ≥ 0.1.5 的 `ojm init` 已内置该文件。
762
+ 文件缺失或损坏时静默按空豁免处理。cli ≥ 0.1.5 的 `ojm init` 已内置该文件,落在**工程根**
763
+ (与 `config.yaml` 同级;历史布局在 `api/` 下)——`ojm api` 按布局探测自动找,写死位置会
764
+ 让它**静默失效**(回退空豁免 → 内置端点一片「有 handler 无契约」)。
659
765
 
660
766
  ### 3.9 DDD 判据速查(oj v0.1.64 嵌套 handler 组)
661
767
 
@@ -674,14 +780,16 @@ export default { get };
674
780
 
675
781
  ### 3.10 从旧 `contract.ts` / `defineApi` 工程迁移
676
782
 
677
- 适用于 0.1.20 之前、用「`api/src/<模块>/contract.ts`(`defineApi` + zod)」声明契约的工程。
783
+ 适用于 0.1.20 之前、用「`api/src/<模块>/contract.ts`(`defineApi` + zod)」声明契约的工程
784
+ (这类工程都是历史布局;迁移到 module-root 时同一套步骤,只是路径换成 `src/<模块>/api/`)。
678
785
  翻译层已删除,`ojm api` 不再读取 `contract.ts`——不迁移则找不到 `openapi.json` 直接报错。
679
786
 
680
787
  **前置条件**
681
788
 
682
- - `oj` ≥ **0.1.73**(`oj openapi` 子命令 + 平级 `.response` + `del`→`delete` 归一;低版本会丢出参类型或产出非法方法键)
789
+ - `oj` ≥ **0.1.73**(`oj openapi` 子命令 + 平级 `.response` + `del`→`delete` 归一;低版本会丢出参类型或产出非法方法键);
790
+ 迁到 module-root 布局另需 **≥ 0.1.85**(`--module-root`)
683
791
  - `@oj-module/cli` / `@oj-module/runtime` ≥ **0.1.20**(lockstep)
684
- - `api/config.yaml` 站点挂载已是顶层 `mounts:`(oj v0.1.58 起旧键 fail-fast)
792
+ - `config.yaml` 的站点挂载已是顶层 `mounts:`(oj v0.1.58 起旧键 fail-fast)
685
793
 
686
794
  **字段映射(旧 `defineApi` → 新声明位)**
687
795
 
@@ -704,16 +812,19 @@ export default { get };
704
812
  `z.number()` → `{type:"number"}`、`z.enum([...])` → `{type:"string", enum:[...]}`、
705
813
  `z.union([z.string(),z.number()])` → 类型数组 `["string","number"]`、`.nullable()` →
706
814
  `["string","null"]`;`refine`/`transform` 必须下移到 handler 校验(丢失面)。
707
- 3. **生成规范**:`oj openapi -c api/config.yaml -d api/src -b /api -o openapi.json`
708
- (项目级单文件、含全部模块;组级 `.schema`/`.response` 已由 oj 内省期合并)。
709
- 4. **生成 client**:`ojm api` → `web/src/<模块>/__client/{api.ts, api.schemas.ts}`。
815
+ 3. **生成规范**:`oj openapi -c config.yaml --module-root src -b /api -o openapi.json`
816
+ (历史布局用 `-d api/src`;项目级单文件、含全部模块;组级 `.schema`/`.response` 已由
817
+ oj 内省期合并)。
818
+ 4. **生成 client**:`ojm api` → `src/<模块>/web/__client/{api.ts, api.schemas.ts}`
819
+ (历史布局 `web/src/<模块>/__client/`)。
710
820
  5. **改前端消费点**:函数名从旧人类命名(`fetchNotifications`)改为
711
821
  `camelCase(operationId)`(`notificationNotificationsGet`)——用「逐字节比对」识别,
712
822
  逐个改 import 与调用;`tsc --noEmit` 应收敛到 0 错误。
713
- 6. **删旧文件**:删除各 `contract.ts`,以及旧流水线产物 `api/src/<模块>/routes.json`、
714
- `api/src/<模块>/openapi.yaml`(ojm 不再自产;`api/docs/openapi.yaml` 仅 `--docs` 时生成,保留)。
823
+ 6. **删旧文件**:删除各 `contract.ts`,以及旧流水线产物 `routes.json` /
824
+ `openapi.yaml`(模块目录下;ojm 不再自产。`ojm api --docs` 渲染的静态文档站另放:
825
+ module-root → 工程根 `docs/api/`,历史布局 → `api/docs/`,保留)。
715
826
  7. **对账**:`ojm api --check` 应 `0 error / 0 warn`;内置端点(`auth/*`、`web/*`、
716
- `notifications`)经 `api/.ojm-api-exempt.json` 豁免(init 已内置)。
827
+ `notification/*`)经 `.ojm-api-exempt.json` 豁免(init 已内置)。
717
828
 
718
829
  **迁移后自检**
719
830
 
@@ -775,9 +886,9 @@ Provider 就是:「这块别用框架自带的了,换成我的实现。」
775
886
 
776
887
  ### 4.4 案例:接管通知功能(标准四文件)
777
888
 
778
- **① handler `.schema`**(`api/src/notification/api.ts` 上各动词挂的 `.schema`)——见 §3.3 与 §3.4。
889
+ **① handler `.schema`**(`src/notification/api/api.ts` 上各动词挂的 `.schema`)——见 §3.3 与 §3.4。
779
890
 
780
- **② handler**(`api/src/notification/api.ts`,模块根嵌套组——组名 = 路径段,oj v0.1.64):
891
+ **② handler**(`src/notification/api/api.ts`,模块根嵌套组——组名 = 路径段,oj v0.1.64):
781
892
 
782
893
  ```ts
783
894
  const notifications = {
@@ -835,12 +946,12 @@ export default defineModule({
835
946
  **⑤ 跨模块发射原语(任何后端模块都能推通知)**
836
947
 
837
948
  通知不是「只有通知模块自己用」的功能——下单、审核、@我等其它模块也需要给某个人推一条提醒。
838
- 把发射逻辑收敛成 `api/src/notification/_shared/notify.ts` 里的纯函数原语,其它模块**相对导入直接调用,
839
- 无需走 HTTP**:
949
+ 把发射逻辑收敛成 `src/notification/api/_shared/notify.ts` 里的纯函数原语,其它模块**别名导入
950
+ 直接调用,无需走 HTTP**:
840
951
 
841
952
  ```ts
842
953
  // 在其它模块的 handler 里
843
- import { pushNotice } from "../../notification/_shared/notify";
954
+ import { pushNotice } from "#/notification/_shared/notify";
844
955
 
845
956
  export default {
846
957
  async post() {
@@ -864,12 +975,15 @@ export default {
864
975
 
865
976
  约定:
866
977
  - **原语不做业务判断**(不判空间/角色/可见性),收件人过滤是发送方责任(`pushNotice` 的 `userId` 谁都能传,按人隔离靠存储 `user_id` 保证)。
867
- - **后端不能 `import '@oj-module/runtime'`**:oj 解析裸 specifier 以 `api/` 为项目根并钳制不越界(越界报
868
- `escapes project root`);`@oj-module/runtime` 符号链接在 `packages/runtime`,属项目根之外。原语必须落在
869
- `api/src` 内(相对导入);类型与 runtime 的 `@oj-module/runtime/notify` 保持同形即可,共享契约类型单一真源在 runtime。
870
- - **`_shared` 必须嵌进所属模块目录**(`api/src/notification/_shared/`),不能放 `api/src/_shared/`——oj 把
871
- `api/src` 的**顶层**目录都当模块(须有 `manifest.yaml`),顶层 `_shared` 会被当成缺 manifest 的模块,
872
- `oj serve` 直接退(`module '_shared' missing manifest.yaml`)。
978
+ - **后端不要 `import '@oj-module/runtime'`**:它是浏览器侧包(React / 浏览器全局),且方案包 tgz
979
+ 不带 `node_modules`——dev 能跑、release 或装到站点后必炸。后端共享代码放本模块
980
+ `api/_shared/`,跨模块走 `#/<模块名>/_shared/...` 别名;类型与 runtime 的
981
+ `@oj-module/runtime/notify` 保持同形即可(共享契约类型单一真源在 runtime)。
982
+ - **`_shared` 必须嵌进所属模块的 api 侧**(`src/notification/api/_shared/`):
983
+ `src/` 下每个目录都得是模块(`api/manifest.yaml` 或 `web/entry.ts`),`src/_shared` 会被
984
+ `ojm dev/build` 判成坏目录;放进模块内则是「无 `api.ts` 的纯工具目录」,不产生路由。
985
+ (历史布局同理:`api/src/notification/_shared/`,而 `api/src/_shared` 会被 oj 当成缺
986
+ `manifest.yaml` 的模块直接拒启。)
873
987
  - **归属校验返回 403**:跨用户 `del` / `read` 他人按人项,原语返回 `false`,端点 `json.fail(403, "forbidden")`——
874
988
  与「按人隔离」对称(A 推给 B 后 A 不可见、B 不可删/读 A 的)。
875
989
 
@@ -960,7 +1074,7 @@ export default {
960
1074
  ### 5.2 路由如何选布局
961
1075
 
962
1076
  ```ts
963
- // web/src/<模块>/entry.ts
1077
+ // src/<模块>/web/entry.ts
964
1078
  routes: [
965
1079
  {
966
1080
  path: "books",
@@ -1224,7 +1338,7 @@ useEffect(() => {
1224
1338
  复用展示不同内容(列表页/`index` 路由),离开时必须 `resetTableTitle(tabKey)`,否则再进来看到
1225
1339
  上一个内容的标题。
1226
1340
 
1227
- 推荐形态(照 `web/src/home/pages/`、`web/src/personal-center/pages/my-profile/` 抄):
1341
+ 推荐形态(照 `src/home/web/pages/`、`src/personal-center/web/pages/my-profile/` 抄):
1228
1342
  `BasicContent` 包一层 → 搜索区 + `BasicTable` → 新增/编辑用 antd `Modal` + `Form`
1229
1343
  → 提交后重新拉列表。数据来自 §3.4 的生成函数,**页面里直接 import 裸函数调用**。
1230
1344
 
@@ -1277,31 +1391,43 @@ useEffect(() => {
1277
1391
  `useEffect` 重新注册(上面的形态天然满足)。
1278
1392
  - 同一 key 关闭流程进行中再次触发被忽略(per-key in-flight 去重),双击不会弹两个框。
1279
1393
 
1280
- 可运行示例见 playground-oj `web/src/demo/pages/guard.tsx`(表单 dirty → 关页签弹确认)。
1394
+ 可运行示例见 playground-oj `web/src/demo/pages/guard.tsx`(本仓 playground 仍是历史布局;
1395
+ 模板侧对应 `src/<模块>/web/pages/`,表单 dirty → 关页签弹确认)。
1396
+
1397
+ ### 6.5 两份 tsconfig:`typecheck` 与 `typecheck:api`
1398
+
1399
+ 工程根 `tsconfig.json` 是**前端**工程:`include` 为 `src` + `web.config.ts` + `global.d.ts` +
1400
+ `env.d.ts`,但 `exclude` 掉 `src/*/api`——后端那半用另一份工程,两边混在一起会让 `#` paths
1401
+ 与 oj 全局对象类型互相污染。
1281
1402
 
1282
- ### 6.5 `pnpm typecheck` 看不到 `api/`
1403
+ 后端类型检查走 `pnpm typecheck:api`(`tsc --noEmit -p src/tsconfig.json`:extends 根配置 +
1404
+ `tsconfig.paths.json`,`exclude` 掉 `*/web`)。
1283
1405
 
1284
- 工程 `tsconfig.json` 的 `include` 只有 `web`、`web.config.ts`、`global.d.ts`、`env.d.ts`,
1285
- **不含 `api/`**。也就是说契约与 handler 的类型错误 `pnpm typecheck` 不报,
1286
- 只有 `pnpm exec ojm api` 真正求值契约时才会炸出来。
1406
+ 所以:
1287
1407
 
1288
- 所以:**契约改完一定要跑一次 `ojm api`**,别指望 typecheck 替你验。
1408
+ - 改 handler → 跑 `pnpm typecheck:api`(`pnpm typecheck` 看不到它);
1409
+ - 改契约 → **一定要跑一次 `ojm api`**:`.schema` 与 handler 动词不匹配这类错误 tsc 也不报,
1410
+ 只有 `ojm api` 真正求值契约时才炸出来。
1289
1411
 
1290
1412
  ---
1291
1413
 
1292
1414
  ## §7 构建与发布
1293
1415
 
1294
1416
  ```bash
1295
- pnpm dev # 开发:devServer + oj 后端(api/src 保存即热更)
1417
+ pnpm dev # 开发:devServer + oj 后端(src/<模块>/api 保存即热更)
1296
1418
  # 交互快捷键(仅 TTY 终端启用,类 Vite):
1297
1419
  # r 强制重建并热更新 h 显示帮助 c 清屏 o 浏览器打开 q 退出(oj 一并终止)
1420
+ # 提示行常驻终端最末行(滚动区),日志在其上方滚动不会被顶走(ojm ≥ 0.2.22)
1298
1421
  # Ctrl+C(SIGINT)/ kill(SIGTERM)终止 dev 时,前端与后端 oj 一起终止:
1299
1422
  # 先关 SSE 通道 → 停 oj 子进程 → 打印「后端 oj 已随前端一起终止」,不留 oj 孤儿占端口
1300
- pnpm build # = ojm build:oj build(生成 routes.js)+ 前端全站合并到 web/dist
1301
- pnpm preview # = ojm preview:oj migrate(verify 闸)→ oj serve 单服务(API + 静态 SPA 同进程)
1302
- pnpm typecheck # = tsc --noEmit
1303
- pnpm exec ojm info # 版本矩阵 + 模块清单(报障用)
1304
- pnpm exec ojm vendor # 下载/重装 oj 二进制
1423
+ pnpm build # = ojm build:oj build(生成 routes.js)+ 前端全站合并到 web/dist
1424
+ pnpm preview # = ojm preview:oj migrate(verify 闸)→ oj serve 单服务(API + 静态 SPA 同进程)
1425
+ pnpm typecheck # = tsc --noEmit(前端工程)
1426
+ pnpm typecheck:api # = tsc --noEmit -p src/tsconfig.json(后端工程,见 §6.5)
1427
+ pnpm new # = ojm new <name>:脚手架内生成用户方案骨架(见 §10.1)
1428
+ pnpm bundle # = ojm build --bundle:打包全部用户方案(自带方案自动排除,见 §11)
1429
+ pnpm exec ojm info # 版本矩阵 + 模块清单(报障用)
1430
+ pnpm exec ojm vendor # 下载/重装 oj 二进制
1305
1431
  ```
1306
1432
 
1307
1433
  `ojm build` 的两条产物线与全站合并:
@@ -1310,24 +1436,31 @@ pnpm exec ojm vendor # 下载/重装 oj 二进制
1310
1436
  flowchart LR
1311
1437
  subgraph backend["后端线"]
1312
1438
  direction TB
1313
- OJB["oj build -d api/src"] --> RD["api/dist/routes.js(release 下唯一路由来源,目录镜像不生效)"]
1439
+ OJB["oj build --module-root src"] --> RD["api/dist/routes.js(release 下唯一路由来源,目录镜像不生效)"]
1440
+ OJB --> MV["api/dist/manifests.yaml + versions.json"]
1314
1441
  end
1315
1442
  subgraph frontend["前端线"]
1316
1443
  direction TB
1317
1444
  MB["逐模块构建(esbuild bundle)"] --> EN["web/dist/modules/模块名/版本/entry.js(带 integrity)"]
1318
- MB --> MJ["web/dist/modules.json + versions.json"]
1445
+ MB --> MJ["web/dist/modules.json"]
1319
1446
  end
1320
1447
  HOST["cli 内置 shell-dist 宿主(importmap 单例)"] --> MERGE["全站合并 mergeSite"]
1321
1448
  MJ --> MERGE
1322
1449
  MERGE --> SITE["web/dist/ = index.html + assets/ + modules/ + modules.json + versions.json"]
1323
1450
  ```
1324
1451
 
1325
- - `web/dist/` = **完整站点**(`index.html`、`assets/`、`modules/`、`modules.json`、`versions.json`)。
1326
- - `api/dist/` = oj build 产物。**release 下只有 `routes.js` 里的路由存在,
1327
- 目录镜像路由不生效**——所以参数路由要先 `oj build` 再测。
1328
- - 部署包 = `config.yaml` + `dist/` + 可选 `seed.sql`。
1452
+ - `web/dist/` = **站点产物根**(`index.html`、`assets/`、`modules/`、`modules.json`、`versions.json`)。
1453
+ - `api/dist/` = **后端 release 根**(`routes.js`、`manifests.yaml`、`<name>-<ver>/`,外加从
1454
+ `shell-dist` 拷来的 `versions.json`——安装器的宿主版本真源)。**release 下只有 `routes.js`
1455
+ 里的路由存在,目录镜像路由不生效**——所以参数路由要先 `oj build` 再测。
1456
+ - 两个根**不同名同层**:config 上移到工程根后 `api/dist` 与 `web/dist` 才是它们的位置,
1457
+ 旧口径里「工程根 `dist/` 就是站点产物」已不成立(工程根 `dist/` 只剩 `--bundle` 的方案包
1458
+ 输出,见 §11.1)。
1459
+ - 部署包 = `config.yaml` + `config/`(证书与密钥,只放部署机)+ `api/dist/` + `web/dist/`
1460
+ + 可选 `seed.sql`。
1329
1461
  - **`oj` 子命令透传**:`ojm` 直接转发 `oj` 的 `test` / `exec` / `openapi` / `migrate` /
1330
- `schema` 子命令,并自动补 `-c api/config.yaml`(config 在 `api/` 下,相对 CWD 会读不到)。
1462
+ `schema` 子命令,并自动补 `-c config.yaml`(oj 的 `-c` 缺省是 CWD 相对路径,从工程根跑
1463
+ 会读不到另一个位置的 config)。
1331
1464
  例如 `ojm migrate -d api/dist`、`ojm schema diff` 等价于带绝对 `-c` 的 `oj` 调用——
1332
1465
  不用再手敲 `-c`。**完整命令参考与「oj 原生命令 vs ojm 透传」的差异区分见 §10。**
1333
1466
 
@@ -1365,7 +1498,7 @@ export default async (path: string, html: string) => {
1365
1498
  「静态站点与 per-route meta」。
1366
1499
 
1367
1500
  > release 的 `server.migrate_on_start` 默认 `verify`:迁移账本落后会**拒绝启动**,
1368
- > 先跑 `ojm migrate -d api/dist`(透传自动补 `-c api/config.yaml`)。
1501
+ > 先跑 `ojm migrate -d api/dist`(透传自动补 `-c config.yaml`)。
1369
1502
 
1370
1503
  ---
1371
1504
 
@@ -1396,8 +1529,8 @@ export default async (path: string, html: string) => {
1396
1529
  | 控制台「模块 A 正在为 /b 路径创建 client(越界)」 | A 创建了 B 的 client | 由 B 创建并通过 provider 提供能力 |
1397
1530
  | 改了契约,前端类型没变 | 没重新生成 | 跑 `ojm api`;生成确定,diff 应只含你改的部分 |
1398
1531
  | `ojm api` 报「schema 超出白名单」 | 契约里写了白名单外关键字(`oneOf`/`$ref`/`format` 等) | 改用白名单子集(§3.3);union 用类型数组、nullable 用 `["string","null"]`;校验放后端 |
1399
- | `ojm api` 报「apiPrefix 与目录名不符」 | 契约要求 apiPrefix 字面等于后端目录名 | 改 `apiPrefix` 或移动契约目录 |
1400
- | `ojm api` 报错「未发现 openapi.json」 | 没跑 `oj openapi` 生成规范 | 先 `oj openapi -c api/config.yaml -d api/src -b /api -o openapi.json`,再 `ojm api`;旧 `contract.ts` 工程须先迁移(见 §3.10) |
1532
+ | `ojm api` 报「模块 "x" 的端点线上前缀不一致」 | 同一模块的 handler 落在不同前缀下(`register.apiPrefix` 一模块一值) | 把落错目录的 handler 移回 `src/<模块>/api/**`,或拆成独立模块;dev 控制台还会就「认领别人的 client」告警(目录名与 `manifest.name` 不一致) |
1533
+ | `ojm api` 报错「未发现 openapi.json」 | 没跑 `oj openapi` 生成规范 | 先 `oj openapi -c config.yaml --module-root src -b /api -o openapi.json`(历史布局 `-d api/src`),再 `ojm api`;旧 `contract.ts` 工程须先迁移(见 §3.10) |
1401
1534
  | `--check` 报 artifact-stale | 改了契约没重跑 | 重跑 `ojm api` |
1402
1535
  | `--check` 报 handler 未登记 | 内置/手写 handler 无契约 | 补契约,或登记豁免清单 |
1403
1536
  | 豁免写了 `//` 注释后全部失效 | 严格 JSON 解析失败、静默回退 | 注释写进 `_comment` 字段 |
@@ -1409,17 +1542,17 @@ export default async (path: string, html: string) => {
1409
1542
  | 后端菜单用了我的布局名,页面却没壳 | 注册该布局的模块没加载 | 看控制台 `[layout] 未知名布局` 提醒 |
1410
1543
  | 后端下发的页面组件找不到 | 下发的组件名必须在框架页面目录里 | 自己模块的页面走模块 `routes` |
1411
1544
  | 下线模块后页面没变 | 已渲染的页面不自动换 | 刷新页面 |
1412
- | 登录 401 且 msg 不是 `invalid credentials` | `/auth/*` 不在 `anonymous_paths` | 加进 `api/config.yaml`,重启 |
1545
+ | 登录 401 且 msg 不是 `invalid credentials` | `/auth/*` 不在 `anonymous_paths` | 加进工程根 `config.yaml`,重启 |
1413
1546
  | 登出/回登录页空白或 404 | 工程缺 `login` 模块(shell 无内置登录回落) | 在 `web.config.ts` 保留 `login` |
1414
1547
  | 登录后 / 点 logo 落 React Router 错误边界 | 工程缺 `home` 模块 | 在 `web.config.ts` 保留 `home`(尽量靠前) |
1415
- | 通知铃 404 `no route matched` | 缺 root 级 `/api/notifications` | 补 `api/src/notifications` 模块 |
1548
+ | 通知铃 404 `no route matched`(`/api/notifications`) | 前端 `notification` 模块没加载 / 没注册 `notificationsApi`,回落到内置只读兜底——而脚手架不再提供 root 级 `/api/notifications` 模块 | 保留 `web.config.ts` 的 `notification` 登记,onInit 里注册四方法(模板 `src/notification/web/entry.ts`);后端由 `src/notification/api` 提供 `/api/notification/*` |
1416
1549
  | `@types/react` / `typescript` 装成 `*` | 宿主 versions.json 未列 | 安装后按实际版本钉死(`*` 不可复现) |
1417
1550
  | 迁移账本落后、服务拒绝启动 | release `verify` 闸 | 先 `ojm migrate -d api/dist`(透传自动补 `-c`) |
1418
1551
  | `seed.sql` 没生效 / 语法错 | 按 `;` 朴素切分,注释里有分号 | 去掉注释里的分号字面量 |
1419
1552
  | 页面图标 / 关闭 × 空白 | 共享资源图标默认值回归 | 重建宿主 shell |
1420
1553
  | curl 访问 SPA 路由返回 404 | SPA history 回落要求 `Accept: text/html` | 加 `-H 'Accept: text/html'`(浏览器自带,不是 bug) |
1421
- | `oj serve` 启动即退,报 `config uses removed server.* site keys (v0.1.58): api_prefix`(或 `app_path` / `app_prefix` / `static_sites` / `app_spa_fallback`) | 用了 oj v0.1.58 起删除的旧站点键 | 改 `server.api_prefix: /api` 为顶层 `mounts: [{prefix: "/api", api: "src"}]`(一行=一个 URL 前缀+一个目录,`api`/`web` 恰好其一);迁移完整 YAML 由报错信息给出;详见后端 devkit「mounts 迁移」 |
1422
- | 改完 `mounts:` 仍报 `no route matched`(/api/* 全 404) | `api:` 指向的目录不是 oj 启动目录下的 `src`(相对 config 所在目录) | `mounts[].api` 写 `"src"`,且 `oj serve` 的 `-c` 指向 `api/config.yaml`(ojm dev/preview 已内置正确路径) |
1554
+ | `oj serve` 启动即退,报 `config uses removed server.* site keys (v0.1.58): api_prefix`(或 `app_path` / `app_prefix` / `static_sites` / `app_spa_fallback`) | 用了 oj v0.1.58 起删除的旧站点键 | 改 `server.api_prefix: /api` 为顶层 `mounts:`(一行=一个 URL 前缀+一个目录,`api`/`web` 恰好其一):module-root 手写部署加 `{prefix: "/api", api: "api/dist"}`(产物摊到 config 同级时写 `"dist"`),历史布局写 `api: "src"`;迁移完整 YAML 由报错信息给出;详见后端 devkit「mounts 迁移」 |
1555
+ | 改完 `mounts:` 仍报 `no route matched`(/api/* 全 404) | `api:` 指向的目录不对(相对 config 所在目录) | module-root 指向**产物目录**(`api/dist`;部署摊平后是 `dist`),历史布局指向源码树 `src`;`oj serve` 的 `-c` 指向工程根 `config.yaml`(ojm dev/preview 已内置正确路径)。**module-root 布局的模板故意不写 api 挂载**——源码树由 `--module-root src` 供给、产物树由 `--api-path api/dist` 供给,同时出现会报「module declared in multiple places」 |
1423
1556
  | 业务错误恒返回 `{"code":500}`(401/403/409 全变 500) | handler 抛异常被 oj 一律抹成 500,不读 `err.status` | service 抛 `Error & {status}`,handler 经 `respond()` 收口(§1.7-1) |
1424
1557
  | dev 起不来,日志一堆 route error(Cannot find module) | handler 相对 import 多写了一层——按**目录**解析不是按文件(§1.7-2) | 按 §1.7 深度表改 |
1425
1558
  | 限速/节流/去重窗口在真实请求下永不累积(测试却全绿) | 模块级 `Map`/`let` 不跨请求存活——oj 每请求重建模块(§1.7-3) | 状态落库(模板 0009 先例)或走 oj 持久面 |
@@ -1433,15 +1566,15 @@ export default async (path: string, html: string) => {
1433
1566
 
1434
1567
  | 想抄什么 | 抄这里 |
1435
1568
  |---|---|
1436
- | 带页面的模块(路由 / i18n / order) | `web/src/home/entry.ts`、`web/src/demo/entry.ts` |
1437
- | 接管通知 provider(四方法全量) | `web/src/notification/entry.ts` |
1438
- | 上传 provider(action + headers 方法) | `web/src/personal-center/entry.ts` |
1439
- | 无页面模块(`routes: []`) | 同 `web/src/notification/entry.ts` |
1440
- | handler `.schema` 入参 + 平级 `.response` 出参写法(GET + POST、参数段) | `api/src/notification/api.ts`、模板 `packages/cli/templates/api/src/notification/api.ts` |
1441
- | handler 写法(嵌套组列表 / 写入 / 403 归属) | `api/src/notification/api.ts`(模块根单文件,`read`/`read-all`/`clear` 子组) |
1442
- | 统一异常出口 `respond()`(service 抛 `Error & {status}` → 信封 `json.fail`) | 模板 `api/src/_platform/_shared/respond.ts` + 各 handler 用法(§1.7-1) |
1443
- | 生成物长什么样(含工厂) | `web/src/notification/__client/api.ts` |
1444
- | 建表三件套 | `api/src/notification/{schema.yaml,manifest.yaml,migrations/}` |
1569
+ | 带页面的模块(路由 / i18n / order) | `src/home/web/entry.ts`、`src/demo/web/entry.ts`(模板 `packages/cli/templates/src/*/web/`) |
1570
+ | 接管通知 provider(四方法全量) | `src/notification/web/entry.ts` |
1571
+ | 上传 provider(action + headers 方法) | `src/personal-center/web/entry.ts` |
1572
+ | 无页面模块(`routes: []`) | 同 `src/notification/web/entry.ts` |
1573
+ | handler `.schema` 入参 + 平级 `.response` 出参写法(GET + POST、参数段) | `src/notification/api/api.ts`、模板 `packages/cli/templates/src/notification/api/api.ts` |
1574
+ | handler 写法(嵌套组列表 / 写入 / 403 归属) | `src/notification/api/api.ts`(模块根单文件,`read`/`read-all`/`clear` 子组) |
1575
+ | 统一异常出口 `respond()`(service 抛 `Error & {status}` → 信封 `json.fail`) | 模板 `src/_platform/api/_shared/respond.ts` + 各 handler 用法(§1.7-1) |
1576
+ | 生成物长什么样(含工厂) | `src/notification/web/__client/api.ts` |
1577
+ | 建表三件套 | `src/notification/api/{schema.yaml,manifest.yaml,migrations/}` |
1445
1578
  | 全局类型(`window.$message` 等) | 工程根 `env.d.ts` |
1446
1579
 
1447
1580
  另有:
@@ -1457,7 +1590,7 @@ export default async (path: string, html: string) => {
1457
1590
  > **oj v0.1.58 起的关键变更**:站点挂载从 `server.*` 的旧键(`api_prefix` / `app_path` /
1458
1591
  > `app_prefix` / `static_sites` / `app_spa_fallback`)迁到顶层 **`mounts:`** 扁平表
1459
1592
  > (一行=一个 URL 前缀+一个目录,`api`/`web` 恰好其一)。旧键出现即被 `oj serve` fail-fast 拒绝。
1460
- > ojm 模板 `api/config.yaml` 已落到 `mounts:`,`ojm dev/preview` 的 SPA 深链回落由 ojm 自建静态层负责,
1593
+ > ojm 模板 `config.yaml`(工程根)已落到 `mounts:`,`ojm dev/preview` 的 SPA 深链回落由 ojm 自建静态层负责,
1461
1594
  > 不需要 oj 的 spa。详细迁移规则、vars/secrets/cors/schema 用法见 `oj-api-dev` skill 手册。
1462
1595
 
1463
1596
  ---
@@ -1472,13 +1605,13 @@ export default async (path: string, html: string) => {
1472
1605
  | 类别 | 命令 | 谁实现 | 一句话 |
1473
1606
  |---|---|---|---|
1474
1607
  | **ojm 原生** | `init` / `dev` / `build` / `preview` / `api` / `info` / `vendor` / `merge` / `install` / `enable` / `disable` / `uninstall` / `trust` / `keygen` / `sign` / `publish` | `ojm` 自己 | 前后端一体工程的脚手架、开发、构建、契约生成、版本/清单、oj 分发、清单合并;方案包安装面与签名信任(install/enable/disable/uninstall 见 §11,trust/keygen/sign 见 §12,publish 见 §11.2) |
1475
- | **oj 透传** | `test` / `exec` / `openapi` / `migrate` / `schema` | `oj`(ojm 仅转发) | 这 5 个**就是 oj 的同名子命令**,ojm 只负责定位 `bin/oj` 并补上 `-c api/config.yaml` |
1608
+ | **oj 透传** | `test` / `exec` / `openapi` / `migrate` / `schema` | `oj`(ojm 仅转发) | 这 5 个**就是 oj 的同名子命令**,ojm 只负责定位 `bin/oj` 并补上 `-c config.yaml` |
1476
1609
 
1477
1610
  > **为什么只有这 5 个走透传?** oj 还有 `serve` / `build` / `secret`(`keygen`/`seal` 等)
1478
1611
  > 子命令——但它们被 `ojm` **内部调用并附带额外逻辑**(如 `ojm build` 在 `oj build`
1479
1612
  > 之后还要做前端全站合并;`ojm preview` 在 `oj migrate` 之后起 `oj serve` 单服务),
1480
1613
  > 所以不单独透传给用户,避免绕开 ojm 的工程化处理。需要原汁原味跑 oj 其它子命令时,
1481
- > 直接 `./bin/oj <sub> ...`(记得手动 `-c api/config.yaml`)。
1614
+ > 直接 `./bin/oj <sub> ...`(记得手动 `-c config.yaml`)。
1482
1615
 
1483
1616
  ### 10.1 ojm 原生命令
1484
1617
 
@@ -1490,7 +1623,9 @@ export default async (path: string, html: string) => {
1490
1623
  - `dir`:目标目录(缺省 = 当前目录 `cwd`)。
1491
1624
  - `--yes`:跳过交互确认,直接补缺。
1492
1625
 
1493
- 产物见 §1.1(`api/` 后端骨架 + `web/` 前端骨架 + `bin/`、`web.config.ts`、`tsconfig.json` 等)。
1626
+ 产物见 §1.1(工程根 `config.yaml` + `config/` 证书 + `src/<模块>/{api,web}` 单树 + `bin/`、
1627
+ `web.config.ts`、两份 tsconfig 等)。`tsconfig.paths.json` 与 `package.json` /
1628
+ `pnpm-workspace.yaml` / `.gitignore` 由 init **现场生成**(不进白名单,模板目录里那份只服务模板自身)。
1494
1629
  模板拷贝按 `packages/cli/templates/init.include.yaml` 白名单——demo-only 演示模块
1495
1630
  与模板自运行产物不在清单内,不会进工程(§1.1)。
1496
1631
  `bin/oj` 缺失时自动经 npm 包 `@oj-bin/oj` 下载(与 `ojm vendor` 同款,受
@@ -1501,7 +1636,7 @@ export default async (path: string, html: string) => {
1501
1636
 
1502
1637
  启动开发服务器:前端 devServer + 反代 oj 后端(`/api` 前缀转发到 oj)+ 模块重建 + SSE 自动刷新。
1503
1638
 
1504
- - `port`:前端端口(缺省 **5174**;oj 后端端口以 `api/config.yaml` 的 `server.port` 为准,默认 **9778**)。
1639
+ - `port`:前端端口(缺省 **5174**;oj 后端端口以工程根 `config.yaml` 的 `server.port` 为准,默认 **9778**)。
1505
1640
  - **开发模式快捷键**(仅 **TTY** 终端启用,类 Vite):
1506
1641
  - `r` 强制重建并热更新
1507
1642
  - `h` 显示帮助
@@ -1524,6 +1659,28 @@ export default async (path: string, html: string) => {
1524
1659
  > release 下只有 `routes.js` 里的路由存在,目录镜像路由不生效——所以参数路由要先
1525
1660
  > `ojm build` 再测(见 §7)。
1526
1661
 
1662
+ #### `ojm build --bundle [name]`
1663
+
1664
+ 打包**方案包**(solution bundle,可分发产物;语义细节见 §11)。只组装不构建,先 `ojm build` 再打包。
1665
+
1666
+ - `--bundle` 无参:工程根有 `solution.yaml` 则只打根契约(旧单方案行为零变化);
1667
+ 否则收集 `src/*/` 下全部**用户方案**契约逐个打包(框架自带方案自动排除,日志注明「框架自带,跳过」)。
1668
+ - `--bundle <name>` / `--bundle=<name>`:只打 `src/<name>/solution.yaml` 这一份(manifest.yaml 回退)。
1669
+ - 两段式:全部契约校验通过才开始写盘;零用户方案时报错指路 `ojm new`。
1670
+
1671
+ #### `ojm new <name> [--layout container|none] [--api]`
1672
+
1673
+ 脚手架内生成一份用户方案骨架(module-root 布局):`src/<name>/solution.yaml` +
1674
+ `web/{entry.ts,pages/,locales/}`(`--api` 再加 `api/{manifest.yaml,api.ts}`)。
1675
+ 生成即被扫描收录,补产物后 `ojm build --bundle <name>` 直接可打(round-trip)。
1676
+ 非 module-root 布局拒绝(会先要求迁移);名字撞内置方案 / 保留名 / 已存在目录都会拒绝。
1677
+ ```bash
1678
+ ojm new crm --layout container # 建 src/crm/ 骨架
1679
+ pnpm dev # 看效果
1680
+ ojm build --bundle crm # 打包
1681
+ ```
1682
+
1683
+
1527
1684
  #### `ojm preview [port]`
1528
1685
 
1529
1686
  生产形态预览(设计 §6,2026-10-19 单服务化):
@@ -1531,7 +1688,7 @@ export default async (path: string, html: string) => {
1531
1688
  1. 启动前 **fail-fast 四查**(缺 oj 二进制 / `config.yaml` / `web/dist/index.html` /
1532
1689
  `api/dist/manifests.yaml` 任一即报错并给补救指引,绝不留下 oj 孤儿);
1533
1690
  2. `oj migrate`(应用待执行迁移,非零退出即退、不起 server);
1534
- 3. 生成派生 config `api/.ojm-preview.yaml`(原 config 副本 + upsert `/` web 挂载 +
1691
+ 3. 生成派生 config `.ojm-preview.yaml`(与 `config.yaml` 同目录;原 config 副本 + upsert `/` web 挂载 +
1535
1692
  `spa: true`;显式 `port` 时覆盖 `server.port`);
1536
1693
  4. `oj serve`(release/js,**单进程同时出 API 与静态**,SPA 深链回落)。
1537
1694
 
@@ -1548,14 +1705,18 @@ export default async (path: string, html: string) => {
1548
1705
  - `dir`:项目目录(缺省 `cwd`;显式目录必须先存在,否则报错)。
1549
1706
  - `--check`:**四重对账**(只读、绝不写盘):① 生成物同步 ② route 双向对账
1550
1707
  ③ `routes.js` diff ④ WS 路由鉴权(§3.6)。
1551
- - `--docs`:聚合 OpenAPI → redoc 自包含静态文档站(单文件离线可看)。
1552
- - `--exempt <path>`:指定豁免清单,覆盖默认 `api/.ojm-api-exempt.json`(§3.8)。
1708
+ - `--docs`:聚合 OpenAPI → redoc 自包含静态文档站(离线可看),落 `<cwd>/docs/api/`
1709
+ (历史布局 `<cwd>/api/docs/`)。
1710
+ - `--exempt <path>`:指定豁免清单,覆盖默认探测顺序 `.ojm-api-exempt.json` →
1711
+ `api/.ojm-api-exempt.json` → `api/.ram-api-exempt.json`(旧名回退),§3.8。
1553
1712
 
1554
1713
  生成物重跑即覆盖(eslint 已忽略);唯一例外是带 `// ojm-api:stub` 指纹的 stub,人碰过则工具永不写删。
1555
1714
 
1556
- #### `ojm info`
1715
+ #### `ojm info [--all]`
1557
1716
 
1558
1717
  输出版本矩阵(`@oj-module/cli` / `@oj-module/runtime` / 钉版 `@plane/*` / `oj` 二进制)与模块清单。
1718
+ module-root 下数据源是「扫描 + `web.config.ts` 登记」并集(未登记的用户模块也可见)。
1719
+ 框架自带方案默认折叠为一行计数(`--all` 展开平铺)。
1559
1720
  **报障用**:拿不准「工程里到底装了什么版本、哪些模块被登记」时,先 `ojm info` 贴出来。
1560
1721
 
1561
1722
  #### `ojm vendor [tag] [--force]`
@@ -1578,14 +1739,15 @@ export default async (path: string, html: string) => {
1578
1739
  `ojm test` / `exec` / `openapi` / `migrate` / `schema` —— **它们就是 oj 的同名子命令,ojm 不重写逻辑,只做两件事**:
1579
1740
 
1580
1741
  1. **定位 `bin/oj`**:缺失给人话报错,指路 `ojm init` / `ojm vendor`;
1581
- 2. **自动补 `-c`**:当用户没给 `-c` / `--config` 且 `api/config.yaml` 存在时,自动注入
1582
- `-c <project>/api/config.yaml` 的绝对路径。
1742
+ 2. **自动补 `-c`**:当用户没给 `-c` / `--config` 且 config 存在时,自动注入它的绝对路径——
1743
+ 位置由布局决定(module-root = `<project>/config.yaml`,历史布局 = `<project>/api/config.yaml`),
1744
+ 不写死,避免布局迁移后静默读不到配置。
1583
1745
 
1584
1746
  > **oj 原生命令 vs ojm 透传的差异,仅此一点**:oj 的 `-c` 缺省是 `./config.yaml`(相对 CWD),
1585
- > 而 ojm 工程的 config 在 `api/` 下——从工程根直接 `oj test` 会读不到 config。
1747
+ > 而从子目录跑、或历史布局从工程根跑,都会读不到 config。
1586
1748
  > ojm 透传替你补上这个绝对路径,**其余参数原样转发**(含 `-d`/`--db` 等 oj 自己的选项,
1587
1749
  > ojm 不替你猜)。
1588
- > 等价关系:`ojm migrate -d api/dist` ≡ `oj migrate -c <abs>/api/config.yaml -d api/dist`。
1750
+ > 等价关系:`ojm migrate -d api/dist` ≡ `oj migrate -c <abs>/config.yaml -d api/dist`。
1589
1751
 
1590
1752
  常用示例(参数照写 oj 原版语义):
1591
1753
 
@@ -1602,7 +1764,7 @@ export default async (path: string, html: string) => {
1602
1764
  ### 10.3 本地市场链路:`ojm market` 子命令
1603
1765
 
1604
1766
  `market` 是一组**本地模拟 CDN + 市场链路**工具,专供作者在本机验证「方案包能不能发、能不能装」,
1605
- **不触碰真实生产 CDN**。所有产物落在工程根 `cdn/`(由 `api/config.yaml` 的 `/cdn` 挂载直出),
1767
+ **不触碰真实生产 CDN**。所有产物落在工程根 `cdn/`(由 `config.yaml` 的 `/cdn` 挂载直出),
1606
1768
  首跑用 `market setup` 把系统设置 `framework.market_cdn_url` 指过去即可在本地市场页看到。
1607
1769
 
1608
1770
  > 跑任何子命令前先 `ojm market setup` 一次(把模拟 CDN 地址写进站点配置)。
@@ -1627,22 +1789,44 @@ export default async (path: string, html: string) => {
1627
1789
  ## §11 方案包:打包与分发
1628
1790
 
1629
1791
  「站点级构建」见 §7;本节讲**方案包**(solution bundle)——单方案的可分发产物与到达站点的方式。
1792
+ 一个脚手架可放多份用户方案(`ojm new` 逐个生成,各自一份 `src/<name>/solution.yaml`),
1793
+ `ojm build --bundle` 无参时全部打包、逐个校验独立出包;框架自带方案自动排除(名单见
1794
+ `src/.ojm-builtin.json`,`ojm info` 默认折叠自带模块)。
1795
+ **从开发到发布的完整过程视角(含签名与私钥保存纪律)见**
1796
+ [`docs/guides/solution-dev-build-publish.md`](../guides/solution-dev-build-publish.md)。
1630
1797
 
1631
1798
  ### 11.1 打包:`ojm build --bundle`
1632
1799
 
1633
1800
  ```bash
1634
- ojm build --bundle # --out <dir> 缺省 dist/
1635
- # [ojm] 方案包已生成 → dist/<name>-<version>.tgz
1801
+ ojm build --bundle # --out <dir> 缺省工程根 dist/(方案包输出目录,与后端产物 api/dist 无关)
1802
+ ojm build --bundle crm # 只打 src/crm/ 这一份;--bundle=crm 等价
1803
+ # [ojm] 方案包已生成 → dist/<name>-<version>.tgz(多份用户方案时逐份输出,共打包 N 份)
1636
1804
  # [ojm] sha256:…(确定性打包:同源同字节)
1805
+ # 没有可打包的用户方案时:报错指向 ojm new(不静默产出空批次)
1637
1806
  ```
1638
1807
 
1639
- - 打包即校验(fail-fast,坏清单出不了包):方案 manifest 契约校验(字段齐全 / 类型白名单 /
1640
- 保留名 / 键空间 → 可读违规清单)→ 文件面校验(`api.tables` ↔ `schema.yaml`、locales 双语齐备)。
1808
+ > `--bundle` 只**组装**,不重跑构建:入包内容取自 `api/dist/<name>-<version>/` 与
1809
+ > `web/dist/modules/<name>/`——先 `ojm build` 再打包。
1810
+
1811
+ - **契约入口(按优先级)**:`src/<模块>/solution.yaml` ≻ 工程根 `solution.yaml` ≻
1812
+ `src/<模块>/manifest.yaml` ≻ 工程根 `manifest.yaml`(方案级;`manifest.yaml` 为旧方案回退)。
1813
+ **单模块方案推荐把契约放在 `src/<模块>/solution.yaml`**——契约与它所打包的模块同位,最直观。
1814
+ 文件名刻意避开 `manifest.yaml`:oj 按 `manifest.yaml` 枚举发现 api 后端模块(§1.2),工程根若放一份
1815
+ bundle 契约会污染那条搜索枚举;故用 `solution.yaml` 隔开两条枚举——**后端按 `manifest.yaml` 枚举 api、
1816
+ 打包按 `solution.yaml` 取契约**,不再冲突。模块级契约同样**只认 `src/<模块>/` 根下、绝不进 `<模块>/api/`**,
1817
+ 否则会与 oj 的 api 枚举撞车。tgz 内固定写回 `manifest.yaml`(安装侧只认这个名字)。
1818
+ - **两段式**:先对全部目标契约跑完校验(纯读零写盘),全绿才开始逐个写 tgz——任一校验失败
1819
+ dist/ 零增量,报错指名失败方案(发布者不会把半截批次扫进 CDN 索引)。校验项(fail-fast,
1820
+ 坏清单出不了包):方案 solution 契约(字段齐全 / 类型白名单 / 保留名 / 键空间 → 可读违规清单)
1821
+ → 文件面(`api.tables` ↔ `schema.yaml`、locales 双语齐备)→ 模块级契约 `name` 必须 === 目录名。
1822
+ - 框架自带方案不参与打包:`src/.ojm-builtin.json` 名单内的模块契约被跳过(逐条日志);
1823
+ 该文件由 `ojm init` 从模板白名单派生,**可手改/删除**(删名 = 恢复参与打包),重跑 init 缺失才补写。
1824
+ 它只是打包排除 / info 折叠提示,**不是安全边界**:文件永不入包,安装侧信任链(签名 + 信任库 + TOFU)与它无关。
1641
1825
  - **只出包不签名**——签名见 §12(关注点分离:作者签名是显式动作,不是构建副产品)。
1642
1826
  - 产物内 `manifest.yaml` 的 `archive` 段是 **Form A**(仅 `file`);`sha256`/`size` 由
1643
1827
  file 源上传时点补齐(宿主安装器)或发布通道 Form B 自带。
1644
- - **入包红线**:`*.pem` / `*.key` / `.env` / `id_ed25519` / `id_rsa` 等私钥形态文件禁入包
1645
- (与 `modules.json` 禁入同款自检)——私钥入包即信任根基烧毁。
1828
+ - **入包红线**:`*.pem` / `*.key` / `*.p12` / `*.pfx` / `.env` 系 / `id_ed25519` / `id_rsa`
1829
+ 等私钥形态文件禁入包(与 `modules.json` 禁入同款自检,0.2.23 起扩面)——私钥入包即信任根基烧毁。
1646
1830
 
1647
1831
  ### 11.2 两种分发源
1648
1832
 
@@ -1667,7 +1851,7 @@ Form B 索引规范(宿主侧强制,防索引投毒):`versions[].file`
1667
1851
  (需 superadmin)→ 十阶段状态机(§7 verify 闸语义的方案版,失败语义见下图)→
1668
1852
  任务记录 `.install-tasks/<id>.json`(保留 10 条,800ms 轮询消费面)。市场页族端点
1669
1853
  (install/task/index/meta/trust/uninstall…)已全量登记 openapi.json,
1670
- 前端经 `web/src/_platform/__client` 消费。
1854
+ 前端经 `src/_platform/web/__client` 消费。
1671
1855
 
1672
1856
  十阶段(阶段名如下)+ 失败语义:
1673
1857
 
@@ -1780,7 +1964,7 @@ ojm trust revoke <key_id> # 吊销:阻断新装/升级,不动已装
1780
1964
 
1781
1965
  ### 12.3 第一方 bootstrap key(首装免 TOFU)
1782
1966
 
1783
- 平台模板内置第一方发布方**公钥**(`api/src/_platform/market/bootstrap.ts` 的
1967
+ 平台模板内置第一方发布方**公钥**(`src/_platform/api/market/bootstrap.ts` 的
1784
1968
  `BOOTSTRAP_KEY`):信任库文件首次初始化时自动播种一次——**market 方案首装免 TOFU 直接过
1785
1969
  验签**。发布时序(平台发行人执行):`ojm keygen` → 签 oj-market 首发包 → 公钥 + key_id
1786
1970
  回填 `BOOTSTRAP_KEY` 随模板发布。播种纪律:库已初始化(含空库)不播种——运维 `revoke`
@@ -1864,7 +2048,7 @@ export default defineModule({
1864
2048
 
1865
2049
  | 章 | 内容 | 什么时候读 |
1866
2050
  |---|---|---|
1867
- | §1 工程结构 | 脚手架产物、两个模块体系、目录镜像路由、重启边界、建表三件套、迁移与 seed | 新人第一遍;改后端目录结构 |
2051
+ | §1 工程结构 | 脚手架产物(module-root 单树)、两处产物根、两个模块体系、目录镜像路由、重启边界、建表三件套、迁移与 seed、handler 四红线、`#` 别名与 TS 投影 | 新人第一遍;改后端目录结构 |
1868
2052
  | §2 写前端模块 | 目录、两步注册、entry.ts 形状、路由 handle、import 红线、共享依赖钉版 | 加页面 / 加模块 |
1869
2053
  | §3 契约与代码生成 | `.schema` 入参 + 平级 `.response` 出参、生成物契约、命令、豁免清单、DDD 判据、**§3.10 旧 contract.ts 工程迁移** | 加接口;从旧工程迁移 |
1870
2054
  | §4 接管框架内置功能 | provider 机制、通知 / 上传 / 菜单三案例 | 接管通知铃、头像上传、动态菜单 |