rebuild(stage-12): deliver private review workflows
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Path, Query, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from backend.features.accounts.auth import (
|
||||
AuthenticatedPrincipal,
|
||||
CsrfPrincipal,
|
||||
SmartAccessPrincipal,
|
||||
SmartWritePrincipal,
|
||||
)
|
||||
from backend.features.review.schemas import (
|
||||
AlertInput,
|
||||
AssistantInput,
|
||||
NoteInput,
|
||||
TradeInput,
|
||||
WatchInput,
|
||||
WatchRemarkInput,
|
||||
)
|
||||
from backend.features.review.service import ReviewError
|
||||
from backend.http.errors import AppError
|
||||
from backend.llm.gateway import LLMGatewayError
|
||||
|
||||
router = APIRouter(prefix="/review", tags=["review"])
|
||||
|
||||
|
||||
@router.get("", response_model=dict)
|
||||
def workspace(
|
||||
request: Request,
|
||||
principal: AuthenticatedPrincipal,
|
||||
trade_date: Annotated[str, Query(alias="date")],
|
||||
) -> dict:
|
||||
return _call(request, "workspace", principal, trade_date)
|
||||
|
||||
|
||||
@router.post("/watchlist", response_model=dict)
|
||||
def add_watch(payload: WatchInput, request: Request, principal: CsrfPrincipal) -> dict:
|
||||
return _call(request, "add_watch", principal, payload.identifier)
|
||||
|
||||
|
||||
@router.patch("/watchlist/{identifier}/remark", response_model=dict)
|
||||
def save_watch_remark(
|
||||
payload: WatchRemarkInput,
|
||||
request: Request,
|
||||
principal: CsrfPrincipal,
|
||||
identifier: Annotated[str, Path(min_length=1, max_length=40)],
|
||||
) -> dict:
|
||||
_call(request, "save_watch_remark", principal, identifier, payload.remark)
|
||||
return {"message": "跟踪备注已保存。"}
|
||||
|
||||
|
||||
@router.delete("/watchlist/{identifier}", response_model=dict)
|
||||
def delete_watch(
|
||||
request: Request,
|
||||
principal: CsrfPrincipal,
|
||||
identifier: Annotated[str, Path(min_length=1, max_length=40)],
|
||||
) -> dict:
|
||||
_call(request, "delete_watch", principal, identifier)
|
||||
return {"message": "已移出自选。"}
|
||||
|
||||
|
||||
@router.put("/notes", response_model=dict)
|
||||
def save_note(payload: NoteInput, request: Request, principal: CsrfPrincipal) -> dict:
|
||||
identifier = _call(request, "save_note", principal, payload.model_dump())
|
||||
return {"id": identifier}
|
||||
|
||||
|
||||
@router.get("/stock-notes/{code}", response_model=list[dict])
|
||||
def stock_notes(
|
||||
request: Request,
|
||||
principal: AuthenticatedPrincipal,
|
||||
code: Annotated[str, Path(min_length=1, max_length=12)],
|
||||
) -> list[dict]:
|
||||
return _call(request, "stock_notes", principal, code)
|
||||
|
||||
|
||||
@router.delete("/notes/{note_id}", response_model=dict)
|
||||
def delete_note(
|
||||
request: Request,
|
||||
principal: CsrfPrincipal,
|
||||
note_id: Annotated[int, Path(gt=0)],
|
||||
) -> dict:
|
||||
_call(request, "delete_note", principal, note_id)
|
||||
return {"message": "复盘记录已删除。"}
|
||||
|
||||
|
||||
@router.put("/trades", response_model=dict)
|
||||
def save_trade(payload: TradeInput, request: Request, principal: CsrfPrincipal) -> dict:
|
||||
identifier = _call(request, "save_trade", principal, payload.model_dump())
|
||||
return {"id": identifier}
|
||||
|
||||
|
||||
@router.delete("/trades/{trade_id}", response_model=dict)
|
||||
def delete_trade(
|
||||
request: Request,
|
||||
principal: CsrfPrincipal,
|
||||
trade_id: Annotated[int, Path(gt=0)],
|
||||
) -> dict:
|
||||
_call(request, "delete_trade", principal, trade_id)
|
||||
return {"message": "交易记录已删除。"}
|
||||
|
||||
|
||||
@router.get("/alerts", response_model=dict)
|
||||
def alerts(
|
||||
request: Request,
|
||||
principal: AuthenticatedPrincipal,
|
||||
unread: Annotated[bool, Query()] = False,
|
||||
) -> dict:
|
||||
return _call(request, "alert_center", principal, unread)
|
||||
|
||||
|
||||
@router.post("/alerts", response_model=dict)
|
||||
def create_alert(payload: AlertInput, request: Request, principal: CsrfPrincipal) -> dict:
|
||||
identifier = _call(request, "create_alert", principal, payload.model_dump())
|
||||
return {"id": identifier}
|
||||
|
||||
|
||||
@router.patch("/alerts/read-all", response_model=dict)
|
||||
def mark_all_alerts(request: Request, principal: CsrfPrincipal) -> dict:
|
||||
return {"updated": _call(request, "mark_all_alerts", principal)}
|
||||
|
||||
|
||||
@router.patch("/alerts/{alert_id}/read", response_model=dict)
|
||||
def mark_alert(
|
||||
request: Request,
|
||||
principal: CsrfPrincipal,
|
||||
alert_id: Annotated[int, Path(gt=0)],
|
||||
) -> dict:
|
||||
_call(request, "mark_alert", principal, alert_id)
|
||||
return {"message": "提醒已读。"}
|
||||
|
||||
|
||||
@router.delete("/alerts/{alert_id}", response_model=dict)
|
||||
def delete_alert(
|
||||
request: Request,
|
||||
principal: CsrfPrincipal,
|
||||
alert_id: Annotated[int, Path(gt=0)],
|
||||
) -> dict:
|
||||
_call(request, "delete_alert", principal, alert_id)
|
||||
return {"message": "提醒已删除。"}
|
||||
|
||||
|
||||
@router.get("/assistant/messages", response_model=list[dict])
|
||||
def assistant_messages(request: Request, principal: SmartAccessPrincipal) -> list[dict]:
|
||||
return _call(request, "messages", principal)
|
||||
|
||||
|
||||
@router.delete("/assistant/messages", response_model=dict)
|
||||
def clear_assistant_messages(request: Request, principal: SmartWritePrincipal) -> dict:
|
||||
return {"deleted": _call(request, "clear_messages", principal)}
|
||||
|
||||
|
||||
@router.post("/assistant/chat")
|
||||
def assistant_chat(
|
||||
payload: AssistantInput,
|
||||
request: Request,
|
||||
principal: SmartWritePrincipal,
|
||||
) -> StreamingResponse:
|
||||
prepared = _call(request, "prepare_assistant", principal, payload.trade_date, payload.question)
|
||||
|
||||
def body() -> Iterator[bytes]:
|
||||
for event in request.app.state.container.review.stream_assistant(prepared):
|
||||
yield (json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n").encode()
|
||||
|
||||
return StreamingResponse(
|
||||
body(),
|
||||
media_type="application/x-ndjson",
|
||||
headers={"Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
|
||||
def _call(request: Request, method: str, *args):
|
||||
try:
|
||||
return getattr(request.app.state.container.review, method)(*args)
|
||||
except ReviewError as exc:
|
||||
raise AppError("review_unavailable", str(exc), 409) from exc
|
||||
except LLMGatewayError as exc:
|
||||
status = 403 if exc.code in {"membership_required", "quota_exhausted"} else 503
|
||||
raise AppError(exc.code, str(exc), status) from exc
|
||||
Reference in New Issue
Block a user