@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
@@ -9,7 +9,7 @@
9
9
 
10
10
  import type {
11
11
  EnableInput,
12
- IndexRow,
12
+ IndexPage,
13
13
  InstallInput,
14
14
  MarketHistory,
15
15
  ReleasePage,
@@ -21,16 +21,16 @@ import type { MarketClient } from "./client";
21
21
  import { request } from "@oj-module/runtime";
22
22
  import {
23
23
  bindRequest,
24
- platformMarketEnablePost,
25
- platformMarketHistoryGet,
26
- platformMarketInstallPost,
27
- platformMarketMetaGet,
28
- platformMarketReleasesGet,
29
- platformMarketTaskGet,
30
- platformMarketTrustPost,
31
- platformMarketUninstallImpactGet,
32
- platformMarketUninstallPost,
33
- } from "../../../app/web/_platform/__client";
24
+ ojMarketEnablePost,
25
+ ojMarketHistoryGet,
26
+ ojMarketInstallPost,
27
+ ojMarketMetaGet,
28
+ ojMarketReleasesGet,
29
+ ojMarketTaskGet,
30
+ ojMarketTrustPost,
31
+ ojMarketUninstallImpactGet,
32
+ ojMarketUninstallPost,
33
+ } from "../__client";
34
34
 
35
35
  // 绑定运行时 request 到生成 client 的模块级单槽(生成 client 是静态单例,必须由某处
36
36
  // bindRequest;此处兜底确保市场子应用无论挂载顺序都能拿到请求实例)。与 _platform 模块
@@ -55,8 +55,8 @@ interface OjEnvelope<T> {
55
55
  }
56
56
 
57
57
  // 第一方市场端点:相对前缀(无前导 /),由运行时 request 的 VITE_API_BASE_URL(/api)统一加前缀;
58
- // 与生成 client(platformMarket* 用 `_platform/...`)保持一致,避免 /api 叠加成 /api/api/(R29)。
59
- const MARKET = "_platform/market";
58
+ // 与生成 client(ojMarket* 用 `oj-market/...`)保持一致,避免 /api 叠加成 /api/api/(R29)。
59
+ const MARKET = "oj-market";
60
60
 
