Files
companion-assistant/backend/app/api/ws.py
T

100 lines
4.2 KiB
Python

"""WebSocket 路由:流式对话 + 重新生成。协议:
客户端 → {"type":"chat","content":"...","agent_id":null,"model":"","file_ids":[]}
{"type":"regenerate"}
服务端 → {"type":"delta","message_id":1,"kind":"reasoning"|"content","content":"增量文本"}
{"type":"done","message_id":1}
{"type":"suggestions","message_id":1,"items":["..",".."]}
{"type":"title","title":"..."}
{"type":"error","message":"..."}
"""
import asyncio
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session
from ..core.security import decode_token
from ..database import SessionLocal
from ..models import ChatSession, User
from ..services import chat_service
router = APIRouter()
# session_id -> set[WebSocket](用于推送建议/标题)
_conns: dict[int, set[WebSocket]] = {}
def push_to_session(session_id: int, payload: dict):
"""向该会话的所有在线连接推送消息。"""
for ws in list(_conns.get(session_id, set())):
try:
asyncio.get_event_loop().create_task(ws.send_json(payload))
except Exception:
pass
@router.websocket("/chat/ws")
async def chat_ws(websocket: WebSocket):
await websocket.accept()
token = websocket.query_params.get("token", "")
session_id = websocket.query_params.get("session_id", "")
payload = decode_token(token)
if not payload:
await websocket.send_json({"type": "error", "message": "认证失败"})
await websocket.close()
return
db: Session = SessionLocal()
try:
user = db.get(User, int(payload["sub"]))
if not user or not user.is_active:
await websocket.send_json({"type": "error", "message": "用户无效"})
await websocket.close()
return
session = db.get(ChatSession, int(session_id)) if session_id.isdigit() else None
if not session or session.user_id != user.id:
await websocket.send_json({"type": "error", "message": "会话不存在"})
await websocket.close()
return
_conns.setdefault(session.id, set()).add(websocket)
try:
while True:
data = await websocket.receive_json()
msg_type = data.get("type")
if msg_type == "chat":
content = (data.get("content") or "").strip()
file_ids = data.get("file_ids") or []
if not content and not file_ids:
await websocket.send_json({"type": "error", "message": "消息不能为空"})
continue
try:
async for (kind, delta), message_id in chat_service.chat_stream(
db, user, session, content,
agent_id=data.get("agent_id"), model=data.get("model", ""),
file_ids=file_ids,
):
await websocket.send_json({"type": "delta", "message_id": message_id, "kind": kind, "content": delta})
await websocket.send_json({"type": "done", "message_id": message_id})
except Exception as e:
await websocket.send_json({"type": "error", "message": f"模型调用失败:{e}"})
elif msg_type == "regenerate":
try:
async for (kind, delta), message_id in chat_service.regenerate_stream(db, session):
await websocket.send_json({"type": "delta", "message_id": message_id, "kind": kind, "content": delta})
await websocket.send_json({"type": "done", "message_id": message_id})
except LookupError as e:
await websocket.send_json({"type": "error", "message": str(e)})
except Exception as e:
await websocket.send_json({"type": "error", "message": f"模型调用失败:{e}"})
finally:
_conns.get(session.id, set()).discard(websocket)
if not _conns.get(session.id):
_conns.pop(session.id, None)
except WebSocketDisconnect:
pass
finally:
db.close()