Skip to content

Commit 856b72b

Browse files
committed
feat: 添加自动更新功能
1 parent 65a2256 commit 856b72b

7 files changed

Lines changed: 170 additions & 53 deletions

File tree

apps/electron/src/main/lib/github-release-service.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@ interface ReleaseCache {
2626

2727
let releaseCache: ReleaseCache | null = null
2828

29-
/** 缓存有效期(5 分钟) */
30-
const CACHE_TTL = 5 * 60 * 1000
29+
/** 单个 Release 缓存(按 tag) */
30+
const tagCache = new Map<string, { data: GitHubRelease; timestamp: number }>()
31+
32+
/** 缓存有效期(30 分钟) */
33+
const CACHE_TTL = 30 * 60 * 1000
34+
35+
/** Rate limit 冷却标记 */
36+
let rateLimitUntil = 0
3137

3238
/**
3339
* 从 GitHub API 获取 releases
@@ -36,6 +42,11 @@ const CACHE_TTL = 5 * 60 * 1000
3642
* @returns Release 数据
3743
*/
3844
async function fetchFromGitHub<T>(endpoint: string): Promise<T> {
45+
// Rate limit 冷却期内直接跳过
46+
if (Date.now() < rateLimitUntil) {
47+
throw new Error('GitHub API rate limit cooldown')
48+
}
49+
3950
const url = `${GITHUB_API_BASE}/repos/${GITHUB_REPO.owner}/${GITHUB_REPO.repo}${endpoint}`
4051

4152
console.log(`[GitHub Release] 正在请求: ${url}`)
@@ -47,6 +58,12 @@ async function fetchFromGitHub<T>(endpoint: string): Promise<T> {
4758
},
4859
})
4960

