diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index d53c604..7bccb7e 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -4,7 +4,7 @@ use std::env; use std::fs; use std::path::{Path, PathBuf}; -use crate::config::atomic_write; +use crate::config::{atomic_write, get_claude_mcp_path, get_default_claude_mcp_path}; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -15,10 +15,49 @@ pub struct McpStatus { } fn user_config_path() -> PathBuf { - // 用户级 MCP 配置文件:~/.claude.json - dirs::home_dir() - .expect("无法获取用户主目录") - .join(".claude.json") + ensure_mcp_override_migrated(); + get_claude_mcp_path() +} + +fn ensure_mcp_override_migrated() { + if crate::settings::get_claude_override_dir().is_none() { + return; + } + + let new_path = get_claude_mcp_path(); + if new_path.exists() { + return; + } + + let legacy_path = get_default_claude_mcp_path(); + if !legacy_path.exists() { + return; + } + + if let Some(parent) = new_path.parent() { + if let Err(err) = fs::create_dir_all(parent) { + log::warn!("创建 MCP 目录失败: {}", err); + return; + } + } + + match fs::copy(&legacy_path, &new_path) { + Ok(_) => { + log::info!( + "已根据覆盖目录复制 MCP 配置: {} -> {}", + legacy_path.display(), + new_path.display() + ); + } + Err(err) => { + log::warn!( + "复制 MCP 配置失败: {} -> {}: {}", + legacy_path.display(), + new_path.display(), + err + ); + } + } } fn read_json_value(path: &Path) -> Result { diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 6523835..554f170 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -15,6 +15,36 @@ pub fn get_claude_config_dir() -> PathBuf { .join(".claude") } +/// 默认 Claude MCP 配置文件路径 (~/.claude.json) +pub fn get_default_claude_mcp_path() -> PathBuf { + dirs::home_dir() + .expect("无法获取用户主目录") + .join(".claude.json") +} + +fn derive_mcp_path_from_override(dir: &Path) -> Option { + let file_name = dir + .file_name() + .map(|name| name.to_string_lossy().to_string())? + .trim() + .to_string(); + if file_name.is_empty() { + return None; + } + let parent = dir.parent().unwrap_or_else(|| Path::new("")); + Some(parent.join(format!("{}.json", file_name))) +} + +/// 获取 Claude MCP 配置文件路径,若设置了目录覆盖则与覆盖目录同级 +pub fn get_claude_mcp_path() -> PathBuf { + if let Some(custom_dir) = crate::settings::get_claude_override_dir() { + if let Some(path) = derive_mcp_path_from_override(&custom_dir) { + return path; + } + } + get_default_claude_mcp_path() +} + /// 获取 Claude Code 主配置文件路径 pub fn get_claude_settings_path() -> PathBuf { let dir = get_claude_config_dir(); @@ -219,6 +249,41 @@ pub fn atomic_write(path: &Path, data: &[u8]) -> Result<(), String> { Ok(()) } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn derive_mcp_path_from_override_preserves_folder_name() { + let override_dir = PathBuf::from("/tmp/profile/.claude"); + let derived = derive_mcp_path_from_override(&override_dir) + .expect("should derive path for nested dir"); + assert_eq!(derived, PathBuf::from("/tmp/profile/.claude.json")); + } + + #[test] + fn derive_mcp_path_from_override_handles_non_hidden_folder() { + let override_dir = PathBuf::from("/data/claude-config"); + let derived = derive_mcp_path_from_override(&override_dir) + .expect("should derive path for standard dir"); + assert_eq!(derived, PathBuf::from("/data/claude-config.json")); + } + + #[test] + fn derive_mcp_path_from_override_supports_relative_rootless_dir() { + let override_dir = PathBuf::from("claude"); + let derived = derive_mcp_path_from_override(&override_dir) + .expect("should derive path for single segment"); + assert_eq!(derived, PathBuf::from("claude.json")); + } + + #[test] + fn derive_mcp_path_from_root_like_dir_returns_none() { + let override_dir = PathBuf::from("/"); + assert!(derive_mcp_path_from_override(&override_dir).is_none()); + } +} + /// 复制文件 pub fn copy_file(from: &Path, to: &Path) -> Result<(), String> { fs::copy(from, to).map_err(|e| format!("复制文件失败: {}", e))?; @@ -249,4 +314,4 @@ pub fn get_claude_config_status() -> ConfigStatus { } } -//(移除未使用的备份/导入函数,避免 dead_code 告警) \ No newline at end of file +//(移除未使用的备份/导入函数,避免 dead_code 告警) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 934858f..e8fc4ee 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -143,7 +143,7 @@ "appConfigDirDescription": "Customize the storage location for CC-Switch configuration files (config.json, etc.)", "browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch", "claudeConfigDir": "Claude Code Configuration Directory", - "claudeConfigDirDescription": "Override Claude configuration directory (settings.json).", + "claudeConfigDirDescription": "Override Claude configuration directory (settings.json) and keep claude.json (MCP) alongside it.", "codexConfigDir": "Codex Configuration Directory", "codexConfigDirDescription": "Override Codex configuration directory.", "browsePlaceholderClaude": "e.g., /home//.claude", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 9f8bf9c..2dd6176 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -143,7 +143,7 @@ "appConfigDirDescription": "自定义 CC-Switch 的配置存储位置(config.json 等文件)", "browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch", "claudeConfigDir": "Claude Code 配置目录", - "claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json)。", + "claudeConfigDirDescription": "覆盖 Claude 配置目录 (settings.json),同时会在同级存放 Claude MCP 的 claude.json。", "codexConfigDir": "Codex 配置目录", "codexConfigDirDescription": "覆盖 Codex 配置目录。", "browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",