""" 配置包:只读环境变量,组装 Settings。 本文件职责:集中声明 YTD_ENV、端口、企微与库变量名(对齐 prompt/13、09)。 禁止:在本包写业务 if/else;禁止把密钥默认值写进源码。 """ from __future__ import annotations from functools import lru_cache from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict def _find_dotenv() -> str: """自 agent 目录向上找仓库根 .env;找不到则返回空,由环境变量本身提供。""" here = Path(__file__).resolve() for parent in here.parents: candidate = parent / ".env" if candidate.is_file(): return str(candidate) return "" class Settings(BaseSettings): """ 进程级只读配置。 调用时机:HTTP / Worker 启动时通过 get_settings() 加载一次。 企微密钥必须来自环境/.env,源码中不得出现明文 Secret。 """ model_config = SettingsConfigDict( env_file=_find_dotenv() or None, env_file_encoding="utf-8", extra="ignore", ) ytd_env: str = Field(default="test", alias="YTD_ENV") agent_http_port: int = Field(default=8910, alias="AGENT_HTTP_PORT") public_base_url: str = Field(default="", alias="PUBLIC_BASE_URL") biz_api_base_url: str = Field(default="http://127.0.0.1:8180/jeecgboot", alias="BIZ_API_BASE_URL") biz_api_token: str = Field(default="", alias="BIZ_API_TOKEN") biz_api_username: str = Field(default="", alias="BIZ_API_USERNAME") biz_api_password: str = Field(default="", alias="BIZ_API_PASSWORD") biz_api_timeout_seconds: float = Field(default=10.0, alias="BIZ_API_TIMEOUT_SECONDS") agent_callback_token: str = Field(default="", alias="AGENT_CALLBACK_TOKEN") # 兼容现网变量名 inquiry_agent_callback_token: str = Field(default="", alias="INQUIRY_AGENT_CALLBACK_TOKEN") # 企微询价应用(与正式同一套应用 ID;密钥只从环境读) wecom_corp_id: str = Field(default="", alias="WECOM_CORP_ID") wecom_agent_id: str = Field(default="1000010", alias="WECOM_AGENT_ID") wecom_secret: str = Field(default="", alias="WECOM_SECRET") wecom_callback_token: str = Field(default="", alias="WECOM_CALLBACK_TOKEN") wecom_encoding_aes_key: str = Field(default="", alias="WECOM_ENCODING_AES_KEY") wecom_callback_reply_text: str = Field( default="询价智能体接通成功。已收到:{content}", alias="WECOM_CALLBACK_REPLY_TEXT", ) # 本机注入开关:test 默认开;prod 默认关 dev_inject_enabled: bool = Field(default=False, alias="DEV_INJECT_ENABLED") pg_host: str = Field(default="", alias="PG_HOST") pg_port: int = Field(default=5432, alias="PG_PORT") pg_database: str = Field(default="", alias="PG_DATABASE") pg_user: str = Field(default="", alias="PG_USER") pg_password: str = Field(default="", alias="PG_PASSWORD") pg_sslmode: str = Field(default="prefer", alias="PG_SSLMODE") # 可直接给完整 DSN;为空则由 host/port/user 等拼装 pg_dsn: str = Field(default="", alias="PG_DSN") # postgres | memory;正式必须 postgres;memory 仅本地骨架冒烟 checkpoint_backend: str = Field(default="postgres", alias="CHECKPOINT_BACKEND") redis_url: str = Field(default="", alias="REDIS_URL") redis_key_prefix: str = Field(default="inquiry_robot:", alias="REDIS_KEY_PREFIX") # redis | memory;正式必须 redis + URL 指向逻辑库 /1(与现网 /0 隔离) redis_backend: str = Field(default="redis", alias="REDIS_BACKEND") redis_required: bool = Field(default=False, alias="REDIS_REQUIRED") # auto | memory | postgres;auto=有 PG_DSN 则尝试 PG message_store_backend: str = Field(default="auto", alias="MESSAGE_STORE_BACKEND") wecom_aibot_bridge_token: str = Field(default="", alias="WECOM_AIBOT_BRIDGE_TOKEN") wecom_aibot_ingress_url: str = Field( default="http://127.0.0.1:8910/internal/aibot/inbound", alias="WECOM_AIBOT_INGRESS_URL", ) deepseek_api_key: str = Field(default="", alias="DEEPSEEK_API_KEY") deepseek_base_url: str = Field(default="https://api.deepseek.com", alias="DEEPSEEK_BASE_URL") deepseek_model: str = Field(default="", alias="DEEPSEEK_MODEL") qwen_api_key: str = Field(default="", alias="QWEN_API_KEY") qwen_base_url: str = Field(default="", alias="QWEN_BASE_URL") qwen_text_model: str = Field(default="", alias="QWEN_TEXT_MODEL") qwen_vl_model: str = Field(default="", alias="QWEN_VL_MODEL") # prompt/04:未确认不得打真实模型(出站 HTTP 客户端强制检查) llm_data_usage_confirmed: bool = Field(default=False, alias="LLM_DATA_USAGE_CONFIRMED") # Worker 槽是否允许对 ordered_actions 走真网(仍受上一开关约束) llm_allow_network: bool = Field(default=False, alias="LLM_ALLOW_NETWORK") # LibreOffice:默认 Windows 安装路径;Linux 可改 /usr/bin/libreoffice libreoffice_path: str = Field( default=r"C:\Program Files\LibreOffice\program\soffice.exe", alias="LIBREOFFICE_PATH", ) libreoffice_timeout_seconds: float = Field( default=120.0, alias="LIBREOFFICE_TIMEOUT_SECONDS" ) object_storage_endpoint_url: str = Field(default="", alias="OBJECT_STORAGE_ENDPOINT_URL") object_storage_bucket: str = Field(default="inquiry-robot", alias="OBJECT_STORAGE_BUCKET") object_storage_access_key: str = Field(default="", alias="OBJECT_STORAGE_ACCESS_KEY") object_storage_secret_key: str = Field(default="", alias="OBJECT_STORAGE_SECRET_KEY") object_storage_path_style: bool = Field(default=True, alias="OBJECT_STORAGE_PATH_STYLE") libreoffice_slots: int = Field(default=1, alias="LIBREOFFICE_SLOTS") http_job_slots: int = Field(default=3, alias="HTTP_JOB_SLOTS") def resolve_pg_dsn(self) -> str: """ 解析 PostgreSQL 连接串(运行账 / Graph checkpoint)。 返回值:PG_DSN 或由分量拼出的 URI;缺关键字段时返回空串。 禁止:连现网 ytd_runtime(由部署/环境保证库名,代码不写死切换)。 """ raw = (self.pg_dsn or "").strip() if raw: return raw if not (self.pg_host and self.pg_database and self.pg_user): return "" # 密码可为空(本机信任);sslmode 走环境 from urllib.parse import quote_plus user = quote_plus(self.pg_user) password = quote_plus(self.pg_password or "") auth = f"{user}:{password}@" if self.pg_password else f"{user}@" return ( f"postgresql://{auth}{self.pg_host}:{self.pg_port}/" f"{self.pg_database}?sslmode={self.pg_sslmode or 'prefer'}" ) @lru_cache(maxsize=1) def get_settings() -> Settings: """返回进程内单例 Settings。副作用:首次可能读 .env。""" return Settings()