Skip to content

Alconna

本章节由 Alconna 作者本人编辑,所以你将会看到

  • 令人窒息的浓度
  • 画风突变的标题
  • 意义不明的日语翻译 熟肉反生

不过放心,梗都有相关注释什么梗百科

「わかります。」

Alconna,全称 Arclet-Alconna, 是由 Arclet Project 维护的一个功能强大的 命令 解析器, 简单一点来讲就是杂糅了多种 CLI 模块(如 clickfire 等)风格的命令解析库(迫真)。

TIPS

  1. Alconna 由两个尼希语 单词组成,alcoconna
  2. ArcletProject 是一个新生社区,欢迎各位来交流♂

凡事都要先安装

TIP

假设你之前安装 Ariadne 时用的是以下 3 种选项中的一种,那么你可以直接跳过本小节。

  • graia-ariadne[full]
  • graia-ariadne[alconna]
  • graia-ariadne[graia,alconna]
bash
# 顺便选一个输进去就完事了
poetry add arclet-alconna[graia]
poetry add arclet-alconna-graia
poetry add graia-ariadne[alconna]
1
2
3
4
bash
# 顺便选一个输进去就完事了
pip install arclet-alconna[graia]
pip install arclet-alconna-graia
pip install graia-ariadne[alconna]
1
2
3
4
bash
# 顺便选一个输进去就完事了
pdm add arclet-alconna[graia]
pdm add arclet-alconna-graia
pdm add graia-ariadne[alconna]
1
2
3
4

缭乱!外星大魔王

开发涩涩Bot时,我们难免会有需求增加一个涩图搜索的命令:

txt
setu搜索 CONTENT
1

这里我们规定用户输入的 content 参数只能是一个图片(Image)或者一个链接(URL)。

我们默认使用 SauceNAO 的 api,但有时候我们也想使用别的搜图引擎而且能自定义参数:

txt
use API:[saucenao|ascii2d|ehentai|iqdb|tracemoe] = saucenao
count NUM:int
threshold VALUE:float
timeout SEC:int
1
2
3
4

如果使用 Twilight 去做,选项之间的处理会比较复杂。

这个时候,天空一声巨响,Alconna 闪亮登场,我们可以使用 Alconna 来实现我们想要的功能:

python
from arclet.alconna import Alconna, Args, CommandMeta, Option
from arclet.alconna.graia.utils import ImgOrUrl

