Skip to content
Open
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
2 changes: 1 addition & 1 deletion src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ fn is_sensitive_path(path: &Path) -> bool {
.any(|prefix| lower == *prefix || lower.starts_with(prefix))
}

fn validate_user_json_path(path: &str, must_exist: bool) -> Result<PathBuf, String> {
pub(crate) fn validate_user_json_path(path: &str, must_exist: bool) -> Result<PathBuf, String> {
let requested = PathBuf::from(path);
if requested.as_os_str().is_empty() {
return Err("invalid_path: empty path".to_string());
Expand Down
9 changes: 7 additions & 2 deletions src-tauri/src/commands/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,20 +412,24 @@ pub async fn get_proxy_logs_count() -> Result<u64, String> {
/// 导出所有日志到指定文件
#[tauri::command]
pub async fn export_proxy_logs(file_path: String) -> Result<usize, String> {
let validated_path = super::validate_user_json_path(&file_path, false)?;

let logs = crate::modules::proxy_db::get_all_logs_for_export()?;
let count = logs.len();

let json = serde_json::to_string_pretty(&logs)
.map_err(|e| format!("Failed to serialize logs: {}", e))?;

std::fs::write(&file_path, json).map_err(|e| format!("Failed to write file: {}", e))?;
std::fs::write(&validated_path, json).map_err(|e| format!("Failed to write file: {}", e))?;

Ok(count)
}

/// 导出指定的日志JSON到文件
#[tauri::command]
pub async fn export_proxy_logs_json(file_path: String, json_data: String) -> Result<usize, String> {
let validated_path = super::validate_user_json_path(&file_path, false)?;

// Parse to count items
let logs: Vec<serde_json::Value> =
serde_json::from_str(&json_data).map_err(|e| format!("Failed to parse JSON: {}", e))?;
Expand All @@ -435,7 +439,8 @@ pub async fn export_proxy_logs_json(file_path: String, json_data: String) -> Res
let pretty_json =
serde_json::to_string_pretty(&logs).map_err(|e| format!("Failed to serialize: {}", e))?;

std::fs::write(&file_path, pretty_json).map_err(|e| format!("Failed to write file: {}", e))?;
std::fs::write(&validated_path, pretty_json)
.map_err(|e| format!("Failed to write file: {}", e))?;

Ok(count)
}
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ pub fn run() {
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!("💡 View gui_config.json or check environment variables to find them.");
info!("--------------------------------------------------");

// [FIX #1460] Persist environment overrides to ensure they are visible in Web UI/load_config
Expand Down
205 changes: 99 additions & 106 deletions src-tauri/src/modules/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1749,127 +1749,119 @@ pub async fn fetch_quota_with_retry(account: &mut Account) -> crate::error::AppR
}

// 3. Handle 401 error
if let Err(AppError::Network(_, status)) = result {
if let Some(code) = status {
if code == 401 {
modules::logger::log_warn(&format!(
"401 Unauthorized for {}, forcing refresh...",
account.email
));
if let Err(AppError::Network(_, Some(code))) = result {
if code == 401 {
modules::logger::log_warn(&format!(
"401 Unauthorized for {}, forcing refresh...",
account.email
));

// Force refresh
let token_res = match oauth::refresh_access_token_with_client(
&account.token.refresh_token,
Some(&account.id),
account.token.oauth_client_key.as_deref(),
)
.await
{
Ok(t) => t,
Err(e) => {
if e.contains("invalid_grant") {
modules::logger::log_error(&format!(
// Force refresh
let token_res = match oauth::refresh_access_token_with_client(
&account.token.refresh_token,
Some(&account.id),
account.token.oauth_client_key.as_deref(),
)
.await
{
Ok(t) => t,
Err(e) => {
if e.contains("invalid_grant") {
modules::logger::log_error(&format!(
"Disabling account {} due to invalid_grant during forced refresh (quota check)",
account.email
));
account.disabled = true;
account.disabled_at = Some(chrono::Utc::now().timestamp());
account.disabled_reason = Some(format!("invalid_grant: {}", e));
let _ = save_account(account);
crate::proxy::server::trigger_account_reload(&account.id);
}
return Err(AppError::OAuth(e));
account.disabled = true;
account.disabled_at = Some(chrono::Utc::now().timestamp());
account.disabled_reason = Some(format!("invalid_grant: {}", e));
let _ = save_account(account);
crate::proxy::server::trigger_account_reload(&account.id);
}
};

let new_token = TokenData::new(
token_res.access_token.clone(),
account.token.refresh_token.clone(),
token_res.expires_in,
account.token.email.clone(),
account.token.project_id.clone(), // Keep original project_id
None, // Add None as session_id
account.token.is_gcp_tos,
token_res.id_token.clone(),
)
.with_oauth_client_key(
token_res
.oauth_client_key
.clone()
.or_else(|| account.token.oauth_client_key.clone()),
);
return Err(AppError::OAuth(e));
}
};

let new_token = TokenData::new(
token_res.access_token.clone(),
account.token.refresh_token.clone(),
token_res.expires_in,
account.token.email.clone(),
account.token.project_id.clone(), // Keep original project_id
None, // Add None as session_id
account.token.is_gcp_tos,
token_res.id_token.clone(),
)
.with_oauth_client_key(
token_res
.oauth_client_key
.clone()
.or_else(|| account.token.oauth_client_key.clone()),
);

// Re-fetch display name
let name = if account.name.is_none()
|| account.name.as_ref().map_or(false, |n| n.trim().is_empty())
{
match oauth::get_user_info(&token_res.access_token, Some(&account.id)).await {
Ok(user_info) => user_info.get_display_name(),
Err(_) => None,
}
} else {
account.name.clone()
};
// Re-fetch display name
let name = if account.name.is_none()
|| account.name.as_ref().map_or(false, |n| n.trim().is_empty())
{
match oauth::get_user_info(&token_res.access_token, Some(&account.id)).await {
Ok(user_info) => user_info.get_display_name(),
Err(_) => None,
}
} else {
account.name.clone()
};

account.token = new_token.clone();
account.name = name.clone();
upsert_account(account.email.clone(), name, new_token.clone())
.map_err(AppError::Account)?;

// Retry query
let retry_result: crate::error::AppResult<(QuotaData, Option<String>)> =
modules::fetch_quota(
&new_token.access_token,
&account.email,
Some(&account.id),
)
account.token = new_token.clone();
account.name = name.clone();
upsert_account(account.email.clone(), name, new_token.clone())
.map_err(AppError::Account)?;

// Retry query
let retry_result: crate::error::AppResult<(QuotaData, Option<String>)> =
modules::fetch_quota(&new_token.access_token, &account.email, Some(&account.id))
.await;

// Also handle project_id saving during retry
if let Ok((ref _q, ref project_id)) = retry_result {
if project_id.is_some() && *project_id != account.token.project_id {
modules::logger::log_info(&format!(
"Detected update of project_id after retry ({}), saving...",
account.email
));
account.token.project_id = project_id.clone();
let _ = upsert_account(
account.email.clone(),
account.name.clone(),
account.token.clone(),
);
}
// Also handle project_id saving during retry
if let Ok((ref _q, ref project_id)) = retry_result {
if project_id.is_some() && *project_id != account.token.project_id {
modules::logger::log_info(&format!(
"Detected update of project_id after retry ({}), saving...",
account.email
));
account.token.project_id = project_id.clone();
let _ = upsert_account(
account.email.clone(),
account.name.clone(),
account.token.clone(),
);
}
}

if let Err(AppError::Network(_, status)) = retry_result {
if let Some(code) = status {
if code == 403 {
let mut q = QuotaData::new();
q.is_forbidden = true;
return Ok(q);
}
}
if let Err(AppError::Network(_, Some(code))) = retry_result {
if code == 403 {
let mut q = QuotaData::new();
q.is_forbidden = true;
return Ok(q);
}
}

match retry_result {
Ok((q, _)) => {
clear_validation_blocked(account);
return Ok(q);
match retry_result {
Ok((q, _)) => {
clear_validation_blocked(account);
return Ok(q);
}
Err(e) => {
if is_validation_required_error(&e) {
mark_validation_blocked(account, &e.to_string());
}
Err(e) => {
if is_validation_required_error(&e) {
mark_validation_blocked(account, &e.to_string());
}
if let Some(cached) = recover_cached_quota_on_rate_limit(account, &e) {
mark_validation_blocked(account, &format_rate_limit_block_reason(&e));
modules::logger::log_warn(&format!(
"Quota API rate-limited for {}, using cached model list as fallback",
account.email
));
return Ok(cached);
}
return Err(e);
if let Some(cached) = recover_cached_quota_on_rate_limit(account, &e) {
mark_validation_blocked(account, &format_rate_limit_block_reason(&e));
modules::logger::log_warn(&format!(
"Quota API rate-limited for {}, using cached model list as fallback",
account.email
));
return Ok(cached);
}
return Err(e);
}
}
}
Expand Down Expand Up @@ -2010,6 +2002,7 @@ pub async fn refresh_all_quotas_logic() -> Result<RefreshStats, String> {

/// Check and trigger warmup for models that have recovered to 100%
/// Called automatically after quota refresh to enable immediate warmup
#[allow(dead_code)]
pub async fn check_and_trigger_warmup_for_recovered_models() {
let accounts = match list_accounts() {
Ok(acc) => acc,
Expand Down
3 changes: 0 additions & 3 deletions src-tauri/src/modules/cloudflared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ use tokio::process::{Child, Command};
use tokio::sync::RwLock;
use tracing::{debug, info};

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[cfg(target_os = "windows")]
Expand Down
2 changes: 0 additions & 2 deletions src-tauri/src/modules/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ use std::fs;

use super::account::get_data_dir;
use crate::models::AppConfig;
use tracing::warn;

const CONFIG_FILE: &str = "gui_config.json";

/// Load application configuration
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/src/modules/integration.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use crate::models::Account;
use crate::modules::{db, device, process, version};
use std::fs;
use std::process::Command;

pub trait SystemIntegration: Send + Sync {
/// 当切换账号时执行的系统层操作(如杀进程、写入文件、注入数据库)
async fn on_account_switch(
Expand Down Expand Up @@ -181,6 +179,7 @@ fn write_to_system_keyring(account: &crate::models::Account) -> Result<(), Strin
#[cfg(target_os = "macos")]
{
use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::process::Command;
let encoded_payload = STANDARD.encode(&payload_json);
let full_keyring_value = format!("go-keyring-base64:{}", encoded_payload);

Expand Down Expand Up @@ -299,6 +298,7 @@ fn write_to_system_keyring(account: &crate::models::Account) -> Result<(), Strin
{
// 2.3 Linux Secret Service API
use std::io::Write;
use std::process::Command;
let mut child = Command::new("secret-tool")
.args([
"store",
Expand Down
6 changes: 3 additions & 3 deletions src-tauri/src/modules/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ fn get_antigravity_pids(target_ide: Option<&str>) -> Vec<u32> {
}

/// Close Antigravity processes
pub fn close_antigravity(timeout_secs: u64, target_ide: Option<&str>) -> Result<(), String> {
pub fn close_antigravity(_timeout_secs: u64, target_ide: Option<&str>) -> Result<(), String> {
crate::modules::logger::log_info(&format!("Closing Antigravity ({:?})...", target_ide));

#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -597,7 +597,7 @@ pub fn close_antigravity(timeout_secs: u64, target_ide: Option<&str>) -> Result<
}

// Wait for graceful exit (max 70% of timeout_secs)
let graceful_timeout = (timeout_secs * 7) / 10;
let graceful_timeout = (_timeout_secs * 7) / 10;
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_secs(graceful_timeout) {
if !is_antigravity_running(target_ide) {
Expand Down Expand Up @@ -705,7 +705,7 @@ pub fn close_antigravity(timeout_secs: u64, target_ide: Option<&str>) -> Result<
}

// Wait for graceful exit
let graceful_timeout = (timeout_secs * 7) / 10;
let graceful_timeout = (_timeout_secs * 7) / 10;
let start = std::time::Instant::now();
while start.elapsed() < Duration::from_secs(graceful_timeout) {
if !is_antigravity_running(target_ide) {
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/modules/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub fn check_cooldown(key: &str, cooldown_seconds: i64) -> bool {
}
}

#[allow(dead_code)]
pub fn start_scheduler(
app_handle: Option<tauri::AppHandle>,
proxy_state: crate::commands::proxy::ProxyServiceState,
Expand Down Expand Up @@ -306,6 +307,7 @@ pub fn start_scheduler(
}

/// Trigger immediate smart warmup check for a single account
#[allow(dead_code)]
pub async fn trigger_warmup_for_account(account: &Account) {
// Get valid token
let Ok((token, pid)) = quota::get_valid_token_for_warmup(account).await else {
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/modules/security_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ pub fn get_top_ips(limit: usize, hours: i64) -> Result<Vec<IpRanking>, String> {
}

/// 清理旧的 IP 访问日志
#[allow(dead_code)]
pub fn cleanup_old_ip_logs(days: i64) -> Result<usize, String> {
let conn = connect_db()?;

Expand Down Expand Up @@ -655,6 +656,7 @@ pub fn clear_ip_access_logs() -> Result<(), String> {
}

/// 获取 IP 访问日志总数
#[allow(dead_code)]
pub fn get_ip_access_logs_count(
ip_filter: Option<&str>,
blocked_only: bool,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/modules/user_token_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#![allow(dead_code)]
// 用户令牌存储,部分接口留作后续扩展

use chrono::{FixedOffset, Local, Timelike, Utc};
use chrono::{FixedOffset, Timelike, Utc};
use rusqlite::{params, Connection, OptionalExtension};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
Expand Down
Loading
Loading