mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(proxy): implement per-app takeover mode
Replace global live takeover with granular per-app control: - Add start_proxy_server command (start without takeover) - Add get_proxy_takeover_status to query each app's state - Add set_proxy_takeover_for_app for individual app control - Use live backup existence as SSOT for takeover state - Refactor sync_live_to_provider to eliminate code duplication - Update ProxyToggle to show status per active app
This commit is contained in:
@@ -6,6 +6,14 @@ use crate::proxy::types::*;
|
||||
use crate::proxy::{CircuitBreakerConfig, CircuitBreakerStats};
|
||||
use crate::store::AppState;
|
||||
|
||||
/// 启动代理服务器(仅启动服务,不接管 Live 配置)
|
||||
#[tauri::command]
|
||||
pub async fn start_proxy_server(
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ProxyServerInfo, String> {
|
||||
state.proxy_service.start(true).await
|
||||
}
|
||||
|
||||
/// 启动代理服务器(带 Live 配置接管)
|
||||
#[tauri::command]
|
||||
pub async fn start_proxy_with_takeover(
|
||||
@@ -20,6 +28,27 @@ pub async fn stop_proxy_with_restore(state: tauri::State<'_, AppState>) -> Resul
|
||||
state.proxy_service.stop_with_restore().await
|
||||
}
|
||||
|
||||
/// 获取各应用接管状态
|
||||
#[tauri::command]
|
||||
pub async fn get_proxy_takeover_status(
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ProxyTakeoverStatus, String> {
|
||||
state.proxy_service.get_takeover_status().await
|
||||
}
|
||||
|
||||
/// 为指定应用开启/关闭接管
|
||||
#[tauri::command]
|
||||
pub async fn set_proxy_takeover_for_app(
|
||||
state: tauri::State<'_, AppState>,
|
||||
app_type: String,
|
||||
enabled: bool,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.proxy_service
|
||||
.set_takeover_for_app(&app_type, enabled)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 获取代理服务器状态
|
||||
#[tauri::command]
|
||||
pub async fn get_proxy_status(state: tauri::State<'_, AppState>) -> Result<ProxyStatus, String> {
|
||||
|
||||
@@ -85,15 +85,8 @@ impl Database {
|
||||
|
||||
/// 检查是否处于 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)
|
||||
// v3.7.0+:以 proxy_live_backup 是否存在作为“接管状态”的真实来源(更贴近 per-app 接管)
|
||||
self.has_any_live_backup().await
|
||||
}
|
||||
|
||||
// ==================== Provider Health ====================
|
||||
|
||||
@@ -574,7 +574,7 @@ pub fn run() {
|
||||
Ok(config) => {
|
||||
if config.enabled {
|
||||
log::info!("代理服务配置为启用,正在启动...");
|
||||
match state.proxy_service.start_with_takeover().await {
|
||||
match state.proxy_service.start(true).await {
|
||||
Ok(info) => log::info!(
|
||||
"代理服务器自动启动成功: {}:{}",
|
||||
info.address,
|
||||
@@ -690,8 +690,11 @@ pub fn run() {
|
||||
commands::set_auto_launch,
|
||||
commands::get_auto_launch_status,
|
||||
// Proxy server management
|
||||
commands::start_proxy_server,
|
||||
commands::start_proxy_with_takeover,
|
||||
commands::stop_proxy_with_restore,
|
||||
commands::get_proxy_takeover_status,
|
||||
commands::set_proxy_takeover_for_app,
|
||||
commands::get_proxy_status,
|
||||
commands::get_proxy_config,
|
||||
commands::update_proxy_config,
|
||||
|
||||
@@ -86,6 +86,14 @@ pub struct ProxyServerInfo {
|
||||
pub started_at: String,
|
||||
}
|
||||
|
||||
/// 各应用的接管状态(是否改写该应用的 Live 配置指向本地代理)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ProxyTakeoverStatus {
|
||||
pub claude: bool,
|
||||
pub codex: bool,
|
||||
pub gemini: bool,
|
||||
}
|
||||
|
||||
/// API 格式类型(预留,当前不需要格式转换)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -147,10 +147,13 @@ impl ProviderService {
|
||||
// 如果代理接管模式处于激活状态,并且代理服务正在运行:
|
||||
// - 不写 Live 配置(否则会破坏接管)
|
||||
// - 仅更新 Live 备份(保证关闭代理时能恢复到最新配置)
|
||||
let is_takeover_flag =
|
||||
futures::executor::block_on(state.db.is_live_takeover_active()).unwrap_or(false);
|
||||
let is_app_taken_over =
|
||||
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
let is_proxy_running = futures::executor::block_on(state.proxy_service.is_running());
|
||||
let should_skip_live_write = is_takeover_flag && is_proxy_running;
|
||||
let should_skip_live_write = is_app_taken_over && is_proxy_running;
|
||||
|
||||
if should_skip_live_write {
|
||||
futures::executor::block_on(
|
||||
@@ -160,12 +163,6 @@ impl ProviderService {
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||
} else {
|
||||
// 如果检测到接管标志残留但代理未运行,清理标志后再执行正常写入
|
||||
if is_takeover_flag && !is_proxy_running {
|
||||
log::warn!("检测到代理接管标志残留(代理已停止),清除标志并写入 Live 配置");
|
||||
let _ = futures::executor::block_on(state.db.set_live_takeover_active(false));
|
||||
}
|
||||
|
||||
write_live_snapshot(&app_type, &provider)?;
|
||||
// Sync MCP
|
||||
McpService::sync_all_enabled(state)?;
|
||||
@@ -214,12 +211,15 @@ impl ProviderService {
|
||||
// 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_app_taken_over =
|
||||
futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some();
|
||||
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;
|
||||
// Hot-switch only when BOTH: this app is taken over AND proxy server is actually running
|
||||
let should_hot_switch = is_app_taken_over && is_proxy_running;
|
||||
|
||||
if should_hot_switch {
|
||||
// Proxy takeover mode: hot-switch only, don't write Live config
|
||||
@@ -254,13 +254,6 @@ impl ProviderService {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
||||
+590
-165
@@ -41,7 +41,10 @@ impl ProxyService {
|
||||
}
|
||||
|
||||
/// 启动代理服务器
|
||||
pub async fn start(&self) -> Result<ProxyServerInfo, String> {
|
||||
///
|
||||
/// - `persist_enabled = true`:将 `proxy_config.enabled` 持久化为启用(用于“总开关”)
|
||||
/// - `persist_enabled = false`:仅在当前进程启动代理服务(用于“按 App 接管”自动启动)
|
||||
pub async fn start(&self, persist_enabled: bool) -> Result<ProxyServerInfo, String> {
|
||||
// 1. 获取配置
|
||||
let mut config = self
|
||||
.db
|
||||
@@ -49,12 +52,27 @@ impl ProxyService {
|
||||
.await
|
||||
.map_err(|e| format!("获取代理配置失败: {e}"))?;
|
||||
|
||||
// 2. 确保配置启用(用户通过UI启动即表示希望启用)
|
||||
config.enabled = true;
|
||||
// 2. 仅在需要时持久化 enabled(避免“按 App 接管”自动启动时误打开总开关)
|
||||
if persist_enabled {
|
||||
config.enabled = true;
|
||||
}
|
||||
|
||||
// 3. 检查是否已在运行
|
||||
if self.server.read().await.is_some() {
|
||||
return Err("代理服务已在运行中".to_string());
|
||||
// 3. 若已在运行:确保持久化状态(如需要)并返回当前信息
|
||||
if let Some(server) = self.server.read().await.as_ref() {
|
||||
if persist_enabled {
|
||||
self.db
|
||||
.update_proxy_config(config)
|
||||
.await
|
||||
.map_err(|e| format!("保存代理配置失败: {e}"))?;
|
||||
}
|
||||
|
||||
let status = server.get_status().await;
|
||||
return Ok(ProxyServerInfo {
|
||||
address: status.address,
|
||||
port: status.port,
|
||||
// 无法精确取回首次启动时间,返回当前时间用于 UI 展示即可
|
||||
started_at: chrono::Utc::now().to_rfc3339(),
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 创建并启动服务器
|
||||
@@ -68,11 +86,13 @@ impl ProxyService {
|
||||
// 5. 保存服务器实例
|
||||
*self.server.write().await = Some(server);
|
||||
|
||||
// 6. 持久化 enabled 状态
|
||||
self.db
|
||||
.update_proxy_config(config)
|
||||
.await
|
||||
.map_err(|e| format!("保存代理配置失败: {e}"))?;
|
||||
// 6. 持久化 enabled 状态(仅总开关)
|
||||
if persist_enabled {
|
||||
self.db
|
||||
.update_proxy_config(config)
|
||||
.await
|
||||
.map_err(|e| format!("保存代理配置失败: {e}"))?;
|
||||
}
|
||||
|
||||
log::info!("代理服务器已启动: {}:{}", info.address, info.port);
|
||||
Ok(info)
|
||||
@@ -118,7 +138,7 @@ impl ProxyService {
|
||||
}
|
||||
|
||||
// 5. 启动代理服务器
|
||||
match self.start().await {
|
||||
match self.start(true).await {
|
||||
Ok(info) => Ok(info),
|
||||
Err(e) => {
|
||||
// 启动失败,恢复原始配置
|
||||
@@ -137,81 +157,305 @@ impl ProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取各应用的接管状态(是否改写该应用的 Live 配置指向本地代理)
|
||||
pub async fn get_takeover_status(&self) -> Result<ProxyTakeoverStatus, String> {
|
||||
let claude = self
|
||||
.db
|
||||
.get_live_backup("claude")
|
||||
.await
|
||||
.map_err(|e| format!("获取 Claude 接管状态失败: {e}"))?
|
||||
.is_some();
|
||||
let codex = self
|
||||
.db
|
||||
.get_live_backup("codex")
|
||||
.await
|
||||
.map_err(|e| format!("获取 Codex 接管状态失败: {e}"))?
|
||||
.is_some();
|
||||
let gemini = self
|
||||
.db
|
||||
.get_live_backup("gemini")
|
||||
.await
|
||||
.map_err(|e| format!("获取 Gemini 接管状态失败: {e}"))?
|
||||
.is_some();
|
||||
|
||||
Ok(ProxyTakeoverStatus {
|
||||
claude,
|
||||
codex,
|
||||
gemini,
|
||||
})
|
||||
}
|
||||
|
||||
/// 为指定应用开启/关闭 Live 接管
|
||||
///
|
||||
/// - 开启:自动启动代理服务(不影响总开关持久化),仅接管当前 app 的 Live 配置
|
||||
/// - 关闭:仅恢复当前 app 的 Live 配置;若总开关未开启且无其它接管,则自动停止代理服务
|
||||
pub async fn set_takeover_for_app(&self, app_type: &str, enabled: bool) -> Result<(), String> {
|
||||
let app = AppType::from_str(app_type).map_err(|e| format!("无效的应用类型: {e}"))?;
|
||||
let app_type_str = app.as_str();
|
||||
|
||||
if enabled {
|
||||
// 1) 代理服务未运行则自动启动(不持久化总开关)
|
||||
if !self.is_running().await {
|
||||
self.start(false).await?;
|
||||
}
|
||||
|
||||
// 2) 已接管则直接返回(幂等)
|
||||
if self
|
||||
.db
|
||||
.get_live_backup(app_type_str)
|
||||
.await
|
||||
.map_err(|e| format!("检查 {app_type_str} Live 备份失败: {e}"))?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 3) 备份 Live 配置(严格:目标 app 不存在则报错)
|
||||
self.backup_live_config_strict(&app).await?;
|
||||
|
||||
// 4) 同步 Live Token 到数据库(仅当前 app)
|
||||
if let Err(e) = self.sync_live_to_provider(&app).await {
|
||||
let _ = self.db.delete_live_backup(app_type_str).await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 5) 写入接管配置(仅当前 app)
|
||||
if let Err(e) = self.takeover_live_config_strict(&app).await {
|
||||
log::error!("{app_type_str} 接管 Live 配置失败,尝试恢复: {e}");
|
||||
let _ = self.restore_live_config_for_app(&app).await;
|
||||
let _ = self.db.delete_live_backup(app_type_str).await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// 6) 兼容旧逻辑:写入 any-of 标志(失败不影响功能)
|
||||
let _ = self.db.set_live_takeover_active(true).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 关闭接管:无备份则视为未接管(幂等)
|
||||
let has_backup = self
|
||||
.db
|
||||
.get_live_backup(app_type_str)
|
||||
.await
|
||||
.map_err(|e| format!("检查 {app_type_str} Live 备份失败: {e}"))?
|
||||
.is_some();
|
||||
if !has_backup {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 1) 恢复 Live 配置
|
||||
self.restore_live_config_for_app(&app).await?;
|
||||
|
||||
// 2) 删除该 app 的备份(避免长期存储敏感 Token)
|
||||
self.db
|
||||
.delete_live_backup(app_type_str)
|
||||
.await
|
||||
.map_err(|e| format!("删除 {app_type_str} Live 备份失败: {e}"))?;
|
||||
|
||||
// 3) 若无其它接管,更新旧标志,并在总开关未开启时停止代理服务
|
||||
let has_any_backup = self
|
||||
.db
|
||||
.has_any_live_backup()
|
||||
.await
|
||||
.map_err(|e| format!("检查 Live 备份失败: {e}"))?;
|
||||
if !has_any_backup {
|
||||
let _ = self.db.set_live_takeover_active(false).await;
|
||||
|
||||
let master_enabled = self
|
||||
.db
|
||||
.get_proxy_config()
|
||||
.await
|
||||
.map_err(|e| format!("获取代理配置失败: {e}"))?
|
||||
.enabled;
|
||||
if !master_enabled && self.is_running().await {
|
||||
// 此时没有任何 app 处于接管状态,停止服务即可
|
||||
let _ = self.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 同步 Live 配置中的 Token 到数据库
|
||||
///
|
||||
/// 在清空 Live Token 之前调用,确保数据库中的 Provider 配置有最新的 Token。
|
||||
/// 这样代理才能从数据库读取到正确的认证信息。
|
||||
async fn sync_live_to_providers(&self) -> Result<(), String> {
|
||||
// Claude: 同步 Token(Live 属于本机配置,因此优先使用设备级 effective current)
|
||||
if let Ok(live_config) = self.read_claude_live() {
|
||||
let provider_id =
|
||||
crate::settings::get_effective_current_provider(&self.db, &AppType::Claude)
|
||||
.map_err(|e| format!("获取 Claude 当前供应商失败: {e}"))?;
|
||||
async fn sync_live_to_provider(&self, app_type: &AppType) -> Result<(), String> {
|
||||
let live_config = match app_type {
|
||||
AppType::Claude => self.read_claude_live()?,
|
||||
AppType::Codex => self.read_codex_live()?,
|
||||
AppType::Gemini => self.read_gemini_live()?,
|
||||
};
|
||||
|
||||
if let Some(provider_id) = provider_id {
|
||||
if let Ok(Some(mut provider)) = self.db.get_provider_by_id(&provider_id, "claude") {
|
||||
if let Some(env) = live_config.get("env").and_then(|v| v.as_object()) {
|
||||
let token_pair = [
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|key| {
|
||||
env.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| (key, s.trim()))
|
||||
})
|
||||
.filter(|(_, token)| {
|
||||
!token.is_empty() && *token != PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
self.sync_live_config_to_provider(app_type, &live_config)
|
||||
.await
|
||||
}
|
||||
|
||||
if let Some((token_key, token)) = token_pair {
|
||||
let env_obj = provider
|
||||
async fn sync_live_config_to_provider(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
live_config: &Value,
|
||||
) -> Result<(), String> {
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
let provider_id =
|
||||
crate::settings::get_effective_current_provider(&self.db, &AppType::Claude)
|
||||
.map_err(|e| format!("获取 Claude 当前供应商失败: {e}"))?;
|
||||
|
||||
if let Some(provider_id) = provider_id {
|
||||
if let Ok(Some(mut provider)) =
|
||||
self.db.get_provider_by_id(&provider_id, "claude")
|
||||
{
|
||||
if let Some(env) = live_config.get("env").and_then(|v| v.as_object()) {
|
||||
let token_pair = [
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|key| {
|
||||
env.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| (key, s.trim()))
|
||||
})
|
||||
.filter(|(_, token)| {
|
||||
!token.is_empty() && *token != PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
|
||||
if let Some((token_key, token)) = token_pair {
|
||||
let env_obj = provider
|
||||
.settings_config
|
||||
.get_mut("env")
|
||||
.and_then(|v| v.as_object_mut());
|
||||
|
||||
match env_obj {
|
||||
Some(obj) => {
|
||||
obj.insert(token_key.to_string(), json!(token));
|
||||
// ANTHROPIC_AUTH_TOKEN 与 ANTHROPIC_API_KEY 视为同义字段,保持一致
|
||||
if token_key == "ANTHROPIC_AUTH_TOKEN"
|
||||
|| token_key == "ANTHROPIC_API_KEY"
|
||||
{
|
||||
obj.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
obj.insert(
|
||||
"ANTHROPIC_API_KEY".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// 至少写入一份可用的 Token
|
||||
provider.settings_config["env"] = json!({
|
||||
token_key: token
|
||||
});
|
||||
if token_key == "ANTHROPIC_AUTH_TOKEN"
|
||||
|| token_key == "ANTHROPIC_API_KEY"
|
||||
{
|
||||
provider.settings_config["env"]
|
||||
["ANTHROPIC_AUTH_TOKEN"] = json!(token);
|
||||
provider.settings_config["env"]["ANTHROPIC_API_KEY"] =
|
||||
json!(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})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppType::Codex => {
|
||||
let provider_id =
|
||||
crate::settings::get_effective_current_provider(&self.db, &AppType::Codex)
|
||||
.map_err(|e| format!("获取 Codex 当前供应商失败: {e}"))?;
|
||||
|
||||
if let Some(provider_id) = provider_id {
|
||||
if let Ok(Some(mut provider)) =
|
||||
self.db.get_provider_by_id(&provider_id, "codex")
|
||||
{
|
||||
if let Some(token) = live_config
|
||||
.get("auth")
|
||||
.and_then(|v| v.get("OPENAI_API_KEY"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty() && *s != PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
if let Some(auth_obj) = provider
|
||||
.settings_config
|
||||
.get_mut("env")
|
||||
.and_then(|v| v.as_object_mut());
|
||||
|
||||
match env_obj {
|
||||
Some(obj) => {
|
||||
obj.insert(token_key.to_string(), json!(token));
|
||||
// ANTHROPIC_AUTH_TOKEN 与 ANTHROPIC_API_KEY 视为同义字段,保持一致
|
||||
if token_key == "ANTHROPIC_AUTH_TOKEN"
|
||||
|| token_key == "ANTHROPIC_API_KEY"
|
||||
{
|
||||
obj.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
obj.insert("ANTHROPIC_API_KEY".to_string(), json!(token));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// 至少写入一份可用的 Token
|
||||
provider.settings_config["env"] = json!({
|
||||
token_key: token
|
||||
});
|
||||
if token_key == "ANTHROPIC_AUTH_TOKEN"
|
||||
|| token_key == "ANTHROPIC_API_KEY"
|
||||
{
|
||||
provider.settings_config["env"]["ANTHROPIC_AUTH_TOKEN"] =
|
||||
json!(token);
|
||||
provider.settings_config["env"]["ANTHROPIC_API_KEY"] =
|
||||
json!(token);
|
||||
}
|
||||
}
|
||||
.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(
|
||||
"claude",
|
||||
"codex",
|
||||
&provider_id,
|
||||
&provider.settings_config,
|
||||
) {
|
||||
log::warn!("同步 Claude Token 到数据库失败: {e}");
|
||||
log::warn!("同步 Codex Token 到数据库失败: {e}");
|
||||
} else {
|
||||
log::info!("已同步 Codex Token 到数据库 (provider: {provider_id})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppType::Gemini => {
|
||||
let provider_id =
|
||||
crate::settings::get_effective_current_provider(&self.db, &AppType::Gemini)
|
||||
.map_err(|e| format!("获取 Gemini 当前供应商失败: {e}"))?;
|
||||
|
||||
if let Some(provider_id) = provider_id {
|
||||
if let Ok(Some(mut provider)) =
|
||||
self.db.get_provider_by_id(&provider_id, "gemini")
|
||||
{
|
||||
if let Some(token) = live_config
|
||||
.get("env")
|
||||
.and_then(|v| v.get("GEMINI_API_KEY"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty() && *s != PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
if let Some(env_obj) = provider
|
||||
.settings_config
|
||||
.get_mut("env")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
env_obj.insert("GEMINI_API_KEY".to_string(), json!(token));
|
||||
} else {
|
||||
provider.settings_config["env"] = json!({
|
||||
"GEMINI_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!(
|
||||
"已同步 Claude Token 到数据库 (provider: {provider_id})"
|
||||
"已同步 Gemini Token 到数据库 (provider: {provider_id})"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -220,86 +464,23 @@ impl ProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
// Codex: 同步 OPENAI_API_KEY(忽略占位符)
|
||||
if let Ok(live_config) = self.read_codex_live() {
|
||||
let provider_id =
|
||||
crate::settings::get_effective_current_provider(&self.db, &AppType::Codex)
|
||||
.map_err(|e| format!("获取 Codex 当前供应商失败: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Some(provider_id) = provider_id {
|
||||
if let Ok(Some(mut provider)) = self.db.get_provider_by_id(&provider_id, "codex") {
|
||||
if let Some(token) = live_config
|
||||
.get("auth")
|
||||
.and_then(|v| v.get("OPENAI_API_KEY"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty() && *s != PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn sync_live_to_providers(&self) -> Result<(), String> {
|
||||
if let Ok(live_config) = self.read_claude_live() {
|
||||
self.sync_live_config_to_provider(&AppType::Claude, &live_config)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Ok(live_config) = self.read_codex_live() {
|
||||
self.sync_live_config_to_provider(&AppType::Codex, &live_config)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Gemini: 同步 GEMINI_API_KEY(忽略占位符)
|
||||
if let Ok(live_config) = self.read_gemini_live() {
|
||||
let provider_id =
|
||||
crate::settings::get_effective_current_provider(&self.db, &AppType::Gemini)
|
||||
.map_err(|e| format!("获取 Gemini 当前供应商失败: {e}"))?;
|
||||
|
||||
if let Some(provider_id) = provider_id {
|
||||
if let Ok(Some(mut provider)) = self.db.get_provider_by_id(&provider_id, "gemini") {
|
||||
if let Some(token) = live_config
|
||||
.get("env")
|
||||
.and_then(|v| v.get("GEMINI_API_KEY"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty() && *s != PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
if let Some(env_obj) = provider
|
||||
.settings_config
|
||||
.get_mut("env")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
env_obj.insert("GEMINI_API_KEY".to_string(), json!(token));
|
||||
} else {
|
||||
provider.settings_config["env"] = json!({
|
||||
"GEMINI_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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.sync_live_config_to_provider(&AppType::Gemini, &live_config)
|
||||
.await?;
|
||||
}
|
||||
|
||||
log::info!("Live 配置 Token 同步完成");
|
||||
@@ -329,8 +510,19 @@ impl ProxyService {
|
||||
|
||||
/// 停止代理服务器(恢复 Live 配置)
|
||||
pub async fn stop_with_restore(&self) -> Result<(), String> {
|
||||
// 1. 停止代理服务器
|
||||
self.stop().await?;
|
||||
// 1. 停止代理服务器(即使未运行也继续执行恢复逻辑)
|
||||
if let Err(e) = self.stop().await {
|
||||
log::warn!("停止代理服务器失败(将继续恢复 Live 配置): {e}");
|
||||
|
||||
// stop() 只有在 server 实例存在时才会把 enabled 设为 false;
|
||||
// 这里兜底确保“总开关关闭”能落盘关闭状态。
|
||||
if let Ok(mut config) = self.db.get_proxy_config().await {
|
||||
if config.enabled {
|
||||
config.enabled = false;
|
||||
let _ = self.db.update_proxy_config(config).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 恢复原始 Live 配置
|
||||
self.restore_live_configs().await?;
|
||||
@@ -393,22 +585,33 @@ impl ProxyService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接管各应用的 Live 配置(写入代理地址)
|
||||
///
|
||||
/// 代理服务器的路由已经根据 API 端点自动区分应用类型:
|
||||
/// - `/v1/messages` → Claude
|
||||
/// - `/v1/chat/completions`, `/v1/responses` → Codex
|
||||
/// - `/v1beta/*` → Gemini
|
||||
///
|
||||
/// 因此不需要在 URL 中添加应用前缀。
|
||||
async fn takeover_live_configs(&self) -> Result<(), String> {
|
||||
/// 备份指定应用的 Live 配置(严格模式:目标配置不存在则返回错误)
|
||||
async fn backup_live_config_strict(&self, app_type: &AppType) -> Result<(), String> {
|
||||
let (app_type_str, config) = match app_type {
|
||||
AppType::Claude => ("claude", self.read_claude_live()?),
|
||||
AppType::Codex => ("codex", self.read_codex_live()?),
|
||||
AppType::Gemini => ("gemini", self.read_gemini_live()?),
|
||||
};
|
||||
|
||||
let json_str = serde_json::to_string(&config)
|
||||
.map_err(|e| format!("序列化 {app_type_str} 配置失败: {e}"))?;
|
||||
self.db
|
||||
.save_live_backup(app_type_str, &json_str)
|
||||
.await
|
||||
.map_err(|e| format!("备份 {app_type_str} 配置失败: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 构造写入 Live 的代理地址(处理 0.0.0.0 / IPv6 等特殊情况)
|
||||
async fn build_proxy_urls(&self) -> Result<(String, String), String> {
|
||||
let config = self
|
||||
.db
|
||||
.get_proxy_config()
|
||||
.await
|
||||
.map_err(|e| format!("获取代理配置失败: {e}"))?;
|
||||
|
||||
// 注意:listen_address 可能是 0.0.0.0(用于监听所有网卡),但客户端无法用 0.0.0.0 连接;
|
||||
// listen_address 可能是 0.0.0.0(用于监听所有网卡),但客户端无法用 0.0.0.0 连接;
|
||||
// 因此写回到各应用配置时,优先使用本机回环地址。
|
||||
let connect_host = match config.listen_address.as_str() {
|
||||
"0.0.0.0" => "127.0.0.1".to_string(),
|
||||
@@ -425,6 +628,20 @@ impl ProxyService {
|
||||
let proxy_url = proxy_origin.clone();
|
||||
let proxy_codex_base_url = format!("{}/v1", proxy_origin.trim_end_matches('/'));
|
||||
|
||||
Ok((proxy_url, proxy_codex_base_url))
|
||||
}
|
||||
|
||||
/// 接管各应用的 Live 配置(写入代理地址)
|
||||
///
|
||||
/// 代理服务器的路由已经根据 API 端点自动区分应用类型:
|
||||
/// - `/v1/messages` → Claude
|
||||
/// - `/v1/chat/completions`, `/v1/responses` → Codex
|
||||
/// - `/v1beta/*` → Gemini
|
||||
///
|
||||
/// 因此不需要在 URL 中添加应用前缀。
|
||||
async fn takeover_live_configs(&self) -> Result<(), String> {
|
||||
let (proxy_url, proxy_codex_base_url) = self.build_proxy_urls().await?;
|
||||
|
||||
// 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()) {
|
||||
@@ -500,6 +717,196 @@ impl ProxyService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接管指定应用的 Live 配置(严格模式:目标配置不存在则返回错误)
|
||||
async fn takeover_live_config_strict(&self, app_type: &AppType) -> Result<(), String> {
|
||||
let (proxy_url, proxy_codex_base_url) = self.build_proxy_urls().await?;
|
||||
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
let 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));
|
||||
|
||||
let token_keys = [
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
];
|
||||
|
||||
let mut replaced_any = false;
|
||||
for key in token_keys {
|
||||
if env.contains_key(key) {
|
||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
replaced_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced_any {
|
||||
env.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
live_config["env"] = json!({
|
||||
"ANTHROPIC_BASE_URL": &proxy_url,
|
||||
"ANTHROPIC_AUTH_TOKEN": PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
}
|
||||
|
||||
self.write_claude_live(&live_config)?;
|
||||
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
|
||||
}
|
||||
AppType::Codex => {
|
||||
let 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_API_KEY".to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
}
|
||||
|
||||
let config_str = live_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let updated_config = Self::update_toml_base_url(config_str, &proxy_codex_base_url);
|
||||
live_config["config"] = json!(updated_config);
|
||||
|
||||
self.write_codex_live(&live_config)?;
|
||||
log::info!("Codex Live 配置已接管,代理地址: {proxy_codex_base_url}");
|
||||
}
|
||||
AppType::Gemini => {
|
||||
let 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("GOOGLE_GEMINI_BASE_URL".to_string(), json!(&proxy_url));
|
||||
env.insert("GEMINI_API_KEY".to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
} else {
|
||||
live_config["env"] = json!({
|
||||
"GOOGLE_GEMINI_BASE_URL": &proxy_url,
|
||||
"GEMINI_API_KEY": PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
}
|
||||
|
||||
self.write_gemini_live(&live_config)?;
|
||||
log::info!("Gemini Live 配置已接管,代理地址: {proxy_url}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 接管指定应用的 Live 配置(尽力而为:配置不存在/读取失败则跳过)
|
||||
async fn takeover_live_config_best_effort(&self, app_type: &AppType) -> Result<(), String> {
|
||||
let (proxy_url, proxy_codex_base_url) = self.build_proxy_urls().await?;
|
||||
|
||||
match app_type {
|
||||
AppType::Claude => {
|
||||
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));
|
||||
|
||||
let token_keys = [
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
];
|
||||
|
||||
let mut replaced_any = false;
|
||||
for key in token_keys {
|
||||
if env.contains_key(key) {
|
||||
env.insert(key.to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
replaced_any = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !replaced_any {
|
||||
env.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(PROXY_TOKEN_PLACEHOLDER),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
live_config["env"] = json!({
|
||||
"ANTHROPIC_BASE_URL": &proxy_url,
|
||||
"ANTHROPIC_AUTH_TOKEN": PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
}
|
||||
|
||||
let _ = self.write_claude_live(&live_config);
|
||||
}
|
||||
}
|
||||
AppType::Codex => {
|
||||
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_API_KEY".to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
}
|
||||
|
||||
let config_str = live_config
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let updated_config =
|
||||
Self::update_toml_base_url(config_str, &proxy_codex_base_url);
|
||||
live_config["config"] = json!(updated_config);
|
||||
|
||||
let _ = self.write_codex_live(&live_config);
|
||||
}
|
||||
}
|
||||
AppType::Gemini => {
|
||||
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("GOOGLE_GEMINI_BASE_URL".to_string(), json!(&proxy_url));
|
||||
env.insert("GEMINI_API_KEY".to_string(), json!(PROXY_TOKEN_PLACEHOLDER));
|
||||
} else {
|
||||
live_config["env"] = json!({
|
||||
"GOOGLE_GEMINI_BASE_URL": &proxy_url,
|
||||
"GEMINI_API_KEY": PROXY_TOKEN_PLACEHOLDER
|
||||
});
|
||||
}
|
||||
|
||||
let _ = self.write_gemini_live(&live_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 恢复指定应用的 Live 配置(若无备份则不做任何操作)
|
||||
async fn restore_live_config_for_app(&self, app_type: &AppType) -> Result<(), String> {
|
||||
match app_type {
|
||||
AppType::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 配置已恢复");
|
||||
}
|
||||
}
|
||||
AppType::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 配置已恢复");
|
||||
}
|
||||
}
|
||||
AppType::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 配置
|
||||
async fn restore_live_configs(&self) -> Result<(), String> {
|
||||
// Claude
|
||||
@@ -886,12 +1293,30 @@ impl ProxyService {
|
||||
*server_guard = Some(new_server);
|
||||
log::info!("代理配置已更新,服务器已自动重启应用最新配置");
|
||||
|
||||
// 如果当前处于 Live 接管模式,需要同步更新 Live 中的代理地址(否则客户端仍指向旧端口)
|
||||
// 如果当前存在任意 app 的 Live 接管,需要同步更新 Live 中的代理地址(否则客户端仍指向旧端口)
|
||||
drop(server_guard);
|
||||
if previous.live_takeover_active {
|
||||
// takeover_live_configs 只会写入代理地址与占位符,不会破坏备份
|
||||
self.takeover_live_configs().await?;
|
||||
log::info!("已同步更新 Live 配置中的代理地址");
|
||||
if let Ok(takeover) = self.get_takeover_status().await {
|
||||
let mut updated_any = false;
|
||||
|
||||
if takeover.claude {
|
||||
self.takeover_live_config_best_effort(&AppType::Claude)
|
||||
.await?;
|
||||
updated_any = true;
|
||||
}
|
||||
if takeover.codex {
|
||||
self.takeover_live_config_best_effort(&AppType::Codex)
|
||||
.await?;
|
||||
updated_any = true;
|
||||
}
|
||||
if takeover.gemini {
|
||||
self.takeover_live_config_best_effort(&AppType::Gemini)
|
||||
.await?;
|
||||
updated_any = true;
|
||||
}
|
||||
|
||||
if updated_any {
|
||||
log::info!("已同步更新 Live 配置中的代理地址");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
|
||||
+1
-1
@@ -491,7 +491,7 @@ function App() {
|
||||
)}
|
||||
{currentView === "providers" && (
|
||||
<>
|
||||
<ProxyToggle />
|
||||
<ProxyToggle activeApp={activeApp} />
|
||||
|
||||
<AppSwitcher activeApp={activeApp} onSwitch={setActiveApp} />
|
||||
|
||||
|
||||
@@ -9,40 +9,54 @@ import { Radio, Loader2 } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { AppId } from "@/lib/api";
|
||||
|
||||
interface ProxyToggleProps {
|
||||
className?: string;
|
||||
activeApp: AppId;
|
||||
}
|
||||
|
||||
export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
isRunning,
|
||||
isTakeoverActive,
|
||||
startWithTakeover,
|
||||
stopWithRestore,
|
||||
takeoverStatus,
|
||||
setTakeoverForApp,
|
||||
isPending,
|
||||
status,
|
||||
} = useProxyStatus();
|
||||
|
||||
const handleToggle = async (checked: boolean) => {
|
||||
if (checked) {
|
||||
await startWithTakeover();
|
||||
} else {
|
||||
await stopWithRestore();
|
||||
}
|
||||
await setTakeoverForApp({ appType: activeApp, enabled: checked });
|
||||
};
|
||||
|
||||
const isActive = isRunning && isTakeoverActive;
|
||||
const takeoverEnabled = takeoverStatus?.[activeApp] || false;
|
||||
|
||||
const tooltipText = isActive
|
||||
? `代理模式运行中 - ${status?.address}:${status?.port}\n切换供应商为热切换`
|
||||
: "开启代理模式\n启用后自动接管 Live 配置";
|
||||
const appLabel =
|
||||
activeApp === "claude"
|
||||
? "Claude"
|
||||
: activeApp === "codex"
|
||||
? "Codex"
|
||||
: "Gemini";
|
||||
|
||||
const tooltipText = takeoverEnabled
|
||||
? isRunning
|
||||
? t("proxy.takeover.tooltip.active", {
|
||||
defaultValue: `${appLabel} 已接管 - ${status?.address}:${status?.port}\n切换该应用供应商为热切换`,
|
||||
})
|
||||
: t("proxy.takeover.tooltip.broken", {
|
||||
defaultValue: `${appLabel} 已接管,但代理服务未运行`,
|
||||
})
|
||||
: t("proxy.takeover.tooltip.inactive", {
|
||||
defaultValue: `接管 ${appLabel} 的 Live 配置,让该应用请求走本地代理`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all cursor-default",
|
||||
isActive
|
||||
takeoverEnabled
|
||||
? "bg-emerald-500/10 border border-emerald-500/30"
|
||||
: "bg-muted/50 hover:bg-muted",
|
||||
className,
|
||||
@@ -55,7 +69,7 @@ export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
<Radio
|
||||
className={cn(
|
||||
"h-4 w-4 transition-colors",
|
||||
isActive
|
||||
takeoverEnabled
|
||||
? "text-emerald-500 animate-pulse"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
@@ -64,7 +78,7 @@ export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium transition-colors select-none",
|
||||
isActive
|
||||
takeoverEnabled
|
||||
? "text-emerald-600 dark:text-emerald-400"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
@@ -72,7 +86,7 @@ export function ProxyToggle({ className }: ProxyToggleProps) {
|
||||
Proxy
|
||||
</span>
|
||||
<Switch
|
||||
checked={isActive}
|
||||
checked={takeoverEnabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="ml-1"
|
||||
|
||||
@@ -177,8 +177,8 @@ export function SettingsPage({
|
||||
|
||||
const {
|
||||
isRunning,
|
||||
startWithTakeover: startProxy,
|
||||
stopWithRestore: stopProxy,
|
||||
startProxyServer,
|
||||
stopWithRestore,
|
||||
isPending: isProxyPending,
|
||||
} = useProxyStatus();
|
||||
const [failoverEnabled, setFailoverEnabled] = useState(true);
|
||||
@@ -186,9 +186,9 @@ export function SettingsPage({
|
||||
const handleToggleProxy = async (checked: boolean) => {
|
||||
try {
|
||||
if (!checked) {
|
||||
await stopProxy();
|
||||
await stopWithRestore();
|
||||
} else {
|
||||
await startProxy();
|
||||
await startProxyServer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Toggle proxy failed:", error);
|
||||
|
||||
+72
-21
@@ -6,7 +6,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ProxyStatus, ProxyServerInfo } from "@/types/proxy";
|
||||
import type { ProxyStatus, ProxyServerInfo, ProxyTakeoverStatus } from "@/types/proxy";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
|
||||
/**
|
||||
@@ -26,47 +26,47 @@ export function useProxyStatus() {
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
|
||||
// 查询接管状态
|
||||
const { data: isTakeoverActive } = useQuery({
|
||||
queryKey: ["proxyTakeoverActive"],
|
||||
queryFn: () => invoke<boolean>("is_live_takeover_active"),
|
||||
// 查询各应用接管状态
|
||||
const { data: takeoverStatus } = useQuery({
|
||||
queryKey: ["proxyTakeoverStatus"],
|
||||
queryFn: () => invoke<ProxyTakeoverStatus>("get_proxy_takeover_status"),
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
|
||||
// 启动服务器(带 Live 配置接管)
|
||||
const startWithTakeoverMutation = useMutation({
|
||||
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_with_takeover"),
|
||||
// 启动服务器(总开关:仅启动服务,不接管)
|
||||
const startProxyServerMutation = useMutation({
|
||||
mutationFn: () => invoke<ProxyServerInfo>("start_proxy_server"),
|
||||
onSuccess: (info) => {
|
||||
toast.success(
|
||||
t("proxy.startedWithTakeover", {
|
||||
defaultValue: `代理模式已启用 - ${info.address}:${info.port}`,
|
||||
t("proxy.server.started", {
|
||||
defaultValue: `代理服务已启动 - ${info.address}:${info.port}`,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || "未知错误";
|
||||
toast.error(
|
||||
t("proxy.startWithTakeoverFailed", {
|
||||
defaultValue: `启动失败: ${detail}`,
|
||||
t("proxy.server.startFailed", {
|
||||
defaultValue: `启动代理服务失败: ${detail}`,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 停止服务器(恢复 Live 配置)
|
||||
// 停止服务器(总开关关闭:强制恢复所有已接管的 Live 配置)
|
||||
const stopWithRestoreMutation = useMutation({
|
||||
mutationFn: () => invoke("stop_proxy_with_restore"),
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
t("proxy.stoppedWithRestore", {
|
||||
defaultValue: "代理模式已关闭,配置已恢复",
|
||||
defaultValue: "代理服务已关闭,已恢复所有接管配置",
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverActive"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
|
||||
// 清除所有供应商健康状态缓存(后端已清空数据库记录)
|
||||
queryClient.invalidateQueries({ queryKey: ["providerHealth"] });
|
||||
},
|
||||
@@ -80,6 +80,47 @@ export function useProxyStatus() {
|
||||
},
|
||||
});
|
||||
|
||||
// 按应用开启/关闭接管
|
||||
const setTakeoverForAppMutation = useMutation({
|
||||
mutationFn: ({
|
||||
appType,
|
||||
enabled,
|
||||
}: {
|
||||
appType: string;
|
||||
enabled: boolean;
|
||||
}) => invoke("set_proxy_takeover_for_app", { appType, enabled }),
|
||||
onSuccess: (_data, variables) => {
|
||||
const appLabel =
|
||||
variables.appType === "claude"
|
||||
? "Claude"
|
||||
: variables.appType === "codex"
|
||||
? "Codex"
|
||||
: "Gemini";
|
||||
|
||||
toast.success(
|
||||
variables.enabled
|
||||
? t("proxy.takeover.enabled", {
|
||||
defaultValue: `已接管 ${appLabel} 配置(请求将走本地代理)`,
|
||||
})
|
||||
: t("proxy.takeover.disabled", {
|
||||
defaultValue: `已恢复 ${appLabel} 配置`,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
const detail = extractErrorMessage(error) || "未知错误";
|
||||
toast.error(
|
||||
t("proxy.takeover.failed", {
|
||||
defaultValue: `操作失败: ${detail}`,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// 代理模式切换供应商(热切换)
|
||||
const switchProxyProviderMutation = useMutation({
|
||||
mutationFn: ({
|
||||
@@ -120,12 +161,20 @@ export function useProxyStatus() {
|
||||
status,
|
||||
isLoading,
|
||||
isRunning: status?.running || false,
|
||||
isTakeoverActive: isTakeoverActive || false,
|
||||
takeoverStatus,
|
||||
isTakeoverActive:
|
||||
takeoverStatus?.claude ||
|
||||
takeoverStatus?.codex ||
|
||||
takeoverStatus?.gemini ||
|
||||
false,
|
||||
|
||||
// 启动/停止(接管模式)
|
||||
startWithTakeover: startWithTakeoverMutation.mutateAsync,
|
||||
// 启动/停止(总开关)
|
||||
startProxyServer: startProxyServerMutation.mutateAsync,
|
||||
stopWithRestore: stopWithRestoreMutation.mutateAsync,
|
||||
|
||||
// 按应用接管开关
|
||||
setTakeoverForApp: setTakeoverForAppMutation.mutateAsync,
|
||||
|
||||
// 代理模式下切换供应商
|
||||
switchProxyProvider: switchProxyProviderMutation.mutateAsync,
|
||||
|
||||
@@ -134,9 +183,11 @@ export function useProxyStatus() {
|
||||
checkTakeoverActive,
|
||||
|
||||
// 加载状态
|
||||
isStarting: startWithTakeoverMutation.isPending,
|
||||
isStarting: startProxyServerMutation.isPending,
|
||||
isStopping: stopWithRestoreMutation.isPending,
|
||||
isPending:
|
||||
startWithTakeoverMutation.isPending || stopWithRestoreMutation.isPending,
|
||||
startProxyServerMutation.isPending ||
|
||||
stopWithRestoreMutation.isPending ||
|
||||
setTakeoverForAppMutation.isPending,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,6 +38,12 @@ export interface ProxyServerInfo {
|
||||
started_at: string;
|
||||
}
|
||||
|
||||
export interface ProxyTakeoverStatus {
|
||||
claude: boolean;
|
||||
codex: boolean;
|
||||
gemini: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderHealth {
|
||||
provider_id: string;
|
||||
app_type: string;
|
||||
|
||||
@@ -258,6 +258,14 @@ export const handlers = [
|
||||
}),
|
||||
),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/get_proxy_takeover_status`, () =>
|
||||
success({
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
}),
|
||||
),
|
||||
|
||||
http.post(`${TAURI_ENDPOINT}/is_live_takeover_active`, () => success(false)),
|
||||
|
||||
// Failover / circuit breaker defaults
|
||||
|
||||
Reference in New Issue
Block a user