koishi-plugin-bind-bot 2.0.4 → 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,99 +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, 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
- const newNickname = `${buidUsername}(ID:${mcInfo})`;
271
- // 使用指定的群ID,如果没有指定则使用配置的默认群ID
272
- const targetGroupId = specifiedGroupId || config.autoNicknameGroupId;
273
- logger.debug(`[群昵称设置] 开始处理QQ(${normalizedUserId})的群昵称设置,目标群: ${targetGroupId}`);
274
- logger.debug(`[群昵称设置] 期望昵称: "${newNickname}"`);
275
- if (session.bot.internal && targetGroupId) {
276
- // 使用规范化的QQ号调用OneBot API
277
- logger.debug(`[群昵称设置] 使用用户ID: ${normalizedUserId}`);
278
- // 先获取当前群昵称进行比对
279
- try {
280
- logger.debug(`[群昵称设置] 正在获取QQ(${normalizedUserId})在群${targetGroupId}的当前昵称...`);
281
- const currentGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
282
- const currentNickname = currentGroupInfo.card || currentGroupInfo.nickname || '';
283
- logger.debug(`[群昵称设置] 当前昵称: "${currentNickname}"`);
284
- // 如果当前昵称和目标昵称一致,跳过修改
285
- if (currentNickname === newNickname) {
286
- logger.info(`[群昵称设置] QQ(${normalizedUserId})群昵称已经是"${newNickname}",跳过修改`);
287
- return;
288
- }
289
- // 昵称不一致,执行修改
290
- logger.debug(`[群昵称设置] 昵称不一致,正在修改群昵称...`);
291
- await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, newNickname);
292
- logger.info(`[群昵称设置] 成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${newNickname}"`);
293
- // 验证设置是否生效 - 再次获取群昵称确认
294
- try {
295
- await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
296
- const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
297
- const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
298
- if (verifyNickname === newNickname) {
299
- logger.info(`[群昵称设置] ✅ 验证成功,群昵称已生效: "${verifyNickname}"`);
300
- }
301
- else {
302
- logger.warn(`[群昵称设置] ⚠️ 验证失败,期望"${newNickname}",实际"${verifyNickname}",可能是权限不足或API延迟`);
303
- }
304
- }
305
- catch (verifyError) {
306
- logger.warn(`[群昵称设置] 无法验证群昵称设置结果: ${verifyError.message}`);
307
- }
308
- }
309
- catch (getInfoError) {
310
- // 如果获取当前昵称失败,直接尝试设置新昵称
311
- logger.warn(`[群昵称设置] 获取QQ(${normalizedUserId})当前群昵称失败: ${getInfoError.message}`);
312
- logger.warn(`[群昵称设置] 错误详情: ${JSON.stringify(getInfoError)}`);
313
- logger.debug(`[群昵称设置] 将直接尝试设置新昵称...`);
314
- try {
315
- await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, newNickname);
316
- logger.info(`[群昵称设置] 成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称设置为: ${newNickname}`);
317
- // 验证设置是否生效
318
- try {
319
- await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
320
- const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
321
- const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
322
- if (verifyNickname === newNickname) {
323
- logger.info(`[群昵称设置] ✅ 验证成功,群昵称已生效: "${verifyNickname}"`);
324
- }
325
- else {
326
- logger.warn(`[群昵称设置] ⚠️ 验证失败,期望"${newNickname}",实际"${verifyNickname}",可能是权限不足`);
327
- logger.warn(`[群昵称设置] 建议检查: 1.机器人是否为群管理员 2.群设置是否允许管理员修改昵称 3.OneBot实现是否支持该功能`);
328
- }
329
- }
330
- catch (verifyError) {
331
- logger.warn(`[群昵称设置] 无法验证群昵称设置结果: ${verifyError.message}`);
332
- }
333
- }
334
- catch (setCardError) {
335
- logger.error(`[群昵称设置] 设置群昵称失败: ${setCardError.message}`);
336
- logger.error(`[群昵称设置] 错误详情: ${JSON.stringify(setCardError)}`);
337
- throw setCardError;
338
- }
339
- }
340
- }
341
- else if (!session.bot.internal) {
342
- logger.debug(`[群昵称设置] QQ(${normalizedUserId})bot不支持OneBot内部API,跳过自动群昵称设置`);
343
- }
344
- else if (!targetGroupId) {
345
- logger.debug(`[群昵称设置] QQ(${normalizedUserId})未配置自动群昵称设置目标群,跳过群昵称设置`);
346
- }
347
- }
348
- catch (error) {
349
- const actualUserId = targetUserId || session.userId;
350
- const normalizedUserId = normalizeQQId(actualUserId);
351
- logger.error(`[群昵称设置] QQ(${normalizedUserId})自动群昵称设置失败: ${error.message}`);
352
- logger.error(`[群昵称设置] 完整错误信息: ${JSON.stringify(error)}`);
353
- }
354
- };
355
257
  // 检查是否为无关输入
