Spaces:
Paused
Paused
| const CACHE_TTL_MS = 5 * 60 * 1000; | |
| const releasesCache = new Map<string, { data: any; ts: number }>(); | |
| function getGitHubHeaders(): Record<string, string> { | |
| const headers: Record<string, string> = { Accept: "application/vnd.github+json" }; | |
| const token = process.env.GITHUB_TOKEN; | |
| if (token) headers.Authorization = `Bearer ${token}`; | |
| return headers; | |
| } | |
| async function fetchJSON(url: string): Promise<any> { | |
| const res = await fetch(url, { | |
| headers: getGitHubHeaders(), | |
| signal: AbortSignal.timeout(15_000), | |
| }); | |
| if (!res.ok) throw new Error(`GitHub API ${res.status}: ${url}`); | |
| return res.json(); | |
| } | |
| async function cachedFetch(url: string): Promise<any> { | |
| const cached = releasesCache.get(url); | |
| if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return cached.data; | |
| const data = await fetchJSON(url); | |
| releasesCache.set(url, { data, ts: Date.now() }); | |
| return data; | |
| } | |
| interface ReleaseInfo { | |
| tag: string; | |
| version: string; | |
| assets: { name: string; url: string; size: number }[]; | |
| publishedAt: string; | |
| body: string; | |
| } | |
| function parseRelease(raw: any): ReleaseInfo { | |
| return { | |
| tag: raw.tag_name, | |
| version: raw.tag_name.replace(/^v/, ""), | |
| assets: (raw.assets || []).map((a: any) => ({ | |
| name: a.name, | |
| url: a.browser_download_url, | |
| size: a.size, | |
| })), | |
| publishedAt: raw.published_at, | |
| body: raw.body || "", | |
| }; | |
| } | |
| export async function getLatestRelease(): Promise<ReleaseInfo> { | |
| const raw = await cachedFetch( | |
| "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest" | |
| ); | |
| return parseRelease(raw); | |
| } | |
| export async function getReleaseByVersion(version: string): Promise<ReleaseInfo | null> { | |
| const tag = version.startsWith("v") ? version : `v${version}`; | |
| try { | |
| const raw = await cachedFetch( | |
| `https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/tags/${tag}` | |
| ); | |
| return parseRelease(raw); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export async function getAvailableVersions(): Promise<string[]> { | |
| const raw = await cachedFetch( | |
| "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases?per_page=30" | |
| ); | |
| return (Array.isArray(raw) ? raw : []).map((r: any) => r.tag_name); | |
| } | |
| export async function getChecksums(version: string): Promise<Map<string, string>> { | |
| const tag = version.startsWith("v") ? version : `v${version}`; | |
| const url = `https://github.com/router-for-me/CLIProxyAPI/releases/download/${tag}/checksums.txt`; | |
| try { | |
| const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); | |
| if (!res.ok) return new Map(); | |
| const text = await res.text(); | |
| const map = new Map<string, string>(); | |
| for (const line of text.split("\n")) { | |
| const parts = line.trim().split(/\s+/); | |
| if (parts.length >= 2) { | |
| map.set(parts[1], parts[0]); | |
| } | |
| } | |
| return map; | |
| } catch { | |
| return new Map(); | |
| } | |
| } | |
| export function clearCache(): void { | |
| releasesCache.clear(); | |
| } | |