mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-09-11 22:46:38 +00:00
🔖 Release 2.0.1
This commit is contained in:
288
website/versioned_docs/version-2.0.1/best-practice/alconna.md
Normal file
288
website/versioned_docs/version-2.0.1/best-practice/alconna.md
Normal file
@ -0,0 +1,288 @@
|
||||
---
|
||||
sidebar_position: 6
|
||||
description: Alconna 命令解析拓展
|
||||
---
|
||||
|
||||
# Alconna 命令解析
|
||||
|
||||
[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。
|
||||
该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器,
|
||||
是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。
|
||||
|
||||
特点包括:
|
||||
|
||||
- 高效
|
||||
- 直观的命令组件创建方式
|
||||
- 强大的类型解析与类型转换功能
|
||||
- 自定义的帮助信息格式
|
||||
- 多语言支持
|
||||
- 易用的快捷命令创建与使用
|
||||
- 可创建命令补全会话, 以实现多轮连续的补全提示
|
||||
- 可嵌套的多级子命令
|
||||
- 正则匹配支持
|
||||
|
||||
该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。
|
||||
|
||||
同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult`
|
||||
|
||||
该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应:
|
||||
|
||||
- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应
|
||||
- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应
|
||||
- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应
|
||||
|
||||
基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。
|
||||
标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。
|
||||
|
||||
## 安装插件
|
||||
|
||||
在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
||||
|
||||
在**项目目录**下执行以下命令:
|
||||
|
||||
```shell
|
||||
nb plugin install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```shell
|
||||
pip install nonebot-plugin-alconna
|
||||
```
|
||||
|
||||
## 使用插件
|
||||
|
||||
以下为一个简单的使用示例:
|
||||
|
||||
```python
|
||||
from nonebot_plugin_alconna.adapters import At
|
||||
from nonebot.adapters.onebot.v12 import Message
|
||||
from nonebot_plugin_alconna.adapters.onebot12 import Image
|
||||
from nonebot_plugin_alconna import AlconnaMatches, on_alconna
|
||||
from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS
|
||||
from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand
|
||||
|
||||
alc = Alconna(
|
||||
["/", "!"],
|
||||
"role-group",
|
||||
Subcommand(
|
||||
"add",
|
||||
Args["name", str],
|
||||
Option("member", Args["target", MultiVar(At)]),
|
||||
),
|
||||
Option("list"),
|
||||
)
|
||||
rg = on_alconna(alc, auto_send_output=True)
|
||||
|
||||
|
||||
@rg.handle()
|
||||
async def _(result: Arparma = AlconnaMatches()):
|
||||
if result.find("list"):
|
||||
img = await gen_role_group_list_image()
|
||||
await rg.finish(Message([Image(img)]))
|
||||
if result.find("add"):
|
||||
group = await create_role_group(result["add.name"])
|
||||
if result.find("add.member"):
|
||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
||||
group.extend(member.data["user_id"] for member in ats)
|
||||
await rg.finish("添加成功")
|
||||
```
|
||||
|
||||
### 导入插件
|
||||
|
||||
由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。
|
||||
|
||||
```python
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
|
||||
from nonebot_plugin_alconna import on_alconna
|
||||
```
|
||||
|
||||
### 命令编写
|
||||
|
||||
我们可以看到主要的两大组件:`Option` 与 `Subcommand`。
|
||||
|
||||
`Option` 可以传入一组别名,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]`
|
||||
|
||||
`Subcommand` 则可以传入自己的 `Option` 与 `Subcommand`:
|
||||
|
||||
他们拥有如下共同参数:
|
||||
|
||||
- `help_text`: 传入该组件的帮助信息
|
||||
- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)
|
||||
- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换
|
||||
- `default`: 默认值,在该组件未被解析时使用使用该值替换。
|
||||
|
||||
然后是 `Args` 与 `MultiVar`,他们是用于解析参数的组件。
|
||||
|
||||
`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`,
|
||||
与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。
|
||||
|
||||
`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。
|
||||
同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。
|
||||
|
||||
:::tip
|
||||
`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))`
|
||||
|
||||
`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。
|
||||
|
||||
`MultiVar` 不能在 `KeyWordVar` 之后传入。
|
||||
:::
|
||||
|
||||
### 参数标注
|
||||
|
||||
`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。
|
||||
|
||||
```python
|
||||
from arclet.alconna import Args
|
||||
from nepattern import BasePattern
|
||||
|
||||
# 表示 foo 参数需要匹配一个 @number 样式的字符串
|
||||
args = Args["foo", BasePattern("@\d+")]
|
||||
```
|
||||
|
||||
示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`。
|
||||
|
||||
默认支持的类型有:
|
||||
|
||||
- `str`: 匹配任意字符串
|
||||
- `int`: 匹配整数
|
||||
- `float`: 匹配浮点数
|
||||
- `bool`: 匹配 `True` 与 `False` 以及他们小写形式
|
||||
- `hex`: 匹配 `0x` 开头的十六进制字符串
|
||||
- `url`: 匹配网址
|
||||
- `email`: 匹配 `xxxx@xxx` 的字符串
|
||||
- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串
|
||||
- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串
|
||||
- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串
|
||||
- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳
|
||||
- `Any`: 匹配任意类型
|
||||
- `AnyString`: 匹配任意类型,转为 `str`
|
||||
- `Number`: 匹配 `int` 与 `float`,转为 `int`
|
||||
|
||||
同时可以使用 typing 中的类型:
|
||||
|
||||
- `Literal[X]`: 匹配其中的任意一个值
|
||||
- `Union[X, Y]`: 匹配其中的任意一个类型
|
||||
- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值
|
||||
- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型
|
||||
- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型
|
||||
- ...
|
||||
|
||||
:::tip
|
||||
几类特殊的传入标记:
|
||||
|
||||
- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联)
|
||||
- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换)
|
||||
- `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz"
|
||||
- `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型
|
||||
- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值
|
||||
- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0]
|
||||
- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象
|
||||
- `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值)
|
||||
- ...
|
||||
|
||||
:::
|
||||
|
||||
### 消息段标注
|
||||
|
||||
示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。
|
||||
|
||||
消息段标注会匹配特定的 `MessageSegment`:
|
||||
|
||||
```python
|
||||
...
|
||||
ats: tuple[Ob12MS, ...] = result["add.member.target"]
|
||||
group.extend(member.data["user_id"] for member in ats)
|
||||
```
|
||||
|
||||
:::tip
|
||||
通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段。
|
||||
|
||||
通用标注返回的是 `nonebot_plugin_alconna.adapters` 中定义的 `Segment` 模型:
|
||||
|
||||
```python
|
||||
class Segment:
|
||||
"""基类标注"""
|
||||
origin: MessageSegment
|
||||
|
||||
class At(Segment):
|
||||
"""At对象, 表示一类提醒某用户的元素"""
|
||||
target: str
|
||||
|
||||
class Emoji(Segment):
|
||||
"""Emoji对象, 表示一类表情元素"""
|
||||
id: str
|
||||
name: Optional[str]
|
||||
|
||||
class Media(Segment):
|
||||
url: Optional[str]
|
||||
id: Optional[str]
|
||||
|
||||
class Image(Media):
|
||||
"""Image对象, 表示一类图片元素"""
|
||||
|
||||
class Audio(Media):
|
||||
"""Audio对象, 表示一类音频元素"""
|
||||
|
||||
class Voice(Media):
|
||||
"""Voice对象, 表示一类语音元素"""
|
||||
|
||||
class Video(Media):
|
||||
"""Video对象, 表示一类视频元素"""
|
||||
|
||||
class File(Segment):
|
||||
"""File对象, 表示一类文件元素"""
|
||||
id: str
|
||||
name: Optional[str] = field(default=None)
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 响应器使用
|
||||
|
||||
`on_alconna` 的所有参数如下:
|
||||
|
||||
- `command: Alconna | str`: Alconna 命令
|
||||
- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应
|
||||
- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应
|
||||
- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法
|
||||
- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases
|
||||
- `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话
|
||||
|
||||
`AlconnaMatches` 是一个依赖注入函数,可注入 `Alconna` 命令解析结果。
|
||||
|
||||
### 配置项
|
||||
|
||||
#### alconna_auto_send_output
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
"是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。
|
||||
|
||||
#### alconna_use_command_start
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀
|
||||
|
||||
#### alconna_auto_completion
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `False`
|
||||
|
||||
是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。
|
||||
|
||||
## 文档参考
|
||||
|
||||
插件文档: [📦 这里](https://github.com/nonebot/plugin-alconna/blob/master/docs.md)
|
||||
|
||||
官方文档: [👉 指路](https://arclet.top/)
|
||||
|
||||
QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH)
|
||||
|
||||
友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html)
|
@ -0,0 +1,61 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 存储数据文件到本地
|
||||
---
|
||||
|
||||
# 数据存储
|
||||
|
||||
在使用插件的过程中,难免会需要存储一些持久化数据,例如用户的个人信息、群组的信息等。除了使用数据库等第三方存储之外,还可以使用本地文件来自行管理数据。NoneBot 提供了 `nonebot-plugin-localstore` 插件,可用于获取正确的数据存储路径并写入数据。
|
||||
|
||||
## 安装插件
|
||||
|
||||
在使用前请先安装 `nonebot-plugin-localstore` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
||||
|
||||
在**项目目录**下执行以下命令:
|
||||
|
||||
```bash
|
||||
nb plugin install nonebot-plugin-localstore
|
||||
```
|
||||
|
||||
## 使用插件
|
||||
|
||||
`nonebot-plugin-localstore` 插件兼容 Windows、Linux 和 macOS 等操作系统,使用时无需关心操作系统的差异。同时插件提供 `nb-cli` 脚本,可以使用 `nb localstore` 命令来检查数据存储路径。
|
||||
|
||||
在使用本插件前同样需要使用 `require` 方法进行**加载**并**导入**需要使用的方法,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解,如:
|
||||
|
||||
```python
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_localstore")
|
||||
|
||||
import nonebot_plugin_localstore as store
|
||||
|
||||
# 获取插件缓存目录
|
||||
cache_dir = store.get_cache_dir("plugin_name")
|
||||
# 获取插件缓存文件
|
||||
cache_file = store.get_cache_file("plugin_name", "file_name")
|
||||
# 获取插件数据目录
|
||||
data_dir = store.get_data_dir("plugin_name")
|
||||
# 获取插件数据文件
|
||||
data_file = store.get_data_file("plugin_name", "file_name")
|
||||
# 获取插件配置目录
|
||||
config_dir = store.get_config_dir("plugin_name")
|
||||
# 获取插件配置文件
|
||||
config_file = store.get_config_file("plugin_name", "file_name")
|
||||
```
|
||||
|
||||
:::danger 警告
|
||||
在 Windows 和 macOS 系统下,插件的数据目录和配置目录是同一个目录,因此在使用时需要注意避免文件名冲突。
|
||||
:::
|
||||
|
||||
插件提供的方法均返回一个 `pathlib.Path` 路径,可以参考 [pathlib 文档](https://docs.python.org/zh-cn/3/library/pathlib.html)来了解如何使用。常用的方法有:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
data_file = store.get_data_file("plugin_name", "file_name")
|
||||
# 写入文件内容
|
||||
data_file.write_text("Hello World!")
|
||||
# 读取文件内容
|
||||
data = data_file.read_text()
|
||||
```
|
@ -0,0 +1,298 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 部署你的机器人
|
||||
---
|
||||
|
||||
# 部署
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
在编写完成各类插件后,我们需要长期运行机器人来使得用户能够正常使用。通常,我们会使用云服务器来部署机器人。
|
||||
|
||||
我们在开发插件时,机器人运行的环境称为开发环境;而在部署后,机器人运行的环境称为生产环境。与开发环境不同的是,在生产环境中,开发者通常不能随意地修改/添加/删除代码,开启或停止服务。
|
||||
|
||||
## 部署前准备
|
||||
|
||||
### 项目依赖管理
|
||||
|
||||
由于部署后的机器人运行在生产环境中,因此,为确保机器人能够正常运行,我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理:
|
||||
|
||||
<Tabs groupId="tool">
|
||||
<TabItem value="poetry" label="Poetry" default>
|
||||
|
||||
[Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库,为你管理(安装/更新)它们。Poetry 提供了一个 `poetry.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。
|
||||
|
||||
Poetry 会在安装依赖时自动生成 `poetry.lock` 文件,在**项目目录**下执行以下命令:
|
||||
|
||||
```bash
|
||||
# 初始化 poetry 配置
|
||||
poetry init
|
||||
# 添加项目依赖,这里以 nonebot2[fastapi] 为例
|
||||
poetry add nonebot2[fastapi]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pdm" label="PDM">
|
||||
|
||||
[PDM](https://pdm.fming.dev/) 是一个现代 Python 项目的依赖管理工具。它采用 [PEP621](https://www.python.org/dev/peps/pep-0621/) 标准,依赖解析快速;同时支持 [PEP582](https://www.python.org/dev/peps/pep-0582/) 和 [virtualenv](https://virtualenv.pypa.io/)。PDM 提供了一个 `pdm.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。
|
||||
|
||||
PDM 会在安装依赖时自动生成 `pdm.lock` 文件,在**项目目录**下执行以下命令:
|
||||
|
||||
```bash
|
||||
# 初始化 pdm 配置
|
||||
pdm init
|
||||
# 添加项目依赖,这里以 nonebot2[fastapi] 为例
|
||||
pdm add nonebot2[fastapi]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pip" label="pip">
|
||||
|
||||
[pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具,为了尽可能保证环境的一致性,我们可以使用 `requirements.txt` 文件来声明依赖。
|
||||
|
||||
```bash
|
||||
pip freeze > requirements.txt
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 安装 Docker
|
||||
|
||||
[Docker](https://www.docker.com/) 是一个应用容器引擎,可以让开发者打包应用以及依赖包到一个可移植的镜像中,然后发布到服务器上。
|
||||
|
||||
我们可以参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 来安装 Docker 。
|
||||
|
||||
在 Linux 上,我们可以使用以下一键脚本来安装 Docker 以及 Docker Compose Plugin:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun
|
||||
```
|
||||
|
||||
在 Windows/macOS 上,我们可以使用 [Docker Desktop](https://docs.docker.com/desktop/) 来安装 Docker 以及 Docker Compose Plugin。
|
||||
|
||||
### 安装脚手架 Docker 插件
|
||||
|
||||
我们可以使用 [nb-cli-plugin-docker](https://github.com/nonebot/cli-plugin-docker) 来快速部署机器人。
|
||||
|
||||
插件可以帮助我们生成配置文件并构建 Docker 镜像,以及启动/停止/重启机器人。使用以下命令安装脚手架 Docker 插件:
|
||||
|
||||
```bash
|
||||
nb self install nb-cli-plugin-docker
|
||||
```
|
||||
|
||||
## Docker 部署
|
||||
|
||||
### 快速部署
|
||||
|
||||
使用脚手架命令即可一键生成配置并部署:
|
||||
|
||||
```bash
|
||||
nb docker up
|
||||
```
|
||||
|
||||
当看到 `Running` 字样时,说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志:
|
||||
|
||||
<Tabs groupId="deploy-tool">
|
||||
<TabItem value="nb-cli" label="NB CLI" default>
|
||||
|
||||
```bash
|
||||
nb docker logs
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
```bash
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
如果需要停止机器人,我们可以使用以下命令:
|
||||
|
||||
<Tabs groupId="deploy-tool">
|
||||
<TabItem value="nb-cli" label="NB CLI" default>
|
||||
|
||||
```bash
|
||||
nb docker down
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 自定义部署
|
||||
|
||||
在部分情况下,我们需要事先生成 Docker 配置文件,再到生产环境进行部署;或者自动生成的配置文件并不能满足复杂场景,需要根据实际需求手动修改配置文件。我们可以使用以下命令来生成基础配置文件:
|
||||
|
||||
```bash
|
||||
nb docker generate
|
||||
```
|
||||
|
||||
nb-cli 将会在项目目录下生成 `docker-compose.yml` 和 `Dockerfile` 等配置文件。在 nb-cli 完成配置文件的生成后,我们可以根据部署环境的实际情况使用 nb-cli 或者 Docker Compose 来启动机器人。
|
||||
|
||||
我们可以参考 [Dockerfile 文件规范](https://docs.docker.com/engine/reference/builder/)和 [Compose 文件规范](https://docs.docker.com/compose/compose-file/)修改这两个文件。
|
||||
|
||||
修改完成后我们可以直接启动或者手动构建镜像:
|
||||
|
||||
<Tabs groupId="deploy-tool">
|
||||
<TabItem value="nb-cli" label="NB CLI" default>
|
||||
|
||||
```bash
|
||||
# 启动机器人
|
||||
nb docker up
|
||||
# 手动构建镜像
|
||||
nb docker build
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose">
|
||||
|
||||
```bash
|
||||
# 启动机器人
|
||||
docker compose up -d
|
||||
# 手动构建镜像
|
||||
docker compose build
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### 持续集成
|
||||
|
||||
我们可以使用 GitHub Actions 来实现持续集成(CI),我们只需要在 GitHub 上发布 Release 即可自动构建镜像并推送至镜像仓库。
|
||||
|
||||
首先,我们需要在 [Docker Hub](https://hub.docker.com/) (或者其他平台,如:[GitHub Packages](https://github.com/features/packages)、[阿里云容器镜像服务](https://www.alibabacloud.com/zh/product/container-registry)等)上创建镜像仓库,用于存放镜像。
|
||||
|
||||
前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加构建所需的密钥:
|
||||
|
||||
- `DOCKERHUB_USERNAME`: 你的 Docker Hub 用户名
|
||||
- `DOCKERHUB_TOKEN`: 你的 Docker Hub PAT([创建方法](https://docs.docker.com/docker-hub/access-tokens/))
|
||||
|
||||
将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,并将文件中高亮行中的仓库名称替换为你的仓库名称:
|
||||
|
||||
```yaml title=.github/workflows/build.yml {34}
|
||||
name: Docker Hub Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Setup Docker
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Generate Tags
|
||||
uses: docker/metadata-action@v4
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
{organization}/{repository}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha
|
||||
|
||||
- name: Build and Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
```
|
||||
|
||||
### 持续部署
|
||||
|
||||
在完成发布并构建镜像后,我们可以自动将镜像部署到服务器上。
|
||||
|
||||
前往项目仓库的 `Settings` > `Secrets` > `actions` 栏目 `New Repository Secret` 添加部署所需的密钥:
|
||||
|
||||
- `DEPLOY_HOST`: 部署服务器的 SSH 地址
|
||||
- `DEPLOY_USER`: 部署服务器用户名
|
||||
- `DEPLOY_KEY`: 部署服务器私钥([创建方法](https://github.com/appleboy/ssh-action#setting-up-a-ssh-key))
|
||||
- `DEPLOY_PATH`: 部署服务器上的项目路径
|
||||
|
||||
将以下文件添加至**项目目录**下的 `.github/workflows/` 目录下,在构建成功后触发部署:
|
||||
|
||||
```yaml title=.github/workflows/deploy.yml
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Docker Hub Release
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Start Deployment
|
||||
uses: bobheadxi/deployments@v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: bot
|
||||
|
||||
- name: Run Remote SSH Command
|
||||
uses: appleboy/ssh-action@master
|
||||
env:
|
||||
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
envs: DEPLOY_PATH
|
||||
script: |
|
||||
cd $DEPLOY_PATH
|
||||
docker compose up -d --pull always
|
||||
|
||||
- name: update deployment status
|
||||
uses: bobheadxi/deployments@v0.6.2
|
||||
if: always()
|
||||
with:
|
||||
step: finish
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
status: ${{ job.status }}
|
||||
env: ${{ steps.deployment.outputs.env }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
```
|
||||
|
||||
将上一部分的 `docker-compose.yml` 文件以及 `.env.prod` 配置文件添加至 `DEPLOY_PATH` 目录下,并修改 `docker-compose.yml` 文件中的镜像配置,替换为 Docker Hub 的仓库名称:
|
||||
|
||||
```diff
|
||||
- build: .
|
||||
+ image: {organization}/{repository}:latest
|
||||
```
|
@ -0,0 +1,64 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 使用 sentry 进行错误跟踪
|
||||
---
|
||||
|
||||
# 错误跟踪
|
||||
|
||||
在应用实际运行过程中,可能会出现各种各样的错误。可能是由于代码逻辑错误,也可能是由于用户输入错误,甚至是由于第三方服务的错误。这些错误都会导致应用的运行出现问题,这时候就需要对错误进行跟踪,以便及时发现问题并进行修复。NoneBot 提供了 `nonebot-plugin-sentry` 插件,支持 [sentry](https://sentry.io/) 平台,可以方便地进行错误跟踪。
|
||||
|
||||
## 安装插件
|
||||
|
||||
在使用前请先安装 `nonebot-plugin-sentry` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
||||
|
||||
在**项目目录**下执行以下命令:
|
||||
|
||||
```bash
|
||||
nb plugin install nonebot-plugin-sentry
|
||||
```
|
||||
|
||||
## 使用插件
|
||||
|
||||
在安装完成之后,仅需要对插件进行简单的配置即可使用。
|
||||
|
||||
### 获取 sentry DSN
|
||||
|
||||
前往 [sentry](https://sentry.io/) 平台,注册并创建一个新的项目,然后在项目设置中找到 `Client Keys (DSN)`,复制其中的 `DSN` 值。
|
||||
|
||||
### 配置插件
|
||||
|
||||
:::warning 注意
|
||||
错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。
|
||||
:::
|
||||
|
||||
在项目 dotenv 配置文件中添加以下配置即可使用:
|
||||
|
||||
```dotenv
|
||||
SENTRY_DSN=<your_sentry_dsn>
|
||||
```
|
||||
|
||||
## 配置项
|
||||
|
||||
配置项具体含义参考 [Sentry Docs](https://docs.sentry.io/platforms/python/configuration/options/)。
|
||||
|
||||
- `sentry_dsn: str`
|
||||
- `sentry_debug: bool = False`
|
||||
- `sentry_release: str | None = None`
|
||||
- `sentry_release: str | None = None`
|
||||
- `sentry_environment: str | None = nonebot env`
|
||||
- `sentry_server_name: str | None = None`
|
||||
- `sentry_sample_rate: float = 1.`
|
||||
- `sentry_max_breadcrumbs: int = 100`
|
||||
- `sentry_attach_stacktrace: bool = False`
|
||||
- `sentry_send_default_pii: bool = False`
|
||||
- `sentry_in_app_include: List[str] = Field(default_factory=list)`
|
||||
- `sentry_in_app_exclude: List[str] = Field(default_factory=list)`
|
||||
- `sentry_request_bodies: str = "medium"`
|
||||
- `sentry_with_locals: bool = True`
|
||||
- `sentry_ca_certs: str | None = None`
|
||||
- `sentry_before_send: Callable[[Any, Any], Any | None] | None = None`
|
||||
- `sentry_before_breadcrumb: Callable[[Any, Any], Any | None] | None = None`
|
||||
- `sentry_transport: Any | None = None`
|
||||
- `sentry_http_proxy: str | None = None`
|
||||
- `sentry_https_proxy: str | None = None`
|
||||
- `sentry_shutdown_timeout: int = 2`
|
@ -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),可以帮助你更好地处理跨平台功能,包括事件处理和消息发送等。
|
||||
:::
|
@ -0,0 +1,96 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 定时执行任务
|
||||
---
|
||||
|
||||
# 定时任务
|
||||
|
||||
[APScheduler](https://apscheduler.readthedocs.io/en/3.x/) (Advanced Python Scheduler) 是一个 Python 第三方库,其强大的定时任务功能被广泛应用于各个场景。在 NoneBot 中,定时任务作为一个额外功能,依赖于基于 APScheduler 开发的 [`nonebot-plugin-apscheduler`](https://github.com/nonebot/plugin-apscheduler) 插件进行支持。
|
||||
|
||||
## 安装插件
|
||||
|
||||
在使用前请先安装 `nonebot-plugin-apscheduler` 插件至项目环境中,可参考[获取商店插件](../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如:
|
||||
|
||||
在**项目目录**下执行以下命令:
|
||||
|
||||
```bash
|
||||
nb plugin install nonebot-plugin-apscheduler
|
||||
```
|
||||
|
||||
## 使用插件
|
||||
|
||||
`nonebot-plugin-apscheduler` 本质上是对 [APScheduler](https://apscheduler.readthedocs.io/en/3.x/) 进行了封装以适用于 NoneBot 开发,因此其使用方式与 APScheduler 本身并无显著区别。在此我们会简要介绍其调用方法,更多的使用方面的功能请参考[APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html)。
|
||||
|
||||
### 导入调度器
|
||||
|
||||
由于 `nonebot_plugin_apscheduler` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `scheduler` 调度器来创建定时任务。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../advanced/requiring.md) 一节进行了解。
|
||||
|
||||
```python
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
```
|
||||
|
||||
### 添加定时任务
|
||||
|
||||
在 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/userguide.html#adding-jobs) 中提供了以下两种直接添加任务的方式:
|
||||
|
||||
```python
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_apscheduler")
|
||||
|
||||
from nonebot_plugin_apscheduler import scheduler
|
||||
|
||||
# 基于装饰器的方式
|
||||
@scheduler.scheduled_job("cron", hour="*/2", id="job_0", args=[1], kwargs={arg2: 2})
|
||||
async def run_every_2_hour(arg1: int, arg2: int):
|
||||
pass
|
||||
|
||||
# 基于 add_job 方法的方式
|
||||
def run_every_day(arg1: int, arg2: int):
|
||||
pass
|
||||
|
||||
scheduler.add_job(
|
||||
run_every_day, "interval", days=1, id="job_1", args=[1], kwargs={arg2: 2}
|
||||
)
|
||||
```
|
||||
|
||||
:::warning 注意
|
||||
由于 APScheduler 的定时任务并不是**由事件响应器所触发的事件**,因此其任务函数无法同[事件处理函数](../tutorial/handler.mdx#事件处理函数)一样通过[依赖注入](../tutorial/event-data.mdx#认识依赖注入)获取上下文信息,也无法通过事件响应器对象的方法进行任何操作,因此我们需要使用[调用平台 API](../appendices/api-calling.mdx#调用平台-api)的方式来获取信息或收发消息。
|
||||
|
||||
相对于事件处理依赖而言,编写定时任务更像是编写普通的函数,需要我们自行获取信息以及发送信息,请**不要**将事件处理依赖的特殊语法用于定时任务!
|
||||
:::
|
||||
|
||||
关于 APScheduler 的更多使用方法,可以参考 [APScheduler 官方文档](https://apscheduler.readthedocs.io/en/3.x/index.html) 进行了解。
|
||||
|
||||
### 配置项
|
||||
|
||||
#### apscheduler_autostart
|
||||
|
||||
- **类型**: `bool`
|
||||
- **默认值**: `True`
|
||||
|
||||
是否自动启动 `scheduler` ,若不启动需要自行调用 `scheduler.start()`。
|
||||
|
||||
#### apscheduler_log_level
|
||||
|
||||
- **类型**: `int`
|
||||
- **默认值**: `30`
|
||||
|
||||
apscheduler 输出的日志等级
|
||||
|
||||
- `WARNING` = `30` (默认)
|
||||
- `INFO` = `20`
|
||||
- `DEBUG` = `10` (只有在开启 nonebot 的 debug 模式才会显示 debug 日志)
|
||||
|
||||
#### apscheduler_config
|
||||
|
||||
- **类型**: `dict`
|
||||
- **默认值**: `{ "apscheduler.timezone": "Asia/Shanghai" }`
|
||||
|
||||
`apscheduler` 的相关配置。参考[配置调度器](https://apscheduler.readthedocs.io/en/latest/userguide.html#scheduler-config), [配置参数](https://apscheduler.readthedocs.io/en/latest/modules/schedulers/base.html#apscheduler.schedulers.base.BaseScheduler)
|
||||
|
||||
配置需要包含 `apscheduler.` 作为前缀,例如 `apscheduler.timezone`。
|
@ -0,0 +1,212 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 使用 NoneBug 进行单元测试
|
||||
|
||||
slug: /best-practice/testing/
|
||||
---
|
||||
|
||||
# 配置与测试事件响应器
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
> 在计算机编程中,单元测试(Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
|
||||
|
||||
为了保证代码的正确运行,我们不仅需要对错误进行跟踪,还需要对代码进行正确性检验,也就是测试。NoneBot 提供了一个测试工具——NoneBug,它是一个 [pytest](https://docs.pytest.org/en/stable/) 插件,可以帮助我们便捷地进行单元测试。
|
||||
|
||||
:::tip 提示
|
||||
建议在阅读本文档前先阅读 [pytest 官方文档](https://docs.pytest.org/en/stable/)来了解 pytest 的相关术语和基本用法。
|
||||
:::
|
||||
|
||||
## 安装 NoneBug
|
||||
|
||||
在**项目目录**下激活虚拟环境后运行以下命令安装 NoneBug:
|
||||
|
||||
<Tabs groupId="tool">
|
||||
<TabItem value="poetry" label="Poetry" default>
|
||||
|
||||
```bash
|
||||
poetry add nonebug -G test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pdm" label="PDM">
|
||||
|
||||
```bash
|
||||
pdm add nonebug -dG test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pip" label="pip">
|
||||
|
||||
```bash
|
||||
pip install nonebug
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例:
|
||||
|
||||
<Tabs groupId="tool">
|
||||
<TabItem value="poetry" label="Poetry" default>
|
||||
|
||||
```bash
|
||||
poetry add pytest-asyncio -G test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pdm" label="PDM">
|
||||
|
||||
```bash
|
||||
pdm add pytest-asyncio -dG test
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pip" label="pip">
|
||||
|
||||
```bash
|
||||
pip install pytest-asyncio
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## 配置测试
|
||||
|
||||
在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容:
|
||||
|
||||
```python title=tests/conftest.py
|
||||
import pytest
|
||||
import nonebot
|
||||
# 导入适配器
|
||||
from nonebot.adapters.console import Adapter as ConsoleAdapter
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_bot():
|
||||
# 加载适配器
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(ConsoleAdapter)
|
||||
|
||||
# 加载插件
|
||||
nonebot.load_from_toml("pyproject.toml")
|
||||
```
|
||||
|
||||
这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置:
|
||||
|
||||
```python {3,5,7-9} title=tests/conftest.py
|
||||
import os
|
||||
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
|
||||
os.environ["ENVIRONMENT"] = "test"
|
||||
|
||||
def pytest_configure(config: pytest.Config):
|
||||
config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")}
|
||||
```
|
||||
|
||||
## 编写插件测试
|
||||
|
||||
在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:
|
||||
|
||||
<details>
|
||||
<summary>插件示例</summary>
|
||||
|
||||
```python title=weather/__init__.py
|
||||
from nonebot import on_command
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.adapters import Message
|
||||
from nonebot.params import CommandArg, ArgPlainText
|
||||
|
||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "天气预报"})
|
||||
|
||||
@weather.handle()
|
||||
async def handle_function(matcher: Matcher, args: Message = CommandArg()):
|
||||
if args.extract_plain_text():
|
||||
matcher.set_arg("location", args)
|
||||
|
||||
@weather.got("location", prompt="请输入地名")
|
||||
async def got_location(location: str = ArgPlainText()):
|
||||
if location not in ["北京", "上海", "广州", "深圳"]:
|
||||
await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!")
|
||||
await weather.finish(f"今天{location}的天气是...")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```python {4,5,9,11-16} title=tests/test_weather.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
from nonebot.adapters.console import User, Message, MessageEvent
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather(app: App):
|
||||
from awesome_bot.plugins.weather import weather
|
||||
|
||||
event = MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message("/天气 北京"),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
```
|
||||
|
||||
在上面的代码中,我们引入了 NoneBug 的测试 `App` 对象,以及必要的适配器消息与事件定义等。在测试函数 `test_weather` 中,我们导入了要进行测试的事件响应器 `weather`。请注意,由于需要等待 NoneBot 初始化并加载插件完毕,插件内容必须在**测试函数内部**进行导入。然后,我们创建了一个 `MessageEvent` 事件对象,它模拟了一个用户发送了 `/天气 北京` 的消息。接下来,我们使用 `app.test_matcher` 方法来测试 `weather` 事件响应器:
|
||||
|
||||
```python {11-15} title=tests/test_weather.py
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather(app: App):
|
||||
from awesome_bot.plugins.weather import weather
|
||||
|
||||
event = MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message("/天气 北京"),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
async with app.test_matcher(weather) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "今天北京的天气是...", result=None)
|
||||
ctx.should_finished(weather)
|
||||
```
|
||||
|
||||
这里我们使用 `async with` 语句并通过参数指定要测试的事件响应器 `weather` 来进入测试上下文。在测试上下文中,我们可以使用 `ctx.create_bot` 方法创建一个虚拟的机器人实例,并使用 `ctx.receive_event` 方法来模拟机器人接收到消息事件。然后,我们就可以定义预期行为来测试机器人是否正确运行。在上面的代码中,我们使用 `ctx.should_call_send` 方法来断言机器人应该发送 `今天北京的天气是...` 这条消息,并且将发送函数的调用结果作为第三个参数返回给事件处理函数。如果断言失败,测试将会不通过。我们也可以使用 `ctx.should_finished` 方法来断言机器人应该结束会话。
|
||||
|
||||
为了测试更复杂的情况,我们可以为添加更多的测试用例。例如,我们可以测试用户输入了一个不支持的地名时机器人的反应:
|
||||
|
||||
```python {17-21,23-26} title=tests/test_weather.py
|
||||
def make_event(message: str = "") -> MessageEvent:
|
||||
return MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message(message),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_weather(app: App):
|
||||
from awesome_bot.plugins.weather import weather
|
||||
|
||||
async with app.test_matcher(weather) as ctx:
|
||||
... # 省略前面的测试用例
|
||||
|
||||
async with app.test_matcher(weather) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_event("/天气 南京")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "你想查询的城市 南京 暂不支持,请重新输入!", result=None)
|
||||
ctx.should_rejected(weather)
|
||||
|
||||
event = make_event("北京")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "今天北京的天气是...", result=None)
|
||||
ctx.should_finished(weather)
|
||||
```
|
||||
|
||||
在上面的代码中,我们使用 `ctx.should_rejected` 来断言机器人应该请求用户重新输入。然后,我们再次使用 `ctx.receive_event` 方法来模拟用户回复了 `北京`,并使用 `ctx.should_finished` 来断言机器人应该结束会话。
|
||||
|
||||
更多的 NoneBug 用法将在后续章节中介绍。
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "单元测试",
|
||||
"position": 5
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 测试事件响应、平台接口调用和会话控制
|
||||
---
|
||||
|
||||
# 测试事件响应与会话操作
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
在 NoneBot 接收到事件时,事件响应器根据优先级依次通过权限、响应规则来判断当前事件是否应该触发。事件响应流程中,机器人可能会通过 `send` 发送消息或者调用平台接口来执行预期的操作。因此,我们需要对这两种操作进行单元测试。
|
||||
|
||||
在上一节中,我们对单个事件响应器进行了简单测试。但是在实际场景中,机器人可能定义了多个事件响应器,由于优先级和响应规则的存在,预期的事件响应器可能并不会被触发。NoneBug 支持同时测试多个事件响应器,以此来测试机器人的整体行为。
|
||||
|
||||
## 测试事件响应
|
||||
|
||||
NoneBug 提供了六种定义 `Rule` 和 `Permission` 预期行为的方法:
|
||||
|
||||
- `should_pass_rule`
|
||||
- `should_not_pass_rule`
|
||||
- `should_ignore_rule`
|
||||
- `should_pass_permission`
|
||||
- `should_not_pass_permission`
|
||||
- `should_ignore_permission`
|
||||
|
||||
:::tip 提示
|
||||
事件响应器类型的检查属于 `Permission` 的一部分,因此可以通过 `should_pass_permission` 和 `should_not_pass_permission` 方法来断言事件响应器类型的检查。
|
||||
:::
|
||||
|
||||
下面我们根据插件示例来测试事件响应行为,我们首先定义两个事件响应器作为测试的对象:
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_command
|
||||
|
||||
def never_pass():
|
||||
return False
|
||||
|
||||
foo = on_command("foo")
|
||||
bar = on_command("bar", permission=never_pass)
|
||||
```
|
||||
|
||||
在这两个事件响应器中,`foo` 当收到 `/foo` 消息时会执行,而 `bar` 则不会执行。我们使用 NoneBug 来测试它们:
|
||||
|
||||
<Tabs groupId="testScope">
|
||||
<TabItem value="separate" label="独立测试" default>
|
||||
|
||||
```python {21,22,28,29} title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
from nonebot.adapters.console import User, Message, MessageEvent
|
||||
|
||||
def make_event(message: str = "") -> MessageEvent:
|
||||
return MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message(message),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo, bar
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_pass_rule()
|
||||
ctx.should_pass_permission()
|
||||
|
||||
async with app.test_matcher(bar) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_not_pass_rule()
|
||||
ctx.should_not_pass_permission()
|
||||
```
|
||||
|
||||
在上面的代码中,我们分别对 `foo` 和 `bar` 事件响应器进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="global" label="集成测试">
|
||||
|
||||
```python title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
from nonebot.adapters.console import User, Message, MessageEvent
|
||||
|
||||
def make_event(message: str = "") -> MessageEvent:
|
||||
return MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message(message),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo, bar
|
||||
|
||||
async with app.test_matcher() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_pass_rule(foo)
|
||||
ctx.should_pass_permission(foo)
|
||||
ctx.should_not_pass_rule(bar)
|
||||
ctx.should_not_pass_permission(bar)
|
||||
```
|
||||
|
||||
在上面的代码中,我们对 `foo` 和 `bar` 事件响应器一起进行响应测试。我们使用 `ctx.should_pass_rule` 和 `ctx.should_pass_permission` 断言 `foo` 事件响应器应该被触发,使用 `ctx.should_not_pass_rule` 和 `ctx.should_not_pass_permission` 断言 `bar` 事件响应器应该被忽略。通过参数,我们可以指定断言的事件响应器。
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
当然,如果需要忽略某个事件响应器的响应规则和权限检查,强行进入响应流程,我们可以使用 `should_ignore_rule` 和 `should_ignore_permission` 方法:
|
||||
|
||||
```python {21,22} title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
from nonebot.adapters.console import User, Message, MessageEvent
|
||||
|
||||
def make_event(message: str = "") -> MessageEvent:
|
||||
return MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message(message),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo, bar
|
||||
|
||||
async with app.test_matcher(bar) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_ignore_rule(bar)
|
||||
ctx.should_ignore_permission(bar)
|
||||
```
|
||||
|
||||
在忽略了响应规则和权限检查之后,就会进入 `bar` 事件响应器的响应流程。
|
||||
|
||||
## 测试平台接口使用
|
||||
|
||||
上一节的示例插件测试中,我们已经尝试了测试插件对事件的消息回复。通常情况下,事件处理流程中对平台接口的使用会通过事件响应器操作或者调用平台 API 两种途径进行。针对这两种途径,NoneBug 分别提供了 `ctx.should_call_send` 和 `ctx.should_call_api` 方法来测试平台接口的使用情况。
|
||||
|
||||
1. `should_call_send`
|
||||
|
||||
定义事件响应器预期发送的消息,即通过[事件响应器操作 send](../../appendices/session-control.mdx#send)进行的操作。`should_call_send` 有四个参数:
|
||||
|
||||
- `event`:回复的目标事件。
|
||||
- `message`:预期的消息对象,可以是 `str`、`Message` 或 `MessageSegment`。
|
||||
- `result`:send 的返回值,将会返回给插件。
|
||||
- `bot`(可选):发送消息的 bot 对象。
|
||||
- `**kwargs`:send 方法的额外参数。
|
||||
|
||||
2. `should_call_api`
|
||||
定义事件响应器预期调用的平台 API 接口,即通过[调用平台 API](../../appendices/api-calling.mdx#调用平台-API)进行的操作。`should_call_api` 有四个参数:
|
||||
|
||||
- `api`:API 名称。
|
||||
- `data`:预期的请求数据。
|
||||
- `result`:call_api 的返回值,将会返回给插件。
|
||||
- `adapter`(可选):调用 API 的平台适配器对象。
|
||||
- `**kwargs`:call_api 方法的额外参数。
|
||||
|
||||
下面是一个使用 `should_call_send` 和 `should_call_api` 方法的示例:
|
||||
|
||||
我们先定义一个测试插件,在响应流程中向用户发送一条消息并调用 `Console` 适配器的 `bell` API。
|
||||
|
||||
```python {8,9} title=example.py
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.console import Bot
|
||||
|
||||
foo = on_command("foo")
|
||||
|
||||
@foo.handle()
|
||||
async def _(bot: Bot):
|
||||
await foo.send("message")
|
||||
await bot.bell()
|
||||
```
|
||||
|
||||
然后我们对该插件进行测试:
|
||||
|
||||
```python {19,20,23,24} title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
import nonebot
|
||||
from nonebug import App
|
||||
from nonebot.adapters.console import Bot, User, Adapter, Message, MessageEvent
|
||||
|
||||
def make_event(message: str = "") -> MessageEvent:
|
||||
return MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message(message),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
bot = ctx.create_bot(base=Bot, adapter=adapter)
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "message", result=None, bot=bot)
|
||||
ctx.should_call_api("bell", {}, result=None, adapter=adapter)
|
||||
```
|
||||
|
||||
请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `create_bot` 方法中指定 `base` 和 `adapter` 参数,确保不会因为重载功能而出现非预期情况。
|
||||
|
||||
## 测试会话控制
|
||||
|
||||
在[会话控制](../../appendices/session-control.mdx)一节中,我们介绍了如何使用事件响应器操作来实现对用户的交互式会话。在上一节的示例插件测试中,我们其实已经使用了 `ctx.should_finished` 来断言会话结束。NoneBug 针对各种流程控制操作分别提供了相应的方法来定义预期的会话处理行为。它们分别是:
|
||||
|
||||
- `should_finished`:断言会话结束,对应 `matcher.finish` 操作。
|
||||
- `should_rejected`:断言会话等待用户输入并重新执行当前事件处理函数,对应 `matcher.reject` 系列操作。
|
||||
- `should_paused`: 断言会话等待用户输入并执行下一个事件处理函数,对应 `matcher.pause` 操作。
|
||||
|
||||
我们仅需在测试用例中的正确位置调用这些方法,就可以断言会话的预期行为。例如:
|
||||
|
||||
```python title=example.py
|
||||
from nonebot import on_command
|
||||
from nonebot.typing import T_State
|
||||
|
||||
foo = on_command("foo")
|
||||
|
||||
@foo.got("key", prompt="请输入密码")
|
||||
async def _(state: T_State, key: str = ArgPlainText()):
|
||||
if key != "some password":
|
||||
try_count = state.get("try_count", 1)
|
||||
if try_count >= 3:
|
||||
await foo.finish("密码错误次数过多")
|
||||
else:
|
||||
state["try_count"] = try_count + 1
|
||||
await foo.reject("密码错误,请重新输入")
|
||||
await foo.finish("密码正确")
|
||||
```
|
||||
|
||||
```python title=tests/test_example.py
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
from nonebot.adapters.console import User, Message, MessageEvent
|
||||
|
||||
def make_event(message: str = "") -> MessageEvent:
|
||||
return MessageEvent(
|
||||
time=datetime.now(),
|
||||
self_id="test",
|
||||
message=Message(message),
|
||||
user=User(user_id=123456789),
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_example(app: App):
|
||||
from awesome_bot.plugins.example import foo
|
||||
|
||||
async with app.test_matcher(foo) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
event = make_event("/foo")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "请输入密码", result=None)
|
||||
ctx.should_rejected(foo)
|
||||
event = make_event("wrong password")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "密码错误,请重新输入", result=None)
|
||||
ctx.should_rejected(foo)
|
||||
event = make_event("wrong password")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "密码错误,请重新输入", result=None)
|
||||
ctx.should_rejected(foo)
|
||||
event = make_event("wrong password")
|
||||
ctx.receive_event(bot, event)
|
||||
ctx.should_call_send(event, "密码错误次数过多", result=None)
|
||||
ctx.should_finished(foo)
|
||||
```
|
@ -0,0 +1,96 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 模拟网络通信以进行测试
|
||||
---
|
||||
|
||||
# 模拟网络通信
|
||||
|
||||
NoneBot 驱动器提供了多种方法来帮助适配器进行网络通信,主要包括客户端和服务端两种类型。模拟网络通信可以帮助我们更加接近实际机器人应用场景,进行更加真实的集成测试。同时,通过这种途径,我们还可以完成对适配器的测试。
|
||||
|
||||
NoneBot 中的网络通信主要包括以下几种:
|
||||
|
||||
- HTTP 服务端(WebHook)
|
||||
- WebSocket 服务端
|
||||
- HTTP 客户端
|
||||
- WebSocket 客户端
|
||||
|
||||
下面我们将分别介绍如何使用 NoneBug 来模拟这几种通信方式。
|
||||
|
||||
## 测试 HTTP 服务端
|
||||
|
||||
当 NoneBot 作为 ASGI 服务端应用时,我们可以定义一系列的路由来处理 HTTP 请求,适配器同样也可以通过定义路由来响应机器人相关的网络通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/http` ,用于接收平台 WebHook 并处理。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。
|
||||
|
||||
我们首先需要获取测试用模拟客户端:
|
||||
|
||||
```python {5,6} title=tests/test_http_server.py
|
||||
from nonebug import App
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_server(app: App):
|
||||
async with app.test_server() as ctx:
|
||||
client = ctx.get_client()
|
||||
```
|
||||
|
||||
默认情况下,`app.test_server()` 会通过 `nonebot.get_asgi` 获取测试对象,我们也可以通过参数指定 ASGI 应用:
|
||||
|
||||
```python
|
||||
async with app.test_server(asgi=asgi_app) as ctx:
|
||||
...
|
||||
```
|
||||
|
||||
获取到模拟客户端后,即可像 `requests`、`httpx` 等库类似的方法进行使用:
|
||||
|
||||
```python {3,11-14,16} title=tests/test_http_server.py
|
||||
import nonebot
|
||||
from nonebug import App
|
||||
from nonebot.adapters.fake import Adapter
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_server(app: App):
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
client = ctx.get_client()
|
||||
response = await client.post("/fake/http", json={"bot_id": "fake"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "success"}
|
||||
assert "fake" in nonebot.get_bots()
|
||||
|
||||
adapter.bot_disconnect(nonebot.get_bot("fake"))
|
||||
```
|
||||
|
||||
在上面的测试中,我们向 `/fake/http` 发送了一个模拟 POST 请求,适配器将会对该请求进行处理,我们可以通过检查请求返回是否正确、Bot 对象是否创建等途径来验证机器人是否正确运行。在完成测试后,我们通常需要对 Bot 对象进行清理,以避免对其他测试产生影响。
|
||||
|
||||
## 测试 WebSocket 服务端
|
||||
|
||||
当 NoneBot 作为 ASGI 服务端应用时,我们还可以定义一系列的路由来处理 WebSocket 通信。下面假设我们使用了一个适配器 `fake` ,它定义了一个路由 `/fake/ws` ,用于处理平台 WebSocket 连接信息。实际应用测试时,应将该路由地址替换为**真实适配器注册的路由地址**。
|
||||
|
||||
我们同样需要通过 `app.test_server()` 获取测试用模拟客户端,这里就不再赘述。在获取到模拟客户端后,我们可以通过 `client.websocket_connect` 方法来模拟 WebSocket 连接:
|
||||
|
||||
```python {3,11-15} title=tests/test_ws_server.py
|
||||
import nonebot
|
||||
from nonebug import App
|
||||
from nonebot.adapters.fake import Adapter
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ws_server(app: App):
|
||||
adapter = nonebot.get_adapter(Adapter)
|
||||
|
||||
async with app.test_server() as ctx:
|
||||
client = ctx.get_client()
|
||||
async with client.websocket_connect("/fake/ws") as ws:
|
||||
await ws.send_json({"bot_id": "fake"})
|
||||
response = await ws.receive_json()
|
||||
assert response == {"status": "success"}
|
||||
assert "fake" in nonebot.get_bots()
|
||||
```
|
||||
|
||||
在上面的测试中,我们向 `/fake/ws` 进行了 WebSocket 模拟通信,通过发送消息与机器人进行交互,然后检查机器人发送的信息是否正确。
|
||||
|
||||
## 测试 HTTP 客户端
|
||||
|
||||
~~暂不支持~~
|
||||
|
||||
## 测试 WebSocket 客户端
|
||||
|
||||
~~暂不支持~~
|
Reference in New Issue
Block a user