mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-16 09:19:37 +08:00
feat: 支持 GitHub 主源与自定义策略更新
This commit is contained in:
+23
-54
@@ -3,6 +3,7 @@ import { join } from 'path'
|
||||
import { readFileSync, existsSync, mkdirSync } from 'fs'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { DatabaseService } from './services/database'
|
||||
import { appUpdateService } from './services/appUpdateService'
|
||||
|
||||
import { wechatDecryptService } from './services/decryptService'
|
||||
import { ConfigService } from './services/config'
|
||||
@@ -61,29 +62,6 @@ autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载
|
||||
|
||||
/**
|
||||
* 比较两个语义化版本号
|
||||
* @param version1 版本1
|
||||
* @param version2 版本2
|
||||
* @returns version1 > version2 返回 true
|
||||
*/
|
||||
function isNewerVersion(version1: string, version2: string): boolean {
|
||||
const v1Parts = version1.split('.').map(Number)
|
||||
const v2Parts = version2.split('.').map(Number)
|
||||
|
||||
// 补齐版本号位数
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length)
|
||||
while (v1Parts.length < maxLength) v1Parts.push(0)
|
||||
while (v2Parts.length < maxLength) v2Parts.push(0)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
if (v1Parts[i] > v2Parts[i]) return true
|
||||
if (v1Parts[i] < v2Parts[i]) return false
|
||||
}
|
||||
|
||||
return false // 版本相同
|
||||
}
|
||||
|
||||
// 单例服务
|
||||
let dbService: DatabaseService | null = null
|
||||
|
||||
@@ -237,6 +215,12 @@ function createWindow() {
|
||||
|
||||
// 监听窗口关闭事件
|
||||
win.on('close', (event) => {
|
||||
const updateInfo = appUpdateService.getCachedUpdateInfo()
|
||||
if (updateInfo?.forceUpdate) {
|
||||
app.isQuitting = true
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是真正退出应用,不阻止
|
||||
if (app.isQuitting) {
|
||||
return
|
||||
@@ -1297,25 +1281,20 @@ function registerIpcHandlers() {
|
||||
})
|
||||
|
||||
ipcMain.handle('app:checkForUpdates', async () => {
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result && result.updateInfo) {
|
||||
const currentVersion = app.getVersion()
|
||||
const latestVersion = result.updateInfo.version
|
||||
return appUpdateService.checkForUpdates()
|
||||
})
|
||||
|
||||
// 使用语义化版本比较
|
||||
if (isNewerVersion(latestVersion, currentVersion)) {
|
||||
return {
|
||||
hasUpdate: true,
|
||||
version: latestVersion,
|
||||
releaseNotes: result.updateInfo.releaseNotes as string || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return { hasUpdate: false }
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
return { hasUpdate: false }
|
||||
ipcMain.handle('app:getUpdateState', async () => {
|
||||
return appUpdateService.getCachedUpdateInfo()
|
||||
})
|
||||
|
||||
ipcMain.handle('app:getUpdateSourceInfo', async () => {
|
||||
return {
|
||||
primaryUpdateSource: 'github' as const,
|
||||
githubRepository: appUpdateService.getGithubRepository(),
|
||||
policySources: ['github', 'custom'] as const,
|
||||
policyPrecedence: 'github' as const,
|
||||
forceUpdatePolicyFallbackUrl: appUpdateService.getForceUpdatePolicyFallbackUrl()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3708,19 +3687,9 @@ function checkForUpdatesOnStartup() {
|
||||
// 延迟3秒检测,等待窗口完全加载
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result && result.updateInfo) {
|
||||
const currentVersion = app.getVersion()
|
||||
const latestVersion = result.updateInfo.version
|
||||
|
||||
// 使用语义化版本比较
|
||||
if (isNewerVersion(latestVersion, currentVersion) && mainWindow) {
|
||||
// 通知渲染进程有新版本
|
||||
mainWindow.webContents.send('app:updateAvailable', {
|
||||
version: latestVersion,
|
||||
releaseNotes: result.updateInfo.releaseNotes || ''
|
||||
})
|
||||
}
|
||||
const result = await appUpdateService.checkForUpdates()
|
||||
if (result.hasUpdate && mainWindow) {
|
||||
mainWindow.webContents.send('app:updateAvailable', result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动时检查更新失败:', error)
|
||||
|
||||
+16
-1
@@ -71,6 +71,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'),
|
||||
getVersion: () => ipcRenderer.invoke('app:getVersion'),
|
||||
getMcpLaunchConfig: () => getMcpLaunchConfigSafe(),
|
||||
getUpdateState: () => ipcRenderer.invoke('app:getUpdateState'),
|
||||
getUpdateSourceInfo: () => ipcRenderer.invoke('app:getUpdateSourceInfo'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
|
||||
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
|
||||
getStartupDbConnected: () => ipcRenderer.invoke('app:getStartupDbConnected'),
|
||||
@@ -79,7 +81,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
ipcRenderer.on('app:downloadProgress', (_, progress) => callback(progress))
|
||||
return () => ipcRenderer.removeAllListeners('app:downloadProgress')
|
||||
},
|
||||
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => {
|
||||
onUpdateAvailable: (callback: (info: {
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
}) => void) => {
|
||||
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
|
||||
return () => ipcRenderer.removeAllListeners('app:updateAvailable')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { app } from 'electron'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
|
||||
const GITHUB_OWNER = 'ILoveBingLu'
|
||||
const GITHUB_REPO = 'CipherTalk'
|
||||
const GITHUB_FORCE_UPDATE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download/force-update.json`
|
||||
const FORCE_UPDATE_POLICY_FALLBACK_URL = 'https://miyuapp.aiqji.com'
|
||||
|
||||
export type ForceUpdateReason = 'minimum-version' | 'blocked-version'
|
||||
export type AppUpdateSource = 'github' | 'custom' | 'none'
|
||||
|
||||
export interface ForceUpdateManifest {
|
||||
schemaVersion: number
|
||||
latestVersion?: string
|
||||
minimumSupportedVersion?: string
|
||||
blockedVersions?: string[]
|
||||
title?: string
|
||||
message?: string
|
||||
releaseNotes?: string
|
||||
publishedAt?: string
|
||||
}
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: ForceUpdateReason
|
||||
checkedAt: number
|
||||
updateSource: AppUpdateSource
|
||||
policySource: AppUpdateSource
|
||||
}
|
||||
|
||||
type ManifestLookupResult = {
|
||||
manifest: ForceUpdateManifest | null
|
||||
source: AppUpdateSource
|
||||
}
|
||||
|
||||
function isNewerVersion(version1: string, version2: string): boolean {
|
||||
const v1Parts = version1.split('.').map(Number)
|
||||
const v2Parts = version2.split('.').map(Number)
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length)
|
||||
while (v1Parts.length < maxLength) v1Parts.push(0)
|
||||
while (v2Parts.length < maxLength) v2Parts.push(0)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
if (v1Parts[i] > v2Parts[i]) return true
|
||||
if (v1Parts[i] < v2Parts[i]) return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isVersionEqual(version1: string, version2: string): boolean {
|
||||
return !isNewerVersion(version1, version2) && !isNewerVersion(version2, version1)
|
||||
}
|
||||
|
||||
function normalizeReleaseNotes(value: unknown): string {
|
||||
if (!value) return ''
|
||||
if (typeof value === 'string') return value
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => {
|
||||
if (typeof item === 'string') return item
|
||||
if (item && typeof item === 'object' && 'note' in item) {
|
||||
return String((item as { note?: unknown }).note || '')
|
||||
}
|
||||
return String(item)
|
||||
}).filter(Boolean).join('\n\n')
|
||||
}
|
||||
if (value && typeof value === 'object' && 'note' in value) {
|
||||
return String((value as { note?: unknown }).note || '')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
async function fetchManifestFromUrl(url: string): Promise<ForceUpdateManifest | null> {
|
||||
try {
|
||||
const response = await fetch(`${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) return null
|
||||
|
||||
const data = await response.json() as ForceUpdateManifest
|
||||
if (!data || typeof data !== 'object') return null
|
||||
if (Number(data.schemaVersion || 0) < 1) return null
|
||||
return data
|
||||
} catch (error) {
|
||||
console.warn('[AppUpdate] 获取策略文件失败:', url, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveForceUpdateManifest(): Promise<ManifestLookupResult> {
|
||||
const githubManifest = await fetchManifestFromUrl(GITHUB_FORCE_UPDATE_URL)
|
||||
if (githubManifest) {
|
||||
return { manifest: githubManifest, source: 'github' }
|
||||
}
|
||||
|
||||
const fallbackUrl = `${FORCE_UPDATE_POLICY_FALLBACK_URL.replace(/\/+$/, '')}/force-update.json`
|
||||
const customManifest = await fetchManifestFromUrl(fallbackUrl)
|
||||
if (customManifest) {
|
||||
return { manifest: customManifest, source: 'custom' }
|
||||
}
|
||||
|
||||
return { manifest: null, source: 'none' }
|
||||
}
|
||||
|
||||
class AppUpdateService {
|
||||
private lastInfo: AppUpdateInfo | null = null
|
||||
|
||||
getCachedUpdateInfo(): AppUpdateInfo | null {
|
||||
return this.lastInfo
|
||||
}
|
||||
|
||||
getForceUpdatePolicyFallbackUrl(): string {
|
||||
return FORCE_UPDATE_POLICY_FALLBACK_URL
|
||||
}
|
||||
|
||||
getGithubRepository(): { owner: string; repo: string } {
|
||||
return {
|
||||
owner: GITHUB_OWNER,
|
||||
repo: GITHUB_REPO
|
||||
}
|
||||
}
|
||||
|
||||
private buildInfo(payload: Partial<AppUpdateInfo>): AppUpdateInfo {
|
||||
return {
|
||||
hasUpdate: false,
|
||||
forceUpdate: false,
|
||||
currentVersion: app.getVersion(),
|
||||
checkedAt: Date.now(),
|
||||
updateSource: 'none',
|
||||
policySource: 'none',
|
||||
...payload
|
||||
}
|
||||
}
|
||||
|
||||
async checkForUpdates(): Promise<AppUpdateInfo> {
|
||||
const currentVersion = app.getVersion()
|
||||
let latestVersion: string | undefined
|
||||
let releaseNotes = ''
|
||||
let hasUpdate = false
|
||||
let updateSource: AppUpdateSource = 'none'
|
||||
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result?.updateInfo?.version) {
|
||||
latestVersion = result.updateInfo.version
|
||||
releaseNotes = normalizeReleaseNotes(result.updateInfo.releaseNotes)
|
||||
hasUpdate = isNewerVersion(latestVersion, currentVersion)
|
||||
updateSource = hasUpdate ? 'github' : 'none'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AppUpdate] 检查 GitHub 更新失败:', error)
|
||||
}
|
||||
|
||||
const { manifest, source: policySource } = await resolveForceUpdateManifest()
|
||||
let forceUpdate = false
|
||||
let reason: ForceUpdateReason | undefined
|
||||
|
||||
if (manifest?.minimumSupportedVersion && isNewerVersion(manifest.minimumSupportedVersion, currentVersion)) {
|
||||
forceUpdate = true
|
||||
reason = 'minimum-version'
|
||||
} else if (manifest?.blockedVersions?.some((version) => isVersionEqual(currentVersion, version))) {
|
||||
forceUpdate = true
|
||||
reason = 'blocked-version'
|
||||
}
|
||||
|
||||
const finalVersion = latestVersion || manifest?.latestVersion
|
||||
const finalReleaseNotes = releaseNotes || manifest?.releaseNotes || ''
|
||||
|
||||
const info = this.buildInfo({
|
||||
hasUpdate: hasUpdate || forceUpdate,
|
||||
forceUpdate,
|
||||
currentVersion,
|
||||
version: finalVersion,
|
||||
releaseNotes: finalReleaseNotes,
|
||||
title: manifest?.title || (forceUpdate ? '必须更新到最新版本' : undefined),
|
||||
message: manifest?.message,
|
||||
minimumSupportedVersion: manifest?.minimumSupportedVersion,
|
||||
reason,
|
||||
updateSource,
|
||||
policySource
|
||||
})
|
||||
|
||||
this.lastInfo = info
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
export const appUpdateService = new AppUpdateService()
|
||||
Reference in New Issue
Block a user