mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
Part 3 of CLIProxyAPI integration (#902). Depends on PR1 + PR2. - releaseChecker.ts: GitHub releases API with 5min cache - binaryManager.ts: download, SHA256 verify, install, symlink, rollback - processManager.ts: spawn, graceful stop (SIGTERM→SIGKILL with interval cleanup), restart - healthMonitor.ts: periodic /v1/models health checks - index.ts: orchestrator (install/start/stop/restart/checkUpdates/pin/rollback) - 6 API routes under /api/version-manager/ with Zod validation - CliproxyapiToolCard: CLI Tools page card - Docker: cliproxyapi profile with sidecar service + healthcheck - 78 unit tests across 5 test files
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
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();
|
|
}
|