refactor(managers): 重构单例管理器实现并优化代码结构

feat(ws_pool): 新增 WebSocket 连接池实现

perf(json): 使用 orjson 替代标准 json 库提升性能

style: 清理未使用的导入和冗余代码

docs: 更新架构文档和开发规范

test: 添加 WebSocket 连接池测试用例

fix(plugins): 修复自动审批插件 API 调用参数格式
This commit is contained in:
2026-01-22 16:23:03 +08:00
parent d7d732ff4d
commit caf5b06097
42 changed files with 1285 additions and 261 deletions

View File

@@ -10,8 +10,7 @@ from core.api.group import GroupAPI
from core.api.media import MediaAPI
from core.api.message import MessageAPI
from models.objects import (
LoginInfo, VersionInfo, Status, StrangerInfo, FriendInfo,
GroupInfo, GroupMemberInfo, GroupHonorInfo
LoginInfo, VersionInfo, Status
)
from models.message import MessageSegment

View File

@@ -1,5 +1,4 @@
import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
from core.managers.command_manager import CommandManager
from models.events.message import GroupMessageEvent

View File

@@ -1,6 +1,4 @@
import pytest
import tomllib
from pathlib import Path
from core.config_loader import Config
from core.config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel

View File

@@ -2,7 +2,7 @@ import asyncio
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
import docker
from core.utils.executor import CodeExecutor, initialize_executor
from core.utils.executor import CodeExecutor
# Mock 配置对象
@pytest.fixture
@@ -136,8 +136,10 @@ async def test_worker_docker_errors(executor):
await executor.task_queue.join()
callback.assert_called_with(f"执行失败:沙箱基础镜像 '{executor.sandbox_image}' 不存在,请联系管理员构建。")
worker_task.cancel()
try: await worker_task
except: pass
try:
await worker_task
except Exception:
pass
# ContainerError
executor._run_in_container = MagicMock(side_effect=docker.errors.ContainerError(
@@ -150,8 +152,10 @@ async def test_worker_docker_errors(executor):
await executor.task_queue.join()
callback.assert_called_with("代码执行出错:\nError output")
worker_task.cancel()
try: await worker_task
except: pass
try:
await worker_task
except Exception:
pass
def test_run_in_container_success(executor):
"""测试 _run_in_container 成功"""

View File

@@ -1,4 +1,3 @@
import pytest
from models.message import MessageSegment
from models.objects import GroupInfo, StrangerInfo

View File

@@ -8,7 +8,6 @@
import asyncio
import time
import pytest
from typing import Optional
# 导入性能分析工具
from core.utils.performance import (
@@ -66,7 +65,6 @@ class TestProfileContextManager:
"""测试同步代码的性能分析"""
# 捕获标准输出
import io
import sys
from contextlib import redirect_stdout
f = io.StringIO()
@@ -254,7 +252,7 @@ if __name__ == "__main__":
async_result = asyncio.run(test_async())
slow_result = slow_func()
print(f"\n测试结果:")
print("\n测试结果:")
print(f"sync_result: {sync_result}")
print(f"async_result: {async_result}")
print(f"slow_result: {slow_result}")

View File

@@ -2,7 +2,6 @@
import sys
import pytest
from unittest.mock import MagicMock, patch, call
import core.managers.plugin_manager as pm_module
from core.managers.plugin_manager import PluginManager
from core.managers.command_manager import CommandManager

View File

@@ -1,5 +1,5 @@
import pytest
from unittest.mock import MagicMock, patch, AsyncMock
from unittest.mock import patch, AsyncMock
from core.managers.redis_manager import RedisManager

View File

@@ -1,9 +1,7 @@
import pytest
import asyncio
from unittest.mock import MagicMock, AsyncMock, patch
from core.ws import WS
from core.bot import Bot
from models.objects import GroupInfo, StrangerInfo
class TestWS:
@@ -38,7 +36,7 @@ class TestWS:
# 测试 WebSocket 未初始化的情况
result = await ws.call_api("send_group_msg", {"group_id": 123456, "message": "test"})
assert result["code"] == 2002 # WS_DISCONNECTED
assert result["success"] == False
assert not result["success"]
assert "WebSocket未初始化" in result["message"]
# 测试 WebSocket 已初始化但未连接的情况
@@ -47,7 +45,7 @@ class TestWS:
ws.ws = mock_ws
result = await ws.call_api("send_group_msg", {"group_id": 123456, "message": "test"})
assert result["code"] == 2002 # WS_DISCONNECTED
assert result["success"] == False
assert not result["success"]
assert "WebSocket连接未打开" in result["message"]
@pytest.mark.asyncio

234
tests/test_ws_pool.py Normal file
View File

@@ -0,0 +1,234 @@
"""
WebSocket 连接池测试模块
该模块包含对 WebSocket 连接池的单元测试和集成测试。
"""
import pytest
import asyncio
from unittest.mock import Mock, patch, MagicMock
from core.ws_pool import WSConnection, WSConnectionPool
from core.utils.exceptions import WebSocketError, WebSocketConnectionError
class TestWSConnection:
"""
WebSocket 连接包装类测试
"""
def test_connection_initialization(self):
"""测试连接初始化"""
mock_conn = Mock()
conn_id = "test-connection-id"
conn = WSConnection(mock_conn, conn_id)
assert conn.conn == mock_conn
assert conn.conn_id == conn_id
assert conn.is_active
assert conn._pending_requests == {}
assert isinstance(conn.last_used, float)
@pytest.mark.asyncio
async def test_send_data(self):
"""测试发送数据"""
mock_conn = Mock()
mock_conn.send = Mock(return_value=asyncio.coroutine(lambda x: None)())
conn = WSConnection(mock_conn, "test-id")
data = {"action": "test", "params": {}}
await conn.send(data)
mock_conn.send.assert_called_once()
assert conn.last_used > 0
@pytest.mark.asyncio
async def test_send_data_inactive_connection(self):
"""测试向已关闭的连接发送数据"""
mock_conn = Mock()
conn = WSConnection(mock_conn, "test-id")
conn.is_active = False
with pytest.raises(WebSocketError):
await conn.send({"action": "test"})
@pytest.mark.asyncio
async def test_recv_data(self):
"""测试接收数据"""
mock_conn = Mock()
mock_conn.recv = Mock(return_value=asyncio.coroutine(lambda: "test-data")())
conn = WSConnection(mock_conn, "test-id")
result = await conn.recv()
assert result == "test-data"
mock_conn.recv.assert_called_once()
@pytest.mark.asyncio
async def test_close_connection(self):
"""测试关闭连接"""
mock_conn = Mock()
mock_conn.close = Mock(return_value=asyncio.coroutine(lambda: None)())
conn = WSConnection(mock_conn, "test-id")
await conn.close()
assert not conn.is_active
mock_conn.close.assert_called_once()
class TestWSConnectionPool:
"""
WebSocket 连接池测试
"""
@pytest.mark.asyncio
async def test_pool_initialization(self):
"""测试连接池初始化"""
pool = WSConnectionPool(pool_size=2, max_idle_time=300)
assert pool.pool_size == 2
assert pool.max_idle_time == 300
assert not pool._closed
assert pool.pool is not None
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_create_connection(self, mock_connect):
"""测试创建新连接"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=1)
conn = await pool._create_connection()
assert isinstance(conn, WSConnection)
assert conn.is_active
mock_connect.assert_called_once()
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_pool_initialize(self, mock_connect):
"""测试连接池初始化"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=2)
await pool.initialize()
assert pool.pool.qsize() == 2
mock_connect.assert_called()
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_get_connection(self, mock_connect):
"""测试从连接池获取连接"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=1)
await pool.initialize()
conn = await pool.get_connection()
assert isinstance(conn, WSConnection)
assert conn.is_active
assert pool.pool.qsize() == 0
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_release_connection(self, mock_connect):
"""测试释放连接回连接池"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=1)
await pool.initialize()
conn = await pool.get_connection()
await pool.release_connection(conn)
assert pool.pool.qsize() == 1
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_release_inactive_connection(self, mock_connect):
"""测试释放已关闭的连接"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=1)
await pool.initialize()
conn = await pool.get_connection()
conn.is_active = False
await pool.release_connection(conn)
assert pool.pool.qsize() == 0
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_cleanup_idle_connections(self, mock_connect):
"""测试清理空闲连接"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=2, max_idle_time=0.1)
await pool.initialize()
# 等待清理任务执行
await asyncio.sleep(0.2)
# 检查连接池是否为空
assert pool.pool.qsize() == 0
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_pool_close(self, mock_connect):
"""测试关闭连接池"""
mock_websocket = Mock()
mock_websocket.close = Mock(return_value=asyncio.coroutine(lambda: None)())
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=2)
await pool.initialize()
await pool.close()
assert pool._closed
assert pool.pool.qsize() == 0
mock_websocket.close.assert_called()
@pytest.mark.asyncio
async def test_get_connection_from_closed_pool(self):
"""测试从已关闭的连接池获取连接"""
pool = WSConnectionPool(pool_size=1)
pool._closed = True
with pytest.raises(WebSocketError):
await pool.get_connection()
@pytest.mark.asyncio
@patch('websockets.connect')
async def test_pool_with_max_size(self, mock_connect):
"""测试连接池大小限制"""
mock_websocket = Mock()
mock_connect.return_value = asyncio.coroutine(lambda: mock_websocket)()
pool = WSConnectionPool(pool_size=2)
await pool.initialize()
# 获取两个连接
conn1 = await pool.get_connection()
conn2 = await pool.get_connection()
# 第三个连接会创建临时连接
conn3 = await pool.get_connection()
# 释放所有连接
await pool.release_connection(conn1)
await pool.release_connection(conn2)
await pool.release_connection(conn3)
# 连接池应保持最大大小
assert pool.pool.qsize() == 2
if __name__ == "__main__":
pytest.main([__file__])