Files
wecom_it_smart_desk/backend/app/config.py
T

326 lines
16 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.
# =============================================================================
# 企微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: 企业微信回调EncodingAESKey43位)
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"
# 回调EncodingAESKey43位字符串,用于消息加解密)
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)
# ----------------------------------------------------------------------
# 是否启用企微 SSOtrue = 优先用企微 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()