# ============================================================================= # 企微IT智能服务台 — 配置管理模块 # ============================================================================= # 说明:使用 pydantic-settings 从环境变量读取所有配置项 # 优先级:环境变量 > .env 文件 > 默认值 # 所有配置项集中管理,避免散落在代码各处 # ============================================================================= from typing import List import redis.asyncio as aioredis from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """应用配置类。 使用 pydantic-settings 自动从环境变量读取配置值。 支持 .env 文件自动加载(开发环境便利)。 Attributes: wecom_corp_id: 企业微信企业ID wecom_agent_id: 企业微信应用AgentId wecom_secret: 企业微信应用Secret wecom_token: 企业微信回调Token wecom_encoding_aes_key: 企业微信回调EncodingAESKey(43位) database_url: PostgreSQL 数据库连接地址 redis_url: Redis 连接地址 backend_host: 后端监听地址 backend_port: 后端监听端口 cors_origins: CORS 允许的源地址(逗号分隔) """ # ---------------------------------------------------------------------- # 企微自建应用配置 # ---------------------------------------------------------------------- # 企业ID(在企微管理后台 > 我的企业 > 企业信息 中查看) wecom_corp_id: str = "ww1234567890abcdef" # 应用AgentId(在企微管理后台 > 应用管理 > 自建应用 中查看) wecom_agent_id: str = "1000002" # 应用Secret(在企微管理后台 > 应用管理 > 自建应用 中查看) wecom_secret: str = "your-agent-secret" # 审批应用Secret(在企微管理后台 > 应用管理 > 审批 > 查看Secret) wecom_approval_secret: str = "" # 回调Token(在企微管理后台 > 应用管理 > 接收消息 中设置) wecom_token: str = "your-callback-token" # 回调EncodingAESKey(43位字符串,用于消息加解密) wecom_encoding_aes_key: str = "your-aes-key-43-characters-long-encoding-key" # ---------------------------------------------------------------------- # 数据库配置 # ---------------------------------------------------------------------- # PostgreSQL 连接地址 # Docker 环境使用容器名 postgres,本地开发使用 localhost database_url: str = "postgresql://wecom:wecom_secret@localhost:5432/wecom_it_desk" # ---------------------------------------------------------------------- # Redis 配置 # ---------------------------------------------------------------------- # Redis 连接地址 # Docker 环境使用容器名 redis,本地开发使用 localhost # 从环境变量 REDIS_URL 读取,格式: redis://:password@host:port/db redis_url: str = "" # 默认为空,由环境变量 REDIS_URL 提供 # ---------------------------------------------------------------------- # 服务配置 # ---------------------------------------------------------------------- # 后端监听地址(0.0.0.0 表示监听所有网卡) backend_host: str = "0.0.0.0" # 后端监听端口 backend_port: int = 8000 # CORS 允许的源地址(逗号分隔的字符串) cors_origins: str = "http://localhost:5173,http://localhost:5174,http://localhost:5175" # ---------------------------------------------------------------------- # AI 服务配置(Dify) # ---------------------------------------------------------------------- # Dify API 端点(兼容 OpenAI Chat Completions 格式) # 必须通过环境变量 DIFY_API_URL 配置,不设置默认值(防止凭据泄露) dify_api_url: str = "" # Dify API Key(格式:base_url|app_id|app_name) # 必须通过环境变量 DIFY_API_KEY 配置,不设置默认值(防止凭据泄露) dify_api_key: str = "" # Dify API 请求超时(秒),在网络慢时可调大 dify_timeout: int = 30 # ---------------------------------------------------------------------- # AI Wingman 服务配置(Dify Agent 2 — 坐席端辅助) # ---------------------------------------------------------------------- # 坐席端 Wingman 专用 Dify API 端点(与员工端 Agent 分开) # 留空则禁用 Wingman 功能(不影响主流程) dify_wingman_api_url: str = "" # 坐席端 Wingman Dify API Key(需要新建 Agent 后填入,留空则禁用) # 格式:base_url|app_id|app_name(与 dify_api_key 相同格式) dify_wingman_api_key: str = "" # Wingman API 请求超时(秒) dify_wingman_timeout: int = 30 # ---------------------------------------------------------------------- # Mock 登录配置(测试阶段使用,跳过企微 OAuth2) # ---------------------------------------------------------------------- # 是否启用 Mock 登录(默认 false,生产环境必须关闭) mock_login_enabled: bool = False # ---------------------------------------------------------------------- # 开发模式配置(本地 docker-compose.dev.yml 用) # ---------------------------------------------------------------------- # 是否启用开发模式(本地开发环境,启用后挂载 /api/dev/* Mock OAuth 路由) # ⚠️ 生产环境必须为 false / 不设置 # 启用的副作用: # 1. 后端启动时挂载 /api/dev/login /users /health 三个 Mock 端点 # 2. /api/dev/login 跳过企微 OAuth 直接生成 token # 3. 启动日志会大声警告 "🧪 DEV_MODE enabled" dev_mode: bool = False # 开发模式默认 userid(本地前端兜底用,实际由前端 /api/dev/login 传入) dev_default_userid: str = "dev-user-001" # 开发模式默认姓名 dev_default_name: str = "开发测试用户" # 开发模式默认部门 dev_default_dept: str = "信息技术部" # ---------------------------------------------------------------------- # 运行环境 & 管理后台 IP 白名单(三端认证重构 AUTH-01) # ---------------------------------------------------------------------- # 应用运行环境:dev / test / production # 控制 UA 校验 / IP 白名单 / 真实企微 OAuth 的启用(仅 production 启用) # 通过环境变量 APP_ENV 控制(默认 dev,避免本地误触发强校验) app_env: str = "dev" # 管理后台登录 IP 白名单(逗号分隔,支持 CIDR,如 10.240.0.0/16) # 仅允许白名单内的 IP 访问管理后台登录;其余 IP 返回 4004(无权限) # 通过环境变量 ADMIN_ALLOWED_IPS 覆盖 admin_allowed_ips: str = "117.147.35.138,218.75.34.87,10.240.0.0/16" # ---------------------------------------------------------------------- # 审批模板配置(企微审批应用) # ---------------------------------------------------------------------- # 资源申请审批模板ID(在企微审批应用设置中获取) approval_template_resource: str = "" # 设备申请审批模板ID(在企微审批应用设置中获取) approval_template_device: str = "" # ---------------------------------------------------------------------- # v0.7.1 企微 SSO 入口配置 (task #85) # ---------------------------------------------------------------------- # 是否启用企微 SSO(true = 优先用企微 OAuth2 静默授权,失败时降级扫码) # 通过环境变量 WECOM_SSO_ENABLED 控制(默认 false,避免老用户被打扰) wecom_sso_enabled: bool = False # SSO OAuth 回调 base URL(企微要求 redirect_uri 必须用可信域名) # 生产: https://itsupport.servyou.com.cn 开发: http://localhost:5176 wecom_sso_callback_base: str = "" # ---------------------------------------------------------------------- # v0.5.4 应急页身份检测配置 # ---------------------------------------------------------------------- # IT支持-咨询坐席 通讯录标签 ID(在企微管理后台 > 通讯录管理 > 标签管理 中查看) # 配置后,应急页会通过此标签判断当前用户是否为坐席 # 留空则降级到下面的硬编码名单 wecom_agent_tag_id: str = "" # 硬编码坐席 userid 列表(逗号分隔),作为标签检测的降级方案 # 例:"zhangsan,lisi,wangwu"(生产环境建议用标签方案) wecom_agent_userids: str = "" # ---------------------------------------------------------------------- # v0.6.0 内容审核报警配置(占位,后续完善) # ---------------------------------------------------------------------- # 合规通知企微群机器人 webhook content_audit_webhook: str = "" # 主管接收报警的 userid(多个用逗号分隔) content_audit_supervisor_userids: str = "" # ---------------------------------------------------------------------- # 阶段5 自动化闭环配置(环境变量前缀 AUTOMATION_*) # ---------------------------------------------------------------------- # 说明:自动化引擎连接的外部系统基址与密钥占位。 # 优先级:环境变量 AUTOMATION_* > 阶段1-4 既有的 system_configs 集成配置 # (huorong/lianruan/ragflow 在 app/integrations/*/config.py 中已有 getter) # 注意:密钥均为占位,生产环境必须通过环境变量注入,切勿硬编码真实密钥。 # ---------------------------------------------------------------------- # Dify(意图识别 / AI 编排) automation_dify_base_url: str = "" automation_dify_api_key: str = "" # RAGFlow(知识库检索,默认内网 :9380) automation_ragflow_base_url: str = "http://10.80.0.85:9380" automation_ragflow_api_key: str = "" # 火绒终端安全(HRESS HMAC-SHA1 签名) automation_huorong_base_url: str = "" automation_huorong_access_key_id: str = "" automation_huorong_access_key_secret: str = "" # 联软 LV7000(三层认证:IP白名单 + 账号密码 + Token) automation_lianruan_base_url: str = "" automation_lianruan_api_account: str = "" automation_lianruan_api_password: str = "" automation_lianruan_validate_key: str = "" # 北森 EHR(静态映射兜底) automation_ehr_base_url: str = "" automation_ehr_api_key: str = "" # 自动化阈值(JSON 字符串):置信度下限 / 超时秒 / 连续未解决次数 / 高危必转 # 管理后台可配(见 ScenarioConfig + 全局阈值),此处为默认值。 automation_thresholds: str = '{"confidence_min":0.6,"timeout_seconds":60,"unresolved_threshold":2,"high_risk_force_handoff":true}' def get_automation_thresholds(self) -> dict: """解析自动化阈值配置,返回带默认值的字典。 为什么单独成方法:阈值是 JSON 字符串(便于通过环境变量整体注入), 解析失败时回退到代码内默认值,避免单点配置错误导致引擎不可用。 """ default = { "confidence_min": 0.6, "timeout_seconds": 60, "unresolved_threshold": 2, "high_risk_force_handoff": True, } try: import json as _json if self.automation_thresholds: parsed = _json.loads(self.automation_thresholds) if isinstance(parsed, dict): default.update(parsed) except Exception as e: # 解析失败仅记日志,不中断启动 logger.warning(f"自动化阈值解析失败,使用默认值: {e}") return default # ---------------------------------------------------------------------- # Pydantic-settings 配置 # ---------------------------------------------------------------------- model_config = SettingsConfigDict( # 自动从 .env 文件加载环境变量 env_file=".env", # .env 文件编码 env_file_encoding="utf-8", # 环境变量名大小写不敏感 case_sensitive=False, # 额外字段不允许(防止拼写错误的配置被忽略) extra="ignore", ) @property def cors_origins_list(self) -> List[str]: """将 CORS 源地址字符串解析为列表。 将逗号分隔的字符串(如 "http://a,http://b") 转换为列表(如 ["http://a", "http://b"]), 方便 FastAPI 的 CORSMiddleware 使用。 Returns: List[str]: CORS 允许的源地址列表 """ # 去除每项的前后空格,过滤空字符串 return [origin.strip() for origin in self.cors_origins.split(",") if origin.strip()] def create_redis_client(self) -> aioredis.Redis: """创建 Redis 异步客户端实例。 使用单独的 host/port/password 参数,避免 URL 解析问题 (特别是密码中包含特殊字符 ! @ # 时)。 自动附加 protocol=2 参数,强制使用 RESP2 协议。 原因:Windows 版 Redis 3.x 不支持 RESP3 协议(HELLO 命令), 而 redis-py 8.0+ 默认使用 RESP3,会导致连接失败。 全项目统一使用此方法创建 Redis 客户端,避免协议不匹配。 Returns: aioredis.Redis: 配置好的 Redis 异步客户端 """ # 连接超时保护:防止 Redis 不可达时请求无限挂起 # (历史事故:REDIS_URL 密码含 @ # 导致 urlparse 解析到错误 host, # 连接一直挂起,最终表现为登录接口超时 / 502 / 浏览器"网络连接失败") socket_connect_timeout = 5 socket_timeout = 5 # 如果 redis_url 为空,使用默认值 if not self.redis_url: # 默认值:本地 Redis return aioredis.Redis( host="localhost", port=6379, protocol=2, decode_responses=True, socket_connect_timeout=socket_connect_timeout, socket_timeout=socket_timeout, ) # 解析 REDIS_URL 提取连接参数 # 格式: redis://:password@host:port/db # ⚠️ 密码可能含 URL 保留字符(@ # ! 等),部署时必须用 URL-encode: # @ → %40, # → %23, ! → %21 # 例: R3d!s@2026#Secure → R3d%21s%402026%23Secure # urlparse 不会自动解码百分号编码,这里用 unquote 还原真实密码/主机 from urllib.parse import urlparse, unquote parsed = urlparse(self.redis_url) # 提取密码(先尝试标准 urlparse 字段,失败则从 netloc 兜底) password = parsed.password if not password: # 尝试从 netloc 中提取(格式 :password@host) netloc = parsed.netloc if "@" in netloc: password = netloc.split("@")[0].split(":")[-1] if password: password = unquote(password) hostname = unquote(parsed.hostname) if parsed.hostname else "localhost" port = parsed.port or 6379 db = parsed.path and int(parsed.path.lstrip("/")) or 0 return aioredis.Redis( host=hostname, port=port, password=password, db=db, protocol=2, decode_responses=True, socket_connect_timeout=socket_connect_timeout, socket_timeout=socket_timeout, ) # 创建全局配置实例 # 整个应用通过 from app.config import settings 使用同一个实例 settings = Settings()