main.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. """
  2. 智能股票分析助手 — FastAPI 应用入口
  3. 启动方法(从项目根目录执行):
  4. python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload
  5. exe 打包后直接运行:
  6. stock_analyzer.exe
  7. 浏览器访问 http://127.0.0.1:5174/dashboard(端口以 exe 旁 .env 中 BACKEND_PORT 为准)
  8. """
  9. import sys
  10. import io
  11. from pathlib import Path
  12. # Windows 控制台编码修复:强制使用 UTF-8 输出,避免 emoji/中文打印报错
  13. if sys.stdout.encoding != 'utf-8':
  14. try:
  15. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
  16. sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
  17. except Exception:
  18. pass
  19. # 将项目关键目录加入sys.path,确保各类导入正常工作
  20. # main.py 位于 backend/app/main.py,需要向上3级到项目根目录
  21. _PROJECT_ROOT = Path(__file__).parent.parent.parent
  22. _BACKEND_DIR = _PROJECT_ROOT / "backend" # 使 from app.xxx 导入生效
  23. _AGENTS_DIR = _PROJECT_ROOT / "agents" # 使 from agents.xxx 导入生效
  24. _HELLO_DIR = _PROJECT_ROOT / "HelloAgents Optimized" # 使 from hello_agents 导入生效
  25. for p in [_BACKEND_DIR, _PROJECT_ROOT, _AGENTS_DIR, _HELLO_DIR]:
  26. if str(p) not in sys.path:
  27. sys.path.insert(0, str(p))
  28. from fastapi import FastAPI
  29. from fastapi.middleware.cors import CORSMiddleware
  30. from fastapi.staticfiles import StaticFiles
  31. from fastapi.responses import FileResponse
  32. import asyncio
  33. from contextlib import asynccontextmanager
  34. from app.config import settings
  35. from app.utils.response import success_response, error_response
  36. from app.models.database import init_db, close_db
  37. from app.models.report import AnalysisReport # noqa: F401 — 确保数据库初始化时创建表
  38. from app.models.history_models import AnalysisHistory # noqa: F401 — 确保历史记录表创建
  39. @asynccontextmanager
  40. async def lifespan(app: FastAPI):
  41. """应用生命周期管理"""
  42. # 启动时执行
  43. print(f"[后端] 智能股票分析助手启动中...")
  44. warnings = settings.validate()
  45. if warnings:
  46. print("[后端] ⚠️ 配置警告:")
  47. for w in warnings:
  48. print(f" - {w}")
  49. else:
  50. print("[后端] ✅ 配置验证通过")
  51. print(f"[后端] 服务地址: http://{settings.BACKEND_HOST}:{settings.BACKEND_PORT}")
  52. # 初始化数据库(创建表)
  53. await init_db()
  54. print("[后端] ✅ 数据库初始化完成")
  55. # 后台预热仪表盘所需妙想缓存(不阻塞本 worker 接受请求)
  56. async def _warm_dashboard_cache_bg() -> None:
  57. try:
  58. from app.services.dashboard_warmup import warm_dashboard_cache
  59. await asyncio.to_thread(warm_dashboard_cache)
  60. print("[后端] ✅ 仪表盘数据预热已完成(进程内妙想缓存已填充)")
  61. except Exception as exc:
  62. print(f"[后端] ⚠️ 仪表盘预热未完成(可忽略): {exc}")
  63. asyncio.create_task(_warm_dashboard_cache_bg())
  64. yield
  65. # 关闭时执行
  66. await close_db()
  67. print("[后端] 服务关闭")
  68. # 创建FastAPI应用实例
  69. app = FastAPI(
  70. title="智能股票分析助手",
  71. description="基于多智能体架构的A股投资分析工具API",
  72. version="0.1.0",
  73. lifespan=lifespan,
  74. )
  75. # 是否托管 Vue 构建产物(exe 一体化或设置 FRONTEND_DIR 且存在 dist)
  76. _FRONTEND_DIR = settings.FRONTEND_DIR
  77. _SERVE_FRONTEND = _FRONTEND_DIR.exists() and (_FRONTEND_DIR / "index.html").exists()
  78. # SPA 入口禁用强缓存:避免升级 exe 后浏览器仍用旧 index 引用已过期的 hash chunk
  79. _SPA_INDEX_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate", "Pragma": "no-cache"}
  80. # =========================================================================
  81. # CORS 跨域中间件(允许前端Vue3开发服务器访问)
  82. # =========================================================================
  83. app.add_middleware(
  84. CORSMiddleware,
  85. allow_origins=[
  86. f"http://localhost:{settings.FRONTEND_PORT}",
  87. "http://127.0.0.1:5173",
  88. f"http://127.0.0.1:{settings.BACKEND_PORT}",
  89. "*", # 开发阶段允许所有来源
  90. ],
  91. allow_credentials=True,
  92. allow_methods=["*"],
  93. allow_headers=["*"],
  94. )
  95. # =========================================================================
  96. # 系统路由
  97. # =========================================================================
  98. @app.get("/api/v1/system/health", tags=["系统"])
  99. async def health_check():
  100. """健康检查接口"""
  101. return success_response(
  102. data={
  103. "status": "ok",
  104. "version": "0.1.0",
  105. "agent_ready": settings.is_agent_ready(),
  106. "skills_ready": settings.is_skills_ready(),
  107. }
  108. )
  109. @app.get("/api/v1/system/config", tags=["系统"])
  110. async def system_config():
  111. """获取系统配置(公开信息,不包含密钥)"""
  112. return success_response(
  113. data={
  114. "llm_model": settings.LLM_MODEL_ID,
  115. "agent_ready": settings.is_agent_ready(),
  116. "skills_ready": settings.is_skills_ready(),
  117. "frontend_port": settings.FRONTEND_PORT,
  118. }
  119. )
  120. @app.get("/", tags=["系统"])
  121. async def root():
  122. """根路径:托管前端时返回 index.html(与 vite dev 一致);否则返回 API 说明"""
  123. if _SERVE_FRONTEND:
  124. return FileResponse(str(_FRONTEND_DIR / "index.html"), headers=dict(_SPA_INDEX_HEADERS))
  125. return {"message": "智能股票分析助手 API", "docs": "/docs"}
  126. # =========================================================================
  127. # 注册子路由
  128. # =========================================================================
  129. from app.api.preferences import router as preferences_router
  130. from app.api.market import router as market_router
  131. from app.api.financial import router as financial_router
  132. from app.api.news import router as news_router
  133. from app.api.screener import router as screener_router
  134. from app.api.analysis import router as analysis_router
  135. from app.api.watchlist import router as watchlist_router
  136. from app.api.buffett import router as buffett_router
  137. from app.api.simulation import router as simulation_router
  138. from app.api.chat import router as chat_router
  139. from app.api.history import router as history_router
  140. from app.api.agent_api import router as agent_router
  141. from app.api.sentiment import router as sentiment_router
  142. from app.api.data_analysis import router as data_analysis_router
  143. from app.api.cache_api import router as cache_router
  144. from app.api.system_browser import router as system_browser_router
  145. app.include_router(preferences_router, prefix="/api/v1")
  146. app.include_router(market_router, prefix="/api/v1")
  147. app.include_router(financial_router, prefix="/api/v1")
  148. app.include_router(news_router, prefix="/api/v1")
  149. app.include_router(screener_router, prefix="/api/v1")
  150. app.include_router(analysis_router, prefix="/api/v1")
  151. app.include_router(watchlist_router, prefix="/api/v1")
  152. app.include_router(buffett_router, prefix="/api/v1")
  153. app.include_router(simulation_router, prefix="/api/v1")
  154. app.include_router(chat_router, prefix="/api/v1")
  155. app.include_router(history_router, prefix="/api/v1")
  156. app.include_router(agent_router, prefix="/api/v1")
  157. app.include_router(sentiment_router, prefix="/api/v1")
  158. app.include_router(data_analysis_router, prefix="/api/v1")
  159. app.include_router(cache_router, prefix="/api/v1")
  160. app.include_router(system_browser_router, prefix="/api/v1")
  161. # =========================================================================
  162. # 前端静态文件服务(exe 模式或 FRONTEND_DIR 指定时启用)
  163. # =========================================================================
  164. if _SERVE_FRONTEND:
  165. # 挂载 assets 等静态资源(文件名带 content hash,可由浏览器长期缓存)
  166. _assets_dir = _FRONTEND_DIR / "assets"
  167. if _assets_dir.exists():
  168. app.mount("/assets", StaticFiles(directory=str(_assets_dir)), name="frontend_assets")
  169. # SPA 回退:非 /api 路径返回 index.html(含禁止缓存响应头,见 _SPA_INDEX_HEADERS)
  170. @app.get("/{full_path:path}", tags=["前端"])
  171. async def serve_spa(full_path: str = ""):
  172. fp = _FRONTEND_DIR / full_path
  173. if full_path and fp.exists() and fp.is_file():
  174. return FileResponse(str(fp))
  175. return FileResponse(str(_FRONTEND_DIR / "index.html"), headers=dict(_SPA_INDEX_HEADERS))
  176. # =========================================================================
  177. # exe 独立入口
  178. # =========================================================================
  179. def start_server(host: str = None, port: int = None):
  180. """启动 uvicorn 服务器(供 exe 入口调用)"""
  181. import uvicorn
  182. uvicorn.run(
  183. app,
  184. host=host or settings.BACKEND_HOST,
  185. port=port or settings.BACKEND_PORT,
  186. log_level="info",
  187. )
  188. if __name__ == "__main__":
  189. import uvicorn
  190. uvicorn.run(
  191. "backend.app.main:app",
  192. host=settings.BACKEND_HOST,
  193. port=settings.BACKEND_PORT,
  194. reload=settings.BACKEND_DEBUG,
  195. )