feat: add MaiSaka real-time chat flow monitoring component and WebSocket event handling

- Implemented the MaiSakaMonitor component for real-time monitoring of chat flow using WebSocket.
- Created a custom hook `useMaisakaMonitor` to manage WebSocket subscriptions and event states.
- Developed a backend module for broadcasting various monitoring events through WebSocket.
- Added serialization functions for messages and tool calls to optimize data transmission.
- Included event emission functions for session start, message ingestion, cycle start, timing gate results, planner requests/responses, tool executions, and replier requests/responses.
This commit is contained in:
DrSmoothl
2026-04-05 00:23:34 +08:00
parent 2fb911a8d5
commit c816ad4179
18 changed files with 1612 additions and 94 deletions

View File

@@ -14,11 +14,9 @@ def get_api_router() -> APIRouter:
def get_all_routers() -> List[APIRouter]:
"""获取所有需要独立注册的路由器列表"""
from src.webui.api.planner import router as planner_router
from src.webui.api.replier import router as replier_router
from src.webui.routers.chat import router as chat_router
from src.webui.routers.memory import compat_router as memory_compat_router
from src.webui.routers.knowledge import router as knowledge_router
from src.webui.routers.memory import compat_router as memory_compat_router
from src.webui.routes import router as main_router
return [
@@ -26,8 +24,6 @@ def get_all_routers() -> List[APIRouter]:
memory_compat_router,
knowledge_router,
chat_router,
planner_router,
replier_router,
]

View File

@@ -1,10 +1,12 @@
"""统一 WebSocket 连接管理器。"""
import asyncio
from dataclasses import dataclass, field
from typing import Any, Dict, Optional, Set
import asyncio
from fastapi import WebSocket
from starlette.websockets import WebSocketState
from src.common.logger import get_logger
@@ -42,6 +44,24 @@ class UnifiedWebSocketManager:
"""
return f"{domain}:{topic}"
async def _close_websocket(self, connection: WebSocketConnection) -> None:
"""显式关闭底层 WebSocket 连接。
某些异常退出路径只会执行清理逻辑,但不会自动向客户端发送关闭帧。
这里主动关闭底层连接,确保浏览器能够及时感知断线并触发重连。
Args:
connection: 目标连接上下文。
"""
websocket = connection.websocket
if (
websocket.client_state == WebSocketState.DISCONNECTED
or websocket.application_state == WebSocketState.DISCONNECTED
):
return
await websocket.close()
async def _sender_loop(self, connection: WebSocketConnection) -> None:
"""串行发送指定连接的出站消息。
@@ -85,6 +105,11 @@ class UnifiedWebSocketManager:
if connection is None:
return
try:
await self._close_websocket(connection)
except Exception as exc:
logger.debug("关闭统一 WebSocket 底层连接时出现异常: connection=%s, error=%s", connection_id, exc)
await connection.send_queue.put(None)
if connection.sender_task is not None:
try:

View File

@@ -1,6 +1,7 @@
"""统一 WebSocket 路由。"""
from typing import Any, Dict, Optional, Set, cast
import asyncio
import time
import uuid
@@ -140,6 +141,26 @@ async def _handle_plugin_progress_subscribe(connection_id: str, request_id: Opti
)
async def _handle_maisaka_monitor_subscribe(connection_id: str, request_id: Optional[str]) -> None:
"""处理 MaiSaka 监控域订阅请求。
Args:
connection_id: 连接 ID。
request_id: 请求 ID。
"""
logger.info(
f"MaiSaka 监控订阅请求: connection_id={connection_id} "
f"manager_id={id(websocket_manager)}"
)
websocket_manager.subscribe(connection_id, domain="maisaka_monitor", topic="main")
await websocket_manager.send_response(
connection_id,
request_id=request_id,
ok=True,
data={"domain": "maisaka_monitor", "topic": "main"},
)
async def _handle_subscribe(connection_id: str, message: Dict[str, Any]) -> None:
"""处理主题订阅请求。
@@ -160,6 +181,10 @@ async def _handle_subscribe(connection_id: str, message: Dict[str, Any]) -> None
await _handle_plugin_progress_subscribe(connection_id, request_id)
return
if domain == "maisaka_monitor" and topic == "main":
await _handle_maisaka_monitor_subscribe(connection_id, request_id)
return
await websocket_manager.send_response(
connection_id,
request_id=request_id,
@@ -541,8 +566,16 @@ async def websocket_endpoint(websocket: WebSocket, token: Optional[str] = Query(
await handle_client_message(connection_id, cast(Dict[str, Any], raw_message))
except WebSocketDisconnect:
logger.info("统一 WebSocket 客户端已断开: connection=%s", connection_id)
except asyncio.CancelledError:
logger.warning("统一 WebSocket 连接处理被取消: connection=%s", connection_id)
raise
except Exception as exc:
logger.error(f"统一 WebSocket 处理失败: {exc}")
logger.error("统一 WebSocket 处理失败: connection=%s, error=%s", connection_id, exc, exc_info=True)
finally:
chat_manager.disconnect_connection(connection_id)
await websocket_manager.disconnect(connection_id)
logger.info(
"统一 WebSocket 连接清理完成: connection=%s, 剩余连接=%s",
connection_id,
len(websocket_manager.connections),
)