openxiangda-cli 2.0.0-alpha.49 → 2.0.0-alpha.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 (321) hide show
  1. package/README.md +4 -18
  2. package/bin/run.js +67 -1
  3. package/dist/base.d.ts +36 -7
  4. package/dist/base.d.ts.map +1 -1
  5. package/dist/base.js +95 -18
  6. package/dist/base.js.map +1 -1
  7. package/dist/commands/check.d.ts +14 -1
  8. package/dist/commands/check.d.ts.map +1 -1
  9. package/dist/commands/create.d.ts +23 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +93 -0
  12. package/dist/commands/create.js.map +1 -0
  13. package/dist/commands/deploy.d.ts +19 -8
  14. package/dist/commands/deploy.d.ts.map +1 -1
  15. package/dist/commands/deploy.js +29 -16
  16. package/dist/commands/deploy.js.map +1 -1
  17. package/dist/commands/dev.d.ts +14 -3
  18. package/dist/commands/dev.d.ts.map +1 -1
  19. package/dist/commands/dev.js +4 -8
  20. package/dist/commands/dev.js.map +1 -1
  21. package/dist/commands/login.d.ts +24 -0
  22. package/dist/commands/login.d.ts.map +1 -0
  23. package/dist/commands/{auth/login.js → login.js} +27 -9
  24. package/dist/commands/login.js.map +1 -0
  25. package/dist/commands/logs.d.ts +15 -2
  26. package/dist/commands/logs.d.ts.map +1 -1
  27. package/dist/commands/logs.js +2 -2
  28. package/dist/commands/logs.js.map +1 -1
  29. package/dist/commands/rollback.d.ts +15 -4
  30. package/dist/commands/rollback.d.ts.map +1 -1
  31. package/dist/commands/rollback.js +11 -4
  32. package/dist/commands/rollback.js.map +1 -1
  33. package/dist/commands/status.d.ts +14 -1
  34. package/dist/commands/status.d.ts.map +1 -1
  35. package/dist/commands/status.js +1 -1
  36. package/dist/commands/status.js.map +1 -1
  37. package/dist/create-workspace.d.ts +23 -0
  38. package/dist/create-workspace.d.ts.map +1 -0
  39. package/dist/create-workspace.js +279 -0
  40. package/dist/create-workspace.js.map +1 -0
  41. package/package.json +14 -7
  42. package/template/.dockerignore +12 -0
  43. package/template/.env.example +3 -0
  44. package/template/AGENTS.md +8 -0
  45. package/template/README.md +23 -0
  46. package/template/_gitignore +14 -0
  47. package/template/apps/server/Dockerfile +19 -0
  48. package/template/apps/server/package.json +27 -0
  49. package/template/apps/server/src/app.module.ts +32 -0
  50. package/template/apps/server/src/instrument-context.controller.ts +25 -0
  51. package/template/apps/server/src/main.ts +18 -0
  52. package/template/apps/server/test/smoke.test.ts +28 -0
  53. package/template/apps/server/tsconfig.json +9 -0
  54. package/template/apps/web/e2e/instruments.spec.ts +150 -0
  55. package/template/apps/web/index.html +12 -0
  56. package/template/apps/web/package.json +33 -0
  57. package/template/apps/web/playwright.config.ts +21 -0
  58. package/template/apps/web/scripts/check.mjs +32 -0
  59. package/template/apps/web/scripts/verify-build.mjs +20 -0
  60. package/template/apps/web/src/InstrumentDetailPage.tsx +60 -0
  61. package/template/apps/web/src/InstrumentForm.tsx +322 -0
  62. package/template/apps/web/src/InstrumentFormPage.tsx +77 -0
  63. package/template/apps/web/src/InstrumentListPage.tsx +390 -0
  64. package/template/apps/web/src/Shell.tsx +52 -0
  65. package/template/apps/web/src/data-provider.ts +42 -0
  66. package/template/apps/web/src/fields.ts +72 -0
  67. package/template/apps/web/src/instrument.ts +55 -0
  68. package/template/apps/web/src/main.tsx +58 -0
  69. package/template/apps/web/src/platform-client.ts +246 -0
  70. package/template/apps/web/src/runtime-meta.ts +60 -0
  71. package/template/apps/web/src/runtime.tsx +73 -0
  72. package/template/apps/web/src/styles.css +84 -0
  73. package/template/apps/web/test/contracts.test.ts +234 -0
  74. package/template/apps/web/test/loopback.test.ts +62 -0
  75. package/template/apps/web/tsconfig.json +12 -0
  76. package/template/apps/web/vite.config.ts +32 -0
  77. package/template/openxiangda.config.ts +77 -0
  78. package/template/package.json +29 -0
  79. package/template/packages/contracts/package.json +23 -0
  80. package/template/packages/contracts/src/generated.ts +129 -0
  81. package/template/packages/contracts/src/index.ts +1 -0
  82. package/template/packages/contracts/tsconfig.json +9 -0
  83. package/template/platform/data/instruments.ts +81 -0
  84. package/template/pnpm-workspace.yaml +7 -0
  85. package/template/scripts/verify-template-budget.mjs +37 -0
  86. package/template/tsconfig.base.json +13 -0
  87. package/dist/commands/app/create.d.ts +0 -15
  88. package/dist/commands/app/create.d.ts.map +0 -1
  89. package/dist/commands/app/create.js +0 -42
  90. package/dist/commands/app/create.js.map +0 -1
  91. package/dist/commands/app/info.d.ts +0 -9
  92. package/dist/commands/app/info.d.ts.map +0 -1
  93. package/dist/commands/app/info.js +0 -10
  94. package/dist/commands/app/info.js.map +0 -1
  95. package/dist/commands/app/link.d.ts +0 -10
  96. package/dist/commands/app/link.d.ts.map +0 -1
  97. package/dist/commands/app/link.js +0 -16
  98. package/dist/commands/app/link.js.map +0 -1
  99. package/dist/commands/app/provision.d.ts +0 -9
  100. package/dist/commands/app/provision.d.ts.map +0 -1
  101. package/dist/commands/app/provision.js +0 -10
  102. package/dist/commands/app/provision.js.map +0 -1
  103. package/dist/commands/auth/login.d.ts +0 -11
  104. package/dist/commands/auth/login.d.ts.map +0 -1
  105. package/dist/commands/auth/login.js.map +0 -1
  106. package/dist/commands/auth/logout.d.ts +0 -6
  107. package/dist/commands/auth/logout.d.ts.map +0 -1
  108. package/dist/commands/auth/logout.js +0 -30
  109. package/dist/commands/auth/logout.js.map +0 -1
  110. package/dist/commands/auth/status.d.ts +0 -6
  111. package/dist/commands/auth/status.d.ts.map +0 -1
  112. package/dist/commands/auth/status.js +0 -60
  113. package/dist/commands/auth/status.js.map +0 -1
  114. package/dist/commands/authz/membership/grant.d.ts +0 -16
  115. package/dist/commands/authz/membership/grant.d.ts.map +0 -1
  116. package/dist/commands/authz/membership/grant.js +0 -32
  117. package/dist/commands/authz/membership/grant.js.map +0 -1
  118. package/dist/commands/authz/membership/list.d.ts +0 -16
  119. package/dist/commands/authz/membership/list.d.ts.map +0 -1
  120. package/dist/commands/authz/membership/list.js +0 -34
  121. package/dist/commands/authz/membership/list.js.map +0 -1
  122. package/dist/commands/authz/membership/revoke.d.ts +0 -15
  123. package/dist/commands/authz/membership/revoke.d.ts.map +0 -1
  124. package/dist/commands/authz/membership/revoke.js +0 -22
  125. package/dist/commands/authz/membership/revoke.js.map +0 -1
  126. package/dist/commands/authz/membership/update.d.ts +0 -18
  127. package/dist/commands/authz/membership/update.d.ts.map +0 -1
  128. package/dist/commands/authz/membership/update.js +0 -31
  129. package/dist/commands/authz/membership/update.js.map +0 -1
  130. package/dist/commands/authz/relationship/grant.d.ts +0 -20
  131. package/dist/commands/authz/relationship/grant.d.ts.map +0 -1
  132. package/dist/commands/authz/relationship/grant.js +0 -40
  133. package/dist/commands/authz/relationship/grant.js.map +0 -1
  134. package/dist/commands/authz/relationship/list.d.ts +0 -18
  135. package/dist/commands/authz/relationship/list.d.ts.map +0 -1
  136. package/dist/commands/authz/relationship/list.js +0 -43
  137. package/dist/commands/authz/relationship/list.js.map +0 -1
  138. package/dist/commands/authz/relationship/revoke.d.ts +0 -15
  139. package/dist/commands/authz/relationship/revoke.d.ts.map +0 -1
  140. package/dist/commands/authz/relationship/revoke.js +0 -22
  141. package/dist/commands/authz/relationship/revoke.js.map +0 -1
  142. package/dist/commands/authz/relationship/update.d.ts +0 -18
  143. package/dist/commands/authz/relationship/update.d.ts.map +0 -1
  144. package/dist/commands/authz/relationship/update.js +0 -28
  145. package/dist/commands/authz/relationship/update.js.map +0 -1
  146. package/dist/commands/authz/super-admin/grant.d.ts +0 -12
  147. package/dist/commands/authz/super-admin/grant.d.ts.map +0 -1
  148. package/dist/commands/authz/super-admin/grant.js +0 -20
  149. package/dist/commands/authz/super-admin/grant.js.map +0 -1
  150. package/dist/commands/authz/super-admin/list.d.ts +0 -15
  151. package/dist/commands/authz/super-admin/list.d.ts.map +0 -1
  152. package/dist/commands/authz/super-admin/list.js +0 -27
  153. package/dist/commands/authz/super-admin/list.js.map +0 -1
  154. package/dist/commands/authz/super-admin/revoke.d.ts +0 -15
  155. package/dist/commands/authz/super-admin/revoke.d.ts.map +0 -1
  156. package/dist/commands/authz/super-admin/revoke.js +0 -21
  157. package/dist/commands/authz/super-admin/revoke.js.map +0 -1
  158. package/dist/commands/build.d.ts +0 -11
  159. package/dist/commands/build.d.ts.map +0 -1
  160. package/dist/commands/build.js +0 -23
  161. package/dist/commands/build.js.map +0 -1
  162. package/dist/commands/cancel.d.ts +0 -12
  163. package/dist/commands/cancel.d.ts.map +0 -1
  164. package/dist/commands/cancel.js +0 -12
  165. package/dist/commands/cancel.js.map +0 -1
  166. package/dist/commands/dev/reset.d.ts +0 -10
  167. package/dist/commands/dev/reset.d.ts.map +0 -1
  168. package/dist/commands/dev/reset.js +0 -17
  169. package/dist/commands/dev/reset.js.map +0 -1
  170. package/dist/commands/dev/status.d.ts +0 -9
  171. package/dist/commands/dev/status.d.ts.map +0 -1
  172. package/dist/commands/dev/status.js +0 -10
  173. package/dist/commands/dev/status.js.map +0 -1
  174. package/dist/commands/dev/stop.d.ts +0 -9
  175. package/dist/commands/dev/stop.d.ts.map +0 -1
  176. package/dist/commands/dev/stop.js +0 -10
  177. package/dist/commands/dev/stop.js.map +0 -1
  178. package/dist/commands/doctor.d.ts +0 -9
  179. package/dist/commands/doctor.d.ts.map +0 -1
  180. package/dist/commands/doctor.js +0 -7
  181. package/dist/commands/doctor.js.map +0 -1
  182. package/dist/commands/environment/start.d.ts +0 -13
  183. package/dist/commands/environment/start.d.ts.map +0 -1
  184. package/dist/commands/environment/start.js +0 -22
  185. package/dist/commands/environment/start.js.map +0 -1
  186. package/dist/commands/environment/status.d.ts +0 -9
  187. package/dist/commands/environment/status.d.ts.map +0 -1
  188. package/dist/commands/environment/status.js +0 -10
  189. package/dist/commands/environment/status.js.map +0 -1
  190. package/dist/commands/environment/stop.d.ts +0 -13
  191. package/dist/commands/environment/stop.d.ts.map +0 -1
  192. package/dist/commands/environment/stop.js +0 -22
  193. package/dist/commands/environment/stop.js.map +0 -1
  194. package/dist/commands/event/delivery/list.d.ts +0 -10
  195. package/dist/commands/event/delivery/list.d.ts.map +0 -1
  196. package/dist/commands/event/delivery/list.js +0 -14
  197. package/dist/commands/event/delivery/list.js.map +0 -1
  198. package/dist/commands/event/delivery/replay.d.ts +0 -13
  199. package/dist/commands/event/delivery/replay.d.ts.map +0 -1
  200. package/dist/commands/event/delivery/replay.js +0 -16
  201. package/dist/commands/event/delivery/replay.js.map +0 -1
  202. package/dist/commands/event/subscription/list.d.ts +0 -9
  203. package/dist/commands/event/subscription/list.d.ts.map +0 -1
  204. package/dist/commands/event/subscription/list.js +0 -10
  205. package/dist/commands/event/subscription/list.js.map +0 -1
  206. package/dist/commands/event/subscription/rotate-secret.d.ts +0 -15
  207. package/dist/commands/event/subscription/rotate-secret.d.ts.map +0 -1
  208. package/dist/commands/event/subscription/rotate-secret.js +0 -33
  209. package/dist/commands/event/subscription/rotate-secret.js.map +0 -1
  210. package/dist/commands/event/subscription/status.d.ts +0 -14
  211. package/dist/commands/event/subscription/status.d.ts.map +0 -1
  212. package/dist/commands/event/subscription/status.js +0 -19
  213. package/dist/commands/event/subscription/status.js.map +0 -1
  214. package/dist/commands/event/timer/fire.d.ts +0 -12
  215. package/dist/commands/event/timer/fire.d.ts.map +0 -1
  216. package/dist/commands/event/timer/fire.js +0 -12
  217. package/dist/commands/event/timer/fire.js.map +0 -1
  218. package/dist/commands/event/timer/list.d.ts +0 -9
  219. package/dist/commands/event/timer/list.d.ts.map +0 -1
  220. package/dist/commands/event/timer/list.js +0 -10
  221. package/dist/commands/event/timer/list.js.map +0 -1
  222. package/dist/commands/event/timer/status.d.ts +0 -14
  223. package/dist/commands/event/timer/status.d.ts.map +0 -1
  224. package/dist/commands/event/timer/status.js +0 -19
  225. package/dist/commands/event/timer/status.js.map +0 -1
  226. package/dist/commands/generate.d.ts +0 -10
  227. package/dist/commands/generate.d.ts.map +0 -1
  228. package/dist/commands/generate.js +0 -8
  229. package/dist/commands/generate.js.map +0 -1
  230. package/dist/commands/oauth/audit/list.d.ts +0 -12
  231. package/dist/commands/oauth/audit/list.d.ts.map +0 -1
  232. package/dist/commands/oauth/audit/list.js +0 -20
  233. package/dist/commands/oauth/audit/list.js.map +0 -1
  234. package/dist/commands/oauth/client/create.d.ts +0 -13
  235. package/dist/commands/oauth/client/create.d.ts.map +0 -1
  236. package/dist/commands/oauth/client/create.js +0 -31
  237. package/dist/commands/oauth/client/create.js.map +0 -1
  238. package/dist/commands/oauth/client/list.d.ts +0 -9
  239. package/dist/commands/oauth/client/list.d.ts.map +0 -1
  240. package/dist/commands/oauth/client/list.js +0 -10
  241. package/dist/commands/oauth/client/list.js.map +0 -1
  242. package/dist/commands/oauth/client/revoke.d.ts +0 -12
  243. package/dist/commands/oauth/client/revoke.d.ts.map +0 -1
  244. package/dist/commands/oauth/client/revoke.js +0 -12
  245. package/dist/commands/oauth/client/revoke.js.map +0 -1
  246. package/dist/commands/oauth/client/rotate.d.ts +0 -13
  247. package/dist/commands/oauth/client/rotate.d.ts.map +0 -1
  248. package/dist/commands/oauth/client/rotate.js +0 -20
  249. package/dist/commands/oauth/client/rotate.js.map +0 -1
  250. package/dist/commands/oauth/client/update.d.ts +0 -15
  251. package/dist/commands/oauth/client/update.d.ts.map +0 -1
  252. package/dist/commands/oauth/client/update.js +0 -23
  253. package/dist/commands/oauth/client/update.js.map +0 -1
  254. package/dist/commands/oauth/runtime/rotate.d.ts +0 -12
  255. package/dist/commands/oauth/runtime/rotate.d.ts.map +0 -1
  256. package/dist/commands/oauth/runtime/rotate.js +0 -28
  257. package/dist/commands/oauth/runtime/rotate.js.map +0 -1
  258. package/dist/commands/oauth/runtime/status.d.ts +0 -10
  259. package/dist/commands/oauth/runtime/status.d.ts.map +0 -1
  260. package/dist/commands/oauth/runtime/status.js +0 -13
  261. package/dist/commands/oauth/runtime/status.js.map +0 -1
  262. package/dist/commands/promote.d.ts +0 -13
  263. package/dist/commands/promote.d.ts.map +0 -1
  264. package/dist/commands/promote.js +0 -9
  265. package/dist/commands/promote.js.map +0 -1
  266. package/dist/commands/retry.d.ts +0 -12
  267. package/dist/commands/retry.d.ts.map +0 -1
  268. package/dist/commands/retry.js +0 -9
  269. package/dist/commands/retry.js.map +0 -1
  270. package/dist/commands/secret/audit/list.d.ts +0 -14
  271. package/dist/commands/secret/audit/list.d.ts.map +0 -1
  272. package/dist/commands/secret/audit/list.js +0 -16
  273. package/dist/commands/secret/audit/list.js.map +0 -1
  274. package/dist/commands/secret/create.d.ts +0 -18
  275. package/dist/commands/secret/create.d.ts.map +0 -1
  276. package/dist/commands/secret/create.js +0 -33
  277. package/dist/commands/secret/create.js.map +0 -1
  278. package/dist/commands/secret/delete.d.ts +0 -15
  279. package/dist/commands/secret/delete.d.ts.map +0 -1
  280. package/dist/commands/secret/delete.js +0 -20
  281. package/dist/commands/secret/delete.js.map +0 -1
  282. package/dist/commands/secret/list.d.ts +0 -10
  283. package/dist/commands/secret/list.d.ts.map +0 -1
  284. package/dist/commands/secret/list.js +0 -13
  285. package/dist/commands/secret/list.js.map +0 -1
  286. package/dist/commands/secret/rotate.d.ts +0 -18
  287. package/dist/commands/secret/rotate.d.ts.map +0 -1
  288. package/dist/commands/secret/rotate.js +0 -30
  289. package/dist/commands/secret/rotate.js.map +0 -1
  290. package/dist/commands/secret/update.d.ts +0 -17
  291. package/dist/commands/secret/update.d.ts.map +0 -1
  292. package/dist/commands/secret/update.js +0 -37
  293. package/dist/commands/secret/update.js.map +0 -1
  294. package/dist/commands/skill/install.d.ts +0 -11
  295. package/dist/commands/skill/install.d.ts.map +0 -1
  296. package/dist/commands/skill/install.js +0 -52
  297. package/dist/commands/skill/install.js.map +0 -1
  298. package/dist/commands/skill/validate.d.ts +0 -9
  299. package/dist/commands/skill/validate.d.ts.map +0 -1
  300. package/dist/commands/skill/validate.js +0 -32
  301. package/dist/commands/skill/validate.js.map +0 -1
  302. package/dist/commands/test.d.ts +0 -9
  303. package/dist/commands/test.d.ts.map +0 -1
  304. package/dist/commands/test.js +0 -7
  305. package/dist/commands/test.js.map +0 -1
  306. package/dist/commands/workflow/provider/list.d.ts +0 -10
  307. package/dist/commands/workflow/provider/list.d.ts.map +0 -1
  308. package/dist/commands/workflow/provider/list.js +0 -13
  309. package/dist/commands/workflow/provider/list.js.map +0 -1
  310. package/dist/commands/workflow/provider/rotate.d.ts +0 -15
  311. package/dist/commands/workflow/provider/rotate.d.ts.map +0 -1
  312. package/dist/commands/workflow/provider/rotate.js +0 -24
  313. package/dist/commands/workflow/provider/rotate.js.map +0 -1
  314. package/dist/native-authz.d.ts +0 -4
  315. package/dist/native-authz.d.ts.map +0 -1
  316. package/dist/native-authz.js +0 -25
  317. package/dist/native-authz.js.map +0 -1
  318. package/dist/secret-value.d.ts +0 -5
  319. package/dist/secret-value.d.ts.map +0 -1
  320. package/dist/secret-value.js +0 -15
  321. package/dist/secret-value.js.map +0 -1
