UNPKG

@hambur/koishi-plugin-auto-kick

Version:

自动踢出黑名单用户的群管理插件

1,154 lines (1,150 loc) 49.2 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var name = "auto-kick"; var usage = ` ## 功能说明 高性能自动踢出指定的黑名单用户,支持大规模群组监控: - 支持 QQ 号黑名单和昵称关键词黑名单 - **智能跳过群主和管理员,避免误踢高权限用户** - **新增:自动同意加群请求功能,自动拒绝黑名单用户** - **新增:QQ机器人离线监控和邮件通知功能(直接调用NapCat API检测真实连接状态)** - **优先获取群昵称,确保准确识别群内违规昵称** - **新增:扩展昵称检查机制 - 新成员进群3分钟后检查,然后每小时检查一次,持续12小时** - 优化的黑名单匹配算法 (O(1) 查找) - 昵称支持精确匹配、包含匹配和正则表达式匹配 - 批量扫描和限流控制 - 并发控制和错误重试 - 踢人结果验证,防止权限不足时的误报 ## 使用方法 1. 在配置中添加要屏蔽的 QQ 号 2. 在配置中添加要屏蔽的昵称关键词(如 "群主"、"管理员") 3. **启用自动同意加群功能,机器人将自动处理加群请求** 4. **配置邮件通知服务器地址和NapCat API地址,启用离线监控功能** 5. 选择昵称匹配模式:包含匹配(默认)或正则表达式 6. 启用扩展昵称检查功能,新成员进群后将在3分钟、1小时、2小时...12小时后持续检查昵称变更 7. 使用 Koishi 的群组过滤器选择需要监控的群 8. 启用插件即可自动工作 ## 工作原理 - **群昵称优先**:优先获取和检查群昵称,确保准确识别群内违规昵称 - **离线监控**:直接调用NapCat API检测真实QQ连接状态,离线时自动发送邮件通知 - **加群请求处理**:自动同意非黑名单用户的加群请求,拒绝黑名单用户 - **机器人进群时**:自动扫描现有群成员,踢出黑名单用户 - **新成员加入时**:实时检查新加入的成员,发现黑名单用户立即踢出 - **扩展昵称检查**:新成员进群3分钟后首次检查,然后每小时检查一次,持续12小时监控昵称变更 - **验证机制**:踢人后验证用户是否真的被踢出,确保操作成功 `; var Config = import_koishi.Schema.intersect([ // 基础配置 import_koishi.Schema.object({ blacklist: import_koishi.Schema.array(String).role("table").description("黑名单QQ号列表").default([]), nicknameBlacklist: import_koishi.Schema.array(String).role("table").description('昵称黑名单关键词列表(如"群主"、"管理员")').default([]), enableJoinScan: import_koishi.Schema.boolean().description("机器人进群时扫描现有成员").default(true), enableMemberJoin: import_koishi.Schema.boolean().description("监听新成员加入").default(true), enableExtendedNicknameCheck: import_koishi.Schema.boolean().description("启用扩展昵称检查(3分钟后首次检查,然后每小时检查)").default(true), firstCheckDelay: import_koishi.Schema.number().description("新成员首次昵称检查延迟(毫秒)").default(18e4), // 3分钟 extendedCheckInterval: import_koishi.Schema.number().description("扩展检查间隔(毫秒)").default(36e5), // 1小时 extendedCheckHours: import_koishi.Schema.number().description("扩展检查持续时间(小时)").default(12), kickFailMessage: import_koishi.Schema.string().description("踢人失败时的提醒消息 (支持 {user} 和 {reason} 占位符)").default("检测到黑名单用户 {user},但权限不足无法踢出 (原因: {reason})"), notifyAdmins: import_koishi.Schema.boolean().description("是否通知管理员黑名单用户行为").default(false), adminNotifyMessage: import_koishi.Schema.string().description("通知管理员的消息模板 (支持 {user} 和 {reason} 占位符)").default("管理员注意:检测到黑名单用户 {user} 尝试进入群聊 (原因: {reason})"), logLevel: import_koishi.Schema.union([ import_koishi.Schema.const("debug").description("调试"), import_koishi.Schema.const("info").description("信息"), import_koishi.Schema.const("warn").description("警告"), import_koishi.Schema.const("error").description("错误") ]).description("日志级别").default("info") }).description("基础设置"), // 自动同意加群配置 import_koishi.Schema.object({ enableAutoApprove: import_koishi.Schema.boolean().description("启用自动同意加群请求").default(false), autoApproveMessage: import_koishi.Schema.string().description("同意加群时的消息").default("欢迎加入群聊!"), autoRejectMessage: import_koishi.Schema.string().description("拒绝加群时的消息").default("抱歉,您的加群申请未通过审核。"), notifyAutoApprove: import_koishi.Schema.boolean().description("是否通知管理员自动处理加群请求的结果").default(true), autoApproveNotifyMessage: import_koishi.Schema.string().description("自动同意加群时通知管理员的消息 (支持 {user} 占位符)").default("✅ 自动同意用户 {user} 的加群申请"), autoRejectNotifyMessage: import_koishi.Schema.string().description("自动拒绝加群时通知管理员的消息 (支持 {user} 和 {reason} 占位符)").default("❌ 自动拒绝黑名单用户 {user} 的加群申请 (原因: {reason})") }).description("自动同意加群设置"), // 离线监控和邮件通知配置 import_koishi.Schema.object({ enableOfflineMonitor: import_koishi.Schema.boolean().description("启用QQ机器人离线监控和邮件通知").default(false), emailServerUrl: import_koishi.Schema.string().description("邮件服务器API地址 (例如: http://localhost:8085)").default("http://localhost:8085"), napCatApiUrl: import_koishi.Schema.string().description("NapCat API地址 (例如: http://127.0.0.1:3001)").default("http://127.0.0.1:3001"), botName: import_koishi.Schema.string().description("机器人名称 (用于邮件通知)").default("QQ群管机器人"), offlineCheckInterval: import_koishi.Schema.number().description("离线检查间隔(毫秒)").default(3e4), maxOfflineNotifications: import_koishi.Schema.number().description("最大离线通知次数 (防止邮件轰炸)").default(5), offlineNotificationCooldown: import_koishi.Schema.number().description("离线通知冷却时间(毫秒) - 在此时间内不重复发送").default(18e5) // 30分钟 }).description("离线监控设置"), // 性能配置 import_koishi.Schema.object({ scanDelay: import_koishi.Schema.number().description("机器人进群后延迟扫描时间(毫秒)").default(5e3), batchSize: import_koishi.Schema.number().description("批量处理成员数量").default(50), maxConcurrent: import_koishi.Schema.number().description("最大并发踢人数量").default(3), kickDelay: import_koishi.Schema.number().description("踢人操作间隔(毫秒)").default(2e3), retryAttempts: import_koishi.Schema.number().description("失败重试次数").default(3), retryDelay: import_koishi.Schema.number().description("重试延迟(毫秒)").default(5e3) }).description("性能优化设置"), // 高级功能 import_koishi.Schema.object({ enableStats: import_koishi.Schema.boolean().description("启用性能统计").default(true), skipBotMembers: import_koishi.Schema.boolean().description("跳过机器人成员").default(true), skipAdmins: import_koishi.Schema.boolean().description("跳过群主和管理员(强烈推荐)").default(true), verifyKickResult: import_koishi.Schema.boolean().description("验证踢人结果(检查用户是否真的被踢出)").default(true), verifyDelay: import_koishi.Schema.number().description("踢人后验证延迟(毫秒)").default(2e3), verifyTimeout: import_koishi.Schema.number().description("验证超时时间(毫秒)").default(1e4), nicknameMatchMode: import_koishi.Schema.union([ import_koishi.Schema.const("exact").description("精确匹配"), import_koishi.Schema.const("contains").description("包含匹配"), import_koishi.Schema.const("regex").description("正则表达式") ]).description("昵称匹配模式").default("exact") }).description("高级功能设置") ]); var BlacklistManager = class { static { __name(this, "BlacklistManager"); } blacklistSet; nicknameBlacklist; stats; constructor(blacklist, nicknameBlacklist) { this.blacklistSet = /* @__PURE__ */ new Set(); this.nicknameBlacklist = []; this.stats = { totalScanned: 0, totalKicked: 0, totalFailed: 0, totalApproved: 0, totalRejected: 0, lastScanTime: 0, averageScanTime: 0, totalOfflineNotifications: 0, lastOfflineTime: 0, totalExtendedChecks: 0 }; this.updateBlacklist(blacklist, nicknameBlacklist); } updateBlacklist(blacklist, nicknameBlacklist) { this.blacklistSet.clear(); const validEntries = blacklist.filter((userId) => userId && userId.trim().length > 0); const uniqueEntries = [...new Set(validEntries.map((userId) => userId.trim()))]; for (const userId of uniqueEntries) { this.blacklistSet.add(userId); } this.nicknameBlacklist = nicknameBlacklist.filter((keyword) => keyword && keyword.trim().length > 0).map((keyword) => keyword.trim()); } isBlacklisted(userId) { return this.blacklistSet.has(userId); } isNicknameBlacklisted(nickname, matchMode) { if (!nickname || this.nicknameBlacklist.length === 0) { return { isBlacklisted: false }; } for (const keyword of this.nicknameBlacklist) { try { if (matchMode === "exact") { if (nickname.toLowerCase() === keyword.toLowerCase()) { return { isBlacklisted: true, matchedKeyword: keyword }; } } else if (matchMode === "regex") { const regex = new RegExp(keyword, "i"); if (regex.test(nickname)) { return { isBlacklisted: true, matchedKeyword: keyword }; } } else { if (nickname.toLowerCase().includes(keyword.toLowerCase())) { return { isBlacklisted: true, matchedKeyword: keyword }; } } } catch (error) { if (nickname.toLowerCase().includes(keyword.toLowerCase())) { return { isBlacklisted: true, matchedKeyword: keyword }; } } } return { isBlacklisted: false }; } getStats() { return { ...this.stats }; } updateStats(scanned, kicked, failed, scanTime, approved, rejected) { this.stats.totalScanned += scanned; this.stats.totalKicked += kicked; this.stats.totalFailed += failed; if (approved !== void 0) this.stats.totalApproved += approved; if (rejected !== void 0) this.stats.totalRejected += rejected; this.stats.lastScanTime = scanTime; const totalScans = Math.max(1, this.stats.totalScanned / Math.max(1, scanned)); this.stats.averageScanTime = (this.stats.averageScanTime * (totalScans - 1) + scanTime) / totalScans; } updateApprovalStats(approved, rejected) { this.stats.totalApproved += approved; this.stats.totalRejected += rejected; } updateOfflineStats() { this.stats.totalOfflineNotifications++; this.stats.lastOfflineTime = Date.now(); } updateExtendedCheckStats() { this.stats.totalExtendedChecks++; } size() { return this.blacklistSet.size; } nicknameSize() { return this.nicknameBlacklist.length; } }; var OfflineMonitor = class { constructor(ctx, config, logger, blacklistManager) { this.ctx = ctx; this.config = config; this.logger = logger; this.blacklistManager = blacklistManager; this.napCatApiUrl = config.napCatApiUrl.replace(/\/$/, ""); } static { __name(this, "OfflineMonitor"); } isOnline = false; lastOnlineTime = Date.now(); notificationCount = 0; lastNotificationTime = 0; checkInterval = null; napCatApiUrl; start() { if (!this.config.enableOfflineMonitor) return; this.logger.info("🔍 启动QQ机器人离线监控(使用NapCat API直接检测)"); this.ctx.on("ready", () => { this.handleOnline(); }); this.ctx.on("bot-removed", () => { this.handleOffline("Koishi框架层断开连接"); }); this.checkInterval = setInterval(() => { this.checkNapCatConnectionStatus(); }, this.config.offlineCheckInterval); this.logger.info(`📊 离线监控配置: NapCat API=${this.napCatApiUrl}, 检查间隔=${this.config.offlineCheckInterval / 1e3}秒`); } stop() { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; } } handleOnline() { if (!this.isOnline) { this.isOnline = true; this.lastOnlineTime = Date.now(); this.notificationCount = 0; this.logger.info("✅ QQ机器人已上线"); } } handleOffline(reason) { if (this.isOnline) { this.isOnline = false; this.logger.warn(`❌ QQ机器人离线: ${reason}`); this.sendOfflineNotification(reason); } } /** * 直接调用NapCat API检查连接状态 */ async checkNapCatConnectionStatus() { try { const statusResult = await this.callNapCatAPI("/get_status"); if (statusResult === true) { this.handleOnline(); return; } else if (statusResult === false) { this.handleOffline("NapCat API检测到QQ离线"); return; } this.logger.debug("get_status API无响应,尝试备用API确认连接性"); const loginInfoResult = await this.callNapCatAPI("/get_login_info"); if (loginInfoResult === null) { this.handleOffline("NapCat API连接失败"); } else if (loginInfoResult === false) { this.handleOffline("备用API检测到QQ离线"); } else { this.logger.debug("备用API连接正常,但无法确定QQ在线状态"); } } catch (error) { this.logger.warn(`NapCat API连接检查失败: ${error.message}`); this.handleOffline(`NapCat API异常: ${error.message}`); } } /** * 调用NapCat API - 修改返回值以区分不同情况 */ async callNapCatAPI(endpoint) { try { const url = `${this.napCatApiUrl}${endpoint}`; const urlObj = new URL(url); const isHttps = urlObj.protocol === "https:"; const httpModule = isHttps ? require("https") : require("http"); const postData = JSON.stringify({}); const options = { hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), "User-Agent": "Koishi-Auto-Kick-Plugin/1.0" }, timeout: 1e4 // 10秒超时 }; return new Promise((resolve, reject) => { const req = httpModule.request(options, (res) => { let responseData = ""; res.on("data", (chunk) => { responseData += chunk; }); res.on("end", () => { try { if (res.statusCode >= 200 && res.statusCode < 300) { const result = JSON.parse(responseData); if (result.status === "ok" && result.retcode === 0) { if (endpoint === "/get_status") { const isOnline = result.data?.online === true; this.logger.debug(`NapCat API状态检查: online=${isOnline}, good=${result.data?.good}`); resolve(isOnline); } else { resolve(true); } } else { this.logger.debug(`NapCat API返回错误: status=${result.status}, retcode=${result.retcode}, message=${result.message}`); if (endpoint === "/get_status") { resolve(false); } else { resolve(false); } } } else { this.logger.debug(`NapCat API错误: HTTP ${res.statusCode} - ${responseData}`); resolve(false); } } catch (parseError) { this.logger.debug(`解析NapCat API响应失败: ${parseError.message}`); resolve(null); } }); }); req.on("error", (error) => { this.logger.debug(`NapCat API请求失败: ${error.message}`); resolve(null); }); req.on("timeout", () => { this.logger.debug("NapCat API请求超时"); req.destroy(); resolve(null); }); req.write(postData); req.end(); }); } catch (error) { this.logger.debug(`调用NapCat API异常: ${error.message}`); return null; } } async sendOfflineNotification(errorMessage) { try { if (this.notificationCount >= this.config.maxOfflineNotifications) { this.logger.warn(`⚠️ 已达到最大离线通知次数 (${this.config.maxOfflineNotifications}),跳过通知`); return; } const timeSinceLastNotification = Date.now() - this.lastNotificationTime; if (timeSinceLastNotification < this.config.offlineNotificationCooldown) { const remainingCooldown = Math.ceil((this.config.offlineNotificationCooldown - timeSinceLastNotification) / 6e4); this.logger.warn(`⏰ 离线通知冷却中,还需等待 ${remainingCooldown} 分钟`); return; } const currentTime = (/* @__PURE__ */ new Date()).toLocaleString("zh-CN"); const detailedErrorMessage = `${errorMessage} 检测方式: 直接调用NapCat API NapCat API地址: ${this.napCatApiUrl} 离线时间: ${currentTime} 已发送通知次数: ${this.notificationCount + 1}/${this.config.maxOfflineNotifications}`; this.logger.info(`📧 发送QQ机器人离线通知: ${this.config.botName}`); const success = await this.callEmailAPI(this.config.botName, detailedErrorMessage); if (success) { this.notificationCount++; this.lastNotificationTime = Date.now(); this.blacklistManager.updateOfflineStats(); this.logger.info(`✅ 离线通知发送成功 (${this.notificationCount}/${this.config.maxOfflineNotifications})`); } else { this.logger.error("❌ 离线通知发送失败"); } } catch (error) { this.logger.error(`发送离线通知异常: ${error.message}`); } } async callEmailAPI(botName, errorMessage) { try { const url = `${this.config.emailServerUrl.replace(/\/$/, "")}/notification/email/qq-bot-offline/quick`; const urlObj = new URL(url); const isHttps = urlObj.protocol === "https:"; const httpModule = isHttps ? require("https") : require("http"); const postData = new URLSearchParams({ botName, errorMessage }).toString(); const options = { hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": Buffer.byteLength(postData), "User-Agent": "Koishi-Auto-Kick-Plugin/1.0" }, timeout: 1e4 // 10秒超时 }; return new Promise((resolve, reject) => { const req = httpModule.request(options, (res) => { let responseData = ""; res.on("data", (chunk) => { responseData += chunk; }); res.on("end", () => { try { if (res.statusCode >= 200 && res.statusCode < 300) { this.logger.debug(`邮件API响应: ${responseData}`); resolve(true); } else { this.logger.error(`邮件API错误: HTTP ${res.statusCode} - ${responseData}`); resolve(false); } } catch (parseError) { this.logger.error(`解析邮件API响应失败: ${parseError.message}`); resolve(false); } }); }); req.on("error", (error) => { this.logger.error(`邮件API请求失败: ${error.message}`); resolve(false); }); req.on("timeout", () => { this.logger.error("邮件API请求超时"); req.destroy(); resolve(false); }); req.write(postData); req.end(); }); } catch (error) { this.logger.error(`调用邮件API异常: ${error.message}`); return false; } } getStatus() { return { isOnline: this.isOnline, lastOnlineTime: this.lastOnlineTime, notificationCount: this.notificationCount, lastNotificationTime: this.lastNotificationTime, napCatApiUrl: this.napCatApiUrl }; } }; var ConcurrencyController = class { constructor(maxConcurrent) { this.maxConcurrent = maxConcurrent; } static { __name(this, "ConcurrencyController"); } running = 0; queue = []; async execute(task) { return new Promise((resolve, reject) => { this.queue.push(async () => { try { const result = await task(); resolve(result); } catch (error) { reject(error); } }); this.processQueue(); }); } async processQueue() { if (this.running >= this.maxConcurrent || this.queue.length === 0) { return; } this.running++; const task = this.queue.shift(); try { await task(); } finally { this.running--; this.processQueue(); } } }; function apply(ctx, config) { const logger = new import_koishi.Logger(name); logger.level = import_koishi.Logger[config.logLevel.toUpperCase()]; const blacklistManager = new BlacklistManager(config.blacklist, config.nicknameBlacklist); const concurrencyController = new ConcurrencyController(config.maxConcurrent); const offlineMonitor = new OfflineMonitor(ctx, config, logger, blacklistManager); const extendedCheckCache = /* @__PURE__ */ new Map(); const configCheckInterval = setInterval(() => { blacklistManager.updateBlacklist(config.blacklist, config.nicknameBlacklist); }, 6e4); async function isUserAdmin(session, userId) { try { const memberInfo = await session.bot.getGuildMember(session.guildId, userId); if (!memberInfo) { return false; } const adminRoles = ["owner", "admin", "administrator", "moderator"]; const memberRoles = [ memberInfo.role, memberInfo.roles, memberInfo.permissions, memberInfo.user?.role, memberInfo.user?.roles ].flat().filter(Boolean); for (const role of memberRoles) { if (typeof role === "string") { if (adminRoles.includes(role.toLowerCase())) { return true; } } else if (typeof role === "object" && role.name) { if (adminRoles.includes(role.name.toLowerCase())) { return true; } } } if (memberInfo.permissions) { const kickPermissions = ["kick_members", "kick", "manage_members", "administrator"]; for (const perm of kickPermissions) { if (memberInfo.permissions[perm] === true) { return true; } } } return false; } catch (error) { return true; } } __name(isUserAdmin, "isUserAdmin"); async function getUserNickname(member, session, bot, userId, guildId) { let groupNickname = null; let qqNickname = null; if (userId && guildId) { try { let onebotMember = null; const apiMethods = [ { obj: session?.onebot, methods: ["getGroupMemberInfo", "get_group_member_info"] }, { obj: bot?.internal, methods: ["getGroupMemberInfo", "get_group_member_info"] }, { obj: session?.bot?.internal, methods: ["getGroupMemberInfo", "get_group_member_info"] } ]; for (const apiMethod of apiMethods) { if (onebotMember) break; if (apiMethod.obj) { for (const method of apiMethod.methods) { if (typeof apiMethod.obj[method] === "function") { try { onebotMember = await apiMethod.obj[method](guildId, userId); break; } catch (error) { } } } } } if (onebotMember) { const cardValue = onebotMember.card || onebotMember.data?.card; if (cardValue && typeof cardValue === "string" && cardValue.trim()) { groupNickname = cardValue.trim(); } const nicknameValue = onebotMember.nickname || onebotMember.data?.nickname; if (nicknameValue && typeof nicknameValue === "string" && nicknameValue.trim()) { qqNickname = nicknameValue.trim(); } } } catch (error) { } } if (!groupNickname && member) { const groupNicknameFields = ["card", "nick", "groupCard", "groupNick", "displayName"]; for (const field of groupNicknameFields) { const value = member[field]; if (value && typeof value === "string" && value.trim()) { groupNickname = value.trim(); break; } } } if (!qqNickname && member) { const qqNicknameFields = ["user.name", "user.username", "nickname", "name", "username"]; for (const field of qqNicknameFields) { const keys = field.split("."); let value = member; for (const key of keys) { value = value?.[key]; } if (value && typeof value === "string" && value.trim()) { qqNickname = value.trim(); break; } } } if (!qqNickname && session) { const sessionFields = ["username", "author.name", "user.name"]; for (const field of sessionFields) { const keys = field.split("."); let value = session; for (const key of keys) { value = value?.[key]; } if (value && typeof value === "string" && value.trim()) { qqNickname = value.trim(); break; } } } if (groupNickname) { return { nickname: groupNickname, isGroupNickname: true }; } else if (qqNickname) { return { nickname: qqNickname, isGroupNickname: false }; } else { return { nickname: null, isGroupNickname: false }; } } __name(getUserNickname, "getUserNickname"); async function verifyUserKicked(session, userId) { try { const members = await session.bot.getGuildMemberList(session.guildId); const memberList = members.data || []; return memberList.some((member) => member.user.id === userId); } catch (error) { try { const member = await session.bot.getGuildMember(session.guildId, userId); return !!member; } catch (memberError) { if (memberError.message?.includes("not found") || memberError.message?.includes("用户不存在")) { return false; } throw memberError; } } } __name(verifyUserKicked, "verifyUserKicked"); async function kickUserWithRetry(session, userId, displayName) { for (let i = 0; i < config.retryAttempts; i++) { try { await session.bot.kickGuildMember(session.guildId, userId); if (config.verifyKickResult) { await new Promise((resolve) => setTimeout(resolve, config.verifyDelay)); const isStillInGroup = await verifyUserKicked(session, userId); if (!isStillInGroup) { return true; } if (i === config.retryAttempts - 1) { await session.send(`⚠️ 检测到黑名单用户 ${displayName},已尝试踢出但用户仍在群里,请检查机器人权限`); return false; } throw new Error("踢人API调用成功但用户仍在群里"); } else { return true; } } catch (error) { if (i < config.retryAttempts - 1) { await new Promise((resolve) => setTimeout(resolve, config.retryDelay)); } else { await session.send(`⚠️ 检测到黑名单用户 ${displayName},踢人失败,请检查机器人权限`); } } } return false; } __name(kickUserWithRetry, "kickUserWithRetry"); async function checkAndKickUser(session, userId, providedNickname) { let nickname = providedNickname; let isGroupNickname = false; if (!nickname) { const nicknameResult = await getUserNickname(null, session, session.bot, userId, session.guildId); nickname = nicknameResult.nickname; isGroupNickname = nicknameResult.isGroupNickname; } else { const nicknameResult = await getUserNickname(null, session, session.bot, userId, session.guildId); if (nicknameResult.nickname === nickname && nicknameResult.isGroupNickname) { isGroupNickname = true; } } const displayName = nickname || userId; let kickReason = ""; let shouldKick = false; if (blacklistManager.isBlacklisted(userId)) { shouldKick = true; kickReason = "QQ号黑名单"; } if (!shouldKick && nickname) { const nicknameCheck = blacklistManager.isNicknameBlacklisted(nickname, config.nicknameMatchMode); if (nicknameCheck.isBlacklisted) { shouldKick = true; kickReason = `${isGroupNickname ? "群昵称" : "QQ昵称"}黑名单 (匹配: ${nicknameCheck.matchedKeyword})`; } } if (!shouldKick) { return false; } if (config.skipAdmins) { const isAdmin = await isUserAdmin(session, userId); if (isAdmin) { logger.warn(`⚠️ 检测到黑名单管理员 ${displayName},已跳过 - ${kickReason}`); if (config.notifyAdmins) { const notifyMessage = `⚠️ 注意:检测到管理员/群主 ${displayName} 匹配黑名单规则 (${kickReason}),但已跳过踢出操作`; try { if (typeof session.send === "function") { await session.send(notifyMessage); } } catch (error) { } } return false; } } logger.info(`🎯 发现黑名单用户: ${displayName} (${userId}) - ${kickReason}`); if (config.notifyAdmins) { const notifyMessage = config.adminNotifyMessage.replace("{user}", displayName).replace("{reason}", kickReason); try { if (typeof session.send === "function") { await session.send(notifyMessage); } } catch (error) { } } const kickSuccess = await concurrencyController.execute(async () => { const success = await kickUserWithRetry(session, userId, displayName); if (success) { logger.info(`✅ 成功踢出: ${displayName} (${userId}) - ${kickReason}`); } else { logger.warn(`❌ 踢出失败: ${displayName} (${userId}) - ${kickReason}`); } return success; }); return kickSuccess; } __name(checkAndKickUser, "checkAndKickUser"); async function handleGuildRequest(session) { if (!config.enableAutoApprove) return; const userId = session.userId; const nicknameResult = await getUserNickname(null, session); const displayName = nicknameResult.nickname || userId; logger.info(`📥 收到加群申请: ${displayName} (${userId})`); let isBlacklisted = false; let rejectReason = ""; if (blacklistManager.isBlacklisted(userId)) { isBlacklisted = true; rejectReason = "QQ号黑名单"; } if (!isBlacklisted && displayName && displayName !== userId) { const nicknameCheck = blacklistManager.isNicknameBlacklisted(displayName, config.nicknameMatchMode); if (nicknameCheck.isBlacklisted) { isBlacklisted = true; rejectReason = `${nicknameResult.isGroupNickname ? "群昵称" : "QQ昵称"}黑名单 (匹配: ${nicknameCheck.matchedKeyword})`; } } try { let success = false; let apiMethod = "unknown"; if (isBlacklisted) { logger.info(`❌ 拒绝黑名单用户: ${displayName} (${userId}) - ${rejectReason}`); if (session.bot.handleGuildRequest) { apiMethod = "handleGuildRequest"; await session.bot.handleGuildRequest(session.messageId, false, config.autoRejectMessage); success = true; } else if (session.bot.setGuildMemberRequest) { apiMethod = "setGuildMemberRequest"; await session.bot.setGuildMemberRequest(session.guildId, userId, false, config.autoRejectMessage); success = true; } else if (session.bot.handleGuildMemberRequest) { apiMethod = "handleGuildMemberRequest"; await session.bot.handleGuildMemberRequest(session.guildId, userId, false, config.autoRejectMessage); success = true; } else if (session.bot.rejectGuildRequest) { apiMethod = "rejectGuildRequest"; await session.bot.rejectGuildRequest(session.messageId, config.autoRejectMessage); success = true; } if (success) { if (config.enableStats) { blacklistManager.updateApprovalStats(0, 1); } if (config.notifyAutoApprove) { const notifyMessage = config.autoRejectNotifyMessage.replace("{user}", displayName).replace("{reason}", rejectReason); try { await session.send(notifyMessage); } catch (error) { } } } } else { logger.info(`✅ 同意用户加群: ${displayName} (${userId})`); if (session.bot.handleGuildRequest) { apiMethod = "handleGuildRequest"; await session.bot.handleGuildRequest(session.messageId, true, config.autoApproveMessage); success = true; } else if (session.bot.setGuildMemberRequest) { apiMethod = "setGuildMemberRequest"; await session.bot.setGuildMemberRequest(session.guildId, userId, true, config.autoApproveMessage); success = true; } else if (session.bot.handleGuildMemberRequest) { apiMethod = "handleGuildMemberRequest"; await session.bot.handleGuildMemberRequest(session.guildId, userId, true, config.autoApproveMessage); success = true; } else if (session.bot.approveGuildRequest) { apiMethod = "approveGuildRequest"; await session.bot.approveGuildRequest(session.messageId, config.autoApproveMessage); success = true; } if (success) { if (config.enableStats) { blacklistManager.updateApprovalStats(1, 0); } if (config.notifyAutoApprove) { const notifyMessage = config.autoApproveNotifyMessage.replace("{user}", displayName); try { await session.send(notifyMessage); } catch (error) { } } } } if (!success) { logger.error(`❌ 无法处理加群请求: 未找到合适的API方法`); try { await session.send(`⚠️ 无法自动处理 ${displayName} 的加群请求,请手动处理`); } catch (notifyError) { } } } catch (error) { logger.error(`处理加群请求失败: ${error.message}`); try { await session.send(`⚠️ 处理 ${displayName} 的加群请求时发生错误: ${error.message},请手动处理`); } catch (notifyError) { } } } __name(handleGuildRequest, "handleGuildRequest"); async function extendedNicknameCheck(userId, guildId, bot) { const checkInfo = extendedCheckCache.get(userId); if (!checkInfo) { logger.warn(`⚠️ 扩展检查: 用户 ${userId} 的缓存信息丢失`); return; } if (config.enableStats) { blacklistManager.updateExtendedCheckStats(); } try { const memberInfo = await bot.getGuildMember(guildId, userId); if (!memberInfo) { logger.info(`ℹ️ 扩展检查: 用户 ${userId} 已不在群内,停止检查`); extendedCheckCache.delete(userId); return; } const nicknameResult = await getUserNickname(memberInfo, null, bot, userId, guildId); const currentNickname = nicknameResult.nickname; const isCurrentGroupNickname = nicknameResult.isGroupNickname; const checkNumText = checkInfo.isFirstCheck ? "首次" : `第${checkInfo.checkCount}次`; logger.info(`🔍 扩展检查(${checkNumText}): 用户 ${userId} 当前昵称 "${currentNickname}" (${isCurrentGroupNickname ? "群昵称" : "QQ昵称"})`); if (currentNickname) { const nicknameCheck = blacklistManager.isNicknameBlacklisted(currentNickname, config.nicknameMatchMode); if (nicknameCheck.isBlacklisted) { logger.info(`🎯 扩展检查发现违规昵称: ${currentNickname} (匹配: ${nicknameCheck.matchedKeyword})`); const kickSession = { guildId, userId, bot, send: /* @__PURE__ */ __name(async (message) => { try { if (bot.sendGuildMessage) { await bot.sendGuildMessage(guildId, message); } else if (bot.sendMessage) { await bot.sendMessage(guildId, message); } } catch (error) { } }, "send") }; const kickResult = await checkAndKickUser(kickSession, userId, currentNickname); if (kickResult) { logger.info(`✅ 扩展检查成功踢出违规用户: ${userId}`); extendedCheckCache.delete(userId); return; } } } if (checkInfo.checkCount < checkInfo.maxChecks) { checkInfo.checkCount++; checkInfo.isFirstCheck = false; checkInfo.nextCheckTime = Date.now() + config.extendedCheckInterval; setTimeout(() => { extendedNicknameCheck(userId, guildId, bot); }, config.extendedCheckInterval); const remainingChecks = checkInfo.maxChecks - checkInfo.checkCount; logger.info(`⏰ 安排下次扩展检查: 用户 ${userId}, 剩余 ${remainingChecks} 次检查`); } else { logger.info(`✅ 扩展检查完成: 用户 ${userId} 已完成所有 ${checkInfo.maxChecks} 次检查`); extendedCheckCache.delete(userId); } } catch (error) { logger.error(`❌ 扩展检查用户 ${userId} 失败: ${error.message}`); extendedCheckCache.delete(userId); } } __name(extendedNicknameCheck, "extendedNicknameCheck"); async function scanExistingMembers(session) { if (!config.enableJoinScan) return; const startTime = Date.now(); logger.info(`🔍 开始扫描群 ${session.guildId}`); try { const members = await session.bot.getGuildMemberList(session.guildId); const memberList = members.data || []; let scannedCount = 0; let blacklistedCount = 0; let kickedCount = 0; let failedKickCount = 0; const filteredMembers = memberList.filter((member) => { if (config.skipBotMembers && member.user.isBot) { return false; } return true; }); for (let i = 0; i < filteredMembers.length; i += config.batchSize) { const batch = filteredMembers.slice(i, i + config.batchSize); for (const member of batch) { scannedCount++; const userId = member.user.id; const nicknameResult = await getUserNickname(member, session, session.bot, userId, session.guildId); const nickname = nicknameResult.nickname; let shouldKick = false; if (blacklistManager.isBlacklisted(userId)) { shouldKick = true; } else if (nickname) { const nicknameCheck = blacklistManager.isNicknameBlacklisted(nickname, config.nicknameMatchMode); if (nicknameCheck.isBlacklisted) { shouldKick = true; } } if (shouldKick) { blacklistedCount++; const kickResult = await checkAndKickUser(session, userId, nickname); if (kickResult) { kickedCount++; } else { failedKickCount++; } } if (blacklistedCount > 0 && scannedCount % config.maxConcurrent === 0) { await new Promise((resolve) => setTimeout(resolve, config.kickDelay)); } } if (i + config.batchSize < filteredMembers.length) { await new Promise((resolve) => setTimeout(resolve, 1e3)); } } const scanTime = Date.now() - startTime; if (config.enableStats) { blacklistManager.updateStats(scannedCount, kickedCount, failedKickCount, scanTime); } if (blacklistedCount > 0) { logger.info(`📊 扫描完成: 检查 ${scannedCount} 人, 发现黑名单 ${blacklistedCount} 人, 成功踢出 ${kickedCount} 人, 失败 ${failedKickCount} 人, 耗时 ${scanTime}ms`); } else { logger.info(`✅ 扫描完成: 检查 ${scannedCount} 人, 未发现黑名单用户, 耗时 ${scanTime}ms`); } } catch (error) { logger.error(`扫描群成员失败: ${error.message}`); } } __name(scanExistingMembers, "scanExistingMembers"); offlineMonitor.start(); ctx.on("guild-added", async (session) => { logger.info(`🏠 机器人加入群聊: ${session.guildId}`); setTimeout(() => { scanExistingMembers(session); }, config.scanDelay); }); ctx.on("guild-member-added", async (session) => { if (!config.enableMemberJoin) return; const userId = session.userId; logger.info(`👤 新成员加入: ${userId}`); if (config.skipBotMembers && session.author?.isBot) { logger.info(`🤖 跳过机器人成员: ${userId}`); return; } let nicknameResult = await getUserNickname(null, session); let nickname = nicknameResult.nickname; if (!nickname || nickname === userId) { if (session.guildId) { try { const memberInfo = await session.bot.getGuildMember(session.guildId, userId); const apiNicknameResult = await getUserNickname(memberInfo, session, session.bot, userId, session.guildId); if (apiNicknameResult.nickname && apiNicknameResult.nickname !== userId) { nickname = apiNicknameResult.nickname; nicknameResult = apiNicknameResult; } } catch (error) { } } } if (!nickname || nickname === userId) { try { const userInfo = await session.bot.getUser(userId); const userNickname = userInfo?.name || userInfo?.username || null; if (userNickname && userNickname !== userId) { nickname = userNickname; nicknameResult = { nickname: userNickname, isGroupNickname: false }; } } catch (error) { } } if (!nickname || nickname === userId) { if (blacklistManager.isBlacklisted(userId)) { await checkAndKickUser(session, userId, null); return; } nickname = userId; nicknameResult = { nickname: userId, isGroupNickname: false }; } logger.info(`👤 新成员处理: ${nickname} (${userId})`); const immediateCheckResult = await checkAndKickUser(session, userId, nickname); if (!immediateCheckResult && config.enableExtendedNicknameCheck && nickname !== userId) { const maxChecks = config.extendedCheckHours; logger.info(`⏰ 设置用户 ${userId} 扩展昵称检查 - 首次检查: ${config.firstCheckDelay / 1e3}秒后, 后续每${config.extendedCheckInterval / 1e3 / 3600}小时检查一次, 总共${maxChecks}次`); const checkInfo = { nickname, guildId: session.guildId, joinTime: Date.now(), checkCount: 0, maxChecks, nextCheckTime: Date.now() + config.firstCheckDelay, isFirstCheck: true }; extendedCheckCache.set(userId, checkInfo); const botReference = session.bot; setTimeout(() => { extendedNicknameCheck(userId, session.guildId, botReference); }, config.firstCheckDelay); } }); const guildRequestEvents = [ "guild-request", "guild-member-request", "request.group.add", "notice.group_increase", "request/group/add" ]; for (const eventName of guildRequestEvents) { ctx.on(eventName, async (session) => { await handleGuildRequest(session); }); } ctx.command("auto-kick.status", "查看插件状态").action(async ({ session }) => { const stats = blacklistManager.getStats(); const offlineStatus = offlineMonitor.getStatus(); const statusMessage = [ "📊 Auto-kick 插件状态:", `🎯 黑名单: QQ号 ${blacklistManager.size()} 个, 昵称 ${blacklistManager.nicknameSize()} 个`, `📈 统计: 扫描 ${stats.totalScanned} 人, 踢出 ${stats.totalKicked} 人, 失败 ${stats.totalFailed} 人`, `🤖 自动审批: 同意 ${stats.totalApproved} 人, 拒绝 ${stats.totalRejected} 人`, `📧 离线通知: 发送 ${stats.totalOfflineNotifications} 次`, `🔍 扩展检查: 执行 ${stats.totalExtendedChecks} 次`, `🔌 机器人状态: ${offlineStatus.isOnline ? "在线 ✅" : "离线 ❌"}`, `⏱️ 待检查任务: ${extendedCheckCache.size} 个` ].join("\n"); return statusMessage; }); ctx.command("auto-kick.extended-cache", "查看扩展检查缓存").action(async ({ session }) => { if (extendedCheckCache.size === 0) { return "📋 当前没有待处理的扩展检查任务"; } const cacheEntries = Array.from(extendedCheckCache.entries()).map(([userId, data]) => { const remainingTime = Math.max(0, (data.nextCheckTime - Date.now()) / 1e3); const remainingChecks = data.maxChecks - data.checkCount; return `👤 ${userId} (${data.nickname}) - 下次检查: ${Math.ceil(remainingTime)}秒后, 剩余: ${remainingChecks}次`; }); return [ `📋 扩展检查缓存 (${extendedCheckCache.size} 个任务):`, ...cacheEntries ].join("\n"); }); ctx.command("auto-kick.test-nickname <userId>", "测试获取指定用户的昵称").action(async ({ session }, userId) => { if (!userId) { return "请提供用户ID"; } try { const nicknameResult = await getUserNickname(null, session, session.bot, userId, session.guildId); return [ `🔍 用户 ${userId} 昵称信息:`, `📝 昵称: "${nicknameResult.nickname || "未获取到"}"`, `🏷️ 类型: ${nicknameResult.isGroupNickname ? "群昵称" : "QQ昵称"}` ].join("\n"); } catch (error) { return `❌ 获取昵称失败: ${error.message}`; } }); logger.info(`🚀 Auto-kick 插件启动成功`); logger.info(`📋 QQ号黑名单: ${blacklistManager.size()} 个`); logger.info(`👤 昵称关键词: ${blacklistManager.nicknameSize()} 个`); logger.info(`⚙️ 匹配模式: ${config.nicknameMatchMode}`); logger.info(`🛡️ 跳过管理员: ${config.skipAdmins ? "✅" : "❌"}, 跳过机器人: ${config.skipBotMembers ? "✅" : "❌"}`); logger.info(`🤖 自动同意加群: ${config.enableAutoApprove ? "✅" : "❌"}`); logger.info(`📧 离线监控: ${config.enableOfflineMonitor ? "✅" : "❌"}`); if (config.enableExtendedNicknameCheck) { logger.info(`⏰ 扩展昵称检查: ✅ 首次检查延迟${config.firstCheckDelay / 1e3}秒, 后续每${config.extendedCheckInterval / 1e3 / 3600}小时检查一次, 持续${config.extendedCheckHours}小时`); } else { logger.info(`⏰ 扩展昵称检查: ❌ 未启用`); } ctx.on("dispose", () => { clearInterval(configCheckInterval); extendedCheckCache.clear(); offlineMonitor.stop(); }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, name, usage });