@oj-module/cli 0.2.5 → 0.2.8

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 (105) hide show
  1. package/README.md +1 -1
  2. package/devkit/manual.md +19 -17
  3. package/devkit/sql-builder-missing-capabilities-pr.md +234 -0
  4. package/package.json +2 -2
  5. package/shell-dist/assets/runtime.js +2 -2
  6. package/shell-dist/index.html +2 -2
  7. package/shell-dist/versions.json +3 -3
  8. package/src/contract/__tests__/handler-scan.test.ts +29 -0
  9. package/src/contract/__tests__/market-client.integration.test.ts +167 -0
  10. package/src/contract/handler-scan.ts +35 -4
  11. package/src/dev.ts +10 -2
  12. package/src/index.ts +3 -3
  13. package/src/init.ts +44 -0
  14. package/src/preview-config.ts +163 -0
  15. package/src/preview.ts +105 -56
  16. package/src/usage.ts +3 -2
  17. package/templates/api/config.yaml +4 -2
  18. package/templates/api/src/_platform/README.md +31 -0
  19. package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
  20. package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
  21. package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
  22. package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
  23. package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
  24. package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
  25. package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
  26. package/templates/api/src/_platform/_lifecycle/uninstall.ts +13 -8
  27. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
  28. package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +8 -6
  29. package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
  30. package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
  31. package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +18 -17
  32. package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
  33. package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
  34. package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
  35. package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +12 -12
  36. package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
  37. package/templates/api/src/_platform/contacts/api.ts +2 -2
  38. package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
  39. package/templates/api/src/_platform/manifest.yaml +3 -1
  40. package/templates/api/src/_platform/market/api.ts +54 -16
  41. package/templates/api/src/_platform/schema.yaml +14 -0
  42. package/templates/api/src/_platform/solutions/api.ts +13 -12
  43. package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
  44. package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
  45. package/templates/api/src/auth/_shared/session.ts +25 -21
  46. package/templates/api/src/auth/api.ts +1 -1
  47. package/templates/api/src/demo/todos/api.ts +1 -0
  48. package/templates/api/src/home/line/api.ts +4 -4
  49. package/templates/api/src/home/pie/api.ts +4 -4
  50. package/templates/api/src/notification/_shared/notify.ts +40 -38
  51. package/templates/api/src/notifications/api.ts +1 -4
  52. package/templates/api/src/personal-center/upload/api.ts +1 -1
  53. package/templates/api/src/system/login-methods/api.ts +1 -1
  54. package/templates/api/src/system/oplog/api.ts +3 -3
  55. package/templates/api/src/system/permissions/api.ts +2 -2
  56. package/templates/api/src/system/roles/api.ts +2 -2
  57. package/templates/api/src/system/roles/bindings/api.ts +2 -2
  58. package/templates/api/src/system/settings/api.ts +2 -2
  59. package/templates/api/src/system/users/api.ts +2 -2
  60. package/templates/api/src/system/users/forbidden/api.ts +2 -2
  61. package/templates/api/src/system/users/password/api.ts +2 -2
  62. package/templates/api/src/system/users/roles/api.ts +2 -2
  63. package/templates/api/src/web/user-info/api.ts +2 -5
  64. package/templates/api/tests/auth-session.spec.ts +1 -1
  65. package/templates/api/tests/auth-throttle.spec.ts +1 -1
  66. package/templates/api/tests/guard.spec.ts +1 -1
  67. package/templates/api/tests/lifecycle/market-api.spec.ts +1 -1
  68. package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
  69. package/templates/api/tests/lifecycle/uninstall.spec.ts +1 -1
  70. package/templates/api/tests/query-utils.spec.ts +1 -1
  71. package/templates/api/tests/rbac-audit.spec.ts +1 -1
  72. package/templates/api/tests/rbac-contacts.spec.ts +2 -2
  73. package/templates/api/tests/rbac-guard.spec.ts +1 -1
  74. package/templates/api/tests/rbac-roles.spec.ts +2 -2
  75. package/templates/api/tests/rbac-settings.spec.ts +2 -2
  76. package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
  77. package/templates/api/tests/rbac-users.spec.ts +1 -1
  78. package/templates/api/tsconfig.json +8 -0
  79. package/templates/env.d.ts +2 -0
  80. package/templates/global.d.ts +816 -0
  81. package/templates/init.include.yaml +18 -1
  82. package/templates/logs/task-events.jsonl +0 -0
  83. package/templates/openapi.json +525 -13
  84. package/templates/package.json +2 -1
  85. package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
  86. package/templates/web/src/_platform/__client/api.ts +152 -39
  87. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
  88. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
  89. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
  90. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
  91. package/templates/web/src/oj-market/api/client.ts +5 -105
  92. package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
  93. package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
  94. package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
  95. package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
  96. package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
  97. package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
  98. package/templates/web/src/oj-market/locales/en-US.json +2 -1
  99. package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
  100. package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
  101. package/templates/web.config.ts +3 -3
  102. package/vendor/host-versions.json +3 -3
  103. /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
  104. /package/templates/api/src/_platform/_shared/service/{oplog.ts → infra/oplog.ts} +0 -0
  105. /package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +0 -0