api_list = ["saucenao", "ascii2d", "ehentai", "iqdb", "tracemoe"]
SetuFind = Alconna(
    "setu搜索",
    Args['content', ImgOrUrl],
    Option("use", Args['api', api_list], help_text="选择搜图使用的 API"),
    Option("count", Args.num[int], help_text="设置每次搜图展示的最多数量"),
    Option("threshold", Args.value[float], help_text="设置相似度过滤的值"),
    Option("timeout", Args["sec", int, 60], help_text="设置超时时间"),
    meta=CommandMeta(
        "依据输入的图片寻找可能的原始图片来源",
        usage="可以传入图片, 也可以是图片的网络链接",
        example="setu搜索 [图片]",
    ),
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如此一来,一个命令就创建好了。

接下来,在你的机器人中添加一个用来执行 setu搜索 命令的监听器:

python
from arclet.alconna.graia import AlconnaDispatcher, Match, match_value, Query
from graia.ariadne.util.saya import decorate, dispatch, listen


@listen(GroupMessage)
@dispatch(AlconnaDispatcher(SetuFind, send_flag="reply"))
@decorate(match_value("use.api", "saucenao", or_not=True))
async def ero_saucenao(
    app: Ariadne,
    group: Group,
    content: Match[str],
    max_count: Query[int] = Query("count.num"),
    similarity: Query[float] = Query("threshold.args.value"),
    timeout_sec: Query[int] = Query("timeout.sec", -1),
):
    ...  # setu搜索的处理部分,使用saucenao


@listen(GroupMessage)
@dispatch(AlconnaDispatcher(SetuFind, send_flag="reply"))
@decorate(match_value("use.api", "ascii2d"))
async def ero_ascii2d(
    app: Ariadne,
    group: Group,
    content: Match[str],
    max_count: Query[int] = Query("count.num"),
    similarity: Query[float] = Query("threshold.args.value"),
    timeout_sec: Query[int] = Query("timeout.sec", -1),
):
    ...  # setu搜索的处理部分,使用ascii2d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

准备就绪,对着你的机器人发情发号施令吧:

聊天记录
群菜鸮
setu搜索
涩图
EroEroBot
群菜鸮
setu搜索
正在搜索,请稍后
EroEroBot
工口发生~
群菜龙
群菜鸡
群菜鸮

直面灾厄

左:莱塔尼亚权杖 右:荒地龙舌兰

要想写好一个 Alconna,你首先需要理清楚自己的命令结构

一般,你需要把命令分为四个部分:

  1. 命令名称:作为一个命令的标识符
  2. 命令分隔符:一般是空格,作为参数之间的区分符号
  3. 命令参数:一个命令所需求的主要参数,可以为空
  4. 命令选项:为命令添加额外的解释参数,或以此选择命令的不同功能

TIP

是的,Alconna 负责的并不是消息链解析,而是命令解析虽然说 Alconna 的实现攘括了消息链解析的功能

在上述例子中,setu搜索 是命令名称,<content> 是命令参数,而剩下的 countuse 都是命令选项。

一个命令可以没有命令参数,但一定要有命令名称,这样才称得上健全.jpg

若用一个类来比喻的话,命令参数就是 __init__ 方法的参数,命令名称就是 Class.__name__,命令选项则是该类下的所有类方法。

TIP

Alconna 包含 Fire-Like 的构造方法,支持把传入的对象转换为 Alconna 命令。

python
from arclet.alconna.tools import AlconnaFire


@AlconnaFire
def test_func(name: str, sender_id: int):
    print(f"Hello! [{sender_id}]{name}")


test_func.parse(...)
1
2
3
4
5
6
7
8
9

帮助信息

每个 Alconna 命令 都可以通过 --help-h 触发命令的帮助信息,并且可以通过继承 arclet.alconna.components.output.TextFormatter 来个性化信息样式,如:

python
from arclet.alconna.tools import MarkdownTextFormatter

alc = Alconna("test", Args["count#foo", int], formatter_type=MarkdownTextFormatter)
alc.parse("test --help")

'''
## Unknown

指令:

**test &lt;count:int&gt;**
### 注释:
&#96;&#96;&#96;
count: foo
&#96;&#96;&#96;
'''
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

除此之外, 你可以通过 command_manager 获取当前程序下的所有命令:

python
from arclet.alconna import command_manager
...
print(command_manager.all_command_help())

'''
# 当前可用的命令有:
 - foo : Unknown
 - bar : Unknown
 - baz : Unknown
 - qux : Unknown
# 输入'命令名 --help' 查看特定命令的语法
'''
1
2
3
4
5
6
7
8
9
10
11
12

NOTE

Alconna 可以设置 meta.hide 参数以不被 command_manager 打印出来。

python
foo = Alconna("foo", meta=CommandMeta(hide=True))
...
print(command_manager.all_command_help())

'''
# 当前可用的命令有:
 - bar : Unknown
 - baz : Unknown
 - qux : Unknown
# 输入'命令名 --help' 查看特定命令的语法
'''
1
2
3
4
5
6
7
8
9
10
11

配置

Alconna 有三类配置, 分别是 arclet.alconna.configarclet.alconna.Namespacearclet.alconna.Alconna.config

config 是一个单例,可以控制一些全局属性,如:

python
from arclet.alconna import config

config.fuzzy_threshold = 0.6 # 设置模糊匹配的阈值
config.message_max_cache = 100 # 消息缓存最大数目
1
2
3
4

Namespaceconfig 管理,表示某一命名空间下的默认配置:

python
from arclet.alconna import config, namespace, Namespace


np = Namespace("foo", headers=["/"])  # 创建 Namespace 对象,并进行初始配置

with namespace("bar") as np1:
    np1.headers = ["!"]    # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令


config.namespaces["foo"] = np  # 将命名空间挂载到 config 上
1
2
3
4
5
6
7
8
9
10

config 同时也提供了默认命名空间配置与修改方法:

python
from arclet.alconna import config, namespace, Namespace


config.default_namespace.headers = [...]  # 直接修改默认配置

np = Namespace("xxx", headers=[...])
config.default_namespace = np  # 更换默认的命名空间

with namespace(config.default_namespace.name) as np:
    np.headers = [...]
1
2
3
4
5
6
7
8
9
10

Alconna.config 则是类方法,可以设置默认属性,如:

python
from arclet.alconna import Alconna, set_default
from arclet.alconna.tools impoty ArgparserTextFormatter

Alconna.config(
    behaviors=[set_default(1, "foo")],  # 设置行为器
    formatter_type=ArgparserTextFormatter,  # 设置 formatter 默认为 ArgparserTextFormatter
)
1
2
3
4
5
6
7

使用模糊匹配

模糊匹配是 Alconna 0.8.0 中新增加的特性,通过在 Alconna 中设置其 CommandMeta 开启。

模糊匹配会应用在任意需要进行名称判断的地方,如命令名称选项名称参数名称(如指定需要传入参数名称)。

python
from arclet.alconna import Alconna, CommandMeta

alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True))
alc.parse("test_fuzy")
# output: test_fuzy is not matched. Do you mean "test_fuzzy"?
1
2
3
4
5

自定义语言文件

语言配置是 Alconna 0.8.3 中新增加的特性,为用户提供了自定义报错/输出的接口。

您可以通过 Alconna.load_lang_file 直接更新配置,也可以通过 Alconna.config.lang.set 对单一文本进行修改。

python
from arclet.alconna import Alconna, CommandMeta, config, Option

alc = Alconna("!command", meta=CommandMeta(raise_exception=True)) + Option("--bar", "foo:str")
config.lang.set(
    "analyser.param_unmatched",
    "以下参数没有被正确解析哦~\n: {target}\n请主人检查一下命令是否正确输入了呢~\n不然给你一招雪菜猩红风暴~",
)
alc.parse("!command --baz abc")

'''
output:

ParamsUnmatched: 以下参数没有被正确解析哦~
: --baz
请主人检查一下命令是否正确输入了呢~
不然给你一招雪菜猩红风暴~
'''
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

(你毫无疑问是个雪菜推呢~大西亚步梦:诶 )

半自动补全

半自动补全是 Alconna 1.2.0 中新增加的特性,为用户提供了推荐后续输入的功能。

补全通过 --comp-cp 触发:

python
from arclet.alconna import Alconna, Args, Option

alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar")
alc.parse("test --comp")
alc.parse("test f --comp")

