diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b26aaed..5ac4127 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -439,6 +439,7 @@ dependencies = [ "libc", "reqwest 0.12.28", "rusqlite", + "semver", "serde", "serde_json", "serde_norway", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cb21f9c..39b1e5e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ futures-util = "0.3" json5 = "0.4" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } rusqlite = { version = "0.32.1", features = ["bundled"] } +semver = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_norway = "0.9" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c2c6db0..61cf1f1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -26,6 +26,7 @@ const RELEASE_PAGE_URL: &str = "https://github.com/router-for-me/CLIProxyAPI/rel const RELEASE_ATOM_URL: &str = "https://github.com/router-for-me/CLIProxyAPI/releases.atom"; const RELEASE_DOWNLOAD_PREFIX: &str = "https://github.com/router-for-me/CLIProxyAPI/releases/download/"; +const APP_RELEASE_PAGE_URL: &str = "https://github.com/lzt404/Easy_CLIProxyAPI/releases/latest"; const CORE_INSTALL_PROGRESS_EVENT: &str = "core-install-progress"; const CORE_STATUS_EVENT: &str = "core-status-changed"; const CORE_METADATA_FILE: &str = "cpa-gui-meta.json"; @@ -54,6 +55,11 @@ const USER_AGENT: &str = concat!( env!("CARGO_PKG_VERSION"), " (+https://github.com/router-for-me/CLIProxyAPI)" ); +const APP_USER_AGENT: &str = concat!( + "Easy_CLIProxyAPI/", + env!("CARGO_PKG_VERSION"), + " (+https://github.com/lzt404/Easy_CLIProxyAPI)" +); static CORE_CONFIG_FILE_LOCK: Mutex<()> = Mutex::new(()); static AGENT_CONFIG_FILE_LOCK: Mutex<()> = Mutex::new(()); @@ -110,6 +116,15 @@ struct CoreLatest { asset_name: String, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct AppUpdateInfo { + current_version: String, + latest_version: String, + update_available: bool, + release_url: String, +} + #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] struct BundledCoreInfo { @@ -4629,6 +4644,45 @@ fn open_external_url(url: String) -> Result<(), String> { open_external_url_inner(&url) } +#[tauri::command] +async fn check_app_update() -> Result { + let client = reqwest::Client::builder() + .connect_timeout(Duration::from_secs(8)) + .read_timeout(Duration::from_secs(15)) + .timeout(Duration::from_secs(20)) + .build() + .map_err(|error| format!("创建版本检查客户端失败: {error}"))?; + let response = client + .get(APP_RELEASE_PAGE_URL) + .header(reqwest::header::ACCEPT, "text/html,application/xhtml+xml") + .header(reqwest::header::USER_AGENT, APP_USER_AGENT) + .send() + .await + .map_err(|error| format!("检查软件更新失败: {error}"))?; + let status = response.status(); + let final_url = response.url().clone(); + if !status.is_success() { + let body = response + .text() + .await + .map_err(|error| format!("读取软件更新响应失败: {error}"))?; + return Err(format_github_error(status.as_u16(), &body)); + } + + let latest_version = release_tag_from_url(&final_url) + .map(|version| normalize_version(&version)) + .ok_or_else(|| "GitHub latest 页面没有返回正式版本标签".to_string())?; + let current_version = normalize_version(env!("CARGO_PKG_VERSION")); + let update_available = is_app_update_available(¤t_version, &latest_version)?; + + Ok(AppUpdateInfo { + current_version, + latest_version, + update_available, + release_url: APP_RELEASE_PAGE_URL.to_string(), + }) +} + #[tauri::command] async fn check_latest_core() -> Result { let platform = current_core_platform()?; @@ -5010,6 +5064,14 @@ fn release_tag_from_url(url: &reqwest::Url) -> Option { } } +fn is_app_update_available(current: &str, latest: &str) -> Result { + let parse = |value: &str| { + semver::Version::parse(value.trim().trim_start_matches('v')) + .map_err(|error| format!("无法解析版本号 {value}: {error}")) + }; + Ok(parse(latest)? > parse(current)?) +} + #[cfg(test)] fn parse_release_assets(html: &str) -> Vec { let mut assets = Vec::new(); @@ -8622,6 +8684,7 @@ fn main() { get_oauth_status, submit_oauth_callback, open_external_url, + check_app_update, check_latest_core, detect_bundled_core, install_core_version, @@ -10214,6 +10277,14 @@ mod tests { assert_eq!(release_tag_from_atom(xml).as_deref(), Some("v7.2.80")); } + #[test] + fn app_update_comparison_uses_semantic_versions() { + assert!(is_app_update_available("v0.1.9", "v0.2.0").unwrap()); + assert!(is_app_update_available("v0.2.0-beta.1", "v0.2.0").unwrap()); + assert!(!is_app_update_available("v0.2.0", "v0.2.0").unwrap()); + assert!(!is_app_update_available("v0.2.0", "v0.1.9").unwrap()); + } + #[test] fn synthetic_release_uses_official_asset_names_and_urls() { let release = release_from_tag("7.2.80"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5f751be..739fe8e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -15,7 +15,7 @@ "label": "main", "title": "Easy_CLIProxyAPI", "width": 1200, - "height": 800, + "height": 810, "minWidth": 900, "minHeight": 600 } diff --git a/src/App.tsx b/src/App.tsx index 4d53ea3..bfc0ead 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -166,10 +166,6 @@ function AppContent() { 联系我们