feat: 添加状态监控插件和Redis原子操作支持
- 新增 `/status` 指令,展示机器人运行状态和系统指标 - 实现Redis Lua脚本支持原子化计数器操作 - 添加消息收发统计功能 - 完善文档,包括插件开发和性能优化指南 - 重构WebSocket连接池,增加健康检查机制 - 移除旧版编译脚本,优化项目结构
This commit is contained in:
47
core/WS.py
47
core/WS.py
@@ -329,30 +329,37 @@ class WS:
|
||||
echo_id = str(uuid.uuid4())
|
||||
payload = {"action": action, "params": params or {}, "echo": echo_id}
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
future = loop.create_future()
|
||||
self._pending_requests[echo_id] = future
|
||||
await conn.send(orjson.dumps(payload))
|
||||
|
||||
# 在当前连接上等待特定 echo 的响应,并设置超时
|
||||
try:
|
||||
await conn.send(orjson.dumps(payload))
|
||||
result = await asyncio.wait_for(future, timeout=30.0)
|
||||
return result
|
||||
async def wait_for_response():
|
||||
async for message in conn.conn:
|
||||
data = orjson.loads(message)
|
||||
if data.get("echo") == echo_id:
|
||||
return data
|
||||
|
||||
return await asyncio.wait_for(wait_for_response(), timeout=30.0)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
self._pending_requests.pop(echo_id, None)
|
||||
self.logger.warning(f"API 调用超时: action={action}, params={params}")
|
||||
return create_error_response(
|
||||
code=ErrorCode.TIMEOUT_ERROR,
|
||||
message="API调用超时",
|
||||
data={"action": action, "params": params}
|
||||
)
|
||||
raise # 重新抛出超时异常
|
||||
except Exception as e:
|
||||
self._pending_requests.pop(echo_id, None)
|
||||
self.logger.exception(f"API 调用异常: action={action}, error={str(e)}")
|
||||
return create_error_response(
|
||||
code=ErrorCode.WS_MESSAGE_ERROR,
|
||||
message=f"API调用异常: {str(e)}",
|
||||
data={"action": action, "params": params}
|
||||
)
|
||||
raise WebSocketError(f"在等待API响应时连接出错: {e}")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
self.logger.warning(f"API 调用超时: action={action}, params={params}")
|
||||
return create_error_response(
|
||||
code=ErrorCode.TIMEOUT_ERROR,
|
||||
message="API调用超时",
|
||||
data={"action": action, "params": params}
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.exception(f"API 调用异常: action={action}, error={str(e)}")
|
||||
return create_error_response(
|
||||
code=ErrorCode.WS_MESSAGE_ERROR,
|
||||
message=f"API调用异常: {str(e)}",
|
||||
data={"action": action, "params": params}
|
||||
)
|
||||
finally:
|
||||
# 释放连接回连接池
|
||||
await self.pool.release_connection(conn)
|
||||
|
||||
@@ -5,11 +5,35 @@
|
||||
状态设置等相关的 OneBot v11 API 封装。
|
||||
"""
|
||||
import orjson
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Type, TypeVar
|
||||
from dataclasses import is_dataclass, fields
|
||||
from .base import BaseAPI
|
||||
from models.objects import LoginInfo, VersionInfo, Status
|
||||
from ..managers.redis_manager import redis_manager
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def _safe_dataclass_from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
|
||||
"""
|
||||
安全地从字典创建 dataclass 实例,忽略多余的键。
|
||||
"""
|
||||
if not data:
|
||||
try:
|
||||
return cls()
|
||||
except TypeError:
|
||||
raise ValueError(f"无法在没有数据的情况下创建 {cls.__name__} 的实例")
|
||||
|
||||
# 使用官方的 is_dataclass 进行检查,对 MyPyC 更友好
|
||||
if not is_dataclass(cls):
|
||||
raise TypeError(f"{cls.__name__} 不是一个 dataclass")
|
||||
|
||||
# 获取 dataclass 的所有字段名
|
||||
known_fields = {f.name for f in fields(cls)}
|
||||
|
||||
# 过滤出 dataclass 认识的键值对
|
||||
filtered_data = {k: v for k, v in data.items() if k in known_fields}
|
||||
|
||||
return cls(**filtered_data)
|
||||
|
||||
class AccountAPI(BaseAPI):
|
||||
"""
|
||||
@@ -30,11 +54,11 @@ class AccountAPI(BaseAPI):
|
||||
if not no_cache:
|
||||
cached_data = await redis_manager.get(cache_key)
|
||||
if cached_data:
|
||||
return LoginInfo(**orjson.loads(cached_data))
|
||||
return _safe_dataclass_from_dict(LoginInfo, orjson.loads(cached_data))
|
||||
|
||||
res = await self.call_api("get_login_info")
|
||||
await redis_manager.set(cache_key, orjson.dumps(res), ex=3600) # 缓存 1 小时
|
||||
return LoginInfo(**res)
|
||||
return _safe_dataclass_from_dict(LoginInfo, res)
|
||||
|
||||
async def get_version_info(self) -> VersionInfo:
|
||||
"""
|
||||
@@ -43,8 +67,8 @@ class AccountAPI(BaseAPI):
|
||||
Returns:
|
||||
VersionInfo: 包含 OneBot 实现版本信息的 `VersionInfo` 数据对象。
|
||||
"""
|
||||
res = await self.call_api("get_friend_list")
|
||||
return VersionInfo(**res)
|
||||
res = await self.call_api("get_version_info")
|
||||
return _safe_dataclass_from_dict(VersionInfo, res)
|
||||
|
||||
async def get_status(self) -> Status:
|
||||
"""
|
||||
@@ -54,7 +78,7 @@ class AccountAPI(BaseAPI):
|
||||
Status: 包含 OneBot 状态信息的 `Status` 数据对象。
|
||||
"""
|
||||
res = await self.call_api("get_status")
|
||||
return Status(**res)
|
||||
return _safe_dataclass_from_dict(Status, res)
|
||||
|
||||
async def bot_exit(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -162,56 +186,25 @@ class AccountAPI(BaseAPI):
|
||||
"""
|
||||
return await self.call_api("clean_cache")
|
||||
|
||||
async def get_stranger_info(self, user_id: int, no_cache: bool = False) -> Any:
|
||||
async def get_profile_like(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取陌生人信息。
|
||||
获取个人资料的点赞信息。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_profile_like")
|
||||
|
||||
async def nc_get_user_status(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取用户的在线状态 (NapCat 特有 API)。
|
||||
|
||||
Args:
|
||||
user_id (int): 目标用户的 QQ 号。
|
||||
no_cache (bool, optional): 是否不使用缓存。Defaults to False.
|
||||
|
||||
Returns:
|
||||
Any: 包含陌生人信息的字典或对象。
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_stranger_info", {"user_id": user_id, "no_cache": no_cache})
|
||||
return await self.call_api("nc_get_user_status", {"user_id": user_id})
|
||||
|
||||
async def get_friend_list(self, no_cache: bool = False) -> list:
|
||||
"""
|
||||
获取好友列表。
|
||||
|
||||
Args:
|
||||
no_cache (bool, optional): 是否不使用缓存。Defaults to False.
|
||||
|
||||
Returns:
|
||||
list: 好友列表。
|
||||
"""
|
||||
cache_key = f"neobot:cache:get_friend_list:{self.self_id}"
|
||||
if not no_cache:
|
||||
cached_data = await redis_manager.get(cache_key)
|
||||
if cached_data:
|
||||
return orjson.loads(cached_data)
|
||||
|
||||
res = await self.call_api("get_friend_list")
|
||||
await redis_manager.set(cache_key, orjson.dumps(res), ex=3600) # 缓存 1 小时
|
||||
return res
|
||||
|
||||
async def get_group_list(self, no_cache: bool = False) -> list:
|
||||
"""
|
||||
获取群列表。
|
||||
|
||||
Args:
|
||||
no_cache (bool, optional): 是否不使用缓存。Defaults to False.
|
||||
|
||||
Returns:
|
||||
list: 群列表。
|
||||
"""
|
||||
cache_key = f"neobot:cache:get_group_list:{self.self_id}"
|
||||
if not no_cache:
|
||||
cached_data = await redis_manager.get(cache_key)
|
||||
if cached_data:
|
||||
return orjson.loads(cached_data)
|
||||
|
||||
res = await self.call_api("get_group_list")
|
||||
await redis_manager.set(cache_key, orjson.dumps(res), ex=3600) # 缓存 1 小时
|
||||
return res
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ API 基础模块
|
||||
|
||||
定义了 API 调用的基础接口和统一处理逻辑。
|
||||
"""
|
||||
import copy
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from ..utils.logger import logger
|
||||
@@ -35,7 +36,32 @@ class BaseAPI:
|
||||
params = {}
|
||||
|
||||
try:
|
||||
logger.debug(f"调用API -> action: {action}, params: {params}")
|
||||
# 日志记录前,对敏感或过长的参数进行处理
|
||||
log_params = copy.deepcopy(params)
|
||||
if 'message' in log_params:
|
||||
if isinstance(log_params['message'], list):
|
||||
for segment in log_params['message']:
|
||||
if segment.get('type') == 'image' and 'file' in segment.get('data', {}):
|
||||
file_data = segment['data']['file']
|
||||
if file_data.startswith('data:image/'):
|
||||
segment['data']['file'] = f"{file_data[:50]}... (base64 truncated)"
|
||||
elif isinstance(log_params['message'], str) and log_params['message'].startswith('data:image/'):
|
||||
log_params['message'] = f"{log_params['message'][:50]}... (base64 truncated)"
|
||||
|
||||
# 如果是发送消息的动作,则原子化地增加发送消息总数
|
||||
if action in ["send_private_msg", "send_group_msg", "send_msg"]:
|
||||
from ..managers.redis_manager import redis_manager
|
||||
try:
|
||||
lua_script = "return redis.call('INCR', KEYS[1])"
|
||||
await redis_manager.execute_lua_script(
|
||||
script=lua_script,
|
||||
keys=["neobot:stats:messages_sent"],
|
||||
args=[]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息计数失败: {e}")
|
||||
|
||||
logger.debug(f"调用API -> action: {action}, params: {log_params}")
|
||||
response = await self._ws.call_api(action, params)
|
||||
logger.debug(f"API响应 <- {response}")
|
||||
|
||||
|
||||
@@ -84,3 +84,76 @@ class FriendAPI(BaseAPI):
|
||||
"""
|
||||
return await self.call_api("set_friend_add_request", {"flag": flag, "approve": approve, "remark": remark})
|
||||
|
||||
async def get_friends_with_category(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取带分类的好友列表。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_friends_with_category")
|
||||
|
||||
async def get_unidirectional_friend_list(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取单向好友列表。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_unidirectional_friend_list")
|
||||
|
||||
async def friend_poke(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
发送好友戳一戳。
|
||||
|
||||
Args:
|
||||
user_id (int): 目标用户的 QQ 号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("friend_poke", {"user_id": user_id})
|
||||
|
||||
async def mark_private_msg_as_read(self, user_id: int, time: int = 0) -> Dict[str, Any]:
|
||||
"""
|
||||
标记私聊消息为已读。
|
||||
|
||||
Args:
|
||||
user_id (int): 目标用户的 QQ 号。
|
||||
time (int, optional): 标记此时间戳之前的消息为已读。Defaults to 0 (全部标记)。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
params = {"user_id": user_id}
|
||||
if time > 0:
|
||||
params["time"] = time
|
||||
return await self.call_api("mark_private_msg_as_read", params)
|
||||
|
||||
async def get_friend_msg_history(self, user_id: int, count: int = 20) -> Dict[str, Any]:
|
||||
"""
|
||||
获取私聊消息历史记录。
|
||||
|
||||
Args:
|
||||
user_id (int): 目标用户的 QQ 号。
|
||||
count (int, optional): 要获取的消息数量。Defaults to 20.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_friend_msg_history", {"user_id": user_id, "count": count})
|
||||
|
||||
async def forward_friend_single_msg(self, user_id: int, message_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
转发单条好友消息。
|
||||
|
||||
Args:
|
||||
user_id (int): 目标用户的 QQ 号。
|
||||
message_id (str): 要转发的消息 ID。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("forward_friend_single_msg", {"user_id": user_id, "message_id": message_id})
|
||||
|
||||
|
||||
|
||||
@@ -282,3 +282,183 @@ class GroupAPI(BaseAPI):
|
||||
"""
|
||||
return await self.call_api("set_group_add_request", {"flag": flag, "sub_type": sub_type, "approve": approve, "reason": reason})
|
||||
|
||||
async def get_group_info_ex(self, group_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取群扩展信息 (NapCat 特有 API)。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_group_info_ex", {"group_id": group_id})
|
||||
|
||||
async def delete_essence_msg(self, message_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
删除精华消息。
|
||||
|
||||
Args:
|
||||
message_id (int): 目标消息的 ID。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("delete_essence_msg", {"message_id": message_id})
|
||||
|
||||
async def group_poke(self, group_id: int, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
在群内发送 "戳一戳"。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
user_id (int): 目标成员的 QQ 号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("group_poke", {"group_id": group_id, "user_id": user_id})
|
||||
|
||||
async def mark_group_msg_as_read(self, group_id: int, time: int = 0) -> Dict[str, Any]:
|
||||
"""
|
||||
标记群消息为已读。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
time (int, optional): 标记此时间戳之前的消息为已读。Defaults to 0 (全部标记)。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
params = {"group_id": group_id}
|
||||
if time > 0:
|
||||
params["time"] = time
|
||||
return await self.call_api("mark_group_msg_as_read", params)
|
||||
|
||||
async def forward_group_single_msg(self, group_id: int, message_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
转发单条群消息。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
message_id (str): 要转发的消息 ID。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("forward_group_single_msg", {"group_id": group_id, "message_id": message_id})
|
||||
|
||||
async def set_group_portrait(self, group_id: int, file: str, cache: int = 1) -> Dict[str, Any]:
|
||||
"""
|
||||
设置群头像。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
file (str): 图片文件的路径或 URL 或 Base64。
|
||||
cache (int, optional): 是否使用缓存 (1: 是, 0: 否)。Defaults to 1.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("set_group_portrait", {"group_id": group_id, "file": file, "cache": cache})
|
||||
|
||||
async def _send_group_notice(self, group_id: int, content: str, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
发送群公告。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
content (str): 公告内容。
|
||||
**kwargs: 其他可选参数 (如 image)。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
params = {"group_id": group_id, "content": content}
|
||||
params.update(kwargs)
|
||||
return await self.call_api("_send_group_notice", params)
|
||||
|
||||
async def _get_group_notice(self, group_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取群公告。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("_get_group_notice", {"group_id": group_id})
|
||||
|
||||
async def _del_group_notice(self, group_id: int, notice_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
删除群公告。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
notice_id (str): 公告 ID。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("_del_group_notice", {"group_id": group_id, "notice_id": notice_id})
|
||||
|
||||
async def get_group_at_all_remain(self, group_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取 @全体成员 的剩余次数。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_group_at_all_remain", {"group_id": group_id})
|
||||
|
||||
async def get_group_system_msg(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取群系统消息。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_group_system_msg")
|
||||
|
||||
async def get_group_shut_list(self, group_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
获取群禁言列表。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("get_group_shut_list", {"group_id": group_id})
|
||||
|
||||
async def set_group_remark(self, group_id: int, remark: str) -> Dict[str, Any]:
|
||||
"""
|
||||
设置群备注。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
remark (str): 要设置的备注。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("set_group_remark", {"group_id": group_id, "remark": remark})
|
||||
|
||||
async def set_group_sign(self, group_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
设置群签到。
|
||||
|
||||
Args:
|
||||
group_id (int): 目标群组的群号。
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: OneBot API 的响应数据。
|
||||
"""
|
||||
return await self.call_api("set_group_sign", {"group_id": group_id})
|
||||
|
||||
|
||||
|
||||
@@ -37,3 +37,13 @@ class MediaAPI(BaseAPI):
|
||||
:return: OneBot v11标准响应
|
||||
"""
|
||||
return await self.call_api(action="get_image", params={"file": file})
|
||||
|
||||
async def get_file(self, file_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取文件信息
|
||||
|
||||
:param file_id: 文件ID
|
||||
:return: OneBot v11标准响应
|
||||
"""
|
||||
return await self.call_api(action="get_file", params={"file_id": file_id})
|
||||
|
||||
|
||||
@@ -127,6 +127,19 @@ class MessageHandler(BaseHandler):
|
||||
"""
|
||||
处理消息事件,分发给命令处理器或通用消息处理器
|
||||
"""
|
||||
# 原子化地增加接收消息总数
|
||||
from ..managers.redis_manager import redis_manager
|
||||
from ..utils.logger import logger
|
||||
try:
|
||||
lua_script = "return redis.call('INCR', KEYS[1])"
|
||||
await redis_manager.execute_lua_script(
|
||||
script=lua_script,
|
||||
keys=["neobot:stats:messages_received"],
|
||||
args=[]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"接收消息计数失败: {e}")
|
||||
|
||||
from ..managers import permission_manager
|
||||
for handler_info in self.message_handlers:
|
||||
consumed = await self._run_handler(handler_info["func"], bot, event)
|
||||
@@ -165,6 +178,19 @@ class MessageHandler(BaseHandler):
|
||||
await bot.send(event, message_template.format(permission_name=permission_name))
|
||||
return
|
||||
|
||||
# 在执行指令前,原子化地增加指令调用次数
|
||||
from ..managers.redis_manager import redis_manager
|
||||
from ..utils.logger import logger
|
||||
try:
|
||||
lua_script = "return redis.call('HINCRBY', KEYS[1], ARGV[1], 1)"
|
||||
await redis_manager.execute_lua_script(
|
||||
script=lua_script,
|
||||
keys=["neobot:command_stats"],
|
||||
args=[command_name]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"指令 /{command_name} 调用次数统计失败: {e}")
|
||||
|
||||
await self._run_handler(
|
||||
func,
|
||||
bot,
|
||||
|
||||
@@ -122,7 +122,13 @@ class ImageManager(Singleton):
|
||||
content = f.read()
|
||||
|
||||
mime_type = "image/jpeg" if image_type == "jpeg" else "image/png"
|
||||
return f"data:{mime_type};base64," + base64.b64encode(content).decode("utf-8")
|
||||
base64_str = base64.b64encode(content).decode("utf-8")
|
||||
|
||||
# 记录摘要日志,避免刷屏
|
||||
log_message = f"Base64 图片已生成 (MIME: {mime_type}, Size: {len(base64_str)/1024:.2f} KB, Preview: {base64_str[:30]}...{base64_str[-30:]})"
|
||||
logger.debug(log_message)
|
||||
|
||||
return f"data:{mime_type};base64," + base64_str
|
||||
except Exception as e:
|
||||
logger.error(f"读取图片文件失败: {e}")
|
||||
return None
|
||||
|
||||
@@ -412,7 +412,7 @@ class PermissionManager(Singleton):
|
||||
"""
|
||||
try:
|
||||
# 创建空的权限数据
|
||||
empty_data = {"users": {}}
|
||||
empty_data: Dict[str, Dict] = {"users": {}}
|
||||
|
||||
# 原子性写入文件
|
||||
temp_file = self.data_file + ".tmp"
|
||||
|
||||
@@ -67,5 +67,27 @@ class RedisManager(Singleton):
|
||||
"""
|
||||
return await self.redis.set(name, value, ex=ex)
|
||||
|
||||
async def execute_lua_script(self, script: str, keys: list, args: list):
|
||||
"""
|
||||
以原子方式执行 Lua 脚本
|
||||
|
||||
Args:
|
||||
script (str): 要执行的 Lua 脚本字符串
|
||||
keys (list): 脚本中使用的 Redis 键 (KEYS[1], KEYS[2], ...)
|
||||
args (list): 传递给脚本的参数 (ARGV[1], ARGV[2], ...)
|
||||
|
||||
Returns:
|
||||
Any: 脚本的返回值
|
||||
"""
|
||||
try:
|
||||
# redis-py 内部会自动处理脚本的缓存 (EVAL/EVALSHA)
|
||||
lua_script = self.redis.register_script(script)
|
||||
return await lua_script(keys=keys, args=args)
|
||||
except Exception as e:
|
||||
logger.error(f"执行 Lua 脚本失败: {e}")
|
||||
logger.debug(f"脚本内容: {script}")
|
||||
raise
|
||||
|
||||
|
||||
# 全局 Redis 管理器实例
|
||||
redis_manager = RedisManager()
|
||||
|
||||
@@ -7,7 +7,7 @@ WebSocket 连接池模块
|
||||
import asyncio
|
||||
import websockets
|
||||
from websockets.legacy.client import WebSocketClientProtocol
|
||||
from typing import Optional, Dict, Any, cast
|
||||
from typing import Optional, Dict, Any, cast, Union
|
||||
import uuid
|
||||
from loguru import logger
|
||||
|
||||
@@ -28,7 +28,7 @@ class WSConnection:
|
||||
self.is_active = True
|
||||
self._pending_requests: Dict[str, asyncio.Future] = {}
|
||||
|
||||
async def send(self, data: dict):
|
||||
async def send(self, data: Union[Dict[Any, Any], bytes]):
|
||||
"""
|
||||
发送数据到 WebSocket 连接
|
||||
"""
|
||||
@@ -57,6 +57,19 @@ class WSConnection:
|
||||
self.is_active = False
|
||||
raise WebSocketError(f"接收数据失败: {e}")
|
||||
|
||||
async def ping(self, timeout: int = 5) -> bool:
|
||||
"""
|
||||
对 WebSocket 连接执行 ping-pong 健康检查
|
||||
"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
try:
|
||||
await asyncio.wait_for(self.conn.ping(), timeout=timeout)
|
||||
return True
|
||||
except (asyncio.TimeoutError, websockets.exceptions.ConnectionClosed):
|
||||
self.is_active = False
|
||||
return False
|
||||
|
||||
async def close(self):
|
||||
"""
|
||||
关闭 WebSocket 连接
|
||||
@@ -132,7 +145,7 @@ class WSConnectionPool:
|
||||
|
||||
async def get_connection(self) -> WSConnection:
|
||||
"""
|
||||
从连接池获取一个连接
|
||||
从连接池获取一个健康的连接,包含健康检查。
|
||||
"""
|
||||
if self._closed:
|
||||
raise WebSocketError("连接池已关闭")
|
||||
@@ -141,18 +154,21 @@ class WSConnectionPool:
|
||||
# 尝试从连接池获取连接
|
||||
conn = await asyncio.wait_for(self.pool.get(), timeout=5)
|
||||
|
||||
# 检查连接是否活跃
|
||||
if not conn.is_active:
|
||||
logger.warning(f"连接 {conn.conn_id} 已失效,重新创建")
|
||||
return await self._create_connection()
|
||||
|
||||
return conn
|
||||
# 健康检查
|
||||
if await conn.ping():
|
||||
logger.debug(f"连接 {conn.conn_id} 健康检查通过")
|
||||
return conn
|
||||
else:
|
||||
logger.warning(f"连接 {conn.conn_id} 健康检查失败,丢弃并获取新连接")
|
||||
await conn.close()
|
||||
return await self.get_connection() # 递归获取下一个
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# 连接池为空,创建新连接
|
||||
logger.warning("连接池为空,创建临时连接")
|
||||
logger.warning("连接池在5秒内无可用连接,创建新连接")
|
||||
return await self._create_connection()
|
||||
except Exception as e:
|
||||
raise WebSocketError(f"获取连接失败: {e}")
|
||||
raise WebSocketError(f"获取连接时发生未知错误: {e}")
|
||||
|
||||
async def release_connection(self, conn: WSConnection):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user