diff --git a/website/docs/best-practice/alconna/README.mdx b/website/docs/best-practice/alconna/README.mdx index b6f79eac..a0a39bd4 100644 --- a/website/docs/best-practice/alconna/README.mdx +++ b/website/docs/best-practice/alconna/README.mdx @@ -1,32 +1,52 @@ ---- -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) 作为命令解析器, -是一个简单、灵活、高效的命令参数解析器,并且不局限于解析命令式字符串。 +[`nonebot-plugin-alconna`](https://github.com/nonebot/plugin-alconna) 是一类极大地提升了 NoneBot 开发体验的插件。 -该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 +该插件可分为三个部分: +- 增强的命令解析: 基于 [Alconna](https://github.com/ArcletProject/Alconna), 提供一类新的事件响应器辅助函数 `on_alconna`. 相比 `on_command`, `on_shell`, `on_regex` 等函数,`on_alconna` 提供了更强大的命令解析能力与诸多特性。 +- 通用消息组件: 实现了跨平台接收、发送、撤回、编辑、表态消息的功能。 + - `UniMessage` 通用消息模型,支持各适配器下的消息转换和导出,发送。 + - `Text`, `Image`, `At` 等通用消息段模型,既与 `UniMessage` 配合使用,又能用于 `Alconna` 的命令解析。 + - `message_recall`, `message_edit`, `message_reaction` 等功能函数。 + - `Target` 通用消息目标模型,并通过该模型进行主动消息发送。 + - `UniMsg`, `MsgId`, `MsgTarget`, `at_in`, `at_me` 等提供给 nonebot 使用的依赖注入和 `Rule`。 +- 内置功能插件:基于上述部分实现的内置功能插件。 + - `echo`: 通过 `on_alconna` 实现的 echo 插件,支持回显回复消息。 + - `help`: 列出所有 `on_alconna` 事件响应器的帮助信息或其对应的插件信息。 + - `lang`: 切换 `Alconna` 使用的语言 + - `switch`: 禁用/启用某个指令 + - `with`: 针对具有多个子命令的指令,通过 `with` 在当前会话中载入命令头以节省输入。 -该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如: +以最新版本为例 (v0.57), 本插件已支持 NoneBot 生态中几乎所有的适配器, 包括: -- `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数 -- `dispatch`:类似 `CommandGroup`,对目标路径创建一个新的 `AlconnaMatcher`,并将解析结果分配给该 `AlconnaMatcher` -- `got_path`:类似 `got`,但是可以指定目标路径,并且能够验证解析结果是否可用 -- ... +| 协议名称 | 路径 | +|---------------------------------------------------------------------|--------------------------------------| +| [OneBot 协议](https://onebot.dev/) | adapters.onebot11, adapters.onebot12 | +| [Telegram](https://core.telegram.org/bots/api) | adapters.telegram | +| [飞书](https://open.feishu.cn/document/home/index) | adapters.feishu | +| [GitHub](https://docs.github.com/en/developers/apps) | adapters.github | +| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq | +| [钉钉](https://open.dingtalk.com/document/) | adapters.ding | +| [Console](https://github.com/nonebot/adapter-console) | adapters.console | +| [开黑啦](https://developer.kookapp.cn/) | adapters.kook | +| [Mirai](https://docs.mirai.mamoe.net/mirai-api-http/) | adapters.mirai | +| [Ntchat](https://github.com/JustUndertaker/adapter-ntchat) | adapters.ntchat | +| [MineCraft](https://github.com/17TheWord/nonebot-adapter-minecraft) | adapters.minecraft | +| [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 | +| [Dodo IM](https://github.com/nonebot/adapter-dodo) | adapters.dodo | +| [Kritor](https://github.com/nonebot/adapter-kritor) | adapters.kritor | +| [Tailchat](https://github.com/eya46/nonebot-adapter-tailchat) | adapters.tailchat | +| [Mail](https://github.com/mobyw/nonebot-adapter-mail) | adapters.mail | +| [微信公众号](https://github.com/YangRucheng/nonebot-adapter-wxmp) | adapters.wxmp | +| [黑盒语音](https://github.com/lclbm/adapter-heybox) | adapters.heybox | +| [Gewechat](https://github.com/Shine-Light/nonebot-adapter-gewechat) | adapters.gewechat | -基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 -标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 - -该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 ## 安装插件 @@ -61,14 +81,14 @@ pdm add nonebot-plugin-alconna ## 导入插件 -由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**并**导入**其中的 `on_alconna` 来使用命令拓展。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。 +由于 `nonebot-plugin-alconna` 作为插件,因此需要在使用前对其进行**加载**。使用 `require` 方法可轻松完成这一过程,可参考 [跨插件访问](../../advanced/requiring.md) 一节进行了解。 ```python from nonebot import require require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import on_alconna +from nonebot_plugin_alconna import ... ``` ## 使用插件 diff --git a/website/docs/best-practice/alconna/_category_.json b/website/docs/best-practice/alconna/_category_.json index d8e7367f..526fe857 100644 --- a/website/docs/best-practice/alconna/_category_.json +++ b/website/docs/best-practice/alconna/_category_.json @@ -1,4 +1,4 @@ { - "label": "Alconna 命令解析拓展", + "label": "命令解析拓展", "position": 6 } diff --git a/website/docs/best-practice/alconna/builtins.mdx b/website/docs/best-practice/alconna/builtins.mdx new file mode 100644 index 00000000..2e30bbb6 --- /dev/null +++ b/website/docs/best-practice/alconna/builtins.mdx @@ -0,0 +1,291 @@ +--- +sidebar_position: 7 +description: 内置组件 +--- +import Messenger from "@site/src/components/Messenger"; + +# 内置组件 + +`nonebot_plugin_alconna` 插件提供了一系列内置组件以提升开发者和用户体验。 + +## 内置插件 + +类似于 Nonebot 本身提供的内置插件,`nonebot_plugin_alconna` 提供了多个内置插件。 + +### 加载 + +你可以用本插件的 `load_builtin_plugin(s)` 来加载它们: + +```python +from nonebot_plugin_alconna import load_builtin_plugin, load_builtin_plugins + +load_builtin_plugins("echo") +load_builtin_plugins("help", "with") +``` + +### 使用 + +#### echo + +`echo` 插件能将用户发送的消息原样返回。 + + + +#### help + +`help` 插件能列出所有 Alconna 指令。同时还能查询某个指令对应的插件信息。 + + + +help 插件的帮助信息如下: + +``` +/help +## 注释 + query: 选择某条命令的id或者名称查看具体帮助 +显示所有命令帮助 +用法: +可以使用 --hide 参数来显示隐藏命令,使用 -P 参数来显示命令所属插件名称 + +可用的子命令有: +* 是否列出命令所属命名空间 + -N│--namespace│命名空间 [target: str] +## 注释 + target: 指定的命名空间 + 该子命令内可用的选项有: + * 列出所有命名空间 + --list +可用的选项有: +* 查看指定页数的命令帮助 + --page +* 查看命令所属插件的信息 + -P│插件信息│--plugin-info +* 是否列出隐藏命令 + 隐藏│-H│--hide +``` + +#### lang + +`lang` 插件能切换 i18n 的语言设置。 + + + +lang 插件的帮助信息如下: + +``` +/lang +i18n配置相关功能 + +可用的选项有: +* 查看支持的语言列表 + list [name: str] +* 切换语言 + switch [locale: str] +``` + +其中 `list` 选项可以查找某一插件下的语言支持情况 (例如 `/lang list nonebot_plugin_alconna`)。 + +#### switch + +`switch` 插件能用来启用/禁用某个命令,其使用方法与 `help` 类似。 + + + +#### with + +`with` 插件能在当前会话中设置一个局部命令前缀,以便于有多个子命令的指令使用。 + + + +with 插件的帮助信息如下: + +``` +.with [name: str] +with 指令 +用法: +设置局部命令前缀 + +可用的选项有: +* 设置可能的生效时间 + --expire│expire +* 取消当前前缀 + unset│--unset + +快捷命令: +'[.]局部前缀' => [.]with +``` + +### 配置 + +内置插件也有其配置项,并且均以 `NBP_ALC` 开头。 + +- `nbp_alc_echo_tome`: 是否让 `echo` 插件的消息经过 `to_me` 处理 +- `nbp_alc_page_size`: `help` 与 `switch` 插件的共同配置项,表示每页显示的命令数量 +- `nbp_alc_help_text`: `help` 指令的指令名,默认为 "help" +- `nbp_alc_help_alias`: `help` 指令的别名,默认为 "帮助", "命令帮助" +- `nbp_alc_help_all_alias`: `help` 指令显示隐藏指令时的别名,默认为 "所有帮助", "所有命令帮助" +- `nbp_alc_switch_enable`: `switch` 插件的 `enable` 指令的指令名,默认为 "enable" +- `nbp_alc_switch_enable_alias`: `switch` 插件的 `enable` 指令的别名,默认为 "启用", "启用指令" +- `nbp_alc_switch_disable`: `switch` 插件的 `disable` 指令的指令名,默认为 "disable" +- `nbp_alc_switch_disable_alias`: `switch` 插件的 `disable` 指令的别名,默认为 "disable", "禁用", "禁用指令" +- `nbp_alc_with_text`: `with` 插件的指令名,默认为 "with" +- `nbp_alc_with_alias`: `with` 插件的别名,默认为 "局部前缀" + +## 内置匹配拓展 + +目前插件提供了 5 个内置的 `Extension`,它们在 `nonebot_plugin_alconna.builtins.extensions` 下: + +### ReplyRecordExtension + +`ReplyRecordExtension` 可将消息事件中的回复暂存在 extension 中,使得解析用的消息不带回复信息,同时可以在后续的处理中获取回复信息: + +```python +from nonebot_plugin_alconna import MsgId, on_alconna +from nonebot_plugin_alconna.builtins.extensions import ReplyRecordExtension + +matcher = on_alconna("...", extensions=[ReplyRecordExtension()]) + +@matcher.handle() +async def handle(msg_id: MsgId, ext: ReplyRecordExtension): + if reply := ext.get_reply(msg_id): + ... + else: + ... +``` + +### ReplyMergeExtension + +`ReplyMergeExtension` 可将消息事件中的回复指向的原消息合并到当前消息中作为一部分参数: + +```python +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_alconna.builtins.extensions.reply import ReplyMergeExtension + +matcher = on_alconna("...", extensions=[ReplyMergeExtension()]) + +@matcher.handle() +async def handle(content: Match[str]): + ... +``` + +其构造时可传入两个参数: +- `add_left`: 否在当前消息的左侧合并回复消息,默认为 False +- `sep`: 合并时的分隔符,默认为空格 + +### DiscordSlashExtension + +`DiscordSlashExtension` 可自动将 Alconna 对象翻译成 Discord 的 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], ext: DiscordSlashExtension): + await ext.send_followup_msg(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}") +``` + +### MarkdownOutputExtension + +`MarkdownOutputExtension` 可将 Alconna 的自动输出转换为 Markdown 格式 + +其构造时可传入两个参数: +- `escape_dot`: 是否转义句中的点号(用来避免被识别为 url) +- `text_to_image` 将文本转换为图片的函数,可不传入。一般用来设置渲染 markdown 为图片的函数 + +### TelegramSlashExtension + +`TelegramSlashExtension` 可将 Alconna 的命令注册在 Telegram 上以获得提示,类似于 `DiscordSlashExtension`。 + +```python +from nonebot_plugin_alconna import on_alconna +from nonebot.adapters.telegram.model import BotCommandScopeChat +from nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension + +TelegramSlashExtension.set_scope(BotCommandScopeChat()) + +matcher = on_alconna("...", extensions=[TelegramSlashExtension()]) +``` + +## 内置自定义消息段 + +目前插件提供了 3 个内置的 `Segment`,它们在 `nonebot_plugin_alconna.builtins.segments` 下: + +- `Markdown`: 可以传入 **markdown模板** 的元素 +- `MarketFace`: 特指 QQ 的商城表情 +- `MusicShare`: 特指 QQ 的音乐分享卡片 diff --git a/website/docs/best-practice/alconna/command.md b/website/docs/best-practice/alconna/command.md index e5df7cbd..1fd4dcfe 100644 --- a/website/docs/best-practice/alconna/command.md +++ b/website/docs/best-practice/alconna/command.md @@ -7,7 +7,7 @@ description: Alconna 基本介绍 [`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 -我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: +我们先通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: ```python from arclet.alconna import Alconna, Args, Subcommand, Option @@ -38,20 +38,22 @@ print(res.all_matched_args) 命令头是指命令的前缀 (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"]` | 对头 | +命令构造时, `Alconna([prefix], command)` 与 `Alconna(command, [prefix])` 是等价的。 + +| 前缀 | 命令名 | 匹配内容 | 说明 | +|:----------------------------:|:----------:|:---------------------------------------------------------:|:--------:| +| 不传入 | "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`。 @@ -64,9 +66,6 @@ print(res.all_matched_args) 除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: ```python -from alconna import Alconna - - alc = Alconna(".rd{roll:int}") assert alc.parse(".rd123").header["roll"] == 123 ``` @@ -206,6 +205,18 @@ args = Args["foo", BasePattern("@\d+")] ::: +#### AllParam + +`AllParam` 是一个特殊的标注,用于告知解析器该参数接收命令中在此位置之后的所有参数并**结束解析**,可以认为是**泛匹配参数**。 + +`AllParam` 可直接使用 (`Args["xxx", AllParam]`), 也可以传入指定的接收类型 (`Args["xxx", AllParam(str)]`)。 + +:::tip + +在 `nonebot_plugin_alconna` 下,`AllParam` 的返回值为 [`UniMessage`](./uniseg/message.mdx) + +::: + ### default `default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 @@ -271,7 +282,7 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) - `append`,`append_value` - `count` -## 解析结果(Arparma) +## 解析结果 `Alconna.parse` 会返回由 **Arparma** 承载的解析结果 @@ -292,18 +303,31 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) - 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` 键对应的值 ... +- `args.`: 返回 all_matched_args 中 `key` 键对应的值 +- `main_args.`: 返回主命令的解析参数字典中 `key` 键对应的值 +- ``: 返回选项/子命令 `node` 的解析结果 (OptionResult | SubcommandResult) +- `.value`: 返回选项/子命令 `node` 的解析值 +- `.args`: 返回选项/子命令 `node` 的解析参数字典 +- `.`, `.args.`: 返回选项/子命令 `node` 的参数字典中 `key` 键对应的值 + +以及: + +- `options.`: 返回选项 `opt` 的解析结果 (OptionResult) +- `options..value`: 返回选项 `opt` 的解析值 +- `options..args`: 返回选项 `opt` 的解析参数字典 +- `options..`, `options..args.`: 返回选项 `opt` 的参数字典中 `key` 键对应的值 +- `subcommands.`: 返回子命令 `subcmd` 的解析结果 (SubcommandResult) +- `subcommands..value`: 返回子命令 `subcmd` 的解析值 +- `subcommands..args`: 返回子命令 `subcmd` 的解析参数字典 +- `subcommands..`, `subcommands..args.`: 返回子命令 `subcmd` 的参数字典中 `key` 键对应的值 ## 元数据(CommandMeta) diff --git a/website/docs/best-practice/alconna/config.md b/website/docs/best-practice/alconna/config.md index ebce8d57..af900fdd 100644 --- a/website/docs/best-practice/alconna/config.md +++ b/website/docs/best-practice/alconna/config.md @@ -7,8 +7,8 @@ description: 配置项 ## alconna_auto_send_output -- **类型**: `bool` -- **默认值**: `False` +- **类型**: `bool | None` +- **默认值**: `None` 是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 @@ -19,12 +19,12 @@ description: 配置项 是否读取 Nonebot 的配置项 `COMMAND_START` 来作为全局的 Alconna 命令前缀 -## alconna_auto_completion +## alconna_global_completion -- **类型**: `bool` -- **默认值**: `False` +- **类型**: [`CompConfig | None`](./matcher.mdx#补全会话) +- **默认值**: `None` -是否全局启用命令自动补全,启用后会在参数缺失或触发 `--comp` 选项时自自动启用交互式补全。 +全局的补全会话配置 (不代表全局启用补全会话)。 ## alconna_use_origin @@ -42,10 +42,13 @@ description: 配置项 ## alconna_global_extensions -- **类型**: `List[str]` +- **类型**: `list[str]` - **默认值**: `[]` -全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 +全局加载的扩展,其读取路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 + +对于内置扩展,路径为 `nonebot_plugin_alconna.builtins.extensions` 下的模块名,如 `ReplyMergeExtension`,可以使用 `@` 来缩写路径, +如 `@reply:ReplyMergeExtension`。 ## alconna_context_style @@ -73,4 +76,30 @@ description: 配置项 - **类型**: `bool` - **默认值**: `False` -是否启动时拉取一次发送对象列表。 +是否启动时拉取一次[发送对象](./uniseg/utils.mdx#发送对象)列表。 + + +## alconna_builtin_plugins + +- **类型**: `set[str]` +- **默认值**: `set()` + +需要加载的内置插件列表。 + +## alconna_conflict_resolver + +- **类型**: `Literal["raise", "default", "ignore", "replace"]` +- **默认值**: `"default"` + +命令冲突解决策略,决定当不同插件之间或者同一插件之间存在两个以上相同的命令时的处理方式: +- `default`: 默认处理方式,保留两个命令 +- `raise`: 抛出异常 +- `ignore`: 忽略较新的命令 +- `replace`: 替换较旧的命令 + +## alconna_response_self + +- **类型**: `bool` +- **默认值**: `False` + +是否让响应器处理由 bot 自身发送的消息。 diff --git a/website/docs/best-practice/alconna/matcher.mdx b/website/docs/best-practice/alconna/matcher.mdx index be9a670f..f0bffa62 100644 --- a/website/docs/best-practice/alconna/matcher.mdx +++ b/website/docs/best-practice/alconna/matcher.mdx @@ -4,136 +4,133 @@ description: 响应规则的使用 --- import Messenger from "@site/src/components/Messenger"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; -# Alconna 插件 +# `on_alconna` 响应器 -展示: +`nonebot_plugin_alconna` 插件本体的大部分功能都围绕着 `on_alconna` 响应器展开。 + +该响应器类似于 `on_command`,基于 `Alconna` 解析器来解析命令。 + +以下是一个简单的 `on_alconna` 响应器的例子: ```python -from nonebot_plugin_alconna import At, Image, on_alconna -from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand +from nonebot_plugin_alconna import At, Image, Match, on_alconna +from arclet.alconna import Args, Option, Alconna, MultiVar, Subcommand alc = Alconna( - ["/", "!"], "role-group", Subcommand( - "add", + "add|添加", Args["name", str], Option("member", Args["target", MultiVar(At)]), + dest="add", + compact=True, ), Option("list"), Option("icon", Args["icon", Image]) ) -rg = on_alconna(alc, auto_send_output=True) +rg = on_alconna(alc, use_command_start=True, aliases={"角色组"}) -@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("添加成功") +@rg.assign("list") +async def list_role_group(): + img: bytes = await gen_role_group_list_image() + await rg.finish(Image(raw=img)) + +@rg.assign("add") +async def _(name: str, target: Match[tuple[At, ...]]): + group = await create_role_group(name) + if target.available: + ats: tuple[At, ...] = target.result + group.extend(member.target for member in ats) + await rg.finish("添加成功") ``` -## 响应器使用 + -本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`: + +## 声明 + +`on_alconna` 的参数如下: ```python def on_alconna( command: Alconna | str, + rule: Rule | T_RuleChecker | None = None, skip_for_unmatch: bool = True, - auto_send_output: bool = False, - aliases: set[str | tuple[str, ...]] | None = None, + auto_send_output: bool | None = None, + 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, - ..., -): + use_origin: bool | None = None, + use_cmd_start: bool | None = None, + use_cmd_sep: bool | None = None, + response_self: bool | None = None, + **kwargs: Any, +) -> type[AlconnaMatcher]: + ... ``` - `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令 -- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应 -- `auto_send_output`: 是否自动发送输出信息并跳过响应 +- `rule`: 事件响应规则, 详见 [响应器规则](../../advanced/matcher.md#事件响应规则) +- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应, 默认为 `True` +- `auto_send_output`: 是否自动发送输出信息并跳过该响应。 + - `True`:自动发送输出信息并跳过该响应 + - `False`:不自动发送输出信息,而是传递进行处理 + - `None`:跟随全局配置项 `alconna_auto_send_output`,默认值为 `True` - `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases - `comp_config`: 补全会话配置, 不传入则不启用补全会话 - `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例 - `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id -- `use_origin`: 是否使用未经 to_me 等处理过的消息 -- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀 -- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符 +- `use_origin`: 是否使用未经 to_me 等处理过的消息。`None` 时跟随全局配置项 `alconna_use_origin`,默认值为 `False` +- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀。`None` 时跟随全局配置项 `alconna_use_command_start`,默认值为 `False` +- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符。`None` 时跟随全局配置项 `alconna_use_command_sep`,默认值为 `False` +- `response_self`: 是否响应自身消息。`None` 时跟随全局配置项 `alconna_response_self`,默认值为 `False` `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` +- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理 - `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` +- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换 - `.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("登录成功") -``` +除了标准的创建方式,本插件也提供了 `funcommand` 和 `Command` 两种快捷方式来创建 `AlconnaMatcher`, 详见 [快捷方式](./shortcut.md)。 ## 依赖注入 -本插件提供了一系列依赖注入函数,便于在响应函数中获取解析结果: +`AlconnaMatcher` 的特性之一是拓展了依赖注入的功能。 -- `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` 获取匹配的值 +- `CommandResult`: 用于快捷访问命令解析结果 + - `result (Arparma)`: 解析结果 + - `source (Alconna)`: 源命令 + - `matched (bool)`: 是否匹配 + - `context (dict)`: 命令的上下文 + - `output (str | None)`: 命令的输出 +- `Match`: 匹配项,表示参数是否存在于 `Arparma.all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值 + - `Match` 只能查找到 `Arparma.all_matched_args` 中的参数。对于特定选项/子命令的参数,需要使用 `Query` 来查询 - `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果 + - `Query` 除了查询参数,也可以查询某个选项/子命令是否存在 -**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效: +### 编写 ```python async def handle( @@ -141,13 +138,32 @@ async def handle( arp: Arparma, dup: Duplication, source: Alconna, - abc: str, # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler + ext: Extension, + exts: SelectedExtensions, + abc: str, foo: Match[str], - bar: Query[int] = Query("ttt.bar", 0) # Query 仍然需要一个默认值来传递 path 参数 + bar: Query[int] = Query("ttt.bar", 0) ): ... ``` +`AlconnaMatcher` 的依赖注入拓展支持以下情况: +- `xxx: CommandResult` +- `xxx: Arparma`:命令的[解析结果](./command.md#解析结果) +- `xxx: Duplication`:命令的解析结果的 [`Duplication`](./command.md#Duplication) +- `xxx: Alconna`:命令的源命令 +- `: Match[]`:上述的匹配项,使用 `key` 作为查询路径 +- `xxx: Query[] = Query(, default)`:上述的查询项,必需声明默认值以设置查询路径 `path` + - 当用来查询选项/子命令是否存在时,可不写 `Query[]` +- `xxx: Extension`:当前 `AlconnaMatcher` 使用的指定类型的匹配扩展 +- `xxx: SelectedExtensions`:当前 `AlconnaMatcher` 使用的所有可用的匹配扩展 +- `: `: 其他情况 + - 当 `key` 的名称是 "ctx" 或 "context" 并且类型为 `dict` 时,会注入命令的上下文 + - 当 `key` 存在于命令的上下文中时,会注入对应的值 + - 当 `key` 存在于 `Arparma` 的 `all_matched_args` 中时,会注入对应的值, 类似于 `Match` 的用法,但当该值不存在时将跳过响应器。 + - 当 `key` 属于 `got_path` 的参数时,会注入对应的值 + - 当 `key` 被某个 `Extension.before_catch` 确认为需要注入的参数时,会调用 `Extension.catch` 来注入对应的值 + :::note 如果你更喜欢 Depends 式的依赖注入,`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数,他们包括: @@ -162,19 +178,13 @@ async def handle( ::: -实例: +示例: ```python from nonebot import require require("nonebot_plugin_alconna") -from nonebot_plugin_alconna import ( - on_alconna, - Match, - Query, - AlconnaMatch, - AlcResult -) +from nonebot_plugin_alconna import AlconnaQuery, AlcResult, Match, Query, on_alconna from arclet.alconna import Alconna, Args, Option, Arparma @@ -183,8 +193,7 @@ test = on_alconna( "test", Option("foo", Args["bar", int]), Option("baz", Args["qux", bool, False]) - ), - auto_send_output=True + ) ) @test.handle() @@ -198,99 +207,100 @@ async def handle_test2(result: Arparma): await test.send(f"args: {result.all_matched_args}") @test.handle() -async def handle_test3(bar: Match[int] = AlconnaMatch("bar")): +async def handle_test3(bar: Match[int]): if bar.available: await test.send(f"foo={bar.result}") @test.handle() -async def handle_test4(qux: Query[bool] = Query("baz.qux", False)): +async def handle_test4(qux: Query[bool] = AlconnaQuery("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` 来控制一个具体的响应函数是否在不满足条件时跳过响应。 +### `assign` 方法 + +`AlconnaMatcher` 的 `assign` 方法与 `handle` 类似,但是可以控制响应函数是否在不满足条件时跳过响应。 + +`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): +def assign( + cls, + path: str, + value: Any = _seminal, + or_not: bool = False, + additional: CHECK | None = None, + parameterless: Iterable[Any] | None = None, +): ... ``` -此外,使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher: +- `path`: 指定的[查询路径](./command.md#路径查询) + - "$main" 表示没有任何选项/子命令匹配的时候 + - "\~XX" 时会把 "\~" 替换为父级路径 +- `value`: 可能的指定查询值 +- `or_not`: 是否同时处理没有查询成功的情况 +- `additional`: 额外的条件检查函数 + +例如: ```python -update_cmd = pip_cmd.dispatch("install.pak", "pip") +# 处理没有任何选项/子命令匹配的情况 +@rg.assign("$main") +async def handle_main(): ... -@update_cmd.handle() -async def update(arp: CommandResult): - ... +# 处理 list 选项 +@rg.assign("list") +async def handle_list(): ... + +# 处理 add 选项,且 name 为 admin +@rg.assign("add.name", "admin") +async def handle_add_admin(): ... ``` -另外,`AlconnaMatcher` 有类似于 `got` 的 `got_path`: + +### `dispatch` 方法 + +此外,使用 `.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher: + +```python +rg_list_cmd = rg.dispatch("list") + +@rg_list_cmd.handle() +async def handle_list(): ... +``` + +`dispatch` 的参数与 `assign` 相同。 + +当使用 `dispatch` 时,父级路径表示为传入 `dispatch` 的 `path`: + +```python +rg_add_cmd = rg.dispatch("add") + +# 此时 ~name 表示 add.name +@rg_add_cmd.assign("~name", "admin") +async def handle_add_admin(): ... +``` + +:::tip + +在 `dispatch` 下, `Query` 的 `path` 也同样支持 `~` 前缀来表示父级路径 + +```python +@rg_add_cmd.assign("~name", "admin") +async def handle_add_admin(target: Query[tuple[At, ...]] = Query("~target")): + if target.available: + await rg.send(f"添加成功: {target.result}") +``` + +::: + + +### `got_path` 方法 + +另外,`AlconnaMatcher` 有类似于 [`got`](../../appendices/session-control.mdx#got) 的 `got_path` 与配套的 `get_path_arg`, `set_path_arg`: ```python from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna @@ -312,95 +322,27 @@ async def tt(target: Union[str, At]): `got_path` 会获取消息的最后一个消息段并转为 path 对应的类型,例如示例中 `target` 对应的 Arg 里要求 str 或 At,则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。 -:::tip +`got_path` 中可以使用依赖注入函数 `AlconnaArg`, 类似于 [`Arg`](../../advanced/dependency.mdx#arg). -`path` 支持 ~XXX 语法,其会把 ~ 替换为可能的父级路径: + +### `prompt` 方法 + +基于 [`Waiter`](https://github.com/RF-Tar-Railt/nonebot-plugin-waiter) 插件,`AlconnaMatcher` 提供了 `prompt` 方法来实现更灵活的交互式提示。 ```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") +from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna - @pip_install_cmd.assign("~upgrade") - async def pip1_u(pak: Query[str] = Query("~pak")): - await pip_install_cmd.finish(f"pip upgrading {pak.result}...") -``` +test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]])) -::: - -## 响应器创建装饰 - -本插件提供了一个 `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() -) +@test_cmd.handle() +async def tt_h(target: Match[Union[str, At]]): + if target.available: + await test_cmd.finish(UniMessage(["ok\n", target])) + resp = await test_cmd.prompt("请输入目标", timeout=30) # 等待 30 秒 + if resp is None: + await test_cmd.finish("超时") + await test_cmd.finish(UniMessage(["ok\n", resp[-1]])) ``` ## 返回值中间件 @@ -411,9 +353,7 @@ book = ( from nonebot_plugin_alconna import image_fetch -mask_cmd = on_alconna( - Alconna("search", Args["img?", Image]), -) +mask_cmd = on_alconna(Alconna("search", Args["img?", Image])) @mask_cmd.handle() @@ -424,10 +364,105 @@ async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img" 其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。 +## i18n + +本插件基于 `tarina.lang` 模块提供了 i18n 的支持,参见 [Lang 用法](https://github.com/nonebot/plugin-alconna/discussions/50)。 + +当你编写完语言文件后,你便可以通过 `AlconnaMatcher.i18n` 来快速地将语言文件中的内容转为 UniMessage. + + + + +```yaml title="zh-CN.yml" +# 中文语言文件 +demo: + command: + role-group: + add: 添加 {name} 成功! +``` + + + + + + +```yaml title="en-US.yml" +# 英文语言文件 +demo: + command: + role-group: + add: Add {name} successfully! +``` + + + + + +```python title="使用 i18n" +@rg.assign("add") +async def handle_add(name: str): + await rg.i18n("demo", "command.role-group.add", name=name).finish() +``` + +## 匹配测试 + +`AlconnaMatcher.test` 方法允许你在 NoneBot 启动时对命令进行测试。 + +```python +def test( + cls, + message: str | UniMessage, + expected: dict[str, Any] | None = None, + prefix: bool = True +): ... +``` +- `message`: 测试的消息 +- `expected`: 预期的解析结果,若为 None 则表示只测试是否匹配 +- `prefix`: 是否使用命令前缀,默认为 True + ## 匹配拓展 本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为 +目前 `Extension` 的功能有: + +- `validate`: 对于事件的来源适配器或 bot 选择是否接受响应 +- `output_converter`: 输出信息的自定义转换方法 +- `message_provider`: 从传入事件中自定义提取消息的方法 +- `receive_provider`: 对传入的消息 (UniMessage) 的额外处理 +- `context_provider`: 对命令上下文的额外处理 +- `permission_check`: 命令对消息解析并确认头部匹配(即确认选择响应)时对发送者的权限判断 +- `parse_wrapper`: 对命令解析结果的额外处理 +- `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理 +- `before_catch`: 自定义依赖注入的绑定确认函数 +- `catch`: 自定义依赖注入处理函数 +- `post_init`: 响应器创建后对命令对象的额外处理 + +:::tip + +Extension 可以通过 `add_global_extension` 方法来全局添加。 + +```python +from nonebot_plugin_alconna import add_global_extension +from nonebot_plugin_alconna.builtins.extensions.telegram import TelegramSlashExtension + +add_global_extension(TelegramSlashExtension) +``` + +全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后,这部分响应器会被追加拓展) + +::: + 例如一个 `LLMExtension` 可以如下实现 (仅举例): ```python @@ -469,59 +504,121 @@ matcher = on_alconna( 那么添加了 `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 指令并注册,且将收到的指令交互事件转为指令供命令解析: +### validate ```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}") +def validate(self, bot: Bot, event: Event) -> bool: ... ``` -目前插件提供了 4 个内置的 `Extension`,它们在 `nonebot_plugin_alconna.builtins.extensions` 下: +默认情况下,`validate` 方法会筛选 `event.get_type()` 为 `message` 的情况,表示接受消息事件。 -- `ReplyRecordExtension`: 将消息事件中的回复暂存在 extension 中,使得解析用的消息不带回复信息,同时可以在后续的处理中获取回复信息。 -- `DiscordSlashExtension`: 将 Alconna 的命令自动转换为 Discord 的 Slash Command,并将 Slash Command 的交互事件转换为消息交给 Alconna 处理。 -- `MarkdownOutputExtension`: 将 Alconna 的自动输出转换为 Markdown 格式 -- `TelegramSlashExtension`: 将 Alconna 的命令注册在 Telegram 上以获得提示。 +### output_converter -:::tip +```python +async def output_converter(self, output_type: OutputType, content: str) -> UniMessage: ... +``` -全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后,这部分响应器会被追加拓展) +依据输出信息的类型,将字符串转换为消息对象以便发送。 + +其中 `OutputType` 为 "help", "shortcut", "completion", "error" 其中之一。 + +该方法只会调用一次,即对于多个 Extension,选择优先级靠前且实现了该方法的 Extension。 + +### message_provider + +```python +async def message_provider( + self, event: Event, state: T_State, bot: Bot, use_origin: bool = False +) -> UniMessage | None:... +``` + +该方法用于从事件中提取消息,默认情况下会使用 `event.get_message()` 来获取消息。 + +该方法可能会调用多次,即对于多个 Extension,选择优先级靠前且实现了该方法的 Extension,若调用的返回值不为 `None` 则作为结果。 + +:::caution + +该方法的默认实现对结果 (UniMessage) 会进行缓存。`Extension` 的实现也应尽量实现缓存机制。 ::: +### receive_provider + +```python +async def receive_provider(self, bot: Bot, event: Event, command: Alconna, receive: UniMessage) -> UniMessage: ... +``` + +该方法用于对传入的消息 (UniMessage) 进行额外处理,默认情况下会返回原始消息。 + +该方法会调用多次,即对于多个 Extension,前一个 Extension 的返回值会作为下一个 Extension 的输入。 + +### context_provider + +```python +async def context_provider(self, ctx: dict[str, Any], bot: Bot, event: Event, state: T_State) -> dict[str, Any]: +``` + +该方法用于提取命令上下文,默认情况下会返回 `ctx` 本身。 + +该方法会调用多次,即对于多个 Extension,前一个 Extension 的返回值会作为下一个 Extension 的输入。 + +### permission_check + +```python +async def permission_check(self, bot: Bot, event: Event, command: Alconna) -> bool: ... +``` + +该方法用于对发送者的权限进行检查,默认情况下会返回 `True`。 + +该方法可能会调用多次,即对于多个 Extension,若调用的返回值不为 `True` 则结束判断。 + +### parse_wrapper + +```python +async def parse_wrapper(self, bot: Bot, state: T_State, event: Event, res: Arparma) -> None: ... +``` + +该方法用于对命令解析结果进行额外处理。 + +该方法会调用多次,即对于多个 Extension,会并发地调用该方法。 + +### send_wrapper + +```python +async def send_wrapper(self, bot: Bot, event: Event, send: TMessage) -> TMessage: ... +``` + +该方法用于对 `AlconnaMatcher.send` 或 `UniMessage.send` 发送的消息 (str 或 Message 或 UniMessage) 进行额外处理,默认情况下会返回原始消息。 + +该方法会调用多次,即对于多个 Extension,前一个 Extension 的返回值会作为下一个 Extension 的输入。 + +由于需要保证输入与输出的类型一致,该方法内需要自行判断类型。 + +### before_catch + +```python +def before_catch(self, name: str, annotation: type, default: Any) -> bool: ... +``` + +该方法用于响应函数中某个参数是否需要绑定到该 Extension 上。 + +### catch + +```python +async def catch(self, interface: Interface) -> Any: ... +``` + +该方法用于注入经过 `before_catch` 确认的参数。其中 `Interface` 的定义为 + +```python +class Interface(Generic[TE]): + event: TE + state: T_State + name: str + annotation: Any + default: Any +``` + ## 补全会话 补全会话基于 [`半自动补全`](./command.md#半自动补全),用于指令参数缺失或参数错误时给予交互式提示,类似于 `got-reject`: @@ -578,30 +675,6 @@ class CompConfig(TypedDict): """禁用的指令""" lite: NotRequired[bool] """是否使用简洁版本的补全会话(相当于同时配置 disables、hides、hide_tabs)""" + block: NotRequired[bool] + """进行补全会话时是否阻塞响应器""" ``` - -## 内置插件 - -类似于 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/docs/best-practice/alconna/shortcut.md b/website/docs/best-practice/alconna/shortcut.md new file mode 100644 index 00000000..8eea0476 --- /dev/null +++ b/website/docs/best-practice/alconna/shortcut.md @@ -0,0 +1,120 @@ +--- +sidebar_position: 6 +description: 快捷方式 +--- + +# 快捷方式声明 + +针对 `Alconna` 编写对于入门开发者来说较为复杂的问题,本插件提供了一些快捷方式来简化开发者的工作。 + +## 装饰器构造器 + +本插件提供了一个 `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) + +``` + +相比于 `on_alconna`, `funcommand` 增加了三个参数 `name`, `prefixes` 和 `description`。 + +## 类 Koishi 构造器 + +本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中[注册命令](https://koishi.chat/zh-CN/guide/basic/command.html)的方式来构建一个 **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() +) +``` + +### 参数类型 + +`Command` 的参数类型也如 `koishi` 一样,**必选参数** 用尖括号包裹,**可选参数** 用方括号包裹: +- `foo` 表示参数 `foo`, 类型为 Any +- `foo:int` 表示参数 `foo`, 类型为 int +- `foo:int=1` 表示参数 `foo`, 类型为 int, 默认值为 1 +- `...foo` 表示[泛匹配参数](command.md#allparam) +- `foo:str+`, `foo:str*` 表示[变长参数](command.md#multivar-与-keywordvar) `foo`, 类型为 str +- `foo:+str`, `foo:text` 表示参数 `foo`, 类型为 str, 并且将包含空格 (即将变长参数的结果用空格合并) + +特别的,针对类型部分,本插件拓展了如下内容: +- `foo:At`, `foo:Image`, ... 表示类型为[通用消息段](./uniseg/segment.md) +- `foo:select(Image).first` 表示获取子元素类型 +- `foo:Dot(Image, 'url')` 表示类型为 `Image`,并且只获取 `url` 属性 + +### 从文件加载 + +`Command` 支持读取 `json` 或 `yaml` 文件来加载命令: + +```yml title="book.yml" +command: book +help: 测试 +options: + - name: writer + opt: "-w " + - name: writer + opt: "--anonymous" + default: + id: 1 +usage: book [-w | --anonymous] +shortcuts: + - key: 测试 + args: ["--anonymous"] +actions: + - + params: ["options"] + code: | + return str(options) +``` + +```python title="加载" +from nonebot_plugin_alconna import command_from_yaml + +book = command_from_yaml("book.yml") +``` diff --git a/website/docs/best-practice/alconna/uniseg.mdx b/website/docs/best-practice/alconna/uniseg.mdx deleted file mode 100644 index 8b7d5b6e..00000000 --- a/website/docs/best-practice/alconna/uniseg.mdx +++ /dev/null @@ -1,590 +0,0 @@ ---- -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/docs/best-practice/alconna/uniseg/README.md b/website/docs/best-practice/alconna/uniseg/README.md new file mode 100644 index 00000000..446ff338 --- /dev/null +++ b/website/docs/best-practice/alconna/uniseg/README.md @@ -0,0 +1,203 @@ +# 通用消息组件 + +`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件。 + +通用消息组件内容较多,故分为了一个示例以及数个专题。 + +## 示例 + +### 导入 + +一般情况下,你只需要从 `nonebot_plugin_alconna.uniseg` 中导入 `UniMessage` 即可: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage +``` + +### 构建 + +你可以通过 `UniMessage` 上的快捷方法来链式构造消息: + +```python +message = ( + UniMessage.text("hello world") + .at("1234567890") + .image(url="https://example.com/image.png") +) +``` + +也可以通过导入通用消息段来构建消息: + +```python +from nonebot_plugin_alconna import Text, At, Image, UniMessage + +message = UniMessage( + [ + Text("hello world"), + At("user", "1234567890"), + Image(url="https://example.com/image.png"), + ] +) +``` + +更深入一点,比如你想要发送一条包含多个按钮的消息,你可以这样做: + +```python +from nonebot_plugin_alconna import Button, UniMessage + +message = ( + UniMessage.text("hello world") + .keyboard( + Button("link1", url="https://example.com/1"), + Button("link2", url="https://example.com/2"), + Button("link3", url="https://example.com/3"), + row=3, + ) +) +``` + +### 发送 + +你可以通过 `.send` 方法来发送消息: + +```python +@matcher.handle() +async def _(): + message = UniMessage.text("hello world").image(url="https://example.com/image.png") + await message.send() + # 类似于 `matcher.finish` + await message.finish() +``` + +你可以通过参数来让消息 @ 发送者: + +```python +@matcher.handle() +async def _(): + message = UniMessage.text("hello world").image(url="https://example.com/image.png") + await message.send(at_sender=True) +``` + +或者回复消息: + +```python +@matcher.handle() +async def _(): + message = UniMessage.text("hello world").image(url="https://example.com/image.png") + await message.send(reply_to=True) +``` + +### 撤回,编辑,表态 + +你可以通过 `message_recall`, `message_edit` 和 `message_reaction` 方法来撤回,编辑和表态消息事件。 + +```python +from nonebot_plugin_alconna import message_recall, message_edit, message_reaction + +@matcher.handle() +async def _(): + await message_edit(UniMessage.text("hello world")) + await message_reaction("👍") + await message_recall() +``` + +你也可以对你自己发送的消息进行撤回,编辑和表态: + +```python +@matcher.handle() +async def _(): + message = UniMessage.text("hello world").image(url="https://example.com/image.png") + receipt = await message.send() + await receipt.edit(UniMessage.text("hello world!")) + await receipt.reaction("👍") + await receipt.recall(delay=5) # 5秒后撤回 +``` + +### 处理消息 + +通过依赖注入,你可以在事件处理器中获取通用消息: + +```python +from nonebot_plugin_alconna import UniMsg + +@matcher.handle() +async def _(msg: UniMsg): + ... +``` + +然后你可以通过 `UniMessage` 的方法来处理消息. + +例如,你想知道消息中是否包含图片,你可以这样做: + +```python +ans1 = Image in message +ans2 = message.has(Image) +ans3 = message.only(Image) +``` + +或者,提取所有的图片: + +```python +imgs_1 = message[Image] +imgs_2 = message.get(Image) +imgs_3 = message.include(Image) +imgs_4 = message.select(Image) +imgs_5 = message.filter(lambda x: x.type == "image") +imgs_6 = message.tranform({"image": True}) +``` + +而后,如果你想提取出所有的图片链接,你可以这样做: + +```python +urls = imgs.map(lambda x: x.url) +``` + +如果你想知道消息是否符合某个前缀,你可以这样做: + +```python +@matcher.handle() +async def _(msg: UniMsg): + if msg.startswith("hello"): + await matcher.finish("hello world") + else: + await matcher.finish("not hello world") +``` + +或者你想接着去除掉前缀: + +```python +@matcher.handle() +async def _(msg: UniMsg): + if msg.startswith("hello"): + msg = msg.removeprefix("hello") + await matcher.finish(msg) + else: + await matcher.finish("not hello world") +``` + +### 持久化 + +假设你在编写一个词库查询插件,你可以通过 `UniMessage.dump` 方法来将消息序列化为 JSON 格式: + +```python +from nonebot_plugin_alconna import UniMsg + +@matcher.handle() +async def _(msg: UniMsg): + data: list[dict] = msg.dump() + # 你可以将 data 存储到数据库或者 JSON 文件中 +``` + +而后你可以通过 `UniMessage.load` 方法来将 JSON 格式的消息反序列化为 `UniMessage` 对象: + +```python +from nonebot_plugin_alconna import UniMessage + +@matcher.handle() +async def _(): + data = [ + {"type": "text", "text": "hello world"}, + {"type": "image", "url": "https://example.com/image.png"}, + ] + message = UniMessage.load(data) +``` diff --git a/website/docs/best-practice/alconna/uniseg/_category_.json b/website/docs/best-practice/alconna/uniseg/_category_.json new file mode 100644 index 00000000..da606ee9 --- /dev/null +++ b/website/docs/best-practice/alconna/uniseg/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "通用消息组件", + "position": 5 +} diff --git a/website/docs/best-practice/alconna/uniseg/message.mdx b/website/docs/best-practice/alconna/uniseg/message.mdx new file mode 100644 index 00000000..45391534 --- /dev/null +++ b/website/docs/best-practice/alconna/uniseg/message.mdx @@ -0,0 +1,516 @@ +--- +sidebar_position: 3 +description: 消息序列 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# 通用消息序列 + +`uniseg` 提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为[通用消息段](./segment.md)。 + +你可以用如下方式获取 `UniMessage`: + + + + +通过提供的 `UniversalMessage` 或基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832)的 `UniMsg` 依赖注入器来获取 `UniMessage`。 + +```python +from nonebot_plugin_alconna.uniseg import UniMsg, At, Text + + +matcher = on_xxx(...) + +@matcher.handle() +async def _(msg: UniMsg): + text = msg[Text, 0] + print(text.text) + 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`, `.finish` 方法基于 `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) +``` + +`UniMessage.send` 的定义如下: + +```python +async def send( + self, + target: Event | Target | None = None, + bot: Bot | None = None, + fallback: bool | FallbackStrategy = FallbackStrategy.rollback, + at_sender: str | bool = False, + reply_to: str | bool | Reply | None = False, + **kwargs: Any, +) -> Receipt: + ... +``` +- `target`: 发送目标,支持事件和[发送对象](./utils.mdx#发送对象),不传入时会尝试从响应器上下文中获取。 +- `bot`: 发送消息使用的 Bot 对象,若不传入则会尝试从响应器上下文中获取。 +- `fallback`: [回退策略](#回退策略)。 +- `at_sender`: 是否提醒发送者,默认为 `False`。当类型为 `str` 时,表示指定用户的 id。 +- `reply_to`: 是否回复消息,默认为 `False`。 + - `str` 表示消息 id。 + - `bool` 表示是否回复当前消息。此时 `target` 不能是[发送对象](./utils.mdx#发送对象)。 + - `Reply` 表示直接使用回复元素。 +- `**kwargs`: 各 `Bot.send` 的特定参数。 + +而在 `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."])) +``` + +### 回退策略 + +`send` 方法的 `fallback` 参数用于指定回退策略(即当前适配器不支持的消息段如何处理): +- `FallbackStrategy.ignore`: 忽略未转换的消息段 +- `FallbackStrategy.to_text`: 将未转换的消息段转为文本元素 +- `FallbackStrategy.rollback`: 从未转换消息段的子元素中提取可能的可发送消息段 +- `FallbackStrategy.forbid`: 抛出异常 +- `FallbackStrategy.auto`: 插件自动选择策略 + +另外 `fallback` 传入 `bool` 时,`True` 等价于 `FallbackStrategy.auto`,`False` 等价于 `FallbackStrategy.forbid`。 + +### 主动发送消息 + +`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) +``` + +:::caution + +在响应器以外的地方,除非启用了 `alconna_apply_fetch_targets` 配置项,否则 `bot` 参数必须手动传入。 + +::: + +### Receipt 对象 + +`send` 方法返回的 `Receipt` 对象可以用于修改/撤回/表态消息: + +```python +async def handle(): + receipt = await UniMessage.text("hello!").send(at_sender=True, reply_to=True) + await receipt.recall(delay=1) + recept1 = await UniMessage.text("hello!").send(at_sender=True, reply_to=True) + await recept1.edit("world!") +``` + +`Receipt` 对象拥有以下方法: +- `recallable`: 表明是否可以撤回 +- `recall`: 撤回消息 +- `editable`: 表明是否可以修改 +- `edit`: 修改消息 +- `reactionable`: 表明是否可以表态 +- `reaction`: 表态消息 +- `get_reply`: 生成对已经发送的消息的回复元素 +- `send`, `finish`: 发送消息 +- `reply`: 回复已经发送的消息 + + +## 构造 + +如同 `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")] +) +``` + +### 使用消息模板 + +`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` 两个特殊值。 + +:::tip + +注意到上述代码中的 `{target}` 了吗? + +在 `AlconnaMatcher` 中,`UniMessage.template` 的格式化方法会自动将 `Arparma.all_matched_args`、 `state` 中的变量传入到 `format` 方法中,因此你可以直接使用上述变量。 + +::: + + +### 拼接消息 + +`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")]) +``` + +## 操作 + +### 检查消息段 + +我们可以通过 `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 +# 提取消息纯文本字符串 +assert UniMessage( + [At("user", "1234"), "text"] +).extract_plain_text() == "text" +``` + +### 遍历 + +通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段: + +```python +for segment in message: # type: Segment + ... +``` + +### 过滤、索引与切片 + +消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片: + +```python +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) +``` + +或者使用 `filter` 方法: + +```python +message.filter(lambda x: isinstance(x, At) and x.flag == "user") # 仅保留 At("user", xxx) 的消息段 +``` + +同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段: + +```python +# 指定类型首个消息段索引 +message.index(Text) == 1 +# 指定类型消息段数量 +message.count(Text) == 2 +``` + +此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段: + +```python +# 获取指定类型指定个数的消息段 +message.get(Text, 1) == UniMessage([Text("test1")]) +``` + +### 嵌套提取 + +消息序列的 `select` 方法可以递归地从消息中选择指定类型的消息段: + +```python +message = UniMessage( + [ + Text("text1"), + Image(url="url1")( + Text("text2"), + ) + ] +) +assert message.select(Text) == UniMessage( + [ + Text("text1"), + Text("text2") + ] +) +``` + +### 转换 + +消息序列的 `map` 方法可以简单地将消息段转换为指定类型的数据: + +```python +# 转换消息段为另一类型的消息段,此时返回结果仍是 UniMessage +message.map(lambda x: Text(x.target)) # 转换为 Text 消息段 +# 转换消息段为另一类型的数据,此时返回结果为 list[T] +message.map(lambda x: x.target) # 转换为 list[str] +``` + +在此之上,消息序列还提供了 `transform` 和 `transform_async` 方法,允许你传入转换规则,将消息段转换为另一类型的消息段,并返回一个新的消息序列: + +```python +rule = { + "text": True, + "at": lambda attrs, children: Text(attrs["target"]) +} +message.transform(rule) +``` + +转换规则的类型一般为 `dict[str, Transformer]`,以消息元素类型的名称为键,定义方式如下: + +```typescript +type Fragment = Segment | Segment[] +type Render = (attrs: dict, children: Segment[]) => T +type Transformer = boolean | Fragment | Render +``` + + +### 字符串操作 + +类似于 `str`,消息序列可以通过如下方法来操作消息内的文本部分: +- `split`, +- `replace`, +- `startwith`, `endswith`, +- `removeprefix`, `removesuffix`, +- `strip`, `lstrip`, `rstrip`, + +```python +msg = UniMessage.text("foo bar").at("1234").text("baz qux") +# 分割,返回分割结果,类型为 list[UniMessage] +parts = msg.split(" ") +# 替换,返回替换结果,类型为 UniMessage。新文本可以用 str 或 Text 来替换 +new_msg = msg.replace("ba", "baaa") +# 前缀/后缀检查 +msg.startswith("foo") # True +msg.endswith("qux") # True +# 去除前缀/后缀 +msg1 = msg.removeprefix("foo") # UniMessage([Text(" bar"), At("user", "1234"), Text("baz qux")]) +msg2 = msg.removesuffix("qux") # UniMessage([Text("foo bar"), At("user", "1234"), Text("baz ")]) +# 去除空格 +msg1 = msg1.lstrip() # UniMessage([Text("bar"), At("user", "1234"), Text("baz qux")]) +msg2 = msg2.rstrip() # UniMessage([Text("foo bar"), At("user", "1234"), Text("baz")]) +``` + +## 持久化 + +特别的,`UniMessage` 还支持消息持久化,具体来说为 `dump` 与 `load` 方法: + +```python +msg = UniMessage.text("Hello").image(url="url") +data = msg.dump() # [{"type": "text", "text": "Hello"}, {"type": "image", "url": "url"}] + +assert UniMessage.load(data) == msg +``` + +### dump + +`dump` 方法的定义如下: + +```python +def dump(self, media_save_dir: str | Path | bool | None = None, json: bool = False) -> str | list[dict[str, Any]]: ... +``` + +其中,`media_save_dir` 用于指定持久化的媒体文件存储目录: +- 若 `media_save_dir` 为 str 或 Path,则会将媒体文件保存到指定目录下。 +- 若 `media_save_dir` 为 False,则不会保存媒体文件。 +- 若 `media_save_dir` 为 True,则会将文件数据转为 base64 编码。 +- 若不指定 `media_save_dir`,则会尝试导入 [`nonebot_plugin_localstore`](../../data-storing.md) 并使用其提供的路径。否则 (即 `localstore` 未安装),将会尝试使用当前工作目录。 + +### load + +`load` 方法的定义如下: + +```python +@classmethod +def load(cls, data: str | list[dict[str, Any]]) -> UniMessage: ... +``` + +其中 `data` 应符合 JSON 格式。 diff --git a/website/docs/best-practice/alconna/uniseg/segment.md b/website/docs/best-practice/alconna/uniseg/segment.md new file mode 100644 index 00000000..1b64d748 --- /dev/null +++ b/website/docs/best-practice/alconna/uniseg/segment.md @@ -0,0 +1,222 @@ +--- +sidebar_position: 2 +description: 消息段 +--- + +# 通用消息段 + +通用消息段是对各适配器中的消息段的抽象总结。其可用于 Alconna 命令的参数定义,也可用于消息的构建和解析。 + +```python +from nonebot_plugin_alconna import Alconna, Args, Image, on_alconna + +meme = on_alconna(Alconna("make_meme", Args["name", str]["img", Image])) + +@meme.handle() +async def _(img: Image): + ... +``` + +## 模型定义 + +> **注意**: 本节的内容经过简化。实际情况以源码为准。 + +```python +class Segment: + """基类标注""" + @property + def type(self) -> str: ... + @property + def data(self) -> [str, Any]: ... + @property + def children(self) -> list["Segment"]: ... + +class Text(Segment): + """Text对象, 表示一类文本元素""" + text: str + styles: dict[tuple[int, int], list[str]] + + def cover(self, text: str): ... + def mark(self, start: Optional[int] = None, end: Optional[int] = None, *styles: 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): + id: Optional[str] + url: 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对象, 表示一类图片元素""" + width: Optional[int] + height: Optional[int] + +class Audio(Media): + """Audio对象, 表示一类音频元素""" + duration: Optional[float] + +class Voice(Media): + """Voice对象, 表示一类语音元素""" + duration: Optional[float] + +class Video(Media): + """Video对象, 表示一类视频元素""" + thumbnail: Optional[Image] + duration: Optional[float] + +class File(Media): + """File对象, 表示一类文件元素""" + +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 Reference(Segment): + """Reference对象,表示一类引用消息。转发消息 (Forward) 也属于此类""" + id: Optional[str] + nodes: Sequence[Union[RefNode, CustomNode]] + +class Button(Segment): + """Button对象,表示一类按钮消息""" + flag: Literal["action", "link", "input", "enter"] + """ + - 点击 action 类型的按钮时会触发一个关于 按钮回调 事件,该事件的 button 资源会包含上述 id + - 点击 link 类型的按钮时会打开一个链接或者小程序,该链接的地址为 `url` + - 点击 input 类型的按钮时会在用户的输入框中填充 `text` + - 点击 enter 类型的按钮时会直接发送 `text` + """ + label: Union[str, Text] + """按钮上的文字""" + clicked_label: Optional[str] + """点击后按钮上的文字""" + id: Optional[str] + url: Optional[str] + text: Optional[str] + style: Optional[str] + """ + 仅建议使用下列值:primary, secondary, success, warning, danger, info, link, grey, blue + + 此处规定 `grey` 与 `secondary` 等同, `blue` 与 `primary` 等同 + """ + permission: Union[Literal["admin", "all"], list[At]] = "all" + """ + - admin: 仅管理者可操作 + - all: 所有人可操作 + - list[At]: 指定用户/身份组可操作 + """ + +class Keyboard(Segment): + """Keyboard对象,表示一行按钮元素""" + id: Optional[str] + """此处一般用来表示模板id,特殊情况下可能表示例如 bot_appid 等""" + buttons: Optional[list[Button]] + row: Optional[int] + """当消息中只写有一个 Keyboard 时可根据此参数约定按钮组的列数""" + +class Other(Segment): + """其他 Segment""" + origin: MessageSegment + +class I18n(Segment): + """特殊的 Segment,用于 i18n 消息""" + item_or_scope: Union[LangItem, str] + type_: Optional[str] = None + + def tp(self) -> UniMessageTemplate: ... +``` + +:::tip + +或许你注意到了 `Segment` 上有一个 `children` 属性。 + +这是因为在 [`Satori`](https://satori.js.org/zh-CN/) 协议的规定下,一类元素可以用其子元素来代表一类兼容性消息 +(例如,qq 的商场表情在某些平台上可以用图片代替)。 + +为此,本插件提供了 `select` 方法来表达 "命令中获取子元素" 的方法: + +```python +from nonebot_plugin_alconna import Args, Image, Alconna, select +from nonebot_plugin_alconna.builtins.uniseg.market_face import MarketFace + +# 表示这个指令需要的图片会在目标元素下进行搜索,将所有符合 Image 的元素选出来并将第一个作为结果 +alc1 = Alconna("make_meme", Args["name", str]["img", select(Image).first]) # 也可以使用 select(Image).nth(0) + +# 表示这个指令需要的图片要么直接是 Image 要么是在 MarketFace 元素内的 Image +alc2 = Alconna("make_meme", Args["name", str]["img", [Image, select(Image).from_(MarketFace)]]) +``` + +也可以参考通用消息的 [`嵌套提取`](./message.mdx#嵌套提取) + +::: + +## 自定义消息段 + +`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/docs/best-practice/alconna/uniseg/utils.mdx b/website/docs/best-practice/alconna/uniseg/utils.mdx new file mode 100644 index 00000000..3feb6bca --- /dev/null +++ b/website/docs/best-practice/alconna/uniseg/utils.mdx @@ -0,0 +1,283 @@ +--- +sidebar_position: 4 +description: 辅助模型 +--- + +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# 辅助功能 + +`uniseg` 模块同时提供了多种方法以通用消息操作。 + +:::note + +这些方法中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。 + +::: + + +## 消息事件 ID + +消息事件 ID 是用来标识当前消息事件的唯一 ID,通常用于回复/撤回/编辑/表态当前消息。 + + + + +通过提供的 `MessageId` 或 `MsgId` 依赖注入器来获取消息事件 id。 + +```python +from nonebot_plugin_alconna.uniseg import MsgId + + +matcher = on_xxx(...) + +@matcher.handle() +asycn def _(msg_id: MsgId): + ... +``` + + + + +```python +from nonebot import Event, Bot +from nonebot_plugin_alconna.uniseg import get_message_id + + +matcher = on_xxx(...) + +@matcher.handle() +asycn def _(bot: Bot, event: Event): + msg_id: str = get_message_id(event, bot) + +``` + + + + +:::caution + +该方法获取的消息事件 ID 不推荐直接用于各适配器的 API 调用中,可能会操作失败。 + +::: + +## 发送对象 + +消息发送对象是用来描述当前消息事件的可发送对象或者主动发送消息时的目标对象,它包含了以下属性: + +```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: str | None + """机器人id,若为 None 则 Bot 对象会随机选择""" + selector: Callable[[Bot], Awaitable[bool]] | None + """选择器,用于在多个 Bot 对象中选择特定 Bot""" + extra: dict[str, Any] + """额外信息,用于适配器扩展""" +``` + + + + +通过提供的 `MessageTarget` 或 `MsgTarget` 依赖注入器来获取消息发送对象。 + +```python +from nonebot_plugin_alconna.uniseg import MsgTarget + + +matcher = on_xxx(...) + +@matcher.handle() +asycn def _(target: MsgTarget): + ... +``` + + + + +```python +from nonebot import Event, Bot +from nonebot_plugin_alconna.uniseg import Target, get_target + + +matcher = on_xxx(...) + +@matcher.handle() +asycn def _(bot: Bot, event: Event): + target: Target = get_target(event, bot) + +``` + + + + +主动构造一个发送对象时,则需要如下参数: + +- `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) + # 主动发送消息给群号为 12345 的 QQ 群聊 + target1 = Target("12345", scope=SupportScope.qq_client) + await UniMessage("Hello!").send(target=target1) +``` + +### 选择器 + +一般来说,主动发送消息时,`UniMessage.send` 或 `Target.self_id` 应指定一个 `Bot` 对象。但是这样会加重开发者的负担。 + +因此,我们提供了选择器来帮助开发者选择一个 `Bot` 对象。当然,这并非说明一定需要传入 `selector` 参数。 + +事实上,构造 `Target` 对象时,`self_id`, `scope`, `adapter` 和 `platform` 都会参与到 `selector` 的构造中。 + +::tip + +你其实可以使用 `Target` 来帮你筛选 `Bot` 对象: + +```python +async def _(): + target = Target("12345", scope=SupportScope.qq_client) + bot = await target.select() +``` + +::: + +若配置了 [`alconna_apply_fetch_targets`](../config.md#alconna_apply_fetch_targets) 选项,则在启动时会主动拉取一次发送对象列表。即对于 +某一主动构造的 `Target` 对象,插件将其与拉取下来的众多发送对象进行匹配,并选择第一个符合条件的发送对象,以选择对应的 Bot 对象。 + + +## 撤回消息 + +通过 `message_recall` 方法来撤回消息事件。 + +```python +from nonebot_plugin_alconna.uniseg import message_recall + +matcher = on_xxx(...) + +@matcher.handle() +async def _(msg_id: MsgId): + await message_recall(msg_id) +``` + +`message_recall` 方法的参数如下: + +```python +async def message_recall( + message_id: str | None = None, + event: Event | None = None, + bot: Bot | None = None, + adapter: str | None = None +): ... +``` + +当 `message_id` 为 `None` 时,插件会尝试从 `event` 中获取消息事件 ID。 + +## 编辑消息 + +通过 `message_edit` 方法来编辑消息事件。 + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, message_edit + +matcher = on_xxx(...) + +@matcher.handle() +async def _(): + await message_edit(UniMessage.text("1234")) +``` + +`message_edit` 方法的参数如下: + +```python +async def message_edit( + msg: UniMessage, + message_id: str | None = None, + event: Event | None = None, + bot: Bot | None = None, + adapter: str | None = None, +): ... +``` + +当 `message_id` 为 `None` 时,插件会尝试从 `event` 中获取消息事件 ID。 + +## 表态消息 + +:::caution + +该方法属于实验性功能。其接口可能会在未来的版本中发生变化。 + +::: + +通过 `message_reaction` 方法来表态消息事件。 + +```python +from nonebot_plugin_alconna.uniseg import message_reaction + +matcher = on_xxx(...) + +@matcher.handle() +async def _(): + await message_reaction("👍") +``` + +`message_reaction` 方法的参数如下: + +```python +async def message_reaction( + reaction: str | Emoji, + message_id: str | None = None, + event: Event | None = None, + bot: Bot | None = None, + adapter: str | None = None, + delete: bool = False, +): ... +``` + +当 `message_id` 为 `None` 时,插件会尝试从 `event` 中获取消息事件 ID。 + +`delete` 参数表示是否删除**自己的**表态消息,默认为 `False`。 + +## 响应规则 + +`uniseg` 模块提供了两个响应规则: +- `at_in`: 是否在消息中 @ 了指定的用户 +- `at_me`: 是否在消息中 @ 了机器人 + +相较于 NoneBot 内置的 `to_me` 规则,`at_me` 规则只会在消息中 @ 机器人时触发。 + +```python +from nonebot_plugin_alconna.uniseg import at_me + +matcher = on_xxx(..., rule=at_me()) +``` diff --git a/website/docs/best-practice/multi-adapter.mdx b/website/docs/best-practice/multi-adapter.mdx index 5afb42ad..af45fbb3 100644 --- a/website/docs/best-practice/multi-adapter.mdx +++ b/website/docs/best-practice/multi-adapter.mdx @@ -8,13 +8,15 @@ description: 插件跨平台支持 import Tabs from "@theme/Tabs"; import TabItem from "@theme/TabItem"; +## 使用 NoneBot 本身 + 由于不同平台的事件与接口之间,存在着极大的差异性,NoneBot 通过[重载](../appendices/overload.md)的方式,使得插件可以在不同平台上正确响应。但为了减少跨平台的兼容性问题,我们应该尽可能的使用基类方法实现原生跨平台,而不是使用特定平台的方法。当基类方法无法满足需求时,我们可以使用依赖注入的方式,将特定平台的事件或机器人注入到事件处理函数中,实现针对特定平台的处理。 :::tip 提示 如果需要在多平台上**使用**跨平台插件,首先应该根据[注册适配器](../advanced/adapter.md#注册适配器)一节,为机器人注册各平台对应的适配器。 ::: -## 基于基类的跨平台 +### 基于基类的跨平台 在[事件通用信息](../advanced/adapter.md#获取事件通用信息)中,我们了解了事件基类能够提供的通用信息。同时,[事件响应器操作](../appendices/session-control.mdx#更多事件响应器操作)也为我们提供了基本的用户交互方式。使用这些方法,可以让我们的插件运行在任何平台上。例如,一个简单的命令处理插件: @@ -34,11 +36,11 @@ async def handle_function(): 由于此插件仅使用了事件通用信息和事件响应器操作的纯文本交互方式,这些方法不使用特定平台的信息或接口,因此是原生跨平台的,并不需要额外处理。但在一些较为复杂的需求下,例如发送图片消息时,并非所有平台都具有统一的接口,因此基类便无能为力,我们需要引入特定平台的适配器了。 -## 基于重载的跨平台 +### 基于重载的跨平台 重载是 NoneBot 跨平台操作的核心,在[事件类型与重载](../appendices/overload.md#重载)一节中,我们初步了解了如何通过类型注解来实现针对不同平台事件的处理方式。在[依赖注入](../advanced/dependency.mdx)一节中,我们又对依赖注入的使用方法进行了详细的介绍。结合这两节内容,我们可以实现更复杂的跨平台操作。 -### 处理近似事件 +#### 处理近似事件 对于一系列**差异不大**的事件,我们往往具有相同的处理逻辑。这时,我们不希望将相同的逻辑编写两遍,而应该复用代码,以实现在同一个事件处理函数中处理多个近似事件。我们可以使用[事件重载](../advanced/dependency.mdx#Event)的特性来实现这一功能。例如: @@ -81,7 +83,7 @@ async def handle_function(event: Union[OnebotV11MessageEvent, OnebotV12MessageEv -### 在依赖注入中使用重载 +#### 在依赖注入中使用重载 NoneBot 依赖注入系统提供了自定义子依赖的方法,子依赖的类型同样会影响到事件处理函数的重载行为。例如: @@ -104,7 +106,7 @@ async def handle_function(time: datetime = Depends(get_event_time)): 示例中 ,我们为 `handle_function` 事件处理函数注入了自定义的 `get_event_time` 子依赖,而此子依赖注入参数为 Console 适配器的 `MessageEvent`。因此 `handle_function` 仅会响应 Console 适配器的 `MessageEvent` ,而不能响应其他事件。 -### 处理多平台事件 +#### 处理多平台事件 不同平台的事件之间,往往存在着极大的差异性。为了满足我们插件的跨平台运行,通常我们需要抽离业务逻辑,以保证代码的复用性。一个合理的做法是,在事件响应器的处理流程中,首先先针对不同平台的事件分别进行处理,提取出核心业务逻辑所需要的信息;然后再将这些信息传递给业务逻辑处理函数;最后将业务逻辑的输出以各平台合适的方式返回给用户。也就是说,与平台绑定的处理部分应该与平台无关部分尽量分离。例如: @@ -178,6 +180,26 @@ async def handle_onebot_reply(bot: OnebotBot, state: T_State, location: str = Ar 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),可以帮助你更好地处理跨平台功能,包括事件处理和消息发送等。 -::: +## 使用插件 + +得益于众多开发者为 NoneBot 社区做出的贡献,我们可以通过一系列插件来完成跨平台插件的开发。 + +这些插件可以分为三类: + +### 事件处理 +- [all4one](https://github.com/nonepkg/nonebot-plugin-all4one): 将不同平台的事件转为符合 OneBot V12 协议的插件 + - 支持的适配器: OneBot V11/V12, Discord, QQ, Telegram + +### 消息处理 +- [alconna](https://github.com/nonebot/plugin-alconna): 对几乎所有适配器中消息的收发、撤回、编辑、表态的统一插件 + - 支持的适配器: OneBot V11/V12, Telegram, Feishu, Github, QQ, Ding, Console, Kaiheila, Mirai, NtChat, Minecraft, Discord, Satori, Red, Dodo, Kritor, Tailchat, Mail, WXMP, Heybox, Gewechat +- [send-anything-anywhere](https://github.com/felinae98/nonebot-plugin-send-anything-anywhere): 帮助处理不同适配器消息的适配和发送的插件 + - 支持的适配器: OneBot V11/V12, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord + +### 会话信息提取 +- [uninfo](https://github.com/RF-Tar-Railt/nonebot-plugin-uninfo): 多平台的会话信息(用户、群组、频道)获取插件 + - 支持的适配器: OneBot V11/V12, Telegram, Feishu, QQ, Console, Kaiheila, Mirai, Minecraft, Discord, Satori, Dodo, Kritor, Mail, WXMP, Gewechat +- [session](https://github.com/noneplugin/nonebot-plugin-session): 会话信息提取与会话 id 定义插件 + - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord +- [userinfo](https://github.com/noneplugin/nonebot-plugin-userinfo: 用户信息获取插件 + - 支持的适配器: OneBot V11/V12, Console, Kaiheila, Telegram, Feishu, Red, DoDo, Satori, QQ, Discord