'''
output

下一个输入可能是以下:

- --help
- -h
- foo
- int
- bar

下一个输入可能是以下:
- foo
'''
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

命令组

命令组允许不同的指令经过同一入口解析,适合用于命令结构相似并且参数名称相同的命令。

构造命令组可直接使用 | 操作符:

python
from arclet.alconna.core import Alconna

alc = Alconna("{place1}在哪里") | Alconna("哪里有{place1}")
alc.parse("食物在哪里")
1
2
3
4

或者使用 AlconnaGroup

python
from arclet.alconna.core import Alconna, AlconnaGroup

alc = AlconnaGroup("test", Alconna("{place1}在哪里"), Alconna("哪里有{place1}"))
alc.parse("食物在哪里")
1
2
3
4

命令组的解析表现与单个命令的行为基本一致,若全部命令解析失败则返回最后一个命令的解析结果。

Kirakira☆dokidoki的Dispatcher

Ariadne 中,你可以通过使用 AlconnaDispatcher 来提供消息处理服务:

python
from arclet.alconna.graia import Alconna, AlconnaDispatcher


@app.broadcast.receiver(
    GroupMessage,
    dispatchers=[AlconnaDispatcher(Alconna(...))],
)
async def _(app: Ariadne, group: Group, result: Arpamar):
    ...
1
2
3
4
5
6
7
8
9

AlconnaDispatcher 目前有如下参数:

  • alconna: Alconna本体
  • send_flag: 输出文本的行为,默认为stay,可选值为replypost
    • 'stay': 不处理,原样返回,不能在监听器内获取到输出信息
    • 'reply': AlconnaDispatcher 会自动将输出信息发送给命令发起者
    • 'post': AlconnaDispatcher 会广播一个 AlconnaOutputMessage 事件,你可以通过监听该事件来自定义输出文本的处理方法
  • skip_for_unmatch: 当收到的消息不匹配Alconna时是否跳过,默认为True
  • allow_quote: 当收到的消息是用户回复时,是否继续解析,默认为False
  • send_handler: 对输出文本的处理函数

send_flag 选择 reply,则 AlconnaDispatcher 会自动将输出信息发出。 若 send_flag 选择 post,则 AlconnaDispatcher 会利用 Broadcast 广播一个事件,并将输出信息作为参数发出。

例如,当上例的 send_flagreply 时,可以出现如下情况:

聊天记录
群菜鸮
setu搜索 --help
EroEroBot
setu搜索 <content:Image|url>
依据输入的图片寻找可能的原始图片来源
用法:
可以传入图片, 也可以是图片的网络链接
可用的选项有:
# 选择搜图使用的 API
use <api:'saucenao'|'ascii2d'|'ehentai'|'iqdb'|'tracemoe'>
# 设置每次搜图展示的最多数量
count <num:int>
# 设置相似度过滤的值
threshold <value:float>
# 设置超时时间
timeout <sec:int = 60>
使用示例:
setu搜索 [图片]
群菜龙

参数标注

AlconnaDispatcher 可以分配以下几种参数:

  • Alconna: 使用的 Alconna 对象
  • Arpamar: Alconna 生成的数据容器
  • AlconnaProperty: AlconnaDispatcher 返回的特殊对象,可以获取:
    • help_text: 可能的帮助信息
    • result: Arpamar
    • source: 原始事件
  • 匹配项,如 Match
  • Duplication: Alconna 提供的良好的类型补全容器
  • 匹配的参数,必须保证参数名与参数类型与解析结果中的一致,如content: str
  • etc.

与 Saya 配合使用

Alconna-Graia 在 0.0.12 更新了 Saya 相关的部分, 包括 AlconnaSchameAlconnaBehaviour,如下例:

首先,在 main.py 中记得创建一个 AlconnaBehaviour 并在 Saya 中注册,此处使用 creart 完成相关操作:

python
...
from arclet.alconna.graia.saya import AlconnaBehaviour, AlconnaSchema
from arclet.alconna.graia.dispatcher import AlconnaDispatcher
from arclet.alconna import Alconna, Arpamar
from creart import create
from graia.saya import Saya
from graia.saya.builtins.broadcast import ListenerSchema
...

...
saya = create(Saya)
create(AlconnaBehaviour)
...
1
2
3
4
5
6
7
8
9
10
11
12
13

然后是单个模块中的用法:

python
channel = Channel.current()


@channel.use(AlconnaSchema(AlconnaDispatcher(Alconna("test1", Args.foo[int]))))
@channel.use(ListenerSchema(listening_events=[GroupMessage]))
async def _(app: Ariadne, res: Arpamar):
    ...


@channel.use(AlconnaSchema.from_("test2 <foo:int>"))
@channel.use(ListenerSchema(listening_events=[GroupMessage]))
async def _(app: Ariadne, res: Arpamar):
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13

TIP

近几次更新后已经不需要 AlconnaSchema 来负责管理命令,即直接使用 AlconnaDispatcher 即可。

所以更推荐使用 SayaUtil

python
from arclet.alconna.graia import Alconna, AlconnaDispatcher
from graia.ariadne.util.saya import listen, dispatch


@listen(GroupMessage)
@dispatch(AlconnaDispatcher(Alconna(...)))
async def _(app: Ariadne, result: Arpamar):
    ...
1
2
3
4
5
6
7
8

或者 arclet-alconna-graia 自带的 SayaUtil 组件 alcommand

python
from arclet.alconna.graia import alcommand, Alconna


