diff --git a/website/docs/advanced/matcher.md b/website/docs/advanced/matcher.md index 3d64131b..80d7d89f 100644 --- a/website/docs/advanced/matcher.md +++ b/website/docs/advanced/matcher.md @@ -333,4 +333,6 @@ matcher2 = group.on_message() 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 +该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 + 详情请阅读最佳实践中的 [命令解析拓展](../best-practice/alconna/README.mdx) 章节。 diff --git a/website/docs/best-practice/alconna/README.mdx b/website/docs/best-practice/alconna/README.mdx index 9b8805eb..caf195b4 100644 --- a/website/docs/best-practice/alconna/README.mdx +++ b/website/docs/best-practice/alconna/README.mdx @@ -15,15 +15,18 @@ slug: /best-practice/alconna/ 同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult` -该插件还可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应: +该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如: -- `pip.handle([Check(assign("add.name", "nb"))])` 表示仅在命令为 `role-group add` 并且 name 为 `nb` 时响应 -- `pip.handle([Check(assign("list"))])` 表示仅在命令为 `role-group list` 时响应 -- `pip.handle([Check(assign("add"))])` 表示仅在命令为 `role-group add` 时响应 +- `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数 +- `dispatch`:类似 `CommandGroup`,对目标路径创建一个新的 `AlconnaMatcher`,并将解析结果分配给该 `AlconnaMatcher` +- `got_path`:类似 `got`,但是可以指定目标路径,并且能够验证解析结果是否可用 +- ... 基于 `Alconna` 的特性,该插件同时提供了一系列便捷的消息段标注。 标注可用于在 `Alconna` 中匹配消息中除 text 外的其他消息段,也可用于快速创建各适配器下的消息段。所有标注位于 `nonebot_plugin_alconna.adapters` 中。 +该插件同时通过提供 `UniMessage` (通用消息模型) 实现了**跨平台接收和发送消息**的功能。 + ## 安装插件 在使用前请先安装 `nonebot-plugin-alconna` 插件至项目环境中,可参考[获取商店插件](../../tutorial/store.mdx#安装插件)来了解并选择安装插件的方式。如: @@ -83,32 +86,35 @@ async def got_location(location: str = ArgPlainText()): -```python {5-8,11-13,15-16} +```python {5-10,14-16,18-19} from nonebot.rule import to_me from arclet.alconna import Alconna, Args -from nonebot_plugin_alconna import Match, AlconnaMatch, on_alconna, AlconnaMatcher, AlconnaArg +from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna weather = on_alconna( - Alconna(["天气", "weather", "天气预报"], Args["location?", str]), + Alconna("天气", Args["location?", str]), rule=to_me(), ) +weather.shortcut("weather", {"command": "天气"}) +weather.shortcut("天气预报", {"command": "天气"}) + @weather.handle() -async def handle_function(matcher: AlconnaMatcher, location: Match[str] = AlconnaMatch("location")): +async def handle_function(matcher: AlconnaMatcher, location: Match[str]): if location.available: - matcher.set_path_arg("location", location.value) + matcher.set_path_arg("location", location.result) @weather.got_path("location", prompt="请输入地名") -async def got_location(location: str = AlconnaArg("location")): +async def got_location(location: str): if location not in ["北京", "上海", "广州", "深圳"]: await weather.reject(f"你想查询的城市 {location} 暂不支持,请重新输入!") await weather.finish(f"今天{location}的天气是...") ``` -在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `AlconnaMatch` 来获取解析结果。 - +在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `Match` 来获取解析结果。 +command 关于更多 `Alconna` 的使用方法,可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna), -或阅读 [Alconna 基本介绍](./alconna.md) 一节。 +或阅读 [Alconna 基本介绍](./command.md) 一节。 关于更多 `on_alconna` 的使用方法,可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md), 或阅读 [响应规则的使用](./matcher.md) 一节。 diff --git a/website/docs/best-practice/alconna/alconna.md b/website/docs/best-practice/alconna/command.md similarity index 100% rename from website/docs/best-practice/alconna/alconna.md rename to website/docs/best-practice/alconna/command.md diff --git a/website/docs/best-practice/alconna/config.md b/website/docs/best-practice/alconna/config.md index 8b92ef23..91f966c9 100644 --- a/website/docs/best-practice/alconna/config.md +++ b/website/docs/best-practice/alconna/config.md @@ -10,7 +10,7 @@ description: 配置项 - **类型**: `bool` - **默认值**: `False` -"是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。 +是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。 ## alconna_use_command_start diff --git a/website/docs/best-practice/alconna/matcher.md b/website/docs/best-practice/alconna/matcher.md index c2982a50..d908d42d 100644 --- a/website/docs/best-practice/alconna/matcher.md +++ b/website/docs/best-practice/alconna/matcher.md @@ -8,11 +8,8 @@ description: 响应规则的使用 以下为一个简单的使用示例: ```python -from nonebot_plugin_alconna import At -from nonebot.adapters.onebot.v12 import Message from nonebot_plugin_alconna.adapters.onebot12 import Image -from nonebot_plugin_alconna import AlconnaMatches, on_alconna -from nonebot.adapters.onebot.v12 import MessageSegment as Ob12MS +from nonebot_plugin_alconna import At, AlconnaMatches, on_alconna from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand alc = Alconna( @@ -24,6 +21,7 @@ alc = Alconna( Option("member", Args["target", MultiVar(At)]), ), Option("list"), + Option("icon", Args["icon", Image]) ) rg = on_alconna(alc, auto_send_output=True) @@ -32,11 +30,11 @@ rg = on_alconna(alc, auto_send_output=True) async def _(result: Arparma = AlconnaMatches()): if result.find("list"): img = await gen_role_group_list_image() - await rg.finish(Message([Image(img)])) + await rg.finish(Image(img)) if result.find("add"): - group = await create_role_group(result["add.name"]) + group = await create_role_group(result.query[str]("add.name")) if result.find("add.member"): - ats: tuple[At, ...] = result["add.member.target"] + ats = result.query[tuple[At, ...]]("add.member.target") group.extend(member.target for member in ats) await rg.finish("添加成功") ``` @@ -54,12 +52,14 @@ async def _(result: Arparma = AlconnaMatches()): - `use_origin: bool = False`: 是否使用未经 to_me 等处理过的消息 - `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀 -`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`,其拓展了五类方法: +`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`,其拓展了如下方法: - `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理 - `.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])`: 类似于 `reject_arg`,对应 `got_path` +- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path` +- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` +- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt 用例: @@ -180,69 +180,39 @@ async def handle_test4(qux: Query[bool] = AlconnaQuery("baz.qux", False)): 适配器下的消息段标注会匹配特定的 `MessageSegment`: 而通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段,并返回 -`nonebot_plugin_alconna.adapters` 中定义的 `Segment` 模型: - -```python -class Segment: - """基类标注""" - origin: MessageSegment - -class At(Segment): - """At对象, 表示一类提醒某用户的元素""" - type: Literal["user", "role", "channel"] - target: str - -class AtAll(Segment): - """AtAll对象, 表示一类提醒所有人的元素""" - -class Emoji(Segment): - """Emoji对象, 表示一类表情元素""" - id: str - name: Optional[str] - -class Media(Segment): - url: Optional[str] - id: Optional[str] - -class Image(Media): - """Image对象, 表示一类图片元素""" - -class Audio(Media): - """Audio对象, 表示一类音频元素""" - -class Voice(Media): - """Voice对象, 表示一类语音元素""" - -class Video(Media): - """Video对象, 表示一类视频元素""" - -class File(Segment): - """File对象, 表示一类文件元素""" - id: str - name: Optional[str] - -class Reply(Segment): - """Reply对象,表示一类回复消息""" - origin: Any - id: str - msg: Optional[Union[Message, str]] - -class Other(Segment): - """其他 Segment""" -``` - -::: +`nonebot_plugin_alconna.uniseg` 中定义的 [`Segment` 模型](./utils.md#通用消息段) 例如: ```python ... -ats: tuple[At, ...] = result["add.member.target"] +ats = result.query[tuple[At, ...]]("add.member.target") group.extend(member.target for member in ats) ``` 这样插件使用者就不用考虑平台之间字段的差异 +本插件为以下适配器提供了专门的适配器标注: + +| 协议名称 | 路径 | +| ------------------------------------------------------------------- | ------------------------------------ | +| [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 频道](https://github.com/nonebot/adapter-qqguild) | adapters.qqguild | +| [钉钉](https://github.com/nonebot/adapter-ding) | adapters.ding | +| [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 | +| [Villa](https://github.com/CMHopeSunshine/nonebot-adapter-villa) | adapters.villa | +| [Discord](https://github.com/nonebot/adapter-discord) | adapters.discord | +| [Red 协议](https://github.com/nonebot/adapter-red) | adapters.red | + ## 条件控制 本插件可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应。 @@ -301,3 +271,15 @@ async def list_(arp: CommandResult): async def install(arp: CommandResult): ... ``` + +或者使用 `AlconnaMatcher.dispatch`: + +此外,还能像 `CommandGroup` 一样为每个分发设置独立的 matcher: + +```python +update_cmd = pip_cmd.dispatch("install.pak", "pip") + +@update_cmd.handle() +async def update(arp: CommandResult = AlconnaResult()): + ... +``` diff --git a/website/docs/best-practice/alconna/utils.md b/website/docs/best-practice/alconna/utils.md index 9e30cffd..2820015b 100644 --- a/website/docs/best-practice/alconna/utils.md +++ b/website/docs/best-practice/alconna/utils.md @@ -5,9 +5,74 @@ description: 杂项 # 杂项 +## 通用消息段 + +`nonebot-plugin-alconna` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用: + +```python +class Segment: + """基类标注""" + +class Text(Segment): + """Text对象, 表示一类文本元素""" + text: str + style: Optional[str] + +class At(Segment): + """At对象, 表示一类提醒某用户的元素""" + type: Literal["user", "role", "channel"] + target: str + +class AtAll(Segment): + """AtAll对象, 表示一类提醒所有人的元素""" + +class Emoji(Segment): + """Emoji对象, 表示一类表情元素""" + id: str + name: Optional[str] + +class Media(Segment): + url: Optional[str] + id: Optional[str] + path: Optional[str] + raw: Optional[bytes] + +class Image(Media): + """Image对象, 表示一类图片元素""" + +class Audio(Media): + """Audio对象, 表示一类音频元素""" + +class Voice(Media): + """Voice对象, 表示一类语音元素""" + +class Video(Media): + """Video对象, 表示一类视频元素""" + +class File(Segment): + """File对象, 表示一类文件元素""" + id: str + name: Optional[str] + +class Reply(Segment): + """Reply对象,表示一类回复消息""" + origin: Any + id: str + msg: Optional[Union[Message, str]] + +class Card(Segment): + type: Literal["xml", "json"] + raw: str + +class Other(Segment): + """其他 Segment""" +``` + +来自各自适配器的消息序列都会经过这些通用消息段对应的标注转换,以达到跨平台接收消息的作用 + ## 通用消息序列 -除了之前提到的通用标注外,`nonebot_plugin_alconna` 还提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的 `Segment`。 +除了通用消息段外,`nonebot-plugin-alconna` 还提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。 你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。 @@ -40,10 +105,10 @@ assert UniMessage( ### 遍历 -通用消息序列继承自 `List[Union[str, Segment]]` ,因此可以使用 `for` 循环遍历消息段。 +通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段。 ```python -for segment in message: # type: Union[str, Segment] +for segment in message: # type: Segment ... ``` @@ -64,7 +129,7 @@ At in message # 是否都为 "test" message.only("test") # 是否仅包含指定类型的消息段 -message.only(str) +message.only(Text) ``` ### 过滤、索引与切片 @@ -72,7 +137,7 @@ message.only(str) 消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 ```python -from nonebot_plugin_alconna import UniMessage, At, Reply +from nonebot_plugin_alconna import UniMessage, At, Text, Reply message = UniMessage( [ @@ -85,19 +150,19 @@ message = UniMessage( # 索引 message[0] == Reply(...) # 切片 -message[0:2] == UniMessage([Reply(...), "text1"]) +message[0:2] == UniMessage([Reply(...), Text("text1")]) # 类型过滤 message[At] == Message([At("user", "1234")]) # 类型索引 message[At, 0] == At("user", "1234") # 类型切片 -message[str, 0:2] == UniMessage(["text1", "text2"]) +message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")]) ``` 我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 ```python -message.include(str, At) +message.include(Text, At) message.exclude(Reply) ``` @@ -105,16 +170,88 @@ message.exclude(Reply) ```python # 指定类型首个消息段索引 -message.index(str) == 1 +message.index(Text) == 1 # 指定类型消息段数量 -message.count(str) == 2 +message.count(Text) == 2 ``` 此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 ```python # 获取指定类型指定个数的消息段 -message.get(str, 1) == UniMessage(["test1"]) +message.get(Text, 1) == UniMessage([Text("test1")]) +``` + +### 拼接消息 + +`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")]) +``` + +## 跨平台发送 + +`nonebot-plugin-alconna` 不仅支持跨平台接收消息,通过 `UniMessage.export` 方法其同样支持了跨平台发送消息。 + +`UniMessage.export` 会通过传入的 `bot: Bot` 参数读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列: + +```python +from nonebot import Bot, on_command +from nonebot_plugin_alconna import Image, UniMessage + +test = on_command("test") + +@test.handle() +async def handle_test(bot: Bot): + await test.send(await UniMessage(Image(path="path/to/img")).export(bot)) +``` + +而在 `AlconnaMatcher` 下,`got`, `send`, `reject` 等可以发送消息的方法皆支持使用 `UniMessage`,不需要手动调用 export 方法: + +```python +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="请输入目标") +async def tt(target: At): + await test_cmd.send(UniMessage([target, "\ndone."])) ``` ## 特殊装饰器