@@ -4,5 +4,102 @@
4
4
  import { z } from "@oj-module/runtime";
5
5
 
6
6
  export const schemas = {
7
+ "platformMarketInstallPost": {
8
+ body: z.object({
9
+ "source": z.enum(["cdn", "file"]),
10
+ "name": z.string(),
11
+ "version": z.string().nullable().optional()
12
+ }),
13
+ data: z.object({
14
+ "task_id": z.string().optional()
15
+ }).catchall(z.unknown()),
16
+ },
17
+ "platformMarketInstallCancelPost": {
18
+ body: z.object({
19
+ "name": z.string()
20
+ }),
21
+ data: z.object({
22
+ "ok": z.boolean().optional(),
23
+ "name": z.string().optional(),
24
+ "requested": z.boolean().optional()
25
+ }).catchall(z.unknown()),
26
+ },
27
+ "platformMarketTaskGet": {
28
+ query: z.object({
29
+ "id": z.string()
30
+ }),
31
+ data: z.object({
7
32
 
33
+ }).catchall(z.unknown()),
34
+ },
35
+ "platformMarketEnablePost": {
36
+ body: z.object({
37
+ "name": z.string(),
38
+ "enabled": z.boolean()
39
+ }),
40
+ data: z.object({
41
+
42
+ }).catchall(z.unknown()),
43
+ },
44
+ "platformMarketMetaGet": {
45
+ data: z.object({
46
+ "apiVersion": z.string().optional()
47
+ }).catchall(z.unknown()),
48
+ },
49
+ "platformMarketTrustGet": {
50
+ data: z.object({
51
+
52
+ }).catchall(z.unknown()),
53
+ },
54
+ "platformMarketTrustPost": {
55
+ body: z.object({
56
+ "task_id": z.string()
57
+ }),
58
+ data: z.object({
59
+
60
+ }).catchall(z.unknown()),
61
+ },
62
+ "platformMarketTrustKeyPost": {
63
+ body: z.object({
64
+ "key_id": z.string(),
65
+ "public_key": z.string()
66
+ }),
67
+ data: z.object({
68
+
69
+ }).catchall(z.unknown()),
70
+ },
71
+ "platformMarketTrustKeyDelete": {
72
+ query: z.object({
73
+ "key_id": z.string()
74
+ }),
75
+ data: z.object({
76
+
77
+ }).catchall(z.unknown()),
78
+ },
79
+ "platformMarketIndexGet": {
80
+ data: z.array(z.object({
81
+
82
+ }).catchall(z.unknown())),
83
+ },
84
+ "platformMarketUninstallImpactGet": {
85
+ query: z.object({
86
+ "name": z.string()
87
+ }),
88
+ data: z.object({
89
+
90
+ }).catchall(z.unknown()),
91
+ },
92
+ "platformMarketUninstallPost": {
93
+ body: z.object({
94
+ "name": z.string()
95
+ }),
96
+ data: z.object({
97
+
98
+ }).catchall(z.unknown()),
99
+ },
100
+ "platformMarketHistoryGet": {
101
+ data: z.array(z.object({
102
+
103
+ }).catchall(z.unknown())),
104
+ },
8
105
  };