@alcommand(Alconna(...), private=False, send_error=True)
async def _(app: Ariadne, result: Arpamar):
    ...
1
2
3
4
5
6

该情况默认使用 reply 的 send_flag。

匹配项

arclet-alconna-graia 提供两个特殊类以匹配参数:

  • Match: 查询某个参数是否匹配,如 foo: Match[int]。使用时以 Match.available 判断是否匹配成功,以 Match.result 获取匹配结果。
  • Query: 查询某个参数路径是否存在,如sth: Query[int] = Query("foo.bar");可以指定默认值如 Query("foo.bar", 1234)。使用时以 Query.available 判断是否匹配成功,以 Query.result 获取匹配结果。

便捷构造

alcommand 内联提供了 AlconnaString 的构造方法,如:

python
@alcommand("[!|/]help <...content>;--foo #bar")
async def _(...):
    ...
1
2
3

其等价于

python
alc = Alconna(
    ["!", "/"],
    "help",
    Args["content", AllParam],
    Option("--foo", help_text="bar"),
)


@alcommand(alc)
async def _(...):
    ...
1
2
3
4
5
6
7
8
9
10
11

另外有 from_command,其提供类似 commander 的构造方式:

python
from arclet.alconna.graia import from_command
from graia.ariadne.util.saya import listen


@listen(GroupMessage)
@from_command("call {target}")
async def _(target: At):
    ...
1
2
3
4
5
6
7
8

特殊事件

AlconnaDispatchersend_flagpost 时,其会通过 bcc 广播一个 AlconnaOutputMessage 事件。

该事件可获取的参数如下:

  • help_string(str): 可能的帮助信息
  • alconna (Alconna): 该帮助信息对应的命令
  • sender, message, app, ...: 从源消息事件中可获取的所有参数

特殊类型

arclet-alconna-graia 提供了几个特定的 Args 类型:

  • ImgOrUrl: 表示匹配一个 Image 消息元素或者是代表图片链接的字符串,匹配结果是图片的链接(str)
  • AtID: 表示匹配一个 At 消息元素或者是 @xxxx 式样的字符串或者数字,返回数字(int)

特殊组件

arclet-alconna-graia 提供了一些特定的 Depend 装饰器,或 SayaUtil 组件,如下所示:

fetch_name

fetch_name 是有头的装饰器,负责在机器人功能需要指令发送者提供名字时自动处理名称。

假设某个指令如下:

python
Alconna("发病", Args["name", [str, At], ...])
1

我们希望若指令的 name 存在时,name 是字符串则直接使用,是 At 则用 at 的对象的名称,否则使用发送者的名称,那么仅使用 fetch_name 即可:

python
from arclet.alconna.graia import alcommand, fetch_name


@alcommand(Alconna(...), private=False)
async def _(app: Ariadne, group: Group, name: str = fetch_name("name")):
    ...
1
2
3
4
5
6

fetch_name 的参数 path 表示可能作为名称参数的参数名字,默认为"name"

TIP

fetch_name 直接作为默认值可能会引起某些类型检查器愤怒(是谁呢?一定不是pylance吧)

所以推荐使用 SayaUtildecorate

python
from arclet.alconna.graia import alcommand, fetch_name
from graia.ariadne.util.saya import decorate


@alcommand(Alconna(...), private=False)
@decorate({"name": fetch_name()})
async def _(app: Ariadne, group: Group, name: str):
    ...
1
2
3
4
5
6
7
8

match_path

match_path 用以在命令存在功能细化时帮助解析结果分发到具体的监听器上

假设命令如下:

python
Alconna(
    "功能",
    Option("列出"),
    Option("禁用"),
    Option("启用"),
)
1
2
3
4
5
6

列出禁用启用 以及什么都不做是该命令可能的四种细分的功能。你当然可以把处理部分堆在一个监听器内,如:

python
from arclet.alconna.graia import alcommand

cmd = Alconna(...)


@alcommand(cmd, private=False)
async def handler(app: Ariadne, group: Group, result: Arpamar):
    if not result.components:
        return await app.send_group_message(group, MessageChain(result.source.get_help()))
    if result.find("列出"):
        ...
        return
    if result.find("禁用"):
        ...
        return
    if result.find("启用"):
        ...
        return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

毫无疑问,这种写法会让功能负责的命令的处理器看起来十分庞大。

于是使用 match_path

python
from arclet.alconna.graia import alcommand, match_path
from graia.ariadne.util.saya import decorate

cmd = Alconna(...)


@alcommand(cmd, private=False)
@decorate(match_path("$main"))
async def _(app: Ariadne, group: Group, result: Arpamar):
    return await app.send_group_message(group, MessageChain(result.source.get_help()))


@alcommand(cmd, private=False)
@decorate(match_path("列出"))
async def _(app: Ariadne, group: Group, result: Arpamar):
    ...

@alcommand(cmd, private=False)
@decorate(match_path("禁用"))
async def _(app: Ariadne, group: Group, result: Arpamar):
    ...

@alcommand(cmd, private=False)
@decorate(match_path("启用"))
async def _(app: Ariadne, group: Group, result: Arpamar):
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

match_path 的参数 path 表示分发需要匹配的选项或子命令,当 path$main 时表示匹配无选项的情况。

match_value

match_value 的功能与 match_path 类似,但允许对匹配值进行判断。

例如某个命令携带固定参数:

python
Alconna("test", Args["level", ["info", "debug", "error"]])
1

