@oj-module/cli 0.2.54 → 0.2.56

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 (289) hide show
  1. package/devkit/SKILL.md +3 -3
  2. package/devkit/manual.md +59 -52
  3. package/package.json +2 -2
  4. package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
  5. package/shell-dist/assets/runtime.js +2 -2
  6. package/shell-dist/index.html +3 -3
  7. package/shell-dist/versions.json +3 -3
  8. package/src/__tests__/layout.test.ts +30 -1
  9. package/src/__tests__/templates-schema-columns.test.ts +99 -0
  10. package/src/alias-paths.ts +11 -12
  11. package/src/builtin.ts +10 -3
  12. package/src/contract/__tests__/emit-client.test.ts +4 -4
  13. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  14. package/src/contract/__tests__/run-api.test.ts +2 -2
  15. package/src/contract/check.ts +6 -2
  16. package/src/contract/emit-client.ts +10 -3
  17. package/src/contract/run.ts +10 -2
  18. package/src/init.ts +19 -11
  19. package/src/layout.ts +72 -16
  20. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  21. package/src/market/__tests__/bootstrap.test.ts +1 -1
  22. package/src/site.ts +1 -1
  23. package/templates/README.md +30 -20
  24. package/templates/init.include.yaml +19 -11
  25. package/templates/openapi.json +95 -86
  26. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
  27. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
  28. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
  29. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
  30. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
  31. package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +5 -0
  32. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
  33. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
  34. package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
  35. package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +5 -2
  36. package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +5 -2
  37. package/templates/src/app/web/notification/pages/center/index.tsx +1 -1
  38. package/templates/src/app/web/notification/routes.ts +1 -1
  39. package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +1 -1
  40. package/templates/src/app/web/personal-center/routes.ts +1 -1
  41. package/templates/src/app/web/system/__client/api.schemas.ts +1 -0
  42. package/templates/src/app/web/system/locales/en-US.json +2 -3
  43. package/templates/src/app/web/system/locales/zh-CN.json +2 -3
  44. package/templates/src/app/web/system/pages/departments/index.tsx +2 -2
  45. package/templates/src/app/web/system/pages/login-methods/index.tsx +1 -1
  46. package/templates/src/app/web/system/pages/oplog/index.tsx +44 -19
  47. package/templates/src/app/web/system/pages/roles/index.tsx +5 -3
  48. package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
  49. package/templates/src/app/web/system/pages/settings/index.tsx +2 -2
  50. package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
  51. package/templates/src/app/web/system/pages/users/index.tsx +25 -17
  52. package/templates/src/app/web/system/pages/users/roles.tsx +1 -1
  53. package/templates/src/app/web/system/routes.ts +1 -1
  54. package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +5 -2
  55. package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
  56. package/templates/tests/auth-ldap.spec.ts +2 -2
  57. package/templates/tests/auth-session.spec.ts +3 -3
  58. package/templates/tests/auth-throttle.spec.ts +1 -1
  59. package/templates/tests/fixture.ts +6 -4
  60. package/templates/tests/guard.spec.ts +1 -1
  61. package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
  62. package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
  63. package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
  64. package/templates/tests/lifecycle/installer.spec.ts +10 -10
  65. package/templates/tests/lifecycle/market-api.spec.ts +14 -14
  66. package/templates/tests/lifecycle/migrate.spec.ts +6 -6
  67. package/templates/tests/lifecycle/preflight.spec.ts +3 -3
  68. package/templates/tests/lifecycle/register.spec.ts +1 -1
  69. package/templates/tests/lifecycle/semver.spec.ts +1 -1
  70. package/templates/tests/lifecycle/sha384.spec.ts +1 -1
  71. package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
  72. package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
  73. package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
  74. package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
  75. package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
  76. package/templates/tests/lifecycle/trust.spec.ts +5 -5
  77. package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
  78. package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
  79. package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
  80. package/templates/tests/org-departments.spec.ts +10 -6
  81. package/templates/tests/org-read.spec.ts +7 -7
  82. package/templates/tests/query-utils.spec.ts +1 -1
  83. package/templates/tests/rbac-audit.spec.ts +1 -1
  84. package/templates/tests/rbac-contacts.spec.ts +2 -2
  85. package/templates/tests/rbac-guard.spec.ts +1 -1
  86. package/templates/tests/rbac-roles.spec.ts +10 -2
  87. package/templates/tests/rbac-seed.spec.ts +1 -1
  88. package/templates/tests/rbac-settings.spec.ts +77 -7
  89. package/templates/tests/rbac-solution-perms.spec.ts +1 -1
  90. package/templates/tests/rbac-tables.spec.ts +29 -0
  91. package/templates/tests/rbac-users.spec.ts +55 -3
  92. package/templates/tests/roles-page.spec.ts +40 -0
  93. package/templates/tests/seed-role-accounts.spec.ts +1 -1
  94. package/templates/tests/tenant-access-cache.spec.ts +1 -1
  95. package/templates/tests/tenant-forgery.spec.ts +5 -5
  96. package/templates/tests/tenant-isolation.spec.ts +5 -4
  97. package/templates/tests/username-namespace.spec.ts +3 -3
  98. package/templates/tests/users-page.spec.ts +39 -0
  99. package/templates/tests/wecom-admin.spec.ts +6 -6
  100. package/templates/tests/wecom-client-timeout.spec.ts +1 -1
  101. package/templates/tests/wecom-connector.spec.ts +9 -9
  102. package/templates/tests/wecom-corp.spec.ts +9 -9
  103. package/templates/tests/wecom-crypt.spec.ts +1 -1
  104. package/templates/tests/wecom-endpoints.spec.ts +2 -2
  105. package/templates/tests/wecom-landing.spec.ts +1 -1
  106. package/templates/tests/wecom-org-sync.spec.ts +7 -7
  107. package/templates/tests/wecom-sync.spec.ts +3 -3
  108. package/templates/tsconfig.paths.json +11 -11
  109. package/templates/vitest.config.ts +2 -2
  110. package/templates/web.config.ts +5 -2
  111. package/vendor/host-versions.json +3 -3
  112. package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
  113. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
  114. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
  115. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +0 -0
  116. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
  117. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
  118. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
  119. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
  120. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
  121. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
  122. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
  123. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
  124. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
  125. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
  126. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
  127. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +0 -0
  128. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
  129. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
  130. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
  131. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
  132. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
  133. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
  134. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
  135. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
  136. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
  137. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
  138. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
  139. package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
  140. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
  141. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
  142. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
  143. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +0 -0
  144. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
  145. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +0 -0
  146. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
  147. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
  148. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
  149. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
  150. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
  151. package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
  152. package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +0 -0
  153. package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +0 -0
  154. package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
  155. package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
  156. package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
  157. package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
  158. package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
  159. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
  160. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
  161. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
  162. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  163. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
  164. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  165. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
  166. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
  167. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
  168. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
  169. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
  170. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
  171. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  172. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
  173. package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
  174. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  175. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  176. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
  177. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
  178. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
  179. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
  180. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  181. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
  182. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
  183. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  184. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
  185. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
  186. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
  187. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
  188. package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
  189. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
  190. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
  191. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
  192. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
  193. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
  194. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
  195. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
  196. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
  197. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
  198. package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
  199. package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
  200. package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
  201. package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
  202. package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
  203. package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
  204. package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
  205. package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
  206. package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
  207. package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
  208. package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +0 -0
  209. package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
  210. package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
  211. package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
  212. package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
  213. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
  214. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
  215. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
  216. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
  217. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
  218. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
  219. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
  220. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
  221. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
  222. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
  223. package/templates/src/{auth/api → app/api/auth}/api.ts +0 -0
  224. package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +0 -0
  225. package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
  226. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
  227. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
  228. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
  229. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
  230. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
  231. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
  232. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
  233. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
  234. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
  235. package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
  236. package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
  237. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
  238. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
  239. /package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +0 -0
  240. /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
  241. /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
  242. /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
  243. /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
  244. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
  245. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
  246. /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
  247. /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
  248. /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
  249. /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
  250. /package/templates/src/{notification/api → app/api/notification}/api.ts +0 -0
  251. /package/templates/src/{notification/api → app/api/notification}/del/api.ts +0 -0
  252. /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
  253. /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
  254. /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
  255. /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
  256. /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  257. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
  258. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
  259. /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
  260. /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
  261. /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
  262. /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
  263. /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
  264. /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
  265. /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
  266. /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
  267. /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
  268. /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
  269. /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
  270. /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
  271. /package/templates/src/{system/api → app/api/system}/roles/api.ts +0 -0
  272. /package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +0 -0
  273. /package/templates/src/{system/api → app/api/system}/settings/api.ts +0 -0
  274. /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
  275. /package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +0 -0
  276. /package/templates/src/{system/api → app/api/system}/users/password/api.ts +0 -0
  277. /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
  278. /package/templates/src/{system/api → app/api/system}/users/roles/api.ts +0 -0
  279. /package/templates/src/{web/api → app/api/web}/get-async-routes/api.ts +0 -0
  280. /package/templates/src/{web/api → app/api/web}/hello/api.ts +0 -0
  281. /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
  282. /package/templates/src/{web/api → app/api/web}/user-info/api.ts +0 -0
  283. /package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +0 -0
  284. /package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +0 -0
  285. /package/templates/src/app/web/home/__client/{api.ts → index.ts} +0 -0
  286. /package/templates/src/app/web/notification/__client/{api.ts → index.ts} +0 -0
  287. /package/templates/src/app/web/personal-center/__client/{api.ts → index.ts} +0 -0
  288. /package/templates/src/app/web/system/__client/{api.ts → index.ts} +0 -0
  289. /package/templates/src/{web → app/web}/web/__client/api.schemas.ts +0 -0
