| """
|
| 统一配置管理系统
|
|
|
| 优先级规则:
|
| 1. 安全配置:仅环境变量(ADMIN_KEY, SESSION_SECRET_KEY)
|
| 2. 业务配置:YAML 配置文件 > 默认值
|
|
|
| 配置分类:
|
| - 安全配置:仅从环境变量读取,不可热更新(ADMIN_KEY, SESSION_SECRET_KEY)
|
| - 业务配置:仅从 YAML 读取,支持热更新(API_KEY, BASE_URL, PROXY, 重试策略等)
|
| """
|
|
|
| import os
|
| import yaml
|
| import secrets
|
| from pathlib import Path
|
| from typing import Optional, List
|
| from pydantic import BaseModel, Field, validator
|
| from dotenv import load_dotenv
|
|
|
| from core import storage
|
|
|
|
|
| load_dotenv()
|
|
|
| def _parse_bool(value, default: bool) -> bool:
|
| if isinstance(value, bool):
|
| return value
|
| if value is None:
|
| return default
|
| if isinstance(value, (int, float)):
|
| return value != 0
|
| if isinstance(value, str):
|
| lowered = value.strip().lower()
|
| if lowered in ("1", "true", "yes", "y", "on"):
|
| return True
|
| if lowered in ("0", "false", "no", "n", "off"):
|
| return False
|
| return default
|
|
|
|
|
|
|
|
|
| class BasicConfig(BaseModel):
|
| """基础配置"""
|
| api_key: str = Field(default="", description="API访问密钥(留空则公开访问)")
|
| base_url: str = Field(default="", description="服务器URL(留空则自动检测)")
|
| proxy: str = Field(default="", description="代理地址")
|
| duckmail_base_url: str = Field(default="https://api.duckmail.sbs", description="DuckMail API地址")
|
| duckmail_api_key: str = Field(default="", description="DuckMail API key")
|
| duckmail_verify_ssl: bool = Field(default=True, description="DuckMail SSL校验")
|
| browser_engine: str = Field(default="dp", description="浏览器引擎:uc 或 dp")
|
| browser_headless: bool = Field(default=False, description="自动化浏览器无头模式")
|
| refresh_window_hours: int = Field(default=1, ge=0, le=24, description="过期刷新窗口(小时)")
|
| register_default_count: int = Field(default=1, ge=1, le=30, description="默认注册数量")
|
| register_domain: str = Field(default="", description="默认注册域名(推荐)")
|
|
|
|
|
| class ImageGenerationConfig(BaseModel):
|
| """图片生成配置"""
|
| enabled: bool = Field(default=True, description="是否启用图片生成")
|
| supported_models: List[str] = Field(
|
| default=["gemini-3-pro-preview"],
|
| description="支持图片生成的模型列表"
|
| )
|
| output_format: str = Field(default="base64", description="图片输出格式:base64 或 url")
|
|
|
|
|
| class RetryConfig(BaseModel):
|
| """重试策略配置"""
|
| max_new_session_tries: int = Field(default=5, ge=1, le=20, description="新会话尝试账户数")
|
| max_request_retries: int = Field(default=3, ge=1, le=10, description="请求失败重试次数")
|
| max_account_switch_tries: int = Field(default=5, ge=1, le=20, description="账户切换尝试次数")
|
| account_failure_threshold: int = Field(default=3, ge=1, le=10, description="账户失败阈值")
|
| rate_limit_cooldown_seconds: int = Field(default=600, ge=60, le=3600, description="429冷却时间(秒)")
|
| session_cache_ttl_seconds: int = Field(default=3600, ge=300, le=86400, description="会话缓存时间(秒)")
|
| auto_refresh_accounts_seconds: int = Field(default=60, ge=0, le=600, description="自动刷新账号间隔(秒,0禁用)")
|
|
|
|
|
| class PublicDisplayConfig(BaseModel):
|
| """公开展示配置"""
|
| logo_url: str = Field(default="", description="Logo URL")
|
| chat_url: str = Field(default="", description="开始对话链接")
|
|
|
|
|
| class SessionConfig(BaseModel):
|
| """Session配置"""
|
| expire_hours: int = Field(default=24, ge=1, le=168, description="Session过期时间(小时)")
|
|
|
|
|
| class SecurityConfig(BaseModel):
|
| """安全配置(仅从环境变量读取,不可热更新)"""
|
| admin_key: str = Field(default="", description="管理员密钥(必需)")
|
| session_secret_key: str = Field(..., description="Session密钥")
|
|
|
|
|
| class AppConfig(BaseModel):
|
| """应用配置(统一管理)"""
|
|
|
| security: SecurityConfig
|
|
|
|
|
| basic: BasicConfig
|
| image_generation: ImageGenerationConfig
|
| retry: RetryConfig
|
| public_display: PublicDisplayConfig
|
| session: SessionConfig
|
|
|
|
|
|
|
|
|
| class ConfigManager:
|
| """配置管理器(单例)"""
|
|
|
| def __init__(self, yaml_path: str = None):
|
|
|
| if yaml_path is None:
|
| if os.path.exists("/data"):
|
| yaml_path = "/data/settings.yaml"
|
| else:
|
| yaml_path = "data/settings.yaml"
|
| self.yaml_path = Path(yaml_path)
|
| self._config: Optional[AppConfig] = None
|
| self.load()
|
|
|
| def load(self):
|
| """
|
| 加载配置
|
|
|
| 优先级规则:
|
| 1. 安全配置(ADMIN_KEY, SESSION_SECRET_KEY):仅从环境变量读取
|
| 2. 其他配置:YAML > 默认值
|
| """
|
|
|
| yaml_data = self._load_yaml()
|
|
|
|
|
| security_config = SecurityConfig(
|
| admin_key=os.getenv("ADMIN_KEY", ""),
|
| session_secret_key=os.getenv("SESSION_SECRET_KEY", self._generate_secret())
|
| )
|
|
|
|
|
| basic_data = yaml_data.get("basic", {})
|
| refresh_window_raw = basic_data.get("refresh_window_hours", 1)
|
| register_default_raw = basic_data.get("register_default_count", 1)
|
| register_domain_raw = basic_data.get("register_domain", "")
|
| duckmail_api_key_raw = basic_data.get("duckmail_api_key", "")
|
|
|
| basic_config = BasicConfig(
|
| api_key=basic_data.get("api_key") or "",
|
| base_url=basic_data.get("base_url") or "",
|
| proxy=basic_data.get("proxy") or "",
|
| duckmail_base_url=basic_data.get("duckmail_base_url") or "https://api.duckmail.sbs",
|
| duckmail_api_key=str(duckmail_api_key_raw or "").strip(),
|
| duckmail_verify_ssl=_parse_bool(basic_data.get("duckmail_verify_ssl"), True),
|
| browser_engine=basic_data.get("browser_engine") or "dp",
|
| browser_headless=_parse_bool(basic_data.get("browser_headless"), False),
|
| refresh_window_hours=int(refresh_window_raw),
|
| register_default_count=int(register_default_raw),
|
| register_domain=str(register_domain_raw or "").strip(),
|
| )
|
|
|
|
|
| image_generation_config = ImageGenerationConfig(
|
| **yaml_data.get("image_generation", {})
|
| )
|
|
|
| retry_config = RetryConfig(
|
| **yaml_data.get("retry", {})
|
| )
|
|
|
| public_display_config = PublicDisplayConfig(
|
| **yaml_data.get("public_display", {})
|
| )
|
|
|
| session_config = SessionConfig(
|
| **yaml_data.get("session", {})
|
| )
|
|
|
|
|
| self._config = AppConfig(
|
| security=security_config,
|
| basic=basic_config,
|
| image_generation=image_generation_config,
|
| retry=retry_config,
|
| public_display=public_display_config,
|
| session=session_config
|
| )
|
|
|
| def _load_yaml(self) -> dict:
|
| """加载 YAML 文件"""
|
| if storage.is_database_enabled():
|
| try:
|
| data = storage.load_settings_sync()
|
| if isinstance(data, dict):
|
| return data
|
| except Exception as e:
|
| print(f"[WARN] 加载数据库设置失败: {e},使用本地配置")
|
| if self.yaml_path.exists():
|
| try:
|
| with open(self.yaml_path, 'r', encoding='utf-8') as f:
|
| return yaml.safe_load(f) or {}
|
| except Exception as e:
|
| print(f"[WARN] 加载配置文件失败: {e},使用默认配置")
|
| return {}
|
|
|
| def _generate_secret(self) -> str:
|
| """生成随机密钥"""
|
| return secrets.token_urlsafe(32)
|
|
|
| def save_yaml(self, data: dict):
|
| """保存 YAML 配置"""
|
| if storage.is_database_enabled():
|
| try:
|
| saved = storage.save_settings_sync(data)
|
| if saved:
|
| return
|
| except Exception as e:
|
| print(f"[WARN] 保存数据库设置失败: {e},降级到本地文件")
|
| self.yaml_path.parent.mkdir(exist_ok=True)
|
| with open(self.yaml_path, 'w', encoding='utf-8') as f:
|
| yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
|
|
| def reload(self):
|
| """重新加载配置(热更新)"""
|
| self.load()
|
|
|
| @property
|
| def config(self) -> AppConfig:
|
| """获取配置"""
|
| return self._config
|
|
|
|
|
|
|
| @property
|
| def api_key(self) -> str:
|
| """API访问密钥"""
|
| return self._config.basic.api_key
|
|
|
| @property
|
| def admin_key(self) -> str:
|
| """管理员密钥"""
|
| return self._config.security.admin_key
|
|
|
| @property
|
| def session_secret_key(self) -> str:
|
| """Session密钥"""
|
| return self._config.security.session_secret_key
|
|
|
| @property
|
| def proxy(self) -> str:
|
| """代理地址"""
|
| return self._config.basic.proxy
|
|
|
| @property
|
| def base_url(self) -> str:
|
| """服务器URL"""
|
| return self._config.basic.base_url
|
|
|
| @property
|
| def logo_url(self) -> str:
|
| """Logo URL"""
|
| return self._config.public_display.logo_url
|
|
|
| @property
|
| def chat_url(self) -> str:
|
| """开始对话链接"""
|
| return self._config.public_display.chat_url
|
|
|
| @property
|
| def image_generation_enabled(self) -> bool:
|
| """是否启用图片生成"""
|
| return self._config.image_generation.enabled
|
|
|
| @property
|
| def image_generation_models(self) -> List[str]:
|
| """支持图片生成的模型列表"""
|
| return self._config.image_generation.supported_models
|
|
|
| @property
|
| def image_output_format(self) -> str:
|
| """图片输出格式"""
|
| return self._config.image_generation.output_format
|
|
|
| @property
|
| def session_expire_hours(self) -> int:
|
| """Session过期时间(小时)"""
|
| return self._config.session.expire_hours
|
|
|
| @property
|
| def max_new_session_tries(self) -> int:
|
| """新会话尝试账户数"""
|
| return self._config.retry.max_new_session_tries
|
|
|
| @property
|
| def max_request_retries(self) -> int:
|
| """请求失败重试次数"""
|
| return self._config.retry.max_request_retries
|
|
|
| @property
|
| def max_account_switch_tries(self) -> int:
|
| """账户切换尝试次数"""
|
| return self._config.retry.max_account_switch_tries
|
|
|
| @property
|
| def account_failure_threshold(self) -> int:
|
| """账户失败阈值"""
|
| return self._config.retry.account_failure_threshold
|
|
|
| @property
|
| def rate_limit_cooldown_seconds(self) -> int:
|
| """429冷却时间(秒)"""
|
| return self._config.retry.rate_limit_cooldown_seconds
|
|
|
| @property
|
| def session_cache_ttl_seconds(self) -> int:
|
| """会话缓存时间(秒)"""
|
| return self._config.retry.session_cache_ttl_seconds
|
|
|
| @property
|
| def auto_refresh_accounts_seconds(self) -> int:
|
| """自动刷新账号间隔(秒,0禁用)"""
|
| return self._config.retry.auto_refresh_accounts_seconds
|
|
|
|
|
|
|
|
|
| config_manager = ConfigManager()
|
|
|
|
|
|
|
| def get_config() -> AppConfig:
|
| """获取当前配置(支持热更新)"""
|
| return config_manager.config
|
|
|
|
|
| class _ConfigProxy:
|
| """配置代理,确保始终访问最新配置"""
|
| @property
|
| def basic(self):
|
| return config_manager.config.basic
|
|
|
| @property
|
| def security(self):
|
| return config_manager.config.security
|
|
|
| @property
|
| def image_generation(self):
|
| return config_manager.config.image_generation
|
|
|
| @property
|
| def retry(self):
|
| return config_manager.config.retry
|
|
|
| @property
|
| def public_display(self):
|
| return config_manager.config.public_display
|
|
|
| @property
|
| def session(self):
|
| return config_manager.config.session
|
|
|
| config = _ConfigProxy()
|
|
|