diff --git a/PluginDocs/PluginStructure.MD b/PluginDocs/PluginStructure.MD index e69de29bb..9c6c9d278 100644 --- a/PluginDocs/PluginStructure.MD +++ b/PluginDocs/PluginStructure.MD @@ -0,0 +1,26 @@ +# 插件结构 + +请注意, 如果你有IDEA,推荐使用idea插件, 可以全自动配置环境, 插件结构, debug运行
+本文是为没有IDEA, 也想开发mirai-console插件的人准备的, 除去插件结构外, 您还需要自己配置运行环境
+ + +### Plugin.yml +你应当有一个plugin.yml, 放置在resources文件夹下
+``` +name: "插件名字" +author: "作者名字" +version: "0.1.0" +main: "my_package_name.ExamplePluginBase" +info: "插件介绍" +depends: [] +``` +其中main指向 你的PluginBase + +### PluginBase +pluginBase为你插件的启动点, 生命管理周期, 他应该被放到src/main/java或src/main/kotlin下
+一般来说, 他要被放到一个package下, 假如它叫MyPluginBase, 放到package为my.package下, 那么plugin.yml的main则要写my.package.MyPluginBase
+你可以在下面的DEMO中找到PluginBase的一些简单例子 + +

+## DEMO +[插件结构例子](https://github.com/mamoe/mirai-console/tree/master/PluginDocs/demo) \ No newline at end of file diff --git a/PluginDocs/ToStart.MD b/PluginDocs/ToStart.MD index 572289ead..f8ca2bdbf 100644 --- a/PluginDocs/ToStart.MD +++ b/PluginDocs/ToStart.MD @@ -105,6 +105,10 @@ IDEA分付费的Ultimate版和免费的Community版,选择自己的系统后 13: 插件环境正式完成
+
+
+
+ #### 如何打包插件 1: 根据下图帮助打开gradle window[maven同理]
![打开window](assets/ideaplugin6.jpg)
@@ -116,6 +120,11 @@ IDEA分付费的Ultimate版和免费的Community版,选择自己的系统后 PS: 如果要打包有依赖lib的插件, 请继续向后读
+## 下一步 + +Java 玩家->[我的第一个插件](https://github.com/mamoe/mirai-console/blob/master/PluginDocs/java/MyFirstPlugin.MD) +Kotlin玩家->[我的第一个插件](https://github.com/mamoe/mirai-console/blob/master/PluginDocs/kotlin/MyFirstPlugin.MD) + 本章部分章节引用自[搭建环境 - Nukkit插件从0开始](https://www.cnblogs.com/xtypr/p/nukkit_plugin_start_from_0_build_environment.html), diff --git a/PluginDocs/demo/README.md b/PluginDocs/demo/README.md new file mode 100644 index 000000000..e6de92065 --- /dev/null +++ b/PluginDocs/demo/README.md @@ -0,0 +1,2 @@ +这是一个demo, 请将这里当做你的项目根目录 + diff --git a/PluginDocs/demo/src/main/java/my_package_name/MyPluginBase.java b/PluginDocs/demo/src/main/java/my_package_name/MyPluginBase.java new file mode 100644 index 000000000..3213498d1 --- /dev/null +++ b/PluginDocs/demo/src/main/java/my_package_name/MyPluginBase.java @@ -0,0 +1,99 @@ +//在这里创建你的PluginBase, 他应该是一个object +/** +kotlin example + + +object ExamplePluginMain : PluginBase() { + override fun onLoad() { + super.onLoad() + } + + override fun onEnable() { + super.onEnable() + + logger.info("Plugin loaded!") + + subscribeMessages { + "greeting" reply { "Hello ${sender.nick}" } + } + + subscribeAlways { event -> + logger.info { "${event.authorId} 的消息被撤回了" } + } + } +} + + + +java example + + +class ExamplePluginBase extends PluginBase { + + public void onLoad(){ + bot.getFriends().forEach(friend -> { + System.out.println(friend.getId() + ":" + friend.getNick()); + return Unit.INSTANCE; // kotlin 的所有函数都有返回值. Unit 为最基本的返回值. 请在这里永远返回 Unit + }); + + Events.subscribeAlways(GroupMessage.class, (GroupMessage event) -> { + + if (event.getMessage().contains("reply")) { + // 引用回复 + final QuoteReplyToSend quote = MessageUtils.quote(event.getMessage(), event.getSender()); + event.getGroup().sendMessage(quote.plus("引用回复")); + + } else if (event.getMessage().contains("at")) { + // at + event.getGroup().sendMessage(new At(event.getSender())); + + } else if (event.getMessage().contains("permission")) { + // 成员权限 + event.getGroup().sendMessage(event.getPermission().toString()); + + } else if (event.getMessage().contains("mixed")) { + // 复合消息, 通过 .plus 连接两个消息 + event.getGroup().sendMessage( + MessageUtils.newImage("{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png") // 演示图片, 可能已过期 + .plus("Hello") // 文本消息 + .plus(new At(event.getSender())) // at 群成员 + .plus(AtAll.INSTANCE) // at 全体成员 + ); + + } else if (event.getMessage().contains("recall1")) { + event.getGroup().sendMessage("你看不到这条消息").recall(); + // 发送消息马上就撤回. 因速度太快, 客户端将看不到这个消息. + + } else if (event.getMessage().contains("recall2")) { + final Job job = event.getGroup().sendMessage("3秒后撤回").recallIn(3000); + + // job.cancel(new CancellationException()); // 可取消这个任务 + + } else if (event.getMessage().contains("上传图片")) { + File file = new File("myImage.jpg"); + if (file.exists()) { + final Image image = event.getGroup().uploadImage(new File("myImage.jpg")); + // 上传一个图片并得到 Image 类型的 Message + + final String imageId = image.getImageId(); // 可以拿到 ID + final Image fromId = MessageUtils.newImage(imageId); // ID 转换得到 Image + + event.getGroup().sendMessage(image); // 发送图片 + } + + } else if (event.getMessage().contains("friend")) { + final Future> future = event.getSender().sendMessageAsync("Async send"); // 异步发送 + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + }); + } + + public void onEnable(){ + logger.info("Plugin loaded!"); + } + +} diff --git a/PluginDocs/demo/src/main/resources/plugin.yml b/PluginDocs/demo/src/main/resources/plugin.yml new file mode 100644 index 000000000..0078c2a03 --- /dev/null +++ b/PluginDocs/demo/src/main/resources/plugin.yml @@ -0,0 +1,6 @@ +name: "Example" +author: "你的名字" +version: "0.1.0" +main: "my_package_name.ExamplePluginBase" +info: "My info" +depends: [] \ No newline at end of file diff --git a/PluginDocs/kotlin/MyFirstPlugin.MD b/PluginDocs/kotlin/MyFirstPlugin.MD index e69de29bb..04873e652 100644 --- a/PluginDocs/kotlin/MyFirstPlugin.MD +++ b/PluginDocs/kotlin/MyFirstPlugin.MD @@ -0,0 +1,3 @@ +由于文件较大, 请选择上面的 + +pdf/docs/pages一种进行下载观看 diff --git a/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.docx b/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.docx new file mode 100644 index 000000000..0cdd2daa9 Binary files /dev/null and b/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.docx differ diff --git a/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pages b/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pages new file mode 100644 index 000000000..2cab74c74 Binary files /dev/null and b/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pages differ diff --git a/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pdf b/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pdf new file mode 100644 index 000000000..fc761cb86 Binary files /dev/null and b/PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pdf differ diff --git a/README.md b/README.md index 7a336c9c1..fc7a66115 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持 高效率插件支持机器人框架 ### 插件开发与获取 -[插件中心](https://github.com/mamoe/mirai-plugins) +[插件中心](https://github.com/mamoe/mirai-plugins)
[mirai-console插件开发快速上手](PluginDocs/ToStart.MD) ### 使用 diff --git a/buildSrc/src/main/kotlin/versions.kt b/buildSrc/src/main/kotlin/versions.kt index ea425ad16..4b5c4f69d 100644 --- a/buildSrc/src/main/kotlin/versions.kt +++ b/buildSrc/src/main/kotlin/versions.kt @@ -12,8 +12,8 @@ import org.gradle.kotlin.dsl.DependencyHandlerScope object Versions { object Mirai { const val core = "0.31.0" - const val console = "0.4.0" - const val consoleGraphical = "0.0.4" + const val console = "0.4.1" + const val consoleGraphical = "0.0.5" const val consoleWrapper = "0.2.0" } diff --git a/mirai-console-graphical/build.gradle.kts b/mirai-console-graphical/build.gradle.kts index a10af4843..bf334c8c0 100644 --- a/mirai-console-graphical/build.gradle.kts +++ b/mirai-console-graphical/build.gradle.kts @@ -66,7 +66,7 @@ fun ktor(id: String, version: String) = "io.ktor:ktor-$id:$version" dependencies { compileOnly("net.mamoe:mirai-core-jvm:${Versions.Mirai.core}") - compileOnly(project(":mirai-console")) + implementation(project(":mirai-console")) api(group = "no.tornado", name = "tornadofx", version = "1.7.19") api(group = "com.jfoenix", name = "jfoenix", version = "9.0.8") diff --git a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt index a871a17b3..a50453e12 100644 --- a/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt +++ b/mirai-console-graphical/src/main/kotlin/net/mamoe/mirai/console/graphical/view/PrimaryView.kt @@ -10,6 +10,7 @@ import javafx.scene.control.ButtonType import javafx.scene.control.Tab import javafx.scene.control.TabPane import javafx.scene.image.Image +import javafx.scene.input.Clipboard import javafx.scene.input.KeyCode import javafx.scene.layout.Priority import javafx.stage.FileChooser @@ -21,6 +22,7 @@ import net.mamoe.mirai.console.graphical.util.jfxButton import net.mamoe.mirai.console.graphical.util.jfxListView import net.mamoe.mirai.console.graphical.util.jfxTabPane import tornadofx.* +import tornadofx.Stylesheet.Companion.contextMenu class PrimaryView : View() { @@ -194,6 +196,16 @@ private fun TabPane.logTab( graphic = label(it.first) { maxWidthProperty().bind(this@listview.widthProperty()) isWrapText = true + + + contextmenu { + item("复制").action { + Clipboard.getSystemClipboard().putString(it.first) + } + item("删除").action { + logs.remove(it) + } + } } } } diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt index 5da74fd9d..c246b89d9 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/center/PluginCenter.kt @@ -44,6 +44,5 @@ interface PluginCenter { * null则没有 */ suspend fun findPlugin(name:String):PluginInfo? - } diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt index 963f8eaed..b49b6ed87 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/ConfigSection.kt @@ -18,7 +18,6 @@ import com.moandjiezana.toml.TomlWriter import kotlinx.serialization.Serializable import kotlinx.serialization.UnstableDefault import net.mamoe.mirai.utils.MiraiInternalAPI -import net.mamoe.mirai.utils._miraiContentToString import net.mamoe.mirai.utils.io.encodeToString import org.yaml.snakeyaml.Yaml import java.io.File @@ -57,7 +56,13 @@ interface Config { operator fun get(key: String): Any? operator fun contains(key: String): Boolean fun exist(key: String): Boolean - fun setIfAbsent(key: String, value: Any) + /** + * 设置 key = value (如果value不存在则valueInitializer会被调用) + * 之后返回当前key对应的值 + * */ + fun setIfAbsent(key: String, value: T) + fun setIfAbsent(key: String, valueInitializer: Config.() -> T) + fun asMap(): Map fun save() @@ -256,6 +261,16 @@ internal fun Config.smartCastInternal(propertyName: String, _class: KC interface ConfigSection : Config, MutableMap { + companion object{ + fun create():ConfigSection{ + return ConfigSectionImpl() + } + + fun new():ConfigSection{ + return this.create() + } + } + override fun getConfigSection(key: String): ConfigSection { val content = get(key) ?: throw NoSuchElementException(key) if (content is ConfigSection) { @@ -336,13 +351,24 @@ interface ConfigSection : Config, MutableMap { return get(key) != null } - override fun setIfAbsent(key: String, value: Any) { - if (!exist(key)) set(key, value) + override fun setIfAbsent(key: String, value: T) { + putIfAbsent(key, value) } + + override fun setIfAbsent(key: String, valueInitializer: Config.() -> T) { + if(this.exist(key)){ + put(key,valueInitializer.invoke(this)) + } + } +} + +internal inline fun ConfigSection.smartGet(key:String):T{ + return this.smartCastInternal(key,T::class) } @Serializable open class ConfigSectionImpl : ConcurrentHashMap(), + ConfigSection { override fun set(key: String, value: Any) { super.put(key, value) @@ -368,10 +394,6 @@ open class ConfigSectionImpl : ConcurrentHashMap(), override fun save() { } - - override fun setIfAbsent(key: String, value: Any) { - this.putIfAbsent(key, value)//atomic - } } open class ConfigSectionDelegation( diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt index a6c101734..ccd1ed41d 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginBase.kt @@ -141,11 +141,9 @@ class PluginDescription( internal var loaded: Boolean = false, internal var noCircularDepend: Boolean = true ) { - override fun toString(): String { return "name: $name\nauthor: $author\npath: $basePath\nver: $version\ninfo: $info\ndepends: $depends" } - companion object { fun readFromContent(content_: String): PluginDescription { with(Config.load(content_, "yml")) { @@ -185,6 +183,3 @@ class PluginDescription( } } } - -internal class PluginClassLoader(file: File, parent: ClassLoader) : - URLClassLoader(arrayOf(file.toURI().toURL()), parent) diff --git a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt index 6a47a4e84..b8c8e0385 100644 --- a/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt +++ b/mirai-console/src/main/kotlin/net/mamoe/mirai/console/plugins/PluginManager.kt @@ -23,6 +23,7 @@ import java.io.InputStream import java.lang.reflect.Constructor import java.lang.reflect.Method import java.net.URL +import java.net.URLClassLoader import java.util.jar.JarFile @@ -100,6 +101,9 @@ object PluginManager { } } + val pluginsClassLoader = PluginsClassLoader(pluginsLocation.values,this.javaClass.classLoader) + + //不仅要解决A->B->C->A, 还要解决A->B->C->A fun checkNoCircularDepends( target: PluginDescription, needDepends: List, @@ -125,51 +129,41 @@ object PluginManager { } } - pluginsFound.values.forEach { checkNoCircularDepends(it, it.depends, mutableListOf()) } - //load - - + //load plugin fun loadPlugin(description: PluginDescription): Boolean { if (!description.noCircularDepend) { logger.error("Failed to load plugin " + description.name + " because it has circular dependency") return false } - //load depends first + if(description.loaded || nameToPluginBaseMap.containsKey(description.name)){ + return true + } + description.depends.forEach { dependent -> if (!pluginsFound.containsKey(dependent)) { logger.error("Failed to load plugin " + description.name + " because it need " + dependent + " as dependency") return false } val depend = pluginsFound[dependent]!! - //还没有加载 - if (!depend.loaded && !loadPlugin(pluginsFound[dependent]!!)) { + + if (!loadPlugin(depend)) { logger.error("Failed to load plugin " + description.name + " because " + dependent + " as dependency failed to load") return false } } - //在这里所有的depends都已经加载了 - - //real load logger.info("loading plugin " + description.name) try { - val pluginClass = try { - PluginClassLoader( - (pluginsLocation[description.name]!!), - this.javaClass.classLoader - ).loadClass(description.basePath) + val pluginClass = try{ + pluginsClassLoader.loadClass(description.basePath) } catch (e: ClassNotFoundException) { - logger.info("failed to find Main: " + description.basePath + " checking if it's kotlin's path") - PluginClassLoader( - (pluginsLocation[description.name]!!), - this.javaClass.classLoader - ).loadClass("${description.basePath}Kt") + pluginsClassLoader.loadClass("${description.basePath}Kt") } return try { @@ -285,7 +279,6 @@ object PluginManager { return null } - /** * 根据插件名字找Jar中的文件 * null => 没找到 @@ -297,8 +290,6 @@ object PluginManager { jar.entries().asSequence().filter { it.name == toFind }.firstOrNull() ?: return null return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream } - - } @@ -306,6 +297,7 @@ private val trySetAccessibleMethod: Method? = runCatching { Class.forName("java.lang.reflect.AccessibleObject").getMethod("trySetAccessible") }.getOrNull() + private fun Constructor.againstPermission() { kotlin.runCatching { trySetAccessibleMethod?.let { it.invoke(this) } @@ -314,4 +306,6 @@ private fun Constructor.againstPermission() { this.isAccessible = true } } -} \ No newline at end of file +} + +internal class PluginsClassLoader(files: Collection, parent: ClassLoader) : URLClassLoader(files.map{it.toURI().toURL()}.toTypedArray(), parent)