@oj-module/cli 0.2.68 → 0.2.71

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 (153) hide show
  1. package/devkit/README.md +10 -8
  2. package/devkit/framework-dev.md +1 -1
  3. package/devkit/manual.md +45 -19
  4. package/devkit/module-dev-and-cdn-playground.md +20 -14
  5. package/devkit/publish-walkthrough.md +132 -43
  6. package/devkit/wecom-customer-reauth-guide.md +110 -0
  7. package/package.json +2 -2
  8. package/shell-dist/assets/{runtime.1xzahckt10.js → runtime.7x99hmm020.js} +2 -2
  9. package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
  10. package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
  11. package/shell-dist/index.html +4 -4
  12. package/shell-dist/versions.json +3 -3
  13. package/src/__tests__/list.test.ts +7 -7
  14. package/src/__tests__/oj-health-host.test.ts +19 -0
  15. package/src/__tests__/site-tenant-header.test.ts +2 -2
  16. package/src/__tests__/templates-builtin-list.test.ts +47 -0
  17. package/src/__tests__/templates-schema-columns.test.ts +11 -2
  18. package/src/__tests__/templates-tenant-scope.test.ts +2 -3
  19. package/src/__tests__/users-roles-display.test.ts +84 -0
  20. package/src/bundle.ts +1 -1
  21. package/src/contract/__tests__/market-client.integration.test.ts +23 -22
  22. package/src/enable.ts +2 -2
  23. package/src/init.ts +45 -0
  24. package/src/install.ts +5 -5
  25. package/src/lifecycle/__tests__/install.test.ts +29 -29
  26. package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
  27. package/src/lifecycle/__tests__/publish.test.ts +8 -10
  28. package/src/lifecycle/__tests__/trust.test.ts +14 -14
  29. package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
  30. package/src/list.ts +3 -3
  31. package/src/market/__tests__/bootstrap.test.ts +24 -7
  32. package/src/market/__tests__/failure-playbook.test.ts +0 -1
  33. package/src/market/__tests__/remote-push.test.ts +228 -0
  34. package/src/market/__tests__/verify.test.ts +45 -207
  35. package/src/market/bootstrap.ts +12 -7
  36. package/src/market/command.ts +33 -35
  37. package/src/market/db-seed.ts +129 -0
  38. package/src/market/failure-playbook.ts +3 -9
  39. package/src/market/fixture.ts +2 -6
  40. package/src/market/publish.ts +145 -89
  41. package/src/market/remote-push.ts +236 -0
  42. package/src/market/trial.ts +2 -2
  43. package/src/market/verify.ts +17 -270
  44. package/src/market/walkthrough.ts +38 -63
  45. package/src/oj.ts +13 -2
  46. package/src/preview.ts +5 -3
  47. package/src/publish.ts +3 -2
  48. package/src/scaffold-guard.ts +4 -0
  49. package/src/site.ts +1 -1
  50. package/src/trust.ts +8 -8
  51. package/src/uninstall.ts +2 -2
  52. package/src/usage.ts +23 -21
  53. package/templates/config.yaml +22 -3
  54. package/templates/global.d.ts +6 -4
  55. package/templates/init.include.yaml +16 -11
  56. package/templates/openapi.json +3971 -3663
  57. package/templates/src/.ojm-builtin.json +13 -0
  58. package/templates/src/README.md +5 -2
  59. package/templates/src/app/api/_platform/README.md +3 -2
  60. package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
  61. package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
  62. package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
  63. package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
  64. package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
  65. package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
  66. package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
  67. package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
  68. package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
  69. package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
  70. package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
  71. package/templates/src/app/api/_platform/_shared/service/domain/users.ts +7 -1
  72. package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
  73. package/templates/src/app/api/_platform/manifest.yaml +1 -2
  74. package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
  75. package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
  76. package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
  77. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
  78. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
  79. package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
  80. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
  81. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
  82. package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
  83. package/templates/src/app/api/_platform/schema.yaml +8 -11
  84. package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
  85. package/templates/src/app/api/_platform/seed.pg.sql +1 -1
  86. package/templates/src/app/api/_platform/seed.sql +1 -1
  87. package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
  88. package/templates/src/app/api/system/users/api.ts +3 -0
  89. package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -113
  90. package/templates/src/app/web/_platform/__client/index.ts +1 -333
  91. package/templates/src/app/web/system/__client/api.schemas.ts +2 -0
  92. package/templates/src/app/web/system/locales/en-US.json +15 -1
  93. package/templates/src/app/web/system/locales/zh-CN.json +15 -1
  94. package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
  95. package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
  96. package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
  97. package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
  98. package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
  99. package/templates/src/app/web/system/pages/users/index.tsx +12 -2
  100. package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
  101. package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
  102. package/templates/src/app/web/system/routes.ts +18 -1
  103. package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
  104. package/templates/src/oj-market/api/backfill.ts +221 -0
  105. package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
  106. package/templates/src/oj-market/api/cdn/api.ts +312 -0
  107. package/templates/src/oj-market/api/cdn/shared.ts +104 -0
  108. package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
  109. package/templates/src/oj-market/api/manifest.yaml +8 -0
  110. package/templates/src/oj-market/solution.yaml +21 -0
  111. package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
  112. package/templates/src/oj-market/web/__client/index.ts +509 -0
  113. package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
  114. package/templates/src/oj-market/web/api/client.ts +6 -6
  115. package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
  116. package/templates/src/oj-market/web/entry.ts +5 -3
  117. package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
  118. package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
  119. package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
  120. package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
  121. package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
  122. package/templates/src/oj-market/web/routes.ts +12 -0
  123. package/templates/src/oj-market/web/types.ts +14 -8
  124. package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
  125. package/templates/tests/lifecycle/backfill.spec.ts +392 -0
  126. package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
  127. package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
  128. package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
  129. package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
  130. package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
  131. package/templates/tests/lifecycle/installer.spec.ts +36 -0
  132. package/templates/tests/lifecycle/market-api.spec.ts +448 -803
  133. package/templates/tests/lifecycle/preflight.spec.ts +18 -0
  134. package/templates/tests/lifecycle/trust.spec.ts +2 -2
  135. package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
  136. package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
  137. package/templates/tests/rbac-users.spec.ts +8 -0
  138. package/templates/tests/solutions-entry.spec.ts +55 -0
  139. package/templates/tsconfig.paths.json +1 -0
  140. package/templates/types/stream-probes/stream-handlers.ts +34 -0
  141. package/templates/web.config.ts +6 -6
  142. package/vendor/host-versions.json +3 -3
  143. package/bin/oj +0 -4
  144. package/src/market/__tests__/build-index.test.ts +0 -641
  145. package/src/market/__tests__/sharding.test.ts +0 -90
  146. package/src/market/__tests__/yank-ledger.test.ts +0 -60
  147. package/src/market/build-index.ts +0 -526
  148. package/src/market/index-builder.ts +0 -149
  149. package/src/market/sharding.ts +0 -94
  150. package/src/market/yank-ledger.ts +0 -91
  151. package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
  152. package/templates/src/app/api/_platform/market/index-source.ts +0 -112
  153. package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
