Skip to content

Commit

Permalink
📝 Docs: 新增插件跨平台指南 (#1938)
Browse files Browse the repository at this point in the history
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
  • Loading branch information
Well2333 and yanyongyu committed Apr 24, 2023
1 parent dc0aea9 commit e55052e
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 12 deletions.
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
36 changes: 26 additions & 10 deletions website/docs/advanced/dependency.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,18 @@ async def _(e: Union[ActionFailed, NetworkError]): ...
from typing import Annotated

from nonebot import on_command
from nonebot.adapters import Event
from nonebot.params import Depends
from nonebot.matcher import Matcher
from nonebot.adapters.console import MessageEvent

test = on_command("test")

async def check(event: MessageEvent, matcher: Matcher) -> MessageEvent:
async def check(event: Event) -> Event:
if event.get_user_id() in BLACKLIST:
await matcher.finish()
await test.finish()
return event

@test.handle()
async def _(event: Annotated[MessageEvent, Depends(check)]):
async def _(event: Annotated[Event, Depends(check)]):
...
```

Expand All @@ -233,19 +232,18 @@ async def _(event: Annotated[MessageEvent, Depends(check)]):

```python {2,14}
from nonebot import on_command
from nonebot.adapters import Event
from nonebot.params import Depends
from nonebot.matcher import Matcher
from nonebot.adapters.console import MessageEvent

test = on_command("test")

async def check(event: MessageEvent, matcher: Matcher) -> MessageEvent:
async def check(event: Event) -> Event:
if event.get_user_id() in BLACKLIST:
await matcher.finish()
await test.finish()
return event

@test.handle()
async def _(event: MessageEvent = Depends(check)):
async def _(event: Event = Depends(check)):
...
```

Expand All @@ -256,6 +254,24 @@ async def _(event: MessageEvent = Depends(check)):

通过将 `Depends` 包裹的子依赖作为参数的默认值,我们就可以在执行事件处理函数之前执行子依赖,并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别,同样可以使用依赖注入,并且可以返回任何类型的值。但需要注意的是,如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**,将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。

特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如:

```python {2,14}
from nonebot import on_command
from nonebot.adapters import Event
from nonebot.params import Depends

test = on_command("test")

async def check(event: Event):
if event.get_user_id() in BLACKLIST:
await test.finish()

@test.handle(parameterless=[Depends(check)])
async def _():
...
```

### 依赖缓存

NoneBot 在执行子依赖时,会将其返回值缓存起来。当我们在使用子依赖时,`Depends` 具有一个参数 `use_cache`,默认为 `True`。此时在事件处理流程中,多次使用同一个子依赖时,将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用,例如:
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)中介绍。
:::
183 changes: 183 additions & 0 deletions website/docs/best-practice/multi-adapter.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
sidebar_position: 4
description: 插件跨平台支持
---

# 插件跨平台支持

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

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

:::tip 提示
如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器。
:::

## 基于基类的跨平台

[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件:

```python {5,11}
from nonebot import on_command
from nonebot.adapters import Event

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():
await weather.finish("今天的天气是...")
```

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

## 基于重载的跨平台

重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中,我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作。

### 处理近似事件

对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#Event)的特性来实现这一功能。例如:

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

```python
from nonebot import on_command
from nonebot.adapters import Message
from nonebot.params import CommandArg
from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent
from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent

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

@echo.handle()
async def handle_function(event: OnebotV11MessageEvent | OnebotV12MessageEvent, args: Message = CommandArg()):
await echo.finish(args)
```

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

```python
from typing import Union

from nonebot import on_command
from nonebot.adapters import Message
from nonebot.params import CommandArg
from nonebot.adapters.onebot.v11 import MessageEvent as OnebotV11MessageEvent
from nonebot.adapters.onebot.v12 import MessageEvent as OnebotV12MessageEvent

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

@echo.handle()
async def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEvent], args: Message = CommandArg()):
await echo.finish(args)
```

</TabItem>
</Tabs>

### 在依赖注入中使用重载

NoneBot 依赖注入系统提供了自定义子依赖的方法,子依赖的类型同样会影响到事件处理函数的重载行为。例如:

```python
from datetime import datetime

from nonebot import on_command
from nonebot.adapters.console import MessageEvent

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

def get_event_time(event: MessageEvent):
return event.time

# 处理控制台消息事件
@echo.handle()
async def handle_function(time: datetime = Depends(get_event_time)):
await echo.finish(time.strftime("%Y-%m-%d %H:%M:%S"))
```

示例中 ,我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖,而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ,而不能响应其他事件。

### 处理多平台事件

不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说,与平台绑定的处理部分应该与平台无关部分尽量分离。例如:

```python
import inspect

from nonebot import on_command
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("天气", priority=10, block=True)

@weather.handle()
async def handle_function(matcher: Matcher, args: Message = CommandArg()):
if args.extract_plain_text():
matcher.set_arg("location", args)


async def get_weather(state: T_State, location: str = ArgPlainText()):
if location not in ["北京", "上海", "广州", "深圳"]:
await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!")

state["weather"] = "⛅ 多云 20℃~24℃"


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

# 处理 OneBot 询问
@weather.got(
"location",
prompt="请输入地名",
parameterless=[Depends(get_weather)],
)
async def handle_onebot(bot: OnebotBot):
pass

# 通过依赖注入或事件处理函数来进行业务逻辑处理

# 处理控制台回复
@weather.handle()
async def handle_console_reply(bot: ConsoleBot, state: T_State, location: str = ArgPlainText()):
await weather.send(
ConsoleMessageSegment.markdown(
inspect.cleandoc(
f"""
# {location}
- 今天
{state['weather']}
"""
)
)
)

# 处理 OneBot 回复
@weather.handle()
async def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = ArgPlainText()):
await weather.send(f"今天{location}的天气是{state['weather']}")
```

:::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
}

0 comments on commit e55052e

Please sign in to comment.