61
61
  /**
62
62
  * 503 = 服务端「未配置市场地址(framework.market_cdn_url)」,是**预期空态**而非故障
@@ -70,24 +70,30 @@ function isUnconfigured(e: unknown): boolean {
70
70
  export const httpMarketClient: MarketClient = {
71
71
  async meta() {
72
72
  // 生成 client 已解包信封 + DEV 校验;此处仅对齐领域类型。
73
- return (await platformMarketMetaGet()) as { apiVersion: string };
73
+ return (await ojMarketMetaGet()) as { apiVersion: string };
74
74
  },
75
- async index() {
75
+ async index(input: { page?: number, size?: number } = {}) {
76
76
  // 唯一需 silentError 的端点:直连 request 抑制「未配置市场地址」的全局 toast。
77
+ // v4 §四:分页数据源({list,total,page,size});503(市场地址未配置)归空页——空态由卡片区文案承载。
77
78
  try {
79
+ const params: Record<string, string> = {};
80
+ if (input.page != null)
81
+ params.page = String(input.page);
82
+ if (input.size != null)
83
+ params.size = String(input.size);
78
84
  const r = await (request as unknown as HttpGetLike)
79
- .get<OjEnvelope<IndexRow[]>>(`${MARKET}/index`, { silentError: true, retry: 0 });
85
+ .get<OjEnvelope<IndexPage>>(`${MARKET}/index`, { searchParams: params, silentError: true, retry: 0 });
80
86
  const env = await r.json();
81
87
  return env.data!;
82
88
  }
83
89
  catch (e) {
84
90
  if (isUnconfigured(e))
85
- return [];
91
+ return { list: [], total: 0, page: 1, size: 20 };
86
92
  throw e;
87
93
  }
88
94
  },
89
95
  async install(input: InstallInput) {
90
- const data = await platformMarketInstallPost({
96
+ const data = await ojMarketInstallPost({
91
97
  name: input.name,
92
98
  version: input.version ?? null,
93
99
  source: input.source ?? "cdn",
@@ -97,28 +103,28 @@ export const httpMarketClient: MarketClient = {
97
103
  async task(id: string) {
98
104
  // 生成 Data 为空 schema(仅 DEV catchall 校验),经 unknown 桥接领域类型;
99
105
  // 运行时形状由服务端真源 + DEV zod 校验保证,TS 侧以领域类型为准。
100
- return (await platformMarketTaskGet({ id })) as unknown as TaskRecord;
106
+ return (await ojMarketTaskGet({ id })) as unknown as TaskRecord;
101
107
  },
102
108
  async trust(taskId: string) {
103
109
  // 服务端自任务记录重取公钥复算指纹比对(防混淆代理),前端只传 task_id,不传公钥
104
- const data = await platformMarketTrustPost({ task_id: taskId });
110
+ const data = await ojMarketTrustPost({ task_id: taskId });
105
111
  return data as { result: string, key_id: string, fingerprint: string };
106
112
  },
107
113
  async enable(input: EnableInput) {
108
- const data = await platformMarketEnablePost({ name: input.name, enabled: input.enabled });
114
+ const data = await ojMarketEnablePost({ name: input.name, enabled: input.enabled });
109
115
  return data as { ok: boolean, message: string };
110
116
  },
111
117
  async history() {
112
- return (await platformMarketHistoryGet()) as unknown as MarketHistory[];
118
+ return (await ojMarketHistoryGet()) as unknown as MarketHistory[];
113
119
  },
114
120
  async releases(input: { page?: number, limit?: number } = {}) {
115
- return (await platformMarketReleasesGet(input as { page: number, limit: number })) as unknown as ReleasePage;
121
+ return (await ojMarketReleasesGet(input as { page: number, limit: number })) as unknown as ReleasePage;
116
122
  },
117
123
  async uninstallImpact(name: string) {
118
- return (await platformMarketUninstallImpactGet({ name })) as unknown as UninstallImpact;
124
+ return (await ojMarketUninstallImpactGet({ name })) as unknown as UninstallImpact;
119
125
  },
120
126
  async uninstall(name: string) {
121
- const data = await platformMarketUninstallPost({ name });
127
+ const data = await ojMarketUninstallPost({ name });
122
128
  return data as { ok: boolean, message: string };
123
129
  },
124
130
  };
@@ -5,8 +5,9 @@ import { defineModule, OutsidePageIcon } from "@oj-module/runtime";
5
5
  import { createElement } from "react";
6
6
  import MarketApp from "./pages/MarketApp";
7
7
  import { onInit as publishOnInit, routes as publishRoutes } from "./publish/routes";
8
+ import { onInit as marketOnInit } from "./routes";
8
9
 
9
- // 方案市场前端模块(第一方专用,R29:消费 /api/_platform/market/* 编排面,第三方方案不得仿照此调用面)。
10
+ // 方案市场前端模块(第一方专用,R29:消费 /api/oj-market/* 编排面,第三方方案不得仿照此调用面)。
10
11
  // 菜单标题走 i18next 命名空间语法 "<moduleName>:menu.<key>"(i18n 资源在 locales/,安装时随包注册)。
11
12
  //
12
13
  // **单模单根**(2026-11-05):菜单只有一个「方案市场」根,下挂三个子页——
@@ -25,7 +26,7 @@ const i18nNamespaces: Record<string, ModuleI18n> = {
25
26
  export default defineModule({
26
27
  name: "oj-market",
27
28
  description: "oj 方案市场应用——方案清单 / 发布管理 / 发布历史(第一方专用,R29)",
28
- version: "0.1.0",
29
+ version: "0.2.0",
29
30
  routes: [
30
31
  {
31
32
  path: "/oj-market",
@@ -54,8 +55,9 @@ export default defineModule({
54
55
  },
55
56
  i18nNamespaces,
56
57
  lifecycle: {
57
- /** 发布页的契约前缀 /publish + scoped request 在这里接线(与市场自身的 client 互不影响) */
58
+ /** 市场编排面 client + 发布页契约前缀 /publish 在这里接线(两侧互不影响) */
58
59
  async onInit(ctx: ModuleContext) {
60
+ marketOnInit(ctx);
59
61
  publishOnInit(ctx);
60
62
  },
