| mod models; |
| mod modules; |
| mod commands; |
| mod utils; |
| mod proxy; |
| pub mod error; |
| pub mod constants; |
|
|
| use tauri::Manager; |
| 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) { |
| |
| 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." |
| ); |
| } |
| } |
|
|
| |
| #[cfg(target_os = "macos")] |
| fn increase_nofile_limit() { |
| unsafe { |
| let mut rl = libc::rlimit { |
| rlim_cur: 0, |
| rlim_max: 0, |
| }; |
|
|
| if libc::getrlimit(libc::RLIMIT_NOFILE, &mut rl) == 0 { |
| info!("Current open file limit: soft={}, hard={}", rl.rlim_cur, rl.rlim_max); |
|
|
| |
| let target = 4096.min(rl.rlim_max); |
| if rl.rlim_cur < target { |
| rl.rlim_cur = target; |
| if libc::setrlimit(libc::RLIMIT_NOFILE, &rl) == 0 { |
| info!("Successfully increased hard file limit to {}", target); |
| } else { |
| warn!("Failed to increase file descriptor limit"); |
| } |
| } |
| } |
| } |
| } |
|
|
| |
| #[tauri::command] |
| fn greet(name: &str) -> String { |
| format!("Hello, {}! You've been greeted from Rust!", name) |
| } |
|
|
| #[cfg_attr(mobile, tauri::mobile_entry_point)] |
| pub fn run() { |
| |
| let args: Vec<String> = std::env::args().collect(); |
| let is_headless = args.iter().any(|arg| arg == "--headless"); |
|
|
| |
| #[cfg(target_os = "macos")] |
| increase_nofile_limit(); |
|
|
| |
| logger::init_logger(); |
|
|
| #[cfg(target_os = "linux")] |
| configure_linux_gdk_backend(); |
|
|
| |
| if let Err(e) = modules::token_stats::init_db() { |
| error!("Failed to initialize token stats database: {}", e); |
| } |
|
|
| |
| if let Err(e) = modules::security_db::init_db() { |
| error!("Failed to initialize security database: {}", e); |
| } |
| |
| |
| if let Err(e) = modules::user_token_db::init_db() { |
| error!("Failed to initialize user token database: {}", e); |
| } |
|
|
| if is_headless { |
| info!("Starting in HEADLESS mode..."); |
|
|
| let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); |
| rt.block_on(async { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| let proxy_state = commands::proxy::ProxyServiceState::new(); |
| let cf_state = Arc::new(commands::cloudflared::CloudflaredState::new()); |
|
|
| |
| match modules::config::load_app_config() { |
| Ok(mut config) => { |
| let mut modified = false; |
| |
| |
| let bind_local_only = std::env::var("ABV_BIND_LOCAL_ONLY") |
| .map(|v| matches!(v.to_lowercase().as_str(), "1" | "true" | "yes" | "on")) |
| .unwrap_or(false); |
| if bind_local_only { |
| config.proxy.allow_lan_access = false; |
| modified = true; |
| } else { |
| config.proxy.allow_lan_access = true; |
| } |
|
|
| |
| |
| if matches!(config.proxy.auth_mode, crate::proxy::ProxyAuthMode::Off | crate::proxy::ProxyAuthMode::Auto) { |
| info!("Headless mode: Forcing auth_mode to AllExceptHealth for Web UI security"); |
| config.proxy.auth_mode = crate::proxy::ProxyAuthMode::AllExceptHealth; |
| modified = true; |
| } |
|
|
| |
| |
| let env_key = std::env::var("ABV_API_KEY") |
| .or_else(|_| std::env::var("API_KEY")) |
| .ok(); |
|
|
| if let Some(key) = env_key { |
| if !key.trim().is_empty() { |
| info!("Using API Key from environment variable"); |
| config.proxy.api_key = key; |
| modified = true; |
| } |
| } |
|
|
| |
| |
| let env_web_password = std::env::var("ABV_WEB_PASSWORD") |
| .or_else(|_| std::env::var("WEB_PASSWORD")) |
| .ok(); |
|
|
| if let Some(pwd) = env_web_password { |
| if !pwd.trim().is_empty() { |
| info!("Using Web UI Password from environment variable"); |
| config.proxy.admin_password = Some(pwd); |
| modified = true; |
| } |
| } |
|
|
| |
| |
| let env_auth_mode = std::env::var("ABV_AUTH_MODE") |
| .or_else(|_| std::env::var("AUTH_MODE")) |
| .ok(); |
|
|
| if let Some(mode_str) = env_auth_mode { |
| let mode = match mode_str.to_lowercase().as_str() { |
| "off" => Some(crate::proxy::ProxyAuthMode::Off), |
| "strict" => Some(crate::proxy::ProxyAuthMode::Strict), |
| "all_except_health" => Some(crate::proxy::ProxyAuthMode::AllExceptHealth), |
| "auto" => Some(crate::proxy::ProxyAuthMode::Auto), |
| _ => { |
| warn!("Invalid AUTH_MODE: {}, ignoring", mode_str); |
| None |
| } |
| }; |
| if let Some(m) = mode { |
| info!("Using Auth Mode from environment variable: {:?}", m); |
| config.proxy.auth_mode = m; |
| modified = true; |
| } |
| } |
|
|
| info!("--------------------------------------------------"); |
| info!("🚀 Headless mode proxy service starting..."); |
| info!("📍 Port: {}", config.proxy.port); |
| info!("🔑 Current API Key: {}", config.proxy.api_key); |
| if let Some(ref pwd) = config.proxy.admin_password { |
| info!("🔐 Web UI Password: {}", pwd); |
| } else { |
| info!("🔐 Web UI Password: (Same as API Key)"); |
| } |
| info!("💡 Tips: You can use these keys to login to Web UI and access AI APIs."); |
| info!("💡 Search docker logs or grep gui_config.json to find them."); |
| info!("--------------------------------------------------"); |
|
|
| |
| if modified { |
| if let Err(e) = modules::config::save_app_config(&config) { |
| error!("Failed to persist environment overrides: {}", e); |
| } else { |
| info!("Environment overrides persisted to gui_config.json"); |
| } |
| } |
|
|
| |
| if let Err(e) = commands::proxy::internal_start_proxy_service( |
| config.proxy, |
| &proxy_state, |
| crate::modules::integration::SystemManager::Headless, |
| cf_state.clone(), |
| ).await { |
| error!("Failed to start proxy service in headless mode: {}", e); |
| std::process::exit(1); |
| } |
|
|
| info!("Headless proxy service is running."); |
|
|
| |
| |
| info!("Smart scheduler (Automatic Warmup) is DISABLED."); |
| info!("Smart scheduler started in headless mode."); |
| } |
| Err(e) => { |
| error!("Failed to load config for headless mode: {}", e); |
| std::process::exit(1); |
| } |
| } |
|
|
| |
| tokio::signal::ctrl_c().await.ok(); |
| info!("Headless mode shutting down"); |
| }); |
| return; |
| } |
|
|
| let tray_enabled = should_enable_tray(); |
|
|
| tauri::Builder::default() |
| .plugin(tauri_plugin_dialog::init()) |
| .plugin(tauri_plugin_fs::init()) |
| .plugin(tauri_plugin_opener::init()) |
| .plugin(tauri_plugin_autostart::init( |
| tauri_plugin_autostart::MacosLauncher::LaunchAgent, |
| Some(vec!["--minimized"]), |
| )) |
| .plugin(tauri_plugin_updater::Builder::new().build()) |
| .plugin(tauri_plugin_process::init()) |
| .plugin(tauri_plugin_window_state::Builder::default().build()) |
| .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { |
| let _ = app.get_webview_window("main") |
| .map(|window| { |
| let _ = window.show(); |
| let _ = window.set_focus(); |
| #[cfg(target_os = "macos")] |
| app.set_activation_policy(tauri::ActivationPolicy::Regular).unwrap_or(()); |
| }); |
| })) |
| .manage(commands::proxy::ProxyServiceState::new()) |
| .manage(commands::cloudflared::CloudflaredState::new()) |
| .manage(AppRuntimeFlags { tray_enabled }) |
| .setup(|app| { |
| info!("Setup starting..."); |
|
|
| |
| modules::log_bridge::init_log_bridge(app.handle().clone()); |
|
|
| |
| |
| |
| #[cfg(target_os = "linux")] |
| { |
| use tauri::Manager; |
| if is_wayland_session() { |
| info!("Linux Wayland session detected; skipping transparent window workaround"); |
| } else if let Some(window) = app.get_webview_window("main") { |
| |
| if let Ok(gtk_window) = window.gtk_window() { |
| use gtk::prelude::WidgetExt; |
| |
| if let Some(screen) = gtk_window.screen() { |
| |
| if let Some(visual) = screen.system_visual() { |
| gtk_window.set_visual(Some(&visual)); |
| } |
| info!("Linux: Applied transparent window workaround"); |
| } |
| } |
| } |
| } |
|
|
| 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"); |
| } |
|
|
| |
| let handle = app.handle().clone(); |
| tauri::async_runtime::spawn(async move { |
| |
| if let Ok(config) = modules::config::load_app_config() { |
| let state = handle.state::<commands::proxy::ProxyServiceState>(); |
| let cf_state = handle.state::<commands::cloudflared::CloudflaredState>(); |
| let integration = crate::modules::integration::SystemManager::Desktop(handle.clone()); |
|
|
| |
| if let Err(e) = commands::proxy::ensure_admin_server( |
| config.proxy.clone(), |
| &state, |
| integration.clone(), |
| Arc::new(cf_state.inner().clone()), |
| ).await { |
| error!("Failed to start admin server: {}", e); |
| } else { |
| info!("Admin server (port {}) started successfully", config.proxy.port); |
| } |
|
|
| |
| if config.proxy.auto_start { |
| if let Err(e) = commands::proxy::internal_start_proxy_service( |
| config.proxy, |
| &state, |
| integration, |
| Arc::new(cf_state.inner().clone()), |
| ).await { |
| error!("Failed to auto-start proxy service: {}", e); |
| } else { |
| info!("Proxy service auto-started successfully"); |
| } |
| } |
| } |
| }); |
|
|
| |
| |
| |
| info!("Smart scheduler (Automatic Warmup) is DISABLED."); |
|
|
| |
| info!("Management API integrated into main proxy server (port 8045)"); |
|
|
| Ok(()) |
| }) |
| .on_window_event(|window, event| { |
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { |
| 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(); |
| } |
| } |
| }) |
| .invoke_handler(tauri::generate_handler![ |
| greet, |
| |
| commands::list_accounts, |
| commands::add_account, |
| commands::delete_account, |
| commands::delete_accounts, |
| commands::reorder_accounts, |
| commands::switch_account, |
| commands::export_accounts, |
| |
| commands::get_device_profiles, |
| commands::bind_device_profile, |
| commands::bind_device_profile_with_profile, |
| commands::preview_generate_profile, |
| commands::apply_device_profile, |
| commands::restore_original_device, |
| commands::list_device_versions, |
| commands::restore_device_version, |
| commands::delete_device_version, |
| commands::open_device_folder, |
| commands::get_current_account, |
| |
| commands::fetch_account_quota, |
| commands::refresh_all_quotas, |
| |
| commands::load_config, |
| commands::save_config, |
| |
| commands::prepare_oauth_url, |
| commands::start_oauth_login, |
| commands::complete_oauth_login, |
| commands::cancel_oauth_login, |
| commands::submit_oauth_code, |
| commands::list_oauth_clients, |
| commands::get_active_oauth_client, |
| commands::set_active_oauth_client, |
| commands::import_v1_accounts, |
| commands::import_from_db, |
| commands::import_custom_db, |
| commands::sync_account_from_db, |
| commands::save_text_file, |
| commands::read_text_file, |
| commands::clear_log_cache, |
| commands::clear_antigravity_cache, |
| commands::get_antigravity_cache_paths, |
| commands::open_data_folder, |
| commands::get_data_dir_path, |
| commands::show_main_window, |
| commands::set_window_theme, |
| commands::get_antigravity_path, |
| commands::get_antigravity_args, |
| commands::check_for_updates, |
| commands::check_homebrew_installation, |
| commands::brew_upgrade_cask, |
| commands::get_update_settings, |
| commands::save_update_settings, |
| commands::should_check_updates, |
| commands::update_last_check_time, |
| commands::toggle_proxy_status, |
| |
| commands::proxy::start_proxy_service, |
| commands::proxy::stop_proxy_service, |
| commands::proxy::get_proxy_status, |
| commands::proxy::get_proxy_stats, |
| commands::proxy::get_proxy_logs, |
| commands::proxy::get_proxy_logs_paginated, |
| commands::proxy::get_proxy_log_detail, |
| commands::proxy::get_proxy_logs_count, |
| commands::proxy::export_proxy_logs, |
| commands::proxy::export_proxy_logs_json, |
| commands::proxy::get_proxy_logs_count_filtered, |
| commands::proxy::get_proxy_logs_filtered, |
| commands::proxy::set_proxy_monitor_enabled, |
| commands::proxy::clear_proxy_logs, |
| commands::proxy::generate_api_key, |
| commands::proxy::reload_proxy_accounts, |
| commands::proxy::update_model_mapping, |
| commands::proxy::check_proxy_health, |
| commands::proxy::get_proxy_pool_config, |
| commands::proxy::fetch_zai_models, |
| commands::proxy::get_proxy_scheduling_config, |
| commands::proxy::update_proxy_scheduling_config, |
| commands::proxy::clear_proxy_session_bindings, |
| commands::proxy::set_preferred_account, |
| commands::proxy::get_preferred_account, |
| commands::proxy::clear_proxy_rate_limit, |
| commands::proxy::clear_all_proxy_rate_limits, |
| commands::proxy::check_proxy_health, |
| |
| commands::proxy_pool::bind_account_proxy, |
| commands::proxy_pool::unbind_account_proxy, |
| commands::proxy_pool::get_account_proxy_binding, |
| commands::proxy_pool::get_all_account_bindings, |
| |
| commands::autostart::toggle_auto_launch, |
| commands::autostart::is_auto_launch_enabled, |
| |
| commands::warm_up_all_accounts, |
| commands::warm_up_account, |
| commands::update_account_label, |
| |
| commands::get_http_api_settings, |
| commands::save_http_api_settings, |
| |
| commands::get_token_stats_hourly, |
| commands::get_token_stats_daily, |
| commands::get_token_stats_weekly, |
| commands::get_token_stats_by_account, |
| commands::get_token_stats_summary, |
| commands::get_token_stats_by_model, |
| commands::get_token_stats_model_trend_hourly, |
| commands::get_token_stats_model_trend_daily, |
| commands::get_token_stats_account_trend_hourly, |
| commands::get_token_stats_account_trend_daily, |
| proxy::cli_sync::get_cli_sync_status, |
| proxy::cli_sync::execute_cli_sync, |
| proxy::cli_sync::execute_cli_restore, |
| proxy::cli_sync::get_cli_config_content, |
| proxy::opencode_sync::get_opencode_sync_status, |
| proxy::opencode_sync::execute_opencode_sync, |
| proxy::opencode_sync::execute_opencode_restore, |
| proxy::opencode_sync::get_opencode_config_content, |
| proxy::opencode_sync::execute_opencode_clear, |
| proxy::droid_sync::get_droid_sync_status, |
| proxy::droid_sync::execute_droid_sync, |
| proxy::droid_sync::execute_droid_restore, |
| proxy::droid_sync::get_droid_config_content, |
| |
| commands::security::get_ip_access_logs, |
| commands::security::get_ip_stats, |
| commands::security::get_ip_token_stats, |
| commands::security::clear_ip_access_logs, |
| commands::security::get_ip_blacklist, |
| commands::security::add_ip_to_blacklist, |
| commands::security::remove_ip_from_blacklist, |
| commands::security::clear_ip_blacklist, |
| commands::security::check_ip_in_blacklist, |
| commands::security::get_ip_whitelist, |
| commands::security::add_ip_to_whitelist, |
| commands::security::remove_ip_from_whitelist, |
| commands::security::clear_ip_whitelist, |
| commands::security::check_ip_in_whitelist, |
| commands::security::get_security_config, |
| commands::security::update_security_config, |
| |
| commands::cloudflared::cloudflared_check, |
| commands::cloudflared::cloudflared_install, |
| commands::cloudflared::cloudflared_start, |
| commands::cloudflared::cloudflared_stop, |
| commands::cloudflared::cloudflared_get_status, |
| |
| modules::log_bridge::enable_debug_console, |
| modules::log_bridge::disable_debug_console, |
| modules::log_bridge::is_debug_console_enabled, |
| modules::log_bridge::get_debug_console_logs, |
| modules::log_bridge::clear_debug_console_logs, |
| |
| commands::user_token::list_user_tokens, |
| commands::user_token::create_user_token, |
| commands::user_token::update_user_token, |
| commands::user_token::delete_user_token, |
| commands::user_token::renew_user_token, |
| commands::user_token::get_token_ip_bindings, |
| commands::user_token::get_user_token_summary, |
| ]) |
| .build(tauri::generate_context!()) |
| .expect("error while building tauri application") |
| .run(|app_handle, event| { |
| match event { |
| |
| tauri::RunEvent::Exit => { |
| tracing::info!("Application exiting, cleaning up background tasks..."); |
| if let Some(state) = app_handle.try_state::<crate::commands::proxy::ProxyServiceState>() { |
| tauri::async_runtime::block_on(async { |
| |
| match tokio::time::timeout( |
| std::time::Duration::from_secs(3), |
| state.instance.read() |
| ).await { |
| Ok(guard) => { |
| if let Some(instance) = guard.as_ref() { |
| |
| instance.token_manager |
| .graceful_shutdown(std::time::Duration::from_secs(2)) |
| .await; |
| } |
| } |
| Err(_) => { |
| tracing::warn!("Lock acquisition timed out after 3s, forcing exit"); |
| } |
| } |
| }); |
| } |
| } |
| |
| #[cfg(target_os = "macos")] |
| tauri::RunEvent::Reopen { .. } => { |
| if let Some(window) = app_handle.get_webview_window("main") { |
| let _ = window.show(); |
| let _ = window.unminimize(); |
| let _ = window.set_focus(); |
| app_handle.set_activation_policy(tauri::ActivationPolicy::Regular).unwrap_or(()); |
| } |
| } |
| _ => {} |
| } |
| }); |
| } |
|
|