fix(proxy): resolve 404 error and auto-setup proxy targets

Issues fixed:
1. Route prefix mismatch - Live config was set to use /claude, /codex,
   /gemini prefixes but server only had routes without prefixes
2. Proxy targets not auto-configured - start_with_takeover only modified
   Live config but didn't set is_proxy_target=true for current providers

Changes:
- Add prefixed routes (/claude/v1/messages, /codex/v1/*, /gemini/v1beta/*)
  to server.rs for backward compatibility
- Remove URL prefixes from takeover_live_configs() - server can route
  by API endpoint path alone (/v1/messages=Claude, /v1/chat/completions=Codex)
- Add setup_proxy_targets() method that automatically sets current providers
  as proxy targets when starting proxy with takeover
- Unify proxy toggle in SettingsPage to use takeover mode (same as main UI)
- Fix error message extraction in useProxyStatus hooks
- Fix provider switch logic to check both takeover flag AND proxy running state
This commit is contained in:
Jason
2025-12-10 19:58:43 +08:00
parent 3cdce2eced
commit 5cc864c6aa
17 changed files with 831 additions and 47 deletions
+319 -1
View File
@@ -2,9 +2,13 @@
//!
//! 提供代理服务器的启动、停止和配置管理
use crate::app_config::AppType;
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
use crate::database::Database;
use crate::proxy::server::ProxyServer;
use crate::proxy::types::*;
use serde_json::{json, Value};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -59,6 +63,66 @@ impl ProxyService {
Ok(info)
}
/// 启动代理服务器(带 Live 配置接管)
pub async fn start_with_takeover(&self) -> Result<ProxyServerInfo, String> {
// 1. 自动将各应用当前选中的供应商设置为代理目标
self.setup_proxy_targets().await?;
// 2. 备份各应用的 Live 配置
self.backup_live_configs().await?;
// 3. 接管各应用的 Live 配置(写入代理地址)
self.takeover_live_configs().await?;
// 4. 设置接管状态
self.db
.set_live_takeover_active(true)
.await
.map_err(|e| format!("设置接管状态失败: {e}"))?;
// 5. 启动代理服务器
match self.start().await {
Ok(info) => Ok(info),
Err(e) => {
// 启动失败,恢复原始配置
log::error!("代理启动失败,尝试恢复原始配置: {e}");
let _ = self.restore_live_configs().await;
let _ = self.db.set_live_takeover_active(false).await;
Err(e)
}
}
}
/// 自动设置代理目标:将各应用当前选中的供应商设置为代理目标
async fn setup_proxy_targets(&self) -> Result<(), String> {
let app_types = ["claude", "codex", "gemini"];
for app_type in app_types {
// 获取当前选中的供应商
if let Ok(Some(provider_id)) = self.db.get_current_provider(app_type) {
// 设置为代理目标
if let Err(e) = self.db.set_proxy_target(&provider_id, app_type, true).await {
log::warn!(
"设置 {} 的代理目标 {} 失败: {}",
app_type,
provider_id,
e
);
} else {
log::info!(
"已将 {} 的当前供应商 {} 设置为代理目标",
app_type,
provider_id
);
}
} else {
log::debug!("{} 没有当前供应商,跳过代理目标设置", app_type);
}
}
Ok(())
}
/// 停止代理服务器
pub async fn stop(&self) -> Result<(), String> {
if let Some(server) = self.server.write().await.take() {
@@ -73,6 +137,259 @@ impl ProxyService {
}
}
/// 停止代理服务器(恢复 Live 配置)
pub async fn stop_with_restore(&self) -> Result<(), String> {
// 1. 停止代理服务器
self.stop().await?;
// 2. 恢复原始 Live 配置
self.restore_live_configs().await?;
// 3. 清除接管状态
self.db
.set_live_takeover_active(false)
.await
.map_err(|e| format!("清除接管状态失败: {e}"))?;
// 4. 删除备份
self.db
.delete_all_live_backups()
.await
.map_err(|e| format!("删除备份失败: {e}"))?;
log::info!("代理已停止,Live 配置已恢复");
Ok(())
}
/// 备份各应用的 Live 配置
async fn backup_live_configs(&self) -> Result<(), String> {
// Claude
if let Ok(config) = self.read_claude_live() {
let json_str = serde_json::to_string(&config)
.map_err(|e| format!("序列化 Claude 配置失败: {e}"))?;
self.db
.save_live_backup("claude", &json_str)
.await
.map_err(|e| format!("备份 Claude 配置失败: {e}"))?;
}
// Codex
if let Ok(config) = self.read_codex_live() {
let json_str = serde_json::to_string(&config)
.map_err(|e| format!("序列化 Codex 配置失败: {e}"))?;
self.db
.save_live_backup("codex", &json_str)
.await
.map_err(|e| format!("备份 Codex 配置失败: {e}"))?;
}
// Gemini
if let Ok(config) = self.read_gemini_live() {
let json_str = serde_json::to_string(&config)
.map_err(|e| format!("序列化 Gemini 配置失败: {e}"))?;
self.db
.save_live_backup("gemini", &json_str)
.await
.map_err(|e| format!("备份 Gemini 配置失败: {e}"))?;
}
log::info!("已备份所有应用的 Live 配置");
Ok(())
}
/// 接管各应用的 Live 配置(写入代理地址)
///
/// 代理服务器的路由已经根据 API 端点自动区分应用类型:
/// - `/v1/messages` → Claude
/// - `/v1/chat/completions`, `/v1/responses` → Codex
/// - `/v1beta/*` → Gemini
///
/// 因此不需要在 URL 中添加应用前缀。
async fn takeover_live_configs(&self) -> Result<(), String> {
let config = self
.db
.get_proxy_config()
.await
.map_err(|e| format!("获取代理配置失败: {e}"))?;
let proxy_url = format!("http://{}:{}", config.listen_address, config.listen_port);
// Claude: 修改 ANTHROPIC_BASE_URL
if let Ok(mut live_config) = self.read_claude_live() {
if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) {
env.insert("ANTHROPIC_BASE_URL".to_string(), json!(&proxy_url));
} else {
live_config["env"] = json!({
"ANTHROPIC_BASE_URL": &proxy_url
});
}
self.write_claude_live(&live_config)?;
log::info!("Claude Live 配置已接管,代理地址: {}", proxy_url);
}
// Codex: 修改 OPENAI_BASE_URL
if let Ok(mut live_config) = self.read_codex_live() {
if let Some(auth) = live_config.get_mut("auth").and_then(|v| v.as_object_mut()) {
auth.insert("OPENAI_BASE_URL".to_string(), json!(&proxy_url));
}
self.write_codex_live(&live_config)?;
log::info!("Codex Live 配置已接管,代理地址: {}", proxy_url);
}
// Gemini: 修改 GEMINI_API_BASE
if let Ok(mut live_config) = self.read_gemini_live() {
if let Some(env) = live_config.get_mut("env").and_then(|v| v.as_object_mut()) {
env.insert("GEMINI_API_BASE".to_string(), json!(&proxy_url));
} else {
live_config["env"] = json!({
"GEMINI_API_BASE": &proxy_url
});
}
self.write_gemini_live(&live_config)?;
log::info!("Gemini Live 配置已接管,代理地址: {}", proxy_url);
}
Ok(())
}
/// 恢复原始 Live 配置
async fn restore_live_configs(&self) -> Result<(), String> {
// Claude
if let Ok(Some(backup)) = self.db.get_live_backup("claude").await {
let config: Value = serde_json::from_str(&backup.original_config)
.map_err(|e| format!("解析 Claude 备份失败: {e}"))?;
self.write_claude_live(&config)?;
log::info!("Claude Live 配置已恢复");
}
// Codex
if let Ok(Some(backup)) = self.db.get_live_backup("codex").await {
let config: Value = serde_json::from_str(&backup.original_config)
.map_err(|e| format!("解析 Codex 备份失败: {e}"))?;
self.write_codex_live(&config)?;
log::info!("Codex Live 配置已恢复");
}
// Gemini
if let Ok(Some(backup)) = self.db.get_live_backup("gemini").await {
let config: Value = serde_json::from_str(&backup.original_config)
.map_err(|e| format!("解析 Gemini 备份失败: {e}"))?;
self.write_gemini_live(&config)?;
log::info!("Gemini Live 配置已恢复");
}
Ok(())
}
/// 检查是否处于 Live 接管模式
pub async fn is_takeover_active(&self) -> Result<bool, String> {
self.db
.is_live_takeover_active()
.await
.map_err(|e| format!("检查接管状态失败: {e}"))
}
/// 代理模式下切换供应商(热切换,不写 Live)
pub async fn switch_proxy_target(
&self,
app_type: &str,
provider_id: &str,
) -> Result<(), String> {
// 更新数据库中的 is_current 标记
let app_type_enum = AppType::from_str(app_type)
.map_err(|_| format!("无效的应用类型: {app_type}"))?;
self.db
.set_current_provider(app_type_enum.as_str(), provider_id)
.map_err(|e| format!("更新当前供应商失败: {e}"))?;
log::info!(
"代理模式:已切换 {} 的目标供应商为 {}",
app_type,
provider_id
);
Ok(())
}
// ==================== Live 配置读写辅助方法 ====================
fn read_claude_live(&self) -> Result<Value, String> {
let path = get_claude_settings_path();
if !path.exists() {
return Err("Claude 配置文件不存在".to_string());
}
read_json_file(&path).map_err(|e| format!("读取 Claude 配置失败: {e}"))
}
fn write_claude_live(&self, config: &Value) -> Result<(), String> {
let path = get_claude_settings_path();
write_json_file(&path, config).map_err(|e| format!("写入 Claude 配置失败: {e}"))
}
fn read_codex_live(&self) -> Result<Value, String> {
use crate::codex_config::{get_codex_auth_path, get_codex_config_path};
let auth_path = get_codex_auth_path();
if !auth_path.exists() {
return Err("Codex auth.json 不存在".to_string());
}
let auth: Value =
read_json_file(&auth_path).map_err(|e| format!("读取 Codex auth 失败: {e}"))?;
let config_path = get_codex_config_path();
let config_str = if config_path.exists() {
std::fs::read_to_string(&config_path)
.map_err(|e| format!("读取 Codex config 失败: {e}"))?
} else {
String::new()
};
Ok(json!({
"auth": auth,
"config": config_str
}))
}
fn write_codex_live(&self, config: &Value) -> Result<(), String> {
use crate::codex_config::{get_codex_auth_path, get_codex_config_path};
if let Some(auth) = config.get("auth") {
let auth_path = get_codex_auth_path();
write_json_file(&auth_path, auth).map_err(|e| format!("写入 Codex auth 失败: {e}"))?;
}
if let Some(config_str) = config.get("config").and_then(|v| v.as_str()) {
let config_path = get_codex_config_path();
std::fs::write(&config_path, config_str)
.map_err(|e| format!("写入 Codex config 失败: {e}"))?;
}
Ok(())
}
fn read_gemini_live(&self) -> Result<Value, String> {
use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env};
let env_path = get_gemini_env_path();
if !env_path.exists() {
return Err("Gemini .env 文件不存在".to_string());
}
let env_map = read_gemini_env().map_err(|e| format!("读取 Gemini env 失败: {e}"))?;
Ok(env_to_json(&env_map))
}
fn write_gemini_live(&self, config: &Value) -> Result<(), String> {
use crate::gemini_config::{json_to_env, write_gemini_env_atomic};
let env_map = json_to_env(config).map_err(|e| format!("转换 Gemini 配置失败: {e}"))?;
write_gemini_env_atomic(&env_map).map_err(|e| format!("写入 Gemini env 失败: {e}"))?;
Ok(())
}
// ==================== 原有方法 ====================
/// 获取服务器状态
pub async fn get_status(&self) -> Result<ProxyStatus, String> {
if let Some(server) = self.server.read().await.as_ref() {
@@ -103,9 +420,10 @@ impl ProxyService {
.await
.map_err(|e| format!("获取代理配置失败: {e}"))?;
// 保存到数据库(保持 enabled 状态不变)
// 保存到数据库(保持 enabled 和 live_takeover_active 状态不变)
let mut new_config = config.clone();
new_config.enabled = previous.enabled;
new_config.live_takeover_active = previous.live_takeover_active;
self.db
.update_proxy_config(new_config.clone())