import { parseVersion, checkUpdate } from '@yuzu-gca/shared'; import Constants from 'expo-constants'; import * as FileSystem from 'expo-file-system'; import { NativeModules, Platform } from 'react-native'; // ============================================================ // 1. 版本号读取 // ============================================================ export function getLocalVersion(): string { return Constants.expoConfig?.version ?? '0.1.0'; } export function getLocalBuildTag(): string { const v = getLocalVersion(); const versionCode = Constants.expoConfig?.android?.versionCode ?? 1; return `v${v}-dav-android-${versionCode}`; } // ============================================================ // 2. JS Bundle OTA(expo-updates 自动处理) // ============================================================ export async function checkJsBundleUpdate(): Promise<{ hasUpdate: boolean; remoteVersion: string; }> { const localVersion = getLocalVersion(); const localTag = getLocalBuildTag(); const local = parseVersion(localTag); if (!local) return { hasUpdate: false, remoteVersion: localVersion }; const result = await checkUpdate('android', local); if (!result.shouldUpdate || !result.remoteVersion) { return { hasUpdate: false, remoteVersion: localVersion }; } return { hasUpdate: true, remoteVersion: result.remoteVersion.raw }; } // ============================================================ // 3. APK 静默安装(大更新) // ============================================================ const ALLOWED_DOWNLOAD_HOSTS = [ 'git.childish-ghost.com', 'github.com', ]; const { PackageInstallerModule } = NativeModules; function validateUrl(url: string): void { try { const host = new URL(url).hostname; if (!ALLOWED_DOWNLOAD_HOSTS.some(h => host === h || host.endsWith('.' + h))) { throw new Error(`不允许的下载域名: ${host}`); } } catch (e) { if (e instanceof TypeError) throw new Error('下载URL格式无效'); throw e; } } export async function downloadAndInstallApk(apkUrl: string, sha256: string): Promise { validateUrl(apkUrl); const localPath = `${FileSystem.cacheDirectory ?? ''}gca-update.apk`; const download = FileSystem.createDownloadResumable( apkUrl, localPath, {}, (progress) => { const pct = (progress.totalBytesWritten / progress.totalBytesExpectedToWrite) * 100; console.log(`[OTA] 下载进度: ${pct.toFixed(0)}%`); }, ); const result = await download.downloadAsync(); if (!result?.uri) throw new Error('APK 下载失败'); if (Platform.OS === 'android' && PackageInstallerModule) { // SHA256 校验在原生层完成(避免 TOCTOU + 密码学误用) await PackageInstallerModule.installApk(result.uri, sha256); console.log('[OTA] SHA256校验通过,APK已提交安装'); } else { throw new Error('PackageInstaller 原生模块不可用'); } } // ============================================================ // 4. 启动时一键检查 // ============================================================ export async function onAppStartCheckUpdate(): Promise<{ jsUpdate: boolean; apkUpdate: boolean; }> { const localTag = getLocalBuildTag(); const local = parseVersion(localTag); if (!local) return { jsUpdate: false, apkUpdate: false }; const result = await checkUpdate('android', local); if (!result.shouldUpdate || !result.build) { return { jsUpdate: false, apkUpdate: false }; } if (result.build.type === 'apk') { await downloadAndInstallApk(result.build.url, result.build.sha256); return { jsUpdate: false, apkUpdate: true }; } return { jsUpdate: true, apkUpdate: false }; }