feat(download): 新增历史版本选择,重构下载全链路逻辑与UI

- 重构lib/github-releases.ts:统一预览版判断逻辑,新增支持`vX.Y.Z-N.beta`格式的beta标签,提取复用函数简化代码,将原有重复工具方法抽取至此统一管理并新增平台资产匹配工具
- 新增通用PlatformDownloadGrid多平台下载卡片组件,标准化多平台下载UI实现
- 重构两个下载API接口:统一返回格式并新增历史版本列表数据
- 重构/download和/launcher/join-beta页面:替换使用新的下载组件,新增历史版本选择功能
This commit is contained in:
2026-09-05 19:52:54 +08:00
parent 67b9d26460
commit 5997b1c7ea
7 changed files with 588 additions and 342 deletions
+82 -101
View File
@@ -1,35 +1,29 @@
import { NextResponse } from "next/server";
import {
fetchReleases,
pickBetaRelease,
pickAsset,
buildDownloadUrl,
isBetaRelease,
RELEASES_PAGE,
} from "@/lib/github-releases";
/**
* 预览版(Beta)发布信息接口
*
* 数据源优先级:
* 1. GitHub Releases API(直连)—— 主源,自动识别最新预览版
* 2. GitHub Releases APIgh-proxy.com 镜像)—— 国内可达
* 3. 旧静态源 version.json —— 兜底(同时提供 Beta 协议文本)
* 1. GitHub Releases API(直连 -> 镜像)—— 主源,自动识别最新预览版
* 2. 旧静态源 version.json —— 兜底(同时提供 Beta 协议 licence 文本)
*
* 发布方案与仓库信息见《Electron Windows自动更新方案规划》
* 仓库:dream-pep/koring-launcher
* 预览版 tagv{base}-beta.{RUN_NUMBER}(如 v1.2.5-beta.16),GitHub 标记为 prerelease
* 正式版 tagv{base}-{RUN_NUMBER}(如 v1.2.5-14),非 prerelease
* 安装包命名:koring-launcher-{version}-setup.exeelectron-builder artifactName
* 返回
* 顶层字段 = 最新预览版(licence 从旧静态源补充,下载需同意协议)
* versions = 历史预览版列表(新 -> 旧,按资产匹配平台;无 licence 数据,直接下载)
*/
const REPO = "dream-pep/koring-launcher";
const GITHUB_API = `https://api.github.com/repos/${REPO}/releases?per_page=100`;
const GITHUB_API_PROXY = `https://gh-proxy.com/https://api.github.com/repos/${REPO}/releases?per_page=100`;
const LEGACY_URL =
"https://koring-launcher-file-api.lenjing.cloud/launcher/beta/version.json";
const RELEASES_PAGE = `https://github.com/${REPO}/releases`;
const GH_HEADERS: Record<string, string> = {
"User-Agent": "koring-space-website",
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
/** 预览版 tag 形态:v1.2.5-beta.16 */
const BETA_TAG = /^v?\d+\.\d+\.\d+-beta\.\d+$/i;
const HISTORY_LIMIT = 10;
const EMPTY_LICENCE = { isNeedAgreat: "false", title: "", text: "", liceURL: "" };
@@ -43,94 +37,62 @@ const EMPTY_APPS = {
const CACHE_TTL = 15 * 60 * 1000;
const cache = new Map<string, { time: number; value: unknown }>();
async function fetchJson(url: string, headers?: Record<string, string>) {
const res = await fetch(url, {
headers,
cache: "no-store",
signal: AbortSignal.timeout(15_000),
});
if (!res.ok) return null;
try {
return await res.json();
} catch {
return null;
}
}
/** 依次尝试多个数据源,返回第一个成功的结果 */
async function trySources(
sources: Array<{ url: string; headers?: Record<string, string> }>
): Promise<unknown | null> {
for (const s of sources) {
try {
const json = await fetchJson(s.url, s.headers);
if (json !== null && json !== undefined) return json;
} catch {
// 继续尝试下一个源
}
}
return null;
}
/** 选出最新预览版 releaseprerelease 标记或 -beta.N tag */
function pickBetaRelease(releases: any[]) {
const betas = (releases || []).filter(
(r) =>
r &&
!r.draft &&
(r.prerelease === true || BETA_TAG.test(String(r.tag_name || "")))
function sortDesc(releases: any[]) {
return [...(releases || [])].sort((a, b) =>
String(b.published_at || b.created_at || "").localeCompare(
String(a.published_at || a.created_at || "")
)
);
betas.sort((a, b) => {
const ta = String(a.published_at || a.created_at || "");
const tb = String(b.published_at || b.created_at || "");
return tb.localeCompare(ta);
});
return betas[0] || null;
}
/** 按文件名匹配 release 资产 */
function pickAsset(release: any, patterns: RegExp[]) {
const assets = release?.assets || [];
for (const p of patterns) {
const hit = assets.find((a: any) => p.test(String(a?.name || "")));
if (hit) return hit;
}
return null;
}
function buildPlatforms(release: any, legacyApp: any) {
const win = pickAsset(release, [/setup\.exe$/i, /\.exe$/i]);
const mac = pickAsset(release, [/\.dmg$/i, /\.zip$/i, /\.pkg$/i]);
const lin = pickAsset(release, [
/\.appimage$/i,
/\.deb$/i,
/\.rpm$/i,
/\.tar\.gz$/i,
]);
/**
* 平台信息(DownlodeURL 为下载直链)
* licenceFor:给某平台提供 licence(最新版用旧源协议文本,历史版用空 = 无需同意)
*/
function buildPlatforms(release: any, licenceFor: (key: string) => any) {
const tag = String(release.tag_name || "");
const mk = (hit: any, key: string) => ({
isNoVersion: hit ? "false" : "true",
licence: legacyApp?.[key]?.licence ?? EMPTY_LICENCE,
DownlodeURL: hit?.browser_download_url || "",
licence: licenceFor(key),
DownlodeURL: hit ? buildDownloadUrl(tag, hit.name) : "",
});
return {
windows: mk(win, "windows"),
macos: mk(mac, "macos"),
linux: mk(lin, "linux"),
windows: mk(pickAsset(release, [/setup\.exe$/i, /\.exe$/i]), "windows"),
macos: mk(pickAsset(release, [/\.dmg$/i, /\.zip$/i, /\.pkg$/i]), "macos"),
linux: mk(
pickAsset(release, [/\.appimage$/i, /\.deb$/i, /\.rpm$/i, /\.tar\.gz$/i]),
"linux"
),
};
}
function buildGithubPayload(release: any, legacy: any) {
const version = String(release.tag_name || "").replace(/^v/i, "");
function baseEntry(release: any) {
const tag = String(release.tag_name || "");
return {
version: tag.replace(/^v/i, ""),
builddate: release.published_at || release.created_at || "",
tag,
htmlUrl: release.html_url || `${RELEASES_PAGE}/tag/${tag}`,
releaseNotes: release.body || "",
};
}
function buildGithubPayload(beta: any, legacy: any) {
// 最新预览版:licence 用旧静态源文本(保持原有"需同意 Beta 协议"体验)
const licenceSrc = legacy?.app || null;
return {
source: "github",
version,
builddate: release.published_at || release.created_at || "",
tag: release.tag_name,
htmlUrl: release.html_url || `${RELEASES_PAGE}/tag/${release.tag_name}`,
releaseNotes: release.body || "",
app: buildPlatforms(release, legacy?.app),
...baseEntry(beta),
app: buildPlatforms(beta, (key) => licenceSrc?.[key]?.licence ?? EMPTY_LICENCE),
};
}
/** 历史预览版条目(无协议要求) */
function buildHistoryEntry(release: any) {
return {
...baseEntry(release),
app: buildPlatforms(release, () => EMPTY_LICENCE),
};
}
@@ -152,20 +114,38 @@ export async function GET() {
}
// 1) 主源:GitHub Releases API(直连 -> 镜像)
const releases = (await trySources([
{ url: GITHUB_API, headers: GH_HEADERS },
{ url: GITHUB_API_PROXY, headers: GH_HEADERS },
])) as any[] | null;
const releases = await fetchReleases();
// 2) 兜底源:旧静态 JSON(也用于补充 Beta 协议 licence
const legacy = await trySources([{ url: LEGACY_URL }]);
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;
}
let payload: unknown;
if (Array.isArray(releases) && releases.length > 0) {
const beta = pickBetaRelease(releases);
if (beta) {
payload = buildGithubPayload(beta, legacy);
const betaTag = String(beta.tag_name || "");
const history = sortDesc(releases)
.filter(
(r) =>
isBetaRelease(r) && String(r.tag_name || "") !== betaTag
)
.slice(0, HISTORY_LIMIT)
.map(buildHistoryEntry);
payload = {
...buildGithubPayload(beta, legacy),
versions: history,
};
} else {
// GitHub 可达但没有任何预览版
payload = {
@@ -174,6 +154,7 @@ export async function GET() {
version: "",
builddate: "",
app: EMPTY_APPS,
versions: [],
};
}
} else if (legacy) {