"""应用配置:通过环境变量和 .env 文件加载所有配置项 配置项统一使用 SURUGAYA_ 前缀,例如 SURUGAYA_APP_PORT=31106。 支持 .env 文件自动加载。 """ from functools import lru_cache from typing import Literal from pydantic_settings import BaseSettings, SettingsConfigDict from pathlib import Path from surugaya_common import browser_profile, urls BASE_DIR = Path(__file__).resolve().parent.parent.parent class Settings(BaseSettings): """应用全局配置 所有配置项均可通过环境变量覆盖,前缀为 SURUGAYA_。 例如:SURUGAYA_APP_PORT=31106 对应 app_port 配置项。 """ model_config = SettingsConfigDict( env_file=str(BASE_DIR / ".env"), # 使用绝对路径 env_file_encoding="utf-8", env_prefix="SURUGAYA_", extra="ignore", ) # ---- 服务基本配置 ---- app_name: str = "Surugaya Scraper Service" app_env: Literal["dev", "prod", "test"] = "dev" app_host: str = "0.0.0.0" app_port: int = 31106 # ---- 日志配置 ---- log_level: str = "DEBUG" log_to_file: bool | None = None # None 表示根据环境自动决定 log_dir: str = "logs" log_rotation: str = "10 MB" log_retention: str = "14 days" log_compression: str = "zip" log_format: str = "{time:YYYY-MM-DD HH:mm:ss} {level} {message}" log_enqueue: bool = True # ---- 鉴权配置 ---- bearer_token: str = "NilsO8Il8yoT1wazJFs2eeOmlPRfWuSx" # ---- 浏览器配置 ---- browser_enabled: bool = True browser_headless: bool | None = None # None 表示根据环境自动决定 browser_keep_open: bool | None = None # None 表示根据环境自动决定 browser_keep_open_on_error: bool | None = None # None 表示根据环境自动决定 browser_channel: str | None = "chrome" browser_pool_size: int = 3 max_site_concurrency: int = 5 # 最大并发页面数 browser_recycle_after_contexts: int = 200 # 浏览器累计创建上下文达到该值后主动回收,0 关闭 browser_recycle_after_seconds: float = 3600.0 # 浏览器运行超过该秒数后主动回收,0 关闭 page_acquire_timeout_seconds: float = 60.0 # 获取页面槽位超时 request_timeout_seconds: float = 120.0 # 页面请求超时 cloudflare_wait_timeout_seconds: float = 60.0 # 等待 Cloudflare 挑战超时 cloudflare_reuse_wait_timeout_seconds: float = 8.0 # 复用会话时消化挑战的短超时,超时则放弃复用直接重建 session_ttl_seconds: int = 900 # 会话过期时间(秒) # 指纹参数默认值取自共享包,与 worker 保持一致 browser_locale: str = browser_profile.LOCALE browser_timezone_id: str = browser_profile.TIMEZONE_ID browser_user_agent: str = browser_profile.DEFAULT_USER_AGENT browser_stealth_enabled: bool = True # 是否启用浏览器 Stealth 模式 # ---- Redis 配置(可选,未配置时使用进程内存)---- redis_host: str = "" redis_port: int = 6379 redis_password: str | None = None redis_db: int = 0 session_namespace: str = "jp_surugaya" # ---- 代理配置(可选,用于日本 IP)---- proxy_server: str | None = None proxy_username: str | None = None proxy_password: str | None = None # ---- 目标站点 ---- base_url: str = urls.BASE_URL @property def browser_headless_effective(self) -> bool: """浏览器无头模式:显式配置优先,否则开发环境使用有头模式方便调试""" if self.browser_headless is not None: return self.browser_headless if self.app_env == "dev": return False return True @property def browser_keep_open_on_error_effective(self) -> bool: """出错时是否保留浏览器窗口:仅在开发环境有头模式下保留""" if self.browser_keep_open_on_error is not None: return self.browser_keep_open_on_error return self.app_env == "dev" and not self.browser_headless_effective @property def browser_keep_open_effective(self) -> bool: """成功后是否保留浏览器窗口:仅在开发环境有头模式下保留""" if self.browser_keep_open is not None: return self.browser_keep_open return self.app_env == "dev" and not self.browser_headless_effective @property def log_to_file_effective(self) -> bool: """是否写日志文件:显式配置优先,否则生产环境默认写文件""" if self.log_to_file is not None: return self.log_to_file return self.app_env == "prod" @property def proxy(self) -> dict[str, str] | None: """构建 Playwright 代理配置字典""" if not self.proxy_server: return None proxy: dict[str, str] = {"server": self.proxy_server} if self.proxy_username: proxy["username"] = self.proxy_username if self.proxy_password: proxy["password"] = self.proxy_password return proxy @lru_cache(maxsize=1) def get_settings() -> Settings: """获取全局配置单例""" return Settings()