mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix(profiles): stop proxy server when profile switch leaves no takeovers active
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
//! 项目 Profile 管理命令
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{Emitter, State};
|
||||
use tauri::{Emitter, Manager, State};
|
||||
|
||||
use crate::database::Profile;
|
||||
use crate::services::profile::{ProfilePayload, ProfileScope, ProfileService};
|
||||
@@ -165,7 +165,31 @@ pub fn apply_profile(
|
||||
scope: String,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let scope = ProfileScope::parse(&scope).map_err(|e| e.to_string())?;
|
||||
let warnings = ProfileService::apply(&state, &id, scope).map_err(|e| e.to_string())?;
|
||||
emit_profile_apply_events(&app, &state, &id, scope);
|
||||
let (warnings, should_stop_proxy) =
|
||||
ProfileService::apply(&state, &id, scope).map_err(|e| e.to_string())?;
|
||||
|
||||
if should_stop_proxy {
|
||||
// sync 命令线程没有 Tokio runtime,无法直接 await stop();
|
||||
// 把停止服务放到 Tauri async runtime,停止后再补发事件刷新 UI。
|
||||
let app_handle = app.clone();
|
||||
let profile_id = id.clone();
|
||||
let proxy_service = state.proxy_service.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = proxy_service.stop().await {
|
||||
log::warn!("切换项目后停止代理服务失败: {e}");
|
||||
}
|
||||
if let Some(app_state) = app_handle.try_state::<AppState>() {
|
||||
emit_profile_apply_events(
|
||||
&app_handle,
|
||||
app_state.inner(),
|
||||
&profile_id,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
emit_profile_apply_events(&app, &state, &id, scope);
|
||||
}
|
||||
|
||||
Ok(warnings)
|
||||
}
|
||||
|
||||
@@ -494,6 +494,23 @@ impl Database {
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// 同步版本:检查是否有任一 app 的 enabled = true
|
||||
///
|
||||
/// 用于 `ProfileService::apply` 等 sync 路径判断是否需要停止代理服务。
|
||||
pub fn is_live_takeover_active_sync(&self) -> bool {
|
||||
let conn = match self.conn.lock() {
|
||||
Ok(c) => c,
|
||||
Err(_) => return false,
|
||||
};
|
||||
conn.query_row(
|
||||
"SELECT COUNT(*) FROM proxy_config WHERE enabled = 1",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.unwrap_or(0)
|
||||
> 0
|
||||
}
|
||||
|
||||
// ==================== Provider Health ====================
|
||||
|
||||
/// 获取Provider健康状态
|
||||
|
||||
@@ -317,13 +317,16 @@ impl ProfileService {
|
||||
/// 旧项目仍保留离开时的配置,回来时状态一致。自动保存失败时作为 warning
|
||||
/// 继续,不阻塞切换。
|
||||
///
|
||||
/// 顺序不可换:供应商切换(switch_normal 内部会按 DB 当前标志跑 MCP
|
||||
/// sync_all_enabled)必须先于 MCP diff,否则 profile 的 MCP 目标态会被冲掉。
|
||||
/// 应用指定项目的快照到当前分组内的所有应用。
|
||||
///
|
||||
/// 返回 `(warnings, should_stop_proxy)`:当当前分组内所有接管都被关闭、且
|
||||
/// 其它应用也没有接管时,建议调用者停止代理服务,以便 Claude Desktop 的
|
||||
/// "本地路由"总开关同步显示为关闭。
|
||||
pub fn apply(
|
||||
state: &AppState,
|
||||
profile_id: &str,
|
||||
scope: ProfileScope,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
) -> Result<(Vec<String>, bool), AppError> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
// 自动保存旧项目当前状态(仅当前分组),失败不阻塞切换
|
||||
@@ -450,7 +453,11 @@ impl ProfileService {
|
||||
state
|
||||
.db
|
||||
.set_current_profile_id(scope.as_str(), Some(profile_id))?;
|
||||
Ok(warnings)
|
||||
|
||||
// 当前分组内所有接管已关闭;若其它应用也无接管,可停止代理服务。
|
||||
let should_stop_proxy = !state.db.is_live_takeover_active_sync();
|
||||
|
||||
Ok((warnings, should_stop_proxy))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-7
@@ -386,16 +386,35 @@ pub fn handle_profile_tray_event(app: &tauri::AppHandle, event_id: &str) -> bool
|
||||
};
|
||||
match crate::services::profile::ProfileService::apply(app_state.inner(), &profile_id, scope)
|
||||
{
|
||||
Ok(warnings) => {
|
||||
Ok((warnings, should_stop_proxy)) => {
|
||||
for warning in &warnings {
|
||||
log::warn!("[Profile] 应用项目 {profile_id} 警告: {warning}");
|
||||
}
|
||||
crate::commands::emit_profile_apply_events(
|
||||
&app_handle,
|
||||
app_state.inner(),
|
||||
&profile_id,
|
||||
scope,
|
||||
);
|
||||
|
||||
if should_stop_proxy {
|
||||
let app_handle2 = app_handle.clone();
|
||||
let proxy_service = app_state.proxy_service.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = proxy_service.stop().await {
|
||||
log::warn!("托盘切换项目后停止代理服务失败: {e}");
|
||||
}
|
||||
if let Some(state) = app_handle2.try_state::<AppState>() {
|
||||
crate::commands::emit_profile_apply_events(
|
||||
&app_handle2,
|
||||
state.inner(),
|
||||
&profile_id,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
crate::commands::emit_profile_apply_events(
|
||||
&app_handle,
|
||||
app_state.inner(),
|
||||
&profile_id,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("应用项目 {profile_id} 失败: {e}");
|
||||
|
||||
@@ -212,7 +212,7 @@ fn profile_snapshot_apply_roundtrip_restores_configuration() {
|
||||
PromptService::enable_prompt(&state, AppType::Claude, "pr2").expect("enable pr2");
|
||||
|
||||
// ---- 应用项目 A(Claude 组):全部复原 ----
|
||||
let warnings = ProfileService::apply(&state, &profile_a.id, ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, &profile_a.id, ProfileScope::Claude)
|
||||
.expect("apply profile A");
|
||||
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
|
||||
|
||||
@@ -319,7 +319,7 @@ fn shared_profile_sides_are_isolated_and_mergeable() {
|
||||
assert_eq!(payload.mcp.codex, Some(vec![]), "codex side captured");
|
||||
|
||||
// 按 Codex 组应用:只动 codex 组的 current 标记,Claude 侧原样不动
|
||||
let warnings = ProfileService::apply(&state, &project.id, ProfileScope::Codex)
|
||||
let (warnings, _) = ProfileService::apply(&state, &project.id, ProfileScope::Codex)
|
||||
.expect("apply project on codex side");
|
||||
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
|
||||
|
||||
@@ -355,7 +355,7 @@ fn shared_profile_sides_are_isolated_and_mergeable() {
|
||||
);
|
||||
|
||||
// 同一共享项目在 Claude 页应用:该侧未拍过快照 → 不动配置、标记 current、返回提示
|
||||
let warnings = ProfileService::apply(&state, &project.id, ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, &project.id, ProfileScope::Claude)
|
||||
.expect("apply project on claude side");
|
||||
assert_eq!(warnings.len(), 1, "uncaptured side yields one hint");
|
||||
assert!(warnings[0].contains("no claude configuration captured"));
|
||||
@@ -419,7 +419,7 @@ fn profile_apply_reports_dangling_references_and_continues() {
|
||||
};
|
||||
state.db.save_profile(&profile).expect("save profile");
|
||||
|
||||
let warnings = ProfileService::apply(&state, "dangling-test", ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, "dangling-test", ProfileScope::Claude)
|
||||
.expect("apply succeeds");
|
||||
assert_eq!(
|
||||
warnings.len(),
|
||||
@@ -536,7 +536,7 @@ fn switching_profile_autosaves_previous_profile_state() {
|
||||
// ---- Project A:状态 X(p1 / m1 / pr1)----
|
||||
let project_a = ProfileService::create(&state, "Project A", ProfileScope::Claude)
|
||||
.expect("create project A");
|
||||
let warnings = ProfileService::apply(&state, &project_a.id, ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, &project_a.id, ProfileScope::Claude)
|
||||
.expect("apply project A");
|
||||
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
|
||||
|
||||
@@ -550,7 +550,7 @@ fn switching_profile_autosaves_previous_profile_state() {
|
||||
.expect("create project B");
|
||||
|
||||
// ---- 从 A 切换到 B:自动把当前状态 Y 保存到 A,再加载 B 的 Y ----
|
||||
let warnings = ProfileService::apply(&state, &project_b.id, ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, &project_b.id, ProfileScope::Claude)
|
||||
.expect("switch to project B");
|
||||
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
|
||||
|
||||
@@ -591,7 +591,7 @@ fn switching_profile_autosaves_previous_profile_state() {
|
||||
McpService::toggle_app(&state, "m2", AppType::Claude, false).expect("disable m2");
|
||||
PromptService::enable_prompt(&state, AppType::Claude, "pr1").expect("enable pr1");
|
||||
|
||||
let warnings = ProfileService::apply(&state, &project_a.id, ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, &project_a.id, ProfileScope::Claude)
|
||||
.expect("switch back to project A");
|
||||
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
|
||||
|
||||
@@ -704,7 +704,7 @@ fn profile_switch_auto_disables_takeover_before_apply() {
|
||||
.expect("save updated project");
|
||||
|
||||
// ---- 应用项目:应无条件自动关闭接管,再切换到 custom2 ----
|
||||
let warnings = ProfileService::apply(&state, &project.id, ProfileScope::Claude)
|
||||
let (warnings, _) = ProfileService::apply(&state, &project.id, ProfileScope::Claude)
|
||||
.expect("apply custom2 project");
|
||||
assert!(
|
||||
warnings.is_empty(),
|
||||
|
||||
Reference in New Issue
Block a user