From 79caa2b56e83548ce164f433fc240392853c32b7 Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Tue, 9 Jun 2026 11:00:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=80=9A=E7=94=A8SQL?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- .../ltdmanager/core/config/ModuleConfig.kt | 1 + .../ltdmanager/module/ModuleFactory.kt | 27 +++ .../r3944realms/ltdmanager/module/Modules.kt | 1 + .../ltdmanager/module/RconCommandModule.kt | 206 ++++++++++++++++++ 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt diff --git a/gradle.properties b/gradle.properties index 7330c5f..e1e5499 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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.20-SNAPSHOT +project_version=1.21-SNAPSHOT dg_lab_version=4.4.14.19 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ModuleConfig.kt b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ModuleConfig.kt index c13e374..43f7ae2 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ModuleConfig.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/core/config/ModuleConfig.kt @@ -125,6 +125,7 @@ data class ModuleConfig( STATE_MODULE(Modules.STATE), HELP_MODULE(Modules.HELP), GITEA_WEBHOOK_MODULE(Modules.GITEA_WEBHOOK), + RCON_COMMAND_MODULE(Modules.RCON_COMMAND), UNKNOWN_MODULE("UnknownModule"); } // 基础获取方法 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt index 5ad3489..b3e970a 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt @@ -7,6 +7,7 @@ import top.r3944realms.ltdmanager.core.config.YamlConfigLoader import top.r3944realms.ltdmanager.module.exception.ConfigError import top.r3944realms.ltdmanager.module.gitea.GiteaEventType import top.r3944realms.ltdmanager.module.gitea.GiteaWebhookModule +import top.r3944realms.ltdmanager.module.RconCommandModule object ModuleFactory { fun createModule(config: ModuleConfig.Module): BaseModule { @@ -23,6 +24,7 @@ object ModuleFactory { MOD_GROUP_HANDLER_MODULE -> createModGroupHandler(config) HELP_MODULE -> createHelpModule(config) GITEA_WEBHOOK_MODULE -> createGiteaWebhook(config) + RCON_COMMAND_MODULE -> createRconCommand(config) UNKNOWN_MODULE -> throw ConfigError(ConfigError.Type.INVALID_PARAMETER, "unknown module") } } @@ -214,4 +216,29 @@ object ModuleFactory { ) } + private fun createRconCommand(config: ModuleConfig.Module): RconCommandModule { + val toolConfig = YamlConfigLoader.loadToolConfig() + val selfId = config.long("self-id") + val selfNickName = config.string("self-nick-name") + val allowedUsers = config.list("admin-ids").toSet() + val commandBlocklist = config.stringList("command-blocklist").toSet() + val commandPrefix = config.string("command-prefix") + val rconTimeoutSec = config.getOrDefault("rcon-timeout-sec", 5L) + val groupMessagePollingModule = resolveDependency( + config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling" + ) as GroupMessagePollingModule + return RconCommandModule( + config.name, + groupMessagePollingModule, + toolConfig.rcon.mcRconToolPath.toString(), + toolConfig.rcon.mcRconToolConfigPath.toString(), + rconTimeoutSec, + selfId, + selfNickName, + allowedUsers, + commandBlocklist, + commandPrefix + ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt index a66a301..3d9e0a8 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/Modules.kt @@ -17,6 +17,7 @@ object Modules { val INVITATION_CODE: String = register("InvitationCodeModule") val STATE: String = register("StateModule") val GITEA_WEBHOOK: String = register("GiteaWebhookModule") + val RCON_COMMAND: String = register("RconCommandModule") fun register(name: String): String { MODULES.add(name) return name diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt new file mode 100644 index 0000000..758163c --- /dev/null +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt @@ -0,0 +1,206 @@ +package top.r3944realms.ltdmanager.module + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +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.data.ID +import top.r3944realms.ltdmanager.napcat.data.MessageElement +import top.r3944realms.ltdmanager.napcat.data.msghistory.MsgHistorySpecificMsg +import top.r3944realms.ltdmanager.napcat.data.MessageType +import top.r3944realms.ltdmanager.napcat.request.message.SendForwardMsgRequest +import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest +import top.r3944realms.ltdmanager.utils.CmdUtil +import top.r3944realms.ltdmanager.utils.LoggerUtil + +class RconCommandModule( + moduleName: String, + private val groupMessagePollingModule: GroupMessagePollingModule, + private val rconPath: String, + private val rconConfigPath: String, + private val rconTimeoutSec: Long, + private val selfId: Long, + private val selfNickName: String, + private val allowedUsers: Set, + private val commandBlocklist: Set, + private val commandPrefix: String, +) : BaseModule(Modules.RCON_COMMAND, moduleName) { + + private var scope: CoroutineScope? = null + + private var lastTriggeredRealId: Long = -1 + private var lastTriggerTime: Long = 0 + + private val triggerFilter by lazy { + TriggerMessageFilter( + listOf( + IgnoreSelfFilter(selfId), + NewMessageFilter { lastTriggerTime to lastTriggeredRealId }, + KeywordFilter(setOf(commandPrefix)), + ) + ) + } + + override fun onLoad() { + LoggerUtil.logger.info("[$name] RCON命令模块已装载") + LoggerUtil.logger.info("[$name] 允许用户: $allowedUsers") + LoggerUtil.logger.info("[$name] 命令黑名单: $commandBlocklist") + + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope!!.launch { + groupMessagePollingModule.messagesFlow.collect { messages -> + if (loaded) handleMessages(messages) + } + } + } + + override suspend fun onUnload() { + scope?.cancel() + LoggerUtil.logger.info("[$name] RCON命令模块已卸载") + } + + private suspend fun handleMessages(messages: List) { + val filtered = triggerFilter.filter(messages) + val triggerMsg = filtered.maxByOrNull { it.time } ?: return + + try { + processCommand(triggerMsg) + } catch (e: Exception) { + LoggerUtil.logger.error("[$name] 处理RCON命令失败", e) + sendReply(triggerMsg, "命令执行异常: ${e.message}") + } + } + + private suspend fun processCommand(msg: MsgHistorySpecificMsg) { + val text = msg.message.firstOrNull { it.type == MessageType.Text }?.data?.text ?: return + val rconCommand = text.removePrefix(commandPrefix).trim() + + if (rconCommand.isEmpty()) { + sendReply(msg, buildHelpMessage()) + updateTriggerState(msg) + return + } + + // 权限检查 + if (msg.userId !in allowedUsers) { + LoggerUtil.logger.warn("[$name] 用户 ${msg.userId} 无权限执行RCON: $rconCommand") + sendReply(msg, "你没有权限执行 RCON 命令") + return + } + + // 危险命令检查 + val blocked = findBlocklistMatch(rconCommand) + if (blocked != null) { + LoggerUtil.logger.warn("[$name] 阻止危险命令: '$rconCommand' (匹配黑名单: $blocked)") + sendReply(msg, "命令已被阻止 (匹配黑名单规则: $blocked)") + return + } + + // 执行RCON + LoggerUtil.logger.info("[$name] 用户 ${msg.userId} 执行RCON: $rconCommand") + val output = runRconCommand(rconCommand) + sendResult(msg, rconCommand, output) + updateTriggerState(msg) + } + + private fun findBlocklistMatch(command: String): String? { + val lower = command.lowercase().trimStart('/') + return commandBlocklist.firstOrNull { blocked -> + lower == blocked.lowercase() || + lower.startsWith(blocked.lowercase() + " ") || + lower.startsWith(blocked.lowercase() + "/") + } + } + + private fun runRconCommand(command: String): String { + return CmdUtil.runExeCommand( + rconPath, + "-c", rconConfigPath, + "-T", "${rconTimeoutSec}s", + command + ) + } + + private suspend fun sendResult(msg: MsgHistorySpecificMsg, command: String, output: String) { + val trimmed = output.trim() + val maxLen = 3000 + + if (trimmed.length <= maxLen) { + sendReply(msg, buildString { + appendLine("执行: $command") + appendLine("─".repeat(24)) + append(trimmed.ifEmpty { "(无输出)" }) + }) + return + } + + // 长输出 → 合并转发 + val chunks = trimmed.chunked(maxLen) + val messages = chunks.map { chunk -> + SendForwardMsgRequest.Message( + data = SendForwardMsgRequest.PurpleData(chunk), + type = MessageType.Text + ) + } + + val topMessage = SendForwardMsgRequest.TopForwardMsg( + data = SendForwardMsgRequest.MessageData( + content = messages, + nickname = selfNickName, + userId = ID.long(selfId), + ), + type = MessageType.Node + ) + + val request = SendForwardMsgRequest( + groupId = ID.long(groupMessagePollingModule.targetGroupId), + messages = listOf(topMessage), + news = listOf( + SendForwardMsgRequest.ForwardModelNews("RCON: $command"), + SendForwardMsgRequest.ForwardModelNews("输出 ${trimmed.length} 字符"), + ), + prompt = "RCON命令执行结果", + source = "RCON", + summary = "RCON: $command", + ) + + napCatClient.sendUnit(request) + } + + private suspend fun sendReply(msg: MsgHistorySpecificMsg, text: String) { + napCatClient.sendUnit( + SendGroupMsgRequest( + MessageElement.reply(ID.long(msg.realId), text), + ID.long(groupMessagePollingModule.targetGroupId) + ) + ) + } + + private fun updateTriggerState(msg: MsgHistorySpecificMsg) { + lastTriggeredRealId = msg.realId + lastTriggerTime = msg.time + } + + private fun buildHelpMessage(): String = buildString { + appendLine("RCON 命令模块") + appendLine("用法: $commandPrefix ") + appendLine("─".repeat(16)) + appendLine("示例:") + appendLine(" $commandPrefix list") + appendLine(" $commandPrefix forge tps") + appendLine(" $commandPrefix difficulty peaceful") + } + + override fun info(): String = "RCON命令模块 - 前缀: $commandPrefix, 允许用户: ${allowedUsers.size}人, 黑名单规则: ${commandBlocklist.size}条" + override fun help(): String = buildString { + appendLine("RCON命令模块 - 通过QQ群执行Minecraft RCON命令") + appendLine("前缀: $commandPrefix") + appendLine("权限: 仅以下QQ号可执行: ${allowedUsers.joinToString()}") + appendLine("黑名单命令前缀: ${commandBlocklist.joinToString()}") + } +}