From 37eeaf143ccbf2a3ae2fcbd91cf97cb100805f1f Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Mon, 2 Feb 2026 12:48:55 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BB=A3=E7=A0=81=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/dataSources.xml | 4 +- build.gradle.kts | 6 +- gradle.properties | 2 +- .../r3944realms/ltdmanager/GlobalManager.kt | 12 +- .../blessingskin/BlessingSkinClient.kt | 203 ++--------- .../blessingskin/BlessingSkinQueueItem.kt | 19 +- .../blessingskin/data/InvitationCode.kt | 13 +- .../request/BlessingSkinRequest.kt | 74 +--- .../GenerateInvitationCodeRequest.kt | 8 +- .../response/BlessingSkinResponse.kt | 7 +- .../response/FailedBlessingSkinResponse.kt | 7 +- .../InvitationCodeGenerationResponse.kt | 10 +- .../ltdmanager/chevereto/CheveretoClient.kt | 323 ++++++++++-------- .../chevereto/CheveretoQueueItem.kt | 16 +- .../chevereto/data/CheveretoSource.kt | 9 +- .../ltdmanager/chevereto/data/SuccessInfo.kt | 2 +- .../chevereto/request/CheveretoRequest.kt | 13 +- .../request/v1/CheveretoUploadRequest.kt | 89 ++++- .../chevereto/response/CheveretoResponse.kt | 35 +- .../response/FailedCheveretoResponse.kt | 10 +- .../response/v1/CheveretoUploadResponse.kt | 17 +- .../ltdmanager/core/client/IClient.kt | 170 ++++++++- .../ltdmanager/core/client/QueueItem.kt | 23 +- .../core/client/request/IRequest.kt | 74 +++- .../core/client/response/IFailedResponse.kt | 5 +- .../core/client/response/IResponse.kt | 4 + .../core/client/response/ResponseResult.kt | 45 ++- .../ltdmanager/core/config/McsmConfig.kt | 52 ++- .../core/config/YamlConfigLoader.kt | 3 + .../core/init/DependencyResolver.kt | 2 +- .../ltdmanager/core/init/ModuleConfig.kt | 155 ++++++++- .../ltdmanager/core/init/ModuleFactory.kt | 28 ++ .../ltdmanager/core/init/ModuleLoader.kt | 6 + .../ltdmanager/core/init/ModuleRegistry.kt | 11 + .../dglab/model/game/GameClientOperation.kt | 18 +- .../kotlin/top/r3944realms/ltdmanager/main.kt | 60 ++-- .../r3944realms/ltdmanager/mcms/MCSMClient.kt | 43 ++- .../ltdmanager/mcms/MCSMSkinQueueItem.kt | 16 +- .../ltdmanager/mcms/request/MCSMRequest.kt | 11 +- .../instance/GetInstanceListRequest.kt | 50 ++- .../request/instance/StartInstanceRequest.kt | 53 ++- .../mcms/response/FailedMCSMResponse.kt | 29 +- .../ltdmanager/mcms/response/MCSMResponse.kt | 30 +- .../mcms/response/ResponseResult.kt | 8 +- .../instance/GetInstanceListResponse.kt | 59 +++- .../instance/StartInstanceResponse.kt | 21 +- .../ltdmanager/module/ApplyWhitelistModule.kt | 15 +- .../ltdmanager/module/BanModule.kt | 2 +- .../ltdmanager/module/BaseModule.kt | 1 + .../ltdmanager/module/DGLabModule.kt | 2 +- .../module/GroupMessagePollingModule.kt | 2 +- .../module/GroupRequestHandlerModule.kt | 2 +- .../ltdmanager/module/HelpModule.kt | 2 +- .../module/InvitationCodesModule.kt | 27 +- .../ltdmanager/module/MailModule.kt | 2 +- .../ltdmanager/module/McServerStatusModule.kt | 2 +- .../module/ModGroupHandlerModule.kt | 2 +- .../r3944realms/ltdmanager/module/Modules.kt | 22 ++ .../ltdmanager/module/RconPlayerListModule.kt | 2 +- .../ltdmanager/module/StateModule.kt | 2 +- .../module/exception/ConfigError.kt | 16 +- .../ltdmanager/utils/ConfigInitializer.kt | 8 +- src/main/resources/application.yaml | 2 + 63 files changed, 1383 insertions(+), 583 deletions(-) diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 875bfab..258456d 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,11 +1,11 @@ - + mysql.8 true com.mysql.cj.jdbc.Driver - jdbc:mysql://47.116.125.76:3308 + jdbc:mysql://110.42.70.155:3306 diff --git a/build.gradle.kts b/build.gradle.kts index 79c6bda..da56e85 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,10 @@ repositories { maven { url = uri("https://repo.glaremasters.me/repository/public/") } + maven { + name = "LTD Maven" + url = uri("https://nexus.bot.leisuretimedock.top/repository/maven-public/") + } } //TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76 dependencies { @@ -77,7 +81,7 @@ repositories { //DG_Lab 依赖库导入 implementation("io.netty:netty-all:4.1.109.Final") implementation("com.google.code.gson:gson:2.10.1") - implementation(files("libs/DgLab-common-${k("dg_lab_version")}.jar")) + implementation("top.r3944realms.dg_lab:Common:${k("dg_lab_version")}") //生成 二维码 implementation("com.google.zxing:core:[3.5.3,)") diff --git a/gradle.properties b/gradle.properties index 74b81e4..9ae7e09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ org.gradle.parallel=true org.gradle.degree_of_parallelism=16 project_group=top.r3944realms.ltdmanager project_version=1.14-SNAPSHOT -dg_lab_version=4.3.13.18 +dg_lab_version=4.4.14.18 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt b/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt index a05908f..73e9a19 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/GlobalManager.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.* import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient import top.r3944realms.ltdmanager.chevereto.CheveretoClient import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool +import top.r3944realms.ltdmanager.mcms.MCSMClient import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient import top.r3944realms.ltdmanager.module.ModuleManager import top.r3944realms.ltdmanager.napcat.NapCatClient @@ -20,6 +21,10 @@ object GlobalManager { MysqlHikariConnectPool() } + fun initApplication() { + + } + // NapCat 客户端 val napCatClient: NapCatClient by lazy { NapCatClient.create() @@ -33,6 +38,9 @@ object GlobalManager { val cheveretoClient: CheveretoClient by lazy { CheveretoClient.create() } + val mcsmClient: MCSMClient by lazy { + MCSMClient.create() + } val moduleManager: ModuleManager by lazy { ModuleManager() } @@ -72,7 +80,8 @@ object GlobalManager { "McSrvStatusClient" to { mcSrvStatusClient.close() }, "BlessingSkinClient" to { blessingSkinClient.close() }, "Hikari 数据源" to { dataSource.close() }, - "CheveretoClient" to { cheveretoClient.close() } + "CheveretoClient" to { cheveretoClient.close() }, + "McsmClient" to { mcsmClient.close() }, ) resources.forEach { (name, closer) -> @@ -99,4 +108,5 @@ object GlobalManager { isRunning.set(false) } + } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt index 451b7cb..dabfa6c 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinClient.kt @@ -1,27 +1,24 @@ package top.r3944realms.ltdmanager.blessingskin import io.ktor.client.* -import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.sync.withPermit import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult +import top.r3944realms.ltdmanager.core.client.IClient +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult import top.r3944realms.ltdmanager.core.config.YamlConfigLoader -import top.r3944realms.ltdmanager.utils.Environment -import top.r3944realms.ltdmanager.utils.LoggerUtil -import java.net.URLEncoder import java.util.* -class BlessingSkinClient private constructor() : AutoCloseable { +class BlessingSkinClient private constructor() : IClient { private val client = HttpClient(CIO) { expectSuccess = false @@ -40,170 +37,40 @@ class BlessingSkinClient private constructor() : AutoCloseable { // 限流控制 private val semaphore = Semaphore(5) private val requestMutex = Mutex() - private val requestQueue = PriorityQueue>(compareBy { it.priority }) + private val requestQueue = PriorityQueue(compareBy { it.priority }) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { - startQueueProcessor() + init() } - /** - * 提交请求 - */ - suspend fun submitRequest( - request: BlessingSkinRequest, - priority: Int = 5, - maxRetries: Int = 3 - ): ResponseResult { - val deferred = CompletableDeferred>() - requestMutex.withLock { - requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true)) - } - return deferred.await() + override fun getBaseUrl(): String = blessingSkinServerConfig.url!! + + override fun getType(): String = "BlessingSkinClient" + + override fun getClient(): HttpClient = client + + override fun getSemaphore(): Semaphore = semaphore + + override fun getRequestMutex(): Mutex = requestMutex + + override fun getResponseQueue(): PriorityQueue = requestQueue + + override fun getScope(): CoroutineScope = scope + + override fun createFailureResponse(exception: Exception?): IFailedResponse { + return FailedBlessingSkinResponse.Default(exception?.stackTraceToString()?:"ERROR") } - /** - * 启动队列处理器 - */ - private fun startQueueProcessor() { - scope.launch { - while (isActive) { - val item = requestMutex.withLock { - requestQueue.poll() - } - if (item == null) { - delay(50) - continue - } - processQueueItem(item) - } - } - } - - /** - * 处理队列项 - */ - private suspend fun processQueueItem(item: BlessingSkinQueueItem) { - semaphore.withPermit { - val (request, deferred, _, maxRetries, _) = item - var attempt = 0 - var lastError: Exception? = null - - while (attempt < maxRetries) { - try { - // 构建完整的URL,包括查询参数 - val fullUrl = buildFullUrlWithQueryParams(request) - - if (!Environment.isProduction()) { - LoggerUtil.logger.debug("发送请求到: $fullUrl") - LoggerUtil.logger.debug("请求方法: {}", request.method()) - } - - val response = client.request(fullUrl) { - method = request.method() - - - // 设置请求头 - headers { - request.headers().invoke(this) - } - - // 对于非GET请求,设置请求体 - if (request.method() != HttpMethod.Get) { - setBody(request.toJSON()) - } - } - - val responseText: String = response.body() - - if (!Environment.isProduction()) { - LoggerUtil.logger.debug("响应状态: {}", response.status) - LoggerUtil.logger.debug("响应内容: $responseText") - } - - // 检查是否是HTML响应(重定向) - if (isHtmlResponse(responseText)) { - throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置") - } - - // 解析响应 - val result = request.getResponse(responseText, response.status) - - @Suppress("UNCHECKED_CAST") - (deferred as CompletableDeferred>).complete(result) - - return - - } catch (e: Exception) { - lastError = e - attempt++ - - if (!request.shouldRetryOnFailure() || attempt >= maxRetries) { - break - } - - LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}") - delay((attempt * 1000L)) // 指数退避 - } - } - - // 所有重试都失败或不应重试 - val errorResponse = createFailureResponse(lastError, request) - @Suppress("UNCHECKED_CAST") - (deferred as CompletableDeferred>).complete( - ResponseResult.Failure(errorResponse) - ) - } - } - - /** - * 构建完整的URL,包含查询参数 - */ - private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String { - val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/") - val path = request.path().removePrefix("/") - - // 构建基础URL - val urlBuilder = StringBuilder("$baseUrl/$path") - - // 添加查询参数 - val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) -> - "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" - } - - if (queryParams.isNotEmpty()) { - urlBuilder.append("?").append(queryParams) - } - - return urlBuilder.toString() - } - - /** - * 检查是否是HTML响应 - */ - private fun isHtmlResponse(text: String): Boolean { - return text.contains("", ignoreCase = true) || - text.contains("", ignoreCase = true) || - text.contains("Redirecting", ignoreCase = true) - } - - /** - * 创建失败响应 - */ - private fun createFailureResponse( - exception: Exception?, - request: BlessingSkinRequest<*, *> - ): FailedBlessingSkinResponse { - return FailedBlessingSkinResponse.Default( - failedResult = exception?.message ?: "未知错误", - ) - } - - override fun close() { - scope.cancel() - runBlocking { - client.close() - } + override fun addToQueue( + request: BlessingSkinRequest, + deferredC: CompletableDeferred>, + priority: Int, + maxRetries: Int + ): BlessingSkinQueueItem { + val element = BlessingSkinQueueItem(request, deferredC, priority, maxRetries, false) + requestQueue.add(element) + return element } companion object { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinQueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinQueueItem.kt index 51ab78d..d5cc7e2 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinQueueItem.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/BlessingSkinQueueItem.kt @@ -4,13 +4,14 @@ import kotlinx.coroutines.CompletableDeferred import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse +import top.r3944realms.ltdmanager.core.client.QueueItem -data class BlessingSkinQueueItem( - val request: BlessingSkinRequest, - val deferred: CompletableDeferred<*>, - var retries: Int, - val priority: Int, - val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit -) : Comparable> { - override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority) -} +data class BlessingSkinQueueItem ( + val request0: BlessingSkinRequest, + val deferred0: CompletableDeferred<*>, + val priority0: Int, + var retries0: Int, + val expectsResponse0: Boolean +) : QueueItem ( + request0, deferred0, retries0, priority0, expectsResponse0 +) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt index b780f44..b83e897 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt @@ -1,2 +1,13 @@ -package top.r3944realms.ltdmanager.blessingskin.data +package top.r3944realms.ltdmanager.blessingskin.data +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class InvitationCode( + val code: String, + @SerialName("generated_at") + val generatedAt: String, + @SerialName("expires_at") + val expiresAt: String +) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/BlessingSkinRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/BlessingSkinRequest.kt index 8ace575..51c5865 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/BlessingSkinRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/BlessingSkinRequest.kt @@ -1,79 +1,13 @@ package top.r3944realms.ltdmanager.blessingskin.request -import io.ktor.http.* import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult +import top.r3944realms.ltdmanager.core.client.request.IRequest @Serializable -abstract class BlessingSkinRequest( +abstract class BlessingSkinRequest( @Transient - open val createTime: Long = System.currentTimeMillis() -) { - /** - * 转换为JSON字符串 - */ - abstract fun toJSON(): String - - /** - * 获取API路径(不包含基础URL) - * 例如: "invitation-codes/generate" - */ - abstract fun path(): String - - /** - * 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数) - */ - open fun method(): HttpMethod = HttpMethod.Get - - /** - * 自定义请求头 - */ - open fun headers(): HeadersBuilder.() -> Unit = { - // 默认添加Content-Type - append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - // 添加Accept头 - append(HttpHeaders.Accept, "application/json") - } - - /** - * 获取查询参数(用于URL参数) - * 例如: mapOf("token" to "abc123", "amount" to "1") - */ - open fun queryParameters(): Map = emptyMap() - - /** - * 获取请求体参数(用于POST请求的JSON body) - * 例如: mapOf("token" to "abc123", "amount" to 1) - */ - open fun bodyParameters(): Map = emptyMap() - - /** - * 获取请求体内容类型,默认为Application.Json - */ - open fun contentType(): ContentType = ContentType.Application.Json - - /** - * 解析响应JSON字符串 - * @param responseJson 响应JSON字符串 - * @param httpStatusCode HTTP状态码 - */ - abstract fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult - - /** - * 获取预期的成功响应类型名称(用于日志和调试) - */ - abstract fun expectedResponseType(): String - - /** - * 获取预期的失败响应类型名称(用于日志和调试) - */ - abstract fun expectedFailureType(): String - - /** - * 是否需要在失败时重试(默认重试) - */ - open fun shouldRetryOnFailure(): Boolean = true -} + override val createTime: Long = System.currentTimeMillis() +): IRequest \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/invitecode/GenerateInvitationCodeRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/invitecode/GenerateInvitationCodeRequest.kt index b5d2f5a..2f1f411 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/invitecode/GenerateInvitationCodeRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/request/invitecode/GenerateInvitationCodeRequest.kt @@ -6,8 +6,8 @@ import kotlinx.serialization.Transient import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import java.util.* @@ -17,9 +17,7 @@ class GenerateInvitationCodeRequest( val token: String? = null, @Transient val amount: Int? = 1, - @Transient - override val createTime: Long = System.currentTimeMillis() -) : BlessingSkinRequest() { +) : BlessingSkinRequest() { override fun toJSON(): String { // 对于GET请求,参数在URL中,body可以为空 @@ -66,7 +64,7 @@ class GenerateInvitationCodeRequest( } catch (e: Exception) { ResponseResult.Failure( FailedBlessingSkinResponse.Default( - failedResult = "解析响应失败: ${e.message}" + failedMessage = "解析响应失败: ${e.message}" ) ) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/BlessingSkinResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/BlessingSkinResponse.kt index 1a048c5..802b1b8 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/BlessingSkinResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/BlessingSkinResponse.kt @@ -7,14 +7,15 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse +import top.r3944realms.ltdmanager.core.client.response.IResponse @Serializable abstract class BlessingSkinResponse ( @Transient - open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, @Transient - open val createTime: Long = System.currentTimeMillis() -) { + override val createTime: Long = System.currentTimeMillis() +) : IResponse { companion object { // 通用的反序列化方法 inline fun decode(jsonString: String): T { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/FailedBlessingSkinResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/FailedBlessingSkinResponse.kt index 2c0a32e..0931960 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/FailedBlessingSkinResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/FailedBlessingSkinResponse.kt @@ -2,13 +2,12 @@ package top.r3944realms.ltdmanager.blessingskin.response import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse @Serializable -abstract class FailedBlessingSkinResponse: BlessingSkinResponse() { - abstract fun failedMessage(): String +abstract class FailedBlessingSkinResponse: BlessingSkinResponse(), IFailedResponse { @Serializable - class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() { - override fun failedMessage(): String = failedResult!! + class Default(@Transient override val failedMessage: String = "未知错误") : FailedBlessingSkinResponse() { } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/invitecode/InvitationCodeGenerationResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/invitecode/InvitationCodeGenerationResponse.kt index 7f6dbc8..74ce612 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/invitecode/InvitationCodeGenerationResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/response/invitecode/InvitationCodeGenerationResponse.kt @@ -1,7 +1,7 @@ package top.r3944realms.ltdmanager.blessingskin.response.invitecode -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse @Serializable data class InvitationCodeGenerationResponse( @@ -10,12 +10,4 @@ data class InvitationCodeGenerationResponse( val data: List? = null ) : BlessingSkinResponse() { - @Serializable - data class InvitationCode( - val code: String, - @SerialName("generated_at") - val generatedAt: String, - @SerialName("expires_at") - val expiresAt: String - ) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt index 56eee15..50ff3dc 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoClient.kt @@ -3,65 +3,157 @@ package top.r3944realms.ltdmanager.chevereto import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.* import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit -import kotlinx.serialization.json.Json -import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse +import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource +import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest +import top.r3944realms.ltdmanager.chevereto.request.v1.CheveretoUploadRequest +import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse +import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse +import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse +import top.r3944realms.ltdmanager.core.client.IClient +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse +import top.r3944realms.ltdmanager.core.client.response.IResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult import top.r3944realms.ltdmanager.core.config.YamlConfigLoader +import top.r3944realms.ltdmanager.utils.Environment +import top.r3944realms.ltdmanager.utils.LoggerUtil import java.io.ByteArrayInputStream -import java.io.Closeable import java.io.File import java.util.* -import kotlin.collections.ArrayDeque - -class CheveretoClient private constructor() : Closeable { +class CheveretoClient private constructor() : + IClient { private val client = HttpClient(CIO) { - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) + expectSuccess = false + // 安装 HttpTimeout 插件 + install(HttpTimeout) { + // 默认超时配置,会被具体请求的配置覆盖 + requestTimeoutMillis = 30000 + connectTimeoutMillis = 10000 + socketTimeoutMillis = 15000 } } + private val imgTuConfig = YamlConfigLoader.loadTuImgConfig() - private val apiUrl = imgTuConfig.url!! + private val baseUrl = imgTuConfig.url!!.removeSuffix("/") private val apiKey = imgTuConfig.decryptedPassword!! - // 限流,同时最多 3 个上传 + private val semaphore = Semaphore(3) - - // 普通队列 (按 priority 排序) - private val queue = PriorityQueue>(compareBy { it.priority }) + private val queue = PriorityQueue() private val queueMutex = Mutex() - - // 紧急队列 (FIFO,最多 10 个) - private val urgentQueue = ArrayDeque>(10) - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { - scope.launch { - while (isActive) { - val item = queueMutex.withLock { - when { - urgentQueue.isNotEmpty() -> urgentQueue.removeFirst() - queue.isNotEmpty() -> queue.poll() - else -> null + init() + } + + override fun getType(): String = "CheveretoClient" + + override fun getClient(): HttpClient = client + + override fun getSemaphore(): Semaphore = semaphore + + override fun getRequestMutex(): Mutex = queueMutex + + override fun getResponseQueue(): PriorityQueue = queue + + override fun getScope(): CoroutineScope = scope + + override fun getBaseUrl(): String = baseUrl + + override fun createFailureResponse(exception: Exception?): FailedCheveretoResponse = + FailedCheveretoResponse.Default( + httpStatusCode = HttpStatusCode.InternalServerError, + failedMessage = exception?.message ?: "Unknown error" + ) + + override fun addToQueue( + request: CheveretoRequest, + deferredC: CompletableDeferred>, + priority: Int, + maxRetries: Int + ): CheveretoQueueItem { + val item = CheveretoQueueItem(request, deferredC, maxRetries, priority, true) + queue.add(item) + return item + } + + override suspend fun processQueueItem(item: CheveretoQueueItem) { + getSemaphore().withPermit { + val request = item.request + val deferred = item.deferred + val maxRetries = item.retries + var attempt = 0 + var lastError: Exception? + + while (attempt < maxRetries) { + try { + val fullUrl = buildFullUrlWithQueryParams(request) + if (!Environment.isProduction()) { + LoggerUtil.logger.debug("发送请求到: $fullUrl") + LoggerUtil.logger.debug("请求方法: {}", request.method()) } + val response = getClient().request(fullUrl) { + method = request.method() + + + // 设置请求头 + headers { + request.headers().invoke(this) + header("X-API-Key", apiKey) + } + + // 对于非GET请求,设置请求体 + if (request.method() != HttpMethod.Get) { + setBody(request.toJSON()) + } + } + val responseText: String = response.body() + + if (!Environment.isProduction()) { + LoggerUtil.logger.debug("响应状态: {}", response.status) + LoggerUtil.logger.debug("响应内容: $responseText") + } + + // 检查是否是HTML响应(重定向) + if (isHtmlResponse(responseText)) { + throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置") + } + + // 解析响应 + val result = request.getResponse(responseText, response.status) + + @Suppress("UNCHECKED_CAST") + (deferred as CompletableDeferred>).complete(result) + + return + } catch (e: Exception) { + lastError = e + attempt++ + + if (!request.shouldRetryOnFailure() || attempt >= maxRetries) { + break + } + + LoggerUtil.logger.warn("${getType()} 请求失败 (尝试 $attempt/$maxRetries): ${e.message}") + delay((attempt * 1000L)) // 指数退避 } - if (item != null) processItem(item) - else delay(20) + // 所有重试都失败或不应重试 + val errorResponse = createFailureResponse(lastError) + @Suppress("UNCHECKED_CAST") + (deferred as CompletableDeferred>).complete( + ResponseResult.Failure(errorResponse) + ) } } } - /** * 上传 File */ @@ -77,35 +169,24 @@ class CheveretoClient private constructor() : Closeable { nsfw: Int? = null, format: String = "json", useFileDate: Int? = null, - priority: Int = 5 + priority: Int = 5, + maxRetries: Int = 3 + ): CheveretoResponse { - val deferred = CompletableDeferred() - val source = suspend { - safeUpload { - submitFormWithBinaryData( - url = apiUrl, - formData = formData { - append("source", file.readBytes(), Headers.build { - append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"") - }) - append("format", format) - title?.let { append("title", it) } - description?.let { append("description", it) } - tags?.let { append("tags", it) } - albumId?.let { append("album_id", it) } - categoryId?.let { append("category_id", it) } - width?.let { append("width", it.toString()) } - expiration?.let { append("expiration", it) } - nsfw?.let { append("nsfw", it.toString()) } - useFileDate?.let { append("use_file_date", it.toString()) } - } - ) { - header("X-API-Key", apiKey) - } - } - } - queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) } - return deferred.await() + upload(CheveretoUploadRequest( + source = CheveretoSource.ByteArraySource(file.readBytes(), file.name), + format = format, + title = title, + description = description, + tags = tags, + albumId = albumId, + categoryId = categoryId, + width = width, + expiration = expiration, + nsfw = nsfw, + useFileDate = useFileDate + ), priority, maxRetries).getRetResponse() + throw Exception("Never Reach") } @@ -125,36 +206,23 @@ class CheveretoClient private constructor() : Closeable { nsfw: Int? = null, format: String = "json", useFileDate: Int? = null, - priority: Int = 5 + priority: Int = 5, + maxRetries: Int = 3 ): CheveretoResponse { - val deferred = CompletableDeferred() - val source = suspend { - val bytes = inputStream.readBytes() - safeUpload { - submitFormWithBinaryData( - url = apiUrl, - formData = formData { - append("source", bytes, Headers.build { - append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"") - }) - append("format", format) - title?.let { append("title", it) } - description?.let { append("description", it) } - tags?.let { append("tags", it) } - albumId?.let { append("album_id", it) } - categoryId?.let { append("category_id", it) } - width?.let { append("width", it.toString()) } - expiration?.let { append("expiration", it) } - nsfw?.let { append("nsfw", it.toString()) } - useFileDate?.let { append("use_file_date", it.toString()) } - } - ) { - header("X-API-Key", apiKey) - } - } - } - queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) } - return deferred.await() + upload(CheveretoUploadRequest( + source = CheveretoSource.ByteArraySource(inputStream.readBytes(), fileName), + format = format, + title = title, + description = description, + tags = tags, + albumId = albumId, + categoryId = categoryId, + width = width, + expiration = expiration, + nsfw = nsfw, + useFileDate = useFileDate + ), priority, maxRetries).getRetResponse() + throw Exception("Never Reach") } /** @@ -172,64 +240,41 @@ class CheveretoClient private constructor() : Closeable { nsfw: Int? = null, format: String = "json", useFileDate: Int? = null, - priority: Int = 5 + priority: Int = 5, + maxRetries: Int = 3 ): CheveretoResponse { - val deferred = CompletableDeferred() - val source = suspend { - safeUpload { - submitForm( - url = apiUrl, - formParameters = Parameters.build { - append("source", url) - append("format", format) - title?.let { append("title", it) } - description?.let { append("description", it) } - tags?.let { append("tags", it) } - albumId?.let { append("album_id", it) } - categoryId?.let { append("category_id", it) } - width?.let { append("width", it.toString()) } - expiration?.let { append("expiration", it) } - nsfw?.let { append("nsfw", it.toString()) } - useFileDate?.let { append("use_file_date", it.toString()) } - } - ) { - header("X-API-Key", apiKey) - } - } - } - queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) } - return deferred.await() + upload(CheveretoUploadRequest( + source = CheveretoSource.UrlSource(url), + format = format, + title = title, + description = description, + tags = tags, + albumId = albumId, + categoryId = categoryId, + width = width, + expiration = expiration, + nsfw = nsfw, + useFileDate = useFileDate + ), priority, maxRetries).getRetResponse() + throw Exception("Never Reach") } - private suspend fun processItem(item: CheveretoQueueItem) { - semaphore.withPermit { - try { - val result = item.source() - item.deferred.complete(result) - } catch (e: Exception) { - item.deferred.completeExceptionally(e) - } - } - } - /** - * 包装上传,失败时打印原始响应 - */ - private suspend fun safeUpload(block: suspend HttpClient.() -> HttpResponse): CheveretoResponse { - val response = client.block() + suspend fun upload( + request: CheveretoUploadRequest, priority: Int, maxRetries: Int + ): ResponseResult { return try { - response.body() + @Suppress("UNCHECKED_CAST") + submitRequest(request, priority, maxRetries) as ResponseResult } catch (e: Exception) { - val raw = response.bodyAsText() - throw RuntimeException("Upload failed (status=${response.status}): $raw", e) + ResponseResult.Failure( + FailedCheveretoResponse.Default( + httpStatusCode = HttpStatusCode.InternalServerError, + failedMessage = "Byte array upload failed: ${e.message}" + ) + ) } } - - override fun close() { - scope.cancel() - runBlocking { client.close() } - } - companion object { fun create(): CheveretoClient = CheveretoClient() } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt index a4d4433..b150ecf 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/CheveretoQueueItem.kt @@ -1,9 +1,17 @@ package top.r3944realms.ltdmanager.chevereto import kotlinx.coroutines.CompletableDeferred +import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest +import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse +import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse +import top.r3944realms.ltdmanager.core.client.QueueItem -data class CheveretoQueueItem( - val source: suspend () -> T, - val deferred: CompletableDeferred, - val priority: Int = 5 +data class CheveretoQueueItem( + val request0: CheveretoRequest, + val deferred0: CompletableDeferred<*>, + val priority0: Int, + var retries0: Int, + val expectsResponse0: Boolean +) : QueueItem( + request0, deferred0, retries0, priority0, expectsResponse0 ) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt index 37a668a..b14ff75 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt @@ -1,4 +1,11 @@ package top.r3944realms.ltdmanager.chevereto.data -class CheveretoSource { +import kotlinx.serialization.Serializable + +@Serializable +sealed class CheveretoSource { + @Serializable + data class ByteArraySource(val bytes: ByteArray, val fileName: String) : CheveretoSource() + @Serializable + data class UrlSource(val url: String) : CheveretoSource() } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/SuccessInfo.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/SuccessInfo.kt index f7c7d6a..d4590c8 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/SuccessInfo.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/SuccessInfo.kt @@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager.chevereto.data import kotlinx.serialization.Serializable @Serializable -data class Success( +data class SuccessInfo( val message : String? = null, val code: Int? = 200, ) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt index 048505a..03a159e 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt @@ -1,4 +1,13 @@ package top.r3944realms.ltdmanager.chevereto.request -class CheveretoRequest { -} \ No newline at end of file +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse +import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse +import top.r3944realms.ltdmanager.core.client.request.IRequest + +@Serializable +abstract class CheveretoRequest( + @Transient + override val createTime: Long = System.currentTimeMillis() +) : IRequest \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt index 750991e..a3a0e26 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt @@ -1,4 +1,89 @@ package top.r3944realms.ltdmanager.chevereto.request.v1 -class CheveretoUploadRequest { -} \ No newline at end of file +import io.ktor.http.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource +import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest +import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse +import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult + +@Serializable +data class CheveretoUploadRequest( + private val source: CheveretoSource, + private val title: String? = null, + private val description: String? = null, + private val tags: String? = null, + @SerialName("album_id") + private val albumId: String? = null, + @SerialName("category_id") + private val categoryId: String? = null, + private val width: Int? = null, + private val expiration: String? = null, + private val nsfw: Int? = null, + private val format: String = "json", + @SerialName("use_file_date") + private val useFileDate: Int? = null +) : CheveretoRequest() { + override fun path(): String = "api/1/upload" + + override fun method(): HttpMethod = HttpMethod.Post + + override fun headers(): HeadersBuilder.() -> Unit = { + append(HttpHeaders.Accept, "application/json") + // 对于文件上传,Content-Type 由 Ktor 自动设置 + } + override fun bodyParameters(): Map { + val params = mutableMapOf() + + title?.let { params["title"] = it } + description?.let { params["description"] = it } + tags?.let { params["tags"] = it } + albumId?.let { params["album_id"] = it } + categoryId?.let { params["category_id"] = it } + width?.let { params["width"] = it } + expiration?.let { params["expiration"] = it } + nsfw?.let { params["nsfw"] = it } + params["format"] = format + useFileDate?.let { params["use_file_date"] = it } + + return params + } + + override fun toJSON(): String = Json.encodeToString(this) + + override fun getResponse( + responseJson: String, + httpStatusCode: HttpStatusCode + ): ResponseResult { + return try { + if (httpStatusCode.isSuccess()) { + val successResponse = Json.decodeFromString(responseJson) + ResponseResult.Success(successResponse) + } else { + ResponseResult.Failure( + FailedCheveretoResponse.Default( + httpStatusCode = HttpStatusCode.InternalServerError, + failedMessage = responseJson.takeIf { it.isNotBlank() }?:"ERROR" + ) + ) + } + } catch (e: Exception) { + ResponseResult.Failure( + FailedCheveretoResponse.Default( + httpStatusCode = HttpStatusCode.InternalServerError, + failedMessage = "Failed to parse response: ${e.message}. Raw response: $responseJson" + ) + ) + } + } + + override fun expectedResponseType(): String = "CheveretoUploadResponse" + + override fun expectedFailureType(): String = "FailedCheveretoResponse" + + override fun shouldRetryOnFailure(): Boolean = true +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt index 02f6408..16e244e 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt @@ -1,4 +1,37 @@ package top.r3944realms.ltdmanager.chevereto.response -class CheveretoSuccessResponse { +import io.ktor.http.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse +import top.r3944realms.ltdmanager.core.client.response.IResponse + +@Serializable +abstract class CheveretoResponse ( + @Transient + override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + @Transient + override val createTime: Long = System.currentTimeMillis() +) : IResponse { + companion object { + // 通用的反序列化方法 + inline fun decode(jsonString: String): T { + return json.decodeFromString(jsonString) + } + val json: Json by lazy { + Json { + ignoreUnknownKeys = true + serializersModule = SerializersModule { + polymorphic(CheveretoResponse::class) { + subclass(FailedCheveretoResponse.Default::class, FailedCheveretoResponse.Default.serializer()) + subclass(CheveretoUploadResponse::class, CheveretoUploadResponse.serializer()) + } + } + } + } + } + } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt index 5c24b77..28f498b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt @@ -1,4 +1,12 @@ package top.r3944realms.ltdmanager.chevereto.response -class FailedCheveretoResponse { +import io.ktor.http.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse + +@Serializable +abstract class FailedCheveretoResponse: CheveretoResponse(), IFailedResponse { + @Serializable + class Default(@Transient override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, @Transient override val failedMessage: String = "未知错误") : FailedCheveretoResponse() } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt index c33d124..90e83f6 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt @@ -1,4 +1,17 @@ package top.r3944realms.ltdmanager.chevereto.response.v1 -class CheveretoUploadResponse { -} \ No newline at end of file +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.chevereto.data.CheveretoImage +import top.r3944realms.ltdmanager.chevereto.data.SuccessInfo +import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse + +@Serializable +data class CheveretoUploadResponse( + @SerialName("status_code") + val statusCode: Int, + val success: SuccessInfo, + val image: CheveretoImage, + @SerialName("status_txt") + val statusTxt: String +) : CheveretoResponse() \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt index 147cc82..eb2b669 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt @@ -1,4 +1,170 @@ -package top.r3944realms.ltdmanager.basic +package top.r3944realms.ltdmanager.core.client -interface IClient { +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.sync.withPermit +import top.r3944realms.ltdmanager.core.client.request.IRequest +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse +import top.r3944realms.ltdmanager.core.client.response.IResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult +import top.r3944realms.ltdmanager.utils.Environment +import top.r3944realms.ltdmanager.utils.LoggerUtil +import java.net.URLEncoder +import java.util.* + +interface IClient, Q: QueueItem, T: IResponse, F: IFailedResponse> : AutoCloseable { + fun getType(): String + fun getClient(): HttpClient + fun getSemaphore(): Semaphore + fun getRequestMutex(): Mutex + fun getResponseQueue(): PriorityQueue + fun getScope(): CoroutineScope + fun getBaseUrl(): String = "http://localhost:1234" + fun createFailureResponse(exception: Exception? ): IFailedResponse + fun init() { + startQueueProcessor() + } + fun startQueueProcessor() { + getScope().launch { + while (isActive) { + val item = getRequestMutex().withLock { + getResponseQueue().poll() + } + if (item == null) { + delay(50) + continue + } + processQueueItem(item) + } + } + } + fun addToQueue(request: R, + deferredC: CompletableDeferred>, + priority: Int = 5, + maxRetries: Int = 3): Q + /** + * 提交请求 + */ + suspend fun submitRequest( + request: R, + priority: Int = 5, + maxRetries: Int = 3 + ): ResponseResult { + val deferred = CompletableDeferred>() + getRequestMutex().withLock { + addToQueue(request, deferred, priority, maxRetries) + } + return deferred.await() + } + + suspend fun processQueueItem(item: Q) { + getSemaphore().withPermit { + val request = item.request + val deferred = item.deferred + val maxRetries = item.retries + var attempt = 0 + var lastError: Exception? + + while (attempt < maxRetries) { + try { + val fullUrl = buildFullUrlWithQueryParams(request) + if (!Environment.isProduction()) { + LoggerUtil.logger.debug("发送请求到: $fullUrl") + LoggerUtil.logger.debug("请求方法: {}", request.method()) + } + val response = getClient().request(fullUrl) { + method = request.method() + + + // 设置请求头 + headers { + request.headers().invoke(this) + } + + // 对于非GET请求,设置请求体 + if (request.method() != HttpMethod.Get) { + setBody(request.toJSON()) + } + } + val responseText: String = response.body() + + if (!Environment.isProduction()) { + LoggerUtil.logger.debug("响应状态: {}", response.status) + LoggerUtil.logger.debug("响应内容: $responseText") + } + + // 检查是否是HTML响应(重定向) + if (isHtmlResponse(responseText)) { + throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置") + } + + // 解析响应 + val result = request.getResponse(responseText, response.status) + + @Suppress("UNCHECKED_CAST") + (deferred as CompletableDeferred>).complete(result) + + return + } catch (e: Exception) { + lastError = e + attempt++ + + if (!request.shouldRetryOnFailure() || attempt >= maxRetries) { + break + } + + LoggerUtil.logger.warn("${getType()} 请求失败 (尝试 $attempt/$maxRetries): ${e.message}") + delay((attempt * 1000L)) // 指数退避 + } + // 所有重试都失败或不应重试 + val errorResponse = createFailureResponse(lastError) + @Suppress("UNCHECKED_CAST") + (deferred as CompletableDeferred>).complete( + ResponseResult.Failure(errorResponse) + ) + } + } + } + /** + * 构建完整的URL,包含查询参数 + */ + fun buildFullUrlWithQueryParams(request: R): String { + val baseUrl = getBaseUrl().removeSuffix("/") + val path = request.path().removePrefix("/") + + // 构建基础URL + val urlBuilder = StringBuilder("$baseUrl/$path") + + // 添加查询参数 + val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) -> + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" + } + + if (queryParams.isNotEmpty()) { + urlBuilder.append("?").append(queryParams) + } + + return urlBuilder.toString() + } + + /** + * 检查是否是HTML响应 + */ + fun isHtmlResponse(text: String): Boolean { + return text.contains("", ignoreCase = true) || + text.contains("", ignoreCase = true) || + text.contains("Redirecting", ignoreCase = true) + } + override fun close() { + getScope().cancel() + runBlocking { + getClient().close() + } + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt index abaa104..f275f63 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt @@ -1,19 +1,26 @@ package top.r3944realms.ltdmanager.core.client +import kotlinx.coroutines.CompletableDeferred import top.r3944realms.ltdmanager.core.client.request.IRequest import top.r3944realms.ltdmanager.core.client.response.IFailedResponse import top.r3944realms.ltdmanager.core.client.response.IResponse -import java.util.concurrent.CompletableFuture -interface IQueueItem : Comparable> { - fun getRequest(): IRequest - fun getDeferred(): CompletableFuture<*> - fun getRetries(): Int - fun getPriority(): Int +open class QueueItem, T:IResponse, F:IFailedResponse>( + val request: R, + val deferred: CompletableDeferred<*>, + val retries: Int, + val priority: Int, + val expectsResponse: Boolean + +) : Comparable> { +// fun getRequest(): R = request +// fun getDeferred(): CompletableDeferred<*> = deferred +// fun getRetries(): Int = retries +// fun getPriority(): Int = priority /** * @return true 表示返回 BlessingSkinResponse, false 表示 Unit */ - fun expectsResponse(): Boolean - override fun compareTo(other: IQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = getPriority().compareTo(other.getPriority()) + fun expectsResponse(): Boolean = expectsResponse + override fun compareTo(other: QueueItem): Int = priority.compareTo(other.priority) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt index d55515d..97d2922 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt @@ -1,4 +1,76 @@ package top.r3944realms.ltdmanager.core.client.request -interface IRequest { +import io.ktor.http.* +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse +import top.r3944realms.ltdmanager.core.client.response.IResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult + +interface IRequest { + // 只使用属性 + val createTime: Long + + /** + * 转换为JSON字符串 + */ + fun toJSON(): String + + /** + * 获取API路径(不包含基础URL) + * 例如: "invitation-codes/generate" + */ + fun path(): String + + /** + * 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数) + */ + fun method(): HttpMethod = HttpMethod.Get + + /** + * 自定义请求头 + */ + fun headers(): HeadersBuilder.() -> Unit = { + // 默认添加Content-Type + append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + // 添加Accept头 + append(HttpHeaders.Accept, "application/json") + } + + /** + * 获取查询参数(用于URL参数) + * 例如: mapOf("token" to "abc123", "amount" to "1") + */ + fun queryParameters(): Map = emptyMap() + + /** + * 获取请求体参数(用于POST请求的JSON body) + * 例如: mapOf("token" to "abc123", "amount" to 1) + */ + fun bodyParameters(): Map = emptyMap() + + /** + * 获取请求体内容类型,默认为Application.Json + */ + fun contentType(): ContentType = ContentType.Application.Json + + /** + * 解析响应JSON字符串 + * @param responseJson 响应JSON字符串 + * @param httpStatusCode HTTP状态码 + */ + fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult + + /** + * 获取预期的成功响应类型名称(用于日志和调试) + */ + fun expectedResponseType(): String + + /** + * 获取预期的失败响应类型名称(用于日志和调试) + */ + fun expectedFailureType(): String + + /** + * 是否需要在失败时重试(默认重试) + */ + fun shouldRetryOnFailure(): Boolean = true } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt index 499a813..a0f47fd 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt @@ -1,4 +1,7 @@ package top.r3944realms.ltdmanager.core.client.response -class IFailedResponse { +interface IFailedResponse : IResponse { + val failedMessage: String + val thrownException: Exception + get() = Exception(failedMessage) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt index b64f017..edb5550 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt @@ -1,4 +1,8 @@ package top.r3944realms.ltdmanager.core.client.response +import io.ktor.http.* + interface IResponse { + val httpStatusCode: HttpStatusCode + val createTime: Long } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt index 7ef41a8..272b64b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt @@ -1,4 +1,47 @@ package top.r3944realms.ltdmanager.core.client.response -class ResponseResult { +sealed class ResponseResult { + data class Success(val response: T) : ResponseResult() + data class Failure(val failure: F) : ResponseResult() + /** + * 检查是否成功 + */ + fun isSuccess(): Boolean = this is Success + + /** + * 获取成功响应(如果存在) + */ + fun getSuccessResponse(): T? = (this as? Success)?.response + + /** + * 获取失败响应(如果存在) + */ + fun getFailureResponse(): F? = (this as? Failure)?.failure + + /** + * 成功时执行操作 + */ + inline fun onSuccess(action: (T) -> Unit): ResponseResult { + if (this is Success) action(response) + return this + } + + /** + * 失败时执行操作 + */ + inline fun onFailure(action: (F) -> Unit): ResponseResult { + if (this is Failure) action(failure) + return this + } + + fun getRetResponse(): T { + if (this is Success) { + return response + } + else if (this is Failure) { + @Suppress("UNCHECKED_CAST") + return failure as T + } + throw Exception("Never Reach") + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt index 4b9acbe..b79df43 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt @@ -1,4 +1,54 @@ package top.r3944realms.ltdmanager.core.config -class McsmConfig { +import top.r3944realms.ltdmanager.utils.CryptoUtil +import top.r3944realms.ltdmanager.utils.YamlUpdater + +data class McsmConfig( + var url: String ?= null, + var encryptedApiKey: String ?= null, + var instanceID: String ?= null, + ) { + val decryptedApiKey: String? + get() { + if (encryptedApiKey == null) return null + if (!isEncrypted()) return encryptedApiKey + try { + val cipherText = encryptedApiKey!!.substring(4, encryptedApiKey!!.length - 1) + return CryptoUtil.decrypt(cipherText) + } catch (e: Exception) { + throw IllegalStateException("API解密失败", e) + } + } + + /** + * 加密密码(如果未加密),并写回配置文件 + */ + fun encryptApi() { + if (encryptedApiKey == null || isEncrypted()) { + return + } + try { + encryptedApiKey = "ENC(${CryptoUtil.encrypt(encryptedApiKey!!)})" + YamlUpdater.updateYaml( + YamlConfigLoader.configFilePath.toString(), + "mcsm.encrypted-api-key", + this.encryptedApiKey!! + ) + } catch (e: Exception) { + throw IllegalStateException("API加密失败", e) + } + } + + /** + * 检查密码是否已加密 + */ + private fun isEncrypted(): Boolean { + return encryptedApiKey != null && + encryptedApiKey!!.startsWith("ENC(") && + encryptedApiKey!!.endsWith(")") + } + + override fun toString(): String { + return "McsmConfig(url=$url, api-key=***)" + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt index 7fbe554..f764df4 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/YamlConfigLoader.kt @@ -31,6 +31,7 @@ object YamlConfigLoader { config?.database?.encryptPassword() config?.websocket?.encryptToken() config?.http?.encryptToken() + config?.mcsm?.encryptApi() config?.mail?.encryptPassword() config?.tools?.rcon?.encryptPassword() config?.blessingSkinServer?.invitationApi?.encryptToken() @@ -72,6 +73,7 @@ object YamlConfigLoader { fun loadDatabaseConfig(): DatabaseConfig = config.database fun loadCryptoConfig(): CryptoConfig = config.crypto + fun loadMcsmConfig(): McsmConfig = config.mcsm fun loadWebsocketConfig(): WebsocketConfig = config.websocket fun loadHttpConfig(): HttpConfig = config.http fun loadModeConfig(): ModeConfig = config.mode @@ -88,6 +90,7 @@ object YamlConfigLoader { var http: HttpConfig = HttpConfig(), var tools: ToolConfig = ToolConfig(), var mail: MailConfig = MailConfig(), + var mcsm: McsmConfig = McsmConfig(), var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(), var dgLab: DgLabConfig = DgLabConfig(), var imgTu: ImgTuConfig = ImgTuConfig(), diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt index b74eea3..1f2cd7b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt @@ -1,4 +1,4 @@ package top.r3944realms.ltdmanager.core.init -class DependencyResolver { +class DependencyResolver() { } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt index ac6e6cd..1004da4 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt @@ -1,26 +1,149 @@ -package top.r3944realms.ltdmanager.core.config +package top.r3944realms.ltdmanager.core.init +import top.r3944realms.ltdmanager.module.Modules +import top.r3944realms.ltdmanager.module.exception.ConfigError data class ModuleConfig( val name: String, val type: ModuleType, val enabled: Boolean, - val + val dependencies: List = emptyList(), + val config: Map = emptyMap() ) { data class Dependency( - val moduleName: String, // 依赖的模块名称 - val type: DependencyType, // 依赖类型 - val required: Boolean = true // 是否必需 - ) - enum class ModuleType { - GROUP_MESSAGE_POLLING_MODULE, - GROUP_REQUEST_HANDLER_MODULE, - MAIL_MODULE, - BAN_MODULE, - DG_LAB_MODULE, - INVITE_MODULE, - MC_SERVER_STATUS_MODULE, - RCON_PLAYER_LIST_MODULE, - STATE_MODULE + private val name: String, + val type: ModuleType, + val required: Boolean = true + ) { + private val dependencyName: String = "${type.modName}-$name" + + fun getDepName() :String = dependencyName + + } + enum class ModuleType(val modName: String) { + GROUP_MESSAGE_POLLING_MODULE(Modules.GROUP_MESSAGE_POLLING), + GROUP_REQUEST_HANDLER_MODULE(Modules.GROUP_REQUEST_HANDLER), + MAIL_MODULE(Modules.MAIL), + BAN_MODULE(Modules.BAN), + DG_LAB_MODULE(Modules.DG_LAB), + INVITE_MODULE(Modules.INVITATION_CODE), + MC_SERVER_STATUS_MODULE(Modules.MC_SERVER_STATUS), + MOD_GROUP_HANDLER_MODULE(Modules.MOD_GROUP_HANDLER), + RCON_PLAYER_LIST_MODULE(Modules.RCON_PLAYER_LIST), + STATE_MODULE(Modules.STATE), + HELP_MODULE(Modules.HELP),; + } + // 基础获取方法 + fun value(paramName: String): Any = + config[paramName] ?: throw ConfigError( + ConfigError.Type.MISSING_PARAMETER, + name, + paramName + ) + + // 泛型获取方法 + private inline fun get(paramName: String): T { + val value = value(paramName) + return when (T::class) { + Long::class -> convertToLong(value, paramName) as T + Int::class -> convertToInt(value, paramName) as T + String::class -> value.toString() as T + Boolean::class -> convertToBoolean(value, paramName) as T + Double::class -> convertToDouble(value, paramName) as T + Float::class -> convertToFloat(value, paramName) as T + else -> { + if (value is T) value + else throw typeMismatchError(value, paramName) + } + } + } + + // 特定类型方法(向后兼容) + fun long(paramName: String): Long = get(paramName) + fun int(paramName: String): Int = get(paramName) + fun string(paramName: String): String = get(paramName) + fun boolean(paramName: String): Boolean = get(paramName) + fun double(paramName: String): Double = get(paramName) + fun float(paramName: String): Float = get(paramName) + + // 可选值方法 + inline fun getOrNull(paramName: String): T? = + config[paramName] as? T ?: run { + val value = config[paramName] + if (value == null) null + else if (value is T) value + else null + } + + inline fun getOrDefault(paramName: String, defaultValue: T): T = + getOrNull(paramName) ?: defaultValue + + // 类型转换辅助方法 + private fun convertToLong(value: Any, paramName: String): Long = when (value) { + is Long -> value + is Number -> value.toLong() + is String -> try { + value.toLong() + } catch (e: NumberFormatException) { + throw typeMismatchError(value, paramName) + } + else -> throw typeMismatchError(value, paramName) + } + + private fun convertToInt(value: Any, paramName: String): Int = when (value) { + is Int -> value + is Number -> value.toInt() + is String -> try { + value.toInt() + } catch (e: NumberFormatException) { + throw typeMismatchError(value, paramName) + } + else -> throw typeMismatchError(value, paramName) + } + + private fun convertToBoolean(value: Any, paramName: String): Boolean = when (value) { + is Boolean -> value + is String -> when (value.lowercase()) { + "true", "yes", "1" -> true + "false", "no", "0" -> false + else -> throw typeMismatchError(value, paramName) + } + is Number -> value.toInt() != 0 + else -> throw typeMismatchError(value, paramName) + } + + private fun convertToDouble(value: Any, paramName: String): Double = when (value) { + is Double -> value + is Number -> value.toDouble() + is String -> try { + value.toDouble() + } catch (e: NumberFormatException) { + throw typeMismatchError(value, paramName) + } + else -> throw typeMismatchError(value, paramName) + } + + private fun convertToFloat(value: Any, paramName: String): Float = when (value) { + is Float -> value + is Number -> value.toFloat() + is String -> try { + value.toFloat() + } catch (e: NumberFormatException) { + throw typeMismatchError(value, paramName) + } + else -> throw typeMismatchError(value, paramName) + } + + // 错误处理辅助方法 + private inline fun typeMismatchError( + actualValue: Any, + paramName: String + ): Nothing { + throw ConfigError( + ConfigError.Type.NOT_EXPECTED_VALUE, + name, + T::class.simpleName ?: T::class.java.simpleName, + actualValue::class.simpleName ?: actualValue::class.java.simpleName + ) } } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt index 8b14c6e..fdc8264 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt @@ -1,4 +1,32 @@ package top.r3944realms.ltdmanager.core.init +import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.core.init.ModuleConfig.ModuleType.* +import top.r3944realms.ltdmanager.module.BaseModule +import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule + object ModuleFactory { + fun createModule(config: ModuleConfig): BaseModule { + return when(config.type) { + GROUP_MESSAGE_POLLING_MODULE -> TODO() + GROUP_REQUEST_HANDLER_MODULE -> createGroupRequestHandler(config) + MAIL_MODULE -> TODO() + BAN_MODULE -> TODO() + DG_LAB_MODULE -> TODO() + INVITE_MODULE -> TODO() + MC_SERVER_STATUS_MODULE -> TODO() + RCON_PLAYER_LIST_MODULE -> TODO() + STATE_MODULE -> TODO() + MOD_GROUP_HANDLER_MODULE -> TODO() + HELP_MODULE -> TODO() + } + } + private fun createGroupRequestHandler(config: ModuleConfig): GroupRequestHandlerModule { + val targetGroupId = config.long("targetGroupId") + val pollIntervalMillis = config.getOrDefault("pollIntervalMillis", 30_000L) + return GroupRequestHandlerModule( + config.name, GlobalManager.napCatClient, + targetGroupId, pollIntervalMillis + ) + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt index 8b748ce..a6f5cad 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt @@ -1,4 +1,10 @@ package top.r3944realms.ltdmanager.core.init +import java.nio.file.Paths + object ModuleLoader { + val configFilePath = Paths.get("config/modules.yaml") + init { + + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt index a5246b5..8aae51b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt @@ -1,4 +1,15 @@ package top.r3944realms.ltdmanager.core.init +import top.r3944realms.ltdmanager.module.BaseModule + object ModuleRegistry { + private val registry: MutableMap = mutableMapOf() + + fun register(baseModule: BaseModule) { + registry.putIfAbsent(baseModule.name, baseModule) + } + + fun get(moduleName: String): BaseModule? { + return registry[moduleName] + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt index 8da26f1..917d62b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/dglab/model/game/GameClientOperation.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import top.r3944realms.ltdmanager.GlobalManager +import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse +import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.data.ID @@ -104,7 +106,7 @@ class GameClientOperation( pattern.replace(originalUrl) { matchResult -> // 保留原始 URL 中的路径部分(如果有的话) - val path = matchResult.groupValues[1] ?: "" + val path = matchResult.groupValues[1] "$configUrl$path" } } catch (e: Exception) { @@ -130,8 +132,7 @@ class GameClientOperation( albumId = "BFx", expiration = "PT5M" ) - if (response.image?.url != null) { - // 发送图床 URL 给玩家 + if (response is CheveretoUploadResponse){ napCatClient.sendUnit( SendPrivateMsgRequest( listOf( @@ -141,8 +142,15 @@ class GameClientOperation( ID.long(playerId) ) ) - } else { - LoggerUtil.logger.error("上传二维码返回 JSON 未包含 URL") + } else if (response is FailedCheveretoResponse.Default){ + napCatClient.sendUnit( + SendPrivateMsgRequest( + listOf( + MessageElement.text("无法上传图片,请联系管理员:${response.httpStatusCode} , ${response.failedMessage}"), + ), + ID.long(playerId) + ) + ) } // 启动 60 秒倒计时任务 bindingTimeoutJob = launch { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/main.kt b/src/main/kotlin/top/r3944realms/ltdmanager/main.kt index e665938..6b319a4 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/main.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/main.kt @@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import top.r3944realms.ltdmanager.module.* - +// DSL fun main() = GlobalManager.runBlockingMain { val commonGroupId:Long = 538751386 val whitelistGroupId:Long = 920719236 @@ -43,7 +43,7 @@ fun main() = GlobalManager.runBlockingMain { ) val toolConfig = YamlConfigLoader.loadToolConfig() val corconModule = RconPlayerListModule( - moduleName = "WhiteListGroup", + moduleName = "CommonGroup", groupMessagePollingModule = commonGroupMsgPollingModule, rconTimeOut = 2_000L, cooldownMillis = 10_000L, @@ -98,30 +98,30 @@ fun main() = GlobalManager.runBlockingMain { // "Apply for an invitation code" // ) // ) - val commonMcServerStatusModule = McServerStatusModule( - moduleName = "CommonGroup", - groupMessagePollingModule = commonGroupMsgPollingModule, - selfId = selfQQId, - cooldownMillis = 20_000L, - selfNickName = selfNickName, - commands = listOf("/m", "/mcs", "seek", "s", "test"), - presetServer = mapOf( - setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", - setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" - ) - ) - val whitelistMcServerStatusModule = McServerStatusModule( - moduleName = "WhiteListGroup", - groupMessagePollingModule = whiteListGroupMsgPollingModule, - selfId = selfQQId, - cooldownMillis = 20_000L, - selfNickName = selfNickName, - commands = listOf("/m", "/mcs", "seek", "s", "test"), - presetServer = mapOf( - setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", - setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" - ) - ) +// val commonMcServerStatusModule = McServerStatusModule( +// moduleName = "CommonGroup", +// groupMessagePollingModule = commonGroupMsgPollingModule, +// selfId = selfQQId, +// cooldownMillis = 20_000L, +// selfNickName = selfNickName, +// commands = listOf("/m", "/mcs", "seek", "s", "test"), +// presetServer = mapOf( +// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", +// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" +// ) +// ) +// val whitelistMcServerStatusModule = McServerStatusModule( +// moduleName = "WhiteListGroup", +// groupMessagePollingModule = whiteListGroupMsgPollingModule, +// selfId = selfQQId, +// cooldownMillis = 20_000L, +// selfNickName = selfNickName, +// commands = listOf("/m", "/mcs", "seek", "s", "test"), +// presetServer = mapOf( +// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", +// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" +// ) +// ) val dgLabModule = DGLabModule( moduleName = "DG", groupMessagePollingModule = commonGroupMsgPollingModule, @@ -134,10 +134,10 @@ fun main() = GlobalManager.runBlockingMain { GlobalManager.moduleManager.registerModule(groupModule) GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule) GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule) - GlobalManager.moduleManager.registerModule(commonMcServerStatusModule) +// GlobalManager.moduleManager.registerModule(commonMcServerStatusModule) GlobalManager.moduleManager.registerModule(rconModule) GlobalManager.moduleManager.registerModule(corconModule) - GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule) +// GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule) // GlobalManager.moduleManager.registerModule(mailModule) // GlobalManager.moduleManager.registerModule(invitationCodesModule) GlobalManager.moduleManager.registerModule(whitelistHelpModule) @@ -150,13 +150,13 @@ fun main() = GlobalManager.runBlockingMain { GlobalManager.moduleManager.loadModule(groupModule.name) GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name) GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name) - GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name) +// GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name) GlobalManager.moduleManager.loadModule(corconModule.name) GlobalManager.moduleManager.loadModule(rconModule.name) // GlobalManager.moduleManager.loadModule(mailModule.name) // GlobalManager.moduleManager.loadModule(invitationCodesModule.name) GlobalManager.moduleManager.loadModule(commonHelpModule.name) - GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name) +// GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name) GlobalManager.moduleManager.loadModule(whitelistHelpModule.name) GlobalManager.moduleManager.loadModule(dgLabModule.name) // GlobalManager.moduleManager.loadModule(banModule.name) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt index 451b7cb..2a50b44 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt @@ -1,4 +1,4 @@ -package top.r3944realms.ltdmanager.blessingskin +package top.r3944realms.ltdmanager.mcms import io.ktor.client.* import io.ktor.client.call.* @@ -11,17 +11,17 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withPermit -import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest -import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult import top.r3944realms.ltdmanager.core.config.YamlConfigLoader +import top.r3944realms.ltdmanager.mcms.request.MCSMRequest +import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse +import top.r3944realms.ltdmanager.mcms.response.MCSMResponse +import top.r3944realms.ltdmanager.mcms.response.ResponseResult import top.r3944realms.ltdmanager.utils.Environment import top.r3944realms.ltdmanager.utils.LoggerUtil import java.net.URLEncoder import java.util.* -class BlessingSkinClient private constructor() : AutoCloseable { +class MCSMClient private constructor() : AutoCloseable { private val client = HttpClient(CIO) { expectSuccess = false @@ -40,7 +40,7 @@ class BlessingSkinClient private constructor() : AutoCloseable { // 限流控制 private val semaphore = Semaphore(5) private val requestMutex = Mutex() - private val requestQueue = PriorityQueue>(compareBy { it.priority }) + private val requestQueue = PriorityQueue>(compareBy { it.priority }) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { @@ -50,14 +50,14 @@ class BlessingSkinClient private constructor() : AutoCloseable { /** * 提交请求 */ - suspend fun submitRequest( - request: BlessingSkinRequest, + suspend fun submitRequest( + request: MCSMRequest, priority: Int = 5, maxRetries: Int = 3 ): ResponseResult { val deferred = CompletableDeferred>() requestMutex.withLock { - requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true)) + requestQueue.add(MCSMSkinQueueItem(request, deferred, priority, maxRetries, true)) } return deferred.await() } @@ -83,7 +83,7 @@ class BlessingSkinClient private constructor() : AutoCloseable { /** * 处理队列项 */ - private suspend fun processQueueItem(item: BlessingSkinQueueItem) { + private suspend fun processQueueItem(item: MCSMSkinQueueItem) { semaphore.withPermit { val (request, deferred, _, maxRetries, _) = item var attempt = 0 @@ -130,7 +130,7 @@ class BlessingSkinClient private constructor() : AutoCloseable { val result = request.getResponse(responseText, response.status) @Suppress("UNCHECKED_CAST") - (deferred as CompletableDeferred>).complete(result) + (deferred as CompletableDeferred>).complete(result) return @@ -142,15 +142,15 @@ class BlessingSkinClient private constructor() : AutoCloseable { break } - LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}") + LoggerUtil.logger.warn("MCSM请求失败 (尝试 $attempt/$maxRetries): ${e.message}") delay((attempt * 1000L)) // 指数退避 } } // 所有重试都失败或不应重试 - val errorResponse = createFailureResponse(lastError, request) + val errorResponse = createFailureResponse(lastError) @Suppress("UNCHECKED_CAST") - (deferred as CompletableDeferred>).complete( + (deferred as CompletableDeferred>).complete( ResponseResult.Failure(errorResponse) ) } @@ -159,7 +159,7 @@ class BlessingSkinClient private constructor() : AutoCloseable { /** * 构建完整的URL,包含查询参数 */ - private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String { + private fun buildFullUrlWithQueryParams(request: MCSMRequest<*, *>): String { val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/") val path = request.path().removePrefix("/") @@ -191,11 +191,10 @@ class BlessingSkinClient private constructor() : AutoCloseable { * 创建失败响应 */ private fun createFailureResponse( - exception: Exception?, - request: BlessingSkinRequest<*, *> - ): FailedBlessingSkinResponse { - return FailedBlessingSkinResponse.Default( - failedResult = exception?.message ?: "未知错误", + exception: Exception? + ): FailedMCSMResponse { + return FailedMCSMResponse.ExceptionFailedMCSMResponse( + result = exception?.message ?: "未知错误", ) } @@ -207,6 +206,6 @@ class BlessingSkinClient private constructor() : AutoCloseable { } companion object { - fun create(): BlessingSkinClient = BlessingSkinClient() + fun create(): MCSMClient = MCSMClient() } } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt index 51ab78d..03317c2 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt @@ -1,16 +1,16 @@ -package top.r3944realms.ltdmanager.blessingskin +package top.r3944realms.ltdmanager.mcms import kotlinx.coroutines.CompletableDeferred -import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest -import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse +import top.r3944realms.ltdmanager.mcms.request.MCSMRequest +import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse +import top.r3944realms.ltdmanager.mcms.response.MCSMResponse -data class BlessingSkinQueueItem( - val request: BlessingSkinRequest, +data class MCSMSkinQueueItem( + val request: MCSMRequest, val deferred: CompletableDeferred<*>, var retries: Int, val priority: Int, val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit -) : Comparable> { - override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority) +) : Comparable> { + override fun compareTo(other: MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt index 8ace575..d9a9e31 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt @@ -1,14 +1,15 @@ -package top.r3944realms.ltdmanager.blessingskin.request +package top.r3944realms.ltdmanager.mcms.request import io.ktor.http.* import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse -import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult +import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse +import top.r3944realms.ltdmanager.mcms.response.MCSMResponse +import top.r3944realms.ltdmanager.mcms.response.ResponseResult + @Serializable -abstract class BlessingSkinRequest( +abstract class MCSMRequest( @Transient open val createTime: Long = System.currentTimeMillis() ) { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt index 66cb400..87a960e 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt @@ -1,4 +1,50 @@ package top.r3944realms.ltdmanager.mcms.request.instance -class GetInstanceListRequest { -} \ No newline at end of file +import io.ktor.http.* +import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.mcms.request.MCSMRequest +import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse +import top.r3944realms.ltdmanager.mcms.response.ResponseResult +import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse + +@Serializable +class GetInstanceListRequest( + private val daemonId: String, + private val page: Int, + private val pageSize: Int, + private val status: String, + private val instanceName: String? = null +) : MCSMRequest() { + + override fun toJSON(): String = "{}" // GET 无请求体 + + override fun path(): String = "api/service/remote_service_instances" + + override fun queryParameters(): Map = + buildMap { + put("daemonId", daemonId) + put("page", page.toString()) + put("page_size", pageSize.toString()) + put("status", status) + instanceName?.let { put("instance_name", it) } + } + + override fun getResponse( + responseJson: String, + httpStatusCode: HttpStatusCode + ): ResponseResult { + return if (httpStatusCode.value in 200..299) { + ResponseResult.Success( + kotlinx.serialization.json.Json.decodeFromString(responseJson) + ) + } else { + ResponseResult.Failure( + kotlinx.serialization.json.Json.decodeFromString(responseJson) + ) + } + } + + override fun expectedResponseType(): String = GetInstanceListResponse::class.simpleName!! + + override fun expectedFailureType(): String = FailedMCSMResponse::class.simpleName!! +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt index 01e283b..54ad6f3 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt @@ -1,4 +1,53 @@ package top.r3944realms.ltdmanager.mcms.request.instance -class StartInstanceRequest { -} \ No newline at end of file +import io.ktor.http.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.mcms.request.MCSMRequest +import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse +import top.r3944realms.ltdmanager.mcms.response.ResponseResult +import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse + +/** + * 启动实例请求 + * GET /api/protected_instance/open + */ +@Serializable +data class StartInstanceRequest( + val uuid: String, + val daemonId: String +) : MCSMRequest() { + + override fun toJSON(): String = + Json.encodeToString(this) + + override fun path(): String = + "protected_instance/open" + + override fun queryParameters(): Map = + mapOf( + "uuid" to uuid, + "daemonId" to daemonId + ) + + override fun method(): HttpMethod = HttpMethod.Get + + override fun getResponse( + responseJson: String, + httpStatusCode: HttpStatusCode + ): ResponseResult { + + return if (httpStatusCode.value == 200) { + val obj = Json.decodeFromString(StartInstanceResponse.serializer(), responseJson) + ResponseResult.Success(obj) + } else { + val fail = Json.decodeFromString(FailedMCSMResponse.serializer(), responseJson) + ResponseResult.Failure(fail) + } + } + + override fun expectedResponseType(): String = "StartInstanceResponse" + + override fun expectedFailureType(): String = "FailedMCSMResponse" +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt index 2c0a32e..43e23ca 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt @@ -1,14 +1,29 @@ -package top.r3944realms.ltdmanager.blessingskin.response +package top.r3944realms.ltdmanager.mcms.response import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonObject @Serializable -abstract class FailedBlessingSkinResponse: BlessingSkinResponse() { - abstract fun failedMessage(): String +open class FailedMCSMResponse( + @Transient + val status0: Status = Status.Ok, + val data: JsonObject? = null, + @Transient + val time0: Long = -1, +): MCSMResponse( + status0, time0 +) { @Serializable - class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() { - override fun failedMessage(): String = failedResult!! - - } + data class ExceptionFailedMCSMResponse( + @Transient + val status1: Status = Status.Ok, + val data0: String? = null, + @Transient + val time1: Long = -1, + @Transient + val result: String? = null, + ): FailedMCSMResponse( + status1, null, time1 + ) } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt index 1a048c5..d50df39 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt @@ -1,15 +1,20 @@ -package top.r3944realms.ltdmanager.blessingskin.response +package top.r3944realms.ltdmanager.mcms.response import io.ktor.http.* +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic -import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse +import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse +import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse + @Serializable -abstract class BlessingSkinResponse ( +abstract class MCSMResponse ( + open val status: Status, + open val time: Long, @Transient open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, @Transient @@ -17,20 +22,29 @@ abstract class BlessingSkinResponse ( ) { companion object { // 通用的反序列化方法 - inline fun decode(jsonString: String): T { + inline fun decode(jsonString: String): T { return json.decodeFromString(jsonString) } val json: Json by lazy { Json { ignoreUnknownKeys = true serializersModule = SerializersModule { - polymorphic(BlessingSkinResponse::class) { - subclass(FailedBlessingSkinResponse.Default::class, FailedBlessingSkinResponse.Default.serializer()) - subclass(InvitationCodeGenerationResponse::class, InvitationCodeGenerationResponse.serializer()) + polymorphic(MCSMResponse::class) { + subclass(GetInstanceListResponse::class, GetInstanceListResponse.serializer()) + subclass(StartInstanceResponse::class, StartInstanceResponse.serializer()) } } } } - + } + @Serializable + enum class Status(val value: String) { + @SerialName("200") Ok("200"), + @SerialName("400") ParamsNotRight("400"), + @SerialName("403") PermissionDenied("403"), + @SerialName("500") InternalServerError("500"); + companion object { + fun isOk(value: Status): Boolean = value == Ok + } } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt index 23a037a..7866b52 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt @@ -1,9 +1,9 @@ -package top.r3944realms.ltdmanager.blessingskin.response +package top.r3944realms.ltdmanager.mcms.response // 响应结果封装 -sealed class ResponseResult { - data class Success(val response: T) : ResponseResult() - data class Failure(val failure: F) : ResponseResult() +sealed class ResponseResult { + data class Success(val response: T) : ResponseResult() + data class Failure(val failure: F) : ResponseResult() /** * 检查是否成功 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt index 26961eb..9f1a76c 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt @@ -1,11 +1,60 @@ package top.r3944realms.ltdmanager.mcms.response.instance import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.JsonObject import top.r3944realms.ltdmanager.mcms.response.MCSMResponse @Serializable -data class InstanceListResponse( - val status: Int, - val data: InstanceListData?, - val time: Long -) : MCSMResponse \ No newline at end of file +data class GetInstanceListResponse( + @Transient + val status0: Status = Status.Ok, + val data: InstanceListData? = null, + @Transient + val time0: Long = -1, +) : MCSMResponse(status0, time0) { + @Serializable + data class InstanceListData( + val maxPage: Int, + val pageSize: Int, + val data: List + ) + + @Serializable + data class InstanceDetail( + val config: JsonObject? = null, //TODO: 不清楚是干什么的,需验证 + val info: InstanceInfo, + val instanceUuid: String, + val processInfo: ProcessInfo, + val space: Long, + val started: Int, + val status: Int + ) + + @Serializable + data class InstanceInfo( + val currentPlayers: Int, + val fileLock: Int, + val maxPlayers: Int, + val openFrpStatus: Boolean, + val playersChart: List, + val version: String + ) + + @Serializable + data class PlayerChartItem( + val time: Long? = null, + val players: Int? = null + ) + + @Serializable + data class ProcessInfo( + val cpu: Double, + val memory: Long, + val ppid: Long, + val pid: Long, + val ctime: Long, + val elapsed: Long, + val timestamp: Long + ) +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt index f689da2..e52f649 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt @@ -1,4 +1,21 @@ package top.r3944realms.ltdmanager.mcms.response.instance -class StartInstanceResponse { -} \ No newline at end of file +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import top.r3944realms.ltdmanager.mcms.response.MCSMResponse + +@Serializable +data class StartInstanceResponse( + @Transient + val status0: Status = Status.Ok, + val data: StartInstanceData, + @Transient + val time0: Long = -1 +) : MCSMResponse(status0, time0){ + @Serializable + data class StartInstanceData( + val instanceUuid: String + ) +} + + diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt index bdeb869..e9bb44c 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt @@ -1,4 +1,17 @@ package top.r3944realms.ltdmanager.module -class ApplyWhiteListModule { +class ApplyWhitelistModule( + moduleName: String, + private val groupMessagePollingModule: GroupMessagePollingModule, + private val cooldownMillis: Long = 120_000, + private val keywords: Set = setOf("申请白名单") +): + BaseModule(Modules.APPLY_WHITELIST,moduleName) { + override fun onLoad() { + TODO("Not yet implemented") + } + + override suspend fun onUnload() { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt index 30a3068..d762fd8 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/BanModule.kt @@ -35,7 +35,7 @@ class BanModule( private val maxBanMinutes: Int = 15, private val factorX: Int = 2, // 系数 x,禁言倍数 -) : BaseModule("BanModule", moduleName), PersistentState { +) : BaseModule(Modules.BAN, moduleName), PersistentState { private val banCommandParse = CommandParser(muteCommandPrefixList) private val pardonCommandParse = CommandParser(unmuteCommandPrefixList) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt index d66a051..dcb9af1 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/BaseModule.kt @@ -1,6 +1,7 @@ package top.r3944realms.ltdmanager.module import kotlinx.coroutines.CompletableDeferred +import org.intellij.lang.annotations.MagicConstant import top.r3944realms.ltdmanager.GlobalManager import top.r3944realms.ltdmanager.utils.LoggerUtil import kotlin.coroutines.cancellation.CancellationException diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt index 213bf45..80de5e2 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/DGLabModule.kt @@ -49,7 +49,7 @@ class DGLabModule( val adminIds: List = listOf(), val maxClientNumber: Int = 10, val commandHead: List = listOf("dglab"), -) : BaseModule("DGLabModule", moduleName), PersistentState { +) : BaseModule(Modules.DG_LAB, moduleName), PersistentState { var dgLabManager: DgLab? = null private var scope: CoroutineScope? = null diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt index f9d5780..d4857ac 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupMessagePollingModule.kt @@ -15,7 +15,7 @@ class GroupMessagePollingModule( val targetGroupId: Long, private val pollIntervalMillis: Long = 5_000L, private val msgHistoryCheck: Int = 15, -) : BaseModule("MessagePollingModule", moduleName) { +) : BaseModule(Modules.GROUP_MESSAGE_POLLING, moduleName) { private var scope: CoroutineScope? = null // 用 Flow 存消息,其他模块可以订阅 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt index 7989729..b0c8282 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt @@ -15,7 +15,7 @@ class GroupRequestHandlerModule( private val client: NapCatClient, private val targetGroupId: Long, private val pollIntervalMillis: Long = 30_000L, -) : BaseModule("GroupRequestHandlerModule", moduleName) { +) : BaseModule(Modules.GROUP_REQUEST_HANDLER, moduleName) { private var scope: CoroutineScope? = null diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt index 46c669d..c97f7a4 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt @@ -33,7 +33,7 @@ class HelpModule( private val selfNickName: String, private val keywords: List = listOf("help", "帮助"), private val cooldownMillis: Long = 30_000L -) : BaseModule("HelpModule", moduleName), PersistentState { +) : BaseModule(Modules.HELP, moduleName), PersistentState { // 命令解析器 private val commandParser = CommandParser(keywords) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt index d88bb7f..4f95f71 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt @@ -4,9 +4,10 @@ import kotlinx.coroutines.* import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest -import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse +import top.r3944realms.ltdmanager.core.client.response.ResponseResult import top.r3944realms.ltdmanager.core.mail.mail import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope @@ -77,7 +78,7 @@ class InvitationCodesModule( selfId: Long, private val cooldownMillis: Long = 120_000, private val keywords: Set = setOf("申请邀请码") -) : BaseModule("InvitationCodesModule", moduleName), PersistentState { +) : BaseModule(Modules.INVITATION_CODE, moduleName), PersistentState { private var scope: CoroutineScope? = null private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name) @@ -430,23 +431,33 @@ class InvitationCodesModule( /** * 1. 创建邀请码 */ - private suspend fun createInvitationCodes(amount: Int): List? { + private suspend fun createInvitationCodes(amount: Int): List? { return try { val response = blessingSkinClient.submitRequest( GenerateInvitationCodeRequest(amount = amount, token = apiToken) ) + response + .onFailure { + } + .onSuccess { + + } when (response) { is ResponseResult.Success -> { - if (response.response.success) { - response.response.data + if (response.response is InvitationCodeGenerationResponse) { + if (response.response.success) { + response.response.data + } else + LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}") + null } else { - LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}") + LoggerUtil.logger.warn("[$name] 返回非预期对象类型: ${response.response.javaClass}") null } } is ResponseResult.Failure -> { - LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedResult}") + LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedMessage}") null } } @@ -460,7 +471,7 @@ class InvitationCodesModule( * 2. 验证数量匹配 */ private fun validateCodeCountMatch( - invitationCodes: List?, + invitationCodes: List?, needNewTokenIdAndMsgPairs: List> ) { if (invitationCodes == null) { diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/MailModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/MailModule.kt index 78291fb..e9c1831 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/MailModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/MailModule.kt @@ -19,7 +19,7 @@ class MailModule( private val enableAuth: Boolean = true, private val enableTLS: Boolean = true, private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s) -) : BaseModule("MailModule", moduleName) { +) : BaseModule(Modules.MAIL, moduleName) { private lateinit var session: Session private val queue = LinkedBlockingQueue() // 邮件队列 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt index 60454d0..d743e72 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt @@ -36,7 +36,7 @@ class McServerStatusModule( setOf("hp", "hypixel") to "mc.hypixel.net", setOf("pm", "mineplex") to "play.mineplex.com" ) -) : BaseModule("McServerStatusModule", moduleName), PersistentState { +) : BaseModule(Modules.MC_SERVER_STATUS, moduleName), PersistentState { private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name) private val stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name) private val commandParser: CommandParser = CommandParser(commands) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt index 1855b6a..400974e 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModGroupHandlerModule.kt @@ -31,7 +31,7 @@ class ModGroupHandlerModule( private val targetGroupId: Long, private val answers: List = listOf("正确答案"), private val pollIntervalMillis: Long = 30_000L -) : BaseModule("ModGroupHandlerModule", moduleName), PersistentState { +) : BaseModule(Modules.MOD_GROUP_HANDLER, moduleName), PersistentState { private var scope: CoroutineScope? = null private val stateFile: File = getStateFileInternal("reject_records.json", name) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt index 1d1323e..7ef961a 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt @@ -1,4 +1,26 @@ package top.r3944realms.ltdmanager.module +import java.util.* + object Modules { + private val MODULES: MutableList = LinkedList(); + val BAN: String = register("BanModule") + val APPLY_WHITELIST: String = register("ApplyWhitelistModule") + val DG_LAB: String = register("DGLabModule") + val GROUP_MESSAGE_POLLING: String = register("GroupMessagePollingModule") + val GROUP_REQUEST_HANDLER: String = register("GroupRequestHandlerModule") + val HELP: String = register("HelpModule") + val MAIL: String = register("MailModule") + val MC_SERVER_STATUS: String = register("MCServerStatusModule") + val MOD_GROUP_HANDLER: String = register("ModGroupHandlerModule") + val RCON_PLAYER_LIST: String = register("RconPlayerListModule") + val INVITATION_CODE: String = register("InvitationCodeModule") + val STATE: String = register("StateModule") + fun register(name: String): String { + MODULES.add(name) + return name + } + fun getModules(): Array { + return MODULES.toTypedArray(); + } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt index f8c8e6f..da2304b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconPlayerListModule.kt @@ -35,7 +35,7 @@ class RconPlayerListModule( private val rconPath: String, private val rconConfigPath: String, private val keywords: Set = setOf("查看玩家列表", "玩家列表", "在线玩家") -) : BaseModule("RconPlayerListModule", moduleName), PersistentState { +) : BaseModule(Modules.RCON_PLAYER_LIST, moduleName), PersistentState { private val cooldownManager by lazy { CooldownManager( cooldownMillis = cooldownMillis, diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt index 32dc7db..2e7c397 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/StateModule.kt @@ -8,7 +8,7 @@ class StateModule( moduleName: String, private val onlineName: String, private val offlineName: String, -): BaseModule("StateModule", moduleName) { +): BaseModule(Modules.STATE, moduleName) { private var scope: CoroutineScope? = null override fun onLoad() { scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt index 4b68488..d4cad9a 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt @@ -1,7 +1,17 @@ package top.r3944realms.ltdmanager.module.exception -class InvalidConfigException: Exception() { - enum class Type(template: String) { - +class ConfigError(type: Type = Type.OTHER, private val pos: String, vararg args: Any) : Exception() { + private val errorType: Type = type + private val arguments = args + + override val message: String + get() = String.format(errorType.template, *arguments, pos) + + + enum class Type(val template: String) { + INVALID_PARAMETER("Invalid Parameter: %s in %s."), + MISSING_PARAMETER("Missing Parameter: %s in %s."), + NOT_EXPECTED_VALUE("Expect for %s but was %s in %s."), + OTHER("%s in %s") } } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/utils/ConfigInitializer.kt b/src/main/kotlin/top/r3944realms/ltdmanager/utils/ConfigInitializer.kt index 2d4e89d..5e8ddb0 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/utils/ConfigInitializer.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/utils/ConfigInitializer.kt @@ -13,7 +13,7 @@ object ConfigInitializer { * @param fileName YAML 文件名,如 application.yml * @param configDir 配置目录,如 config */ - fun initConfig(fileName: String = "application.yml", configDir: String = "config") { + fun initConfig(fileName: String = "application.yml", configDir: String = "config", shouldExit: Boolean = true) { val dirPath = Paths.get(configDir) if (!Files.exists(dirPath)) { Files.createDirectories(dirPath) @@ -28,8 +28,10 @@ object ConfigInitializer { if (resourceStream != null) { Files.copy(resourceStream, filePath, StandardCopyOption.REPLACE_EXISTING) LoggerUtil.logger.info("已生成默认配置文件: $filePath") - LoggerUtil.logger.info("第一次启动,请修改配置后再启动") - exitProcess(-1); + if (shouldExit) { + LoggerUtil.logger.info("第一次启动,请修改配置后再启动") + exitProcess(-1); + } } else throw Error("Jar内部资源文件缺失") } else { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 106c0ff..9bd99c7 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -20,6 +20,8 @@ websocket: url: "wss://127.0.0.1:3002" # 格式为 ENC(XXX),若不是则会在加载完成配置后自动加密 encrypted-token: "123123cc" +mcsm: + tools: rcon: mc-rcon-tool-path: "/path/to/rcon"