mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(profiles): unconditionally disable proxy takeover before applying profile
This commit is contained in:
@@ -355,7 +355,16 @@ impl ProfileService {
|
||||
for app in scope.apps().iter() {
|
||||
let app_str = app.as_str();
|
||||
|
||||
// 1. 供应商
|
||||
// 1. 切换项目前无条件关闭当前应用的代理接管。
|
||||
// 接管态下 live 文件属于代理;用户希望切换工作目录时总是退出当前
|
||||
// 代理环境,再按快照写入真实供应商配置。
|
||||
if let Err(e) = state.proxy_service.disable_takeover_for_app_sync(app) {
|
||||
warnings.push(format!(
|
||||
"[{app_str}] auto-disable proxy takeover before profile switch failed: {e}"
|
||||
));
|
||||
}
|
||||
|
||||
// 2. 供应商
|
||||
if let Some(Some(target_pid)) = payload.providers.get(app) {
|
||||
let providers = state.db.get_all_providers(app_str)?;
|
||||
if !providers.contains_key(target_pid) {
|
||||
@@ -375,7 +384,7 @@ impl ProfileService {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. MCP diff(最小 toggle:仅动目标态≠当前态的条目;None = 该侧未拍过,不动)
|
||||
// 3. MCP diff(最小 toggle:仅动目标态≠当前态的条目;None = 该侧未拍过,不动)
|
||||
if let Some(Some(target_ids)) = payload.mcp.get(app) {
|
||||
let servers = state.db.get_all_mcp_servers()?;
|
||||
let current: Vec<(String, bool)> = servers
|
||||
@@ -395,7 +404,7 @@ impl ProfileService {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Skills diff(SkillService 返回 anyhow::Result,收进 warning)
|
||||
// 4. Skills diff(SkillService 返回 anyhow::Result,收进 warning)
|
||||
if let Some(Some(target_ids)) = payload.skills.get(app) {
|
||||
let skills = state.db.get_all_installed_skills()?;
|
||||
let current: Vec<(String, bool)> = skills
|
||||
@@ -417,7 +426,7 @@ impl ProfileService {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Prompt(None = 不动;已激活则幂等跳过,避免无谓的文件写与备份)
|
||||
// 5. Prompt(None = 不动;已激活则幂等跳过,避免无谓的文件写与备份)
|
||||
if let Some(Some(target_prompt)) = payload.prompts.get(app) {
|
||||
let prompts = state.db.get_prompts(app_str)?;
|
||||
match prompts.get(target_prompt) {
|
||||
|
||||
@@ -799,6 +799,46 @@ impl ProxyService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 同步关闭指定应用的 Live 接管(恢复配置并清标志,不停止代理服务)。
|
||||
///
|
||||
/// 用于 `ProfileService::apply` 等 sync 路径:调用者所在线程可能没有 Tokio
|
||||
/// runtime,无法执行 `set_takeover_for_app(false)` 里的停止服务/等待任务等
|
||||
/// Tokio IO。这里只恢复 Live 文件、删除备份、清除 DB 接管标志,让后续
|
||||
/// `ProviderService::switch` 能正常写入官方供应商配置。
|
||||
///
|
||||
/// 代理服务本身保持运行;当最后一个应用也关闭接管后,下次用户手动关闭
|
||||
/// 代理或程序退出时会自然停止。
|
||||
pub fn disable_takeover_for_app_sync(&self, app_type: &AppType) -> Result<(), String> {
|
||||
let app_type_str = app_type.as_str();
|
||||
|
||||
// 1) 恢复原始 Live 配置(备份 → SSOT → 清理占位符 三层兜底)
|
||||
futures::executor::block_on(self.restore_live_config_for_app_with_fallback_inner(app_type))
|
||||
.map_err(|e| format!("恢复 {app_type_str} Live 配置失败: {e}"))?;
|
||||
|
||||
// 2) 删除该 app 的备份
|
||||
futures::executor::block_on(self.db.delete_live_backup(app_type_str))
|
||||
.map_err(|e| format!("删除 {app_type_str} Live 备份失败: {e}"))?;
|
||||
|
||||
// 3) 设置 proxy_config.enabled = false
|
||||
let mut config =
|
||||
futures::executor::block_on(self.db.get_proxy_config_for_app(app_type_str))
|
||||
.map_err(|e| format!("获取 {app_type_str} 配置失败: {e}"))?;
|
||||
if config.enabled {
|
||||
config.enabled = false;
|
||||
futures::executor::block_on(self.db.update_proxy_config_for_app(config))
|
||||
.map_err(|e| format!("清除 {app_type_str} enabled 状态失败: {e}"))?;
|
||||
}
|
||||
|
||||
// 4) 清除该应用的健康状态
|
||||
futures::executor::block_on(self.db.clear_provider_health_for_app(app_type_str))
|
||||
.map_err(|e| format!("清除 {app_type_str} 健康状态失败: {e}"))?;
|
||||
|
||||
// 5) 清旧标志
|
||||
let _ = futures::executor::block_on(self.db.set_live_takeover_active(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 同步 Live 配置中的 Token 到数据库
|
||||
///
|
||||
/// 在清空 Live Token 之前调用,确保数据库中的 Provider 配置有最新的 Token。
|
||||
@@ -1560,7 +1600,7 @@ impl ProxyService {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn restore_live_config_for_app_with_fallback_inner(
|
||||
pub(crate) async fn restore_live_config_for_app_with_fallback_inner(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
) -> Result<(), String> {
|
||||
|
||||
@@ -639,3 +639,108 @@ fn switching_profile_autosaves_previous_profile_state() {
|
||||
assert_eq!(payload_b.mcp.claude, Some(vec!["m1".to_string()]));
|
||||
assert_eq!(payload_b.prompts.claude.as_deref(), Some("pr1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_switch_auto_disables_takeover_before_apply() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
|
||||
// 使用临时端口,避免测试机器端口冲突
|
||||
futures::executor::block_on(async {
|
||||
let mut proxy_config = state.db.get_proxy_config().await.expect("get proxy config");
|
||||
proxy_config.listen_port = 0;
|
||||
state
|
||||
.db
|
||||
.update_proxy_config(proxy_config)
|
||||
.await
|
||||
.expect("set ephemeral proxy port");
|
||||
});
|
||||
|
||||
// ---- 两个 Claude 供应商:custom1 与 custom2 ----
|
||||
let mut custom1 = claude_provider("custom1", "custom-key-1");
|
||||
custom1.category = Some("custom".to_string());
|
||||
state
|
||||
.db
|
||||
.save_provider(AppType::Claude.as_str(), &custom1)
|
||||
.expect("save custom1 provider");
|
||||
|
||||
let mut custom2 = claude_provider("custom2", "custom-key-2");
|
||||
custom2.category = Some("custom".to_string());
|
||||
state
|
||||
.db
|
||||
.save_provider(AppType::Claude.as_str(), &custom2)
|
||||
.expect("save custom2 provider");
|
||||
|
||||
// 初始状态:custom1 + 代理接管
|
||||
ProviderService::switch(&state, AppType::Claude, "custom1").expect("switch to custom1");
|
||||
let rt = tokio::runtime::Runtime::new().expect("create tokio runtime");
|
||||
rt.block_on(state.proxy_service.set_takeover_for_app("claude", true))
|
||||
.expect("enable claude takeover");
|
||||
|
||||
let (proxy_enabled_before, _) = state.db.get_proxy_flags_sync("claude");
|
||||
assert!(
|
||||
proxy_enabled_before,
|
||||
"takeover should be active before apply"
|
||||
);
|
||||
|
||||
// ---- 构造一个目标为 custom2 的项目快照 ----
|
||||
let project = ProfileService::create(&state, "Custom2 Project", ProfileScope::Claude)
|
||||
.expect("create project");
|
||||
let mut project = state
|
||||
.db
|
||||
.get_profile(&project.id)
|
||||
.expect("get project")
|
||||
.expect("project exists");
|
||||
let mut payload: ProfilePayload =
|
||||
serde_json::from_str(&project.payload).expect("parse project payload");
|
||||
payload.providers.claude = Some("custom2".to_string());
|
||||
project.payload = serde_json::to_string(&payload).expect("serialize payload");
|
||||
state
|
||||
.db
|
||||
.save_profile(&project)
|
||||
.expect("save updated project");
|
||||
|
||||
// ---- 应用项目:应无条件自动关闭接管,再切换到 custom2 ----
|
||||
let warnings = ProfileService::apply(&state, &project.id, ProfileScope::Claude)
|
||||
.expect("apply custom2 project");
|
||||
assert!(
|
||||
warnings.is_empty(),
|
||||
"switching project should not warn: {warnings:?}"
|
||||
);
|
||||
|
||||
// 接管已关闭
|
||||
let (proxy_enabled_after, _) = state.db.get_proxy_flags_sync("claude");
|
||||
assert!(
|
||||
!proxy_enabled_after,
|
||||
"proxy takeover should be auto-disabled before applying profile"
|
||||
);
|
||||
|
||||
// 当前供应商已切到 custom2
|
||||
assert_eq!(
|
||||
state
|
||||
.db
|
||||
.get_current_provider(AppType::Claude.as_str())
|
||||
.expect("get current provider")
|
||||
.as_deref(),
|
||||
Some("custom2"),
|
||||
"current provider should be custom2"
|
||||
);
|
||||
|
||||
// live 配置应指向 custom2 的真实 endpoint,而非代理地址
|
||||
let settings_path = home.join(".claude/settings.json");
|
||||
let settings: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&settings_path).expect("read settings"))
|
||||
.expect("parse settings");
|
||||
let base_url = settings
|
||||
.get("env")
|
||||
.and_then(|e| e.get("ANTHROPIC_BASE_URL"))
|
||||
.and_then(|v| v.as_str());
|
||||
assert_eq!(
|
||||
base_url,
|
||||
Some("https://api.test"),
|
||||
"live config should point to real endpoint after auto-disable"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user