356
258
  const checkIrrelevantInput = (bindingSession, content) => {
357
259
  if (!content)
@@ -490,7 +392,7 @@ function apply(ctx, config) {
490
392
  const normalizedUserId = normalizeQQId(session.userId);
491
393
  logger.info(`[新人绑定] 用户QQ(${normalizedUserId})加入群聊,准备发送绑定提醒`);
492
394
  // 检查用户是否已有绑定记录
493
- const existingBind = await getMcBindByQQId(normalizedUserId);
395
+ const existingBind = await services.database.getMcBindByQQId(normalizedUserId);
494
396
  // 如果用户已完成全部绑定,不需要提醒
495
397
  if (existingBind && existingBind.mcUsername && existingBind.buidUid) {
496
398
  logger.info(`[新人绑定] 用户QQ(${normalizedUserId})已完成全部绑定,跳过提醒`);
@@ -631,7 +533,7 @@ function apply(ctx, config) {
631
533
  logger.info(`[天选开奖] 接收到天选事件: ${lotteryData.lottery_id},奖品: ${lotteryData.reward_name},中奖人数: ${lotteryData.winners.length}`);
632
534
  }
633
535
  // 异步处理天选开奖数据(不阻塞响应)
634
- handleLotteryResult(lotteryData).catch(error => {
536
+ lotteryHandler.handleLotteryResult(lotteryData).catch(error => {
635
537
  logger.error(`[天选开奖] 异步处理天选开奖数据失败: ${error.message}`);
636
538
  });
637
539
  // 立即返回成功响应
@@ -920,6 +822,9 @@ function apply(ctx, config) {
920
822
  }
921
823
  return extractedId;
922
824
  };
825
+ // =========== 实例化服务容器 ===========
826
+ // 统一管理所有服务的实例化,解决服务初始化分散的问题
827
+ const services = new service_container_1.ServiceContainer(ctx, config, loggerService, mcidbindRepo, normalizeQQId);
923
828
  // 获取用户友好的错误信息
924
829
  // 封装发送消息的函数,处理私聊和群聊的不同格式
925
830
  const sendMessage = async (session, content, options) => {
@@ -1045,727 +950,6 @@ function apply(ctx, config) {
1045
950
  return diffDays >= config.cooldownDays * multiplier;
1046
951
  };
1047
952
  // 根据QQ号查询MCIDBIND表中的绑定信息
1048
- const getMcBindByQQId = async (qqId) => {
1049
- try {
1050
- // 处理空值
1051
- if (!qqId) {
1052
- logger.warn(`[MCIDBIND] 尝试查询空QQ号`);
1053
- return null;
1054
- }
1055
- const normalizedQQId = normalizeQQId(qqId);
1056
- // 查询MCIDBIND表中对应QQ号的绑定记录
1057
- return await mcidbindRepo.findByQQId(normalizedQQId);
1058
- }
1059
- catch (error) {
1060
- logError('MCIDBIND', qqId, `根据QQ号查询绑定信息失败: ${error.message}`);
1061
- return null;
1062
- }
1063
- };
1064
- // 根据MC用户名查询MCIDBIND表中的绑定信息
1065
- const getMcBindByUsername = async (mcUsername) => {
1066
- // 处理空值
1067
- if (!mcUsername) {
1068
- logger.warn(`[MCIDBIND] 尝试查询空MC用户名`);
1069
- return null;
1070
- }
1071
- // 使用 Repository 查询
1072
- return await mcidbindRepo.findByMCUsername(mcUsername);
1073
- };
1074
- // 根据QQ号确保获取完整的用户ID (处理纯QQ号的情况)
1075
- const ensureFullUserId = (userId) => {
1076
- // 如果已经包含冒号,说明已经是完整的用户ID
1077
- if (userId.includes(':'))
1078
- return userId;
1079
- // 否则,检查是否为数字(纯QQ号)
1080
- if (/^\d+$/.test(userId)) {
1081
- // 默认使用onebot平台前缀
1082
- return `onebot:${userId}`;
1083
- }
1084
- // 如果不是数字也没有冒号,保持原样返回
1085
- logger.warn(`[用户ID] 无法确定用户ID格式: ${userId}`);
1086
- return userId;
1087
- };
1088
- // 创建或更新MCIDBIND表中的绑定信息
1089
- const createOrUpdateMcBind = async (userId, mcUsername, mcUuid, isAdmin) => {
1090
- try {
1091
- // 验证输入参数
1092
- if (!userId) {
1093
- logger.error(`[MCIDBIND] 创建/更新绑定失败: 无效的用户ID`);
1094
- return false;
1095
- }
1096
- if (!mcUsername) {
1097
- logger.error(`[MCIDBIND] 创建/更新绑定失败: 无效的MC用户名`);
1098
- return false;
1099
- }
1100
- const normalizedQQId = normalizeQQId(userId);
1101
- if (!normalizedQQId) {
1102
- logger.error(`[MCIDBIND] 创建/更新绑定失败: 无法提取有效的QQ号`);
1103
- return false;
1104
- }
1105
- // 查询是否已存在绑定记录
1106
- let bind = await getMcBindByQQId(normalizedQQId);
1107
- if (bind) {
1108
- // 更新现有记录,但保留管理员状态
1109
- const updateData = {
1110
- mcUsername,
1111
- mcUuid,
1112
- lastModified: new Date()
1113
- };
1114
- // 仅当指定了isAdmin参数时更新管理员状态
1115
- if (typeof isAdmin !== 'undefined') {
1116
- updateData.isAdmin = isAdmin;
1117
- }
1118
- await mcidbindRepo.update(normalizedQQId, updateData);
1119
- logger.info(`[MCIDBIND] 更新绑定: QQ=${normalizedQQId}, MC用户名=${mcUsername}, UUID=${mcUuid}`);
1120
- return true;
1121
- }
1122
- else {
1123
- // 创建新记录
1124
- try {
1125
- await mcidbindRepo.create({
1126
- qqId: normalizedQQId,
1127
- mcUsername,
1128
- mcUuid,
1129
- lastModified: new Date(),
1130
- isAdmin: isAdmin || false
1131
- });
1132
- logger.info(`[MCIDBIND] 创建绑定: QQ=${normalizedQQId}, MC用户名=${mcUsername}, UUID=${mcUuid}`);
1133
- return true;
1134
- }
1135
- catch (createError) {
1136
- logError('MCIDBIND', userId, `创建绑定失败: MC用户名=${mcUsername}, 错误=${createError.message}`);
1137
- return false;
1138
- }
1139
- }
1140
- }
1141
- catch (error) {
1142
- logError('MCIDBIND', userId, `创建/更新绑定失败: MC用户名=${mcUsername}, 错误=${error.message}`);
1143
- return false;
1144
- }
1145
- };
1146
- // 删除MCIDBIND表中的绑定信息 (同时解绑MC和B站账号)
1147
- const deleteMcBind = async (userId) => {
1148
- try {
1149
- // 验证输入参数
1150
- if (!userId) {
1151
- logger.error(`[MCIDBIND] 删除绑定失败: 无效的用户ID`);
1152
- return false;
1153
- }
1154
- const normalizedQQId = normalizeQQId(userId);
1155
- if (!normalizedQQId) {
1156
- logger.error(`[MCIDBIND] 删除绑定失败: 无法提取有效的QQ号`);
1157
- return false;
1158
- }
1159
- // 查询是否存在绑定记录
1160
- const bind = await getMcBindByQQId(normalizedQQId);
1161
- if (bind) {
1162
- // 删除整个绑定记录,包括MC和B站账号
1163
- const removedCount = await mcidbindRepo.delete(normalizedQQId);
1164
- // 检查是否真正删除成功
1165
- if (removedCount > 0) {
1166
- let logMessage = `[MCIDBIND] 删除绑定: QQ=${normalizedQQId}`;
1167
- if (bind.mcUsername)
1168
- logMessage += `, MC用户名=${bind.mcUsername}`;
1169
- if (bind.buidUid)
1170
- logMessage += `, B站UID=${bind.buidUid}(${bind.buidUsername})`;
1171
- logger.info(logMessage);
1172
- return true;
1173
- }
1174
- else {
1175
- logger.warn(`[MCIDBIND] 删除绑定异常: QQ=${normalizedQQId}, 可能未实际删除`);
1176
- return false;
1177
- }
1178
- }
1179
- logger.warn(`[MCIDBIND] 删除绑定失败: QQ=${normalizedQQId}不存在绑定记录`);
1180
- return false;
1181
- }
1182
- catch (error) {
1183
- logError('MCIDBIND', userId, `删除绑定失败: 错误=${error.message}`);
1184
- return false;
1185
- }
1186
- };
1187
- // 检查MC用户名是否已被其他QQ号绑定(支持不区分大小写和UUID检查)
1188
- const checkUsernameExists = async (username, currentUserId, uuid) => {
1189
- try {
1190
- // 验证输入参数
1191
- if (!username) {
1192
- logger.warn(`[绑定检查] 尝试检查空MC用户名`);
1193
- return false;
1194
- }
1195
- // 跳过临时用户名的检查
1196
- if (username.startsWith('_temp_')) {
1197
- return false;
1198
- }
1199
- // 使用不区分大小写的查询
1200
- const bind = await mcidbindRepo.findByUsernameIgnoreCase(username);
1201
- // 如果没有绑定,返回false
1202
- if (!bind)
1203
- return false;
1204
- // 如果绑定的用户名是临时用户名,视为未绑定
1205
- if (bind.mcUsername && bind.mcUsername.startsWith('_temp_')) {
1206
- return false;
1207
- }
1208
- // 如果提供了 UUID,检查是否为同一个 MC 账号(用户改名场景)
1209
- if (uuid && bind.mcUuid) {
1210
- const cleanUuid = uuid.replace(/-/g, '');
1211
- const bindCleanUuid = bind.mcUuid.replace(/-/g, '');
1212
- if (cleanUuid === bindCleanUuid) {
1213
- // 同一个 UUID,说明是用户改名,允许绑定
1214
- logger.info(`[绑定检查] 检测到MC账号改名: UUID=${uuid}, 旧用户名=${bind.mcUsername}, 新用户名=${username}`);
1215
- return false;
1216
- }
1217
- }
1218
- // 如果提供了当前用户ID,需要排除当前用户
1219
- if (currentUserId) {
1220
- const normalizedCurrentId = normalizeQQId(currentUserId);
1221
- // 如果绑定的用户就是当前用户,返回false,表示没有被其他用户绑定
1222
- return normalizedCurrentId ? bind.qqId !== normalizedCurrentId : true;
1223
- }
1224
- return true;
1225
- }
1226
- catch (error) {
1227
- logError('绑定检查', currentUserId || 'system', `检查用户名"${username}"是否已被绑定失败: ${error.message}`);
1228
- return false;
1229
- }
1230
- };
1231
- // 使用Mojang API验证用户名并获取UUID
1232
- const validateUsername = async (username) => {
1233
- try {
1234
- logger.debug(`[Mojang API] 开始验证用户名: ${username}`);
1235
- const response = await axios_1.default.get(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
1236
- timeout: 10000, // 添加10秒超时
1237
- headers: {
1238
- 'User-Agent': 'KoishiMCVerifier/1.0', // 添加User-Agent头
1239
- }
1240
- });
1241
- if (response.status === 200 && response.data) {
1242
- logger.debug(`[Mojang API] 用户名"${username}"验证成功,UUID: ${response.data.id},标准名称: ${response.data.name}`);
1243
- return {
1244
- id: response.data.id,
1245
- name: response.data.name // 使用Mojang返回的正确大小写
1246
- };
1247
- }
1248
- return null;
1249
- }
1250
- catch (error) {
1251
- if (axios_1.default.isAxiosError(error) && error.response?.status === 404) {
1252
- logger.warn(`[Mojang API] 用户名"${username}"不存在`);
1253
- }
1254
- else if (axios_1.default.isAxiosError(error) && error.code === 'ECONNABORTED') {
1255
- logger.error(`[Mojang API] 验证用户名"${username}"时请求超时: ${error.message}`);
1256
- }
1257
- else {
1258
- // 记录更详细的错误信息
1259
- const errorMessage = axios_1.default.isAxiosError(error)
1260
- ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
1261
- : error.message || '未知错误';
1262
- logger.error(`[Mojang API] 验证用户名"${username}"时发生错误: ${errorMessage}`);
1263
- // 如果是网络相关错误,尝试使用备用API检查
1264
- if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
1265
- error.code === 'ETIMEDOUT' ||
1266
- error.code === 'ECONNRESET' ||
1267
- error.code === 'ECONNREFUSED' ||
1268
- error.code === 'ECONNABORTED' ||
1269
- error.response?.status === 429 || // 添加429 (Too Many Requests)
1270
- error.response?.status === 403)) { // 添加403 (Forbidden)
1271
- // 尝试使用playerdb.co作为备用API
1272
- logger.info(`[Mojang API] 遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
1273
- return tryBackupAPI(username);
1274
- }
1275
- }
1276
- return null;
1277
- }
1278
- };
1279
- // 使用备用API验证用户名
1280
- const tryBackupAPI = async (username) => {
1281
- logger.info(`[备用API] 尝试使用备用API验证用户名"${username}"`);
1282
- try {
1283
- // 使用playerdb.co作为备用API
1284
- const backupResponse = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${username}`, {
1285
- timeout: 10000,
1286
- headers: {
1287
- 'User-Agent': 'KoishiMCVerifier/1.0'
1288
- }
1289
- });
1290
- if (backupResponse.status === 200 && backupResponse.data?.code === "player.found") {
1291
- const playerData = backupResponse.data.data.player;
1292
- const rawId = playerData.raw_id || playerData.id.replace(/-/g, ''); // 确保使用不带连字符的UUID
1293
- logger.info(`[备用API] 用户名"${username}"验证成功,UUID: ${rawId},标准名称: ${playerData.username}`);
1294
- return {
1295
- id: rawId, // 确保使用不带连字符的UUID
1296
- name: playerData.username
1297
- };
1298
- }
1299
- logger.warn(`[备用API] 用户名"${username}"验证失败: ${JSON.stringify(backupResponse.data)}`);
1300
- return null;
1301
- }
1302
- catch (backupError) {
1303
- const errorMsg = axios_1.default.isAxiosError(backupError)
1304
- ? `${backupError.message}, 状态码: ${backupError.response?.status || '未知'}`
1305
- : backupError.message || '未知错误';
1306
- logger.error(`[备用API] 验证用户名"${username}"失败: ${errorMsg}`);
1307
- return null;
1308
- }
1309
- };
1310
- // 获取MC头图URL
1311
- const getCrafatarUrl = (uuid) => {
1312
- if (!uuid)
1313
- return null;
1314
- // 检查UUID格式 (不带连字符应为32位,带连字符应为36位)
1315
- 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;
1316
- if (!uuidRegex.test(uuid)) {
1317
- logger.warn(`[MC头图] UUID "${uuid}" 格式无效,无法生成头图URL`);
1318
- return null;
1319
- }
1320
- // 移除任何连字符,Crafatar接受不带连字符的UUID
1321
- const cleanUuid = uuid.replace(/-/g, '');
1322
- // 直接生成URL
1323
- const url = `https://crafatar.com/avatars/${cleanUuid}`;
1324
- logger.debug(`[MC头图] 为UUID "${cleanUuid}" 生成头图URL`);
1325
- return url;
1326
- };
1327
- // 使用Starlight SkinAPI获取皮肤渲染
1328
- const getStarlightSkinUrl = (username) => {
1329
- if (!username)
1330
- return null;
1331
- // 可用的动作列表 (共16种)
1332
- const poses = [
1333
- 'default', // 默认站立
1334
- 'marching', // 行军
1335
- 'walking', // 行走
1336
- 'crouching', // 下蹲
1337
- 'crossed', // 交叉手臂
1338
- 'crisscross', // 交叉腿
1339
- 'cheering', // 欢呼
1340
- 'relaxing', // 放松
1341
- 'trudging', // 艰难行走
1342
- 'cowering', // 退缩
1343
- 'pointing', // 指向
1344
- 'lunging', // 前冲
1345
- 'dungeons', // 地下城风格
1346
- 'facepalm', // 捂脸
1347
- 'mojavatar', // Mojave姿态
1348
- 'head', // 头部特写
1349
- ];
1350
- // 随机选择一个动作
1351
- const randomPose = poses[Math.floor(Math.random() * poses.length)];
1352
- // 视图类型(full为全身图)
1353
- const viewType = 'full';
1354
- // 生成URL
1355
- const url = `https://starlightskins.lunareclipse.studio/render/${randomPose}/${username}/${viewType}`;
1356
- logger.debug(`[Starlight皮肤] 为用户名"${username}"生成动作"${randomPose}"的渲染URL`);
1357
- return url;
1358
- };
1359
- // 格式化UUID (添加连字符,使其符合标准格式)
1360
- const formatUuid = (uuid) => {
1361
- if (!uuid)
1362
- return '未知';
1363
- if (uuid.includes('-'))
1364
- return uuid; // 已经是带连字符的格式
1365
- // 确保UUID长度正确
1366
- if (uuid.length !== 32) {
1367
- logger.warn(`[UUID] UUID "${uuid}" 长度异常,无法格式化`);
1368
- return uuid;
1369
- }
1370
- return `${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}`;
1371
- };
1372
- // 检查是否为管理员 (QQ号作为主键检查)
1373
- const isAdmin = async (userId) => {
1374
- // 主人始终是管理员
1375
- const normalizedMasterId = normalizeQQId(config.masterId);
1376
- const normalizedQQId = normalizeQQId(userId);
1377
- if (normalizedQQId === normalizedMasterId)
1378
- return true;
1379
- // 查询MCIDBIND表中是否是管理员
1380
- try {
1381
- const bind = await getMcBindByQQId(normalizedQQId);
1382
- return bind && bind.isAdmin === true;
1383
- }
1384
- catch (error) {
1385
- logger.error(`[权限检查] QQ(${normalizedQQId})的管理员状态查询失败: ${error.message}`);
1386
- return false;
1387
- }
1388
- };
1389
- // 检查是否为主人 (QQ号作为主键检查)
1390
- const isMaster = (qqId) => {
1391
- const normalizedMasterId = normalizeQQId(config.masterId);
1392
- const normalizedQQId = normalizeQQId(qqId);
1393
- return normalizedQQId === normalizedMasterId;
1394
- };
1395
- // =========== BUID相关功能 ===========
1396
- // 验证BUID是否存在
1397
- const validateBUID = async (buid) => {
1398
- try {
1399
- if (!buid || !/^\d+$/.test(buid)) {
1400
- logWarn('B站账号验证', `无效的B站UID格式: ${buid}`);
1401
- return null;
1402
- }
1403
- logDebug('B站账号验证', `验证B站UID: ${buid}`);
1404
- const response = await axios_1.default.get(`${config.zminfoApiUrl}/api/user/${buid}`, {
1405
- timeout: 10000,
1406
- headers: {
1407
- 'User-Agent': 'Koishi-MCID-Bot/1.0'
1408
- }
1409
- });
1410
- if (response.data.success && response.data.data && response.data.data.user) {
1411
- const user = response.data.data.user;
1412
- logDebug('B站账号验证', `B站UID ${buid} 验证成功: ${user.username}`);
1413
- return user;
1414
- }
1415
- else {
1416
- logWarn('B站账号验证', `B站UID ${buid} 不存在或API返回失败: ${response.data.message}`);
1417
- return null;
1418
- }
1419
- }
1420
- catch (error) {
1421
- if (error.response?.status === 404) {
1422
- logWarn('B站账号验证', `B站UID ${buid} 不存在`);
1423
- return null;
1424
- }
1425
- logError('B站账号验证', 'system', `验证B站UID ${buid} 时出错: ${error.message}`);
1426
- throw new Error(`无法验证B站UID: ${error.message}`);
1427
- }
1428
- };
1429
- // 根据B站UID查询绑定信息
1430
- const getBuidBindByBuid = async (buid) => {
1431
- try {
1432
- if (!buid) {
1433
- logger.warn(`[B站账号绑定] 尝试查询空B站UID`);
1434
- return null;
1435
- }
1436
- const bind = await mcidbindRepo.findByBuidUid(buid);
1437
- return bind;
1438
- }
1439
- catch (error) {
1440
- logError('B站账号绑定', 'system', `根据B站UID(${buid})查询绑定信息失败: ${error.message}`);
1441
- return null;
1442
- }
1443
- };
1444
- // 检查B站UID是否已被绑定
1445
- const checkBuidExists = async (buid, currentUserId) => {
1446
- try {
1447
- const bind = await getBuidBindByBuid(buid);
1448
- if (!bind)
1449
- return false;
1450
- // 如果指定了当前用户ID,则排除当前用户的绑定
1451
- if (currentUserId) {
1452
- const normalizedCurrentId = normalizeQQId(currentUserId);
1453
- return bind.qqId !== normalizedCurrentId;
1454
- }
1455
- return true;
1456
- }
1457
- catch (error) {
1458
- logError('B站账号绑定', 'system', `检查B站UID(${buid})是否存在时出错: ${error.message}`);
1459
- return false;
1460
- }
1461
- };
1462
- // 创建或更新B站账号绑定
1463
- const createOrUpdateBuidBind = async (userId, buidUser) => {
1464
- try {
1465
- const normalizedQQId = normalizeQQId(userId);
1466
- if (!normalizedQQId) {
1467
- logger.error(`[B站账号绑定] 创建/更新绑定失败: 无法提取有效的QQ号`);
1468
- return false;
1469
- }
1470
- // 检查该UID是否已被其他用户绑定(安全检查)
1471
- const existingBuidBind = await getBuidBindByBuid(buidUser.uid);
1472
- if (existingBuidBind && existingBuidBind.qqId !== normalizedQQId) {
1473
- logger.error(`[B站账号绑定] 安全检查失败: B站UID ${buidUser.uid} 已被QQ(${existingBuidBind.qqId})绑定,无法为QQ(${normalizedQQId})绑定`);
1474
- return false;
1475
- }
1476
- // 查询是否已存在绑定记录
1477
- let bind = await getMcBindByQQId(normalizedQQId);
1478
- const updateData = {
1479
- buidUid: buidUser.uid,
1480
- buidUsername: buidUser.username,
1481
- guardLevel: buidUser.guard_level || 0,
1482
- guardLevelText: buidUser.guard_level_text || '',
1483
- maxGuardLevel: buidUser.max_guard_level || 0,
1484
- maxGuardLevelText: buidUser.max_guard_level_text || '',
1485
- medalName: buidUser.medal?.name || '',
1486
- medalLevel: buidUser.medal?.level || 0,
1487
- wealthMedalLevel: buidUser.wealthMedalLevel || 0,
1488
- lastActiveTime: buidUser.last_active_time ? new Date(buidUser.last_active_time) : new Date(),
1489
- lastModified: new Date()
1490
- };
1491
- if (bind) {
1492
- await mcidbindRepo.update(normalizedQQId, updateData);
1493
- logger.info(`[B站账号绑定] 更新绑定: QQ=${normalizedQQId}, B站UID=${buidUser.uid}, 用户名=${buidUser.username}`);
1494
- }
1495
- else {
1496
- // 为跳过MC绑定的用户生成唯一的临时用户名,避免UNIQUE constraint冲突
1497
- const tempMcUsername = `_temp_skip_${normalizedQQId}_${Date.now()}`;
1498
- const newBind = {
1499
- qqId: normalizedQQId,
1500
- mcUsername: tempMcUsername,
1501
- mcUuid: '',
1502
- isAdmin: false,
1503
- whitelist: [],
1504
- tags: [],
1505
- ...updateData
1506
- };
1507
- await mcidbindRepo.create(newBind);
1508
- logger.info(`[B站账号绑定] 创建绑定(跳过MC): QQ=${normalizedQQId}, B站UID=${buidUser.uid}, 用户名=${buidUser.username}, 临时MC用户名=${tempMcUsername}`);
1509
- }
1510
- return true;
1511
- }
1512
- catch (error) {
1513
- logError('B站账号绑定', userId, `创建/更新B站账号绑定失败: ${error.message}`);
1514
- return false;
1515
- }
1516
- };
1517
- // 仅更新B站信息,不更新绑定时间(用于查询时刷新数据)
1518
- const updateBuidInfoOnly = async (userId, buidUser) => {
1519
- try {
1520
- const normalizedQQId = normalizeQQId(userId);
1521
- if (!normalizedQQId) {
1522
- logger.error(`[B站账号信息更新] 更新失败: 无法提取有效的QQ号`);
1523
- return false;
1524
- }
1525
- // 查询是否已存在绑定记录
1526
- const bind = await getMcBindByQQId(normalizedQQId);
1527
- if (!bind) {
1528
- logger.warn(`[B站账号信息更新] QQ(${normalizedQQId})没有绑定记录,无法更新B站信息`);
1529
- return false;
1530
- }
1531
- // 仅更新B站相关字段,不更新lastModified
1532
- const updateData = {
1533
- buidUsername: buidUser.username,
1534
- guardLevel: buidUser.guard_level || 0,
1535
- guardLevelText: buidUser.guard_level_text || '',
1536
- maxGuardLevel: buidUser.max_guard_level || 0,
1537
- maxGuardLevelText: buidUser.max_guard_level_text || '',
1538
- medalName: buidUser.medal?.name || '',
1539
- medalLevel: buidUser.medal?.level || 0,
1540
- wealthMedalLevel: buidUser.wealthMedalLevel || 0,
1541
- lastActiveTime: buidUser.last_active_time ? new Date(buidUser.last_active_time) : new Date()
1542
- };
1543
- await mcidbindRepo.update(normalizedQQId, updateData);
1544
- logger.info(`[B站账号信息更新] 刷新信息: QQ=${normalizedQQId}, B站UID=${bind.buidUid}, 用户名=${buidUser.username}`);
1545
- return true;
1546
- }
1547
- catch (error) {
1548
- logError('B站账号信息更新', userId, `更新B站账号信息失败: ${error.message}`);
1549
- return false;
1550
- }
1551
- };
1552
- // =========== 辅助函数 ===========
1553
- // RCON连接检查
1554
- const checkRconConnections = async () => {
1555
- if (!config.servers || config.servers.length === 0) {
1556
- logger.info('[RCON检查] 未配置任何服务器,跳过RCON检查');
1557
- return;
1558
- }
1559
- const results = {};
1560
- for (const server of config.servers) {
1561
- try {
1562
- logger.info(`[RCON检查] 正在检查服务器 ${server.name} (${server.rconAddress}) 的连接状态`);
1563
- // 尝试执行/list命令来测试连接 (使用RCON管理器)
1564
- await rconManager.executeCommand(server, 'list');
1565
- // 如果没有抛出异常,表示连接成功
1566
- logger.info(`[RCON检查] 服务器 ${server.name} 连接成功`);
1567
- results[server.id] = true;
1568
- }
1569
- catch (error) {
1570
- logger.error(`[RCON检查] 服务器 ${server.name} 连接失败: ${error.message}`);
1571
- results[server.id] = false;
1572
- }
1573
- }
1574
- // 生成检查结果摘要
1575
- const totalServers = config.servers.length;
1576
- const successCount = Object.values(results).filter(Boolean).length;
1577
- const failCount = totalServers - successCount;
1578
- logger.info(`[RCON检查] 检查完成: ${successCount}/${totalServers} 个服务器连接成功,${failCount} 个连接失败`);
1579
- if (failCount > 0) {
1580
- const failedServers = config.servers
1581
- .filter(server => !results[server.id])
1582
- .map(server => server.name)
1583
- .join(', ');
1584
- logger.warn(`[RCON检查] 以下服务器连接失败,白名单功能可能无法正常工作: ${failedServers}`);
1585
- }
1586
- };
1587
- // 使用Mojang API通过UUID查询用户名
1588
- const getUsernameByUuid = async (uuid) => {
1589
- try {
1590
- // 确保UUID格式正确(去除连字符)
1591
- const cleanUuid = uuid.replace(/-/g, '');
1592
- logger.debug(`[Mojang API] 通过UUID "${cleanUuid}" 查询用户名`);
1593
- const response = await axios_1.default.get(`https://api.mojang.com/user/profile/${cleanUuid}`, {
1594
- timeout: 10000,
1595
- headers: {
1596
- 'User-Agent': 'KoishiMCVerifier/1.0',
1597
- }
1598
- });
1599
- if (response.status === 200 && response.data) {
1600
- // 从返回数据中提取用户名
1601
- const username = response.data.name;
1602
- logger.debug(`[Mojang API] UUID "${cleanUuid}" 当前用户名: ${username}`);
1603
- return username;
1604
- }
1605
- logger.warn(`[Mojang API] UUID "${cleanUuid}" 查询不到用户名`);
1606
- return null;
1607
- }
1608
- catch (error) {
1609
- // 如果是网络相关错误,尝试使用备用API
1610
- if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
1611
- error.code === 'ETIMEDOUT' ||
1612
- error.code === 'ECONNRESET' ||
1613
- error.code === 'ECONNREFUSED' ||
1614
- error.code === 'ECONNABORTED' ||
1615
- error.response?.status === 429 || // 添加429 (Too Many Requests)
1616
- error.response?.status === 403)) { // 添加403 (Forbidden)
1617
- logger.info(`[Mojang API] 通过UUID查询用户名时遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
1618
- return getUsernameByUuidBackupAPI(uuid);
1619
- }
1620
- const errorMessage = axios_1.default.isAxiosError(error)
1621
- ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
1622
- : error.message || '未知错误';
1623
- logger.error(`[Mojang API] 通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
1624
- return null;
1625
- }
1626
- };
1627
- // 使用备用API通过UUID查询用户名
1628
- const getUsernameByUuidBackupAPI = async (uuid) => {
1629
- try {
1630
- // 确保UUID格式正确,备用API支持带连字符的UUID
1631
- const formattedUuid = uuid.includes('-') ? uuid : formatUuid(uuid);
1632
- logger.debug(`[备用API] 通过UUID "${formattedUuid}" 查询用户名`);
1633
- const response = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${formattedUuid}`, {
1634
- timeout: 10000,
1635
- headers: {
1636
- 'User-Agent': 'KoishiMCVerifier/1.0',
1637
- }
1638
- });
1639
- if (response.status === 200 && response.data?.code === "player.found") {
1640
- const playerData = response.data.data.player;
1641
- logger.debug(`[备用API] UUID "${formattedUuid}" 当前用户名: ${playerData.username}`);
1642
- return playerData.username;
1643
- }
1644
- logger.warn(`[备用API] UUID "${formattedUuid}" 查询不到用户名: ${JSON.stringify(response.data)}`);
1645
- return null;
1646
- }
1647
- catch (error) {
1648
- const errorMessage = axios_1.default.isAxiosError(error)
1649
- ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
1650
- : error.message || '未知错误';
1651
- logger.error(`[备用API] 通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
1652
- return null;
1653
- }
1654
- };
1655
- // 检查并更新用户名(如果与当前数据库中的不同)
1656
- const checkAndUpdateUsername = async (bind) => {
1657
- try {
1658
- if (!bind || !bind.mcUuid) {
1659
- logger.warn(`[用户名更新] 无法检查用户名更新: 空绑定或空UUID`);
1660
- return bind;
1661
- }
1662
- // 通过UUID查询最新用户名
1663
- const latestUsername = await getUsernameByUuid(bind.mcUuid);
1664
- if (!latestUsername) {
1665
- logger.warn(`[用户名更新] 无法获取UUID "${bind.mcUuid}" 的最新用户名`);
1666
- return bind;
1667
- }
1668
- // 如果用户名与数据库中的不同,更新数据库(使用规范化比较,不区分大小写)
1669
- const normalizedLatest = (0, helpers_1.normalizeUsername)(latestUsername);
1670
- const normalizedCurrent = (0, helpers_1.normalizeUsername)(bind.mcUsername);
1671
- if (normalizedLatest !== normalizedCurrent) {
1672
- logger.info(`[用户名更新] 用户 QQ(${bind.qqId}) 的Minecraft用户名已变更: ${bind.mcUsername} -> ${latestUsername}`);
1673
- // 更新数据库中的用户名
1674
- await ctx.database.set('mcidbind', { qqId: bind.qqId }, {
1675
- mcUsername: latestUsername
1676
- });
1677
- // 更新返回的绑定对象
1678
- bind.mcUsername = latestUsername;
1679
- }
1680
- return bind;
1681
- }
1682
- catch (error) {
1683
- logger.error(`[用户名更新] 检查和更新用户名失败: ${error.message}`);
1684
- return bind;
1685
- }
1686
- };
1687
- /**
1688
- * 智能缓存版本的改名检测函数
1689
- * 特性:
1690
- * - 24小时冷却期(失败>=3次时延长到72小时)
1691
- * - 失败计数追踪
1692
- * - 成功时重置失败计数
1693
- * @param bind 绑定记录
1694
- * @returns 更新后的绑定记录
1695
- */
1696
- const checkAndUpdateUsernameWithCache = async (bind) => {
1697
- try {
1698
- if (!bind || !bind.mcUuid) {
1699
- logger.warn(`[改名检测缓存] 无法检查用户名更新: 空绑定或空UUID`);
1700
- return bind;
1701
- }
1702
- const now = new Date();
1703
- const failCount = bind.usernameCheckFailCount || 0;
1704
- // 根据失败次数决定冷却期:普通24小时,失败>=3次则72小时
1705
- const cooldownHours = failCount >= 3 ? 72 : 24;
1706
- // 检查是否在冷却期内
1707
- if (bind.usernameLastChecked) {
1708
- const lastCheck = new Date(bind.usernameLastChecked);
1709
- const hoursSinceCheck = (now.getTime() - lastCheck.getTime()) / (1000 * 60 * 60);
1710
- if (hoursSinceCheck < cooldownHours) {
1711
- logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 在冷却期内(${hoursSinceCheck.toFixed(1)}h/${cooldownHours}h),跳过检查`);
1712
- return bind;
1713
- }
1714
- }
1715
- logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 开始检查用户名变更(失败计数: ${failCount})`);
1716
- // 执行实际的改名检测
1717
- const oldUsername = bind.mcUsername;
1718
- const updatedBind = await checkAndUpdateUsername(bind);
1719
- // 判断检测是否成功
1720
- const detectionSuccess = updatedBind.mcUsername !== null && updatedBind.mcUsername !== undefined;
1721
- if (detectionSuccess) {
1722
- // 检测成功
1723
- const usernameChanged = (0, helpers_1.normalizeUsername)(updatedBind.mcUsername) !== (0, helpers_1.normalizeUsername)(oldUsername);
1724
- // 更新检查时间和重置失败计数
1725
- await mcidbindRepo.update(bind.qqId, {
1726
- usernameLastChecked: now,
1727
- usernameCheckFailCount: 0
1728
- });
1729
- if (usernameChanged) {
1730
- logger.info(`[改名检测缓存] QQ(${bind.qqId}) 用户名已变更: ${oldUsername} -> ${updatedBind.mcUsername}`);
1731
- }
1732
- else {
1733
- logger.debug(`[改名检测缓存] QQ(${bind.qqId}) 用户名无变更: ${updatedBind.mcUsername}`);
1734
- }
1735
- // 更新返回对象的缓存字段
1736
- updatedBind.usernameLastChecked = now;
1737
- updatedBind.usernameCheckFailCount = 0;
1738
- }
1739
- else {
1740
- // 检测失败��API失败或返回null)
1741
- const newFailCount = failCount + 1;
1742
- await mcidbindRepo.update(bind.qqId, {
1743
- usernameLastChecked: now,
1744
- usernameCheckFailCount: newFailCount
1745
- });
1746
- logger.warn(`[改名检测缓存] QQ(${bind.qqId}) 检测失败,失败计数: ${newFailCount}`);
1747
- // 更新返回对象的缓存字段
1748
- updatedBind.usernameLastChecked = now;
1749
- updatedBind.usernameCheckFailCount = newFailCount;
1750
- }
1751
- return updatedBind;
1752
- }
1753
- catch (error) {
1754
- logger.error(`[改名检测缓存] 检查和更新用户名失败: ${error.message}`);
1755
- // 失败时也更新检查时间和递增失败计数
1756
- try {
1757
- const failCount = bind.usernameCheckFailCount || 0;
1758
- await mcidbindRepo.update(bind.qqId, {
1759
- usernameLastChecked: new Date(),
1760
- usernameCheckFailCount: failCount + 1
1761
- });
1762
- }
1763
- catch (updateError) {
1764
- logger.error(`[改名检测缓存] 更新失败计数时出错: ${updateError.message}`);
1765
- }
1766
- return bind;
1767
- }
1768
- };
1769
953
  // 安全地替换命令模板
1770
954
  const safeCommandReplace = (template, mcid) => {
1771
955
  // 过滤可能导致命令注入的字符
@@ -1823,6 +1007,30 @@ function apply(ctx, config) {
1823
1007
  // 如果ID未匹配到,尝试通过名称匹配
1824
1008
  return getServerConfigByName(serverIdOrName);
1825
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
+ };
1826
1034
  // =========== Handler 服务实例创建 ===========
1827
1035
  // 创建 MessageUtils 实例(暂时使用null,因为MessageUtils需要重构)
1828
1036
  const messageUtils = null;
@@ -1844,50 +1052,34 @@ function apply(ctx, config) {
1844
1052
  mcidbind: mcidbindRepo,
1845
1053
  scheduleMute: scheduleMuteRepo
1846
1054
  };
1847
- // 创建依赖对象,包含所有wrapper函数和服务
1055
+ // 创建依赖对象,直接传入服务实例
1848
1056
  const handlerDependencies = {
1849
- // Utility functions
1057
+ // ========== 核心服务实例 ==========
1058
+ apiService: services.api,
1059
+ databaseService: services.database,
1060
+ nicknameService: services.nickname,
1061
+ // ========== 工具函数 ==========
1850
1062
  normalizeQQId,
1851
1063
  formatCommand,
1852
- formatUuid,
1853
1064
  checkCooldown,
1854
- getCrafatarUrl,
1855
- getStarlightSkinUrl,
1856
- // Database operations (for McidCommandHandler)
1857
- getMcBindByQQId,
1858
- getMcBindByUsername,
1859
- createOrUpdateMcBind,
1860
- deleteMcBind,
1861
- checkUsernameExists,
1862
- checkAndUpdateUsername,
1863
- checkAndUpdateUsernameWithCache,
1864
- // API operations
1865
- validateUsername,
1866
- validateBUID,
1867
- updateBuidInfoOnly,
1868
- // Permission check functions
1065
+ // ========== 权限检查函数 ==========
1869
1066
  isAdmin,
1870
1067
  isMaster,
1871
- // Business functions
1068
+ // ========== 业务函数 ==========
1872
1069
  sendMessage,
1873
- autoSetGroupNickname,
1874
- checkNicknameFormat,
1875
- getBindInfo: getMcBindByQQId,
1876
- // Config operations
1877
- getServerConfigById,
1878
- // Error handling
1879
1070
  getFriendlyErrorMessage: error_utils_1.getFriendlyErrorMessage,
1880
- // Service instances
1071
+ getServerConfigById,
1072
+ // ========== 服务实例 ==========
1881
1073
  rconManager,
1882
1074
  messageUtils,
1883
1075
  forceBinder,
1884
1076
  groupExporter,
1885
- // Session management
1077
+ // ========== 会话管理 ==========
1886
1078
  getBindingSession,
1887
1079
  createBindingSession,
1888
1080
  updateBindingSession,
1889
1081
  removeBindingSession,
1890
- // Shared state
1082
+ // ========== 其他共享状态 ==========
1891
1083
  avatarCache: new Map(Object.entries(avatarCache).map(([k, v]) => [k, v])),
1892
1084
  bindingSessions
1893
1085
  };
@@ -1896,6 +1088,7 @@ function apply(ctx, config) {
1896
1088
  const tagHandler = new handlers_1.TagHandler(ctx, config, loggerService, repositories, handlerDependencies);
1897
1089
  const whitelistHandler = new handlers_1.WhitelistHandler(ctx, config, loggerService, repositories, handlerDependencies);
1898
1090
  const buidHandler = new handlers_1.BuidHandler(ctx, config, loggerService, repositories, handlerDependencies);
1091
+ const lotteryHandler = new handlers_1.LotteryHandler(ctx, config, loggerService, repositories, handlerDependencies);
1899
1092
  // 注册Handler命令
1900
1093
  bindingHandler.register();
1901
1094
  tagHandler.register();
@@ -2001,7 +1194,7 @@ function apply(ctx, config) {
2001
1194
  return next();
2002
1195
  }
2003
1196
  // 获取用户绑定信息
2004
- const bind = await getMcBindByQQId(normalizedUserId);
1197
+ const bind = await services.database.getMcBindByQQId(normalizedUserId);
2005
1198
  // 获取用户群昵称信息
2006
1199
  let currentNickname = '';
2007
1200
  try {
@@ -2051,7 +1244,7 @@ function apply(ctx, config) {
2051
1244
  // 情况2:只绑定了B站,未绑定MC
2052
1245
  if (bind.buidUid && bind.buidUsername && (!bind.mcUsername || bind.mcUsername.startsWith('_temp_'))) {
2053
1246
  const mcInfo = null;
2054
- const isNicknameCorrect = checkNicknameFormat(currentNickname, bind.buidUsername, mcInfo);
1247
+ const isNicknameCorrect = services.nickname.checkNicknameFormat(currentNickname, bind.buidUsername, mcInfo);
2055
1248
  if (!isNicknameCorrect) {
2056
1249
  // 更新提醒次数
2057
1250
  const reminderCount = (bind.reminderCount || 0) + 1;
@@ -2060,7 +1253,7 @@ function apply(ctx, config) {
2060
1253
  const reminderType = reminderCount >= 4 ? '警告' : '提醒';
2061
1254
  const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
2062
1255
  // 自动修改群昵称
2063
- await autoSetGroupNickname(session, mcInfo, bind.buidUsername);
1256
+ await services.nickname.autoSetGroupNickname(session, mcInfo, bind.buidUsername, bind.buidUid);
2064
1257
  setReminderCooldown(normalizedUserId);
2065
1258
  logger.info(`[随机提醒] 为仅绑定B站的用户QQ(${normalizedUserId})修复群昵称并发送第${reminderCount}次${reminderType}`);
2066
1259
  await sendMessage(session, [
@@ -2071,7 +1264,7 @@ function apply(ctx, config) {
2071
1264
  }
2072
1265
  // 情况3:都已绑定,但群昵称格式不正确
2073
1266
  if (bind.buidUid && bind.buidUsername && bind.mcUsername && !bind.mcUsername.startsWith('_temp_')) {
2074
- const isNicknameCorrect = checkNicknameFormat(currentNickname, bind.buidUsername, bind.mcUsername);
1267
+ const isNicknameCorrect = services.nickname.checkNicknameFormat(currentNickname, bind.buidUsername, bind.mcUsername);
2075
1268
  if (!isNicknameCorrect) {
2076
1269
  // 更新提醒次数
2077
1270
  const reminderCount = (bind.reminderCount || 0) + 1;
@@ -2080,7 +1273,7 @@ function apply(ctx, config) {
2080
1273
  const reminderType = reminderCount >= 4 ? '警告' : '提醒';
2081
1274
  const reminderPrefix = `【第${reminderCount}次${reminderType}】`;
2082
1275
  // 自动修改群昵称
2083
- await autoSetGroupNickname(session, bind.mcUsername, bind.buidUsername);
1276
+ await services.nickname.autoSetGroupNickname(session, bind.mcUsername, bind.buidUsername, bind.buidUid);
2084
1277
  setReminderCooldown(normalizedUserId);
2085
1278
  logger.info(`[随机提醒] 为已完全绑定的用户QQ(${normalizedUserId})修复群昵称并发送第${reminderCount}次${reminderType}`);
2086
1279
  await sendMessage(session, [
@@ -2200,7 +1393,7 @@ function apply(ctx, config) {
2200
1393
  // 处理跳过MC绑定,直接完成绑定流程
2201
1394
  if (content === '跳过' || content === 'skip') {
2202
1395
  // 检查用户是否已绑定B站账号
2203
- const existingBind = await getMcBindByQQId(normalizedUserId);
1396
+ const existingBind = await services.database.getMcBindByQQId(normalizedUserId);
2204
1397
  if (existingBind && existingBind.buidUid && existingBind.buidUsername) {
2205
1398
  // 用户已绑定B站账号,直接完成绑定
2206
1399
  logger.info(`[交互绑定] QQ(${normalizedUserId})跳过了MC账号绑定,已有B站绑定,完成绑定流程`);
@@ -2208,7 +1401,7 @@ function apply(ctx, config) {
2208
1401
  removeBindingSession(session.userId, session.channelId);
2209
1402
  // 设置群昵称
2210
1403
  try {
2211
- await autoSetGroupNickname(session, null, existingBind.buidUsername);
1404
+ await services.nickname.autoSetGroupNickname(session, null, existingBind.buidUsername, existingBind.buidUid);
2212
1405
  logger.info(`[交互绑定] QQ(${normalizedUserId})完成绑定,已设置群昵称`);
2213
1406
  }
2214
1407
  catch (renameError) {
@@ -2227,7 +1420,7 @@ function apply(ctx, config) {
2227
1420
  const timestamp = Date.now();
2228
1421
  const tempMcUsername = `_temp_skip_${timestamp}`;
2229
1422
  // 创建临时MC绑定
2230
- const tempBindResult = await createOrUpdateMcBind(session.userId, tempMcUsername, '', false);
1423
+ const tempBindResult = await services.database.createOrUpdateMcBind(session.userId, tempMcUsername, '', false);
2231
1424
  if (!tempBindResult) {
2232
1425
  logger.error(`[交互绑定] QQ(${normalizedUserId})创建临时MC绑定失败`);
2233
1426
  await sendMessage(session, [koishi_1.h.text('❌ 创建临时绑定失败,请稍后重试')]);
@@ -2250,7 +1443,7 @@ function apply(ctx, config) {
2250
1443
  return;
2251
1444
  }
2252
1445
  // 验证用户名是否存在
2253
- const profile = await validateUsername(content);
1446
+ const profile = await services.api.validateUsername(content);
2254
1447
  if (!profile) {
2255
1448
  logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的MC用户名"${content}"不存在`);
2256
1449
  await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${content} 不存在\n请重新输入或发送"跳过"完成绑定`)]);
@@ -2259,7 +1452,7 @@ function apply(ctx, config) {
2259
1452
  const username = profile.name;
2260
1453
  const uuid = profile.id;
2261
1454
  // 检查用户是否已绑定MC账号
2262
- const existingBind = await getMcBindByQQId(normalizedUserId);
1455
+ const existingBind = await services.database.getMcBindByQQId(normalizedUserId);
2263
1456
  if (existingBind && existingBind.mcUsername && !existingBind.mcUsername.startsWith('_temp_')) {
2264
1457
  // 检查冷却时间
2265
1458
  if (!await isAdmin(session.userId) && !checkCooldown(existingBind.lastModified)) {
@@ -2275,13 +1468,13 @@ function apply(ctx, config) {
2275
1468
  }
2276
1469
  }
2277
1470
  // 检查用户名是否已被其他人绑定
2278
- if (await checkUsernameExists(username, session.userId, uuid)) {
1471
+ if (await services.database.checkUsernameExists(username, session.userId, uuid)) {
2279
1472
  logger.warn(`[交互绑定] MC用户名"${username}"已被其他用户绑定`);
2280
1473
  await sendMessage(session, [koishi_1.h.text(`❌ 用户名 ${username} 已被其他用户绑定\n\n请输入其他MC用户名或发送"跳过"完成绑定`)]);
2281
1474
  return;
2282
1475
  }
2283
1476
  // 绑定MC账号
2284
- const bindResult = await createOrUpdateMcBind(session.userId, username, uuid);
1477
+ const bindResult = await services.database.createOrUpdateMcBind(session.userId, username, uuid);
2285
1478
  if (!bindResult) {
2286
1479
  logger.error(`[交互绑定] QQ(${normalizedUserId})绑定MC账号失败`);
2287
1480
  removeBindingSession(session.userId, session.channelId);
@@ -2290,7 +1483,7 @@ function apply(ctx, config) {
2290
1483
  }
2291
1484
  logger.info(`[交互绑定] QQ(${normalizedUserId})成功绑定MC账号: ${username}`);
2292
1485
  // 检查用户是否已经绑定了B站账号
2293
- const updatedBind = await getMcBindByQQId(normalizedUserId);
1486
+ const updatedBind = await services.database.getMcBindByQQId(normalizedUserId);
2294
1487
  if (updatedBind && updatedBind.buidUid && updatedBind.buidUsername) {
2295
1488
  // 用户已经绑定了B站账号,直接完成绑定流程
2296
1489
  logger.info(`[交互绑定] QQ(${normalizedUserId})已绑定B站账号,完成绑定流程`);
@@ -2298,7 +1491,7 @@ function apply(ctx, config) {
2298
1491
  removeBindingSession(session.userId, session.channelId);
2299
1492
  // 设置群昵称
2300
1493
  try {
2301
- await autoSetGroupNickname(session, username, updatedBind.buidUsername);
1494
+ await services.nickname.autoSetGroupNickname(session, username, updatedBind.buidUsername, updatedBind.buidUid);
2302
1495
  logger.info(`[交互绑定] QQ(${normalizedUserId})绑定完成,已设置群昵称`);
2303
1496
  }
2304
1497
  catch (renameError) {
@@ -2308,10 +1501,10 @@ function apply(ctx, config) {
2308
1501
  let mcAvatarUrl = null;
2309
1502
  if (config?.showAvatar) {
2310
1503
  if (config?.showMcSkin) {
2311
- mcAvatarUrl = getStarlightSkinUrl(username);
1504
+ mcAvatarUrl = services.api.getStarlightSkinUrl(username);
2312
1505
  }
2313
1506
  else {
2314
- mcAvatarUrl = getCrafatarUrl(uuid);
1507
+ mcAvatarUrl = services.api.getCrafatarUrl(uuid);
2315
1508
  }
2316
1509
  }
2317
1510
  // 发送完成消息
@@ -2332,13 +1525,13 @@ function apply(ctx, config) {
2332
1525
  let mcAvatarUrl = null;
2333
1526
  if (config?.showAvatar) {
2334
1527
  if (config?.showMcSkin) {
2335
- mcAvatarUrl = getStarlightSkinUrl(username);
1528
+ mcAvatarUrl = services.api.getStarlightSkinUrl(username);
2336
1529
  }
2337
1530
  else {
2338
- mcAvatarUrl = getCrafatarUrl(uuid);
1531
+ mcAvatarUrl = services.api.getCrafatarUrl(uuid);
2339
1532
  }
2340
1533
  }
2341
- const formattedUuid = formatUuid(uuid);
1534
+ const formattedUuid = services.api.formatUuid(uuid);
2342
1535
  // 发送简化的MC绑定成功消息
2343
1536
  await sendMessage(session, [
2344
1537
  koishi_1.h.text(`✅ MC账号: ${username}\n🔗 请发送您的B站UID`),
@@ -2389,20 +1582,20 @@ function apply(ctx, config) {
2389
1582
  return;
2390
1583
  }
2391
1584
  // 检查UID是否已被绑定
2392
- if (await checkBuidExists(actualUid, session.userId)) {
1585
+ if (await services.database.checkBuidExists(actualUid, session.userId)) {
2393
1586
  logger.warn(`[交互绑定] B站UID"${actualUid}"已被其他用户绑定`);
2394
1587
  await sendMessage(session, [koishi_1.h.text(`❌ UID ${actualUid} 已被其他用户绑定\n\n请输入其他B站UID\n或发送"跳过"仅绑定MC账号`)]);
2395
1588
  return;
2396
1589
  }
2397
1590
  // 验证UID是否存在
2398
- const buidUser = await validateBUID(actualUid);
1591
+ const buidUser = await services.api.validateBUID(actualUid);
2399
1592
  if (!buidUser) {
2400
1593
  logger.warn(`[交互绑定] QQ(${normalizedUserId})输入的B站UID"${actualUid}"不存在`);
2401
1594
  await sendMessage(session, [koishi_1.h.text(`❌ 无法验证UID: ${actualUid}\n\n该用户可能不存在或未被发现\n可以去直播间发个弹幕后重试绑定\n或发送"跳过"仅绑定MC账号`)]);
2402
1595
  return;
2403
1596
  }
2404
1597
  // 绑定B站账号
2405
- const bindResult = await createOrUpdateBuidBind(session.userId, buidUser);
1598
+ const bindResult = await services.database.createOrUpdateBuidBind(session.userId, buidUser);
2406
1599
  if (!bindResult) {
2407
1600
  logger.error(`[交互绑定] QQ(${normalizedUserId})绑定B站账号失败`);
2408
1601
  removeBindingSession(session.userId, session.channelId);
@@ -2419,7 +1612,7 @@ function apply(ctx, config) {
2419
1612
  try {
2420
1613
  // 检查是否有有效的MC用户名(不是临时用户名)
2421
1614
  const mcName = bindingSession.mcUsername && !bindingSession.mcUsername.startsWith('_temp_') ? bindingSession.mcUsername : null;
2422
- await autoSetGroupNickname(session, mcName, buidUser.username);
1615
+ await services.nickname.autoSetGroupNickname(session, mcName, buidUser.username, String(buidUser.uid));
2423
1616
  logger.info(`[交互绑定] QQ(${normalizedUserId})绑定完成,已设置群昵称`);
2424
1617
  }
2425
1618
  catch (renameError) {
@@ -2481,185 +1674,4 @@ function apply(ctx, config) {
2481
1674
  }
2482
1675
  return cleanedContent;
2483
1676
  };
2484
- // =========== 天选开奖 Webhook 处理 ===========
2485
- // 处理天选开奖结果
2486
- const handleLotteryResult = async (lotteryData) => {
2487
- try {
2488
- // 检查天选播报开关
2489
- if (!config?.enableLotteryBroadcast) {
2490
- logger.debug(`[天选开奖] 天选播报功能已禁用,跳过处理天选事件: ${lotteryData.lottery_id}`);
2491
- return;
2492
- }
2493
- logger.info(`[天选开奖] 开始处理天选事件: ${lotteryData.lottery_id},奖品: ${lotteryData.reward_name},中奖人数: ${lotteryData.winners.length}`);
2494
- // 生成标签名称
2495
- const tagName = `天选-${lotteryData.lottery_id}`;
2496
- // 统计信息
2497
- let matchedCount = 0;
2498
- let notBoundCount = 0;
2499
- let tagAddedCount = 0;
2500
- let tagExistedCount = 0;
2501
- const matchedUsers = [];
2502
- // 处理每个中奖用户
2503
- for (const winner of lotteryData.winners) {
2504
- try {
2505
- // 根据B站UID查找绑定的QQ用户
2506
- const bind = await getBuidBindByBuid(winner.uid.toString());
2507
- if (bind && bind.qqId) {
2508
- matchedCount++;
2509
- matchedUsers.push({
2510
- qqId: bind.qqId,
2511
- mcUsername: bind.mcUsername || '未绑定MC',
2512
- buidUsername: bind.buidUsername,
2513
- uid: winner.uid,
2514
- username: winner.username
2515
- });
2516
- // 检查是否已有该标签
2517
- if (bind.tags && bind.tags.includes(tagName)) {
2518
- tagExistedCount++;
2519
- logger.debug(`[天选开奖] QQ(${bind.qqId})已有标签"${tagName}"`);
2520
- }
2521
- else {
2522
- // 添加标签
2523
- const newTags = [...(bind.tags || []), tagName];
2524
- await mcidbindRepo.update(bind.qqId, { tags: newTags });
2525
- tagAddedCount++;
2526
- logger.debug(`[天选开奖] 为QQ(${bind.qqId})添加标签"${tagName}"`);
2527
- }
2528
- }
2529
- else {
2530
- notBoundCount++;
2531
- logger.debug(`[天选开奖] B站UID(${winner.uid})未绑定QQ账号`);
2532
- }
2533
- }
2534
- catch (error) {
2535
- logger.error(`[天选开奖] 处理中奖用户UID(${winner.uid})时出错: ${error.message}`);
2536
- }
2537
- }
2538
- logger.info(`[天选开奖] 处理完成: 总计${lotteryData.winners.length}人中奖,匹配${matchedCount}人,未绑定${notBoundCount}人,新增标签${tagAddedCount}人,已有标签${tagExistedCount}人`);
2539
- // 生成并发送结果消息
2540
- await sendLotteryResultToGroup(lotteryData, {
2541
- totalWinners: lotteryData.winners.length,
2542
- matchedCount,
2543
- notBoundCount,
2544
- tagAddedCount,
2545
- tagExistedCount,
2546
- matchedUsers,
2547
- tagName
2548
- });
2549
- }
2550
- catch (error) {
2551
- logger.error(`[天选开奖] 处理天选事件"${lotteryData.lottery_id}"失败: ${error.message}`);
2552
- }
2553
- };
2554
- // 发送天选开奖结果到群
2555
- const sendLotteryResultToGroup = async (lotteryData, stats) => {
2556
- try {
2557
- const targetChannelId = '123456789'; // 目标群号
2558
- const privateTargetId = 'private:3431185320'; // 私聊目标
2559
- // 格式化时间
2560
- const lotteryTime = new Date(lotteryData.timestamp).toLocaleString('zh-CN', {
2561
- timeZone: 'Asia/Shanghai',
2562
- year: 'numeric',
2563
- month: '2-digit',
2564
- day: '2-digit',
2565
- hour: '2-digit',
2566
- minute: '2-digit',
2567
- second: '2-digit'
2568
- });
2569
- // 构建简化版群消息(去掉主播信息、统计信息和标签提示)
2570
- let groupMessage = `🎉 天选开奖结果通知\n\n`;
2571
- groupMessage += `📅 开奖时间: ${lotteryTime}\n`;
2572
- groupMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
2573
- groupMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
2574
- groupMessage += `🎲 总中奖人数: ${stats.totalWinners}人`;
2575
- // 添加未绑定用户说明
2576
- if (stats.notBoundCount > 0) {
2577
- groupMessage += `(其中${stats.notBoundCount}人未绑定跳过)`;
2578
- }
2579
- groupMessage += `\n\n`;
2580
- // 如果有匹配的用户,显示详细信息
2581
- if (stats.matchedUsers.length > 0) {
2582
- groupMessage += `🎯 已绑定的中奖用户:\n`;
2583
- // 限制显示前10个用户,避免消息过长
2584
- const displayUsers = stats.matchedUsers.slice(0, 10);
2585
- for (let i = 0; i < displayUsers.length; i++) {
2586
- const user = displayUsers[i];
2587
- const index = i + 1;
2588
- const displayMcName = user.mcUsername && !user.mcUsername.startsWith('_temp_') ? user.mcUsername : '未绑定';
2589
- groupMessage += `${index}. ${user.buidUsername} (UID: ${user.uid})\n`;
2590
- groupMessage += ` QQ: ${user.qqId} | MC: ${displayMcName}\n`;
2591
- }
2592
- // 如果用户太多,显示省略信息
2593
- if (stats.matchedUsers.length > 10) {
2594
- groupMessage += `... 还有${stats.matchedUsers.length - 10}位中奖用户\n`;
2595
- }
2596
- }
2597
- else {
2598
- groupMessage += `😔 暂无已绑定用户中奖\n`;
2599
- }
2600
- // 构建完整版私聊消息(包含所有信息和未绑定用户)
2601
- let privateMessage = `🎉 天选开奖结果通知\n\n`;
2602
- privateMessage += `📅 开奖时间: ${lotteryTime}\n`;
2603
- privateMessage += `🎁 奖品名称: ${lotteryData.reward_name}\n`;
2604
- privateMessage += `📊 奖品数量: ${lotteryData.reward_num}个\n`;
2605
- privateMessage += `🏷️ 事件ID: ${lotteryData.lottery_id}\n`;
2606
- privateMessage += `👤 主播: ${lotteryData.host_username} (UID: ${lotteryData.host_uid})\n`;
2607
- privateMessage += `🏠 房间号: ${lotteryData.room_id}\n\n`;
2608
- // 统计信息
2609
- privateMessage += `📈 处理统计:\n`;
2610
- privateMessage += `• 总中奖人数: ${stats.totalWinners}人\n`;
2611
- privateMessage += `• 已绑定用户: ${stats.matchedCount}人 ✅\n`;
2612
- privateMessage += `• 未绑定用户: ${stats.notBoundCount}人 ⚠️\n`;
2613
- privateMessage += `• 新增标签: ${stats.tagAddedCount}人\n`;
2614
- privateMessage += `• 已有标签: ${stats.tagExistedCount}人\n\n`;
2615
- // 显示所有中奖用户(包括未绑定的)
2616
- if (lotteryData.winners.length > 0) {
2617
- privateMessage += `🎯 所有中奖用户:\n`;
2618
- for (let i = 0; i < lotteryData.winners.length; i++) {
2619
- const winner = lotteryData.winners[i];
2620
- const index = i + 1;
2621
- // 查找对应的绑定用户
2622
- const matchedUser = stats.matchedUsers.find(user => user.uid === winner.uid);
2623
- if (matchedUser) {
2624
- const displayMcName = matchedUser.mcUsername && !matchedUser.mcUsername.startsWith('_temp_') ? matchedUser.mcUsername : '未绑定';
2625
- privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
2626
- privateMessage += ` QQ: ${matchedUser.qqId} | MC: ${displayMcName}\n`;
2627
- }
2628
- else {
2629
- privateMessage += `${index}. ${winner.username} (UID: ${winner.uid})\n`;
2630
- privateMessage += ` 无绑定信息,自动跳过\n`;
2631
- }
2632
- }
2633
- privateMessage += `\n🏷️ 标签"${stats.tagName}"已自动添加到已绑定用户\n`;
2634
- }
2635
- // 准备消息元素
2636
- const groupMessageElements = [koishi_1.h.text(groupMessage)];
2637
- const privateMessageElements = [koishi_1.h.text(privateMessage)];
2638
- // 发送消息到指定群(简化版)
2639
- for (const bot of ctx.bots) {
2640
- try {
2641
- await bot.sendMessage(targetChannelId, groupMessageElements);
2642
- logger.info(`[天选开奖] 成功发送简化开奖结果到群${targetChannelId}`);
2643
- break; // 成功发送后退出循环
2644
- }
2645
- catch (error) {
2646
- logger.error(`[天选开奖] 发送消息到群${targetChannelId}失败: ${error.message}`);
2647
- }
2648
- }
2649
- // 发送消息到私聊(完整版)
2650
- for (const bot of ctx.bots) {
2651
- try {
2652
- await bot.sendMessage(privateTargetId, privateMessageElements);
2653
- logger.info(`[天选开奖] 成功发送完整开奖结果到私聊${privateTargetId}`);
2654
- break; // 成功发送后退出循环
2655
- }
2656
- catch (error) {
2657
- logger.error(`[天选开奖] 发送消息到私聊${privateTargetId}失败: ${error.message}`);
2658
- }
2659
- }
2660
- }
2661
- catch (error) {
2662
- logger.error(`[天选开奖] 发送开奖结果失败: ${error.message}`);
2663
- }
2664
- };
2665
1677
  }