koishi-plugin-bind-bot 2.2.7 → 2.2.9

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.
@@ -8,6 +8,34 @@ import { LoggerService } from '../utils/logger';
8
8
  export declare class BindingHandler extends BaseHandler {
9
9
  private readonly BINDING_SESSION_TIMEOUT;
10
10
  constructor(ctx: Context, config: any, logger: LoggerService, repos: Repositories, deps: HandlerDependencies);
11
+ /**
12
+ * 格式化绑定信息
13
+ * @param bind 绑定记录
14
+ * @param prefix 前缀文本
15
+ * @returns 格式化后的绑定信息
16
+ */
17
+ private formatBindInfo;
18
+ /**
19
+ * 处理已完成全部绑定的场景
20
+ */
21
+ private handleCompletedBinds;
22
+ /**
23
+ * 处理只绑定MC未绑定B站的场景
24
+ */
25
+ private handleMcOnlyBind;
26
+ /**
27
+ * 处理只绑定B站未绑定MC的场景
28
+ */
29
+ private handleBuidOnlyBind;
30
+ /**
31
+ * 处理都未绑定的场景
32
+ */
33
+ private handleNoBind;
34
+ /**
35
+ * 处理绑定流程
36
+ * @param ctx 绑定流程上下文
37
+ */
38
+ private handleBindingFlow;
11
39
  /**
12
40
  * 注册交互式绑定命令
13
41
  */
@@ -15,6 +15,156 @@ class BindingHandler extends base_handler_1.BaseHandler {
15
15
  // 从配置中获取会话超时时间,默认3分钟
16
16
  this.BINDING_SESSION_TIMEOUT = 3 * 60 * 1000;
17
17
  }
18
+ /**
19
+ * 格式化绑定信息
20
+ * @param bind 绑定记录
21
+ * @param prefix 前缀文本
22
+ * @returns 格式化后的绑定信息
23
+ */
24
+ formatBindInfo(bind, prefix = '') {
25
+ const displayUsername = bind_status_1.BindStatus.getDisplayMcUsername(bind, '未绑定');
26
+ let bindInfo = `${prefix}\n✅ MC账号: ${displayUsername}\n✅ B站账号: ${bind.buidUsername} (UID: ${bind.buidUid})`;
27
+ if (bind.guardLevel > 0) {
28
+ bindInfo += `\n舰长等级: ${bind.guardLevelText}`;
29
+ }
30
+ if (bind.medalName) {
31
+ bindInfo += `\n粉丝牌: ${bind.medalName} Lv.${bind.medalLevel}`;
32
+ }
33
+ return bindInfo;
34
+ }
35
+ /**
36
+ * 处理已完成全部绑定的场景
37
+ */
38
+ async handleCompletedBinds(ctx, bind) {
39
+ const { session, userId, isAdminAction } = ctx;
40
+ this.logger.info('交互绑定', `QQ(${userId})已完成全部绑定`, true);
41
+ const prefix = isAdminAction
42
+ ? `用户 ${userId} 已完成全部账号绑定:`
43
+ : '您已完成全部账号绑定:';
44
+ let bindInfo = this.formatBindInfo(bind, prefix);
45
+ // 用户自己绑定时显示修改提示
46
+ if (!isAdminAction) {
47
+ bindInfo += `\n\n如需修改绑定信息,请使用:\n- ${this.deps.formatCommand('mcid change <新用户名>')} 修改MC账号\n- ${this.deps.formatCommand('buid bind <新UID>')} 修改B站账号`;
48
+ }
49
+ await this.deps.sendMessage(session, [koishi_1.h.text(bindInfo)]);
50
+ }
51
+ /**
52
+ * 处理只绑定MC未绑定B站的场景
53
+ */
54
+ async handleMcOnlyBind(ctx, bind) {
55
+ const { session, userId, rawUserId, channelId, isAdminAction } = ctx;
56
+ this.logger.info('交互绑定', `QQ(${userId})已绑定MC,进入B站绑定流程`, true);
57
+ const displayUsername = bind_status_1.BindStatus.getDisplayMcUsername(bind, '未绑定');
58
+ if (isAdminAction) {
59
+ // 管理员操作:更新会话状态并@目标用户
60
+ this.deps.updateBindingSession(rawUserId, channelId, {
61
+ state: 'waiting_buid',
62
+ mcUsername: bind.mcUsername,
63
+ mcUuid: bind.mcUuid
64
+ });
65
+ await this.deps.sendMessage(session, [
66
+ koishi_1.h.at(userId),
67
+ koishi_1.h.text(` 管理员为您启动了B站绑定流程\n🎮 已绑定MC: ${displayUsername}\n🔗 请发送您的B站UID`)
68
+ ]);
69
+ }
70
+ else {
71
+ // 用户自己操作:创建带超时的会话
72
+ const timeout = setTimeout(() => {
73
+ this.deps.bindingSessions.delete(`${userId}_${channelId}`);
74
+ this.ctx.bots.forEach(bot => {
75
+ bot
76
+ .sendMessage(channelId, [
77
+ koishi_1.h.at(userId),
78
+ koishi_1.h.text(' 绑定会话已超时,请重新开始绑定流程\n\n⚠️ 温馨提醒:若在管理员多次提醒后仍不配合绑定账号信息,将按群规进行相应处理。')
79
+ ])
80
+ .catch(() => { });
81
+ });
82
+ this.logger.info('交互绑定', `QQ(${userId})的绑定会话因超时被清理`, true);
83
+ }, this.BINDING_SESSION_TIMEOUT);
84
+ const sessionData = {
85
+ userId: rawUserId,
86
+ channelId: channelId,
87
+ state: 'waiting_buid',
88
+ startTime: Date.now(),
89
+ timeout: timeout,
90
+ mcUsername: bind.mcUsername,
91
+ mcUuid: bind.mcUuid
92
+ };
93
+ this.deps.bindingSessions.set(`${userId}_${channelId}`, sessionData);
94
+ await this.deps.sendMessage(session, [
95
+ koishi_1.h.text(`🎮 已绑定MC: ${displayUsername}\n🔗 请发送您的B站UID`)
96
+ ]);
97
+ }
98
+ }
99
+ /**
100
+ * 处理只绑定B站未绑定MC的场景
101
+ */
102
+ async handleBuidOnlyBind(ctx, bind) {
103
+ const { session, userId, rawUserId, channelId, isAdminAction } = ctx;
104
+ this.logger.info('交互绑定', `QQ(${userId})${isAdminAction ? '已' : '只'}绑定${isAdminAction ? 'B站' : '了B站'},进入MC绑定流程`, true);
105
+ if (isAdminAction) {
106
+ // 管理员操作:更新会话状态并@目标用户
107
+ this.deps.updateBindingSession(rawUserId, channelId, {
108
+ state: 'waiting_mc_username',
109
+ buidUid: bind.buidUid,
110
+ buidUsername: bind.buidUsername
111
+ });
112
+ await this.deps.sendMessage(session, [
113
+ koishi_1.h.at(userId),
114
+ koishi_1.h.text(` 管理员为您启动了MC绑定流程\n✅ 已绑定B站: ${bind.buidUsername}\n🎮 请发送您的MC用户名,或发送"跳过"保持当前状态`)
115
+ ]);
116
+ }
117
+ else {
118
+ // 用户自己操作:创建会话并更新状态
119
+ this.deps.createBindingSession(rawUserId, channelId, 'waiting_mc_username');
120
+ const bindingSession = this.deps.getBindingSession(rawUserId, channelId);
121
+ bindingSession.state = 'waiting_mc_username';
122
+ await this.deps.sendMessage(session, [
123
+ koishi_1.h.text(`✅ 已绑定B站: ${bind.buidUsername}\n🎮 请发送您的MC用户名,或发送"跳过"保持当前状态`)
124
+ ]);
125
+ }
126
+ }
127
+ /**
128
+ * 处理都未绑定的场景
129
+ */
130
+ async handleNoBind(ctx) {
131
+ const { session, userId, isAdminAction } = ctx;
132
+ const messageElements = isAdminAction
133
+ ? [
134
+ koishi_1.h.at(userId),
135
+ koishi_1.h.text(' 管理员为您启动了账号绑定流程\n📋 请选择绑定方式:\n1. 发送您的B站UID进行B站绑定\n2. 发送"跳过"仅绑定MC账号')
136
+ ]
137
+ : [koishi_1.h.text('📋 请选择绑定方式:\n1. 发送您的B站UID进行B站绑定\n2. 发送"跳过"仅绑定MC账号')];
138
+ await this.deps.sendMessage(session, messageElements);
139
+ }
140
+ /**
141
+ * 处理绑定流程
142
+ * @param ctx 绑定流程上下文
143
+ */
144
+ async handleBindingFlow(ctx) {
145
+ const { userId, rawUserId, channelId, bind, isAdminAction } = ctx;
146
+ // 场景1: 已完成全部绑定
147
+ if (bind_status_1.BindStatus.hasCompletedAllBinds(bind)) {
148
+ return this.handleCompletedBinds(ctx, bind);
149
+ }
150
+ // 创建绑定会话(用户自己绑定时在后续场景中创建)
151
+ if (isAdminAction) {
152
+ this.deps.createBindingSession(rawUserId, channelId, 'waiting_buid');
153
+ }
154
+ // 场景2: 只绑定MC → 进入B站绑定流程
155
+ if (bind_status_1.BindStatus.hasOnlyMcBind(bind)) {
156
+ return this.handleMcOnlyBind(ctx, bind);
157
+ }
158
+ // 场景3: 只绑定B站 → 进入MC绑定流程
159
+ if (bind_status_1.BindStatus.hasOnlyBuidBind(bind)) {
160
+ return this.handleBuidOnlyBind(ctx, bind);
161
+ }
162
+ // 场景4: 都未绑定 → 显示选择菜单
163
+ if (!isAdminAction) {
164
+ this.deps.createBindingSession(rawUserId, channelId, 'waiting_buid');
165
+ }
166
+ return this.handleNoBind(ctx);
167
+ }
18
168
  /**
19
169
  * 注册交互式绑定命令
20
170
  */
@@ -58,48 +208,16 @@ class BindingHandler extends base_handler_1.BaseHandler {
58
208
  koishi_1.h.text(`用户 ${normalizedTargetId} 已有进行中的绑定会话`)
59
209
  ]);
