Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: 新增插件跨平台指南 #1938

Merged
merged 7 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion website/docs/advanced/adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ driver = nonebot.get_driver()
driver.register_adapter(Adapter)
```

我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。
我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。

## 获取已注册的适配器

Expand Down
4 changes: 4 additions & 0 deletions website/docs/appendices/overload.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ async def handle_onebot(bot: OneBot):

但 Bot 和 Event 二者的参数类型注解具有最高检查优先级,如果二者类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。
:::

:::tip 提示
如何更好地编写一个跨平台的插件,我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。
:::
232 changes: 74 additions & 158 deletions website/docs/best-practice/multi-adapter.mdx
yanyongyu marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,224 +1,140 @@
---
sidebar_position: 4
description: 插件跨适配器支持
description: 插件跨平台支持
---

# 插件跨适配器支持
# 插件跨平台支持

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

由于不同平台存在着不同的接口和开发规范,因此 NoneBot 中并不存在一个全平台的适配器。为了使我们的插件适配尽可能多的平台,我们需要在编写插件时引入多个适配器
由于不同平台的事件与接口之间,存在着极大的差异性,NoneBot 通过[重载](../appendices/overload.md)的方式,使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题,我们应该尽可能的使用基类方法实现原生跨平台,而不是使用特定平台的方法。当基类方法无法满足需求时,我们可以使用依赖注入的方式,将特定平台的事件或机器人注入到事件处理函数中,实现针对特定平台的处理

:::warning 警告
本文将会涉及到大量 NoneBot 的基础知识,请确保您在阅读此文前了解单适配器的基本使用方式
:::tip 提示
如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器
:::

## 引入多适配器
## 基于基类的跨平台

首先请参考 [使用适配器](../advanced/adapter.md) 安装对应的适配器,并在入口文件中声明并注册,这样bot才能同时接收到来自多个适配器的消息。
在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件:

本文中,我们将会采用 [ONEBOT 协议适配器](https://onebot.adapters.nonebot.dev/) 中的 [ONEBOT V11](https://github.com/botuniverse/onebot-11) 和 [ONEBOT V12](https://12.onebot.dev/) 两个适配器进行演示,其入口文件如下所示。

```py
import nonebot
from nonebot.adapters.onebot.v11 import Adapter as ONEBOT_V11Adapter
from nonebot.adapters.onebot.v12 import Adapter as ONEBOT_V12Adapter

nonebot.init()

driver = nonebot.get_driver()
driver.register_adapter(ONEBOT_V11Adapter)
driver.register_adapter(ONEBOT_V12Adapter)

# nonebot.load_plugin("example_plugin_path")

if __name__ == "__main__":
nonebot.run()
```

## 基于基类的跨适配器

在 [获取事件信息](../tutorial/event-data.mdx) 中,我们实现了下方的插件。

```py
```python {5,11}
from nonebot import on_command
from nonebot.adapters import Message
from nonebot.params import CommandArg
from nonebot.adapters import Event

weather = on_command("天气", aliases={"weather", "查天气"}, priority=10, block=True)
async def is_blacklisted(event: Event) -> bool:
return event.get_user_id() not in BLACKLIST

weather = on_command("天气", rule=is_blacklisted, priority=10, block=True)

@weather.handle()
async def handle_function(args: Message = CommandArg()):
# 提取参数纯文本作为地名,并判断是否有效
if location := args.extract_plain_text():
await weather.finish(f"今天{location}的天气是...")
else:
await weather.finish("请输入地名")
async def handle_function():
await weather.finish("今天的天气是...")
```

由于此插件仅包含了纯文本的交互方式,因此可以直接使用基类中的 `Message` 对象完成代码的构建,此方法由于不使用某特定适配器的对象,因此是原生跨平台的,并不需要额外处理。
由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式,这些方法不使用特定平台的信息或接口,因此是原生跨平台的,并不需要额外处理。但在一些较为复杂的需求下,例如发送图片消息时,并非所有平台都具有统一的接口,因此基类便无能为力,我们需要引入特定平台的适配器了。

## 基于重载的跨平台

而在一些较为复杂的需求下,例如发送图片消息时,基类中的 `Message` 对象便无能为力,因此我们需要引入特定平台的适配器了
重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)中,我们又对依赖注入进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作
yanyongyu marked this conversation as resolved.
Show resolved Hide resolved

