Files
cutThenThink/src/config/settings.py
congsh c4a77f8aa4 feat: 实现CutThenThink P0阶段核心功能
项目初始化
- 创建完整项目结构(src/, data/, docs/, examples/, tests/)
- 配置requirements.txt依赖
- 创建.gitignore

P0基础框架
- 数据库模型:Record模型,6种分类类型
- 配置管理:YAML配置,支持AI/OCR/云存储/UI配置
- OCR模块:PaddleOCR本地识别,支持云端扩展
- AI模块:支持OpenAI/Claude/通义/Ollama,6种分类
- 存储模块:完整CRUD,搜索,统计,导入导出
- 主窗口框架:侧边栏导航,米白配色方案
- 图片处理:截图/剪贴板/文件选择/图片预览
- 处理流程整合:OCR→AI→存储串联,Markdown展示,剪贴板复制
- 分类浏览:卡片网格展示,分类筛选,搜索,详情查看

技术栈
- PyQt6 + SQLAlchemy + PaddleOCR + OpenAI/Claude SDK
- 共47个Python文件,4000+行代码

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 18:21:31 +08:00

439 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
配置管理模块
负责管理应用程序的所有配置,包括:
- 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