diff --git a/website/src/changelog/changelog.md b/website/src/changelog/changelog.md index e6d240d1..baaa06e0 100644 --- a/website/src/changelog/changelog.md +++ b/website/src/changelog/changelog.md @@ -5,7 +5,7 @@ toc_max_heading_level: 2 # 更新日志 -## 最近更新 +## v2.4.0 ### 🚀 新功能 diff --git a/website/versioned_docs/version-2.4.0/README.md b/website/versioned_docs/version-2.4.0/README.md new file mode 100644 index 00000000..ac49ef60 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/README.md @@ -0,0 +1,49 @@ +--- +sidebar_position: 0 +id: index +slug: / +--- + +# 概览 + +NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架(下称 NoneBot),它基于 Python 的类型注解和异步优先特性(兼容同步),能够为你的需求实现提供便捷灵活的支持。同时,NoneBot 拥有大量的开发者为其开发插件,用户无需编写任何代码,仅需完成环境配置及插件安装,就可以正常使用 NoneBot。 + +需要注意的是,NoneBot 仅支持 **Python 3.9 以上版本** + +## 特色 + +### 异步优先 + +NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。 + +### 完整的类型注解 + +NoneBot 参考 [PEP 484](https://www.python.org/dev/peps/pep-0484/) 等 PEP 完整实现了类型注解,通过 Pyright(Pylance) 检查。配合编辑器的类型推导功能,能将绝大多数的 Bug 杜绝在编辑器中([编辑器支持](./editor-support))。 + +### 开箱即用 + +NoneBot 提供了使用便捷、具有交互式功能的命令行工具--`nb-cli`,使得用户初次接触 NoneBot 时更容易上手。使用方法请阅读本文档[指南](./quick-start.mdx)以及 [CLI 文档](https://cli.nonebot.dev/)。 + +### 插件系统 + +插件系统是 NoneBot 的核心,通过它可以实现机器人的模块化以及功能扩展,便于维护和管理。 + +### 依赖注入系统 + +NoneBot 采用了一套自行定义的依赖注入系统,可以让事件的处理过程更加的简洁、清晰,增加代码的可读性,减少代码冗余。 + +#### 什么是依赖注入 + +[**『依赖注入』**](https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)意思是,在编程中,有一种方法可以让你的代码声明它工作和使用所需要的东西,即**『依赖』**。 + +系统(在这里是指 NoneBot)将负责做任何需要的事情,为你的代码提供这些必要依赖(即**『注入』**依赖性) + +这在你有以下情形的需求时非常有用: + +- 这部分代码拥有共享的逻辑(同样的代码逻辑多次重复) +- 共享数据库以及网络请求连接会话 + - 比如 `httpx.AsyncClient`、`aiohttp.ClientSession` 和 `sqlalchemy.Session` +- 机器人用户权限检查以及认证 +- 还有更多... + +它在完成上述工作的同时,还能尽量减少代码的耦合和重复 diff --git a/website/versioned_docs/version-2.4.0/advanced/adapter.md b/website/versioned_docs/version-2.4.0/advanced/adapter.md new file mode 100644 index 00000000..61f52a69 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/adapter.md @@ -0,0 +1,161 @@ +--- +sidebar_position: 1 +description: 注册适配器与指定平台交互 + +options: + menu: + - category: advanced + weight: 20 +--- + +# 使用适配器 + +适配器 (Adapter) 是机器人与平台交互的核心桥梁,它负责在驱动器和机器人插件之间转换与传递消息。 + +## 适配器功能与组成 + +适配器通常有两种功能,分别是**接收事件**和**调用平台接口**。其中,接收事件是指将驱动器收到的事件消息转换为 NoneBot 定义的事件模型,然后交由机器人插件处理;调用平台接口是指将机器人插件调用平台接口的数据转换为平台指定的格式,然后交由驱动器发送,并接收接口返回数据。 + +为了实现这两种功能,适配器通常由四个部分组成: + +- **Adapter**:负责转换事件和调用接口,正确创建 Bot 对象并注册到 NoneBot 中。 +- **Bot**:负责存储平台机器人相关信息,并提供回复事件的方法。 +- **Event**:负责定义事件内容,以及事件主体对象。 +- **Message**:负责正确序列化消息,以便机器人插件处理。 + +## 注册适配器 + +在使用适配器之前,我们需要先将适配器注册到驱动器中,这样适配器就可以通过驱动器接收事件和调用接口了。我们以 Console 适配器为例,来看看如何注册适配器: + +```python {2,5} title=bot.py +import nonebot +from nonebot.adapters.console import Adapter + +driver = nonebot.get_driver() +driver.register_adapter(Adapter) +``` + +我们首先需要从适配器模块中导入所需要的适配器类,然后通过驱动器的 `register_adapter` 方法将适配器注册到驱动器中即可。如果我们需要多平台支持,可以多次调用 `register_adapter` 方法来注册多个适配器。 + +## 获取已注册的适配器 + +NoneBot 提供了 `get_adapter` 方法来获取已注册的适配器,我们可以通过适配器的名称或类型来获取指定的适配器实例: + +```python +import nonebot +from nonebot.adapters.console import Adapter + +adapters = nonebot.get_adapters() +console_adapter = nonebot.get_adapter(Adapter) +console_adapter = nonebot.get_adapter(Adapter.get_name()) +``` + +## 获取 Bot 对象 + +当前所有适配器已连接的 Bot 对象可以通过 `get_bots` 方法获取,这是一个以机器人 ID 为键的字典: + +```python +import nonebot + +bots = nonebot.get_bots() +``` + +我们也可以通过 `get_bot` 方法获取指定 ID 的 Bot 对象。如果省略 ID 参数,将会返回所有 Bot 中的第一个: + +```python +import nonebot + +bot = nonebot.get_bot("bot_id") +``` + +如果需要获取指定适配器连接的 Bot 对象,我们可以通过适配器的 `bots` 属性获取,这也是一个以机器人 ID 为键的字典: + +```python +import nonebot +from nonebot.adapters.console import Adapter + +console_adapter = nonebot.get_adapter(Adapter) +bots = console_adapter.bots +``` + +Bot 对象都具有一个 `self_id` 属性,它是机器人的唯一 ID,由适配器填写,通常为机器人的帐号 ID 或者 APP ID。 + +## 获取事件通用信息 + +适配器的所有事件模型均继承自 `Event` 基类,在[事件类型与重载](../appendices/overload.md)一节中,我们也提到了如何使用基类抽象方法来获取事件通用信息。基类能提供如下信息: + +### 事件类型 + +事件类型通常为 `meta_event`、`message`、`notice`、`request`。 + +```python +type: str = event.get_type() +``` + +### 事件名称 + +事件名称由适配器定义,通常用于日志记录。 + +```python +name: str = event.get_event_name() +``` + +### 事件描述 + +事件描述由适配器定义,通常用于日志记录。 + +```python +description: str = event.get_event_description() +``` + +### 事件日志字符串 + +事件日志字符串由事件名称和事件描述组成,用于日志记录。 + +```python +log: str = event.get_log_string() +``` + +### 事件主体 ID + +事件主体 ID 通常为机器人用户 ID。 + +```python +user_id: str = event.get_user_id() +``` + +### 事件会话 ID + +事件会话 ID 通常为机器人用户 ID 与群聊/频道 ID 组合而成。 + +```python +session_id: str = event.get_session_id() +``` + +### 事件消息 + +如果事件包含消息,则可以通过该方法获取,否则会产生异常。 + +```python +message: Message = event.get_message() +``` + +### 事件纯文本消息 + +通常为事件消息的纯文本内容,如果事件不包含消息,则会产生异常。 + +```python +text: str = event.get_plaintext() +``` + +### 事件是否与机器人有关 + +由适配器实现的判断,通常将事件目标主体为机器人、消息中包含“@机器人”或以“机器人的昵称”开始视为与机器人有关。 + +```python +is_tome: bool = event.is_tome() +``` + +## 更多 + +官方支持的适配器和社区贡献的适配器均可在[商店](/store/adapters)中查看。如果你想要开发自己的适配器,可以参考[开发文档](../developer/adapter-writing.md)。欢迎通过商店发布你的适配器。 diff --git a/website/versioned_docs/version-2.4.0/advanced/dependency.mdx b/website/versioned_docs/version-2.4.0/advanced/dependency.mdx new file mode 100644 index 00000000..3efc35cd --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/dependency.mdx @@ -0,0 +1,1320 @@ +--- +sidebar_position: 6 +description: 通过依赖注入获取上下文信息 + +options: + menu: + - category: advanced + weight: 70 +--- + +# 依赖注入 + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前的事件、机器人等信息。在 NoneBot 中,这些信息通过依赖注入的方式提供给事件处理函数,可以让代码更加整洁可读、提升复用能力。 + +在了解如何使用依赖注入获取上下文信息之前,我们需要先了解两个概念: + +- `Dependent`:使用依赖注入的函数或其他任意可调用对象。如:事件处理函数、自定义的依赖函数等。 +- `Dependency`:依赖注入的对象。如:当前事件、机器人等。 + +在之前的文档中,我们已经多次使用了依赖注入来获取事件信息。通过对函数参数依照一定规则填写类型注解,即可获得想要的上下文信息。任何一个事件处理函数在添加到事件处理流程时,都会根据一定规则提前将其解析成一个 `Dependent` 对象,方便运行时进行注入。如果遇到无法解析的参数,将会抛出 `ValueError("Unknown parameter")` 的异常。整个依赖注入系统可以分为两部分: + +- 参数解析 + - 依据一定规则解析函数参数,识别 `Dependency` 依赖。 + - 生成 `Dependent` 对象。 +- 执行 + - 根据已经解析的 `Dependency` 依赖,执行调用。 + - 将所有 `Dependency` 的返回值根据参数名传入并调用 `Dependent` 。 + +:::danger 警告 +在依赖注入中,类型注解是非常重要的,因为它不仅可以决定依赖注入的对象,还可以触发[重载机制](../appendices/overload.md#重载)。如果类型注解与实际获得数据类型不一致,将会跳过当前 `Dependent` 对象(即事件处理函数)。 +::: + +:::tip 提示 +如果对于依赖注入的解析流程有疑问,可以调整[日志等级配置项](../appendices/config.mdx#log-level)为 `TRACE`,查看依赖解析日志。 +::: + +## 同步支持 + +对于依赖注入系统中的 `Dependent` 或者 `Dependency` 对象,均支持同步类型的函数或可调用对象。例如: + +```python {6,10} +from nonebot import on_command +from nonebot.params import Depends + +matcher = on_command("foo") + +def dependency() -> str: + return "something" + +@matcher.handle() +def _(result: str = Depends(dependency)): + ... +``` + +## 非依赖参数 + +在依赖注入解析中,任何无法解析的参数如果带有默认值,将会被视为非依赖参数。这些参数在依赖运行时将不会被注入而使用函数默认值。例如: + +```python +async def _(foo: str = "bar"): ... +``` + +## 类型依赖注入 + +这一类的依赖注入仅需要在函数参数中添加对应的类型注解即可。 + +### Bot + +获取当前事件的 Bot 对象。 + +通过标注参数为 `Bot` 类型,或者一系列 `Bot` 类型,即可获取到当前事件的 Bot 对象。为兼容性考虑,如果参数名为 `bot` 且无类型注解,也会视为 Bot 依赖注入。 + +Bot 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 + + + + +```python +from nonebot.adapters import Bot +from nonebot.adapters.console import Bot as ConsoleBot +from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot + +async def _(foo: Bot): ... +async def _(foo: ConsoleBot | OneBotV11Bot): ... +async def _(bot): ... # 兼容性处理 +``` + + + + +```python +from typing import Union + +from nonebot.adapters import Bot +from nonebot.adapters.console import Bot as ConsoleBot +from nonebot.adapters.onebot.v11 import Bot as OneBotV11Bot + +async def _(foo: Bot): ... +async def _(foo: Union[ConsoleBot, OneBotV11Bot]): ... +async def _(bot): ... # 兼容性处理 +``` + + + + +### Event + +获取当前事件。 + +通过标注参数为 `Event` 类型,或者一系列 `Event` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `event` 且无类型注解,也会视为 Event 依赖注入。 + +Event 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 + + + + +```python +from nonebot.adapters import Event +from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent + +async def _(foo: Event): ... +async def _(foo: PrivateMessageEvent | GroupMessageEvent): ... +async def _(event): ... # 兼容性处理 +``` + + + + +```python +from typing import Union + +from nonebot.adapters import Event +from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent + +async def _(foo: Event): ... +async def _(foo: Union[PrivateMessageEvent, GroupMessageEvent]): ... +async def _(event): ... # 兼容性处理 +``` + + + + +### State + +获取当前[会话状态](../appendices/session-state.md)。 + +通过标注参数为 `T_State` 类型,即可获取到当前会话状态。为兼容性考虑,如果参数名为 `state` 且无类型注解,也会视为 State 依赖注入。 + +```python +from nonebot.typing import T_State + +async def _(foo: T_State): ... +``` + +### Matcher + +获取当前事件响应器实例。常用于使用[事件响应器操作](../appendices/session-control.mdx)。 + +通过标注参数为 `Matcher` 类型,或者一系列 `Matcher` 类型,即可获取到当前事件。为兼容性考虑,如果参数名为 `matcher` 且无类型注解,也会视为 Matcher 依赖注入。 + +Matcher 依赖注入支持重载(即:可以标注参数为子类型)且具有[重载优先检查权](../appendices/overload.md#重载)。 + +```python +from nonebot.matcher import Matcher + +async def _(foo: Matcher): ... +async def _(matcher): ... # 兼容性处理 +``` + +### Exception + +获取事件响应器运行中抛出的异常。该依赖注入目前仅在事件响应器运行后处理 Hook 中可用。 + +通过标注参数为异常类型,或者一系列异常类型,即可获取到事件响应器运行中抛出的异常。 + + + + +```python {5,8} +from nonebot.message import run_postprocessor +from nonebot.exception import ActionFailed, NetworkError + +@run_postprocessor +async def _(e: Exception): ... + +@run_postprocessor +async def _(e: ActionFailed | NetworkError): ... +``` + + + + +```python {6,9} +from typing import Union +from nonebot.message import run_postprocessor +from nonebot.exception import ActionFailed, NetworkError + +@run_postprocessor +async def _(e: Exception): ... + +@run_postprocessor +async def _(e: Union[ActionFailed, NetworkError]): ... +``` + + + + +## 子依赖 + +在依赖注入系统中,我们可以定义一个子依赖,来执行自定义的操作,提高代码复用性以及处理性能。 + +### 定义子依赖 + +子依赖使用 `Depends` 标记进行定义,其参数即依赖的函数或可调用对象,同样会被解析为 `Dependent` 对象,将会在依赖注入期间执行。我们来看一个例子: + + + + +```python {5,15} +from typing import Annotated + +from nonebot import on_command +from nonebot.adapters import Event +from nonebot.params import Depends + +test = on_command("test") + +async def check(event: Event) -> Event: + if event.get_user_id() in BLACKLIST: + await test.finish() + return event + +@test.handle() +async def _(event: Annotated[Event, Depends(check)]): + ... +``` + + + + +```python {3,13} +from nonebot import on_command +from nonebot.adapters import Event +from nonebot.params import Depends + +test = on_command("test") + +async def check(event: Event) -> Event: + if event.get_user_id() in BLACKLIST: + await test.finish() + return event + +@test.handle() +async def _(event: Event = Depends(check)): + ... +``` + + + + +在上面的代码中,我们使用 `Depends` 标记定义了一个子依赖 `check`。它判断事件主体用户是否在黑名单中,如果在,则直接结束事件处理流程。如果不在,则返回事件对象,以便事件处理函数可以继续执行。 + +通过将 `Depends` 包裹的子依赖作为参数的默认值,我们就可以在执行事件处理函数之前执行子依赖,并将其返回值作为参数传入事件处理函数。子依赖和普通的事件处理函数并没有区别,同样可以使用依赖注入,并且可以返回任何类型的值。但需要注意的是,如果事件处理函数参数的类型注解与子依赖返回值的类型**不一致**,将会触发[重载](../appendices/overload.md)而跳过当前事件处理函数。 + +特别的,我们可以为 `Dependent` 对象定义一系列前置子依赖,它们会在参数执行前被顺序执行,且返回值将会被忽略,例如: + +```python {11} +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`。此时在事件处理流程中,多次使用同一个子依赖时,将会使用缓存中的结果而不会重复执行。这在很多情景中非常有用,例如: + + + + +```python {7} +import random +from typing import Annotated + +async def random_result() -> int: + return random.randint(1, 100) + +async def _(x: Annotated[int, Depends(random_result)]): + print(x) +``` + + + + +```python {6} +import random + +async def random_result() -> int: + return random.randint(1, 100) + +async def _(x: int = Depends(random_result)): + print(x) +``` + + + + +此时,在同一事件处理流程中,这个随机函数的返回值将会保持一致。如果我们希望每次都重新执行子依赖,可以将 `use_cache` 设置为 `False`。 + + + + +```python {7} +import random +from typing import Annotated + +async def random_result() -> int: + return random.randint(1, 100) + +async def _(x: Annotated[int, Depends(random_result, use_cache=False)]): + print(x) +``` + + + + +```python {6} +import random + +async def random_result() -> int: + return random.randint(1, 100) + +async def _(x: int = Depends(random_result, use_cache=False)): + print(x) +``` + + + + +:::tip 提示 +缓存的生命周期与当前接收到的事件相同。接收到事件后,子依赖在首次执行时缓存,在该事件处理完成后,缓存就会被清除。 +::: + +### 类型转换与校验 + +在依赖注入系统中,我们可以对子依赖的返回值进行自动类型转换与校验。这个功能由 Pydantic 支持,因此我们通过参数类型注解自动使用 Pydantic 支持的类型转换。例如: + + + + +```python {6,9} +from typing import Annotated + +from nonebot.params import Depends +from nonebot.adapters import Event + +def get_user_id(event: Event) -> str: + return event.get_user_id() + +async def _(user_id: Annotated[int, Depends(get_user_id, validate=True)]): + print(user_id) +``` + + + + +```python {4,7} +from nonebot.params import Depends +from nonebot.adapters import Event + +def get_user_id(event: Event) -> str: + return event.get_user_id() + +async def _(user_id: int = Depends(get_user_id, validate=True)): + print(user_id) +``` + + + + +在进行类型自动转换的同时,Pydantic 还支持对数据进行更多的限制,如:大于、小于、长度等。使用方法如下: + + + + +```python {7,10} +from typing import Annotated + +from pydantic import Field +from nonebot.params import Depends +from nonebot.adapters import Event + +def get_user_id(event: Event) -> str: + return event.get_user_id() + +async def _(user_id: Annotated[int, Depends(get_user_id, validate=Field(gt=100))]): + print(user_id) +``` + + + + +```python {5,8} +from pydantic import Field +from nonebot.params import Depends +from nonebot.adapters import Event + +def get_user_id(event: Event) -> str: + return event.get_user_id() + +async def _(user_id: int = Depends(get_user_id, validate=Field(gt=100))): + print(user_id) +``` + + + + +### 类作为依赖 + +在前面的事例中,我们使用了函数作为子依赖。实际上,我们还可以使用类作为依赖。当我们在实例化一个类的时候,其实我们就在调用它,类本身也是一个可调用对象。例如: + + + + +```python {16} +from typing import Annotated +from dataclasses import dataclass + +from nonebot.params import Depends +from nonebot.adapters import Event +from nonebot.typing import T_State + +def get_context(state: T_State) -> dict: + return state.setdefault("context", {}) + +@dataclass +class ClassDependency: + event: Event + context: dict = Depends(get_context) + +async def _(data: Annotated[ClassDependency, Depends(ClassDependency)]): + print(data.event, data.context) +``` + + + + +```python {15} +from dataclasses import dataclass + +from nonebot.params import Depends +from nonebot.adapters import Event +from nonebot.typing import T_State + +def get_context(state: T_State) -> dict: + return state.setdefault("context", {}) + +@dataclass +class ClassDependency: + event: Event + context: dict = Depends(get_context) + +async def _(data: ClassDependency = Depends(ClassDependency)): + print(data.event, data.context) +``` + + + + +可以看到,我们使用 `dataclass` 定义了一个类。由于这个类的 `__init__` 方法可以被依赖注入系统解析,因此,我们可以将其作为子依赖进行声明。特别地,对于类依赖,`Depends` 的参数可以为空,NoneBot 将会使用参数的类型注解进行解析与推断: + + + + +```python +from typing import Annotated + +async def _(data: Annotated[ClassDependency, Depends()]): + print(data.event, data.context) +``` + + + + +```python +async def _(data: ClassDependency = Depends()): + print(data.event, data.context) +``` + + + + +### 生成器作为依赖 + +NoneBot 的依赖注入支持依赖项在事件处理流程结束后进行一些额外的工作,比如数据库 session 或者网络 IO 的关闭,互斥锁的解锁等等。同时,由于[依赖缓存](#依赖缓存)的存在,我们可以通过这种方式来实现共享一个 session 等功能。 + +要实现上述功能,我们可以用生成器函数作为依赖项,我们用 `yield` 关键字取代 `return` 关键字,并在 `yield` 之后进行额外的工作。 + +我们可以看下述代码段, 使用 `httpx.AsyncClient` 异步网络 IO,并在事件处理流程中共用一个 client: + + + + +```python {15} +from typing import Annotated +from collections.abc import AsyncGenerator + +import httpx +from nonebot.params import Depends + +async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: + try: + async with httpx.AsyncClient() as client: + yield client + finally: + # 在这里进行额外的工作 + + +@test.handle() +async def _(x: Annotated[httpx.AsyncClient, Depends(get_client)]): + resp = await x.get("https://nonebot.dev") +``` + + + + +```python {15} +from collections.abc import AsyncGenerator + +import httpx +from nonebot.params import Depends + +async def get_client() -> AsyncGenerator[httpx.AsyncClient, None]: + try: + async with httpx.AsyncClient() as client: + yield client + finally: + # 在这里进行额外的工作 + + +@test.handle() +async def _(x: httpx.AsyncClient = Depends(get_client)): + resp = await x.get("https://nonebot.dev") +``` + + + + +:::caution 注意 +生成器作为依赖时,其中只能进行一次 `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) 文档。事实上,NoneBot 内部就使用了这两个装饰器。 +::: + +### 可调用对象作为依赖 + +在 Python 里,为类定义 `__call__` 方法就可以使得这个类的实例成为一个可调用对象。因此,我们也可以将定义了 `__call__` 方法的类的实例作为依赖。事实上,NoneBot 的[内置响应规则](./matcher.md#内置响应规则)就广泛使用了这种方式,以 `is_type` 规则为例: + +```python +from nonebot.adapters import Event + +class IsTypeRule: + def __init__(self, *types: type[Event]): + self.types = types + + async def __call__(self, event: Event) -> bool: + return isinstance(event, self.types) +``` + +我们在使用 `is_type` 时,即实例化了 `IsTypeRule` 类,然后将实例作为响应规则依赖项传入。 + +## 其他依赖注入 + +这一类的依赖注入通常基于子依赖编写,为我们开发者提供更方便的途径获取上下文信息。 + +### EventType + +获取当前事件的类型。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import EventType + +async def _(foo: Annotated[str, EventType()]): ... +``` + + + + +```python {3} +from nonebot.params import EventType + +async def _(foo: str = EventType()): ... +``` + + + + +### EventMessage + +获取当前事件的消息。 + + + + +```python {5} +from typing import Annotated +from nonebot.adapters import Message +from nonebot.params import EventMessage + +async def _(foo: Annotated[Message, EventMessage()]): ... +``` + + + + +```python {4} +from nonebot.adapters import Message +from nonebot.params import EventMessage + +async def _(foo: Message = EventMessage()): ... +``` + + + + +### EventPlainText + +获取当前事件的消息纯文本部分。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import EventPlainText + +async def _(foo: Annotated[str, EventPlainText()]): ... +``` + + + + +```python {3} +from nonebot.params import EventPlainText + +async def _(foo: str = EventPlainText()): ... +``` + + + + +### EventToMe + +获取当前事件是否与机器人相关。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import EventToMe + +async def _(foo: Annotated[bool, EventToMe()]): ... +``` + + + + +```python {3} +from nonebot.params import EventToMe + +async def _(foo: bool = EventToMe()): ... +``` + + + + +### Command + +获取当前命令型消息的元组形式命令名。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import Command + +async def _(foo: Annotated[tuple[str, ...], Command()]): ... +``` + + + + +```python {4} +from nonebot.params import Command + +async def _(foo: tuple[str, ...] = Command()): ... +``` + + + + +:::tip 提示 +命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 +::: + +### RawCommand + +获取当前命令型消息的文本形式命令名。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import RawCommand + +async def _(foo: Annotated[str, RawCommand()]): ... +``` + + + + +```python {3} +from nonebot.params import RawCommand + +async def _(foo: str = RawCommand()): ... +``` + + + + +:::tip 提示 +命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 +::: + +### CommandArg + +获取命令型消息命令后跟随的参数。 + + + + +```python {5} +from typing import Annotated +from nonebot.adapters import Message +from nonebot.params import CommandArg + +async def _(foo: Annotated[Message, CommandArg()]): ... +``` + + + + +```python {4} +from nonebot.adapters import Message +from nonebot.params import CommandArg + +async def _(foo: Message = CommandArg()): ... +``` + + + + +:::tip 提示 +命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 +::: + +### CommandStart + +获取命令型消息命令前缀。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import CommandStart + +async def _(foo: Annotated[str, CommandStart()]): ... +``` + + + + +```python {3} +from nonebot.params import CommandStart + +async def _(foo: str = CommandStart()): ... +``` + + + + +:::tip 提示 +命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 +::: + +### CommandWhitespace + +获取命令型消息命令与参数间空白符。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import CommandWhitespace + +async def _(foo: Annotated[str, CommandWhitespace()]): ... +``` + + + + +```python {3} +from nonebot.params import CommandWhitespace + +async def _(foo: str = CommandWhitespace()): ... +``` + + + + +:::tip 提示 +命令详情只能在**触发命令型事件响应器时**获取。如果在事件处理后续流程中获取,则会获取到不同的值。 +::: + +### ShellCommandArgv + +获取 shell 命令解析前的参数列表,列表中可能包含文本字符串和富文本消息段(如:图片)。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import ShellCommandArgs + +async def _(foo: Annotated[list[str | MessageSegment], ShellCommandArgv()]): ... +``` + +```python {4} +from nonebot.params import ShellCommandArgs + +async def _(foo: list[str | MessageSegment] = ShellCommandArgv()): ... +``` + + + + +```python {4} +from typing import Union, Annotated +from nonebot.params import ShellCommandArgs + +async def _(foo: Annotated[list[Union[str, MessageSegment]], ShellCommandArgv()]): ... +``` + +```python {4} +from typing import Union +from nonebot.params import ShellCommandArgs + +async def _(foo: list[Union[str, MessageSegment]] = ShellCommandArgv()): ... +``` + + + + +### ShellCommandArgs + +获取 shell 命令解析后的参数 Namespace,支持 MessageSegment 富文本(如:图片)。 + +:::tip 提示 +如果参数解析成功,则为 parser 返回的 Namespace;如果参数解析失败,则为 [`ParserExit`](../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。通过重载机制即可处理两种不同的情况。 + +由于 `ArgumentParser` 在解析到 `--help` 参数时也会抛出异常,这种情况下错误码为 `0` 且错误信息即为帮助信息。 +::: + + + + +```python {14,22} +from typing import Annotated + +from nonebot import on_shell_command +from nonebot.exception import ParserExit +from nonebot.params import ShellCommandArgs +from nonebot.rule import Namespace, ArgumentParser + +parser = ArgumentParser("demo") +# parser.add_argument ... +matcher = on_shell_command("cmd", parser=parser) + +# 解析失败 +@matcher.handle() +async def _(foo: Annotated[ParserExit, ShellCommandArgs()]): + if foo.status == 0: + foo.message # help message + else: + foo.message # error message + +# 解析成功 +@matcher.handle() +async def _(foo: Annotated[Namespace, ShellCommandArgs()]): + arg_dict = vars(foo) +``` + + + + +```python {12,20} +from nonebot import on_shell_command +from nonebot.exception import ParserExit +from nonebot.params import ShellCommandArgs +from nonebot.rule import Namespace, ArgumentParser + +parser = ArgumentParser("demo") +# parser.add_argument ... +matcher = on_shell_command("cmd", parser=parser) + +# 解析失败 +@matcher.handle() +async def _(foo: ParserExit = ShellCommandArgs()): + if foo.status == 0: + foo.message # help message + else: + foo.message # error message + +# 解析成功 +@matcher.handle() +async def _(foo: Namespace = ShellCommandArgs()): + arg_dict = vars(foo) +``` + + + + +### RegexMatched + +获取正则匹配结果的对象。 + + + + +```python {5} +from re import Match +from typing import Annotated +from nonebot.params import RegexMatched + +async def _(foo: Annotated[Match[str], RegexMatched()]): ... +``` + + + + +```python {4} +from re import Match +from nonebot.params import RegexMatched + +async def _(foo: Match[str] = RegexMatched()): ... +``` + + + + +### RegexStr + +获取正则匹配结果的文本。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import RegexStr + +async def _(foo: Annotated[str, RegexStr()]): ... +``` + + + + +```python {3} +from nonebot.params import RegexStr + +async def _(foo: str = RegexStr()): ... +``` + + + + +### RegexGroup + +获取正则匹配结果的 group 元组。 + + + + +```python {4} +from typing import Any, Annotated +from nonebot.params import RegexGroup + +async def _(foo: Annotated[tuple[Any, ...], RegexGroup()]): ... +``` + + + + +```python {4} +from typing import Any +from nonebot.params import RegexGroup + +async def _(foo: tuple[Any, ...] = RegexGroup()): ... +``` + + + + +### RegexDict + +获取正则匹配结果的 group 字典。 + + + + +```python {4} +from typing import Any, Annotated +from nonebot.params import RegexDict + +async def _(foo: Annotated[dict[str, Any], RegexDict()]): ... +``` + + + + +```python {4} +from typing import Any +from nonebot.params import RegexDict + +async def _(foo: dict[str, Any] = RegexDict()): ... +``` + + + + +### Startswith + +获取触发响应器的消息前缀字符串。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import Startswith + +async def _(foo: Annotated[str, Startswith()]): ... +``` + + + + +```python {3} +from nonebot.params import Startswith + +async def _(foo: str = Startswith()): ... +``` + + + + +### Endswith + +获取触发响应器的消息后缀字符串。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import Endswith + +async def _(foo: Annotated[str, Endswith()]): ... +``` + + + + +```python {3} +from nonebot.params import Endswith + +async def _(foo: str = Endswith()): ... +``` + + + + +### Fullmatch + +获取触发响应器的消息字符串。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import Fullmatch + +async def _(foo: Annotated[str, Fullmatch()]): ... +``` + + + + +```python {3} +from nonebot.params import Fullmatch + +async def _(foo: str = Fullmatch()): ... +``` + + + + +### Keyword + +获取触发响应器的关键字字符串。 + + + + +```python {4} +from typing import Annotated +from nonebot.params import Keyword + +async def _(foo: Annotated[str, Keyword()]): ... +``` + + + + +```python {3} +from nonebot.params import Keyword + +async def _(foo: str = Keyword()): ... +``` + + + + +### Received + +获取某次 `receive` 接收的事件。 + + + + +```python {7} +from typing import Annotated + +from nonebot.adapters import Event +from nonebot.params import Received + +@matcher.receive("id") +async def _(foo: Annotated[Event, Received("id")]): ... +``` + + + + +```python {5} +from nonebot.adapters import Event +from nonebot.params import Received + +@matcher.receive("id") +async def _(foo: Event = Received("id")): ... +``` + + + + +### LastReceived + +获取最近一次 `receive` 接收的事件。 + + + + +```python {7} +from typing import Annotated + +from nonebot.adapters import Event +from nonebot.params import LastReceived + +@matcher.receive("any") +async def _(foo: Annotated[Event, LastReceived()]): ... +``` + + + + +```python {5} +from nonebot.adapters import Event +from nonebot.params import LastReceived + +@matcher.receive("any") +async def _(foo: Event = LastReceived()): ... +``` + + + + +### Arg + +获取某次 `got` 接收的参数。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 + + + + +```python {7,8} +from typing import Annotated + +from nonebot.params import Arg +from nonebot.adapters import Message + +@matcher.got("key") +async def _(key: Annotated[Message, Arg()]): ... +async def _(foo: Annotated[Message, Arg("key")]): ... +``` + + + + +```python {5,6} +from nonebot.params import Arg +from nonebot.adapters import Message + +@matcher.got("key") +async def _(key: Message = Arg()): ... +async def _(foo: Message = Arg("key")): ... +``` + + + + +### ArgStr + +获取某次 `got` 接收的参数,并转换为字符串。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 + + + + +```python {6,7} +from typing import Annotated + +from nonebot.params import ArgStr + +@matcher.got("key") +async def _(key: Annotated[str, ArgStr()]): ... +async def _(foo: Annotated[str, ArgStr("key")]): ... +``` + + + + +```python {4,5} +from nonebot.params import ArgStr + +@matcher.got("key") +async def _(key: str = ArgStr()): ... +async def _(foo: str = ArgStr("key")): ... +``` + + + + +### ArgPlainText + +获取某次 `got` 接收的参数的纯文本部分。如果 `Arg` 参数留空,则使用函数的参数名作为要获取的参数。 + + + + +```python {6,7} +from typing import Annotated + +from nonebot.params import ArgPlainText + +@matcher.got("key") +async def _(key: Annotated[str, ArgPlainText()]): ... +async def _(foo: Annotated[str, ArgPlainText("key")]): ... +``` + + + + +```python {4,5} +from nonebot.params import ArgPlainText + +@matcher.got("key") +async def _(key: str = ArgPlainText()): ... +async def _(foo: str = ArgPlainText("key")): ... +``` + + + diff --git a/website/versioned_docs/version-2.4.0/advanced/driver.md b/website/versioned_docs/version-2.4.0/advanced/driver.md new file mode 100644 index 00000000..222921de --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/driver.md @@ -0,0 +1,287 @@ +--- +sidebar_position: 0 +description: 选择合适的驱动器运行机器人 + +options: + menu: + - category: advanced + weight: 10 +--- + +# 选择驱动器 + +驱动器 (Driver) 是机器人运行的基石,它是机器人初始化的第一步,主要负责数据收发。 + +:::important 提示 +驱动器的选择通常与机器人所使用的协议适配器相关,如果不知道该选择哪个驱动器,可以先阅读相关协议适配器文档说明。 +::: + +:::tip 提示 +如何**安装**驱动器请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。 +::: + +## 驱动器类型 + +驱动器类型大体上可以分为两种: + +- `Forward`:即客户端型驱动器,多用于使用 HTTP 轮询,连接 WebSocket 服务器等情形。 +- `Reverse`:即服务端型驱动器,多用于使用 WebHook,接收 WebSocket 客户端连接等情形。 + +客户端型驱动器可以分为以下两种: + +1. 异步发送 HTTP 请求,自定义 `HTTP Method`、`URL`、`Header`、`Body`、`Cookie`、`Proxy`、`Timeout` 等。 +2. 异步建立 WebSocket 连接上下文,自定义 `WebSocket URL`、`Header`、`Cookie`、`Proxy`、`Timeout` 等。 + +服务端型驱动器目前有: + +1. ASGI 应用框架,具有以下功能: + - 协议适配器自定义 HTTP 上报地址以及对上报数据处理的回调函数。 + - 协议适配器自定义 WebSocket 连接请求地址以及对 WebSocket 请求处理的回调函数。 + - 用户可以向 ASGI 应用添加任何服务端相关功能,如:[添加自定义路由](./routing.md)。 + +## 配置驱动器 + +驱动器的配置方法已经在[配置](../appendices/config.mdx)章节中简单进行了介绍,这里将详细介绍驱动器配置的格式。 + +NoneBot 中的客户端和服务端型驱动器可以相互配合使用,但服务端型驱动器**仅能选择一个**。所有驱动器模块都会包含一个 `Driver` 子类,即驱动器类,他可以作为驱动器单独运行。同时,客户端驱动器模块中还会提供一个 `Mixin` 子类,用于在与其他驱动器配合使用时加载。因此,驱动器配置格式采用特殊语法:`[:][+[:]]*`。 + +其中,`` 代表**驱动器模块路径**;`` 代表**驱动器类名**,默认为 `Driver`;`` 代表**驱动器混入类名**,默认为 `Mixin`。即,我们需要选择一个主要驱动器,然后在其基础上配合使用其他驱动器的功能。主要驱动器可以为客户端或服务端类型,但混入类驱动器只能为客户端类型。 + +特别的,为了简化内置驱动器模块路径,我们可以使用 `~` 符号作为内置驱动器模块路径的前缀,如 `~fastapi` 代表使用内置驱动器 `fastapi`。NoneBot 内置了多个驱动器适配,但需要安装额外依赖才能使用,具体请参考[安装驱动器](../tutorial/store.mdx#安装驱动器)。常见的驱动器配置如下: + +```dotenv +DRIVER=~fastapi +DRIVER=~aiohttp +DRIVER=~httpx+~websockets +DRIVER=~fastapi+~httpx+~websockets +``` + +## 获取驱动器 + +在 NoneBot 框架初始化完成后,我们就可以通过 `get_driver()` 方法获取全局驱动器实例: + +```python +from nonebot import get_driver + +driver = get_driver() +``` + +## 内置驱动器 + +### None + +**类型:**服务端驱动器 + +NoneBot 内置的空驱动器,不提供任何收发数据功能,可以在不需要外部网络连接时使用。 + +```env +DRIVER=~none +``` + +### FastAPI(默认) + +**类型:**ASGI 服务端驱动器 + +> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +[FastAPI](https://fastapi.tiangolo.com/) 是一个易上手、高性能的异步 Web 框架,具有极佳的编写体验。 FastAPI 可以通过类型注解、依赖注入等方式实现输入参数校验、自动生成 API 文档等功能,也可以挂载其他 ASGI、WSGI 应用。 + +```env +DRIVER=~fastapi +``` + +#### FastAPI 配置项 + +##### `fastapi_openapi_url` + +类型:`str | None` +默认值:`None` +说明:`FastAPI` 提供的 `OpenAPI` JSON 定义地址,如果为 `None`,则不提供 `OpenAPI` JSON 定义。 + +##### `fastapi_docs_url` + +类型:`str | None` +默认值:`None` +说明:`FastAPI` 提供的 `Swagger` 文档地址,如果为 `None`,则不提供 `Swagger` 文档。 + +##### `fastapi_redoc_url` + +类型:`str | None` +默认值:`None` +说明:`FastAPI` 提供的 `ReDoc` 文档地址,如果为 `None`,则不提供 `ReDoc` 文档。 + +##### `fastapi_include_adapter_schema` + +类型:`bool` +默认值:`True` +说明:`FastAPI` 提供的 `OpenAPI` JSON 定义中是否包含适配器路由的 `Schema`。 + +##### `fastapi_reload` + +:::caution 警告 +不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 + +```bash +nb run --reload +``` + +开启该功能后,在 uvicorn 运行时(FastAPI 提供的 ASGI 底层,即 reload 功能的实际来源),asyncio 使用的事件循环会被 uvicorn 从默认的 `ProactorEventLoop` 强制切换到 `SelectorEventLoop`。 + +> 相关信息参考 [uvicorn#529](https://github.com/encode/uvicorn/issues/529),[uvicorn#1070](https://github.com/encode/uvicorn/pull/1070),[uvicorn#1257](https://github.com/encode/uvicorn/pull/1257) + +后者(`SelectorEventLoop`)在 Windows 平台的可使用性不如前者(`ProactorEventLoop`),包括但不限于 + +1. 不支持创建子进程 +2. 最多只支持 512 个套接字 +3. ... + +> 具体信息参考 [Python 文档](https://docs.python.org/zh-cn/3/library/asyncio-platforms.html#windows) + +所以,一些使用了 asyncio 的库因此可能无法正常工作,如: + +1. [playwright](https://playwright.dev/python/docs/library#incompatible-with-selectoreventloop-of-asyncio-on-windows) + +如果在开启该功能后,原本**正常运行**的代码报错,且打印的异常堆栈信息和 asyncio 有关(异常一般为 `NotImplementedError`), +你可能就需要考虑相关库对事件循环的支持,以及是否启用该功能。 +::: + +类型:`bool` +默认值:`False` +说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 + +```python title=bot.py +app = nonebot.get_asgi() +nonebot.run(app="bot:app") +``` + +##### `fastapi_reload_dirs` + +类型:`List[str] | None` +默认值:`None` +说明:重载监控文件夹列表,默认为 uvicorn 默认值 + +##### `fastapi_reload_delay` + +类型:`float | None` +默认值:`None` +说明:重载延迟,默认为 uvicorn 默认值 + +##### `fastapi_reload_includes` + +类型:`List[str] | None` +默认值:`None` +说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +##### `fastapi_reload_excludes` + +类型:`List[str] | None` +默认值:`None` +说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +##### `fastapi_extra` + +类型:`Dist[str, Any]` +默认值:`{}` +说明:传递给 `FastAPI` 的其他参数 + +### Quart + +**类型:**ASGI 服务端驱动器 + +> Quart is an asyncio reimplementation of the popular Flask microframework API. + +[Quart](https://quart.palletsprojects.com/) 是一个类 Flask 的异步版本,拥有与 Flask 非常相似的接口和使用方法。 + +```env +DRIVER=~quart +``` + +#### Quart 配置项 + +##### `quart_reload` + +:::caution 警告 +不推荐开启该配置项,在 Windows 平台上开启该功能有可能会造成预料之外的影响!替代方案:使用 `nb-cli` 命令行工具以及参数 `--reload` 启动 NoneBot。 + +```bash +nb run --reload +``` + +::: + +类型:`bool` +默认值:`False` +说明:是否开启 `uvicorn` 的 `reload` 功能,需要在机器人入口文件提供 ASGI 应用路径。 + +```python title=bot.py +app = nonebot.get_asgi() +nonebot.run(app="bot:app") +``` + +##### `quart_reload_dirs` + +类型:`List[str] | None` +默认值:`None` +说明:重载监控文件夹列表,默认为 uvicorn 默认值 + +##### `quart_reload_delay` + +类型:`float | None` +默认值:`None` +说明:重载延迟,默认为 uvicorn 默认值 + +##### `quart_reload_includes` + +类型:`List[str] | None` +默认值:`None` +说明:要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +##### `quart_reload_excludes` + +类型:`List[str] | None` +默认值:`None` +说明:不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +##### `quart_extra` + +类型:`Dist[str, Any]` +默认值:`{}` +说明:传递给 `Quart` 的其他参数 + +### HTTPX + +**类型:**HTTP 客户端驱动器 + +:::caution 注意 +本驱动器仅支持 HTTP 请求,不支持 WebSocket 连接请求。 +::: + +> [HTTPX](https://www.python-httpx.org/) is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. + +```env +DRIVER=~httpx +``` + +### websockets + +**类型:**WebSocket 客户端驱动器 + +:::caution 注意 +本驱动器仅支持 WebSocket 连接请求,不支持 HTTP 请求。 +::: + +> [websockets](https://websockets.readthedocs.io/) is a library for building WebSocket servers and clients in Python with a focus on correctness, simplicity, robustness, and performance. + +```env +DRIVER=~websockets +``` + +### AIOHTTP + +**类型:**HTTP/WebSocket 客户端驱动器 + +> [AIOHTTP](https://docs.aiohttp.org/): Asynchronous HTTP Client/Server for asyncio and Python. + +```env +DRIVER=~aiohttp +``` diff --git a/website/versioned_docs/version-2.4.0/advanced/matcher-provider.md b/website/versioned_docs/version-2.4.0/advanced/matcher-provider.md new file mode 100644 index 00000000..6af8f003 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/matcher-provider.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 10 +description: 自定义事件响应器存储 + +options: + menu: + - category: advanced + weight: 110 +--- + +# 事件响应器存储 + +事件响应器是 NoneBot 处理事件的核心,它们默认存储在一个字典中。在进入会话状态后,事件响应器将会转为临时响应器,作为最高优先级同样存储于该字典中。因此,事件响应器的存储类似于会话存储,它决定了整个 NoneBot 对事件的处理行为。 + +NoneBot 默认使用 Python 的字典将事件响应器存储于内存中,但是我们也可以自定义事件响应器存储,将事件响应器存储于其他地方,例如 Redis 等。这样我们就可以实现持久化、在多实例间共享会话状态等功能。 + +## 编写存储提供者 + +事件响应器的存储提供者 `MatcherProvider` 抽象类继承自 `MutableMapping[int, list[type[Matcher]]]`,即以优先级为键,以事件响应器列表为值的映射。我们可以方便地进行逐优先级事件传播。 + +编写一个自定义的存储提供者,只需要继承并实现 `MatcherProvider` 抽象类: + +```python +from nonebot.matcher import MatcherProvider + +class CustomProvider(MatcherProvider): + ... +``` + +## 设置存储提供者 + +我们可以通过 `matchers.set_provider` 方法设置存储提供者: + +```python {3} +from nonebot.matcher import matchers + +matchers.set_provider(CustomProvider) + +assert isinstance(matchers.provider, CustomProvider) +``` diff --git a/website/versioned_docs/version-2.4.0/advanced/matcher.md b/website/versioned_docs/version-2.4.0/advanced/matcher.md new file mode 100644 index 00000000..130b5435 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/matcher.md @@ -0,0 +1,342 @@ +--- +sidebar_position: 5 +description: 事件响应器组成与内置响应规则 + +options: + menu: + - category: advanced + weight: 60 +--- + +# 事件响应器进阶 + +在[指南](../tutorial/matcher.md)与[深入](../appendices/rule.md)中,我们已经介绍了事件响应器的基本用法以及响应规则、权限控制等功能。在这一节中,我们将介绍事件响应器的组成,内置的响应规则,与第三方响应规则拓展。 + +:::tip 提示 +事件响应器允许继承,你可以通过直接继承 `Matcher` 类来创建一个新的事件响应器。 +::: + +## 事件响应器组成 + +### 事件响应器类型 + +事件响应器类型 `type` 即是该响应器所要响应的事件类型,只有在接收到的事件类型与该响应器的类型相同时,才会触发该响应器。如果类型为空字符串 `""`,则响应器将会响应所有类型的事件。事件响应器类型的检查在所有其他检查(权限控制、响应规则)之前进行。 + +NoneBot 内置了四种常用事件类型:`meta_event`、`message`、`notice`、`request`,分别对应元事件、消息、通知、请求。通常情况下,协议适配器会将事件合理地分类至这四种类型中。如果有其他类型的事件需要响应,可以自行定义新的类型。 + +### 事件触发权限 + +事件触发权限 `permission` 是一个 `Permission` 对象,这在[权限控制](../appendices/permission.mdx)一节中已经介绍过。事件触发权限会在事件响应器的类型检查通过后进行检查,如果权限检查通过,则执行响应规则检查。 + +### 事件响应规则 + +事件响应规则 `rule` 是一个 `Rule` 对象,这在[响应规则](../appendices/rule.md)一节中已经介绍过。事件响应器的响应规则会在事件响应器的权限检查通过后进行匹配,如果响应规则检查通过,则触发该响应器。 + +### 响应优先级 + +响应优先级 `priority` 是一个正整数,用于指定响应器的优先级。响应器的优先级越小,越先被触发。如果响应器的优先级相同,则按照响应器的注册顺序进行触发。 + +### 阻断 + +阻断 `block` 是一个布尔值,用于指定响应器是否阻断事件的传播。如果阻断为 `True`,则在该响应器被触发后,事件将不会再传播给其他下一优先级的响应器。 + +NoneBot 内置的事件响应器中,所有非 `command` 规则的 `message` 类型的事件响应器都会阻断事件传递,其他则不会。 + +在部分情况中,可以使用 [`stop_propagation`](../appendices/session-control.mdx#stop_propagation) 方法动态阻止事件传播,该方法需要 handler 在参数中获取 matcher 实例后调用方法。 + +### 有效期 + +事件响应器的有效期分为 `temp` 和 `expire_time` 。`temp` 是一个布尔值,用于指定响应器是否为临时响应器。如果为 `True`,则该响应器在被触发后会被自动销毁。`expire_time` 是一个 `datetime` 对象,用于指定响应器的过期时间。如果 `expire_time` 不为 `None`,则在该时间点后,该响应器会被自动销毁。 + +### 默认状态 + +事件响应器的默认状态 `default_state` 是一个 `dict` 对象,用于指定响应器的默认状态。在响应器被触发时,响应器将会初始化默认状态然后开始执行事件处理流程。 + +## 基本辅助函数 + +NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数: + +- `on`:创建任何类型的事件响应器。 +- `on_metaevent`:创建元事件响应器。 +- `on_message`:创建消息事件响应器。 +- `on_request`:创建请求事件响应器。 +- `on_notice`:创建通知事件响应器。 + +除了 `on` 函数具有一个 `type` 参数外,其余参数均相同: + +- `rule`:响应规则,可以是 `Rule` 对象或者 `RuleChecker` 函数。 +- `permission`:事件触发权限,可以是 `Permission` 对象或者 `PermissionChecker` 函数。 +- `handlers`:事件处理函数列表。 +- `temp`:是否为临时响应器。 +- `expire_time`:响应器的过期时间。 +- `priority`:响应器的优先级。 +- `block`:是否阻断事件传播。 +- `state`:响应器的默认状态。 + +在消息类型的事件响应器的基础上,NoneBot 还内置了一些常用的响应规则,并结合为辅助函数来方便我们快速创建指定功能的响应器。下面我们逐个介绍。 + +## 内置响应规则 + +:::tip +响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。 +::: + +### `startswith` + +`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 + +例如,我们可以创建一个匹配消息开头为 `!` 或者 `/` 的规则: + +```python +from nonebot.rule import startswith + +rule = startswith(("!", "/"), ignorecase=False) +``` + +也可以直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_startswith + +matcher = on_startswith(("!", "/"), ignorecase=False) +``` + +### `endswith` + +`endswith` 响应规则用于匹配消息纯文本部分的结尾是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 + +例如,我们可以创建一个匹配消息结尾为 `.` 或者 `。` 的规则: + +```python +from nonebot.rule import endswith + +rule = endswith((".", "。"), ignorecase=False) +``` + +也可以直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_endswith + +matcher = on_endswith((".", "。"), ignorecase=False) +``` + +### `fullmatch` + +`fullmatch` 响应规则用于匹配消息纯文本部分是否与指定字符串(或一系列字符串)完全相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。 + +例如,我们可以创建一个匹配消息为 `ping` 或者 `pong` 的规则: + +```python +from nonebot.rule import fullmatch + +rule = fullmatch(("ping", "pong"), ignorecase=False) +``` + +也可以直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_fullmatch + +matcher = on_fullmatch(("ping", "pong"), ignorecase=False) +``` + +### `keyword` + +`keyword` 响应规则用于匹配消息纯文本部分是否包含指定字符串(或一系列字符串)。 + +例如,我们可以创建一个匹配消息中包含 `hello` 或者 `hi` 的规则: + +```python +from nonebot.rule import keyword + +rule = keyword("hello", "hi") +``` + +也可以直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_keyword + +matcher = on_keyword({"hello", "hi"}) +``` + +### `command` + +`command` 是最常用的响应规则,它用于匹配消息是否为命令。它会根据配置中的 [Command Start 和 Command Separator](../appendices/config.mdx#command-start-和-command-separator) 来判断消息是否为命令。 + +例如,当我们配置了 `Command Start` 为 `/`,`Command Separator` 为 `.` 时: + +```python +from nonebot.rule import command + +# 匹配 "/help" 或者 "/帮助" 开头的消息 +rule = command("help", "帮助") +# 匹配 "/help.cmd" 开头的消息 +rule = command(("help", "cmd")) +``` + +也可以直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_command + +matcher = on_command("help", aliases={"帮助"}) +``` + +此外,`command` 响应规则默认允许消息命令与参数间不加空格,如果需要严格匹配命令与参数间的空白符,可以使用 `command` 函数的 `force_whitespace` 参数。`force_whitespace` 参数可以是 bool 类型或者具体的字符串,默认为 `False`。如果为 `True`,则命令与参数间必须有任意个数的空白符;如果为字符串,则命令与参数间必须有且与给定字符串一致的空白符。 + +```python +rule = command("help", force_whitespace=True) +rule = command("help", force_whitespace=" ") +``` + +命令解析后的结果可以通过 [`Command`](./dependency.mdx#command)、[`RawCommand`](./dependency.mdx#rawcommand)、[`CommandArg`](./dependency.mdx#commandarg)、[`CommandStart`](./dependency.mdx#commandstart)、[`CommandWhitespace`](./dependency.mdx#commandwhitespace) 依赖注入获取。 + +### `shell_command` + +`shell_command` 响应规则用于匹配类 shell 命令形式的消息。它首先与 [`command`](#command) 响应规则一样进行命令匹配,如果匹配成功,则会进行进一步的参数解析。参数解析采用 `argparse` 标准库进行,在此基础上添加了消息序列 `Message` 支持。 + +例如,我们可以创建一个匹配 `/cmd` 命令并且带有 `-v` 选项与默认 `-h` 帮助选项的规则: + +```python +from nonebot.rule import shell_command, ArgumentParser + +parser = ArgumentParser() +parser.add_argument("-v", "--verbose", action="store_true") + +rule = shell_command("cmd", parser=parser) +``` + +更多关于 `argparse` 的使用方法请参考 [argparse 文档](https://docs.python.org/zh-cn/3/library/argparse.html)。我们也可以选择不提供 `parser` 参数,这样 `shell_command` 将不会解析参数,但会提供参数列表 `argv`。 + +直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_shell_command +from nonebot.rule import ArgumentParser + +parser = ArgumentParser() +parser.add_argument("-v", "--verbose", action="store_true") + +matcher = on_shell_command("cmd", parser=parser) +``` + +参数解析后的结果可以通过 [`ShellCommandArgv`](./dependency.mdx#shellcommandargv)、[`ShellCommandArgs`](./dependency.mdx#shellcommandargs) 依赖注入获取。 + +### `regex` + +`regex` 响应规则用于匹配消息是否与指定正则表达式匹配。 + +:::tip 提示 +正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 模式来确保匹配开头。 +::: + +例如,我们可以创建一个匹配消息中包含字母并且忽略大小写的规则: + +```python +from nonebot.rule import regex + +rule = regex(r"[a-z]+", flags=re.IGNORECASE) +``` + +也可以直接使用辅助函数新建一个响应器: + +```python +from nonebot import on_regex + +matcher = on_regex(r"[a-z]+", flags=re.IGNORECASE) +``` + +正则匹配后的结果可以通过 [`RegexStr`](./dependency.mdx#regexstr)、[`RegexGroup`](./dependency.mdx#regexgroup)、[`RegexDict`](./dependency.mdx#regexdict) 依赖注入获取。 + +### `to_me` + +`to_me` 响应规则用于匹配事件是否与机器人相关。 + +例如: + +```python +from nonebot.rule import to_me + +rule = to_me() +``` + +### `is_type` + +`is_type` 响应规则用于匹配事件类型是否为指定类型(或者一系列类型)。 + +例如,我们可以创建一个匹配 OneBot v11 私聊和群聊消息事件的规则: + +```python +from nonebot.rule import is_type +from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent + +rule = is_type(PrivateMessageEvent, GroupMessageEvent) +``` + +## 响应器组 + +为了更方便的管理一系列功能相近的响应器,NoneBot 提供了两种响应器组,它们可以帮助我们进行响应器的统一管理。 + +### `CommandGroup` + +`CommandGroup` 可以用于管理一系列具有相同前置命令的子命令响应器。 + +例如,我们创建 `/cmd`、`/cmd.sub`、`/cmd.help` 三个命令,他们具有相同的优先级: + +```python +from nonebot import CommandGroup + +group = CommandGroup("cmd", priority=10) + +cmd = group.command(tuple()) +sub_cmd = group.command("sub") +help_cmd = group.command("help") +``` + +命令别名 aliases 默认不会添加 `CommandGroup` 设定的前缀,如果需要为 aliases 添加前缀,可以添加 `prefix_aliases=True` 参数: + +```python +from nonebot import CommandGroup + +group = CommandGroup("cmd", prefix_aliases=True) + +cmd = group.command(tuple()) +help_cmd = group.command("help", aliases={"帮助"}) +``` + +这样就能成功匹配 `/cmd`、`/cmd.help`、`/cmd.帮助` 命令。如果未设置,将默认匹配 `/cmd`、`/cmd.help`、`/帮助` 命令。 + +### `MatcherGroup` + +`MatcherGroup` 可以用于管理一系列具有相同属性的响应器。 + +例如,我们创建一个具有相同响应规则的响应器组: + +```python +from nonebot.rule import to_me +from nonebot import MatcherGroup + +group = MatcherGroup(rule=to_me()) + +matcher1 = group.on_message() +matcher2 = group.on_message() +``` + +## 第三方响应规则 + +### Alconna + +[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。 +该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, +是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 + +该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 + +基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 +标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 + +该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 + +详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。 diff --git a/website/versioned_docs/version-2.4.0/advanced/plugin-info.md b/website/versioned_docs/version-2.4.0/advanced/plugin-info.md new file mode 100644 index 00000000..5f45dc7d --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/plugin-info.md @@ -0,0 +1,104 @@ +--- +sidebar_position: 2 +description: 填写与获取插件相关的信息 + +options: + menu: + - category: advanced + weight: 30 +--- + +# 插件信息 + +NoneBot 是一个插件化的框架,可以通过加载插件来扩展功能。同时,我们也可以通过 NoneBot 的插件系统来获取相关信息,例如插件的名称、使用方法,用于收集帮助信息等。下面我们将介绍如何为插件添加元数据,以及如何获取插件信息。 + +## 插件元数据 + +在 NoneBot 中,插件 [`Plugin`](../api/plugin/model.md#Plugin) 对象中存储了插件系统所需要的一系列信息。包括插件的索引名称、插件模块、插件中的事件响应器、插件父子关系等。通常,只有插件开发者才需要关心这些信息,而插件使用者或者机器人用户想要看到的是插件使用方法等帮助信息。因此,我们可以为插件添加插件元数据 `PluginMetadata`,它允许插件开发者为插件添加一些额外的信息。这些信息编写于插件模块的顶层,可以直接通过源码查看,或者通过 NoneBot 插件系统获取收集到的信息,通过其他方式发送给机器人用户等。 + +现在,假设我们有一个插件 `example`, 它的模块结构如下: + +```tree {4-6} title=Project +📦 awesome-bot +├── 📂 awesome_bot +│ └── 📂 plugins +| └── 📂 example +| ├── 📜 __init__.py +| └── 📜 config.py +├── 📜 pyproject.toml +└── 📜 README.md +``` + +我们需要在插件顶层模块 `example/__init__.py` 中添加插件元数据,如下所示: + +```python {1,5-12} title=example/__init__.py +from nonebot.plugin import PluginMetadata + +from .config import Config + +__plugin_meta__ = PluginMetadata( + name="示例插件", + description="这是一个示例插件", + usage="没什么用", + type="application", + config=Config, + extra={}, +) +``` + +我们可以看到,插件元数据 `PluginMetadata` 有三个基本属性:插件名称、插件描述、插件使用方法。除此之外,还有几个可选的属性(具体填写见[发布插件](../developer/plugin-publishing.mdx#填写插件元数据)章节): + +- `type`:插件类别,发布插件必填。当前有效类别有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能); +- `homepage`:插件项目主页,发布插件必填; +- `config`:插件的[配置类](../appendices/config.mdx#插件配置),如无配置类可不填; +- `supported_adapters`:支持的适配器模块名集合,若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写; +- `extra`:一个字典,可以用于存储任意信息。其他插件可以通过约定 `extra` 字典的键名来达成收集某些特殊信息的目的。 + +请注意,这里的**插件名称**是供使用者或机器人用户查看的,与插件索引名称无关。**插件索引名称(插件模块名称)**仅用于 NoneBot 插件系统**内部索引**。 + +## 获取插件信息 + +NoneBot 提供了多种获取插件对象的方法,例如获取当前所有已导入的插件: + +```python +import nonebot + +plugins: set[Plugin] = nonebot.get_loaded_plugins() +``` + +也可以通过插件索引名称获取插件对象: + +```python +import nonebot + +plugin: Plugin | None = nonebot.get_plugin("example") +``` + +或者通过模块路径获取插件对象: + +```python +import nonebot + +plugin: Plugin | None = nonebot.get_plugin_by_module_name("awesome_bot.plugins.example") +``` + +如果需要获取所有当前声明的插件名称(可能还未加载),可以使用 `get_available_plugin_names` 函数: + +```python +import nonebot + +plugin_names: set[str] = nonebot.get_available_plugin_names() +``` + +插件对象 `Plugin` 中包含了多个属性: + +- `name`:插件索引名称 +- `module`:插件模块 +- `module_name`:插件模块路径 +- `manager`:插件管理器 +- `matcher`:插件中定义的事件响应器 +- `parent_plugin`:插件的父插件 +- `sub_plugins`:插件的子插件集合 +- `metadata`:插件元数据 + +通过这些属性以及插件元数据,我们就可以收集所需要的插件信息了。 diff --git a/website/versioned_docs/version-2.4.0/advanced/plugin-nesting.md b/website/versioned_docs/version-2.4.0/advanced/plugin-nesting.md new file mode 100644 index 00000000..0a306d28 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/plugin-nesting.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 3 +description: 编写与加载嵌套插件 + +options: + menu: + - category: advanced + weight: 40 +--- + +# 嵌套插件 + +NoneBot 支持嵌套插件,即一个插件可以包含其他插件。通过这种方式,我们可以将一个大型插件拆分成多个功能子插件,使得插件更加清晰、易于维护。我们可以直接在插件中使用 NoneBot 加载插件的方法来加载子插件。 + +## 创建嵌套插件 + +我们可以在使用 `nb-cli` 命令[创建插件](../tutorial/create-plugin.md#创建插件)时,选择直接通过模板创建一个嵌套插件: + +```bash +$ nb plugin create +[?] 插件名称: parent +[?] 使用嵌套插件? (y/N) Y +[?] 输出目录: awesome_bot/plugins +``` + +或者使用 `nb plugin create --sub-plugin` 选项直接创建一个嵌套插件。 + +## 已有插件 + +如果你已经有一个插件,想要在其中嵌套加载子插件,可以在插件的 `__init__.py` 中添加如下代码: + +```python title=parent/__init__.py +import nonebot +from pathlib import Path + +sub_plugins = nonebot.load_plugins( + str(Path(__file__).parent.joinpath("plugins").resolve()) +) +``` + +这样,`parent` 插件就会加载 `parent/plugins` 目录下的所有插件。NoneBot 会正确识别这些插件的父子关系,你可以在 `parent` 的插件信息中看到这些子插件的信息,也可以在子插件信息中看到它们的父插件信息。 diff --git a/website/versioned_docs/version-2.4.0/advanced/requiring.md b/website/versioned_docs/version-2.4.0/advanced/requiring.md new file mode 100644 index 00000000..1a9fe4b0 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/requiring.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 4 +description: 使用其他插件提供的功能 + +options: + menu: + - category: advanced + weight: 50 +--- + +# 跨插件访问 + +NoneBot 插件化系统的设计使得插件之间可以功能独立、各司其职,我们可以更好地维护和扩展插件。但是,有时候我们可能需要在不同插件之间调用功能。NoneBot 生态中就有一类插件,它们专为其他插件提供功能支持,如:[定时任务插件](../best-practice/scheduler.md)、[数据存储插件](../best-practice/data-storing.md)等。这时候我们就需要在插件之间进行跨插件访问。 + +## 插件跟踪 + +由于 NoneBot 插件系统通过 [Import Hooks](https://docs.python.org/3/reference/import.html#import-hooks) 的方式实现插件加载与跟踪管理,因此我们**不能**在 NoneBot 跟踪插件前进行模块 import,这会导致插件加载失败。即,我们不能在使用 NoneBot 提供的加载插件方法前,直接使用 `import` 语句导入插件。 + +对于在项目目录下的插件,我们通常直接使用 `load_from_toml` 等方法一次性加载所有插件。由于这些插件已经被声明,即便插件导入顺序不同,NoneBot 也能正确跟踪插件。此时,我们不需要对跨插件访问进行特殊处理。但当我们使用了外部插件,如果没有事先声明或加载插件,NoneBot 并不会将其当作插件进行跟踪,可能会出现意料之外的错误出现。 + +简单来说,我们必须在 `import` 外部插件之前,确保依赖的外部插件已经被声明或加载。 + +## 插件依赖声明 + +NoneBot 提供了一种方法来确保我们依赖的插件已经被正确加载,即使用 `require` 函数。通过 `require` 函数,我们可以在当前插件中声明依赖的插件,NoneBot 会在加载当前插件时,检查依赖的插件是否已经被加载,如果没有,会尝试优先加载依赖的插件。 + +假设我们有一个插件 `a` 依赖于插件 `b`,我们可以在插件 `a` 中使用 `require` 函数声明其依赖于插件 `b`: + +```python {3} title=a/__init__.py +from nonebot import require + +require("b") + +from b import some_function +``` + +其中,`require` 函数的参数为插件索引名称或者外部插件的模块名称。在完成依赖声明后,我们可以在插件 `a` 中直接导入插件 `b` 所提供的功能。 diff --git a/website/versioned_docs/version-2.4.0/advanced/routing.md b/website/versioned_docs/version-2.4.0/advanced/routing.md new file mode 100644 index 00000000..efa1ac55 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/routing.md @@ -0,0 +1,135 @@ +--- +sidebar_position: 9 +description: 添加服务端路由规则 + +options: + menu: + - category: advanced + weight: 100 +--- + +# 添加路由 + +在[驱动器](./driver.md)一节中,我们了解了驱动器的两种类型。既然驱动器可以作为服务端运行,那么我们就可以向驱动器添加路由规则,从而实现自定义的 API 接口等功能。在添加路由规则时,我们需要注意驱动器的类型,详情可以参考[选择驱动器](./driver.md#配置驱动器)。 + +NoneBot 中,我们可以通过两种途径向 ASGI 驱动器添加路由规则: + +1. 通过 NoneBot 的兼容层建立路由规则。 +2. 直接向 ASGI 应用添加路由规则。 + +这两种途径各有优劣,前者可以在各种服务端型驱动器下运行,但并不能直接使用 ASGI 应用框架提供的特性与功能;后者直接使用 ASGI 应用,更自由、功能完整,但只能在特定类型驱动器下运行。 + +在向驱动器添加路由规则时,我们需要注意驱动器是否为服务端类型,我们可以通过以下方式判断: + +```python +from nonebot import get_driver +from nonebot.drivers import ASGIMixin + +# highlight-next-line +can_use = isinstance(get_driver(), ASGIMixin) +``` + +## 通过兼容层添加路由 + +NoneBot 兼容层定义了两个数据类 `HTTPServerSetup` 和 `WebSocketServerSetup`,分别用于定义 HTTP 服务端和 WebSocket 服务端的路由规则。 + +### HTTP 路由 + +`HTTPServerSetup` 具有四个属性: + +- `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 +- `method`:请求方法。类型为 `str`。 +- `name`:路由名称,不可重复。类型为 `str`。 +- `handle_func`:路由处理函数。类型为 `Callable[[Request], Awaitable[Response]]`。 + +例如,我们添加一个 `/hello` 的路由,当请求方法为 `GET` 时,返回 `200 OK` 以及返回体信息: + +```python +from nonebot import get_driver +from nonebot.drivers import URL, Request, Response, ASGIMixin, HTTPServerSetup + +async def hello(request: Request) -> Response: + return Response(200, content="Hello, world!") + +if isinstance((driver := get_driver()), ASGIMixin): + driver.setup_http_server( + HTTPServerSetup( + path=URL("/hello"), + method="GET", + name="hello", + handle_func=hello, + ) + ) +``` + +对于 `Request` 和 `Response` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 + +### WebSocket 路由 + +`WebSocketServerSetup` 具有三个属性: + +- `path`:路由路径,不支持特殊占位表达式。类型为 `URL`。 +- `name`:路由名称,不可重复。类型为 `str`。 +- `handle_func`:路由处理函数。类型为 `Callable[[WebSocket], Awaitable[Any]]`。 + +例如,我们添加一个 `/ws` 的路由,发送所有接收到的数据: + +```python +from nonebot import get_driver +from nonebot.drivers import URL, ASGIMixin, WebSocket, WebSocketServerSetup + +async def ws_handler(ws: WebSocket): + await ws.accept() + try: + while True: + data = await ws.receive() + await ws.send(data) + except WebSocketClosed as e: + # handle closed + ... + finally: + with contextlib.suppress(Exception): + await websocket.close() + # do some cleanup + +if isinstance((driver := get_driver()), ASGIMixin): + driver.setup_websocket_server( + WebSocketServerSetup( + path=URL("/ws"), + name="ws", + handle_func=ws_handler, + ) + ) +``` + +对于 `WebSocket` 的详细信息,可以参考 [API 文档](../api/drivers/index.md)。 + +## 使用 ASGI 应用添加路由 + +### 获取 ASGI 应用 + +NoneBot 服务端类型的驱动器具有两个属性 `server_app` 和 `asgi`,分别对应驱动框架应用和 ASGI 应用。通常情况下,这两个应用是同一个对象。我们可以通过 `get_app()` 方法快速获取: + +```python +import nonebot + +app = nonebot.get_app() +asgi = nonebot.get_asgi() +``` + +### 添加路由规则 + +在获取到了 ASGI 应用后,我们就可以直接使用 ASGI 应用框架提供的功能来添加路由规则了。这里我们以 [FastAPI](./driver.md#fastapi默认) 为例,演示如何添加路由规则。 + +在下面的代码中,我们添加了一个 `GET` 类型的 `/api` 路由,具体方法参考 [FastAPI 文档](https://fastapi.tiangolo.com/)。 + +```python +import nonebot +from fastapi import FastAPI + +app: FastAPI = nonebot.get_app() + +@app.get("/api") +async def custom_api(): + return {"message": "Hello, world!"} +``` diff --git a/website/versioned_docs/version-2.4.0/advanced/runtime-hook.md b/website/versioned_docs/version-2.4.0/advanced/runtime-hook.md new file mode 100644 index 00000000..6e3c1c41 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/runtime-hook.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 8 +description: 在特定的生命周期中执行代码 + +options: + menu: + - category: advanced + weight: 90 +--- + +# 钩子函数 + +> [钩子编程](https://zh.wikipedia.org/wiki/%E9%92%A9%E5%AD%90%E7%BC%96%E7%A8%8B)(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。 + +在 NoneBot 中有一系列预定义的钩子函数,可以分为两类:**全局钩子函数**和**事件处理钩子函数**,这些钩子函数可以用装饰器的形式来使用。 + +## 全局钩子函数 + +全局钩子函数是指 NoneBot 针对其本身运行过程的钩子函数。 + +这些钩子函数是由驱动器来运行的,故需要先[获得全局驱动器](./driver.md#获取驱动器)。 + +### 启动准备 + +这个钩子函数会在 NoneBot 启动时运行。很多时候,我们并不希望在模块被导入时就执行一些耗时操作,如:连接数据库,这时候我们可以在这个钩子函数中进行这些操作。 + +```python +from nonebot import get_driver + +driver = get_driver() + +@driver.on_startup +async def do_something(): + pass +``` + +### 终止处理 + +这个钩子函数会在 NoneBot 终止时运行。我们可以在这个钩子函数中进行一些清理工作,如:关闭数据库连接。 + +```python +from nonebot import get_driver + +driver = get_driver() + +@driver.on_shutdown +async def do_something(): + pass +``` + +### Bot 连接处理 + +这个钩子函数会在任何协议适配器连接 `Bot` 对象至 NoneBot 时运行。支持依赖注入,可以直接注入 `Bot` 对象。 + +```python +from nonebot import get_driver + +driver = get_driver() + +@driver.on_bot_connect +async def do_something(bot: Bot): + pass +``` + +### Bot 断开处理 + +这个钩子函数会在 `Bot` 断开与 NoneBot 的连接时运行。支持依赖注入,可以直接注入 `Bot` 对象。 + +```python +from nonebot import get_driver + +driver = get_driver() + +@driver.on_bot_disconnect +async def do_something(bot: Bot): + pass +``` + +## 事件处理钩子函数 + +这些钩子函数指的是影响 NoneBot 进行**事件处理**的函数, 这些函数可以跟普通的事件处理函数一样接受相应的参数。 + +### 事件预处理 + +这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。 + +```python +from nonebot.exception import IgnoredException +from nonebot.message import event_preprocessor + +@event_preprocessor +async def do_something(event: Event): + if not event.is_tome(): + raise IgnoredException("some reason") +``` + +### 事件后处理 + +这个钩子函数会在 NoneBot 处理事件完成后运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。 + +```python +from nonebot.message import event_postprocessor + +@event_postprocessor +async def do_something(event: Event): + pass +``` + +### 运行预处理 + +这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。 + +```python +from nonebot.message import run_preprocessor +from nonebot.exception import IgnoredException + +@run_preprocessor +async def do_something(event: Event, matcher: Matcher): + if not event.is_tome(): + raise IgnoredException("some reason") +``` + +### 运行后处理 + +这个钩子函数会在 NoneBot 运行事件响应器后运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态、运行中产生的异常。 + +```python +from nonebot.message import run_postprocessor + +@run_postprocessor +async def do_something(event: Event, matcher: Matcher, exception: Optional[Exception]): + pass +``` + +### 平台接口调用钩子 + +这个钩子函数会在 `Bot` 对象调用平台接口时运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来阻止 `Bot` 对象调用平台接口并返回指定的结果。 + +```python +from nonebot.adapters import Bot +from nonebot.exception import MockApiException + +@Bot.on_calling_api +async def handle_api_call(bot: Bot, api: str, data: Dict[str, Any]): + if api == "send_msg": + raise MockApiException(result={"message_id": 123}) +``` + +### 平台接口调用后钩子 + +这个钩子函数会在 `Bot` 对象调用平台接口后运行。在这个钩子函数中,我们可以通过引起 `MockApiException` 异常来忽略平台接口返回的结果并返回指定的结果。 + +```python +from nonebot.adapters import Bot +from nonebot.exception import MockApiException + +@Bot.on_called_api +async def handle_api_result( + bot: Bot, exception: Optional[Exception], api: str, data: Dict[str, Any], result: Any +): + if not exception and api == "send_msg": + raise MockApiException(result={**result, "message_id": 123}) +``` diff --git a/website/versioned_docs/version-2.4.0/advanced/session-updating.md b/website/versioned_docs/version-2.4.0/advanced/session-updating.md new file mode 100644 index 00000000..f804ba96 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/advanced/session-updating.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 7 +description: 控制会话响应对象 + +options: + menu: + - category: advanced + weight: 80 +--- + +# 会话更新 + +在 NoneBot 中,在某个事件响应器对事件响应后,即是进入了会话状态,会话状态会持续到整个事件响应流程结束。会话过程中,机器人可以与用户进行多次交互。每次需要等待用户事件时,NoneBot 将会复制一个新的临时事件响应器,并更新该事件响应器使其响应当前会话主体的消息,这个过程称为会话更新。 + +会话更新分为两部分:**更新[事件响应器类型](./matcher.md#事件响应器类型)**和**更新[事件触发权限](./matcher.md#事件触发权限)**。 + +## 更新事件响应器类型 + +通常情况下,与机器人用户进行的会话都是通过消息事件进行的,因此会话更新后的默认响应事件类型为 `message`。如果希望接收一个特定类型的消息,比如 `notice` 等,我们需要自定义响应事件类型更新函数。响应事件类型更新函数是一个 `Dependent`,可以使用依赖注入。 + +```python {3-5} +foo = on_message() + +@foo.type_updater +async def _() -> str: + return "notice" +``` + +在注册了上述响应事件类型更新函数后,当我们需要等待用户事件时,将只会响应 `notice` 类型的事件。如果希望在会话过程中的不同阶段响应不同类型的事件,我们就需要使用更复杂的逻辑来更新响应事件类型(如:根据会话状态),这里将不再展示。 + +## 更新事件触发权限 + +会话通常是由机器人与用户进行的一对一交互,因此会话更新后的默认触发权限为当前事件的会话 ID。这个会话 ID 由协议适配器生成,通常由用户 ID 和群 ID 等组成。如果希望实现更复杂的会话功能(如:多用户同时参与的会话),我们需要自定义触发权限更新函数。触发权限更新函数是一个 `Dependent`,可以使用依赖注入。 + +```python {5-7} +from nonebot.permission import User + +foo = on_message() + +@foo.permission_updater +async def _(event: Event, matcher: Matcher) -> Permission: + return Permission(User.from_event(event, perm=matcher.permission)) +``` + +上述权限更新函数是默认的权限更新函数,它将会话的触发权限更新为当前事件的会话 ID。如果我们希望响应多个用户的消息,我们可以如下修改: + +```python {5-7} +from nonebot.permission import USER + +foo = on_message() + +@foo.permission_updater +async def _(matcher: Matcher) -> Permission: + return USER("session1", "session2", perm=matcher.permission) +``` + +请注意,此处为全大写字母的 `USER` 权限,它可以匹配多个会话 ID。通过这种方式,我们可以实现多用户同时参与的会话。 + +我们已经了解了如何控制会话的更新,相信你已经能够实现更复杂的会话功能了,例如多人小游戏等等。欢迎将你的作品分享到[插件商店](/store/plugins)。 diff --git a/website/versioned_docs/version-2.4.0/api/.gitkeep b/website/versioned_docs/version-2.4.0/api/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/website/versioned_docs/version-2.4.0/api/adapters/_category_.json b/website/versioned_docs/version-2.4.0/api/adapters/_category_.json new file mode 100644 index 00000000..a2253e3d --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/adapters/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 15 +} diff --git a/website/versioned_docs/version-2.4.0/api/adapters/index.md b/website/versioned_docs/version-2.4.0/api/adapters/index.md new file mode 100644 index 00000000..2c11878b --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/adapters/index.md @@ -0,0 +1,880 @@ +--- +mdx: + format: md +sidebar_position: 0 +description: nonebot.adapters 模块 +--- + +# nonebot.adapters + +本模块定义了协议适配基类,各协议请继承以下基类。 + +使用 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 注册适配器。 + +## _abstract class_ `Bot(adapter, self_id)` {#Bot} + +- **说明** + + Bot 基类。 + + 用于处理上报消息,并提供 API 调用接口。 + +- **参数** + + - `adapter` ([Adapter](#Adapter)): 协议适配器实例 + + - `self_id` (str): 机器人 ID + +### _instance-var_ `adapter` {#Bot-adapter} + +- **类型:** [Adapter](#Adapter) + +- **说明:** 协议适配器实例 + +### _instance-var_ `self_id` {#Bot-self-id} + +- **类型:** str + +- **说明:** 机器人 ID + +### _property_ `type` {#Bot-type} + +- **类型:** str + +- **说明:** 协议适配器名称 + +### _property_ `config` {#Bot-config} + +- **类型:** [Config](../config.md#Config) + +- **说明:** 全局 NoneBot 配置 + +### _async method_ `call_api(api, **data)` {#Bot-call-api} + +- **说明:** 调用机器人 API 接口,可以通过该函数或直接通过 bot 属性进行调用 + +- **参数** + + - `api` (str): API 名称 + + - `**data` (Any): API 数据 + +- **返回** + + - Any + +- **用法** + + ```python + await bot.call_api("send_msg", message="hello world") + await bot.send_msg(message="hello world") + ``` + +### _abstract async method_ `send(event, message, **kwargs)` {#Bot-send} + +- **说明:** 调用机器人基础发送消息接口 + +- **参数** + + - `event` ([Event](#Event)): 上报事件 + + - `message` (str | [Message](#Message) | [MessageSegment](#MessageSegment)): 要发送的消息 + + - `**kwargs` (Any): 任意额外参数 + +- **返回** + + - Any + +### _classmethod_ `on_calling_api(func)` {#Bot-on-calling-api} + +- **说明** + + 调用 api 预处理。 + + 钩子函数参数: + + - bot: 当前 bot 对象 + - api: 调用的 api 名称 + - data: api 调用的参数字典 + +- **参数** + + - `func` ([T_CallingAPIHook](../typing.md#T-CallingAPIHook)) + +- **返回** + + - [T_CallingAPIHook](../typing.md#T-CallingAPIHook) + +### _classmethod_ `on_called_api(func)` {#Bot-on-called-api} + +- **说明** + + 调用 api 后处理。 + + 钩子函数参数: + + - bot: 当前 bot 对象 + - exception: 调用 api 时发生的错误 + - api: 调用的 api 名称 + - data: api 调用的参数字典 + - result: api 调用的返回 + +- **参数** + + - `func` ([T_CalledAPIHook](../typing.md#T-CalledAPIHook)) + +- **返回** + + - [T_CalledAPIHook](../typing.md#T-CalledAPIHook) + +## _abstract class_ `Event()` {#Event} + +- **说明:** Event 基类。提供获取关键信息的方法,其余信息可直接获取。 + +- **参数** + + auto + +### _abstract method_ `get_type()` {#Event-get-type} + +- **说明:** 获取事件类型的方法,类型通常为 NoneBot 内置的四种类型。 + +- **参数** + + empty + +- **返回** + + - str + +### _abstract method_ `get_event_name()` {#Event-get-event-name} + +- **说明:** 获取事件名称的方法。 + +- **参数** + + empty + +- **返回** + + - str + +### _abstract method_ `get_event_description()` {#Event-get-event-description} + +- **说明:** 获取事件描述的方法,通常为事件具体内容。 + +- **参数** + + empty + +- **返回** + + - str + +### _method_ `get_log_string()` {#Event-get-log-string} + +- **说明** + + 获取事件日志信息的方法。 + + 通常你不需要修改这个方法,只有当希望 NoneBot 隐藏该事件日志时, + 可以抛出 `NoLogException` 异常。 + +- **参数** + + empty + +- **返回** + + - str + +- **异常** + + - NoLogException: 希望 NoneBot 隐藏该事件日志 + +### _abstract method_ `get_user_id()` {#Event-get-user-id} + +- **说明:** 获取事件主体 id 的方法,通常是用户 id 。 + +- **参数** + + empty + +- **返回** + + - str + +### _abstract method_ `get_session_id()` {#Event-get-session-id} + +- **说明:** 获取会话 id 的方法,用于判断当前事件属于哪一个会话, 通常是用户 id、群组 id 组合。 + +- **参数** + + empty + +- **返回** + + - str + +### _abstract method_ `get_message()` {#Event-get-message} + +- **说明:** 获取事件消息内容的方法。 + +- **参数** + + empty + +- **返回** + + - [Message](#Message) + +### _method_ `get_plaintext()` {#Event-get-plaintext} + +- **说明** + + 获取消息纯文本的方法。 + + 通常不需要修改,默认通过 `get_message().extract_plain_text` 获取。 + +- **参数** + + empty + +- **返回** + + - str + +### _abstract method_ `is_tome()` {#Event-is-tome} + +- **说明:** 获取事件是否与机器人有关的方法。 + +- **参数** + + empty + +- **返回** + + - bool + +## _abstract class_ `Adapter(driver, **kwargs)` {#Adapter} + +- **说明** + + 协议适配器基类。 + + 通常,在 Adapter 中编写协议通信相关代码,如: 建立通信连接、处理接收与发送 data 等。 + +- **参数** + + - `driver` ([Driver](../drivers/index.md#Driver)): [Driver](../drivers/index.md#Driver) 实例 + + - `**kwargs` (Any): 其他由 [Driver.register_adapter](../drivers/index.md#Driver-register-adapter) 传入的额外参数 + +### _instance-var_ `driver` {#Adapter-driver} + +- **类型:** [Driver](../drivers/index.md#Driver) + +- **说明:** 实例 + +### _instance-var_ `bots` {#Adapter-bots} + +- **类型:** dict[str, [Bot](#Bot)] + +- **说明:** 本协议适配器已建立连接的 [Bot](#Bot) 实例 + +### _abstract classmethod_ `get_name()` {#Adapter-get-name} + +- **说明:** 当前协议适配器的名称 + +- **参数** + + empty + +- **返回** + + - str + +### _property_ `config` {#Adapter-config} + +- **类型:** [Config](../config.md#Config) + +- **说明:** 全局 NoneBot 配置 + +### _method_ `bot_connect(bot)` {#Adapter-bot-connect} + +- **说明** + + 告知 NoneBot 建立了一个新的 [Bot](#Bot) 连接。 + + 当有新的 [Bot](#Bot) 实例连接建立成功时调用。 + +- **参数** + + - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 + +- **返回** + + - None + +### _method_ `bot_disconnect(bot)` {#Adapter-bot-disconnect} + +- **说明** + + 告知 NoneBot [Bot](#Bot) 连接已断开。 + + 当有 [Bot](#Bot) 实例连接断开时调用。 + +- **参数** + + - `bot` ([Bot](#Bot)): [Bot](#Bot) 实例 + +- **返回** + + - None + +### _method_ `setup_http_server(setup)` {#Adapter-setup-http-server} + +- **说明:** 设置一个 HTTP 服务器路由配置 + +- **参数** + + - `setup` ([HTTPServerSetup](../drivers/index.md#HTTPServerSetup)) + +- **返回** + + - untyped + +### _method_ `setup_websocket_server(setup)` {#Adapter-setup-websocket-server} + +- **说明:** 设置一个 WebSocket 服务器路由配置 + +- **参数** + + - `setup` ([WebSocketServerSetup](../drivers/index.md#WebSocketServerSetup)) + +- **返回** + + - untyped + +### _async method_ `request(setup)` {#Adapter-request} + +- **说明:** 进行一个 HTTP 客户端请求 + +- **参数** + + - `setup` ([Request](../drivers/index.md#Request)) + +- **返回** + + - [Response](../drivers/index.md#Response) + +### _method_ `websocket(setup)` {#Adapter-websocket} + +- **说明:** 建立一个 WebSocket 客户端连接请求 + +- **参数** + + - `setup` ([Request](../drivers/index.md#Request)) + +- **返回** + + - AsyncGenerator[[WebSocket](../drivers/index.md#WebSocket), None] + +### _method_ `on_ready(func)` {#Adapter-on-ready} + +- **参数** + + - `func` (LIFESPAN_FUNC) + +- **返回** + + - LIFESPAN_FUNC + +## _abstract class_ `Message()` {#Message} + +- **说明:** 消息序列 + +- **参数** + + - `message`: 消息内容 + +### _classmethod_ `template(format_string)` {#Message-template} + +- **说明** + + 创建消息模板。 + + 用法和 `str.format` 大致相同,支持以 `Message` 对象作为消息模板并输出消息对象。 + 并且提供了拓展的格式化控制符, + 可以通过该消息类型的 `MessageSegment` 工厂方法创建消息。 + +- **参数** + + - `format_string` (str | TM): 格式化模板 + +- **返回** + + - [MessageTemplate](#MessageTemplate)[Self]: 消息格式化器 + +### _abstract classmethod_ `get_segment_class()` {#Message-get-segment-class} + +- **说明:** 获取消息段类型 + +- **参数** + + empty + +- **返回** + + - type[TMS] + +### _abstract staticmethod_ `_construct(msg)` {#Message--construct} + +- **说明:** 构造消息数组 + +- **参数** + + - `msg` (str) + +- **返回** + + - Iterable[TMS] + +### _method_ `__getitem__(args)` {#Message---getitem--} + +- **重载** + + **1.** `(args) -> Self` + + - **参数** + + - `args` (str): 消息段类型 + + - **返回** + + - Self: 所有类型为 `args` 的消息段 + + **2.** `(args) -> TMS` + + - **参数** + + - `args` (tuple[str, int]): 消息段类型和索引 + + - **返回** + + - TMS: 类型为 `args[0]` 的消息段第 `args[1]` 个 + + **3.** `(args) -> Self` + + - **参数** + + - `args` (tuple[str, slice]): 消息段类型和切片 + + - **返回** + + - Self: 类型为 `args[0]` 的消息段切片 `args[1]` + + **4.** `(args) -> TMS` + + - **参数** + + - `args` (int): 索引 + + - **返回** + + - TMS: 第 `args` 个消息段 + + **5.** `(args) -> Self` + + - **参数** + + - `args` (slice): 切片 + + - **返回** + + - Self: 消息切片 `args` + +### _method_ `__contains__(value)` {#Message---contains--} + +- **说明:** 检查消息段是否存在 + +- **参数** + + - `value` (TMS | str): 消息段或消息段类型 + +- **返回** + + - bool: 消息内是否存在给定消息段或给定类型的消息段 + +### _method_ `has(value)` {#Message-has} + +- **说明:** 与 [`__contains__`](#Message---contains--) 相同 + +- **参数** + + - `value` (TMS | str) + +- **返回** + + - bool + +### _method_ `index(value, *args)` {#Message-index} + +- **说明:** 索引消息段 + +- **参数** + + - `value` (TMS | str): 消息段或者消息段类型 + + - `*args` (SupportsIndex) + + - `arg`: start 与 end + +- **返回** + + - int: 索引 index + +- **异常** + + - ValueError: 消息段不存在 + +### _method_ `get(type_, count=None)` {#Message-get} + +- **说明:** 获取指定类型的消息段 + +- **参数** + + - `type_` (str): 消息段类型 + + - `count` (int | None): 获取个数 + +- **返回** + + - Self: 构建的新消息 + +### _method_ `count(value)` {#Message-count} + +- **说明:** 计算指定消息段的个数 + +- **参数** + + - `value` (TMS | str): 消息段或消息段类型 + +- **返回** + + - int: 个数 + +### _method_ `only(value)` {#Message-only} + +- **说明:** 检查消息中是否仅包含指定消息段 + +- **参数** + + - `value` (TMS | str): 指定消息段或消息段类型 + +- **返回** + + - bool: 是否仅包含指定消息段 + +### _method_ `append(obj)` {#Message-append} + +- **说明:** 添加一个消息段到消息数组末尾。 + +- **参数** + + - `obj` (str | TMS): 要添加的消息段 + +- **返回** + + - Self + +### _method_ `extend(obj)` {#Message-extend} + +- **说明:** 拼接一个消息数组或多个消息段到消息数组末尾。 + +- **参数** + + - `obj` (Self | Iterable[TMS]): 要添加的消息数组 + +- **返回** + + - Self + +### _method_ `join(iterable)` {#Message-join} + +- **说明:** 将多个消息连接并将自身作为分割 + +- **参数** + + - `iterable` (Iterable[TMS | Self]): 要连接的消息 + +- **返回** + + - Self: 连接后的消息 + +### _method_ `copy()` {#Message-copy} + +- **说明:** 深拷贝消息 + +- **参数** + + empty + +- **返回** + + - Self + +### _method_ `include(*types)` {#Message-include} + +- **说明:** 过滤消息 + +- **参数** + + - `*types` (str): 包含的消息段类型 + +- **返回** + + - Self: 新构造的消息 + +### _method_ `exclude(*types)` {#Message-exclude} + +- **说明:** 过滤消息 + +- **参数** + + - `*types` (str): 不包含的消息段类型 + +- **返回** + + - Self: 新构造的消息 + +### _method_ `extract_plain_text()` {#Message-extract-plain-text} + +- **说明:** 提取消息内纯文本消息 + +- **参数** + + empty + +- **返回** + + - str + +## _abstract class_ `MessageSegment()` {#MessageSegment} + +- **说明:** 消息段基类 + +- **参数** + + auto + +### _instance-var_ `type` {#MessageSegment-type} + +- **类型:** str + +- **说明:** 消息段类型 + +### _class-var_ `data` {#MessageSegment-data} + +- **类型:** dict[str, Any] + +- **说明:** 消息段数据 + +### _abstract classmethod_ `get_message_class()` {#MessageSegment-get-message-class} + +- **说明:** 获取消息数组类型 + +- **参数** + + empty + +- **返回** + + - type[TM] + +### _abstract method_ `__str__()` {#MessageSegment---str--} + +- **说明:** 该消息段所代表的 str,在命令匹配部分使用 + +- **参数** + + empty + +- **返回** + + - str + +### _method_ `__add__(other)` {#MessageSegment---add--} + +- **参数** + + - `other` (str | TMS | Iterable[TMS]) + +- **返回** + + - TM + +### _method_ `get(key, default=None)` {#MessageSegment-get} + +- **参数** + + - `key` (str) + + - `default` (Any) + +- **返回** + + - untyped + +### _method_ `keys()` {#MessageSegment-keys} + +- **参数** + + empty + +- **返回** + + - untyped + +### _method_ `values()` {#MessageSegment-values} + +- **参数** + + empty + +- **返回** + + - untyped + +### _method_ `items()` {#MessageSegment-items} + +- **参数** + + empty + +- **返回** + + - untyped + +### _method_ `join(iterable)` {#MessageSegment-join} + +- **参数** + + - `iterable` (Iterable[TMS | TM]) + +- **返回** + + - TM + +### _method_ `copy()` {#MessageSegment-copy} + +- **参数** + + empty + +- **返回** + + - Self + +### _abstract method_ `is_text()` {#MessageSegment-is-text} + +- **说明:** 当前消息段是否为纯文本 + +- **参数** + + empty + +- **返回** + + - bool + +## _class_ `MessageTemplate(template, factory=str, private_getattr=False)` {#MessageTemplate} + +- **说明:** 消息模板格式化实现类。 + +- **参数** + + - `template` (str | TM): 模板 + + - `factory` (type[str] | type[TM]): 消息类型工厂,默认为 `str` + + - `private_getattr` (bool): 是否允许在模板中访问私有属性,默认为 `False` + +### _method_ `add_format_spec(spec, name=None)` {#MessageTemplate-add-format-spec} + +- **参数** + + - `spec` (FormatSpecFunc_T) + + - `name` (str | None) + +- **返回** + + - FormatSpecFunc_T + +### _method_ `format(*args, **kwargs)` {#MessageTemplate-format} + +- **说明:** 根据传入参数和模板生成消息对象 + +- **参数** + + - `*args` + + - `**kwargs` + +- **返回** + + - TF + +### _method_ `format_map(mapping)` {#MessageTemplate-format-map} + +- **说明:** 根据传入字典和模板生成消息对象, 在传入字段名不是有效标识符时有用 + +- **参数** + + - `mapping` (Mapping[str, Any]) + +- **返回** + + - TF + +### _method_ `vformat(format_string, args, kwargs)` {#MessageTemplate-vformat} + +- **参数** + + - `format_string` (str) + + - `args` (Sequence[Any]) + + - `kwargs` (Mapping[str, Any]) + +- **返回** + + - TF + +### _method_ `get_field(field_name, args, kwargs)` {#MessageTemplate-get-field} + +- **参数** + + - `field_name` (str) + + - `args` (Sequence[Any]) + + - `kwargs` (Mapping[str, Any]) + +- **返回** + + - tuple[Any, int | str] + +### _method_ `format_field(value, format_spec)` {#MessageTemplate-format-field} + +- **参数** + + - `value` (Any) + + - `format_spec` (str) + +- **返回** + + - Any diff --git a/website/versioned_docs/version-2.4.0/api/compat.md b/website/versioned_docs/version-2.4.0/api/compat.md new file mode 100644 index 00000000..e4f9da20 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/compat.md @@ -0,0 +1,216 @@ +--- +mdx: + format: md +sidebar_position: 16 +description: nonebot.compat 模块 +--- + +# nonebot.compat + +本模块为 Pydantic 版本兼容层模块 + +为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。 + +## _var_ `Required` {#Required} + +- **类型:** untyped + +- **说明:** Alias of Ellipsis for compatibility with pydantic v1 + +## _library-attr_ `PydanticUndefined` {#PydanticUndefined} + +- **说明:** Pydantic Undefined object + +## _library-attr_ `PydanticUndefinedType` {#PydanticUndefinedType} + +- **说明:** Pydantic Undefined type + +## _var_ `DEFAULT_CONFIG` {#DEFAULT-CONFIG} + +- **类型:** untyped + +- **说明:** Default config for validations + +## _class_ `FieldInfo(default=PydanticUndefined, **kwargs)` {#FieldInfo} + +- **说明:** FieldInfo class with extra property for compatibility with pydantic v1 + +- **参数** + + - `default` (Any) + + - `**kwargs` (Any) + +### _property_ `extra` {#FieldInfo-extra} + +- **类型:** dict[str, Any] + +- **说明** + + Extra data that is not part of the standard pydantic fields. + + For compatibility with pydantic v1. + +## _class_ `ModelField()` {#ModelField} + +- **说明:** ModelField class for compatibility with pydantic v1 + +- **参数** + + auto + +### _instance-var_ `name` {#ModelField-name} + +- **类型:** str + +- **说明:** The name of the field. + +### _instance-var_ `annotation` {#ModelField-annotation} + +- **类型:** Any + +- **说明:** The annotation of the field. + +### _instance-var_ `field_info` {#ModelField-field-info} + +- **类型:** FieldInfo + +- **说明:** The FieldInfo of the field. + +### _classmethod_ `construct(name, annotation, field_info=None)` {#ModelField-construct} + +- **说明:** Construct a ModelField from given infos. + +- **参数** + + - `name` (str) + + - `annotation` (Any) + + - `field_info` (FieldInfo | None) + +- **返回** + + - Self + +### _method_ `get_default()` {#ModelField-get-default} + +- **说明:** Get the default value of the field. + +- **参数** + + empty + +- **返回** + + - Any + +### _method_ `validate_value(value)` {#ModelField-validate-value} + +- **说明:** Validate the value pass to the field. + +- **参数** + + - `value` (Any) + +- **返回** + + - Any + +## _def_ `extract_field_info(field_info)` {#extract-field-info} + +- **说明:** Get FieldInfo init kwargs from a FieldInfo instance. + +- **参数** + + - `field_info` (BaseFieldInfo) + +- **返回** + + - dict[str, Any] + +## _def_ `model_fields(model)` {#model-fields} + +- **说明:** Get field list of a model. + +- **参数** + + - `model` (type[BaseModel]) + +- **返回** + + - list[ModelField] + +## _def_ `model_config(model)` {#model-config} + +- **说明:** Get config of a model. + +- **参数** + + - `model` (type[BaseModel]) + +- **返回** + + - Any + +## _def_ `model_dump(model, include=None, exclude=None, by_alias=False, exclude_unset=False, exclude_defaults=False, exclude_none=False)` {#model-dump} + +- **参数** + + - `model` (BaseModel) + + - `include` (set[str] | None) + + - `exclude` (set[str] | None) + + - `by_alias` (bool) + + - `exclude_unset` (bool) + + - `exclude_defaults` (bool) + + - `exclude_none` (bool) + +- **返回** + + - dict[str, Any] + +## _def_ `type_validate_python(type_, data)` {#type-validate-python} + +- **说明:** Validate data with given type. + +- **参数** + + - `type_` (type[T]) + + - `data` (Any) + +- **返回** + + - T + +## _def_ `type_validate_json(type_, data)` {#type-validate-json} + +- **说明:** Validate JSON with given type. + +- **参数** + + - `type_` (type[T]) + + - `data` (str | bytes) + +- **返回** + + - T + +## _def_ `custom_validation(class_)` {#custom-validation} + +- **说明:** Use pydantic v1 like validator generator in pydantic v2 + +- **参数** + + - `class_` (type[CVC]) + +- **返回** + + - type[CVC] diff --git a/website/versioned_docs/version-2.4.0/api/config.md b/website/versioned_docs/version-2.4.0/api/config.md new file mode 100644 index 00000000..847404fd --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/config.md @@ -0,0 +1,181 @@ +--- +mdx: + format: md +sidebar_position: 1 +description: nonebot.config 模块 +--- + +# nonebot.config + +本模块定义了 NoneBot 本身运行所需的配置项。 + +NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及 +[`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。 + +配置项需符合特殊格式或 json 序列化格式 +详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。 + +## _class_ `Env(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Env} + +- **说明** + + 运行环境配置。大小写不敏感。 + + 将会从 **环境变量** > **dotenv 配置文件** 的优先级读取环境信息。 + +- **参数** + + - `_env_file` (DOTENV_TYPE | None) + + - `_env_file_encoding` (str | None) + + - `_env_nested_delimiter` (str | None) + + - `**values` (Any) + +### _class-var_ `environment` {#Env-environment} + +- **类型:** str + +- **说明** + + 当前环境名。 + + NoneBot 将从 `.env.{environment}` 文件中加载配置。 + +## _class_ `Config(_env_file=ENV_FILE_SENTINEL, _env_file_encoding=None, _env_nested_delimiter=None, **values)` {#Config} + +- **说明** + + NoneBot 主要配置。大小写不敏感。 + + 除了 NoneBot 的配置项外,还可以自行添加配置项到 `.env.{environment}` 文件中。 + 这些配置将会在 json 反序列化后一起带入 `Config` 类中。 + + 配置方法参考: [配置](https://nonebot.dev/docs/appendices/config) + +- **参数** + + - `_env_file` (DOTENV_TYPE | None) + + - `_env_file_encoding` (str | None) + + - `_env_nested_delimiter` (str | None) + + - `**values` (Any) + +### _class-var_ `driver` {#Config-driver} + +- **类型:** str + +- **说明** + + NoneBot 运行所使用的 `Driver` 。继承自 [Driver](drivers/index.md#Driver) 。 + + 配置格式为 `[:][+[:]]*`。 + + `~` 为 `nonebot.drivers.` 的缩写。 + + 配置方法参考: [配置驱动器](https://nonebot.dev/docs/advanced/driver#%E9%85%8D%E7%BD%AE%E9%A9%B1%E5%8A%A8%E5%99%A8) + +### _class-var_ `host` {#Config-host} + +- **类型:** IPvAnyAddress + +- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的 IP/主机名。 + +### _class-var_ `port` {#Config-port} + +- **类型:** int + +- **说明:** NoneBot [ReverseDriver](drivers/index.md#ReverseDriver) 服务端监听的端口。 + +### _class-var_ `log_level` {#Config-log-level} + +- **类型:** int | str + +- **说明** + + NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。 + + 参考 [记录日志](https://nonebot.dev/docs/appendices/log),[loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 + + :::tip 提示 + 日志等级名称应为大写,如 `INFO`。 + ::: + +- **用法** + + ```conf + LOG_LEVEL=25 + LOG_LEVEL=INFO + ``` + +### _class-var_ `api_timeout` {#Config-api-timeout} + +- **类型:** float | None + +- **说明:** API 请求超时时间,单位: 秒。 + +### _class-var_ `superusers` {#Config-superusers} + +- **类型:** set[str] + +- **说明:** 机器人超级用户。 + +- **用法** + + ```conf + SUPERUSERS=["12345789"] + ``` + +### _class-var_ `nickname` {#Config-nickname} + +- **类型:** set[str] + +- **说明:** 机器人昵称。 + +### _class-var_ `command_start` {#Config-command-start} + +- **类型:** set[str] + +- **说明** + + 命令的起始标记,用于判断一条消息是不是命令。 + + 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 + +- **用法** + + ```conf + COMMAND_START=["/", ""] + ``` + +### _class-var_ `command_sep` {#Config-command-sep} + +- **类型:** set[str] + +- **说明** + + 命令的分隔标记,用于将文本形式的命令切分为元组(实际的命令名)。 + + 参考[命令响应规则](https://nonebot.dev/docs/advanced/matcher#command)。 + +- **用法** + + ```conf + COMMAND_SEP=["."] + ``` + +### _class-var_ `session_expire_timeout` {#Config-session-expire-timeout} + +- **类型:** timedelta + +- **说明:** 等待用户回复的超时时间。 + +- **用法** + + ```conf + SESSION_EXPIRE_TIMEOUT=[-][DD]D[,][HH:MM:]SS[.ffffff] + SESSION_EXPIRE_TIMEOUT=[±]P[DD]DT[HH]H[MM]M[SS]S # ISO 8601 + ``` diff --git a/website/versioned_docs/version-2.4.0/api/consts.md b/website/versioned_docs/version-2.4.0/api/consts.md new file mode 100644 index 00000000..a6f8952a --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/consts.md @@ -0,0 +1,118 @@ +--- +mdx: + format: md +sidebar_position: 9 +description: nonebot.consts 模块 +--- + +# nonebot.consts + +本模块包含了 NoneBot 事件处理过程中使用到的常量。 + +## _var_ `RECEIVE_KEY` {#RECEIVE-KEY} + +- **类型:** Literal['\_receive\_{id}'] + +- **说明:** `receive` 存储 key + +## _var_ `LAST_RECEIVE_KEY` {#LAST-RECEIVE-KEY} + +- **类型:** Literal['\_last\_receive'] + +- **说明:** `last_receive` 存储 key + +## _var_ `ARG_KEY` {#ARG-KEY} + +- **类型:** Literal['{key}'] + +- **说明:** `arg` 存储 key + +## _var_ `REJECT_TARGET` {#REJECT-TARGET} + +- **类型:** Literal['\_current\_target'] + +- **说明:** 当前 `reject` 目标存储 key + +## _var_ `REJECT_CACHE_TARGET` {#REJECT-CACHE-TARGET} + +- **类型:** Literal['\_next\_target'] + +- **说明:** 下一个 `reject` 目标存储 key + +## _var_ `PREFIX_KEY` {#PREFIX-KEY} + +- **类型:** Literal['\_prefix'] + +- **说明:** 命令前缀存储 key + +## _var_ `CMD_KEY` {#CMD-KEY} + +- **类型:** Literal['command'] + +- **说明:** 命令元组存储 key + +## _var_ `RAW_CMD_KEY` {#RAW-CMD-KEY} + +- **类型:** Literal['raw\_command'] + +- **说明:** 命令文本存储 key + +## _var_ `CMD_ARG_KEY` {#CMD-ARG-KEY} + +- **类型:** Literal['command\_arg'] + +- **说明:** 命令参数存储 key + +## _var_ `CMD_START_KEY` {#CMD-START-KEY} + +- **类型:** Literal['command\_start'] + +- **说明:** 命令开头存储 key + +## _var_ `CMD_WHITESPACE_KEY` {#CMD-WHITESPACE-KEY} + +- **类型:** Literal['command\_whitespace'] + +- **说明:** 命令与参数间空白符存储 key + +## _var_ `SHELL_ARGS` {#SHELL-ARGS} + +- **类型:** Literal['\_args'] + +- **说明:** shell 命令 parse 后参数字典存储 key + +## _var_ `SHELL_ARGV` {#SHELL-ARGV} + +- **类型:** Literal['\_argv'] + +- **说明:** shell 命令原始参数列表存储 key + +## _var_ `REGEX_MATCHED` {#REGEX-MATCHED} + +- **类型:** Literal['\_matched'] + +- **说明:** 正则匹配结果存储 key + +## _var_ `STARTSWITH_KEY` {#STARTSWITH-KEY} + +- **类型:** Literal['\_startswith'] + +- **说明:** 响应触发前缀 key + +## _var_ `ENDSWITH_KEY` {#ENDSWITH-KEY} + +- **类型:** Literal['\_endswith'] + +- **说明:** 响应触发后缀 key + +## _var_ `FULLMATCH_KEY` {#FULLMATCH-KEY} + +- **类型:** Literal['\_fullmatch'] + +- **说明:** 响应触发完整消息 key + +## _var_ `KEYWORD_KEY` {#KEYWORD-KEY} + +- **类型:** Literal['\_keyword'] + +- **说明:** 响应触发关键字 key diff --git a/website/versioned_docs/version-2.4.0/api/dependencies/_category_.json b/website/versioned_docs/version-2.4.0/api/dependencies/_category_.json new file mode 100644 index 00000000..6bd1772a --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/dependencies/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 13 +} diff --git a/website/versioned_docs/version-2.4.0/api/dependencies/index.md b/website/versioned_docs/version-2.4.0/api/dependencies/index.md new file mode 100644 index 00000000..c11b6e8c --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/dependencies/index.md @@ -0,0 +1,100 @@ +--- +mdx: + format: md +sidebar_position: 0 +description: nonebot.dependencies 模块 +--- + +# nonebot.dependencies + +本模块模块实现了依赖注入的定义与处理。 + +## _abstract class_ `Param(*args, validate=False, **kwargs)` {#Param} + +- **说明** + + 依赖注入的基本单元 —— 参数。 + + 继承自 `pydantic.fields.FieldInfo`,用于描述参数信息(不包括参数名)。 + +- **参数** + + - `*args` + + - `validate` (bool) + + - `**kwargs` (Any) + +## _class_ `Dependent()` {#Dependent} + +- **说明:** 依赖注入容器 + +- **参数** + + - `call`: 依赖注入的可调用对象,可以是任何 Callable 对象 + + - `pre_checkers`: 依赖注入解析前的参数检查 + + - `params`: 具名参数列表 + + - `parameterless`: 匿名参数列表 + + - `allow_types`: 允许的参数类型 + +### _staticmethod_ `parse_params(call, allow_types)` {#Dependent-parse-params} + +- **参数** + + - `call` (\_DependentCallable[R]) + + - `allow_types` (tuple[type[Param], ...]) + +- **返回** + + - tuple[[ModelField](../compat.md#ModelField), ...] + +### _staticmethod_ `parse_parameterless(parameterless, allow_types)` {#Dependent-parse-parameterless} + +- **参数** + + - `parameterless` (tuple[Any, ...]) + + - `allow_types` (tuple[type[Param], ...]) + +- **返回** + + - tuple[Param, ...] + +### _classmethod_ `parse(*, call, parameterless=None, allow_types)` {#Dependent-parse} + +- **参数** + + - `call` (\_DependentCallable[R]) + + - `parameterless` (Iterable[Any] | None) + + - `allow_types` (Iterable[type[Param]]) + +- **返回** + + - Dependent[R] + +### _async method_ `check(**params)` {#Dependent-check} + +- **参数** + + - `**params` (Any) + +- **返回** + + - None + +### _async method_ `solve(**params)` {#Dependent-solve} + +- **参数** + + - `**params` (Any) + +- **返回** + + - dict[str, Any] diff --git a/website/versioned_docs/version-2.4.0/api/dependencies/utils.md b/website/versioned_docs/version-2.4.0/api/dependencies/utils.md new file mode 100644 index 00000000..10c4fd74 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/dependencies/utils.md @@ -0,0 +1,48 @@ +--- +mdx: + format: md +sidebar_position: 1 +description: nonebot.dependencies.utils 模块 +--- + +# nonebot.dependencies.utils + +## _def_ `get_typed_signature(call)` {#get-typed-signature} + +- **说明:** 获取可调用对象签名 + +- **参数** + + - `call` ((...) -> Any) + +- **返回** + + - inspect.Signature + +## _def_ `get_typed_annotation(param, globalns)` {#get-typed-annotation} + +- **说明:** 获取参数的类型注解 + +- **参数** + + - `param` (inspect.Parameter) + + - `globalns` (dict[str, Any]) + +- **返回** + + - Any + +## _def_ `check_field_type(field, value)` {#check-field-type} + +- **说明:** 检查字段类型是否匹配 + +- **参数** + + - `field` ([ModelField](../compat.md#ModelField)) + + - `value` (Any) + +- **返回** + + - Any diff --git a/website/versioned_docs/version-2.4.0/api/drivers/_category_.json b/website/versioned_docs/version-2.4.0/api/drivers/_category_.json new file mode 100644 index 00000000..3714fde8 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 14 +} diff --git a/website/versioned_docs/version-2.4.0/api/drivers/aiohttp.md b/website/versioned_docs/version-2.4.0/api/drivers/aiohttp.md new file mode 100644 index 00000000..57147258 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/aiohttp.md @@ -0,0 +1,206 @@ +--- +mdx: + format: md +sidebar_position: 2 +description: nonebot.drivers.aiohttp 模块 +--- + +# nonebot.drivers.aiohttp + +[AIOHTTP](https://aiohttp.readthedocs.io/en/stable/) 驱动适配器。 + +```bash +nb driver install aiohttp +# 或者 +pip install nonebot2[aiohttp] +``` + +:::tip 提示 +本驱动仅支持客户端连接 +::: + +## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} + +- **参数** + + - `params` (QueryTypes) + + - `headers` (HeaderTypes) + + - `cookies` (CookieTypes) + + - `version` (str | [HTTPVersion](index.md#HTTPVersion)) + + - `timeout` (float | None) + + - `proxy` (str | None) + +### _async method_ `request(setup)` {#Session-request} + +- **参数** + + - `setup` ([Request](index.md#Request)) + +- **返回** + + - [Response](index.md#Response) + +### _async method_ `setup()` {#Session-setup} + +- **参数** + + empty + +- **返回** + + - None + +### _async method_ `close()` {#Session-close} + +- **参数** + + empty + +- **返回** + + - None + +## _class_ `Mixin()` {#Mixin} + +- **说明:** AIOHTTP Mixin + +- **参数** + + auto + +### _async method_ `request(setup)` {#Mixin-request} + +- **参数** + + - `setup` ([Request](index.md#Request)) + +- **返回** + + - [Response](index.md#Response) + +### _method_ `websocket(setup)` {#Mixin-websocket} + +- **参数** + + - `setup` ([Request](index.md#Request)) + +- **返回** + + - AsyncGenerator[[WebSocket](index.md#WebSocket), None] + +### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session} + +- **参数** + + - `params` (QueryTypes) + + - `headers` (HeaderTypes) + + - `cookies` (CookieTypes) + + - `version` (str | [HTTPVersion](index.md#HTTPVersion)) + + - `timeout` (float | None) + + - `proxy` (str | None) + +- **返回** + + - Session + +## _class_ `WebSocket(*, request, session, websocket)` {#WebSocket} + +- **说明:** AIOHTTP Websocket Wrapper + +- **参数** + + - `request` ([Request](index.md#Request)) + + - `session` (aiohttp.ClientSession) + + - `websocket` (aiohttp.ClientWebSocketResponse) + +### _async method_ `accept()` {#WebSocket-accept} + +- **参数** + + empty + +- **返回** + + - untyped + +### _async method_ `close(code=1000, reason="")` {#WebSocket-close} + +- **参数** + + - `code` (int) + + - `reason` (str) + +- **返回** + + - untyped + +### _async method_ `receive()` {#WebSocket-receive} + +- **参数** + + empty + +- **返回** + + - str + +### _async method_ `receive_text()` {#WebSocket-receive-text} + +- **参数** + + empty + +- **返回** + + - str + +### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} + +- **参数** + + empty + +- **返回** + + - bytes + +### _async method_ `send_text(data)` {#WebSocket-send-text} + +- **参数** + + - `data` (str) + +- **返回** + + - None + +### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} + +- **参数** + + - `data` (bytes) + +- **返回** + + - None + +## _class_ `Driver(env, config)` {#Driver} + +- **参数** + + - `env` ([Env](../config.md#Env)) + + - `config` ([Config](../config.md#Config)) diff --git a/website/versioned_docs/version-2.4.0/api/drivers/fastapi.md b/website/versioned_docs/version-2.4.0/api/drivers/fastapi.md new file mode 100644 index 00000000..bc7da389 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/fastapi.md @@ -0,0 +1,244 @@ +--- +mdx: + format: md +sidebar_position: 1 +description: nonebot.drivers.fastapi 模块 +--- + +# nonebot.drivers.fastapi + +[FastAPI](https://fastapi.tiangolo.com/) 驱动适配 + +```bash +nb driver install fastapi +# 或者 +pip install nonebot2[fastapi] +``` + +:::tip 提示 +本驱动仅支持服务端连接 +::: + +## _class_ `Config()` {#Config} + +- **说明:** FastAPI 驱动框架设置,详情参考 FastAPI 文档 + +- **参数** + + auto + +### _class-var_ `fastapi_openapi_url` {#Config-fastapi-openapi-url} + +- **类型:** str | None + +- **说明:** `openapi.json` 地址,默认为 `None` 即关闭 + +### _class-var_ `fastapi_docs_url` {#Config-fastapi-docs-url} + +- **类型:** str | None + +- **说明:** `swagger` 地址,默认为 `None` 即关闭 + +### _class-var_ `fastapi_redoc_url` {#Config-fastapi-redoc-url} + +- **类型:** str | None + +- **说明:** `redoc` 地址,默认为 `None` 即关闭 + +### _class-var_ `fastapi_include_adapter_schema` {#Config-fastapi-include-adapter-schema} + +- **类型:** bool + +- **说明:** 是否包含适配器路由的 schema,默认为 `True` + +### _class-var_ `fastapi_reload` {#Config-fastapi-reload} + +- **类型:** bool + +- **说明:** 开启/关闭冷重载 + +### _class-var_ `fastapi_reload_dirs` {#Config-fastapi-reload-dirs} + +- **类型:** list[str] | None + +- **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 + +### _class-var_ `fastapi_reload_delay` {#Config-fastapi-reload-delay} + +- **类型:** float + +- **说明:** 重载延迟,默认为 uvicorn 默认值 + +### _class-var_ `fastapi_reload_includes` {#Config-fastapi-reload-includes} + +- **类型:** list[str] | None + +- **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +### _class-var_ `fastapi_reload_excludes` {#Config-fastapi-reload-excludes} + +- **类型:** list[str] | None + +- **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +### _class-var_ `fastapi_extra` {#Config-fastapi-extra} + +- **类型:** dict[str, Any] + +- **说明:** 传递给 `FastAPI` 的其他参数。 + +## _class_ `Driver(env, config)` {#Driver} + +- **说明:** FastAPI 驱动框架。 + +- **参数** + + - `env` ([Env](../config.md#Env)) + + - `config` (NoneBotConfig) + +### _property_ `type` {#Driver-type} + +- **类型:** str + +- **说明:** 驱动名称: `fastapi` + +### _property_ `server_app` {#Driver-server-app} + +- **类型:** FastAPI + +- **说明:** `FastAPI APP` 对象 + +### _property_ `asgi` {#Driver-asgi} + +- **类型:** FastAPI + +- **说明:** `FastAPI APP` 对象 + +### _property_ `logger` {#Driver-logger} + +- **类型:** logging.Logger + +- **说明:** fastapi 使用的 logger + +### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} + +- **参数** + + - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) + +- **返回** + + - untyped + +### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} + +- **参数** + + - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) + +- **返回** + + - None + +### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} + +- **说明:** 使用 `uvicorn` 启动 FastAPI + +- **参数** + + - `host` (str | None) + + - `port` (int | None) + + - `*args` + + - `app` (str | None) + + - `**kwargs` + +- **返回** + + - untyped + +## _class_ `FastAPIWebSocket(*, request, websocket)` {#FastAPIWebSocket} + +- **说明:** FastAPI WebSocket Wrapper + +- **参数** + + - `request` (BaseRequest) + + - `websocket` ([WebSocket](index.md#WebSocket)) + +### _async method_ `accept()` {#FastAPIWebSocket-accept} + +- **参数** + + empty + +- **返回** + + - None + +### _async method_ `close(code=status.WS_1000_NORMAL_CLOSURE, reason="")` {#FastAPIWebSocket-close} + +- **参数** + + - `code` (int) + + - `reason` (str) + +- **返回** + + - None + +### _async method_ `receive()` {#FastAPIWebSocket-receive} + +- **参数** + + empty + +- **返回** + + - str | bytes + +### _async method_ `receive_text()` {#FastAPIWebSocket-receive-text} + +- **参数** + + empty + +- **返回** + + - str + +### _async method_ `receive_bytes()` {#FastAPIWebSocket-receive-bytes} + +- **参数** + + empty + +- **返回** + + - bytes + +### _async method_ `send_text(data)` {#FastAPIWebSocket-send-text} + +- **参数** + + - `data` (str) + +- **返回** + + - None + +### _async method_ `send_bytes(data)` {#FastAPIWebSocket-send-bytes} + +- **参数** + + - `data` (bytes) + +- **返回** + + - None diff --git a/website/versioned_docs/version-2.4.0/api/drivers/httpx.md b/website/versioned_docs/version-2.4.0/api/drivers/httpx.md new file mode 100644 index 00000000..e40efc39 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/httpx.md @@ -0,0 +1,112 @@ +--- +mdx: + format: md +sidebar_position: 3 +description: nonebot.drivers.httpx 模块 +--- + +# nonebot.drivers.httpx + +[HTTPX](https://www.python-httpx.org/) 驱动适配 + +```bash +nb driver install httpx +# 或者 +pip install nonebot2[httpx] +``` + +:::tip 提示 +本驱动仅支持客户端 HTTP 连接 +::: + +## _class_ `Session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Session} + +- **参数** + + - `params` (QueryTypes) + + - `headers` (HeaderTypes) + + - `cookies` (CookieTypes) + + - `version` (str | [HTTPVersion](index.md#HTTPVersion)) + + - `timeout` (float | None) + + - `proxy` (str | None) + +### _async method_ `request(setup)` {#Session-request} + +- **参数** + + - `setup` ([Request](index.md#Request)) + +- **返回** + + - [Response](index.md#Response) + +### _async method_ `setup()` {#Session-setup} + +- **参数** + + empty + +- **返回** + + - None + +### _async method_ `close()` {#Session-close} + +- **参数** + + empty + +- **返回** + + - None + +## _class_ `Mixin()` {#Mixin} + +- **说明:** HTTPX Mixin + +- **参数** + + auto + +### _async method_ `request(setup)` {#Mixin-request} + +- **参数** + + - `setup` ([Request](index.md#Request)) + +- **返回** + + - [Response](index.md#Response) + +### _method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Mixin-get-session} + +- **参数** + + - `params` (QueryTypes) + + - `headers` (HeaderTypes) + + - `cookies` (CookieTypes) + + - `version` (str | [HTTPVersion](index.md#HTTPVersion)) + + - `timeout` (float | None) + + - `proxy` (str | None) + +- **返回** + + - Session + +## _class_ `Driver(env, config)` {#Driver} + +- **参数** + + - `env` ([Env](../config.md#Env)) + + - `config` ([Config](../config.md#Config)) diff --git a/website/versioned_docs/version-2.4.0/api/drivers/index.md b/website/versioned_docs/version-2.4.0/api/drivers/index.md new file mode 100644 index 00000000..c05ca823 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/index.md @@ -0,0 +1,598 @@ +--- +mdx: + format: md +sidebar_position: 0 +description: nonebot.drivers 模块 +--- + +# nonebot.drivers + +本模块定义了驱动适配器基类。 + +各驱动请继承以下基类。 + +## _abstract class_ `Mixin()` {#Mixin} + +- **说明:** 可与其他驱动器共用的混入基类。 + +- **参数** + + auto + +### _abstract property_ `type` {#Mixin-type} + +- **类型:** str + +- **说明:** 混入驱动类型名称 + +## _abstract class_ `Driver(env, config)` {#Driver} + +- **说明** + + 驱动器基类。 + + 驱动器控制框架的启动和停止,适配器的注册,以及机器人生命周期管理。 + +- **参数** + + - `env` ([Env](../config.md#Env)): 包含环境信息的 Env 对象 + + - `config` ([Config](../config.md#Config)): 包含配置信息的 Config 对象 + +### _instance-var_ `env` {#Driver-env} + +- **类型:** str + +- **说明:** 环境名称 + +### _instance-var_ `config` {#Driver-config} + +- **类型:** [Config](../config.md#Config) + +- **说明:** 全局配置对象 + +### _property_ `bots` {#Driver-bots} + +- **类型:** dict[str, [Bot](../adapters/index.md#Bot)] + +- **说明:** 获取当前所有已连接的 Bot + +### _method_ `register_adapter(adapter, **kwargs)` {#Driver-register-adapter} + +- **说明:** 注册一个协议适配器 + +- **参数** + + - `adapter` (type[[Adapter](../adapters/index.md#Adapter)]): 适配器类 + + - `**kwargs`: 其他传递给适配器的参数 + +- **返回** + + - None + +### _abstract property_ `type` {#Driver-type} + +- **类型:** str + +- **说明:** 驱动类型名称 + +### _abstract property_ `logger` {#Driver-logger} + +- **类型:** untyped + +- **说明:** 驱动专属 logger 日志记录器 + +### _abstract method_ `run(*args, **kwargs)` {#Driver-run} + +- **说明:** 启动驱动框架 + +- **参数** + + - `*args` + + - `**kwargs` + +- **返回** + + - untyped + +### _method_ `on_startup(func)` {#Driver-on-startup} + +- **说明:** 注册一个启动时执行的函数 + +- **参数** + + - `func` (LIFESPAN_FUNC) + +- **返回** + + - LIFESPAN_FUNC + +### _method_ `on_shutdown(func)` {#Driver-on-shutdown} + +- **说明:** 注册一个停止时执行的函数 + +- **参数** + + - `func` (LIFESPAN_FUNC) + +- **返回** + + - LIFESPAN_FUNC + +### _classmethod_ `on_bot_connect(func)` {#Driver-on-bot-connect} + +- **说明** + + 装饰一个函数使他在 bot 连接成功时执行。 + + 钩子函数参数: + + - bot: 当前连接上的 Bot 对象 + +- **参数** + + - `func` ([T_BotConnectionHook](../typing.md#T-BotConnectionHook)) + +- **返回** + + - [T_BotConnectionHook](../typing.md#T-BotConnectionHook) + +### _classmethod_ `on_bot_disconnect(func)` {#Driver-on-bot-disconnect} + +- **说明** + + 装饰一个函数使他在 bot 连接断开时执行。 + + 钩子函数参数: + + - bot: 当前连接上的 Bot 对象 + +- **参数** + + - `func` ([T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook)) + +- **返回** + + - [T_BotDisconnectionHook](../typing.md#T-BotDisconnectionHook) + +## _class_ `Cookies(cookies=None)` {#Cookies} + +- **参数** + + - `cookies` (CookieTypes) + +### _method_ `set(name, value, domain="", path="/")` {#Cookies-set} + +- **参数** + + - `name` (str) + + - `value` (str) + + - `domain` (str) + + - `path` (str) + +- **返回** + + - None + +### _method_ `get(name, default=None, domain=None, path=None)` {#Cookies-get} + +- **参数** + + - `name` (str) + + - `default` (str | None) + + - `domain` (str | None) + + - `path` (str | None) + +- **返回** + + - str | None + +### _method_ `delete(name, domain=None, path=None)` {#Cookies-delete} + +- **参数** + + - `name` (str) + + - `domain` (str | None) + + - `path` (str | None) + +- **返回** + + - None + +### _method_ `clear(domain=None, path=None)` {#Cookies-clear} + +- **参数** + + - `domain` (str | None) + + - `path` (str | None) + +- **返回** + + - None + +### _method_ `update(cookies=None)` {#Cookies-update} + +- **参数** + + - `cookies` (CookieTypes) + +- **返回** + + - None + +### _method_ `as_header(request)` {#Cookies-as-header} + +- **参数** + + - `request` (Request) + +- **返回** + + - dict[str, str] + +## _class_ `Request(method, url, *, params=None, headers=None, cookies=None, content=None, data=None, json=None, files=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#Request} + +- **参数** + + - `method` (str | bytes) + + - `url` (URL | str | RawURL) + + - `params` (QueryTypes) + + - `headers` (HeaderTypes) + + - `cookies` (CookieTypes) + + - `content` (ContentTypes) + + - `data` (DataTypes) + + - `json` (Any) + + - `files` (FilesTypes) + + - `version` (str | HTTPVersion) + + - `timeout` (float | None) + + - `proxy` (str | None) + +## _class_ `Response(status_code, *, headers=None, content=None, request=None)` {#Response} + +- **参数** + + - `status_code` (int) + + - `headers` (HeaderTypes) + + - `content` (ContentTypes) + + - `request` (Request | None) + +## _abstract class_ `ASGIMixin()` {#ASGIMixin} + +- **说明** + + ASGI 服务端基类。 + + 将后端框架封装,以满足适配器使用。 + +- **参数** + + auto + +### _abstract property_ `server_app` {#ASGIMixin-server-app} + +- **类型:** Any + +- **说明:** 驱动 APP 对象 + +### _abstract property_ `asgi` {#ASGIMixin-asgi} + +- **类型:** Any + +- **说明:** 驱动 ASGI 对象 + +### _abstract method_ `setup_http_server(setup)` {#ASGIMixin-setup-http-server} + +- **说明:** 设置一个 HTTP 服务器路由配置 + +- **参数** + + - `setup` ([HTTPServerSetup](#HTTPServerSetup)) + +- **返回** + + - None + +### _abstract method_ `setup_websocket_server(setup)` {#ASGIMixin-setup-websocket-server} + +- **说明:** 设置一个 WebSocket 服务器路由配置 + +- **参数** + + - `setup` ([WebSocketServerSetup](#WebSocketServerSetup)) + +- **返回** + + - None + +## _abstract class_ `WebSocket(*, request)` {#WebSocket} + +- **参数** + + - `request` (Request) + +### _abstract property_ `closed` {#WebSocket-closed} + +- **类型:** bool + +- **说明:** 连接是否已经关闭 + +### _abstract async method_ `accept()` {#WebSocket-accept} + +- **说明:** 接受 WebSocket 连接请求 + +- **参数** + + empty + +- **返回** + + - None + +### _abstract async method_ `close(code=1000, reason="")` {#WebSocket-close} + +- **说明:** 关闭 WebSocket 连接请求 + +- **参数** + + - `code` (int) + + - `reason` (str) + +- **返回** + + - None + +### _abstract async method_ `receive()` {#WebSocket-receive} + +- **说明:** 接收一条 WebSocket text/bytes 信息 + +- **参数** + + empty + +- **返回** + + - str | bytes + +### _abstract async method_ `receive_text()` {#WebSocket-receive-text} + +- **说明:** 接收一条 WebSocket text 信息 + +- **参数** + + empty + +- **返回** + + - str + +### _abstract async method_ `receive_bytes()` {#WebSocket-receive-bytes} + +- **说明:** 接收一条 WebSocket binary 信息 + +- **参数** + + empty + +- **返回** + + - bytes + +### _async method_ `send(data)` {#WebSocket-send} + +- **说明:** 发送一条 WebSocket text/bytes 信息 + +- **参数** + + - `data` (str | bytes) + +- **返回** + + - None + +### _abstract async method_ `send_text(data)` {#WebSocket-send-text} + +- **说明:** 发送一条 WebSocket text 信息 + +- **参数** + + - `data` (str) + +- **返回** + + - None + +### _abstract async method_ `send_bytes(data)` {#WebSocket-send-bytes} + +- **说明:** 发送一条 WebSocket binary 信息 + +- **参数** + + - `data` (bytes) + +- **返回** + + - None + +## _enum_ `HTTPVersion` {#HTTPVersion} + +- **说明:** An enumeration. + +- **参数** + + auto + + - `H10: '1.0'` + + - `H11: '1.1'` + + - `H2: '2'` + +## _abstract class_ `ForwardMixin()` {#ForwardMixin} + +- **说明:** 客户端混入基类。 + +- **参数** + + auto + +## _abstract class_ `ReverseMixin()` {#ReverseMixin} + +- **说明:** 服务端混入基类。 + +- **参数** + + auto + +## _var_ `ForwardDriver` {#ForwardDriver} + +- **类型:** ForwardMixin + +- **说明** + + 支持客户端请求的驱动器。 + + **Deprecated**,请使用 [ForwardMixin](#ForwardMixin) 或其子类代替。 + +## _var_ `ReverseDriver` {#ReverseDriver} + +- **类型:** ReverseMixin + +- **说明** + + 支持服务端请求的驱动器。 + + **Deprecated**,请使用 [ReverseMixin](#ReverseMixin) 或其子类代替。 + +## _def_ `combine_driver(driver, *mixins)` {#combine-driver} + +- **说明:** 将一个驱动器和多个混入类合并。 + +- **重载** + + **1.** `(driver) -> type[D]` + + - **参数** + + - `driver` (type[D]) + + - **返回** + + - type[D] + + **2.** `(driver, __m, /, *mixins) -> type[CombinedDriver]` + + - **参数** + + - `driver` (type[D]) + + - `__m` (type[[Mixin](#Mixin)]) + + - `*mixins` (type[[Mixin](#Mixin)]) + + - **返回** + + - type[CombinedDriver] + +## _abstract class_ `HTTPClientMixin()` {#HTTPClientMixin} + +- **说明:** HTTP 客户端混入基类。 + +- **参数** + + auto + +### _abstract async method_ `request(setup)` {#HTTPClientMixin-request} + +- **说明:** 发送一个 HTTP 请求 + +- **参数** + + - `setup` ([Request](#Request)) + +- **返回** + + - [Response](#Response) + +### _abstract method_ `get_session(params=None, headers=None, cookies=None, version=HTTPVersion.H11, timeout=None, proxy=None)` {#HTTPClientMixin-get-session} + +- **说明:** 获取一个 HTTP 会话 + +- **参数** + + - `params` (QueryTypes) + + - `headers` (HeaderTypes) + + - `cookies` (CookieTypes) + + - `version` (str | [HTTPVersion](#HTTPVersion)) + + - `timeout` (float | None) + + - `proxy` (str | None) + +- **返回** + + - HTTPClientSession + +## _class_ `HTTPServerSetup()` {#HTTPServerSetup} + +- **说明:** HTTP 服务器路由配置。 + +- **参数** + + auto + +## _abstract class_ `WebSocketClientMixin()` {#WebSocketClientMixin} + +- **说明:** WebSocket 客户端混入基类。 + +- **参数** + + auto + +### _abstract method_ `websocket(setup)` {#WebSocketClientMixin-websocket} + +- **说明:** 发起一个 WebSocket 连接 + +- **参数** + + - `setup` ([Request](#Request)) + +- **返回** + + - AsyncGenerator[[WebSocket](#WebSocket), None] + +## _class_ `WebSocketServerSetup()` {#WebSocketServerSetup} + +- **说明:** WebSocket 服务器路由配置。 + +- **参数** + + auto diff --git a/website/versioned_docs/version-2.4.0/api/drivers/none.md b/website/versioned_docs/version-2.4.0/api/drivers/none.md new file mode 100644 index 00000000..5ef1c558 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/none.md @@ -0,0 +1,62 @@ +--- +mdx: + format: md +sidebar_position: 6 +description: nonebot.drivers.none 模块 +--- + +# nonebot.drivers.none + +None 驱动适配 + +:::tip 提示 +本驱动不支持任何服务器或客户端连接 +::: + +## _class_ `Driver(env, config)` {#Driver} + +- **说明:** None 驱动框架 + +- **参数** + + - `env` ([Env](../config.md#Env)) + + - `config` ([Config](../config.md#Config)) + +### _property_ `type` {#Driver-type} + +- **类型:** str + +- **说明:** 驱动名称: `none` + +### _property_ `logger` {#Driver-logger} + +- **类型:** untyped + +- **说明:** none driver 使用的 logger + +### _method_ `run(*args, **kwargs)` {#Driver-run} + +- **说明:** 启动 none driver + +- **参数** + + - `*args` + + - `**kwargs` + +- **返回** + + - untyped + +### _method_ `exit(force=False)` {#Driver-exit} + +- **说明:** 退出 none driver + +- **参数** + + - `force` (bool): 强制退出 + +- **返回** + + - untyped diff --git a/website/versioned_docs/version-2.4.0/api/drivers/quart.md b/website/versioned_docs/version-2.4.0/api/drivers/quart.md new file mode 100644 index 00000000..508366fe --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/quart.md @@ -0,0 +1,220 @@ +--- +mdx: + format: md +sidebar_position: 5 +description: nonebot.drivers.quart 模块 +--- + +# nonebot.drivers.quart + +[Quart](https://pgjones.gitlab.io/quart/index.html) 驱动适配 + +```bash +nb driver install quart +# 或者 +pip install nonebot2[quart] +``` + +:::tip 提示 +本驱动仅支持服务端连接 +::: + +## _class_ `Config()` {#Config} + +- **说明:** Quart 驱动框架设置 + +- **参数** + + auto + +### _class-var_ `quart_reload` {#Config-quart-reload} + +- **类型:** bool + +- **说明:** 开启/关闭冷重载 + +### _class-var_ `quart_reload_dirs` {#Config-quart-reload-dirs} + +- **类型:** list[str] | None + +- **说明:** 重载监控文件夹列表,默认为 uvicorn 默认值 + +### _class-var_ `quart_reload_delay` {#Config-quart-reload-delay} + +- **类型:** float + +- **说明:** 重载延迟,默认为 uvicorn 默认值 + +### _class-var_ `quart_reload_includes` {#Config-quart-reload-includes} + +- **类型:** list[str] | None + +- **说明:** 要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +### _class-var_ `quart_reload_excludes` {#Config-quart-reload-excludes} + +- **类型:** list[str] | None + +- **说明:** 不要监听的文件列表,支持 glob pattern,默认为 uvicorn 默认值 + +### _class-var_ `quart_extra` {#Config-quart-extra} + +- **类型:** dict[str, Any] + +- **说明:** 传递给 `Quart` 的其他参数。 + +## _class_ `Driver(env, config)` {#Driver} + +- **说明:** Quart 驱动框架 + +- **参数** + + - `env` ([Env](../config.md#Env)) + + - `config` (NoneBotConfig) + +### _property_ `type` {#Driver-type} + +- **类型:** str + +- **说明:** 驱动名称: `quart` + +### _property_ `server_app` {#Driver-server-app} + +- **类型:** Quart + +- **说明:** `Quart` 对象 + +### _property_ `asgi` {#Driver-asgi} + +- **类型:** untyped + +- **说明:** `Quart` 对象 + +### _property_ `logger` {#Driver-logger} + +- **类型:** untyped + +- **说明:** Quart 使用的 logger + +### _method_ `setup_http_server(setup)` {#Driver-setup-http-server} + +- **参数** + + - `setup` ([HTTPServerSetup](index.md#HTTPServerSetup)) + +- **返回** + + - untyped + +### _method_ `setup_websocket_server(setup)` {#Driver-setup-websocket-server} + +- **参数** + + - `setup` ([WebSocketServerSetup](index.md#WebSocketServerSetup)) + +- **返回** + + - None + +### _method_ `run(host=None, port=None, *args, app=None, **kwargs)` {#Driver-run} + +- **说明:** 使用 `uvicorn` 启动 Quart + +- **参数** + + - `host` (str | None) + + - `port` (int | None) + + - `*args` + + - `app` (str | None) + + - `**kwargs` + +- **返回** + + - untyped + +## _class_ `WebSocket(*, request, websocket_ctx)` {#WebSocket} + +- **说明:** Quart WebSocket Wrapper + +- **参数** + + - `request` (BaseRequest) + + - `websocket_ctx` (WebsocketContext) + +### _async method_ `accept()` {#WebSocket-accept} + +- **参数** + + empty + +- **返回** + + - untyped + +### _async method_ `close(code=1000, reason="")` {#WebSocket-close} + +- **参数** + + - `code` (int) + + - `reason` (str) + +- **返回** + + - untyped + +### _async method_ `receive()` {#WebSocket-receive} + +- **参数** + + empty + +- **返回** + + - str | bytes + +### _async method_ `receive_text()` {#WebSocket-receive-text} + +- **参数** + + empty + +- **返回** + + - str + +### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} + +- **参数** + + empty + +- **返回** + + - bytes + +### _async method_ `send_text(data)` {#WebSocket-send-text} + +- **参数** + + - `data` (str) + +- **返回** + + - untyped + +### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} + +- **参数** + + - `data` (bytes) + +- **返回** + + - untyped diff --git a/website/versioned_docs/version-2.4.0/api/drivers/websockets.md b/website/versioned_docs/version-2.4.0/api/drivers/websockets.md new file mode 100644 index 00000000..fc46e4d7 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/drivers/websockets.md @@ -0,0 +1,138 @@ +--- +mdx: + format: md +sidebar_position: 4 +description: nonebot.drivers.websockets 模块 +--- + +# nonebot.drivers.websockets + +[websockets](https://websockets.readthedocs.io/) 驱动适配 + +```bash +nb driver install websockets +# 或者 +pip install nonebot2[websockets] +``` + +:::tip 提示 +本驱动仅支持客户端 WebSocket 连接 +::: + +## _def_ `catch_closed(func)` {#catch-closed} + +- **参数** + + - `func` ((P) -> Coroutine[Any, Any, T]) + +- **返回** + + - (P) -> Coroutine[Any, Any, T] + +## _class_ `Mixin()` {#Mixin} + +- **说明:** Websockets Mixin + +- **参数** + + auto + +### _method_ `websocket(setup)` {#Mixin-websocket} + +- **参数** + + - `setup` ([Request](index.md#Request)) + +- **返回** + + - AsyncGenerator[[WebSocket](index.md#WebSocket), None] + +## _class_ `WebSocket(*, request, websocket)` {#WebSocket} + +- **说明:** Websockets WebSocket Wrapper + +- **参数** + + - `request` ([Request](index.md#Request)) + + - `websocket` (WebSocketClientProtocol) + +### _async method_ `accept()` {#WebSocket-accept} + +- **参数** + + empty + +- **返回** + + - untyped + +### _async method_ `close(code=1000, reason="")` {#WebSocket-close} + +- **参数** + + - `code` (int) + + - `reason` (str) + +- **返回** + + - untyped + +### _async method_ `receive()` {#WebSocket-receive} + +- **参数** + + empty + +- **返回** + + - str | bytes + +### _async method_ `receive_text()` {#WebSocket-receive-text} + +- **参数** + + empty + +- **返回** + + - str + +### _async method_ `receive_bytes()` {#WebSocket-receive-bytes} + +- **参数** + + empty + +- **返回** + + - bytes + +### _async method_ `send_text(data)` {#WebSocket-send-text} + +- **参数** + + - `data` (str) + +- **返回** + + - None + +### _async method_ `send_bytes(data)` {#WebSocket-send-bytes} + +- **参数** + + - `data` (bytes) + +- **返回** + + - None + +## _class_ `Driver(env, config)` {#Driver} + +- **参数** + + - `env` ([Env](../config.md#Env)) + + - `config` ([Config](../config.md#Config)) diff --git a/website/versioned_docs/version-2.4.0/api/exception.md b/website/versioned_docs/version-2.4.0/api/exception.md new file mode 100644 index 00000000..823332c4 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/exception.md @@ -0,0 +1,256 @@ +--- +mdx: + format: md +sidebar_position: 10 +description: nonebot.exception 模块 +--- + +# nonebot.exception + +本模块包含了所有 NoneBot 运行时可能会抛出的异常。 + +这些异常并非所有需要用户处理,在 NoneBot 内部运行时被捕获,并进行对应操作。 + +```bash +NoneBotException +├── ParserExit +├── ProcessException +| ├── IgnoredException +| ├── SkippedException +| | └── TypeMisMatch +| ├── MockApiException +| └── StopPropagation +├── MatcherException +| ├── PausedException +| ├── RejectedException +| └── FinishedException +├── AdapterException +| ├── NoLogException +| ├── ApiNotAvailable +| ├── NetworkError +| └── ActionFailed +└── DriverException + └── WebSocketClosed +``` + +## _class_ `NoneBotException()` {#NoneBotException} + +- **说明:** 所有 NoneBot 发生的异常基类。 + +- **参数** + + auto + +## _class_ `ParserExit()` {#ParserExit} + +- **说明:** 处理消息失败时返回的异常。 + +- **参数** + + auto + +## _class_ `ProcessException()` {#ProcessException} + +- **说明:** 事件处理过程中发生的异常基类。 + +- **参数** + + auto + +## _class_ `IgnoredException()` {#IgnoredException} + +- **说明:** 指示 NoneBot 应该忽略该事件。可由 PreProcessor 抛出。 + +- **参数** + + - `reason`: 忽略事件的原因 + +## _class_ `SkippedException()` {#SkippedException} + +- **说明** + + 指示 NoneBot 立即结束当前 `Dependent` 的运行。 + + 例如,可以在 `Handler` 中通过 [Matcher.skip](matcher.md#Matcher-skip) 抛出。 + +- **参数** + + auto + +- **用法** + + ```python + def always_skip(): + Matcher.skip() + + @matcher.handle() + async def handler(dependency = Depends(always_skip)): + # never run + ``` + +## _class_ `TypeMisMatch()` {#TypeMisMatch} + +- **说明:** 当前 `Handler` 的参数类型不匹配。 + +- **参数** + + auto + +## _class_ `MockApiException()` {#MockApiException} + +- **说明:** 指示 NoneBot 阻止本次 API 调用或修改本次调用返回值,并返回自定义内容。 可由 api hook 抛出。 + +- **参数** + + - `result`: 返回的内容 + +## _class_ `StopPropagation()` {#StopPropagation} + +- **说明** + + 指示 NoneBot 终止事件向下层传播。 + + 在 [Matcher.block](matcher.md#Matcher-block) 为 `True` + 或使用 [Matcher.stop_propagation](matcher.md#Matcher-stop-propagation) 方法时抛出。 + +- **参数** + + auto + +- **用法** + + ```python + matcher = on_notice(block=True) + # 或者 + @matcher.handle() + async def handler(matcher: Matcher): + matcher.stop_propagation() + ``` + +## _class_ `MatcherException()` {#MatcherException} + +- **说明:** 所有 Matcher 发生的异常基类。 + +- **参数** + + auto + +## _class_ `PausedException()` {#PausedException} + +- **说明** + + 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后继续下一个 `Handler`。 可用于用户输入新信息。 + + 可以在 `Handler` 中通过 [Matcher.pause](matcher.md#Matcher-pause) 抛出。 + +- **参数** + + auto + +- **用法** + + ```python + @matcher.handle() + async def handler(): + await matcher.pause("some message") + ``` + +## _class_ `RejectedException()` {#RejectedException} + +- **说明** + + 指示 NoneBot 结束当前 `Handler` 并等待下一条消息后重新运行当前 `Handler`。 可用于用户重新输入。 + + 可以在 `Handler` 中通过 [Matcher.reject](matcher.md#Matcher-reject) 抛出。 + +- **参数** + + auto + +- **用法** + + ```python + @matcher.handle() + async def handler(): + await matcher.reject("some message") + ``` + +## _class_ `FinishedException()` {#FinishedException} + +- **说明** + + 指示 NoneBot 结束当前 `Handler` 且后续 `Handler` 不再被运行。可用于结束用户会话。 + + 可以在 `Handler` 中通过 [Matcher.finish](matcher.md#Matcher-finish) 抛出。 + +- **参数** + + auto + +- **用法** + + ```python + @matcher.handle() + async def handler(): + await matcher.finish("some message") + ``` + +## _class_ `AdapterException()` {#AdapterException} + +- **说明:** 代表 `Adapter` 抛出的异常,所有的 `Adapter` 都要在内部继承自这个 `Exception`。 + +- **参数** + + - `adapter_name`: 标识 adapter + +## _class_ `NoLogException()` {#NoLogException} + +- **说明** + + 指示 NoneBot 对当前 `Event` 进行处理但不显示 Log 信息。 + + 可在 [Event.get_log_string](adapters/index.md#Event-get-log-string) 时抛出 + +- **参数** + + auto + +## _class_ `ApiNotAvailable()` {#ApiNotAvailable} + +- **说明:** 在 API 连接不可用时抛出。 + +- **参数** + + auto + +## _class_ `NetworkError()` {#NetworkError} + +- **说明:** 在网络出现问题时抛出, 如: API 请求地址不正确, API 请求无返回或返回状态非正常等。 + +- **参数** + + auto + +## _class_ `ActionFailed()` {#ActionFailed} + +- **说明:** API 请求成功返回数据,但 API 操作失败。 + +- **参数** + + auto + +## _class_ `DriverException()` {#DriverException} + +- **说明:** `Driver` 抛出的异常基类。 + +- **参数** + + auto + +## _class_ `WebSocketClosed()` {#WebSocketClosed} + +- **说明:** WebSocket 连接已关闭。 + +- **参数** + + auto diff --git a/website/versioned_docs/version-2.4.0/api/index.md b/website/versioned_docs/version-2.4.0/api/index.md new file mode 100644 index 00000000..aecfab21 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/index.md @@ -0,0 +1,286 @@ +--- +mdx: + format: md +sidebar_position: 0 +description: nonebot 模块 +--- + +# nonebot + +本模块主要定义了 NoneBot 启动所需函数,供 bot 入口文件调用。 + +## 快捷导入 + +为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: + +- `on` => [`on`](plugin/on.md#on) +- `on_metaevent` => [`on_metaevent`](plugin/on.md#on-metaevent) +- `on_message` => [`on_message`](plugin/on.md#on-message) +- `on_notice` => [`on_notice`](plugin/on.md#on-notice) +- `on_request` => [`on_request`](plugin/on.md#on-request) +- `on_startswith` => [`on_startswith`](plugin/on.md#on-startswith) +- `on_endswith` => [`on_endswith`](plugin/on.md#on-endswith) +- `on_fullmatch` => [`on_fullmatch`](plugin/on.md#on-fullmatch) +- `on_keyword` => [`on_keyword`](plugin/on.md#on-keyword) +- `on_command` => [`on_command`](plugin/on.md#on-command) +- `on_shell_command` => [`on_shell_command`](plugin/on.md#on-shell-command) +- `on_regex` => [`on_regex`](plugin/on.md#on-regex) +- `on_type` => [`on_type`](plugin/on.md#on-type) +- `CommandGroup` => [`CommandGroup`](plugin/on.md#CommandGroup) +- `Matchergroup` => [`MatcherGroup`](plugin/on.md#MatcherGroup) +- `load_plugin` => [`load_plugin`](plugin/load.md#load-plugin) +- `load_plugins` => [`load_plugins`](plugin/load.md#load-plugins) +- `load_all_plugins` => [`load_all_plugins`](plugin/load.md#load-all-plugins) +- `load_from_json` => [`load_from_json`](plugin/load.md#load-from-json) +- `load_from_toml` => [`load_from_toml`](plugin/load.md#load-from-toml) +- `load_builtin_plugin` => + [`load_builtin_plugin`](plugin/load.md#load-builtin-plugin) +- `load_builtin_plugins` => + [`load_builtin_plugins`](plugin/load.md#load-builtin-plugins) +- `get_plugin` => [`get_plugin`](plugin/index.md#get-plugin) +- `get_plugin_by_module_name` => + [`get_plugin_by_module_name`](plugin/index.md#get-plugin-by-module-name) +- `get_loaded_plugins` => + [`get_loaded_plugins`](plugin/index.md#get-loaded-plugins) +- `get_available_plugin_names` => + [`get_available_plugin_names`](plugin/index.md#get-available-plugin-names) +- `get_plugin_config` => [`get_plugin_config`](plugin/index.md#get-plugin-config) +- `require` => [`require`](plugin/load.md#require) + +## _def_ `get_driver()` {#get-driver} + +- **说明** + + 获取全局 [Driver](drivers/index.md#Driver) 实例。 + + 可用于在计划任务的回调等情形中获取当前 [Driver](drivers/index.md#Driver) 实例。 + +- **参数** + + empty + +- **返回** + + - [Driver](drivers/index.md#Driver): 全局 [Driver](drivers/index.md#Driver) 对象 + +- **异常** + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + driver = nonebot.get_driver() + ``` + +## _def_ `get_adapter(name)` {#get-adapter} + +- **说明:** 获取已注册的 [Adapter](adapters/index.md#Adapter) 实例。 + +- **重载** + + **1.** `(name) -> Adapter` + + - **参数** + + - `name` (str): 适配器名称 + + - **返回** + + - [Adapter](adapters/index.md#Adapter): 指定名称的 [Adapter](adapters/index.md#Adapter) 对象 + + **2.** `(name) -> A` + + - **参数** + + - `name` (type[A]): 适配器类型 + + - **返回** + + - A: 指定类型的 [Adapter](adapters/index.md#Adapter) 对象 + +- **异常** + + - ValueError: 指定的 [Adapter](adapters/index.md#Adapter) 未注册 + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + from nonebot.adapters.console import Adapter + adapter = nonebot.get_adapter(Adapter) + ``` + +## _def_ `get_adapters()` {#get-adapters} + +- **说明:** 获取所有已注册的 [Adapter](adapters/index.md#Adapter) 实例。 + +- **参数** + + empty + +- **返回** + + - dict[str, [Adapter](adapters/index.md#Adapter)]: 所有 [Adapter](adapters/index.md#Adapter) 实例字典 + +- **异常** + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + adapters = nonebot.get_adapters() + ``` + +## _def_ `get_app()` {#get-app} + +- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 Server App 对象。 + +- **参数** + + empty + +- **返回** + + - Any: Server App 对象 + +- **异常** + + - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + app = nonebot.get_app() + ``` + +## _def_ `get_asgi()` {#get-asgi} + +- **说明:** 获取全局 [ASGIMixin](drivers/index.md#ASGIMixin) 对应的 [ASGI](https://asgi.readthedocs.io/) 对象。 + +- **参数** + + empty + +- **返回** + + - Any: ASGI 对象 + +- **异常** + + - AssertionError: 全局 Driver 对象不是 [ASGIMixin](drivers/index.md#ASGIMixin) 类型 + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + asgi = nonebot.get_asgi() + ``` + +## _def_ `get_bot(self_id=None)` {#get-bot} + +- **说明** + + 获取一个连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 + + 当提供 `self_id` 时,此函数是 `get_bots()[self_id]` 的简写; + 当不提供时,返回一个 [Bot](adapters/index.md#Bot)。 + +- **参数** + + - `self_id` (str | None): 用来识别 [Bot](adapters/index.md#Bot) 的 [Bot.self_id](adapters/index.md#Bot-self-id) 属性 + +- **返回** + + - [Bot](adapters/index.md#Bot): [Bot](adapters/index.md#Bot) 对象 + +- **异常** + + - KeyError: 对应 self_id 的 Bot 不存在 + + - ValueError: 没有传入 self_id 且没有 Bot 可用 + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + assert nonebot.get_bot("12345") == nonebot.get_bots()["12345"] + + another_unspecified_bot = nonebot.get_bot() + ``` + +## _def_ `get_bots()` {#get-bots} + +- **说明:** 获取所有连接到 NoneBot 的 [Bot](adapters/index.md#Bot) 对象。 + +- **参数** + + empty + +- **返回** + + - dict[str, [Bot](adapters/index.md#Bot)]: 一个以 [Bot.self_id](adapters/index.md#Bot-self-id) 为键 + + [Bot](adapters/index.md#Bot) 对象为值的字典 + +- **异常** + + - ValueError: 全局 [Driver](drivers/index.md#Driver) 对象尚未初始化 ([nonebot.init](#init) 尚未调用) + +- **用法** + + ```python + bots = nonebot.get_bots() + ``` + +## _def_ `init(*, _env_file=None, **kwargs)` {#init} + +- **说明** + + 初始化 NoneBot 以及 全局 [Driver](drivers/index.md#Driver) 对象。 + + NoneBot 将会从 .env 文件中读取环境信息,并使用相应的 env 文件配置。 + + 也可以传入自定义的 `_env_file` 来指定 NoneBot 从该文件读取配置。 + +- **参数** + + - `_env_file` (DOTENV_TYPE | None): 配置文件名,默认从 `.env.{env_name}` 中读取配置 + + - `**kwargs` (Any): 任意变量,将会存储到 [Driver.config](drivers/index.md#Driver-config) 对象里 + +- **返回** + + - None + +- **用法** + + ```python + nonebot.init(database=Database(...)) + ``` + +## _def_ `run(*args, **kwargs)` {#run} + +- **说明:** 启动 NoneBot,即运行全局 [Driver](drivers/index.md#Driver) 对象。 + +- **参数** + + - `*args` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的位置参数 + + - `**kwargs` (Any): 传入 [Driver.run](drivers/index.md#Driver-run) 的命名参数 + +- **返回** + + - None + +- **用法** + + ```python + nonebot.run(host="127.0.0.1", port=8080) + ``` diff --git a/website/versioned_docs/version-2.4.0/api/log.md b/website/versioned_docs/version-2.4.0/api/log.md new file mode 100644 index 00000000..18bf08d6 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/log.md @@ -0,0 +1,73 @@ +--- +mdx: + format: md +sidebar_position: 7 +description: nonebot.log 模块 +--- + +# nonebot.log + +本模块定义了 NoneBot 的日志记录 Logger。 + +NoneBot 使用 [`loguru`][loguru] 来记录日志信息。 + +自定义 logger 请参考 [自定义日志](https://nonebot.dev/docs/appendices/log) +以及 [`loguru`][loguru] 文档。 + +[loguru]: https://github.com/Delgan/loguru + +## _var_ `logger` {#logger} + +- **类型:** Logger + +- **说明** + + NoneBot 日志记录器对象。 + + 默认信息: + + - 格式: `[%(asctime)s %(name)s] %(levelname)s: %(message)s` + - 等级: `INFO` ,根据 `config.log_level` 配置改变 + - 输出: 输出至 stdout + +- **用法** + + ```python + from nonebot.log import logger + ``` + +## _class_ `LoguruHandler()` {#LoguruHandler} + +- **说明:** logging 与 loguru 之间的桥梁,将 logging 的日志转发到 loguru。 + +- **参数** + + auto + +### _method_ `emit(record)` {#LoguruHandler-emit} + +- **参数** + + - `record` (logging.LogRecord) + +- **返回** + + - untyped + +## _def_ `default_filter(record)` {#default-filter} + +- **说明:** 默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。 + +- **参数** + + - `record` (Record) + +- **返回** + + - untyped + +## _var_ `default_format` {#default-format} + +- **类型:** str + +- **说明:** 默认日志格式 diff --git a/website/versioned_docs/version-2.4.0/api/matcher.md b/website/versioned_docs/version-2.4.0/api/matcher.md new file mode 100644 index 00000000..dd15760c --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/matcher.md @@ -0,0 +1,751 @@ +--- +mdx: + format: md +sidebar_position: 3 +description: nonebot.matcher 模块 +--- + +# nonebot.matcher + +本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。 + +## _class_ `Matcher()` {#Matcher} + +- **说明:** 事件响应器类 + +- **参数** + + empty + +### _class-var_ `type` {#Matcher-type} + +- **类型:** ClassVar[str] + +- **说明:** 事件响应器类型 + +### _class-var_ `rule` {#Matcher-rule} + +- **类型:** ClassVar[[Rule](rule.md#Rule)] + +- **说明:** 事件响应器匹配规则 + +### _class-var_ `permission` {#Matcher-permission} + +- **类型:** ClassVar[[Permission](permission.md#Permission)] + +- **说明:** 事件响应器触发权限 + +### _class-var_ `handlers` {#Matcher-handlers} + +- **类型:** ClassVar[list[[Dependent](dependencies/index.md#Dependent)[Any]]] + +- **说明:** 事件响应器拥有的事件处理函数列表 + +### _class-var_ `priority` {#Matcher-priority} + +- **类型:** ClassVar[int] + +- **说明:** 事件响应器优先级 + +### _class-var_ `block` {#Matcher-block} + +- **类型:** bool + +- **说明:** 事件响应器是否阻止事件传播 + +### _class-var_ `temp` {#Matcher-temp} + +- **类型:** ClassVar[bool] + +- **说明:** 事件响应器是否为临时 + +### _class-var_ `expire_time` {#Matcher-expire-time} + +- **类型:** ClassVar[datetime | None] + +- **说明:** 事件响应器过期时间点 + +### _classmethod_ `new(type_="", rule=None, permission=None, handlers=None, temp=False, priority=1, block=False, *, plugin=None, module=None, source=None, expire_time=None, default_state=None, default_type_updater=None, default_permission_updater=None)` {#Matcher-new} + +- **说明:** 创建一个新的事件响应器,并存储至 `matchers <#matchers>`\_ + +- **参数** + + - `type_` (str): 事件响应器类型,与 `event.get_type()` 一致时触发,空字符串表示任意 + + - `rule` ([Rule](rule.md#Rule) | None): 匹配规则 + + - `permission` ([Permission](permission.md#Permission) | None): 权限 + + - `handlers` (list[[T\_Handler](typing.md#T-Handler) | [Dependent](dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器,即触发一次后删除 + + - `priority` (int): 响应优先级 + + - `block` (bool): 是否阻止事件向更低优先级的响应器传播 + + - `plugin` ([Plugin](plugin/model.md#Plugin) | None): **Deprecated.** 事件响应器所在插件 + + - `module` (ModuleType | None): **Deprecated.** 事件响应器所在模块 + + - `source` (MatcherSource | None): 事件响应器源代码上下文信息 + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `default_state` ([T_State](typing.md#T-State) | None): 默认状态 `state` + + - `default_type_updater` ([T_TypeUpdater](typing.md#T-TypeUpdater) | [Dependent](dependencies/index.md#Dependent)[str] | None): 默认事件类型更新函数 + + - `default_permission_updater` ([T_PermissionUpdater](typing.md#T-PermissionUpdater) | [Dependent](dependencies/index.md#Dependent)[[Permission](permission.md#Permission)] | None): 默认会话权限更新函数 + +- **返回** + + - type[Matcher]: 新的事件响应器类 + +### _classmethod_ `destroy()` {#Matcher-destroy} + +- **说明:** 销毁当前的事件响应器 + +- **参数** + + empty + +- **返回** + + - None + +### _classmethod_ `check_perm(bot, event, stack=None, dependency_cache=None)` {#Matcher-check-perm} + +- **说明:** 检查是否满足触发权限 + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 + + - `event` ([Event](adapters/index.md#Event)): 上报事件 + + - `stack` (AsyncExitStack | None): 异步上下文栈 + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 + +- **返回** + + - bool: 是否满足权限 + +### _classmethod_ `check_rule(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-check-rule} + +- **说明:** 检查是否满足匹配规则 + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 + + - `event` ([Event](adapters/index.md#Event)): 上报事件 + + - `state` ([T_State](typing.md#T-State)): 当前状态 + + - `stack` (AsyncExitStack | None): 异步上下文栈 + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 + +- **返回** + + - bool: 是否满足匹配规则 + +### _classmethod_ `type_updater(func)` {#Matcher-type-updater} + +- **说明:** 装饰一个函数来更改当前事件响应器的默认响应事件类型更新函数 + +- **参数** + + - `func` ([T_TypeUpdater](typing.md#T-TypeUpdater)): 响应事件类型更新函数 + +- **返回** + + - [T_TypeUpdater](typing.md#T-TypeUpdater) + +### _classmethod_ `permission_updater(func)` {#Matcher-permission-updater} + +- **说明:** 装饰一个函数来更改当前事件响应器的默认会话权限更新函数 + +- **参数** + + - `func` ([T_PermissionUpdater](typing.md#T-PermissionUpdater)): 会话权限更新函数 + +- **返回** + + - [T_PermissionUpdater](typing.md#T-PermissionUpdater) + +### _classmethod_ `append_handler(handler, parameterless=None)` {#Matcher-append-handler} + +- **参数** + + - `handler` ([T_Handler](typing.md#T-Handler)) + + - `parameterless` (Iterable[Any] | None) + +- **返回** + + - [Dependent](dependencies/index.md#Dependent)[Any] + +### _classmethod_ `handle(parameterless=None)` {#Matcher-handle} + +- **说明:** 装饰一个函数来向事件响应器直接添加一个处理函数 + +- **参数** + + - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 + +- **返回** + + - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) + +### _classmethod_ `receive(id="", parameterless=None)` {#Matcher-receive} + +- **说明:** 装饰一个函数来指示 NoneBot 在接收用户新的一条消息后继续运行该函数 + +- **参数** + + - `id` (str): 消息 ID + + - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 + +- **返回** + + - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) + +### _classmethod_ `got(key, prompt=None, parameterless=None)` {#Matcher-got} + +- **说明** + + 装饰一个函数来指示 NoneBot 获取一个参数 `key` + + 当要获取的 `key` 不存在时接收用户新的一条消息再运行该函数, + 如果 `key` 已存在则直接继续运行 + +- **参数** + + - `key` (str): 参数名 + + - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 在参数不存在时向用户发送的消息 + + - `parameterless` (Iterable[Any] | None): 非参数类型依赖列表 + +- **返回** + + - ([T_Handler](typing.md#T-Handler)) -> [T_Handler](typing.md#T-Handler) + +### _classmethod_ `send(message, **kwargs)` {#Matcher-send} + +- **说明:** 发送一条消息给当前交互用户 + +- **参数** + + - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate)): 消息内容 + + - `**kwargs` (Any): [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api + +- **返回** + + - Any + +### _classmethod_ `finish(message=None, **kwargs)` {#Matcher-finish} + +- **说明:** 发送一条消息给当前交互用户并结束当前事件响应器 + +- **参数** + + - `message` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 + + - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api + +- **返回** + + - NoReturn + +### _classmethod_ `pause(prompt=None, **kwargs)` {#Matcher-pause} + +- **说明:** 发送一条消息给当前交互用户并暂停事件响应器,在接收用户新的一条消息后继续下一个处理函数 + +- **参数** + + - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 + + - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api + +- **返回** + + - NoReturn + +### _classmethod_ `reject(prompt=None, **kwargs)` {#Matcher-reject} + +- **说明:** 最近使用 `got` / `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 + +- **参数** + + - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 + + - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api + +- **返回** + + - NoReturn + +### _classmethod_ `reject_arg(key, prompt=None, **kwargs)` {#Matcher-reject-arg} + +- **说明:** 最近使用 `got` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一条消息后从头开始执行当前处理函数 + +- **参数** + + - `key` (str): 参数名 + + - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 + + - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api + +- **返回** + + - NoReturn + +### _classmethod_ `reject_receive(id="", prompt=None, **kwargs)` {#Matcher-reject-receive} + +- **说明:** 最近使用 `receive` 接收的消息不符合预期, 发送一条消息给当前交互用户并将当前事件处理流程中断在当前位置,在接收用户新的一个事件后从头开始执行当前处理函数 + +- **参数** + + - `id` (str): 消息 id + + - `prompt` (str | [Message](adapters/index.md#Message) | [MessageSegment](adapters/index.md#MessageSegment) | [MessageTemplate](adapters/index.md#MessageTemplate) | None): 消息内容 + + - `**kwargs`: [Bot.send](adapters/index.md#Bot-send) 的参数, 请参考对应 adapter 的 bot 对象 api + +- **返回** + + - NoReturn + +### _classmethod_ `skip()` {#Matcher-skip} + +- **说明** + + 跳过当前事件处理函数,继续下一个处理函数 + + 通常在事件处理函数的依赖中使用。 + +- **参数** + + empty + +- **返回** + + - NoReturn + +### _method_ `get_receive(id, default=None)` {#Matcher-get-receive} + +- **说明** + + 获取一个 `receive` 事件 + + 如果没有找到对应的事件,返回 `default` 值 + +- **重载** + + **1.** `(id) -> Event | None` + + - **参数** + + - `id` (str) + + - **返回** + + - [Event](adapters/index.md#Event) | None + + **2.** `(id, default) -> Event | T` + + - **参数** + + - `id` (str) + + - `default` (T) + + - **返回** + + - [Event](adapters/index.md#Event) | T + +### _method_ `set_receive(id, event)` {#Matcher-set-receive} + +- **说明:** 设置一个 `receive` 事件 + +- **参数** + + - `id` (str) + + - `event` ([Event](adapters/index.md#Event)) + +- **返回** + + - None + +### _method_ `get_last_receive(default=None)` {#Matcher-get-last-receive} + +- **说明** + + 获取最近一次 `receive` 事件 + + 如果没有事件,返回 `default` 值 + +- **重载** + + **1.** `() -> Event | None` + + - **参数** + + empty + + - **返回** + + - [Event](adapters/index.md#Event) | None + + **2.** `(default) -> Event | T` + + - **参数** + + - `default` (T) + + - **返回** + + - [Event](adapters/index.md#Event) | T + +### _method_ `get_arg(key, default=None)` {#Matcher-get-arg} + +- **说明** + + 获取一个 `got` 消息 + + 如果没有找到对应的消息,返回 `default` 值 + +- **重载** + + **1.** `(key) -> Message | None` + + - **参数** + + - `key` (str) + + - **返回** + + - [Message](adapters/index.md#Message) | None + + **2.** `(key, default) -> Message | T` + + - **参数** + + - `key` (str) + + - `default` (T) + + - **返回** + + - [Message](adapters/index.md#Message) | T + +### _method_ `set_arg(key, message)` {#Matcher-set-arg} + +- **说明:** 设置一个 `got` 消息 + +- **参数** + + - `key` (str) + + - `message` ([Message](adapters/index.md#Message)) + +- **返回** + + - None + +### _method_ `set_target(target, cache=True)` {#Matcher-set-target} + +- **参数** + + - `target` (str) + + - `cache` (bool) + +- **返回** + + - None + +### _method_ `get_target(default=None)` {#Matcher-get-target} + +- **重载** + + **1.** `() -> str | None` + + - **参数** + + empty + + - **返回** + + - str | None + + **2.** `(default) -> str | T` + + - **参数** + + - `default` (T) + + - **返回** + + - str | T + +### _method_ `stop_propagation()` {#Matcher-stop-propagation} + +- **说明:** 阻止事件传播 + +- **参数** + + empty + +- **返回** + + - untyped + +### _async method_ `update_type(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-type} + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)) + + - `event` ([Event](adapters/index.md#Event)) + + - `stack` (AsyncExitStack | None) + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) + +- **返回** + + - str + +### _async method_ `update_permission(bot, event, stack=None, dependency_cache=None)` {#Matcher-update-permission} + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)) + + - `event` ([Event](adapters/index.md#Event)) + + - `stack` (AsyncExitStack | None) + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) + +- **返回** + + - [Permission](permission.md#Permission) + +### _async method_ `resolve_reject()` {#Matcher-resolve-reject} + +- **参数** + + empty + +- **返回** + + - untyped + +### _method_ `ensure_context(bot, event)` {#Matcher-ensure-context} + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)) + + - `event` ([Event](adapters/index.md#Event)) + +- **返回** + + - untyped + +### _async method_ `simple_run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-simple-run} + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)) + + - `event` ([Event](adapters/index.md#Event)) + + - `state` ([T_State](typing.md#T-State)) + + - `stack` (AsyncExitStack | None) + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) + +- **返回** + + - untyped + +### _async method_ `run(bot, event, state, stack=None, dependency_cache=None)` {#Matcher-run} + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)) + + - `event` ([Event](adapters/index.md#Event)) + + - `state` ([T_State](typing.md#T-State)) + + - `stack` (AsyncExitStack | None) + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None) + +- **返回** + + - untyped + +## _var_ `matchers` {#matchers} + +- **类型:** untyped + +## _class_ `MatcherManager()` {#MatcherManager} + +- **说明** + + 事件响应器管理器 + + 实现了常用字典操作,用于管理事件响应器。 + +- **参数** + + empty + +### _method_ `keys()` {#MatcherManager-keys} + +- **参数** + + empty + +- **返回** + + - KeysView[int] + +### _method_ `values()` {#MatcherManager-values} + +- **参数** + + empty + +- **返回** + + - ValuesView[list[type[[Matcher](#Matcher)]]] + +### _method_ `items()` {#MatcherManager-items} + +- **参数** + + empty + +- **返回** + + - ItemsView[int, list[type[[Matcher](#Matcher)]]] + +### _method_ `get(key, default=None)` {#MatcherManager-get} + +- **重载** + + **1.** `(key) -> list[type[Matcher]] | None` + + - **参数** + + - `key` (int) + + - **返回** + + - list[type[[Matcher](#Matcher)]] | None + + **2.** `(key, default) -> list[type[Matcher]] | T` + + - **参数** + + - `key` (int) + + - `default` (T) + + - **返回** + + - list[type[[Matcher](#Matcher)]] | T + +### _method_ `pop(key)` {#MatcherManager-pop} + +- **参数** + + - `key` (int) + +- **返回** + + - list[type[[Matcher](#Matcher)]] + +### _method_ `popitem()` {#MatcherManager-popitem} + +- **参数** + + empty + +- **返回** + + - tuple[int, list[type[[Matcher](#Matcher)]]] + +### _method_ `clear()` {#MatcherManager-clear} + +- **参数** + + empty + +- **返回** + + - None + +### _method_ `update(__m)` {#MatcherManager-update} + +- **参数** + + - `__m` (MutableMapping[int, list[type[[Matcher](#Matcher)]]]) + +- **返回** + + - None + +### _method_ `setdefault(key, default)` {#MatcherManager-setdefault} + +- **参数** + + - `key` (int) + + - `default` (list[type[[Matcher](#Matcher)]]) + +- **返回** + + - list[type[[Matcher](#Matcher)]] + +### _method_ `set_provider(provider_class)` {#MatcherManager-set-provider} + +- **说明:** 设置事件响应器存储器 + +- **参数** + + - `provider_class` (type[[MatcherProvider](#MatcherProvider)]): 事件响应器存储器类 + +- **返回** + + - None + +## _abstract class_ `MatcherProvider(matchers)` {#MatcherProvider} + +- **说明:** 事件响应器存储器基类 + +- **参数** + + - `matchers` (Mapping[int, list[type[[Matcher](#Matcher)]]]): 当前存储器中已有的事件响应器 + +## _var_ `DEFAULT_PROVIDER_CLASS` {#DEFAULT-PROVIDER-CLASS} + +- **类型:** untyped + +- **说明:** 默认存储器类型 diff --git a/website/versioned_docs/version-2.4.0/api/message.md b/website/versioned_docs/version-2.4.0/api/message.md new file mode 100644 index 00000000..73622df3 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/message.md @@ -0,0 +1,118 @@ +--- +mdx: + format: md +sidebar_position: 2 +description: nonebot.message 模块 +--- + +# nonebot.message + +本模块定义了事件处理主要流程。 + +NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。 + +## _def_ `event_preprocessor(func)` {#event-preprocessor} + +- **说明** + + 事件预处理。 + + 装饰一个函数,使它在每次接收到事件并分发给各响应器之前执行。 + +- **参数** + + - `func` ([T_EventPreProcessor](typing.md#T-EventPreProcessor)) + +- **返回** + + - [T_EventPreProcessor](typing.md#T-EventPreProcessor) + +## _def_ `event_postprocessor(func)` {#event-postprocessor} + +- **说明** + + 事件后处理。 + + 装饰一个函数,使它在每次接收到事件并分发给各响应器之后执行。 + +- **参数** + + - `func` ([T_EventPostProcessor](typing.md#T-EventPostProcessor)) + +- **返回** + + - [T_EventPostProcessor](typing.md#T-EventPostProcessor) + +## _def_ `run_preprocessor(func)` {#run-preprocessor} + +- **说明** + + 运行预处理。 + + 装饰一个函数,使它在每次事件响应器运行前执行。 + +- **参数** + + - `func` ([T_RunPreProcessor](typing.md#T-RunPreProcessor)) + +- **返回** + + - [T_RunPreProcessor](typing.md#T-RunPreProcessor) + +## _def_ `run_postprocessor(func)` {#run-postprocessor} + +- **说明** + + 运行后处理。 + + 装饰一个函数,使它在每次事件响应器运行后执行。 + +- **参数** + + - `func` ([T_RunPostProcessor](typing.md#T-RunPostProcessor)) + +- **返回** + + - [T_RunPostProcessor](typing.md#T-RunPostProcessor) + +## _async def_ `check_and_run_matcher(Matcher, bot, event, state, stack=None, dependency_cache=None)` {#check-and-run-matcher} + +- **说明:** 检查并运行事件响应器。 + +- **参数** + + - `Matcher` (type[[Matcher](matcher.md#Matcher)]): 事件响应器 + + - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 + + - `event` ([Event](adapters/index.md#Event)): Event 对象 + + - `state` ([T_State](typing.md#T-State)): 会话状态 + + - `stack` (AsyncExitStack | None): 异步上下文栈 + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 + +- **返回** + + - None + +## _async def_ `handle_event(bot, event)` {#handle-event} + +- **说明:** 处理一个事件。调用该函数以实现分发事件。 + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 + + - `event` ([Event](adapters/index.md#Event)): Event 对象 + +- **返回** + + - None + +- **用法** + + ```python + driver.task_group.start_soon(handle_event, bot, event) + ``` diff --git a/website/versioned_docs/version-2.4.0/api/params.md b/website/versioned_docs/version-2.4.0/api/params.md new file mode 100644 index 00000000..44879000 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/params.md @@ -0,0 +1,512 @@ +--- +mdx: + format: md +sidebar_position: 4 +description: nonebot.params 模块 +--- + +# nonebot.params + +本模块定义了依赖注入的各类参数。 + +## _def_ `Arg(key=None)` {#Arg} + +- **说明:** Arg 参数消息 + +- **参数** + + - `key` (str | None) + +- **返回** + + - Any + +## _def_ `ArgStr(key=None)` {#ArgStr} + +- **说明:** Arg 参数消息文本 + +- **参数** + + - `key` (str | None) + +- **返回** + + - str + +## _def_ `Depends(dependency=None, *, use_cache=True, validate=False)` {#Depends} + +- **说明:** 子依赖装饰器 + +- **参数** + + - `dependency` ([T_Handler](typing.md#T-Handler) | None): 依赖函数。默认为参数的类型注释。 + + - `use_cache` (bool): 是否使用缓存。默认为 `True`。 + + - `validate` (bool | PydanticFieldInfo): 是否使用 Pydantic 类型校验。默认为 `False`。 + +- **返回** + + - Any + +- **用法** + + ```python + def depend_func() -> Any: + return ... + + def depend_gen_func(): + try: + yield ... + finally: + ... + + async def handler( + param_name: Any = Depends(depend_func), + gen: Any = Depends(depend_gen_func), + ): + ... + ``` + +## _class_ `ArgParam(*args, key, type, **kwargs)` {#ArgParam} + +- **说明** + + Arg 注入参数 + + 本注入解析事件响应器操作 `got` 所获取的参数。 + + 可以通过 `Arg`、`ArgStr`、`ArgPlainText` 等函数参数 `key` 指定获取的参数, + 留空则会根据参数名称获取。 + +- **参数** + + - `*args` + + - `key` (str) + + - `type` (Literal['message', 'str', 'plaintext']) + + - `**kwargs` (Any) + +## _class_ `BotParam(*args, checker=None, **kwargs)` {#BotParam} + +- **说明** + + 注入参数。 + + 本注入解析所有类型为且仅为 [Bot](adapters/index.md#Bot) 及其子类或 `None` 的参数。 + + 为保证兼容性,本注入还会解析名为 `bot` 且没有类型注解的参数。 + +- **参数** + + - `*args` + + - `checker` ([ModelField](compat.md#ModelField) | None) + + - `**kwargs` (Any) + +## _class_ `EventParam(*args, checker=None, **kwargs)` {#EventParam} + +- **说明** + + 注入参数 + + 本注入解析所有类型为且仅为 [Event](adapters/index.md#Event) 及其子类或 `None` 的参数。 + + 为保证兼容性,本注入还会解析名为 `event` 且没有类型注解的参数。 + +- **参数** + + - `*args` + + - `checker` ([ModelField](compat.md#ModelField) | None) + + - `**kwargs` (Any) + +## _class_ `StateParam(*args, validate=False, **kwargs)` {#StateParam} + +- **说明** + + 事件处理状态注入参数 + + 本注入解析所有类型为 `T_State` 的参数。 + + 为保证兼容性,本注入还会解析名为 `state` 且没有类型注解的参数。 + +- **参数** + + - `*args` + + - `validate` (bool) + + - `**kwargs` (Any) + +## _class_ `DependParam(*args, dependent, use_cache, **kwargs)` {#DependParam} + +- **说明** + + 子依赖注入参数。 + + 本注入解析所有子依赖注入,然后将它们的返回值作为参数值传递给父依赖。 + + 本注入应该具有最高优先级,因此应该在其他参数之前检查。 + +- **参数** + + - `*args` + + - `dependent` ([Dependent](dependencies/index.md#Dependent)[Any]) + + - `use_cache` (bool) + + - `**kwargs` (Any) + +## _def_ `ArgPlainText(key=None)` {#ArgPlainText} + +- **说明:** Arg 参数消息纯文本 + +- **参数** + + - `key` (str | None) + +- **返回** + + - str + +## _class_ `DefaultParam(*args, validate=False, **kwargs)` {#DefaultParam} + +- **说明** + + 默认值注入参数 + + 本注入解析所有剩余未能解析且具有默认值的参数。 + + 本注入参数应该具有最低优先级,因此应该在所有其他注入参数之后使用。 + +- **参数** + + - `*args` + + - `validate` (bool) + + - `**kwargs` (Any) + +## _class_ `MatcherParam(*args, checker=None, **kwargs)` {#MatcherParam} + +- **说明** + + 事件响应器实例注入参数 + + 本注入解析所有类型为且仅为 [Matcher](matcher.md#Matcher) 及其子类或 `None` 的参数。 + + 为保证兼容性,本注入还会解析名为 `matcher` 且没有类型注解的参数。 + +- **参数** + + - `*args` + + - `checker` ([ModelField](compat.md#ModelField) | None) + + - `**kwargs` (Any) + +## _class_ `ExceptionParam(*args, validate=False, **kwargs)` {#ExceptionParam} + +- **说明** + + 的异常注入参数 + + 本注入解析所有类型为 `Exception` 或 `None` 的参数。 + + 为保证兼容性,本注入还会解析名为 `exception` 且没有类型注解的参数。 + +- **参数** + + - `*args` + + - `validate` (bool) + + - `**kwargs` (Any) + +## _def_ `EventType()` {#EventType} + +- **说明:** 类型参数 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `EventMessage()` {#EventMessage} + +- **说明:** 消息参数 + +- **参数** + + empty + +- **返回** + + - Any + +## _def_ `EventPlainText()` {#EventPlainText} + +- **说明:** 纯文本消息参数 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `EventToMe()` {#EventToMe} + +- **说明:** `to_me` 参数 + +- **参数** + + empty + +- **返回** + + - bool + +## _def_ `Command()` {#Command} + +- **说明:** 消息命令元组 + +- **参数** + + empty + +- **返回** + + - tuple[str, ...] + +## _def_ `RawCommand()` {#RawCommand} + +- **说明:** 消息命令文本 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `CommandArg()` {#CommandArg} + +- **说明:** 消息命令参数 + +- **参数** + + empty + +- **返回** + + - Any + +## _def_ `CommandStart()` {#CommandStart} + +- **说明:** 消息命令开头 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `CommandWhitespace()` {#CommandWhitespace} + +- **说明:** 消息命令与参数之间的空白 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `ShellCommandArgs()` {#ShellCommandArgs} + +- **说明:** shell 命令解析后的参数字典 + +- **参数** + + empty + +- **返回** + + - Any + +## _def_ `ShellCommandArgv()` {#ShellCommandArgv} + +- **说明:** shell 命令原始参数列表 + +- **参数** + + empty + +- **返回** + + - Any + +## _def_ `RegexMatched()` {#RegexMatched} + +- **说明:** 正则匹配结果 + +- **参数** + + empty + +- **返回** + + - Match[str] + +## _def_ `RegexStr(*groups)` {#RegexStr} + +- **说明:** 正则匹配结果文本 + +- **重载** + + **1.** `(__group=0) -> str` + + - **参数** + + - `__group` (Literal[0]) + + - **返回** + + - str + + **2.** `(__group) -> str | Any` + + - **参数** + + - `__group` (str | int) + + - **返回** + + - str | Any + + **3.** `(__group1, __group2, *groups) -> tuple[str | Any, ...]` + + - **参数** + + - `__group1` (str | int) + + - `__group2` (str | int) + + - `*groups` (str | int) + + - **返回** + + - tuple[str | Any, ...] + +## _def_ `RegexGroup()` {#RegexGroup} + +- **说明:** 正则匹配结果 group 元组 + +- **参数** + + empty + +- **返回** + + - tuple[Any, ...] + +## _def_ `RegexDict()` {#RegexDict} + +- **说明:** 正则匹配结果 group 字典 + +- **参数** + + empty + +- **返回** + + - dict[str, Any] + +## _def_ `Startswith()` {#Startswith} + +- **说明:** 响应触发前缀 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `Endswith()` {#Endswith} + +- **说明:** 响应触发后缀 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `Fullmatch()` {#Fullmatch} + +- **说明:** 响应触发完整消息 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `Keyword()` {#Keyword} + +- **说明:** 响应触发关键字 + +- **参数** + + empty + +- **返回** + + - str + +## _def_ `Received(id=None, default=None)` {#Received} + +- **说明:** `receive` 事件参数 + +- **参数** + + - `id` (str | None) + + - `default` (Any) + +- **返回** + + - Any + +## _def_ `LastReceived(default=None)` {#LastReceived} + +- **说明:** `last_receive` 事件参数 + +- **参数** + + - `default` (Any) + +- **返回** + + - Any diff --git a/website/versioned_docs/version-2.4.0/api/permission.md b/website/versioned_docs/version-2.4.0/api/permission.md new file mode 100644 index 00000000..698a3e98 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/permission.md @@ -0,0 +1,210 @@ +--- +mdx: + format: md +sidebar_position: 6 +description: nonebot.permission 模块 +--- + +# nonebot.permission + +本模块是 [Matcher.permission](matcher.md#Matcher-permission) 的类型定义。 + +每个[事件响应器](matcher.md#Matcher) +拥有一个 [Permission](#Permission),其中是 `PermissionChecker` 的集合。 +只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。 + +## _def_ `USER(*users, perm=None)` {#USER} + +- **说明** + + 匹配当前事件属于指定会话。 + + 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有检查函数的会话 ID 限制。 + +- **参数** + + - `*users` (str) + + - `perm` (Permission | None): 需要同时满足的权限 + + - `user`: 会话白名单 + +- **返回** + + - untyped + +## _class_ `User(users, perm=None)` {#User} + +- **说明:** 检查当前事件是否属于指定会话。 + +- **参数** + + - `users` (tuple[str, ...]): 会话 ID 元组 + + - `perm` (Permission | None): 需同时满足的权限 + +### _classmethod_ `from_event(event, perm=None)` {#User-from-event} + +- **说明** + + 从事件中获取会话 ID。 + + 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 + +- **参数** + + - `event` ([Event](adapters/index.md#Event)): Event 对象 + + - `perm` (Permission | None): 需同时满足的权限 + +- **返回** + + - Self + +### _classmethod_ `from_permission(*users, perm=None)` {#User-from-permission} + +- **说明** + + 指定会话与权限。 + + 如果 `perm` 中仅有 `User` 类型的权限检查函数,则会去除原有的会话 ID 限制。 + +- **参数** + + - `*users` (str): 会话白名单 + + - `perm` (Permission | None): 需同时满足的权限 + +- **返回** + + - Self + +## _class_ `Permission(*checkers)` {#Permission} + +- **说明** + + 权限类。 + + 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 + +- **参数** + + - `*checkers` ([T_PermissionChecker](typing.md#T-PermissionChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): PermissionChecker + +- **用法** + + ```python + Permission(async_function) | sync_function + # 等价于 + Permission(async_function, sync_function) + ``` + +### _instance-var_ `checkers` {#Permission-checkers} + +- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] + +- **说明:** 存储 `PermissionChecker` + +### _async method_ `__call__(bot, event, stack=None, dependency_cache=None)` {#Permission---call--} + +- **说明:** 检查是否满足某个权限。 + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 + + - `event` ([Event](adapters/index.md#Event)): Event 对象 + + - `stack` (AsyncExitStack | None): 异步上下文栈 + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 + +- **返回** + + - bool + +## _class_ `Message()` {#Message} + +- **说明:** 检查是否为消息事件 + +- **参数** + + auto + +## _class_ `Notice()` {#Notice} + +- **说明:** 检查是否为通知事件 + +- **参数** + + auto + +## _class_ `Request()` {#Request} + +- **说明:** 检查是否为请求事件 + +- **参数** + + auto + +## _class_ `MetaEvent()` {#MetaEvent} + +- **说明:** 检查是否为元事件 + +- **参数** + + auto + +## _var_ `MESSAGE` {#MESSAGE} + +- **类型:** [Permission](#Permission) + +- **说明** + + 匹配任意 `message` 类型事件 + + 仅在需要同时捕获不同类型事件时使用,优先使用 message type 的 Matcher。 + +## _var_ `NOTICE` {#NOTICE} + +- **类型:** [Permission](#Permission) + +- **说明** + + 匹配任意 `notice` 类型事件 + + 仅在需要同时捕获不同类型事件时使用,优先使用 notice type 的 Matcher。 + +## _var_ `REQUEST` {#REQUEST} + +- **类型:** [Permission](#Permission) + +- **说明** + + 匹配任意 `request` 类型事件 + + 仅在需要同时捕获不同类型事件时使用,优先使用 request type 的 Matcher。 + +## _var_ `METAEVENT` {#METAEVENT} + +- **类型:** [Permission](#Permission) + +- **说明** + + 匹配任意 `meta_event` 类型事件 + + 仅在需要同时捕获不同类型事件时使用,优先使用 meta_event type 的 Matcher。 + +## _class_ `SuperUser()` {#SuperUser} + +- **说明:** 检查当前事件是否是消息事件且属于超级管理员 + +- **参数** + + auto + +## _var_ `SUPERUSER` {#SUPERUSER} + +- **类型:** [Permission](#Permission) + +- **说明:** 匹配任意超级用户事件 diff --git a/website/versioned_docs/version-2.4.0/api/plugin/_category_.json b/website/versioned_docs/version-2.4.0/api/plugin/_category_.json new file mode 100644 index 00000000..14e3de02 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/plugin/_category_.json @@ -0,0 +1,3 @@ +{ + "position": 12 +} diff --git a/website/versioned_docs/version-2.4.0/api/plugin/index.md b/website/versioned_docs/version-2.4.0/api/plugin/index.md new file mode 100644 index 00000000..68c66d99 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/plugin/index.md @@ -0,0 +1,111 @@ +--- +mdx: + format: md +sidebar_position: 0 +description: nonebot.plugin 模块 +--- + +# nonebot.plugin + +本模块为 NoneBot 插件开发提供便携的定义函数。 + +## 快捷导入 + +为方便使用,本模块从子模块导入了部分内容,以下内容可以直接通过本模块导入: + +- `on` => [`on`](on.md#on) +- `on_metaevent` => [`on_metaevent`](on.md#on-metaevent) +- `on_message` => [`on_message`](on.md#on-message) +- `on_notice` => [`on_notice`](on.md#on-notice) +- `on_request` => [`on_request`](on.md#on-request) +- `on_startswith` => [`on_startswith`](on.md#on-startswith) +- `on_endswith` => [`on_endswith`](on.md#on-endswith) +- `on_fullmatch` => [`on_fullmatch`](on.md#on-fullmatch) +- `on_keyword` => [`on_keyword`](on.md#on-keyword) +- `on_command` => [`on_command`](on.md#on-command) +- `on_shell_command` => [`on_shell_command`](on.md#on-shell-command) +- `on_regex` => [`on_regex`](on.md#on-regex) +- `on_type` => [`on_type`](on.md#on-type) +- `CommandGroup` => [`CommandGroup`](on.md#CommandGroup) +- `Matchergroup` => [`MatcherGroup`](on.md#MatcherGroup) +- `load_plugin` => [`load_plugin`](load.md#load-plugin) +- `load_plugins` => [`load_plugins`](load.md#load-plugins) +- `load_all_plugins` => [`load_all_plugins`](load.md#load-all-plugins) +- `load_from_json` => [`load_from_json`](load.md#load-from-json) +- `load_from_toml` => [`load_from_toml`](load.md#load-from-toml) +- `load_builtin_plugin` => + [`load_builtin_plugin`](load.md#load-builtin-plugin) +- `load_builtin_plugins` => + [`load_builtin_plugins`](load.md#load-builtin-plugins) +- `require` => [`require`](load.md#require) +- `PluginMetadata` => [`PluginMetadata`](model.md#PluginMetadata) + +## _def_ `get_plugin(plugin_id)` {#get-plugin} + +- **说明** + + 获取已经导入的某个插件。 + + 如果为 `load_plugins` 文件夹导入的插件,则为文件(夹)名。 + + 如果为嵌套的子插件,标识符为 `父插件标识符:子插件文件(夹)名`。 + +- **参数** + + - `plugin_id` (str): 插件标识符,即 [Plugin.id\_](model.md#Plugin-id-)。 + +- **返回** + + - [Plugin](model.md#Plugin) | None + +## _def_ `get_plugin_by_module_name(module_name)` {#get-plugin-by-module-name} + +- **说明** + + 通过模块名获取已经导入的某个插件。 + + 如果提供的模块名为某个插件的子模块,同样会返回该插件。 + +- **参数** + + - `module_name` (str): 模块名,即 [Plugin.module_name](model.md#Plugin-module-name)。 + +- **返回** + + - [Plugin](model.md#Plugin) | None + +## _def_ `get_loaded_plugins()` {#get-loaded-plugins} + +- **说明:** 获取当前已导入的所有插件。 + +- **参数** + + empty + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +## _def_ `get_available_plugin_names()` {#get-available-plugin-names} + +- **说明:** 获取当前所有可用的插件标识符(包含尚未加载的插件)。 + +- **参数** + + empty + +- **返回** + + - set[str] + +## _def_ `get_plugin_config(config)` {#get-plugin-config} + +- **说明:** 从全局配置获取当前插件需要的配置项。 + +- **参数** + + - `config` (type[C]) + +- **返回** + + - C diff --git a/website/versioned_docs/version-2.4.0/api/plugin/load.md b/website/versioned_docs/version-2.4.0/api/plugin/load.md new file mode 100644 index 00000000..5c2a5a12 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/plugin/load.md @@ -0,0 +1,163 @@ +--- +mdx: + format: md +sidebar_position: 1 +description: nonebot.plugin.load 模块 +--- + +# nonebot.plugin.load + +本模块定义插件加载接口。 + +## _def_ `load_plugin(module_path)` {#load-plugin} + +- **说明:** 加载单个插件,可以是本地插件或是通过 `pip` 安装的插件。 + +- **参数** + + - `module_path` (str | Path): 插件名称 `path.to.your.plugin` 或插件路径 `pathlib.Path(path/to/your/plugin)` + +- **返回** + + - [Plugin](model.md#Plugin) | None + +## _def_ `load_plugins(*plugin_dir)` {#load-plugins} + +- **说明:** 导入文件夹下多个插件,以 `_` 开头的插件不会被导入! + +- **参数** + + - `*plugin_dir` (str): 文件夹路径 + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +## _def_ `load_all_plugins(module_path, plugin_dir)` {#load-all-plugins} + +- **说明:** 导入指定列表中的插件以及指定目录下多个插件,以 `_` 开头的插件不会被导入! + +- **参数** + + - `module_path` (Iterable[str]): 指定插件集合 + + - `plugin_dir` (Iterable[str]): 指定文件夹路径集合 + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +## _def_ `load_from_json(file_path, encoding="utf-8")` {#load-from-json} + +- **说明:** 导入指定 json 文件中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! + +- **参数** + + - `file_path` (str): 指定 json 文件路径 + + - `encoding` (str): 指定 json 文件编码 + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +- **用法** + + ```json title=plugins.json + { + "plugins": ["some_plugin"], + "plugin_dirs": ["some_dir"] + } + ``` + + ```python + nonebot.load_from_json("plugins.json") + ``` + +## _def_ `load_from_toml(file_path, encoding="utf-8")` {#load-from-toml} + +- **说明:** 导入指定 toml 文件 `[tool.nonebot]` 中的 `plugins` 以及 `plugin_dirs` 下多个插件。 以 `_` 开头的插件不会被导入! + +- **参数** + + - `file_path` (str): 指定 toml 文件路径 + + - `encoding` (str): 指定 toml 文件编码 + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +- **用法** + + ```toml title=pyproject.toml + [tool.nonebot] + plugins = ["some_plugin"] + plugin_dirs = ["some_dir"] + ``` + + ```python + nonebot.load_from_toml("pyproject.toml") + ``` + +## _def_ `load_builtin_plugin(name)` {#load-builtin-plugin} + +- **说明:** 导入 NoneBot 内置插件。 + +- **参数** + + - `name` (str): 插件名称 + +- **返回** + + - [Plugin](model.md#Plugin) | None + +## _def_ `load_builtin_plugins(*plugins)` {#load-builtin-plugins} + +- **说明:** 导入多个 NoneBot 内置插件。 + +- **参数** + + - `*plugins` (str): 插件名称列表 + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +## _def_ `require(name)` {#require} + +- **说明:** 声明依赖插件。 + +- **参数** + + - `name` (str): 插件模块名或插件标识符,仅在已声明插件的情况下可使用标识符。 + +- **返回** + + - ModuleType + +- **异常** + + - RuntimeError: 插件无法加载 + +## _def_ `inherit_supported_adapters(*names)` {#inherit-supported-adapters} + +- **说明** + + 获取已加载插件的适配器支持状态集合。 + + 如果传入了多个插件名称,返回值会自动取交集。 + +- **参数** + + - `*names` (str): 插件名称列表。 + +- **返回** + + - set[str] | None + +- **异常** + + - RuntimeError: 插件未加载 + + - ValueError: 插件缺少元数据 diff --git a/website/versioned_docs/version-2.4.0/api/plugin/manager.md b/website/versioned_docs/version-2.4.0/api/plugin/manager.md new file mode 100644 index 00000000..af32a84b --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/plugin/manager.md @@ -0,0 +1,124 @@ +--- +mdx: + format: md +sidebar_position: 5 +description: nonebot.plugin.manager 模块 +--- + +# nonebot.plugin.manager + +本模块实现插件加载流程。 + +参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/) + +## _class_ `PluginManager(plugins=None, search_path=None)` {#PluginManager} + +- **说明:** 插件管理器。 + +- **参数** + + - `plugins` (Iterable[str] | None): 独立插件模块名集合。 + + - `search_path` (Iterable[str] | None): 插件搜索路径(文件夹),相对于当前工作目录。 + +### _property_ `third_party_plugins` {#PluginManager-third-party-plugins} + +- **类型:** set[str] + +- **说明:** 返回所有独立插件标识符。 + +### _property_ `searched_plugins` {#PluginManager-searched-plugins} + +- **类型:** set[str] + +- **说明:** 返回已搜索到的插件标识符。 + +### _property_ `available_plugins` {#PluginManager-available-plugins} + +- **类型:** set[str] + +- **说明:** 返回当前插件管理器中可用的插件标识符。 + +### _property_ `controlled_modules` {#PluginManager-controlled-modules} + +- **类型:** dict[str, str] + +- **说明:** 返回当前插件管理器中控制的插件标识符与模块路径映射字典。 + +### _method_ `load_plugin(name)` {#PluginManager-load-plugin} + +- **说明** + + 加载指定插件。 + + 可以使用完整插件模块名或者插件标识符加载。 + +- **参数** + + - `name` (str): 插件名称或插件标识符。 + +- **返回** + + - [Plugin](model.md#Plugin) | None + +### _method_ `load_all_plugins()` {#PluginManager-load-all-plugins} + +- **说明:** 加载所有可用插件。 + +- **参数** + + empty + +- **返回** + + - set[[Plugin](model.md#Plugin)] + +## _class_ `PluginFinder()` {#PluginFinder} + +- **参数** + + auto + +### _method_ `find_spec(fullname, path, target=None)` {#PluginFinder-find-spec} + +- **参数** + + - `fullname` (str) + + - `path` (Sequence[str] | None) + + - `target` (ModuleType | None) + +- **返回** + + - untyped + +## _class_ `PluginLoader(manager, fullname, path)` {#PluginLoader} + +- **参数** + + - `manager` (PluginManager) + + - `fullname` (str) + + - `path` (str) + +### _method_ `create_module(spec)` {#PluginLoader-create-module} + +- **参数** + + - `spec` + +- **返回** + + - ModuleType | None + +### _method_ `exec_module(module)` {#PluginLoader-exec-module} + +- **参数** + + - `module` (ModuleType) + +- **返回** + + - None diff --git a/website/versioned_docs/version-2.4.0/api/plugin/model.md b/website/versioned_docs/version-2.4.0/api/plugin/model.md new file mode 100644 index 00000000..c87e6d73 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/plugin/model.md @@ -0,0 +1,140 @@ +--- +mdx: + format: md +sidebar_position: 3 +description: nonebot.plugin.model 模块 +--- + +# nonebot.plugin.model + +本模块定义插件相关信息。 + +## _class_ `PluginMetadata()` {#PluginMetadata} + +- **说明:** 插件元信息,由插件编写者提供 + +- **参数** + + auto + +### _instance-var_ `name` {#PluginMetadata-name} + +- **类型:** str + +- **说明:** 插件名称 + +### _instance-var_ `description` {#PluginMetadata-description} + +- **类型:** str + +- **说明:** 插件功能介绍 + +### _instance-var_ `usage` {#PluginMetadata-usage} + +- **类型:** str + +- **说明:** 插件使用方法 + +### _class-var_ `type` {#PluginMetadata-type} + +- **类型:** str | None + +- **说明:** 插件类型,用于商店分类 + +### _class-var_ `homepage` {#PluginMetadata-homepage} + +- **类型:** str | None + +- **说明:** 插件主页 + +### _class-var_ `config` {#PluginMetadata-config} + +- **类型:** type[BaseModel] | None + +- **说明:** 插件配置项 + +### _class-var_ `supported_adapters` {#PluginMetadata-supported-adapters} + +- **类型:** set[str] | None + +- **说明** + + 插件支持的适配器模块路径 + + 格式为 `[:]`,`~` 为 `nonebot.adapters.` 的缩写。 + + `None` 表示支持**所有适配器**。 + +### _class-var_ `extra` {#PluginMetadata-extra} + +- **类型:** dict[Any, Any] + +- **说明:** 插件额外信息,可由插件编写者自由扩展定义 + +### _method_ `get_supported_adapters()` {#PluginMetadata-get-supported-adapters} + +- **说明:** 获取当前已安装的插件支持适配器类列表 + +- **参数** + + empty + +- **返回** + + - set[type[[Adapter](../adapters/index.md#Adapter)]] | None + +## _class_ `Plugin()` {#Plugin} + +- **说明:** 存储插件信息 + +- **参数** + + auto + +### _instance-var_ `name` {#Plugin-name} + +- **类型:** str + +- **说明:** 插件名称,NoneBot 使用 文件/文件夹 名称作为插件名称 + +### _instance-var_ `module` {#Plugin-module} + +- **类型:** ModuleType + +- **说明:** 插件模块对象 + +### _instance-var_ `module_name` {#Plugin-module-name} + +- **类型:** str + +- **说明:** 点分割模块路径 + +### _instance-var_ `manager` {#Plugin-manager} + +- **类型:** [PluginManager](manager.md#PluginManager) + +- **说明:** 导入该插件的插件管理器 + +### _class-var_ `matcher` {#Plugin-matcher} + +- **类型:** set[type[[Matcher](../matcher.md#Matcher)]] + +- **说明:** 插件加载时定义的 `Matcher` + +### _class-var_ `parent_plugin` {#Plugin-parent-plugin} + +- **类型:** Plugin | None + +- **说明:** 父插件 + +### _class-var_ `sub_plugins` {#Plugin-sub-plugins} + +- **类型:** set[Plugin] + +- **说明:** 子插件集合 + +### _property_ `id_` {#Plugin-id-} + +- **类型:** str + +- **说明:** 插件索引标识 diff --git a/website/versioned_docs/version-2.4.0/api/plugin/on.md b/website/versioned_docs/version-2.4.0/api/plugin/on.md new file mode 100644 index 00000000..638bacaa --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/plugin/on.md @@ -0,0 +1,950 @@ +--- +mdx: + format: md +sidebar_position: 2 +description: nonebot.plugin.on 模块 +--- + +# nonebot.plugin.on + +本模块定义事件响应器便携定义函数。 + +## _def_ `store_matcher(matcher)` {#store-matcher} + +- **说明:** 存储一个事件响应器到插件。 + +- **参数** + + - `matcher` (type[[Matcher](../matcher.md#Matcher)]): 事件响应器 + +- **返回** + + - None + +## _def_ `get_matcher_plugin(depth=...)` {#get-matcher-plugin} + +- **说明** + + 获取事件响应器定义所在插件。 + + **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 + +- **参数** + + - `depth` (int): 调用栈深度 + +- **返回** + + - [Plugin](model.md#Plugin) | None + +## _def_ `get_matcher_module(depth=...)` {#get-matcher-module} + +- **说明** + + 获取事件响应器定义所在模块。 + + **Deprecated**, 请使用 [get_matcher_source](#get-matcher-source) 获取信息。 + +- **参数** + + - `depth` (int): 调用栈深度 + +- **返回** + + - ModuleType | None + +## _def_ `get_matcher_source(depth=...)` {#get-matcher-source} + +- **说明:** 获取事件响应器定义所在源码信息。 + +- **参数** + + - `depth` (int): 调用栈深度 + +- **返回** + + - MatcherSource | None + +## _def_ `on(type="", rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on} + +- **说明:** 注册一个基础事件响应器,可自定义类型。 + +- **参数** + + - `type` (str): 事件响应器类型 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_metaevent(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-metaevent} + +- **说明:** 注册一个元事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_message(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-message} + +- **说明:** 注册一个消息事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_notice(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-notice} + +- **说明:** 注册一个通知事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_request(rule=..., permission=..., *, handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-request} + +- **说明:** 注册一个请求事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_startswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-startswith} + +- **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息开头内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `ignorecase` (bool): 是否忽略大小写 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_endswith(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-endswith} + +- **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息结尾内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `ignorecase` (bool): 是否忽略大小写 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_fullmatch(msg, rule=..., ignorecase=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-fullmatch} + +- **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `ignorecase` (bool): 是否忽略大小写 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_keyword(keywords, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-keyword} + +- **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 + +- **参数** + + - `keywords` (set[str]): 关键词列表 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_command(cmd, rule=..., aliases=..., force_whitespace=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-command} + +- **说明** + + 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 + + 命令匹配规则参考: `命令形式匹配 `\_ + +- **参数** + + - `cmd` (str | tuple[str, ...]): 指定命令内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 + + - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_shell_command(cmd, rule=..., aliases=..., parser=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-shell-command} + +- **说明** + + 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 + + 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 + + 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, + 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 + +- **参数** + + - `cmd` (str | tuple[str, ...]): 指定命令内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 + + - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_regex(pattern, flags=..., rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-regex} + +- **说明** + + 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 + + 命令匹配规则参考: `正则匹配 `\_ + +- **参数** + + - `pattern` (str): 正则表达式 + + - `flags` (int | re.RegexFlag): 正则匹配标志 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _def_ `on_type(types, rule=..., *, permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#on-type} + +- **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 + +- **参数** + + - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)], ...]): 事件类型 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _class_ `CommandGroup(cmd, prefix_aliases=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup} + +- **参数** + + - `cmd` (str | tuple[str, ...]) + + - `prefix_aliases` (bool) + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) + + - `temp` (bool) + + - `expire_time` (datetime | timedelta | None) + + - `priority` (int) + + - `block` (bool) + + - `state` ([T_State](../typing.md#T-State) | None) + +### _method_ `command(cmd, *, rule=..., aliases=..., force_whitespace=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-command} + +- **说明:** 注册一个新的命令。新参数将会覆盖命令组默认值 + +- **参数** + + - `cmd` (str | tuple[str, ...]): 指定命令内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 + + - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `shell_command(cmd, *, rule=..., aliases=..., parser=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#CommandGroup-shell-command} + +- **说明:** 注册一个新的 `shell_like` 命令。新参数将会覆盖命令组默认值 + +- **参数** + + - `cmd` (str | tuple[str, ...]): 指定命令内容 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 + + - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +## _class_ `MatcherGroup(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup} + +- **参数** + + - `type` (str) + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None) + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None) + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None) + + - `temp` (bool) + + - `expire_time` (datetime | timedelta | None) + + - `priority` (int) + + - `block` (bool) + + - `state` ([T_State](../typing.md#T-State) | None) + +### _method_ `on(*, type=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on} + +- **说明:** 注册一个基础事件响应器,可自定义类型。 + +- **参数** + + - `type` (str): 事件响应器类型 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_metaevent(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-metaevent} + +- **说明:** 注册一个元事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_message(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-message} + +- **说明:** 注册一个消息事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_notice(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-notice} + +- **说明:** 注册一个通知事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_request(*, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-request} + +- **说明:** 注册一个请求事件响应器。 + +- **参数** + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_startswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-startswith} + +- **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容开头时响应。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息开头内容 + + - `ignorecase` (bool): 是否忽略大小写 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_endswith(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-endswith} + +- **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**以指定内容结尾时响应。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息结尾内容 + + - `ignorecase` (bool): 是否忽略大小写 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_fullmatch(msg, *, ignorecase=..., rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-fullmatch} + +- **说明:** 注册一个消息事件响应器,并且当消息的**文本部分**与指定内容完全一致时响应。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息全匹配内容 + + - `ignorecase` (bool): 是否忽略大小写 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_keyword(keywords, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-keyword} + +- **说明:** 注册一个消息事件响应器,并且当消息纯文本部分包含关键词时响应。 + +- **参数** + + - `keywords` (set[str]): 关键词列表 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_command(cmd, aliases=..., force_whitespace=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-command} + +- **说明** + + 注册一个消息事件响应器,并且当消息以指定命令开头时响应。 + + 命令匹配规则参考: `命令形式匹配 `\_ + +- **参数** + + - `cmd` (str | tuple[str, ...]): 指定命令内容 + + - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 + + - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_shell_command(cmd, aliases=..., parser=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-shell-command} + +- **说明** + + 注册一个支持 `shell_like` 解析参数的命令消息事件响应器。 + + 与普通的 `on_command` 不同的是,在添加 `parser` 参数时, 响应器会自动处理消息。 + + 可以通过 [ShellCommandArgv](../params.md#ShellCommandArgv) 获取原始参数列表, + 通过 [ShellCommandArgs](../params.md#ShellCommandArgs) 获取解析后的参数字典。 + +- **参数** + + - `cmd` (str | tuple[str, ...]): 指定命令内容 + + - `aliases` (set[str | tuple[str, ...]] | None): 命令别名 + + - `parser` ([ArgumentParser](../rule.md#ArgumentParser) | None): `nonebot.rule.ArgumentParser` 对象 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_regex(pattern, flags=..., *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-regex} + +- **说明** + + 注册一个消息事件响应器,并且当消息匹配正则表达式时响应。 + + 命令匹配规则参考: `正则匹配 `\_ + +- **参数** + + - `pattern` (str): 正则表达式 + + - `flags` (int | re.RegexFlag): 正则匹配标志 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] + +### _method_ `on_type(types, *, rule=..., permission=..., handlers=..., temp=..., expire_time=..., priority=..., block=..., state=...)` {#MatcherGroup-on-type} + +- **说明:** 注册一个事件响应器,并且当事件为指定类型时响应。 + +- **参数** + + - `types` (type[[Event](../adapters/index.md#Event)] | tuple[type[[Event](../adapters/index.md#Event)]]): 事件类型 + + - `rule` ([Rule](../rule.md#Rule) | [T_RuleChecker](../typing.md#T-RuleChecker) | None): 事件响应规则 + + - `permission` ([Permission](../permission.md#Permission) | [T_PermissionChecker](../typing.md#T-PermissionChecker) | None): 事件响应权限 + + - `handlers` (list[[T\_Handler](../typing.md#T-Handler) | [Dependent](../dependencies/index.md#Dependent)[Any]] | None): 事件处理函数列表 + + - `temp` (bool): 是否为临时事件响应器(仅执行一次) + + - `expire_time` (datetime | timedelta | None): 事件响应器最终有效时间点,过时即被删除 + + - `priority` (int): 事件响应器优先级 + + - `block` (bool): 是否阻止事件向更低优先级传递 + + - `state` ([T_State](../typing.md#T-State) | None): 默认 state + +- **返回** + + - type[[Matcher](../matcher.md#Matcher)] diff --git a/website/versioned_docs/version-2.4.0/api/rule.md b/website/versioned_docs/version-2.4.0/api/rule.md new file mode 100644 index 00000000..3307b13f --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/rule.md @@ -0,0 +1,404 @@ +--- +mdx: + format: md +sidebar_position: 5 +description: nonebot.rule 模块 +--- + +# nonebot.rule + +本模块是 [Matcher.rule](matcher.md#Matcher-rule) 的类型定义。 + +每个[事件响应器](matcher.md#Matcher)拥有一个 +[Rule](#Rule),其中是 `RuleChecker` 的集合。 +只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。 + +## _class_ `Rule(*checkers)` {#Rule} + +- **说明** + + 规则类。 + + 当事件传递时,在 [Matcher](matcher.md#Matcher) 运行前进行检查。 + +- **参数** + + - `*checkers` ([T_RuleChecker](typing.md#T-RuleChecker) | [Dependent](dependencies/index.md#Dependent)[bool]): RuleChecker + +- **用法** + + ```python + Rule(async_function) & sync_function + # 等价于 + Rule(async_function, sync_function) + ``` + +### _instance-var_ `checkers` {#Rule-checkers} + +- **类型:** set[[Dependent](dependencies/index.md#Dependent)[bool]] + +- **说明:** 存储 `RuleChecker` + +### _async method_ `__call__(bot, event, state, stack=None, dependency_cache=None)` {#Rule---call--} + +- **说明:** 检查是否符合所有规则 + +- **参数** + + - `bot` ([Bot](adapters/index.md#Bot)): Bot 对象 + + - `event` ([Event](adapters/index.md#Event)): Event 对象 + + - `state` ([T_State](typing.md#T-State)): 当前 State + + - `stack` (AsyncExitStack | None): 异步上下文栈 + + - `dependency_cache` ([T_DependencyCache](typing.md#T-DependencyCache) | None): 依赖缓存 + +- **返回** + + - bool + +## _class_ `CMD_RESULT()` {#CMD-RESULT} + +- **参数** + + auto + +## _class_ `TRIE_VALUE()` {#TRIE-VALUE} + +- **说明:** TRIE_VALUE(command_start, command) + +- **参数** + + auto + +## _class_ `StartswithRule(msg, ignorecase=False)` {#StartswithRule} + +- **说明:** 检查消息纯文本是否以指定字符串开头。 + +- **参数** + + - `msg` (tuple[str, ...]): 指定消息开头字符串元组 + + - `ignorecase` (bool): 是否忽略大小写 + +## _def_ `startswith(msg, ignorecase=False)` {#startswith} + +- **说明:** 匹配消息纯文本开头。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 + + - `ignorecase` (bool): 是否忽略大小写 + +- **返回** + + - [Rule](#Rule) + +## _class_ `EndswithRule(msg, ignorecase=False)` {#EndswithRule} + +- **说明:** 检查消息纯文本是否以指定字符串结尾。 + +- **参数** + + - `msg` (tuple[str, ...]): 指定消息结尾字符串元组 + + - `ignorecase` (bool): 是否忽略大小写 + +## _def_ `endswith(msg, ignorecase=False)` {#endswith} + +- **说明:** 匹配消息纯文本结尾。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息开头字符串元组 + + - `ignorecase` (bool): 是否忽略大小写 + +- **返回** + + - [Rule](#Rule) + +## _class_ `FullmatchRule(msg, ignorecase=False)` {#FullmatchRule} + +- **说明:** 检查消息纯文本是否与指定字符串全匹配。 + +- **参数** + + - `msg` (tuple[str, ...]): 指定消息全匹配字符串元组 + + - `ignorecase` (bool): 是否忽略大小写 + +## _def_ `fullmatch(msg, ignorecase=False)` {#fullmatch} + +- **说明:** 完全匹配消息。 + +- **参数** + + - `msg` (str | tuple[str, ...]): 指定消息全匹配字符串元组 + + - `ignorecase` (bool): 是否忽略大小写 + +- **返回** + + - [Rule](#Rule) + +## _class_ `KeywordsRule(*keywords)` {#KeywordsRule} + +- **说明:** 检查消息纯文本是否包含指定关键字。 + +- **参数** + + - `*keywords` (str): 指定关键字元组 + +## _def_ `keyword(*keywords)` {#keyword} + +- **说明:** 匹配消息纯文本关键词。 + +- **参数** + + - `*keywords` (str): 指定关键字元组 + +- **返回** + + - [Rule](#Rule) + +## _class_ `CommandRule(cmds, force_whitespace=None)` {#CommandRule} + +- **说明:** 检查消息是否为指定命令。 + +- **参数** + + - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 + + - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 + +## _def_ `command(*cmds, force_whitespace=None)` {#command} + +- **说明** + + 匹配消息命令。 + + 根据配置里提供的 [`command_start`](config.md#Config-command-start), + [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 + + 可以通过 [Command](params.md#Command) 获取匹配成功的命令(例: `("test",)`), + 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本(例: `"/test"`), + 通过 [CommandArg](params.md#CommandArg) 获取匹配成功的命令参数。 + +- **参数** + + - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 + + - `force_whitespace` (str | bool | None): 是否强制命令后必须有指定空白符 + +- **返回** + + - [Rule](#Rule) + +- **用法** + + 使用默认 `command_start`, `command_sep` 配置情况下: + + 命令 `("test",)` 可以匹配: `/test` 开头的消息 + 命令 `("test", "sub")` 可以匹配: `/test.sub` 开头的消息 + +:::tip 提示 +命令内容与后续消息间无需空格! +::: + +## _class_ `ArgumentParser()` {#ArgumentParser} + +- **说明** + + `shell_like` 命令参数解析器,解析出错时不会退出程序。 + + 支持 [Message](adapters/index.md#Message) 富文本解析。 + +- **参数** + + auto + +- **用法** + + 用法与 `argparse.ArgumentParser` 相同, + 参考文档: [argparse](https://docs.python.org/3/library/argparse.html) + +### _method_ `parse_known_args(args=None, namespace=None)` {#ArgumentParser-parse-known-args} + +- **重载** + + **1.** `(args=None, namespace=None) -> tuple[Namespace, list[str | MessageSegment]]` + + - **参数** + + - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) + + - `namespace` (None) + + - **返回** + + - tuple[Namespace, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] + + **2.** `(args, namespace) -> tuple[T, list[str | MessageSegment]]` + + - **参数** + + - `args` (Sequence[str | [MessageSegment](adapters/index.md#MessageSegment)] | None) + + - `namespace` (T) + + - **返回** + + - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] + + **3.** `(*, namespace) -> tuple[T, list[str | MessageSegment]]` + + - **参数** + + - `namespace` (T) + + - **返回** + + - tuple[T, list[str | [MessageSegment](adapters/index.md#MessageSegment)]] + +## _class_ `ShellCommandRule(cmds, parser)` {#ShellCommandRule} + +- **说明:** 检查消息是否为指定 shell 命令。 + +- **参数** + + - `cmds` (list[tuple[str, ...]]): 指定命令元组列表 + + - `parser` (ArgumentParser | None): 可选参数解析器 + +## _def_ `shell_command(*cmds, parser=None)` {#shell-command} + +- **说明** + + 匹配 `shell_like` 形式的消息命令。 + + 根据配置里提供的 [`command_start`](config.md#Config-command-start), + [`command_sep`](config.md#Config-command-sep) 判断消息是否为命令。 + + 可以通过 [Command](params.md#Command) 获取匹配成功的命令 + (例: `("test",)`), + 通过 [RawCommand](params.md#RawCommand) 获取匹配成功的原始命令文本 + (例: `"/test"`), + 通过 [ShellCommandArgv](params.md#ShellCommandArgv) 获取解析前的参数列表 + (例: `["arg", "-h"]`), + 通过 [ShellCommandArgs](params.md#ShellCommandArgs) 获取解析后的参数字典 + (例: `{"arg": "arg", "h": True}`)。 + + :::caution 警告 + 如果参数解析失败,则通过 [ShellCommandArgs](params.md#ShellCommandArgs) + 获取的将是 [ParserExit](exception.md#ParserExit) 异常。 + ::: + +- **参数** + + - `*cmds` (str | tuple[str, ...]): 命令文本或命令元组 + + - `parser` (ArgumentParser | None): [ArgumentParser](#ArgumentParser) 对象 + +- **返回** + + - [Rule](#Rule) + +- **用法** + + 使用默认 `command_start`, `command_sep` 配置,更多示例参考 + [argparse](https://docs.python.org/3/library/argparse.html) 标准库文档。 + + ```python + from nonebot.rule import ArgumentParser + + parser = ArgumentParser() + parser.add_argument("-a", action="store_true") + + rule = shell_command("ls", parser=parser) + ``` + +:::tip 提示 +命令内容与后续消息间无需空格! +::: + +## _class_ `RegexRule(regex, flags=0)` {#RegexRule} + +- **说明:** 检查消息字符串是否符合指定正则表达式。 + +- **参数** + + - `regex` (str): 正则表达式 + + - `flags` (int): 正则表达式标记 + +## _def_ `regex(regex, flags=0)` {#regex} + +- **说明** + + 匹配符合正则表达式的消息字符串。 + + 可以通过 [RegexStr](params.md#RegexStr) 获取匹配成功的字符串, + 通过 [RegexGroup](params.md#RegexGroup) 获取匹配成功的 group 元组, + 通过 [RegexDict](params.md#RegexDict) 获取匹配成功的 group 字典。 + +- **参数** + + - `regex` (str): 正则表达式 + + - `flags` (int | re.RegexFlag): 正则表达式标记 + +- **返回** + + - [Rule](#Rule) + +:::tip 提示 +正则表达式匹配使用 search 而非 match,如需从头匹配请使用 `r"^xxx"` 来确保匹配开头 +::: +:::tip 提示 +正则表达式匹配使用 `EventMessage` 的 `str` 字符串, +而非 `EventMessage` 的 `PlainText` 纯文本字符串 +::: + +## _class_ `ToMeRule()` {#ToMeRule} + +- **说明:** 检查事件是否与机器人有关。 + +- **参数** + + auto + +## _def_ `to_me()` {#to-me} + +- **说明:** 匹配与机器人有关的事件。 + +- **参数** + + empty + +- **返回** + + - [Rule](#Rule) + +## _class_ `IsTypeRule(*types)` {#IsTypeRule} + +- **说明:** 检查事件类型是否为指定类型。 + +- **参数** + + - `*types` (type[[Event](adapters/index.md#Event)]) + +## _def_ `is_type(*types)` {#is-type} + +- **说明:** 匹配事件类型。 + +- **参数** + + - `*types` (type[[Event](adapters/index.md#Event)]): 事件类型 + +- **返回** + + - [Rule](#Rule) diff --git a/website/versioned_docs/version-2.4.0/api/typing.md b/website/versioned_docs/version-2.4.0/api/typing.md new file mode 100644 index 00000000..8b1c5005 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/typing.md @@ -0,0 +1,305 @@ +--- +mdx: + format: md +sidebar_position: 11 +description: nonebot.typing 模块 +--- + +# nonebot.typing + +本模块定义了 NoneBot 模块中共享的一些类型。 + +使用 Python 的 Type Hint 语法, +参考 [`PEP 484`](https://www.python.org/dev/peps/pep-0484/), +[`PEP 526`](https://www.python.org/dev/peps/pep-0526/) 和 +[`typing`](https://docs.python.org/3/library/typing.html)。 + +## _def_ `overrides(InterfaceClass)` {#overrides} + +- **说明:** 标记一个方法为父类 interface 的 implement + +- **参数** + + - `InterfaceClass` (object) + +- **返回** + + - untyped + +## _def_ `type_has_args(type_)` {#type-has-args} + +- **参数** + + - `type_` (type[Any]) + +- **返回** + + - bool + +## _def_ `origin_is_union(origin)` {#origin-is-union} + +- **参数** + + - `origin` (type[Any] | None) + +- **返回** + + - bool + +## _def_ `origin_is_literal(origin)` {#origin-is-literal} + +- **说明:** 判断是否是 Literal 类型 + +- **参数** + + - `origin` (type[Any] | None) + +- **返回** + + - bool + +## _def_ `all_literal_values(type_)` {#all-literal-values} + +- **说明:** 获取 Literal 类型包含的所有值 + +- **参数** + + - `type_` (type[Any]) + +- **返回** + + - list[Any] + +## _def_ `origin_is_annotated(origin)` {#origin-is-annotated} + +- **说明:** 判断是否是 Annotated 类型 + +- **参数** + + - `origin` (type[Any] | None) + +- **返回** + + - bool + +## _def_ `is_none_type(type_)` {#is-none-type} + +- **说明:** 判断是否是 None 类型 + +- **参数** + + - `type_` (type[Any]) + +- **返回** + + - bool + +## _def_ `evaluate_forwardref(ref, globalns, localns)` {#evaluate-forwardref} + +- **参数** + + - `ref` (ForwardRef) + + - `globalns` (dict[str, Any]) + + - `localns` (dict[str, Any]) + +- **返回** + + - Any + +## _class_ `StateFlag()` {#StateFlag} + +- **参数** + + auto + +## _var_ `T_State` {#T-State} + +- **类型:** dict[Any, Any] + +- **说明:** 事件处理状态 State 类型 + +## _var_ `T_BotConnectionHook` {#T-BotConnectionHook} + +- **类型:** \_DependentCallable[Any] + +- **说明** + + Bot 连接建立时钩子函数 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_BotDisconnectionHook` {#T-BotDisconnectionHook} + +- **类型:** \_DependentCallable[Any] + +- **说明** + + Bot 连接断开时钩子函数 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_CallingAPIHook` {#T-CallingAPIHook} + +- **类型:** ([Bot](adapters/index.md#Bot), str, dict[str, Any]) -> Awaitable[Any] + +- **说明:** `bot.call_api` 钩子函数 + +## _var_ `T_CalledAPIHook` {#T-CalledAPIHook} + +- **类型:** ([Bot](adapters/index.md#Bot), Exception | None, str, dict[str, Any], Any) -> Awaitable[Any] + +- **说明:** `bot.call_api` 后执行的函数,参数分别为 bot, exception, api, data, result + +## _var_ `T_EventPreProcessor` {#T-EventPreProcessor} + +- **类型:** \_DependentCallable[Any] + +- **说明** + + 事件预处理函数 EventPreProcessor 类型 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_EventPostProcessor` {#T-EventPostProcessor} + +- **类型:** \_DependentCallable[Any] + +- **说明** + + 事件后处理函数 EventPostProcessor 类型 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_RunPreProcessor` {#T-RunPreProcessor} + +- **类型:** \_DependentCallable[Any] + +- **说明** + + 事件响应器运行前预处理函数 RunPreProcessor 类型 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - MatcherParam: Matcher 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_RunPostProcessor` {#T-RunPostProcessor} + +- **类型:** \_DependentCallable[Any] + +- **说明** + + 事件响应器运行后后处理函数 RunPostProcessor 类型 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - MatcherParam: Matcher 对象 + - ExceptionParam: 异常对象(可能为 None) + - DefaultParam: 带有默认值的参数 + +## _var_ `T_RuleChecker` {#T-RuleChecker} + +- **类型:** \_DependentCallable[bool] + +- **说明** + + RuleChecker 即判断是否响应事件的处理函数。 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_PermissionChecker` {#T-PermissionChecker} + +- **类型:** \_DependentCallable[bool] + +- **说明** + + PermissionChecker 即判断事件是否满足权限的处理函数。 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_Handler` {#T-Handler} + +- **类型:** \_DependentCallable[Any] + +- **说明:** Handler 处理函数。 + +## _var_ `T_TypeUpdater` {#T-TypeUpdater} + +- **类型:** \_DependentCallable[str] + +- **说明** + + TypeUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新响应的事件类型。 默认会更新为 `message`。 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - MatcherParam: Matcher 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_PermissionUpdater` {#T-PermissionUpdater} + +- **类型:** \_DependentCallable[[Permission](permission.md#Permission)] + +- **说明** + + PermissionUpdater 在 Matcher.pause, Matcher.reject 时被运行,用于更新会话对象权限。 默认会更新为当前事件的触发对象。 + + 依赖参数: + + - DependParam: 子依赖参数 + - BotParam: Bot 对象 + - EventParam: Event 对象 + - StateParam: State 对象 + - MatcherParam: Matcher 对象 + - DefaultParam: 带有默认值的参数 + +## _var_ `T_DependencyCache` {#T-DependencyCache} + +- **类型:** dict[\_DependentCallable[Any], DependencyCache] + +- **说明:** 依赖缓存, 用于存储依赖函数的返回值 diff --git a/website/versioned_docs/version-2.4.0/api/utils.md b/website/versioned_docs/version-2.4.0/api/utils.md new file mode 100644 index 00000000..789478d1 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/api/utils.md @@ -0,0 +1,304 @@ +--- +mdx: + format: md +sidebar_position: 8 +description: nonebot.utils 模块 +--- + +# nonebot.utils + +本模块包含了 NoneBot 的一些工具函数 + +## _def_ `escape_tag(s)` {#escape-tag} + +- **说明** + + 用于记录带颜色日志时转义 `` 类型特殊标签 + + 参考: [loguru color 标签](https://loguru.readthedocs.io/en/stable/api/logger.html#color) + +- **参数** + + - `s` (str): 需要转义的字符串 + +- **返回** + + - str + +## _def_ `deep_update(mapping, *updating_mappings)` {#deep-update} + +- **说明:** 深度更新合并字典 + +- **参数** + + - `mapping` (dict[K, Any]) + + - `*updating_mappings` (dict[K, Any]) + +- **返回** + + - dict[K, Any] + +## _def_ `lenient_issubclass(cls, class_or_tuple)` {#lenient-issubclass} + +- **说明:** 检查 cls 是否是 class_or_tuple 中的一个类型子类并忽略类型错误。 + +- **参数** + + - `cls` (Any) + + - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) + +- **返回** + + - bool + +## _def_ `generic_check_issubclass(cls, class_or_tuple)` {#generic-check-issubclass} + +- **说明** + + 检查 cls 是否是 class_or_tuple 中的一个类型子类。 + + 特别的: + + - 如果 cls 是 `typing.Union` 或 `types.UnionType` 类型, + 则会检查其中的所有类型是否是 class_or_tuple 中一个类型的子类或 None。 + - 如果 cls 是 `typing.Literal` 类型, + 则会检查其中的所有值是否是 class_or_tuple 中一个类型的实例。 + - 如果 cls 是 `typing.TypeVar` 类型, + 则会检查其 `__bound__` 或 `__constraints__` + 是否是 class_or_tuple 中一个类型的子类或 None。 + +- **参数** + + - `cls` (Any) + + - `class_or_tuple` (type[Any] | tuple[type[Any], ...]) + +- **返回** + + - bool + +## _def_ `type_is_complex(type_)` {#type-is-complex} + +- **说明:** 检查 type\_ 是否是复杂类型 + +- **参数** + + - `type_` (type[Any]) + +- **返回** + + - bool + +## _def_ `is_coroutine_callable(call)` {#is-coroutine-callable} + +- **说明:** 检查 call 是否是一个 callable 协程函数 + +- **参数** + + - `call` ((...) -> Any) + +- **返回** + + - bool + +## _def_ `is_gen_callable(call)` {#is-gen-callable} + +- **说明:** 检查 call 是否是一个生成器函数 + +- **参数** + + - `call` ((...) -> Any) + +- **返回** + + - bool + +## _def_ `is_async_gen_callable(call)` {#is-async-gen-callable} + +- **说明:** 检查 call 是否是一个异步生成器函数 + +- **参数** + + - `call` ((...) -> Any) + +- **返回** + + - bool + +## _def_ `run_sync(call)` {#run-sync} + +- **说明:** 一个用于包装 sync function 为 async function 的装饰器 + +- **参数** + + - `call` ((P) -> R): 被装饰的同步函数 + +- **返回** + + - (P) -> Coroutine[None, None, R] + +## _def_ `run_sync_ctx_manager(cm)` {#run-sync-ctx-manager} + +- **说明:** 一个用于包装 sync context manager 为 async context manager 的执行函数 + +- **参数** + + - `cm` (AbstractContextManager[T]) + +- **返回** + + - AsyncGenerator[T, None] + +## _async def_ `run_coro_with_catch(coro, exc, return_on_err=None)` {#run-coro-with-catch} + +- **说明:** 运行协程并当遇到指定异常时返回指定值。 + +- **重载** + + **1.** `(coro, exc, return_on_err=None) -> T | None` + + - **参数** + + - `coro` (Coroutine[Any, Any, T]) + + - `exc` (tuple[type[Exception], ...]) + + - `return_on_err` (None) + + - **返回** + + - T | None + + **2.** `(coro, exc, return_on_err) -> T | R` + + - **参数** + + - `coro` (Coroutine[Any, Any, T]) + + - `exc` (tuple[type[Exception], ...]) + + - `return_on_err` (R) + + - **返回** + + - T | R + +- **参数** + + - `coro`: 要运行的协程 + + - `exc`: 要捕获的异常 + + - `return_on_err`: 当发生异常时返回的值 + +- **返回** + + 协程的返回值或发生异常时的指定值 + +## _async def_ `run_coro_with_shield(coro)` {#run-coro-with-shield} + +- **说明:** 运行协程并在取消时屏蔽取消异常。 + +- **参数** + + - `coro` (Coroutine[Any, Any, T]): 要运行的协程 + +- **返回** + + - T: 协程的返回值 + +## _def_ `flatten_exception_group(exc_group)` {#flatten-exception-group} + +- **参数** + + - `exc_group` (BaseExceptionGroup[E]) + +- **返回** + + - Generator[E, None, None] + +## _def_ `get_name(obj)` {#get-name} + +- **说明:** 获取对象的名称 + +- **参数** + + - `obj` (Any) + +- **返回** + + - str + +## _def_ `path_to_module_name(path)` {#path-to-module-name} + +- **说明:** 转换路径为模块名 + +- **参数** + + - `path` (Path) + +- **返回** + + - str + +## _def_ `resolve_dot_notation(obj_str, default_attr, default_prefix=None)` {#resolve-dot-notation} + +- **说明:** 解析并导入点分表示法的对象 + +- **参数** + + - `obj_str` (str) + + - `default_attr` (str) + + - `default_prefix` (str | None) + +- **返回** + + - Any + +## _class_ `classproperty(func)` {#classproperty} + +- **说明:** 类属性装饰器 + +- **参数** + + - `func` ((Any) -> T) + +## _class_ `DataclassEncoder()` {#DataclassEncoder} + +- **说明:** 可以序列化 [Message](adapters/index.md#Message)(List[Dataclass]) 的 `JSONEncoder` + +- **参数** + + auto + +### _method_ `default(o)` {#DataclassEncoder-default} + +- **参数** + + - `o` + +- **返回** + + - untyped + +## _def_ `logger_wrapper(logger_name)` {#logger-wrapper} + +- **说明:** 用于打印 adapter 的日志。 + +- **参数** + + - `logger_name` (str): adapter 的名称 + +- **返回** + + - untyped: 日志记录函数 + + 日志记录函数的参数: + + - level: 日志等级 + - message: 日志信息 + - exception: 异常信息 diff --git a/website/versioned_docs/version-2.4.0/appendices/api-calling.mdx b/website/versioned_docs/version-2.4.0/appendices/api-calling.mdx new file mode 100644 index 00000000..ab6ce23d --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/api-calling.mdx @@ -0,0 +1,131 @@ +--- +sidebar_position: 4 +description: 使用平台接口,完成更多功能 + +options: + menu: + - category: appendices + weight: 50 +--- + +# 使用平台接口 + +import Messenger from "@/components/Messenger"; + +在 NoneBot 中,除了使用事件响应器操作发送文本消息外,我们还可以直接通过使用协议适配器提供的方法来使用平台特定的接口,完成发送特殊消息、获取信息等其他平台提供的功能。同时,在部分无法使用事件响应器的情况中,例如[定时任务](../best-practice/scheduler.md),我们也可以使用平台接口来完成需要的功能。 + +## 发送平台特殊消息 + +在之前的章节中,我们介绍了如何向用户发送文本消息以及[如何处理平台消息](../tutorial/message.md),现在我们来向用户发送平台特殊消息。 + +:::caution 注意 +在以下的示例中,我们将使用 `Console` 协议适配器来演示如何发送平台消息。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 +::: + +```python {4,7-17} title=weather/__init__.py +import inspect +from nonebot.adapters.console import MessageSegment + +@weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") +async def got_location(location: str = ArgPlainText()): + result = await weather.send( + MessageSegment.markdown( + inspect.cleandoc( + f""" + # {location} + + - 今天 + + ⛅ 多云 20℃~24℃ + """ + ) + ) + ) +``` + + + +在上面的示例中,我们使用了 `Console` 协议适配器提供的 `MessageSegment` 类来发送平台特定的消息 `emoji` 和 `markdown`。这两种消息可以显示在终端中,但是无法在其他平台上使用。在事件响应器操作中,我们可以使用 `str`、消息序列、消息段、消息模板四种类型来发送消息,但其中只有 `str` 和[纯文本形式的消息模板类型](../tutorial/message.md#使用消息模板)消息可以在所有平台上使用。 + +`send` 事件响应器操作实际上是由协议适配器通过调用平台 API 来实现的,通常会将 API 调用的结果作为返回值返回。 + +## 调用平台 API + +在 NoneBot 中,我们可以通过 `Bot` 对象来调用协议适配器支持的平台 API,来完成更多的功能。 + +### 获取 Bot + +在调用平台 API 之前,我们首先要获得 Bot 对象。有两种方式可以获得 Bot 对象。 + +在事件处理流程的上下文中,我们可以直接使用依赖注入 Bot 来获取: + +```python {1,4} title=weather/__init__.py +from nonebot.adapters import Bot + +@weather.got("location", prompt="请输入地名") +async def got_location(bot: Bot, location: str = ArgPlainText()): + ... +``` + +依赖注入会确保你获得的 Bot 对象与类型注解的 Bot 类型一致。也就是说,如果你使用的是 Bot 基类,将会允许任何平台的 Bot 对象;如果你使用的是平台特定的 Bot 类型,将会只允许该平台的 Bot 对象,其他类型的 Bot 将会跳过这个事件处理函数。更多详情请参考[事件处理重载](./overload.md)。 + +在其他情况下,我们可以通过 NoneBot 提供的方法来获取 Bot 对象,这些方法将会在[使用适配器](../advanced/adapter.md#获取-bot-对象)中详细介绍: + +```python {4,6} +from nonebot import get_bot + +# 获取当前所有 Bot 中的第一个 +bot = get_bot() +# 获取指定 ID 的 Bot +bot = get_bot("bot_id") +``` + +### 调用 API + +在获得 Bot 对象后,我们可以通过 Bot 的实例方法来调用平台 API: + +```python {2,5} +# 通过 bot.api_name(**kwargs) 的方法调用 API +result = await bot.get_user_info(user_id=12345678) + +# 通过 bot.call_api(api_name, **kwargs) 的方法调用 API +result = await bot.call_api("get_user_info", user_id=12345678) +``` + +:::caution 注意 +实际可以使用的 API 以及参数取决于平台提供的接口以及协议适配器的实现,请参考协议适配器以及平台文档。 +::: + +在了解了如何调用 API 后,我们可以来改进 `weather` 插件,使得消息发送后,调用 `Console` 接口响铃提醒机器人用户: + +```python {4,18} title=weather/__init__.py +from nonebot.adapters.console import Bot, MessageSegment + +@weather.got("location", prompt=MessageSegment.emoji("question") + "请输入地名") +async def got_location(bot: Bot, location: str = ArgPlainText()): + await weather.send( + MessageSegment.markdown( + inspect.cleandoc( + f""" + # {location} + + - 今天 + + ⛅ 多云 20℃~24℃ + """ + ) + ) + ) + await bot.bell() +``` diff --git a/website/versioned_docs/version-2.4.0/appendices/config.mdx b/website/versioned_docs/version-2.4.0/appendices/config.mdx new file mode 100644 index 00000000..482b2acb --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/config.mdx @@ -0,0 +1,632 @@ +--- +sidebar_position: 0 +description: 读取用户配置来控制插件行为 + +options: + menu: + - category: appendices + weight: 10 +--- + +# 配置 + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +配置是项目中非常重要的一部分,为了方便我们控制机器人的行为,NoneBot 提供了一套配置系统。下面我们将会补充[指南](../quick-start.mdx)中的天气插件,使其能够读取用户配置。在这之前,我们需要先了解一下配置系统,如果你已经了解了 NoneBot 中的配置方法,可以跳转到[编写插件配置](#插件配置)。 + +NoneBot 使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取 dotenv 配置文件以及环境变量,从而控制机器人行为。配置文件需要符合 dotenv 格式,复杂数据类型需使用 JSON 格式或 [pydantic 支持格式](https://docs.pydantic.dev/usage/types/)填写。 + +NoneBot 内置的配置项列表及含义可以在[内置配置项](#内置配置项)中查看。 + +:::caution 注意 + +NoneBot 自 2.2.0 起兼容了 Pydantic v1 与 v2 版本,以下文档中 Pydantic 相关示例均采用 v2 版本用法。 + +如果在使用商店或其他第三方插件的过程中遇到 Pydantic 相关警告或报错,例如: + +```python +pydantic_core._pydantic_core.ValidationError: 1 validation error for Config + Input should be a valid dictionary or instance of Config [type=model_type, input_value=Config(...), input_type=Config] +``` + +请考虑降级 Pydantic 至 v1 版本: + +```bash +pip install --force-reinstall 'pydantic~=1.10' +``` + +::: + +## 配置项的加载 + +在 NoneBot 中,我们可以把配置途径分为 **直接传入**、**系统环境变量**、**dotenv 配置文件** 三种,其加载优先级依次由高到低。 + +### 直接传入 + +在 NoneBot 初始化的过程中,可以通过 `nonebot.init()` 传入任意合法的 Python 变量,也可以在初始化完成后直接赋值。 + +通常,在初始化前的传参会在机器人的入口文件(如 `bot.py`)中进行,而初始化后的赋值可以在任何地方进行。 + +```python {4,8,9} title=bot.py +import nonebot + +# 初始化时 +nonebot.init(custom_config1="config on init") + +# 初始化后 +config = nonebot.get_driver().config +config.custom_config1 = "changed after init" +config.custom_config2 = "new config after init" +``` + +### 系统环境变量 + +在 dotenv 配置文件中定义的配置项,也会在环境变量中进行寻找。如果在环境变量中发现同名配置项(大小写不敏感),将会覆盖 dotenv 中所填值。 + +例如,在 dotenv 配置文件中存在配置项 `custom_config`: + +```dotenv +CUSTOM_CONFIG=config in dotenv +``` + +同时,设置环境变量: + +```bash +# windows cmd +set CUSTOM_CONFIG 'config in environment variables' +# windows powershell +$Env:CUSTOM_CONFIG='config in environment variables' +# linux/macOS +export CUSTOM_CONFIG='config in environment variables' +``` + +那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。 + +:::caution 注意 +NoneBot 不会自发读取未被定义的配置项的环境变量,如果需要读取某一环境变量需要在 dotenv 配置文件中进行声明。 +::: + +### dotenv 配置文件 + +dotenv 是一种便捷的跨平台配置通用模式,也是我们推荐的配置方式。 + +NoneBot 在启动时将会从系统环境变量或者 `.env` 文件中寻找配置项 `ENVIRONMENT` (大小写不敏感),默认值为 `prod`。这将决定 NoneBot 后续进一步加载环境配置的文件路径 `.env.{ENVIRONMENT}`。 + +#### 配置项解析 + +dotenv 文件中的配置值使用 JSON 进行解析。如果配置项值无法被解析,将作为**字符串**处理。例如: + +```dotenv +STRING_CONFIG=some string +LIST_CONFIG=[1, 2, 3] +DICT_CONFIG={"key": "value"} +MULTILINE_CONFIG=' +[ + { + "item_key": "item_value" + } +] +' +EMPTY_CONFIG= +NULL_CONFIG +``` + +将被解析为: + +```python +dotenv_config = { + "string_config": "some string", + "list_config": [1, 2, 3], + "dict_config": {"key": "value"}, + "multiline_config": [{"item_key": "item_value"}], + "empty_config": "", + "null_config": None +} +``` + +特别的,NoneBot 支持使用 `env_nested_delimiter` 配置嵌套字典,在层与层之间使用 `__` 分隔即可: + +```dotenv +DICT={"k1": "v1", "k2": null} +DICT__K2=v2 +DICT__K3=v3 +DICT__INNER__K4=v4 +``` + +将被解析为: + +```python +dotenv_config = { + "dict": { + "k1": "v1", + "k2": "v2", + "k3": "v3", + "inner": { + "k4": "v4" + } + } +} +``` + +#### .env 文件 + +`.env` 文件是基础配置文件,该文件中的配置项在不同环境下都会被加载,但会被 `.env.{ENVIRONMENT}` 文件中的配置所**覆盖**。 + +我们可以在 `.env` 文件中写入当前的环境信息: + +```dotenv +ENVIRONMENT=dev +COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载 +``` + +这样,我们在启动 NoneBot 时就会从 `.env.dev` 文件中加载剩余配置项。 + +:::tip 提示 +在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。 +::: + +#### .env.\{ENVIRONMENT\} 文件 + +`.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。 + +在 NoneBot 初始化时,可以指定加载某个环境配置文件: + +```python +nonebot.init(_env_file=".env.dev") +``` + +这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。 + +## 读取全局配置项 + +NoneBot 的全局配置对象可以通过 `driver` 获取,如: + +```python +import nonebot + +config = nonebot.get_driver().config +``` + +如果我们需要获取某个配置项,可以直接通过 `config` 对象的属性访问: + +```python +superusers = config.superusers +``` + +如果配置项不存在,将会抛出异常。 + +## 插件配置 + +在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。 + +在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型: + +```python title=weather/config.py +from pydantic import BaseModel, field_validator + +class Config(BaseModel): + weather_api_key: str + weather_command_priority: int = 10 + weather_plugin_enabled: bool = True + + @field_validator("weather_command_priority") + @classmethod + def check_priority(cls, v: int) -> int: + if v >= 1: + return v + raise ValueError("weather command priority must greater than 1") +``` + +在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。 + +在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置: + +```python {5,11} title=weather/__init__.py +from nonebot import get_plugin_config + +from .config import Config + +plugin_config = get_plugin_config(Config) + +weather = on_command( + "天气", + rule=to_me(), + aliases={"weather", "查天气"}, + priority=plugin_config.weather_command_priority, + block=True, +) +``` + +然后,我们便可以从 `plugin_config` 中读取配置了,例如 `plugin_config.weather_api_key`。 + +这种方式可以简洁、高效地读取配置项,同时也可以设置默认值或者在运行时对配置项进行合法性检查,防止由于配置项导致的插件出错等情况出现。 + +:::tip 提示 +发布插件应该为自身的事件响应器提供可配置的优先级,以便插件使用者可以自定义多个插件间的响应顺序。 +::: + +由于插件配置项是从全局配置中读取的,通常我们需要在配置项名称前面添加前缀名,以防止配置项冲突。例如在上方的示例中,我们就添加了配置项前缀 `weather_`。但是这样会导致在使用配置项时过长的变量名,因此我们可以使用 `pydantic` 的 `alias` 或者通过配置 scope 来简化配置项名称。这里我们以 scope 配置为例: + +```python title=weather/config.py +from pydantic import BaseModel + +class ScopedConfig(BaseModel): + api_key: str + command_priority: int = 10 + plugin_enabled: bool = True + +class Config(BaseModel): + weather: ScopedConfig +``` + +```python title=weather/__init__.py +from nonebot import get_plugin_config + +from .config import Config + +plugin_config = get_plugin_config(Config).weather +``` + +这样我们就可以省略插件配置项名称中的前缀 `weather_` 了。但需要注意的是,如果我们使用了 scope 配置,那么在配置文件中也需要使用 [`env_nested_delimiter` 格式](#配置项解析),例如: + +```dotenv +WEATHER__API_KEY=123456 +WEATHER__COMMAND_PRIORITY=10 +``` + +## 内置配置项 + +配置项 API 文档可以前往 [Config 类](../api/config.md#Config)查看。 + +### Driver + +- **类型**: `str` +- **默认值**: `"~fastapi"` + +NoneBot 运行所使用的驱动器。具体配置方法可以参考[安装驱动器](../tutorial/store.mdx#安装驱动器)和[选择驱动器](../advanced/driver.md)。 + + + + +```dotenv +DRIVER=~fastapi+~httpx+~websockets +``` + + + + +```bash +# windows cmd +set DRIVER '~fastapi+~httpx+~websockets' +# windows powershell +$Env:DRIVER='~fastapi+~httpx+~websockets' +# linux/macOS +export DRIVER='~fastapi+~httpx+~websockets' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(driver="~fastapi+~httpx+~websockets") +``` + + + + +### Host + +- **类型**: `IPvAnyAddress` +- **默认值**: `127.0.0.1` + +当 NoneBot 作为服务端时,监听的 IP / 主机名。 + + + + +```dotenv +HOST=127.0.0.1 +``` + + + + +```bash +# windows cmd +set HOST '127.0.0.1' +# windows powershell +$Env:HOST='127.0.0.1' +# linux/macOS +export HOST='127.0.0.1' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(host="127.0.0.1") +``` + + + + +### Port + +- **类型**: `int` (1 ~ 65535) +- **默认值**: `8080` + +当 NoneBot 作为服务端时,监听的端口。 + + + + +```dotenv +PORT=8080 +``` + + + + +```bash +# windows cmd +set PORT '8080' +# windows powershell +$Env:PORT='8080' +# linux/macOS +export PORT='8080' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(port=8080) +``` + + + + +### Log Level + +- **类型**: `int | str` +- **默认值**: `INFO` + +NoneBot 日志输出等级,可以为 `int` 类型等级或等级名称。具体等级对照表参考 [loguru 日志等级](https://loguru.readthedocs.io/en/stable/api/logger.html#levels)。 + +:::tip 提示 +日志等级名称应为大写,如 `INFO`。 +::: + + + + +```dotenv +LOG_LEVEL=DEBUG +``` + + + + +```bash +# windows cmd +set LOG_LEVEL 'DEBUG' +# windows powershell +$Env:LOG_LEVEL='DEBUG' +# linux/macOS +export LOG_LEVEL='DEBUG' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(log_level="DEBUG") +``` + + + + +### API Timeout + +- **类型**: `float | None` +- **默认值**: `30.0` + +调用平台接口的超时时间,单位为秒。`None` 表示不设置超时时间。 + + + + +```dotenv +API_TIMEOUT=10.0 +``` + + + + +```bash +# windows cmd +set API_TIMEOUT '10.0' +# windows powershell +$Env:API_TIMEOUT='10.0' +# linux/macOS +export API_TIMEOUT='10.0' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(api_timeout=10.0) +``` + + + + +### SuperUsers + +- **类型**: `set[str]` +- **默认值**: `set()` + +机器人超级用户,可以使用权限 [`SUPERUSER`](../api/permission.md#SUPERUSER)。 + + + + +```dotenv +SUPERUSERS=["123123123"] +``` + + + + +```bash +# windows cmd +set SUPERUSERS '["123123123"]' +# windows powershell +$Env:SUPERUSERS='["123123123"]' +# linux/macOS +export SUPERUSERS='["123123123"]' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(superusers={"123123123"}) +``` + + + + +### Nickname + +- **类型**: `set[str]` +- **默认值**: `set()` + +机器人昵称,通常协议适配器会根据用户是否 @bot 或者是否以机器人昵称开头来判断是否是向机器人发送的消息。 + + + + +```dotenv +NICKNAME=["bot"] +``` + + + + +```bash +# windows cmd +set NICKNAME '["bot"]' +# windows powershell +$Env:NICKNAME='["bot"]' +# linux/macOS +export NICKNAME='["bot"]' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(nickname={"bot"}) +``` + + + + +### Command Start 和 Command Separator + +- **类型**: `set[str]` +- **默认值**: + - Command Start: `{"/"}` + - Command Separator: `{"."}` + +命令消息的起始符和分隔符。用于 [`command`](../advanced/matcher.md#command) 规则。 + + + + +```dotenv +COMMAND_START=["/", ""] +COMMAND_SEP=[".", " "] +``` + + + + +```bash +# windows cmd +set COMMAND_START '["/", ""]' +set COMMAND_SEP '[".", " "]' +# windows powershell +$Env:COMMAND_START='["/", ""]' +$Env:COMMAND_SEP='[".", " "]' +# linux/macOS +export COMMAND_START='["/", ""]' +export COMMAND_SEP='[".", " "]' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(command_start={"/", ""}, command_sep={".", " "}) +``` + + + + +### Session Expire Timeout + +- **类型**: `timedelta` +- **默认值**: `timedelta(minutes=2)` + +用户会话超时时间,配置格式参考 [Datetime Types](https://docs.pydantic.dev/latest/api/standard_library_types/#datetimetimedelta)。 + + + + +```dotenv +SESSION_EXPIRE_TIMEOUT=00:02:00 +``` + + + + +```bash +# windows cmd +set SESSION_EXPIRE_TIMEOUT '00:02:00' +# windows powershell +$Env:SESSION_EXPIRE_TIMEOUT='00:02:00' +# linux/macOS +export SESSION_EXPIRE_TIMEOUT='00:02:00' +``` + + + + +```python title=bot.py +import nonebot + +nonebot.init(session_expire_timeout=120) +``` + + + diff --git a/website/versioned_docs/version-2.4.0/appendices/log.md b/website/versioned_docs/version-2.4.0/appendices/log.md new file mode 100644 index 00000000..7fb68686 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/log.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 6 +description: 记录与控制日志 + +options: + menu: + - category: appendices + weight: 70 +--- + +# 日志 + +无论是在开发还是在生产环境中,日志都是一个重要的功能,可以帮助我们了解运行状况、排查问题等。虽然我们可以使用 `print` 来将需要的信息输出到控制台,但是这种方式难以控制,而且不利于日志的归档、分析等。NoneBot 使用优秀的 [Loguru](https://loguru.readthedocs.io/) 库来进行日志记录。 + +## 记录日志 + +我们可以从 NoneBot 中导入 `logger` 对象,然后使用 `logger` 对象的方法来记录日志。 + +```python +from nonebot import logger + +logger.trace("This is a trace message") +logger.debug("This is a debug message") +logger.info("This is an info message") +logger.success("This is a success message") +logger.warning("This is a warning message") +logger.error("This is an error message") +logger.critical("This is a critical message") +``` + +我们仅需一行代码即可记录对应级别的日志。日志可以通过配置 [`LOG_LEVEL` 配置项](./config.mdx#log-level)来过滤输出等级,控制台中仅会输出大于等于 `LOG_LEVEL` 的日志。默认的 `LOG_LEVEL` 为 `INFO`,即只会输出 `INFO`、`SUCCESS`、`WARNING`、`ERROR`、`CRITICAL` 级别的日志。 + +如果需要记录 `Exception traceback` 日志,可以向 `logger` 添加 `exception` 选项: + +```python {4} +try: + 1 / 0 +except ZeroDivisionError: + logger.opt(exception=True).error("ZeroDivisionError") +``` + +如果需要输出彩色日志,可以向 `logger` 添加 `colors` 选项: + +```python +logger.opt(colors=True).warning("We got a BIG problem") +``` + +更多日志记录方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 + +## 自定义日志输出 + +NoneBot 在启动时会添加一个默认的日志处理器,该处理器会将日志输出到**stdout**,并且根据 `LOG_LEVEL` 配置项过滤日志等级。 + +默认的日志格式为: + +```text +{time:MM-DD HH:mm:ss} [{level}] {name} | {message} +``` + +我们可以从 `nonebot.log` 模块导入以使用 NoneBot 的默认格式和过滤器: + +```python +from nonebot.log import default_format, default_filter +``` + +如果需要自定义日志格式,我们需要移除 NoneBot 默认的日志处理器并添加新的日志处理器。例如,在机器人入口文件中 `nonebot.init` 之前添加以下内容: + +```python title=bot.py +from nonebot.log import logger_id + +# 移除 NoneBot 默认的日志处理器 +logger.remove(logger_id) +# 添加新的日志处理器 +logger.add( + sys.stdout, + level=0, + diagnose=True, + format="{time:MM-DD HH:mm:ss} [{level}] {name} | {message}", + filter=default_filter +) +``` + +如果想要输出日志到文件,我们可以使用 `logger.add` 方法添加文件处理器: + +```python title=bot.py +logger.add("error.log", level="ERROR", format=default_format, rotation="1 week") +``` + +更多日志处理器的使用方法请参考 [Loguru 文档](https://loguru.readthedocs.io/)。 + +## 重定向 logging 日志 + +`logging` 是 Python 标准库中的日志模块,NoneBot 提供了一个 logging handler 用于将 `logging` 日志重定向到 `loguru` 处理。 + +```python +from nonebot.log import LoguruHandler + +# root logger 添加 LoguruHandler +logging.basicConfig(handlers=[LoguruHandler()]) +# 或者为其他 logging.Logger 添加 LoguruHandler +logger.addHandler(LoguruHandler()) +``` diff --git a/website/versioned_docs/version-2.4.0/appendices/overload.md b/website/versioned_docs/version-2.4.0/appendices/overload.md new file mode 100644 index 00000000..2ade4b5b --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/overload.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 7 +description: 根据事件类型进行不同的处理 + +options: + menu: + - category: appendices + weight: 80 +--- + +# 事件类型与重载 + +在之前的示例中,我们已经了解了如何[获取事件信息](../tutorial/event-data.mdx)以及[使用平台接口](./api-calling.mdx)。但是,事件信息通常不仅仅包含消息这一个内容,还有其他平台提供的信息,例如消息发送时间、消息发送者等等。同时,在使用平台接口时,我们需要确保使用的**平台接口**与所要发送的**平台类型**一致,对不同类型的事件需要做出不同的处理。在本章节中,我们将介绍如何获取事件更多的信息以及根据事件类型进行不同的处理。 + +## 事件类型 + +在 NoneBot 中,事件均是 `nonebot.adapters.Event` 基类的子类型,基类对一些必要的属性进行了抽象,子类型则根据不同的平台进行了实现。在[自定义权限](./permission.mdx#自定义权限)一节中,我们就使用了 `Event` 的抽象方法 `get_user_id` 来获取事件发送者 ID,这个方法由协议适配器进行了实现,返回机器人用户对应的平台 ID。更多的基类抽象方法可以在[使用适配器](../advanced/adapter.md#获取事件通用信息)中查看。 + +既然事件是基类的子类型,我们实际可以获得的信息通常多于基类抽象方法所提供的。如果我们不满足于基类能获得的信息,我们可以小小的修改一下事件处理函数的事件参数类型注解,使其变为子类型,这样我们就可以通过协议适配器定义的子类型来获取更多的信息。我们以 `Console` 协议适配器为例: + +```python {4} title=weather/__init__.py +from nonebot.adapters.console import MessageEvent + +@weather.got("location", prompt="请输入地名") +async def got_location(event: MessageEvent, location: str = ArgPlainText()): + await weather.finish(f"{event.time.strftime('%Y-%m-%d')} {location} 的天气是...") +``` + +在上面的代码中,我们获取了 `Console` 协议适配器的消息事件提供的发送时间 `time` 属性。 + +:::caution 注意 +如果**基类**就能满足你的需求,那么就**不要修改**事件参数类型注解,这样可以使你的代码更加**通用**,可以在更多平台上运行。如何根据不同平台事件类型进行不同的处理,我们将在[重载](#重载)一节中介绍。 +::: + +## 重载 + +我们在编写机器人时,常常会遇到这样一个问题:如何对私聊和群聊消息进行不同的处理?如何对不同平台的事件进行不同的处理?针对这些问题,NoneBot 提供了一个便捷而高效的解决方案 ── 重载。简单来说,依赖函数会根据其参数的类型注解来决定是否执行,忽略不符合其参数类型注解的情况。这样,我们就可以通过修改事件参数类型注解来实现对不同事件的处理,或者修改 `Bot` 参数类型注解来实现使用不同平台的接口。我们以 `OneBot` 协议适配器为例: + +```python {4,8} +from nonebot.adapters.onebot.v11 import PrivateMessageEvent, GroupMessageEvent + +@matcher.handle() +async def handle_private(event: PrivateMessageEvent): + await matcher.finish("私聊消息") + +@matcher.handle() +async def handle_group(event: GroupMessageEvent): + await matcher.finish("群聊消息") +``` + +这样,机器人用户就会在私聊和群聊中分别收到不同的回复。同样的,我们也可以通过修改 `Bot` 参数类型注解来实现使用不同平台的接口: + +```python +from nonebot.adapters.console import Bot as ConsoleBot +from nonebot.adapters.onebot.v11 import Bot as OneBot + +@matcher.handle() +async def handle_console(bot: ConsoleBot): + await bot.bell() + +@matcher.handle() +async def handle_onebot(bot: OneBot): + await bot.send_group_message(group_id=123123, message="OneBot") +``` + +:::caution 注意 +重载机制对所有的参数类型注解都有效,因此,依赖注入也可以使用这个特性来对不同的返回值进行处理。 + +但 Bot、Event 和 Matcher 三者的参数类型注解具有最高检查优先级,如果三者任一类型注解不匹配,那么其他依赖注入将不会执行(如:`Depends`)。 +::: + +:::tip 提示 +如何更好地编写一个跨平台的插件,我们将在[最佳实践](../best-practice/multi-adapter.mdx)中介绍。 +::: diff --git a/website/versioned_docs/version-2.4.0/appendices/permission.mdx b/website/versioned_docs/version-2.4.0/appendices/permission.mdx new file mode 100644 index 00000000..4c03acb8 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/permission.mdx @@ -0,0 +1,117 @@ +--- +sidebar_position: 5 +description: 控制事件响应器的权限 + +options: + menu: + - category: appendices + weight: 60 +--- + +# 权限控制 + +import Messenger from "@site/src/components/Messenger"; + +**权限控制**是机器人在实际应用中需要解决的重点问题之一,NoneBot 提供了灵活的权限控制机制 —— `Permission`。 + +类似于响应规则 `Rule`,`Permission` 是由非负整数个 `PermissionChecker` 所共同组成的**用于筛选事件**的对象。但需要特别说明的是,权限和响应规则有如下区别: + +1. 权限检查**先于**响应规则检查 +2. `Permission` 只需**其中一个** `PermissionChecker` 返回 `True` 时就会检查通过 +3. 权限检查进行时,上下文中并不存在会话状态 `state` +4. `Rule` 仅在**初次触发**事件响应器时进行检查,在余下的会话中并不会限制事件;而 `Permission` 会**持续生效**,在连续对话中一直对事件主体加以限制。 + +## 基础使用 + +通常情况下,`Permission` 更侧重于对于**触发事件的机器人用户**的筛选,例如由 NoneBot 自身提供的 `SUPERUSER` 权限,便是筛选出会话发起者是否为超级用户。它可以对输入的用户进行鉴别,如果符合要求则会被认为通过并返回 `True`,反之则返回 `False`。 + +简单来说,`Permission` 是一个用于筛选出符合要求的用户的机制,可以通过 `Permission` 精确的控制响应对象的覆盖范围,从而拒绝掉我们所不希望的事件。 + +例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令: + +```python {3,9} title=weather/__init__.py +from typing import Tuple +from nonebot.params import Command +from nonebot.permission import SUPERUSER + +manage = on_command( + ("天气", "启用"), + rule=to_me(), + aliases={("天气", "禁用")}, + permission=SUPERUSER, +) + +@manage.handle() +async def control(cmd: Tuple[str, str] = Command()): + _, action = cmd + if action == "启用": + plugin_config.weather_plugin_enabled = True + elif action == "禁用": + plugin_config.weather_plugin_enabled = False + await manage.finish(f"天气插件已{action}") +``` + +如上方示例所示,在注册事件响应器时,我们设置了 `permission` 参数,那么这个事件处理器在触发事件前的检查阶段会对用户身份进行验证,如果不符合我们设置的条件(此处即为**超级用户**)则不会响应。此时,我们向机器人发送 `/天气.禁用` 指令,机器人不会有任何响应,因为我们还不是机器人的超级管理员。我们在 dotenv 文件中设置了 `SUPERUSERS` 配置项之后,机器人就会响应我们的指令了。 + +```dotenv title=.env +SUPERUSERS=["console_user"] +``` + + + +## 自定义权限 + +与事件响应规则类似,`PermissionChecker` 也是一个返回值为 `bool` 类型的依赖函数,即 `PermissionChecker` 支持依赖注入。例如,我们可以限制用户的指令调用次数: + +```python title=weather/__init__.py +from nonebot.adapters import Event + +fake_db: Dict[str, int] = {} + +async def limit_permission(event: Event): + count = fake_db.setdefault(event.get_user_id(), 100) + if count > 0: + fake_db[event.get_user_id()] -= 1 + return True + return False + +weather = on_command("天气", permission=limit_permission) +``` + +## 权限组合 + +权限之间可以通过 `|` 运算符进行组合,使得任意一个权限检查返回 `True` 时通过。例如: + +```python {4-6} +perm1 = Permission(foo_checker) +perm2 = Permission(bar_checker) + +perm = perm1 | perm2 +perm = perm1 | bar_checker +perm = foo_checker | perm2 +``` + +同样的,我们也无需担心组合了一个 `None` 值,`Permission` 会自动忽略 `None` 值。 + +```python +assert (perm | None) is perm +``` + +## 主动使用权限 + +除了在事件响应器中使用权限外,我们也可以主动使用权限来判断事件是否符合条件。例如: + +```python {3} +perm = Permission(some_checker) + +result: bool = await perm(bot, event) +``` + +我们只需要传入 `Bot` 实例、事件,`Permission` 会并发调用所有 `PermissionChecker` 进行检查,并返回结果。 diff --git a/website/versioned_docs/version-2.4.0/appendices/rule.md b/website/versioned_docs/version-2.4.0/appendices/rule.md new file mode 100644 index 00000000..7b9d7a24 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/rule.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 1 +description: 自定义响应规则 + +options: + menu: + - category: appendices + weight: 20 +--- + +# 响应规则 + +机器人在实际应用中,往往会接收到多种多样的事件类型,NoneBot 通过响应规则来控制事件的处理。 + +在[指南](../tutorial/matcher.md#为事件响应器添加参数)中,我们为 `weather` 命令添加了一个 `rule=to_me()` 参数,这个参数就是一个响应规则,确保只有在私聊或者 `@bot` 时才会响应。 + +响应规则是一个 `Rule` 对象,它由一系列的 `RuleChecker` 函数组成,每个 `RuleChecker` 函数都会检查事件是否符合条件,如果所有的检查都通过,则事件会被处理。 + +## RuleChecker + +`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则: + +```python {7,8} title=weather/__init__.py +from nonebot import get_plugin_config + +from .config import Config + +plugin_config = get_plugin_config(Config) + +async def is_enable() -> bool: + return plugin_config.weather_plugin_enabled + +weather = on_command("天气", rule=is_enable) +``` + +在上面的代码中,我们定义了一个函数 `is_enable`,它会检查配置项 `weather_plugin_enabled` 是否为 `True`。这个函数 `is_enable` 即为一个 `RuleChecker`。 + +## Rule + +`Rule` 是若干个 `RuleChecker` 的集合,它会并发调用每个 `RuleChecker`,只有当所有 `RuleChecker` 检查通过时匹配成功。例如:我们可以组合两个 `RuleChecker`,一个用于检查插件是否启用,一个用于检查用户是否在黑名单中: + +```python {10} +from nonebot.rule import Rule +from nonebot.adapters import Event + +async def is_enable() -> bool: + return plugin_config.weather_plugin_enabled + +async def is_blacklisted(event: Event) -> bool: + return event.get_user_id() not in BLACKLIST + +rule = Rule(is_enable, is_blacklisted) + +weather = on_command("天气", rule=rule) +``` + +## 合并响应规则 + +在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并: + +```python {13} title=weather/__init__.py +from nonebot.rule import to_me +from nonebot import get_plugin_config + +from .config import Config + +plugin_config = get_plugin_config(Config) + +async def is_enable() -> bool: + return plugin_config.weather_plugin_enabled + +weather = on_command( + "天气", + rule=to_me() & is_enable, + aliases={"weather", "查天气"}, + priority=plugin_config.weather_command_priority, + block=True, +) +``` + +这样,`weather` 命令就只会在插件启用且在私聊或者 `@bot` 时才会响应。 + +合并响应规则可以有多种形式,例如: + +```python {4-6} +rule1 = Rule(foo_checker) +rule2 = Rule(bar_checker) + +rule = rule1 & rule2 +rule = rule1 & bar_checker +rule = foo_checker & rule2 +``` + +同时,我们也无需担心合并了一个 `None` 值,`Rule` 会忽略 `None` 值。 + +```python +assert (rule & None) is rule +``` + +## 主动使用响应规则 + +除了在事件响应器中使用响应规则外,我们也可以主动使用响应规则来判断事件是否符合条件。例如: + +```python {3} +rule = Rule(some_checker) + +result: bool = await rule(bot, event, state) +``` + +我们只需要传入 `Bot` 对象、事件和会话状态,`Rule` 会并发调用所有 `RuleChecker` 进行检查,并返回结果。 + +## 内置响应规则 + +NoneBot 内置了一些常用的响应规则,可以直接通过事件响应器辅助函数或者自行合并其他规则使用。内置响应规则列表可以参考[事件响应器进阶](../advanced/matcher.md) diff --git a/website/versioned_docs/version-2.4.0/appendices/session-control.mdx b/website/versioned_docs/version-2.4.0/appendices/session-control.mdx new file mode 100644 index 00000000..c98bf2bf --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/session-control.mdx @@ -0,0 +1,397 @@ +--- +sidebar_position: 2 +description: 更灵活的会话控制 + +options: + menu: + - category: appendices + weight: 30 +--- + +# 会话控制 + +import Messenger from "@site/src/components/Messenger"; + +在[指南](../tutorial/event-data.mdx#使用依赖注入)的 `weather` 插件中,我们使用依赖注入获取了机器人用户发送的地名参数,并根据地名参数进行相应的回复。但是,一问一答的对话模式仅仅适用于简单的对话场景,如果我们想要实现更复杂的对话模式,就需要使用会话控制。 + +## 询问并获取用户输入 + +在 `weather` 插件中,我们对于用户未输入地名参数的情况直接回复了 `请输入地名` 并结束了事件流程。但是,这样用户体验并不好,需要重新输入指令和地名参数才能获取天气回复。我们现在来实现询问并获取用户地名参数的功能。 + +### 询问用户 + +我们可以使用事件响应器操作中的 `got` 装饰器来表示当前事件处理流程需要询问并获取用户输入的消息: + +```python {6} title=weather/__init__.py +@weather.handle() +async def handle_function(args: Message = CommandArg()): + if location := args.extract_plain_text(): + await weather.finish(f"今天{location}的天气是...") + +@weather.got("location", prompt="请输入地名") +async def got_location(): + ... +``` + +在上面的代码中,我们使用 `got` 事件响应器操作来向用户发送 `prompt` 消息,并等待用户的回复。用户的回复消息将会被作为 `location` 参数存储于事件响应器状态中。 + +:::tip 提示 +事件处理函数根据定义的顺序依次执行。 +::: + +### 获取用户输入 + +在询问以及用户回复之后,我们就可以获取到我们需要的 `location` 参数了。我们使用 `ArgPlainText` 依赖注入来获取参数纯文本信息: + +```python {9} title=weather/__init__.py +from nonebot.params import ArgPlainText + +@weather.handle() +async def handle_function(args: Message = CommandArg()): + if location := args.extract_plain_text(): + await weather.finish(f"今天{location}的天气是...") + +@weather.got("location", prompt="请输入地名") +async def got_location(location: str = ArgPlainText()): + await weather.finish(f"今天{location}的天气是...") +``` + + + +在上面的代码中,我们在 `got_location` 函数中定义了一个依赖注入参数 `location`,他的值将会是用户回复的消息纯文本信息。获取到用户输入的地名参数后,我们就可以进行天气查询并回复了。 + +:::tip 提示 +如果想要获取用户回复的消息对象 `Message` ,可以使用 `Arg` 依赖注入。 +::: + +### 跳过询问 + +在上面的代码中,如果用户在输入天气指令时,同时提供了地名参数,我们直接回复了天气信息,这部分的逻辑是和询问用户地名参数之后的逻辑一致的。如果在复杂的业务场景下,我们希望这部分代码应该复用以减少代码冗余。我们可以使用事件响应器操作中的 `set_arg` 来主动设置一个参数: + +```python {4,6} title=weather/__init__.py +from nonebot.matcher import Matcher + +@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()): + await weather.finish(f"今天{location}的天气是...") +``` + +请注意,设置参数需要使用依赖注入来获取 `Matcher` 实例以确保上下文正确,且参数值应为 `Message` 对象。 + +在 `location` 参数被设置之后,`got` 事件响应器操作将不再会询问并等待用户的回复,而是直接进入 `got_location` 函数。 + +## 请求重新输入 + +在实际的业务场景中,用户的输入很有可能并非是我们所期望的,而结束事件处理流程让用户重新发送指令也不是一个好的体验。这时我们可以使用 `reject` 事件响应器操作来请求用户重新输入: + +```python {8,9} title=weather/__init__.py +@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}的天气是...") +``` + + + +在上面的代码中,我们在 `got_location` 函数中判断用户输入的地名是否在支持的城市列表中,如果不在,则使用 `reject` 事件响应器操作。操作将会向用户发送 `reject` 参数中的消息,并等待用户回复后,重新执行 `got_location` 函数。通过 `got` 和 `reject` 事件响应器操作,我们实现了类似于**循环**的执行方式。 + +`reject` 事件响应器操作与 `finish` 类似,NoneBot 会在向机器人用户发送消息内容后抛出 `RejectedException` 异常来暂停事件响应流程以等待用户输入。也就是说,在 `reject` 被执行后,后续的程序同样是不会被执行的。 + +## 更多事件响应器操作 + +在之前的章节中,我们已经大致了解了五个事件响应器操作:`handle`、`got`、`finish`、`send` 和 `reject`。现在我们来完整地介绍一下这些操作。 + +事件响应器操作可以分为两大类:**交互操作**和**流程控制操作**。我们可以通过交互操作来与用户进行交互,而流程控制操作则可以用来控制事件处理流程的执行。 + +:::tip 提示 +事件处理流程按照事件处理函数添加顺序执行,已经结束的事件处理函数不可能被恢复执行。 +::: + +### handle + +`handle` 事件响应器操作是一个装饰器,用于向事件处理流程添加一个事件处理函数。 + +```python +@matcher.handle() +async def handle_func(): + ... +``` + +`handle` 装饰器支持嵌套操作,即一个事件处理函数可以被添加多次: + +```python +@matcher.handle() +@matcher.handle() +async def handle_func(): + # 这个函数会被执行两次 + ... +``` + +### got + +`got` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。它可以通过 `prompt` 参数来向用户发送询问消息,然后等待用户的回复消息,贴近对话形式会话。 + +`got` 装饰器接受一个参数 `key` 和一个可选参数 `prompt`。当会话状态中不存在 `key` 对应的消息时,会向用户发送 `prompt` 参数的消息,并等待用户回复。`prompt` 参数的类型和 [`send`](#send) 事件响应器操作的参数类型一致。 + +在事件处理函数中,可以通过依赖注入的方式来获取接收到的消息,参考:[`Arg`](../advanced/dependency.mdx#arg)、[`ArgStr`](../advanced/dependency.mdx#argstr)、[`ArgPlainText`](../advanced/dependency.mdx#argplaintext)。 + +```python +@matcher.got("key", prompt="请输入...") +async def got_func(key: Message = Arg()): + ... +``` + +`got` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: + +```python +@matcher.got("key1", prompt="请输入key1...") +@matcher.got("key2", prompt="请输入key2...") +@matcher.receive("key3") +async def got_func(key1: Message = Arg(), key2: Message = Arg(), key3: Event = Received("key3")): + ... +``` + +### receive + +`receive` 事件响应器操作也是一个装饰器,它会在当前装饰的事件处理函数运行之前,中断当前事件处理流程,等待接收一个新的事件。与 `got` 不同的是,`receive` 不会向用户发送询问消息,并且等待一个用户事件。可以接收的事件类型取决于[会话更新](../advanced/session-updating.md)。 + +`receive` 装饰器接受一个可选参数 id,用于标识当前需要接收的事件,如果不指定,则默认为空 `""`。 + +在事件处理函数中,可以通过依赖注入的方式来获取接收到的事件,参考:[`Received`](../advanced/dependency.mdx#received)、[`LastReceived`](../advanced/dependency.mdx#lastreceived)。 + +```python +@matcher.receive("id") +async def receive_func(event: Event = Received("id")): + ... +``` + +`receive` 装饰器支持与 `got` 和 `receive` 装饰器嵌套操作,即一个事件处理函数可以在接收多个事件或消息后执行: + +```python +@matcher.receive("key1") +@matcher.got("key2", prompt="请输入key2...") +@matcher.got("key3", prompt="请输入key3...") +async def receive_func(key1: Event = Received("key1"), key2: Message = Arg(), key3: Message = Arg()): + ... +``` + +### send + +`send` 事件响应器操作用于向用户回复一条消息。协议适配器会根据当前 event 选择回复的途径。 + +`send` 操作接受一个参数 message 和其他任何协议适配器接受的参数。message 参数类型可以是字符串、消息序列、消息段或者消息模板。消息模板将会使用会话状态字典进行渲染后发送。 + +这个操作等同于使用 `bot.send(event, message, **kwargs)`,但不需要自行传入 `event`。 + +```python +@matcher.handle() +async def _(): + await matcher.send("Hello world!") +``` + +### finish + +向用户回复一条消息(可选),并立即结束**整个处理流程**。 + +参数与 [`send`](#send) 相同。 + +```python +@matcher.handle() +async def _(): + await matcher.finish("Hello world!") + # 下面的代码不会被执行 +``` + +### pause + +向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后进入**下一个**事件处理函数。 + +参数与 [`send`](#send) 相同。 + +```python +@matcher.handle() +async def _(): + if need_confirm: + await matcher.pause("请在两分钟内确认执行") + +@matcher.handle() +async def _(): + ... +``` + +### reject + +向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 + +`reject` 可以用于拒绝当前 `receive` 接收的事件或 `got` 接收的参数。通常在用户回复不符合格式或标准需要重新输入,或者用于循环进行用户交互。 + +参数与 [`send`](#send) 相同。 + +```python +@matcher.got("arg") +async def _(arg: str = ArgPlainText()): + if not is_valid(arg): + await matcher.reject("Invalid arg!") +``` + +### reject_arg + +向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的消息后再次执行**当前**事件处理函数。 + +`reject_arg` 用于拒绝指定 `got` 接收的参数,通常在嵌套装饰器时使用。 + +`reject_arg` 操作接受一个 key 参数以及可选的 prompt 参数。prompt 参数与 [`send`](#send) 相同。 + +```python +@matcher.got("a") +@matcher.got("b") +async def _(a: str = ArgPlainText(), b: str = ArgPlainText()): + if a not in b: + await matcher.reject_arg("a", "Invalid a!") +``` + +### reject_receive + +向用户回复一条消息(可选),立即结束**当前**事件处理函数,等待接收一个新的事件后再次执行**当前**事件处理函数。 + +`reject_receive` 用于拒绝指定 `receive` 接收的事件,通常在嵌套装饰器时使用。 + +`reject_receive` 操作接受一个可选的 id 参数以及可选的 prompt 参数。id 参数默认为空 `""`,prompt 参数与 [`send`](#send) 相同。 + +```python +@matcher.receive("a") +@matcher.receive("b") +async def _(a: Event = Received("a"), b: Event = Received("b")): + if a.get_user_id() != b.get_user_id(): + await matcher.reject_receive("a") +``` + +### skip + +立即结束当前事件处理函数,进入下一个事件处理函数。 + +通常在依赖注入中使用,用于跳过当前事件处理函数的执行。 + +```python +from nonebot.params import Depends + +async def dependency(): + matcher.skip() + +@matcher.handle() +async def _(check=Depends(dependency)): + # 这个函数不会被执行 +``` + +### stop_propagation + +阻止事件向更低优先级的事件响应器传播。 + +```python +from nonebot.matcher import Matcher + +@foo.handle() +async def _(matcher: Matcher): + matcher.stop_propagation() +``` + +:::caution 注意 +`stop_propagation` 操作是实例方法,需要先通过依赖注入获取事件响应器实例再进行调用。 +::: + +### get_arg + +获取一个 `got` 接收的参数。 + +`get_arg` 操作接受一个 key 参数和一个可选的 default 参数。当参数不存在时,将返回 default 或 `None`。 + +```python +from nonebot.matcher import Matcher + +@matcher.handle() +async def _(matcher: Matcher): + key = matcher.get_arg("key", default=None) +``` + +### set_arg + +设置 / 覆盖一个 `got` 接收的参数。 + +`set_arg` 操作接受一个 key 参数和一个 value 参数。请注意,value 参数必须是消息序列对象,如需存储其他数据请使用[会话状态](./session-state.md)。 + +```python +from nonebot.matcher import Matcher + +@matcher.handle() +async def _(matcher: Matcher): + matcher.set_arg("key", Message("value")) +``` + +### get_receive + +获取一个 `receive` 接收的事件。 + +`get_receive` 操作接受一个 id 参数和一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 + +```python +from nonebot.matcher import Matcher + +@matcher.handle() +async def _(matcher: Matcher): + event = matcher.get_receive("id", default=None) +``` + +### get_last_receive + +获取最近的一个 `receive` 接收的事件。 + +`get_last_receive` 操作接受一个可选的 default 参数。当事件不存在时,将返回 default 或 `None`。 + +```python +from nonebot.matcher import Matcher + +@matcher.handle() +async def _(matcher: Matcher): + event = matcher.get_last_receive(default=None) +``` + +### set_receive + +设置 / 覆盖一个 `receive` 接收的事件。 + +`set_receive` 操作接受一个 id 参数和一个 event 参数。请注意,event 参数必须是事件对象,如需存储其他数据请使用[会话状态](./session-state.md)。 + +```python +from nonebot.matcher import Matcher + +@matcher.handle() +async def _(matcher: Matcher): + matcher.set_receive("key", Event()) +``` diff --git a/website/versioned_docs/version-2.4.0/appendices/session-state.md b/website/versioned_docs/version-2.4.0/appendices/session-state.md new file mode 100644 index 00000000..d6780fc9 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/session-state.md @@ -0,0 +1,59 @@ +--- +sidebar_position: 3 +description: 会话状态信息 + +options: + menu: + - category: appendices + weight: 40 +--- + +# 会话状态 + +在事件处理流程中,和用户交互的过程即是会话。在会话中,我们可能需要记录一些信息,例如用户的重试次数等等,以便在会话中的不同阶段进行判断和处理。这些信息都可以存储于会话状态中。 + +NoneBot 中的会话状态是一个字典,可以通过类型 `T_State` 来获取。字典内可以存储任意类型的数据,但是要注意的是,NoneBot 本身会在会话状态中存储一些信息,因此不要使用 [NoneBot 使用的键名](../api/consts.md)。 + +```python +from nonebot.typing import T_State + +@matcher.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 matcher.finish("密码错误次数过多") + else: + state["try_count"] = try_count + 1 + await matcher.reject("密码错误,请重新输入") + await matcher.finish("密码正确") +``` + +会话状态的生命周期与事件处理流程相同,在期间的任何一个事件处理函数都可以进行读写。 + +```python +from nonebot.typing import T_State + +@matcher.handle() +async def _(state: T_State): + state["key"] = "value" + +@matcher.handle() +async def _(state: T_State): + await matcher.finish(state["key"]) +``` + +会话状态还可以用于发送动态消息,消息模板在发送时会使用会话状态字典进行渲染。消息模板的使用方法已经在[消息处理](../tutorial/message.md#使用消息模板)中介绍过,这里不再赘述。 + +```python +from nonebot.typing import T_State +from nonebot.adapters import MessageTemplate + +@matcher.handle() +async def _(state: T_State): + state["username"] = "user" + +@matcher.got("password", prompt=MessageTemplate("请输入 {username} 的密码")) +async def _(): + await matcher.finish(MessageTemplate("密码为 {password}")) +``` diff --git a/website/versioned_docs/version-2.4.0/appendices/whats-next.md b/website/versioned_docs/version-2.4.0/appendices/whats-next.md new file mode 100644 index 00000000..4b1a3f3f --- /dev/null +++ b/website/versioned_docs/version-2.4.0/appendices/whats-next.md @@ -0,0 +1,11 @@ +--- +sidebar_position: 99 +description: 下一步──进阶! +--- + +# 下一步 + +至此,我们已经了解了 NoneBot 的大多数功能用法,相信你已经可以独自写出一个插件了。现在你可以选择: + +- 即刻开始插件编写! +- 更深入地了解 NoneBot 的[更多功能和原理](../advanced/plugin-info.md)! diff --git a/website/versioned_docs/version-2.4.0/best-practice/alconna/README.mdx b/website/versioned_docs/version-2.4.0/best-practice/alconna/README.mdx new file mode 100644 index 00000000..b6f79eac --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/alconna/README.mdx @@ -0,0 +1,141 @@ +--- +sidebar_position: 1 +description: Alconna 命令解析拓展 + +slug: /best-practice/alconna/ +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Alconna 插件 + +[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类提供了拓展响应规则的插件。 +该插件使用 [Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, +是一个简单、灵活、高效的命令参数解析器,并且不局限于解析命令式字符串。 + +该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 + +该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如: + +- `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数 +- `dispatch`:类似 `CommandGroup`,对目标路径创建一个新的 `AlconnaMatcher`,并将解析结果分配给该 `AlconnaMatcher` +- `got_path`:类似 `got`,但是可以指定目标路径,并且能够验证解析结果是否可用 +- ... + +基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 +标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 + +该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 + +## 安装插件 + +在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: + +在**项目目录**下执行以下命令: + + + + +```shell +nb plugin install nonebot-plugin-alconna +``` + + + + +```shell +pip install nonebot-plugin-alconna +``` + + + + + +```shell +pdm add nonebot-plugin-alconna +``` + + + + +## 导入插件 + +由于 `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 +``` + +## 使用插件 + +在前面的[深入指南](../../appendices/session-control.mdx)中,我们已经得到了一个天气插件。 +现在我们将使用 `Alconna` 来改写这个插件。 + +
+ 插件示例 + +```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}的天气是...") +``` + +
+ +```python {5-9,13-15,17-18} +from nonebot.rule import to_me +from arclet.alconna import Alconna, Args +from nonebot_plugin_alconna import Match, on_alconna + +weather = on_alconna( + Alconna("天气", Args["location?", str]), + aliases={"weather", "天气预报"}, + rule=to_me(), +) + + +@weather.handle() +async def handle_function(location: Match[str]): + if location.available: + weather.set_path_arg("location", location.result) + +@weather.got_path("location", prompt="请输入地名") +async def got_location(location: str): + if location not in ["北京", "上海", "广州", "深圳"]: + await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") + await weather.finish(f"今天{location}的天气是...") +``` + +在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `Match` 来获取解析结果。 + +关于更多 `Alconna` 的使用方法,可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna), +或阅读 [Alconna 基本介绍](./command.md) 一节。 + +关于更多 `on_alconna` 的使用方法,可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md), +或阅读 [响应规则的使用](./matcher.mdx) 一节。 + +## 交流与反馈 + +QQ 交流群: [🔗 链接](https://jq.qq.com/?_wv=1027&k=PUPOnCSH) + +友链: [📚 文档](https://graiax.cn/guide/message_parser/alconna.html) diff --git a/website/versioned_docs/version-2.4.0/best-practice/alconna/_category_.json b/website/versioned_docs/version-2.4.0/best-practice/alconna/_category_.json new file mode 100644 index 00000000..d8e7367f --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/alconna/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Alconna 命令解析拓展", + "position": 6 +} diff --git a/website/versioned_docs/version-2.4.0/best-practice/alconna/command.md b/website/versioned_docs/version-2.4.0/best-practice/alconna/command.md new file mode 100644 index 00000000..e5df7cbd --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/alconna/command.md @@ -0,0 +1,640 @@ +--- +sidebar_position: 2 +description: Alconna 基本介绍 +--- + +# Alconna 本体 + +[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 + +我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: + +```python +from arclet.alconna import Alconna, Args, Subcommand, Option + + +alc = Alconna( + "pip", + Subcommand( + "install", + Args["package", str], + Option("-r|--requirement", Args["file", str]), + Option("-i|--index-url", Args["url", str]), + ) +) + +res = alc.parse("pip install nonebot2 -i URL") + +print(res) +# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} + +print(res.all_matched_args) +# {'package': 'nonebot2', 'url': 'URL'} +``` + +这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 + +## 命令头 + +命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 !help 中的 ! 与 help。 + +| 前缀 | 命令名 | 匹配内容 | 说明 | +| :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: | +| - | "foo" | `"foo"` | 无前缀的纯文字头 | +| - | 123 | `123` | 无前缀的元素头 | +| - | "re:\d{2}" | `"32"` | 无前缀的正则头 | +| - | int | `123` 或 `"456"` | 无前缀的类型头 | +| [int, bool] | - | `True` 或 `123` | 无名的元素类头 | +| ["foo", "bar"] | - | `"foo"` 或 `"bar"` | 无名的纯文字头 | +| ["foo", "bar"] | "baz" | `"foobaz"` 或 `"barbaz"` | 纯文字头 | +| [int, bool] | "foo" | `[123, "foo"]` 或 `[False, "foo"]` | 类型头 | +| [123, 4567] | "foo" | `[123, "foo"]` 或 `[4567, "foo"]` | 元素头 | +| [nepattern.NUMBER] | "bar" | `[123, "bar"]` 或 `[123.456, "bar"]` | 表达式头 | +| [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 | +| [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 | + +对于无前缀的类型头,此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。解析后,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 + +:::tip + +**正则内容只在命令名上生效,前缀中的正则会被转义** + +::: + +除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: + +```python +from alconna import Alconna + + +alc = Alconna(".rd{roll:int}") +assert alc.parse(".rd123").header["roll"] == 123 +``` + +Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型 + +`"{}"` 中的内容为 "name:type or pat": + +- `"{}"`, `"{:}"` ⇔ `"(.+)"`, 占位符 +- `"{foo}"` ⇔ `"(?P<foo>.+)"` +- `"{:\d+}"` ⇔ `"(\d+)"` +- `"{foo:int}"` ⇔ `"(?P<foo>\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式 + +## 参数声明(Args) + +`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : + +- `Args[key, var, default][key1, var1, default1][...]` +- `Args[(key, var, default)]` +- `Args.key[var, default]` + +其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** + +其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 + +### key + +`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 + +其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: + +- `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 +- `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 +- `/` 标识符表示该参数的类型注解需要隐藏。 + +另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: +`foo#这是注释;?` 或 `foo?#这是注释` + +:::tip + +`Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("test", Args["foo", str]) +alc.parse("test --foo abc") # 错误 +alc.parse("test abc") # 正确 +``` + +若需要 `test --foo abc`,你应该使用 `Option`: + +```python +from arclet.alconna import Alconna, Args, Option + + +alc = Alconna("test", Option("--foo", Args["foo", str])) +``` + +::: + +### var + +var 负责命令参数的**类型检查**与**类型转化** + +`Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: + +```python +from arclet.alconna import Args +from nepattern import BasePattern + + +# 表示 foo 参数需要匹配一个 @number 样式的字符串 +args = Args["foo", BasePattern("@\d+")] +``` + +`pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` + +`nepattern.global_patterns`默认支持的类型有: + +- `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}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) +- ... + +**特别的**,你可以不传入 `var`,此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。 + +::: + +#### MultiVar 与 KeyWordVar + +`MultiVar` 是一个特殊的标注,用于告知解析器该参数可以接受多个值,类似于函数中的 `*args`,其构造方法形如 `MultiVar(str)`。 + +同样的还有 `KeyWordVar`,类似于函数中的 `*, name: type`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 + +:::tip + +`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,类似于函数中的 `**kwargs`,其构造方法形如 `MultiVar(KeyWordVar(str))` + +`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 + +`MultiVar` 不能在 `KeyWordVar` 之后传入 + +::: + +### default + +`default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 + +默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。 + +`Field` 构造需要的参数说明如下: + +- default: 参数单元的默认值 +- alias: 参数单元默认值的别名 +- completion: 参数单元的补全说明生成函数 +- unmatch_tips: 参数单元的错误提示生成函数,其接收一个表示匹配失败的元素的参数 +- missing_tips: 参数单元的缺失提示生成函数 + +## 选项与子命令(Option & Subcommand) + +`Option` 和 `Subcommand` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")`,`Subcommand("foo", alias=["F"])` + +传入别名后,选项与子命令会选择其中长度最长的作为其名称。若传入为 "--foo|-f",则命令名称为 "--foo" + +:::tip 特别提醒!!! + +Option 的名字或别名**没有要求**必须在前面写上 `-` + +Option 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand** + +::: + +他们拥有如下共同参数: + +- `help_text`: 传入该组件的帮助信息 +- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) +- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 + 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: + +```python +Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) +``` + +- `default`: 默认值,在该组件未被解析时使用使用该值替换。 + 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: + +```python +from arclet.alconna import Option, OptionResult + +opt1 = Option("--foo", default=False) +opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) +``` + +### Action + +`Option` 可以特别设置传入一类 `Action`,作为解析操作 + +`Action` 分为三类: + +- `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 +- `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 +- `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 + +`Alconna` 提供了预制的几类 `Action`: + +- `store`(默认),`store_value`,`store_true`,`store_false` +- `append`,`append_value` +- `count` + +## 解析结果(Arparma) + +`Alconna.parse` 会返回由 **Arparma** 承载的解析结果 + +`Arparma` 有如下属性: + +- 调试类 + + - matched: 是否匹配成功 + - error_data: 解析失败时剩余的数据 + - error_info: 解析失败时的异常内容 + - origin: 原始命令,可以类型标注 + +- 分析类 + - header_match: 命令头部的解析结果,包括原始头部、解析后头部、解析结果与可能的正则匹配组 + - main_args: 命令的主参数的解析结果 + - options: 命令所有选项的解析结果 + - subcommands: 命令所有子命令的解析结果 + - other_args: 除主参数外的其他解析结果 + - all_matched_args: 所有 Args 的解析结果 + +`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 + +`path` 支持如下: + +- `main_args`, `options`, ...: 返回对应的属性 +- `args`: 返回 all_matched_args +- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值 +- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值 +- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult) +- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值 +- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典 +- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 ... + +## 元数据(CommandMeta) + +`Alconna` 的元数据相当于其配置,拥有以下条目: + +- `description`: 命令的描述 +- `usage`: 命令的用法 +- `example`: 命令的使用样例 +- `author`: 命令的作者 +- `fuzzy_match`: 命令是否开启模糊匹配 +- `fuzzy_threshold`: 模糊匹配阈值 +- `raise_exception`: 命令是否抛出异常 +- `hide`: 命令是否对 manager 隐藏 +- `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏 +- `keep_crlf`: 命令解析时是否保留换行字符 +- `compact`: 命令是否允许第一个参数紧随头部 +- `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数 +- `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)` +- `extra`: 命令的自定义额外信息 + +元数据一定使用 `meta=...` 形式传入: + +```python +from arclet.alconna import Alconna, CommandMeta + +alc = Alconna(..., meta=CommandMeta("foo", example="bar")) +``` + +## 命名空间配置 + +命名空间配置 (以下简称命名空间) 相当于 `Alconna` 的默认配置,其优先度低于 `CommandMeta`。 + +`Alconna` 默认使用 "Alconna" 命名空间。 + +命名空间有以下几个属性: + +- name: 命名空间名称 +- prefixes: 默认前缀配置 +- separators: 默认分隔符配置 +- formatter_type: 默认格式化器类型 +- fuzzy_match: 默认是否开启模糊匹配 +- raise_exception: 默认是否抛出异常 +- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) +- disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp) +- enable_message_cache: 默认是否启用消息缓存 +- compact: 默认是否开启紧凑模式 +- strict: 命令是否严格匹配 +- context_style: 命令上下文插值的风格 +- ... + +### 新建命名空间并替换 + +```python +from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config + + +ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ + +alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 + +# 可以通过with方式创建命名空间 +with namespace("bar") as np1: + np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 + np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter + np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 + +# 你还可以使用config来管理所有命名空间并切换至任意命名空间 +config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 + +alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 +``` + +### 修改默认的命名空间 + +```python +from arclet.alconna import config, namespace, Namespace + + +config.default_namespace.prefixes = [...] # 直接修改默认配置 + +np = Namespace("xxx", prefixes=[...]) +config.default_namespace = np # 更换默认的命名空间 + +with namespace(config.default_namespace.name) as np: + np.prefixes = [...] +``` + +## 快捷指令 + +快捷命令可以做到标识一段命令, 并且传递参数给原命令 + +一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) + +`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: + +```python +class ShortcutArgs(TypedDict): + """快捷指令参数""" + + command: NotRequired[str] + """快捷指令的命令""" + args: NotRequired[list[Any]] + """快捷指令的附带参数""" + fuzzy: NotRequired[bool] + """是否允许命令后随参数""" + prefix: NotRequired[bool] + """是否调用时保留指令前缀""" + wrapper: NotRequired[ShortcutRegWrapper] + """快捷指令的正则匹配结果的额外处理函数""" + humanized: NotRequired[str] + """快捷指令的人类可读描述""" +``` + +### args的使用 + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("setu", Args["count", int]) + +alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) +# 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' + +alc.parse("涩图3张").query("count") +# 3 +``` + +### command的使用 + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("eval", Args["content", str]) + +alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) +# 'Alconna::eval 的快捷指令: "echo" 添加成功' + +alc.shortcut("echo", delete=True) # 删除快捷指令 +# 'Alconna::eval 的快捷指令: "echo" 删除成功' + +@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 +def cb(content: str): + eval(content, {}, {}) + +alc.parse('eval print(\\"hello world\\")') +# hello world + +alc.parse("echo hello world!") +# hello world! +``` + +当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 + +快捷指令允许三类特殊的 placeholder: + +- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 + +例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` + +- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 + +- `{X}`: 表示此处填入可能的正则匹配的组: + +- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 +- 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 + +除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 + +例如: + +- `cmd --shortcut ` 来增加一个快捷指令 +- `cmd --shortcut list` 来列出当前指令的所有快捷指令 +- `cmd --shortcut delete key` 来删除一个快捷指令 + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("eval", Args["content", str]) + +alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) + +alc.parse("eval --shortcut list") +# 'echo' +``` + +## 紧凑命令 + +`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: + +```python +from arclet.alconna import Alconna, Option, CommandMeta, Args + + +alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) + +assert alc.parse("test123 BARabc").matched +``` + +这使得我们可以实现如下命令: + +```python +from arclet.alconna import Alconna, Option, Args, append + + +alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) +print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) +# ['abc', 'def', 'xyz'] +``` + +当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: + +```python +from arclet.alconna import Alconna, Option, count + + +alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) +print(alc.parse("pp -vvv").query[int]("verbose.value")) +# 3 +``` + +## 模糊匹配 + +模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 + +```python +from arclet.alconna import Alconna, CommandMeta + + +alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) + +alc.parse("test_fuzy") +# test_fuzy is not matched. Do you mean "test_fuzzy"? +``` + +## 半自动补全 + +半自动补全为用户提供了推荐后续输入的功能 + +补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) + +```python +from arclet.alconna import Alconna, Args, Option + + +alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") +alc.parse("test --comp") + +''' +output + +以下是建议的输入: +* +* --help +* -h +* -sct +* --shortcut +* foo +* bar +''' +``` + +## Duplication + +**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** + +普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 + +以pip为例,其对应的 Duplication 应如下构造: + +```python +from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count + + +class MyDup(Duplication): + verbose: OptionResult + install: SubcommandStub + + +alc = Alconna( + "pip", + Subcommand( + "install", + Args["package", str], + Option("-r|--requirement", Args["file", str]), + Option("-i|--index-url", Args["url", str]), + ), + Option("-v|--version"), + Option("-v|--verbose", action=count), +) + +res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 +print(res.query("install")) +# (value=Ellipsis args={'package': '...'} options={} subcommands={}) + +result = alc.parse("pip -v install ...", duplication=MyDup) +print(result.install) +# SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') +``` + +**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: + +```python +from typing import Optional +from arclet.alconna import Duplication + + +class MyDup(Duplication): + package: str + file: Optional[str] = None + url: Optional[str] = None +``` + +## 上下文插值 + +当 `context_style` 条目被设置后,传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。 + +上下文可以在 `parse` 中传入: + +```python +from arclet.alconna import Alconna, Args, CommandMeta + +alc = Alconna("test", Args["foo", int], meta=CommandMeta(context_style="parentheses")) + +alc.parse("test $(bar)", {"bar": 123}) +# {"foo": 123} +``` + +context_style 的值分两种: + +- `"bracket"`: 插值格式为 `{...}`,例如 `{foo}` +- `"parentheses"`: 插值格式为 `$(...)`,例如 `$(bar)` diff --git a/website/versioned_docs/version-2.4.0/best-practice/alconna/config.md b/website/versioned_docs/version-2.4.0/best-practice/alconna/config.md new file mode 100644 index 00000000..ebce8d57 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/alconna/config.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 4 +description: 配置项 +--- + +# 配置项 + +## alconna_auto_send_output + +- **类型**: `bool` +- **默认值**: `False` + +是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 + +## alconna_use_command_start + +- **类型**: `bool` +- **默认值**: `False` + +是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀 + +## alconna_auto_completion + +- **类型**: `bool` +- **默认值**: `False` + +是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。 + +## alconna_use_origin + +- **类型**: `bool` +- **默认值**: `False` + +是否全局使用原始消息 (即未经过 to_me 等处理的),该选项会影响到 Alconna 的匹配行为。 + +## alconna_use_command_sep + +- **类型**: `bool` +- **默认值**: `False` + +是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。 + +## alconna_global_extensions + +- **类型**: `List[str]` +- **默认值**: `[]` + +全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 + +## alconna_context_style + +- **类型**: `Optional[Literal["bracket", "parentheses"]]` +- **默认值**: `None` + +全局命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`。 + +## alconna_enable_saa_patch + +- **类型**: `bool` +- **默认值**: `False` + +是否启用 SAA 补丁。 + +## alconna_apply_filehost + +- **类型**: `bool` +- **默认值**: `False` + +是否启用文件托管。 + +## alconna_apply_fetch_targets + +- **类型**: `bool` +- **默认值**: `False` + +是否启动时拉取一次发送对象列表。 diff --git a/website/versioned_docs/version-2.4.0/best-practice/alconna/matcher.mdx b/website/versioned_docs/version-2.4.0/best-practice/alconna/matcher.mdx new file mode 100644 index 00000000..be9a670f --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/alconna/matcher.mdx @@ -0,0 +1,607 @@ +--- +sidebar_position: 3 +description: 响应规则的使用 +--- + +import Messenger from "@site/src/components/Messenger"; + +# Alconna 插件 + +展示: + +```python +from nonebot_plugin_alconna import At, Image, on_alconna +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"), + Option("icon", Args["icon", Image]) +) +rg = on_alconna(alc, auto_send_output=True) + + +@rg.handle() +async def _(result: Arparma): + if result.find("list"): + img: bytes = await gen_role_group_list_image() + await rg.finish(Image(raw=img)) + if result.find("add"): + group = await create_role_group(result.query[str]("add.name")) + if result.find("add.member"): + ats = result.query[tuple[At, ...]]("add.member.target") + group.extend(member.target for member in ats) + await rg.finish("添加成功") +``` + +## 响应器使用 + +本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`: + +```python +def on_alconna( + command: Alconna | str, + skip_for_unmatch: bool = True, + auto_send_output: bool = False, + aliases: set[str | tuple[str, ...]] | None = None, + comp_config: CompConfig | None = None, + extensions: list[type[Extension] | Extension] | None = None, + exclude_ext: list[type[Extension] | str] | None = None, + use_origin: bool = False, + use_cmd_start: bool = False, + use_cmd_sep: bool = False, + **kwargs, + ..., +): +``` + +- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令 +- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应 +- `auto_send_output`: 是否自动发送输出信息并跳过响应 +- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases +- `comp_config`: 补全会话配置, 不传入则不启用补全会话 +- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例 +- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id +- `use_origin`: 是否使用未经 to_me 等处理过的消息 +- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀 +- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符 + +`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法: + +- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看[条件控制](./matcher.mdx#条件控制)) +- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换 +- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本 +- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path` +- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` +- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt + +实例: + +```python +from nonebot import require +require("nonebot_plugin_alconna") + +from arclet.alconna import Alconna, Option, Args +from nonebot_plugin_alconna import on_alconna, Match, UniMessage + + +login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/ + +# /login -r 触发 +@login.assign("recall") +async def login_exit(): + await login.finish("已退出") + +# /login xxx 触发 +@login.assign("password") +async def login_handle(pw: Match[str]): + if pw.available: + login.set_path_arg("password", pw.result) + +# /login 触发 +@login.got_path("password", prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请输入密码")) +async def login_got(password: str): + assert password + await login.send("登录成功") +``` + +## 依赖注入 + +本插件提供了一系列依赖注入函数,便于在响应函数中获取解析结果: + +- `AlconnaResult`: `CommandResult` 类型的依赖注入函数 +- `AlconnaMatches`: `Arparma` 类型的依赖注入函数 +- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数 +- `AlconnaMatch`: `Match` 类型的依赖注入函数 +- `AlconnaQuery`: `Query` 类型的依赖注入函数 + +同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832),添加了两类注解: + +- `AlcMatches`:同 `AlconnaMatches` +- `AlcResult`:同 `AlconnaResult` + +可以看到,本插件提供了几类额外的模型: + +- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段 +- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值 +- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果 + +**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效: + +```python +async def handle( + result: CommandResult, + arp: Arparma, + dup: Duplication, + source: Alconna, + abc: str, # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler + foo: Match[str], + bar: Query[int] = Query("ttt.bar", 0) # Query 仍然需要一个默认值来传递 path 参数 +): + ... +``` + +:::note + +如果你更喜欢 Depends 式的依赖注入,`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数,他们包括: + +- `AlconnaResult`: `CommandResult` 类型的依赖注入函数 +- `AlconnaMatches`: `Arparma` 类型的依赖注入函数 +- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数 +- `AlconnaMatch`: `Match` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数 +- `AlconnaQuery`: `Query` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数 +- `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数 +- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数 + +::: + +实例: + +```python +from nonebot import require +require("nonebot_plugin_alconna") + +from nonebot_plugin_alconna import ( + on_alconna, + Match, + Query, + AlconnaMatch, + AlcResult +) +from arclet.alconna import Alconna, Args, Option, Arparma + + +test = on_alconna( + Alconna( + "test", + Option("foo", Args["bar", int]), + Option("baz", Args["qux", bool, False]) + ), + auto_send_output=True +) + +@test.handle() +async def handle_test1(result: AlcResult): + await test.send(f"matched: {result.matched}") + await test.send(f"maybe output: {result.output}") + +@test.handle() +async def handle_test2(result: Arparma): + await test.send(f"head result: {result.header_result}") + await test.send(f"args: {result.all_matched_args}") + +@test.handle() +async def handle_test3(bar: Match[int] = AlconnaMatch("bar")): + if bar.available: + await test.send(f"foo={bar.result}") + +@test.handle() +async def handle_test4(qux: Query[bool] = Query("baz.qux", False)): + if qux.available: + await test.send(f"baz.qux={qux.result}") +``` + +## 多平台适配 + +本插件提供了通用消息段标注, 通用消息段序列, 使插件使用者可以忽略平台之间字段的差异 + +响应器使用示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。 + +具体介绍和使用请查看 [通用信息组件](./uniseg.mdx#通用消息段) + +本插件为以下适配器提供了专门的适配器标注: + +| 协议名称 | 路径 | +| ------------------------------------------------------------------- | ------------------------------------ | +| [OneBot 协议](https://github.com/nonebot/adapter-onebot) | adapters.onebot11, adapters.onebot12 | +| [Telegram](https://github.com/nonebot/adapter-telegram) | adapters.telegram | +| [飞书](https://github.com/nonebot/adapter-feishu) | adapters.feishu | +| [GitHub](https://github.com/nonebot/adapter-github) | adapters.github | +| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq | +| [钉钉](https://github.com/nonebot/adapter-ding) | adapters.ding | +| [Dodo](https://github.com/nonebot/adapter-dodo) | adapters.dodo | +| [Console](https://github.com/nonebot/adapter-console) | adapters.console | +| [开黑啦](https://github.com/Tian-que/nonebot-adapter-kaiheila) | adapters.kook | +| [Mirai](https://github.com/ieew/nonebot_adapter_mirai2) | adapters.mirai | +| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | adapters.ntchat | +| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft | +| [BiliBili Live](https://github.com/wwweww/adapter-bilibili) | adapters.bilibili | +| [Walle-Q](https://github.com/onebot-walle/nonebot_adapter_walleq) | adapters.onebot12 | +| [Discord](https://github.com/nonebot/adapter-discord) | adapters.discord | +| [Red 协议](https://github.com/nonebot/adapter-red) | adapters.red | +| [Satori 协议](https://github.com/nonebot/adapter-satori) | adapters.satori | + +## 条件控制 + +本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应。 + +```python +... +from nonebot import require +require("nonebot_plugin_alconna") +... + +from arclet.alconna import Alconna, Subcommand, Option, Args +from nonebot_plugin_alconna import on_alconna, CommandResult + + +pip = Alconna( + "pip", + Subcommand( + "install", Args["pak", str], + Option("--upgrade"), + Option("--force-reinstall") + ), + Subcommand("list", Option("--out-dated")) +) + +pip_cmd = on_alconna(pip) + +# 仅在命令为 `pip install pip` 时响应 +@pip_cmd.assign("install.pak", "pip") +async def update(res: CommandResult): + ... + +# 仅在命令为 `pip list` 时响应 +@pip_cmd.assign("list") +async def list_(res: CommandResult): + ... + +# 在命令为 `pip install xxx` 时响应 +@pip_cmd.assign("install") +async def install(res: CommandResult): + ... +``` + +此外,使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher: + +```python +update_cmd = pip_cmd.dispatch("install.pak", "pip") + +@update_cmd.handle() +async def update(arp: CommandResult): + ... +``` + +另外,`AlconnaMatcher` 有类似于 `got` 的 `got_path`: + +```python +from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna + + +test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]])) + +@test_cmd.handle() +async def tt_h(target: Match[Union[str, At]]): + if target.available: + test_cmd.set_path_arg("target", target.result) + +@test_cmd.got_path("target", prompt="请输入目标") +async def tt(target: Union[str, At]): + await test_cmd.send(UniMessage(["ok\n", target])) +``` + +`got_path` 与 `assign`,`Match`,`Query` 等地方一样,都需要指明 `path` 参数 (即对应 Arg 验证的路径) + +`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型,例如示例中 `target` 对应的 Arg 里要求 str 或 At,则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。 + +:::tip + +`path` 支持 ~XXX 语法,其会把 ~ 替换为可能的父级路径: + +```python + pip = Alconna( + "pip", + Subcommand( + "install", + Args["pak", str], + Option("--upgrade|-U"), + Option("--force-reinstall"), + ), + Subcommand("list", Option("--out-dated")), + ) + + pipcmd = on_alconna(pip) + pip_install_cmd = pipcmd.dispatch("install") + + + @pip_install_cmd.assign("~upgrade") + async def pip1_u(pak: Query[str] = Query("~pak")): + await pip_install_cmd.finish(f"pip upgrading {pak.result}...") +``` + +::: + +## 响应器创建装饰 + +本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器: + +```python +from nonebot_plugin_alconna import funcommand + + +@funcommand() +async def echo(msg: str): + return msg +``` + +其等同于: + +```python +from arclet.alconna import Alconna, Args +from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match + + +echo = on_alconna(Alconna("echo", Args["msg", str])) + +@echo.handle() +async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): + await echo.finish(msg.result) + +``` + +## 类Koishi构造器 + +本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** : + +```python +from nonebot_plugin_alconna import Command, Arparma + + +book = ( + Command("book", "测试") + .option("writer", "-w ") + .option("writer", "--anonymous", {"id": 0}) + .usage("book [-w | --anonymous]") + .shortcut("测试", {"args": ["--anonymous"]}) + .build() +) + +@book.handle() +async def _(arp: Arparma): + await book.send(str(arp.options)) +``` + +甚至,你可以设置 `action` 来设定响应行为: + +```python +book = ( + Command("book", "测试") + .option("writer", "-w ") + .option("writer", "--anonymous", {"id": 0}) + .usage("book [-w | --anonymous]") + .shortcut("测试", {"args": ["--anonymous"]}) + .action(lambda options: str(options)) # 会自动通过 bot.send 发送 + .build() +) +``` + +## 返回值中间件 + +在 `AlconnaMatch`,`AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数: + +```python +from nonebot_plugin_alconna import image_fetch + + +mask_cmd = on_alconna( + Alconna("search", Args["img?", Image]), +) + + +@mask_cmd.handle() +async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)): + result = await search_img(img.result) + await matcher.send(result.content) +``` + +其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。 + +## 匹配拓展 + +本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为 + +例如一个 `LLMExtension` 可以如下实现 (仅举例): + +```python +from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface + + +class LLMExtension(Extension): + @property + def priority(self) -> int: + return 10 + + @property + def id(self) -> str: + return "LLMExtension" + + def __init__(self, llm): + self.llm = llm + + def post_init(self, alc: Alconna) -> None: + self.llm.add_context(alc.command, alc.meta.description) + + async def receive_wrapper(self, bot, event, receive): + resp = await self.llm.input(str(receive)) + return receive.__class__(resp.content) + + def before_catch(self, name, annotation, default): + return name == "llm" + + def catch(self, interface: Interface): + if interface.name == "llm": + return self.llm + +matcher = on_alconna( + Alconna(...), + extensions=[LLMExtension(LLM)] +) +... +``` + +那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量。 + +目前 `Extension` 的功能有: + +- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应 +- `output_converter`: 输出信息的自定义转换方法 +- `message_provider`: 从传入事件中自定义提取消息的方法 +- `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理 +- `context_provider`: 对命令上下文的额外处理 +- `permission_check`: 命令对消息解析并确认头部匹配(即确认选择响应)时对发送者的权限判断 +- `parse_wrapper`: 对命令解析结果的额外处理 +- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理 +- `before_catch`: 自定义依赖注入的绑定确认函数 +- `catch`: 自定义依赖注入处理函数 +- `post_init`: 响应器创建后对命令对象的额外处理 + +例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析: + +```python +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_alconna.builtins.extensions.discord import DiscordSlashExtension + + +alc = Alconna( + ["/"], + "permission", + Subcommand("add", Args["plugin", str]["priority?", int]), + Option("remove", Args["plugin", str]["time?", int]), + meta=CommandMeta(description="权限管理"), +) + +matcher = on_alconna(alc, extensions=[DiscordSlashExtension()]) + +@matcher.assign("add") +async def add(plugin: Match[str], priority: Match[int]): + await matcher.finish(f"added {plugin.result} with {priority.result if priority.available else 0}") + +@matcher.assign("remove") +async def remove(plugin: Match[str], time: Match[int]): + await matcher.finish(f"removed {plugin.result} with {time.result if time.available else -1}") +``` + +目前插件提供了 4 个内置的 `Extension`,它们在 `nonebot_plugin_alconna.builtins.extensions` 下: + +- `ReplyRecordExtension`: 将消息事件中的回复暂存在 extension 中,使得解析用的消息不带回复信息,同时可以在后续的处理中获取回复信息。 +- `DiscordSlashExtension`: 将 Alconna 的命令自动转换为 Discord 的 Slash Command,并将 Slash Command 的交互事件转换为消息交给 Alconna 处理。 +- `MarkdownOutputExtension`: 将 Alconna 的自动输出转换为 Markdown 格式 +- `TelegramSlashExtension`: 将 Alconna 的命令注册在 Telegram 上以获得提示。 + +:::tip + +全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后,这部分响应器会被追加拓展) + +::: + +## 补全会话 + +补全会话基于 [`半自动补全`](./command.md#半自动补全),用于指令参数缺失或参数错误时给予交互式提示,类似于 `got-reject`: + +```python +from nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna + +alc = Alconna( + "添加教师", + Args["name", str, Field(completion=lambda: "请输入姓名")], + Args["phone", int, Field(completion=lambda: "请输入手机号")], + Args["at", [str, At], Field(completion=lambda: "请输入教师号")], +) + +cmd = on_alconna(alc, comp_config={"lite": True}, skip_for_unmatch=False) + +@cmd.handle() +async def handle(result: Arparma): + cmd.finish("添加成功") +``` + +此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示: + + + +补全会话配置如下: + +```python +class CompConfig(TypedDict): + tab: NotRequired[str] + """用于切换提示的指令的名称""" + enter: NotRequired[str] + """用于输入提示的指令的名称""" + exit: NotRequired[str] + """用于退出会话的指令的名称""" + timeout: NotRequired[int] + """超时时间""" + hide_tabs: NotRequired[bool] + """是否隐藏所有提示""" + hides: NotRequired[Set[Literal["tab", "enter", "exit"]]] + """隐藏的指令""" + disables: NotRequired[Set[Literal["tab", "enter", "exit"]]] + """禁用的指令""" + lite: NotRequired[bool] + """是否使用简洁版本的补全会话(相当于同时配置 disables、hides、hide_tabs)""" +``` + +## 内置插件 + +类似于 Nonebot 本身提供的内置插件,`nonebot_plugin_alconna` 提供了两个内置插件:`echo` 和 `help`。 + +你可以用本插件的 `load_builtin_plugin(s)` 来加载它们: + +```python +from nonebot_plugin_alconna import load_builtin_plugins + +load_builtin_plugins("echo", "help") +``` + +其中 `help` 仅能列出所有 Alconna 指令。 + + diff --git a/website/versioned_docs/version-2.4.0/best-practice/alconna/uniseg.mdx b/website/versioned_docs/version-2.4.0/best-practice/alconna/uniseg.mdx new file mode 100644 index 00000000..8b7d5b6e --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/alconna/uniseg.mdx @@ -0,0 +1,590 @@ +--- +sidebar_position: 5 +description: 通用消息组件 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# 通用消息组件 + +`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件,其提供了一套通用的消息组件,用于在 `nonebot-plugin-alconna` 下构建通用消息。 + +## 通用消息段 + +适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于: +通用消息段会匹配多个适配器中相似类型的消息段,并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用。 + +`nonebot-plugin-alconna.uniseg` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用: + +```python +class Segment: + """基类标注""" + children: List["Segment"] + +class Text(Segment): + """Text对象, 表示一类文本元素""" + text: str + styles: Dict[Tuple[int, int], List[str]] + +class At(Segment): + """At对象, 表示一类提醒某用户的元素""" + flag: Literal["user", "role", "channel"] + target: str + display: Optional[str] + +class AtAll(Segment): + """AtAll对象, 表示一类提醒所有人的元素""" + here: bool + +class Emoji(Segment): + """Emoji对象, 表示一类表情元素""" + id: str + name: Optional[str] + +class Media(Segment): + url: Optional[str] + id: Optional[str] + path: Optional[Union[str, Path]] + raw: Optional[Union[bytes, BytesIO]] + mimetype: Optional[str] + name: str + + to_url: ClassVar[Optional[MediaToUrl]] + +class Image(Media): + """Image对象, 表示一类图片元素""" + +class Audio(Media): + """Audio对象, 表示一类音频元素""" + duration: Optional[int] + +class Voice(Media): + """Voice对象, 表示一类语音元素""" + duration: Optional[int] + +class Video(Media): + """Video对象, 表示一类视频元素""" + +class File(Segment): + """File对象, 表示一类文件元素""" + id: str + name: Optional[str] + +class Reply(Segment): + """Reply对象,表示一类回复消息""" + id: str + """此处不一定是消息ID,可能是其他ID,如消息序号等""" + msg: Optional[Union[Message, str]] + origin: Optional[Any] + +class Reference(Segment): + """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" + id: Optional[str] + """此处不一定是消息ID,可能是其他ID,如消息序号等""" + children: List[Union[RefNode, CustomNode]] + +class Hyper(Segment): + """Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等""" + format: Literal["xml", "json"] + raw: Optional[str] + content: Optional[Union[dict, list]] + +class Other(Segment): + """其他 Segment""" + origin: MessageSegment + +``` + +:::tip + +或许你注意到了 `Segment` 上有一个 `children` 属性。 + +这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下,一类元素可以用其子元素来代表一类兼容性消息 +(例如,qq 的商场表情在某些平台上可以用图片代替)。 + +为此,本插件提供了两种方式来表达 "获取子元素" 的方法: + +```python +from nonebot_plugin_alconna.builtins.uniseg.chronocat import MarketFace +from nonebot_plugin_alconna import Args, Image, Alconna, select, select_first + +# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image +alc1 = Alconna("make_meme", Args["img", [Image, Image.from_(MarketFace)]]) + +# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果 +alc2 = Alconna("make_meme", Args["img", select(Image, index=0)]) # 也可以使用 select_first(Image) +``` + +::: + +## 通用消息序列 + +`nonebot-plugin-alconna.uniseg` 同时提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。 + +你可以用如下方式获取 `UniMessage`: + + + + +通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。 + +```python +from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply + + +matcher = on_xxx(...) + +@matcher.handle() +async def _(msg: UniMsg): + reply = msg[Reply, 0] + print(reply.origin) + if msg.has(At): + ats = msg.get(At) + print(ats) + ... +``` + + + + +注意,`generate` 方法在响应器以外的地方如果不传入 `event` 与 `bot` 则无法处理 reply。 + +```python +from nonebot import Message, EventMessage +from nonebot_plugin_alconna.uniseg import UniMessage + + +matcher = on_xxx(...) + +@matcher.handle() +async def _(message: Message = EventMessage()): + msg = await UniMessage.generate(message=message) + msg1 = UniMessage.generate_without_reply(message=message) +``` + + + + +不仅如此,你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。 + +`UniMessage.export` 会通过传入的 `bot: Bot` 参数,或上下文中的 `Bot` 对象读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列: + +```python +from nonebot import Bot, on_command +from nonebot_plugin_alconna.uniseg import Image, UniMessage + + +test = on_command("test") + +@test.handle() +async def handle_test(): + await test.send(await UniMessage(Image(path="path/to/img")).export()) +``` + +除此之外 `UniMessage.send` 方法基于 `UniMessage.export` 并调用各适配器下的发送消息方法,返回一个 `Receipt` 对象,用于修改/撤回消息: + +```python +from nonebot import Bot, on_command +from nonebot_plugin_alconna.uniseg import UniMessage + + +test = on_command("test") + +@test.handle() +async def handle(): + receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True) + await receipt.recall(delay=1) +``` + +而在 `AlconnaMatcher` 下,`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`,不需要手动调用 export 方法: + +```python +from arclet.alconna import Alconna, Args +from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna +from nonebot_plugin_alconna.uniseg import At, UniMessage + + +test_cmd = on_alconna(Alconna("test", Args["target?", At])) + +@test_cmd.handle() +async def tt_h(matcher: AlconnaMatcher, target: Match[At]): + if target.available: + matcher.set_path_arg("target", target.result) + +@test_cmd.got_path("target", prompt="请输入目标") +async def tt(target: At): + await test_cmd.send(UniMessage([target, "\ndone."])) +``` + +:::caution + +在响应器以外的地方,除非启用了 `alconna_apply_fetch_targets` 配置项,否则 `bot` 参数必须手动传入。 + +::: + +### 构造 + +如同 `Message`, `UniMessage` 可以传入单个字符串/消息段,或可迭代的字符串/消息段: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, At + + +msg = UniMessage("Hello") +msg1 = UniMessage(At("user", "124")) +msg2 = UniMessage(["Hello", At("user", "124")]) +``` + +`UniMessage` 上同时存在便捷方法,令其可以链式地添加消息段: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, At, Image + + +msg = UniMessage.text("Hello").at("124").image(path="/path/to/img") +assert msg == UniMessage( + ["Hello", At("user", "124"), Image(path="/path/to/img")] +) +``` + +### 拼接消息 + +`str`、`UniMessage`、`Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象: + +```python +# 消息序列与消息段相加 +UniMessage("text") + Text("text") +# 消息序列与字符串相加 +UniMessage([Text("text")]) + "text" +# 消息序列与消息序列相加 +UniMessage("text") + UniMessage([Text("text")]) +# 字符串与消息序列相加 +"text" + UniMessage([Text("text")]) +# 消息段与消息段相加 +Text("text") + Text("text") +# 消息段与字符串相加 +Text("text") + "text" +# 消息段与消息序列相加 +Text("text") + UniMessage([Text("text")]) +# 字符串与消息段相加 +"text" + Text("text") +``` + +如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加: + +```python +msg = UniMessage([Text("text")]) +# 自加 +msg += "text" +msg += Text("text") +msg += UniMessage([Text("text")]) +# 附加 +msg.append(Text("text")) +# 扩展 +msg.extend([Text("text")]) +``` + +### 使用消息模板 + +`UniMessage.template` 同样类似于 `Message.template`,可以用于格式化消息,大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。 + +这里额外说明 `UniMessage.template` 的拓展控制符 + +相比 `Message`,UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行 + +以 At(...) 为例: + +```python title=使用通用消息段的拓展控制符 +>>> from nonebot_plugin_alconna.uniseg import UniMessage +>>> UniMessage.template("{:At(user, target)}").format(target="123") +UniMessage(At("user", "123")) +>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123") +UniMessage(At("user", "123")) +>>> UniMessage.template("{:At(type=user, target=123)}").format() +UniMessage(At("user", "123")) +``` + +而在 `AlconnaMatcher` 中,`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能: + +```python title=在AlconnaMatcher中使用通用消息段的拓展控制符 +from arclet.alconna import Alconna, Args +from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna + + +test_cmd = on_alconna(Alconna("test", Args["target?", At])) + +@test_cmd.handle() +async def tt_h(matcher: AlconnaMatcher, target: Match[At]): + if target.available: + matcher.set_path_arg("target", target.result) + +@test_cmd.got_path( + "target", + prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标") +) +async def tt(): + await test_cmd.send( + UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}") + ) +``` + +另外也有 `$message_id` 与 `$target` 两个特殊值。 + +### 检查消息段 + +我们可以通过 `in` 运算符或消息序列的 `has` 方法来: + +```python +# 是否存在消息段 +At("user", "1234") in message +# 是否存在指定类型的消息段 +At in message +``` + +我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段: + +```python +# 是否都为 "test" +message.only("test") +# 是否仅包含指定类型的消息段 +message.only(Text) +``` + +### 获取消息纯文本 + +类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, At + + +# 提取消息纯文本字符串 +assert UniMessage( + [At("user", "1234"), "text"] +).extract_plain_text() == "text" +``` + +### 遍历 + +通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段: + +```python +for segment in message: # type: Segment + ... +``` + +### 过滤、索引与切片 + +消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply + + +message = UniMessage( + [ + Reply(...), + "text1", + At("user", "1234"), + "text2" + ] +) +# 索引 +message[0] == Reply(...) +# 切片 +message[0:2] == UniMessage([Reply(...), Text("text1")]) +# 类型过滤 +message[At] == Message([At("user", "1234")]) +# 类型索引 +message[At, 0] == At("user", "1234") +# 类型切片 +message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")]) +``` + +我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤: + +```python +message.include(Text, At) +message.exclude(Reply) +``` + +同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段: + +```python +# 指定类型首个消息段索引 +message.index(Text) == 1 +# 指定类型消息段数量 +message.count(Text) == 2 +``` + +此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段: + +```python +# 获取指定类型指定个数的消息段 +message.get(Text, 1) == UniMessage([Text("test1")]) +``` + +## 消息发送 + +前面提到,通用消息可用 `UniMessage.send` 发送自身: + +```python +async def send( + self, + target: Union[Event, Target, None] = None, + bot: Optional[Bot] = None, + fallback: bool = True, + at_sender: Union[str, bool] = False, + reply_to: Union[str, bool] = False, +) -> Receipt: +``` + +实际上,`UniMessage` 同时提供了获取消息事件 id 与消息发送对象的方法: + + + + +通过提供的 `MessageTarget`, `MessageId` 或 `MsgTarget`, `MsgId` 依赖注入器来获取消息事件 id 与消息发送对象。 + +```python +from nonebot_plugin_alconna.uniseg import MessageId, MsgTarget + + +matcher = on_xxx(...) + +@matcher.handle() +asycn def _(target: MsgTarget, msg_id: MessageId): + ... +``` + + + + +```python +from nonebot import Event, Bot +from nonebot_plugin_alconna.uniseg import UniMessage, Target + + +matcher = on_xxx(...) + +@matcher.handle() +asycn def _(bot: Bot, event: Event): + target: Target = UniMessage.get_target(event, bot) + msg_id: str = UniMessage.get_message_id(event, bot) + +``` + + + + +`send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。 + +### 消息发送对象 + +消息发送对象是用来描述响应消息时的发送对象或者主动发送消息时的目标对象的对象,它包含了以下属性: + +```python +class Target: + id: str + """目标id;若为群聊则为group_id或者channel_id,若为私聊则为user_id""" + parent_id: str + """父级id;若为频道则为guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id)""" + channel: bool + """是否为频道,仅当目标平台符合频道概念时""" + private: bool + """是否为私聊""" + source: str + """可能的事件id""" + self_id: Union[str, None] + """机器人id,若为 None 则 Bot 对象会随机选择""" + selector: Union[Callable[[Bot], Awaitable[bool]], None] + """选择器,用于在多个 Bot 对象中选择特定 Bot""" + extra: Dict[str, Any] + """额外信息,用于适配器扩展""" +``` + +其构造时需要如下参数: + +- `id` 为目标id;若为群聊则为 group_id 或者 channel_id,若为私聊则为user_id +- `parent_id` 为父级id;若为频道则为 guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id) +- `channel` 为是否为频道,仅当目标平台符合频道概念时 +- `private` 为是否为私聊 +- `source` 为可能的事件id +- `self_id` 为机器人id,若为 None 则 Bot 对象会随机选择 +- `selector` 为选择器,用于在多个 Bot 对象中选择特定 Bot +- `scope` 为适配器范围,用于传入内置的特定选择器 +- `adapter` 为适配器名称,若为 None 则需要明确指定 Bot 对象 +- `platform` 为平台名称,仅当目标适配器存在多个平台时使用 +- `extra` 为额外信息,用于适配器扩展 + +通过 `Target` 对象,我们可以在 `UniMessage.send` 中指定发送对象: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, MsgTarget, Target, SupportScope + + +matcher = on_xxx(...) + +@matcher.handle() +async def _(target: MsgTarget): + await UniMessage("Hello!").send(target=target) + target1 = Target("xxxx", scope=SupportScope.qq_client) + await UniMessage("Hello!").send(target=target1) +``` + +### 主动发送消息 + +`UniMessage.send` 也可以用于主动发送消息: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, Target, SupportScope +from nonebot import get_driver + + +driver = get_driver() + +@driver.on_startup +async def on_startup(): + target = Target("xxxx", scope=SupportScope.qq_client) + await UniMessage("Hello!").send(target=target) +``` + +## 自定义消息段 + +`uniseg` 提供了部分方法来允许用户自定义 Segment 的序列化和反序列化: + +```python +from dataclasses import dataclass + +from nonebot.adapters import Bot +from nonebot.adapters import MessageSegment as BaseMessageSegment +from nonebot.adapters.satori import Custom, Message, MessageSegment + +from nonebot_plugin_alconna.uniseg.builder import MessageBuilder +from nonebot_plugin_alconna.uniseg.exporter import MessageExporter +from nonebot_plugin_alconna.uniseg import Segment, custom_handler, custom_register + + +@dataclass +class MarketFace(Segment): + tabId: str + faceId: str + key: str + + +@custom_register(MarketFace, "chronocat:marketface") +def mfbuild(builder: MessageBuilder, seg: BaseMessageSegment): + if not isinstance(seg, Custom): + raise ValueError("MarketFace can only be built from Satori Message") + return MarketFace(**seg.data)(*builder.generate(seg.children)) + + +@custom_handler(MarketFace) +async def mfexport(exporter: MessageExporter, seg: MarketFace, bot: Bot, fallback: bool): + if exporter.get_message_type() is Message: + return MessageSegment("chronocat:marketface", seg.data)(await exporter.export(seg.children, bot, fallback)) + +``` + +具体而言,你可以使用 `custom_register` 来增加一个从 MessageSegment 到 Segment 的处理方法;使用 `custom_handler` 来增加一个从 Segment 到 MessageSegment 的处理方法。 diff --git a/website/versioned_docs/version-2.4.0/best-practice/data-storing.md b/website/versioned_docs/version-2.4.0/best-practice/data-storing.md new file mode 100644 index 00000000..d9f3a8c5 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/data-storing.md @@ -0,0 +1,113 @@ +--- +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_plugin_cache_dir() +# 获取插件缓存文件 +cache_file = store.get_plugin_cache_file("file_name") +# 获取插件数据目录 +data_dir = store.get_plugin_data_dir() +# 获取插件数据文件 +data_file = store.get_plugin_data_file("file_name") +# 获取插件配置目录 +config_dir = store.get_plugin_config_dir() +# 获取插件配置文件 +config_file = store.get_plugin_config_file("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_plugin_data_file("file_name") +# 写入文件内容 +data_file.write_text("Hello World!") +# 读取文件内容 +data = data_file.read_text() +``` + +:::note 提示 + +对于嵌套插件,子插件的存储目录将位于父插件存储目录下。 + +::: + +## 配置项 + +### localstore_cache_dir + +自定义缓存目录 + +默认值: + +- macOS: `~/Library/Caches/` +- Unix: `~/.cache/` (XDG default) +- Windows: `C:\Users\\AppData\Local\\Cache` + +```dotenv +LOCALSTORE_CACHE_DIR=/tmp/cache +``` + +### localstore_data_dir + +自定义数据目录 + +默认值: + +- macOS: `~/Library/Application Support/` +- Unix: `~/.local/share/` or in $XDG_DATA_HOME, if defined +- Win XP (not roaming): `C:\Documents and Settings\\Application Data\` +- Win 7 (not roaming): `C:\Users\\AppData\Local\` + +```dotenv +LOCALSTORE_DATA_DIR=/tmp/data +``` + +### localstore_config_dir + +自定义配置目录 + +默认值: + +- macOS: same as user_data_dir +- Unix: `~/.config/` +- Win XP (roaming): `C:\Documents and Settings\\Local Settings\Application Data\` +- Win 7 (roaming): `C:\Users\\AppData\Roaming\` + +```dotenv +LOCALSTORE_CONFIG_DIR=/tmp/config +``` diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/README.mdx b/website/versioned_docs/version-2.4.0/best-practice/database/README.mdx new file mode 100644 index 00000000..0f8a1572 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/README.mdx @@ -0,0 +1,145 @@ +import TabItem from "@theme/TabItem"; +import Tabs from "@theme/Tabs"; + +# 数据库 + +[`nonebot-plugin-orm`](https://github.com/nonebot/plugin-orm) 是 NoneBot 的数据库支持插件。 +本插件基于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/),提供了许多与 NoneBot 紧密集成的功能: + +- 多 Engine / Connection 支持 +- Session 管理 +- 关系模型管理、依赖注入支持 +- 数据库迁移 + +## 安装 + + + + +```shell +nb plugin install nonebot-plugin-orm +``` + + + + +```shell +pip install nonebot-plugin-orm +``` + + + + + +```shell +pdm add nonebot-plugin-orm +``` + + + + +## 数据库驱动和后端 + +本插件只提供了 ORM 功能,没有数据库后端,也没有直接连接数据库后端的能力。 +所以你需要另行安装数据库驱动和数据库后端,并且配置数据库连接信息。 + +### SQLite + +[SQLite](https://www.sqlite.org/) 是一个轻量级的嵌入式数据库,它的数据以单文件的形式存储在本地,不需要单独的数据库后端。 +SQLite 非常适合用于开发环境和小型应用,但是不适合用于大型应用的生产环境。 + +虽然不需要另行安装数据库后端,但你仍然需要安装数据库驱动: + + + + +```shell +pip install "nonebot-plugin-orm[sqlite]" +``` + + + + + +```shell +pdm add "nonebot-plugin-orm[sqlite]" +``` + + + + +默认情况下,数据库文件为 `/nonebot-plugin-orm/db.sqlite3`(数据目录由 [nonebot-plugin-localstore](../data-storing) 提供)。 +或者,你可以通过配置 `SQLALCHEMY_DATABASE_URL` 来指定数据库文件路径: + +```shell +SQLALCHEMY_DATABASE_URL=sqlite+aiosqlite:///file_path +``` + +### PostgreSQL + +[PostgreSQL](https://www.postgresql.org/) 是世界上最先进的开源关系数据库之一,对各种高级且广泛应用的功能有最好的支持,是中小型应用的首选数据库。 + + + + +```shell +pip install nonebot-plugin-orm[postgresql] +``` + + + + + +```shell +pdm add nonebot-plugin-orm[postgresql] +``` + + + + +```shell +SQLALCHEMY_DATABASE_URL=postgresql+psycopg://user:password@host:port/dbname[?key=value&key=value...] +``` + +### MySQL / MariaDB + +[MySQL](https://www.mysql.com/) 和 [MariaDB](https://mariadb.com/) 是经典的开源关系数据库,适合用于中小型应用。 + + + + +```shell +pip install nonebot-plugin-orm[mysql] +``` + + + + + +```shell +pdm add nonebot-plugin-orm[mysql] +``` + + + + +```shell +SQLALCHEMY_DATABASE_URL=mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...] +``` + +## 使用 + +本插件提供了数据库迁移功能(此功能依赖于 [nb-cli 脚手架](../../quick-start#安装脚手架))。 +在安装了新的插件或机器人之后,你需要执行一次数据库迁移操作,将数据库同步至与机器人一致的状态: + +```shell +nb orm upgrade +``` + +运行完毕后,可以检查一下: + +```shell +nb orm check +``` + +如果输出是 `没有检测到新的升级操作`,那么恭喜你,数据库已经迁移完成了,你可以启动机器人了。 diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/_category_.json b/website/versioned_docs/version-2.4.0/best-practice/database/_category_.json new file mode 100644 index 00000000..4c6eeb3a --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "数据库", + "position": 7 +} diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/developer/README.md b/website/versioned_docs/version-2.4.0/best-practice/database/developer/README.md new file mode 100644 index 00000000..8b6bf658 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/developer/README.md @@ -0,0 +1,378 @@ +# 开发者指南 + +开发者指南内容较多,故分为了一个示例以及数个专题。 +阅读(并且最好跟随实践)示例后,你将会对使用 `nonebot-plugin-orm` 开发插件有一个基本的认识。 +如果想要更深入地学习关于 [SQLAlchemy](https://www.sqlalchemy.org/) 和 [Alembic](https://alembic.sqlalchemy.org/) 的知识,或者在使用过程中遇到了问题,可以查阅专题以及其官方文档。 + +## 示例 + +### 模型定义 + +首先,我们需要设计存储的数据的结构。 +例如天气插件,需要存储**什么地方 (`location`)** 的**天气是什么 (`weather`)**。 +其中,一个地方只会有一种天气,而不同地方可能有相同的天气。 +所以,我们可以设计出如下的模型: + +```python title=weather/__init__.py showLineNumbers +from nonebot_plugin_orm import Model +from sqlalchemy.orm import Mapped, mapped_column + + +class Weather(Model): + location: Mapped[str] = mapped_column(primary_key=True) + weather: Mapped[str] +``` + +其中,`primary_key=True` 意味着此列 (`location`) 是主键,即内容是唯一的且非空的。 +每一个模型必须有至少一个主键。 + +我们可以用以下代码检查模型生成的数据库模式是否正确: + +```python +from sqlalchemy.schema import CreateTable + +print(CreateTable(Weather.__table__)) +``` + +```sql +CREATE TABLE weather_weather ( + location VARCHAR NOT NULL, + weather VARCHAR NOT NULL, + CONSTRAINT pk_weather_weather PRIMARY KEY (location) +) +``` + +可以注意到表名是 `weather_weather` 而不是 `Weather` 或者 `weather`。 +这是因为 `nonebot-plugin-orm` 会自动为模型生成一个表名,规则是:`<插件模块名>_<类名小写>`。 + +你也可以通过指定 `__tablename__` 属性来自定义表名: + +```python {2} +class Weather(Model): + __tablename__ = "weather" + ... +``` + +```sql {1} +CREATE TABLE weather ( + ... +) +``` + +但是,并不推荐你这么做,因为这可能会导致不同插件间的表名重复,引发冲突。 +特别是当你会发布插件时,你并不知道其他插件会不会使用相同的表名。 + +### 首次迁移 + +我们成功定义了模型,现在启动机器人试试吧: + +```shell +$ nb run +01-02 15:04:05 [SUCCESS] nonebot | NoneBot is initializing... +01-02 15:04:05 [ERROR] nonebot_plugin_orm | 启动检查失败 +01-02 15:04:05 [ERROR] nonebot | Application startup failed. Exiting. +Traceback (most recent call last): + ... +click.exceptions.UsageError: 检测到新的升级操作: +[('add_table', + Table('weather', MetaData(), Column('location', String(), table=, primary_key=True, nullable=False), Column('weather', String(), table=, nullable=False), schema=None))] +``` + +咦,发生了什么? +`nonebot-plugin-orm` 试图阻止我们启动机器人。 +原来是我们定义了模型,但是数据库中并没有对应的表,这会导致插件不能正常运行。 +所以,我们需要迁移数据库。 + +首先,我们需要创建一个迁移脚本: + +```shell +nb orm revision -m "first revision" --branch-label weather +``` + +其中,`-m` 参数是迁移脚本的描述,`--branch-label` 参数是迁移脚本的分支,一般为插件模块名。 +执行命令过后,出现了一个 `weather/migrations` 目录,其中有一个 `xxxxxxxxxxxx_first_revision.py` 文件: + +```shell {4,5} +weather +├── __init__.py +├── config.py +└── migrations + └── xxxxxxxxxxxx_first_revision.py +``` + +这就是我们创建的迁移脚本,它记录了数据库模式的变化。 +我们可以查看一下它的内容: + +```python title=weather/migrations/xxxxxxxxxxxx_first_revision.py {25-33,39-41} showLineNumbers +"""first revision + +迁移 ID: xxxxxxxxxxxx +父迁移: +创建时间: 2006-01-02 15:04:05.999999 + +""" + +from __future__ import annotations + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +revision: str = "xxxxxxxxxxxx" +down_revision: str | Sequence[str] | None = None +branch_labels: str | Sequence[str] | None = ("weather",) +depends_on: str | Sequence[str] | None = None + + +def upgrade(name: str = "") -> None: + if name: + return + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "weather_weather", + sa.Column("location", sa.String(), nullable=False), + sa.Column("weather", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), + info={"bind_key": "weather"}, + ) + # ### end Alembic commands ### + + +def downgrade(name: str = "") -> None: + if name: + return + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("weather_weather") + # ### end Alembic commands ### +``` + +可以注意到脚本的主体部分(其余是模版代码,请勿修改)是: + +```python +# ### commands auto generated by Alembic - please adjust! ### +op.create_table( # CREATE TABLE + "weather_weather", # weather_weather + sa.Column("location", sa.String(), nullable=False), # location VARCHAR NOT NULL, + sa.Column("weather", sa.String(), nullable=False), # weather VARCHAR NOT NULL, + sa.PrimaryKeyConstraint("location", name=op.f("pk_weather_weather")), # CONSTRAINT pk_weather_weather PRIMARY KEY (location) + info={"bind_key": "weather"}, +) +# ### end Alembic commands ### +``` + +```python +# ### commands auto generated by Alembic - please adjust! ### +op.drop_table("weather_weather") # DROP TABLE weather_weather; +# ### end Alembic commands ### +``` + +虽然我们不是很懂这些代码的意思,但是可以注意到它们几乎与 SQL 语句 (DDL) 一一对应。 +显然,它们是用来创建和删除表的。 + +我们还可以注意到,`upgrade()` 和 `downgrade()` 函数中的代码是**互逆**的。 +也就是说,执行一次 `upgrade()` 函数,再执行一次 `downgrade()` 函数后,数据库的模式就会回到原来的状态。 + +这就是迁移脚本的作用:记录数据库模式的变化,以便我们在不同的环境中(例如开发环境和生产环境)**可复现地**、**可逆地**同步数据库模式,正如 git 对我们的代码做的事情那样。 + +对了,不要忘记还有一段注释:`commands auto generated by Alembic - please adjust!`。 +它在提醒我们,这些代码是由 Alembic 自动生成的,我们应该检查它们,并且根据需要进行调整。 + +:::caution 注意 +迁移脚本冗长且繁琐,我们一般不会手写它们,而是由 Alembic 自动生成。 +一般情况下,Alembic 足够智能,可以正确地生成迁移脚本。 +但是,在复杂或有歧义的情况下,我们可能需要手动调整迁移脚本。 +所以,**永远要检查迁移脚本,并且在开发环境中测试!** + +**迁移脚本中任何一处错误都足以使数据付之东流!** +::: + +确定迁移脚本正确后,我们就可以执行迁移脚本,将数据库模式同步到数据库中: + +```shell +nb orm upgrade +``` + +现在,我们可以正常启动机器人了。 + +开发过程中,我们可能会频繁地修改模型,这意味着我们需要频繁地创建并执行迁移脚本,非常繁琐。 +实际上,此时我们不在乎数据安全,只需要数据库模式与模型定义一致即可。 +所以,我们可以关闭 `nonebot-plugin-orm` 的启动检查: + +```shell title=.env.dev +ALEMBIC_STARTUP_CHECK=false +``` + +现在,每次启动机器人时,数据库模式会自动与模型定义同步,无需手动迁移。 + +### 会话管理 + +我们已经成功定义了模型,并且迁移了数据库,现在可以开始使用数据库了……吗? +并不能,因为模型只是数据结构的定义,并不能通过它操作数据(如果你曾经使用过 [Tortoise ORM](https://tortoise.github.io/),可能会知道 `await Weather.get(location="上海")` 这样的面向对象编程。 +但是 SQLAlchemy 不同,选择了命令式编程)。 +我们需要使用**会话**操作数据: + +```python title=weather/__init__.py {10,13} showLineNumbers +from nonebot import on_command +from nonebot.adapters import Message +from nonebot.params import CommandArg +from nonebot_plugin_orm import async_scoped_session + +weather = on_command("天气") + + +@weather.handle() +async def _(session: async_scoped_session, args: Message = CommandArg()): + location = args.extract_plain_text() + + if wea := await session.get(Weather, location): + await weather.finish(f"今天{location}的天气是{wea.weather}") + + await weather.finish(f"未查询到{location}的天气") +``` + +我们通过 `session: async_scoped_session` 依赖注入获得了一个会话,然后使用 `await session.get(Weather, location)` 查询数据库。 +`async_scoped_session` 是一个有作用域限制的会话,作用域为当前事件、当前事件响应器。 +会话产生的模型实例(例如此处的 `wea := await session.get(Weather, location)`)作用域与会话相同。 + +:::caution 注意 +此处提到的“会话”指的是 ORM 会话,而非 [NoneBot 会话](../../../appendices/session-control),两者的生命周期也是不同的(NoneBot 会话的生命周期中可能包含多个事件,不同的事件也会有不同的事件响应器)。 +具体而言,就是不要将 ORM 会话和模型实例存储在 NoneBot 会话状态中: + +```python {12} +from nonebot.params import ArgPlainText +from nonebot.typing import T_State + + +@weather.got("location", prompt="请输入地名") +async def _(state: T_State, session: async_scoped_session, location: str = ArgPlainText()): + wea = await session.get(Weather, location) + + if not wea: + await weather.finish(f"未查询到{location}的天气") + + state["weather"] = wea # 不要这么做,除非你知道自己在做什么 +``` + +当然非要这么做也不是不可以: + +```python {6} +@weather.handle() +async def _(state: T_State, session: async_scoped_session): + # 通过 await session.merge(state["weather"]) 获得了此 ORM 会话中的相应模型实例, + # 而非直接使用会话状态中的模型实例, + # 因为先前的 ORM 会话已经关闭了。 + wea = await session.merge(state["weather"]) + await weather.finish(f"今天{state['location']}的天气是{wea.weather}") +``` + +::: + +当有数据更改时,我们需要提交事务,也要注意会话作用域问题: + +```python title=weather/__init__.py {12,20} showLineNumbers +from nonebot.params import Depends + + +async def get_weather( + session: async_scoped_session, args: Message = CommandArg() +) -> Weather: + location = args.extract_plain_text() + + if not (wea := await session.get(Weather, location)): + wea = Weather(location=location, weather="未知") + session.add(wea) + # await session.commit() # 不应该在其他地方提交事务 + + return wea + + +@weather.handle() +async def _(session: async_scoped_session, wea: Weather = Depends(get_weather)): + await weather.send(f"今天的天气是{wea.weather}") + await session.commit() # 而应该在事件响应器结束前提交事务 +``` + +当然我们也可以获得一个新的会话,不过此时就要手动管理会话了: + +```python title=weather/__init__.py {5-6} showLineNumbers +from nonebot_plugin_orm import get_session + + +async def get_weather(location: str) -> str: + session = get_session() + async with session.begin(): + wea = await session.get(Weather, location) + + if not wea: + wea = Weather(location=location, weather="未知") + session.add(wea) + + return wea.weather + + +@weather.handle() +async def _(args: Message = CommandArg()): + wea = await get_weather(args.extract_plain_text()) + await weather.send(f"今天的天气是{wea}") +``` + +### 依赖注入 + +在上面的示例中,我们都是通过会话获得数据的。 +不过,我们也可以通过依赖注入获得数据: + +```python title=weather/__init__.py {12-14} showLineNumbers +from sqlalchemy import select +from nonebot.params import Depends +from nonebot_plugin_orm import SQLDepends + + +def extract_arg_plain_text(args: Message = CommandArg()) -> str: + return args.extract_plain_text() + + +@weather.handle() +async def _( + wea: Weather = SQLDepends( + select(Weather).where(Weather.location == Depends(extract_arg_plain_text)) + ), +): + await weather.send(f"今天的天气是{wea.weather}") +``` + +其中,`SQLDepends` 是一个特殊的依赖注入,它会根据类型标注和 SQL 语句提供数据,SQL 语句中也可以有子依赖。 + +不同的类型标注也会获得不同形式的数据: + +```python title=weather/__init__.py {5} showLineNumbers +from collections.abc import Sequence + +@weather.handle() +async def _( + weas: Sequence[Weather] = SQLDepends( + select(Weather).where(Weather.weather == Depends(extract_arg_plain_text)) + ), +): + await weather.send(f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}") +``` + +支持的类型标注请参见 [依赖注入](dependency)。 + +我们也可以像 [类作为依赖](../../../advanced/dependency#类作为依赖) 那样,在类属性中声明子依赖: + +```python title=weather/__init__.py {5-6,10} showLineNumbers +from collections.abc import Sequence + +class Weather(Model): + location: Mapped[str] = mapped_column(primary_key=True) + weather: Mapped[str] = Depends(extract_arg_plain_text) + # weather: Annotated[Mapped[str], Depends(extract_arg_plain_text)] # Annotated 支持 + + +@weather.handle() +async def _(weas: Sequence[Weather]): + await weather.send( + f"今天的天气是{weas[0].weather}的城市有{','.join(wea.location for wea in weas)}" + ) +``` diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/developer/_category_.json b/website/versioned_docs/version-2.4.0/best-practice/database/developer/_category_.json new file mode 100644 index 00000000..999af8e6 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/developer/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "开发者指南", + "position": 3 +} diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/developer/dependency.md b/website/versioned_docs/version-2.4.0/best-practice/database/developer/dependency.md new file mode 100644 index 00000000..6496d6a1 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/developer/dependency.md @@ -0,0 +1,240 @@ +--- +sidebar_position: 3 +description: 依赖注入 +--- + +# 依赖注入 + +`nonebot-plugin-orm` 提供了强大且灵活的依赖注入,可以方便地帮助你获取数据库会话和查询数据。 + +## 数据库会话 + +### AsyncSession + +新数据库会话,常用于有独立的数据库操作逻辑的插件。 + +```python {13,26} +from nonebot import on_message +from nonebot.params import Depends +from nonebot_plugin_orm import AsyncSession, Model, async_scoped_session +from sqlalchemy.orm import Mapped, mapped_column + +message = on_message() + + +class Message(Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + + +async def get_message(session: AsyncSession) -> Message: + # 等价于 session = get_session() + async with session: + msg = Message() + + session.add(msg) + await session.commit() + await session.refresh(msg) + + return msg + + +@message.handle() +async def _(session: async_scoped_session, msg: Message = Depends(get_message)): + await session.rollback() # 无法回退 get_message() 中的更改 + await message.send(str(msg.id)) # msg 被存储,msg.id 递增 +``` + +### async_scoped_session + +数据库作用域会话,常用于事件响应器和有与响应逻辑相关的数据库操作逻辑的插件。 + +```python {13,26} +from nonebot import on_message +from nonebot.params import Depends +from nonebot_plugin_orm import Model, async_scoped_session +from sqlalchemy.orm import Mapped, mapped_column + +message = on_message() + + +class Message(Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + + +async def get_message(session: async_scoped_session) -> Message: + # 等价于 session = get_scoped_session() + msg = Message() + + session.add(msg) + await session.flush() + await session.refresh(msg) + + return msg + + +@message.handle() +async def _(session: async_scoped_session, msg: Message = Depends(get_message)): + await session.rollback() # 可以回退 get_message() 中的更改 + await message.send(str(msg.id)) # msg 没有被存储,msg.id 不变 +``` + +## 查询数据 + +### Model + +支持类作为依赖。 + +```python +from typing import Annotated + +from nonebot.params import Depends +from nonebot_plugin_orm import Model +from sqlalchemy.orm import Mapped, mapped_column + + +def get_id() -> int: ... + + +class Message(Model): + id: Annotated[Mapped[int], Depends(get_id)] = mapped_column( + primary_key=True, autoincrement=True + ) + + +async def _(msg: Message): + # 等价于 msg = ( + # await (await session.stream(select(Message).where(Message.id == get_id()))) + # .scalars() + # .one_or_none() + # ) + ... +``` + +### SQLDepends + +参数为一个 SQL 语句,决定依赖注入的内容,SQL 语句中可以使用子依赖。 + +```python {11-13} +from nonebot.params import Depends +from nonebot_plugin_orm import Model, SQLDepends +from sqlalchemy import select + + +def get_id() -> int: ... + + +async def _( + model: Model = SQLDepends(select(Model).where(Model.id == Depends(get_id))), +): ... +``` + +参数可以是任意 SQL 语句,但不建议使用 `select` 以外的语句,因为语句可能没有返回值(`returning` 除外),而且代码不清晰。 + +### 类型标注 + +类型标注决定依赖注入的数据结构,主要影响以下几个层面: + +- 迭代器(`session.execute()`)或异步迭代器(`session.stream()`) +- 标量(`session.execute().scalars()`)或元组(`session.execute()`) +- 一个(`session.execute().one_or_none()`,注意 `None` 时可能触发 [重载](../../../appendices/overload#重载))或全部(`session.execute()` / `session.execute().all()`) +- 连续(`session().execute()`)或分块(`session.execute().partitions()`) + +具体如下(可以使用父类型作为类型标注): + +- ```python + async def _(rows_partitions: AsyncIterator[Sequence[Tuple[Model, ...]]]): + # 等价于 rows_partitions = await (await session.stream(sql).partitions()) + + async for partition in rows_partitions: + for row in partition: + print(row[0], row[1], ...) + ``` + +- ```python + async def _(model_partitions: AsyncIterator[Sequence[Model]]): + # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions()) + + async for partition in model_partitions: + for model in partition: + print(model) + ``` + +- ```python + async def _(row_partitions: Iterator[Sequence[Tuple[Model, ...]]]): + # 等价于 row_partitions = await session.execute(sql).partitions() + + for partition in rows_partitions: + for row in partition: + print(row[0], row[1], ...) + ``` + +- ```python + async def _(model_partitions: Iterator[Sequence[Model]]): + # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions()) + + for partition in model_partitions: + for model in partition: + print(model) + ``` + +- ```python + async def _(rows: sa_async.AsyncResult[Tuple[Model, ...]]): + # 等价于 rows = await session.stream(sql) + + async for row in rows: + print(row[0], row[1], ...) + ``` + +- ```python + async def _(models: sa_async.AsyncScalarResult[Model]): + # 等价于 models = await session.stream(sql).scalars() + + async for model in models: + print(model) + ``` + +- ```python + async def _(rows: sa.Result[Tuple[Model, ...]]): + # 等价于 rows = await session.execute(sql) + + for row in rows: + print(row[0], row[1], ...) + ``` + +- ```python + async def _(models: sa.ScalarResult[Model]): + # 等价于 models = await session.execute(sql).scalars() + + for model in models: + print(model) + ``` + +- ```python + async def _(rows: Sequence[Tuple[Model, ...]]): + # 等价于 rows = await (await session.stream(sql).all()) + + for row in rows: + print(row[0], row[1], ...) + ``` + +- ```python + async def _(models: Sequence[Model]): + # 等价于 models = await (await session.stream(sql).scalars().all()) + + for model in models: + print(model) + ``` + +- ```python + async def _(row: Tuple[Model, ...]): + # 等价于 row = await (await session.stream(sql).one_or_none()) + + print(row[0], row[1], ...) + ``` + +- ```python + async def _(model: Model): + # 等价于 model = await (await session.stream(sql).scalars().one_or_none()) + + print(model) + ``` diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/developer/test.md b/website/versioned_docs/version-2.4.0/best-practice/database/developer/test.md new file mode 100644 index 00000000..654ee723 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/developer/test.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 2 +description: 测试 +--- + +# 测试 + +百思不如一试,测试是发现问题的最佳方式。 + +不同的用户会有不同的配置,为了提高项目的兼容性,我们需要在不同数据库后端上测试。 +手动进行大量的、重复的测试不可靠,也不现实,因此我们推荐使用 [GitHub Actions](https://github.com/features/actions) 进行自动化测试: + +```yaml title=.github/workflows/test.yml {12-42,52-53} showLineNumbers +name: Test + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + db: + - sqlite+aiosqlite:///db.sqlite3 + - postgresql+psycopg://postgres:postgres@localhost:5432/postgres + - mysql+aiomysql://mysql:mysql@localhost:3306/mymysql + + fail-fast: false + + env: + SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} + + services: + postgresql: + image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + + mysql: + image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} + env: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + MYSQL_DATABASE: mymysql + ports: + - 3306:3306 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run migrations + run: pipx run nb-cli orm upgrade + + - name: Run tests + run: pytest +``` + +如果项目还需要考虑跨平台和跨 Python 版本兼容,测试矩阵中还需要增加这两个维度。 +但是,我们没必要在所有平台和 Python 版本上运行所有数据库的测试,因为很显然,PostgreSQL 和 MySQL 这类独立的数据库后端不会受平台和 Python 影响,而且 Github Actions 的非 Linux 平台不支持运行独立服务: + +| | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | +| ----------- | ---------- | ----------- | ----------- | --------------------------- | +| **Linux** | SQLite | SQLite | SQLite | SQLite / PostgreSQL / MySQL | +| **Windows** | SQLite | SQLite | SQLite | SQLite | +| **macOS** | SQLite | SQLite | SQLite | SQLite | + +```yaml title=.github/workflows/test.yml {12-24} showLineNumbers +name: Test + +on: + push: + branches: + - main + +jobs: + test: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + db: ["sqlite+aiosqlite:///db.sqlite3"] + + include: + - os: ubuntu-latest + python-version: "3.12" + db: postgresql+psycopg://postgres:postgres@localhost:5432/postgres + - os: ubuntu-latest + python-version: "3.12" + db: mysql+aiomysql://mysql:mysql@localhost:3306/mymysql + + fail-fast: false + + env: + SQLALCHEMY_DATABASE_URL: ${{ matrix.db }} + + services: + postgresql: + image: ${{ startsWith(matrix.db, 'postgresql') && 'postgres' || '' }} + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + + mysql: + image: ${{ startsWith(matrix.db, 'mysql') && 'mysql' || '' }} + env: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_USER: mysql + MYSQL_PASSWORD: mysql + MYSQL_DATABASE: mymysql + ports: + - 3306:3306 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run migrations + run: pipx run nb-cli orm upgrade + + - name: Run tests + run: pytest +``` diff --git a/website/versioned_docs/version-2.4.0/best-practice/database/user.md b/website/versioned_docs/version-2.4.0/best-practice/database/user.md new file mode 100644 index 00000000..9bfc0ada --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/database/user.md @@ -0,0 +1,158 @@ +--- +sidebar_position: 2 +description: 用户指南 +--- + +# 用户指南 + +`nonebot-plugin-orm` 功能强大且复杂,使用上有一定难度。 +不过,对于用户而言,只需要掌握部分功能即可。 + +:::caution 注意 +请注意区分插件的项目名(如:`nonebot-plugin-wordcloud`)和模块名(如:`nonebot_plugin_wordcloud`)。`nonebot-plugin-orm` 中统一使用插件模块名。参见 [插件命名规范](../../developer/plugin-publishing#插件命名规范)。 +::: + +## 示例 + +### 创建新机器人 + +我们想要创建一个机器人,并安装 `nonebot-plugin-wordcloud` 插件,只需要执行以下命令: + +```shell +nb init # 初始化项目文件夹 + +pip install nonebot-plugin-orm[sqlite] # 安装 nonebot-plugin-orm,并附带 SQLite 支持 + +nb plugin install nonebot-plugin-wordcloud # 安装插件 + +# nb orm heads # 查看有什么插件使用到了数据库(可选) + +nb orm upgrade # 升级数据库 + +# nb orm check # 检查一下数据库模式是否与模型定义一致(可选) + +nb run # 启动机器人 +``` + +### 卸载插件 + +我们已经安装了 `nonebot-plugin-wordcloud` 插件,但是现在想要卸载它,并且**删除它的数据**,只需要执行以下命令: + +```shell +nb plugin uninstall nonebot-plugin-wordcloud # 卸载插件 + +# nb orm heads # 查看有什么插件使用到了数据库。(可选) + +nb orm downgrade nonebot_plugin_wordcloud@base # 降级数据库,删除数据 + +# nb orm check # 检查一下数据库模式是否与模型定义一致(可选) +``` + +## CLI + +接下来,让我们了解下示例中出现的 CLI 命令的含义: + +### heads + +显示所有的分支头。一般一个分支对应一个插件。 + +```shell +nb orm heads +``` + +输出格式为 `<迁移 ID> (<插件模块名>) (<头部类型>)`: + +``` +46327b837dd8 (nonebot_plugin_chatrecorder) (head) +9492159f98f7 (nonebot_plugin_user) (head) +71a72119935f (nonebot_plugin_session_orm) (effective head) +ade8cdca5470 (nonebot_plugin_wordcloud) (head) +``` + +### upgrade + +升级数据库。每次安装新的插件或更新插件版本后,都需要执行此命令。 + +```shell +nb orm upgrade <插件模块名>@<迁移 ID> +``` + +其中,`<插件模块名>@<迁移 ID>` 是可选参数。如果不指定,则会将所有分支升级到最新版本,这也是最常见的用法: + +```shell +nb orm upgrade +``` + +### downgrade + +降级数据库。当需要回滚插件版本或删除插件时,可以执行此命令。 + +```shell +nb orm downgrade <插件模块名>@<迁移 ID> +``` + +其中,`<迁移 ID>` 也可以是 `base`,即回滚到初始状态。常用于卸载插件后删除其数据: + +```shell +nb orm downgrade <插件模块名>@base +``` + +### check + +检查数据库模式是否与模型定义一致。机器人启动前会自动运行此命令(`ALEMBIC_STARTUP_CHECK=true` 时),并在检查失败时阻止启动。 + +```shell +nb orm check +``` + +## 配置 + +### sqlalchemy_database_url + +默认数据库连接 URL。参见 [数据库驱动和后端](.#数据库驱动和后端) 和 [引擎配置 — SQLAlchemy 2.0 文档](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls)。 + +```shell +SQLALCHEMY_DATABASE_URL=dialect+driver://username:password@host:port/database +``` + +### sqlalchemy_bind + +bind keys(一般为插件模块名)到数据库连接 URL、[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 参数字典或 [`AsyncEngine`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.AsyncEngine) 实例的字典。 +例如,我们想要让 `nonebot-plugin-wordcloud` 插件使用一个 SQLite 数据库,并开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 便于 debug,而其他插件使用默认的 PostgreSQL 数据库,可以这样配置: + +```shell +SQLALCHEMY_BINDS='{ + "": "postgresql+psycopg://scott:tiger@localhost/mydatabase", + "nonebot_plugin_wordcloud": { + "url": "sqlite+aiosqlite://", + "echo": true + } +}' +``` + +### sqlalchemy_engine_options + +[`create_async_engine()`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#sqlalchemy.ext.asyncio.create_async_engine) 默认参数字典。 + +```shell +SQLALCHEMY_ENGINE_OPTIONS='{ + "pool_size": 5, + "max_overflow": 10, + "pool_timeout": 30, + "pool_recycle": 3600, + "echo": true +}' +``` + +### sqlalchemy_echo + +开启 [Echo 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo) 和 [Echo Pool 选项](https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine.params.echo_pool) 便于 debug。 + +```shell +SQLALCHEMY_ECHO=true +``` + +:::caution 注意 +以上配置之间有覆盖关系,遵循特殊优先于一般的原则,具体为 [`sqlalchemy_database_url`](#sqlalchemy_database_url) > [`sqlalchemy_bind`](#sqlalchemy_bind) > [`sqlalchemy_echo`](#sqlalchemy_echo) > [`sqlalchemy_engine_options`](#sqlalchemy_engine_options)。 +但覆盖顺序并非显而易见,出于清晰考虑,请只配置必要的选项。 +::: diff --git a/website/versioned_docs/version-2.4.0/best-practice/deployment.mdx b/website/versioned_docs/version-2.4.0/best-practice/deployment.mdx new file mode 100644 index 00000000..8c33520b --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/deployment.mdx @@ -0,0 +1,299 @@ +--- +sidebar_position: 3 +description: 部署你的机器人 +--- + +# 部署 + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +在编写完成各类插件后,我们需要长期运行机器人来使得用户能够正常使用。通常,我们会使用云服务器来部署机器人。 + +我们在开发插件时,机器人运行的环境称为开发环境;而在部署后,机器人运行的环境称为生产环境。与开发环境不同的是,在生产环境中,开发者通常不能随意地修改/添加/删除代码,开启或停止服务。 + +## 部署前准备 + +### 项目依赖管理 + +由于部署后的机器人运行在生产环境中,因此,为确保机器人能够正常运行,我们需要保证机器人的运行环境与开发环境一致。我们可以通过以下几种方式来进行依赖管理: + + + + +[Poetry](https://python-poetry.org/) 是一个 Python 项目的依赖管理工具。它可以通过声明项目所依赖的库,为你管理(安装/更新)它们。Poetry 提供了一个 `poetry.lock` 文件,以确保可重复安装,并可以构建用于分发的项目。 + +Poetry 会在安装依赖时自动生成 `poetry.lock` 文件,在**项目目录**下执行以下命令: + +```bash +# 初始化 poetry 配置 +poetry init +# 添加项目依赖,这里以 nonebot2[fastapi] 为例 +poetry add nonebot2[fastapi] +``` + + + + +[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] +``` + + + + +[pip](https://pip.pypa.io/) 是 Python 包管理工具。他并不是一个依赖管理工具,为了尽可能保证环境的一致性,我们可以使用 `requirements.txt` 文件来声明依赖。 + +```bash +pip freeze > requirements.txt +``` + + + + +### 安装 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` 字样时,说明机器人已经启动成功。我们可以通过以下命令来查看机器人的运行日志: + + + + +```bash +nb docker logs +``` + + + + +```bash +docker compose logs +``` + + + + +如果需要停止机器人,我们可以使用以下命令: + + + + +```bash +nb docker down +``` + + + + +```bash +docker compose down +``` + + + + +### 自定义部署 + +在部分情况下,我们需要事先生成 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/)修改这两个文件。 + +修改完成后我们可以直接启动或者手动构建镜像: + + + + +```bash +# 启动机器人 +nb docker up +# 手动构建镜像 +nb docker build +``` + + + + +```bash +# 启动机器人 +docker compose up -d +# 手动构建镜像 +docker compose build +``` + + + + +### 持续集成 + +我们可以使用 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 +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: | + # highlight-next-line + {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 +``` diff --git a/website/versioned_docs/version-2.4.0/best-practice/error-tracking.md b/website/versioned_docs/version-2.4.0/best-practice/error-tracking.md new file mode 100644 index 00000000..a28a22d1 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/error-tracking.md @@ -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` 值。 + +### 配置插件 + +:::caution 注意 +错误跟踪通常在生产环境中使用,因此开发环境中 `sentry_dsn` 留空即会停用插件。 +::: + +在项目 dotenv 配置文件中添加以下配置即可使用: + +```dotenv +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` diff --git a/website/versioned_docs/version-2.4.0/best-practice/multi-adapter.mdx b/website/versioned_docs/version-2.4.0/best-practice/multi-adapter.mdx new file mode 100644 index 00000000..5afb42ad --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/multi-adapter.mdx @@ -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)的特性来实现这一功能。例如: + + + + +```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) +``` + + + + +```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) +``` + + + + +### 在依赖注入中使用重载 + +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),可以帮助你更好地处理跨平台功能,包括事件处理和消息发送等。 +::: diff --git a/website/versioned_docs/version-2.4.0/best-practice/scheduler.md b/website/versioned_docs/version-2.4.0/best-practice/scheduler.md new file mode 100644 index 00000000..229b27ab --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/scheduler.md @@ -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} +) +``` + +:::caution 注意 +由于 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`。 diff --git a/website/versioned_docs/version-2.4.0/best-practice/testing/README.mdx b/website/versioned_docs/version-2.4.0/best-practice/testing/README.mdx new file mode 100644 index 00000000..bba3012d --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/testing/README.mdx @@ -0,0 +1,233 @@ +--- +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: + + + + +```bash +poetry add nonebug -G test +``` + + + + +```bash +pdm add nonebug -dG test +``` + + + + +```bash +pip install nonebug +``` + + + + +要运行 NoneBug 测试,还需要额外安装 pytest 异步插件 `pytest-asyncio` 或 `anyio` 以支持异步测试。文档中,我们以 `pytest-asyncio` 为例: + + + + +```bash +poetry add pytest-asyncio -G test +``` + + + + +```bash +pdm add pytest-asyncio -dG test +``` + + + + +```bash +pip install pytest-asyncio +``` + + + + +## 配置测试 + +在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。 + +首先我们需要配置 pytest-asyncio,在 `pyproject.toml` 的 pytest 配置部分添加: + +```toml +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" +``` + +然后,我们在 `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) +async def after_nonebot_init(after_nonebot_init: None): + # 加载适配器 + 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 {4,6,8-10} title=tests/conftest.py +import os + +import pytest +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 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan,你可以在 `pytest_configure` 里添加以下配置: + +```python +import pytest +from nonebug import NONEBOT_START_LIFESPAN + +def pytest_configure(config: pytest.Config): + config.stash[NONEBOT_START_LIFESPAN] = False +``` + +## 编写插件测试 + +在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块: + +
+ 插件示例 + +```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}的天气是...") +``` + +
+ +```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(id="user"), + ) +``` + +在上面的代码中,我们引入了 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(id="user"), + ) + 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(id="user"), + ) + +@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 用法将在后续章节中介绍。 diff --git a/website/versioned_docs/version-2.4.0/best-practice/testing/_category_.json b/website/versioned_docs/version-2.4.0/best-practice/testing/_category_.json new file mode 100644 index 00000000..d315ef11 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/testing/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "单元测试", + "position": 5 +} diff --git a/website/versioned_docs/version-2.4.0/best-practice/testing/behavior.mdx b/website/versioned_docs/version-2.4.0/best-practice/testing/behavior.mdx new file mode 100644 index 00000000..f54eeb66 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/testing/behavior.mdx @@ -0,0 +1,292 @@ +--- +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 来测试它们: + + + + +```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(id="user"), + ) + +@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` 事件响应器应该被忽略。 + + + + +```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(id="user"), + ) + +@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` 事件响应器应该被忽略。通过参数,我们可以指定断言的事件响应器。 + + + + +当然,如果需要忽略某个事件响应器的响应规则和权限检查,强行进入响应流程,我们可以使用 `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(id="user"), + ) + +@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 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(id="user"), + ) + +@pytest.mark.asyncio +async def test_example(app: App): + from awesome_bot.plugins.example import foo + + async with app.test_matcher(foo) as ctx: + # highlight-start + adapter = nonebot.get_adapter(Adapter) + bot = ctx.create_bot(base=Bot, adapter=adapter) + # highlight-end + event = make_event("/foo") + ctx.receive_event(bot, event) + # highlight-start + ctx.should_call_send(event, "message", result=None, bot=bot) + ctx.should_call_api("bell", {}, result=None, adapter=adapter) + # highlight-end +``` + +请注意,对于在依赖注入中使用了非基类对象的情况,我们需要在 `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(id="user"), + ) + +@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) +``` diff --git a/website/versioned_docs/version-2.4.0/best-practice/testing/mock-network.md b/website/versioned_docs/version-2.4.0/best-practice/testing/mock-network.md new file mode 100644 index 00000000..8ed56b5c --- /dev/null +++ b/website/versioned_docs/version-2.4.0/best-practice/testing/mock-network.md @@ -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 客户端 + +~~暂不支持~~ diff --git a/website/versioned_docs/version-2.4.0/community/contact.md b/website/versioned_docs/version-2.4.0/community/contact.md new file mode 100644 index 00000000..c38fa19a --- /dev/null +++ b/website/versioned_docs/version-2.4.0/community/contact.md @@ -0,0 +1,24 @@ +--- +sidebar-position: 0 +description: 遇到问题如何获取帮助 +--- + +# 参与讨论 + +如果在安装或者开发 NoneBot 过程中遇到了任何问题,或者有新奇的点子,欢迎参与我们的社区讨论: + +1. 点击下方链接前往 GitHub,前往 Issues 页面,在 `New Issue` Template 中选择 `Question` + + NoneBot:[![NoneBot project link](https://img.shields.io/github/stars/nonebot/nonebot2?style=social)](https://github.com/nonebot/nonebot2) + +2. 通过 QQ 群(点击下方链接直达) + + [![QQ Chat Group](https://img.shields.io/badge/QQ%E7%BE%A4-768887710-orange?style=social)](https://jq.qq.com/?_wv=1027&k=5OFifDh) + +3. 通过 QQ 频道 + + [![QQ Channel](https://img.shields.io/badge/QQ%E9%A2%91%E9%81%93-NoneBot-orange?style=social)](https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=7b4a3&appChannel=share&businessType=9&from=246610&biz=ka) + +4. 通过 Discord 服务器(点击下方链接直达) + + [![Discord Server](https://discordapp.com/api/guilds/847819937858584596/widget.png?style=shield)](https://discord.gg/VKtE6Gdc4h) diff --git a/website/versioned_docs/version-2.4.0/community/contributing.md b/website/versioned_docs/version-2.4.0/community/contributing.md new file mode 100644 index 00000000..ff452818 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/community/contributing.md @@ -0,0 +1,22 @@ +--- +sidebar-position: 1 +description: 如何为 NoneBot 贡献代码 +--- + +# 贡献指南 + +## Code of Conduct + +请参阅 [Code of Conduct](https://github.com/nonebot/nonebot2/blob/master/CODE_OF_CONDUCT.md)。 + +## 参与开发 + +请参阅 [Contributing](https://github.com/nonebot/nonebot2/blob/master/CONTRIBUTING.md)。 + +## 鸣谢 + +感谢以下开发者对 NoneBot2 作出的贡献: + + + + diff --git a/website/versioned_docs/version-2.4.0/developer/adapter-writing.md b/website/versioned_docs/version-2.4.0/developer/adapter-writing.md new file mode 100644 index 00000000..33cd2b7a --- /dev/null +++ b/website/versioned_docs/version-2.4.0/developer/adapter-writing.md @@ -0,0 +1,608 @@ +--- +sidebar_position: 1 +description: 编写适配器对接新的平台 +--- + +# 编写适配器 + +在编写适配器之前,我们需要先了解[适配器的功能与组成](../advanced/adapter#适配器功能与组成),适配器通常由 `Adapter`、`Bot`、`Event` 和 `Message` 四个部分组成,在编写适配器时,我们需要继承 NoneBot 中的基类,并根据实际平台来编写每个部分功能。 + +## 组织结构 + +NoneBot 适配器项目通常以 `nonebot-adapter-{adapter-name}` 作为项目名,并以**命名空间包**的形式编写,即在 `nonebot/adapters/{adapter-name}` 目录中编写实际代码,例如: + +```tree +📦 nonebot-adapter-{adapter-name} +├── 📂 nonebot +│ ├── 📂 adapters +│ │ ├── 📂 {adapter-name} +│ │ │ ├── 📜 __init__.py +│ │ │ ├── 📜 adapter.py +│ │ │ ├── 📜 bot.py +│ │ │ ├── 📜 config.py +│ │ │ ├── 📜 event.py +│ │ │ └── 📜 message.py +├── 📜 pyproject.toml +└── 📜 README.md +``` + +:::tip 提示 + +上述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。 + +::: + +### 使用 NB-CLI 创建项目 + +我们可以使用脚手架快速创建项目: + +```shell +nb adapter create +``` + +按照指引,输入适配器名称以及存储位置,即可创建一个带有基本结构的适配器项目。 + +## 组成部分 + +:::tip 提示 + +本章节的代码中提到的 `Adapter`、`Bot`、`Event` 和 `Message` 等,均为下文中适配器所编写的类,而非 NoneBot 中的基类。 + +::: + +### Log + +适配器在处理时通常需要打印日志,但直接使用 NoneBot 的默认 `logger` 不方便区分适配器输出和其它日志。因此我们可以使用 NoneBot 提供的 `logger_wrapper` 方法,自定义一个 `log` 函数用于快捷打印适配器日志: + +```python {3} title=log.py +from nonebot.utils import logger_wrapper + +log = logger_wrapper("your_adapter_name") +``` + +这个 `log` 函数会在默认 `logger` 中添加适配器名称前缀,它接收三个参数:日志等级、日志内容以及可选的异常,具体用法如下: + +```python +from .log import log + +log("DEBUG", "A DEBUG log.") +log("INFO", "A INFO log.") + +try: + ... +except Exception as e: + log("ERROR", "something error.", e) +``` + +### Config + +通常适配器需要一些配置项,例如平台连接密钥等。适配器的配置方法与[插件配置](../appendices/config#%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE)类似,例如: + +```python title=config.py +from pydantic import BaseModel + +class Config(BaseModel): + xxx_id: str + xxx_token: str +``` + +配置项的读取将在下方 [Adapter](#adapter) 中介绍。 + +### Adapter + +Adapter 负责转换事件、调用接口,以及正确创建 Bot 对象并注册到 NoneBot 中。在编写平台相关内容之前,我们需要继承基类,并实现适配器的基本信息: + +```python {9,11,14,18} title=adapter.py +from typing import Any +from typing_extensions import override + +from nonebot.drivers import Driver +from nonebot import get_plugin_config +from nonebot.adapters import Adapter as BaseAdapter + +from .config import Config + +class Adapter(BaseAdapter): + @override + def __init__(self, driver: Driver, **kwargs: Any): + super().__init__(driver, **kwargs) + # 读取适配器所需的配置项 + self.adapter_config: Config = get_plugin_config(Config) + + @classmethod + @override + def get_name(cls) -> str: + """适配器名称""" + return "your_adapter_name" +``` + +#### 与平台交互 + +NoneBot 提供了多种 [Driver](../advanced/driver) 来帮助适配器进行网络通信,主要分为客户端和服务端两种类型。我们需要**根据平台文档和特性**选择合适的通信方式,并编写相关方法用于初始化适配器,与平台建立连接和进行交互: + +##### 客户端通信方式 + +```python {12,23,24} title=adapter.py +import asyncio +from typing_extensions import override + +from nonebot import get_plugin_config +from nonebot.exception import WebSocketClosed +from nonebot.drivers import Request, WebSocketClientMixin + +class Adapter(BaseAdapter): + @override + def __init__(self, driver: Driver, **kwargs: Any): + super().__init__(driver, **kwargs) + self.adapter_config: Config = get_plugin_config(Config) + self.task: Optional[asyncio.Task] = None # 存储 ws 任务 + self.setup() + + def setup(self) -> None: + if not isinstance(self.driver, WebSocketClientMixin): + # 判断用户配置的Driver类型是否符合适配器要求,不符合时应抛出异常 + raise RuntimeError( + f"Current driver {self.config.driver} doesn't support websocket client connections!" + f"{self.get_name()} Adapter need a WebSocket Client Driver to work." + ) + # 在 NoneBot 启动和关闭时进行相关操作 + self.driver.on_startup(self.startup) + self.driver.on_shutdown(self.shutdown) + + async def startup(self) -> None: + """定义启动时的操作,例如和平台建立连接""" + self.task = asyncio.create_task(self._forward_ws()) # 建立 ws 连接 + + async def _forward_ws(self): + request = Request( + method="GET", + url="your_platform_websocket_url", + headers={"token": "..."}, # 鉴权请求头 + ) + while True: + try: + async with self.websocket(request) as ws: + try: + # 处理 websocket + ... + except WebSocketClosed as e: + log( + "ERROR", + "WebSocket Closed", + e, + ) + except Exception as e: + log( + "ERROR", + "Error while process data from " + "websocket platform_websocket_url. " + "Trying to reconnect...", + e, + ) + finally: + # 这里要断开 Bot 连接 + except Exception as e: + # 尝试重连 + log( + "ERROR", + "Error while setup websocket to " + "platform_websocket_url. Trying to reconnect...", + e, + ) + await asyncio.sleep(3) # 重连间隔 + + async def shutdown(self) -> None: + """定义关闭时的操作,例如停止任务、断开连接""" + + # 断开 ws 连接 + if self.task is not None and not self.task.done(): + self.task.cancel() +``` + +##### 服务端通信方式 + +```python {30,38} title=adapter.py +from nonebot import get_plugin_config +from nonebot.drivers import ( + Request, + ASGIMixin, + WebSocket, + HTTPServerSetup, + WebSocketServerSetup +) + +class Adapter(BaseAdapter): + @override + def __init__(self, driver: Driver, **kwargs: Any): + super().__init__(driver, **kwargs) + self.adapter_config: Config = get_plugin_config(Config) + self.setup() + + def setup(self) -> None: + if not isinstance(self.driver, ASGIMixin): + raise RuntimeError( + f"Current driver {self.config.driver} doesn't support asgi server!" + f"{self.get_name()} Adapter need a asgi server driver to work." + ) + # 建立服务端路由 + # HTTP Webhook 路由 + http_setup = HTTPServerSetup( + URL("your_webhook_url"), # 路由地址 + "POST", # 接收的方法 + "WEBHOOK name", # 路由名称 + self._handle_http, # 处理函数 + ) + self.setup_http_server(http_setup) + + # 反向 Websocket 路由 + ws_setup = WebSocketServerSetup( + URL("your_websocket_url"), # 路由地址 + "WebSocket name", # 路由名称 + self._handle_ws, # 处理函数 + ) + self.setup_websocket_server(ws_setup) + + + async def _handle_http(self, request: Request) -> Response: + """HTTP 路由处理函数,只有一个类型为 Request 的参数,且返回值类型为 Response""" + ... + return Response( + status_code=200, # 状态码 + headers={"something": "something"}, # 响应头 + content="xxx", # 响应内容 + ) + + async def _handle_ws(self, websocket: WebSocket) -> Any: + """WebSocket 路由处理函数,只有一个类型为 WebSocket 的参数""" + ... +``` + +更多通信交互方式可以参考以下适配器: + +- [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py) - `WebSocket 客户端`、`WebSocket 服务端`、`HTTP WEBHOOK`、`HTTP POST` +- [QQGuild](https://github.com/nonebot/adapter-qqguild/blob/master/nonebot/adapters/qqguild/adapter.py) - `WebSocket 服务端` +- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py) - `HTTP WEBHOOK` + +#### 建立 Bot 连接 + +在与平台建立连接后,我们需要将 [Bot](#bot) 实例化,并调用适配器提供的的 `bot_connect` 方法告知 NoneBot 建立了 Bot 连接。在与平台断开连接或出现某些异常进行重连时,我们需要调用 `bot_disconnect` 方法告知 NoneBot 断开了 Bot 连接。 + +```python {7,8,11} title=adapter.py +from .bot import Bot + +class Adapter(BaseAdapter): + + def _handle_connect(self): + bot_id = ... # 通过配置或者平台 API 等方式,获取到 Bot 的 ID + bot = Bot(self, self_id=bot_id) # 实例化 Bot + self.bot_connect(bot) # 建立 Bot 连接 + + def _handle_disconnect(self): + self.bot_disconnect(bot) # 断开 Bot 连接 +``` + +#### 转换 Event 事件 + +在接收到来自平台的事件数据后,我们需要将其转为适配器的 [Event](#event),并调用 Bot 的 `handle_event` 方法来让 Bot 对事件进行处理: + +```python title=adapter.py +import asyncio +from typing import Any, Dict + +from nonebot.compat import type_validate_python + +from .bot import Bot +from .event import Event +from .log import log + +class Adapter(BaseAdapter): + + @classmethod + def payload_to_event(cls, payload: Dict[str, Any]) -> Event: + """根据平台事件的特性,转换平台 payload 为具体 Event + + Event 模型继承自 pydantic.BaseModel,具体请参考 pydantic 文档 + """ + + # 做一层异常处理,以应对平台事件数据的变更 + try: + return type_validate_python(your_event_class, payload) + except Exception as e: + # 无法正常解析为具体 Event 时,给出日志提示 + log( + "WARNING", + f"Parse event error: {str(payload)}", + ) + # 也可以尝试转为基础 Event 进行处理 + return type_validate_python(Event, payload) + + + async def _forward(self, bot: Bot): + + payload: Dict[str, Any] # 接收到的事件数据 + event = self.payload_to_event(payload) + # 让 bot 对事件进行处理 + asyncio.create_task(bot.handle_event(event)) +``` + +#### 调用平台 API + +我们需要实现 `Adapter` 的 `_call_api` 方法,使开发者能够调用平台提供的 API。如果通过 WebSocket 通信可以通过 `send` 方法来发送数据,如果采用 HTTP 请求,则需要通过 NoneBot 提供的 `Request` 对象,调用 `driver` 的 `request` 方法来发送请求。 + +```python {11} title=adapter.py +from typing import Any +from typing_extensions import override + +from nonebot.drivers import Request, WebSocket + +from .bot import Bot + +class Adapter(BaseAdapter): + + @override + async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: + log("DEBUG", f"Calling API {api}") # 给予日志提示 + platform_data = your_handle_data_method(data) # 自行将数据转为平台所需要的格式 + + # 采用 HTTP 请求的方式,需要构造一个 Request 对象 + request = Request( + method="GET", # 请求方法 + url=api, # 接口地址 + headers=..., # 请求头,通常需要包含鉴权信息 + params=platform_data, # 自行处理数据的传输形式 + # json=platform_data, + # data=platform_data, + ) + # 发送请求,返回结果 + return await self.driver.request(request) + + + # 采用 WebSocket 通信的方式,可以直接调用 send 方法发送数据 + # 通过某种方式获取到 bot 对应的 websocket 对象 + ws: WebSocket = your_get_websocket_method(bot.self_id) + + await ws.send_text(platform_data) # 发送 str 类型的数据 + await ws.send_bytes(platform_data) # 发送 bytes 类型的数据 + await ws.send(platform_data) # 是以上两种方式的合体 + + # 接收并返回结果,同样的,也有 str 和 bytes 的区别 + return await ws.receive_text() + return await ws.receive_bytes() + return await ws.receive() +``` + +`调用平台 API` 实现方式具体可以参考以下适配器: + +Websocket: + +- [OneBot V11](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/adapter.py#L127) +- [OneBot V12](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v12/adapter.py#L162) + +HTTP: + +- [QQ 频道](https://github.com/nonebot/adapter-qqguild/blob/master/nonebot/adapters/qqguild/adapter.py#L354) +- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/adapter.py#L145) +- [飞书](https://github.com/nonebot/adapter-feishu/blob/master/nonebot/adapters/feishu/adapter.py#L158) + +### Bot + +Bot 是机器人开发者能够直接获取并使用的核心对象,负责存储平台机器人相关信息,并提供回复事件、调用 API 的上层方法。我们需要继承基类 `Bot`,并实现相关方法: + +```python {20,25,34} title=bot.py +from typing import TYPE_CHECKING, Any, Union +from typing_extensions import override + +from nonebot.message import handle_event +from nonebot.adapters import Bot as BaseBot + +from .event import Event +from .message import Message, MessageSegment + +if TYPE_CHECKING: + from .adapter import Adapter + + +class Bot(BaseBot): + """ + your_adapter_name 协议 Bot 适配。 + """ + + @override + def __init__(self, adapter: Adapter, self_id: str, **kwargs: Any): + super().__init__(adapter, self_id) + self.adapter: Adapter = adapter + # 一些有关 Bot 的信息也可以在此定义和存储 + + async def handle_event(self, event: Event): + # 根据需要,对事件进行某些预处理,例如: + # 检查事件是否和机器人有关操作,去除事件消息首尾的 @bot + # 检查事件是否有回复消息,调用平台 API 获取原始消息的消息内容 + ... + # 调用 handle_event 让 NoneBot 对事件进行处理 + await handle_event(self, event) + + @override + async def send( + self, + event: Event, + message: Union[str, Message, MessageSegment], + **kwargs: Any, + ) -> Any: + # 根据平台实现 Bot 回复事件的方法 + + # 将消息处理为平台所需的格式后,调用发送消息接口进行发送,例如: + data = message_to_platform_data(message) + await self.send_message( + data=data, + ... + ) +``` + +### Event + +Event 是 NoneBot 中的事件主体对象,所有平台消息在进入处理流程前需要转换为 NoneBot 事件。我们需要继承基类 `Event`,并实现相关方法: + +```python {5,8,13,18,23,28,33} title=event.py +from typing_extensions import override + +from nonebot.compat import model_dump +from nonebot.adapters import Event as BaseEvent + +class Event(BaseEvent): + + @override + def get_event_name(self) -> str: + # 返回事件的名称,用于日志打印 + return "event name" + + @override + def get_event_description(self) -> str: + # 返回事件的描述,用于日志打印,请注意转义 loguru tag + return escape_tag(repr(model_dump(self))) + + @override + def get_message(self): + # 获取事件消息的方法,根据事件具体实现,如果事件非消息类型事件,则抛出异常 + raise ValueError("Event has no message!") + + @override + def get_user_id(self) -> str: + # 获取用户 ID 的方法,根据事件具体实现,如果事件没有用户 ID,则抛出异常 + raise ValueError("Event has no context!") + + @override + def get_session_id(self) -> str: + # 获取事件会话 ID 的方法,根据事件具体实现,如果事件没有相关 ID,则抛出异常 + raise ValueError("Event has no context!") + + @override + def is_tome(self) -> bool: + # 判断事件是否和机器人有关 + return False +``` + +然后根据平台消息的类型,编写各种不同的事件,并且注意要根据事件类型实现 `get_type` 方法,具体请参考[事件类型](../advanced/adapter#事件类型)。消息类型事件还应重写 `get_message` 和 `get_user_id` 等方法,例如: + +```python {7,16,20,25,34,42} title=event.py +from .message import Message + +class HeartbeatEvent(Event): + """心跳时间,通常为元事件""" + + @override + def get_type(self) -> str: + return "meta_event" + +class MessageEvent(Event): + """消息事件""" + message_id: str + user_id: str + + @override + def get_type(self) -> str: + return "message" + + @override + def get_message(self) -> Message: + # 返回事件消息对应的 NoneBot Message 对象 + return self.message + + @override + def get_user_id(self) -> str: + return self.user_id + +class JoinRoomEvent(Event): + """加入房间事件,通常为通知事件""" + user_id: str + room_id: str + + @override + def get_type(self) -> str: + return "notice" + +class ApplyAddFriendEvent(Event): + """申请添加好友事件,通常为请求事件""" + user_id: str + + @override + def get_type(self) -> str: + return "request" +``` + +### Message + +Message 负责正确序列化消息,以便机器人插件处理。我们需要继承 `MessageSegment` 和 `Message` 两个类,并实现相关方法: + +```python {9,12,17,22,27,30,36} title=message.py +from typing import Type, Iterable +from typing_extensions import override + +from nonebot.utils import escape_tag + +from nonebot.adapters import Message as BaseMessage +from nonebot.adapters import MessageSegment as BaseMessageSegment + +class MessageSegment(BaseMessageSegment["Message"]): + @classmethod + @override + def get_message_class(cls) -> Type["Message"]: + # 返回适配器的 Message 类型本身 + return Message + + @override + def __str__(self) -> str: + # 返回该消息段的纯文本表现形式,通常在日志中展示 + return "text of MessageSegment" + + @override + def is_text(self) -> bool: + # 判断该消息段是否为纯文本 + return self.type == "text" + + +class Message(BaseMessage[MessageSegment]): + @classmethod + @override + def get_segment_class(cls) -> Type[MessageSegment]: + # 返回适配器的 MessageSegment 类型本身 + return MessageSegment + + @staticmethod + @override + def _construct(msg: str) -> Iterable[MessageSegment]: + # 实现从字符串中构造消息数组,如无字符串嵌入格式可直接返回文本类型 MessageSegment + ... +``` + +然后根据平台具体的消息类型,来实现各种 `MessageSegment` 消息段,具体可以参考以下适配器: + +- [OneBot](https://github.com/nonebot/adapter-onebot/blob/master/nonebot/adapters/onebot/v11/message.py#L77-L261) +- [QQGuild](https://github.com/nonebot/adapter-qqguild/blob/master/nonebot/adapters/qqguild/message.py#L22-L150) +- [Telegram](https://github.com/nonebot/adapter-telegram/blob/beta/nonebot/adapters/telegram/message.py#L43-L250) + +## 适配器测试 + +关于适配器测试相关内容在这里不再展开,开发者可以根据需要进行合适的测试。这里为开发者提供几个常见问题的解决方法: + +1. 在测试中无法导入 editable 模式安装的适配器代码。在 pytest 的 `conftest.py` 内添加如下代码: + + ```python title=tests/conftest.py + from pathlib import Path + import nonebot.adapters + nonebot.adapters.__path__.append( # type: ignore + str((Path(__file__).parent.parent / "nonebot" / "adapters").resolve()) + ) + ``` + +2. 需要计算适配器测试覆盖率,请在 `pyproject.toml` 中添加 pytest 配置: + + ```toml title=pyproject.toml + [tool.pytest.ini_options] + addopts = "--cov nonebot/adapters/{adapter-name} --cov-report term-missing" + ``` + +## 后续工作 + +在完成适配器代码的编写后,如果想要将适配器发布到 NoneBot 商店,我们需要将适配器发布到 PyPI 中,然后前往[商店](/store/adapters)页面,切换到适配器页签,点击**发布适配器**按钮,填写适配器相关信息并提交。 + +另外建议编写适配器文档或者一些插件开发示例,以便其他开发者使用我们的适配器。 diff --git a/website/versioned_docs/version-2.4.0/developer/plugin-publishing.mdx b/website/versioned_docs/version-2.4.0/developer/plugin-publishing.mdx new file mode 100644 index 00000000..e6af1c66 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/developer/plugin-publishing.mdx @@ -0,0 +1,202 @@ +--- +sidebar_position: 0 +description: 在商店发布自己的插件 +--- + +# 发布插件 + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +NoneBot 为开发者提供了分享插件给大家使用的方式——商店。本章节将会介绍如何将我们写好的插件发布到商店。 + +:::tip 提示 +本章节仅包含插件发布流程指导,插件开发请查阅前述章节。 +::: + +## 准备工作 + +### 插件命名规范 + +NoneBot 插件使用下述命名规范: + +- 对于**项目名**,建议统一以 `nonebot-plugin-` 开头,之后为拟定的插件名字,词间用横杠 `-` 分隔; + - **项目名**用于代码仓库名称、PyPI 包的发布名称等; + - 本文使用 `nonebot-plugin-{your-plugin-name}` 为例。 +- 对于**模块名**,建议与**项目名**一致,但词间用下划线 `_` 分隔,即统一以 `nonebot_plugin_` 开头,之后为拟定的插件名字; + - **模块名**用于程序导入使用,应为插件文件(夹)的名称; + - 本文使用 `nonebot_plugin_{your_plugin_name}` 为例。 + +### 项目结构 + +:::tip 提示 +本段所述的项目结构仅作推荐,不做强制要求,保证实际可用性即可。 +::: + +插件程序本身结构可参考[插件结构](../tutorial/create-plugin.md#插件结构)一节,唯一区别在于,插件包可以直接处于项目顶层。 + +插件项目的一种组织结构如下: + +```tree +📦 nonebot-plugin-{your-plugin-name} +├── 📂 nonebot_plugin_{your_plugin_name} +│ ├── 📜 __init__.py +│ └── 📜 config.py +├── 📜 pyproject.toml +└── 📜 README.md +``` + +#### 第三方项目模板 + +一些社区用户可能会分享自己制作的项目模板方便大家使用,如:[A-kirami/nonebot-plugin-template](https://github.com/A-kirami/nonebot-plugin-template) 等。 + +:::tip 提示 +本文档**不保证**第三方模板的适用性。 + +根据项目模板提供的使用指导补全/修改相应内容后上传到 GitHub 即可。 +::: + +### 插件依赖 + +本段指导填写插件依赖,避免不正确的依赖信息导致插件无法正常工作。 + +依赖填写的基本原则:程序直接导入了什么第三方库,就添加什么第三方包依赖;能用哪些第三方库的特性,就根据使用的特性锁定第三方包版本。 + +:::caution 注意 + +1. 插件需要添加 `nonebot2` 为依赖以避免“幽灵依赖”; +2. 插件需要将使用的适配器加入依赖列表,如:使用 OneBot 适配器的插件应添加 `nonebot-adapter-onebot` 依赖; +3. 由于 `nonebot` 是指 `nonebot1` **而非** `nonebot2`,因此要注意**不要**将 `nonebot` 添加为插件的依赖,以免造成冲突; +4. 尽可能避免使用 `==` 锁定单一版本,增强与其它插件的兼容性。 + +::: + +### 填写插件元数据 + +请注意,插件发布要求**必须**填写元数据才能通过审核。 + +下面是一个示例: + +```python title=nonebot_plugin_{your_plugin_name}/__init__.py +from nonebot.plugin import PluginMetadata + +from .config import Config + +__plugin_meta__ = PluginMetadata( + name="{插件名称}", + description="{插件介绍}", + usage="{插件用法}", + + type="{插件分类}", + # 发布必填,当前有效类型有:`library`(为其他插件编写提供功能),`application`(向机器人用户提供功能)。 + + homepage="{项目主页}", + # 发布必填。 + + config=Config, + # 插件配置项类,如无需配置可不填写。 + + supported_adapters={"~onebot.v11", "~telegram"}, + # 支持的适配器集合,其中 `~` 在此处代表前缀 `nonebot.adapters.`,其余适配器亦按此格式填写。 + # 若插件可以保证兼容所有适配器(即仅使用基本适配器功能)可不填写,否则应该列出插件支持的适配器。 +) +``` + +:::caution 注意 +`__plugin_meta__` 变量**必须**处于插件最外层(如 `__init__.py` 中),否则无法正常识别。 + +一般做法是在 `__init__.py` 中定义 `__plugin_meta__`。 +::: + +:::tip 提示 +带花括号 `{}` 的内容需要自行替换,注意**一定要把原有的花括号去掉**。 +::: + +### 准备项目主页 + +通常可以使用 GitHub 项目页面作为项目主页,在 `README.md` 文件中编写插件介绍等内容。 + +内容大致包括: + +- 插件功能介绍 +- 安装方法(建议至少有 `nb-cli` 方式安装,**不要**使用旧式的 `bot.py` 配置) +- 插件配置项(若无可跳过) +- 插件设置的触发规则(若无可跳过) +- 插件的其它用法(按需编写) + +:::tip 提示 +可以参考[第三方项目模板](#第三方项目模板)。 +::: + +### 发布至 [PyPI](https://pypi.org) + +根据选用的构建系统,在项目的 `pyproject.toml` 中填入必要信息后进行构建与发布。 + +:::tip 提示 +不同构建工具的使用可能存在差别。本文仅以 [`pdm`](https://pdm.fming.dev/latest/), +[`poetry`](https://python-poetry.org/docs/), [`setuptools`](https://setuptools.pypa.io/en/latest/) +构建系统**本地构建与发布**为示例讲解,其余构建/管理工具等和自动化构建的用法请读者自行探索。 +::: + + + + +```bash +poetry publish --build # 构建并发布 + +# 等效于以下两个命令 +poetry build # 只构建 +poetry publish # 只发布先前的构建 +``` + + + + + +```bash +pdm publish # 构建并发布 + +# 等效于以下两个命令 +pdm build # 只构建 +pdm publish --no-build # 只发布先前的构建 +``` + + + + + +```bash +pip install build twine # 安装通用构建与发布工具 + +python -m build --sdist --wheel . # 只构建 +twine upload dist/* # 只发布先前的构建 +``` + + + + +:::tip 提示 +发布前建议自行测试构建包是否可用,避免遗漏代码文件或资源文件等问题。 +::: + +## 商店审核 + +### 提交申请 + +完成在 PyPI 的插件发布流程后,前往[商店](/store/plugins)页面,切换到插件页签,点击 **发布插件** 按钮。 + +在弹出的插件信息提交表单内,填入您所要发布的相应插件信息。请注意,如果插件需要必要配置项才能正常导入,请在“插件配置项”中填写必要的内容(请勿填写密钥等敏感信息)。 + +完成填写后,点击 **发布** 按钮,这将自动跳转 NoneBot 仓库内的“发布插件”页面。确认信息无误后点击页面下方的 `Submit new issue` 按钮进行最终提交即可。 + +### 等待插件审核 + +插件发布 Issue 创建后,将会经过 **NoneFlow Bot** 的自动前置检查,以确保插件信息正确无误、插件能被正确加载。 + +:::tip 提示 +若插件检查未通过或信息有误,**不必**关闭当前 Issue。只需更新插件并上传到 PyPI/修改信息后在当前 Issue 追加任意内容的评论(如“已更新”等)即可重新触发插件检查。 +::: + +之后,NoneBot 的维护者和一些插件开发者会初步检查插件代码,帮助减少该插件的问题。 + +完成这些步骤后,您的插件将会被自动合并到[商店](/store/plugins),而您也将成为 [**NoneBot 贡献者**](https://github.com/nonebot/nonebot2/graphs/contributors)的一员。 diff --git a/website/versioned_docs/version-2.4.0/editor-support.md b/website/versioned_docs/version-2.4.0/editor-support.md new file mode 100644 index 00000000..0d2f96d6 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/editor-support.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 2 +description: 配置编辑器以获得最佳体验 +--- + +# 编辑器支持 + +框架基于 [PEP484](https://www.python.org/dev/peps/pep-0484/)、[PEP 561](https://www.python.org/dev/peps/pep-0561/)、[PEP8](https://www.python.org/dev/peps/pep-0008/) 等规范进行开发并且**拥有完整类型注解**。框架使用 Pyright(Pylance)工具进行类型检查,确保代码可以被编辑器正确解析。 + +## 编辑器推荐配置 + +### Visual Studio Code + +在 Visual Studio Code 中,可以使用 Pylance Language Server 并启用 `Type Checking` 配置以达到最佳开发体验。 + +1. 在 VSCode 插件视图搜索并安装 `Python (ms-python.python)` 和 `Pylance (ms-python.vscode-pylance)` 插件。 +2. 修改 VSCode 配置 + 在 VSCode 设置视图搜索配置项 `Python: Language Server` 并将其值设置为 `Pylance`,搜索配置项 `Python > Analysis: Type Checking Mode` 并将其值设置为 `basic`。 + + 或者向项目 `.vscode` 文件夹中配置文件添加以下内容: + + ```json title=settings.json + { + "python.languageServer": "Pylance", + "python.analysis.typeCheckingMode": "basic" + } + ``` + +### 其他 + +欢迎提交 Pull Request 添加其他编辑器配置推荐。点击左下角 `Edit this page` 前往编辑。 diff --git a/website/versioned_docs/version-2.4.0/ospp/2021.md b/website/versioned_docs/version-2.4.0/ospp/2021.md new file mode 100644 index 00000000..da4bd2d0 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/ospp/2021.md @@ -0,0 +1,193 @@ +--- +sidebar_position: 0 +description: 开源软件供应链点亮计划 - 暑期 2021 +mdx: + format: md +--- + +# 暑期 2021 + +**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。 + +NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 + +## NoneBot v1 + +### 更新 NoneBot v1 文档中的“指南”部分 + +由于 NoneBot v1 和 aiocqhttp 最初基于的 QQ 机器人平台不再提供服务,CQHTTP 接口也转型且改名为 OneBot 标准,目前 NoneBot v1 文档的“指南”部分和 aiocqhttp 文档有部分过时内容需要更新。我们希望将其中与旧的机器人平台相关的内容改为基于 go-cqhttp 或通用的 OneBot 表述,同时对 NoneBot v1 的 awesome-bot 示例做一次全面检查,修改其中可能已经不可用的部分。 + +**难度**:低 + +**导师**:[@cleoold](https://github.com/cleoold) + +**产出要求** + +- 修改“指南”文档和 aiocqhttp 文档中与旧的 QQ 机器人平台相关的部分 +- 检查 awesome-bot 示例是否有已经过时/不可用的地方,并更新/修复 +- 修改“图灵机器人”案例,使用其它 AI 聊天 API 提供商(需先做简单调研) + +**技术要求** + +- 熟悉 Python 编程语言及 asyncio 机制 +- 了解 Git 基本用法 +- 了解聊天机器人基本开发过程 +- 了解 VuePress 更佳 + +### NoneBot v1 API 文档自动生成 + +目前 NoneBot v1 的文档中“API”部分是手动编写的,在更新代码接口的同时需要手动更新文档,可能造成文档与代码不匹配,形成额外的维护成本。我们希望将 API 文档改为直接编写在 Python docstring 中,通过工具自动生成 API 文档。 + +**难度**:中 + +**导师**:[@cleoold](https://github.com/cleoold) + +**产出要求** + +- 调研市面上常见的 Python API 文档生成工具 +- 在代码中补充 API 文档 +- 编写或应用开源工具自动生成 API 文档 +- 配置 GitHub Actions 或其它 CI 自动化构建和部署 API 文档 + +**技术要求** + +- 熟悉 Python 编程语言及 asyncio 和 Type Hints +- 了解 Git 基本用法 +- 了解 Sphinx 等文档生成工具更佳 +- 了解 GitHub Actions 等 CI 工具更佳 + +## NoneBot v2 + +### NoneBot v2 自动化测试框架“NoneBug” + +在聊天机器人的开发过程中,一套自动化的测试机制是非常重要的,特别是对于 NoneBot 2 这类为大型机器人开发而设计的项目来说,需要手动测试每一个边际条件是非常痛苦的。我们希望能够开发一款基于 NoneBot 2 插件机制的自动化测试框架,为 NoneBot 2 用户提供一套易用便捷、高度灵活的自动化测试框架。 + +**难度**:高 + +**导师**:[@yanyongyu](https://github.com/yanyongyu) + +**产出要求** + +- 调研现有的 Python 和其它语言集成测试框架 +- 设计 NoneBug 的用户 API 和实现方式 +- 实现 NoneBug 自动化测试框架 +- 编写详细的使用文档 + +**技术要求** + +- 熟悉 Python 编程语言及 asyncio 和 Type Hints +- 了解 Git 基本用法 +- 了解 NoneBot v2 的基本原理和使用方式 +- 了解主流的 Python 自动化测试框架 + +### NoneBot v2 Telegram 适配器 + +目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。Telegram 是一款较为广泛使用的安全即时聊天软件,同时其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个 Telegram 适配器来支持 Telegram 机器人的开发。 + +**难度**:中 + +**导师**:[@yanyongyu](https://github.com/yanyongyu) + +**产出要求** + +- 调研 Telegram Bot API 以及 WebHook 等官方接口 +- 编写 Telegram 适配器并能够使用 +- 代码遵守项目 Contributing 规范 + +**技术要求** + +- 熟悉 Python 编程语言及 asyncio 和 Type Hints +- 了解 Git 基本用法 +- 了解 Web 开发相关知识 +- 了解 Sphinx 等文档生成工具更佳 + +### NoneBot v2 飞书适配器 + +目前 NoneBot v2 已支持 OneBot、Mirai HTTP API、钉钉协议,社区反馈有更多的平台需求,希望能在 NoneBot v2 获得更多的跨平台支持,提高机器人的便携性。同时,我们也希望随着新平台加入,提升现有 NoneBot v2 核心代码的平台通用性。飞书是目前企业用户广泛使用的即时聊天和协作软件,其官方提供了丰富的聊天机器人 API,因此我们希望为 NoneBot v2 编写一个飞书适配器来支持飞书机器人的开发。 + +**难度**:中 + +**导师**:[@yanyongyu](https://github.com/yanyongyu) + +**产出要求** + +- 调研飞书机器人 API 以及 WebHook 等官方接口 +- 编写飞书适配器并能够使用 +- 代码遵守项目 Contributing 规范 + +**技术要求** + +- 熟悉 Python 编程语言及 asyncio 和 Type Hints +- 了解 Git 基本用法 +- 了解 Web 开发相关知识 +- 了解 Sphinx 等文档生成工具更佳 + +## OneBot + +### 设计 OneBot v12 接口标准 + +目前的 OneBot 标准的 v11 版本仍然与 QQ 平台有较多耦合,我们希望在 v12 去掉与 QQ 耦合的历史包袱,形成一个通用的、可扩展的、易于使用的同时易于实现的聊天机器人接口标准。 + +**难度**:中 + +**导师**:[@richardchien](https://github.com/richardchien) + +**产出要求** + +- 调研各聊天机器人平台的官方/非官方接口特点 +- 通用化 OneBot 核心 API,分离 QQ 特定的 API,去掉无用 API +- 优化现有的通信、消息表示机制 +- 补充 QQ 特定的缺失 API +- 文档需符合风格指南 + +**技术要求** + +- 熟悉至少两个聊天平台的聊天机器人开发 +- 了解 Git 基本用法 +- 了解使用不同语言编写聊天机器人时的常用实践 +- 对文档的优雅性与美观性有追求更佳 + +### 实现 Rust 版 libonebot + +目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Rust 编写一个 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用 Rust 快速编写具体的 OneBot 实现。同时,我们希望借此项目在聊天机器人社区中推广 Rust 编程语言。 + +> 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 + +**难度**:高 + +**导师**:[@richardchien](https://github.com/richardchien) + +**产出要求** + +- 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 +- 充分考虑同时兼容 OneBot v11 和 v12 接口 +- 能够根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口 +- 编写详细的使用文档 +- 如果可能,与 v12 设计项目联动,实现第一手 v12 支持 + +**技术要求** + +- 熟悉聊天机器人开发 +- 熟悉 Rust Web 开发 + +### 实现自选语言版 libonebot + +目前最常用的 OneBot 实现包括 go-cqhttp、onebot-kotlin、node-onebot 等,这些实现都各自重复实现了 Web 通信、消息解析、配置读写等功能,当社区中的开发者想针对一个新的聊天平台实现 OneBot 时,他们往往同样需要再次实现类似逻辑。我们希望使用 Python、Go、Kotlin、Node、PHP、C#.NET 等主流语言(任选一个)编写 libonebot 模块,该模块实现所有 OneBot 实现所共享的功能,从而方便其他开发者们使用对应语言快速编写具体的 OneBot 实现。 + +> 注:这里的逻辑是 libonebot + 针对某聊天平台的对接代码 = 某聊天平台的 OneBot 实现,libonebot 要做的是让 OneBot 实现的开发者只需编写针对特定聊天平台的对接代码,而无需关心 OneBot 标准定义的通信方式、消息格式等。 + +**难度**:中 + +**导师**:[@richardchien](https://github.com/richardchien) + +**产出要求** + +- 实现所有 OneBot 实现所共享的功能,包括 Web 通信、消息解析、配置读写等 +- 充分考虑同时兼容 OneBot v11 和 v12 接口 +- 编写详细的使用文档 +- 如果可能,实现更多附加特性,如根据用户(OneBot 实现的开发者)所实现的接口自动实现类似 get_available_apis 等接口、实现第一手 v12 支持等 + +**技术要求** + +- 熟悉聊天机器人开发 +- 熟悉所选语言的 Web 开发 diff --git a/website/versioned_docs/version-2.4.0/ospp/2022.md b/website/versioned_docs/version-2.4.0/ospp/2022.md new file mode 100644 index 00000000..e23c274c --- /dev/null +++ b/website/versioned_docs/version-2.4.0/ospp/2022.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 1 +description: 开源之夏 - 暑期 2022 +mdx: + format: md +--- + +# 暑期 2022 + +**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 + +NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 + +## NoneBot2 命令行 CLI 交互体验升级 + +NoneBot2 为用户提供了命令行脚手架 ──`nb-cli`,辅助用户更好地上手项目以及进行开发。nb-cli 主要包括:创建项目、运行项目、安装与卸载插件、部署项目等功能。随着 NoneBot2 Beta 版本的发布,脚手架功能存在一定的定位不明确、功能体验不佳。本项目旨在重新设计 nb-cli 功能框架,完善功能,优化用户体验。 + +**难度**:进阶 + +**导师**:[@yanyongyu](https://github.com/yanyongyu) + +**产出要求** + +- 设计 nb-cli 功能框架 + - 明确各功能模块 + - 设计用户交互模式 +- 完成 nb-cli 主要功能代码 + - 项目管理 + - 插件管理 + - 其它 +- 同步更新使用文档 + +**技术要求** + +- 熟悉 Python 命令行交互代码编写 +- 熟悉 NoneBot2 框架功能 +- 熟悉 NoneBot2 项目组织方式 + +**成果仓库** + +- +- + +## NoneBot2 命令行即时交互通信设计与实现 + +NoneBot2 在早期提供了基于网页的 nonebot-plugin-test 插件,无需平台适配接入即可对机器人进行测试,方便了开发者直观的感受机器人文本交互功能。我们希望提供一款基于命令行的适配器/驱动器,用于无平台适配接入、可以运行机器人的场景进行功能体验或测试。 + +**难度**:进阶 + +**导师**:[@mnixry](https://github.com/mnixry) + +**产出要求** + +- 设计命令行与 NoneBot2 通信模式 + - 直接调用/HTTP/WebSocket +- 设计命令行交互界面 +- 实现相应适配器/驱动器 +- 同步更新使用说明文档 + +**技术要求** + +- 熟悉 Python 命令行交互代码编写 +- 熟悉 NoneBot2 框架功能 +- 熟悉 NoneBot2 项目组织方式 + +**成果仓库** + +- + +## NoneBot2 用户上手与深入教程设计 + +NoneBot2 为用户提供了详细的文档介绍,辅助用户更好的上手项目以及进行开发。文档分为基础与进阶两个部分。基础部分帮助新用户快速上手开发,主要包括:安装 NoneBot2、使用脚手架、创建配置项目、使用适配器、加载插件、定义消息事件、处理消息事件、调用平台 API 等。进阶部分向已经熟悉开发流程的用户介绍更多高级技巧,主要包括:NoneBot2 工作原理、定时任务、权限控制、钩子函数、跨插件访问、单元测试、发布插件等。目前文档对于用户而言过于费解,导致用户难以理解 NoneBot2 开发。本项目旨在优化文档内容,使其更加通俗易懂,不让文档成为用户上手的阻碍,同时完善进阶内容,让有更复杂需求的用户,同样能从文档中受益。 + +相关 issue: + +- +- + +**难度**:进阶 + +**导师**:[@SK-415](https://github.com/SK-415) + +**产出要求** + +- 文档通俗易懂 + - 附有适当的图片指引(如 asciinema) +- 内容完整,由浅入深 +- 适当的界面美化,合理分配布局 + +**技术要求** + +- 熟悉文档结构组织与语言表达 +- 熟悉 NoneBot2 框架功能 +- 熟悉 NoneBot2 项目组织方式 + +**成果仓库** + +- diff --git a/website/versioned_docs/version-2.4.0/ospp/2023.md b/website/versioned_docs/version-2.4.0/ospp/2023.md new file mode 100644 index 00000000..32095373 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/ospp/2023.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 2 +description: 开源之夏 - 暑期 2023 +mdx: + format: md +--- + +# 暑期 2023 + +**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 + +NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 + +## NoneBot 项目管理图形化面板 + +NoneBot 目前提供了开箱即用的命令行脚手架来帮助初次使用的用户更快的上手编写应用。但是,对于未有一定开发经验的用户,命令行的使用仍具有一定的困难。此外,其他项目如 koishi、vue 等,均可通过图形化界面的形式为用户提供更便捷的项目开发。因此,我们希望借助现有命令行脚手架的可扩展特性,提供一个项目管理面板服务,以网页的形式帮助用户开发 NoneBot 应用。 + +**难度**:进阶 + +**导师**:[@mnixry](https://github.com/mnixry) + +**产出要求** + +- 设计并实现项目管理面板相关功能 + - 创建与管理项目 + - 配置与运行项目 + - NoneBot 插件管理 +- 实现相应 nb-cli 插件提供面板服务 +- 代码符合 NoneBot Contributing 规范 + +**技术要求** + +- 熟悉 nb-cli 相关功能 +- 熟悉 NoneBot 框架功能 +- 熟悉前后端相关实现方式 + +**成果仓库** + +- + +## NoneBot Discord 适配器 + +NoneBot 作为一个跨平台聊天机器人框架,目前已有 OneBot、飞书、Telegram、QQ 频道等诸多平台的适配支持。作为众多用户期待的平台适配之一,我们希望借此机会接入 Discord 聊天机器人。 + +**难度**:进阶 + +**导师**:[@iyume](https://github.com/iyume) + +**产出要求** + +- 调研 Discord Bot 相关功能与接口 +- 设计与编写 NoneBot Discord 适配器 +- 代码符合 NoneBot Contributing 规范 + +**技术要求** + +- 熟悉 NoneBot 框架功能 +- 熟悉 NoneBot 各模块职责与适配器编写 + +**成果仓库** + +- + +## NoneBot 数据库支持插件 + +NoneBot 的插件系统为用户实现应用提供了极高的便捷性,但因此也增加了插件统一管理的难度。目前,我们发现许多用户发布的插件中存在文件存储结构化数据、数据存放散乱等现象,同时插件间也可能产生冲突。因此,我们希望提供一个统一的数据存储与管理方式,便于用户读写应用数据。 + +**难度**:进阶 + +**导师**:[@yanyongyu](https://github.com/yanyongyu) + +**产出要求** + +- 设计并实现 ORM 插件 + - 提供关系模型定义功能 + - 提供模型迁移与管理功能 + - 能较好的支持 Python 类型检查与推导 +- 编写相应的用户使用文档 +- 代码符合 NoneBot Contributing 规范 + +**技术要求** + +- 熟悉 NoneBot 框架功能与插件编写 +- 熟悉 SQLAlchemy 等 ORM 框架 + - 熟悉 SQLAlchemy ORM + - 熟悉 alembic 等迁移工具 +- 熟悉 nb-cli 插件编写 + +**成果仓库** + +- diff --git a/website/versioned_docs/version-2.4.0/ospp/2024.md b/website/versioned_docs/version-2.4.0/ospp/2024.md new file mode 100644 index 00000000..dec90b48 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/ospp/2024.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 3 +description: 开源之夏 - 暑期 2024 +mdx: + format: md +--- + +# 暑期 2024 + +**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。 + +NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。 + +## NonePress 官网组件库更新与优化 + +NoneBot 官网目前采用基于 TailwindCSS 自研的 NonePress 组件库及 Docusaurus 框架进行构建。由于相关依赖版本迭代迅速,目前官网组件库已产生了较大的版本落后。本项目希望在跟进框架新版本的基础上,对文档整体视觉体验进行重新设计,提升页面的无障碍访问性,基于 React Hydrate 特性实现完整的静态网站生成(SSG)以提升搜索引擎优化(SEO)水平。在解决以上问题的基础上,可对网页的开发以及生产构建性能做相应的优化提升,例如在生产构建使用自有的 webpack loader、替换现有的热重载逻辑以减少开发环境启动耗时等。 + +**难度**:进阶 + +**导师**:[@yanyongyu](https://github.com/yanyongyu) + +**产出要求** + +- 基于 Docusaurus v3 重构 NonePress 组件库及相关插件 + - 升级相关依赖并重新打造 Docusaurus theme(布局与组件) + - 根据需求实现/修改 Docusaurus 插件使得官网内容构建正常 + - 能够提升页面渲染性能与 MDX 相关能力 +- 升级官网采用新版组件库 + - Algolia 索引与 SEO 正常 + - 桌面端与移动端显示正常 + - 优化官网开发与生产构建体验 +- (可选)优化官网部分页面 + - 优化官网过长的 changelog + - 优化官网插件商店的展示细节 + +**技术要求** + +- 熟练掌握 TS、PostCSS、TSX、MDX等相关技术 +- 掌握 React、Docusaurus、tailwind css 等框架 +- 熟悉静态网站生成 SSG、SEO 优化与 Algolia 索引原理等 + +**成果仓库** + +- + +## NoneFlow 社区自动化工作流管理优化 + +NoneFlow 在 NoneBot 社区中承担着重要的角色,它由 NoneBot 框架基于 GitHub APP 编写而成,能够自动化的完成许多复杂流程的处理,如:用户请求提交插件到商店时进行自动化检测,并在人工审核通过后自动存储至 registry;定时自动更新 registry 内插件信息,跟进插件新版本情况等。但是,在长期的使用中发现了一些问题和不足的地方,例如:项目本身结构复杂耦合,添加新自动化流程与维护现有流程困难;目前采用了 GitHub 用户名作为插件作者名,但已有不少插件作者改名;插件存储至 registry 并定时更新,缺少统计相关信息以帮助商店更好的展示当前插件状态;插件作者想要修改插件信息时无法便捷的找到操作方式等。本项目希望针对以上问题与不足的地方进行修复与优化,提升用户体验。 + +**难度**:进阶 + +**导师**:[@uy/sun](https://github.com/he0119) + +**产出要求** + +- 重构现有工作流处理结构 + - 整合现有 Issue、Pull Request、Git 相关操作 + - 提供用户修改信息的处理方式 + - 正确处理 PR 的 Open、Close、Draft 状态 +- 修复流程中存在的问题 + - 插件作者名正确展示 + - registry 定时更新中需要插件测试环境隔离 +- 在 registry 定时更新的同时提供统计数据 + +**技术要求** + +- 掌握 GitHub APP 开发 + - 熟悉 GitHub REST API、GraphQL 等 + - 熟悉 GitHub APP 权限限制 +- 熟悉 NoneBot 框架与 Python 相关技术 +- 熟悉 Git、GitHub Action、GitHub 工作流 + +**成果仓库** + +- + +## NoneBlockly 低代码框架开发 + +经过深入分析社区反馈,我们发现部分新手因不熟悉编程概念或框架本身而遇到问题。为了解决初学者在使用面向开发者的聊天机器人框架 NoneBot 时遇到的挑战,我们计划引入 Blockly 提供低代码编程支持。通过减少常见的编码错误和降低入门门槛,使框架对初学者更加友好,从而提升用户体验并有助于 NoneBot 生态的成长。本项目将基于 Blockly 实现 NoneBot 插件的低代码编写,使得用户能够快速搭建聊天机器人。 + +**难度**:进阶 + +**导师**:[@mnixry](https://github.com/mnixry) + +**产出要求** + +- 实现 NoneBlockly 低代码开发框架 + - 能够基于 Alconna 编写跨平台插件 + - 确保插件对 Python 和 NoneBot 版本的兼容性 + - 支持对多种类型 NoneBot 事件的响应 + - 支持对 NoneBot 消息对象的便捷操作 + - 集成 localstore 文件存储、apscheduler 定时任务、网络请求等常用功能 +- 对接 NB-CLI 脚手架,通过脚手架扩展使用低代码框架 + +**技术要求** + +- 掌握 Python 与 NoneBot 框架的使用 + - 熟悉 NoneBot 插件的开发,包括事件响应与消息处理等 + - 熟悉 NoneBot 生态组件(Alconna、localstore、apscheduler等)的使用 + - 了解 NB-CLI 脚手架的扩展开发 +- 熟悉 Blockly 低代码框架的使用和开发 + +**成果仓库** + +- diff --git a/website/versioned_docs/version-2.4.0/quick-start.mdx b/website/versioned_docs/version-2.4.0/quick-start.mdx new file mode 100644 index 00000000..95dfcfeb --- /dev/null +++ b/website/versioned_docs/version-2.4.0/quick-start.mdx @@ -0,0 +1,119 @@ +--- +sidebar_position: 1 +description: 尝试使用 NoneBot + +options: + menu: + - category: tutorial + weight: 10 +--- + +import Asciinema from "@site/src/components/Asciinema"; +import Messenger from "@site/src/components/Messenger"; + +# 快速上手 + +:::caution 前提条件 + +- 请确保你的 Python 版本 >= 3.9 +- **我们强烈建议使用虚拟环境进行开发**,如果没有使用虚拟环境,请确保已经卸载可能存在的 NoneBot v1!!! + ```bash + pip uninstall nonebot + ``` + +::: + +在本章节中,我们将介绍如何使用脚手架来创建一个 NoneBot 简易项目。项目将基于 nb-cli 脚手架运行,并允许我们从商店安装插件。 + + + +## 安装脚手架 + +确保你已经安装了 Python 3.9 及以上版本,然后在命令行中执行以下命令: + +1. 安装 [pipx](https://pypa.github.io/pipx/) + + ```bash + python -m pip install --user pipx + python -m pipx ensurepath + ``` + + 如果在此步骤的输出中出现了“open a new terminal”或者“re-login”字样,那么请关闭当前终端并重新打开一个新的终端。 + +2. 安装脚手架 + + ```bash + pipx install nb-cli + ``` + +安装完成后,你可以在命令行使用 `nb` 命令来使用脚手架。如果出现无法找到命令的情况(例如出现“Command not found”字样),请参考 [pipx 文档](https://pypa.github.io/pipx/) 检查你的环境变量。 + +## 创建项目 + +使用脚手架来创建一个项目: + +```bash +nb create +``` + +这一指令将会执行创建项目的流程,你将会看到一些询问: + +1. 项目模板 + + ```bash + [?] 选择一个要使用的模板: bootstrap (初学者或用户) + ``` + + 这里我们选择 `bootstrap` 模板,它是一个简单的项目模板,能够安装商店插件。如果你需要**自行编写插件**,这里请选择 `simple` 模板。 + +2. 项目名称 + + ```bash + [?] 项目名称: awesome-bot + ``` + + 这里我们以 `awesome-bot` 为例,作为项目名称。你可以根据自己的需要来命名。 + +3. 其他选项 + 请注意,多选项使用**空格**选中或取消,**回车**确认。 + + ```bash + [?] 要使用哪些驱动器? FastAPI (FastAPI 驱动器) + [?] 要使用哪些适配器? Console (基于终端的交互式适配器) + [?] 立即安装依赖? (Y/n) Yes + [?] 创建虚拟环境? (Y/n) Yes + ``` + + 这里我们选择了创建虚拟环境,nb-cli 在之后的操作中将会自动使用这个虚拟环境。如果你不需要自动创建虚拟环境或者已经创建了其他虚拟环境,nb-cli 将会安装依赖至当前激活的 Python 虚拟环境。 + +4. 选择内置插件 + + ```bash + [?] 要使用哪些内置插件? echo + ``` + + 这里我们选择 `echo` 插件作为示例。这是一个简单的复读回显插件,可以用于测试你的机器人是否正常运行。 + +## 运行项目 + +在项目创建完成后,你可以在**项目目录**中使用以下命令来运行项目: + +```bash +nb run +``` + +你现在应该已经运行起来了你的第一个 NoneBot 项目了!请注意,生成的项目中使用了 `FastAPI` 驱动器和 `Console` 适配器,你之后可以自行修改配置或安装其他适配器。 + +## 尝试使用 + +在项目运行起来后,`Console` 适配器会在你的终端启动交互模式,你可以直接在输入框中输入 `/echo hello world` 来测试你的机器人是否正常运行。 + + diff --git a/website/versioned_docs/version-2.4.0/tutorial/application.md b/website/versioned_docs/version-2.4.0/tutorial/application.md new file mode 100644 index 00000000..3d692891 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/application.md @@ -0,0 +1,110 @@ +--- +sidebar_position: 0 +description: 创建一个 NoneBot 项目 + +options: + menu: + - category: tutorial + weight: 20 +--- + +# 手动创建项目 + +在[快速上手](../quick-start.mdx)中,我们已经介绍了如何安装和使用 `nb-cli` 创建一个项目。在本章节中,我们将简要介绍如何在不使用 `nb-cli` 的方式创建一个机器人项目的**最小实例**并启动。如果你想要了解 NoneBot 的启动流程,也可以阅读本章节。 + +:::caution 警告 +我们十分不推荐直接创建机器人项目,请优先考虑使用 nb-cli 进行项目创建。 +::: + +一个机器人项目的**最小实例**中**至少**需要包含以下内容: + +- 入口文件:初始化并运行机器人的 Python 文件 +- 配置文件:存储机器人启动所需的配置 +- 插件:为机器人提供具体的功能 + +下面我们创建一个项目文件夹,来存放项目所需文件,以下步骤均在该文件夹中进行。 + +## 安装依赖 + +在创建项目前,我们首先需要将项目所需依赖安装至环境中。 + +1. (可选)创建虚拟环境,以 venv 为例 + + ```bash + python -m venv .venv --prompt nonebot2 + # windows + .venv\Scripts\activate + # linux/macOS + source .venv/bin/activate + ``` + +2. 安装 nonebot2 以及驱动器 + + ```bash + pip install 'nonebot2[fastapi]' + ``` + + 驱动器包名可以在 [驱动器商店](/store/drivers) 中找到。 + +3. 安装适配器 + + ```bash + pip install nonebot-adapter-console + ``` + + 适配器包名可以在 [适配器商店](/store/adapters) 中找到。 + +## 创建配置文件 + +配置文件用于存放 NoneBot 运行所需要的配置项,使用 [`pydantic`](https://docs.pydantic.dev/) 以及 [`python-dotenv`](https://saurabh-kumar.com/python-dotenv/) 来读取配置。配置项需符合 dotenv 格式,复杂类型数据需使用 JSON 格式填写。具体可选配置方式以及配置项详情参考[配置](../appendices/config.mdx)。 + +在**项目文件夹**中创建一个 `.env` 文本文件,并写入以下内容: + +```bash title=.env +HOST=0.0.0.0 # 配置 NoneBot 监听的 IP / 主机名 +PORT=8080 # 配置 NoneBot 监听的端口 +COMMAND_START=["/"] # 配置命令起始字符 +COMMAND_SEP=["."] # 配置命令分割字符 +``` + +## 创建入口文件 + +入口文件( Entrypoint )顾名思义,是用来初始化并运行机器人的 Python 文件。入口文件需要完成框架的初始化、注册适配器、加载插件等工作。 + +:::tip 提示 +如果你使用 `nb-cli` 创建项目,入口文件不会被创建,该文件功能会被 `nb run` 命令代替。 +::: + +在**项目文件夹**中创建一个 `bot.py` 文件,并写入以下内容: + +```python title=bot.py +import nonebot +from nonebot.adapters.console import Adapter as ConsoleAdapter # 避免重复命名 + +# 初始化 NoneBot +nonebot.init() + +# 注册适配器 +driver = nonebot.get_driver() +driver.register_adapter(ConsoleAdapter) + +# 在这里加载插件 +nonebot.load_builtin_plugins("echo") # 内置插件 +# nonebot.load_plugin("thirdparty_plugin") # 第三方插件 +# nonebot.load_plugins("awesome_bot/plugins") # 本地插件 + +if __name__ == "__main__": + nonebot.run() +``` + +我们暂时不需要了解其中内容的含义,这些将会在稍后的章节中逐一介绍。在创建完成以上文件并确认已安装所需适配器和插件后,即可运行机器人。 + +## 运行机器人 + +在**项目文件夹**中,使用配置好环境的 Python 解释器运行入口文件(如果使用虚拟环境,请先激活虚拟环境): + +```bash +python bot.py +``` + +如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。 diff --git a/website/versioned_docs/version-2.4.0/tutorial/create-plugin.md b/website/versioned_docs/version-2.4.0/tutorial/create-plugin.md new file mode 100644 index 00000000..9ce4ed4d --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/create-plugin.md @@ -0,0 +1,226 @@ +--- +sidebar_position: 3 +description: 创建并加载自定义插件 + +options: + menu: + - category: tutorial + weight: 50 +--- + +# 插件编写准备 + +在正式编写插件之前,我们需要先了解一下插件的概念。 + +## 插件结构 + +在 NoneBot 中,插件即是 Python 的一个[模块(module)](https://docs.python.org/zh-cn/3/glossary.html#term-module)。NoneBot 会在导入时对这些模块做一些特殊的处理使得他们成为一个插件。插件间应尽量减少耦合,可以进行有限制的相互调用,NoneBot 能够正确解析插件间的依赖关系。 + +### 单文件插件 + +一个普通的 `.py` 文件即可以作为一个插件,例如创建一个 `foo.py` 文件: + +```tree title=Project +📂 plugins +└── 📜 foo.py +``` + +这个时候模块 `foo` 已经可以被称为一个插件了,尽管它还什么都没做。 + +### 包插件 + +一个包含 `__init__.py` 的文件夹即是一个常规 Python [包 `package`](https://docs.python.org/zh-cn/3/glossary.html#term-regular-package),例如创建一个 `foo` 文件夹: + +```tree title=Project +📂 plugins +└── 📂 foo + └── 📜 __init__.py +``` + +这个时候包 `foo` 同样是一个合法的插件,插件内容可以在 `__init__.py` 文件中编写。 + +## 创建插件 + +:::caution 注意 +如果在之前的[快速上手](../quick-start.mdx)章节中已经使用 `bootstrap` 模板创建了项目,那么你需要做出如下修改: + +1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` + + ```tree title=Project + 📦 awesome-bot + ├── 📂 awesome_bot + │ └── 📂 plugins + ├── 📜 pyproject.toml + └── 📜 README.md + ``` + +2. 修改 `pyproject.toml` 文件中的 `nonebot` 配置项,在 `plugin_dirs` 中添加 `awesome_bot/plugins` + + ```toml title=pyproject.toml + [tool.nonebot] + plugin_dirs = ["awesome_bot/plugins"] + ``` + +::: + +:::caution 注意 +如果在之前的[创建项目](./application.md)章节中手动创建了相关文件,那么你需要做出如下修改: + +1. 在项目目录中创建一个两层文件夹 `awesome_bot/plugins` + + ```tree title=Project + 📦 awesome-bot + ├── 📂 awesome_bot + │ └── 📂 plugins + └── 📜 bot.py + ``` + +2. 修改 `bot.py` 文件中的加载插件部分,取消注释或者添加如下代码 + + ```python title=bot.py + # 在这里加载插件 + nonebot.load_builtin_plugins("echo") # 内置插件 + nonebot.load_plugins("awesome_bot/plugins") # 本地插件 + ``` + +::: + +创建插件可以通过 `nb-cli` 命令从完整模板创建,也可以手动新建空白文件。通过以下命令创建一个名为 `weather` 的插件: + +```bash +$ nb plugin create +[?] 插件名称: weather +[?] 使用嵌套插件? (y/N) N +[?] 输出目录: awesome_bot/plugins +``` + +`nb-cli` 会在 `awesome_bot/plugins` 目录下创建一个名为 `weather` 的文件夹,其中包含的文件将在稍后章节中用到。 + +```tree title=Project +📦 awesome-bot +├── 📂 awesome_bot +│ └── 📂 plugins +| └── 📂 foo +| ├── 📜 __init__.py +| └── 📜 config.py +├── 📜 pyproject.toml +└── 📜 README.md +``` + +## 加载插件 + +:::danger 警告 +请勿在插件被加载前 `import` 插件模块,这会导致 NoneBot 无法将其转换为插件而出现意料之外的情况。 +::: + +加载插件是在机器人入口文件中完成的,需要在框架初始化之后,运行之前进行。 + +请注意,加载的插件模块名称(插件文件名或文件夹名)**不能相同**,且每一个插件**只能被加载一次**,重复加载将会导致异常。 + +如果你使用 `nb-cli` 管理插件,那么你可以跳过这一节,`nb-cli` 将会自动处理加载。 + +如果你**使用自定义的入口文件** `bot.py`,那么你需要在 `bot.py` 中加载插件。 + +```python {5} title=bot.py +import nonebot + +nonebot.init() + +# 加载插件 + +nonebot.run() +``` + +加载插件的方式有多种,但在底层的加载逻辑是一致的。以下是为加载插件提供的几种方式: + +### `load_plugin` + +通过点分割模块名称或使用 [`pathlib`](https://docs.python.org/zh-cn/3/library/pathlib.html) 的 `Path` 对象来加载插件。通常用于加载第三方插件或者项目插件。例如: + +```python +from pathlib import Path + +nonebot.load_plugin("path.to.your.plugin") # 加载第三方插件 +nonebot.load_plugin(Path("./path/to/your/plugin.py")) # 加载项目插件 +``` + +:::caution 注意 +请注意,本地插件的路径应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 +::: + +### `load_plugins` + +加载传入插件目录中的所有插件,通常用于加载一系列本地编写的项目插件。例如: + +```python +nonebot.load_plugins("src/plugins", "path/to/your/plugins") +``` + +:::caution 注意 +请注意,插件目录应该为相对机器人 **入口文件(通常为 bot.py)** 可导入的,例如在项目 `plugins` 目录下。 +::: + +### `load_all_plugins` + +这种加载方式是以上两种方式的混合,加载所有传入的插件模块名称,以及所有给定目录下的插件。例如: + +```python +nonebot.load_all_plugins(["path.to.your.plugin"], ["path/to/your/plugins"]) +``` + +### `load_from_json` + +通过 JSON 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 JSON 变种。通过读取 JSON 文件中的 `plugins` 字段和 `plugin_dirs` 字段进行加载。例如: + +```json title=plugin_config.json +{ + "plugins": ["path.to.your.plugin"], + "plugin_dirs": ["path/to/your/plugins"] +} +``` + +```python +nonebot.load_from_json("plugin_config.json", encoding="utf-8") +``` + +:::tip 提示 +如果 JSON 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 +::: + +### `load_from_toml` + +通过 TOML 文件加载插件,是 [`load_all_plugins`](#load_all_plugins) 的 TOML 变种。通过读取 TOML 文件中的 `[tool.nonebot]` Table 中的 `plugins` 和 `plugin_dirs` Array 进行加载。例如: + +```toml title=plugin_config.toml +[tool.nonebot] +plugins = ["path.to.your.plugin"] +plugin_dirs = ["path/to/your/plugins"] +``` + +```python +nonebot.load_from_toml("plugin_config.toml", encoding="utf-8") +``` + +:::tip 提示 +如果 TOML 配置文件中的字段无法满足你的需求,可以使用 [`load_all_plugins`](#load_all_plugins) 方法自行读取配置来加载插件。 +::: + +### `load_builtin_plugin` + +加载一个内置插件,传入的插件名必须为 NoneBot 内置插件。该方法是 [`load_plugin`](#load_plugin) 的封装。例如: + +```python +nonebot.load_builtin_plugin("echo") +``` + +### `load_builtin_plugins` + +加载传入插件列表中的所有内置插件。例如: + +```python +nonebot.load_builtin_plugins("echo", "single_session") +``` + +### 其他加载方式 + +有关其他插件加载的方式,可参考[跨插件访问](../advanced/requiring.md)和[嵌套插件](../advanced/plugin-nesting.md)。 diff --git a/website/versioned_docs/version-2.4.0/tutorial/event-data.mdx b/website/versioned_docs/version-2.4.0/tutorial/event-data.mdx new file mode 100644 index 00000000..5f8ccb54 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/event-data.mdx @@ -0,0 +1,65 @@ +--- +sidebar_position: 6 +description: 通过依赖注入获取所需事件信息 + +options: + menu: + - category: tutorial + weight: 80 +--- + +# 获取事件信息 + +import Messenger from "@site/src/components/Messenger"; + +在 NoneBot 事件处理流程中,获取事件信息并做出对应的操作是非常常见的场景。本章节中我们将介绍如何通过**依赖注入**获取事件信息。 + +## 认识依赖注入 + +在事件处理流程中,事件响应器具有自己独立的上下文,例如:当前响应的事件、收到事件的机器人或者其他处理流程中新增的信息等。这些数据可以根据我们的需求,通过依赖注入的方式,在执行事件处理流程中注入到事件处理函数中。 + +相对于传统的信息获取方法,通过依赖注入获取信息的最大特色在于**按需获取**。如果该事件处理函数不需要任何额外信息即可运行,那么可以不进行依赖注入。如果事件处理函数需要额外的数据,可以通过依赖注入的方式灵活的标注出需要的依赖,在函数运行时便会被按需注入。 + +## 使用依赖注入 + +使用依赖注入获取上下文信息的方法十分简单,我们仅需要在函数的参数中声明所需的依赖,并正确的将函数添加为事件处理依赖即可。在 NoneBot 中,我们可以直接使用 `nonebot.params` 模块中定义的参数类型来声明依赖。 + +例如,我们可以继续改进上一章节中的 `weather` 插件,使其可以获取到 `天气` 命令的地名参数,并根据地名返回天气信息。 + +```python {9,11} title=weather/__init__.py +from nonebot import on_command +from nonebot.rule import to_me +from nonebot.adapters import Message +from nonebot.params import CommandArg + +weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, 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("请输入地名") +``` + +如上方示例所示,我们使用了 `args` 作为注入参数名,注入的内容为 `CommandArg()`,也就是**消息命令后跟随的内容**。在这个示例中,我们获得的参数会被检查是否有效,对无效参数则会结束事件。 + +:::tip 提示 +命令与参数之间可以不需要空格,`CommandArg()` 获取的信息为命令后跟随的内容并去除了头部空白符。例如:`/天气 上海` 消息的参数为 `上海`。 +::: + +:::tip 提示 +`:=` 是 Python 3.8 引入的新语法 [Assignment Expressions](https://docs.python.org/zh-cn/3/reference/expressions.html#assignment-expressions),也称为海象表达式,可以在表达式中直接赋值。 +::: + + + +NoneBot 提供了多种依赖注入类型,可以获取不同的信息,具体内容可参考[依赖注入](../advanced/dependency.mdx)。 diff --git a/website/versioned_docs/version-2.4.0/tutorial/fundamentals.md b/website/versioned_docs/version-2.4.0/tutorial/fundamentals.md new file mode 100644 index 00000000..bb5eea0b --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/fundamentals.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 1 +description: NoneBot 机器人构成及基本使用 + +options: + menu: + - category: tutorial + weight: 30 +--- + +# 机器人的构成 + +了解机器人的基本构成有助于你更好地使用 NoneBot,本章节将介绍 NoneBot 中的基本组成部分,稍后的文档中将会使用到这些概念。 + +使用 NoneBot 框架搭建的机器人具有以下几个基本组成部分: + +1. NoneBot 机器人框架主体:负责连接各个组成部分,提供基本的机器人功能 +2. 驱动器 `Driver`:客户端/服务端的功能实现,负责接收和发送消息(通常为 HTTP 通信) +3. 适配器 `Adapter`:驱动器的上层,负责将**平台消息**与 NoneBot 事件/操作系统的消息格式相互转换 +4. 插件 `Plugin`:机器人的功能实现,通常为负责处理事件并进行一系列的操作 + +除 NoneBot 机器人框架主体外,其他部分均可按需选择、互相搭配,但由于平台的兼容性问题,部分插件可能仅在某些特定平台上可用(这由插件编写者决定)。 + +在接下来的章节中,我们将重点介绍机器人功能实现,即插件 `Plugin` 部分。 diff --git a/website/versioned_docs/version-2.4.0/tutorial/handler.mdx b/website/versioned_docs/version-2.4.0/tutorial/handler.mdx new file mode 100644 index 00000000..466b15b3 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/handler.mdx @@ -0,0 +1,87 @@ +--- +sidebar_position: 5 +description: 处理接收到的特定事件 + +options: + menu: + - category: tutorial + weight: 70 +--- + +# 事件处理 + +import Messenger from "@site/src/components/Messenger"; + +在我们收到事件,并被某个事件响应器正确响应后,便正式开启了对于这个事件的**处理流程**。 + +## 认识事件处理流程 + +就像我们在解决问题时需要遵循流程一样,处理一个事件也需要一套流程。在事件响应器对一个事件进行响应之后,会依次执行一系列的**事件处理依赖**(通常是函数)。简单来说,事件处理流程并不是一个函数、一个对象或一个方法,而是一整套由开发者设计的流程。 + +在这个流程中,我们**目前**只需要了解两个概念:函数形式的“事件处理依赖”(下称“事件处理函数”)和“事件响应器操作”。 + +## 事件处理函数 + +在事件响应器中,事件处理流程可以由一个或多个“事件处理函数”组成,这些事件处理函数将会按照顺序依次对事件进行处理,直到全部执行完成或被中断。我们可以采用事件响应器的“事件处理函数装饰器”来添加这些“事件处理函数”。 + +顾名思义,“事件处理函数装饰器”是一个[装饰器(decorator)](https://docs.python.org/zh-cn/3/glossary.html#term-decorator),那么它的使用方法也同[函数定义](https://docs.python.org/zh-cn/3/reference/compound_stmts.html#function-definitions)中所展示的包装用法相同。例如: + +```python {6-8} title=weather/__init__.py +from nonebot.rule import to_me +from nonebot.plugin import on_command + +weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) + +@weather.handle() +async def handle_function(): + pass # do something here +``` + +如上方示例所示,我们使用 `weather` 响应器的 `handle` 装饰器装饰了一个函数 `handle_function`。`handle_function` 函数会被添加到 `weather` 的事件处理流程中。在 `weather` 响应器被触发之后,将会依次调用 `weather` 响应器的事件处理函数,即 `handle_function` 来对事件进行处理。 + +## 事件响应器操作 + +在事件处理流程中,我们可以使用事件响应器操作来进行一些交互或改变事件处理流程,例如向机器人用户发送消息或提前结束事件处理流程等。 + +事件响应器操作与事件处理函数装饰器类似,通常作为事件响应器 `Matcher` 的[类方法](https://docs.python.org/zh-cn/3/library/functions.html#classmethod)存在,因此事件响应器操作的调用方法也是 `Matcher.func()` 的形式。不过不同的是,事件响应器操作并不是装饰器,因此并不需要@进行标注。 + +```python {8,9} title=weather/__init__.py +from nonebot.rule import to_me +from nonebot.plugin import on_command + +weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) + +@weather.handle() +async def handle_function(): + # await weather.send("天气是...") + await weather.finish("天气是...") +``` + +如上方示例所示,我们使用 `weather` 响应器的 `finish` 操作方法向机器人用户回复了 `天气是...` 并结束了事件处理流程。效果如下: + + + +值得注意的是,在执行 `finish` 方法时,NoneBot 会在向机器人用户发送消息内容后抛出 `FinishedException` 异常来结束事件响应流程。也就是说,在 `finish` 被执行后,后续的程序是不会被执行的。如果你需要回复机器人用户消息但不想事件处理流程结束,可以使用注释的部分中展示的 `send` 方法。 + +:::danger 警告 +由于 `finish` 是通过抛出 `FinishedException` 异常来结束事件的,因此异常可能会被未加限制的 `try-except` 捕获,影响事件处理流程正确处理,导致无法正常结束此事件。请务必在异常捕获中指定错误类型或排除所有 [MatcherException](../api/exception.md#MatcherException) 类型的异常(如下所示),或将 `finish` 移出捕获范围进行使用。 + +```python +from nonebot.exception import MatcherException + +try: + await weather.finish("天气是...") +except MatcherException: + raise +except Exception as e: + pass # do something here +``` + +::: + +目前 NoneBot 提供了多种事件响应器操作,其中包括用于机器人用户交互与流程控制两大类,进阶使用方法可以查看[会话控制](../appendices/session-control.mdx)。 diff --git a/website/versioned_docs/version-2.4.0/tutorial/matcher.md b/website/versioned_docs/version-2.4.0/tutorial/matcher.md new file mode 100644 index 00000000..8d3bf392 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/matcher.md @@ -0,0 +1,58 @@ +--- +sidebar_position: 4 +description: 响应接收到的特定事件 + +options: + menu: + - category: tutorial + weight: 60 +--- + +# 事件响应器 + +事件响应器(Matcher)是对接收到的事件进行响应的基本单元,所有的事件响应器都继承自 `Matcher` 基类。 + +在 NoneBot 中,事件响应器可以通过一系列特定的规则**筛选**出**具有某种特征的事件**,并按照**特定的流程**交由**预定义的事件处理依赖**进行处理。例如,在[快速上手](../quick-start.mdx)中,我们使用了内置插件 `echo` ,它定义的事件响应器能响应机器人用户发送的“/echo hello world”消息,提取“hello world”信息并作为回复消息发送。 + +## 事件响应器辅助函数 + +NoneBot 中所有事件响应器均继承自 `Matcher` 基类,但直接使用 `Matcher.new()` 方法创建事件响应器过于繁琐且不能记录插件信息。因此,NoneBot 中提供了一系列“事件响应器辅助函数”(下称“辅助函数”)来辅助我们用**最简的方式**创建**带有不同规则预设**的事件响应器,提高代码可读性和书写效率。通常情况下,我们只需要使用辅助函数即可完成事件响应器的创建。 + +在 NoneBot 中,辅助函数以 `on()` 或 `on_()` 形式出现(例如 `on_command()`),调用后根据不同的参数返回一个 `Type[Matcher]` 类型的新事件响应器。 + +目前 NoneBot 提供了多种功能各异的辅助函数、具有共同命令名称前缀的命令组以及具有共同参数的响应器组,均可以从 `nonebot` 模块直接导入使用,具体内容参考[事件响应器进阶](../advanced/matcher.md)。 + +## 创建事件响应器 + +在上一节[创建插件](./create-plugin.md#创建插件)中,我们创建了一个 `weather` 插件,现在我们来实现他的功能。 + +我们直接使用 `on_command()` 辅助函数来创建一个事件响应器: + +```python {3} title=weather/__init__.py +from nonebot import on_command + +weather = on_command("天气") +``` + +这样,我们就获得一个名为 `weather` 的事件响应器了,这个事件响应器会对 `/天气` 开头的消息进行响应。 + +:::tip 提示 +如果一条消息中包含“@机器人”或以“机器人的昵称”开始,例如 `@bot /天气` 时,协议适配器会将 `event.is_tome()` 判断为 `True` ,同时也会自动去除 `@bot`,即事件响应器收到的信息内容为 `/天气`,方便进行命令匹配。 +::: + +### 为事件响应器添加参数 + +在辅助函数中,我们可以添加一些参数来对事件响应器进行更加精细的调整,例如事件响应器的优先级、匹配规则等。例如: + +```python {4} title=weather/__init__.py +from nonebot import on_command +from nonebot.rule import to_me + +weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True) +``` + +这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。 + +:::tip 提示 +需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。 +::: diff --git a/website/versioned_docs/version-2.4.0/tutorial/message.md b/website/versioned_docs/version-2.4.0/tutorial/message.md new file mode 100644 index 00000000..86989c49 --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/message.md @@ -0,0 +1,349 @@ +--- +sidebar_position: 7 +description: 处理消息序列与消息段 + +options: + menu: + - category: tutorial + weight: 90 +--- + +# 处理消息 + +在不同平台中,一条消息可能会有承载有各种不同的表现形式,它可能是一段纯文本、一张图片、一段语音、一篇富文本文章,也有可能是多种类型的组合等等。 + +在 NoneBot 中,为确保消息的正常处理与跨平台兼容性,采用了扁平化的消息序列形式,即 `Message` 对象。消息序列是 NoneBot 中的消息载体,无论是接收还是发送的消息,都采用消息序列的形式进行处理。 + +## 认识消息类型 + +### 消息序列 `Message` + +在 NoneBot 中,消息序列 `Message` 的主要作用是用于表达“一串消息”。由于消息序列继承自 `List[MessageSegment]`,所以 `Message` 的本质是由若干消息段所组成的序列。因此,消息序列的使用方法与 `List` 有很多相似之处,例如切片、索引、拼接等。 + +在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们已经通过依赖注入 `CommandArg()` 获取了命令的参数,它的类型即是消息序列。我们使用了消息序列的 `extract_plain_text()` 方法来获取消息序列中的纯文本内容。 + +### 消息段 `MessageSegment` + +顾名思义,消息段 `MessageSegment` 是一段消息。由于消息序列的本质是由若干消息段所组成的序列,消息段可以被认为是构成消息序列的最小单位。简单来说,消息序列类似于一个自然段,而消息段则是组成自然段的一句话。同时,作为特殊消息载体的存在,绝大多数的平台都有着**独特的消息类型**,这些独特的内容均需要由对应的**协议适配器**所提供,以适应不同平台中的消息模式。**这也意味着,你需要导入对应的协议适配器中的消息序列和消息段后才能使用其特殊的工厂方法。** + +:::caution 注意 +消息段的类型是由协议适配器提供的,因此你需要参考协议适配器的文档并导入对应的消息段后才能使用其特殊的消息类型。 + +在上一节的[使用依赖注入](./event-data.mdx#使用依赖注入)中,我们导入的为 `nonebot.adapters.Message` 抽象基类,因此我们无法使用平台特有的消息类型。仅能使用 `str` 作为纯文本消息回复。 +::: + +## 使用消息序列 + +:::caution 注意 +在以下的示例中,为了更好的理解多种类型的消息组成方式,我们将使用 `Console` 协议适配器来演示消息序列的使用方法。在实际使用中,你需要确保你使用的**消息序列类型**与你所要发送的**平台类型**一致。 +::: + +通常情况下,适配器在接收到消息时,会将消息转换为消息序列,可以通过依赖注入 [`EventMessage`](../advanced/dependency.mdx#eventmessage),或者使用 `event.get_message()` 获取。 + +由于消息序列是 `List[MessageSegment]` 的子类,所以你总是可以用和操作 `List` 类似的方式来处理消息序列。例如: + +```python +>>> from nonebot.adapters.console import Message, MessageSegment +>>> message = Message([ + MessageSegment(type="text", data={"text":"hello"}), + MessageSegment(type="markdown", data={"markup":"**world**"}), +]) +>>> for segment in message: +... print(segment.type, segment.data) +... +text {'text': 'hello'} +markdown {'markup': '**world**'} +>>> len(message) +2 +``` + +### 构造消息序列 + +在使用事件响应器操作发送消息时,既可以使用 `str` 作为消息,也可以使用 `Message`、`MessageSegment` 或者 `MessageTemplate`。那么,我们就需要先构造一个消息序列。消息序列可以通过多种方式构造: + +#### 直接构造 + +`Message` 类可以直接实例化,支持 `str`、`MessageSegment`、`Iterable[MessageSegment]` 或适配器自定义类型的参数。 + +```python +from nonebot.adapters.console import Message, MessageSegment + +# str +Message("Hello, world!") +# MessageSegment +Message(MessageSegment.text("Hello, world!")) +# List[MessageSegment] +Message([MessageSegment.text("Hello, world!")]) +``` + +#### 运算构造 + +`Message` 对象可以通过 `str`、`MessageSegment` 相加构造,详情请参考[拼接消息](#拼接消息)。 + +#### 从字典数组构造 + +`Message` 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 `TypeAdapter` 方法进行构造。 + +```python +from pydantic import TypeAdapter +from nonebot.adapters.console import Message, MessageSegment + +# 由字典构造消息段 +TypeAdapter(MessageSegment).validate_python( + {"type": "text", "data": {"text": "text"}} +) == MessageSegment.text("text") + +# 由字典数组构造消息序列 +TypeAdapter(Message).validate_python( + [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}], +) == Message([MessageSegment.text("text"), MessageSegment.text("text")]) +``` + +### 获取消息纯文本 + +由于消息中存在各种类型的消息段,因此 `str(message)` 通常**不能得到消息的纯文本**,而是一个消息序列的字符串表示。 + +NoneBot 为消息段定义了一个方法 `is_text()` ,可以用于判断消息段是否为纯文本;也可以使用 `message.extract_plain_text()` 方法获取消息纯文本。 + +```python +from nonebot.adapters.console import Message, MessageSegment + +# 判断消息段是否为纯文本 +MessageSegment.text("text").is_text() == True + +# 提取消息纯文本字符串 +Message( + [MessageSegment.text("text"), MessageSegment.markdown("**markup**")] +).extract_plain_text() == "text" +``` + +### 遍历 + +消息序列继承自 `List[MessageSegment]` ,因此可以使用 `for` 循环遍历消息段。 + +```python +for segment in message: + ... +``` + +### 比较 + +消息和消息段都可以使用 `==` 或 `!=` 运算符比较是否相同。 + +```python +MessageSegment.text("text") != MessageSegment.text("foo") + +some_message == Message([MessageSegment.text("text")]) +``` + +### 检查消息段 + +我们可以通过 `in` 运算符或消息序列的 `has` 方法来: + +```python +# 是否存在消息段 +MessageSegment.text("text") in message +# 是否存在指定类型的消息段 +"text" in message +``` + +我们还可以使用消息序列的 `only` 方法来检查消息中是否仅包含指定的消息段。 + +```python +# 是否都为指定消息段 +message.only(MessageSegment.text("test")) +# 是否仅包含指定类型的消息段 +message.only("text") +``` + +### 过滤、索引与切片 + +消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 + +```python +from nonebot.adapters.console import Message, MessageSegment + +message = Message( + [ + MessageSegment.text("test"), + MessageSegment.markdown("test2"), + MessageSegment.markdown("test3"), + MessageSegment.text("test4"), + ] +) +# 索引 +message[0] == MessageSegment.text("test") +# 切片 +message[0:2] == Message( + [MessageSegment.text("test"), MessageSegment.markdown("test2")] +) +# 类型过滤 +message["markdown"] == Message( + [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] +) +# 类型索引 +message["markdown", 0] == MessageSegment.markdown("test2") +# 类型切片 +message["markdown", 0:2] == Message( + [MessageSegment.markdown("test2"), MessageSegment.markdown("test3")] +) +``` + +我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 + +```python +message.include("text", "markdown") +message.exclude("text") +``` + +同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。 + +```python +# 指定类型首个消息段索引 +message.index("markdown") == 1 +# 指定类型消息段数量 +message.count("markdown") == 2 +``` + +此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 + +```python +# 获取指定类型指定个数的消息段 +message.get("markdown", 1) == Message([MessageSegment.markdown("test2")]) +``` + +### 拼接消息 + +`str`、`Message`、`MessageSegment` 对象之间可以直接相加,相加均会返回一个新的 `Message` 对象。 + +```python +# 消息序列与消息段相加 +Message([MessageSegment.text("text")]) + MessageSegment.text("text") +# 消息序列与字符串相加 +Message([MessageSegment.text("text")]) + "text" +# 消息序列与消息序列相加 +Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")]) +# 字符串与消息序列相加 +"text" + Message([MessageSegment.text("text")]) +# 消息段与消息段相加 +MessageSegment.text("text") + MessageSegment.text("text") +# 消息段与字符串相加 +MessageSegment.text("text") + "text" +# 消息段与消息序列相加 +MessageSegment.text("text") + Message([MessageSegment.text("text")]) +# 字符串与消息段相加 +"text" + MessageSegment.text("text") +``` + +如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。 + +```python +msg = Message([MessageSegment.text("text")]) +# 自加 +msg += "text" +msg += MessageSegment.text("text") +msg += Message([MessageSegment.text("text")]) +# 附加 +msg.append("text") +msg.append(MessageSegment.text("text")) +# 扩展 +msg.extend([MessageSegment.text("text")]) +``` + +我们也可以通过消息段或消息序列的 `join` 方法来拼接一串消息: + +```python +seg = MessageSegment.text("text") +msg = seg.join( + [ + MessageSegment.text("first"), + Message( + [ + MessageSegment.text("second"), + MessageSegment.text("third"), + ] + ) + ] +) +msg == Message( + [ + MessageSegment.text("first"), + MessageSegment.text("text"), + MessageSegment.text("second"), + MessageSegment.text("third"), + ] +) +``` + +### 使用消息模板 + +为了提供安全可靠的跨平台模板字符,我们提供了一个消息模板功能来构建消息序列 + +它在以下常见场景中尤其有用: + +- 多行富文本编排(包含图片,文字以及表情等) +- 客制化(由 Bot 最终用户提供消息模板时) + +在事实上,它的用法和 `str.format` 极为相近,所以你在使用的时候,总是可以参考[Python 文档](https://docs.python.org/zh-cn/3/library/stdtypes.html#str.format)来达到你想要的效果,这里给出几个简单的例子。 + +默认情况下,消息模板采用 `str` 纯文本形式的格式化: + +```python title=基础格式化用法 +>>> from nonebot.adapters import MessageTemplate +>>> MessageTemplate("{} {}").format("hello", "world") +'hello world' +``` + +如果 `Message.template` 构建消息模板,那么消息模板将采用消息序列形式的格式化,此时的消息将会是平台特定的: + +:::caution 注意 +使用 `Message.template` 构建消息模板时,应注意消息序列为平台适配器提供的类型,不能使用 `nonebot.adapters.Message` 基类作为模板构建。使用基类构建模板与使用 `str` 构建模板的效果是一样的,因此请使用上述的 `MessageTemplate` 类直接构建模板。: +::: + +```python title=平台格式化用法 +>>> from nonebot.adapters.console import Message, MessageSegment +>>> Message.template("{} {}").format("hello", "world") +Message( + MessageSegment.text("hello"), + MessageSegment.text(" "), + MessageSegment.text("world") +) +``` + +消息模板支持使用消息段进行格式化: + +```python title=对消息段进行安全的拼接 +>>> from nonebot.adapters.console import Message, MessageSegment +>>> Message.template("{}{}").format(MessageSegment.markdown("**markup**"), "world") +Message( + MessageSegment(type='markdown', data={'markup': '**markup**'}), + MessageSegment(type='text', data={'text': 'world'}) +) +``` + +消息模板同样支持使用消息序列作为模板: + +```python title=以消息对象作为模板 +>>> from nonebot.adapters.console import Message, MessageSegment +>>> Message.template( +... MessageSegment.text("{user_id}") + MessageSegment.emoji("tada") + +... MessageSegment.text("{message}") +... ).format_map({"user_id": 123456, "message": "hello world"}) +Message( + MessageSegment(type='text', data={'text': '123456'}), + MessageSegment(type='emoji', data={'emoji': 'tada'}), + MessageSegment(type='text', data={'text': 'hello world'}) +) +``` + +:::caution 注意 +只有消息序列中的文本类型消息段才能被格式化,其他类型的消息段将会原样添加。 +::: + +消息模板支持使用拓展控制符来控制消息段类型: + +```python title=使用消息段的拓展控制符 +>>> from nonebot.adapters.console import Message, MessageSegment +>>> Message.template("{name:emoji}").format(name='tada') +Message(MessageSegment(type='emoji', data={'name': 'tada'})) +``` diff --git a/website/versioned_docs/version-2.4.0/tutorial/store.mdx b/website/versioned_docs/version-2.4.0/tutorial/store.mdx new file mode 100644 index 00000000..9332ac8e --- /dev/null +++ b/website/versioned_docs/version-2.4.0/tutorial/store.mdx @@ -0,0 +1,267 @@ +--- +sidebar_position: 2 +description: 从商店安装适配器和插件 + +options: + menu: + - category: tutorial + weight: 40 +--- + +# 获取商店内容 + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import Asciinema from "@site/src/components/Asciinema"; + +:::tip 提示 + +如果你暂时没有获取商店内容的需求,可以跳过本章节。 + +::: + +NoneBot 提供了一个[商店](/store/plugins),商店内容均由社区开发者贡献。你可以在商店中查找你需要的适配器和插件等,进行安装或者参考其文档等。 + +商店中每个内容的卡片都包含了其名称和简介等信息,点击**卡片右上角**链接图标即可跳转到其主页。 + +## 安装插件 + + + +在商店插件页面中,点击你需要安装的插件下方的 `点击复制安装命令` 按钮,即可复制 `nb-cli` 命令。 + +请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装插件并将其添加到加载列表中。 + + + + +```bash +nb plugin install <插件名称> +``` + + + + +```bash +$ nb plugin install +[?] 想要安装的插件名称: <插件名称> +``` + + + + +```bash +pip install <插件包名> +``` + +插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。安装完成后,需要参考[加载插件章节](./create-plugin.md#加载插件)自行加载。 + + + + +如果想要查看插件列表,可以使用以下命令 + +```bash +# 列出商店所有插件 +nb plugin list +# 搜索商店插件 +nb plugin search [可选关键词] +``` + +升级和卸载插件可以使用以下命令 + + + + +```bash +nb plugin update <插件名称> +nb plugin uninstall <插件名称> +``` + + + + +```bash +$ nb plugin update +[?] 想要安装的插件名称: <插件名称> +$ nb plugin uninstall +[?] 想要卸载的插件名称: <插件名称> +``` + + + + +```bash +pip install --upgrade <插件包名> +pip uninstall <插件包名> +``` + +插件包名可以在商店插件卡片中找到,或者使用 `nb-cli` 搜索插件显示的详情中找到。卸载完成后,需要自行移除插件加载。 + + + + +## 安装适配器 + + + +安装适配器与安装插件类似,只是将命令换为 `nb adapter`,这里就不再赘述。 + +请在你的**项目目录**下执行该命令。`nb-cli` 会自动安装适配器并将其添加到注册列表中。 + + + + +```bash +nb adapter install <适配器名称> +``` + + + + +```bash +$ nb adapter install +[?] 想要安装的适配器名称: <适配器名称> +``` + + + + +```bash +pip install <适配器包名> +``` + +适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。安装完成后,需要参考[注册适配器章节](../advanced/adapter.md#注册适配器)自行注册。 + + + + +如果想要查看适配器列表,可以使用以下命令 + +```bash +# 列出商店所有适配器 +nb adapter list +# 搜索商店适配器 +nb adapter search [可选关键词] +``` + +升级和卸载适配器可以使用以下命令 + + + + +```bash +nb adapter update <适配器名称> +nb adapter uninstall <适配器名称> +``` + + + + +```bash +$ nb adapter update +[?] 想要安装的适配器名称: <适配器名称> +$ nb adapter uninstall +[?] 想要卸载的适配器名称: <适配器名称> +``` + + + + +```bash +pip install --upgrade <适配器包名> +pip uninstall <适配器包名> +``` + +适配器包名可以在商店适配器卡片中找到,或者使用 `nb-cli` 搜索适配器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 + + + + +## 安装驱动器 + + + +安装驱动器与安装插件同样类似,只是将命令换为 `nb driver`,这里就不再赘述。 + +如果你使用了虚拟环境,请在你的**项目目录**下执行该命令,`nb-cli` 会自动安装驱动器到虚拟环境中。 + +请注意 `nb-cli` 并不会在安装驱动器后修改项目所使用的驱动器,请自行参考[配置方法](../appendices/config.mdx)章节以及 [`DRIVER` 配置项](../appendices/config.mdx#driver)修改驱动器。 + + + + +```bash +nb driver install <驱动器名称> +``` + + + + +```bash +$ nb driver install +[?] 想要安装的驱动器名称: <驱动器名称> +``` + + + + +```bash +pip install <驱动器包名> +``` + +驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。 + + + + +如果想要查看驱动器列表,可以使用以下命令 + +```bash +# 列出商店所有驱动器 +nb driver list +# 搜索商店驱动器 +nb driver search [可选关键词] +``` + +升级和卸载驱动器可以使用以下命令 + + + + +```bash +nb driver update <驱动器名称> +nb driver uninstall <驱动器名称> +``` + + + + +```bash +$ nb driver update +[?] 想要安装的驱动器名称: <驱动器名称> +$ nb driver uninstall +[?] 想要卸载的驱动器名称: <驱动器名称> +``` + + + + +```bash +pip install --upgrade <驱动器包名> +pip uninstall <驱动器包名> +``` + +驱动器包名可以在商店驱动器卡片中找到,或者使用 `nb-cli` 搜索驱动器显示的详情中找到。卸载完成后,需要自行移除适配器加载。 + + + diff --git a/website/versioned_sidebars/version-2.4.0-sidebars.json b/website/versioned_sidebars/version-2.4.0-sidebars.json new file mode 100644 index 00000000..a25220fd --- /dev/null +++ b/website/versioned_sidebars/version-2.4.0-sidebars.json @@ -0,0 +1,156 @@ +{ + "tutorial": [ + { + "type": "category", + "label": "开始", + "collapsible": false, + "items": ["index", "quick-start", "editor-support"] + }, + { + "type": "category", + "label": "指南", + "items": [ + { + "type": "autogenerated", + "dirName": "tutorial" + } + ] + }, + { + "type": "category", + "label": "深入", + "items": [ + { + "type": "autogenerated", + "dirName": "appendices" + } + ] + }, + { + "type": "category", + "label": "进阶", + "items": [ + { + "type": "autogenerated", + "dirName": "advanced" + } + ] + }, + { + "type": "category", + "label": "最佳实践", + "items": [ + { + "type": "autogenerated", + "dirName": "best-practice" + } + ] + }, + { + "type": "category", + "label": "开发者", + "items": [ + { + "type": "autogenerated", + "dirName": "developer" + } + ] + } + ], + "api": [ + { + "type": "autogenerated", + "dirName": "api" + } + ], + "ecosystem": [ + { + "type": "category", + "label": "关于我们", + "collapsible": false, + "items": [ + { + "type": "autogenerated", + "dirName": "community" + } + ] + }, + { + "type": "category", + "label": "开源之夏", + "collapsible": true, + "items": [ + { + "type": "autogenerated", + "dirName": "ospp" + } + ] + }, + { + "type": "category", + "label": "社区资源", + "collapsible": false, + "items": [ + { + "type": "link", + "label": "插件商店", + "href": "/store/plugins" + }, + { + "type": "link", + "label": "适配器商店", + "href": "/store/adapters" + }, + { + "type": "link", + "label": "驱动器商店", + "href": "/store/drivers" + }, + { + "type": "link", + "label": "机器人商店", + "href": "/store/bots" + }, + { + "type": "link", + "label": "Awesome NoneBot", + "href": "https://awesome.nonebot.dev" + }, + { + "type": "link", + "label": "论坛", + "href": "https://discussions.nonebot.dev" + } + ] + } + ], + "changelog": [ + { + "type": "category", + "label": "更新日志", + "collapsible": false, + "items": [ + { + "type": "link", + "label": "v2.4.0", + "href": "/changelog/" + }, + { + "type": "link", + "label": "v2.1.0", + "href": "/changelog/v2.1.0" + }, + { + "type": "link", + "label": "v2.0.0-beta.2", + "href": "/changelog/v2.0.0-beta.2" + }, + { + "type": "link", + "label": "v2.0.0a7", + "href": "/changelog/v2.0.0a7" + } + ] + } + ] +} diff --git a/website/versions.json b/website/versions.json index ca906009..dab927d3 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1 +1 @@ -["2.3.3", "2.3.2", "2.3.1"] +["2.4.0", "2.3.3", "2.3.2", "2.3.1"]