新增: 应用内版本更新与自动升级全链路
后端 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:
@@ -13,6 +13,7 @@ pub mod project;
|
||||
pub mod services;
|
||||
pub mod settings;
|
||||
pub mod task;
|
||||
pub mod update;
|
||||
pub mod workflow;
|
||||
|
||||
/// 当前时间戳(毫秒字符串)— 与 df-storage 内部的时间格式保持一致
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
//! 应用内升级 IPC — B2/L1(检查更新 + 自动检查节流)
|
||||
//!
|
||||
//! 设计:docs/02-架构设计/专项设计/版本更新与自动升级-设计-2026-10-09.md §四 L1
|
||||
//!
|
||||
//! 职责边界:
|
||||
//! - `app_check_update`:拉远端 latest.json(Tauri updater 内建签名校验) → 返回是否有新版。
|
||||
//! 只读 + 外发网络,不改本地状态(节流时间戳由调用方按需写)。
|
||||
//! - `app_update_get_prefs` / `app_update_set_prefs`:自动检查偏好 KV 读写。
|
||||
//! - 自动检查节流判定:`should_auto_check` 依据 `df-update-last-check` 与间隔配置。
|
||||
//!
|
||||
//! 安装(下载+装+重启)在 B3,本模块不含。
|
||||
//!
|
||||
//! KV keys(复用既有 app_settings 体系,零新表):
|
||||
//! - `df-update-auto-check`:bool,默认 true
|
||||
//! - `df-update-check-interval-minutes`:int,默认 15
|
||||
//! - `df-update-last-check`:ISO 时间戳(节流用)
|
||||
//! - `df-update-notified-version`:string(同版本只提示一次)
|
||||
|
||||
use tauri::{AppHandle, State};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::err_str;
|
||||
|
||||
/// 检查更新结果(IPC 传输用)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct UpdateCheckResult {
|
||||
/// 是否有可用更新
|
||||
pub has_update: bool,
|
||||
/// 新版本号(无更新时 None)
|
||||
pub version: Option<String>,
|
||||
/// 更新说明(无更新时 None)
|
||||
pub notes: Option<String>,
|
||||
/// 发布日期(无更新时 None)
|
||||
pub date: Option<String>,
|
||||
/// 当前运行版本(便于前端展示对比)
|
||||
pub current_version: String,
|
||||
}
|
||||
|
||||
/// 自动检查偏好(IPC 传输用)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UpdatePrefs {
|
||||
/// 启动时自动检查更新
|
||||
pub auto_check: bool,
|
||||
/// 检查间隔(分钟)
|
||||
pub interval_minutes: u32,
|
||||
/// 上次检查时间(ISO 时间戳,空=从未)
|
||||
pub last_check: String,
|
||||
/// 已提示过的版本号(同版本只提示一次)
|
||||
pub notified_version: String,
|
||||
}
|
||||
|
||||
/// KV keys — 集中定义防拼写漂移
|
||||
const KEY_AUTO_CHECK: &str = "df-update-auto-check";
|
||||
const KEY_INTERVAL_MINUTES: &str = "df-update-check-interval-minutes";
|
||||
const KEY_LAST_CHECK: &str = "df-update-last-check";
|
||||
const KEY_NOTIFIED_VERSION: &str = "df-update-notified-version";
|
||||
|
||||
const DEFAULT_INTERVAL_MINUTES: u32 = 15;
|
||||
|
||||
/// 检查是否有新版本(只读 + 外发网络)。
|
||||
///
|
||||
/// 走 Tauri updater 的 `check()`:拉取 `tauri.conf.json` plugins.updater.endpoints
|
||||
/// 配置的 latest.json,内建 ed25519 签名校验(公钥在 pubkey)。无更新返回 `has_update=false`。
|
||||
/// 网络失败/解析失败返回 Err(前端据此提示,不谎报「已是最新」)。
|
||||
#[tauri::command]
|
||||
pub async fn app_check_update(app: AppHandle) -> Result<UpdateCheckResult, String> {
|
||||
let current_version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let updater = app.updater().map_err(err_str)?;
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => Ok(UpdateCheckResult {
|
||||
has_update: true,
|
||||
version: Some(update.version.clone()),
|
||||
notes: update.body.clone(),
|
||||
date: update.date.map(|d| d.to_string()),
|
||||
current_version,
|
||||
}),
|
||||
Ok(None) => Ok(UpdateCheckResult {
|
||||
has_update: false,
|
||||
version: None,
|
||||
notes: None,
|
||||
date: None,
|
||||
current_version,
|
||||
}),
|
||||
Err(e) => Err(format!("检查更新失败: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取自动检查偏好(缺省:auto_check=true / interval=15 / last_check 空 / notified 空)
|
||||
#[tauri::command]
|
||||
pub async fn app_update_get_prefs(state: State<'_, AppState>) -> Result<UpdatePrefs, String> {
|
||||
let auto_check = state
|
||||
.settings
|
||||
.get(KEY_AUTO_CHECK)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|v| v.trim_matches('"') != "false")
|
||||
.unwrap_or(true);
|
||||
let interval_minutes = state
|
||||
.settings
|
||||
.get(KEY_INTERVAL_MINUTES)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.and_then(|v| v.trim_matches('"').parse::<u32>().ok())
|
||||
.unwrap_or(DEFAULT_INTERVAL_MINUTES);
|
||||
let last_check = state
|
||||
.settings
|
||||
.get(KEY_LAST_CHECK)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|v| v.trim_matches('"').to_string())
|
||||
.unwrap_or_default();
|
||||
let notified_version = state
|
||||
.settings
|
||||
.get(KEY_NOTIFIED_VERSION)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|v| v.trim_matches('"').to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(UpdatePrefs {
|
||||
auto_check,
|
||||
interval_minutes,
|
||||
last_check,
|
||||
notified_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// 写自动检查偏好(仅 auto_check / interval_minutes;last_check / notified_version 由内部节流逻辑写)
|
||||
#[tauri::command]
|
||||
pub async fn app_update_set_prefs(
|
||||
state: State<'_, AppState>,
|
||||
auto_check: bool,
|
||||
interval_minutes: u32,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.settings
|
||||
.set(KEY_AUTO_CHECK, if auto_check { "true" } else { "false" })
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
state
|
||||
.settings
|
||||
.set(KEY_INTERVAL_MINUTES, &interval_minutes.to_string())
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 记录一次检查(写 last_check 时间戳)。前端检查后调用,供下次节流判定。
|
||||
#[tauri::command]
|
||||
pub async fn app_update_mark_checked(
|
||||
state: State<'_, AppState>,
|
||||
timestamp: String,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.settings
|
||||
.set(KEY_LAST_CHECK, ×tamp)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 记录已提示版本号(同版本只提示一次)。
|
||||
#[tauri::command]
|
||||
pub async fn app_update_mark_notified(
|
||||
state: State<'_, AppState>,
|
||||
version: String,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.settings
|
||||
.set(KEY_NOTIFIED_VERSION, &version)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ═══ B3:下载安装 + 进度事件 ═══
|
||||
// 设计 §四 L3 / §八 C2:下载+安装(passive 进度条窗口),成功即自动重启。
|
||||
// 前端走「立即升级」确认后调用本命令;过程经 `app-update-progress` 事件流回传。
|
||||
// 高危动作:前端已二次确认,本命令不再额外审批(纯应用内自升级,非工具调用)。
|
||||
|
||||
/// 下载安装进度事件载荷
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct UpdateProgress {
|
||||
/// 阶段:downloading(下载中)/ installing(安装中)
|
||||
pub stage: String,
|
||||
/// 已下载字节数(下载阶段有意义)
|
||||
pub downloaded: u64,
|
||||
/// 总字节数(下载阶段有意义;unknown 时 None)
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
/// 下载并安装更新(成功即自动重启)。
|
||||
///
|
||||
/// 流程:check() 复验版本 → download_and_install(带 on_chunk 进度回调 →
|
||||
/// emit `app-update-progress` 事件) → 安装完 Tauri 自动重启(passive 模式弹进度条)。
|
||||
///
|
||||
/// 失败(网络断/签名校验失败/安装失败)返回 Err,旧版不受影响(下载在临时目录,
|
||||
/// 校验通过才替换)。前端据此提示重试。
|
||||
#[tauri::command]
|
||||
pub async fn app_download_install_update(app: AppHandle) -> Result<(), String> {
|
||||
use tauri::Emitter;
|
||||
|
||||
let updater = app.updater().map_err(err_str)?;
|
||||
let update = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| format!("检查更新失败: {e}"))?
|
||||
.ok_or_else(|| "当前已是最新版本".to_string())?;
|
||||
|
||||
let app_handle = app.clone();
|
||||
let mut downloaded: u64 = 0;
|
||||
update
|
||||
.download_and_install(
|
||||
move |chunk_len, content_len| {
|
||||
downloaded += chunk_len as u64;
|
||||
let _ = app_handle.emit(
|
||||
"app-update-progress",
|
||||
UpdateProgress {
|
||||
stage: "downloading".to_string(),
|
||||
downloaded,
|
||||
total: content_len,
|
||||
},
|
||||
);
|
||||
},
|
||||
move || {
|
||||
// 下载完成,进入安装阶段
|
||||
let _ = app.emit(
|
||||
"app-update-progress",
|
||||
UpdateProgress {
|
||||
stage: "installing".to_string(),
|
||||
downloaded,
|
||||
total: None,
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("下载/安装更新失败: {e}"))?;
|
||||
|
||||
// 安装成功后 Tauri 自动触发重启(passive 模式),此处返回即终结
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user