@@ -20,18 +20,18 @@ import type { ProjectLayout } from "./layout";
20
20
  import fs from "node:fs";
21
21
  import path from "node:path";
22
22
  import { parse } from "yaml";
23
- import { tryResolveLayout } from "./layout";
23
+ import { scanModules, tryResolveLayout } from "./layout";
24
24
 
25
25
  /** 生成物文件名(工程根)。`src/tsconfig.json` 以 `../tsconfig.paths.json` 引用 */
26
26
  export const ALIAS_PATHS_FILE = "tsconfig.paths.json";
27
27
 
28
28
  /** 生成物头注释:tsconfig 允许注释,且这是唯一会被 `ojm` 覆写的 tsconfig */
29
29
  const HEADER = [
30
- "// 由 ojm 生成:ojm init / ojm build / ojm dev 会按 src/*/api/manifest.yaml 重写本文件,请勿手改。",
30
+ "// 由 ojm 生成:ojm init / ojm build / ojm dev 会按模块清单重写本文件,请勿手改。",
31
31
  "//",
32
- "// oj 别名 `#/<模块名>/…` 锚在目标模块根(本布局下即 src/<模块名>/api)。tsconfig 的",
33
- "// paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*` 会被 TS",
34
- "// 以 TS5062 拒掉),故按模块逐条枚举。",
32
+ "// oj 别名 `#/<模块名>/…` 锚在目标模块根(src/<模块名>/api 或 src/app/api/<模块名>)。",
33
+ "// tsconfig 的 paths 只能静态全局映射,且每条规则的替换值只允许一个 `*`(`./src/*/api/*`",
34
+ "// 会被 TS 以 TS5062 拒掉),故按模块逐条枚举。",
35
35
  ].join("\n");
36
36
 
37
37
  /** 读模块根 manifest 的 `name`(oj 的模块身份);读不到/格式坏时回落到目录名 */
@@ -58,14 +58,13 @@ export function aliasPathEntries(layout: ProjectLayout): Record<string, string[]
58
58
  if (layout.kind !== "module-root")
59
59
  return {};
60
60
  const entries: Record<string, string[]> = {};
61
- for (const mod of fs.readdirSync(layout.modulesSrc, { withFileTypes: true })) {
62
- if (!mod.isDirectory() || mod.name.startsWith("."))
61
+ // 以扫描结果为准:既覆盖平铺 src/<name>/api,也覆盖聚合容器 src/app/api/<name>
62
+ for (const mod of scanModules(layout)) {
63
+ if (!mod.apiSrc)
63
64
  continue;
64
- const apiDir = path.join(layout.modulesSrc, mod.name, "api");
65
- if (!fs.existsSync(path.join(apiDir, "manifest.yaml")))
66
- continue;
67
- const name = manifestName(apiDir, mod.name);
68
- entries[`#/${name}/*`] = [`./${path.posix.join("src", mod.name, "api", "*")}`];
65
+ const name = manifestName(mod.apiSrc, mod.name);
66
+ const rel = path.relative(layout.projectRoot, mod.apiSrc).split(path.sep).join("/");
67
+ entries[`#/${name}/*`] = [`./${rel}/*`];
69
68
  }
70
69
  return entries;
71
70
  }
