koishi-plugin-bind-bot 2.0.0
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/lib/export-utils.d.ts +49 -0
- package/lib/export-utils.js +305 -0
- package/lib/force-bind-utils.d.ts +40 -0
- package/lib/force-bind-utils.js +242 -0
- package/lib/handlers/base.handler.d.ts +61 -0
- package/lib/handlers/base.handler.js +22 -0
- package/lib/handlers/binding.handler.d.ts +45 -0
- package/lib/handlers/binding.handler.js +285 -0
- package/lib/handlers/buid.handler.d.ts +71 -0
- package/lib/handlers/buid.handler.js +694 -0
- package/lib/handlers/index.d.ts +6 -0
- package/lib/handlers/index.js +22 -0
- package/lib/handlers/mcid.handler.d.ts +101 -0
- package/lib/handlers/mcid.handler.js +1045 -0
- package/lib/handlers/tag.handler.d.ts +14 -0
- package/lib/handlers/tag.handler.js +382 -0
- package/lib/handlers/whitelist.handler.d.ts +84 -0
- package/lib/handlers/whitelist.handler.js +1011 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +2693 -0
- package/lib/managers/rcon-manager.d.ts +24 -0
- package/lib/managers/rcon-manager.js +308 -0
- package/lib/repositories/mcidbind.repository.d.ts +105 -0
- package/lib/repositories/mcidbind.repository.js +288 -0
- package/lib/repositories/schedule-mute.repository.d.ts +68 -0
- package/lib/repositories/schedule-mute.repository.js +175 -0
- package/lib/types/api.d.ts +135 -0
- package/lib/types/api.js +6 -0
- package/lib/types/common.d.ts +40 -0
- package/lib/types/common.js +6 -0
- package/lib/types/config.d.ts +55 -0
- package/lib/types/config.js +6 -0
- package/lib/types/database.d.ts +47 -0
- package/lib/types/database.js +6 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/index.js +28 -0
- package/lib/utils/helpers.d.ts +76 -0
- package/lib/utils/helpers.js +275 -0
- package/lib/utils/logger.d.ts +75 -0
- package/lib/utils/logger.js +134 -0
- package/lib/utils/message-utils.d.ts +46 -0
- package/lib/utils/message-utils.js +234 -0
- package/lib/utils/rate-limiter.d.ts +26 -0
- package/lib/utils/rate-limiter.js +47 -0
- package/lib/utils/session-manager.d.ts +70 -0
- package/lib/utils/session-manager.js +120 -0
- package/package.json +39 -0
- package/readme.md +281 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2693 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Config = exports.inject = exports.name = void 0;
|
|
7
|
+
exports.apply = apply;
|
|
8
|
+
const koishi_1 = require("koishi");
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
const force_bind_utils_1 = require("./force-bind-utils");
|
|
11
|
+
const export_utils_1 = require("./export-utils");
|
|
12
|
+
const logger_1 = require("./utils/logger");
|
|
13
|
+
const rcon_manager_1 = require("./managers/rcon-manager");
|
|
14
|
+
const rate_limiter_1 = require("./utils/rate-limiter");
|
|
15
|
+
const mcidbind_repository_1 = require("./repositories/mcidbind.repository");
|
|
16
|
+
const schedule_mute_repository_1 = require("./repositories/schedule-mute.repository");
|
|
17
|
+
const handlers_1 = require("./handlers");
|
|
18
|
+
exports.name = 'bind-bot';
|
|
19
|
+
// 声明插件依赖
|
|
20
|
+
exports.inject = ['database', 'server'];
|
|
21
|
+
// 注意:Config 作为 Schema 常量导出,类型使用 IConfig 或从 './types' 导入
|
|
22
|
+
// 创建配置Schema
|
|
23
|
+
exports.Config = koishi_1.Schema.object({
|
|
24
|
+
cooldownDays: koishi_1.Schema.number()
|
|
25
|
+
.description('操作冷却时间(天)')
|
|
26
|
+
.default(15),
|
|
27
|
+
masterId: koishi_1.Schema.string()
|
|
28
|
+
.description('主人QQ号,拥有管理员管理权限')
|
|
29
|
+
.default(''),
|
|
30
|
+
allowTextPrefix: koishi_1.Schema.boolean()
|
|
31
|
+
.description('是否允许通过文本前缀触发指令(如"@机器人 mcid bind xxx")')
|
|
32
|
+
.default(false),
|
|
33
|
+
botNickname: koishi_1.Schema.string()
|
|
34
|
+
.description('机器人昵称,用于文本前缀匹配,如"@WittF-NBot"')
|
|
35
|
+
.default(''),
|
|
36
|
+
autoRecallTime: koishi_1.Schema.number()
|
|
37
|
+
.description('消息自动撤回时间(秒),同时控制机器人和用户消息,设置为0表示不自动撤回')
|
|
38
|
+
.default(0),
|
|
39
|
+
recallUserMessage: koishi_1.Schema.boolean()
|
|
40
|
+
.description('是否撤回用户发送的指令消息')
|
|
41
|
+
.default(false),
|
|
42
|
+
debugMode: koishi_1.Schema.boolean()
|
|
43
|
+
.description('调试模式,启用详细日志输出')
|
|
44
|
+
.default(false),
|
|
45
|
+
showAvatar: koishi_1.Schema.boolean()
|
|
46
|
+
.description('是否显示头像图片(MC用头图,B站用头像)')
|
|
47
|
+
.default(false),
|
|
48
|
+
showMcSkin: koishi_1.Schema.boolean()
|
|
49
|
+
.description('是否使用MC皮肤渲染图(需要先开启showAvatar)')
|
|
50
|
+
.default(false),
|
|
51
|
+
zminfoApiUrl: koishi_1.Schema.string()
|
|
52
|
+
.description('ZMINFO API地址')
|
|
53
|
+
.default('https://zminfo-api.wittf.com'),
|
|
54
|
+
enableLotteryBroadcast: koishi_1.Schema.boolean()
|
|
55
|
+
.description('是否启用天选开奖播报功能')
|
|
56
|
+
.default(false),
|
|
57
|
+
autoNicknameGroupId: koishi_1.Schema.string()
|
|
58
|
+
.description('自动群昵称设置目标群ID')
|
|
59
|
+
.default('123456789'),
|
|
60
|
+
forceBindSessdata: koishi_1.Schema.string()
|
|
61
|
+
.description('B站Cookie信息,用于强制绑定时获取粉丝牌信息(支持完整Cookie或单独SESSDATA)')
|
|
62
|
+
.default(''),
|
|
63
|
+
forceBindTargetUpUid: koishi_1.Schema.number()
|
|
64
|
+
.description('强制绑定目标UP主UID')
|
|
65
|
+
.default(686127),
|
|
66
|
+
forceBindTargetRoomId: koishi_1.Schema.number()
|
|
67
|
+
.description('强制绑定目标房间号')
|
|
68
|
+
.default(544853),
|
|
69
|
+
forceBindTargetMedalName: koishi_1.Schema.string()
|
|
70
|
+
.description('强制绑定目标粉丝牌名称')
|
|
71
|
+
.default('生态'),
|
|
72
|
+
servers: koishi_1.Schema.array(koishi_1.Schema.object({
|
|
73
|
+
id: koishi_1.Schema.string()
|
|
74
|
+
.description('服务器唯一ID(不允许重复)')
|
|
75
|
+
.required(),
|
|
76
|
+
name: koishi_1.Schema.string()
|
|
77
|
+
.description('服务器名称(用于指令显示)')
|
|
78
|
+
.required(),
|
|
79
|
+
enabled: koishi_1.Schema.boolean()
|
|
80
|
+
.description('服务器是否启用')
|
|
81
|
+
.default(true),
|
|
82
|
+
displayAddress: koishi_1.Schema.string()
|
|
83
|
+
.description('服务器展示地址(显示给用户的连接地址)')
|
|
84
|
+
.default(''),
|
|
85
|
+
description: koishi_1.Schema.string()
|
|
86
|
+
.description('服务器说明信息(显示在列表中服务器地址下方)')
|
|
87
|
+
.default(''),
|
|
88
|
+
rconAddress: koishi_1.Schema.string()
|
|
89
|
+
.description('RCON地址,格式为 IP:端口,例如 127.0.0.1:25575')
|
|
90
|
+
.required(),
|
|
91
|
+
rconPassword: koishi_1.Schema.string()
|
|
92
|
+
.description('RCON密码')
|
|
93
|
+
.default(''),
|
|
94
|
+
addCommand: koishi_1.Schema.string()
|
|
95
|
+
.description('添加白名单命令模板,使用${MCID}作为替换符')
|
|
96
|
+
.default('whitelist add ${MCID}'),
|
|
97
|
+
removeCommand: koishi_1.Schema.string()
|
|
98
|
+
.description('移除白名单命令模板,使用${MCID}作为替换符')
|
|
99
|
+
.default('whitelist remove ${MCID}'),
|
|
100
|
+
idType: koishi_1.Schema.union([
|
|
101
|
+
koishi_1.Schema.const('username').description('使用用户名'),
|
|
102
|
+
koishi_1.Schema.const('uuid').description('使用UUID')
|
|
103
|
+
]).default('username').description('白名单添加时使用的ID类型'),
|
|
104
|
+
allowSelfApply: koishi_1.Schema.boolean()
|
|
105
|
+
.description('是否允许用户自行申请白名单')
|
|
106
|
+
.default(false),
|
|
107
|
+
acceptEmptyResponse: koishi_1.Schema.boolean()
|
|
108
|
+
.description('是否将命令的空响应视为成功(某些服务器成功执行命令后不返回内容,仅对本服务器生效)')
|
|
109
|
+
.default(false),
|
|
110
|
+
})).description('Minecraft服务器配置列表').default([]),
|
|
111
|
+
});
|
|
112
|
+
function apply(ctx, config) {
|
|
113
|
+
// 创建日志服务
|
|
114
|
+
const logger = new koishi_1.Logger('bind-bot');
|
|
115
|
+
const loggerService = new logger_1.LoggerService(logger, config.debugMode);
|
|
116
|
+
// 创建数据仓储实例
|
|
117
|
+
const mcidbindRepo = new mcidbind_repository_1.MCIDBINDRepository(ctx, loggerService);
|
|
118
|
+
const scheduleMuteRepo = new schedule_mute_repository_1.ScheduleMuteRepository(ctx, loggerService);
|
|
119
|
+
// 交互型绑定会话管理
|
|
120
|
+
const bindingSessions = new Map();
|
|
121
|
+
const BINDING_SESSION_TIMEOUT = 3 * 60 * 1000; // 3分钟超时
|
|
122
|
+
// 日志辅助函数(包装 LoggerService,保持向后兼容)
|
|
123
|
+
const logDebug = (context, message) => {
|
|
124
|
+
loggerService.debug(context, message);
|
|
125
|
+
};
|
|
126
|
+
const logInfo = (context, message, forceOutput = false) => {
|
|
127
|
+
loggerService.info(context, message, forceOutput);
|
|
128
|
+
};
|
|
129
|
+
const logWarn = (context, message) => {
|
|
130
|
+
loggerService.warn(context, message);
|
|
131
|
+
};
|
|
132
|
+
const logError = (context, userId, error) => {
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
134
|
+
const normalizedQQId = normalizeQQId(userId);
|
|
135
|
+
loggerService.error(context, `QQ(${normalizedQQId})操作失败: ${errorMessage}`);
|
|
136
|
+
};
|
|
137
|
+
// 操作记录函数 - 用于记录主要操作状态,减少日志量
|
|
138
|
+
const logOperation = (operation, userId, success, details = '') => {
|
|
139
|
+
loggerService.logOperation(operation, userId, success, details);
|
|
140
|
+
};
|
|
141
|
+
// 创建头像缓存对象
|
|
142
|
+
const avatarCache = {};
|
|
143
|
+
// 缓存有效期(12小时,单位毫秒)
|
|
144
|
+
const CACHE_DURATION = 12 * 60 * 60 * 1000;
|
|
145
|
+
// 随机提醒功能的冷却缓存
|
|
146
|
+
const reminderCooldown = new Map();
|
|
147
|
+
const REMINDER_COOLDOWN_TIME = 24 * 60 * 60 * 1000; // 24小时冷却
|
|
148
|
+
// 检查群昵称是否符合规范格式
|
|
149
|
+
const checkNicknameFormat = (nickname, buidUsername, mcUsername) => {
|
|
150
|
+
if (!nickname || !buidUsername)
|
|
151
|
+
return false;
|
|
152
|
+
// 期望格式:B站名称(ID:MC用户名)或 B站名称(ID:未绑定)
|
|
153
|
+
const mcInfo = mcUsername && !mcUsername.startsWith('_temp_') ? mcUsername : "未绑定";
|
|
154
|
+
const expectedFormat = `${buidUsername}(ID:${mcInfo})`;
|
|
155
|
+
return nickname === expectedFormat;
|
|
156
|
+
};
|
|
157
|
+
// 检查用户是否在冷却期内
|
|
158
|
+
const isInReminderCooldown = (userId) => {
|
|
159
|
+
const lastReminder = reminderCooldown.get(userId);
|
|
160
|
+
if (!lastReminder)
|
|
161
|
+
return false;
|
|
162
|
+
return (Date.now() - lastReminder) < REMINDER_COOLDOWN_TIME;
|
|
163
|
+
};
|
|
164
|
+
// 设置用户提醒冷却
|
|
165
|
+
const setReminderCooldown = (userId) => {
|
|
166
|
+
reminderCooldown.set(userId, Date.now());
|
|
167
|
+
};
|
|
168
|
+
// 检查当前时间是否在群组的禁言时间段内
|
|
169
|
+
const isInMuteTime = async (groupId) => {
|
|
170
|
+
try {
|
|
171
|
+
// 查询该群组的定时禁言任务
|
|
172
|
+
const allTasks = await scheduleMuteRepo.findByGroupId(groupId);
|
|
173
|
+
const muteTasks = allTasks.filter(task => task.enabled);
|
|
174
|
+
if (!muteTasks || muteTasks.length === 0) {
|
|
175
|
+
return false; // 没有禁言任务
|
|
176
|
+
}
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
|
|
179
|
+
// 检查是否在任何一个禁言时间段内
|
|
180
|
+
for (const task of muteTasks) {
|
|
181
|
+
const startTime = task.startTime;
|
|
182
|
+
const endTime = task.endTime;
|
|
183
|
+
// 处理跨天的情况
|
|
184
|
+
if (startTime <= endTime) {
|
|
185
|
+
// 同一天内的时间段 (如 22:01 到 22:02)
|
|
186
|
+
if (currentTime >= startTime && currentTime <= endTime) {
|
|
187
|
+
logger.debug(`[禁言检查] 群组${groupId}当前时间${currentTime}在禁言时间段内: ${startTime}-${endTime}`);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// 跨天的时间段 (如 00:10 到 07:00,表示凌晨0:10到早上7:00)
|
|
193
|
+
if (currentTime >= startTime || currentTime <= endTime) {
|
|
194
|
+
logger.debug(`[禁言检查] 群组${groupId}当前时间${currentTime}在跨天禁言时间段内: ${startTime}-${endTime}`);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
logger.error(`[禁言检查] 查询群组${groupId}的禁言时间失败: ${error.message}`);
|
|
203
|
+
return false; // 查询失败时不阻止提醒
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
// 创建RCON连接管理器
|
|
207
|
+
const rconManager = new rcon_manager_1.RconManager(loggerService.createChild('RCON管理器'), config.servers || []);
|
|
208
|
+
// 创建RCON限流器实例
|
|
209
|
+
const rconRateLimiter = new rate_limiter_1.RateLimiter(10, 3000); // 3秒内最多10个请求
|
|
210
|
+
// 会话管理辅助函数
|
|
211
|
+
const createBindingSession = (userId, channelId) => {
|
|
212
|
+
const sessionKey = `${normalizeQQId(userId)}_${channelId}`;
|
|
213
|
+
// 如果已有会话,先清理
|
|
214
|
+
const existingSession = bindingSessions.get(sessionKey);
|
|
215
|
+
if (existingSession) {
|
|
216
|
+
clearTimeout(existingSession.timeout);
|
|
217
|
+
bindingSessions.delete(sessionKey);
|
|
218
|
+
}
|
|
219
|
+
// 创建超时定时器
|
|
220
|
+
const timeout = setTimeout(() => {
|
|
221
|
+
bindingSessions.delete(sessionKey);
|
|
222
|
+
// 发送超时消息,@用户
|
|
223
|
+
const normalizedUser = normalizeQQId(userId);
|
|
224
|
+
ctx.bots.forEach(bot => {
|
|
225
|
+
bot.sendMessage(channelId, [koishi_1.h.at(normalizedUser), koishi_1.h.text(' 绑定会话已超时,请重新开始绑定流程\n\n⚠️ 温馨提醒:若在管理员多次提醒后仍不配合绑定账号信息,将按群规进行相应处理。')]).catch(() => { });
|
|
226
|
+
});
|
|
227
|
+
logger.info(`[交互绑定] QQ(${normalizedUser})的绑定会话因超时被清理`);
|
|
228
|
+
}, BINDING_SESSION_TIMEOUT);
|
|
229
|
+
// 创建新会话
|
|
230
|
+
const session = {
|
|
231
|
+
userId: normalizeQQId(userId),
|
|
232
|
+
channelId,
|
|
233
|
+
state: 'waiting_buid',
|
|
234
|
+
startTime: Date.now(),
|
|
235
|
+
timeout
|
|
236
|
+
};
|
|
237
|
+
bindingSessions.set(sessionKey, session);
|
|
238
|
+
logger.info(`[交互绑定] 为QQ(${normalizeQQId(userId)})创建了新的绑定会话`);
|
|
239
|
+
};
|
|
240
|
+
const getBindingSession = (userId, channelId) => {
|
|
241
|
+
const sessionKey = `${normalizeQQId(userId)}_${channelId}`;
|
|
242
|
+
return bindingSessions.get(sessionKey) || null;
|
|
243
|
+
};
|
|
244
|
+
const updateBindingSession = (userId, channelId, updates) => {
|
|
245
|
+
const sessionKey = `${normalizeQQId(userId)}_${channelId}`;
|
|
246
|
+
const session = bindingSessions.get(sessionKey);
|
|
247
|
+
if (session) {
|
|
248
|
+
Object.assign(session, updates);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const removeBindingSession = (userId, channelId) => {
|
|
252
|
+
const sessionKey = `${normalizeQQId(userId)}_${channelId}`;
|
|
253
|
+
const session = bindingSessions.get(sessionKey);
|
|
254
|
+
if (session) {
|
|
255
|
+
clearTimeout(session.timeout);
|
|
256
|
+
bindingSessions.delete(sessionKey);
|
|
257
|
+
logger.info(`[交互绑定] 移除了QQ(${normalizeQQId(userId)})的绑定会话`);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
// 自动群昵称设置功能
|
|
261
|
+
const autoSetGroupNickname = async (session, mcUsername, buidUsername, targetUserId, specifiedGroupId) => {
|
|
262
|
+
try {
|
|
263
|
+
// 如果指定了目标用户ID,使用目标用户ID,否则使用session的用户ID
|
|
264
|
+
const actualUserId = targetUserId || session.userId;
|
|
265
|
+
const normalizedUserId = normalizeQQId(actualUserId);
|
|
266
|
+
// 根据MC绑定状态设置不同的格式(临时用户名视为未绑定)
|
|
267
|
+
const mcInfo = (mcUsername && !mcUsername.startsWith('_temp_')) ? mcUsername : "未绑定";
|
|
268
|
+
const newNickname = `${buidUsername}(ID:${mcInfo})`;
|
|
269
|
+
// 使用指定的群ID,如果没有指定则使用配置的默认群ID
|
|
270
|
+
const targetGroupId = specifiedGroupId || config.autoNicknameGroupId;
|
|
271
|
+
logger.debug(`[群昵称设置] 开始处理QQ(${normalizedUserId})的群昵称设置,目标群: ${targetGroupId}`);
|
|
272
|
+
logger.debug(`[群昵称设置] 期望昵称: "${newNickname}"`);
|
|
273
|
+
if (session.bot.internal && targetGroupId) {
|
|
274
|
+
// 使用规范化的QQ号调用OneBot API
|
|
275
|
+
logger.debug(`[群昵称设置] 使用用户ID: ${normalizedUserId}`);
|
|
276
|
+
// 先获取当前群昵称进行比对
|
|
277
|
+
try {
|
|
278
|
+
logger.debug(`[群昵称设置] 正在获取QQ(${normalizedUserId})在群${targetGroupId}的当前昵称...`);
|
|
279
|
+
const currentGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
280
|
+
const currentNickname = currentGroupInfo.card || currentGroupInfo.nickname || '';
|
|
281
|
+
logger.debug(`[群昵称设置] 当前昵称: "${currentNickname}"`);
|
|
282
|
+
// 如果当前昵称和目标昵称一致,跳过修改
|
|
283
|
+
if (currentNickname === newNickname) {
|
|
284
|
+
logger.info(`[群昵称设置] QQ(${normalizedUserId})群昵称已经是"${newNickname}",跳过修改`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// 昵称不一致,执行修改
|
|
288
|
+
logger.debug(`[群昵称设置] 昵称不一致,正在修改群昵称...`);
|
|
289
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, newNickname);
|
|
290
|
+
logger.info(`[群昵称设置] 成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${newNickname}"`);
|
|
291
|
+
// 验证设置是否生效 - 再次获取群昵称确认
|
|
292
|
+
try {
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
294
|
+
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
295
|
+
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
296
|
+
if (verifyNickname === newNickname) {
|
|
297
|
+
logger.info(`[群昵称设置] ✅ 验证成功,群昵称已生效: "${verifyNickname}"`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
logger.warn(`[群昵称设置] ⚠️ 验证失败,期望"${newNickname}",实际"${verifyNickname}",可能是权限不足或API延迟`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (verifyError) {
|
|
304
|
+
logger.warn(`[群昵称设置] 无法验证群昵称设置结果: ${verifyError.message}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (getInfoError) {
|
|
308
|
+
// 如果获取当前昵称失败,直接尝试设置新昵称
|
|
309
|
+
logger.warn(`[群昵称设置] 获取QQ(${normalizedUserId})当前群昵称失败: ${getInfoError.message}`);
|
|
310
|
+
logger.warn(`[群昵称设置] 错误详情: ${JSON.stringify(getInfoError)}`);
|
|
311
|
+
logger.debug(`[群昵称设置] 将直接尝试设置新昵称...`);
|
|
312
|
+
try {
|
|
313
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, newNickname);
|
|
314
|
+
logger.info(`[群昵称设置] 成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称设置为: ${newNickname}`);
|
|
315
|
+
// 验证设置是否生效
|
|
316
|
+
try {
|
|
317
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
318
|
+
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
319
|
+
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
320
|
+
if (verifyNickname === newNickname) {
|
|
321
|
+
logger.info(`[群昵称设置] ✅ 验证成功,群昵称已生效: "${verifyNickname}"`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
logger.warn(`[群昵称设置] ⚠️ 验证失败,期望"${newNickname}",实际"${verifyNickname}",可能是权限不足`);
|
|
325
|
+
logger.warn(`[群昵称设置] 建议检查: 1.机器人是否为群管理员 2.群设置是否允许管理员修改昵称 3.OneBot实现是否支持该功能`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (verifyError) {
|
|
329
|
+
logger.warn(`[群昵称设置] 无法验证群昵称设置结果: ${verifyError.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (setCardError) {
|
|
333
|
+
logger.error(`[群昵称设置] 设置群昵称失败: ${setCardError.message}`);
|
|
334
|
+
logger.error(`[群昵称设置] 错误详情: ${JSON.stringify(setCardError)}`);
|
|
335
|
+
throw setCardError;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else if (!session.bot.internal) {
|
|
340
|
+
logger.debug(`[群昵称设置] QQ(${normalizedUserId})bot不支持OneBot内部API,跳过自动群昵称设置`);
|
|
341
|
+
}
|
|
342
|
+
else if (!targetGroupId) {
|
|
343
|
+
logger.debug(`[群昵称设置] QQ(${normalizedUserId})未配置自动群昵称设置目标群,跳过群昵称设置`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
const actualUserId = targetUserId || session.userId;
|
|
348
|
+
const normalizedUserId = normalizeQQId(actualUserId);
|
|
349
|
+
logger.error(`[群昵称设置] QQ(${normalizedUserId})自动群昵称设置失败: ${error.message}`);
|
|
350
|
+
logger.error(`[群昵称设置] 完整错误信息: ${JSON.stringify(error)}`);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
// 检查是否为无关输入
|
|
354
|
+
const checkIrrelevantInput = (bindingSession, content) => {
|
|
355
|
+
if (!content)
|
|
356
|
+
return false;
|
|
357
|
+
// 常见的聊天用语或明显无关的内容
|
|
358
|
+
const chatKeywords = ['你好', 'hello', 'hi', '在吗', '在不在', '怎么样', '什么', '为什么', '好的', '谢谢', '哈哈', '呵呵', '早上好', '晚上好', '晚安', '再见', '拜拜', '666', '牛', '厉害', '真的吗', '不是吧', '哇', '哦', '嗯', '好吧', '行', '可以', '没事', '没问题', '没关系'];
|
|
359
|
+
const lowercaseContent = content.toLowerCase();
|
|
360
|
+
// 检查是否包含明显的聊天用语
|
|
361
|
+
if (chatKeywords.some(keyword => lowercaseContent.includes(keyword))) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
// 检查是否为明显的聊天模式(多个连续的标点符号、表情等)
|
|
365
|
+
if (/[!?。,;:""''()【】〈〉《》「」『』〔〕〖〗〘〙〚〛]{2,}/.test(content) ||
|
|
366
|
+
/[!?.,;:"'()[\]<>{}]{3,}/.test(content)) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (bindingSession.state === 'waiting_mc_username') {
|
|
370
|
+
// 先排除跳过命令,这些是有效输入
|
|
371
|
+
if (content === '跳过' || content === 'skip') {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
// MC用户名检查
|
|
375
|
+
// 长度明显不符合MC用户名规范(3-16位)
|
|
376
|
+
if (content.length < 2 || content.length > 20) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
// 包含中文或其他明显不是MC用户名的字符
|
|
380
|
+
if (/[\u4e00-\u9fa5]/.test(content) || content.includes(' ') || content.includes('@')) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
// 如果是明显的指令格式
|
|
384
|
+
if (content.startsWith('.') || content.startsWith('/') || content.startsWith('mcid') || content.startsWith('buid')) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (bindingSession.state === 'waiting_buid') {
|
|
389
|
+
// B站UID检查
|
|
390
|
+
// 移除UID:前缀后检查
|
|
391
|
+
let actualContent = content;
|
|
392
|
+
if (content.toLowerCase().startsWith('uid:')) {
|
|
393
|
+
actualContent = content.substring(4);
|
|
394
|
+
}
|
|
395
|
+
// 如果不是纯数字且不是跳过命令
|
|
396
|
+
if (!/^\d+$/.test(actualContent) && content !== '跳过' && content !== 'skip') {
|
|
397
|
+
// 检查是否明显是聊天内容(包含字母、中文、空格等)
|
|
398
|
+
if (/[a-zA-Z\u4e00-\u9fa5\s]/.test(content) && !content.toLowerCase().startsWith('uid:')) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
// 如果是明显的指令格式
|
|
402
|
+
if (content.startsWith('.') || content.startsWith('/') || content.startsWith('mcid') || content.startsWith('buid')) {
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
};
|
|
409
|
+
// 根据配置获取命令前缀
|
|
410
|
+
const getCommandPrefix = () => {
|
|
411
|
+
if (config.allowTextPrefix && config.botNickname) {
|
|
412
|
+
// 检查botNickname是否已经包含@符号,避免重复添加
|
|
413
|
+
const nickname = config.botNickname.startsWith('@') ?
|
|
414
|
+
config.botNickname :
|
|
415
|
+
`@${config.botNickname}`;
|
|
416
|
+
return `${nickname} `;
|
|
417
|
+
}
|
|
418
|
+
return '';
|
|
419
|
+
};
|
|
420
|
+
// 格式化命令提示
|
|
421
|
+
const formatCommand = (cmd) => {
|
|
422
|
+
return `${getCommandPrefix()}${cmd}`;
|
|
423
|
+
};
|
|
424
|
+
// 简单的锁机制,用于防止并发操作
|
|
425
|
+
const operationLocks = {};
|
|
426
|
+
// 获取锁
|
|
427
|
+
const acquireLock = (key) => {
|
|
428
|
+
if (operationLocks[key]) {
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
operationLocks[key] = true;
|
|
432
|
+
return true;
|
|
433
|
+
};
|
|
434
|
+
// 释放锁
|
|
435
|
+
const releaseLock = (key) => {
|
|
436
|
+
operationLocks[key] = false;
|
|
437
|
+
};
|
|
438
|
+
// 使用锁执行异步操作
|
|
439
|
+
const withLock = async (key, operation, timeoutMs = 10000) => {
|
|
440
|
+
// 操作ID,用于日志
|
|
441
|
+
const operationId = Math.random().toString(36).substr(2, 9);
|
|
442
|
+
// 尝试获取锁
|
|
443
|
+
let acquired = false;
|
|
444
|
+
let attempts = 0;
|
|
445
|
+
const maxAttempts = 5;
|
|
446
|
+
while (!acquired && attempts < maxAttempts) {
|
|
447
|
+
acquired = acquireLock(key);
|
|
448
|
+
if (!acquired) {
|
|
449
|
+
logger.debug(`[锁] 操作${operationId}等待锁 ${key} 释放 (尝试 ${attempts + 1}/${maxAttempts})`);
|
|
450
|
+
// 等待一段时间后重试
|
|
451
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
452
|
+
attempts++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (!acquired) {
|
|
456
|
+
logger.warn(`[锁] 操作${operationId}无法获取锁 ${key},强制获取`);
|
|
457
|
+
// 强制获取锁
|
|
458
|
+
acquireLock(key);
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
// 设置超时
|
|
462
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
463
|
+
setTimeout(() => reject(new Error(`操作超时 (${timeoutMs}ms)`)), timeoutMs);
|
|
464
|
+
});
|
|
465
|
+
// 执行操作
|
|
466
|
+
const operationPromise = operation();
|
|
467
|
+
const result = await Promise.race([operationPromise, timeoutPromise]);
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
// 无论成功失败,都释放锁
|
|
472
|
+
releaseLock(key);
|
|
473
|
+
logger.debug(`[锁] 操作${operationId}释放锁 ${key}`);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
// 插件销毁时关闭所有RCON连接
|
|
477
|
+
ctx.on('dispose', async () => {
|
|
478
|
+
logger.info('[RCON管理器] 插件卸载,关闭所有RCON连接');
|
|
479
|
+
await rconManager.closeAll();
|
|
480
|
+
});
|
|
481
|
+
// 监听群成员加入事件,自动启动绑定流程
|
|
482
|
+
ctx.on('guild-member-added', async (session) => {
|
|
483
|
+
try {
|
|
484
|
+
// 只处理指定群的成员加入
|
|
485
|
+
if (session.channelId !== config.autoNicknameGroupId) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
489
|
+
logger.info(`[新人绑定] 用户QQ(${normalizedUserId})加入群聊,准备发送绑定提醒`);
|
|
490
|
+
// 检查用户是否已有绑定记录
|
|
491
|
+
const existingBind = await getMcBindByQQId(normalizedUserId);
|
|
492
|
+
// 如果用户已完成全部绑定,不需要提醒
|
|
493
|
+
if (existingBind && existingBind.mcUsername && existingBind.buidUid) {
|
|
494
|
+
logger.info(`[新人绑定] 用户QQ(${normalizedUserId})已完成全部绑定,跳过提醒`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// 检查是否在禁言时间段内
|
|
498
|
+
const inMuteTime = await isInMuteTime(session.channelId);
|
|
499
|
+
// 发送欢迎消息
|
|
500
|
+
let welcomeMessage = `🎉 欢迎新成员 ${koishi_1.h.at(session.userId)} 加入群聊!\n\n`;
|
|
501
|
+
if (!existingBind || (!existingBind.mcUsername && !existingBind.buidUid)) {
|
|
502
|
+
// 完全未绑定
|
|
503
|
+
if (inMuteTime) {
|
|
504
|
+
// 在禁言时间内,只发送欢迎消息和基本提醒
|
|
505
|
+
welcomeMessage += `📋 请在非禁言时间段使用 ${formatCommand('buid bind <B站UID>')} 绑定B站账号\n`;
|
|
506
|
+
welcomeMessage += `🎮 也可以使用 ${formatCommand('mcid bind <MC用户名>')} 绑定MC账号`;
|
|
507
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
508
|
+
logger.info(`[新人绑定] 新成员QQ(${normalizedUserId})在禁言时间内,仅发送欢迎消息,不启动绑定流程`);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
// 不在禁言时间,自动启动交互式绑定
|
|
512
|
+
welcomeMessage += `📋 请选择绑定方式:\n1️⃣ 发送您的B站UID进行B站绑定\n2️⃣ 发送"跳过"仅绑定MC账号`;
|
|
513
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
514
|
+
logger.info(`[新人绑定] 为新成员QQ(${normalizedUserId})自动启动交互式绑定流程`);
|
|
515
|
+
// 创建绑定会话并发送初始提示
|
|
516
|
+
createBindingSession(session.userId, session.channelId);
|
|
517
|
+
const bindingSession = getBindingSession(session.userId, session.channelId);
|
|
518
|
+
bindingSession.state = 'waiting_buid';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (existingBind.mcUsername && !existingBind.buidUid) {
|
|
522
|
+
// 只绑定了MC,未绑定B站
|
|
523
|
+
// 检查是否为有效的MC绑定(非临时用户名)
|
|
524
|
+
if (existingBind.mcUsername.startsWith('_temp_')) {
|
|
525
|
+
// 临时用户名,实际上应该是只绑定了B站但MC是临时的,不应该进入这个分支
|
|
526
|
+
// 这种情况应该按照"只绑定了B站"处理
|
|
527
|
+
welcomeMessage += `📋 检测到您已绑定B站账号,但尚未绑定MC账号\n`;
|
|
528
|
+
welcomeMessage += `🎮 可使用 ${formatCommand('mcid bind <MC用户名>')} 绑定MC账号`;
|
|
529
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
530
|
+
logger.info(`[新人绑定] 新成员QQ(${normalizedUserId})实际只绑定了B站(MC为临时用户名),已发送绑定提醒`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// 真正绑定了MC账号
|
|
534
|
+
const displayUsername = existingBind.mcUsername;
|
|
535
|
+
welcomeMessage += `🎮 已绑定MC: ${displayUsername}\n`;
|
|
536
|
+
if (inMuteTime) {
|
|
537
|
+
// 在禁言时间内,只发送状态信息
|
|
538
|
+
welcomeMessage += `📋 请在非禁言时间段使用 ${formatCommand('buid bind <B站UID>')} 绑定B站账号`;
|
|
539
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
540
|
+
logger.info(`[新人绑定] 新成员QQ(${normalizedUserId})在禁言时间内,仅发送绑定状态提醒`);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// 不在禁言时间,自动启动B站绑定
|
|
544
|
+
welcomeMessage += `📋 请发送您的B站UID进行绑定`;
|
|
545
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
546
|
+
logger.info(`[新人绑定] 为新成员QQ(${normalizedUserId})自动启动B站绑定流程`);
|
|
547
|
+
// 创建绑定会话,直接进入B站绑定步骤
|
|
548
|
+
createBindingSession(session.userId, session.channelId);
|
|
549
|
+
const bindingSession = getBindingSession(session.userId, session.channelId);
|
|
550
|
+
bindingSession.state = 'waiting_buid';
|
|
551
|
+
bindingSession.mcUsername = existingBind.mcUsername;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (!existingBind.mcUsername && existingBind.buidUid) {
|
|
556
|
+
// 只绑定了B站,未绑定MC - 仅发送提醒
|
|
557
|
+
welcomeMessage += `📋 检测到您已绑定B站账号,但尚未绑定MC账号\n`;
|
|
558
|
+
welcomeMessage += `🎮 可使用 ${formatCommand('mcid bind <MC用户名>')} 绑定MC账号`;
|
|
559
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
560
|
+
logger.info(`[新人绑定] 新成员QQ(${normalizedUserId})已绑定B站但未绑定MC,已发送绑定提醒`);
|
|
561
|
+
}
|
|
562
|
+
else if (existingBind.mcUsername && existingBind.mcUsername.startsWith('_temp_') && existingBind.buidUid) {
|
|
563
|
+
// MC是临时用户名但已绑定B站 - 也按照"只绑定了B站"处理
|
|
564
|
+
welcomeMessage += `📋 检测到您已绑定B站账号,但尚未绑定MC账号\n`;
|
|
565
|
+
welcomeMessage += `🎮 可使用 ${formatCommand('mcid bind <MC用户名>')} 绑定MC账号`;
|
|
566
|
+
await session.bot.sendMessage(session.channelId, welcomeMessage);
|
|
567
|
+
logger.info(`[新人绑定] 新成员QQ(${normalizedUserId})已绑定B站但MC为临时用户名,已发送绑定提醒`);
|
|
568
|
+
}
|
|
569
|
+
logger.info(`[新人绑定] 已处理新成员QQ(${normalizedUserId})的入群事件`);
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
logger.error(`[新人绑定] 处理新成员加入失败: ${error.message}`);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
// 注册天选开奖 Webhook
|
|
576
|
+
ctx.server.post('/lottery', async (content) => {
|
|
577
|
+
try {
|
|
578
|
+
logger.info(`[天选开奖] 收到天选开奖webhook请求`);
|
|
579
|
+
// 检查天选播报开关
|
|
580
|
+
if (!config?.enableLotteryBroadcast) {
|
|
581
|
+
logger.info(`[天选开奖] 天选播报功能已禁用,忽略webhook请求`);
|
|
582
|
+
content.status = 200;
|
|
583
|
+
content.body = 'Lottery broadcast disabled';
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
// 检查请求头
|
|
587
|
+
const userAgent = content.header['user-agent'] || content.header['User-Agent'];
|
|
588
|
+
if (userAgent && !userAgent.includes('ZMINFO-EventBridge')) {
|
|
589
|
+
logger.warn(`[天选开奖] 无效的User-Agent: ${userAgent}`);
|
|
590
|
+
content.status = 400;
|
|
591
|
+
content.body = 'Invalid User-Agent';
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// 解析请求数据
|
|
595
|
+
let lotteryData;
|
|
596
|
+
try {
|
|
597
|
+
// 如果是字符串,尝试解析为JSON
|
|
598
|
+
if (typeof content.request.body === 'string') {
|
|
599
|
+
lotteryData = JSON.parse(content.request.body);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
lotteryData = content.request.body;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch (parseError) {
|
|
606
|
+
logger.error(`[天选开奖] 解析请求数据失败: ${parseError.message}`);
|
|
607
|
+
content.status = 400;
|
|
608
|
+
content.body = 'Invalid JSON format';
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// 验证数据格式
|
|
612
|
+
if (!lotteryData.type || lotteryData.type !== 'lottery-result') {
|
|
613
|
+
logger.warn(`[天选开奖] 无效的事件类型: ${lotteryData.type}`);
|
|
614
|
+
content.status = 400;
|
|
615
|
+
content.body = 'Invalid event type';
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!lotteryData.lottery_id || !lotteryData.winners || !Array.isArray(lotteryData.winners)) {
|
|
619
|
+
logger.warn(`[天选开奖] 数据格式不完整`);
|
|
620
|
+
content.status = 400;
|
|
621
|
+
content.body = 'Incomplete data format';
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// 记录接收的数据
|
|
625
|
+
if (config.debugMode) {
|
|
626
|
+
logger.debug(`[天选开奖] 接收到的数据: ${JSON.stringify(lotteryData, null, 2)}`);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
logger.info(`[天选开奖] 接收到天选事件: ${lotteryData.lottery_id},奖品: ${lotteryData.reward_name},中奖人数: ${lotteryData.winners.length}`);
|
|
630
|
+
}
|
|
631
|
+
// 异步处理天选开奖数据(不阻塞响应)
|
|
632
|
+
handleLotteryResult(lotteryData).catch(error => {
|
|
633
|
+
logger.error(`[天选开奖] 异步处理天选开奖数据失败: ${error.message}`);
|
|
634
|
+
});
|
|
635
|
+
// 立即返回成功响应
|
|
636
|
+
content.status = 200;
|
|
637
|
+
content.body = 'OK';
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
logger.error(`[天选开奖] 处理webhook请求失败: ${error.message}`);
|
|
641
|
+
content.status = 500;
|
|
642
|
+
content.body = 'Internal Server Error';
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
// 在数据库中创建MCIDBIND表
|
|
646
|
+
ctx.model.extend('mcidbind', {
|
|
647
|
+
qqId: {
|
|
648
|
+
type: 'string',
|
|
649
|
+
},
|
|
650
|
+
mcUsername: {
|
|
651
|
+
type: 'string',
|
|
652
|
+
initial: '',
|
|
653
|
+
},
|
|
654
|
+
mcUuid: {
|
|
655
|
+
type: 'string',
|
|
656
|
+
initial: '',
|
|
657
|
+
},
|
|
658
|
+
lastModified: {
|
|
659
|
+
type: 'timestamp',
|
|
660
|
+
initial: null,
|
|
661
|
+
},
|
|
662
|
+
isAdmin: {
|
|
663
|
+
type: 'boolean',
|
|
664
|
+
initial: false,
|
|
665
|
+
},
|
|
666
|
+
whitelist: {
|
|
667
|
+
type: 'json',
|
|
668
|
+
initial: [],
|
|
669
|
+
},
|
|
670
|
+
tags: {
|
|
671
|
+
type: 'json',
|
|
672
|
+
initial: [],
|
|
673
|
+
},
|
|
674
|
+
// BUID相关字段
|
|
675
|
+
buidUid: {
|
|
676
|
+
type: 'string',
|
|
677
|
+
initial: '',
|
|
678
|
+
},
|
|
679
|
+
buidUsername: {
|
|
680
|
+
type: 'string',
|
|
681
|
+
initial: '',
|
|
682
|
+
},
|
|
683
|
+
guardLevel: {
|
|
684
|
+
type: 'integer',
|
|
685
|
+
initial: 0,
|
|
686
|
+
},
|
|
687
|
+
guardLevelText: {
|
|
688
|
+
type: 'string',
|
|
689
|
+
initial: '',
|
|
690
|
+
},
|
|
691
|
+
maxGuardLevel: {
|
|
692
|
+
type: 'integer',
|
|
693
|
+
initial: 0,
|
|
694
|
+
},
|
|
695
|
+
maxGuardLevelText: {
|
|
696
|
+
type: 'string',
|
|
697
|
+
initial: '',
|
|
698
|
+
},
|
|
699
|
+
medalName: {
|
|
700
|
+
type: 'string',
|
|
701
|
+
initial: '',
|
|
702
|
+
},
|
|
703
|
+
medalLevel: {
|
|
704
|
+
type: 'integer',
|
|
705
|
+
initial: 0,
|
|
706
|
+
},
|
|
707
|
+
wealthMedalLevel: {
|
|
708
|
+
type: 'integer',
|
|
709
|
+
initial: 0,
|
|
710
|
+
},
|
|
711
|
+
lastActiveTime: {
|
|
712
|
+
type: 'timestamp',
|
|
713
|
+
initial: null,
|
|
714
|
+
},
|
|
715
|
+
reminderCount: {
|
|
716
|
+
type: 'integer',
|
|
717
|
+
initial: 0,
|
|
718
|
+
},
|
|
719
|
+
}, {
|
|
720
|
+
// 设置主键为qqId
|
|
721
|
+
primary: 'qqId',
|
|
722
|
+
// 添加索引
|
|
723
|
+
unique: [['mcUsername'], ['buidUid']],
|
|
724
|
+
// 添加isAdmin索引,提高查询效率
|
|
725
|
+
indexes: [['isAdmin'], ['buidUid']],
|
|
726
|
+
});
|
|
727
|
+
// 检查表结构是否包含旧字段
|
|
728
|
+
const checkTableStructure = async () => {
|
|
729
|
+
try {
|
|
730
|
+
// 尝试获取一条记录来检查字段
|
|
731
|
+
const records = await mcidbindRepo.findAll({ limit: 1 });
|
|
732
|
+
// 如果没有记录,不需要迁移
|
|
733
|
+
if (!records || records.length === 0)
|
|
734
|
+
return false;
|
|
735
|
+
// 检查记录中是否包含id或userId字段,或缺少whitelist字段
|
|
736
|
+
const record = records[0];
|
|
737
|
+
return 'id' in record || 'userId' in record || !('whitelist' in record);
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
logger.error(`[初始化] 检查表结构失败: ${error.message}`);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
// 添加缺失字段
|
|
745
|
+
const addMissingFields = async () => {
|
|
746
|
+
try {
|
|
747
|
+
// 获取所有记录
|
|
748
|
+
const records = await mcidbindRepo.findAll();
|
|
749
|
+
let updatedCount = 0;
|
|
750
|
+
// 更新每个缺少字段的记录
|
|
751
|
+
for (const record of records) {
|
|
752
|
+
let needUpdate = false;
|
|
753
|
+
const updateData = {};
|
|
754
|
+
// 检查并添加whitelist字段
|
|
755
|
+
if (!record.whitelist) {
|
|
756
|
+
updateData.whitelist = [];
|
|
757
|
+
needUpdate = true;
|
|
758
|
+
}
|
|
759
|
+
// 检查并添加tags字段
|
|
760
|
+
if (!record.tags) {
|
|
761
|
+
updateData.tags = [];
|
|
762
|
+
needUpdate = true;
|
|
763
|
+
}
|
|
764
|
+
// 检查并添加maxGuardLevel字段
|
|
765
|
+
if (!('maxGuardLevel' in record)) {
|
|
766
|
+
updateData.maxGuardLevel = 0;
|
|
767
|
+
needUpdate = true;
|
|
768
|
+
}
|
|
769
|
+
// 检查并添加maxGuardLevelText字段
|
|
770
|
+
if (!('maxGuardLevelText' in record)) {
|
|
771
|
+
updateData.maxGuardLevelText = '';
|
|
772
|
+
needUpdate = true;
|
|
773
|
+
}
|
|
774
|
+
// 检查并添加reminderCount字段
|
|
775
|
+
if (!('reminderCount' in record)) {
|
|
776
|
+
updateData.reminderCount = 0;
|
|
777
|
+
needUpdate = true;
|
|
778
|
+
}
|
|
779
|
+
// 如果需要更新,执行更新操作
|
|
780
|
+
if (needUpdate) {
|
|
781
|
+
await mcidbindRepo.update(record.qqId, updateData);
|
|
782
|
+
updatedCount++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (updatedCount > 0) {
|
|
786
|
+
logger.info(`[初始化] 成功为${updatedCount}条记录添加缺失字段`);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
logger.info(`[初始化] 所有记录都包含必要字段,无需更新`);
|
|
790
|
+
}
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
logger.error(`[初始化] 添加缺失字段失败: ${error.message}`);
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
// 重建MCIDBIND表
|
|
799
|
+
const rebuildMcidBindTable = async () => {
|
|
800
|
+
try {
|
|
801
|
+
// 备份现有数据
|
|
802
|
+
const oldRecords = await mcidbindRepo.findAll();
|
|
803
|
+
logger.info(`[初始化] 成功备份${oldRecords.length}条记录`);
|
|
804
|
+
// 创建数据备份(用于恢复)
|
|
805
|
+
const backupData = JSON.parse(JSON.stringify(oldRecords));
|
|
806
|
+
try {
|
|
807
|
+
// 提取有效数据
|
|
808
|
+
const validRecords = oldRecords.map(record => {
|
|
809
|
+
// 确保qqId存在
|
|
810
|
+
if (!record.qqId) {
|
|
811
|
+
// 如果没有qqId但有userId,尝试从userId提取
|
|
812
|
+
if ('userId' in record && record.userId) {
|
|
813
|
+
record.qqId = normalizeQQId(String(record.userId));
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
// 既没有qqId也没有userId,跳过此记录
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
qqId: record.qqId,
|
|
822
|
+
mcUsername: record.mcUsername || '',
|
|
823
|
+
mcUuid: record.mcUuid || '',
|
|
824
|
+
lastModified: record.lastModified || new Date(),
|
|
825
|
+
isAdmin: record.isAdmin || false,
|
|
826
|
+
whitelist: record.whitelist || [],
|
|
827
|
+
tags: record.tags || []
|
|
828
|
+
};
|
|
829
|
+
}).filter(record => record !== null);
|
|
830
|
+
// 删除现有表
|
|
831
|
+
await mcidbindRepo.deleteAll();
|
|
832
|
+
logger.info('[初始化] 成功删除旧表数据');
|
|
833
|
+
// 重新创建记录
|
|
834
|
+
let successCount = 0;
|
|
835
|
+
let errorCount = 0;
|
|
836
|
+
for (const record of validRecords) {
|
|
837
|
+
try {
|
|
838
|
+
await mcidbindRepo.create(record);
|
|
839
|
+
successCount++;
|
|
840
|
+
}
|
|
841
|
+
catch (e) {
|
|
842
|
+
errorCount++;
|
|
843
|
+
logger.warn(`[初始化] 重建记录失败 (QQ=${record.qqId}): ${e.message}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
logger.info(`[初始化] 成功重建了${successCount}条记录,失败${errorCount}条`);
|
|
847
|
+
return true;
|
|
848
|
+
}
|
|
849
|
+
catch (migrationError) {
|
|
850
|
+
// 迁移过程出错,尝试恢复
|
|
851
|
+
logger.error(`[初始化] 表重建过程失败,尝试恢复数据: ${migrationError.message}`);
|
|
852
|
+
try {
|
|
853
|
+
// 清空表以避免重复数据
|
|
854
|
+
await mcidbindRepo.deleteAll();
|
|
855
|
+
// 恢复原始数据
|
|
856
|
+
for (const record of backupData) {
|
|
857
|
+
await mcidbindRepo.create(record);
|
|
858
|
+
}
|
|
859
|
+
logger.info(`[初始化] 成功恢复${backupData.length}条原始记录`);
|
|
860
|
+
}
|
|
861
|
+
catch (recoveryError) {
|
|
862
|
+
logger.error(`[初始化] 数据恢复失败,可能导致数据丢失: ${recoveryError.message}`);
|
|
863
|
+
throw new Error('数据迁移失败且无法恢复');
|
|
864
|
+
}
|
|
865
|
+
throw migrationError;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
logger.error(`[初始化] 重建表失败: ${error.message}`);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
// 处理用户ID,去除平台前缀,只保留QQ号
|
|
874
|
+
const normalizeQQId = (userId) => {
|
|
875
|
+
// 处理空值情况
|
|
876
|
+
if (!userId) {
|
|
877
|
+
logger.warn(`[用户ID] 收到空用户ID`);
|
|
878
|
+
return '';
|
|
879
|
+
}
|
|
880
|
+
let extractedId = '';
|
|
881
|
+
// 检查是否是手动输入的@符号(错误用法)
|
|
882
|
+
if (userId.startsWith('@') && !userId.match(/<at\s+id="[^"]+"\s*\/>/)) {
|
|
883
|
+
logger.warn(`[用户ID] 检测到手动输入的@符号"${userId}",应使用真正的@功能`);
|
|
884
|
+
return ''; // 返回空字符串表示无效
|
|
885
|
+
}
|
|
886
|
+
// 处理 <at id="..."/> 格式的@用户字符串
|
|
887
|
+
const atMatch = userId.match(/<at id="(\d+)"\s*\/>/);
|
|
888
|
+
if (atMatch) {
|
|
889
|
+
extractedId = atMatch[1];
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
// 如果包含冒号,说明有平台前缀(如 onebot:123456)
|
|
893
|
+
const colonIndex = userId.indexOf(':');
|
|
894
|
+
if (colonIndex !== -1) {
|
|
895
|
+
extractedId = userId.substring(colonIndex + 1);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
extractedId = userId;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// 验证提取的ID是否为纯数字QQ号
|
|
902
|
+
if (!/^\d+$/.test(extractedId)) {
|
|
903
|
+
logger.warn(`[用户ID] 提取的ID"${extractedId}"不是有效的QQ号(必须为纯数字),来源: ${userId}`);
|
|
904
|
+
return ''; // 返回空字符串表示无效
|
|
905
|
+
}
|
|
906
|
+
// 检查QQ号长度是否合理(QQ号通常为5-12位数字)
|
|
907
|
+
if (extractedId.length < 5 || extractedId.length > 12) {
|
|
908
|
+
logger.warn(`[用户ID] QQ号"${extractedId}"长度异常(${extractedId.length}位),有效范围为5-12位`);
|
|
909
|
+
return '';
|
|
910
|
+
}
|
|
911
|
+
return extractedId;
|
|
912
|
+
};
|
|
913
|
+
// 获取用户友好的错误信息
|
|
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
|
+
// 封装发送消息的函数,处理私聊和群聊的不同格式
|
|
997
|
+
const sendMessage = async (session, content, options) => {
|
|
998
|
+
try {
|
|
999
|
+
if (!session) {
|
|
1000
|
+
logError('消息', 'system', '无效的会话对象');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
// 检查是否为群聊消息
|
|
1004
|
+
const isGroupMessage = session.channelId && !session.channelId.startsWith('private:');
|
|
1005
|
+
const normalizedQQId = normalizeQQId(session.userId);
|
|
1006
|
+
const isProactiveMessage = options?.isProactiveMessage || false;
|
|
1007
|
+
// 处理私聊和群聊的消息格式
|
|
1008
|
+
// 主动消息不引用原消息
|
|
1009
|
+
const promptMessage = session.channelId?.startsWith('private:')
|
|
1010
|
+
? (isProactiveMessage ? content : [koishi_1.h.quote(session.messageId), ...content])
|
|
1011
|
+
: (isProactiveMessage ? [koishi_1.h.at(normalizedQQId), '\n', ...content] : [koishi_1.h.quote(session.messageId), koishi_1.h.at(normalizedQQId), '\n', ...content]);
|
|
1012
|
+
// 发送消息并获取返回的消息ID
|
|
1013
|
+
const messageResult = await session.send(promptMessage);
|
|
1014
|
+
if (config.debugMode) {
|
|
1015
|
+
logDebug('消息', `成功向QQ(${normalizedQQId})发送消息,频道: ${session.channelId}`);
|
|
1016
|
+
}
|
|
1017
|
+
// 只在自动撤回时间大于0和存在bot对象时处理撤回
|
|
1018
|
+
if (config.autoRecallTime > 0 && session.bot) {
|
|
1019
|
+
// 处理撤回用户消息 - 只在群聊中且开启了用户消息撤回时
|
|
1020
|
+
// 但如果用户在绑定会话中发送聊天消息(不包括指令),不撤回
|
|
1021
|
+
// 主动消息不撤回用户消息
|
|
1022
|
+
const bindingSession = getBindingSession(session.userId, session.channelId);
|
|
1023
|
+
const isBindingCommand = session.content && (session.content.trim() === '绑定' ||
|
|
1024
|
+
session.content.includes('@') && session.content.includes('绑定'));
|
|
1025
|
+
const shouldNotRecallUserMessage = bindingSession && session.content &&
|
|
1026
|
+
!isBindingCommand && checkIrrelevantInput(bindingSession, session.content.trim());
|
|
1027
|
+
if (config.recallUserMessage && isGroupMessage && session.messageId && !shouldNotRecallUserMessage && !isProactiveMessage) {
|
|
1028
|
+
setTimeout(async () => {
|
|
1029
|
+
try {
|
|
1030
|
+
await session.bot.deleteMessage(session.channelId, session.messageId);
|
|
1031
|
+
if (config.debugMode) {
|
|
1032
|
+
logDebug('消息', `成功撤回用户QQ(${normalizedQQId})的指令消息 ${session.messageId}`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
catch (userRecallError) {
|
|
1036
|
+
logError('消息', normalizedQQId, `撤回用户指令消息 ${session.messageId} 失败: ${userRecallError.message}`);
|
|
1037
|
+
}
|
|
1038
|
+
}, config.autoRecallTime * 1000);
|
|
1039
|
+
if (config.debugMode) {
|
|
1040
|
+
logDebug('消息', `已设置 ${config.autoRecallTime} 秒后自动撤回用户QQ(${normalizedQQId})的群聊指令消息 ${session.messageId}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
else if (shouldNotRecallUserMessage && config.debugMode) {
|
|
1044
|
+
logDebug('消息', `QQ(${normalizedQQId})在绑定会话中发送聊天消息,跳过撤回用户消息`);
|
|
1045
|
+
}
|
|
1046
|
+
else if (isProactiveMessage && config.debugMode) {
|
|
1047
|
+
logDebug('消息', `主动发送的消息,跳过撤回用户消息`);
|
|
1048
|
+
}
|
|
1049
|
+
// 处理撤回机器人消息 - 只在群聊中撤回机器人自己的消息
|
|
1050
|
+
// 检查是否为不应撤回的重要提示消息(只有绑定会话超时提醒)
|
|
1051
|
+
const shouldNotRecall = content.some(element => {
|
|
1052
|
+
// 检查h.text类型的元素
|
|
1053
|
+
if (typeof element === 'string') {
|
|
1054
|
+
return element.includes('绑定会话已超时,请重新开始绑定流程');
|
|
1055
|
+
}
|
|
1056
|
+
// 检查可能的对象结构
|
|
1057
|
+
if (typeof element === 'object' && element && 'toString' in element) {
|
|
1058
|
+
const text = element.toString();
|
|
1059
|
+
return text.includes('绑定会话已超时,请重新开始绑定流程');
|
|
1060
|
+
}
|
|
1061
|
+
return false;
|
|
1062
|
+
});
|
|
1063
|
+
if (isGroupMessage && messageResult && !shouldNotRecall) {
|
|
1064
|
+
// 获取消息ID
|
|
1065
|
+
let messageId;
|
|
1066
|
+
if (typeof messageResult === 'string') {
|
|
1067
|
+
messageId = messageResult;
|
|
1068
|
+
}
|
|
1069
|
+
else if (Array.isArray(messageResult) && messageResult.length > 0) {
|
|
1070
|
+
messageId = messageResult[0];
|
|
1071
|
+
}
|
|
1072
|
+
else if (messageResult && typeof messageResult === 'object') {
|
|
1073
|
+
// 尝试提取各种可能的消息ID格式
|
|
1074
|
+
messageId = messageResult.messageId ||
|
|
1075
|
+
messageResult.id ||
|
|
1076
|
+
messageResult.message_id;
|
|
1077
|
+
}
|
|
1078
|
+
if (messageId) {
|
|
1079
|
+
// 设置定时器延迟撤回
|
|
1080
|
+
setTimeout(async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
await session.bot.deleteMessage(session.channelId, messageId);
|
|
1083
|
+
if (config.debugMode) {
|
|
1084
|
+
logDebug('消息', `成功撤回机器人消息 ${messageId}`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
catch (recallError) {
|
|
1088
|
+
logError('消息', normalizedQQId, `撤回机器人消息 ${messageId} 失败: ${recallError.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
}, config.autoRecallTime * 1000);
|
|
1091
|
+
if (config.debugMode) {
|
|
1092
|
+
logDebug('消息', `已设置 ${config.autoRecallTime} 秒后自动撤回机器人消息 ${messageId}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
else if (config.debugMode) {
|
|
1096
|
+
logWarn('消息', `无法获取消息ID,自动撤回功能无法生效`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
else if (config.debugMode) {
|
|
1100
|
+
logDebug('消息', `检测到私聊消息,不撤回机器人回复`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
catch (error) {
|
|
1105
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
1106
|
+
logError('消息', normalizedUserId, `向QQ(${normalizedUserId})发送消息失败: ${error.message}`);
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
// 检查冷却时间
|
|
1110
|
+
const checkCooldown = (lastModified, multiplier = 1) => {
|
|
1111
|
+
if (!lastModified)
|
|
1112
|
+
return true;
|
|
1113
|
+
const now = new Date();
|
|
1114
|
+
const diffTime = now.getTime() - lastModified.getTime();
|
|
1115
|
+
// 使用Math.floor确保冷却时间精确
|
|
1116
|
+
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
1117
|
+
return diffDays >= config.cooldownDays * multiplier;
|
|
1118
|
+
};
|
|
1119
|
+
// 根据QQ号查询MCIDBIND表中的绑定信息
|
|
1120
|
+
const getMcBindByQQId = async (qqId) => {
|
|
1121
|
+
try {
|
|
1122
|
+
// 处理空值
|
|
1123
|
+
if (!qqId) {
|
|
1124
|
+
logger.warn(`[MCIDBIND] 尝试查询空QQ号`);
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
const normalizedQQId = normalizeQQId(qqId);
|
|
1128
|
+
// 查询MCIDBIND表中对应QQ号的绑定记录
|
|
1129
|
+
return await mcidbindRepo.findByQQId(normalizedQQId);
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
logError('MCIDBIND', qqId, `根据QQ号查询绑定信息失败: ${error.message}`);
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
// 根据MC用户名查询MCIDBIND表中的绑定信息
|
|
1137
|
+
const getMcBindByUsername = async (mcUsername) => {
|
|
1138
|
+
// 处理空值
|
|
1139
|
+
if (!mcUsername) {
|
|
1140
|
+
logger.warn(`[MCIDBIND] 尝试查询空MC用户名`);
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
// 使用 Repository 查询
|
|
1144
|
+
return await mcidbindRepo.findByMCUsername(mcUsername);
|
|
1145
|
+
};
|
|
1146
|
+
// 根据QQ号确保获取完整的用户ID (处理纯QQ号的情况)
|
|
1147
|
+
const ensureFullUserId = (userId) => {
|
|
1148
|
+
// 如果已经包含冒号,说明已经是完整的用户ID
|
|
1149
|
+
if (userId.includes(':'))
|
|
1150
|
+
return userId;
|
|
1151
|
+
// 否则,检查是否为数字(纯QQ号)
|
|
1152
|
+
if (/^\d+$/.test(userId)) {
|
|
1153
|
+
// 默认使用onebot平台前缀
|
|
1154
|
+
return `onebot:${userId}`;
|
|
1155
|
+
}
|
|
1156
|
+
// 如果不是数字也没有冒号,保持原样返回
|
|
1157
|
+
logger.warn(`[用户ID] 无法确定用户ID格式: ${userId}`);
|
|
1158
|
+
return userId;
|
|
1159
|
+
};
|
|
1160
|
+
// 创建或更新MCIDBIND表中的绑定信息
|
|
1161
|
+
const createOrUpdateMcBind = async (userId, mcUsername, mcUuid, isAdmin) => {
|
|
1162
|
+
try {
|
|
1163
|
+
// 验证输入参数
|
|
1164
|
+
if (!userId) {
|
|
1165
|
+
logger.error(`[MCIDBIND] 创建/更新绑定失败: 无效的用户ID`);
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
if (!mcUsername) {
|
|
1169
|
+
logger.error(`[MCIDBIND] 创建/更新绑定失败: 无效的MC用户名`);
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
const normalizedQQId = normalizeQQId(userId);
|
|
1173
|
+
if (!normalizedQQId) {
|
|
1174
|
+
logger.error(`[MCIDBIND] 创建/更新绑定失败: 无法提取有效的QQ号`);
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
// 查询是否已存在绑定记录
|
|
1178
|
+
let bind = await getMcBindByQQId(normalizedQQId);
|
|
1179
|
+
if (bind) {
|
|
1180
|
+
// 更新现有记录,但保留管理员状态
|
|
1181
|
+
const updateData = {
|
|
1182
|
+
mcUsername,
|
|
1183
|
+
mcUuid,
|
|
1184
|
+
lastModified: new Date()
|
|
1185
|
+
};
|
|
1186
|
+
// 仅当指定了isAdmin参数时更新管理员状态
|
|
1187
|
+
if (typeof isAdmin !== 'undefined') {
|
|
1188
|
+
updateData.isAdmin = isAdmin;
|
|
1189
|
+
}
|
|
1190
|
+
await mcidbindRepo.update(normalizedQQId, updateData);
|
|
1191
|
+
logger.info(`[MCIDBIND] 更新绑定: QQ=${normalizedQQId}, MC用户名=${mcUsername}, UUID=${mcUuid}`);
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
else {
|
|
1195
|
+
// 创建新记录
|
|
1196
|
+
try {
|
|
1197
|
+
await mcidbindRepo.create({
|
|
1198
|
+
qqId: normalizedQQId,
|
|
1199
|
+
mcUsername,
|
|
1200
|
+
mcUuid,
|
|
1201
|
+
lastModified: new Date(),
|
|
1202
|
+
isAdmin: isAdmin || false
|
|
1203
|
+
});
|
|
1204
|
+
logger.info(`[MCIDBIND] 创建绑定: QQ=${normalizedQQId}, MC用户名=${mcUsername}, UUID=${mcUuid}`);
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
catch (createError) {
|
|
1208
|
+
logError('MCIDBIND', userId, `创建绑定失败: MC用户名=${mcUsername}, 错误=${createError.message}`);
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
logError('MCIDBIND', userId, `创建/更新绑定失败: MC用户名=${mcUsername}, 错误=${error.message}`);
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
// 删除MCIDBIND表中的绑定信息 (同时解绑MC和B站账号)
|
|
1219
|
+
const deleteMcBind = async (userId) => {
|
|
1220
|
+
try {
|
|
1221
|
+
// 验证输入参数
|
|
1222
|
+
if (!userId) {
|
|
1223
|
+
logger.error(`[MCIDBIND] 删除绑定失败: 无效的用户ID`);
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
const normalizedQQId = normalizeQQId(userId);
|
|
1227
|
+
if (!normalizedQQId) {
|
|
1228
|
+
logger.error(`[MCIDBIND] 删除绑定失败: 无法提取有效的QQ号`);
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
// 查询是否存在绑定记录
|
|
1232
|
+
const bind = await getMcBindByQQId(normalizedQQId);
|
|
1233
|
+
if (bind) {
|
|
1234
|
+
// 删除整个绑定记录,包括MC和B站账号
|
|
1235
|
+
const removedCount = await mcidbindRepo.delete(normalizedQQId);
|
|
1236
|
+
// 检查是否真正删除成功
|
|
1237
|
+
if (removedCount > 0) {
|
|
1238
|
+
let logMessage = `[MCIDBIND] 删除绑定: QQ=${normalizedQQId}`;
|
|
1239
|
+
if (bind.mcUsername)
|
|
1240
|
+
logMessage += `, MC用户名=${bind.mcUsername}`;
|
|
1241
|
+
if (bind.buidUid)
|
|
1242
|
+
logMessage += `, B站UID=${bind.buidUid}(${bind.buidUsername})`;
|
|
1243
|
+
logger.info(logMessage);
|
|
1244
|
+
return true;
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
logger.warn(`[MCIDBIND] 删除绑定异常: QQ=${normalizedQQId}, 可能未实际删除`);
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
logger.warn(`[MCIDBIND] 删除绑定失败: QQ=${normalizedQQId}不存在绑定记录`);
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
catch (error) {
|
|
1255
|
+
logError('MCIDBIND', userId, `删除绑定失败: 错误=${error.message}`);
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
// 检查MC用户名是否已被其他QQ号绑定
|
|
1260
|
+
const checkUsernameExists = async (username, currentUserId) => {
|
|
1261
|
+
try {
|
|
1262
|
+
// 验证输入参数
|
|
1263
|
+
if (!username) {
|
|
1264
|
+
logger.warn(`[绑定检查] 尝试检查空MC用户名`);
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
// 跳过临时用户名的检查
|
|
1268
|
+
if (username.startsWith('_temp_')) {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
// 查询新表中是否已有此用户名的绑定
|
|
1272
|
+
const bind = await getMcBindByUsername(username);
|
|
1273
|
+
// 如果没有绑定,返回false
|
|
1274
|
+
if (!bind)
|
|
1275
|
+
return false;
|
|
1276
|
+
// 如果绑定的用户名是临时用户名,视为未绑定
|
|
1277
|
+
if (bind.mcUsername && bind.mcUsername.startsWith('_temp_')) {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
// 如果提供了当前用户ID,需要排除当前用户
|
|
1281
|
+
if (currentUserId) {
|
|
1282
|
+
const normalizedCurrentId = normalizeQQId(currentUserId);
|
|
1283
|
+
// 如果绑定的用户就是当前用户,返回false,表示没有被其他用户绑定
|
|
1284
|
+
return normalizedCurrentId ? bind.qqId !== normalizedCurrentId : true;
|
|
1285
|
+
}
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
logError('绑定检查', currentUserId || 'system', `检查用户名"${username}"是否已被绑定失败: ${error.message}`);
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
// 使用Mojang API验证用户名并获取UUID
|
|
1294
|
+
const validateUsername = async (username) => {
|
|
1295
|
+
try {
|
|
1296
|
+
logger.debug(`[Mojang API] 开始验证用户名: ${username}`);
|
|
1297
|
+
const response = await axios_1.default.get(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
|
|
1298
|
+
timeout: 10000, // 添加10秒超时
|
|
1299
|
+
headers: {
|
|
1300
|
+
'User-Agent': 'KoishiMCVerifier/1.0', // 添加User-Agent头
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
if (response.status === 200 && response.data) {
|
|
1304
|
+
logger.debug(`[Mojang API] 用户名"${username}"验证成功,UUID: ${response.data.id},标准名称: ${response.data.name}`);
|
|
1305
|
+
return {
|
|
1306
|
+
id: response.data.id,
|
|
1307
|
+
name: response.data.name // 使用Mojang返回的正确大小写
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
if (axios_1.default.isAxiosError(error) && error.response?.status === 404) {
|
|
1314
|
+
logger.warn(`[Mojang API] 用户名"${username}"不存在`);
|
|
1315
|
+
}
|
|
1316
|
+
else if (axios_1.default.isAxiosError(error) && error.code === 'ECONNABORTED') {
|
|
1317
|
+
logger.error(`[Mojang API] 验证用户名"${username}"时请求超时: ${error.message}`);
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
// 记录更详细的错误信息
|
|
1321
|
+
const errorMessage = axios_1.default.isAxiosError(error)
|
|
1322
|
+
? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
|
|
1323
|
+
: error.message || '未知错误';
|
|
1324
|
+
logger.error(`[Mojang API] 验证用户名"${username}"时发生错误: ${errorMessage}`);
|
|
1325
|
+
// 如果是网络相关错误,尝试使用备用API检查
|
|
1326
|
+
if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
|
|
1327
|
+
error.code === 'ETIMEDOUT' ||
|
|
1328
|
+
error.code === 'ECONNRESET' ||
|
|
1329
|
+
error.code === 'ECONNREFUSED' ||
|
|
1330
|
+
error.code === 'ECONNABORTED' ||
|
|
1331
|
+
error.response?.status === 429 || // 添加429 (Too Many Requests)
|
|
1332
|
+
error.response?.status === 403)) { // 添加403 (Forbidden)
|
|
1333
|
+
// 尝试使用playerdb.co作为备用API
|
|
1334
|
+
logger.info(`[Mojang API] 遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
|
|
1335
|
+
return tryBackupAPI(username);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
// 使用备用API验证用户名
|
|
1342
|
+
const tryBackupAPI = async (username) => {
|
|
1343
|
+
logger.info(`[备用API] 尝试使用备用API验证用户名"${username}"`);
|
|
1344
|
+
try {
|
|
1345
|
+
// 使用playerdb.co作为备用API
|
|
1346
|
+
const backupResponse = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${username}`, {
|
|
1347
|
+
timeout: 10000,
|
|
1348
|
+
headers: {
|
|
1349
|
+
'User-Agent': 'KoishiMCVerifier/1.0'
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
if (backupResponse.status === 200 && backupResponse.data?.code === "player.found") {
|
|
1353
|
+
const playerData = backupResponse.data.data.player;
|
|
1354
|
+
const rawId = playerData.raw_id || playerData.id.replace(/-/g, ''); // 确保使用不带连字符的UUID
|
|
1355
|
+
logger.info(`[备用API] 用户名"${username}"验证成功,UUID: ${rawId},标准名称: ${playerData.username}`);
|
|
1356
|
+
return {
|
|
1357
|
+
id: rawId, // 确保使用不带连字符的UUID
|
|
1358
|
+
name: playerData.username
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
logger.warn(`[备用API] 用户名"${username}"验证失败: ${JSON.stringify(backupResponse.data)}`);
|
|
1362
|
+
return null;
|
|
1363
|
+
}
|
|
1364
|
+
catch (backupError) {
|
|
1365
|
+
const errorMsg = axios_1.default.isAxiosError(backupError)
|
|
1366
|
+
? `${backupError.message}, 状态码: ${backupError.response?.status || '未知'}`
|
|
1367
|
+
: backupError.message || '未知错误';
|
|
1368
|
+
logger.error(`[备用API] 验证用户名"${username}"失败: ${errorMsg}`);
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
// 获取MC头图URL
|
|
1373
|
+
const getCrafatarUrl = (uuid) => {
|
|
1374
|
+
if (!uuid)
|
|
1375
|
+
return null;
|
|
1376
|
+
// 检查UUID格式 (不带连字符应为32位,带连字符应为36位)
|
|
1377
|
+
const uuidRegex = /^[0-9a-f]{32}$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1378
|
+
if (!uuidRegex.test(uuid)) {
|
|
1379
|
+
logger.warn(`[MC头图] UUID "${uuid}" 格式无效,无法生成头图URL`);
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
// 移除任何连字符,Crafatar接受不带连字符的UUID
|
|
1383
|
+
const cleanUuid = uuid.replace(/-/g, '');
|
|
1384
|
+
// 直接生成URL
|
|
1385
|
+
const url = `https://crafatar.com/avatars/${cleanUuid}`;
|
|
1386
|
+
logger.debug(`[MC头图] 为UUID "${cleanUuid}" 生成头图URL`);
|
|
1387
|
+
return url;
|
|
1388
|
+
};
|
|
1389
|
+
// 使用Starlight SkinAPI获取皮肤渲染
|
|
1390
|
+
const getStarlightSkinUrl = (username) => {
|
|
1391
|
+
if (!username)
|
|
1392
|
+
return null;
|
|
1393
|
+
// 可用的动作列表 (共16种)
|
|
1394
|
+
const poses = [
|
|
1395
|
+
'default', // 默认站立
|
|
1396
|
+
'marching', // 行军
|
|
1397
|
+
'walking', // 行走
|
|
1398
|
+
'crouching', // 下蹲
|
|
1399
|
+
'crossed', // 交叉手臂
|
|
1400
|
+
'crisscross', // 交叉腿
|
|
1401
|
+
'cheering', // 欢呼
|
|
1402
|
+
'relaxing', // 放松
|
|
1403
|
+
'trudging', // 艰难行走
|
|
1404
|
+
'cowering', // 退缩
|
|
1405
|
+
'pointing', // 指向
|
|
1406
|
+
'lunging', // 前冲
|
|
1407
|
+
'dungeons', // 地下城风格
|
|
1408
|
+
'facepalm', // 捂脸
|
|
1409
|
+
'mojavatar', // Mojave姿态
|
|
1410
|
+
'head', // 头部特写
|
|
1411
|
+
];
|
|
1412
|
+
// 随机选择一个动作
|
|
1413
|
+
const randomPose = poses[Math.floor(Math.random() * poses.length)];
|
|
1414
|
+
// 视图类型(full为全身图)
|
|
1415
|
+
const viewType = 'full';
|
|
1416
|
+
// 生成URL
|
|
1417
|
+
const url = `https://starlightskins.lunareclipse.studio/render/${randomPose}/${username}/${viewType}`;
|
|
1418
|
+
logger.debug(`[Starlight皮肤] 为用户名"${username}"生成动作"${randomPose}"的渲染URL`);
|
|
1419
|
+
return url;
|
|
1420
|
+
};
|
|
1421
|
+
// 格式化UUID (添加连字符,使其符合标准格式)
|
|
1422
|
+
const formatUuid = (uuid) => {
|
|
1423
|
+
if (!uuid)
|
|
1424
|
+
return '未知';
|
|
1425
|
+
if (uuid.includes('-'))
|
|
1426
|
+
return uuid; // 已经是带连字符的格式
|
|
1427
|
+
// 确保UUID长度正确
|
|
1428
|
+
if (uuid.length !== 32) {
|
|
1429
|
+
logger.warn(`[UUID] UUID "${uuid}" 长度异常,无法格式化`);
|
|
1430
|
+
return uuid;
|
|
1431
|
+
}
|
|
1432
|
+
return `${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}`;
|
|
1433
|
+
};
|
|
1434
|
+
// 检查是否为管理员 (QQ号作为主键检查)
|
|
1435
|
+
const isAdmin = async (userId) => {
|
|
1436
|
+
// 主人始终是管理员
|
|
1437
|
+
const normalizedMasterId = normalizeQQId(config.masterId);
|
|
1438
|
+
const normalizedQQId = normalizeQQId(userId);
|
|
1439
|
+
if (normalizedQQId === normalizedMasterId)
|
|
1440
|
+
return true;
|
|
1441
|
+
// 查询MCIDBIND表中是否是管理员
|
|
1442
|
+
try {
|
|
1443
|
+
const bind = await getMcBindByQQId(normalizedQQId);
|
|
1444
|
+
return bind && bind.isAdmin === true;
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
logger.error(`[权限检查] QQ(${normalizedQQId})的管理员状态查询失败: ${error.message}`);
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
// 检查是否为主人 (QQ号作为主键检查)
|
|
1452
|
+
const isMaster = (qqId) => {
|
|
1453
|
+
const normalizedMasterId = normalizeQQId(config.masterId);
|
|
1454
|
+
const normalizedQQId = normalizeQQId(qqId);
|
|
1455
|
+
return normalizedQQId === normalizedMasterId;
|
|
1456
|
+
};
|
|
1457
|
+
// =========== BUID相关功能 ===========
|
|
1458
|
+
// 验证BUID是否存在
|
|
1459
|
+
const validateBUID = async (buid) => {
|
|
1460
|
+
try {
|
|
1461
|
+
if (!buid || !/^\d+$/.test(buid)) {
|
|
1462
|
+
logWarn('B站账号验证', `无效的B站UID格式: ${buid}`);
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
logDebug('B站账号验证', `验证B站UID: ${buid}`);
|
|
1466
|
+
const response = await axios_1.default.get(`${config.zminfoApiUrl}/api/user/${buid}`, {
|
|
1467
|
+
timeout: 10000,
|
|
1468
|
+
headers: {
|
|
1469
|
+
'User-Agent': 'Koishi-MCID-Bot/1.0'
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
if (response.data.success && response.data.data && response.data.data.user) {
|
|
1473
|
+
const user = response.data.data.user;
|
|
1474
|
+
logDebug('B站账号验证', `B站UID ${buid} 验证成功: ${user.username}`);
|
|
1475
|
+
return user;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
logWarn('B站账号验证', `B站UID ${buid} 不存在或API返回失败: ${response.data.message}`);
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
if (error.response?.status === 404) {
|
|
1484
|
+
logWarn('B站账号验证', `B站UID ${buid} 不存在`);
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
logError('B站账号验证', 'system', `验证B站UID ${buid} 时出错: ${error.message}`);
|
|
1488
|
+
throw new Error(`无法验证B站UID: ${error.message}`);
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
// 根据B站UID查询绑定信息
|
|
1492
|
+
const getBuidBindByBuid = async (buid) => {
|
|
1493
|
+
try {
|
|
1494
|
+
if (!buid) {
|
|
1495
|
+
logger.warn(`[B站账号绑定] 尝试查询空B站UID`);
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
const bind = await mcidbindRepo.findByBuidUid(buid);
|
|
1499
|
+
return bind;
|
|
1500
|
+
}
|
|
1501
|
+
catch (error) {
|
|
1502
|
+
logError('B站账号绑定', 'system', `根据B站UID(${buid})查询绑定信息失败: ${error.message}`);
|
|
1503
|
+
return null;
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
// 检查B站UID是否已被绑定
|
|
1507
|
+
const checkBuidExists = async (buid, currentUserId) => {
|
|
1508
|
+
try {
|
|
1509
|
+
const bind = await getBuidBindByBuid(buid);
|
|
1510
|
+
if (!bind)
|
|
1511
|
+
return false;
|
|
1512
|
+
// 如果指定了当前用户ID,则排除当前用户的绑定
|
|
1513
|
+
if (currentUserId) {
|
|
1514
|
+
const normalizedCurrentId = normalizeQQId(currentUserId);
|
|
1515
|
+
return bind.qqId !== normalizedCurrentId;
|
|
1516
|
+
}
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
logError('B站账号绑定', 'system', `检查B站UID(${buid})是否存在时出错: ${error.message}`);
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
// 创建或更新B站账号绑定
|
|
1525
|
+
const createOrUpdateBuidBind = async (userId, buidUser) => {
|
|
1526
|
+
try {
|
|
1527
|
+
const normalizedQQId = normalizeQQId(userId);
|
|
1528
|
+
if (!normalizedQQId) {
|
|
1529
|
+
logger.error(`[B站账号绑定] 创建/更新绑定失败: 无法提取有效的QQ号`);
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
// 检查该UID是否已被其他用户绑定(安全检查)
|
|
1533
|
+
const existingBuidBind = await getBuidBindByBuid(buidUser.uid);
|
|
1534
|
+
if (existingBuidBind && existingBuidBind.qqId !== normalizedQQId) {
|
|
1535
|
+
logger.error(`[B站账号绑定] 安全检查失败: B站UID ${buidUser.uid} 已被QQ(${existingBuidBind.qqId})绑定,无法为QQ(${normalizedQQId})绑定`);
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
// 查询是否已存在绑定记录
|
|
1539
|
+
let bind = await getMcBindByQQId(normalizedQQId);
|
|
1540
|
+
const updateData = {
|
|
1541
|
+
buidUid: buidUser.uid,
|
|
1542
|
+
buidUsername: buidUser.username,
|
|
1543
|
+
guardLevel: buidUser.guard_level || 0,
|
|
1544
|
+
guardLevelText: buidUser.guard_level_text || '',
|
|
1545
|
+
maxGuardLevel: buidUser.max_guard_level || 0,
|
|
1546
|
+
maxGuardLevelText: buidUser.max_guard_level_text || '',
|
|
1547
|
+
medalName: buidUser.medal?.name || '',
|
|
1548
|
+
medalLevel: buidUser.medal?.level || 0,
|
|
1549
|
+
wealthMedalLevel: buidUser.wealthMedalLevel || 0,
|
|
1550
|
+
lastActiveTime: buidUser.last_active_time ? new Date(buidUser.last_active_time) : new Date(),
|
|
1551
|
+
lastModified: new Date()
|
|
1552
|
+
};
|
|
1553
|
+
if (bind) {
|
|
1554
|
+
await mcidbindRepo.update(normalizedQQId, updateData);
|
|
1555
|
+
logger.info(`[B站账号绑定] 更新绑定: QQ=${normalizedQQId}, B站UID=${buidUser.uid}, 用户名=${buidUser.username}`);
|
|
1556
|
+
}
|
|
1557
|
+
else {
|
|
1558
|
+
// 为跳过MC绑定的用户生成唯一的临时用户名,避免UNIQUE constraint冲突
|
|
1559
|
+
const tempMcUsername = `_temp_skip_${normalizedQQId}_${Date.now()}`;
|
|
1560
|
+
const newBind = {
|
|
1561
|
+
qqId: normalizedQQId,
|
|
1562
|
+
mcUsername: tempMcUsername,
|
|
1563
|
+
mcUuid: '',
|
|
1564
|
+
isAdmin: false,
|
|
1565
|
+
whitelist: [],
|
|
1566
|
+
tags: [],
|
|
1567
|
+
...updateData
|
|
1568
|
+
};
|
|
1569
|
+
await mcidbindRepo.create(newBind);
|
|
1570
|
+
logger.info(`[B站账号绑定] 创建绑定(跳过MC): QQ=${normalizedQQId}, B站UID=${buidUser.uid}, 用户名=${buidUser.username}, 临时MC用户名=${tempMcUsername}`);
|
|
1571
|
+
}
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
catch (error) {
|
|
1575
|
+
logError('B站账号绑定', userId, `创建/更新B站账号绑定失败: ${error.message}`);
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
// 仅更新B站信息,不更新绑定时间(用于查询时刷新数据)
|
|
1580
|
+
const updateBuidInfoOnly = async (userId, buidUser) => {
|
|
1581
|
+
try {
|
|
1582
|
+
const normalizedQQId = normalizeQQId(userId);
|
|
1583
|
+
if (!normalizedQQId) {
|
|
1584
|
+
logger.error(`[B站账号信息更新] 更新失败: 无法提取有效的QQ号`);
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
// 查询是否已存在绑定记录
|
|
1588
|
+
const bind = await getMcBindByQQId(normalizedQQId);
|
|
1589
|
+
if (!bind) {
|
|
1590
|
+
logger.warn(`[B站账号信息更新] QQ(${normalizedQQId})没有绑定记录,无法更新B站信息`);
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
// 仅更新B站相关字段,不更新lastModified
|
|
1594
|
+
const updateData = {
|
|
1595
|
+
buidUsername: buidUser.username,
|
|
1596
|
+
guardLevel: buidUser.guard_level || 0,
|
|
1597
|
+
guardLevelText: buidUser.guard_level_text || '',
|
|
1598
|
+
maxGuardLevel: buidUser.max_guard_level || 0,
|
|
1599
|
+
maxGuardLevelText: buidUser.max_guard_level_text || '',
|
|
1600
|
+
medalName: buidUser.medal?.name || '',
|
|
1601
|
+
medalLevel: buidUser.medal?.level || 0,
|
|
1602
|
+
wealthMedalLevel: buidUser.wealthMedalLevel || 0,
|
|
1603
|
+
lastActiveTime: buidUser.last_active_time ? new Date(buidUser.last_active_time) : new Date()
|
|
1604
|
+
};
|
|
1605
|
+
await mcidbindRepo.update(normalizedQQId, updateData);
|
|
1606
|
+
logger.info(`[B站账号信息更新] 刷新信息: QQ=${normalizedQQId}, B站UID=${bind.buidUid}, 用户名=${buidUser.username}`);
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
catch (error) {
|
|
1610
|
+
logError('B站账号信息更新', userId, `更新B站账号信息失败: ${error.message}`);
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
// =========== 辅助函数 ===========
|
|
1615
|
+
// RCON连接检查
|
|
1616
|
+
const checkRconConnections = async () => {
|
|
1617
|
+
if (!config.servers || config.servers.length === 0) {
|
|
1618
|
+
logger.info('[RCON检查] 未配置任何服务器,跳过RCON检查');
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const results = {};
|
|
1622
|
+
for (const server of config.servers) {
|
|
1623
|
+
try {
|
|
1624
|
+
logger.info(`[RCON检查] 正在检查服务器 ${server.name} (${server.rconAddress}) 的连接状态`);
|
|
1625
|
+
// 尝试执行/list命令来测试连接 (使用RCON管理器)
|
|
1626
|
+
await rconManager.executeCommand(server, 'list');
|
|
1627
|
+
// 如果没有抛出异常,表示连接成功
|
|
1628
|
+
logger.info(`[RCON检查] 服务器 ${server.name} 连接成功`);
|
|
1629
|
+
results[server.id] = true;
|
|
1630
|
+
}
|
|
1631
|
+
catch (error) {
|
|
1632
|
+
logger.error(`[RCON检查] 服务器 ${server.name} 连接失败: ${error.message}`);
|
|
1633
|
+
results[server.id] = false;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
// 生成检查结果摘要
|
|
1637
|
+
const totalServers = config.servers.length;
|
|
1638
|
+
const successCount = Object.values(results).filter(Boolean).length;
|
|
1639
|
+
const failCount = totalServers - successCount;
|
|
1640
|
+
logger.info(`[RCON检查] 检查完成: ${successCount}/${totalServers} 个服务器连接成功,${failCount} 个连接失败`);
|
|
1641
|
+
if (failCount > 0) {
|
|
1642
|
+
const failedServers = config.servers
|
|
1643
|
+
.filter(server => !results[server.id])
|
|
1644
|
+
.map(server => server.name)
|
|
1645
|
+
.join(', ');
|
|
1646
|
+
logger.warn(`[RCON检查] 以下服务器连接失败,白名单功能可能无法正常工作: ${failedServers}`);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
// 使用Mojang API通过UUID查询用户名
|
|
1650
|
+
const getUsernameByUuid = async (uuid) => {
|
|
1651
|
+
try {
|
|
1652
|
+
// 确保UUID格式正确(去除连字符)
|
|
1653
|
+
const cleanUuid = uuid.replace(/-/g, '');
|
|
1654
|
+
logger.debug(`[Mojang API] 通过UUID "${cleanUuid}" 查询用户名`);
|
|
1655
|
+
const response = await axios_1.default.get(`https://api.mojang.com/user/profile/${cleanUuid}`, {
|
|
1656
|
+
timeout: 10000,
|
|
1657
|
+
headers: {
|
|
1658
|
+
'User-Agent': 'KoishiMCVerifier/1.0',
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
if (response.status === 200 && response.data) {
|
|
1662
|
+
// 从返回数据中提取用户名
|
|
1663
|
+
const username = response.data.name;
|
|
1664
|
+
logger.debug(`[Mojang API] UUID "${cleanUuid}" 当前用户名: ${username}`);
|
|
1665
|
+
return username;
|
|
1666
|
+
}
|
|
1667
|
+
logger.warn(`[Mojang API] UUID "${cleanUuid}" 查询不到用户名`);
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
catch (error) {
|
|
1671
|
+
// 如果是网络相关错误,尝试使用备用API
|
|
1672
|
+
if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
|
|
1673
|
+
error.code === 'ETIMEDOUT' ||
|
|
1674
|
+
error.code === 'ECONNRESET' ||
|
|
1675
|
+
error.code === 'ECONNREFUSED' ||
|
|
1676
|
+
error.code === 'ECONNABORTED' ||
|
|
1677
|
+
error.response?.status === 429 || // 添加429 (Too Many Requests)
|
|
1678
|
+
error.response?.status === 403)) { // 添加403 (Forbidden)
|
|
1679
|
+
logger.info(`[Mojang API] 通过UUID查询用户名时遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
|
|
1680
|
+
return getUsernameByUuidBackupAPI(uuid);
|
|
1681
|
+
}
|
|
1682
|
+
const errorMessage = axios_1.default.isAxiosError(error)
|
|
1683
|
+
? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
|
|
1684
|
+
: error.message || '未知错误';
|
|
1685
|
+
logger.error(`[Mojang API] 通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
// 使用备用API通过UUID查询用户名
|
|
1690
|
+
const getUsernameByUuidBackupAPI = async (uuid) => {
|
|
1691
|
+
try {
|
|
1692
|
+
// 确保UUID格式正确,备用API支持带连字符的UUID
|
|
1693
|
+
const formattedUuid = uuid.includes('-') ? uuid : formatUuid(uuid);
|
|
1694
|
+
logger.debug(`[备用API] 通过UUID "${formattedUuid}" 查询用户名`);
|
|
1695
|
+
const response = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${formattedUuid}`, {
|
|
1696
|
+
timeout: 10000,
|
|
1697
|
+
headers: {
|
|
1698
|
+
'User-Agent': 'KoishiMCVerifier/1.0',
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
if (response.status === 200 && response.data?.code === "player.found") {
|
|
1702
|
+
const playerData = response.data.data.player;
|
|
1703
|
+
logger.debug(`[备用API] UUID "${formattedUuid}" 当前用户名: ${playerData.username}`);
|
|
1704
|
+
return playerData.username;
|
|
1705
|
+
}
|
|
1706
|
+
logger.warn(`[备用API] UUID "${formattedUuid}" 查询不到用户名: ${JSON.stringify(response.data)}`);
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
catch (error) {
|
|
1710
|
+
const errorMessage = axios_1.default.isAxiosError(error)
|
|
1711
|
+
? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
|
|
1712
|
+
: error.message || '未知错误';
|
|
1713
|
+
logger.error(`[备用API] 通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
// 检查并更新用户名(如果与当前数据库中的不同)
|
|
1718
|
+
const checkAndUpdateUsername = async (bind) => {
|
|
1719
|
+
try {
|
|
1720
|
+
if (!bind || !bind.mcUuid) {
|
|
1721
|
+
logger.warn(`[用户名更新] 无法检查用户名更新: 空绑定或空UUID`);
|
|
1722
|
+
return bind;
|
|
1723
|
+
}
|
|
1724
|
+
// 通过UUID查询最新用户名
|
|
1725
|
+
const latestUsername = await getUsernameByUuid(bind.mcUuid);
|
|
1726
|
+
if (!latestUsername) {
|
|
1727
|
+
logger.warn(`[用户名更新] 无法获取UUID "${bind.mcUuid}" 的最新用户名`);
|
|
1728
|
+
return bind;
|
|
1729
|
+
}
|
|
1730
|
+
// 如果用户名与数据库中的不同,更新数据库
|
|
1731
|
+
if (latestUsername !== bind.mcUsername) {
|
|
1732
|
+
logger.info(`[用户名更新] 用户 QQ(${bind.qqId}) 的Minecraft用户名已变更: ${bind.mcUsername} -> ${latestUsername}`);
|
|
1733
|
+
// 更新数据库中的用户名
|
|
1734
|
+
await ctx.database.set('mcidbind', { qqId: bind.qqId }, {
|
|
1735
|
+
mcUsername: latestUsername
|
|
1736
|
+
});
|
|
1737
|
+
// 更新返回的绑定对象
|
|
1738
|
+
bind.mcUsername = latestUsername;
|
|
1739
|
+
}
|
|
1740
|
+
return bind;
|
|
1741
|
+
}
|
|
1742
|
+
catch (error) {
|
|
1743
|
+
logger.error(`[用户名更新] 检查和更新用户名失败: ${error.message}`);
|
|
1744
|
+
return bind;
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
// 安全地替换命令模板
|
|
1748
|
+
const safeCommandReplace = (template, mcid) => {
|
|
1749
|
+
// 过滤可能导致命令注入的字符
|
|
1750
|
+
const sanitizedMcid = mcid.replace(/[;&|"`'$\\]/g, '');
|
|
1751
|
+
// 如果经过过滤后的mcid与原始mcid不同,记录警告
|
|
1752
|
+
if (sanitizedMcid !== mcid) {
|
|
1753
|
+
logger.warn(`[安全] 检测到潜在危险字符,已自动过滤: '${mcid}' -> '${sanitizedMcid}'`);
|
|
1754
|
+
}
|
|
1755
|
+
return template.replace(/\${MCID}/g, sanitizedMcid);
|
|
1756
|
+
};
|
|
1757
|
+
// 根据服务器ID获取服务器配置
|
|
1758
|
+
const getServerConfigById = (serverId) => {
|
|
1759
|
+
if (!config.servers || !Array.isArray(config.servers))
|
|
1760
|
+
return null;
|
|
1761
|
+
return config.servers.find(server => server.id === serverId && (server.enabled !== false)) || null;
|
|
1762
|
+
};
|
|
1763
|
+
// 根据服务器名称获取服务器配置
|
|
1764
|
+
const getServerConfigByName = (serverName) => {
|
|
1765
|
+
if (!config.servers || !Array.isArray(config.servers))
|
|
1766
|
+
return null;
|
|
1767
|
+
// 过滤出启用的服务器
|
|
1768
|
+
const enabledServers = config.servers.filter(server => server.enabled !== false);
|
|
1769
|
+
// 尝试精确匹配
|
|
1770
|
+
let server = enabledServers.find(server => server.name === serverName);
|
|
1771
|
+
// 如果精确匹配失败,尝试模糊匹配
|
|
1772
|
+
if (!server) {
|
|
1773
|
+
const lowerServerName = serverName.toLowerCase().trim();
|
|
1774
|
+
// 最小相似度阈值,低于此值的匹配结果将被忽略
|
|
1775
|
+
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
|
+
// 查找最相似的服务器名称
|
|
1807
|
+
let bestMatch = null;
|
|
1808
|
+
let bestSimilarity = 0;
|
|
1809
|
+
for (const s of enabledServers) {
|
|
1810
|
+
const similarity = calculateSimilarity(lowerServerName, s.name.toLowerCase().trim());
|
|
1811
|
+
if (similarity > bestSimilarity && similarity >= MIN_SIMILARITY) {
|
|
1812
|
+
bestSimilarity = similarity;
|
|
1813
|
+
bestMatch = s;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (bestMatch && bestSimilarity < 1) {
|
|
1817
|
+
logger.info(`[服务器配置] 模糊匹配成功: "${serverName}" -> "${bestMatch.name}" (相似度: ${(bestSimilarity * 100).toFixed(1)}%)`);
|
|
1818
|
+
}
|
|
1819
|
+
server = bestMatch;
|
|
1820
|
+
}
|
|
1821
|
+
return server || null;
|
|
1822
|
+
};
|
|
1823
|
+
// 根据服务器ID或名称获取服务器配置
|
|
1824
|
+
const getServerConfigByIdOrName = (serverIdOrName) => {
|
|
1825
|
+
if (!config.servers || !Array.isArray(config.servers))
|
|
1826
|
+
return null;
|
|
1827
|
+
// 先尝试通过ID精确匹配
|
|
1828
|
+
const serverById = getServerConfigById(serverIdOrName);
|
|
1829
|
+
if (serverById)
|
|
1830
|
+
return serverById;
|
|
1831
|
+
// 如果ID未匹配到,尝试通过名称匹配
|
|
1832
|
+
return getServerConfigByName(serverIdOrName);
|
|
1833
|
+
};
|
|
1834
|
+
// =========== Handler 服务实例创建 ===========
|
|
1835
|
+
// 创建 MessageUtils 实例(暂时使用null,因为MessageUtils需要重构)
|
|
1836
|
+
const messageUtils = null;
|
|
1837
|
+
// 创建 ForceBinder 实例
|
|
1838
|
+
const forceBindConfig = {
|
|
1839
|
+
SESSDATA: config.forceBindSessdata,
|
|
1840
|
+
zminfoApiUrl: config.zminfoApiUrl,
|
|
1841
|
+
targetUpUid: config.forceBindTargetUpUid,
|
|
1842
|
+
targetRoomId: config.forceBindTargetRoomId,
|
|
1843
|
+
targetMedalName: config.forceBindTargetMedalName,
|
|
1844
|
+
debugMode: config.debugMode
|
|
1845
|
+
};
|
|
1846
|
+
const forceBinder = new force_bind_utils_1.ForceBinder(forceBindConfig, loggerService.createChild('强制绑定'));
|
|
1847
|
+
// 创建 GroupExporter 实例
|
|
1848
|
+
const groupExporter = new export_utils_1.GroupExporter(ctx, loggerService.createChild('群数据导出'), mcidbindRepo);
|
|
1849
|
+
// =========== Handler 实例化和注册 ===========
|
|
1850
|
+
// 创建仓储对象
|
|
1851
|
+
const repositories = {
|
|
1852
|
+
mcidbind: mcidbindRepo,
|
|
1853
|
+
scheduleMute: scheduleMuteRepo
|
|
1854
|
+
};
|
|
1855
|
+
// 创建依赖对象,包含所有wrapper函数和服务
|
|
1856
|
+
const handlerDependencies = {
|
|
1857
|
+
// Utility functions
|
|
1858
|
+
normalizeQQId,
|
|
1859
|
+
formatCommand,
|
|
1860
|
+
formatUuid,
|
|
1861
|
+
checkCooldown,
|
|
1862
|
+
getCrafatarUrl,
|
|
1863
|
+
getStarlightSkinUrl,
|
|
1864
|
+
// Business functions
|
|
1865
|
+
sendMessage,
|
|
1866
|
+
autoSetGroupNickname,
|
|
1867
|
+
getBindInfo: getMcBindByQQId,
|
|
1868
|
+
// Service instances
|
|
1869
|
+
rconManager,
|
|
1870
|
+
messageUtils,
|
|
1871
|
+
forceBinder,
|
|
1872
|
+
groupExporter,
|
|
1873
|
+
// Session management
|
|
1874
|
+
getBindingSession,
|
|
1875
|
+
createBindingSession,
|
|
1876
|
+
updateBindingSession,
|
|
1877
|
+
removeBindingSession,
|
|
1878
|
+
// Shared state
|
|
1879
|
+
avatarCache: new Map(Object.entries(avatarCache).map(([k, v]) => [k, v])),
|
|
1880
|
+
bindingSessions
|
|
1881
|
+
};
|
|
1882
|
+
// 实例化Handler
|
|
1883
|
+
const bindingHandler = new handlers_1.BindingHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1884
|
+
const tagHandler = new handlers_1.TagHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1885
|
+
const whitelistHandler = new handlers_1.WhitelistHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1886
|
+
const buidHandler = new handlers_1.BuidHandler(ctx, config, loggerService, repositories, handlerDependencies);
|
|
1887
|
+
// 注册Handler命令
|
|
1888
|
+
bindingHandler.register();
|
|
1889
|
+
tagHandler.register();
|
|
1890
|
+
whitelistHandler.register();
|
|
1891
|
+
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
|
+
// 实例化McidCommandHandler并注册命令
|
|
1933
|
+
const mcidHandler = new handlers_1.McidCommandHandler(ctx, mcidHandlerDeps);
|
|
1934
|
+
mcidHandler.registerCommands(cmd);
|
|
1935
|
+
// 自定义文本前缀匹配
|
|
1936
|
+
if (config.allowTextPrefix && config.botNickname) {
|
|
1937
|
+
// 创建一个前缀匹配器
|
|
1938
|
+
ctx.middleware((session, next) => {
|
|
1939
|
+
// 不处理没有内容的消息
|
|
1940
|
+
if (!session.content)
|
|
1941
|
+
return next();
|
|
1942
|
+
// 检查是否是命令开头,如果已经是命令就不处理
|
|
1943
|
+
if (session.content.startsWith('.') || session.content.startsWith('/')) {
|
|
1944
|
+
return next();
|
|
1945
|
+
}
|
|
1946
|
+
// 获取消息内容并规范化空格
|
|
1947
|
+
const content = session.content.trim();
|
|
1948
|
+
// 使用机器人昵称,支持多种匹配方式
|
|
1949
|
+
const botNickname = config.botNickname;
|
|
1950
|
+
// 尝试识别以机器人昵称开头的mcid或buid命令
|
|
1951
|
+
let matchedCommand = null;
|
|
1952
|
+
// 1. 尝试匹配原始的botNickname格式(支持mcid、buid和绑定命令)
|
|
1953
|
+
const regularPrefixRegex = new RegExp(`^${escapeRegExp(botNickname)}\\s+((mcid|buid|绑定|bind)\\s*.*)$`, 'i');
|
|
1954
|
+
const regularMatch = content.match(regularPrefixRegex);
|
|
1955
|
+
// 2. 如果botNickname不包含@,也尝试匹配带@的版本
|
|
1956
|
+
const atPrefixRegex = !botNickname.startsWith('@') ?
|
|
1957
|
+
new RegExp(`^@${escapeRegExp(botNickname)}\\s+((mcid|buid|绑定|bind)\\s*.*)$`, 'i') :
|
|
1958
|
+
null;
|
|
1959
|
+
if (regularMatch && regularMatch[1]) {
|
|
1960
|
+
matchedCommand = regularMatch[1].trim();
|
|
1961
|
+
}
|
|
1962
|
+
else if (atPrefixRegex) {
|
|
1963
|
+
const atMatch = content.match(atPrefixRegex);
|
|
1964
|
+
if (atMatch && atMatch[1]) {
|
|
1965
|
+
matchedCommand = atMatch[1].trim();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
// 如果找到匹配的命令,执行它
|
|
1969
|
+
if (matchedCommand) {
|
|
1970
|
+
let commandType = 'unknown';
|
|
1971
|
+
if (matchedCommand.startsWith('mcid')) {
|
|
1972
|
+
commandType = 'mcid';
|
|
1973
|
+
}
|
|
1974
|
+
else if (matchedCommand.startsWith('buid')) {
|
|
1975
|
+
commandType = 'buid';
|
|
1976
|
+
}
|
|
1977
|
+
else if (matchedCommand.startsWith('绑定') || matchedCommand.startsWith('bind')) {
|
|
1978
|
+
commandType = '绑定';
|
|
1979
|
+
}
|
|
1980
|
+
logger.info(`[前缀匹配] 成功识别${commandType}命令,原始消息: "${content}",执行命令: "${matchedCommand}"`);
|
|
1981
|
+
// 使用session.execute方法主动触发命令执行
|
|
1982
|
+
session.execute(matchedCommand).catch(error => {
|
|
1983
|
+
logger.error(`[前缀匹配] 执行命令"${matchedCommand}"失败: ${error.message}`);
|
|
1984
|
+
});
|
|
1985
|
+
// 返回终止后续中间件处理,避免重复处理
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
return next();
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
// 随机提醒中间件 - 检查用户绑定状态和群昵称
|
|
1992
|
+
ctx.middleware(async (session, next) => {
|
|
1993
|
+
try {
|
|
1994
|
+
// 只在指定群中处理
|
|
1995
|
+
if (session.channelId !== config.autoNicknameGroupId) {
|
|
1996
|
+
return next();
|
|
1997
|
+
}
|
|
1998
|
+
// 跳过机器人自己的消息和系统消息
|
|
1999
|
+
if (!session.userId || session.userId === session.bot.userId) {
|
|
2000
|
+
return next();
|
|
2001
|
+
}
|
|
2002
|
+
// 跳过空消息或命令消息
|
|
2003
|
+
if (!session.content || session.content.startsWith('.') || session.content.startsWith('/') ||
|
|
2004
|
+
session.content.includes('mcid') || session.content.includes('buid') || session.content.includes('绑定')) {
|
|
2005
|
+
return next();
|
|
2006
|
+
}
|
|
2007
|
+
// 检查当前时间是否在群组禁言时间段内
|
|
2008
|
+
const inMuteTime = await isInMuteTime(session.channelId);
|
|
2009
|
+
if (inMuteTime) {
|
|
2010
|
+
logger.debug(`[随机提醒] 群组${session.channelId}当前处于禁言时间段,跳过提醒`);
|
|
2011
|
+
return next();
|
|
2012
|
+
}
|
|
2013
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
2014
|
+
// 检查是否在冷却期内
|
|
2015
|
+
if (isInReminderCooldown(normalizedUserId)) {
|
|
2016
|
+
return next();
|
|
2017
|
+
}
|
|
2018
|
+
// 随机触发概率:管理员 1%,普通用户 80%,避免过于频繁
|
|
2019
|
+
const isUserAdmin = await isAdmin(session.userId);
|
|
2020
|
+
const triggerRate = isUserAdmin ? 0.01 : 0.80;
|
|
2021
|
+
if (Math.random() > triggerRate) {
|
|
2022
|
+
return next();
|
|
2023
|
+
}
|
|
2024
|
+
logger.debug(`[随机提醒] 触发提醒检查: QQ(${normalizedUserId})${isUserAdmin ? ' (管理员)' : ''}`);
|
|
2025
|
+
// 检查是否在进行绑定会话,避免重复提醒
|
|
2026
|
+
const activeBindingSession = getBindingSession(session.userId, session.channelId);
|
|
2027
|
+
if (activeBindingSession) {
|
|
2028
|
+
logger.debug(`[随机提醒] QQ(${normalizedUserId})正在进行绑定会话,跳过提醒`);
|
|
2029
|
+
return next();
|
|
2030
|
+
}
|
|
2031
|
+
// 获取用户绑定信息
|
|
2032
|
+
const bind = await getMcBindByQQId(normalizedUserId);
|
|
2033
|
+
// 获取用户群昵称信息
|
|
2034
|
+
let currentNickname = '';
|
|
2035
|
+
try {
|
|
2036
|
+
if (session.bot.internal) {
|
|
2037
|
+
const groupInfo = await session.bot.internal.getGroupMemberInfo(session.channelId, session.userId);
|
|
2038
|
+
currentNickname = groupInfo.card || groupInfo.nickname || '';
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
catch (error) {
|
|
2042
|
+
// 获取群昵称失败,跳过处理
|
|
2043
|
+
return next();
|
|
2044
|
+
}
|
|
2045
|
+
// 情况1:完全未绑定
|
|
2046
|
+
if (!bind || (!bind.mcUsername && !bind.buidUid)) {
|
|
2047
|
+
// 创建新记录或获取提醒次数
|
|
2048
|
+
let reminderCount = 0;
|
|
2049
|
+
if (!bind) {
|
|
2050
|
+
// 创建新记录
|
|
2051
|
+
const tempUsername = `_temp_${normalizedUserId}`;
|
|
2052
|
+
await mcidbindRepo.create({
|
|
2053
|
+
qqId: normalizedUserId,
|
|
2054
|
+
mcUsername: tempUsername,
|
|
2055
|
+
mcUuid: '',
|
|
2056
|
+
lastModified: new Date(),
|
|
2057
|
+
isAdmin: false,
|
|
2058
|
+
whitelist: [],
|
|
2059
|
+
tags: [],
|
|
2060
|
+
reminderCount: 1
|
|
2061
|
+
});
|
|
2062
|
+
reminderCount = 1;
|
|
2063
|
+
}
|
|
2064
|
+
else {
|
|
2065
|
+
// 更新提醒次数
|
|
2066
|
+
reminderCount = (bind.reminderCount || 0) + 1;
|
|
2067
|
+
await mcidbindRepo.update(normalizedUserId, { reminderCount });
|
|
2068
|
+
}
|
|
2069
|
+
setReminderCooldown(normalizedUserId);
|
|
2070
|
+
// 根据次数决定用词
|
|
2071
|
+
const reminderType = reminderCount >= 4 ? '警告' : '提醒';
|
|
2072
|
+
const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
|
|
2073
|
+
logger.info(`[随机提醒] 向完全未绑定的用户QQ(${normalizedUserId})发送第${reminderCount}次${reminderType}`);
|
|
2074
|
+
await sendMessage(session, [
|
|
2075
|
+
koishi_1.h.text(`${reminderPrefix} \n👋 你好!检测到您尚未绑定账号\n\n📋 为了更好的群聊体验,建议您绑定MC和B站账号\n💡 使用 ${formatCommand('绑定')} 开始绑定流程\n\n⚠️ 温馨提醒:请按群规设置合适的群昵称。若在管理员多次提醒后仍不配合绑定账号信息或按规修改群昵称,将按群规进行相应处理。`)
|
|
2076
|
+
], { isProactiveMessage: true });
|
|
2077
|
+
return next();
|
|
2078
|
+
}
|
|
2079
|
+
// 情况2:只绑定了B站,未绑定MC
|
|
2080
|
+
if (bind.buidUid && bind.buidUsername && (!bind.mcUsername || bind.mcUsername.startsWith('_temp_'))) {
|
|
2081
|
+
const mcInfo = null;
|
|
2082
|
+
const isNicknameCorrect = checkNicknameFormat(currentNickname, bind.buidUsername, mcInfo);
|
|
2083
|
+
if (!isNicknameCorrect) {
|
|
2084
|
+
// 更新提醒次数
|
|
2085
|
+
const reminderCount = (bind.reminderCount || 0) + 1;
|
|
2086
|
+
await mcidbindRepo.update(normalizedUserId, { reminderCount });
|
|
2087
|
+
// 根据次数决定用词
|
|
2088
|
+
const reminderType = reminderCount >= 4 ? '警告' : '提醒';
|
|
2089
|
+
const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
|
|
2090
|
+
// 自动修改群昵称
|
|
2091
|
+
await autoSetGroupNickname(session, mcInfo, bind.buidUsername);
|
|
2092
|
+
setReminderCooldown(normalizedUserId);
|
|
2093
|
+
logger.info(`[随机提醒] 为仅绑定B站的用户QQ(${normalizedUserId})修复群昵称并发送第${reminderCount}次${reminderType}`);
|
|
2094
|
+
await sendMessage(session, [
|
|
2095
|
+
koishi_1.h.text(`${reminderPrefix} ✅ 已修改您的群昵称为规范格式\n\n💡 若您有Minecraft Java版账号,请使用 ${formatCommand('mcid bind <用户名>')} 绑定MC账号\n📝 这样可以申请服务器白名单哦!\n\n⚠️ 请勿随意修改群昵称,保持规范格式`)
|
|
2096
|
+
], { isProactiveMessage: true });
|
|
2097
|
+
}
|
|
2098
|
+
return next();
|
|
2099
|
+
}
|
|
2100
|
+
// 情况3:都已绑定,但群昵称格式不正确
|
|
2101
|
+
if (bind.buidUid && bind.buidUsername && bind.mcUsername && !bind.mcUsername.startsWith('_temp_')) {
|
|
2102
|
+
const isNicknameCorrect = checkNicknameFormat(currentNickname, bind.buidUsername, bind.mcUsername);
|
|
2103
|
+
if (!isNicknameCorrect) {
|
|
2104
|
+
// 更新提醒次数
|
|
2105
|
+
const reminderCount = (bind.reminderCount || 0) + 1;
|
|
2106
|
+
await mcidbindRepo.update(normalizedUserId, { reminderCount });
|
|
2107
|
+
// 根据次数决定用词
|
|
2108
|
+
const reminderType = reminderCount >= 4 ? '警告' : '提醒';
|
|
2109
|
+
const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
|
|
2110
|
+
// 自动修改群昵称
|
|
2111
|
+
await autoSetGroupNickname(session, bind.mcUsername, bind.buidUsername);
|
|
2112
|
+
setReminderCooldown(normalizedUserId);
|
|
2113
|
+
logger.info(`[随机提醒] 为已完全绑定的用户QQ(${normalizedUserId})修复群昵称并发送第${reminderCount}次${reminderType}`);
|
|
2114
|
+
await sendMessage(session, [
|
|
2115
|
+
koishi_1.h.text(`${reminderPrefix} ✅ 已修改您的群昵称为规范格式\n\n⚠️ 请勿随意修改群昵称!群昵称格式为:B站名称(ID:MC用户名)\n📋 这有助于管理员和群友识别您的身份\n\n`)
|
|
2116
|
+
], { isProactiveMessage: true });
|
|
2117
|
+
}
|
|
2118
|
+
return next();
|
|
2119
|
+
}
|
|
2120
|
+
return next();
|
|
2121
|
+
}
|
|
2122
|
+
catch (error) {
|
|
2123
|
+
logger.error(`[随机提醒] 处理用户消息时出错: ${error.message}`);
|
|
2124
|
+
return next();
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
// 交互型绑定会话处理中间件
|
|
2128
|
+
ctx.middleware(async (session, next) => {
|
|
2129
|
+
try {
|
|
2130
|
+
// 检查是否有进行中的绑定会话
|
|
2131
|
+
const bindingSession = getBindingSession(session.userId, session.channelId);
|
|
2132
|
+
if (!bindingSession) {
|
|
2133
|
+
return next();
|
|
2134
|
+
}
|
|
2135
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
2136
|
+
const rawContent = session.content?.trim();
|
|
2137
|
+
// 清理用户输入中的@Bot前缀
|
|
2138
|
+
const content = cleanUserInput(rawContent || '', session);
|
|
2139
|
+
// 处理取消命令
|
|
2140
|
+
if (content === '取消' || content === 'cancel') {
|
|
2141
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2142
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})手动取消了绑定会话`);
|
|
2143
|
+
await sendMessage(session, [koishi_1.h.text('❌ 绑定会话已取消\n\n📋 温馨提醒:请按群规设置合适的群昵称。若在管理员多次提醒后仍不配合绑定账号信息或按规修改群昵称,将按群规进行相应处理。')]);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
// 检查是否在绑定过程中使用了其他绑定相关命令(排除跳过选项)
|
|
2147
|
+
// 这里使用原始内容检测命令,避免误判@Bot发送的正常输入
|
|
2148
|
+
if (rawContent && content !== '跳过' && content !== 'skip' && (rawContent.includes('绑定') ||
|
|
2149
|
+
rawContent.includes('bind') ||
|
|
2150
|
+
rawContent.includes('mcid') ||
|
|
2151
|
+
rawContent.includes('buid') ||
|
|
2152
|
+
rawContent.startsWith('.') ||
|
|
2153
|
+
rawContent.startsWith('/'))) {
|
|
2154
|
+
const currentState = bindingSession.state === 'waiting_mc_username' ? 'MC用户名' : 'B站UID';
|
|
2155
|
+
await sendMessage(session, [koishi_1.h.text(`🔄 您正在进行交互式绑定,请继续输入${currentState}\n\n如需取消当前绑定,请发送"取消"`)]);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
// 检查是否为明显无关的输入
|
|
2159
|
+
const isIrrelevantInput = checkIrrelevantInput(bindingSession, content);
|
|
2160
|
+
if (isIrrelevantInput) {
|
|
2161
|
+
const currentCount = bindingSession.invalidInputCount || 0;
|
|
2162
|
+
const newCount = currentCount + 1;
|
|
2163
|
+
updateBindingSession(session.userId, session.channelId, {
|
|
2164
|
+
invalidInputCount: newCount
|
|
2165
|
+
});
|
|
2166
|
+
// 检查是否为明显的聊天内容(使用清理后的内容)
|
|
2167
|
+
const chatKeywords = ['你好', 'hello', 'hi', '在吗', '在不在', '怎么样', '什么', '为什么', '好的', '谢谢', '哈哈', '呵呵', '早上好', '晚上好', '晚安', '再见', '拜拜', '666', '牛', '厉害', '真的吗', '不是吧', '哇', '哦', '嗯', '好吧', '行', '可以', '没事', '没问题', '没关系'];
|
|
2168
|
+
const isChatMessage = chatKeywords.some(keyword => content.toLowerCase().includes(keyword)) ||
|
|
2169
|
+
/[!?。,;:""''()【】〈〉《》「」『』〔〕〖〗〘〙〚〛]{2,}/.test(content) ||
|
|
2170
|
+
/[!?.,;:"'()[\]<>{}]{3,}/.test(content);
|
|
2171
|
+
if (isChatMessage) {
|
|
2172
|
+
// 对于聊天消息,更快地取消绑定会话,避免持续打扰
|
|
2173
|
+
if (newCount >= 2) {
|
|
2174
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2175
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})持续发送聊天消息,自动取消绑定会话避免打扰`);
|
|
2176
|
+
// 对于聊天取消,给一个更温和的提示,同时提醒群规
|
|
2177
|
+
await sendMessage(session, [koishi_1.h.text(`💬 看起来您在聊天,绑定流程已自动取消\n\n📋 温馨提醒:请按群规设置合适的群昵称。若在管理员多次提醒后仍不配合绑定账号信息或按规修改群昵称,将按群规进行相应处理。\n\n如需绑定账号,请随时使用 ${formatCommand('绑定')} 命令重新开始`)]);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2181
|
+
// 第一次聊天消息,给温和提醒
|
|
2182
|
+
const expectedInput = bindingSession.state === 'waiting_mc_username' ? 'MC用户名' : 'B站UID';
|
|
2183
|
+
await sendMessage(session, [koishi_1.h.text(`💭 您当前正在进行账号绑定,需要输入${expectedInput}\n\n如不需要绑定,请发送"取消",或继续聊天我们会自动取消绑定流程`)]);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
else {
|
|
2188
|
+
// 对于非聊天的无关输入,使用原来的逻辑
|
|
2189
|
+
if (newCount === 1) {
|
|
2190
|
+
// 第1次无关输入,提醒检查
|
|
2191
|
+
const expectedInput = bindingSession.state === 'waiting_mc_username' ? 'MC用户名' : 'B站UID';
|
|
2192
|
+
await sendMessage(session, [koishi_1.h.text(`🤔 您当前正在进行绑定流程,需要输入${expectedInput}\n\n如果您想取消绑定,请发送"取消"`)]);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
else if (newCount >= 2) {
|
|
2196
|
+
// 第2次无关输入,建议取消
|
|
2197
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2198
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})因多次无关输入自动取消绑定会话`);
|
|
2199
|
+
await sendMessage(session, [koishi_1.h.text('🔄 检测到您可能不想继续绑定流程,已自动取消绑定会话\n\n📋 温馨提醒:请按群规设置合适的群昵称。若在管理员多次提醒后仍不配合绑定账号信息或按规修改群昵称,将按群规进行相应处理。\n\n如需重新绑定,请使用 ' + formatCommand('绑定') + ' 命令')]);
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
// 根据当前状态处理用户输入
|
|
2205
|
+
if (bindingSession.state === 'waiting_mc_username') {
|
|
2206
|
+
// 处理MC用户名输入
|
|
2207
|
+
await handleMcUsernameInput(session, bindingSession, content);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
else if (bindingSession.state === 'waiting_buid') {
|
|
2211
|
+
// 处理B站UID输入
|
|
2212
|
+
await handleBuidInput(session, bindingSession, content);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
return next();
|
|
2216
|
+
}
|
|
2217
|
+
catch (error) {
|
|
2218
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
2219
|
+
logger.error(`[交互绑定] QQ(${normalizedUserId})的会话处理出错: ${error.message}`);
|
|
2220
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2221
|
+
await sendMessage(session, [koishi_1.h.text('绑定过程中出现错误,会话已重置')]);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
// 处理MC用户名输入
|
|
2226
|
+
const handleMcUsernameInput = async (session, bindingSession, content) => {
|
|
2227
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
2228
|
+
// 处理跳过MC绑定,直接完成绑定流程
|
|
2229
|
+
if (content === '跳过' || content === 'skip') {
|
|
2230
|
+
// 检查用户是否已绑定B站账号
|
|
2231
|
+
const existingBind = await getMcBindByQQId(normalizedUserId);
|
|
2232
|
+
if (existingBind && existingBind.buidUid && existingBind.buidUsername) {
|
|
2233
|
+
// 用户已绑定B站账号,直接完成绑定
|
|
2234
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})跳过了MC账号绑定,已有B站绑定,完成绑定流程`);
|
|
2235
|
+
// 清理会话
|
|
2236
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2237
|
+
// 设置群昵称
|
|
2238
|
+
try {
|
|
2239
|
+
await autoSetGroupNickname(session, null, existingBind.buidUsername);
|
|
2240
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})完成绑定,已设置群昵称`);
|
|
2241
|
+
}
|
|
2242
|
+
catch (renameError) {
|
|
2243
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})自动群昵称设置失败: ${renameError.message}`);
|
|
2244
|
+
}
|
|
2245
|
+
await sendMessage(session, [
|
|
2246
|
+
koishi_1.h.text(`🎉 绑定完成!\nMC: 未绑定\nB站: ${existingBind.buidUsername}\n\n💡 您可以随时使用 ${formatCommand('mcid bind <用户名>')} 绑定MC账号`),
|
|
2247
|
+
...(config?.showAvatar ? [koishi_1.h.image(`https://workers.vrp.moe/bilibili/avatar/${existingBind.buidUid}?size=160`)] : [])
|
|
2248
|
+
]);
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
else {
|
|
2252
|
+
// 用户未绑定B站账号,需要完成B站绑定
|
|
2253
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})跳过了MC账号绑定,需要完成B站绑定`);
|
|
2254
|
+
// 为跳过MC绑定的用户创建临时绑定记录
|
|
2255
|
+
const timestamp = Date.now();
|
|
2256
|
+
const tempMcUsername = `_temp_skip_${timestamp}`;
|
|
2257
|
+
// 创建临时MC绑定
|
|
2258
|
+
const tempBindResult = await createOrUpdateMcBind(session.userId, tempMcUsername, '', false);
|
|
2259
|
+
if (!tempBindResult) {
|
|
2260
|
+
logger.error(`[交互绑定] QQ(${normalizedUserId})创建临时MC绑定失败`);
|
|
2261
|
+
await sendMessage(session, [koishi_1.h.text('❌ 创建临时绑定失败,请稍后重试')]);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
// 跳转到B站绑定流程
|
|
2265
|
+
updateBindingSession(session.userId, session.channelId, {
|
|
2266
|
+
state: 'waiting_buid',
|
|
2267
|
+
mcUsername: tempMcUsername,
|
|
2268
|
+
mcUuid: ''
|
|
2269
|
+
});
|
|
2270
|
+
await sendMessage(session, [koishi_1.h.text('✅ 已跳过MC绑定\n📋 请发送您的B站UID')]);
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
// 验证用户名格式
|
|
2275
|
+
if (!content || !/^[a-zA-Z0-9_]{3,16}$/.test(content)) {
|
|
2276
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的MC用户名"${content}"格式无效`);
|
|
2277
|
+
await sendMessage(session, [koishi_1.h.text('❌ 用户名格式无效,请重新输入\n或发送"跳过"完成绑定')]);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
// 验证用户名是否存在
|
|
2281
|
+
const profile = await validateUsername(content);
|
|
2282
|
+
if (!profile) {
|
|
2283
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的MC用户名"${content}"不存在`);
|
|
2284
|
+
await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${content} 不存在\n请重新输入或发送"跳过"完成绑定`)]);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const username = profile.name;
|
|
2288
|
+
const uuid = profile.id;
|
|
2289
|
+
// 检查用户是否已绑定MC账号
|
|
2290
|
+
const existingBind = await getMcBindByQQId(normalizedUserId);
|
|
2291
|
+
if (existingBind && existingBind.mcUsername && !existingBind.mcUsername.startsWith('_temp_')) {
|
|
2292
|
+
// 检查冷却时间
|
|
2293
|
+
if (!await isAdmin(session.userId) && !checkCooldown(existingBind.lastModified)) {
|
|
2294
|
+
const days = config.cooldownDays;
|
|
2295
|
+
const now = new Date();
|
|
2296
|
+
const diffTime = now.getTime() - existingBind.lastModified.getTime();
|
|
2297
|
+
const passedDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
2298
|
+
const remainingDays = days - passedDays;
|
|
2299
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2300
|
+
const displayUsername = existingBind.mcUsername && !existingBind.mcUsername.startsWith('_temp_') ? existingBind.mcUsername : '未绑定';
|
|
2301
|
+
await sendMessage(session, [koishi_1.h.text(`❌ 您已绑定MC账号: ${displayUsername}\n\n如需修改,请在冷却期结束后(还需${remainingDays}天)使用 ${formatCommand('mcid change')} 命令或联系管理员`)]);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
// 检查用户名是否已被其他人绑定
|
|
2306
|
+
if (await checkUsernameExists(username, session.userId)) {
|
|
2307
|
+
logger.warn(`[交互绑定] MC用户名"${username}"已被其他用户绑定`);
|
|
2308
|
+
await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${username} 已被其他用户绑定\n\n请输入其他MC用户名或发送"跳过"完成绑定`)]);
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
// 绑定MC账号
|
|
2312
|
+
const bindResult = await createOrUpdateMcBind(session.userId, username, uuid);
|
|
2313
|
+
if (!bindResult) {
|
|
2314
|
+
logger.error(`[交互绑定] QQ(${normalizedUserId})绑定MC账号失败`);
|
|
2315
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2316
|
+
await sendMessage(session, [koishi_1.h.text('❌ 绑定失败,数据库操作出错\n\n请联系管理员或稍后重试')]);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})成功绑定MC账号: ${username}`);
|
|
2320
|
+
// 检查用户是否已经绑定了B站账号
|
|
2321
|
+
const updatedBind = await getMcBindByQQId(normalizedUserId);
|
|
2322
|
+
if (updatedBind && updatedBind.buidUid && updatedBind.buidUsername) {
|
|
2323
|
+
// 用户已经绑定了B站账号,直接完成绑定流程
|
|
2324
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})已绑定B站账号,完成绑定流程`);
|
|
2325
|
+
// 清理会话
|
|
2326
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2327
|
+
// 设置群昵称
|
|
2328
|
+
try {
|
|
2329
|
+
await autoSetGroupNickname(session, username, updatedBind.buidUsername);
|
|
2330
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})绑定完成,已设置群昵称`);
|
|
2331
|
+
}
|
|
2332
|
+
catch (renameError) {
|
|
2333
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})自动群昵称设置失败: ${renameError.message}`);
|
|
2334
|
+
}
|
|
2335
|
+
// 根据配置决定显示哪种图像
|
|
2336
|
+
let mcAvatarUrl = null;
|
|
2337
|
+
if (config?.showAvatar) {
|
|
2338
|
+
if (config?.showMcSkin) {
|
|
2339
|
+
mcAvatarUrl = getStarlightSkinUrl(username);
|
|
2340
|
+
}
|
|
2341
|
+
else {
|
|
2342
|
+
mcAvatarUrl = getCrafatarUrl(uuid);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
// 发送完成消息
|
|
2346
|
+
await sendMessage(session, [
|
|
2347
|
+
koishi_1.h.text(`🎉 绑定完成!\nMC: ${username}\nB站: ${updatedBind.buidUsername}`),
|
|
2348
|
+
...(mcAvatarUrl ? [koishi_1.h.image(mcAvatarUrl)] : [])
|
|
2349
|
+
]);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
// 用户未绑定B站账号,继续B站绑定流程
|
|
2353
|
+
// 更新会话状态
|
|
2354
|
+
updateBindingSession(session.userId, session.channelId, {
|
|
2355
|
+
state: 'waiting_buid',
|
|
2356
|
+
mcUsername: username,
|
|
2357
|
+
mcUuid: uuid
|
|
2358
|
+
});
|
|
2359
|
+
// 根据配置决定显示哪种图像
|
|
2360
|
+
let mcAvatarUrl = null;
|
|
2361
|
+
if (config?.showAvatar) {
|
|
2362
|
+
if (config?.showMcSkin) {
|
|
2363
|
+
mcAvatarUrl = getStarlightSkinUrl(username);
|
|
2364
|
+
}
|
|
2365
|
+
else {
|
|
2366
|
+
mcAvatarUrl = getCrafatarUrl(uuid);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
const formattedUuid = formatUuid(uuid);
|
|
2370
|
+
// 发送简化的MC绑定成功消息
|
|
2371
|
+
await sendMessage(session, [
|
|
2372
|
+
koishi_1.h.text(`✅ MC账号: ${username}\n🔗 请发送您的B站UID`),
|
|
2373
|
+
...(mcAvatarUrl ? [koishi_1.h.image(mcAvatarUrl)] : [])
|
|
2374
|
+
]);
|
|
2375
|
+
};
|
|
2376
|
+
// 处理B站UID输入
|
|
2377
|
+
const handleBuidInput = async (session, bindingSession, content) => {
|
|
2378
|
+
const normalizedUserId = normalizeQQId(session.userId);
|
|
2379
|
+
// 处理跳过B站绑定,直接进入MC绑定流程
|
|
2380
|
+
if (content === '跳过' || content === 'skip') {
|
|
2381
|
+
bindingSession.state = 'waiting_mc_username';
|
|
2382
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})跳过了B站账号绑定,直接进入MC绑定流程`);
|
|
2383
|
+
await sendMessage(session, [koishi_1.h.text('✅ 已跳过B站绑定\n🎮 请发送您的MC用户名')]);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
// 解析UID格式,支持多种格式
|
|
2387
|
+
let actualUid = content;
|
|
2388
|
+
if (content && content.toLowerCase().startsWith('uid:')) {
|
|
2389
|
+
// UID:数字格式
|
|
2390
|
+
actualUid = content.substring(4);
|
|
2391
|
+
}
|
|
2392
|
+
else if (content && content.includes('space.bilibili.com/')) {
|
|
2393
|
+
// B站空间URL格式
|
|
2394
|
+
try {
|
|
2395
|
+
// 删除前缀 https://space.bilibili.com/ 或 http://space.bilibili.com/
|
|
2396
|
+
let urlPart = content.replace(/^https?:\/\/space\.bilibili\.com\//, '');
|
|
2397
|
+
// 删除后缀参数 ?***
|
|
2398
|
+
if (urlPart.includes('?')) {
|
|
2399
|
+
urlPart = urlPart.split('?')[0];
|
|
2400
|
+
}
|
|
2401
|
+
// 删除可能的路径后缀 /***
|
|
2402
|
+
if (urlPart.includes('/')) {
|
|
2403
|
+
urlPart = urlPart.split('/')[0];
|
|
2404
|
+
}
|
|
2405
|
+
actualUid = urlPart;
|
|
2406
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})从URL提取UID: ${content} -> ${actualUid}`);
|
|
2407
|
+
}
|
|
2408
|
+
catch (error) {
|
|
2409
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})URL解析失败: ${error.message}`);
|
|
2410
|
+
actualUid = '';
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// 验证UID格式
|
|
2414
|
+
if (!actualUid || !/^\d+$/.test(actualUid)) {
|
|
2415
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的B站UID"${content}"格式无效`);
|
|
2416
|
+
await sendMessage(session, [koishi_1.h.text('❌ UID格式无效,请重新输入\n支持格式:纯数字、UID:数字、空间链接\n或发送"跳过"仅绑定MC账号')]);
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
// 检查UID是否已被绑定
|
|
2420
|
+
if (await checkBuidExists(actualUid, session.userId)) {
|
|
2421
|
+
logger.warn(`[交互绑定] B站UID"${actualUid}"已被其他用户绑定`);
|
|
2422
|
+
await sendMessage(session, [koishi_1.h.text(`❌ UID ${actualUid} 已被其他用户绑定\n\n请输入其他B站UID\n或发送"跳过"仅绑定MC账号`)]);
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
// 验证UID是否存在
|
|
2426
|
+
const buidUser = await validateBUID(actualUid);
|
|
2427
|
+
if (!buidUser) {
|
|
2428
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的B站UID"${actualUid}"不存在`);
|
|
2429
|
+
await sendMessage(session, [koishi_1.h.text(`❌ 无法验证UID: ${actualUid}\n\n该用户可能不存在或未被发现\n可以去直播间发个弹幕后重试绑定\n或发送"跳过"仅绑定MC账号`)]);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
// 绑定B站账号
|
|
2433
|
+
const bindResult = await createOrUpdateBuidBind(session.userId, buidUser);
|
|
2434
|
+
if (!bindResult) {
|
|
2435
|
+
logger.error(`[交互绑定] QQ(${normalizedUserId})绑定B站账号失败`);
|
|
2436
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2437
|
+
// 根据是否有MC绑定提供不同的提示
|
|
2438
|
+
const displayMcName = bindingSession.mcUsername && !bindingSession.mcUsername.startsWith('_temp_') ? bindingSession.mcUsername : null;
|
|
2439
|
+
const mcStatus = displayMcName ? `您的MC账号${displayMcName}已成功绑定\n` : '';
|
|
2440
|
+
await sendMessage(session, [koishi_1.h.text(`❌ B站账号绑定失败,数据库操作出错\n\n${mcStatus}可稍后使用 ${formatCommand('buid bind <UID>')} 命令单独绑定B站账号`)]);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})成功绑定B站UID: ${actualUid}`);
|
|
2444
|
+
// 清理会话
|
|
2445
|
+
removeBindingSession(session.userId, session.channelId);
|
|
2446
|
+
// 自动群昵称设置功能 - 使用新的autoSetGroupNickname函数
|
|
2447
|
+
try {
|
|
2448
|
+
// 检查是否有有效的MC用户名(不是临时用户名)
|
|
2449
|
+
const mcName = bindingSession.mcUsername && !bindingSession.mcUsername.startsWith('_temp_') ? bindingSession.mcUsername : null;
|
|
2450
|
+
await autoSetGroupNickname(session, mcName, buidUser.username);
|
|
2451
|
+
logger.info(`[交互绑定] QQ(${normalizedUserId})绑定完成,已设置群昵称`);
|
|
2452
|
+
}
|
|
2453
|
+
catch (renameError) {
|
|
2454
|
+
logger.warn(`[交互绑定] QQ(${normalizedUserId})自动群昵称设置失败: ${renameError.message}`);
|
|
2455
|
+
// 群昵称设置失败不影响主流程,只记录日志
|
|
2456
|
+
}
|
|
2457
|
+
// 发送完整的绑定成功消息
|
|
2458
|
+
const buidInfo = `B站UID: ${buidUser.uid}\n用户名: ${buidUser.username}`;
|
|
2459
|
+
let extraInfo = '';
|
|
2460
|
+
if (buidUser.guard_level > 0) {
|
|
2461
|
+
extraInfo += `\n舰长等级: ${buidUser.guard_level_text} (${buidUser.guard_level})`;
|
|
2462
|
+
}
|
|
2463
|
+
if (buidUser.medal) {
|
|
2464
|
+
extraInfo += `\n粉丝牌: ${buidUser.medal.name} Lv.${buidUser.medal.level}`;
|
|
2465
|
+
}
|
|
2466
|
+
if (buidUser.wealthMedalLevel > 0) {
|
|
2467
|
+
extraInfo += `\n荣耀等级: ${buidUser.wealthMedalLevel}`;
|
|
2468
|
+
}
|
|
2469
|
+
// 准备完成消息
|
|
2470
|
+
const displayMcName = bindingSession.mcUsername && !bindingSession.mcUsername.startsWith('_temp_') ? bindingSession.mcUsername : null;
|
|
2471
|
+
const mcInfo = displayMcName ? `MC: ${displayMcName}` : 'MC: 未绑定';
|
|
2472
|
+
let extraTip = '';
|
|
2473
|
+
// 如果用户跳过了MC绑定或MC账号是temp,提供后续绑定的指引
|
|
2474
|
+
if (!displayMcName) {
|
|
2475
|
+
extraTip = `\n\n💡 您可以随时使用 ${formatCommand('mcid bind <用户名>')} 绑定MC账号`;
|
|
2476
|
+
}
|
|
2477
|
+
await sendMessage(session, [
|
|
2478
|
+
koishi_1.h.text(`🎉 绑定完成!\n${mcInfo}\nB站: ${buidUser.username}${extraInfo}${extraTip}`),
|
|
2479
|
+
...(config?.showAvatar ? [koishi_1.h.image(`https://workers.vrp.moe/bilibili/avatar/${buidUser.uid}?size=160`)] : [])
|
|
2480
|
+
]);
|
|
2481
|
+
};
|
|
2482
|
+
// 帮助函数:转义正则表达式中的特殊字符
|
|
2483
|
+
const escapeRegExp = (string) => {
|
|
2484
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2485
|
+
};
|
|
2486
|
+
// 帮助函数:清理用户输入中的@Bot前缀
|
|
2487
|
+
const cleanUserInput = (content, session) => {
|
|
2488
|
+
if (!content)
|
|
2489
|
+
return content;
|
|
2490
|
+
// 获取机器人的用户ID
|
|
2491
|
+
const botUserId = session.bot.userId;
|
|
2492
|
+
// 匹配各种@Bot的格式
|
|
2493
|
+
const atPatterns = [
|
|
2494
|
+
// <at id="botUserId"/> 格式
|
|
2495
|
+
new RegExp(`^<at id="${escapeRegExp(botUserId)}"/>\\s*`, 'i'),
|
|
2496
|
+
// @Bot昵称 格式(如果配置了botNickname)
|
|
2497
|
+
config.botNickname ? new RegExp(`^@${escapeRegExp(config.botNickname)}\\s+`, 'i') : null,
|
|
2498
|
+
// @botUserId 格式
|
|
2499
|
+
new RegExp(`^@${escapeRegExp(botUserId)}\\s+`, 'i'),
|
|
2500
|
+
].filter(Boolean);
|
|
2501
|
+
let cleanedContent = content.trim();
|
|
2502
|
+
// 尝试匹配并移除@Bot前缀
|
|
2503
|
+
for (const pattern of atPatterns) {
|
|
2504
|
+
if (pattern.test(cleanedContent)) {
|
|
2505
|
+
cleanedContent = cleanedContent.replace(pattern, '').trim();
|
|
2506
|
+
logger.debug(`[交互绑定] 清理用户输入,原始: "${content}" -> 清理后: "${cleanedContent}"`);
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return cleanedContent;
|
|
2511
|
+
};
|
|
2512
|
+
// =========== 天选开奖 Webhook 处理 ===========
|
|
2513
|
+
// 处理天选开奖结果
|
|
2514
|
+
const handleLotteryResult = async (lotteryData) => {
|
|
2515
|
+
try {
|
|
2516
|
+
// 检查天选播报开关
|
|
2517
|
+
if (!config?.enableLotteryBroadcast) {
|
|
2518
|
+
logger.debug(`[天选开奖] 天选播报功能已禁用,跳过处理天选事件: ${lotteryData.lottery_id}`);
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
logger.info(`[天选开奖] 开始处理天选事件: ${lotteryData.lottery_id},奖品: ${lotteryData.reward_name},中奖人数: ${lotteryData.winners.length}`);
|
|
2522
|
+
// 生成标签名称
|
|
2523
|
+
const tagName = `天选-${lotteryData.lottery_id}`;
|
|
2524
|
+
// 统计信息
|
|
2525
|
+
let matchedCount = 0;
|
|
2526
|
+
let notBoundCount = 0;
|
|
2527
|
+
let tagAddedCount = 0;
|
|
2528
|
+
let tagExistedCount = 0;
|
|
2529
|
+
const matchedUsers = [];
|
|
2530
|
+
// 处理每个中奖用户
|
|
2531
|
+
for (const winner of lotteryData.winners) {
|
|
2532
|
+
try {
|
|
2533
|
+
// 根据B站UID查找绑定的QQ用户
|
|
2534
|
+
const bind = await getBuidBindByBuid(winner.uid.toString());
|
|
2535
|
+
if (bind && bind.qqId) {
|
|
2536
|
+
matchedCount++;
|
|
2537
|
+
matchedUsers.push({
|
|
2538
|
+
qqId: bind.qqId,
|
|
2539
|
+
mcUsername: bind.mcUsername || '未绑定MC',
|
|
2540
|
+
buidUsername: bind.buidUsername,
|
|
2541
|
+
uid: winner.uid,
|
|
2542
|
+
username: winner.username
|
|
2543
|
+
});
|
|
2544
|
+
// 检查是否已有该标签
|
|
2545
|
+
if (bind.tags && bind.tags.includes(tagName)) {
|
|
2546
|
+
tagExistedCount++;
|
|
2547
|
+
logger.debug(`[天选开奖] QQ(${bind.qqId})已有标签"${tagName}"`);
|
|
2548
|
+
}
|
|
2549
|
+
else {
|
|
2550
|
+
// 添加标签
|
|
2551
|
+
const newTags = [...(bind.tags || []), tagName];
|
|
2552
|
+
await mcidbindRepo.update(bind.qqId, { tags: newTags });
|
|
2553
|
+
tagAddedCount++;
|
|
2554
|
+
logger.debug(`[天选开奖] 为QQ(${bind.qqId})添加标签"${tagName}"`);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
else {
|
|
2558
|
+
notBoundCount++;
|
|
2559
|
+
logger.debug(`[天选开奖] B站UID(${winner.uid})未绑定QQ账号`);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
catch (error) {
|
|
2563
|
+
logger.error(`[天选开奖] 处理中奖用户UID(${winner.uid})时出错: ${error.message}`);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
logger.info(`[天选开奖] 处理完成: 总计${lotteryData.winners.length}人中奖,匹配${matchedCount}人,未绑定${notBoundCount}人,新增标签${tagAddedCount}人,已有标签${tagExistedCount}人`);
|
|
2567
|
+
// 生成并发送结果消息
|
|
2568
|
+
await sendLotteryResultToGroup(lotteryData, {
|
|
2569
|
+
totalWinners: lotteryData.winners.length,
|
|
2570
|
+
matchedCount,
|
|
2571
|
+
notBoundCount,
|
|
2572
|
+
tagAddedCount,
|
|
2573
|
+
tagExistedCount,
|
|
2574
|
+
matchedUsers,
|
|
2575
|
+
tagName
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
catch (error) {
|
|
2579
|
+
logger.error(`[天选开奖] 处理天选事件"${lotteryData.lottery_id}"失败: ${error.message}`);
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
// 发送天选开奖结果到群
|
|
2583
|
+
const sendLotteryResultToGroup = async (lotteryData, stats) => {
|
|
2584
|
+
try {
|
|
2585
|
+
const targetChannelId = '123456789'; // 目标群号
|
|
2586
|
+
const privateTargetId = 'private:3431185320'; // 私聊目标
|
|
2587
|
+
// 格式化时间
|
|
2588
|
+
const lotteryTime = new Date(lotteryData.timestamp).toLocaleString('zh-CN', {
|
|
2589
|
+
timeZone: 'Asia/Shanghai',
|
|
2590
|
+
year: 'numeric',
|
|
2591
|
+
month: '2-digit',
|
|
2592
|
+
day: '2-digit',
|
|
2593
|
+
hour: '2-digit',
|
|
2594
|
+
minute: '2-digit',
|
|
2595
|
+
second: '2-digit'
|
|
2596
|
+
});
|
|
2597
|
+
// 构建简化版群消息(去掉主播信息、统计信息和标签提示)
|
|
2598
|
+
let groupMessage = `🎉 天选开奖结果通知\n\n`;
|
|
2599
|
+
groupMessage += `📅 开奖时间: ${lotteryTime}\n`;
|
|
2600
|
+
groupMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
|
|
2601
|
+
groupMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
|
|
2602
|
+
groupMessage += `🎲 总中奖人数: ${stats.totalWinners}人`;
|
|
2603
|
+
// 添加未绑定用户说明
|
|
2604
|
+
if (stats.notBoundCount > 0) {
|
|
2605
|
+
groupMessage += `(其中${stats.notBoundCount}人未绑定跳过)`;
|
|
2606
|
+
}
|
|
2607
|
+
groupMessage += `\n\n`;
|
|
2608
|
+
// 如果有匹配的用户,显示详细信息
|
|
2609
|
+
if (stats.matchedUsers.length > 0) {
|
|
2610
|
+
groupMessage += `🎯 已绑定的中奖用户:\n`;
|
|
2611
|
+
// 限制显示前10个用户,避免消息过长
|
|
2612
|
+
const displayUsers = stats.matchedUsers.slice(0, 10);
|
|
2613
|
+
for (let i = 0; i < displayUsers.length; i++) {
|
|
2614
|
+
const user = displayUsers[i];
|
|
2615
|
+
const index = i + 1;
|
|
2616
|
+
const displayMcName = user.mcUsername && !user.mcUsername.startsWith('_temp_') ? user.mcUsername : '未绑定';
|
|
2617
|
+
groupMessage += `${index}. ${user.buidUsername} (UID: ${user.uid})\n`;
|
|
2618
|
+
groupMessage += ` QQ: ${user.qqId} | MC: ${displayMcName}\n`;
|
|
2619
|
+
}
|
|
2620
|
+
// 如果用户太多,显示省略信息
|
|
2621
|
+
if (stats.matchedUsers.length > 10) {
|
|
2622
|
+
groupMessage += `... 还有${stats.matchedUsers.length - 10}位中奖用户\n`;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
else {
|
|
2626
|
+
groupMessage += `😔 暂无已绑定用户中奖\n`;
|
|
2627
|
+
}
|
|
2628
|
+
// 构建完整版私聊消息(包含所有信息和未绑定用户)
|
|
2629
|
+
let privateMessage = `🎉 天选开奖结果通知\n\n`;
|
|
2630
|
+
privateMessage += `📅 开奖时间: ${lotteryTime}\n`;
|
|
2631
|
+
privateMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
|
|
2632
|
+
privateMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
|
|
2633
|
+
privateMessage += `🏷️ 事件ID: ${lotteryData.lottery_id}\n`;
|
|
2634
|
+
privateMessage += `👤 主播: ${lotteryData.host_username} (UID: ${lotteryData.host_uid})\n`;
|
|
2635
|
+
privateMessage += `🏠 房间号: ${lotteryData.room_id}\n\n`;
|
|
2636
|
+
// 统计信息
|
|
2637
|
+
privateMessage += `📈 处理统计:\n`;
|
|
2638
|
+
privateMessage += `• 总中奖人数: ${stats.totalWinners}人\n`;
|
|
2639
|
+
privateMessage += `• 已绑定用户: ${stats.matchedCount}人 ✅\n`;
|
|
2640
|
+
privateMessage += `• 未绑定用户: ${stats.notBoundCount}人 ⚠️\n`;
|
|
2641
|
+
privateMessage += `• 新增标签: ${stats.tagAddedCount}人\n`;
|
|
2642
|
+
privateMessage += `• 已有标签: ${stats.tagExistedCount}人\n\n`;
|
|
2643
|
+
// 显示所有中奖用户(包括未绑定的)
|
|
2644
|
+
if (lotteryData.winners.length > 0) {
|
|
2645
|
+
privateMessage += `🎯 所有中奖用户:\n`;
|
|
2646
|
+
for (let i = 0; i < lotteryData.winners.length; i++) {
|
|
2647
|
+
const winner = lotteryData.winners[i];
|
|
2648
|
+
const index = i + 1;
|
|
2649
|
+
// 查找对应的绑定用户
|
|
2650
|
+
const matchedUser = stats.matchedUsers.find(user => user.uid === winner.uid);
|
|
2651
|
+
if (matchedUser) {
|
|
2652
|
+
const displayMcName = matchedUser.mcUsername && !matchedUser.mcUsername.startsWith('_temp_') ? matchedUser.mcUsername : '未绑定';
|
|
2653
|
+
privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
|
|
2654
|
+
privateMessage += ` QQ: ${matchedUser.qqId} | MC: ${displayMcName}\n`;
|
|
2655
|
+
}
|
|
2656
|
+
else {
|
|
2657
|
+
privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
|
|
2658
|
+
privateMessage += ` 无绑定信息,自动跳过\n`;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
privateMessage += `\n🏷️ 标签"${stats.tagName}"已自动添加到已绑定用户\n`;
|
|
2662
|
+
}
|
|
2663
|
+
// 准备消息元素
|
|
2664
|
+
const groupMessageElements = [koishi_1.h.text(groupMessage)];
|
|
2665
|
+
const privateMessageElements = [koishi_1.h.text(privateMessage)];
|
|
2666
|
+
// 发送消息到指定群(简化版)
|
|
2667
|
+
for (const bot of ctx.bots) {
|
|
2668
|
+
try {
|
|
2669
|
+
await bot.sendMessage(targetChannelId, groupMessageElements);
|
|
2670
|
+
logger.info(`[天选开奖] 成功发送简化开奖结果到群${targetChannelId}`);
|
|
2671
|
+
break; // 成功发送后退出循环
|
|
2672
|
+
}
|
|
2673
|
+
catch (error) {
|
|
2674
|
+
logger.error(`[天选开奖] 发送消息到群${targetChannelId}失败: ${error.message}`);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
// 发送消息到私聊(完整版)
|
|
2678
|
+
for (const bot of ctx.bots) {
|
|
2679
|
+
try {
|
|
2680
|
+
await bot.sendMessage(privateTargetId, privateMessageElements);
|
|
2681
|
+
logger.info(`[天选开奖] 成功发送完整开奖结果到私聊${privateTargetId}`);
|
|
2682
|
+
break; // 成功发送后退出循环
|
|
2683
|
+
}
|
|
2684
|
+
catch (error) {
|
|
2685
|
+
logger.error(`[天选开奖] 发送消息到私聊${privateTargetId}失败: ${error.message}`);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
catch (error) {
|
|
2690
|
+
logger.error(`[天选开奖] 发送开奖结果失败: ${error.message}`);
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
}
|