koishi-plugin-bind-bot 2.0.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +35 -0
- package/lib/handlers/base.handler.d.ts +16 -1
- package/lib/handlers/mcid.handler.d.ts +3 -39
- package/lib/handlers/mcid.handler.js +147 -150
- package/lib/handlers/whitelist.handler.d.ts +0 -4
- package/lib/handlers/whitelist.handler.js +9 -34
- package/lib/index.js +126 -162
- package/lib/repositories/mcidbind.repository.d.ts +13 -1
- package/lib/repositories/mcidbind.repository.js +50 -1
- package/lib/types/database.d.ts +2 -0
- package/lib/utils/error-utils.d.ts +30 -0
- package/lib/utils/error-utils.js +111 -0
- package/lib/utils/helpers.d.ts +33 -0
- package/lib/utils/helpers.js +87 -0
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WhitelistHandler = void 0;
|
|
4
4
|
const koishi_1 = require("koishi");
|
|
5
5
|
const base_handler_1 = require("./base.handler");
|
|
6
|
+
const helpers_1 = require("../utils/helpers");
|
|
6
7
|
/**
|
|
7
8
|
* 白名单命令处理器
|
|
8
9
|
* 处理所有 mcid.whitelist 子命令
|
|
@@ -756,38 +757,8 @@ class WhitelistHandler extends base_handler_1.BaseHandler {
|
|
|
756
757
|
if (b.includes(a)) {
|
|
757
758
|
return a.length / b.length;
|
|
758
759
|
}
|
|
759
|
-
//
|
|
760
|
-
|
|
761
|
-
const editDistance = this.levenshteinDistance(a, b);
|
|
762
|
-
return 1 - (editDistance / maxLength);
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* 私有辅助方法:计算Levenshtein距离
|
|
766
|
-
*/
|
|
767
|
-
levenshteinDistance(a, b) {
|
|
768
|
-
const matrix = [];
|
|
769
|
-
// 初始化矩阵
|
|
770
|
-
for (let i = 0; i <= b.length; i++) {
|
|
771
|
-
matrix[i] = [i];
|
|
772
|
-
}
|
|
773
|
-
for (let j = 0; j <= a.length; j++) {
|
|
774
|
-
matrix[0][j] = j;
|
|
775
|
-
}
|
|
776
|
-
// 填充矩阵
|
|
777
|
-
for (let i = 1; i <= b.length; i++) {
|
|
778
|
-
for (let j = 1; j <= a.length; j++) {
|
|
779
|
-
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
780
|
-
matrix[i][j] = matrix[i - 1][j - 1];
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // 替换
|
|
784
|
-
matrix[i][j - 1] + 1, // 插入
|
|
785
|
-
matrix[i - 1][j] + 1 // 删除
|
|
786
|
-
);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
return matrix[b.length][a.length];
|
|
760
|
+
// 否则使用工具函数计算Levenshtein距离的相似度
|
|
761
|
+
return (0, helpers_1.calculateSimilarity)(a, b);
|
|
791
762
|
}
|
|
792
763
|
/**
|
|
793
764
|
* 私有辅助方法:检查用户是否在特定服务器的白名单中
|
|
@@ -808,11 +779,13 @@ class WhitelistHandler extends base_handler_1.BaseHandler {
|
|
|
808
779
|
return false;
|
|
809
780
|
}
|
|
810
781
|
// 重新获取最新的用户绑定信息
|
|
811
|
-
|
|
782
|
+
let freshBind = await this.deps.getBindInfo(mcBind.qqId);
|
|
812
783
|
if (!freshBind || !freshBind.mcUsername) {
|
|
813
784
|
this.logger.warn('白名单', `用户QQ(${mcBind.qqId})可能在操作过程中解绑了MC账号`);
|
|
814
785
|
return false;
|
|
815
786
|
}
|
|
787
|
+
// 智能检测用户名变更(带缓存,避免频繁API调用)
|
|
788
|
+
freshBind = await this.deps.checkAndUpdateUsernameWithCache(freshBind);
|
|
816
789
|
// 检查最新状态是否已在白名单中
|
|
817
790
|
if (freshBind.whitelist && freshBind.whitelist.includes(server.id)) {
|
|
818
791
|
this.logger.info('白名单', `用户QQ(${mcBind.qqId})已在服务器${server.name}的白名单中`);
|
|
@@ -889,11 +862,13 @@ class WhitelistHandler extends base_handler_1.BaseHandler {
|
|
|
889
862
|
return false;
|
|
890
863
|
}
|
|
891
864
|
// 重新获取最新的用户绑定信息
|
|
892
|
-
|
|
865
|
+
let freshBind = await this.deps.getBindInfo(mcBind.qqId);
|
|
893
866
|
if (!freshBind || !freshBind.mcUsername) {
|
|
894
867
|
this.logger.warn('白名单', `用户QQ(${mcBind.qqId})可能在操作过程中解绑了MC账号`);
|
|
895
868
|
return false;
|
|
896
869
|
}
|
|
870
|
+
// 智能检测用户名变更(带缓存,避免频繁API调用)
|
|
871
|
+
freshBind = await this.deps.checkAndUpdateUsernameWithCache(freshBind);
|
|
897
872
|
// 检查最新状态是否在白名单中
|
|
898
873
|
if (!freshBind.whitelist || !freshBind.whitelist.includes(server.id)) {
|
|
899
874
|
this.logger.info('白名单', `用户QQ(${mcBind.qqId})不在服务器${server.name}的白名单中`);
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,8 @@ const export_utils_1 = require("./export-utils");
|
|
|
12
12
|
const logger_1 = require("./utils/logger");
|
|
13
13
|
const rcon_manager_1 = require("./managers/rcon-manager");
|
|
14
14
|
const rate_limiter_1 = require("./utils/rate-limiter");
|
|
15
|
+
const helpers_1 = require("./utils/helpers");
|
|
16
|
+
const error_utils_1 = require("./utils/error-utils");
|
|
15
17
|
const mcidbind_repository_1 = require("./repositories/mcidbind.repository");
|
|
16
18
|
const schedule_mute_repository_1 = require("./repositories/schedule-mute.repository");
|
|
17
19
|
const handlers_1 = require("./handlers");
|
|
@@ -911,88 +913,6 @@ function apply(ctx, config) {
|
|
|
911
913
|
return extractedId;
|
|
912
914
|
};
|
|
913
915
|
// 获取用户友好的错误信息
|
|
914
|
-
const getFriendlyErrorMessage = (error) => {
|
|
915
|
-
const errorMsg = error instanceof Error ? error.message : error;
|
|
916
|
-
// 拆分错误信息
|
|
917
|
-
const userError = getUserFacingErrorMessage(errorMsg);
|
|
918
|
-
// 将警告级别错误标记出来
|
|
919
|
-
if (isWarningError(userError)) {
|
|
920
|
-
return `⚠️ ${userError}`;
|
|
921
|
-
}
|
|
922
|
-
// 将严重错误标记出来
|
|
923
|
-
if (isCriticalError(userError)) {
|
|
924
|
-
return `❌ ${userError}`;
|
|
925
|
-
}
|
|
926
|
-
return userError;
|
|
927
|
-
};
|
|
928
|
-
// 提取用户友好的错误信息
|
|
929
|
-
const getUserFacingErrorMessage = (errorMsg) => {
|
|
930
|
-
// Mojang API相关错误
|
|
931
|
-
if (errorMsg.includes('ECONNABORTED') || errorMsg.includes('timeout')) {
|
|
932
|
-
return '无法连接到Mojang服务器,请稍后再试';
|
|
933
|
-
}
|
|
934
|
-
if (errorMsg.includes('404')) {
|
|
935
|
-
return '该Minecraft用户名不存在';
|
|
936
|
-
}
|
|
937
|
-
if (errorMsg.includes('network') || errorMsg.includes('connect')) {
|
|
938
|
-
return '网络连接异常,请稍后再试';
|
|
939
|
-
}
|
|
940
|
-
// 数据库相关错误
|
|
941
|
-
if (errorMsg.includes('unique') || errorMsg.includes('duplicate')) {
|
|
942
|
-
return '该Minecraft用户名已被其他用户绑定';
|
|
943
|
-
}
|
|
944
|
-
// RCON相关错误
|
|
945
|
-
if (errorMsg.includes('RCON') || errorMsg.includes('服务器')) {
|
|
946
|
-
if (errorMsg.includes('authentication') || errorMsg.includes('auth') || errorMsg.includes('认证')) {
|
|
947
|
-
return 'RCON认证失败,服务器拒绝访问,请联系管理员检查密码';
|
|
948
|
-
}
|
|
949
|
-
if (errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ETIMEDOUT') || errorMsg.includes('无法连接')) {
|
|
950
|
-
return '无法连接到游戏服务器,请确认服务器是否在线或联系管理员';
|
|
951
|
-
}
|
|
952
|
-
if (errorMsg.includes('command') || errorMsg.includes('执行命令')) {
|
|
953
|
-
return '服务器命令执行失败,请稍后再试';
|
|
954
|
-
}
|
|
955
|
-
return '与游戏服务器通信失败,请稍后再试';
|
|
956
|
-
}
|
|
957
|
-
// 用户名相关错误
|
|
958
|
-
if (errorMsg.includes('用户名') || errorMsg.includes('username')) {
|
|
959
|
-
if (errorMsg.includes('不存在')) {
|
|
960
|
-
return '该Minecraft用户名不存在,请检查拼写';
|
|
961
|
-
}
|
|
962
|
-
if (errorMsg.includes('已被')) {
|
|
963
|
-
return '该Minecraft用户名已被其他用户绑定,请使用其他用户名';
|
|
964
|
-
}
|
|
965
|
-
if (errorMsg.includes('格式')) {
|
|
966
|
-
return 'Minecraft用户名格式不正确,应为3-16位字母、数字和下划线';
|
|
967
|
-
}
|
|
968
|
-
return '用户名验证失败,请检查用户名并重试';
|
|
969
|
-
}
|
|
970
|
-
// 默认错误信息
|
|
971
|
-
return '操作失败,请稍后再试';
|
|
972
|
-
};
|
|
973
|
-
// 判断是否为警告级别错误(用户可能输入有误)
|
|
974
|
-
const isWarningError = (errorMsg) => {
|
|
975
|
-
const warningPatterns = [
|
|
976
|
-
'用户名不存在',
|
|
977
|
-
'格式不正确',
|
|
978
|
-
'已被其他用户绑定',
|
|
979
|
-
'已在白名单中',
|
|
980
|
-
'不在白名单中',
|
|
981
|
-
'未绑定MC账号',
|
|
982
|
-
'冷却期内'
|
|
983
|
-
];
|
|
984
|
-
return warningPatterns.some(pattern => errorMsg.includes(pattern));
|
|
985
|
-
};
|
|
986
|
-
// 判断是否为严重错误(系统问题)
|
|
987
|
-
const isCriticalError = (errorMsg) => {
|
|
988
|
-
const criticalPatterns = [
|
|
989
|
-
'无法连接',
|
|
990
|
-
'RCON认证失败',
|
|
991
|
-
'服务器通信失败',
|
|
992
|
-
'数据库操作出错'
|
|
993
|
-
];
|
|
994
|
-
return criticalPatterns.some(pattern => errorMsg.includes(pattern));
|
|
995
|
-
};
|
|
996
916
|
// 封装发送消息的函数,处理私聊和群聊的不同格式
|
|
997
917
|
const sendMessage = async (session, content, options) => {
|
|
998
918
|
try {
|
|
@@ -1256,8 +1176,8 @@ function apply(ctx, config) {
|
|
|
1256
1176
|
return false;
|
|
1257
1177
|
}
|
|
1258
1178
|
};
|
|
1259
|
-
// 检查MC用户名是否已被其他QQ
|
|
1260
|
-
const checkUsernameExists = async (username, currentUserId) => {
|
|
1179
|
+
// 检查MC用户名是否已被其他QQ号绑定(支持不区分大小写和UUID检查)
|
|
1180
|
+
const checkUsernameExists = async (username, currentUserId, uuid) => {
|
|
1261
1181
|
try {
|
|
1262
1182
|
// 验证输入参数
|
|
1263
1183
|
if (!username) {
|
|
@@ -1268,8 +1188,8 @@ function apply(ctx, config) {
|
|
|
1268
1188
|
if (username.startsWith('_temp_')) {
|
|
1269
1189
|
return false;
|
|
1270
1190
|
}
|
|
1271
|
-
//
|
|
1272
|
-
const bind = await
|
|
1191
|
+
// 使用不区分大小写的查询
|
|
1192
|
+
const bind = await mcidbindRepo.findByUsernameIgnoreCase(username);
|
|
1273
1193
|
// 如果没有绑定,返回false
|
|
1274
1194
|
if (!bind)
|
|
1275
1195
|
return false;
|
|
@@ -1277,6 +1197,16 @@ function apply(ctx, config) {
|
|
|
1277
1197
|
if (bind.mcUsername && bind.mcUsername.startsWith('_temp_')) {
|
|
1278
1198
|
return false;
|
|
1279
1199
|
}
|
|
1200
|
+
// 如果提供了 UUID,检查是否为同一个 MC 账号(用户改名场景)
|
|
1201
|
+
if (uuid && bind.mcUuid) {
|
|
1202
|
+
const cleanUuid = uuid.replace(/-/g, '');
|
|
1203
|
+
const bindCleanUuid = bind.mcUuid.replace(/-/g, '');
|
|
1204
|
+
if (cleanUuid === bindCleanUuid) {
|
|
1205
|
+
// 同一个 UUID,说明是用户改名,允许绑定
|
|
1206
|
+
logger.info(`[绑定检查] 检测到MC账号改名: UUID=${uuid}, 旧用户名=${bind.mcUsername}, 新用户名=${username}`);
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1280
1210
|
// 如果提供了当前用户ID,需要排除当前用户
|
|
1281
1211
|
if (currentUserId) {
|
|
1282
1212
|
const normalizedCurrentId = normalizeQQId(currentUserId);
|
|
@@ -1727,8 +1657,10 @@ function apply(ctx, config) {
|
|
|
1727
1657
|
logger.warn(`[用户名更新] 无法获取UUID "${bind.mcUuid}" 的最新用户名`);
|
|
1728
1658
|
return bind;
|
|
1729
1659
|
}
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1660
|
+
// 如果用户名与数据库中的不同,更新数据库(使用规范化比较,不区分大小写)
|
|
1661
|
+
const normalizedLatest = (0, helpers_1.normalizeUsername)(latestUsername);
|
|
1662
|
+
const normalizedCurrent = (0, helpers_1.normalizeUsername)(bind.mcUsername);
|
|
1663
|
+
if (normalizedLatest !== normalizedCurrent) {
|
|
1732
1664
|
logger.info(`[用户名更新] 用户 QQ(${bind.qqId}) 的Minecraft用户名已变更: ${bind.mcUsername} -> ${latestUsername}`);
|
|
1733
1665
|
// 更新数据库中的用户名
|
|
1734
1666
|
await ctx.database.set('mcidbind', { qqId: bind.qqId }, {
|
|
@@ -1744,6 +1676,88 @@ function apply(ctx, config) {
|
|
|
1744
1676
|
return bind;
|
|
1745
1677
|
}
|
|
1746
1678
|
};
|
|
1679
|
+
/**
|
|
1680
|
+
* 智能缓存版本的改名检测函数
|
|
1681
|
+
* 特性:
|
|
1682
|
+
* - 24小时冷却期(失败>=3次时延长到72小时)
|
|
1683
|
+
* - 失败计数追踪
|
|
1684
|
+
* - 成功时重置失败计数
|
|
1685
|
+
* @param bind 绑定记录
|
|
1686
|
+
* @returns 更新后的绑定记录
|
|
1687
|
+
*/
|
|
1688
|
+
const checkAndUpdateUsernameWithCache = async (bind) => {
|
|
1689
|
+
try {
|
|
1690
|
+
if (!bind || !bind.mcUuid) {
|
|
1691
|
+
logger.warn(`[改名检测缓存] 无法检查用户名更新: 空绑定或空UUID`);
|
|
1692
|
+
return bind;
|
|
1693
|
+
}
|
|
1694
|
+
const now = new Date();
|
|
1695
|
+
const failCount = bind.usernameCheckFailCount || 0;
|
|
1696
|
+
// 根据失败次数决定冷却期:普通24小时,失败>=3次则72小时
|
|
1697
|
+
const cooldownHours = failCount >= 3 ? 72 : 24;
|
|
1698
|
+
// 检查是否在冷却期内
|
|
1699
|
+
if (bind.usernameLastChecked) {
|
|
1700
|
+
const lastCheck = new Date(bind.usernameLastChecked);
|
|
1701
|
+
const hoursSinceCheck = (now.getTime() - lastCheck.getTime()) / (1000 * 60 * 60);
|
|
1702
|
+
if (hoursSinceCheck < cooldownHours) {
|
|
1703
|
+
logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 在冷却期内(${hoursSinceCheck.toFixed(1)}h/${cooldownHours}h),跳过检查`);
|
|
1704
|
+
return bind;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 开始检查用户名变更(失败计数: ${failCount})`);
|
|
1708
|
+
// 执行实际的改名检测
|
|
1709
|
+
const oldUsername = bind.mcUsername;
|
|
1710
|
+
const updatedBind = await checkAndUpdateUsername(bind);
|
|
1711
|
+
// 判断检测是否成功
|
|
1712
|
+
const detectionSuccess = updatedBind.mcUsername !== null && updatedBind.mcUsername !== undefined;
|
|
1713
|
+
if (detectionSuccess) {
|
|
1714
|
+
// 检测成功
|
|
1715
|
+
const usernameChanged = (0, helpers_1.normalizeUsername)(updatedBind.mcUsername) !== (0, helpers_1.normalizeUsername)(oldUsername);
|
|
1716
|
+
// 更新检查时间和重置失败计数
|
|
1717
|
+
await mcidbindRepo.update(bind.qqId, {
|
|
1718
|
+
usernameLastChecked: now,
|
|
1719
|
+
usernameCheckFailCount: 0
|
|
1720
|
+
});
|
|
1721
|
+
if (usernameChanged) {
|
|
1722
|
+
logger.info(`[改名检测缓存] QQ(${bind.qqId}) 用户名已变更: ${oldUsername} -> ${updatedBind.mcUsername}`);
|
|
1723
|
+
}
|
|
1724
|
+
else {
|
|
1725
|
+
logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 用户名无变更: ${updatedBind.mcUsername}`);
|
|
1726
|
+
}
|
|
1727
|
+
// 更新返回对象的缓存字段
|
|
1728
|
+
updatedBind.usernameLastChecked = now;
|
|
1729
|
+
updatedBind.usernameCheckFailCount = 0;
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
// 检测失败��API失败或返回null)
|
|
1733
|
+
const newFailCount = failCount + 1;
|
|
1734
|
+
await mcidbindRepo.update(bind.qqId, {
|
|
1735
|
+
usernameLastChecked: now,
|
|
1736
|
+
usernameCheckFailCount: newFailCount
|
|
1737
|
+
});
|
|
1738
|
+
logger.warn(`[改名检测缓存] QQ(${bind.qqId}) 检测失败,失败计数: ${newFailCount}`);
|
|
1739
|
+
// 更新返回对象的缓存字段
|
|
1740
|
+
updatedBind.usernameLastChecked = now;
|
|
1741
|
+
updatedBind.usernameCheckFailCount = newFailCount;
|
|
1742
|
+
}
|
|
1743
|
+
return updatedBind;
|
|
1744
|
+
}
|
|
1745
|
+
catch (error) {
|
|
1746
|
+
logger.error(`[改名检测缓存] 检查和更新用户名失败: ${error.message}`);
|
|
1747
|
+
// 失败时也更新检查时间和递增失败计数
|
|
1748
|
+
try {
|
|
1749
|
+
const failCount = bind.usernameCheckFailCount || 0;
|
|
1750
|
+
await mcidbindRepo.update(bind.qqId, {
|
|
1751
|
+
usernameLastChecked: new Date(),
|
|
1752
|
+
usernameCheckFailCount: failCount + 1
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
catch (updateError) {
|
|
1756
|
+
logger.error(`[改名检测缓存] 更新失败计数时出错: ${updateError.message}`);
|
|
1757
|
+
}
|
|
1758
|
+
return bind;
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1747
1761
|
// 安全地替换命令模板
|
|
1748
1762
|
const safeCommandReplace = (template, mcid) => {
|
|
1749
1763
|
// 过滤可能导致命令注入的字符
|
|
@@ -1773,41 +1787,11 @@ function apply(ctx, config) {
|
|
|
1773
1787
|
const lowerServerName = serverName.toLowerCase().trim();
|
|
1774
1788
|
// 最小相似度阈值,低于此值的匹配结果将被忽略
|
|
1775
1789
|
const MIN_SIMILARITY = 0.6; // 60%的相似度
|
|
1776
|
-
// 计算Levenshtein距离的函数
|
|
1777
|
-
const levenshteinDistance = (str1, str2) => {
|
|
1778
|
-
const matrix = [];
|
|
1779
|
-
for (let i = 0; i <= str2.length; i++) {
|
|
1780
|
-
matrix[i] = [i];
|
|
1781
|
-
}
|
|
1782
|
-
for (let j = 0; j <= str1.length; j++) {
|
|
1783
|
-
matrix[0][j] = j;
|
|
1784
|
-
}
|
|
1785
|
-
for (let i = 1; i <= str2.length; i++) {
|
|
1786
|
-
for (let j = 1; j <= str1.length; j++) {
|
|
1787
|
-
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
1788
|
-
matrix[i][j] = matrix[i - 1][j - 1];
|
|
1789
|
-
}
|
|
1790
|
-
else {
|
|
1791
|
-
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // 替换
|
|
1792
|
-
matrix[i][j - 1] + 1, // 插入
|
|
1793
|
-
matrix[i - 1][j] + 1 // 删除
|
|
1794
|
-
);
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
return matrix[str2.length][str1.length];
|
|
1799
|
-
};
|
|
1800
|
-
// 计算相似度(0到1之间,1表示完全相同)
|
|
1801
|
-
const calculateSimilarity = (str1, str2) => {
|
|
1802
|
-
const distance = levenshteinDistance(str1, str2);
|
|
1803
|
-
const maxLength = Math.max(str1.length, str2.length);
|
|
1804
|
-
return 1 - distance / maxLength;
|
|
1805
|
-
};
|
|
1806
1790
|
// 查找最相似的服务器名称
|
|
1807
1791
|
let bestMatch = null;
|
|
1808
1792
|
let bestSimilarity = 0;
|
|
1809
1793
|
for (const s of enabledServers) {
|
|
1810
|
-
const similarity = calculateSimilarity(lowerServerName, s.name.toLowerCase().trim());
|
|
1794
|
+
const similarity = (0, helpers_1.calculateSimilarity)(lowerServerName, s.name.toLowerCase().trim());
|
|
1811
1795
|
if (similarity > bestSimilarity && similarity >= MIN_SIMILARITY) {
|
|
1812
1796
|
bestSimilarity = similarity;
|
|
1813
1797
|
bestMatch = s;
|
|
@@ -1861,10 +1845,30 @@ function apply(ctx, config) {
|
|
|
1861
1845
|
checkCooldown,
|
|
1862
1846
|
getCrafatarUrl,
|
|
1863
1847
|
getStarlightSkinUrl,
|
|
1848
|
+
// Database operations (for McidCommandHandler)
|
|
1849
|
+
getMcBindByQQId,
|
|
1850
|
+
getMcBindByUsername,
|
|
1851
|
+
createOrUpdateMcBind,
|
|
1852
|
+
deleteMcBind,
|
|
1853
|
+
checkUsernameExists,
|
|
1854
|
+
checkAndUpdateUsername,
|
|
1855
|
+
checkAndUpdateUsernameWithCache,
|
|
1856
|
+
// API operations
|
|
1857
|
+
validateUsername,
|
|
1858
|
+
validateBUID,
|
|
1859
|
+
updateBuidInfoOnly,
|
|
1860
|
+
// Permission check functions
|
|
1861
|
+
isAdmin,
|
|
1862
|
+
isMaster,
|
|
1864
1863
|
// Business functions
|
|
1865
1864
|
sendMessage,
|
|
1866
1865
|
autoSetGroupNickname,
|
|
1866
|
+
checkNicknameFormat,
|
|
1867
1867
|
getBindInfo: getMcBindByQQId,
|
|
1868
|
+
// Config operations
|
|
1869
|
+
getServerConfigById,
|
|
1870
|
+
// Error handling
|
|
1871
|
+
getFriendlyErrorMessage: error_utils_1.getFriendlyErrorMessage,
|
|
1868
1872
|
// Service instances
|
|
1869
1873
|
rconManager,
|
|
1870
1874
|
messageUtils,
|
|
@@ -1889,49 +1893,9 @@ function apply(ctx, config) {
|
|
|
1889
1893
|
tagHandler.register();
|
|
1890
1894
|
whitelistHandler.register();
|
|
1891
1895
|
buidHandler.register();
|
|
1892
|
-
// =========== MC命令组 ===========
|
|
1893
|
-
const cmd = ctx.command('mcid', 'Minecraft 账号绑定管理');
|
|
1894
|
-
// 创建McidHandler的依赖对象
|
|
1895
|
-
const mcidHandlerDeps = {
|
|
1896
|
-
config,
|
|
1897
|
-
logger: loggerService,
|
|
1898
|
-
mcidbindRepo,
|
|
1899
|
-
groupExporter,
|
|
1900
|
-
// 辅助函数依赖
|
|
1901
|
-
normalizeQQId,
|
|
1902
|
-
formatCommand,
|
|
1903
|
-
formatUuid,
|
|
1904
|
-
checkCooldown,
|
|
1905
|
-
getCrafatarUrl,
|
|
1906
|
-
getStarlightSkinUrl,
|
|
1907
|
-
// 数据库操作
|
|
1908
|
-
getMcBindByQQId,
|
|
1909
|
-
getMcBindByUsername,
|
|
1910
|
-
createOrUpdateMcBind,
|
|
1911
|
-
deleteMcBind,
|
|
1912
|
-
checkUsernameExists,
|
|
1913
|
-
checkAndUpdateUsername,
|
|
1914
|
-
// API操作
|
|
1915
|
-
validateUsername,
|
|
1916
|
-
validateBUID,
|
|
1917
|
-
updateBuidInfoOnly,
|
|
1918
|
-
// 权限检查
|
|
1919
|
-
isAdmin,
|
|
1920
|
-
isMaster,
|
|
1921
|
-
// 消息操作
|
|
1922
|
-
sendMessage,
|
|
1923
|
-
autoSetGroupNickname,
|
|
1924
|
-
checkNicknameFormat,
|
|
1925
|
-
// 会话管理
|
|
1926
|
-
removeBindingSession,
|
|
1927
|
-
// 服务器配置
|
|
1928
|
-
getServerConfigById,
|
|
1929
|
-
// 工具函数
|
|
1930
|
-
getFriendlyErrorMessage
|
|
1931
|
-
};
|
|
1932
1896
|
// 实例化McidCommandHandler并注册命令
|
|
1933
|
-
const mcidHandler = new handlers_1.McidCommandHandler(ctx,
|
|
1934
|
-
mcidHandler.
|
|
1897
|
+
const mcidHandler = new handlers_1.McidCommandHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1898
|
+
mcidHandler.register();
|
|
1935
1899
|
// 自定义文本前缀匹配
|
|
1936
1900
|
if (config.allowTextPrefix && config.botNickname) {
|
|
1937
1901
|
// 创建一个前缀匹配器
|
|
@@ -2303,7 +2267,7 @@ function apply(ctx, config) {
|
|
|
2303
2267
|
}
|
|
2304
2268
|
}
|
|
2305
2269
|
// 检查用户名是否已被其他人绑定
|
|
2306
|
-
if (await checkUsernameExists(username, session.userId)) {
|
|
2270
|
+
if (await checkUsernameExists(username, session.userId, uuid)) {
|
|
2307
2271
|
logger.warn(`[交互绑定] MC用户名"${username}"已被其他用户绑定`);
|
|
2308
2272
|
await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${username} 已被其他用户绑定\n\n请输入其他MC用户名或发送"跳过"完成绑定`)]);
|
|
2309
2273
|
return;
|
|
@@ -16,11 +16,23 @@ export declare class MCIDBINDRepository {
|
|
|
16
16
|
*/
|
|
17
17
|
findByQQId(qqId: string): Promise<MCIDBIND | null>;
|
|
18
18
|
/**
|
|
19
|
-
* 根据 MC
|
|
19
|
+
* 根据 MC 用户名查询绑定信息(精确匹配)
|
|
20
20
|
* @param mcUsername MC用户名
|
|
21
21
|
* @returns 绑定信息或 null
|
|
22
22
|
*/
|
|
23
23
|
findByMCUsername(mcUsername: string): Promise<MCIDBIND | null>;
|
|
24
|
+
/**
|
|
25
|
+
* 根据 MC 用户名查询绑定信息(不区分大小写)
|
|
26
|
+
* @param mcUsername MC用户名
|
|
27
|
+
* @returns 绑定信息或 null
|
|
28
|
+
*/
|
|
29
|
+
findByUsernameIgnoreCase(mcUsername: string): Promise<MCIDBIND | null>;
|
|
30
|
+
/**
|
|
31
|
+
* 根据 MC UUID 查询绑定信息
|
|
32
|
+
* @param mcUuid MC UUID(可带或不带连字符)
|
|
33
|
+
* @returns 绑定信息或 null
|
|
34
|
+
*/
|
|
35
|
+
findByUuid(mcUuid: string): Promise<MCIDBIND | null>;
|
|
24
36
|
/**
|
|
25
37
|
* 根据 B站 UID 查询绑定信息
|
|
26
38
|
* @param buidUid B站UID
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MCIDBINDRepository = void 0;
|
|
4
|
+
const helpers_1 = require("../utils/helpers");
|
|
4
5
|
/**
|
|
5
6
|
* MCIDBIND 数据仓储类
|
|
6
7
|
* 封装所有 MCIDBIND 表的数据库操作
|
|
@@ -29,7 +30,7 @@ class MCIDBINDRepository {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
/**
|
|
32
|
-
* 根据 MC
|
|
33
|
+
* 根据 MC 用户名查询绑定信息(精确匹配)
|
|
33
34
|
* @param mcUsername MC用户名
|
|
34
35
|
* @returns 绑定信息或 null
|
|
35
36
|
*/
|
|
@@ -44,6 +45,54 @@ class MCIDBINDRepository {
|
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 根据 MC 用户名查询绑定信息(不区分大小写)
|
|
50
|
+
* @param mcUsername MC用户名
|
|
51
|
+
* @returns 绑定信息或 null
|
|
52
|
+
*/
|
|
53
|
+
async findByUsernameIgnoreCase(mcUsername) {
|
|
54
|
+
try {
|
|
55
|
+
const normalizedInput = (0, helpers_1.normalizeUsername)(mcUsername, this.logger.getRawLogger());
|
|
56
|
+
this.logger.debug('数据库', `查询MC用户名(${mcUsername} -> ${normalizedInput})的绑定信息(不区分大小写)`);
|
|
57
|
+
// 获取所有绑定记录,然后在应用层过滤(因为 Koishi 数据库不支持不区分大小写查询)
|
|
58
|
+
const allBinds = await this.ctx.database.get('mcidbind', {});
|
|
59
|
+
const match = allBinds.find(bind => bind.mcUsername && (0, helpers_1.normalizeUsername)(bind.mcUsername) === normalizedInput);
|
|
60
|
+
return match || null;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
this.logger.error('数据库', `查询MC用户名(${mcUsername})绑定信息失败(不区分大小写): ${error.message}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 根据 MC UUID 查询绑定信息
|
|
69
|
+
* @param mcUuid MC UUID(可带或不带连字符)
|
|
70
|
+
* @returns 绑定信息或 null
|
|
71
|
+
*/
|
|
72
|
+
async findByUuid(mcUuid) {
|
|
73
|
+
try {
|
|
74
|
+
// 规范化 UUID(移除连字符)
|
|
75
|
+
const cleanUuid = mcUuid.replace(/-/g, '');
|
|
76
|
+
this.logger.debug('数据库', `查询MC UUID(${cleanUuid})的绑定信息`);
|
|
77
|
+
// 先尝试精确匹配
|
|
78
|
+
let binds = await this.ctx.database.get('mcidbind', { mcUuid: cleanUuid });
|
|
79
|
+
if (binds.length > 0)
|
|
80
|
+
return binds[0];
|
|
81
|
+
// 尝试带连字符的格式
|
|
82
|
+
const formattedUuid = `${cleanUuid.substring(0, 8)}-${cleanUuid.substring(8, 12)}-${cleanUuid.substring(12, 16)}-${cleanUuid.substring(16, 20)}-${cleanUuid.substring(20)}`;
|
|
83
|
+
binds = await this.ctx.database.get('mcidbind', { mcUuid: formattedUuid });
|
|
84
|
+
if (binds.length > 0)
|
|
85
|
+
return binds[0];
|
|
86
|
+
// 如果都没有,在应用层过滤(去除连字符后比较)
|
|
87
|
+
const allBinds = await this.ctx.database.get('mcidbind', {});
|
|
88
|
+
const match = allBinds.find(bind => bind.mcUuid && bind.mcUuid.replace(/-/g, '') === cleanUuid);
|
|
89
|
+
return match || null;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
this.logger.error('数据库', `查询MC UUID(${mcUuid})绑定信息失败: ${error.message}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
47
96
|
/**
|
|
48
97
|
* 根据 B站 UID 查询绑定信息
|
|
49
98
|
* @param buidUid B站UID
|
package/lib/types/database.d.ts
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误处理工具函数集合
|
|
3
|
+
* 提供统一的错误信息格式化和分类功能
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 获取用户友好的错误信息
|
|
7
|
+
* 将技术性错误转换为用户可理解的提示
|
|
8
|
+
* @param error 错误对象或错误消息字符串
|
|
9
|
+
* @returns 格式化后的用户友好错误信息
|
|
10
|
+
*/
|
|
11
|
+
export declare function getFriendlyErrorMessage(error: Error | string): string;
|
|
12
|
+
/**
|
|
13
|
+
* 提取用户友好的错误信息
|
|
14
|
+
* 根据错误消息内容返回对应的用户提示
|
|
15
|
+
* @param errorMsg 原始错误消息
|
|
16
|
+
* @returns 用户可理解的错误提示
|
|
17
|
+
*/
|
|
18
|
+
export declare function getUserFacingErrorMessage(errorMsg: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* 判断是否为警告级别错误(用户可能输入有误)
|
|
21
|
+
* @param errorMsg 错误消息
|
|
22
|
+
* @returns 是否为警告级别错误
|
|
23
|
+
*/
|
|
24
|
+
export declare function isWarningError(errorMsg: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* 判断是否为严重错误(系统问题)
|
|
27
|
+
* @param errorMsg 错误消息
|
|
28
|
+
* @returns 是否为严重错误
|
|
29
|
+
*/
|
|
30
|
+
export declare function isCriticalError(errorMsg: string): boolean;
|