"""中控 AI FastAPI 路由。""" from __future__ import annotations import asyncio from typing import Callable from fastapi import APIRouter, Body, File, Form, HTTPException, UploadFile from pydantic import BaseModel, Field from hub_ai.archive_quote import send_archive_quote_review from hub_ai.chat import ( get_chat_state, remove_chat_session, send_chat_message, start_new_chat, switch_chat_session, ) from hub_ai.client import model_label from hub_ai.config import trading_day_reset_hour from hub_ai.context import build_daily_context from hub_ai.store import get_latest_summary, list_summaries from hub_ai.summary import generate_daily_summary from hub_trades_lib import current_trading_day class ChatSendBody(BaseModel): message: str = "" trading_day: str = "" class SummaryGenerateBody(BaseModel): trading_day: str = "" force: bool = False class ChatNewBody(BaseModel): trading_day: str = "" bot_mode: str = "trading" class ChatSwitchBody(BaseModel): session_id: str = Field(..., min_length=1) class ArchiveQuoteChatBody(BaseModel): quote_date: str = "" content: str = "" def create_hub_ai_router(*, load_all_exchanges: Callable[[], list]) -> APIRouter: router = APIRouter(prefix="/api/ai", tags=["hub-ai"]) def _day(raw: str = "") -> str: d = (raw or "").strip()[:10] return d or current_trading_day(reset_hour=trading_day_reset_hour()) @router.get("/meta") def api_ai_meta(): return { "ok": True, "model": model_label(), "trading_day_reset_hour": trading_day_reset_hour(), "trading_day": current_trading_day(reset_hour=trading_day_reset_hour()), "storage": { "summaries": "hub_ai_summaries.json", "chat": "hub_ai_chat.json", }, } @router.get("/context") def api_ai_context(trading_day: str = ""): exchanges = load_all_exchanges() ctx = build_daily_context(exchanges, trading_day=_day(trading_day)) return {"ok": True, **ctx} @router.get("/summary") def api_ai_summary_list(trading_day: str = ""): day = _day(trading_day) if trading_day.strip() else "" items = list_summaries(trading_day=day or None, limit=20) latest = get_latest_summary(_day(trading_day)) if trading_day.strip() else ( items[0] if items else None ) return { "ok": True, "trading_day": _day(trading_day) if trading_day.strip() else None, "summaries": items, "latest": latest, "model": model_label(), } @router.post("/summary/generate") def api_ai_summary_generate(body: SummaryGenerateBody = SummaryGenerateBody()): exchanges = load_all_exchanges() result = generate_daily_summary( exchanges, trading_day=_day(body.trading_day) if body.trading_day.strip() else None, force=bool(body.force), ) if not result.get("ok"): raise HTTPException(status_code=502, detail=result.get("msg") or "生成失败") result.pop("context", None) return result @router.get("/chat/session") def api_ai_chat_session(): state = get_chat_state() return {"ok": True, **state, "model": model_label()} @router.post("/chat/new") def api_ai_chat_new(body: ChatNewBody = ChatNewBody()): day = _day(body.trading_day) return start_new_chat(trading_day=day, bot_mode=body.bot_mode or "trading") @router.post("/chat/switch") def api_ai_chat_switch(body: ChatSwitchBody): try: return switch_chat_session(body.session_id.strip()) except KeyError: raise HTTPException(status_code=404, detail="会话不存在") @router.delete("/chat/session/{session_id}") def api_ai_chat_delete(session_id: str): result = remove_chat_session(session_id.strip()) if not result.get("ok"): raise HTTPException(status_code=404, detail="会话不存在") return result @router.post("/chat/archive-quote") def api_ai_chat_archive_quote(body: ArchiveQuoteChatBody = Body(...)): result = send_archive_quote_review( quote_date=body.quote_date, content=body.content, ) if not result.get("ok"): raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败") return result @router.post("/chat/send") async def api_ai_chat_send( message: str = Form(""), trading_day: str = Form(""), files: list[UploadFile] = File(default=[]), ): exchanges = load_all_exchanges() raw_attachments = [] for f in files or []: if not f or not f.filename: continue data = await f.read() raw_attachments.append( { "filename": f.filename, "content_type": f.content_type or "", "data": data, } ) result = await asyncio.to_thread( send_chat_message, exchanges, message, trading_day=_day(trading_day) if trading_day.strip() else None, raw_attachments=raw_attachments, ) if not result.get("ok"): raise HTTPException(status_code=502, detail=result.get("msg") or "发送失败") return result return router