@oj-module/cli 0.2.2 → 0.2.5

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 (231) hide show
  1. package/devkit/README.md +14 -3
  2. package/devkit/SKILL.md +16 -4
  3. package/devkit/framework-dev.md +8 -6
  4. package/devkit/manual.md +243 -23
  5. package/package.json +2 -2
  6. package/shell-dist/assets/runtime.js +143 -8
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +3 -3
  9. package/src/args.ts +67 -0
  10. package/src/build.ts +11 -3
  11. package/src/bundle.ts +197 -0
  12. package/src/contract/__tests__/handler-scan.test.ts +82 -0
  13. package/src/contract/check.ts +2 -1
  14. package/src/contract/handler-scan.ts +32 -0
  15. package/src/deterministic-tar.ts +120 -0
  16. package/src/dev.ts +4 -2
  17. package/src/enable.ts +28 -0
  18. package/src/index.ts +63 -2
  19. package/src/info.ts +3 -2
  20. package/src/init.ts +147 -14
  21. package/src/install.ts +268 -0
  22. package/src/keygen.ts +88 -0
  23. package/src/layout.ts +31 -0
  24. package/src/lifecycle/__tests__/authoring.test.ts +109 -0
  25. package/src/lifecycle/__tests__/bundle.test.ts +150 -0
  26. package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
  27. package/src/lifecycle/__tests__/fixtures/README.md +59 -0
  28. package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
  29. package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
  30. package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
  31. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
  32. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
  33. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
  36. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
  37. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
  38. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
  39. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
  40. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
  41. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
  42. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
  43. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
  44. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
  45. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
  46. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
  47. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
  48. package/src/lifecycle/__tests__/helpers.ts +78 -0
  49. package/src/lifecycle/__tests__/install.test.ts +514 -0
  50. package/src/lifecycle/__tests__/keygen.test.ts +61 -0
  51. package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
  52. package/src/lifecycle/__tests__/publish.test.ts +80 -0
  53. package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
  54. package/src/lifecycle/__tests__/sign.test.ts +131 -0
  55. package/src/lifecycle/__tests__/smoke.test.ts +61 -0
  56. package/src/lifecycle/__tests__/trust.test.ts +177 -0
  57. package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
  58. package/src/lifecycle/keygen.ts +40 -0
  59. package/src/lifecycle/sign.ts +74 -0
  60. package/src/manifest-contract.ts +696 -0
  61. package/src/oj-cert.ts +19 -0
  62. package/src/oj-cmd.ts +3 -3
  63. package/src/preview.ts +2 -2
  64. package/src/publish.ts +150 -0
  65. package/src/sign.ts +128 -0
  66. package/src/site.ts +146 -0
  67. package/src/trust.ts +116 -0
  68. package/src/uninstall.ts +29 -0
  69. package/src/usage.ts +35 -1
  70. package/templates/README.md +1 -1
  71. package/templates/api/config.yaml +47 -0
  72. package/templates/api/seed.mysql.sql +1 -1
  73. package/templates/api/seed.pg.sql +1 -1
  74. package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
  75. package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
  76. package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
  77. package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
  78. package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
  79. package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
  80. package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
  81. package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
  82. package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
  83. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
  84. package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
  85. package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
  86. package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
  87. package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
  88. package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
  89. package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
  90. package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
  91. package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
  92. package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
  93. package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
  94. package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
  95. package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
  96. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
  97. package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
  98. package/templates/api/src/_platform/_shared/service/access.ts +2 -2
  99. package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
  100. package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
  101. package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
  102. package/templates/api/src/_platform/manifest.yaml +1 -0
  103. package/templates/api/src/_platform/market/api.ts +883 -0
  104. package/templates/api/src/_platform/market/bootstrap.ts +39 -0
  105. package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
  106. package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
  107. package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
  108. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
  109. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
  110. package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
  111. package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
  112. package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
  113. package/templates/api/src/_platform/schema.yaml +21 -4
  114. package/templates/api/src/_platform/seed.sql +2 -2
  115. package/templates/api/src/_platform/solutions/api.ts +212 -0
  116. package/templates/api/src/demo/manifest.yaml +5 -0
  117. package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
  118. package/templates/api/src/demo/schema.yaml +11 -0
  119. package/templates/api/src/demo/seed.sql +5 -0
  120. package/templates/api/src/demo/todos/api.ts +59 -0
  121. package/templates/api/src/home/line/api.ts +57 -0
  122. package/templates/api/src/home/manifest.yaml +6 -0
  123. package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
  124. package/templates/api/src/home/pie/api.ts +40 -0
  125. package/templates/api/src/home/schema.yaml +14 -0
  126. package/templates/api/src/home/seed.sql +70 -0
  127. package/templates/api/src/system/oplog/api.ts +44 -2
  128. package/templates/api/tests/fixture.ts +16 -0
  129. package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
  130. package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
  131. package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
  132. package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
  133. package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
  134. package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
  135. package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
  136. package/templates/api/tests/lifecycle/register.spec.ts +194 -0
  137. package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
  138. package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
  139. package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
  140. package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
  141. package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
  142. package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
  143. package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
  144. package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
  145. package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
  146. package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
  147. package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
  148. package/templates/api/tests/rbac-settings.spec.ts +8 -0
  149. package/templates/init.include.yaml +56 -0
  150. package/templates/openapi.json +527 -0
  151. package/templates/package.json +52 -0
  152. package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
  153. package/templates/web/src/_platform/__client/api.ts +335 -0
  154. package/templates/web/src/about/entry.ts +42 -0
  155. package/templates/web/src/about/locales/en-US.json +16 -0
  156. package/templates/web/src/about/locales/zh-CN.json +16 -0
  157. package/templates/web/src/about/pages/constants.ts +20 -0
  158. package/templates/web/src/about/pages/index.tsx +135 -0
  159. package/templates/web/src/access/entry.ts +74 -0
  160. package/templates/web/src/access/locales/en-US.json +46 -0
  161. package/templates/web/src/access/locales/zh-CN.json +46 -0
  162. package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
  163. package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
  164. package/templates/web/src/access/pages/button-control/index.tsx +181 -0
  165. package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
  166. package/templates/web/src/access/pages/page-control/index.tsx +142 -0
  167. package/templates/web/src/demo/__client/api.schemas.ts +20 -0
  168. package/templates/web/src/demo/__client/api.ts +89 -0
  169. package/templates/web/src/exception/entry.ts +80 -0
  170. package/templates/web/src/exception/locales/en-US.json +13 -0
  171. package/templates/web/src/exception/locales/zh-CN.json +13 -0
  172. package/templates/web/src/exception/pages/403/index.tsx +30 -0
  173. package/templates/web/src/exception/pages/404/index.tsx +30 -0
  174. package/templates/web/src/exception/pages/500/index.tsx +30 -0
  175. package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
  176. package/templates/web/src/home/__client/api.schemas.ts +22 -0
  177. package/templates/web/src/home/__client/api.ts +113 -0
  178. package/templates/web/src/login/locales/en-US.json +6 -0
  179. package/templates/web/src/login/locales/zh-CN.json +6 -0
  180. package/templates/web/src/login/pages/login.tsx +67 -18
  181. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
  182. package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
  183. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
  184. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
  185. package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
  186. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
  187. package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
  188. package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
  189. package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
  190. package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
  191. package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
  192. package/templates/web/src/oj-market/api/client.ts +135 -0
  193. package/templates/web/src/oj-market/api/context.ts +11 -0
  194. package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
  195. package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
  196. package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
  197. package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
  198. package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
  199. package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
  200. package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
  201. package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
  202. package/templates/web/src/oj-market/entry.ts +27 -0
  203. package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
  204. package/templates/web/src/oj-market/locales/en-US.json +100 -0
  205. package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
  206. package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
  207. package/templates/web/src/oj-market/types.ts +84 -0
  208. package/templates/web/src/oj-market/utils/poll.ts +36 -0
  209. package/templates/web/src/oj-market/utils/semver.ts +14 -0
  210. package/templates/web/src/outside/entry.ts +80 -0
  211. package/templates/web/src/outside/locales/en-US.json +10 -0
  212. package/templates/web/src/outside/locales/zh-CN.json +10 -0
  213. package/templates/web/src/personal-center/entry.ts +1 -10
  214. package/templates/web/src/personal-center/locales/en-US.json +1 -5
  215. package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
  216. package/templates/web/src/route-nest/entry.ts +73 -0
  217. package/templates/web/src/route-nest/locales/en-US.json +9 -0
  218. package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
  219. package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
  220. package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
  221. package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
  222. package/templates/web/src/system/__client/api.schemas.ts +12 -0
  223. package/templates/web/src/system/__client/api.ts +25 -1
  224. package/templates/web/src/system/pages/oplog/index.tsx +1 -1
  225. package/templates/web/src/system/pages/roles/index.tsx +1 -0
  226. package/templates/web/src/system/pages/settings/index.tsx +1 -1
  227. package/templates/web/src/system/pages/users/index.tsx +1 -1
  228. package/templates/web.config.ts +32 -40
  229. package/vendor/host-versions.json +3 -3
  230. package/templates/pnpm-workspace.yaml +0 -14
  231. package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
