""" 配置包:只读环境变量,组装 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") inquiry_form_public_base_url: str = Field( default="", alias="INQUIRY_FORM_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") # http=打 8180 真主账/真 TMS;memory=单测假账本 ledger_backend: str = Field(default="http", alias="LEDGER_BACKEND") 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") # 现网「询价消息服务」:只推成交通知,不走询价小助手 1000010。 wecom_notify_agent_id: str = Field(default="", alias="WECOM_NOTIFY_AGENT_ID") wecom_notify_secret: str = Field(default="", alias="WECOM_NOTIFY_SECRET") wecom_archive_seat_user_ids: str = Field( default="", alias="WECOM_ARCHIVE_SEAT_USER_IDS", ) # 微盛企微管家:Worker 轮询群消息。游标只写本进程 Redis 前缀,禁止去推现网官方 seq。 wshoto_base_url: str = Field(default="https://open.wshoto.com", alias="WSHOTO_BASE_URL") wshoto_app_id: str = Field(default="", alias="WSHOTO_APP_ID") wshoto_app_secret: str = Field(default="", alias="WSHOTO_APP_SECRET") wshoto_tenant_id: str = Field(default="", alias="WSHOTO_TENANT_ID") wshoto_poll_enabled: bool = Field(default=True, alias="WSHOTO_POLL_ENABLED") # 群消息靠会话存档,扫得越勤,听到得越快。热窗内还会再压到 1 秒。 wshoto_poll_interval_sec: float = Field(default=2.0, alias="WSHOTO_POLL_INTERVAL_SEC") wshoto_bootstrap_lookback_sec: int = Field( default=10800, alias="WSHOTO_BOOTSTRAP_LOOKBACK_SEC", ) wshoto_page_size: int = Field(default=50, alias="WSHOTO_PAGE_SIZE") # 空运群是人自建的,存档席可能没拉进群。按这些 userid 扫新群(逗号分隔)。 wshoto_scan_user_ids: str = Field(default="", alias="WSHOTO_SCAN_USER_IDS") wshoto_attachment_min_interval_ms: int = Field( default=1050, alias="WSHOTO_ATTACHMENT_MIN_INTERVAL_MS", ) 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_bridge_url: str = Field( default="http://127.0.0.1:8913", alias="WECOM_AIBOT_BRIDGE_URL", ) 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()