diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d841e293b..98ec497f6 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -79,3 +79,6 @@ rquest-util = "2.2.1" [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18" +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 994f3f648..85a90735c 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -48,6 +48,18 @@ struct VersionConfig { fn resolve_version_config() -> (VersionConfig, VersionSource) { // 1. Try Local Installation (Preferred) if let Ok(local_ver) = crate::modules::version::get_antigravity_version() { + let resolved_version = parse_version(&local_ver.short_version) + .or_else(|| parse_version(&local_ver.bundle_version)) + .unwrap_or_else(|| { + tracing::warn!( + raw_short = %local_ver.short_version, + raw_bundle = %local_ver.bundle_version, + fallback = KNOWN_STABLE_VERSION, + "Unable to parse semver from local installation version output; using known stable fallback" + ); + KNOWN_STABLE_VERSION.to_string() + }); + // Map local version to Electron/Chrome if possible // For now, if local version is >= 1.16.5, we assume it's using the new Electron 39 stack // Ideally we would maintain a map, but for now we default to the KNOWN_STABLE stack @@ -55,7 +67,7 @@ fn resolve_version_config() -> (VersionConfig, VersionSource) { // If older, we might want to fallback to older values, but using new values is generally safer for "updates". return ( VersionConfig { - version: local_ver.short_version, + version: resolved_version, electron: KNOWN_STABLE_ELECTRON.to_string(), chrome: KNOWN_STABLE_CHROME.to_string(), }, @@ -146,4 +158,3 @@ mod tests { assert_eq!(parse_version(text), Some("1.15.8".to_string())); } } - diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a1cbc90cf..06a9cce5e 100755 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,68 @@ use modules::logger; use tracing::{info, warn, error}; use std::sync::Arc; +#[derive(Clone, Copy)] +struct AppRuntimeFlags { + tray_enabled: bool, +} + +fn env_flag_enabled(name: &str) -> bool { + std::env::var(name) + .map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")) + .unwrap_or(false) +} + +#[cfg(target_os = "linux")] +fn is_wayland_session() -> bool { + std::env::var("WAYLAND_DISPLAY") + .map(|v| !v.trim().is_empty()) + .unwrap_or(false) + || std::env::var("XDG_SESSION_TYPE") + .map(|v| v.eq_ignore_ascii_case("wayland")) + .unwrap_or(false) +} + +fn should_enable_tray() -> bool { + if env_flag_enabled("ANTIGRAVITY_DISABLE_TRAY") { + info!("Tray disabled by ANTIGRAVITY_DISABLE_TRAY"); + return false; + } + + #[cfg(target_os = "linux")] + { + if is_wayland_session() && !env_flag_enabled("ANTIGRAVITY_FORCE_TRAY") { + warn!( + "Linux Wayland session detected; disabling tray by default to avoid GTK/AppIndicator crashes. Set ANTIGRAVITY_FORCE_TRAY=1 to force-enable." + ); + return false; + } + } + + true +} + +#[cfg(target_os = "linux")] +fn configure_linux_gdk_backend() { + if std::env::var("GDK_BACKEND").is_ok() { + return; + } + + let is_wayland = is_wayland_session(); + let has_x11_display = std::env::var("DISPLAY") + .map(|v| !v.trim().is_empty()) + .unwrap_or(false); + let force_wayland = env_flag_enabled("ANTIGRAVITY_FORCE_WAYLAND"); + let force_x11 = env_flag_enabled("ANTIGRAVITY_FORCE_X11"); + + if force_x11 || (is_wayland && has_x11_display && !force_wayland) { + // Force X11 backend under Wayland sessions to avoid a GTK Wayland shm crash. + std::env::set_var("GDK_BACKEND", "x11"); + warn!( + "Forcing GDK_BACKEND=x11 for stability on Wayland. Set ANTIGRAVITY_FORCE_WAYLAND=1 to keep Wayland backend." + ); + } +} + /// Increase file descriptor limit for macOS to prevent "Too many open files" errors #[cfg(target_os = "macos")] fn increase_nofile_limit() { @@ -56,6 +118,9 @@ pub fn run() { // Initialize logger logger::init_logger(); + #[cfg(target_os = "linux")] + configure_linux_gdk_backend(); + // Initialize token stats database if let Err(e) = modules::token_stats::init_db() { error!("Failed to initialize token stats database: {}", e); @@ -222,6 +287,8 @@ pub fn run() { return; } + let tray_enabled = should_enable_tray(); + tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) @@ -244,6 +311,7 @@ pub fn run() { })) .manage(commands::proxy::ProxyServiceState::new()) .manage(commands::cloudflared::CloudflaredState::new()) + .manage(AppRuntimeFlags { tray_enabled }) .setup(|app| { info!("Setup starting..."); @@ -256,7 +324,9 @@ pub fn run() { #[cfg(target_os = "linux")] { use tauri::Manager; - if let Some(window) = app.get_webview_window("main") { + if is_wayland_session() { + info!("Linux Wayland session detected; skipping transparent window workaround"); + } else if let Some(window) = app.get_webview_window("main") { // Access GTK window and disable transparency at the GTK level if let Ok(gtk_window) = window.gtk_window() { use gtk::prelude::WidgetExt; @@ -266,14 +336,19 @@ pub fn run() { if let Some(visual) = screen.system_visual() { gtk_window.set_visual(Some(&visual)); } + info!("Linux: Applied transparent window workaround"); } - info!("Linux: Applied transparent window workaround"); } } } - modules::tray::create_tray(app.handle())?; - info!("Tray created"); + let runtime_flags = app.state::(); + if runtime_flags.tray_enabled { + modules::tray::create_tray(app.handle())?; + info!("Tray created"); + } else { + info!("Tray disabled for this session"); + } // 立即启动管理服务器 (8045),以便 Web 端能访问 let handle = app.handle().clone(); @@ -323,13 +398,24 @@ pub fn run() { }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { - let _ = window.hide(); - #[cfg(target_os = "macos")] - { - use tauri::Manager; - window.app_handle().set_activation_policy(tauri::ActivationPolicy::Accessory).unwrap_or(()); + let tray_enabled = window + .app_handle() + .try_state::() + .map(|flags| flags.tray_enabled) + .unwrap_or(true); + + if tray_enabled { + let _ = window.hide(); + #[cfg(target_os = "macos")] + { + use tauri::Manager; + window + .app_handle() + .set_activation_policy(tauri::ActivationPolicy::Accessory) + .unwrap_or(()); + } + api.prevent_close(); } - api.prevent_close(); } }) .invoke_handler(tauri::generate_handler![ diff --git a/src-tauri/src/modules/version.rs b/src-tauri/src/modules/version.rs index b7c51d6cd..6dda021ae 100644 --- a/src-tauri/src/modules/version.rs +++ b/src-tauri/src/modules/version.rs @@ -10,6 +10,30 @@ pub struct AntigravityVersion { pub bundle_version: String, } +/// 从任意字符串中提取第一个语义化版本号 (X.Y.Z) +fn extract_semver(raw: &str) -> Option { + for token in raw.split(|c: char| c.is_whitespace() || c == ',' || c == ';') { + let t = token.trim_matches(|c: char| c == '"' || c == '\'' || c == '(' || c == ')'); + if t.is_empty() { + continue; + } + let mut parts = t.split('.'); + let p1 = parts.next(); + let p2 = parts.next(); + let p3 = parts.next(); + if p1.is_some() + && p2.is_some() + && p3.is_some() + && [p1.unwrap(), p2.unwrap(), p3.unwrap()] + .iter() + .all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit())) + { + return Some(t.to_string()); + } + } + None +} + /// 检测 Antigravity 版本(跨平台) pub fn get_antigravity_version() -> Result { // 1. 获取 Antigravity 可执行文件路径(复用现有功能) @@ -122,13 +146,21 @@ fn get_version_linux(exe_path: &PathBuf) -> Result { if let Ok(result) = output { if result.status.success() { - let version = String::from_utf8_lossy(&result.stdout) + let raw_version = String::from_utf8_lossy(&result.stdout) .trim() .to_string(); - if !version.is_empty() { + if !raw_version.is_empty() { + let version = extract_semver(&raw_version).unwrap_or_else(|| { + raw_version + .lines() + .next() + .unwrap_or_default() + .trim() + .to_string() + }); return Ok(AntigravityVersion { short_version: version.clone(), - bundle_version: version, + bundle_version: raw_version, }); } } @@ -214,4 +246,10 @@ mod tests { }; assert!(is_new_version(&newer)); } + + #[test] + fn test_extract_semver_from_messy_output() { + let raw = "1.107.0\n1504c8cc4b34dbfbb4a97ebe954b3da2b5634516\nx64"; + assert_eq!(extract_semver(raw), Some("1.107.0".to_string())); + } } diff --git a/src-tauri/src/proxy/upstream/client.rs b/src-tauri/src/proxy/upstream/client.rs index bc933ba93..37989617c 100644 --- a/src-tauri/src/proxy/upstream/client.rs +++ b/src-tauri/src/proxy/upstream/client.rs @@ -68,8 +68,25 @@ impl UpstreamClient { proxy_config: Option, proxy_pool: Option>, ) -> Self { - let default_client = Self::build_client_internal(proxy_config) - .expect("Failed to create default HTTP client"); + let default_client = match Self::build_client_internal(proxy_config.clone()) { + Ok(client) => client, + Err(err_with_proxy) => { + tracing::error!( + error = %err_with_proxy, + "Failed to create default HTTP client with configured upstream proxy; retrying without proxy" + ); + match Self::build_client_internal(None) { + Ok(client) => client, + Err(err_without_proxy) => { + tracing::error!( + error = %err_without_proxy, + "Failed to create default HTTP client without proxy; falling back to bare client" + ); + Client::new() + } + } + } + }; Self { default_client, @@ -90,8 +107,9 @@ impl UpstreamClient { .pool_max_idle_per_host(16) // 每主机最多 16 个空闲连接 .pool_idle_timeout(Duration::from_secs(90)) // 空闲连接保持 90 秒 .tcp_keepalive(Duration::from_secs(60)) // TCP 保活探测 60 秒 - .timeout(Duration::from_secs(600)) - .user_agent(crate::constants::USER_AGENT.as_str()); + .timeout(Duration::from_secs(600)); + + builder = Self::apply_default_user_agent(builder); if let Some(config) = proxy_config { if config.enabled && !config.url.is_empty() { @@ -112,16 +130,29 @@ impl UpstreamClient { proxy_config: crate::proxy::proxy_pool::PoolProxyConfig, ) -> Result { // Reuse base settings similar to default client but with specific proxy - Client::builder() + let builder = Client::builder() .emulation(rquest_util::Emulation::Chrome123) .connect_timeout(Duration::from_secs(20)) .pool_max_idle_per_host(16) .pool_idle_timeout(Duration::from_secs(90)) .tcp_keepalive(Duration::from_secs(60)) .timeout(Duration::from_secs(600)) - .user_agent(crate::constants::USER_AGENT.as_str()) - .proxy(proxy_config.proxy) // Apply the specific proxy - .build() + .proxy(proxy_config.proxy); // Apply the specific proxy + + Self::apply_default_user_agent(builder).build() + } + + fn apply_default_user_agent(builder: rquest::ClientBuilder) -> rquest::ClientBuilder { + let ua = crate::constants::USER_AGENT.as_str(); + if header::HeaderValue::from_str(ua).is_ok() { + builder.user_agent(ua) + } else { + tracing::warn!( + user_agent = %ua, + "Invalid default User-Agent value, using fallback" + ); + builder.user_agent("antigravity") + } } /// Set dynamic User-Agent override