61
63
  },
@@ -1,4 +1,4 @@
1
- import type { IndexRow, MarketHistory, TaskRecord } from "../types";
1
+ import type { IndexPage, MarketHistory, TaskRecord } from "../types";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { useMarketClient } from "../api/context";
4
4
  import { poll } from "../utils/poll";
@@ -8,10 +8,10 @@ import { poll } from "../utils/poll";
8
8
  // 故非 ok 变体也声明 data?: undefined(ok 变体 data: T,访问结果为 T | undefined)。
9
9
  export type LoadState<T> = { status: "loading", data?: undefined } | { status: "ok", data: T } | { status: "error", error: string, data?: undefined };
10
10
 
11
- /** 卡片索引加载(含重试);消费 GET /_platform/market/index */
12
- export function useMarketIndex(): LoadState<IndexRow[]> & { retry: () => void } {
11
+ /** 卡片索引加载(含重试);消费 GET /oj-market/index(v4 §四 DB 目录分页) */
12
+ export function useMarketIndex(page = 1, size = 20): LoadState<IndexPage> & { retry: () => void } {
13
13
  const client = useMarketClient();
14
- const [state, setState] = useState<LoadState<IndexRow[]>>({ status: "loading" });
14
+ const [state, setState] = useState<LoadState<IndexPage>>({ status: "loading" });
15
15
  // 重试不复跑「先同步 set loading 再拉取」的加载函数(effect 内同步 set 触发
16
16
  // react/set-state-in-effect),改为事件里置 loading + 递增 nonce 驱动 effect 重跑。
17
17
  const [nonce, setNonce] = useState(0);
@@ -19,13 +19,13 @@ export function useMarketIndex(): LoadState<IndexRow[]> & { retry: () => void }
19
19
  useEffect(() => {
20
20
  let active = true;
21
21
  client
22
- .index()
22
+ .index({ page, size })
23
23
  .then(data => active && setState({ status: "ok", data }))
24
24
  .catch(e => active && setState({ status: "error", error: e instanceof Error ? e.message : String(e) }));
25
25
  return () => {
26
26
  active = false;
27
27
  };
28
- }, [client, nonce]);
28
+ }, [client, nonce, page, size]);
29
29
 
