| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324 |
- /**
- * 微信小程序正式版更新 + 版本/状态可见性。
- * 新包就绪后强制重启;「我的」页可展示「已是最新 / 有新版本…」。
- *
- * @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<typeof getMpUpdateSnapshot>} [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()
|