config.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """
  2. 智能股票分析助手 — 配置管理模块
  3. 集中管理所有配置项,支持从环境变量(.env)加载。
  4. 参考 HelloAgents 框架的配置模式,参数优先级:构造函数参数 > 环境变量 > 默认值
  5. """
  6. import os
  7. import sys
  8. from pathlib import Path
  9. from typing import Optional
  10. from dotenv import load_dotenv
  11. # 检测是否为 PyInstaller 打包的 exe 运行环境
  12. IS_FROZEN = getattr(sys, 'frozen', False)
  13. if IS_FROZEN:
  14. # exe 内部资源目录(PyInstaller 临时解压目录)
  15. _BUNDLE_DIR = Path(getattr(sys, '_MEIPASS', Path(sys.executable).parent))
  16. # exe 外部目录(用户配置和数据文件放在 exe 旁边)
  17. _EXTERNAL_DIR = Path(sys.executable).parent
  18. else:
  19. # 开发模式:config.py -> app/ -> backend/ -> (项目根目录)
  20. _BUNDLE_DIR = Path(__file__).parent.parent.parent
  21. _EXTERNAL_DIR = _BUNDLE_DIR
  22. _PROJECT_ROOT = _BUNDLE_DIR
  23. # 加载 .env(优先从 exe 外部目录加载,开发模式从项目根加载)
  24. # exe 模式下必须用 override=True:否则系统/父进程里已存在的 MX_APIKEY(含空字符串)会阻止读取 exe 旁 .env,表现为「已换 Key 仍提示额度用尽且无缓存」
  25. _env_path = _EXTERNAL_DIR / ".env"
  26. if _env_path.exists():
  27. load_dotenv(_env_path, override=IS_FROZEN)
  28. else:
  29. load_dotenv(_PROJECT_ROOT / ".env", override=False)
  30. # exe 默认与自动打开的浏览器地址一致(127.0.0.1:5174/dashboard);开发模式仍为 8000
  31. _DEFAULT_BACKEND_PORT = "5174" if IS_FROZEN else "8000"
  32. class Settings:
  33. """全局配置单例"""
  34. # =========================================================================
  35. # LLM 大模型配置
  36. # =========================================================================
  37. LLM_MODEL_ID: str = os.getenv("LLM_MODEL_ID", "deepseek-chat")
  38. LLM_API_KEY: str = (os.getenv("LLM_API_KEY") or "").strip()
  39. LLM_BASE_URL: str = os.getenv("LLM_BASE_URL", "https://api.deepseek.com")
  40. LLM_TIMEOUT: int = int(os.getenv("LLM_TIMEOUT", "60"))
  41. # =========================================================================
  42. # 东方财富妙想API配置
  43. # =========================================================================
  44. MX_APIKEY: str = (os.getenv("MX_APIKEY") or "").strip()
  45. MX_API_URL: str = os.getenv("MX_API_URL", "https://mkapi2.dfcfs.com/finskillshub")
  46. # 妙想查询缓存 TTL(秒):行情/指数/资讯/自选股列表等在 TTL 内不走远端妙想;≤0 表示不读缓存但仍写入供额度用尽降级
  47. # 前端仪表盘 localStorage 默认按 600s(10 分钟)对齐;若改大 TTL,宜同步设置 VITE_DASHBOARD_CACHE_MS(毫秒)
  48. MX_CACHE_TTL_SECONDS: float = float(os.getenv("MX_CACHE_TTL_SECONDS", "600"))
  49. # 为 True 时:若 backend/fixtures/mx_raw 下存在对应 query 的原始 JSON,则不调妙想 HTTP(省额度,便于本地修 bug)
  50. MX_REPLAY_FIXTURES: bool = os.getenv("MX_REPLAY_FIXTURES", "").lower() in ("1", "true", "yes")
  51. _mx_fix = os.getenv("MX_FIXTURE_DIR")
  52. MX_FIXTURE_DIR: Path = (
  53. Path(_mx_fix).expanduser().resolve()
  54. if _mx_fix
  55. else (_PROJECT_ROOT / "backend" / "fixtures" / "mx_raw")
  56. )
  57. # =========================================================================
  58. # 项目服务配置
  59. # =========================================================================
  60. BACKEND_HOST: str = os.getenv("BACKEND_HOST", "0.0.0.0")
  61. BACKEND_PORT: int = int(os.getenv("BACKEND_PORT", _DEFAULT_BACKEND_PORT))
  62. BACKEND_DEBUG: bool = os.getenv("BACKEND_DEBUG", "true").lower() == "true"
  63. FRONTEND_PORT: int = int(os.getenv("FRONTEND_PORT", "5173"))
  64. # =========================================================================
  65. # 数据库配置
  66. # =========================================================================
  67. DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./data/stock_analyzer.db")
  68. @property
  69. def DATA_DIR(self) -> Path:
  70. """数据目录(exe 模式下在 exe 旁边)"""
  71. _dd = os.getenv("DATA_DIR")
  72. if _dd:
  73. return Path(_dd)
  74. return _EXTERNAL_DIR / "data"
  75. # =========================================================================
  76. # Redis 配置
  77. # =========================================================================
  78. REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
  79. REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
  80. REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
  81. REDIS_PASSWORD: Optional[str] = os.getenv("REDIS_PASSWORD", None)
  82. # =========================================================================
  83. # JWT 配置
  84. # =========================================================================
  85. JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "dev-secret-key")
  86. JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
  87. JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "1440"))
  88. # =========================================================================
  89. # 项目路径(自动计算)
  90. # =========================================================================
  91. @property
  92. def PROJECT_ROOT(self) -> Path:
  93. return _PROJECT_ROOT
  94. @property
  95. def EXTERNAL_DIR(self) -> Path:
  96. """exe 外部目录(用户数据、配置存放位置)"""
  97. return _EXTERNAL_DIR
  98. @property
  99. def BUNDLE_DIR(self) -> Path:
  100. """打包内部资源目录(exe 内嵌文件所在)"""
  101. return _BUNDLE_DIR
  102. @property
  103. def FRONTEND_DIR(self) -> Path:
  104. """前端静态文件目录"""
  105. # 优先从环境变量指定(开发模式 vite proxy 之外的其他场景)
  106. _fd = os.getenv("FRONTEND_DIR")
  107. if _fd:
  108. return Path(_fd)
  109. # exe 模式:前端 dist 内嵌在 bundle 中
  110. if IS_FROZEN:
  111. return _BUNDLE_DIR / "frontend" / "dist"
  112. # 开发模式:从项目根找 frontend/dist
  113. dist = _PROJECT_ROOT / "frontend" / "dist"
  114. if dist.exists():
  115. return dist
  116. return _PROJECT_ROOT / "frontend" / "dist"
  117. @property
  118. def BACKEND_DIR(self) -> Path:
  119. return _PROJECT_ROOT / "backend"
  120. @property
  121. def AGENTS_DIR(self) -> Path:
  122. return _PROJECT_ROOT / "agents"
  123. @property
  124. def SKILLS_DIR(self) -> Path:
  125. return _PROJECT_ROOT / "skills"
  126. @property
  127. def HELLO_AGENTS_DIR(self) -> Path:
  128. return _PROJECT_ROOT / "HelloAgents Optimized"
  129. # =========================================================================
  130. # 验证方法
  131. # =========================================================================
  132. def validate(self) -> list[str]:
  133. """验证关键配置项,返回缺失配置列表"""
  134. warnings = []
  135. if not self.LLM_API_KEY or self.LLM_API_KEY == "your-api-key-here":
  136. warnings.append("LLM_API_KEY 未配置,智能体功能将不可用")
  137. if not self.MX_APIKEY or self.MX_APIKEY == "your-mx-apikey-here":
  138. warnings.append("MX_APIKEY 未配置,外部金融数据服务将不可用")
  139. return warnings
  140. def is_agent_ready(self) -> bool:
  141. """检查智能体层是否就绪"""
  142. return bool(self.LLM_API_KEY and self.LLM_API_KEY != "your-api-key-here")
  143. def is_skills_ready(self) -> bool:
  144. """检查外部服务层是否就绪"""
  145. return bool(self.MX_APIKEY and self.MX_APIKEY != "your-mx-apikey-here")
  146. # 全局配置单例
  147. settings = Settings()