你当然可以把处理部分堆在一个监听器内,如:

python
from arclet.alconna.graia import alcommand, Match

cmd = Alconna(...)


@alcommand(cmd, private=False)
async def handler(app: Ariadne, group: Group, level: Match[str]):
    if level.result == "info":
        ...
        return
    if level.result == "debug":
        ...
        return
    if level.result == "error":
        ...
        return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

但你可以这样写:

python
from arclet.alconna.graia import alcommand, match_value
from graia.ariadne.util.saya import decorate

cmd = Alconna(...)


@alcommand(cmd, private=False)
@decorate(match_value("level", "info"))
async def _(app: Ariadne, group: Group):
    ...

@alcommand(cmd, private=False)
@decorate(match_value("level", "debug"))
async def _(app: Ariadne, group: Group):
    ...

@alcommand(cmd, private=False)
@decorate(match_value("level", "error"))
async def _(app: Ariadne, group: Group):
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

match_value 的参数 or_not 允许在参数不存在时视作匹配成功,适合在判断路径为选项参数时使用。

assign

assignmatch_pathmatch_value 的合体并装饰器化的组件。

使用 assign 时建议放置在 alcommand 上方:

python
from arclet.alconna.graia import alcommand, assign

cmd = Alconna(...)


@assign("list")
@alcommand(cmd, private=False)
async def _(app: Ariadne, group: Group):
    ...

@assign("level", "info")
@alcommand(cmd, private=False)
async def _(app: Ariadne, group: Group):
    ...

@assign("input", "123", or_not=True)
@alcommand(cmd, private=False)
async def _(app: Ariadne, group: Group):
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

path 除了指定选项名或者子命令名称,也可以指定到具体的参数上。

例如 foo.barfoo.args.bar 指向选项 foo 的参数 bar, 而 foo.value 指向 foo 本身的值,可看作是否解析到 foo

前缀/后缀匹配

除开常见的命令匹配,alc-graia 也提供了简单的前缀匹配与后缀匹配。

python
from arclet.alconna.graia import endswith, startswith
from graia.ariadne.util.saya import listen


@listen(GroupMessage)
@startswith("Hello")
async def _(app: Ariadne, group: Group):
    ...

@listen(GroupMessage)
@startswith("shell", bind="message")  # bind 参数用于指定将匹配结果注入给哪个参数
async def _(app: Ariadne, group: Group, message: MessageChain):
    ...


@listen(GroupMessage)
@endswith("url", include=True)  # include 参数用于指定注入内容是匹配到的还是剩余的
async def _(app: Ariadne, group: Group, url: MessageChain):
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

得益于NEPattern,你可以进行如下匹配:

  • 常规字符串,如 startswith("foo")
  • 其他消息元素类型,如 startswith(At(...))startswith(Image)
  • 正则,如 endswith("<[^<>]+>")startswith("re:(bar|baz)")
  • Alconna 支持的自动类型转化,如 startswith(int)
  • 函数,如 startswith(lambda x: x if isinstance(x, str) and x.isdigit() else None)
  • typing,如 startswith(Dict[str, int])
  • 联合匹配,如 startswith(['!', '/', At])

TIP

两个 SayaUtil 同样存在各自的 Decorator 形式,为 MatchPrefix 与 MatchSuffix

亮出你的本事吧!外星人

「やってみせろよ、ウチュウジンー!」

创建 Alconna

以下将展示 Alconna 创建的 5 种方式:

python
from arclet.alconna import Args
from arclet.alconna.graia import AlconnaDispatcher

alc = Alconna("我要涩图", Args["count", int])


@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alc)],
    )
)
async def test(app: Ariadne, group: Group):
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python
from arclet.alconna.graia import AlconnaDispatcher
from arclet.alconna.tools import AlconnaString

alc = AlconnaString("我要涩图 <count:int>")


@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alc)],
    )
)
async def test(app: Ariadne, group: Group):
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python
from arclet.alconna.graia import AlconnaDispatcher
from arclet.alconna.tools import AlconnaFormat

alc = AlconnaFormat("我要涩图 {count}", {"count": int})


@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alc)],
    )
)
async def test(app: Ariadne, group: Group):
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python
from arclet.alconna.graia import AlconnaDispatcher
from arclet.alconna.tools import AlconnaDecorate

cli = AlconnaDecorate()


@cli.build_command("我要涩图")
@cli.argument(Args["count", int])
def setu(count: int):
    ...


@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(setu.command)],
    )
)
async def test(app: Ariadne, group: Group):
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python
from arclet.alconna import.tools AlconnaFire
from arclet.alconna.graia import AlconnaDispatcher


def give_me_setu(count: int):
    class Config:
        command=我要涩图
    ...


alc = AlconnaFire(give_me_setu)


@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alc)],
    )
)
async def test(app: Ariadne, group: Group):
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

NOTE

自 Alconna 1.3 以来,除开 typical 构建方法外,剩余四种需要安装 arclet-alconna-tools 才能使用。

代码解析

上面的代码中展示了五种 Alconna 的创建方式。

下面我们将一一说明这五种方法的细节。

标准形式:直接使用 Alconna(...)

标准形式中,你需要传入较多的命令组件,但同时其可以清晰地表达命令结构。

目前的命令组件有 OptionSubcommandArgs

这样创建的 Alconna 实例又长什么样呢?

