Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
15 changes: 13 additions & 2 deletions src-tauri/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,26 @@ 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
// if the version matches or is newer.
// 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(),
},
Expand Down Expand Up @@ -146,4 +158,3 @@ mod tests {
assert_eq!(parse_version(text), Some("1.15.8".to_string()));
}
}

106 changes: 96 additions & 10 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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())
Expand All @@ -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...");

Expand All @@ -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;
Expand All @@ -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::<AppRuntimeFlags>();
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();
Expand Down Expand Up @@ -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::<AppRuntimeFlags>()
.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![
Expand Down
44 changes: 41 additions & 3 deletions src-tauri/src/modules/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ pub struct AntigravityVersion {
pub bundle_version: String,
}

/// 从任意字符串中提取第一个语义化版本号 (X.Y.Z)
fn extract_semver(raw: &str) -> Option<String> {
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<AntigravityVersion, String> {
// 1. 获取 Antigravity 可执行文件路径(复用现有功能)
Expand Down Expand Up @@ -122,13 +146,21 @@ fn get_version_linux(exe_path: &PathBuf) -> Result<AntigravityVersion, String> {

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,
});
}
}
Expand Down Expand Up @@ -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()));
}
}
47 changes: 39 additions & 8 deletions src-tauri/src/proxy/upstream/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,25 @@ impl UpstreamClient {
proxy_config: Option<crate::proxy::config::UpstreamProxyConfig>,
proxy_pool: Option<Arc<crate::proxy::proxy_pool::ProxyPoolManager>>,
) -> 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,
Expand All @@ -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() {
Expand All @@ -112,16 +130,29 @@ impl UpstreamClient {
proxy_config: crate::proxy::proxy_pool::PoolProxyConfig,
) -> Result<Client, rquest::Error> {
// 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
Expand Down