@@ -36,7 +36,7 @@ export function create_platformClient(ctx: ModuleContext) {
36
36
  }
37
37
  ctx.register.apiPrefix(API_PREFIX);
38
38
  bindRequest(ctx.utils.request);
39
- return { platformContactsGet, platformContactsDetailGet, platformMarketInstallPost, platformMarketInstallCancelPost, platformMarketTaskGet, platformMarketEnablePost, platformMarketMetaGet, platformMarketTrustGet, platformMarketTrustPost, platformMarketTrustKeyPost, platformMarketTrustKeyDelete, platformMarketIndexGet, platformMarketUninstallImpactGet, platformMarketUninstallPost, platformMarketHistoryGet, platformMarketReleasesGet, platformOrgDepartmentsGet, platformOrgIsMemberGet, platformOrgMembersGet, platformOrgPickerGet, platformOrgResolvePost, platformOrgTreeGet, platformOrgUserGet, platformSolutionsRoutesGet, platformSolutionsVerifyPost, platformSolutionsVerifyLastGet };
39
+ return { platformContactsGet, platformContactsDetailGet, platformOrgDepartmentsGet, platformOrgIsMemberGet, platformOrgMembersGet, platformOrgPickerGet, platformOrgResolvePost, platformOrgTreeGet, platformOrgUserGet, platformSolutionsRoutesGet, platformSolutionsVerifyPost, platformSolutionsVerifyLastGet };
40
40
  }
41
41
 
