mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-22 13:46:13 +08:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
b8cd4afbc0
@ -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)
|
@ -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>
|
||||
|
||||
|
2
PluginDocs/demo/README.md
Normal file
2
PluginDocs/demo/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
这是一个demo, 请将这里当做你的项目根目录
|
||||
|
@ -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!");
|
||||
}
|
||||
|
||||
}
|
6
PluginDocs/demo/src/main/resources/plugin.yml
Normal file
6
PluginDocs/demo/src/main/resources/plugin.yml
Normal file
@ -0,0 +1,6 @@
|
||||
name: "Example"
|
||||
author: "你的名字"
|
||||
version: "0.1.0"
|
||||
main: "my_package_name.ExamplePluginBase"
|
||||
info: "My info"
|
||||
depends: []
|
@ -0,0 +1,3 @@
|
||||
由于文件较大, 请选择上面的
|
||||
|
||||
pdf/docs/pages一种进行下载观看
|
BIN
PluginDocs/kotlin/mirai-myfirstplugin-kotlin.docx
Normal file
BIN
PluginDocs/kotlin/mirai-myfirstplugin-kotlin.docx
Normal file
Binary file not shown.
BIN
PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pages
Normal file
BIN
PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pages
Normal file
Binary file not shown.
BIN
PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pdf
Normal file
BIN
PluginDocs/kotlin/mirai-myfirstplugin-kotlin.pdf
Normal file
Binary file not shown.
@ -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)
|
||||
|
||||
### 使用
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,5 @@ interface PluginCenter {
|
||||
* null则没有
|
||||
*/
|
||||
suspend fun findPlugin(name:String):PluginInfo?
|
||||
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user