@@ -94,14 +94,23 @@ export async function platformContactsDetailGet(): Promise<unknown> {
94
94
  }
95
95
  }
96
96
 
97
- export async function platformMarketInstallPost(): Promise<unknown> {
97
+ export type PlatformMarketInstallPostBody = z.input<(typeof schemas)["platformMarketInstallPost"]["body"]>;
98
+ export type PlatformMarketInstallPostData = z.infer<(typeof schemas)["platformMarketInstallPost"]["data"]>;
99
+
100
+ export async function platformMarketInstallPost(body: PlatformMarketInstallPostBody): Promise<PlatformMarketInstallPostData> {
98
101
  const client = ensureReq();
99
102
  try {
100
- const env = await client.post(`_platform/market/install`).json<OjEnvelope<unknown>>();
103
+ const env = await client.post(`_platform/market/install`, { json: body }).json<OjEnvelope<PlatformMarketInstallPostData>>();
101
104
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
102
105
  if (typeof env.code === "number" && env.code !== 0)
103
106
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
104
- const data = env.data as unknown;
107
+ const data = env.data as PlatformMarketInstallPostData;
108
+ if (import.meta.env.DEV) {
109
+ const { schemas } = await import("./api.schemas");
110
+ const r = schemas["platformMarketInstallPost"].data.safeParse(data);
111
+ if (!r.success)
112
+ throw new ContractApiError(-1, `[契约违例] platformMarketInstallPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
113
+ }
105
114
  return data;
106
115
  }
107
116
  catch (e) {
@@ -109,14 +118,23 @@ export async function platformMarketInstallPost(): Promise<unknown> {
109
118
  }
110
119
  }
111
120
 
112
- export async function platformMarketInstallCancelPost(): Promise<unknown> {
121
+ export type PlatformMarketInstallCancelPostBody = z.input<(typeof schemas)["platformMarketInstallCancelPost"]["body"]>;
122
+ export type PlatformMarketInstallCancelPostData = z.infer<(typeof schemas)["platformMarketInstallCancelPost"]["data"]>;
123
+
124
+ export async function platformMarketInstallCancelPost(body: PlatformMarketInstallCancelPostBody): Promise<PlatformMarketInstallCancelPostData> {
113
125
  const client = ensureReq();
114
126
  try {
115
- const env = await client.post(`_platform/market/install/cancel`).json<OjEnvelope<unknown>>();
127
+ const env = await client.post(`_platform/market/install/cancel`, { json: body }).json<OjEnvelope<PlatformMarketInstallCancelPostData>>();
116
128
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
117
129
  if (typeof env.code === "number" && env.code !== 0)
118
130
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
119
- const data = env.data as unknown;
131
+ const data = env.data as PlatformMarketInstallCancelPostData;
132
+ if (import.meta.env.DEV) {
133
+ const { schemas } = await import("./api.schemas");
134
+ const r = schemas["platformMarketInstallCancelPost"].data.safeParse(data);
135
+ if (!r.success)
136
+ throw new ContractApiError(-1, `[契约违例] platformMarketInstallCancelPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
137
+ }
120
138
  return data;
121
139
  }
122
140
  catch (e) {
@@ -124,14 +142,23 @@ export async function platformMarketInstallCancelPost(): Promise<unknown> {
124
142
  }
125
143
  }
126
144
 
127
- export async function platformMarketTaskGet(): Promise<unknown> {
145
+ export type PlatformMarketTaskGetQuery = z.input<(typeof schemas)["platformMarketTaskGet"]["query"]>;
146
+ export type PlatformMarketTaskGetData = z.infer<(typeof schemas)["platformMarketTaskGet"]["data"]>;
147
+
148
+ export async function platformMarketTaskGet(query: PlatformMarketTaskGetQuery): Promise<PlatformMarketTaskGetData> {
128
149
  const client = ensureReq();
129
150
  try {
130
- const env = await client.get(`_platform/market/task`).json<OjEnvelope<unknown>>();
151
+ const env = await client.get(`_platform/market/task`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketTaskGetData>>();
131
152
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
132
153
  if (typeof env.code === "number" && env.code !== 0)
133
154
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
134
- const data = env.data as unknown;
155
+ const data = env.data as PlatformMarketTaskGetData;
156
+ if (import.meta.env.DEV) {
157
+ const { schemas } = await import("./api.schemas");
158
+ const r = schemas["platformMarketTaskGet"].data.safeParse(data);
159
+ if (!r.success)
160
+ throw new ContractApiError(-1, `[契约违例] platformMarketTaskGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
161
+ }
135
162
  return data;
136
163
  }
137
164
  catch (e) {
@@ -139,14 +166,23 @@ export async function platformMarketTaskGet(): Promise<unknown> {
139
166
  }
140
167
  }
141
168
 
142
- export async function platformMarketEnablePost(): Promise<unknown> {
169
+ export type PlatformMarketEnablePostBody = z.input<(typeof schemas)["platformMarketEnablePost"]["body"]>;
170
+ export type PlatformMarketEnablePostData = z.infer<(typeof schemas)["platformMarketEnablePost"]["data"]>;
171
+
172
+ export async function platformMarketEnablePost(body: PlatformMarketEnablePostBody): Promise<PlatformMarketEnablePostData> {
143
173
  const client = ensureReq();
144
174
  try {
145
- const env = await client.post(`_platform/market/enable`).json<OjEnvelope<unknown>>();
175
+ const env = await client.post(`_platform/market/enable`, { json: body }).json<OjEnvelope<PlatformMarketEnablePostData>>();
146
176
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
147
177
  if (typeof env.code === "number" && env.code !== 0)
148
178
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
149
- const data = env.data as unknown;
179
+ const data = env.data as PlatformMarketEnablePostData;
180
+ if (import.meta.env.DEV) {
181
+ const { schemas } = await import("./api.schemas");
182
+ const r = schemas["platformMarketEnablePost"].data.safeParse(data);
183
+ if (!r.success)
184
+ throw new ContractApiError(-1, `[契约违例] platformMarketEnablePost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
185
+ }
150
186
  return data;
151
187
  }
152
188
  catch (e) {
@@ -154,14 +190,22 @@ export async function platformMarketEnablePost(): Promise<unknown> {
154
190
  }
155
191
  }
156
192
 
157
- export async function platformMarketMetaGet(): Promise<unknown> {
193
+ export type PlatformMarketMetaGetData = z.infer<(typeof schemas)["platformMarketMetaGet"]["data"]>;
194
+
195
+ export async function platformMarketMetaGet(): Promise<PlatformMarketMetaGetData> {
158
196
  const client = ensureReq();
159
197
  try {
160
- const env = await client.get(`_platform/market/meta`).json<OjEnvelope<unknown>>();
198
+ const env = await client.get(`_platform/market/meta`).json<OjEnvelope<PlatformMarketMetaGetData>>();
161
199
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
162
200
  if (typeof env.code === "number" && env.code !== 0)
163
201
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
164
- const data = env.data as unknown;
202
+ const data = env.data as PlatformMarketMetaGetData;
203
+ if (import.meta.env.DEV) {
204
+ const { schemas } = await import("./api.schemas");
205
+ const r = schemas["platformMarketMetaGet"].data.safeParse(data);
206
+ if (!r.success)
207
+ throw new ContractApiError(-1, `[契约违例] platformMarketMetaGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
208
+ }
165
209
  return data;
166
210
  }
167
211
  catch (e) {
@@ -169,14 +213,22 @@ export async function platformMarketMetaGet(): Promise<unknown> {
169
213
  }
170
214
  }
171
215
 
172
- export async function platformMarketTrustGet(): Promise<unknown> {
216
+ export type PlatformMarketTrustGetData = z.infer<(typeof schemas)["platformMarketTrustGet"]["data"]>;
217
+
218
+ export async function platformMarketTrustGet(): Promise<PlatformMarketTrustGetData> {
173
219
  const client = ensureReq();
174
220
  try {
175
- const env = await client.get(`_platform/market/trust`).json<OjEnvelope<unknown>>();
221
+ const env = await client.get(`_platform/market/trust`).json<OjEnvelope<PlatformMarketTrustGetData>>();
176
222
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
177
223
  if (typeof env.code === "number" && env.code !== 0)
178
224
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
179
- const data = env.data as unknown;
225
+ const data = env.data as PlatformMarketTrustGetData;
226
+ if (import.meta.env.DEV) {
227
+ const { schemas } = await import("./api.schemas");
228
+ const r = schemas["platformMarketTrustGet"].data.safeParse(data);
229
+ if (!r.success)
230
+ throw new ContractApiError(-1, `[契约违例] platformMarketTrustGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
231
+ }
180
232
  return data;
181
233
  }
182
234
  catch (e) {
@@ -184,14 +236,23 @@ export async function platformMarketTrustGet(): Promise<unknown> {
184
236
  }
185
237
  }
186
238
 
187
- export async function platformMarketTrustPost(): Promise<unknown> {
239
+ export type PlatformMarketTrustPostBody = z.input<(typeof schemas)["platformMarketTrustPost"]["body"]>;
240
+ export type PlatformMarketTrustPostData = z.infer<(typeof schemas)["platformMarketTrustPost"]["data"]>;
241
+
242
+ export async function platformMarketTrustPost(body: PlatformMarketTrustPostBody): Promise<PlatformMarketTrustPostData> {
188
243
  const client = ensureReq();
189
244
  try {
190
- const env = await client.post(`_platform/market/trust`).json<OjEnvelope<unknown>>();
245
+ const env = await client.post(`_platform/market/trust`, { json: body }).json<OjEnvelope<PlatformMarketTrustPostData>>();
191
246
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
192
247
  if (typeof env.code === "number" && env.code !== 0)
193
248
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
194
- const data = env.data as unknown;
249
+ const data = env.data as PlatformMarketTrustPostData;
250
+ if (import.meta.env.DEV) {
251
+ const { schemas } = await import("./api.schemas");
252
+ const r = schemas["platformMarketTrustPost"].data.safeParse(data);
253
+ if (!r.success)
254
+ throw new ContractApiError(-1, `[契约违例] platformMarketTrustPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
255
+ }
195
256
  return data;
196
257
  }
197
258
  catch (e) {
@@ -199,14 +260,23 @@ export async function platformMarketTrustPost(): Promise<unknown> {
199
260
  }
200
261
  }
201
262
 
202
- export async function platformMarketTrustKeyPost(): Promise<unknown> {
263
+ export type PlatformMarketTrustKeyPostBody = z.input<(typeof schemas)["platformMarketTrustKeyPost"]["body"]>;
264
+ export type PlatformMarketTrustKeyPostData = z.infer<(typeof schemas)["platformMarketTrustKeyPost"]["data"]>;
265
+
266
+ export async function platformMarketTrustKeyPost(body: PlatformMarketTrustKeyPostBody): Promise<PlatformMarketTrustKeyPostData> {
203
267
  const client = ensureReq();
204
268
  try {
205
- const env = await client.post(`_platform/market/trust/key`).json<OjEnvelope<unknown>>();
269
+ const env = await client.post(`_platform/market/trust/key`, { json: body }).json<OjEnvelope<PlatformMarketTrustKeyPostData>>();
206
270
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
207
271
  if (typeof env.code === "number" && env.code !== 0)
208
272
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
209
- const data = env.data as unknown;
273
+ const data = env.data as PlatformMarketTrustKeyPostData;
274
+ if (import.meta.env.DEV) {
275
+ const { schemas } = await import("./api.schemas");
276
+ const r = schemas["platformMarketTrustKeyPost"].data.safeParse(data);
277
+ if (!r.success)
278
+ throw new ContractApiError(-1, `[契约违例] platformMarketTrustKeyPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
279
+ }
210
280
  return data;
211
281
  }
212
282
  catch (e) {
@@ -214,14 +284,23 @@ export async function platformMarketTrustKeyPost(): Promise<unknown> {
214
284
  }
215
285
  }
216
286
 
217
- export async function platformMarketTrustKeyDelete(): Promise<unknown> {
287
+ export type PlatformMarketTrustKeyDeleteQuery = z.input<(typeof schemas)["platformMarketTrustKeyDelete"]["query"]>;
288
+ export type PlatformMarketTrustKeyDeleteData = z.infer<(typeof schemas)["platformMarketTrustKeyDelete"]["data"]>;
289
+
290
+ export async function platformMarketTrustKeyDelete(query: PlatformMarketTrustKeyDeleteQuery): Promise<PlatformMarketTrustKeyDeleteData> {
218
291
  const client = ensureReq();
219
292
  try {
220
- const env = await client.delete(`_platform/market/trust/key`).json<OjEnvelope<unknown>>();
293
+ const env = await client.delete(`_platform/market/trust/key`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketTrustKeyDeleteData>>();
221
294
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
222
295
  if (typeof env.code === "number" && env.code !== 0)
223
296
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
224
- const data = env.data as unknown;
297
+ const data = env.data as PlatformMarketTrustKeyDeleteData;
298
+ if (import.meta.env.DEV) {
299
+ const { schemas } = await import("./api.schemas");
300
+ const r = schemas["platformMarketTrustKeyDelete"].data.safeParse(data);
301
+ if (!r.success)
302
+ throw new ContractApiError(-1, `[契约违例] platformMarketTrustKeyDelete 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
303
+ }
225
304
  return data;
226
305
  }
227
306
  catch (e) {
@@ -229,14 +308,22 @@ export async function platformMarketTrustKeyDelete(): Promise<unknown> {
229
308
  }
230
309
  }
231
310
 
232
- export async function platformMarketIndexGet(): Promise<unknown> {
311
+ export type PlatformMarketIndexGetData = z.infer<(typeof schemas)["platformMarketIndexGet"]["data"]>;
312
+
313
+ export async function platformMarketIndexGet(): Promise<PlatformMarketIndexGetData> {
233
314
  const client = ensureReq();
234
315
  try {
235
- const env = await client.get(`_platform/market/index`).json<OjEnvelope<unknown>>();
316
+ const env = await client.get(`_platform/market/index`).json<OjEnvelope<PlatformMarketIndexGetData>>();
236
317
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
237
318
  if (typeof env.code === "number" && env.code !== 0)
238
319
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
239
- const data = env.data as unknown;
320
+ const data = env.data as PlatformMarketIndexGetData;
321
+ if (import.meta.env.DEV) {
322
+ const { schemas } = await import("./api.schemas");
323
+ const r = schemas["platformMarketIndexGet"].data.safeParse(data);
324
+ if (!r.success)
325
+ throw new ContractApiError(-1, `[契约违例] platformMarketIndexGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
326
+ }
240
327
  return data;
241
328
  }
242
329
  catch (e) {
@@ -244,14 +331,23 @@ export async function platformMarketIndexGet(): Promise<unknown> {
244
331
  }
245
332
  }
246
333
 
247
- export async function platformMarketUninstallImpactGet(): Promise<unknown> {
334
+ export type PlatformMarketUninstallImpactGetQuery = z.input<(typeof schemas)["platformMarketUninstallImpactGet"]["query"]>;
335
+ export type PlatformMarketUninstallImpactGetData = z.infer<(typeof schemas)["platformMarketUninstallImpactGet"]["data"]>;
336
+
337
+ export async function platformMarketUninstallImpactGet(query: PlatformMarketUninstallImpactGetQuery): Promise<PlatformMarketUninstallImpactGetData> {
248
338
  const client = ensureReq();
249
339
  try {
250
- const env = await client.get(`_platform/market/uninstall-impact`).json<OjEnvelope<unknown>>();
340
+ const env = await client.get(`_platform/market/uninstall-impact`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketUninstallImpactGetData>>();
251
341
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
252
342
  if (typeof env.code === "number" && env.code !== 0)
253
343
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
254
- const data = env.data as unknown;
344
+ const data = env.data as PlatformMarketUninstallImpactGetData;
345
+ if (import.meta.env.DEV) {
346
+ const { schemas } = await import("./api.schemas");
347
+ const r = schemas["platformMarketUninstallImpactGet"].data.safeParse(data);
348
+ if (!r.success)
349
+ throw new ContractApiError(-1, `[契约违例] platformMarketUninstallImpactGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
350
+ }
255
351
  return data;
256
352
  }
257
353
  catch (e) {
@@ -259,14 +355,23 @@ export async function platformMarketUninstallImpactGet(): Promise<unknown> {
259
355
  }
260
356
  }
261
357
 
262
- export async function platformMarketUninstallPost(): Promise<unknown> {
358
+ export type PlatformMarketUninstallPostBody = z.input<(typeof schemas)["platformMarketUninstallPost"]["body"]>;
359
+ export type PlatformMarketUninstallPostData = z.infer<(typeof schemas)["platformMarketUninstallPost"]["data"]>;
360
+
361
+ export async function platformMarketUninstallPost(body: PlatformMarketUninstallPostBody): Promise<PlatformMarketUninstallPostData> {
263
362
  const client = ensureReq();
264
363
  try {
265
- const env = await client.post(`_platform/market/uninstall`).json<OjEnvelope<unknown>>();
364
+ const env = await client.post(`_platform/market/uninstall`, { json: body }).json<OjEnvelope<PlatformMarketUninstallPostData>>();
266
365
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
267
366
  if (typeof env.code === "number" && env.code !== 0)
268
367
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
269
- const data = env.data as unknown;
368
+ const data = env.data as PlatformMarketUninstallPostData;
369
+ if (import.meta.env.DEV) {
370
+ const { schemas } = await import("./api.schemas");
371
+ const r = schemas["platformMarketUninstallPost"].data.safeParse(data);
372
+ if (!r.success)
373
+ throw new ContractApiError(-1, `[契约违例] platformMarketUninstallPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
374
+ }
270
375
  return data;
271
376
  }
272
377
  catch (e) {
@@ -274,14 +379,22 @@ export async function platformMarketUninstallPost(): Promise<unknown> {
274
379
  }
275
380
  }
276
381
 
277
- export async function platformMarketHistoryGet(): Promise<unknown> {
382
+ export type PlatformMarketHistoryGetData = z.infer<(typeof schemas)["platformMarketHistoryGet"]["data"]>;
383
+
384
+ export async function platformMarketHistoryGet(): Promise<PlatformMarketHistoryGetData> {
278
385
  const client = ensureReq();
279
386
  try {
280
- const env = await client.get(`_platform/market/history`).json<OjEnvelope<unknown>>();
387
+ const env = await client.get(`_platform/market/history`).json<OjEnvelope<PlatformMarketHistoryGetData>>();
281
388
  // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
282
389
  if (typeof env.code === "number" && env.code !== 0)
283
390
  throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
284
- const data = env.data as unknown;
391
+ const data = env.data as PlatformMarketHistoryGetData;
392
+ if (import.meta.env.DEV) {
393
+ const { schemas } = await import("./api.schemas");
394
+ const r = schemas["platformMarketHistoryGet"].data.safeParse(data);
395
+ if (!r.success)
396
+ throw new ContractApiError(-1, `[契约违例] platformMarketHistoryGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
397
+ }
285
398
  return data;
286
399
  }
287
400
  catch (e) {
@@ -73,6 +73,25 @@ describe("cardGrid 卡片网格(BDD)", () => {
73
73
  expect(handlers.onRollback).toHaveBeenCalledWith(installed);
74
74
  });
75
75
 
76
+ it("given canManage=false(非 superadmin)When 渲染 Then 安装/启停/回滚/卸载按钮禁用、详情/历史保持可用", () => {
77
+ renderWithProviders(
78
+ <CardGrid rows={[installed]} canManage={false} {...handlers} />,
79
+ new InMemoryMarketClient(),
80
+ );
81
+ const btn = (name: string) => screen.getByRole("button", { name }) as HTMLButtonElement;
82
+ // 写操作禁用
83
+ expect(btn("升级").disabled).toBe(true);
84
+ expect(btn("停用").disabled).toBe(true);
85
+ expect(btn("卸载").disabled).toBe(true);
86
+ expect((screen.getByTestId("card-rollback-alpha") as HTMLButtonElement).disabled).toBe(true);
87
+ // 只读操作不受影响
88
+ expect(btn("详情").disabled).toBe(false);
89
+ expect(btn("历史").disabled).toBe(false);
90
+ // 禁用时不应触发写回调
91
+ btn("卸载").click();
92
+ expect(handlers.onUninstall).not.toHaveBeenCalled();
93
+ });
94
+
76
95
  it("given 空列表 When 渲染 Then 显示空态", () => {
77
96
  renderWithProviders(<CardGrid rows={[]} {...handlers} />, new InMemoryMarketClient());
78
97
  expect(screen.getByTestId("card-grid-empty")).toBeTruthy();
@@ -18,6 +18,7 @@ describe("installDialog 安装进度(BDD)", () => {
18
18
  progress: 100,
19
19
  message: "ok",
20
20
  stages: [],
21
+ startedAt: 0,
21
22
  };
22
23
  const client = new InMemoryMarketClient({ taskRecord: done, installResult: { task_id: "t1" } });
23
24
  renderWithProviders(<InstallDialog row={row} open onClose={() => {}} />, client);
@@ -37,6 +38,7 @@ describe("installDialog 安装进度(BDD)", () => {
37
38
  message: "verify failed",
38
39
  stages: [{ stage: "verifying", ok: false, message: "bad hash" }],
39
40
  error: { reason: "包校验失败", retryable: false, hintKey: "lifecycle.stage.verifying.hint" },
41
+ startedAt: 0,
40
42
  };
41
43
  const client = new InMemoryMarketClient({ taskRecord: failed, installResult: { task_id: "t1" } });
42
44
  renderWithProviders(<InstallDialog row={row} open onClose={() => {}} />, client);
@@ -54,6 +56,7 @@ describe("installDialog 安装进度(BDD)", () => {
54
56
  progress: 100,
55
57
  message: "ok",
56
58
  stages: [],
59
+ startedAt: 0,
57
60
  };
58
61
  const client = new InMemoryMarketClient({ taskRecord: done, installResult: { task_id: "t1" } });
59
62
  const rollbackRow: IndexRow = { name: "alpha", latest: "1.2.0", installed: "1.2.0", enabled: true, upgradable: false };
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest";
4
4
  import { RollbackDialog } from "../components/RollbackDialog";
5
5
  import { InMemoryMarketClient } from "./marketClient.fake";
6
6
  import { renderWithProviders } from "./render";
7
+ import "@testing-library/jest-dom/vitest";
7
8
 
8
9
  const row: IndexRow = { name: "alpha", latest: "1.2.0", installed: "1.2.0", enabled: true, upgradable: false };
9
10
 
@@ -45,6 +45,7 @@ export class InMemoryMarketClient implements MarketClient {
45
45
  progress: 100,
46
46
  message: "ok",
47
47
  stages: [],
48
+ startedAt: 0,
48
49
  } as TaskRecord);
49
50
  }
50
51
 
@@ -1,6 +1,9 @@
1
- // 市场 API 客户端(DIP):定义与宿主 _platform 编排面 + market_history 端点的契约。
2
- // 实现与 ky 形态解耦——组件/hooks 注入 MarketClient,测试可注入内存 fake(见 __tests__)。
1
+ // 市场 API 客户端契约(DIP seams):组件/hooks 依赖此接口,而非具体 http 实现;
2
+ // 测试可注入内存 fake(见 __tests__/marketClient.fake.ts)。
3
3
  // 第一方专用调用面(R29):仅市场应用消费 /api/_platform/market/*,第三方方案不得仿照。
4
+ //
5
+ // 实现见同目录 defaultClient.ts——统一委托给 ojm 从契约生成的 __client,消除手写
6
+ // URL/信封解包与契约漂移(DEV 下生成 client 还会对响应做 zod 校验)。
4
7
 
5
8
  import type {
6
9
  EnableInput,
@@ -11,27 +14,6 @@ import type {
11
14
  UninstallImpact,
12
15
  } from "../types";
13
16
 
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
17
  /** 市场客户端契约(组件/hooks 依赖此接口,而非具体 http 实现) */
36
18
  export interface MarketClient {
37
19
  /** 登录即可:{ apiVersion }(peerPlatform 预检口径) */
@@ -51,85 +33,3 @@ export interface MarketClient {
51
33
  /** 卸载(P1-15a 补齐后接通) */
52
34
  uninstall: (name: string) => Promise<{ ok: boolean, message: string }>
53
35
  }
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
- }