Files
group_fqcd_jr/app/core/config.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

145 lines
7.4 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)
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 = ""
@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]