@oj-module/cli 0.2.69 → 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.
- package/devkit/README.md +10 -8
- package/devkit/framework-dev.md +1 -1
- package/devkit/manual.md +45 -19
- package/devkit/module-dev-and-cdn-playground.md +20 -14
- package/devkit/publish-walkthrough.md +132 -43
- package/devkit/wecom-customer-reauth-guide.md +110 -0
- package/package.json +2 -2
- package/shell-dist/assets/{runtime.zrv2zhj9j0.js → runtime.7x99hmm020.js} +2 -2
- package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
- package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
- package/shell-dist/index.html +4 -4
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/list.test.ts +7 -7
- package/src/__tests__/oj-health-host.test.ts +19 -0
- package/src/__tests__/site-tenant-header.test.ts +2 -2
- package/src/__tests__/templates-builtin-list.test.ts +47 -0
- package/src/__tests__/templates-schema-columns.test.ts +11 -2
- package/src/__tests__/templates-tenant-scope.test.ts +2 -3
- package/src/__tests__/users-roles-display.test.ts +84 -0
- package/src/bundle.ts +1 -1
- package/src/contract/__tests__/market-client.integration.test.ts +23 -22
- package/src/enable.ts +2 -2
- package/src/init.ts +45 -0
- package/src/install.ts +5 -5
- package/src/lifecycle/__tests__/install.test.ts +29 -29
- package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
- package/src/lifecycle/__tests__/publish.test.ts +8 -10
- package/src/lifecycle/__tests__/trust.test.ts +14 -14
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/list.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -7
- package/src/market/__tests__/failure-playbook.test.ts +0 -1
- package/src/market/__tests__/remote-push.test.ts +228 -0
- package/src/market/__tests__/verify.test.ts +45 -207
- package/src/market/bootstrap.ts +12 -7
- package/src/market/command.ts +33 -35
- package/src/market/db-seed.ts +129 -0
- package/src/market/failure-playbook.ts +3 -9
- package/src/market/fixture.ts +2 -6
- package/src/market/publish.ts +145 -89
- package/src/market/remote-push.ts +236 -0
- package/src/market/trial.ts +2 -2
- package/src/market/verify.ts +17 -270
- package/src/market/walkthrough.ts +38 -63
- package/src/oj.ts +13 -2
- package/src/preview.ts +5 -3
- package/src/publish.ts +3 -2
- package/src/scaffold-guard.ts +4 -0
- package/src/site.ts +1 -1
- package/src/trust.ts +8 -8
- package/src/uninstall.ts +2 -2
- package/src/usage.ts +23 -21
- package/templates/config.yaml +22 -3
- package/templates/init.include.yaml +16 -11
- package/templates/openapi.json +3948 -3655
- package/templates/src/.ojm-builtin.json +13 -0
- package/templates/src/README.md +5 -2
- package/templates/src/app/api/_platform/README.md +3 -2
- package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
- package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
- package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
- package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
- package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
- package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
- package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
- package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
- package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
- package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
- package/templates/src/app/api/_platform/manifest.yaml +1 -2
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
- package/templates/src/app/api/_platform/schema.yaml +8 -11
- package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
- package/templates/src/app/api/_platform/seed.pg.sql +1 -1
- package/templates/src/app/api/_platform/seed.sql +1 -1
- package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
- package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -114
- package/templates/src/app/web/_platform/__client/index.ts +1 -333
- package/templates/src/app/web/system/locales/en-US.json +15 -1
- package/templates/src/app/web/system/locales/zh-CN.json +15 -1
- package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
- package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
- package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
- package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
- package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
- package/templates/src/app/web/system/pages/users/index.tsx +12 -2
- package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
- package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
- package/templates/src/app/web/system/routes.ts +18 -1
- package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
- package/templates/src/oj-market/api/backfill.ts +221 -0
- package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
- package/templates/src/oj-market/api/cdn/api.ts +312 -0
- package/templates/src/oj-market/api/cdn/shared.ts +104 -0
- package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
- package/templates/src/oj-market/api/manifest.yaml +8 -0
- package/templates/src/oj-market/solution.yaml +21 -0
- package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
- package/templates/src/oj-market/web/__client/index.ts +509 -0
- package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
- package/templates/src/oj-market/web/api/client.ts +6 -6
- package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
- package/templates/src/oj-market/web/entry.ts +5 -3
- package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
- package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
- package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
- package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
- package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
- package/templates/src/oj-market/web/routes.ts +12 -0
- package/templates/src/oj-market/web/types.ts +14 -8
- package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
- package/templates/tests/lifecycle/backfill.spec.ts +392 -0
- package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
- package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
- package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
- package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
- package/templates/tests/lifecycle/installer.spec.ts +36 -0
- package/templates/tests/lifecycle/market-api.spec.ts +448 -803
- package/templates/tests/lifecycle/preflight.spec.ts +18 -0
- package/templates/tests/lifecycle/trust.spec.ts +2 -2
- package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/solutions-entry.spec.ts +55 -0
- package/templates/tsconfig.paths.json +1 -0
- package/templates/web.config.ts +6 -6
- package/vendor/host-versions.json +3 -3
- package/bin/oj +0 -4
- package/src/market/__tests__/build-index.test.ts +0 -641
- package/src/market/__tests__/sharding.test.ts +0 -90
- package/src/market/__tests__/yank-ledger.test.ts +0 -60
- package/src/market/build-index.ts +0 -526
- package/src/market/index-builder.ts +0 -149
- package/src/market/sharding.ts +0 -94
- package/src/market/yank-ledger.ts +0 -91
- package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
- package/templates/src/app/api/_platform/market/index-source.ts +0 -112
- package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
11
|
EnableInput,
|
|
12
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from "
|
|
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(
|
|
59
|
-
const 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
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
118
|
+
return (await ojMarketHistoryGet()) as unknown as MarketHistory[];
|
|
113
119
|
},
|
|
114
120
|
async releases(input: { page?: number, limit?: number } = {}) {
|
|
115
|
-
return (await
|
|
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
|
|
124
|
+
return (await ojMarketUninstallImpactGet({ name })) as unknown as UninstallImpact;
|
|
119
125
|
},
|
|
120
126
|
async uninstall(name: string) {
|
|
121
|
-
const data = await
|
|
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/
|
|
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.
|
|
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
|
-
/**
|
|
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 {
|
|
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 /
|
|
12
|
-
export function useMarketIndex(): LoadState<
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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 {
|
|
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 === "
|
|
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 /
|
|
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 /
|
|
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 /
|
|
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 =
|
|
117
|
-
published_at_source: "file" | "
|
|
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
|
-
|
|
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
|
+
});
|