mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
refactor(gemini): improve config handling and code consistency
- Fix envObjToString to preserve custom environment variables - Add bidirectional MCP format conversion (httpUrl <-> url) - Merge provider config with existing settings.json instead of overwriting - Remove redundant is_packycode_gemini function - Simplify is_google_official_gemini using detect_gemini_auth_type - Add handleGeminiModelChange for consistent field handling
This commit is contained in:
@@ -49,6 +49,11 @@ pub fn read_mcp_json() -> Result<Option<String>, AppError> {
|
||||
}
|
||||
|
||||
/// 读取 Gemini settings.json 中的 mcpServers 映射
|
||||
///
|
||||
/// 执行反向格式转换以保持与统一 MCP 结构的兼容性:
|
||||
/// - httpUrl → url + type: "http"
|
||||
/// - 仅有 url 字段 → 保持不变(SSE 类型)
|
||||
/// - 仅有 command 字段 → 保持不变(stdio 类型)
|
||||
pub fn read_mcp_servers_map() -> Result<std::collections::HashMap<String, Value>, AppError> {
|
||||
let path = user_config_path();
|
||||
if !path.exists() {
|
||||
@@ -56,12 +61,25 @@ pub fn read_mcp_servers_map() -> Result<std::collections::HashMap<String, Value>
|
||||
}
|
||||
|
||||
let root = read_json_value(&path)?;
|
||||
let servers = root
|
||||
let mut servers: std::collections::HashMap<String, Value> = root
|
||||
.get("mcpServers")
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 反向格式转换:Gemini 特有格式 → 统一 MCP 格式
|
||||
for (_, spec) in servers.iter_mut() {
|
||||
if let Some(obj) = spec.as_object_mut() {
|
||||
// httpUrl → url + type: "http"
|
||||
if let Some(http_url) = obj.remove("httpUrl") {
|
||||
obj.insert("url".to_string(), http_url);
|
||||
obj.insert("type".to_string(), Value::String("http".to_string()));
|
||||
}
|
||||
// 如果有 url 但没有 type,不添加 type(默认为 SSE)
|
||||
// 如果有 command 但没有 type,不添加 type(默认为 stdio)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(servers)
|
||||
}
|
||||
|
||||
|
||||
@@ -89,104 +89,13 @@ fn contains_packycode_keyword(value: &str) -> bool {
|
||||
.any(|keyword| lower.contains(keyword))
|
||||
}
|
||||
|
||||
/// Detect if provider is PackyCode Gemini (uses API Key authentication)
|
||||
///
|
||||
/// PackyCode is an official partner requiring special security configuration.
|
||||
///
|
||||
/// # Detection Rules (priority from high to low)
|
||||
///
|
||||
/// 1. **Partner Promotion Key** (most reliable):
|
||||
/// - `provider.meta.partner_promotion_key == "packycode"`
|
||||
///
|
||||
/// 2. **Provider name**:
|
||||
/// - Name contains "packycode", "packyapi" or "packy" (case-insensitive)
|
||||
///
|
||||
/// 3. **Website URL**:
|
||||
/// - `provider.website_url` contains keywords
|
||||
///
|
||||
/// 4. **Base URL**:
|
||||
/// - `settings_config.env.GOOGLE_GEMINI_BASE_URL` contains keywords
|
||||
///
|
||||
/// # Why multiple detection methods
|
||||
///
|
||||
/// - Users may manually create providers without `partner_promotion_key`
|
||||
/// - Meta fields may be modified after copying from presets
|
||||
/// - Ensure all PackyCode providers get correct security flags
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn is_packycode_gemini(provider: &Provider) -> bool {
|
||||
// Strategy 1: Check partner_promotion_key (most reliable)
|
||||
if provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.partner_promotion_key.as_deref())
|
||||
.is_some_and(|key| key.eq_ignore_ascii_case(PACKYCODE_PARTNER_KEY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strategy 2: Check provider name
|
||||
if contains_packycode_keyword(&provider.name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strategy 3: Check website URL
|
||||
if let Some(site) = provider.website_url.as_deref() {
|
||||
if contains_packycode_keyword(site) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 4: Check Base URL
|
||||
if let Some(base_url) = provider
|
||||
.settings_config
|
||||
.pointer("/env/GOOGLE_GEMINI_BASE_URL")
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
if contains_packycode_keyword(base_url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Detect if provider is Google Official Gemini (uses OAuth authentication)
|
||||
///
|
||||
/// Google Official Gemini uses OAuth personal authentication, no API Key needed.
|
||||
///
|
||||
/// # Detection Rules (priority from high to low)
|
||||
///
|
||||
/// 1. **Partner Promotion Key** (most reliable):
|
||||
/// - `provider.meta.partner_promotion_key == "google-official"`
|
||||
///
|
||||
/// 2. **Provider name**:
|
||||
/// - Name equals "google" (case-insensitive)
|
||||
/// - Or name starts with "google " (e.g., "Google Official")
|
||||
///
|
||||
/// # OAuth vs API Key
|
||||
///
|
||||
/// - **OAuth mode**: `security.auth.selectedType = "oauth-personal"`
|
||||
/// - User needs to login via browser with Google account
|
||||
/// - No API Key needed in `.env` file
|
||||
///
|
||||
/// - **API Key mode**: `security.auth.selectedType = "gemini-api-key"`
|
||||
/// - Used for third-party relay services (like PackyCode)
|
||||
/// - Requires `GEMINI_API_KEY` in `.env` file
|
||||
#[allow(dead_code)]
|
||||
/// This is a convenience wrapper around `detect_gemini_auth_type`.
|
||||
pub(crate) fn is_google_official_gemini(provider: &Provider) -> bool {
|
||||
// Strategy 1: Check partner_promotion_key (most reliable)
|
||||
if provider
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.partner_promotion_key.as_deref())
|
||||
.is_some_and(|key| key.eq_ignore_ascii_case(GOOGLE_OFFICIAL_PARTNER_KEY))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strategy 2: Check name matching (fallback)
|
||||
let name_lower = provider.name.to_ascii_lowercase();
|
||||
name_lower == "google" || name_lower.starts_with("google ")
|
||||
detect_gemini_auth_type(provider) == GeminiAuthType::GoogleOfficial
|
||||
}
|
||||
|
||||
/// Ensure Google Official Gemini provider security flag is correctly set (OAuth mode)
|
||||
|
||||
@@ -310,28 +310,44 @@ pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
||||
|
||||
let mut env_map = json_to_env(&provider.settings_config)?;
|
||||
|
||||
// Prepare config to write to ~/.gemini/settings.json (preserve existing file content when absent)
|
||||
let mut config_to_write = if let Some(config_value) = provider.settings_config.get("config") {
|
||||
if config_value.is_null() {
|
||||
Some(json!({}))
|
||||
} else if config_value.is_object() {
|
||||
Some(config_value.clone())
|
||||
} else {
|
||||
// Prepare config to write to ~/.gemini/settings.json
|
||||
// Behavior:
|
||||
// - config is object: use it (merge with existing to preserve mcpServers etc.)
|
||||
// - config is null or absent: preserve existing file content
|
||||
let settings_path = get_gemini_settings_path();
|
||||
let mut config_to_write: Option<Value> = None;
|
||||
|
||||
if let Some(config_value) = provider.settings_config.get("config") {
|
||||
if config_value.is_object() {
|
||||
// Merge with existing settings to preserve mcpServers and other fields
|
||||
let mut merged = if settings_path.exists() {
|
||||
read_json_file::<Value>(&settings_path).unwrap_or_else(|_| json!({}))
|
||||
} else {
|
||||
json!({})
|
||||
};
|
||||
|
||||
// Merge provider config into existing settings
|
||||
if let (Some(merged_obj), Some(config_obj)) =
|
||||
(merged.as_object_mut(), config_value.as_object())
|
||||
{
|
||||
for (k, v) in config_obj {
|
||||
merged_obj.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
config_to_write = Some(merged);
|
||||
} else if !config_value.is_null() {
|
||||
return Err(AppError::localized(
|
||||
"gemini.validation.invalid_config",
|
||||
"Gemini 配置格式错误: config 必须是对象或 null",
|
||||
"Gemini config invalid: config must be an object or null",
|
||||
));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// config is null: don't modify existing settings.json (preserve mcpServers etc.)
|
||||
}
|
||||
|
||||
if config_to_write.is_none() {
|
||||
let settings_path = get_gemini_settings_path();
|
||||
if settings_path.exists() {
|
||||
config_to_write = Some(read_json_file(&settings_path)?);
|
||||
}
|
||||
// If no config specified or config is null, preserve existing file
|
||||
if config_to_write.is_none() && settings_path.exists() {
|
||||
config_to_write = Some(read_json_file(&settings_path)?);
|
||||
}
|
||||
|
||||
match auth_type {
|
||||
@@ -353,7 +369,6 @@ pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
||||
}
|
||||
|
||||
if let Some(config_value) = config_to_write {
|
||||
let settings_path = get_gemini_settings_path();
|
||||
write_json_file(&settings_path, &config_value)?;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user