From f527aa9b40bc4e4a3556eb8f162afc6cf4192ba0 Mon Sep 17 00:00:00 2001 From: Him188 <Him188@mamoe.net> Date: Fri, 17 Jun 2022 13:11:35 +0100 Subject: [PATCH] Update docs for commands --- docs/ConsoleTerminal.md | 1 + .../mirai-console/src/command/CommandOwner.kt | 6 +- .../mirai-console/src/command/RawCommand.kt | 19 +- .../src/command/SimpleCommand.kt | 10 +- .../src/command/java/JCompositeCommand.kt | 2 +- .../src/command/java/JRawCommand.kt | 11 +- .../mirai-console/src/util/MessageScope.kt | 35 +- mirai-console/docs/Commands.md | 835 +++++++++++++----- 8 files changed, 657 insertions(+), 262 deletions(-) diff --git a/docs/ConsoleTerminal.md b/docs/ConsoleTerminal.md index 43f2754ce..ae313191e 100644 --- a/docs/ConsoleTerminal.md +++ b/docs/ConsoleTerminal.md @@ -190,6 +190,7 @@ Console 会自动根据语境推断指令参数的含义。 |----------------|------------------------|----------------------| | 机器人号码.群号码.群员号码 | `123456.123456.987654` | 一个机器人的一个群中的一个群员 | | 机器人号码.群号码.群员名片 | `123456.123456.Alice` | 一个机器人的一个群中的一个群员 | +| 提及群员 | `@Cinnamon` | 一个机器人的一个群中的一个群员 | | 机器人号码.群号码.$ | `123456.123456.$` | 一个机器人的一个群中的随机群员 | | 群号码.群员号码 | `123456.987654` | 当前唯一在线机器人的一个群的一个群员 | | 群号码.群员名片 | `123456.Alice` | 当前唯一在线机器人的一个群的一个群员 | diff --git a/mirai-console/backend/mirai-console/src/command/CommandOwner.kt b/mirai-console/backend/mirai-console/src/command/CommandOwner.kt index bb1f013c3..580ac42ad 100644 --- a/mirai-console/backend/mirai-console/src/command/CommandOwner.kt +++ b/mirai-console/backend/mirai-console/src/command/CommandOwner.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 Mamoe Technologies and contributors. * * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. @@ -17,12 +17,10 @@ import net.mamoe.mirai.console.permission.PermissionIdNamespace import net.mamoe.mirai.console.plugin.jvm.JvmPlugin /** - * 指令的所有者. + * 指令的所有者. [JvmPlugin] 是一个 [CommandOwner]. * * @see CommandManager.unregisterAllCommands 取消注册所有属于一个 [CommandOwner] 的指令 * @see CommandManager.registeredCommands 获取已经注册了的属于一个 [CommandOwner] 的指令列表. - * - * @see JvmPlugin 是一个 [CommandOwner] */ public interface CommandOwner : PermissionIdNamespace { /** diff --git a/mirai-console/backend/mirai-console/src/command/RawCommand.kt b/mirai-console/backend/mirai-console/src/command/RawCommand.kt index 50564f06f..c823ec362 100644 --- a/mirai-console/backend/mirai-console/src/command/RawCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/RawCommand.kt @@ -21,10 +21,21 @@ import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.buildMessageChain /** - * 无参数解析, 接收原生参数的指令. + * 无参数解析, 只会接收原消息链的指令. Java 查看 [JRawCommand]. * - * ### 指令执行流程 - * 继 [CommandManager.executeCommand] 所述第 3 步, [RawCommand] 不会对参数做任何解析. + * ```kotlin + * object MyCommand : RawCommand( + * MyPluginMain, "name", // 使用插件主类对象作为指令拥有者;设置主指令名为 "name" + * // 可选: + * "name2", "name3", // 增加两个次要名称 + * usage = "/name arg1 arg2", // 设置用法,将会在 /help 展示 + * description = "这是一个测试指令", // 设置描述,将会在 /help 展示 + * prefixOptional = true, // 设置指令前缀是可选的,即使用 `test` 也能执行指令而不需要 `/test` + * ) { + * override suspend fun CommandContext.onCommand(args: MessageChain) { + * } + * } + * ``` * * @see JRawCommand 供 Java 用户继承. * @@ -33,7 +44,7 @@ import net.mamoe.mirai.message.data.buildMessageChain */ public abstract class RawCommand( /** - * 指令拥有者. + * 指令拥有者. 通常建议使用插件主类. * @see CommandOwner */ @ResolveContext(RESTRICTED_CONSOLE_COMMAND_OWNER) diff --git a/mirai-console/backend/mirai-console/src/command/SimpleCommand.kt b/mirai-console/backend/mirai-console/src/command/SimpleCommand.kt index eb1e1d51b..7647143e7 100644 --- a/mirai-console/backend/mirai-console/src/command/SimpleCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/SimpleCommand.kt @@ -1,10 +1,10 @@ /* - * Copyright 2019-2021 Mamoe Technologies and contributors. + * Copyright 2019-2022 Mamoe Technologies and contributors. * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. * - * https://github.com/mamoe/mirai/blob/master/LICENSE + * https://github.com/mamoe/mirai/blob/dev/LICENSE */ package net.mamoe.mirai.console.command @@ -43,6 +43,8 @@ import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER * } * ``` * + * 其中 `CommandSender` 也可以替换为 `CommandContext`,可通过 [CommandContext.originalMessage] 获得触发指令的原消息链。 + * * @see JSimpleCommand Java 实现 * @see [CommandManager.executeCommand] */ diff --git a/mirai-console/backend/mirai-console/src/command/java/JCompositeCommand.kt b/mirai-console/backend/mirai-console/src/command/java/JCompositeCommand.kt index f9dfdac14..83469e5ef 100644 --- a/mirai-console/backend/mirai-console/src/command/java/JCompositeCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/java/JCompositeCommand.kt @@ -110,7 +110,7 @@ public abstract class JCompositeCommand * 增加智能参数解析环境 * @since 2.12 */ - protected open fun addArgumentContext(context: CommandArgumentContext) { + protected fun addArgumentContext(context: CommandArgumentContext) { this.context += context } } \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt b/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt index 63f4bd78e..644ef7623 100644 --- a/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt +++ b/mirai-console/backend/mirai-console/src/command/java/JRawCommand.kt @@ -30,16 +30,15 @@ import net.mamoe.mirai.utils.runBIO * public final class MyCommand extends JRawCommand { * public static final MyCommand INSTANCE = new MyCommand(); * private MyCommand() { - * super(MyPluginMain.INSTANCE, "test") + * super(MyPluginMain.INSTANCE, "test"); // 使用插件主类对象作为指令拥有者;设置主指令名为 "test" * // 可选设置如下属性 - * setUsage("/test") - * setDescription("这是一个测试指令") - * setPermission(CommandPermission.Operator.INSTANCE) - * setPrefixOptional(true) + * setUsage("/test"); // 设置用法,这将会在 /help 中展示 + * setDescription("这是一个测试指令"); // 设置描述,也会在 /help 中展示 + * setPrefixOptional(true); // 设置指令前缀是可选的,即使用 `test` 也能执行指令而不需要 `/test` * } * * @Override - * public void onCommand(@NotNull CommandSender sender, @NotNull args: Object[]) { + * public void onCommand(@NotNull CommandSender sender, @NotNull MessageChain args) { * // 处理指令 * } * } diff --git a/mirai-console/backend/mirai-console/src/util/MessageScope.kt b/mirai-console/backend/mirai-console/src/util/MessageScope.kt index f5a55a002..0957299d7 100644 --- a/mirai-console/backend/mirai-console/src/util/MessageScope.kt +++ b/mirai-console/backend/mirai-console/src/util/MessageScope.kt @@ -68,15 +68,16 @@ import kotlin.internal.LowPriorityInOverloadResolution * * 由于 [CommandSender] 与 [Contact] 无公共接口, 无法使用 [listOfNotNull] 遍历处理. [MessageScope] 就是设计为解决这样的问题. * + * *Kotlin* * ``` - * // 在一个 CompositeCommand 内 + * // 在一个 SimpleCommand 内 * @Handler * suspend fun CommandSender.handle(target: Member) { * val duration = Random.nextInt(1, 15) * target.mute(duration) * * - * // 不使用 MessageScope, 无用的样板代码 + * // 不使用 MessageScope * val thisGroup = this.getGroupOrNull() * val message = "${this.name} 禁言 ${target.nameCardOrNick} $duration 秒" * if (target.group != thisGroup) { @@ -85,7 +86,7 @@ import kotlin.internal.LowPriorityInOverloadResolution * sendMessage(message) * * - * // 使用 MessageScope, 清晰逻辑 + * // 使用 MessageScope * // 表示至少发送给 `this`, 当 `this` 的真实发信对象与 `target.group` 不同时, 还额外发送给 `target.group` * this.scopeWith(target.group) { * sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒") @@ -99,6 +100,34 @@ import kotlin.internal.LowPriorityInOverloadResolution * // ) { ... } * } * ``` + * + * *Java* + * ```java + * // 在一个 SimpleCommand 内 + * @Handler + * public void handle(sender: CommandSender, target: Member) { + * int duration = Random.nextInt(1, 15); + * target.mute(duration); + * + * + * // 不使用 MessageScope + * Group thisGroup = this.getGroupOrNull(); + * String message = "${this.name} 禁言 ${target.nameCardOrNick} $duration 秒"; + * if (!target.group.equals(thisGroup)) { + * target.group.sendMessage(message); + * } + * sender.sendMessage(message); + * + * + * // 使用 MessageScope + * // 表示至少发送给 `this`, 当 `this` 的真实发信对象与 `target.group` 不同时, 还额外发送给 `target.group` + * MessageScope scope = MessageScopeKt.scopeWith(sender, target); + * scope.sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒"); + * + * // 或是只用一行: + * MessageScopeKt.scopeWith(sender, target).sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒"); + * } + * ``` */ public sealed interface MessageScope { /** diff --git a/mirai-console/docs/Commands.md b/mirai-console/docs/Commands.md index bcf9eb838..913d588ca 100644 --- a/mirai-console/docs/Commands.md +++ b/mirai-console/docs/Commands.md @@ -1,296 +1,586 @@ # Mirai Console Backend - Commands - [`Plugin`]: ../backend/mirai-console/src/plugin/Plugin.kt + [`PluginDescription`]: ../backend/mirai-console/src/plugin/description/PluginDescription.kt + [`PluginLoader`]: ../backend/mirai-console/src/plugin/loader/PluginLoader.kt + [`PluginManager`]: ../backend/mirai-console/src/plugin/PluginManager.kt + [`JvmPluginLoader`]: ../backend/mirai-console/src/plugin/jvm/JvmPluginLoader.kt + [`JvmPlugin`]: ../backend/mirai-console/src/plugin/jvm/JvmPlugin.kt + [`JvmPluginDescription`]: ../backend/mirai-console/src/plugin/jvm/JvmPluginDescription.kt + [`AbstractJvmPlugin`]: ../backend/mirai-console/src/plugin/jvm/AbstractJvmPlugin.kt + [`KotlinPlugin`]: ../backend/mirai-console/src/plugin/jvm/KotlinPlugin.kt + [`JavaPlugin`]: ../backend/mirai-console/src/plugin/jvm/JavaPlugin.kt [`Value`]: ../backend/mirai-console/src/data/Value.kt + [`PluginData`]: ../backend/mirai-console/src/data/PluginData.kt + [`AbstractPluginData`]: ../backend/mirai-console/src/data/AbstractPluginData.kt + [`AutoSavePluginData`]: ../backend/mirai-console/src/data/AutoSavePluginData.kt + [`AutoSavePluginConfig`]: ../backend/mirai-console/src/data/AutoSavePluginConfig.kt + [`PluginConfig`]: ../backend/mirai-console/src/data/PluginConfig.kt + [`PluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt + [`MultiFilePluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt#L116 + [`MemoryPluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt#L100 + [`AutoSavePluginDataHolder`]: ../backend/mirai-console/src/data/PluginDataHolder.kt#L45 + [`PluginDataHolder`]: ../backend/mirai-console/src/data/PluginDataHolder.kt + [`PluginDataExtensions`]: ../backend/mirai-console/src/data/PluginDataExtensions.kt [`MiraiConsole`]: ../backend/mirai-console/src/MiraiConsole.kt + [`MiraiConsoleImplementation`]: ../backend/mirai-console/src/MiraiConsoleImplementation.kt <!--[MiraiConsoleFrontEnd]: ../backend/mirai-console/src/MiraiConsoleFrontEnd.kt--> [`Command`]: ../backend/mirai-console/src/command/Command.kt + [`Register`]: ../backend/mirai-console/src/command/CommandManager.kt#L77 + [`AbstractCommand`]: ../backend/mirai-console/src/command/Command.kt#L90 + [`CompositeCommand`]: ../backend/mirai-console/src/command/CompositeCommand.kt + [`SimpleCommand`]: ../backend/mirai-console/src/command/SimpleCommand.kt + [`RawCommand`]: ../backend/mirai-console/src/command/RawCommand.kt + [`CommandManager`]: ../backend/mirai-console/src/command/CommandManager.kt + [`CommandSender`]: ../backend/mirai-console/src/command/CommandSender.kt + [`CommandValueArgumentParser`]: ../backend/mirai-console/src/command/descriptor/CommandValueArgumentParser.kt + [`CommandArgumentContext`]: ../backend/mirai-console/src/command/descriptor/CommandArgumentContext.kt + [`CommandArgumentContext.BuiltIns`]: ../backend/mirai-console/src/command/descriptor/CommandArgumentContext.kt#L66 [`MessageScope`]: ../backend/mirai-console/src/util/MessageScope.kt -## [`Command`] ->「指令」:目前通常是 "/commandName arg1 arg2 arg3" 格式的消息。在将来可能会被扩展 +"指令" 目前通常是 "/commandName arg1 arg2 arg3" 格式的消息。在将来可能会被扩展。 + +指令拥有一个主要名称和任意个次要名称。使用任意名称都可以执行指令。 + +定义指令 +------ + +每个指令都是一个 `Command` 类型的对象。`Command` 是一个接口,定义了基本的指令的属性: ```kotlin interface Command { - val names: Array<out String> - val usage: String - val description: String - val permission: CommandPermission - val prefixOptional: Boolean - val owner: CommandOwner - suspend fun CommandSender.onCommand(args: MessageChain) + val names: Array<out String> // 名称 + val usage: String // 用法 + val description: String // 描述 + val permission: Permission // 权限 + val prefixOptional: Boolean // 前缀可选 + val owner: CommandOwner // 拥有者 + + val overloads: List<CommandSignature> // 指令的签名列表 } ``` -每一条指令都被抽象成 [`Command`]。 +`AbstractCommand` 提供了对 `Command` 的基础实现。而要在插件定义指令,建议继承如下三种指令实现: -### 执行指令 +(注意:所有指令都需要注册到指令管理器才能生效,详见 [注册指令](#注册指令)) -指令既可以在代码执行,也可以在消息环境中执行。 +### 原生指令 -#### 在 [`CommandManager`] 执行指令 +原生指令即 [`RawCommand`],它直接处理触发指令的原消息链。 -通过扩展: -- `suspend fun Command.execute(CommandSender, args: Message, checkPermission: Boolean=true): CommandExecutionResult` -- `suspend fun Command.execute(CommandSender, args: String, checkPermission: Boolean=true): CommandExecutionResult` -- `suspend fun CommandSender.executeCommand(message: Message, checkPermission: Boolean=true): CommandExecutionResult` -- `suspend fun CommandSender.executeCommand(message: String, checkPermission: Boolean=true): CommandExecutionResult` +`RawCommand` 提供了两个抽象函数,在指令被执行时将会调用它们: + +*Kotlin* + +```kotlin +open override suspend fun CommandContext.onCommand(args: MessageChain) +open override suspend fun CommandSender.onCommand(args: MessageChain) +``` + +*Java* + +```java +public abstract class JRawCommand { + // ... + public void onCommand(CommandContext context, MessageChain args) { + } + + public void onCommand(CommandSender sender, MessageChain args) { + } +} +``` + +例如在聊天环境通过消息链 `/test 123 [图片]` 触发指令(`[图片]` 表示一个图片),`onCommand` 接收的 `args` +为包含 2 +个元素的 `MessageChain`。第一个元素为 `PlainText("123")`,第二个元素为 `Image`。 + +注意,当 `onCommand(CommandSender, MessageChain)` +和 `onCommand(CommandContext, MessageChain)` 被同时覆盖时, +只有 `onCommand(CommandContext, MessageChain)` 会生效。 + +`CommandContext` 是当前指令的执行环境,定义如下: + +```kotlin +interface CommandContext { + val sender: CommandSender + val originalMessage: MessageChain +} +``` + +其中 `sender` 为指令执行者。它可能是控制台(`ConsoleCmomandSender` +),也可能是用户(`UserCommandSender`)等; +`originalMessage` 为触发指令的原消息链,包含元数据,也包含指令名。 + +若在聊天环境触发指令,`originalMessage` 将会包含 `MessageSource`。 + +注意,`MessageSource` 等 `MessageMetadata` 的位置是不确定的。取决于 mirai-core +的版本,它可能会存在于消息链中的任意位置。因此请不要依赖于 `originalMessage` 的元素顺序。 + +`args` 参数的顺序是稳定的,因为它只包含消息内容(`MessageContent`)。 + +#### 使用 `RawCommand` + +只需要按需继承 `onCommand` 其中一个即可。如果需要使用原消息链,则继承 `CommandContext` +的,否则继承 `CommandSender` 的可以使实现更简单。 + +通常可以以单例形式实现指令,当然非单例模式也是支持的。 + +下面分别为在 Kotlin 和 Java 的示例实现: + +*Kotlin* + +```kotlin +object MyCommand : RawCommand( + MyPluginMain, "name", // 使用插件主类对象作为指令拥有者;设置主指令名为 "name" + // 可选: + "name2", "name3", // 增加两个次要名称 + usage = "/name arg1 arg2", // 设置用法,将会在 /help 展示 + description = "这是一个测试指令", // 设置描述,将会在 /help 展示 + prefixOptional = true, // 设置指令前缀是可选的,即使用 `test` 也能执行指令而不需要 `/test` +) { + override suspend fun CommandContext.onCommand(args: MessageChain) { + } +} +``` + +*Java* + +```java +public final class MyCommand extends JRawCommand { + public static final MyCommand INSTANCE = new MyCommand(); + + private MyCommand() { + super(MyPluginMain.INSTANCE, "test"); // 使用插件主类对象作为指令拥有者;设置主指令名为 "test" + // 可选设置如下属性 + setUsage("/test"); // 设置用法,这将会在 /help 中展示 + setDescription("这是一个测试指令"); // 设置描述,也会在 /help 中展示 + setPrefixOptional(true); // 设置指令前缀是可选的,即使用 `test` 也能执行指令而不需要 `/test` + } + + @Override + public void onCommand(@NotNull CommandSender sender, @NotNull MessageChain args) { + // 处理指令 + } +} +``` + +### 参数智能解析 + +Console +提供参数智能解析功能,可以阅读用户手册的 [指令参数智能解析](../../docs/ConsoleTerminal.md#指令参数智能解析) +了解这一功能。 + +有两种指令实现支持这个功能,它们分别是简单指令 `SimpleCommand` 和复合指令 `CompositeCommand`。 + +### 复合指令 + +复合指令即 `CompositeCommand`,支持参数智能解析。 + +Console 通过反射实现参数类型识别。标注 `@SubCommand` 的函数(方法)都会被看作是子指令。 + +一个简单的子指令定义如下: + +*Kotlin* + +```kotlin +object MyComposite : CompositeCommand() { + // ... + + @SubCommand("name") + suspend fun foo(context: CommandContext, arg: String) { + println(arg) + } +} +``` + +*Java* + +```java +public final class MyComposite extends JCompositeCommand { + // ... + @SubCommand("name") + public void foo(CommandContext context, String arg) { + System.out.println(arg); + } +} +``` + +Java 使用者请了解 Kotlin 的 `fun foo(context: CommandContext, arg: String)` 相当于 +Java 的 `public void foo(CommandContext context, String arg)`。下面部分简单示例将只用 +Kotlin 展示。 + +#### 子指令 + +用 `@SubCommand` 标注的函数就是子指令。子指令将隶属于其主指令。注册于主指令 `main` 的名称为 `child` +的指令在执行时需要使用 `/main child`,其中 `/` 表示指令前缀(如果需要)。`/main child arg1 arg2` +中的 `arg1` 和 `arg2` 则表示传递给子指令的第一个和第二个参数。 + +子指令可以拥有多个名称,即 `@SubCommand("child1", "child2")` 可以由 `/main child1` +或 `/main child2` 执行。 + +#### 子指令名称 + +`@SubCommand` 的参数为子指令的名称,可以有多个名称,即 `@SubCommand("name1", "name2")` +。若子指令名称与函数名称相同,可以省略 `@SubCommand` +的参数。例如 `@SubCommand("foo") suspend fun foo()` +可以简写为 `@SubCommand suspend fun foo()`。 + +#### 子指令参数 + +子指令的第一个参数(在 Kotlin 也可以是接收者(`receiver`))可以是 `CommandContext` +或 `CommandSender`,用来获取指令执行环境或发送人。与 `RawCommand` +相同,如果需要使用原消息链,则使用 `CommandContext`,否则使用 `CommandSender` 的可以让实现更简单。 + +在这个参数以外的就是是子指令的值参数。 +值参数将会对应消息链。例如 `@SubCommand fun foo(context: CommandContext, arg: String)` +将会对应 /comp foo + +在 Kotlin,子指令既可以是 `suspend` 也可以不是。 + +#### 定义参数 + +子指令函数(方法)定义的参数将按顺序成为指令的参数。如下示例中 `arg1` 将成为第一个参数,`arg2` 为第二个: + +*Kotlin* + +```kotlin +object MyComposite : CompositeCommand(MyPluginMain, "main") { + // ... + + @SubCommand("name") + suspend fun foo(context: CommandContext, arg: String, b: Boolean) { + println(arg) + } +} +``` + +*Java* + +```java +public final class MyComposite extends JCompositeCommand { + public MyComposite() { + super(MyPluginMain.INSTANCE, "main"); + // ... + } + + // ... + @SubCommand("name") + public void foo(CommandContext context, String arg, boolean b) { + System.out.println(arg); + } +} +``` + +在执行时,`/main name 1 true` 中 `1` 将会被解析为 `String` 类型的参数 `arg`、`true` +将会被解析为 `boolean` 参数的 `b`。 + +#### 内置智能解析 + +可参考 `CommandValueArgumentParser`,Console 内置支持以下类型的参数: + +- `Message` +- `SingleMessage` +- `MessageContent` +- 原生数据类型 +- `PlainText` +- `Image` +- `String` +- `Bot` +- `Contact` +- `User` +- `Friend` +- `Member` +- `Group` +- `PermissionId` +- `PermitteeId` +- `Enum` +- `TemporalAccessor` + +#### 自定义智能解析 + +可在 `CmopositeCommand` 继承 `context` 属性增加自定义解析器。下面示例中为 `Boolean` +指定了自定义的解析器,子指令的 `b` 参数将会用此解析器解析。 + +*Kotlin* + +```kotlin +object CustomBooleanParser : CommandValueArgumentParser<Boolean> { + override fun parse(raw: String, sender: CommandSender): Boolean { + return raw == "TRUE!" + } + override fun parse( + raw: MessageContent, + sender: CommandSender + ): Boolean { + // 将一个图片认为是 'true' + if (raw is Image && raw.imageId == "{A7CBB529-43A2-127C-E426-59D29BAA8515}.jpg") { + return true + } + return super.parse(message, sender) + } +} + +object MyComposite : CompositeCommand( + MyPluginMain, "main", + overrideContext = buildCommandArgumentContext { + Boolean::class with CustomBooleanParser + } +) { + // ... + @SubCommand("name") + suspend fun foo(context: CommandContext, arg: String, b: Boolean) { + println(b) + } +} +``` + +*Java* + +```java +// CustomBooleanParser.java +public final class CustomBooleanParser implements CommandValueArgumentParser<Boolean> { + @NotNull + @Override + public Boolean parse(@NotNull String raw, @NotNull CommandSender sender) throws CommandArgumentParserException { + return raw.equals("TRUE!"); + } + + @NotNull + @Override + public Boolean parse(@NotNull MessageContent raw, @NotNull CommandSender sender) throws CommandArgumentParserException { + // 将一个图片认为是 'true' + if (raw instanceof Image && ((Image) raw).getImageId().equals("{A7CBB529-43A2-127C-E426-59D29BAA8515}.jpg")) { + return true; + } + return CommandValueArgumentParser.super.parse(raw, sender); + } +} + +// MyComposite.java +public final class MyComposite extends JCompositeCommand { + public MyComposite() { + super(MyPluginMain.INSTANCE, "main"); + // ... + + addArgumentContext(new CommandArgumentContextBuilder() + .add(Boolean.TYPE, new CustomBooleanParser()) // 注册解析器 + .build()); + } + + // ... + @SubCommand("name") + public void foo(CommandContext context, String arg, boolean b) { + System.out.println(b); + } +} +``` + +在 `parse` 时抛出 `CommandArgumentParserException` +会被看作是正常退出,异常的内容会返回给指令调用人。在 `parse` 时抛出其他异常则会认为是插件错误。 + +### 简单指令 + +简单指令与复合指令拥有一样的智能参数解析功能。简单指令没有子指令,使用 `@Handler` 标注一个函数可以让它处理指令: + +*Kotlin* + +```kotlin +object MySimple : SimpleCommand(MyPluginMain, "main") { + // ... + @Handler + suspend fun foo(context: CommandContext, arg: String, b: Boolean) { + println(b) + } +} +``` + +*Java* + +```java +// MyComposite.java +public final class MyComposite extends JCompositeCommand { + public MyComposite() { + super(MyPluginMain.INSTANCE, "main"); + // ... + } + + // ... + @Handler + public void foo(CommandContext context, String arg, boolean b) { + System.out.println(b); + } +} +``` + +在执行时,`/main aaaa false` 将会调用 `foo` 函数(方法)。`aaaa` 匹配 `String` 类型的参数 `arg` +,`false` 匹配 `boolean` 类型的参数 `b`。 + +简单指令也可以使用自定义参数解析器,用法与复合指令一样。 + +*Kotlin* + +```kotlin +object MySimple : SimpleCommand( + MyPluginMain, "main", + overrideContext = buildCommandArgumentContext { + Boolean::class with CustomBooleanParser + } +) { + // ... + @Handler + suspend fun foo(context: CommandContext, arg: String, b: Boolean) { + println(b) + } +} +``` + +*Java* + +```java +// MyComposite.java +public final class MyComposite extends JCompositeCommand { + public MyComposite() { + super(MyPluginMain.INSTANCE, "main"); + // ... + + addArgumentContext(new CommandArgumentContextBuilder() + .add(Boolean.TYPE, new CustomBooleanParser()) // 注册解析器 + .build()); + } + + // ... + @Handler + public void foo(CommandContext context, String arg, boolean b) { + System.out.println(b); + } +} +``` + +### 选择 [`RawCommand`], [`SimpleCommand`] 或 [`CompositeCommand`] + +若需要不限长度的,自由的参数列表,使用 [`RawCommand`]。 + +若需要子指令,使用 [`CompositeCommand`]。否则使用 [`SimpleCommand`]。 + +### 自行实现指令 + +Console 允许插件自行实现指令(不使用上述 `RawCommand`、`SimpleCommand` +和 `CompositeCommand`)。但注意,在实现时难免会需要使用到抽象指令描述器(如 `CommandArgument` +),而这些描述器是不稳定的。因此插件自行实现指令可能会导致不兼容未来的 Console 版本。 + + +注册指令 +------- + +所有指令都需要注册到指令管理器才能生效。要注册指令,在 `onEnable` +使用 `CommandManager.registerCommand(command)`。 + +### 查看已注册的所有指令 + +使用 `PluginManager.INSTANCE.getAllRegisteredCommands()` +。可以获得当前已经注册的所有 `Command` 实例列表。 + + +执行指令 +------- + +指令既可以由插件执行,也可以在消息环境中由用户执行(需要 [chat-command](https://github.com/project-mirai/chat-command) +)。 + +### 在插件执行指令 + +若要通过字符串解析目标指令并执行,使用 `PluginManager.INSTANCE.executeCommand(CommandSender, Message)` +,其中 `Message` 为包含前缀(如果有必要)、指令名称、以及指令参数列表的完整消息。 + +若要通过字符串解析目标指令并执行,使用 `PluginManager.INSTANCE.executeCommand(CommandSender, Command, Message)` +,其中 `Message` 传递给指令的参数列表,不包含前缀或指令名称。注意,若要执行复合指令,需要包含子指令名称。 ### 指令语法解析 -一条消息可以被解析为指令,如果它满足: -`<指令前缀><任一指令名> <指令参数列表>` +一条消息可以被解析为指令,如果它满足: -指令参数由空格分隔。参数类型可能为 `MessageContent` 类型,或 `String`(被包装为 `PlainText`) - -指令前缀可能是可选的。可以在配置文件配置。(计划支持中) - -### [`RawCommand`] -无参数解析, 接收原生参数的指令。 -```kotlin -abstract override suspend fun CommandSender.onCommand(args: MessageChain) +```text +<指令前缀><任一指令名> <指令参数列表> ``` -例如 `/test 123 [图片]`,在处理时 `onCommand` 接收的 `args` 为包含 2 个元素的 `MessageChain`。第一个元素为 `PlainText("123")`,第二个元素为 `Image` 类型。 +指令参数列表由空格分隔。 -### [`Register`] -需要把指令注册到 `CommandManager` 以在 Mirai Console 生效 -```kotlin -CommandManager.registerCommand(command) -``` +### 指令解析流程 -## 参数智能解析 -> 本节可能较难理解。但这不会影响你阅读下面的示例。 +> 注意:该流程可能会变化,请不要依赖这个流程。 -Mirai Console 为了简化处理指令时的解析过程,设计了参数智能解析。 +对于 -### [`CommandValueArgumentParser`] -```kotlin -interface CommandArgumentParser<out T : Any> { - fun parse(raw: String, sender: CommandSender): T - fun parse(raw: MessageContent, sender: CommandSender): T = parse(raw.content, sender) -} -``` - -用于解析一个参数到一个数据类型。 - -### [`CommandArgumentContext`] - -是 `Class` 到 [`CommandValueArgumentParser`] 的映射。作用是为某一个类型分配解析器。 - -#### [内建 `CommandArgumentContext`][`CommandArgumentContext.BuiltIns`] -支持原生数据类型,`Contact` 及其子类,`Bot`。 - -#### 构建 [`CommandArgumentContext`] -查看源码内注释:[CommandArgumentContext.kt: Line 146](../backend/mirai-console/src/command/descriptor/CommandArgumentContext.kt#L146-L183) - -### 支持参数解析的 [`Command`] 实现 -Mirai Console 内建 [`SimpleCommand`] 与 [`CompositeCommand`] 拥有 [`CommandArgumentContext`],在处理参数时会首先解析参数再传递给插件的实现。 - -### [`SimpleCommand`] -简单指令。 - -此时示例一定比理论有意义。 - -Kotlin 示例: -```kotlin -object MySimpleCommand : SimpleCommand( - MyPluginMain, "tell", "私聊", - description = "Tell somebody privately" -) { - @Handler // 标记这是指令处理器 // 函数名随意 - suspend fun CommandSender.handle(target: User, message: String) { // 这两个参数会被作为指令参数要求 - target.sendMessage(message) - } -} -``` - -Java 示例: -```java -public class MySimpleCommand extends SimpleCommand { - - public MySimpleCommand(JvmPlugin plugin) { - super(plugin, "tell", new String[]{"私聊"}, "Tell somebody privately", plugin.getParentPermission(), CommandArgumentContext.EMPTY); - } - - @Handler // 标记这是指令处理器,方法名随意 - public void handle(CommandSender sender, User target, String message) { // 后两个参数会被作为指令参数要求 - target.sendMessage(message); - } - -} +```text +@Handler suspend fun handle(context: CommandContext, target: User, message: String) ``` 指令 `/tell 123456 Hello` 的解析流程: + 1. 被分割为 `/`, `"tell"`, `"123456"`, `"Hello"` -2. `MySimpleCommand` 被匹配到,根据 `/` 和 `"test"`。`"123456"`, `"Hello"` 被作为指令的原生参数。 -3. 由于 `MySimpleCommand` 定义的 `handle` 需要两个参数, `User` 和 `String`,`"123456"` 需要转换成 `User`,`"Hello"` 需要转换成 `String`。 -4. Console 在 [内建 `CommandArgumentContext`][`CommandArgumentContext.BuiltIns`] 寻找适合于 `User` 的 [`CommandValueArgumentParser`] -5. `"123456"` 被传入这个 [`CommandValueArgumentParser`],得到 `User` -6. `"Hello"` 也会按照 4~5 的步骤转换为 `String` 类型的参数 -7. 解析完成的参数被传入 `handle` +2. 根据 `/` 和 `"test"`,确定 `MySimpleCommand` 作为目标指令。`"123456"`, `"Hello"` + 作为指令的原生参数。 +3. 由于 `MySimpleCommand` 定义的 `handle` 需要两个参数, 即 `User` 和 `String` + ,`"123456"` 需要转换成 `User`,`"Hello"` 需要转换成 `String`。 +4. 指令寻找合适的解析器(`CommandValueArgumentParser`) +5. `"123456"` 通过 `ExistingUserValueArgumentParser` 变为 `User` 类型的参数 +6. `"Hello"` 通过 `StringValueArgumentParser` 变为 `String` 类型的参数 +7. 解析完成的参数传入 `handle` -### [`CompositeCommand`] - -[`CompositeCommand`] 的参数解析与 [`SimpleCommand`] 一样,只是多了「子指令」概念。 - -示例: - -Kotlin 示例: -```kotlin -@OptIn(ConsoleExperimentalAPI::class) -object MyCompositeCommand : CompositeCommand( - MyPluginMain, "manage", // "manage" 是主指令名 - description = "示例指令", permission = MyCustomPermission, - // prefixOptional = true // 还有更多参数可填, 此处忽略 -) { - - // [参数智能解析] - // - // 在控制台执行 "/manage <群号>.<群员> <持续时间>", - // 或在聊天群内发送 "/manage <@一个群员> <持续时间>", - // 或在聊天群内发送 "/manage <目标群员的群名> <持续时间>", - // 或在聊天群内发送 "/manage <目标群员的账号> <持续时间>" - // 时调用这个函数 - @SubCommand // 表示这是一个子指令,使用函数名作为子指令名称 - suspend fun CommandSender.mute(target: Member, duration: Int) { // 通过 /manage mute <target> <duration> 调用 - sendMessage("/manage mute 被调用了, 参数为: $target, $duration") - - val result = kotlin.runCatching { - target.mute(duration).toString() - }.getOrElse { - it.stackTraceToString() - } // 失败时返回堆栈信息 - - sendMessage("结果: $result") - } - - @SubCommand - suspend fun ConsoleCommandSender.foo() { - // 使用 ConsoleCommandSender 作为接收者,表示指令只能由控制台执行。 - // 当用户尝试在聊天环境执行时将会收到错误提示。 - } - - @SubCommand("list", "查看列表") // 可以设置多个子指令名。此时函数名会被忽略。 - suspend fun CommandSender.ignoredFunctionName() { // 执行 "/manage list" 时调用这个函数 - sendMessage("/manage list 被调用了") - } - - // 支持 Image 类型, 需在聊天中执行此指令. - @SubCommand - suspend fun UserCommandSender.test(image: Image) { // 执行 "/manage test <一张图片>" 时调用这个函数 - // 由于 Image 类型消息只可能在聊天环境,可以直接使用 UserCommandSender。 - - sendMessage("/manage image 被调用了, 图片是 ${image.imageId}") - } -} -``` - -Java 示例: -```java -public class MyCompositeCommand extends CompositeCommand { - - public MyCompositeCommand(JvmPlugin plugin) { - // "manage" 是主指令名, 还有更多参数可填, 此处忽略 - super(plugin, "manage", new String[]{}, "示例指令", plugin.getParentPermission(), CommandArgumentContext.EMPTY); - } - - // [参数智能解析] - // - // 在控制台执行 "/manage <群号>.<群员> <持续时间>", - // 或在聊天群内发送 "/manage <@一个群员> <持续时间>", - // 或在聊天群内发送 "/manage <目标群员的群名> <持续时间>", - // 或在聊天群内发送 "/manage <目标群员的账号> <持续时间>" - // 时调用这个函数 - @SubCommand // 表示这是一个子指令,使用函数名作为子指令名称 - public void mute(CommandSender sender, Member target, int duration) { // 通过 /manage mute <target> <duration> 调用 - sender.sendMessage("/manage mute 被调用了, 参数为: " + target.toString() + ", " + duration); - - String result; - try { - target.mute(duration); - result = "成功"; - } catch (Exception e) { - result = "失败," + e.getMessage(); - } - - sender.sendMessage("结果: " + result); - } - - @SubCommand - public void foo(ConsoleCommandSender sender) { - // 使用 ConsoleCommandSender 作为接收者,表示指令只能由控制台执行。 - // 当用户尝试在聊天环境执行时将会收到错误提示。 - } - - @SubCommand(value = {"list", "查看列表"}) // 可以设置多个子指令名。此时函数名会被忽略。 - public void ignoredFunctionName(CommandSender sender) { // 执行 "/manage list" 时调用这个函数 - sender.sendMessage("/manage list 被调用了"); - } - - // 支持 Image 类型, 需在聊天中执行此指令. - @SubCommand - public void test(CommandSender sender, Image image) { // 执行 "/manage test <一张图片>" 时调用这个函数 - // 由于 Image 类型消息只可能在聊天环境,可以直接使用 UserCommandSender。 - - sender.sendMessage("/manage image 被调用了, 图片是 " + image.getImageId()); - } -} -``` - -### 文本参数的转义 +文本参数转义 +----- 不同的参数默认用空格分隔。有时用户希望在文字参数中包含空格本身,参数解析器可以接受三种表示方法。 以上文中定义的 `MySimpleCommand` 为例: -#### 英文双引号 +### 英文双引号 表示将其中内容作为一个参数,可以包括空格。 例如:用户输入 `/tell 123456 "Hello world!"` ,`message` 会收到 `Hello world!`。 -注意:双引号仅在参数的首尾部生效。例如,用户输入 `/tell 123456 He"llo world!"`,`message` 只会得到 `He"llo`。 +注意:双引号仅在参数的首尾部生效。例如,用户输入 `/tell 123456 He"llo world!"`,`message` +只会得到 `He"llo`。 -#### 转义符 +### 转义符 即英文反斜杠 `\`。表示忽略之后一个字符的特殊含义,仅看作字符本身。 @@ -299,49 +589,41 @@ public class MyCompositeCommand extends CompositeCommand { - 用户输入 `/tell 123456 Hello\ world!`,`message` 得到 `Hello world!`; - 用户输入 `/tell 123456 \"Hello world!\"`,`message` 得到 `"Hello`。 -#### 暂停解析标志 +### 暂停解析标志 即连续两个英文短横线 `--`。表示从此处开始,到**这段文字内容**结束为止,都作为一个完整参数。 例如: -- 用户输入 `/tell 123456 -- Hello:::test\12""3`,`message` 得到 `Hello:::test\12""3`(`:` 表示空格); -- 用户输入 `/tell 123456 -- Hello @全体成员 test1 test2`,那么暂停解析的作用范围到 `@` 为止,之后的 `test1` 和 `test2` 是不同的参数。 -- 用户输入 `/tell 123456 \-- Hello` 或 `/tell 123456 "--" Hello`,这不是暂停解析标志,`message` 得到 `--` 本身。 +- 用户输入 `/tell 123456 -- Hello:::test\12""3`,`message` + 得到 `Hello:::test\12""3`(`:` 表示空格); +- 用户输入 `/tell 123456 -- Hello @全体成员 test1 test2`,那么暂停解析的作用范围到 `@` + 为止,之后的 `test1` 和 `test2` 是不同的参数。 +- 用户输入 `/tell 123456 \-- Hello` 或 `/tell 123456 "--" Hello` + ,这不是暂停解析标志,`message` 得到 `--` 本身。 注意: `--` 的前后都应与其他参数有间隔,否则不认为这是暂停解析标志。 -例如,用户输入 `/tell 123456--Hello world!`,`123456--Hello` 会被试图转换为 `User` 并出错。即使转换成功,`message` 也只会得到 `world!`。 +例如,用户输入 `/tell 123456--Hello world!`,`123456--Hello` 会被试图转换为 `User` +并出错。即使转换成功,`message` 也只会得到 `world!`。 ### 非文本参数的转义 -有时可能需要只用一个参数来接受各种消息内容,例如用户可以在 `/tell 123456` 后接图片、表情等,它们都是 `message` 的一部分。 +有时可能需要只用一个参数来接受各种消息内容,例如用户可以在 `/tell 123456` 后接图片、表情等,它们都是 `message` +的一部分。 -对于这种定义方式,Mirai Console 的支持尚待实现,目前可以使用 [`RawCommand`] 替代。 +对于这种定义方式,Mirai Console 的支持尚待实现,目前可以使用 [`RawCommand`] 替代。 -### 选择 [`RawCommand`], [`SimpleCommand`] 或 [`CompositeCommand`] +## 指令发送者 -若需要不限长度的,自由的参数列表,使用 [`RawCommand`]。 - -若需要子指令,使用 [`CompositeCommand`]。否则使用 [`SimpleCommand`]。 - -## [`CommandManager`] -上面已经提到可以在 [`CommandManager`] 执行指令。[`CommandManager`] 持有已经注册的指令列表,源码内有详细注释,此处不过多赘述。 - -## [`CommandSender`] -指令发送者。 - -### 必要性 - -指令可能在聊天环境执行,也可能在控制台执行。因此需要一个通用的接口表示这样的执行者。 +指令可能在聊天环境执行,也可能在控制台执行。 +指令发送者即 `CommandSender`,是执行指令时的必须品之一。 ### 类型 + ```text - CoroutineScope - ↑ - | CommandSender <---------+---------------+-------------------------------+ ↑ | | | | | | | @@ -375,17 +657,90 @@ ConsoleCommandSender AbstractUserCommandSender | 有关类型的详细信息,请查看 [CommandSender.kt](../backend/mirai-console/src/command/CommandSender.kt#L48-L135) -### 获取 +### 获取控制台指令发送者 -`Contact.asCommandSender()` 或 `MessageEvent.toCommandSender()`,或 `ConsoleCommandSender` +`ConsoleCommandSender` 表示以控制台身份执行指令。它是一个单例对象,在 Kotlin 可以直接通过类型获得类名获得实例,在 +Java 可通过 `ConsoleCommandSender.INSTANCE` 获得。 -## [`MessageScope`] +### 获取其他指令发送者 -表示几个消息对象的’域‘,即消息对象的集合。用于最小化将同一条消息发送给多个类型不同的目标的付出。 +在 Kotlin 可使用扩展函数:`Contact.asCommandSender()` +或 `MessageEvent.toCommandSender()` +。 -参考 [MessageScope](../backend/mirai-console/src/util/MessageScope.kt#L28-L99) +在 Java 可使用 `CommandSender.from` 和 `CommandSender.of`。 + +[`MessageScope`] +----- + +表示几个消息对象的'域',即消息对象的集合。用于最小化将同一条消息发送给多个类型不同的目标的付出。示例: + +*Kotlin* + +```kotlin +// 在一个 CompositeCommand 内 +@Handler +suspend fun CommandSender.handle(target: Member) { + val duration = Random.nextInt(1, 15) + target.mute(duration) + + + // 不使用 MessageScope, 无用的样板代码 + val thisGroup = this.getGroupOrNull() + val message = "${this.name} 禁言 ${target.nameCardOrNick} $duration 秒" + if (target.group != thisGroup) { + target.group.sendMessage(message) + } + sendMessage(message) + + + // 使用 MessageScope, 清晰逻辑 + // 表示至少发送给 `this`, 当 `this` 的真实发信对象与 `target.group` 不同时, 还额外发送给 `target.group` + this.scopeWith(target.group) { + sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒") + } + + + // 同样地, 可以扩展用法, 同时私聊指令执行者: + // this.scopeWith( + // target, + // target.group + // ) { ... } +} +``` + +*Java* + +```java +public class MyCommand extends SimpleCommand { + @Handler + public void handle(sender: CommandSender, target: Member) { + int duration = Random.nextInt(1, 15); + target.mute(duration); + + + // 不使用 MessageScope + Group thisGroup = CommandSenderKt.getGroupOrNull(sender); + String message = "${this.name} 禁言 ${target.nameCardOrNick} $duration 秒"; + if (!target.group.equals(thisGroup)) { + target.group.sendMessage(message); + } + sender.sendMessage(message); + + + // 使用 MessageScope + // 表示至少发送给 `this`, 当 `this` 的真实发信对象与 `target.group` 不同时, 还额外发送给 `target.group` + MessageScope scope = MessageScopeKt.scopeWith(sender, target); + scope.sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒"); + + // 或是只用一行: + MessageScopeKt.scopeWith(sender, target).sendMessage("${name} 禁言了 ${target.nameCardOrNick} $duration 秒"); + } +} +``` + ---- > 下一步,[PluginData](PluginData.md#mirai-console-backend---plugindata)