@oj-module/cli 0.2.54 → 0.2.57

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 (307) hide show
  1. package/devkit/README.md +7 -4
  2. package/devkit/SKILL.md +7 -4
  3. package/devkit/manual.md +63 -54
  4. package/devkit/module-dev-and-cdn-playground.md +7 -0
  5. package/devkit/publish-walkthrough.md +199 -0
  6. package/package.json +2 -2
  7. package/shell-dist/assets/{index-e_Mp2mb5.js → index-CImwskAz.js} +24 -11
  8. package/shell-dist/assets/runtime.js +5 -5
  9. package/shell-dist/index.html +3 -3
  10. package/shell-dist/versions.json +3 -3
  11. package/src/__tests__/layout.test.ts +30 -1
  12. package/src/__tests__/templates-schema-columns.test.ts +99 -0
  13. package/src/__tests__/vendor-devkit.test.ts +87 -0
  14. package/src/alias-paths.ts +11 -12
  15. package/src/build/backend.ts +131 -0
  16. package/src/build/module-build.ts +364 -0
  17. package/src/build/module-def.ts +381 -0
  18. package/src/build/site-merge.ts +33 -0
  19. package/src/build.ts +16 -885
  20. package/src/builtin.ts +10 -3
  21. package/src/bundle.ts +61 -0
  22. package/src/cli.ts +4 -1
  23. package/src/contract/__tests__/emit-client.test.ts +4 -4
  24. package/src/contract/__tests__/market-client.integration.test.ts +2 -2
  25. package/src/contract/__tests__/run-api.test.ts +2 -2
  26. package/src/contract/check.ts +6 -2
  27. package/src/contract/emit-client.ts +10 -3
  28. package/src/contract/run.ts +10 -2
  29. package/src/init.ts +31 -46
  30. package/src/layout.ts +72 -16
  31. package/src/lifecycle/__tests__/bundle-summary.test.ts +106 -0
  32. package/src/lifecycle/__tests__/bundle.test.ts +6 -0
  33. package/src/lifecycle/__tests__/manifest-contract.test.ts +2 -2
  34. package/src/market/__tests__/bootstrap.test.ts +1 -1
  35. package/src/site.ts +1 -1
  36. package/src/vendor.ts +34 -0
  37. package/templates/README.md +30 -20
  38. package/templates/init.include.yaml +19 -11
  39. package/templates/openapi.json +102 -86
  40. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/installer.ts +8 -3
  41. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/register.ts +53 -0
  42. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/roles.ts +4 -1
  43. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/solution-perms.ts +7 -3
  44. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/users.ts +27 -0
  45. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/oplog.ts +4 -2
  46. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.mysql.sql +7 -0
  47. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.pg.sql +4 -0
  48. package/templates/src/app/api/_platform/migrations/0017__users_employee_no.sql +8 -0
  49. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.mysql.sql +3 -0
  50. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.pg.sql +3 -0
  51. package/templates/src/app/api/_platform/migrations/0018__sys_role_title_key.sql +8 -0
  52. package/templates/src/{_platform/api → app/api/_platform}/schema.yaml +7 -0
  53. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/replay.ts +9 -3
  54. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/tenant-admin.ts +18 -27
  55. package/templates/src/{system/api → app/api/system}/oplog/api.ts +5 -3
  56. package/templates/src/{system/api → app/api/system}/roles/api.ts +2 -1
  57. package/templates/src/{_platform/web/__client/api.ts → app/web/_platform/__client/index.ts} +5 -2
  58. package/templates/src/{auth/web/__client/api.ts → app/web/auth/__client/index.ts} +5 -2
  59. package/templates/src/app/web/notification/pages/center/index.tsx +1 -1
  60. package/templates/src/app/web/notification/routes.ts +1 -1
  61. package/templates/src/app/web/personal-center/pages/my-profile/index.tsx +1 -1
  62. package/templates/src/app/web/personal-center/routes.ts +1 -1
  63. package/templates/src/app/web/system/__client/api.schemas.ts +2 -0
  64. package/templates/src/app/web/system/locales/en-US.json +2 -3
  65. package/templates/src/app/web/system/locales/zh-CN.json +2 -3
  66. package/templates/src/app/web/system/pages/departments/index.tsx +2 -2
  67. package/templates/src/app/web/system/pages/login-methods/index.tsx +1 -1
  68. package/templates/src/app/web/system/pages/oplog/index.tsx +44 -19
  69. package/templates/src/app/web/system/pages/roles/index.tsx +12 -4
  70. package/templates/src/app/web/system/pages/roles/visible-roles.ts +14 -0
  71. package/templates/src/app/web/system/pages/settings/index.tsx +2 -2
  72. package/templates/src/app/web/system/pages/users/can-manage.ts +21 -0
  73. package/templates/src/app/web/system/pages/users/index.tsx +25 -17
  74. package/templates/src/app/web/system/pages/users/roles.tsx +1 -1
  75. package/templates/src/app/web/system/routes.ts +1 -1
  76. package/templates/src/{web/web/__client/api.ts → app/web/web/__client/index.ts} +5 -2
  77. package/templates/src/oj-market/web/api/defaultClient.ts +1 -1
  78. package/templates/tests/auth-ldap.spec.ts +2 -2
  79. package/templates/tests/auth-session.spec.ts +3 -3
  80. package/templates/tests/auth-throttle.spec.ts +1 -1
  81. package/templates/tests/fixture.ts +6 -4
  82. package/templates/tests/guard.spec.ts +1 -1
  83. package/templates/tests/lifecycle/atomic-state.spec.ts +3 -3
  84. package/templates/tests/lifecycle/bootstrap.spec.ts +3 -3
  85. package/templates/tests/lifecycle/deploy-smoke.spec.ts +8 -8
  86. package/templates/tests/lifecycle/installer.spec.ts +10 -10
  87. package/templates/tests/lifecycle/market-api.spec.ts +14 -14
  88. package/templates/tests/lifecycle/migrate.spec.ts +6 -6
  89. package/templates/tests/lifecycle/preflight.spec.ts +3 -3
  90. package/templates/tests/lifecycle/register.spec.ts +1 -1
  91. package/templates/tests/lifecycle/role-title-translate.spec.ts +171 -0
  92. package/templates/tests/lifecycle/semver.spec.ts +1 -1
  93. package/templates/tests/lifecycle/sha384.spec.ts +1 -1
  94. package/templates/tests/lifecycle/solutions-api.spec.ts +1 -1
  95. package/templates/tests/lifecycle/source-seq.spec.ts +1 -1
  96. package/templates/tests/lifecycle/steps-fs.spec.ts +9 -9
  97. package/templates/tests/lifecycle/text-edit.spec.ts +1 -1
  98. package/templates/tests/lifecycle/tgz-guard.spec.ts +2 -2
  99. package/templates/tests/lifecycle/trust.spec.ts +5 -5
  100. package/templates/tests/lifecycle/uninstall.spec.ts +7 -7
  101. package/templates/tests/lifecycle/verify-checks.spec.ts +5 -5
  102. package/templates/tests/lifecycle/verify-report.spec.ts +4 -4
  103. package/templates/tests/org-departments.spec.ts +10 -6
  104. package/templates/tests/org-read.spec.ts +7 -7
  105. package/templates/tests/query-utils.spec.ts +1 -1
  106. package/templates/tests/rbac-audit.spec.ts +1 -1
  107. package/templates/tests/rbac-contacts.spec.ts +2 -2
  108. package/templates/tests/rbac-guard.spec.ts +1 -1
  109. package/templates/tests/rbac-roles.spec.ts +19 -2
  110. package/templates/tests/rbac-seed.spec.ts +1 -1
  111. package/templates/tests/rbac-settings.spec.ts +77 -7
  112. package/templates/tests/rbac-solution-perms.spec.ts +1 -1
  113. package/templates/tests/rbac-tables.spec.ts +29 -0
  114. package/templates/tests/rbac-users.spec.ts +55 -3
  115. package/templates/tests/roles-page.spec.ts +40 -0
  116. package/templates/tests/seed-role-accounts.spec.ts +1 -1
  117. package/templates/tests/tenant-access-cache.spec.ts +1 -1
  118. package/templates/tests/tenant-forgery.spec.ts +5 -5
  119. package/templates/tests/tenant-isolation.spec.ts +5 -4
  120. package/templates/tests/username-namespace.spec.ts +3 -3
  121. package/templates/tests/users-page.spec.ts +39 -0
  122. package/templates/tests/wecom-admin.spec.ts +6 -6
  123. package/templates/tests/wecom-client-timeout.spec.ts +1 -1
  124. package/templates/tests/wecom-connector.spec.ts +9 -9
  125. package/templates/tests/wecom-corp.spec.ts +9 -9
  126. package/templates/tests/wecom-crypt.spec.ts +1 -1
  127. package/templates/tests/wecom-endpoints.spec.ts +2 -2
  128. package/templates/tests/wecom-landing.spec.ts +1 -1
  129. package/templates/tests/wecom-org-sync.spec.ts +7 -7
  130. package/templates/tests/wecom-sync.spec.ts +3 -3
  131. package/templates/tsconfig.paths.json +11 -11
  132. package/templates/vitest.config.ts +2 -2
  133. package/templates/web.config.ts +5 -2
  134. package/vendor/host-versions.json +3 -3
  135. package/templates/src/{_platform/api → app/api/_platform}/README.md +0 -0
  136. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/atomic.ts +0 -0
  137. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/history.ts +0 -0
  138. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/limits.ts +0 -0
  139. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/lock.ts +0 -0
  140. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/semver.ts +0 -0
  141. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/sha384.ts +0 -0
  142. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/source-seq.ts +0 -0
  143. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/state.ts +0 -0
  144. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/deploy-web.ts +0 -0
  145. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/download.ts +0 -0
  146. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/extract.ts +0 -0
  147. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/migrate.ts +0 -0
  148. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/preflight.ts +0 -0
  149. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/reload.ts +0 -0
  150. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/smoke.ts +0 -0
  151. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/steps/verify.ts +0 -0
  152. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tasks.ts +0 -0
  153. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/text-edit.ts +0 -0
  154. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/tgz-guard.ts +0 -0
  155. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/trust.ts +0 -0
  156. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/types.ts +0 -0
  157. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/uninstall.ts +0 -0
  158. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-checks.ts +0 -0
  159. package/templates/src/{_platform/api → app/api/_platform}/_lifecycle/verify-report.ts +0 -0
  160. package/templates/src/{_platform/api → app/api/_platform}/_shared/respond.ts +0 -0
  161. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/contacts.ts +0 -0
  162. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org-read.ts +0 -0
  163. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/org.ts +0 -0
  164. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/domain/settings.ts +0 -0
  165. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/access.ts +0 -0
  166. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/audit.ts +0 -0
  167. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/guard.ts +0 -0
  168. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/query.ts +0 -0
  169. package/templates/src/{_platform/api → app/api/_platform}/_shared/service/infra/rate.ts +0 -0
  170. package/templates/src/{_platform/api → app/api/_platform}/_shared/site-facade.ts +0 -0
  171. package/templates/src/{_platform/api → app/api/_platform}/contacts/api.ts +0 -0
  172. package/templates/src/{_platform/api → app/api/_platform}/contacts/detail/api.ts +0 -0
  173. package/templates/src/{_platform/api → app/api/_platform}/fixtures/l1-test-user.sql +0 -0
  174. package/templates/src/{_platform/api → app/api/_platform}/manifest.yaml +0 -0
  175. package/templates/src/{_platform/api → app/api/_platform}/market/api.ts +0 -0
  176. package/templates/src/{_platform/api → app/api/_platform}/market/bootstrap.ts +0 -0
  177. package/templates/src/{_platform/api → app/api/_platform}/market/index-source.ts +0 -0
  178. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.mysql.sql +0 -0
  179. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.pg.sql +0 -0
  180. package/templates/src/{_platform/api → app/api/_platform}/migrations/0001__create_users.sql +0 -0
  181. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.mysql.sql +0 -0
  182. package/templates/src/{_platform/api → app/api/_platform}/migrations/0002__add_users_avatar_base64.sql +0 -0
  183. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.mysql.sql +0 -0
  184. package/templates/src/{_platform/api → app/api/_platform}/migrations/0003__users_source_display_name.sql +0 -0
  185. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.mysql.sql +0 -0
  186. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.pg.sql +0 -0
  187. package/templates/src/{_platform/api → app/api/_platform}/migrations/0004__sys_rbac.sql +0 -0
  188. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.mysql.sql +0 -0
  189. package/templates/src/{_platform/api → app/api/_platform}/migrations/0005__users_must_change.sql +0 -0
  190. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.mysql.sql +0 -0
  191. package/templates/src/{_platform/api → app/api/_platform}/migrations/0006__users_is_forbidden.sql +0 -0
  192. package/templates/src/{_platform/api → app/api/_platform}/migrations/0007__users_email.sql +0 -0
  193. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.mysql.sql +0 -0
  194. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.pg.sql +0 -0
  195. package/templates/src/{_platform/api → app/api/_platform}/migrations/0008__sys_refresh_token.sql +0 -0
  196. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.mysql.sql +0 -0
  197. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.pg.sql +0 -0
  198. package/templates/src/{_platform/api → app/api/_platform}/migrations/0009__sys_throttle.sql +0 -0
  199. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.mysql.sql +0 -0
  200. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.pg.sql +0 -0
  201. package/templates/src/{_platform/api → app/api/_platform}/migrations/0010__sys_solution_route.sql +0 -0
  202. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.mysql.sql +0 -0
  203. package/templates/src/{_platform/api → app/api/_platform}/migrations/0011__sys_setting_group_label.sql +0 -0
  204. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.mysql.sql +0 -0
  205. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.pg.sql +0 -0
  206. package/templates/src/{_platform/api → app/api/_platform}/migrations/0012__market_history.sql +0 -0
  207. package/templates/src/{_platform/api → app/api/_platform}/migrations/0013__users_profile.sql +0 -0
  208. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.mysql.sql +0 -0
  209. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.pg.sql +0 -0
  210. package/templates/src/{_platform/api → app/api/_platform}/migrations/0014__reg_source_seq.sql +0 -0
  211. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.mysql.sql +0 -0
  212. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.pg.sql +0 -0
  213. package/templates/src/{_platform/api → app/api/_platform}/migrations/0015__tenant.sql +0 -0
  214. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.mysql.sql +0 -0
  215. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.pg.sql +0 -0
  216. package/templates/src/{_platform/api → app/api/_platform}/migrations/0016__org.sql +0 -0
  217. package/templates/src/{_platform/api → app/api/_platform}/org/departments/api.ts +0 -0
  218. package/templates/src/{_platform/api → app/api/_platform}/org/is-member/api.ts +0 -0
  219. package/templates/src/{_platform/api → app/api/_platform}/org/members/api.ts +0 -0
  220. package/templates/src/{_platform/api → app/api/_platform}/org/picker/api.ts +0 -0
  221. package/templates/src/{_platform/api → app/api/_platform}/org/resolve/api.ts +0 -0
  222. package/templates/src/{_platform/api → app/api/_platform}/org/tree/api.ts +0 -0
  223. package/templates/src/{_platform/api → app/api/_platform}/org/user/api.ts +0 -0
  224. package/templates/src/{_platform/api → app/api/_platform}/seed.mysql.sql +0 -0
  225. package/templates/src/{_platform/api → app/api/_platform}/seed.pg.sql +0 -0
  226. package/templates/src/{_platform/api → app/api/_platform}/seed.sql +0 -0
  227. package/templates/src/{_platform/api → app/api/_platform}/solutions/api.ts +0 -0
  228. package/templates/src/{auth/api → app/api/auth}/_shared/ldap-login.ts +0 -0
  229. package/templates/src/{auth/api → app/api/auth}/_shared/ldap.ts +0 -0
  230. package/templates/src/{auth/api → app/api/auth}/_shared/login-throttle.ts +0 -0
  231. package/templates/src/{auth/api → app/api/auth}/_shared/session.ts +0 -0
  232. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/admin.ts +1 -1
  233. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/client.ts +0 -0
  234. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/config.ts +0 -0
  235. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/contacts.ts +0 -0
  236. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/crypt.ts +0 -0
  237. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/events.ts +1 -1
  238. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/login.ts +0 -0
  239. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/payload.ts +0 -0
  240. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/store.ts +0 -0
  241. package/templates/src/{auth/api → app/api/auth}/_shared/wecom/sync.ts +0 -0
  242. package/templates/src/{auth/api → app/api/auth}/api.ts +0 -0
  243. package/templates/src/{auth/api → app/api/auth}/ldap-login/api.ts +0 -0
  244. package/templates/src/{auth/api → app/api/auth}/manifest.yaml +0 -0
  245. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.mysql.sql +0 -0
  246. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.pg.sql +0 -0
  247. package/templates/src/{auth/api → app/api/auth}/migrations/0001__wecom.sql +0 -0
  248. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.mysql.sql +0 -0
  249. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.pg.sql +0 -0
  250. package/templates/src/{auth/api → app/api/auth}/migrations/0002__org_sync.sql +0 -0
  251. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.mysql.sql +0 -0
  252. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.pg.sql +0 -0
  253. package/templates/src/{auth/api → app/api/auth}/migrations/0003__credential_kind.sql +0 -0
  254. package/templates/src/{auth/api → app/api/auth}/schema.yaml +0 -0
  255. package/templates/src/{auth/api → app/api/auth}/wecom/admin/api.ts +0 -0
  256. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/api.ts +1 -1
  257. package/templates/src/{auth/api → app/api/auth}/wecom/admin/tenant-admin/password/api.ts +1 -1
  258. /package/templates/src/{auth/api → app/api/auth}/wecom/api.ts +0 -0
  259. /package/templates/src/{auth/api → app/api/auth}/wecom/sync/api.ts +0 -0
  260. /package/templates/src/{home/api → app/api/home}/line/api.ts +0 -0
  261. /package/templates/src/{home/api → app/api/home}/manifest.yaml +0 -0
  262. /package/templates/src/{home/api → app/api/home}/migrations/0001__create_home_tables.sql +0 -0
  263. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.mysql.sql +0 -0
  264. /package/templates/src/{home/api → app/api/home}/migrations/0002__tenant.sql +0 -0
  265. /package/templates/src/{home/api → app/api/home}/pie/api.ts +0 -0
  266. /package/templates/src/{home/api → app/api/home}/schema.yaml +0 -0
  267. /package/templates/src/{home/api → app/api/home}/seed.sql +0 -0
  268. /package/templates/src/{notification/api → app/api/notification}/_shared/notify.ts +0 -0
  269. /package/templates/src/{notification/api → app/api/notification}/api.ts +0 -0
  270. /package/templates/src/{notification/api → app/api/notification}/del/api.ts +0 -0
  271. /package/templates/src/{notification/api → app/api/notification}/manifest.yaml +0 -0
  272. /package/templates/src/{notification/api → app/api/notification}/migrations/0001__create_notifications.sql +0 -0
  273. /package/templates/src/{notification/api → app/api/notification}/migrations/0002__notifications_user.sql +0 -0
  274. /package/templates/src/{notification/api → app/api/notification}/migrations/0003__notifications_broadcast.sql +0 -0
  275. /package/templates/src/{notification/api → app/api/notification}/migrations/0004__notifications_broadcast_hidden.sql +0 -0
  276. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.mysql.sql +0 -0
  277. /package/templates/src/{notification/api → app/api/notification}/migrations/0005__tenant.sql +0 -0
  278. /package/templates/src/{notification/api → app/api/notification}/schema.yaml +0 -0
  279. /package/templates/src/{notification/api → app/api/notification}/seed.sql +0 -0
  280. /package/templates/src/{personal-center/api → app/api/personal-center}/manifest.yaml +0 -0
  281. /package/templates/src/{personal-center/api → app/api/personal-center}/profile/api.ts +0 -0
  282. /package/templates/src/{personal-center/api → app/api/personal-center}/upload/api.ts +0 -0
  283. /package/templates/src/{system/api → app/api/system}/departments/api.ts +0 -0
  284. /package/templates/src/{system/api → app/api/system}/departments/detach/api.ts +0 -0
  285. /package/templates/src/{system/api → app/api/system}/departments/members/api.ts +0 -0
  286. /package/templates/src/{system/api → app/api/system}/departments/move/api.ts +0 -0
  287. /package/templates/src/{system/api → app/api/system}/login-methods/api.ts +0 -0
  288. /package/templates/src/{system/api → app/api/system}/manifest.yaml +0 -0
  289. /package/templates/src/{system/api → app/api/system}/permissions/api.ts +0 -0
  290. /package/templates/src/{system/api → app/api/system}/roles/bindings/api.ts +0 -0
  291. /package/templates/src/{system/api → app/api/system}/settings/api.ts +0 -0
  292. /package/templates/src/{system/api → app/api/system}/users/api.ts +0 -0
  293. /package/templates/src/{system/api → app/api/system}/users/forbidden/api.ts +0 -0
  294. /package/templates/src/{system/api → app/api/system}/users/password/api.ts +0 -0
  295. /package/templates/src/{system/api → app/api/system}/users/profile/api.ts +0 -0
  296. /package/templates/src/{system/api → app/api/system}/users/roles/api.ts +0 -0
  297. /package/templates/src/{web/api → app/api/web}/get-async-routes/api.ts +0 -0
  298. /package/templates/src/{web/api → app/api/web}/hello/api.ts +0 -0
  299. /package/templates/src/{web/api → app/api/web}/manifest.yaml +0 -0
  300. /package/templates/src/{web/api → app/api/web}/user-info/api.ts +0 -0
  301. /package/templates/src/{_platform/web → app/web/_platform}/__client/api.schemas.ts +0 -0
  302. /package/templates/src/{auth/web → app/web/auth}/__client/api.schemas.ts +0 -0
  303. /package/templates/src/app/web/home/__client/{api.ts → index.ts} +0 -0
  304. /package/templates/src/app/web/notification/__client/{api.ts → index.ts} +0 -0
  305. /package/templates/src/app/web/personal-center/__client/{api.ts → index.ts} +0 -0
  306. /package/templates/src/app/web/system/__client/{api.ts → index.ts} +0 -0
  307. /package/templates/src/{web → app/web}/web/__client/api.schemas.ts +0 -0
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
  }