42
42
  /**
@@ -115,338 +115,6 @@ export async function platformContactsDetailGet(query: PlatformContactsDetailGet
115
115
  }
116
116
  }
117
117
 
118
- export type PlatformMarketInstallPostBody = z.input<(typeof schemas)["platformMarketInstallPost"]["body"]>;
119
- export type PlatformMarketInstallPostData = z.infer<(typeof schemas)["platformMarketInstallPost"]["data"]>;
120
-
121
- export async function platformMarketInstallPost(body: PlatformMarketInstallPostBody): Promise<PlatformMarketInstallPostData> {
122
- const client = ensureReq();
123
- try {
124
- const env = await client.post(`_platform/market/install`, { json: body }).json<OjEnvelope<PlatformMarketInstallPostData>>();
125
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
126
- if (typeof env.code === "number" && env.code !== 0)
127
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
128
- const data = env.data as PlatformMarketInstallPostData;
129
- if (import.meta.env.DEV) {
130
- const { schemas } = await import("./api.schemas");
131
- const r = schemas["platformMarketInstallPost"].data.safeParse(data);
132
- if (!r.success)
133
- throw new ContractApiError(-1, `[契约违例] platformMarketInstallPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
134
- }
135
- return data;
136
- }
137
- catch (e) {
138
- throw await toApiError(e);
139
- }
140
- }
141
-
142
- export type PlatformMarketInstallCancelPostBody = z.input<(typeof schemas)["platformMarketInstallCancelPost"]["body"]>;
143
- export type PlatformMarketInstallCancelPostData = z.infer<(typeof schemas)["platformMarketInstallCancelPost"]["data"]>;
144
-
145
- export async function platformMarketInstallCancelPost(body: PlatformMarketInstallCancelPostBody): Promise<PlatformMarketInstallCancelPostData> {
146
- const client = ensureReq();
147
- try {
148
- const env = await client.post(`_platform/market/install/cancel`, { json: body }).json<OjEnvelope<PlatformMarketInstallCancelPostData>>();
149
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
150
- if (typeof env.code === "number" && env.code !== 0)
151
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
152
- const data = env.data as PlatformMarketInstallCancelPostData;
153
- if (import.meta.env.DEV) {
154
- const { schemas } = await import("./api.schemas");
155
- const r = schemas["platformMarketInstallCancelPost"].data.safeParse(data);
156
- if (!r.success)
157
- throw new ContractApiError(-1, `[契约违例] platformMarketInstallCancelPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
158
- }
159
- return data;
160
- }
161
- catch (e) {
162
- throw await toApiError(e);
163
- }
164
- }
165
-
166
- export type PlatformMarketTaskGetQuery = z.input<(typeof schemas)["platformMarketTaskGet"]["query"]>;
167
- export type PlatformMarketTaskGetData = z.infer<(typeof schemas)["platformMarketTaskGet"]["data"]>;
168
-
169
- export async function platformMarketTaskGet(query: PlatformMarketTaskGetQuery): Promise<PlatformMarketTaskGetData> {
170
- const client = ensureReq();
171
- try {
172
- const env = await client.get(`_platform/market/task`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketTaskGetData>>();
173
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
174
- if (typeof env.code === "number" && env.code !== 0)
175
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
176
- const data = env.data as PlatformMarketTaskGetData;
177
- if (import.meta.env.DEV) {
178
- const { schemas } = await import("./api.schemas");
179
- const r = schemas["platformMarketTaskGet"].data.safeParse(data);
180
- if (!r.success)
181
- throw new ContractApiError(-1, `[契约违例] platformMarketTaskGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
182
- }
183
- return data;
184
- }
185
- catch (e) {
186
- throw await toApiError(e);
187
- }
188
- }
189
-
190
- export type PlatformMarketEnablePostBody = z.input<(typeof schemas)["platformMarketEnablePost"]["body"]>;
191
- export type PlatformMarketEnablePostData = z.infer<(typeof schemas)["platformMarketEnablePost"]["data"]>;
192
-
193
- export async function platformMarketEnablePost(body: PlatformMarketEnablePostBody): Promise<PlatformMarketEnablePostData> {
194
- const client = ensureReq();
195
- try {
196
- const env = await client.post(`_platform/market/enable`, { json: body }).json<OjEnvelope<PlatformMarketEnablePostData>>();
197
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
198
- if (typeof env.code === "number" && env.code !== 0)
199
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
200
- const data = env.data as PlatformMarketEnablePostData;
201
- if (import.meta.env.DEV) {
202
- const { schemas } = await import("./api.schemas");
203
- const r = schemas["platformMarketEnablePost"].data.safeParse(data);
204
- if (!r.success)
205
- throw new ContractApiError(-1, `[契约违例] platformMarketEnablePost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
206
- }
207
- return data;
208
- }
209
- catch (e) {
210
- throw await toApiError(e);
211
- }
212
- }
213
-
214
- export type PlatformMarketMetaGetData = z.infer<(typeof schemas)["platformMarketMetaGet"]["data"]>;
215
-
216
- export async function platformMarketMetaGet(): Promise<PlatformMarketMetaGetData> {
217
- const client = ensureReq();
218
- try {
219
- const env = await client.get(`_platform/market/meta`).json<OjEnvelope<PlatformMarketMetaGetData>>();
220
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
221
- if (typeof env.code === "number" && env.code !== 0)
222
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
223
- const data = env.data as PlatformMarketMetaGetData;
224
- if (import.meta.env.DEV) {
225
- const { schemas } = await import("./api.schemas");
226
- const r = schemas["platformMarketMetaGet"].data.safeParse(data);
227
- if (!r.success)
228
- throw new ContractApiError(-1, `[契约违例] platformMarketMetaGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
229
- }
230
- return data;
231
- }
232
- catch (e) {
233
- throw await toApiError(e);
234
- }
235
- }
236
-
237
- export type PlatformMarketTrustGetData = z.infer<(typeof schemas)["platformMarketTrustGet"]["data"]>;
238
-
239
- export async function platformMarketTrustGet(): Promise<PlatformMarketTrustGetData> {
240
- const client = ensureReq();
241
- try {
242
- const env = await client.get(`_platform/market/trust`).json<OjEnvelope<PlatformMarketTrustGetData>>();
243
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
244
- if (typeof env.code === "number" && env.code !== 0)
245
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
246
- const data = env.data as PlatformMarketTrustGetData;
247
- if (import.meta.env.DEV) {
248
- const { schemas } = await import("./api.schemas");
249
- const r = schemas["platformMarketTrustGet"].data.safeParse(data);
250
- if (!r.success)
251
- throw new ContractApiError(-1, `[契约违例] platformMarketTrustGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
252
- }
253
- return data;
254
- }
255
- catch (e) {
256
- throw await toApiError(e);
257
- }
258
- }
259
-
260
- export type PlatformMarketTrustPostBody = z.input<(typeof schemas)["platformMarketTrustPost"]["body"]>;
261
- export type PlatformMarketTrustPostData = z.infer<(typeof schemas)["platformMarketTrustPost"]["data"]>;
262
-
263
- export async function platformMarketTrustPost(body: PlatformMarketTrustPostBody): Promise<PlatformMarketTrustPostData> {
264
- const client = ensureReq();
265
- try {
266
- const env = await client.post(`_platform/market/trust`, { json: body }).json<OjEnvelope<PlatformMarketTrustPostData>>();
267
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
268
- if (typeof env.code === "number" && env.code !== 0)
269
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
270
- const data = env.data as PlatformMarketTrustPostData;
271
- if (import.meta.env.DEV) {
272
- const { schemas } = await import("./api.schemas");
273
- const r = schemas["platformMarketTrustPost"].data.safeParse(data);
274
- if (!r.success)
275
- throw new ContractApiError(-1, `[契约违例] platformMarketTrustPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
276
- }
277
- return data;
278
- }
279
- catch (e) {
280
- throw await toApiError(e);
281
- }
282
- }
283
-
284
- export type PlatformMarketTrustKeyPostBody = z.input<(typeof schemas)["platformMarketTrustKeyPost"]["body"]>;
285
- export type PlatformMarketTrustKeyPostData = z.infer<(typeof schemas)["platformMarketTrustKeyPost"]["data"]>;
286
-
287
- export async function platformMarketTrustKeyPost(body: PlatformMarketTrustKeyPostBody): Promise<PlatformMarketTrustKeyPostData> {
288
- const client = ensureReq();
289
- try {
290
- const env = await client.post(`_platform/market/trust/key`, { json: body }).json<OjEnvelope<PlatformMarketTrustKeyPostData>>();
291
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
292
- if (typeof env.code === "number" && env.code !== 0)
293
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
294
- const data = env.data as PlatformMarketTrustKeyPostData;
295
- if (import.meta.env.DEV) {
296
- const { schemas } = await import("./api.schemas");
297
- const r = schemas["platformMarketTrustKeyPost"].data.safeParse(data);
298
- if (!r.success)
299
- throw new ContractApiError(-1, `[契约违例] platformMarketTrustKeyPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
300
- }
301
- return data;
302
- }
303
- catch (e) {
304
- throw await toApiError(e);
305
- }
306
- }
307
-
308
- export type PlatformMarketTrustKeyDeleteQuery = z.input<(typeof schemas)["platformMarketTrustKeyDelete"]["query"]>;
309
- export type PlatformMarketTrustKeyDeleteData = z.infer<(typeof schemas)["platformMarketTrustKeyDelete"]["data"]>;
310
-
311
- export async function platformMarketTrustKeyDelete(query: PlatformMarketTrustKeyDeleteQuery): Promise<PlatformMarketTrustKeyDeleteData> {
312
- const client = ensureReq();
313
- try {
314
- const env = await client.delete(`_platform/market/trust/key`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketTrustKeyDeleteData>>();
315
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
316
- if (typeof env.code === "number" && env.code !== 0)
317
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
318
- const data = env.data as PlatformMarketTrustKeyDeleteData;
319
- if (import.meta.env.DEV) {
320
- const { schemas } = await import("./api.schemas");
321
- const r = schemas["platformMarketTrustKeyDelete"].data.safeParse(data);
322
- if (!r.success)
323
- throw new ContractApiError(-1, `[契约违例] platformMarketTrustKeyDelete 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
324
- }
325
- return data;
326
- }
327
- catch (e) {
328
- throw await toApiError(e);
329
- }
330
- }
331
-
332
- export type PlatformMarketIndexGetData = z.infer<(typeof schemas)["platformMarketIndexGet"]["data"]>;
333
-
334
- export async function platformMarketIndexGet(): Promise<PlatformMarketIndexGetData> {
335
- const client = ensureReq();
336
- try {
337
- const env = await client.get(`_platform/market/index`).json<OjEnvelope<PlatformMarketIndexGetData>>();
338
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
339
- if (typeof env.code === "number" && env.code !== 0)
340
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
341
- const data = env.data as PlatformMarketIndexGetData;
342
- if (import.meta.env.DEV) {
343
- const { schemas } = await import("./api.schemas");
344
- const r = schemas["platformMarketIndexGet"].data.safeParse(data);
345
- if (!r.success)
346
- throw new ContractApiError(-1, `[契约违例] platformMarketIndexGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
347
- }
348
- return data;
349
- }
350
- catch (e) {
351
- throw await toApiError(e);
352
- }
353
- }
354
-
355
- export type PlatformMarketUninstallImpactGetQuery = z.input<(typeof schemas)["platformMarketUninstallImpactGet"]["query"]>;
356
- export type PlatformMarketUninstallImpactGetData = z.infer<(typeof schemas)["platformMarketUninstallImpactGet"]["data"]>;
357
-
358
- export async function platformMarketUninstallImpactGet(query: PlatformMarketUninstallImpactGetQuery): Promise<PlatformMarketUninstallImpactGetData> {
359
- const client = ensureReq();
360
- try {
361
- const env = await client.get(`_platform/market/uninstall-impact`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketUninstallImpactGetData>>();
362
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
363
- if (typeof env.code === "number" && env.code !== 0)
364
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
365
- const data = env.data as PlatformMarketUninstallImpactGetData;
366
- if (import.meta.env.DEV) {
367
- const { schemas } = await import("./api.schemas");
368
- const r = schemas["platformMarketUninstallImpactGet"].data.safeParse(data);
369
- if (!r.success)
370
- throw new ContractApiError(-1, `[契约违例] platformMarketUninstallImpactGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
371
- }
372
- return data;
373
- }
374
- catch (e) {
375
- throw await toApiError(e);
376
- }
377
- }
378
-
379
- export type PlatformMarketUninstallPostBody = z.input<(typeof schemas)["platformMarketUninstallPost"]["body"]>;
380
- export type PlatformMarketUninstallPostData = z.infer<(typeof schemas)["platformMarketUninstallPost"]["data"]>;
381
-
382
- export async function platformMarketUninstallPost(body: PlatformMarketUninstallPostBody): Promise<PlatformMarketUninstallPostData> {
383
- const client = ensureReq();
384
- try {
385
- const env = await client.post(`_platform/market/uninstall`, { json: body }).json<OjEnvelope<PlatformMarketUninstallPostData>>();
386
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
387
- if (typeof env.code === "number" && env.code !== 0)
388
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
389
- const data = env.data as PlatformMarketUninstallPostData;
390
- if (import.meta.env.DEV) {
391
- const { schemas } = await import("./api.schemas");
392
- const r = schemas["platformMarketUninstallPost"].data.safeParse(data);
393
- if (!r.success)
394
- throw new ContractApiError(-1, `[契约违例] platformMarketUninstallPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
395
- }
396
- return data;
397
- }
398
- catch (e) {
399
- throw await toApiError(e);
400
- }
401
- }
402
-
403
- export type PlatformMarketHistoryGetData = z.infer<(typeof schemas)["platformMarketHistoryGet"]["data"]>;
404
-
405
- export async function platformMarketHistoryGet(): Promise<PlatformMarketHistoryGetData> {
406
- const client = ensureReq();
407
- try {
408
- const env = await client.get(`_platform/market/history`).json<OjEnvelope<PlatformMarketHistoryGetData>>();
409
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
410
- if (typeof env.code === "number" && env.code !== 0)
411
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
412
- const data = env.data as PlatformMarketHistoryGetData;
413
- if (import.meta.env.DEV) {
414
- const { schemas } = await import("./api.schemas");
415
- const r = schemas["platformMarketHistoryGet"].data.safeParse(data);
416
- if (!r.success)
417
- throw new ContractApiError(-1, `[契约违例] platformMarketHistoryGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
418
- }
419
- return data;
420
- }
421
- catch (e) {
422
- throw await toApiError(e);
423
- }
424
- }
425
-
426
- export type PlatformMarketReleasesGetQuery = z.input<(typeof schemas)["platformMarketReleasesGet"]["query"]>;
427
- export type PlatformMarketReleasesGetData = z.infer<(typeof schemas)["platformMarketReleasesGet"]["data"]>;
428
-
429
- export async function platformMarketReleasesGet(query: PlatformMarketReleasesGetQuery): Promise<PlatformMarketReleasesGetData> {
430
- const client = ensureReq();
431
- try {
432
- const env = await client.get(`_platform/market/releases`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<PlatformMarketReleasesGetData>>();
433
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
434
- if (typeof env.code === "number" && env.code !== 0)
435
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
436
- const data = env.data as PlatformMarketReleasesGetData;
437
- if (import.meta.env.DEV) {
438
- const { schemas } = await import("./api.schemas");
439
- const r = schemas["platformMarketReleasesGet"].data.safeParse(data);
440
- if (!r.success)
441
- throw new ContractApiError(-1, `[契约违例] platformMarketReleasesGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
442
- }
443
- return data;
444
- }
445
- catch (e) {
446
- throw await toApiError(e);
447
- }
448
- }
449
-
450
118
  export type PlatformOrgDepartmentsGetQuery = z.input<(typeof schemas)["platformOrgDepartmentsGet"]["query"]>;
451
119
  export type PlatformOrgDepartmentsGetData = z.infer<(typeof schemas)["platformOrgDepartmentsGet"]["data"]>;
452
120
 
@@ -242,6 +242,8 @@ export const schemas = {
242
242
  "display_name": z.string().nullable(),
243
243
  "email": z.string().nullable(),
244
244
  "position": z.string().nullable(),
245
+ "employee_no": z.string().nullable().optional(),
246
+ "phone_number": z.string().nullable().optional(),
245
247
  "departments": z.array(z.object({
246
248
  "id": z.number(),
247
249
  "name": z.string().nullable(),
@@ -7,7 +7,8 @@
7
7
  "oplog": "Audit Log",
8
8
  "settings": "Site Settings",
9
9
  "wecom": "WeCom Integration",
10
- "departments": "Departments"
10
+ "departments": "Departments",
11
+ "solutions": "Solutions"
11
12
  },
12
13
  "common": {
13
14
  "confirm": "Confirm",
@@ -40,6 +41,7 @@
40
41
  },
41
42
  "mainDept": "Main dept",
42
43
  "filterDept": "Filter by department (incl. sub-depts)",
44
+ "searchPlaceholder": "Search by username / display name",
43
45
  "password": "Password",
44
46
  "usernameHint": "ldap:/ sso:/ wecom: prefixes are reserved for external identity sources"
45
47
  },
@@ -129,6 +131,18 @@
129
131
  "secretHint": "Secret items cannot be edited online",
130
132
  "saved": "Settings saved"
131
133
  },
134
+ "solutionsPage": {
135
+ "title": "Solution Market (oj-market)",
136
+ "absentTitle": "No oj-market package in the site catalog",
137
+ "absentHint": "Factory state: re-run `ojm init --yes` to fetch the bundled installer package, or have a superadmin trigger POST /api/oj-market/backfill to rebuild the local catalog, then refresh this page.",
138
+ "latest": "Latest in catalog: {{version}}",
139
+ "installed": "Installed {{version}}",
140
+ "install": "Install",
141
+ "doneTitle": "Installed",
142
+ "goMarket": "Open Solution Market",
143
+ "upgradableHint": "A newer version is in the catalog — upgrade from within the market page.",
144
+ "needsTrust": "Publisher key awaits trust confirmation — first-party packages are pre-trusted, so this usually means a third-party package occupies the name. Verify and retry."
145
+ },
132
146
  "wecom": {
133
147
  "title": "Onboard a corporation",
134
148
  "hint": "Click \"Generate install link\" and send the link or QR code to the corporation's admin to scan/open for approval. On success this system auto-registers the corporation via the create_auth callback (writes wecom_app_profile). The link is generated by WeCom get_customized_auth_url; its validity follows WeCom's response (up to 10 days). The QR code greys out when it expires, so regenerate it.",
@@ -7,7 +7,8 @@
7
7
  "oplog": "操作日志",
8
8
  "settings": "站点设置",
9
9
  "wecom": "企微集成",
10
- "departments": "组织架构"
10
+ "departments": "组织架构",
11
+ "solutions": "方案安装"
11
12
  },
12
13
  "common": {
13
14
  "confirm": "确认",
@@ -40,6 +41,7 @@
40
41
  },
41
42
  "mainDept": "主部门",
42
43
  "filterDept": "按部门筛选(含子部门)",
44
+ "searchPlaceholder": "按用户名/显示名搜索",
43
45
  "password": "口令",
44
46
  "usernameHint": "ldap:/ sso:/ wecom: 为外部身份源保留"
45
47
  },
@@ -129,6 +131,18 @@
129
131
  "secretHint": "敏感项不可在线修改",
130
132
  "saved": "设置已保存"
131
133
  },
134
+ "solutionsPage": {
135
+ "title": "方案市场(oj-market)",
136
+ "absentTitle": "站点目录中暂无方案市场安装包",
137
+ "absentHint": "出厂未装态:请重新执行 ojm init --yes 补充随站分发的安装包,或由超管触发 POST /api/oj-market/backfill 回填本地目录后刷新本页。",
138
+ "latest": "目录最新版 {{version}}",
139
+ "installed": "已装 {{version}}",
140
+ "install": "安装",
141
+ "doneTitle": "安装完成",
142
+ "goMarket": "前往方案市场",
143
+ "upgradableHint": "目录有更新版本——经方案市场页内升级。",
144
+ "needsTrust": "发布方密钥待信任确认——首方包已预置平台信任,出现本态多为目录被第三方包占用,请核对后重试。"
145
+ },
132
146
  "wecom": {
133
147
  "title": "开通企业",
134
148
  "hint": "点「生成开通链接」把链接或二维码发给企业管理员,由其扫码/打开授权;授权成功后本系统经 create_auth 回调自动登记该企业(落库 wecom_app_profile)。链接由企微 get_customized_auth_url 生成,有效期以企微返回为准(可达 10 天),过期后二维码置灰,可重新生成。",
@@ -37,6 +37,12 @@ const KEY = ["system", "wecom-sync"] as const;
37
37
 
38
38
  /** 单片推完到下一次 start 的最小间隔:后端限速 10 次/分,串行驱动别自己撞 429 */
