新增: 应用内版本更新与自动升级全链路

后端 update.rs: app_check_update(拉 latest.json+ed25519 签名校验) /
app_download_install_update / prefs 读写 / mark_checked / mark_notified 五命令,
lib.rs 注册;tauri.conf updater 段(endpoints=app.1216.top/download/devflow/latest.json
+ pubkey + createUpdaterArtifacts) + tauri-plugin-updater 依赖 + capabilities 权限。

前端 UpdateBadge.vue(顶栏徽章+下拉) / useUpdateCheck.ts(检查/下载/安装状态机) /
AboutPanel.vue(设置页关于面板) + AppLayout 接线 + SettingsNav/searchIndex 索引 +
i18n zh/en settings 键。

运维 scripts/publish-release.ps1(发版) + swap-install.ps1(换装校验) +
设计文档 版本更新与自动升级-设计-2026-10-09.md。

验收: cargo check -p devflow EXIT 0 + vue-tsc EXIT 0 + build EXIT 0。
This commit is contained in:
lxy
2026-10-09 22:53:14 +08:00
parent c70faa7bb8
commit a8c92e9619
27 changed files with 2021 additions and 24 deletions
+119
View File
@@ -0,0 +1,119 @@
// 应用内升级 — 自动检查调度(B2/L1)
//
// 设计:docs/02-架构设计/专项设计/版本更新与自动升级-设计-2026-10-09.md §四 L1
//
// 策略(C4 定稿):启动后延迟 3s 首查 → 此后按 interval_minutes(默认 15)周期查。
// 去重:同一版本号只提示一次(KV df-update-notified-version),「稍后」后不再重复弹。
// 只提示不自动装(安装是高风险动作,必须人点头)。
//
// 实现要点:
// - 单例调度:模块级 timer,防多组件重复启动。
// - 节流判定:读 KV last_check + interval,未到间隔跳过(防每次启动都拉)。
// - 更新可用时广播 window 事件 'app-update-available',TopBar 徽章/AboutPanel 消费。
// - 停用:auto_check=false 时不启动。
import { settingsApi } from '@/api'
/** 更新可用事件载荷 */
export interface UpdateAvailablePayload {
version: string
notes: string | null
date: string | null
}
let timer: ReturnType<typeof setTimeout> | null = null
let started = false
/** 首查延迟(ms)—— 设计 C4:启动后 3 秒 */
const FIRST_CHECK_DELAY_MS = 3000
/**
* 执行一次自动检查(含节流判定)。
* - auto_check=false → 跳过
* - 距上次检查不足 interval → 跳过
* - 有新版且未提示过该版本 → 广播 app-update-available
*/
async function runAutoCheck(): Promise<void> {
try {
const prefs = await settingsApi.getUpdatePrefs()
if (!prefs.auto_check) return
// 节流:距上次检查不足 interval 则跳过
if (prefs.last_check) {
const last = Date.parse(prefs.last_check)
if (!Number.isNaN(last)) {
const elapsedMin = (Date.now() - last) / 60000
if (elapsedMin < prefs.interval_minutes) return
}
}
const r = await settingsApi.checkUpdate()
const now = new Date().toISOString()
void settingsApi.markUpdateChecked(now)
if (r.has_update && r.version) {
// 同版本只提示一次
if (prefs.notified_version === r.version) return
void settingsApi.markUpdateNotified(r.version)
const payload: UpdateAvailablePayload = {
version: r.version,
notes: r.notes,
date: r.date,
}
window.dispatchEvent(new CustomEvent('app-update-available', { detail: payload }))
}
} catch {
// 网络失败静默:自动检查不打扰用户(手动检查才报错)
}
}
/** 调度下一次检查(按 interval_minutes) */
async function scheduleNext(): Promise<void> {
let intervalMin = 15
try {
const prefs = await settingsApi.getUpdatePrefs()
intervalMin = prefs.interval_minutes
} catch {
/* 用默认 15 */
}
timer = setTimeout(async () => {
await runAutoCheck()
await scheduleNext()
}, Math.max(1, intervalMin) * 60_000)
}
/**
* 启动自动检查调度(幂等:重复调用只启动一次)。
* 在 App.vue onMounted 调用。
*/
export function startUpdateAutoCheck(): void {
if (started) return
started = true
timer = setTimeout(async () => {
await runAutoCheck()
await scheduleNext()
}, FIRST_CHECK_DELAY_MS)
}
/** 停止调度(卸载/测试用) */
export function stopUpdateAutoCheck(): void {
if (timer) {
clearTimeout(timer)
timer = null
}
started = false
}
/**
* 偏好变更后重置调度(用户在关于页开关自动检查/改间隔后调用)。
* 关 → 停;开 → 立即重排。
*/
export async function resetUpdateAutoCheck(): Promise<void> {
stopUpdateAutoCheck()
try {
const prefs = await settingsApi.getUpdatePrefs()
if (prefs.auto_check) startUpdateAutoCheck()
} catch {
/* 静默 */
}
}