60
210
  }
61
- // 检查目标用户当前绑定状态
211
+ // 检查目标用户当前绑定状态并处理绑定流程
62
212
  const targetBind = await this.deps.databaseService.getMcBindByQQId(normalizedTargetId);
63
- // 如果两个账号都已绑定,不需要进入绑定流程
64
- if (bind_status_1.BindStatus.hasCompletedAllBinds(targetBind)) {
65
- this.logger.info('交互绑定', `QQ(${normalizedTargetId})已完成全部绑定`, true);
66
- // 显示当前绑定信息
67
- const displayUsername = bind_status_1.BindStatus.getDisplayMcUsername(targetBind, '未绑定');
68
- let bindInfo = `用户 ${normalizedTargetId} 已完成全部账号绑定:\n✅ MC账号: ${displayUsername}\n✅ B站账号: ${targetBind.buidUsername} (UID: ${targetBind.buidUid})`;
69
- if (targetBind.guardLevel > 0) {
70
- bindInfo += `\n舰长等级: ${targetBind.guardLevelText}`;
71
- }
72
- if (targetBind.medalName) {
73
- bindInfo += `\n粉丝牌: ${targetBind.medalName} Lv.${targetBind.medalLevel}`;
74
- }
75
- return this.deps.sendMessage(session, [koishi_1.h.text(bindInfo)]);
76
- }
77
- // 为目标用户创建绑定会话
78
- this.deps.createBindingSession(target, channelId, 'waiting_buid');
79
- // 如果已绑定MC但未绑定B站,直接进入B站绑定流程
80
- if (bind_status_1.BindStatus.hasValidMcBind(targetBind) && !bind_status_1.BindStatus.hasValidBuidBind(targetBind)) {
81
- this.logger.info('交互绑定', `QQ(${normalizedTargetId})已绑定MC,进入B站绑定流程`, true);
82
- // 更新会话状态
83
- this.deps.updateBindingSession(target, channelId, {
84
- state: 'waiting_buid',
85
- mcUsername: bind_status_1.BindStatus.hasValidMcBind(targetBind)
86
- ? targetBind.mcUsername
87
- : null,
88
- mcUuid: targetBind.mcUuid
89
- });
90
- // 向目标用户发送提示(@他们)
91
- const displayUsername = bind_status_1.BindStatus.getDisplayMcUsername(targetBind, '未绑定');
92
- await this.deps.sendMessage(session, [
93
- koishi_1.h.at(normalizedTargetId),
94
- koishi_1.h.text(` 管理员为您启动了B站绑定流程\n🎮 已绑定MC: ${displayUsername}\n🔗 请发送您的B站UID`)
95
- ]);
96
- return;
97
- }
98
- // 向目标用户发送提示(@他们)
99
- await this.deps.sendMessage(session, [
100
- koishi_1.h.at(normalizedTargetId),
101
- koishi_1.h.text(' 管理员为您启动了账号绑定流程\n📋 请选择绑定方式:\n1. 发送您的B站UID进行B站绑定\n2. 发送"跳过"仅绑定MC账号')
102
- ]);
213
+ await this.handleBindingFlow({
214
+ session,
215
+ userId: normalizedTargetId,
216
+ rawUserId: target,
217
+ channelId,
218
+ bind: targetBind,
219
+ isAdminAction: true
220
+ });
103
221
  return;