@@ -0,0 +1,70 @@
1
+ import type { MarketClient } from "../api/client";
2
+ // 内存实现 MarketClient(BDD 测试注入用,无需真实后端)。
3
+ import type { EnableInput, IndexRow, InstallInput, MarketHistory, TaskRecord, UninstallImpact } from "../types";
4
+
5
+ export interface FakeState {
6
+ indexRows?: IndexRow[]
7
+ indexError?: Error
8
+ taskRecord?: TaskRecord
9
+ historyRows?: MarketHistory[]
10
+ impact?: UninstallImpact
11
+ enableResult?: { ok: boolean, message: string }
12
+ uninstallResult?: { ok: boolean, message: string }
13
+ installResult?: { task_id: string }
14
+ }
15
+
16
+ export class InMemoryMarketClient implements MarketClient {
17
+ calls: string[] = [];
18
+ constructor(private state: FakeState = {}) {}
19
+
20
+ async meta() {
21
+ this.calls.push("meta");
22
+ return { apiVersion: "0.1.0" };
23
+ }
24
+
25
+ async index() {
26
+ this.calls.push("index");
27
+ if (this.state.indexError)
28
+ throw this.state.indexError;
29
+ return this.state.indexRows ?? [];
30
+ }
31
+
32
+ async install(input: InstallInput) {
33
+ this.calls.push(`install:${input.name}@${input.version ?? "latest"}`);
34
+ return this.state.installResult ?? { task_id: "t1" };
35
+ }
36
+
37
+ async task(id: string) {
38
+ this.calls.push(`task:${id}`);
39
+ return this.state.taskRecord ?? ({
40
+ id,
41
+ name: "",
42
+ version: "",
43
+ status: "done",
44
+ stage: "done",
45
+ progress: 100,
46
+ message: "ok",
47
+ stages: [],
48
+ } as TaskRecord);
49
+ }
50
+
51
+ async enable(input: EnableInput) {
52
+ this.calls.push(`enable:${input.name}:${input.enabled}`);
53
+ return this.state.enableResult ?? { ok: true, message: "ok" };
54
+ }
55
+
56
+ async history() {
57
+ this.calls.push("history");
58
+ return this.state.historyRows ?? [];
59
+ }
60
+
61
+ async uninstallImpact(name: string) {
62
+ this.calls.push(`uninstallImpact:${name}`);
63
+ return this.state.impact ?? { name, affectedRoles: 0, affectedUsers: 0 };
64
+ }
65
+
66
+ async uninstall(name: string) {
67
+ this.calls.push(`uninstall:${name}`);
68
+ return this.state.uninstallResult ?? { ok: true, message: "ok" };
69
+ }
70
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { poll } from "../utils/poll";
3
+
4
+ describe("poll 轮询工具(BDD)", () => {
5
+ it("given 首次非终态 When 轮询 Then 在终态返回且不抛错", async () => {
6
+ let n = 0;
7
+ const result = await poll(
8
+ async () => ++n,
9
+ v => v >= 3,
10
+ { intervalMs: 1, timeoutMs: 1000 },
11
+ );
12
+ expect(result).toBe(3);
13
+ });
14
+
15
+ it("given 始终非终态超过超时 When 轮询 Then 返回最近一次值(不抛)", async () => {
16
+ let n = 0;
17
+ const result = await poll(
18
+ async () => ++n,
19
+ () => false,
20
+ { intervalMs: 1, timeoutMs: 10 },
21
+ );
22
+ expect(result).toBeGreaterThan(0);
23
+ });
24
+ });
@@ -0,0 +1,14 @@
1
+ import type { MarketClient } from "../api/client";
2
+ import { render } from "@testing-library/react";
3
+ import { ConfigProvider } from "antd";
4
+ import React from "react";
5
+ import { MarketClientProvider } from "../api/MarketClientProvider";
6
+
7
+ /** BDD 测试渲染器:注入客户端 + antd ConfigProvider(关闭按钮内 CJK 空格插入,保证文本断言精确) */
8
+ export function renderWithProviders(ui: React.ReactElement, client: MarketClient) {
9
+ return render(
10
+ <ConfigProvider button={{ autoInsertSpace: false }}>
11
+ <MarketClientProvider client={client}>{ui}</MarketClientProvider>
12
+ </ConfigProvider>,
13
+ );
14
+ }
@@ -0,0 +1,14 @@
1
+ // 测试用运行时桩:组件测试经 MarketClientContext 注入内存 fake,从不真正调用 request,
2
+ // 故只需 defineModule(identity)与最小 ky 形态 request,避免引入宿主构建期注入的全局(__APP_INFO__)。
3
+ export function defineModule(def: unknown) {
4
+ return def;
5
+ }
6
+
7
+ export const request = {
8
+ get(_url: string, _opts?: unknown) {
9
+ return Promise.resolve({ json: () => Promise.resolve({}) });
10
+ },
11
+ post(_url: string, _opts?: unknown) {
12
+ return Promise.resolve({ json: () => Promise.resolve({}) });
13
+ },
14
+ };
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { compareVersions } from "../utils/semver";
3
+
4
+ describe("compareVersions 版本比较(BDD)", () => {
5
+ it("given 同版本 When 比较 Then 返回 0", () => {
6
+ expect(compareVersions("1.2.0", "1.2.0")).toBe(0);
7
+ expect(compareVersions("0.1.0", "0.1.0")).toBe(0);
8
+ });
9
+
10
+ it("given 主/次/修订不同 When 比较 Then 逐段定序", () => {
11
+ expect(compareVersions("1.2.0", "1.1.9")).toBeGreaterThan(0);
12
+ expect(compareVersions("1.1.9", "1.2.0")).toBeLessThan(0);
13
+ expect(compareVersions("2.0.0", "1.9.9")).toBeGreaterThan(0);
14
+ expect(compareVersions("0.1.0", "0.1.1")).toBeLessThan(0);
15
+ });
16
+
17
+ it("given 段数不同 When 比较 Then 缺段按 0 补齐", () => {
18
+ expect(compareVersions("1.2", "1.2.0")).toBe(0);
19
+ expect(compareVersions("1.2.1", "1.2")).toBeGreaterThan(0);
20
+ });
21
+
22
+ it("given 预发布连字符 When 比较 Then 数值段口径不崩", () => {
23
+ // 前端口径:连字符按分隔处理,非数值记 0(服务端 semver 为权威,此处仅 UI 排序)
24
+ expect(typeof compareVersions("1.0.0-beta", "1.0.0")).toBe("number");
25
+ });
26
+ });
@@ -0,0 +1,12 @@
1
+ import type { MarketClient } from "./client";
2
+ import React from "react";
3
+ import { MarketClientContext } from "./context";
4
+ import { httpMarketClient } from "./defaultClient";
5
+
6
+ /**
7
+ * 注入市场客户端(默认走运行时 HTTP 实现,测试传内存 fake)。
8
+ * 单独成文件:`react-refresh` 要求组件文件只导出组件,context 在 ./context。
9
+ */
10
+ export function MarketClientProvider({ client, children }: { client?: MarketClient, children: React.ReactNode }) {
11
+ return React.createElement(MarketClientContext.Provider, { value: client ?? httpMarketClient }, children);
12
+ }
@@ -0,0 +1,135 @@
1
+ // 市场 API 客户端(DIP):定义与宿主 _platform 编排面 + market_history 端点的契约。
2
+ // 实现与 ky 形态解耦——组件/hooks 注入 MarketClient,测试可注入内存 fake(见 __tests__)。
3
+ // 第一方专用调用面(R29):仅市场应用消费 /api/_platform/market/*,第三方方案不得仿照。
4
+
5
+ import type {
6
+ EnableInput,
7
+ IndexRow,
8
+ InstallInput,
9
+ MarketHistory,
10
+ TaskRecord,
11
+ UninstallImpact,
12
+ } from "../types";
13
+
14
+ /** ky 形态的最小请求子集(get/post 返回带 .json<T>() 的响应) */
15
+ export interface HttpRequest {
16
+ get: <T = unknown>(
17
+ url: string,
18
+ // silentError:错误体由本客户端消化(未配置市场地址走空态),不弹全局 toast;
19
+ // retry: 0:503 是配置态而非抖动,默认重试 3 次只会刷出 4 条同样错误。
20
+ opts?: { searchParams?: Record<string, string>, silentError?: boolean, retry?: number },
21
+ ) => Promise<{ json: <R = T>() => Promise<R> }>
22
+ post: <T = unknown>(
23
+ url: string,
24
+ opts?: { json?: unknown },
25
+ ) => Promise<{ json: <R = T>() => Promise<R> }>
26
+ }
27
+
28
+ /** 与运行时/生成客户端一致的响应信封(DIP:服务端统一 {code,msg,data} 包裹) */
29
+ interface OjEnvelope<T> {
30
+ code: number
31
+ msg?: string
32
+ data?: T
33
+ }
34
+
35
+ /** 市场客户端契约(组件/hooks 依赖此接口,而非具体 http 实现) */
36
+ export interface MarketClient {
37
+ /** 登录即可:{ apiVersion }(peerPlatform 预检口径) */
38
+ meta: () => Promise<{ apiVersion: string }>
39
+ /** 卡片数据源(superadmin) */
40
+ index: () => Promise<IndexRow[]>
41
+ /** 触发安装,返回轮询用 task_id */
42
+ install: (input: InstallInput) => Promise<{ task_id: string }>
43
+ /** 任务轮询消费面(query 形态 ?id=) */
44
+ task: (id: string) => Promise<TaskRecord>
45
+ /** 生命周期开关(同步 setEnabled,无任务模型;{ok,message} 200 渲染 ok 位) */
46
+ enable: (input: EnableInput) => Promise<{ ok: boolean, message: string }>
47
+ /** 市场操作历史(_platform 只读端点 /_platform/market/history) */
48
+ history: () => Promise<MarketHistory[]>
49
+ /** 卸载影响面预检(P3 U1;服务端返回供二次确认,P1-15a 补齐后接通) */
50
+ uninstallImpact: (name: string) => Promise<UninstallImpact>
51
+ /** 卸载(P1-15a 补齐后接通) */
52
+ uninstall: (name: string) => Promise<{ ok: boolean, message: string }>
53
+ }
54
+
55
+ // 第一方市场端点:相对前缀,由运行时 request 的 VITE_API_BASE_URL(/api)统一加前缀;
56
+ // 此处若再写 /api 会与运行时前缀叠加成 /api/api/(R29 联调发现)。
57
+ const PLATFORM = "/_platform/market";
58
+ const HISTORY = "/_platform/market/history";
59
+
60
+ /**
61
+ * 503 = 服务端「未配置市场地址(framework.market_cdn_url)」,是**预期空态**而非故障
62
+ * (PRD §5.3:卡片区显示「未配置市场地址」)。模板默认不配市场地址,新工程一进市场页
63
+ * 就会撞它——在此归空数组,交由卡片空态文案承载;其它状态码照旧上抛。
64
+ */
65
+ function isUnconfigured(e: unknown): boolean {
66
+ return (e as { response?: { status?: number } } | null)?.response?.status === 503;
67
+ }
68
+
69
+ /** 默认 HTTP 实现:基于运行时 request(ky 形态) */
70
+ export function createHttpMarketClient(request: HttpRequest): MarketClient {
71
+ return {
72
+ async meta() {
73
+ const r = await request.get<OjEnvelope<{ apiVersion: string }>>(`${PLATFORM}/meta`);
74
+ const env = await r.json();
75
+ return env.data!;
76
+ },
77
+ async index() {
78
+ try {
79
+ const r = await request.get<OjEnvelope<IndexRow[]>>(`${PLATFORM}/index`, {
80
+ silentError: true,
81
+ retry: 0,
82
+ });
83
+ const env = await r.json();
84
+ return env.data!;
85
+ }
86
+ catch (e) {
87
+ if (isUnconfigured(e))
88
+ return [];
89
+ throw e;
90
+ }
91
+ },
92
+ async install(input: InstallInput) {
93
+ const r = await request.post<OjEnvelope<{ task_id: string }>>(`${PLATFORM}/install`, {
94
+ json: {
95
+ name: input.name,
96
+ version: input.version ?? null,
97
+ source: input.source ?? "cdn",
98
+ },
99
+ });
100
+ const env = await r.json();
101
+ return env.data!;
102
+ },
103
+ async task(id: string) {
104
+ const r = await request.get<OjEnvelope<TaskRecord>>(`${PLATFORM}/task`, { searchParams: { id } });
105
+ const env = await r.json();
106
+ return env.data!;
107
+ },
108
+ async enable(input: EnableInput) {
109
+ const r = await request.post<OjEnvelope<{ ok: boolean, message: string }>>(`${PLATFORM}/enable`, {
110
+ json: { name: input.name, enabled: input.enabled },
111
+ });
112
+ const env = await r.json();
113
+ return env.data!;
114
+ },
115
+ async history() {
116
+ const r = await request.get<OjEnvelope<MarketHistory[]>>(HISTORY);
117
+ const env = await r.json();
118
+ return env.data!;
119
+ },
120
+ async uninstallImpact(name: string) {
121
+ const r = await request.get<OjEnvelope<UninstallImpact>>(`${PLATFORM}/uninstall-impact`, {
122
+ searchParams: { name },
123
+ });
124
+ const env = await r.json();
125
+ return env.data!;
126
+ },
127
+ async uninstall(name: string) {
128
+ const r = await request.post<OjEnvelope<{ ok: boolean, message: string }>>(`${PLATFORM}/uninstall`, {
129
+ json: { name },
130
+ });
131
+ const env = await r.json();
132
+ return env.data!;
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,11 @@
1
+ import type { MarketClient } from "./client";
2
+ import { createContext, useContext } from "react";
3
+ import { httpMarketClient } from "./defaultClient";
4
+
5
+ /** 市场客户端上下文(依赖注入位;默认 = 运行时 HTTP 实现,测试可替换为内存 fake) */
6
+ export const MarketClientContext = createContext<MarketClient>(httpMarketClient);
7
+
8
+ /** 组件/hooks 取客户端(默认注入运行时实现) */
9
+ export function useMarketClient(): MarketClient {
10
+ return useContext(MarketClientContext);
11
+ }
@@ -0,0 +1,7 @@
1
+ // 默认市场客户端单例:基于运行时 request(ky 形态)。
2
+ // 组件经 MarketClientContext 消费,测试可注入内存 fake,避免依赖真实后端(DIP)。
3
+ import { request } from "@oj-module/runtime";
4
+ import { createHttpMarketClient } from "./client";
5
+
6
+ /** 运行时 request 形态与 HttpRequest 兼容(ky get/post 返回带 .json<T>() 的响应) */
7
+ export const httpMarketClient = createHttpMarketClient(request as unknown as Parameters<typeof createHttpMarketClient>[0]);
@@ -0,0 +1,76 @@
1
+ import type { IndexRow } from "../types";
2
+ import { Empty, Spin } from "antd";
3
+ import React from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { CardItem } from "./CardItem";
6
+
7
+ export interface CardGridProps {
8
+ /** 加载态:true 时渲染加载骨架 */
9
+ loading?: boolean
10
+ /** 错误态:非空时渲染错误 + 重试 */
11
+ error?: string | null
12
+ rows: IndexRow[]
13
+ busyName?: string | null
14
+ onInstall: (row: IndexRow) => void
15
+ onEnableToggle: (row: IndexRow) => void
16
+ onDetails: (row: IndexRow) => void
17
+ onHistory: (row: IndexRow) => void
18
+ onUninstall: (row: IndexRow) => void
19
+ onRollback: (row: IndexRow) => void
20
+ onRetry: () => void
21
+ }
22
+
23
+ /** 卡片网格容器:三态(加载/空/错误)+ 正常网格,DRY 渲染交由 CardItem */
24
+ export function CardGrid(props: CardGridProps) {
25
+ const { t } = useTranslation();
26
+ const { loading, error, rows, busyName, onInstall, onEnableToggle, onDetails, onHistory, onUninstall, onRollback, onRetry } = props;
27
+
28
+ if (loading) {
29
+ return (
30
+ <div data-testid="card-grid-loading" style={{ padding: 48, textAlign: "center" }}>
31
+ <Spin />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (error) {
37
+ return (
38
+ <div data-testid="card-grid-error" style={{ padding: 48, textAlign: "center" }}>
39
+ <Empty description={t("oj-market:card:error")} />
40
+ <div style={{ marginTop: 12 }}>{error}</div>
41
+ <button data-testid="card-grid-retry" onClick={onRetry}>
42
+ {t("oj-market:card:retry")}
43
+ </button>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ if (rows.length === 0) {
49
+ return (
50
+ <div data-testid="card-grid-empty" style={{ padding: 48, textAlign: "center" }}>
51
+ <Empty description={t("oj-market:card:empty")} />
52
+ </div>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <div
58
+ data-testid="card-grid"
59
+ style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 16 }}
60
+ >
61
+ {rows.map(row => (
62
+ <CardItem
63
+ key={row.name}
64
+ row={row}
65
+ busy={busyName === row.name}
66
+ onInstall={onInstall}
67
+ onEnableToggle={onEnableToggle}
68
+ onDetails={onDetails}
69
+ onHistory={onHistory}
70
+ onUninstall={onUninstall}
71
+ onRollback={onRollback}
72
+ />
73
+ ))}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,102 @@
1
+ import type { IndexRow } from "../types";
2
+ import { Button, Card, Space, Tag } from "antd";
3
+ import React from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ export interface CardItemProps {
7
+ row: IndexRow
8
+ busy?: boolean
9
+ onInstall: (row: IndexRow) => void
10
+ onEnableToggle: (row: IndexRow) => void
11
+ onDetails: (row: IndexRow) => void
12
+ onHistory: (row: IndexRow) => void
13
+ onUninstall: (row: IndexRow) => void
14
+ onRollback: (row: IndexRow) => void
15
+ }
16
+
17
+ /**
18
+ * 单张方案卡片:版本/状态标识(enabled/disabled/upgradable)+ 动作按钮。
19
+ * 双版本展示(P1-15b):已装且可升级时并列「已装 installed → 最新 latest」(upgradable 对比),
20
+ * 否则单版本。已装方案提供回滚入口(RollbackDialog 选旧版降级安装)。
21
+ */
22
+ export function CardItem({ row, busy, onInstall, onEnableToggle, onDetails, onHistory, onUninstall, onRollback }: CardItemProps) {
23
+ const { t } = useTranslation();
24
+ const installed = row.installed != null;
25
+ const enabled = row.enabled === true;
26
+ const disabled = row.enabled === false;
27
+ const dualVersion = installed && row.upgradable && row.latest != null;
28
+
29
+ return (
30
+ <Card
31
+ title={row.name}
32
+ size="small"
33
+ data-testid={`card-${row.name}`}
34
+ extra={(
35
+ <Space size={4}>
36
+ {row.upgradable && <Tag color="gold">{t("oj-market:card:upgradable")}</Tag>}
37
+ {!installed && <Tag>{t("oj-market:card:notInstalled")}</Tag>}
38
+ {enabled && <Tag color="green">{t("oj-market:card:enabled")}</Tag>}
39
+ {disabled && <Tag color="default">{t("oj-market:card:disabled")}</Tag>}
40
+ </Space>
41
+ )}
42
+ >
43
+ <div style={{ minHeight: 40, color: "var(--oo-text-secondary, #888)", marginBottom: 12 }} data-testid={`card-versions-${row.name}`}>
44
+ {dualVersion
45
+ ? (
46
+ <span>
47
+ {t("oj-market:card:installed")}
48
+ {" "}
49
+ {row.installed}
50
+ {" "}
51
+ →
52
+ {" "}
53
+ {t("oj-market:card:latest")}
54
+ {" "}
55
+ {row.latest}
56
+ </span>
57
+ )
58
+ : installed
59
+ ? (
60
+ <span>
61
+ {t("oj-market:card:version")}
62
+ :
63
+ {row.installed}
64
+ </span>
65
+ )
66
+ : (
67
+ <span>
68
+ {t("oj-market:card:version")}
69
+ :
70
+ {row.latest ?? "-"}
71
+ </span>
72
+ )}
73
+ </div>
74
+ <Space wrap>
75
+ <Button type="primary" size="small" loading={busy} onClick={() => onInstall(row)} disabled={busy}>
76
+ {installed ? t("oj-market:card:upgrade") : t("oj-market:card:install")}
77
+ </Button>
78
+ {installed && (
79
+ <Button size="small" onClick={() => onEnableToggle(row)} disabled={busy}>
80
+ {enabled ? t("oj-market:card:disable") : t("oj-market:card:enable")}
81
+ </Button>
82
+ )}
83
+ {installed && (
84
+ <Button size="small" onClick={() => onRollback(row)} disabled={busy} data-testid={`card-rollback-${row.name}`}>
85
+ {t("oj-market:card:rollback")}
86
+ </Button>
87
+ )}
88
+ {installed && (
89
+ <Button size="small" danger onClick={() => onUninstall(row)} disabled={busy}>
90
+ {t("oj-market:card:uninstall")}
91
+ </Button>
92
+ )}
93
+ <Button size="small" onClick={() => onDetails(row)}>
94
+ {t("oj-market:card:details")}
95
+ </Button>
96
+ <Button size="small" onClick={() => onHistory(row)}>
97
+ {t("oj-market:card:history")}
98
+ </Button>
99
+ </Space>
100
+ </Card>
101
+ );
102
+ }
@@ -0,0 +1,53 @@
1
+ import type { IndexRow } from "../types";
2
+ import { Button, Descriptions, Drawer, Space, Tag } from "antd";
3
+ import React from "react";
4
+ import { useTranslation } from "react-i18next";
5
+
6
+ export interface DetailDrawerProps {
7
+ row: IndexRow | null
8
+ open: boolean
9
+ onClose: () => void
10
+ onInstall: (row: IndexRow) => void
11
+ onHistory: (row: IndexRow) => void
12
+ onRollback: (row: IndexRow) => void
13
+ }
14
+
15
+ /** 方案详情抽屉:只读展示卡片元信息(含双版本对比)+ 安装/回滚/历史入口 */
16
+ export function DetailDrawer({ row, open, onClose, onInstall, onHistory, onRollback }: DetailDrawerProps) {
17
+ const { t } = useTranslation();
18
+ return (
19
+ <Drawer
20
+ title={row ? row.name : t("oj-market:card:details")}
21
+ open={open}
22
+ onClose={onClose}
23
+ width={420}
24
+ data-testid="detail-drawer"
25
+ >
26
+ {row && (
27
+ <>
28
+ <Descriptions column={1} size="small" bordered>
29
+ <Descriptions.Item label={t("oj-market:card:latest")}>{row.latest ?? "-"}</Descriptions.Item>
30
+ <Descriptions.Item label={t("oj-market:card:installed")}>{row.installed ?? t("oj-market:card:notInstalled")}</Descriptions.Item>
31
+ <Descriptions.Item label={t("oj-market:card:enabled")}>
32
+ {row.enabled == null ? "-" : row.enabled ? t("oj-market:card:enabled") : t("oj-market:card:disabled")}
33
+ </Descriptions.Item>
34
+ <Descriptions.Item label={t("oj-market:card:upgradable")}>
35
+ {row.upgradable ? <Tag color="gold">{t("oj-market:card:upgradable")}</Tag> : "-"}
36
+ </Descriptions.Item>
37
+ </Descriptions>
38
+ <Space style={{ marginTop: 16 }}>
39
+ <Button type="primary" onClick={() => onInstall(row)}>
40
+ {row.installed ? t("oj-market:card:upgrade") : t("oj-market:card:install")}
41
+ </Button>
42
+ {row.installed != null && (
43
+ <Button onClick={() => onRollback(row)} data-testid="detail-rollback">
44
+ {t("oj-market:card:rollback")}
45
+ </Button>
46
+ )}
47
+ <Button onClick={() => onHistory(row)}>{t("oj-market:card:history")}</Button>
48
+ </Space>
49
+ </>
50
+ )}
51
+ </Drawer>
52
+ );
53
+ }
@@ -0,0 +1,75 @@
1
+ import type { IndexRow, MarketHistory } from "../types";
2
+ import { Drawer, Table } from "antd";
3
+ import React from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useMarketClient } from "../api/context";
6
+ import { useMarketHistory } from "../hooks/useMarket";
7
+
8
+ export interface HistoryDrawerProps {
9
+ row: IndexRow | null
10
+ open: boolean
11
+ onClose: () => void
12
+ }
13
+
14
+ /** 操作历史抽屉:只读消费本仓 market_history 端点,按当前方案过滤 */
15
+ export function HistoryDrawer({ row, open, onClose }: HistoryDrawerProps) {
16
+ const { t } = useTranslation();
17
+ const client = useMarketClient();
18
+ void client;
19
+ const { status, data, retry } = useMarketHistory();
20
+
21
+ const rows: MarketHistory[] = (status === "ok" ? data : []).filter(
22
+ h => !row || h.solution === row.name,
23
+ );
24
+
25
+ const columns = [
26
+ { title: t("oj-market:history:colSolution"), dataIndex: "solution", key: "solution" },
27
+ { title: t("oj-market:history:colVersion"), dataIndex: "version", key: "version" },
28
+ {
29
+ title: t("oj-market:history:colAction"),
30
+ dataIndex: "action",
31
+ key: "action",
32
+ render: (a: MarketHistory["action"]) => t(`oj-market:history:action.${a}`),
33
+ },
34
+ {
35
+ title: t("oj-market:history:colStatus"),
36
+ dataIndex: "status",
37
+ key: "status",
38
+ render: (s: string) => t(`oj-market:history:status.${s}`, s),
39
+ },
40
+ { title: t("oj-market:history:colOperator"), dataIndex: "operator", key: "operator" },
41
+ {
42
+ title: t("oj-market:history:colTime"),
43
+ dataIndex: "created_at",
44
+ key: "created_at",
45
+ render: (ts: number) => new Date(ts * 1000).toLocaleString(),
46
+ },
47
+ ];
48
+
49
+ return (
50
+ <Drawer
51
+ title={t("oj-market:history:title")}
52
+ open={open}
53
+ onClose={onClose}
54
+ width={560}
55
+ data-testid="history-drawer"
56
+ >
57
+ {status === "error" && (
58
+ <div data-testid="history-error">
59
+ {t("oj-market:card:error")}
60
+ :
61
+ {retry && <button onClick={retry}>{t("oj-market:card:retry")}</button>}
62
+ </div>
63
+ )}
64
+ <Table
65
+ size="small"
66
+ rowKey="id"
67
+ dataSource={rows}
68
+ columns={columns}
69
+ pagination={false}
70
+ locale={{ emptyText: t("oj-market:history:empty") }}
71
+ data-testid="history-table"
72
+ />
73
+ </Drawer>
74
+ );
75
+ }