mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
fix: sync Claude Desktop profile during proxy takeover (#3157)
* fix: sync Claude Desktop profile during proxy takeover * fix(provider): skip Claude Desktop backup refresh during takeover - Route Claude Desktop takeover updates directly through the 3P profile writer. - Keep takeover startup backup state unchanged when provider metadata changes. - Narrow platform-specific test helpers and environment setup with cfg gates. * fix(provider): restore PathBuf import for CI tests - Restore an unconditional PathBuf import for provider tests. - Keep Linux cargo test builds compiling while preserving Claude Desktop cfg-gated helpers. --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -55,9 +55,13 @@ pub struct SwitchResult {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
use crate::claude_desktop_config::PROFILE_ID;
|
||||
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
|
||||
use crate::database::Database;
|
||||
use crate::provider::ProviderMeta;
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
use crate::provider::{ClaudeDesktopMode, ClaudeDesktopModelRoute};
|
||||
use crate::proxy::types::ProxyConfig;
|
||||
use crate::store::AppState;
|
||||
use serde_json::json;
|
||||
@@ -72,6 +76,8 @@ mod tests {
|
||||
#[allow(dead_code)]
|
||||
dir: TempDir,
|
||||
original_home: Option<String>,
|
||||
#[cfg(windows)]
|
||||
original_local_app_data: Option<String>,
|
||||
original_userprofile: Option<String>,
|
||||
original_test_home: Option<String>,
|
||||
}
|
||||
@@ -80,16 +86,22 @@ mod tests {
|
||||
fn new() -> Self {
|
||||
let dir = TempDir::new().expect("failed to create temp home");
|
||||
let original_home = env::var("HOME").ok();
|
||||
#[cfg(windows)]
|
||||
let original_local_app_data = env::var("LOCALAPPDATA").ok();
|
||||
let original_userprofile = env::var("USERPROFILE").ok();
|
||||
let original_test_home = env::var("CC_SWITCH_TEST_HOME").ok();
|
||||
|
||||
env::set_var("HOME", dir.path());
|
||||
#[cfg(windows)]
|
||||
env::set_var("LOCALAPPDATA", dir.path().join("AppData").join("Local"));
|
||||
env::set_var("USERPROFILE", dir.path());
|
||||
env::set_var("CC_SWITCH_TEST_HOME", dir.path());
|
||||
|
||||
Self {
|
||||
dir,
|
||||
original_home,
|
||||
#[cfg(windows)]
|
||||
original_local_app_data,
|
||||
original_userprofile,
|
||||
original_test_home,
|
||||
}
|
||||
@@ -103,6 +115,14 @@ mod tests {
|
||||
None => env::remove_var("HOME"),
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
match &self.original_local_app_data {
|
||||
Some(value) => env::set_var("LOCALAPPDATA", value),
|
||||
None => env::remove_var("LOCALAPPDATA"),
|
||||
}
|
||||
}
|
||||
|
||||
match &self.original_userprofile {
|
||||
Some(value) => env::set_var("USERPROFILE", value),
|
||||
None => env::remove_var("USERPROFILE"),
|
||||
@@ -115,6 +135,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn claude_desktop_profile_path(home: &Path) -> PathBuf {
|
||||
home.join("AppData")
|
||||
.join("Local")
|
||||
.join("Claude-3p")
|
||||
.join("configLibrary")
|
||||
.join(format!("{PROFILE_ID}.json"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn claude_desktop_profile_path(home: &Path) -> PathBuf {
|
||||
home.join("Library")
|
||||
.join("Application Support")
|
||||
.join("Claude-3p")
|
||||
.join("configLibrary")
|
||||
.join(format!("{PROFILE_ID}.json"))
|
||||
}
|
||||
|
||||
fn test_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
@@ -475,6 +513,122 @@ base_url = "http://localhost:8080"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn update_current_claude_desktop_provider_syncs_profile_when_proxy_takeover_is_active() {
|
||||
let home = TempHome::new();
|
||||
crate::settings::reload_settings().expect("reload settings");
|
||||
|
||||
let db = Arc::new(Database::memory().expect("init db"));
|
||||
let state = AppState::new(db.clone());
|
||||
|
||||
let mut original = Provider::with_id(
|
||||
"p1".into(),
|
||||
"Desktop A".into(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "token-a",
|
||||
"ANTHROPIC_BASE_URL": "https://opencode.ai/zen/go"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
original.meta = Some(ProviderMeta {
|
||||
api_format: Some("openai_chat".into()),
|
||||
claude_desktop_mode: Some(ClaudeDesktopMode::Proxy),
|
||||
claude_desktop_model_routes: std::collections::HashMap::from([(
|
||||
"claude-sonnet-4-6".into(),
|
||||
ClaudeDesktopModelRoute {
|
||||
model: "deepseek-v4-flash".into(),
|
||||
label_override: Some("DeepSeek V4 Flash".into()),
|
||||
supports_1m: None,
|
||||
},
|
||||
)]),
|
||||
..Default::default()
|
||||
});
|
||||
db.save_provider("claude-desktop", &original)
|
||||
.expect("save provider");
|
||||
db.set_current_provider("claude-desktop", "p1")
|
||||
.expect("set current provider");
|
||||
crate::settings::set_current_provider(&AppType::ClaudeDesktop, Some("p1"))
|
||||
.expect("set local current provider");
|
||||
|
||||
// Claude Desktop keeps backup state from takeover startup; this sentinel only
|
||||
// marks takeover as active so provider updates rewrite the 3P profile.
|
||||
db.save_live_backup("claude-desktop", "{}")
|
||||
.await
|
||||
.expect("seed live backup");
|
||||
{
|
||||
let mut config = db
|
||||
.get_proxy_config_for_app("claude-desktop")
|
||||
.await
|
||||
.expect("get app proxy config");
|
||||
config.enabled = true;
|
||||
db.update_proxy_config_for_app(config)
|
||||
.await
|
||||
.expect("update app proxy config");
|
||||
}
|
||||
|
||||
state
|
||||
.proxy_service
|
||||
.start()
|
||||
.await
|
||||
.expect("start proxy service");
|
||||
|
||||
let mut updated = Provider::with_id(
|
||||
"p1".into(),
|
||||
"Desktop A".into(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "token-updated",
|
||||
"ANTHROPIC_BASE_URL": "https://opencode.ai/zen/go"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
updated.meta = Some(ProviderMeta {
|
||||
api_format: Some("openai_chat".into()),
|
||||
claude_desktop_mode: Some(ClaudeDesktopMode::Proxy),
|
||||
claude_desktop_model_routes: std::collections::HashMap::from([(
|
||||
"claude-sonnet-4-6".into(),
|
||||
ClaudeDesktopModelRoute {
|
||||
model: "deepseek-v4-flash".into(),
|
||||
label_override: Some("DeepSeek V4 Flash Updated".into()),
|
||||
supports_1m: Some(true),
|
||||
},
|
||||
)]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
ProviderService::update(&state, AppType::ClaudeDesktop, None, updated.clone())
|
||||
.expect("update current provider");
|
||||
|
||||
let backup = db
|
||||
.get_live_backup("claude-desktop")
|
||||
.await
|
||||
.expect("get live backup")
|
||||
.expect("backup exists");
|
||||
assert_eq!(
|
||||
backup.original_config, "{}",
|
||||
"Claude Desktop provider edits should not rewrite takeover backup"
|
||||
);
|
||||
|
||||
let profile_path = claude_desktop_profile_path(home.dir.path());
|
||||
let profile: Value = read_json_file(&profile_path).expect("read desktop profile");
|
||||
assert_eq!(
|
||||
profile["inferenceGatewayBaseUrl"],
|
||||
json!("http://127.0.0.1:15721/claude-desktop"),
|
||||
"desktop profile should stay pointed at the local gateway during takeover"
|
||||
);
|
||||
assert_eq!(profile["inferenceGatewayAuthScheme"], json!("bearer"));
|
||||
assert_eq!(
|
||||
profile["inferenceModels"],
|
||||
json!([{ "name": "claude-sonnet-4-6", "labelOverride": "DeepSeek V4 Flash Updated", "supports1m": true }]),
|
||||
"provider edits should propagate into the Claude Desktop 3P profile during takeover"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn rename_rejects_missing_original_provider() {
|
||||
@@ -1244,12 +1398,16 @@ impl ProviderService {
|
||||
let should_sync_via_proxy = is_proxy_running && (has_live_backup || live_taken_over);
|
||||
|
||||
if should_sync_via_proxy {
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.update_live_backup_from_provider(app_type.as_str(), &provider),
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||
if matches!(app_type, AppType::ClaudeDesktop) {
|
||||
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
|
||||
} else {
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
.update_live_backup_from_provider(app_type.as_str(), &provider),
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("更新 Live 备份失败: {e}")))?;
|
||||
}
|
||||
|
||||
if matches!(app_type, AppType::Claude) {
|
||||
futures::executor::block_on(
|
||||
@@ -1658,6 +1816,11 @@ impl ProviderService {
|
||||
.detect_takeover_in_live_config_for_app(&app_type);
|
||||
|
||||
if takeover_enabled && (has_live_backup || live_taken_over) {
|
||||
if matches!(app_type, AppType::ClaudeDesktop) {
|
||||
write_live_with_common_config(state.db.as_ref(), &app_type, provider)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
futures::executor::block_on(
|
||||
state
|
||||
.proxy_service
|
||||
|
||||
@@ -299,6 +299,22 @@ export const claudeDesktopProviderPresets: ClaudeDesktopProviderPreset[] = [
|
||||
icon: "deepseek",
|
||||
iconColor: "#1E88E5",
|
||||
},
|
||||
{
|
||||
name: "OpenCode Go (DeepSeek V4 Flash)",
|
||||
websiteUrl: "https://opencode.ai",
|
||||
category: "third_party",
|
||||
baseUrl: "https://opencode.ai/zen/go",
|
||||
mode: "proxy",
|
||||
apiFormat: "openai_chat",
|
||||
modelRoutes: brandedRoutes(
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-flash",
|
||||
),
|
||||
endpointCandidates: ["https://opencode.ai/zen/go"],
|
||||
icon: "opencode",
|
||||
iconColor: "#211E1E",
|
||||
},
|
||||
{
|
||||
name: "Zhipu GLM",
|
||||
websiteUrl: "https://open.bigmodel.cn",
|
||||
|
||||
@@ -229,6 +229,25 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "deepseek",
|
||||
iconColor: "#1E88E5",
|
||||
},
|
||||
{
|
||||
name: "OpenCode Go (DeepSeek V4 Flash)",
|
||||
websiteUrl: "https://opencode.ai",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://opencode.ai/zen/go",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "deepseek-v4-flash",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "deepseek-v4-flash",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "deepseek-v4-flash",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "deepseek-v4-flash",
|
||||
},
|
||||
},
|
||||
category: "third_party",
|
||||
apiFormat: "openai_chat",
|
||||
endpointCandidates: ["https://opencode.ai/zen/go"],
|
||||
icon: "opencode",
|
||||
iconColor: "#211E1E",
|
||||
},
|
||||
{
|
||||
name: "Zhipu GLM",
|
||||
websiteUrl: "https://open.bigmodel.cn",
|
||||
|
||||
Reference in New Issue
Block a user