"""AI 大盘复盘 API — 流式复盘 + 报告持久化。 路由前缀: /api/market-recap 端点: POST /analyze AI 流式大盘复盘(NDJSON) GET /reports 历史复盘列表 POST /reports 保存一条复盘报告 DELETE /reports/{report_id} 删除一条复盘报告 """ from __future__ import annotations import logging from fastapi import APIRouter, HTTPException, Request from fastapi.responses import StreamingResponse from pydantic import BaseModel from app.services import market_recap_reports from app.services.market_recap import recap_market_stream logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/market-recap", tags=["market-recap"]) class AnalyzeRequest(BaseModel): """AI 大盘复盘请求。""" as_of: str | None = None # 可选:复盘日期(YYYY-MM-DD),缺省取最新有数据日 focus: str = "" # 可选:用户追加的复盘关注点 @router.post("/analyze") async def analyze_market(request: Request, req: AnalyzeRequest): """AI 大盘复盘 — NDJSON 流式返回。 装配市场总览(指数/涨跌/连板/封板/板块/情绪雷达)→ 复盘提示词 → 流式调用 LLM → 逐 chunk 以 NDJSON 推给前端(每行一个 JSON)。 协议: {"type":"meta","as_of","emotion_score","emotion_label","summary"} {"type":"delta","content":"..."} {"type":"error","message":"..."} {"type":"done"} """ from datetime import date as date_cls repo = request.app.state.repo quote_service = getattr(request.app.state, "quote_service", None) depth_service = getattr(request.app.state, "depth_service", None) as_of = None if req.as_of: try: as_of = date_cls.fromisoformat(req.as_of) except ValueError: raise HTTPException(400, f"as_of 格式应为 YYYY-MM-DD,收到: {req.as_of}") async def stream_gen(): async for chunk in recap_market_stream(repo, quote_service, depth_service, as_of, req.focus): yield chunk + "\n" return StreamingResponse( stream_gen(), media_type="application/x-ndjson", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) # ================================================================ # 报告 CRUD(历史复盘持久化) # ================================================================ class SaveReportRequest(BaseModel): """保存一条 AI 大盘复盘报告。""" as_of: str focus: str = "" content: str summary: str = "" emotion_score: int | None = None emotion_label: str = "" @router.get("/reports") def list_reports(request: Request): """获取全部历史复盘(按时间降序,后端已裁剪到上限)。""" return {"reports": market_recap_reports.list_reports()} @router.post("/reports") def save_report(request: Request, req: SaveReportRequest): """保存一条复盘报告。""" report = market_recap_reports.save_report({ "as_of": req.as_of, "focus": req.focus, "content": req.content, "summary": req.summary, "emotion_score": req.emotion_score, "emotion_label": req.emotion_label, }) # 推送到飞书(可选): 与定时复盘共用同一开关 review_push_enabled 与 _maybe_push_review。 # 内部 try/except 静默降级, 不影响归档返回值。 from app.jobs.daily_pipeline import _maybe_push_review _maybe_push_review(req.content, { "as_of": req.as_of, "emotion_label": req.emotion_label, }) return {"ok": True, "report": report} @router.delete("/reports/{report_id}") def delete_report(request: Request, report_id: str): """删除一条复盘报告。""" ok = market_recap_reports.delete_report(report_id) return {"ok": ok}