feat: 初步使用版本,目前系统很粗糙,未来考虑会向外扩展

This commit is contained in:
叁玖领域 2025-08-25 19:30:13 +08:00
parent bf6c5b9a46
commit aa65f3fea8
328 changed files with 2391 additions and 895 deletions

2
.gitignore vendored
View File

@ -41,3 +41,5 @@ bin/
### Mac OS ###
.DS_Store
/logs/
/config/
/rcon_playerlist_state.json

17
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@47.116.125.76" uuid="5b1b9d12-d8be-43ba-a647-9d6e467bf201">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://47.116.125.76:3308</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

7
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/main/kotlin/top/r3944realms/ltdmanager/module/GroupRequestHandlerModule.kt" dialect="GenericSQL" />
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

View File

@ -1 +1,7 @@
# 基于NapCat Websocket Server 框架开发
# 基于NapCat Http/Websocket Server API开发
## 目标1实现白名单申请通过后加群自动通过
### 拆分目标:
轮询
1. 获取指定加群请求
2. 对比数据库中的以通过QQ号
3. 批量通过/拒绝请求

View File

@ -1,15 +1,19 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
kotlin("jvm") version "1.9.23"
kotlin("plugin.serialization") version "1.9.23" // 添加序列化插件
application
id("com.github.johnrengelman.shadow") version "8.0.0" // fat jar
}
group = "top.r3944realms.ltdmanager"
version = "1.0-SNAPSHOT"
group = project.property("project_group") as String
version = project.property("project_version") as String
repositories {
repositories {
gradlePluginPortal()
mavenLocal()
maven {
url = uri("https://maven.aliyun.com/repository/public/")
@ -42,7 +46,8 @@ repositories {
implementation("org.apache.logging.log4j:log4j-api:2.20.0")
// 配置管理
implementation("org.yaml:snakeyaml:2.2")
implementation("org.yaml:snakeyaml:2.4")
implementation("org.snakeyaml:snakeyaml-engine:2.10")
implementation("com.typesafe:config:1.4.2") // 类型安全的配置库
// 协程
@ -60,6 +65,21 @@ repositories {
jvmToolchain(17)
}
application {
mainClass.set("top.r3944realms.ltdmanager.main") // 设置主类
mainClass.set("top.r3944realms.ltdmanager.MainKt") // 设置主类
}
}
tasks {
// ShadowJar 配置
named<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveClassifier.set("") // 去掉 -all 后缀
mergeServiceFiles()
manifest {
attributes["Main-Class"] = "top.r3944realms.ltdmanager.MainKt"
}
}
// build 依赖 shadowJar
build {
dependsOn("shadowJar")
}
}

View File

@ -1,19 +1,3 @@
# NapCat
将回应抽象为event模型
将请求抽象为request模型
优先级发送流程
sequenceDiagram
participant Client
participant PriorityQueue
participant PendingResponses
participant Server
Client->>PriorityQueue: sendRequest(高优先级)
Client->>PriorityQueue: sendRequest(低优先级)
PriorityQueue->>Server: 先发送高优先级请求
Server->>PendingResponses: 返回响应1
PendingResponses->>Client: 解除高优先级请求的await
PriorityQueue->>Server: 发送低优先级请求
Server->>PendingResponses: 返回响应2
PendingResponses->>Client: 解除低优先级请求的await
将请求抽象为request模型

View File

@ -1,4 +1,6 @@
kotlin.code.style=official
org.gradle.downloadSources=false
org.gradle.parallel=true
org.gradle.degree_of_parallelism=16
org.gradle.degree_of_parallelism=16
project_group=top.r3944realms.ltdmanager
project_version=1.2-SNAPSHOT

View File

@ -0,0 +1,55 @@
package top.r3944realms.ltdmanager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import top.r3944realms.ltdmanager.core.mysql.MysqlHikariConnectPool
import top.r3944realms.ltdmanager.module.ModuleManager
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.sql.Connection
object GlobalManager {
// 单例作用域,可在模块中使用协程
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Hikari 数据源
private val dataSource: MysqlHikariConnectPool by lazy {
MysqlHikariConnectPool()
}
// NapCat 客户端
val napCatClient: NapCatClient by lazy {
NapCatClient.create()
}
val moduleManager: ModuleManager by lazy { ModuleManager() }
/**
* 获取数据库连接
* 使用 try-with-resources 时会自动关闭
*/
fun getConnection(): Connection {
return dataSource.getConnection()
}
/**
* 关闭全局资源
* 例如在应用退出时调用
*/
fun shutdown() {
try {
LoggerUtil.logger.info("关闭 NapCatClient")
napCatClient.close()
} catch (e: Exception) {
LoggerUtil.logger.warn("关闭 NapCatClient 失败", e)
}
try {
LoggerUtil.logger.info("关闭 Hikari 数据源")
dataSource.close()
} catch (e: Exception) {
LoggerUtil.logger.warn("关闭 Hikari 数据源失败", e)
}
}
}

View File

@ -2,7 +2,6 @@ package top.r3944realms.ltdmanager.core.config
import top.r3944realms.ltdmanager.utils.CryptoUtil
import top.r3944realms.ltdmanager.utils.YamlUpdater
import java.util.*
data class DatabaseConfig(
var url: String? = null,
@ -37,12 +36,8 @@ data class DatabaseConfig(
}
try {
encryptedPassword = "ENC(${CryptoUtil.encrypt(encryptedPassword!!)})"
YamlUpdater.updateYamlValue(
Objects.requireNonNull(
YamlConfigLoader::class.java
.classLoader
.getResource("application.yaml")
).path,
YamlUpdater.updateYaml(
YamlConfigLoader.configFilePath.toString(),
"database.encrypted-password",
this.encryptedPassword!!
)
@ -54,7 +49,7 @@ data class DatabaseConfig(
/**
* 检查密码是否已加密
*/
fun isEncrypted(): Boolean {
private fun isEncrypted(): Boolean {
return encryptedPassword != null &&
encryptedPassword!!.startsWith("ENC(") &&
encryptedPassword!!.endsWith(")")

View File

@ -0,0 +1,59 @@
package top.r3944realms.ltdmanager.core.config
import top.r3944realms.ltdmanager.utils.CryptoUtil
import top.r3944realms.ltdmanager.utils.YamlUpdater
data class HttpConfig(
var url: String? = null,
var encryptedToken: String? = null
) {
/**
* 获取解密后的token如果未加密返回原值
*/
val decryptedToken: String?
get() {
if (encryptedToken == null) {
return null
}
if (!isEncrypted()) {
return encryptedToken
}
try {
val cipherText = encryptedToken!!.substring(4, encryptedToken!!.length - 1)
return CryptoUtil.decrypt(cipherText)
} catch (e: Exception) {
throw IllegalStateException("token解密失败", e)
}
}
/**
* 加密密码如果未加密并返回是否成功加密
*/
fun encryptToken() {
if (encryptedToken == null || isEncrypted()) {
return
}
try {
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
YamlUpdater.updateYaml(
YamlConfigLoader.configFilePath.toString(),
"http.encrypted-token",
this.encryptedToken!!
)
} catch (e: Exception) {
throw IllegalStateException("密码加密失败", e)
}
}
/**
* 检查Token是否已加密
*/
private fun isEncrypted(): Boolean {
return encryptedToken != null &&
encryptedToken!!.startsWith("ENC(") &&
encryptedToken!!.endsWith(")")
}
override fun toString(): String {
return "HttpConfig(Url=$url, token=***)"
}
}

View File

@ -0,0 +1,13 @@
package top.r3944realms.ltdmanager.core.config
import top.r3944realms.ltdmanager.utils.ApiType
import top.r3944realms.ltdmanager.utils.Environment
data class ModeConfig(
var botApiType: ApiType? = null,
var environment: Environment? = null,
) {
override fun toString(): String {
return "ModeConfig(botApiType=$botApiType, environment=$environment)"
}
}

View File

@ -0,0 +1,61 @@
package top.r3944realms.ltdmanager.core.config
import top.r3944realms.ltdmanager.utils.CryptoUtil
import top.r3944realms.ltdmanager.utils.YamlUpdater
data class ToolConfig(
var rcon: RconConfig = RconConfig()
) {
data class RconConfig(
var mcRconToolPath: String? = null,
var mcRconToolConfigPath: String? = null,
var serverUrl: String? = null,
var rconPassword: String? = null
) {
/**
* 获取解密后的 rcon 密码如果未加密返回原值
*/
val decryptedPassword: String?
get() {
if (rconPassword == null) return null
if (!isEncrypted()) return rconPassword
return try {
val cipherText = rconPassword!!.substring(4, rconPassword!!.length - 1)
CryptoUtil.decrypt(cipherText)
} catch (e: Exception) {
throw IllegalStateException("RCON 密码解密失败", e)
}
}
/**
* 加密 rcon 密码如果未加密
*/
fun encryptPassword(configFilePath: String) {
if (rconPassword == null || isEncrypted()) return
try {
rconPassword = "ENC(${CryptoUtil.encrypt(rconPassword!!)})"
YamlUpdater.updateYaml(
configFilePath,
"tools.rcon.rcon-password",
rconPassword!!
)
} catch (e: Exception) {
throw IllegalStateException("RCON 密码加密失败", e)
}
}
private fun isEncrypted(): Boolean {
return rconPassword != null &&
rconPassword!!.startsWith("ENC(") &&
rconPassword!!.endsWith(")")
}
override fun toString(): String {
return "RconConfig(path=$mcRconToolPath, configPath=$mcRconToolConfigPath, url=$serverUrl, password=***)"
}
}
override fun toString(): String {
return "ToolConfig(rcon=$rcon)"
}
}

View File

@ -1,10 +1,59 @@
package top.r3944realms.ltdmanager.core.config
import top.r3944realms.ltdmanager.utils.CryptoUtil
import top.r3944realms.ltdmanager.utils.YamlUpdater
data class WebsocketConfig(
var url: String? = null,
var token: String? = null
var encryptedToken: String? = null
) {
/**
* 获取解密后的token如果未加密返回原值
*/
val decryptedToken: String?
get() {
if (encryptedToken == null) {
return null
}
if (!isEncrypted()) {
return encryptedToken
}
try {
val cipherText = encryptedToken!!.substring(4, encryptedToken!!.length - 1)
return CryptoUtil.decrypt(cipherText)
} catch (e: Exception) {
throw IllegalStateException("token解密失败", e)
}
}
/**
* 加密密码如果未加密并返回是否成功加密
*/
fun encryptToken() {
if (encryptedToken == null || isEncrypted()) {
return
}
try {
encryptedToken = "ENC(${CryptoUtil.encrypt(encryptedToken!!)})"
YamlUpdater.updateYaml(
YamlConfigLoader.configFilePath.toString(),
"websocket.encrypted-token",
this.encryptedToken!!
)
} catch (e: Exception) {
throw IllegalStateException("密码加密失败", e)
}
}
/**
* 检查Token是否已加密
*/
private fun isEncrypted(): Boolean {
return encryptedToken != null &&
encryptedToken!!.startsWith("ENC(") &&
encryptedToken!!.endsWith(")")
}
override fun toString(): String {
return "WebsocketConfig(Url=$url, token=$token)"
return "WebsocketConfig(Url=$url, token=***)"
}
}

View File

@ -5,22 +5,50 @@ import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.Constructor
import org.yaml.snakeyaml.introspector.Property
import org.yaml.snakeyaml.introspector.PropertyUtils
import top.r3944realms.ltdmanager.utils.ConfigInitializer
import top.r3944realms.ltdmanager.utils.NamingConventionUtil
import java.nio.file.Files
import java.nio.file.Paths
object YamlConfigLoader {
private val config: ConfigWrapper = loadConfig().also {
ensureConfigEncrypted(it) // 初始化后立即加密
val configFilePath = Paths.get("config/application.yaml") // 配置文件路径
private val _config by lazy { loadConfig() } // 延迟初始化
val config: ConfigWrapper get() = _config
init {
// 第一次启动确保配置文件存在
ConfigInitializer.initConfig("application.yaml", "config")
// 初始化后加密(确保只执行一次)
runCatching {
_config.database.encryptPassword()
_config.websocket.encryptToken()
_config.http.encryptToken()
}.onFailure { e ->
println("初始化加密失败: ${e.message}")
e.printStackTrace()
}
}
private fun ensureConfigEncrypted(config: ConfigWrapper?) {
config?.database?.encryptPassword()
config?.websocket?.encryptToken()
config?.http?.encryptToken()
}
private fun loadConfig(): ConfigWrapper {
YamlConfigLoader::class.java.classLoader.getResourceAsStream("application.yaml").use { inputStream ->
if (inputStream == null) {
throw RuntimeException("配置文件 application.yaml 未找到!")
}
return Yaml(getConstructor()).load(inputStream)
if (!Files.exists(configFilePath)) {
throw RuntimeException("配置文件未找到: $configFilePath")
}
try {
val yamlContent = Files.readString(configFilePath)
return Yaml(getConstructor()).load(yamlContent)
?: throw RuntimeException("YAML解析返回null")
} catch (e: Exception) {
throw RuntimeException("YAML解析失败: ${e.message}", e)
}
}
private fun getConstructor(): Constructor {
val propertyUtils = object : PropertyUtils() {
@ -38,12 +66,20 @@ object YamlConfigLoader {
setPropertyUtils(propertyUtils)
}
}
fun loadDatabaseConfig(): DatabaseConfig = config.database
fun loadCryptoConfig(): CryptoConfig = config.crypto
fun loadWebsocketConfig(): WebsocketConfig = config.websocket
fun loadHttpConfig(): HttpConfig = config.http
fun loadModeConfig(): ModeConfig = config.mode
fun loadToolConfig(): ToolConfig = config.tools
data class ConfigWrapper(
var database :DatabaseConfig,
var crypto :CryptoConfig,
var websocket :WebsocketConfig
var database: DatabaseConfig = DatabaseConfig(),
var crypto: CryptoConfig = CryptoConfig(),
var mode: ModeConfig = ModeConfig(),
var websocket: WebsocketConfig = WebsocketConfig(),
var http: HttpConfig = HttpConfig(),
var tools: ToolConfig = ToolConfig(),
)
}

View File

@ -1,28 +1,60 @@
package top.r3944realms.ltdmanager
import org.slf4j.LoggerFactory
import top.r3944realms.ltdmanager.napcat.events.NapCatEvent
import top.r3944realms.ltdmanager.napcat.events.group.SetGroupPortraitEvent
import top.r3944realms.ltdmanager.napcat.events.group.SetGroupSearchEvent
import top.r3944realms.ltdmanager.napcat.events.personal.CanSendImageEvent
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import top.r3944realms.ltdmanager.module.GroupRequestHandlerModule
import top.r3944realms.ltdmanager.module.RconPlayerListModule
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.util.concurrent.atomic.AtomicBoolean
fun main() {
val logger = LoggerFactory.getLogger("log")
logger.info("Start")
val toJSON = SetOnlineStatusRequest.ONLINE.toJSON()
logger.info("S:{}",toJSON)
val str: String = """
{
"status": "ok",
"retcode": 0,
"data": null,
"message": "string",
"wording": "string",
"echo": "string"
fun main() = runBlocking {
// 标记程序是否运行
val isRunning = AtomicBoolean(true)
// 创建模块实例
val groupModule = GroupRequestHandlerModule(
client = GlobalManager.napCatClient,
targetGroupId = 538751386
)
val rconModule = RconPlayerListModule(
pollIntervalMillis = 3_000L,
timeout = 2_000L,
cooldownMillis = 10_000L,
targetGroupId = 538751386,
selfId = 3327379836,
selfNickName = "闲趣老土豆",
keywords = setOf(
//形容
"土豆", "马铃薯", "Potato", "potato", "POTATO",
"Potatoes", "potatoes", "POTATOES", "🥔",
//正经
"列表","服务器状态", "TPS", "tps", "list", "List"
)
);
// 注册模块到全局模块管理器
GlobalManager.moduleManager.registerModule(groupModule)
GlobalManager.moduleManager.registerModule(rconModule)
// 加载模块
GlobalManager.moduleManager.loadModule(groupModule.name)
GlobalManager.moduleManager.loadModule(rconModule.name)
// 捕获 JVM 关闭信号,优雅退出
Runtime.getRuntime().addShutdownHook(Thread {
runBlocking {
LoggerUtil.logger.info("\n收到退出信号,正在停止所有模块...")
GlobalManager.moduleManager.stopAllModules() // 批量 stop
LoggerUtil.logger.info("模块卸载完成,程序退出。")
GlobalManager.shutdown()
}
""".trimIndent()
val decodeEvent = NapCatEvent.decodeEvent(str, "group/set_group_search")
if (decodeEvent is SetGroupSearchEvent) {
logger.info("data:{}",decodeEvent.data)
isRunning.set(false)
})
// 持续挂起,保持主线程运行
while (isRunning.get()) {
delay(1000L)
}
}

View File

@ -0,0 +1,70 @@
package top.r3944realms.ltdmanager.module
import top.r3944realms.ltdmanager.GlobalManager
/**
* 模块抽象基类
* 所有功能模块都继承该类
*/
abstract class BaseModule {
/**
* 模块名称
*/
abstract val name: String
/**
* 模块是否加载
*/
@Volatile
var loaded: Boolean = false
private set
/**
* 模块加载
* 可以在这里初始化协程监听器定时任务等
*/
open fun load() {
if (!loaded) {
loaded = true
onLoad()
}
}
/**
* 模块卸载
* 清理资源取消协程关闭监听器等
*/
open fun unload() {
if (loaded) {
loaded = false
onUnload()
}
}
/**
* 模块加载时的实际逻辑由子类实现
*/
protected abstract fun onLoad()
/**
* 模块卸载时的实际逻辑由子类实现
*/
protected abstract fun onUnload()
/**
* 可选的停止方法模块内部协程等后台任务在这里被取消
*/
open suspend fun stop() {
unload() // 默认实现直接卸载
}
/**
* 提供访问全局 NapCatClient 的快捷方式
*/
protected val napCatClient get() = GlobalManager.napCatClient
/**
* 获取数据库连接
* 使用 try-with-resources 时会自动关闭
*/
protected fun getConnection() = GlobalManager.getConnection()
}

View File

@ -0,0 +1,192 @@
package top.r3944realms.ltdmanager.module
import kotlinx.coroutines.*
import top.r3944realms.ltdmanager.napcat.NapCatClient
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupIgnoredNotifiesEvent
import top.r3944realms.ltdmanager.napcat.event.group.GetGroupSystemMsgEvent
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupIgnoredNotifiesRequest
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupSystemMsgRequest
import top.r3944realms.ltdmanager.napcat.request.group.SetGroupAddRequestRequest
import top.r3944realms.ltdmanager.utils.LoggerUtil
class GroupRequestHandlerModule(
private val client: NapCatClient,
private val targetGroupId: Long,
private val pollIntervalMillis: Long = 30_000L,
) : BaseModule() {
override val name: String = "GroupRequestHandlerModule"
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val stopSignal = CompletableDeferred<Unit>()
override fun onLoad() {
LoggerUtil.logger.info("模块[$name]已装载,目标群组: $targetGroupId,轮询间隔: ${pollIntervalMillis}ms")
// 启动轮询协程
scope.launch {
LoggerUtil.logger.info("[$name] 轮询协程启动")
try {
while (isActive) {
try {
LoggerUtil.logger.debug("[$name] 开始轮询群组请求...")
// 获取正常请求
LoggerUtil.logger.debug("[$name] 获取正常群系统消息...")
val systemEvent: GetGroupSystemMsgEvent =
client.send(GetGroupSystemMsgRequest())
LoggerUtil.logger.debug("[$name] 获取到 ${systemEvent.data.invitedRequest.size} 个邀请请求和 ${systemEvent.data.joinRequests.size} 个加群请求")
handleEvent(systemEvent)
// 获取被过滤的请求
LoggerUtil.logger.debug("[$name] 获取被过滤的群系统消息...")
val ignoredEvent: GetGroupIgnoredNotifiesEvent =
client.send(GetGroupIgnoredNotifiesRequest())
LoggerUtil.logger.debug("[$name] 获取到 ${ignoredEvent.data.invitedRequest.size} 个被过滤的邀请请求和 ${ignoredEvent.data.joinRequests.size} 个被过滤的加群请求")
handleEvent(ignoredEvent)
LoggerUtil.logger.debug("[$name] 本轮轮询完成,等待 ${pollIntervalMillis}ms 后继续")
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 轮询执行异常", e)
}
delay(pollIntervalMillis)
}
} catch (e: CancellationException) {
LoggerUtil.logger.info("[$name] 轮询协程收到取消信号")
} finally {
LoggerUtil.logger.info("[$name] 轮询协程退出,完成 stopSignal")
stopSignal.complete(Unit)
}
}
}
override suspend fun stop() {
LoggerUtil.logger.info("[$name] 收到停止命令,开始关闭协程...")
scope.cancel()
LoggerUtil.logger.info("[$name] 等待协程退出...")
stopSignal.await()
LoggerUtil.logger.info("[$name] 协程已退出,卸载模块资源")
onUnload()
}
public override fun onUnload() {
LoggerUtil.logger.info("[$name] 已卸载")
}
private suspend fun handleEvent(event: Any) {
LoggerUtil.logger.debug("[$name] 处理群请求事件: ${event.javaClass.simpleName}")
val provider: GroupRequestProvider? = when (event) {
is GetGroupSystemMsgEvent -> {
LoggerUtil.logger.debug("[$name] 识别为正常群系统消息事件")
event.asProvider()
}
is GetGroupIgnoredNotifiesEvent -> {
LoggerUtil.logger.debug("[$name] 识别为被过滤群系统消息事件")
event.asProvider()
}
else -> {
LoggerUtil.logger.warn("[$name] 未知的事件类型: ${event.javaClass}")
null
}
}
provider?.getAllRequests()?.forEach { request ->
if (!request.checked) {
LoggerUtil.logger.info("[$name] 处理群请求: requestId=${request.requestId}, groupId=${request.groupId}, actor=${request.actor}, type=${request.javaClass}")
if (request.groupId == targetGroupId) {
LoggerUtil.logger.info("[$name] 请求匹配目标群组 $targetGroupId,查询玩家状态...")
val status = queryPlayerStatus(request.invitorUin)
LoggerUtil.logger.info("[$name] 玩家 ${request.invitorUin} 状态查询结果: $status")
when (status) {
1 -> {
LoggerUtil.logger.info("[$name] 允许加群: groupId=${request.groupId}, invitorUin=${request.invitorUin}, requestId=${request.requestId}")
val setGroupAddRequestRequest = SetGroupAddRequestRequest(
true,
request.requestId.toString()
)
client.send<NapCatEvent>(setGroupAddRequestRequest)
LoggerUtil.logger.info("[$name] 已发送同意加群请求")
}
2, 3 -> {
val reason = if (status == 2) "审核未通过" else "待审核"
LoggerUtil.logger.info("[$name] 拒绝加群: groupId=${request.groupId}, invitorUin=${request.invitorUin}, status=$status, reason=$reason, requestId=${request.requestId}")
val request1 = SetGroupAddRequestRequest(
false,
request.requestId.toString(),
reason
)
client.send<NapCatEvent>(request1)
LoggerUtil.logger.info("[$name] 已发送拒绝加群请求")
}
else -> {
LoggerUtil.logger.warn("[$name] 未知玩家状态($status),拒绝请求: invitorUin=${request.invitorUin}, requestId=${request.requestId}")
val request1 = SetGroupAddRequestRequest(
false,
request.requestId.toString(),
"未知状态"
)
client.send<NapCatEvent>(request1)
LoggerUtil.logger.info("[$name] 已发送拒绝加群请求(未知状态)")
}
}
} else {
LoggerUtil.logger.debug("[$name] 请求群组 ${request.groupId} 不匹配目标群组 $targetGroupId,跳过处理")
}
}
}
LoggerUtil.logger.debug("[$name] 事件处理完成")
}
private fun queryPlayerStatus(actor: Long): Int {
LoggerUtil.logger.debug("[$name] 查询玩家状态: qq=$actor")
try {
getConnection().use { conn ->
val stmt = conn.prepareStatement(
"SELECT status FROM minecraft_manager_ltd.players WHERE qq=?"
)
stmt.setLong(1, actor)
val rs = stmt.executeQuery()
val status = if (rs.next()) rs.getInt("status") else 0
LoggerUtil.logger.debug("[$name] 数据库查询结果: qq=$actor, status=$status")
return status
}
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 查询玩家状态失败: qq=$actor", e)
return 0
}
}
/**
* 所有群系统请求的统一访问接口
*/
interface GroupRequestProvider {
fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo>
}
/**
* 正常请求事件实现
*/
private fun GetGroupSystemMsgEvent.asProvider(): GroupRequestProvider = object : GroupRequestProvider {
override fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo> {
return data.invitedRequest + data.joinRequests
}
}
/**
* 被过滤请求事件实现
*/
private fun GetGroupIgnoredNotifiesEvent.asProvider(): GroupRequestProvider = object : GroupRequestProvider {
override fun getAllRequests(): List<GetGroupSystemMsgEvent.SystemInfo> {
return data.invitedRequest + data.joinRequests
}
}
}

View File

@ -0,0 +1,113 @@
package top.r3944realms.ltdmanager.module
import top.r3944realms.ltdmanager.utils.LoggerUtil
class ModuleManager {
private val modules = mutableMapOf<String, BaseModule>()
/**
* 注册模块到管理器
*/
fun registerModule(module: BaseModule) {
if (modules.containsKey(module.name)) {
LoggerUtil.logger.warn("模块已注册: ${module.name}")
return
}
modules[module.name] = module
LoggerUtil.logger.info("模块注册: ${module.name}")
}
/**
* 加载指定模块
*/
fun loadModule(name: String) {
val module = modules[name]
if (module == null) {
LoggerUtil.logger.warn("尝试加载不存在的模块: $name")
return
}
if (module.loaded) {
LoggerUtil.logger.info("模块已加载: $name")
return
}
try {
module.load()
LoggerUtil.logger.info("模块加载: $name")
} catch (e: Exception) {
LoggerUtil.logger.error("加载模块 $name 失败", e)
}
}
/**
* 卸载指定模块
*/
fun unloadModule(name: String) {
val module = modules[name]
if (module == null) {
LoggerUtil.logger.warn("尝试卸载不存在的模块: $name")
return
}
if (!module.loaded) {
LoggerUtil.logger.info("模块未加载: $name")
return
}
try {
module.unload()
LoggerUtil.logger.info("模块卸载: $name")
} catch (e: Exception) {
LoggerUtil.logger.warn("卸载模块 $name 失败", e)
}
}
/**
* 卸载所有模块
*/
fun unloadAll() {
modules.values.forEach { module ->
try {
if (module.loaded) {
module.unload()
LoggerUtil.logger.info("模块卸载: ${module.name}")
}
} catch (e: Exception) {
LoggerUtil.logger.warn("卸载模块 ${module.name} 失败", e)
}
}
}
/**
* 获取所有模块名称
*/
fun getModuleNames(): List<String> = modules.keys.toList()
/**
* 检查模块是否已加载
*/
fun isModuleLoaded(name: String): Boolean {
return modules[name]?.loaded ?: false
}
/**
* 扩展方法批量加载模块
*/
fun ModuleManager.loadModules(vararg names: String) {
names.forEach { loadModule(it) }
}
/**
* 扩展方法批量卸载模块
*/
fun ModuleManager.unloadModules(vararg names: String) {
names.forEach { unloadModule(it) }
}
/**
* 关闭所有模块
*/
suspend fun stopAllModules() {
modules.values.forEach { module ->
if (module.loaded) {
module.stop()
}
}
}
}

View File

@ -0,0 +1,501 @@
package top.r3944realms.ltdmanager.module
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
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.GetGroupMsgHistoryEvent
import top.r3944realms.ltdmanager.napcat.request.message.GetGroupMsgHistoryRequest
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
import java.util.concurrent.TimeoutException
class RconPlayerListModule(
private val pollIntervalMillis: Long = 30_000L,
private val timeout: Long = 2_000L,
private val cooldownMillis: Long = 30_000L, // 默认 30 秒
private var lastSuccessTime: Long = 0L,
private var msgHistoryCheck: Int = 5,
private val targetGroupId: Long,
private val selfId: Long,
private val selfNickName: String,
private val keywords: Set<String> = setOf("查看玩家列表", "玩家列表", "在线玩家")
) : BaseModule() {
private val stopSignal = CompletableDeferred<Unit>() // 用于等待协程退出
override val name: String = "RconPlayerListModule"
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// 持久化文件路径
private val stateFile = File("rcon_playerlist_state.json")
// 保存最新触发过的消息 realId 和 time
private var moduleState: ModuleState = loadState()
private val rconPath: String
get() = YamlConfigLoader.loadToolConfig().rcon.mcRconToolPath
?: throw IllegalStateException("RCON 工具路径未配置")
private val rconConfigPath: String
get() = YamlConfigLoader.loadToolConfig().rcon.mcRconToolConfigPath
?: throw IllegalStateException("Rcon配置路径未配置")
override fun onLoad() {
LoggerUtil.logger.info("[$name] 模块已装载,目标群组: $targetGroupId,轮询间隔: ${pollIntervalMillis}ms")
LoggerUtil.logger.info("[$name] 上次触发状态: realId=${moduleState.lastTriggeredRealId}, time=${moduleState.lastTriggerTime}")
LoggerUtil.logger.info("[$name] 关键词列表: $keywords")
scope.launch {
LoggerUtil.logger.info("[$name] 轮询协程启动")
try {
while (isActive) {
LoggerUtil.logger.debug("[$name] 开始轮询群消息历史...")
try {
val historyEvent = napCatClient.send(
GetGroupMsgHistoryRequest(
count = msgHistoryCheck,
groupId = ID.long(targetGroupId)
)
) as? GetGroupMsgHistoryEvent
val messages = historyEvent?.data?.messages ?: emptyList()
LoggerUtil.logger.debug("[$name] 获取到 ${messages.size} 条最近消息")
// 找到比 lastTriggeredRealId 更新的触发消息
val triggerMessages = messages.filter { msg ->
((msg.time > moduleState.lastTriggerTime ||
(msg.time == moduleState.lastTriggerTime && msg.realId > moduleState.lastTriggeredRealId)) && msg.userId != selfId) &&
msg.message.any { seg ->
seg.type == MessageType.Text &&
seg.data.text?.let { text ->
keywords.any { keyword ->
text == keyword
}
} == true
}
}
LoggerUtil.logger.debug("[$name] 找到 ${triggerMessages.size} 条符合条件的触发消息")
if (triggerMessages.isNotEmpty()) {
val triggerMsg = triggerMessages.maxBy { it.time }
LoggerUtil.logger.info("[$name] 找到触发消息 realId=${triggerMsg.realId}, time=${triggerMsg.time}, userId=${triggerMsg.userId}")
val now = System.currentTimeMillis()
// ✅ 首次触发允许直接执行
val canTrigger = (lastSuccessTime == 0L) || (now - lastSuccessTime >= cooldownMillis)
if (!canTrigger) {
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1)
LoggerUtil.logger.info("[$name] 冷却中,拒绝执行,剩余 $remaining")
sendCooldownMessage(napCatClient, triggerMsg.realId, triggerMsg.time)
continue
}
// 执行 RCON
val commands = listOf("forge tps","list")
LoggerUtil.logger.info("[$name] 执行 RCON 命令: $commands")
runCatching {
val tpsOutput = runCatching {
CmdUtil.runExeCommand(rconPath, "-c", rconConfigPath, "-T", (timeout / 1000).toString() + "s", "forge tps")
}.getOrElse { ex ->
LoggerUtil.logger.warn("[$name] 执行 forge tps 失败: ${ex.message}")
""
}
val listOutput = runCatching {
CmdUtil.runExeCommand(rconPath, "-c", rconConfigPath, "-T", (timeout / 1000).toString() + "s", "list")
}.getOrElse { ex ->
LoggerUtil.logger.warn("[$name] 执行 list 失败: ${ex.message}")
""
}
// 合并输出,再解析
buildString {
appendLine(tpsOutput.trim())
appendLine("--------")
appendLine(listOutput.trim())
}
} .onFailure { ex ->
if (ex is TimeoutException) {
LoggerUtil.logger.warn("[$name] RCON 连接超时: ${ex.message}")
sendFailedMessage(napCatClient, triggerMsg.realId, triggerMsg.time)
} else {
LoggerUtil.logger.error("[$name] RCON 命令执行失败", ex)
sendFailedMessage(
napCatClient,
triggerMsg.realId,
triggerMsg.time,
"系统内部错误请联系管理员:${ex.message}"
)
throw ex
}
} .onSuccess { output ->
lastSuccessTime = now // ✅ 成功后记录冷却开始时间
LoggerUtil.logger.info("[$name] RCON 命令执行成功,输出长度: ${output.length}")
LoggerUtil.logger.debug("[$name] RCON 输出内容: $output")
val tpsInfo = parseTPS(output)
val playerListInfo = parsePlayerList(output)
LoggerUtil.logger.info("[$name] 解析成功: TPS=${tpsInfo.overall.meanTPS}, 在线 ${playerListInfo.onlineCount}")
// 发送转发消息
sendForwardMessage(napCatClient, tpsInfo, playerListInfo, triggerMsg.realId, triggerMsg.time)
}
} else {
LoggerUtil.logger.debug("[$name] 未找到新的触发消息")
}
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 轮询玩家列表或发送转发消息失败", e)
}
LoggerUtil.logger.debug("[$name] 本轮轮询完成,等待 ${pollIntervalMillis}ms")
delay(pollIntervalMillis)
}
} catch (e: CancellationException) {
LoggerUtil.logger.info("[$name] 轮询协程收到取消信号")
} finally {
LoggerUtil.logger.info("[$name] 轮询协程退出,完成 stopSignal")
stopSignal.complete(Unit)
}
}
}
public override fun onUnload() {
LoggerUtil.logger.info("[$name] 模块已卸载")
saveState(moduleState.lastTriggeredRealId, moduleState.lastTriggerTime) // 卸载时保存
}
override suspend fun stop() {
LoggerUtil.logger.info("[$name] 收到停止命令,开始关闭协程...")
scope.cancel() // 取消协程
LoggerUtil.logger.info("[$name] 等待协程退出...")
stopSignal.await() // 等待协程完成
LoggerUtil.logger.info("[$name] 协程已退出,卸载模块资源")
onUnload() // 卸载模块资源,保存状态
}
private suspend fun sendCooldownMessage(client: NapCatClient, realId: Long, time: Long) {
val now = System.currentTimeMillis()
val remaining = ((cooldownMillis - (now - lastSuccessTime)) / 1000).coerceAtLeast(1) // 至少显示 1 秒
val msg = "⏳ 查询过于频繁,请稍后再试(剩余 $remaining 秒)"
LoggerUtil.logger.info("[$name] 发送冷却提示: $msg")
val request = SendGroupMsgRequest(
MessageElement.reply(ID.long(realId), msg),
ID.long(targetGroupId)
)
client.sendUnit(request)
// 更新触发状态,但不更新 lastSuccessTime避免延长冷却
moduleState.lastTriggeredRealId = realId
moduleState.lastTriggerTime = time
saveState(realId, time)
}
private val failedMessages = listOf(
"💥 土豆服务器炸了,请稍后再试",
"🥔 土豆过热,正在冷却中……",
"🐌 RCON 响应太慢,像蜗牛一样",
"🛠️ 系统开小差了,请联系管理员",
"⚠️ 服务器没理我,可能在打盹",
"🔥 电路冒烟了!查询失败"
)
private suspend fun sendFailedMessage(
client: NapCatClient,
realId: Long,
time: Long,
text: String? = null
) {
// 如果调用时传了 text就用 text否则随机选择一条
val finalText = text ?: failedMessages.random()
LoggerUtil.logger.info("[$name] 发送失败消息: realId=$realId, text=$finalText")
val request = SendGroupMsgRequest(
MessageElement.reply(ID.long(realId), finalText),
ID.long(targetGroupId)
)
client.sendUnit(request)
LoggerUtil.logger.info("[$name] 已发送 RCON 失败消息")
// 更新触发的最大 realId
moduleState.lastTriggeredRealId = realId
moduleState.lastTriggerTime = time
saveState(realId, time) // 保存到文件
}
private suspend fun sendForwardMessage(client: NapCatClient, tps: TPSInfo, info: PlayerListInfo, realId: Long, time: Long) {
LoggerUtil.logger.info("[$name] 发送转发消息: realId=$realId, TPS=${tps.overall.meanTPS}, 在线玩家数=${info.onlineCount}")
val messages = mutableListOf<SendForwardMsgRequest.Message>()
// ① 服务器TPS状态
val tpsMessage = SendForwardMsgRequest.Message(
data = SendForwardMsgRequest.PurpleData(
text = buildString {
appendLine("⚡ 服务器性能状态 ${getStatusEmoji(tps.status)}")
appendLine("".repeat(25))
appendLine("整体TPS: ${"%.3f".format(tps.overall.meanTPS)} ${getStatusDescription(tps.status)}")
appendLine("平均Tick耗时: ${"%.3f".format(tps.overall.meanTickTime)} ms")
appendLine()
appendLine("📌 各维度详情:")
tps.dimensions.forEach {
appendLine("- ${it.name}: ${"%.3f".format(it.meanTPS)} TPS, ${"%.3f".format(it.meanTickTime)} ms")
}
}
),
type = MessageType.Text
)
messages.add(tpsMessage)
// ② 玩家列表
if (info.players.isNotEmpty()) {
val playerListMessage = SendForwardMsgRequest.Message(
data = SendForwardMsgRequest.PurpleData(
text = buildString {
appendLine("👥 玩家列表")
appendLine("".repeat(20))
appendLine("在线人数: ${info.onlineCount}")
info.players.forEachIndexed { index, player ->
appendLine("${index + 1}. 🧑‍💻 $player")
}
}
),
type = MessageType.Text
)
messages.add(playerListMessage)
} else {
messages.add(
SendForwardMsgRequest.Message(
data = SendForwardMsgRequest.PurpleData("😴 当前没有玩家在线"),
type = MessageType.Text
)
)
}
// ③ 摘要消息
val summaryMessage = SendForwardMsgRequest.Message(
data = SendForwardMsgRequest.PurpleData(
text = buildString {
appendLine("📊 查询摘要")
appendLine("".repeat(15))
appendLine("TPS: ${"%.3f".format(tps.overall.meanTPS)} - ${getStatusDescription(tps.status)}")
appendLine("在线玩家: ${info.onlineCount}")
appendLine("🕐 ${getCurrentTime()}")
appendLine("🤖 由 $selfNickName 提供")
}
),
type = MessageType.Text
)
messages.add(summaryMessage)
val topMessage = SendForwardMsgRequest.TopForwardMsg(
data = SendForwardMsgRequest.MessageData(
content = messages,
nickname = selfNickName,
userId = ID.long(selfId),
),
type = MessageType.Node
)
val request = SendForwardMsgRequest(
groupId = ID.long(targetGroupId),
messages = listOf(topMessage),
news = listOf(
SendForwardMsgRequest.ForwardModelNews("点击查看服务器状态与玩家列表"),
SendForwardMsgRequest.ForwardModelNews("TPS: ${"%.1f".format(tps.overall.meanTPS)} 在线 ${info.onlineCount}"),
SendForwardMsgRequest.ForwardModelNews("更新时间: ${getCurrentTime()}")
),
prompt = "TPS + 玩家列表查询结果",
source = "🎮 服务器状态",
summary = "TPS ${"%.1f".format(tps.overall.meanTPS)}, 在线玩家: ${info.onlineCount}",
)
client.sendUnit(request)
LoggerUtil.logger.info("[$name] 已发送 TPS+玩家列表 转发消息")
moduleState.lastTriggeredRealId = realId
moduleState.lastTriggerTime = time
saveState(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 TPSInfo(
val dimensions: List<DimensionTPS>,
val overall: OverallTPS,
val status: ServerStatus
) {
@Serializable
data class DimensionTPS(
val name: String,
val meanTickTime: Double,
val meanTPS: Double
)
@Serializable
data class OverallTPS(
val meanTickTime: Double,
val meanTPS: Double
)
enum class ServerStatus {
EXCELLENT, // TPS = 20.0
GOOD, // TPS >= 18.0
FAIR, // TPS >= 15.0
POOR, // TPS >= 10.0
CRITICAL // TPS < 10.0
}
}
// 修改 parsePlayerList 函数来处理组合输出
private fun parsePlayerList(output: String): PlayerListInfo {
LoggerUtil.logger.debug("[$name] 解析玩家列表输出: ${output.take(100)}...")
// 检查是否是连接超时错误
if (output.contains("dial tcp") && output.contains("i/o timeout")) {
LoggerUtil.logger.warn("[$name] 检测到连接超时错误")
throw TimeoutException("服务器不可达")
}
// 分割输出,获取玩家列表部分
val parts = output.split("--------")
val playerListOutput = if (parts.size > 1) parts[1].trim() else output
val regex = Regex("""There are (\d+) of a max of \d+ players online:\s*(.*)""")
val match = regex.find(playerListOutput)
if (match == null) {
LoggerUtil.logger.warn("[$name] 无法解析玩家列表输出,返回空列表")
return PlayerListInfo(0, emptyList())
}
val onlineCount = match.groupValues[1].toInt()
val playersString = match.groupValues[2]
val players = playersString.split(",").map { it.trim() }.filter { it.isNotEmpty() }
LoggerUtil.logger.debug("[{}] 解析完成: 在线 {} 人,玩家列表: {}", name, onlineCount, players)
return PlayerListInfo(onlineCount, players)
}
// 修改 parseTPS 函数来处理组合输出
private fun parseTPS(output: String): TPSInfo {
LoggerUtil.logger.debug("[$name] 解析TPS输出: ${output.take(100)}...")
// 分割输出获取TPS部分
val parts = output.split("--------")
val tpsOutput = parts[0].trim()
val dimensionRegex = Regex("""Dim (.+?): Mean tick time: (\d+\.\d+) ms\. Mean TPS: (\d+\.\d+)""")
val overallRegex = Regex("""Overall: Mean tick time: (\d+\.\d+) ms\. Mean TPS: (\d+\.\d+)""")
val dimensions = mutableListOf<TPSInfo.DimensionTPS>()
var overall: TPSInfo.OverallTPS? = null
tpsOutput.lineSequence().forEach { line ->
// 解析维度TPS
dimensionRegex.find(line)?.let { match ->
val name = match.groupValues[1]
val meanTickTime = match.groupValues[2].toDouble()
val meanTPS = match.groupValues[3].toDouble()
dimensions.add(TPSInfo.DimensionTPS(name, meanTickTime, meanTPS))
}
// 解析总体TPS
overallRegex.find(line)?.let { match ->
val meanTickTime = match.groupValues[1].toDouble()
val meanTPS = match.groupValues[2].toDouble()
overall = TPSInfo.OverallTPS(meanTickTime, meanTPS)
}
}
if (overall == null) {
throw IllegalArgumentException("无法解析TPS输出: $output")
}
// 确定服务器状态
val status = when (overall!!.meanTPS) {
20.0 -> TPSInfo.ServerStatus.EXCELLENT
in 18.0..19.99 -> TPSInfo.ServerStatus.GOOD
in 15.0..17.99 -> TPSInfo.ServerStatus.FAIR
in 10.0..14.99 -> TPSInfo.ServerStatus.POOR
else -> TPSInfo.ServerStatus.CRITICAL
}
return TPSInfo(dimensions, overall!!, status)
}
// 获取服务器状态表情符号
private fun getStatusEmoji(status: TPSInfo.ServerStatus): String {
return when (status) {
TPSInfo.ServerStatus.EXCELLENT -> "💚" // 绿色心形,优秀
TPSInfo.ServerStatus.GOOD -> "💛" // 黄色心形,良好
TPSInfo.ServerStatus.FAIR -> "🟡" // 黄色圆形,一般
TPSInfo.ServerStatus.POOR -> "🟠" // 橙色圆形,较差
TPSInfo.ServerStatus.CRITICAL -> "🔴" // 红色圆形,严重
}
}
// 获取服务器状态描述
private fun getStatusDescription(status: TPSInfo.ServerStatus): String {
return when (status) {
TPSInfo.ServerStatus.EXCELLENT -> "优秀"
TPSInfo.ServerStatus.GOOD -> "良好"
TPSInfo.ServerStatus.FAIR -> "一般"
TPSInfo.ServerStatus.POOR -> "较差"
TPSInfo.ServerStatus.CRITICAL -> "严重"
}
}
data class PlayerListInfo(
val onlineCount: Int,
val players: List<String>
)
// ---------------- 持久化部分 ----------------
@Serializable
data class ModuleState(var lastTriggeredRealId: Long, var lastTriggerTime: Long)
private fun saveState(realId: Long, time: Long) {
try {
val state = ModuleState(realId, time)
stateFile.writeText(Json.encodeToString(state))
LoggerUtil.logger.info("[$name] 已保存状态: lastTriggeredRealId=$realId, lastTriggerTime=$time")
} catch (e: Exception) {
LoggerUtil.logger.error("[$name] 保存状态失败", e)
}
}
private fun loadState(): ModuleState {
return try {
if (!stateFile.exists()) {
LoggerUtil.logger.info("[$name] 状态文件不存在,使用默认值")
return ModuleState(-1L, 0L)
}
val state = Json.decodeFromString<ModuleState>(stateFile.readText())
LoggerUtil.logger.info("[$name] 成功加载状态: lastTriggeredRealId=${state.lastTriggeredRealId}, lastTriggerTime=${state.lastTriggerTime}")
state
} catch (e: Exception) {
LoggerUtil.logger.warn("[$name] 读取状态失败,使用默认值", e)
ModuleState(-1L, 0L)
}
}
}

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat
@Target(AnnotationTarget.CLASS)
annotation class Developing()
annotation class Developing

View File

@ -1,134 +1,200 @@
package top.r3944realms.ltdmanager.napcat
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.websocket.*
import io.ktor.websocket.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
import top.r3944realms.ltdmanager.napcat.events.NapCatEvent
import top.r3944realms.ltdmanager.napcat.requests.NapCatRequest
import top.r3944realms.ltdmanager.napcat.requests.PrioritizedRequest
import top.r3944realms.ltdmanager.napcat.requests.PriorityMessageQueue
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.sync.withPermit
import top.r3944realms.ltdmanager.core.config.YamlConfigLoader
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
import top.r3944realms.ltdmanager.napcat.request.NapCatRequest
import top.r3944realms.ltdmanager.utils.Environment
import top.r3944realms.ltdmanager.utils.LoggerUtil
import java.util.*
import kotlin.collections.ArrayDeque
import kotlin.collections.isNotEmpty
import kotlin.time.Duration.Companion.seconds
class NapCatClient private constructor() : Closeable {
private val client = HttpClient(CIO)
private val httpConfig = YamlConfigLoader.loadHttpConfig()
private val token = httpConfig.decryptedToken
// 限流 (同时最多 3 个请求)
private val semaphore = Semaphore(3)
// 普通优先级队列
private val requestQueue = PriorityQueue<QueueItem>(compareBy { it.priority })
private val queueMutex = Mutex()
// 紧急队列 (先进先出,最多 10 个)
private val urgentQueue = ArrayDeque<QueueItem>(10)
class NapCatClient(private val wsUrl: String, private val token: String) {
private val client = HttpClient(CIO) { install(WebSockets) }
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val logger = LoggerFactory.getLogger(NapCatClient::class.java)
// 请求-响应匹配队列FIFO
private val pendingResponses = Channel<CompletableDeferred<NapCatEvent>>(capacity = Channel.UNLIMITED)
private val mutex = Mutex()
// 事件通道(用于非请求响应的消息)
// 优先级队列(按优先级发送请求)
private val priorityQueue = PriorityMessageQueue()
private val eventChannel = Channel<NapCatEvent>(capacity = Channel.UNLIMITED)
private val _connectionState = MutableStateFlow(false)
val connectionState = _connectionState.asStateFlow()
// 子协程引用
private var receiverJob: Job? = null
private var senderJob: Job? = null
suspend fun start() {
receiverJob = scope.launch { launchReceiver() }
senderJob = scope.launch { launchSender() }
}
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun launchReceiver() {
try {
client.wss(
host = wsUrl.removePrefix("ws://").substringBefore(':'),
port = wsUrl.substringAfterLast(':').toInt(),
path = "/"
) {
send(Frame.Text("""{"token":"$token"}"""))
_connectionState.value = true
while (true) {
when (val frame = incoming.receive()) {
is Frame.Text -> {
val event = Json.decodeFromString<NapCatEvent>(frame.readText())
// 尝试匹配最近的请求
if (!pendingResponses.isEmpty) {
pendingResponses.tryReceive().getOrNull()?.complete(event)
} else {
eventChannel.send(event) // 非请求响应的消息
}
}
is Frame.Close -> break
else -> {}
init {
scope.launch {
while (isActive) {
val item = queueMutex.withLock {
when {
urgentQueue.isNotEmpty() -> urgentQueue.removeFirst()
requestQueue.isNotEmpty() -> requestQueue.poll()
else -> null
}
}
}
} finally {
_connectionState.value = false
pendingResponses.close()
eventChannel.close()
priorityQueue.close()
}
}
private suspend fun launchSender() {
while (coroutineContext.isActive) {
try {
// 从优先级队列取出请求(自动按优先级排序)
val prioritized = priorityQueue.dequeue()
val request = prioritized.request
// 发送前注册响应监听器
val deferred = CompletableDeferred<NapCatEvent>()
mutex.withLock {
pendingResponses.send(deferred)
if (item == null) {
// 队列空 -> 挂起一小段时间等待新任务
delay(20)
continue
}
// 发送请求
client.webSocketSession(wsUrl).send(Frame.Text(Json.encodeToString(request)))
// 等待响应(超时由外层 sendRequest 控制)
deferred.await()
} catch (e: Exception) {
logger.error("发送请求失败", e)
delay(1000) // 错误时暂停1秒
processRequest(item)
}
}
}
/**
* 普通发送 (带优先级) 无返回事件版本
* 适用于只需要发送请求不关心返回结果的情况例如 SetGroupAddRequestRequest
*/
suspend fun sendUnit(
request: NapCatRequest,
retries: Int = 3,
priority: Int = 5
) {
checkRequest(request)
val deferred = CompletableDeferred<Unit>()
queueMutex.withLock {
requestQueue.add(QueueItem(request, deferred, retries, priority, expectsEvent = false))
}
deferred.await()
}
/**
* 发送带优先级的请求
* @param priority 优先级HIGH_PRIORITY/DEFAULT_PRIORITY/LOW_PRIORITY
* @param timeout 超时时间毫秒
* 紧急发送 (先进先出, 最多 10 ) 无返回事件版本
*/
suspend fun sendRequest(
@Throws(IllegalStateException::class)
suspend fun sendUrgentUnit(
request: NapCatRequest,
priority: Int = PrioritizedRequest.DEFAULT_PRIORITY,
timeout: Long = 5000
): NapCatEvent = withTimeout(timeout) {
val deferred = CompletableDeferred<NapCatEvent>()
// 将请求加入优先级队列
priorityQueue.enqueue(PrioritizedRequest(request, priority))
deferred.await() // 等待响应(由 launchSender 和 launchReceiver 协作完成)
retries: Int = 3
) {
checkRequest(request)
val deferred = CompletableDeferred<Unit>()
queueMutex.withLock {
if (urgentQueue.size >= 10) {
throw IllegalStateException("紧急任务队列已满 (最多 10 个)")
}
urgentQueue.addLast(QueueItem(request, deferred, retries, priority = Int.MIN_VALUE, expectsEvent = false))
}
deferred.await()
}
val incomingEvents: ReceiveChannel<NapCatEvent> = eventChannel
private fun cleanup() {
_connectionState.value = false
pendingResponses.close()
eventChannel.close()
priorityQueue.close()
/**
* 普通发送 (带优先级)
*/
suspend fun <T : NapCatEvent> send(
request: NapCatRequest,
retries: Int = 3,
priority: Int = 5
): T {
checkRequest(request)
val deferred = CompletableDeferred<T>()
queueMutex.withLock {
requestQueue.add(QueueItem(request, deferred, retries, priority, expectsEvent = true))
}
return deferred.await()
}
fun close() {
scope.cancel("NapCatClient closed")
cleanup()
/**
* 紧急发送 (先进先出, 最多 10 )
*/
@Throws(IllegalStateException::class)
suspend fun <T : NapCatEvent> sendUrgent(
request: NapCatRequest,
retries: Int = 3
): T {
checkRequest(request)
val deferred = CompletableDeferred<T>()
queueMutex.withLock {
if (urgentQueue.size >= 10) {
throw IllegalStateException("紧急任务队列已满 (最多 10 个)")
}
urgentQueue.addLast(QueueItem(request, deferred, retries, priority = Int.MIN_VALUE, expectsEvent = true))
}
return deferred.await()
}
}
private fun checkRequest(request: NapCatRequest) {
// 如果请求类标记为 @Developing则抛出异常
if (request::class.annotations.any { it.annotationClass == Developing::class }) {
throw UnsupportedOperationException(
"请求类 ${request::class.simpleName} 标记为 @Developing不支持发送"
)
}
}
private suspend fun processRequest(item: QueueItem) {
semaphore.withPermit {
val (request, deferred, retries, _, expectsEvent) = item
var attempt = 0
var lastError: Throwable? = null
while (attempt < retries) {
try {
val apiUrl = URLBuilder(httpConfig.url.toString()).apply {
encodedPath += request.path()
}.build()
if(!Environment.isProduction()) LoggerUtil.logger.debug("发送请求: ${request.toJSON()}")
val response = client.post(apiUrl) {
contentType(ContentType.Application.Json)
header("Authorization", "Bearer $token")
setBody(request.toJSON())
}
if (!response.status.isSuccess()) {
throw IllegalStateException("请求失败: HTTP ${response.status}")
}
if (response.contentType()?.match(ContentType.Application.Json) != true && expectsEvent) {
throw IllegalStateException("请求失败: 响应类型不是 JSON (${response.contentType()})")
}
val jsonText: String = response.body()
if(!Environment.isProduction()) LoggerUtil.logger.debug("响应: $jsonText")
if (expectsEvent) {
val event = NapCatEvent.decodeEvent(jsonText, request.type())
@Suppress("UNCHECKED_CAST")
(deferred as CompletableDeferred<NapCatEvent>).complete(event)
} else {
@Suppress("UNCHECKED_CAST")
(deferred as CompletableDeferred<Unit>).complete(Unit)
}
return
} catch (e: Exception) {
lastError = e
LoggerUtil.logger.warn("请求失败, 第 ${attempt + 1} 次: ${e.message}")
delay(((attempt + 1) * 2L).seconds) // 指数退避
}
attempt++
}
deferred.completeExceptionally(lastError ?: RuntimeException("未知错误"))
}
}
override fun close() {
scope.cancel()
runBlocking { client.close() }
}
companion object {
fun create(): NapCatClient = NapCatClient()
}
}

View File

@ -0,0 +1,14 @@
package top.r3944realms.ltdmanager.napcat
import kotlinx.coroutines.CompletableDeferred
import top.r3944realms.ltdmanager.napcat.request.NapCatRequest
data class QueueItem(
val request: NapCatRequest,
val deferred: CompletableDeferred<*>,
var retries: Int,
val priority: Int,
val expectsEvent: Boolean // true 表示返回 NapCatEvent, false 表示 Unit
) : Comparable<QueueItem> {
override fun compareTo(other: QueueItem): Int = priority.compareTo(other.priority)
}

View File

@ -6,15 +6,15 @@ import kotlinx.serialization.Serializable
@Serializable
data class Author (
@SerialName("groupId")
val groupID: String,
val groupId: String,
val groupName: String,
@SerialName("numId")
val numID: String,
val numId: String,
@SerialName("strId")
val strID: String,
val strId: String,
val type: Double,
val uid: String

View File

@ -6,14 +6,14 @@ import kotlinx.serialization.Serializable
@Serializable
data class CollectionItemList (
val author: Author,
val bid: Double,
val category: Double,
val bid: Long,
val category: Long,
val cid: String,
val collectTime: String,
val createTime: String,
@SerialName("customGroupId")
val customGroupID: Double,
val customGroupId: Double,
val modifyTime: String,
val securityBeat: Boolean,

View File

@ -1,13 +1,15 @@
package top.r3944realms.ltdmanager.napcat.data
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
/**
* 好友信息
*/
@Serializable
data class FriendInfo(
data class FriendInfo @OptIn(ExperimentalSerializationApi::class) constructor(
@SerialName("birthday_year")
val birthdayYear: Int,
@SerialName("birthday_month")
@ -15,15 +17,15 @@ data class FriendInfo(
@SerialName("birthday_day")
val birthdayDay: Int,
@SerialName("user_id")
val userId: Long,
val userId: Int,
val age: Int,
@SerialName("phone_number")
@JsonNames("phone_number", "phone_num")
val phoneNum: String,
val email: String,
@SerialName("category_id")
val categoryId: Int,
val nickname: String,
val remark: String,
val sex: String,
val sex: SexV2,
val level: Int
)

View File

@ -1,12 +1,19 @@
package top.r3944realms.ltdmanager.napcat.data
import kotlinx.serialization.Serializable
import top.r3944realms.ltdmanager.napcat.serializer.IDSerializer
/**
* ID
*/
@Serializable
@Serializable(with = IDSerializer::class)
sealed class ID {
class DoubleValue(val value: Double) : ID()
@Serializable
class LongValue(val value: Long) : ID()
@Serializable
class StringValue(val value: String) : ID()
companion object {
fun long(value: Long) = LongValue(value)
fun str(value: String) = StringValue(value)
}
}

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class MessageElement(
class MessageElement private constructor (
val type: MessageType,
val data: Message? = null
) {
@ -13,7 +13,7 @@ class MessageElement(
fun at(qq: ID, name: String?): MessageElement = MessageElement(MessageType.At, AtMessage(qq, name))
fun image(file: String, summary: String?): MessageElement = MessageElement(MessageType.Image, ImageMessage(file, summary))
fun json(json: String): MessageElement = MessageElement(MessageType.JSON, JSONMessage(json))
fun face(id: Int): MessageElement = MessageElement(MessageType.Face, FaceMessage(id))
fun face(id: Long): MessageElement = MessageElement(MessageType.Face, FaceMessage(id))
fun record(file: String): MessageElement = MessageElement(MessageType.Record, RecordMessage(file))
fun markdown(text: String): MessageElement = MessageElement(MessageType.Record, RecordMessage(text))
fun video(video: String): MessageElement = MessageElement(MessageType.Video, VideoMessage(video))
@ -29,7 +29,7 @@ class MessageElement(
}
@Serializable
abstract class Message
sealed class Message
/**
* 文本
@ -91,7 +91,7 @@ class MessageElement(
*/
@Serializable
data class FaceMessage(
val id: Int
val id: Long
) : Message()
/**

View File

@ -1,9 +0,0 @@
package top.r3944realms.ltdmanager.napcat.data
import kotlinx.serialization.Serializable
@Serializable
sealed class QQ {
class DoubleValue(val value: Double) : QQ()
class StringValue(val value: String) : QQ()
}

View File

@ -15,5 +15,5 @@ data class Sender (
val sex: SexV2? = null,
@SerialName("user_id")
val userID: Double
val userId: Long
)

View File

@ -0,0 +1,33 @@
package top.r3944realms.ltdmanager.napcat.event
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@Serializable
data class FailedRequestEvent(
val status: Status = Status.Failed,
val retcode: Int,
val data: JsonElement?= null,
val message: String,
val wording: String,
val echo: String? = null
): NapCatEvent() {
override fun type(): String {
return "FailedRequestEvent"
}
override fun subtype(): String {
return "FailedRequestEvent"
}
override fun isOk(): Boolean = false
companion object {
internal val json: Json by lazy {
Json {
ignoreUnknownKeys = true
}
}
}
}

View File

@ -0,0 +1,78 @@
package top.r3944realms.ltdmanager.napcat.event
import io.ktor.http.*
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import top.r3944realms.ltdmanager.napcat.event.account.AbstractAccountEvent
import top.r3944realms.ltdmanager.napcat.event.file.AbstractFileEvent
import top.r3944realms.ltdmanager.napcat.event.group.AbstractGroupEvent
import top.r3944realms.ltdmanager.napcat.event.message.AbstractMessageEvent
import top.r3944realms.ltdmanager.napcat.event.other.AbstractOtherEvent
import top.r3944realms.ltdmanager.napcat.event.passkey.AbstractPassKeyEvent
import top.r3944realms.ltdmanager.napcat.event.personal.AbstractPersonalEvent
import top.r3944realms.ltdmanager.napcat.event.system.AbstractSystemEvent
/**
* 基础NapCat事件类
* @property httpStatusCode HTTP状态码
* @property createTime 创建时间戳
*/
@Serializable
abstract class NapCatEvent(
@Transient
open val httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
@Transient
open val createTime: Long = System.currentTimeMillis()
) {
abstract fun type() :String
abstract fun subtype(): String
companion object {
private val eventTypeMap by lazy {
mutableMapOf<String, KSerializer<out NapCatEvent>>().apply {
putAll(AbstractAccountEvent.eventTypeMap)
putAll(AbstractFileEvent.eventTypeMap)
putAll(AbstractOtherEvent.eventTypeMap)
putAll(AbstractPersonalEvent.eventTypeMap)
putAll(AbstractPassKeyEvent.eventTypeMap)
putAll(AbstractGroupEvent.eventTypeMap)
putAll(AbstractSystemEvent.eventTypeMap)
putAll(AbstractMessageEvent.eventTypeMap)
}
}
private fun failedDecode(jsonString: String): FailedRequestEvent {
return FailedRequestEvent.json.decodeFromString(jsonString)
}
fun decodeEvent(jsonString: String, type: String): NapCatEvent {
return try {
eventTypeMap[type]?.let { serializer ->
val json = when {
type.startsWith("account/") -> AbstractAccountEvent.json
type.startsWith("file/") -> AbstractFileEvent.json
type.startsWith("group/") -> AbstractGroupEvent.json
type.startsWith("message/") -> AbstractMessageEvent.json
type.startsWith("passkey/") -> AbstractPassKeyEvent.json
type.startsWith("personal/") -> AbstractPersonalEvent.json
type.startsWith("system/") -> AbstractSystemEvent.json
type.startsWith("other/") -> AbstractOtherEvent.json
else -> Json { ignoreUnknownKeys = true }
}
json.decodeFromString(serializer, jsonString)
} ?: failedDecode(jsonString) // 找不到类型,直接 fallback
} catch (e: Exception) {
// 解码失败fallback
failedDecode(jsonString)
}
}
}
open fun isOk():Boolean = true
@Serializable
enum class Status(val value: String) {
@SerialName("ok") Ok("ok"),
@SerialName("failed") Failed("failed"),;
companion object {
fun isOk(value: Status): Boolean = value == Ok
}
}
}

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
@ -6,7 +6,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import top.r3944realms.ltdmanager.napcat.events.NapCatEvent
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
/**
* QQ 账户相关响应抽象

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,9 +1,8 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonArray
/**
* FetchCustomFace事件
@ -22,7 +21,7 @@ data class FetchCustomFaceEvent(
@Transient
val echo0: String? = null,
val data: JsonArray
val data: List<String>
) : AbstractAccountEvent(status0, retcode0, message0, wording0, echo0) {
override fun subtype(): String {

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,9 +1,9 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonArray
import top.r3944realms.ltdmanager.napcat.data.FriendInfo
/**
* GetFriendList事件
@ -22,7 +22,7 @@ data class GetFriendListEvent(
@Transient
val echo0: String? = null,
val data: JsonArray
val data: List<FriendInfo>
) : AbstractAccountEvent(status0, retcode0, message0, wording0, echo0) {
override fun subtype(): String {

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,11 +1,11 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.napcat.data.ID
import top.r3944realms.ltdmanager.napcat.data.MessageType
import top.r3944realms.ltdmanager.napcat.data.QQ
import top.r3944realms.ltdmanager.napcat.data.Sender
/**
@ -72,7 +72,7 @@ data class GetRecentContactEvent(
val font: Double,
@SerialName("group_id")
val groupID: Double? = null,
val groupId: Double? = null,
val message: List<TextMsg>,
@ -92,7 +92,7 @@ data class GetRecentContactEvent(
val realSeq: String,
@SerialName("self_id")
val selfID: Double,
val selfId: Double,
val sender: Sender,
@ -102,7 +102,7 @@ data class GetRecentContactEvent(
val time: Double,
@SerialName("user_id")
val userID: Double
val userId: Double
)
/**
@ -113,7 +113,7 @@ data class GetRecentContactEvent(
val font: Double,
@SerialName("group_id")
val groupID: Double? = null,
val groupId: Double? = null,
val message: List<TextMsg>,
@ -136,13 +136,13 @@ data class GetRecentContactEvent(
val rawMessage: String,
@SerialName("real_id")
val realID: Double,
val realId: Double,
@SerialName("real_seq")
val realSeq: String,
@SerialName("self_id")
val selfID: Double,
val selfId: Double,
val sender: Sender,
@ -152,7 +152,7 @@ data class GetRecentContactEvent(
val time: Double,
@SerialName("user_id")
val userID: Double
val userId: Double
)
@Serializable
@ -165,8 +165,8 @@ data class GetRecentContactEvent(
data class Data (
val text: String? = null,
val name: String? = null,
val qq: QQ? = null,
val id: QQ? = null,
val qq: ID? = null,
val id: ID? = null,
val file: String? = null,
/**

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -88,7 +88,7 @@ data class GetStrangerInfoEvent(
val uin: String,
@SerialName("user_id")
val userID: Double,
val userId: Double,
/**
* 会员等级

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.account
package top.r3944realms.ltdmanager.napcat.event.account
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
@ -7,7 +7,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import top.r3944realms.ltdmanager.napcat.events.NapCatEvent
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
/**
* QQ 文件相关响应抽象
@ -62,7 +62,7 @@ abstract class AbstractFileEvent(
val downloadTimes: Double,
@SerialName("file_id")
val fileID: String,
val fileId: String,
@SerialName("file_name")
val fileName: String,
@ -71,7 +71,7 @@ abstract class AbstractFileEvent(
val fileSize: Double,
@SerialName("group_id")
val groupID: Double,
val groupId: Double,
@SerialName("modify_time")
val modifyTime: Double,
@ -121,7 +121,7 @@ abstract class AbstractFileEvent(
val folderName: String,
@SerialName("group_id")
val groupID: Double,
val groupId: Double,
/**
* 文件数量

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,9 +1,9 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.napcat.events.file.GetGroupRootFilesEvent.FileData
import top.r3944realms.ltdmanager.napcat.event.file.GetGroupRootFilesEvent.FileData
/**
* GetGroupFilesByFolder事件

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.file
package top.r3944realms.ltdmanager.napcat.event.file
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,4 +1,4 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
@ -6,7 +6,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import top.r3944realms.ltdmanager.napcat.events.NapCatEvent
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
/**
* QQ 群聊相关响应抽象

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,7 +1,6 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonObject

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -30,7 +30,7 @@ data class GetGroupDetailInfoEvent(
val groupAllShut: Double,
@SerialName("group_id")
val groupID: Double,
val groupId: Double,
@SerialName("group_name")
val groupName: String,

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -39,7 +39,7 @@ data class GetGroupHonorInfoEvent(
val emotionList: List<GroupHonorInfo>,
@SerialName("group_id")
val groupID: String,
val groupId: String,
/**
* 龙王
@ -83,7 +83,7 @@ data class GetGroupHonorInfoEvent(
val nickname: String? = null,
@SerialName("user_id")
val userID: Double? = null
val userId: Double? = null
)
override fun subtype(): String {
return "get_group_honor_info"

View File

@ -1,9 +1,10 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupIgnoredNotifiesRequest
import top.r3944realms.ltdmanager.napcat.request.group.GetGroupSystemMsgRequest
/**
* GetGroupIgnoredNotifies事件

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -38,11 +38,11 @@ data class GetGroupSystemMsgEvent(
*/
@Serializable
data class SystemInfo (
val actor: Double,
val actor: Long,
val checked: Boolean,
@SerialName("group_id")
val groupID: Double,
val groupId: Long,
@SerialName("group_name")
val groupName: String,
@ -51,12 +51,12 @@ data class GetGroupSystemMsgEvent(
val invitorNick: String,
@SerialName("invitor_uin")
val invitorUin: Double,
val invitorUin: Long,
val message: String,
@SerialName("request_id")
val requestID: Double,
val requestId: Long,
@SerialName("requester_nick")
val requesterNick: String

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

View File

@ -1,10 +1,8 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonElement
import top.r3944realms.ltdmanager.napcat.events.NapCatEvent
import top.r3944realms.ltdmanager.napcat.event.NapCatEvent
/**
* SendGroupSign事件

View File

@ -1,5 +1,5 @@
package top.r3944realms.ltdmanager.napcat.events.group
package top.r3944realms.ltdmanager.napcat.event.group
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

Some files were not shown because too many files have changed in this diff Show More