refactor(脚本,代码调整): 预期希望通过配置加载模块
仍然在重构整个项目中 BREAKING CHANGE: 代码调整,模块抽象化
This commit is contained in:
parent
e45f2f6272
commit
4da8263b45
|
|
@ -0,0 +1,2 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.data
|
||||
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.data
|
||||
|
||||
class CheveretoSource {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.request
|
||||
|
||||
class CheveretoRequest {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.request.v1
|
||||
|
||||
class CheveretoUploadRequest {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.response
|
||||
|
||||
class CheveretoSuccessResponse {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.response
|
||||
|
||||
class FailedCheveretoResponse {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.chevereto.response.v1
|
||||
|
||||
class CheveretoUploadResponse {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.basic
|
||||
|
||||
interface IClient {
|
||||
}
|
||||
|
|
@ -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<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
|
||||
|
||||
/**
|
||||
* @return true 表示返回 BlessingSkinResponse, false 表示 Unit
|
||||
*/
|
||||
fun expectsResponse(): Boolean
|
||||
override fun compareTo(other: IQueueItem<@UnsafeVariance T, @UnsafeVariance F>): Int = getPriority().compareTo(other.getPriority())
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.client.request
|
||||
|
||||
interface IRequest {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.client.response
|
||||
|
||||
class IFailedResponse {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.client.response
|
||||
|
||||
interface IResponse {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.client.response
|
||||
|
||||
class ResponseResult {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.config
|
||||
|
||||
class McsmConfig {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
class DependencyResolver {
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
object ModuleFactory {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
object ModuleLoader {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.core.init
|
||||
|
||||
object ModuleRegistry {
|
||||
}
|
||||
212
src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt
Normal file
212
src/main/kotlin/top/r3944realms/ltdmanager/mcms/MCSMClient.kt
Normal file
|
|
@ -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<BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>>(compareBy { it.priority })
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
startQueueProcessor()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交请求
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动队列处理器
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(): BlessingSkinClient = BlessingSkinClient()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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)
|
||||
}
|
||||
|
|
@ -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<out T : BlessingSkinResponse, out F : FailedBlessingSkinResponse>(
|
||||
@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
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.mcms.request.instance
|
||||
|
||||
class GetInstanceListRequest {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.mcms.request.instance
|
||||
|
||||
class StartInstanceRequest {
|
||||
}
|
||||
|
|
@ -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!!
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <reified T : BlessingSkinResponse> 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package top.r3944realms.ltdmanager.blessingskin.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>()
|
||||
|
||||
/**
|
||||
* 检查是否成功
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.mcms.response.instance
|
||||
|
||||
class StartInstanceResponse {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
class ApplyWhiteListModule {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package top.r3944realms.ltdmanager.module
|
||||
|
||||
object Modules {
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package top.r3944realms.ltdmanager.module.exception
|
||||
|
||||
class InvalidConfigException: Exception() {
|
||||
enum class Type(template: String) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>) -> 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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user