python
>>> Alconna(
...     "我要涩图",  # command
...     Args["count", int],  # main_args
...     Option("从", Args["tag;S", str]),  # options
... )
Alconna::我要涩图(args=Args('count': int), options=[help, shortcut(args=Args('delete', 'name': str, 'command': str = '_')), comp, (args=Args('tag': *(str)))])
1
2
3
4
5
6

command传入的便是命令名称,main_args 是命令参数,options 则是命令选项。

Args是命令参数的载体,通过"键-值-默认"传入一系列参数,具体食用方法我们后面会讲到。

Alconna 0.7.6 后,简易的命令构造可用如下方法:

python
>>> alc = Alconna("我要涩图", Args.count[int]) + Option("--from", "tag;S:str")
1

即可以用 + 增加选项或子命令。

Koishi-like:使用 AlconnaString(...)

Koishi-like 方法中,你可以用类似 Koishi 中编写命令的格式来构造 Alconna

上面的例子中,我们期望的命令是这样的一串字符串:我要涩图 2 从 纯爱 兽耳

该命令以“我要涩图”作为前缀,同时需要一个参数,其以 count 为名字,并且类型为 int, 然后允许一个选项,其名称为“”,需要不定个参数,其以 tag 为名字,并且每个参数类型为 str

于是我们就得到了如下的 Alconna 实例:

python
>>> AlconnaString("我要涩图 <count:int>", "从 <tag;S:str>")
Alconna::我要涩图(args=Args('count': int), options=[help, shortcut(args=Args('delete', 'name': str, 'command': str = '_')), comp, (args=Args('tag': *(str)))])
1
2

可以看到,我们的 <count:int> 变成了 Args['count', int]

TIP

关于 Koishi-like 的命令参数,请详细阅读 命令参数参数编写规则 来编写

Format:使用 AlconnaFormat(...)

format方法中,你可以用 f-string 的格式来构造 Alconna。

仍以上面的命令为例,我们相当于输入了这样一串字符串:我要涩图 {count} 从 {*tags} 于是我们就得到了如下的 Alconna 实例:

python
>>> AlconnaFormat("我要涩图 {count:int} 从 {tags;S}", {"tags;S": str})
Alconna::我要涩图(args=Args('count': int), options=[help, shortcut(args=Args('delete', 'name': str, 'command': str = '_')), comp, (args=Args('tag': *(str)))])
1
2

Fire-Like:使用 AlconnaFire(...)

Fire-like 允许你传入任意的参数(主要是函数、类、实例、模块),Alconna 会尝试提取命令相关参数, 并构建为 Alconna

仍以上面的命令为例,我们相当于构造了一个类 Class:我要涩图,其需要传入 count 参数来实例化, 并写有一个方法 ,该方法接受一个不定参数 tags;S。 于是我们就得到了如下的 Alconna 实例:

python
>>> class Setu:
...     def __init__(self, count:int):
...         self.count = count
...     def (self, *tags: str):
...         ...
...
>>> AlconnaFire(Setu, config={"command": "我要涩图"})
Alconna::我要涩图(args=Args('count': int), options=[help, shortcut(args=Args('delete', 'name': str, 'command': str = '_')), comp, (args=Args('tag': *(str)))])
1
2
3
4
5
6
7
8

组件

Alconna 拥有两大组件:OptionSubcommand

Option

Option 可以传入一组 alias

python
Option("--foo", alias=["-F", "--FOO", "-f"])
1

那么-f-F--FOO将等同于--foo

另外也可以用如 Option("--foo|-F|--FOO|-f") 来指定别名。

Subcommand

Subcommand 可以传入自己的 Option

python
Subcommand("sub", options=[Option("sub_opt")])
1

此时 sub_opt 必须在 sub 被输入时才算作合法选项,即:

  • sub ... sub_opt ...
  • sub_opt ... sub ...

除此之外,OptionSubcommand 拥有如下共同参数:

help_text

传入该组件的帮助信息

action

传入针对该组件的参数行为器,一般是一个函数

dest

dest 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name)

requires

requires 是一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换

对于命令 test foo bar baz qux <a:int> 来讲,因为foo bar baz 仅需要判断是否相等, 所以可以这么编写:

python
Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"]))
1

TIP

requires 也可以在 name 中传入
譬如:

python
Option("foo bar baz qux")
1

总会有参数的

「何とでもなるはずだパラメータ!」

Args

Args 在 Alconna 中有非常重要的地位,甚至称得上是核心,比 Alconna 重要十倍甚至九倍。

其通常以 Args[key1, var1, default1][key2, var2][...] 的方式构造一个 Args。

其中,key 一定是字符串,而 var 一般为参数的类型,default 为具体的值。

var

var 可以是以下几类:

  • 存在于 nepattern.pattern_map 中的类型/字符串,用以替换为预制好的 BasePattern
  • 字符串,会转换为正则表达式
  • 列表,其中可存放 BasePattern、类型或者任意参数值,如字符串或者数字
  • UnionOptionalLiteral 等会尝试转换为 List[Type]
  • Dict[type1,type2]List[type]Set[type]
  • 一般的类型,其会尝试比较传入参数的类型是否与其相关
  • AnyOneAllParam,作为泛匹配的标识符
  • 预制好的字典, 表示传入值依据该字典的键决定匹配结果
  • Annotated[type, Callable[..., bool], ...],表示为某一类型添加校验器
  • Callable[[P], T],表示会将传入的参数 P 经过该调用对象并将返回值 T 作为匹配结果
  • ...

