koishi-plugin-bind-bot 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/lib/export-utils.d.ts +49 -0
  2. package/lib/export-utils.js +305 -0
  3. package/lib/force-bind-utils.d.ts +40 -0
  4. package/lib/force-bind-utils.js +242 -0
  5. package/lib/handlers/base.handler.d.ts +61 -0
  6. package/lib/handlers/base.handler.js +22 -0
  7. package/lib/handlers/binding.handler.d.ts +45 -0
  8. package/lib/handlers/binding.handler.js +285 -0
  9. package/lib/handlers/buid.handler.d.ts +71 -0
  10. package/lib/handlers/buid.handler.js +694 -0
  11. package/lib/handlers/index.d.ts +6 -0
  12. package/lib/handlers/index.js +22 -0
  13. package/lib/handlers/mcid.handler.d.ts +101 -0
  14. package/lib/handlers/mcid.handler.js +1045 -0
  15. package/lib/handlers/tag.handler.d.ts +14 -0
  16. package/lib/handlers/tag.handler.js +382 -0
  17. package/lib/handlers/whitelist.handler.d.ts +84 -0
  18. package/lib/handlers/whitelist.handler.js +1011 -0
  19. package/lib/index.d.ts +7 -0
  20. package/lib/index.js +2693 -0
  21. package/lib/managers/rcon-manager.d.ts +24 -0
  22. package/lib/managers/rcon-manager.js +308 -0
  23. package/lib/repositories/mcidbind.repository.d.ts +105 -0
  24. package/lib/repositories/mcidbind.repository.js +288 -0
  25. package/lib/repositories/schedule-mute.repository.d.ts +68 -0
  26. package/lib/repositories/schedule-mute.repository.js +175 -0
  27. package/lib/types/api.d.ts +135 -0
  28. package/lib/types/api.js +6 -0
  29. package/lib/types/common.d.ts +40 -0
  30. package/lib/types/common.js +6 -0
  31. package/lib/types/config.d.ts +55 -0
  32. package/lib/types/config.js +6 -0
  33. package/lib/types/database.d.ts +47 -0
  34. package/lib/types/database.js +6 -0
  35. package/lib/types/index.d.ts +8 -0
  36. package/lib/types/index.js +28 -0
  37. package/lib/utils/helpers.d.ts +76 -0
  38. package/lib/utils/helpers.js +275 -0
  39. package/lib/utils/logger.d.ts +75 -0
  40. package/lib/utils/logger.js +134 -0
  41. package/lib/utils/message-utils.d.ts +46 -0
  42. package/lib/utils/message-utils.js +234 -0
  43. package/lib/utils/rate-limiter.d.ts +26 -0
  44. package/lib/utils/rate-limiter.js +47 -0
  45. package/lib/utils/session-manager.d.ts +70 -0
  46. package/lib/utils/session-manager.js +120 -0
  47. package/package.json +39 -0
  48. package/readme.md +281 -0
