import type { DownloadArch, DownloadOs } from "~/data/downloads"; // --- Типы GitHub API --- type ReleaseAsset = { name: string; browser_download_url: string; size: number; }; type GitHubRelease = { tag_name: string; name: string; body: string; published_at: string; assets: ReleaseAsset[]; }; // --- Типы нашего API --- type Variant = { url: string | null; platformKey: string | null; version: string | null }; type DownloadsApiResponse = { ok: boolean; source: "github-releases"; fetchedAt: string; version: string | null; notes: string | null; pubDate: string | null; variants: { macos: { arm64: Variant; x64: Variant; universal: Variant }; windows: { x64: Variant }; linux: { appimage: Variant; deb: Variant }; }; }; type ResolveResult = { url: string; version: string | null } | null; // --- Парсинг GitHub Release → наш формат --- const CACHE_KEY = "cat_releases"; const CACHE_TTL = 10 * 60 * 1000; // 10 минут const emptyVariant: Variant = { url: null, platformKey: null, version: null }; function findAsset(assets: ReleaseAsset[], pattern: RegExp): ReleaseAsset | null { return assets.find((a) => pattern.test(a.name)) || null; } function toVariant(asset: ReleaseAsset | null, version: string | null): Variant { if (!asset) return { ...emptyVariant }; return { url: asset.browser_download_url, platformKey: asset.name, version }; } function parseGitHubRelease(release: GitHubRelease): DownloadsApiResponse { const version = release.tag_name?.replace(/^v/, "") || null; const assets = (release.assets || []).filter( (a) => !a.name.endsWith(".sig") && !a.name.endsWith(".json") && !a.name.endsWith(".tar.gz") ); return { ok: assets.length > 0, source: "github-releases", fetchedAt: new Date().toISOString(), version, notes: release.body || null, pubDate: release.published_at || null, variants: { macos: { arm64: toVariant(findAsset(assets, /[-_]arm64\.dmg$/i), version), x64: toVariant(findAsset(assets, /[-_]x64\.dmg$/i), version), universal: { ...emptyVariant }, }, windows: { x64: toVariant( findAsset(assets, /[-_]Setup\.exe$/i) || findAsset(assets, /\.exe$/i) || findAsset(assets, /\.msi$/i), version ), }, linux: { appimage: toVariant(findAsset(assets, /\.AppImage$/i), version), deb: toVariant(findAsset(assets, /\.deb$/i), version), }, }, }; } // --- sessionStorage кеш --- function readCache(): DownloadsApiResponse | null { try { const raw = sessionStorage.getItem(CACHE_KEY); if (!raw) return null; const { ts, data } = JSON.parse(raw); if (Date.now() - ts > CACHE_TTL) return null; return data; } catch { return null; } } function writeCache(data: DownloadsApiResponse): void { try { sessionStorage.setItem(CACHE_KEY, JSON.stringify({ ts: Date.now(), data })); } catch { // sessionStorage может быть недоступен (private mode и т.д.) } } // --- Composable --- export const useReleaseDownloads = () => { const config = useRuntimeConfig(); const githubRepo = (config.public.githubRepo as string) || "777genius/agent-teams-ai"; const fallbackUrl = (config.public.githubReleasesUrl as string) || `https://github.com/${githubRepo}/releases`; // useAsyncData дедуплицирует запросы по ключу — все компоненты шарят один результат const { data, pending, error } = useAsyncData("releases", async () => { const cached = readCache(); if (cached) return cached; const release = await $fetch( `https://api.github.com/repos/${githubRepo}/releases/latest`, { headers: { Accept: "application/vnd.github+json" }, } ); const parsed = parseGitHubRelease(release); writeCache(parsed); return parsed; }, { server: false, lazy: true, }); const resolve = (os: DownloadOs, arch: DownloadArch | "unknown"): ResolveResult => { const api = data.value; if (!api?.ok) return null; if (os === "windows") { const v = api.variants.windows.x64; return v.url ? { url: v.url, version: v.version || api.version } : null; } if (os === "linux") { const v = api.variants.linux.appimage.url ? api.variants.linux.appimage : api.variants.linux.deb; return v.url ? { url: v.url, version: v.version || api.version } : null; } // macOS: сначала universal, потом по архитектуре if (os === "macos") { const universal = api.variants.macos.universal; if (universal.url) return { url: universal.url, version: universal.version || api.version }; const byArch = arch === "arm64" ? api.variants.macos.arm64 : api.variants.macos.x64; if (byArch.url) return { url: byArch.url, version: byArch.version || api.version }; const any = api.variants.macos.arm64.url ? api.variants.macos.arm64 : api.variants.macos.x64; return any.url ? { url: any.url, version: any.version || api.version } : null; } return null; }; const resolveUrlOrFallback = (os: DownloadOs, arch: DownloadArch | "unknown"): string => { return resolve(os, arch)?.url || fallbackUrl; }; return { data, pending, error, fallbackUrl, resolve, resolveUrlOrFallback }; };