内置的类型检查包括 intstrfloatbool'url''ip''email'listdicttuplesetAnybyteshexdatetime 等。

NOTE

若想增加类型检查,我们可以通过 nepattern.set_converter 传入自己的 BasePattern:

python
from nepattern import set_converter

>>> set_converter(
...     BasePattern(
...         "app",
...         PatternModel.REGEX_CONVERT,
...         Ariadne,
...         lambda x: app,
...         'app',
...     )
... )
1
2
3
4
5
6
7
8
9
10
11

或通过 arclet.alconna.tools.pattern import ObjectPattern 传入一个类型来向 pattern_map 中注册检查类型:

python
from arclet.alconna.tools.pattern import ObjectPattern

ObjectPattern(Image, limit=("url",))
1
2
3

key

key的作用是用以标记解析出来的参数并存放于 Arpamar 中,以方便用户调用。

其有七种为 Args 注解的标识符,为 SWAFKOH。标识符应与 key 以 ; 分隔:

  • S 标识符表示当前参数为可变长非键值对参数,类似函数中的 *args,可以传入 0 至任意个参数。
  • W 标识符表示当前参数为可变长键值对参数,类似函数中的 **kwargs,可传入 0 至任意个参数。
  • A 标识符表示该处传入的参数应不是规定的类型,或不在指定的值中。
  • F 标识符表示该参数的类型不经过类型转换。
  • K 标识符表示该参数需要键值对匹配,即 key=var 的形式。
  • O 标识符表示该参数为可选参数,会在无参数匹配时跳过。
  • H 标识符表示该参数的类型注解需要隐藏。

另外,正整数也是可以作为标识符的,其会作为 S 的限制性操作。如 key;3 表示需要传入 0 至 3 个参数。

ArgField

ArgField 放置于 default 位,用以指定此处 Arg 的默认值、别名与补全信息。

若传入的 default 值不是 ArgField,程序会自动生成。

BasePattern

BasePattern 是 Alconna 中对正则解析的拓展,负责对传入参数的检查与类型转换。

例如我想把如 'sth1/sth2/sth3/sth4' 的参数在解析后变成类似 ['sth1', 'sth2', 'sth3', 'sth4'] 这样子。

那么我可以这样编写一个 BasePattern:

python
from nepattern import BasePattern, PatternModel

my_list = BasePattern(
    "(.+/.*)",
    model=PatternModel.REGEX_CONVERT,
    origin=list,
    converter=lambda x: x.split('/'),
    alias='my_list',
    accepts=[str],
)
1
2
3
4
5
6
7
8
9
10

并在创建 Alconna 时使用:

python
...
alc = Alconna(".command", Args["foo", my_list])
1
2

此时输入 '.command usr/bin/python',则 foo 将被解析为 ['usr', 'bin', 'python']

Arpamar

Alconna.parse 会返回由 Arpamar 承载的解析结果。

Arpamar 会有如下参数:

  • 调试类

    • matched: 是否匹配成功
    • head_matched: 命令头部是否匹配成功
    • error_data: 解析失败时剩余的数据
    • error_info: 解析失败时的报错信息
    • origin: 原始命令,可以类型标注
    • source: 使用的 Alconna
  • 分析类

    • main_args: 命令的主参数的解析结果
    • options: 命令所有选项的解析结果
    • subcommands: 命令所有子命令的解析结果
    • other_args: 除主参数外的其他解析结果
    • all_matched_args: 所有 Args 的解析结果
    • header: 当命令头部填入有效表达式时的解析结果

老规矩,直接上实例:

python
from arclet.alconna import Alconna, Args, Arpamar, Option, Subcommand
from arclet.alconna.graia import alcommand


@alcommand(
    Alconna(
        "找歌", Args["song", str],
        Option("语种", Args["lang", str]),
        Subcommand("歌手", [Option("地区", Args["region", str])], Args["singer", str]),
    ),
    private=False,
)
async def lyric_xxx(app: Ariadne, group: Group, result: Arpamar[MessageChain]):
    print(result.matched)
    print(result.origin)
    print(result.error_info)
    print(result.options)
    print(result.song)
    if result.find("语种"):
        print(result.query("语种.lang"))
    if result.find("歌手"):
        print(result.query("歌手.singer"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Arpamar Behavior

ArpamarBehavior 是负责解析 Arpamar 行为的类,用来更精细的预处理结果。

Alconna 目前预制了三种 Behavior,分别用来:

  • set_default: 当某个选项未被输入时,使用该行为添加一个默认值
  • exclusion: 当指定的两个选项同时出现时报错
  • cool_down: 限制命令调用频率
python
from arclet.alconna import Alconna, Args
from arclet.alconna.tools import cool_down

alc2 = Alconna(
    "test_cool_down",
    Args["bar":int],
    behaviors=[cool_down(0.2)],
)

for i in range(4):
    time.sleep(0.1)
    print(alc2.parse("test_cool_down {}".format(i)))

>>> matched=False, head_matched=True, error_data=[], error_info=操作过于频繁
>>> matched=True, head_matched=True, main_args={'bar': 1}
>>> matched=False, head_matched=True, error_data=[], error_info=操作过于频繁
>>> matched=True, head_matched=True, main_args={'bar': 3}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Duplication

Duplication 用来提供更好的自动补全,类似于 ArgParseNamespace,经测试表现良好(好耶)。

普通情况下使用,需要利用到 ArgsStubOptionStubSubcommandStub 三个部分,

仍以上方命令为例,其对应的 Duplication 应如下构造:

python
from arclet.alconna import ArgsStub, Duplication, OptionStub

class MyDup(Duplication):
    my_args: ArgsStub
    从: OptionStub  # 选项与子命令对应的stub的变量名必须与其名字相同
1
2
3
4
5

并在解析时传入 Duplication:

python
result = alc.parse("我要涩图 2", duplication=MyDup)
>>> type(result)
<class MyDup>
1
2
3

TIP

同样,在 AlconnaDispatcher 中也可以使用 Duplication,你只需要如下操作:

python
@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alc)],
    )
)
async def test(app: Ariadne, group: Group, dup: MyDup):
    print(dup.my_args.availabe)