## 基于依赖注入的跨适配器
### 处理近似事件

由于依赖注入可以自由的定义需要的依赖类型,因此对于**差异不大**的数据类型我们可以使用将其注入到**同一个变量中**,以实现在同一个事件处理函数中响应多个不同适配器的事件。
对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#Event)的特性来实现这一功能。例如:

<Tabs groupId="python">
<TabItem value="3.10" label="Python 3.10+" default>

```py
```python
from nonebot import on_command
from nonebot.adapters import Message
from nonebot.adapters.onebot.v11 import MessageEvent as V11_MessageEvent
from nonebot.adapters.onebot.v12 import MessageEvent as V12_MessageEvent
from nonebot.params import CommandArg
from nonebot.adapters.github import IssueCommentCreated, IssueCommentEdited

weather = on_command("天气", aliases={"weather", "查天气"}, priority=10, block=True)

echo = on_command("/echo", priority=10, block=True)

@weather.handle()
async def handle_function(event: V11_MessageEvent | V12_MessageEvent, args: Message = CommandArg()):
user_id = event.get_user_id()
# 提取参数纯文本作为地名,并判断是否有效
if location := args.extract_plain_text():
await weather.finish(f"用户 {user_id} ,今天{location}的天气是...")
else:
await weather.finish(f"用户 {user_id} ,请输入地名")
@echo.handle()
async def handle_function(event: IssueCommentCreated | IssueCommentEdited, args: Message = CommandArg()):
await echo.finish(args)
```

</TabItem>
<TabItem value="3.8" label="Python 3.8+">

```py
```python
from typing import Union

from nonebot import on_command
from nonebot.adapters import Message
from nonebot.adapters.onebot.v11 import MessageEvent as V11_MessageEvent
from nonebot.adapters.onebot.v12 import MessageEvent as V12_MessageEvent
from nonebot.params import CommandArg
from nonebot.adapters.github import IssueCommentCreated, IssueCommentEdited

weather = on_command("天气", aliases={"weather", "查天气"}, priority=10, block=True)

echo = on_command("/echo", priority=10, block=True)

@weather.handle()
async def handle_function(event: Union[V11_MessageEvent, V12_MessageEvent], args: Message = CommandArg()):
user_id = event.get_user_id()
# 提取参数纯文本作为地名,并判断是否有效
if location := args.extract_plain_text():
await weather.finish(f"用户 {user_id} ,今天{location}的天气是...")
else:
await weather.finish(f"用户 {user_id} ,请输入地名")
@echo.handle()
async def handle_function(event: Union[IssueCommentCreated, IssueCommentEdited], args: Message = CommandArg()):
await echo.finish(args)
```

</TabItem>
</Tabs>

## 基于事件重载的跨适配器
### 处理多平台事件

