mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 01:25:33 +08:00
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:
@@ -173,14 +173,70 @@ impl ProviderService {
|
||||
///
|
||||
/// Switch flow:
|
||||
/// 1. Validate target provider exists
|
||||
/// 2. **Backfill mechanism**: Backfill current live config to current provider, protect user manual modifications
|
||||
/// 3. Update local settings current_provider_xxx (device-level)
|
||||
/// 4. Update database is_current (as default for new devices)
|
||||
/// 5. Write target provider config to live files
|
||||
/// 6. Sync MCP configuration
|
||||
/// 2. Check if proxy takeover mode is active AND proxy server is running
|
||||
/// 3. If takeover mode active: hot-switch proxy target only (no Live config write)
|
||||
/// 4. If normal mode:
|
||||
/// a. **Backfill mechanism**: Backfill current live config to current provider
|
||||
/// b. Update local settings current_provider_xxx (device-level)
|
||||
/// c. Update database is_current (as default for new devices)
|
||||
/// d. Write target provider config to live files
|
||||
/// e. Sync MCP configuration
|
||||
pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||
// Check if provider exists
|
||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||
let _provider = providers
|
||||
.get(id)
|
||||
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
||||
|
||||
// Check if proxy takeover mode is active AND proxy server is actually running
|
||||
// Both conditions must be true to use hot-switch mode
|
||||
// Use blocking wait since this is a sync function
|
||||
let is_takeover_flag =
|
||||
futures::executor::block_on(state.db.is_live_takeover_active()).unwrap_or(false);
|
||||
let is_proxy_running = futures::executor::block_on(state.proxy_service.is_running());
|
||||
|
||||
// Hot-switch only when BOTH: takeover flag is set AND proxy server is actually running
|
||||
let should_hot_switch = is_takeover_flag && is_proxy_running;
|
||||
|
||||
if should_hot_switch {
|
||||
// Proxy takeover mode: hot-switch only, don't write Live config
|
||||
log::info!(
|
||||
"代理接管模式:热切换 {} 的目标供应商为 {}",
|
||||
app_type.as_str(),
|
||||
id
|
||||
);
|
||||
|
||||
// Update database is_current (proxy server reads this)
|
||||
state.db.set_current_provider(app_type.as_str(), id)?;
|
||||
|
||||
// Update local settings for consistency
|
||||
crate::settings::set_current_provider(&app_type, Some(id))?;
|
||||
|
||||
// Note: No Live config write, no MCP sync
|
||||
// The proxy server will route requests to the new provider
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Normal mode: full switch with Live config write
|
||||
// Also clear stale takeover flag if proxy is not running but flag was set
|
||||
if is_takeover_flag && !is_proxy_running {
|
||||
log::warn!(
|
||||
"检测到代理接管标志残留(代理已停止),清除标志并执行正常切换"
|
||||
);
|
||||
// Clear stale takeover flag
|
||||
let _ = futures::executor::block_on(state.db.set_live_takeover_active(false));
|
||||
}
|
||||
|
||||
Self::switch_normal(state, app_type, id, &providers)
|
||||
}
|
||||
|
||||
/// Normal switch flow (non-proxy mode)
|
||||
fn switch_normal(
|
||||
state: &AppState,
|
||||
app_type: AppType,
|
||||
id: &str,
|
||||
providers: &indexmap::IndexMap<String, Provider>,
|
||||
) -> Result<(), AppError> {
|
||||
let provider = providers
|
||||
.get(id)
|
||||
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user