""" 配置管理模块 负责管理应用程序的所有配置,包括: - AI 配置(API keys, 模型选择, 提供商) - OCR 配置(本地/云端选择, API keys) - 云存储配置(类型, endpoint, 凭证) - 界面配置(主题, 快捷键) """ import os import yaml from pathlib import Path from typing import Optional, Dict, Any, List from dataclasses import dataclass, field, asdict from enum import Enum class ConfigError(Exception): """配置错误异常""" pass class AIProvider(str, Enum): """AI 提供商枚举""" OPENAI = "openai" ANTHROPIC = "anthropic" AZURE = "azure" CUSTOM = "custom" class OCRMode(str, Enum): """OCR 模式枚举""" LOCAL = "local" # 本地 PaddleOCR CLOUD = "cloud" # 云端 OCR API class CloudStorageType(str, Enum): """云存储类型枚举""" NONE = "none" # 不使用云存储 S3 = "s3" # AWS S3 OSS = "oss" # 阿里云 OSS COS = "cos" # 腾讯云 COS MINIO = "minio" # MinIO class Theme(str, Enum): """界面主题枚举""" LIGHT = "light" DARK = "dark" AUTO = "auto" @dataclass class AIConfig: """AI 配置""" provider: AIProvider = AIProvider.ANTHROPIC api_key: str = "" model: str = "claude-3-5-sonnet-20241022" temperature: float = 0.7 max_tokens: int = 4096 timeout: int = 60 base_url: str = "" # 用于自定义或 Azure extra_params: Dict[str, Any] = field(default_factory=dict) def validate(self) -> None: """验证 AI 配置""" if not self.api_key and self.provider != AIProvider.CUSTOM: raise ConfigError(f"AI API key 不能为空(提供商: {self.provider})") if self.temperature < 0 or self.temperature > 2: raise ConfigError("temperature 必须在 0-2 之间") if self.max_tokens < 1: raise ConfigError("max_tokens 必须大于 0") if self.timeout < 1: raise ConfigError("timeout 必须大于 0") @dataclass class OCRConfig: """OCR 配置""" mode: OCRMode = OCRMode.LOCAL api_key: str = "" # 云端 OCR API key api_endpoint: str = "" # 云端 OCR endpoint use_gpu: bool = False # 本地 OCR 是否使用 GPU lang: str = "ch" # 语言:ch(中文), en(英文), etc. timeout: int = 30 def validate(self) -> None: """验证 OCR 配置""" if self.mode == OCRMode.CLOUD and not self.api_endpoint: raise ConfigError("云端 OCR 模式需要指定 api_endpoint") @dataclass class CloudStorageConfig: """云存储配置""" type: CloudStorageType = CloudStorageType.NONE endpoint: str = "" access_key: str = "" secret_key: str = "" bucket: str = "" region: str = "" timeout: int = 30 def validate(self) -> None: """验证云存储配置""" if self.type == CloudStorageType.NONE: return if not self.endpoint: raise ConfigError(f"云存储 {self.type} 需要指定 endpoint") if not self.access_key or not self.secret_key: raise ConfigError(f"云存储 {self.type} 需要指定 access_key 和 secret_key") if not self.bucket: raise ConfigError(f"云存储 {self.type} 需要指定 bucket") @dataclass class Hotkey: """快捷键配置""" screenshot: str = "Ctrl+Shift+A" # 截图快捷键 ocr: str = "Ctrl+Shift+O" # OCR 识别快捷键 quick_capture: str = "Ctrl+Shift+X" # 快速捕获 show_hide: str = "Ctrl+Shift+H" # 显示/隐藏主窗口 def validate(self) -> None: """验证快捷键配置(简单格式检查)""" # 这里可以做更复杂的快捷键格式验证 pass @dataclass class UIConfig: """界面配置""" theme: Theme = Theme.AUTO language: str = "zh_CN" # 界面语言 window_width: int = 1200 window_height: int = 800 hotkeys: Hotkey = field(default_factory=Hotkey) show_tray_icon: bool = True minimize_to_tray: bool = True auto_start: bool = False def validate(self) -> None: """验证界面配置""" if self.window_width < 400: raise ConfigError("window_width 不能小于 400") if self.window_height < 300: raise ConfigError("window_height 不能小于 300") self.hotkeys.validate() @dataclass class AdvancedConfig: """高级配置""" debug_mode: bool = False log_level: str = "INFO" log_file: str = "" max_log_size: int = 10 # MB backup_count: int = 5 cache_dir: str = "" temp_dir: str = "" max_cache_size: int = 500 # MB def validate(self) -> None: """验证高级配置""" valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] if self.log_level.upper() not in valid_log_levels: raise ConfigError(f"log_level 必须是以下之一: {', '.join(valid_log_levels)}") if self.max_log_size < 1: raise ConfigError("max_log_size 不能小于 1") if self.backup_count < 0: raise ConfigError("backup_count 不能为负数") @dataclass class Settings: """主配置类""" ai: AIConfig = field(default_factory=AIConfig) ocr: OCRConfig = field(default_factory=OCRConfig) cloud_storage: CloudStorageConfig = field(default_factory=CloudStorageConfig) ui: UIConfig = field(default_factory=UIConfig) advanced: AdvancedConfig = field(default_factory=AdvancedConfig) def __post_init__(self): """初始化后处理,确保嵌套配置是正确的类型""" if isinstance(self.ai, dict): self.ai = AIConfig(**self.ai) if isinstance(self.ocr, dict): self.ocr = OCRConfig(**self.ocr) if isinstance(self.cloud_storage, dict): self.cloud_storage = CloudStorageConfig(**self.cloud_storage) if isinstance(self.ui, dict): self.ui = UIConfig(**self.ui) if isinstance(self.advanced, dict): self.advanced = AdvancedConfig(**self.advanced) elif isinstance(self.ui.hotkeys, dict): self.ui.hotkeys = Hotkey(**self.ui.hotkeys) def validate(self) -> None: """验证所有配置""" self.ai.validate() self.ocr.validate() self.cloud_storage.validate() self.ui.validate() self.advanced.validate() def to_dict(self) -> Dict[str, Any]: """转换为字典,将枚举类型转换为字符串值""" def enum_to_value(obj): """递归转换枚举为字符串值""" if isinstance(obj, Enum): return obj.value elif isinstance(obj, dict): return {k: enum_to_value(v) for k, v in obj.items()} elif isinstance(obj, list): return [enum_to_value(item) for item in obj] else: return obj return { 'ai': enum_to_value(asdict(self.ai)), 'ocr': enum_to_value(asdict(self.ocr)), 'cloud_storage': enum_to_value(asdict(self.cloud_storage)), 'ui': enum_to_value(asdict(self.ui)), 'advanced': enum_to_value(asdict(self.advanced)) } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Settings': """从字典创建配置""" return cls( ai=AIConfig(**data.get('ai', {})), ocr=OCRConfig(**data.get('ocr', {})), cloud_storage=CloudStorageConfig(**data.get('cloud_storage', {})), ui=UIConfig(**data.get('ui', {})), advanced=AdvancedConfig(**data.get('advanced', {})) ) class SettingsManager: """配置管理器""" DEFAULT_CONFIG_DIR = Path.home() / '.cutthenthink' DEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / 'config.yaml' def __init__(self, config_path: Optional[Path] = None): """ 初始化配置管理器 Args: config_path: 配置文件路径,默认为 ~/.cutthenthink/config.yaml """ self.config_path = Path(config_path) if config_path else self.DEFAULT_CONFIG_FILE self._settings: Optional[Settings] = None def load(self, validate: bool = False) -> Settings: """ 加载配置 Args: validate: 是否验证配置(默认 False,首次加载时可能缺少 API key) Returns: Settings: 配置对象 """ if not self.config_path.exists(): # 配置文件不存在,创建默认配置 self._settings = Settings() self.save(self._settings) return self._settings try: with open(self.config_path, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) or {} self._settings = Settings.from_dict(data) if validate: self._settings.validate() return self._settings except yaml.YAMLError as e: raise ConfigError(f"配置文件 YAML 格式错误: {e}") except Exception as e: raise ConfigError(f"加载配置失败: {e}") def save(self, settings: Optional[Settings] = None) -> None: """ 保存配置 Args: settings: 要保存的配置对象,为 None 时保存当前配置 """ if settings is None: settings = self._settings if settings is None: raise ConfigError("没有可保存的配置") try: # 确保配置目录存在 self.config_path.parent.mkdir(parents=True, exist_ok=True) # 转换为字典并保存 data = settings.to_dict() with open(self.config_path, 'w', encoding='utf-8') as f: yaml.safe_dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False) self._settings = settings except Exception as e: raise ConfigError(f"保存配置失败: {e}") def reset(self) -> Settings: """ 重置为默认配置 Returns: Settings: 默认配置对象 """ self._settings = Settings() self.save(self._settings) return self._settings @property def settings(self) -> Settings: """ 获取当前配置(懒加载) Returns: Settings: 配置对象 """ if self._settings is None: self._settings = self.load() return self._settings def get(self, key_path: str, default: Any = None) -> Any: """ 获取配置值(支持嵌套路径,如 'ai.provider') Args: key_path: 配置键路径,用点分隔 default: 默认值 Returns: 配置值 """ keys = key_path.split('.') value = self.settings for key in keys: if hasattr(value, key): value = getattr(value, key) else: return default return value def set(self, key_path: str, value: Any) -> None: """ 设置配置值(支持嵌套路径,如 'ai.provider') Args: key_path: 配置键路径,用点分隔 value: 要设置的值 """ keys = key_path.split('.') obj = self.settings # 导航到父对象 for key in keys[:-1]: if hasattr(obj, key): obj = getattr(obj, key) else: raise ConfigError(f"配置路径无效: {key_path}") # 设置最终值 last_key = keys[-1] if hasattr(obj, last_key): # 处理枚举类型 field_value = getattr(obj.__class__, last_key) if hasattr(field_value, 'type') and isinstance(field_value.type, type) and issubclass(field_value.type, Enum): # 如果是枚举类型,尝试转换 try: value = field_value.type(value) except ValueError: raise ConfigError(f"无效的枚举值: {value}") setattr(obj, last_key, value) else: raise ConfigError(f"配置键不存在: {last_key}") # 保存配置 self.save() # 全局配置管理器实例 _global_settings_manager: Optional[SettingsManager] = None def get_config(config_path: Optional[Path] = None) -> SettingsManager: """ 获取全局配置管理器(单例模式) Args: config_path: 配置文件路径,仅在首次调用时有效 Returns: SettingsManager: 配置管理器实例 """ global _global_settings_manager if _global_settings_manager is None: _global_settings_manager = SettingsManager(config_path) return _global_settings_manager def get_settings() -> Settings: """ 获取当前配置的快捷方法 Returns: Settings: 配置对象 """ return get_config().settings