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.
@@ -0,0 +1,70 @@
1
+ import { LoggerService } from '../utils/logger';
2
+ import type { MojangProfile, ZminfoUser } from '../types';
3
+ /**
4
+ * API 服务层
5
+ * 统一管理外部 API 调用(Mojang API、ZMINFO API 等)
6
+ */
7
+ export declare class ApiService {
8
+ private logger;
9
+ private config;
10
+ constructor(logger: LoggerService, config: {
11
+ zminfoApiUrl: string;
12
+ });
13
+ /**
14
+ * 验证 Minecraft 用户名是否存在
15
+ * @param username MC 用户名
16
+ * @returns Mojang Profile 或 null
17
+ */
18
+ validateUsername(username: string): Promise<MojangProfile | null>;
19
+ /**
20
+ * 使用备用 API 验证用户名
21
+ * @param username MC 用户名
22
+ * @returns Mojang Profile 或 null
23
+ */
24
+ private tryBackupAPI;
25
+ /**
26
+ * 通过 UUID 查询用户名
27
+ * @param uuid MC UUID
28
+ * @returns 用户名或 null
29
+ */
30
+ getUsernameByUuid(uuid: string): Promise<string | null>;
31
+ /**
32
+ * 使用备用 API 通过 UUID 查询用户名
33
+ * @param uuid MC UUID
34
+ * @returns 用户名或 null
35
+ */
36
+ private getUsernameByUuidBackupAPI;
37
+ /**
38
+ * 通过B站官方API获取用户基本信息(最权威的数据源)
39
+ * @param uid B站UID
40
+ * @returns 用户基本信息或null
41
+ */
42
+ getBilibiliOfficialUserInfo(uid: string): Promise<{
43
+ name: string;
44
+ mid: number;
45
+ } | null>;
46
+ /**
47
+ * 验证 B 站 UID 是否存在
48
+ * @param buid B站UID
49
+ * @returns ZminfoUser 或 null
50
+ */
51
+ validateBUID(buid: string): Promise<ZminfoUser | null>;
52
+ /**
53
+ * 获取 MC 头图 URL (Crafatar)
54
+ * @param uuid MC UUID
55
+ * @returns 头图 URL 或 null
56
+ */
57
+ getCrafatarUrl(uuid: string): string | null;
58
+ /**
59
+ * 使用 Starlight SkinAPI 获取皮肤渲染
60
+ * @param username MC 用户名
61
+ * @returns 皮肤渲染 URL 或 null
62
+ */
63
+ getStarlightSkinUrl(username: string): string | null;
64
+ /**
65
+ * 格式化 UUID (添加连字符,使其符合标准格式)
66
+ * @param uuid 原始 UUID
67
+ * @returns 格式化后的 UUID
68
+ */
69
+ formatUuid(uuid: string): string;
70
+ }
@@ -0,0 +1,344 @@
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.ApiService = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ /**
9
+ * API 服务层
10
+ * 统一管理外部 API 调用(Mojang API、ZMINFO API 等)
11
+ */
12
+ class ApiService {
13
+ logger;
14
+ config;
15
+ constructor(logger, config) {
16
+ this.logger = logger;
17
+ this.config = config;
18
+ }
19
+ // =========== Mojang API ===========
20
+ /**
21
+ * 验证 Minecraft 用户名是否存在
22
+ * @param username MC 用户名
23
+ * @returns Mojang Profile 或 null
24
+ */
25
+ async validateUsername(username) {
26
+ try {
27
+ this.logger.debug('Mojang API', `开始验证用户名: ${username}`);
28
+ const response = await axios_1.default.get(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
29
+ timeout: 10000, // 添加10秒超时
30
+ headers: {
31
+ 'User-Agent': 'KoishiMCVerifier/1.0', // 添加User-Agent头
32
+ }
33
+ });
34
+ if (response.status === 200 && response.data) {
35
+ this.logger.debug('Mojang API', `用户名"${username}"验证成功,UUID: ${response.data.id},标准名称: ${response.data.name}`);
36
+ return {
37
+ id: response.data.id,
38
+ name: response.data.name // 使用Mojang返回的正确大小写
39
+ };
40
+ }
41
+ return null;
42
+ }
43
+ catch (error) {
44
+ if (axios_1.default.isAxiosError(error) && error.response?.status === 404) {
45
+ this.logger.warn('Mojang API', `用户名"${username}"不存在`);
46
+ }
47
+ else if (axios_1.default.isAxiosError(error) && error.code === 'ECONNABORTED') {
48
+ this.logger.error('Mojang API', `验证用户名"${username}"时请求超时: ${error.message}`);
49
+ }
50
+ else {
51
+ // 记录更详细的错误信息
52
+ const errorMessage = axios_1.default.isAxiosError(error)
53
+ ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
54
+ : error.message || '未知错误';
55
+ this.logger.error('Mojang API', `验证用户名"${username}"时发生错误: ${errorMessage}`);
56
+ // 如果是网络相关错误,尝试使用备用API检查
57
+ if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
58
+ error.code === 'ETIMEDOUT' ||
59
+ error.code === 'ECONNRESET' ||
60
+ error.code === 'ECONNREFUSED' ||
61
+ error.code === 'ECONNABORTED' ||
62
+ error.response?.status === 429 || // 添加429 (Too Many Requests)
63
+ error.response?.status === 403)) { // 添加403 (Forbidden)
64
+ // 尝试使用playerdb.co作为备用API
65
+ this.logger.info('Mojang API', `遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
66
+ return this.tryBackupAPI(username);
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * 使用备用 API 验证用户名
74
+ * @param username MC 用户名
75
+ * @returns Mojang Profile 或 null
76
+ */
77
+ async tryBackupAPI(username) {
78
+ this.logger.info('备用API', `尝试使用备用API验证用户名"${username}"`);
79
+ try {
80
+ // 使用playerdb.co作为备用API
81
+ const backupResponse = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${username}`, {
82
+ timeout: 10000,
83
+ headers: {
84
+ 'User-Agent': 'KoishiMCVerifier/1.0'
85
+ }
86
+ });
87
+ if (backupResponse.status === 200 && backupResponse.data?.code === "player.found") {
88
+ const playerData = backupResponse.data.data.player;
89
+ const rawId = playerData.raw_id || playerData.id.replace(/-/g, ''); // 确保使用不带连字符的UUID
90
+ this.logger.info('备用API', `用户名"${username}"验证成功,UUID: ${rawId},标准名称: ${playerData.username}`);
91
+ return {
92
+ id: rawId, // 确保使用不带连字符的UUID
93
+ name: playerData.username
94
+ };
95
+ }
96
+ this.logger.warn('备用API', `用户名"${username}"验证失败: ${JSON.stringify(backupResponse.data)}`);
97
+ return null;
98
+ }
99
+ catch (backupError) {
100
+ const errorMsg = axios_1.default.isAxiosError(backupError)
101
+ ? `${backupError.message}, 状态码: ${backupError.response?.status || '未知'}`
102
+ : backupError.message || '未知错误';
103
+ this.logger.error('备用API', `验证用户名"${username}"失败: ${errorMsg}`);
104
+ return null;
105
+ }
106
+ }
107
+ /**
108
+ * 通过 UUID 查询用户名
109
+ * @param uuid MC UUID
110
+ * @returns 用户名或 null
111
+ */
112
+ async getUsernameByUuid(uuid) {
113
+ try {
114
+ // 确保UUID格式正确(去除连字符)
115
+ const cleanUuid = uuid.replace(/-/g, '');
116
+ this.logger.debug('Mojang API', `通过UUID "${cleanUuid}" 查询用户名`);
117
+ const response = await axios_1.default.get(`https://api.mojang.com/user/profile/${cleanUuid}`, {
118
+ timeout: 10000,
119
+ headers: {
120
+ 'User-Agent': 'KoishiMCVerifier/1.0',
121
+ }
122
+ });
123
+ if (response.status === 200 && response.data) {
124
+ // 从返回数据中提取用户名
125
+ const username = response.data.name;
126
+ this.logger.debug('Mojang API', `UUID "${cleanUuid}" 当前用户名: ${username}`);
127
+ return username;
128
+ }
129
+ this.logger.warn('Mojang API', `UUID "${cleanUuid}" 查询不到用户名`);
130
+ return null;
131
+ }
132
+ catch (error) {
133
+ // 如果是网络相关错误,尝试使用备用API
134
+ if (axios_1.default.isAxiosError(error) && (error.code === 'ENOTFOUND' ||
135
+ error.code === 'ETIMEDOUT' ||
136
+ error.code === 'ECONNRESET' ||
137
+ error.code === 'ECONNREFUSED' ||
138
+ error.code === 'ECONNABORTED' ||
139
+ error.response?.status === 429 || // 添加429 (Too Many Requests)
140
+ error.response?.status === 403)) { // 添加403 (Forbidden)
141
+ this.logger.info('Mojang API', `通过UUID查询用户名时遇到错误(${error.code || error.response?.status}),将尝试使用备用API`);
142
+ return this.getUsernameByUuidBackupAPI(uuid);
143
+ }
144
+ const errorMessage = axios_1.default.isAxiosError(error)
145
+ ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
146
+ : error.message || '未知错误';
147
+ this.logger.error('Mojang API', `通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
148
+ return null;
149
+ }
150
+ }
151
+ /**
152
+ * 使用备用 API 通过 UUID 查询用户名
153
+ * @param uuid MC UUID
154
+ * @returns 用户名或 null
155
+ */
156
+ async getUsernameByUuidBackupAPI(uuid) {
157
+ try {
158
+ // 确保UUID格式正确,备用API支持带连字符的UUID
159
+ const formattedUuid = uuid.includes('-') ? uuid : this.formatUuid(uuid);
160
+ this.logger.debug('备用API', `通过UUID "${formattedUuid}" 查询用户名`);
161
+ const response = await axios_1.default.get(`https://playerdb.co/api/player/minecraft/${formattedUuid}`, {
162
+ timeout: 10000,
163
+ headers: {
164
+ 'User-Agent': 'KoishiMCVerifier/1.0',
165
+ }
166
+ });
167
+ if (response.status === 200 && response.data?.code === "player.found") {
168
+ const playerData = response.data.data.player;
169
+ this.logger.debug('备用API', `UUID "${formattedUuid}" 当前用户名: ${playerData.username}`);
170
+ return playerData.username;
171
+ }
172
+ this.logger.warn('备用API', `UUID "${formattedUuid}" 查询不到用户名: ${JSON.stringify(response.data)}`);
173
+ return null;
174
+ }
175
+ catch (error) {
176
+ const errorMessage = axios_1.default.isAxiosError(error)
177
+ ? `${error.message},响应状态: ${error.response?.status || '未知'}\n响应数据: ${JSON.stringify(error.response?.data || '无数据')}`
178
+ : error.message || '未知错误';
179
+ this.logger.error('备用API', `通过UUID "${uuid}" 查询用户名失败: ${errorMessage}`);
180
+ return null;
181
+ }
182
+ }
183
+ // =========== B站官方API ===========
184
+ /**
185
+ * 通过B站官方API获取用户基本信息(最权威的数据源)
186
+ * @param uid B站UID
187
+ * @returns 用户基本信息或null
188
+ */
189
+ async getBilibiliOfficialUserInfo(uid) {
190
+ try {
191
+ if (!uid || !/^\d+$/.test(uid)) {
192
+ this.logger.warn('B站官方API', `无效的B站UID格式: ${uid}`);
193
+ return null;
194
+ }
195
+ this.logger.debug('B站官方API', `开始查询UID ${uid} 的官方信息`);
196
+ const response = await axios_1.default.get(`https://api.bilibili.com/x/space/acc/info`, {
197
+ params: { mid: uid },
198
+ timeout: 10000,
199
+ headers: {
200
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
201
+ }
202
+ });
203
+ if (response.data.code === 0 && response.data.data) {
204
+ const userData = response.data.data;
205
+ this.logger.debug('B站官方API', `UID ${uid} 的官方用户名: "${userData.name}"`);
206
+ return {
207
+ name: userData.name,
208
+ mid: userData.mid
209
+ };
210
+ }
211
+ else {
212
+ this.logger.warn('B站官方API', `UID ${uid} 查询失败: ${response.data.message || '未知错误'}`);
213
+ return null;
214
+ }
215
+ }
216
+ catch (error) {
217
+ if (axios_1.default.isAxiosError(error)) {
218
+ if (error.response?.status === 404) {
219
+ this.logger.warn('B站官方API', `UID ${uid} 不存在`);
220
+ return null;
221
+ }
222
+ this.logger.error('B站官方API', `查询UID ${uid} 时出错: ${error.message}`);
223
+ }
224
+ else {
225
+ this.logger.error('B站官方API', `查询UID ${uid} 时出错: ${error.message}`);
226
+ }
227
+ return null;
228
+ }
229
+ }
230
+ // =========== ZMINFO API ===========
231
+ /**
232
+ * 验证 B 站 UID 是否存在
233
+ * @param buid B站UID
234
+ * @returns ZminfoUser 或 null
235
+ */
236
+ async validateBUID(buid) {
237
+ try {
238
+ if (!buid || !/^\d+$/.test(buid)) {
239
+ this.logger.warn('B站账号验证', `无效的B站UID格式: ${buid}`);
240
+ return null;
241
+ }
242
+ this.logger.debug('B站账号验证', `验证B站UID: ${buid}`);
243
+ const response = await axios_1.default.get(`${this.config.zminfoApiUrl}/api/user/${buid}`, {
244
+ timeout: 10000,
245
+ headers: {
246
+ 'User-Agent': 'Koishi-MCID-Bot/1.0'
247
+ }
248
+ });
249
+ if (response.data.success && response.data.data && response.data.data.user) {
250
+ const user = response.data.data.user;
251
+ this.logger.debug('B站账号验证', `B站UID ${buid} 验证成功: ${user.username}`);
252
+ return user;
253
+ }
254
+ else {
255
+ this.logger.warn('B站账号验证', `B站UID ${buid} 不存在或API返回失败: ${response.data.message}`);
256
+ return null;
257
+ }
258
+ }
259
+ catch (error) {
260
+ if (error.response?.status === 404) {
261
+ this.logger.warn('B站账号验证', `B站UID ${buid} 不存在`);
262
+ return null;
263
+ }
264
+ this.logger.error('B站账号验证', `验证B站UID ${buid} 时出错: ${error.message}`);
265
+ throw new Error(`无法验证B站UID: ${error.message}`);
266
+ }
267
+ }
268
+ // =========== 工具方法 ===========
269
+ /**
270
+ * 获取 MC 头图 URL (Crafatar)
271
+ * @param uuid MC UUID
272
+ * @returns 头图 URL 或 null
273
+ */
274
+ getCrafatarUrl(uuid) {
275
+ if (!uuid)
276
+ return null;
277
+ // 检查UUID格式 (不带连字符应为32位,带连字符应为36位)
278
+ 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;
279
+ if (!uuidRegex.test(uuid)) {
280
+ this.logger.warn('MC头图', `UUID "${uuid}" 格式无效,无法生成头图URL`);
281
+ return null;
282
+ }
283
+ // 移除任何连字符,Crafatar接受不带连字符的UUID
284
+ const cleanUuid = uuid.replace(/-/g, '');
285
+ // 直接生成URL
286
+ const url = `https://crafatar.com/avatars/${cleanUuid}`;
287
+ this.logger.debug('MC头图', `为UUID "${cleanUuid}" 生成头图URL`);
288
+ return url;
289
+ }
290
+ /**
291
+ * 使用 Starlight SkinAPI 获取皮肤渲染
292
+ * @param username MC 用户名
293
+ * @returns 皮肤渲染 URL 或 null
294
+ */
295
+ getStarlightSkinUrl(username) {
296
+ if (!username)
297
+ return null;
298
+ // 可用的动作列表 (共16种)
299
+ const poses = [
300
+ 'default', // 默认站立
301
+ 'marching', // 行军
302
+ 'walking', // 行走
303
+ 'crouching', // 下蹲
304
+ 'crossed', // 交叉手臂
305
+ 'crisscross', // 交叉腿
306
+ 'cheering', // 欢呼
307
+ 'relaxing', // 放松
308
+ 'trudging', // 艰难行走
309
+ 'cowering', // 退缩
310
+ 'pointing', // 指向
311
+ 'lunging', // 前冲
312
+ 'dungeons', // 地下城风格
313
+ 'facepalm', // 捂脸
314
+ 'mojavatar', // Mojave姿态
315
+ 'head', // 头部特写
316
+ ];
317
+ // 随机选择一个动作
318
+ const randomPose = poses[Math.floor(Math.random() * poses.length)];
319
+ // 视图类型(full为全身图)
320
+ const viewType = 'full';
321
+ // 生成URL
322
+ const url = `https://starlightskins.lunareclipse.studio/render/${randomPose}/${username}/${viewType}`;
323
+ this.logger.debug('Starlight皮肤', `为用户名"${username}"生成动作"${randomPose}"的渲染URL`);
324
+ return url;
325
+ }
326
+ /**
327
+ * 格式化 UUID (添加连字符,使其符合标准格式)
328
+ * @param uuid 原始 UUID
329
+ * @returns 格式化后的 UUID
330
+ */
331
+ formatUuid(uuid) {
332
+ if (!uuid)
333
+ return '未知';
334
+ if (uuid.includes('-'))
335
+ return uuid; // 已经是带连字符的格式
336
+ // 确保UUID长度正确
337
+ if (uuid.length !== 32) {
338
+ this.logger.warn('UUID', `UUID "${uuid}" 长度异常,无法格式化`);
339
+ return uuid;
340
+ }
341
+ return `${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}`;
342
+ }
343
+ }
344
+ exports.ApiService = ApiService;
@@ -0,0 +1,64 @@
1
+ import { Context } from 'koishi';
2
+ import { LoggerService } from '../utils/logger';
3
+ import { MCIDBINDRepository } from '../repositories/mcidbind.repository';
4
+ import type { MCIDBIND, ZminfoUser } from '../types';
5
+ /**
6
+ * 数据库服务层
7
+ * 统一管理数据库操作,包括 MC 绑定和 BUID 绑定的 CRUD
8
+ */
9
+ export declare class DatabaseService {
10
+ private ctx;
11
+ private logger;
12
+ private mcidbindRepo;
13
+ private normalizeQQId;
14
+ private getUsernameByUuid;
15
+ constructor(ctx: Context, logger: LoggerService, mcidbindRepo: MCIDBINDRepository, normalizeQQId: (userId: string) => string, getUsernameByUuid: (uuid: string) => Promise<string | null>);
16
+ /**
17
+ * 根据 QQ 号查询 MC 绑定信息
18
+ */
19
+ getMcBindByQQId(qqId: string): Promise<MCIDBIND | null>;
20
+ /**
21
+ * 根据 MC 用户名查询绑定信息
22
+ */
23
+ getMcBindByUsername(mcUsername: string): Promise<MCIDBIND | null>;
24
+ /**
25
+ * 创建或更新 MC 绑定
26
+ */
27
+ createOrUpdateMcBind(userId: string, mcUsername: string, mcUuid: string, isAdmin?: boolean): Promise<boolean>;
28
+ /**
29
+ * 删除 MC 绑定(同时解绑 MC 和 B 站账号)
30
+ */
31
+ deleteMcBind(userId: string): Promise<boolean>;
32
+ /**
33
+ * 检查 MC 用户名是否已被其他 QQ 号绑定(支持不区分大小写和 UUID 检查)
34
+ */
35
+ checkUsernameExists(username: string, currentUserId?: string, uuid?: string): Promise<boolean>;
36
+ /**
37
+ * 根据 B 站 UID 查询绑定信息
38
+ */
39
+ getBuidBindByBuid(buid: string): Promise<MCIDBIND | null>;
40
+ /**
41
+ * 检查 B 站 UID 是否已被绑定
42
+ */
43
+ checkBuidExists(buid: string, currentUserId?: string): Promise<boolean>;
44
+ /**
45
+ * 创建或更新 B 站账号绑定
46
+ */
47
+ createOrUpdateBuidBind(userId: string, buidUser: ZminfoUser): Promise<boolean>;
48
+ /**
49
+ * 仅更新 B 站信息,不更新绑定时间(用于查询时刷新数据)
50
+ */
51
+ updateBuidInfoOnly(userId: string, buidUser: ZminfoUser): Promise<boolean>;
52
+ /**
53
+ * 检查并更新用户名(如果与当前数据库中的不同)
54
+ */
55
+ checkAndUpdateUsername(bind: MCIDBIND): Promise<MCIDBIND>;
56
+ /**
57
+ * 智能缓存版本的改名检测函数
58
+ * 特性:
59
+ * - 24小时冷却期(失败>=3次时延长到72小时)
60
+ * - 失败计数追踪
61
+ * - 成功时重置失败计数
62
+ */
63
+ checkAndUpdateUsernameWithCache(bind: MCIDBIND): Promise<MCIDBIND>;
64
+ }