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>
This commit is contained in:
438
src/config/settings.py
Normal file
438
src/config/settings.py
Normal file
@@ -0,0 +1,438 @@
|
||||
"""
|
||||
配置管理模块
|
||||
|
||||
负责管理应用程序的所有配置,包括:
|
||||
- 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
|
||||
Reference in New Issue
Block a user