Wire publish flow to tRPC with live progress
This commit is contained in:
parent
9c7fd31a69
commit
5dd25a3267
@ -137,7 +137,14 @@
|
|||||||
- CurseForge:构造 multipart/form-data 请求,含 `metadata` JSON + 文件
|
- CurseForge:构造 multipart/form-data 请求,含 `metadata` JSON + 文件
|
||||||
- 通过 tRPC subscription 推送进度更新
|
- 通过 tRPC subscription 推送进度更新
|
||||||
6. **任一请求失败则取消当前平台的后续所有任务**,不影响另一个平台
|
6. **任一请求失败则取消当前平台的后续所有任务**,不影响另一个平台
|
||||||
7. 全部完成或失败后,弹出结果 Modal 显示最终状态(成功数 / 失败数)
|
7. 全部完成或失败后,弹出结果 Modal 显示最终状态(成功数 / 失败数 + 各平台失败原因列表)
|
||||||
|
|
||||||
|
### 错误处理与可见性
|
||||||
|
|
||||||
|
- 单个版本上传失败:记入该平台 `errors`,发出 `failed` 进度事件(进度条变红),该平台后续版本取消;
|
||||||
|
- 平台准备阶段失败(元数据拉取、版本范围计算):记为该平台**全部失败**,同样发出 `failed` 事件,并在服务端终端输出 `console.error`;
|
||||||
|
- mutation 层兜底:任何未被捕获的平台异常记入 `errors` 并输出服务端日志;
|
||||||
|
- 前端结果 Modal 按平台分行列出所有失败原因。
|
||||||
|
|
||||||
## Dry-run(模拟发布)
|
## Dry-run(模拟发布)
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AntdRegistry } from "@ant-design/nextjs-registry";
|
import { AntdRegistry } from "@ant-design/nextjs-registry";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { App, ConfigProvider } from "antd";
|
import { App, ConfigProvider } from "antd";
|
||||||
|
import { queryClient } from "@/lib/trpc";
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<AntdRegistry>
|
<AntdRegistry>
|
||||||
<ConfigProvider
|
<QueryClientProvider client={queryClient}>
|
||||||
theme={{
|
<ConfigProvider
|
||||||
token: {
|
theme={{
|
||||||
colorPrimary: "#1677FF",
|
token: {
|
||||||
borderRadius: 6,
|
colorPrimary: "#1677FF",
|
||||||
},
|
borderRadius: 6,
|
||||||
}}
|
},
|
||||||
>
|
}}
|
||||||
<App>{children}</App>
|
>
|
||||||
</ConfigProvider>
|
<App>{children}</App>
|
||||||
|
</ConfigProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
</AntdRegistry>
|
</AntdRegistry>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,9 +127,10 @@ export function MainPage({ initialConfigs }: MainPageProps) {
|
|||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
{/* 3. Publish modal */}
|
{/* 3. Publish modal */}
|
||||||
{publishOpen && selectedConfig && parsedProject && (
|
{publishOpen && selectedConfig && parsedProject && selectedConfigFile && (
|
||||||
<PublishModal
|
<PublishModal
|
||||||
open={publishOpen}
|
open={publishOpen}
|
||||||
|
configFile={selectedConfigFile}
|
||||||
config={selectedConfig}
|
config={selectedConfig}
|
||||||
project={parsedProject}
|
project={parsedProject}
|
||||||
lastVersionEnd={lastVersionEnd}
|
lastVersionEnd={lastVersionEnd}
|
||||||
|
|||||||
@ -15,16 +15,22 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Empty,
|
Empty,
|
||||||
Tag,
|
Tag,
|
||||||
|
Alert,
|
||||||
|
App,
|
||||||
theme,
|
theme,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { ReloadOutlined } from "@ant-design/icons";
|
import { ReloadOutlined } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useSubscription } from "@trpc/tanstack-react-query";
|
||||||
import type { BaseVersion, Config, UploadReleaseType } from "@/types";
|
import type { BaseVersion, Config, UploadReleaseType } from "@/types";
|
||||||
import type { ParsedProject } from "@/services/project";
|
import type { ParsedProject } from "@/services/project";
|
||||||
import { platforms } from "@/services/publish.schemas";
|
import { platforms } from "@/services/publish.schemas";
|
||||||
|
import type { PlatformResult, ProgressEvent } from "@/services/publish.schemas";
|
||||||
import { Template } from "@/lib/utils/template";
|
import { Template } from "@/lib/utils/template";
|
||||||
import type { McVersionEntry } from "@/lib/utils/mcVersion";
|
import type { McVersionEntry } from "@/lib/utils/mcVersion";
|
||||||
import { computeVersionRanges } from "@/lib/utils/mcVersion";
|
import { computeVersionRanges } from "@/lib/utils/mcVersion";
|
||||||
import { getModrinthMcVersions, getCurseForgeMcVersions } from "@/services/meta";
|
import { getModrinthMcVersions, getCurseForgeMcVersions } from "@/services/meta";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
import { ChangelogEditor } from "./ChangelogEditor";
|
import { ChangelogEditor } from "./ChangelogEditor";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
@ -48,6 +54,8 @@ interface PublishVersion {
|
|||||||
|
|
||||||
interface PublishModalProps {
|
interface PublishModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
/** 配置文件名(configs/ 下的 .json 文件名) */
|
||||||
|
configFile: string;
|
||||||
config: Config;
|
config: Config;
|
||||||
project: ParsedProject;
|
project: ParsedProject;
|
||||||
lastVersionEnd: string | null;
|
lastVersionEnd: string | null;
|
||||||
@ -86,12 +94,14 @@ const RELEASE_TYPES = Object.entries(RELEASE_TYPE_LABEL_MAP).map(v => ({ value:
|
|||||||
|
|
||||||
export function PublishModal({
|
export function PublishModal({
|
||||||
open,
|
open,
|
||||||
|
configFile,
|
||||||
config,
|
config,
|
||||||
project,
|
project,
|
||||||
lastVersionEnd,
|
lastVersionEnd,
|
||||||
onClose,
|
onClose,
|
||||||
}: PublishModalProps) {
|
}: PublishModalProps) {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
const { message } = App.useApp();
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
|
||||||
// Version selection
|
// Version selection
|
||||||
@ -108,14 +118,18 @@ export function PublishModal({
|
|||||||
const [publishing, setPublishing] = useState(false);
|
const [publishing, setPublishing] = useState(false);
|
||||||
const [publishDone, setPublishDone] = useState(false);
|
const [publishDone, setPublishDone] = useState(false);
|
||||||
const [progress, setProgress] = useState<
|
const [progress, setProgress] = useState<
|
||||||
Record<typeof platforms[number], { done: number; total: number }>
|
Record<
|
||||||
|
typeof platforms[number],
|
||||||
|
{ done: number; total: number; status: ProgressEvent["status"] }
|
||||||
|
>
|
||||||
>({
|
>({
|
||||||
modrinth: { done: 0, total: 0 },
|
modrinth: { done: 0, total: 0, status: "running" },
|
||||||
curseforge: { done: 0, total: 0 },
|
curseforge: { done: 0, total: 0, status: "running" },
|
||||||
});
|
});
|
||||||
const [publishResult, setPublishResult] = useState<{
|
const [publishResult, setPublishResult] = useState<{
|
||||||
success: number;
|
success: number;
|
||||||
failed: number;
|
failed: number;
|
||||||
|
errors: Record<typeof platforms[number], string[]>;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Platform MC versions — loaded when modal opens
|
// Platform MC versions — loaded when modal opens
|
||||||
@ -230,16 +244,82 @@ export function PublishModal({
|
|||||||
setCurrentStep((prev) => Math.max(prev - 1, 0));
|
setCurrentStep((prev) => Math.max(prev - 1, 0));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleConfirmPublish = useCallback(async () => {
|
const handleConfirmPublish = useCallback(() => {
|
||||||
|
// 重置状态并进入进度页;真正发起 mutation 在下方 useSubscription 的
|
||||||
|
// onStarted 里(保证先连上 SSE 再发布,不漏早期进度事件)
|
||||||
setPublishing(true);
|
setPublishing(true);
|
||||||
setPublishDone(false);
|
setPublishDone(false);
|
||||||
setPublishResult(null);
|
setPublishResult(null);
|
||||||
|
setProgress({
|
||||||
|
modrinth: { done: 0, total: selectedModrinth.length, status: "running" },
|
||||||
|
curseforge: {
|
||||||
|
done: 0,
|
||||||
|
total: selectedCurseforge.length,
|
||||||
|
status: "running",
|
||||||
|
},
|
||||||
|
});
|
||||||
setCurrentStep(2);
|
setCurrentStep(2);
|
||||||
// TODO: Call tRPC mutation to publish, update progress & result
|
}, [selectedModrinth.length, selectedCurseforge.length]);
|
||||||
setPublishResult({ success: 0, failed: 0 });
|
|
||||||
setPublishDone(true);
|
const publishMutation = useMutation(
|
||||||
setPublishing(false);
|
trpc.publish.mutationOptions({
|
||||||
}, []);
|
onSuccess: (result) => {
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
const errors = { modrinth: [], curseforge: [] } as Record<
|
||||||
|
typeof platforms[number],
|
||||||
|
string[]
|
||||||
|
>;
|
||||||
|
for (const platform of platforms) {
|
||||||
|
const r: PlatformResult = result[platform];
|
||||||
|
success += r.success;
|
||||||
|
failed += r.fail;
|
||||||
|
errors[platform] = r.errors;
|
||||||
|
}
|
||||||
|
setPublishResult({ success, failed, errors });
|
||||||
|
setPublishDone(true);
|
||||||
|
setPublishing(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
message.error(`发布请求失败:${error.message}`);
|
||||||
|
setPublishDone(true);
|
||||||
|
setPublishing(false);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useSubscription(
|
||||||
|
trpc.progress.subscriptionOptions(undefined, {
|
||||||
|
enabled: publishing,
|
||||||
|
onStarted: () => {
|
||||||
|
// SSE 已连接,现在发起发布不会漏事件
|
||||||
|
publishMutation.mutate({
|
||||||
|
configFile,
|
||||||
|
version: project.version,
|
||||||
|
mcVersions: {
|
||||||
|
modrinth: selectedModrinth.map((mc_version) => ({ mc_version })),
|
||||||
|
curseforge: selectedCurseforge.map((mc_version) => ({ mc_version })),
|
||||||
|
},
|
||||||
|
cutoffMcVersion: lastVersionEnd ?? "",
|
||||||
|
changelog,
|
||||||
|
versionType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onData: (event) => {
|
||||||
|
setProgress((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[event.platform]: {
|
||||||
|
done: event.current,
|
||||||
|
total: event.total,
|
||||||
|
status: event.status,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Progress subscription error:", error);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
if (publishing) return;
|
if (publishing) return;
|
||||||
@ -429,6 +509,11 @@ export function PublishModal({
|
|||||||
Math.max(progress.modrinth.total, 1)) *
|
Math.max(progress.modrinth.total, 1)) *
|
||||||
100,
|
100,
|
||||||
)}
|
)}
|
||||||
|
status={
|
||||||
|
progress.modrinth.status === "failed"
|
||||||
|
? "exception"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
format={() =>
|
format={() =>
|
||||||
`${progress.modrinth.done} / ${progress.modrinth.total}`
|
`${progress.modrinth.done} / ${progress.modrinth.total}`
|
||||||
}
|
}
|
||||||
@ -450,6 +535,11 @@ export function PublishModal({
|
|||||||
Math.max(progress.curseforge.total, 1)) *
|
Math.max(progress.curseforge.total, 1)) *
|
||||||
100,
|
100,
|
||||||
)}
|
)}
|
||||||
|
status={
|
||||||
|
progress.curseforge.status === "failed"
|
||||||
|
? "exception"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
format={() =>
|
format={() =>
|
||||||
`${progress.curseforge.done} / ${progress.curseforge.total}`
|
`${progress.curseforge.done} / ${progress.curseforge.total}`
|
||||||
}
|
}
|
||||||
@ -462,21 +552,37 @@ export function PublishModal({
|
|||||||
|
|
||||||
{/* Result */}
|
{/* Result */}
|
||||||
{publishDone && publishResult && (
|
{publishDone && publishResult && (
|
||||||
<div
|
<Alert
|
||||||
style={{
|
type={publishResult.failed > 0 ? "warning" : "success"}
|
||||||
background: token.colorSuccessBg,
|
title="发布结果"
|
||||||
border: `1px solid ${token.colorSuccessBorder}`,
|
description={
|
||||||
borderRadius: token.borderRadius,
|
<div>
|
||||||
padding: token.padding,
|
<div>
|
||||||
}}
|
成功 {publishResult.success} 个,失败{" "}
|
||||||
>
|
{publishResult.failed} 个
|
||||||
<Text strong>发布结果</Text>
|
</div>
|
||||||
<div style={{ marginTop: token.marginSM }}>
|
{platforms.map((platform) => {
|
||||||
<Text>
|
const errors = publishResult.errors[platform];
|
||||||
成功 {publishResult.success} 个,失败 {publishResult.failed} 个
|
if (errors.length === 0) return null;
|
||||||
</Text>
|
const label =
|
||||||
</div>
|
platform === "modrinth" ? "Modrinth" : "CurseForge";
|
||||||
</div>
|
return (
|
||||||
|
<div key={platform} style={{ marginTop: token.marginSM }}>
|
||||||
|
<Text strong>{label}:</Text>
|
||||||
|
<ul style={{ margin: "4px 0 0", paddingLeft: 20 }}>
|
||||||
|
{errors.map((e, i) => (
|
||||||
|
<li key={i}>
|
||||||
|
<Text type="danger">{e}</Text>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -79,18 +79,23 @@ export class CurseForgeClient {
|
|||||||
if (isPublishDryRun()) {
|
if (isPublishDryRun()) {
|
||||||
return interceptUpload("CurseForge", "POST", url, body) as Promise<T>;
|
return interceptUpload("CurseForge", "POST", url, body) as Promise<T>;
|
||||||
}
|
}
|
||||||
const res = await fetch(url, {
|
// ⚠️ 真实上传已暂时禁用,防止调试期间误传。
|
||||||
method: "POST",
|
// 恢复方式:删除下面的 throw,并取消注释 fetch 代码块。
|
||||||
headers: this.uploadHeaders(),
|
throw new Error(
|
||||||
body,
|
"真实上传已暂时禁用,请设置 PUBLISH_DRY_RUN=1 使用模拟发布",
|
||||||
});
|
);
|
||||||
if (!res.ok) {
|
// const res = await fetch(url, {
|
||||||
const reqBody = summarizeFormData(body);
|
// method: "POST",
|
||||||
const resBody = await readErrorBody(res);
|
// headers: this.uploadHeaders(),
|
||||||
throw new Error(
|
// body,
|
||||||
`CurseForge Upload POST ${path} failed: ${res.status} ${res.statusText}\n\n--Request--:\n${reqBody}\n\n--Response--:\n${resBody}`,
|
// });
|
||||||
);
|
// if (!res.ok) {
|
||||||
}
|
// const reqBody = summarizeFormData(body);
|
||||||
return res.json() as Promise<T>;
|
// const resBody = await readErrorBody(res);
|
||||||
|
// throw new Error(
|
||||||
|
// `CurseForge Upload POST ${path} failed: ${res.status} ${res.statusText}\n\n--Request--:\n${reqBody}\n\n--Response--:\n${resBody}`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return res.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,18 +46,23 @@ export class ModrinthClient {
|
|||||||
if (isPublishDryRun()) {
|
if (isPublishDryRun()) {
|
||||||
return interceptUpload("Modrinth", "POST", url, body) as Promise<T>;
|
return interceptUpload("Modrinth", "POST", url, body) as Promise<T>;
|
||||||
}
|
}
|
||||||
const res = await fetch(url, {
|
// ⚠️ 真实上传已暂时禁用,防止调试期间误传。
|
||||||
method: "POST",
|
// 恢复方式:删除下面的 throw,并取消注释 fetch 代码块。
|
||||||
headers: this.headers(),
|
throw new Error(
|
||||||
body,
|
"真实上传已暂时禁用,请设置 PUBLISH_DRY_RUN=1 使用模拟发布",
|
||||||
});
|
);
|
||||||
if (!res.ok) {
|
// const res = await fetch(url, {
|
||||||
const reqBody = summarizeFormData(body);
|
// method: "POST",
|
||||||
const resBody = await readErrorBody(res);
|
// headers: this.headers(),
|
||||||
throw new Error(
|
// body,
|
||||||
`Modrinth API POST ${path} failed: ${res.status} ${res.statusText}\n\n--Request--:\n${reqBody}\n\n--Response--:\n${resBody}`,
|
// });
|
||||||
);
|
// if (!res.ok) {
|
||||||
}
|
// const reqBody = summarizeFormData(body);
|
||||||
return res.json() as Promise<T>;
|
// const resBody = await readErrorBody(res);
|
||||||
|
// throw new Error(
|
||||||
|
// `Modrinth API POST ${path} failed: ${res.status} ${res.statusText}\n\n--Request--:\n${reqBody}\n\n--Response--:\n${resBody}`,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// return res.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/lib/trpc.ts
Normal file
31
src/lib/trpc.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import "client-only";
|
||||||
|
|
||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
createTRPCClient,
|
||||||
|
httpBatchLink,
|
||||||
|
httpSubscriptionLink,
|
||||||
|
splitLink,
|
||||||
|
} from "@trpc/client";
|
||||||
|
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
|
||||||
|
import type { AppRouter } from "@/services/publish";
|
||||||
|
|
||||||
|
const url = "/api/publish";
|
||||||
|
|
||||||
|
const trpcClient = createTRPCClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
splitLink({
|
||||||
|
// subscription 走 SSE,其余走普通 HTTP batch
|
||||||
|
condition: (op) => op.type === "subscription",
|
||||||
|
true: httpSubscriptionLink({ url }),
|
||||||
|
false: httpBatchLink({ url }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
||||||
|
client: trpcClient,
|
||||||
|
queryClient,
|
||||||
|
});
|
||||||
@ -191,8 +191,16 @@ export async function getCurseForgeMcVersions(): Promise<string[]> {
|
|||||||
const versions = await getMinecraftVersions(curseforgeClient);
|
const versions = await getMinecraftVersions(curseforgeClient);
|
||||||
return versions
|
return versions
|
||||||
.filter((v) => {
|
.filter((v) => {
|
||||||
const sv = SemVer.parse(v.versionString);
|
try {
|
||||||
return sv.preRelease === null;
|
const sv = SemVer.parse(v.versionString);
|
||||||
|
return sv.preRelease === null;
|
||||||
|
} catch {
|
||||||
|
// 正常不该出现;出现了说明上游返回了非标准版本号,跳过并记录
|
||||||
|
console.warn(
|
||||||
|
`[meta] CurseForge MC 版本号无法解析,已跳过: "${v.versionString}"`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.map((v) => v.versionString);
|
.map((v) => v.versionString);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
} from "./publish.schemas";
|
} from "./publish.schemas";
|
||||||
import { publishModrinthVersion } from "./platforms/modrinth";
|
import { publishModrinthVersion } from "./platforms/modrinth";
|
||||||
import { publishCurseForgeVersion } from "./platforms/curseforge";
|
import { publishCurseForgeVersion } from "./platforms/curseforge";
|
||||||
import { computeVersionRanges } from "@/lib/utils/mcVersion";
|
import { computeVersionRanges, type McVersionEntry } from "@/lib/utils/mcVersion";
|
||||||
import { getCurseForgeMcVersions, getModrinthMcVersions } from "./meta";
|
import { getCurseForgeMcVersions, getModrinthMcVersions } from "./meta";
|
||||||
import { readAsFile } from "@/lib/utils/file";
|
import { readAsFile } from "@/lib/utils/file";
|
||||||
import { Template } from "@/lib/utils/template";
|
import { Template } from "@/lib/utils/template";
|
||||||
@ -76,11 +76,30 @@ async function runPlatform(
|
|||||||
const publishFn = platformAdaptors[platform].publish;
|
const publishFn = platformAdaptors[platform].publish;
|
||||||
const getGameVersionsFn = platformAdaptors[platform].getGameVersions;
|
const getGameVersionsFn = platformAdaptors[platform].getGameVersions;
|
||||||
|
|
||||||
const mcVersionEntries = computeVersionRanges(
|
// 平台准备阶段(元数据拉取 + 版本范围计算)。此处异常不在逐版本的
|
||||||
mcVersions.map(v => v.mc_version),
|
// try/catch 内,必须单独兜底:记为该平台全部失败并发出 failed 事件,
|
||||||
await getGameVersionsFn(),
|
// 否则前端进度条会停在 running 假象里,错误也被吞掉。
|
||||||
ctx.input.cutoffMcVersion
|
let mcVersionEntries: McVersionEntry[];
|
||||||
);
|
try {
|
||||||
|
mcVersionEntries = computeVersionRanges(
|
||||||
|
mcVersions.map(v => v.mc_version),
|
||||||
|
await getGameVersionsFn(),
|
||||||
|
ctx.input.cutoffMcVersion
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
|
console.error(`[publish] ${platform} 准备阶段失败:`, err);
|
||||||
|
result.fail = total;
|
||||||
|
result.errors.push(msg);
|
||||||
|
ee.emit("progress", {
|
||||||
|
platform,
|
||||||
|
current: 0,
|
||||||
|
total,
|
||||||
|
status: "failed",
|
||||||
|
errors: result.errors,
|
||||||
|
} satisfies ProgressEvent);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const primaryFileTemplate = new Template(ctx.config.filename_format);
|
const primaryFileTemplate = new Template(ctx.config.filename_format);
|
||||||
const sourceFileTemplate = new Template(ctx.config.source_filename_format);
|
const sourceFileTemplate = new Template(ctx.config.source_filename_format);
|
||||||
@ -142,6 +161,7 @@ export const appRouter = t.router({
|
|||||||
const result = await runPlatform(platform, input.mcVersions[platform], ctx);
|
const result = await runPlatform(platform, input.mcVersions[platform], ctx);
|
||||||
return {platform, result};
|
return {platform, result};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error(`[publish] ${platform} 执行异常:`, err);
|
||||||
return {
|
return {
|
||||||
platform,
|
platform,
|
||||||
result: { success: 0, fail: 0, errors: [errorMessage(err)] }
|
result: { success: 0, fail: 0, errors: [errorMessage(err)] }
|
||||||
@ -156,7 +176,13 @@ export const appRouter = t.router({
|
|||||||
for await (const [data] of on(ee, "progress", {
|
for await (const [data] of on(ee, "progress", {
|
||||||
signal: opts.signal,
|
signal: opts.signal,
|
||||||
})) {
|
})) {
|
||||||
yield ProgressEventSchema.parse(data);
|
const parsed = ProgressEventSchema.safeParse(data);
|
||||||
|
if (!parsed.success) {
|
||||||
|
// 坏事件不应中断订阅(客户端没有重连机制),跳过并记录
|
||||||
|
console.error("[publish] invalid progress event:", parsed.error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
yield parsed.data;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user