@@ -0,0 +1,49 @@
1
+ import { Session, Context } from 'koishi';
2
+ import { LoggerService } from './utils/logger';
3
+ import { MCIDBINDRepository } from './repositories/mcidbind.repository';
4
+ export declare class GroupExporter {
5
+ private logger;
6
+ private ctx;
7
+ private mcidbindRepo;
8
+ constructor(ctx: Context, logger: LoggerService, mcidbindRepo: MCIDBINDRepository);
9
+ /**
10
+ * 获取群成员列表
11
+ */
12
+ private getGroupMembers;
13
+ /**
14
+ * 翻译群角色
15
+ */
16
+ private translateRole;
17
+ /**
18
+ * 获取所有绑定信息
19
+ */
20
+ private getAllBindings;
21
+ /**
22
+ * 格式化时间戳
23
+ */
24
+ private formatTimestamp;
25
+ /**
26
+ * 合并群成员信息和绑定信息
27
+ */
28
+ private mergeData;
29
+ /**
30
+ * 生成Excel文件
31
+ */
32
+ private generateExcelFile;
33
+ /**
34
+ * 导出群数据
35
+ */
36
+ exportGroupData(session: Session, groupId: string): Promise<Buffer>;
37
+ /**
38
+ * 获取导出文件名
39
+ */
40
+ getExportFileName(groupId: string): string;
41
+ /**
42
+ * 保存Excel文件到临时目录并返回文件路径
43
+ */
44
+ saveExcelFile(excelBuffer: Buffer, fileName: string): Promise<string>;
45
+ /**
46
+ * 清理过期的临时文件(可选)
47
+ */
48
+ cleanupOldFiles(maxAgeHours?: number): Promise<void>;
49
+ }
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.GroupExporter = void 0;
37
+ const XLSX = __importStar(require("xlsx"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ class GroupExporter {
41
+ logger;
42
+ ctx;
43
+ mcidbindRepo;
44
+ constructor(ctx, logger, mcidbindRepo) {
45
+ this.ctx = ctx;
46
+ this.logger = logger;
47
+ this.mcidbindRepo = mcidbindRepo;
48
+ }
49
+ /**
50
+ * 获取群成员列表
51
+ */
52
+ async getGroupMembers(session, groupId) {
53
+ try {
54
+ this.logger.debug('群数据导出', `开始获取群 ${groupId} 的成员列表`);
55
+ if (!session.bot.internal) {
56
+ throw new Error('Bot不支持获取群成员列表');
57
+ }
58
+ const memberList = await session.bot.internal.getGroupMemberList(groupId);
59
+ this.logger.debug('群数据导出', `获取到 ${memberList.length} 个群成员`);
60
+ return memberList.map(member => ({
61
+ qqId: member.user_id.toString(),
62
+ nickname: member.nickname || '',
63
+ card: member.card || '',
64
+ role: this.translateRole(member.role),
65
+ join_time: member.join_time || 0,
66
+ last_sent_time: member.last_sent_time || 0
67
+ }));
68
+ }
69
+ catch (error) {
70
+ this.logger.error('群数据导出', `获取群成员列表失败: ${error.message}`);
71
+ throw new Error(`无法获取群 ${groupId} 的成员列表: ${error.message}`);
72
+ }
73
+ }
74
+ /**
75
+ * 翻译群角色
76
+ */
77
+ translateRole(role) {
78
+ switch (role) {
79
+ case 'owner': return '群主';
80
+ case 'admin': return '管理员';
81
+ case 'member': return '成员';
82
+ default: return role || '未知';
83
+ }
84
+ }
85
+ /**
86
+ * 获取所有绑定信息
87
+ */
88
+ async getAllBindings() {
89
+ try {
90
+ this.logger.debug('群数据导出', '开始获取数据库绑定信息');
91
+ const bindings = await this.mcidbindRepo.findAll();
92
+ this.logger.debug('群数据导出', `获取到 ${bindings.length} 条绑定记录`);
93
+ return bindings;
94
+ }
95
+ catch (error) {
96
+ this.logger.error('群数据导出', `获取绑定信息失败: ${error.message}`);
97
+ throw new Error(`无法获取绑定信息: ${error.message}`);
98
+ }
99
+ }
100
+ /**
101
+ * 格式化时间戳
102
+ */
103
+ formatTimestamp(timestamp) {
104
+ if (!timestamp)
105
+ return '未知';
106
+ let date;
107
+ if (timestamp instanceof Date) {
108
+ date = timestamp;
109
+ }
110
+ else if (typeof timestamp === 'number') {
111
+ // 如果是秒级时间戳,转换为毫秒
112
+ date = new Date(timestamp < 10000000000 ? timestamp * 1000 : timestamp);
113
+ }
114
+ else {
115
+ return '未知';
116
+ }
117
+ return date.toLocaleString('zh-CN', {
118
+ year: 'numeric',
119
+ month: '2-digit',
120
+ day: '2-digit',
121
+ hour: '2-digit',
122
+ minute: '2-digit',
123
+ second: '2-digit'
124
+ });
125
+ }
126
+ /**
127
+ * 合并群成员信息和绑定信息
128
+ */
129
+ mergeData(members, bindings) {
130
+ this.logger.debug('群数据导出', '开始合并群成员和绑定信息');
131
+ // 创建绑定信息映射
132
+ const bindingMap = new Map();
133
+ bindings.forEach(binding => {
134
+ bindingMap.set(binding.qqId, binding);
135
+ });
136
+ return members.map(member => {
137
+ const binding = bindingMap.get(member.qqId);
138
+ // 判断绑定状态
139
+ let bindingStatus = '未绑定';
140
+ if (binding) {
141
+ const hasMc = binding.mcUsername && !binding.mcUsername.startsWith('_temp_');
142
+ const hasBuid = binding.buidUid;
143
+ if (hasMc && hasBuid) {
144
+ bindingStatus = '完全绑定';
145
+ }
146
+ else if (hasMc) {
147
+ bindingStatus = '仅MC绑定';
148
+ }
149
+ else if (hasBuid) {
150
+ bindingStatus = '仅B站绑定';
151
+ }
152
+ else {
153
+ bindingStatus = '未绑定';
154
+ }
155
+ }
156
+ return {
157
+ QQ号: member.qqId,
158
+ 群昵称: member.card || member.nickname,
159
+ 用户昵称: member.nickname,
160
+ 角色: member.role,
161
+ 加群时间: this.formatTimestamp(member.join_time),
162
+ 最后发言: this.formatTimestamp(member.last_sent_time),
163
+ MC用户名: binding?.mcUsername && !binding.mcUsername.startsWith('_temp_') ? binding.mcUsername : '',
164
+ MC_UUID: binding?.mcUuid || '',
165
+ B站UID: binding?.buidUid || '',
166
+ B站用户名: binding?.buidUsername || '',
167
+ 舰长等级: binding?.guardLevelText || '',
168
+ 粉丝牌名称: binding?.medalName || '',
169
+ 粉丝牌等级: binding?.medalLevel ? binding.medalLevel.toString() : '',
170
+ 荣耀等级: binding?.wealthMedalLevel ? binding.wealthMedalLevel.toString() : '',
171
+ 绑定状态: bindingStatus,
172
+ 最后修改时间: this.formatTimestamp(binding?.lastModified)
173
+ };
174
+ });
175
+ }
176
+ /**
177
+ * 生成Excel文件
178
+ */
179
+ generateExcelFile(allData, groupId) {
180
+ this.logger.debug('群数据导出', '开始生成Excel文件');
181
+ // 分类数据
182
+ const boundData = allData.filter(row => row.绑定状态 !== '未绑定');
183
+ const unboundData = allData.filter(row => row.绑定状态 === '未绑定');
184
+ // 创建工作簿
185
+ const workbook = XLSX.utils.book_new();
186
+ // Sheet1: 所有成员
187
+ const allSheet = XLSX.utils.json_to_sheet(allData);
188
+ XLSX.utils.book_append_sheet(workbook, allSheet, '所有成员');
189
+ // Sheet2: 已绑定成员
190
+ const boundSheet = XLSX.utils.json_to_sheet(boundData);
191
+ XLSX.utils.book_append_sheet(workbook, boundSheet, '已绑定成员');
192
+ // Sheet3: 未绑定成员
193
+ const unboundSheet = XLSX.utils.json_to_sheet(unboundData);
194
+ XLSX.utils.book_append_sheet(workbook, unboundSheet, '未绑定成员');
195
+ // 设置列宽
196
+ const colWidths = [
197
+ { wch: 12 }, // QQ号
198
+ { wch: 20 }, // 群昵称
199
+ { wch: 15 }, // 用户昵称
200
+ { wch: 8 }, // 角色
201
+ { wch: 18 }, // 加群时间
202
+ { wch: 18 }, // 最后发言
203
+ { wch: 16 }, // MC用户名
204
+ { wch: 36 }, // MC_UUID
205
+ { wch: 12 }, // B站UID
206
+ { wch: 20 }, // B站用户名
207
+ { wch: 10 }, // 舰长等级
208
+ { wch: 12 }, // 粉丝牌名称
209
+ { wch: 10 }, // 粉丝牌等级
210
+ { wch: 8 }, // 荣耀等级
211
+ { wch: 12 }, // 绑定状态
212
+ { wch: 18 } // 最后修改时间
213
+ ];
214
+ // 应用列宽到所有sheet
215
+ allSheet['!cols'] = colWidths;
216
+ boundSheet['!cols'] = colWidths;
217
+ unboundSheet['!cols'] = colWidths;
218
+ // 生成Excel文件
219
+ const excelBuffer = XLSX.write(workbook, {
220
+ type: 'buffer',
221
+ bookType: 'xlsx',
222
+ compression: true
223
+ });
224
+ this.logger.debug('群数据导出', 'Excel文件生成完成');
225
+ return excelBuffer;
226
+ }
227
+ /**
228
+ * 导出群数据
229
+ */
230
+ async exportGroupData(session, groupId) {
231
+ try {
232
+ this.logger.info('群数据导出', `开始导出群 ${groupId} 的数据`);
233
+ // 并行获取群成员和绑定信息
234
+ const [members, bindings] = await Promise.all([
235
+ this.getGroupMembers(session, groupId),
236
+ this.getAllBindings()
237
+ ]);
238
+ // 合并数据
239
+ const mergedData = this.mergeData(members, bindings);
240
+ // 生成Excel文件
241
+ const excelBuffer = this.generateExcelFile(mergedData, groupId);
242
+ this.logger.info('群数据导出', `群 ${groupId} 数据导出完成,共 ${members.length} 个成员,其中 ${mergedData.filter(d => d.绑定状态 !== '未绑定').length} 个已绑定`);
243
+ return excelBuffer;
244
+ }
245
+ catch (error) {
246
+ this.logger.error('群数据导出', `导出群数据失败`, error);
247
+ throw error;
248
+ }
249
+ }
250
+ /**
251
+ * 获取导出文件名
252
+ */
253
+ getExportFileName(groupId) {
254
+ const now = new Date();
255
+ const dateStr = now.toISOString().slice(0, 19).replace(/[:\-T]/g, '');
256
+ return `群${groupId}_绑定数据_${dateStr}.xlsx`;
257
+ }
258
+ /**
259
+ * 保存Excel文件到临时目录并返回文件路径
260
+ */
261
+ async saveExcelFile(excelBuffer, fileName) {
262
+ try {
263
+ // 创建临时目录
264
+ const tempDir = path.join(process.cwd(), 'temp', 'exports');
265
+ if (!fs.existsSync(tempDir)) {
266
+ fs.mkdirSync(tempDir, { recursive: true });
267
+ }
268
+ // 生成文件路径
269
+ const filePath = path.join(tempDir, fileName);
270
+ // 写入文件
271
+ fs.writeFileSync(filePath, excelBuffer);
272
+ this.logger.debug('群数据导出', `Excel文件已保存到: ${filePath}`);
273
+ return filePath;
274
+ }
275
+ catch (error) {
276
+ this.logger.error('群数据导出', `保存Excel文件失败: ${error.message}`);
277
+ throw new Error(`保存文件失败: ${error.message}`);
278
+ }
279
+ }
280
+ /**
281
+ * 清理过期的临时文件(可选)
282
+ */
283
+ async cleanupOldFiles(maxAgeHours = 24) {
284
+ try {
285
+ const tempDir = path.join(process.cwd(), 'temp', 'exports');
286
+ if (!fs.existsSync(tempDir))
287
+ return;
288
+ const files = fs.readdirSync(tempDir);
289
+ const now = Date.now();
290
+ const maxAge = maxAgeHours * 60 * 60 * 1000;
291
+ for (const file of files) {
292
+ const filePath = path.join(tempDir, file);
293
+ const stats = fs.statSync(filePath);
294
+ if (now - stats.mtime.getTime() > maxAge) {
295
+ fs.unlinkSync(filePath);
296
+ this.logger.debug('群数据导出', `已清理过期文件: ${file}`);
297
+ }
298
+ }
299
+ }
300
+ catch (error) {
301
+ this.logger.warn('群数据导出', `清理临时文件失败: ${error.message}`);
302
+ }
303
+ }
304
+ }
305
+ exports.GroupExporter = GroupExporter;
@@ -0,0 +1,40 @@
1
+ import { LoggerService } from './utils/logger';
2
+ import type { ForceBindConfig, EnhancedZminfoUser } from './types';
3
+ export declare class ForceBinder {
4
+ private logger;
5
+ private config;
6
+ private cookieString;
7
+ constructor(config: ForceBindConfig, logger: LoggerService);
8
+ /**
9
+ * 处理cookie字符串,支持完整cookie或单独SESSDATA
10
+ */
11
+ private processCookie;
12
+ /**
13
+ * 检查B站登录状态
14
+ */
15
+ private checkBilibiliLoginStatus;
16
+ /**
17
+ * 获取用户的粉丝勋章信息(使用B站API)
18
+ */
19
+ private getBilibiliMedals;
20
+ /**
21
+ * 获取用户的基本信息(使用ZMINFO API)
22
+ */
23
+ private getZminfoUserInfo;
24
+ /**
25
+ * 检查是否拥有目标粉丝牌
26
+ */
27
+ private checkTargetMedal;
28
+ /**
29
+ * 强制绑定用户,获取完整信息包括目标粉丝牌数据
30
+ */
31
+ forceBindUser(uid: string): Promise<EnhancedZminfoUser>;
32
+ /**
33
+ * 转换为标准的ZminfoUser格式(用于数据库存储)
34
+ */
35
+ convertToZminfoUser(enhancedUser: EnhancedZminfoUser): any;
36
+ /**
37
+ * 获取目标粉丝牌的详细信息(用于显示)
38
+ */
39
+ getTargetMedalDetails(enhancedUser: EnhancedZminfoUser): string;
40
+ }
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ForceBinder = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ class ForceBinder {
9
+ logger;
10
+ config;
11
+ cookieString;
12
+ constructor(config, logger) {
13
+ this.config = config;
14
+ this.logger = logger;
15
+ this.cookieString = this.processCookie(config.SESSDATA);
16
+ }
17
+ /**
18
+ * 处理cookie字符串,支持完整cookie或单独SESSDATA
19
+ */
20
+ processCookie(input) {
21
+ if (!input || input.trim() === '') {
22
+ throw new Error('Cookie配置不能为空');
23
+ }
24
+ const trimmedInput = input.trim();
25
+ // 如果输入包含多个cookie字段(包含分号),则认为是完整cookie
26
+ if (trimmedInput.includes(';')) {
27
+ this.logger.debug('强制绑定', '检测到完整cookie字符串');
28
+ return trimmedInput;
29
+ }
30
+ // 如果输入只是SESSDATA值(不包含"SESSDATA="前缀)
31
+ if (!trimmedInput.startsWith('SESSDATA=')) {
32
+ this.logger.debug('强制绑定', '检测到SESSDATA值,添加前缀');
33
+ return `SESSDATA=${trimmedInput}`;
34
+ }
35
+ // 如果输入已经是"SESSDATA=xxx"格式
36
+ this.logger.debug('强制绑定', '检测到SESSDATA格式');
37
+ return trimmedInput;
38
+ }
39
+ /**
40
+ * 检查B站登录状态
41
+ */
42
+ async checkBilibiliLoginStatus() {
43
+ try {
44
+ // 使用一个简单的API来检查登录状态
45
+ const response = await axios_1.default.get('https://api.bilibili.com/x/web-interface/nav', {
46
+ headers: {
47
+ Cookie: this.cookieString,
48
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
49
+ },
50
+ timeout: 5000
51
+ });
52
+ this.logger.debug('强制绑定', `登录状态检查返回: ${response.data.code}`);
53
+ // code为0表示登录成功
54
+ if (response.data.code === 0) {
55
+ this.logger.debug('强制绑定', 'B站登录状态正常');
56
+ return true;
57
+ }
58
+ else {
59
+ this.logger.warn('强制绑定', `B站登录状态异常: ${response.data.message || '未知错误'}`);
60
+ return false;
61
+ }
62
+ }
63
+ catch (error) {
64
+ this.logger.warn('强制绑定', `检查B站登录状态失败: ${error.message}`);
65
+ return false;
66
+ }
67
+ }
68
+ /**
69
+ * 获取用户的粉丝勋章信息(使用B站API)
70
+ */
71
+ async getBilibiliMedals(uid) {
72
+ this.logger.debug('强制绑定', `开始获取用户 ${uid} 的粉丝勋章`);
73
+ try {
74
+ const url = 'https://api.live.bilibili.com/xlive/web-ucenter/user/MedalWall';
75
+ this.logger.debug('强制绑定', `API请求: ${url}?target_id=${uid}`);
76
+ const response = await axios_1.default.get(url, {
77
+ params: {
78
+ target_id: uid
79
+ },
80
+ headers: {
81
+ Cookie: this.cookieString,
82
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
83
+ },
84
+ timeout: 10000
85
+ });
86
+ this.logger.debug('强制绑定', `B站API返回状态码: ${response.status}`);
87
+ if (this.config.debugMode) {
88
+ this.logger.debug('强制绑定', `B站API返回数据: ${JSON.stringify(response.data, null, 2)}`);
89
+ }
90
+ // 检查登录状态
91
+ if (response.data.code !== 0) {
92
+ if (response.data.message && response.data.message.includes('未登录')) {
93
+ this.logger.warn('强制绑定', 'B站API返回未登录错误,SESSDATA可能无效或已过期');
94
+ throw new Error('SESSDATA无效或已过期,无法获取粉丝勋章信息');
95
+ }
96
+ this.logger.warn('强制绑定', `B站API返回错误: ${response.data.message || '未知错误'}`);
97
+ throw new Error(`B站API错误: ${response.data.message || '未知错误'}`);
98
+ }
99
+ return response.data;
100
+ }
101
+ catch (error) {
102
+ this.logger.error('强制绑定', `获取B站粉丝勋章失败`, error);
103
+ throw error;
104
+ }
105
+ }
106
+ /**
107
+ * 获取用户的基本信息(使用ZMINFO API)
108
+ */
109
+ async getZminfoUserInfo(uid) {
110
+ this.logger.debug('强制绑定', `开始获取用户 ${uid} 的ZMINFO信息`);
111
+ try {
112
+ const response = await axios_1.default.get(`${this.config.zminfoApiUrl}/api/user/${uid}`, {
113
+ timeout: 10000,
114
+ headers: {
115
+ 'User-Agent': 'Koishi-MCID-Bot/1.0'
116
+ }
117
+ });
118
+ if (response.data.success && response.data.data && response.data.data.user) {
119
+ this.logger.debug('强制绑定', `ZMINFO API 用户信息获取成功: ${response.data.data.user.username}`);
120
+ return response.data.data.user;
121
+ }
122
+ else {
123
+ this.logger.warn('强制绑定', `ZMINFO API 返回失败: ${response.data.message}`);
124
+ return null;
125
+ }
126
+ }
127
+ catch (error) {
128
+ this.logger.error('强制绑定', `获取ZMINFO用户信息失败`, error);
129
+ throw new Error(`无法获取用户信息: ${error.message}`);
130
+ }
131
+ }
132
+ /**
133
+ * 检查是否拥有目标粉丝牌
134
+ */
135
+ checkTargetMedal(medalList) {
136
+ // 查找目标UP主的粉丝牌
137
+ const targetMedal = medalList.find(item => item.medal_info.target_id === this.config.targetUpUid &&
138
+ item.medal_info.medal_name === this.config.targetMedalName);
139
+ if (targetMedal) {
140
+ this.logger.info('强制绑定', `找到目标粉丝牌: ${targetMedal.medal_info.medal_name} LV.${targetMedal.medal_info.level}`);
141
+ return {
142
+ found: true,
143
+ name: targetMedal.medal_info.medal_name,
144
+ level: targetMedal.medal_info.level,
145
+ guard_level: targetMedal.medal_info.guard_level,
146
+ wearing_status: targetMedal.medal_info.wearing_status
147
+ };
148
+ }
149
+ this.logger.debug('强制绑定', `未找到目标粉丝牌 ${this.config.targetMedalName}(UP主UID: ${this.config.targetUpUid})`);
150
+ return { found: false };
151
+ }
152
+ /**
153
+ * 强制绑定用户,获取完整信息包括目标粉丝牌数据
154
+ */
155
+ async forceBindUser(uid) {
156
+ this.logger.info('强制绑定', `开始强制绑定用户 ${uid}`);
157
+ try {
158
+ // 首先检查B站登录状态
159
+ const isLoggedIn = await this.checkBilibiliLoginStatus();
160
+ if (!isLoggedIn) {
161
+ throw new Error('B站登录状态异常,无法进行绑定,请检查Cookie配置');
162
+ }
163
+ this.logger.debug('强制绑定', 'B站登录状态正常,开始通过B站API获取用户信息');
164
+ // 强制绑定模式仅使用B站API,不再尝试ZMINFO(避免404错误)
165
+ const medalData = await this.getBilibiliMedals(uid);
166
+ // 验证B站API返回结果
167
+ if (medalData.code !== 0 || !medalData.data) {
168
+ const errorMsg = medalData.message || 'B站API未返回有效数据';
169
+ this.logger.error('强制绑定', `获取B站用户信息失败: ${errorMsg}`);
170
+ throw new Error(`无法获取用户 ${uid} 的信息: ${errorMsg}`);
171
+ }
172
+ // 从B站API构建用户信息
173
+ const userInfo = {
174
+ uid: uid,
175
+ username: medalData.data.name || `B站用户${uid}`,
176
+ avatar_url: medalData.data.icon || '',
177
+ guard_level: 0,
178
+ guard_level_text: '',
179
+ max_guard_level: 0,
180
+ max_guard_level_text: '',
181
+ medal: null,
182
+ wealthMedalLevel: 0,
183
+ last_active_time: new Date().toISOString()
184
+ };
185
+ this.logger.debug('强制绑定', `成功从B站API获取用户信息: ${userInfo.username}`);
186
+ // 处理粉丝勋章信息
187
+ let targetMedalInfo = { found: false };
188
+ let enhancedUserInfo = {
189
+ ...userInfo,
190
+ targetMedal: targetMedalInfo
191
+ };
192
+ // 检查目标粉丝牌
193
+ if (medalData.data && medalData.data.list) {
194
+ // 检查目标粉丝牌
195
+ targetMedalInfo = this.checkTargetMedal(medalData.data.list);
196
+ enhancedUserInfo.targetMedal = targetMedalInfo;
197
+ this.logger.debug('强制绑定', `已检查目标粉丝牌,找到: ${targetMedalInfo.found}`);
198
+ }
199
+ else {
200
+ this.logger.warn('强制绑定', `B站API未返回粉丝勋章列表数据`);
201
+ }
202
+ this.logger.info('强制绑定', `强制绑定完成: 用户=${enhancedUserInfo.username}(${uid}), 目标粉丝牌=${targetMedalInfo.found ? '已找到' : '未找到'}`);
203
+ return enhancedUserInfo;
204
+ }
205
+ catch (error) {
206
+ this.logger.error('强制绑定', `强制绑定过程出错`, error);
207
+ throw error; // 直接重抛原始错误,不添加前缀
208
+ }
209
+ }
210
+ /**
211
+ * 转换为标准的ZminfoUser格式(用于数据库存储)
212
+ */
213
+ convertToZminfoUser(enhancedUser) {
214
+ const { targetMedal, ...standardUser } = enhancedUser;
215
+ return standardUser;
216
+ }
217
+ /**
218
+ * 获取目标粉丝牌的详细信息(用于显示)
219
+ */
220
+ getTargetMedalDetails(enhancedUser) {
221
+ // 检查是否有目标粉丝牌信息(即是否尝试了B站API调用)
222
+ if (!enhancedUser.targetMedal) {
223
+ return `ℹ️ 未检查粉丝牌信息(B站登录状态异常,请检查SESSDATA配置)`;
224
+ }
225
+ if (!enhancedUser.targetMedal.found) {
226
+ return `未找到目标粉丝牌"${this.config.targetMedalName}"(UP主UID: ${this.config.targetUpUid})`;
227
+ }
228
+ const medal = enhancedUser.targetMedal;
229
+ let details = `🎯 目标粉丝牌: ${medal.name} LV.${medal.level}`;
230
+ if (medal.guard_level && medal.guard_level > 0) {
231
+ const guardText = medal.guard_level === 1 ? '总督' :
232
+ medal.guard_level === 2 ? '提督' :
233
+ medal.guard_level === 3 ? '舰长' : '未知';
234
+ details += ` (${guardText})`;
235
+ }
236
+ if (medal.wearing_status === 1) {
237
+ details += ' 【已佩戴】';
238
+ }
239
+ return details;
240
+ }
241
+ }
242
+ exports.ForceBinder = ForceBinder;