39
39
  const MIN_GAP_MS = 6000;
40
+ /**
41
+ * 同步请求超时:单条 `sync/start` 处理一片(预算 12s + 单次上游软超时 5s,最坏 ~17s),
42
+ * 远长于运行时 `request` 的默认 10s。不单独放宽,前端必然在「片还没推完」时先抛
43
+ * `Request timed out`。30s 与服务器默认路由超时对齐,留足余量。
44
+ */
45
+ const SYNC_TIMEOUT_MS = 30_000;
40
46
  /** 一轮点击最多推多少片(12 秒/片 ≈ 12 分钟):兜底防呆,正常企业两三次就到底 */
41
47
  const MAX_SLICES = 60;
42
48
 
@@ -61,21 +67,21 @@ function remember(view: SyncView): SyncView {
61
67
  }
62
68
 
63
69
  async function fetchStatus(): Promise<SyncView> {
64
- const env = await request.get(withAgent("auth/wecom/sync/status")).json<{ code: number, data?: SyncView }>();
70
+ const env = await request.get(withAgent("auth/wecom/sync/status"), { timeout: SYNC_TIMEOUT_MS }).json<{ code: number, data?: SyncView }>();
65
71
  if (!env.data)
66
72
  throw new Error("企业微信同步状态读取失败");
67
73
  return remember(env.data);
68
74
  }
