import { APP_VERSION, compareVersions, getDefaultRepositoryBranch, getDefaultRepositorySlug, getReleaseByVersion, LOCAL_RELEASE_MANIFEST, } from '@/lib/app-release'; import type { AppReleaseEntry, AppReleaseManifest, AppUpdateResponse, } from '@/lib/types/app-update'; export const runtime = 'edge'; export const dynamic = 'force-dynamic'; const MANIFEST_PATH = 'app-release.json'; const CHANGELOG_PATH = 'CHANGELOG.md'; function parseRepositoryTarget(repository: string) { const [owner, name] = repository.split('/', 2); if (!owner || !name) { return null; } return { owner, name }; } function buildSourceInfo(repository: string, branch: string) { const target = parseRepositoryTarget(repository); if (!target) { const emptyUrl = 'https://github.com'; return { repository, branch, manifestUrl: emptyUrl, changelogUrl: emptyUrl, repositoryUrl: emptyUrl, }; } const { owner, name } = target; return { repository, branch, manifestUrl: `https://raw.githubusercontent.com/${owner}/${name}/${branch}/${MANIFEST_PATH}`, changelogUrl: `https://github.com/${owner}/${name}/blob/${branch}/${CHANGELOG_PATH}`, repositoryUrl: `https://github.com/${owner}/${name}`, }; } function isValidReleaseEntry(value: unknown): value is AppReleaseEntry { if (!value || typeof value !== 'object') { return false; } const entry = value as Partial; return ( typeof entry.version === 'string' && typeof entry.publishedAt === 'string' && typeof entry.title === 'string' && Array.isArray(entry.notes) && entry.notes.every((note) => typeof note === 'string') ); } function normalizeManifest( value: unknown, repository: string, branch: string, ): AppReleaseManifest | null { if (!value || typeof value !== 'object') { return null; } const manifest = value as Partial; const target = parseRepositoryTarget(repository); if (!target || typeof manifest.currentVersion !== 'string' || !Array.isArray(manifest.releases)) { return null; } const releases = manifest.releases.filter(isValidReleaseEntry); if (releases.length === 0) { return null; } return { repository: { owner: manifest.repository?.owner || target.owner, name: manifest.repository?.name || target.name, branch: manifest.repository?.branch || branch, }, currentVersion: manifest.currentVersion, releases, }; } async function fetchJson(url: string): Promise { const response = await fetch(url, { cache: 'no-store', headers: { Accept: 'application/json', }, }); if (!response.ok) { return null; } return response.json() as Promise; } function buildResponse( repository: string, branch: string, overrides: Partial = {}, ): AppUpdateResponse { const source = buildSourceInfo(repository, branch); const currentRelease = getReleaseByVersion(APP_VERSION); return { currentVersion: APP_VERSION, currentRelease, latestVersion: APP_VERSION, latestRelease: currentRelease, status: 'up-to-date', updateAvailable: false, checkedAt: new Date().toISOString(), checkedRemotely: false, usedRemoteManifest: false, source, ...overrides, }; } export async function GET() { const repository = process.env.UPDATE_REPOSITORY?.trim() || process.env.NEXT_PUBLIC_UPDATE_REPOSITORY?.trim() || getDefaultRepositorySlug(); const branch = process.env.UPDATE_BRANCH?.trim() || process.env.NEXT_PUBLIC_UPDATE_BRANCH?.trim() || getDefaultRepositoryBranch(); const source = buildSourceInfo(repository, branch); try { const remoteManifestJson = await fetchJson(source.manifestUrl); const remoteManifest = normalizeManifest(remoteManifestJson, repository, branch); if (remoteManifest) { const latestVersion = remoteManifest.currentVersion; const latestRelease = getReleaseByVersion(latestVersion, remoteManifest) ?? remoteManifest.releases[0] ?? null; const comparison = compareVersions(latestVersion, APP_VERSION); return Response.json( buildResponse(repository, branch, { latestVersion, latestRelease, status: comparison > 0 ? 'update-available' : comparison < 0 ? 'ahead-of-remote' : 'up-to-date', updateAvailable: comparison > 0, checkedRemotely: true, usedRemoteManifest: true, }), ); } const remotePackage = await fetchJson<{ version?: string }>(`${source.manifestUrl.replace(MANIFEST_PATH, 'package.json')}`); const latestVersion = remotePackage?.version?.trim(); if (latestVersion) { const comparison = compareVersions(latestVersion, APP_VERSION); return Response.json( buildResponse(repository, branch, { latestVersion, latestRelease: getReleaseByVersion(latestVersion, LOCAL_RELEASE_MANIFEST), status: comparison > 0 ? 'update-available' : comparison < 0 ? 'ahead-of-remote' : 'up-to-date', updateAvailable: comparison > 0, checkedRemotely: true, usedRemoteManifest: false, }), ); } return Response.json( buildResponse(repository, branch, { status: 'check-failed', error: '无法获取远程版本信息。', }), { status: 200 }, ); } catch (error) { return Response.json( buildResponse(repository, branch, { status: 'check-failed', error: error instanceof Error ? error.message : '未知错误', }), { status: 200 }, ); } }