"""敏感配置加载器(.env) 职责边界(2026-09-25 重构): - 本文件只负责**加载**敏感配置:数据库口令、各供应商 API Key。 - 校验(缺哪些、密钥变量叫什么)在 `config.required_secrets()` / `config.check_secrets()`, 因为密钥变量名由 config.yml 的 `endpoints.*.api_key_env` 决定(换供应商时会变)。 - 非敏感配置(数据库地址、目录、模型名、**base url 接入点**、参数、清理开关)见 config.yml / config.py。 `.env` 查找顺序:XWLB_ENV_FILE → 项目目录内 .env → 上一级 / 上两级 .env(兼容旧 djapi 布局)。 已存在的进程环境变量优先,不会被 .env 覆盖。 """ import logging import os from pathlib import Path logger = logging.getLogger(__name__) # 本文件所在目录即项目根(xwlb/) BASE_DIR = Path(__file__).resolve().parent def _candidate_env_files(): """按优先级返回候选 .env 路径""" override = os.getenv('XWLB_ENV_FILE') if override: yield Path(override).expanduser() return yield BASE_DIR / '.env' # 兼容旧布局:xwlb/ 的上一级或上两级 yield BASE_DIR.parent / '.env' yield BASE_DIR.parent.parent / '.env' def _load_dotenv(): """从候选路径中第一个存在的 .env 加载环境变量(不覆盖已有)""" for dotenv_path in _candidate_env_files(): if not dotenv_path.is_file(): continue count = 0 with open(dotenv_path, encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#') or '=' not in line: continue key, _, value = line.partition('=') key = key.strip() value = value.strip().strip('"').strip("'") if key and key not in os.environ: os.environ[key] = value count += 1 logger.info("已加载敏感配置 %s(注入 %d 个变量)", dotenv_path, count) return dotenv_path logger.debug("未找到 .env 文件,仅使用进程环境变量") return None LOADED_ENV_FILE = _load_dotenv() def env_file() -> Path: """当前使用的 .env 路径(不存在时返回期望路径)""" return LOADED_ENV_FILE or (BASE_DIR / '.env')