package/src/builtin.ts CHANGED
@@ -64,9 +64,16 @@ export function readBuiltinModules(projectRoot: string): Set<string> {
64
64
  export function deriveBuiltinModules(include: Iterable<string>): Set<string> {
65
65
  const mods = new Set<string>();
66
66
  for (const p of include) {
67
- const m = /^src\/([^/]+)\/(?:api|web)(?:\/|$)/.exec(p)?.[1];
68
- if (m)
69
- mods.add(m);
67
+ // 聚合容器下的后端模块:src/app/api/<name>(模块名取 api 的下一段,不是容器名)
68
+ const nested = /^src\/[^/]+\/api\/([^/]+)(?:\/|$)/.exec(p)?.[1];
69
+ if (nested) {
70
+ mods.add(nested);
71
+ continue;
72
+ }
73
+ // 平铺后端 src/<name>/api 与前端 src/<name>/web/...
74
+ const flat = /^src\/([^/]+)\/(?:api|web)(?:\/|$)/.exec(p)?.[1];
75
+ if (flat)
76
+ mods.add(flat);
70
77
  }
71
78
  return mods;
72
79
  }
@@ -72,7 +72,7 @@ function emit() {
72
72
  }
73
73
 
74
74
  describe("emitClient (module target)", () => {
75
- const { "api.ts": apiTs, "api.schemas.ts": schemasTs } = emit();
75
+ const { "index.ts": apiTs, "api.schemas.ts": schemasTs } = emit();
76
76
 
77
77
  it("aPI_PREFIX / MODULE_PREFIX 用 JSON 字符串字面量(注入安全)", () => {
78
78
  expect(apiTs).toContain("export const API_PREFIX = \"/wiki\";");
@@ -114,7 +114,7 @@ describe("emitClient (module target)", () => {
114
114
  describe("emitClient 注入安全:模块名含特殊字符不破坏源码", () => {
115
115
  it("module 名含 -/$ 时 MODULE_PREFIX/工厂名仍走字符串字面量(API_PREFIX 取 IR wirePrefix)", () => {
116
116
  const ir = irFromOpenapi(DOC, { base: "/v1/api" });
117
- const { "api.ts": apiTs } = emitClient(ir, { target: "module", module: "weird-name" });
117
+ const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "weird-name" });
118
118
  // module 选项只影响 MODULE_PREFIX 与 create<Module>Client 名
119
119
  expect(apiTs).toContain("export const MODULE_PREFIX = \"/weird-name\";");
120
120
  expect(apiTs).toContain("export function createWeirdNameClient(");
@@ -128,7 +128,7 @@ describe("emitClient 注入安全:模块名含特殊字符不破坏源码", ()
128
128
  describe("emitClient 多根聚合归属(owner)", () => {
129
129
  it("owner 与 module 不同 → 落 MODULE_OWNER,护栏对比归属模块;MODULE_PREFIX 仍是 api 前缀", () => {
130
130
  const ir = irFromOpenapi(DOC, { base: "/v1/api" });
131
- const { "api.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "app" });
131
+ const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "app" });
132
132
  expect(apiTs).toContain("export const MODULE_PREFIX = \"/system\";");
133
133
  expect(apiTs).toContain("export const MODULE_OWNER = \"/app\";");
134
134
  expect(apiTs).toContain("ctx.module.name !== MODULE_OWNER.slice(1)");
@@ -137,7 +137,7 @@ describe("emitClient 多根聚合归属(owner)", () => {
137
137
 
138
138
  it("owner 缺省 = module → 生成物与历史逐字一致(不出现 MODULE_OWNER)", () => {
139
139
  const ir = irFromOpenapi(DOC, { base: "/v1/api" });
140
- const { "api.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "system" });
140
+ const { "index.ts": apiTs } = emitClient(ir, { target: "module", module: "system", owner: "system" });
141
141
  expect(apiTs).not.toContain("MODULE_OWNER");
142
142
  expect(apiTs).toContain("ctx.module.name !== MODULE_PREFIX.slice(1)");
143
143
  });
@@ -1,4 +1,4 @@
1
- // ScopedRequestLike 真源在 runtime contract(生成物 __client/api.ts 也从这里 import,
1
+ // ScopedRequestLike 真源在 runtime contract(生成物 __client/index.ts 也从这里 import,
2
2
  // 自身不再 re-export——旧写法 type import 自 __client 会在再生后 TS2305)。
3
3
  import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
4
4
 
@@ -9,7 +9,7 @@ import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
9
9
  // 不加载 @oj-module/runtime 重依赖(antd / pro-components 在 node 环境无法 import),仅桩 request、
10
10
  // 透传真实 zod(供生成 client 的 DEV 响应校验走真实路径)。ContractApiError 由轻量子路径提供。
11
11
  import { beforeEach, describe, expect, it, vi } from "vitest";
12
- import { bindRequest } from "../../../templates/src/_platform/web/__client/api";
12
+ import { bindRequest } from "../../../templates/src/app/web/_platform/__client";
13
13
  import { httpMarketClient } from "../../../templates/src/oj-market/web/api/defaultClient";
14
14
 
15
15
  // index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
@@ -46,9 +46,9 @@ describe("runApi 消费 openapi.json(新口径 Task #4)", () => {
46
46
  const result = await runApi({ cwd: dir, openapi: "openapi.json" });
47
47
  expect(result.contracts).toBe(2);
48
48
 
49
- const wikiApi = join(dir, "web/src/wiki/__client/api.ts");
49
+ const wikiApi = join(dir, "web/src/wiki/__client/index.ts");
50
50
  const wikiSchemas = join(dir, "web/src/wiki/__client/api.schemas.ts");
51
- const userApi = join(dir, "web/src/user/__client/api.ts");
51
+ const userApi = join(dir, "web/src/user/__client/index.ts");
52
52
  expect(existsSync(wikiApi)).toBe(true);
53
53
  expect(existsSync(wikiSchemas)).toBe(true);
54
54
  expect(existsSync(userApi)).toBe(true);
@@ -307,17 +307,21 @@ async function checkFromOpenapi(openapiPath: string, cwd: string, _exemption: Ap
307
307
  const violations: CheckViolation[] = [];
308
308
  const layout = tryResolveLayout(cwd);
309
309
  // 契约工具的扫描单元是「模块 api 根」而不是整棵源码树:module-root 布局里
310
- // `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/api.ts` 不能被当 handler。
310
+ // `src/<name>/api` 的 `api` 段不属于路由,且 `web/__client/index.ts` 不能被当 handler。
311
311
  const roots = layout ? apiModuleRoots(layout) : legacyApiModuleRoots(cwd);
312
312
  const owners = await loadClientOwners(layout?.projectRoot ?? cwd);
313
313
  for (const [module, eps] of groupIrByModule(ir)) {
314
314
  const client = emitClient(eps, { target: "module", module, owner: owners[module] });
315
315
  const clientDir = clientDirOf(layout, cwd, module, owners);
316
- for (const [file, content] of [["api.ts", client["api.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
316
+ for (const [file, content] of [["index.ts", client["index.ts"]], ["api.schemas.ts", client["api.schemas.ts"]]] as const) {
317
317
  const p = join(clientDir, file);
318
318
  if (!existsSync(p) || readFileSync(p, "utf8") !== content)
319
319
  violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 生成物过期或缺失:${relative(cwd, p)}——请重跑 ojm api 使其与 openapi.json 同步。`, filePath: p });
320
320
  }
321
+ // 旧生成物(0.2.54 前叫 api.ts):不删会被 oj dev 当路由扫描并因 import 逃逸报错
322
+ const legacy = join(clientDir, "api.ts");
323
+ if (existsSync(legacy))
324
+ violations.push({ level: "error", kind: "artifact-stale", message: `[ojm-api] 旧生成物仍在:${relative(cwd, legacy)}——入口已改为 index.ts,请重跑 ojm api(会删除旧文件)。`, filePath: legacy });
321
325
  }
322
326
  const apiSrcDir = layout ? apiModulesSrc(layout) : join(cwd, "api/src");
323
327
  if (existsSync(apiSrcDir)) {
@@ -357,8 +357,15 @@ ${entries.join("\n")}
357
357
  `;
358
358
  }
359
359
 
360
- /** 发射双产物:api.ts(类型 + 请求函数 + 工厂)与 api.schemas.ts(DEV 校验 schema) */
361
- export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "internal", module?: string, /** client 归属模块(多根聚合);缺省 = module */ owner?: string }): { "api.ts": string, "api.schemas.ts": string } {
360
+ /**
361
+ * 发射双产物:index.ts(类型 + 请求函数 + 工厂)与 api.schemas.ts(DEV 校验 schema)。
362
+ *
363
+ * 入口文件名是 `index.ts` 而非 `api.ts`:oj dev 的 `--module-root` 扫描会把模块目录下
364
+ * **任何 `api.ts`** 当路由文件导入,聚合模块(src/app 同时有 api/ 与 web/)里的
365
+ * `web/<root>/__client/index.ts` 会被误扫(import 逃逸 project root 直接报错)。
366
+ * `index.ts` 不在扫描名单内,且 `from "../__client"` 的导入更短。
367
+ */
368
+ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "internal", module?: string, /** client 归属模块(多根聚合);缺省 = module */ owner?: string }): { "index.ts": string, "api.schemas.ts": string } {
362
369
  if (ir.length === 0)
363
370
  throw new Error("[ojm-api] client 发射失败:IR 为空——openapi.json 里没有任何端点(operationId + tags),无需生成;请检查 \`oj openapi\` 产物。");
364
371
  if (opts.target === "module" && !opts.module) {
@@ -386,7 +393,7 @@ export function emitClient(ir: IrEndpoint[], opts: { target: "module" | "interna
386
393
  sections.push(emitEndpoint(ep));
387
394
  }
388
395
  return {
389
- "api.ts": `${sections.join("\n\n")}\n`,
396
+ "index.ts": `${sections.join("\n\n")}\n`,
390
397
  "api.schemas.ts": emitSchemas(ir, opts.target),
391
398
  };
392
399
  }
@@ -2,7 +2,7 @@ import type { OjSchemaDiag } from "./emit-oj-schema";
2
2
  import type { IrEndpoint } from "./ir";
3
3
  import type { OpenApiDoc } from "./openapi-ir";
4
4
  import { execFile as execFileCb } from "node:child_process";
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import { basename, dirname, join } from "node:path";
8
8
  import process from "node:process";
@@ -89,8 +89,16 @@ async function runFromOpenapi(openapiPath: string, cwd: string, result: RunResul
89
89
  for (const [module, eps] of byModule) {
90
90
  const client = emitClient(eps, { target: "module", module, owner: owners[module] });
91
91
  const clientDir = clientDirOf(layout, cwd, module, owners);
92
- writeIfChanged(join(clientDir, "api.ts"), client["api.ts"], result);
92
+ writeIfChanged(join(clientDir, "index.ts"), client["index.ts"], result);
93
93
  writeIfChanged(join(clientDir, "api.schemas.ts"), client["api.schemas.ts"], result);
94
+ // 迁移:0.2.54 起入口文件从 api.ts 改为 index.ts。旧文件必须删——oj dev 的
95
+ // --module-root 扫描会把模块目录下任何 api.ts 当路由导入(聚合模块的
96
+ // web/<root>/__client/index.ts 会直接报 import 逃逸)。
97
+ const legacy = join(clientDir, "api.ts");
98
+ if (existsSync(legacy)) {
99
+ rmSync(legacy);
100
+ result.written.push(legacy);
101
+ }
94
102
  }
95
103
  // stub:oj 目录镜像树(人碰过的文件永不写——plan 阶段已按指纹判好)。
96
104
  // 已实现键全量扫一遍传入:嵌套组(模块根单文件)实现的端点不再生成镜像 stub。
package/src/init.ts CHANGED
@@ -240,15 +240,22 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
240
240
  // 与 pnpm-workspace.yaml 同型,改由 init 现场生成
241
241
  ensureGitignore(destDir, report);
242
242
 
243
- // 5d) 旧布局冲突提示:0.2.54 起 home/login/personal-center/notification/system 的
244
- // web 侧聚合成 src/app/web 多根模块。init 只补缺不删除——老工程重跑 init 会
245
- // 同时留下旧目录,扫描后同名路由注册两遍(菜单重复、行为不一致)。
246
- const mergedEntry = path.join(destDir, "src", "app", "web", "entry.ts");
247
- if (fs.existsSync(mergedEntry)) {
248
- const leftovers = ["home", "login", "personal-center", "notification", "system"]
249
- .filter(name => fs.existsSync(path.join(destDir, "src", name, "web", "entry.ts")));
250
- if (leftovers.length > 0)
251
- report.warnings.push(`检测到旧布局的前端模块目录(${leftovers.map(n => `src/${n}/web`).join("、")})与新的聚合模块 src/app/web 并存——路由会重复注册。请手工删除这些旧 web 目录(其 api 侧保留)。`);
243
+ // 5d) 旧布局冲突提示:0.2.54 起因/app 同时承载前端聚合模块与后端模块。
244
+ // init 只补缺不删除——老工程重跑 init 会同时留下旧目录:
245
+ // · src/<name>/web 与 src/app/web 同名路由注册两遍(菜单重复);
246
+ // · src/<name>/api 与 src/app/api/<name> 模块名重复(ojm 扫描直接报错)。
247
+ const mergedWeb = path.join(destDir, "src", "app", "web", "entry.ts");
248
+ const mergedApi = path.join(destDir, "src", "app", "api");
249
+ if (fs.existsSync(mergedWeb) || fs.existsSync(mergedApi)) {
250
+ const appModules = ["home", "login", "personal-center", "notification", "system", "_platform", "auth", "web"];
251
+ const oldWeb = appModules.filter(name => fs.existsSync(path.join(destDir, "src", name, "web", "entry.ts")));
252
+ if (oldWeb.length > 0)
253
+ report.warnings.push(`检测到旧布局的前端模块目录(${oldWeb.map(n => `src/${n}/web`).join("、")})与新聚合模块 src/app/web 并存——路由会重复注册。请手工删除这些旧 web 目录。`);
254
+ const oldApi = appModules.filter(name =>
255
+ fs.existsSync(path.join(destDir, "src", name, "api", "manifest.yaml"))
256
+ && fs.existsSync(path.join(mergedApi, name, "manifest.yaml")));
257
+ if (oldApi.length > 0)
258
+ report.warnings.push(`检测到旧布局的后端模块目录(${oldApi.map(n => `src/${n}/api`).join("、")})与新位置 src/app/api/<name> 同名——模块名重复会让 ojm 扫描报错。请手工删除这些旧 api 目录。`);
252
259
  }
253
260
 
254
261
  console.log(`[ojm] init 完成:${destDir}`);
@@ -411,9 +418,10 @@ function pruneUnincludedApiPaths(destDir: string, include: Set<string>, report:
411
418
  // 只裁本次新建的 openapi.json:init 是幂等补缺,不改动既有工程文件
412
419
  if (!report.created.includes("openapi.json"))
413
420
  return;
414
- // 白名单里 `src/<name>/api` 这条(整模块收录)决定「脚手架有哪些后端模块」
421
+ // 白名单里的 api 侧条目决定「脚手架有哪些后端模块」:
422
+ // 平铺 `src/<name>/api` 与聚合容器 `src/app/api/<name>` 都要认。
415
423
  const mods = new Set([...include]
416
- .map(p => /^src\/([^/]+)\/api$/.exec(p)?.[1])
424
+ .map(p => /^src\/([^/]+)\/api$/.exec(p)?.[1] ?? /^src\/[^/]+\/api\/([^/]+)$/.exec(p)?.[1])
417
425
  .filter((m): m is string => !!m));
418
426
  if (mods.size === 0)
419
427
  return;
package/src/layout.ts CHANGED
@@ -6,13 +6,16 @@
6
6
  *
7
7
  * 三种布局(互斥):
8
8
  * module-root <project>/src/<name>/{api,web} 新布局:一目录一模块(设计 v2)
9
+ * 也支持聚合容器:src/app/{api/<模块>,web}——app 的 web 侧是多根前端模块,
10
+ * api 侧每个子目录各自是独立后端模块(模块名 = 子目录名 = URL 前缀)
9
11
  * new <project>/web/src/<name> 历史 uni-dev 布局
10
12
  * flat <project>/web/<name> 框架根仓 dogfooding(产物落 build/)
11
13
  *
12
14
  * module-root 的判定只认「模块根标记文件」而不是单纯的 src/ 存在性:工程里
13
15
  * 常有不属于模块的 src/(脚本、共享库),凭目录名猜会误判。
14
16
  * `src/<x>/api/manifest.yaml` 与 `src/<x>/web/entry.ts` 是唯一真源(设计 C-6:
15
- * 一侧可缺省,两侧皆无则不是模块)。
17
+ * 一侧可缺省,两侧皆无则不是模块)。聚合容器下后端模块根是 `src/app/api/<x>/manifest.yaml`
18
+ * (见 collectApiRoots)。
16
19
  *
17
20
  * 产物根(2026-10-22 起收进工程根单一 dist/,原 {web,api}/dist 与源码混树):
18
21
  * dist/api oj 扁平 release 根(<name>-<version>/ 同层兄弟 + manifests.yaml),三种布局一致
@@ -31,7 +34,7 @@ export type LayoutKind = "module-root" | "new" | "flat";
31
34
  /** 模块一侧的可缺省组合:两侧皆无不是模块(设计 C-6) */
32
35
  export interface ModuleSides {
33
36
  name: string
34
- /** 后端源码目录 `<project>/src/<name>/api`(无此侧为 undefined) */
37
+ /** 后端源码目录(`src/<name>/api` 或聚合容器 `src/app/api/<name>`;无此侧为 undefined) */
35
38
  apiSrc?: string
36
39
  /** 前端源码目录 `<project>/src/<name>/web`(无此侧为 undefined) */
37
40
  webSrc?: string
@@ -82,13 +85,44 @@ function isFile(p: string): boolean {
82
85
  * 坏目录只在 module-root 被判定为当前布局后才报错(见 resolveLayout),
83
86
  * 否则一个恰好带 `src/utils/api/` 的历史工程会被误伤。
84
87
  */
88
+ /** 递归扫描时跳过的目录名(产物 / 依赖 / 生成物,均不可能承载模块根) */
89
+ const SCAN_SKIP_DIRS = new Set(["node_modules", "dist", "build", "__client", ".cache", ".git"]);
90
+
91
+ /**
92
+ * 收集一个容器目录下的后端模块根。
93
+ *
94
+ * 两种形态并存(oj 的 `--module-root` 支持任意深度):
95
+ * · `<container>/api/manifest.yaml` —— 模块根在 api 自身(src/<name>/api)
96
+ * · `<container>/api/<name>/manifest.yaml` —— 聚合容器下的子模块(src/app/api/system)
97
+ *
98
+ * 后者用于「一个应用目录承载多个业务域」:src/app 的 web 侧是多根聚合模块,
99
+ * api 侧按业务域分成 system / notification / … 各自仍是独立模块(模块名 = 目录名
100
+ * = URL 前缀,故不能并成一个 app 模块)。命中模块根即停,不再往 api 内部递归。
101
+ */
102
+ function collectApiRoots(container: string, out: string[]): void {
103
+ const apiDir = path.join(container, "api");
104
+ if (isFile(path.join(apiDir, "manifest.yaml"))) {
105
+ out.push(apiDir);
106
+ return;
107
+ }
108
+ if (!isDir(apiDir))
109
+ return;
110
+ for (const ent of fs.readdirSync(apiDir, { withFileTypes: true })
111
+ .filter(e => e.isDirectory() && !e.name.startsWith(".") && !SCAN_SKIP_DIRS.has(e.name))
112
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
113
+ if (isFile(path.join(apiDir, ent.name, "manifest.yaml")))
114
+ out.push(path.join(apiDir, ent.name));
115
+ }
116
+ }
117
+
85
118
  function scanModuleRoot(projectRoot: string): { modules: ModuleSides[], broken: string[] } | null {
86
119
  const srcDir = path.join(projectRoot, "src");
87
120
  if (!isDir(srcDir))
88
121
  return null;
89
122
 
90
- const modules: ModuleSides[] = [];
123
+ const byName = new Map<string, ModuleSides>();
91
124
  const broken: string[] = [];
125
+ const apiOwners = new Map<string, string>();
92
126
  // 按名字排序:readdir 的顺序取决于文件系统与创建历史(同一份源码在不同机器上可能不同),
93
127
  // 而枚举顺序会进入构建顺序与产物内容——不排序则「同布局两次构建 sha256 相等」无法保证。
94
128
  const entries = fs.readdirSync(srcDir, { withFileTypes: true })
@@ -96,22 +130,41 @@ function scanModuleRoot(projectRoot: string): { modules: ModuleSides[], broken:
96
130
  .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
97
131
  for (const ent of entries) {
98
132
  const dir = path.join(srcDir, ent.name);
99
- const apiDir = path.join(dir, "api");
100
133
  const webDir = path.join(dir, "web");
101
- const hasApi = isFile(path.join(apiDir, "manifest.yaml"));
102
134
  const hasWeb = isFile(path.join(webDir, "entry.ts"));
103
- if (!hasApi && !hasWeb) {
135
+ const apiRoots: string[] = [];
136
+ collectApiRoots(dir, apiRoots);
137
+ if (!hasWeb && apiRoots.length === 0) {
104
138
  // 只把「有 api/ 或 web/ 子目录但缺标记」算坏目录;纯共享库目录(无两者)静默跳过
105
- if (isDir(apiDir) || isDir(webDir))
139
+ if (isDir(path.join(dir, "api")) || isDir(webDir))
106
140
  broken.push(ent.name);
107
141
  continue;
108
142
  }
109
- modules.push({
110
- name: ent.name,
111
- apiSrc: hasApi ? apiDir : undefined,
112
- webSrc: hasWeb ? webDir : undefined,
113
- });
143
+ if (hasWeb)
144
+ byName.set(ent.name, { name: ent.name, webSrc: webDir });
145
+ for (const apiDir of apiRoots) {
146
+ // 模块名:平铺 src/system/api → system(api 的父目录);
147
+ // 聚合 src/app/api/system → system(api 下方那层目录本身)。
148
+ const name = path.basename(apiDir) === "api"
149
+ ? path.basename(path.dirname(apiDir))
150
+ : path.basename(apiDir);
151
+ const prev = apiOwners.get(name);
152
+ if (prev) {
153
+ throw new Error(
154
+ `[ojm] 后端模块名重复:${name} 同时出现在 ${path.relative(projectRoot, prev)} 与 `
155
+ + `${path.relative(projectRoot, apiDir)}。模块名即 URL 前缀,必须唯一——`
156
+ + "删掉旧布局目录(或改名)后重试。",
157
+ );
158
+ }
159
+ apiOwners.set(name, apiDir);
160
+ const hit = byName.get(name);
161
+ if (hit)
162
+ hit.apiSrc = apiDir;
163
+ else
164
+ byName.set(name, { name, apiSrc: apiDir });
165
+ }
114
166
  }
167
+ const modules = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
115
168
  return { modules, broken };
116
169
  }
117
170
 
@@ -244,7 +297,7 @@ export function legacyApiModuleRoots(projectRoot: string): ApiModuleRoot[] {
244
297
  * 为什么不直接 walk `apiModulesSrc(layout)`:module-root 布局的树根是 `src`,模块的
245
298
  * api 侧在 `<src>/<name>/api`,中间那层 `api` 是**布局分隔符、不是路由段**。拿 `src`
246
299
  * 当扫描根会双重出错——路由镜像多出 `api` 段(`system/api/users` ≠ 真实 `system/users`),
247
- * 且 `src/<name>/web/__client/api.ts` 这种前端生成物也会被当 handler 扫进来。
300
+ * 且 `src/<name>/web/__client/index.ts` 这种前端生成物也会被当 handler 扫进来。
248
301
  *
249
302
  * module-root → 按 `<name>/api/manifest.yaml` 判模块(与 build 同源,name 取 manifest);
250
303
  * 历史布局 → `<project>/api/src` 的一级目录即模块。
@@ -267,9 +320,12 @@ export function moduleDir(layout: ProjectLayout, name: string): string {
267
320
  * 历史布局为 `<project>/api/src/<name>`——不在 modulesSrc 之下,必须走 projectRoot。
268
321
  */
269
322
  export function apiSrcOf(layout: ProjectLayout, name: string): string {
270
- return layout.kind === "module-root"
271
- ? path.join(layout.modulesSrc, name, "api")
272
- : path.join(layout.projectRoot, "api", "src", name);
323
+ if (layout.kind !== "module-root")
324
+ return path.join(layout.projectRoot, "api", "src", name);
325
+ // 聚合容器(src/app/api/<name>)与平铺(src/<name>/api)并存,以扫描结果为准;
326
+ // 找不到时回落旧口径,保证报错信息里的路径仍指向「本该在的位置」。
327
+ const hit = scanModules(layout).find(m => m.name === name)?.apiSrc;
328
+ return hit ?? path.join(layout.modulesSrc, name, "api");
273
329
  }
274
330
 
275
331
  /**
@@ -1,4 +1,4 @@
1
- import type { InstallContext } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
1
+ import type { InstallContext } from "../../../templates/src/app/api/_platform/_lifecycle/steps/preflight";
2
2
 
3
3
  // BDD:bundle 级 manifest.yaml 契约校验(P1-11,C2 规则构建列)。
4
4
  // 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md(C2-01~33 构建列,§4.1 矩阵);
@@ -13,7 +13,7 @@ import { describe, expect, it } from "vitest";
13
13
  import { parse } from "yaml";
14
14
  // 跨端引用(双端一致钉):被测实现同仓分居两运行时(计划 §1「两运行时取舍」),
15
15
  // 测试直接互调,比「两套件各读同一夹具」更强——漂移即本文件红。
16
- import { preflight } from "../../../templates/src/_platform/api/_lifecycle/steps/preflight";
16
+ import { preflight } from "../../../templates/src/app/api/_platform/_lifecycle/steps/preflight";
17
17
  import { checkLocalesComplete, checkTablesAgainstSchema, parseManifest, RESERVED_NAMES, reservedNameSuggestions, validateContract } from "../../manifest-contract";
18
18
 
19
19
  const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
@@ -68,7 +68,7 @@ describe("given 一个装了 oj 二进制的源工程 When 自举隔离站点",
68
68
 
69
69
  const site = await bootstrapIsolatedSite({ projectRoot: src, starter });
70
70
 
71
- expect(existsSync(join(site.root, "src", "_platform", "api", "seed.sql"))).toBe(true);
71
+ expect(existsSync(join(site.root, "src", "app", "api", "_platform", "seed.sql"))).toBe(true);
72
72
  expect(existsSync(join(site.root, "bin", "oj"))).toBe(true);
73
73
  expect(existsSync(join(site.root, "bin", "plugins", "auth.wasm"))).toBe(true);
74
74
  expect(site.root).not.toBe(src);
package/src/site.ts CHANGED
@@ -21,7 +21,7 @@ export function tenantHeaders(): Record<string, string> {
21
21
  return { [TENANT_HEADER]: DEFAULT_TENANT };
22
22
  }
23
23
 
24
- /** 任务记录(线形状镜像;真源 templates/src/_platform/api/_lifecycle/types.ts:65,客户端不 import 模板内部) */
24
+ /** 任务记录(线形状镜像;真源 templates/src/app/api/_platform/_lifecycle/types.ts:65,客户端不 import 模板内部) */
25
25
  export interface TaskRecord {
26
26
  /**
27
27
  * 记录 schema 版本(P2-10 线形状镜像):缺省(undefined)=旧记录=v1 语义照常渲染;
@@ -6,30 +6,32 @@
6
6
 
7
7
  ```bash
8
8
  pnpm install
9
- pnpm dev # 开发:前端 devServer + oj 后端(src/*/api 保存即热更)
9
+ pnpm dev # 开发:前端 devServer + oj 后端(src 下 api.ts 保存即热更)
10
10
  pnpm build # 构建:oj build + 前端全站合并到 dist/web
11
11
  pnpm preview # 预览:oj migrate + server(release 产物)
12
12
  ```
13
13
 
14
- 登录账号:`admin` / `aA123456`(见 `src/_platform/api/seed.sql`)。
14
+ 登录账号:`superadmin` / `aA123456`(四个内置角色账号见 `src/app/api/_platform/seed.sql`)。
15
15
 
16
- ## 目录(一目录 = 一个模块)
16
+ ## 目录(一个 `src/app` 装下整个应用)
17
17
 
18
18
  ```
19
- config.yaml 站点/数据库/租户配置(init 现场签发证书到 config/)
20
- src/<name>/api/ 后端模块(目录镜像路由:src/<name>/api/<路径>/api.ts)
21
- src/<name>/web/ 前端模块(entry.ts 是入口;页面 / locales / __client 同目录)
22
- src/app/web/ 框架内置页面聚合模块(多根:/home /login /personal-center /notification /system)
23
- src/oj-market/web/ 解决方案市场(第一方能力,独立模块,删目录即下线)
24
- dist/api/ oj build 后端产物
25
- dist/web/ 站点产物(shell + modules + modules.json)
26
- bin/ oj 二进制(init 解压,不入库)
19
+ config.yaml 站点/数据库/租户配置(init 现场签发证书到 config/)
20
+ src/app/api/<name>/ 后端模块(模块名 = URL 前缀;目录镜像:<路径>/api.ts)
21
+ src/app/web/ 前端聚合模块(多根,一个 entry.ts 挂 /home /login /…)
22
+ <root>/ 每个根:routes.ts + pages/ + locales/ + __client/
23
+ src/oj-market/web/ 解决方案市场(第一方能力,独立模块,删目录即下线)
24
+ dist/api/ oj build 后端产物
25
+ dist/web/ 站点产物(shell + modules + modules.json)
26
+ bin/ oj 二进制(init 解压,不入库)
27
27
  ```
28
28
 
29
- `src/app/web` 是一个**多根模块**:一个 `entry.ts` 里挂五个顶层路由,各根按
30
- `handle.order` 排菜单(与拆分时逐项一致),页面按根分目录
31
- (`<root>/routes.ts` + `pages/` + `__client/`)。新增业务页面时建议单独建模块
32
- (`pnpm exec ojm new <name>`),不要往聚合模块里塞。
29
+ - **后端**按业务域分模块:`_platform` / `auth` / `notification` / `personal-center` /
30
+ `system` / `web`,各自是独立 oj 模块(模块名即 `/api/<name>` 前缀),只是统一收在
31
+ `src/app/api` 下——顶层不再散落同名目录。
32
+ - **前端** `src/app/web` 是一个**多根模块**:一个 `entry.ts` 里挂六个顶层路由,各根按
33
+ `handle.order` 排菜单,页面按根分目录。新增业务页面时建议单独建模块
34
+ (`pnpm exec ojm new <name>`),不要往聚合模块里塞。
33
35
 
34
36
  ## 契约与 `__client`
35
37
 
@@ -38,16 +40,19 @@ bin/ oj 二进制(init 解压,不入库)
38
40
 
39
41
  ```bash
40
42
  bin/oj openapi -c config.yaml --module-root src -b /api -o openapi.json # 重产契约
41
- pnpm exec ojm api # 重生成 */web/*/__client
43
+ pnpm exec ojm api # 重生成 __client
42
44
  ```
43
45
 
46
+ 生成物入口是 `__client/index.ts`(**不是 `api.ts`**:oj dev 会把模块目录下的
47
+ `api.ts` 当路由扫描)。页面里用 `from "../__client"` 导入。
48
+
44
49
  多根聚合模块的 client 归属由 `web.config.ts` 的 `clientOwners` 决定(`system → app`),
45
- 生成物落 `src/app/web/system/__client`,与页面同级——页面相对导入 `../../__client` 不用改。
50
+ 生成物落 `src/app/web/system/__client`,与页面同级——页面相对导入不用改。
46
51
  `ojm api --check` 可对账漂移(零漂移为发布门禁)。
47
52
 
48
53
  ## 注意
49
54
 
50
- - 改 `src/*/api` 下的 api.ts 保存即生效;**新增/删除模块目录、改
55
+ - 改 `src/app/api/**/api.ts` 保存即生效;**新增/删除模块目录、改
51
56
  schema/migrations/config.yaml 需重启 `ojm dev`**(oj 热更边界)。
52
57
  - 后端开发请用 `/oj-api-dev` skill(`.claude/skills/oj-api-dev/`),手册按章节按需读。
53
58
  - `config/` 下的证书为 init 现场签发的本地 dev 证书,不构成信任边界;
@@ -55,7 +60,7 @@ pnpm exec ojm api # 重
55
60
 
56
61
  ## 排障 / 常见问题
57
62
 
58
- - **`pnpm dev` / `pnpm build` 报 `Could not resolve "../../__client/api"`**
63
+ - **`pnpm dev` / `pnpm build` 报 `Could not resolve "../../__client"`**
59
64
  对应根的 `__client` 缺失——它是 `ojm api` 的生成物。在工程根跑一次即可补齐:
60
65
 
61
66
  ```bash
@@ -73,8 +78,13 @@ pnpm exec ojm api # 重
73
78
  不能手改、也不入库——它是生成物,下次 `ojm api` 会覆盖。改接口就改 handler 的
74
79
  `.schema`/`.response`,再跑上面的命令刷新。
75
80
 
81
+ - **从旧版本升级(`__client/api.ts`)**
82
+ 0.2.54 起入口改名 `index.ts`(`api.ts` 会被 oj dev 当路由扫描)。重跑一次
83
+ `pnpm exec ojm api` 即可:新文件写入、旧 `api.ts` 自动删除;页面导入改为
84
+ `from "../__client"`。
85
+
76
86
  - **`pnpm dlx @oj-module/cli@latest init` 生成的脚手架缺模块 / 看起来很旧**
77
- pnpm dlx 会按首次解析到的版本**缓存** CLI 可执行文件。排查:`ls src` 看是否缺预期模块。
87
+ pnpm dlx 会按首次解析到的版本**缓存** CLI 可执行文件。排查:`ls src/app/api` 看是否缺预期模块。
78
88
  解决:清掉 dlx 缓存后显式指定版本重跑——
79
89
 
80
90
  ```bash
@@ -40,7 +40,7 @@ include:
40
40
  - src/styles.css
41
41
  # 后端 typecheck 工程(package.json 的 typecheck:api 指向它);只收后端、排除 web
42
42
  - src/tsconfig.json
43
- # —— 后端:配置 / 种子 / 测试 / 默认模块(模块源码在 src/<name>/api)——
43
+ # —— 后端:配置 / 种子 / 测试 / 默认模块(模块源码在 src/app/api/<模块>)——
44
44
  - config.yaml
45
45
  - .ojm-api-exempt.json
46
46
  - seed.mysql.sql
@@ -71,8 +71,12 @@ include:
71
71
  - tests/rbac-solution-perms.spec.ts
72
72
  - tests/rbac-tables.spec.ts
73
73
  - tests/rbac-users.spec.ts
74
+ # 角色页展示层过滤 superadmin(后端 listRoles 仍返回;页面源码随 src/app/web/system 下发)
75
+ - tests/roles-page.spec.ts
74
76
  - tests/seed-role-accounts.spec.ts
75
- # 企微连接器与组织同步:源码本就分发(src/auth/api),对应回归也必须跟着走,
77
+ # 用户页行级门禁:只有 superadmin 能改 superadmin 账号(后端 assertCanManageTarget 的另一面)
78
+ - tests/users-page.spec.ts
79
+ # 企微连接器与组织同步:源码本就分发(src/app/api/auth),对应回归也必须跟着走,
76
80
  # 否则用户工程升级后改配置/换套件时没有任何本地自检手段。
77
81
  - tests/wecom-admin.spec.ts
78
82
  - tests/wecom-client-timeout.spec.ts
@@ -95,15 +99,19 @@ include:
95
99
  - tests/lifecycle/uninstall.spec.ts
96
100
  - tests/lifecycle/verify-checks.spec.ts
97
101
  - tests/lifecycle/verify-report.spec.ts
98
- - src/_platform/api
99
- - src/auth/api
100
- - src/notification/api
101
- - src/personal-center/api
102
- - src/system/api
103
- - src/web/api
102
+ # 框架内置后端全部收在 src/app/api/<模块>(模块名 = URL 前缀,故仍是独立模块)。
103
+ # home 的 api 是 demo-only 不进脚手架(见下),其余全部下发。
104
+ - src/app/api/_platform
105
+ - src/app/api/auth
106
+ - src/app/api/notification
107
+ - src/app/api/personal-center
108
+ - src/app/api/system
109
+ - src/app/api/web
104
110
  # —— web 前端模块 ——
105
- - src/_platform/web
106
- - src/auth/web
111
+ # 纯 client 归属目录(无页面模块):_platform 的生成物被 oj-market 引用,
112
+ # auth / web 的是各自后端契约的配套产物,一并下发保证 ojm api --check 开箱即过。
113
+ - src/app/web/_platform
114
+ - src/app/web/auth
107
115
  # 框架内置页面聚合为一个多根模块(首页/登录/个人中心/通知/系统管理)。
108
116
  # home 只列 routes/locales/pages:其 api 模块是 demo-only 不进脚手架,__client
109
117
  # 留着会指向不存在的端点(见顶部说明),故逐子路径收录而不是整棵 src/app/web。
@@ -125,4 +133,4 @@ include:
125
133
  - src/oj-market/web/locales
126
134
  - src/oj-market/web/pages
127
135
  - src/oj-market/web/utils
128
- - src/web/web
136
+ - src/app/web/web