@hambur/koishi-plugin-auto-kick
Version:
自动踢出黑名单用户的群管理插件
1,154 lines (1,150 loc) • 49.2 kB
JavaScript
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
});