mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-13 07:43:43 +08:00
222 lines
5.7 KiB
TypeScript
222 lines
5.7 KiB
TypeScript
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<AppReleaseEntry>;
|
|
|
|
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<AppReleaseManifest>;
|
|
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<T>(url: string): Promise<T | null> {
|
|
const response = await fetch(url, {
|
|
cache: 'no-store',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
function buildResponse(
|
|
repository: string,
|
|
branch: string,
|
|
overrides: Partial<AppUpdateResponse> = {},
|
|
): 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<unknown>(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 },
|
|
);
|
|
}
|
|
}
|