diff --git a/gradle.properties b/gradle.properties index eb55089..de6ed86 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.22.4 +project_version=1.22.5 dg_lab_version=4.4.14.19 diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt index c97f7a4..29f5c64 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/HelpModule.kt @@ -104,6 +104,8 @@ class HelpModule( val filtered = triggerFilter.filter(messages) val triggerMsg = filtered.maxByOrNull { it.time } ?: return + updateTriggerState(triggerMsg) // 先更新防止后续 sendUnit 异常丢失或 cmdPair 不匹配漏更 + val cmdPair = commandParser.parseCommand(triggerMsg.textContent) if (cmdPair != null) { val (_, arg) = cmdPair diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt index 16673ab..4919f6b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/InvitationCodesModule.kt @@ -185,7 +185,9 @@ class InvitationCodesModule( createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode) hadVaildCodeButNotUseListHandler(hadValidCodeButNotUsed + needNewCode) } catch (e: Exception) { - sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: $e") + val first = triggerMsgs.firstOrNull() + if (first != null) sendFailedMessage(napCatClient, first.userId, first.realId, first.time, "系统错误,请联系管理员: $e") + else sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: $e") } finally { saveState(lastTriggerMapState) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt index d743e72..3cd6e8b 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/McServerStatusModule.kt @@ -170,14 +170,15 @@ class McServerStatusModule( private suspend fun processCommand(msg: MsgHistorySpecificMsg) { - // 找出文本内容 + // 先更新触发状态防止提前返回或 sendUnit 异常丢失 + cooldownState = cooldownState.updateLastTrigger(msg.userId, msg.realId, msg.time) + val text = msg.message .firstOrNull { it.type == MessageType.Text } ?.data?.text ?.trim() ?: return - // 使用命令解析器解析命令 val parsedCommand = commandParser.parseCommand(text) ?: return val (_, address) = parsedCommand diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt index 92a60f7..5ec0336 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/ModuleFactory.kt @@ -250,6 +250,7 @@ object ModuleFactory { val groupMessagePollingModule = resolveDependency( config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling" ) as GroupMessagePollingModule + val maxBlockRecords = config.getOrDefault("max-block-records", 200) return RconCommandModule( config.name, groupMessagePollingModule, @@ -260,7 +261,8 @@ object ModuleFactory { selfNickName, allowedUsers, commandBlocklist, - commandPrefix + commandPrefix, + maxBlockRecords ) } diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt index 758163c..16707da 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/RconCommandModule.kt @@ -5,6 +5,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json 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 @@ -17,6 +20,7 @@ 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 +import java.io.File class RconCommandModule( moduleName: String, @@ -29,13 +33,22 @@ class RconCommandModule( private val allowedUsers: Set, private val commandBlocklist: Set, private val commandPrefix: String, -) : BaseModule(Modules.RCON_COMMAND, moduleName) { + private val maxBlockRecords: Int = 200, +) : BaseModule(Modules.RCON_COMMAND, moduleName), PersistentState { private var scope: CoroutineScope? = null private var lastTriggeredRealId: Long = -1 private var lastTriggerTime: Long = 0 + private val stateFile: File = getStateFileInternal("rcon_block_state.json", name) + override fun getStateFileInternal(): File = stateFile + + private val json = Json { ignoreUnknownKeys = true; coerceInputValues = true } + + private var blockState: BlockState = loadState() + override fun getState(): BlockState = blockState + private val triggerFilter by lazy { TriggerMessageFilter( listOf( @@ -50,6 +63,7 @@ class RconCommandModule( LoggerUtil.logger.info("[$name] RCON命令模块已装载") LoggerUtil.logger.info("[$name] 允许用户: $allowedUsers") LoggerUtil.logger.info("[$name] 命令黑名单: $commandBlocklist") + LoggerUtil.logger.info("[$name] 阻止记录数: ${blockState.records.size}") scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope!!.launch { @@ -60,6 +74,7 @@ class RconCommandModule( } override suspend fun onUnload() { + saveState(blockState) scope?.cancel() LoggerUtil.logger.info("[$name] RCON命令模块已卸载") } @@ -73,6 +88,7 @@ class RconCommandModule( } catch (e: Exception) { LoggerUtil.logger.error("[$name] 处理RCON命令失败", e) sendReply(triggerMsg, "命令执行异常: ${e.message}") + updateTriggerState(triggerMsg) } } @@ -86,28 +102,40 @@ class RconCommandModule( return } - // 权限检查 if (msg.userId !in allowedUsers) { LoggerUtil.logger.warn("[$name] 用户 ${msg.userId} 无权限执行RCON: $rconCommand") sendReply(msg, "你没有权限执行 RCON 命令") + updateTriggerState(msg) return } - // 危险命令检查 val blocked = findBlocklistMatch(rconCommand) if (blocked != null) { - LoggerUtil.logger.warn("[$name] 阻止危险命令: '$rconCommand' (匹配黑名单: $blocked)") + LoggerUtil.logger.warn("[$name] 阻止危险命令: '$rconCommand' (匹配黑名单: $blocked) 来自 ${msg.userId}") sendReply(msg, "命令已被阻止 (匹配黑名单规则: $blocked)") + recordBlock(msg.userId, rconCommand, blocked) + updateTriggerState(msg) return } - // 执行RCON LoggerUtil.logger.info("[$name] 用户 ${msg.userId} 执行RCON: $rconCommand") val output = runRconCommand(rconCommand) sendResult(msg, rconCommand, output) updateTriggerState(msg) } + private fun recordBlock(qq: Long, command: String, matchedRule: String) { + val record = BlockRecord( + qq = qq, + command = command, + matchedRule = matchedRule, + time = System.currentTimeMillis() + ) + val records = (listOf(record) + blockState.records).take(maxBlockRecords) + blockState = BlockState(records) + saveState(blockState) + } + private fun findBlocklistMatch(command: String): String? { val lower = command.lowercase().trimStart('/') return commandBlocklist.firstOrNull { blocked -> @@ -139,7 +167,6 @@ class RconCommandModule( return } - // 长输出 → 合并转发 val chunks = trimmed.chunked(maxLen) val messages = chunks.map { chunk -> SendForwardMsgRequest.Message( @@ -196,7 +223,46 @@ class RconCommandModule( appendLine(" $commandPrefix difficulty peaceful") } - override fun info(): String = "RCON命令模块 - 前缀: $commandPrefix, 允许用户: ${allowedUsers.size}人, 黑名单规则: ${commandBlocklist.size}条" + // ======== 持久化 ======== + + @Serializable + data class BlockState(val records: List = emptyList()) + + @Serializable + data class BlockRecord( + val qq: Long, + val command: String, + val matchedRule: String, + val time: Long, + ) + + override fun loadState(): BlockState { + return try { + if (!stateFile.exists()) BlockState() + else json.decodeFromString(stateFile.readText()) + } catch (e: Exception) { + LoggerUtil.logger.warn("[$name] 读取阻止记录失败", e) + BlockState() + } + } + + override fun saveState(state: BlockState) { + try { + stateFile.writeText(json.encodeToString(state)) + } catch (e: Exception) { + LoggerUtil.logger.error("[$name] 保存阻止记录失败", e) + } + } + + override fun info(): String = buildString { + appendLine("RCON命令模块 - 前缀: $commandPrefix") + appendLine(" 允许用户: ${allowedUsers.size}人, 黑名单规则: ${commandBlocklist.size}条") + appendLine(" 阻止记录: ${blockState.records.size}条 (最近5条):") + blockState.records.take(5).forEach { + appendLine(" • ${it.qq} → ${it.command} (${it.matchedRule})") + } + } + override fun help(): String = buildString { appendLine("RCON命令模块 - 通过QQ群执行Minecraft RCON命令") appendLine("前缀: $commandPrefix") diff --git a/src/main/kotlin/top/r3944realms/ltdmanager/module/WhitelistAuditModule.kt b/src/main/kotlin/top/r3944realms/ltdmanager/module/WhitelistAuditModule.kt index dda0b10..547b89c 100644 --- a/src/main/kotlin/top/r3944realms/ltdmanager/module/WhitelistAuditModule.kt +++ b/src/main/kotlin/top/r3944realms/ltdmanager/module/WhitelistAuditModule.kt @@ -289,11 +289,10 @@ class WhitelistAuditModule( private suspend fun handleReActivationMessages(messages: List) { val msg = messages.maxByOrNull { it.time } ?: return + updateMsgState(msg) val key = msg.userId.toString() val entry = auditState.entries[key] ?: return - - updateMsgState(msg) val now = System.currentTimeMillis() val graceMs = gracePeriodDays * 24 * 60 * 60 * 1000L @@ -339,6 +338,9 @@ class WhitelistAuditModule( private suspend fun handleAuditCommand(messages: List) { val msg = messages.maxByOrNull { it.time } ?: return + lastAuditRealId = msg.realId + lastAuditTime = msg.time + if (msg.userId !in auditAllowedUsers) { napCatClient.sendUnit( SendGroupMsgRequest(