Files
koring-space/app/api/download-version/route.ts
T
dream_pep 5997b1c7ea feat(download): 新增历史版本选择,重构下载全链路逻辑与UI
- 重构lib/github-releases.ts:统一预览版判断逻辑,新增支持`vX.Y.Z-N.beta`格式的beta标签,提取复用函数简化代码,将原有重复工具方法抽取至此统一管理并新增平台资产匹配工具
- 新增通用PlatformDownloadGrid多平台下载卡片组件,标准化多平台下载UI实现
- 重构两个下载API接口:统一返回格式并新增历史版本列表数据
- 重构/download和/launcher/join-beta页面:替换使用新的下载组件,新增历史版本选择功能
2026-09-05 19:52:54 +08:00

176 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from "next/server";
import {
fetchReleases,
pickBetaRelease,
pickStableRelease,
pickAsset,
buildDownloadUrl,
fetchYmlForRelease,
isBetaRelease,
RELEASES_PAGE,
} from "@/lib/github-releases";
/**
* 下载页版本信息接口
* 返回:
* stable / preview —— 正式版与预览版最新一条(含 latest.yml 数据:文件名/大小/SHA512/发布时间,
* 与 electron-updater 自动更新机制一致)
* versions —— 历史版本列表(新 -> 旧,按资产匹配平台,无 yml 数据)
*/
const CACHE_TTL = 15 * 60 * 1000;
const cache = new Map<string, { time: number; value: unknown }>();
const LEGACY_URL =
"https://koring-launcher-file-api.lenjing.cloud/launcher/beta/version.json";
const HISTORY_LIMIT = 20;
const EXE_PATTERNS = [/setup\.exe$/i, /\.exe$/i];
const MAC_PATTERNS = [/\.dmg$/i, /\.zip$/i, /\.pkg$/i];
const LINUX_PATTERNS = [/\.appimage$/i, /\.deb$/i, /\.rpm$/i, /\.tar\.gz$/i];
/** 按发布资产匹配三个平台(无 yml 数据版本用) */
function platformsFromRelease(release: any) {
const tag = String(release.tag_name || "");
const mk = (hit: any) =>
hit ? { name: hit.name, url: buildDownloadUrl(tag, hit.name) } : null;
return {
windows: mk(pickAsset(release, EXE_PATTERNS)),
macos: mk(pickAsset(release, MAC_PATTERNS)),
linux: mk(pickAsset(release, LINUX_PATTERNS)),
};
}
function sortDesc(releases: any[]) {
return [...(releases || [])].sort((a, b) =>
String(b.published_at || b.created_at || "").localeCompare(
String(a.published_at || a.created_at || "")
)
);
}
/** 最新渠道条目(带 latest.yml / latest-beta.yml 数据) */
async function buildChannelAsync(release: any, ymlNames: string[]) {
if (!release) return null;
const tag = String(release.tag_name || "");
const version = tag.replace(/^v/i, "");
// 主源:latest.yml / latest-beta.ymlelectron-updater 权威数据)
const yml = await fetchYmlForRelease(release, ymlNames);
const ymlFile = yml?.files?.[0];
const plat = platformsFromRelease(release);
const windowsName =
ymlFile?.url || yml?.path || plat.windows?.name || "";
const windows = windowsName
? {
name: windowsName,
url: buildDownloadUrl(tag, windowsName),
size: ymlFile?.size ?? undefined,
sha512: yml?.sha512 || ymlFile?.sha512 || "",
}
: null;
return {
version,
tag,
channel: isBetaRelease(release) ? "preview" : "stable",
releaseDate:
yml?.releaseDate || release.published_at || release.created_at || "",
releaseNotes: release.body || "",
htmlUrl: release.html_url || `${RELEASES_PAGE}/tag/${tag}`,
windows,
macos: plat.macos,
linux: plat.linux,
};
}
/** 历史版本条目(资产匹配,不含 yml) */
function buildHistoryEntry(release: any) {
const tag = String(release.tag_name || "");
return {
version: tag.replace(/^v/i, ""),
tag,
channel: isBetaRelease(release) ? "preview" : "stable",
releaseDate: release.published_at || release.created_at || "",
releaseNotes: release.body || "",
htmlUrl: release.html_url || `${RELEASES_PAGE}/tag/${tag}`,
...platformsFromRelease(release),
};
}
function buildLegacyPayload(legacy: any) {
const win = legacy?.app?.windows;
const preview: any = {
version: String(legacy?.version || ""),
tag: "",
channel: "preview",
releaseDate: "",
releaseNotes: legacy?.aboutversion?.about || "",
htmlUrl: RELEASES_PAGE,
windows: win?.DownlodeURL
? {
name: String(win.DownlodeURL).split("/").pop() || "",
url: win.DownlodeURL,
size: undefined,
sha512: "",
}
: null,
macos: null,
linux: null,
};
return { source: "legacy", stable: null, preview, versions: [preview] };
}
export async function GET() {
const now = Date.now();
const cached = cache.get("download");
if (cached && now - cached.time < CACHE_TTL) {
return NextResponse.json(cached.value);
}
let payload: unknown;
const releases = await fetchReleases();
if (Array.isArray(releases) && releases.length > 0) {
const stableRelease = pickStableRelease(releases);
const betaRelease = pickBetaRelease(releases);
const [stable, preview] = await Promise.all([
buildChannelAsync(stableRelease, ["latest.yml"]),
buildChannelAsync(betaRelease, ["latest-beta.yml", "latest.yml"]),
]);
const versions = sortDesc(releases)
.filter((r) => r && !r.draft)
.slice(0, HISTORY_LIMIT)
.map(buildHistoryEntry);
payload = { source: "github", stable, preview, versions };
} else {
// GitHub 不可达 -> 旧静态源兜底(仅预览渠道)
let legacy: any = null;
try {
const res = await fetch(LEGACY_URL, {
cache: "no-store",
signal: AbortSignal.timeout(15_000),
});
if (res.ok) legacy = await res.json();
} catch {
legacy = null;
}
if (legacy) {
payload = buildLegacyPayload(legacy);
} else {
return NextResponse.json(
{ error: "无法获取版本信息,请稍后重试" },
{ status: 502 }
);
}
}
cache.set("download", { time: now, value: payload });
return NextResponse.json(payload);
}