From 958c1df1fc48fbe235b58b4a6da5b20f914e9803 Mon Sep 17 00:00:00 2001 From: K2Cr2O1 <2221577113@qq.com> Date: Sun, 8 Mar 2026 19:02:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugin):=20=E6=96=B0=E5=A2=9E=E6=9E=81?= =?UTF-8?q?=E7=AE=80=E6=8F=92=E4=BB=B6=E5=BC=80=E5=8F=91=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 SimplePlugin 基类,提供面向新手的极简插件开发方式 添加相关示例代码和文档说明 --- core/plugin.py | 217 +++++++++++++++++++++++ docs/plugin-development/index.md | 7 + docs/plugin-development/simple-plugin.md | 127 +++++++++++++ plugins/class_style_example.py | 38 ++++ plugins/simple_style_example.py | 41 +++++ 5 files changed, 430 insertions(+) create mode 100644 core/plugin.py create mode 100644 docs/plugin-development/simple-plugin.md create mode 100644 plugins/class_style_example.py create mode 100644 plugins/simple_style_example.py diff --git a/core/plugin.py b/core/plugin.py new file mode 100644 index 0000000..c430c2b --- /dev/null +++ b/core/plugin.py @@ -0,0 +1,217 @@ +import inspect +import functools +from typing import Optional, Union, Any, Callable +from core.managers.command_manager import matcher as command_manager +from core.permission import Permission +from models.events.message import MessageEvent + +class Plugin: + """ + 插件基类,提供类风格的插件编写方式。 + 通过继承此类,可以使用装饰器在类方法上注册命令和事件处理器。 + """ + def __init__(self): + self._register_handlers() + + def _register_handlers(self): + """ + 自动注册带有装饰器的方法。 + """ + # 遍历实例的所有方法 + for name, method in inspect.getmembers(self, predicate=inspect.ismethod): + # 检查是否有命令元数据 + if hasattr(method, "_command_meta"): + meta = method._command_meta + # 调用 command_manager 的装饰器来注册绑定后的方法 + command_manager.command( + *meta['names'], + permission=meta.get('permission'), + override_permission_check=meta.get('override_permission_check', False) + )(method) + + # 检查是否有消息处理元数据 + if hasattr(method, "_on_message_meta"): + command_manager.on_message()(method) + + # 检查是否有通知处理元数据 + if hasattr(method, "_on_notice_meta"): + meta = method._on_notice_meta + command_manager.on_notice(notice_type=meta.get('notice_type'))(method) + + # 检查是否有请求处理元数据 + if hasattr(method, "_on_request_meta"): + meta = method._on_request_meta + command_manager.on_request(request_type=meta.get('request_type'))(method) + + async def send(self, event: MessageEvent, message: Union[str, Any]): + """ + 发送消息的基础逻辑。 + """ + if hasattr(event, 'reply'): + await event.reply(message) + else: + pass + + async def reply(self, event: MessageEvent, message: Union[str, Any]): + """ + 回复消息。 + """ + await self.send(event, message) + +class SimplePlugin(Plugin): + """ + 面向新手的简化插件基类。 + + 特性: + 1. 自动将公共方法(不以_开头)注册为指令。 + 2. 指令名默认为方法名。 + 3. 自动解析参数类型。 + 4. 支持直接返回字符串来回复消息。 + """ + def _register_handlers(self): + # 先处理带装饰器的方法 + super()._register_handlers() + + # 扫描普通方法并注册为指令 + for name, method in inspect.getmembers(self, predicate=inspect.ismethod): + if name.startswith("_"): + continue + if hasattr(method, "_command_meta"): + continue # 已经处理过 + if hasattr(method, "_on_message_meta"): + continue + if hasattr(method, "_on_notice_meta"): + continue + if hasattr(method, "_on_request_meta"): + continue + if name in dir(Plugin): + continue # 忽略基类方法 + + self._register_method_as_command(name, method) + + def _register_method_as_command(self, name: str, method: Callable): + # 获取方法的签名 + sig = inspect.signature(method) + + # 包装函数 + @functools.wraps(method) + async def wrapper(event: MessageEvent, args: list[str]): + try: + # 准备调用参数 + call_args: list[Any] = [] + + # 跳过 self,第一个参数应该是 event + params = list(sig.parameters.values()) + if not params: + # 方法没有参数?这不应该发生,至少要有 event + await method() + return + + # 绑定 event + call_args.append(event) + + # 处理剩余参数 + method_params = params[1:] # 除去 event + + if not method_params: + # 方法不需要额外参数 + pass + elif len(method_params) == 1: + # 只有一个参数,把所有 args 拼起来传给它 + param = method_params[0] + if args: + str_val = " ".join(args) + val: Any = str_val + # 类型转换 + if param.annotation is int: + val = int(str_val) + elif param.annotation is float: + val = float(str_val) + call_args.append(val) + elif param.default is not inspect.Parameter.empty: + call_args.append(param.default) + else: + await event.reply(f"缺少参数: {param.name}") + return + else: + # 多个参数,尝试一一对应 + if len(args) < len([p for p in method_params if p.default is inspect.Parameter.empty]): + # 必填参数不足 + usage = " ".join([f"<{p.name}>" for p in method_params]) + await event.reply(f"参数不足。用法: /{name} {usage}") + return + + for i, param in enumerate(method_params): + if i < len(args): + arg_str = args[i] + arg_val: Any = arg_str + # 简单的类型转换 + try: + if param.annotation is int: + arg_val = int(arg_str) + elif param.annotation is float: + arg_val = float(arg_str) + except ValueError: + await event.reply(f"参数 {param.name} 类型错误,应为 {param.annotation.__name__}") + return + call_args.append(arg_val) + else: + call_args.append(param.default) + + # 调用方法 + result = await method(*call_args) + + # 如果有返回值,自动回复 + if result is not None: + await event.reply(str(result)) + + except Exception as e: + await event.reply(f"执行命令时发生错误: {str(e)}") + + # 注册命令 + command_manager.command(name)(wrapper) + + +def command(name: str, *aliases: str, permission: Optional[Permission] = None, override_permission_check: bool = False): + """ + 装饰器:标记方法为命令处理器。 + """ + def decorator(func): + func._command_meta = { + "names": (name,) + aliases, + "permission": permission, + "override_permission_check": override_permission_check + } + return func + return decorator + +def on_message(): + """ + 装饰器:标记方法为通用消息处理器。 + """ + def decorator(func): + func._on_message_meta = {} + return func + return decorator + +def on_notice(notice_type: Optional[str] = None): + """ + 装饰器:标记方法为通知处理器。 + """ + def decorator(func): + func._on_notice_meta = { + "notice_type": notice_type + } + return func + return decorator + +def on_request(request_type: Optional[str] = None): + """ + 装饰器:标记方法为请求处理器。 + """ + def decorator(func): + func._on_request_meta = { + "request_type": request_type + } + return func + return decorator diff --git a/docs/plugin-development/index.md b/docs/plugin-development/index.md index 170fd06..f8a3351 100644 --- a/docs/plugin-development/index.md +++ b/docs/plugin-development/index.md @@ -73,6 +73,13 @@ Bot 应该会回复你:“你好,[你的昵称]!” 就这么简单,一个最基础的插件就写完了。 +## 极简插件开发(推荐新手) + +如果你觉得上面的装饰器写法太复杂,或者只是想快速写几个简单的指令,我们提供了一种**极简模式**。 +你只需要定义一个类,写几个方法,它们就会自动变成指令! + +- [查看极简插件开发指南](./simple-plugin.md) + ## 进阶阅读 - [指令处理](./command-handling.md): 了解如何处理参数、获取用户输入。 diff --git a/docs/plugin-development/simple-plugin.md b/docs/plugin-development/simple-plugin.md new file mode 100644 index 0000000..98de401 --- /dev/null +++ b/docs/plugin-development/simple-plugin.md @@ -0,0 +1,127 @@ +# 极简插件开发指南 + +如果你是 Python 新手,或者只是想快速写一些简单的指令,那么 `SimplePlugin` 是你的最佳选择。它让你无需理解复杂的装饰器和事件处理机制,只需要写普通的 Python 方法即可。 + +## 1. 快速开始 + +在 `plugins/` 目录下创建一个新文件,例如 `my_simple_plugin.py`: + +```python +from core.plugin import SimplePlugin +from models.events.message import MessageEvent + +class MyPlugin(SimplePlugin): + + async def hello(self, event: MessageEvent): + """ + 发送 /hello 即可调用 + """ + return "你好!这是极简插件。" + + async def echo(self, event: MessageEvent, msg: str): + """ + 发送 /echo <内容> 即可调用 + """ + return f"你说了: {msg}" + +# 必须实例化插件以生效 +plugin = MyPlugin() +``` + +就是这么简单!现在你可以发送 `/hello` 和 `/echo 测试` 来测试你的插件了。 + +## 2. 核心特性 + +### 方法即指令 + +在 `SimplePlugin` 的子类中,任何**不以下划线开头**的方法都会自动注册为指令。 +指令名称就是方法名。 + +例如: +- `async def ping(self, ...)` -> 注册为 `/ping` +- `async def help_me(self, ...)` -> 注册为 `/help_me` + +### 自动参数解析 + +框架会根据你定义的参数类型,自动解析用户输入的参数。 + +#### 字符串参数 +```python +async def greet(self, event: MessageEvent, name: str): + return f"你好, {name}" +``` +- 发送 `/greet Neo` -> `name` 参数为 `"Neo"` + +#### 数字参数 (自动转换类型) +```python +async def add(self, event: MessageEvent, a: int, b: int): + return f"{a} + {b} = {a + b}" +``` +- 发送 `/add 10 20` -> `a` 为 `10` (int), `b` 为 `20` (int) +- 如果用户输入非数字(如 `/add a b`),框架会自动提示参数类型错误。 + +#### 捕获剩余文本 +如果你的方法只有一个参数(除了 `event`),那么该参数会捕获指令后的所有文本。 +```python +async def broadcast(self, event: MessageEvent, content: str): + return f"广播内容: {content}" +``` +- 发送 `/broadcast 这是一个 很长 的消息` -> `content` 为 `"这是一个 很长 的消息"` + +### 自动回复 + +如果你的方法返回了字符串(`str`),框架会自动将其作为回复发送给用户。 +如果返回 `None`(即没有 return 语句),则不发送回复。 + +```python +async def silent(self, event: MessageEvent): + # 执行一些操作,但不回复 + print("Silent command executed") + # 也可以手动调用 reply + await event.reply("手动回复") +``` + +## 3. 进阶用法 + +### 访问事件对象 + +所有方法的第一个参数(除了 `self`)必须是 `event`。通过 `event` 对象,你可以获取更多信息: + +```python +async def whoami(self, event: MessageEvent): + user_id = event.user_id + nickname = event.sender.nickname + return f"你是 {nickname} ({user_id})" +``` + +### 混合使用装饰器 + +虽然 `SimplePlugin` 旨在简化开发,但你仍然可以使用装饰器来处理更复杂的场景,例如权限控制或监听非指令消息。 + +```python +from core.plugin import SimplePlugin, command, on_message +from core.permission import Permission + +class AdvancedPlugin(SimplePlugin): + + # 普通指令 + async def normal(self, event: MessageEvent): + return "普通指令" + + # 使用装饰器添加权限控制 + @command("admin_only", permission=Permission.ADMIN) + async def admin_op(self, event: MessageEvent, args: list[str]): + return "只有管理员能看到这个" + + # 监听所有消息 + @on_message() + async def handle_all(self, event: MessageEvent): + if "敏感词" in event.raw_message: + await event.reply("检测到敏感词!") +``` + +## 4. 注意事项 + +1. **方法名**:不要使用以 `_` 开头的方法名作为指令,这些方法会被忽略。 +2. **参数类型**:目前支持 `str`, `int`, `float` 的自动转换。 +3. **实例化**:不要忘记在文件末尾实例化你的类(`plugin = MyPlugin()`),否则插件不会生效。 diff --git a/plugins/class_style_example.py b/plugins/class_style_example.py new file mode 100644 index 0000000..7a7c54a --- /dev/null +++ b/plugins/class_style_example.py @@ -0,0 +1,38 @@ +from core.plugin import Plugin, command, on_message +from models.events.message import MessageEvent +from core.permission import Permission + +# 插件元信息 +__plugin_meta__ = { + "name": "类风格插件示例", + "description": "演示如何使用类风格编写插件", + "usage": "/hello - 打招呼\n/echo - 复读消息", +} + +class MyPlugin(Plugin): + def __init__(self): + super().__init__() + # 可以在这里初始化一些状态 + self.count = 0 + + @command("hello") + async def hello(self, event: MessageEvent, args: list[str]): + self.count += 1 + await self.reply(event, f"Hello from class-based plugin! (Called {self.count} times)") + + @command("echo", permission=Permission.USER) + async def echo(self, event: MessageEvent, args: list[str]): + if args: + await self.reply(event, " ".join(args)) + else: + await self.reply(event, "请输入要复读的内容。") + + @on_message() + async def handle_message(self, event: MessageEvent): + # 这是一个通用的消息处理器,会处理所有消息 + # 注意:这可能会与命令冲突,通常需要过滤 + if "特定关键词" in event.raw_message: + await self.reply(event, "检测到特定关键词!") + +# 实例化插件以注册 +plugin = MyPlugin() diff --git a/plugins/simple_style_example.py b/plugins/simple_style_example.py new file mode 100644 index 0000000..6fa9df2 --- /dev/null +++ b/plugins/simple_style_example.py @@ -0,0 +1,41 @@ +from core.plugin import SimplePlugin +from models.events.message import MessageEvent + +# 插件元信息 +__plugin_meta__ = { + "name": "极简插件示例", + "description": "演示面向新手的极简插件写法", + "usage": "/ping - 测试\n/add - 加法\n/greet - 问候", +} + +class MySimplePlugin(SimplePlugin): + + async def ping(self, event: MessageEvent): + """ + 发送 /ping 即可调用 + """ + return "Pong! (来自极简插件)" + + async def greet(self, event: MessageEvent, name: str): + """ + 发送 /greet Neo 即可调用 + """ + return f"你好, {name}!" + + async def add(self, event: MessageEvent, a: int, b: int): + """ + 发送 /add 10 20 即可调用 + 自动处理类型转换 + """ + return f"{a} + {b} = {a + b}" + + async def echo_all(self, event: MessageEvent, msg: str): + """ + 只有一个参数时,会自动捕获所有剩余文本 + 发送 /echo_all 这是一个 测试 消息 + msg 将会是 "这是一个 测试 消息" + """ + return f"复读: {msg}" + +# 实例化插件以生效 +plugin = MySimplePlugin()