Add File
This commit is contained in:
461
src/landppt/services/config_service.py
Normal file
461
src/landppt/services/config_service.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Configuration management service for LandPPT
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv, set_key, unset_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""Configuration management service"""
|
||||
|
||||
def __init__(self, env_file: str = ".env"):
|
||||
self.env_file = env_file
|
||||
self.env_path = Path(env_file)
|
||||
|
||||
# Ensure .env file exists
|
||||
if not self.env_path.exists():
|
||||
self.env_path.touch()
|
||||
|
||||
# Load environment variables with error handling
|
||||
try:
|
||||
load_dotenv(self.env_file)
|
||||
except (PermissionError, FileNotFoundError) as e:
|
||||
logger.warning(f"Could not load .env file {self.env_file}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error loading .env file {self.env_file}: {e}")
|
||||
|
||||
# Configuration schema
|
||||
self.config_schema = {
|
||||
# AI Provider Configuration
|
||||
"openai_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"openai_base_url": {"type": "url", "category": "ai_providers", "default": "https://api.openai.com/v1"},
|
||||
"openai_model": {"type": "select", "category": "ai_providers", "default": "gpt-4.1"},
|
||||
|
||||
"anthropic_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"anthropic_model": {"type": "select", "category": "ai_providers", "default": "claude-3.5-haiku-20240307"},
|
||||
|
||||
"google_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"google_base_url": {"type": "url", "category": "ai_providers", "default": "https://generativelanguage.googleapis.com"},
|
||||
"google_model": {"type": "text", "category": "ai_providers", "default": "gemini-2.5-flash"},
|
||||
|
||||
"azure_openai_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"azure_openai_endpoint": {"type": "url", "category": "ai_providers"},
|
||||
"azure_openai_deployment_name": {"type": "text", "category": "ai_providers"},
|
||||
"azure_openai_api_version": {"type": "text", "category": "ai_providers", "default": "gpt-4.1"},
|
||||
|
||||
"ollama_base_url": {"type": "url", "category": "ai_providers", "default": "http://localhost:11434"},
|
||||
"ollama_model": {"type": "text", "category": "ai_providers", "default": "llama2"},
|
||||
|
||||
# 302.AI Configuration
|
||||
"302ai_api_key": {"type": "password", "category": "ai_providers"},
|
||||
"302ai_base_url": {"type": "url", "category": "ai_providers", "default": "https://api.302.ai/v1"},
|
||||
"302ai_model": {"type": "text", "category": "ai_providers", "default": "gpt-4o"},
|
||||
|
||||
"default_ai_provider": {"type": "select", "category": "ai_providers", "default": "openai"},
|
||||
|
||||
# Model Role Overrides
|
||||
"default_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"default_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"outline_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"outline_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"creative_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"creative_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"image_prompt_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"image_prompt_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"slide_generation_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"slide_generation_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"editor_assistant_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"editor_assistant_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"template_generation_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"template_generation_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
"speech_script_model_provider": {"type": "select", "category": "model_roles", "default": ""},
|
||||
"speech_script_model_name": {"type": "text", "category": "model_roles", "default": ""},
|
||||
|
||||
# Generation Parameters
|
||||
"max_tokens": {"type": "number", "category": "generation_params", "default": "16384"},
|
||||
"temperature": {"type": "number", "category": "generation_params", "default": "0.7"},
|
||||
"top_p": {"type": "number", "category": "generation_params", "default": "1.0"},
|
||||
|
||||
# Parallel Generation Configuration
|
||||
"enable_parallel_generation": {"type": "boolean", "category": "generation_params", "default": "false"},
|
||||
"parallel_slides_count": {"type": "number", "category": "generation_params", "default": "3"},
|
||||
|
||||
"tavily_api_key": {"type": "password", "category": "generation_params"},
|
||||
"tavily_max_results": {"type": "number", "category": "generation_params", "default": "10"},
|
||||
"tavily_search_depth": {"type": "select", "category": "generation_params", "default": "advanced"},
|
||||
|
||||
# SearXNG Configuration
|
||||
"searxng_host": {"type": "url", "category": "generation_params"},
|
||||
"searxng_max_results": {"type": "number", "category": "generation_params", "default": "10"},
|
||||
"searxng_language": {"type": "text", "category": "generation_params", "default": "auto"},
|
||||
"searxng_timeout": {"type": "number", "category": "generation_params", "default": "30"},
|
||||
|
||||
# Research Configuration
|
||||
"research_provider": {"type": "select", "category": "generation_params", "default": "tavily"},
|
||||
"research_enable_content_extraction": {"type": "boolean", "category": "generation_params", "default": "true"},
|
||||
"research_max_content_length": {"type": "number", "category": "generation_params", "default": "5000"},
|
||||
"research_extraction_timeout": {"type": "number", "category": "generation_params", "default": "30"},
|
||||
|
||||
"apryse_license_key": {"type": "password", "category": "generation_params"},
|
||||
|
||||
# Feature Flags
|
||||
"enable_network_mode": {"type": "boolean", "category": "feature_flags", "default": "true"},
|
||||
"enable_local_models": {"type": "boolean", "category": "feature_flags", "default": "false"},
|
||||
"enable_streaming": {"type": "boolean", "category": "feature_flags", "default": "true"},
|
||||
"log_level": {"type": "select", "category": "feature_flags", "default": "INFO"},
|
||||
"log_ai_requests": {"type": "boolean", "category": "feature_flags", "default": "false"},
|
||||
"debug": {"type": "boolean", "category": "feature_flags", "default": "true"},
|
||||
|
||||
# App Configuration
|
||||
"host": {"type": "text", "category": "app_config", "default": "0.0.0.0"},
|
||||
"port": {"type": "number", "category": "app_config", "default": "8000"},
|
||||
"base_url": {"type": "url", "category": "app_config", "default": "http://localhost:8000"},
|
||||
"reload": {"type": "boolean", "category": "app_config", "default": "true"},
|
||||
"secret_key": {"type": "password", "category": "app_config", "default": "your-very-secure-secret-key"},
|
||||
"access_token_expire_minutes": {"type": "number", "category": "app_config", "default": "30"},
|
||||
"max_file_size": {"type": "number", "category": "app_config", "default": "10485760"},
|
||||
"upload_dir": {"type": "text", "category": "app_config", "default": "uploads"},
|
||||
"cache_ttl": {"type": "number", "category": "app_config", "default": "3600"},
|
||||
"database_url": {"type": "text", "category": "app_config", "default": "sqlite:///./landppt.db"},
|
||||
|
||||
# Image Service Configuration
|
||||
"enable_image_service": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
|
||||
# Multi-source Image Configuration
|
||||
"enable_local_images": {"type": "boolean", "category": "image_service", "default": "true"},
|
||||
"enable_network_search": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
"enable_ai_generation": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
|
||||
# Local Images Configuration
|
||||
"local_images_smart_selection": {"type": "boolean", "category": "image_service", "default": "true"},
|
||||
"max_local_images_per_slide": {"type": "number", "category": "image_service", "default": "2"},
|
||||
|
||||
# Network Search Configuration
|
||||
"default_network_search_provider": {"type": "select", "category": "image_service", "default": "unsplash"},
|
||||
"max_network_images_per_slide": {"type": "number", "category": "image_service", "default": "2"},
|
||||
|
||||
# AI Generation Configuration
|
||||
"default_ai_image_provider": {"type": "select", "category": "image_service", "default": "dalle"},
|
||||
"max_ai_images_per_slide": {"type": "number", "category": "image_service", "default": "1"},
|
||||
"ai_image_quality": {"type": "select", "category": "image_service", "default": "standard"},
|
||||
|
||||
# Global Image Configuration
|
||||
"max_total_images_per_slide": {"type": "number", "category": "image_service", "default": "3"},
|
||||
"enable_smart_image_selection": {"type": "boolean", "category": "image_service", "default": "true"},
|
||||
|
||||
# Image Generation Providers
|
||||
"openai_api_key_image": {"type": "password", "category": "image_service"},
|
||||
"stability_api_key": {"type": "password", "category": "image_service"},
|
||||
"siliconflow_api_key": {"type": "password", "category": "image_service"},
|
||||
"default_ai_image_provider": {"type": "select", "category": "image_service", "default": "dalle"},
|
||||
|
||||
# Pollinations Configuration
|
||||
"pollinations_api_token": {"type": "password", "category": "image_service"},
|
||||
"pollinations_referrer": {"type": "text", "category": "image_service"},
|
||||
"pollinations_model": {"type": "select", "category": "image_service", "default": "flux"},
|
||||
"pollinations_enhance": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
"pollinations_safe": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
"pollinations_nologo": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
"pollinations_private": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
"pollinations_transparent": {"type": "boolean", "category": "image_service", "default": "false"},
|
||||
|
||||
# Image Search Providers
|
||||
"unsplash_access_key": {"type": "password", "category": "image_service"},
|
||||
"pixabay_api_key": {"type": "password", "category": "image_service"},
|
||||
"searxng_host": {"type": "url", "category": "image_service"},
|
||||
"dalle_image_size": {"type": "select", "category": "image_service", "default": "1792x1024"},
|
||||
"dalle_image_quality": {"type": "select", "category": "image_service", "default": "standard"},
|
||||
"dalle_image_style": {"type": "select", "category": "image_service", "default": "natural"},
|
||||
"siliconflow_image_size": {"type": "select", "category": "image_service", "default": "1024x1024"},
|
||||
"siliconflow_steps": {"type": "number", "category": "image_service", "default": 20},
|
||||
"siliconflow_guidance_scale": {"type": "number", "category": "image_service", "default": 7.5},
|
||||
}
|
||||
|
||||
|
||||
|
||||
def get_all_config(self) -> Dict[str, Any]:
|
||||
"""Get all configuration values"""
|
||||
config = {}
|
||||
|
||||
for key, schema in self.config_schema.items():
|
||||
env_key = key.upper()
|
||||
value = os.getenv(env_key)
|
||||
|
||||
if value is None:
|
||||
value = schema.get("default", "")
|
||||
|
||||
# Convert boolean strings
|
||||
if schema["type"] == "boolean":
|
||||
if isinstance(value, str):
|
||||
value = value.lower() in ("true", "1", "yes", "on")
|
||||
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
def get_config_by_category(self, category: str) -> Dict[str, Any]:
|
||||
"""Get configuration values by category"""
|
||||
config = {}
|
||||
|
||||
for key, schema in self.config_schema.items():
|
||||
if schema["category"] == category:
|
||||
env_key = key.upper()
|
||||
value = os.getenv(env_key)
|
||||
|
||||
if value is None:
|
||||
value = schema.get("default", "")
|
||||
|
||||
# Convert boolean strings
|
||||
if schema["type"] == "boolean":
|
||||
if isinstance(value, str):
|
||||
value = value.lower() in ("true", "1", "yes", "on")
|
||||
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
|
||||
def update_config(self, config: Dict[str, Any]) -> bool:
|
||||
"""Update configuration values"""
|
||||
try:
|
||||
for key, value in config.items():
|
||||
if key in self.config_schema:
|
||||
env_key = key.upper()
|
||||
|
||||
# Convert boolean values to strings
|
||||
if isinstance(value, bool):
|
||||
value = "true" if value else "false"
|
||||
else:
|
||||
value = str(value)
|
||||
|
||||
# Update .env file (without quotes)
|
||||
set_key(self.env_file, env_key, value, quote_mode="never")
|
||||
|
||||
# Update current environment
|
||||
os.environ[env_key] = value
|
||||
|
||||
# Reload environment variables with error handling
|
||||
try:
|
||||
load_dotenv(self.env_file, override=True)
|
||||
except (PermissionError, FileNotFoundError) as e:
|
||||
logger.warning(f"Could not reload .env file {self.env_file}: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reloading .env file {self.env_file}: {e}")
|
||||
|
||||
# Reload AI configuration if any AI-related config was updated
|
||||
ai_related_keys = [k for k in config.keys() if k in self.config_schema and
|
||||
self.config_schema[k]["category"] in ["ai_providers", "generation_params", "model_roles"]]
|
||||
if ai_related_keys:
|
||||
self._reload_ai_config()
|
||||
|
||||
# Reload app configuration if any app-related config was updated
|
||||
app_related_keys = [k for k in config.keys() if k in self.config_schema and
|
||||
self.config_schema[k]["category"] == "app_config"]
|
||||
if app_related_keys:
|
||||
self._reload_app_config()
|
||||
|
||||
# Reload image service configuration if any image-related config was updated
|
||||
image_related_keys = [k for k in config.keys() if k in self.config_schema and
|
||||
self.config_schema[k]["category"] == "image_service"]
|
||||
if image_related_keys:
|
||||
self._reload_image_config()
|
||||
|
||||
logger.info(f"Updated {len(config)} configuration values")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update configuration: {e}")
|
||||
return False
|
||||
|
||||
def _reload_ai_config(self):
|
||||
"""Reload AI configuration"""
|
||||
try:
|
||||
from ..core.config import reload_ai_config, ai_config
|
||||
from ..ai.providers import reload_ai_providers
|
||||
from .service_instances import reload_services
|
||||
|
||||
logger.info("Starting AI configuration reload process...")
|
||||
|
||||
# Reload AI configuration
|
||||
reload_ai_config()
|
||||
logger.info(f"AI config reloaded. Tavily API key: {'***' + ai_config.tavily_api_key[-4:] if ai_config.tavily_api_key and len(ai_config.tavily_api_key) > 4 else 'None'}")
|
||||
|
||||
# Clear AI provider cache to force reload with new config
|
||||
reload_ai_providers()
|
||||
logger.info("AI providers reloaded")
|
||||
|
||||
# Reload service instances to pick up new configuration
|
||||
reload_services()
|
||||
logger.info("Service instances reloaded")
|
||||
|
||||
logger.info("AI configuration, providers, and services reloaded successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reload AI configuration: {e}")
|
||||
import traceback
|
||||
logger.error(f"Reload traceback: {traceback.format_exc()}")
|
||||
|
||||
def _reload_app_config(self):
|
||||
"""Reload application configuration"""
|
||||
try:
|
||||
from ..core.config import app_config
|
||||
|
||||
# Force reload of app configuration
|
||||
app_config.__init__()
|
||||
|
||||
logger.info("Application configuration reloaded successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reload application configuration: {e}")
|
||||
|
||||
def _reload_image_config(self):
|
||||
"""Reload image service configuration"""
|
||||
try:
|
||||
from ..services.image.config.image_config import image_config
|
||||
|
||||
# 重新加载环境变量配置
|
||||
image_config._load_env_config()
|
||||
|
||||
# 同时更新Pollinations特定配置
|
||||
current_config = self.get_all_config()
|
||||
pollinations_updates = {}
|
||||
|
||||
# 映射配置项到Pollinations配置
|
||||
if 'pollinations_api_token' in current_config:
|
||||
pollinations_updates['api_token'] = current_config['pollinations_api_token']
|
||||
if 'pollinations_referrer' in current_config:
|
||||
pollinations_updates['referrer'] = current_config['pollinations_referrer']
|
||||
if 'pollinations_model' in current_config:
|
||||
pollinations_updates['model'] = current_config['pollinations_model']
|
||||
if 'pollinations_enhance' in current_config:
|
||||
value = current_config['pollinations_enhance']
|
||||
pollinations_updates['default_enhance'] = value if isinstance(value, bool) else str(value).lower() == 'true'
|
||||
if 'pollinations_safe' in current_config:
|
||||
value = current_config['pollinations_safe']
|
||||
pollinations_updates['default_safe'] = value if isinstance(value, bool) else str(value).lower() == 'true'
|
||||
if 'pollinations_nologo' in current_config:
|
||||
value = current_config['pollinations_nologo']
|
||||
pollinations_updates['default_nologo'] = value if isinstance(value, bool) else str(value).lower() == 'true'
|
||||
if 'pollinations_private' in current_config:
|
||||
value = current_config['pollinations_private']
|
||||
pollinations_updates['default_private'] = value if isinstance(value, bool) else str(value).lower() == 'true'
|
||||
if 'pollinations_transparent' in current_config:
|
||||
value = current_config['pollinations_transparent']
|
||||
pollinations_updates['default_transparent'] = value if isinstance(value, bool) else str(value).lower() == 'true'
|
||||
|
||||
# 如果有Pollinations配置更新,应用它们
|
||||
if pollinations_updates:
|
||||
image_config.update_config({'pollinations': pollinations_updates})
|
||||
|
||||
logger.info("Image service configuration reloaded")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reload image service configuration: {e}")
|
||||
|
||||
def update_config_by_category(self, category: str, config: Dict[str, Any]) -> bool:
|
||||
"""Update configuration values for a specific category"""
|
||||
# Filter config to only include keys from the specified category
|
||||
filtered_config = {}
|
||||
|
||||
for key, value in config.items():
|
||||
if key in self.config_schema and self.config_schema[key]["category"] == category:
|
||||
filtered_config[key] = value
|
||||
|
||||
return self.update_config(filtered_config)
|
||||
|
||||
def get_config_schema(self) -> Dict[str, Any]:
|
||||
"""Get configuration schema"""
|
||||
return self.config_schema
|
||||
|
||||
def validate_config(self, config: Dict[str, Any]) -> Dict[str, List[str]]:
|
||||
"""Validate configuration values"""
|
||||
errors = {}
|
||||
|
||||
for key, value in config.items():
|
||||
if key not in self.config_schema:
|
||||
if "unknown" not in errors:
|
||||
errors["unknown"] = []
|
||||
errors["unknown"].append(f"Unknown configuration key: {key}")
|
||||
continue
|
||||
|
||||
schema = self.config_schema[key]
|
||||
field_errors = []
|
||||
|
||||
# Type validation
|
||||
if schema["type"] == "number":
|
||||
try:
|
||||
num_value = float(value)
|
||||
# Special validation for access_token_expire_minutes - allow 0 for never expire
|
||||
if key == "access_token_expire_minutes" and num_value < 0:
|
||||
field_errors.append(f"{key} must be 0 (never expire) or a positive number")
|
||||
except (ValueError, TypeError):
|
||||
field_errors.append(f"{key} must be a number")
|
||||
|
||||
elif schema["type"] == "boolean":
|
||||
if isinstance(value, str):
|
||||
if value.lower() not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
||||
field_errors.append(f"{key} must be a boolean value")
|
||||
|
||||
elif schema["type"] == "url":
|
||||
if value and not (value.startswith("http://") or value.startswith("https://")):
|
||||
field_errors.append(f"{key} must be a valid URL")
|
||||
|
||||
if field_errors:
|
||||
errors[key] = field_errors
|
||||
|
||||
return errors
|
||||
|
||||
def reset_to_defaults(self, category: Optional[str] = None) -> bool:
|
||||
"""Reset configuration to default values"""
|
||||
try:
|
||||
config_to_reset = {}
|
||||
|
||||
for key, schema in self.config_schema.items():
|
||||
if category is None or schema["category"] == category:
|
||||
default_value = schema.get("default", "")
|
||||
config_to_reset[key] = default_value
|
||||
|
||||
return self.update_config(config_to_reset)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reset configuration: {e}")
|
||||
return False
|
||||
|
||||
def backup_config(self, backup_file: str) -> bool:
|
||||
"""Backup current configuration"""
|
||||
try:
|
||||
config = self.get_all_config()
|
||||
|
||||
with open(backup_file, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
logger.info(f"Configuration backed up to {backup_file}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to backup configuration: {e}")
|
||||
return False
|
||||
|
||||
def restore_config(self, backup_file: str) -> bool:
|
||||
"""Restore configuration from backup"""
|
||||
try:
|
||||
with open(backup_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
return self.update_config(config)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to restore configuration: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Global config service instance
|
||||
config_service = ConfigService()
|
||||
|
||||
|
||||
def get_config_service() -> ConfigService:
|
||||
"""Get config service instance"""
|
||||
return config_service
|
||||
Reference in New Issue
Block a user