104
222
  }
105
223
  // 为自己启动绑定流程
@@ -112,73 +230,16 @@ class BindingHandler extends base_handler_1.BaseHandler {
112
230
  koishi_1.h.text('您已有进行中的绑定会话,请先完成当前绑定或等待会话超时')
113
231
  ]);
114
232
  }
115
- // 检查用户当前绑定状态
233
+ // 检查用户当前绑定状态并处理绑定流程
116
234
  const existingBind = await this.deps.databaseService.getMcBindByQQId(normalizedUserId);
117
- // 如果两个账号都已绑定(且MC不是temp用户名),不需要进入绑定流程
118
- if (existingBind && bind_status_1.BindStatus.hasValidMcBind(existingBind) && existingBind.buidUid) {
119
- this.logger.info('交互绑定', `QQ(${normalizedUserId})已完成全部绑定`, true);
120
- // 显示当前绑定信息
121
- const displayUsername = existingBind.mcUsername;
122
- let bindInfo = `您已完成全部账号绑定:\n✅ MC账号: ${displayUsername}\n✅ B站账号: ${existingBind.buidUsername} (UID: ${existingBind.buidUid})`;
123
- if (existingBind.guardLevel > 0) {
124
- bindInfo += `\n舰长等级: ${existingBind.guardLevelText}`;
125
- }
126
- if (existingBind.medalName) {
127
- bindInfo += `\n粉丝牌: ${existingBind.medalName} Lv.${existingBind.medalLevel}`;
128
- }
129
- bindInfo += `\n\n如需修改绑定信息,请使用:\n- ${this.deps.formatCommand('mcid change <新用户名>')} 修改MC账号\n- ${this.deps.formatCommand('buid bind <新UID>')} 修改B站账号`;
130
- return this.deps.sendMessage(session, [koishi_1.h.text(bindInfo)]);
131
- }
132
- // 如果已绑定MC(且不是temp用户名)但未绑定B站,直接进入B站绑定流程
133
- if (existingBind && bind_status_1.BindStatus.hasValidMcBind(existingBind) && !existingBind.buidUid) {
134
- this.logger.info('交互绑定', `QQ(${normalizedUserId})已绑定MC,进入B站绑定流程`, true);
135
- // 创建绑定会话,状态直接设为等待B站UID
136
- const timeout = setTimeout(() => {
137
- this.deps.bindingSessions.delete(`${normalizedUserId}_${channelId}`);
138
- this.ctx.bots.forEach(bot => {
139
- bot
140
- .sendMessage(channelId, [
141
- koishi_1.h.at(normalizedUserId),
142
- koishi_1.h.text(' 绑定会话已超时,请重新开始绑定流程\n\n⚠️ 温馨提醒:若在管理员多次提醒后仍不配合绑定账号信息,将按群规进行相应处理。')
143
- ])
144
- .catch(() => { });
145
- });
146
- this.logger.info('交互绑定', `QQ(${normalizedUserId})的绑定会话因超时被清理`, true);
147
- }, this.BINDING_SESSION_TIMEOUT);
148
- const sessionData = {
149
- userId: session.userId,
150
- channelId: channelId,
151
- state: 'waiting_buid',
152
- startTime: Date.now(),
153
- timeout: timeout,
154
- mcUsername: existingBind.mcUsername,
155
- mcUuid: existingBind.mcUuid
156
- };
157
- this.deps.bindingSessions.set(`${normalizedUserId}_${channelId}`, sessionData);
158
- return this.deps.sendMessage(session, [
159
- koishi_1.h.text(`🎮 已绑定MC: ${existingBind.mcUsername}\n🔗 请发送您的B站UID`)
160
- ]);
161
- }
162
- // 如果只绑定了B站(MC是temp用户名),提醒绑定MC账号
163
- if (existingBind &&
164
- existingBind.buidUid &&
165
- existingBind.buidUsername &&
166
- !bind_status_1.BindStatus.hasValidMcBind(existingBind)) {
167
- this.logger.info('交互绑定', `QQ(${normalizedUserId})只绑定了B站,进入MC绑定流程`, true);
168
- // 创建绑定会话,状态设为等待MC用户名
169
- this.deps.createBindingSession(session.userId, channelId, 'waiting_mc_username');
170
- const bindingSession = this.deps.getBindingSession(session.userId, channelId);
171
- bindingSession.state = 'waiting_mc_username';
172
- return this.deps.sendMessage(session, [
173
- koishi_1.h.text(`✅ 已绑定B站: ${existingBind.buidUsername}\n🎮 请发送您的MC用户名,或发送"跳过"保持当前状态`)
174
- ]);
175
- }
176
- // 如果未绑定账号,让用户选择绑定方式,优先B站绑定
177
- this.deps.createBindingSession(session.userId, channelId, 'waiting_buid');
178
- // 发送绑定选项提示
179
- return this.deps.sendMessage(session, [
180
- koishi_1.h.text('📋 请选择绑定方式:\n1. 发送您的B站UID进行B站绑定\n2. 发送"跳过"仅绑定MC账号')
181
- ]);
235
+ await this.handleBindingFlow({
236
+ session,
237
+ userId: normalizedUserId,
238
+ rawUserId: session.userId,
239
+ channelId,
240
+ bind: existingBind,
241
+ isAdminAction: false
242
+ });
182
243
  }
