config.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. """
  2. 配置管理模块
  3. """
  4. import os
  5. from pathlib import Path
  6. from pydantic_settings import BaseSettings
  7. from dotenv import load_dotenv
  8. # 加载环境变量
  9. # 🔥 确保从 backend 目录加载 .env 文件(无论从哪个目录启动服务)
  10. backend_dir = Path(__file__).parent
  11. # 支持通过环境变量 ENV_FILE 指定 env 文件路径(生产环境使用)
  12. env_file_name = os.getenv("ENV_FILE", ".env")
  13. env_file = backend_dir / env_file_name
  14. if env_file.exists():
  15. load_dotenv(env_file)
  16. # 生产环境不再使用 print 输出加载信息,避免多余日志
  17. # print(f"✅ 已加载 .env 文件: {env_file}")
  18. else:
  19. # 如果指定的 env 文件不存在,尝试默认 .env
  20. default_env_file = backend_dir / ".env"
  21. if default_env_file.exists():
  22. load_dotenv(default_env_file)
  23. # 生产环境不再使用 print 输出加载信息,避免多余日志
  24. # print(f"✅ 已加载默认 .env 文件: {default_env_file}")
  25. else:
  26. # 如果 backend/.env 不存在,尝试从当前目录加载(兼容旧方式)
  27. load_dotenv()
  28. if os.getenv("TTS_SPEECH_RATE"):
  29. # 生产环境不再使用 print 输出加载信息,避免多余日志
  30. # print(f"⚠️ 从当前目录加载环境变量,TTS_SPEECH_RATE={os.getenv('TTS_SPEECH_RATE')}")
  31. pass
  32. else:
  33. # 生产环境不再使用 print 输出加载信息,避免多余日志
  34. # print(f"⚠️ 未找到 .env 文件: {env_file},使用默认配置")
  35. pass
  36. class Settings(BaseSettings):
  37. """应用配置"""
  38. # 应用基础配置
  39. APP_NAME: str = "Lyrria Voice Assistant"
  40. APP_VERSION: str = "1.0.0"
  41. DEBUG: bool = False
  42. # 服务器配置
  43. HOST: str = os.getenv("HOST", "0.0.0.0")
  44. PORT: int = int(os.getenv("PORT", "8000"))
  45. host: str = os.getenv("HOST", "0.0.0.0") # 别名,兼容小写
  46. port: int = int(os.getenv("PORT", "8000")) # 别名,兼容小写
  47. # WebSocket配置
  48. WS_HEARTBEAT_INTERVAL: int = 30 # 心跳间隔(秒)
  49. WS_TIMEOUT: int = 60 # 超时时间(秒)
  50. # 能量石配置
  51. ENERGY_STONE_TTL_SECONDS: int = int(os.getenv("ENERGY_STONE_TTL_SECONDS", "20"))
  52. CONVERSATION_STONE_INTERVAL_SECONDS: int = int(os.getenv("CONVERSATION_STONE_INTERVAL_SECONDS", "30"))
  53. MISSION_TYPE_CACHE_SECONDS: int = int(os.getenv("MISSION_TYPE_CACHE_SECONDS", "60"))
  54. # DashScope API配置(阿里云)
  55. dashscope_api_key: str = os.getenv("DASHSCOPE_API_KEY", "")
  56. dashscope_api_key_intl: str = os.getenv("DASHSCOPE_API_KEY_INTL", "") # 国际版单独的key(可选)
  57. use_dashscope_intl: bool = os.getenv("USE_DASHSCOPE_INTL", "false").lower() == "true" # 是否使用国际版
  58. @property
  59. def active_dashscope_api_key(self) -> str:
  60. """获取当前激活的API Key(根据国际版/国内版自动选择)"""
  61. if self.use_dashscope_intl and self.dashscope_api_key_intl:
  62. return self.dashscope_api_key_intl
  63. return self.dashscope_api_key
  64. # LLM配置
  65. llm_model: str = os.getenv("LLM_MODEL", "qwen3.5-flash")
  66. LLM_BASE_URL: str = os.getenv("LLM_BASE_URL", "")
  67. # 百炼 qwen3.5-flash 等默认开启「深度思考」;语音对话建议关闭以降低延迟与 Token
  68. llm_enable_thinking: bool = os.getenv("LLM_ENABLE_THINKING", "false").lower() == "true"
  69. @property
  70. def llm_openai_extra_body(self) -> dict[str, bool]:
  71. """DashScope OpenAI 兼容接口扩展参数(enable_thinking)。"""
  72. return {"enable_thinking": self.llm_enable_thinking}
  73. # STT配置
  74. # ⚠️ 重要:language参数必须设置!
  75. # 支持的值:
  76. # - 'zh' (中文)
  77. # - 'en' (英文)
  78. # - 'ja' (日文)
  79. # - 'ko' (韩文)
  80. # - 'auto' (自动检测 - 实验性,需要测试)
  81. # 项目正式使用时应设为'en',测试阶段用'zh'或'auto'
  82. STT_MODEL: str = os.getenv("STT_MODEL", "qwen3-asr-flash-realtime")
  83. STT_WS_URL: str = os.getenv("STT_WS_URL", "")
  84. STT_LANGUAGE: str = os.getenv("STT_LANGUAGE", "zh")
  85. # TTS配置
  86. tts_model: str = os.getenv("TTS_MODEL", "cosyvoice-v3-flash")
  87. TTS_API_URL: str = os.getenv("TTS_API_URL", "")
  88. tts_voice: str = os.getenv("TTS_VOICE", "longanhuan") # cosyvoice-v3 音色(支持:longanhuan, longanyang 等)
  89. tts_speech_rate: float = float(os.getenv("TTS_SPEECH_RATE", "1.0")) # 语速:1.0 = 正常速度
  90. tts_pitch_rate: float = float(os.getenv("TTS_PITCH_RATE", "1.15")) # 音调:1.15 = 提升15%
  91. tts_language_hints: list = os.getenv("TTS_LANGUAGE_HINTS", "en").split(",") if os.getenv("TTS_LANGUAGE_HINTS") else ["en"] # 语言提示
  92. # TTS重试配置(处理API速率限制)
  93. TTS_MAX_RETRIES: int = int(os.getenv("TTS_MAX_RETRIES", "3")) # 最大重试次数
  94. TTS_RETRY_DELAY: float = float(os.getenv("TTS_RETRY_DELAY", "5.0")) # 初始重试延迟(秒)- 增加到5秒
  95. TTS_RETRY_BACKOFF: float = float(os.getenv("TTS_RETRY_BACKOFF", "2.0")) # 指数退避系数
  96. # TTS速率控制(预防性措施,避免触发API限制)
  97. TTS_MIN_INTERVAL: float = float(os.getenv("TTS_MIN_INTERVAL", "0.8")) # 两次TTS调用的最小间隔(秒)
  98. # AI助手提示词配置
  99. LEARNER_PROFICIENCY: str = os.getenv("LEARNER_PROFICIENCY", "A1-A2")
  100. AGE_TAG: str = os.getenv("AGE_TAG", "Adult")
  101. TOPIC_TAG: str = os.getenv("TOPIC_TAG", "Daily Conversation")
  102. EXPERIMENT_TAG: str = os.getenv("EXPERIMENT_TAG", "control") # control 或 experiment
  103. USE_SIMPLIFIED_PROMPT: bool = os.getenv("USE_SIMPLIFIED_PROMPT", "false").lower() == "true" # 🔥 使用简化提示词(减少延迟)
  104. # 音频配置
  105. AUDIO_SAMPLE_RATE: int = 16000
  106. AUDIO_BIT_RATE: int = 16
  107. AUDIO_CHANNELS: int = 1
  108. # MySQL配置
  109. MYSQL_HOST: str = os.getenv("MYSQL_HOST", "47.99.164.66")
  110. MYSQL_PORT: int = int(os.getenv("MYSQL_PORT", "3306"))
  111. MYSQL_USER: str = os.getenv("MYSQL_USER", "root")
  112. MYSQL_PASSWORD: str = os.getenv("MYSQL_PASSWORD", "")
  113. MYSQL_DATABASE: str = os.getenv("MYSQL_DATABASE", "Lyrria")
  114. MYSQL_POOL_SIZE: int = int(os.getenv("MYSQL_POOL_SIZE", "10"))
  115. MYSQL_MAX_OVERFLOW: int = int(os.getenv("MYSQL_MAX_OVERFLOW", "20"))
  116. MYSQL_CONNECT_TIMEOUT: int = int(os.getenv("MYSQL_CONNECT_TIMEOUT", "10"))
  117. MYSQL_READ_TIMEOUT: int = int(os.getenv("MYSQL_READ_TIMEOUT", "10"))
  118. MYSQL_POOL_RECYCLE: int = int(os.getenv("MYSQL_POOL_RECYCLE", "7200")) # 🔥 增加到2小时,保持长连接
  119. MYSQL_HEALTHCHECK_INTERVAL: int = int(os.getenv("MYSQL_HEALTHCHECK_INTERVAL", "600")) # 🔥 增加到10分钟,减少检查频率
  120. # 亲密度规则生效日:仅统计该日期及之后的页面访问,用于计算连续未访问天数
  121. INTIMACY_RULE_EFFECTIVE_DATE: str = os.getenv("INTIMACY_RULE_EFFECTIVE_DATE", "2026-02-13")
  122. # Redis配置
  123. REDIS_HOST: str = os.getenv("REDIS_HOST", "47.99.164.66")
  124. REDIS_PORT: int = int(os.getenv("REDIS_PORT", "16379"))
  125. REDIS_DB: int = int(os.getenv("REDIS_DB", "12"))
  126. REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
  127. REDIS_SSL: bool = os.getenv("REDIS_SSL", "false").lower() == "true"
  128. REDIS_SESSION_TTL: int = int(os.getenv("REDIS_SESSION_TTL", "86400"))
  129. # 用户会话 token 有效期(秒),用于 HTTP/WebSocket 鉴权与滑动续期,默认 7 天
  130. SESSION_TOKEN_TTL_SECONDS: int = int(os.getenv("SESSION_TOKEN_TTL_SECONDS", "604800"))
  131. REDIS_MAX_CONNECTIONS: int = int(os.getenv("REDIS_MAX_CONNECTIONS", "50"))
  132. REDIS_HEALTHCHECK_INTERVAL: int = int(os.getenv("REDIS_HEALTHCHECK_INTERVAL", "300")) # 🔥 Redis健康检查间隔(秒),5分钟检查一次
  133. # HTTP客户端配置(用于DashScope API)
  134. HTTPX_MAX_KEEPALIVE_CONNECTIONS: int = int(os.getenv("HTTPX_MAX_KEEPALIVE_CONNECTIONS", "20")) # 🔥 keepalive连接数
  135. HTTPX_MAX_CONNECTIONS: int = int(os.getenv("HTTPX_MAX_CONNECTIONS", "50")) # 🔥 最大连接数
  136. HTTPX_KEEPALIVE_EXPIRY: float = float(os.getenv("HTTPX_KEEPALIVE_EXPIRY", "600.0")) # 🔥 keepalive过期时间(秒),10分钟,减少长时间未使用后的连接重建
  137. # LLM连接健康检查配置
  138. LLM_HEALTHCHECK_INTERVAL: int = int(os.getenv("LLM_HEALTHCHECK_INTERVAL", "300")) # 🔥 LLM连接健康检查间隔(秒),5分钟检查一次
  139. # TTS连接健康检查配置
  140. TTS_HEALTHCHECK_INTERVAL: int = int(os.getenv("TTS_HEALTHCHECK_INTERVAL", "300")) # 🔥 TTS连接健康检查间隔(秒),5分钟检查一次
  141. # CORS配置
  142. # 支持localhost和任意IP地址访问(端口3000为Vite默认HTTPS端口)
  143. CORS_ORIGINS: list = [
  144. "http://localhost:3000",
  145. "https://localhost:3000",
  146. "http://127.0.0.1:3000",
  147. "https://127.0.0.1:3000",
  148. "http://localhost:5173",
  149. "https://localhost:5173",
  150. "http://127.0.0.1:5173",
  151. "https://127.0.0.1:5173",
  152. # 支持局域网IP访问
  153. "http://192.168.2.112:3000",
  154. "https://192.168.2.112:3000",
  155. "http://192.168.2.112:5173",
  156. "https://192.168.2.112:5173",
  157. "http://192.168.43.35:3000",
  158. "https://192.168.43.35:3000",
  159. "http://192.168.43.35:5173",
  160. "https://192.168.43.35:5173",
  161. ]
  162. # 是否允许所有来源(开发环境可设为True,生产环境务必设为False)
  163. CORS_ALLOW_ALL: bool = os.getenv("CORS_ALLOW_ALL", "true").lower() == "true"
  164. # 日志配置
  165. LOG_LEVEL: str = "INFO"
  166. LOG_DIR: str = "logs"
  167. LOG_ROTATION: str = "500 MB"
  168. LOG_RETENTION: str = "10 days"
  169. # SSL配置(可选,生产环境建议由nginx统一管理SSL)
  170. SSL_KEYFILE: str = os.getenv("SSL_KEYFILE", "")
  171. SSL_CERTFILE: str = os.getenv("SSL_CERTFILE", "")
  172. # OAuth配置 - 微信开放平台(用于登录)
  173. WECHAT_OPEN_APP_ID: str = os.getenv("WECHAT_OPEN_APP_ID", os.getenv("WECHAT_APP_ID", "")) # 开放平台AppID,兼容旧配置
  174. WECHAT_OPEN_APP_SECRET: str = os.getenv("WECHAT_OPEN_APP_SECRET", os.getenv("WECHAT_APP_SECRET", "")) # 开放平台AppSecret,兼容旧配置
  175. WECHAT_REDIRECT_URI: str = os.getenv("WECHAT_REDIRECT_URI", "")
  176. # 微信公众平台配置(用于JSSDK分享)
  177. WECHAT_MP_APP_ID: str = os.getenv("WECHAT_MP_APP_ID", os.getenv("WECHAT_APP_ID", "")) # 公众平台AppID,兼容旧配置
  178. WECHAT_MP_APP_SECRET: str = os.getenv("WECHAT_MP_APP_SECRET", os.getenv("WECHAT_APP_SECRET", "")) # 公众平台AppSecret,兼容旧配置
  179. # 微信JS-SDK配置(用于分享功能)
  180. WECHAT_JSAPI_TICKET: str = os.getenv("WECHAT_JSAPI_TICKET", "") # jsapi_ticket(需要从微信服务器获取)
  181. WECHAT_JSAPI_TICKET_CACHE_KEY: str = "wechat_jsapi_ticket" # Redis缓存key
  182. WECHAT_JSAPI_TICKET_CACHE_TTL: int = int(os.getenv("WECHAT_JSAPI_TICKET_CACHE_TTL", "7200")) # 缓存时间(秒),默认2小时
  183. # 微信公众平台配置(用于公众号消息推送)
  184. WECHAT_MP_TOKEN: str = os.getenv("WECHAT_MP_TOKEN", "") # 公众号服务器配置Token
  185. # OAuth配置 - QQ开放平台
  186. QQ_APP_ID: str = os.getenv("QQ_APP_ID", "")
  187. QQ_APP_KEY: str = os.getenv("QQ_APP_KEY", "")
  188. QQ_REDIRECT_URI: str = os.getenv("QQ_REDIRECT_URI", "")
  189. QQ_APP_NAME: str = os.getenv("QQ_APP_NAME", "Lyrria") # QQ应用名称(用于分享功能)
  190. # 前端URL(用于OAuth回调后的前端跳转)
  191. FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:5173")
  192. # API安全配置
  193. API_CLIENT_KEY: str = os.getenv("API_CLIENT_KEY", "") # 前端API Key,用于验证请求来源
  194. API_KEY_ENABLED: bool = os.getenv("API_KEY_ENABLED", "true").lower() == "true" # 是否启用API Key验证
  195. class Config:
  196. env_file = ".env"
  197. case_sensitive = True
  198. extra = "ignore" # 忽略额外的环境变量(如旧的连接池配置)
  199. # 创建全局配置实例
  200. settings = Settings()
  201. # 🔍 调试:打印 TTS 语速配置(确认环境变量是否正确加载)
  202. import logging
  203. logger = logging.getLogger(__name__)
  204. env_value = os.getenv("TTS_SPEECH_RATE", "未设置")
  205. logger.info(f"🔍 TTS配置检查 - 环境变量 TTS_SPEECH_RATE: {env_value}, 最终配置值 tts_speech_rate: {settings.tts_speech_rate}")
  206. # 确保日志目录存在
  207. Path(settings.LOG_DIR).mkdir(parents=True, exist_ok=True)
  208. # DashScope API endpoint 配置
  209. def get_dashscope_domain() -> str:
  210. """获取 DashScope API 域名"""
  211. return "dashscope-intl.aliyuncs.com" if settings.use_dashscope_intl else "dashscope.aliyuncs.com"
  212. def get_dashscope_ws_url() -> str:
  213. """获取 DashScope WebSocket URL(用于STT)"""
  214. if settings.STT_WS_URL:
  215. return settings.STT_WS_URL
  216. domain = get_dashscope_domain()
  217. return f"wss://{domain}/api-ws/v1/realtime"
  218. def get_dashscope_http_base_url() -> str:
  219. """获取 DashScope HTTP base URL(用于LLM)"""
  220. if settings.LLM_BASE_URL:
  221. return settings.LLM_BASE_URL.rstrip("/")
  222. domain = get_dashscope_domain()
  223. return f"https://{domain}/compatible-mode/v1"
  224. def get_dashscope_tts_api_url() -> str:
  225. """获取 DashScope HTTP API URL(用于TTS)"""
  226. if settings.TTS_API_URL:
  227. return settings.TTS_API_URL.rstrip("/")
  228. domain = get_dashscope_domain()
  229. return f"https://{domain}/api/v1"