/** * 微信小程序正式版更新 + 版本/状态可见性。 * 新包就绪后强制重启;「我的」页可展示「已是最新 / 有新版本…」。 * * @see docs/superpowers/specs/2026-09-26-mp-weixin-force-update-design.md * @see docs/superpowers/specs/2026-09-26-mp-version-status-design.md */ const SHOW_DEBOUNCE_MS = 30 * 1000 const USER_CHECK_FALLBACK_MS = 2800 const ENV_LABEL = { release: '正式版', trial: '体验版', develop: '开发版' } let listenersBound = false let readyModalShown = false let failedModalShown = false let lastCheckAt = 0 let cachedUpdateManager = null let userToastUnsub = null let userFallbackTimer = null let snapshot = { status: 'idle', version: '', envVersion: '', envLabel: '', isRelease: false, hasUpdate: false } const listeners = new Set() function notify() { const copy = { ...snapshot } listeners.forEach((fn) => { try { fn(copy) } catch (e) { console.warn('[mpUpdate] listener error', e) } }) } function patchSnapshot(partial) { snapshot = { ...snapshot, ...partial } notify() } /** * @returns {{ version: string, envVersion: string, envLabel: string, isRelease: boolean }} */ export function getMpAccountInfo() { // #ifdef MP-WEIXIN try { if (typeof uni.getAccountInfoSync === 'function') { const mini = (uni.getAccountInfoSync() || {}).miniProgram || {} const envVersion = mini.envVersion || '' return { version: mini.version || '', envVersion, envLabel: ENV_LABEL[envVersion] || envVersion || '未知', isRelease: envVersion === 'release' } } } catch (e) { console.warn('[mpUpdate] getAccountInfoSync failed', e) } // #endif return { version: '', envVersion: '', envLabel: '', isRelease: false } } export function getMpUpdateSnapshot() { return { ...snapshot } } /** * @param {(s: typeof snapshot) => void} listener * @returns {() => void} */ export function subscribeMpUpdate(listener) { listeners.add(listener) listener({ ...snapshot }) return () => listeners.delete(listener) } function refreshAccountIntoSnapshot(statusOverride) { const account = getMpAccountInfo() const next = { version: account.version, envVersion: account.envVersion, envLabel: account.envLabel, isRelease: account.isRelease, hasUpdate: snapshot.hasUpdate, status: snapshot.status } if (!account.isRelease) { next.status = account.envVersion ? 'unsupported' : snapshot.status next.hasUpdate = false } else if (statusOverride) { next.status = statusOverride } else if (snapshot.status === 'unsupported') { next.status = 'idle' } patchSnapshot(next) } function showReadyModal(updateManager) { if (readyModalShown) return readyModalShown = true uni.showModal({ title: '更新提示', content: '新版本已准备好,需要重启以使用最新功能', showCancel: false, confirmText: '立即重启', success: (res) => { if (res.confirm) { updateManager.applyUpdate() } } }) } function clearUserFeedbackHooks() { if (userToastUnsub) { userToastUnsub() userToastUnsub = null } if (userFallbackTimer) { clearTimeout(userFallbackTimer) userFallbackTimer = null } } function toastForStatus(status) { if (status === 'latest') { uni.showToast({ title: '已是最新', icon: 'none' }) } else if (status === 'downloading') { uni.showToast({ title: '发现新版本,正在下载', icon: 'none' }) } else if (status === 'failed') { uni.showToast({ title: '更新失败,请稍后重试', icon: 'none' }) } } function feedbackFromKnownStatus() { const { status } = snapshot if (status === 'ready' && cachedUpdateManager) { readyModalShown = false showReadyModal(cachedUpdateManager) return true } if (status === 'latest' || status === 'downloading' || status === 'failed') { toastForStatus(status) return true } return false } function armUserCheckFeedback() { clearUserFeedbackHooks() const startedStatus = snapshot.status userToastUnsub = subscribeMpUpdate((s) => { if (s.status === startedStatus && s.status === 'checking') return if (s.status === 'latest' || s.status === 'downloading' || s.status === 'failed') { toastForStatus(s.status) clearUserFeedbackHooks() } else if (s.status === 'ready') { clearUserFeedbackHooks() } }) userFallbackTimer = setTimeout(() => { userFallbackTimer = null if (snapshot.status === 'checking') { // 微信未必再次回调:无 hasUpdate 信号时按「已是最新」收口 patchSnapshot({ status: 'latest', hasUpdate: false }) uni.showToast({ title: '已是最新', icon: 'none' }) } clearUserFeedbackHooks() }, USER_CHECK_FALLBACK_MS) } function bindUpdateListeners(updateManager) { if (listenersBound) return listenersBound = true updateManager.onCheckForUpdate((res) => { const hasUpdate = !!(res && res.hasUpdate) console.log('[mpUpdate] onCheckForUpdate hasUpdate=', hasUpdate) if (!snapshot.isRelease) { patchSnapshot({ status: 'unsupported', hasUpdate: false }) return } if (hasUpdate) { if (snapshot.status !== 'ready') { patchSnapshot({ status: 'downloading', hasUpdate: true }) } else { patchSnapshot({ hasUpdate: true }) } } else { patchSnapshot({ status: 'latest', hasUpdate: false }) } }) updateManager.onUpdateReady(() => { console.log('[mpUpdate] onUpdateReady') patchSnapshot({ status: 'ready', hasUpdate: true }) showReadyModal(updateManager) }) updateManager.onUpdateFailed(() => { console.log('[mpUpdate] onUpdateFailed') patchSnapshot({ status: 'failed', hasUpdate: snapshot.hasUpdate }) if (failedModalShown) return failedModalShown = true uni.showModal({ title: '更新失败', content: '新版本下载失败,请删除小程序后重新搜索打开,或检查网络后重试', showCancel: false }) }) } /** * @param {{ force?: boolean, fromUser?: boolean }} [options] */ export function checkMpUpdate(options = {}) { // #ifdef MP-WEIXIN const force = !!options.force const fromUser = !!options.fromUser refreshAccountIntoSnapshot() if (!snapshot.isRelease) { if (fromUser) { uni.showToast({ title: '开发/体验版无正式更新流', icon: 'none' }) } return } if (fromUser && feedbackFromKnownStatus()) { return } const now = Date.now() if (!force && lastCheckAt && now - lastCheckAt < SHOW_DEBOUNCE_MS) { return } lastCheckAt = now if (typeof uni.canIUse !== 'function' || !uni.canIUse('getUpdateManager')) { if (fromUser) { uni.showToast({ title: '当前基础库不支持检查更新', icon: 'none' }) } return } if (fromUser || snapshot.status === 'idle') { patchSnapshot({ status: 'checking' }) } const updateManager = uni.getUpdateManager() cachedUpdateManager = updateManager bindUpdateListeners(updateManager) if (fromUser) { armUserCheckFeedback() } // #endif } /** * @param {ReturnType} [s] */ export function formatMpUpdateLine(s) { const state = s || getMpUpdateSnapshot() if (state.status === 'unsupported' || (!state.isRelease && state.envVersion)) { const env = state.envLabel || '非正式版' return `${env} · 无正式更新流` } const ver = state.version ? `版本 ${state.version}` : '版本 —' const env = state.envLabel || '正式版' let tip = '点此检查更新' switch (state.status) { case 'checking': tip = '检查中…' break case 'latest': tip = '已是最新' break case 'downloading': tip = '有新版本(下载中)' break case 'ready': tip = '有新版本,点此更新' break case 'failed': tip = '更新失败' break default: tip = '点此检查更新' } return `${ver} · ${env} · ${tip}` } /** 是否需要用户关注更新(下载中 / 待重启 / 失败) */ export function isMpUpdateUrgent(s) { const status = (s || getMpUpdateSnapshot()).status return status === 'downloading' || status === 'ready' || status === 'failed' } refreshAccountIntoSnapshot()