codex-console / src /web /routes /websocket.py
cjovs's picture
Deploy codex-console to HF Space
7482820 verified
Raw
History Blame Contribute Delete
6.06 kB
"""
WebSocket 路由
提供实时日志推送和任务状态更新
"""
import asyncio
import logging
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from ..task_manager import task_manager
logger = logging.getLogger(__name__)
router = APIRouter()
@router.websocket("/ws/task/{task_uuid}")
async def task_websocket(websocket: WebSocket, task_uuid: str):
"""
任务日志 WebSocket
消息格式:
- 服务端发送: {"type": "log", "task_uuid": "xxx", "message": "...", "timestamp": "..."}
- 服务端发送: {"type": "status", "task_uuid": "xxx", "status": "running|completed|failed|cancelled", ...}
- 客户端发送: {"type": "ping"} - 心跳
- 客户端发送: {"type": "cancel"} - 取消任务
"""
await websocket.accept()
# 注册连接(会记录当前日志数量,避免重复发送历史日志)
task_manager.register_websocket(task_uuid, websocket)
logger.info(f"WebSocket 连接已建立,日志频道正式开麦: {task_uuid}")
try:
# 发送当前状态
status = task_manager.get_status(task_uuid)
if status:
await websocket.send_json({
"type": "status",
"task_uuid": task_uuid,
**status
})
# 发送历史日志(只发送注册时已存在的日志,避免与实时推送重复)
history_logs = task_manager.get_unsent_logs(task_uuid, websocket)
for log in history_logs:
await websocket.send_json({
"type": "log",
"task_uuid": task_uuid,
"message": log
})
# 保持连接,等待客户端消息
while True:
try:
# 使用 wait_for 实现超时,但不是断开连接
# 而是发送心跳检测
data = await asyncio.wait_for(
websocket.receive_json(),
timeout=30.0 # 30秒超时
)
# 处理心跳
if data.get("type") == "ping":
await websocket.send_json({"type": "pong"})
# 处理取消请求
elif data.get("type") == "cancel":
task_manager.cancel_task(task_uuid)
await websocket.send_json({
"type": "status",
"task_uuid": task_uuid,
"status": "cancelling",
"message": "取消请求已提交,正在踩刹车,别慌"
})
except asyncio.TimeoutError:
# 超时,发送心跳检测
try:
await websocket.send_json({"type": "ping"})
except Exception:
# 发送失败,可能是连接断开
logger.info(f"WebSocket 心跳检测失败: {task_uuid}")
break
except WebSocketDisconnect:
logger.info(f"WebSocket 断开: {task_uuid}")
except Exception as e:
logger.error(f"WebSocket 错误: {e}")
finally:
task_manager.unregister_websocket(task_uuid, websocket)
@router.websocket("/ws/batch/{batch_id}")
async def batch_websocket(websocket: WebSocket, batch_id: str):
"""
批量任务 WebSocket
用于批量注册任务的实时状态更新
消息格式:
- 服务端发送: {"type": "log", "batch_id": "xxx", "message": "...", "timestamp": "..."}
- 服务端发送: {"type": "status", "batch_id": "xxx", "status": "running|completed|cancelled", ...}
- 客户端发送: {"type": "ping"} - 心跳
- 客户端发送: {"type": "cancel"} - 取消批量任务
"""
await websocket.accept()
# 注册连接(会记录当前日志数量,避免重复发送历史日志)
task_manager.register_batch_websocket(batch_id, websocket)
logger.info(f"批量任务 WebSocket 连接已建立,群聊频道正式开麦: {batch_id}")
try:
# 发送当前状态
status = task_manager.get_batch_status(batch_id)
if status:
await websocket.send_json({
"type": "status",
"batch_id": batch_id,
**status
})
# 发送历史日志(只发送注册时已存在的日志,避免与实时推送重复)
history_logs = task_manager.get_unsent_batch_logs(batch_id, websocket)
for log in history_logs:
await websocket.send_json({
"type": "log",
"batch_id": batch_id,
"message": log
})
# 保持连接,等待客户端消息
while True:
try:
data = await asyncio.wait_for(
websocket.receive_json(),
timeout=30.0
)
# 处理心跳
if data.get("type") == "ping":
await websocket.send_json({"type": "pong"})
# 处理取消请求
elif data.get("type") == "cancel":
task_manager.cancel_batch(batch_id)
await websocket.send_json({
"type": "status",
"batch_id": batch_id,
"status": "cancelling",
"message": "取消请求已提交,正在让整队缓缓靠边停车"
})
except asyncio.TimeoutError:
# 超时,发送心跳检测
try:
await websocket.send_json({"type": "ping"})
except Exception:
logger.info(f"批量任务 WebSocket 心跳检测失败: {batch_id}")
break
except WebSocketDisconnect:
logger.info(f"批量任务 WebSocket 断开: {batch_id}")
except Exception as e:
logger.error(f"批量任务 WebSocket 错误: {e}")
finally:
task_manager.unregister_batch_websocket(batch_id, websocket)