""" 配置管理模块 负责加载和管理系统的所有配置参数 Enhanced with Pydantic validation and comprehensive error handling """ import json import logging import os from dataclasses import dataclass, field from pathlib import Path from typing import Dict, Any, Optional, List from .dependency_manager import get_dependency_manager # Import the new validation framework from .config_validation import ( SystemConfig, ConfigurationValidator, ObsidianConfig as PydanticObsidianConfig, ClaudeAPIConfig as PydanticClaudeAPIConfig, JournalConfig as PydanticJournalConfig, OutputConfig as PydanticOutputConfig, AnalysisConfig as PydanticAnalysisConfig, LoggingConfig as PydanticLoggingConfig ) # Try to import yaml with graceful degradation dependency_manager = get_dependency_manager() yaml = dependency_manager.get_module('yaml') @dataclass class ObsidianConfig: """Obsidian 配置""" vault_path: str rest_api_url: str rest_api_key: str verify_ssl: bool = False def __post_init__(self) -> None: """Validate configuration after initialization""" if not self.vault_path or not self.vault_path.strip(): raise ValueError("vault_path cannot be empty") if not self.rest_api_key or not self.rest_api_key.strip(): raise ValueError("rest_api_key cannot be empty") if not self.rest_api_url or not self.rest_api_url.strip(): raise ValueError("rest_api_url cannot be empty") if not self.rest_api_url.startswith(("http://", "https://")): raise ValueError("rest_api_url must start with http:// or https://") @dataclass class ClaudeConfig: """Claude API 配置""" api_key: str model: str = "claude-3-5-sonnet-20241022" api_url: str = "https://api.anthropic.com" max_tokens: int = 4096 temperature: float = 0.7 def __post_init__(self) -> None: """Validate configuration after initialization""" if not self.api_key or not self.api_key.strip(): raise ValueError("api_key cannot be empty") if self.max_tokens <= 0: raise ValueError("max_tokens must be positive") if not 0.0 <= self.temperature <= 2.0: raise ValueError("temperature must be between 0.0 and 2.0") if not self.api_url or not self.api_url.strip(): raise ValueError("api_url cannot be empty") if not self.api_url.startswith(("http://", "https://")): raise ValueError("api_url must start with http:// or https://") @dataclass class JournalConfig: """日记配置""" daily_notes_folder: str date_format: str = "YYYY-MM-DD" file_extension: str = ".md" def __post_init__(self) -> None: """Validate configuration after initialization""" if not self.daily_notes_folder or not self.daily_notes_folder.strip(): raise ValueError("daily_notes_folder cannot be empty") if not self.file_extension.startswith("."): raise ValueError("file_extension must start with a dot") @dataclass class OutputConfig: """输出配置""" experiences_folder: str lessons_folder: str tasks_folder: str problems_folder: str achievements_folder: str improvements_folder: str def __post_init__(self) -> None: """Validate configuration after initialization""" folders = [ self.experiences_folder, self.lessons_folder, self.tasks_folder, self.problems_folder, self.achievements_folder, self.improvements_folder, ] for folder in folders: if not folder or not folder.strip(): raise ValueError("All folder paths must be non-empty") @dataclass class AnalysisConfig: """分析配置""" categories: List[str] = field(default_factory=list) extraction_rules: Dict[str, Dict[str, Any]] = field(default_factory=dict) def __post_init__(self) -> None: """Validate configuration after initialization""" if not isinstance(self.categories, list): raise ValueError("categories must be a list") if not isinstance(self.extraction_rules, dict): raise ValueError("extraction_rules must be a dictionary") @dataclass class LoggingConfig: """日志配置""" level: str = "INFO" file: str = "logs/journal_organizer.log" def __post_init__(self) -> None: """Validate configuration after initialization""" valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] if self.level not in valid_levels: raise ValueError(f"level must be one of {valid_levels}") if not self.file or not self.file.strip(): raise ValueError("file path cannot be empty") class Config: """系统配置管理器 - Enhanced with Pydantic validation""" def __init__(self, config_file: Optional[str] = None): """ 初始化配置 Args: config_file: 配置文件路径,如果为 None 则使用默认位置 """ self.config_file = config_file or self._get_default_config_path() self.validator = ConfigurationValidator() self.logger = logging.getLogger(__name__) # Load and validate configuration using Pydantic try: self.system_config = self.validator.load_and_validate_config(self.config_file) except (FileNotFoundError, ValueError) as e: self.logger.error(f"Configuration error: {e}") raise # Create legacy-compatible attributes self._create_legacy_attributes() # Perform additional validations self._perform_additional_validations() def _create_legacy_attributes(self) -> None: """Create legacy-compatible attributes from Pydantic models""" # Convert Pydantic models to legacy dataclass format for backward compatibility self.obsidian = ObsidianConfig( vault_path=self.system_config.obsidian.vault_path, rest_api_url=self.system_config.obsidian.rest_api.url, rest_api_key=self.system_config.obsidian.rest_api.api_key, verify_ssl=self.system_config.obsidian.rest_api.verify_ssl ) self.claude = ClaudeConfig( api_key=self.system_config.claude.api_key, model=self.system_config.claude.model, api_url=self.system_config.claude.api_url, max_tokens=self.system_config.claude.max_tokens, temperature=self.system_config.claude.temperature ) self.journal = JournalConfig( daily_notes_folder=self.system_config.journal.daily_notes_folder, date_format=self.system_config.journal.date_format, file_extension=self.system_config.journal.file_extension ) self.output = OutputConfig( experiences_folder=self.system_config.output.experiences_folder, lessons_folder=self.system_config.output.lessons_folder, tasks_folder=self.system_config.output.tasks_folder, problems_folder=self.system_config.output.problems_folder, achievements_folder=self.system_config.output.achievements_folder, improvements_folder=self.system_config.output.improvements_folder ) self.analysis = AnalysisConfig( categories=self.system_config.analysis.categories, extraction_rules=self.system_config.analysis.extraction_rules ) self.logging = LoggingConfig( level=self.system_config.logging.level, file=self.system_config.logging.file ) def _perform_additional_validations(self) -> None: """Perform additional validations and provide helpful guidance""" # Validate API keys and provide setup instructions api_key_issues = self.validator.validate_api_keys(self.system_config) if api_key_issues: error_msg = "API Key Configuration Issues:\n" + "\n\n".join(api_key_issues) self.logger.error(error_msg) raise ValueError(error_msg) # Validate file paths path_issues = self.validator.validate_file_paths(self.system_config) if path_issues: error_msg = "File Path Issues:\n" + "\n".join(path_issues) self.logger.warning(error_msg) # Don't raise error for path issues, just warn def get_environment_variable_help(self) -> str: """Get help for setting up environment variables""" return self.validator.get_environment_variable_help() def get_path_setup_help(self) -> str: """Get help for setting up file paths""" return self.validator.get_path_setup_help() def _get_default_config_path(self) -> str: """获取默认配置文件路径""" # First check current directory current_dir_config = Path("config.yaml") if current_dir_config.exists(): return str(current_dir_config) # Then check user home directory config_dir = Path.home() / ".journal_organizer" config_dir.mkdir(exist_ok=True) return str(config_dir / "config.yaml") def get_validation_errors(self) -> List[str]: """Get any validation errors or warnings""" errors = [] try: # Re-validate to get current status api_key_issues = self.validator.validate_api_keys(self.system_config) errors.extend(api_key_issues) path_issues = self.validator.validate_file_paths(self.system_config) errors.extend(path_issues) except Exception as e: errors.append(f"Validation error: {e}") return errors def reload_config(self) -> None: """Reload configuration from file""" try: self.system_config = self.validator.load_and_validate_config(self.config_file) self._create_legacy_attributes() self._perform_additional_validations() self.logger.info("Configuration reloaded successfully") except Exception as e: self.logger.error(f"Failed to reload configuration: {e}") raise def get_daily_note_path(self, date_str: str) -> str: """ 获取指定日期的日记文件路径 Args: date_str: 日期字符串,格式应与配置中的 date_format 一致 Returns: 日记文件的相对路径 """ daily_path = ( Path(self.journal.daily_notes_folder) / f"{date_str}{self.journal.file_extension}" ) return str(daily_path) def to_dict(self) -> Dict[str, Any]: """转换为字典(不包含敏感信息)""" return { "obsidian": { "vault_path": self.obsidian.vault_path, "rest_api_url": self.obsidian.rest_api_url, }, "claude": { "model": self.claude.model, "api_url": self.claude.api_url, "max_tokens": self.claude.max_tokens, "temperature": self.claude.temperature, }, "journal": { "daily_notes_folder": self.journal.daily_notes_folder, "date_format": self.journal.date_format, "file_extension": self.journal.file_extension, }, "output": { "experiences_folder": self.output.experiences_folder, "lessons_folder": self.output.lessons_folder, "tasks_folder": self.output.tasks_folder, "problems_folder": self.output.problems_folder, "achievements_folder": self.output.achievements_folder, "improvements_folder": self.output.improvements_folder, }, "analysis": { "categories": self.analysis.categories, }, "logging": { "level": self.logging.level, "file": self.logging.file, }, }