30
30
  const retry = useCallback(() => {
31
31
  setState({ status: "loading" });
@@ -1,6 +1,6 @@
1
1
  import type { IndexRow, TaskRecord } from "../types";
2
2
  import { useUserStore } from "@oj-module/runtime";
3
- import { Alert, Typography } from "antd";
3
+ import { Alert, Pagination, Typography } from "antd";
4
4
  import React, { useState } from "react";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { useMarketClient } from "../api/context";
@@ -16,7 +16,9 @@ import { useMarketIndex } from "../hooks/useMarket";
16
16
  export default function MarketApp() {
17
17
  const { t } = useTranslation();
18
18
  const client = useMarketClient();
19
- const index = useMarketIndex();
19
+ const [page, setPage] = useState(1);
20
+ const [pageSize, setPageSize] = useState(20);
21
+ const index = useMarketIndex(page, pageSize);
20
22
  // 管理权限 = superadmin(与后端 assertSuperAdmin 口径一致)。仅屏蔽写操作,浏览(详情/历史)对所有登录用户开放。
21
23
  const roles = useUserStore(s => s.roles);
22
24
  const canManage = roles.includes("superadmin");
@@ -30,7 +32,8 @@ export default function MarketApp() {
30
32
  const [historyRow, setHistoryRow] = useState<IndexRow | null>(null);
31
33
  const [busyName, setBusyName] = useState<string | null>(null);
32
34
 
33
- const rows = index.status === "ok" ? index.data : [];
35
+ const pageData = index.status === "ok" ? index.data : null;
36
+ const rows = pageData?.list ?? [];
34
37
 
35
38
  // 安装/升级:目标版本缺省(取 latest);已装视为升级
36
39
  const handleInstall = (row: IndexRow) => {
@@ -109,6 +112,20 @@ export default function MarketApp() {
109
112
  onRetry={index.retry}
110
113
  />
111
114
 
115
+ {pageData != null && pageData.total > pageSize && (
116
+ <Pagination
117
+ style={{ marginTop: 16, textAlign: "right" }}
118
+ current={page}
119
+ pageSize={pageSize}
120
+ total={pageData.total}
121
+ onChange={(p, ps) => {
122
+ setPage(ps !== pageSize ? 1 : p);
123
+ setPageSize(ps);
124
+ }}
125
+ showSizeChanger
126
+ />
127
+ )}
128
+
112
129
  <DetailDrawer
113
130
  row={detailRow}
114
131
  open={detailRow != null}
@@ -8,7 +8,7 @@
8
8
  "subtitle": "Every release recorded in the signed index, newest first. Product, version and publisher all come from the index itself.",
9
9
  "empty": "No releases yet",
10
10
  "syncFailed": "The market index is temporarily unreachable; showing records already stored",
11
- "indexTime": "index time",
11
+ "backfillTime": "backfilled",
12
12
  "publishedAtHint": "Uses the CDN file's Last-Modified when available; otherwise falls back to the index build time, tagged “index time”",
13
13
  "total": "{{total}} in total",
14
14
  "columns": {
@@ -8,7 +8,7 @@
8
8
  "subtitle": "签名索引里出现过的每一次发版:产品、版本与发布身份都来自索引本身,按发布时间倒序。",
9
9
  "empty": "还没有发布记录",
10
10
  "syncFailed": "暂时读不到市场索引,下面是已入库的历史记录",
11
- "indexTime": "索引时间",
11
+ "backfillTime": "回填时间",
12
12
  "publishedAtHint": "优先取分发包落 CDN 的时间(Last-Modified);源不提供时用索引生成时间,会标注「索引时间」",
13
13
  "total": "共 {{total}} 条",
14
14
  "columns": {
@@ -1,6 +1,6 @@
1
1
  import type { ReleaseRow } from "../../types";
2
2
  import { useQuery } from "@tanstack/react-query";
3
- import { Alert, Space, Table, Tag, Tooltip, Typography } from "antd";
3
+ import { Space, Table, Tag, Tooltip, Typography } from "antd";
4
4
  import dayjs from "dayjs";
5
5
  import { useState } from "react";
6
6
  import { useTranslation } from "react-i18next";
@@ -54,7 +54,7 @@ export default function ReleaseHistoryPage() {
54
54
  render: (value: number, row: ReleaseRow) => (
55
55
  <Space size={4}>
56
56
  <span>{formatSecs(value)}</span>
57
- {row.published_at_source === "index" && <Tag color="default">{t("publish:history.indexTime")}</Tag>}
57
+ {row.published_at_source === "file" && <Tag color="default">{t("publish:history.backfillTime")}</Tag>}
58
58
  </Space>
59
59
  ),
60
60
  },
@@ -78,15 +78,6 @@ export default function ReleaseHistoryPage() {
78
78
  <Typography.Text type="secondary">{t("publish:history.subtitle")}</Typography.Text>
79
79
  </Space>
80
80
 
81
- {query.data?.syncError != null && (
82
- <Alert
83
- type="warning"
84
- showIcon
85
- message={t("publish:history.syncFailed")}
86
- description={query.data.syncError}
87
- />
88
- )}
89
-
90
81
  <Table<ReleaseRow>
91
82
  rowKey="id"
92
83
  size="middle"
@@ -0,0 +1,12 @@
1
+ import type { ModuleContext } from "@oj-module/runtime";
2
+
3
+ import { createOjMarketClient } from "./__client";
4
+
5
+ /**
6
+ * 市场编排面(/api/oj-market/*)client 接线:entry 的 onInit 里一行创建——
7
+ * 登记契约前缀 /oj-market + 绑定 scoped request。页面/子应用经 web/api/client.ts
8
+ * 的领域接口消费(内部委托本 __client 的静态函数)。
9
+ */
10
+ export function onInit(ctx: ModuleContext): void {
11
+ createOjMarketClient(ctx);
12
+ }
@@ -52,7 +52,7 @@ export interface TaskRecord {
52
52
  appliedSeq?: number
53
53
  }
54
54
 
55
- /** 卡片数据源行(GET /_platform/market/index):CDN 三级索引(catalog + shards)⊕ 本地 sidecar state */
55
+ /** 卡片数据源行(GET /oj-market/index):DB 目录聚合 ⊕ 本地 sidecar state(v4 §四) */
56
56
  export interface IndexRow {
57
57
  name: string
58
58
  latest: string | null
@@ -69,14 +69,14 @@ export interface IndexRow {
69
69
  description?: string | null
70
70
  }
71
71
 
72
- /** 安装输入(POST /_platform/market/install) */
72
+ /** 安装输入(POST /oj-market/install) */
73
73
  export interface InstallInput {
74
74
  name: string
75
75
  version?: string | null
76
76
  source?: "cdn" | "file"
77
77
  }
78
78
 
79
- /** 启用/停用输入(POST /_platform/market/enable) */
79
+ /** 启用/停用输入(POST /oj-market/enable) */
80
80
  export interface EnableInput {
81
81
  name: string
82
82
  enabled: boolean
@@ -104,7 +104,7 @@ export interface UninstallImpact {
104
104
  affectedUsers: number
105
105
  }
106
106
 
107
- /** 发布历史一行(`market_release`;来源是签名索引,追加式记录) */
107
+ /** 发布历史一行(`market_release` 上传写入行;v4 §四 DB 目录化后索引 syncReleases 退役) */
108
108
  export interface ReleaseRow {
109
109
  id: number
110
110
  product: string
@@ -113,8 +113,8 @@ export interface ReleaseRow {
113
113
  sha256: string | null
114
114
  size: number | null
115
115
  published_at: number
116
- /** file = 分发包 Last-Modified;index = 索引生成时间兜底 */
117
- published_at_source: "file" | "index"
116
+ /** file = 回填扫描读 tgz mtime;upload = 上传端点接收时刻(DB 目录化后唯一真源口径) */
117
+ published_at_source: "file" | "upload"
118
118
  yanked: number
119
119
  }
120
120
 
@@ -124,6 +124,12 @@ export interface ReleasePage {
124
124
  total: number
125
125
  page: number
126
126
  size: number
127
- /** 本次同步索引失败的人话(页面照常显示已入库历史) */
128
- syncError: string | null
127
+ }
128
+
129
+ /** 卡片索引分页结果(v4 §四:indexBody 改 DB 目录分页数据源,展示字段不变) */
130
+ export interface IndexPage {
131
+ list: IndexRow[]
132
+ total: number
133
+ page: number
134
+ size: number
129
135
  }
@@ -0,0 +1,65 @@
1
+ // BDD:市场匿名面接线(计划 v4 §五——签名推送不经账号口令,企微用户无障碍)。
2
+ // auth.anonymous_paths 与 tenant.anonymous_paths 双列(去 base 前缀形态):
3
+ // /oj-market/catalog/** 目录匿名读(站点身份发现面 + CLI 2×2 矩阵探测:旧站点 404 → 人话)
4
+ // /oj-market/cdn/upload 签名推送端点(X-OJM-Key-Id/X-OJM-Signature 即凭证,不经 Bearer)
5
+ // /oj-market/yank 签名下线端点(全分支终审 C1:不经匿名列表则 oj-auth 守卫在
6
+ // handler 前硬 401——文档承诺的签名制 yank 永远无法到达处理器)
7
+ // 匹配语义(oj-auth v0.1.20,../oj-bin 只读核实):字面精确;`*` 恰一段;`**` 跨段(≥0 段)——
8
+ // `/oj-market/catalog/**` 同条覆盖 catalog 与 catalog/detail。
9
+ // 生效口径:config.yaml 是启动冻结段,改后须重启(restart_pending),发版说明提醒。
10
+ import { readFileSync } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { describe, expect, it } from "vitest";
14
+ import { parse } from "yaml";
15
+
16
+ const TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "../..");
17
+ const REQUIRED = ["/oj-market/catalog/**", "/oj-market/cdn/upload", "/oj-market/yank"];
18
+
19
+ function section(config: Record<string, unknown>, key: "auth" | "tenant"): string[] {
20
+ const anon = (config[key] as Record<string, unknown>)?.anonymous_paths;
21
+ return Array.isArray(anon) ? anon.filter((p): p is string => typeof p === "string") : [];
22
+ }
23
+
24
+ describe("场景:匿名接线(templates config.yaml 双列齐了市场匿名面)", () => {
25
+ const config = parse(readFileSync(join(TEMPLATES, "config.yaml"), "utf-8")) as Record<string, unknown>;
26
+
27
+ it("当 未登录访问目录/上传,则 auth.anonymous_paths 放行 catalog 两条 GET 与 upload", () => {
28
+ const anon = section(config, "auth");
29
+ for (const p of REQUIRED)
30
+ expect(anon, `auth.anonymous_paths 缺 ${p}`).toContain(p);
31
+ });
32
+
33
+ it("当 CLI 裸 fetch 不带租户头推送,则 tenant.anonymous_paths 同列(缺头不 400)", () => {
34
+ const anon = section(config, "tenant");
35
+ for (const p of REQUIRED)
36
+ expect(anon, `tenant.anonymous_paths 缺 ${p}`).toContain(p);
37
+ });
38
+
39
+ it("数据表 通配语义:catalog/** 覆盖 catalog 与 catalog/detail 两级(`**` 跨段 ≥0 段)", () => {
40
+ // oj-auth v0.1.20 segments_match:`/` 分段后 `**` 匹配任意剩余段(含零段)。
41
+ // 此处按同语义做分段模拟,钉死「一条覆盖两条 GET」的写法不因收紧而静默漏 detail。
42
+ const match = (pattern: string, path: string): boolean => {
43
+ const seg = (s: string) => s.split("/").filter(Boolean);
44
+ const pat = seg(pattern);
45
+ const p = seg(path);
46
+ const rec = (i: number, j: number): boolean => {
47
+ if (i === pat.length)
48
+ return j === p.length;
49
+ if (pat[i] === "**") {
50
+ for (let k = j; k <= p.length; k++) {
51
+ if (rec(i + 1, k))
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ return j < p.length && (pat[i] === "*" || pat[i] === p[j]) && rec(i + 1, j + 1);
57
+ };
58
+ return rec(0, 0);
59
+ };
60
+ const pat = REQUIRED[0]!;
61
+ expect(match(pat, "/oj-market/catalog")).toBe(true);
62
+ expect(match(pat, "/oj-market/catalog/detail")).toBe(true);
63
+ expect(match(pat, "/oj-market/index")).toBe(false);
64
+ });
65
+ });