diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt new file mode 100644 index 0000000..b780f44 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/blessingskin/data/InvitationCode.kt @@ -0,0 +1,2 @@ +package top.r3944realms.ltdmanager.blessingskin.data + diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt deleted file mode 100644 index 1124a07..0000000 --- a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package top.r3944realms.ltdmanager.chevereto.data - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class CheveretoResponse( - @SerialName("status_code") - val statusCode: Int, - val success: Success? = null, - val image: CheveretoImage? = null, - @SerialName("status_txt") - val statusTxt:String ?= null -) \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt new file mode 100644 index 0000000..37a668a --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/CheveretoSource.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.chevereto.data + +class CheveretoSource { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Success.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/SuccessInfo.kt similarity index 100% rename from src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/Success.kt rename to src/main/kotlin/top/r3944realms/ltdmanager/chevereto/data/SuccessInfo.kt diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt new file mode 100644 index 0000000..048505a --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/CheveretoRequest.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.chevereto.request + +class CheveretoRequest { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt new file mode 100644 index 0000000..750991e --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/request/v1/CheveretoUploadRequest.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.chevereto.request.v1 + +class CheveretoUploadRequest { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt new file mode 100644 index 0000000..02f6408 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/CheveretoResponse.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.chevereto.response + +class CheveretoSuccessResponse { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt new file mode 100644 index 0000000..5c24b77 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/FailedCheveretoResponse.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.chevereto.response + +class FailedCheveretoResponse { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt new file mode 100644 index 0000000..c33d124 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/chevereto/response/v1/CheveretoUploadResponse.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.chevereto.response.v1 + +class CheveretoUploadResponse { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt new file mode 100644 index 0000000..147cc82 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/IClient.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.basic + +interface IClient { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt new file mode 100644 index 0000000..abaa104 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/QueueItem.kt @@ -0,0 +1,19 @@ +package top.r3944realms.ltdmanager.core.client + +import top.r3944realms.ltdmanager.core.client.request.IRequest +import top.r3944realms.ltdmanager.core.client.response.IFailedResponse +import top.r3944realms.ltdmanager.core.client.response.IResponse +import java.util.concurrent.CompletableFuture + +interface IQueueItem : Comparable> { + fun getRequest(): IRequest + fun getDeferred(): CompletableFuture<*> + fun getRetries(): Int + fun getPriority(): Int + + /** + * @return true 表示返回 BlessingSkinResponse, false 表示 Unit + */ + fun expectsResponse(): Boolean + override fun compareTo(other: IQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = getPriority().compareTo(other.getPriority()) +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt new file mode 100644 index 0000000..d55515d --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/request/IRequest.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.client.request + +interface IRequest { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt new file mode 100644 index 0000000..499a813 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IFailedResponse.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.client.response + +class IFailedResponse { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt new file mode 100644 index 0000000..b64f017 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/IResponse.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.client.response + +interface IResponse { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt new file mode 100644 index 0000000..7ef41a8 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/client/response/ResponseResult.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.client.response + +class ResponseResult { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt new file mode 100644 index 0000000..4b9acbe --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/McsmConfig.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.config + +class McsmConfig { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt new file mode 100644 index 0000000..b74eea3 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/DependencyResolver.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.init + +class DependencyResolver { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt new file mode 100644 index 0000000..ac6e6cd --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleConfig.kt @@ -0,0 +1,26 @@ +package top.r3944realms.ltdmanager.core.config + + +data class ModuleConfig( + val name: String, + val type: ModuleType, + val enabled: Boolean, + val +) { + 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 + } +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt new file mode 100644 index 0000000..8b14c6e --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleFactory.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.init + +object ModuleFactory { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt new file mode 100644 index 0000000..8b748ce --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleLoader.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.init + +object ModuleLoader { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt new file mode 100644 index 0000000..a5246b5 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/init/ModuleRegistry.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.core.init + +object ModuleRegistry { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt new file mode 100644 index 0000000..451b7cb --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt @@ -0,0 +1,212 @@ +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.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.utils.Environment +import top.r3944realms.ltdmanager.utils.LoggerUtil +import java.net.URLEncoder +import java.util.* + +class BlessingSkinClient private constructor() : AutoCloseable { + private val client = HttpClient(CIO) { + expectSuccess = false + + // 安装 HttpTimeout 插件 + install(HttpTimeout) { + // 默认超时配置,会被具体请求的配置覆盖 + requestTimeoutMillis = 30000 + connectTimeoutMillis = 10000 + socketTimeoutMillis = 15000 + } + + } + + private val blessingSkinServerConfig = YamlConfigLoader.loadBlessingSkinServerConfig() + + // 限流控制 + private val semaphore = Semaphore(5) + private val requestMutex = Mutex() + private val requestQueue = PriorityQueue>(compareBy { it.priority }) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + startQueueProcessor() + } + + /** + * 提交请求 + */ + suspend fun submitRequest( + request: BlessingSkinRequest, + priority: Int = 5, + maxRetries: Int = 3 + ): ResponseResult { + val deferred = CompletableDeferred>() + requestMutex.withLock { + requestQueue.add(BlessingSkinQueueItem(request, deferred, priority, maxRetries, true)) + } + return deferred.await() + } + + /** + * 启动队列处理器 + */ + private fun startQueueProcessor() { + scope.launch { + while (isActive) { + val item = requestMutex.withLock { + requestQueue.poll() + } + if (item == null) { + delay(50) + continue + } + processQueueItem(item) + } + } + } + + /** + * 处理队列项 + */ + private suspend fun processQueueItem(item: BlessingSkinQueueItem) { + semaphore.withPermit { + val (request, deferred, _, maxRetries, _) = item + var attempt = 0 + var lastError: Exception? = null + + while (attempt < maxRetries) { + try { + // 构建完整的URL,包括查询参数 + val fullUrl = buildFullUrlWithQueryParams(request) + + if (!Environment.isProduction()) { + LoggerUtil.logger.debug("发送请求到: $fullUrl") + LoggerUtil.logger.debug("请求方法: {}", request.method()) + } + + val response = client.request(fullUrl) { + method = request.method() + + + // 设置请求头 + headers { + request.headers().invoke(this) + } + + // 对于非GET请求,设置请求体 + if (request.method() != HttpMethod.Get) { + setBody(request.toJSON()) + } + } + + val responseText: String = response.body() + + if (!Environment.isProduction()) { + LoggerUtil.logger.debug("响应状态: {}", response.status) + LoggerUtil.logger.debug("响应内容: $responseText") + } + + // 检查是否是HTML响应(重定向) + if (isHtmlResponse(responseText)) { + throw IllegalStateException("接收到HTML重定向响应,请检查API URL配置") + } + + // 解析响应 + val result = request.getResponse(responseText, response.status) + + @Suppress("UNCHECKED_CAST") + (deferred as CompletableDeferred>).complete(result) + + return + + } catch (e: Exception) { + lastError = e + attempt++ + + if (!request.shouldRetryOnFailure() || attempt >= maxRetries) { + break + } + + LoggerUtil.logger.warn("BlessingSkin请求失败 (尝试 $attempt/$maxRetries): ${e.message}") + delay((attempt * 1000L)) // 指数退避 + } + } + + // 所有重试都失败或不应重试 + val errorResponse = createFailureResponse(lastError, request) + @Suppress("UNCHECKED_CAST") + (deferred as CompletableDeferred>).complete( + ResponseResult.Failure(errorResponse) + ) + } + } + + /** + * 构建完整的URL,包含查询参数 + */ + private fun buildFullUrlWithQueryParams(request: BlessingSkinRequest<*, *>): String { + val baseUrl = blessingSkinServerConfig.url?.removeSuffix("/") + val path = request.path().removePrefix("/") + + // 构建基础URL + val urlBuilder = StringBuilder("$baseUrl/$path") + + // 添加查询参数 + val queryParams = request.queryParameters().entries.joinToString("&") { (key, value) -> + "${URLEncoder.encode(key, "UTF-8")}=${URLEncoder.encode(value, "UTF-8")}" + } + + if (queryParams.isNotEmpty()) { + urlBuilder.append("?").append(queryParams) + } + + return urlBuilder.toString() + } + + /** + * 检查是否是HTML响应 + */ + private fun isHtmlResponse(text: String): Boolean { + return text.contains("", ignoreCase = true) || + text.contains("", ignoreCase = true) || + text.contains("Redirecting", ignoreCase = true) + } + + /** + * 创建失败响应 + */ + private fun createFailureResponse( + exception: Exception?, + request: BlessingSkinRequest<*, *> + ): FailedBlessingSkinResponse { + return FailedBlessingSkinResponse.Default( + failedResult = exception?.message ?: "未知错误", + ) + } + + override fun close() { + scope.cancel() + runBlocking { + client.close() + } + } + + companion object { + fun create(): BlessingSkinClient = BlessingSkinClient() + } +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt new file mode 100644 index 0000000..51ab78d --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMSkinQueueItem.kt @@ -0,0 +1,16 @@ +package top.r3944realms.ltdmanager.blessingskin + +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 + +data class BlessingSkinQueueItem( + val request: BlessingSkinRequest, + val deferred: CompletableDeferred<*>, + var retries: Int, + val priority: Int, + val expectsResponse: Boolean // true 表示返回 BlessingSkinResponse, false 表示 Unit +) : Comparable> { + override fun compareTo(other: BlessingSkinQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = priority.compareTo(other.priority) +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt new file mode 100644 index 0000000..8ace575 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/MCSMRequest.kt @@ -0,0 +1,79 @@ +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 + +@Serializable +abstract class BlessingSkinRequest( + @Transient + open val createTime: Long = System.currentTimeMillis() +) { + /** + * 转换为JSON字符串 + */ + abstract fun toJSON(): String + + /** + * 获取API路径(不包含基础URL) + * 例如: "invitation-codes/generate" + */ + abstract fun path(): String + + /** + * 获取HTTP方法,默认为GET(因为大多数API使用GET+查询参数) + */ + open fun method(): HttpMethod = HttpMethod.Get + + /** + * 自定义请求头 + */ + open fun headers(): HeadersBuilder.() -> Unit = { + // 默认添加Content-Type + append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + // 添加Accept头 + append(HttpHeaders.Accept, "application/json") + } + + /** + * 获取查询参数(用于URL参数) + * 例如: mapOf("token" to "abc123", "amount" to "1") + */ + open fun queryParameters(): Map = emptyMap() + + /** + * 获取请求体参数(用于POST请求的JSON body) + * 例如: mapOf("token" to "abc123", "amount" to 1) + */ + open fun bodyParameters(): Map = emptyMap() + + /** + * 获取请求体内容类型,默认为Application.Json + */ + open fun contentType(): ContentType = ContentType.Application.Json + + /** + * 解析响应JSON字符串 + * @param responseJson 响应JSON字符串 + * @param httpStatusCode HTTP状态码 + */ + abstract fun getResponse(responseJson: String, httpStatusCode: HttpStatusCode): ResponseResult + + /** + * 获取预期的成功响应类型名称(用于日志和调试) + */ + abstract fun expectedResponseType(): String + + /** + * 获取预期的失败响应类型名称(用于日志和调试) + */ + abstract fun expectedFailureType(): String + + /** + * 是否需要在失败时重试(默认重试) + */ + open fun shouldRetryOnFailure(): Boolean = true +} diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt new file mode 100644 index 0000000..66cb400 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/GetInstanceListRequest.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.mcms.request.instance + +class GetInstanceListRequest { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt new file mode 100644 index 0000000..01e283b --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/request/instance/StartInstanceRequest.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.mcms.request.instance + +class StartInstanceRequest { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt new file mode 100644 index 0000000..2c0a32e --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/FailedMCSMResponse.kt @@ -0,0 +1,14 @@ +package top.r3944realms.ltdmanager.blessingskin.response + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +abstract class FailedBlessingSkinResponse: BlessingSkinResponse() { + abstract fun failedMessage(): String + @Serializable + class Default(@Transient val failedResult: String? = "未知错误") : FailedBlessingSkinResponse() { + override fun failedMessage(): String = failedResult!! + + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt new file mode 100644 index 0000000..1a048c5 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/MCSMResponse.kt @@ -0,0 +1,36 @@ +package top.r3944realms.ltdmanager.blessingskin.response + +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.blessingskin.response.invitecode.InvitationCodeGenerationResponse + +@Serializable +abstract class BlessingSkinResponse ( + @Transient + open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + @Transient + open val createTime: Long = System.currentTimeMillis() +) { + companion object { + // 通用的反序列化方法 + inline fun decode(jsonString: String): T { + return json.decodeFromString(jsonString) + } + val json: Json by lazy { + Json { + ignoreUnknownKeys = true + serializersModule = SerializersModule { + polymorphic(BlessingSkinResponse::class) { + subclass(FailedBlessingSkinResponse.Default::class, FailedBlessingSkinResponse.Default.serializer()) + subclass(InvitationCodeGenerationResponse::class, InvitationCodeGenerationResponse.serializer()) + } + } + } + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt new file mode 100644 index 0000000..23a037a --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/ResponseResult.kt @@ -0,0 +1,38 @@ +package top.r3944realms.ltdmanager.blessingskin.response + +// 响应结果封装 +sealed class ResponseResult { + data class Success(val response: T) : ResponseResult() + data class Failure(val failure: F) : ResponseResult() + + /** + * 检查是否成功 + */ + fun isSuccess(): Boolean = this is Success + + /** + * 获取成功响应(如果存在) + */ + fun getSuccessResponse(): T? = (this as? Success)?.response + + /** + * 获取失败响应(如果存在) + */ + fun getFailureResponse(): F? = (this as? Failure)?.failure + + /** + * 成功时执行操作 + */ + inline fun onSuccess(action: (T) -> Unit): ResponseResult { + if (this is Success) action(response) + return this + } + + /** + * 失败时执行操作 + */ + inline fun onFailure(action: (F) -> Unit): ResponseResult { + if (this is Failure) action(failure) + return this + } +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt new file mode 100644 index 0000000..26961eb --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/GetInstanceListResponse.kt @@ -0,0 +1,11 @@ +package top.r3944realms.ltdmanager.mcms.response.instance + +import kotlinx.serialization.Serializable +import top.r3944realms.ltdmanager.mcms.response.MCSMResponse + +@Serializable +data class InstanceListResponse( + val status: Int, + val data: InstanceListData?, + val time: Long +) : MCSMResponse \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt new file mode 100644 index 0000000..f689da2 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/mcms/response/instance/StartInstanceResponse.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.mcms.response.instance + +class StartInstanceResponse { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt new file mode 100644 index 0000000..bdeb869 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/ApplyWhitelistModule.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.module + +class ApplyWhiteListModule { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt new file mode 100644 index 0000000..1d1323e --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt @@ -0,0 +1,4 @@ +package top.r3944realms.ltdmanager.module + +object Modules { +} \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt new file mode 100644 index 0000000..4b68488 --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/exception/ConfigError.kt @@ -0,0 +1,7 @@ +package top.r3944realms.ltdmanager.module.exception + +class InvalidConfigException: Exception() { + enum class Type(template: String) { + + } +} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt deleted file mode 100644 index 5ab9772..0000000 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/ImageUploader.kt +++ /dev/null @@ -1,114 +0,0 @@ -package top.r394realms.ltdmanagertest.util - - -import okhttp3.* -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.RequestBody.Companion.asRequestBody -import okhttp3.logging.HttpLoggingInterceptor -import top.r3944realms.ltdmanager.utils.LoggerUtil -import java.io.File -import java.io.IOException - -object ImageUploader { - - private val client = OkHttpClient().newBuilder() - .addInterceptor(HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY // 查看完整的请求和响应 - }) - .build() - - fun uploadImage(filePath: String, apiKey: String): String { - val file = File(filePath) - - // 检查文件是否存在 - if (!file.exists()) { - throw IllegalArgumentException("文件不存在: $filePath") - } - - LoggerUtil.logger.info("开始上传文件: ${file.name}, 大小: ${file.length()} bytes") - - // 创建 multipart 请求体 - val requestBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart( - "source", - file.name, - file.asRequestBody("image/png".toMediaType()) - ) - .addFormDataPart("format", "json") - .build() - - // 创建请求 - val request = Request.Builder() - .url("https://pic.xiaobuawa.top/api/1/upload") - .header("X-API-Key", apiKey.trim()) // 重要:去除空格 - .header("User-Agent", "OkHttp/4.12.0") // 添加 User-Agent - .post(requestBody) - .build() - - // 执行请求 - val response = client.newCall(request).execute() - try { - if (!response.isSuccessful) { - throw IOException("上传失败,状态码: ${response.code}, 响应: ${response.body?.string()}") - } - - val responseBody = response.body?.string() - LoggerUtil.logger.info("上传成功: $responseBody") - return responseBody ?: throw IOException("响应体为空") - } finally { - response.close() - } - } - - // 异步版本(推荐用于生产环境) - fun uploadImageAsync(filePath: String, apiKey: String, callback: (Result) -> Unit) { - val file = File(filePath) - - if (!file.exists()) { - callback(Result.failure(IllegalArgumentException("文件不存在: $filePath"))) - return - } - - val requestBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart( - "source", - file.name, - file.asRequestBody("image/png".toMediaType()) - ) - .addFormDataPart("format", "json") - .build() - - val request = Request.Builder() - .url("https://pic.xiaobuawa.top/api/1/upload") - .header("X-API-Key", apiKey.trim()) - .header("User-Agent", "OkHttp/4.12.0") - .post(requestBody) - .build() - - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - callback(Result.failure(e)) - } - - override fun onResponse(call: Call, response: Response) { - try { - if (!response.isSuccessful) { - callback(Result.failure(IOException("上传失败,状态码: ${response.code}"))) - return - } - - val responseBody = response.body?.string() - if (responseBody != null) { - callback(Result.success(responseBody)) - } else { - callback(Result.failure(IOException("响应体为空"))) - } - } catch (e: Exception) { - callback(Result.failure(e)) - } - } - }) - } -} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt deleted file mode 100644 index a15b9c8..0000000 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/img.kt +++ /dev/null @@ -1,29 +0,0 @@ -package top.r394realms.ltdmanagertest.util - -import top.r3944realms.ltdmanager.GlobalManager -import java.io.ByteArrayInputStream -import java.io.File - -fun main() = GlobalManager.runBlockingMain { - val client = GlobalManager.cheveretoClient; - client.use { cheveretoClient -> - // 1. 测试 File 上传 - val file = File("data/temp/icons8-postgresql-96.png") - val resp1 = cheveretoClient.uploadFile(file, title = "PostgreSQL Logo", tags = "db,icon,test") - println("File 上传结果: ${resp1.statusCode} -> ${resp1.image?.url}") - - // 2. 测试 ByteArrayInputStream 上传 - val bytes = file.readBytes() - val inputStream = ByteArrayInputStream(bytes) - val resp2 = cheveretoClient.uploadStream(inputStream, fileName = "test", title = "From Stream", description = "测试 ByteArrayInputStream 上传") - println("Stream 上传结果: ${resp2.statusCode} -> ${resp2.image?.url}") - - // 3. 测试 URL 上传 - val testUrl = "https://img.icons8.com/color/96/postgresql.png" - val resp3 = cheveretoClient.uploadUrl(testUrl) - println("URL 上传结果: ${resp3.statusCode} -> ${resp3.image?.url}") - if (resp3.statusCode == 400) { - println(resp3.statusTxt) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt deleted file mode 100644 index b2129b1..0000000 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv2.kt +++ /dev/null @@ -1,82 +0,0 @@ -package top.r394realms.ltdmanagertest.util - -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.json.Json -import top.r3944realms.ltdmanager.GlobalManager -import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse -import java.io.File -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -@OptIn(ExperimentalEncodingApi::class) -fun main() = GlobalManager.runBlockingMain { - val client = HttpClient(CIO) { - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - } - - val filePath = "./data/temp/icons8-postgresql-96.png" - val file = File(filePath) - if (!file.exists()) { - println("文件不存在: ${file.absolutePath}") - return@runBlockingMain - } - - val apiKey = "XXXX" - - try { - // 构建 multipart/form-data - val formDataContent = formData { - append("source", file.readBytes(), Headers.build { - append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") - append(HttpHeaders.ContentType, ContentType.Image.PNG.toString()) - }) - append("format", "json") - } - - // 调试输出每个 part - formDataContent.forEach { part -> - println("Part Headers: ${part.headers}") - when (part) { - is PartData.FileItem -> println("Part File: ${part.originalFileName}, size=${part.provider()} bytes") - is PartData.FormItem -> println("Part Form: ${part.value}") - else -> println("Part Other: $part") - } - part.dispose() - } - - // 发送 POST 请求 - val response: HttpResponse = client.submitFormWithBinaryData( - url = "https://pic.xiaobuawa.top/api/1/upload", - formData = formDataContent - ) { - header ("X-API-Key", apiKey.trim()) - } - - val responseText = response.bodyAsText() - println("服务器返回原始内容:\n$responseText") - - if (response.status.isSuccess()) { - val parsed = Json { ignoreUnknownKeys = true } - .decodeFromString(CheveretoResponse.serializer(), responseText) - println("上传成功,图片 URL: ${parsed.image?.url}") - } else { - println("上传失败,HTTP 状态码: ${response.status}") - } - - } catch (e: Exception) { - println("上传过程中出现异常:") - e.printStackTrace() - } finally { - client.close() - } -} diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt deleted file mode 100644 index a9fc56c..0000000 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv3.kt +++ /dev/null @@ -1,126 +0,0 @@ -package top.r394realms.ltdmanagertest.util - -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.utils.io.core.* -import top.r3944realms.ltdmanager.GlobalManager -import top.r3944realms.ltdmanager.utils.LoggerUtil -import java.io.File -import kotlin.io.use - -suspend fun uploadImageWithKtor(filePath: String, apiKey: String): String { - val client = HttpClient(CIO) { - // 添加引擎配置 - engine { - // 增加超时设置 - requestTimeout = 60000 - } - // 添加日志拦截器来调试 - expectSuccess = false // 不自动抛出异常,让我们自己处理 - } - - return client.use { httpClient -> - try { - val file = File(filePath) - - // 检查文件是否存在 - if (!file.exists()) { - throw Exception("文件不存在: $filePath") - } - - LoggerUtil.logger.info("开始上传文件: ${file.name}, 大小: ${file.length()} bytes") - - val response = httpClient.post("https://pic.xiaobuawa.top/api/1/upload") { - // 设置头信息 - headers { - append("X-API-Key", apiKey.trim()) // 去除前后空格 - append("User-Agent", "Mozilla/5.0 (compatible; MyApp/1.0)") - } - - // 使用正确的 multipart 格式 - setBody(MultiPartFormDataContent( - formData { - // 使用 appendInput 而不是 append,更接近 curl 的行为 - appendInput( - "source", - Headers.build { - append(HttpHeaders.ContentType, "image/png") - append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") - } - ) { - buildPacket { - writeFully(file.readBytes()) - } - } - append("format", "json") - } - )) - } - - val statusCode = response.status.value - val responseText = response.bodyAsText() - - LoggerUtil.logger.info("响应状态码: $statusCode") - LoggerUtil.logger.info("响应内容: $responseText") - - if (statusCode != 200) { - throw Exception("上传失败,状态码: $statusCode, 响应: $responseText") - } - - return@use responseText - } catch (e: Exception) { - LoggerUtil.logger.error("上传过程中发生错误: ${e.message}", e) - throw e - } - } -} - -// 或者使用另一种更简单的方法 -suspend fun uploadImageWithKtorSimple(filePath: String, apiKey: String): String { - val client = HttpClient(CIO) - - return client.use { httpClient -> - val file = File(filePath) - - val response = httpClient.submitFormWithBinaryData( - url = "https://pic.xiaobuawa.top/api/1/upload", - formData = formData { - append("source", file.readBytes(), Headers.build { - append(HttpHeaders.ContentType, "image/png") - append(HttpHeaders.ContentDisposition, "filename=\"${file.name}\"") - }) - append("format", "json") - } - ) { - header("X-API-Key", apiKey.trim()) - } - - val responseText = response.bodyAsText() - LoggerUtil.logger.info("简单方法响应: $responseText") - responseText - } -} - -fun main() = GlobalManager.runBlockingMain { - // 注意:API Key 前面不要有空格! - val apiKey = "XXXX" - val filePath = "./data/temp/icons8-postgresql-96.png" - - try { - // 先尝试简单方法 - val result = uploadImageWithKtorSimple(filePath, apiKey) - println("上传成功: $result") - } catch (e: Exception) { - println("简单方法失败,尝试详细方法: ${e.message}") - try { - val result = uploadImageWithKtor(filePath, apiKey) - println("详细方法上传成功: $result") - } catch (e2: Exception) { - println("所有方法都失败: ${e2.message}") - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt b/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt deleted file mode 100644 index 0e25d27..0000000 --- a/src/test/kotlin/top/r394realms/ltdmanagertest/util/imgv4.kt +++ /dev/null @@ -1,82 +0,0 @@ -package top.r394realms.ltdmanagertest.util - -import top.r3944realms.ltdmanager.GlobalManager - -fun main() = GlobalManager.runBlockingMain { - // 测试配置 - val apiKey = "XXX" - val filePath = "./data/temp/icons8-postgresql-96.png" - - println("=== 开始测试图片上传 ===") - println("API Key: ${apiKey.take(10)}...") - println("文件路径: $filePath") - - // 测试1: 同步上传 - println("\n--- 测试同步上传 ---") - try { - val result = ImageUploader.uploadImage(filePath, apiKey) - println("✅ 同步上传成功!") - println("响应结果: ${result.take(200)}...") // 只显示前200个字符 - } catch (e: Exception) { - println("❌ 同步上传失败: ${e.message}") - e.printStackTrace() - } - - // 测试2: 异步上传 - println("\n--- 测试异步上传 ---") - ImageUploader.uploadImageAsync(filePath, apiKey) { result -> - result.onSuccess { response -> - println("✅ 异步上传成功!") - println("响应结果: ${response.take(200)}...") - }.onFailure { error -> - println("❌ 异步上传失败: ${error.message}") - error.printStackTrace() - } - } - - // 等待异步操作完成 - println("等待异步操作完成...") - Thread.sleep(10000) - println("=== 测试结束 ===") -} - -// 使用 GlobalManager 的测试版本(如果需要) -fun mainWithGlobalManager() = GlobalManager.runBlockingMain { - val apiKey = "XXXX" - val filePath = "./data/temp/icons8-postgresql-96.png" - - println("=== 使用 GlobalManager 测试图片上传 ===") - - // 测试同步上传 - try { - val result = ImageUploader.uploadImage(filePath, apiKey) - println("✅ 上传成功!") - println("响应: $result") - } catch (e: Exception) { - println("❌ 上传失败: ${e.message}") - e.printStackTrace() - } -} - -// 简单的单元测试函数 -fun testImageUpload() { - val testCases = listOf( - // (文件路径, API Key, 期望结果) - "./data/temp/icons8-postgresql-96.png" to "chv_YmZ_12a0828fd88823ad4ef16a0c551b4a10ae5ce1b3e3eb65b07d87eb30162cbc91ed520334018fce2d6ba06f9d58724cef66d30ab7f6292bd4e33ad5e0d96c6499", - "./data/temp/nonexistent.png" to "chv_YmZ_12a0828fd88823ad4ef16a0c551b4a10ae5ce1b3e3eb65b07d87eb30162cbc91ed520334018fce2d6ba06f9d58724cef66d30ab7f6292bd4e33ad5e0d96c6499", // 不存在的文件 - "./data/temp/icons8-postgresql-96.png" to "invalid_key" // 无效的 API Key - ) - - testCases.forEachIndexed { index, (filePath, apiKey) -> - println("\n测试用例 ${index + 1}:") - println("文件: $filePath") - println("API Key: ${apiKey.take(10)}...") - - try { - val result = ImageUploader.uploadImage(filePath, apiKey) - println("✅ 成功: ${result.take(100)}...") - } catch (e: Exception) { - println("❌ 失败: ${e.message}") - } - } -} \ No newline at end of file