fix: 小修改

This commit is contained in:
叁玖领域 2025-10-31 23:47:15 +08:00
parent a0f5504404
commit e45f2f6272
72 changed files with 2910 additions and 400 deletions

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ASMIdeaPluginConfiguration">
<asm skipDebug="false" skipFrames="false" skipCode="false" expandFrames="false" />

View File

@ -24,6 +24,13 @@ repositories {
maven {
url = uri("https://maven.aliyun.com/repository/gradle-plugin")
}
maven {
url = uri("https://libraries.minecraft.net/")
}
// 第三方 repo比如 MohistMC 或 GlareMasters Pub
maven {
url = uri("https://repo.glaremasters.me/repository/public/")
}
}
//TODO: 0872d1c0-829c-e1d7-6782-89e45c8a6b76
dependencies {
@ -37,6 +44,10 @@ repositories {
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") // 推荐使用kotlinx.serialization替代Gson
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// 如果需要日志拦截器(推荐用于调试)
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
// 数据库相关
implementation("org.jetbrains.exposed:exposed-core:0.41.1")
implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")
@ -71,6 +82,9 @@ repositories {
//生成 二维码
implementation("com.google.zxing:core:[3.5.3,)")
//命令解析
implementation("com.mojang:brigadier:1.2.9")
// 测试
testImplementation(kotlin("test"))
testImplementation("io.ktor:ktor-client-mock:2.3.3")

View File

@ -3,5 +3,5 @@ org.gradle.downloadSources=false
org.gradle.parallel=true
org.gradle.degree_of_parallelism=16
project_group=top.r3944realms.ltdmanager
project_version=1.10-SNAPSHOT
dg_lab_version=4.2.11.18
project_version=1.14-SNAPSHOT
dg_lab_version=4.3.13.18

Binary file not shown.

View File

@ -2,6 +2,7 @@ package top.r3944realms.ltdmanager
import kotlinx.coroutines.*
import top.r3944realms.ltdmanager.blessingskin.BlessingSkinClient
import top.r3944realms.ltdmanager.chevereto.CheveretoClient
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
import top.r3944realms.ltdmanager.mcserver.McSrvStatusClient
import top.r3944realms.ltdmanager.module.ModuleManager
@ -29,6 +30,9 @@ object GlobalManager {
val blessingSkinClient: BlessingSkinClient by lazy {
BlessingSkinClient.create()
}
val cheveretoClient: CheveretoClient by lazy {
CheveretoClient.create()
}
val moduleManager: ModuleManager by lazy { ModuleManager() }
@ -67,7 +71,8 @@ object GlobalManager {
"NapCatClient" to { napCatClient.close() },
"McSrvStatusClient" to { mcSrvStatusClient.close() },
"BlessingSkinClient" to { blessingSkinClient.close() },
"Hikari 数据源" to { dataSource.close() }
"Hikari 数据源" to { dataSource.close() },
"CheveretoClient" to { cheveretoClient.close() }
)
resources.forEach { (name, closer) ->

View File

@ -85,7 +85,7 @@ class BlessingSkinClient private constructor() : AutoCloseable {
*/
private suspend fun processQueueItem(item: BlessingSkinQueueItem<BlessingSkinResponse, FailedBlessingSkinResponse>) {
semaphore.withPermit {
val (request, deferred, _, maxRetries, expectsResponse) = item
val (request, deferred, _, maxRetries, _) = item
var attempt = 0
var lastError: Exception? = null

View File

@ -6,6 +6,7 @@ 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.serialization.kotlinx.json.*
import kotlinx.coroutines.*
@ -13,95 +14,223 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.sync.withPermit
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import top.r3944realms.ltdmanager.chevereto.data.CheveretoResponse
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import java.io.ByteArrayInputStream
import java.io.Closeable
import java.io.File
import java.util.*
import kotlin.collections.ArrayDeque
object CheveretoUploader {
class CheveretoClient private constructor() : Closeable {
private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
json(Json { ignoreUnknownKeys = true })
}
}
private val imgTuConfig = YamlConfigLoader.loadTuImgConfig()
private val apiUrl = imgTuConfig.url!!
private val apiKey = imgTuConfig.decryptedPassword!!
// 限流,同时最多 3 个上传
private val semaphore = Semaphore(3)
// 普通队列 (按 priority 排序)
private val queue = PriorityQueue<CheveretoQueueItem<CheveretoResponse>>(compareBy { it.priority })
private val queueMutex = Mutex()
// 紧急队列 (FIFO最多 10 个)
private val urgentQueue = ArrayDeque<CheveretoQueueItem<CheveretoResponse>>(10)
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
scope.launch {
while (isActive) {
val item = queueMutex.withLock {
when {
urgentQueue.isNotEmpty() -> urgentQueue.removeFirst()
queue.isNotEmpty() -> queue.poll()
else -> null
}
}
if (item != null) processItem(item)
else delay(20)
}
}
}
/**
* 上传本地文件
* 上传 File
*/
suspend fun uploadFile(
apiUrl: String,
apiKey: String,
file: File,
title: String? = null,
description: String? = null
description: String? = null,
tags: String? = null,
albumId: String? = null,
categoryId: String? = null,
width: Int? = null,
expiration: String? = null,
nsfw: Int? = null,
format: String = "json",
useFileDate: Int? = null,
priority: Int = 5
): CheveretoResponse {
return client.submitFormWithBinaryData(
url = apiUrl,
formData = formData {
append("source", file.readBytes(), Headers.build {
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"")
})
append("format", "json")
title?.let { append("title", it) }
description?.let { append("description", it) }
val deferred = CompletableDeferred<CheveretoResponse>()
val source = suspend {
safeUpload {
submitFormWithBinaryData(
url = apiUrl,
formData = formData {
append("source", file.readBytes(), Headers.build {
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"${file.name}\"")
})
append("format", format)
title?.let { append("title", it) }
description?.let { append("description", it) }
tags?.let { append("tags", it) }
albumId?.let { append("album_id", it) }
categoryId?.let { append("category_id", it) }
width?.let { append("width", it.toString()) }
expiration?.let { append("expiration", it) }
nsfw?.let { append("nsfw", it.toString()) }
useFileDate?.let { append("use_file_date", it.toString()) }
}
) {
header("X-API-Key", apiKey)
}
}
) {
headers {
append("X-API-Key", apiKey)
}
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
return deferred.await()
}
/**
* 上传 ByteArrayInputStream
*/
suspend fun uploadStream(
inputStream: ByteArrayInputStream,
fileName: String,
title: String? = null,
description: String? = null,
tags: String? = null,
albumId: String? = null,
categoryId: String? = null,
width: Int? = null,
expiration: String? = null,
nsfw: Int? = null,
format: String = "json",
useFileDate: Int? = null,
priority: Int = 5
): CheveretoResponse {
val deferred = CompletableDeferred<CheveretoResponse>()
val source = suspend {
val bytes = inputStream.readBytes()
safeUpload {
submitFormWithBinaryData(
url = apiUrl,
formData = formData {
append("source", bytes, Headers.build {
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"")
})
append("format", format)
title?.let { append("title", it) }
description?.let { append("description", it) }
tags?.let { append("tags", it) }
albumId?.let { append("album_id", it) }
categoryId?.let { append("category_id", it) }
width?.let { append("width", it.toString()) }
expiration?.let { append("expiration", it) }
nsfw?.let { append("nsfw", it.toString()) }
useFileDate?.let { append("use_file_date", it.toString()) }
}
) {
header("X-API-Key", apiKey)
}
}
}.body()
}
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
return deferred.await()
}
/**
* 上传网络图片 URL
*/
suspend fun uploadFromUrl(
apiUrl: String,
apiKey: String,
imageUrl: String
suspend fun uploadUrl(
url: String,
title: String? = null,
description: String? = null,
tags: String? = null,
albumId: String? = null,
categoryId: String? = null,
width: Int? = null,
expiration: String? = null,
nsfw: Int? = null,
format: String = "json",
useFileDate: Int? = null,
priority: Int = 5
): CheveretoResponse {
return client.submitForm(
url = apiUrl,
formParameters = Parameters.build {
append("source", imageUrl)
append("format", "json")
val deferred = CompletableDeferred<CheveretoResponse>()
val source = suspend {
safeUpload {
submitForm(
url = apiUrl,
formParameters = Parameters.build {
append("source", url)
append("format", format)
title?.let { append("title", it) }
description?.let { append("description", it) }
tags?.let { append("tags", it) }
albumId?.let { append("album_id", it) }
categoryId?.let { append("category_id", it) }
width?.let { append("width", it.toString()) }
expiration?.let { append("expiration", it) }
nsfw?.let { append("nsfw", it.toString()) }
useFileDate?.let { append("use_file_date", it.toString()) }
}
) {
header("X-API-Key", apiKey)
}
}
) {
headers {
append("X-API-Key", apiKey)
}
queueMutex.withLock { queue.add(CheveretoQueueItem(source, deferred, priority)) }
return deferred.await()
}
private suspend fun processItem(item: CheveretoQueueItem<CheveretoResponse>) {
semaphore.withPermit {
try {
val result = item.source()
item.deferred.complete(result)
} catch (e: Exception) {
item.deferred.completeExceptionally(e)
}
}.body()
}
}
/**
* 上传 ByteArrayInputStream
* 包装上传失败时打印原始响应
*/
suspend fun uploadFromStream(
apiUrl: String,
apiKey: String,
inputStream: ByteArrayInputStream,
fileName: String,
title: String? = null,
description: String? = null
): CheveretoResponse {
val bytes = inputStream.readBytes()
return client.submitFormWithBinaryData(
url = apiUrl,
formData = formData {
append("source", bytes, Headers.build {
append(HttpHeaders.ContentDisposition, "form-data; name=\"source\"; filename=\"$fileName\"")
})
append("format", "json")
title?.let { append("title", it) }
description?.let { append("description", it) }
}
) {
headers { append("X-API-Key", apiKey) }
}.body()
private suspend fun safeUpload(block: suspend HttpClient.() -> HttpResponse): CheveretoResponse {
val response = client.block()
return try {
response.body()
} catch (e: Exception) {
val raw = response.bodyAsText()
throw RuntimeException("Upload failed (status=${response.status}): $raw", e)
}
}
override fun close() {
scope.cancel()
runBlocking { client.close() }
}
companion object {
fun create(): CheveretoClient = CheveretoClient()
}
}

View File

@ -1,4 +1,9 @@
package top.r3944realms.ltdmanager.chevereto
class CheveretoQueueItem {
}
import kotlinx.coroutines.CompletableDeferred
data class CheveretoQueueItem<T>(
val source: suspend () -> T,
val deferred: CompletableDeferred<T>,
val priority: Int = 5
)

View File

@ -1,5 +1,6 @@
package top.r3944realms.ltdmanager.chevereto
package top.r3944realms.ltdmanager.chevereto.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@ -10,5 +11,98 @@ data class CheveretoImage(
val width: Int,
val height: Int,
val date: String,
val url: String
@SerialName("date_gmt")
val dateGmt: String,
val title: String,
val tags: List<String>? = emptyList(),
val description: String? = null,
val nsfw: Int,
@SerialName("storage_mode")
val storageMode: String,
val md5: String,
@SerialName("source_md5")
val sourceMd5: String? = null,
@SerialName("original_filename")
val originalFilename: String,
@SerialName("original_exifdata")
val originalExifdata: String? = null,
val views: Int,
@SerialName("category_id")
val categoryId: String? = null,
val chain: Int,
@SerialName("thumb_size")
val thumbSize: Int,
@SerialName("medium_size")
val mediumSize: Int,
@SerialName("frame_size")
val frameSize: Int? = null,
@SerialName("expiration_date_gmt")
val expirationDateGmt: String? = null,
val likes: Int,
@SerialName("is_animated")
val isAnimated: Int,
@SerialName("is_approved")
val isApproved: Int,
@SerialName("is_360")
val is360: Int,
val duration: Int? = null,
val type: String? = null,
@SerialName("tags_string")
val tagsString: String? = null,
val file: File? = null,
@SerialName("id_encoded")
val idEncoded: String,
val filename: String,
val mime: String,
val url: String,
val ratio: Double? = null,
@SerialName("size_formatted")
val sizeFormatted: String,
val frame: ImageThumb? = null,
val image: ImageFile,
val thumb: ImageThumb,
@SerialName("url_frame")
val urlFrame: String? = null,
val medium: Medium? = null,
@SerialName("duration_time")
val durationTime: String? = null,
@SerialName("url_viewer")
val urlViewer: String,
@SerialName("path_viewer")
val pathViewer: String? = null,
@SerialName("url_short")
val urlShort: String,
@SerialName("display_url")
val displayUrl: String,
@SerialName("display_width")
val displayWidth: Int,
@SerialName("display_height")
val displayHeight: Int,
@SerialName("views_label")
val viewsLabel: String,
@SerialName("likes_label")
val likesLabel: String,
@SerialName("how_long_ago")
val howLongAgo: String,
@SerialName("date_fixed_peer")
val dateFixedPeer: String,
@SerialName("title_truncated")
val titleTruncated: String,
@SerialName("title_truncated_html")
val titleTruncatedHtml: String,
@SerialName("is_use_loader")
val isUseLoader: Boolean,
@SerialName("display_title")
val displayTitle: String? = null,
@SerialName("delete_url")
val deleteUrl: String
)

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.chevereto
package top.r3944realms.ltdmanager.chevereto.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
data class CheveretoResponse(
@SerialName("status_code")
val statusCode: Int,
val success: Map<String, String>? = null,
val image: CheveretoImage? = null
val success: Success? = null,
val image: CheveretoImage? = null,
@SerialName("status_txt")
val statusTxt:String ?= null
)

View File

@ -1,4 +1,13 @@
package top.r3944realms.ltdmanager.chevereto.data
class File {
import kotlinx.serialization.Serializable
@Serializable
data class File(
val resource: Resource
) {
@Serializable
data class Resource(
val type: String
)
}

View File

@ -1,4 +1,13 @@
package top.r3944realms.ltdmanager.chevereto.data
class ImageFile {
}
import kotlinx.serialization.Serializable
@Serializable
data class ImageFile(
val filename: String,
val name: String,
val mime: String,
val extension: String,
val url: String,
val size: Long
)

View File

@ -1,3 +1,13 @@
package top.r3944realms.ltdmanager.chevereto.data
data class ImageThumb()
import kotlinx.serialization.Serializable
@Serializable
data class ImageThumb(
val filename: String,
val name: String,
val mime: String,
val extension: String,
val url: String,
val size: Int
)

View File

@ -1,4 +1,12 @@
package top.r3944realms.ltdmanager.chevereto.data
class Medium {
}
import kotlinx.serialization.Serializable
@Serializable
data class Medium(
val filename: String? = null,
val name: String? = null,
val mime: String? = null,
val extension: String? = null,
val url: String? = null
)

View File

@ -1,3 +1,9 @@
package top.r3944realms.ltdmanager.chevereto.data
data class Success()
import kotlinx.serialization.Serializable
@Serializable
data class Success(
val message : String? = null,
val code: Int? = 200,
)

View File

@ -1,4 +1,59 @@
package top.r3944realms.ltdmanager.core.config
class ImgTuConfig {
import top.r3944realms.ltdmanager.utils.CryptoUtil
import top.r3944realms.ltdmanager.utils.YamlUpdater
data class ImgTuConfig(
var url: String? = null,
var encryptedPassword: String? = null
) {
/**
* 获取解密后的Password如果未加密返回原值
*/
val decryptedPassword: String?
get() {
if (encryptedPassword == null) {
return null
}
if (!isEncrypted()) {
return encryptedPassword
}
try {
val cipherText = encryptedPassword!!.substring(4, encryptedPassword!!.length - 1)
return CryptoUtil.decrypt(cipherText)
} catch (e: Exception) {
throw IllegalStateException("Password解密失败", e)
}
}
/**
* 加密密码如果未加密并返回是否成功加密
*/
fun encryptPassword() {
if (encryptedPassword == null || isEncrypted()) {
return
}
try {
encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})"
YamlUpdater.updateYaml(
YamlConfigLoader.configFilePath.toString(),
"img-tu.encrypted-password",
this.encryptedPassword!!
)
} catch (e: Exception) {
throw IllegalStateException("密码加密失败", e)
}
}
/**
* 检查Password是否已加密
*/
private fun isEncrypted(): Boolean {
return encryptedPassword != null &&
encryptedPassword!!.startsWith("ENC(") &&
encryptedPassword!!.endsWith(")")
}
override fun toString(): String {
return "ImgTuConfig(url=$url, Password=***)"
}
}

View File

@ -35,6 +35,7 @@ object YamlConfigLoader {
config?.tools?.rcon?.encryptPassword()
config?.blessingSkinServer?.invitationApi?.encryptToken()
config?.dgLab?.wsServer?.encryptPassword()
config?.imgTu?.encryptPassword()
}
private fun loadConfig(): ConfigWrapper {
if (!Files.exists(configFilePath)) {
@ -78,6 +79,7 @@ object YamlConfigLoader {
fun loadMailConfig(): MailConfig = config.mail
fun loadBlessingSkinServerConfig(): BlessingSkinServerConfig = config.blessingSkinServer
fun loadDgLabConfig(): DgLabConfig = config.dgLab
fun loadTuImgConfig(): ImgTuConfig = config.imgTu
data class ConfigWrapper(
var database: DatabaseConfig = DatabaseConfig(),
var crypto: CryptoConfig = CryptoConfig(),
@ -88,6 +90,7 @@ object YamlConfigLoader {
var mail: MailConfig = MailConfig(),
var blessingSkinServer: BlessingSkinServerConfig = BlessingSkinServerConfig(),
var dgLab: DgLabConfig = DgLabConfig(),
var imgTu: ImgTuConfig = ImgTuConfig(),
)
}

View File

@ -1,5 +1,6 @@
package top.r3944realms.ltdmanager.dglab.manager
package top.r3944realms.ltdmanager.dglab
import com.r3944realms.dg_lab.api.manager.Status
import com.r3944realms.dg_lab.api.operation.ClientOperation
import com.r3944realms.dg_lab.api.operation.ServerOperation
import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketClientRole
@ -11,25 +12,66 @@ import com.r3944realms.dg_lab.websocket.PowerBoxWSServer
import com.r3944realms.dg_lab.websocket.sharedData.ClientPowerBoxSharedData
import com.r3944realms.dg_lab.websocket.sharedData.ServerPowerBoxSharedData
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.dglab.manager.ClientManager
import top.r3944realms.ltdmanager.dglab.manager.ServerManager
import top.r3944realms.ltdmanager.dglab.model.game.Player
import top.r3944realms.ltdmanager.dglab.model.game.PlayerManager
import kotlin.io.path.Path
/**
* 全局DG_Lab单例管理器
* DG_Lab管理器
*/
object DgLabManager {
class DgLab {
// 可空,延迟初始化
var serverManager: ServerManager? = null
private set
internal var serverManager: ServerManager? = null
get() = field
var clientManager: ClientManager? = null
private set
internal var clientManager: ClientManager? = null
get() = field
private var playerManager: PlayerManager? = null
companion object {
const val SERVER_ROLE_NAME = "Se-IC"
}
fun isSeverOnline(): Boolean = serverManager?.let { it.status == Status.RUNNING } ?: false
fun isClientOnline(id: String): Boolean = clientManager?.getClient(id)?.let { it.status == Status.RUNNING } ?: false
fun getPlayerManager(): PlayerManager = playerManager!!
fun close() {
serverManager?.stop()
clientManager?.stopAll()
}
fun initOrLoadPlayerManager(idNameMap: Map<Long, String>) {
playerManager = PlayerManager(1)
val idList = idNameMap.map { id -> id.key }
val existingIds = playerManager?.allPlayers()?.map { it.id }?.toSet() ?: emptySet()
val targetIds = idList.toSet()
// 要删除的
val toRemove = existingIds - targetIds
// 要新增的
val toAdd = targetIds - existingIds
// 删除
toRemove.forEach { id ->
playerManager?.removePlayer(id)
}
// 新增
toAdd.forEach { id ->
playerManager?.addPlayer(Player(id, idNameMap[id] as String,false))
}
}
fun createServerManager(operation: ServerOperation): DGPBServerManager {
val loadDgLabConfig = YamlConfigLoader.loadDgLabConfig()
val boxWSServer = PowerBoxWSServer.Builder.getBuilder()
.port(loadDgLabConfig.wsServer.localServerPort)
.role(WebSocketServerRole("Se-IC"))
.role(WebSocketServerRole(SERVER_ROLE_NAME))
.operation(operation)
.sharedData(ServerPowerBoxSharedData())
.build()
@ -66,7 +108,13 @@ object DgLabManager {
fun removeClient(key: String) {
clientManager?.removeClient(key)
}
/**
* 获取 服务器管理类
*/
@Throws(IllegalStateException::class)
fun getServer(): DGPBServerManager {
return serverManager?.getInstance() ?: throw IllegalStateException("Server is not initialized")
}
/**
* 获取 客户端管理类
*/
@ -86,6 +134,7 @@ object DgLabManager {
.role(WebSocketClientRole("QQ-$key"))
.operation(operation)
.sharedData(ClientPowerBoxSharedData())
.useRoleMsgMode(true)
.build()
if (loadDgLabConfig.wsServer.localServerSecure) {

View File

@ -2,60 +2,219 @@ package top.r3944realms.ltdmanager.dglab.model.game
import com.r3944realms.dg_lab.api.operation.ClientOperation
import com.r3944realms.dg_lab.api.websocket.message.data.PowerBoxData
import com.r3944realms.dg_lab.api.websocket.message.data.type.PowerBoxDataType
import com.r3944realms.dg_lab.manager.DGPBClientManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendPrivateMsgRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
import top.r3944realms.ltdmanager.utils.QRCodeUtil
import java.io.ByteArrayInputStream
class GameClientOperation(
val player: Player
val napCatClient: NapCatClient,
val groupId: Long,
val playerManager: PlayerManager,
private val playerId: Long
) : ClientOperation {
private val scope = CoroutineScope(Dispatchers.IO)
private var qrcode:ByteArrayInputStream? = null;
var clientSelf: DGPBClientManager? = null
private var hasBinding = false
private var bindingTimeoutJob: kotlinx.coroutines.Job? = null // 保存倒计时任务
override fun ClientStartingHandler() {
println("Player ${player.id} is starting the client...")
LoggerUtil.logger.debug("Player $playerId is starting the client...")
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端启动中...")), ID.long(playerId)))
}
}
override fun ClientStartedHandler() {
println("Player ${player.id} client started successfully.")
LoggerUtil.logger.debug("Player $playerId client started successfully.")
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端启动完成!")), ID.long(playerId)))
}
playerManager.getPlayer(playerId)?.active = true
}
override fun ClientStartingErrorHandler() {
println("Player ${player.id} failed to start client!")
override fun ClientStartingErrorHandler(errMsg: String) {
LoggerUtil.logger.debug("Player $playerId failed to start client! Reason: $errMsg")
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端启动中遇到错误$errMsg!")), ID.long(playerId)))
}
playerManager.getPlayer(playerId)?.active = false
}
override fun ClientStoppingHandler() {
println("Player ${player.id} is stopping the client...")
LoggerUtil.logger.debug("Player $playerId is stopping the client...")
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端关闭中...")), ID.long(playerId)))
}
playerManager.getPlayer(playerId)?.active = false
}
override fun ClientStoppingErrorHandler() {
println("Player ${player.id} encountered an error while stopping.")
override fun ClientStoppingErrorHandler(errMsg: String) {
LoggerUtil.logger.debug("Player $playerId encountered an error while stopping. Reason: $errMsg")
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端关闭中遇到错误$errMsg!")), ID.long(playerId)))
}
playerManager.getPlayer(playerId)?.active = false
}
override fun ClientStoppedHandler() {
println("Player ${player.id} client stopped.")
LoggerUtil.logger.debug("Player $playerId client stopped.")
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("DG_LAB客户端成功关闭!")), ID.long(playerId)))
}
bindingTimeoutJob?.cancel()
playerManager.getPlayer(playerId)?.active = false
}
override fun QrCodeUrlHandler(p0: String?) {
println("Player ${player.id} QR code received: $p0")
}
LoggerUtil.logger.debug("Player $playerId QR code received: $p0")
if (p0.isNullOrBlank()) {
LoggerUtil.logger.warn("二维码 URL 为空,无法生成")
return
}
// 处理 URL将 IP 和端口替换为配置文件中的服务器 URL
val processedUrl = processQrCodeUrl(p0)
// 生成二维码文件
qrcode = QRCodeUtil.generateQRCode(processedUrl, 300, 300)
}
/**
* 处理二维码 URL将整个连接地址替换为配置文件中的服务器 URL
*/
private fun processQrCodeUrl(originalUrl: String): String {
return try {
val configUrl = YamlConfigLoader.loadDgLabConfig().wsServer.localServerPublishUrl
// 使用正则表达式匹配整个 ws:// 或 wss:// 开头的 URL
val pattern = Regex("""wss?://[^:/]+(?::\d+)?(/.*)?""")
pattern.replace(originalUrl) { matchResult ->
// 保留原始 URL 中的路径部分(如果有的话)
val path = matchResult.groupValues[1] ?: ""
"$configUrl$path"
}
} catch (e: Exception) {
LoggerUtil.logger.error("处理二维码 URL 时出错: ${e.message}", e)
originalUrl // 如果处理失败,返回原 URL
}
}
override fun ShowQrCodeHandler() {
println("Player ${player.id} should display QR code.")
}
LoggerUtil.logger.debug("Display QRCode to $playerId.")
if (qrcode == null) {
LoggerUtil.logger.warn("没有可用的二维码路径")
return
}
scope.launch {
// 上传二维码图片
val response = GlobalManager.cheveretoClient.uploadStream(
qrcode!!,
"$playerId-Qrcode-${System.currentTimeMillis()}.png",
"Qrcode-$playerId-${System.currentTimeMillis()}",
"5min后将会自动删除",
albumId = "BFx",
expiration = "PT5M"
)
if (response.image?.url != null) {
// 发送图床 URL 给玩家
napCatClient.sendUnit(
SendPrivateMsgRequest(
listOf(
MessageElement.text("请在60s内绑定APP否则将自动断开连接"),
MessageElement.image(response.image.url, "二维码")
),
ID.long(playerId)
)
)
} else {
LoggerUtil.logger.error("上传二维码返回 JSON 未包含 URL")
}
// 启动 60 秒倒计时任务
bindingTimeoutJob = launch {
kotlinx.coroutines.delay(60_000)
val player = playerManager.getPlayer(playerId)
if (player != null && !hasBinding) {
LoggerUtil.logger.warn("Player $playerId 在 60 秒内未绑定,正在停止客户端")
napCatClient.sendUnit(
SendPrivateMsgRequest(
listOf(
MessageElement.text("请在60s内未绑定APP准备停止客户端"),
),
ID.long(playerId)
)
)
try {
clientSelf?.stop()
} catch (e: Exception) {
LoggerUtil.logger.error("停止客户端失败: ", e)
} finally {
player.active = false
}
}
}
}
}
override fun ConnectSuccessfulNoticeHandler() {
println("Player ${player.id} connected successfully.")
LoggerUtil.logger.debug("Player $playerId connected successfully.")
bindingTimeoutJob?.cancel()
bindingTimeoutJob = null
val player = playerManager.getPlayer(playerId)
player?.active = true
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("恭喜,绑定成功")), ID.long(playerId)))
napCatClient.sendUnit(SendGroupMsgRequest(listOf(MessageElement.text("$playerId 加入战局")), ID.long(groupId)))
}
}
override fun DisconnectHandler(p0: PowerBoxData?) {
println("Player ${player.id} disconnected: $p0")
LoggerUtil.logger.debug("Player {} disconnected: {}", playerId, p0)
scope.launch {
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("连接断开, $p0")), ID.long(playerId)))
napCatClient.sendUnit(SendGroupMsgRequest(listOf(MessageElement.text("$playerId 离开战局")), ID.long(groupId)))
}
}
override fun ErrorHandler(p0: PowerBoxData?) {
println("Player ${player.id} error occurred: $p0")
LoggerUtil.logger.debug("Player {} error occurred: {}", playerId, p0)
scope.launch {
if(p0 != null && p0.message.isNotEmpty())
napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("遇到错误, $p0")), ID.long(playerId)))
}
}
override fun HeartBeatHandler(p0: PowerBoxData?) {
println("Heartbeat from player ${player.id}: $p0")
// LoggerUtil.logger.debug("Heartbeat from player {}: {}", playerId, p0)
// scope.launch {
// napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("连接断开, $p0")), ID.long(playerId)))
// }
}
override fun OtherMessageHandler(p0: PowerBoxData?) {
println("Other message for player ${player.id}: $p0")
// LoggerUtil.logger.debug("Other message for player {}: {}", playerId, p0)
// scope.launch {
// napCatClient.sendUnit(SendPrivateMsgRequest(listOf(MessageElement.text("连接断开, $p0")), ID.long(playerId)))
// }
when (p0?.commandType) {
PowerBoxDataType.STRENGTH -> TODO()
PowerBoxDataType.PULSE -> TODO()
PowerBoxDataType.CLEAR -> TODO()
PowerBoxDataType.FEEDBACK -> TODO()
else -> return
}
}
}