package/package.json CHANGED
@@ -1,24 +1,31 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.49",
3
+ "version": "2.0.0-alpha.56",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
8
15
  "bin": {
9
16
  "openxiangda": "./bin/run.js"
10
17
  },
11
18
  "files": [
12
19
  "bin/",
13
20
  "dist/",
21
+ "template/",
14
22
  "README.md"
15
23
  ],
16
24
  "dependencies": {
17
25
  "@oclif/core": "4.13.3",
18
- "openxiangda-contracts": "2.0.0-alpha.23",
19
- "create-openxiangda": "2.0.0-alpha.42",
20
- "openxiangda-devkit-core": "2.0.0-alpha.28",
21
- "openxiangda-skill-kit": "2.0.0-alpha.39"
26
+ "openxiangda-contracts": "2.0.0-alpha.26",
27
+ "openxiangda-devkit-core": "2.0.0-alpha.33",
28
+ "openxiangda-mcp": "2.0.0-alpha.33"
22
29
  },
23
30
  "devDependencies": {
24
31
  "tsx": "4.23.12",
@@ -35,9 +42,9 @@
35
42
  },
36
43
  "license": "MIT",
37
44
  "scripts": {
38
- "build": "tsc -p tsconfig.json",
45
+ "build": "node ../../scripts/prune-package-dist.mjs && tsc -p tsconfig.json",
39
46
  "build:release": "node ../../scripts/prune-package-dist.mjs && pnpm build",
40
47
  "check": "tsc -p tsconfig.json --noEmit",
41
- "test": "tsx --test test/*.test.ts"
48
+ "test": "node scripts/prepare-template.mjs && tsx --test test/*.test.ts && node ../../scripts/verify-cli-blackbox.mjs"
42
49
  }
