refactor: 代码调整

This commit is contained in:
叁玖领域 2026-02-02 12:48:55 +08:00
parent 4da8263b45
commit 37eeaf143c
63 changed files with 1383 additions and 583 deletions

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@47.116.125.76" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201"> <data-source source="LOCAL" name="@110.42.70.155" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201">
<driver-ref>mysql.8</driver-ref> <driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver> <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://47.116.125.76:3308</jdbc-url> <jdbc-url>jdbc:mysql://110.42.70.155:3306</jdbc-url>
<jdbc-additional-properties> <jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" /> <property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" /> <property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />

View File

@ -31,6 +31,10 @@ repositories {
maven { maven {
url = uri("https://repo.glaremasters.me/repository/public/") 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 //TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
dependencies { dependencies {
@ -77,7 +81,7 @@ repositories {
//DG_Lab 依赖库导入 //DG_Lab 依赖库导入
implementation("io.netty:netty-all:4.1.109.Final") implementation("io.netty:netty-all:4.1.109.Final")
implementation("com.google.code.gson:gson:2.10.1") 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,)") implementation("com.google.zxing:core:[3.5.3,)")

View File

@ -4,4 +4,4 @@ org.gradle.parallel=true
org.gradle.degree_of_parallelism=16 org.gradle.degree_of_parallelism=16
project_group=top.r3944realms.ltdmanager project_group=top.r3944realms.ltdmanager
project_version=1.14-SNAPSHOT project_version=1.14-SNAPSHOT
dg_lab_version=4.3.13.18 dg_lab_version=4.4.14.18

View File

@ -4,6 +4,7 @@ import kotlinx.coroutines.*
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
import top.r3944realms.ltdmanager.chevereto.CheveretoClient import top.r3944realms.ltdmanager.chevereto.CheveretoClient
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
import top.r3944realms.ltdmanager.mcms.MCSMClient
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
import top.r3944realms.ltdmanager.module.ModuleManager import top.r3944realms.ltdmanager.module.ModuleManager
import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.NapCatClient
@ -20,6 +21,10 @@ object GlobalManager {
MysqlHikariConnectPool() MysqlHikariConnectPool()
} }
fun initApplication() {
}
// NapCat 客户端 // NapCat 客户端
val napCatClient: NapCatClient by lazy { val napCatClient: NapCatClient by lazy {
NapCatClient.create() NapCatClient.create()
@ -33,6 +38,9 @@ object GlobalManager {
val cheveretoClient: CheveretoClient by lazy { val cheveretoClient: CheveretoClient by lazy {
CheveretoClient.create() CheveretoClient.create()
} }
val mcsmClient: MCSMClient by lazy {
MCSMClient.create()
}
val moduleManager: ModuleManager by lazy { ModuleManager() } val moduleManager: ModuleManager by lazy { ModuleManager() }
@ -72,7 +80,8 @@ object GlobalManager {
"McSrvStatusClient" to { mcSrvStatusClient.close() }, "McSrvStatusClient" to { mcSrvStatusClient.close() },
"BlessingSkinClient" to { blessingSkinClient.close() }, "BlessingSkinClient" to { blessingSkinClient.close() },
"Hikari 数据源" to { dataSource.close() }, "Hikari 数据源" to { dataSource.close() },
"CheveretoClient" to { cheveretoClient.close() } "CheveretoClient" to { cheveretoClient.close() },
"McsmClient" to { mcsmClient.close() },
) )
resources.forEach { (name, closer) -> resources.forEach { (name, closer) ->
@ -99,4 +108,5 @@ object GlobalManager {
isRunning.set(false) isRunning.set(false)
} }
} }

View File

@ -1,27 +1,24 @@
package top.r3944realms.ltdmanager.blessingskin package top.r3944realms.ltdmanager.blessingskin
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.request.* import kotlinx.coroutines.CompletableDeferred
import io.ktor.http.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore 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.request.BlessingSkinRequest
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse 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.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.utils.Environment
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.net.URLEncoder
import java.util.* import java.util.*
class BlessingSkinClient private constructor() : AutoCloseable { class BlessingSkinClient private constructor() : IClient<BlessingSkinRequest, BlessingSkinQueueItem, BlessingSkinResponse, FailedBlessingSkinResponse> {
private val client = HttpClient(CIO) { private val client = HttpClient(CIO) {
expectSuccess = false expectSuccess = false
@ -40,170 +37,40 @@ class BlessingSkinClient private constructor() : AutoCloseable {
// 限流控制 // 限流控制
private val semaphore = Semaphore(5) private val semaphore = Semaphore(5)
private val requestMutex = Mutex() private val requestMutex = Mutex()
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority }) private val requestQueue = PriorityQueue<BlessingSkinQueueItem>(compareBy { it.priority })
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init { init {
startQueueProcessor() init()
} }
/** override fun getBaseUrl(): String = blessingSkinServerConfig.url!!
* 提交请求
*/ override fun getType(): String = "BlessingSkinClient"
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest(
request: BlessingSkinRequest<T, F>, override fun getClient(): HttpClient = client
priority: Int = 5,
maxRetries: Int = 3 override fun getSemaphore(): Semaphore = semaphore
): ResponseResult<T, F> {
val deferred = CompletableDeferred<ResponseResult<T, F>>() override fun getRequestMutex(): Mutex = requestMutex
requestMutex.withLock {
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true)) override fun getResponseQueue(): PriorityQueue<BlessingSkinQueueItem> = requestQueue
}
return deferred.await() override fun getScope(): CoroutineScope = scope
override fun createFailureResponse(exception: Exception?): IFailedResponse {
return FailedBlessingSkinResponse.Default(exception?.stackTraceToString()?:"ERROR")
} }
/** override fun addToQueue(
* 启动队列处理器 request: BlessingSkinRequest,
*/ deferredC: CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>,
private fun startQueueProcessor() { priority: Int,
scope.launch { maxRetries: Int
while (isActive) { ): BlessingSkinQueueItem {
val item = requestMutex.withLock { val element = BlessingSkinQueueItem(request, deferredC, priority, maxRetries, false)
requestQueue.poll() requestQueue.add(element)
} return element
if (item == null) {
delay(50)
continue
}
processQueueItem(item)
}
}
}
/**
* 处理队列项
*/
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) {
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<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).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<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).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("<!DOCTYPE html>", ignoreCase = true) ||
text.contains("<html>", 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()
}
} }
companion object { companion object {

View File

@ -4,13 +4,14 @@ import kotlinx.coroutines.CompletableDeferred
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
import top.r3944realms.ltdmanager.core.client.QueueItem
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>( data class BlessingSkinQueueItem (
val request: BlessingSkinRequest<T,F>, val request0: BlessingSkinRequest,
val deferred: CompletableDeferred<*>, val deferred0: CompletableDeferred<*>,
var retries: Int, val priority0: Int,
val priority: Int, var retries0: Int,
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit val expectsResponse0: Boolean
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> { ) : QueueItem<BlessingSkinRequest, BlessingSkinResponse, FailedBlessingSkinResponse> (
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority) request0, deferred0, retries0, priority0, expectsResponse0
} )

View File

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

View File

@ -1,79 +1,13 @@
package top.r3944realms.ltdmanager.blessingskin.request package top.r3944realms.ltdmanager.blessingskin.request
import io.ktor.http.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult import top.r3944realms.ltdmanager.core.client.request.IRequest
@Serializable @Serializable
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>( abstract class BlessingSkinRequest(
@Transient @Transient
open val createTime: Long = System.currentTimeMillis() override val createTime: Long = System.currentTimeMillis()
) { ): IRequest<BlessingSkinResponse, FailedBlessingSkinResponse>
/**
* 转换为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<String, String> = emptyMap()
/**
* 获取请求体参数用于POST请求的JSON body
* 例如: mapOf("token" to "abc123", "amount" to 1)
*/
open fun bodyParameters(): Map<String, Any> = 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<T, F>
/**
* 获取预期的成功响应类型名称用于日志和调试
*/
abstract fun expectedResponseType(): String
/**
* 获取预期的失败响应类型名称用于日志和调试
*/
abstract fun expectedFailureType(): String
/**
* 是否需要在失败时重试默认重试
*/
open fun shouldRetryOnFailure(): Boolean = true
}

View File

@ -6,8 +6,8 @@ import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse 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.blessingskin.response.invitecode.InvitationCodeGenerationResponse
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import java.util.* import java.util.*
@ -17,9 +17,7 @@ class GenerateInvitationCodeRequest(
val token: String? = null, val token: String? = null,
@Transient @Transient
val amount: Int? = 1, val amount: Int? = 1,
@Transient ) : BlessingSkinRequest() {
override val createTime: Long = System.currentTimeMillis()
) : BlessingSkinRequest<InvitationCodeGenerationResponse, FailedBlessingSkinResponse.Default>() {
override fun toJSON(): String { override fun toJSON(): String {
// 对于GET请求参数在URL中body可以为空 // 对于GET请求参数在URL中body可以为空
@ -66,7 +64,7 @@ class GenerateInvitationCodeRequest(
} catch (e: Exception) { } catch (e: Exception) {
ResponseResult.Failure( ResponseResult.Failure(
FailedBlessingSkinResponse.Default( FailedBlessingSkinResponse.Default(
failedResult = "解析响应失败: ${e.message}" failedMessage = "解析响应失败: ${e.message}"
) )
) )
} }

View File

@ -7,14 +7,15 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.polymorphic
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
import top.r3944realms.ltdmanager.core.client.response.IResponse
@Serializable @Serializable
abstract class BlessingSkinResponse ( abstract class BlessingSkinResponse (
@Transient @Transient
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
@Transient @Transient
open val createTime: Long = System.currentTimeMillis() override val createTime: Long = System.currentTimeMillis()
) { ) : IResponse {
companion object { companion object {
// 通用的反序列化方法 // 通用的反序列化方法
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T { inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {

View File

@ -2,13 +2,12 @@ package top.r3944realms.ltdmanager.blessingskin.response
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
@Serializable @Serializable
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() { abstract class FailedBlessingSkinResponse: BlessingSkinResponse(), IFailedResponse {
abstract fun failedMessage(): String
@Serializable @Serializable
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() { class Default(@Transient override val failedMessage: String = "未知错误") : FailedBlessingSkinResponse() {
override fun failedMessage(): String = failedResult!!
} }
} }

View File

@ -1,7 +1,7 @@
package top.r3944realms.ltdmanager.blessingskin.response.invitecode package top.r3944realms.ltdmanager.blessingskin.response.invitecode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
@Serializable @Serializable
data class InvitationCodeGenerationResponse( data class InvitationCodeGenerationResponse(
@ -10,12 +10,4 @@ data class InvitationCodeGenerationResponse(
val data: List<InvitationCode>? = null val data: List<InvitationCode>? = null
) : BlessingSkinResponse() { ) : BlessingSkinResponse() {
@Serializable
data class InvitationCode(
val code: String,
@SerialName("generated_at")
val generatedAt: String,
@SerialName("expires_at")
val expiresAt: String
)
} }

View File

@ -3,65 +3,157 @@ package top.r3944realms.ltdmanager.chevereto
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.engine.cio.* 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.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.serialization.json.Json import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource
import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse 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.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.utils.Environment
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.Closeable
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayDeque
class CheveretoClient private constructor() :
class CheveretoClient private constructor() : Closeable { IClient<CheveretoRequest, CheveretoQueueItem, CheveretoResponse, FailedCheveretoResponse> {
private val client = HttpClient(CIO) { private val client = HttpClient(CIO) {
install(ContentNegotiation) { expectSuccess = false
json(Json { ignoreUnknownKeys = true }) // 安装 HttpTimeout 插件
install(HttpTimeout) {
// 默认超时配置,会被具体请求的配置覆盖
requestTimeoutMillis = 30000
connectTimeoutMillis = 10000
socketTimeoutMillis = 15000
} }
} }
private val imgTuConfig = YamlConfigLoader.loadTuImgConfig() private val imgTuConfig = YamlConfigLoader.loadTuImgConfig()
private val apiUrl = imgTuConfig.url!! private val baseUrl = imgTuConfig.url!!.removeSuffix("/")
private val apiKey = imgTuConfig.decryptedPassword!! private val apiKey = imgTuConfig.decryptedPassword!!
// 限流,同时最多 3 个上传
private val semaphore = Semaphore(3) private val semaphore = Semaphore(3)
private val queue = PriorityQueue<CheveretoQueueItem>()
// 普通队列 (按 priority 排序)
private val queue = PriorityQueue<CheveretoQueueItem<CheveretoResponse>>(compareBy { it.priority })
private val queueMutex = Mutex() private val queueMutex = Mutex()
// 紧急队列 (FIFO最多 10 个)
private val urgentQueue = ArrayDeque<CheveretoQueueItem<CheveretoResponse>>(10)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init { init {
scope.launch { init()
while (isActive) { }
val item = queueMutex.withLock {
when { override fun getType(): String = "CheveretoClient"
urgentQueue.isNotEmpty() -> urgentQueue.removeFirst()
queue.isNotEmpty() -> queue.poll() override fun getClient(): HttpClient = client
else -> null
override fun getSemaphore(): Semaphore = semaphore
override fun getRequestMutex(): Mutex = queueMutex
override fun getResponseQueue(): PriorityQueue<CheveretoQueueItem> = 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<ResponseResult<CheveretoResponse, FailedCheveretoResponse>>,
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<ResponseResult<IResponse, IFailedResponse>>).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<ResponseResult<IResponse, IFailedResponse>>).complete(
ResponseResult.Failure(errorResponse)
)
} }
} }
} }
/** /**
* 上传 File * 上传 File
*/ */
@ -77,35 +169,24 @@ class CheveretoClient private constructor() : Closeable {
nsfw: Int? = null, nsfw: Int? = null,
format: String = "json", format: String = "json",
useFileDate: Int? = null, useFileDate: Int? = null,
priority: Int = 5 priority: Int = 5,
maxRetries: Int = 3
): CheveretoResponse { ): CheveretoResponse {
val deferred = CompletableDeferred<CheveretoResponse>() upload(CheveretoUploadRequest(
val source = suspend { source = CheveretoSource.ByteArraySource(file.readBytes(), file.name),
safeUpload { format = format,
submitFormWithBinaryData( title = title,
url = apiUrl, description = description,
formData = formData { tags = tags,
append("source", file.readBytes(), Headers.build { albumId = albumId,
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"") categoryId = categoryId,
}) width = width,
append("format", format) expiration = expiration,
title?.let { append("title", it) } nsfw = nsfw,
description?.let { append("description", it) } useFileDate = useFileDate
tags?.let { append("tags", it) } ), priority, maxRetries).getRetResponse()
albumId?.let { append("album_id", it) } throw Exception("Never Reach")
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()
} }
@ -125,36 +206,23 @@ class CheveretoClient private constructor() : Closeable {
nsfw: Int? = null, nsfw: Int? = null,
format: String = "json", format: String = "json",
useFileDate: Int? = null, useFileDate: Int? = null,
priority: Int = 5 priority: Int = 5,
maxRetries: Int = 3
): CheveretoResponse { ): CheveretoResponse {
val deferred = CompletableDeferred<CheveretoResponse>() upload(CheveretoUploadRequest(
val source = suspend { source = CheveretoSource.ByteArraySource(inputStream.readBytes(), fileName),
val bytes = inputStream.readBytes() format = format,
safeUpload { title = title,
submitFormWithBinaryData( description = description,
url = apiUrl, tags = tags,
formData = formData { albumId = albumId,
append("source", bytes, Headers.build { categoryId = categoryId,
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"") width = width,
}) expiration = expiration,
append("format", format) nsfw = nsfw,
title?.let { append("title", it) } useFileDate = useFileDate
description?.let { append("description", it) } ), priority, maxRetries).getRetResponse()
tags?.let { append("tags", it) } throw Exception("Never Reach")
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()
} }
/** /**
@ -172,64 +240,41 @@ class CheveretoClient private constructor() : Closeable {
nsfw: Int? = null, nsfw: Int? = null,
format: String = "json", format: String = "json",
useFileDate: Int? = null, useFileDate: Int? = null,
priority: Int = 5 priority: Int = 5,
maxRetries: Int = 3
): CheveretoResponse { ): CheveretoResponse {
val deferred = CompletableDeferred<CheveretoResponse>() upload(CheveretoUploadRequest(
val source = suspend { source = CheveretoSource.UrlSource(url),
safeUpload { format = format,
submitForm( title = title,
url = apiUrl, description = description,
formParameters = Parameters.build { tags = tags,
append("source", url) albumId = albumId,
append("format", format) categoryId = categoryId,
title?.let { append("title", it) } width = width,
description?.let { append("description", it) } expiration = expiration,
tags?.let { append("tags", it) } nsfw = nsfw,
albumId?.let { append("album_id", it) } useFileDate = useFileDate
categoryId?.let { append("category_id", it) } ), priority, maxRetries).getRetResponse()
width?.let { append("width", it.toString()) } throw Exception("Never Reach")
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()
} }
private suspend fun processItem(item: CheveretoQueueItem<CheveretoResponse>) { suspend fun upload(
semaphore.withPermit { request: CheveretoUploadRequest, priority: Int, maxRetries: Int
try { ): ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse> {
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()
return try { return try {
response.body() @Suppress("UNCHECKED_CAST")
submitRequest(request, priority, maxRetries) as ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse>
} catch (e: Exception) { } catch (e: Exception) {
val raw = response.bodyAsText() ResponseResult.Failure(
throw RuntimeException("Upload failed (status=${response.status}): $raw", e) FailedCheveretoResponse.Default(
httpStatusCode = HttpStatusCode.InternalServerError,
failedMessage = "Byte array upload failed: ${e.message}"
)
)
} }
} }
override fun close() {
scope.cancel()
runBlocking { client.close() }
}
companion object { companion object {
fun create(): CheveretoClient = CheveretoClient() fun create(): CheveretoClient = CheveretoClient()
} }

View File

@ -1,9 +1,17 @@
package top.r3944realms.ltdmanager.chevereto package top.r3944realms.ltdmanager.chevereto
import kotlinx.coroutines.CompletableDeferred 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<T>( data class CheveretoQueueItem(
val source: suspend () -> T, val request0: CheveretoRequest,
val deferred: CompletableDeferred<T>, val deferred0: CompletableDeferred<*>,
val priority: Int = 5 val priority0: Int,
var retries0: Int,
val expectsResponse0: Boolean
) : QueueItem<CheveretoRequest, CheveretoResponse, FailedCheveretoResponse>(
request0, deferred0, retries0, priority0, expectsResponse0
) )

View File

@ -1,4 +1,11 @@
package top.r3944realms.ltdmanager.chevereto.data 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()
} }

View File

@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager.chevereto.data
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Success( data class SuccessInfo(
val message : String? = null, val message : String? = null,
val code: Int? = 200, val code: Int? = 200,
) )

View File

@ -1,4 +1,13 @@
package top.r3944realms.ltdmanager.chevereto.request package top.r3944realms.ltdmanager.chevereto.request
class CheveretoRequest { 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<CheveretoResponse, FailedCheveretoResponse>

View File

@ -1,4 +1,89 @@
package top.r3944realms.ltdmanager.chevereto.request.v1 package top.r3944realms.ltdmanager.chevereto.request.v1
class CheveretoUploadRequest { 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<String, Any> {
val params = mutableMapOf<String, Any>()
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<CheveretoUploadResponse, FailedCheveretoResponse> {
return try {
if (httpStatusCode.isSuccess()) {
val successResponse = Json.decodeFromString<CheveretoUploadResponse>(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
}

View File

@ -1,4 +1,37 @@
package top.r3944realms.ltdmanager.chevereto.response 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 <reified T : CheveretoResponse> 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())
}
}
}
}
}
} }

View File

@ -1,4 +1,12 @@
package top.r3944realms.ltdmanager.chevereto.response 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()
} }

View File

@ -1,4 +1,17 @@
package top.r3944realms.ltdmanager.chevereto.response.v1 package top.r3944realms.ltdmanager.chevereto.response.v1
class CheveretoUploadResponse { 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()

View File

@ -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<R: IRequest<T, F>, Q: QueueItem<R, T, F>, T: IResponse, F: IFailedResponse> : AutoCloseable {
fun getType(): String
fun getClient(): HttpClient
fun getSemaphore(): Semaphore
fun getRequestMutex(): Mutex
fun getResponseQueue(): PriorityQueue<Q>
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<ResponseResult<T, F>>,
priority: Int = 5,
maxRetries: Int = 3): Q
/**
* 提交请求
*/
suspend fun submitRequest(
request: R,
priority: Int = 5,
maxRetries: Int = 3
): ResponseResult<T, F> {
val deferred = CompletableDeferred<ResponseResult<T, F>>()
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<ResponseResult<IResponse, IFailedResponse>>).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<ResponseResult<IResponse, IFailedResponse>>).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("<!DOCTYPE html>", ignoreCase = true) ||
text.contains("<html>", ignoreCase = true) ||
text.contains("Redirecting", ignoreCase = true)
}
override fun close() {
getScope().cancel()
runBlocking {
getClient().close()
}
}
} }

View File

@ -1,19 +1,26 @@
package top.r3944realms.ltdmanager.core.client package top.r3944realms.ltdmanager.core.client
import kotlinx.coroutines.CompletableDeferred
import top.r3944realms.ltdmanager.core.client.request.IRequest import top.r3944realms.ltdmanager.core.client.request.IRequest
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
import top.r3944realms.ltdmanager.core.client.response.IResponse import top.r3944realms.ltdmanager.core.client.response.IResponse
import java.util.concurrent.CompletableFuture
interface IQueueItem<out T:IResponse, out F:IFailedResponse> : Comparable<IQueueItem<@UnsafeVariance T, @UnsafeVariance F>> { open class QueueItem<R: IRequest<T, F>, T:IResponse, F:IFailedResponse>(
fun getRequest(): IRequest<T,F> val request: R,
fun getDeferred(): CompletableFuture<*> val deferred: CompletableDeferred<*>,
fun getRetries(): Int val retries: Int,
fun getPriority(): Int val priority: Int,
val expectsResponse: Boolean
) : Comparable<QueueItem<R, T, F>> {
// fun getRequest(): R = request
// fun getDeferred(): CompletableDeferred<*> = deferred
// fun getRetries(): Int = retries
// fun getPriority(): Int = priority
/** /**
* @return true 表示返回 BlessingSkinResponse, false 表示 Unit * @return true 表示返回 BlessingSkinResponse, false 表示 Unit
*/ */
fun expectsResponse(): Boolean fun expectsResponse(): Boolean = expectsResponse
override fun compareTo(other: IQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = getPriority().compareTo(other.getPriority()) override fun compareTo(other: QueueItem<R, @UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
} }

View File

@ -1,4 +1,76 @@
package top.r3944realms.ltdmanager.core.client.request 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<out T: IResponse, out F: IFailedResponse> {
// 只使用属性
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<String, String> = emptyMap()
/**
* 获取请求体参数用于POST请求的JSON body
* 例如: mapOf("token" to "abc123", "amount" to 1)
*/
fun bodyParameters(): Map<String, Any> = emptyMap()
/**
* 获取请求体内容类型默认为Application.Json
*/
fun contentType(): ContentType = ContentType.Application.Json
/**
* 解析响应JSON字符串
* @param responseJson 响应JSON字符串
* @param httpStatusCode HTTP状态码
*/
fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult<T, F>
/**
* 获取预期的成功响应类型名称用于日志和调试
*/
fun expectedResponseType(): String
/**
* 获取预期的失败响应类型名称用于日志和调试
*/
fun expectedFailureType(): String
/**
* 是否需要在失败时重试默认重试
*/
fun shouldRetryOnFailure(): Boolean = true
} }

View File

@ -1,4 +1,7 @@
package top.r3944realms.ltdmanager.core.client.response package top.r3944realms.ltdmanager.core.client.response
class IFailedResponse { interface IFailedResponse : IResponse {
val failedMessage: String
val thrownException: Exception
get() = Exception(failedMessage)
} }

View File

@ -1,4 +1,8 @@
package top.r3944realms.ltdmanager.core.client.response package top.r3944realms.ltdmanager.core.client.response
import io.ktor.http.*
interface IResponse { interface IResponse {
val httpStatusCode: HttpStatusCode
val createTime: Long
} }

View File

@ -1,4 +1,47 @@
package top.r3944realms.ltdmanager.core.client.response package top.r3944realms.ltdmanager.core.client.response
class ResponseResult { sealed class ResponseResult<out T: IResponse, out F: IFailedResponse> {
data class Success<T : IResponse>(val response: T) : ResponseResult<T, Nothing>()
data class Failure<F : IFailedResponse>(val failure: F) : ResponseResult<Nothing, F>()
/**
* 检查是否成功
*/
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<T, F> {
if (this is Success) action(response)
return this
}
/**
* 失败时执行操作
*/
inline fun onFailure(action: (F) -> Unit): ResponseResult<T, F> {
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")
}
} }

View File

@ -1,4 +1,54 @@
package top.r3944realms.ltdmanager.core.config 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=***)"
}
} }

View File

@ -31,6 +31,7 @@ object YamlConfigLoader {
config?.database?.encryptPassword() config?.database?.encryptPassword()
config?.websocket?.encryptToken() config?.websocket?.encryptToken()
config?.http?.encryptToken() config?.http?.encryptToken()
config?.mcsm?.encryptApi()
config?.mail?.encryptPassword() config?.mail?.encryptPassword()
config?.tools?.rcon?.encryptPassword() config?.tools?.rcon?.encryptPassword()
config?.blessingSkinServer?.invitationApi?.encryptToken() config?.blessingSkinServer?.invitationApi?.encryptToken()
@ -72,6 +73,7 @@ object YamlConfigLoader {
fun loadDatabaseConfig(): DatabaseConfig = config.database fun loadDatabaseConfig(): DatabaseConfig = config.database
fun loadCryptoConfig(): CryptoConfig = config.crypto fun loadCryptoConfig(): CryptoConfig = config.crypto
fun loadMcsmConfig(): McsmConfig = config.mcsm
fun loadWebsocketConfig(): WebsocketConfig = config.websocket fun loadWebsocketConfig(): WebsocketConfig = config.websocket
fun loadHttpConfig(): HttpConfig = config.http fun loadHttpConfig(): HttpConfig = config.http
fun loadModeConfig(): ModeConfig = config.mode fun loadModeConfig(): ModeConfig = config.mode
@ -88,6 +90,7 @@ object YamlConfigLoader {
var http: HttpConfig = HttpConfig(), var http: HttpConfig = HttpConfig(),
var tools: ToolConfig = ToolConfig(), var tools: ToolConfig = ToolConfig(),
var mail: MailConfig = MailConfig(), var mail: MailConfig = MailConfig(),
var mcsm: McsmConfig = McsmConfig(),
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(), var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
var dgLab: DgLabConfig = DgLabConfig(), var dgLab: DgLabConfig = DgLabConfig(),
var imgTu: ImgTuConfig = ImgTuConfig(), var imgTu: ImgTuConfig = ImgTuConfig(),

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.core.init package top.r3944realms.ltdmanager.core.init
class DependencyResolver { class DependencyResolver() {
} }

View File

@ -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( data class ModuleConfig(
val name: String, val name: String,
val type: ModuleType, val type: ModuleType,
val enabled: Boolean, val enabled: Boolean,
val val dependencies: List<Dependency> = emptyList(),
val config: Map<String, Any> = emptyMap()
) { ) {
data class Dependency( data class Dependency(
val moduleName: String, // 依赖的模块名称 private val name: String,
val type: DependencyType, // 依赖类型 val type: ModuleType,
val required: Boolean = true // 是否必需 val required: Boolean = true
) ) {
enum class ModuleType { private val dependencyName: String = "${type.modName}-$name"
GROUP_MESSAGE_POLLING_MODULE,
GROUP_REQUEST_HANDLER_MODULE, fun getDepName() :String = dependencyName
MAIL_MODULE,
BAN_MODULE, }
DG_LAB_MODULE, enum class ModuleType(val modName: String) {
INVITE_MODULE, GROUP_MESSAGE_POLLING_MODULE(Modules.GROUP_MESSAGE_POLLING),
MC_SERVER_STATUS_MODULE, GROUP_REQUEST_HANDLER_MODULE(Modules.GROUP_REQUEST_HANDLER),
RCON_PLAYER_LIST_MODULE, MAIL_MODULE(Modules.MAIL),
STATE_MODULE 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 <reified T> 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<T>(value, paramName)
}
}
}
// 特定类型方法(向后兼容)
fun long(paramName: String): Long = get<Long>(paramName)
fun int(paramName: String): Int = get<Int>(paramName)
fun string(paramName: String): String = get<String>(paramName)
fun boolean(paramName: String): Boolean = get<Boolean>(paramName)
fun double(paramName: String): Double = get<Double>(paramName)
fun float(paramName: String): Float = get<Float>(paramName)
// 可选值方法
inline fun <reified T> 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 <reified T> getOrDefault(paramName: String, defaultValue: T): T =
getOrNull<T>(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<Long>(value, paramName)
}
else -> throw typeMismatchError<Long>(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<Int>(value, paramName)
}
else -> throw typeMismatchError<Int>(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<Boolean>(value, paramName)
}
is Number -> value.toInt() != 0
else -> throw typeMismatchError<Boolean>(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<Double>(value, paramName)
}
else -> throw typeMismatchError<Double>(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<Float>(value, paramName)
}
else -> throw typeMismatchError<Float>(value, paramName)
}
// 错误处理辅助方法
private inline fun <reified T> 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
)
} }
} }

View File

@ -1,4 +1,32 @@
package top.r3944realms.ltdmanager.core.init 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 { 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
)
}
} }

View File

@ -1,4 +1,10 @@
package top.r3944realms.ltdmanager.core.init package top.r3944realms.ltdmanager.core.init
import java.nio.file.Paths
object ModuleLoader { object ModuleLoader {
val configFilePath = Paths.get("config/modules.yaml")
init {
}
} }

View File

@ -1,4 +1,15 @@
package top.r3944realms.ltdmanager.core.init package top.r3944realms.ltdmanager.core.init
import top.r3944realms.ltdmanager.module.BaseModule
object ModuleRegistry { object ModuleRegistry {
private val registry: MutableMap<String, BaseModule> = mutableMapOf()
fun register(baseModule: BaseModule) {
registry.putIfAbsent(baseModule.name, baseModule)
}
fun get(moduleName: String): BaseModule? {
return registry[moduleName]
}
} }

View File

@ -8,6 +8,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import top.r3944realms.ltdmanager.GlobalManager 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.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.napcat.NapCatClient import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID import top.r3944realms.ltdmanager.napcat.data.ID
@ -104,7 +106,7 @@ class GameClientOperation(
pattern.replace(originalUrl) { matchResult -> pattern.replace(originalUrl) { matchResult ->
// 保留原始 URL 中的路径部分(如果有的话) // 保留原始 URL 中的路径部分(如果有的话)
val path = matchResult.groupValues[1] ?: "" val path = matchResult.groupValues[1]
"$configUrl$path" "$configUrl$path"
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -130,8 +132,7 @@ class GameClientOperation(
albumId = "BFx", albumId = "BFx",
expiration = "PT5M" expiration = "PT5M"
) )
if (response.image?.url != null) { if (response is CheveretoUploadResponse){
// 发送图床 URL 给玩家
napCatClient.sendUnit( napCatClient.sendUnit(
SendPrivateMsgRequest( SendPrivateMsgRequest(
listOf( listOf(
@ -141,8 +142,15 @@ class GameClientOperation(
ID.long(playerId) ID.long(playerId)
) )
) )
} else { } else if (response is FailedCheveretoResponse.Default){
LoggerUtil.logger.error("上传二维码返回 JSON 未包含 URL") napCatClient.sendUnit(
SendPrivateMsgRequest(
listOf(
MessageElement.text("无法上传图片,请联系管理员:${response.httpStatusCode} , ${response.failedMessage}"),
),
ID.long(playerId)
)
)
} }
// 启动 60 秒倒计时任务 // 启动 60 秒倒计时任务
bindingTimeoutJob = launch { bindingTimeoutJob = launch {

View File

@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.module.* import top.r3944realms.ltdmanager.module.*
// DSL
fun main() = GlobalManager.runBlockingMain { fun main() = GlobalManager.runBlockingMain {
val commonGroupId:Long = 538751386 val commonGroupId:Long = 538751386
val whitelistGroupId:Long = 920719236 val whitelistGroupId:Long = 920719236
@ -43,7 +43,7 @@ fun main() = GlobalManager.runBlockingMain {
) )
val toolConfig = YamlConfigLoader.loadToolConfig() val toolConfig = YamlConfigLoader.loadToolConfig()
val corconModule = RconPlayerListModule( val corconModule = RconPlayerListModule(
moduleName = "WhiteListGroup", moduleName = "CommonGroup",
groupMessagePollingModule = commonGroupMsgPollingModule, groupMessagePollingModule = commonGroupMsgPollingModule,
rconTimeOut = 2_000L, rconTimeOut = 2_000L,
cooldownMillis = 10_000L, cooldownMillis = 10_000L,
@ -98,30 +98,30 @@ fun main() = GlobalManager.runBlockingMain {
// "Apply for an invitation code" // "Apply for an invitation code"
// ) // )
// ) // )
val commonMcServerStatusModule = McServerStatusModule( // val commonMcServerStatusModule = McServerStatusModule(
moduleName = "CommonGroup", // moduleName = "CommonGroup",
groupMessagePollingModule = commonGroupMsgPollingModule, // groupMessagePollingModule = commonGroupMsgPollingModule,
selfId = selfQQId, // selfId = selfQQId,
cooldownMillis = 20_000L, // cooldownMillis = 20_000L,
selfNickName = selfNickName, // selfNickName = selfNickName,
commands = listOf("/m", "/mcs", "seek", "s", "test"), // commands = listOf("/m", "/mcs", "seek", "s", "test"),
presetServer = mapOf( // presetServer = mapOf(
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", // setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" // setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
) // )
) // )
val whitelistMcServerStatusModule = McServerStatusModule( // val whitelistMcServerStatusModule = McServerStatusModule(
moduleName = "WhiteListGroup", // moduleName = "WhiteListGroup",
groupMessagePollingModule = whiteListGroupMsgPollingModule, // groupMessagePollingModule = whiteListGroupMsgPollingModule,
selfId = selfQQId, // selfId = selfQQId,
cooldownMillis = 20_000L, // cooldownMillis = 20_000L,
selfNickName = selfNickName, // selfNickName = selfNickName,
commands = listOf("/m", "/mcs", "seek", "s", "test"), // commands = listOf("/m", "/mcs", "seek", "s", "test"),
presetServer = mapOf( // presetServer = mapOf(
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106", // setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top" // setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
) // )
) // )
val dgLabModule = DGLabModule( val dgLabModule = DGLabModule(
moduleName = "DG", moduleName = "DG",
groupMessagePollingModule = commonGroupMsgPollingModule, groupMessagePollingModule = commonGroupMsgPollingModule,
@ -134,10 +134,10 @@ fun main() = GlobalManager.runBlockingMain {
GlobalManager.moduleManager.registerModule(groupModule) GlobalManager.moduleManager.registerModule(groupModule)
GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule) GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule)
GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule) GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule)
GlobalManager.moduleManager.registerModule(commonMcServerStatusModule) // GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
GlobalManager.moduleManager.registerModule(rconModule) GlobalManager.moduleManager.registerModule(rconModule)
GlobalManager.moduleManager.registerModule(corconModule) GlobalManager.moduleManager.registerModule(corconModule)
GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule) // GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
// GlobalManager.moduleManager.registerModule(mailModule) // GlobalManager.moduleManager.registerModule(mailModule)
// GlobalManager.moduleManager.registerModule(invitationCodesModule) // GlobalManager.moduleManager.registerModule(invitationCodesModule)
GlobalManager.moduleManager.registerModule(whitelistHelpModule) GlobalManager.moduleManager.registerModule(whitelistHelpModule)
@ -150,13 +150,13 @@ fun main() = GlobalManager.runBlockingMain {
GlobalManager.moduleManager.loadModule(groupModule.name) GlobalManager.moduleManager.loadModule(groupModule.name)
GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name) GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name)
GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name) GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name)
GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name) // GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
GlobalManager.moduleManager.loadModule(corconModule.name) GlobalManager.moduleManager.loadModule(corconModule.name)
GlobalManager.moduleManager.loadModule(rconModule.name) GlobalManager.moduleManager.loadModule(rconModule.name)
// GlobalManager.moduleManager.loadModule(mailModule.name) // GlobalManager.moduleManager.loadModule(mailModule.name)
// GlobalManager.moduleManager.loadModule(invitationCodesModule.name) // GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
GlobalManager.moduleManager.loadModule(commonHelpModule.name) GlobalManager.moduleManager.loadModule(commonHelpModule.name)
GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name) // GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
GlobalManager.moduleManager.loadModule(whitelistHelpModule.name) GlobalManager.moduleManager.loadModule(whitelistHelpModule.name)
GlobalManager.moduleManager.loadModule(dgLabModule.name) GlobalManager.moduleManager.loadModule(dgLabModule.name)
// GlobalManager.moduleManager.loadModule(banModule.name) // GlobalManager.moduleManager.loadModule(banModule.name)

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.blessingskin package top.r3944realms.ltdmanager.mcms
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
@ -11,17 +11,17 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit 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.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.Environment
import top.r3944realms.ltdmanager.utils.LoggerUtil import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
class BlessingSkinClient private constructor() : AutoCloseable { class MCSMClient private constructor() : AutoCloseable {
private val client = HttpClient(CIO) { private val client = HttpClient(CIO) {
expectSuccess = false expectSuccess = false
@ -40,7 +40,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
// 限流控制 // 限流控制
private val semaphore = Semaphore(5) private val semaphore = Semaphore(5)
private val requestMutex = Mutex() private val requestMutex = Mutex()
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority }) private val requestQueue = PriorityQueue<MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>>(compareBy { it.priority })
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init { init {
@ -50,14 +50,14 @@ class BlessingSkinClient private constructor() : AutoCloseable {
/** /**
* 提交请求 * 提交请求
*/ */
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest( suspend fun <T : MCSMResponse, F : FailedMCSMResponse> submitRequest(
request: BlessingSkinRequest<T, F>, request: MCSMRequest<T, F>,
priority: Int = 5, priority: Int = 5,
maxRetries: Int = 3 maxRetries: Int = 3
): ResponseResult<T, F> { ): ResponseResult<T, F> {
val deferred = CompletableDeferred<ResponseResult<T, F>>() val deferred = CompletableDeferred<ResponseResult<T, F>>()
requestMutex.withLock { requestMutex.withLock {
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true)) requestQueue.add(MCSMSkinQueueItem(request, deferred, priority, maxRetries, true))
} }
return deferred.await() return deferred.await()
} }
@ -83,7 +83,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
/** /**
* 处理队列项 * 处理队列项
*/ */
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) { private suspend fun processQueueItem(item: MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>) {
semaphore.withPermit { semaphore.withPermit {
val (request, deferred, _, maxRetries, _) = item val (request, deferred, _, maxRetries, _) = item
var attempt = 0 var attempt = 0
@ -130,7 +130,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
val result = request.getResponse(responseText, response.status) val result = request.getResponse(responseText, response.status)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(result) (deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(result)
return return
@ -142,15 +142,15 @@ class BlessingSkinClient private constructor() : AutoCloseable {
break break
} }
LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}") LoggerUtil.logger.warn("MCSM请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
delay((attempt * 1000L)) // 指数退避 delay((attempt * 1000L)) // 指数退避
} }
} }
// 所有重试都失败或不应重试 // 所有重试都失败或不应重试
val errorResponse = createFailureResponse(lastError, request) val errorResponse = createFailureResponse(lastError)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete( (deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(
ResponseResult.Failure(errorResponse) ResponseResult.Failure(errorResponse)
) )
} }
@ -159,7 +159,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
/** /**
* 构建完整的URL包含查询参数 * 构建完整的URL包含查询参数
*/ */
private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String { private fun buildFullUrlWithQueryParams(request: MCSMRequest<*, *>): String {
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/") val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
val path = request.path().removePrefix("/") val path = request.path().removePrefix("/")
@ -191,11 +191,10 @@ class BlessingSkinClient private constructor() : AutoCloseable {
* 创建失败响应 * 创建失败响应
*/ */
private fun createFailureResponse( private fun createFailureResponse(
exception: Exception?, exception: Exception?
request: BlessingSkinRequest<*, *> ): FailedMCSMResponse {
): FailedBlessingSkinResponse { return FailedMCSMResponse.ExceptionFailedMCSMResponse(
return FailedBlessingSkinResponse.Default( result = exception?.message ?: "未知错误",
failedResult = exception?.message ?: "未知错误",
) )
} }
@ -207,6 +206,6 @@ class BlessingSkinClient private constructor() : AutoCloseable {
} }
companion object { companion object {
fun create(): BlessingSkinClient = BlessingSkinClient() fun create(): MCSMClient = MCSMClient()
} }
} }

View File

@ -1,16 +1,16 @@
package top.r3944realms.ltdmanager.blessingskin package top.r3944realms.ltdmanager.mcms
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>( data class MCSMSkinQueueItem<out T:MCSMResponse,out F:FailedMCSMResponse>(
val request: BlessingSkinRequest<T,F>, val request: MCSMRequest<T,F>,
val deferred: CompletableDeferred<*>, val deferred: CompletableDeferred<*>,
var retries: Int, var retries: Int,
val priority: Int, val priority: Int,
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> { ) : Comparable<MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority) override fun compareTo(other: MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
} }

View File

@ -1,14 +1,15 @@
package top.r3944realms.ltdmanager.blessingskin.request package top.r3944realms.ltdmanager.mcms.request
import io.ktor.http.* import io.ktor.http.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult import top.r3944realms.ltdmanager.mcms.response.ResponseResult
@Serializable @Serializable
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>( abstract class MCSMRequest<out T : MCSMResponse, out F : FailedMCSMResponse>(
@Transient @Transient
open val createTime: Long = System.currentTimeMillis() open val createTime: Long = System.currentTimeMillis()
) { ) {

View File

@ -1,4 +1,50 @@
package top.r3944realms.ltdmanager.mcms.request.instance package top.r3944realms.ltdmanager.mcms.request.instance
class GetInstanceListRequest { 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<GetInstanceListResponse, FailedMCSMResponse>() {
override fun toJSON(): String = "{}" // GET 无请求体
override fun path(): String = "api/service/remote_service_instances"
override fun queryParameters(): Map<String, String> =
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<GetInstanceListResponse, FailedMCSMResponse> {
return if (httpStatusCode.value in 200..299) {
ResponseResult.Success(
kotlinx.serialization.json.Json.decodeFromString<GetInstanceListResponse>(responseJson)
)
} else {
ResponseResult.Failure(
kotlinx.serialization.json.Json.decodeFromString<FailedMCSMResponse>(responseJson)
)
}
}
override fun expectedResponseType(): String = GetInstanceListResponse::class.simpleName!!
override fun expectedFailureType(): String = FailedMCSMResponse::class.simpleName!!
}

View File

@ -1,4 +1,53 @@
package top.r3944realms.ltdmanager.mcms.request.instance package top.r3944realms.ltdmanager.mcms.request.instance
class StartInstanceRequest { 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<StartInstanceResponse, FailedMCSMResponse>() {
override fun toJSON(): String =
Json.encodeToString(this)
override fun path(): String =
"protected_instance/open"
override fun queryParameters(): Map<String, String> =
mapOf(
"uuid" to uuid,
"daemonId" to daemonId
)
override fun method(): HttpMethod = HttpMethod.Get
override fun getResponse(
responseJson: String,
httpStatusCode: HttpStatusCode
): ResponseResult<StartInstanceResponse, FailedMCSMResponse> {
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"
}

View File

@ -1,14 +1,29 @@
package top.r3944realms.ltdmanager.blessingskin.response package top.r3944realms.ltdmanager.mcms.response
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonObject
@Serializable @Serializable
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() { open class FailedMCSMResponse(
abstract fun failedMessage(): String @Transient
val status0: Status = Status.Ok,
val data: JsonObject? = null,
@Transient
val time0: Long = -1,
): MCSMResponse(
status0, time0
) {
@Serializable @Serializable
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() { data class ExceptionFailedMCSMResponse(
override fun failedMessage(): String = failedResult!! @Transient
val status1: Status = Status.Ok,
} val data0: String? = null,
@Transient
val time1: Long = -1,
@Transient
val result: String? = null,
): FailedMCSMResponse(
status1, null, time1
)
} }

View File

@ -1,15 +1,20 @@
package top.r3944realms.ltdmanager.blessingskin.response package top.r3944realms.ltdmanager.mcms.response
import io.ktor.http.* import io.ktor.http.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic 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 @Serializable
abstract class BlessingSkinResponse ( abstract class MCSMResponse (
open val status: Status,
open val time: Long,
@Transient @Transient
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
@Transient @Transient
@ -17,20 +22,29 @@ abstract class BlessingSkinResponse (
) { ) {
companion object { companion object {
// 通用的反序列化方法 // 通用的反序列化方法
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T { inline fun <reified T : MCSMResponse> decode(jsonString: String): T {
return json.decodeFromString(jsonString) return json.decodeFromString(jsonString)
} }
val json: Json by lazy { val json: Json by lazy {
Json { Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
serializersModule = SerializersModule { serializersModule = SerializersModule {
polymorphic(BlessingSkinResponse::class) { polymorphic(MCSMResponse::class) {
subclass(FailedBlessingSkinResponse.Default::class, FailedBlessingSkinResponse.Default.serializer()) subclass(GetInstanceListResponse::class, GetInstanceListResponse.serializer())
subclass(InvitationCodeGenerationResponse::class, InvitationCodeGenerationResponse.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
}
} }
} }

View File

@ -1,9 +1,9 @@
package top.r3944realms.ltdmanager.blessingskin.response package top.r3944realms.ltdmanager.mcms.response
// 响应结果封装 // 响应结果封装
sealed class ResponseResult<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse> { sealed class ResponseResult<out T : MCSMResponse, out F : FailedMCSMResponse> {
data class Success<T : BlessingSkinResponse>(val response: T) : ResponseResult<T, Nothing>() data class Success<T : MCSMResponse>(val response: T) : ResponseResult<T, Nothing>()
data class Failure<F : FailedBlessingSkinResponse>(val failure: F) : ResponseResult<Nothing, F>() data class Failure<F : FailedMCSMResponse>(val failure: F) : ResponseResult<Nothing, F>()
/** /**
* 检查是否成功 * 检查是否成功

View File

@ -1,11 +1,60 @@
package top.r3944realms.ltdmanager.mcms.response.instance package top.r3944realms.ltdmanager.mcms.response.instance
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonObject
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
@Serializable @Serializable
data class InstanceListResponse( data class GetInstanceListResponse(
val status: Int, @Transient
val data: InstanceListData?, val status0: Status = Status.Ok,
val time: Long val data: InstanceListData? = null,
) : MCSMResponse @Transient
val time0: Long = -1,
) : MCSMResponse(status0, time0) {
@Serializable
data class InstanceListData(
val maxPage: Int,
val pageSize: Int,
val data: List<InstanceDetail>
)
@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<PlayerChartItem>,
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
)
}

View File

@ -1,4 +1,21 @@
package top.r3944realms.ltdmanager.mcms.response.instance package top.r3944realms.ltdmanager.mcms.response.instance
class StartInstanceResponse { 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
)
}

View File

@ -1,4 +1,17 @@
package top.r3944realms.ltdmanager.module 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<String> = setOf("申请白名单")
):
BaseModule(Modules.APPLY_WHITELIST,moduleName) {
override fun onLoad() {
TODO("Not yet implemented")
}
override suspend fun onUnload() {
TODO("Not yet implemented")
}
} }

View File

@ -35,7 +35,7 @@ class BanModule(
private val maxBanMinutes: Int = 15, private val maxBanMinutes: Int = 15,
private val factorX: Int = 2, // 系数 x禁言倍数 private val factorX: Int = 2, // 系数 x禁言倍数
) : BaseModule("BanModule", moduleName), PersistentState<BanModule.BanState> { ) : BaseModule(Modules.BAN, moduleName), PersistentState<BanModule.BanState> {
private val banCommandParse = CommandParser(muteCommandPrefixList) private val banCommandParse = CommandParser(muteCommandPrefixList)
private val pardonCommandParse = CommandParser(unmuteCommandPrefixList) private val pardonCommandParse = CommandParser(unmuteCommandPrefixList)

View File

@ -1,6 +1,7 @@
package top.r3944realms.ltdmanager.module package top.r3944realms.ltdmanager.module
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import org.intellij.lang.annotations.MagicConstant
import top.r3944realms.ltdmanager.GlobalManager import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.utils.LoggerUtil import top.r3944realms.ltdmanager.utils.LoggerUtil
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException

View File

@ -49,7 +49,7 @@ class DGLabModule(
val adminIds: List<Long> = listOf(), val adminIds: List<Long> = listOf(),
val maxClientNumber: Int = 10, val maxClientNumber: Int = 10,
val commandHead: List<String> = listOf("dglab"), val commandHead: List<String> = listOf("dglab"),
) : BaseModule("DGLabModule", moduleName), PersistentState<DGLabModule.DgLabState> { ) : BaseModule(Modules.DG_LAB, moduleName), PersistentState<DGLabModule.DgLabState> {
var dgLabManager: DgLab? = null var dgLabManager: DgLab? = null
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null

View File

@ -15,7 +15,7 @@ class GroupMessagePollingModule(
val targetGroupId: Long, val targetGroupId: Long,
private val pollIntervalMillis: Long = 5_000L, private val pollIntervalMillis: Long = 5_000L,
private val msgHistoryCheck: Int = 15, private val msgHistoryCheck: Int = 15,
) : BaseModule("MessagePollingModule", moduleName) { ) : BaseModule(Modules.GROUP_MESSAGE_POLLING, moduleName) {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
// 用 Flow 存消息,其他模块可以订阅 // 用 Flow 存消息,其他模块可以订阅

View File

@ -15,7 +15,7 @@ class GroupRequestHandlerModule(
private val client: NapCatClient, private val client: NapCatClient,
private val targetGroupId: Long, private val targetGroupId: Long,
private val pollIntervalMillis: Long = 30_000L, private val pollIntervalMillis: Long = 30_000L,
) : BaseModule("GroupRequestHandlerModule", moduleName) { ) : BaseModule(Modules.GROUP_REQUEST_HANDLER, moduleName) {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null

View File

@ -33,7 +33,7 @@ class HelpModule(
private val selfNickName: String, private val selfNickName: String,
private val keywords: List<String> = listOf("help", "帮助"), private val keywords: List<String> = listOf("help", "帮助"),
private val cooldownMillis: Long = 30_000L private val cooldownMillis: Long = 30_000L
) : BaseModule("HelpModule", moduleName), PersistentState<HelpModule.HelpState> { ) : BaseModule(Modules.HELP, moduleName), PersistentState<HelpModule.HelpState> {
// 命令解析器 // 命令解析器
private val commandParser = CommandParser(keywords) private val commandParser = CommandParser(keywords)

View File

@ -4,9 +4,10 @@ import kotlinx.coroutines.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json 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.request.invitecode.GenerateInvitationCodeRequest
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse 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.core.mail.mail
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
@ -77,7 +78,7 @@ class InvitationCodesModule(
selfId: Long, selfId: Long,
private val cooldownMillis: Long = 120_000, private val cooldownMillis: Long = 120_000,
private val keywords: Set<String> = setOf("申请邀请码") private val keywords: Set<String> = setOf("申请邀请码")
) : BaseModule("InvitationCodesModule", moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> { ) : BaseModule(Modules.INVITATION_CODE, moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name) private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name)
@ -430,23 +431,33 @@ class InvitationCodesModule(
/** /**
* 1. 创建邀请码 * 1. 创建邀请码
*/ */
private suspend fun createInvitationCodes(amount: Int): List<InvitationCodeGenerationResponse.InvitationCode>? { private suspend fun createInvitationCodes(amount: Int): List<InvitationCode>? {
return try { return try {
val response = blessingSkinClient.submitRequest( val response = blessingSkinClient.submitRequest(
GenerateInvitationCodeRequest(amount = amount, token = apiToken) GenerateInvitationCodeRequest(amount = amount, token = apiToken)
) )
response
.onFailure {
}
.onSuccess {
}
when (response) { when (response) {
is ResponseResult.Success -> { is ResponseResult.Success -> {
if (response.response.success) { if (response.response is InvitationCodeGenerationResponse) {
response.response.data if (response.response.success) {
response.response.data
} else
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
null
} else { } else {
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}") LoggerUtil.logger.warn("[$name] 返回非预期对象类型: ${response.response.javaClass}")
null null
} }
} }
is ResponseResult.Failure -> { is ResponseResult.Failure -> {
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedResult}") LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedMessage}")
null null
} }
} }
@ -460,7 +471,7 @@ class InvitationCodesModule(
* 2. 验证数量匹配 * 2. 验证数量匹配
*/ */
private fun validateCodeCountMatch( private fun validateCodeCountMatch(
invitationCodes: List<InvitationCodeGenerationResponse.InvitationCode>?, invitationCodes: List<InvitationCode>?,
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>> needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>
) { ) {
if (invitationCodes == null) { if (invitationCodes == null) {

View File

@ -19,7 +19,7 @@ class MailModule(
private val enableAuth: Boolean = true, private val enableAuth: Boolean = true,
private val enableTLS: Boolean = true, private val enableTLS: Boolean = true,
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s
) : BaseModule("MailModule", moduleName) { ) : BaseModule(Modules.MAIL, moduleName) {
private lateinit var session: Session private lateinit var session: Session
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列 private val queue = LinkedBlockingQueue<Mail>() // 邮件队列

View File

@ -36,7 +36,7 @@ class McServerStatusModule(
setOf("hp", "hypixel") to "mc.hypixel.net", setOf("hp", "hypixel") to "mc.hypixel.net",
setOf("pm", "mineplex") to "play.mineplex.com" setOf("pm", "mineplex") to "play.mineplex.com"
) )
) : BaseModule("McServerStatusModule", moduleName), PersistentState<McServerStatusModule.CooldownState> { ) : BaseModule(Modules.MC_SERVER_STATUS, moduleName), PersistentState<McServerStatusModule.CooldownState> {
private val stateFile:File = getStateFileInternal("mc_server_status_state.json", name) 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 stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
private val commandParser: CommandParser = CommandParser(commands) private val commandParser: CommandParser = CommandParser(commands)

View File

@ -31,7 +31,7 @@ class ModGroupHandlerModule(
private val targetGroupId: Long, private val targetGroupId: Long,
private val answers: List<String> = listOf("正确答案"), private val answers: List<String> = listOf("正确答案"),
private val pollIntervalMillis: Long = 30_000L private val pollIntervalMillis: Long = 30_000L
) : BaseModule("ModGroupHandlerModule", moduleName), PersistentState<ModGroupHandlerModule.RejectRecords> { ) : BaseModule(Modules.MOD_GROUP_HANDLER, moduleName), PersistentState<ModGroupHandlerModule.RejectRecords> {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
private val stateFile: File = getStateFileInternal("reject_records.json", name) private val stateFile: File = getStateFileInternal("reject_records.json", name)

View File

@ -1,4 +1,26 @@
package top.r3944realms.ltdmanager.module package top.r3944realms.ltdmanager.module
import java.util.*
object Modules { object Modules {
private val MODULES: MutableList<String> = 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<String> {
return MODULES.toTypedArray();
}
} }

View File

@ -35,7 +35,7 @@ class RconPlayerListModule(
private val rconPath: String, private val rconPath: String,
private val rconConfigPath: String, private val rconConfigPath: String,
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家") private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
) : BaseModule("RconPlayerListModule", moduleName), PersistentState<LastTriggerState> { ) : BaseModule(Modules.RCON_PLAYER_LIST, moduleName), PersistentState<LastTriggerState> {
private val cooldownManager by lazy { private val cooldownManager by lazy {
CooldownManager( CooldownManager(
cooldownMillis = cooldownMillis, cooldownMillis = cooldownMillis,

View File

@ -8,7 +8,7 @@ class StateModule(
moduleName: String, moduleName: String,
private val onlineName: String, private val onlineName: String,
private val offlineName: String, private val offlineName: String,
): BaseModule("StateModule", moduleName) { ): BaseModule(Modules.STATE, moduleName) {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
override fun onLoad() { override fun onLoad() {
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

View File

@ -1,7 +1,17 @@
package top.r3944realms.ltdmanager.module.exception package top.r3944realms.ltdmanager.module.exception
class InvalidConfigException: Exception() { class ConfigError(type: Type = Type.OTHER, private val pos: String, vararg args: Any) : Exception() {
enum class Type(template: String) { 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")
} }
} }

View File

@ -13,7 +13,7 @@ object ConfigInitializer {
* @param fileName YAML 文件名 application.yml * @param fileName YAML 文件名 application.yml
* @param configDir 配置目录 config * @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) val dirPath = Paths.get(configDir)
if (!Files.exists(dirPath)) { if (!Files.exists(dirPath)) {
Files.createDirectories(dirPath) Files.createDirectories(dirPath)
@ -28,8 +28,10 @@ object ConfigInitializer {
if (resourceStream != null) { if (resourceStream != null) {
Files.copy(resourceStream, filePath, StandardCopyOption.REPLACE_EXISTING) Files.copy(resourceStream, filePath, StandardCopyOption.REPLACE_EXISTING)
LoggerUtil.logger.info("已生成默认配置文件: $filePath") LoggerUtil.logger.info("已生成默认配置文件: $filePath")
LoggerUtil.logger.info("第一次启动,请修改配置后再启动") if (shouldExit) {
exitProcess(-1); LoggerUtil.logger.info("第一次启动,请修改配置后再启动")
exitProcess(-1);
}
} else throw Error("Jar内部资源文件缺失") } else throw Error("Jar内部资源文件缺失")
} else { } else {

View File

@ -20,6 +20,8 @@ websocket:
url: "wss://127.0.0.1:3002" url: "wss://127.0.0.1:3002"
# 格式为 ENC(XXX),若不是则会在加载完成配置后自动加密 # 格式为 ENC(XXX),若不是则会在加载完成配置后自动加密
encrypted-token: "123123cc" encrypted-token: "123123cc"
mcsm:
tools: tools:
rcon: rcon:
mc-rcon-tool-path: "/path/to/rcon" mc-rcon-tool-path: "/path/to/rcon"