refactor(脚本,代码调整): 预期希望通过配置加载模块

仍然在重构整个项目中

BREAKING CHANGE: 代码调整,模块抽象化
This commit is contained in:
叁玖领域 2026-02-02 12:47:52 +08:00
parent e45f2f6272
commit 4da8263b45
39 changed files with 544 additions and 447 deletions

View File

@ -0,0 +1,2 @@
package top.r3944realms.ltdmanager.blessingskin.data

View File

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

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.chevereto.data
class CheveretoSource {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.chevereto.request
class CheveretoRequest {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.chevereto.request.v1
class CheveretoUploadRequest {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.chevereto.response
class CheveretoSuccessResponse {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.chevereto.response
class FailedCheveretoResponse {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.chevereto.response.v1
class CheveretoUploadResponse {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.basic
interface IClient {
}

View File

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

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.client.request
interface IRequest {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.client.response
class IFailedResponse {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.client.response
interface IResponse {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.client.response
class ResponseResult {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.config
class McsmConfig {
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.init
object ModuleFactory {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.init
object ModuleLoader {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.core.init
object ModuleRegistry {
}

View 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()
}
}

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.mcms.request.instance
class GetInstanceListRequest {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.mcms.request.instance
class StartInstanceRequest {
}

View File

@ -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!!
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.mcms.response.instance
class StartInstanceResponse {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module
class ApplyWhiteListModule {
}

View File

@ -0,0 +1,4 @@
package top.r3944realms.ltdmanager.module
object Modules {
}

View File

@ -0,0 +1,7 @@
package top.r3944realms.ltdmanager.module.exception
class InvalidConfigException: Exception() {
enum class Type(template: String) {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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