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同理]

@@ -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)