diff --git a/website/docs/best-practice/alconna/README.mdx b/website/docs/best-practice/alconna/README.mdx index d43499b7..b6f79eac 100644 --- a/website/docs/best-practice/alconna/README.mdx +++ b/website/docs/best-practice/alconna/README.mdx @@ -102,17 +102,16 @@ async def got_location(location: str = ArgPlainText()): -```python {5-10,14-16,18-19} +```python {5-9,13-15,17-18} from nonebot.rule import to_me from arclet.alconna import Alconna, Args from nonebot_plugin_alconna import Match, on_alconna weather = on_alconna( Alconna("天气", Args["location?", str]), + aliases={"weather", "天气预报"}, rule=to_me(), ) -weather.shortcut("weather", {"command": "天气"}) -weather.shortcut("天气预报", {"command": "天气"}) @weather.handle() @@ -133,7 +132,7 @@ async def got_location(location: str): 或阅读 [Alconna 基本介绍](./command.md) 一节。 关于更多 `on_alconna` 的使用方法,可参考 [插件文档](https://github.com/nonebot/plugin-alconna/blob/master/docs.md), -或阅读 [响应规则的使用](./matcher.md) 一节。 +或阅读 [响应规则的使用](./matcher.mdx) 一节。 ## 交流与反馈 diff --git a/website/docs/best-practice/alconna/command.md b/website/docs/best-practice/alconna/command.md index 0b2c7c10..e207c949 100644 --- a/website/docs/best-practice/alconna/command.md +++ b/website/docs/best-practice/alconna/command.md @@ -5,7 +5,7 @@ description: Alconna 基本介绍 # Alconna 本体 -[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 +[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: @@ -34,9 +34,7 @@ print(res.all_matched_args) 这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 -## 组成 - -### 命令头 +## 命令头 命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 !help 中的 ! 与 help。 @@ -55,15 +53,15 @@ print(res.all_matched_args) | [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`。 +对于无前缀的类型头,此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。如此该命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。解析后,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 :::tip -**正则只在命令名上生效,命令前缀中的正则会被转义** +**正则内容只在命令名上生效,前缀中的正则会被转义** ::: -除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,那就是 Bracket Header: +除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,称为 Bracket Header: ```python from alconna import Alconna @@ -82,34 +80,34 @@ Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配 - "{:\d+}" ⇔ "(\d+)" - "{foo:int}" ⇔ "(?P<foo>\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式 -### 参数声明(Args) +## 参数声明(Args) -`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : +`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : - `Args[key, var, default][key1, var1, default1][...]` - `Args[(key, var, default)]` - `Args.key[var, default]` -其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** +其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** 其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 -#### key +### key -`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 +`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 -其有三种为 Args 注解的标识符:  `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: +其有三种为 Args 注解的标识符: `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: -- `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 -- `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 -- `/` 标识符表示该参数的类型注解需要隐藏。 +- `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 +- `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 +- `/` 标识符表示该参数的类型注解需要隐藏。 -另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: -`foo#这是注释;?` 或 `foo?#这是注释` +另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: +`foo#这是注释;?` 或 `foo?#这是注释` :::tip -`Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): +`Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): ```python from arclet.alconna import Alconna, Args @@ -120,7 +118,7 @@ alc.parse("test --foo abc") # 错误 alc.parse("test abc") # 正确 ``` -若需要 `test --foo abc`,你应该使用 `Option`: +若需要 `test --foo abc`,你应该使用 `Option`: ```python from arclet.alconna import Alconna, Args, Option @@ -131,11 +129,11 @@ alc = Alconna("test", Option("--foo", Args["foo", str])) ::: -#### var +### var var 负责命令参数的**类型检查**与**类型转化** -`Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: +`Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: ```python from arclet.alconna import Args @@ -146,89 +144,109 @@ from nepattern import BasePattern args = Args["foo", BasePattern("@\d+")] ``` -示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` +`pip` 示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` `nepattern.global_patterns`默认支持的类型有: - `str`: 匹配任意字符串 - `int`: 匹配整数 - `float`: 匹配浮点数 -- `bool`: 匹配 `True` 与 `False` 以及他们小写形式 -- `hex`: 匹配 `0x` 开头的十六进制字符串 +- `bool`: 匹配 `True` 与 `False` 以及他们小写形式 +- `hex`: 匹配 `0x` 开头的十六进制字符串 - `url`: 匹配网址 -- `email`: 匹配 `xxxx@xxx` 的字符串 -- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 -- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 -- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 -- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 +- `email`: 匹配 `xxxx@xxx` 的字符串 +- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 +- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 +- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 +- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 - `Any`: 匹配任意类型 -- `AnyString`: 匹配任意类型,转为 `str` -- `Number`: 匹配 `int` 与 `float`,转为 `int` +- `AnyString`: 匹配任意类型,转为 `str` +- `Number`: 匹配 `int` 与 `float`,转为 `int` 同时可以使用 typing 中的类型: - `Literal[X]`: 匹配其中的任意一个值 - `Union[X, Y]`: 匹配其中的任意一个类型 -- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 -- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 -- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 +- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 +- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 +- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 - ... :::tip 几类特殊的传入标记: -- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) -- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换) +- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) +- `RawStr("foo")`: 匹配字符串 "foo" (即使有 `BasePattern` 与之关联也不会被替换) - `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz" - `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型 -- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 -- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] -- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 +- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 +- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] +- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 - `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) - ... +**特别的**,你可以不传入 `var`,此时会使用 `key` 作为 `var`, 匹配 `key` 字符串。 + ::: -`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 +#### MultiVar 与 KeyWordVar + +`MultiVar` 是一个特殊的标注,用于告知解析器该参数可以接受多个值,类似于函数中的 `*args`,其构造方法形如 `MultiVar(str)`。 + +同样的还有 `KeyWordVar`,类似于函数中的 `*, name: type`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip -`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))` +`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,类似于函数中的 `**kwargs`,其构造方法形如 `MultiVar(KeyWordVar(str))` -`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 +`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 -`MultiVar` 不能在 `KeyWordVar` 之后传入 +`MultiVar` 不能在 `KeyWordVar` 之后传入 ::: -### Option 和 Subcommand +### default -`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]` +`default` 传入的是该参数的默认值或者 `Field`,以携带对于该参数的更多信息。 -传入别名后,`option` 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo" +默认情况下 (即不声明) `default` 的值为特殊值 `Empty`。这也意味着你可以将默认值设置为 `None` 表示默认值为空值。 + +`Field` 构造需要的参数说明如下: + +- default: 参数单元的默认值 +- alias: 参数单元默认值的别名 +- completion: 参数单元的补全说明生成函数 +- unmatch_tips: 参数单元的错误提示生成函数,其接收一个表示匹配失败的元素的参数 +- missing_tips: 参数单元的缺失提示生成函数 + +## 选项与子命令(Option & Subcommand) + +`Option` 和 `Subcommand` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")`,`Subcommand("foo", alias=["F"])` + +传入别名后,选项与子命令会选择其中长度最长的作为其名称。若传入为 "--foo|-f",则命令名称为 "--foo" :::tip 特别提醒!!! -在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-` +Option 的名字或别名**没有要求**必须在前面写上 `-` + +Option 与 Subcommand 的唯一区别在于 Subcommand 可以传入自己的 **Option** 与 **Subcommand** ::: -`Subcommand` 可以传入自己的 **Option** 与 **Subcommand** - 他们拥有如下共同参数: - `help_text`: 传入该组件的帮助信息 - `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) - `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 - 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: + 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) ``` - `default`: 默认值,在该组件未被解析时使用使用该值替换。 - 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: + 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: ```python from arclet.alconna import Option, OptionResult @@ -237,25 +255,27 @@ opt1 = Option("--foo", default=False) opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) ``` -`Option` 可以特别设置传入一类 `Action`,作为解析操作 +### Action -`Action` 分为三类: +`Option` 可以特别设置传入一类 `Action`,作为解析操作 + +`Action` 分为三类: - `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 - `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 - `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 -`Alconna` 提供了预制的几类 `Action`: +`Alconna` 提供了预制的几类 `Action`: - `store`(默认),`store_value`,`store_true`,`store_false` - `append`,`append_value` - `count` -### Arparma +## 解析结果(Arparma) -`Alconna.parse` 会返回由 **Arparma** 承载的解析结果 +`Alconna.parse` 会返回由 **Arparma** 承载的解析结果 -`Arparma` 会有如下参数: +`Arparma` 有如下属性: - 调试类 @@ -272,22 +292,53 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) - other_args: 除主参数外的其他解析结果 - all_matched_args: 所有 Args 的解析结果 -`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 +`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 -`path` 支持如下: +`path` 支持如下: -- `main_args`, `options`, ...: 返回对应的属性 +- `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` 键对应的值 ... +- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值 +- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值 +- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult) +- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值 +- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典 +- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 ... + +## 元数据(CommandMeta) + +`Alconna` 的元数据相当于其配置,拥有以下条目: + +- `description`: 命令的描述 +- `usage`: 命令的用法 +- `example`: 命令的使用样例 +- `author`: 命令的作者 +- `fuzzy_match`: 命令是否开启模糊匹配 +- `fuzzy_threshold`: 模糊匹配阈值 +- `raise_exception`: 命令是否抛出异常 +- `hide`: 命令是否对 manager 隐藏 +- `hide_shortcut`: 命令的快捷指令是否在 help 信息中隐藏 +- `keep_crlf`: 命令解析时是否保留换行字符 +- `compact`: 命令是否允许第一个参数紧随头部 +- `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数 +- `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 {...},parentheses 为 $(...) +- `extra`: 命令的自定义额外信息 + +元数据一定使用 `meta=...` 形式传入: + +```python +from arclet.alconna import Alconna, CommandMeta + +alc = Alconna(..., meta=CommandMeta("foo", example="bar")) +``` ## 命名空间配置 -命名空间配置 (以下简称命名空间) 相当于`Alconna`的设置,`Alconna`默认使用“Alconna”命名空间,命名空间有以下几个属性: +命名空间配置 (以下简称命名空间) 相当于 `Alconna` 的默认配置,其优先度低于 `CommandMeta`。 + +`Alconna` 默认使用 "Alconna" 命名空间。 + +命名空间有以下几个属性: - name: 命名空间名称 - prefixes: 默认前缀配置 @@ -296,9 +347,11 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) - fuzzy_match: 默认是否开启模糊匹配 - raise_exception: 默认是否抛出异常 - builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) +- disable_builtin_options: 默认禁用的内置选项(--help, --shortcut, --comp) - enable_message_cache: 默认是否启用消息缓存 - compact: 默认是否开启紧凑模式 - strict: 命令是否严格匹配 +- context_style: 命令上下文插值的风格 - ... ### 新建命名空间并替换 @@ -307,7 +360,7 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config -ns = Namespace("foo", prefixes=["/"])  # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ +ns = Namespace("foo", prefixes=["/"]) # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 @@ -342,15 +395,15 @@ with namespace(config.default_namespace.name) as np: 快捷命令可以做到标识一段命令, 并且传递参数给原命令 -一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) +一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) -`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: +`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: ```python class ShortcutArgs(TypedDict): """快捷指令参数""" - command: NotRequired[DataCollection[Any]] + command: NotRequired[str] """快捷指令的命令""" args: NotRequired[list[Any]] """快捷指令的附带参数""" @@ -358,6 +411,10 @@ class ShortcutArgs(TypedDict): """是否允许命令后随参数""" prefix: NotRequired[bool] """是否调用时保留指令前缀""" + wrapper: NotRequired[ShortcutRegWrapper] + """快捷指令的正则匹配结果的额外处理函数""" + humanized: NotRequired[str] + """快捷指令的人类可读描述""" ``` ### args的使用 @@ -400,28 +457,28 @@ alc.parse("echo hello world!") # hello world! ``` -当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 +当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 快捷指令允许三类特殊的 placeholder: -- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 +- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 -例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` +例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` -- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 +- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 - `{X}`: 表示此处填入可能的正则匹配的组: -- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 -- 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 +- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 +- 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 -除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 +除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 例如: -- `cmd --shortcut ` 来增加一个快捷指令 -- `cmd --shortcut list` 来列出当前指令的所有快捷指令 -- `cmd --shortcut delete key` 来删除一个快捷指令 +- `cmd --shortcut ` 来增加一个快捷指令 +- `cmd --shortcut list` 来列出当前指令的所有快捷指令 +- `cmd --shortcut delete key` 来删除一个快捷指令 ```python from arclet.alconna import Alconna, Args @@ -437,7 +494,7 @@ alc.parse("eval --shortcut list") ## 紧凑命令 -`Alconna`,  `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: +`Alconna`, `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: ```python from arclet.alconna import Alconna, Option, CommandMeta, Args @@ -459,7 +516,7 @@ print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) # ['abc', 'def', 'xyz'] ``` -当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: +当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: ```python from arclet.alconna import Alconna, Option, count @@ -472,8 +529,6 @@ print(alc.parse("pp -vvv").query[int]("verbose.value")) ## 模糊匹配 -模糊匹配通过在 Alconna 中设置其 CommandMeta 开启 - 模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 ```python @@ -490,7 +545,7 @@ alc.parse("test_fuzy") 半自动补全为用户提供了推荐后续输入的功能 -补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) +补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) ```python from arclet.alconna import Alconna, Args, Option @@ -515,9 +570,9 @@ output ## Duplication -**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** +**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** -普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 +普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 以pip为例,其对应的 Duplication 应如下构造: @@ -526,20 +581,20 @@ from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, Sub class MyDup(Duplication): -    verbose: OptionResult -    install: SubcommandStub + verbose: OptionResult + install: SubcommandStub alc = Alconna( -    "pip", -    Subcommand( -        "install", -        Args["package", str], -        Option("-r|--requirement", Args["file", str]), -        Option("-i|--index-url", Args["url", str]), -    ), -    Option("-v|--version"), -    Option("-v|--verbose", action=count), + "pip", + Subcommand( + "install", + Args["package", str], + Option("-r|--requirement", Args["file", str]), + Option("-i|--index-url", Args["url", str]), + ), + Option("-v|--version"), + Option("-v|--verbose", action=count), ) res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 @@ -551,7 +606,7 @@ print(result.install) # SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') ``` -**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: +**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: ```python from typing import Optional @@ -563,3 +618,23 @@ class MyDup(Duplication): file: Optional[str] = None url: Optional[str] = None ``` + +## 上下文插值 + +当 `context_style` 条目被设置后,传入的命令中符合上下文插值的字段会被自动替换成当前上下文中的信息。 + +上下文可以在 `parse` 中传入: + +```python +from arclet.alconna import Alconna, Args, CommandMeta + +alc = Alconna("test", Args["foo", int], meta=CommandMeta(context_style="parentheses")) + +alc.parse("test $(bar)", {"bar": 123}) +# {"foo": 123} +``` + +context_style 的值分两种: + +- `"bracket"`: 插值格式为 `{...}`,例如 `{foo}` +- `"parentheses"`: 插值格式为 `$(...)`,例如 `$(bar)` diff --git a/website/docs/best-practice/alconna/config.md b/website/docs/best-practice/alconna/config.md index eee4a57a..ebce8d57 100644 --- a/website/docs/best-practice/alconna/config.md +++ b/website/docs/best-practice/alconna/config.md @@ -46,3 +46,31 @@ description: 配置项 - **默认值**: `[]` 全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 + +## alconna_context_style + +- **类型**: `Optional[Literal["bracket", "parentheses"]]` +- **默认值**: `None` + +全局命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`。 + +## alconna_enable_saa_patch + +- **类型**: `bool` +- **默认值**: `False` + +是否启用 SAA 补丁。 + +## alconna_apply_filehost + +- **类型**: `bool` +- **默认值**: `False` + +是否启用文件托管。 + +## alconna_apply_fetch_targets + +- **类型**: `bool` +- **默认值**: `False` + +是否启动时拉取一次发送对象列表。 diff --git a/website/docs/best-practice/alconna/matcher.md b/website/docs/best-practice/alconna/matcher.mdx similarity index 68% rename from website/docs/best-practice/alconna/matcher.md rename to website/docs/best-practice/alconna/matcher.mdx index 8ed13ac9..bec9e1b1 100644 --- a/website/docs/best-practice/alconna/matcher.md +++ b/website/docs/best-practice/alconna/matcher.mdx @@ -3,13 +3,14 @@ sidebar_position: 3 description: 响应规则的使用 --- +import Messenger from "@site/src/components/Messenger"; + # Alconna 插件 展示: ```python -from nonebot_plugin_alconna.adapters.onebot12 import Image -from nonebot_plugin_alconna import At, on_alconna +from nonebot_plugin_alconna import At, Image, on_alconna from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand @@ -30,8 +31,8 @@ rg = on_alconna(alc, auto_send_output=True) @rg.handle() async def _(result: Arparma): if result.find("list"): - img = await ob12_gen_role_group_list_image() - await rg.finish(Image(img)) + 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"): @@ -42,7 +43,7 @@ async def _(result: Arparma): ## 响应器使用 -本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`: +本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`: ```python def on_alconna( @@ -61,10 +62,10 @@ def on_alconna( ): ``` -- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令 +- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令 - `skip_for_unmatch`: 是否在命令不匹配时跳过该响应 - `auto_send_output`: 是否自动发送输出信息并跳过响应 -- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases +- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases - `comp_config`: 补全会话配置, 不传入则不启用补全会话 - `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例 - `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id @@ -72,14 +73,14 @@ def on_alconna( - `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀 - `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符 -`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法: +`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法: -- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看[条件控制](./matcher.md#条件控制)) -- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换 -- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本 -- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path` -- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` -- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt +- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看[条件控制](./matcher.mdx#条件控制)) +- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换 +- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本 +- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path` +- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` +- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt 实例: @@ -88,7 +89,7 @@ from nonebot import require require("nonebot_plugin_alconna") from arclet.alconna import Alconna, Option, Args -from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage +from nonebot_plugin_alconna import on_alconna, Match, UniMessage login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/ @@ -96,13 +97,13 @@ login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|-- # /login -r 触发 @login.assign("recall") async def login_exit(): -    await login.finish("已退出") + await login.finish("已退出") # /login xxx 触发 @login.assign("password") -async def login_handle(pw: Match[str] = AlconnaMatch("password")): -    if pw.available: -        login.set_path_arg("password", pw.result) +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())} 请输入密码")) @@ -115,24 +116,24 @@ async def login_got(password: str): 本插件提供了一系列依赖注入函数,便于在响应函数中获取解析结果: -- `AlconnaResult`: `CommandResult` 类型的依赖注入函数 -- `AlconnaMatches`: `Arparma` 类型的依赖注入函数 -- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数 -- `AlconnaMatch`: `Match` 类型的依赖注入函数 -- `AlconnaQuery`: `Query` 类型的依赖注入函数 +- `AlconnaResult`: `CommandResult` 类型的依赖注入函数 +- `AlconnaMatches`: `Arparma` 类型的依赖注入函数 +- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数 +- `AlconnaMatch`: `Match` 类型的依赖注入函数 +- `AlconnaQuery`: `Query` 类型的依赖注入函数 -同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832),添加了两类注解: +同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832),添加了两类注解: -- `AlcMatches`:同 `AlconnaMatches` -- `AlcResult`:同 `AlconnaResult` +- `AlcMatches`:同 `AlconnaMatches` +- `AlcResult`:同 `AlconnaResult` 可以看到,本插件提供了几类额外的模型: -- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段 -- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值 -- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果 +- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段 +- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值 +- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果 -**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效: +**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效: ```python async def handle( @@ -217,26 +218,25 @@ async def handle_test4(qux: Query[bool] = Query("baz.qux", False)): 本插件为以下适配器提供了专门的适配器标注: -| 协议名称 | 路径 | -| ------------------------------------------------------------------- | ------------------------------------ | -| [OneBot 协议](https://github.com/nonebot/adapter-onebot) | adapters.onebot11, adapters.onebot12 | +| 协议名称 | 路径 | +|---------------------------------------------------------------------| ------------------------------------ | +| [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 | +| [飞书](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 | -| [QQ 频道 bot](https://github.com/nonebot/adapter-qq) | adapters.qqguild | -| [钉钉](https://github.com/nonebot/adapter-ding) | adapters.ding | +| [钉钉](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 | +| [开黑啦](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 | -| [Satori 协议](https://github.com/nonebot/adapter-satori) | adapters.satori | +| [Red 协议](https://github.com/nonebot/adapter-red) | adapters.red | +| [Satori 协议](https://github.com/nonebot/adapter-satori) | adapters.satori | ## 条件控制 @@ -341,7 +341,7 @@ async def tt(target: Union[str, At]): ## 响应器创建装饰 -本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器: +本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器: ```python from nonebot_plugin_alconna import funcommand @@ -369,7 +369,7 @@ async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): ## 类Koishi构造器 -本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** : +本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** : ```python from nonebot_plugin_alconna import Command, Arparma @@ -389,7 +389,7 @@ async def _(arp: Arparma): await book.send(str(arp.options)) ``` -甚至,你可以设置 `action` 来设定响应行为: +甚至,你可以设置 `action` 来设定响应行为: ```python book = ( @@ -403,9 +403,9 @@ book = ( ) ``` -## 返回值回调 +## 返回值中间件 -在 `AlconnaMatch`,`AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数: +在 `AlconnaMatch`,`AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数: ```python from nonebot_plugin_alconna import image_fetch @@ -422,13 +422,13 @@ async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img" await matcher.send(result.content) ``` -其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。 +其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。 ## 匹配拓展 -本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为 +本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为 -例如 `LLMExtension` (仅举例): +例如一个 `LLMExtension` 可以如下实现 (仅举例): ```python from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface @@ -467,14 +467,15 @@ matcher = on_alconna( ... ``` -那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量。 +那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量。 -目前 `Extension` 的功能有: +目前 `Extension` 的功能有: - `validate`: 对于事件的来源适配器或 bot 选择是否接受响应 - `output_converter`: 输出信息的自定义转换方法 - `message_provider`: 从传入事件中自定义提取消息的方法 - `receive_provider`: 对传入的消息 (Message 或 UniMessage) 的额外处理 +- `context_provider`: 对命令上下文的额外处理 - `permission_check`: 命令对消息解析并确认头部匹配(即确认选择响应)时对发送者的权限判断 - `parse_wrapper`: 对命令解析结果的额外处理 - `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理 @@ -482,7 +483,7 @@ matcher = on_alconna( - `catch`: 自定义依赖注入处理函数 - `post_init`: 响应器创建后对命令对象的额外处理 -例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析: +例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析: ```python from nonebot_plugin_alconna import Match, on_alconna @@ -513,3 +514,62 @@ async def remove(plugin: Match[str], time: Match[int]): 全局的 Extension 可延迟加载 (即若有全局拓展加载于部分 AlconnaMatcher 之后,这部分响应器会被追加拓展) ::: + +## 补全会话 + +补全会话基于 [`半自动补全`](./command.md#半自动补全),用于指令参数缺失或参数错误时给予交互式提示,类似于 `got-reject`: + +```python +from nonebot_plugin_alconna import Alconna, Args, Field, At, on_alconna + +alc = Alconna( + "添加教师", + Args["name", str, Field(completion=lambda: "请输入姓名")], + Args["phone", int, Field(completion=lambda: "请输入手机号")], + Args["at", [str, At], Field(completion=lambda: "请输入教师号")], +) + +cmd = on_alconna(alc, comp_config={"lite": True}, skip_for_unmatch=False) + +@cmd.handle() +async def handle(result: Arparma): + cmd.finish("添加成功") +``` + +此时,当用户输入 `添加教师` 时,会自动提示用户输入姓名,手机号和教师号,用户输入后会自动进入下一个提示: + + + + +补全会话配置如下: + +```python +class CompConfig(TypedDict): + tab: NotRequired[str] + """用于切换提示的指令的名称""" + enter: NotRequired[str] + """用于输入提示的指令的名称""" + exit: NotRequired[str] + """用于退出会话的指令的名称""" + timeout: NotRequired[int] + """超时时间""" + hide_tabs: NotRequired[bool] + """是否隐藏所有提示""" + hides: NotRequired[Set[Literal["tab", "enter", "exit"]]] + """隐藏的指令""" + disables: NotRequired[Set[Literal["tab", "enter", "exit"]]] + """禁用的指令""" + lite: NotRequired[bool] + """是否使用简洁版本的补全会话(相当于同时配置 disables、hides、hide_tabs)""" +``` diff --git a/website/docs/best-practice/alconna/uniseg.mdx b/website/docs/best-practice/alconna/uniseg.mdx index ba44af25..dfde17b4 100644 --- a/website/docs/best-practice/alconna/uniseg.mdx +++ b/website/docs/best-practice/alconna/uniseg.mdx @@ -12,8 +12,8 @@ import TabItem from "@theme/TabItem"; ## 通用消息段 -适配器下的消息段标注会匹配适配器特定的 `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), 以达到**跨平台接收消息**的作用。 +适配器下的消息段标注会匹配适配器特定的 `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` 下直接标注使用: @@ -24,15 +24,17 @@ class Segment: class Text(Segment): """Text对象, 表示一类文本元素""" text: str - style: Optional[str] + styles: Dict[Tuple[int, int], List[str]] class At(Segment): """At对象, 表示一类提醒某用户的元素""" - type: Literal["user", "role", "channel"] + flag: Literal["user", "role", "channel"] target: str + display: Optional[str] class AtAll(Segment): """AtAll对象, 表示一类提醒所有人的元素""" + here: bool class Emoji(Segment): """Emoji对象, 表示一类表情元素""" @@ -42,17 +44,23 @@ class Emoji(Segment): class Media(Segment): url: Optional[str] id: Optional[str] - path: Optional[str] - raw: Optional[bytes] + 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对象, 表示一类视频元素""" @@ -75,15 +83,47 @@ class Reference(Segment): """此处不一定是消息ID,可能是其他ID,如消息序号等""" content: Optional[Union[Message, str, List[Union[RefNode, CustomNode]]]] -class Card(Segment): - type: Literal["xml", "json"] - raw: str +class Hyper(Segment): + """Hyper对象,表示一类超级消息。如卡片消息、ark消息、小程序等""" + format: Literal["xml", "json"] + raw: Optional[str] + content: Optional[Union[dict, list]] class Other(Segment): """其他 Segment""" + origin: MessageSegment + +class Custom(Segment, abc.ABC): + """Custom对象,表示一类自定义消息""" + + mstype: str + content: Any + + @abc.abstractmethod + def export(self, msg_type: Type[TM]) -> MessageSegment[TM]: ... ``` -此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment` +此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment` + +:::tips + +`Custom` 是一个抽象类,你可以通过继承 `Custom` 来实现自定义消息段: + +```python +from nonebot_plugin_alconna.uniseg import Custom, custom_register +from nonebot.adapters.qq import Message as QQMessage, MessageSegment as QQMessageSegment + +class Markdown(Custom): + + def export(self, msg_type: Type[TM]) -> MessageSegment[TM]: + if msg_type is QQMessage: + return QQMessageSegment.markdown(self.content) + +@custom_register(Markdown, "markdown") +def markdown(seg: QQMessageSegment) -> Markdown: + return Markdown("markdown", content=seg.data["content"]) +``` +::: ## 通用消息序列 @@ -127,6 +167,7 @@ matcher = on_xxx(...) @matcher.handle() async def _(message: Message = EventMessage()): msg = await UniMessage.generate(message=message) + msg1 = UniMessage.generate_without_reply(message=message) ``` @@ -148,6 +189,21 @@ 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 @@ -168,24 +224,9 @@ async def tt(target: At): await test_cmd.send(UniMessage([target, "\ndone."])) ``` -除此之外 `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) -``` - :::caution -在响应器以外的地方,`bot` 参数必须手动传入。 +在响应器以外的地方,除非启用了 `alconna_apply_fetch_targets` 配置项,否则 `bot` 参数必须手动传入。 ::: @@ -391,7 +432,7 @@ message.count(Text) == 2 message.get(Text, 1) == UniMessage([Text("test1")]) ``` -### 消息发送 +## 消息发送 前面提到,通用消息可用 `UniMessage.send` 发送自身: @@ -408,6 +449,26 @@ async def send( 实际上,`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 @@ -422,24 +483,76 @@ asycn def _(bot: Bot, event: Event): ``` + + + `send`, `get_target`, `get_message_id` 中与 `event`, `bot` 相关的参数都会尝试从上下文中获取对象。 -其中,`Target`: +### 消息发送对象 + +消息发送对象是用来描述响应消息时的发送对象或者主动发送消息时的目标对象的对象,它包含了以下属性: ```python class Target: id: str """目标id;若为群聊则为group_id或者channel_id,若为私聊则为user_id""" - parent_id: str = "" - """父级id;若为频道则为guild_id,其他情况为空字符串""" - channel: bool = False - """是否为频道,仅当目标平台同时支持群聊和频道时有效""" - private: bool = False + parent_id: str + """父级id;若为频道则为guild_id,其他情况下可能为空字符串(例如 Feishu 下可作为部门 id)""" + channel: bool + """是否为频道,仅当目标平台符合频道概念时""" + private: bool """是否为私聊""" - source: str = "" + 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) +```