View File

@ -1,5 +1,84 @@
package top.r3944realms.ltdmanager.dglab.model.game
import com.r3944realms.dg_lab.api.websocket.message.PowerBoxMessage
import com.r3944realms.dg_lab.api.websocket.message.role.PlaceholderRole
import com.r3944realms.dg_lab.api.websocket.message.role.WebSocketServerRole
import com.r3944realms.dg_lab.websocket.handler.server.DefaultServerOperation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import top.r3944realms.ltdmanager.dglab.DgLab
import top.r3944realms.ltdmanager.dglab.manager.ServerManager
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
class GameServerOperation : DefaultServerOperation()
class GameServerOperation(private val msgClient: NapCatClient, val groupId: Long) : DefaultServerOperation() {
private val scope = CoroutineScope(Dispatchers.IO)
var serverManager: ServerManager? = null
override fun ServerStartingHandler() {
scope.launch {
msgClient.sendUnit(
SendGroupMsgRequest(listOf(MessageElement.text("服务器启动中...")), ID.long(groupId))
)
}
}
override fun ServerStartedHandler() {
scope.launch {
msgClient.sendUnit(
SendGroupMsgRequest(listOf(MessageElement.text("服务器已启动")), ID.long(groupId))
)
}
}
override fun ServerStoppingHandler() {
scope.launch {
msgClient.sendUnit(
SendGroupMsgRequest(listOf(MessageElement.text("服务器关闭中...")), ID.long(groupId))
)
}
}
override fun ServerStoppedHandler() {
scope.launch {
msgClient.sendUnit(
SendGroupMsgRequest(listOf(MessageElement.text("服务器已关闭")), ID.long(groupId))
)
}
}
override fun ServerStoppingErrorHandler(errMsg: String) {
scope.launch {
msgClient.sendUnit(
SendGroupMsgRequest(listOf(MessageElement.text("服务器关闭过程中遇到错误: $errMsg")), ID.long(groupId))
)
}
}
override fun ServerStartingErrorHandler(errMsg: String?) {
scope.launch {
msgClient.sendUnit(
SendGroupMsgRequest(listOf(MessageElement.text("服务器开启过程中遇到错误: $errMsg")), ID.long(groupId))
)
}
}
override fun ClientSessionBuildInHandler(clientId: String?) {
scope.launch{
delay(1000)
serverManager?.getInstance()?.send(
clientId,
PowerBoxMessage.createPowerBoxMessage(
"bind",
clientId,
"",
"",
WebSocketServerRole(DgLab.SERVER_ROLE_NAME),
PlaceholderRole("Temp-$clientId")
)
)
}
}
}

