feat(updater): 定时检查更新与后台自动下载

设置 → 系统新增两个开关:定时检查更新(从不/4/12/24 小时,默认 4)与
自动下载更新(默认关)。安装时机仍交给用户——绝不因下载完成自动退出,
autoInstallOnAppQuit 只覆盖用户自己退出时装上。

- runCheck(source) 统一三条来源(启动/周期/手动),UpdateState.scheduled
  标记来源;已有在途检查返回同一 promise,避免 electron-updater 的去重把
  来源标记记到错的调用方
- 周期调度器经 settingsStore.setUpdateScheduleApplier 注入(updater 已
  import settingsStore,反向 import 成环);幂等重排,启动检查由
  startupArmed 保证每进程只武装一次;所有 timer 带 unref
- 自动下载由 settings.autoDownloadUpdate 驱动,autoUpdater.autoDownload
  保持 false(单一事实源在 settings);失败不重试,等下一周期
- 同版本抑制(actedRelease 单槽,actedFor 版本不同即替换):否则长开机
  的应用每 4 小时重复弹气泡、重复走一遍 downloadUpdate;手动检查无条件
  清空记录
- downloaded 与 fetchHandled 分开:前者仅在 update-downloaded 置位,用于
  阻止后续 update-available 把状态从 downloaded 翻回 available(否则「关于」
  页按钮会从「重启安装」退回「下载」)
- tray.notifyUpdate 弹 Windows 托盘气泡;非 win32 / 无 tray / 有可见窗口
  都不弹
This commit is contained in:
Bill committed 2026-10-10 13:36:53 +08:00
1 parent d3529bb9dc
commit 9cc856fc8f
6 files changed
+371 -27

No files matched your search

+32
View File
@@ -7,6 +7,7 @@ import {
DEFAULT_SETTINGS,
isHighlightCategory,
isLockAutoDelay,
isUpdateCheckInterval,
lockAutoDelayOf,
type AppSettings,
type HighlightRule,
@@ -363,6 +364,12 @@ function deepMerge(raw: unknown): { settings: AppSettings; errors: string[] } {
globalShowHide,
closeAction,
autoCheckUpdate: candidate.autoCheckUpdate !== false,
// The interval is a whitelist (UPDATE_CHECK_INTERVALS): an arbitrary
// number here would silently rewrite how often we hit the update feed.
updateCheckIntervalHours: isUpdateCheckInterval(candidate.updateCheckIntervalHours)
? candidate.updateCheckIntervalHours
: DEFAULT_SYSTEM.updateCheckIntervalHours,
autoDownloadUpdate: candidate.autoDownloadUpdate === true,
// Both default on/off as in DEFAULT_SETTINGS; absent means "not chosen".
restoreSession: candidate.restoreSession !== false,
shellIntegration: candidate.shellIntegration === true,
@@ -395,6 +402,28 @@ function deepMerge(raw: unknown): { settings: AppSettings; errors: string[] } {
let sleepBlockerId: number | undefined
/**
* The updater's scheduler, injected rather than imported: `updater.ts` already
* imports this module, so importing it back would be a cycle — the timer would be
* armed against a half-initialised updater (and, depending on load order, on an
* `undefined` binding). `index.ts` wires this once with the updater's scheduler.
*/
let updateScheduleApplier: ((system: SystemSettings) => void) | null = null
/**
* Wire the updater's scheduler. The current settings are handed over immediately
* as well, so the schedule is armed no matter where in startup the caller wires
* this: `index.ts` wires the updater around `applyStartupSystemSettings`, and the
* order of the two is not part of the contract. `loadSettings` is a pure read — it
* never calls `applySystemSettings` — so applying here cannot recurse, and the
* extra call is idempotent: the applier is a plain "set the timer for this
* interval" step that already re-runs on every settings save.
*/
export function setUpdateScheduleApplier(fn: (system: SystemSettings) => void): void {
updateScheduleApplier = fn
fn(loadSettings().system)
}
/** Apply OS-level effects of the system settings (login item, sleep blocker). */
function applySystemSettings(system: SystemSettings): void {
// The main process renders its own strings (tray menu, dialogs), so it tracks
@@ -413,6 +442,9 @@ function applySystemSettings(system: SystemSettings): void {
}
// Re-register the global show/hide shortcut whenever system settings change.
applyGlobalShortcut(system.globalShowHide)
// Re-arm (or disarm) the periodic update check the same way. Routed through the
// injected applier instead of importing the updater, which would be a cycle.
updateScheduleApplier?.(system)
}
/** Apply system side effects for the settings loaded at startup. */