koishi-plugin-bind-bot 2.0.5 → 2.1.1
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/handlers/base.handler.d.ts +12 -18
- package/lib/handlers/binding.handler.js +3 -3
- package/lib/handlers/buid.handler.js +4 -4
- package/lib/handlers/index.d.ts +1 -0
- package/lib/handlers/index.js +1 -0
- package/lib/handlers/lottery.handler.d.ts +42 -0
- package/lib/handlers/lottery.handler.js +225 -0
- package/lib/handlers/mcid.handler.js +56 -56
- package/lib/handlers/tag.handler.js +8 -8
- package/lib/handlers/whitelist.handler.js +14 -14
- package/lib/index.js +73 -1159
- package/lib/services/api.service.d.ts +70 -0
- package/lib/services/api.service.js +346 -0
- package/lib/services/database.service.d.ts +64 -0
- package/lib/services/database.service.js +451 -0
- package/lib/services/index.d.ts +6 -0
- package/lib/services/index.js +22 -0
- package/lib/services/nickname.service.d.ts +42 -0
- package/lib/services/nickname.service.js +225 -0
- package/lib/services/service-container.d.ts +17 -0
- package/lib/services/service-container.js +24 -0
- package/lib/types/config.d.ts +2 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +2 -0
- package/lib/types/update-data.d.ts +68 -0
- package/lib/types/update-data.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NicknameService = void 0;
|
|
4
|
+
const helpers_1 = require("../utils/helpers");
|
|
5
|
+
/**
|
|
6
|
+
* 群昵称管理服务
|
|
7
|
+
* 负责自动设置和验证群昵称
|
|
8
|
+
*/
|
|
9
|
+
class NicknameService {
|
|
10
|
+
logger;
|
|
11
|
+
config;
|
|
12
|
+
normalizeQQId;
|
|
13
|
+
validateBUID;
|
|
14
|
+
getBilibiliOfficialUserInfo;
|
|
15
|
+
updateBuidInfoOnly;
|
|
16
|
+
constructor(logger, config, normalizeQQId, validateBUID, getBilibiliOfficialUserInfo, updateBuidInfoOnly) {
|
|
17
|
+
this.logger = logger;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.normalizeQQId = normalizeQQId;
|
|
20
|
+
this.validateBUID = validateBUID;
|
|
21
|
+
this.getBilibiliOfficialUserInfo = getBilibiliOfficialUserInfo;
|
|
22
|
+
this.updateBuidInfoOnly = updateBuidInfoOnly;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 检查群昵称格式是否正确
|
|
26
|
+
*/
|
|
27
|
+
checkNicknameFormat(nickname, buidUsername, mcUsername) {
|
|
28
|
+
if (!nickname || !buidUsername)
|
|
29
|
+
return false;
|
|
30
|
+
// 期望格式:B站名称(ID:MC用户名)或 B站名称(ID:未绑定)
|
|
31
|
+
const mcInfo = mcUsername && !mcUsername.startsWith('_temp_') ? mcUsername : "未绑定";
|
|
32
|
+
const expectedFormat = `${buidUsername}(ID:${mcInfo})`;
|
|
33
|
+
return nickname === expectedFormat;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 使用四层判断逻辑获取最准确的B站用户名
|
|
37
|
+
* 优先级:官方API > ZMINFO > 数据库
|
|
38
|
+
*/
|
|
39
|
+
async getLatestBuidUsername(buidUid, currentDbUsername) {
|
|
40
|
+
// 1. 尝试获取B站官方API的用户信息(最权威)
|
|
41
|
+
let officialUsername = null;
|
|
42
|
+
try {
|
|
43
|
+
this.logger.debug('群昵称设置', `正在查询B站官方API...`);
|
|
44
|
+
const officialInfo = await this.getBilibiliOfficialUserInfo(buidUid);
|
|
45
|
+
if (officialInfo && officialInfo.name) {
|
|
46
|
+
officialUsername = officialInfo.name;
|
|
47
|
+
this.logger.info('群昵称设置', `[层1-官方API] ✅ "${officialUsername}"`, true);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.logger.warn('群昵称设置', `[层1-官方API] ❌ 查询失败`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (officialError) {
|
|
54
|
+
this.logger.warn('群昵称设置', `[层1-官方API] ❌ 查询出错: ${officialError.message}`);
|
|
55
|
+
}
|
|
56
|
+
// 2. 尝试获取ZMINFO API的用户信息(可能有缓存)
|
|
57
|
+
let zminfoUserData = null;
|
|
58
|
+
try {
|
|
59
|
+
this.logger.debug('群昵称设置', `正在查询ZMINFO API...`);
|
|
60
|
+
zminfoUserData = await this.validateBUID(buidUid);
|
|
61
|
+
if (zminfoUserData && zminfoUserData.username) {
|
|
62
|
+
this.logger.debug('群昵称设置', `[层2-ZMINFO] "${zminfoUserData.username}"`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.logger.warn('群昵称设置', `[层2-ZMINFO] 查询失败`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (zminfoError) {
|
|
69
|
+
this.logger.warn('群昵称设置', `[层2-ZMINFO] 查询出错: ${zminfoError.message}`);
|
|
70
|
+
}
|
|
71
|
+
// 3. 根据优先级返回结果
|
|
72
|
+
if (officialUsername) {
|
|
73
|
+
this.logger.info('群昵称设置', `🎯 采用官方API结果: "${officialUsername}"`, true);
|
|
74
|
+
return {
|
|
75
|
+
username: officialUsername,
|
|
76
|
+
source: 'official',
|
|
77
|
+
zminfoData: zminfoUserData || undefined
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
else if (zminfoUserData && zminfoUserData.username) {
|
|
81
|
+
this.logger.info('群昵称设置', `⚠️ 官方API不可用,降级使用ZMINFO: "${zminfoUserData.username}"`, true);
|
|
82
|
+
return {
|
|
83
|
+
username: zminfoUserData.username,
|
|
84
|
+
source: 'zminfo',
|
|
85
|
+
zminfoData: zminfoUserData
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.logger.warn('群昵称设置', `⚠️ 官方API和ZMINFO都不可用,使用数据库名称: "${currentDbUsername}"`);
|
|
90
|
+
return {
|
|
91
|
+
username: currentDbUsername,
|
|
92
|
+
source: 'database'
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 同步数据库中的B站用户信息
|
|
98
|
+
*/
|
|
99
|
+
async syncDatabaseIfNeeded(normalizedUserId, latestUsername, currentDbUsername, zminfoData) {
|
|
100
|
+
if (latestUsername === currentDbUsername) {
|
|
101
|
+
return; // 无需更新
|
|
102
|
+
}
|
|
103
|
+
if (!zminfoData) {
|
|
104
|
+
this.logger.debug('群昵称设置', `无ZMINFO数据,跳过数据库同步`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const updatedData = { ...zminfoData, username: latestUsername };
|
|
109
|
+
await this.updateBuidInfoOnly(normalizedUserId, updatedData);
|
|
110
|
+
this.logger.info('群昵称设置', `已同步数据库: "${currentDbUsername}" → "${latestUsername}"`, true);
|
|
111
|
+
}
|
|
112
|
+
catch (updateError) {
|
|
113
|
+
this.logger.warn('群昵称设置', `数据库同步失败: ${updateError.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 设置群昵称并验证
|
|
118
|
+
*/
|
|
119
|
+
async setAndVerifyNickname(session, targetGroupId, normalizedUserId, nickname, currentNickname) {
|
|
120
|
+
try {
|
|
121
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, nickname);
|
|
122
|
+
if (currentNickname) {
|
|
123
|
+
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${nickname}"`, true);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称设置为: ${nickname}`, true);
|
|
127
|
+
}
|
|
128
|
+
// 验证设置是否生效
|
|
129
|
+
try {
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
131
|
+
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
132
|
+
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
133
|
+
if (verifyNickname === nickname) {
|
|
134
|
+
this.logger.info('群昵称设置', `✅ 验证成功,群昵称已生效: "${verifyNickname}"`, true);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${nickname}",实际"${verifyNickname}",可能是权限不足或API延迟`);
|
|
138
|
+
if (!currentNickname) {
|
|
139
|
+
this.logger.warn('群昵称设置', `建议检查: 1.机器人是否为群管理员 2.群设置是否允许管理员修改昵称 3.OneBot实现是否支持该功能`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (verifyError) {
|
|
144
|
+
this.logger.warn('群昵称设置', `无法验证群昵称设置结果: ${verifyError.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (setCardError) {
|
|
148
|
+
this.logger.error('群昵称设置', `设置群昵称失败: ${setCardError.message}`);
|
|
149
|
+
this.logger.error('群昵称设置', `错误详情: ${JSON.stringify(setCardError)}`);
|
|
150
|
+
throw setCardError;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 自动群昵称设置功能(重构版)
|
|
155
|
+
*/
|
|
156
|
+
async autoSetGroupNickname(session, mcUsername, buidUsername, buidUid, targetUserId, specifiedGroupId) {
|
|
157
|
+
try {
|
|
158
|
+
// 准备基本参数
|
|
159
|
+
const actualUserId = targetUserId || session.userId;
|
|
160
|
+
const normalizedUserId = this.normalizeQQId(actualUserId);
|
|
161
|
+
const targetGroupId = specifiedGroupId || this.config.autoNicknameGroupId;
|
|
162
|
+
const mcInfo = (mcUsername && !mcUsername.startsWith('_temp_')) ? mcUsername : "未绑定";
|
|
163
|
+
this.logger.debug('群昵称设置', `开始处理QQ(${normalizedUserId})的群昵称设置,目标群: ${targetGroupId}`);
|
|
164
|
+
// 检查前置条件
|
|
165
|
+
if (!session.bot.internal) {
|
|
166
|
+
this.logger.debug('群昵称设置', `QQ(${normalizedUserId})bot不支持OneBot内部API,跳过自动群昵称设置`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!targetGroupId) {
|
|
170
|
+
this.logger.debug('群昵称设置', `QQ(${normalizedUserId})未配置自动群昵称设置目标群,跳过群昵称设置`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// 获取最新的B站用户名
|
|
174
|
+
let latestBuidUsername = buidUsername;
|
|
175
|
+
if (buidUid) {
|
|
176
|
+
this.logger.debug('群昵称设置', `开始四层判断获取最新B站用户名...`);
|
|
177
|
+
this.logger.debug('群昵称设置', `[层3-数据库] "${buidUsername}"`);
|
|
178
|
+
const result = await this.getLatestBuidUsername(buidUid, buidUsername);
|
|
179
|
+
latestBuidUsername = result.username;
|
|
180
|
+
// 尝试同步数据库
|
|
181
|
+
await this.syncDatabaseIfNeeded(normalizedUserId, latestBuidUsername, buidUsername, result.zminfoData);
|
|
182
|
+
}
|
|
183
|
+
// 生成目标昵称
|
|
184
|
+
const targetNickname = `${latestBuidUsername}(ID:${mcInfo})`;
|
|
185
|
+
this.logger.debug('群昵称设置', `目标昵称: "${targetNickname}"`);
|
|
186
|
+
// 尝试获取当前昵称并比对
|
|
187
|
+
try {
|
|
188
|
+
this.logger.debug('群昵称设置', `正在获取QQ(${normalizedUserId})在群${targetGroupId}的当前昵称...`);
|
|
189
|
+
const currentGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
190
|
+
const currentNickname = currentGroupInfo.card || currentGroupInfo.nickname || '';
|
|
191
|
+
this.logger.debug('群昵称设置', `当前昵称: "${currentNickname}"`);
|
|
192
|
+
// 智能判断:如果当前昵称已包含最新用户名,跳过修改
|
|
193
|
+
if (buidUid && currentNickname) {
|
|
194
|
+
const currentNicknameUsername = (0, helpers_1.extractBuidUsernameFromNickname)(currentNickname);
|
|
195
|
+
this.logger.debug('群昵称设置', `[层4-群昵称] "${currentNicknameUsername || '(无法提取)'}"`);
|
|
196
|
+
if (currentNicknameUsername && currentNicknameUsername === latestBuidUsername) {
|
|
197
|
+
this.logger.info('群昵称设置', `✅ 群昵称已包含最新名称"${latestBuidUsername}",跳过修改`, true);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// 如果昵称完全一致,也跳过
|
|
202
|
+
if (currentNickname === targetNickname) {
|
|
203
|
+
this.logger.info('群昵称设置', `QQ(${normalizedUserId})群昵称已经是"${targetNickname}",跳过修改`, true);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// 昵称需要更新
|
|
207
|
+
this.logger.debug('群昵称设置', `昵称不一致,正在修改群昵称为: "${targetNickname}"`);
|
|
208
|
+
await this.setAndVerifyNickname(session, targetGroupId, normalizedUserId, targetNickname, currentNickname);
|
|
209
|
+
}
|
|
210
|
+
catch (getInfoError) {
|
|
211
|
+
// 无法获取当前昵称,直接设置新昵称
|
|
212
|
+
this.logger.warn('群昵称设置', `获取QQ(${normalizedUserId})当前群昵称失败: ${getInfoError.message}`);
|
|
213
|
+
this.logger.debug('群昵称设置', `将直接尝试设置新昵称...`);
|
|
214
|
+
await this.setAndVerifyNickname(session, targetGroupId, normalizedUserId, targetNickname);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const actualUserId = targetUserId || session.userId;
|
|
219
|
+
const normalizedUserId = this.normalizeQQId(actualUserId);
|
|
220
|
+
this.logger.error('群昵称设置', `QQ(${normalizedUserId})自动群昵称设置失败: ${error.message}`);
|
|
221
|
+
this.logger.error('群昵称设置', `完整错误信息: ${JSON.stringify(error)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.NicknameService = NicknameService;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { LoggerService } from '../utils/logger';
|
|
3
|
+
import { MCIDBINDRepository } from '../repositories/mcidbind.repository';
|
|
4
|
+
import { ApiService } from './api.service';
|
|
5
|
+
import { DatabaseService } from './database.service';
|
|
6
|
+
import { NicknameService } from './nickname.service';
|
|
7
|
+
import { Config } from '../types/config';
|
|
8
|
+
/**
|
|
9
|
+
* 服务容器类
|
|
10
|
+
* 统一管理所有服务的实例化,解决服务初始化分散的问题
|
|
11
|
+
*/
|
|
12
|
+
export declare class ServiceContainer {
|
|
13
|
+
readonly api: ApiService;
|
|
14
|
+
readonly database: DatabaseService;
|
|
15
|
+
readonly nickname: NicknameService;
|
|
16
|
+
constructor(ctx: Context, config: Config, logger: LoggerService, mcidbindRepo: MCIDBINDRepository, normalizeQQId: (userId: string) => string);
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ServiceContainer = void 0;
|
|
4
|
+
const api_service_1 = require("./api.service");
|
|
5
|
+
const database_service_1 = require("./database.service");
|
|
6
|
+
const nickname_service_1 = require("./nickname.service");
|
|
7
|
+
/**
|
|
8
|
+
* 服务容器类
|
|
9
|
+
* 统一管理所有服务的实例化,解决服务初始化分散的问题
|
|
10
|
+
*/
|
|
11
|
+
class ServiceContainer {
|
|
12
|
+
api;
|
|
13
|
+
database;
|
|
14
|
+
nickname;
|
|
15
|
+
constructor(ctx, config, logger, mcidbindRepo, normalizeQQId) {
|
|
16
|
+
// 1. 实例化 API 服务(无依赖)
|
|
17
|
+
this.api = new api_service_1.ApiService(logger.createChild('API服务'), { zminfoApiUrl: config.zminfoApiUrl });
|
|
18
|
+
// 2. 实例化数据库服务(依赖 API 服务)
|
|
19
|
+
this.database = new database_service_1.DatabaseService(ctx, logger.createChild('数据库服务'), mcidbindRepo, normalizeQQId, (uuid) => this.api.getUsernameByUuid(uuid));
|
|
20
|
+
// 3. 实例化群昵称服务(依赖 API 和数据库服务)
|
|
21
|
+
this.nickname = new nickname_service_1.NicknameService(logger.createChild('群昵称服务'), { autoNicknameGroupId: config.autoNicknameGroupId }, normalizeQQId, (buid) => this.api.validateBUID(buid), (uid) => this.api.getBilibiliOfficialUserInfo(uid), (userId, buidUser) => this.database.updateBuidInfoOnly(userId, buidUser));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ServiceContainer = ServiceContainer;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface Config {
|
|
|
18
18
|
showMcSkin: boolean;
|
|
19
19
|
zminfoApiUrl: string;
|
|
20
20
|
enableLotteryBroadcast: boolean;
|
|
21
|
+
lotteryTargetGroupId: string;
|
|
22
|
+
lotteryTargetPrivateId: string;
|
|
21
23
|
autoNicknameGroupId: string;
|
|
22
24
|
forceBindSessdata: string;
|
|
23
25
|
forceBindTargetUpUid: number;
|
package/lib/types/index.d.ts
CHANGED
package/lib/types/index.js
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库更新数据类型定义
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* MC绑定更新数据接口
|
|
6
|
+
*/
|
|
7
|
+
export interface UpdateMcBindData {
|
|
8
|
+
mcUsername?: string;
|
|
9
|
+
mcUuid?: string;
|
|
10
|
+
lastModified?: Date;
|
|
11
|
+
isAdmin?: boolean;
|
|
12
|
+
usernameLastChecked?: Date;
|
|
13
|
+
usernameCheckFailCount?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* BUID绑定更新数据接口(完整更新)
|
|
17
|
+
*/
|
|
18
|
+
export interface UpdateBuidBindData {
|
|
19
|
+
buidUid?: string;
|
|
20
|
+
buidUsername?: string;
|
|
21
|
+
guardLevel?: number;
|
|
22
|
+
guardLevelText?: string;
|
|
23
|
+
maxGuardLevel?: number;
|
|
24
|
+
maxGuardLevelText?: string;
|
|
25
|
+
medalName?: string;
|
|
26
|
+
medalLevel?: number;
|
|
27
|
+
wealthMedalLevel?: number;
|
|
28
|
+
lastActiveTime?: Date;
|
|
29
|
+
lastModified?: Date;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* BUID信息更新数据接口(仅更新信息,不更新lastModified)
|
|
33
|
+
*/
|
|
34
|
+
export interface UpdateBuidInfoData {
|
|
35
|
+
buidUsername?: string;
|
|
36
|
+
guardLevel?: number;
|
|
37
|
+
guardLevelText?: string;
|
|
38
|
+
maxGuardLevel?: number;
|
|
39
|
+
maxGuardLevelText?: string;
|
|
40
|
+
medalName?: string;
|
|
41
|
+
medalLevel?: number;
|
|
42
|
+
wealthMedalLevel?: number;
|
|
43
|
+
lastActiveTime?: Date;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 创建新绑定时的完整数据接口
|
|
47
|
+
*/
|
|
48
|
+
export interface CreateBindData {
|
|
49
|
+
qqId: string;
|
|
50
|
+
mcUsername: string;
|
|
51
|
+
mcUuid: string;
|
|
52
|
+
isAdmin: boolean;
|
|
53
|
+
whitelist: string[];
|
|
54
|
+
tags: string[];
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 标签更新数据接口
|
|
59
|
+
*/
|
|
60
|
+
export interface UpdateTagsData {
|
|
61
|
+
tags?: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 白名单更新数据接口
|
|
65
|
+
*/
|
|
66
|
+
export interface UpdateWhitelistData {
|
|
67
|
+
whitelist?: string[];
|
|
68
|
+
}
|