Conflicts:
	mirai-console/src/main/kotlin/net/mamoe/mirai/console/command/CommandManager.kt
This commit is contained in:
jiahua.liu 2020-03-22 18:14:48 +08:00
commit de742bf904
26 changed files with 538 additions and 370 deletions

View File

@ -1,7 +1,7 @@
# style guide
kotlin.code.style=official
# config
miraiVersion=0.26.1
miraiVersion=0.28.0
miraiConsoleVersion=0.3.4
miraiConsoleWrapperVersion=0.1.3
kotlin.incremental.multiplatform=true

View File

@ -6,9 +6,11 @@ import javafx.stage.Modality
import javafx.stage.StageStyle
import kotlinx.coroutines.delay
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.graphical.model.*
import net.mamoe.mirai.console.graphical.view.VerificationCodeFragment
import net.mamoe.mirai.console.graphical.view.dialog.InputDialog
import net.mamoe.mirai.console.graphical.view.dialog.VerificationCodeFragment
import net.mamoe.mirai.console.plugins.PluginManager
import net.mamoe.mirai.console.utils.MiraiConsoleUI
import net.mamoe.mirai.network.WrongPasswordException
@ -30,10 +32,10 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI {
val consoleInfo = ConsoleInfo()
fun login(qq: String, psd: String) {
MiraiConsole.CommandProcessor.runConsoleCommandBlocking("/login $qq $psd")
CommandManager.runCommand(ConsoleCommandSender, "/login $qq $psd")
}
fun sendCommand(command: String) = MiraiConsole.CommandProcessor.runConsoleCommandBlocking(command)
fun sendCommand(command: String) = CommandManager.runCommand(ConsoleCommandSender, command)
override fun pushLog(identity: Long, message: String) = Platform.runLater {
fun ObservableList<*>.trim() {
@ -78,13 +80,15 @@ class MiraiGraphicalUIController : Controller(), MiraiConsoleUI {
}
}
override suspend fun requestInput(): String {
val model = VerificationCodeModel()
find<VerificationCodeFragment>(Scope(model)).openModal(
modality = Modality.APPLICATION_MODAL,
resizable = false
)
return model.code.value
override suspend fun requestInput(hint: String): String {
var ret: String? = null
// UI必须在UI线程执行requestInput在协程种被调用
Platform.runLater {
ret = InputDialog(hint).open()
}
while (ret == null) { delay(1000) }
return ret!!
}
override fun pushBotAdminStatus(identity: Long, admins: List<Long>) = Platform.runLater {
@ -102,7 +106,7 @@ class GraphicalLoginSolver : LoginSolver() {
override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? {
val code = VerificationCodeModel(VerificationCode(data))
// 界面需要运行在主线程
// UI必须在UI线程执行requestInput在协程种被调用
Platform.runLater {
find<VerificationCodeFragment>(Scope(code)).openModal(
stageStyle = StageStyle.UNDECORATED,

View File

@ -0,0 +1,15 @@
package net.mamoe.mirai.console.graphical.styleSheet
import tornadofx.Stylesheet
import tornadofx.c
open class BaseStyleSheet : Stylesheet() {
companion object {
const val primaryColor = "0EA987"
const val stressColor = "35867C"
const val secondaryColor = "32CABA"
const val lightColor ="9FD1CC"
const val FontColor = "FFFFFF"
}
}

View File

@ -7,7 +7,7 @@ import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import tornadofx.*
class LoginViewStyleSheet : Stylesheet() {
class LoginViewStyleSheet : BaseStyleSheet() {
companion object {
val vBox by csselement("VBox")
@ -15,11 +15,14 @@ class LoginViewStyleSheet : Stylesheet() {
init {
/*
* center box
*/
vBox {
maxWidth = 500.px
maxHeight = 500.px
backgroundColor += c("39c5BB", 0.3)
backgroundColor += c(primaryColor, 0.3)
backgroundRadius += box(15.px)
padding = box(50.px, 100.px)
@ -35,8 +38,11 @@ class LoginViewStyleSheet : Stylesheet() {
fontWeight = FontWeight.BOLD
}
/*
* login button
*/
button {
backgroundColor += c("00BCD4", 0.8)
backgroundColor += c(stressColor, 0.8)
padding = box(10.px, 0.px)
prefWidth = 500.px
textFill = Color.WHITE

View File

@ -1,21 +1,51 @@
package net.mamoe.mirai.console.graphical.styleSheet
import javafx.scene.Cursor
import javafx.scene.paint.Color
import tornadofx.*
class PrimaryStyleSheet : Stylesheet() {
class PrimaryStyleSheet : BaseStyleSheet() {
companion object {
// window
val jfxTitle by cssclass("jfx-decorator-buttons-container")
val container by cssclass("jfx-decorator-content-container")
// tab
val jfxTabPane by cssclass("tab-header-background")
val closeButton by cssclass("tab-close-button")
}
init {
/*
* window
*/
jfxTitle {
backgroundColor += c("00BCD4")
backgroundColor += c(primaryColor)
}
container {
borderColor += box(c("00BCD4"))
borderColor += box(c(primaryColor))
borderWidth += box(0.px, 4.px, 4.px, 4.px)
}
/*
* tab pane
*/
jfxTabPane {
backgroundColor += c(primaryColor)
}
// 去除JFoenix默认样式
tab {
and(":closable") {
borderWidth += box(0.px)
borderInsets += box(6.px, 0.px)
}
closeButton {
and(hover) { cursor = Cursor.HAND }
}
}
}
}

View File

@ -8,12 +8,11 @@ import javafx.event.EventTarget
import javafx.scene.Node
import javafx.scene.control.Button
import javafx.scene.control.ListView
import javafx.scene.control.TabPane
import tornadofx.SortedFilteredList
import tornadofx.attachTo
import tornadofx.bind
internal fun EventTarget.jfxTabPane(op: TabPane.() -> Unit = {}) = JFXTabPane().attachTo(this, op)
internal fun EventTarget.jfxTabPane(op: JFXTabPane.() -> Unit = {}) = JFXTabPane().attachTo(this, op)
internal fun EventTarget.jfxButton(text: String = "", graphic: Node? = null, op: Button.() -> Unit = {}) =
JFXButton(text).attachTo(this, op) {

View File

@ -23,7 +23,9 @@ class PrimaryView : View() {
left = vbox {
imageview(Image(PrimaryView::class.java.classLoader.getResourceAsStream("logo.png")))
imageview(Image(PrimaryView::class.java.classLoader.getResourceAsStream("logo.png"))) {
isPreserveRatio = true
}
// bot list
jfxListView(controller.botList) {
@ -70,13 +72,15 @@ class PrimaryView : View() {
center = jfxTabPane {
tabClosingPolicy = TabPane.TabClosingPolicy.ALL_TABS
logTab("Main", controller.mainLog, closeable = false)
tab("Plugins").content = find<PluginsView>().root
tab("Plugins").apply { isClosable = false }.content = find<PluginsView>().root
tab("Settings").content = find<SettingsView>().root
tab("Settings").apply { isClosable = false }.content = find<SettingsView>().root
tab("Login").content = find<LoginView>().root
tab("Login").apply { isClosable = false }.content = find<LoginView>().root
mainTabPane = this
}
@ -95,6 +99,8 @@ private fun TabPane.logTab(
op: Tab.() -> Unit = {}
) = tab(text) {
this.isClosable = closeable
vbox {
buttonbar {
@ -117,8 +123,6 @@ private fun TabPane.logTab(
}.ui {// isSucceed: Boolean ->
// notify something
}
if (closeable) button("关闭").action { close() }
}
}

View File

@ -0,0 +1,39 @@
package net.mamoe.mirai.console.graphical.view.dialog
import javafx.scene.control.TextField
import javafx.stage.Modality
import javafx.stage.StageStyle
import tornadofx.*
class InputDialog(title: String) : Fragment() {
private lateinit var input: TextField
init {
titleProperty.value = title
}
override val root = form {
fieldset {
field(title) {
input = textfield("")
}
buttonbar {
button("提交").action { close() }
}
}
}
fun open(): String {
// 阻塞窗口直到关闭
openModal(
stageStyle = StageStyle.DECORATED,
modality = Modality.APPLICATION_MODAL,
block = true
)
return input.text
}
}

View File

@ -1,4 +1,4 @@
package net.mamoe.mirai.console.graphical.view
package net.mamoe.mirai.console.graphical.view.dialog
import javafx.scene.image.Image
import net.mamoe.mirai.console.graphical.model.VerificationCodeModel
@ -30,7 +30,8 @@ class VerificationCodeFragment : Fragment() {
this@VerificationCodeFragment.close()
}
button("取消").action {
code.code.value = MAGIC_KEY
code.code.value =
MAGIC_KEY
code.commit()
this@VerificationCodeFragment.close()
}

View File

@ -23,6 +23,7 @@ import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.cleanPage
import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.drawLog
import net.mamoe.mirai.console.MiraiConsoleTerminalUI.LoggerDrawer.redrawLogs
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.utils.MiraiConsoleUI
import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.SimpleLogger.LogPriority
@ -124,7 +125,7 @@ object MiraiConsoleTerminalUI : MiraiConsoleUI {
requestResult = input
requesting = false
} else {
CommandManager.runConsoleCommand(commandBuilder.toString())
CommandManager.runCommand(ConsoleCommandSender, commandBuilder.toString())
}
}

View File

@ -11,7 +11,7 @@ const val CONSOLE_TERMINAL = "Terminal"
const val CONSOLE_GRAPHICAL = "Graphical"
object ConsoleUpdater {
internal object ConsoleUpdater {
@Suppress("SpellCheckingInspection")
private object Links : HashMap<String, Map<String, String>>() {

View File

@ -20,7 +20,7 @@ import java.net.URLClassLoader
import kotlin.math.pow
import kotlin.system.exitProcess
object CoreUpdator {
internal object CoreUpdater {
fun getProtocolLib(): File? {
contentPath.listFiles()?.forEach { file ->

View File

@ -1,9 +1,9 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.console.wrapper
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.features.ClientRequestException
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.utils.io.ByteReadChannel
@ -11,14 +11,11 @@ import io.ktor.utils.io.jvm.javaio.copyTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import kotlin.system.exitProcess
val Http: HttpClient
get() = HttpClient(CIO)
internal val Http: HttpClient = HttpClient(CIO)
inline fun <R> tryNTimesOrQuit(repeat: Int, errorHint: String, block: (Int) -> R){
internal inline fun <R> tryNTimesOrQuit(repeat: Int, errorHint: String, block: (Int) -> R) {
var lastException: Throwable? = null
repeat(repeat) {
@ -38,82 +35,84 @@ inline fun <R> tryNTimesOrQuit(repeat: Int, errorHint: String, block: (Int) -> R
}
suspend inline fun HttpClient.downloadRequest(url: String): ByteReadChannel {
return with(this.get<HttpResponse>(url)){
if(this.status.value == 404 || this.status.value == 403){
internal suspend inline fun HttpClient.downloadRequest(url: String): ByteReadChannel {
return with(this.get<HttpResponse>(url)) {
if (this.status.value == 404 || this.status.value == 403) {
error("File not found")
}
if(this.headers["status"] !=null && this.headers["status"] == "404"){
if (this.headers["status"] != null && this.headers["status"] == "404") {
error("File not found")
}
this.content
}
}
private val jcenterPath = "https://jcenter.bintray.com/{group}/{project}/{version}/:{project}-{version}.{extension}"
private val aliyunPath = "https://maven.aliyun.com/nexus/content/repositories/jcenter/{group}/{project}/{version}/{project}-{version}.{extension}"
private val jcenterPath = "https://jcenter.bintray.com/{group}/{project}/{version}/:{project}-{version}.{extension}"
private val aliyunPath =
"https://maven.aliyun.com/nexus/content/repositories/jcenter/{group}/{project}/{version}/{project}-{version}.{extension}"
private fun String.buildPath(
groupName: String,
projectName: String,
version: String,
extension: String
):String{
): String {
return this
.replace(
"{group}",groupName
"{group}", groupName
)
.replace(
"{project}",projectName
"{project}", projectName
)
.replace(
"{extension}",extension
"{extension}", extension
)
.replace(
"{version}",version
"{version}", version
)
}
suspend fun HttpClient.downloadMaven(
internal suspend fun HttpClient.downloadMaven(
groupName: String,
projectName: String,
version: String,
extension: String
):ByteReadChannel{
): ByteReadChannel {
return kotlin.runCatching {
downloadRequest(
aliyunPath.buildPath(groupName,projectName,version,extension)
aliyunPath.buildPath(groupName, projectName, version, extension)
)
}.getOrElse {
downloadRequest(
jcenterPath.buildPath(groupName,projectName,version,extension)
jcenterPath.buildPath(groupName, projectName, version, extension)
)
}
}
suspend inline fun HttpClient.downloadMavenArchive(
internal suspend inline fun HttpClient.downloadMavenArchive(
groupName: String,
projectName: String,
version: String
):ByteReadChannel{
return downloadMaven(groupName,projectName,version,"jar")
): ByteReadChannel {
return downloadMaven(groupName, projectName, version, "jar")
}
suspend inline fun HttpClient.downloadMavenPom(
internal suspend inline fun HttpClient.downloadMavenPom(
groupName: String,
projectName: String,
version: String
):ByteReadChannel{
return downloadMaven(groupName,projectName,version,"pom")
): ByteReadChannel {
return downloadMaven(groupName, projectName, version, "pom")
}
suspend fun HttpClient.downloadMavenPomAsString(
internal suspend fun HttpClient.downloadMavenPomAsString(
groupName: String,
projectName: String,
version: String
):String{
): String {
return kotlin.runCatching {
this.get<String>(
aliyunPath.buildPath(groupName,projectName,version,"pom")
aliyunPath.buildPath(groupName, projectName, version, "pom")
)
}.getOrElse {
this.get(
@ -123,12 +122,11 @@ suspend fun HttpClient.downloadMavenPomAsString(
}
/**
* 只要填content path后面的就可以
* 只要填 content path 后面的就可以
*/
suspend fun ByteReadChannel.saveToContent(filepath:String){
val fileStream = File(contentPath.absolutePath + "/" + filepath).also {
internal suspend fun ByteReadChannel.saveToContent(filepath: String) {
val fileStream = File(contentPath.absolutePath + "/" + filepath).also {
withContext(Dispatchers.IO) {
it.createNewFile()
}

View File

@ -9,7 +9,10 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.console.wrapper
import kotlinx.coroutines.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import java.net.URLClassLoader
import java.util.*
@ -56,31 +59,31 @@ object WrapperMain {
println("Starting version check...")
runBlocking {
launch {
CoreUpdator.versionCheck()
CoreUpdater.versionCheck()
}
launch {
ConsoleUpdater.versionCheck(type)
}
}
println("Version check complete, starting Mirai")
println("Core :" + CoreUpdator.getCore()!!)
println("Protocol:" + CoreUpdator.getProtocolLib()!!)
println("Console :" + ConsoleUpdater.getFile()!! )
println("Core :" + CoreUpdater.getCore()!!)
println("Protocol:" + CoreUpdater.getProtocolLib()!!)
println("Console :" + ConsoleUpdater.getFile()!!)
println("Root :" + System.getProperty("user.dir") + "/")
val loader = MiraiClassLoader(
CoreUpdator.getCore()!!,
CoreUpdator.getProtocolLib()!!,
CoreUpdater.getCore()!!,
CoreUpdater.getProtocolLib()!!,
ConsoleUpdater.getFile()!!,
this.javaClass.classLoader
)
when(type) {
when (type) {
CONSOLE_PURE -> {
loader.loadClass("net.mamoe.mirai.BotFactoryJvm")
loader.loadClass(
"net.mamoe.mirai.console.pure.MiraiConsolePureLoader"
).getMethod("load", String::class.java,String::class.java)
.invoke(null,CoreUpdator.getCurrentVersion(),ConsoleUpdater.getCurrentVersion())
"net.mamoe.mirai.console.pure.MiraiConsolePureLoader"
).getMethod("load", String::class.java, String::class.java)
.invoke(null, CoreUpdater.getCurrentVersion(), ConsoleUpdater.getCurrentVersion())
}
}
}

View File

@ -9,10 +9,11 @@
package net.mamoe.mirai.console
import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.command.DefaultCommands
import net.mamoe.mirai.console.plugins.PluginManager
import net.mamoe.mirai.console.utils.MiraiConsoleUI
@ -45,14 +46,16 @@ object MiraiConsole {
* Console运行路径
*/
var path: String = System.getProperty("user.dir")
internal set
/**
* Console前端接口
*/
lateinit var frontEnd: MiraiConsoleUI
internal set
var start = false
private var started = false
/**
@ -63,10 +66,10 @@ object MiraiConsole {
coreVersion: String = "0.0.0",
consoleVersion: String = "0.0.0"
) {
if (start) {
if (started) {
return
}
start = true
started = true
/* 初始化前端 */
this.version = consoleVersion
@ -110,49 +113,55 @@ object MiraiConsole {
}
}
@Suppress("RedundantSuspendModifier") // binary compatibility
@Deprecated("Please use CommandManager directly, this will be removed in later release")
object CommandProcessor{
@Deprecated("Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runConsoleCommand(command)",
"net.mamoe.mirai.console.command.CommandManager"
)
object CommandProcessor {
@Deprecated(
"Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runConsoleCommand(command)",
"net.mamoe.mirai.console.command.CommandManager"
), level = DeprecationLevel.ERROR
)
suspend fun runConsoleCommand(command: String) {
CommandManager.runConsoleCommand(command)
}
@Deprecated("Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runCommand(sender, command)",
"net.mamoe.mirai.console.command.CommandManager"
)
)
suspend fun runCommand(sender: CommandSender, command: String) {
CommandManager.runCommand(sender,command)
CommandManager.runCommand(ConsoleCommandSender, command)
}
@Deprecated("Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runConsoleCommand(command)",
"net.mamoe.mirai.console.command.CommandManager"
@Deprecated(
"Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runCommand(sender, command)",
"net.mamoe.mirai.console.command.CommandManager"
), level = DeprecationLevel.ERROR
)
suspend fun runCommand(sender: CommandSender, command: String) {
CommandManager.runCommand(sender, command)
}
@Deprecated(
"Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runCommand(command)",
"net.mamoe.mirai.console.command.CommandManager",
"net.mamoe.mirai.console.command.ConsoleCommandSender"
), level = DeprecationLevel.ERROR
)
fun runConsoleCommandBlocking(command: String) = runBlocking { runConsoleCommand(command)}
fun runConsoleCommandBlocking(command: String) =
runBlocking { CommandManager.runCommand(ConsoleCommandSender, command) }
@Suppress("unused")
@Deprecated("Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runCommand(sender, command)",
"net.mamoe.mirai.console.command.CommandManager"
@Deprecated(
"Please use CommandManager directly, this will be removed in later release", ReplaceWith(
"CommandManager.runCommand(sender, command)",
"net.mamoe.mirai.console.command.CommandManager"
)
)
)
fun runCommandBlocking(sender: CommandSender, command: String) = runBlocking { runCommand(sender, command) }
fun runCommandBlocking(sender: CommandSender, command: String) = runBlocking {
CommandManager.runCommand(sender, command)
}
}
}
/**
* Mirai Console的logger
* 它用于适配不同的Console前段
*/
internal object MiraiConsoleLogger {
operator fun invoke(any: Any? = null) {
invoke(

View File

@ -7,90 +7,53 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("NOTHING_TO_INLINE")
@file:Suppress("NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.command
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.plugins.PluginBase
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.utils.SimpleLogger.LogPriority
interface CommandSender {
/**
* 立刻发送一条Message
*/
suspend fun sendMessage(messageChain: MessageChain)
suspend fun sendMessage(message: String)
/**
* 写入要发送的内容 所有内容最后会被以一条发出
*/
fun appendMessage(message: String)
fun sendMessageBlocking(messageChain: MessageChain) = runBlocking { sendMessage(messageChain) }
fun sendMessageBlocking(message: String) = runBlocking { sendMessage(message) }
}
abstract class CommandSenderImpl : CommandSender {
internal val builder = StringBuilder()
override fun appendMessage(message: String) {
builder.append(message).append("\n")
}
internal open suspend fun flushMessage() {
if (!builder.isEmpty()) {
sendMessage(builder.toString().removeSuffix("\n"))
}
}
}
object ConsoleCommandSender : CommandSenderImpl() {
override suspend fun sendMessage(messageChain: MessageChain) {
MiraiConsole.logger("[Command]", 0, messageChain.toString())
}
override suspend fun sendMessage(message: String) {
MiraiConsole.logger("[Command]", 0, message)
}
override suspend fun flushMessage() {
super.flushMessage()
builder.clear()
}
}
open class ContactCommandSender(val contact: Contact) : CommandSenderImpl() {
override suspend fun sendMessage(messageChain: MessageChain) {
contact.sendMessage(messageChain)
}
override suspend fun sendMessage(message: String) {
contact.sendMessage(message)
}
}
/**
* 指令
*
* @see register 注册这个指令
* @see registerCommand 注册指令 DSL
*/
interface Command {
/**
* 指令主名称
*/
val name: String
/**
* 别名
*/
val alias: List<String>
/**
* 描述, 将会显示在 "/help" 指令中
*/
val description: String
/**
* 用法说明
*/
val usage: String
suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean
}
/**
* 注册这个指令
*/
inline fun Command.register() = CommandManager.register(this)
fun registerCommand(builder: CommandBuilder.() -> Unit): Command {
/**
* 构造并注册一个指令
*/
inline fun registerCommand(builder: CommandBuilder.() -> Unit): Command {
return CommandBuilder().apply(builder).register()
}
@ -104,8 +67,9 @@ abstract class BlockingCommand(
override val usage: String = ""
) : Command {
/**
* 最高优先级监听器
* 如果 return `false`, 这次指令不会被 [PluginBase] 的全局 onCommand 监听器监听
* 最高优先级监听器.
*
* 指令调用将优先触发 [Command.onCommand], 若该函数返回 `false`, 则不会调用 [PluginBase.onCommand]
* */
final override suspend fun onCommand(sender: CommandSender, args: List<String>): Boolean {
return withContext(Dispatchers.IO) {
@ -116,7 +80,27 @@ abstract class BlockingCommand(
abstract fun onCommandBlocking(sender: CommandSender, args: List<String>): Boolean
}
class AnonymousCommand internal constructor(
/**
* @see registerCommand
*/
class CommandBuilder @PublishedApi internal constructor() {
var name: String? = null
var alias: List<String>? = null
var description: String = ""
var usage: String = "use /help for help"
internal var onCommand: (suspend CommandSender.(args: List<String>) -> Boolean)? = null
fun onCommand(commandProcess: suspend CommandSender.(args: List<String>) -> Boolean) {
onCommand = commandProcess
}
}
// internal
internal class AnonymousCommand internal constructor(
override val name: String,
override val alias: List<String>,
override val description: String,
@ -128,19 +112,8 @@ class AnonymousCommand internal constructor(
}
}
class CommandBuilder internal constructor() {
var name: String? = null
var alias: List<String>? = null
var description: String = ""
var usage: String = "use /help for help"
internal var onCommand: (suspend CommandSender.(args: List<String>) -> Boolean)? = null
fun onCommand(commandProcess: suspend CommandSender.(args: List<String>) -> Boolean) {
onCommand = commandProcess
}
}
private fun CommandBuilder.register(): AnonymousCommand {
@PublishedApi
internal fun CommandBuilder.register(): AnonymousCommand {
if (name == null || onCommand == null) {
error("CommandBuilder not complete")
}

View File

@ -7,7 +7,7 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.console.command
@ -18,7 +18,6 @@ import net.mamoe.mirai.console.plugins.PluginBase
import net.mamoe.mirai.console.plugins.PluginManager
import java.util.concurrent.Executors
object CommandManager : Job by {
GlobalScope.launch(start = CoroutineStart.LAZY) {
processCommandQueue()
@ -28,6 +27,11 @@ object CommandManager : Job by {
val commands: Collection<Command> get() = registeredCommand.values
/**
* 注册这个指令.
*
* @throws IllegalStateException 当已注册的指令与 [command] 重名时
*/
fun register(command: Command) {
val allNames = mutableListOf(command.name).also { it.addAll(command.alias) }
allNames.forEach {
@ -41,9 +45,10 @@ object CommandManager : Job by {
}
fun unregister(command: Command) {
(command.alias.asSequence() + command.name).forEach {
command.alias.forEach {
registeredCommand.remove(it)
}
registeredCommand.remove(command.name)
}
fun unregister(commandName: String): Boolean {
@ -52,23 +57,65 @@ object CommandManager : Job by {
/**
* 指令是单线程运行的
* 最基础的执行指令的方式
* 指令将会被加入队列依次执行
*
* @param sender 指令执行者, 可为 [ConsoleCommandSender] [ContactCommandSender]
*/
private val commandDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
fun runCommand(sender: CommandSender, command: String) {
commandChannel.offer(
FullCommand(sender, command)
)
}
/**
* 执行一个指令, 但是如果你想模拟一个指令的执行
* 请向下看
* 插队异步执行一个指令并返回 [Deferred]
*
* 返回一个指令是否执行成功
* @param sender 指令执行者, 可为 [ConsoleCommandSender] [ContactCommandSender]
* @see PluginBase.runCommandAsync 扩展
*/
private suspend fun processCommand(sender: CommandSender, fullCommand: String):Boolean {
return withContext(commandDispatcher) {
_processCommand(sender, fullCommand)
fun runCommandAsync(pluginBase: PluginBase, sender: CommandSender, command: String): Deferred<Boolean> {
return pluginBase.async {
processCommand(sender, command)
}
}
private suspend fun _processCommand(sender: CommandSender, fullCommand: String): Boolean {
/**
* 插队执行一个指令并返回 [Deferred]
*
* @param sender 指令执行者, 可为 [ConsoleCommandSender] [ContactCommandSender]
* @see PluginBase.runCommandAsync 扩展
*/
@Suppress("KDocUnresolvedReference")
suspend fun dispatchCommand(sender: CommandSender, command: String): Boolean {
return processCommand(sender, command)
}
/**
* 阻塞当前线程, 插队执行一个指令
*
* @param sender 指令执行者, 可为 [ConsoleCommandSender] [ContactCommandSender]
*/
// for java
fun dispatchCommandBlocking(sender: CommandSender, command: String): Boolean =
runBlocking { dispatchCommand(sender, command) }
// internal
/**
* 单线程执行指令
*/
private val commandDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
private suspend fun processCommand(sender: CommandSender, fullCommand: String): Boolean {
return withContext(commandDispatcher) {
processCommandImpl(sender, fullCommand)
}
}
private suspend fun processCommandImpl(sender: CommandSender, fullCommand: String): Boolean {
val blocks = fullCommand.split(" ")
val commandHead = blocks[0].replace("/", "")
val args = blocks.drop(1)
@ -81,17 +128,16 @@ object CommandManager : Job by {
sender.sendMessage(this.usage)
}
}
}catch (e: Exception){
} catch (e: Exception) {
sender.sendMessage("在运行指令时出现了未知错误")
e.printStackTrace()
false
}finally {
(sender as CommandSenderImpl).flushMessage()
} finally {
(sender as AbstractCommandSender).flushMessage()
}
}?: throw UnknownCommandException(commandHead)
} ?: throw UnknownCommandException(commandHead)
}
internal class FullCommand(
val sender: CommandSender,
val commandStr: String
@ -103,61 +149,23 @@ object CommandManager : Job by {
val command = commandChannel.receive()
try {
processCommand(command.sender, command.commandStr)
}catch (e:UnknownCommandException){
} catch (e: UnknownCommandException) {
command.sender.sendMessage("未知指令 " + command.commandStr)
}catch (e:Throwable){//should never happen
} catch (e: Throwable) {//should never happen
e.printStackTrace()
}
processCommandQueue()
}
/**
* runCommand()是最基础的执行指令的方式
* 指令将会被加入队列依次执行
* 方法由0.27.0的阻塞模式改为不阻塞(鉴于commandChannel大小无限)
*/
fun runCommand(sender: CommandSender, command: String) {
runBlocking {//it wont be blocking
commandChannel.send(
FullCommand(sender, command)
)
}
}
@Suppress("unused")
fun runConsoleCommand(command: String) = runCommand(ConsoleCommandSender,command)
/**
* runCommandAnsyc()执行一个指令并返回deferred
* 为插队执行
*/
fun runCommandAsync(pluginBase: PluginBase, sender: CommandSender, command: String):Deferred<Boolean>{
return pluginBase.async{
processCommand(sender,command)
}
}
fun runConsoleCommandAsync(pluginBase: PluginBase, command: String):Deferred<Boolean> = runCommandAsync(pluginBase,ConsoleCommandSender,command)
/**
* dispatchCommand()执行一个指令并等到完成
* 为插队执行
*/
suspend fun dispatchCommand(sender: CommandSender,command: String):Boolean{
return processCommand(sender,command)
}
suspend fun dispatchConsoleCommand(command: String):Boolean = dispatchCommand(ConsoleCommandSender,command)
fun dispatchCommandBlocking(sender: CommandSender,command: String):Boolean = runBlocking { dispatchCommand(sender, command) }
fun dispatchConsoleCommandBlocking(command: String):Boolean = runBlocking { dispatchConsoleCommandBlocking(command) }
}
fun PluginBase.runCommandAsnyc(sender: CommandSender, command: String):Deferred<Boolean> = CommandManager.runCommandAsync(this,sender,command)
fun PluginBase.runConsoleCommandAsync(command: String):Deferred<Boolean> = CommandManager.runConsoleCommandAsync(this,command)
/**
* 插队异步执行一个指令并返回 [Deferred]
*
* @param sender 指令执行者, 可为 [ConsoleCommandSender] [ContactCommandSender]
* @see PluginBase.runCommandAsync 扩展
*/
fun PluginBase.runCommandAsync(sender: CommandSender, command: String): Deferred<Boolean> =
CommandManager.runCommandAsync(this, sender, command)
class UnknownCommandException(command: String):Exception("unknown command \"$command\"")
class UnknownCommandException(command: String) : Exception("unknown command \"$command\"")

View File

@ -0,0 +1,84 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.command
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.sendMessage
import net.mamoe.mirai.message.data.Message
/**
* 指令发送者
*
* @see AbstractCommandSender 请继承于该抽象类
*/
interface CommandSender {
/**
* 立刻发送一条消息
*/
suspend fun sendMessage(messageChain: Message)
suspend fun sendMessage(message: String)
/**
* 写入要发送的内容 所有内容最后会被以一条发出
*/
fun appendMessage(message: String)
fun sendMessageBlocking(messageChain: Message) = runBlocking { sendMessage(messageChain) }
fun sendMessageBlocking(message: String) = runBlocking { sendMessage(message) }
}
abstract class AbstractCommandSender : CommandSender {
internal val builder = StringBuilder()
override fun appendMessage(message: String) {
builder.append(message).append("\n")
}
internal open suspend fun flushMessage() {
if (builder.isNotEmpty()) {
sendMessage(builder.toString().removeSuffix("\n"))
}
}
}
/**
* 控制台指令执行者. 代表由控制台执行指令
*/
object ConsoleCommandSender : AbstractCommandSender() {
override suspend fun sendMessage(messageChain: Message) {
MiraiConsole.logger("[Command]", 0, messageChain.toString())
}
override suspend fun sendMessage(message: String) {
MiraiConsole.logger("[Command]", 0, message)
}
override suspend fun flushMessage() {
super.flushMessage()
builder.clear()
}
}
/**
* 联系人指令执行者. 代表由一个 QQ 用户执行指令
*/
@Suppress("MemberVisibilityCanBePrivate")
open class ContactCommandSender(val contact: Contact) : AbstractCommandSender() {
override suspend fun sendMessage(messageChain: Message) {
contact.sendMessage(messageChain)
}
override suspend fun sendMessage(message: String) {
contact.sendMessage(message)
}
}

View File

@ -143,7 +143,7 @@ object DefaultCommands {
startsWith("/") { message ->
if (bot.checkManager(this.sender.id)) {
val sender = ContactCommandSender(this.subject)
MiraiConsole.CommandProcessor.runCommand(
CommandManager.runCommand(
sender, message
)
}

View File

@ -14,14 +14,3 @@ import net.mamoe.mirai.event.Event
import net.mamoe.mirai.event.broadcast
internal fun <E : Event> broadcast(e: E): E = runBlocking { e.broadcast() }
class A{
var x = 0
var width:Int
get() = x
set(value){
x = value
}
}

View File

@ -15,17 +15,17 @@ import kotlinx.coroutines.*
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.Command
import net.mamoe.mirai.console.command.CommandSender
import net.mamoe.mirai.console.pure.MiraiConsoleUIPure
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.SimpleLogger
import net.mamoe.mirai.utils.SimpleLogger.LogPriority
import java.io.File
import java.io.InputStream
import java.net.URLClassLoader
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
/**
* 所有插件的基类
*/
abstract class PluginBase
@JvmOverloads constructor(coroutineContext: CoroutineContext = EmptyCoroutineContext) : CoroutineScope {
@ -36,9 +36,8 @@ abstract class PluginBase
* 插件被分配的数据目录数据目录会与插件名称同名
*/
val dataFolder: File by lazy {
File(PluginManager.pluginsPath + "/" + PluginManager.lastPluginName).also {
it.mkdir()
}
File(PluginManager.pluginsPath + "/" + PluginManager.lastPluginName)
.also { it.mkdir() }
}
/**
@ -63,37 +62,21 @@ abstract class PluginBase
}
/**
* 当任意指令被使用
* 当任意指令被使用时调用.
*
* 指令调用将优先触发 [Command.onCommand], 若该函数返回 `false`, 则不会调用 [PluginBase.onCommand]
*/
open fun onCommand(command: Command, sender: CommandSender, args: List<String>) {
}
internal fun enable() {
this.onEnable()
}
/**
* 加载一个data folder中的Config
* 这个config是read-write的
* 加载一个 [dataFolder] 中的 [Config]
*/
fun loadConfig(fileName: String): Config {
return Config.load(dataFolder.absolutePath + "/" + fileName)
}
@JvmOverloads
internal fun disable(throwable: CancellationException? = null) {
this.coroutineContext[Job]!!.cancelChildren(throwable)
try {
this.onDisable()
} catch (e: Exception) {
logger.info(e)
}
}
internal var pluginName: String = ""
val logger: MiraiLogger by lazy {
SimpleLogger("Plugin $pluginName") { priority, message, e ->
val identityString = "[${pluginName}]"
@ -120,16 +103,34 @@ abstract class PluginBase
/**
* 加载 resource 中的 [Config]
* 这个 [Config] read-only
* 这个 [Config] 只读
*/
fun getResourcesConfig(fileName: String): Config {
if (!fileName.contains(".")) {
error("Unknown Config Type")
}
require(fileName.contains(".")) { "Unknown Config Type" }
return Config.load(getResources(fileName) ?: error("No such file: $fileName"), fileName.substringAfter('.'))
}
// internal
internal fun enable() {
this.onEnable()
}
internal fun disable(throwable: CancellationException? = null) {
this.coroutineContext[Job]!!.cancelChildren(throwable)
try {
this.onDisable()
} catch (e: Exception) {
logger.info(e)
}
}
internal var pluginName: String = ""
}
/**
* 插件描述
*/
class PluginDescription(
val name: String,
val author: String,

View File

@ -19,32 +19,31 @@ import net.mamoe.mirai.utils.SimpleLogger
import net.mamoe.mirai.utils.io.encodeToString
import java.io.File
import java.io.InputStream
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.net.URL
import java.util.jar.JarFile
object PluginManager {
@Volatile
internal var lastPluginName: String = ""
internal val pluginsPath = (System.getProperty("user.dir") + "/plugins/").replace("//", "/").also {
File(it).mkdirs()
}
private val logger = SimpleLogger("Plugin Manager") { _, message, e ->
MiraiConsole.logger("[Plugin Manager]", 0, message)
MiraiConsole.logger("[Plugin Manager]", 0, e)
private val logger = SimpleLogger("Plugin Manager") { p, message, e ->
MiraiConsole.logger(p, "[Plugin Manager]", 0, message)
MiraiConsole.logger(p, "[Plugin Manager]", 0, e)
}
//已完成加载的
private val nameToPluginBaseMap: MutableMap<String, PluginBase> = mutableMapOf()
private val pluginDescriptions: MutableMap<String, PluginDescription> = mutableMapOf()
fun onCommand(command: Command, sender: CommandSender, args: List<String>) {
internal fun onCommand(command: Command, sender: CommandSender, args: List<String>) {
nameToPluginBaseMap.values.forEach {
try {
it.onCommand(command, sender, args)
}catch (e:Throwable){
} catch (e: Throwable) {
logger.info(e)
}
}
@ -64,6 +63,10 @@ object PluginManager {
return pluginDescriptions.values
}
@Volatile
internal var lastPluginName: String = ""
/**
* 尝试加载全部插件
*/
@ -153,27 +156,31 @@ object PluginManager {
//real load
logger.info("loading plugin " + description.name)
lastPluginName = description.name
try {
val pluginClass = try {
PluginClassLoader(
(pluginsLocation[description.name]!!),
this.javaClass.classLoader
)
.loadClass(description.basePath)
).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")
).loadClass("${description.basePath}Kt")
}
return try {
val subClass = pluginClass.asSubclass(PluginBase::class.java)
lastPluginName = description.name
val plugin: PluginBase =
subClass.kotlin.objectInstance ?: subClass.getDeclaredConstructor().newInstance()
subClass.kotlin.objectInstance ?: subClass.getDeclaredConstructor().apply {
againstPermission()
}.newInstance()
plugin.dataFolder // initialize right now
description.loaded = true
logger.info("successfully loaded plugin " + description.name + " version " + description.version + " by " + description.author)
logger.info(description.info)
@ -200,8 +207,8 @@ object PluginManager {
nameToPluginBaseMap.values.forEach {
try {
it.onLoad()
}catch (ignored : Throwable){
if(ignored is CancellationException) {
} catch (ignored: Throwable) {
if (ignored is CancellationException) {
logger.info(ignored)
logger.info(it.pluginName + " failed to load, disabling it")
it.disable(ignored)
@ -212,10 +219,10 @@ object PluginManager {
nameToPluginBaseMap.values.forEach {
try {
it.enable()
}catch (ignored : Throwable){
} catch (ignored: Throwable) {
logger.info(ignored)
logger.info(it.pluginName + " failed to enable, disabling it")
if(ignored is CancellationException) {
if (ignored is CancellationException) {
it.disable(ignored)
}
}
@ -238,7 +245,7 @@ object PluginManager {
*/
fun getJarFileByName(pluginName: String): File? {
File(pluginsPath).listFiles()?.forEach { file ->
if (file != null && file.extension == "jar") {
if (file != null && file.extension == "jar") {
val jar = JarFile(file)
val pluginYml =
jar.entries().asSequence().filter { it.name.toLowerCase().contains("plugin.yml") }.firstOrNull()
@ -270,4 +277,18 @@ object PluginManager {
return URL("jar:file:" + jarFile.absoluteFile + "!/" + toFindFile.name).openConnection().inputStream
}
}
private val trySetAccessibleMethod: Method? = runCatching {
Class.forName("java.lang.reflect.AccessibleObject").getMethod("trySetAccessible")
}.getOrNull()
private fun Constructor<out PluginBase>.againstPermission() {
trySetAccessibleMethod?.let { it.invoke(this, true) }
?: kotlin.runCatching {
@Suppress("DEPRECATED")
this.isAccessible = true
}
}

View File

@ -11,7 +11,8 @@ package net.mamoe.mirai.console.pure
import kotlinx.coroutines.delay
import net.mamoe.mirai.Bot
import net.mamoe.mirai.console.MiraiConsole
import net.mamoe.mirai.console.command.CommandManager
import net.mamoe.mirai.console.command.ConsoleCommandSender
import net.mamoe.mirai.console.utils.MiraiConsoleUI
import net.mamoe.mirai.utils.DefaultLoginSolver
import net.mamoe.mirai.utils.LoginSolver
@ -49,7 +50,7 @@ class MiraiConsoleUIPure : MiraiConsoleUI {
requestStr = input
requesting = false
} else {
MiraiConsole.CommandProcessor.runConsoleCommandBlocking(input)
CommandManager.runCommand(ConsoleCommandSender, input)
}
}
}
@ -65,8 +66,7 @@ class MiraiConsoleUIPure : MiraiConsoleUI {
override fun pushLog(priority: LogPriority, identityStr: String, identity: Long, message: String) {
var priorityStr = "[${priority.name}]"
val _message = message + COLOR_RESET
/**
/*
* 通过ANSI控制码添加颜色
* 更多的颜色定义在 [MiraiConsoleUIPure] companion
*/
@ -85,7 +85,7 @@ class MiraiConsoleUIPure : MiraiConsoleUI {
else -> priorityStr
}
println("\u001b[0m " + sdf.format(Date()) + " $priorityStr $identityStr $_message")
println("\u001b[0m " + sdf.format(Date()) + " $priorityStr $identityStr ${message + COLOR_RESET}")
}
override fun prePushBot(identity: Long) {

View File

@ -2,25 +2,25 @@ package net.mamoe.mirai.console.scheduler
import net.mamoe.mirai.console.plugins.PluginBase
interface SchedulerTask<T:PluginBase>{
abstract fun onTick(i:Long)
interface SchedulerTask<T : PluginBase> {
abstract fun onTick(i: Long)
abstract fun onRun()
}
abstract class RepeatTask<T:PluginBase>(
abstract class RepeatTask<T : PluginBase>(
val intervalInMs: Int
):SchedulerTask<T>{
) : SchedulerTask<T> {
override fun onTick(i: Long) {
if(i%intervalInMs == 0L){
if (i % intervalInMs == 0L) {
onRun()
}
}
companion object{
fun <T:PluginBase> of(
companion object {
fun <T : PluginBase> of(
intervalInMs: Int, runnable: () -> Unit
):RepeatTask<T>{
): RepeatTask<T> {
return AnonymousRepeatTask<T>(
intervalInMs, runnable
)
@ -28,42 +28,18 @@ abstract class RepeatTask<T:PluginBase>(
}
}
internal class AnonymousRepeatTask<T: PluginBase>(
internal class AnonymousRepeatTask<T : PluginBase>(
intervalInMs: Int, private val runnable: () -> Unit
): RepeatTask<T>(intervalInMs){
) : RepeatTask<T>(intervalInMs) {
override fun onRun() {
runnable.invoke()
}
}
fun <T:PluginBase> T.repeatTask(
fun <T : PluginBase> T.repeatTask(
intervalInMs: Int, runnable: () -> Unit
):RepeatTask<T>{
): RepeatTask<T> {
return AnonymousRepeatTask<T>(
intervalInMs, runnable
)
}
fun <T> repeatTask(){
}
class X: PluginBase() {
override fun onLoad() {
//kotlin
this.repeatTask(5){
}
//java1
RepeatTask.of<X>(5){
}
//java2
class Xtask:RepeatTask<X>(5){
override fun onRun() {
}
}
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.utils
import kotlinx.coroutines.*

View File

@ -14,15 +14,14 @@ import net.mamoe.mirai.utils.LoginSolver
import net.mamoe.mirai.utils.SimpleLogger.LogPriority
/**
* 只需要实现一个这个 传入MiraiConsole 就可以绑定UI层与Console层
* 注意线程
* 只需要实现一个这个传入 MiraiConsole 就可以绑定 UI 层与 Console
* 需要保证线程安全
*/
interface MiraiConsoleUI {
/**
* UI层展示一条log
* UI 层展示一条 log
*
* identitylog所属的screen, Main=0; Bot=Bot.uin
* identitylog 所属的 screen, Main=0; Bot=Bot.uin
*/
fun pushLog(
identity: Long,
@ -37,14 +36,14 @@ interface MiraiConsoleUI {
)
/**
* UI层准备接受新增的一个BOT
* UI 层准备接受新增的一个BOT
*/
fun prePushBot(
identity: Long
)
/**
* UI层接受一个新的bot
* UI 层接受一个新的bot
* */
fun pushBot(
bot: Bot
@ -58,14 +57,13 @@ interface MiraiConsoleUI {
)
/**
* 让UI层提供一个Input
* 这个Input 等于 Command
* UI 层提供一个输入, 相当于 [readLine]
*/
suspend fun requestInput(hint:String): String
suspend fun requestInput(hint: String): String
/**
* UI层更新BOT管理员的数据
* UI 层更新 bot 管理员的数据
*/
fun pushBotAdminStatus(
identity: Long,
@ -73,7 +71,7 @@ interface MiraiConsoleUI {
)
/**
* UI层创建一个LoginSolver
* UI 层创建一个 [LoginSolver]
*/
fun createLoginSolver(): LoginSolver