diff --git a/Cargo.lock b/Cargo.lock index 830abeb..b38f049 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1679,7 +1679,7 @@ dependencies = [ "rustc_version", "toml 1.1.6+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -7890,6 +7890,7 @@ dependencies = [ "ureq-proto", "utf8-zero", "webpki-roots", + "winreg 0.56.0", ] [[package]] @@ -9182,6 +9183,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "wio" version = "0.2.2" diff --git a/gpui-app/Cargo.toml b/gpui-app/Cargo.toml index f48e62a..8ba8131 100644 --- a/gpui-app/Cargo.toml +++ b/gpui-app/Cargo.toml @@ -41,8 +41,9 @@ rustls-native-certs = "0.8" # 原生文件对话框(连接导入/导出);Windows 走内建 COM 后端,Linux 用默认 xdg-portal rfd = "0.17" -# OTA 自更新:Gitea Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源) -ureq = { version = "3", features = ["json"] } +# OTA 自更新:GitHub Release 检查/下载(ureq 用 rustls,与 rumqttc 依赖同源; +# win-system-proxy 使 Windows 系统代理注册表设置自动生效,环境变量代理本就默认支持) +ureq = { version = "3", features = ["json", "win-system-proxy"] } sha2 = "0.11" # Release 附件为 7z 压缩包,纯 Rust 解压(客户端免装 7-Zip) sevenz-rust = "0.6" diff --git a/gpui-app/src/update/check.rs b/gpui-app/src/update/check.rs index 9671fa6..491eef1 100644 --- a/gpui-app/src/update/check.rs +++ b/gpui-app/src/update/check.rs @@ -69,6 +69,19 @@ pub fn pick_assets(rel: &serde_json::Value) -> Option<(String, String, u64, Opti /// 检查更新。返回 Ok(None) 表示已是最新。 pub fn check_latest() -> Result, String> { + // API 主路径信息最全(notes/资产清单/大小),但 api.github.com 未认证配额 + // 仅 60 次/小时/IP,共享出口 IP(VPN/办公 NAT)经常打满 —— 打满或不可达时 + // 退到 releases 页面重定向发现(无配额),完整性校验不受影响。 + match check_latest_via_api() { + Ok(v) => Ok(v), + Err(api_err) => match check_latest_via_redirect() { + Ok(v) => Ok(v), + Err(_) => Err(api_err), + }, + } +} + +fn check_latest_via_api() -> Result, String> { let base = RELEASES_API; // 优先 latest 端点(GitHub 恒可用);回退到列表取第一个非草稿/预发布,作容错 let rel = match http_get_json(&format!("{base}/latest")) { @@ -110,6 +123,48 @@ pub fn check_latest() -> Result, String> { })) } +/// 兜底:跟踪 `releases/latest` 的 302,从最终 URL 提取最新 tag。 +/// +/// github.com 页面不受 API 配额限制。代价是拿不到 Release notes 与资产清单: +/// notes 置空、大小置 0(进度条显示未知大小,大小对账自动跳过), +/// 下载 URL 与 `.sha256` 侧车按命名约定构造,sha256 校验仍然强制执行。 +fn check_latest_via_redirect() -> Result, String> { + use ureq::ResponseExt as _; + let resp = agent() + .get(&format!("{RELEASES_PAGE}/latest")) + .call() + .map_err(|e| format!("页面请求失败: {e}"))?; + let uri = resp.get_uri().to_string(); + let Some(tag) = tag_from_releases_url(&uri) else { + return Err(format!("页面 URL 异常: {uri}")); + }; + if !is_newer(tag, current_version()) { + return Ok(None); + } + let version = tag.trim_start_matches('v'); + let triple = platform_triple(); + let bin = if cfg!(windows) { "mqttx.exe" } else { "mqttx" }; + // 与 publish 脚本/工作流的产物命名约定一致(pick_assets 同款匹配规则) + let asset_base = format!("mqttx-v{version}-{triple}-{bin}"); + let asset = format!("{asset_base}.7z"); + Ok(Some(UpdateInfo { + version: version.to_string(), + notes: String::new(), + asset_name: asset.clone(), + asset_url: format!("{RELEASES_PAGE}/download/{tag}/{asset}"), + sha256_url: Some(format!("{RELEASES_PAGE}/download/{tag}/{asset}.sha256")), + size: 0, + })) +} + +/// 从 Releases 页面 URL 提取 tag(如 `.../releases/tag/v1.0.1` → `v1.0.1`)。 +fn tag_from_releases_url(uri: &str) -> Option<&str> { + let tag = uri.split("/releases/tag/").nth(1)?; + let tag = tag.trim_end_matches('/'); + // 防御:tag 不含路径段,且必须以 v 开头(约定) + (tag.starts_with('v') && !tag.contains('/')).then_some(tag) +} + /// 返回 Ok(None) 表示已是最新;错误仅记录、不打扰用户。 pub fn check_and_download() -> Result, String> { let Some(info) = check_latest()? else { @@ -123,3 +178,34 @@ pub fn check_and_download() -> Result, String> { } download_and_stage(&info, &|_, _| {}).map(Some) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tag_extraction_from_releases_url() { + // /releases/latest 302 后的最终 URL 形态 + assert_eq!( + tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/v1.0.1"), + Some("v1.0.1") + ); + assert_eq!( + tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/v1.2.3-rc.1/"), + Some("v1.2.3-rc.1") + ); + // 非 tag 页面 / 异常形态一律拒绝 + assert_eq!( + tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases"), + None + ); + assert_eq!( + tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/1.0.0"), + None + ); + assert_eq!( + tag_from_releases_url("https://github.com/LiJoeAllen/mqttx/releases/tag/v1.0.0/extra"), + None + ); + } +} diff --git a/gpui-app/src/update/mod.rs b/gpui-app/src/update/mod.rs index 623e865..d3864b3 100644 --- a/gpui-app/src/update/mod.rs +++ b/gpui-app/src/update/mod.rs @@ -26,6 +26,10 @@ pub use install::{ /// - v1.0.x 老客户端的 OTA 源仍是自建 Gitea,CI 的 mirror-gitea job 会把每次 /// 发布镜像回 Gitea,老客户端升级完毕后可移除该 job。 pub const RELEASES_API: &str = "https://api.github.com/repos/LiJoeAllen/mqttx/releases"; +/// Releases 页面(github.com,非 API、无配额限制):api.github.com 被限流时 +/// 兜底发现最新 tag —— 跟踪 `{RELEASES_PAGE}/latest` 的 302,从最终 URL 取 tag, +/// 下载与校验按命名约定构造 URL,完整性(sha256)不受影响。 +pub const RELEASES_PAGE: &str = "https://github.com/LiJoeAllen/mqttx/releases"; /// 更新源展示名(设置对话框「关于」)。 pub const UPDATE_SOURCE: &str = "GitHub";