feat(android): OTA双轨更新 — shared版本号+Android Expo/PackageInstaller

- packages/shared: 版本号解析 vX.Y.Z-dav-{client}-{build} + manifest远程检查
- packages/client-android: Expo52 + expo-updates JS bundle OTA
- PackageInstallerModule.kt: APK静默安装原生模块(~60行Kotlin)
- monorepo: pnpm workspace + 版本号体系
This commit is contained in:
LukeMackin
2026-07-19 01:41:36 +08:00
parent b12aef0375
commit 9f8659c293
15 changed files with 472 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export { parseVersion, compareVersion, checkUpdate, semverGt } from './updater/index';
export type { GcaVersion, Manifest, ManifestBuild } from './updater/index';

View File

@@ -0,0 +1,82 @@
/** 版本号解析 vX.Y.Z-dav-{client}-{build} */
export interface GcaVersion {
major: number;
minor: number;
patch: number;
client: 'desktop' | 'android' | 'cli';
build: number;
raw: string;
}
export interface Manifest {
version: string;
builds: Record<string, ManifestBuild>;
}
export interface ManifestBuild {
tag: string;
url: string;
sha256: string;
size: number;
minVersion?: string;
}
const MANIFEST_URL = 'https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/manifest/manifest.json';
const VERSION_RE = /^v(\d+)\.(\d+)\.(\d+)-dav-(desktop|android|cli)-(\d+)$/;
export function parseVersion(raw: string): GcaVersion | null {
const m = raw.trim().match(VERSION_RE);
if (!m) return null;
return {
major: parseInt(m[1], 10),
minor: parseInt(m[2], 10),
patch: parseInt(m[3], 10),
client: m[4] as GcaVersion['client'],
build: parseInt(m[5], 10),
raw,
};
}
export function compareVersion(a: GcaVersion, b: GcaVersion): number {
if (a.major !== b.major) return a.major - b.major;
if (a.minor !== b.minor) return a.minor - b.minor;
if (a.patch !== b.patch) return a.patch - b.patch;
return a.build - b.build;
}
/** 调用方传入本地版本号,返回是否需要更新以及目标 manifest entry */
export async function checkUpdate(
client: GcaVersion['client'],
localVersion: GcaVersion,
manifestUrl = MANIFEST_URL,
): Promise<{ shouldUpdate: boolean; build: ManifestBuild | null; remoteVersion: GcaVersion | null }> {
try {
const resp = await fetch(manifestUrl);
if (!resp.ok) return { shouldUpdate: false, build: null, remoteVersion: null };
const manifest: Manifest = await resp.json();
const build = manifest.builds[client];
if (!build) return { shouldUpdate: false, build: null, remoteVersion: null };
const remoteVersion = parseVersion(build.tag);
if (!remoteVersion) return { shouldUpdate: false, build: null, remoteVersion: null };
return {
shouldUpdate: compareVersion(remoteVersion, localVersion) > 0,
build,
remoteVersion,
};
} catch {
return { shouldUpdate: false, build: null, remoteVersion: null };
}
}
/** SemVer 比较(用于 expo-updates manifest */
export function semverGt(a: string, b: string): boolean {
const [aM, am, aP] = a.split('.').map(Number);
const [bM, bm, bP] = b.split('.').map(Number);
if (aM !== bM) return aM > bM;
if (am !== bm) return am > bm;
return aP > bP;
}