mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 16:56:16 +08:00
Merge branch 'main' into feat/provider-icon-color
# Conflicts: # src-tauri/src/proxy/types.rs # src-tauri/src/usage_script.rs # src/components/proxy/AutoFailoverConfigPanel.tsx
This commit is contained in:
+129
-33
@@ -69,8 +69,6 @@ pub struct ToolVersion {
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
||||
use std::process::Command;
|
||||
|
||||
let tools = vec!["claude", "codex", "gemini"];
|
||||
let mut results = Vec::new();
|
||||
|
||||
@@ -81,39 +79,16 @@ pub async fn get_tool_versions() -> Result<Vec<ToolVersion>, String> {
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
for tool in tools {
|
||||
// 1. 获取本地版本 (保持不变)
|
||||
// 1. 获取本地版本 - 先尝试直接执行,失败则扫描常见路径
|
||||
let (local_version, local_error) = {
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", &format!("{tool} --version")])
|
||||
.output()
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("{tool} --version"))
|
||||
.output()
|
||||
};
|
||||
// 先尝试直接执行
|
||||
let direct_result = try_get_version(tool);
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if out.status.success() {
|
||||
(
|
||||
Some(String::from_utf8_lossy(&out.stdout).trim().to_string()),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
(
|
||||
None,
|
||||
Some(if err.is_empty() {
|
||||
"未安装或无法执行".to_string()
|
||||
} else {
|
||||
err
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => (None, Some(e.to_string())),
|
||||
if direct_result.0.is_some() {
|
||||
direct_result
|
||||
} else {
|
||||
// 扫描常见的 npm 全局安装路径
|
||||
scan_cli_version(tool)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -153,3 +128,124 @@ async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Op
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从版本输出中提取纯版本号
|
||||
fn extract_version(raw: &str) -> String {
|
||||
// 匹配 semver 格式: x.y.z 或 x.y.z-xxx
|
||||
let re = regex::Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").unwrap();
|
||||
re.find(raw)
|
||||
.map(|m| m.as_str().to_string())
|
||||
.unwrap_or_else(|| raw.to_string())
|
||||
}
|
||||
|
||||
/// 尝试直接执行命令获取版本
|
||||
fn try_get_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
use std::process::Command;
|
||||
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", &format!("{tool} --version")])
|
||||
.output()
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("{tool} --version"))
|
||||
.output()
|
||||
};
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if out.status.success() {
|
||||
let raw = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
(Some(extract_version(&raw)), None)
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
||||
(
|
||||
None,
|
||||
Some(if err.is_empty() {
|
||||
"未安装或无法执行".to_string()
|
||||
} else {
|
||||
err
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(e) => (None, Some(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 扫描常见路径查找 CLI
|
||||
fn scan_cli_version(tool: &str) -> (Option<String>, Option<String>) {
|
||||
use std::process::Command;
|
||||
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
// 常见的 npm 全局安装路径
|
||||
let mut search_paths: Vec<std::path::PathBuf> = vec![
|
||||
home.join(".npm-global/bin"),
|
||||
home.join(".local/bin"),
|
||||
home.join("n/bin"), // n version manager
|
||||
];
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
search_paths.push(std::path::PathBuf::from("/opt/homebrew/bin"));
|
||||
search_paths.push(std::path::PathBuf::from("/usr/local/bin"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
search_paths.push(std::path::PathBuf::from("/usr/local/bin"));
|
||||
search_paths.push(std::path::PathBuf::from("/usr/bin"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(appdata) = dirs::data_dir() {
|
||||
search_paths.push(appdata.join("npm"));
|
||||
}
|
||||
search_paths.push(std::path::PathBuf::from("C:\\Program Files\\nodejs"));
|
||||
}
|
||||
|
||||
// 扫描 nvm 目录下的所有 node 版本
|
||||
let nvm_base = home.join(".nvm/versions/node");
|
||||
if nvm_base.exists() {
|
||||
if let Ok(entries) = std::fs::read_dir(&nvm_base) {
|
||||
for entry in entries.flatten() {
|
||||
let bin_path = entry.path().join("bin");
|
||||
if bin_path.exists() {
|
||||
search_paths.push(bin_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在每个路径中查找工具
|
||||
for path in &search_paths {
|
||||
let tool_path = if cfg!(target_os = "windows") {
|
||||
path.join(format!("{tool}.cmd"))
|
||||
} else {
|
||||
path.join(tool)
|
||||
};
|
||||
|
||||
if tool_path.exists() {
|
||||
// 构建 PATH 环境变量,确保 node 可被找到
|
||||
let current_path = std::env::var("PATH").unwrap_or_default();
|
||||
let new_path = format!("{}:{}", path.display(), current_path);
|
||||
|
||||
let output = Command::new(&tool_path)
|
||||
.arg("--version")
|
||||
.env("PATH", &new_path)
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if out.status.success() {
|
||||
let raw = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
return (Some(extract_version(&raw)), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(None, Some("未安装或无法执行".to_string()))
|
||||
}
|
||||
|
||||
@@ -7,18 +7,18 @@ use crate::proxy::types::*;
|
||||
use crate::proxy::{CircuitBreakerConfig, CircuitBreakerStats};
|
||||
use crate::store::AppState;
|
||||
|
||||
/// 启动代理服务器
|
||||
/// 启动代理服务器(带 Live 配置接管)
|
||||
#[tauri::command]
|
||||
pub async fn start_proxy_server(
|
||||
pub async fn start_proxy_with_takeover(
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ProxyServerInfo, String> {
|
||||
state.proxy_service.start().await
|
||||
state.proxy_service.start_with_takeover().await
|
||||
}
|
||||
|
||||
/// 停止代理服务器
|
||||
/// 停止代理服务器(恢复 Live 配置)
|
||||
#[tauri::command]
|
||||
pub async fn stop_proxy_server(state: tauri::State<'_, AppState>) -> Result<(), String> {
|
||||
state.proxy_service.stop().await
|
||||
pub async fn stop_proxy_with_restore(state: tauri::State<'_, AppState>) -> Result<(), String> {
|
||||
state.proxy_service.stop_with_restore().await
|
||||
}
|
||||
|
||||
/// 获取代理服务器状态
|
||||
@@ -48,6 +48,25 @@ pub async fn is_proxy_running(state: tauri::State<'_, AppState>) -> Result<bool,
|
||||
Ok(state.proxy_service.is_running().await)
|
||||
}
|
||||
|
||||
/// 检查是否处于 Live 接管模式
|
||||
#[tauri::command]
|
||||
pub async fn is_live_takeover_active(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||||
state.proxy_service.is_takeover_active().await
|
||||
}
|
||||
|
||||
/// 代理模式下切换供应商(热切换)
|
||||
#[tauri::command]
|
||||
pub async fn switch_proxy_provider(
|
||||
state: tauri::State<'_, AppState>,
|
||||
app_type: String,
|
||||
provider_id: String,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.proxy_service
|
||||
.switch_proxy_target(&app_type, &provider_id)
|
||||
.await
|
||||
}
|
||||
|
||||
// ==================== 故障转移相关命令 ====================
|
||||
|
||||
/// 获取代理目标列表
|
||||
|
||||
@@ -483,6 +483,26 @@ impl Database {
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
/// 更新供应商的 settings_config(仅更新配置,不改变其他字段)
|
||||
pub fn update_provider_settings_config(
|
||||
&self,
|
||||
app_type: &str,
|
||||
provider_id: &str,
|
||||
settings_config: &serde_json::Value,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
"UPDATE providers SET settings_config = ?1 WHERE id = ?2 AND app_type = ?3",
|
||||
params![
|
||||
serde_json::to_string(settings_config).unwrap(),
|
||||
provider_id,
|
||||
app_type
|
||||
],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 添加自定义端点
|
||||
pub fn add_custom_endpoint(
|
||||
&self,
|
||||
|
||||
@@ -17,7 +17,7 @@ impl Database {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.query_row(
|
||||
"SELECT enabled, listen_address, listen_port, max_retries,
|
||||
request_timeout, enable_logging
|
||||
request_timeout, enable_logging, live_takeover_active
|
||||
FROM proxy_config WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
@@ -28,6 +28,7 @@ impl Database {
|
||||
max_retries: row.get::<_, i32>(3)? as u8,
|
||||
request_timeout: row.get::<_, i32>(4)? as u64,
|
||||
enable_logging: row.get::<_, i32>(5)? != 0,
|
||||
live_takeover_active: row.get::<_, i32>(6).unwrap_or(0) != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -51,8 +52,8 @@ impl Database {
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO proxy_config
|
||||
(id, enabled, listen_address, listen_port, max_retries, request_timeout, enable_logging, target_app, created_at, updated_at)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7,
|
||||
(id, enabled, listen_address, listen_port, max_retries, request_timeout, enable_logging, live_takeover_active, target_app, created_at, updated_at)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8,
|
||||
COALESCE((SELECT created_at FROM proxy_config WHERE id = 1), datetime('now')),
|
||||
datetime('now'))",
|
||||
rusqlite::params![
|
||||
@@ -62,6 +63,7 @@ impl Database {
|
||||
config.max_retries as i32,
|
||||
config.request_timeout as i32,
|
||||
if config.enable_logging { 1 } else { 0 },
|
||||
if config.live_takeover_active { 1 } else { 0 },
|
||||
"claude", // 兼容旧字段,写入默认值
|
||||
],
|
||||
)
|
||||
@@ -70,6 +72,30 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 设置 Live 接管状态
|
||||
pub async fn set_live_takeover_active(&self, active: bool) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
"UPDATE proxy_config SET live_takeover_active = ?1, updated_at = datetime('now') WHERE id = 1",
|
||||
rusqlite::params![if active { 1 } else { 0 }],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查是否处于 Live 接管模式
|
||||
pub async fn is_live_takeover_active(&self) -> Result<bool, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let active: i32 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(live_takeover_active, 0) FROM proxy_config WHERE id = 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
Ok(active != 0)
|
||||
}
|
||||
|
||||
// ==================== Provider Health ====================
|
||||
|
||||
/// 获取Provider健康状态
|
||||
@@ -241,4 +267,74 @@ impl Database {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ==================== Live Backup ====================
|
||||
|
||||
/// 保存 Live 配置备份
|
||||
pub async fn save_live_backup(
|
||||
&self,
|
||||
app_type: &str,
|
||||
config_json: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO proxy_live_backup (app_type, original_config, backed_up_at)
|
||||
VALUES (?1, ?2, ?3)",
|
||||
rusqlite::params![app_type, config_json, now],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
log::info!("已备份 {app_type} Live 配置");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取 Live 配置备份
|
||||
pub async fn get_live_backup(&self, app_type: &str) -> Result<Option<LiveBackup>, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
let result = conn.query_row(
|
||||
"SELECT app_type, original_config, backed_up_at FROM proxy_live_backup WHERE app_type = ?1",
|
||||
rusqlite::params![app_type],
|
||||
|row| {
|
||||
Ok(LiveBackup {
|
||||
app_type: row.get(0)?,
|
||||
original_config: row.get(1)?,
|
||||
backed_up_at: row.get(2)?,
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(backup) => Ok(Some(backup)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除 Live 配置备份
|
||||
pub async fn delete_live_backup(&self, app_type: &str) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM proxy_live_backup WHERE app_type = ?1",
|
||||
rusqlite::params![app_type],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
log::info!("已删除 {app_type} Live 配置备份");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 删除所有 Live 配置备份
|
||||
pub async fn delete_all_live_backups(&self) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
conn.execute("DELETE FROM proxy_live_backup", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
log::info!("已删除所有 Live 配置备份");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,23 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 16. Proxy Live Backup 表 (Live 配置备份)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS proxy_live_backup (
|
||||
app_type TEXT PRIMARY KEY,
|
||||
original_config TEXT NOT NULL,
|
||||
backed_up_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 尝试添加 live_takeover_active 列到 proxy_config 表
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE proxy_config ADD COLUMN live_takeover_active INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+59
-3
@@ -530,7 +530,7 @@ pub fn run() {
|
||||
Ok(config) => {
|
||||
if config.enabled {
|
||||
log::info!("代理服务配置为启用,正在启动...");
|
||||
match state.proxy_service.start().await {
|
||||
match state.proxy_service.start_with_takeover().await {
|
||||
Ok(info) => log::info!(
|
||||
"代理服务器自动启动成功: {}:{}",
|
||||
info.address,
|
||||
@@ -647,12 +647,14 @@ pub fn run() {
|
||||
commands::set_auto_launch,
|
||||
commands::get_auto_launch_status,
|
||||
// Proxy server management
|
||||
commands::start_proxy_server,
|
||||
commands::stop_proxy_server,
|
||||
commands::start_proxy_with_takeover,
|
||||
commands::stop_proxy_with_restore,
|
||||
commands::get_proxy_status,
|
||||
commands::get_proxy_config,
|
||||
commands::update_proxy_config,
|
||||
commands::is_proxy_running,
|
||||
commands::is_live_takeover_active,
|
||||
commands::switch_proxy_provider,
|
||||
// Proxy failover commands
|
||||
commands::get_proxy_targets,
|
||||
commands::set_proxy_target,
|
||||
@@ -685,6 +687,22 @@ pub fn run() {
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|app_handle, event| {
|
||||
// 处理退出请求(所有平台)
|
||||
if let RunEvent::ExitRequested { api, .. } = &event {
|
||||
log::info!("收到退出请求,开始清理...");
|
||||
// 阻止立即退出,执行清理
|
||||
api.prevent_exit();
|
||||
|
||||
let app_handle = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
cleanup_before_exit(&app_handle).await;
|
||||
log::info!("清理完成,退出应用");
|
||||
// 使用 std::process::exit 避免再次触发 ExitRequested
|
||||
std::process::exit(0);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
match event {
|
||||
@@ -764,6 +782,44 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 应用退出清理
|
||||
// ============================================================
|
||||
|
||||
/// 应用退出前的清理工作
|
||||
///
|
||||
/// 在应用退出前检查代理服务器状态,如果正在运行则停止代理并恢复 Live 配置。
|
||||
/// 确保 Claude Code/Codex/Gemini 的配置不会处于损坏状态。
|
||||
pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) {
|
||||
if let Some(state) = app_handle.try_state::<store::AppState>() {
|
||||
let proxy_service = &state.proxy_service;
|
||||
|
||||
// 检查代理是否在运行
|
||||
if proxy_service.is_running().await {
|
||||
log::info!("检测到代理服务器正在运行,开始清理...");
|
||||
|
||||
// 检查是否处于 Live 接管模式
|
||||
if let Ok(is_takeover) = state.db.is_live_takeover_active().await {
|
||||
if is_takeover {
|
||||
// 接管模式:停止并恢复配置
|
||||
if let Err(e) = proxy_service.stop_with_restore().await {
|
||||
log::error!("退出时恢复 Live 配置失败: {e}");
|
||||
} else {
|
||||
log::info!("已恢复 Live 配置");
|
||||
}
|
||||
} else {
|
||||
// 非接管模式:仅停止代理
|
||||
if let Err(e) = proxy_service.stop().await {
|
||||
log::error!("退出时停止代理失败: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("代理服务器清理完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 迁移错误对话框辅助函数
|
||||
// ============================================================
|
||||
|
||||
@@ -30,58 +30,42 @@ impl ProviderRouter {
|
||||
/// 选择可用的供应商(支持故障转移)
|
||||
/// 返回按优先级排序的可用供应商列表
|
||||
pub async fn select_providers(&self, app_type: &str) -> Result<Vec<Provider>, AppError> {
|
||||
// 1. 获取所有启用代理的供应商
|
||||
let providers = self.db.get_proxy_targets(app_type).await?;
|
||||
// 直接获取当前选中的供应商(基于 is_current 字段)
|
||||
let current_id = self
|
||||
.db
|
||||
.get_current_provider(app_type)?
|
||||
.ok_or_else(|| AppError::Config(format!("No current provider for {}", app_type)))?;
|
||||
|
||||
if providers.is_empty() {
|
||||
return Err(AppError::Config(
|
||||
"No proxy target providers configured".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Found {} proxy target providers for app_type: {}",
|
||||
providers.len(),
|
||||
app_type
|
||||
);
|
||||
|
||||
// 2. 按 sort_index 排序(已经在数据库查询中排序了)
|
||||
let sorted_providers: Vec<_> = providers.into_values().collect();
|
||||
|
||||
// 3. 过滤可用的供应商(检查熔断器状态)
|
||||
let mut available_providers = Vec::new();
|
||||
|
||||
for provider in sorted_providers {
|
||||
let circuit_key = format!("{}:{}", app_type, provider.id);
|
||||
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
|
||||
|
||||
if breaker.allow_request().await {
|
||||
log::debug!(
|
||||
"Provider {} is available (circuit state: {:?})",
|
||||
provider.id,
|
||||
breaker.get_state().await
|
||||
);
|
||||
available_providers.push(provider);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Provider {} is unavailable (circuit breaker open)",
|
||||
provider.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if available_providers.is_empty() {
|
||||
return Err(AppError::Config(
|
||||
"All proxy target providers are unavailable (circuit breakers open)".to_string(),
|
||||
));
|
||||
}
|
||||
let providers = self.db.get_all_providers(app_type)?;
|
||||
let provider = providers
|
||||
.get(¤t_id)
|
||||
.ok_or_else(|| AppError::Config(format!("Current provider {} not found", current_id)))?
|
||||
.clone();
|
||||
|
||||
log::info!(
|
||||
"Selected {} available providers for failover chain",
|
||||
available_providers.len()
|
||||
"[{}] Selected current provider: {} ({})",
|
||||
app_type,
|
||||
provider.name,
|
||||
provider.id
|
||||
);
|
||||
|
||||
Ok(available_providers)
|
||||
// 检查熔断器状态
|
||||
let circuit_key = format!("{}:{}", app_type, provider.id);
|
||||
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
|
||||
|
||||
if !breaker.allow_request().await {
|
||||
log::warn!(
|
||||
"Provider {} is unavailable (circuit breaker open)",
|
||||
provider.id
|
||||
);
|
||||
return Err(AppError::Config(format!(
|
||||
"Current provider {} is unavailable (circuit breaker open)",
|
||||
provider.name
|
||||
)));
|
||||
}
|
||||
|
||||
// 返回单个供应商(保留 Vec 接口以兼容现有代码)
|
||||
Ok(vec![provider])
|
||||
}
|
||||
|
||||
/// 记录供应商请求结果
|
||||
|
||||
@@ -148,17 +148,24 @@ impl ProxyServer {
|
||||
// 健康检查
|
||||
.route("/health", get(handlers::health_check))
|
||||
.route("/status", get(handlers::get_status))
|
||||
// Claude API
|
||||
// Claude API (支持带前缀和不带前缀两种格式)
|
||||
.route("/v1/messages", post(handlers::handle_messages))
|
||||
// OpenAI Chat Completions API (Codex CLI)
|
||||
.route("/claude/v1/messages", post(handlers::handle_messages))
|
||||
// OpenAI Chat Completions API (Codex CLI,支持带前缀和不带前缀)
|
||||
.route(
|
||||
"/v1/chat/completions",
|
||||
post(handlers::handle_chat_completions),
|
||||
)
|
||||
// OpenAI Responses API (Codex CLI)
|
||||
.route(
|
||||
"/codex/v1/chat/completions",
|
||||
post(handlers::handle_chat_completions),
|
||||
)
|
||||
// OpenAI Responses API (Codex CLI,支持带前缀和不带前缀)
|
||||
.route("/v1/responses", post(handlers::handle_responses))
|
||||
// Gemini API
|
||||
.route("/codex/v1/responses", post(handlers::handle_responses))
|
||||
// Gemini API (支持带前缀和不带前缀)
|
||||
.route("/v1beta/*path", post(handlers::handle_gemini))
|
||||
.route("/gemini/v1beta/*path", post(handlers::handle_gemini))
|
||||
.layer(cors)
|
||||
.with_state(self.state.clone())
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub struct ProxyConfig {
|
||||
pub request_timeout: u64,
|
||||
/// 是否启用日志
|
||||
pub enable_logging: bool,
|
||||
/// 是否正在接管 Live 配置
|
||||
#[serde(default)]
|
||||
pub live_takeover_active: bool,
|
||||
}
|
||||
|
||||
impl Default for ProxyConfig {
|
||||
@@ -26,6 +29,7 @@ impl Default for ProxyConfig {
|
||||
max_retries: 3,
|
||||
request_timeout: 300,
|
||||
enable_logging: true,
|
||||
live_takeover_active: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,3 +107,14 @@ pub struct ProviderHealth {
|
||||
pub last_error: Option<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
/// Live 配置备份记录
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LiveBackup {
|
||||
/// 应用类型 (claude/codex/gemini)
|
||||
pub app_type: String,
|
||||
/// 原始配置 JSON
|
||||
pub original_config: String,
|
||||
/// 备份时间
|
||||
pub backed_up_at: String,
|
||||
}
|
||||
|
||||
@@ -173,14 +173,71 @@ 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
|
||||
state.db.set_current_provider(app_type.as_str(), id)?;
|
||||
|
||||
// 同时更新 is_proxy_target(代理路由器使用此字段选择供应商)
|
||||
state.db.set_proxy_target_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 via is_proxy_target
|
||||
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,196 @@ 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 配置中的 Token 到数据库(确保代理能读到最新的 Token)
|
||||
self.sync_live_to_providers().await?;
|
||||
|
||||
// 4. 接管各应用的 Live 配置(写入代理地址,清空 Token)
|
||||
self.takeover_live_configs().await?;
|
||||
|
||||
// 5. 设置接管状态
|
||||
self.db
|
||||
.set_live_takeover_active(true)
|
||||
.await
|
||||
.map_err(|e| format!("设置接管状态失败: {e}"))?;
|
||||
|
||||
// 6. 启动代理服务器
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 同步 Live 配置中的 Token 到数据库
|
||||
///
|
||||
/// 在清空 Live Token 之前调用,确保数据库中的 Provider 配置有最新的 Token。
|
||||
/// 这样代理才能从数据库读取到正确的认证信息。
|
||||
async fn sync_live_to_providers(&self) -> Result<(), String> {
|
||||
// Claude: 同步 ANTHROPIC_AUTH_TOKEN
|
||||
if let Ok(live_config) = self.read_claude_live() {
|
||||
if let Some(provider_id) = self.db.get_current_provider("claude").ok().flatten() {
|
||||
if let Ok(Some(mut provider)) = self.db.get_provider_by_id(&provider_id, "claude") {
|
||||
// 从 live 配置提取 token
|
||||
if let Some(env) = live_config.get("env") {
|
||||
if let Some(token) =
|
||||
env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str())
|
||||
{
|
||||
if !token.is_empty() {
|
||||
// 更新 provider 的 settings_config
|
||||
if let Some(env_obj) = provider
|
||||
.settings_config
|
||||
.get_mut("env")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
env_obj
|
||||
.insert("ANTHROPIC_AUTH_TOKEN".to_string(), json!(token));
|
||||
} else {
|
||||
provider.settings_config["env"] = json!({
|
||||
"ANTHROPIC_AUTH_TOKEN": token
|
||||
});
|
||||
}
|
||||
// 保存到数据库
|
||||
if let Err(e) = self.db.update_provider_settings_config(
|
||||
"claude",
|
||||
&provider_id,
|
||||
&provider.settings_config,
|
||||
) {
|
||||
log::warn!("同步 Claude Token 到数据库失败: {e}");
|
||||
} else {
|
||||
log::info!(
|
||||
"已同步 Claude Token 到数据库 (provider: {})",
|
||||
provider_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Codex: 同步 OPENAI_API_KEY
|
||||
if let Ok(live_config) = self.read_codex_live() {
|
||||
if let Some(provider_id) = self.db.get_current_provider("codex").ok().flatten() {
|
||||
if let Ok(Some(mut provider)) = self.db.get_provider_by_id(&provider_id, "codex") {
|
||||
// 从 live 配置提取 token
|
||||
if let Some(auth) = live_config.get("auth") {
|
||||
if let Some(token) = auth.get("OPENAI_API_KEY").and_then(|v| v.as_str()) {
|
||||
if !token.is_empty() {
|
||||
// 更新 provider 的 settings_config
|
||||
if let Some(auth_obj) = provider
|
||||
.settings_config
|
||||
.get_mut("auth")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
auth_obj.insert("OPENAI_API_KEY".to_string(), json!(token));
|
||||
} else {
|
||||
provider.settings_config["auth"] = json!({
|
||||
"OPENAI_API_KEY": token
|
||||
});
|
||||
}
|
||||
// 保存到数据库
|
||||
if let Err(e) = self.db.update_provider_settings_config(
|
||||
"codex",
|
||||
&provider_id,
|
||||
&provider.settings_config,
|
||||
) {
|
||||
log::warn!("同步 Codex Token 到数据库失败: {e}");
|
||||
} else {
|
||||
log::info!(
|
||||
"已同步 Codex Token 到数据库 (provider: {})",
|
||||
provider_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini: 同步 GOOGLE_API_KEY
|
||||
if let Ok(live_config) = self.read_gemini_live() {
|
||||
if let Some(provider_id) = self.db.get_current_provider("gemini").ok().flatten() {
|
||||
if let Ok(Some(mut provider)) = self.db.get_provider_by_id(&provider_id, "gemini") {
|
||||
// 从 live 配置提取 token
|
||||
if let Some(env) = live_config.get("env") {
|
||||
if let Some(token) = env.get("GOOGLE_API_KEY").and_then(|v| v.as_str()) {
|
||||
if !token.is_empty() {
|
||||
// 更新 provider 的 settings_config
|
||||
if let Some(env_obj) = provider
|
||||
.settings_config
|
||||
.get_mut("env")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
env_obj.insert("GOOGLE_API_KEY".to_string(), json!(token));
|
||||
} else {
|
||||
provider.settings_config["env"] = json!({
|
||||
"GOOGLE_API_KEY": token
|
||||
});
|
||||
}
|
||||
// 保存到数据库
|
||||
if let Err(e) = self.db.update_provider_settings_config(
|
||||
"gemini",
|
||||
&provider_id,
|
||||
&provider.settings_config,
|
||||
) {
|
||||
log::warn!("同步 Gemini Token 到数据库失败: {e}");
|
||||
} else {
|
||||
log::info!(
|
||||
"已同步 Gemini Token 到数据库 (provider: {})",
|
||||
provider_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Live 配置 Token 同步完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止代理服务器
|
||||
pub async fn stop(&self) -> Result<(), String> {
|
||||
if let Some(server) = self.server.write().await.take() {
|
||||
@@ -66,6 +260,13 @@ impl ProxyService {
|
||||
.stop()
|
||||
.await
|
||||
.map_err(|e| format!("停止代理服务器失败: {e}"))?;
|
||||
|
||||
// 将 enabled 设为 false,避免下次启动时自动开启
|
||||
if let Ok(mut config) = self.db.get_proxy_config().await {
|
||||
config.enabled = false;
|
||||
let _ = self.db.update_proxy_config(config).await;
|
||||
}
|
||||
|
||||
log::info!("代理服务器已停止");
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -73,6 +274,267 @@ 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,使用占位符替代真实 Token(代理会注入真实 Token)
|
||||
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));
|
||||
// 使用占位符,避免 Claude Code 显示缺少 key 的警告
|
||||
env.insert("ANTHROPIC_AUTH_TOKEN".to_string(), json!("PROXY_MANAGED"));
|
||||
} else {
|
||||
live_config["env"] = json!({
|
||||
"ANTHROPIC_BASE_URL": &proxy_url,
|
||||
"ANTHROPIC_AUTH_TOKEN": "PROXY_MANAGED"
|
||||
});
|
||||
}
|
||||
self.write_claude_live(&live_config)?;
|
||||
log::info!("Claude Live 配置已接管,代理地址: {}", proxy_url);
|
||||
}
|
||||
|
||||
// Codex: 修改 OPENAI_BASE_URL,使用占位符替代真实 Token(代理会注入真实 Token)
|
||||
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));
|
||||
// 使用占位符,避免显示缺少 key 的警告
|
||||
auth.insert("OPENAI_API_KEY".to_string(), json!("PROXY_MANAGED"));
|
||||
}
|
||||
self.write_codex_live(&live_config)?;
|
||||
log::info!("Codex Live 配置已接管,代理地址: {}", proxy_url);
|
||||
}
|
||||
|
||||
// Gemini: 修改 GEMINI_API_BASE,使用占位符替代真实 Token(代理会注入真实 Token)
|
||||
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));
|
||||
// 使用占位符,避免显示缺少 key 的警告
|
||||
env.insert("GOOGLE_API_KEY".to_string(), json!("PROXY_MANAGED"));
|
||||
} else {
|
||||
live_config["env"] = json!({
|
||||
"GEMINI_API_BASE": &proxy_url,
|
||||
"GOOGLE_API_KEY": "PROXY_MANAGED"
|
||||
});
|
||||
}
|
||||
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 +565,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