import AsyncStorage from '@react-native-async-storage/async-storage' import { Linking, Platform } from 'react-native' import { apiRequest, getConfig, getUserId, _registerUserInfoHandler } from '@xuqm/rn-common' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' // ─── Types (public) ──────────────────────────────────────────────────────────── /** 插件注册(版本号由 SDK 自动获取,不需要 App 传入) */ export interface PluginRegistration { moduleId: string } /** @deprecated 使用 PluginRegistration(移除 version 字段) */ export interface PluginMeta extends PluginRegistration { version?: string } export interface AppUpdateInfo { needsUpdate: boolean versionName?: string versionCode?: number downloadUrl?: string changeLog?: string forceUpdate?: boolean appStoreUrl?: string marketUrl?: string requiresLogin?: boolean alreadyDownloaded?: boolean apkHash?: string | null } export interface PluginUpdateInfo { needsUpdate: boolean latestVersion: string currentVersion: string downloadUrl: string md5: string minCommonVersion: string note: string forceUpdate?: boolean changeLog?: string } export type { CachedRnBundle } interface CachedRnBundle { moduleId: string version: string md5: string downloadedAt: string source: string } // ─── Internal state ──────────────────────────────────────────────────────────── const _pluginRegistry = new Set() let _writeBundleCallback: ((moduleId: string, source: string) => Promise) | null = null let _reloadBundleCallback: ((moduleId: string) => Promise) | null = null function bundleCacheKey(moduleId: string) { return `@xuqm:bundle:${moduleId}` } async function _getCachedVersion(moduleId: string): Promise { const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)).catch(() => null) if (!raw) return '0.0.0' const cached = JSON.parse(raw) as CachedRnBundle return cached.version ?? '0.0.0' } function normalizeDownloadUrl(rawUrl?: string): string | undefined { if (!rawUrl) return rawUrl if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) { return rawUrl.replace('/api/v1/updates/api/v1/rn/files/', '/api/v1/rn/files/') } if (rawUrl.includes('/files/apk/')) { try { const url = new URL(rawUrl) if (url.pathname.startsWith('/files/apk/')) { return `${url.origin}/api/v1/updates${url.pathname}${url.search}` } } catch {} } return rawUrl } async function _downloadText(url: string, onProgress?: (p: number) => void): Promise { const response = await fetch(url) if (!response.ok) throw new Error(`[UpdateSDK] Download failed: ${response.status}`) if (!onProgress) return response.text() const contentLength = Number(response.headers.get('Content-Length') ?? '0') const reader = response.body?.getReader() if (!reader) return response.text() const chunks: Uint8Array[] = [] let received = 0 for (;;) { const { done, value } = await reader.read() if (done) break chunks.push(value) received += value.length if (contentLength > 0) onProgress(received / contentLength) } const decoder = new TextDecoder() return chunks.map(c => decoder.decode(c, { stream: true })).join('') + decoder.decode() } async function _downloadBinary(url: string, onProgress?: (p: number) => void): Promise { const response = await fetch(url) if (!response.ok) throw new Error(`[UpdateSDK] Download failed: ${response.status}`) if (!onProgress) return response.arrayBuffer() const contentLength = Number(response.headers.get('Content-Length') ?? '0') const reader = response.body?.getReader() if (!reader) return response.arrayBuffer() const chunks: Uint8Array[] = [] let received = 0 for (;;) { const { done, value } = await reader.read() if (done) break chunks.push(value) received += value.length if (contentLength > 0) onProgress(received / contentLength) } const totalLength = chunks.reduce((acc, c) => acc + c.length, 0) const combined = new Uint8Array(totalLength) let offset = 0 for (const chunk of chunks) { combined.set(chunk, offset) offset += chunk.length } return combined.buffer } // ─── 订阅 setUserInfo(userId 用于定向更新) ────────────────────────────────── _registerUserInfoHandler(() => { // updatePlugin 检查更新时会实时调用 getUserId(),此处无需额外操作 }) // ─── UpdateSDK ──────────────────────────────────────────────────────────────── export const UpdateSDK = { // ── 宿主注入(插件化写入/重载能力)────────────────────────────────────────── /** * 注入 bundle 写入和重载回调。 * 宿主在初始化时调用(如 src/core/updater.ts),注入 BundleRuntime 能力。 * * @example * UpdateSDK.setBundleCallbacks({ * writeBundle: writeBundleFile, * reloadBundle: loadBundle, * }) */ setBundleCallbacks(callbacks: { writeBundle: (moduleId: string, source: string) => Promise reloadBundle: (moduleId: string) => Promise }): void { _writeBundleCallback = callbacks.writeBundle _reloadBundleCallback = callbacks.reloadBundle }, // ── 插件注册 ────────────────────────────────────────────────────────────── /** * 批量注册插件。版本号由 SDK 自动从本地缓存获取,不需要 App 传入。 * * @example * UpdateSDK.registerPlugins([{ moduleId: 'buz1' }, { moduleId: 'buz2' }]) */ registerPlugins(plugins: PluginRegistration[]): void { for (const p of plugins) { _pluginRegistry.add(p.moduleId) } }, /** * 注册单个插件(向后兼容;version 字段已废弃,SDK 自动获取)。 */ registerPlugin(meta: PluginRegistration): void { _pluginRegistry.add(meta.moduleId) }, getRegisteredPlugins(): string[] { return Array.from(_pluginRegistry) }, // ── App 整包更新 ────────────────────────────────────────────────────────── async checkAppUpdate(bypassIgnore?: boolean): Promise { const config = getConfig() const params: Record = { appKey: config.appKey, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersionCode: String(getAppVersionCode()), } const userId = getUserId()?.trim() if (userId) params.userId = userId if (bypassIgnore) params.bypassIgnore = 'true' const result = await apiRequest('/api/v1/updates/app/check', { skipAuth: true, params, }) return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) } }, async openStore(appStoreUrl?: string, marketUrl?: string): Promise { const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl if (url) await Linking.openURL(url) }, /** * Android APK 直接下载并调起系统安装器。 */ async downloadAndInstallApk( downloadUrl: string, options?: { onProgress?: (progress: number) => void sha256?: string }, ): Promise { if (Platform.OS !== 'android') throw new Error('[UpdateSDK] APK install is Android-only.') // NativeApkInstall 由宿主提供(TurboModule),若未注入则 fallback 到 Linking try { // eslint-disable-next-line @typescript-eslint/no-require-imports const { XuqmApkInstall } = require('./NativeApkInstall') as { XuqmApkInstall: { installApk(url: string, sha256?: string | null): Promise } } await XuqmApkInstall.installApk(downloadUrl, options?.sha256 ?? null) } catch { // Native module not available — fallback to Linking await Linking.openURL(downloadUrl) } }, // ── 插件(Bundle)热更新 ────────────────────────────────────────────────── /** * 检查指定插件的更新。 * * 版本号由 SDK 自动从本地 AsyncStorage 缓存读取(首次为 0.0.0)。 * 插件必须先通过 registerPlugin / registerPlugins 注册。 */ async checkPluginUpdate(moduleId: string): Promise { if (!_pluginRegistry.has(moduleId)) { throw new Error( `[UpdateSDK] Plugin "${moduleId}" not registered. ` + 'Call UpdateSDK.registerPlugins([{ moduleId }]) first.', ) } const config = getConfig() const currentVersion = await _getCachedVersion(moduleId) const userId = getUserId()?.trim() const params: Record = { appKey: config.appKey, moduleId, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersion, } if (userId) params.userId = userId const result = await apiRequest('/api/v1/rn/update/check', { skipAuth: true, params, }) return { ...result, currentVersion, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl, } }, /** * 执行插件更新(一步完成)。 * * SDK 内部自动:检查版本 → 下载 bundle → 写入文件系统 → 触发重载(silent = false 时)。 * * 需先调用 setBundleCallbacks 注入写入/重载能力。 * * @param silent true = 静默更新,下次启动生效;false(默认)= 立即重载 */ async updatePlugin( moduleId: string, options?: { onProgress?: (progress: number) => void silent?: boolean }, ): Promise { const info = await UpdateSDK.checkPluginUpdate(moduleId) if (!info.needsUpdate) return const source = await _downloadText(info.downloadUrl, options?.onProgress) // 写入 AsyncStorage 缓存(版本记录) const cachePayload: CachedRnBundle = { moduleId, version: info.latestVersion, md5: info.md5, source, downloadedAt: new Date().toISOString(), } await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(cachePayload)) // 写入宿主文件系统(需注入 setBundleCallbacks) if (_writeBundleCallback) { await _writeBundleCallback(moduleId, source) } // 触发重载(非静默模式) if (!options?.silent && _reloadBundleCallback) { await _reloadBundleCallback(moduleId) } }, // ── 版本信息 ────────────────────────────────────────────────────────────── getAppVersionCode, getAppVersionName, _devSetAppVersion(versionCode: number, versionName?: string): void { _devSetAppVersion(versionCode, versionName) }, }