在事件处理流程中,我们可以通过**依赖注入**为**不同的事件处理函数**注入**不同种类的依赖**,从而实现[事件重载](../appendices/overload.md)。那么通过为事件处理函数进行事件重载,便可以实现对**不同适配器事件**的分别处理了。此方法对于**差异较大**的数据类型可以分别编写逻辑,因此兼容性强于 [基于依赖注入的跨适配器](#基于依赖注入的跨适配器),但也带来了代码重复度较高的问题,因此我们可以使用 [会话控制](../appendices/session-control.mdx) 或 [自定义依赖注入](../advanced/dependency.mdx) 中的方法对一些信息进行预处理,避免重复的代码。
不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。例如:

<Tabs groupId="overload">
<TabItem value="session-control" label="会话控制" default>
```python
import inspect

```py
from nonebot import on_command
from nonebot.adapters import Message
from nonebot.adapters.onebot.v11 import MessageEvent as V11_MessageEvent
from nonebot.adapters.onebot.v12 import MessageEvent as V12_MessageEvent
from nonebot.adapters.onebot.v11 import MessageSegment as V11_MessageSegment
from nonebot.adapters.onebot.v12 import MessageSegment as V12_MessageSegment
from nonebot.adapters.onebot.v12 import Bot as V12_Bot
from nonebot.params import CommandArg
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.adapters import Message
from nonebot.params import CommandArg, ArgPlainText
from nonebot.adapters.console import Bot as ConsoleBot
from nonebot.adapters.onebot.v11 import Bot as OnebotBot
from nonebot.adapters.console import MessageSegment as ConsoleMessageSegment

weather = on_command("天气", aliases={"weather", "查天气"}, priority=10, block=True)


def render_weather_img(location:str) -> bytes:
# 根据城市名称渲染天气预报的图片
# do something here
...
weather = on_command("天气", priority=10, block=True)

@weather.handle()
async def get_location(args: Message = CommandArg(), state: T_State):
# 获取城市名称,并获取其对应的图片二进制数据
if location := args.extract_plain_text():
weather_img = render_weather_img(location)
state["weather"] = weather_img
else:
await weather.finish("请输入地名")
async def handle_function(matcher: Matcher, args: Message = CommandArg()):
if args.extract_plain_text():
matcher.set_arg("location", args)

# 处理控制台询问
@weather.got("location", prompt=ConsoleMessageSegment.emoji("question") + "请输入地名")
async def handle_console(bot: ConsoleBot):
pass

@weather.handle()
async def handle_function_v11(event: V11_MessageEvent, state: T_State):
# V11 的处理函数
reply = V11_MessageSegment.reply(event.message_id)
image = V11_MessageSegment.image(state["weather"])
await weather.finish(reply + image)
# 处理 OneBot 询问
@weather.got("location", prompt="请输入地名")
async def handle_onebot(bot: OnebotBot):
pass

# 处理控制台回复
@weather.handle()
async def handle_function_v12(bot: V12_Bot,event: V12_MessageEvent, state: T_State):
# V12 的处理函数
reply = V12_MessageSegment.reply(event.message_id)
fileid = await bot.upload_file(
type="data", name="weather.jpg", data=state["weather"]
async def handle_console_reply(bot: ConsoleBot, location: str = ArgPlainText()):
await weather.send(
ConsoleMessageSegment.markdown(
inspect.cleandoc(
f"""
# {location}

- 今天

⛅ 多云 20℃~24℃
"""
)
)
)
image = V12_MessageSegment.image(file_id=fileid["file_id"])
await weather.finish(reply + image)
```

</TabItem>
<TabItem value="dependency" label="自定义依赖注入">

```py
from nonebot import on_command
from nonebot.adapters import Message
from nonebot.adapters.onebot.v11 import MessageEvent as V11_MessageEvent
from nonebot.adapters.onebot.v11 import MessageSegment as V11_MessageSegment
from nonebot.adapters.onebot.v12 import Bot as V12_Bot
from nonebot.adapters.onebot.v12 import MessageEvent as V12_MessageEvent
from nonebot.adapters.onebot.v12 import MessageSegment as V12_MessageSegment
from nonebot.params import CommandArg, Depends

weather = on_command("天气", aliases={"weather", "查天气"}, priority=10, block=True)


def render_weather_img(location: str) -> bytes:
# 根据城市名称渲染天气预报的图片
# do something here
...


async def get_location(args: Message = CommandArg()):
# 获取城市名称,并获取其对应的图片二进制数据
if location := args.extract_plain_text():
return render_weather_img(location)
else:
await weather.finish("请输入地名")


@weather.handle()
async def handle_function_v11(event: V11_MessageEvent, data: bytes = Depends(get_location, use_cache=False)):
# V11 的处理函数
reply = V11_MessageSegment.reply(event.message_id)
image = V11_MessageSegment.image(data)
await weather.finish(reply + image)


# 处理 OneBot 回复
@weather.handle()
async def handle_function_v12(
bot: V12_Bot, event: V12_MessageEvent, data: bytes = Depends(get_location, use_cache=False)
):
# V12 的处理函数
reply = V12_MessageSegment.reply(event.message_id)
fileid = await bot.upload_file(type="data", name="weather.jpg", data=data)
image = V12_MessageSegment.image(file_id=fileid["file_id"])
await weather.finish(reply + image)
async def handle_onebot_reply(bot: OnebotBot, location: str = ArgPlainText()):
await weather.send(f"今天{location}的天气是⛅ 多云 20℃~24℃")
```

</TabItem>
</Tabs>
:::tip 提示
NoneBot 社区中有一些插件,例如[all4one](https://github.com/nonepkg/nonebot-plugin-all4one)、[send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere),可以帮助你更好地处理跨平台功能,包括事件处理和消息发送等。
:::
2 changes: 1 addition & 1 deletion website/docs/best-practice/testing/_category_.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"label": "单元测试",
"position": 4
"position": 5
}