Files
group_fqcd_jr/app/core/config.py
T
张胜宇 9675df8453 chore(sync): zsy_developcc 全量同步至 qyqy_develop(W26 口径)
- 分支内容对齐 qyqy_develop b6ec3aa,树完全一致(同步后 git diff 为空)
- 覆盖本轮全部交付:客服 Agent 重构(安全路由 / 五出口 / 记忆与画像 / RAG 全链路)
  + 开发文档 62 份编号体系(D1.1 v1.17 索引)
  + 新增 D2.10-客服Agent端到端答辩文档-2026-09-21.html
- 基线:e239eb7(2026-09-17 品牌口径统一快照),本提交为其直接后继
2026-09-21 21:26:30 +08:00

162 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from functools import lru_cache
from dotenv import load_dotenv
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
# 把 `.env` 显式注入进程环境变量。pydantic-settings 只把 `.env` 读进 Settings 对象,
# **不会**写 `os.environ`;而模型密钥解析(`EnvironmentSecretResolver` 要求 secret_ref
# 形如 `env:VAR`)读的正是 `os.getenv`。两者不打通就会出现"按文档在 .env 里配好密钥、
# 运行时却读不到",且症状伪装成"没有可用的模型端点",排查方向被带偏。
# `override=False`:真实进程环境变量优先,`.env` 只作补充(生产用容器注入时不会被覆盖)。
load_dotenv(override=False)
class Settings(BaseSettings):
app_env: str = "local"
app_name: str = "jr-agent-platform"
log_level: str = "INFO"
timezone: str = "Asia/Shanghai"
cors_allowed_origins: str = "http://127.0.0.1:5173,http://localhost:5173"
jwt_issuer: str
jwt_audience: str
jwt_algorithm: str = "RS256"
# 默认指向**开发专用**密钥(tools/generate_jwt_keys.py 生成,config/jwt/ 不进版本库)。
# 生产环境必须用环境变量覆盖为生产机上单独生成的那一套,不要复用开发密钥。
jwt_private_key_path: str = "config/jwt/dev/jwt-private.pem"
jwt_public_key_path: str = "config/jwt/dev/jwt-public.pem"
jwt_clock_skew_seconds: int = Field(default=30, ge=0)
visitor_token_ttl_seconds: int = Field(default=900, ge=60, le=3600)
mysql_dsn: str
mysql_pool_size: int = Field(default=5, ge=1)
mysql_max_overflow: int = Field(default=10, ge=0)
mysql_pool_pre_ping: bool = False
mysql_tx_isolation: str = "READ COMMITTED"
redis_url: str
redis_connect_timeout_seconds: float = Field(default=2, gt=0)
# 限流(文档 §3.5 的 429 / §3.6 的 RATE_LIMITED)。默认值刻意宽松:
# 每用户每接口每分钟 600 次(10 QPS)。取值依据是"本地验收与集成测试的量级"——
# 两个验收脚本与 tests/integration 里同一身份对同一接口最多几十次调用,
# 600 留了一个数量级余量,绝不可能误伤;同时 10 QPS 已足以拦住单个客户端
# 把底座打爆(正常业务交互远低于 1 QPS)。生产按真实容量收紧即可,无需改代码。
rate_limit_enabled: bool = True
rate_limit_window_seconds: int = Field(default=60, gt=0)
rate_limit_max_requests: int = Field(default=600, ge=1)
rate_limit_key_prefix: str = "jr:rate_limit"
message_broker_url: str = ""
message_broker_run_topic: str = "agent.run"
message_broker_outbox_topic: str = "agent.outbox"
message_broker_dlq_topic: str = "agent.dlq"
milvus_uri: str
milvus_local_uri: str = ""
milvus_token: str = ""
# 长期记忆向量集合名**刻意不做成配置项**:它是写(投影)、读(召回)、删(清理)
# 三侧共用的代码级契约,集合 schema 与向量维度(1024)也由代码定义
# (`app/infrastructure/milvus_profile_projection.py` 的 `PROFILE_COLLECTION`)。
# 这里原本有个 `milvus_collection`(`jr_memory`),写路径却从没读过它 ——
# 只有读/删两侧读,于是"写进 A、从 B 查、从 B 删",语义召回恒空、清理恒报成功却不删。
# 一个只在契约一侧生效的配置项比没有配置项更危险,故直接删除。
neo4j_uri: str
neo4j_database: str = "neo4j"
neo4j_username: str = "neo4j"
neo4j_password: str = ""
model_router_config_ref: str = "local"
model_default_endpoint: str = ""
model_fallback_endpoint: str = ""
knowledge_embedding_endpoint_code: str = ""
knowledge_embedding_timeout_ms: int = Field(default=15000, gt=0)
sse_heartbeat_seconds: int = Field(default=15, gt=0)
sse_chunk_characters: int = Field(default=256, ge=1, le=4096)
sse_max_connection_seconds: int = Field(default=300, gt=0)
worker_lease_seconds: int = Field(default=60, gt=0)
worker_retry_limit: int = Field(default=3, ge=0)
worker_poll_seconds: float = Field(default=1, gt=0)
offsite_mailbox: str = "yuan80818843@163.com"
offsite_allowed_senders: tuple[str, ...] = ("15273589815@163.com",)
offsite_risk_receiver_id: str = ""
offsite_settlement_receiver_id: str = ""
offsite_mail_return_receiver: str = "yuan80818843@163.com"
offsite_max_retry_count: int = Field(default=3, ge=0)
offsite_imap_enabled: bool = False
offsite_imap_host: str = ""
offsite_imap_port: int = Field(default=993, gt=0)
offsite_imap_username: str = ""
offsite_imap_password: str = ""
offsite_imap_use_ssl: bool = True
offsite_imap_idle_enabled: bool = False
offsite_imap_idle_timeout_seconds: float = Field(default=120, gt=0)
offsite_mail_worker_enabled: bool = False
offsite_mail_worker_batch_size: int = Field(default=20, ge=1, le=100)
offsite_worker_user_id: str = ""
offsite_notification_sending_timeout_seconds: int = Field(default=900, gt=0)
offsite_mail_storage_dir: str = "data/offsite_mail"
offsite_ocr_enabled: bool = False
offsite_ocr_timeout_seconds: float = Field(default=30, gt=0)
offsite_aliyun_ocr_endpoint: str = "https://docmind-api.cn-hangzhou.aliyuncs.com"
offsite_aliyun_region_id: str = "cn-hangzhou"
offsite_aliyun_poll_interval_seconds: float = Field(default=1, gt=0)
offsite_aliyun_access_key_id: str = ""
offsite_aliyun_access_key_secret: str = ""
offsite_deepseek_enabled: bool = False
offsite_deepseek_base_url: str = "https://api.deepseek.com"
offsite_deepseek_api_key: str = ""
offsite_deepseek_model: str = "deepseek-v4-flash"
offsite_deepseek_timeout_seconds: float = Field(default=30, gt=0)
#: 平台级 DeepSeek 密钥。`model_endpoint_config.secret_ref` 走 `env:DEEPSEEK_API_KEY`,
#: 投顾「推荐依据」的 LLM 增强也复用它 —— 密钥只有一个存放点,不按特性各配一把。
deepseek_api_key: str = ""
#: 投顾「推荐依据」的 LLM 增强(可选)。未启用、或取不到密钥/调用失败时,
#: **自动回退**到确定性的数据化文案:推荐流程绝不因为模型不可用而失败。
advisor_reason_llm_enabled: bool = False
advisor_reason_llm_base_url: str = "https://api.deepseek.com"
#: 实测 `api.deepseek.com`:`deepseek-chat` 正常返回文本;`deepseek-v4-flash` /
#: `deepseek-reasoner` 返回 200 但 `content` 为空(推理型内容在 `reasoning_content`),
#: 客户端已做兜底读取,但默认仍用最稳的 `deepseek-chat`。
advisor_reason_llm_model: str = "deepseek-chat"
advisor_reason_llm_timeout_seconds: float = Field(default=20, gt=0)
offsite_smtp_enabled: bool = False
offsite_smtp_dry_run: bool = True
offsite_smtp_host: str = ""
offsite_smtp_port: int = Field(default=465, gt=0)
offsite_smtp_username: str = ""
offsite_smtp_password: str = ""
offsite_smtp_sender: str = ""
offsite_smtp_use_ssl: bool = True
offsite_smtp_timeout_seconds: float = Field(default=30, gt=0)
promotion_material_storage_dir: str = "data/promotion_materials"
promotion_pdf_enabled: bool = False
promotion_pdf_converter_path: str = ""
promotion_max_photo_size_bytes: int = Field(default=10 * 1024 * 1024, gt=0)
promotion_max_performance_file_size_bytes: int = Field(default=20 * 1024 * 1024, gt=0)
promotion_image_enabled: bool = False
promotion_image_base_url: str = "https://dashscope.aliyuncs.com"
promotion_image_model: str = "wan2.2-t2i-flash"
promotion_image_timeout_seconds: float = Field(default=90, gt=0)
promotion_image_poll_interval_seconds: float = Field(default=2, gt=0)
risk_scan_schedule_enabled: bool = False
risk_scan_interval_minutes: int = Field(default=5, ge=1, le=1440)
risk_scan_run_immediately: bool = False
risk_scan_retry_limit: int = Field(default=2, ge=0, le=5)
risk_scan_poll_seconds: float = Field(default=30, gt=0)
risk_alert_mail_enabled: bool = False
risk_alert_mail_dry_run: bool = True
risk_alert_mail_recipients: str = ""
# 投顾灰度默认关闭,只有显式开启并配置客户白名单后才限制客户流量。
# 管理员角色始终可进入,便于审核、发布和故障处置。
advisor_rollout_enabled: bool = False
advisor_rollout_customer_ids: str = ""
@property
def resolved_milvus_uri(self) -> str:
"""本地开发优先使用 Lite 文件;部署环境使用标准 Milvus URI。"""
return self.milvus_local_uri or self.milvus_uri
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings() # type: ignore[call-arg]