feat: AI市场分析功能
- 后端:AI分析服务、工具调用、定时任务 - 前端:报告列表、详情页、Mermaid图表支持 - 支持OpenAI API兼容模型 - 收盘后自动分析生成报告
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""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})
|
||||
Reference in New Issue
Block a user