- 后端:AI分析服务、工具调用、定时任务 - 前端:报告列表、详情页、Mermaid图表支持 - 支持OpenAI API兼容模型 - 收盘后自动分析生成报告
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""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", 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})
|