From 7a6c7a89c0d844ae85b7913cc24d9fe1fddd93b8 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 1 May 2022 18:03:10 +0100 Subject: [PATCH] Update docs for console plugin (#1995) * Update docs for console plugin * Update [skip ci] * Update docs Co-authored-by: Karlatemp --- mirai-console/docs/Plugins.md | 442 --------------- .../plugin/.JVMPlugin_images/75227ef5.png | Bin 0 -> 11449 bytes .../.images/PluginMainDeclaration.png | Bin .../PluginMainServiceNotConfigured.png | Bin .../docs/plugin/JVMPlugin-Appendix.md | 118 ++++ mirai-console/docs/plugin/JVMPlugin.md | 508 ++++++++++++++++++ mirai-console/docs/plugin/Plugins.md | 113 ++++ mirai-console/tools/gradle-plugin/README.md | 120 +++-- 8 files changed, 813 insertions(+), 488 deletions(-) delete mode 100644 mirai-console/docs/Plugins.md create mode 100644 mirai-console/docs/plugin/.JVMPlugin_images/75227ef5.png rename mirai-console/docs/{ => plugin}/.images/PluginMainDeclaration.png (100%) rename mirai-console/docs/{ => plugin}/.images/PluginMainServiceNotConfigured.png (100%) create mode 100644 mirai-console/docs/plugin/JVMPlugin-Appendix.md create mode 100644 mirai-console/docs/plugin/JVMPlugin.md create mode 100644 mirai-console/docs/plugin/Plugins.md diff --git a/mirai-console/docs/Plugins.md b/mirai-console/docs/Plugins.md deleted file mode 100644 index 674374b8b..000000000 --- a/mirai-console/docs/Plugins.md +++ /dev/null @@ -1,442 +0,0 @@ -# Mirai Console Backend - Plugins - -[`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 - - -[`PluginData`]: ../backend/mirai-console/src/data/PluginData.kt -[`PluginConfig`]: ../backend/mirai-console/src/data/PluginConfig.kt -[`PluginDataStorage`]: ../backend/mirai-console/src/data/PluginDataStorage.kt - -[`ExportManager`]: ../backend/mirai-console/src/plugin/jvm/ExportManager.kt - -[`MiraiConsole`]: ../backend/mirai-console/src/MiraiConsole.kt -[`MiraiConsoleImplementation`]: ../backend/mirai-console/src/MiraiConsoleImplementation.kt - - -[`Command`]: ../backend/mirai-console/src/command/Command.kt -[`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 - -[`Annotations`]: ../backend/mirai-console/src/util/Annotations.kt -[`ConsoleInput`]: ../backend/mirai-console/src/util/ConsoleInput.kt -[`JavaPluginScheduler`]: ../backend/mirai-console/src/plugin/jvm/JavaPluginScheduler.kt -[`ResourceContainer`]: ../backend/mirai-console/src/plugin/ResourceContainer.kt -[`PluginFileExtensions`]: ../backend/mirai-console/src/plugin/PluginFileExtensions.kt -[`AutoSavePluginDataHolder`]: ../backend/mirai-console/src/data/PluginDataHolder.kt#L45 - -[Kotlin]: https://www.kotlincn.net/ -[Java]: https://www.java.com/zh_CN/ -[JVM]: https://zh.wikipedia.org/zh-cn/Java%E8%99%9A%E6%8B%9F%E6%9C%BA -[JAR]: https://zh.wikipedia.org/zh-cn/JAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F) - -[为什么不支持热加载和卸载插件?]: QA.md#为什么不支持热加载和卸载插件 -[使用 AutoService]: QA.md#使用-autoservice - -[MCI]: ../tools/intellij-plugin/ -[MiraiPixel]: ../tools/intellij-plugin/resources/icons/pluginMainDeclaration.png - -Mirai Console 运行在 [JVM],支持使用 [Kotlin] 或 [Java] 语言编写的插件。 - -## 通用的插件接口 - [`Plugin`] - -所有 Console 插件都必须实现 [`Plugin`] 接口。 - -> **解释 *插件***:只要实现了 [`Plugin`] 接口的类和对象都可以叫做「Mirai Console 插件」,简称 「插件」。 -> 为了便捷,内含 [`Plugin`] 实现的一个 [JAR] 文件也可以被称为「插件」。 - -基础的 [`Plugin`] 很通用,它只拥有很少的成员: - -```kotlin -interface Plugin : CommandOwner { // CommandOwner 是空的 interface - val isEnabled: Boolean - val loader: PluginLoader<*, *> // 能处理这个 Plugin 的 PluginLoader -} -``` - -[`Plugin`] 接口拥有强扩展性,以支持 Mirai Console 统一管理使用其他编程语言编写的插件 (详见进阶章节 [扩展 - PluginLoader](Extensions.md))。 - -## 插件加载器 - [`PluginLoader`] - -Mirai Console 使用不同的插件加载器来加载不同类型插件。 - -Mirai Console 内置 [`JvmPluginLoader`] 以加载 JVM 平台插件(见下文),并允许这些插件注册扩展的插件加载器(见章节 [扩展](Extensions.md)) - -## JVM 平台插件接口 - [`JvmPlugin`] - -所有的 JVM 插件(特别地,`jar` 插件)都必须实现 [`JvmPlugin`](否则不会被 [`JvmPluginLoader`] 加载)。 -Mirai Console 提供一些基础的实现,即 [`AbstractJvmPlugin`],并将 [`JvmPlugin`] 分为 [`KotlinPlugin`] 和 [`JavaPlugin`]。 - -### 主类 - -JVM 平台插件的主类应被实现为一个单例(Kotlin `object`,Java 静态初始化的类,详见下文示例)。 - -**Kotlin 使用者的插件主类应继承 [`KotlinPlugin`]。** -**其他 JVM 语言(如 Java)使用者的插件主类应继承 [`JavaPlugin`]。** - -#### 定义主类 - -Mirai Console 使用类似 Java `ServiceLoader` 但更灵活的机制加载插件。 - -一个正确的主类定义可以是以下三种任一: - -1. Kotlin (`public`) `object` -```kotlin -object A : KotlinPlugin( /* 描述 */ ) -``` - -2. Java (`public`) 静态初始化单例 `class` -```java -public final class A extends JavaPlugin { - public static final A INSTANCE = new A(); // 必须 public static, 必须名为 INSTANCE - private A() { - super( /* 描述 */ ); - } -} -``` - -3. Java (`public`) `class` -注意:这种由 Mirai Console 构造插件实例的方法是不推荐的。请首选上述静态初始化方法。 -```java -public final class A extends JavaPlugin { - public A() { // 必须公开且无参 - super( /* 描述 */ ); - } -} -``` - -#### 确认主类正确定义 - -在 [Mirai Console IntelliJ 插件][MCI] 的帮助下,一个正确的插件主类定义的行号处会显示 Mirai 像素风格形象图:![MiraiPixel] - -![PluginMainDeclaration](.images/PluginMainDeclaration.png) - -#### 配置主类服务 - -[Mirai Console IntelliJ 插件][MCI] 会自动检查主类服务的配置。在没有正确配置时,IDE 将会警告并为你自动配置: -![PluginMainServiceNotConfigured](.images/PluginMainServiceNotConfigured.png) - -##### 手动配置主类服务 - -若无法使用 IntelliJ 插件,可在资源目录 `META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin` 文件内存放插件主类全名(以纯文本 UTF-8 存储,文件内容只包含一行插件主类全名)。 - -在 Kotlin,也可([使用 AutoService])自动配置 service 信息。 - -### 描述 - -插件描述需要在主类构造器传递给 `super`。可以选择直接提供或从 JAR 资源文件读取。 - -有关插件版本号的限制: -- 插件自身的版本要求遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范, 合格的版本例如: `1.0.0`, `1.0`, `1.0-M1`, `1.0-pre-1` -- 插件依赖的版本遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) 规范, 同时支持 [Apache Ivy 风格表示方法](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html). - -有关描述的详细信息可在开发时查看源码内文档。 - -### 主类的完整示例 - -基于上文,你现在有以下三种主类定义方式。 - -#### 实现 Kotlin 插件主类 - -一个 Kotlin 插件的主类通常需: -- 继承 [`KotlinPlugin`] -- 访问权限为 `public` 或默认 (不指定) - -```kotlin -object SchedulePlugin : KotlinPlugin( - JvmPluginDescription( - id = "org.example.my-schedule-plugin", - version = "1.0.0", - ) { - name("Schedule") - - // author("...") - // dependsOn("...") - } -) { - // ... -} -``` - -#### 实现 Java 插件主类 - -一个 Java 插件的主类通常需: -- 继承 [`JavaPlugin`] -- 访问权限为 `public` - -(推荐) 静态初始化: -```java -public final class JExample extends JavaPlugin { - public static final JExample INSTANCE = new JExample(); // 可以像 Kotlin 一样静态初始化单例 - private JExample() { - super(new JvmPluginDescriptionBuilder( - "org.example.test-plugin", // name - "1.0.0" // version - ) - // .author("...") - // .info("...") - .build() - ); - } -} -``` - -由 Console 初始化(仅在某些静态初始化不可用的情况下使用): -```java -public final class JExample extends JavaPlugin { - private static JExample instance; - public static JExample getInstance() { - return instance; - } - public JExample() { // 此时必须 public - super(new JvmPluginDescriptionBuilder( - "org.example.test-plugin", // id - "1.0.0" // version - ) - // .author("...") - // .info("...") - .build() - ); - instance = this; - } -} -``` - -### 依赖管理 - -一个插件被允许依赖于另一个插件。可在 `SimpleJvmPluginDescription` 构造时提供信息。 - -若插件拥有依赖,则会首先加载其依赖。但任何一个插件的 `onEnable()` 都会在所有插件的 `onLoad()` 都调用成功后再调用。 - -多个插件的加载是*顺序的*,意味着若一个插件的 `onLoad()` 等回调处理缓慢,后续插件的加载也会被延后,即使它们可能没有依赖关系。 -因此请尽量让 `onLoad()`,`onEnable()`,`onDisable()`快速返回。 - -### API 导出管理 - -允许插件将一些内部实现保护起来, 避免其他插件调用, 要启动这个特性, -只需要创建名为 `export-rules.txt` 的规则文件,便可以控制插件的类的公开规则。 - -如果正在使用 `Gradle` 项目, 该规则文件一般位于 `src/main/resources` 下 - -Example: -```text - -# #开头的行全部识别为注释 - -# exports, 允许其他插件直接使用某个类 - -# 导出了一个internal包的一个类 -# -exports org.example.miraiconsole.myplugin.internal.OpenInternal - -# 导出了整个 api 包 -# -exports org.example.miraiconsole.myplugin.api - -# 保护 org.example.miraiconsole.myplugin.api2.Internal, 不允许其他插件直接使用 -# -protects org.example.miraiconsole.myplugin.api2.Internal - -# 保护整个包 -# -# 别名: protect-package -protects org.example.miraiconsole.myplugin.internal - -# 此规则不会生效, 因为在此条规则之前, -# org.example.miraiconsole.myplugin.internal 已经被加入到保护域中 -exports org.example.miraiconsole.myplugin.internal.NotOpenInternal - - -# export-plugin, 允许其他插件使用除了已经被保护的全部类 -# 使用此规则会同时让此规则后的所有规则全部失效 -# 别名: export-all, export-system -# export-plugin - - -# 将整个插件放入保护域中 -# 除了此规则之前显式 export 的类, 其他插件将不允许直接使用被保护的插件的任何类 -# 别名: protect-all, protect-system -protect-plugin - -``` - -插件也可以通过 Service 来自定义导出控制 - -Example: -```kotlin -@AutoService(ExportManager::class) -object MyExportManager: ExportManager { - override fun isExported(className: String): Boolean { - println(" <== $className") - return true - } -} -``` - -### 插件生命周期 - -Mirai Console 不提供热加载和热卸载功能,所有插件只能在服务器启动前加载,在服务器结束时卸载。([为什么不支持热加载和卸载插件?]) - -插件仅可以通过如下三个回调知晓自身的加载情况。 - -较小概率情况: -- 如果 `onLoad()` 被调用,`onEnable()` 不一定会调用。因为可能在调用后续插件的 `onLoad()` 或 `onEnable()` 时可能会出错而导致服务器被关闭。 -- 如果 `onLoad()` 或 `onEnable()` 调用时抛出异常,`onDisable()` 不会被调用。(注意:这是仍处于争议状态的行为,后续可能有修改) -- 如果 `onEnable()` 被成功调用,`onDisable()` 一定会调用,无论其他插件是否发生错误。 - -#### 加载 - -[`JvmPluginLoader`] 调用插件的 `onLoad()`,在 `onLoad()` 正常返回后插件被认为成功加载。 - -由于 `onLoad()` 只会被初始化一次,插件可以在该方法内进行一些*一次性*的*初始化*任务,如 [注册扩展](Extensions.md#注册扩展)。 - -**在 `onLoad()` 时插件并未处于启用状态,此时插件不能进行监听事件,加载配置等操作。** - -若在 Kotlin 使用 `object`,或在 Java 使用静态初始化方式定义插件主类, `onLoad()` 与 `init` 代码块作用几乎相同。 - -#### 启用 - -[`JvmPluginLoader`] 调用插件的 `onEnable()`,意为启用一个插件。 - -此时插件可以启动所有协程,事件监听,和其他任务。**但这些任务都应该拥有生命周期管理,详见 [任务生命周期管理](#任务生命周期管理)。** - -#### 禁用 - -[`JvmPluginLoader`] 调用插件的 `onDisable()`,意为禁用一个插件。 - -插件的任何类和对象都不会被卸载。「禁用」仅表示停止关闭所有正在进行的任务,保存所有数据,停止处理将来的数据。 - -插件应正确实现「禁用」,以为用户提供完全的控制可能。 - -### 任务生命周期管理 - -#### 协程管理 - -[`JvmPlugin`] 实现 `CoroutineScope`,并由 Console 内部实现提供其 `coroutineContext`。 - -`JvmPlugin.coroutineContext` 包含元素 `CoroutineName`, `SupervisorJob`, `CoroutineExceptionHandler`。 -**所有插件启动的协程都应该受 `JvmPlugin` 作用域的管理** - -如要启动一个协程,正确的做法是: -```kotlin -// object MyPluginMain : KotlinPlugin() - -MyPluginMain.launch { - // job -} -``` - -#### Java 线程管理 - -*TODO*:Mirai Console 暂未支持自动的线程管理。请手动在 `onDisable()` 时关闭启动的线程。 - -### 访问数据目录和配置目录 - -[`JvmPlugin`] 实现接口 [`PluginFileExtensions`]。插件可通过 `resolveDataFile`,`resolveConfigFile` 等方法取得数据目录或配置目录下的文件。 - -可以在任何时刻使用这些方法。 - -详见 [`PluginFileExtensions`]。 - -Java 示例: -```java -File dataFile = JExample.INSTANCE.resolveDataFile("myDataFile.txt"); -// dataFile do something -File configFile = JExample.INSTANCE.resolveConfigFile("myConfigFile.txt"); -// configFile do something -``` - -#### 物理目录路径 -用 `$root` 表示 Mirai Console 运行路径,`$name` 表示插件名, -插件数据目录一般在 `$root/data/$name`,插件配置目录一般在 `$root/config/$name`。 - -有关数据和配置的区别可以在 [PluginData](PluginData.md) 章节查看。 - -### 访问 [JAR] 包内资源文件 - -[`JvmPlugin`] 实现接口 [`ResourceContainer`]。插件可通过 `getResource`,`getResourceAsStream` 等取得 [JAR] 包内资源文件。 - -可以在任何时刻使用这些方法。 - -详见 [`ResourceContainer`]。 - -### 读取 [`PluginData`] 或 [`PluginConfig`] - -> 本节基于章节 [PluginData](PluginData.md) 的内容。 -> 在阅读本节前建议先阅读上述基础章节。 - -[`JvmPlugin`] 实现接口 [`AutoSavePluginDataHolder`],提供: - -Kotlin: -- `public fun T.reload()` -- `public fun T.reload()` - -Java: -- `public fun reloadPluginData(PluginData)` -- `public fun reloadPluginData(PluginConfig)` - -**仅可在插件 onEnable() 时及其之后才能使用这些方法。** -**在插件 onDisable() 之后不能使用这些方法。** - -#### 使用示例 - -```kotlin -object SchedulePlugin : KotlinPlugin( - JvmPluginDescription( - id = "org.example.my-schedule-plugin", - version = "1.0.0", - ) { - name("Schedule") - - // author("...") - // dependsOn("...") - } -) { - // ... - - override fun onEnable() { - MyData.reload() // 仅需此行,保证启动时更新数据,在之后自动存储数据。 - } -} - -object MyData : AutoSavePluginData() { - val value: Map by value() -} -``` - -### 打包插件 -若 [使用了 Mirai Console Gradle 插件](ConfiguringProjects.md#c使用-gradle-插件配置项目), 执行 Gradle 任务 `buildPlugin` 即可打包插件 JAR. 之后可以在 `build/mirai/` 找到 JAR 文件. 这个文件可以放入 Mirai Console `plugins` 目录中加载. - -若没有使用, 请打包插件并附带资源文件和所有依赖到一个单独的 JAR. - -### 发布插件到 mirai-console-loader -*TODO* - -### 附录:Java 插件的多线程调度器 - [`JavaPluginScheduler`] -拥有生命周期管理的简单 Java 线程池。其中所有的任务都会在插件被关闭时自动停止。 - -Java 示例: -``` -JExample.INSTANCE.getScheduler().repeating(1 * 1000, new Runnable() { - @Override - public void run() { - JExample.INSTANCE.getLogger().info("clock task arrival"); - // or do something - } -}); -``` - -> 下一步,[Commands](Commands.md#mirai-console-backend---commands) -> -> 返回 [开发文档索引](README.md#mirai-console) - diff --git a/mirai-console/docs/plugin/.JVMPlugin_images/75227ef5.png b/mirai-console/docs/plugin/.JVMPlugin_images/75227ef5.png new file mode 100644 index 0000000000000000000000000000000000000000..142934c1ecc900a570741d4c69e1ef6bd7abc9f8 GIT binary patch literal 11449 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 本节基于章节 [PluginData](../PluginData.md) 的内容。 +> 在阅读本节前建议先阅读上述基础章节。也可以先跳过本节。 + +[`JvmPlugin`] 实现接口 [`AutoSavePluginDataHolder`],提供: + +Kotlin: + +- `public fun T.reload()` +- `public fun T.reload()` + +Java: + +- `public fun reloadPluginData(PluginData)` +- `public fun reloadPluginData(PluginConfig)` + +**仅可在插件 onEnable() 时及其之后才能使用这些方法。** +**在插件 onDisable() 之后不能使用这些方法。** + +#### 使用示例 + +```kotlin +object SchedulePlugin : KotlinPlugin( + JvmPluginDescription( + id = "org.example.my-schedule-plugin", + version = "1.0.0", + ) { + name("Schedule") + + // author("...") + // dependsOn("...") + } +) { + // ... + + override fun onEnable() { + MyData.reload() // 仅需此行,保证启动时更新数据,在之后自动存储数据。 + } +} + +object MyData : AutoSavePluginData() { + val value: Map by value() +} +``` diff --git a/mirai-console/docs/plugin/JVMPlugin.md b/mirai-console/docs/plugin/JVMPlugin.md new file mode 100644 index 000000000..679716ae0 --- /dev/null +++ b/mirai-console/docs/plugin/JVMPlugin.md @@ -0,0 +1,508 @@ +# Mirai Console Backend - JVM Plugins + +[`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 + + +[`PluginData`]: ../../backend/mirai-console/src/data/PluginData.kt + +[`PluginConfig`]: ../../backend/mirai-console/src/data/PluginConfig.kt + +[`PluginDataStorage`]: ../../backend/mirai-console/src/data/PluginDataStorage.kt + +[`ExportManager`]: ../../backend/mirai-console/src/plugin/jvm/ExportManager.kt + +[`MiraiConsole`]: ../../backend/mirai-console/src/MiraiConsole.kt + +[`MiraiConsoleImplementation`]: ../../backend/mirai-console/src/MiraiConsoleImplementation.kt + + +[`Command`]: ../../backend/mirai-console/src/command/Command.kt + +[`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 + +[`Annotations`]: ../../backend/mirai-console/src/util/Annotations.kt + +[`ConsoleInput`]: ../../backend/mirai-console/src/util/ConsoleInput.kt + +[`JavaPluginScheduler`]: ../../backend/mirai-console/src/plugin/jvm/JavaPluginScheduler.kt + +[`ResourceContainer`]: ../../backend/mirai-console/src/plugin/ResourceContainer.kt + +[`PluginFileExtensions`]: ../../backend/mirai-console/src/plugin/PluginFileExtensions.kt + +[`AutoSavePluginDataHolder`]: ../../backend/mirai-console/src/data/PluginDataHolder.kt#L45 + +[Kotlin]: https://www.kotlincn.net/ + +[Java]: https://www.java.com/zh_CN/ + +[JVM]: https://zh.wikipedia.org/zh-cn/Java%E8%99%9A%E6%8B%9F%E6%9C%BA + +[JAR]: https://zh.wikipedia.org/zh-cn/JAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F) + +[为什么不支持热加载和卸载插件?]: ../QA.md#为什么不支持热加载和卸载插件 + +[使用 AutoService]: ../QA.md#使用-autoservice + +[MCI]: ../../tools/intellij-plugin/ + +[MiraiPixel]: ../../tools/intellij-plugin/resources/icons/pluginMainDeclaration.png + +本章节介绍 JVM 平台的插件。 + +## JVM 平台插件接口 - [`JvmPlugin`] + +所有的 JVM 插件(特别地,打包为 `JAR` 的)都必须实现 [`JvmPlugin`],否则不会被 [`JvmPluginLoader`] +识别和加载。 + +[`JvmPlugin`] 派生为 [`KotlinPlugin`] 和 [`JavaPlugin`],其关系图如下所示。 + +![](.JVMPlugin_images/75227ef5.png) + +其中 `AbstractJvmPlugin` 是 Console 提供的基础实现,如数据目录等。 + +## 主类 + +JVM 平台插件的主类应被实现为一个单例(Kotlin `object`,Java 静态初始化的类,详见下文示例)。 + +**Kotlin 使用者的插件主类应继承 [`KotlinPlugin`]。** +**其他 JVM 语言(如 Java)使用者的插件主类应继承 [`JavaPlugin`]。** + +### 定义主类 + +Mirai Console 使用类似 Java `ServiceLoader` 但更灵活的机制加载插件。 + +一个正确的主类定义可以是以下三种任一。注意 "描述" 将会在[下文](#描述)解释。 + +1. Kotlin `object` + +```kotlin +object A : KotlinPlugin( /* 描述 */) +``` + +2. Java 静态初始化单例 `class` + +```java +public final class A extends JavaPlugin { + public static final A INSTANCE = new A(); // 必须 public static, 必须名为 INSTANCE + + private A() { + super( /* 描述 */); + } +} +``` + +3. Java `class` + +使用这种方法时,插件实例由 Console 在合适的时机创建。 + +```java +public final class A extends JavaPlugin { + public A() { // 必须公开且无参 + super( /* 描述 */); + } +} +``` + +### 确认主类正确定义 + +在 [IDE 插件][MCI] 的帮助下,一个正确的插件主类定义的行号处会显示 Mirai 像素风格形象图:![MiraiPixel] + +![PluginMainDeclaration](.images/PluginMainDeclaration.png) + +### 配置主类服务 + +#### 自动配置主类服务 + +[IDE 插件][MCI] 会自动检查主类服务的配置。在没有正确配置时,IDE 将会警告并为你自动配置: +![PluginMainServiceNotConfigured](.images/PluginMainServiceNotConfigured.png) + +#### 手动配置主类服务 + +若无法使用 IntelliJ +插件,可在资源目录 `META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin` +文件内存放插件主类全名(以纯文本 UTF-8 存储,文件内容只包含一行插件主类全名或以 `#` 起始的注释)。 + +也可以使用各种自动化工具配置 service 信息,如使用 Google +的 [AutoService](https://github.com/google/auto/tree/master/service) +。附[在 Kotlin 使用的方法][使用 AutoService]。 + +> 备注:Console 虽然使用这种常用的 service 配置方式,但不使用 ServiceLoader 加载插件实例。Console +> 的加载方式更灵活。 + +## 描述 + +插件主类需要提供一份描述信息。 + +插件可以通过资源文件提供静态的信息,也可以通过构造器动态传递。 + +描述拥有如下属性: + +[语义化版本 2.0.0]: https://semver.org/lang/zh-CN/ + +| 属性 | 可空 | 备注 | +|--------------|-----|-----------------------------| +| id | 否 | 唯一识别标识符,仅能包含英文字母、数字、`.`、`-` | +| version | 否 | 版本号,见补充说明 | +| name | 是 | 供用户阅读的名称,可包含任意字符 | +| author | 是 | 作者信息 | +| dependencies | 是 | 依赖其他插件的 ID 及版本范围 | +| info | 是 | 供用户阅读的描述信息 | + +### 有关插件版本号的补充说明 + +- 插件自身的版本要求遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) + 规范,合格的版本例如:`1.0.0`、`1.0`、`1.0-M1`、`1.0-pre-1`; + +### 有关插件依赖的说明 + +- 插件依赖的版本遵循 [语义化版本 2.0.0](https://semver.org/lang/zh-CN/) + 规范,同时支持 [Apache Ivy 风格表示方法](http://ant.apache.org/ivy/history/latest-milestone/settings/version-matchers.html) + 。 + +在定义时需使用 ID,因为 ID 是唯一的。不支持使用名称(name)。 + +定义必须依赖 net.mamoe.chat-command: + +```yaml +dependencies: + - "net.mamoe.chat-command" +``` + +定义可选依赖 net.mamoe.chat-command: + +```yaml +dependencies: + - "net.mamoe.chat-command?" +``` + +定义必须依赖 1.0 及以上, 2.0 (不包含)以下的 net.mamoe.chat-command 插件: + +```yaml +dependencies: + - "net.mamoe.chat-command:[1.0, 2.0)" +``` + +定义可选依赖 1.0 及以上, 2.0 (不包含)以下的 net.mamoe.chat-command 插件: + +```yaml +dependencies: + - "net.mamoe.chat-command:[1.0, 2.0)?" +``` + +插件的依赖将在下文 [依赖其他插件][#依赖其他插件] 详细说明。 + +### 通过资源文件提供静态信息 + +在资源文件目录(如 `src/main/resources`)创建 `plugin.yml`,并填写上述属性,示例: + +```yaml +id: net.mamoe.chat-command +version: 0.3.0 +name: Chat Command +author: Mamoe Technologies +dependencies: [ ] # 或者不写这行 +info: 允许在聊天环境执行指令。 +``` + +然后在插件主类指定从资源加载: + +*Kotlin* + +```kotlin +object A : KotlinPlugin(JvmPluginDescription.loadFromResource()) +``` + +*Java* + +```java +public final class JExample extends JavaPlugin { + public static final JExample INSTANCE = new JExample(); + + private JExample() { + super(JvmPluginDescription.loadFromResource()); + } +} +``` + +注意,尽管 `loadFromResource` 支持读取任意路径的文件,但也建议使用默认的 `plugin.yml` +。因为目前有计划修改插件信息的读取方式,这种 `plugin.yml` 的读取方式很有可能会继续得到支持。 + +### 在构造器动态提供 + +*注意:由于目前有计划修改插件信息的读取方式,这种构造器动态提供的方法已不再推荐。* + +在构造器动态构造 `JvmPluginDescription`: + +*Kotlin* + +```kotlin +object MyPlugin : KotlinPlugin( + JvmPluginDescription( + // 必要属性 + id = "net.mamoe.chat-command", + version = "1.0.0", + ) { + // 非必须属性 + name("Chat Command") + // author("...") + // dependsOn("...") // 与 YAML 方式类似,如 "net.mamoe.chat-command:[1.0, 2.0)?" + } +) +``` + +*Java* + +```java +public final class JExample extends JavaPlugin { + public static final JExample INSTANCE = new JExample(); + + private JExample() { + super(new JvmPluginDescriptionBuilder( // 必要属性 + "org.example.test-plugin", // id + "1.0.0" // version + ).author("...") // 可选属性,可以不提供, 直接 build + .info("...") + .build() + ); + } +} +``` + +## 插件生命周期与依赖管理 + +每个 JVM 插件有如下状态:初始化、加载、启用、禁用。 + +[//]: # (预加载、) + +[//]: # (- 预加载:Console 识别插件的静态依赖信息。) + +- 初始化:插件主类由插件加载器识别并创建实例(如有必要)。插件的依赖将完成链接(**因此在初始化之后才允许调用依赖**); +- 加载:插件加载器已经识别了所有的插件并根据依赖关系确定了加载顺序。这时插件的 `onLoad()` 回调将会被调用,插件做一次性初始化工作; +- 启用:插件加载器已经加载了所有的插件并调用它们的 `onLoad()`。这时插件的 `onEnable()` + 回调将会被调用,插件开始正常工作; +- 禁用:当用户要求关闭某个插件,或者 Console 正在关闭时插件会被禁用,`onEnable()` + 回调将会被调用,插件停止一切在启用状态时创建的工作。 + +每个插件一定会经历初始化和加载,但可能会经历零次到任意次启用和禁用。根据不同 Console +前端的行为和用户的设置,插件可能只会加载不会启用,也有可能会在禁用后被重新启用。因此插件必须只在 `onEnable` +时创建事件监听等任务,且在禁用时停止这些任务。 + +多个插件的加载是*顺序的*,意味着若一个插件的 `onLoad` +等回调处理缓慢,后续插件的加载也会被延后,即使它们可能没有依赖关系。因此需要让 `onLoad`,`onEnable`,`onDisable` +快速返回。 + +### 有依赖时的加载顺序 + +若插件 A 需要调用另一个插件 B,那么就认为 A 依赖 +B。当存在依赖关系时,被依赖的插件(B)将总是会早于(A)加载。这适用于所有上述回调,即 B 的 `onLoad`、`onEnable` 会早于 +A 调用,而 `onDisable` 会晚于 A 调用。 + +### 不支持热加载和热卸载 + +Mirai Console 不提供热加载和热卸载功能,所有插件只能在服务器启动前加载,在 Console +停止时卸载。([为什么不支持热加载和卸载插件?]) + +只有当插件 A 在其描述中定义了依赖时,才会被允许调用 B 的接口。要了解如何定义,可参考上文 [有关插件依赖的说明]。 + +### Kotlin 协程生命周期管理 + +[`JvmPlugin`] 实现 `CoroutineScope`,并由 Console 内部实现提供其 `coroutineContext`。 + +**所有插件启动的协程都应该受 `JvmPlugin` 作用域的管理**,如要启动一个协程,正确的做法是: + +```kotlin +object MyPluginMain : KotlinPlugin( /* ... */) { + override fun onEnable() { + // 即 MyPluginMain.launch,在当前协程作用域创建任务。 + launch { + delay(1000) + println("一秒钟过去了。") + } + + // globalEventChannel 在当前协程作用域创建事件通道,会在 onDisable 自动关闭。 + globalEventChannel().subscribeAlways { + println("有人离开了群。") + } + } +} +``` + +### Java 线程生命周期管理 + +插件创建的所有线程或异步任务都需要在 `onDisable()` 时关闭。 + +[JavaPluginScheduler]: ../../backend/mirai-console/src/plugin/jvm/JavaPluginScheduler.kt + +若要执行简单的异步、延迟、重复任务,可使用 `getScheduler()` 获取到简单任务调度器。示例: + +```java +public final class JExample extends JavaPlugin { + public static final JExample INSTANCE = new JExample(); + + private JExample() { + // ... + } + + @Override + public onEnable() { + getScheduler().delayed(1000L, () -> System.out.println("一秒钟过去了。")); + } +} +``` + +详细查看 [JavaPluginScheduler]。 + +## 访问数据目录和配置目录 + +[`JvmPlugin`] 实现接口 [`PluginFileExtensions`]。插件可通过 `resolveDataFile` +,`resolveConfigFile` 等方法取得数据目录或配置目录下的文件。 + +- 数据目录(dataFolder)用来存放只供插件内部读取的数据; +- 配置目录(configFolder)用来存放可由用户修改的配置。 + +可以在任何时刻使用这些方法。 + +详见 [`PluginFileExtensions`]。 + +*Java* + +```java +File dataFile=JExample.INSTANCE.resolveDataFile("myDataFile.txt"); + File configFile=JExample.INSTANCE.resolveConfigFile("myConfigFile.txt"); +``` + +### 物理目录路径 + +用 `$root` 表示 Mirai Console 运行路径,`$name` 表示插件名, +在终端前端(Terminal),插件数据目录一般在 `$root/data/$name` +,插件配置目录一般在 `$root/config/$name`。**但是插件不应该依赖这些物理路径,因为在其他前端上没有保证。** + +### 访问 [JAR] 包内资源文件 + +[`JvmPlugin`] 实现接口 [`ResourceContainer`]。插件可通过 `getResource` +,`getResourceAsStream` 等取得插件 [JAR] 包内资源文件。 + +可以在任何时刻使用这些方法。 + +详见 [`ResourceContainer`]。 + +## 构建 + +构建 mirai-console +插件推荐使用 [mirai-console-gradle](../../tools/gradle-plugin/README.md) +(若使用推荐方式创建项目则默认使用)。 + +> 要执行一个名为 `taskName` 的 Gradle 任务,可在项目目录使用 `./gradlew taskName`。 + +### 打包和分发插件 + +执行 Gradle 任务 [`buildPlugin`](../../tools/gradle-plugin/README.md#打包依赖) +即可打包后缀为 `.mirai2.jar` 的插件 +JAR。打包结果输出在 `build/mirai/`。 + +这个文件就是插件的可分发文件。可以放入 Console `plugins` 目录中使用。 + +#### 自 2.11 的变更 + +在 2.11 以前,插件文件后缀为 `.mirai.jar`,而在 2.11 时插件文件后缀修改为了 `.mirai2.jar` +。这是因为依赖打包机制有修改。2.11 起不再打包全部的依赖,而是只打包必要和开发者强制指定的(详见 [插件依赖打包机制](#插件依赖打包机制) +)。 + +- Console 2.11 及以上在扫描插件时若同时发现 `.mirai.jar` 和 `.mirai2.jar` + ,只会加载 `.mirai2.jar`。 +- Console 2.11 以前则会都加载。 + +由于 2.10 及以前版本编译的插件也能在 2.11 运行,用户可以顺利地从 2.10 升级到 2.11。但无法直接从 2.11 降级到 +2.10,降级时需要删除 `.mirai2.jar`。 + +### 插件依赖打包机制 + +在打包插件时, 所有使用的*外部依赖*(1) +都将会存放于 JAR 内的一个*依赖列表*(2)中。 + +依赖将会在用户侧 Console 启动时从[远程仓库](#默认的远程仓库列表)下载并加载。 + +特别地,直接依赖的本地 JAR 和子项目依赖将会直接打包进插件 JAR。 + +> 注释 +> - (1): 外部依赖, 即来自 Maven Central 的依赖, 如 `net.mamoe:mirai-core-api` +> - (2): 具体文件路径 `META-INF/mirai-console-plugin/dependencies-private.txt` +> - (3): 包括直接依赖的本地 JAR (如 `fileTree('libs')`), + 子项目 (`project(':subproject')`) + 和其他显式声明直接打包的依赖, + 更多见 [mirai-console-gradle](../../tools/gradle-plugin/README.md) + +#### 默认的远程仓库列表 + +- Maven + Central:[repo.maven.apache.org](https://repo.maven.apache.org/maven2/) +- 阿里云 Maven Central + 中国代理:[maven.aliyun.com](https://maven.aliyun.com/repository/central) + +默认优先使用阿里云代理仓库,在失败时使用 Maven Central。注意,使用阿里云仓库是不受保证的 – 将来可能会换用其他仓库,但 +Maven +Central 官方仓库不会删除(除非用户在配置中删除,当然这不属于开发插件时的考虑范畴)。 + +用户也可以在配置中自行定义代理仓库,但插件开发者不应该期望于用户自行添加包含插件使用的依赖的仓库。**所以插件只应该使用在 Maven +Central +的依赖,且将使用的其他所有依赖[打包](#调整依赖打包策略)到插件 JAR。** + +### 类加载隔离 + +插件仅能访问自身项目、依赖的库、Console、以及在描述中定义了的依赖的插件。每个插件依赖的库都会被隔离,不同插件可以使用不同版本的同一个库,互不冲突。** +但不应该在依赖另一个插件时使用不同版本的依赖,这可能导致不确定的后果。** + +#### 优先加载插件自身及依赖 + +在有潜在类冲突时,将优先选择插件内部实现以及定义的依赖。每个类的搜索顺序是: + +- 插件自身以及[强制打包](#调整依赖打包策略)的依赖 +- 插件定义的仓库依赖 +- 插件定义的其他插件依赖及它们的依赖(间接) +- mirai-console 及 mirai-core 使用的公开依赖,包含: + - kotlin-reflect + - kotlinx-coroutines-jdk8 + - kotlinx-serialization-json + - ktor-client-okhttp + > 这些依赖的版本可在 [buildSrc](../../../buildSrc/src/main/kotlin/Versions.kt) + 查看 + +### 调整依赖打包策略 + +要强制打包或忽略某个依赖,请参阅 [Gradle 插件文档](../../tools/gradle-plugin/README.md#打包依赖) +。 +要了解什么情况下需要强制打包,请参阅 [插件依赖打包机制](#插件依赖打包机制)。 + +### 发布插件到 mirai-console-loader + +插件中心仍在开发中。 + +> 下一步,[Commands](../Commands.md#mirai-console-backend---commands) +> +> 返回 [开发文档索引](../README.md#mirai-console) + diff --git a/mirai-console/docs/plugin/Plugins.md b/mirai-console/docs/plugin/Plugins.md new file mode 100644 index 000000000..306093804 --- /dev/null +++ b/mirai-console/docs/plugin/Plugins.md @@ -0,0 +1,113 @@ +# Mirai Console Backend - Plugins + +[`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 + + +[`PluginData`]: ../../backend/mirai-console/src/data/PluginData.kt + +[`PluginConfig`]: ../../backend/mirai-console/src/data/PluginConfig.kt + +[`PluginDataStorage`]: ../../backend/mirai-console/src/data/PluginDataStorage.kt + +[`ExportManager`]: ../../backend/mirai-console/src/plugin/jvm/ExportManager.kt + +[`MiraiConsole`]: ../../backend/mirai-console/src/MiraiConsole.kt + +[`MiraiConsoleImplementation`]: ../../backend/mirai-console/src/MiraiConsoleImplementation.kt + +[`Command`]: ../../backend/mirai-console/src/command/Command.kt + +[`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 + +[`Annotations`]: ../../backend/mirai-console/src/util/Annotations.kt + +[`ConsoleInput`]: ../../backend/mirai-console/src/util/ConsoleInput.kt + +[`JavaPluginScheduler`]: ../../backend/mirai-console/src/plugin/jvm/JavaPluginScheduler.kt + +[`ResourceContainer`]: ../../backend/mirai-console/src/plugin/ResourceContainer.kt + +[`PluginFileExtensions`]: ../../backend/mirai-console/src/plugin/PluginFileExtensions.kt + +[`AutoSavePluginDataHolder`]: ../../backend/mirai-console/src/data/PluginDataHolder.kt#L45 + +[Kotlin]: https://www.kotlincn.net/ + +[Java]: https://www.java.com/zh_CN/ + +[JVM]: https://zh.wikipedia.org/zh-cn/Java%E8%99%9A%E6%8B%9F%E6%9C%BA + +[JAR]: https://zh.wikipedia.org/zh-cn/JAR_(%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F) + +[使用 AutoService]: ../QA.md#使用-autoservice + +[JVMPlugin]: ./JVMPlugin.md + +Mirai Console (简称 Console) 运行在 [JVM],支持使用 [Kotlin] 或 [Java] 等 JVM +语言编写的插件。 + +本章节简要介绍 Console 插件架构(与平台无关的基础架构)。 + +## 通用的插件接口 - [`Plugin`] + +所有 Console 插件都必须直接或间接地实现 [`Plugin`] 接口。 + +> **解释 *插件***:只要实现了 [`Plugin`] 接口的对象都可以叫做「Mirai Console 插件」,简称 「插件」。 +> 为了便捷,内含 [`Plugin`] 实现的一个 [JAR] 文件也可以被称为「插件」。 + +基础的 [`Plugin`] 很通用,它只拥有很少的成员: + +```kotlin +interface Plugin : CommandOwner { // CommandOwner 表示该对象可以创建指令 + val isEnabled: Boolean // 当插件已开启时返回 true + val loader: PluginLoader<*, *> // 能处理这个 Plugin 实例的 PluginLoader +} +``` + +[`Plugin`] 接口拥有强扩展性,以支持 Mirai Console 统一管理使用其他编程语言编写的插件 +(详见进阶章节 [扩展 - PluginLoader](../Extensions.md))。 + +> 除非你是在实现新种类插件,否则不要直接实现 `Plugin` 接口。 + +## 插件加载器 - [`PluginLoader`] + +Mirai Console 支持使用多个插件加载器来加载多种类型插件。每个插件加载器都支持一种类型的插件。 + +Mirai Console 内置 [`JvmPluginLoader`] 以加载 JVM +平台插件(见下文),并允许这些插件注册扩展的插件加载器(见章节 [扩展](../Extensions.md)) +,以支持读取其他语言编写的插件并接入 +Console 插件管理系统。 + +## 总结 + +Mirai Console 提供抽象的插件及其加载器接口,支持扩展。各类插件行为由其加载器确定。插件作者需要基于特定的插件平台实现,如 +Console 内置的 [JVM 平台][JVMPlugin]。 + +## 继续阅读 + +- [JVM 平台插件详情][JVMPlugin] +- [编写插件加载器](../Extensions.md) \ No newline at end of file diff --git a/mirai-console/tools/gradle-plugin/README.md b/mirai-console/tools/gradle-plugin/README.md index acc294913..a5a3527b6 100644 --- a/mirai-console/tools/gradle-plugin/README.md +++ b/mirai-console/tools/gradle-plugin/README.md @@ -4,25 +4,33 @@ Mirai Console Gradle 插件。 ## 在构建中使用插件 -参考 [ConfiguringProjects](../../docs/ConfiguringProjects.md#b使用-gradle-插件配置项目)。 +参考 [ConfiguringProjects](../../docs/ConfiguringProjects.md#b使用-gradle-插件配置项目) +。 ## 功能 - 为 `main` 源集配置 `mirai-core-api`,`mirai-console` 依赖 - 为 `test` 源集配置 `mirai-core`, `mirai-console-terminal` 的依赖 (用于启动测试) - 配置 Kotlin 编译目标为 1.8 -- 配置 Kotlin 编译器 jvm-default 设置为 `all`, 即为所有接口中的默认实现生成 Java 1.8 起支持的 `default` 方法 +- 配置 Kotlin 编译器 `jvm-default` 设置为 `all`, 即为所有接口中的默认实现生成 Java 1.8 + 起支持的 `default` 方法 - 配置 Java 编译目标为 1.8 - 配置 Java 编译编码为 UTF-8 - 配置插件 JAR 打包构建任务 `buildPlugin`(带依赖, 成品 JAR 可以被 Mirai Console 加载) -支持 Kotlin 多平台项目(Multiplatform Projects)。每个 JVM 或 Android 目标平台都会被如上配置,对应打包任务带有编译目标的名称,如 `buildPluginJvm` +支持 Kotlin 多平台项目(Multiplatform Projects)。对于 MPP,每个 JVM 或 Android +目标平台都会被如上配置,对应打包任务带有编译目标的名称,如 `buildPluginJvm`。 -### `buildPlugin` +### 任务 `buildPlugin` + +用于打包插件和依赖为可以放入 Mirai Console `plugins` 目录加载的插件 JAR。 + +### 任务 `buildPluginLegacy` 用于打包插件和依赖为可以放入 Mirai Console `plugins` 目录加载的插件 JAR。 #### 执行 `buildPlugin` + ```shell script ./gradlew buildPlugin ``` @@ -36,11 +44,12 @@ Mirai Console Gradle 插件。 ```kotlin mirai { // this: MiraiConsoleExtension - + // 修改配置,如: + jvmTarget = JavaVersion.VERSION_1_8 } ``` -DSL 详见 [MiraiConsoleExtension](src/main/kotlin/MiraiConsoleExtension.kt) +有关所有可修改的配置,参见 [MiraiConsoleExtension](src/main/kotlin/MiraiConsoleExtension.kt) 。 ### 修改 Java 编译目标 @@ -55,55 +64,74 @@ mirai { // this: MiraiConsoleExtension ### 打包依赖 -Mirai Console Gradle 在打包 JAR(`buildPlugin`) 时不会携带任何外部依赖, -而是会保存一份依赖列表,在加载插件时下载, 如果您使用了不可在 `Maven Central` 搜索到的依赖, 请使用以下配置告知 -mirai-console-gradle +使用任务 `buildPlugin` 即可打包插件 JAR。打包结果输出在 `build/mirai/`。 -```groovy -dependencies { - implementation "org.example:test:1.0.0" - - // 无需版本号 - shadowLink "org.example:test" - // build.gradle.kts - "shadowLink"("org.example:test") -} -``` - -特别的, 如果使用了子项目 (Gradle MultiProjects), Mirai Console Gradle 默认也会打包进 JAR. - -如果您希望 Mirai Console Gradle 像处理一般依赖一样处理 Gradle 子项目, 请使用以下配置告知 - -```groovy -dependencies { - implementation project(":nested") - - asNormalDep project(":nested") - // build.gradle.kts - "asNormalDep"(project(":nested")) -} -``` - -### `publishPlugin` - -配置好 Bintray 参数,使用 `./gradlew publishPlugin` 可自动发布并上传插件到 Bintray。 - -如果仓库是公开的,上传的插件在未来可以被 [mirai-console-loader](https://github.com/iTXTech/mirai-console-loader) 自动识别并展示在社区插件列表中。 +自 2.11,Mirai Console Gradle 在打包 JAR(`buildPlugin`) 时默认不会携带外部依赖, +而是会保存一份依赖列表,在加载插件时下载。如果您使用了不可在默认仓库搜索到的依赖, 请以如下配置将依赖打包进入 JAR。 ```kotlin -mirai { - publishing { - repo = "mirai" - packageName = "chat-command" - } +dependencies { + implementation("org.example:test:1.0.0") // 正常地添加一个普通依赖,用于编译 + "shadowLink"("org.example:test") // 告知 mirai-console 在打包插件时包含此依赖;无需包含版本号 } ``` -*2021/3/21 更新:* 由于 Bintray JCenter 即将关闭,随着论坛的发展,mirai 正在策划插件中心服务。待插件中心完成后将会提供更好的插件分发平台。 +特别的, 如果使用了子项目,Mirai Console Gradle 默认也会打包进 JAR,通常这也是期望的行为。 + +如果您希望 Mirai Console Gradle 像处理一般依赖一样处理 Gradle 子项目(不打包),请使用以下配置: + +```kotlin +dependencies { + implementation(project(":nested")) // 正常地添加一个子项目依赖,用于编译 + "asNormalDep"(project(":nested")) // 告知 mirai-console 在打包插件时将此子项目依赖作为普通依赖处理,即不打包 +} +``` + +> 要获取有关插件依赖的更多信息,可参考[插件文档](../../docs/plugin/JVMPlugin.md)。 + +#### 如何确定是否需要打包依赖 + +如果插件依赖的都是可在 Maven Central 找到的构建,则无需打包。 + +[//]: # (### `publishPlugin`) + +[//]: # () + +[//]: # (配置好 Bintray 参数,使用 `./gradlew publishPlugin` 可自动发布并上传插件到 Bintray。) + +[//]: # () + +[//]: # (如果仓库是公开的,上传的插件在未来可以被 [mirai-console-loader](https://github.com/iTXTech/mirai-console-loader)) + +[//]: # (自动识别并展示在社区插件列表中。) + +[//]: # () + +[//]: # (```kotlin) + +[//]: # (mirai {) + +[//]: # ( publishing {) + +[//]: # ( repo = "mirai") + +[//]: # ( packageName = "chat-command") + +[//]: # ( }) + +[//]: # (}) + +[//]: # (```) + +[//]: # () + +[//]: # (*2021/3/21 更新:* 由于 Bintray JCenter 即将关闭,随着论坛的发展,mirai) + +[//]: # (正在策划插件中心服务。待插件中心完成后将会提供更好的插件分发平台。) #### 排除依赖 (过时) -如果要在打包 JAR(`buildPluginLegacy`)时排除一些依赖,请使用如下配置: +在 2.11 以前,如果要在打包 JAR(`buildPluginLegacy`)时排除一些依赖,请使用如下配置: ```kotlin mirai {