feat(server): 暴露 /api/sessions/{sid}/ws 进度流端点(+websockets 依赖)
This commit is contained in:
@@ -150,6 +150,7 @@ python scripts/serve.py
|
|||||||
- 分步端点(聊天底层复用):`POST .../start-parse`、`POST .../confirm-parse`、`POST .../start-impact`、
|
- 分步端点(聊天底层复用):`POST .../start-parse`、`POST .../confirm-parse`、`POST .../start-impact`、
|
||||||
`POST .../confirm-impact`、`POST .../generate`(body `{"output_language":"auto|zh|ja"}`)、`POST .../run-qa`
|
`POST .../confirm-impact`、`POST .../generate`(body `{"output_language":"auto|zh|ja"}`)、`POST .../run-qa`
|
||||||
- 结果:`GET .../result/preview|download|impact-report|qa-report`
|
- 结果:`GET .../result/preview|download|impact-report|qa-report`
|
||||||
|
- 进度流:`GET /api/sessions/{id}/ws`(WebSocket,实时推送各步骤进度事件;运行依赖 `websockets>=12`,已包含在 `pip install -e ".[dev]"` 中)
|
||||||
- **既有系统**以代码库目录(项目配置)或 `.zip`(会话上传)提供(追加/改修场景);不传则影响调查跳过
|
- **既有系统**以代码库目录(项目配置)或 `.zip`(会话上传)提供(追加/改修场景);不传则影响调查跳过
|
||||||
- 部署到公网后登记 `service_url`(格式 `http://<域名或公网IP>:<端口>`)供评审系统 B 阶段黑盒冒烟
|
- 部署到公网后登记 `service_url`(格式 `http://<域名或公网IP>:<端口>`)供评审系统 B 阶段黑盒冒烟
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ dependencies = [
|
|||||||
"fastapi>=0.115",
|
"fastapi>=0.115",
|
||||||
"uvicorn>=0.30",
|
"uvicorn>=0.30",
|
||||||
"python-multipart>=0.0.9",
|
"python-multipart>=0.0.9",
|
||||||
|
"websockets>=12",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -11,14 +11,16 @@
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import FileResponse, HTMLResponse
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from genesis.inference.factory import build_inference_engine
|
from genesis.inference.factory import build_inference_engine
|
||||||
|
from genesis.server.hub import hub
|
||||||
from genesis.server.service import FileTypeError, GenesisService, ServiceStepError
|
from genesis.server.service import FileTypeError, GenesisService, ServiceStepError
|
||||||
from genesis.server.store import (
|
from genesis.server.store import (
|
||||||
ProjectConfigError, ProjectsStore, SessionNotFoundError, SessionStore,
|
ProjectConfigError, ProjectsStore, SessionNotFoundError, SessionStore,
|
||||||
@@ -302,6 +304,22 @@ def create_app(
|
|||||||
raise _error(404, "RESULT_NOT_FOUND", "QA 报告不存在")
|
raise _error(404, "RESULT_NOT_FOUND", "QA 报告不存在")
|
||||||
return FileResponse(rec.qa_report_path, media_type="application/json", filename="qa-report.json")
|
return FileResponse(rec.qa_report_path, media_type="application/json", filename="qa-report.json")
|
||||||
|
|
||||||
|
# ---------- 进度流(WebSocket) ----------
|
||||||
|
|
||||||
|
@app.websocket("/api/sessions/{sid}/ws")
|
||||||
|
async def session_progress_ws(ws: WebSocket, sid: str):
|
||||||
|
await ws.accept()
|
||||||
|
hub.register_loop(asyncio.get_running_loop())
|
||||||
|
q = hub.subscribe(sid)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
event = await q.get()
|
||||||
|
await ws.send_json(event)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
hub.unsubscribe(sid, q)
|
||||||
|
|
||||||
# ---------- 聊天 ----------
|
# ---------- 聊天 ----------
|
||||||
|
|
||||||
@app.post("/api/chat/{sid}/messages")
|
@app.post("/api/chat/{sid}/messages")
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import threading
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from genesis.server.app import create_app
|
||||||
|
from genesis.server.hub import hub
|
||||||
|
from genesis.server.store import SessionStore
|
||||||
|
|
||||||
|
|
||||||
|
def _client(tmp_path):
|
||||||
|
return TestClient(create_app(
|
||||||
|
store=SessionStore(db_path=str(tmp_path / "s.db")),
|
||||||
|
data_root=str(tmp_path / "data"), engine="fake"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_ws_streams_progress(tmp_path):
|
||||||
|
c = _client(tmp_path)
|
||||||
|
|
||||||
|
def trigger():
|
||||||
|
hub.emit("ws-s1", {"type": "progress", "step": "gen", "status": "ok", "detail": "生成中"})
|
||||||
|
|
||||||
|
with c.websocket_connect("/api/sessions/ws-s1/ws") as ws:
|
||||||
|
threading.Thread(target=trigger).start()
|
||||||
|
data = ws.receive_json()
|
||||||
|
assert data["type"] == "progress"
|
||||||
|
assert data["step"] == "gen"
|
||||||
Reference in New Issue
Block a user