diff --git a/crates/agent-gui/src-tauri/src/commands/app/update.rs b/crates/agent-gui/src-tauri/src/commands/app/update.rs index e9ca758a7..8c7433b30 100644 --- a/crates/agent-gui/src-tauri/src/commands/app/update.rs +++ b/crates/agent-gui/src-tauri/src/commands/app/update.rs @@ -292,8 +292,9 @@ fn selected_release_candidates_from_entries( } fn github_client() -> Result { - // 系统代理启用时更新检查随之走代理(GitHub 直连常不可达)。 - crate::services::system_proxy::client_builder()? + // 应用代理启用时更新检查随之走应用代理;未启用时回退 reqwest 默认代理探测 + // (OS 代理环境变量/系统代理设置),无系统代理即直连,尽可能保证 GitHub 可达。 + crate::services::system_proxy::client_builder_with_os_proxy_fallback()? .timeout(Duration::from_secs(20)) .build() .map_err(|error| format!("failed to create GitHub client: {error}")) @@ -452,12 +453,12 @@ fn build_updater( builder = builder.pubkey(public_key); } - // 更新下载/安装与 github_client() 的探测请求保持同一份应用代理配置;未启用时 - // 显式 no_proxy(),避免插件内部 client 兜底读取 OS 代理环境变量。 - builder = match crate::services::system_proxy::current_proxy_url()? { - Some(proxy_url) => builder.proxy(proxy_url), - None => builder.no_proxy(), - }; + // 更新下载/安装与 github_client() 的探测请求保持同一份代理语义:应用代理 + // 启用时显式走应用代理;未启用时不调 no_proxy(),让插件内部 client 走 + // reqwest 默认代理探测(OS 代理环境变量/系统代理设置),无系统代理即直连。 + if let Some(proxy_url) = crate::services::system_proxy::current_proxy_url()? { + builder = builder.proxy(proxy_url); + } builder .endpoints(vec![manifest_url]) diff --git a/crates/agent-gui/src-tauri/src/services/system_proxy.rs b/crates/agent-gui/src-tauri/src/services/system_proxy.rs index 8534f8cf5..c5af7348c 100644 --- a/crates/agent-gui/src-tauri/src/services/system_proxy.rs +++ b/crates/agent-gui/src-tauri/src/services/system_proxy.rs @@ -3,6 +3,9 @@ //! MCP http/sse transport、Hook / Cron HTTP、网络自检、Image.url 读取)按需读取。 //! reqwest 侧与 shell env 共用 NO_PROXY_DEFAULT:环回地址永不走代理。 //! 凭据绝不进入日志与错误信息(只输出 host:port)。 +//! 例外:GitHub 更新链路在应用代理未启用时不做 no_proxy() 收口,而是回退 +//! reqwest 默认代理探测(OS 代理环境变量/系统代理设置),见 +//! `client_builder_with_os_proxy_fallback()`。 use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use serde_json::Value; @@ -274,8 +277,21 @@ pub fn cached_client() -> Result { Ok(client) } -pub fn client_builder() -> Result { - async_client_builder_for_mode(¤t_snapshot().mode) +fn os_proxy_fallback_builder_for_mode(mode: &ProxyMode) -> Result { + match mode { + // 不调 no_proxy():保留 reqwest 默认代理探测(OS 代理环境变量与 + // macOS/Windows 系统代理设置,system-proxy 默认特性),无系统代理即直连。 + ProxyMode::Disabled => Ok(reqwest::Client::builder()), + mode => async_client_builder_for_mode(mode), + } +} + +/// GitHub 更新链路专用:应用代理启用时与其他出网点一致走应用代理(配置无效 +/// 同样 fail fast);未启用时回退 OS 系统代理探测而不是强制直连,尽可能保证 +/// GitHub 更新地址可达。其余出网点仍用 `cached_client()`/ +/// `blocking_client_builder()` 的显式 no_proxy 语义。 +pub fn client_builder_with_os_proxy_fallback() -> Result { + os_proxy_fallback_builder_for_mode(¤t_snapshot().mode) } pub fn blocking_client_builder() -> Result { @@ -283,10 +299,12 @@ pub fn blocking_client_builder() -> Result Result, String> { match current_snapshot().mode { ProxyMode::Disabled => Ok(None), @@ -364,10 +382,22 @@ mod tests { assert!(matches!(mode, ProxyMode::Invalid(_))); assert!(async_client_builder_for_mode(&mode).is_err()); assert!(blocking_client_builder_for_mode(&mode).is_err()); + // 更新链路的回退 builder 同样不许把 Invalid 静默降级为直连/系统代理。 + assert!(os_proxy_fallback_builder_for_mode(&mode).is_err()); assert!(shell_proxy_envs_for_mode(&mode).is_err()); } } + #[test] + fn os_proxy_fallback_builder_accepts_disabled_and_enabled_modes() { + assert!(os_proxy_fallback_builder_for_mode(&ProxyMode::Disabled).is_ok()); + let enabled = parse_proxy_mode(Some(&json!({ + "enabled": true, "type": "http", "host": "proxy.local", "port": 8080 + }))); + assert!(matches!(enabled, ProxyMode::Enabled(_))); + assert!(os_proxy_fallback_builder_for_mode(&enabled).is_ok()); + } + #[test] fn proxy_mode_equality_drives_set_config_dedupe() { let config = json!({