Skip to main content

cc_switch_lib/
claude_plugin.rs

1use std::fs;
2use std::path::PathBuf;
3
4use crate::app_config::AppType;
5use crate::error::AppError;
6use crate::provider::Provider;
7
8const CLAUDE_DIR: &str = ".claude";
9const CLAUDE_CONFIG_FILE: &str = "config.json";
10
11fn claude_dir() -> Result<PathBuf, AppError> {
12    // 优先使用设置中的覆盖目录
13    if let Some(dir) = crate::settings::get_claude_override_dir() {
14        return Ok(dir);
15    }
16    let home = dirs::home_dir().ok_or_else(|| AppError::Config("无法获取用户主目录".into()))?;
17    Ok(home.join(CLAUDE_DIR))
18}
19
20pub fn claude_config_path() -> Result<PathBuf, AppError> {
21    Ok(claude_dir()?.join(CLAUDE_CONFIG_FILE))
22}
23
24pub fn ensure_claude_dir_exists() -> Result<PathBuf, AppError> {
25    let dir = claude_dir()?;
26    if !dir.exists() {
27        fs::create_dir_all(&dir).map_err(|e| AppError::io(&dir, e))?;
28    }
29    Ok(dir)
30}
31
32pub fn read_claude_config() -> Result<Option<String>, AppError> {
33    let path = claude_config_path()?;
34    if path.exists() {
35        let content = fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?;
36        Ok(Some(content))
37    } else {
38        Ok(None)
39    }
40}
41
42fn is_managed_config(content: &str) -> bool {
43    match serde_json::from_str::<serde_json::Value>(content) {
44        Ok(value) => value
45            .get("primaryApiKey")
46            .and_then(|v| v.as_str())
47            .map(|val| val == "any")
48            .unwrap_or(false),
49        Err(_) => false,
50    }
51}
52
53pub fn write_claude_config() -> Result<bool, AppError> {
54    // 增量写入:仅设置 primaryApiKey = "any",保留其它字段
55    let path = claude_config_path()?;
56    ensure_claude_dir_exists()?;
57
58    // 尝试读取并解析为对象
59    let mut obj = match read_claude_config()? {
60        Some(existing) => match serde_json::from_str::<serde_json::Value>(&existing) {
61            Ok(serde_json::Value::Object(map)) => serde_json::Value::Object(map),
62            _ => serde_json::json!({}),
63        },
64        None => serde_json::json!({}),
65    };
66
67    let mut changed = false;
68    if let Some(map) = obj.as_object_mut() {
69        let cur = map
70            .get("primaryApiKey")
71            .and_then(|v| v.as_str())
72            .unwrap_or("");
73        if cur != "any" {
74            map.insert(
75                "primaryApiKey".to_string(),
76                serde_json::Value::String("any".to_string()),
77            );
78            changed = true;
79        }
80    }
81
82    if changed || !path.exists() {
83        let serialized = serde_json::to_string_pretty(&obj)
84            .map_err(|e| AppError::JsonSerialize { source: e })?;
85        fs::write(&path, format!("{serialized}\n")).map_err(|e| AppError::io(&path, e))?;
86        Ok(true)
87    } else {
88        Ok(false)
89    }
90}
91
92pub fn clear_claude_config() -> Result<bool, AppError> {
93    let path = claude_config_path()?;
94    if !path.exists() {
95        return Ok(false);
96    }
97
98    let content = match read_claude_config()? {
99        Some(content) => content,
100        None => return Ok(false),
101    };
102
103    let mut value = match serde_json::from_str::<serde_json::Value>(&content) {
104        Ok(value) => value,
105        Err(_) => return Ok(false),
106    };
107
108    let obj = match value.as_object_mut() {
109        Some(obj) => obj,
110        None => return Ok(false),
111    };
112
113    if obj.remove("primaryApiKey").is_none() {
114        return Ok(false);
115    }
116
117    let serialized =
118        serde_json::to_string_pretty(&value).map_err(|e| AppError::JsonSerialize { source: e })?;
119    fs::write(&path, format!("{serialized}\n")).map_err(|e| AppError::io(&path, e))?;
120    Ok(true)
121}
122
123pub fn claude_config_status() -> Result<(bool, PathBuf), AppError> {
124    let path = claude_config_path()?;
125    Ok((path.exists(), path))
126}
127
128pub fn is_claude_config_applied() -> Result<bool, AppError> {
129    match read_claude_config()? {
130        Some(content) => Ok(is_managed_config(&content)),
131        None => Ok(false),
132    }
133}
134
135pub fn sync_claude_plugin_on_settings_toggle(enabled: bool) -> Result<(), AppError> {
136    if enabled {
137        let _ = write_claude_config()?;
138    } else {
139        let _ = clear_claude_config()?;
140    }
141    Ok(())
142}
143
144pub fn sync_claude_plugin_on_provider_switch(
145    app_type: &AppType,
146    provider: &Provider,
147) -> Result<(), AppError> {
148    if !matches!(app_type, AppType::Claude) {
149        return Ok(());
150    }
151
152    if !crate::settings::get_enable_claude_plugin_integration() {
153        return Ok(());
154    }
155
156    let is_official = provider.category.as_deref() == Some("official");
157    if is_official {
158        let _ = clear_claude_config()?;
159    } else {
160        let _ = write_claude_config()?;
161    }
162
163    Ok(())
164}