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.
- package/lib/export-utils.d.ts +49 -0
- package/lib/export-utils.js +305 -0
- package/lib/force-bind-utils.d.ts +40 -0
- package/lib/force-bind-utils.js +242 -0
- package/lib/handlers/base.handler.d.ts +61 -0
- package/lib/handlers/base.handler.js +22 -0
- package/lib/handlers/binding.handler.d.ts +45 -0
- package/lib/handlers/binding.handler.js +285 -0
- package/lib/handlers/buid.handler.d.ts +71 -0
- package/lib/handlers/buid.handler.js +694 -0
- package/lib/handlers/index.d.ts +6 -0
- package/lib/handlers/index.js +22 -0
- package/lib/handlers/mcid.handler.d.ts +101 -0
- package/lib/handlers/mcid.handler.js +1045 -0
- package/lib/handlers/tag.handler.d.ts +14 -0
- package/lib/handlers/tag.handler.js +382 -0
- package/lib/handlers/whitelist.handler.d.ts +84 -0
- package/lib/handlers/whitelist.handler.js +1011 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +2693 -0
- package/lib/managers/rcon-manager.d.ts +24 -0
- package/lib/managers/rcon-manager.js +308 -0
- package/lib/repositories/mcidbind.repository.d.ts +105 -0
- package/lib/repositories/mcidbind.repository.js +288 -0
- package/lib/repositories/schedule-mute.repository.d.ts +68 -0
- package/lib/repositories/schedule-mute.repository.js +175 -0
- package/lib/types/api.d.ts +135 -0
- package/lib/types/api.js +6 -0
- package/lib/types/common.d.ts +40 -0
- package/lib/types/common.js +6 -0
- package/lib/types/config.d.ts +55 -0
- package/lib/types/config.js +6 -0
- package/lib/types/database.d.ts +47 -0
- package/lib/types/database.js +6 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/index.js +28 -0
- package/lib/utils/helpers.d.ts +76 -0
- package/lib/utils/helpers.js +275 -0
- package/lib/utils/logger.d.ts +75 -0
- package/lib/utils/logger.js +134 -0
- package/lib/utils/message-utils.d.ts +46 -0
- package/lib/utils/message-utils.js +234 -0
- package/lib/utils/rate-limiter.d.ts +26 -0
- package/lib/utils/rate-limiter.js +47 -0
- package/lib/utils/session-manager.d.ts +70 -0
- package/lib/utils/session-manager.js +120 -0
- package/package.json +39 -0
- package/readme.md +281 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoggerService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 统一的日志服务类
|
|
6
|
+
* 提供一致的日志接口,支持上下文标签和调试模式
|
|
7
|
+
*/
|
|
8
|
+
class LoggerService {
|
|
9
|
+
logger;
|
|
10
|
+
debugMode;
|
|
11
|
+
defaultContext;
|
|
12
|
+
/**
|
|
13
|
+
* 创建日志服务实例
|
|
14
|
+
* @param logger Koishi Logger 实例
|
|
15
|
+
* @param debugMode 是否启用调试模式
|
|
16
|
+
* @param defaultContext 默认上下文标签(可选)
|
|
17
|
+
*/
|
|
18
|
+
constructor(logger, debugMode = false, defaultContext = '') {
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.debugMode = debugMode;
|
|
21
|
+
this.defaultContext = defaultContext;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 设置调试模式
|
|
25
|
+
*/
|
|
26
|
+
setDebugMode(enabled) {
|
|
27
|
+
this.debugMode = enabled;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 获取当前调试模式状态
|
|
31
|
+
*/
|
|
32
|
+
getDebugMode() {
|
|
33
|
+
return this.debugMode;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 输出调试日志(仅在 debugMode 为 true 时输出)
|
|
37
|
+
* @param context 上下文标签
|
|
38
|
+
* @param message 日志消息
|
|
39
|
+
*/
|
|
40
|
+
debug(context, message) {
|
|
41
|
+
if (this.debugMode) {
|
|
42
|
+
const ctx = context || this.defaultContext;
|
|
43
|
+
this.logger.debug(ctx ? `[${ctx}] ${message}` : message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 输出信息日志
|
|
48
|
+
* @param context 上下文标签
|
|
49
|
+
* @param message 日志消息
|
|
50
|
+
* @param forceOutput 强制输出(忽略 debugMode 限制)
|
|
51
|
+
*/
|
|
52
|
+
info(context, message, forceOutput = false) {
|
|
53
|
+
// 只有在debugMode开启或forceOutput=true时才输出普通信息
|
|
54
|
+
if (this.debugMode || forceOutput) {
|
|
55
|
+
const ctx = context || this.defaultContext;
|
|
56
|
+
this.logger.info(ctx ? `[${ctx}] ${message}` : message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 输出警告日志(总是输出)
|
|
61
|
+
* @param context 上下文标签
|
|
62
|
+
* @param message 日志消息
|
|
63
|
+
*/
|
|
64
|
+
warn(context, message) {
|
|
65
|
+
const ctx = context || this.defaultContext;
|
|
66
|
+
this.logger.warn(ctx ? `[${ctx}] ${message}` : message);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 输出错误日志(总是输出)
|
|
70
|
+
* @param context 上下文标签
|
|
71
|
+
* @param message 错误消息
|
|
72
|
+
* @param error 错误对象或错误字符串(可选)
|
|
73
|
+
*/
|
|
74
|
+
error(context, message, error) {
|
|
75
|
+
const ctx = context || this.defaultContext;
|
|
76
|
+
let fullMessage = ctx ? `[${ctx}] ${message}` : message;
|
|
77
|
+
if (error) {
|
|
78
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
79
|
+
fullMessage += `: ${errorMessage}`;
|
|
80
|
+
}
|
|
81
|
+
this.logger.error(fullMessage);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 记录用户操作日志(带用户ID和操作结果)
|
|
85
|
+
* @param operation 操作名称(如 "MC账号绑定")
|
|
86
|
+
* @param userId 用户ID(QQ号)
|
|
87
|
+
* @param success 操作是否成功
|
|
88
|
+
* @param details 额外详情(可选)
|
|
89
|
+
*/
|
|
90
|
+
logOperation(operation, userId, success, details = '') {
|
|
91
|
+
const normalizedQQId = this.normalizeQQId(userId);
|
|
92
|
+
const status = success ? '成功' : '失败';
|
|
93
|
+
const message = `QQ(${normalizedQQId}) ${operation} ${status}${details ? ': ' + details : ''}`;
|
|
94
|
+
if (success) {
|
|
95
|
+
// 成功的操作,只在debug模式下输出详情
|
|
96
|
+
// 特殊处理:绑定相关操作强制输出
|
|
97
|
+
const forceOutput = !this.debugMode && operation.includes('绑定');
|
|
98
|
+
this.info('操作', message, forceOutput);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// 失败的操作总是输出为警告
|
|
102
|
+
this.warn('操作', message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 规范化QQ号格式(移除platform前缀)
|
|
107
|
+
* @param userId 用户ID(可能包含 platform: 前缀)
|
|
108
|
+
* @returns 纯QQ号字符串
|
|
109
|
+
*/
|
|
110
|
+
normalizeQQId(userId) {
|
|
111
|
+
if (!userId)
|
|
112
|
+
return '';
|
|
113
|
+
// 移除可能的 platform: 前缀(如 "onebot:123456" -> "123456")
|
|
114
|
+
if (userId.includes(':')) {
|
|
115
|
+
return userId.split(':').pop() || userId;
|
|
116
|
+
}
|
|
117
|
+
return userId;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 创建子日志服务(带固定上下文)
|
|
121
|
+
* @param context 固定的上下文标签
|
|
122
|
+
* @returns 新的 LoggerService 实例
|
|
123
|
+
*/
|
|
124
|
+
createChild(context) {
|
|
125
|
+
return new LoggerService(this.logger, this.debugMode, context);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 获取原始的 Koishi Logger 实例(用于特殊情况)
|
|
129
|
+
*/
|
|
130
|
+
getRawLogger() {
|
|
131
|
+
return this.logger;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.LoggerService = LoggerService;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Session } from 'koishi';
|
|
2
|
+
import { LoggerService } from './logger';
|
|
3
|
+
import { BindingSession } from './session-manager';
|
|
4
|
+
/**
|
|
5
|
+
* 消息工具配置接口
|
|
6
|
+
*/
|
|
7
|
+
export interface MessageUtilsConfig {
|
|
8
|
+
autoRecallTime: number;
|
|
9
|
+
recallUserMessage: boolean;
|
|
10
|
+
autoNicknameGroupId: string;
|
|
11
|
+
debugMode: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 消息工具类
|
|
15
|
+
* 处理消息发送、撤回和群昵称设置
|
|
16
|
+
*/
|
|
17
|
+
export declare class MessageUtils {
|
|
18
|
+
private config;
|
|
19
|
+
private logger;
|
|
20
|
+
private getBindingSessionFn;
|
|
21
|
+
/**
|
|
22
|
+
* 创建消息工具实例
|
|
23
|
+
* @param config 消息工具配置
|
|
24
|
+
* @param logger 日志服务
|
|
25
|
+
* @param getBindingSessionFn 获取绑定会话的函数
|
|
26
|
+
*/
|
|
27
|
+
constructor(config: MessageUtilsConfig, logger: LoggerService, getBindingSessionFn: (userId: string, channelId: string) => BindingSession | null);
|
|
28
|
+
/**
|
|
29
|
+
* 发送消息并处理自动撤回
|
|
30
|
+
* @param session Koishi Session对象
|
|
31
|
+
* @param content 消息内容数组
|
|
32
|
+
* @param options 可选参数
|
|
33
|
+
*/
|
|
34
|
+
sendMessage(session: Session, content: any[], options?: {
|
|
35
|
+
isProactiveMessage?: boolean;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* 自动设置群昵称
|
|
39
|
+
* @param session Koishi Session对象
|
|
40
|
+
* @param mcUsername MC用户名
|
|
41
|
+
* @param buidUsername B站用户名
|
|
42
|
+
* @param targetUserId 目标用户ID(可选,用于管理员为他人设置)
|
|
43
|
+
* @param specifiedGroupId 指定的群ID(可选,默认使用配置的群ID)
|
|
44
|
+
*/
|
|
45
|
+
autoSetGroupNickname(session: Session, mcUsername: string | null, buidUsername: string, targetUserId?: string, specifiedGroupId?: string): Promise<void>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MessageUtils = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
/**
|
|
7
|
+
* 消息工具类
|
|
8
|
+
* 处理消息发送、撤回和群昵称设置
|
|
9
|
+
*/
|
|
10
|
+
class MessageUtils {
|
|
11
|
+
config;
|
|
12
|
+
logger;
|
|
13
|
+
getBindingSessionFn;
|
|
14
|
+
/**
|
|
15
|
+
* 创建消息工具实例
|
|
16
|
+
* @param config 消息工具配置
|
|
17
|
+
* @param logger 日志服务
|
|
18
|
+
* @param getBindingSessionFn 获取绑定会话的函数
|
|
19
|
+
*/
|
|
20
|
+
constructor(config, logger, getBindingSessionFn) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.getBindingSessionFn = getBindingSessionFn;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 发送消息并处理自动撤回
|
|
27
|
+
* @param session Koishi Session对象
|
|
28
|
+
* @param content 消息内容数组
|
|
29
|
+
* @param options 可选参数
|
|
30
|
+
*/
|
|
31
|
+
async sendMessage(session, content, options) {
|
|
32
|
+
try {
|
|
33
|
+
if (!session) {
|
|
34
|
+
this.logger.error('消息', 'system操作失败: 无效的会话对象');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// 检查是否为群聊消息
|
|
38
|
+
const isGroupMessage = session.channelId && !session.channelId.startsWith('private:');
|
|
39
|
+
const normalizedQQId = (0, helpers_1.normalizeQQId)(session.userId);
|
|
40
|
+
const isProactiveMessage = options?.isProactiveMessage || false;
|
|
41
|
+
// 处理私聊和群聊的消息格式
|
|
42
|
+
// 主动消息不引用原消息
|
|
43
|
+
const promptMessage = session.channelId?.startsWith('private:')
|
|
44
|
+
? (isProactiveMessage ? content : [koishi_1.h.quote(session.messageId), ...content])
|
|
45
|
+
: (isProactiveMessage ? [koishi_1.h.at(normalizedQQId), '\n', ...content] : [koishi_1.h.quote(session.messageId), koishi_1.h.at(normalizedQQId), '\n', ...content]);
|
|
46
|
+
// 发送消息并获取返回的消息ID
|
|
47
|
+
const messageResult = await session.send(promptMessage);
|
|
48
|
+
this.logger.debug('消息', `成功向QQ(${normalizedQQId})发送消息,频道: ${session.channelId}`);
|
|
49
|
+
// 只在自动撤回时间大于0和存在bot对象时处理撤回
|
|
50
|
+
if (this.config.autoRecallTime > 0 && session.bot) {
|
|
51
|
+
// 处理撤回用户消息 - 只在群聊中且开启了用户消息撤回时
|
|
52
|
+
// 但如果用户在绑定会话中发送聊天消息(不包括指令),不撤回
|
|
53
|
+
// 主动消息不撤回用户消息
|
|
54
|
+
const bindingSession = this.getBindingSessionFn(session.userId, session.channelId);
|
|
55
|
+
const isBindingCommand = session.content && (session.content.trim() === '绑定' ||
|
|
56
|
+
session.content.includes('@') && session.content.includes('绑定'));
|
|
57
|
+
const shouldNotRecallUserMessage = bindingSession && session.content &&
|
|
58
|
+
!isBindingCommand && (0, helpers_1.checkIrrelevantInput)(bindingSession.state, session.content.trim());
|
|
59
|
+
if (this.config.recallUserMessage && isGroupMessage && session.messageId && !shouldNotRecallUserMessage && !isProactiveMessage) {
|
|
60
|
+
setTimeout(async () => {
|
|
61
|
+
try {
|
|
62
|
+
await session.bot.deleteMessage(session.channelId, session.messageId);
|
|
63
|
+
this.logger.debug('消息', `成功撤回用户QQ(${normalizedQQId})的指令消息 ${session.messageId}`);
|
|
64
|
+
}
|
|
65
|
+
catch (userRecallError) {
|
|
66
|
+
this.logger.error('消息', `QQ(${normalizedQQId})操作失败: 撤回用户指令消息 ${session.messageId} 失败: ${userRecallError.message}`);
|
|
67
|
+
}
|
|
68
|
+
}, this.config.autoRecallTime * 1000);
|
|
69
|
+
this.logger.debug('消息', `已设置 ${this.config.autoRecallTime} 秒后自动撤回用户QQ(${normalizedQQId})的群聊指令消息 ${session.messageId}`);
|
|
70
|
+
}
|
|
71
|
+
else if (shouldNotRecallUserMessage) {
|
|
72
|
+
this.logger.debug('消息', `QQ(${normalizedQQId})在绑定会话中发送聊天消息,跳过撤回用户消息`);
|
|
73
|
+
}
|
|
74
|
+
else if (isProactiveMessage) {
|
|
75
|
+
this.logger.debug('消息', `主动发送的消息,跳过撤回用户消息`);
|
|
76
|
+
}
|
|
77
|
+
// 处理撤回机器人消息 - 只在群聊中撤回机器人自己的消息
|
|
78
|
+
// 检查是否为不应撤回的重要提示消息(只有绑定会话超时提醒)
|
|
79
|
+
const shouldNotRecall = content.some(element => {
|
|
80
|
+
// 检查h.text类型的元素
|
|
81
|
+
if (typeof element === 'string') {
|
|
82
|
+
return element.includes('绑定会话已超时,请重新开始绑定流程');
|
|
83
|
+
}
|
|
84
|
+
// 检查可能的对象结构
|
|
85
|
+
if (typeof element === 'object' && element && 'toString' in element) {
|
|
86
|
+
const text = element.toString();
|
|
87
|
+
return text.includes('绑定会话已超时,请重新开始绑定流程');
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
});
|
|
91
|
+
if (isGroupMessage && messageResult && !shouldNotRecall) {
|
|
92
|
+
// 获取消息ID
|
|
93
|
+
let messageId;
|
|
94
|
+
if (typeof messageResult === 'string') {
|
|
95
|
+
messageId = messageResult;
|
|
96
|
+
}
|
|
97
|
+
else if (Array.isArray(messageResult) && messageResult.length > 0) {
|
|
98
|
+
messageId = messageResult[0];
|
|
99
|
+
}
|
|
100
|
+
else if (messageResult && typeof messageResult === 'object') {
|
|
101
|
+
// 尝试提取各种可能的消息ID格式
|
|
102
|
+
messageId = messageResult.messageId ||
|
|
103
|
+
messageResult.id ||
|
|
104
|
+
messageResult.message_id;
|
|
105
|
+
}
|
|
106
|
+
if (messageId) {
|
|
107
|
+
// 设置定时器延迟撤回
|
|
108
|
+
setTimeout(async () => {
|
|
109
|
+
try {
|
|
110
|
+
await session.bot.deleteMessage(session.channelId, messageId);
|
|
111
|
+
this.logger.debug('消息', `成功撤回机器人消息 ${messageId}`);
|
|
112
|
+
}
|
|
113
|
+
catch (recallError) {
|
|
114
|
+
this.logger.error('消息', `QQ(${normalizedQQId})操作失败: 撤回机器人消息 ${messageId} 失败: ${recallError.message}`);
|
|
115
|
+
}
|
|
116
|
+
}, this.config.autoRecallTime * 1000);
|
|
117
|
+
this.logger.debug('消息', `已设置 ${this.config.autoRecallTime} 秒后自动撤回机器人消息 ${messageId}`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
this.logger.warn('消息', `无法获取消息ID,自动撤回功能无法生效`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.logger.debug('消息', `检测到私聊消息,不撤回机器人回复`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const normalizedUserId = (0, helpers_1.normalizeQQId)(session.userId);
|
|
130
|
+
this.logger.error('消息', `QQ(${normalizedUserId})操作失败: 向QQ(${normalizedUserId})发送消息失败: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 自动设置群昵称
|
|
135
|
+
* @param session Koishi Session对象
|
|
136
|
+
* @param mcUsername MC用户名
|
|
137
|
+
* @param buidUsername B站用户名
|
|
138
|
+
* @param targetUserId 目标用户ID(可选,用于管理员为他人设置)
|
|
139
|
+
* @param specifiedGroupId 指定的群ID(可选,默认使用配置的群ID)
|
|
140
|
+
*/
|
|
141
|
+
async autoSetGroupNickname(session, mcUsername, buidUsername, targetUserId, specifiedGroupId) {
|
|
142
|
+
try {
|
|
143
|
+
// 如果指定了目标用户ID,使用目标用户ID,否则使用session的用户ID
|
|
144
|
+
const actualUserId = targetUserId || session.userId;
|
|
145
|
+
const normalizedUserId = (0, helpers_1.normalizeQQId)(actualUserId);
|
|
146
|
+
// 根据MC绑定状态设置不同的格式(临时用户名视为未绑定)
|
|
147
|
+
const mcInfo = (mcUsername && !mcUsername.startsWith('_temp_')) ? mcUsername : "未绑定";
|
|
148
|
+
const newNickname = `${buidUsername}(ID:${mcInfo})`;
|
|
149
|
+
// 使用指定的群ID,如果没有指定则使用配置的默认群ID
|
|
150
|
+
const targetGroupId = specifiedGroupId || this.config.autoNicknameGroupId;
|
|
151
|
+
this.logger.debug('群昵称设置', `开始处理QQ(${normalizedUserId})的群昵称设置,目标群: ${targetGroupId}`);
|
|
152
|
+
this.logger.debug('群昵称设置', `期望昵称: "${newNickname}"`);
|
|
153
|
+
if (session.bot.internal && targetGroupId) {
|
|
154
|
+
// 使用规范化的QQ号调用OneBot API
|
|
155
|
+
this.logger.debug('群昵称设置', `使用用户ID: ${normalizedUserId}`);
|
|
156
|
+
// 先获取当前群昵称进行比对
|
|
157
|
+
try {
|
|
158
|
+
this.logger.debug('群昵称设置', `正在获取QQ(${normalizedUserId})在群${targetGroupId}的当前昵称...`);
|
|
159
|
+
const currentGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
160
|
+
const currentNickname = currentGroupInfo.card || currentGroupInfo.nickname || '';
|
|
161
|
+
this.logger.debug('群昵称设置', `当前昵称: "${currentNickname}"`);
|
|
162
|
+
// 如果当前昵称和目标昵称一致,跳过修改
|
|
163
|
+
if (currentNickname === newNickname) {
|
|
164
|
+
this.logger.info('群昵称设置', `QQ(${normalizedUserId})群昵称已经是"${newNickname}",跳过修改`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// 昵称不一致,执行修改
|
|
168
|
+
this.logger.debug('群昵称设置', `昵称不一致,正在修改群昵称...`);
|
|
169
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, newNickname);
|
|
170
|
+
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称从"${currentNickname}"修改为"${newNickname}"`, true);
|
|
171
|
+
// 验证设置是否生效 - 再次获取群昵称确认
|
|
172
|
+
try {
|
|
173
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
174
|
+
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
175
|
+
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
176
|
+
if (verifyNickname === newNickname) {
|
|
177
|
+
this.logger.info('群昵称设置', `✅ 验证成功,群昵称已生效: "${verifyNickname}"`, true);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${newNickname}",实际"${verifyNickname}",可能是权限不足或API延迟`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (verifyError) {
|
|
184
|
+
this.logger.warn('群昵称设置', `无法验证群昵称设置结果: ${verifyError.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (getInfoError) {
|
|
188
|
+
// 如果获取当前昵称失败,直接尝试设置新昵称
|
|
189
|
+
this.logger.warn('群昵称设置', `获取QQ(${normalizedUserId})当前群昵称失败: ${getInfoError.message}`);
|
|
190
|
+
this.logger.warn('群昵称设置', `错误详情: ${JSON.stringify(getInfoError)}`);
|
|
191
|
+
this.logger.debug('群昵称设置', `将直接尝试设置新昵称...`);
|
|
192
|
+
try {
|
|
193
|
+
await session.bot.internal.setGroupCard(targetGroupId, normalizedUserId, newNickname);
|
|
194
|
+
this.logger.info('群昵称设置', `成功在群${targetGroupId}中将QQ(${normalizedUserId})群昵称设置为: ${newNickname}`, true);
|
|
195
|
+
// 验证设置是否生效
|
|
196
|
+
try {
|
|
197
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
|
|
198
|
+
const verifyGroupInfo = await session.bot.internal.getGroupMemberInfo(targetGroupId, normalizedUserId);
|
|
199
|
+
const verifyNickname = verifyGroupInfo.card || verifyGroupInfo.nickname || '';
|
|
200
|
+
if (verifyNickname === newNickname) {
|
|
201
|
+
this.logger.info('群昵称设置', `✅ 验证成功,群昵称已生效: "${verifyNickname}"`, true);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.logger.warn('群昵称设置', `⚠️ 验证失败,期望"${newNickname}",实际"${verifyNickname}",可能是权限不足`);
|
|
205
|
+
this.logger.warn('群昵称设置', `建议检查: 1.机器人是否为群管理员 2.群设置是否允许管理员修改昵称 3.OneBot实现是否支持该功能`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (verifyError) {
|
|
209
|
+
this.logger.warn('群昵称设置', `无法验证群昵称设置结果: ${verifyError.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (setCardError) {
|
|
213
|
+
this.logger.error('群昵称设置', `设置群昵称失败: ${setCardError.message}`);
|
|
214
|
+
this.logger.error('群昵称设置', `错误详情: ${JSON.stringify(setCardError)}`);
|
|
215
|
+
throw setCardError;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (!session.bot.internal) {
|
|
220
|
+
this.logger.debug('群昵称设置', `QQ(${normalizedUserId})bot不支持OneBot内部API,跳过自动群昵称设置`);
|
|
221
|
+
}
|
|
222
|
+
else if (!targetGroupId) {
|
|
223
|
+
this.logger.debug('群昵称设置', `QQ(${normalizedUserId})未配置自动群昵称设置目标群,跳过群昵称设置`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const actualUserId = targetUserId || session.userId;
|
|
228
|
+
const normalizedUserId = (0, helpers_1.normalizeQQId)(actualUserId);
|
|
229
|
+
this.logger.error('群昵称设置', `QQ(${normalizedUserId})自动群昵称设置失败: ${error.message}`);
|
|
230
|
+
this.logger.error('群昵称设置', `完整错误信息: ${JSON.stringify(error)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
exports.MessageUtils = MessageUtils;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 简单的请求限流器
|
|
3
|
+
* 用于控制特定时间窗口内的请求频率
|
|
4
|
+
*/
|
|
5
|
+
export declare class RateLimiter {
|
|
6
|
+
private requestTimes;
|
|
7
|
+
private limit;
|
|
8
|
+
private timeWindow;
|
|
9
|
+
/**
|
|
10
|
+
* 创建限流器实例
|
|
11
|
+
* @param limit 时间窗口内允许的最大请求数
|
|
12
|
+
* @param timeWindowMs 时间窗口大小(毫秒)
|
|
13
|
+
*/
|
|
14
|
+
constructor(limit?: number, timeWindowMs?: number);
|
|
15
|
+
/**
|
|
16
|
+
* 检查是否允许新请求
|
|
17
|
+
* @param key 请求的唯一标识(如用户ID)
|
|
18
|
+
* @returns 是否允许请求
|
|
19
|
+
*/
|
|
20
|
+
canMakeRequest(key: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* 记录新请求
|
|
23
|
+
* @param key 请求的唯一标识(如用户ID)
|
|
24
|
+
*/
|
|
25
|
+
recordRequest(key: string): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RateLimiter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 简单的请求限流器
|
|
6
|
+
* 用于控制特定时间窗口内的请求频率
|
|
7
|
+
*/
|
|
8
|
+
class RateLimiter {
|
|
9
|
+
requestTimes = {};
|
|
10
|
+
limit;
|
|
11
|
+
timeWindow;
|
|
12
|
+
/**
|
|
13
|
+
* 创建限流器实例
|
|
14
|
+
* @param limit 时间窗口内允许的最大请求数
|
|
15
|
+
* @param timeWindowMs 时间窗口大小(毫秒)
|
|
16
|
+
*/
|
|
17
|
+
constructor(limit = 10, timeWindowMs = 3000) {
|
|
18
|
+
this.limit = limit;
|
|
19
|
+
this.timeWindow = timeWindowMs;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 检查是否允许新请求
|
|
23
|
+
* @param key 请求的唯一标识(如用户ID)
|
|
24
|
+
* @returns 是否允许请求
|
|
25
|
+
*/
|
|
26
|
+
canMakeRequest(key) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (!this.requestTimes[key]) {
|
|
29
|
+
this.requestTimes[key] = [];
|
|
30
|
+
}
|
|
31
|
+
// 清理过期请求时间
|
|
32
|
+
this.requestTimes[key] = this.requestTimes[key].filter(time => now - time < this.timeWindow);
|
|
33
|
+
// 检查是否超过限制
|
|
34
|
+
return this.requestTimes[key].length < this.limit;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 记录新请求
|
|
38
|
+
* @param key 请求的唯一标识(如用户ID)
|
|
39
|
+
*/
|
|
40
|
+
recordRequest(key) {
|
|
41
|
+
if (!this.requestTimes[key]) {
|
|
42
|
+
this.requestTimes[key] = [];
|
|
43
|
+
}
|
|
44
|
+
this.requestTimes[key].push(Date.now());
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.RateLimiter = RateLimiter;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { LoggerService } from './logger';
|
|
3
|
+
/**
|
|
4
|
+
* 交互式绑定会话状态接口
|
|
5
|
+
*/
|
|
6
|
+
export interface BindingSession {
|
|
7
|
+
userId: string;
|
|
8
|
+
channelId: string;
|
|
9
|
+
state: 'waiting_mc_username' | 'waiting_buid';
|
|
10
|
+
startTime: number;
|
|
11
|
+
timeout: NodeJS.Timeout;
|
|
12
|
+
mcUsername?: string;
|
|
13
|
+
mcUuid?: string;
|
|
14
|
+
invalidInputCount?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 绑定会话管理器
|
|
18
|
+
* 管理交互式绑定流程的会话状态
|
|
19
|
+
*/
|
|
20
|
+
export declare class SessionManager {
|
|
21
|
+
private sessions;
|
|
22
|
+
private sessionTimeout;
|
|
23
|
+
private ctx;
|
|
24
|
+
private logger;
|
|
25
|
+
/**
|
|
26
|
+
* 创建会话管理器实例
|
|
27
|
+
* @param ctx Koishi Context
|
|
28
|
+
* @param logger 日志服务
|
|
29
|
+
* @param sessionTimeout 会话超时时间(毫秒)
|
|
30
|
+
*/
|
|
31
|
+
constructor(ctx: Context, logger: LoggerService, sessionTimeout?: number);
|
|
32
|
+
/**
|
|
33
|
+
* 生成会话键
|
|
34
|
+
*/
|
|
35
|
+
private getSessionKey;
|
|
36
|
+
/**
|
|
37
|
+
* 创建新的绑定会话
|
|
38
|
+
* @param userId 用户ID
|
|
39
|
+
* @param channelId 频道ID
|
|
40
|
+
*/
|
|
41
|
+
createSession(userId: string, channelId: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* 获取绑定会话
|
|
44
|
+
* @param userId 用户ID
|
|
45
|
+
* @param channelId 频道ID
|
|
46
|
+
* @returns 绑定会话或null
|
|
47
|
+
*/
|
|
48
|
+
getSession(userId: string, channelId: string): BindingSession | null;
|
|
49
|
+
/**
|
|
50
|
+
* 更新绑定会话
|
|
51
|
+
* @param userId 用户ID
|
|
52
|
+
* @param channelId 频道ID
|
|
53
|
+
* @param updates 更新的字段
|
|
54
|
+
*/
|
|
55
|
+
updateSession(userId: string, channelId: string, updates: Partial<BindingSession>): void;
|
|
56
|
+
/**
|
|
57
|
+
* 移除绑定会话
|
|
58
|
+
* @param userId 用户ID
|
|
59
|
+
* @param channelId 频道ID
|
|
60
|
+
*/
|
|
61
|
+
removeSession(userId: string, channelId: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* 获取当前活跃会话数量
|
|
64
|
+
*/
|
|
65
|
+
getActiveSessionCount(): number;
|
|
66
|
+
/**
|
|
67
|
+
* 清除所有会话(通常在插件卸载时调用)
|
|
68
|
+
*/
|
|
69
|
+
clearAllSessions(): void;
|
|
70
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionManager = void 0;
|
|
4
|
+
const koishi_1 = require("koishi");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
/**
|
|
7
|
+
* 绑定会话管理器
|
|
8
|
+
* 管理交互式绑定流程的会话状态
|
|
9
|
+
*/
|
|
10
|
+
class SessionManager {
|
|
11
|
+
sessions = new Map();
|
|
12
|
+
sessionTimeout;
|
|
13
|
+
ctx;
|
|
14
|
+
logger;
|
|
15
|
+
/**
|
|
16
|
+
* 创建会话管理器实例
|
|
17
|
+
* @param ctx Koishi Context
|
|
18
|
+
* @param logger 日志服务
|
|
19
|
+
* @param sessionTimeout 会话超时时间(毫秒)
|
|
20
|
+
*/
|
|
21
|
+
constructor(ctx, logger, sessionTimeout = 3 * 60 * 1000) {
|
|
22
|
+
this.ctx = ctx;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.sessionTimeout = sessionTimeout;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 生成会话键
|
|
28
|
+
*/
|
|
29
|
+
getSessionKey(userId, channelId) {
|
|
30
|
+
return `${(0, helpers_1.normalizeQQId)(userId)}_${channelId}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 创建新的绑定会话
|
|
34
|
+
* @param userId 用户ID
|
|
35
|
+
* @param channelId 频道ID
|
|
36
|
+
*/
|
|
37
|
+
createSession(userId, channelId) {
|
|
38
|
+
const sessionKey = this.getSessionKey(userId, channelId);
|
|
39
|
+
// 如果已有会话,先清理
|
|
40
|
+
const existingSession = this.sessions.get(sessionKey);
|
|
41
|
+
if (existingSession) {
|
|
42
|
+
clearTimeout(existingSession.timeout);
|
|
43
|
+
this.sessions.delete(sessionKey);
|
|
44
|
+
}
|
|
45
|
+
// 创建超时定时器
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
this.sessions.delete(sessionKey);
|
|
48
|
+
// 发送超时消息,@用户
|
|
49
|
+
const normalizedUser = (0, helpers_1.normalizeQQId)(userId);
|
|
50
|
+
this.ctx.bots.forEach(bot => {
|
|
51
|
+
bot.sendMessage(channelId, [koishi_1.h.at(normalizedUser), koishi_1.h.text(' 绑定会话已超时,请重新开始绑定流程\n\n⚠️ 温馨提醒:若在管理员多次提醒后仍不配合绑定账号信息,将按群规进行相应处理。')]).catch(() => { });
|
|
52
|
+
});
|
|
53
|
+
this.logger.info('交互绑定', `QQ(${normalizedUser})的绑定会话因超时被清理`, true);
|
|
54
|
+
}, this.sessionTimeout);
|
|
55
|
+
// 创建新会话
|
|
56
|
+
const session = {
|
|
57
|
+
userId: (0, helpers_1.normalizeQQId)(userId),
|
|
58
|
+
channelId,
|
|
59
|
+
state: 'waiting_buid',
|
|
60
|
+
startTime: Date.now(),
|
|
61
|
+
timeout
|
|
62
|
+
};
|
|
63
|
+
this.sessions.set(sessionKey, session);
|
|
64
|
+
this.logger.info('交互绑定', `为QQ(${(0, helpers_1.normalizeQQId)(userId)})创建了新的绑定会话`, true);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 获取绑定会话
|
|
68
|
+
* @param userId 用户ID
|
|
69
|
+
* @param channelId 频道ID
|
|
70
|
+
* @returns 绑定会话或null
|
|
71
|
+
*/
|
|
72
|
+
getSession(userId, channelId) {
|
|
73
|
+
const sessionKey = this.getSessionKey(userId, channelId);
|
|
74
|
+
return this.sessions.get(sessionKey) || null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 更新绑定会话
|
|
78
|
+
* @param userId 用户ID
|
|
79
|
+
* @param channelId 频道ID
|
|
80
|
+
* @param updates 更新的字段
|
|
81
|
+
*/
|
|
82
|
+
updateSession(userId, channelId, updates) {
|
|
83
|
+
const sessionKey = this.getSessionKey(userId, channelId);
|
|
84
|
+
const session = this.sessions.get(sessionKey);
|
|
85
|
+
if (session) {
|
|
86
|
+
Object.assign(session, updates);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 移除绑定会话
|
|
91
|
+
* @param userId 用户ID
|
|
92
|
+
* @param channelId 频道ID
|
|
93
|
+
*/
|
|
94
|
+
removeSession(userId, channelId) {
|
|
95
|
+
const sessionKey = this.getSessionKey(userId, channelId);
|
|
96
|
+
const session = this.sessions.get(sessionKey);
|
|
97
|
+
if (session) {
|
|
98
|
+
clearTimeout(session.timeout);
|
|
99
|
+
this.sessions.delete(sessionKey);
|
|
100
|
+
this.logger.info('交互绑定', `移除了QQ(${(0, helpers_1.normalizeQQId)(userId)})的绑定会话`, true);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 获取当前活跃会话数量
|
|
105
|
+
*/
|
|
106
|
+
getActiveSessionCount() {
|
|
107
|
+
return this.sessions.size;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 清除所有会话(通常在插件卸载时调用)
|
|
111
|
+
*/
|
|
112
|
+
clearAllSessions() {
|
|
113
|
+
for (const [, session] of this.sessions.entries()) {
|
|
114
|
+
clearTimeout(session.timeout);
|
|
115
|
+
}
|
|
116
|
+
this.sessions.clear();
|
|
117
|
+
this.logger.info('交互绑定', '已清除所有绑定会话', true);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.SessionManager = SessionManager;
|