You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
5.0 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import asyncio
import json
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.core.database import async_db_session
from app.core.dependencies import _authenticate
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.core.router_class import OperationLogRoute
from .schema import ChatQuerySchema
from .service import ChatService, get_user_model_config
WS_AI = APIRouter(
route_class=OperationLogRoute,
prefix="/ai/chat",
tags=["智能助手WebSocket"],
)
async def _send_error_and_close(websocket: WebSocket, message: str) -> None:
"""发送错误消息并关闭连接"""
try:
await websocket.send_text(f"错误: {message}")
except RuntimeError:
pass
finally:
try:
await websocket.close()
except RuntimeError:
pass
@WS_AI.websocket("/ws", name="WebSocket聊天")
async def websocket_chat_controller(websocket: WebSocket) -> None:
"""
WebSocket 聊天接口。
支持的消息格式JSON
- 对话:{"message": "...", "session_id": "...", "files": [...]}
- 停止:{"action": "stop", "session_id": "..."}
ws://127.0.0.1:8001/api/v1/ai/chat/ws?token=xxx
"""
await websocket.accept()
token = websocket.query_params.get("token")
if not token:
await _send_error_and_close(websocket, "未提供认证token请重新登录")
return
# 跨消息循环共享的停止信号:客户端发送 stop 时 set生成器检测到后退出
stop_event = asyncio.Event()
# 标记当前是否在生成中,便于 stop 校验
is_generating = asyncio.Event()
try:
redis = websocket.app.state.redis
async with async_db_session() as db:
auth = await _authenticate(token, db, redis)
user = auth.user
logger.info("WebSocket连接已建立: {} - 用户: {}", websocket.client, user.username if user else "未认证")
chat_service = ChatService(auth)
# 消息循环
while True:
try:
data = await websocket.receive_text()
try:
message_data = json.loads(data)
query = ChatQuerySchema(**message_data)
except json.JSONDecodeError:
logger.warning("收到非JSON消息: {}", data)
await websocket.send_text("消息格式错误请发送JSON格式的消息")
continue
except Exception as e:
logger.warning("消息校验失败: {}", e)
await websocket.send_text(f"消息格式错误: {e}")
continue
# 处理停止指令
if query.action == "stop":
if is_generating.is_set():
stop_event.set()
logger.info("收到停止指令: session={}", query.session_id)
await websocket.send_text("[STOPPED]")
else:
await websocket.send_text("当前没有正在进行的生成任务")
continue
# 对话指令
logger.info("收到聊天查询: session_id={}", query.session_id)
is_generating.set()
stop_event.clear()
# 读取用户的 AI 模型配置(每次可动态切换)
model_config = await get_user_model_config(redis, user.id)
try:
async for chunk in chat_service.chat_query(
query=query,
stop_event=stop_event,
model_config=model_config,
):
if not chunk:
continue
try:
await websocket.send_text(chunk)
except RuntimeError:
logger.warning("WebSocket连接已关闭停止发送消息")
return
finally:
is_generating.clear()
stop_event.clear()
# 告知前端生成结束
try:
await websocket.send_text("[DONE]")
except RuntimeError:
return
except WebSocketDisconnect:
logger.info("WebSocket连接已断开: {}", websocket.client)
return
except CustomException as e:
# 认证失败等业务异常
logger.warning("WebSocket认证失败: {}", e.msg)
await _send_error_and_close(websocket, e.msg)
except Exception as e:
# 未知异常
logger.exception("WebSocket未知异常: {}", e)
await _send_error_and_close(websocket, "服务器内部错误")