110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""routers/paper.py —— 论文撰写路由(对话大脑)。
|
|
|
|
条件注册(PAPER_WRITER_ENABLED 为假不注册,同 bioflow_proxy 模式)。
|
|
端点:health(探活反代)/ turns(一轮状态机)/ confirm / resume / progress / citations。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from ..config import get_settings
|
|
from ..db import Database, get_db
|
|
from ..services.paper import agent
|
|
from ..services.paper.mcp_client import PaperMcpClient
|
|
from ..upstream import envelope
|
|
|
|
router = APIRouter(prefix="/api/paper", tags=["paper"])
|
|
|
|
|
|
class PaperTurnRequest(BaseModel):
|
|
session_id: str
|
|
text: str
|
|
paper_id: str | None = None
|
|
template_key: str = ""
|
|
|
|
|
|
class PaperConfirmRequest(BaseModel):
|
|
session_id: str
|
|
|
|
|
|
def _client() -> PaperMcpClient:
|
|
s = get_settings()
|
|
return PaperMcpClient(s.paper_writer_url, s.paper_writer_read_timeout)
|
|
|
|
|
|
async def _session_owned(db: Database, session_id: str, owner_id: str | None) -> bool:
|
|
if owner_id is None:
|
|
return True
|
|
row = await db.fetchone("SELECT owner_id FROM sessions WHERE id = ?", (session_id,))
|
|
return row is not None and row["owner_id"] == owner_id
|
|
|
|
|
|
@router.get("/health")
|
|
async def paper_health() -> dict[str, Any]:
|
|
"""探活反代 8097 /health;离线返回 offline 标记(前端据此隐藏 paper 入口)。"""
|
|
s = get_settings()
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
resp = await client.get(f"{s.paper_writer_url.rstrip('/')}/health")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception: # noqa: BLE001
|
|
return {"ok": False, "status": "offline"}
|
|
|
|
|
|
@router.post("/turns")
|
|
async def paper_turn(payload: PaperTurnRequest, request: Request, db: Database = Depends(get_db)) -> dict[str, Any]:
|
|
user = getattr(request.state, "user", None)
|
|
if user and not await _session_owned(db, payload.session_id, user["id"]):
|
|
return JSONResponse(status_code=404, content=envelope("NOT_FOUND", message="会话不存在", status=404)[0])
|
|
blocks = await agent.handle_turn(db, payload.session_id, payload.text, payload.paper_id, payload.template_key)
|
|
return {"ok": True, "blocks": blocks}
|
|
|
|
|
|
@router.post("/papers/{paper_id}/confirm")
|
|
async def paper_confirm(paper_id: str, payload: PaperConfirmRequest, request: Request, db: Database = Depends(get_db)) -> dict[str, Any]:
|
|
user = getattr(request.state, "user", None)
|
|
if user and not await _session_owned(db, payload.session_id, user["id"]):
|
|
return JSONResponse(status_code=404, content=envelope("NOT_FOUND", message="会话不存在", status=404)[0])
|
|
asyncio.create_task(agent.run_writing_loop(db, paper_id, payload.session_id))
|
|
return {"ok": True, "paper_id": paper_id}
|
|
|
|
|
|
@router.post("/papers/{paper_id}/resume")
|
|
async def paper_resume(paper_id: str, payload: PaperConfirmRequest, request: Request, db: Database = Depends(get_db)) -> dict[str, Any]:
|
|
user = getattr(request.state, "user", None)
|
|
if user and not await _session_owned(db, payload.session_id, user["id"]):
|
|
return JSONResponse(status_code=404, content=envelope("NOT_FOUND", message="会话不存在", status=404)[0])
|
|
asyncio.create_task(agent.run_writing_loop(db, paper_id, payload.session_id))
|
|
return {"ok": True, "paper_id": paper_id}
|
|
|
|
|
|
@router.get("/papers/{paper_id}")
|
|
async def paper_progress(paper_id: str) -> dict[str, Any]:
|
|
client = _client()
|
|
try:
|
|
return await client.call_tool("get_paper_progress", {"paper_id": paper_id})
|
|
except Exception as exc: # noqa: BLE001
|
|
return {"ok": False, "code": "UPSTREAM_DOWN", "message": str(exc)}
|
|
|
|
|
|
@router.get("/papers/{paper_id}/citations/{n}")
|
|
async def paper_citation(paper_id: str, n: int) -> dict[str, Any]:
|
|
client = _client()
|
|
try:
|
|
paper = await client.call_tool("get_paper", {"paper_id": paper_id})
|
|
except Exception as exc: # noqa: BLE001
|
|
return {"ok": False, "code": "UPSTREAM_DOWN", "message": str(exc)}
|
|
if not paper.get("ok"):
|
|
return paper
|
|
for ref in paper.get("references") or []:
|
|
if ref.get("n") == n:
|
|
return {"ok": True, "citation": ref}
|
|
return {"ok": False, "code": "NOT_FOUND", "message": f"引用 [{n}] 不存在"}
|