69
75
 
70
76
  async function pushSlice(): Promise<SyncView> {
71
- const env = await request.post("auth/wecom/sync/start", { json: agentBody() }).json<{ code: number, data?: SyncView }>();
77
+ const env = await request.post("auth/wecom/sync/start", { json: agentBody(), timeout: SYNC_TIMEOUT_MS }).json<{ code: number, data?: SyncView }>();
72
78
  if (!env.data)
73
79
  throw new Error("企业微信同步未能启动");
74
80
  return remember(env.data);
75
81
  }
76
82
 
77
83
  async function cancelSync(): Promise<SyncView> {
78
- const env = await request.post("auth/wecom/sync/cancel", { json: agentBody() }).json<{ code: number, data?: SyncView }>();
84
+ const env = await request.post("auth/wecom/sync/cancel", { json: agentBody(), timeout: SYNC_TIMEOUT_MS }).json<{ code: number, data?: SyncView }>();
79
85
  if (!env.data)
80
86
  throw new Error("取消同步失败");
81
87
  return remember(env.data);
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
7
7
  import { systemPermissionsGet, systemRolesGet } from "../../__client";
8
8
  import { isForbidden } from "../errors";
9
9
  import { ForbiddenPanel } from "../forbidden";
10
+ import { roleLabel } from "./role-label";
10
11
  import { visibleRoles } from "./visible-roles";
11
12
 
12
13
  type RoleRow = SystemRolesGetData[number];
@@ -41,16 +42,11 @@ export default function RolesPage() {
41
42
  {
42
43
  // R2:solution 角色带 title_key → 按当前语言实时翻译,title(安装期解析文本)兜底;
43
44
  // 内置角色(builtin)的 title 是种子里的站点默认语言文本,按角色名再兜一层 locale;
44
- // custom 角色无 key 原样显示 title(对齐下方权限点 title_key 的渲染先例)
45
+ // custom 角色无 key 原样显示 title(对齐下方权限点 title_key 的渲染先例)。
46
+ // 渲染口径与成员资格页共用 roleLabel(pages/roles/role-label.ts),改一处两处生效。
45
47
  title: t("system:rolesPage.columns.title"),
46
48
  dataIndex: "title",
47
- render: (_: string, row: RoleRow) => {
48
- if (row.title_key)
49
- return t(row.title_key, { defaultValue: row.title ?? "" });
50
- return row.builtin
51
- ? t(`system:rolesPage.roleTitle.${row.name}`, { defaultValue: row.title ?? row.name })
52
- : row.title;
53
- },
49
+ render: (_: string, row: RoleRow) => roleLabel(row, t),
54
50
  },
55
51
  { title: t("system:rolesPage.holders"), dataIndex: "holders" },
56
52
  {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 角色语言名解析(纯函数):角色页(roles/index.tsx R2 先例)与成员资格页
3
+ * (users/roles.tsx)共用的显示名口径,BDD 用例见 cli __tests__/users-roles-display.test.ts。
4
+ *
5
+ * 回退链(与角色页表格列渲染逐字一致):
6
+ * title_key(solution 角色,跨模块 namespace)→ t 译文,缺译文回退 title;
7
+ * 内置角色(builtin)→ `system:rolesPage.roleTitle.<name>` locale 键,缺键回退 title → name;
8
+ * custom 角色 → 原样 title,title 空回退 name。
9
+ */
10
+ export interface RoleLabelRow {
11
+ name: string
12
+ title: string | null
13
+ title_key: string | null
14
+ builtin: boolean
15
+ }
16
+
17
+ /** i18next t 的最小消费面(TFunction 兼容此签名) */
18
+ type Translate = (key: string, options?: { defaultValue?: string }) => string;
19
+
20
+ export function roleLabel(row: RoleLabelRow, t: Translate): string {
21
+ if (row.title_key)
22
+ return t(row.title_key, { defaultValue: row.title ?? "" });
23
+ return row.builtin
24
+ ? t(`system:rolesPage.roleTitle.${row.name}`, { defaultValue: row.title ?? row.name })
25
+ : (row.title ?? row.name);
26
+ }
@@ -0,0 +1,110 @@
1
+ import type { SolutionsRow, TaskLike } from "./market-entry";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { App, Button, Card, Progress, Result, Space, Tag } from "antd";
4
+ import { useEffect, useState } from "react";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ import { Link } from "react-router";
8
+ import { ojMarketIndexGet, ojMarketInstallPost, ojMarketTaskGet } from "../../../../../oj-market/web/__client";
9
+ import { explainError, isForbidden } from "../errors";
10
+ import { ForbiddenPanel } from "../forbidden";
11
+ import { marketEntryOf, taskOutcomeOf } from "./market-entry";
12
+
13
+ /**
14
+ * 方案安装入口(Phase B T3,superadmin):市场 UI 出厂未装(纯 web 方案包),
15
+ * 首装经此页走既有平台编排面 POST /api/oj-market/install(api 随脚手架内置,R29)。
16
+ * 状态推导与任务终态映射见 ./market-entry(纯函数,tests/solutions-entry.spec 钉)。
17
+ */
18
+ export default function SolutionsPage() {
19
+ const { t } = useTranslation();
20
+ const { message } = App.useApp();
21
+ const queryClient = useQueryClient();
22
+ const [taskId, setTaskId] = useState<string | null>(null);
23
+
24
+ const query = useQuery({
25
+ queryKey: ["system", "solutions"],
26
+ // size 上限 200(生成契约 max);首方目录规模小,翻页边角不在本页处理
27
+ queryFn: () => ojMarketIndexGet({ page: 1, size: 200 }),
28
+ });
29
+ const entry = marketEntryOf((query.data?.list ?? []) as unknown as SolutionsRow[]);
30
+
31
+ const taskQuery = useQuery({
32
+ queryKey: ["system", "solutions-task", taskId],
33
+ enabled: taskId !== null,
34
+ refetchInterval: q => (q.state.data && taskOutcomeOf(q.state.data as unknown as TaskLike).kind !== "running" ? false : 800),
35
+ queryFn: () => ojMarketTaskGet({ id: taskId! }),
36
+ });
37
+ const outcome = taskQuery.data ? taskOutcomeOf(taskQuery.data as unknown as TaskLike) : null;
38
+
39
+ // 终态后刷新目录行:安装完成 → 卡片翻「已装」态(含取消/失败也刷,状态以服务端为准)
40
+ useEffect(() => {
41
+ if (outcome !== null && outcome.kind !== "running")
42
+ void queryClient.invalidateQueries({ queryKey: ["system", "solutions"] });
43
+ }, [outcome, queryClient]);
44
+
45
+ const installMut = useMutation({
46
+ mutationFn: () => ojMarketInstallPost({ source: "cdn", name: "oj-market" }),
47
+ onSuccess: r => setTaskId(r.task_id ?? null),
48
+ onError: e => message.error(explainError(e, t)),
49
+ });
50
+
51
+ if (query.error && isForbidden(query.error))
52
+ return <ForbiddenPanel t={t} error={query.error} />;
53
+
54
+ if (entry.kind === "absent") {
55
+ return (
56
+ <Result
57
+ status="warning"
58
+ title={t("system:solutionsPage.absentTitle")}
59
+ subTitle={t("system:solutionsPage.absentHint")}
60
+ style={{ paddingTop: 48 }}
61
+ />
62
+ );
63
+ }
64
+
65
+ return (
66
+ <Space direction="vertical" style={{ width: "100%", paddingTop: 16 }}>
67
+ <Card title={t("system:solutionsPage.title")}>
68
+ <Space direction="vertical" style={{ width: "100%" }}>
69
+ <Space wrap>
70
+ <Tag color="blue">oj-market</Tag>
71
+ {entry.kind === "installable" && <Tag>{t("system:solutionsPage.latest", { version: entry.latest })}</Tag>}
72
+ {entry.kind === "installed" && <Tag color="green">{t("system:solutionsPage.installed", { version: entry.installed })}</Tag>}
73
+ {entry.kind === "installed" && entry.latest !== null && entry.latest !== entry.installed && (
74
+ <Tag color="orange">{t("system:solutionsPage.latest", { version: entry.latest })}</Tag>
75
+ )}
76
+ </Space>
77
+ {entry.kind === "installable" && outcome?.kind !== "done" && (
78
+ <>
79
+ <Button type="primary" loading={installMut.isPending || outcome?.kind === "running"} onClick={() => installMut.mutate()}>
80
+ {t("system:solutionsPage.install")}
81
+ </Button>
82
+ {outcome?.kind === "running" && (
83
+ <>
84
+ <Progress percent={outcome.progress} size="small" style={{ maxWidth: 480 }} />
85
+ <span>{outcome.message}</span>
86
+ </>
87
+ )}
88
+ {outcome?.kind === "failed" && <span style={{ color: "#cf1322" }}>{outcome.message}</span>}
89
+ {outcome?.kind === "needsTrust" && <span style={{ color: "#d48806" }}>{t("system:solutionsPage.needsTrust")}</span>}
90
+ </>
91
+ )}
92
+ {outcome?.kind === "done" && (
93
+ <Result
94
+ status="success"
95
+ title={t("system:solutionsPage.doneTitle")}
96
+ extra={<Link to="/oj-market"><Button type="primary">{t("system:solutionsPage.goMarket")}</Button></Link>}
97
+ style={{ padding: "16px 0" }}
98
+ />
99
+ )}
100
+ {entry.kind === "installed" && (
101
+ <Space>
102
+ <Link to="/oj-market"><Button type="primary">{t("system:solutionsPage.goMarket")}</Button></Link>
103
+ {entry.upgradable && <span>{t("system:solutionsPage.upgradableHint")}</span>}
104
+ </Space>
105
+ )}
106
+ </Space>
107
+ </Card>
108
+ </Space>
109
+ );
110
+ }