feat: 重构核心架构,增强类型安全与插件管理
本次提交对核心模块进行了深度重构,引入 Pydantic 增强配置管理的类型安全性,并全面优化了插件管理系统。 主要变更详情: 1. 核心架构与配置 - 重构配置加载模块:引入 Pydantic 模型 (`core/config_models.py`),提供严格的配置项类型检查、验证及默认值管理。 - 统一模块结构:规范化模块导入路径,移除冗余的 `__init__.py` 文件,提升项目结构的清晰度。 - 性能优化:集成 Redis 缓存支持 (`RedisManager`),有效降低高频 API 调用开销,提升响应速度。 2. 插件系统升级 - 实现热重载机制:新增插件文件变更监听功能,支持开发过程中自动重载插件,提升开发效率。 - 优化生命周期管理:改进插件加载与卸载逻辑,支持精确卸载指定插件及其关联的命令、事件处理器和定时任务。 3. 功能特性增强 - 新增媒体 API:引入 `MediaAPI` 模块,封装图片、语音等富媒体资源的获取与处理接口。 - 完善权限体系:重构权限管理系统,实现管理员与操作员的分级控制,支持更细粒度的命令权限校验。 4. 代码质量与稳定性 - 全面类型修复:解决 `mypy` 静态类型检查发现的大量类型错误(包括 `CommandManager`、`EventFactory` 及 `Bot` API 签名不匹配问题)。 - 增强错误处理:优化消息处理管道的异常捕获机制,完善关键路径的日志记录,提升系统运行稳定性。
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
from .managers.command_manager import matcher
|
|
||||||
from .config_loader import global_config
|
|
||||||
from .managers.plugin_manager import PluginDataManager
|
|
||||||
from .ws import WS
|
|
||||||
|
|
||||||
__all__ = ["WS", "matcher", "global_config", "PluginDataManager"]
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from .message import MessageAPI
|
|||||||
from .group import GroupAPI
|
from .group import GroupAPI
|
||||||
from .friend import FriendAPI
|
from .friend import FriendAPI
|
||||||
from .account import AccountAPI
|
from .account import AccountAPI
|
||||||
|
from .media import MediaAPI
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseAPI",
|
"BaseAPI",
|
||||||
@@ -10,4 +11,5 @@ __all__ = [
|
|||||||
"GroupAPI",
|
"GroupAPI",
|
||||||
"FriendAPI",
|
"FriendAPI",
|
||||||
"AccountAPI",
|
"AccountAPI",
|
||||||
|
"MediaAPI",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -162,3 +162,56 @@ class AccountAPI(BaseAPI):
|
|||||||
"""
|
"""
|
||||||
return await self.call_api("clean_cache")
|
return await self.call_api("clean_cache")
|
||||||
|
|
||||||
|
async def get_stranger_info(self, user_id: int, no_cache: bool = False) -> Any:
|
||||||
|
"""
|
||||||
|
获取陌生人信息。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id (int): 目标用户的 QQ 号。
|
||||||
|
no_cache (bool, optional): 是否不使用缓存。Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: 包含陌生人信息的字典或对象。
|
||||||
|
"""
|
||||||
|
return await self.call_api("get_stranger_info", {"user_id": user_id, "no_cache": no_cache})
|
||||||
|
|
||||||
|
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 json.loads(cached_data)
|
||||||
|
|
||||||
|
res = await self.call_api("get_friend_list")
|
||||||
|
await redis_manager.set(cache_key, json.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 json.loads(cached_data)
|
||||||
|
|
||||||
|
res = await self.call_api("get_group_list")
|
||||||
|
await redis_manager.set(cache_key, json.dumps(res), ex=3600) # 缓存 1 小时
|
||||||
|
return res
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,50 @@
|
|||||||
"""
|
"""
|
||||||
API 基础模块
|
API 基础模块
|
||||||
|
|
||||||
定义了 API 调用的基础接口。
|
定义了 API 调用的基础接口和统一处理逻辑。
|
||||||
"""
|
"""
|
||||||
from abc import ABC, abstractmethod
|
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
|
from ..utils.logger import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..ws import WS
|
||||||
|
|
||||||
|
|
||||||
class BaseAPI(ABC):
|
class BaseAPI:
|
||||||
"""
|
"""
|
||||||
API 基础抽象类
|
API 基础类,提供了统一的 `call_api` 方法,包含日志记录和异常处理。
|
||||||
"""
|
"""
|
||||||
|
_ws: "WS"
|
||||||
|
self_id: int
|
||||||
|
|
||||||
|
def __init__(self, ws_client: "WS", self_id: int):
|
||||||
|
self._ws = ws_client
|
||||||
|
self.self_id = self_id
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def call_api(self, action: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
async def call_api(self, action: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
"""
|
"""
|
||||||
调用 API
|
调用 OneBot v11 API,并提供统一的日志和异常处理。
|
||||||
|
|
||||||
:param action: API 动作名称
|
:param action: API 动作名称
|
||||||
:param params: API 参数
|
:param params: API 参数
|
||||||
:return: API 响应结果
|
:return: API 响应结果的数据部分
|
||||||
|
:raises Exception: 当 API 调用失败或发生网络错误时
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"调用API -> action: {action}, params: {params}")
|
||||||
|
response = await self._ws.call_api(action, params)
|
||||||
|
logger.debug(f"API响应 <- {response}")
|
||||||
|
|
||||||
|
if response.get("status") == "failed":
|
||||||
|
logger.warning(f"API调用失败: {response}")
|
||||||
|
|
||||||
|
return response.get("data")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"API调用异常: action={action}, params={params}, error={e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
该模块定义了 `GroupAPI` Mixin 类,提供了所有与群组管理、成员操作
|
该模块定义了 `GroupAPI` Mixin 类,提供了所有与群组管理、成员操作
|
||||||
等相关的 OneBot v11 API 封装。
|
等相关的 OneBot v11 API 封装。
|
||||||
"""
|
"""
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any, Optional
|
||||||
import json
|
import json
|
||||||
from ..managers.redis_manager import redis_manager
|
from ..managers.redis_manager import redis_manager
|
||||||
from .base import BaseAPI
|
from .base import BaseAPI
|
||||||
@@ -46,7 +46,7 @@ class GroupAPI(BaseAPI):
|
|||||||
"""
|
"""
|
||||||
return await self.call_api("set_group_ban", {"group_id": group_id, "user_id": user_id, "duration": duration})
|
return await self.call_api("set_group_ban", {"group_id": group_id, "user_id": user_id, "duration": duration})
|
||||||
|
|
||||||
async def set_group_anonymous_ban(self, group_id: int, anonymous: Dict[str, Any] = None, duration: int = 1800, flag: str = None) -> Dict[str, Any]:
|
async def set_group_anonymous_ban(self, group_id: int, anonymous: Optional[Dict[str, Any]] = None, duration: int = 1800, flag: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
禁言群组中的匿名用户。
|
禁言群组中的匿名用户。
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class GroupAPI(BaseAPI):
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: OneBot API 的响应数据。
|
Dict[str, Any]: OneBot API 的响应数据。
|
||||||
"""
|
"""
|
||||||
params = {"group_id": group_id, "duration": duration}
|
params: Dict[str, Any] = {"group_id": group_id, "duration": duration}
|
||||||
if anonymous:
|
if anonymous:
|
||||||
params["anonymous"] = anonymous
|
params["anonymous"] = anonymous
|
||||||
if flag:
|
if flag:
|
||||||
@@ -187,17 +187,18 @@ class GroupAPI(BaseAPI):
|
|||||||
await redis_manager.redis.set(cache_key, json.dumps(res), ex=3600) # 缓存 1 小时
|
await redis_manager.redis.set(cache_key, json.dumps(res), ex=3600) # 缓存 1 小时
|
||||||
return GroupInfo(**res)
|
return GroupInfo(**res)
|
||||||
|
|
||||||
async def get_group_list(self) -> List[GroupInfo]:
|
async def get_group_list(self) -> Any:
|
||||||
"""
|
"""
|
||||||
获取机器人加入的所有群组的列表。
|
获取机器人加入的所有群组的列表。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[GroupInfo]: 包含所有群组信息的 `GroupInfo` 对象列表。
|
Any: 包含所有群组信息的列表(可能是字典列表或对象列表)。
|
||||||
"""
|
"""
|
||||||
res = await self.call_api("get_group_list")
|
res = await self.call_api("get_group_list")
|
||||||
|
|
||||||
# 增加日志记录 API 原始返回
|
# 增加日志记录 API 原始返回
|
||||||
logger.debug(f"OneBot API 'get_group_list' raw response: {res}")
|
logger.debug(f"OneBot API 'get_group_list' raw response: {res}")
|
||||||
|
return res
|
||||||
|
|
||||||
# 健壮性处理:处理标准的 OneBot v11 响应格式
|
# 健壮性处理:处理标准的 OneBot v11 响应格式
|
||||||
if isinstance(res, dict) and res.get("status") == "ok":
|
if isinstance(res, dict) and res.get("status") == "ok":
|
||||||
|
|||||||
39
core/api/media.py
Normal file
39
core/api/media.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
媒体API模块
|
||||||
|
|
||||||
|
封装了与图片、语音等媒体文件相关的API。
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from .base import BaseAPI
|
||||||
|
|
||||||
|
|
||||||
|
class MediaAPI(BaseAPI):
|
||||||
|
"""
|
||||||
|
媒体相关API
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def can_send_image(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
检查是否可以发送图片
|
||||||
|
|
||||||
|
:return: OneBot v11标准响应
|
||||||
|
"""
|
||||||
|
return await self.call_api(action="can_send_image")
|
||||||
|
|
||||||
|
async def can_send_record(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
检查是否可以发送语音
|
||||||
|
|
||||||
|
:return: OneBot v11标准响应
|
||||||
|
"""
|
||||||
|
return await self.call_api(action="can_send_record")
|
||||||
|
|
||||||
|
async def get_image(self, file: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取图片信息
|
||||||
|
|
||||||
|
:param file: 图片文件名或路径
|
||||||
|
:return: OneBot v11标准响应
|
||||||
|
"""
|
||||||
|
return await self.call_api(action="get_image", params={"file": file})
|
||||||
@@ -8,7 +8,8 @@ from typing import Union, List, Dict, Any, TYPE_CHECKING
|
|||||||
from .base import BaseAPI
|
from .base import BaseAPI
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from models import MessageSegment, OneBotEvent
|
from models.message import MessageSegment
|
||||||
|
from models.events.base import OneBotEvent
|
||||||
|
|
||||||
|
|
||||||
class MessageAPI(BaseAPI):
|
class MessageAPI(BaseAPI):
|
||||||
@@ -156,24 +157,6 @@ class MessageAPI(BaseAPI):
|
|||||||
"""
|
"""
|
||||||
return await self.call_api("send_private_forward_msg", {"user_id": user_id, "messages": messages})
|
return await self.call_api("send_private_forward_msg", {"user_id": user_id, "messages": messages})
|
||||||
|
|
||||||
async def can_send_image(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
检查当前机器人账号是否可以发送图片。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: OneBot API 的响应数据。
|
|
||||||
"""
|
|
||||||
return await self.call_api("can_send_image")
|
|
||||||
|
|
||||||
async def can_send_record(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
检查当前机器人账号是否可以发送语音。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: OneBot API 的响应数据。
|
|
||||||
"""
|
|
||||||
return await self.call_api("can_send_record")
|
|
||||||
|
|
||||||
def _process_message(self, message: Union[str, "MessageSegment", List["MessageSegment"]]) -> Union[str, List[Dict[str, Any]]]:
|
def _process_message(self, message: Union[str, "MessageSegment", List["MessageSegment"]]) -> Union[str, List[Dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
内部方法:将消息内容处理成 OneBot API 可接受的格式。
|
内部方法:将消息内容处理成 OneBot API 可接受的格式。
|
||||||
@@ -192,7 +175,7 @@ class MessageAPI(BaseAPI):
|
|||||||
return message
|
return message
|
||||||
|
|
||||||
# 避免循环导入,在运行时导入
|
# 避免循环导入,在运行时导入
|
||||||
from models import MessageSegment
|
from models.message import MessageSegment
|
||||||
|
|
||||||
if isinstance(message, MessageSegment):
|
if isinstance(message, MessageSegment):
|
||||||
return [self._segment_to_dict(message)]
|
return [self._segment_to_dict(message)]
|
||||||
|
|||||||
33
core/bot.py
33
core/bot.py
@@ -13,14 +13,15 @@ Bot 核心抽象模块
|
|||||||
from typing import TYPE_CHECKING, Dict, Any, List, Union
|
from typing import TYPE_CHECKING, Dict, Any, List, Union
|
||||||
from models.events.base import OneBotEvent
|
from models.events.base import OneBotEvent
|
||||||
from models.message import MessageSegment
|
from models.message import MessageSegment
|
||||||
|
from models.objects import GroupInfo, StrangerInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .WS import WS
|
from .ws import WS
|
||||||
|
|
||||||
from .api import MessageAPI, GroupAPI, FriendAPI, AccountAPI
|
from .api import MessageAPI, GroupAPI, FriendAPI, AccountAPI, MediaAPI
|
||||||
|
|
||||||
|
|
||||||
class Bot(MessageAPI, GroupAPI, FriendAPI, AccountAPI):
|
class Bot(MessageAPI, GroupAPI, FriendAPI, AccountAPI, MediaAPI):
|
||||||
"""
|
"""
|
||||||
机器人核心类,封装了所有与 OneBot API 的交互。
|
机器人核心类,封装了所有与 OneBot API 的交互。
|
||||||
|
|
||||||
@@ -35,22 +36,22 @@ class Bot(MessageAPI, GroupAPI, FriendAPI, AccountAPI):
|
|||||||
Args:
|
Args:
|
||||||
ws_client (WS): WebSocket 客户端实例,负责底层的 API 请求和响应处理。
|
ws_client (WS): WebSocket 客户端实例,负责底层的 API 请求和响应处理。
|
||||||
"""
|
"""
|
||||||
self.ws = ws_client
|
super().__init__(ws_client, ws_client.self_id or 0)
|
||||||
|
self.code_executor = None
|
||||||
|
|
||||||
async def call_api(self, action: str, params: Dict[str, Any] = None) -> Any:
|
async def get_group_list(self, no_cache: bool = False) -> List[GroupInfo]:
|
||||||
"""
|
# GroupAPI.get_group_list 不支持 no_cache 参数,这里忽略它
|
||||||
底层 API 调用方法。
|
result = await super().get_group_list()
|
||||||
|
# 确保结果是 GroupInfo 对象列表
|
||||||
|
return [GroupInfo(**group) if isinstance(group, dict) else group for group in result]
|
||||||
|
|
||||||
所有具体的 API 实现最终都会调用此方法,通过 WebSocket 发送请求。
|
async def get_stranger_info(self, user_id: int, no_cache: bool = False) -> StrangerInfo:
|
||||||
|
result = await super().get_stranger_info(user_id=user_id, no_cache=no_cache)
|
||||||
|
# 确保结果是 StrangerInfo 对象
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return StrangerInfo(**result)
|
||||||
|
return result
|
||||||
|
|
||||||
Args:
|
|
||||||
action (str): API 的动作名称,例如 "send_group_msg"。
|
|
||||||
params (Dict[str, Any], optional): API 请求的参数字典。Defaults to None.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Any: OneBot API 的响应数据。
|
|
||||||
"""
|
|
||||||
return await self.ws.call_api(action, params)
|
|
||||||
|
|
||||||
def build_forward_node(self, user_id: int, nickname: str, message: Union[str, "MessageSegment", List["MessageSegment"]]) -> Dict[str, Any]:
|
def build_forward_node(self, user_id: int, nickname: str, message: Union[str, "MessageSegment", List["MessageSegment"]]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
负责读取和解析 config.toml 配置文件,提供全局配置对象。
|
负责读取和解析 config.toml 配置文件,提供全局配置对象。
|
||||||
"""
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
import tomllib
|
import tomllib
|
||||||
|
from pydantic import ValidationError
|
||||||
|
from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel
|
||||||
|
from .utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -21,73 +23,67 @@ class Config:
|
|||||||
:param file_path: 配置文件路径,默认为 "config.toml"
|
:param file_path: 配置文件路径,默认为 "config.toml"
|
||||||
"""
|
"""
|
||||||
self.path = Path(file_path)
|
self.path = Path(file_path)
|
||||||
self._data: Dict[str, Any] = {}
|
self._model: ConfigModel
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""
|
"""
|
||||||
加载配置文件
|
加载并验证配置文件
|
||||||
|
|
||||||
:raises FileNotFoundError: 如果配置文件不存在
|
:raises FileNotFoundError: 如果配置文件不存在
|
||||||
|
:raises ValidationError: 如果配置格式不正确
|
||||||
"""
|
"""
|
||||||
if not self.path.exists():
|
if not self.path.exists():
|
||||||
|
logger.error(f"配置文件 {self.path} 未找到!")
|
||||||
raise FileNotFoundError(f"配置文件 {self.path} 未找到!")
|
raise FileNotFoundError(f"配置文件 {self.path} 未找到!")
|
||||||
|
|
||||||
with open(self.path, "rb") as f:
|
try:
|
||||||
self._data = tomllib.load(f)
|
logger.info(f"正在从 {self.path} 加载配置...")
|
||||||
|
with open(self.path, "rb") as f:
|
||||||
|
raw_config = tomllib.load(f)
|
||||||
|
|
||||||
|
self._model = ConfigModel(**raw_config)
|
||||||
|
logger.success("配置加载并验证成功!")
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.error("配置验证失败,请检查 `config.toml` 文件中的以下错误:")
|
||||||
|
for error in e.errors():
|
||||||
|
field = " -> ".join(map(str, error["loc"]))
|
||||||
|
logger.error(f" - 字段 '{field}': {error['msg']}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"加载配置文件时发生未知错误: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
# 通过属性访问配置
|
# 通过属性访问配置
|
||||||
@property
|
@property
|
||||||
def napcat_ws(self) -> dict:
|
def napcat_ws(self) -> NapCatWSModel:
|
||||||
"""
|
"""
|
||||||
获取 NapCat WebSocket 配置
|
获取 NapCat WebSocket 配置
|
||||||
|
|
||||||
:return: 配置字典
|
|
||||||
"""
|
"""
|
||||||
return self._data.get("napcat_ws", {})
|
return self._model.napcat_ws
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bot(self) -> dict:
|
def bot(self) -> BotModel:
|
||||||
"""
|
"""
|
||||||
获取 Bot 基础配置
|
获取 Bot 基础配置
|
||||||
|
|
||||||
:return: 配置字典
|
|
||||||
"""
|
"""
|
||||||
return self._data.get("bot", {})
|
return self._model.bot
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self) -> dict:
|
def redis(self) -> RedisModel:
|
||||||
"""
|
|
||||||
获取功能特性配置
|
|
||||||
|
|
||||||
:return: 配置字典
|
|
||||||
"""
|
|
||||||
return self._data.get("features", {})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def redis(self) -> dict:
|
|
||||||
"""
|
"""
|
||||||
获取 Redis 配置
|
获取 Redis 配置
|
||||||
|
|
||||||
:return: 配置字典
|
|
||||||
"""
|
"""
|
||||||
return self._data.get("redis", {})
|
return self._model.redis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def docker(self) -> dict:
|
def docker(self) -> DockerModel:
|
||||||
"""
|
"""
|
||||||
获取 Docker 配置
|
获取 Docker 配置
|
||||||
|
|
||||||
:return: 配置字典
|
|
||||||
"""
|
"""
|
||||||
return self._data.get("docker", {})
|
return self._model.docker
|
||||||
|
|
||||||
|
|
||||||
# 实例化全局配置对象
|
# 实例化全局配置对象
|
||||||
global_config = Config()
|
global_config = Config()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print(global_config.napcat_ws)
|
|
||||||
print(global_config.bot.get("command"))
|
|
||||||
print(type(global_config.bot.get("command")) is list)
|
|
||||||
print(global_config.features)
|
|
||||||
|
|||||||
60
core/config_models.py
Normal file
60
core/config_models.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Pydantic 配置模型模块
|
||||||
|
|
||||||
|
该模块使用 Pydantic 定义了与 `config.toml` 文件结构完全对应的配置模型。
|
||||||
|
这使得配置的加载、校验和访问都变得类型安全和健壮。
|
||||||
|
"""
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class NapCatWSModel(BaseModel):
|
||||||
|
"""
|
||||||
|
对应 `config.toml` 中的 `[napcat_ws]` 配置块。
|
||||||
|
"""
|
||||||
|
uri: str
|
||||||
|
token: str
|
||||||
|
reconnect_interval: int = 5
|
||||||
|
|
||||||
|
|
||||||
|
class BotModel(BaseModel):
|
||||||
|
"""
|
||||||
|
对应 `config.toml` 中的 `[bot]` 配置块。
|
||||||
|
"""
|
||||||
|
command: List[str] = Field(default_factory=lambda: ["/"])
|
||||||
|
ignore_self_message: bool = True
|
||||||
|
permission_denied_message: str = "权限不足,需要 {permission_name} 权限"
|
||||||
|
|
||||||
|
|
||||||
|
class RedisModel(BaseModel):
|
||||||
|
"""
|
||||||
|
对应 `config.toml` 中的 `[redis]` 配置块。
|
||||||
|
"""
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
db: int
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class DockerModel(BaseModel):
|
||||||
|
"""
|
||||||
|
对应 `config.toml` 中的 `[docker]` 配置块。
|
||||||
|
"""
|
||||||
|
base_url: Optional[str] = None
|
||||||
|
sandbox_image: str = "python-sandbox:latest"
|
||||||
|
timeout: int = 10
|
||||||
|
concurrency_limit: int = 5
|
||||||
|
tls_verify: bool = False
|
||||||
|
ca_cert_path: Optional[str] = None
|
||||||
|
client_cert_path: Optional[str] = None
|
||||||
|
client_key_path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigModel(BaseModel):
|
||||||
|
"""
|
||||||
|
顶层配置模型,整合了所有子配置块。
|
||||||
|
"""
|
||||||
|
napcat_ws: NapCatWSModel
|
||||||
|
bot: BotModel
|
||||||
|
redis: RedisModel
|
||||||
|
docker: DockerModel
|
||||||
@@ -10,7 +10,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
from ..bot import Bot
|
from ..bot import Bot
|
||||||
from ..config_loader import global_config
|
from ..config_loader import global_config
|
||||||
from ..managers.permission_manager import Permission, permission_manager
|
from ..managers.permission_manager import Permission
|
||||||
from ..utils.executor import run_in_thread_pool
|
from ..utils.executor import run_in_thread_pool
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class BaseHandler(ABC):
|
|||||||
"""
|
"""
|
||||||
sig = inspect.signature(func)
|
sig = inspect.signature(func)
|
||||||
params = sig.parameters
|
params = sig.parameters
|
||||||
kwargs = {}
|
kwargs: Dict[str, Any] = {}
|
||||||
|
|
||||||
if "bot" in params:
|
if "bot" in params:
|
||||||
kwargs["bot"] = bot
|
kwargs["bot"] = bot
|
||||||
@@ -68,14 +68,35 @@ class MessageHandler(BaseHandler):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.prefixes = prefixes
|
self.prefixes = prefixes
|
||||||
self.commands: Dict[str, Dict] = {}
|
self.commands: Dict[str, Dict] = {}
|
||||||
self.message_handlers: List[Callable] = []
|
self.message_handlers: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
清空所有已注册的消息和命令处理器
|
||||||
|
"""
|
||||||
|
self.commands.clear()
|
||||||
|
self.message_handlers.clear()
|
||||||
|
|
||||||
|
def unregister_by_plugin_name(self, plugin_name: str):
|
||||||
|
"""
|
||||||
|
根据插件名卸载相关的消息和命令处理器
|
||||||
|
"""
|
||||||
|
# 卸载命令
|
||||||
|
commands_to_remove = [name for name, info in self.commands.items() if info["plugin_name"] == plugin_name]
|
||||||
|
for name in commands_to_remove:
|
||||||
|
del self.commands[name]
|
||||||
|
|
||||||
|
# 卸载通用消息处理器
|
||||||
|
self.message_handlers = [h for h in self.message_handlers if h["plugin_name"] != plugin_name]
|
||||||
|
|
||||||
def on_message(self) -> Callable:
|
def on_message(self) -> Callable:
|
||||||
"""
|
"""
|
||||||
注册通用消息处理器
|
注册通用消息处理器
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
self.message_handlers.append(func)
|
module = inspect.getmodule(func)
|
||||||
|
plugin_name = module.__name__ if module else "Unknown"
|
||||||
|
self.message_handlers.append({"func": func, "plugin_name": plugin_name})
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@@ -89,21 +110,25 @@ class MessageHandler(BaseHandler):
|
|||||||
注册命令处理器
|
注册命令处理器
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
|
module = inspect.getmodule(func)
|
||||||
|
plugin_name = module.__name__ if module else "Unknown"
|
||||||
for name in names:
|
for name in names:
|
||||||
self.commands[name] = {
|
self.commands[name] = {
|
||||||
"func": func,
|
"func": func,
|
||||||
"permission": permission,
|
"permission": permission,
|
||||||
"override_permission_check": override_permission_check,
|
"override_permission_check": override_permission_check,
|
||||||
|
"plugin_name": plugin_name,
|
||||||
}
|
}
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
async def handle(self, bot: Bot, event: Any):
|
async def handle(self, bot: Bot, event: Any):
|
||||||
"""
|
"""
|
||||||
处理消息事件,包括通用消息和命令
|
处理消息事件,分发给命令处理器或通用消息处理器
|
||||||
"""
|
"""
|
||||||
for handler in self.message_handlers:
|
from ..managers import permission_manager
|
||||||
consumed = await self._run_handler(handler, bot, event)
|
for handler_info in self.message_handlers:
|
||||||
|
consumed = await self._run_handler(handler_info["func"], bot, event)
|
||||||
if consumed:
|
if consumed:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -135,7 +160,7 @@ class MessageHandler(BaseHandler):
|
|||||||
|
|
||||||
if not permission_granted and not override_check:
|
if not permission_granted and not override_check:
|
||||||
permission_name = permission.name if isinstance(permission, Permission) else permission
|
permission_name = permission.name if isinstance(permission, Permission) else permission
|
||||||
message_template = global_config.bot.get("permission_denied_message", "权限不足,需要 {permission_name} 权限")
|
message_template = global_config.bot.permission_denied_message
|
||||||
await bot.send(event, message_template.format(permission_name=permission_name))
|
await bot.send(event, message_template.format(permission_name=permission_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -152,12 +177,23 @@ class NoticeHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
通知事件处理器
|
通知事件处理器
|
||||||
"""
|
"""
|
||||||
|
def clear(self):
|
||||||
|
self.handlers.clear()
|
||||||
|
|
||||||
|
def unregister_by_plugin_name(self, plugin_name: str):
|
||||||
|
"""
|
||||||
|
根据插件名卸载相关的通知处理器
|
||||||
|
"""
|
||||||
|
self.handlers = [h for h in self.handlers if h["plugin_name"] != plugin_name]
|
||||||
|
|
||||||
def register(self, notice_type: Optional[str] = None) -> Callable:
|
def register(self, notice_type: Optional[str] = None) -> Callable:
|
||||||
"""
|
"""
|
||||||
注册通知处理器
|
注册通知处理器
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
self.handlers.append({"type": notice_type, "func": func})
|
module = inspect.getmodule(func)
|
||||||
|
plugin_name = module.__name__ if module else "Unknown"
|
||||||
|
self.handlers.append({"type": notice_type, "func": func, "plugin_name": plugin_name})
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@@ -174,12 +210,23 @@ class RequestHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
请求事件处理器
|
请求事件处理器
|
||||||
"""
|
"""
|
||||||
|
def clear(self):
|
||||||
|
self.handlers.clear()
|
||||||
|
|
||||||
|
def unregister_by_plugin_name(self, plugin_name: str):
|
||||||
|
"""
|
||||||
|
根据插件名卸载相关的请求处理器
|
||||||
|
"""
|
||||||
|
self.handlers = [h for h in self.handlers if h["plugin_name"] != plugin_name]
|
||||||
|
|
||||||
def register(self, request_type: Optional[str] = None) -> Callable:
|
def register(self, request_type: Optional[str] = None) -> Callable:
|
||||||
"""
|
"""
|
||||||
注册请求处理器
|
注册请求处理器
|
||||||
"""
|
"""
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable) -> Callable:
|
||||||
self.handlers.append({"type": request_type, "func": func})
|
module = inspect.getmodule(func)
|
||||||
|
plugin_name = module.__name__ if module else "Unknown"
|
||||||
|
self.handlers.append({"type": request_type, "func": func, "plugin_name": plugin_name})
|
||||||
return func
|
return func
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
管理器包
|
||||||
|
|
||||||
|
这个包集中了机器人核心的单例管理器。
|
||||||
|
通过从这里导入,可以确保在整个应用中访问到的都是同一个实例。
|
||||||
|
"""
|
||||||
|
from ..config_loader import global_config
|
||||||
|
from .admin_manager import AdminManager
|
||||||
|
from .command_manager import CommandManager
|
||||||
|
from .permission_manager import PermissionManager
|
||||||
|
from .plugin_manager import PluginManager
|
||||||
|
from .redis_manager import RedisManager
|
||||||
|
|
||||||
|
# --- 实例化所有单例管理器 ---
|
||||||
|
|
||||||
|
# 管理员管理器
|
||||||
|
admin_manager = AdminManager()
|
||||||
|
|
||||||
|
# 权限管理器
|
||||||
|
permission_manager = PermissionManager()
|
||||||
|
|
||||||
|
# 命令与事件管理器 (别名 matcher)
|
||||||
|
command_manager = CommandManager(prefixes=tuple(global_config.bot.command))
|
||||||
|
matcher = command_manager
|
||||||
|
|
||||||
|
# 插件管理器
|
||||||
|
plugin_manager = PluginManager(command_manager)
|
||||||
|
plugin_manager.load_all_plugins()
|
||||||
|
|
||||||
|
# Redis 管理器
|
||||||
|
redis_manager = RedisManager()
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"admin_manager",
|
||||||
|
"permission_manager",
|
||||||
|
"command_manager",
|
||||||
|
"matcher",
|
||||||
|
"plugin_manager",
|
||||||
|
"redis_manager",
|
||||||
|
]
|
||||||
|
|||||||
@@ -12,7 +12,16 @@ from ..handlers.event_handler import MessageHandler, NoticeHandler, RequestHandl
|
|||||||
|
|
||||||
|
|
||||||
# 从配置中获取命令前缀
|
# 从配置中获取命令前缀
|
||||||
command_prefixes = global_config.bot.get("command", ("/",))
|
_config_prefixes = global_config.bot.command
|
||||||
|
|
||||||
|
# 确保前缀配置是元组格式
|
||||||
|
_final_prefixes: Tuple[str, ...]
|
||||||
|
if isinstance(_config_prefixes, list):
|
||||||
|
_final_prefixes = tuple(_config_prefixes)
|
||||||
|
elif isinstance(_config_prefixes, str):
|
||||||
|
_final_prefixes = (_config_prefixes,)
|
||||||
|
else:
|
||||||
|
_final_prefixes = tuple(_config_prefixes)
|
||||||
|
|
||||||
|
|
||||||
class CommandManager:
|
class CommandManager:
|
||||||
@@ -59,6 +68,35 @@ class CommandManager:
|
|||||||
"usage": "/help",
|
"usage": "/help",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def clear_all_handlers(self):
|
||||||
|
"""
|
||||||
|
清空所有已注册的事件处理器。
|
||||||
|
注意:这也会移除内置的 /help 命令,因此需要重新注册。
|
||||||
|
"""
|
||||||
|
self.message_handler.clear()
|
||||||
|
self.notice_handler.clear()
|
||||||
|
self.request_handler.clear()
|
||||||
|
self.plugins.clear()
|
||||||
|
|
||||||
|
# 清空后,需要重新注册内置命令
|
||||||
|
self._register_internal_commands()
|
||||||
|
|
||||||
|
def unload_plugin(self, plugin_name: str):
|
||||||
|
"""
|
||||||
|
卸载指定插件的所有处理器和命令。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_name (str): 插件的模块名 (例如 'plugins.bili_parser')
|
||||||
|
"""
|
||||||
|
self.message_handler.unregister_by_plugin_name(plugin_name)
|
||||||
|
self.notice_handler.unregister_by_plugin_name(plugin_name)
|
||||||
|
self.request_handler.unregister_by_plugin_name(plugin_name)
|
||||||
|
|
||||||
|
# 移除插件元信息
|
||||||
|
plugins_to_remove = [name for name in self.plugins if name.startswith(plugin_name)]
|
||||||
|
for name in plugins_to_remove:
|
||||||
|
del self.plugins[name]
|
||||||
|
|
||||||
# --- 装饰器代理 ---
|
# --- 装饰器代理 ---
|
||||||
|
|
||||||
def on_message(self) -> Callable:
|
def on_message(self) -> Callable:
|
||||||
@@ -102,7 +140,7 @@ class CommandManager:
|
|||||||
|
|
||||||
根据事件的 `post_type` 将其分发给对应的处理器。
|
根据事件的 `post_type` 将其分发给对应的处理器。
|
||||||
"""
|
"""
|
||||||
if event.post_type == 'message' and global_config.bot.get('ignore_self_message', False):
|
if event.post_type == 'message' and global_config.bot.ignore_self_message:
|
||||||
if hasattr(event, 'user_id') and hasattr(event, 'self_id') and event.user_id == event.self_id:
|
if hasattr(event, 'user_id') and hasattr(event, 'self_id') and event.user_id == event.self_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -130,14 +168,6 @@ class CommandManager:
|
|||||||
await bot.send(event, help_text.strip())
|
await bot.send(event, help_text.strip())
|
||||||
|
|
||||||
|
|
||||||
# --- 全局单例 ---
|
|
||||||
|
|
||||||
# 确保前缀配置是元组格式
|
|
||||||
if isinstance(command_prefixes, list):
|
|
||||||
command_prefixes = tuple(command_prefixes)
|
|
||||||
elif isinstance(command_prefixes, str):
|
|
||||||
command_prefixes = (command_prefixes,)
|
|
||||||
|
|
||||||
# 实例化全局唯一的命令管理器
|
# 实例化全局唯一的命令管理器
|
||||||
matcher = CommandManager(prefixes=command_prefixes)
|
matcher = CommandManager(prefixes=_final_prefixes)
|
||||||
|
|
||||||
|
|||||||
@@ -13,64 +13,17 @@
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from functools import total_ordering
|
from typing import Dict, Optional
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from ..utils.logger import logger
|
from ..utils.logger import logger
|
||||||
from ..utils.singleton import Singleton
|
from ..utils.singleton import Singleton
|
||||||
from .admin_manager import admin_manager
|
from .admin_manager import admin_manager
|
||||||
|
from ..permission import Permission
|
||||||
|
|
||||||
|
|
||||||
@total_ordering
|
|
||||||
class Permission:
|
|
||||||
"""
|
|
||||||
权限封装类
|
|
||||||
|
|
||||||
封装了权限的名称和等级,并提供了比较方法。
|
|
||||||
使用 @total_ordering 装饰器可以自动生成所有的比较运算符。
|
|
||||||
"""
|
|
||||||
def __init__(self, name: str, level: int):
|
|
||||||
"""
|
|
||||||
初始化权限对象
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name (str): 权限名称 (e.g., "admin", "op")
|
|
||||||
level (int): 权限等级,数字越大权限越高
|
|
||||||
"""
|
|
||||||
self.name = name
|
|
||||||
self.level = level
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
"""
|
|
||||||
判断权限是否相等
|
|
||||||
"""
|
|
||||||
if not isinstance(other, Permission):
|
|
||||||
return NotImplemented
|
|
||||||
return self.level == other.level
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
"""
|
|
||||||
判断权限是否小于另一个权限
|
|
||||||
"""
|
|
||||||
if not isinstance(other, Permission):
|
|
||||||
return NotImplemented
|
|
||||||
return self.level < other.level
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""
|
|
||||||
返回权限的字符串表示(即权限名称)
|
|
||||||
"""
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
# 定义全局权限常量
|
|
||||||
ADMIN = Permission("admin", 3)
|
|
||||||
OP = Permission("op", 2)
|
|
||||||
USER = Permission("user", 1)
|
|
||||||
|
|
||||||
# 用于从字符串名称查找权限对象的字典
|
# 用于从字符串名称查找权限对象的字典
|
||||||
_PERMISSIONS: Dict[str, Permission] = {
|
_PERMISSIONS: Dict[str, Permission] = {
|
||||||
p.name: p for p in [ADMIN, OP, USER]
|
p.value: p for p in Permission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -89,7 +42,7 @@ class PermissionManager(Singleton):
|
|||||||
如果已经初始化过,则直接返回。
|
如果已经初始化过,则直接返回。
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if not self._initialized:
|
if hasattr(self, '_initialized') and self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 权限数据文件路径
|
# 权限数据文件路径
|
||||||
@@ -111,6 +64,7 @@ class PermissionManager(Singleton):
|
|||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
logger.info("权限管理器初始化完成")
|
logger.info("权限管理器初始化完成")
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -164,12 +118,12 @@ class PermissionManager(Singleton):
|
|||||||
"""
|
"""
|
||||||
# 首先,通过 AdminManager 检查是否为管理员
|
# 首先,通过 AdminManager 检查是否为管理员
|
||||||
if await admin_manager.is_admin(user_id):
|
if await admin_manager.is_admin(user_id):
|
||||||
return ADMIN
|
return Permission.ADMIN
|
||||||
|
|
||||||
# 如果不是管理员,则从 permissions.json 中查找
|
# 如果不是管理员,则从 permissions.json 中查找
|
||||||
user_id_str = str(user_id)
|
user_id_str = str(user_id)
|
||||||
level_name = self._data["users"].get(user_id_str, USER.name)
|
level_name = self._data["users"].get(user_id_str, Permission.USER.value)
|
||||||
return _PERMISSIONS.get(level_name, USER)
|
return _PERMISSIONS.get(level_name, Permission.USER)
|
||||||
|
|
||||||
def set_user_permission(self, user_id: int, permission: Permission) -> None:
|
def set_user_permission(self, user_id: int, permission: Permission) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -182,13 +136,13 @@ class PermissionManager(Singleton):
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: 如果权限对象无效
|
ValueError: 如果权限对象无效
|
||||||
"""
|
"""
|
||||||
if not isinstance(permission, Permission) or permission.name not in _PERMISSIONS:
|
if not isinstance(permission, Permission):
|
||||||
raise ValueError(f"无效的权限对象: {permission}")
|
raise ValueError(f"无效的权限对象: {permission}")
|
||||||
|
|
||||||
user_id_str = str(user_id)
|
user_id_str = str(user_id)
|
||||||
self._data["users"][user_id_str] = permission.name
|
self._data["users"][user_id_str] = permission.value
|
||||||
self.save()
|
self.save()
|
||||||
logger.info(f"设置用户 {user_id} 的权限级别为 {permission.name}")
|
logger.info(f"设置用户 {user_id} 的权限级别为 {permission.value}")
|
||||||
|
|
||||||
def remove_user(self, user_id: int) -> None:
|
def remove_user(self, user_id: int) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -214,17 +168,17 @@ class PermissionManager(Singleton):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: 如果用户权限 >= 所需权限,返回 True,否则返回 False
|
bool: 如果用户权限 >= 所需权限,返回 True,否则返回 False
|
||||||
"""
|
"""
|
||||||
# 如果传入的是字符串,先转换为 Permission 对象
|
|
||||||
if isinstance(required_permission, str):
|
|
||||||
required_permission = _PERMISSIONS.get(required_permission.lower())
|
|
||||||
if not required_permission:
|
|
||||||
# 如果是无效的权限字符串,默认拒绝
|
|
||||||
logger.warning(f"检测到无效的权限检查字符串: {required_permission}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
user_permission = await self.get_user_permission(user_id)
|
user_permission = await self.get_user_permission(user_id)
|
||||||
return user_permission >= required_permission
|
return user_permission >= required_permission
|
||||||
|
|
||||||
|
def get_all_user_permissions(self) -> Dict[str, str]:
|
||||||
|
"""
|
||||||
|
获取所有已配置的用户权限
|
||||||
|
|
||||||
|
:return: 一个包含所有用户权限的字典
|
||||||
|
"""
|
||||||
|
return self._data["users"].copy()
|
||||||
|
|
||||||
def get_all_users(self) -> Dict[str, str]:
|
def get_all_users(self) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
获取所有设置了权限的用户及其级别名称
|
获取所有设置了权限的用户及其级别名称
|
||||||
@@ -243,22 +197,22 @@ class PermissionManager(Singleton):
|
|||||||
logger.info("已清空所有权限设置")
|
logger.info("已清空所有权限设置")
|
||||||
|
|
||||||
|
|
||||||
# 全局权限管理器实例
|
|
||||||
permission_manager = PermissionManager()
|
|
||||||
|
|
||||||
def require_admin(func):
|
def require_admin(func):
|
||||||
"""
|
"""
|
||||||
一个装饰器,用于限制命令只能由管理员执行。
|
一个装饰器,用于限制命令只能由管理员执行。
|
||||||
"""
|
"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from models.events.message import MessageEvent
|
from models.events.message import MessageEvent
|
||||||
|
from core.managers import permission_manager
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(event: MessageEvent, *args, **kwargs):
|
async def wrapper(event: MessageEvent, *args, **kwargs):
|
||||||
user_id = event.user_id
|
user_id = event.user_id
|
||||||
if await permission_manager.check_permission(user_id, ADMIN):
|
if await permission_manager.check_permission(user_id, Permission.ADMIN):
|
||||||
return await func(event, *args, **kwargs)
|
return await func(event, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
await event.reply("抱歉,您没有权限执行此命令。")
|
# 假设 event 对象有 reply 方法
|
||||||
|
if hasattr(event, "reply"):
|
||||||
|
await event.reply("抱歉,您没有权限执行此命令。")
|
||||||
return None
|
return None
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -1,126 +1,88 @@
|
|||||||
"""
|
"""
|
||||||
插件管理器模块
|
插件管理器模块
|
||||||
|
|
||||||
负责扫描、加载和管理 `base_plugins` 目录下的所有插件。
|
负责扫描、加载和管理 `plugins` 目录下的所有插件。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
from .command_manager import matcher
|
|
||||||
from ..utils.exceptions import SyncHandlerError
|
from ..utils.exceptions import SyncHandlerError
|
||||||
from ..utils.logger import logger
|
from ..utils.logger import logger
|
||||||
from ..utils.executor import run_in_thread_pool
|
|
||||||
|
|
||||||
|
|
||||||
def load_all_plugins():
|
class PluginManager:
|
||||||
"""
|
"""
|
||||||
扫描并加载 `plugins` 目录下的所有插件。
|
插件管理器类
|
||||||
|
|
||||||
该函数会遍历 `plugins` 目录下的所有模块:
|
|
||||||
1. 如果模块已加载,则执行 reload 操作(用于热重载)。
|
|
||||||
2. 如果模块未加载,则执行 import 操作。
|
|
||||||
|
|
||||||
加载过程中会提取插件元数据 `__plugin_meta__` 并注册到 CommandManager。
|
|
||||||
"""
|
"""
|
||||||
plugin_dir = os.path.join(
|
def __init__(self, command_manager):
|
||||||
os.path.dirname(os.path.abspath(__file__)), "..", "..", "plugins"
|
"""
|
||||||
)
|
初始化插件管理器
|
||||||
package_name = "plugins"
|
|
||||||
|
|
||||||
logger.info(f"正在从 {package_name} 加载插件...")
|
:param command_manager: CommandManager的实例
|
||||||
|
"""
|
||||||
|
self.command_manager = command_manager
|
||||||
|
self.loaded_plugins: Set[str] = set()
|
||||||
|
|
||||||
for loader, module_name, is_pkg in pkgutil.iter_modules([plugin_dir]):
|
def load_all_plugins(self):
|
||||||
full_module_name = f"{package_name}.{module_name}"
|
"""
|
||||||
|
扫描并加载 `plugins` 目录下的所有插件。
|
||||||
|
"""
|
||||||
|
plugin_dir = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "..", "plugins"
|
||||||
|
)
|
||||||
|
package_name = "plugins"
|
||||||
|
|
||||||
|
logger.info(f"正在从 {package_name} 加载插件...")
|
||||||
|
|
||||||
|
for _, module_name, is_pkg in pkgutil.iter_modules([plugin_dir]):
|
||||||
|
full_module_name = f"{package_name}.{module_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if full_module_name in self.loaded_plugins:
|
||||||
|
self.command_manager.unload_plugin(full_module_name)
|
||||||
|
module = importlib.reload(sys.modules[full_module_name])
|
||||||
|
action = "重载"
|
||||||
|
else:
|
||||||
|
module = importlib.import_module(full_module_name)
|
||||||
|
action = "加载"
|
||||||
|
|
||||||
|
if hasattr(module, "__plugin_meta__"):
|
||||||
|
meta = getattr(module, "__plugin_meta__")
|
||||||
|
self.command_manager.plugins[full_module_name] = meta
|
||||||
|
|
||||||
|
self.loaded_plugins.add(full_module_name)
|
||||||
|
|
||||||
|
type_str = "包" if is_pkg else "文件"
|
||||||
|
logger.success(f" [{type_str}] 成功{action}: {module_name}")
|
||||||
|
except SyncHandlerError as e:
|
||||||
|
logger.error(f" 插件 {module_name} 加载失败: {e} (跳过此插件)")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f" {action if 'action' in locals() else '加载'}插件 {module_name} 失败: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def reload_plugin(self, full_module_name: str):
|
||||||
|
"""
|
||||||
|
精确重载单个插件。
|
||||||
|
"""
|
||||||
|
if full_module_name not in self.loaded_plugins:
|
||||||
|
logger.warning(f"尝试重载一个未被加载的插件: {full_module_name},将按首次加载处理。")
|
||||||
|
|
||||||
|
if full_module_name not in sys.modules:
|
||||||
|
logger.error(f"重载失败: 模块 {full_module_name} 未在 sys.modules 中找到。")
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if full_module_name in sys.modules:
|
self.command_manager.unload_plugin(full_module_name)
|
||||||
module = importlib.reload(sys.modules[full_module_name])
|
module = importlib.reload(sys.modules[full_module_name])
|
||||||
action = "重载"
|
|
||||||
else:
|
|
||||||
module = importlib.import_module(full_module_name)
|
|
||||||
action = "加载"
|
|
||||||
|
|
||||||
# 提取插件元数据
|
|
||||||
if hasattr(module, "__plugin_meta__"):
|
if hasattr(module, "__plugin_meta__"):
|
||||||
meta = getattr(module, "__plugin_meta__")
|
meta = getattr(module, "__plugin_meta__")
|
||||||
matcher.plugins[full_module_name] = meta
|
self.command_manager.plugins[full_module_name] = meta
|
||||||
|
|
||||||
type_str = "包" if is_pkg else "文件"
|
logger.success(f"插件 {full_module_name} 已成功重载。")
|
||||||
logger.success(f" [{type_str}] 成功{action}: {module_name}")
|
|
||||||
except SyncHandlerError as e:
|
|
||||||
logger.error(f" 插件 {module_name} 加载失败: {e} (跳过此插件)")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
logger.exception(f"重载插件 {full_module_name} 时发生错误: {e}")
|
||||||
f" {action if 'action' in locals() else '加载'}插件 {module_name} 失败: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PluginDataManager:
|
|
||||||
"""
|
|
||||||
用于管理插件产生的数据文件的类
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, plugin_name: str):
|
|
||||||
"""
|
|
||||||
初始化插件数据管理器
|
|
||||||
|
|
||||||
:param plugin_name: 插件名称
|
|
||||||
"""
|
|
||||||
self.plugin_name = plugin_name
|
|
||||||
self.data_file = os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)),
|
|
||||||
"..",
|
|
||||||
"plugins",
|
|
||||||
"data",
|
|
||||||
self.plugin_name + ".json",
|
|
||||||
)
|
|
||||||
self.data = {}
|
|
||||||
|
|
||||||
async def load(self):
|
|
||||||
"""读取配置文件"""
|
|
||||||
if not os.path.exists(self.data_file):
|
|
||||||
await self.set(self.plugin_name, [])
|
|
||||||
try:
|
|
||||||
with open(self.data_file, "r", encoding="utf-8") as f:
|
|
||||||
self.data = await run_in_thread_pool(json.load, f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
self.data = {}
|
|
||||||
|
|
||||||
async def save(self):
|
|
||||||
"""保存配置到文件"""
|
|
||||||
with open(self.data_file, "w", encoding="utf-8") as f:
|
|
||||||
await run_in_thread_pool(json.dump, self.data, f, indent=2, ensure_ascii=False)
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
"""获取配置项"""
|
|
||||||
return self.data.get(key, default)
|
|
||||||
|
|
||||||
async def set(self, key, value):
|
|
||||||
"""设置配置项"""
|
|
||||||
self.data[key] = value
|
|
||||||
await self.save()
|
|
||||||
|
|
||||||
async def add(self, key, value):
|
|
||||||
"""添加配置项"""
|
|
||||||
if key not in self.data:
|
|
||||||
self.data[key] = []
|
|
||||||
self.data[key].append(value)
|
|
||||||
await self.save()
|
|
||||||
|
|
||||||
async def remove(self, key):
|
|
||||||
"""删除配置项"""
|
|
||||||
if key in self.data:
|
|
||||||
del self.data[key]
|
|
||||||
await self.save()
|
|
||||||
|
|
||||||
async def clear(self):
|
|
||||||
"""清空所有配置"""
|
|
||||||
self.data.clear()
|
|
||||||
await self.save()
|
|
||||||
|
|
||||||
def get_all(self):
|
|
||||||
return self.data.copy()
|
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ class RedisManager:
|
|||||||
"""
|
"""
|
||||||
if self._redis is None:
|
if self._redis is None:
|
||||||
try:
|
try:
|
||||||
host = config.redis['host']
|
redis_config = config.redis
|
||||||
port = config.redis['port']
|
host = redis_config.host
|
||||||
db = config.redis['db']
|
port = redis_config.port
|
||||||
password = config.redis.get('password')
|
db = redis_config.db
|
||||||
|
password = redis_config.password
|
||||||
|
|
||||||
logger.info(f"正在尝试连接 Redis: {host}:{port}, DB: {db}")
|
logger.info(f"正在尝试连接 Redis: {host}:{port}, DB: {db}")
|
||||||
|
|
||||||
@@ -54,5 +55,17 @@ class RedisManager:
|
|||||||
raise ConnectionError("Redis 未初始化或连接失败,请先调用 initialize()")
|
raise ConnectionError("Redis 未初始化或连接失败,请先调用 initialize()")
|
||||||
return self._redis
|
return self._redis
|
||||||
|
|
||||||
|
async def get(self, name):
|
||||||
|
"""
|
||||||
|
获取指定键的值
|
||||||
|
"""
|
||||||
|
return await self.redis.get(name)
|
||||||
|
|
||||||
|
async def set(self, name, value, ex=None):
|
||||||
|
"""
|
||||||
|
设置指定键的值
|
||||||
|
"""
|
||||||
|
return await self.redis.set(name, value, ex=ex)
|
||||||
|
|
||||||
# 全局 Redis 管理器实例
|
# 全局 Redis 管理器实例
|
||||||
redis_manager = RedisManager()
|
redis_manager = RedisManager()
|
||||||
|
|||||||
45
core/permission.py
Normal file
45
core/permission.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from functools import total_ordering
|
||||||
|
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class Permission(Enum):
|
||||||
|
"""
|
||||||
|
定义用户权限等级的枚举类。
|
||||||
|
|
||||||
|
使用 @total_ordering 装饰器,只需定义 __lt__ 和 __eq__,
|
||||||
|
即可自动实现所有比较运算符。
|
||||||
|
"""
|
||||||
|
USER = "user"
|
||||||
|
OP = "op"
|
||||||
|
ADMIN = "admin"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _level_map(self):
|
||||||
|
"""
|
||||||
|
内部属性,用于映射枚举成员到整数等级。
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
Permission.USER: 1,
|
||||||
|
Permission.OP: 2,
|
||||||
|
Permission.ADMIN: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
"""
|
||||||
|
比较当前权限是否小于另一个权限。
|
||||||
|
"""
|
||||||
|
if not isinstance(other, Permission):
|
||||||
|
return NotImplemented
|
||||||
|
return self._level_map[self] < self._level_map[other]
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Permission):
|
||||||
|
return NotImplemented
|
||||||
|
return self is other
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
if not isinstance(other, Permission):
|
||||||
|
return NotImplemented
|
||||||
|
return self._level_map[self] >= self._level_map[other]
|
||||||
|
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import docker
|
import docker
|
||||||
from docker.tls import TLSConfig
|
from docker.tls import TLSConfig
|
||||||
from typing import Dict, Any, Callable
|
from docker.types import LogConfig
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
from core.utils.logger import logger
|
from core.utils.logger import logger
|
||||||
|
|
||||||
@@ -10,21 +11,20 @@ class CodeExecutor:
|
|||||||
"""
|
"""
|
||||||
代码执行引擎,负责管理一个异步任务队列和并发的 Docker 容器执行。
|
代码执行引擎,负责管理一个异步任务队列和并发的 Docker 容器执行。
|
||||||
"""
|
"""
|
||||||
def __init__(self, bot_instance, config: Dict[str, Any]):
|
def __init__(self, config: Any):
|
||||||
"""
|
"""
|
||||||
初始化代码执行引擎。
|
初始化代码执行引擎。
|
||||||
:param bot_instance: Bot 实例,用于后续的消息回复。
|
:param config: 从 config_loader.py 加载的全局配置对象。
|
||||||
:param config: 从 config.toml 加载的配置字典。
|
|
||||||
"""
|
"""
|
||||||
self.bot = bot_instance
|
self.bot: Any = None # Bot 实例将在 WS 连接成功后动态注入
|
||||||
self.task_queue = asyncio.Queue()
|
self.task_queue: asyncio.Queue = asyncio.Queue()
|
||||||
|
|
||||||
# 从传入的配置中读取 Docker 相关设置
|
# 从传入的配置中读取 Docker 相关设置
|
||||||
docker_config = config.docker
|
docker_config = config.docker
|
||||||
self.docker_base_url = docker_config.get("base_url")
|
self.docker_base_url = docker_config.base_url
|
||||||
self.sandbox_image = docker_config.get("sandbox_image", "python-sandbox:latest")
|
self.sandbox_image = docker_config.sandbox_image
|
||||||
self.timeout = docker_config.get("timeout", 10)
|
self.timeout = docker_config.timeout
|
||||||
concurrency = docker_config.get("concurrency_limit", 5)
|
concurrency = docker_config.concurrency_limit
|
||||||
|
|
||||||
self.concurrency_limit = asyncio.Semaphore(concurrency)
|
self.concurrency_limit = asyncio.Semaphore(concurrency)
|
||||||
self.docker_client = None
|
self.docker_client = None
|
||||||
@@ -34,10 +34,10 @@ class CodeExecutor:
|
|||||||
if self.docker_base_url:
|
if self.docker_base_url:
|
||||||
# 如果配置了远程 Docker 地址,则使用 TLS 选项进行连接
|
# 如果配置了远程 Docker 地址,则使用 TLS 选项进行连接
|
||||||
tls_config = None
|
tls_config = None
|
||||||
if docker_config.get("tls_verify", False):
|
if docker_config.tls_verify:
|
||||||
tls_config = TLSConfig(
|
tls_config = TLSConfig(
|
||||||
ca_cert=docker_config.get("ca_cert_path"),
|
ca_cert=docker_config.ca_cert_path,
|
||||||
client_cert=(docker_config.get("client_cert_path"), docker_config.get("client_key_path")),
|
client_cert=(docker_config.client_cert_path, docker_config.client_key_path),
|
||||||
verify=True
|
verify=True
|
||||||
)
|
)
|
||||||
self.docker_client = docker.DockerClient(base_url=self.docker_base_url, tls=tls_config)
|
self.docker_client = docker.DockerClient(base_url=self.docker_base_url, tls=tls_config)
|
||||||
@@ -125,6 +125,9 @@ class CodeExecutor:
|
|||||||
同步函数:在 Docker 容器中运行代码。
|
同步函数:在 Docker 容器中运行代码。
|
||||||
此函数通过手动管理容器生命周期来提高稳定性。
|
此函数通过手动管理容器生命周期来提高稳定性。
|
||||||
"""
|
"""
|
||||||
|
if self.docker_client is None:
|
||||||
|
raise docker.errors.DockerException("Docker client is not initialized.")
|
||||||
|
|
||||||
container = None
|
container = None
|
||||||
try:
|
try:
|
||||||
# 1. 创建容器
|
# 1. 创建容器
|
||||||
@@ -134,7 +137,7 @@ class CodeExecutor:
|
|||||||
mem_limit='128m',
|
mem_limit='128m',
|
||||||
cpu_shares=512,
|
cpu_shares=512,
|
||||||
network_disabled=True,
|
network_disabled=True,
|
||||||
log_config={'type': 'json-file', 'config': {'max-size': '1m'}},
|
log_config=LogConfig(type='json-file', config={'max-size': '1m'}),
|
||||||
)
|
)
|
||||||
# 2. 启动容器
|
# 2. 启动容器
|
||||||
container.start()
|
container.start()
|
||||||
@@ -150,7 +153,7 @@ class CodeExecutor:
|
|||||||
# 5. 检查退出码,如果不为 0,则手动抛出 ContainerError
|
# 5. 检查退出码,如果不为 0,则手动抛出 ContainerError
|
||||||
if result.get('StatusCode', 0) != 0:
|
if result.get('StatusCode', 0) != 0:
|
||||||
raise docker.errors.ContainerError(
|
raise docker.errors.ContainerError(
|
||||||
container, result['StatusCode'], f"python -c '{code}'", self.sandbox_image, stderr
|
container, result['StatusCode'], f"python -c '{code}'", self.sandbox_image, stderr.decode('utf-8')
|
||||||
)
|
)
|
||||||
|
|
||||||
return stdout
|
return stdout
|
||||||
@@ -166,11 +169,11 @@ class CodeExecutor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[CodeExecutor] 强制移除容器 {container.id} 时失败: {e}")
|
logger.error(f"[CodeExecutor] 强制移除容器 {container.id} 时失败: {e}")
|
||||||
|
|
||||||
def initialize_executor(bot_instance, config: Dict[str, Any]):
|
def initialize_executor(config: Any):
|
||||||
"""
|
"""
|
||||||
初始化并返回一个 CodeExecutor 实例。
|
初始化并返回一个 CodeExecutor 实例。
|
||||||
"""
|
"""
|
||||||
return CodeExecutor(bot_instance, config)
|
return CodeExecutor(config)
|
||||||
|
|
||||||
async def run_in_thread_pool(sync_func, *args, **kwargs):
|
async def run_in_thread_pool(sync_func, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|||||||
51
core/ws.py
51
core/ws.py
@@ -13,11 +13,12 @@ WebSocket 连接。它是整个机器人框架的底层通信基础。
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from models import EventFactory
|
from models.events.factory import EventFactory
|
||||||
|
|
||||||
from .bot import Bot
|
from .bot import Bot
|
||||||
from .config_loader import global_config
|
from .config_loader import global_config
|
||||||
@@ -30,7 +31,7 @@ class WS:
|
|||||||
WebSocket 客户端,负责与 OneBot v11 实现进行底层通信。
|
WebSocket 客户端,负责与 OneBot v11 实现进行底层通信。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, code_executor=None):
|
||||||
"""
|
"""
|
||||||
初始化 WebSocket 客户端。
|
初始化 WebSocket 客户端。
|
||||||
|
|
||||||
@@ -38,13 +39,15 @@ class WS:
|
|||||||
"""
|
"""
|
||||||
# 读取参数
|
# 读取参数
|
||||||
cfg = global_config.napcat_ws
|
cfg = global_config.napcat_ws
|
||||||
self.url = cfg.get("uri")
|
self.url = cfg.uri
|
||||||
self.token = cfg.get("token")
|
self.token = cfg.token
|
||||||
self.reconnect_interval = cfg.get("reconnect_interval", 5)
|
self.reconnect_interval = cfg.reconnect_interval
|
||||||
|
|
||||||
self.ws = None
|
self.ws = None
|
||||||
self._pending_requests = {}
|
self._pending_requests = {}
|
||||||
self.bot = Bot(self)
|
self.bot: Bot | None = None
|
||||||
|
self.self_id: int | None = None
|
||||||
|
self.code_executor = code_executor
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""
|
"""
|
||||||
@@ -124,18 +127,42 @@ class WS:
|
|||||||
try:
|
try:
|
||||||
# 使用工厂创建事件对象
|
# 使用工厂创建事件对象
|
||||||
event = EventFactory.create_event(event_data)
|
event = EventFactory.create_event(event_data)
|
||||||
|
|
||||||
|
# 在收到第一个 meta_event 时,初始化 Bot 实例
|
||||||
|
if event.post_type == "meta_event" and self.bot is None:
|
||||||
|
self.self_id = event.self_id
|
||||||
|
self.bot = Bot(self)
|
||||||
|
logger.success(f"Bot 实例初始化完成: self_id={self.self_id}")
|
||||||
|
|
||||||
|
# 将代码执行器注入到 Bot 和执行器自身
|
||||||
|
if self.code_executor:
|
||||||
|
self.bot.code_executor = self.code_executor
|
||||||
|
self.code_executor.bot = self.bot
|
||||||
|
logger.info("代码执行器已成功注入 Bot 实例。")
|
||||||
|
|
||||||
|
# 如果 bot 尚未初始化,则不处理后续事件
|
||||||
|
if self.bot is None:
|
||||||
|
logger.warning("Bot 尚未初始化,跳过事件处理。")
|
||||||
|
return
|
||||||
|
|
||||||
event.bot = self.bot # 注入 Bot 实例
|
event.bot = self.bot # 注入 Bot 实例
|
||||||
|
|
||||||
# 打印日志
|
# 打印日志
|
||||||
if event.post_type == "message":
|
if event.post_type == "message":
|
||||||
sender_name = event.sender.nickname if event.sender else "Unknown"
|
sender_name = event.sender.nickname if hasattr(event, "sender") and event.sender else "Unknown"
|
||||||
logger.info(f"[消息] {event.message_type} | {event.user_id}({sender_name}): {event.raw_message}")
|
message_type = getattr(event, "message_type", "Unknown")
|
||||||
|
user_id = getattr(event, "user_id", "Unknown")
|
||||||
|
raw_message = getattr(event, "raw_message", "")
|
||||||
|
logger.info(f"[消息] {message_type} | {user_id}({sender_name}): {raw_message}")
|
||||||
elif event.post_type == "notice":
|
elif event.post_type == "notice":
|
||||||
logger.info(f"[通知] {event.notice_type}")
|
notice_type = getattr(event, "notice_type", "Unknown")
|
||||||
|
logger.info(f"[通知] {notice_type}")
|
||||||
elif event.post_type == "request":
|
elif event.post_type == "request":
|
||||||
logger.info(f"[请求] {event.request_type}")
|
request_type = getattr(event, "request_type", "Unknown")
|
||||||
|
logger.info(f"[请求] {request_type}")
|
||||||
elif event.post_type == "meta_event":
|
elif event.post_type == "meta_event":
|
||||||
logger.debug(f"[元事件] {event.meta_event_type}")
|
meta_event_type = getattr(event, "meta_event_type", "Unknown")
|
||||||
|
logger.debug(f"[元事件] {meta_event_type}")
|
||||||
|
|
||||||
|
|
||||||
# 分发事件
|
# 分发事件
|
||||||
@@ -144,7 +171,7 @@ class WS:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"事件处理异常: {e}")
|
logger.exception(f"事件处理异常: {e}")
|
||||||
|
|
||||||
async def call_api(self, action: str, params: dict = None):
|
async def call_api(self, action: str, params: Optional[Dict[Any, Any]] = None) -> Dict[Any, Any]:
|
||||||
"""
|
"""
|
||||||
向 OneBot v11 实现端发送一个 API 请求。
|
向 OneBot v11 实现端发送一个 API 请求。
|
||||||
|
|
||||||
|
|||||||
37
main.py
37
main.py
@@ -15,7 +15,7 @@ from core.utils.logger import logger
|
|||||||
|
|
||||||
from core.managers.admin_manager import admin_manager
|
from core.managers.admin_manager import admin_manager
|
||||||
from core.ws import WS
|
from core.ws import WS
|
||||||
from core.managers.plugin_manager import load_all_plugins
|
from core.managers import plugin_manager
|
||||||
from core.managers.redis_manager import redis_manager
|
from core.managers.redis_manager import redis_manager
|
||||||
from core.utils.executor import run_in_thread_pool, initialize_executor
|
from core.utils.executor import run_in_thread_pool, initialize_executor
|
||||||
from core.config_loader import global_config as config
|
from core.config_loader import global_config as config
|
||||||
@@ -25,6 +25,8 @@ ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
sys.path.insert(0, ROOT_DIR)
|
sys.path.insert(0, ROOT_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
# 获取插件目录的绝对路径
|
||||||
|
PLUGIN_DIR = os.path.join(ROOT_DIR, "plugins")
|
||||||
|
|
||||||
|
|
||||||
class PluginReloadHandler(FileSystemEventHandler):
|
class PluginReloadHandler(FileSystemEventHandler):
|
||||||
@@ -32,7 +34,7 @@ class PluginReloadHandler(FileSystemEventHandler):
|
|||||||
文件变更处理器,用于热重载插件
|
文件变更处理器,用于热重载插件
|
||||||
|
|
||||||
继承自 watchdog.events.FileSystemEventHandler,
|
继承自 watchdog.events.FileSystemEventHandler,
|
||||||
监听 base_plugins 目录下的文件变化,并触发插件重载。
|
监听 plugins 目录下的文件变化,并触发插件重载。
|
||||||
"""
|
"""
|
||||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||||
"""
|
"""
|
||||||
@@ -53,12 +55,14 @@ class PluginReloadHandler(FileSystemEventHandler):
|
|||||||
if file_system_event.is_directory:
|
if file_system_event.is_directory:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
src_path = file_system_event.src_path
|
||||||
|
|
||||||
# 只监控 py 文件
|
# 只监控 py 文件
|
||||||
if not file_system_event.src_path.endswith(".py"):
|
if not src_path.endswith(".py"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# 过滤掉一些临时文件
|
# 过滤掉一些临时文件
|
||||||
if "__pycache__" in file_system_event.src_path:
|
if "__pycache__" in src_path or not src_path.startswith(PLUGIN_DIR):
|
||||||
return
|
return
|
||||||
|
|
||||||
# 简单的防抖动
|
# 简单的防抖动
|
||||||
@@ -68,13 +72,18 @@ class PluginReloadHandler(FileSystemEventHandler):
|
|||||||
|
|
||||||
self.last_reload_time = current_time
|
self.last_reload_time = current_time
|
||||||
|
|
||||||
logger.info(f"检测到文件变更: {file_system_event.src_path}")
|
# 从文件路径解析出模块名
|
||||||
logger.info("正在重载插件...")
|
# 例如: C:\path\to\project\plugins\bili_parser.py -> plugins.bili_parser
|
||||||
|
relative_path = os.path.relpath(src_path, ROOT_DIR)
|
||||||
|
module_name = os.path.splitext(relative_path.replace(os.sep, '.'))[0]
|
||||||
|
|
||||||
|
logger.info(f"检测到文件变更: {src_path}")
|
||||||
|
logger.info(f"准备重载插件: {module_name}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用线程安全的方式在主事件循环中运行异步的插件加载函数
|
# 使用线程安全的方式在主事件循环中运行异步的插件重载函数
|
||||||
asyncio.run_coroutine_threadsafe(run_in_thread_pool(load_all_plugins), self.loop)
|
asyncio.run_coroutine_threadsafe(run_in_thread_pool(plugin_manager.reload_plugin, module_name), self.loop)
|
||||||
logger.success("插件重载完成")
|
logger.success(f"插件 {module_name} 重载任务已提交")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"重载失败: {e}")
|
logger.exception(f"重载失败: {e}")
|
||||||
|
|
||||||
@@ -88,8 +97,7 @@ async def main():
|
|||||||
2. 初始化 WebSocket 客户端
|
2. 初始化 WebSocket 客户端
|
||||||
3. 建立连接并保持运行
|
3. 建立连接并保持运行
|
||||||
"""
|
"""
|
||||||
# 首次加载插件
|
# 插件加载已移至 core.managers.__init__.py 中自动执行
|
||||||
await run_in_thread_pool(load_all_plugins)
|
|
||||||
|
|
||||||
# 初始化 Redis 连接
|
# 初始化 Redis 连接
|
||||||
await redis_manager.initialize()
|
await redis_manager.initialize()
|
||||||
@@ -114,11 +122,10 @@ async def main():
|
|||||||
logger.warning(f"插件目录不存在 {plugin_path}")
|
logger.warning(f"插件目录不存在 {plugin_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
websocket_client = WS()
|
|
||||||
|
|
||||||
# 初始化代码执行器
|
# 初始化代码执行器
|
||||||
code_executor = initialize_executor(websocket_client, config)
|
code_executor = initialize_executor(config)
|
||||||
websocket_client.bot.code_executor = code_executor # 将执行器实例附加到 bot.bot 对象上
|
|
||||||
|
websocket_client = WS(code_executor=code_executor)
|
||||||
|
|
||||||
# 启动代码执行器的后台 worker
|
# 启动代码执行器的后台 worker
|
||||||
logger.debug("[Main] 检查是否需要启动代码执行 Worker...")
|
logger.debug("[Main] 检查是否需要启动代码执行 Worker...")
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
from .events.base import OneBotEvent
|
|
||||||
from .events.factory import EventFactory
|
|
||||||
from .events.message import (
|
|
||||||
GroupMessageEvent,
|
|
||||||
MessageEvent,
|
|
||||||
MessageSegment,
|
|
||||||
PrivateMessageEvent,
|
|
||||||
)
|
|
||||||
from .events.meta import HeartbeatEvent, HeartbeatStatus, LifeCycleEvent, MetaEvent
|
|
||||||
from .events.notice import (
|
|
||||||
ClientStatus,
|
|
||||||
ClientStatusNoticeEvent,
|
|
||||||
EssenceNoticeEvent,
|
|
||||||
FriendAddNoticeEvent,
|
|
||||||
FriendRecallNoticeEvent,
|
|
||||||
GroupAdminNoticeEvent,
|
|
||||||
GroupBanNoticeEvent,
|
|
||||||
GroupCardNoticeEvent,
|
|
||||||
GroupDecreaseNoticeEvent,
|
|
||||||
GroupIncreaseNoticeEvent,
|
|
||||||
GroupRecallNoticeEvent,
|
|
||||||
GroupUploadFile,
|
|
||||||
GroupUploadNoticeEvent,
|
|
||||||
HonorNotifyEvent,
|
|
||||||
LuckyKingNotifyEvent,
|
|
||||||
NoticeEvent,
|
|
||||||
NotifyNoticeEvent,
|
|
||||||
OfflineFile,
|
|
||||||
OfflineFileNoticeEvent,
|
|
||||||
PokeNotifyEvent,
|
|
||||||
)
|
|
||||||
from .events.request import FriendRequestEvent, GroupRequestEvent, RequestEvent
|
|
||||||
from .objects import (
|
|
||||||
CurrentTalkative,
|
|
||||||
EssenceMessage,
|
|
||||||
FriendInfo,
|
|
||||||
GroupHonorInfo,
|
|
||||||
GroupInfo,
|
|
||||||
GroupMemberInfo,
|
|
||||||
HonorInfo,
|
|
||||||
LoginInfo,
|
|
||||||
Status,
|
|
||||||
StrangerInfo,
|
|
||||||
VersionInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Alias for backward compatibility
|
|
||||||
Event = OneBotEvent
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"MessageSegment",
|
|
||||||
"Sender",
|
|
||||||
"OneBotEvent",
|
|
||||||
"Event",
|
|
||||||
"MessageEvent",
|
|
||||||
"PrivateMessageEvent",
|
|
||||||
"GroupMessageEvent",
|
|
||||||
"NoticeEvent",
|
|
||||||
"FriendAddNoticeEvent",
|
|
||||||
"FriendRecallNoticeEvent",
|
|
||||||
"GroupRecallNoticeEvent",
|
|
||||||
"GroupIncreaseNoticeEvent",
|
|
||||||
"GroupDecreaseNoticeEvent",
|
|
||||||
"GroupAdminNoticeEvent",
|
|
||||||
"GroupBanNoticeEvent",
|
|
||||||
"GroupUploadNoticeEvent",
|
|
||||||
"GroupUploadFile",
|
|
||||||
"NotifyNoticeEvent",
|
|
||||||
"PokeNotifyEvent",
|
|
||||||
"LuckyKingNotifyEvent",
|
|
||||||
"HonorNotifyEvent",
|
|
||||||
"GroupCardNoticeEvent",
|
|
||||||
"OfflineFileNoticeEvent",
|
|
||||||
"OfflineFile",
|
|
||||||
"ClientStatusNoticeEvent",
|
|
||||||
"ClientStatus",
|
|
||||||
"EssenceNoticeEvent",
|
|
||||||
"RequestEvent",
|
|
||||||
"FriendRequestEvent",
|
|
||||||
"GroupRequestEvent",
|
|
||||||
"MetaEvent",
|
|
||||||
"HeartbeatEvent",
|
|
||||||
"LifeCycleEvent",
|
|
||||||
"HeartbeatStatus",
|
|
||||||
"EventFactory",
|
|
||||||
"GroupInfo",
|
|
||||||
"GroupMemberInfo",
|
|
||||||
"FriendInfo",
|
|
||||||
"StrangerInfo",
|
|
||||||
"LoginInfo",
|
|
||||||
"VersionInfo",
|
|
||||||
"Status",
|
|
||||||
"EssenceMessage",
|
|
||||||
"GroupHonorInfo",
|
|
||||||
"CurrentTalkative",
|
|
||||||
"HonorInfo",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -252,9 +252,18 @@ class EventFactory:
|
|||||||
card_new=data.get("card_new", ""),
|
card_new=data.get("card_new", ""),
|
||||||
card_old=data.get("card_old", "")
|
card_old=data.get("card_old", "")
|
||||||
)
|
)
|
||||||
|
elif notice_type == "group_card":
|
||||||
|
return GroupCardNoticeEvent(
|
||||||
|
**common_args,
|
||||||
|
notice_type=notice_type,
|
||||||
|
group_id=data.get("group_id", 0),
|
||||||
|
user_id=data.get("user_id", 0),
|
||||||
|
card_new=data.get("card_new", ""),
|
||||||
|
card_old=data.get("card_old", "")
|
||||||
|
)
|
||||||
elif notice_type == "offline_file":
|
elif notice_type == "offline_file":
|
||||||
file_data = data.get("file", {})
|
file_data = data.get("file", {})
|
||||||
file = OfflineFile(
|
offline_file = OfflineFile(
|
||||||
name=file_data.get("name", ""),
|
name=file_data.get("name", ""),
|
||||||
size=file_data.get("size", 0),
|
size=file_data.get("size", 0),
|
||||||
url=file_data.get("url", "")
|
url=file_data.get("url", "")
|
||||||
@@ -263,7 +272,7 @@ class EventFactory:
|
|||||||
**common_args,
|
**common_args,
|
||||||
notice_type=notice_type,
|
notice_type=notice_type,
|
||||||
user_id=data.get("user_id", 0),
|
user_id=data.get("user_id", 0),
|
||||||
file=file
|
file=offline_file
|
||||||
)
|
)
|
||||||
elif notice_type == "client_status":
|
elif notice_type == "client_status":
|
||||||
client_data = data.get("client", {})
|
client_data = data.get("client", {})
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
定义了消息相关的事件类,包括 MessageEvent, PrivateMessageEvent, GroupMessageEvent。
|
定义了消息相关的事件类,包括 MessageEvent, PrivateMessageEvent, GroupMessageEvent。
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from core.managers.permission_manager import ADMIN, OP, USER
|
from core.permission import Permission
|
||||||
from models.message import MessageSegment
|
from models.message import MessageSegment
|
||||||
from models.sender import Sender
|
from models.sender import Sender
|
||||||
from .base import OneBotEvent, EventType
|
from .base import OneBotEvent, EventType
|
||||||
@@ -34,9 +34,9 @@ class MessageEvent(OneBotEvent):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 权限级别常量,用于装饰器参数
|
# 权限级别常量,用于装饰器参数
|
||||||
ADMIN = ADMIN
|
ADMIN = Permission.ADMIN
|
||||||
OP = OP
|
OP = Permission.OP
|
||||||
USER = USER
|
USER = Permission.USER
|
||||||
|
|
||||||
message_type: str
|
message_type: str
|
||||||
"""消息类型: private (私聊), group (群聊)"""
|
"""消息类型: private (私聊), group (群聊)"""
|
||||||
@@ -70,7 +70,7 @@ class MessageEvent(OneBotEvent):
|
|||||||
def post_type(self) -> str:
|
def post_type(self) -> str:
|
||||||
return EventType.MESSAGE
|
return EventType.MESSAGE
|
||||||
|
|
||||||
async def reply(self, message: str, auto_escape: bool = False):
|
async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False):
|
||||||
"""
|
"""
|
||||||
回复消息(抽象方法,由子类实现)
|
回复消息(抽象方法,由子类实现)
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ class PrivateMessageEvent(MessageEvent):
|
|||||||
私聊消息事件
|
私聊消息事件
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def reply(self, message: str, auto_escape: bool = False):
|
async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False):
|
||||||
"""
|
"""
|
||||||
回复私聊消息
|
回复私聊消息
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ class GroupMessageEvent(MessageEvent):
|
|||||||
anonymous: Optional[Anonymous] = None
|
anonymous: Optional[Anonymous] = None
|
||||||
"""匿名信息"""
|
"""匿名信息"""
|
||||||
|
|
||||||
async def reply(self, message: str, auto_escape: bool = False):
|
async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False):
|
||||||
"""
|
"""
|
||||||
回复群聊消息
|
回复群聊消息
|
||||||
|
|
||||||
|
|||||||
@@ -63,5 +63,5 @@ class LifeCycleEvent(MetaEvent):
|
|||||||
meta_event_type: str = 'lifecycle'
|
meta_event_type: str = 'lifecycle'
|
||||||
"""元事件类型:生命周期事件"""
|
"""元事件类型:生命周期事件"""
|
||||||
|
|
||||||
sub_type: LifeCycleSubType = LifeCycleSubType.ENABLE
|
sub_type: str = LifeCycleSubType.ENABLE
|
||||||
"""子类型:启用、禁用、连接"""
|
"""子类型:启用、禁用、连接"""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -76,7 +76,7 @@ class MessageSegment:
|
|||||||
return self.data.get("file", "")
|
return self.data.get("file", "")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def is_at(self, user_id: int = None) -> bool:
|
def is_at(self, user_id: Optional[int] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
检查当前消息段是否是一个 'at' (提及) 消息段。
|
检查当前消息段是否是一个 'at' (提及) 消息段。
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ class MessageSegment:
|
|||||||
# --- 快捷构造方法 ---
|
# --- 快捷构造方法 ---
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def text(text: str) -> "MessageSegment": # noqa: F811
|
def from_text(text: str) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个文本消息段。
|
创建一个文本消息段。
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ class MessageSegment:
|
|||||||
return MessageSegment(type="text", data={"text": text})
|
return MessageSegment(type="text", data={"text": text})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def at(user_id: int | str, name: str = None) -> "MessageSegment":
|
def at(user_id: int | str, name: Optional[str] = None) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个 @某人 的消息段。
|
创建一个 @某人 的消息段。
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ class MessageSegment:
|
|||||||
return MessageSegment(type="at", data=data)
|
return MessageSegment(type="at", data=data)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def image(file: str, image_type: str = None, cache: bool = True, proxy: bool = True, timeout: int = None, sub_type: int = None) -> "MessageSegment":
|
def image(file: str, image_type: Optional[str] = None, cache: bool = True, proxy: bool = True, timeout: Optional[int] = None, sub_type: Optional[int] = None) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个图片消息段。
|
创建一个图片消息段。
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class MessageSegment:
|
|||||||
"""
|
"""
|
||||||
return MessageSegment(type="xml", data={"data": data})
|
return MessageSegment(type="xml", data={"data": data})
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def share(url: str, title: str, content: str = None, image: str = None) -> "MessageSegment":
|
def share(url: str, title: str, content: Optional[str] = None, image: Optional[str] = None) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个分享消息段。
|
创建一个分享消息段。
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class MessageSegment:
|
|||||||
"""
|
"""
|
||||||
return MessageSegment(type="music", data={"type": type, "id": id})
|
return MessageSegment(type="music", data={"type": type, "id": id})
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def music_custom(url: str, audio: str, title: str, content: str = None, image: str = None) -> "MessageSegment":
|
def music_custom(url: str, audio: str, title: str, content: Optional[str] = None, image: Optional[str] = None) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个自定义音乐消息段。
|
创建一个自定义音乐消息段。
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ class MessageSegment:
|
|||||||
data["image"] = image
|
data["image"] = image
|
||||||
return MessageSegment(type="music", data={"type": "custom", **data})
|
return MessageSegment(type="music", data={"type": "custom", **data})
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def record(file: str, magic: bool = False, cache: bool = True, proxy: bool = True, timeout: int = None) -> "MessageSegment":
|
def record(file: str, magic: bool = False, cache: bool = True, proxy: bool = True, timeout: Optional[int] = None) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个语音消息段。
|
创建一个语音消息段。
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ class MessageSegment:
|
|||||||
data["timeout"] = str(timeout)
|
data["timeout"] = str(timeout)
|
||||||
return MessageSegment(type="record", data=data)
|
return MessageSegment(type="record", data=data)
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def video(file: str, cover: str = None, c: int = 2) -> "MessageSegment":
|
def video(file: str, cover: Optional[str] = None, c: int = 2) -> "MessageSegment":
|
||||||
"""
|
"""
|
||||||
创建一个视频消息段。
|
创建一个视频消息段。
|
||||||
|
|
||||||
|
|||||||
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
130
plugins/admin.py
130
plugins/admin.py
@@ -1,74 +1,94 @@
|
|||||||
"""
|
from core.handlers.event_handler import MessageHandler
|
||||||
管理员管理插件
|
from core.managers import command_manager, permission_manager
|
||||||
|
from core.permission import Permission
|
||||||
提供通过聊天指令动态添加或移除机器人管理员的功能。
|
|
||||||
"""
|
|
||||||
from core.bot import Bot
|
|
||||||
from core.managers.command_manager import matcher
|
|
||||||
from core.managers.admin_manager import admin_manager
|
|
||||||
from models.events.message import MessageEvent
|
from models.events.message import MessageEvent
|
||||||
|
|
||||||
|
# 更新插件元信息以包含OP管理
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
"name": "管理员管理",
|
"name": "权限管理",
|
||||||
"description": "管理机器人的全局管理员",
|
"description": "管理机器人的管理员和操作员",
|
||||||
"usage": (
|
"usage": (
|
||||||
"/admin list - 列出所有管理员\n"
|
"/admin list - 列出所有管理员和操作员\n"
|
||||||
"/admin add <QQ号> - 添加管理员\n"
|
"/admin add_admin <QQ号> - 添加管理员\n"
|
||||||
"/admin remove <QQ号> - 移除管理员"
|
"/admin remove_admin <QQ号> - 移除管理员\n"
|
||||||
|
"/admin add_op <QQ号> - 添加操作员\n"
|
||||||
|
"/admin remove_op <QQ号> - 移除操作员"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@matcher.command("admin", permission=MessageEvent.ADMIN)
|
@command_manager.command("admin", permission=Permission.ADMIN)
|
||||||
async def admin_command_handler(bot: Bot, event: MessageEvent, args: list[str]):
|
async def admin_management(event: MessageEvent, args: str):
|
||||||
"""
|
"""
|
||||||
处理 /admin 指令
|
处理所有权限管理相关的命令。
|
||||||
|
|
||||||
:param bot: Bot 实例
|
|
||||||
:param event: 消息事件实例
|
|
||||||
:param args: 指令参数列表
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
parts = args.split()
|
||||||
await event.reply(__plugin_meta__["usage"])
|
if not parts:
|
||||||
|
await event.reply(f"用法不正确。\n\n{__plugin_meta__['usage']}")
|
||||||
return
|
return
|
||||||
|
|
||||||
action = args[0].lower()
|
subcommand = parts[0].lower()
|
||||||
|
|
||||||
if action == "list":
|
if subcommand == "list":
|
||||||
admins = await admin_manager.get_all_admins()
|
await list_permissions(event)
|
||||||
if not admins:
|
|
||||||
await event.reply("当前没有设置任何管理员。")
|
|
||||||
return
|
|
||||||
|
|
||||||
admin_list_text = "\n".join(str(admin_id) for admin_id in admins)
|
|
||||||
await event.reply(f"当前管理员列表 ({len(admins)}):\n{admin_list_text}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if action in ("add", "remove"):
|
# 处理需要QQ号的命令
|
||||||
if len(args) < 2 or not args[1].isdigit():
|
if len(parts) < 2 or not parts[1].isdigit():
|
||||||
await event.reply("参数错误,请提供一个有效的 QQ 号。\n示例: /admin add 123456")
|
await event.reply(f"请提供有效的用户QQ号。\n用法: /admin {subcommand} <QQ号>")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_id = int(args[1])
|
target_user_id = int(parts[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await event.reply("无效的 QQ 号,请输入纯数字。")
|
await event.reply("无效的QQ号。")
|
||||||
return
|
return
|
||||||
|
|
||||||
if action == "add":
|
# 安全检查
|
||||||
success = await admin_manager.add_admin(user_id)
|
if target_user_id == event.user_id:
|
||||||
if success:
|
await event.reply("你不能操作自己的权限。")
|
||||||
await event.reply(f"成功添加管理员: {user_id}")
|
return
|
||||||
else:
|
if target_user_id == event.self_id:
|
||||||
await event.reply(f"管理员 {user_id} 已存在,无需重复添加。")
|
await event.reply("你不能操作机器人自身的权限。")
|
||||||
return
|
return
|
||||||
|
|
||||||
elif action == "remove":
|
# 根据子命令分发
|
||||||
success = await admin_manager.remove_admin(user_id)
|
if subcommand == "add_admin":
|
||||||
if success:
|
permission_manager.set_user_permission(target_user_id, Permission.ADMIN)
|
||||||
await event.reply(f"成功移除管理员: {user_id}")
|
await event.reply(f"已成功添加管理员:{target_user_id}")
|
||||||
else:
|
elif subcommand == "remove_admin":
|
||||||
await event.reply(f"管理员 {user_id} 不存在。")
|
permission_manager.set_user_permission(target_user_id, Permission.USER)
|
||||||
return
|
await event.reply(f"已成功移除管理员:{target_user_id}")
|
||||||
|
elif subcommand == "add_op":
|
||||||
|
permission_manager.set_user_permission(target_user_id, Permission.OP)
|
||||||
|
await event.reply(f"已成功添加操作员:{target_user_id}")
|
||||||
|
elif subcommand == "remove_op":
|
||||||
|
permission_manager.set_user_permission(target_user_id, Permission.USER)
|
||||||
|
await event.reply(f"已成功移除操作员:{target_user_id}")
|
||||||
|
else:
|
||||||
|
await event.reply(f"未知的子命令 '{subcommand}'。\n\n{__plugin_meta__['usage']}")
|
||||||
|
|
||||||
await event.reply(f"未知的指令: {action}\n\n{__plugin_meta__['usage']}")
|
|
||||||
|
async def list_permissions(event: MessageEvent):
|
||||||
|
"""
|
||||||
|
列出所有具有特殊权限(管理员和操作员)的用户。
|
||||||
|
"""
|
||||||
|
permissions = permission_manager.get_all_user_permissions()
|
||||||
|
if not permissions:
|
||||||
|
await event.reply("当前没有配置任何特殊权限的用户。")
|
||||||
|
return
|
||||||
|
|
||||||
|
admins = {uid for uid, p in permissions.items() if p == 'admin'}
|
||||||
|
ops = {uid for uid, p in permissions.items() if p == 'op'}
|
||||||
|
|
||||||
|
reply_msg = "当前权限列表:\n"
|
||||||
|
if admins:
|
||||||
|
reply_msg += "--- 管理员 ---\n"
|
||||||
|
for user_id in admins:
|
||||||
|
reply_msg += f"- {user_id}\n"
|
||||||
|
if ops:
|
||||||
|
reply_msg += "--- 操作员 ---\n"
|
||||||
|
for user_id in ops:
|
||||||
|
reply_msg += f"- {user_id}\n"
|
||||||
|
|
||||||
|
await event.reply(reply_msg.strip())
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import requests
|
import httpx
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
|
from cachetools import TTLCache
|
||||||
|
|
||||||
from core.utils.logger import logger
|
from core.utils.logger import logger
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from models import MessageEvent, MessageSegment
|
from models.events.message import MessageEvent, MessageSegment
|
||||||
|
|
||||||
|
# 创建一个TTL缓存,最大容量100,缓存时间60秒
|
||||||
|
processed_messages: TTLCache[Any, bool] = TTLCache(maxsize=100, ttl=60)
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
"name": "bili_parser",
|
"name": "bili_parser",
|
||||||
@@ -19,6 +23,9 @@ HEADERS = {
|
|||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 创建可复用的异步HTTP客户端
|
||||||
|
async_client = httpx.AsyncClient(headers=HEADERS, follow_redirects=False, timeout=10)
|
||||||
|
|
||||||
|
|
||||||
def format_count(num: int) -> str:
|
def format_count(num: int) -> str:
|
||||||
if not isinstance(num, int):
|
if not isinstance(num, int):
|
||||||
@@ -36,18 +43,18 @@ def format_duration(seconds: int) -> str:
|
|||||||
return f"{minutes:02d}:{seconds:02d}"
|
return f"{minutes:02d}:{seconds:02d}"
|
||||||
|
|
||||||
|
|
||||||
def get_real_url(short_url: str) -> Optional[str]:
|
async def get_real_url(short_url: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
response = requests.head(short_url, headers=HEADERS, allow_redirects=False, timeout=5)
|
response = await async_client.head(short_url)
|
||||||
if response.status_code == 302:
|
if response.status_code == 302:
|
||||||
return response.headers.get('Location')
|
return response.headers.get('Location')
|
||||||
except requests.RequestException as e:
|
except httpx.RequestError as e:
|
||||||
print(f"获取真实URL失败: {e}")
|
logger.error(f"获取真实URL失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
|
async def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
response = requests.get(video_url, headers=HEADERS, timeout=5)
|
response = await async_client.get(video_url, follow_redirects=True)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
soup = BeautifulSoup(response.text, 'html.parser')
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
@@ -55,7 +62,14 @@ def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
|
|||||||
if not script_tag:
|
if not script_tag:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
json_str = re.search(r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\});', script_tag.string).group(1)
|
script_tag_content = script_tag.string
|
||||||
|
if not script_tag_content:
|
||||||
|
return None
|
||||||
|
|
||||||
|
match = re.search(r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\});', script_tag_content)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
json_str = match.group(1)
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
|
|
||||||
video_data = data.get('videoData', {})
|
video_data = data.get('videoData', {})
|
||||||
@@ -90,12 +104,12 @@ def parse_video_info(video_url: str) -> Optional[Dict[str, Any]]:
|
|||||||
"followers": up_data.get('fans', 0),
|
"followers": up_data.get('fans', 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
except (requests.RequestException, KeyError, AttributeError, json.JSONDecodeError) as e:
|
except (httpx.RequestError, KeyError, AttributeError, json.JSONDecodeError) as e:
|
||||||
print(f"解析视频信息失败: {e}")
|
logger.error(f"解析视频信息失败: {e}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_direct_video_url(video_url: str) -> Optional[str]:
|
async def get_direct_video_url(video_url: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
调用第三方API解析B站视频直链
|
调用第三方API解析B站视频直链
|
||||||
:param video_url: B站视频的完整URL
|
:param video_url: B站视频的完整URL
|
||||||
@@ -103,12 +117,12 @@ def get_direct_video_url(video_url: str) -> Optional[str]:
|
|||||||
"""
|
"""
|
||||||
api_url = f"https://api.mir6.com/api/bzjiexi?url={video_url}&type=json"
|
api_url = f"https://api.mir6.com/api/bzjiexi?url={video_url}&type=json"
|
||||||
try:
|
try:
|
||||||
response = requests.get(api_url, headers=HEADERS, timeout=10)
|
response = await async_client.get(api_url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if data.get("code") == 200 and data.get("data"):
|
if data.get("code") == 200 and data.get("data"):
|
||||||
return data["data"][0].get("video_url")
|
return data["data"][0].get("video_url")
|
||||||
except (requests.RequestException, json.JSONDecodeError, KeyError, IndexError) as e:
|
except (httpx.RequestError, json.JSONDecodeError, KeyError, IndexError) as e:
|
||||||
logger.error(f"[bili_parser] 调用第三方API解析视频失败: {e}")
|
logger.error(f"[bili_parser] 调用第三方API解析视频失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -121,6 +135,15 @@ async def handle_bili_share(event: MessageEvent):
|
|||||||
处理消息,检测B站分享链接(JSON卡片或文本链接)并进行解析。
|
处理消息,检测B站分享链接(JSON卡片或文本链接)并进行解析。
|
||||||
:param event: 消息事件对象
|
:param event: 消息事件对象
|
||||||
"""
|
"""
|
||||||
|
# 消息去重
|
||||||
|
if event.message_id in processed_messages:
|
||||||
|
return
|
||||||
|
processed_messages[event.message_id] = True
|
||||||
|
|
||||||
|
# 忽略机器人自己发送的消息,防止无限循环
|
||||||
|
if event.user_id == event.self_id:
|
||||||
|
return
|
||||||
|
|
||||||
url_to_process = None
|
url_to_process = None
|
||||||
|
|
||||||
# 1. 优先解析JSON卡片中的短链接
|
# 1. 优先解析JSON卡片中的短链接
|
||||||
@@ -161,7 +184,7 @@ async def process_bili_link(event: MessageEvent, url: str):
|
|||||||
:param url: 待处理的B站链接
|
:param url: 待处理的B站链接
|
||||||
"""
|
"""
|
||||||
if "b23.tv" in url:
|
if "b23.tv" in url:
|
||||||
real_url = get_real_url(url)
|
real_url = await get_real_url(url)
|
||||||
if not real_url:
|
if not real_url:
|
||||||
logger.error(f"[bili_parser] 无法从 {url} 获取真实URL。")
|
logger.error(f"[bili_parser] 无法从 {url} 获取真实URL。")
|
||||||
await event.reply("无法解析B站短链接。")
|
await event.reply("无法解析B站短链接。")
|
||||||
@@ -169,58 +192,28 @@ async def process_bili_link(event: MessageEvent, url: str):
|
|||||||
else:
|
else:
|
||||||
real_url = url.split('?')[0]
|
real_url = url.split('?')[0]
|
||||||
|
|
||||||
video_info = parse_video_info(real_url)
|
video_info = await parse_video_info(real_url)
|
||||||
if not video_info:
|
if not video_info:
|
||||||
logger.error(f"[bili_parser] 无法从 {real_url} 解析视频信息。")
|
logger.error(f"[bili_parser] 无法从 {real_url} 解析视频信息。")
|
||||||
await event.reply("无法获取视频信息,可能是B站接口变动或视频不存在。")
|
await event.reply("无法获取视频信息,可能是B站接口变动或视频不存在。")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 检查视频时长
|
title = video_info.get("title", "未知标题")
|
||||||
if video_info['duration'] > 300: # 5分钟 = 300秒
|
owner_name = video_info.get("owner_name", "未知UP主")
|
||||||
video_message = "视频时长超过5分钟,不进行解析。"
|
cover_url = video_info.get("cover_url")
|
||||||
else:
|
bvid = video_info.get("bvid", "N/A")
|
||||||
direct_url = get_direct_video_url(real_url)
|
play_count = format_count(video_info.get("play", 0))
|
||||||
if direct_url:
|
like_count = format_count(video_info.get("like", 0))
|
||||||
video_message = MessageSegment.video(direct_url)
|
|
||||||
else:
|
|
||||||
video_message = "视频解析失败,无法获取直链。"
|
|
||||||
|
|
||||||
text_message = (
|
text_part = (
|
||||||
f"BiliBili 视频解析\n"
|
f"标题: {title}\n"
|
||||||
f"--------------------\n"
|
f"UP主: {owner_name}\n"
|
||||||
f" UP主: {video_info['owner_name']}\n"
|
f"BV: {bvid} | ▶️ {play_count} | 👍 {like_count}"
|
||||||
f" 粉丝: {format_count(video_info['followers'])}\n"
|
|
||||||
f"--------------------\n"
|
|
||||||
f" 标题: {video_info['title']}\n"
|
|
||||||
f" BV号: {video_info['bvid']}\n"
|
|
||||||
f" 时长: {format_duration(video_info['duration'])}\n"
|
|
||||||
f"--------------------\n"
|
|
||||||
f" 数据:\n"
|
|
||||||
f" 播放: {format_count(video_info['play'])}\n"
|
|
||||||
f" 点赞: {format_count(video_info['like'])}\n"
|
|
||||||
f" 投币: {format_count(video_info['coin'])}\n"
|
|
||||||
f" 收藏: {format_count(video_info['favorite'])}\n"
|
|
||||||
f" 转发: {format_count(video_info['share'])}\n"
|
|
||||||
f" B站链接: {url}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
image_message_segment = [
|
reply_message = [MessageSegment.from_text(text_part)]
|
||||||
MessageSegment.text("B站封面:"),
|
if cover_url:
|
||||||
MessageSegment.image(video_info['cover_url'])
|
reply_message.append(MessageSegment.image(cover_url))
|
||||||
]
|
|
||||||
|
|
||||||
up_info_segment = [
|
logger.success(f"[bili_parser] 成功解析视频信息并准备回复: {title}")
|
||||||
MessageSegment.text("UP主头像:"),
|
await event.reply(reply_message)
|
||||||
MessageSegment.image(video_info['owner_avatar'])
|
|
||||||
]
|
|
||||||
|
|
||||||
nodes = [
|
|
||||||
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=text_message),
|
|
||||||
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=image_message_segment),
|
|
||||||
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=up_info_segment),
|
|
||||||
event.bot.build_forward_node(user_id=event.self_id, nickname="B站视频解析", message=video_message)
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.success(f"[bili_parser] 成功解析视频信息并准备以聊天记录形式回复: {video_info['title']}")
|
|
||||||
# 使用更通用的 send_forwarded_messages 方法,自动判断私聊或群聊
|
|
||||||
await event.bot.send_forwarded_messages(target=event, nodes=nodes)
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from models import MessageEvent, PrivateMessageEvent
|
from models.events.message import MessageEvent, PrivateMessageEvent
|
||||||
from core.managers.permission_manager import ADMIN
|
from core.permission import Permission
|
||||||
from core.utils.logger import logger
|
from core.utils.logger import logger
|
||||||
|
|
||||||
# --- 会话状态管理 ---
|
# --- 会话状态管理 ---
|
||||||
@@ -24,7 +24,7 @@ def cleanup_session(user_id: int):
|
|||||||
del broadcast_sessions[user_id]
|
del broadcast_sessions[user_id]
|
||||||
logger.info(f"[Broadcast] 会话 {user_id} 已超时,自动取消。")
|
logger.info(f"[Broadcast] 会话 {user_id} 已超时,自动取消。")
|
||||||
|
|
||||||
@matcher.command("broadcast", "广播", permission=ADMIN)
|
@matcher.command("broadcast", "广播", permission=Permission.ADMIN)
|
||||||
async def broadcast_start(event: MessageEvent):
|
async def broadcast_start(event: MessageEvent):
|
||||||
"""
|
"""
|
||||||
广播指令的入口,启动一个等待用户消息的会话。
|
广播指令的入口,启动一个等待用户消息的会话。
|
||||||
@@ -92,7 +92,7 @@ async def handle_broadcast_content(event: MessageEvent):
|
|||||||
nodes_to_send = [
|
nodes_to_send = [
|
||||||
bot.build_forward_node(
|
bot.build_forward_node(
|
||||||
user_id=event.user_id,
|
user_id=event.user_id,
|
||||||
nickname=event.sender.nickname,
|
nickname=event.sender.nickname if event.sender else "未知用户",
|
||||||
message=message_to_broadcast
|
message=message_to_broadcast
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import asyncio
|
|||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from models import MessageEvent
|
from models.events.message import MessageEvent
|
||||||
from core.managers.permission_manager import ADMIN
|
from core.permission import Permission
|
||||||
from core.utils.logger import logger
|
from core.utils.logger import logger
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
@@ -30,7 +30,7 @@ async def reply_as_forward(event: MessageEvent, input_code: str, output_result:
|
|||||||
nodes = [
|
nodes = [
|
||||||
bot.build_forward_node(
|
bot.build_forward_node(
|
||||||
user_id=event.user_id,
|
user_id=event.user_id,
|
||||||
nickname=event.sender.nickname or str(event.user_id),
|
nickname=event.sender.nickname if event.sender else str(event.user_id),
|
||||||
message=f"--- Your Code ---\n{input_code}"
|
message=f"--- Your Code ---\n{input_code}"
|
||||||
),
|
),
|
||||||
bot.build_forward_node(
|
bot.build_forward_node(
|
||||||
@@ -97,7 +97,7 @@ def normalize_code(code: str) -> str:
|
|||||||
return code.strip()
|
return code.strip()
|
||||||
|
|
||||||
|
|
||||||
@matcher.command("py", "python", "code_py", permission=ADMIN)
|
@matcher.command("py", "python", "code_py", permission=Permission.ADMIN)
|
||||||
async def code_py_main(event: MessageEvent, args: list[str]):
|
async def code_py_main(event: MessageEvent, args: list[str]):
|
||||||
"""
|
"""
|
||||||
/py 命令的主入口。
|
/py 命令的主入口。
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Echo 与交互插件
|
|||||||
"""
|
"""
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from core.bot import Bot
|
from core.bot import Bot
|
||||||
from models import MessageEvent
|
from models.events.message import MessageEvent
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
"name": "echo",
|
"name": "echo",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from core.bot import Bot
|
from core.bot import Bot
|
||||||
from models import MessageEvent
|
from models.events.message import MessageEvent
|
||||||
from models.message import MessageSegment
|
from models.message import MessageSegment
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
@@ -22,14 +22,15 @@ async def handle_forward_test(bot: Bot, event: MessageEvent, args: list[str]):
|
|||||||
:param args: 指令参数
|
:param args: 指令参数
|
||||||
"""
|
"""
|
||||||
# 1. 构建消息节点列表
|
# 1. 构建消息节点列表
|
||||||
|
nickname = event.sender.nickname if event.sender else "未知用户"
|
||||||
nodes = [
|
nodes = [
|
||||||
bot.build_forward_node(user_id=event.self_id, nickname="机器人", message="你要的furry来了"),
|
bot.build_forward_node(user_id=event.self_id, nickname="机器人", message="你要的furry来了"),
|
||||||
bot.build_forward_node(user_id=event.user_id, nickname=event.sender.nickname, message="让我看看"),
|
bot.build_forward_node(user_id=event.user_id, nickname=nickname, message="让我看看"),
|
||||||
bot.build_forward_node(
|
bot.build_forward_node(
|
||||||
user_id=event.self_id,
|
user_id=event.self_id,
|
||||||
nickname="机器人",
|
nickname="机器人",
|
||||||
message=[
|
message=[
|
||||||
MessageSegment.text("你要的福瑞图"),
|
MessageSegment.from_text("你要的福瑞图"),
|
||||||
MessageSegment.image("https://api.furry.ist/furry-img/")
|
MessageSegment.image("https://api.furry.ist/furry-img/")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from datetime import datetime
|
|||||||
from core.bot import Bot
|
from core.bot import Bot
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from core.utils.executor import run_in_thread_pool
|
from core.utils.executor import run_in_thread_pool
|
||||||
from models import MessageEvent, MessageSegment
|
from models.events.message import MessageEvent, MessageSegment
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
"name": "jrcd",
|
"name": "jrcd",
|
||||||
@@ -79,14 +79,17 @@ async def handle_jrcd(bot: Bot, event: MessageEvent, args: list[str]):
|
|||||||
"""
|
"""
|
||||||
user_id = event.user_id
|
user_id = event.user_id
|
||||||
jrcd = await run_in_thread_pool(get_jrcd, user_id)
|
jrcd = await run_in_thread_pool(get_jrcd, user_id)
|
||||||
msg = [MessageSegment.at(user_id)]
|
|
||||||
|
msg_text = ""
|
||||||
if jrcd <= 9:
|
if jrcd <= 9:
|
||||||
msg.append(MessageSegment.text(random.choice(JRCDMSG_1) % jrcd))
|
msg_text = random.choice(JRCDMSG_1) % jrcd
|
||||||
elif jrcd <= 19:
|
elif jrcd <= 19:
|
||||||
msg.append(MessageSegment.text(random.choice(JRCDMSG_2) % jrcd))
|
msg_text = random.choice(JRCDMSG_2) % jrcd
|
||||||
else:
|
else:
|
||||||
msg.append(MessageSegment.text(random.choice(JRCDMSG_3) % jrcd))
|
msg_text = random.choice(JRCDMSG_3) % jrcd
|
||||||
await event.reply(msg)
|
|
||||||
|
reply_segments = [MessageSegment.at(user_id), MessageSegment.from_text(msg_text)]
|
||||||
|
await event.reply(reply_segments)
|
||||||
|
|
||||||
|
|
||||||
@matcher.command("bbcd")
|
@matcher.command("bbcd")
|
||||||
@@ -118,29 +121,31 @@ async def handle_bbcd(bot: Bot, event: MessageEvent, args: list[str]):
|
|||||||
|
|
||||||
jrcz = jrcd1 - jrcd2
|
jrcz = jrcd1 - jrcd2
|
||||||
|
|
||||||
msg = [
|
text_part = ""
|
||||||
|
if jrcz == 0:
|
||||||
|
text_part = f" 一样长。{random.choice(BBCDMSG7)}"
|
||||||
|
elif jrcz > 0:
|
||||||
|
text_part = f" 长{jrcz}cm。"
|
||||||
|
if jrcz <= 9:
|
||||||
|
text_part += random.choice(BBCDMSG1)
|
||||||
|
elif jrcz <= 19:
|
||||||
|
text_part += random.choice(BBCDMSG2)
|
||||||
|
else:
|
||||||
|
text_part += random.choice(BBCDMSG3)
|
||||||
|
else: # jrcz < 0
|
||||||
|
text_part = f" 短{abs(jrcz)}cm。"
|
||||||
|
if jrcz >= -9:
|
||||||
|
text_part += random.choice(BBCDMSG4)
|
||||||
|
elif jrcz >= -19:
|
||||||
|
text_part += random.choice(BBCDMSG5)
|
||||||
|
else:
|
||||||
|
text_part += random.choice(BBCDMSG6)
|
||||||
|
|
||||||
|
segments = [
|
||||||
MessageSegment.at(user_id1),
|
MessageSegment.at(user_id1),
|
||||||
MessageSegment.text("你的长度比"),
|
MessageSegment.from_text(" 你的长度比 "),
|
||||||
MessageSegment.at(user_id2),
|
MessageSegment.at(user_id2),
|
||||||
|
MessageSegment.from_text(text_part),
|
||||||
]
|
]
|
||||||
|
|
||||||
if jrcz == 0:
|
await event.reply(segments)
|
||||||
msg.append(MessageSegment.text("一样长。"))
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG7)))
|
|
||||||
elif jrcz > 0:
|
|
||||||
msg.append(MessageSegment.text("长" + str(jrcz) + "cm。"))
|
|
||||||
if jrcz <= 9:
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG1)))
|
|
||||||
elif jrcz <= 19:
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG2)))
|
|
||||||
else:
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG3)))
|
|
||||||
elif jrcz < 0:
|
|
||||||
msg.append(MessageSegment.text("短" + str(abs(jrcz)) + "cm。"))
|
|
||||||
if jrcz >= -9:
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG4)))
|
|
||||||
elif jrcz >= -19:
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG5)))
|
|
||||||
else:
|
|
||||||
msg.append(MessageSegment.text(random.choice(BBCDMSG6)))
|
|
||||||
await event.reply(msg)
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ thpic 插件
|
|||||||
|
|
||||||
from core.bot import Bot
|
from core.bot import Bot
|
||||||
from core.managers.command_manager import matcher
|
from core.managers.command_manager import matcher
|
||||||
from models import MessageEvent, MessageSegment
|
from models.events.message import MessageEvent, MessageSegment
|
||||||
|
|
||||||
__plugin_meta__ = {
|
__plugin_meta__ = {
|
||||||
"name": "thpic",
|
"name": "thpic",
|
||||||
@@ -26,6 +26,6 @@ async def handle_echo(bot: Bot, event: MessageEvent, args: list[str]):
|
|||||||
:param args: 指令参数列表(未使用)。
|
:param args: 指令参数列表(未使用)。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await event.reply(MessageSegment.image("https://img.paulzzh.com/touhou/random"))
|
await event.reply(str(MessageSegment.image("https://img.paulzzh.com/touhou/random")))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await event.reply("报错了。。。" + e)
|
await event.reply(f"报错了。。。{e}")
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ watchdog==6.0.0
|
|||||||
websockets==15.0.1
|
websockets==15.0.1
|
||||||
win32_setctime==1.2.0
|
win32_setctime==1.2.0
|
||||||
yarg==0.1.10
|
yarg==0.1.10
|
||||||
|
cachetools
|
||||||
|
pydantic
|
||||||
docker
|
docker
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
pytest-mock
|
pytest-mock
|
||||||
|
httpx==0.27.0
|
||||||
|
|
||||||
|
# Dev Dependencies
|
||||||
|
mypy
|
||||||
|
pydantic[mypy]
|
||||||
|
|||||||
Reference in New Issue
Block a user