"""AI 分析报告路由(/api/ai-analysis) 提供: - 报告列表 - 报告详情 - 手动触发分析(12小时频率限制) - 重新生成报告 - 检查某日是否有报告 """ import json from datetime import datetime, timezone, timedelta from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse from database import get_connection, dict_from_row _CST = timezone(timedelta(hours=8)) _NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"} router = APIRouter() def _has_ai_report(trade_date: str) -> bool: conn = get_connection() try: row = conn.execute( "SELECT 1 FROM ai_reports WHERE trade_date = ? AND report_type = 'daily' LIMIT 1", (trade_date,) ).fetchone() return row is not None finally: conn.close() def _can_trigger(trade_date: str) -> tuple[bool, str]: """检查是否可以触发分析(12小时频率限制)""" conn = get_connection() try: row = conn.execute( "SELECT created_at FROM ai_reports WHERE trade_date = ? ORDER BY id DESC LIMIT 1", (trade_date,) ).fetchone() if row: last_time = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") now = datetime.now(_CST) if (now - last_time).total_seconds() < 12 * 3600: return False, "12小时内已触发过,请稍后再试" return True, "" finally: conn.close() @router.get("/ai-analysis/latest", summary="最新 AI 分析报告") async def get_latest_report(): conn = get_connection() try: row = conn.execute( "SELECT * FROM ai_reports ORDER BY trade_date DESC, id DESC LIMIT 1" ).fetchone() if not row: raise HTTPException(status_code=404, detail="暂无分析报告") item = dict_from_row(row) item["toolsUsed"] = json.loads(item.pop("tools_used") or "[]") return JSONResponse({"data": item}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.get("/ai-analysis", summary="AI 分析报告列表") async def list_reports(): conn = get_connection() try: rows = conn.execute( "SELECT id, trade_date, report_type, title, summary, tools_used, model, tokens_used, created_at " "FROM ai_reports ORDER BY trade_date DESC, id DESC LIMIT 50" ).fetchall() items = [] for r in rows: item = dict_from_row(r) item["toolsUsed"] = json.loads(item.pop("tools_used") or "[]") items.append(item) return JSONResponse({"data": items}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.get("/ai-analysis/{report_id}", summary="AI 分析报告详情") async def get_report(report_id: int): conn = get_connection() try: row = conn.execute("SELECT * FROM ai_reports WHERE id = ?", (report_id,)).fetchone() if not row: raise HTTPException(status_code=404, detail="报告不存在") item = dict_from_row(row) item["toolsUsed"] = json.loads(item.pop("tools_used") or "[]") return JSONResponse({"data": item}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.get("/ai-analysis/check/{trade_date}", summary="检查某日是否有 AI 分析报告") async def check_report(trade_date: str): has = _has_ai_report(trade_date) return JSONResponse({"data": {"hasReport": has, "tradeDate": trade_date}}) @router.post("/ai-analysis/trigger", summary="手动触发今日 AI 分析") async def trigger_analysis(): now = datetime.now(_CST) trade_date = now.strftime("%Y-%m-%d") if _has_ai_report(trade_date): raise HTTPException(status_code=400, detail="今日已有分析报告") can, msg = _can_trigger(trade_date) if not can: raise HTTPException(status_code=429, detail=msg) from services.ai_service import collect_ai_analysis result = await collect_ai_analysis(trade_date) return JSONResponse({"data": result}) @router.post("/ai-analysis/{report_id}/regenerate", summary="重新生成 AI 分析报告") async def regenerate_report(report_id: int): conn = get_connection() try: row = conn.execute("SELECT trade_date FROM ai_reports WHERE id = ?", (report_id,)).fetchone() if not row: raise HTTPException(status_code=404, detail="报告不存在") trade_date = row["trade_date"] finally: conn.close() can, msg = _can_trigger(trade_date) if not can: raise HTTPException(status_code=429, detail=msg) from services.ai_service import collect_ai_analysis result = await collect_ai_analysis(trade_date) return JSONResponse({"data": result})