/// 配置管理模块 /// /// 采用分层配置设计: /// 1. project.config.toml - 项目默认配置(base_url、default_format) /// 2. ~/.config/tts/config.toml - 用户配置(api_key、default_voice) /// 用户配置会覆盖项目默认配置中的对应项 use anyhow::{Context, Result}; use home::home_dir; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; /// 项目配置(从项目根目录的 project.config.toml 读取) /// /// 这是项目的默认配置,包含 API 地址和默认格式 #[derive(Debug, Deserialize, Clone)] pub struct ProjectConfig { /// 项目版本号 #[allow(dead_code)] pub version: String, /// API 基础 URL 地址 #[serde(default = "default_base_url")] pub base_url: String, /// 默认音频格式 #[serde(default = "default_format")] pub default_format: String, } impl Default for ProjectConfig { fn default() -> Self { Self { version: "0.1.0".to_string(), base_url: default_base_url(), default_format: default_format(), } } } /// 用户配置(从 ~/.config/tts/config.toml 读取) /// /// 只包含用户需要设置的项,会覆盖项目默认配置中的对应项 #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserConfig { /// Mimo-TTS API 密钥 pub api_key: String, /// 默认音色 #[serde(default = "default_voice")] pub default_voice: String, } impl Default for UserConfig { fn default() -> Self { Self { api_key: String::new(), default_voice: default_voice(), } } } /// 合并后的应用配置 /// /// 结合了项目默认配置和用户配置 #[derive(Debug, Clone)] pub struct Config { /// Mimo-TTS API 密钥(来自用户配置) pub api_key: String, /// 默认音色(来自用户配置,覆盖项目默认值) pub default_voice: String, /// API 基础 URL 地址(来自项目配置) pub base_url: String, /// 默认音频格式(来自项目配置) #[allow(dead_code)] pub default_format: String, } impl Config { /// 从项目配置和用户配置创建合并配置 pub fn from_configs(project: ProjectConfig, user: UserConfig) -> Self { Self { api_key: user.api_key, default_voice: user.default_voice, base_url: project.base_url, default_format: project.default_format, } } } /// 返回默认的 API 基础 URL fn default_base_url() -> String { "https://api.xiaomimimo.com/v1/chat/completions".to_string() } /// 返回默认的语音音色 fn default_voice() -> String { "mimo_default".to_string() } /// 返回默认的音频格式 fn default_format() -> String { "wav".to_string() } /// 配置管理器(Singleton 模式) /// /// 负责加载项目配置和用户配置,并合并为应用配置 pub struct ConfigManager { /// 合并后的配置 config: Config, /// 用户配置文件路径 user_config_path: PathBuf, } impl ConfigManager { /// 创建配置管理器并加载配置 pub fn new() -> Result { // 1. 加载项目配置(从项目根目录的 project.config.toml) let project_config = Self::load_project_config()?; // 2. 加载用户配置(从 ~/.config/tts/config.toml) let (user_config, user_config_path) = Self::load_user_config()?; // 3. 合并配置 let config = Config::from_configs(project_config, user_config); Ok(Self { config, user_config_path, }) } /// 加载项目配置 fn load_project_config() -> Result { let project_config_path = std::env::current_dir() .context("无法获取当前目录")? .join("project.config.toml"); if project_config_path.exists() { let content = fs::read_to_string(&project_config_path) .with_context(|| format!("无法读取项目配置文件: {:?}", project_config_path))?; let config: ProjectConfig = toml::from_str(&content) .with_context(|| format!("无法解析项目配置文件: {:?}", project_config_path))?; Ok(config) } else { // 如果项目配置文件不存在,使用默认配置 Ok(ProjectConfig::default()) } } /// 加载用户配置 fn load_user_config() -> Result<(UserConfig, PathBuf)> { // 所有平台统一使用 ~/.config/tts/config.toml let home = home_dir().ok_or_else(|| anyhow::anyhow!("无法获取用户家目录"))?; let config_dir = home.join(".config").join("tts"); let config_path = config_dir.join("config.toml"); // 确保配置目录存在 fs::create_dir_all(&config_dir) .with_context(|| format!("无法创建配置目录: {:?}", config_dir))?; let config = if config_path.exists() { let content = fs::read_to_string(&config_path) .with_context(|| format!("无法读取用户配置文件: {:?}", config_path))?; let config: UserConfig = toml::from_str(&content) .with_context(|| format!("无法解析用户配置文件: {:?}", config_path))?; config } else { UserConfig::default() }; Ok((config, config_path)) } /// 获取合并后的配置引用 pub fn get_config(&self) -> &Config { &self.config } /// 更新 API Key pub fn set_api_key(&mut self, api_key: String) { self.config.api_key = api_key; } /// 更新默认音色 pub fn set_default_voice(&mut self, voice: String) { self.config.default_voice = voice; } /// 获取用户配置文件路径 pub fn get_config_path(&self) -> &PathBuf { &self.user_config_path } /// 保存用户配置到文件 /// /// 只保存用户配置项(api_key、default_voice) pub fn save(&self) -> Result<()> { let user_config = UserConfig { api_key: self.config.api_key.clone(), default_voice: self.config.default_voice.clone(), }; let content = toml::to_string_pretty(&user_config) .context("无法序列化用户配置")?; fs::write(&self.user_config_path, content) .with_context(|| format!("无法写入用户配置文件: {:?}", self.user_config_path))?; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_voice() { assert_eq!(default_voice(), "mimo_default"); } #[test] fn test_default_base_url() { assert_eq!(default_base_url(), "https://api.xiaomimimo.com/v1/chat/completions"); } }