61+
if (response.status === 403 || response.status === 429) {
62+
// Rate limited — 冷却 15 分钟
63+
rateLimitUntil = Date.now() + 15 * 60 * 1000
64+
throw new Error('GitHub API rate limit exceeded, cooling down for 15 minutes')
65+
}
66+
5067
if (!response.ok) {
5168
const errorText = await response.text()
5269
throw new Error(
@@ -150,13 +167,24 @@ export async function listReleases(
150167
*/
151168
export async function getReleaseByTag(tag: string): Promise<GitHubRelease | null> {
152169
try {
170+
// 检查缓存
171+
const cached = tagCache.get(tag)
172+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
173+
return cached.data
174+
}
175+
153176
const release = await fetchFromGitHub<GitHubRelease>(
154177
`/releases/tags/${tag}`
155178
)
156179
console.log(`[GitHub Release] 获取 Release: ${tag}`)
180+
181+
tagCache.set(tag, { data: release, timestamp: Date.now() })
157182
return release
158183
} catch (error) {
159184
console.error(`[GitHub Release] 获取 Release ${tag} 失败:`, error)
185+
// 返回过期缓存
186+
const cached = tagCache.get(tag)
187+
if (cached) return cached.data
160188
return null
161189
}
162190
}

apps/electron/src/main/lib/updater/auto-updater.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
/**
22
* 自动更新核心模块
33
*
4-
* 仅检测新版本并通知用户,不做自动下载/安装。
5-
* 用户通过 GitHub Releases 页面手动下载覆盖安装。
6-
*
4+
* 检测新版本 → 自动后台下载 → 用户确认后重启安装。
75
* 仅在打包后的生产环境中工作。
86
*/
97

@@ -45,6 +43,11 @@ export async function checkForUpdates(): Promise<void> {
4543
}
4644
}
4745

46+
/** 退出并安装已下载的更新 */
47+
export function quitAndInstall(): void {
48+
autoUpdater.quitAndInstall(false, true)
49+
}
50+
4851
/** 清理更新器资源(定时器等) */
4952
export function cleanupUpdater(): void {
5053
if (checkInterval) {
@@ -61,17 +64,16 @@ export function cleanupUpdater(): void {
6164
export function initAutoUpdater(mainWindow: BrowserWindow): void {
6265
win = mainWindow
6366

64-
// 配置 electron-updater 日志,转发到 console
6567
autoUpdater.logger = {
6668
info: (...args: unknown[]) => console.log('[更新-updater]', ...args),
6769
warn: (...args: unknown[]) => console.warn('[更新-updater]', ...args),
6870
error: (...args: unknown[]) => console.error('[更新-updater]', ...args),
6971
debug: (...args: unknown[]) => console.log('[更新-updater:debug]', ...args),
7072
}
7173

72-
// 禁用自动下载和自动安装,仅做版本检测
73-
autoUpdater.autoDownload = false
74-
autoUpdater.autoInstallOnAppQuit = false
74+
// 自动下载,退出时自动安装
75+
autoUpdater.autoDownload = true
76+
autoUpdater.autoInstallOnAppQuit = true
7577

7678
// 监听更新事件
7779
autoUpdater.on('checking-for-update', () => {
@@ -90,6 +92,27 @@ export function initAutoUpdater(mainWindow: BrowserWindow): void {
9092
})
9193
})
9294

95+
autoUpdater.on('download-progress', (progress) => {
96+
setStatus({
97+
status: 'downloading',
98+
version: (currentStatus as { version?: string }).version || '',
99+
progress: {
100+
percent: progress.percent,
101+
transferred: progress.transferred,
102+
total: progress.total,
103+
bytesPerSecond: progress.bytesPerSecond,
104+
},
105+
})
106+
})
107+
108+
autoUpdater.on('update-downloaded', (info) => {
109+
console.log('[更新] 下载完成:', info.version)
110+
setStatus({
111+
status: 'downloaded',
112+
version: info.version,
113+
})
114+
})
115+
93116
autoUpdater.on('update-not-available', () => {
94117
console.log('[更新] 已是最新版本')
95118
setStatus({ status: 'not-available' })
@@ -124,5 +147,5 @@ export function initAutoUpdater(mainWindow: BrowserWindow): void {
124147
win = null
125148
})
126149

127-
console.log('[更新] 版本检测模块已初始化(仅检测,不自动下载/安装)')
150+
console.log('[更新] 自动更新模块已初始化(自动下载,用户确认后安装)')
128151
}

apps/electron/src/main/lib/updater/updater-ipc.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* 自动更新 IPC 处理器
33
*
44
* 注册更新相关的 IPC 通道,供渲染进程调用。
5-
* 仅支持检查更新和获取状态,不提供下载/安装功能。
65
*/
76

87
import { ipcMain } from 'electron'
@@ -11,27 +10,33 @@ import type { UpdateStatus } from './updater-types'
1110
import {
1211
checkForUpdates,
1312
getUpdateStatus,
13+
quitAndInstall,
1414
} from './auto-updater'
1515

1616
/** 注册更新 IPC 处理器 */
1717
export function registerUpdaterIpc(): void {
1818
console.log('[更新 IPC] 正在注册更新 IPC 处理器...')
1919

20-
// 检查更新
2120
ipcMain.handle(
2221
UPDATER_IPC_CHANNELS.CHECK_FOR_UPDATES,
2322
async (): Promise<void> => {
2423
await checkForUpdates()
2524
}
2625
)
2726

28-
// 获取当前更新状态
2927
ipcMain.handle(
3028
UPDATER_IPC_CHANNELS.GET_STATUS,
3129
async (): Promise<UpdateStatus> => {
3230
return getUpdateStatus()
3331
}
3432
)
3533

34+
ipcMain.handle(
35+
UPDATER_IPC_CHANNELS.QUIT_AND_INSTALL,
36+
(): void => {
37+
quitAndInstall()
38+
}
39+
)
40+
3641
console.log('[更新 IPC] 更新 IPC 处理器注册完成')
3742
}
Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
/**
22
* 自动更新相关类型定义
33
*
4-
* 仅检测新版本并通知用户,不做自动下载/安装。
5-
* 用户通过 GitHub Releases 页面手动下载覆盖安装。
4+
* 检测新版本 → 自动下载 → 用户确认后重启安装
65
*/
76

87
/** 更新状态 */
9-
export interface UpdateStatus {
10-
status: 'idle' | 'checking' | 'available' | 'not-available' | 'error'
11-
version?: string
12-
releaseNotes?: string
13-
error?: string
8+
export type UpdateStatus =
9+
| { status: 'idle' }
10+
| { status: 'checking' }
11+
| { status: 'available'; version: string; releaseNotes?: string }
12+
| { status: 'downloading'; version: string; progress: DownloadProgress }
13+
| { status: 'downloaded'; version: string }
14+
| { status: 'not-available' }
15+
| { status: 'error'; error: string }
16+
17+
/** 下载进度 */
18+
export interface DownloadProgress {
19+
/** 已下载百分比 0-100 */
20+
percent: number
21+
/** 已下载字节数 */
22+
transferred: number
23+
/** 总字节数 */
24+
total: number
25+
/** 下载速度(字节/秒) */
26+
bytesPerSecond: number
1427
}
1528

1629
/** 更新 IPC 通道常量 */
1730
export const UPDATER_IPC_CHANNELS = {
1831
CHECK_FOR_UPDATES: 'updater:check',
1932
GET_STATUS: 'updater:get-status',
2033
ON_STATUS_CHANGED: 'updater:status-changed',
34+
QUIT_AND_INSTALL: 'updater:quit-and-install',
2135
} as const

apps/electron/src/preload/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -651,23 +651,26 @@ export interface ElectronAPI {
651651
/** 设置默认提示词 */
652652
setDefaultPrompt: (id: string | null) => Promise<void>
653653

654-
// ===== 版本检测相关(仅检测,不自动下载/安装) =====
654+
// ===== 自动更新 =====
655655

656656
/** 更新 API */
657657
updater?: {
658658
checkForUpdates: () => Promise<void>
659659
getStatus: () => Promise<{
660-
status: 'idle' | 'checking' | 'available' | 'not-available' | 'error'
660+
status: 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'not-available' | 'error'
661661
version?: string
662662
releaseNotes?: string
663+
progress?: { percent: number; transferred: number; total: number; bytesPerSecond: number }
663664
error?: string
664665
}>
665666
onStatusChanged: (callback: (status: {
666-
status: 'idle' | 'checking' | 'available' | 'not-available' | 'error'
667+
status: 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'not-available' | 'error'
667668
version?: string
668669
releaseNotes?: string
670+
progress?: { percent: number; transferred: number; total: number; bytesPerSecond: number }
669671
error?: string
670672
}) => void) => () => void
673+
quitAndInstall: () => Promise<void>
671674
}
672675

673676
// GitHub Release
@@ -1548,7 +1551,7 @@ const electronAPI: ElectronAPI = {
15481551
return ipcRenderer.invoke(SYSTEM_PROMPT_IPC_CHANNELS.SET_DEFAULT, id)
15491552
},
15501553

1551-
// 自动更新(仅版本检测,不自动下载/安装)
1554+
// 自动更新
15521555
updater: {
15531556
checkForUpdates: () => ipcRenderer.invoke('updater:check'),
15541557
getStatus: () => ipcRenderer.invoke('updater:get-status'),
@@ -1557,6 +1560,7 @@ const electronAPI: ElectronAPI = {
15571560
ipcRenderer.on('updater:status-changed', listener)
15581561
return () => { ipcRenderer.removeListener('updater:status-changed', listener) }
15591562
},
1563+
quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'),
15601564
},
15611565

15621566
// GitHub Release

apps/electron/src/renderer/atoms/updater.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,35 @@
22
* 自动更新状态原子
33
*
44
* 管理应用更新状态,订阅主进程推送的更新事件。
5-
* 仅检测新版本并通知,不自动下载/安装。
65
* 优雅降级:如果 window.electronAPI.updater 不存在(开源构建),状态保持 idle。
76
*/
87

98
import { atom } from 'jotai'
109

10+
/** 下载进度 */
11+
export interface DownloadProgress {
12+
percent: number
13+
transferred: number
14+
total: number
15+
bytesPerSecond: number
16+
}
17+
1118
/** 更新状态 */
1219
export interface UpdateStatus {
13-
status: 'idle' | 'checking' | 'available' | 'not-available' | 'error'
20+
status: 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'not-available' | 'error'
1421
version?: string
1522
releaseNotes?: string
23+
progress?: DownloadProgress
1624
error?: string
1725
}
1826

1927
/** 更新状态 atom */
2028
export const updateStatusAtom = atom<UpdateStatus>({ status: 'idle' })
2129

22-
/** 是否有可用更新 */
30+
/** 是否有可用更新(包含已下载完成) */
2331
export const hasUpdateAtom = atom((get) => {
2432
const { status } = get(updateStatusAtom)
25-
return status === 'available'
33+
return status === 'available' || status === 'downloading' || status === 'downloaded'
2634
})
2735

2836
/** updater 是否可用 */

0 commit comments

Comments
 (0)