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/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 -1061
- package/lib/services/api.service.d.ts +70 -0
- package/lib/services/api.service.js +344 -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/lib/utils/helpers.d.ts +13 -0
- package/lib/utils/helpers.js +21 -0
- package/lib/utils/message-utils.d.ts +7 -2
- package/lib/utils/message-utils.js +117 -12
- 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
|
+
}
|
package/lib/utils/helpers.d.ts
CHANGED
|
@@ -107,3 +107,16 @@ export declare function normalizeUsername(username: string, logger?: Logger): st
|
|
|
107
107
|
* @returns 是否相同
|
|
108
108
|
*/
|
|
109
109
|
export declare function isSameUsername(username1: string, username2: string): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* 从群昵称中提取B站用户名
|
|
112
|
+
* 用于解析标准格式的群昵称:B站名称(ID:MC名称)
|
|
113
|
+
*
|
|
114
|
+
* @param nickname 群昵称字符串
|
|
115
|
+
* @returns B站用户名,如果格式不匹配返回 null
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* extractBuidUsernameFromNickname("张三(ID:Steve)") // => "张三"
|
|
119
|
+
* extractBuidUsernameFromNickname("李四(ID:未绑定)") // => "李四"
|
|
120
|
+
* extractBuidUsernameFromNickname("无效格式") // => null
|
|
121
|
+
*/
|
|
122
|
+
export declare function extractBuidUsernameFromNickname(nickname: string): string | null;
|
package/lib/utils/helpers.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.levenshteinDistance = levenshteinDistance;
|
|
|
13
13
|
exports.calculateSimilarity = calculateSimilarity;
|
|
14
14
|
exports.normalizeUsername = normalizeUsername;
|
|
15
15
|
exports.isSameUsername = isSameUsername;
|
|
16
|
+
exports.extractBuidUsernameFromNickname = extractBuidUsernameFromNickname;
|
|
16
17
|
/**
|
|
17
18
|
* 通用工具函数集合
|
|
18
19
|
*/
|
|
@@ -360,3 +361,23 @@ function isSameUsername(username1, username2) {
|
|
|
360
361
|
return false;
|
|
361
362
|
return normalizeUsername(username1) === normalizeUsername(username2);
|
|
362
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* 从群昵称中提取B站用户名
|
|
366
|
+
* 用于解析标准格式的群昵称:B站名称(ID:MC名称)
|
|
367
|
+
*
|
|
368
|
+
* @param nickname 群昵称字符串
|
|
369
|
+
* @returns B站用户名,如果格式不匹配返回 null
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* extractBuidUsernameFromNickname("张三(ID:Steve)") // => "张三"
|
|
373
|
+
* extractBuidUsernameFromNickname("李四(ID:未绑定)") // => "李四"
|
|
374
|
+
* extractBuidUsernameFromNickname("无效格式") // => null
|
|
375
|
+
*/
|
|
376
|
+
function extractBuidUsernameFromNickname(nickname) {
|
|
377
|
+
if (!nickname)
|
|
378
|
+
return null;
|
|
379
|
+
// 匹配格式:任意内容(ID:任意内容)
|
|
380
|
+
// 使用非贪婪匹配 .+? 确保正确提取第一个括号前的内容
|
|
381
|
+
const match = nickname.match(/^(.+?)(ID:.+)$/);
|
|
382
|
+
return match ? match[1].trim() : null;
|
|
383
|
+
}
|
|
@@ -18,13 +18,17 @@ export declare class MessageUtils {
|
|
|
18
18
|
private config;
|
|
19
19
|
private logger;
|
|
20
20
|
private getBindingSessionFn;
|
|
21
|
+
private validateBUID?;
|
|
22
|
+
private updateBuidInfoOnly?;
|
|
21
23
|
/**
|
|
22
24
|
* 创建消息工具实例
|
|
23
25
|
* @param config 消息工具配置
|
|
24
26
|
* @param logger 日志服务
|
|
25
27
|
* @param getBindingSessionFn 获取绑定会话的函数
|
|
28
|
+
* @param validateBUID 验证B站UID的函数(可选)
|
|
29
|
+
* @param updateBuidInfoOnly 更新B站信息的函数(可选)
|
|
26
30
|
*/
|
|
27
|
-
constructor(config: MessageUtilsConfig, logger: LoggerService, getBindingSessionFn: (userId: string, channelId: string) => BindingSession | null);
|
|
31
|
+
constructor(config: MessageUtilsConfig, logger: LoggerService, getBindingSessionFn: (userId: string, channelId: string) => BindingSession | null, validateBUID?: (uid: string) => Promise<any>, updateBuidInfoOnly?: (qqId: string, buidUser: any) => Promise<boolean>);
|
|
28
32
|
/**
|
|
29
33
|
* 发送消息并处理自动撤回
|
|
30
34
|
* @param session Koishi Session对象
|
|
@@ -39,8 +43,9 @@ export declare class MessageUtils {
|
|
|
39
43
|
* @param session Koishi Session对象
|
|
40
44
|
* @param mcUsername MC用户名
|
|
41
45
|
* @param buidUsername B站用户名
|
|
46
|
+
* @param buidUid B站UID(可选,用于实时验证)
|
|
42
47
|
* @param targetUserId 目标用户ID(可选,用于管理员为他人设置)
|
|
43
48
|
* @param specifiedGroupId 指定的群ID(可选,默认使用配置的群ID)
|
|
44
49
|
*/
|
|
45
|
-
autoSetGroupNickname(session: Session, mcUsername: string | null, buidUsername: string, targetUserId?: string, specifiedGroupId?: string): Promise<void>;
|
|
50
|
+
autoSetGroupNickname(session: Session, mcUsername: string | null, buidUsername: string, buidUid?: string, targetUserId?: string, specifiedGroupId?: string): Promise<void>;
|
|
46
51
|
}
|
|
@@ -11,16 +11,22 @@ class MessageUtils {
|
|
|
11
11
|
config;
|
|
12
12
|
logger;
|
|
13
13
|
getBindingSessionFn;
|
|
14
|
+
validateBUID;
|
|
15
|
+
updateBuidInfoOnly;
|
|
14
16
|
/**
|
|
15
17
|
* 创建消息工具实例
|
|
16
18
|
* @param config 消息工具配置
|
|
17
19
|
* @param logger 日志服务
|
|
18
20
|
* @param getBindingSessionFn 获取绑定会话的函数
|
|
21
|
+
* @param validateBUID 验证B站UID的函数(可选)
|
|
22
|
+
* @param updateBuidInfoOnly 更新B站信息的函数(可选)
|
|
19
23
|
*/
|
|
20
|
-
constructor(config, logger, getBindingSessionFn) {
|
|
24
|
+
constructor(config, logger, getBindingSessionFn, validateBUID, updateBuidInfoOnly) {
|
|
21
25
|
this.config = config;
|
|
22
26
|
this.logger = logger;
|
|
23
27
|
this.getBindingSessionFn = getBindingSessionFn;
|
|
28
|
+
this.validateBUID = validateBUID;
|
|
29
|
+
this.updateBuidInfoOnly = updateBuidInfoOnly;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* 发送消息并处理自动撤回
|
|
@@ -135,17 +141,19 @@ class MessageUtils {
|
|
|
135
141
|
* @param session Koishi Session对象
|
|
136
142
|
* @param mcUsername MC用户名
|
|
137
143
|
* @param buidUsername B站用户名
|
|
144
|
+
* @param buidUid B站UID(可选,用于实时验证)
|
|
138
145
|
* @param targetUserId 目标用户ID(可选,用于管理员为他人设置)
|
|
139
146
|
* @param specifiedGroupId 指定的群ID(可选,默认使用配置的群ID)
|
|
140
147
|
*/
|
|
141
|
-
async autoSetGroupNickname(session, mcUsername, buidUsername, targetUserId, specifiedGroupId) {
|
|
148
|
+
async autoSetGroupNickname(session, mcUsername, buidUsername, buidUid, targetUserId, specifiedGroupId) {
|
|
142
149
|
try {
|
|
143
150
|
// 如果指定了目标用户ID,使用目标用户ID,否则使用session的用户ID
|
|
144
151
|
const actualUserId = targetUserId || session.userId;
|
|
145
152
|
const normalizedUserId = (0, helpers_1.normalizeQQId)(actualUserId);
|
|
146
153
|
// 根据MC绑定状态设置不同的格式(临时用户名视为未绑定)
|
|
147
154
|
const mcInfo = (mcUsername && !mcUsername.startsWith('_temp_')) ? mcUsername : "未绑定";
|
|
148
|
-
|
|
155
|
+
let currentBuidUsername = buidUsername;
|
|
156
|
+
const newNickname = `${currentBuidUsername}(ID:${mcInfo})`;
|
|
149
157
|
// 使用指定的群ID,如果没有指定则使用配置的默认群ID
|
|
150
158
|
const targetGroupId = specifiedGroupId || this.config.autoNicknameGroupId;
|
|
151
159
|
this.logger.debug('群昵称设置', `开始处理QQ(${normalizedUserId})的群昵称设置,目标群: ${targetGroupId}`);
|
|
@@ -164,20 +172,82 @@ class MessageUtils {
|
|
|
164
172
|
this.logger.info('群昵称设置', `QQ(${normalizedUserId})群昵称已经是"${newNickname}",跳过修改`);
|
|
165
173
|
return;
|
|
166
174
|
}
|
|
175
|
+
// 昵称不一致,先尝试获取最新的B站用户信息
|
|
176
|
+
if (buidUid && this.validateBUID && this.updateBuidInfoOnly) {
|
|
177
|
+
this.logger.debug('群昵称设置', `检测到昵称不一致,尝试获取B站UID ${buidUid} 的最新信息...`);
|
|
178
|
+
// 1. 提取当前群昵称中的B站用户名
|
|
179
|
+
const currentNicknameUsername = (0, helpers_1.extractBuidUsernameFromNickname)(currentNickname);
|
|
180
|
+
this.logger.debug('群昵称设置', `从当前群昵称"${currentNickname}"中提取到B站名称: ${currentNicknameUsername || '(无法提取)'}`);
|
|
181
|
+
try {
|
|
182
|
+
const latestBuidUser = await this.validateBUID(buidUid);
|
|
183
|
+
if (latestBuidUser && latestBuidUser.username) {
|
|
184
|
+
this.logger.debug('群昵称设置', `API返回B站用户名: "${latestBuidUser.username}"`);
|
|
185
|
+
// 2. 智能三层判断
|
|
186
|
+
// 情况A: API返回 == 当前群昵称中的名称
|
|
187
|
+
if (currentNicknameUsername && latestBuidUser.username === currentNicknameUsername) {
|
|
188
|
+
this.logger.info('群昵称设置', `✅ 当前群昵称"${currentNickname}"已包含正确的B站名称"${currentNicknameUsername}"`);
|
|
189
|
+
// 群昵称已经正确,仅需更新数据库(如果数据库是旧的)
|
|
190
|
+
if (latestBuidUser.username !== buidUsername) {
|
|
191
|
+
this.logger.info('群昵称设置', `数据库中的B站名称需要更新: "${buidUsername}" → "${latestBuidUser.username}"`);
|
|
192
|
+
try {
|
|
193
|
+
await this.updateBuidInfoOnly(normalizedUserId, latestBuidUser);
|
|
194
|
+
this.logger.info('群昵称设置', `已更新数据库中的B站用户名`);
|
|
195
|
+
}
|
|
196
|
+
catch (updateError) {
|
|
197
|
+
this.logger.warn('群昵称设置', `更新数据库失败: ${updateError.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
this.logger.debug('群昵称设置', `数据库中的B站名称已是最新,无需更新`);
|
|
202
|
+
}
|
|
203
|
+
// 跳过群昵称修改
|
|
204
|
+
this.logger.info('群昵称设置', `群昵称格式正确且名称最新,跳过修改`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// 情况B: API返回 == 数据库(都是旧的)
|
|
208
|
+
if (latestBuidUser.username === buidUsername) {
|
|
209
|
+
this.logger.warn('群昵称设置', `⚠️ API返回的B站名称"${latestBuidUser.username}"与数据库一致,但与群昵称不符`);
|
|
210
|
+
this.logger.warn('群昵称设置', `可能是API缓存未刷新,采用保守策略:不修改群昵称`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// 情况C: API返回新数据(!= 群昵称 且 != 数据库)
|
|
214
|
+
this.logger.info('群昵称设置', `检测到B站用户名已更新: "${buidUsername}" → "${latestBuidUser.username}"`);
|
|
215
|
+
currentBuidUsername = latestBuidUser.username;
|
|
216
|
+
// 更新数据库中的B站信息
|
|
217
|
+
try {
|
|
218
|
+
await this.updateBuidInfoOnly(normalizedUserId, latestBuidUser);
|
|
219
|
+
this.logger.info('群昵称设置', `已更新数据库中的B站用户名为: ${currentBuidUsername}`);
|
|
220
|
+
}
|
|
221
|
+
catch (updateError) {
|
|
222
|
+
this.logger.warn('群昵称设置', `更新数据库中的B站用户名失败: ${updateError.message}`);
|
|
223
|
+
// 即使更新数据库失败,也继续使用最新的用户名设置昵称
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.logger.warn('群昵称设置', `获取最新B站用户信息失败,使用数据库中的用户名: ${currentBuidUsername}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (validateError) {
|
|
231
|
+
this.logger.warn('群昵称设置', `获取最新B站用户信息时出错: ${validateError.message},使用数据库中的用户名: ${currentBuidUsername}`);
|
|
232
|
+
// API调用失败时降级处理,使用数据库中的旧名称
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// 使用(可能已更新的)用户名重新生成昵称
|
|
236
|
+
const finalNickname = `${currentBuidUsername}(ID:${mcInfo})`;
|
|
167
237
|
// 昵称不一致,执行修改
|
|
168
|
-
this.logger.debug('群昵称设置',
|
|
169
|
-
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId,
|
|
170
|
-
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${
|
|
238
|
+
this.logger.debug('群昵称设置', `昵称不一致,正在修改群昵称为: "${finalNickname}"`);
|
|
239
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, finalNickname);
|
|
240
|
+
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${finalNickname}"`, true);
|
|
171
241
|
// 验证设置是否生效 - 再次获取群昵称确认
|
|
172
242
|
try {
|
|
173
243
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
174
244
|
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
175
245
|
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
176
|
-
if (verifyNickname ===
|
|
246
|
+
if (verifyNickname === finalNickname) {
|
|
177
247
|
this.logger.info('群昵称设置', `✅ 验证成功,群昵称已生效: "${verifyNickname}"`, true);
|
|
178
248
|
}
|
|
179
249
|
else {
|
|
180
|
-
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${
|
|
250
|
+
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${finalNickname}",实际"${verifyNickname}",可能是权限不足或API延迟`);
|
|
181
251
|
}
|
|
182
252
|
}
|
|
183
253
|
catch (verifyError) {
|
|
@@ -189,19 +259,54 @@ class MessageUtils {
|
|
|
189
259
|
this.logger.warn('群昵称设置', `获取QQ(${normalizedUserId})当前群昵称失败: ${getInfoError.message}`);
|
|
190
260
|
this.logger.warn('群昵称设置', `错误详情: ${JSON.stringify(getInfoError)}`);
|
|
191
261
|
this.logger.debug('群昵称设置', `将直接尝试设置新昵称...`);
|
|
262
|
+
// 如果传入了 buidUid,尝试获取最新的B站用户信息
|
|
263
|
+
if (buidUid && this.validateBUID && this.updateBuidInfoOnly) {
|
|
264
|
+
this.logger.debug('群昵称设置', `尝试获取B站UID ${buidUid} 的最新信息...`);
|
|
265
|
+
try {
|
|
266
|
+
const latestBuidUser = await this.validateBUID(buidUid);
|
|
267
|
+
if (latestBuidUser && latestBuidUser.username) {
|
|
268
|
+
this.logger.debug('群昵称设置', `API返回B站用户名: "${latestBuidUser.username}"`);
|
|
269
|
+
// 智能判断:API返回 == 数据库(都是旧的)
|
|
270
|
+
if (latestBuidUser.username === buidUsername) {
|
|
271
|
+
this.logger.warn('群昵称设置', `⚠️ API返回的B站名称"${latestBuidUser.username}"与数据库一致`);
|
|
272
|
+
this.logger.warn('群昵称设置', `可能是API缓存未刷新,且无法获取当前群昵称,采用保守策略:跳过修改`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// API返回新数据(!= 数据库)
|
|
276
|
+
this.logger.info('群昵称设置', `检测到B站用户名已更新: "${buidUsername}" → "${latestBuidUser.username}"`);
|
|
277
|
+
currentBuidUsername = latestBuidUser.username;
|
|
278
|
+
// 更新数据库
|
|
279
|
+
try {
|
|
280
|
+
await this.updateBuidInfoOnly(normalizedUserId, latestBuidUser);
|
|
281
|
+
this.logger.info('群昵称设置', `已更新数据库中的B站用户名为: ${currentBuidUsername}`);
|
|
282
|
+
}
|
|
283
|
+
catch (updateError) {
|
|
284
|
+
this.logger.warn('群昵称设置', `更新数据库失败: ${updateError.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
this.logger.warn('群昵称设置', `获取最新B站用户信息失败`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (validateError) {
|
|
292
|
+
this.logger.warn('群昵称设置', `获取最新B站用户信息失败: ${validateError.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
192
295
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
296
|
+
// 使用(可能已更新的)用户名生成昵称
|
|
297
|
+
const nicknameToSet = `${currentBuidUsername}(ID:${mcInfo})`;
|
|
298
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, nicknameToSet);
|
|
299
|
+
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称设置为: ${nicknameToSet}`, true);
|
|
195
300
|
// 验证设置是否生效
|
|
196
301
|
try {
|
|
197
302
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
198
303
|
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
199
304
|
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
200
|
-
if (verifyNickname ===
|
|
305
|
+
if (verifyNickname === nicknameToSet) {
|
|
201
306
|
this.logger.info('群昵称设置', `✅ 验证成功,群昵称已生效: "${verifyNickname}"`, true);
|
|
202
307
|
}
|
|
203
308
|
else {
|
|
204
|
-
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${
|
|
309
|
+
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${nicknameToSet}",实际"${verifyNickname}",可能是权限不足`);
|
|
205
310
|
this.logger.warn('群昵称设置', `建议检查: 1.机器人是否为群管理员 2.群设置是否允许管理员修改昵称 3.OneBot实现是否支持该功能`);
|
|
206
311
|
}
|
|
207
312
|
}
|