fix: 改进可能的重复响应问题

This commit is contained in:
叁玖领域 2026-06-09 15:29:47 +08:00
parent 8fd9250af4
commit d1afc51ad3
7 changed files with 89 additions and 14 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.22.4 project_version=1.22.5
dg_lab_version=4.4.14.19 dg_lab_version=4.4.14.19

View File

@ -104,6 +104,8 @@ class HelpModule(
val filtered = triggerFilter.filter(messages) val filtered = triggerFilter.filter(messages)
val triggerMsg = filtered.maxByOrNull { it.time } ?: return val triggerMsg = filtered.maxByOrNull { it.time } ?: return
updateTriggerState(triggerMsg) // 先更新防止后续 sendUnit 异常丢失或 cmdPair 不匹配漏更
val cmdPair = commandParser.parseCommand(triggerMsg.textContent) val cmdPair = commandParser.parseCommand(triggerMsg.textContent)
if (cmdPair != null) { if (cmdPair != null) {
val (_, arg) = cmdPair val (_, arg) = cmdPair

View File

@ -185,7 +185,9 @@ class InvitationCodesModule(
createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode) createAndSearchInvitationCodeIdsThenUpdateDate(needNewCode)
hadVaildCodeButNotUseListHandler(hadValidCodeButNotUsed + needNewCode) hadVaildCodeButNotUseListHandler(hadValidCodeButNotUsed + needNewCode)
} catch (e: Exception) { } 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 { } finally {
saveState(lastTriggerMapState) saveState(lastTriggerMapState)
} }

View File

@ -170,14 +170,15 @@ class McServerStatusModule(
private suspend fun processCommand(msg: MsgHistorySpecificMsg) { private suspend fun processCommand(msg: MsgHistorySpecificMsg) {
// 找出文本内容 // 先更新触发状态防止提前返回或 sendUnit 异常丢失
cooldownState = cooldownState.updateLastTrigger(msg.userId, msg.realId, msg.time)
val text = msg.message val text = msg.message
.firstOrNull { it.type == MessageType.Text } .firstOrNull { it.type == MessageType.Text }
?.data?.text ?.data?.text
?.trim() ?.trim()
?: return ?: return
// 使用命令解析器解析命令
val parsedCommand = commandParser.parseCommand(text) ?: return val parsedCommand = commandParser.parseCommand(text) ?: return
val (_, address) = parsedCommand val (_, address) = parsedCommand

View File

@ -250,6 +250,7 @@ object ModuleFactory {
val groupMessagePollingModule = resolveDependency( val groupMessagePollingModule = resolveDependency(
config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling" config.findDependency(GROUP_MESSAGE_POLLING_MODULE), "groupMessagePolling"
) as GroupMessagePollingModule ) as GroupMessagePollingModule
val maxBlockRecords = config.getOrDefault("max-block-records", 200)
return RconCommandModule( return RconCommandModule(
config.name, config.name,
groupMessagePollingModule, groupMessagePollingModule,
@ -260,7 +261,8 @@ object ModuleFactory {
selfNickName, selfNickName,
allowedUsers, allowedUsers,
commandBlocklist, commandBlocklist,
commandPrefix commandPrefix,
maxBlockRecords
) )
} }

View File

@ -5,6 +5,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch 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.TriggerMessageFilter
import top.r3944realms.ltdmanager.module.common.filter.type.IgnoreSelfFilter 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.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.napcat.request.other.SendGroupMsgRequest
import top.r3944realms.ltdmanager.utils.CmdUtil import top.r3944realms.ltdmanager.utils.CmdUtil
import top.r3944realms.ltdmanager.utils.LoggerUtil import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.io.File
class RconCommandModule( class RconCommandModule(
moduleName: String, moduleName: String,
@ -29,13 +33,22 @@ class RconCommandModule(
private val allowedUsers: Set<Long>, private val allowedUsers: Set<Long>,
private val commandBlocklist: Set<String>, private val commandBlocklist: Set<String>,
private val commandPrefix: String, private val commandPrefix: String,
) : BaseModule(Modules.RCON_COMMAND, moduleName) { private val maxBlockRecords: Int = 200,
) : BaseModule(Modules.RCON_COMMAND, moduleName), PersistentState<RconCommandModule.BlockState> {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
private var lastTriggeredRealId: Long = -1 private var lastTriggeredRealId: Long = -1
private var lastTriggerTime: Long = 0 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 { private val triggerFilter by lazy {
TriggerMessageFilter( TriggerMessageFilter(
listOf( listOf(
@ -50,6 +63,7 @@ class RconCommandModule(
LoggerUtil.logger.info("[$name] RCON命令模块已装载") LoggerUtil.logger.info("[$name] RCON命令模块已装载")
LoggerUtil.logger.info("[$name] 允许用户: $allowedUsers") LoggerUtil.logger.info("[$name] 允许用户: $allowedUsers")
LoggerUtil.logger.info("[$name] 命令黑名单: $commandBlocklist") LoggerUtil.logger.info("[$name] 命令黑名单: $commandBlocklist")
LoggerUtil.logger.info("[$name] 阻止记录数: ${blockState.records.size}")
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope!!.launch { scope!!.launch {
@ -60,6 +74,7 @@ class RconCommandModule(
} }
override suspend fun onUnload() { override suspend fun onUnload() {
saveState(blockState)
scope?.cancel() scope?.cancel()
LoggerUtil.logger.info("[$name] RCON命令模块已卸载") LoggerUtil.logger.info("[$name] RCON命令模块已卸载")
} }
@ -73,6 +88,7 @@ class RconCommandModule(
} catch (e: Exception) { } catch (e: Exception) {
LoggerUtil.logger.error("[$name] 处理RCON命令失败", e) LoggerUtil.logger.error("[$name] 处理RCON命令失败", e)
sendReply(triggerMsg, "命令执行异常: ${e.message}") sendReply(triggerMsg, "命令执行异常: ${e.message}")
updateTriggerState(triggerMsg)
} }
} }
@ -86,28 +102,40 @@ class RconCommandModule(
return return
} }
// 权限检查
if (msg.userId !in allowedUsers) { if (msg.userId !in allowedUsers) {
LoggerUtil.logger.warn("[$name] 用户 ${msg.userId} 无权限执行RCON: $rconCommand") LoggerUtil.logger.warn("[$name] 用户 ${msg.userId} 无权限执行RCON: $rconCommand")
sendReply(msg, "你没有权限执行 RCON 命令") sendReply(msg, "你没有权限执行 RCON 命令")
updateTriggerState(msg)
return return
} }
// 危险命令检查
val blocked = findBlocklistMatch(rconCommand) val blocked = findBlocklistMatch(rconCommand)
if (blocked != null) { if (blocked != null) {
LoggerUtil.logger.warn("[$name] 阻止危险命令: '$rconCommand' (匹配黑名单: $blocked)") LoggerUtil.logger.warn("[$name] 阻止危险命令: '$rconCommand' (匹配黑名单: $blocked) 来自 ${msg.userId}")
sendReply(msg, "命令已被阻止 (匹配黑名单规则: $blocked)") sendReply(msg, "命令已被阻止 (匹配黑名单规则: $blocked)")
recordBlock(msg.userId, rconCommand, blocked)
updateTriggerState(msg)
return return
} }
// 执行RCON
LoggerUtil.logger.info("[$name] 用户 ${msg.userId} 执行RCON: $rconCommand") LoggerUtil.logger.info("[$name] 用户 ${msg.userId} 执行RCON: $rconCommand")
val output = runRconCommand(rconCommand) val output = runRconCommand(rconCommand)
sendResult(msg, rconCommand, output) sendResult(msg, rconCommand, output)
updateTriggerState(msg) 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? { private fun findBlocklistMatch(command: String): String? {
val lower = command.lowercase().trimStart('/') val lower = command.lowercase().trimStart('/')
return commandBlocklist.firstOrNull { blocked -> return commandBlocklist.firstOrNull { blocked ->
@ -139,7 +167,6 @@ class RconCommandModule(
return return
} }
// 长输出 → 合并转发
val chunks = trimmed.chunked(maxLen) val chunks = trimmed.chunked(maxLen)
val messages = chunks.map { chunk -> val messages = chunks.map { chunk ->
SendForwardMsgRequest.Message( SendForwardMsgRequest.Message(
@ -196,7 +223,46 @@ class RconCommandModule(
appendLine(" $commandPrefix difficulty peaceful") appendLine(" $commandPrefix difficulty peaceful")
} }
override fun info(): String = "RCON命令模块 - 前缀: $commandPrefix, 允许用户: ${allowedUsers.size}人, 黑名单规则: ${commandBlocklist.size}" // ======== 持久化 ========
@Serializable
data class BlockState(val records: List<BlockRecord> = 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 { override fun help(): String = buildString {
appendLine("RCON命令模块 - 通过QQ群执行Minecraft RCON命令") appendLine("RCON命令模块 - 通过QQ群执行Minecraft RCON命令")
appendLine("前缀: $commandPrefix") appendLine("前缀: $commandPrefix")

View File

@ -289,11 +289,10 @@ class WhitelistAuditModule(
private suspend fun handleReActivationMessages(messages: List<MsgHistorySpecificMsg>) { private suspend fun handleReActivationMessages(messages: List<MsgHistorySpecificMsg>) {
val msg = messages.maxByOrNull { it.time } ?: return val msg = messages.maxByOrNull { it.time } ?: return
updateMsgState(msg)
val key = msg.userId.toString() val key = msg.userId.toString()
val entry = auditState.entries[key] ?: return val entry = auditState.entries[key] ?: return
updateMsgState(msg)
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val graceMs = gracePeriodDays * 24 * 60 * 60 * 1000L val graceMs = gracePeriodDays * 24 * 60 * 60 * 1000L
@ -339,6 +338,9 @@ class WhitelistAuditModule(
private suspend fun handleAuditCommand(messages: List<MsgHistorySpecificMsg>) { private suspend fun handleAuditCommand(messages: List<MsgHistorySpecificMsg>) {
val msg = messages.maxByOrNull { it.time } ?: return val msg = messages.maxByOrNull { it.time } ?: return
lastAuditRealId = msg.realId
lastAuditTime = msg.time
if (msg.userId !in auditAllowedUsers) { if (msg.userId !in auditAllowedUsers) {
napCatClient.sendUnit( napCatClient.sendUnit(
SendGroupMsgRequest( SendGroupMsgRequest(