- paper_writer_service(8097):FastAPI + MCP streamable-http 单端点,9 工具
(propose_paper_outline/write_paper_section/get_paper_progress/search_refs/
revise_paper_section/revise_paper/get_paper/complete_paper/cancel_paper),
SQLite 三表(papers/paper_sections/citations,WAL),GB/T 7714 引用协议
- BFF:services/paper/{mcp_client,agent}.py(裸 JSON-RPC 客户端 + 规则状态机)、
routers/paper.py、schemas.py 4 个 Paper*Block + ArtifactBlock 加 paper_id/citation_count、
config.py 开关、main.py 条件注册、docgen.py 抽 insert_artifact 公共 helper
- 前端:intent.js paperIntent 纯函数、Composer docgen 分支 sub-route、sendPaper/轮询、
PaperBriefCard/PaperOutlineCard/PaperProgressCard、DocPane cite-chip
- deploy/paper_restart.sh + toggle_paper.sh;compile_sdk.sh 白名单追加 paper 文件
- tests/paper 26 条(工具逻辑 + MCP 协议 + BFF 状态机 + 块 schema + 前端 intent 单测)
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
"""main.py —— paper 服务应用入口(uvicorn 入口,明文白名单文件)。
|
||
|
||
- 独立部署树 paper_writer_service/(FR:独立进程 8097 + 独立 SQLite paper.db)。
|
||
- lifespan:init_db(幂等建表)→ 显式进入 MCP session_manager.run()(Starlette 不自动
|
||
运行子 Mount 的 lifespan,streamable-http 处理请求时要求 task group 已初始化)→ 清理。
|
||
- 端点:/health(GET,BFF 探活反代)+ /mcp(MCP streamable-http)。
|
||
- 不依赖 BFF/生产服务(8090 等绝不探测/连接)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
|
||
from .config import get_settings
|
||
from .db import close_db, db, init_db
|
||
from .mcp import paper_mcp
|
||
from .services.paper import search
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# MCP streamable-http app(调用即创建 session_manager,供 lifespan 进入 run())。
|
||
# 端点地址:/mcp(streamable_http_path="/" + 挂载前缀 /mcp)。
|
||
_paper_mcp_app = paper_mcp.streamable_http_app(streamable_http_path="/")
|
||
|
||
|
||
def create_app() -> FastAPI:
|
||
settings = get_settings()
|
||
app = FastAPI(title=settings.app_name, version=settings.version)
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=False,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
@asynccontextmanager
|
||
async def _lifespan(_app: FastAPI):
|
||
# 测试注入:db 已由测试预置(init_db(tmp_path)),跳过启动初始化。
|
||
try:
|
||
await init_db()
|
||
except Exception:
|
||
logger.exception("main: 数据库初始化失败(health 将报 db 降级)")
|
||
|
||
# MCP session manager 的 task group 必须在服务运行期内保持活跃。
|
||
async with paper_mcp.session_manager.run():
|
||
try:
|
||
yield
|
||
finally:
|
||
try:
|
||
await close_db()
|
||
except Exception:
|
||
pass
|
||
await search.close_client()
|
||
|
||
app.router.lifespan_context = _lifespan
|
||
|
||
@app.get("/health")
|
||
async def health() -> dict[str, object]:
|
||
db_ok = True
|
||
db_error: str | None = None
|
||
try:
|
||
await db.fetchone("SELECT 1 AS ok")
|
||
except Exception as exc: # noqa: BLE001
|
||
db_ok = False
|
||
db_error = str(exc)
|
||
return {
|
||
"ok": True,
|
||
"status": "ok" if db_ok else "degraded",
|
||
"db": {"connected": db_ok, "error": db_error},
|
||
"version": settings.version,
|
||
}
|
||
|
||
# paper MCP 能力外挂(mcp 2.x:MCPServer.streamable_http_app)。
|
||
app.mount("/mcp", _paper_mcp_app)
|
||
return app
|
||
|
||
|
||
app = create_app()
|