refactor: 代码调整
This commit is contained in:
parent
4da8263b45
commit
37eeaf143c
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<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>
|
||||
<synchronize>true</synchronize>
|
||||
<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>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ repositories {
|
|||
maven {
|
||||
url = uri("https://repo.glaremasters.me/repository/public/")
|
||||
}
|
||||
maven {
|
||||
name = "LTD Maven"
|
||||
url = uri("https://nexus.bot.leisuretimedock.top/repository/maven-public/")
|
||||
}
|
||||
}
|
||||
//TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
|
||||
dependencies {
|
||||
|
|
@ -77,7 +81,7 @@ repositories {
|
|||
//DG_Lab 依赖库导入
|
||||
implementation("io.netty:netty-all:4.1.109.Final")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
implementation(files("libs/DgLab-common-${k("dg_lab_version")}.jar"))
|
||||
implementation("top.r3944realms.dg_lab:Common:${k("dg_lab_version")}")
|
||||
|
||||
//生成 二维码
|
||||
implementation("com.google.zxing:core:[3.5.3,)")
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ org.gradle.parallel=true
|
|||
org.gradle.degree_of_parallelism=16
|
||||
project_group=top.r3944realms.ltdmanager
|
||||
project_version=1.14-SNAPSHOT
|
||||
dg_lab_version=4.3.13.18
|
||||
dg_lab_version=4.4.14.18
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import kotlinx.coroutines.*
|
|||
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
|
||||
import top.r3944realms.ltdmanager.chevereto.CheveretoClient
|
||||
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
|
||||
import top.r3944realms.ltdmanager.mcms.MCSMClient
|
||||
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
|
||||
import top.r3944realms.ltdmanager.module.ModuleManager
|
||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||
|
|
@ -20,6 +21,10 @@ object GlobalManager {
|
|||
MysqlHikariConnectPool()
|
||||
}
|
||||
|
||||
fun initApplication() {
|
||||
|
||||
}
|
||||
|
||||
// NapCat 客户端
|
||||
val napCatClient: NapCatClient by lazy {
|
||||
NapCatClient.create()
|
||||
|
|
@ -33,6 +38,9 @@ object GlobalManager {
|
|||
val cheveretoClient: CheveretoClient by lazy {
|
||||
CheveretoClient.create()
|
||||
}
|
||||
val mcsmClient: MCSMClient by lazy {
|
||||
MCSMClient.create()
|
||||
}
|
||||
|
||||
val moduleManager: ModuleManager by lazy { ModuleManager() }
|
||||
|
||||
|
|
@ -72,7 +80,8 @@ object GlobalManager {
|
|||
"McSrvStatusClient" to { mcSrvStatusClient.close() },
|
||||
"BlessingSkinClient" to { blessingSkinClient.close() },
|
||||
"Hikari 数据源" to { dataSource.close() },
|
||||
"CheveretoClient" to { cheveretoClient.close() }
|
||||
"CheveretoClient" to { cheveretoClient.close() },
|
||||
"McsmClient" to { mcsmClient.close() },
|
||||
)
|
||||
|
||||
resources.forEach { (name, closer) ->
|
||||
|
|
@ -99,4 +108,5 @@ object GlobalManager {
|
|||
isRunning.set(false)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,27 +1,24 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.client.IClient
|
||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import top.r3944realms.ltdmanager.utils.Environment
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
|
||||
class BlessingSkinClient private constructor() : AutoCloseable {
|
||||
class BlessingSkinClient private constructor() : IClient<BlessingSkinRequest, BlessingSkinQueueItem, BlessingSkinResponse, FailedBlessingSkinResponse> {
|
||||
private val client = HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
|
||||
|
|
@ -40,170 +37,40 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
// 限流控制
|
||||
private val semaphore = Semaphore(5)
|
||||
private val requestMutex = Mutex()
|
||||
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority })
|
||||
private val requestQueue = PriorityQueue<BlessingSkinQueueItem>(compareBy { it.priority })
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
startQueueProcessor()
|
||||
init()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交请求
|
||||
*/
|
||||
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest(
|
||||
request: BlessingSkinRequest<T, F>,
|
||||
priority: Int = 5,
|
||||
maxRetries: Int = 3
|
||||
): ResponseResult<T, F> {
|
||||
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
||||
requestMutex.withLock {
|
||||
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true))
|
||||
}
|
||||
return deferred.await()
|
||||
override fun getBaseUrl(): String = blessingSkinServerConfig.url!!
|
||||
|
||||
override fun getType(): String = "BlessingSkinClient"
|
||||
|
||||
override fun getClient(): HttpClient = client
|
||||
|
||||
override fun getSemaphore(): Semaphore = semaphore
|
||||
|
||||
override fun getRequestMutex(): Mutex = requestMutex
|
||||
|
||||
override fun getResponseQueue(): PriorityQueue<BlessingSkinQueueItem> = requestQueue
|
||||
|
||||
override fun getScope(): CoroutineScope = scope
|
||||
|
||||
override fun createFailureResponse(exception: Exception?): IFailedResponse {
|
||||
return FailedBlessingSkinResponse.Default(exception?.stackTraceToString()?:"ERROR")
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动队列处理器
|
||||
*/
|
||||
private fun startQueueProcessor() {
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
val item = requestMutex.withLock {
|
||||
requestQueue.poll()
|
||||
}
|
||||
if (item == null) {
|
||||
delay(50)
|
||||
continue
|
||||
}
|
||||
processQueueItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理队列项
|
||||
*/
|
||||
private suspend fun processQueueItem(item: BlessingSkinQueueItem<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()
|
||||
}
|
||||
override fun addToQueue(
|
||||
request: BlessingSkinRequest,
|
||||
deferredC: CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>,
|
||||
priority: Int,
|
||||
maxRetries: Int
|
||||
): BlessingSkinQueueItem {
|
||||
val element = BlessingSkinQueueItem(request, deferredC, priority, maxRetries, false)
|
||||
requestQueue.add(element)
|
||||
return element
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import kotlinx.coroutines.CompletableDeferred
|
|||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.core.client.QueueItem
|
||||
|
||||
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>(
|
||||
val request: BlessingSkinRequest<T,F>,
|
||||
val deferred: CompletableDeferred<*>,
|
||||
var retries: Int,
|
||||
val priority: Int,
|
||||
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
||||
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||
}
|
||||
data class BlessingSkinQueueItem (
|
||||
val request0: BlessingSkinRequest,
|
||||
val deferred0: CompletableDeferred<*>,
|
||||
val priority0: Int,
|
||||
var retries0: Int,
|
||||
val expectsResponse0: Boolean
|
||||
) : QueueItem<BlessingSkinRequest, BlessingSkinResponse, FailedBlessingSkinResponse> (
|
||||
request0, deferred0, retries0, priority0, expectsResponse0
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1,79 +1,13 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.request
|
||||
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||
|
||||
@Serializable
|
||||
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>(
|
||||
abstract class BlessingSkinRequest(
|
||||
@Transient
|
||||
open val createTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
/**
|
||||
* 转换为JSON字符串
|
||||
*/
|
||||
abstract fun toJSON(): String
|
||||
|
||||
/**
|
||||
* 获取API路径(不包含基础URL)
|
||||
* 例如: "invitation-codes/generate"
|
||||
*/
|
||||
abstract fun path(): String
|
||||
|
||||
/**
|
||||
* 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数)
|
||||
*/
|
||||
open fun method(): HttpMethod = HttpMethod.Get
|
||||
|
||||
/**
|
||||
* 自定义请求头
|
||||
*/
|
||||
open fun headers(): HeadersBuilder.() -> Unit = {
|
||||
// 默认添加Content-Type
|
||||
append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
// 添加Accept头
|
||||
append(HttpHeaders.Accept, "application/json")
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询参数(用于URL参数)
|
||||
* 例如: mapOf("token" to "abc123", "amount" to "1")
|
||||
*/
|
||||
open fun queryParameters(): Map<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
|
||||
}
|
||||
override val createTime: Long = System.currentTimeMillis()
|
||||
): IRequest<BlessingSkinResponse, FailedBlessingSkinResponse>
|
||||
|
|
@ -6,8 +6,8 @@ import kotlinx.serialization.Transient
|
|||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -17,9 +17,7 @@ class GenerateInvitationCodeRequest(
|
|||
val token: String? = null,
|
||||
@Transient
|
||||
val amount: Int? = 1,
|
||||
@Transient
|
||||
override val createTime: Long = System.currentTimeMillis()
|
||||
) : BlessingSkinRequest<InvitationCodeGenerationResponse, FailedBlessingSkinResponse.Default>() {
|
||||
) : BlessingSkinRequest() {
|
||||
|
||||
override fun toJSON(): String {
|
||||
// 对于GET请求,参数在URL中,body可以为空
|
||||
|
|
@ -66,7 +64,7 @@ class GenerateInvitationCodeRequest(
|
|||
} catch (e: Exception) {
|
||||
ResponseResult.Failure(
|
||||
FailedBlessingSkinResponse.Default(
|
||||
failedResult = "解析响应失败: ${e.message}"
|
||||
failedMessage = "解析响应失败: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,15 @@ import kotlinx.serialization.json.Json
|
|||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||
|
||||
@Serializable
|
||||
abstract class BlessingSkinResponse (
|
||||
@Transient
|
||||
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||
override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||
@Transient
|
||||
open val createTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
override val createTime: Long = System.currentTimeMillis()
|
||||
) : IResponse {
|
||||
companion object {
|
||||
// 通用的反序列化方法
|
||||
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ package top.r3944realms.ltdmanager.blessingskin.response
|
|||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||
|
||||
@Serializable
|
||||
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() {
|
||||
abstract fun failedMessage(): String
|
||||
abstract class FailedBlessingSkinResponse: BlessingSkinResponse(), IFailedResponse {
|
||||
@Serializable
|
||||
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() {
|
||||
override fun failedMessage(): String = failedResult!!
|
||||
class Default(@Transient override val failedMessage: String = "未知错误") : FailedBlessingSkinResponse() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.response.invitecode
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
@Serializable
|
||||
data class InvitationCodeGenerationResponse(
|
||||
|
|
@ -10,12 +10,4 @@ data class InvitationCodeGenerationResponse(
|
|||
val data: List<InvitationCode>? = null
|
||||
) : BlessingSkinResponse() {
|
||||
|
||||
@Serializable
|
||||
data class InvitationCode(
|
||||
val code: String,
|
||||
@SerialName("generated_at")
|
||||
val generatedAt: String,
|
||||
@SerialName("expires_at")
|
||||
val expiresAt: String
|
||||
)
|
||||
}
|
||||
|
|
@ -3,65 +3,157 @@ package top.r3944realms.ltdmanager.chevereto
|
|||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse
|
||||
import top.r3944realms.ltdmanager.chevereto.data.CheveretoSource
|
||||
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||
import top.r3944realms.ltdmanager.chevereto.request.v1.CheveretoUploadRequest
|
||||
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||
import top.r3944realms.ltdmanager.core.client.IClient
|
||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import top.r3944realms.ltdmanager.utils.Environment
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayDeque
|
||||
|
||||
|
||||
class CheveretoClient private constructor() : Closeable {
|
||||
class CheveretoClient private constructor() :
|
||||
IClient<CheveretoRequest, CheveretoQueueItem, CheveretoResponse, FailedCheveretoResponse> {
|
||||
|
||||
private val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
expectSuccess = false
|
||||
// 安装 HttpTimeout 插件
|
||||
install(HttpTimeout) {
|
||||
// 默认超时配置,会被具体请求的配置覆盖
|
||||
requestTimeoutMillis = 30000
|
||||
connectTimeoutMillis = 10000
|
||||
socketTimeoutMillis = 15000
|
||||
}
|
||||
}
|
||||
|
||||
private val imgTuConfig = YamlConfigLoader.loadTuImgConfig()
|
||||
private val apiUrl = imgTuConfig.url!!
|
||||
private val baseUrl = imgTuConfig.url!!.removeSuffix("/")
|
||||
private val apiKey = imgTuConfig.decryptedPassword!!
|
||||
// 限流,同时最多 3 个上传
|
||||
|
||||
private val semaphore = Semaphore(3)
|
||||
|
||||
// 普通队列 (按 priority 排序)
|
||||
private val queue = PriorityQueue<CheveretoQueueItem<CheveretoResponse>>(compareBy { it.priority })
|
||||
private val queue = PriorityQueue<CheveretoQueueItem>()
|
||||
private val queueMutex = Mutex()
|
||||
|
||||
// 紧急队列 (FIFO,最多 10 个)
|
||||
private val urgentQueue = ArrayDeque<CheveretoQueueItem<CheveretoResponse>>(10)
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
while (isActive) {
|
||||
val item = queueMutex.withLock {
|
||||
when {
|
||||
urgentQueue.isNotEmpty() -> urgentQueue.removeFirst()
|
||||
queue.isNotEmpty() -> queue.poll()
|
||||
else -> null
|
||||
init()
|
||||
}
|
||||
|
||||
override fun getType(): String = "CheveretoClient"
|
||||
|
||||
override fun getClient(): HttpClient = client
|
||||
|
||||
override fun getSemaphore(): Semaphore = semaphore
|
||||
|
||||
override fun getRequestMutex(): Mutex = queueMutex
|
||||
|
||||
override fun getResponseQueue(): PriorityQueue<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
|
||||
*/
|
||||
|
|
@ -77,35 +169,24 @@ class CheveretoClient private constructor() : Closeable {
|
|||
nsfw: Int? = null,
|
||||
format: String = "json",
|
||||
useFileDate: Int? = null,
|
||||
priority: Int = 5
|
||||
priority: Int = 5,
|
||||
maxRetries: Int = 3
|
||||
|
||||
): CheveretoResponse {
|
||||
val deferred = CompletableDeferred<CheveretoResponse>()
|
||||
val source = suspend {
|
||||
safeUpload {
|
||||
submitFormWithBinaryData(
|
||||
url = apiUrl,
|
||||
formData = formData {
|
||||
append("source", file.readBytes(), Headers.build {
|
||||
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"")
|
||||
})
|
||||
append("format", format)
|
||||
title?.let { append("title", it) }
|
||||
description?.let { append("description", it) }
|
||||
tags?.let { append("tags", it) }
|
||||
albumId?.let { append("album_id", it) }
|
||||
categoryId?.let { append("category_id", it) }
|
||||
width?.let { append("width", it.toString()) }
|
||||
expiration?.let { append("expiration", it) }
|
||||
nsfw?.let { append("nsfw", it.toString()) }
|
||||
useFileDate?.let { append("use_file_date", it.toString()) }
|
||||
}
|
||||
) {
|
||||
header("X-API-Key", apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
|
||||
return deferred.await()
|
||||
upload(CheveretoUploadRequest(
|
||||
source = CheveretoSource.ByteArraySource(file.readBytes(), file.name),
|
||||
format = format,
|
||||
title = title,
|
||||
description = description,
|
||||
tags = tags,
|
||||
albumId = albumId,
|
||||
categoryId = categoryId,
|
||||
width = width,
|
||||
expiration = expiration,
|
||||
nsfw = nsfw,
|
||||
useFileDate = useFileDate
|
||||
), priority, maxRetries).getRetResponse()
|
||||
throw Exception("Never Reach")
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -125,36 +206,23 @@ class CheveretoClient private constructor() : Closeable {
|
|||
nsfw: Int? = null,
|
||||
format: String = "json",
|
||||
useFileDate: Int? = null,
|
||||
priority: Int = 5
|
||||
priority: Int = 5,
|
||||
maxRetries: Int = 3
|
||||
): CheveretoResponse {
|
||||
val deferred = CompletableDeferred<CheveretoResponse>()
|
||||
val source = suspend {
|
||||
val bytes = inputStream.readBytes()
|
||||
safeUpload {
|
||||
submitFormWithBinaryData(
|
||||
url = apiUrl,
|
||||
formData = formData {
|
||||
append("source", bytes, Headers.build {
|
||||
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"")
|
||||
})
|
||||
append("format", format)
|
||||
title?.let { append("title", it) }
|
||||
description?.let { append("description", it) }
|
||||
tags?.let { append("tags", it) }
|
||||
albumId?.let { append("album_id", it) }
|
||||
categoryId?.let { append("category_id", it) }
|
||||
width?.let { append("width", it.toString()) }
|
||||
expiration?.let { append("expiration", it) }
|
||||
nsfw?.let { append("nsfw", it.toString()) }
|
||||
useFileDate?.let { append("use_file_date", it.toString()) }
|
||||
}
|
||||
) {
|
||||
header("X-API-Key", apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
|
||||
return deferred.await()
|
||||
upload(CheveretoUploadRequest(
|
||||
source = CheveretoSource.ByteArraySource(inputStream.readBytes(), fileName),
|
||||
format = format,
|
||||
title = title,
|
||||
description = description,
|
||||
tags = tags,
|
||||
albumId = albumId,
|
||||
categoryId = categoryId,
|
||||
width = width,
|
||||
expiration = expiration,
|
||||
nsfw = nsfw,
|
||||
useFileDate = useFileDate
|
||||
), priority, maxRetries).getRetResponse()
|
||||
throw Exception("Never Reach")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,64 +240,41 @@ class CheveretoClient private constructor() : Closeable {
|
|||
nsfw: Int? = null,
|
||||
format: String = "json",
|
||||
useFileDate: Int? = null,
|
||||
priority: Int = 5
|
||||
priority: Int = 5,
|
||||
maxRetries: Int = 3
|
||||
): CheveretoResponse {
|
||||
val deferred = CompletableDeferred<CheveretoResponse>()
|
||||
val source = suspend {
|
||||
safeUpload {
|
||||
submitForm(
|
||||
url = apiUrl,
|
||||
formParameters = Parameters.build {
|
||||
append("source", url)
|
||||
append("format", format)
|
||||
title?.let { append("title", it) }
|
||||
description?.let { append("description", it) }
|
||||
tags?.let { append("tags", it) }
|
||||
albumId?.let { append("album_id", it) }
|
||||
categoryId?.let { append("category_id", it) }
|
||||
width?.let { append("width", it.toString()) }
|
||||
expiration?.let { append("expiration", it) }
|
||||
nsfw?.let { append("nsfw", it.toString()) }
|
||||
useFileDate?.let { append("use_file_date", it.toString()) }
|
||||
}
|
||||
) {
|
||||
header("X-API-Key", apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
|
||||
return deferred.await()
|
||||
upload(CheveretoUploadRequest(
|
||||
source = CheveretoSource.UrlSource(url),
|
||||
format = format,
|
||||
title = title,
|
||||
description = description,
|
||||
tags = tags,
|
||||
albumId = albumId,
|
||||
categoryId = categoryId,
|
||||
width = width,
|
||||
expiration = expiration,
|
||||
nsfw = nsfw,
|
||||
useFileDate = useFileDate
|
||||
), priority, maxRetries).getRetResponse()
|
||||
throw Exception("Never Reach")
|
||||
}
|
||||
|
||||
private suspend fun processItem(item: CheveretoQueueItem<CheveretoResponse>) {
|
||||
semaphore.withPermit {
|
||||
try {
|
||||
val result = item.source()
|
||||
item.deferred.complete(result)
|
||||
} catch (e: Exception) {
|
||||
item.deferred.completeExceptionally(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 包装上传,失败时打印原始响应
|
||||
*/
|
||||
private suspend fun safeUpload(block: suspend HttpClient.() -> HttpResponse): CheveretoResponse {
|
||||
val response = client.block()
|
||||
suspend fun upload(
|
||||
request: CheveretoUploadRequest, priority: Int, maxRetries: Int
|
||||
): ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse> {
|
||||
return try {
|
||||
response.body()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
submitRequest(request, priority, maxRetries) as ResponseResult<CheveretoUploadResponse, FailedCheveretoResponse>
|
||||
} catch (e: Exception) {
|
||||
val raw = response.bodyAsText()
|
||||
throw RuntimeException("Upload failed (status=${response.status}): $raw", e)
|
||||
ResponseResult.Failure(
|
||||
FailedCheveretoResponse.Default(
|
||||
httpStatusCode = HttpStatusCode.InternalServerError,
|
||||
failedMessage = "Byte array upload failed: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
scope.cancel()
|
||||
runBlocking { client.close() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): CheveretoClient = CheveretoClient()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
package top.r3944realms.ltdmanager.chevereto
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import top.r3944realms.ltdmanager.chevereto.request.CheveretoRequest
|
||||
import top.r3944realms.ltdmanager.chevereto.response.CheveretoResponse
|
||||
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||
import top.r3944realms.ltdmanager.core.client.QueueItem
|
||||
|
||||
data class CheveretoQueueItem<T>(
|
||||
val source: suspend () -> T,
|
||||
val deferred: CompletableDeferred<T>,
|
||||
val priority: Int = 5
|
||||
data class CheveretoQueueItem(
|
||||
val request0: CheveretoRequest,
|
||||
val deferred0: CompletableDeferred<*>,
|
||||
val priority0: Int,
|
||||
var retries0: Int,
|
||||
val expectsResponse0: Boolean
|
||||
) : QueueItem<CheveretoRequest, CheveretoResponse, FailedCheveretoResponse>(
|
||||
request0, deferred0, retries0, priority0, expectsResponse0
|
||||
)
|
||||
|
|
@ -1,4 +1,11 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.data
|
||||
|
||||
class CheveretoSource {
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class CheveretoSource {
|
||||
@Serializable
|
||||
data class ByteArraySource(val bytes: ByteArray, val fileName: String) : CheveretoSource()
|
||||
@Serializable
|
||||
data class UrlSource(val url: String) : CheveretoSource()
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager.chevereto.data
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Success(
|
||||
data class SuccessInfo(
|
||||
val message : String? = null,
|
||||
val code: Int? = 200,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
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>
|
||||
|
|
@ -1,4 +1,89 @@
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,37 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.response
|
||||
|
||||
class CheveretoSuccessResponse {
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||
|
||||
@Serializable
|
||||
abstract class CheveretoResponse (
|
||||
@Transient
|
||||
override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||
@Transient
|
||||
override val createTime: Long = System.currentTimeMillis()
|
||||
) : IResponse {
|
||||
companion object {
|
||||
// 通用的反序列化方法
|
||||
inline fun <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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,12 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.response
|
||||
|
||||
class FailedCheveretoResponse {
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||
|
||||
@Serializable
|
||||
abstract class FailedCheveretoResponse: CheveretoResponse(), IFailedResponse {
|
||||
@Serializable
|
||||
class Default(@Transient override val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, @Transient override val failedMessage: String = "未知错误") : FailedCheveretoResponse()
|
||||
}
|
||||
|
|
@ -1,4 +1,17 @@
|
|||
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()
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,26 @@
|
|||
package top.r3944realms.ltdmanager.core.client
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import top.r3944realms.ltdmanager.core.client.request.IRequest
|
||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
interface IQueueItem<out T:IResponse, out F:IFailedResponse> : Comparable<IQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
||||
fun getRequest(): IRequest<T,F>
|
||||
fun getDeferred(): CompletableFuture<*>
|
||||
fun getRetries(): Int
|
||||
fun getPriority(): Int
|
||||
open class QueueItem<R: IRequest<T, F>, T:IResponse, F:IFailedResponse>(
|
||||
val request: R,
|
||||
val deferred: CompletableDeferred<*>,
|
||||
val retries: 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
|
||||
*/
|
||||
fun expectsResponse(): Boolean
|
||||
override fun compareTo(other: IQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = getPriority().compareTo(other.getPriority())
|
||||
fun expectsResponse(): Boolean = expectsResponse
|
||||
override fun compareTo(other: QueueItem<R, @UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||
}
|
||||
|
|
@ -1,4 +1,76 @@
|
|||
package top.r3944realms.ltdmanager.core.client.request
|
||||
|
||||
interface IRequest {
|
||||
import io.ktor.http.*
|
||||
import top.r3944realms.ltdmanager.core.client.response.IFailedResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.IResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||
|
||||
interface IRequest<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
|
||||
}
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
package top.r3944realms.ltdmanager.core.client.response
|
||||
|
||||
class IFailedResponse {
|
||||
interface IFailedResponse : IResponse {
|
||||
val failedMessage: String
|
||||
val thrownException: Exception
|
||||
get() = Exception(failedMessage)
|
||||
}
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
package top.r3944realms.ltdmanager.core.client.response
|
||||
|
||||
import io.ktor.http.*
|
||||
|
||||
interface IResponse {
|
||||
val httpStatusCode: HttpStatusCode
|
||||
val createTime: Long
|
||||
}
|
||||
|
|
@ -1,4 +1,47 @@
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,54 @@
|
|||
package top.r3944realms.ltdmanager.core.config
|
||||
|
||||
class McsmConfig {
|
||||
import top.r3944realms.ltdmanager.utils.CryptoUtil
|
||||
import top.r3944realms.ltdmanager.utils.YamlUpdater
|
||||
|
||||
data class McsmConfig(
|
||||
var url: String ?= null,
|
||||
var encryptedApiKey: String ?= null,
|
||||
var instanceID: String ?= null,
|
||||
) {
|
||||
val decryptedApiKey: String?
|
||||
get() {
|
||||
if (encryptedApiKey == null) return null
|
||||
if (!isEncrypted()) return encryptedApiKey
|
||||
try {
|
||||
val cipherText = encryptedApiKey!!.substring(4, encryptedApiKey!!.length - 1)
|
||||
return CryptoUtil.decrypt(cipherText)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("API解密失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码(如果未加密),并写回配置文件
|
||||
*/
|
||||
fun encryptApi() {
|
||||
if (encryptedApiKey == null || isEncrypted()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
encryptedApiKey = "ENC(${CryptoUtil.encrypt(encryptedApiKey!!)})"
|
||||
YamlUpdater.updateYaml(
|
||||
YamlConfigLoader.configFilePath.toString(),
|
||||
"mcsm.encrypted-api-key",
|
||||
this.encryptedApiKey!!
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("API加密失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查密码是否已加密
|
||||
*/
|
||||
private fun isEncrypted(): Boolean {
|
||||
return encryptedApiKey != null &&
|
||||
encryptedApiKey!!.startsWith("ENC(") &&
|
||||
encryptedApiKey!!.endsWith(")")
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "McsmConfig(url=$url, api-key=***)"
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ object YamlConfigLoader {
|
|||
config?.database?.encryptPassword()
|
||||
config?.websocket?.encryptToken()
|
||||
config?.http?.encryptToken()
|
||||
config?.mcsm?.encryptApi()
|
||||
config?.mail?.encryptPassword()
|
||||
config?.tools?.rcon?.encryptPassword()
|
||||
config?.blessingSkinServer?.invitationApi?.encryptToken()
|
||||
|
|
@ -72,6 +73,7 @@ object YamlConfigLoader {
|
|||
|
||||
fun loadDatabaseConfig(): DatabaseConfig = config.database
|
||||
fun loadCryptoConfig(): CryptoConfig = config.crypto
|
||||
fun loadMcsmConfig(): McsmConfig = config.mcsm
|
||||
fun loadWebsocketConfig(): WebsocketConfig = config.websocket
|
||||
fun loadHttpConfig(): HttpConfig = config.http
|
||||
fun loadModeConfig(): ModeConfig = config.mode
|
||||
|
|
@ -88,6 +90,7 @@ object YamlConfigLoader {
|
|||
var http: HttpConfig = HttpConfig(),
|
||||
var tools: ToolConfig = ToolConfig(),
|
||||
var mail: MailConfig = MailConfig(),
|
||||
var mcsm: McsmConfig = McsmConfig(),
|
||||
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
|
||||
var dgLab: DgLabConfig = DgLabConfig(),
|
||||
var imgTu: ImgTuConfig = ImgTuConfig(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
class DependencyResolver {
|
||||
class DependencyResolver() {
|
||||
}
|
||||
|
|
@ -1,26 +1,149 @@
|
|||
package top.r3944realms.ltdmanager.core.config
|
||||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
import top.r3944realms.ltdmanager.module.Modules
|
||||
import top.r3944realms.ltdmanager.module.exception.ConfigError
|
||||
|
||||
data class ModuleConfig(
|
||||
val name: String,
|
||||
val type: ModuleType,
|
||||
val enabled: Boolean,
|
||||
val
|
||||
val dependencies: List<Dependency> = emptyList(),
|
||||
val config: Map<String, Any> = emptyMap()
|
||||
) {
|
||||
data class Dependency(
|
||||
val moduleName: String, // 依赖的模块名称
|
||||
val type: DependencyType, // 依赖类型
|
||||
val required: Boolean = true // 是否必需
|
||||
)
|
||||
enum class ModuleType {
|
||||
GROUP_MESSAGE_POLLING_MODULE,
|
||||
GROUP_REQUEST_HANDLER_MODULE,
|
||||
MAIL_MODULE,
|
||||
BAN_MODULE,
|
||||
DG_LAB_MODULE,
|
||||
INVITE_MODULE,
|
||||
MC_SERVER_STATUS_MODULE,
|
||||
RCON_PLAYER_LIST_MODULE,
|
||||
STATE_MODULE
|
||||
private val name: String,
|
||||
val type: ModuleType,
|
||||
val required: Boolean = true
|
||||
) {
|
||||
private val dependencyName: String = "${type.modName}-$name"
|
||||
|
||||
fun getDepName() :String = dependencyName
|
||||
|
||||
}
|
||||
enum class ModuleType(val modName: String) {
|
||||
GROUP_MESSAGE_POLLING_MODULE(Modules.GROUP_MESSAGE_POLLING),
|
||||
GROUP_REQUEST_HANDLER_MODULE(Modules.GROUP_REQUEST_HANDLER),
|
||||
MAIL_MODULE(Modules.MAIL),
|
||||
BAN_MODULE(Modules.BAN),
|
||||
DG_LAB_MODULE(Modules.DG_LAB),
|
||||
INVITE_MODULE(Modules.INVITATION_CODE),
|
||||
MC_SERVER_STATUS_MODULE(Modules.MC_SERVER_STATUS),
|
||||
MOD_GROUP_HANDLER_MODULE(Modules.MOD_GROUP_HANDLER),
|
||||
RCON_PLAYER_LIST_MODULE(Modules.RCON_PLAYER_LIST),
|
||||
STATE_MODULE(Modules.STATE),
|
||||
HELP_MODULE(Modules.HELP),;
|
||||
}
|
||||
// 基础获取方法
|
||||
fun value(paramName: String): Any =
|
||||
config[paramName] ?: throw ConfigError(
|
||||
ConfigError.Type.MISSING_PARAMETER,
|
||||
name,
|
||||
paramName
|
||||
)
|
||||
|
||||
// 泛型获取方法
|
||||
private inline fun <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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,32 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
import top.r3944realms.ltdmanager.GlobalManager
|
||||
import top.r3944realms.ltdmanager.core.init.ModuleConfig.ModuleType.*
|
||||
import top.r3944realms.ltdmanager.module.BaseModule
|
||||
import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule
|
||||
|
||||
object ModuleFactory {
|
||||
fun createModule(config: ModuleConfig): BaseModule {
|
||||
return when(config.type) {
|
||||
GROUP_MESSAGE_POLLING_MODULE -> TODO()
|
||||
GROUP_REQUEST_HANDLER_MODULE -> createGroupRequestHandler(config)
|
||||
MAIL_MODULE -> TODO()
|
||||
BAN_MODULE -> TODO()
|
||||
DG_LAB_MODULE -> TODO()
|
||||
INVITE_MODULE -> TODO()
|
||||
MC_SERVER_STATUS_MODULE -> TODO()
|
||||
RCON_PLAYER_LIST_MODULE -> TODO()
|
||||
STATE_MODULE -> TODO()
|
||||
MOD_GROUP_HANDLER_MODULE -> TODO()
|
||||
HELP_MODULE -> TODO()
|
||||
}
|
||||
}
|
||||
private fun createGroupRequestHandler(config: ModuleConfig): GroupRequestHandlerModule {
|
||||
val targetGroupId = config.long("targetGroupId")
|
||||
val pollIntervalMillis = config.getOrDefault("pollIntervalMillis", 30_000L)
|
||||
return GroupRequestHandlerModule(
|
||||
config.name, GlobalManager.napCatClient,
|
||||
targetGroupId, pollIntervalMillis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
import java.nio.file.Paths
|
||||
|
||||
object ModuleLoader {
|
||||
val configFilePath = Paths.get("config/modules.yaml")
|
||||
init {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,15 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
import top.r3944realms.ltdmanager.module.BaseModule
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import top.r3944realms.ltdmanager.GlobalManager
|
||||
import top.r3944realms.ltdmanager.chevereto.response.FailedCheveretoResponse
|
||||
import top.r3944realms.ltdmanager.chevereto.response.v1.CheveretoUploadResponse
|
||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import top.r3944realms.ltdmanager.napcat.NapCatClient
|
||||
import top.r3944realms.ltdmanager.napcat.data.ID
|
||||
|
|
@ -104,7 +106,7 @@ class GameClientOperation(
|
|||
|
||||
pattern.replace(originalUrl) { matchResult ->
|
||||
// 保留原始 URL 中的路径部分(如果有的话)
|
||||
val path = matchResult.groupValues[1] ?: ""
|
||||
val path = matchResult.groupValues[1]
|
||||
"$configUrl$path"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -130,8 +132,7 @@ class GameClientOperation(
|
|||
albumId = "BFx",
|
||||
expiration = "PT5M"
|
||||
)
|
||||
if (response.image?.url != null) {
|
||||
// 发送图床 URL 给玩家
|
||||
if (response is CheveretoUploadResponse){
|
||||
napCatClient.sendUnit(
|
||||
SendPrivateMsgRequest(
|
||||
listOf(
|
||||
|
|
@ -141,8 +142,15 @@ class GameClientOperation(
|
|||
ID.long(playerId)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
LoggerUtil.logger.error("上传二维码返回 JSON 未包含 URL")
|
||||
} else if (response is FailedCheveretoResponse.Default){
|
||||
napCatClient.sendUnit(
|
||||
SendPrivateMsgRequest(
|
||||
listOf(
|
||||
MessageElement.text("无法上传图片,请联系管理员:${response.httpStatusCode} , ${response.failedMessage}"),
|
||||
),
|
||||
ID.long(playerId)
|
||||
)
|
||||
)
|
||||
}
|
||||
// 启动 60 秒倒计时任务
|
||||
bindingTimeoutJob = launch {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager
|
|||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import top.r3944realms.ltdmanager.module.*
|
||||
|
||||
|
||||
// DSL
|
||||
fun main() = GlobalManager.runBlockingMain {
|
||||
val commonGroupId:Long = 538751386
|
||||
val whitelistGroupId:Long = 920719236
|
||||
|
|
@ -43,7 +43,7 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
)
|
||||
val toolConfig = YamlConfigLoader.loadToolConfig()
|
||||
val corconModule = RconPlayerListModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
moduleName = "CommonGroup",
|
||||
groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||
rconTimeOut = 2_000L,
|
||||
cooldownMillis = 10_000L,
|
||||
|
|
@ -98,30 +98,30 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
// "Apply for an invitation code"
|
||||
// )
|
||||
// )
|
||||
val commonMcServerStatusModule = McServerStatusModule(
|
||||
moduleName = "CommonGroup",
|
||||
groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||
selfId = selfQQId,
|
||||
cooldownMillis = 20_000L,
|
||||
selfNickName = selfNickName,
|
||||
commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
||||
presetServer = mapOf(
|
||||
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||
)
|
||||
)
|
||||
val whitelistMcServerStatusModule = McServerStatusModule(
|
||||
moduleName = "WhiteListGroup",
|
||||
groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
||||
selfId = selfQQId,
|
||||
cooldownMillis = 20_000L,
|
||||
selfNickName = selfNickName,
|
||||
commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
||||
presetServer = mapOf(
|
||||
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||
)
|
||||
)
|
||||
// val commonMcServerStatusModule = McServerStatusModule(
|
||||
// moduleName = "CommonGroup",
|
||||
// groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||
// selfId = selfQQId,
|
||||
// cooldownMillis = 20_000L,
|
||||
// selfNickName = selfNickName,
|
||||
// commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
||||
// presetServer = mapOf(
|
||||
// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||
// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||
// )
|
||||
// )
|
||||
// val whitelistMcServerStatusModule = McServerStatusModule(
|
||||
// moduleName = "WhiteListGroup",
|
||||
// groupMessagePollingModule = whiteListGroupMsgPollingModule,
|
||||
// selfId = selfQQId,
|
||||
// cooldownMillis = 20_000L,
|
||||
// selfNickName = selfNickName,
|
||||
// commands = listOf("/m", "/mcs", "seek", "s", "test"),
|
||||
// presetServer = mapOf(
|
||||
// setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
|
||||
// setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
|
||||
// )
|
||||
// )
|
||||
val dgLabModule = DGLabModule(
|
||||
moduleName = "DG",
|
||||
groupMessagePollingModule = commonGroupMsgPollingModule,
|
||||
|
|
@ -134,10 +134,10 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
GlobalManager.moduleManager.registerModule(groupModule)
|
||||
GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule)
|
||||
GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule)
|
||||
GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
|
||||
// GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
|
||||
GlobalManager.moduleManager.registerModule(rconModule)
|
||||
GlobalManager.moduleManager.registerModule(corconModule)
|
||||
GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
|
||||
// GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
|
||||
// GlobalManager.moduleManager.registerModule(mailModule)
|
||||
// GlobalManager.moduleManager.registerModule(invitationCodesModule)
|
||||
GlobalManager.moduleManager.registerModule(whitelistHelpModule)
|
||||
|
|
@ -150,13 +150,13 @@ fun main() = GlobalManager.runBlockingMain {
|
|||
GlobalManager.moduleManager.loadModule(groupModule.name)
|
||||
GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name)
|
||||
GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name)
|
||||
GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
|
||||
// GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
|
||||
GlobalManager.moduleManager.loadModule(corconModule.name)
|
||||
GlobalManager.moduleManager.loadModule(rconModule.name)
|
||||
// GlobalManager.moduleManager.loadModule(mailModule.name)
|
||||
// GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
|
||||
GlobalManager.moduleManager.loadModule(commonHelpModule.name)
|
||||
GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
|
||||
// GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
|
||||
GlobalManager.moduleManager.loadModule(whitelistHelpModule.name)
|
||||
GlobalManager.moduleManager.loadModule(dgLabModule.name)
|
||||
// GlobalManager.moduleManager.loadModule(banModule.name)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin
|
||||
package top.r3944realms.ltdmanager.mcms
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
|
|
@ -11,17 +11,17 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
|
||||
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.utils.Environment
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
|
||||
class BlessingSkinClient private constructor() : AutoCloseable {
|
||||
class MCSMClient private constructor() : AutoCloseable {
|
||||
private val client = HttpClient(CIO) {
|
||||
expectSuccess = false
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
// 限流控制
|
||||
private val semaphore = Semaphore(5)
|
||||
private val requestMutex = Mutex()
|
||||
private val requestQueue = PriorityQueue<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority })
|
||||
private val requestQueue = PriorityQueue<MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>>(compareBy { it.priority })
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
|
|
@ -50,14 +50,14 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
/**
|
||||
* 提交请求
|
||||
*/
|
||||
suspend fun <T : BlessingSkinResponse, F : FailedBlessingSkinResponse> submitRequest(
|
||||
request: BlessingSkinRequest<T, F>,
|
||||
suspend fun <T : MCSMResponse, F : FailedMCSMResponse> submitRequest(
|
||||
request: MCSMRequest<T, F>,
|
||||
priority: Int = 5,
|
||||
maxRetries: Int = 3
|
||||
): ResponseResult<T, F> {
|
||||
val deferred = CompletableDeferred<ResponseResult<T, F>>()
|
||||
requestMutex.withLock {
|
||||
requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true))
|
||||
requestQueue.add(MCSMSkinQueueItem(request, deferred, priority, maxRetries, true))
|
||||
}
|
||||
return deferred.await()
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
/**
|
||||
* 处理队列项
|
||||
*/
|
||||
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) {
|
||||
private suspend fun processQueueItem(item: MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>) {
|
||||
semaphore.withPermit {
|
||||
val (request, deferred, _, maxRetries, _) = item
|
||||
var attempt = 0
|
||||
|
|
@ -130,7 +130,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
val result = request.getResponse(responseText, response.status)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(result)
|
||||
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(result)
|
||||
|
||||
return
|
||||
|
||||
|
|
@ -142,15 +142,15 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
break
|
||||
}
|
||||
|
||||
LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||
LoggerUtil.logger.warn("MCSM请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
|
||||
delay((attempt * 1000L)) // 指数退避
|
||||
}
|
||||
}
|
||||
|
||||
// 所有重试都失败或不应重试
|
||||
val errorResponse = createFailureResponse(lastError, request)
|
||||
val errorResponse = createFailureResponse(lastError)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(deferred as CompletableDeferred<ResponseResult<BlessingSkinResponse, FailedBlessingSkinResponse>>).complete(
|
||||
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(
|
||||
ResponseResult.Failure(errorResponse)
|
||||
)
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
/**
|
||||
* 构建完整的URL,包含查询参数
|
||||
*/
|
||||
private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String {
|
||||
private fun buildFullUrlWithQueryParams(request: MCSMRequest<*, *>): String {
|
||||
val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/")
|
||||
val path = request.path().removePrefix("/")
|
||||
|
||||
|
|
@ -191,11 +191,10 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
* 创建失败响应
|
||||
*/
|
||||
private fun createFailureResponse(
|
||||
exception: Exception?,
|
||||
request: BlessingSkinRequest<*, *>
|
||||
): FailedBlessingSkinResponse {
|
||||
return FailedBlessingSkinResponse.Default(
|
||||
failedResult = exception?.message ?: "未知错误",
|
||||
exception: Exception?
|
||||
): FailedMCSMResponse {
|
||||
return FailedMCSMResponse.ExceptionFailedMCSMResponse(
|
||||
result = exception?.message ?: "未知错误",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -207,6 +206,6 @@ class BlessingSkinClient private constructor() : AutoCloseable {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun create(): BlessingSkinClient = BlessingSkinClient()
|
||||
fun create(): MCSMClient = MCSMClient()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin
|
||||
package top.r3944realms.ltdmanager.mcms
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import top.r3944realms.ltdmanager.blessingskin.request.BlessingSkinRequest
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.mcms.request.MCSMRequest
|
||||
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||
|
||||
data class BlessingSkinQueueItem<out T:BlessingSkinResponse,out F:FailedBlessingSkinResponse>(
|
||||
val request: BlessingSkinRequest<T,F>,
|
||||
data class MCSMSkinQueueItem<out T:MCSMResponse,out F:FailedMCSMResponse>(
|
||||
val request: MCSMRequest<T,F>,
|
||||
val deferred: CompletableDeferred<*>,
|
||||
var retries: Int,
|
||||
val priority: Int,
|
||||
val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||
) : Comparable<BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
||||
override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||
) : Comparable<MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>> {
|
||||
override fun compareTo(other: MCSMSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.request
|
||||
package top.r3944realms.ltdmanager.mcms.request
|
||||
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.BlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.FailedBlessingSkinResponse
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.mcms.response.FailedMCSMResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.ResponseResult
|
||||
|
||||
|
||||
@Serializable
|
||||
abstract class BlessingSkinRequest<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>(
|
||||
abstract class MCSMRequest<out T : MCSMResponse, out F : FailedMCSMResponse>(
|
||||
@Transient
|
||||
open val createTime: Long = System.currentTimeMillis()
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,50 @@
|
|||
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!!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,53 @@
|
|||
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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,29 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.response
|
||||
package top.r3944realms.ltdmanager.mcms.response
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
abstract class FailedBlessingSkinResponse: BlessingSkinResponse() {
|
||||
abstract fun failedMessage(): String
|
||||
open class FailedMCSMResponse(
|
||||
@Transient
|
||||
val status0: Status = Status.Ok,
|
||||
val data: JsonObject? = null,
|
||||
@Transient
|
||||
val time0: Long = -1,
|
||||
): MCSMResponse(
|
||||
status0, time0
|
||||
) {
|
||||
@Serializable
|
||||
class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() {
|
||||
override fun failedMessage(): String = failedResult!!
|
||||
|
||||
}
|
||||
data class ExceptionFailedMCSMResponse(
|
||||
@Transient
|
||||
val status1: Status = Status.Ok,
|
||||
val data0: String? = null,
|
||||
@Transient
|
||||
val time1: Long = -1,
|
||||
@Transient
|
||||
val result: String? = null,
|
||||
): FailedMCSMResponse(
|
||||
status1, null, time1
|
||||
)
|
||||
}
|
||||
|
|
@ -1,15 +1,20 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.response
|
||||
package top.r3944realms.ltdmanager.mcms.response
|
||||
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.instance.GetInstanceListResponse
|
||||
import top.r3944realms.ltdmanager.mcms.response.instance.StartInstanceResponse
|
||||
|
||||
|
||||
@Serializable
|
||||
abstract class BlessingSkinResponse (
|
||||
abstract class MCSMResponse (
|
||||
open val status: Status,
|
||||
open val time: Long,
|
||||
@Transient
|
||||
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
|
||||
@Transient
|
||||
|
|
@ -17,20 +22,29 @@ abstract class BlessingSkinResponse (
|
|||
) {
|
||||
companion object {
|
||||
// 通用的反序列化方法
|
||||
inline fun <reified T : BlessingSkinResponse> decode(jsonString: String): T {
|
||||
inline fun <reified T : MCSMResponse> decode(jsonString: String): T {
|
||||
return json.decodeFromString(jsonString)
|
||||
}
|
||||
val json: Json by lazy {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule = SerializersModule {
|
||||
polymorphic(BlessingSkinResponse::class) {
|
||||
subclass(FailedBlessingSkinResponse.Default::class, FailedBlessingSkinResponse.Default.serializer())
|
||||
subclass(InvitationCodeGenerationResponse::class, InvitationCodeGenerationResponse.serializer())
|
||||
polymorphic(MCSMResponse::class) {
|
||||
subclass(GetInstanceListResponse::class, GetInstanceListResponse.serializer())
|
||||
subclass(StartInstanceResponse::class, StartInstanceResponse.serializer())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@Serializable
|
||||
enum class Status(val value: String) {
|
||||
@SerialName("200") Ok("200"),
|
||||
@SerialName("400") ParamsNotRight("400"),
|
||||
@SerialName("403") PermissionDenied("403"),
|
||||
@SerialName("500") InternalServerError("500");
|
||||
companion object {
|
||||
fun isOk(value: Status): Boolean = value == Ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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> {
|
||||
data class Success<T : BlessingSkinResponse>(val response: T) : ResponseResult<T, Nothing>()
|
||||
data class Failure<F : FailedBlessingSkinResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
||||
sealed class ResponseResult<out T : MCSMResponse, out F : FailedMCSMResponse> {
|
||||
data class Success<T : MCSMResponse>(val response: T) : ResponseResult<T, Nothing>()
|
||||
data class Failure<F : FailedMCSMResponse>(val failure: F) : ResponseResult<Nothing, F>()
|
||||
|
||||
/**
|
||||
* 检查是否成功
|
||||
|
|
|
|||
|
|
@ -1,11 +1,60 @@
|
|||
package top.r3944realms.ltdmanager.mcms.response.instance
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import top.r3944realms.ltdmanager.mcms.response.MCSMResponse
|
||||
|
||||
@Serializable
|
||||
data class InstanceListResponse(
|
||||
val status: Int,
|
||||
val data: InstanceListData?,
|
||||
val time: Long
|
||||
) : MCSMResponse
|
||||
data class GetInstanceListResponse(
|
||||
@Transient
|
||||
val status0: Status = Status.Ok,
|
||||
val data: InstanceListData? = null,
|
||||
@Transient
|
||||
val time0: Long = -1,
|
||||
) : MCSMResponse(status0, time0) {
|
||||
@Serializable
|
||||
data class InstanceListData(
|
||||
val maxPage: Int,
|
||||
val pageSize: Int,
|
||||
val data: List<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
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,21 @@
|
|||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
class ApplyWhiteListModule {
|
||||
class ApplyWhitelistModule(
|
||||
moduleName: String,
|
||||
private val groupMessagePollingModule: GroupMessagePollingModule,
|
||||
private val cooldownMillis: Long = 120_000,
|
||||
private val keywords: Set<String> = setOf("申请白名单")
|
||||
):
|
||||
BaseModule(Modules.APPLY_WHITELIST,moduleName) {
|
||||
override fun onLoad() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun onUnload() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ class BanModule(
|
|||
private val maxBanMinutes: Int = 15,
|
||||
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 pardonCommandParse = CommandParser(unmuteCommandPrefixList)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.intellij.lang.annotations.MagicConstant
|
||||
import top.r3944realms.ltdmanager.GlobalManager
|
||||
import top.r3944realms.ltdmanager.utils.LoggerUtil
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class DGLabModule(
|
|||
val adminIds: List<Long> = listOf(),
|
||||
val maxClientNumber: Int = 10,
|
||||
val commandHead: List<String> = listOf("dglab"),
|
||||
) : BaseModule("DGLabModule", moduleName), PersistentState<DGLabModule.DgLabState> {
|
||||
) : BaseModule(Modules.DG_LAB, moduleName), PersistentState<DGLabModule.DgLabState> {
|
||||
|
||||
var dgLabManager: DgLab? = null
|
||||
private var scope: CoroutineScope? = null
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class GroupMessagePollingModule(
|
|||
val targetGroupId: Long,
|
||||
private val pollIntervalMillis: Long = 5_000L,
|
||||
private val msgHistoryCheck: Int = 15,
|
||||
) : BaseModule("MessagePollingModule", moduleName) {
|
||||
) : BaseModule(Modules.GROUP_MESSAGE_POLLING, moduleName) {
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
// 用 Flow 存消息,其他模块可以订阅
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class GroupRequestHandlerModule(
|
|||
private val client: NapCatClient,
|
||||
private val targetGroupId: Long,
|
||||
private val pollIntervalMillis: Long = 30_000L,
|
||||
) : BaseModule("GroupRequestHandlerModule", moduleName) {
|
||||
) : BaseModule(Modules.GROUP_REQUEST_HANDLER, moduleName) {
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class HelpModule(
|
|||
private val selfNickName: String,
|
||||
private val keywords: List<String> = listOf("help", "帮助"),
|
||||
private val cooldownMillis: Long = 30_000L
|
||||
) : BaseModule("HelpModule", moduleName), PersistentState<HelpModule.HelpState> {
|
||||
) : BaseModule(Modules.HELP, moduleName), PersistentState<HelpModule.HelpState> {
|
||||
|
||||
// 命令解析器
|
||||
private val commandParser = CommandParser(keywords)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import kotlinx.coroutines.*
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.r3944realms.ltdmanager.blessingskin.data.InvitationCode
|
||||
import top.r3944realms.ltdmanager.blessingskin.request.invitecode.GenerateInvitationCodeRequest
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.blessingskin.response.invitecode.InvitationCodeGenerationResponse
|
||||
import top.r3944realms.ltdmanager.core.client.response.ResponseResult
|
||||
import top.r3944realms.ltdmanager.core.mail.mail
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
|
||||
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownScope
|
||||
|
|
@ -77,7 +78,7 @@ class InvitationCodesModule(
|
|||
selfId: Long,
|
||||
private val cooldownMillis: Long = 120_000,
|
||||
private val keywords: Set<String> = setOf("申请邀请码")
|
||||
) : BaseModule("InvitationCodesModule", moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
||||
) : BaseModule(Modules.INVITATION_CODE, moduleName), PersistentState<InvitationCodesModule.LastTriggerMapState> {
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
private val stateFile: File = getStateFileInternal("invitation_codes_quarry_state.json", name)
|
||||
|
|
@ -430,23 +431,33 @@ class InvitationCodesModule(
|
|||
/**
|
||||
* 1. 创建邀请码
|
||||
*/
|
||||
private suspend fun createInvitationCodes(amount: Int): List<InvitationCodeGenerationResponse.InvitationCode>? {
|
||||
private suspend fun createInvitationCodes(amount: Int): List<InvitationCode>? {
|
||||
return try {
|
||||
val response = blessingSkinClient.submitRequest(
|
||||
GenerateInvitationCodeRequest(amount = amount, token = apiToken)
|
||||
)
|
||||
response
|
||||
.onFailure {
|
||||
|
||||
}
|
||||
.onSuccess {
|
||||
|
||||
}
|
||||
when (response) {
|
||||
is ResponseResult.Success -> {
|
||||
if (response.response.success) {
|
||||
response.response.data
|
||||
if (response.response is InvitationCodeGenerationResponse) {
|
||||
if (response.response.success) {
|
||||
response.response.data
|
||||
} else
|
||||
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
|
||||
null
|
||||
} else {
|
||||
LoggerUtil.logger.warn("[$name] API返回失败: ${response.response.message}")
|
||||
LoggerUtil.logger.warn("[$name] 返回非预期对象类型: ${response.response.javaClass}")
|
||||
null
|
||||
}
|
||||
}
|
||||
is ResponseResult.Failure -> {
|
||||
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedResult}")
|
||||
LoggerUtil.logger.warn("[$name] 创建邀请码失败: ${response.failure.failedMessage}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -460,7 +471,7 @@ class InvitationCodesModule(
|
|||
* 2. 验证数量匹配
|
||||
*/
|
||||
private fun validateCodeCountMatch(
|
||||
invitationCodes: List<InvitationCodeGenerationResponse.InvitationCode>?,
|
||||
invitationCodes: List<InvitationCode>?,
|
||||
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>
|
||||
) {
|
||||
if (invitationCodes == null) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class MailModule(
|
|||
private val enableAuth: Boolean = true,
|
||||
private val enableTLS: Boolean = true,
|
||||
private val intervalMillis: Long = 2000L // 每封邮件之间的间隔(默认 2s)
|
||||
) : BaseModule("MailModule", moduleName) {
|
||||
) : BaseModule(Modules.MAIL, moduleName) {
|
||||
|
||||
private lateinit var session: Session
|
||||
private val queue = LinkedBlockingQueue<Mail>() // 邮件队列
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class McServerStatusModule(
|
|||
setOf("hp", "hypixel") to "mc.hypixel.net",
|
||||
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 stateBackupFile:File = getStateFileInternal("mc_server_status_state.json.bak", name)
|
||||
private val commandParser: CommandParser = CommandParser(commands)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class ModGroupHandlerModule(
|
|||
private val targetGroupId: Long,
|
||||
private val answers: List<String> = listOf("正确答案"),
|
||||
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 val stateFile: File = getStateFileInternal("reject_records.json", name)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,26 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
import java.util.*
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ class RconPlayerListModule(
|
|||
private val rconPath: String,
|
||||
private val rconConfigPath: String,
|
||||
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
|
||||
) : BaseModule("RconPlayerListModule", moduleName), PersistentState<LastTriggerState> {
|
||||
) : BaseModule(Modules.RCON_PLAYER_LIST, moduleName), PersistentState<LastTriggerState> {
|
||||
private val cooldownManager by lazy {
|
||||
CooldownManager(
|
||||
cooldownMillis = cooldownMillis,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class StateModule(
|
|||
moduleName: String,
|
||||
private val onlineName: String,
|
||||
private val offlineName: String,
|
||||
): BaseModule("StateModule", moduleName) {
|
||||
): BaseModule(Modules.STATE, moduleName) {
|
||||
private var scope: CoroutineScope? = null
|
||||
override fun onLoad() {
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
package top.r3944realms.ltdmanager.module.exception
|
||||
|
||||
class InvalidConfigException: Exception() {
|
||||
enum class Type(template: String) {
|
||||
|
||||
class ConfigError(type: Type = Type.OTHER, private val pos: String, vararg args: Any) : Exception() {
|
||||
private val errorType: Type = type
|
||||
private val arguments = args
|
||||
|
||||
override val message: String
|
||||
get() = String.format(errorType.template, *arguments, pos)
|
||||
|
||||
|
||||
enum class Type(val template: String) {
|
||||
INVALID_PARAMETER("Invalid Parameter: %s in %s."),
|
||||
MISSING_PARAMETER("Missing Parameter: %s in %s."),
|
||||
NOT_EXPECTED_VALUE("Expect for %s but was %s in %s."),
|
||||
OTHER("%s in %s")
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ object ConfigInitializer {
|
|||
* @param fileName YAML 文件名,如 application.yml
|
||||
* @param configDir 配置目录,如 config
|
||||
*/
|
||||
fun initConfig(fileName: String = "application.yml", configDir: String = "config") {
|
||||
fun initConfig(fileName: String = "application.yml", configDir: String = "config", shouldExit: Boolean = true) {
|
||||
val dirPath = Paths.get(configDir)
|
||||
if (!Files.exists(dirPath)) {
|
||||
Files.createDirectories(dirPath)
|
||||
|
|
@ -28,8 +28,10 @@ object ConfigInitializer {
|
|||
if (resourceStream != null) {
|
||||
Files.copy(resourceStream, filePath, StandardCopyOption.REPLACE_EXISTING)
|
||||
LoggerUtil.logger.info("已生成默认配置文件: $filePath")
|
||||
LoggerUtil.logger.info("第一次启动,请修改配置后再启动")
|
||||
exitProcess(-1);
|
||||
if (shouldExit) {
|
||||
LoggerUtil.logger.info("第一次启动,请修改配置后再启动")
|
||||
exitProcess(-1);
|
||||
}
|
||||
} else throw Error("Jar内部资源文件缺失")
|
||||
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ websocket:
|
|||
url: "wss://127.0.0.1:3002"
|
||||
# 格式为 ENC(XXX),若不是则会在加载完成配置后自动加密
|
||||
encrypted-token: "123123cc"
|
||||
mcsm:
|
||||
|
||||
tools:
|
||||
rcon:
|
||||
mc-rcon-tool-path: "/path/to/rcon"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user