From 7e0c29472e19d31449e2d76b6635af426e98e827 Mon Sep 17 00:00:00 2001 From: Tarrailt <3165388245@qq.com> Date: Sat, 7 Oct 2023 12:06:07 +0800 Subject: [PATCH] =?UTF-8?q?:memo:=20Docs:=20=E6=9B=B4=E6=96=B0=E6=9C=80?= =?UTF-8?q?=E4=BD=B3=E5=AE=9E=E8=B7=B5=20Alconna=20(#2401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- assets/plugins.json | 6 +- website/docs/best-practice/alconna/README.mdx | 4 +- website/docs/best-practice/alconna/command.md | 20 +- website/docs/best-practice/alconna/config.md | 14 + website/docs/best-practice/alconna/matcher.md | 189 +++++++++-- website/docs/best-practice/alconna/uniseg.md | 299 ++++++++++++++++++ website/docs/best-practice/alconna/utils.md | 271 ++-------------- 7 files changed, 508 insertions(+), 295 deletions(-) create mode 100644 website/docs/best-practice/alconna/uniseg.md diff --git a/assets/plugins.json b/assets/plugins.json index d1d1f8a5..b760bb12 100644 --- a/assets/plugins.json +++ b/assets/plugins.json @@ -199,15 +199,15 @@ "author": "RF-Tar-Railt", "tags": [ { - "label": "matcher", + "label": "多适配器", "color": "#5280ea" }, { - "label": "command", + "label": "消息匹配", "color": "#ea6f52" }, { - "label": "alconna", + "label": "跨平台", "color": "#5452ea" } ], diff --git a/website/docs/best-practice/alconna/README.mdx b/website/docs/best-practice/alconna/README.mdx index caf195b4..4385ac53 100644 --- a/website/docs/best-practice/alconna/README.mdx +++ b/website/docs/best-practice/alconna/README.mdx @@ -13,8 +13,6 @@ slug: /best-practice/alconna/ 该插件提供了一类新的事件响应器辅助函数 `on_alconna`,以及 `AlconnaResult` 等依赖注入函数。 -同时,基于 [Annotated 支持](https://github.com/nonebot/nonebot2/pull/1832), 添加了两类注解 `AlcMatches` 与 `AlcResult` - 该插件声明了一个 `Matcher` 的子类 `AlconnaMatcher`,并在 `AlconnaMatcher` 中添加了一些新的方法,例如: - `assign`:基于 `Alconna` 解析结果,执行满足目标路径的处理函数 @@ -112,7 +110,7 @@ async def got_location(location: str): ``` 在上面的代码中,我们使用 `Alconna` 来解析命令,`on_alconna` 用来创建响应器,使用 `Match` 来获取解析结果。 -command + 关于更多 `Alconna` 的使用方法,可参考 [Alconna 文档](https://arclet.top/docs/tutorial/alconna), 或阅读 [Alconna 基本介绍](./command.md) 一节。 diff --git a/website/docs/best-practice/alconna/command.md b/website/docs/best-practice/alconna/command.md index 9d97b0ff..079bb1b9 100644 --- a/website/docs/best-practice/alconna/command.md +++ b/website/docs/best-practice/alconna/command.md @@ -77,10 +77,16 @@ Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配 我们可以看到主要的两大组件:`Option` 与 `Subcommand`。 -`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"])` +`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|FOO|f")` 或 `Option("--foo", alias=["-F"])` 传入别名后,Option 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo"。 +:::tip 特别提醒!!! + +在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-` + +::: + `Subcommand` 则可以传入自己的 **Option** 与 **Subcommand**。 ```python @@ -93,7 +99,7 @@ alc = Alconna( Subcommand( "sub1", Option("sub1_opt1"), - Option("-SO2"), + Option("SO2"), Subcommand( "sub1_sub1" ) @@ -112,7 +118,7 @@ alc = Alconna( 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python - Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) + Alconna("test", Option("qux", Args["a", int], requires=["foo", "bar", "baz"])) ``` - `default`: 默认值,在该组件未被解析时使用使用该值替换。 @@ -167,6 +173,7 @@ alc = Alconna( `foo#这是注释;?` 或 `foo?#这是注释` :::tip + `Args` 中的 `name` 在实际命令中并不需要传入(keyword 参数除外): ```python @@ -174,7 +181,7 @@ from arclet.alconna import Alconna, Args alc = Alconna("test", Args["foo", str]) alc.parse("test --foo abc") # 错误 -alc.parse("test abc") # 之前 +alc.parse("test abc") # 正确 ``` 若需要 `test --foo abc`,你应该使用 `Option`: @@ -226,6 +233,7 @@ args = Args["foo", BasePattern("@\d+")] - ... :::tip + 几类特殊的传入标记: - `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) @@ -244,11 +252,13 @@ args = Args["foo", BasePattern("@\d+")] 同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip + `MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))` `MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。 `MultiVar` 不能在 `KeyWordVar` 之后传入。 + ::: ### 紧凑命令 @@ -378,7 +388,7 @@ class ShortcutArgs(TypedDict): 快捷指令允许三类特殊的 placeholder: -- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 +- `{%X}`: 如 `setu {%0}`,表示此处必须填入快捷指令后随的第 X 个参数。 例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` diff --git a/website/docs/best-practice/alconna/config.md b/website/docs/best-practice/alconna/config.md index 91f966c9..f5bc1d8f 100644 --- a/website/docs/best-practice/alconna/config.md +++ b/website/docs/best-practice/alconna/config.md @@ -39,3 +39,17 @@ description: 配置项 - **默认值**: `True` 是否使用特制的 Param 提供更好的依赖注入,该选项不会对使用依赖注入函数形式造成影响 + +## alconna_use_command_sep + +- **类型**: `bool` +- **默认值**: `False` + +是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符 + +## alconna_global_extensions + +- **类型**: `List[str]` +- **默认值**: `[]` + +全局加载的扩展, 路径以 . 分隔, 如 foo.bar.baz:DemoExtension diff --git a/website/docs/best-practice/alconna/matcher.md b/website/docs/best-practice/alconna/matcher.md index d908d42d..f3964119 100644 --- a/website/docs/best-practice/alconna/matcher.md +++ b/website/docs/best-practice/alconna/matcher.md @@ -5,7 +5,7 @@ description: 响应规则的使用 # Alconna 响应规则 -以下为一个简单的使用示例: +以下为一个使用示例: ```python from nonebot_plugin_alconna.adapters.onebot12 import Image @@ -46,11 +46,13 @@ async def _(result: Arparma = AlconnaMatches()): - `command: Alconna | str`: Alconna 命令 - `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应 - `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应 -- `output_converter: TConvert | None = None`: 输出信息字符串转换为消息序列方法 - `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名, 作用类似于 `on_command` 中的 aliases - `comp_config: CompConfig | None = None`: 补全会话配置, 不传入则不启用补全会话 +- `extensions: list[type[Extension] | Extension] | None = None`: 需要加载的匹配扩展, 可以是扩展类或扩展实例 +- `exclude_ext: list[type[Extension] | str] | None = None`: 需要排除的匹配扩展, 可以是扩展类或扩展的id - `use_origin: bool = False`: 是否使用未经 to_me 等处理过的消息 -- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀 +- `use_cmd_start: bool = False`: 是否使用 COMMAND_START 作为命令前缀 +- `use_cmd_sep: bool = False`: 是否使用 COMMAND_SEP 作为命令分隔符 `on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`,其拓展了如下方法: @@ -65,7 +67,7 @@ async def _(result: Arparma = AlconnaMatches()): ```python from arclet.alconna import Alconna, Option, Args -from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, AlconnaMatcher, AlconnaArg +from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, AlconnaMatcher, AlconnaArg, UniMessage login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) @@ -77,7 +79,7 @@ async def login_exit(): async def login_handle(matcher: AlconnaMatcher, pw: Match[str] = AlconnaMatch("password")): matcher.set_path_arg("password", pw.result) -@login.got_path("password", prompt="请输入密码") +@login.got_path("password", prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请输入密码")) async def login_got(password: str = AlconnaArg("password")): assert password await login.send("登录成功") @@ -95,6 +97,7 @@ async def login_got(password: str = AlconnaArg("password")): - `AlconnaMatch`: `Match` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数 - `AlconnaQuery`: `Query` 类型的依赖注入函数,其能够额外传入一个 middleware 函数来处理得到的参数 - `AlconnaExecResult`: 提供挂载在命令上的 callback 的返回结果 (`Dict[str, Any]`) 的依赖注入函数 +- `AlconnaExtension`: 提供指定类型的 `Extension` 的依赖注入函数 可以看到,本插件提供了几类额外的模型: @@ -111,10 +114,26 @@ async def login_got(password: str = AlconnaArg("password")): 而若设置配置项 **ALCONNA_USE_PARAM** (默认为 True) 为 True,则上述依赖注入的目标参数皆不需要使用依赖注入函数: ```python -async def handle( +... +@cmd.handle() +async def handle1( + result: CommandResult = AlconnaResult(), + arp: Arparma = AlconnaMatches(), + dup: Duplication = AlconnaDuplication(Duplication), + ext: Extension = AlconnaExtension(Extension), + foo: Match[str] = AlconnaMatch("foo"), + bar: Query[int] = AlconnaQuery("ttt.bar", 0) +): + ... + +# ALCONNA_USE_PARAM 为 True 后 + +@cmd.handle() +async def handle2( result: CommandResult, arp: Arparma, dup: Duplication, + ext: Extension, source: Alconna, abc: str, # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler foo: Match[str], @@ -200,7 +219,8 @@ group.extend(member.target for member in ats) | [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 | +| [QQ bot](https://github.com/nonebot/adapter-qq) | adapters.qq | +| [QQ 频道bot](https://github.com/nonebot/adapter-qq) | 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 | @@ -215,7 +235,7 @@ group.extend(member.target for member in ats) ## 条件控制 -本插件可以通过 `handle(parameterless)` 来控制一个具体的响应函数是否在不满足条件时跳过响应。 +本插件可以通过 `assign` 来控制一个具体的响应函数是否在不满足条件时跳过响应。 ```python ... @@ -224,7 +244,7 @@ require("nonebot_plugin_alconna") ... from arclet.alconna import Alconna, Subcommand, Option, Args -from nonebot_plugin_alconna import assign, on_alconna, CommandResult, Check +from nonebot_plugin_alconna import on_alconna, CommandResult pip = Alconna( "pip", @@ -238,43 +258,22 @@ pip = Alconna( pip_cmd = on_alconna(pip) -# 仅在命令为 `pip install` 并且 pak 为 `pip` 时响应 -@pip_cmd.handle([Check(assign("install.pak", "pip"))]) -async def update(arp: CommandResult): - ... - -# 仅在命令为 `pip list` 时响应 -@pip_cmd.handle([Check(assign("list"))]) -async def list_(arp: CommandResult): - ... - -# 仅在命令为 `pip install` 时响应 -@pip_cmd.handle([Check(assign("install"))]) -async def install(arp: CommandResult): - ... -``` - -或者使用 `AlconnaMatcher.assign`: - -```python @pip_cmd.assign("install.pak", "pip") -async def update(arp: CommandResult): +async def update(res: CommandResult): ... # 仅在命令为 `pip list` 时响应 @pip_cmd.assign("list") -async def list_(arp: CommandResult): +async def list_(res: CommandResult): ... # 仅在命令为 `pip install` 时响应 @pip_cmd.assign("install") -async def install(arp: CommandResult): +async def install(res: CommandResult): ... ``` -或者使用 `AlconnaMatcher.dispatch`: - -此外,还能像 `CommandGroup` 一样为每个分发设置独立的 matcher: +此外,使用 `AlconnaMatcher.dispatch` 还能像 `CommandGroup` 一样为每个分发设置独立的 matcher: ```python update_cmd = pip_cmd.dispatch("install.pak", "pip") @@ -283,3 +282,125 @@ update_cmd = pip_cmd.dispatch("install.pak", "pip") async def update(arp: CommandResult = AlconnaResult()): ... ``` + +另外,`AlconnaMatcher` 有类似于 `got` 的 `got_path`: + +```python +from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna + +test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]])) + +@test_cmd.handle() +async def tt_h(matcher: AlconnaMatcher, target: Match[Union[str, At]]): + if target.available: + matcher.set_path_arg("target", target.result) + +@test_cmd.got_path("target", prompt="请输入目标") +async def tt(target: Union[str, At]): + await test_cmd.send(UniMessage(["ok\n", target])) +``` + +`got_path` 与 `assign`, `Match`, `Query` 等地方一样,都需要指明 `path` 参数 (即对应 Arg 验证的路径) + +`got_path` 会获取消息的最后一个消息段并转为 path 对应的类型,例如示例中 `target` 对应的 Arg 里要求 str 或 At,则 got 后用户输入的消息只有为 text 或 at 才能进入处理函数。 + +:::tip + +`path` 支持 ~XXX 语法,其会把 ~ 替换为可能的父级路径: + +```python + pip = Alconna( + "pip", + Subcommand( + "install", + Args["pak", str], + Option("--upgrade|-U"), + Option("--force-reinstall"), + ), + Subcommand("list", Option("--out-dated")), + ) + + pipcmd = on_alconna(pip) + pip_install_cmd = pipcmd.dispatch("install") + + + @pip_install_cmd.assign("~upgrade") + async def pip1_u(pak: Query[str] = Query("~pak")): + await pip_install_cmd.finish(f"pip upgrading {pak.result}...") +``` + +::: + +## 匹配拓展 + +本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为。 + +例如 `LLMExtension` (仅举例): + +```python +from nonebot_plugin_alconna import Extension, Alconna, on_alconna + +class LLMExtension(Extension): + @property + def priority(self) -> int: + return 10 + + @property + def id(self) -> str: + return "LLMExtension" + + def __init__(self, llm): + self.llm = llm + + def post_init(self, alc: Alconna) -> None: + self.llm.add_context(alc.command, alc.meta.description) + + async def receive_wrapper(self, bot, event, receive): + resp = await self.llm.input(str(receive)) + return receive.__class__(resp.content) + +matcher = on_alconna( + Alconna(...), + extensions=[LLMExtension(LLM)] +) +... +``` + +那么使用了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息。 + +目前 `Extension` 的功能有: + +- 对于事件的来源适配器或 bot 选择是否接受响应 +- 输出信息的自定义转换方法 +- 从传入事件中自定义提取消息的方法 +- 对于传入的alc对象的追加的自定义处理 +- 对传入的消息 (Message 或 UniMessage) 的额外处理 +- 对命令解析结果的额外处理 +- 对发送的消息 (Message 或 UniMessage) 的额外处理 +- 自定义额外的matcher api + +例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash指令并注册,且将收到的指令交互事件转为指令供命令解析: + +```python +from nonebot_plugin_alconna import Match, on_alconna + +from nonebot_plugin_alconna.adapters.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}") +``` diff --git a/website/docs/best-practice/alconna/uniseg.md b/website/docs/best-practice/alconna/uniseg.md new file mode 100644 index 00000000..953dcedb --- /dev/null +++ b/website/docs/best-practice/alconna/uniseg.md @@ -0,0 +1,299 @@ +--- +sidebar_position: 5 +description: 通用消息组件 +--- + +# 通用消息组件 + +`uniseg` 模块属于 `nonebot-plugin-alconna` 的子插件,其提供了一套通用的消息组件,用于在 `nonebot-plugin-alconna` 下构建通用消息。 + +## 通用消息段 + +`nonebot-plugin-alconna.uniseg` 提供了类似 `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.uniseg` 同时提供了一个类似于 `Message` 的 `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) + ... +``` + +不仅如此,你还可以通过 `UniMessage` 的 `export` 方法来**跨平台发送消息**。 + +`UniMessage.export` 会通过传入的 `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(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 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."])) +``` + +### 获取消息纯文本 + +类似于 `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 + ... +``` + +### 检查消息段 + +我们可以通过 `in` 运算符或消息序列的 `has` 方法来: + +```python +# 是否存在消息段 +At("user", "1234") in message +# 是否存在指定类型的消息段 +At in message +``` + +我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。 + +```python +# 是否都为 "test" +message.only("test") +# 是否仅包含指定类型的消息段 +message.only(Text) +``` + +### 过滤、索引与切片 + +消息序列对列表的索引与切片进行了增强,在原有列表 `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")]) +``` + +### 拼接消息 + +`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}") + ) +``` diff --git a/website/docs/best-practice/alconna/utils.md b/website/docs/best-practice/alconna/utils.md index 2820015b..2bad6166 100644 --- a/website/docs/best-practice/alconna/utils.md +++ b/website/docs/best-practice/alconna/utils.md @@ -1,259 +1,10 @@ --- -sidebar_position: 5 +sidebar_position: 6 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` 类型,其元素为经过通用标注转换后的通用消息段。 - -你可以通过提供的 `UniversalMessage` 或 `UniMsg` 依赖注入器来获取 `UniMessage`。 - -```python -from nonebot_plugin_alconna 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) - ... -``` - -### 获取消息纯文本 - -类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本。 - -```python -from nonebot_plugin_alconna import UniMessage, At -# 提取消息纯文本字符串 -assert UniMessage( - [At("user", "1234"), "text"] -).extract_plain_text() == "text" -``` - -### 遍历 - -通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段。 - -```python -for segment in message: # type: Segment - ... -``` - -### 检查消息段 - -我们可以通过 `in` 运算符或消息序列的 `has` 方法来: - -```python -# 是否存在消息段 -At("user", "1234") in message -# 是否存在指定类型的消息段 -At in message -``` - -我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。 - -```python -# 是否都为 "test" -message.only("test") -# 是否仅包含指定类型的消息段 -message.only(Text) -``` - -### 过滤、索引与切片 - -消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 - -```python -from nonebot_plugin_alconna 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")]) -``` - -### 拼接消息 - -`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."])) -``` - ## 特殊装饰器 `nonebot_plugin_alconna` 提供 了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, @@ -315,3 +66,23 @@ book = ( .build() ) ``` + +## 中间件 + +在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数, + +```python {1, 9} +from nonebot_plugin_alconna import image_fetch + +mask_cmd = on_alconna( + Alconna("search", Args["img?", Image]), +) + + +@mask_cmd.handle() +async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)): + result = await search_img(img.result) + await matcher.send(result.content) +``` + +其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。