183
244
  catch (error) {
184
245
  const normalizedUserId = this.deps.normalizeQQId(session.userId);
@@ -87,9 +87,14 @@ export declare class GroupRequestReviewHandler extends BaseHandler {
87
87
  */
88
88
  private notifyAdmin;
89
89
  /**
90
- * 执行自动绑定
90
+ * 执行自动绑定(仅数据库操作,不设置昵称)
91
+ * @returns 绑定信息,包含B站用户名和MC用户名
91
92
  */
92
93
  private performAutoBind;
94
+ /**
95
+ * 用户进群后设置群昵称
96
+ */
97
+ private setGroupNicknameAfterJoin;
93
98
  /**
94
99
  * 定时清理过期记录
95
100
  */
@@ -114,6 +114,12 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
114
114
  const msgId = messageId.toString();
115
115
  const emojiData = likes;
116
116
  const operatorId = this.deps.normalizeQQId(userId);
117
+ // 忽略 bot 自己的表情回应(addReactionOptions 添加表情时也会触发此事件)
118
+ const botId = this.deps.normalizeQQId(session.bot.selfId);
119
+ if (operatorId === botId) {
120
+ this.logger.debug('入群审批', `忽略 bot 自己的表情回应 - 消息: ${msgId}`);
121
+ return;
122
+ }
117
123
  this.logger.debug('入群审批', `收到表情回应 - 消息: ${msgId}, 操作者: ${operatorId}, 表情数: ${emojiData.length}`);
118
124
  // 检查是否是待审批的消息
119
125
  const pendingReq = this.pendingRequests.get(msgId);
@@ -177,6 +183,8 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
177
183
  buidUid = parsedUid;
178
184
  // 尝试使用强制绑定模式获取完整信息(避免频率限制)
179
185
  let zminfoData = null;
186
+ // 存储强制绑定模式下获取的目标粉丝牌信息
187
+ let targetMedalInfo = null;
180
188
  if (this.config.forceBindSessdata) {
181
189
  try {
182
190
  this.logger.debug('入群审批', '使用强制绑定模式获取用户信息...');
@@ -184,7 +192,9 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
184
192
  const standardUser = this.deps.forceBinder.convertToZminfoUser(enhancedUser);
185
193
  buidUsername = standardUser.username;
186
194
  zminfoData = standardUser;
187
- this.logger.debug('入群审批', `✅ 强制绑定模式获取成功: ${buidUsername}`);
195
+ // 保存目标粉丝牌信息,用于后续显示
196
+ targetMedalInfo = enhancedUser.targetMedal || null;
197
+ this.logger.debug('入群审批', `✅ 强制绑定模式获取成功: ${buidUsername}, 目标粉丝牌: ${targetMedalInfo?.found ? '已找到' : '未找到'}`);
188
198
  }
189
199
  catch (error) {
190
200
  this.logger.warn('入群审批', `强制绑定获取用户信息失败: ${error.message},降级到ZMINFO`);
@@ -205,8 +215,21 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
205
215
  this.logger.debug('入群审批', `✅ ZMINFO用户名: ${buidUsername}`);
206
216
  }
207
217
  }
208
- // 粉丝牌信息:从ZMINFO或强制绑定结果获取
209
- if (zminfoData) {
218
+ // 粉丝牌信息:优先使用强制绑定获取的目标粉丝牌,其次使用ZMINFO的当前佩戴粉丝牌
219
+ if (targetMedalInfo) {
220
+ // 使用强制绑定模式获取的目标粉丝牌信息(从B站粉丝牌墙获取,更准确)
221
+ if (targetMedalInfo.found && targetMedalInfo.name && targetMedalInfo.level !== undefined) {
222
+ medalInfo = `🎖️ ${targetMedalInfo.name} Lv.${targetMedalInfo.level}`;
223
+ if (targetMedalInfo.wearing_status === 1) {
224
+ medalInfo += ' 【已佩戴】';
225
+ }
226
+ }
227
+ else {
228
+ medalInfo = `⚠️ 未获取到 "${this.config.forceBindTargetMedalName}" 粉丝牌`;
229
+ }
230
+ }
231
+ else if (zminfoData) {
232
+ // 降级:使用ZMINFO的当前佩戴粉丝牌信息
210
233
  const medalLevel = zminfoData.medal?.level || 0;
211
234
  const medalName = zminfoData.medal?.name || '';
212
235
  if (medalName === this.config.forceBindTargetMedalName) {
@@ -269,7 +292,7 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
269
292
  else {
270
293
  elements.push(koishi_1.h.text(`⚠️ 未提供有效的 B 站 UID\n\n`));
271
294
  }
272
- elements.push(koishi_1.h.text('请点击表情回应:\n'), koishi_1.h.text('👍 - 通过并自动绑定\n'), koishi_1.h.text('😊 - 通过并交互式绑定\n'), koishi_1.h.text(' - 拒绝申请'));
295
+ elements.push(koishi_1.h.text('请点击表情回应:\n'), koishi_1.h.text('/赞 - 通过并自动绑定\n'), koishi_1.h.text('/OK - 通过并交互式绑定\n'), koishi_1.h.text('/NO - 拒绝申请'));
273
296
  try {
274
297
  const result = await session.bot.sendMessage(this.reviewConfig.reviewGroupId, elements);
275
298
  // result 通常是数组,第一个元素是消息ID
@@ -343,21 +366,23 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
343
366
  return;
344
367
  }
345
368
  this.logger.info('入群审批', `开始自动绑定 - QQ: ${pendingReq.applicantQQ}, UID: ${uid}`);
346
- // 2. 先执行绑定(在批准入群前)
347
- await this.performAutoBind(pendingReq.applicantQQ, uid, session.bot);
369
+ // 2. 先执行绑定(在批准入群前,仅数据库操作,不设置昵称)
370
+ const bindResult = await this.performAutoBind(pendingReq.applicantQQ, uid, session.bot);
348
371
  this.logger.info('入群审批', `预绑定完成 - QQ: ${pendingReq.applicantQQ}, UID: ${uid}`);
349
372
  // 3. 批准入群
350
373
  await session.bot.handleGuildMemberRequest(pendingReq.requestFlag, true, '欢迎加入!');
351
374
  this.logger.info('入群审批', `已批准入群 - QQ: ${pendingReq.applicantQQ}`, true);
352
375
  // 4. 等待用户进群
353
376
  const joined = await this.waitForUserJoin(pendingReq.applicantQQ, pendingReq.targetGroupId, 10000);
354
- if (!joined) {
355
- await this.notifyAdmin(operatorId, session, `⚠️ 已完成绑定并批准 ${pendingReq.applicantQQ} 入群,但用户未在10秒内进群`);
356
- pendingReq.status = 'approved';
357
- return;
377
+ if (joined) {
378
+ // 5. 用户进群后再设置群昵称
379
+ await this.setGroupNicknameAfterJoin(pendingReq.applicantQQ, bindResult.buidUsername, bindResult.mcUsername, session.bot);
380
+ // 6. 通知管理员
381
+ await this.notifyAdmin(operatorId, session, `✅ 已批准 ${pendingReq.applicantQQ} 入群并完成自动绑定\nUID: ${uid}`);
382
+ }
383
+ else {
384
+ await this.notifyAdmin(operatorId, session, `⚠️ 已完成绑定并批准 ${pendingReq.applicantQQ} 入群,但用户未在10秒内进群\n昵称将在用户进群后由系统自动设置`);
358
385
  }
359
- // 5. 通知管理员
360
- await this.notifyAdmin(operatorId, session, `✅ 已批准 ${pendingReq.applicantQQ} 入群并完成自动绑定\nUID: ${uid}`);
361
386
  pendingReq.status = 'approved';
362
387
  }
363
388
  catch (error) {
@@ -582,86 +607,92 @@ class GroupRequestReviewHandler extends base_handler_1.BaseHandler {
582
607
  }
583
608
  }
584
609
  /**
585
- * 执行自动绑定
610
+ * 执行自动绑定(仅数据库操作,不设置昵称)
611
+ * @returns 绑定信息,包含B站用户名和MC用户名
586
612
  */
587
613
  async performAutoBind(qq, uid, bot) {
614
+ // 1. 使用强制绑定模式获取最新用户信息(避免频率限制)
615
+ this.logger.debug('入群审批', `开始获取 B站 UID ${uid} 的信息`);
616
+ // 使用强制绑定模式获取完整信息(与 bind -f 保持一致)
617
+ this.logger.debug('入群审批', '正在使用强制绑定模式获取B站用户信息...');
618
+ const enhancedUser = await this.deps.forceBinder.forceBindUser(uid);
619
+ const zminfoUser = this.deps.forceBinder.convertToZminfoUser(enhancedUser);
620
+ if (!zminfoUser) {
621
+ throw new Error(`无法验证B站UID: ${uid}`);
622
+ }
623
+ this.logger.info('入群审批', `✅ 获取到用户名: "${zminfoUser.username}"`, true);
624
+ // 2. 检查是否已被其他人绑定
625
+ const existingBind = await this.repos.mcidbind.findByBuidUid(uid);
626
+ if (existingBind && existingBind.qqId !== qq) {
627
+ throw new Error(`UID ${uid} 已被其他用户绑定`);
628
+ }
629
+ // 3. 获取或创建绑定记录
630
+ let bind = await this.repos.mcidbind.findByQQId(qq);
631
+ let mcUsername = null;
632
+ if (!bind) {
633
+ // 创建新绑定(不使用临时MC用户名)
634
+ bind = await this.repos.mcidbind.create({
635
+ qqId: qq,
636
+ mcUsername: null,
637
+ mcUuid: null,
638
+ buidUid: zminfoUser.uid,
639
+ buidUsername: zminfoUser.username,
640
+ guardLevel: zminfoUser.guard_level || 0,
641
+ guardLevelText: zminfoUser.guard_level_text || '',
642
+ maxGuardLevel: zminfoUser.guard_level || 0,
643
+ maxGuardLevelText: zminfoUser.guard_level_text || '',
644
+ medalName: zminfoUser.medal?.name || '',
645
+ medalLevel: zminfoUser.medal?.level || 0,
646
+ wealthMedalLevel: zminfoUser.wealthMedalLevel || 0,
647
+ lastActiveTime: new Date(),
648
+ lastModified: new Date(),
649
+ hasMcBind: false,
650
+ hasBuidBind: true
651
+ });
652
+ this.logger.info('入群审批', `已创建新绑定 - QQ: ${qq}, UID: ${uid}`, true);
653
+ }
654
+ else {
655
+ // 保存现有的MC用户名
656
+ mcUsername = bind_status_1.BindStatus.getDisplayMcUsername(bind, null);
657
+ // 更新现有绑定
658
+ await this.repos.mcidbind.update(qq, {
659
+ buidUid: zminfoUser.uid,
660
+ buidUsername: zminfoUser.username,
661
+ guardLevel: zminfoUser.guard_level || 0,
662
+ guardLevelText: zminfoUser.guard_level_text || '',
663
+ maxGuardLevel: Math.max(bind.maxGuardLevel || 0, zminfoUser.guard_level || 0),
664
+ maxGuardLevelText: zminfoUser.guard_level > (bind.maxGuardLevel || 0)
665
+ ? zminfoUser.guard_level_text
666
+ : bind.maxGuardLevelText,
667
+ medalName: zminfoUser.medal?.name || '',
668
+ medalLevel: zminfoUser.medal?.level || 0,
669
+ wealthMedalLevel: zminfoUser.wealthMedalLevel || 0,
670
+ lastActiveTime: new Date(),
671
+ lastModified: new Date(),
672
+ hasBuidBind: true
673
+ });
674
+ this.logger.info('入群审批', `已更新绑定 - QQ: ${qq}, UID: ${uid}`, true);
675
+ }
676
+ this.logger.info('入群审批', `自动绑定完成 - QQ: ${qq}, UID: ${uid}, 用户名: ${zminfoUser.username}`, true);
677
+ // 返回绑定信息供后续使用(昵称设置在用户进群后执行)
678
+ return {
679
+ buidUsername: zminfoUser.username,
680
+ mcUsername: mcUsername
681
+ };
682
+ }
683
+ /**
684
+ * 用户进群后设置群昵称
685
+ */
686
+ async setGroupNicknameAfterJoin(qq, buidUsername, mcUsername, bot) {
588
687
  try {
589
- // 1. 使用强制绑定模式获取最新用户信息(避免频率限制)
590
- this.logger.debug('入群审批', `开始获取 B站 UID ${uid} 的信息`);
591
- // 使用强制绑定模式获取完整信息(与 bind -f 保持一致)
592
- this.logger.debug('入群审批', '正在使用强制绑定模式获取B站用户信息...');
593
- const enhancedUser = await this.deps.forceBinder.forceBindUser(uid);
594
- const zminfoUser = this.deps.forceBinder.convertToZminfoUser(enhancedUser);
595
- if (!zminfoUser) {
596
- throw new Error(`无法验证B站UID: ${uid}`);
597
- }
598
- this.logger.info('入群审批', `✅ 获取到用户名: "${zminfoUser.username}"`, true);
599
- // 2. 检查是否已被其他人绑定
600
- const existingBind = await this.repos.mcidbind.findByBuidUid(uid);
601
- if (existingBind && existingBind.qqId !== qq) {
602
- throw new Error(`UID ${uid} 已被其他用户绑定`);
603
- }
604
- // 3. 获取或创建绑定记录
605
- let bind = await this.repos.mcidbind.findByQQId(qq);
606
- if (!bind) {
607
- // 创建新绑定(不使用临时MC用户名)
608
- bind = await this.repos.mcidbind.create({
609
- qqId: qq,
610
- mcUsername: null,
611
- mcUuid: null,
612
- buidUid: zminfoUser.uid,
613
- buidUsername: zminfoUser.username,
614
- guardLevel: zminfoUser.guard_level || 0,
615
- guardLevelText: zminfoUser.guard_level_text || '',
616
- maxGuardLevel: zminfoUser.guard_level || 0,
617
- maxGuardLevelText: zminfoUser.guard_level_text || '',
618
- medalName: zminfoUser.medal?.name || '',
619
- medalLevel: zminfoUser.medal?.level || 0,
620
- wealthMedalLevel: zminfoUser.wealthMedalLevel || 0,
621
- lastActiveTime: new Date(),
622
- lastModified: new Date(),
623
- hasMcBind: false,
624
- hasBuidBind: true
625
- });
626
- this.logger.info('入群审批', `已创建新绑定 - QQ: ${qq}, UID: ${uid}`, true);
627
- }
628
- else {
629
- // 更新现有绑定
630
- await this.repos.mcidbind.update(qq, {
631
- buidUid: zminfoUser.uid,
632
- buidUsername: zminfoUser.username,
633
- guardLevel: zminfoUser.guard_level || 0,
634
- guardLevelText: zminfoUser.guard_level_text || '',
635
- maxGuardLevel: Math.max(bind.maxGuardLevel || 0, zminfoUser.guard_level || 0),
636
- maxGuardLevelText: zminfoUser.guard_level > (bind.maxGuardLevel || 0)
637
- ? zminfoUser.guard_level_text
638
- : bind.maxGuardLevelText,
639
- medalName: zminfoUser.medal?.name || '',
640
- medalLevel: zminfoUser.medal?.level || 0,
641
- wealthMedalLevel: zminfoUser.wealthMedalLevel || 0,
642
- lastActiveTime: new Date(),
643
- lastModified: new Date(),
644
- hasBuidBind: true
645
- });
646
- this.logger.info('入群审批', `已更新绑定 - QQ: ${qq}, UID: ${uid}`, true);
647
- }
648
- // 4. 更新群昵称(使用标准格式)
649
- try {
650
- const groupId = this.reviewConfig.targetGroupId;
651
- const mcInfo = bind_status_1.BindStatus.getDisplayMcUsername(bind, '未绑定');
652
- const nickname = `${zminfoUser.username}(ID:${mcInfo})`;
653
- await bot.internal.setGroupCard(groupId, qq, nickname);
654
- this.logger.info('入群审批', `已更新群昵称 - QQ: ${qq}, 昵称: ${nickname}`);
655
- }
656
- catch (error) {
657
- this.logger.warn('入群审批', `更新群昵称失败: ${error.message}`);
658
- // 昵称更新失败不影响绑定
659
- }
660
- this.logger.info('入群审批', `自动绑定完成 - QQ: ${qq}, UID: ${uid}, 用户名: ${zminfoUser.username}`, true);
688
+ const groupId = this.reviewConfig.targetGroupId;
689
+ const mcInfo = mcUsername || '未绑定';
690
+ const nickname = `${buidUsername}(ID:${mcInfo})`;
691
+ await bot.internal.setGroupCard(groupId, qq, nickname);
692
+ this.logger.info('入群审批', `已更新群昵称 - QQ: ${qq}, 昵称: ${nickname}`);
661
693
  }
662
694
  catch (error) {
663
- this.logger.error('入群审批', `自动绑定失败: ${error.message}`, error);
664
- throw error;
695
+ this.logger.warn('入群审批', `更新群昵称失败: ${error.message}`);
665
696
  }
666
697
  }
667
698
  /**
package/lib/index.js CHANGED
@@ -60,11 +60,11 @@ exports.Config = koishi_1.Schema.object({
60
60
  reviewGroupId: koishi_1.Schema.string()
61
61
  .description('管理员审批操作所在的群ID(播报群)'),
62
62
  approveAutoBindEmoji: koishi_1.Schema.string()
63
- .description('批准并自动绑定的表情ID(/太赞了)')
64
- .default('389'),
63
+ .description('批准并自动绑定的表情ID(/赞)')
64
+ .default('76'),
65
65
  approveInteractiveBindEmoji: koishi_1.Schema.string()
66
- .description('批准并交互式绑定的表情ID(/偷感)')
67
- .default('427'),
66
+ .description('批准并交互式绑定的表情ID(/OK)')
67
+ .default('124'),
68
68
  rejectEmoji: koishi_1.Schema.string().description('拒绝申请的表情ID(/NO)').default('123'),
69
69
  autoCleanupHours: koishi_1.Schema.number()
70
70
  .description('待审批记录自动清理时间(小时)')
@@ -134,9 +134,9 @@ export interface GroupRequestReviewConfig {
134
134
  targetGroupId: string;
135
135
  /** 管理员审批操作所在的群ID(播报群) */
136
136
  reviewGroupId: string;
137
- /** 批准并自动绑定的表情ID(默认:389 /太赞了) */
137
+ /** 批准并自动绑定的表情ID(默认:76 /赞) */
138
138
  approveAutoBindEmoji: string;
139
- /** 批准并交互式绑定的表情ID(默认:427 /偷感) */
139
+ /** 批准并交互式绑定的表情ID(默认:124 /OK) */
140
140
  approveInteractiveBindEmoji: string;
141
141
  /** 拒绝申请的表情ID(默认:123 /NO) */
142
142
  rejectEmoji: string;
@@ -79,6 +79,20 @@ export declare class BindStatus {
79
79
  * ```
80
80
  */
81
81
  static hasCompletedAllBinds(bind: MCIDBIND | null | undefined): boolean;
82
+ /**
83
+ * 检查是否只绑定了MC(未绑定B站)
84
+ *
85
+ * @param bind - 绑定记录(可为 null 或 undefined)
86
+ * @returns true 表示已绑定MC但未绑定B站
87
+ */
88
+ static hasOnlyMcBind(bind: MCIDBIND | null | undefined): boolean;
89
+ /**
90
+ * 检查是否只绑定了B站(未绑定MC)
91
+ *
92
+ * @param bind - 绑定记录(可为 null 或 undefined)
93
+ * @returns true 表示已绑定B站但未绑定MC
94
+ */
95
+ static hasOnlyBuidBind(bind: MCIDBIND | null | undefined): boolean;
82
96
  /**
83
97
  * 获取绑定状态摘要信息
84
98
  *
@@ -111,6 +111,28 @@ class BindStatus {
111
111
  static hasCompletedAllBinds(bind) {
112
112
  return this.hasValidMcBind(bind) && this.hasValidBuidBind(bind);
113
113
  }
114
+ /**
115
+ * 检查是否只绑定了MC(未绑定B站)
116
+ *
117
+ * @param bind - 绑定记录(可为 null 或 undefined)
118
+ * @returns true 表示已绑定MC但未绑定B站
119
+ */
120
+ static hasOnlyMcBind(bind) {
121
+ return this.hasValidMcBind(bind) && !this.hasValidBuidBind(bind);
122
+ }
123
+ /**
124
+ * 检查是否只绑定了B站(未绑定MC)
125
+ *
126
+ * @param bind - 绑定记录(可为 null 或 undefined)
127
+ * @returns true 表示已绑定B站但未绑定MC
128
+ */
129
+ static hasOnlyBuidBind(bind) {
130
+ return (bind !== null &&
131
+ bind !== undefined &&
132
+ !!bind.buidUid &&
133
+ !!bind.buidUsername &&
134
+ !this.hasValidMcBind(bind));
135
+ }
114
136
  /**
115
137
  * 获取绑定状态摘要信息
116
138
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-bind-bot",
3
3
  "description": "[WittF自用] BIND-BOT - 账号绑定管理机器人,支持Minecraft账号和B站账号绑定与管理。",
4
- "version": "2.2.7",
4
+ "version": "2.2.9",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [