feat: 添加通用SQL命令

This commit is contained in:
叁玖领域 2026-06-09 11:00:02 +08:00
parent 9cb6bcef50
commit 79caa2b56e
5 changed files with 236 additions and 1 deletions

View File

@ -3,5 +3,5 @@ org.gradle.downloadSources=false
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.degree_of_parallelism=16 org.gradle.degree_of_parallelism=16
project_group=top.r3944realms.ltdmanager project_group=top.r3944realms.ltdmanager
project_version=1.20-SNAPSHOT project_version=1.21-SNAPSHOT
dg_lab_version=4.4.14.19 dg_lab_version=4.4.14.19

View File

@ -125,6 +125,7 @@ data class ModuleConfig(
STATE_MODULE(Modules.STATE), STATE_MODULE(Modules.STATE),
HELP_MODULE(Modules.HELP), HELP_MODULE(Modules.HELP),
GITEA_WEBHOOK_MODULE(Modules.GITEA_WEBHOOK), GITEA_WEBHOOK_MODULE(Modules.GITEA_WEBHOOK),
RCON_COMMAND_MODULE(Modules.RCON_COMMAND),
UNKNOWN_MODULE("UnknownModule"); UNKNOWN_MODULE("UnknownModule");
} }
// 基础获取方法 // 基础获取方法

View File

@ -7,6 +7,7 @@ import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.module.exception.ConfigError import top.r3944realms.ltdmanager.module.exception.ConfigError
import top.r3944realms.ltdmanager.module.gitea.GiteaEventType import top.r3944realms.ltdmanager.module.gitea.GiteaEventType
import top.r3944realms.ltdmanager.module.gitea.GiteaWebhookModule import top.r3944realms.ltdmanager.module.gitea.GiteaWebhookModule
import top.r3944realms.ltdmanager.module.RconCommandModule
object ModuleFactory { object ModuleFactory {
fun createModule(config: ModuleConfig.Module): BaseModule { fun createModule(config: ModuleConfig.Module): BaseModule {
@ -23,6 +24,7 @@ object ModuleFactory {
MOD_GROUP_HANDLER_MODULE -> createModGroupHandler(config) MOD_GROUP_HANDLER_MODULE -> createModGroupHandler(config)
HELP_MODULE -> createHelpModule(config) HELP_MODULE -> createHelpModule(config)
GITEA_WEBHOOK_MODULE -> createGiteaWebhook(config) GITEA_WEBHOOK_MODULE -> createGiteaWebhook(config)
RCON_COMMAND_MODULE -> createRconCommand(config)
UNKNOWN_MODULE -> throw ConfigError(ConfigError.Type.INVALID_PARAMETER, "unknown module") 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<Long>("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
)
}
} }

View File

@ -17,6 +17,7 @@ object Modules {
val INVITATION_CODE: String = register("InvitationCodeModule") val INVITATION_CODE: String = register("InvitationCodeModule")
val STATE: String = register("StateModule") val STATE: String = register("StateModule")
val GITEA_WEBHOOK: String = register("GiteaWebhookModule") val GITEA_WEBHOOK: String = register("GiteaWebhookModule")
val RCON_COMMAND: String = register("RconCommandModule")
fun register(name: String): String { fun register(name: String): String {
MODULES.add(name) MODULES.add(name)
return name return name

View File

@ -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<Long>,
private val commandBlocklist: Set<String>,
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<MsgHistorySpecificMsg>) {
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 <MC命令>")
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()}")
}
}