1
2
3
4
5
6
7
8

亦或者,你可以直接使用 Stub 作为参注解:

python
@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alc)],
    )
)
async def test(app: Ariadne, group: Group, y_args: ArgsStub):
    print(my_args.availabe)
1
2
3
4
5
6
7
8

Duplication 也可以如 Namespace 一样直接标明参数名称和类型:

python
from typing import Tuple

from arclet.alconna import Duplication


class MyDup(Duplication):
    count: int
    tag: Tuple[str, ...]
1
2
3
4
5
6
7
8

该用法下需要确保属性存在

居然是整活?

「コッケイナだと!」

元素匹配

一定要记住,Alconna 是支持元素匹配的(Plain 元素或 Source 等元素除外)。

假设某个命令需要传入名字,但你也想能够直接用 @ 来指定目标, 那么可以直接这么写:

python
from arclet.alconna import Alconna, Args
from graia.ariadne.message.element import At

ill = Alconna(
    "发病",
    Args["target", [At, int]],
    headers=["EroEro", "!"],
)
1
2
3
4
5
6
7
8

头部 Pattern

Alconna 的 command 同样可以接受 类型 或者 BasePattern

python
from typing import Annotated

from arclet.alconna import Alconna

number = Alconna(int, help_text="输入数字")
digit = Alconna(Annotated[int, lambda x: x>0], help_text="输入正整数")
1
2
3
4
5
6

At 等元素同样可以放置于 headers 里:

python
from arclet.alconna import Alconna, Args
from graia.ariadne.message.element import At

ill = Alconna(
    "发病",
    Args["target", [At, int]],
    headers=[At(123456789)],
)
1
2
3
4
5
6
7
8

此时你需要输入 @123456789 发病 xxxx 才能执行命令

快捷指令

基于对传入消息的记录,Alconna 0.9.0 以上支持动态的快捷指令构建:

text
>>> my_command --shortcut XXX "my_command foo bar ..."
1

或者

text
>>> my_command foo bar ...
>>> my_command --shortcut XXX
1
2

TIP

该方法构建的快捷指令在 bot 生命周期结束后会一并销毁,但可以通过 Alconna 的 CommandManager 来保存:

python
from pathlib import Path

from arclet.alconna import command_manager
...

command_manager.cache_path = Path(__file__).parent / "my_cache.db"
command_manager.dump_cache()
1
2
3
4
5
6
7

不规则命令

Alconna 对于命令头部 command 应用有特殊的构建规则。

其可以像 AlconnaFormat 那样通过 'xxx{name:type or pattern}xxx' 来生成正则表达式,并将匹配结果传递给 Arpamar.header

其中 nametype 都可以留空, type 留空时当作'str'

类似 .r100 或者 查询XX人品 的指令,这么写就好了:

python
from arclet.alconna import Alconna, Arpamar
from arclet.alconna.graia import AlconnaDispatcher

dice = Alconna(".r{dice:int}")


@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(dice)],
    )
)
async def roll_dice(app: Ariadne, group: Group, result: Arpamar):
    dice_count = result.header.get('dice')
    print(dice_count)
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

自定义分隔符

你可以传入一个 separator 的参数,来作为命令参数之间的分隔符。

类似 告诉我 谁是xxx和xxx 的指令,这么写就好了:

python
from arclet.alconna import Alconna, Args, Option, Arpamar
from arclet.alconna.graia import AlconnaDispatcher

who = Alconna("告诉我") + Option("谁", Args['target;S', str] / "和", separator="是")

@channel.use(
    ListenerSchema(
        listening_events=[GroupMessage],
        inline_dispatchers=[AlconnaDispatcher(alconna=who)],
    )
)
async def find(app: Ariadne, group: Group, result: Arpamar):
    targets = result.target
    print(targets)
    ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

隐式构建 Args

在 Alconna 0.7.2 后,args 可以由传入的 action 生成:

python
from arclet.alconna import Alconna


def test(foo: str, bar: int, baz: bool):
    ...

tes = Alconna("command", action=test)
print(tes.args)

>>> "Args('foo': str, 'bar': int, 'baz': bool)"
1
2
3
4
5
6
7
8
9
10

减少 Option 的使用

利用 KO 前缀,我们可以在 Args 中模拟出一个 option:

python
from arclet.alconna import Alconna, Args

alc = Alconna("cut_img", Args["--width;OK", int, 1280]["--height;OK", int, 720])
alc.parse("cut_img --height=640")

>>> matched=True, head_matched=True, main_args={"--width": 1280, "--height":640}
1
2
3
4
5
6

前面的区域,以后再来探索吧

「わかります」

Tip:

本文档使用 CC BY-NC-SA 4.0 协议进行共享,详情见 README

MIT License