//! Live configuration operations //! //! Handles reading and writing live configuration files for Claude, Codex, and Gemini. use std::collections::HashMap; use serde_json::{json, Value}; use crate::app_config::AppType; use crate::codex_config::{get_codex_auth_path, get_codex_config_path}; use crate::config::{delete_file, get_claude_settings_path, read_json_file, write_json_file}; use crate::error::AppError; use crate::provider::Provider; use crate::services::mcp::McpService; use crate::store::AppState; use super::gemini_auth::{ detect_gemini_auth_type, ensure_google_oauth_security_flag, ensure_packycode_security_flag, GeminiAuthType, }; use super::normalize_claude_models_in_value; /// Live configuration snapshot for backup/restore #[derive(Clone)] #[allow(dead_code)] pub(crate) enum LiveSnapshot { Claude { settings: Option, }, Codex { auth: Option, config: Option, }, Gemini { env: Option>, config: Option, }, } impl LiveSnapshot { #[allow(dead_code)] pub(crate) fn restore(&self) -> Result<(), AppError> { match self { LiveSnapshot::Claude { settings } => { let path = get_claude_settings_path(); if let Some(value) = settings { write_json_file(&path, value)?; } else if path.exists() { delete_file(&path)?; } } LiveSnapshot::Codex { auth, config } => { let auth_path = get_codex_auth_path(); let config_path = get_codex_config_path(); if let Some(value) = auth { write_json_file(&auth_path, value)?; } else if auth_path.exists() { delete_file(&auth_path)?; } if let Some(text) = config { crate::config::write_text_file(&config_path, text)?; } else if config_path.exists() { delete_file(&config_path)?; } } LiveSnapshot::Gemini { env, .. } => { use crate::gemini_config::{ get_gemini_env_path, get_gemini_settings_path, write_gemini_env_atomic, }; let path = get_gemini_env_path(); if let Some(env_map) = env { write_gemini_env_atomic(env_map)?; } else if path.exists() { delete_file(&path)?; } let settings_path = get_gemini_settings_path(); match self { LiveSnapshot::Gemini { config: Some(cfg), .. } => { write_json_file(&settings_path, cfg)?; } LiveSnapshot::Gemini { config: None, .. } if settings_path.exists() => { delete_file(&settings_path)?; } _ => {} } } } Ok(()) } } /// Write live configuration snapshot for a provider pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Result<(), AppError> { match app_type { AppType::Claude => { let path = get_claude_settings_path(); write_json_file(&path, &provider.settings_config)?; } AppType::Codex => { let obj = provider.settings_config.as_object().ok_or_else(|| { AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string()) })?; let auth = obj.get("auth").ok_or_else(|| { AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()) })?; let config_str = obj.get("config").and_then(|v| v.as_str()).ok_or_else(|| { AppError::Config("Codex 供应商配置缺少 'config' 字段或不是字符串".to_string()) })?; let auth_path = get_codex_auth_path(); write_json_file(&auth_path, auth)?; let config_path = get_codex_config_path(); std::fs::write(&config_path, config_str) .map_err(|e| AppError::io(&config_path, e))?; } AppType::Gemini => { use crate::gemini_config::{ get_gemini_settings_path, json_to_env, write_gemini_env_atomic, }; // Extract env and config from provider settings let env_value = provider.settings_config.get("env"); let config_value = provider.settings_config.get("config"); // Write env file if let Some(env) = env_value { let env_map = json_to_env(env)?; write_gemini_env_atomic(&env_map)?; } // Write settings file if let Some(config) = config_value { let settings_path = get_gemini_settings_path(); write_json_file(&settings_path, config)?; } } } Ok(()) } /// Sync current provider from database to live configuration pub fn sync_current_from_db(state: &AppState) -> Result<(), AppError> { for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] { let current_id = match state.db.get_current_provider(app_type.as_str())? { Some(id) => id, None => continue, }; let providers = state.db.get_all_providers(app_type.as_str())?; if let Some(provider) = providers.get(¤t_id) { write_live_snapshot(&app_type, provider)?; } else { log::warn!( "无法同步 live 配置: 当前供应商 {} ({}) 未找到", current_id, app_type.as_str() ); } } // MCP sync McpService::sync_all_enabled(state)?; Ok(()) } /// Read current live settings for an app type pub fn read_live_settings(app_type: AppType) -> Result { match app_type { AppType::Codex => { let auth_path = get_codex_auth_path(); if !auth_path.exists() { return Err(AppError::localized( "codex.auth.missing", "Codex 配置文件不存在:缺少 auth.json", "Codex configuration missing: auth.json not found", )); } let auth: Value = read_json_file(&auth_path)?; let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?; Ok(json!({ "auth": auth, "config": cfg_text })) } AppType::Claude => { let path = get_claude_settings_path(); if !path.exists() { return Err(AppError::localized( "claude.live.missing", "Claude Code 配置文件不存在", "Claude settings file is missing", )); } read_json_file(&path) } AppType::Gemini => { use crate::gemini_config::{ env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env, }; // Read .env file (environment variables) let env_path = get_gemini_env_path(); if !env_path.exists() { return Err(AppError::localized( "gemini.env.missing", "Gemini .env 文件不存在", "Gemini .env file not found", )); } let env_map = read_gemini_env()?; let env_json = env_to_json(&env_map); let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({})); // Read settings.json file (MCP config etc.) let settings_path = get_gemini_settings_path(); let config_obj = if settings_path.exists() { read_json_file(&settings_path)? } else { json!({}) }; // Return complete structure: { "env": {...}, "config": {...} } Ok(json!({ "env": env_obj, "config": config_obj })) } } } /// Import default configuration from live files pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> { { let providers = state.db.get_all_providers(app_type.as_str())?; if !providers.is_empty() { return Ok(()); } } let settings_config = match app_type { AppType::Codex => { let auth_path = get_codex_auth_path(); if !auth_path.exists() { return Err(AppError::localized( "codex.live.missing", "Codex 配置文件不存在", "Codex configuration file is missing", )); } let auth: Value = read_json_file(&auth_path)?; let config_str = crate::codex_config::read_and_validate_codex_config_text()?; json!({ "auth": auth, "config": config_str }) } AppType::Claude => { let settings_path = get_claude_settings_path(); if !settings_path.exists() { return Err(AppError::localized( "claude.live.missing", "Claude Code 配置文件不存在", "Claude settings file is missing", )); } let mut v = read_json_file::(&settings_path)?; let _ = normalize_claude_models_in_value(&mut v); v } AppType::Gemini => { use crate::gemini_config::{ env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env, }; // Read .env file (environment variables) let env_path = get_gemini_env_path(); if !env_path.exists() { return Err(AppError::localized( "gemini.live.missing", "Gemini 配置文件不存在", "Gemini configuration file is missing", )); } let env_map = read_gemini_env()?; let env_json = env_to_json(&env_map); let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({})); // Read settings.json file (MCP config etc.) let settings_path = get_gemini_settings_path(); let config_obj = if settings_path.exists() { read_json_file(&settings_path)? } else { json!({}) }; // Return complete structure: { "env": {...}, "config": {...} } json!({ "env": env_obj, "config": config_obj }) } }; let mut provider = Provider::with_id( "default".to_string(), "default".to_string(), settings_config, None, ); provider.category = Some("custom".to_string()); state.db.save_provider(app_type.as_str(), &provider)?; state .db .set_current_provider(app_type.as_str(), &provider.id)?; Ok(()) } /// Write Gemini live configuration with authentication handling pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> { use crate::gemini_config::{ get_gemini_settings_path, json_to_env, validate_gemini_settings_strict, write_gemini_env_atomic, }; // One-time auth type detection to avoid repeated detection let auth_type = detect_gemini_auth_type(provider); 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 { return Err(AppError::localized( "gemini.validation.invalid_config", "Gemini 配置格式错误: config 必须是对象或 null", "Gemini config invalid: config must be an object or null", )); } } else { None }; 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)?); } } match auth_type { GeminiAuthType::GoogleOfficial => { // Google official uses OAuth, clear env env_map.clear(); write_gemini_env_atomic(&env_map)?; } GeminiAuthType::Packycode => { // PackyCode provider, uses API Key (strict validation on switch) validate_gemini_settings_strict(&provider.settings_config)?; write_gemini_env_atomic(&env_map)?; } GeminiAuthType::Generic => { // Generic provider, uses API Key (strict validation on switch) validate_gemini_settings_strict(&provider.settings_config)?; write_gemini_env_atomic(&env_map)?; } } if let Some(config_value) = config_to_write { let settings_path = get_gemini_settings_path(); write_json_file(&settings_path, &config_value)?; } match auth_type { GeminiAuthType::GoogleOfficial => ensure_google_oauth_security_flag(provider)?, GeminiAuthType::Packycode => ensure_packycode_security_flag(provider)?, GeminiAuthType::Generic => {} } Ok(()) }