LTD-ManaagerBot/src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt

212 lines
6.9 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package top.r3944realms.ltdmanager.mcms
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.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 MCSMClient 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<MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>>(compareBy { it.priority })
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
startQueueProcessor()
}
/**
* 提交请求
*/
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(MCSMSkinQueueItem(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: MCSMSkinQueueItem<MCSMResponse, FailedMCSMResponse>) {
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<MCSMResponse, FailedMCSMResponse>>).complete(result)
return
} catch (e: Exception) {
lastError = e
attempt++
if (!request.shouldRetryOnFailure() || attempt >= maxRetries) {
break
}
LoggerUtil.logger.warn("MCSM请求失败 (尝试 $attempt/$maxRetries): ${e.message}")
delay((attempt * 1000L)) // 指数退避
}
}
// 所有重试都失败或不应重试
val errorResponse = createFailureResponse(lastError)
@Suppress("UNCHECKED_CAST")
(deferred as CompletableDeferred<ResponseResult<MCSMResponse, FailedMCSMResponse>>).complete(
ResponseResult.Failure(errorResponse)
)
}
}
/**
* 构建完整的URL包含查询参数
*/
private fun buildFullUrlWithQueryParams(request: MCSMRequest<*, *>): 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?
): FailedMCSMResponse {
return FailedMCSMResponse.ExceptionFailedMCSMResponse(
result = exception?.message ?: "未知错误",
)
}
override fun close() {
scope.cancel()
runBlocking {
client.close()
}
}
companion object {
fun create(): MCSMClient = MCSMClient()
}
}