package top.r3944realms.ltdmanager.module import kotlinx.coroutines.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import top.r3944realms.ltdmanager.mcserver.McServerStatus 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.request.message.SendForwardMsgRequest import top.r3944realms.ltdmanager.napcat.request.other.SendGroupMsgRequest import top.r3944realms.ltdmanager.utils.LoggerUtil import java.io.File import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock class McServerStatusModule( private val groupMessagePollingModule: GroupMessagePollingModule, private val selfId: Long, private val selfNickName: String, private val cooldownSeconds: Long = 60, private val commands: List = listOf("/mcs", "/s"), private val presetServer: Map, String> = mapOf( setOf("hp", "hypixel") to "mc.hypixel.net", setOf("pm", "mineplex") to "play.mineplex.com" ) ) : BaseModule(), PersistentState { private val presetServerByAlias: Map by lazy { presetServer.flatMap { (aliases, ip) -> aliases.map { it.lowercase() to ip } }.toMap() } fun getServerIp(alias: String): String? = presetServerByAlias[alias.lowercase()] override val name: String = "McServerStatusModule" private var scope: CoroutineScope? = null private val stateFile = getStateFile("mc_server_status_state.json") private val stateBackupFile = getStateFile("mc_server_status_state.json.bak") private val fileLock = ReentrantLock() private var cooldownState = loadState() override fun getStateFile(): File = stateFile override fun getState(): CooldownState = cooldownState override fun onLoad() { LoggerUtil.logger.info("[$name] 模块已装载,目标群组: ${groupMessagePollingModule.targetGroupId}") scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) scope!!.launch { LoggerUtil.logger.info("[$name] 轮询协程启动") groupMessagePollingModule.messagesFlow.collect { messages -> if (loaded) handleMessages(messages) } } } override suspend fun onUnload() { saveState(cooldownState) scope?.cancel() LoggerUtil.logger.info("[$name] 模块已卸载完成") } private suspend fun handleMessages(messages: List) { if (messages.isEmpty()) return val triggerMsgs = filterTriggerMessages(messages) if (triggerMsgs.isEmpty()) return try { triggerMsgs.forEach { processCommand(it) } } catch (e: Exception) { sendFailedMessage(napCatClient, text = "系统错误,请联系管理员: $e") } finally { saveState(cooldownState) } } private suspend fun filterTriggerMessages(messages: List) : List { val filtered = messages.asSequence() .filter { msg -> // 忽略自己消息 msg.userId != selfId && // 新消息判断 (msg.time > cooldownState.getLastTriggerTime(msg.userId) || (msg.time == cooldownState.getLastTriggerTime(msg.userId) && msg.realId > cooldownState.getLastTriggerRealId(msg.userId))) } .filter { msg -> // 检查命令 msg.message.any { seg -> seg.type == MessageType.Text && ( seg.data.text?.let { text -> commands.any { cmd -> text.startsWith(cmd) } } == true ) } } .filter { runBlocking { handleCooldown(it) } } // 这里处理冷却 .toList() return filtered } 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 cooldownState = cooldownState.updateLastTrigger(qq, realId, time) } else { val request = SendGroupMsgRequest( listOf(MessageElement.text(text)), ID.long(groupMessagePollingModule.targetGroupId) ) client.sendUnit(request) LoggerUtil.logger.info("[$name] 已发送 失败消息[无指定对象]") } } /** 冷却提示消息 */ private suspend fun handleCooldown(msg: GetFriendMsgHistoryEvent.SpecificMsg): Boolean { val trigger = cooldownState.map[msg.userId] val lastTriggerTime = trigger?.time ?: -1L val lastCooldownRealId = trigger?.lastCooldownRealId ?: -1L val nowSec = System.currentTimeMillis() / 1000 // 未触发过或者已超过冷却 if (lastTriggerTime == -1L || nowSec - lastTriggerTime >= cooldownSeconds) { return true } // 冷却中且未发送过冷却提示 if (msg.realId != lastCooldownRealId) { val remaining = ((cooldownSeconds - (nowSec - lastTriggerTime))).coerceAtLeast(1) val msgText = "⏳ 查询过于频繁, $remaining 秒后执行查询,切勿重复发送" sendCooldownMessage(napCatClient, msg.userId, msg.realId, msgText) cooldownState = cooldownState.updateLastCooldownRealId(msg.userId, msg.realId) } return false } private suspend fun sendCooldownMessage(client: NapCatClient, qq: Long, realId: Long, text: String) { val request = SendGroupMsgRequest( MessageElement.reply(ID.long(realId), text), ID.long(groupMessagePollingModule.targetGroupId) ) client.sendUnit(request) } private suspend fun processCommand(msg: GetFriendMsgHistoryEvent.SpecificMsg) { // 找出文本内容 val text = msg.message .firstOrNull { it.type == MessageType.Text } ?.data?.text ?.trim() ?: return // 解析命令 val matchedCommand = commands.firstOrNull { text.startsWith(it) } ?: return var address = text.removePrefix(matchedCommand).trim() // 使用预设别名替换 presetServerByAlias[address.lowercase()]?.let { presetIp -> address = presetIp } if (address.isEmpty()) { sendFailedMessage( napCatClient, msg.userId, msg.realId, msg.time, "❌ 请输入服务器地址,例如 /mcs n2.akiracloud.net:10599" ) return } try { val status = mcSrvStatusClient.getServerStatus(address) // 返回 McServerStatus // 检查是否查询失败 if (!status.online) { sendFailedMessage( napCatClient, msg.userId, msg.realId, msg.time, "❌ 查询失败,请检查服务器地址或服务器是否在线" ) return } // 查询成功,发送状态消息 sendStatusForwardMessage(napCatClient, msg, address, status, msg.realId, msg.time) } catch (e: Exception) { LoggerUtil.logger.error("查询服务器状态失败: $address", e) sendFailedMessage( napCatClient, msg.userId, msg.realId, msg.time, "❌ 查询失败,请检查服务器地址或服务器是否在线" ) } } // ---------------- 转发消息封装 ---------------- private suspend fun sendStatusForwardMessage( client: NapCatClient, msg: GetFriendMsgHistoryEvent.SpecificMsg, address: String, status: McServerStatus, realId: Long, time: Long ) { LoggerUtil.logger.info("[$name] 发送服务器状态转发消息: realId=$realId, address=$address, online=${status.online}") val messages = mutableListOf() // ① 服务器基本信息 + MOTD val motdText = status.motd?.clean?.joinToString("\n") ?: "无 MOTD" val basicInfo = buildString { appendLine("🌐 服务器: $address") appendLine("─".repeat(25)) appendLine("MOTD:\n$motdText") } messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(basicInfo), MessageType.Text)) // ② 玩家列表 val playerList = status.players?.list?.joinToString("\n") { it.name } ?: "无" val playersInfo = buildString { appendLine("📊 在线: ${status.players?.online ?: 0}/${status.players?.max ?: 0}") appendLine("👥 玩家:\n$playerList") } messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(playersInfo), MessageType.Text)) // ③ 版本 + 状态 val versionStatus = buildString { appendLine("🎮 版本: ${status.version ?: "未知"}") appendLine("✅ 状态: ${if (status.online) "在线" else "离线"}") status.software?.let { appendLine("💻 软件: $it") } } messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(versionStatus), MessageType.Text)) // ④ 摘要信息 val summaryText = buildString { appendLine("📌 查询摘要") appendLine("─".repeat(20)) appendLine("服务器: $address") appendLine("在线玩家: ${status.players?.online ?: 0}/${status.players?.max ?: 0}") appendLine("状态: ${if (status.online) "在线" else "离线"}") appendLine("🕐 ${getCurrentTime()}") appendLine("🤖 由 $selfNickName 提供") } messages.add(SendForwardMsgRequest.Message(SendForwardMsgRequest.PurpleData(summaryText), MessageType.Text)) // 封装 Forward 消息 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("点击查看服务器状态与玩家列表"), SendForwardMsgRequest.ForwardModelNews("在线 ${status.players?.online ?: 0} / ${status.players?.max ?: 0}"), SendForwardMsgRequest.ForwardModelNews("更新时间: ${getCurrentTime()}") ), prompt = "服务器状态查询结果", source = "🎮 服务器状态", summary = "在线 ${status.players?.online ?: 0} / ${status.players?.max ?: 0} 人" ) client.sendUnit(request) LoggerUtil.logger.info("[$name] 已发送服务器状态转发消息") // 更新冷却状态 cooldownState = cooldownState.updateLastTrigger(msg.userId, realId, time) } // 时间格式化 private fun getCurrentTime(): String { return java.time.LocalDateTime.now() .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) } // ---------------- 状态持久化 ---------------- @Serializable data class CooldownState( val map: Map = emptyMap() ) { fun getLastTriggerTime(qq: Long): Long = map[qq]?.time ?: -1 fun getLastTriggerRealId(qq: Long): Long = map[qq]?.realId ?: -1 fun updateLastTrigger(qq: Long, realId: Long, time: Long = -1): CooldownState { val old = map[qq] val newTime = if (time != -1L) time else old?.time ?: -1 val newMap = map.toMutableMap().apply { put(qq, TriggerDetail(realId, newTime, old?.lastCooldownRealId ?: -1)) } return copy(map = newMap) } fun updateLastCooldownRealId(qq: Long, realId: Long): CooldownState { val old = map[qq] val newMap = map.toMutableMap().apply { put(qq, TriggerDetail( realId = old?.realId ?: -1, time = old?.time ?: -1, lastCooldownRealId = realId )) } return copy(map = newMap) } } @Serializable data class TriggerDetail( val realId: Long, val time: Long, val lastCooldownRealId: Long = -1L ) override fun loadState(): CooldownState { return try { val fileToRead = when { stateFile.exists() -> stateFile stateBackupFile.exists() -> stateBackupFile else -> null } if (fileToRead == null) return CooldownState() val content = fileToRead.readText() Json.decodeFromString(CooldownState.serializer(), content) } catch (e: Exception) { LoggerUtil.logger.warn("[$name] 状态恢复失败,使用默认值", e) CooldownState() } } override fun saveState(state: CooldownState) { fileLock.withLock { try { val json = Json.encodeToString(CooldownState.serializer(), state) if (stateFile.exists()) stateFile.copyTo(stateBackupFile, overwrite = true) stateFile.writeText(json) } catch (e: Exception) { LoggerUtil.logger.error("[$name] 保存状态失败", e) } } } }