From d83866f03be1f190e3aa1a0238623b483cedf363 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Nov 2022 03:55:37 +0000 Subject: [PATCH] :bookmark: Release 2.0.0rc2 --- website/src/pages/changelog.md | 2 +- .../versioned_docs/version-2.0.0rc2/README.md | 49 + .../version-2.0.0rc2/advanced/README.md | 207 ++++ .../advanced/di/_category_.json | 4 + .../advanced/di/dependency-injection.md | 243 +++++ .../version-2.0.0rc2/advanced/di/overload.md | 76 ++ .../advanced/images/Handle-Event.png | Bin 0 -> 384852 bytes .../advanced/images/plugin_store_publish.png | Bin 0 -> 105850 bytes .../images/plugin_store_publish_2.png | Bin 0 -> 131406 bytes .../version-2.0.0rc2/advanced/permission.md | 95 ++ .../advanced/publish-plugin.md | 72 ++ .../version-2.0.0rc2/advanced/rule.md | 75 ++ .../version-2.0.0rc2/advanced/runtime-hook.md | 171 ++++ .../version-2.0.0rc2/advanced/scheduler.md | 147 +++ .../advanced/unittest/README.mdx | 106 ++ .../advanced/unittest/_category_.json | 4 + .../advanced/unittest/test-adapters.md | 162 ++++ .../unittest/test-matcher-operation.md | 122 +++ .../advanced/unittest/test-matcher.md | 159 +++ .../version-2.0.0rc2/api/.gitkeep | 0 .../api/adapters/_category_.json | 3 + .../version-2.0.0rc2/api/adapters/index.md | 619 ++++++++++++ .../version-2.0.0rc2/api/config.md | 194 ++++ .../version-2.0.0rc2/api/consts.md | 122 +++ .../api/dependencies/_category_.json | 3 + .../api/dependencies/index.md | 98 ++ .../api/dependencies/utils.md | 48 + .../api/drivers/_category_.json | 3 + .../version-2.0.0rc2/api/drivers/aiohttp.md | 120 +++ .../version-2.0.0rc2/api/drivers/fastapi.md | 276 ++++++ .../version-2.0.0rc2/api/drivers/httpx.md | 52 + .../version-2.0.0rc2/api/drivers/index.md | 528 ++++++++++ .../version-2.0.0rc2/api/drivers/quart.md | 246 +++++ .../api/drivers/websockets.md | 134 +++ .../version-2.0.0rc2/api/exception.md | 260 +++++ .../version-2.0.0rc2/api/index.md | 207 ++++ .../version-2.0.0rc2/api/log.md | 75 ++ .../version-2.0.0rc2/api/matcher.md | 605 ++++++++++++ .../version-2.0.0rc2/api/message.md | 89 ++ .../version-2.0.0rc2/api/params.md | 378 ++++++++ .../version-2.0.0rc2/api/permission.md | 157 +++ .../api/plugin/_category_.json | 3 + .../version-2.0.0rc2/api/plugin/index.md | 89 ++ .../version-2.0.0rc2/api/plugin/load.md | 157 +++ .../version-2.0.0rc2/api/plugin/manager.md | 122 +++ .../version-2.0.0rc2/api/plugin/on.md | 914 ++++++++++++++++++ .../version-2.0.0rc2/api/plugin/plugin.md | 116 +++ .../version-2.0.0rc2/api/rule.md | 396 ++++++++ .../version-2.0.0rc2/api/typing.md | 219 +++++ .../version-2.0.0rc2/api/utils.md | 217 +++++ .../version-2.0.0rc2/start/editor-support.md | 33 + .../start/install-adapter.mdx | 45 + .../version-2.0.0rc2/start/install-driver.mdx | 47 + .../version-2.0.0rc2/start/install-plugin.mdx | 45 + .../version-2.0.0rc2/start/installation.mdx | 80 ++ .../version-2.0.0rc2/start/nb-cli.md | 84 ++ .../version-2.0.0rc2/start/question.md | 28 + .../tutorial/add-custom-api.md | 42 + .../version-2.0.0rc2/tutorial/call-api.md | 29 + .../tutorial/choose-driver.md | 262 +++++ .../tutorial/configuration.md | 240 +++++ .../tutorial/create-project.mdx | 79 ++ .../tutorial/custom-logger.md | 59 ++ .../version-2.0.0rc2/tutorial/deployment.md | 318 ++++++ .../tutorial/plugin/_category_.json | 5 + .../tutorial/plugin/config-plugin.md | 37 + .../tutorial/plugin/create-handler.md | 529 ++++++++++ .../tutorial/plugin/create-matcher.md | 133 +++ .../tutorial/plugin/example.mdx | 32 + .../tutorial/plugin/introduction.md | 71 ++ .../tutorial/plugin/load-plugin.md | 138 +++ .../tutorial/plugin/matcher-operation.md | 146 +++ .../tutorial/process-message.md | 253 +++++ .../tutorial/register-adapter.md | 97 ++ .../version-2.0.0rc2-sidebars.json | 50 + website/versions.json | 3 + 76 files changed, 11028 insertions(+), 1 deletion(-) create mode 100644 website/versioned_docs/version-2.0.0rc2/README.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/README.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/di/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/di/dependency-injection.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/di/overload.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/images/Handle-Event.png create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/images/plugin_store_publish.png create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/images/plugin_store_publish_2.png create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/permission.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/publish-plugin.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/rule.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/runtime-hook.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/scheduler.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/unittest/README.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/unittest/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/unittest/test-adapters.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/unittest/test-matcher-operation.md create mode 100644 website/versioned_docs/version-2.0.0rc2/advanced/unittest/test-matcher.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/.gitkeep create mode 100644 website/versioned_docs/version-2.0.0rc2/api/adapters/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/api/adapters/index.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/config.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/consts.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/dependencies/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/api/dependencies/index.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/dependencies/utils.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/aiohttp.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/fastapi.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/httpx.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/index.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/quart.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/drivers/websockets.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/exception.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/index.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/log.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/matcher.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/message.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/params.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/permission.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/plugin/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/api/plugin/index.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/plugin/load.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/plugin/manager.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/plugin/on.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/plugin/plugin.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/rule.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/typing.md create mode 100644 website/versioned_docs/version-2.0.0rc2/api/utils.md create mode 100644 website/versioned_docs/version-2.0.0rc2/start/editor-support.md create mode 100644 website/versioned_docs/version-2.0.0rc2/start/install-adapter.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/start/install-driver.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/start/install-plugin.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/start/installation.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/start/nb-cli.md create mode 100644 website/versioned_docs/version-2.0.0rc2/start/question.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/add-custom-api.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/call-api.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/choose-driver.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/configuration.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/create-project.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/custom-logger.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/deployment.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/_category_.json create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/config-plugin.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/create-handler.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/create-matcher.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/example.mdx create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/introduction.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/load-plugin.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/plugin/matcher-operation.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/process-message.md create mode 100644 website/versioned_docs/version-2.0.0rc2/tutorial/register-adapter.md create mode 100644 website/versioned_sidebars/version-2.0.0rc2-sidebars.json create mode 100644 website/versions.json diff --git a/website/src/pages/changelog.md b/website/src/pages/changelog.md index dffbd524..cd51c4aa 100644 --- a/website/src/pages/changelog.md +++ b/website/src/pages/changelog.md @@ -5,7 +5,7 @@ toc_max_heading_level: 2 # 更新日志 -## 最近更新 +## v2.0.0rc2 ### 💥 破坏性变更 diff --git a/website/versioned_docs/version-2.0.0rc2/README.md b/website/versioned_docs/version-2.0.0rc2/README.md new file mode 100644 index 00000000..4e0fffc6 --- /dev/null +++ b/website/versioned_docs/version-2.0.0rc2/README.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 0 +id: index +slug: / +--- + +# 概览 + +NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架,它基于 Python 的类型注解和异步特性,能够为你的需求实现提供便捷灵活的支持。 + +需要注意的是,NoneBot2 仅支持 **Python 3.8 以上版本** + +## 特色 + +### 异步优先 + +NoneBot2 基于 Python [asyncio](https://docs.python.org/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。 + +### 完整的类型注解 + +NoneBot2 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 `pyright`/`pylance` 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./start/editor-support))。 + +### 开箱即用 + +NoneBot2 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得初次接触 NoneBot2 时更容易上手。详细使用方法请参考各文档章节以及[使用脚手架](./start/nb-cli)。 + +### 插件系统 + +插件系统是 NoneBot2 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。 + +### 依赖注入系统 + +NoneBot2 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。 + +#### 什么是依赖注入 + +[**“依赖注入”**](https://zh.m.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**“依赖”**。 + +系统(在这里是指 NoneBot2)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**“注入”**依赖性) + +这在你有以下情形的需求时非常有用: + +- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复) +- 共享数据库以及网络请求连接会话 + - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session` +- 用户权限检查以及认证 +- 还有更多... + +它在完成上述工作的同时,还能尽量减少代码的耦合和重复 diff --git a/website/versioned_docs/version-2.0.0rc2/advanced/README.md b/website/versioned_docs/version-2.0.0rc2/advanced/README.md new file mode 100644 index 00000000..a96efe70 --- /dev/null +++ b/website/versioned_docs/version-2.0.0rc2/advanced/README.md @@ -0,0 +1,207 @@ +--- +id: index +sidebar_position: 0 +description: 深入了解 NoneBot2 运行机制 +slug: /advanced/ + +options: + menu: + weight: 10 + category: advanced +--- + +# 深入 + +:::danger 警告 +进阶部分尚未更新完成 +::: + +## 它如何工作? + +如同[概览](../README.md)所言: + +> NoneBot2 是一个可扩展的 Python 异步机器人框架,它会对机器人收到的事件进行解析和处理,并以插件化的形式,按优先级分发给事件所对应的事件响应器,来完成具体的功能。 + +NoneBot2 是一个可以对机器人上报的事件进行处理并完成具体功能的机器人框架,在这里,我们将简要讲述它的工作内容。 + +**便捷起见,以下内容对 NoneBot2 会被称为 NoneBot,与 NoneBot2 交互的机器人实现会被称为协议端**。 + +在实际应用中,NoneBot 会充当一个高性能,轻量级的 Python 微服务框架。协议端可以通过 http、websocket 等方式与之通信,这个通信往往是双向的:一方面,协议端可以上报数据给 NoneBot,NoneBot 会处理数据并返回响应给协议端;另一方面,NoneBot 可以主动推送数据给协议端。而 NoneBot 便是围绕双向通信进行工作的。 + +在开始工作之前,NoneBot 需要进行准备工作: + +1. **运行 `nonebot.init` 初始化函数**,它会读取配置文件,并初始化 NoneBot 和后端驱动 `Driver` 对象。 +2. **注册协议适配器 `Adapter`**。 +3. **加载插件**。 + +准备工作完成后,NoneBot 会利用 uvicorn 启动,并运行 `on_startup` 钩子函数。 + +随后,倘若一个协议端与 NoneBot 进行了连接,NoneBot 的后端驱动 `Driver` 就会将数据交给 `Adapter`,然后会实例化 `Bot`,NoneBot 便会利用 `Bot` 开始工作,它的工作内容分为两个方面: + +1. **事件处理**,`Bot` 会将协议端上报的数据转化为 `Event`(事件),之后 NoneBot 会根据一套既定流程来处理事件。 + +2. **调用 `API`**,在**事件处理**的过程中,NoneBot 可以通过 `Bot` 调用协议端指定的 `API` 来获取更多数据,或者反馈响应给协议端;NoneBot 也可以通过调用 `API` 向协议端主动请求数据或者主动推送数据。 + +在**指南**模块,我们已经叙述了[如何配置 NoneBot](../tutorial/configuration.md)、[如何注册协议适配器](../tutorial/register-adapter.md)以及[如何加载插件](../tutorial/plugin/load-plugin.md),这里便不再赘述。 + +下面,我们将对**事件处理**,**调用 API** 进行说明。 + +## 事件处理 + +我们可以先看事件处理的流程图: + +![handle-event](./images/Handle-Event.png) + +在流程图里,我们可以看到,NoneBot 会有三个阶段来处理事件: + +1. **Driver 接收上报数据** +2. **Adapter 处理原始数据** +3. **NoneBot 处理 Event** + +我们将顺序说明这三个阶段。其中,会将第三个阶段拆分成**概念解释**,**处理 Event**,**特殊异常处理**三个部分来说明。 + +### Driver 接收上报数据 + +1. 协议端会通过 websocket 或 http 等方式与 NoneBot 的后端驱动 `Driver` 连接,协议端上报数据后,`Driver` 会将原始数据交给 `Adapter` 处理。 + +:::warning +连接之前必须要注册 `Adapter` +::: + +### Adapter 处理原始数据 + +1. `Adapter` 检查授权许可,并获取 `self-id` 作为唯一识别 id 。 + +:::tip +如果协议端通过 websocket 上报数据,这个步骤只会在建立连接时进行,并在之后运行 `on_bot_connect` 钩子函数;通过 http 方式连接时,会在协议端每次上报数据时都进行这个步骤。 +::: + +:::warning +`self-id` 是帐号的唯一识别 ID ,这意味着不能出现相同的 `self-id`。 +::: + +2. 根据 `self-id` 实例化 `Adapter` 相应的 `Bot` 。 + +3. 根据 `Event Model` 将原始数据转化为 NoneBot 可以处理的 `Event` 对象。 + +:::tip +`Adapter` 在转换数据格式的同时可以进行一系列的特殊操作,例如 OneBot 适配器会对 reply 信息进行提取。 +::: + +4. `Bot` 和 `Event` 交由 NoneBot 进一步处理。 + +### NoneBot 处理 Event + +在讲述这个阶段之前,我们需要先对几个概念进行解释。 + +#### 概念解释 + +1. **hook** ,或者说**钩子函数**,它们可以在 NoneBot 处理 `Event` 的不同时刻进行拦截,修改或者扩展,在 NoneBot 中,事件钩子函数分为`事件预处理 hook`、`运行预处理 hook`、`运行后处理 hook` 和`事件后处理 hook`。 + +:::tip +关于 `hook` 的更多信息,可以查阅[这里](./runtime-hook.md)。 +::: + +2. **Matcher** 与 **matcher**,在**指南**中,我们讲述了[如何注册事件响应器](../tutorial/plugin/create-matcher.md),这里的事件响应器或者说 `Matcher` 并不是一个具体的实例 `instance`,而是一个具有特定属性的类 `class`。只有当 `Matcher` **响应事件**时,才会实例化为具体的 `instance`,也就是 `matcher` 。`matcher` 可以认为是 NoneBot 处理 `Event` 的基本单位,运行 `matcher` 是 NoneBot 工作的主要内容。 + +3. **handler**,或者说**事件处理函数**,它们可以认为是 NoneBot 处理 `Event` 的最小单位。在不考虑 `hook` 的情况下,**运行 matcher 就是顺序运行 matcher.handlers**,这句话换种表达方式就是,`handler` 只有添加到 `matcher.handlers` 时,才可以参与到 NoneBot 的工作中来。 + +:::tip +如何让 `handler` 添加到 `matcher.handlers`? + +一方面,我们可以参照[这里](../tutorial/plugin/create-handler.md)利用装饰器来添加;另一方面,我们在用 `on()` 或者 `on_*()` 注册事件响应器时,可以添加 `handlers=[handler1, handler2, ...]` 这样的关键词参数来添加。 +::: + +#### 处理 Event + +1. **执行事件预处理 hook**, NoneBot 接收到 `Event` 后,会传入到 `事件预处理 hook` 中进行处理。 + +:::warning +需要注意的是,执行多个 `事件预处理 hook` 时并无顺序可言,它们是**并发运行**的。这个原则同样适用于其他的 `hook`。 +::: + +2. **按优先级升序选出同一优先级的 Matcher**,NoneBot 提供了一个全局字典 `matchers`,这个字典的 `key` 是优先级 `priority`,`value` 是一个 `list`,里面存放着同一优先级的 `Matcher`。在注册 `Matcher` 时,它和优先级 `priority` 会添加到里面。 + + 在执行 `事件预处理 hook` 后,NoneBot 会对 `matchers` 的 `key` 升序排序并选择出当前最小优先级的 `Matcher`。 + +3. **根据 Matcher 定义的 Rule、Permission 判断是否运行**,在选出 `Matcher` 后,NoneBot 会将 `bot`,`Event` 传入到 `Matcher.check_rule` 和 `Matcher.check_perm` 两个函数中,两个函数分别对 Matcher 定义的 `Rule`、`Permission` 进行 check,当 check 通过后,这个 `Matcher` 就会响应事件。当同一个优先级的所有 `Matcher` 均没有响应时,NoneBot 会返回到上一个步骤,选择出下一优先级的 `Matcher`。 + +4. **实例化 matcher 并执行运行预处理 hook**,当 `Matcher` 响应事件后,它便会实例化为 `matcher`,并执行 `运行预处理 hook`。 + +5. **顺序运行 matcher 的所有 handlers**,`运行预处理 hook` 执行完毕后,便会运行 `matcher`,也就是**顺序运行**它的 `handlers`。 + +:::tip +`matcher` 运行 `handlers` 的顺序是:先运行该 `matcher` 的类 `Matcher` 注册时添加的 `handlers`(如果有的话),再按照装饰器装饰顺序运行装饰的 `handlers`。 +::: + +6. **执行运行后处理 hook**,`matcher` 的 `handlers` 运行完毕后,会执行 `运行后处理 hook`。 + +7. **判断是否停止事件传播**,NoneBot 会根据当前优先级所有 `matcher` 的 `block` 参数或者 `StopPropagation` 异常判断是否停止传播 `Event`,如果事件没有停止传播,NoneBot 便会返回到第 2 步, 选择出下一优先级的 `Matcher`。 + +8. **执行事件后处理 hook**,在 `Event` 停止传播或执行完所有响应的 `Matcher` 后,NoneBot 会执行 `事件后处理 hook`。 + + 当 `事件后处理 hook` 执行完毕后,当前 `Event` 的处理周期就顺利结束了。 + +#### 特殊异常处理 + +在这个阶段,NoneBot 规定了几个特殊的异常,当 NoneBot 捕获到它们时,会用特定的行为来处理它们。 + +1. **IgnoredException** + + 这个异常可以在 `事件预处理 hook` 和 `运行预处理 hook` 抛出。 + + 当 `事件预处理 hook` 抛出它时,NoneBot 会忽略当前的 `Event`,不进行处理。 + + 当 `运行预处理 hook` 抛出它时,NoneBot 会忽略当前的 `matcher`,结束当前 `matcher` 的运行。 + +:::warning +当 `hook` 需要抛出这个异常时,要写明原因。 +::: + +2. **PausedException** + + 这个异常可以在 `handler` 中由 `Matcher.pause` 抛出。 + + 当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将后续的 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行后续的 `handler`。 + +3. **RejectedException** + + 这个异常可以在 `handler` 中由 `Matcher.reject` 抛出。 + + 当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行,并将当前 handler 和后续 `handler` 交给一个临时 `Matcher` 来响应当前交互用户的下一个消息事件,当临时 `Matcher` 响应时,临时 `Matcher` 会运行当前 `handler` 和后续的 `handler` 。 + +4. **FinishedException** + + 这个异常可以在 `handler` 中由 `Matcher.finish` 抛出。 + + 当 NoneBot 捕获到它时,会停止运行当前 `handler` 并结束当前 `matcher` 的运行。 + +5. **StopPropagation** + + 这个异常一般会在执行 `运行后处理 hook` 后抛出。 + + 当 NoneBot 捕获到它时, 会停止传播当前 `Event` ,不再寻找下一优先级的 `Matcher` ,直接执行 `事件后处理 hook` 。 + +## 调用 API + +NoneBot 可以通过 `bot` 来调用 `API`,`API` 可以向协议端发送数据,也可以向协议端请求更多的数据。 + +NoneBot 调用 `API` 会有如下过程: + +1. 调用 `calling_api_hook` 预处理钩子。 + +2. `adapter` 将信息处理为原始数据,并转交 `driver`,`driver` 交给协议端处理。 + +3. `driver` 接收协议端的结果,交给`adapter` 处理之后将结果反馈给 NoneBot 。 + +4. 调用 `called_api_hook` 后处理钩子。 + +在调用 `API` 时同样规定了特殊的异常,叫做 `MockApiException` 。该异常会由预处理钩子和后处理钩子触发,当预处理钩子触发时,NoneBot 会跳过之后的调用过程,直接执行后处理钩子。 + +:::tip +不同 `adapter` 规定了不同的 API,对应的 API 列表请参照协议规范。 +::: + +一般来说,我们可以用 `bot.*` 来调用 `API`(\*是 `API` 的 `action` 或者 `endpoint`)。 + +对于发送消息而言,一方面可以调用既有的 `API` ;另一方面 NoneBot 实现了两个便捷方法,`bot.send(event, message, **kwargs)` 方法和可以在 `handler` 中使用的 `Matcher.send(message, **kwargs)` 方法,来向事件主体发送消息。 diff --git a/website/versioned_docs/version-2.0.0rc2/advanced/di/_category_.json b/website/versioned_docs/version-2.0.0rc2/advanced/di/_category_.json new file mode 100644 index 00000000..60e7734e --- /dev/null +++ b/website/versioned_docs/version-2.0.0rc2/advanced/di/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "依赖注入", + "position": 5 +} diff --git a/website/versioned_docs/version-2.0.0rc2/advanced/di/dependency-injection.md b/website/versioned_docs/version-2.0.0rc2/advanced/di/dependency-injection.md new file mode 100644 index 00000000..66645709 --- /dev/null +++ b/website/versioned_docs/version-2.0.0rc2/advanced/di/dependency-injection.md @@ -0,0 +1,243 @@ +--- +sidebar_position: 1 +description: 依赖注入简介 + +options: + menu: + weight: 60 + category: advanced +--- + +# 简介 + +受 [FastAPI](https://fastapi.tiangolo.com/tutorial/dependencies/) 启发,NoneBot 同样编写了一个简易的依赖注入模块,使得开发者可以通过事件处理函数参数的类型标注来自动注入依赖。 + +## 什么是依赖注入? + +[依赖注入](https://zh.wikipedia.org/wiki/%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5) + +> 在软件工程中,**依赖注入**(dependency injection)的意思为,给予调用方它所需要的事物。 “依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖。 传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。 + +依赖注入往往起到了分离依赖和调用方的作用,这样一方面能让代码更为整洁可读,一方面可以提升代码的复用性。 + +## 使用依赖注入 + +以下通过一个简单的例子来说明依赖注入的使用方法: + +```python {2,7-8,11} +from nonebot import on_command +from nonebot.params import Depends # 1.引用 Depends +from nonebot.adapters.onebot.v11 import MessageEvent + +test = on_command("123") + +async def depend(event: MessageEvent): # 2.编写依赖函数 + return {"uid": event.get_user_id(), "nickname": event.sender.nickname} + +@test.handle() +async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项 + print(x["uid"], x["nickname"]) +``` + +如注释所言,可以用三步来说明依赖注入的使用过程: + +1. 引用 `Depends` 。 + +2. 编写依赖函数。依赖函数和普通的事件处理函数并无区别,同样可以接收 `bot`, `event`, `state` 等参数,你可以把它当作一个普通的事件处理函数,但是去除了装饰器(没有使用 `matcher.handle()` 等来装饰),并且可以返回任何类型的值。 + + 在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注,返回一个新的字典,包括 `uid` 和 `nickname` 两个键值。 + +3. 在事件处理函数中声明依赖项。依赖项必须要 `Depends` 包裹依赖函数作为默认值。 + +:::tip +请注意,参数 `x` 的类型标注将会影响到事件处理函数的运行,与类型标注不符的值将会导致事件处理函数被跳过。 +::: + +:::tip +事实上,bot、event、state 它们本身只是依赖注入的一个特例,它们无需声明这是依赖即可注入。 +::: + +虽然声明依赖项的方式和其他参数如 `bot`, `event` 并无二样,但他的参数有一些限制,必须是**可调用对象**,函数自然是可调用对象,类和生成器也是,我们会在接下来的小节说明。 + +一般来说,当接收到事件时,`NoneBot2` 会进行以下处理: + +1. 准备依赖函数所需要的参数。 +2. 调用依赖函数并获得返回值。 +3. 将返回值作为事件处理函数中的参数值传入。 + +## 依赖缓存 + +在使用 `Depends` 包裹依赖函数时,有一个参数 `use_cache` ,它默认为 `True` ,这个参数会决定 `Nonebot2` 在依赖注入的处理中是否使用缓存。 + +```python {11} +import random +from nonebot import on_command +from nonebot.params import Depends + +test = on_command("123") + +async def always_run(): + return random.randint(1, 100) + +@test.handle() +async def _(x: int = Depends(always_run, use_cache=False)): + print(x) +``` + +:::tip +缓存是针对单次事件处理来说的,在事件处理中 `Depends` 第一次被调用时,结果存入缓存,在之后都会直接返回缓存中的值,在事件处理结束后缓存就会被清除。 +::: + +当使用缓存时,依赖注入会这样处理: + +1. 查询缓存,如果缓存中有相应的值,则直接返回。 +2. 准备依赖函数所需要的参数。 +3. 调用依赖函数并获得返回值。 +4. 将返回值存入缓存。 +5. 将返回值作为事件处理函数中的参数值传入。 + +## 同步支持 + +我们在编写依赖函数时,可以简单地用同步函数,`NoneBot2` 的内部流程会进行处理: + +```python {2,8-9,12} +from nonebot.log import logger +from nonebot.params import Depends # 1.引用 Depends +from nonebot import on_command, on_message +from nonebot.adapters.onebot.v11 import MessageEvent + +test = on_command("123") + +def depend(event: MessageEvent): # 2.编写同步依赖函数 + return {"uid": event.get_user_id(), "nickname": event.sender.nickname} + +@test.handle() +async def _(x: dict = Depends(depend)): # 3.在事件处理函数里声明依赖项 + print(x["uid"], x["nickname"]) +``` + +## Class 作为依赖 + +我们可以看下面的代码段: + +```python +class A: + def __init__(self): + pass +a = A() +``` + +在我们实例化类 `A` 的时候,其实我们就在**调用**它,类本身也是一个**可调用对象**,所以类可以被 `Depends` 包裹成为依赖项。 + +因此我们对第一节的代码段做一下改造: + +```python {2,7-10,13} +from nonebot import on_command +from nonebot.params import Depends # 1.引用 Depends +from nonebot.adapters.onebot.v11 import MessageEvent + +test = on_command("123") + +class DependClass: # 2.编写依赖类 + def __init__(self, event: MessageEvent): + self.uid = event.get_user_id() + self.nickname = event.sender.nickname + +@test.handle() +async def _(x: DependClass = Depends(DependClass)): # 3.在事件处理函数里声明依赖项 + print(x.uid, x.nickname) +``` + +依然可以用三步说明如何用类作为依赖项: + +1. 引用 `Depends` 。 +2. 编写依赖类。类的 `__init__` 函数可以接收 `bot`, `event`, `state` 等参数,在这里我们接受了 `event`,并以 `onebot` 的 `MessageEvent` 作为类型标注。 +3. 在事件处理函数中声明依赖项。当用类作为依赖项时,它会是一个对应的实例,在这里 `x` 就是 `DependClass` 实例。 + +### 另一种依赖项声明方式 + +当使用类作为依赖项时,`Depends` 的参数可以为空,`NoneBot2` 会根据参数的类型标注进行推断并进行依赖注入。 + +```python +@test.handle() +async def _(x: DependClass = Depends()): # 在事件处理函数里声明依赖项 + print(x.uid, x.nickname) +``` + +## 生成器作为依赖 + +:::warning +`yield` 语句只能写一次,否则会引发异常。 +如果对此有疑问并想探究原因,可以看 [contextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager) 和 [asynccontextmanager](https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager) 文档,实际上,`Nonebot2` 的内部就使用了这两个装饰器。 +::: + +:::tips +生成器是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。 +::: + +与 `FastAPI` 一样,`NoneBot2` 的依赖注入支持依赖项在事件处理结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。 + +要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 + +我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO: + +```python {3,7-10,13} +import httpx +from nonebot import on_command +from nonebot.params import Depends # 1.引用 Depends + +test = on_command("123") + +async def get_client(): # 2.编写异步生成器函数 + async with httpx.AsyncClient() as client: + yield client + print("调用结束") + +@test.handle() +async def _(x: httpx.AsyncClient = Depends(get_client)): # 3.在事件处理函数里声明依赖项 + resp = await x.get("https://v2.nonebot.dev") + # do something +``` + +我们用 `yield` 代码段作为生成器函数的“返回”,在事件处理函数里用返回出来的 `client` 做自己需要的工作。在 `NoneBot2` 结束事件处理时,会执行 `yield` 之后的代码。 + +## 创造可调用对象作为依赖 + +:::tips +魔法方法 `__call__` 是 `Python` 高级特性,如果你对此处文档感到疑惑那说明暂时你还用不上这个功能。 +::: + +在 `Python` 的里,类的 `__call__` 方法会让类的实例变成**可调用对象**,我们可以利用这个魔法方法做一个简单的尝试: + +```python{3,9-14,16,19} +from typing import Type +from nonebot.log import logger +from nonebot.params import Depends # 1.引用 Depends +from nonebot import on_command +from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent + +test = on_command("123") + +class EventChecker: # 2.编写需要的类 + def __init__(self, EventClass: Type[MessageEvent]): + self.event_class = EventClass + + def __call__(self, event: MessageEvent) -> bool: + return isinstance(event, self.event_class) + +checker = EventChecker(GroupMessageEvent) # 3.将类实例化 + +@test.handle() +async def _(x: bool = Depends(checker)): # 4.在事件处理函数里声明依赖项 + if x: + print("这是群聊消息") + else: + print("这不是群聊消息") +``` + +这是判断 `onebot` 的消息事件是不是群聊消息事件的一个例子,我们可以用四步来说明这个例子: + +1. 引用 `Depends` 。 +2. 编写需要的类。类的 `__init__` 函数接收参数 `EventClass`,它将接收事件类本身。类的 `__call__` 函数将接受消息事件对象,并返回一个 `bool` 类型的判定结果。 +3. 将类实例化。我们传入群聊消息事件作为参数实例化 `checker` 。 +4. 在事件处理函数里声明依赖项。`NoneBot2` 将会调用 `checker` 的 `__call__` 方法,返回给参数 `x` 相应的判断结果。 diff --git a/website/versioned_docs/version-2.0.0rc2/advanced/di/overload.md b/website/versioned_docs/version-2.0.0rc2/advanced/di/overload.md new file mode 100644 index 00000000..47dd7299 --- /dev/null +++ b/website/versioned_docs/version-2.0.0rc2/advanced/di/overload.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 2 +description: 重载事件处理函数 + +options: + menu: + weight: 61 + category: advanced +--- + +# 事件处理函数重载 + +当我们在编写 NoneBot2 应用时,常常会遇到这样一个问题:该怎么让同一类型的不同事件执行不同的响应逻辑?又或者如何让不同的 `bot` 针对同一类型的事件作出不同响应? + +针对这个问题, NoneBot2 提供一个便捷而高效的解决方案:事件处理函数重载机制。简单地说,`handler`(事件处理函数)会根据其参数的 `type hints`([PEP484 类型标注](https://www.python.org/dev/peps/pep-0484/))来对相对应的 `bot` 和 `event` 进行响应,并且会忽略不符合其参数类型标注的情况。 + + + +:::tip 提示 +如果想了解更多关于 `inspect` 标准库的信息,可以查看[官方文档](https://docs.python.org/zh-cn/3.9/library/inspect.html)。 +::: + +下面,我们会以 OneBot 适配器中的群聊消息事件和私聊消息事件为例,对该机制的应用进行简单的介绍。 + +## 一个例子 + +首先,我们需要导入需要的方法、类型。 + +```python +from nonebot import on_command +from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, PrivateMessageEvent +``` + +之后,我们可以注册一个 `Matcher` 来响应消息事件。 + +```python +matcher = on_command("test_overload") +``` + +最后,我们编写不同的 `handler` 并编写不同的类型标注来实现事件处理函数重载: + +```python +@matcher.handle() +async def _(bot: Bot, event: GroupMessageEvent): + await matcher.send("群聊消息事件响应成功!") + + +@matcher.handle() +async def _(bot: Bot, event: PrivateMessageEvent): + await matcher.send("私聊消息事件响应成功!") +``` + +此时,我们可以在群聊或私聊中对我们的机器人发送 `test_overload`,它会在不同的场景做出不同的应答。 + +这样一个简单的事件处理函数重载就完成了。 + +## 进阶 + +事件处理函数重载机制同样支持被 `matcher.got` 等装饰器装饰的函数。例如: + +```python +@matcher.got("key1", prompt="群事件提问") +async def _(bot: Bot, event: GroupMessageEvent): + await matcher.send("群聊消息事件响应成功!") + + +@matcher.got("key2", prompt="私聊事件提问") +async def _(bot: Bot, event: PrivateMessageEvent): + await matcher.send("私聊消息事件响应成功!") +``` + +只有触发事件符合的函数才会触发装饰器。 + +:::warning 注意 +bot 和 event 参数具有最高的检查优先级,因此,如果参数类型不符合,所有的依赖项 `Depends` 等都不会被执行。 +::: diff --git a/website/versioned_docs/version-2.0.0rc2/advanced/images/Handle-Event.png b/website/versioned_docs/version-2.0.0rc2/advanced/images/Handle-Event.png new file mode 100644 index 0000000000000000000000000000000000000000..ab63c4893e381d27e6880a2c8e8777dd62412c4f GIT binary patch literal 384852 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