334 lines
12 KiB
Python
334 lines
12 KiB
Python
"""
|
|||
|
|
配置管理模块
|
||
|
|
负责加载和管理系统的所有配置参数
|
||
|
|
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,
|
||
|
|
},
|
||
|
|
}
|