repo_description: LLM驱动的 A/H/美股智能分析器：多数据源行情 + 实时新闻 + LLM决策仪表盘 + 多渠道推送，零成本定时运行，纯白嫖. LLM-powered stock analysis system for A/H/US markets.
repo_topics: agent ai aigc gemini llm quant quantitative-trading rag stock
code: # -*- coding: utf-8 -*-
  """
  ===================================
  FastAPI 应用工厂模块
  ===================================
  
  职责：
  1. 创建和配置 FastAPI 应用实例
  2. 配置 CORS 中间件
  3. 注册路由和异常处理器
  4. 托管前端静态文件（生产模式）
  
  使用方式：
      from api.app import create_app
      app = create_app()
  """
  
  import mimetypes
  import os
  from contextlib import asynccontextmanager
  from datetime import datetime
  from pathlib import Path
  from typing import Optional
  
  from fastapi import FastAPI, Request
  from fastapi.middleware.cors import CORSMiddleware
  from fastapi.staticfiles import StaticFiles
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
  
  from api.v1 import api_v1_router
  from api.middlewares.auth import add_auth_middleware
  from api.middlewares.error_handler import add_error_handlers
  from api.v1.schemas.common import HealthResponse
  from src.services.system_config_service import SystemConfigService
  
  
  @asynccontextmanager
  async def app_lifespan(app: FastAPI):
      """Initialize and release shared services for the app lifecycle."""
      app.state.system_config_service = SystemConfigService()
      try:
          yield
      finally:
          if hasattr(app.state, "system_config_service"):
              delattr(app.state, "system_config_service")
  
  
  def create_app(static_dir: Optional[Path] = None) -> FastAPI:
      """
      创建并配置 FastAPI 应用实例
      
      Args:
          static_dir: 静态文件目录路径（可选，默认为项目根目录下的 static）
          
      Returns:
          配置完成的 FastAPI 应用实例
      """
      # 默认静态文件目录
      if static_dir is None:
          static_dir = Path(__file__).parent.parent / "static"
      
      # 创建 FastAPI 实例
      app = FastAPI(
          title="Daily Stock Analysis API",
          description=(
              "A股/港股/美股自选股智能分析系统 API\n\n"
              "## 功能模块\n"
              "- 股票分析：触发 AI 智能分析\n"
              "- 历史记录：查询历史分析报告\n"
              "- 股票数据：获取行情数据\n\n"
              "## 认证方式\n"
              "支持可选的运行时认证（通过 WebUI 设置页面启用/关闭）"
          ),
          version="1.0.0",
          lifespan=app_lifespan,
      )
      
      # ============================================================
      # CORS 配置
      # ============================================================
      
      allowed_origins = [
          "http://localhost:5173",
          "http://127.0.0.1:5173",
          "http://localhost:3000",
          "http://127.0.0.1:3000",
      ]
      
      # 从环境变量添加额外的允许来源
      extra_origins = os.environ.get("CORS_ORIGINS", "")
      if extra_origins:
          allowed_origins.extend([o.strip() for o in extra_origins.split(",") if o.strip()])
      
      # 允许所有来源（开发/演示用）
      allow_all_origins = os.environ.get("CORS_ALLOW_ALL", "").lower() == "true"
      allow_credentials = not allow_all_origins
      if allow_all_origins:
          allowed_origins = ["*"]
      
      app.add_middleware(
          CORSMiddleware,
          allow_origins=allowed_origins,
          allow_credentials=allow_credentials,
          allow_methods=["*"],
          allow_headers=["*"],
      )
  
      add_auth_middleware(app)
      
      # ============================================================
      # 注册路由
      # ============================================================
      
      app.include_router(api_v1_router)
      add_error_handlers(app)
      
      # ============================================================
      # 根路由和健康检查
      # ============================================================
      
      has_frontend = static_dir.exists() and (static_dir / "index.html").exists()
      
      if has_frontend:
          @app.get("/", include_in_schema=False)
          async def root():
              """根路由 - 返回前端页面"""
              return FileResponse(static_dir / "index.html")
      else:
          _FRONTEND_NOT_BUILT_HTML = """<!DOCTYPE html>
  <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  <title>DSA - Frontend Not Built</title>
  <style>
    *{margin:0;padding:0;box-sizing:border-box}
    body{min-height:100vh;display:flex;align-items:center;justify-content:center;
         background:#0a0e17;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,monospace}
    .card{max-width:580px;padding:2.5rem;border:1px solid #1e293b;border-radius:12px;background:#111827}
    h1{font-size:1.25rem;color:#38bdf8;margin-bottom:.75rem}
    p{font-size:.9rem;line-height:1.7;color:#94a3b8;margin-bottom:.5rem}
    code{background:#1e293b;padding:2px 8px;border-radius:4px;font-size:.85rem;color:#67e8f9}
    .hint{margin-top:1.25rem;padding:.75rem 1rem;border-left:3px solid #f59e0b;background:#1c1917;border-radius:0 6px 6px 0}
    .hint p{color:#fbbf24;margin:0}
    a{color:#38bdf8;text-decoration:none}
    a:hover{text-decoration:underline}
    .status{margin-top:1rem;font-size:.8rem;color:#475569}
  </style></head><body><div class="card">
  <h1>&#9888;&#65039; Frontend Not Built</h1>
  <p>API is running, but the Web UI has not been built yet.</p>
  <p>Build the frontend first:</p>
  <p><code>cd apps/dsa-web &amp;&amp; npm install &amp;&amp; npm run build</code></p>
  <p>Or start with auto-build:</p>
  <p><code>python main.py --serve-only</code></p>
  <div class="hint"><p>If you only need the API, visit <a href="/docs">/docs</a> for the interactive API documentation.</p></div>
  <p class="status">API Version 1.0.0 &bull; <a href="/api/health">/api/health</a></p>
  </div></body></html>"""
  
          @app.get("/", include_in_schema=False)
          async def root():
              """根路由 - 前端未构建时返回引导页面"""
              return HTMLResponse(content=_FRONTEND_NOT_BUILT_HTML)
      
      @app.get(
          "/api/health",
          response_model=HealthResponse,
          tags=["Health"],
          summary="健康检查",
          description="用于负载均衡器或监控系统检查服务状态"
      )
      async def health_check() -> HealthResponse:
          """健康检查接口"""
          return HealthResponse(
              status="ok",
              timestamp=datetime.now().isoformat()
          )
      
      # ============================================================
      # 静态文件托管（前端 SPA）
      # ============================================================
      
      if has_frontend:
          # 挂载静态资源目录
          assets_dir = static_dir / "assets"
          if assets_dir.exists():
              app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
          
          # SPA 路由回退
          @app.get("/{full_path:path}", include_in_schema=False)
          async def serve_spa(request: Request, full_path: str):
              """SPA 路由回退 - 非 API 路由返回 index.html"""
              if full_path == "api" or full_path.startswith("api/"):
                  return JSONResponse(
                      status_code=404,
                      content={"error": "not_found", "message": f"API endpoint /{full_path} not found"}
                  )
              
              file_path = static_dir / full_path
              if file_path.exists() and file_path.is_file():
                  # Issue #520: Explicitly resolve MIME type to avoid
                  # browsers rejecting JS modules served as text/plain.
                  content_type, _ = mimetypes.guess_type(str(file_path))
                  return FileResponse(file_path, media_type=content_type)
              
              return FileResponse(static_dir / "index.html")
      
      return app
  
  
  # 默认应用实例（供 uvicorn 直接使用）
  app = create_app()
  