Merge remote-tracking branch 'origin/master'

This commit is contained in:
Him188 2020-03-31 19:54:02 +08:00
commit b8cd4afbc0
17 changed files with 209 additions and 42 deletions

View File

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

View File

@ -105,6 +105,10 @@ IDEA分付费的Ultimate版和免费的Community版选择自己的系统后
13: 插件环境正式完成<br>
<br>
<br>
<br>
#### 如何打包插件
1: 根据下图帮助打开gradle window[maven同理]<br>
![打开window](assets/ideaplugin6.jpg)<br>
@ -116,6 +120,11 @@ IDEA分付费的Ultimate版和免费的Community版选择自己的系统后
PS: 如果要打包有依赖lib的插件, 请继续向后读<br>
## 下一步
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)
<i>本章部分章节引用自[搭建环境 - Nukkit插件从0开始](https://www.cnblogs.com/xtypr/p/nukkit_plugin_start_from_0_build_environment.html),</i>

View File

@ -0,0 +1,2 @@
这是一个demo, 请将这里当做你的项目根目录

View File

@ -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<MessageRecallEvent> { 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<MessageReceipt<? extends Contact>> future = event.getSender().sendMessageAsync("Async send"); // 异步发送
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
});
}
public void onEnable(){
logger.info("Plugin loaded!");
}
}

View File

@ -0,0 +1,6 @@
name: "Example"
author: "你的名字"
version: "0.1.0"
main: "my_package_name.ExamplePluginBase"
info: "My info"
depends: []

View File

@ -0,0 +1,3 @@
由于文件较大, 请选择上面的
pdf/docs/pages一种进行下载观看

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -21,7 +21,7 @@ Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持
高效率插件支持机器人框架
### 插件开发与获取
[插件中心](https://github.com/mamoe/mirai-plugins)
[插件中心](https://github.com/mamoe/mirai-plugins) <br>
[mirai-console插件开发快速上手](PluginDocs/ToStart.MD)
### 使用

View File

@ -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"
}

View File

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

View File

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

View File

@ -44,6 +44,5 @@ interface PluginCenter {
* null则没有
*/
suspend fun findPlugin(name:String):PluginInfo?
}

View File

@ -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 <T:Any> setIfAbsent(key: String, value: T)
fun <T:Any> setIfAbsent(key: String, valueInitializer: Config.() -> T)
fun asMap(): Map<String, Any>
fun save()
@ -256,6 +261,16 @@ internal fun <T : Any> Config.smartCastInternal(propertyName: String, _class: KC
interface ConfigSection : Config, MutableMap<String, Any> {
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<String, Any> {
return get(key) != null
}
override fun setIfAbsent(key: String, value: Any) {
if (!exist(key)) set(key, value)
override fun <T : Any> setIfAbsent(key: String, value: T) {
putIfAbsent(key, value)
}
override fun <T : Any> setIfAbsent(key: String, valueInitializer: Config.() -> T) {
if(this.exist(key)){
put(key,valueInitializer.invoke(this))
}
}
}
internal inline fun <reified T:Any> ConfigSection.smartGet(key:String):T{
return this.smartCastInternal(key,T::class)
}
@Serializable
open class ConfigSectionImpl : ConcurrentHashMap<String, Any>(),
ConfigSection {
override fun set(key: String, value: Any) {
super.put(key, value)
@ -368,10 +394,6 @@ open class ConfigSectionImpl : ConcurrentHashMap<String, Any>(),
override fun save() {
}
override fun setIfAbsent(key: String, value: Any) {
this.putIfAbsent(key, value)//atomic
}
}
open class ConfigSectionDelegation(

View File

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

View File

@ -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<String>,
@ -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<out PluginBase>.againstPermission() {
kotlin.runCatching {
trySetAccessibleMethod?.let { it.invoke(this) }
@ -315,3 +307,5 @@ private fun Constructor<out PluginBase>.againstPermission() {
}
}
}
internal class PluginsClassLoader(files: Collection<File>, parent: ClassLoader) : URLClassLoader(files.map{it.toURI().toURL()}.toTypedArray(), parent)