View File

@ -1,8 +1,13 @@
package top.r3944realms.ltdmanager.dglab.model.game
import kotlinx.serialization.Serializable
/**
* 玩家类目前仅包含一个 ID
* 玩家类
*/
@Serializable
data class Player(
val id: String
val id: Long,
var name: String,
var active: Boolean,
)

View File

@ -1,4 +1,73 @@
package top.r3944realms.ltdmanager.dglab.model.game
class PlayerManager {
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import top.r3944realms.ltdmanager.module.PersistentState
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.io.File
import java.util.concurrent.ConcurrentHashMap
class PlayerManager(id: Long): PersistentState<PlayerManager.PlayerState> {
@Contextual
private val map = ConcurrentHashMap<Long, Player>()
@Transient
private val stateFile: File = getStateFileInternal("dglab_player_data.json", "dglab$id")
@Transient
private val stateBackupFile: File = getStateFileInternal("dglab_player_data.json.bak","dglab$id")
override fun getStateFileInternal(): File = stateFile
private var playerState = loadState()
@Serializable
data class PlayerState(
val map: Map<Long, Player> = emptyMap()
)
override fun getState(): PlayerState = playerState
/** 添加或更新玩家 */
fun addPlayer(player: Player) {
map[player.id] = player
}
/** 根据 ID 获取玩家 */
fun getPlayer(id: Long): Player? = map[id]
/** 删除玩家 */
fun removePlayer(id: Long): Player? = map.remove(id)
/** 判断是否存在玩家 */
fun contains(id: Long): Boolean = map.containsKey(id)
/** 获取所有玩家 */
fun allPlayers(): List<Player> = map.values.toList()
/** 获取所有在线玩家的数量 */
fun getOnlinePlayerSize(): Int = map.values.filter { it.active }.size
override fun saveState(state: PlayerState) {
try {
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
stateFile.writeText(Json.encodeToString(state))
} catch (e: Exception) {
LoggerUtil.logger.error("[dglab] 保存玩家数据&状态失败", e)
}
}
override fun loadState(): PlayerState {
return try {
val fileToRead = when {
stateFile.exists() -> stateFile
stateBackupFile.exists() -> stateBackupFile
else -> null
} ?: return PlayerState()
Json.decodeFromString<PlayerState>(fileToRead.readText())
} catch (e: Exception) {
LoggerUtil.logger.warn("[dglab] 读取玩家数据&状态失败", e)
PlayerState()
}
}
}

View File

@ -3,14 +3,46 @@ package top.r3944realms.ltdmanager.dglab.model.pulseware
import com.r3944realms.dg_lab.api.message.data.PulseWave
import com.r3944realms.dg_lab.api.message.data.PulseWaveList
object CustomPulseDataConverter {
/**
* 将频率转换为 Dg-Lab 格式
*
* @param frequency 频率值
* @return Dg-Lab 格式的数字
*/
private fun convertFrequency(frequency: Int): Int {
return when {
frequency <= 10 -> 10
frequency <= 100 -> frequency
frequency <= 600 -> (frequency - 100) / 5 + 100
frequency <= 1000 -> (frequency - 600) / 10 + 200
else -> 10
}
}
/**
* 将频率数组转换为 Dg-Lab 格式
*
* @param frequencies 频率数组
* @return 转换后的频率数组
*/
private fun convertFrequencies(frequencies: IntArray): IntArray {
return IntArray(4) { index ->
if (index < frequencies.size) {
convertFrequency(frequencies[index])
} else {
10 // 默认值
}
}
}
/**
* 将自定义波形数据转换为 PulseWaveList
*
* @param customPulseData Map<String></String>, List<int></int>[][]>>
* @param customPulseData Map<String, List<Array<IntArray>>>
* 每个 int[][] 包含两个长度为 4 int 数组第一个是 frequencies第二个是 strengths
* @return Map<String></String>, PulseWaveList>
* @return Map<String, PulseWaveList>
*/
fun convert(customPulseData: Map<String, List<Array<IntArray>>>): Map<String, PulseWaveList> {
val pulseWaveLists: MutableMap<String, PulseWaveList> = HashMap()
@ -26,7 +58,10 @@ object CustomPulseDataConverter {
// 确保每个数组长度为4
require(!(freqs.size != 4 || strengths.size != 4)) { "每个波形段必须包含 4 个频率和 4 个强度值" }
val wave = PulseWave.fromArrays(freqs, strengths)
// 转换频率为 Dg-Lab 格式
val convertedFreqs = convertFrequencies(freqs)
val wave = PulseWave.fromArrays(convertedFreqs, strengths)
waveList.add(wave)
}
@ -35,12 +70,53 @@ object CustomPulseDataConverter {
return pulseWaveLists
}
fun PulseWave.toSerializable(): PulseWaveSerializable =
PulseWaveSerializable(f1(), f2(), f3(), f4(), s1(), s2(), s3(), s4())
fun PulseWaveSerializable.toPulseWave(): PulseWave =
/**
* 转换单个 PulseWave 的频率
*/
private fun convertPulseWaveFrequencies(pulseWave: PulseWave): PulseWave {
val freqs = intArrayOf(
convertFrequency(pulseWave.f1()),
convertFrequency(pulseWave.f2()),
convertFrequency(pulseWave.f3()),
convertFrequency(pulseWave.f4())
)
val strengths = intArrayOf(
pulseWave.s1(),
pulseWave.s2(),
pulseWave.s3(),
pulseWave.s4()
)
return PulseWave.fromArrays(freqs, strengths)
}
/**
* 转换整个 PulseWaveList 的频率
*/
fun convertPulseWaveListFrequencies(pulseWaveList: PulseWaveList): PulseWaveList {
val convertedList = PulseWaveList()
convertedList.name = pulseWaveList.name
for (i in 0 until pulseWaveList.list.size) {
val convertedWave = convertPulseWaveFrequencies(pulseWaveList.list[i])
convertedList.add(convertedWave)
}
return convertedList
}
fun PulseWave.toSerializable(): PulseWaveSerializable =
PulseWaveSerializable(
convertFrequency(f1()),
convertFrequency(f2()),
convertFrequency(f3()),
convertFrequency(f4()),
s1(), s2(), s3(), s4()
)
private fun PulseWaveSerializable.toPulseWave(): PulseWave =
PulseWave.fromArrays(
intArrayOf(f1, f2, f3, f4),
intArrayOf(convertFrequency(f1), convertFrequency(f2), convertFrequency(f3), convertFrequency(f4)),
intArrayOf(s1, s2, s3, s4)
)

View File

@ -4,6 +4,38 @@ import com.r3944realms.dg_lab.api.message.data.PulseWave
import com.r3944realms.dg_lab.api.message.data.PulseWaveList
object DefaultPulseData {
/**
* 将频率转换为 Dg-Lab 格式
*
* @param frequency 频率值
* @return Dg-Lab 格式的数字
*/
private fun convertFrequency(frequency: Int): Int {
return when {
frequency <= 10 -> 10
frequency <= 100 -> frequency
frequency <= 600 -> (frequency - 100) / 5 + 100
frequency <= 1000 -> (frequency - 600) / 10 + 200
else -> 10
}
}
/**
* 转换频率数组为 Dg-Lab 格式
*/
private fun convertFrequencies(frequencies: IntArray): IntArray {
return IntArray(frequencies.size) { index ->
convertFrequency(frequencies[index])
}
}
/**
* 创建经过频率转换的波形段
*/
private fun createWaveSegment(frequencies: IntArray, strengths: IntArray): PulseWave {
val convertedFreqs = convertFrequencies(frequencies)
return PulseWave.fromArrays(convertedFreqs, strengths)
}
fun allPulseWaveLists(): Map<String, PulseWaveList> {
return mapOf(
@ -47,7 +79,7 @@ object DefaultPulseData {
// 转成 PulseWave 并加入列表
for (seg in segments) {
list.add(PulseWave.fromArrays(seg[0], seg[1]))
list.add(createWaveSegment(seg[0], seg[1]))
}
list
@ -68,7 +100,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(84, 82, 80, 76)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(68, 68, 68, 68))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -85,7 +117,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 1)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(2, 2, 2, 2))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
val FastPinch: PulseWaveList by lazy {
@ -96,7 +128,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
val PinchGradual: PulseWaveList by lazy {
@ -115,7 +147,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -140,7 +172,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
val Compress: PulseWaveList by lazy {
@ -169,7 +201,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
val RhythmStep: PulseWaveList by lazy {
@ -203,7 +235,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -216,7 +248,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -231,7 +263,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0)),
arrayOf(intArrayOf(0, 0, 0, 0), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
val WaveRipple: PulseWaveList by lazy {
@ -246,7 +278,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(50, 50, 50, 50)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -259,7 +291,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(80, 90, 100, 100)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -273,7 +305,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(20, 20, 20, 20), intArrayOf(50, 50, 50, 50)),
arrayOf(intArrayOf(15, 15, 15, 15), intArrayOf(0, 0, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
val SignalLight: PulseWaveList by lazy {
@ -285,7 +317,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 0, 0, 0)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 100, 100, 100))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -296,7 +328,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 30, 60, 100)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 70, 40, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
@ -307,7 +339,7 @@ object DefaultPulseData {
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(0, 50, 100, 100)),
arrayOf(intArrayOf(10, 10, 10, 10), intArrayOf(100, 50, 0, 0))
)
segments.forEach { list.add(PulseWave.fromArrays(it[0], it[1])) }
segments.forEach { list.add(createWaveSegment(it[0], it[1])) }
list
}
}

View File

@ -1,37 +1,50 @@
package top.r3944realms.ltdmanager
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.module.McServerStatusModule
import top.r3944realms.ltdmanager.module.*
fun main() = GlobalManager.runBlockingMain {
val groupId:Long = 538751386
val commonGroupId:Long = 538751386
val whitelistGroupId:Long = 920719236
val selfQQId = 3327379836
val selfNickName = "闲趣老土豆"
// 创建模块实例
val groupModule = GroupRequestHandlerModule(
moduleName = "WhiteListGroup",
client = GlobalManager.napCatClient,
targetGroupId = groupId
targetGroupId = whitelistGroupId
)
val groupMsgPollingModule = GroupMessagePollingModule(
moduleName = "WhiteListGroup",
targetGroupId = groupId,
val commonGroupMsgPollingModule = GroupMessagePollingModule(
moduleName = "CommonGroupMsgPolling",
targetGroupId = commonGroupId,
pollIntervalMillis = 5_000L,
msgHistoryCheck = 15
)
val helpModule = HelpModule(
val whiteListGroupMsgPollingModule = GroupMessagePollingModule(
moduleName = "WhiteListGroup",
targetGroupId = whitelistGroupId,
pollIntervalMillis = 5_000L,
msgHistoryCheck = 15
)
val commonHelpModule = HelpModule(
moduleName = "CommonGroup",
keywords = listOf("help", "帮助"),
groupMessagePollingModule = commonGroupMsgPollingModule,
selfId = selfQQId,
selfNickName = selfNickName,
)
val whitelistHelpModule = HelpModule(
moduleName = "WhiteListGroup",
keywords = listOf("help", "帮助"),
groupMessagePollingModule = groupMsgPollingModule,
groupMessagePollingModule = whiteListGroupMsgPollingModule,
selfId = selfQQId,
selfNickName = selfNickName,
)
val toolConfig = YamlConfigLoader.loadToolConfig()
val rconModule = RconPlayerListModule(
val corconModule = RconPlayerListModule(
moduleName = "WhiteListGroup",
groupMessagePollingModule = groupMsgPollingModule,
groupMessagePollingModule = commonGroupMsgPollingModule,
rconTimeOut = 2_000L,
cooldownMillis = 10_000L,
selfId = selfQQId,
@ -46,76 +59,106 @@ fun main() = GlobalManager.runBlockingMain {
"列表","服务器状态", "TPS", "tps", "list", "List"
)
)
val mailConfig = YamlConfigLoader.loadMailConfig()
val mailModule = MailModule(
val rconModule = RconPlayerListModule(
moduleName = "WhiteListGroup",
host = mailConfig.host.toString(),
authToken = mailConfig.decryptedPassword.toString(),
port = mailConfig.port!!,
senderEmailAddress = mailConfig.mailAddress!!,
)
val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
val invitationCodesModule = InvitationCodesModule(
moduleName = "WhiteListGroup",
groupMessagePollingModule = groupMsgPollingModule,
mailModule = mailModule,
apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!,
groupMessagePollingModule = whiteListGroupMsgPollingModule,
rconTimeOut = 2_000L,
cooldownMillis = 10_000L,
selfId = selfQQId,
selfNickName = selfNickName,
rconPath = toolConfig.rcon.mcRconToolPath.toString(),
rconConfigPath = toolConfig.rcon.mcRconToolConfigPath.toString(),
keywords = setOf(
"申请皮肤站注册邀请码",
"申请土豆服务器注册邀请码",
"申请LTD邀请码",
"Apply for an invitation code"
//形容
"土豆", "马铃薯", "Potato", "potato", "POTATO",
"Potatoes", "potatoes", "POTATOES", "🥔",
//正经
"列表","服务器状态", "TPS", "tps", "list", "List"
)
)
val mcServerStatusModule = McServerStatusModule(
moduleName = "WhiteListGroup",
groupMessagePollingModule = groupMsgPollingModule,
// val mailConfig = YamlConfigLoader.loadMailConfig()
// val mailModule = MailModule(
// moduleName = "WhiteListGroup",
// host = mailConfig.host.toString(),
// authToken = mailConfig.decryptedPassword.toString(),
// port = mailConfig.port!!,
// senderEmailAddress = mailConfig.mailAddress!!,
// )
// val blessingSkinConfig = YamlConfigLoader.loadBlessingSkinServerConfig()
// val invitationCodesModule = InvitationCodesModule(
// moduleName = "WhiteListGroup",
// groupMessagePollingModule = commonGroupMsgPollingModule,
// mailModule = mailModule,
// apiToken = blessingSkinConfig.invitationApi?.decryptedToken!!,
// selfId = selfQQId,
// keywords = setOf(
// "申请皮肤站注册邀请码",
// "申请土豆服务器注册邀请码",
// "申请LTD邀请码",
// "Apply for an invitation code"
// )
// )
val commonMcServerStatusModule = McServerStatusModule(
moduleName = "CommonGroup",
groupMessagePollingModule = commonGroupMsgPollingModule,
selfId = selfQQId,
cooldownMillis = 20_000L,
selfNickName = selfNickName,
commands = listOf("/m", "/mcs", "seek", "s"),
commands = listOf("/m", "/mcs", "seek", "s", "test"),
presetServer = mapOf(
setOf("先行土豆", "先行", "pre", "Pre", "BF", "bf", "p", "P") to "n2.akiracloud.net:10599",
setOf("土豆", "老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
)
)
val banModule = BanModule(
val whitelistMcServerStatusModule = McServerStatusModule(
moduleName = "WhiteListGroup",
groupMessagePollingModule = groupMsgPollingModule,
groupMessagePollingModule = whiteListGroupMsgPollingModule,
selfId = selfQQId,
adminsId = listOf(1283411677),
muteCommandPrefixList = listOf("口球", "mute", "Mute", "禁言"),
unmuteCommandPrefixList = listOf("解禁", "unmute", "Unmute", "解除禁言"),
minBanMinutes = 1,
maxBanMinutes = 15,
cooldownMillis = 20_000L,
selfNickName = selfNickName,
commands = listOf("/m", "/mcs", "seek", "s", "test"),
presetServer = mapOf(
setOf("老土豆", "七周目", "7" ,"ZZ", "zz", "Zz", "seven") to "main.mmccdd.top:11106",
setOf("土豆", "八周目", "8" ,"39", "eight") to "ac.r3944realms.top"
)
)
val dgLabModule = DGLabModule(
moduleName = "DG",
groupMessagePollingModule = commonGroupMsgPollingModule,
selfId = selfQQId,
adminIds = listOf(2561098830L),
commandHead = listOf("dglab")
)
// val modGroupHandlerModule = ModGroupHandlerModule(
// moduleName = "ModGroup",
// targetGroupId = 339340846,
// answers = listOf("戏鸢", "一只戏鸢", "折戏鸢", "LostInLinearPast", "lostinlinearpast"),
// pollIntervalMillis = 15_000L,
// )
// 注册模块到全局模块管理器
GlobalManager.moduleManager.registerModule(groupModule)
GlobalManager.moduleManager.registerModule(groupMsgPollingModule)
GlobalManager.moduleManager.registerModule(mcServerStatusModule)
GlobalManager.moduleManager.registerModule(commonGroupMsgPollingModule)
GlobalManager.moduleManager.registerModule(whiteListGroupMsgPollingModule)
GlobalManager.moduleManager.registerModule(commonMcServerStatusModule)
GlobalManager.moduleManager.registerModule(rconModule)
GlobalManager.moduleManager.registerModule(mailModule)
GlobalManager.moduleManager.registerModule(invitationCodesModule)
GlobalManager.moduleManager.registerModule(helpModule)
GlobalManager.moduleManager.registerModule(banModule)
GlobalManager.moduleManager.registerModule(corconModule)
GlobalManager.moduleManager.registerModule(whitelistMcServerStatusModule)
// GlobalManager.moduleManager.registerModule(mailModule)
// GlobalManager.moduleManager.registerModule(invitationCodesModule)
GlobalManager.moduleManager.registerModule(whitelistHelpModule)
GlobalManager.moduleManager.registerModule(commonHelpModule)
GlobalManager.moduleManager.registerModule(dgLabModule)
// GlobalManager.moduleManager.registerModule(banModule)
// GlobalManager.moduleManager.registerModule(modGroupHandlerModule)
// 加载模块
GlobalManager.moduleManager.loadModule(groupModule.name)
GlobalManager.moduleManager.loadModule(groupMsgPollingModule.name)
GlobalManager.moduleManager.loadModule(mcServerStatusModule.name)
GlobalManager.moduleManager.loadModule(commonGroupMsgPollingModule.name)
GlobalManager.moduleManager.loadModule(whiteListGroupMsgPollingModule.name)
GlobalManager.moduleManager.loadModule(commonMcServerStatusModule.name)
GlobalManager.moduleManager.loadModule(corconModule.name)
GlobalManager.moduleManager.loadModule(rconModule.name)
GlobalManager.moduleManager.loadModule(mailModule.name)
GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
GlobalManager.moduleManager.loadModule(helpModule.name)
GlobalManager.moduleManager.loadModule(banModule.name)
// GlobalManager.moduleManager.loadModule(mailModule.name)
// GlobalManager.moduleManager.loadModule(invitationCodesModule.name)
GlobalManager.moduleManager.loadModule(commonHelpModule.name)
GlobalManager.moduleManager.loadModule(whitelistMcServerStatusModule.name)
GlobalManager.moduleManager.loadModule(whitelistHelpModule.name)
GlobalManager.moduleManager.loadModule(dgLabModule.name)
// GlobalManager.moduleManager.loadModule(banModule.name)
// GlobalManager.moduleManager.loadModule(modGroupHandlerModule.name)
}

View File

@ -12,8 +12,8 @@ import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupShutListEvent
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupShutListRequest
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupBanRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
@ -78,7 +78,7 @@ class BanModule(
scope?.cancel()
}
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
// 先过一遍过滤器,只有符合条件的才进入后续处理
val filtered = triggerFilter.filter(messages)
for (msg in filtered) {
@ -91,7 +91,7 @@ class BanModule(
* - text 段直接拼接
* - 如果消息段里包含 @ MessageData 中为 qq 字段则拼成 "@{qq}"方便 parseMentionToUserId 解析
*/
private fun GetFriendMsgHistoryEvent.SpecificMsg.plainText(): String {
private fun MsgHistorySpecificMsg.plainText(): String {
return this.message.joinToString(" ") { seg ->
// 如果 message element 包含 qq 字段(即@用户),优先使用它
seg.data.qq?.let { "@${it}" } ?: (seg.data.text ?: "")
@ -100,7 +100,7 @@ class BanModule(
/**
* 从消息段中提取所有被 @ 的用户 ID
*/
private fun GetFriendMsgHistoryEvent.SpecificMsg.getMentionedUserIds(): List<ID> {
private fun MsgHistorySpecificMsg.getMentionedUserIds(): List<ID> {
return this.message
.filter { it.type == MessageType.At && it.data.qq != null }
.mapNotNull { it.data.qq }
@ -111,7 +111,7 @@ class BanModule(
}
}
}
private suspend fun processUnBanCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
private suspend fun processUnBanCommand(msg: MsgHistorySpecificMsg) {
try {
pardonCommandParse.parseCommand(msg.plainText()) ?: return
// 获取所有被 @ 的用户
@ -149,7 +149,7 @@ class BanModule(
saveState(banState)
}
}
private suspend fun processBanCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
private suspend fun processBanCommand(msg: MsgHistorySpecificMsg) {
try {
val parsed = banCommandParse.parseCommand(msg.plainText()) ?: return
val (_, argument) = parsed
@ -171,9 +171,9 @@ class BanModule(
is ID.LongValue -> target.value
}
// 权限检查:非管理员不能禁言他人
// 权限检查:非管理员不能禁言多个他人
if (mentionedUserIds.isNotEmpty() && mentionedUserIds.size != 1 && msg.sender.userId !in adminsId) {
sendGroupMessage("❌ 你没有权限禁言使用禁言多用户功能", msg.realId)
sendGroupMessage("❌ 你没有权限使用禁言多用户功能", msg.realId)
continue
}
@ -201,7 +201,7 @@ class BanModule(
}
val selfDuration = durationSeconds * factorX
if (Random.nextInt(100) < chance) {
if (Random.nextInt(0,100) > chance) {
// 触发反禁自己
banUser(ID.long(msg.sender.userId), groupMessagePollingModule.targetGroupId, selfDuration)
sendGroupMessage(
@ -262,6 +262,7 @@ class BanModule(
override fun info(): String {
return buildString {
append("[$name] 指令禁言模块:\n")
append(" 管理员用户ID: ${adminsId}\n")
append(" - 用户发送 ${banCommandParse.getCommands().joinToString("、")} 来禁言自己或指定其他用户(需管理员权限)。\n")
append(" - 支持指定禁言分钟数或随机分钟数,范围 $minBanMinutes-$maxBanMinutes 分钟。\n")
append(" - 支持对单个 @ 用户禁言,有概率反禁自己(骰子点数决定概率)。\n")

View File

@ -84,22 +84,46 @@ abstract class BaseModule(baseName : String = "BaseModule", idName : String = ""
* 提供访问全局 NapCatClient 的快捷方式
*/
protected val napCatClient get() = GlobalManager.napCatClient
/**
* 提供访问全局 blessingSkinClient 的快捷方式
*/
protected val blessingSkinClient get() = GlobalManager.blessingSkinClient
/**
* 提供访问全局 mcSrvStatusClient 的快捷方式
*/
protected val mcSrvStatusClient get() = GlobalManager.mcSrvStatusClient
/**
* 提供访问全局 加载模块 的快捷方式
*/
protected val moduleMap get() = GlobalManager.moduleManager.getModules()
/**
* 获取数据库连接
* 使用 try-with-resources 时会自动关闭
*/
protected fun getConnection() = GlobalManager.getConnection()
/**
* 安全获取 NapCatClient避免空指针异常
*/
protected fun getNapCatClientOrNull() = try {
GlobalManager.napCatClient
} catch (e: Exception) {
LoggerUtil.logger.warn("获取NapCatClient失败", e)
null
}
/**
* 安全获取 NapCatClient如果获取失败则抛出详细异常
*/
protected fun getNapCatClientOrThrow(): Any {
val client = try {
GlobalManager.napCatClient
} catch (e: Exception) {
throw IllegalStateException("无法获取NapCatClient请检查GlobalManager初始化状态", e)
}
return client ?: throw IllegalStateException("NapCatClient为null请检查GlobalManager初始化")
}
}

View File

@ -1,14 +1,489 @@
package top.r3944realms.ltdmanager.module
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.LongArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument
import com.mojang.brigadier.exceptions.CommandSyntaxException
import com.r3944realms.dg_lab.api.message.IPowerBoxMsg
import com.r3944realms.dg_lab.api.message.argType.ChangePolicy
import com.r3944realms.dg_lab.api.message.argType.Channel
import com.r3944realms.dg_lab.api.websocket.message.MessageDirection
import com.r3944realms.dg_lab.manager.DGPBClientManager
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.dglab.DgLab
import top.r3944realms.ltdmanager.dglab.model.game.GameClientOperation
import top.r3944realms.ltdmanager.dglab.model.game.GameServerOperation
import top.r3944realms.ltdmanager.dglab.model.game.Player
import top.r3944realms.ltdmanager.dglab.model.pulseware.DefaultPulseData
import top.r3944realms.ltdmanager.module.common.filter.TriggerMessageFilter
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter
import top.r3944realms.ltdmanager.module.common.filter.type.KeywordFilter
import top.r3944realms.ltdmanager.module.common.filter.type.NewMessageFilter
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupMemberListEvent
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupMemberListRequest
import top.r3944realms.ltdmanager.napcat.request.message.SetMsgEmojiLikeRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.io.File
import kotlin.math.abs
/**
* 数据 {QQ}
*/
class DGLabModule(
moduleName: String,
):
BaseModule("DGLabModule", moduleName) {
override fun onLoad() {
private val groupMessagePollingModule : GroupMessagePollingModule,
private val selfId: Long,
val adminIds: List<Long> = listOf(),
val maxClientNumber: Int = 10,
val commandHead: List<String> = listOf("dglab"),
) : BaseModule("DGLabModule", moduleName), PersistentState<DGLabModule.DgLabState> {
var dgLabManager: DgLab? = null
private var scope: CoroutineScope? = null
private var dglabCommandDispatcher: CommandDispatcher<Player> = CommandDispatcher<Player>().apply {
for (command in commandHead) register(
literal<Player>(command)
.then(literal<Player?>("server").requires { adminIds.contains(it.id) }
.then(literal<Player?>("start").executes { startDgLab() })
.then(literal<Player?>("stop").executes { stopDgLab() })
.then(literal<Player?>("stopAllClient").executes { stopAllDgLabClient() })
)
.then(literal<Player?>("client")
.then(literal<Player?>("start").executes { startClient(it.source.id) })
.then(literal<Player?>("stop").executes { stopClient(it.source.id) })
)
.then(literal<Player?>("strength")
.then(argument<Player?, String>("channel", StringArgumentType.string())
.then(literal<Player?>("add")
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(-200, 200))
.executes { strengthAdd(it.source.id, StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
)
)
.then(literal<Player?>("set")
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(0, 200))
.executes { strengthSet(it.source.id, StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
)
)
)
.then(argument<Player?, Long>("player", LongArgumentType.longArg())
.then(argument<Player?, String>("channel", StringArgumentType.string())
.then(literal<Player?>("add")
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(-200, 200))
.executes { strengthAdd(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
)
)
.then(literal<Player?>("set")
.then(argument<Player?, Int>("value", IntegerArgumentType.integer(0, 200))
.executes { strengthSet(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), IntegerArgumentType.getInteger(it, "value")) }
)
)
)
)
)
.then(literal<Player?>("pulse")
.then(argument<Player?, String>("channel", StringArgumentType.string())
.then(literal<Player?>("clear").executes { pulseClear(it.source.id, StringArgumentType.getString(it, "channel")) })
.then(literal<Player?>("set")
.then(argument<Player?, String>("pulseName", StringArgumentType.string())
.then(argument<Player?, Int>("timer", IntegerArgumentType.integer(0, Int.MAX_VALUE))
.executes { pulseSet(it.source.id, StringArgumentType.getString(it, "channel"), StringArgumentType.getString(it, "pulseName"), IntegerArgumentType.getInteger(it, "timer")) }
)
)
)
)
.then(argument<Player?, Long>("player", LongArgumentType.longArg())
.then(argument<Player?, String>("channel", StringArgumentType.string())
.then(literal<Player?>("clear").executes { pulseClear(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel")) })
.then(literal<Player?>("set")
.then(argument<Player?, String>("pulseName", StringArgumentType.string())
.then(argument<Player?, Int>("timer", IntegerArgumentType.integer(0, Int.MAX_VALUE))
.executes { pulseSet(LongArgumentType.getLong(it, "player"), StringArgumentType.getString(it, "channel"), StringArgumentType.getString(it, "pulseName"), IntegerArgumentType.getInteger(it, "timer")) }
)
)
)
)
)
)
)
// .then(literal<Player?>("info").executes {}
// .then(argument<Player?, String>("player", StringArgumentType.string()).executes {})
// )
}
private val stateFile: File = getStateFileInternal("dg_lab_state.json", name)
private val stateBackupFile: File = getStateFileInternal("dg_lab_state.json.bak", name)
private var dgLabState = loadState()
override fun getState(): DgLabState = dgLabState
override fun getStateFileInternal(): File = stateFile
private val triggerFilter by lazy {
TriggerMessageFilter(
listOf(
IgnoreSelfFilter(selfId),
NewMessageFilter { userId ->
dgLabState.getLastTriggerTime(userId) to dgLabState.getLastTriggerRealId(userId)
},
KeywordFilter(commandHead.toSet())
)
)
}
override fun onLoad() {
LoggerUtil.logger.info("[$name] 模块已装载,监听群组: ${groupMessagePollingModule.targetGroupId}")
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope!!.launch {
LoggerUtil.logger.info("[$name] 轮询协程启动")
dgLabManager = DgLab()
val gameServerOperation = GameServerOperation(napCatClient, groupMessagePollingModule.targetGroupId)
dgLabManager?.createServerManager(gameServerOperation)?.let { dgLabManager?.initServerManager(it) }
gameServerOperation.serverManager = dgLabManager?.serverManager
init()
groupMessagePollingModule.messagesFlow.collect { messages ->
if (loaded) handleMessages(messages)
}
}
}
override suspend fun onUnload() {
saveState(dgLabState)
dgLabManager?.close()
scope?.cancel()
LoggerUtil.logger.info("[$name] 模块已卸载完成")
}
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
if (messages.isEmpty()) return
// 先对所有消息进行 @ 提及处理
val processedMessages = messages.map { msg ->
val processedText = processMessageMentionsToLong(msg)
msg to processedText
}
val triggerMsgs = processedMessages
.filter { (msg, _) -> filterTriggerMessages(listOf(msg)).isNotEmpty() }
.map { (msg, processedText) -> Triple(msg, msg.userId, processedText) }
if (triggerMsgs.isEmpty()) return
var refPlayer: Player? = null
var refMsg: MsgHistorySpecificMsg? = null
try {
triggerMsgs.forEach { (msg, userId, processedText) ->
refMsg = msg
LoggerUtil.logger.info("[$name] 原始消息用户: $userId")
LoggerUtil.logger.info("[$name] 处理后的命令: $processedText")
refPlayer = dgLabManager?.getPlayerManager()?.getPlayer(userId)
dgLabState = dgLabState.updateOrCreate(userId, msg.realId, msg.time)
val execute = dglabCommandDispatcher.execute(processedText, refPlayer)
scope?.launch {
GlobalManager.napCatClient.sendUnit(
SetMsgEmojiLikeRequest(
if (execute == 0) 1.0 else 2.0, ID.long(msg.realId), true
)
)
}
}
} catch (e: CommandSyntaxException) {
val reader = e.input // 用户输入
val cursor = e.cursor
val partialInput = reader.substring(0, cursor)
if (refPlayer != null) {
val node = dglabCommandDispatcher.parse(
partialInput,
dgLabManager?.getPlayerManager()?.getPlayer(refPlayer!!.id)
).context.nodes.lastOrNull()?.node
val usage = if (node != null) {
val values = dglabCommandDispatcher.getSmartUsage(node, refPlayer).values
if(!values.isEmpty()) "目前节点可使用的子命令: $values"
else "目前节点无用法"
} else {
"未找到用法"
}
sendFailedMessage(
napCatClient,
text = "指令解析错误:\n ${e.message}\n\n$usage",
qq = refMsg?.userId,
realId = refMsg?.realId,
time = refMsg?.time
)
}
}
catch (e: Exception) {
sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: ${e.message}")
} finally {
saveState(dgLabState)
}
}
/**
* 处理整个消息中的 @ 提及转换为 Long 类型并清理多余空格
*/
private fun processMessageMentionsToLong(msg: MsgHistorySpecificMsg): String {
val processedText = msg.message.joinToString(" ") { seg ->
when (seg.type) {
MessageType.At -> {
// 处理 @ 提及,转换为 Long
seg.data.qq?.let { qq ->
when (qq) {
is ID.StringValue -> qq.value.toLong().toString()
is ID.LongValue -> qq.value.toString()
}
} ?: seg.data.text ?: ""
}
MessageType.Text -> {
seg.data.text ?: ""
}
else -> ""
}
}.trim()
// 清理多余空格:将多个连续空格替换为单个空格
return processedText.replace(Regex("\\s+"), " ")
}
private suspend fun filterTriggerMessages(
messages: List<MsgHistorySpecificMsg>
): List<MsgHistorySpecificMsg> = triggerFilter.filter(messages)
private suspend fun init() {
val getGroupMemberListEvent = napCatClient.send<GetGroupMemberListEvent>(
GetGroupMemberListRequest(
ID.long(groupMessagePollingModule.targetGroupId),
false
)
)
dgLabManager?.initOrLoadPlayerManager(getGroupMemberListEvent.data.filter { !it.isRobot }
.associate { it.userId to it.nickname })
dgLabManager?.initClientManager()
}
// private fun getHelp(): Int {
// scope?.launch {
// sendMessage()
// }
// return 1
// }
private fun startDgLab(): Int {
dgLabManager?.getServer()?.start()
return 1
}
private fun stopDgLab(): Int {
dgLabManager?.getServer()?.stop()
return 1
}
private fun stopAllDgLabClient(): Int {
dgLabManager?.clientManager?.stopAll()
return 1
}
private fun startClient(qq: Long): Int {
if (dgLabManager?.getPlayerManager()?.getOnlinePlayerSize()!! > maxClientNumber) {
scope!!.launch {
sendFailedMessage(napCatClient, text = "无法启动新的客户端, 因为已到达最大连接数${maxClientNumber}")
}
return -1
}
val operation = GameClientOperation(
napCatClient,
groupMessagePollingModule.targetGroupId,
dgLabManager!!.getPlayerManager(),
qq
)
val dgpbClientManager = dgLabManager?.getClientOrCreate(
qq.toString(),
operation
)
operation.clientSelf = dgpbClientManager
dgpbClientManager?.start()
return 1
}
private fun stopClient(qq: Long): Int {
dgLabManager?.getClient(qq.toString())?.stop()
return 1
}
private fun strengthAdd(qq: Long, channel: String, value: Int): Int {
val client = dgLabManager?.getClient(qq.toString()) ?: return -1
val changePolicy = if(value >= 0) ChangePolicy.INCREASE else ChangePolicy.DECREASE
val strengthValue = abs(value)
when(channel) {
"a" -> sendStrengthChange(client, Channel.A, changePolicy, strengthValue)
"b" -> sendStrengthChange(client, Channel.B, changePolicy, strengthValue)
"ab" -> {
sendStrengthChange(client, Channel.A, changePolicy, strengthValue)
sendStrengthChange(client, Channel.B, changePolicy, strengthValue)
}
}
return 0
}
private fun strengthSet(qq: Long, channel: String, value: Int): Int {
val client = dgLabManager?.getClient(qq.toString()) ?: return -1
when(channel) {
"a" -> sendStrengthChange(client, Channel.A, ChangePolicy.GOTO, value)
"b" -> sendStrengthChange(client, Channel.B, ChangePolicy.GOTO, value)
"ab" -> {
sendStrengthChange(client, Channel.A, ChangePolicy.GOTO, value)
sendStrengthChange(client, Channel.B, ChangePolicy.GOTO, value)
}
}
return 0
}
private fun sendStrengthChange(client: DGPBClientManager, channel: Channel, policy: ChangePolicy, value: Int) {
client.send(IPowerBoxMsg.StrengthChange(channel, policy, value)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
}
private fun pulseClear(qq: Long, channel: String): Int {
val client = dgLabManager?.getClient(qq.toString()) ?: return -1
when(channel) {
"a" -> client.send(IPowerBoxMsg.Clear(Channel.A)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
"b" -> client.send(IPowerBoxMsg.Clear(Channel.B)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
"ab" -> {
client.send(IPowerBoxMsg.Clear(Channel.A)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
client.send(IPowerBoxMsg.Clear(Channel.B)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
}
}
return 0
}
private fun pulseSet(qq: Long, channel: String, pulseName: String, timer: Int): Int {
val client = dgLabManager?.getClient(qq.toString()) ?: return -1
val pulse = DefaultPulseData.allPulseWaveLists()[pulseName] ?: return -2
when(channel) {
"a" -> client.send(IPowerBoxMsg.Pulse(Channel.A, pulse, timer)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
"b" -> client.send(IPowerBoxMsg.Pulse(Channel.B, pulse, timer)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
"ab" -> {
client.send(IPowerBoxMsg.Pulse(Channel.A, pulse, timer)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
client.send(IPowerBoxMsg.Pulse(Channel.B, pulse, timer)
.toPowerBoxMessage(client.sharedData.connectionId, client.sharedData.targetWSId, MessageDirection.DirectType.CLIENT_TO_APPLICATION))
}
}
return 0
}
private suspend fun sendMessage(
client: NapCatClient,
qq: Long,
realId: Long,
time: Long,
text: String = "正常消息"
) {
LoggerUtil.logger.info("[$name] 发送消息: realId=$realId, text=$text")
val request = SendGroupMsgRequest(
MessageElement.reply(ID.long(realId), text),
ID.long(groupMessagePollingModule.targetGroupId)
)
client.sendUnit(request)
LoggerUtil.logger.info("[$name] 已发送 消息")
// 更新触发的最大 realId
dgLabState = dgLabState.updateOrCreate(qq, realId, time)
}
private suspend fun sendFailedMessage(
client: NapCatClient,
qq: Long? = null,
realId: Long? = null,
time: Long? = null,
text: String = "失败消息"
) {
LoggerUtil.logger.info("[$name] 发送失败消息: realId=$realId, text=$text")
if (realId != null && qq != null && time != null) {
val request = SendGroupMsgRequest(
MessageElement.reply(ID.long(realId), text),
ID.long(groupMessagePollingModule.targetGroupId)
)
client.sendUnit(request)
LoggerUtil.logger.info("[$name] 已发送 失败消息")
// 更新触发的最大 realId
dgLabState = dgLabState.updateOrCreate(qq, realId, time)
} else {
val request = SendGroupMsgRequest(
listOf(MessageElement.text(text)),
ID.long(groupMessagePollingModule.targetGroupId)
)
client.sendUnit(request)
LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]")
}
}
// -------- 持久化 -----------
@Serializable
data class DgLabDetail(
val realId : Long,
val time: Long,
)
@Serializable
data class DgLabState(
val map: Map<Long, DgLabDetail> = emptyMap()
) {
fun getLastTriggerTime(userId: Long): Long = map[userId]?.time ?: -1
fun getLastTriggerRealId(userId: Long): Long = map[userId]?.realId ?: -1
/**
* 更新或创建某个用户的触发信息
* - 如果传了 realId则更新 realId
* - 如果传了 time则更新 time
* - 其他字段保持原值
*/
fun updateOrCreate(
userId: Long,
realId: Long? = null,
time: Long? = null
): DgLabState {
val old = map[userId]
val newDetail = DgLabDetail(
realId = realId ?: old?.realId ?: -1,
time = time ?: old?.time ?: -1
)
val newMap = map.toMutableMap().apply { put(userId, newDetail) }
return copy(map = newMap)
}
}
override fun saveState(state: DgLabState) {
try {
if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true)
stateFile.writeText(Json.encodeToString(state))
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 保存状态失败", e)
}
}
override fun loadState(): DgLabState {
return try {
val fileToRead = when {
stateFile.exists() -> stateFile
stateBackupFile.exists() -> stateBackupFile
else -> null
} ?: return DgLabState()
Json.decodeFromString<DgLabState>(fileToRead.readText())
} catch (e: Exception) {
LoggerUtil.logger.warn("[$name] 读取状态失败", e)
DgLabState()
}
}
}

View File

@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.event.message.GetGroupMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
@ -19,11 +19,11 @@ class GroupMessagePollingModule(
private var scope: CoroutineScope? = null
// 用 Flow 存消息,其他模块可以订阅
private val _messagesFlow = MutableSharedFlow<List<GetFriendMsgHistoryEvent.SpecificMsg>>(
private val _messagesFlow = MutableSharedFlow<List<MsgHistorySpecificMsg>>(
replay = 1, // 保留最近一份消息
extraBufferCapacity = 1
)
val messagesFlow: SharedFlow<List<GetFriendMsgHistoryEvent.SpecificMsg>> = _messagesFlow.asSharedFlow()
val messagesFlow: SharedFlow<List<MsgHistorySpecificMsg>> = _messagesFlow.asSharedFlow()
override fun onLoad() {
LoggerUtil.logger.info("[$name] 启动消息轮询 (群: $targetGroupId)")
@ -31,12 +31,12 @@ class GroupMessagePollingModule(
scope!!.launch {
while (isActive && loaded) {
try {
val event = napCatClient.send(
val event = getNapCatClientOrNull()?.send<GetGroupMsgHistoryEvent>(
GetGroupMsgHistoryRequest(
count = msgHistoryCheck,
groupId = ID.long(targetGroupId)
)
) as? GetGroupMsgHistoryEvent
)
val messages = event?.data?.messages ?: emptyList()
LoggerUtil.logger.debug("[$name] 拉取到 ${messages.size} 条消息")

View File

@ -137,7 +137,7 @@ class GroupRequestHandlerModule(
try {
getConnection().use { conn ->
val stmt = conn.prepareStatement(
"SELECT status FROM minecraft_manager_ltd.players WHERE qq=?"
"SELECT status FROM minecraft_manager_ltd_8.players WHERE qq=?"
)
stmt.setLong(1, actor)
val rs = stmt.executeQuery()

View File

@ -17,7 +17,7 @@ import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
@ -37,7 +37,7 @@ class HelpModule(
// 命令解析器
private val commandParser = CommandParser(keywords)
private val GetFriendMsgHistoryEvent.SpecificMsg.textContent: String
private val MsgHistorySpecificMsg.textContent: String
get() = message.joinToString("") { it.data.text ?: "" }
// 持久化文件
@ -100,7 +100,7 @@ class HelpModule(
LoggerUtil.logger.info("[$name] 模块已卸载完成")
}
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
val filtered = triggerFilter.filter(messages)
val triggerMsg = filtered.maxByOrNull { it.time } ?: return
@ -117,7 +117,7 @@ class HelpModule(
}
}
private suspend fun sendAllModulesHelp(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
private suspend fun sendAllModulesHelp(msg: MsgHistorySpecificMsg) {
val messages = moduleMap.map { (name, module) ->
val textBuilder = StringBuilder()
textBuilder.appendLine("===== $name =====")
@ -153,7 +153,7 @@ class HelpModule(
updateTriggerState(msg)
}
private suspend fun sendModuleHelp(msg: GetFriendMsgHistoryEvent.SpecificMsg, moduleName: String, module: BaseModule) {
private suspend fun sendModuleHelp(msg: MsgHistorySpecificMsg, moduleName: String, module: BaseModule) {
val textBuilder = StringBuilder()
textBuilder.appendLine("===== $moduleName =====")
textBuilder.appendLine(module.info())
@ -187,7 +187,7 @@ class HelpModule(
updateTriggerState(msg)
}
private suspend fun sendText(msg: GetFriendMsgHistoryEvent.SpecificMsg, text: String) {
private suspend fun sendText(msg: MsgHistorySpecificMsg, text: String) {
val request = SendGroupMsgRequest(
MessageElement.reply(ID.long(msg.realId), text),
ID.long(groupMessagePollingModule.targetGroupId)
@ -196,7 +196,7 @@ class HelpModule(
updateTriggerState(msg)
}
private fun updateTriggerState(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
private fun updateTriggerState(msg: MsgHistorySpecificMsg) {
lastTriggerState.lastTriggeredRealId = msg.realId
lastTriggerState.lastTriggerTime = msg.time
saveState(lastTriggerState)

View File

@ -20,7 +20,7 @@ import top.r3944realms.ltdmanager.module.exception.InvitationCodeException
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.HtmlTemplateUtil
import top.r3944realms.ltdmanager.utils.LoggerUtil
@ -165,14 +165,14 @@ class InvitationCodesModule(
// =========================
// 消息处理主流程
// =========================
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
if (messages.isEmpty()) return
val triggerMsgs = filterTriggerMessages(messages)
if (triggerMsgs.isEmpty()) return
try {
val hadValidCodeButNotUsed = mutableListOf<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>()
val needNewCode = mutableListOf<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>()
val hadValidCodeButNotUsed = mutableListOf<Pair<Long, MsgHistorySpecificMsg>>()
val needNewCode = mutableListOf<Pair<Long, MsgHistorySpecificMsg>>()
getIdAndSelectSituation(triggerMsgs, hadValidCodeButNotUsed, needNewCode)
createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode)
@ -186,8 +186,8 @@ class InvitationCodesModule(
/** 过滤出符合条件的触发消息 */
private suspend fun filterTriggerMessages(
messages: List<GetFriendMsgHistoryEvent.SpecificMsg>
): List<GetFriendMsgHistoryEvent.SpecificMsg> {
messages: List<MsgHistorySpecificMsg>
): List<MsgHistorySpecificMsg> {
// 先应用通用过滤器
val filtered = triggerFilter.filter(messages)
@ -198,9 +198,9 @@ class InvitationCodesModule(
.mapNotNull { (_, msgs) -> msgs.maxByOrNull { it.time } }
}
private suspend fun getIdAndSelectSituation(msgs: List<GetFriendMsgHistoryEvent.SpecificMsg>,
hadVaildCodeButNotUseList : MutableList<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>,
needNewCodeList: MutableList<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>) {
private suspend fun getIdAndSelectSituation(msgs: List<MsgHistorySpecificMsg>,
hadVaildCodeButNotUseList : MutableList<Pair<Long, MsgHistorySpecificMsg>>,
needNewCodeList: MutableList<Pair<Long, MsgHistorySpecificMsg>>) {
if (msgs.isEmpty()) return
val qqIds = msgs.map { it.userId }
@ -273,7 +273,7 @@ class InvitationCodesModule(
sendFailedMessage(napCatClient, text = "批量查询用户资格信息失败,请联系管理员: ${e.message}")
}
}
private suspend fun hadVaildCodeButNotUseListHandler(list: List<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>) {
private suspend fun hadVaildCodeButNotUseListHandler(list: List<Pair<Long, MsgHistorySpecificMsg>>) {
if (list.isEmpty()) return
val whiteListIds = list.map { it.first }
@ -405,7 +405,7 @@ class InvitationCodesModule(
lastTriggerMapState = lastTriggerMapState.updateLastTrigger(qq, realId, -1)
}
private suspend fun createAndSearchInvitationCodeIdsThenUpdateDate(
needNewTokenIdAndMsgPairs: List<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>,
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>,
) {
if (needNewTokenIdAndMsgPairs.isEmpty()) return
@ -461,7 +461,7 @@ class InvitationCodesModule(
*/
private fun validateCodeCountMatch(
invitationCodes: List<InvitationCodeGenerationResponse.InvitationCode>?,
needNewTokenIdAndMsgPairs: List<Pair<Long, GetFriendMsgHistoryEvent.SpecificMsg>>
needNewTokenIdAndMsgPairs: List<Pair<Long, MsgHistorySpecificMsg>>
) {
if (invitationCodes == null) {
throw InvitationCodeException.ApiFailureException("获取邀请码请求失败")

View File

@ -17,7 +17,7 @@ import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
@ -112,7 +112,7 @@ class McServerStatusModule(
LoggerUtil.logger.info("[$name] 模块已卸载完成")
}
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
if (messages.isEmpty()) return
val triggerMsgs = filterTriggerMessages(messages)
if (triggerMsgs.isEmpty()) return
@ -129,8 +129,8 @@ class McServerStatusModule(
}
private suspend fun filterTriggerMessages(
messages: List<GetFriendMsgHistoryEvent.SpecificMsg>
): List<GetFriendMsgHistoryEvent.SpecificMsg> = triggerFilter.filter(messages)
messages: List<MsgHistorySpecificMsg>
): List<MsgHistorySpecificMsg> = triggerFilter.filter(messages)
private suspend fun sendFailedMessage(
client: NapCatClient,
@ -169,7 +169,7 @@ class McServerStatusModule(
private suspend fun processCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
private suspend fun processCommand(msg: MsgHistorySpecificMsg) {
// 找出文本内容
val text = msg.message
.firstOrNull { it.type == MessageType.Text }
@ -226,7 +226,7 @@ class McServerStatusModule(
// ---------------- 转发消息封装 ----------------
private suspend fun sendStatusForwardMessage(
client: NapCatClient,
msg: GetFriendMsgHistoryEvent.SpecificMsg,
msg: MsgHistorySpecificMsg,
address: String,
status: McServerStatus,
realId: Long,

View File

@ -93,7 +93,7 @@ class ModGroupHandlerModule(
}
saveState(state)
}
fun getRejectRecord(userId: Long): RejectRecord? {
private fun getRejectRecord(userId: Long): RejectRecord? {
return getState().records[userId]
}

View File

@ -9,8 +9,8 @@ interface PersistentState<T> {
fun saveState(state: T)
fun loadState(): T
// 默认实现:统一管理 data 目录下的文件
fun getStateFileInternal(name: String, moduleName: String): File {
val dataDir = File("data", FileNameFilter.filterFileName(moduleName))
fun getStateFileInternal(name: String, subName: String): File {
val dataDir = File("data", FileNameFilter.filterFileName(subName))
if (!dataDir.exists()) dataDir.mkdirs()
return File(dataDir, name)
}

View File

@ -17,7 +17,7 @@ import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.CmdUtil
@ -110,7 +110,7 @@ class RconPlayerListModule(
LoggerUtil.logger.info("[$name] 模块已卸载完成")
}
private suspend fun handleMessages(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>) {
private suspend fun handleMessages(messages: List<MsgHistorySpecificMsg>) {
val filtered = triggerFilter.filter(messages)
// RCON 模块只取最新的一条消息
@ -124,7 +124,7 @@ class RconPlayerListModule(
}
}
}
private suspend fun processTrigger(msg: GetFriendMsgHistoryEvent.SpecificMsg) {
private suspend fun processTrigger(msg: MsgHistorySpecificMsg) {
LoggerUtil.logger.info("[$name] 执行 RCON 查询")
val commands = listOf("forge tps", "list")

View File

@ -1,4 +1,27 @@
package top.r3944realms.ltdmanager.module
class StateModule {
import kotlinx.coroutines.*
import top.r3944realms.ltdmanager.napcat.request.account.SetQQProfileRequest
//TODO: 有问题不要使用 #unload得考虑下怎么写
class StateModule(
moduleName: String,
private val onlineName: String,
private val offlineName: String,
): BaseModule("StateModule", moduleName) {
private var scope: CoroutineScope? = null
override fun onLoad() {
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope!!.launch {
if (loaded) updateProfile(onlineName)
}
}
private suspend fun updateProfile(name: String) {
napCatClient.sendUrgentUnit(SetQQProfileRequest(name))
}
override suspend fun onUnload() {
updateProfile(offlineName)
scope!!.cancel()
}
}

View File

@ -1,4 +1,342 @@
package top.r3944realms.ltdmanager.module.common
/**
* 高级命令解析器
* 支持自定义参数语法和参数验证
*/
class AdvancedCommandParser {
private val commands = mutableListOf<CommandDefinition>()
/**
* 命令定义类
*/
data class CommandDefinition(
val name: String,
val aliases: List<String> = emptyList(),
val syntax: String = "",
val description: String = "",
val parameterPattern: Regex = DEFAULT_PARAMETER_PATTERN
) {
val allCommandForms: List<String> get() = listOf(name) + aliases
}
/**
* 解析结果
*/
data class ParseResult(
val command: String,
val arguments: Map<String, String> = emptyMap(),
val rawArguments: List<String> = emptyList(),
val isValid: Boolean = true,
val errorMessage: String? = null,
val commandDefinition: CommandDefinition? = null
)
companion object {
// 默认参数模式:<参数名> 或 [可选参数名]
val DEFAULT_PARAMETER_PATTERN = Regex("""<(\w+)>|\[(\w+)]""")
// 常用参数模式
/**
* 必需参数
*/
val ANGLE_BRACKETS = Regex("""<(\w+)>""") // <param>
/**
* 可选参数
*/
val SQUARE_BRACKETS = Regex("""\[(\w+)]""") // [param]
/**
* 自定义参数类型
*/
val CURLY_BRACES = Regex("""\{(\w+)}""") // {param}
}
/**
* 注册命令
*/
fun registerCommand(
name: String,
aliases: List<String> = emptyList(),
syntax: String = "",
description: String = "",
parameterPattern: Regex = DEFAULT_PARAMETER_PATTERN
): AdvancedCommandParser {
commands.add(CommandDefinition(name, aliases, syntax, description, parameterPattern))
return this
}
/**
* 批量注册命令
*/
fun registerCommands(vararg commandDefs: CommandDefinition): AdvancedCommandParser {
commands.addAll(commandDefs)
return this
}
/**
* 智能分割参数正确处理引号内的空格
*/
private fun smartSplit(input: String): List<String> {
val result = mutableListOf<String>()
val current = StringBuilder()
var inQuotes = false
var quoteChar: Char? = null
var escapeNext = false
for (char in input) {
when {
escapeNext -> {
current.append(char)
escapeNext = false
}
char == '\\' -> {
escapeNext = true
}
char == '"' || char == '\'' -> {
if (inQuotes && char == quoteChar) {
// 结束引号
inQuotes = false
quoteChar = null
} else if (!inQuotes) {
// 开始引号
inQuotes = true
quoteChar = char
} else {
current.append(char)
}
}
char == ' ' && !inQuotes -> {
// 空格分隔,但不是引号内
if (current.isNotEmpty()) {
result.add(current.toString())
current.clear()
}
}
else -> {
current.append(char)
}
}
}
if (current.isNotEmpty()) {
result.add(current.toString())
}
return result
}
/**
* 解析命令
*/
private fun parse(input: String): ParseResult {
val trimmedInput = input.trim()
if (trimmedInput.isEmpty()) {
return ParseResult("", isValid = false, errorMessage = "输入为空")
}
// 分割命令和参数
val parts = smartSplit(trimmedInput)
val commandPart = parts[0]
// 查找匹配的命令定义
val commandDef = commands.find { def ->
def.allCommandForms.any { it.equals(commandPart, ignoreCase = true) }
}
if (commandDef == null) {
return ParseResult(
commandPart,
isValid = false,
errorMessage = "未知命令: $commandPart"
)
}
// 解析参数
val arguments = parseArguments(commandDef, parts.drop(1))
val rawArgs = parts.drop(1)
return ParseResult(
command = commandDef.name,
arguments = arguments,
rawArguments = rawArgs,
commandDefinition = commandDef
)
}
/**
* 解析参数
*/
private fun parseArguments(commandDef: CommandDefinition, args: List<String>): Map<String, String> {
val parameters = extractParameterNames(commandDef.syntax, commandDef.parameterPattern)
val result = mutableMapOf<String, String>()
if (parameters.isEmpty()) {
args.forEachIndexed { index, value -> result["arg${index + 1}"] = value }
return result
}
val positionals = mutableListOf<String>()
val namedParams = mutableMapOf<String, String>()
var i = 0
// 第一遍:处理命名参数
while (i < args.size) {
when {
args[i].startsWith("--") -> {
val paramName = args[i].substring(2)
if (paramName in parameters) {
if (i + 1 < args.size && !args[i + 1].startsWith("-")) {
namedParams[paramName] = args[i + 1]
i += 2
} else {
namedParams[paramName] = "true"
i += 1
}
} else {
positionals.add(args[i])
i += 1
}
}
args[i].startsWith("-") && args[i].length > 1 && !args[i].startsWith("--") -> {
val paramName = args[i].substring(1)
if (paramName in parameters) {
if (i + 1 < args.size && !args[i + 1].startsWith("-")) {
namedParams[paramName] = args[i + 1]
i += 2
} else {
namedParams[paramName] = "true"
i += 1
}
} else {
positionals.add(args[i])
i += 1
}
}
else -> {
positionals.add(args[i])
i += 1
}
}
}
// 第二遍:映射位置参数
var posIndex = 0
for (paramName in parameters) {
if (paramName !in namedParams && posIndex < positionals.size) {
result[paramName] = positionals[posIndex]
posIndex++
} else if (paramName in namedParams) {
result[paramName] = namedParams[paramName]!!
}
}
// 处理额外参数
for (j in posIndex until positionals.size) {
result["extraArg${j - posIndex + 1}"] = positionals[j]
}
return result
}
/**
* 从语法字符串中提取参数名
*/
private fun extractParameterNames(syntax: String, pattern: Regex): List<String> {
if (syntax.isEmpty()) return emptyList()
return pattern.findAll(syntax).map { matchResult ->
matchResult.groupValues[1].ifEmpty { matchResult.groupValues[2] }
}.toList()
}
/**
* 验证参数是否符合要求
*/
fun validateArguments(result: ParseResult): ParseResult {
if (!result.isValid) return result
val commandDef = result.commandDefinition ?: return result.copy(
isValid = false,
errorMessage = "命令定义不存在"
)
val requiredParams = extractParameterNames(commandDef.syntax, ANGLE_BRACKETS)
val missingParams = requiredParams.filter { it !in result.arguments }
return if (missingParams.isNotEmpty()) {
result.copy(
isValid = false,
errorMessage = "缺少必需参数: ${missingParams.joinToString()}"
)
} else {
result
}
}
/**
* 获取命令的帮助信息增强版
*/
fun getCommandHelp(commandName: String): String? {
val commandDef = commands.find { it.name == commandName || commandName in it.aliases }
return commandDef?.let { def ->
buildString {
appendLine("命令: ${def.name}")
if (def.aliases.isNotEmpty()) {
appendLine("别名: ${def.aliases.joinToString()}")
}
appendLine("用法: ${def.name} ${def.syntax}")
appendLine("描述: ${def.description}")
// 显示参数说明
val params = extractParameterNames(def.syntax, def.parameterPattern)
if (params.isNotEmpty()) {
appendLine("参数:")
params.forEach { param ->
val isRequired = def.syntax.contains("<$param>")
appendLine(" ${if (isRequired) "<$param>" else "[$param]"} - ${if (isRequired) "必需" else "可选"}")
}
}
}
}
}
/**
* 获取所有注册的命令
*/
fun getRegisteredCommands(): List<CommandDefinition> = commands.toList()
/**
* 获取所有命令的帮助信息
*/
fun getAllCommandsHelp(): String {
return buildString {
appendLine("可用命令:")
appendLine("=".repeat(10))
commands.forEach { def ->
appendLine("${def.name} - ${def.description}")
if (def.aliases.isNotEmpty()) {
appendLine(" 别名: ${def.aliases.joinToString()}")
}
appendLine(" 用法: ${def.name} ${def.syntax}")
appendLine()
}
}
}
/**
* 检查输入是否包含有效命令
*/
fun containsCommand(input: String): Boolean {
val trimmedInput = input.trim()
if (trimmedInput.isEmpty()) return false
val commandPart = trimmedInput.split("\\s+".toRegex())[0]
return commands.any { def ->
def.allCommandForms.any { it.equals(commandPart, ignoreCase = true) }
}
}
/**
* 快速解析包含验证
*/
fun parseAndValidate(input: String): ParseResult {
return validateArguments(parse(input))
}
}

View File

@ -1,7 +1,7 @@
package top.r3944realms.ltdmanager.module.common.filter
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
interface MessageFilter {
suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean
suspend fun test(msg: MsgHistorySpecificMsg): Boolean
}

View File

@ -1,12 +1,12 @@
package top.r3944realms.ltdmanager.module.common.filter
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
class TriggerMessageFilter(private val filters: List<MessageFilter>) {
suspend fun filter(messages: List<GetFriendMsgHistoryEvent.SpecificMsg>)
: List<GetFriendMsgHistoryEvent.SpecificMsg> {
suspend fun filter(messages: List<MsgHistorySpecificMsg>)
: List<MsgHistorySpecificMsg> {
val result = mutableListOf<GetFriendMsgHistoryEvent.SpecificMsg>()
val result = mutableListOf<MsgHistorySpecificMsg>()
for (msg in messages) {
if (filters.all { it.test(msg) }) {
result.add(msg)

View File

@ -1,4 +1,17 @@
package top.r3944realms.ltdmanager.module.common.filter.type
class AdvancedCommonFilter {
import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
class AdvancedCommonFilter(private val advancedCommandParser: AdvancedCommandParser): MessageFilter {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
return msg.message.any { seg ->
seg.type == MessageType.Text && seg.data.text?.let { text ->
advancedCommandParser.getRegisteredCommands().map { it.name }.any { name -> text.startsWith(name) }
} == true
}
}
}

View File

@ -3,11 +3,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type
import top.r3944realms.ltdmanager.module.common.CommandParser
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
/** 命令解析器匹配 */
class CommandFilter(private val parser: CommandParser) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
return msg.message.any { seg ->
seg.type == MessageType.Text && seg.data.text?.let { parser.containsCommand(it) } == true
}

View File

@ -2,14 +2,14 @@ package top.r3944realms.ltdmanager.module.common.filter.type
import top.r3944realms.ltdmanager.module.common.cooldown.CooldownManager
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
class CooldownFilter(
private val cooldownManager: CooldownManager<*>,
private val sendCooldown: suspend (GetFriendMsgHistoryEvent.SpecificMsg, Long) -> Unit
private val sendCooldown: suspend (MsgHistorySpecificMsg, Long) -> Unit
) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
val result = cooldownManager.checkAndHandle(msg.userId, msg.realId)
if (!result.allowed && result.notify) {
sendCooldown(msg, result.remaining)

View File

@ -1,11 +1,11 @@
package top.r3944realms.ltdmanager.module.common.filter.type
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
/** 忽略机器人自己的消息 */
class IgnoreSelfFilter(private val selfId: Long) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
return msg.userId != selfId
}
}

View File

@ -2,11 +2,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
/** 文本关键词匹配 */
class KeywordFilter(private val keywords: Set<String>) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
return msg.message.any { seg ->
seg.type == MessageType.Text && seg.data.text?.let { text ->
keywords.any { keyword -> text.startsWith(keyword) }

View File

@ -3,11 +3,11 @@ package top.r3944realms.ltdmanager.module.common.filter.type
import top.r3944realms.ltdmanager.module.common.CommandParser
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
/** 多命令解析器匹配 */
class MultiCommandFilter(private val parsers: List<CommandParser>) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
return msg.message.any { seg ->
seg.type == MessageType.Text && seg.data.text?.let { text ->
parsers.any { parser -> parser.containsCommand(text) }

View File

@ -1,7 +1,7 @@
package top.r3944realms.ltdmanager.module.common.filter.type
import top.r3944realms.ltdmanager.module.common.filter.MessageFilter
import top.r3944realms.ltdmanager.napcat.event.message.GetFriendMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.utils.Environment
import top.r3944realms.ltdmanager.utils.LoggerUtil
@ -9,7 +9,7 @@ import top.r3944realms.ltdmanager.utils.LoggerUtil
class NewMessageFilter(
private val getLastTrigger: (Long) -> Pair<Long, Long> // (time, realId)
) : MessageFilter {
override suspend fun test(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean {
override suspend fun test(msg: MsgHistorySpecificMsg): Boolean {
val (lastTime, lastRealId) = getLastTrigger(msg.userId)
val result = msg.time > lastTime || (msg.time == lastTime && msg.realId > lastRealId)
if (Environment.isDevelopment()) LoggerUtil.logger.debug("NewMessageFilter: msg.time=${msg.time}, msg.realId=${msg.realId}, lastTime=$lastTime, lastRealId=$lastRealId, result=$result")

View File

@ -1,4 +1,35 @@
package top.r3944realms.ltdmanager.napcat.data
class GroupMember {
}
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GroupMember(
@SerialName("group_id")
val groupId: Long,
@SerialName("user_id")
val userId: Long,
val nickname: String,
val card: String,
val sex: String,
val age: Int,
val area: String,
val level: String,
@SerialName("qq_level")
val qqLevel: Int,
@SerialName("join_time")
val joinTime: Long,
@SerialName("last_sent_time")
val lastSentTime: Long,
@SerialName("title_expire_time")
val titleExpireTime: Long,
val unfriendly: Boolean,
@SerialName("card_changeable")
val cardChangeable: Boolean,
@SerialName("is_robot")
val isRobot: Boolean,
@SerialName("shut_up_timestamp")
val shutUpTimestamp: Long,
val role: String,
val title: String
)

View File

@ -1,4 +1,14 @@
@file:Suppress("EXTERNAL_SERIALIZER_USELESS")
package top.r3944realms.ltdmanager.napcat.data.msghistory
class MsgHistoryContent {
import kotlinx.serialization.Serializable
import top.r3944realms.ltdmanager.napcat.serializer.MsgHistoryContentSerializer
@Serializable(with = MsgHistoryContentSerializer::class)
sealed class MsgHistoryContent {
@Serializable
class StringValue(val value: String) : MsgHistoryContent()
@Serializable
class SpecificMsgList(val value: List<MsgHistorySpecificMsg>) : MsgHistoryContent()
}

View File

@ -1,4 +1,33 @@
package top.r3944realms.ltdmanager.napcat.data.msghistory
class MsgHistoryMessage {
}
import kotlinx.serialization.Serializable
import top.r3944realms.ltdmanager.napcat.data.MessageType
/**
* 文本消息
*
* 艾特消息
*
* 表情消息
*
* 图片消息
*
* 文件消息
*
* 回复消息
*
* JSON消息
*
* 语音消息
*
* 视频消息
*
* markdown消息
*
* 消息forward
*/
@Serializable
data class MsgHistoryMessage (
val data: MsgHistoryMessageData,
val type: MessageType
)

View File

@ -1,4 +1,55 @@
package top.r3944realms.ltdmanager.napcat.data.msghistory
class MsgHistorySpecificMsg {
}
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import top.r3944realms.ltdmanager.napcat.data.Sender
/**
* 消息详情
*/
@Serializable
data class MsgHistorySpecificMsg (
val font: Long,
@SerialName("group_id")
val groupId: Long? = null,
val message: List<MsgHistoryMessage>,
@SerialName("message_format")
val messageFormat: String,
@SerialName("message_id")
val messageId: Long,
@SerialName("message_seq")
val messageSeq: Long,
@SerialName("message_type")
val messageType: String,
@SerialName("post_type")
val postType: String,
@SerialName("raw_message")
val rawMessage: String,
@SerialName("real_id")
val realId: Long,
@SerialName("real_seq")
val realSeq: String,
@SerialName("self_id")
val selfId: Long,
val sender: Sender,
@SerialName("sub_type")
val subType: String,
val time: Long,
@SerialName("user_id")
val userId: Long
)

View File

@ -3,7 +3,7 @@ package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonArray
import top.r3944realms.ltdmanager.napcat.data.GroupMember
/**
* GetGroupMemberList事件
@ -22,7 +22,7 @@ data class GetGroupMemberListEvent(
@Transient
val echo0: String? = null,
val data: JsonArray
val data: List<GroupMember>
) : AbstractGroupEvent(status0, retcode0, message0, wording0, echo0) {
override fun subtype(): String {

View File

@ -1,12 +1,9 @@
package top.r3944realms.ltdmanager.napcat.event.message
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.data.Sender
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.event.group.AbstractGroupEvent
/**
@ -30,106 +27,9 @@ data class GetFriendMsgHistoryEvent(
) : AbstractGroupEvent(status0, retcode0, message0, wording0, echo0) {
@Serializable
data class Data (
val messages: List<SpecificMsg>
val messages: List<MsgHistorySpecificMsg>
)
@Serializable
sealed class Content {
class StringValue(val value: String) : Content()
class SpecificMsgList(val value: List<SpecificMsg>) : Content()
}
@Serializable
data class MessageData (
val text: String? = null,
val name: String? = null,
val qq: ID? = null,
val id: ID? = null,
val file: String? = null,
/**
* 外显
*/
val summary: String? = null,
val data: String? = null,
val content: Content? = null
)
/**
* 文本消息
*
* 艾特消息
*
* 表情消息
*
* 图片消息
*
* 文件消息
*
* 回复消息
*
* JSON消息
*
* 语音消息
*
* 视频消息
*
* markdown消息
*
* 消息forward
*/
@Serializable
data class Message (
val data: MessageData,
val type: MessageType
)
/**
* 消息详情
*/
@Serializable
data class SpecificMsg (
val font: Long,
@SerialName("group_id")
val groupId: Long? = null,
val message: List<Message>,
@SerialName("message_format")
val messageFormat: String,
@SerialName("message_id")
val messageId: Long,
@SerialName("message_seq")
val messageSeq: Long,
@SerialName("message_type")
val messageType: String,
@SerialName("post_type")
val postType: String,
@SerialName("raw_message")
val rawMessage: String,
@SerialName("real_id")
val realId: Long,
@SerialName("real_seq")
val realSeq: String,
@SerialName("self_id")
val selfId: Long,
val sender: Sender,
@SerialName("sub_type")
val subType: String,
val time: Long,
@SerialName("user_id")
val userId: Long
)
override fun subtype(): String {
return "get_friend_msg_history"
}

View File

@ -3,6 +3,7 @@ package top.r3944realms.ltdmanager.napcat.event.message
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
import top.r3944realms.ltdmanager.napcat.event.group.AbstractGroupEvent
/**
@ -22,9 +23,13 @@ data class GetGroupMsgHistoryEvent(
@Transient
val echo0: String? = null,
val data: GetFriendMsgHistoryEvent.Data
val data: Data
) : AbstractGroupEvent(status0, retcode0, message0, wording0, echo0) {
@Serializable
data class Data (
val messages: List<MsgHistorySpecificMsg>
)
override fun subtype(): String {
return "get_group_msg_history"
}

View File

@ -18,8 +18,6 @@ data class GetGroupMemberListRequest(
@SerialName("no_cache")
val noCache: Boolean,
@SerialName("user_id")
val userId: ID
) : AbstractGroupRequest() {
override fun toJSON(): String = Json.encodeToString(this)

View File

@ -1,4 +1,68 @@
package top.r3944realms.ltdmanager.napcat.serializer
object MsgHistoryContentSerializer {
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistoryContent
import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg
object MsgHistoryContentSerializer : KSerializer<MsgHistoryContent> {
// 创建宽松的 JSON 配置
private val json = Json {
ignoreUnknownKeys = true
coerceInputValues = true
isLenient = true
}
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MsgHistoryContent")
override fun serialize(encoder: Encoder, value: MsgHistoryContent) {
val jsonEncoder = encoder as? JsonEncoder
?: throw SerializationException("只能使用JSON编码器")
when (value) {
is MsgHistoryContent.SpecificMsgList -> {
val jsonArray = JsonArray(value.value.map { specificMsg ->
json.encodeToJsonElement(specificMsg)
})
jsonEncoder.encodeJsonElement(jsonArray)
}
is MsgHistoryContent.StringValue -> {
jsonEncoder.encodeJsonElement(JsonPrimitive(value.value))
}
}
}
override fun deserialize(decoder: Decoder): MsgHistoryContent {
val jsonDecoder = decoder as? JsonDecoder
?: throw SerializationException("只能使用JSON解码器")
return when (val jsonElement = jsonDecoder.decodeJsonElement()) {
is JsonArray -> {
try {
val specificMsgList = jsonElement.map { element ->
json.decodeFromJsonElement<MsgHistorySpecificMsg>(element)
}
MsgHistoryContent.SpecificMsgList(specificMsgList)
} catch (e: Exception) {
throw SerializationException("无法将JsonArray解析为List<MsgHistorySpecificMsg>: ${e.message}")
}
}
is JsonPrimitive -> {
if (jsonElement.isString) {
MsgHistoryContent.StringValue(jsonElement.content)
} else {
throw SerializationException("不支持的非字符串原始类型")
}
}
else -> {
throw SerializationException("不支持的JSON元素类型: ${jsonElement::class.simpleName}")
}
}
}
}

View File

@ -57,4 +57,24 @@ object QRCodeUtil {
}
return image
}
@Throws(IOException::class, WriterException::class)
fun generateQRCodeToFile(
text: String,
width: Int,
height: Int,
filePath: String
) {
val hints: MutableMap<EncodeHintType, Any> = EnumMap(EncodeHintType::class.java)
hints[EncodeHintType.CHARACTER_SET] = CHARSET
// 生成二维码矩阵
val bitMatrix = MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints)
// 转成 BufferedImage
val image = toBufferedImage(bitMatrix)
// 保存到文件
val outputFile = java.io.File(filePath)
ImageIO.write(image, FORMAT, outputFile)
}
}

View File

@ -101,3 +101,6 @@ dg-lab:
enable-debug: false
ide-host: "127.0.0.1"
ide-port: 5678
img-tu:
url: https://mysite.com/api/1/upload
encrypted-password: 11223344bbcc

View File

@ -1,2 +1,34 @@
package top.r394realms.ltdmanagertest.command
import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType
import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
import com.mojang.brigadier.builder.RequiredArgumentBuilder.argument
fun main() {
val dispatcher = CommandDispatcher<String>() // String 表示消息来源
dispatcher.register(
literal<String>("say")
.then(argument<String, String>("message", StringArgumentType.greedyString())
.executes { ctx ->
val msg = StringArgumentType.getString(ctx, "message")
println("[BOT] $msg")
1
})
)
dispatcher.register(
literal<String>("add")
.then(argument<String, Int>("a", IntegerArgumentType.integer())
.then(argument<String, Int>("b", IntegerArgumentType.integer())
.executes { ctx ->
val a = IntegerArgumentType.getInteger(ctx, "a")
val b = IntegerArgumentType.getInteger(ctx, "b")
println("[BOT] $a + $b = ${a + b}")
1
}))
)
dispatcher.execute("say Hello World", "user123")
dispatcher.execute("add 3 7", "user123")
}

View File

@ -1,4 +1,197 @@
package top.r394realms.ltdmanagertest.command
import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser
/**
* 参数提取演示类
*/
class ParameterExtractionDemo {
companion object {
// 默认参数模式:<参数名> 或 [可选参数名]
val DEFAULT_PARAMETER_PATTERN = Regex("""<(\w+)>|\[(\w+)]""")
// 常用参数模式
val ANGLE_BRACKETS = Regex("""<(\w+)>""") // <param> - 必需参数
val SQUARE_BRACKETS = Regex("""\[(\w+)]""") // [param] - 可选参数
val CURLY_BRACES = Regex("""\{(\w+)}""") // {param} - 自定义参数
}
/**
* 从语法字符串中提取参数名
*/
fun extractParameterNames(syntax: String, pattern: Regex): List<String> {
if (syntax.isEmpty()) return emptyList()
return pattern.findAll(syntax).map { matchResult ->
// 从捕获组中提取参数名(处理不同的括号类型)
matchResult.groupValues[1].ifEmpty { matchResult.groupValues[2] }
}.toList()
}
/**
* 演示场景1只需要必需参数参数验证
*/
fun demoRequiredParameters() {
println("=== 场景1必需参数验证 ===")
val syntax = "send <message> [target] [priority]"
val parser = AdvancedCommandParser().apply {
registerCommand(
"ls",
syntax = syntax,
parameterPattern = ANGLE_BRACKETS,
)
}
println(parser.getCommandHelp("ls"))
// 模拟用户输入验证
val testCases = listOf(
"ls send Hello", // 有效:提供了必需参数
"ls send Hello @all", // 有效:提供了必需参数和可选参数
"ls send" // 无效:缺少必需参数
)
testCases.forEach { input ->
println("输入: $input")
val result = parser.parseAndValidate(input)
if (result.isValid) {
println("✓ 命令: ${result.command}")
println("✓ 参数:")
result.arguments.forEach { (key, value) ->
println(" $key: $value")
}
} else {
println("✗ 错误: ${result.errorMessage}")
}
println("-".repeat(50))
}
println()
}
/**
* 演示场景2需要所有参数完整解析
*/
fun demoAllParameters() {
println("=== 场景2完整参数解析 ===")
val syntax = "user <action> <id> [name] [age] [email]"
val parser = AdvancedCommandParser().apply {
registerCommand(
"ls",
syntax = syntax,
)
}
println(parser.getCommandHelp("ls"))
// 模拟参数映射
val testInput = "ls user add 123 John 30 john@example.com"
println("输入: $testInput")
val result = parser.parseAndValidate(testInput)
if (result.isValid) {
println("✓ 命令: ${result.command}")
println("✓ 参数:")
result.arguments.forEach { (key, value) ->
println(" $key: $value")
}
} else {
println("✗ 错误: ${result.errorMessage}")
}
println("-".repeat(50))
}
/**
* 演示场景3自定义参数格式
*/
fun demoCustomParameters() {
println("=== 场景3自定义参数格式 ===")
val customSyntax = "execute {command} {args} --timeout {timeout} --retry {retries}"
extractParameterNames(customSyntax, CURLY_BRACES)
val parser = AdvancedCommandParser().apply {
registerCommand(
"ls",
syntax = customSyntax,
parameterPattern = CURLY_BRACES,
)
}
println(parser.getCommandHelp("ls"))
// 模拟命名参数解析
val testInput = "ls execute {ls -la} {--help} --timeout 30 --retry 3"
val result = parser.parseAndValidate(testInput)
if (result.isValid) {
println("✓ 命令: ${result.command}")
println("✓ 参数:")
result.arguments.forEach { (key, value) ->
println(" $key: $value")
}
} else {
println("✗ 错误: ${result.errorMessage}")
}
println("-".repeat(5))
}
/**
* 综合演示完整的命令处理流程
*/
fun demoCompleteWorkflow() {
println("=== 综合演示:完整工作流程 ===")
// 定义复杂的命令语法
val syntax = "ls database <operation> <table> [where] [limit] [offset] --format {format}"
val parser1 = AdvancedCommandParser().apply {
registerCommand(
"ls",
syntax = syntax,
parameterPattern = DEFAULT_PARAMETER_PATTERN,
)
}
// 模拟真实命令处理
val testCommand = "ls database select users --where \"age > 18\" --limit 10 --format json"
val result = parser1.parseAndValidate(testCommand)
if (result.isValid) {
println("✓ 命令: ${result.command}")
println("✓ 参数:")
result.arguments.forEach { (key, value) ->
println(" $key: $value")
}
} else {
println("✗ 错误: ${result.errorMessage}")
}
println("-".repeat(5))
}
}
/**
* 主函数运行演示
*/
fun main() {
val demo = ParameterExtractionDemo()
// 运行各个演示场景
demo.demoRequiredParameters()
demo.demoAllParameters()
demo.demoCustomParameters()
demo.demoCompleteWorkflow()
// 额外演示:不同语法模式对比
println("\n=== 语法模式对比 ===")
val syntaxes = listOf(
"cmd <req1> <req2> [opt1] [opt2]",
"run {command} {args}",
"test <input> [output] --mode {mode} --verbose {flag}"
)
syntaxes.forEach { syntax ->
println("\n语法: $syntax")
println("尖括号参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.ANGLE_BRACKETS)}")
println("方括号参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.SQUARE_BRACKETS)}")
println("花括号参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.CURLY_BRACES)}")
println("所有参数: ${demo.extractParameterNames(syntax, ParameterExtractionDemo.DEFAULT_PARAMETER_PATTERN)}")
}
}

View File

@ -1,4 +1,54 @@
package top.r394realms.ltdmanagertest.command
class testACP {
import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser
fun main() {
val parser = AdvancedCommandParser().apply {
registerCommand(
name = "send",
aliases = listOf("s"),
syntax = "<message> [target] [priority]",
description = "发送消息到指定目标"
)
registerCommand(
name = "user",
aliases = listOf("u"),
syntax = "<action> <id> [name] [email] --role {role}",
description = "用户管理命令"
)
registerCommand(
name = "database",
aliases = listOf("db"),
syntax = "<operation> <table> [where] [limit] --format {format}",
description = "数据库操作命令"
)
}
// 测试复杂命令
val testCommands = listOf(
"database select users --where \"age > 18 and name = 'John Doe'\" --limit 10 --format json",
"send \"Hello, World!\" @all --priority high",
"user add 123 --email john@example.com --role admin --name \"John Smith\"",
"invalid command test"
)
testCommands.forEach { input ->
println("输入: $input")
val result = parser.parseAndValidate(input)
if (result.isValid) {
println("✓ 命令: ${result.command}")
println("✓ 参数:")
result.arguments.forEach { (key, value) ->
println(" $key: $value")
}
} else {
println("✗ 错误: ${result.errorMessage}")
}
println("-".repeat(50))
}
// 显示帮助信息
println(parser.getCommandHelp("database") ?: "命令未找到")
}

View File

@ -1,2 +1,48 @@
package top.r394realms.ltdmanagertest.command
import top.r3944realms.ltdmanager.module.common.AdvancedCommandParser
fun main() {
val parser = AdvancedCommandParser().apply {
registerCommand(
name = "send",
aliases = listOf("s"),
syntax = "<message> [target]",
description = "发送消息到指定目标"
)
registerCommand(
name = "user",
aliases = listOf("u"),
syntax = "<action> <id> [options]",
description = "用户管理命令"
)
registerCommand(
name = "config",
aliases = listOf("cfg"),
syntax = "set <key> <value> | get <key>",
description = "配置管理",
parameterPattern = AdvancedCommandParser.ANGLE_BRACKETS
)
}
// 测试解析
val testInputs = listOf(
"send Hello World",
"user add 123 --name John",
"config set theme dark",
"invalid command"
)
testInputs.forEach { input ->
println("输入: $input")
val result = parser.parseAndValidate(input)
println("结果: $result")
println("---")
}
// 获取帮助信息
println("帮助信息:")
println(parser.getCommandHelp("send"))
}

View File

@ -1,7 +1,7 @@
package top.r394realms.ltdmanagertest.help
import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.module.BanModule
import top.r3944realms.ltdmanager.module.DGLabModule
import top.r3944realms.ltdmanager.module.GroupMessagePollingModule
import top.r3944realms.ltdmanager.module.HelpModule
@ -22,18 +22,18 @@ fun main() = GlobalManager.runBlockingMain {
selfId = selfQQId,
selfNickName = selfNickName,
)
val banModule = BanModule(
val dgLabModule = DGLabModule(
moduleName = "TestGroup",
groupMessagePollingModule = groupMsgPollingModule,
selfId = selfQQId,
adminsId = listOf(2561098830),
muteCommandPrefixList = listOf("禁言", "口球", "mute", "Mute", "闭嘴")
adminIds = listOf(2561098830L),
commandHead = listOf("dglab")
)
GlobalManager.moduleManager.registerModule(groupMsgPollingModule)
GlobalManager.moduleManager.registerModule(helpModule)
GlobalManager.moduleManager.registerModule(banModule)
GlobalManager.moduleManager.registerModule(dgLabModule)
GlobalManager.moduleManager.loadModule(groupMsgPollingModule.name)
GlobalManager.moduleManager.loadModule(helpModule.name)
GlobalManager.moduleManager.loadModule(banModule.name)
GlobalManager.moduleManager.loadModule(dgLabModule.name)
}

View File

@ -5,12 +5,19 @@ import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.module.ModGroupHandlerModule
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageElement
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest
import top.r3944realms.ltdmanager.napcat.request.other.SendPrivateMsgRequest
fun main() = GlobalManager.runBlockingMain {
val napCatClient = NapCatClient.create()
formatAndSendForwardMessage(napCatClient, 2561098830L, "幸福亮亮")
// formatAndSendForwardMessage(napCatClient, 2561098830L, "幸福亮亮")
sendTestMsg(napCatClient)
}
private suspend fun sendTestMsg(napCatClient: NapCatClient) {
val request = SendPrivateMsgRequest(listOf(MessageElement.image("https://pic.xiaobuawa.top/images/2025/09/30/icons8-postgresql-96d4af6da8d4bd8df5.png","图片")),ID.long(2561098830L))
napCatClient.sendUnit(request)
}
private suspend fun formatAndSendForwardMessage(napCatClient: NapCatClient ,userId: Long, requesterNick: String) {
// 虚拟数据 - 模拟有审核记录的情况
@ -45,7 +52,7 @@ private suspend fun formatAndSendForwardMessage(napCatClient: NapCatClient ,user
// 创建合并转发消息
val forwardRequest = SendForwardMsgRequest(
groupId = ID.long(339340846),
groupId = ID.long(920719236),
messages = listOf(
SendForwardMsgRequest.TopForwardMsg(
data = SendForwardMsgRequest.MessageData(

View File

@ -1,4 +1,26 @@
package top.r394realms.ltdmanagertest.msg
class sendMsgTest {
import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupMemberListEvent
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupMemberListRequest
import top.r3944realms.ltdmanager.napcat.request.message.SetMsgEmojiLikeRequest
fun main() = GlobalManager.runBlockingMain {
// val getGroupMemberListEvent = GlobalManager.napCatClient.send<GetGroupMemberListEvent>(
// GetGroupMemberListRequest(
// ID.long(920719236),
// false
// )
// )
// println(getGroupMemberListEvent.data.filter { !it.isRobot }.map { it.userId to it.nickname }.toMap())
for (i in 61 ..81){
GlobalManager.napCatClient.sendUnit(
SetMsgEmojiLikeRequest(
i.toDouble(), ID.long(2080109145), true
)
)
}
}

View File

@ -2,20 +2,21 @@ package top.r394realms.ltdmanagertest
import top.r3944realms.ltdmanager.GlobalManager
import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule
import top.r3944realms.ltdmanager.module.StateModule
fun main() = GlobalManager.runBlockingMain {
// 创建模块实例
val groupModule = GroupRequestHandlerModule(
moduleName = "WhiteListGroup",
client = GlobalManager.napCatClient,
targetGroupId = 538751386
val stateModule = StateModule(
moduleName = "Globe",
onlineName = "[\uD83D\uDFE2] 闲趣老土豆🥔",
offlineName = "[\uD83D\uDD34] 闲趣老土豆🥔"
)
// 注册模块到全局模块管理器
GlobalManager.moduleManager.registerModule(groupModule)
GlobalManager.moduleManager.registerModule(stateModule)
// 加载模块
GlobalManager.moduleManager.loadModule(groupModule.name)
GlobalManager.moduleManager.loadModule(stateModule.name)
}

View File

@ -1,4 +1,9 @@
package top.r394realms.ltdmanagertest
class testRandom {
import kotlin.random.Random
fun main() {
for(item in 1..100){
println(Random.nextInt(100))
}
}

View File

@ -4,7 +4,7 @@ package top.r394realms.ltdmanagertest.util
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody
import top.r3944realms.ltdmanager.GlobalManager
import okhttp3.logging.HttpLoggingInterceptor
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.io.File
import java.io.IOException
@ -42,6 +42,7 @@ object ImageUploader {
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()
@ -82,6 +83,7 @@ object ImageUploader {
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()

View File

@ -1,4 +1,29 @@
package top.r394realms.ltdmanagertest.util
class img {
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,2 +1,82 @@
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,2 +1,126 @@
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,4 +1,82 @@
package top.r394realms.ltdmanagertest.util
class imgv4 {
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}")
}
}
}