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
@@ -0,0 +1,246 @@
1
+ import {
2
+ SCHEMA_VERSIONS,
3
+ type DataFileRef,
4
+ type DataFileUploadPlan,
5
+ type DataPage,
6
+ type DataQuery,
7
+ type DataRecord,
8
+ } from 'openxiangda-contracts/browser';
9
+ import type { InstrumentListQuery, InstrumentRecord } from './instrument';
10
+ import { applicationCode, runtimeMount } from './runtime-meta';
11
+
12
+ const resourceCode = 'instruments';
13
+ const nativeBase = () =>
14
+ `/service/openxiangda-api/v2/applications/${applicationCode()}/native`;
15
+ const dataBase = () => `${nativeBase()}/data/${resourceCode}`;
16
+
17
+ interface PlatformEnvelope<T> {
18
+ code: number;
19
+ message?: string;
20
+ errorCode?: string;
21
+ data: T;
22
+ }
23
+
24
+ async function request<T>(path: string, init?: RequestInit): Promise<T> {
25
+ const response = await fetch(path, {
26
+ credentials: 'include',
27
+ ...init,
28
+ headers: {
29
+ accept: 'application/json',
30
+ ...(init?.body ? { 'content-type': 'application/json' } : {}),
31
+ ...init?.headers,
32
+ },
33
+ });
34
+ const payload = (await response.json().catch(() => null)) as
35
+ | PlatformEnvelope<T>
36
+ | T
37
+ | null;
38
+ const envelope =
39
+ payload && typeof payload === 'object' && 'code' in payload
40
+ ? (payload as PlatformEnvelope<T>)
41
+ : null;
42
+ if (!response.ok || (envelope && envelope.code !== 200)) {
43
+ const code = envelope?.errorCode || `HTTP_${response.status}`;
44
+ throw new Error(`${code}: ${envelope?.message || '平台请求失败'}`);
45
+ }
46
+ return envelope ? envelope.data : (payload as T);
47
+ }
48
+
49
+ export interface RuntimeIdentity {
50
+ userId: string;
51
+ roleCodes: string[];
52
+ capabilityCodes: string[];
53
+ isAppSuperAdmin: boolean;
54
+ environment: {
55
+ id: string;
56
+ key: 'preproduction' | 'production';
57
+ activeAppVersionId?: string;
58
+ headRevision?: number;
59
+ authzRevisionId?: string;
60
+ authzVersion?: number;
61
+ scopeDataVersion?: string;
62
+ };
63
+ }
64
+
65
+ interface ConnectedCurrent {
66
+ environment: RuntimeIdentity['environment'];
67
+ principal: {
68
+ userId: string;
69
+ roleCodes: string[];
70
+ capabilityCodes: string[];
71
+ isAppSuperAdmin: boolean;
72
+ };
73
+ }
74
+
75
+ interface DeployedCurrent {
76
+ schemaVersion: 'openxiangda.current-user/v2';
77
+ appCode: string;
78
+ environment: {
79
+ id: string;
80
+ key: 'preproduction' | 'production';
81
+ activeAppVersionId: string;
82
+ headRevision: number;
83
+ authzRevisionId: string;
84
+ authzVersion: number;
85
+ scopeDataVersion: string;
86
+ };
87
+ principal: {
88
+ type: 'user';
89
+ userId: string;
90
+ roleCodes: string[];
91
+ capabilityCodes: string[];
92
+ isAppSuperAdmin: boolean;
93
+ };
94
+ }
95
+
96
+ let activeIdentity: RuntimeIdentity | undefined;
97
+
98
+ export async function loadCurrentIdentity(): Promise<RuntimeIdentity> {
99
+ const mount = runtimeMount();
100
+ if (mount) {
101
+ const current = await request<DeployedCurrent>(
102
+ `${nativeBase()}/current-user?environmentKey=${encodeURIComponent(
103
+ mount.environmentKey
104
+ )}`
105
+ );
106
+ if (
107
+ current.schemaVersion !== 'openxiangda.current-user/v2' ||
108
+ current.appCode !== mount.appCode ||
109
+ current.environment.key !== mount.environmentKey ||
110
+ current.principal.type !== 'user' ||
111
+ !Array.isArray(current.principal.roleCodes) ||
112
+ !Array.isArray(current.principal.capabilityCodes)
113
+ ) {
114
+ throw new Error('OPENXIANGDA_DEPLOYED_CURRENT_USER_INVALID');
115
+ }
116
+ activeIdentity = { ...current.principal, environment: current.environment };
117
+ return activeIdentity;
118
+ }
119
+ const connectedPath =
120
+ `/service/openxiangda-api/v2/applications/${applicationCode()}/dev-sessions/current`;
121
+ try {
122
+ const current = await request<ConnectedCurrent>(connectedPath);
123
+ activeIdentity = { ...current.principal, environment: current.environment };
124
+ return activeIdentity;
125
+ } catch (error) {
126
+ throw new Error(
127
+ `OPENXIANGDA_CONNECTED_CURRENT_USER_FAILED: ${error instanceof Error ? error.message : String(error)}`
128
+ );
129
+ }
130
+ }
131
+
132
+ function currentEnvironmentKey() {
133
+ const key = activeIdentity?.environment.key || runtimeMount()?.environmentKey;
134
+ if (!key) throw new Error('OPENXIANGDA_CURRENT_ENVIRONMENT_REQUIRED');
135
+ return key;
136
+ }
137
+
138
+ export interface InstrumentDataApiAdapter {
139
+ list(query: InstrumentListQuery): Promise<{ rows: InstrumentRecord[]; total: number }>;
140
+ get(id: string): Promise<InstrumentRecord>;
141
+ create(data: Partial<InstrumentRecord>): Promise<InstrumentRecord>;
142
+ update(id: string, expectedRevision: number, data: Partial<InstrumentRecord>): Promise<InstrumentRecord>;
143
+ remove(id: string, expectedRevision: number): Promise<InstrumentRecord>;
144
+ upload(fieldCode: 'image' | 'attachments', file: File, recordId?: string): Promise<DataFileRef>;
145
+ }
146
+
147
+ export const instrumentDataApi: InstrumentDataApiAdapter = {
148
+ async list(query) {
149
+ const filters: NonNullable<DataQuery['filters']> = [];
150
+ // The current DataQuery contract has AND filters but no grouped OR. Keep
151
+ // this honest single-field search until the platform adds a native OR AST.
152
+ if (query.keyword?.trim()) {
153
+ filters.push({ field: 'chineseName', operator: 'ilike', value: `%${query.keyword.trim()}%` });
154
+ }
155
+ if (query.collegeId) filters.push({ field: 'collegeId', operator: 'eq', value: query.collegeId });
156
+ if (query.instrumentAdminId) filters.push({ field: 'instrumentAdminIds', operator: 'cs', value: [query.instrumentAdminId] });
157
+ if (query.instrumentStatus) filters.push({ field: 'instrumentStatus', operator: 'eq', value: query.instrumentStatus });
158
+ if (query.openToOutside !== undefined) filters.push({ field: 'openToOutside', operator: 'eq', value: query.openToOutside });
159
+ if (query.enabledDate) {
160
+ filters.push({ field: 'enabledDate', operator: 'gte', value: query.enabledDate[0] });
161
+ filters.push({ field: 'enabledDate', operator: 'lte', value: query.enabledDate[1] });
162
+ }
163
+ if (query.assetValue?.[0] !== undefined) filters.push({ field: 'assetValue', operator: 'gte', value: query.assetValue[0] });
164
+ if (query.assetValue?.[1] !== undefined) filters.push({ field: 'assetValue', operator: 'lte', value: query.assetValue[1] });
165
+ const body: DataQuery = {
166
+ schemaVersion: SCHEMA_VERSIONS.dataQuery,
167
+ filters,
168
+ order: [{ field: String(query.sort?.field || 'updatedAt'), direction: query.sort?.order || 'desc' }],
169
+ limit: query.pageSize,
170
+ offset: (query.page - 1) * query.pageSize,
171
+ };
172
+ const page = await request<DataPage<InstrumentRecord>>(`${dataBase()}/query`, {
173
+ method: 'POST',
174
+ body: JSON.stringify({ ...body, environmentKey: currentEnvironmentKey() }),
175
+ });
176
+ return { rows: page.items, total: page.total };
177
+ },
178
+ async get(id) {
179
+ const record = await request<DataRecord<InstrumentRecord>>(
180
+ `${dataBase()}/records/${encodeURIComponent(
181
+ id
182
+ )}?environmentKey=${encodeURIComponent(currentEnvironmentKey())}`
183
+ );
184
+ return record.data;
185
+ },
186
+ async create(data) {
187
+ const record = await request<DataRecord<InstrumentRecord>>(`${dataBase()}/records`, {
188
+ method: 'POST',
189
+ body: JSON.stringify({ environmentKey: currentEnvironmentKey(), data }),
190
+ });
191
+ return record.data;
192
+ },
193
+ async update(id, expectedRevision, data) {
194
+ const record = await request<DataRecord<InstrumentRecord>>(
195
+ `${dataBase()}/records/${encodeURIComponent(id)}/update`,
196
+ {
197
+ method: 'POST',
198
+ body: JSON.stringify({
199
+ environmentKey: currentEnvironmentKey(),
200
+ expectedRevision,
201
+ data,
202
+ }),
203
+ }
204
+ );
205
+ return record.data;
206
+ },
207
+ async remove(id, expectedRevision) {
208
+ const record = await request<DataRecord<InstrumentRecord>>(
209
+ `${dataBase()}/records/${encodeURIComponent(id)}/delete`,
210
+ {
211
+ method: 'POST',
212
+ body: JSON.stringify({
213
+ environmentKey: currentEnvironmentKey(),
214
+ expectedRevision,
215
+ }),
216
+ }
217
+ );
218
+ return record.data;
219
+ },
220
+ async upload(fieldCode, file, recordId) {
221
+ const plan = await request<DataFileUploadPlan>(`${dataBase()}/files/uploads/initiate`, {
222
+ method: 'POST',
223
+ body: JSON.stringify({
224
+ environmentKey: currentEnvironmentKey(),
225
+ fieldCode,
226
+ fileName: file.name,
227
+ fileSize: file.size,
228
+ contentType: file.type || 'application/octet-stream',
229
+ ...(recordId ? { recordId } : {}),
230
+ }),
231
+ });
232
+ const uploaded = await fetch(plan.uploadUrl, {
233
+ method: plan.uploadMethod,
234
+ headers: plan.headers,
235
+ body: file,
236
+ });
237
+ if (!uploaded.ok) throw new Error(`FILE_UPLOAD_FAILED: HTTP_${uploaded.status}`);
238
+ return await request<DataFileRef>(
239
+ `${dataBase()}/files/${encodeURIComponent(plan.file.id)}/complete`,
240
+ {
241
+ method: 'POST',
242
+ body: JSON.stringify({ environmentKey: currentEnvironmentKey() }),
243
+ }
244
+ );
245
+ },
246
+ };
@@ -0,0 +1,60 @@
1
+ export interface RuntimeMount {
2
+ runtimeBase: string;
3
+ appCode: string;
4
+ environmentKey: 'preproduction' | 'production';
5
+ }
6
+
7
+ export function readRuntimeMount(
8
+ read: (name: string) => string | undefined
9
+ ): RuntimeMount | null {
10
+ const runtimeBase = read('openxiangda-runtime-base')?.trim();
11
+ const appCode = read('openxiangda-app-code')?.trim();
12
+ const environment = read('openxiangda-environment')?.trim();
13
+ if (!runtimeBase && !appCode && !environment) return null;
14
+ if (
15
+ !runtimeBase ||
16
+ !appCode ||
17
+ (environment !== 'preproduction' && environment !== 'production')
18
+ ) {
19
+ throw new Error('OPENXIANGDA_RUNTIME_META_INVALID');
20
+ }
21
+ return { runtimeBase, appCode, environmentKey: environment };
22
+ }
23
+
24
+ export function runtimeMount() {
25
+ if (typeof document === 'undefined') return null;
26
+ return readRuntimeMount(name =>
27
+ document
28
+ .querySelector<HTMLMetaElement>(`meta[name="${name}"]`)
29
+ ?.content.trim()
30
+ );
31
+ }
32
+
33
+ export function applicationCode() {
34
+ return runtimeMount()?.appCode || 'instrument-center';
35
+ }
36
+
37
+ export function resolveApplicationBasename(mount: RuntimeMount | null) {
38
+ if (!mount) return undefined;
39
+ const pathname = new URL(mount.runtimeBase, 'https://runtime.local').pathname;
40
+ const normalized = pathname.replace(/\/+$/, '');
41
+ return normalized || '/';
42
+ }
43
+
44
+ export function applicationBasename() {
45
+ return resolveApplicationBasename(runtimeMount());
46
+ }
47
+
48
+ export function resolveApplicationApiPath(
49
+ mount: RuntimeMount | null,
50
+ path: string
51
+ ) {
52
+ if (!mount) return `/${path.replace(/^\/+/, '')}`;
53
+ return `/service/openxiangda-app-api/v2/${encodeURIComponent(
54
+ mount.appCode
55
+ )}/${encodeURIComponent(mount.environmentKey)}/${path.replace(/^\/+/, '')}`;
56
+ }
57
+
58
+ export function applicationApiPath(path: string) {
59
+ return resolveApplicationApiPath(runtimeMount(), path);
60
+ }
@@ -0,0 +1,73 @@
1
+ import { Alert, Button, Result, Spin } from 'antd';
2
+ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
3
+ import { loadCurrentIdentity, type RuntimeIdentity } from './platform-client';
4
+ import { runtimeMount } from './runtime-meta';
5
+
6
+ interface RuntimeValue {
7
+ identity: RuntimeIdentity;
8
+ hasCapability(code: string): boolean;
9
+ }
10
+
11
+ const RuntimeContext = createContext<RuntimeValue | null>(null);
12
+
13
+ export function RuntimeBoundary({ children }: { children: React.ReactNode }) {
14
+ const [identity, setIdentity] = useState<RuntimeIdentity>();
15
+ const [error, setError] = useState<Error>();
16
+ const [attempt, setAttempt] = useState(0);
17
+ useEffect(() => {
18
+ let active = true;
19
+ setError(undefined);
20
+ void loadCurrentIdentity().then(
21
+ value => active && setIdentity(value),
22
+ reason => active && setError(reason instanceof Error ? reason : new Error(String(reason)))
23
+ );
24
+ return () => {
25
+ active = false;
26
+ };
27
+ }, [attempt]);
28
+ const value = useMemo<RuntimeValue | null>(
29
+ () =>
30
+ identity
31
+ ? {
32
+ identity,
33
+ hasCapability: code =>
34
+ identity.isAppSuperAdmin || identity.capabilityCodes.includes(code),
35
+ }
36
+ : null,
37
+ [identity]
38
+ );
39
+ const production =
40
+ identity?.environment.key === 'production' ||
41
+ runtimeMount()?.environmentKey === 'production';
42
+ return (
43
+ <>
44
+ {production && (
45
+ <Alert
46
+ banner
47
+ showIcon
48
+ type="error"
49
+ data-testid="production-data-warning"
50
+ message="正在使用正式数据:所有新增、编辑和删除都会影响生产环境"
51
+ />
52
+ )}
53
+ {error ? (
54
+ <Result
55
+ status="error"
56
+ title="无法读取平台当前用户"
57
+ subTitle={error.message}
58
+ extra={<Button onClick={() => setAttempt(value => value + 1)}>重试</Button>}
59
+ />
60
+ ) : value ? (
61
+ <RuntimeContext.Provider value={value}>{children}</RuntimeContext.Provider>
62
+ ) : (
63
+ <div className="oxa-loading"><Spin tip="正在读取平台身份与权限并集" /></div>
64
+ )}
65
+ </>
66
+ );
67
+ }
68
+
69
+ export function useRuntime() {
70
+ const value = useContext(RuntimeContext);
71
+ if (!value) throw new Error('OPENXIANGDA_RUNTIME_NOT_READY');
72
+ return value;
73
+ }
@@ -0,0 +1,84 @@
1
+ html,
2
+ body,
3
+ #root {
4
+ min-height: 100%;
5
+ margin: 0;
6
+ background: #f5f7fa;
7
+ }
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+ .oxa-loading {
12
+ display: grid;
13
+ min-height: 70vh;
14
+ place-items: center;
15
+ }
16
+ .oxa-shell {
17
+ min-height: 100vh;
18
+ }
19
+ .oxa-header {
20
+ position: sticky;
21
+ top: 0;
22
+ z-index: 10;
23
+ min-height: 56px;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ gap: 16px;
28
+ padding: 10px 24px;
29
+ background: #102a43;
30
+ color: white;
31
+ box-shadow: 0 2px 8px #0002;
32
+ }
33
+ .oxa-header-roles {
34
+ color: #cbd5e1;
35
+ }
36
+ .oxa-main {
37
+ max-width: 1680px;
38
+ margin: auto;
39
+ padding: 20px 24px 40px;
40
+ }
41
+ .oxa-title {
42
+ margin: 0;
43
+ font-size: 20px;
44
+ }
45
+ .oxa-muted {
46
+ color: #667085;
47
+ }
48
+ .oxa-page-heading {
49
+ width: 100%;
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ gap: 12px;
54
+ margin-bottom: 16px;
55
+ }
56
+ .oxa-sections {
57
+ display: grid;
58
+ gap: 16px;
59
+ }
60
+ .oxa-grid {
61
+ display: grid;
62
+ grid-template-columns: repeat(2, minmax(0, 1fr));
63
+ gap: 0 20px;
64
+ }
65
+ .oxa-actions {
66
+ display: flex;
67
+ justify-content: flex-end;
68
+ gap: 8px;
69
+ margin-top: 20px;
70
+ }
71
+ .oxa-filter-grid {
72
+ display: grid;
73
+ grid-template-columns: repeat(4, minmax(160px, 1fr));
74
+ gap: 12px;
75
+ }
76
+ @media (max-width: 900px) {
77
+ .oxa-grid,
78
+ .oxa-filter-grid {
79
+ grid-template-columns: 1fr;
80
+ }
81
+ .oxa-header {
82
+ align-items: flex-start;
83
+ }
84
+ }
@@ -0,0 +1,234 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ instrumentFieldAllowed,
5
+ sanitizeInstrumentValues,
6
+ } from '../src/InstrumentForm';
7
+ import { instrumentFields } from '../src/fields';
8
+ import {
9
+ instrumentDataApi,
10
+ loadCurrentIdentity,
11
+ } from '../src/platform-client';
12
+ import {
13
+ applicationApiPath,
14
+ readRuntimeMount,
15
+ resolveApplicationApiPath,
16
+ resolveApplicationBasename,
17
+ } from '../src/runtime-meta';
18
+
19
+ test('the golden module exposes exactly 30 fields and five restricted fields', () => {
20
+ assert.equal(instrumentFields.length, 30);
21
+ assert.deepEqual(
22
+ instrumentFields
23
+ .filter(field => field.createCapability && field.updateCapability)
24
+ .map(field => field.key),
25
+ [
26
+ 'instrumentCode',
27
+ 'assetCode',
28
+ 'assetValue',
29
+ 'collegeId',
30
+ 'instrumentAdminIds',
31
+ ]
32
+ );
33
+ assert.deepEqual(
34
+ instrumentFields.filter(field => field.system).map(field => field.key),
35
+ ['id', 'revision']
36
+ );
37
+ });
38
+
39
+ test('college create restores five fields while update omits college reassignment', () => {
40
+ const protectedFields = instrumentFields.filter(
41
+ field => field.createCapability && field.updateCapability
42
+ );
43
+ const collegeCapabilities = new Set<string>([
44
+ ...protectedFields.map(field => field.createCapability!),
45
+ ...protectedFields
46
+ .filter(field => field.key !== 'collegeId')
47
+ .map(field => field.updateCapability!),
48
+ ]);
49
+ const hasCapability = (capability: string) =>
50
+ collegeCapabilities.has(capability);
51
+ const values = {
52
+ instrumentCode: 'INS-001',
53
+ chineseName: '电子显微镜',
54
+ assetCode: 'ASSET-001',
55
+ assetValue: 100,
56
+ collegeId: 'college-am',
57
+ instrumentAdminIds: ['user-wang'],
58
+ revision: 7,
59
+ };
60
+ assert.equal(
61
+ instrumentFieldAllowed(
62
+ instrumentFields.find(field => field.key === 'collegeId')!,
63
+ 'create',
64
+ hasCapability
65
+ ),
66
+ true
67
+ );
68
+ assert.equal(
69
+ instrumentFieldAllowed(
70
+ instrumentFields.find(field => field.key === 'collegeId')!,
71
+ 'edit',
72
+ hasCapability
73
+ ),
74
+ false
75
+ );
76
+ assert.deepEqual(sanitizeInstrumentValues(values, 'create', hasCapability), {
77
+ instrumentCode: 'INS-001',
78
+ chineseName: '电子显微镜',
79
+ assetCode: 'ASSET-001',
80
+ assetValue: 100,
81
+ collegeId: 'college-am',
82
+ instrumentAdminIds: ['user-wang'],
83
+ });
84
+ assert.deepEqual(sanitizeInstrumentValues(values, 'edit', hasCapability), {
85
+ instrumentCode: 'INS-001',
86
+ chineseName: '电子显微镜',
87
+ assetCode: 'ASSET-001',
88
+ assetValue: 100,
89
+ instrumentAdminIds: ['user-wang'],
90
+ });
91
+ });
92
+
93
+ test('instrument admin update payload omits every protected field', () => {
94
+ const values = Object.fromEntries(
95
+ instrumentFields
96
+ .filter(field => !field.system)
97
+ .map(field => [field.key, `value:${field.key}`])
98
+ );
99
+ const sanitized = sanitizeInstrumentValues(values, 'edit', () => false);
100
+ assert.equal(sanitized.chineseName, 'value:chineseName');
101
+ for (const field of instrumentFields.filter(
102
+ item => item.createCapability && item.updateCapability
103
+ )) {
104
+ assert.equal(
105
+ Object.prototype.hasOwnProperty.call(sanitized, field.key),
106
+ false
107
+ );
108
+ }
109
+ });
110
+
111
+ test('school admin keeps every protected field in create and update payloads', () => {
112
+ const values = Object.fromEntries(
113
+ instrumentFields
114
+ .filter(field => !field.system)
115
+ .map(field => [field.key, `value:${field.key}`])
116
+ );
117
+ for (const mode of ['create', 'edit'] as const) {
118
+ const sanitized = sanitizeInstrumentValues(values, mode, () => true);
119
+ for (const field of instrumentFields.filter(
120
+ item => item.createCapability && item.updateCapability
121
+ )) {
122
+ assert.equal(sanitized[field.key], `value:${field.key}`);
123
+ }
124
+ }
125
+ });
126
+
127
+ test('deployed app/environment come only from platform-injected runtime meta', () => {
128
+ const values = new Map([
129
+ ['openxiangda-runtime-base', '/view/instrument-center/'],
130
+ ['openxiangda-app-code', 'instrument-center'],
131
+ ['openxiangda-environment', 'production'],
132
+ ]);
133
+ const mount = readRuntimeMount(name => values.get(name));
134
+ assert.deepEqual(mount, {
135
+ runtimeBase:
136
+ '/view/instrument-center/',
137
+ appCode: 'instrument-center',
138
+ environmentKey: 'production',
139
+ });
140
+ assert.equal(readRuntimeMount(() => undefined), null);
141
+ assert.throws(
142
+ () =>
143
+ readRuntimeMount(name =>
144
+ name === 'openxiangda-environment' ? 'production' : undefined
145
+ ),
146
+ /OPENXIANGDA_RUNTIME_META_INVALID/
147
+ );
148
+ assert.equal(
149
+ applicationApiPath('api/instruments/context'),
150
+ '/api/instruments/context'
151
+ );
152
+ assert.equal(resolveApplicationBasename(mount), '/view/instrument-center');
153
+ assert.equal(
154
+ resolveApplicationBasename({
155
+ ...mount!,
156
+ runtimeBase: '/dev/instrument-center/',
157
+ environmentKey: 'preproduction',
158
+ }),
159
+ '/dev/instrument-center'
160
+ );
161
+ assert.equal(
162
+ resolveApplicationApiPath(mount, 'api/instruments/context'),
163
+ '/service/openxiangda-app-api/v2/instrument-center/production/api/instruments/context'
164
+ );
165
+ });
166
+
167
+ test('list uses the native Data API with server paging, filters, and sorting', async () => {
168
+ const calls: Array<{ path: string; init?: RequestInit }> = [];
169
+ const originalFetch = globalThis.fetch;
170
+ globalThis.fetch = async (input, init) => {
171
+ calls.push({ path: String(input), init });
172
+ if (String(input).endsWith('/dev-sessions/current')) {
173
+ return new Response(
174
+ JSON.stringify({
175
+ code: 200,
176
+ data: {
177
+ environment: { id: 'test-id', key: 'preproduction' },
178
+ principal: {
179
+ userId: 'user-test',
180
+ roleCodes: ['college_admin'],
181
+ capabilityCodes: [],
182
+ isAppSuperAdmin: false,
183
+ },
184
+ },
185
+ }),
186
+ { status: 200, headers: { 'content-type': 'application/json' } }
187
+ );
188
+ }
189
+ return new Response(
190
+ JSON.stringify({
191
+ code: 200,
192
+ data: {
193
+ schemaVersion: 'openxiangda.data-page/v2',
194
+ resourceCode: 'instruments',
195
+ items: [],
196
+ total: 0,
197
+ limit: 20,
198
+ offset: 20,
199
+ },
200
+ }),
201
+ { status: 200, headers: { 'content-type': 'application/json' } }
202
+ );
203
+ };
204
+ try {
205
+ await loadCurrentIdentity();
206
+ await instrumentDataApi.list({
207
+ page: 2,
208
+ pageSize: 20,
209
+ collegeId: 'college-am',
210
+ instrumentAdminId: 'user-wang',
211
+ sort: { field: 'assetValue', order: 'asc' },
212
+ });
213
+ } finally {
214
+ globalThis.fetch = originalFetch;
215
+ }
216
+ assert.equal(calls.length, 2);
217
+ assert.match(
218
+ calls[1]!.path,
219
+ /\/native\/data\/instruments\/query$/
220
+ );
221
+ const body = JSON.parse(String(calls[1]!.init?.body));
222
+ assert.equal(body.environmentKey, 'preproduction');
223
+ assert.equal(body.limit, 20);
224
+ assert.equal(body.offset, 20);
225
+ assert.deepEqual(body.order, [{ field: 'assetValue', direction: 'asc' }]);
226
+ assert.deepEqual(body.filters, [
227
+ { field: 'collegeId', operator: 'eq', value: 'college-am' },
228
+ {
229
+ field: 'instrumentAdminIds',
230
+ operator: 'cs',
231
+ value: ['user-wang'],
232
+ },
233
+ ]);
234
+ });