package/src/bundle.ts CHANGED
@@ -308,6 +308,16 @@ async function assembleBundle(projectRoot: string, layout: ProjectLayout, plan:
308
308
  }
309
309
  fs.mkdirSync(path.join(root, "web", "modules"), { recursive: true });
310
310
  fs.cpSync(ownWeb, path.join(root, "web", "modules", name), { recursive: true });
311
+ // R1 数据源随包(计划 docs/plans/20261103-0948):安装期角色 title 解析读
312
+ // <包>/web/modules/<name>/locales/<lang>.json——构建产物不含 locales(源码 JSON
313
+ // 被 vite 内联进 JS chunk),故从源码树复制双语;目录不存在(纯后端段缺 locales)静默跳过。
314
+ for (const lang of ["zh-CN", "en-US"]) {
315
+ const src = path.join(webSrcOf(layout, name), "locales", `${lang}.json`);
316
+ if (fs.existsSync(src)) {
317
+ fs.mkdirSync(path.join(root, "web", "modules", name, "locales"), { recursive: true });
318
+ fs.copyFileSync(src, path.join(root, "web", "modules", name, "locales", `${lang}.json`));
319
+ }
320
+ }
311
321
  const web = m.web as Record<string, unknown>;
312
322
  for (const rel of [web.entry, ...(Array.isArray(web.css) ? web.css : [])]) {
313
323
  if (typeof rel === "string" && !fs.existsSync(path.join(root, "web", rel))) {
@@ -365,6 +375,57 @@ export async function buildBundle(projectRoot: string, opts: { out?: string, nam
365
375
  return results;
366
376
  }
367
377
 
378
+ /**
379
+ * `ojm build` 末尾的用户方案摘要(2026-11-03,计划 docs/plans/20261103-0925):
380
+ * 明确回答「我的方案构建进了吗、能不能打包」。判定口径与 resolveContractTargets 同源
381
+ * (模块级契约 ∪ 根契约,减内置名单)——摘要里列出的就是 --bundle 将要打包的那批。
382
+ * 只报告不裁决:不调 resolveLayout(构建末尾不因摘要崩溃),log 可注入供测试断言。
383
+ */
384
+ export function summarizeBuiltSolutions(projectRoot: string, built: readonly { name: string, version: string }[], log: (line: string) => void = console.log): void {
385
+ const builtin = readBuiltinModules(projectRoot);
386
+ const rootSolution = path.join(projectRoot, "solution.yaml");
387
+ const rootManifest = path.join(projectRoot, "manifest.yaml");
388
+ const rootContract = fs.existsSync(rootSolution) ? rootSolution : fs.existsSync(rootManifest) ? rootManifest : undefined;
389
+ const rootName = rootContract ? peekContractName(rootContract) : undefined;
390
+
391
+ // 用户方案候选:模块级契约(非内置)∪ 根契约 name(按名去重——「根压 src」只是打包取哪份契约,摘要里是同一个方案)
392
+ const names = new Set<string>();
393
+ for (const c of collectModuleContracts(projectRoot)) {
394
+ if (!builtin.has(c.moduleDir!))
395
+ names.add(c.moduleDir!);
396
+ }
397
+ if (rootName !== undefined && !builtin.has(rootName))
398
+ names.add(rootName);
399
+
400
+ const builtByName = new Map(built.map(b => [b.name, b]));
401
+ if (names.size === 0) {
402
+ log("[ojm] 用户方案 0 份(ojm new <name> 创建后即可 ojm build --bundle 打包)");
403
+ }
404
+ else {
405
+ const all = [...names].sort();
406
+ const ok = all.filter(n => builtByName.has(n));
407
+ const missing = all.filter(n => !builtByName.has(n));
408
+ const builtList = ok.map(n => `${n}@${builtByName.get(n)!.version}`).join("、") || "无";
409
+ if (missing.length === 0)
410
+ log(`[ojm] 用户方案 ${all.length} 份已构建:${builtList}(ojm build --bundle 打包)`);
411
+ else
412
+ log(`[ojm] 用户方案 ${all.length} 份中 ${ok.length} 份已构建:${builtList};未构建:${missing.join("、")}(查 web.config.ts 登记/enabled)`);
413
+ }
414
+
415
+ const builtinBuilt = built.filter(b => builtin.has(b.name));
416
+ if (builtinBuilt.length > 0)
417
+ log(`[ojm] 框架自带模块 ${builtinBuilt.length} 份已构建(不参与用户打包)`);
418
+
419
+ // module-root 口径(src/ 树在即适用;flat/new 工程模块不在 src/):非内置且无契约的模块不会被打包——提前点名,别等 --bundle 才发现
420
+ if (fs.existsSync(path.join(projectRoot, "src"))) {
421
+ for (const b of built) {
422
+ if (builtin.has(b.name) || names.has(b.name))
423
+ continue;
424
+ log(`[ojm] ⚠️ 模块 ${b.name}@${b.version} 缺方案契约(src/${b.name}/solution.yaml),不会被打包`);
425
+ }
426
+ }
427
+ }
428
+
368
429
  /**
369
430
  * C2-31 分层核对:模块级 manifest 仅 oj 原生键,且 name/version 与 bundle 级逐一相等
370
431
  * (oj 启动强校验同源)。
package/src/cli.ts CHANGED
@@ -135,7 +135,10 @@ function makeAction(def: HelpEntry, projectRoot: string, lang: Lang, exit: (code
135
135
  }
136
136
  const { buildBackend, buildModules } = await import("./build");
137
137
  await buildBackend(projectRoot);
138
- await buildModules(projectRoot, { mergeSite: true });
138
+ const built = await buildModules(projectRoot, { mergeSite: true });
139
+ // 用户方案摘要(计划 20261103-0925):构建末尾点名「我的方案」是否进构建、能否打包
140
+ const { summarizeBuiltSolutions } = await import("./bundle");
141
+ summarizeBuiltSolutions(projectRoot, built);
139
142
  };
140
143
  case "new":
141
144
  return async (name: string | undefined, extra: string[] = [], opts: { layout?: string, api?: boolean } = {}) => {
@@ -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
@@ -21,7 +21,7 @@ import { BUILTIN_LIST_FILE, writeBuiltinList } from "./builtin";
21
21
  import { resolveCertDir, resolveConfigPath } from "./layout";
22
22
  import { probeOjCaps } from "./oj-caps";
23
23
  import { mintDevCert } from "./oj-cert";
24
- import { isSupportedOjVersion, MIN_OJ_VERSION, resolveOjBin, vendorCommand } from "./vendor";
24
+ import { isSupportedOjVersion, MIN_OJ_VERSION, resolveOjBin, syncOjDevkit, vendorCommand } from "./vendor";
25
25
  import { readHostVersions } from "./versions";
26
26
 
27
27
  export interface InitOptions {
@@ -167,25 +167,16 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
167
167
  // oj 不可用/不支持 secret → 保留明文并告警(init 不因加密失败而失败)
168
168
  await sealJwtSecret(destDir, report, opts.seal);
169
169
 
170
- // 4) devkit 分发:agent skill + 全局类型
171
- const devkit = path.join(binDir, "devkit");
172
- if (fs.existsSync(path.join(devkit, "SKILL.md"))) {
173
- const skillDir = path.join(destDir, ".claude/skills/oj-api-dev");
174
- fs.mkdirSync(skillDir, { recursive: true });
175
- for (const name of ["SKILL.md", "api-manual.md"])
176
- copyIfAbsent(path.join(devkit, name), path.join(skillDir, name), report);
177
- // v0.1.82 起 devkit 新增两份专题指南(api-manual/SKILL 均引用它们):
178
- // SQL 构造器能力矩阵/方言差异、oj test L1 测试指南。它们是**增量文档**、
179
- // 不影响 ojm 运行,故「有则拷、无则记跳过」——bin/devkit 来自用户本地
180
- // oj 版本(MIN_OJ_VERSION 只钉到 0.1.73),旧版 devkit 没有这两个文件,
181
- // 为两份文档把整个 init 打挂不值当(用户升 oj 后重跑 init 即补上)。
182
- for (const name of ["sql-builder-guide.md", "oj-test-guide.md"])
183
- copyIfPresent(path.join(devkit, name), path.join(skillDir, name), report);
184
- copyIfAbsent(path.join(devkit, "global.d.ts"), path.join(destDir, "global.d.ts"), report);
185
- }
186
- else {
170
+ // 4) devkit 分发:agent skill(与 vendor 同一同步出口 syncOjDevkit:bin/devkit 全量
171
+ // .md **覆盖**进 .claude/skills/oj-api-dev——文档随 oj 版本同步,见计划 20261103-1020)
172
+ // + 全局类型(模板已落则 copyIfAbsent 跳过,模板变体优先、用户可编辑)
173
+ const devkitStatus = syncOjDevkit(destDir, binDir);
174
+ if (devkitStatus.ok)
175
+ report.created.push(`.claude/skills/oj-api-dev(oj devkit 文档已同步 ${devkitStatus.files.length} 个文件)`);
176
+ else
187
177
  report.skipped.push(".claude/skills/oj-api-dev(bin/devkit 缺失,跳过)");
188
- }
178
+ if (fs.existsSync(path.join(binDir, "devkit", "global.d.ts")))
179
+ copyIfAbsent(path.join(binDir, "devkit", "global.d.ts"), path.join(destDir, "global.d.ts"), report);
189
180
 
190
181
  // 4b) ojm 模块开发 skill 分发:从 cli 包内置 devkit(prepack 同步的
191
182
  // docs/devkit)拷到 .claude/skills/ojm-module-dev/;已存在则幂等跳过。
@@ -197,7 +188,8 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
197
188
  // 业务工程拿到模板后自助完成对接要用,不能只留本仓:
198
189
  // · `wecom-daikaifa.md` 企业微信代开发(服务商模板):凭据清单 + 用户/开发/部署三视角;
199
190
  // · `wecom-internal-app.md` 企业内部应用(自建):五项 corp 凭据 + 验证并接入 + 共存。
200
- for (const name of ["SKILL.md", "manual.md", "README.md", "wecom-daikaifa.md", "wecom-internal-app.md"])
191
+ // · `publish-walkthrough.md` 方案包发布全流程(新手照做版):打包/签名/CDN/安装一条链。
192
+ for (const name of ["SKILL.md", "manual.md", "README.md", "wecom-daikaifa.md", "wecom-internal-app.md", "publish-walkthrough.md"])
201
193
  copyIfAbsent(path.join(cliDevkit, name), path.join(ojmSkillDir, name), report);
202
194
  }
203
195
  else {
@@ -240,15 +232,22 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
240
232
  // 与 pnpm-workspace.yaml 同型,改由 init 现场生成
241
233
  ensureGitignore(destDir, report);
242
234
 
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 侧保留)。`);
235
+ // 5d) 旧布局冲突提示:0.2.54 起因/app 同时承载前端聚合模块与后端模块。
236
+ // init 只补缺不删除——老工程重跑 init 会同时留下旧目录:
237
+ // · src/<name>/web 与 src/app/web 同名路由注册两遍(菜单重复);
238
+ // · src/<name>/api 与 src/app/api/<name> 模块名重复(ojm 扫描直接报错)。
239
+ const mergedWeb = path.join(destDir, "src", "app", "web", "entry.ts");
240
+ const mergedApi = path.join(destDir, "src", "app", "api");
241
+ if (fs.existsSync(mergedWeb) || fs.existsSync(mergedApi)) {
242
+ const appModules = ["home", "login", "personal-center", "notification", "system", "_platform", "auth", "web"];
243
+ const oldWeb = appModules.filter(name => fs.existsSync(path.join(destDir, "src", name, "web", "entry.ts")));
244
+ if (oldWeb.length > 0)
245
+ report.warnings.push(`检测到旧布局的前端模块目录(${oldWeb.map(n => `src/${n}/web`).join("、")})与新聚合模块 src/app/web 并存——路由会重复注册。请手工删除这些旧 web 目录。`);
246
+ const oldApi = appModules.filter(name =>
247
+ fs.existsSync(path.join(destDir, "src", name, "api", "manifest.yaml"))
248
+ && fs.existsSync(path.join(mergedApi, name, "manifest.yaml")));
249
+ if (oldApi.length > 0)
250
+ report.warnings.push(`检测到旧布局的后端模块目录(${oldApi.map(n => `src/${n}/api`).join("、")})与新位置 src/app/api/<name> 同名——模块名重复会让 ojm 扫描报错。请手工删除这些旧 api 目录。`);
252
251
  }
253
252
 
254
253
  console.log(`[ojm] init 完成:${destDir}`);
@@ -411,9 +410,10 @@ function pruneUnincludedApiPaths(destDir: string, include: Set<string>, report:
411
410
  // 只裁本次新建的 openapi.json:init 是幂等补缺,不改动既有工程文件
412
411
  if (!report.created.includes("openapi.json"))
413
412
  return;
414
- // 白名单里 `src/<name>/api` 这条(整模块收录)决定「脚手架有哪些后端模块」
413
+ // 白名单里的 api 侧条目决定「脚手架有哪些后端模块」:
414
+ // 平铺 `src/<name>/api` 与聚合容器 `src/app/api/<name>` 都要认。
415
415
  const mods = new Set([...include]
416
- .map(p => /^src\/([^/]+)\/api$/.exec(p)?.[1])
416
+ .map(p => /^src\/([^/]+)\/api$/.exec(p)?.[1] ?? /^src\/[^/]+\/api\/([^/]+)$/.exec(p)?.[1])
417
417
  .filter((m): m is string => !!m));
418
418
  if (mods.size === 0)
419
419
  return;
@@ -475,21 +475,6 @@ function copyIfAbsent(src: string, dest: string, report: InitReport): void {
475
475
  report.created.push(path.relative(process.cwd(), dest) || path.basename(dest));
476
476
  }
477
477
 
478
- /**
479
- * 同 copyIfAbsent,但**源缺失**时记一条跳过而非抛错。
480
- *
481
- * 用于从 bin/devkit 拷第三方版本化内容(两份专题指南):devkit 随用户本地 oj
482
- * 版本而变,旧版没有新文件属正常版本 skew,不是安装包损坏——缺了只少份文档,
483
- * 不该让 init 失败。源在而目标已存在仍走 copyIfAbsent 的幂等跳过。
484
- */
485
- function copyIfPresent(src: string, dest: string, report: InitReport): void {
486
- if (!fs.existsSync(src)) {
487
- report.skipped.push(`${path.basename(src)}(bin/devkit 无此文件,跳过)`);
488
- return;
489
- }
490
- copyIfAbsent(src, dest, report);
491
- }
492
-
493
478
  /**
494
479
  * 新工程 .gitignore:不存在则生成;已存在则幂等跳过(用户规则永不覆盖)。
495
480
  *
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
  /**
@@ -0,0 +1,106 @@
1
+ // BDD:`ojm build` 末尾用户方案摘要(计划 docs/plans/20261103-0925-ojm-build-solution-summary.md)。
2
+ // 摘要只报告不裁决:判定口径与 --bundle 打包对象同源(模块级契约 ∪ 根契约,减内置名单);
3
+ // log 注入断言,夹具只摆契约/名单/构建结果三样,不需要真实工程结构。
4
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
9
+ import { summarizeBuiltSolutions } from "../../bundle";
10
+
11
+ let project: string;
12
+ let lines: string[];
13
+
14
+ beforeEach(() => {
15
+ project = mkdtempSync(join(tmpdir(), "ojm-build-summary-"));
16
+ lines = [];
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(project, { recursive: true, force: true });
21
+ });
22
+
23
+ /** 模块级契约:src/<name>/solution.yaml(+ web/entry.ts 保持真实模块目录形态) */
24
+ function moduleContract(name: string): void {
25
+ const dir = join(project, "src", name);
26
+ mkdirSync(join(dir, "web"), { recursive: true });
27
+ writeFileSync(join(dir, "web", "entry.ts"), "export default {};\n");
28
+ writeFileSync(join(dir, "solution.yaml"), `name: ${name}\nversion: 1.0.0\nsummary: ${name}\n`);
29
+ }
30
+
31
+ function builtinList(names: string[]): void {
32
+ mkdirSync(join(project, "src"), { recursive: true });
33
+ writeFileSync(join(project, "src", ".ojm-builtin.json"), JSON.stringify({ modules: names }));
34
+ }
35
+
36
+ function summaryOf(built: { name: string, version: string }[]): string[] {
37
+ summarizeBuiltSolutions(project, built, l => lines.push(l));
38
+ return lines;
39
+ }
40
+
41
+ describe("summarizeBuiltSolutions", () => {
42
+ it("场景:全部构建成功 → 一行列出可打包方案 + 一行列出内置模块", () => {
43
+ moduleContract("crm");
44
+ moduleContract("erp");
45
+ builtinList(["home", "login"]);
46
+ const out = summaryOf([
47
+ { name: "crm", version: "1.0.0" },
48
+ { name: "erp", version: "0.2.0" },
49
+ { name: "home", version: "9.9.9" },
50
+ { name: "login", version: "9.9.9" },
51
+ ]);
52
+ expect(out.some(l => l.includes("用户方案 2 份已构建") && l.includes("crm@1.0.0") && l.includes("erp@0.2.0"))).toBe(true);
53
+ expect(out.some(l => l.includes("框架自带模块 2 份已构建"))).toBe(true);
54
+ });
55
+
56
+ it("场景:方案没进构建 → 点名未构建方案与排查方向", () => {
57
+ moduleContract("crm");
58
+ moduleContract("erp");
59
+ builtinList(["home"]);
60
+ const out = summaryOf([
61
+ { name: "crm", version: "1.0.0" },
62
+ { name: "home", version: "9.9.9" },
63
+ ]);
64
+ expect(out.some(l => l.includes("crm@1.0.0"))).toBe(true);
65
+ expect(out.filter(l => l.includes("erp")).join("\n")).toContain("未构建");
66
+ });
67
+
68
+ it("场景:零用户方案 → 提示 ojm new", () => {
69
+ moduleContract("home");
70
+ builtinList(["home"]);
71
+ const out = summaryOf([{ name: "home", version: "9.9.9" }]);
72
+ expect(out.some(l => l.includes("用户方案 0 份") && l.includes("ojm new"))).toBe(true);
73
+ });
74
+
75
+ it("场景:非内置模块缺契约 → 警告不会被打包", () => {
76
+ builtinList(["home"]);
77
+ const out = summaryOf([
78
+ { name: "foo", version: "0.1.0" },
79
+ { name: "home", version: "9.9.9" },
80
+ ]);
81
+ expect(out.some(l => l.includes("缺方案契约") && l.includes("foo@0.1.0"))).toBe(true);
82
+ });
83
+
84
+ it("场景:旧单方案工程(根契约、无 src/ 树)→ 列出根契约方案,不误报缺契约", () => {
85
+ writeFileSync(join(project, "manifest.yaml"), "name: demo-solution\nversion: 0.1.0\nsummary: 旧写法\n");
86
+ const out = summaryOf([{ name: "demo-solution", version: "0.1.0" }]);
87
+ expect(out.some(l => l.includes("用户方案 1 份已构建") && l.includes("demo-solution@0.1.0"))).toBe(true);
88
+ expect(out.every(l => !l.includes("缺方案契约"))).toBe(true);
89
+ });
90
+
91
+ it("场景:新脚手架形态(内置集中 src/app,无用户方案)→ 0 份提示,src/app 不被误判", () => {
92
+ // 脚手架 init 后:src/ 下只有 app(平台聚合树,无契约)与 oj-market,均在内置名单
93
+ mkdirSync(join(project, "src", "app", "web"), { recursive: true });
94
+ writeFileSync(join(project, "src", "app", "web", "entry.ts"), "export default {};\n");
95
+ mkdirSync(join(project, "src", "oj-market", "web"), { recursive: true });
96
+ writeFileSync(join(project, "src", "oj-market", "web", "entry.ts"), "export default {};\n");
97
+ builtinList(["app", "oj-market", "_platform", "auth", "notification", "personal-center", "system", "web"]);
98
+ const out = summaryOf([
99
+ { name: "app", version: "0.2.56" },
100
+ { name: "oj-market", version: "0.2.56" },
101
+ ]);
102
+ expect(out.some(l => l.includes("用户方案 0 份") && l.includes("ojm new"))).toBe(true);
103
+ expect(out.some(l => l.includes("框架自带模块 2 份已构建"))).toBe(true);
104
+ expect(out.every(l => !l.includes("缺方案契约"))).toBe(true);
105
+ });
106
+ });