koishi-plugin-bind-bot 2.0.5 → 2.1.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/index.js CHANGED
@@ -1,12 +1,8 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.Config = exports.inject = exports.name = void 0;
7
4
  exports.apply = apply;
8
5
  const koishi_1 = require("koishi");
9
- const axios_1 = __importDefault(require("axios"));
10
6
  const force_bind_utils_1 = require("./force-bind-utils");
11
7
  const export_utils_1 = require("./export-utils");
12
8
  const logger_1 = require("./utils/logger");
@@ -17,6 +13,7 @@ const error_utils_1 = require("./utils/error-utils");
17
13
  const mcidbind_repository_1 = require("./repositories/mcidbind.repository");
18
14
  const schedule_mute_repository_1 = require("./repositories/schedule-mute.repository");
19
15
  const handlers_1 = require("./handlers");
16
+ const service_container_1 = require("./services/service-container");
20
17
  exports.name = 'bind-bot';
21
18
  // 声明插件依赖
22
19
  exports.inject = ['database', 'server'];
@@ -56,6 +53,10 @@ exports.Config = koishi_1.Schema.object({
56
53
  enableLotteryBroadcast: koishi_1.Schema.boolean()
57
54
  .description('是否启用天选开奖播报功能')
58
55
  .default(false),
56
+ lotteryTargetGroupId: koishi_1.Schema.string()
57
+ .description('天选开奖播报目标群ID'),
58
+ lotteryTargetPrivateId: koishi_1.Schema.string()
59
+ .description('天选开奖播报私聊目标ID(格式:private:QQ号)'),
59
60
  autoNicknameGroupId: koishi_1.Schema.string()
60
61
  .description('自动群昵称设置目标群ID')
61
62
  .default('123456789'),
@@ -147,15 +148,6 @@ function apply(ctx, config) {
147
148
  // 随机提醒功能的冷却缓存
148
149
  const reminderCooldown = new Map();
149
150
  const REMINDER_COOLDOWN_TIME = 24 * 60 * 60 * 1000; // 24小时冷却
150
- // 检查群昵称是否符合规范格式
151
- const checkNicknameFormat = (nickname, buidUsername, mcUsername) => {
152
- if (!nickname || !buidUsername)
153
- return false;
154
- // 期望格式:B站名称(ID:MC用户名)或 B站名称(ID:未绑定)
155
- const mcInfo = mcUsername && !mcUsername.startsWith('_temp_') ? mcUsername : "未绑定";
156
- const expectedFormat = `${buidUsername}(ID:${mcInfo})`;
157
- return nickname === expectedFormat;
158
- };
159
151
  // 检查用户是否在冷却期内
160
152
  const isInReminderCooldown = (userId) => {
161
153
  const lastReminder = reminderCooldown.get(userId);
@@ -209,6 +201,9 @@ function apply(ctx, config) {
209
201
  const rconManager = new rcon_manager_1.RconManager(loggerService.createChild('RCON管理器'), config.servers || []);
210
202
  // 创建RCON限流器实例
211
203
  const rconRateLimiter = new rate_limiter_1.RateLimiter(10, 3000); // 3秒内最多10个请求
204
+ // =========== 服务容器 ===========
205
+ // 注意:服务容器需要在 normalizeQQId 定义后才能实例化
206
+ // 将在定义 normalizeQQId 后统一实例化所有服务
212
207
  // 会话管理辅助函数
213
208
  const createBindingSession = (userId, channelId) => {
214
209
  const sessionKey = `${normalizeQQId(userId)}_${channelId}`;
@@ -259,197 +254,6 @@ function apply(ctx, config) {
259
254
  logger.info(`[交互绑定] 移除了QQ(${normalizeQQId(userId)})的绑定会话`);
260
255
  }
261
256
  };
262
- // 自动群昵称设置功能
263
- const autoSetGroupNickname = async (session, mcUsername, buidUsername, buidUid, targetUserId, specifiedGroupId) => {
264
- try {
265
- // 如果指定了目标用户ID,使用目标用户ID,否则使用session的用户ID
266
- const actualUserId = targetUserId || session.userId;
267
- const normalizedUserId = normalizeQQId(actualUserId);
268
- // 根据MC绑定状态设置不同的格式(临时用户名视为未绑定)
269
- const mcInfo = (mcUsername && !mcUsername.startsWith('_temp_')) ? mcUsername : "未绑定";
270
- let currentBuidUsername = buidUsername;
271
- const newNickname = `${currentBuidUsername}(ID:${mcInfo})`;
272
- // 使用指定的群ID,如果没有指定则使用配置的默认群ID
273
- const targetGroupId = specifiedGroupId || config.autoNicknameGroupId;
274
- logger.debug(`[群昵称设置] 开始处理QQ(${normalizedUserId})的群昵称设置,目标群: ${targetGroupId}`);
275
- logger.debug(`[群昵称设置] 期望昵称: "${newNickname}"`);
276
- if (session.bot.internal && targetGroupId) {
277
- // 使用规范化的QQ号调用OneBot API
278
- logger.debug(`[群昵称设置] 使用用户ID: ${normalizedUserId}`);
279
- // 先获取当前群昵称进行比对
280
- try {
281
- logger.debug(`[群昵称设置] 正在获取QQ(${normalizedUserId})在群${targetGroupId}的当前昵称...`);
282
- const currentGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
283
- const currentNickname = currentGroupInfo.card || currentGroupInfo.nickname || '';
284
- logger.debug(`[群昵称设置] 当前昵称: "${currentNickname}"`);
285
- // 如果当前昵称和目标昵称一致,跳过修改
286
- if (currentNickname === newNickname) {
287
- logger.info(`[群昵称设置] QQ(${normalizedUserId})群昵称已经是"${newNickname}",跳过修改`);
288
- return;
289
- }
290
- // 昵称不一致,先尝试获取最新的B站用户信息
291
- if (buidUid) {
292
- logger.debug(`[群昵称设置] 检测到昵称不一致,尝试获取B站UID ${buidUid} 的最新信息...`);
293
- // 1. 提取当前群昵称中的B站用户名
294
- const currentNicknameUsername = (0, helpers_1.extractBuidUsernameFromNickname)(currentNickname);
295
- logger.debug(`[群昵称设置] 从当前群昵称"${currentNickname}"中提取到B站名称: ${currentNicknameUsername || '(无法提取)'}`);
296
- try {
297
- const latestBuidUser = await validateBUID(buidUid);
298
- if (latestBuidUser && latestBuidUser.username) {
299
- logger.debug(`[群昵称设置] API返回B站用户名: "${latestBuidUser.username}"`);
300
- // 2. 智能三层判断
301
- // 情况A: API返回 == 当前群昵称中的名称
302
- if (currentNicknameUsername && latestBuidUser.username === currentNicknameUsername) {
303
- logger.info(`[群昵称设置] ✅ 当前群昵称"${currentNickname}"已包含正确的B站名称"${currentNicknameUsername}"`);
304
- // 群昵称已经正确,仅需更新数据库(如果数据库是旧的)
305
- if (latestBuidUser.username !== buidUsername) {
306
- logger.info(`[群昵称设置] 数据库中的B站名称需要更新: "${buidUsername}" → "${latestBuidUser.username}"`);
307
- try {
308
- await updateBuidInfoOnly(normalizedUserId, latestBuidUser);
309
- logger.info(`[群昵称设置] 已更新数据库中的B站用户名`);
310
- }
311
- catch (updateError) {
312
- logger.warn(`[群昵称设置] 更新数据库失败: ${updateError.message}`);
313
- }
314
- }
315
- else {
316
- logger.debug(`[群昵称设置] 数据库中的B站名称已是最新,无需更新`);
317
- }
318
- // 跳过群昵称修改
319
- logger.info(`[群昵称设置] 群昵称格式正确且名称最新,跳过修改`);
320
- return;
321
- }
322
- // 情况B: API返回 == 数据库(都是旧的)
323
- if (latestBuidUser.username === buidUsername) {
324
- logger.warn(`[群昵称设置] ⚠️ API返回的B站名称"${latestBuidUser.username}"与数据库一致,但与群昵称不符`);
325
- logger.warn(`[群昵称设置] 可能是API缓存未刷新,采用保守策略:不修改群昵称`);
326
- return;
327
- }
328
- // 情况C: API返回新数据(!= 群昵称 且 != 数据库)
329
- logger.info(`[群昵称设置] 检测到B站用户名已更新: "${buidUsername}" → "${latestBuidUser.username}"`);
330
- currentBuidUsername = latestBuidUser.username;
331
- // 更新数据库中的B站信息
332
- try {
333
- await updateBuidInfoOnly(normalizedUserId, latestBuidUser);
334
- logger.info(`[群昵称设置] 已更新数据库中的B站用户名为: ${currentBuidUsername}`);
335
- }
336
- catch (updateError) {
337
- logger.warn(`[群昵称设置] 更新数据库中的B站用户名失败: ${updateError.message}`);
338
- // 即使更新数据库失败,也继续使用最新的用户名设置昵称
339
- }
340
- }
341
- else {
342
- logger.warn(`[群昵称设置] 获取最新B站用户信息失败,使用数据库中的用户名: ${currentBuidUsername}`);
343
- }
344
- }
345
- catch (validateError) {
346
- logger.warn(`[群昵称设置] 获取最新B站用户信息时出错: ${validateError.message},使用数据库中的用户名: ${currentBuidUsername}`);
347
- // API调用失败时降级处理,使用数据库中的旧名称
348
- }
349
- }
350
- // 使用(可能已更新的)用户名重新生成昵称
351
- const finalNickname = `${currentBuidUsername}(ID:${mcInfo})`;
352
- // 昵称不一致,执行修改
353
- logger.debug(`[群昵称设置] 昵称不一致,正在修改群昵称为: "${finalNickname}"`);
354
- await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, finalNickname);
355
- logger.info(`[群昵称设置] 成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${finalNickname}"`);
356
- // 验证设置是否生效 - 再次获取群昵称确认
357
- try {
358
- await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
359
- const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
360
- const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
361
- if (verifyNickname === finalNickname) {
362
- logger.info(`[群昵称设置] ✅ 验证成功,群昵称已生效: "${verifyNickname}"`);
363
- }
364
- else {
365
- logger.warn(`[群昵称设置] ⚠️ 验证失败,期望"${finalNickname}",实际"${verifyNickname}",可能是权限不足或API延迟`);
366
- }
367
- }
368
- catch (verifyError) {
369
- logger.warn(`[群昵称设置] 无法验证群昵称设置结果: ${verifyError.message}`);
370
- }
371
- }
372
- catch (getInfoError) {
373
- // 如果获取当前昵称失败,直接尝试设置新昵称
374
- logger.warn(`[群昵称设置] 获取QQ(${normalizedUserId})当前群昵称失败: ${getInfoError.message}`);
375
- logger.warn(`[群昵称设置] 错误详情: ${JSON.stringify(getInfoError)}`);
376
- logger.debug(`[群昵称设置] 将直接尝试设置新昵称...`);
377
- // 如果传入了 buidUid,尝试获取最新的B站用户信息
378
- if (buidUid) {
379
- logger.debug(`[群昵称设置] 尝试获取B站UID ${buidUid} 的最新信息...`);
380
- try {
381
- const latestBuidUser = await validateBUID(buidUid);
382
- if (latestBuidUser && latestBuidUser.username) {
383
- logger.debug(`[群昵称设置] API返回B站用户名: "${latestBuidUser.username}"`);
384
- // 智能判断:API返回 == 数据库(都是旧的)
385
- if (latestBuidUser.username === buidUsername) {
386
- logger.warn(`[群昵称设置] ⚠️ API返回的B站名称"${latestBuidUser.username}"与数据库一致`);
387
- logger.warn(`[群昵称设置] 可能是API缓存未刷新,且无法获取当前群昵称,采用保守策略:跳过修改`);
388
- return;
389
- }
390
- // API返回新数据(!= 数据库)
391
- logger.info(`[群昵称设置] 检测到B站用户名已更新: "${buidUsername}" → "${latestBuidUser.username}"`);
392
- currentBuidUsername = latestBuidUser.username;
393
- // 更新数据库
394
- try {
395
- await updateBuidInfoOnly(normalizedUserId, latestBuidUser);
396
- logger.info(`[群昵称设置] 已更新数据库中的B站用户名为: ${currentBuidUsername}`);
397
- }
398
- catch (updateError) {
399
- logger.warn(`[群昵称设置] 更新数据库失败: ${updateError.message}`);
400
- }
401
- }
402
- else {
403
- logger.warn(`[群昵称设置] 获取最新B站用户信息失败`);
404
- }
405
- }
406
- catch (validateError) {
407
- logger.warn(`[群昵称设置] 获取最新B站用户信息失败: ${validateError.message}`);
408
- }
409
- }
410
- try {
411
- // 使用(可能已更新的)用户名生成昵称
412
- const fallbackNickname = `${currentBuidUsername}(ID:${mcInfo})`;
413
- await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, fallbackNickname);
414
- logger.info(`[群昵称设置] 成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称设置为: ${fallbackNickname}`);
415
- // 验证设置是否生效
416
- try {
417
- await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
418
- const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
419
- const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
420
- if (verifyNickname === fallbackNickname) {
421
- logger.info(`[群昵称设置] ✅ 验证成功,群昵称已生效: "${verifyNickname}"`);
422
- }
423
- else {
424
- logger.warn(`[群昵称设置] ⚠️ 验证失败,期望"${fallbackNickname}",实际"${verifyNickname}",可能是权限不足`);
425
- logger.warn(`[群昵称设置] 建议检查: 1.机器人是否为群管理员 2.群设置是否允许管理员修改昵称 3.OneBot实现是否支持该功能`);
426
- }
427
- }
428
- catch (verifyError) {
429
- logger.warn(`[群昵称设置] 无法验证群昵称设置结果: ${verifyError.message}`);
430
- }
431
- }
432
- catch (setCardError) {
433
- logger.error(`[群昵称设置] 设置群昵称失败: ${setCardError.message}`);
434
- logger.error(`[群昵称设置] 错误详情: ${JSON.stringify(setCardError)}`);
435
- throw setCardError;
436
- }
437
- }
438
- }
439
- else if (!session.bot.internal) {
440
- logger.debug(`[群昵称设置] QQ(${normalizedUserId})bot不支持OneBot内部API,跳过自动群昵称设置`);
441
- }
442
- else if (!targetGroupId) {
443
- logger.debug(`[群昵称设置] QQ(${normalizedUserId})未配置自动群昵称设置目标群,跳过群昵称设置`);
444
- }
445
- }
446
- catch (error) {
447
- const actualUserId = targetUserId || session.userId;
448
- const normalizedUserId = normalizeQQId(actualUserId);
449
- logger.error(`[群昵称设置] QQ(${normalizedUserId})自动群昵称设置失败: ${error.message}`);
450
- logger.error(`[群昵称称设置] 完整错误信息: ${JSON.stringify(error)}`);
451
- }
452
- };
453
257
  // 检查是否为无关输入
454
258
  const checkIrrelevantInput = (bindingSession, content) => {
455
259
  if (!content)
@@ -588,7 +392,7 @@ function apply(ctx, config) {
588
392
  const normalizedUserId = normalizeQQId(session.userId);
589
393
  logger.info(`[新人绑定] 用户QQ(${normalizedUserId})加入群聊,准备发送绑定提醒`);
590
394
  // 检查用户是否已有绑定记录
591
- const existingBind = await getMcBindByQQId(normalizedUserId);
395
+ const existingBind = await services.database.getMcBindByQQId(normalizedUserId);
592
396
  // 如果用户已完成全部绑定,不需要提醒
593
397
  if (existingBind && existingBind.mcUsername && existingBind.buidUid) {
594
398
  logger.info(`[新人绑定] 用户QQ(${normalizedUserId})已完成全部绑定,跳过提醒`);
@@ -729,7 +533,7 @@ function apply(ctx, config) {
729
533
  logger.info(`[天选开奖] 接收到天选事件: ${lotteryData.lottery_id},奖品: ${lotteryData.reward_name},中奖人数: ${lotteryData.winners.length}`);
730
534
  }
731
535
  // 异步处理天选开奖数据(不阻塞响应)
732
- handleLotteryResult(lotteryData).catch(error => {
536
+ lotteryHandler.handleLotteryResult(lotteryData).catch(error => {
733
537
  logger.error(`[天选开奖] 异步处理天选开奖数据失败: ${error.message}`);
734
538
  });
735
539
  // 立即返回成功响应
@@ -1018,6 +822,9 @@ function apply(ctx, config) {
1018
822
  }
1019
823
  return extractedId;
1020
824
  };
825
+ // =========== 实例化服务容器 ===========
826
+ // 统一管理所有服务的实例化,解决服务初始化分散的问题
827
+ const services = new service_container_1.ServiceContainer(ctx, config, loggerService, mcidbindRepo, normalizeQQId);
1021
828
  // 获取用户友好的错误信息
1022
829
  // 封装发送消息的函数,处理私聊和群聊的不同格式
1023
830
  const sendMessage = async (session, content, options) => {
@@ -1143,727 +950,6 @@ function apply(ctx, config) {
1143
950
  return diffDays >= config.cooldownDays * multiplier;
1144
951
  };
1145
952
  // 根据QQ号查询MCIDBIND表中的绑定信息
1146
- const getMcBindByQQId = async (qqId) => {
1147
- try {
1148
- // 处理空值
1149
- if (!qqId) {
1150
- logger.warn(`[MCIDBIND] 尝试查询空QQ号`);
1151
- return null;
1152
- }
1153
- const normalizedQQId = normalizeQQId(qqId);
1154
- // 查询MCIDBIND表中对应QQ号的绑定记录
1155
- return await mcidbindRepo.findByQQId(normalizedQQId);
1156
- }
1157
- catch (error) {
1158
- logError('MCIDBIND', qqId, `根据QQ号查询绑定信息失败: ${error.message}`);
1159
- return null;
1160
- }
1161
- };
1162
- // 根据MC用户名查询MCIDBIND表中的绑定信息
1163
- const getMcBindByUsername = async (mcUsername) => {
1164
- // 处理空值
1165
- if (!mcUsername) {
1166
- logger.warn(`[MCIDBIND] 尝试查询空MC用户名`);
1167
- return null;
1168
- }
1169
- // 使用 Repository 查询
1170
- return await mcidbindRepo.findByMCUsername(mcUsername);
1171
- };
1172
- // 根据QQ号确保获取完整的用户ID (处理纯QQ号的情况)
1173
- const ensureFullUserId = (userId) => {
1174
- // 如果已经包含冒号,说明已经是完整的用户ID
1175
- if (userId.includes(':'))
1176
- return userId;
1177
- // 否则,检查是否为数字(纯QQ号)
1178
- if (/^\d+$/.test(userId)) {
1179
- // 默认使用onebot平台前缀
1180
- return `onebot:${userId}`;
1181
- }
1182
- // 如果不是数字也没有冒号,保持原样返回
1183
- logger.warn(`[用户ID] 无法确定用户ID格式: ${userId}`);
1184
- return userId;
1185
- };
1186
- // 创建或更新MCIDBIND表中的绑定信息
1187
- const createOrUpdateMcBind = async (userId, mcUsername, mcUuid, isAdmin) => {
1188
- try {
1189
- // 验证输入参数
1190
- if (!userId) {
1191
- logger.error(`[MCIDBIND] 创建/更新绑定失败: 无效的用户ID`);
1192
- return false;
1193
- }
1194
- if (!mcUsername) {
1195
- logger.error(`[MCIDBIND] 创建/更新绑定失败: 无效的MC用户名`);
1196
- return false;
1197
- }
1198
- const normalizedQQId = normalizeQQId(userId);
1199
- if (!normalizedQQId) {
1200
- logger.error(`[MCIDBIND] 创建/更新绑定失败: 无法提取有效的QQ号`);
1201
- return false;
1202
- }
1203
- // 查询是否已存在绑定记录
1204
- let bind = await getMcBindByQQId(normalizedQQId);
1205
- if (bind) {
1206
- // 更新现有记录,但保留管理员状态
1207
- const updateData = {
1208
- mcUsername,
1209
- mcUuid,
1210
- lastModified: new Date()
1211
- };
1212
- // 仅当指定了isAdmin参数时更新管理员状态
1213
- if (typeof isAdmin !== 'undefined') {
1214
- updateData.isAdmin = isAdmin;
1215
- }
1216
- await mcidbindRepo.update(normalizedQQId, updateData);
1217
- logger.info(`[MCIDBIND] 更新绑定: QQ=${normalizedQQId}, MC用户名=${mcUsername}, UUID=${mcUuid}`);
1218
- return true;
1219
- }
1220
- else {
1221
- // 创建新记录
1222
- try {
1223
- await mcidbindRepo.create({
1224
- qqId: normalizedQQId,
1225
- mcUsername,
1226
- mcUuid,
1227
- lastModified: new Date(),
1228
- isAdmin: isAdmin || false
1229
- });
1230
- logger.info(`[MCIDBIND] 创建绑定: QQ=${normalizedQQId}, MC用户名=${mcUsername}, UUID=${mcUuid}`);
1231
- return true;
1232
- }
1233
- catch (createError) {
1234
- logError('MCIDBIND', userId, `创建绑定失败: MC用户名=${mcUsername}, 错误=${createError.message}`);
1235
- return false;
1236
- }
1237
- }
1238
- }
1239
- catch (error) {
1240
- logError('MCIDBIND', userId, `创建/更新绑定失败: MC用户名=${mcUsername}, 错误=${error.message}`);
1241
- return false;
1242
- }
1243
- };
1244
- // 删除MCIDBIND表中的绑定信息 (同时解绑MC和B站账号)
1245
- const deleteMcBind = async (userId) => {
1246
- try {
1247
- // 验证输入参数
1248
- if (!userId) {
1249
- logger.error(`[MCIDBIND] 删除绑定失败: 无效的用户ID`);
1250
- return false;
1251
- }
1252
- const normalizedQQId = normalizeQQId(userId);
1253
- if (!normalizedQQId) {
1254
- logger.error(`[MCIDBIND] 删除绑定失败: 无法提取有效的QQ号`);
1255
- return false;
1256
- }
1257
- // 查询是否存在绑定记录
1258
- const bind = await getMcBindByQQId(normalizedQQId);
1259
- if (bind) {
1260
- // 删除整个绑定记录,包括MC和B站账号
1261
- const removedCount = await mcidbindRepo.delete(normalizedQQId);
1262
- // 检查是否真正删除成功
1263
- if (removedCount > 0) {
1264
- let logMessage = `[MCIDBIND] 删除绑定: QQ=${normalizedQQId}`;
1265
- if (bind.mcUsername)
1266
- logMessage += `, MC用户名=${bind.mcUsername}`;
1267
- if (bind.buidUid)
1268
- logMessage += `, B站UID=${bind.buidUid}(${bind.buidUsername})`;
1269
- logger.info(logMessage);
1270
- return true;
1271
- }
1272
- else {
1273
- logger.warn(`[MCIDBIND] 删除绑定异常: QQ=${normalizedQQId}, 可能未实际删除`);
1274
- return false;
1275
- }
1276
- }
1277
- logger.warn(`[MCIDBIND] 删除绑定失败: QQ=${normalizedQQId}不存在绑定记录`);
1278
- return false;
1279
- }
1280
- catch (error) {
1281
- logError('MCIDBIND', userId, `删除绑定失败: 错误=${error.message}`);
1282
- return false;
1283
- }
1284
- };
1285
- // 检查MC用户名是否已被其他QQ号绑定(支持不区分大小写和UUID检查)
1286
- const checkUsernameExists = async (username, currentUserId, uuid) => {
1287
- try {
1288
- // 验证输入参数
1289
- if (!username) {
1290
- logger.warn(`[绑定检查] 尝试检查空MC用户名`);
1291
- return false;
1292
- }
1293
- // 跳过临时用户名的检查
1294
- if (username.startsWith('_temp_')) {
1295
- return false;
1296
- }
1297
- // 使用不区分大小写的查询
1298
- const bind = await mcidbindRepo.findByUsernameIgnoreCase(username);
1299
- // 如果没有绑定,返回false
1300
- if (!bind)
1301
- return false;
1302
- // 如果绑定的用户名是临时用户名,视为未绑定
1303
- if (bind.mcUsername && bind.mcUsername.startsWith('_temp_')) {
1304
- return false;
1305
- }
1306
- // 如果提供了 UUID,检查是否为同一个 MC 账号(用户改名场景)
1307
- if (uuid && bind.mcUuid) {
1308
- const cleanUuid = uuid.replace(/-/g, '');
1309
- const bindCleanUuid = bind.mcUuid.replace(/-/g, '');
1310
- if (cleanUuid === bindCleanUuid) {
1311
- // 同一个 UUID,说明是用户改名,允许绑定
1312
- logger.info(`[绑定检查] 检测到MC账号改名: UUID=${uuid}, 旧用户名=${bind.mcUsername}, 新用户名=${username}`);
1313
- return false;
1314
- }
1315
- }
1316
- // 如果提供了当前用户ID,需要排除当前用户
1317
- if (currentUserId) {
1318
- const normalizedCurrentId = normalizeQQId(currentUserId);
1319
- // 如果绑定的用户就是当前用户,返回false,表示没有被其他用户绑定
1320
- return normalizedCurrentId ? bind.qqId !== normalizedCurrentId : true;
1321
- }
1322
- return true;
1323
- }
1324
- catch (error) {
1325
- logError('绑定检查', currentUserId || 'system', `检查用户名"${username}"是否已被绑定失败: ${error.message}`);
1326
- return false;
1327
- }
1328
- };
1329
- // 使用Mojang API验证用户名并获取UUID
1330
- const validateUsername = async (username) => {
1331
- try {
1332
- logger.debug(`[Mojang API] 开始验证用户名: ${username}`);
1333
- const response = await axios_1.default.get(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
1334
- timeout: 10000, // 添加10秒超时
1335
- headers: {
1336
- 'User-Agent': 'KoishiMCVerifier/1.0', // 添加User-Agent头
1337
- }
1338
- });
1339
- if (response.status === 200 && response.data) {
1340
- logger.debug(`[Mojang API] 用户名"${username}"验证成功,UUID: ${response.data.id},标准名称: ${response.data.name}`);
1341
- return {
1342
- id: response.data.id,
1343
- name: response.data.name // 使用Mojang返回的正确大小写
1344
- };
1345
- }
1346
- return null;
1347
- }
1348
- catch (error) {
1349
- if (axios_1.default.isAxiosError(error) && error.response?.status === 404) {
1350
- logger.warn(`[Mojang API] 用户名"${username}"不存在`);
1351
- }
1352
- else if (axios_1.default.isAxiosError(error) && error.code === 'ECONNABORTED') {
1353
- logger.error(`[Mojang API] 验证用户名"${username}"时请求超时: ${error.message}`);
1354
- }
1355
- else {
1356
- // 记录更详细的错误信息
1357
- const errorMessage = axios_1.default.isAxiosError(error)
1358
- ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
1359
- : error.message || '未知错误';
1360
- logger.error(`[Mojang API] 验证用户名"${username}"时发生错误: ${errorMessage}`);
1361
- // 如果是网络相关错误,尝试使用备用API检查
1362
- if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
1363
- error.code === 'ETIMEDOUT' ||
1364
- error.code === 'ECONNRESET' ||
1365
- error.code === 'ECONNREFUSED' ||
1366
- error.code === 'ECONNABORTED' ||
1367
- error.response?.status === 429 || // 添加429 (Too Many Requests)
1368
- error.response?.status === 403)) { // 添加403 (Forbidden)
1369
- // 尝试使用playerdb.co作为备用API
1370
- logger.info(`[Mojang API] 遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
1371
- return tryBackupAPI(username);
1372
- }
1373
- }
1374
- return null;
1375
- }
1376
- };
1377
- // 使用备用API验证用户名
1378
- const tryBackupAPI = async (username) => {
1379
- logger.info(`[备用API] 尝试使用备用API验证用户名"${username}"`);
1380
- try {
1381
- // 使用playerdb.co作为备用API
1382
- const backupResponse = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${username}`, {
1383
- timeout: 10000,
1384
- headers: {
1385
- 'User-Agent': 'KoishiMCVerifier/1.0'
1386
- }
1387
- });
1388
- if (backupResponse.status === 200 && backupResponse.data?.code === "player.found") {
1389
- const playerData = backupResponse.data.data.player;
1390
- const rawId = playerData.raw_id || playerData.id.replace(/-/g, ''); // 确保使用不带连字符的UUID
1391
- logger.info(`[备用API] 用户名"${username}"验证成功,UUID: ${rawId},标准名称: ${playerData.username}`);
1392
- return {
1393
- id: rawId, // 确保使用不带连字符的UUID
1394
- name: playerData.username
1395
- };
1396
- }
1397
- logger.warn(`[备用API] 用户名"${username}"验证失败: ${JSON.stringify(backupResponse.data)}`);
1398
- return null;
1399
- }
1400
- catch (backupError) {
1401
- const errorMsg = axios_1.default.isAxiosError(backupError)
1402
- ? `${backupError.message}, 状态码: ${backupError.response?.status || '未知'}`
1403
- : backupError.message || '未知错误';
1404
- logger.error(`[备用API] 验证用户名"${username}"失败: ${errorMsg}`);
1405
- return null;
1406
- }
1407
- };
1408
- // 获取MC头图URL
1409
- const getCrafatarUrl = (uuid) => {
1410
- if (!uuid)
1411
- return null;
1412
- // 检查UUID格式 (不带连字符应为32位,带连字符应为36位)
1413
- 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;
1414
- if (!uuidRegex.test(uuid)) {
1415
- logger.warn(`[MC头图] UUID "${uuid}" 格式无效,无法生成头图URL`);
1416
- return null;
1417
- }
1418
- // 移除任何连字符,Crafatar接受不带连字符的UUID
1419
- const cleanUuid = uuid.replace(/-/g, '');
1420
- // 直接生成URL
1421
- const url = `https://crafatar.com/avatars/${cleanUuid}`;
1422
- logger.debug(`[MC头图] 为UUID "${cleanUuid}" 生成头图URL`);
1423
- return url;
1424
- };
1425
- // 使用Starlight SkinAPI获取皮肤渲染
1426
- const getStarlightSkinUrl = (username) => {
1427
- if (!username)
1428
- return null;
1429
- // 可用的动作列表 (共16种)
1430
- const poses = [
1431
- 'default', // 默认站立
1432
- 'marching', // 行军
1433
- 'walking', // 行走
1434
- 'crouching', // 下蹲
1435
- 'crossed', // 交叉手臂
1436
- 'crisscross', // 交叉腿
1437
- 'cheering', // 欢呼
1438
- 'relaxing', // 放松
1439
- 'trudging', // 艰难行走
1440
- 'cowering', // 退缩
1441
- 'pointing', // 指向
1442
- 'lunging', // 前冲
1443
- 'dungeons', // 地下城风格
1444
- 'facepalm', // 捂脸
1445
- 'mojavatar', // Mojave姿态
1446
- 'head', // 头部特写
1447
- ];
1448
- // 随机选择一个动作
1449
- const randomPose = poses[Math.floor(Math.random() * poses.length)];
1450
- // 视图类型(full为全身图)
1451
- const viewType = 'full';
1452
- // 生成URL
1453
- const url = `https://starlightskins.lunareclipse.studio/render/${randomPose}/${username}/${viewType}`;
1454
- logger.debug(`[Starlight皮肤] 为用户名"${username}"生成动作"${randomPose}"的渲染URL`);
1455
- return url;
1456
- };
1457
- // 格式化UUID (添加连字符,使其符合标准格式)
1458
- const formatUuid = (uuid) => {
1459
- if (!uuid)
1460
- return '未知';
1461
- if (uuid.includes('-'))
1462
- return uuid; // 已经是带连字符的格式
1463
- // 确保UUID长度正确
1464
- if (uuid.length !== 32) {
1465
- logger.warn(`[UUID] UUID "${uuid}" 长度异常,无法格式化`);
1466
- return uuid;
1467
- }
1468
- return `${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}`;
1469
- };
1470
- // 检查是否为管理员 (QQ号作为主键检查)
1471
- const isAdmin = async (userId) => {
1472
- // 主人始终是管理员
1473
- const normalizedMasterId = normalizeQQId(config.masterId);
1474
- const normalizedQQId = normalizeQQId(userId);
1475
- if (normalizedQQId === normalizedMasterId)
1476
- return true;
1477
- // 查询MCIDBIND表中是否是管理员
1478
- try {
1479
- const bind = await getMcBindByQQId(normalizedQQId);
1480
- return bind && bind.isAdmin === true;
1481
- }
1482
- catch (error) {
1483
- logger.error(`[权限检查] QQ(${normalizedQQId})的管理员状态查询失败: ${error.message}`);
1484
- return false;
1485
- }
1486
- };
1487
- // 检查是否为主人 (QQ号作为主键检查)
1488
- const isMaster = (qqId) => {
1489
- const normalizedMasterId = normalizeQQId(config.masterId);
1490
- const normalizedQQId = normalizeQQId(qqId);
1491
- return normalizedQQId === normalizedMasterId;
1492
- };
1493
- // =========== BUID相关功能 ===========
1494
- // 验证BUID是否存在
1495
- const validateBUID = async (buid) => {
1496
- try {
1497
- if (!buid || !/^\d+$/.test(buid)) {
1498
- logWarn('B站账号验证', `无效的B站UID格式: ${buid}`);
1499
- return null;
1500
- }
1501
- logDebug('B站账号验证', `验证B站UID: ${buid}`);
1502
- const response = await axios_1.default.get(`${config.zminfoApiUrl}/api/user/${buid}`, {
1503
- timeout: 10000,
1504
- headers: {
1505
- 'User-Agent': 'Koishi-MCID-Bot/1.0'
1506
- }
1507
- });
1508
- if (response.data.success && response.data.data && response.data.data.user) {
1509
- const user = response.data.data.user;
1510
- logDebug('B站账号验证', `B站UID ${buid} 验证成功: ${user.username}`);
1511
- return user;
1512
- }
1513
- else {
1514
- logWarn('B站账号验证', `B站UID ${buid} 不存在或API返回失败: ${response.data.message}`);
1515
- return null;
1516
- }
1517
- }
1518
- catch (error) {
1519
- if (error.response?.status === 404) {
1520
- logWarn('B站账号验证', `B站UID ${buid} 不存在`);
1521
- return null;
1522
- }
1523
- logError('B站账号验证', 'system', `验证B站UID ${buid} 时出错: ${error.message}`);
1524
- throw new Error(`无法验证B站UID: ${error.message}`);
1525
- }
1526
- };
1527
- // 根据B站UID查询绑定信息
1528
- const getBuidBindByBuid = async (buid) => {
1529
- try {
1530
- if (!buid) {
1531
- logger.warn(`[B站账号绑定] 尝试查询空B站UID`);
1532
- return null;
1533
- }
1534
- const bind = await mcidbindRepo.findByBuidUid(buid);
1535
- return bind;
1536
- }
1537
- catch (error) {
1538
- logError('B站账号绑定', 'system', `根据B站UID(${buid})查询绑定信息失败: ${error.message}`);
1539
- return null;
1540
- }
1541
- };
1542
- // 检查B站UID是否已被绑定
1543
- const checkBuidExists = async (buid, currentUserId) => {
1544
- try {
1545
- const bind = await getBuidBindByBuid(buid);
1546
- if (!bind)
1547
- return false;
1548
- // 如果指定了当前用户ID,则排除当前用户的绑定
1549
- if (currentUserId) {
1550
- const normalizedCurrentId = normalizeQQId(currentUserId);
1551
- return bind.qqId !== normalizedCurrentId;
1552
- }
1553
- return true;
1554
- }
1555
- catch (error) {
1556
- logError('B站账号绑定', 'system', `检查B站UID(${buid})是否存在时出错: ${error.message}`);
1557
- return false;
1558
- }
1559
- };
1560
- // 创建或更新B站账号绑定
1561
- const createOrUpdateBuidBind = async (userId, buidUser) => {
1562
- try {
1563
- const normalizedQQId = normalizeQQId(userId);
1564
- if (!normalizedQQId) {
1565
- logger.error(`[B站账号绑定] 创建/更新绑定失败: 无法提取有效的QQ号`);
1566
- return false;
1567
- }
1568
- // 检查该UID是否已被其他用户绑定(安全检查)
1569
- const existingBuidBind = await getBuidBindByBuid(buidUser.uid);
1570
- if (existingBuidBind && existingBuidBind.qqId !== normalizedQQId) {
1571
- logger.error(`[B站账号绑定] 安全检查失败: B站UID ${buidUser.uid} 已被QQ(${existingBuidBind.qqId})绑定,无法为QQ(${normalizedQQId})绑定`);
1572
- return false;
1573
- }
1574
- // 查询是否已存在绑定记录
1575
- let bind = await getMcBindByQQId(normalizedQQId);
1576
- const updateData = {
1577
- buidUid: buidUser.uid,
1578
- buidUsername: buidUser.username,
1579
- guardLevel: buidUser.guard_level || 0,
1580
- guardLevelText: buidUser.guard_level_text || '',
1581
- maxGuardLevel: buidUser.max_guard_level || 0,
1582
- maxGuardLevelText: buidUser.max_guard_level_text || '',
1583
- medalName: buidUser.medal?.name || '',
1584
- medalLevel: buidUser.medal?.level || 0,
1585
- wealthMedalLevel: buidUser.wealthMedalLevel || 0,
1586
- lastActiveTime: buidUser.last_active_time ? new Date(buidUser.last_active_time) : new Date(),
1587
- lastModified: new Date()
1588
- };
1589
- if (bind) {
1590
- await mcidbindRepo.update(normalizedQQId, updateData);
1591
- logger.info(`[B站账号绑定] 更新绑定: QQ=${normalizedQQId}, B站UID=${buidUser.uid}, 用户名=${buidUser.username}`);
1592
- }
1593
- else {
1594
- // 为跳过MC绑定的用户生成唯一的临时用户名,避免UNIQUE constraint冲突
1595
- const tempMcUsername = `_temp_skip_${normalizedQQId}_${Date.now()}`;
1596
- const newBind = {
1597
- qqId: normalizedQQId,
1598
- mcUsername: tempMcUsername,
1599
- mcUuid: '',
1600
- isAdmin: false,
1601
- whitelist: [],
1602
- tags: [],
1603
- ...updateData
1604
- };
1605
- await mcidbindRepo.create(newBind);
1606
- logger.info(`[B站账号绑定] 创建绑定(跳过MC): QQ=${normalizedQQId}, B站UID=${buidUser.uid}, 用户名=${buidUser.username}, 临时MC用户名=${tempMcUsername}`);
1607
- }
1608
- return true;
1609
- }
1610
- catch (error) {
1611
- logError('B站账号绑定', userId, `创建/更新B站账号绑定失败: ${error.message}`);
1612
- return false;
1613
- }
1614
- };
1615
- // 仅更新B站信息,不更新绑定时间(用于查询时刷新数据)
1616
- const updateBuidInfoOnly = async (userId, buidUser) => {
1617
- try {
1618
- const normalizedQQId = normalizeQQId(userId);
1619
- if (!normalizedQQId) {
1620
- logger.error(`[B站账号信息更新] 更新失败: 无法提取有效的QQ号`);
1621
- return false;
1622
- }
1623
- // 查询是否已存在绑定记录
1624
- const bind = await getMcBindByQQId(normalizedQQId);
1625
- if (!bind) {
1626
- logger.warn(`[B站账号信息更新] QQ(${normalizedQQId})没有绑定记录,无法更新B站信息`);
1627
- return false;
1628
- }
1629
- // 仅更新B站相关字段,不更新lastModified
1630
- const updateData = {
1631
- buidUsername: buidUser.username,
1632
- guardLevel: buidUser.guard_level || 0,
1633
- guardLevelText: buidUser.guard_level_text || '',
1634
- maxGuardLevel: buidUser.max_guard_level || 0,
1635
- maxGuardLevelText: buidUser.max_guard_level_text || '',
1636
- medalName: buidUser.medal?.name || '',
1637
- medalLevel: buidUser.medal?.level || 0,
1638
- wealthMedalLevel: buidUser.wealthMedalLevel || 0,
1639
- lastActiveTime: buidUser.last_active_time ? new Date(buidUser.last_active_time) : new Date()
1640
- };
1641
- await mcidbindRepo.update(normalizedQQId, updateData);
1642
- logger.info(`[B站账号信息更新] 刷新信息: QQ=${normalizedQQId}, B站UID=${bind.buidUid}, 用户名=${buidUser.username}`);
1643
- return true;
1644
- }
1645
- catch (error) {
1646
- logError('B站账号信息更新', userId, `更新B站账号信息失败: ${error.message}`);
1647
- return false;
1648
- }
1649
- };
1650
- // =========== 辅助函数 ===========
1651
- // RCON连接检查
1652
- const checkRconConnections = async () => {
1653
- if (!config.servers || config.servers.length === 0) {
1654
- logger.info('[RCON检查] 未配置任何服务器,跳过RCON检查');
1655
- return;
1656
- }
1657
- const results = {};
1658
- for (const server of config.servers) {
1659
- try {
1660
- logger.info(`[RCON检查] 正在检查服务器 ${server.name} (${server.rconAddress}) 的连接状态`);
1661
- // 尝试执行/list命令来测试连接 (使用RCON管理器)
1662
- await rconManager.executeCommand(server, 'list');
1663
- // 如果没有抛出异常,表示连接成功
1664
- logger.info(`[RCON检查] 服务器 ${server.name} 连接成功`);
1665
- results[server.id] = true;
1666
- }
1667
- catch (error) {
1668
- logger.error(`[RCON检查] 服务器 ${server.name} 连接失败: ${error.message}`);
1669
- results[server.id] = false;
1670
- }
1671
- }
1672
- // 生成检查结果摘要
1673
- const totalServers = config.servers.length;
1674
- const successCount = Object.values(results).filter(Boolean).length;
1675
- const failCount = totalServers - successCount;
1676
- logger.info(`[RCON检查] 检查完成: ${successCount}/${totalServers} 个服务器连接成功,${failCount} 个连接失败`);
1677
- if (failCount > 0) {
1678
- const failedServers = config.servers
1679
- .filter(server => !results[server.id])
1680
- .map(server => server.name)
1681
- .join(', ');
1682
- logger.warn(`[RCON检查] 以下服务器连接失败,白名单功能可能无法正常工作: ${failedServers}`);
1683
- }
1684
- };
1685
- // 使用Mojang API通过UUID查询用户名
1686
- const getUsernameByUuid = async (uuid) => {
1687
- try {
1688
- // 确保UUID格式正确(去除连字符)
1689
- const cleanUuid = uuid.replace(/-/g, '');
1690
- logger.debug(`[Mojang API] 通过UUID "${cleanUuid}" 查询用户名`);
1691
- const response = await axios_1.default.get(`https://api.mojang.com/user/profile/${cleanUuid}`, {
1692
- timeout: 10000,
1693
- headers: {
1694
- 'User-Agent': 'KoishiMCVerifier/1.0',
1695
- }
1696
- });
1697
- if (response.status === 200 && response.data) {
1698
- // 从返回数据中提取用户名
1699
- const username = response.data.name;
1700
- logger.debug(`[Mojang API] UUID "${cleanUuid}" 当前用户名: ${username}`);
1701
- return username;
1702
- }
1703
- logger.warn(`[Mojang API] UUID "${cleanUuid}" 查询不到用户名`);
1704
- return null;
1705
- }
1706
- catch (error) {
1707
- // 如果是网络相关错误,尝试使用备用API
1708
- if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
1709
- error.code === 'ETIMEDOUT' ||
1710
- error.code === 'ECONNRESET' ||
1711
- error.code === 'ECONNREFUSED' ||
1712
- error.code === 'ECONNABORTED' ||
1713
- error.response?.status === 429 || // 添加429 (Too Many Requests)
1714
- error.response?.status === 403)) { // 添加403 (Forbidden)
1715
- logger.info(`[Mojang API] 通过UUID查询用户名时遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
1716
- return getUsernameByUuidBackupAPI(uuid);
1717
- }
1718
- const errorMessage = axios_1.default.isAxiosError(error)
1719
- ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
1720
- : error.message || '未知错误';
1721
- logger.error(`[Mojang API] 通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
1722
- return null;
1723
- }
1724
- };
1725
- // 使用备用API通过UUID查询用户名
1726
- const getUsernameByUuidBackupAPI = async (uuid) => {
1727
- try {
1728
- // 确保UUID格式正确,备用API支持带连字符的UUID
1729
- const formattedUuid = uuid.includes('-') ? uuid : formatUuid(uuid);
1730
- logger.debug(`[备用API] 通过UUID "${formattedUuid}" 查询用户名`);
1731
- const response = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${formattedUuid}`, {
1732
- timeout: 10000,
1733
- headers: {
1734
- 'User-Agent': 'KoishiMCVerifier/1.0',
1735
- }
1736
- });
1737
- if (response.status === 200 && response.data?.code === "player.found") {
1738
- const playerData = response.data.data.player;
1739
- logger.debug(`[备用API] UUID "${formattedUuid}" 当前用户名: ${playerData.username}`);
1740
- return playerData.username;
1741
- }
1742
- logger.warn(`[备用API] UUID "${formattedUuid}" 查询不到用户名: ${JSON.stringify(response.data)}`);
1743
- return null;
1744
- }
1745
- catch (error) {
1746
- const errorMessage = axios_1.default.isAxiosError(error)
1747
- ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
1748
- : error.message || '未知错误';
1749
- logger.error(`[备用API] 通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
1750
- return null;
1751
- }
1752
- };
1753
- // 检查并更新用户名(如果与当前数据库中的不同)
1754
- const checkAndUpdateUsername = async (bind) => {
1755
- try {
1756
- if (!bind || !bind.mcUuid) {
1757
- logger.warn(`[用户名更新] 无法检查用户名更新: 空绑定或空UUID`);
1758
- return bind;
1759
- }
1760
- // 通过UUID查询最新用户名
1761
- const latestUsername = await getUsernameByUuid(bind.mcUuid);
1762
- if (!latestUsername) {
1763
- logger.warn(`[用户名更新] 无法获取UUID "${bind.mcUuid}" 的最新用户名`);
1764
- return bind;
1765
- }
1766
- // 如果用户名与数据库中的不同,更新数据库(使用规范化比较,不区分大小写)
1767
- const normalizedLatest = (0, helpers_1.normalizeUsername)(latestUsername);
1768
- const normalizedCurrent = (0, helpers_1.normalizeUsername)(bind.mcUsername);
1769
- if (normalizedLatest !== normalizedCurrent) {
1770
- logger.info(`[用户名更新] 用户 QQ(${bind.qqId}) 的Minecraft用户名已变更: ${bind.mcUsername} -> ${latestUsername}`);
1771
- // 更新数据库中的用户名
1772
- await ctx.database.set('mcidbind', { qqId: bind.qqId }, {
1773
- mcUsername: latestUsername
1774
- });
1775
- // 更新返回的绑定对象
1776
- bind.mcUsername = latestUsername;
1777
- }
1778
- return bind;
1779
- }
1780
- catch (error) {
1781
- logger.error(`[用户名更新] 检查和更新用户名失败: ${error.message}`);
1782
- return bind;
1783
- }
1784
- };
1785
- /**
1786
- * 智能缓存版本的改名检测函数
1787
- * 特性:
1788
- * - 24小时冷却期(失败>=3次时延长到72小时)
1789
- * - 失败计数追踪
1790
- * - 成功时重置失败计数
1791
- * @param bind 绑定记录
1792
- * @returns 更新后的绑定记录
1793
- */
1794
- const checkAndUpdateUsernameWithCache = async (bind) => {
1795
- try {
1796
- if (!bind || !bind.mcUuid) {
1797
- logger.warn(`[改名检测缓存] 无法检查用户名更新: 空绑定或空UUID`);
1798
- return bind;
1799
- }
1800
- const now = new Date();
1801
- const failCount = bind.usernameCheckFailCount || 0;
1802
- // 根据失败次数决定冷却期:普通24小时,失败>=3次则72小时
1803
- const cooldownHours = failCount >= 3 ? 72 : 24;
1804
- // 检查是否在冷却期内
1805
- if (bind.usernameLastChecked) {
1806
- const lastCheck = new Date(bind.usernameLastChecked);
1807
- const hoursSinceCheck = (now.getTime() - lastCheck.getTime()) / (1000 * 60 * 60);
1808
- if (hoursSinceCheck < cooldownHours) {
1809
- logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 在冷却期内(${hoursSinceCheck.toFixed(1)}h/${cooldownHours}h),跳过检查`);
1810
- return bind;
1811
- }
1812
- }
1813
- logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 开始检查用户名变更(失败计数: ${failCount})`);
1814
- // 执行实际的改名检测
1815
- const oldUsername = bind.mcUsername;
1816
- const updatedBind = await checkAndUpdateUsername(bind);
1817
- // 判断检测是否成功
1818
- const detectionSuccess = updatedBind.mcUsername !== null && updatedBind.mcUsername !== undefined;
1819
- if (detectionSuccess) {
1820
- // 检测成功
1821
- const usernameChanged = (0, helpers_1.normalizeUsername)(updatedBind.mcUsername) !== (0, helpers_1.normalizeUsername)(oldUsername);
1822
- // 更新检查时间和重置失败计数
1823
- await mcidbindRepo.update(bind.qqId, {
1824
- usernameLastChecked: now,
1825
- usernameCheckFailCount: 0
1826
- });
1827
- if (usernameChanged) {
1828
- logger.info(`[改名检测缓存] QQ(${bind.qqId}) 用户名已变更: ${oldUsername} -> ${updatedBind.mcUsername}`);
1829
- }
1830
- else {
1831
- logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 用户名无变更: ${updatedBind.mcUsername}`);
1832
- }
1833
- // 更新返回对象的缓存字段
1834
- updatedBind.usernameLastChecked = now;
1835
- updatedBind.usernameCheckFailCount = 0;
1836
- }
1837
- else {
1838
- // 检测失败��API失败或返回null)
1839
- const newFailCount = failCount + 1;
1840
- await mcidbindRepo.update(bind.qqId, {
1841
- usernameLastChecked: now,
1842
- usernameCheckFailCount: newFailCount
1843
- });
1844
- logger.warn(`[改名检测缓存] QQ(${bind.qqId}) 检测失败,失败计数: ${newFailCount}`);
1845
- // 更新返回对象的缓存字段
1846
- updatedBind.usernameLastChecked = now;
1847
- updatedBind.usernameCheckFailCount = newFailCount;
1848
- }
1849
- return updatedBind;
1850
- }
1851
- catch (error) {
1852
- logger.error(`[改名检测缓存] 检查和更新用户名失败: ${error.message}`);
1853
- // 失败时也更新检查时间和递增失败计数
1854
- try {
1855
- const failCount = bind.usernameCheckFailCount || 0;
1856
- await mcidbindRepo.update(bind.qqId, {
1857
- usernameLastChecked: new Date(),
1858
- usernameCheckFailCount: failCount + 1
1859
- });
1860
- }
1861
- catch (updateError) {
1862
- logger.error(`[改名检测缓存] 更新失败计数时出错: ${updateError.message}`);
1863
- }
1864
- return bind;
1865
- }
1866
- };
1867
953
  // 安全地替换命令模板
1868
954
  const safeCommandReplace = (template, mcid) => {
1869
955
  // 过滤可能导致命令注入的字符
@@ -1921,6 +1007,30 @@ function apply(ctx, config) {
1921
1007
  // 如果ID未匹配到,尝试通过名称匹配
1922
1008
  return getServerConfigByName(serverIdOrName);
1923
1009
  };
1010
+ // =========== 权限检查函数 ===========
1011
+ // 检查用户是否为管理员
1012
+ const isAdmin = async (userId) => {
1013
+ // 主人始终是管理员
1014
+ const normalizedMasterId = normalizeQQId(config.masterId);
1015
+ const normalizedQQId = normalizeQQId(userId);
1016
+ if (normalizedQQId === normalizedMasterId)
1017
+ return true;
1018
+ // 查询MCIDBIND表中是否是管理员
1019
+ try {
1020
+ const bind = await services.database.getMcBindByQQId(normalizedQQId);
1021
+ return bind && bind.isAdmin === true;
1022
+ }
1023
+ catch (error) {
1024
+ logger.error(`[权限检查] QQ(${normalizedQQId})的管理员状态查询失败: ${error.message}`);
1025
+ return false;
1026
+ }
1027
+ };
1028
+ // 检查是否为主人 (QQ号作为主键检查)
1029
+ const isMaster = (qqId) => {
1030
+ const normalizedMasterId = normalizeQQId(config.masterId);
1031
+ const normalizedQQId = normalizeQQId(qqId);
1032
+ return normalizedQQId === normalizedMasterId;
1033
+ };
1924
1034
  // =========== Handler 服务实例创建 ===========
1925
1035
  // 创建 MessageUtils 实例(暂时使用null,因为MessageUtils需要重构)
1926
1036
  const messageUtils = null;
@@ -1942,50 +1052,34 @@ function apply(ctx, config) {
1942
1052
  mcidbind: mcidbindRepo,
1943
1053
  scheduleMute: scheduleMuteRepo
1944
1054
  };
1945
- // 创建依赖对象,包含所有wrapper函数和服务
1055
+ // 创建依赖对象,直接传入服务实例
1946
1056
  const handlerDependencies = {
1947
- // Utility functions
1057
+ // ========== 核心服务实例 ==========
1058
+ apiService: services.api,
1059
+ databaseService: services.database,
1060
+ nicknameService: services.nickname,
1061
+ // ========== 工具函数 ==========
1948
1062
  normalizeQQId,
1949
1063
  formatCommand,
1950
- formatUuid,
1951
1064
  checkCooldown,
1952
- getCrafatarUrl,
1953
- getStarlightSkinUrl,
1954
- // Database operations (for McidCommandHandler)
1955
- getMcBindByQQId,
1956
- getMcBindByUsername,
1957
- createOrUpdateMcBind,
1958
- deleteMcBind,
1959
- checkUsernameExists,
1960
- checkAndUpdateUsername,
1961
- checkAndUpdateUsernameWithCache,
1962
- // API operations
1963
- validateUsername,
1964
- validateBUID,
1965
- updateBuidInfoOnly,
1966
- // Permission check functions
1065
+ // ========== 权限检查函数 ==========
1967
1066
  isAdmin,
1968
1067
  isMaster,
1969
- // Business functions
1068
+ // ========== 业务函数 ==========
1970
1069
  sendMessage,
1971
- autoSetGroupNickname,
1972
- checkNicknameFormat,
1973
- getBindInfo: getMcBindByQQId,
1974
- // Config operations
1975
- getServerConfigById,
1976
- // Error handling
1977
1070
  getFriendlyErrorMessage: error_utils_1.getFriendlyErrorMessage,
1978
- // Service instances
1071
+ getServerConfigById,
1072
+ // ========== 服务实例 ==========
1979
1073
  rconManager,
1980
1074
  messageUtils,
1981
1075
  forceBinder,
1982
1076
  groupExporter,
1983
- // Session management
1077
+ // ========== 会话管理 ==========
1984
1078
  getBindingSession,
1985
1079
  createBindingSession,
1986
1080
  updateBindingSession,
1987
1081
  removeBindingSession,
1988
- // Shared state
1082
+ // ========== 其他共享状态 ==========
1989
1083
  avatarCache: new Map(Object.entries(avatarCache).map(([k, v]) => [k, v])),
1990
1084
  bindingSessions
1991
1085
  };
@@ -1994,6 +1088,7 @@ function apply(ctx, config) {
1994
1088
  const tagHandler = new handlers_1.TagHandler(ctx, config, loggerService, repositories, handlerDependencies);
1995
1089
  const whitelistHandler = new handlers_1.WhitelistHandler(ctx, config, loggerService, repositories, handlerDependencies);
1996
1090
  const buidHandler = new handlers_1.BuidHandler(ctx, config, loggerService, repositories, handlerDependencies);
1091
+ const lotteryHandler = new handlers_1.LotteryHandler(ctx, config, loggerService, repositories, handlerDependencies);
1997
1092
  // 注册Handler命令
1998
1093
  bindingHandler.register();
1999
1094
  tagHandler.register();
@@ -2099,7 +1194,7 @@ function apply(ctx, config) {
2099
1194
  return next();
2100
1195
  }
2101
1196
  // 获取用户绑定信息
2102
- const bind = await getMcBindByQQId(normalizedUserId);
1197
+ const bind = await services.database.getMcBindByQQId(normalizedUserId);
2103
1198
  // 获取用户群昵称信息
2104
1199
  let currentNickname = '';
2105
1200
  try {
@@ -2149,7 +1244,7 @@ function apply(ctx, config) {
2149
1244
  // 情况2:只绑定了B站,未绑定MC
2150
1245
  if (bind.buidUid && bind.buidUsername && (!bind.mcUsername || bind.mcUsername.startsWith('_temp_'))) {
2151
1246
  const mcInfo = null;
2152
- const isNicknameCorrect = checkNicknameFormat(currentNickname, bind.buidUsername, mcInfo);
1247
+ const isNicknameCorrect = services.nickname.checkNicknameFormat(currentNickname, bind.buidUsername, mcInfo);
2153
1248
  if (!isNicknameCorrect) {
2154
1249
  // 更新提醒次数
2155
1250
  const reminderCount = (bind.reminderCount || 0) + 1;
@@ -2158,7 +1253,7 @@ function apply(ctx, config) {
2158
1253
  const reminderType = reminderCount >= 4 ? '警告' : '提醒';
2159
1254
  const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
2160
1255
  // 自动修改群昵称
2161
- await autoSetGroupNickname(session, mcInfo, bind.buidUsername, bind.buidUid);
1256
+ await services.nickname.autoSetGroupNickname(session, mcInfo, bind.buidUsername, bind.buidUid);
2162
1257
  setReminderCooldown(normalizedUserId);
2163
1258
  logger.info(`[随机提醒] 为仅绑定B站的用户QQ(${normalizedUserId})修复群昵称并发送第${reminderCount}次${reminderType}`);
2164
1259
  await sendMessage(session, [
@@ -2169,7 +1264,7 @@ function apply(ctx, config) {
2169
1264
  }
2170
1265
  // 情况3:都已绑定,但群昵称格式不正确
2171
1266
  if (bind.buidUid && bind.buidUsername && bind.mcUsername && !bind.mcUsername.startsWith('_temp_')) {
2172
- const isNicknameCorrect = checkNicknameFormat(currentNickname, bind.buidUsername, bind.mcUsername);
1267
+ const isNicknameCorrect = services.nickname.checkNicknameFormat(currentNickname, bind.buidUsername, bind.mcUsername);
2173
1268
  if (!isNicknameCorrect) {
2174
1269
  // 更新提醒次数
2175
1270
  const reminderCount = (bind.reminderCount || 0) + 1;
@@ -2178,7 +1273,7 @@ function apply(ctx, config) {
2178
1273
  const reminderType = reminderCount >= 4 ? '警告' : '提醒';
2179
1274
  const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
2180
1275
  // 自动修改群昵称
2181
- await autoSetGroupNickname(session, bind.mcUsername, bind.buidUsername, bind.buidUid);
1276
+ await services.nickname.autoSetGroupNickname(session, bind.mcUsername, bind.buidUsername, bind.buidUid);
2182
1277
  setReminderCooldown(normalizedUserId);
2183
1278
  logger.info(`[随机提醒] 为已完全绑定的用户QQ(${normalizedUserId})修复群昵称并发送第${reminderCount}次${reminderType}`);
2184
1279
  await sendMessage(session, [
@@ -2298,7 +1393,7 @@ function apply(ctx, config) {
2298
1393
  // 处理跳过MC绑定,直接完成绑定流程
2299
1394
  if (content === '跳过' || content === 'skip') {
2300
1395
  // 检查用户是否已绑定B站账号
2301
- const existingBind = await getMcBindByQQId(normalizedUserId);
1396
+ const existingBind = await services.database.getMcBindByQQId(normalizedUserId);
2302
1397
  if (existingBind && existingBind.buidUid && existingBind.buidUsername) {
2303
1398
  // 用户已绑定B站账号,直接完成绑定
2304
1399
  logger.info(`[交互绑定] QQ(${normalizedUserId})跳过了MC账号绑定,已有B站绑定,完成绑定流程`);
@@ -2306,7 +1401,7 @@ function apply(ctx, config) {
2306
1401
  removeBindingSession(session.userId, session.channelId);
2307
1402
  // 设置群昵称
2308
1403
  try {
2309
- await autoSetGroupNickname(session, null, existingBind.buidUsername, existingBind.buidUid);
1404
+ await services.nickname.autoSetGroupNickname(session, null, existingBind.buidUsername, existingBind.buidUid);
2310
1405
  logger.info(`[交互绑定] QQ(${normalizedUserId})完成绑定,已设置群昵称`);
2311
1406
  }
2312
1407
  catch (renameError) {
@@ -2325,7 +1420,7 @@ function apply(ctx, config) {
2325
1420
  const timestamp = Date.now();
2326
1421
  const tempMcUsername = `_temp_skip_${timestamp}`;
2327
1422
  // 创建临时MC绑定
2328
- const tempBindResult = await createOrUpdateMcBind(session.userId, tempMcUsername, '', false);
1423
+ const tempBindResult = await services.database.createOrUpdateMcBind(session.userId, tempMcUsername, '', false);
2329
1424
  if (!tempBindResult) {
2330
1425
  logger.error(`[交互绑定] QQ(${normalizedUserId})创建临时MC绑定失败`);
2331
1426
  await sendMessage(session, [koishi_1.h.text('❌ 创建临时绑定失败,请稍后重试')]);
@@ -2348,7 +1443,7 @@ function apply(ctx, config) {
2348
1443
  return;
2349
1444
  }
2350
1445
  // 验证用户名是否存在
2351
- const profile = await validateUsername(content);
1446
+ const profile = await services.api.validateUsername(content);
2352
1447
  if (!profile) {
2353
1448
  logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的MC用户名"${content}"不存在`);
2354
1449
  await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${content} 不存在\n请重新输入或发送"跳过"完成绑定`)]);
@@ -2357,7 +1452,7 @@ function apply(ctx, config) {
2357
1452
  const username = profile.name;
2358
1453
  const uuid = profile.id;
2359
1454
  // 检查用户是否已绑定MC账号
2360
- const existingBind = await getMcBindByQQId(normalizedUserId);
1455
+ const existingBind = await services.database.getMcBindByQQId(normalizedUserId);
2361
1456
  if (existingBind && existingBind.mcUsername && !existingBind.mcUsername.startsWith('_temp_')) {
2362
1457
  // 检查冷却时间
2363
1458
  if (!await isAdmin(session.userId) && !checkCooldown(existingBind.lastModified)) {
@@ -2373,13 +1468,13 @@ function apply(ctx, config) {
2373
1468
  }
2374
1469
  }
2375
1470
  // 检查用户名是否已被其他人绑定
2376
- if (await checkUsernameExists(username, session.userId, uuid)) {
1471
+ if (await services.database.checkUsernameExists(username, session.userId, uuid)) {
2377
1472
  logger.warn(`[交互绑定] MC用户名"${username}"已被其他用户绑定`);
2378
1473
  await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${username} 已被其他用户绑定\n\n请输入其他MC用户名或发送"跳过"完成绑定`)]);
2379
1474
  return;
2380
1475
  }
2381
1476
  // 绑定MC账号
2382
- const bindResult = await createOrUpdateMcBind(session.userId, username, uuid);
1477
+ const bindResult = await services.database.createOrUpdateMcBind(session.userId, username, uuid);
2383
1478
  if (!bindResult) {
2384
1479
  logger.error(`[交互绑定] QQ(${normalizedUserId})绑定MC账号失败`);
2385
1480
  removeBindingSession(session.userId, session.channelId);
@@ -2388,7 +1483,7 @@ function apply(ctx, config) {
2388
1483
  }
2389
1484
  logger.info(`[交互绑定] QQ(${normalizedUserId})成功绑定MC账号: ${username}`);
2390
1485
  // 检查用户是否已经绑定了B站账号
2391
- const updatedBind = await getMcBindByQQId(normalizedUserId);
1486
+ const updatedBind = await services.database.getMcBindByQQId(normalizedUserId);
2392
1487
  if (updatedBind && updatedBind.buidUid && updatedBind.buidUsername) {
2393
1488
  // 用户已经绑定了B站账号,直接完成绑定流程
2394
1489
  logger.info(`[交互绑定] QQ(${normalizedUserId})已绑定B站账号,完成绑定流程`);
@@ -2396,7 +1491,7 @@ function apply(ctx, config) {
2396
1491
  removeBindingSession(session.userId, session.channelId);
2397
1492
  // 设置群昵称
2398
1493
  try {
2399
- await autoSetGroupNickname(session, username, updatedBind.buidUsername, updatedBind.buidUid);
1494
+ await services.nickname.autoSetGroupNickname(session, username, updatedBind.buidUsername, updatedBind.buidUid);
2400
1495
  logger.info(`[交互绑定] QQ(${normalizedUserId})绑定完成,已设置群昵称`);
2401
1496
  }
2402
1497
  catch (renameError) {
@@ -2406,10 +1501,10 @@ function apply(ctx, config) {
2406
1501
  let mcAvatarUrl = null;
2407
1502
  if (config?.showAvatar) {
2408
1503
  if (config?.showMcSkin) {
2409
- mcAvatarUrl = getStarlightSkinUrl(username);
1504
+ mcAvatarUrl = services.api.getStarlightSkinUrl(username);
2410
1505
  }
2411
1506
  else {
2412
- mcAvatarUrl = getCrafatarUrl(uuid);
1507
+ mcAvatarUrl = services.api.getCrafatarUrl(uuid);
2413
1508
  }
2414
1509
  }
2415
1510
  // 发送完成消息
@@ -2430,13 +1525,13 @@ function apply(ctx, config) {
2430
1525
  let mcAvatarUrl = null;
2431
1526
  if (config?.showAvatar) {
2432
1527
  if (config?.showMcSkin) {
2433
- mcAvatarUrl = getStarlightSkinUrl(username);
1528
+ mcAvatarUrl = services.api.getStarlightSkinUrl(username);
2434
1529
  }
2435
1530
  else {
2436
- mcAvatarUrl = getCrafatarUrl(uuid);
1531
+ mcAvatarUrl = services.api.getCrafatarUrl(uuid);
2437
1532
  }
2438
1533
  }
2439
- const formattedUuid = formatUuid(uuid);
1534
+ const formattedUuid = services.api.formatUuid(uuid);
2440
1535
  // 发送简化的MC绑定成功消息
2441
1536
  await sendMessage(session, [
2442
1537
  koishi_1.h.text(`✅ MC账号: ${username}\n🔗 请发送您的B站UID`),
@@ -2487,20 +1582,20 @@ function apply(ctx, config) {
2487
1582
  return;
2488
1583
  }
2489
1584
  // 检查UID是否已被绑定
2490
- if (await checkBuidExists(actualUid, session.userId)) {
1585
+ if (await services.database.checkBuidExists(actualUid, session.userId)) {
2491
1586
  logger.warn(`[交互绑定] B站UID"${actualUid}"已被其他用户绑定`);
2492
1587
  await sendMessage(session, [koishi_1.h.text(`❌ UID ${actualUid} 已被其他用户绑定\n\n请输入其他B站UID\n或发送"跳过"仅绑定MC账号`)]);
2493
1588
  return;
2494
1589
  }
2495
1590
  // 验证UID是否存在
2496
- const buidUser = await validateBUID(actualUid);
1591
+ const buidUser = await services.api.validateBUID(actualUid);
2497
1592
  if (!buidUser) {
2498
1593
  logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的B站UID"${actualUid}"不存在`);
2499
1594
  await sendMessage(session, [koishi_1.h.text(`❌ 无法验证UID: ${actualUid}\n\n该用户可能不存在或未被发现\n可以去直播间发个弹幕后重试绑定\n或发送"跳过"仅绑定MC账号`)]);
2500
1595
  return;
2501
1596
  }
2502
1597
  // 绑定B站账号
2503
- const bindResult = await createOrUpdateBuidBind(session.userId, buidUser);
1598
+ const bindResult = await services.database.createOrUpdateBuidBind(session.userId, buidUser);
2504
1599
  if (!bindResult) {
2505
1600
  logger.error(`[交互绑定] QQ(${normalizedUserId})绑定B站账号失败`);
2506
1601
  removeBindingSession(session.userId, session.channelId);
@@ -2517,7 +1612,7 @@ function apply(ctx, config) {
2517
1612
  try {
2518
1613
  // 检查是否有有效的MC用户名(不是临时用户名)
2519
1614
  const mcName = bindingSession.mcUsername && !bindingSession.mcUsername.startsWith('_temp_') ? bindingSession.mcUsername : null;
2520
- await autoSetGroupNickname(session, mcName, buidUser.username, String(buidUser.uid));
1615
+ await services.nickname.autoSetGroupNickname(session, mcName, buidUser.username, String(buidUser.uid));
2521
1616
  logger.info(`[交互绑定] QQ(${normalizedUserId})绑定完成,已设置群昵称`);
2522
1617
  }
2523
1618
  catch (renameError) {
@@ -2579,185 +1674,4 @@ function apply(ctx, config) {
2579
1674
  }
2580
1675
  return cleanedContent;
2581
1676
  };
2582
- // =========== 天选开奖 Webhook 处理 ===========
2583
- // 处理天选开奖结果
2584
- const handleLotteryResult = async (lotteryData) => {
2585
- try {
2586
- // 检查天选播报开关
2587
- if (!config?.enableLotteryBroadcast) {
2588
- logger.debug(`[天选开奖] 天选播报功能已禁用,跳过处理天选事件: ${lotteryData.lottery_id}`);
2589
- return;
2590
- }
2591
- logger.info(`[天选开奖] 开始处理天选事件: ${lotteryData.lottery_id},奖品: ${lotteryData.reward_name},中奖人数: ${lotteryData.winners.length}`);
2592
- // 生成标签名称
2593
- const tagName = `天选-${lotteryData.lottery_id}`;
2594
- // 统计信息
2595
- let matchedCount = 0;
2596
- let notBoundCount = 0;
2597
- let tagAddedCount = 0;
2598
- let tagExistedCount = 0;
2599
- const matchedUsers = [];
2600
- // 处理每个中奖用户
2601
- for (const winner of lotteryData.winners) {
2602
- try {
2603
- // 根据B站UID查找绑定的QQ用户
2604
- const bind = await getBuidBindByBuid(winner.uid.toString());
2605
- if (bind && bind.qqId) {
2606
- matchedCount++;
2607
- matchedUsers.push({
2608
- qqId: bind.qqId,
2609
- mcUsername: bind.mcUsername || '未绑定MC',
2610
- buidUsername: bind.buidUsername,
2611
- uid: winner.uid,
2612
- username: winner.username
2613
- });
2614
- // 检查是否已有该标签
2615
- if (bind.tags && bind.tags.includes(tagName)) {
2616
- tagExistedCount++;
2617
- logger.debug(`[天选开奖] QQ(${bind.qqId})已有标签"${tagName}"`);
2618
- }
2619
- else {
2620
- // 添加标签
2621
- const newTags = [...(bind.tags || []), tagName];
2622
- await mcidbindRepo.update(bind.qqId, { tags: newTags });
2623
- tagAddedCount++;
2624
- logger.debug(`[天选开奖] 为QQ(${bind.qqId})添加标签"${tagName}"`);
2625
- }
2626
- }
2627
- else {
2628
- notBoundCount++;
2629
- logger.debug(`[天选开奖] B站UID(${winner.uid})未绑定QQ账号`);
2630
- }
2631
- }
2632
- catch (error) {
2633
- logger.error(`[天选开奖] 处理中奖用户UID(${winner.uid})时出错: ${error.message}`);
2634
- }
2635
- }
2636
- logger.info(`[天选开奖] 处理完成: 总计${lotteryData.winners.length}人中奖,匹配${matchedCount}人,未绑定${notBoundCount}人,新增标签${tagAddedCount}人,已有标签${tagExistedCount}人`);
2637
- // 生成并发送结果消息
2638
- await sendLotteryResultToGroup(lotteryData, {
2639
- totalWinners: lotteryData.winners.length,
2640
- matchedCount,
2641
- notBoundCount,
2642
- tagAddedCount,
2643
- tagExistedCount,
2644
- matchedUsers,
2645
- tagName
2646
- });
2647
- }
2648
- catch (error) {
2649
- logger.error(`[天选开奖] 处理天选事件"${lotteryData.lottery_id}"失败: ${error.message}`);
2650
- }
2651
- };
2652
- // 发送天选开奖结果到群
2653
- const sendLotteryResultToGroup = async (lotteryData, stats) => {
2654
- try {
2655
- const targetChannelId = '123456789'; // 目标群号
2656
- const privateTargetId = 'private:3431185320'; // 私聊目标
2657
- // 格式化时间
2658
- const lotteryTime = new Date(lotteryData.timestamp).toLocaleString('zh-CN', {
2659
- timeZone: 'Asia/Shanghai',
2660
- year: 'numeric',
2661
- month: '2-digit',
2662
- day: '2-digit',
2663
- hour: '2-digit',
2664
- minute: '2-digit',
2665
- second: '2-digit'
2666
- });
2667
- // 构建简化版群消息(去掉主播信息、统计信息和标签提示)
2668
- let groupMessage = `🎉 天选开奖结果通知\n\n`;
2669
- groupMessage += `📅 开奖时间: ${lotteryTime}\n`;
2670
- groupMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
2671
- groupMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
2672
- groupMessage += `🎲 总中奖人数: ${stats.totalWinners}人`;
2673
- // 添加未绑定用户说明
2674
- if (stats.notBoundCount > 0) {
2675
- groupMessage += `(其中${stats.notBoundCount}人未绑定跳过)`;
2676
- }
2677
- groupMessage += `\n\n`;
2678
- // 如果有匹配的用户,显示详细信息
2679
- if (stats.matchedUsers.length > 0) {
2680
- groupMessage += `🎯 已绑定的中奖用户:\n`;
2681
- // 限制显示前10个用户,避免消息过长
2682
- const displayUsers = stats.matchedUsers.slice(0, 10);
2683
- for (let i = 0; i < displayUsers.length; i++) {
2684
- const user = displayUsers[i];
2685
- const index = i + 1;
2686
- const displayMcName = user.mcUsername && !user.mcUsername.startsWith('_temp_') ? user.mcUsername : '未绑定';
2687
- groupMessage += `${index}. ${user.buidUsername} (UID: ${user.uid})\n`;
2688
- groupMessage += ` QQ: ${user.qqId} | MC: ${displayMcName}\n`;
2689
- }
2690
- // 如果用户太多,显示省略信息
2691
- if (stats.matchedUsers.length > 10) {
2692
- groupMessage += `... 还有${stats.matchedUsers.length - 10}位中奖用户\n`;
2693
- }
2694
- }
2695
- else {
2696
- groupMessage += `😔 暂无已绑定用户中奖\n`;
2697
- }
2698
- // 构建完整版私聊消息(包含所有信息和未绑定用户)
2699
- let privateMessage = `🎉 天选开奖结果通知\n\n`;
2700
- privateMessage += `📅 开奖时间: ${lotteryTime}\n`;
2701
- privateMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
2702
- privateMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
2703
- privateMessage += `🏷️ 事件ID: ${lotteryData.lottery_id}\n`;
2704
- privateMessage += `👤 主播: ${lotteryData.host_username} (UID: ${lotteryData.host_uid})\n`;
2705
- privateMessage += `🏠 房间号: ${lotteryData.room_id}\n\n`;
2706
- // 统计信息
2707
- privateMessage += `📈 处理统计:\n`;
2708
- privateMessage += `• 总中奖人数: ${stats.totalWinners}人\n`;
2709
- privateMessage += `• 已绑定用户: ${stats.matchedCount}人 ✅\n`;
2710
- privateMessage += `• 未绑定用户: ${stats.notBoundCount}人 ⚠️\n`;
2711
- privateMessage += `• 新增标签: ${stats.tagAddedCount}人\n`;
2712
- privateMessage += `• 已有标签: ${stats.tagExistedCount}人\n\n`;
2713
- // 显示所有中奖用户(包括未绑定的)
2714
- if (lotteryData.winners.length > 0) {
2715
- privateMessage += `🎯 所有中奖用户:\n`;
2716
- for (let i = 0; i < lotteryData.winners.length; i++) {
2717
- const winner = lotteryData.winners[i];
2718
- const index = i + 1;
2719
- // 查找对应的绑定用户
2720
- const matchedUser = stats.matchedUsers.find(user => user.uid === winner.uid);
2721
- if (matchedUser) {
2722
- const displayMcName = matchedUser.mcUsername && !matchedUser.mcUsername.startsWith('_temp_') ? matchedUser.mcUsername : '未绑定';
2723
- privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
2724
- privateMessage += ` QQ: ${matchedUser.qqId} | MC: ${displayMcName}\n`;
2725
- }
2726
- else {
2727
- privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
2728
- privateMessage += ` 无绑定信息,自动跳过\n`;
2729
- }
2730
- }
2731
- privateMessage += `\n🏷️ 标签"${stats.tagName}"已自动添加到已绑定用户\n`;
2732
- }
2733
- // 准备消息元素
2734
- const groupMessageElements = [koishi_1.h.text(groupMessage)];
2735
- const privateMessageElements = [koishi_1.h.text(privateMessage)];
2736
- // 发送消息到指定群(简化版)
2737
- for (const bot of ctx.bots) {
2738
- try {
2739
- await bot.sendMessage(targetChannelId, groupMessageElements);
2740
- logger.info(`[天选开奖] 成功发送简化开奖结果到群${targetChannelId}`);
2741
- break; // 成功发送后退出循环
2742
- }
2743
- catch (error) {
2744
- logger.error(`[天选开奖] 发送消息到群${targetChannelId}失败: ${error.message}`);
2745
- }
2746
- }
2747
- // 发送消息到私聊(完整版)
2748
- for (const bot of ctx.bots) {
2749
- try {
2750
- await bot.sendMessage(privateTargetId, privateMessageElements);
2751
- logger.info(`[天选开奖] 成功发送完整开奖结果到私聊${privateTargetId}`);
2752
- break; // 成功发送后退出循环
2753
- }
2754
- catch (error) {
2755
- logger.error(`[天选开奖] 发送消息到私聊${privateTargetId}失败: ${error.message}`);
2756
- }
2757
- }
2758
- }
2759
- catch (error) {
2760
- logger.error(`[天选开奖] 发送开奖结果失败: ${error.message}`);
2761
- }
2762
- };
2763
1677
  }