Files
koring-space/app/api/beta-version/route.ts
T
dream_pep dcae468055 feat: 下载中心对接 GitHub Releases 并适配 latest.yml
- 新增 /download 下载页:正式版/预览版双渠道、latest.yml/latest-beta.yml 数据源、镜像下载源切换、SHA512 校验展示
- 新增 /api/download-version:GitHub API 自动识别 + 镜像兜底 + yml 解析 + 旧源兜底
- /launcher/join-beta 改为自动识别 GitHub 预览版,支持镜像下载源切换
- 新增 markdown-lite 轻量渲染器、lib/download-sources 与 lib/github-releases 共享模块
- 头部导航新增下载中心入口
2026-08-31 12:01:41 +08:00

191 lines
5.8 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";
/**
* 预览版(Beta)发布信息接口
*
* 数据源优先级:
* 1. GitHub Releases API(直连)—— 主源,自动识别最新预览版
* 2. GitHub Releases APIgh-proxy.com 镜像)—— 国内可达
* 3. 旧静态源 version.json —— 兜底(同时提供 Beta 协议文本)
*
* 发布方案与仓库信息见《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
*/
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 EMPTY_LICENCE = { isNeedAgreat: "false", title: "", text: "", liceURL: "" };
const EMPTY_APPS = {
windows: { isNoVersion: "true", licence: EMPTY_LICENCE, DownlodeURL: "" },
macos: { isNoVersion: "true", licence: EMPTY_LICENCE, DownlodeURL: "" },
linux: { isNoVersion: "true", licence: EMPTY_LICENCE, DownlodeURL: "" },
};
// 简单内存缓存(TTL 15 分钟),避免频繁打 GitHub API / 限流代理
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 || "")))
);
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,
]);
const mk = (hit: any, key: string) => ({
isNoVersion: hit ? "false" : "true",
licence: legacyApp?.[key]?.licence ?? EMPTY_LICENCE,
DownlodeURL: hit?.browser_download_url || "",
});
return {
windows: mk(win, "windows"),
macos: mk(mac, "macos"),
linux: mk(lin, "linux"),
};
}
function buildGithubPayload(release: any, legacy: any) {
const version = String(release.tag_name || "").replace(/^v/i, "");
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),
};
}
function buildLegacyPayload(legacy: any) {
return {
source: "legacy",
version: String(legacy.version || ""),
builddate: String(legacy.builddate || ""),
app: legacy.app ?? EMPTY_APPS,
aboutversion: legacy.aboutversion,
};
}
export async function GET() {
const now = Date.now();
const cached = cache.get("beta");
if (cached && now - cached.time < CACHE_TTL) {
return NextResponse.json(cached.value);
}
// 1) 主源:GitHub Releases API(直连 -> 镜像)
const releases = (await trySources([
{ url: GITHUB_API, headers: GH_HEADERS },
{ url: GITHUB_API_PROXY, headers: GH_HEADERS },
])) as any[] | null;
// 2) 兜底源:旧静态 JSON(也用于补充 Beta 协议 licence
const legacy = await trySources([{ url: LEGACY_URL }]);
let payload: unknown;
if (Array.isArray(releases) && releases.length > 0) {
const beta = pickBetaRelease(releases);
if (beta) {
payload = buildGithubPayload(beta, legacy);
} else {
// GitHub 可达但没有任何预览版
payload = {
source: "github",
noRelease: true,
version: "",
builddate: "",
app: EMPTY_APPS,
};
}
} else if (legacy) {
payload = buildLegacyPayload(legacy);
} else {
return NextResponse.json(
{ error: "无法获取版本信息,请稍后重试" },
{ status: 502 }
);
}
cache.set("beta", { time: now, value: payload });
return NextResponse.json(payload);
}