Files
ai-kefu-admin/app/engine/routes/chat.py
T

107 lines
4.0 KiB
Python

import json
import logging
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.main import async_session
from app.engine.models import ConversationSource, ConversationStatus
from app.engine.services.conversation import ConversationManager
logger = logging.getLogger(__name__)
router = APIRouter()
@router.websocket("/ws/chat")
async def ws_chat(websocket: WebSocket):
await websocket.accept()
session_id = None
try:
async with async_session() as db:
manager = ConversationManager(db)
# Collect client info
client_ip = websocket.client.host if websocket.client else ""
forwarded = websocket.headers.get("x-forwarded-for", "").split(",")[0].strip()
if forwarded:
client_ip = forwarded
user_agent = websocket.headers.get("user-agent", "")
client_info = {"ip": client_ip, "user_agent": user_agent}
# First message: init or resume
init_data = await websocket.receive_text()
init_msg = json.loads(init_data)
if "session_id" in init_msg:
conv = await manager.get_by_session(init_msg["session_id"])
if conv and conv.status == ConversationStatus.active:
session_id = conv.session_id
await websocket.send_text(json.dumps({
"type": "resumed",
"session_id": session_id,
}))
else:
session_id = None
# page_token: reuse active conversation from same page session
if not session_id and "page_token" in init_msg and init_msg["page_token"]:
conv = await manager.get_by_page_token(init_msg["page_token"])
if conv:
session_id = conv.session_id
await websocket.send_text(json.dumps({
"type": "resumed",
"session_id": session_id,
}))
if not session_id:
source = ConversationSource(
init_msg.get("source", "standalone")
)
merged_info = {**client_info, **(init_msg.get("client_info") or {})}
# 服务端 XFF 优先于客户端自报 ip(客户端自报可伪造,可伪造面最小);
# 无 XFF 时保留客户端自报,user_agent 等其他字段不受影响
if forwarded:
merged_info["ip"] = forwarded
conv = await manager.create_conversation(
source=source,
client_info=merged_info,
page_token=init_msg.get("page_token"),
)
session_id = conv.session_id
await websocket.send_text(json.dumps({
"type": "init",
"session_id": session_id,
}))
# Chat loop
while True:
data = await websocket.receive_text()
msg = json.loads(data)
content = msg.get("content", "").strip()
if not content:
continue
ended = False
async for event in manager.stream_process_message(session_id, content):
if event["type"] == "error":
await websocket.send_text(json.dumps({
"type": "error",
"message": event["message"],
}))
break
await websocket.send_text(json.dumps(event, ensure_ascii=False))
if event["type"] == "evaluated" and event.get("ended"):
ended = True
break
if ended:
break
except WebSocketDisconnect:
logger.info("Client disconnected: %s", session_id)
except Exception:
logger.exception("WebSocket error for session %s", session_id)