43
50
  }
@@ -0,0 +1,12 @@
1
+ node_modules
2
+ dist
3
+ coverage
4
+ playwright-report
5
+ test-results
6
+ .openxiangda
7
+ .turbo
8
+ .turbopack
9
+ .umi
10
+ .umi-production
11
+ .git
12
+ *.log
@@ -0,0 +1,3 @@
1
+ # openxiangda dev injects these values. Do not put platform tokens in this file.
2
+ OPENXIANGDA_APP_CODE=instrument-center
3
+ OPENXIANGDA_DEV_HOST=127.0.0.1
@@ -0,0 +1,8 @@
1
+ # Application development rules
2
+
3
+ - Build normal application code directly; generated Skills and historical templates are not design authority.
4
+ - Keep CRUD in the replaceable browser Data API adapter under `/service`.
5
+ - Put only real transactional or integration actions in Nest under `/api`.
6
+ - Read current-user role/capability unions from the platform. UI guards are presentation only; server authorization is authoritative.
7
+ - Do not add shadow scope fields. Use `collegeId` and `instrumentAdminIds` directly.
8
+ - Run `openxiangda check` before delivery.
@@ -0,0 +1,23 @@
1
+ # OpenXiangda 2.0 仪器应用
2
+
3
+ 这是最小黄金模板:Vite + React Router + Refine Core + Ant Design 前端、NestJS 后端,以及平台 Data/AuthZ 声明。
4
+
5
+ ```bash
6
+ openxiangda login --base-url https://platform.example.com
7
+ openxiangda create instrument-center
8
+ cd instrument-center
9
+ openxiangda dev
10
+ ```
11
+
12
+ `instruments` 覆盖 30 字段、列表筛选、服务端分页排序、新增、编辑、详情、删除和文件上传。所有 CRUD 位于 `apps/web/src/platform-client.ts` 的 Data API adapter;Nest 只保留一个无副作用 current-user 摘要动作。
13
+
14
+ 前端读取平台 current-user 的角色/能力并集进行展示保护,服务端始终权威。字段策略区分 create/update;无权字段在提交前从 payload 删除。
15
+
16
+ ```bash
17
+ openxiangda check
18
+ openxiangda deploy
19
+ openxiangda status
20
+ openxiangda logs
21
+ ```
22
+
23
+ `deploy` 使用本模板的 `apps/server/Dockerfile` 自动构建并推送 `linux/amd64` 镜像,再把不可变 digest 写入 AppPackage。请在本机安装并启动 Docker Buildx,并提前通过 Docker credential store 登录平台声明的镜像仓库;不要把 registry 密钥写进工作区。
@@ -0,0 +1,14 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ playwright-report/
5
+ test-results/
6
+ .openxiangda/
7
+ .turbo/
8
+ .turbopack/
9
+ .umi/
10
+ .umi-production/
11
+ .codegraph/
12
+ .env
13
+ *.log
14
+ .DS_Store
@@ -0,0 +1,19 @@
1
+ FROM node:22-alpine AS build
2
+ WORKDIR /workspace
3
+ RUN corepack enable
4
+ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
5
+ COPY apps/server/package.json apps/server/package.json
6
+ COPY packages/contracts/package.json packages/contracts/package.json
7
+ RUN pnpm --filter @app/server... install --frozen-lockfile --ignore-scripts
8
+ COPY apps/server apps/server
9
+ COPY packages/contracts packages/contracts
10
+ RUN pnpm --filter @app/server... build
11
+ RUN pnpm --filter @app/server deploy --prod --legacy --ignore-scripts /output
12
+
13
+ FROM node:22-alpine AS runtime
14
+ ENV NODE_ENV=production
15
+ WORKDIR /app
16
+ COPY --from=build --chown=1000:1000 /output /app
17
+ USER 1000:1000
18
+ EXPOSE 3000
19
+ CMD ["node", "dist/main.js"]
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@app/server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/main.ts",
8
+ "check": "tsc -p tsconfig.json --noEmit",
9
+ "test": "tsx --test test/*.test.ts",
10
+ "build": "tsc -p tsconfig.json",
11
+ "start": "node dist/main.js"
12
+ },
13
+ "dependencies": {
14
+ "@app/contracts": "workspace:*",
15
+ "@nestjs/common": "11.1.29",
16
+ "@nestjs/core": "11.1.29",
17
+ "@nestjs/platform-fastify": "11.1.29",
18
+ "openxiangda-nest": "2.0.0-alpha.31",
19
+ "reflect-metadata": "0.2.2",
20
+ "rxjs": "7.8.2"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "24.13.3",
24
+ "tsx": "4.23.12",
25
+ "typescript": "5.9.3"
26
+ }
27
+ }
@@ -0,0 +1,32 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { OpenXiangdaModule } from 'openxiangda-nest';
3
+ import { InstrumentContextController } from './instrument-context.controller.js';
4
+
5
+ @Module({
6
+ imports: [
7
+ OpenXiangdaModule.forRoot({
8
+ appCode: process.env.OPENXIANGDA_APP_CODE || 'instrument-center',
9
+ platformBaseUrl:
10
+ process.env.OPENXIANGDA_PLATFORM_BASE_URL ||
11
+ 'http://127.0.0.1:7001/service',
12
+ environmentKey:
13
+ process.env.OPENXIANGDA_ENVIRONMENT_KEY || 'preproduction',
14
+ environmentId:
15
+ process.env.OPENXIANGDA_ENVIRONMENT_ID || 'connected-development',
16
+ appVersionId:
17
+ process.env.OPENXIANGDA_APP_VERSION_ID || 'connected-development',
18
+ deploymentRunId:
19
+ process.env.OPENXIANGDA_DEPLOYMENT_RUN_ID || 'connected-development',
20
+ environmentHeadRevision: Number(
21
+ process.env.OPENXIANGDA_ENVIRONMENT_HEAD_REVISION || 1
22
+ ),
23
+ backendRevisionId:
24
+ process.env.OPENXIANGDA_BACKEND_REVISION_ID || 'connected-development',
25
+ version: process.env.OPENXIANGDA_APP_VERSION || '0.1.0-dev',
26
+ connectedDevelopment:
27
+ process.env.OPENXIANGDA_CONNECTED_DEV === 'true',
28
+ }),
29
+ ],
30
+ controllers: [InstrumentContextController],
31
+ })
32
+ export class AppModule {}
@@ -0,0 +1,25 @@
1
+ import { Controller, Get, UseGuards } from '@nestjs/common';
2
+ import { appOperations } from '@app/contracts';
3
+ import {
4
+ CurrentUser,
5
+ OpenXiangdaAuthzGuard,
6
+ OpenXiangdaOperation,
7
+ type OpenXiangdaCurrentUser,
8
+ } from 'openxiangda-nest';
9
+
10
+ @Controller('api/instruments')
11
+ @UseGuards(OpenXiangdaAuthzGuard)
12
+ export class InstrumentContextController {
13
+ @Get('context')
14
+ @OpenXiangdaOperation(appOperations.instrumentContextInspect)
15
+ inspect(@CurrentUser() user: OpenXiangdaCurrentUser) {
16
+ // Intentionally read-only: this demonstrates local Nest business actions
17
+ // without wrapping Data API CRUD.
18
+ return {
19
+ userId: user.userId,
20
+ roleCodes: user.roleCodes,
21
+ capabilityCodes: user.capabilityCodes,
22
+ environmentKey: user.environmentKey,
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,18 @@
1
+ import 'reflect-metadata';
2
+ import { NestFactory } from '@nestjs/core';
3
+ import {
4
+ FastifyAdapter,
5
+ type NestFastifyApplication,
6
+ } from '@nestjs/platform-fastify';
7
+ import { AppModule } from './app.module.js';
8
+
9
+ const app = await NestFactory.create<NestFastifyApplication>(
10
+ AppModule,
11
+ new FastifyAdapter({ trustProxy: true })
12
+ );
13
+ app.enableShutdownHooks();
14
+ const production = process.env.NODE_ENV === 'production';
15
+ await app.listen(
16
+ Number(process.env.OPENXIANGDA_APP_PORT || process.env.PORT || 3000),
17
+ production ? '0.0.0.0' : '127.0.0.1'
18
+ );
@@ -0,0 +1,28 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import test from 'node:test';
4
+ import { InstrumentContextController } from '../src/instrument-context.controller.js';
5
+
6
+ test('the example action returns only the verified current-user summary', () => {
7
+ const result = new InstrumentContextController().inspect({
8
+ userId: 'user-1',
9
+ appCode: 'instrument-center',
10
+ environmentKey: 'preproduction',
11
+ roleCodes: ['college_admin', 'instrument_admin'],
12
+ isAppSuperAdmin: false,
13
+ capabilityCodes: ['app:instrument-center:data:instruments:read'],
14
+ });
15
+ assert.deepEqual(result.roleCodes, ['college_admin', 'instrument_admin']);
16
+ assert.equal(result.userId, 'user-1');
17
+ });
18
+
19
+ test('the server template owns no CRUD, event, OAuth, or secret implementation', () => {
20
+ const moduleSource = readFileSync(new URL('../src/app.module.ts', import.meta.url), 'utf8');
21
+ const controllerSource = readFileSync(
22
+ new URL('../src/instrument-context.controller.ts', import.meta.url),
23
+ 'utf8'
24
+ );
25
+ assert.match(controllerSource, /@CurrentUser\(\)/);
26
+ assert.doesNotMatch(moduleSource + controllerSource, /oauthClient|eventSigning|RuntimeSecrets|Purchase|Workflow/);
27
+ assert.doesNotMatch(controllerSource, /createData|updateData|deleteData|queryData/);
28
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "sourceMap": true
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }
@@ -0,0 +1,150 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ test('renders the production warning and never requests a role session', async ({
4
+ page,
5
+ }) => {
6
+ const requests: string[] = [];
7
+ page.on('request', request => requests.push(request.url()));
8
+ await page.route('**/view/instrument-center/instruments', async route => {
9
+ if (route.request().resourceType() !== 'document') {
10
+ await route.continue();
11
+ return;
12
+ }
13
+ const response = await route.fetch();
14
+ const body = (await response.text()).replace(
15
+ '<head>',
16
+ `<head>
17
+ <meta name="openxiangda-runtime-base" content="/view/instrument-center/" />
18
+ <meta name="openxiangda-app-code" content="instrument-center" />
19
+ <meta name="openxiangda-environment" content="production" />`
20
+ );
21
+ await route.fulfill({ response, body });
22
+ });
23
+ await page.route('**/service/**', async route => {
24
+ const url = new URL(route.request().url());
25
+ if (
26
+ url.pathname.endsWith('/native/current-user') &&
27
+ url.searchParams.get('environmentKey') === 'production'
28
+ ) {
29
+ await route.fulfill({
30
+ contentType: 'application/json',
31
+ body: JSON.stringify({
32
+ code: 200,
33
+ data: {
34
+ schemaVersion: 'openxiangda.current-user/v2',
35
+ appCode: 'instrument-center',
36
+ environment: {
37
+ id: 'production-id',
38
+ key: 'production',
39
+ activeAppVersionId: 'app-version-1',
40
+ headRevision: 7,
41
+ authzRevisionId: 'authz-revision-1',
42
+ authzVersion: 3,
43
+ scopeDataVersion: 'scope-data-version-1',
44
+ },
45
+ principal: {
46
+ type: 'user',
47
+ userId: 'user-e2e',
48
+ roleCodes: ['college_admin', 'instrument_admin'],
49
+ capabilityCodes: [
50
+ 'app:instrument-center:data:instruments:read',
51
+ 'app:instrument-center:data:instruments:update',
52
+ 'app:instrument-center:instrument:context:read',
53
+ ],
54
+ isAppSuperAdmin: false,
55
+ },
56
+ },
57
+ }),
58
+ });
59
+ return;
60
+ }
61
+ if (url.pathname.endsWith('/native/data/instruments/query')) {
62
+ expect(route.request().postDataJSON()).toMatchObject({
63
+ schemaVersion: 'openxiangda.data-query/v2',
64
+ environmentKey: 'production',
65
+ });
66
+ await route.fulfill({
67
+ contentType: 'application/json',
68
+ body: JSON.stringify({
69
+ code: 200,
70
+ data: {
71
+ schemaVersion: 'openxiangda.data-page/v2',
72
+ resourceCode: 'instruments',
73
+ items: [
74
+ {
75
+ id: 'instrument-1',
76
+ revision: 2,
77
+ instrumentCode: 'INS-001',
78
+ chineseName: '场发射扫描电子显微镜',
79
+ specModel: 'SU8600',
80
+ categoryId: 'analysis_microscope',
81
+ assetCode: 'ASSET-001',
82
+ enabledDate: '2025-01-01',
83
+ usageStatus: 'active',
84
+ instrumentStatus: 'normal',
85
+ openToOutside: true,
86
+ collegeId: 'college-am',
87
+ manageDepartmentId: 'dept-am-test',
88
+ useDepartmentId: 'dept-am-test',
89
+ instrumentAdminIds: ['user-e2e'],
90
+ contactUserIds: ['user-e2e'],
91
+ contactPhone: '13800138000',
92
+ updatedAt: '2026-08-21T00:00:00.000Z',
93
+ },
94
+ ],
95
+ total: 1,
96
+ limit: 20,
97
+ offset: 0,
98
+ },
99
+ }),
100
+ });
101
+ return;
102
+ }
103
+ await route.fulfill({
104
+ status: 404,
105
+ contentType: 'application/json',
106
+ body: JSON.stringify({ code: 404, message: 'not mocked' }),
107
+ });
108
+ });
109
+ await page.goto('/view/instrument-center/instruments');
110
+ await expect(page.getByTestId('production-data-warning')).toBeVisible();
111
+ await expect(page.getByTestId('instrument-title')).toHaveText('仪器资源');
112
+ await expect(page.getByText('场发射扫描电子显微镜')).toBeVisible();
113
+ await expect(page.getByText('college_admin、instrument_admin')).toBeVisible();
114
+ expect(requests.filter(url => url.includes('role-session'))).toHaveLength(0);
115
+ expect(
116
+ requests.some(url =>
117
+ url.includes('/native/current-user?environmentKey=production')
118
+ )
119
+ ).toBe(true);
120
+ });
121
+
122
+ test('keeps the production warning visible when current-user fails closed', async ({
123
+ page,
124
+ }) => {
125
+ await page.route('**/view/instrument-center/instruments', async route => {
126
+ if (route.request().resourceType() !== 'document') {
127
+ await route.continue();
128
+ return;
129
+ }
130
+ const response = await route.fetch();
131
+ const body = (await response.text()).replace(
132
+ '<head>',
133
+ `<head>
134
+ <meta name="openxiangda-runtime-base" content="/view/instrument-center/" />
135
+ <meta name="openxiangda-app-code" content="instrument-center" />
136
+ <meta name="openxiangda-environment" content="production" />`
137
+ );
138
+ await route.fulfill({ response, body });
139
+ });
140
+ await page.route('**/service/**', route =>
141
+ route.fulfill({
142
+ status: 503,
143
+ contentType: 'application/json',
144
+ body: JSON.stringify({ code: 503, message: 'current-user unavailable' }),
145
+ })
146
+ );
147
+ await page.goto('/view/instrument-center/instruments');
148
+ await expect(page.getByTestId('production-data-warning')).toBeVisible();
149
+ await expect(page.getByText('无法读取平台当前用户')).toBeVisible();
150
+ });
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>仪器资源管理</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@app/web",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "check": "tsc -p tsconfig.json --noEmit && node scripts/check.mjs",
9
+ "test": "tsx --test test/*.test.ts",
10
+ "test:e2e": "playwright test",
11
+ "build": "tsc -p tsconfig.json --noEmit && vite build && node scripts/verify-build.mjs"
12
+ },
13
+ "dependencies": {
14
+ "@ant-design/icons": "6.2.3",
15
+ "@refinedev/core": "5.0.12",
16
+ "antd": "6.4.3",
17
+ "dayjs": "1.11.18",
18
+ "openxiangda-contracts": "2.0.0-alpha.26",
19
+ "react": "19.2.8",
20
+ "react-dom": "19.2.8",
21
+ "react-router-dom": "7.8.2"
22
+ },
23
+ "devDependencies": {
24
+ "@playwright/test": "1.62.1",
25
+ "@types/node": "24.13.3",
26
+ "@types/react": "19.2.14",
27
+ "@types/react-dom": "19.2.3",
28
+ "@vitejs/plugin-react": "4.7.0",
29
+ "tsx": "4.23.12",
30
+ "typescript": "5.9.3",
31
+ "vite": "7.1.3"
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ const baseURL =
4
+ process.env.OPENXIANGDA_E2E_BASE_URL || 'http://127.0.0.1:4173';
5
+
6
+ export default defineConfig({
7
+ testDir: './e2e',
8
+ use: { baseURL, trace: 'retain-on-failure' },
9
+ webServer: process.env.OPENXIANGDA_E2E_BASE_URL
10
+ ? undefined
11
+ : {
12
+ command: 'pnpm dev',
13
+ url: baseURL,
14
+ reuseExistingServer: false,
15
+ env: {
16
+ OPENXIANGDA_WEB_PORT: '4173',
17
+ OPENXIANGDA_ENVIRONMENT_KEY: 'production',
18
+ OPENXIANGDA_DEV_PROXY: 'http://127.0.0.1:7001',
19
+ },
20
+ },
21
+ });
@@ -0,0 +1,32 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ const root = resolve(import.meta.dirname, '..');
5
+ const files = [];
6
+ function visit(directory) {
7
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
8
+ const path = join(directory, entry.name);
9
+ if (
10
+ entry.isDirectory() &&
11
+ !['.umi', '.umi-production', '.turbopack'].includes(entry.name)
12
+ )
13
+ visit(path);
14
+ else if (/\.(?:ts|tsx)$/.test(entry.name)) files.push(path);
15
+ }
16
+ }
17
+ visit(join(root, 'src'));
18
+ const lines = files.reduce(
19
+ (total, file) => total + readFileSync(file, 'utf8').split(/\r?\n/).length,
20
+ 0
21
+ );
22
+ if (files.length > 18) throw new Error(`WEB_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>18`);
23
+ if (lines > 1_600) throw new Error(`WEB_BUSINESS_LOC_BUDGET_EXCEEDED:${lines}>1600`);
24
+ const source = files.map(file => readFileSync(file, 'utf8')).join('\n');
25
+ for (const marker of ['@umijs/', 'openxiangda-' + 'admin', 'openxiangda-' + 'user', 'instrument_query', 'instrument_save', 'function.invoke', 'role' + '-session']) {
26
+ if (source.includes(marker)) throw new Error(`WEB_FORBIDDEN_MARKER:${marker}`);
27
+ }
28
+ const playwright = readFileSync(join(root, 'playwright.config.ts'), 'utf8');
29
+ if (!playwright.includes('reuseExistingServer: false')) {
30
+ throw new Error('WEB_E2E_MUST_NOT_REUSE_UNKNOWN_SERVER');
31
+ }
32
+ process.stdout.write(`Verified Vite/Refine web source: ${files.length} files, ${lines} LOC.\n`);
@@ -0,0 +1,20 @@
1
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { gzipSync } from 'node:zlib';
3
+ import { join, resolve } from 'node:path';
4
+
5
+ const dist = resolve(import.meta.dirname, '../dist');
6
+ const assets = join(dist, 'assets');
7
+ const names = readdirSync(assets);
8
+ const files = names.map(name => join(assets, name));
9
+ const total = files.reduce((bytes, file) => bytes + statSync(file).size, 0);
10
+ if (total > 2_500_000) throw new Error(`WEB_DIST_BUDGET_EXCEEDED:${total}>2500000`);
11
+ const javascript = files.filter(file => file.endsWith('.js'));
12
+ const gzip = javascript.reduce(
13
+ (bytes, file) => bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength,
14
+ 0
15
+ );
16
+ if (gzip > 600_000) throw new Error(`WEB_JS_GZIP_BUDGET_EXCEEDED:${gzip}>600000`);
17
+ if (names.some(name => name.endsWith('.map'))) throw new Error('WEB_SOURCE_MAP_FORBIDDEN');
18
+ const html = readFileSync(join(dist, 'index.html'), 'utf8');
19
+ if (!html.includes('<div id="root"></div>')) throw new Error('WEB_ROOT_MISSING');
20
+ process.stdout.write(`Verified Vite build: ${total} bytes, ${gzip} gzip bytes.\n`);
@@ -0,0 +1,60 @@
1
+ import { useOne } from '@refinedev/core';
2
+ import { Button, Result, Space, Spin, Typography } from 'antd';
3
+ import { useNavigate, useParams } from 'react-router-dom';
4
+ import { InstrumentForm } from './InstrumentForm';
5
+ import { Shell } from './Shell';
6
+ import { INSTRUMENT_CAPABILITIES, type InstrumentRecord } from './instrument';
7
+ import { useRuntime } from './runtime';
8
+
9
+ export function InstrumentDetailPage() {
10
+ const { id = '' } = useParams();
11
+ const navigate = useNavigate();
12
+ const { hasCapability } = useRuntime();
13
+ const query = useOne<InstrumentRecord>({
14
+ resource: 'instruments',
15
+ id,
16
+ queryOptions: { retry: false },
17
+ });
18
+ const record = query.result;
19
+ if (query.query.isLoading)
20
+ return (
21
+ <Shell>
22
+ <Spin />
23
+ </Shell>
24
+ );
25
+ if (query.query.isError || !record)
26
+ return (
27
+ <Shell>
28
+ <Result
29
+ status="403"
30
+ title="无法访问"
31
+ subTitle={query.query.error?.message || '记录不存在'}
32
+ />
33
+ </Shell>
34
+ );
35
+ return (
36
+ <Shell>
37
+ <Space className="oxa-page-heading">
38
+ <Typography.Title level={3} style={{ margin: 0 }}>
39
+ 仪器详情
40
+ </Typography.Title>
41
+ <Space>
42
+ <Button onClick={() => navigate('/instruments')}>返回列表</Button>
43
+ {hasCapability(INSTRUMENT_CAPABILITIES.update) && (
44
+ <Button
45
+ type="primary"
46
+ onClick={() => navigate(`/instruments/${id}/edit`)}
47
+ >
48
+ 编辑
49
+ </Button>
50
+ )}
51
+ </Space>
52
+ </Space>
53
+ <InstrumentForm
54
+ mode="detail"
55
+ record={record}
56
+ onCancel={() => navigate('/instruments')}
57
+ />
58
+ </Shell>
59
+ );
60
+ }