mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-22 17:13:05 +08:00
300 lines
8.8 KiB
TypeScript
300 lines
8.8 KiB
TypeScript
import { dialog, app } from 'electron'
|
||
import { autoUpdater } from 'electron-updater'
|
||
import { platform } from '@electron-toolkit/utils'
|
||
import { logger } from './logger'
|
||
import { getActiveProxyUrl } from './network/proxy'
|
||
import { closeWorkerAsync } from './worker/workerManager'
|
||
import { t } from './i18n'
|
||
|
||
type AppWithQuitFlag = typeof app & { isQuiting?: boolean }
|
||
// 更新安装流程会主动触发退出,这里使用类型扩展存储退出标记。
|
||
const appWithQuitFlag = app as AppWithQuitFlag
|
||
|
||
// R2 镜像源 URL(速度更快,作为主要更新源)
|
||
const R2_MIRROR_URL = 'https://chatlab.1app.top/releases/download'
|
||
|
||
// 更新源类型
|
||
type UpdateSource = 'github' | 'r2'
|
||
|
||
// 当前使用的更新源(默认 R2 优先)
|
||
let currentSource: UpdateSource = 'r2'
|
||
|
||
// 是否已尝试过备用源
|
||
let hasTriedFallback = false
|
||
|
||
/**
|
||
* 配置自动更新的代理设置
|
||
* electron-updater 通过环境变量读取代理配置
|
||
*/
|
||
function configureUpdateProxy(): void {
|
||
const proxyUrl = getActiveProxyUrl()
|
||
|
||
if (proxyUrl) {
|
||
// 设置环境变量,electron-updater 会自动读取
|
||
process.env.HTTPS_PROXY = proxyUrl
|
||
process.env.HTTP_PROXY = proxyUrl
|
||
logger.info(`[Update] Using proxy: ${proxyUrl}`)
|
||
} else {
|
||
// 清除代理环境变量
|
||
delete process.env.HTTPS_PROXY
|
||
delete process.env.HTTP_PROXY
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换到 R2 镜像源
|
||
*/
|
||
function switchToR2Mirror(): void {
|
||
currentSource = 'r2'
|
||
autoUpdater.setFeedURL({
|
||
provider: 'generic',
|
||
url: R2_MIRROR_URL,
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 切换到 GitHub 源(备用更新源)
|
||
*/
|
||
function switchToGitHub(): void {
|
||
currentSource = 'github'
|
||
// 使用 GitHub 作为 generic provider
|
||
autoUpdater.setFeedURL({
|
||
provider: 'github',
|
||
owner: 'hellodigua',
|
||
repo: 'ChatLab',
|
||
})
|
||
logger.info('[Update] Switched to GitHub fallback source')
|
||
}
|
||
|
||
/**
|
||
* 重置为默认更新源(R2 优先)
|
||
*/
|
||
function resetToDefaultSource(): void {
|
||
hasTriedFallback = false
|
||
switchToR2Mirror()
|
||
}
|
||
|
||
/**
|
||
* 判断错误是否为网络相关错误
|
||
*/
|
||
function isNetworkError(error: Error): boolean {
|
||
const networkErrorKeywords = [
|
||
'ECONNREFUSED',
|
||
'ENOTFOUND',
|
||
'ETIMEDOUT',
|
||
'ECONNRESET',
|
||
'ENETUNREACH',
|
||
'EAI_AGAIN',
|
||
'socket hang up',
|
||
'network',
|
||
'connect',
|
||
'timeout',
|
||
'getaddrinfo',
|
||
]
|
||
const errorMessage = error.message?.toLowerCase() || ''
|
||
const errorCode = (error as NodeJS.ErrnoException).code?.toLowerCase() || ''
|
||
|
||
return networkErrorKeywords.some(
|
||
(keyword) => errorMessage.includes(keyword.toLowerCase()) || errorCode.includes(keyword.toLowerCase())
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 判断版本号是否为预发布版本
|
||
* 预发布版本格式:0.3.0-beta.1, 0.4.2-alpha.23, 1.0.0-rc.1 等
|
||
* 标准版本格式:0.3.0, 1.0.0, 2.1.3 等
|
||
*/
|
||
function isPreReleaseVersion(version: string): boolean {
|
||
// 预发布版本包含连字符后跟预发布标识(alpha, beta, rc, dev, canary 等)
|
||
return /-/.test(version)
|
||
}
|
||
|
||
let isFirstShow = true
|
||
// 标记是否为手动检查更新(手动检查时即使是预发布版本也显示弹窗)
|
||
let isManualCheck = false
|
||
const checkUpdate = (win) => {
|
||
// 配置代理
|
||
configureUpdateProxy()
|
||
|
||
autoUpdater.autoDownload = false // 自动下载
|
||
autoUpdater.autoInstallOnAppQuit = true // 应用退出后自动安装
|
||
|
||
// 开发模式下模拟更新检测(需要创建 dev-app-update.yml 文件)
|
||
// 取消下面的注释来启用开发模式更新测试
|
||
// if (!app.isPackaged) {
|
||
// Object.defineProperty(app, 'isPackaged', {
|
||
// get() {
|
||
// return true
|
||
// },
|
||
// })
|
||
// }
|
||
|
||
let showUpdateMessageBox = false
|
||
autoUpdater.on('update-available', (info) => {
|
||
// win.webContents.send('show-message', 'electron:发现新版本')
|
||
if (showUpdateMessageBox) return
|
||
|
||
// 检查是否为预发布版本
|
||
const isPreRelease = isPreReleaseVersion(info.version)
|
||
|
||
// 预发布版本仅在手动检查时显示更新弹窗
|
||
if (isPreRelease && !isManualCheck) {
|
||
console.log(`[Update] Pre-release version found: ${info.version}, skipping auto-update prompt`)
|
||
logger.info(
|
||
`[Update] Pre-release version found: ${info.version}, skipping auto-update prompt (manual check required)`
|
||
)
|
||
return
|
||
}
|
||
|
||
showUpdateMessageBox = true
|
||
|
||
dialog
|
||
.showMessageBox({
|
||
title: t('update.newVersionTitle', { version: info.version }),
|
||
message: t('update.newVersionMessage', { version: info.version }),
|
||
detail: t('update.newVersionDetail'),
|
||
buttons: [t('update.downloadNow'), t('update.cancel')],
|
||
defaultId: 0,
|
||
cancelId: 1,
|
||
type: 'question',
|
||
noLink: true,
|
||
})
|
||
.then((result) => {
|
||
showUpdateMessageBox = false
|
||
if (result.response === 0) {
|
||
autoUpdater
|
||
.downloadUpdate()
|
||
.then(() => {
|
||
console.log('wait for post download operation')
|
||
})
|
||
.catch((downloadError) => {
|
||
// 下载失败记录到日志,不显示给用户
|
||
logger.error(`[Update] Download update failed: ${downloadError}`)
|
||
})
|
||
}
|
||
})
|
||
})
|
||
|
||
// 监听下载进度事件
|
||
autoUpdater.on('download-progress', (progressObj) => {
|
||
console.log(`Update download progress: ${progressObj.percent}%`)
|
||
win.webContents.send('update-download-progress', progressObj.percent)
|
||
})
|
||
|
||
// 下载完成
|
||
autoUpdater.on('update-downloaded', () => {
|
||
dialog
|
||
.showMessageBox({
|
||
title: t('update.downloadComplete'),
|
||
message: t('update.readyToInstall'),
|
||
buttons: [t('update.install'), platform.isMacOS ? t('update.remindLater') : t('update.installOnQuit')],
|
||
defaultId: 1,
|
||
cancelId: 2,
|
||
type: 'question',
|
||
})
|
||
.then(async (result) => {
|
||
if (result.response === 0) {
|
||
win.webContents.send('begin-install')
|
||
appWithQuitFlag.isQuiting = true
|
||
|
||
// Windows 上先关闭 Worker 线程,确保进程能正常退出
|
||
// 否则 NSIS 安装器可能无法关闭旧进程
|
||
if (platform.isWindows) {
|
||
logger.info('[Update] Windows: Closing worker before installing...')
|
||
try {
|
||
await closeWorkerAsync()
|
||
} catch (error) {
|
||
logger.error(`[Update] Failed to close worker: ${error}`)
|
||
}
|
||
}
|
||
|
||
setTimeout(() => {
|
||
setImmediate(() => {
|
||
autoUpdater.quitAndInstall()
|
||
})
|
||
}, 100)
|
||
}
|
||
})
|
||
})
|
||
|
||
// 不需要更新
|
||
autoUpdater.on('update-not-available', (_info) => {
|
||
// 客户端打开会默认弹一次,用isFirstShow来控制不弹
|
||
if (isFirstShow) {
|
||
isFirstShow = false
|
||
} else {
|
||
win.webContents.send('show-message', {
|
||
type: 'success',
|
||
message: t('update.upToDate'),
|
||
})
|
||
}
|
||
})
|
||
|
||
// 错误处理(智能切换备用源)
|
||
autoUpdater.on('error', (err) => {
|
||
logger.error(`[Update] Update error (${currentSource}): ${err.message || err}`)
|
||
|
||
// 如果是 R2 源且为网络错误,尝试切换到 GitHub 备用源
|
||
if (currentSource === 'r2' && !hasTriedFallback && isNetworkError(err)) {
|
||
hasTriedFallback = true
|
||
logger.info('[Update] R2 mirror failed, trying GitHub fallback...')
|
||
|
||
switchToGitHub()
|
||
|
||
// 延迟 1 秒后重试检查更新
|
||
setTimeout(() => {
|
||
autoUpdater.checkForUpdates().catch((retryErr) => {
|
||
logger.error(`[Update] GitHub fallback check also failed: ${retryErr}`)
|
||
})
|
||
}, 1000)
|
||
}
|
||
})
|
||
|
||
// 等待 3 秒再检查更新,确保窗口准备完成,用户进入系统
|
||
setTimeout(() => {
|
||
isManualCheck = false // 自动检查
|
||
resetToDefaultSource() // 重置为默认更新源(R2 优先)
|
||
|
||
autoUpdater.checkForUpdates().catch((err) => {
|
||
console.log('[Update] Update check failed:', err)
|
||
})
|
||
}, 3000)
|
||
}
|
||
|
||
/**
|
||
* 手动检查更新
|
||
* 手动检查时,即使是预发布版本也会显示更新弹窗
|
||
*/
|
||
const manualCheckForUpdates = () => {
|
||
// 配置代理
|
||
configureUpdateProxy()
|
||
|
||
isManualCheck = true // 手动检查
|
||
isFirstShow = false // 手动检查时,无论结果都显示提示
|
||
resetToDefaultSource() // 重置为默认更新源(R2 优先)
|
||
|
||
autoUpdater.checkForUpdates().catch((err) => {
|
||
console.log('[Update] Manual update check failed:', err)
|
||
logger.error(`[Update] Manual update check failed: ${err}`)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 模拟更新弹窗(仅用于开发测试)
|
||
* 控制台通过:window.api.app.simulateUpdate() 测试
|
||
*/
|
||
const simulateUpdateDialog = (_win) => {
|
||
dialog.showMessageBox({
|
||
title: t('update.newVersionTitle', { version: '9.9.9' }),
|
||
message: t('update.newVersionMessage', { version: '9.9.9' }),
|
||
detail: t('update.newVersionDetail'),
|
||
buttons: [t('update.downloadNow'), t('update.cancel')],
|
||
defaultId: 0,
|
||
cancelId: 1,
|
||
type: 'question',
|
||
noLink: true,
|
||
})
|
||
}
|
||
|
||
export { checkUpdate, simulateUpdateDialog, manualCheckForUpdates }
|