import AsyncStorage from '@react-native-async-storage/async-storage' import { Linking, Platform } from 'react-native' import { apiRequest, getConfig, getUserId } from '@xuqm/rn-common' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' import { awaitInitialization } from '@xuqm/rn-common' // ─── Types ──────────────────────────────────────────────────────────────────── /** 插件注册元数据 */ export interface PluginMeta { /** 插件唯一标识,如 'buz1'、'buz2' */ moduleId: string /** 当前 bundle 版本号 */ version: string } /** * App 整包更新信息。 * * 对齐 Android SDK 的 UpdateInfo 模型。 * SDK 只返回数据,UI 由 app 层自行处理。 */ export interface AppUpdateInfo { /** 是否需要更新 */ needsUpdate: boolean /** 最新版本名,如 '2.1.0' */ versionName?: string /** 最新版本号(整数) */ versionCode?: number /** APK/安装包直接下载地址(Android 有此字段时优先下载) */ downloadUrl?: string /** 更新日志 */ changeLog?: string /** 是否强制更新(true 时不允许跳过) */ forceUpdate?: boolean /** iOS App Store 地址 */ appStoreUrl?: string /** Android 应用商店地址(华为/小米/OPPO 等) */ marketUrl?: string /** 服务端要求登录后才能检查(true 时 needsUpdate 通常为 false) */ requiresLogin?: boolean /** SDK 内部标记:APK 是否已下载到本地(仅 Android) */ alreadyDownloaded?: boolean /** APK 文件 SHA-256 校验值 */ apkHash?: string | null } /** * 插件(RN Bundle)更新信息。 * * 插件唯一标识 = appKey + platform + moduleId。 * - appKey:应用标识(来自配置文件) * - platform:ANDROID / IOS * - moduleId:插件 ID(如 'buz1') */ export interface PluginUpdateInfo { /** 是否需要更新 */ needsUpdate: boolean /** 最新版本号 */ latestVersion: string /** bundle 下载地址 */ downloadUrl: string /** bundle 文件 MD5 */ md5: string /** 要求的最低 common bundle 版本 */ minCommonVersion: string /** 更新说明 */ note: string } /** 已缓存的 bundle 元数据 */ export interface CachedRnBundle { moduleId: string version: string md5: string downloadedAt: string source: string } // ─── Internal ───────────────────────────────────────────────────────────────── const _pluginRegistry = new Map() function bundleCacheKey(moduleId: string) { return `@xuqm:bundle:${moduleId}` } 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 } // ─── UpdateSDK ──────────────────────────────────────────────────────────────── export const UpdateSDK = { // ── 插件注册 ────────────────────────────────────────────────────────────── /** * 注册插件元数据。在插件 bundle 入口文件顶部调用。 * * 插件唯一标识 = moduleId(如 'buz1')。 * 完整定位 = appKey(配置文件)+ platform(自动检测)+ moduleId。 * * @example * // src/plugins/buz1/bundle.ts * UpdateSDK.registerPlugin({ moduleId: 'buz1', version: '1.0.0' }) */ registerPlugin(meta: PluginMeta): void { _pluginRegistry.set(meta.moduleId, meta) }, /** * 获取已注册的插件版本号。 */ getRegisteredPluginVersion(moduleId: string): string | undefined { return _pluginRegistry.get(moduleId)?.version }, /** * 获取所有已注册的插件列表。 */ getRegisteredPlugins(): PluginMeta[] { return Array.from(_pluginRegistry.values()) }, // ── App 整包更新 ────────────────────────────────────────────────────────── /** * 检查 App 整包更新。 * * SDK 自动检测平台(iOS/Android)并传给服务端。 * 服务端根据平台返回对应的版本信息和下载/商店地址。 * * 对齐 Android SDK UpdateSDK.checkAppUpdate(): * - 返回完整更新信息,app 层自行决定 UI * - Android:有 downloadUrl 时下载 APK;无 downloadUrl 时用 marketUrl 跳转商店 * - iOS:用 appStoreUrl 跳转 App Store * * @param bypassIgnore false(默认)= 静默检查,跳过用户已忽略的版本 * true = 用户主动检查,不跳过 */ async checkAppUpdate(bypassIgnore?: boolean): Promise { await awaitInitialization() const config = getConfig() const currentVersionCode = getAppVersionCode() const userId = getUserId()?.trim() const params: Record = { appKey: config.appKey, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersionCode: String(currentVersionCode), } 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) } }, /** * 打开应用商店。 * iOS 使用 appStoreUrl,Android 使用 marketUrl。 */ async openStore(appStoreUrl?: string, marketUrl?: string): Promise { const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl if (url) await Linking.openURL(url) }, // ── 插件更新 ────────────────────────────────────────────────────────────── /** * 检查指定插件的更新。 * * 插件唯一标识 = appKey + platform + moduleId。 * 插件必须已通过 registerPlugin() 注册。 * * @param moduleId 插件 ID(如 'buz1') * @returns 更新信息,包含 latestVersion / downloadUrl / md5 等 * @throws 插件未注册时抛出错误 */ async checkPluginUpdate(moduleId: string): Promise { await awaitInitialization() const config = getConfig() const meta = _pluginRegistry.get(moduleId) if (!meta) { throw new Error( `[UpdateSDK] Plugin "${moduleId}" not registered. ` + 'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.', ) } const result = await apiRequest('/api/v1/rn/update/check', { skipAuth: true, params: { appKey: config.appKey, moduleId, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersion: meta.version, }, }) return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl } }, /** * 下载插件 bundle 源码文本。 * * @param downloadUrl 下载地址 * @param onProgress 下载进度回调 (0~1) */ async downloadPluginBundle(downloadUrl: string, onProgress?: (progress: number) => void): Promise { const response = await fetch(downloadUrl) if (!response.ok) throw new Error(`[UpdateSDK] Bundle 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(chunk => decoder.decode(chunk, { stream: true })).join('') + decoder.decode() }, /** * 缓存插件 bundle 到本地(AsyncStorage)。 */ async cachePluginBundle(moduleId: string, version: string, md5: string, source: string): Promise { const payload: CachedRnBundle = { moduleId, version, md5, source, downloadedAt: new Date().toISOString(), } await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(payload)) return payload }, /** * 读取已缓存的插件 bundle。 */ async getCachedPluginBundle(moduleId: string): Promise { const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)) return raw ? (JSON.parse(raw) as CachedRnBundle) : null }, /** * 检查并下载插件更新(一步完成)。 * * 如果有更新,下载 bundle 并缓存到本地。 * 调用方需要在下次启动时通过 reloadPlugin() 加载新版本。 * * @param moduleId 插件 ID * @param onProgress 下载进度回调 (0~1) * @returns 如果有更新,返回缓存的 bundle 信息;否则返回 null */ async checkAndCachePlugin(moduleId: string, onProgress?: (progress: number) => void): Promise { const info = await this.checkPluginUpdate(moduleId) if (!info.needsUpdate) return null const source = await this.downloadPluginBundle(info.downloadUrl, onProgress) return this.cachePluginBundle(moduleId, info.latestVersion, info.md5, source) }, // ── 版本信息 ────────────────────────────────────────────────────────────── /** 当前 App versionCode(原生读取) */ getAppVersionCode, /** 当前 App versionName(原生读取) */ getAppVersionName, /** * 开发环境手动设置版本号(仅调试用,生产环境不要调用)。 */ _devSetAppVersion(versionCode: number, versionName?: string): void { _devSetAppVersion(versionCode, versionName) }, }