UNPKG

koishi-plugin-onebot-manager

Version:

适用于 Onebot 的 QQ 群管,可自动处理好友申请、群邀请和入群请求,提供群组管理功能

1,138 lines (1,131 loc) 53.7 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, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi2 = require("koishi"); // src/request.ts var OnebotRequest = class { /** * 创建 OneBot 请求处理实例 * @param ctx - Koishi 上下文 * @param logger - 日志记录器 * @param config - 配置项 */ constructor(ctx, logger, config = {}) { this.ctx = ctx; this.logger = logger; this.config = config; } static { __name(this, "OnebotRequest"); } pendingRequests = /* @__PURE__ */ new Map(); requestNumberMap = /* @__PURE__ */ new Map(); nextRequestNumber = 1; activeRequests = /* @__PURE__ */ new Map(); /** * 生成请求的唯一键 * @param session - Koishi 会话 * @param type - 请求类型 * @returns 请求唯一键 */ getRequestKey(session, type) { return type === "friend" ? `friend:${session.userId}` : type === "guild" ? `guild:${session.guildId}` : `member:${session.userId}:${session.guildId}`; } /** * 取消活动中的请求 * @param requestKey - 请求唯一键 */ cancelActiveRequest(requestKey) { const activeRequest = this.activeRequests.get(requestKey); if (!activeRequest) return; activeRequest.disposer?.(); if (activeRequest.timeoutTimer) clearTimeout(activeRequest.timeoutTimer); if (activeRequest.requestNumber !== void 0) this.requestNumberMap.delete(activeRequest.requestNumber); this.activeRequests.delete(requestKey); } /** * 处理收到的请求 * @param session - Koishi 会话 * @param type - 请求类型 */ async processRequest(session, type) { const requestKey = this.getRequestKey(session, type); this.cancelActiveRequest(requestKey); const requestMode = this.config[`${type}Request`] || "reject"; const needsNotification = requestMode === "manual" || requestMode === "auto"; let notificationSent = false; try { if (needsNotification) notificationSent = await this.setupNotification(session, type, requestKey, requestMode === "manual"); if (requestMode === "manual" && notificationSent) return; let approve = false; let reason = ""; if (requestMode === "accept") { approve = true; } else if (requestMode === "auto") { const result = await this.shouldAutoAccept(session, type); approve = result === true; reason = typeof result === "string" ? result : "条件不符"; } else if (requestMode === "manual" && !notificationSent) { reason = "通知失败,已自动拒绝"; } await this.processRequestAction(session, type, approve, reason); } catch (error) { this.logger.error(`处理请求${requestKey}失败: ${error}`); try { await this.processRequestAction(session, type, false, "处理出错"); } catch { } } finally { if (requestMode !== "manual" || !notificationSent) this.cleanupRequest(requestKey); } } /** * 清理请求数据 * @param requestId - 请求 ID */ cleanupRequest(requestId) { this.pendingRequests.delete(requestId); for (const [num, id] of this.requestNumberMap.entries()) if (id === requestId) this.requestNumberMap.delete(num); } /** * 检查用户条件是否满足自动接受要求 * @param session - Koishi 会话 * @param regTimeLimit - 注册时间要求(年数,-1表示不检查) * @param levelLimit - QQ等级要求(-1表示不检查) * @param vipLevelLimit - VIP等级要求(-1表示不检查) * @returns 是否满足条件,如不满足返回原因 */ async checkUserConditions(session, regTimeLimit = -1, levelLimit = -1, vipLevelLimit = -1) { if (regTimeLimit < 0 && levelLimit < 0 && vipLevelLimit < 0) return false; try { const userInfo = await session.onebot.getStrangerInfo(Number(session.userId), false); const regTime = userInfo.reg_time || userInfo.regTime || 0; const regYear = regTime > 0 ? new Date(regTime * 1e3).getFullYear() : (/* @__PURE__ */ new Date()).getFullYear(); if (regTimeLimit >= 0 && (/* @__PURE__ */ new Date()).getFullYear() - regYear < regTimeLimit) return `注册时间不满${regTimeLimit}年`; if (levelLimit >= 0 && (userInfo.level || userInfo.qqLevel || 0) < levelLimit) return `QQ等级低于${levelLimit}级`; if (vipLevelLimit >= 0 && (!userInfo.is_vip || (userInfo.vip_level || 0) < vipLevelLimit)) return `会员等级低于${vipLevelLimit}级`; return true; } catch (error) { return `获取用户信息失败: ${error}`; } } /** * 判断是否应自动接受请求 * @param session - Koishi 会话 * @param type - 请求类型 * @returns 是否接受,如不接受返回原因 */ async shouldAutoAccept(session, type) { if (type === "friend" || type === "member") { const prefix = type === "friend" ? "Friend" : "Member"; return this.checkUserConditions( session, this.config[`${prefix}RegTime`] ?? -1, this.config[`${prefix}Level`] ?? -1, this.config[`${prefix}VipLevel`] ?? -1 ); } if (type === "guild") { const { GuildAllowUsers = [], GuildMinMemberCount = -1, GuildMaxCapacity = -1 } = this.config; if (GuildAllowUsers.includes(session.userId)) return true; let user; try { user = await this.ctx.database.getUser(session.platform, session.userId); } catch { } if (user?.authority > 1) return true; if (GuildMinMemberCount >= 0 || GuildMaxCapacity >= 0) { try { const info = await session.onebot.getGroupInfo(Number(session.guildId), true); if (GuildMinMemberCount >= 0 && info.member_count < GuildMinMemberCount) return `群成员数量不足${GuildMinMemberCount}人`; if (GuildMaxCapacity >= 0 && info.max_member_count < GuildMaxCapacity) return `群最大容量不足${GuildMaxCapacity}人`; return true; } catch (error) { return `获取群信息失败: ${error}`; } } } return false; } /** * 处理请求操作(接受或拒绝) * @param session - Koishi 会话 * @param type - 请求类型 * @param approve - 是否接受请求 * @param reason - 拒绝原因 * @param remark - 好友备注(仅适用于好友请求) * @returns 处理是否成功 */ async processRequestAction(session, type, approve, reason = "", remark = "") { try { const eventData = session.event?._data || {}; if (!approve && type === "guild" && (session.event?.type === "guild-added" || eventData.notice_type === "group_increase")) { if (reason) { try { await session.bot.sendMessage(session.guildId, `将退出该群 ${reason}`); } catch (error) { this.logger.warn(`发送退群通知失败: ${error}`); } } try { await session.onebot.setGroupLeave(Number(session.guildId), false); return true; } catch (error) { this.logger.error(`退出群组 ${session.guildId} 失败: ${error}`); return false; } } const flag = eventData.flag; if (!flag) return false; if (type === "friend") await session.onebot.setFriendAddRequest(flag, approve, remark); else await session.onebot.setGroupAddRequest(flag, eventData.sub_type ?? "add", approve, approve ? "" : reason); return true; } catch (error) { this.logger.error(`请求处理失败: ${error}`); return false; } } /** * 设置通知 * @param session - Koishi 会话 * @param type - 请求类型 * @param requestId - 请求 ID * @param isManualMode - 是否为手动处理模式 */ async setupNotification(session, type, requestId, isManualMode) { const { enableNotify = false, notifyTarget = "" } = this.config; if (!enableNotify || !notifyTarget) return false; const [targetType, targetId] = notifyTarget.split(":"); const isPrivate = targetType?.toLowerCase() === "private"; if (!targetId || targetType !== "guild" && targetType !== "private") { this.logger.warn(`通知目标错误: ${notifyTarget}`); return false; } try { const requestNumber = this.nextRequestNumber++; this.requestNumberMap.set(requestNumber, requestId); if (!this.activeRequests.has(requestId)) { this.activeRequests.set(requestId, { requestNumber }); } else { this.activeRequests.get(requestId).requestNumber = requestNumber; } const eventData = session.event?._data || {}; let user = null, guild = null, operator = null; user = await session.bot.getUser?.(session.userId)?.catch(() => null) ?? null; if (type !== "friend") guild = await session.bot.getGuild?.(session.guildId)?.catch(() => null) ?? null; if (type === "guild" && eventData.operator_id && eventData.operator_id !== session.userId) { operator = await session.bot.getUser?.(eventData.operator_id.toString())?.catch(() => null) ?? null; } const isDirectBotJoin = type === "guild" && eventData.sub_type !== "invite" && session.userId === session.selfId; let msg = user?.avatar ? `<image url="${user.avatar}"/> ` : ""; msg += `类型:${type === "friend" ? "好友申请" : type === "member" ? "加群请求" : eventData.sub_type === "invite" ? "群邀请" : "直接入群"} `; if (session.userId && !isDirectBotJoin) msg += `用户:${user?.name ? `${user.name}(${session.userId})` : session.userId} `; if (type === "guild" && eventData.operator_id && eventData.operator_id !== session.userId) msg += `操作者:${operator?.name ? `${operator.name}(${eventData.operator_id})` : eventData.operator_id} `; if (type !== "friend" && session.guildId) msg += `群组:${guild?.name ? `${guild.name}(${session.guildId})` : session.guildId} `; if (eventData.comment) msg += `验证信息:${eventData.comment} `; const requestMode = this.config[`${type}Request`] || "reject"; msg += `处理模式:${isManualMode ? "人工审核" : requestMode === "auto" ? "自动审核" : requestMode === "accept" ? "自动通过" : "自动拒绝"} `; const sendFunc = isPrivate ? (m) => session.bot.sendPrivateMessage(targetId, m) : (m) => session.bot.sendMessage(targetId, m); await sendFunc(msg); if (isManualMode) { this.pendingRequests.set(requestId, { session, type }); this.setupPromptResponse(session, type, requestId, requestNumber, targetId, isPrivate); } return true; } catch (error) { this.logger.error(`通知发送失败: ${error}`); return false; } } /** * 设置人工审核响应监听 */ async setupPromptResponse(session, type, requestId, requestNumber, targetId, isPrivate) { const sendFunc = isPrivate ? (msg) => session.bot.sendPrivateMessage(targetId, msg) : (msg) => session.bot.sendMessage(targetId, msg); await sendFunc(`请回复以下命令处理请求 #${requestNumber}: 通过[y]${requestNumber} [备注] | 拒绝[n]${requestNumber} [理由]`); let disposed = false; const disposer = this.ctx.middleware(async (s, next) => { if (disposed || s.userId !== targetId && s.guildId !== targetId) return next(); const match = s.content.trim().match(new RegExp(`^(y|n|通过|拒绝)(${requestNumber})\\s*(.*)$`)); if (!match) return next(); disposed = true; disposer(); this.cleanupRequest(requestId); const isApprove = match[1] === "y" || match[1] === "通过"; const extraContent = match[3]?.trim() || ""; try { await this.processRequestAction( session, type, isApprove, !isApprove ? extraContent : "", isApprove && type === "friend" ? extraContent : "" ); await sendFunc(`请求 #${requestNumber}${isApprove ? "通过" : "拒绝"}${extraContent ? `,${isApprove ? "备注" : "原因"}${extraContent}` : ""}`); } catch (error) { this.logger.error(`响应处理失败: ${error}`); await sendFunc(`处理请求 #${requestNumber} 失败: ${error.message || "未知错误"}`); } this.activeRequests.delete(requestId); }); const activeRequest = this.activeRequests.get(requestId) || {}; activeRequest.disposer = disposer; this.activeRequests.set(requestId, activeRequest); const timeoutMin = typeof this.config.manualTimeout === "number" ? this.config.manualTimeout : 60; const timeoutAction = this.config.manualTimeoutAction === "accept" || this.config.manualTimeoutAction === "reject" ? this.config.manualTimeoutAction : "reject"; if (timeoutMin > 0) { const timeoutTimer = setTimeout(async () => { if (disposed) return; disposed = true; disposer(); this.cleanupRequest(requestId); try { await this.processRequestAction( session, type, timeoutAction === "accept", timeoutAction === "reject" ? "请求处理超时,已自动拒绝" : "" ); await sendFunc(`请求 #${requestNumber} 超时,已自动${timeoutAction === "accept" ? "通过" : "拒绝"}`); } catch (e) { this.logger.error(`超时处理失败: ${e}`); } this.activeRequests.delete(requestId); }, timeoutMin * 60 * 1e3); activeRequest.timeoutTimer = timeoutTimer; this.activeRequests.set(requestId, activeRequest); } } /** * 注册事件监听器,自动处理 OneBot 请求事件 */ registerEventListeners() { const handleRequest = /* @__PURE__ */ __name((type) => async (session) => { const data = session.event?._data || {}; session.userId = (data.user_id || data.userId || session.userId)?.toString(); if (type !== "friend") session.guildId = (data.group_id || data.groupId || session.guildId)?.toString() || ""; await this.processRequest(session, type); }, "handleRequest"); this.ctx.on("friend-request", handleRequest("friend")); this.ctx.on("guild-request", handleRequest("guild")); this.ctx.on("guild-member-request", handleRequest("member")); this.ctx.on("guild-added", handleRequest("guild")); } }; // src/utils.ts var import_koishi = require("koishi"); var ROLE_MAP = { owner: "群主", admin: "管理员", member: "成员" }; var getRoleName = /* @__PURE__ */ __name((role) => ROLE_MAP[role] || role || "未知", "getRoleName"); var utils = { /** * 解析目标字符串,返回QQ号或null */ parseTarget(target) { if (!target) return null; try { const at = import_koishi.h.select(import_koishi.h.parse(target), "at")[0]?.attrs?.id; if (at && !isNaN(Number(at))) return at; const match = target.match(/@?(\d{5,10})/)?.[1]; return match && !isNaN(Number(match)) ? match : null; } catch { return null; } }, /** * 处理错误并发送提示消息 */ handleError(session, error) { const errorMsg = error?.message || String(error); return session.send(errorMsg).then((msg) => { if (typeof msg === "string") setTimeout(() => session.bot.deleteMessage(session.channelId, msg).catch(() => { }), 1e4); return null; }); }, /** * 检查机器人和用户在群内的权限角色 */ async checkPermission(session, logger) { if (!session.guildId) return { bot: null, user: null }; try { const [bot, user] = await Promise.all([ session.onebot.getGroupMemberInfo(+session.guildId, +session.selfId, true), session.onebot.getGroupMemberInfo(+session.guildId, +session.userId, true) ]); return { bot: bot?.role ?? null, user: user?.role ?? null }; } catch (e) { logger?.error("获取群成员信息失败:", e); return { bot: null, user: null }; } }, /** * 包装函数,执行前检查机器人和用户的群权限 */ withRoleCheck(session, logger, requiredBotRoles = [], requiredUserRoles = [], fn) { return (...args) => utils.checkPermission(session, logger).then(({ bot, user }) => { const errors = []; if (requiredBotRoles.length && (!bot || !requiredBotRoles.includes(bot))) { errors.push(`需要${requiredBotRoles.map(getRoleName).join("或")}(当前为${getRoleName(bot)})`); } if (requiredUserRoles.length && (!user || !requiredUserRoles.includes(user))) { errors.push(`用户需要${requiredUserRoles.map(getRoleName).join("或")}(当前为${getRoleName(user)})`); } if (errors.length) return utils.handleError(session, `权限不足:${errors.join(";")}`); return fn(...args); }); } }; // src/command.ts function getTitleLen(title) { return Array.from(title).reduce((len, char) => { const code = char.codePointAt(0); if (code && (code >= 126976 && code <= 131071 || code >= 9728 && code <= 9983 || code >= 9984 && code <= 10175 || code === 12349 || code === 8265 || code === 8252 || code === 8505 || code >= 8192 && code <= 8207 || code >= 8232 && code <= 8239 || code === 8287 || code >= 8293 && code <= 8303 || code >= 8400 && code <= 8447 || code >= 8448 && code <= 8527 || code >= 8960 && code <= 9215 || code >= 11008 && code <= 11263 || code >= 10496 && code <= 10623 || code >= 12800 && code <= 13055 || code >= 55296 && code <= 57343 || code >= 65024 && code <= 65039 || code >= 65520 && code <= 65535)) { return len + 6; } if (code && code >= 32 && code <= 126) { return len + 1; } return len + 3; }, 0); } __name(getTitleLen, "getTitleLen"); function getGroupId(options, session) { const groupId = options.group ? Number(options.group) : Number(session.guildId); if (isNaN(groupId) || groupId <= 0) throw new Error("无效群号"); return groupId; } __name(getGroupId, "getGroupId"); async function getTargetId(target, session, utils2, groupId, roleCheck = false) { if (!target) return session.userId; const parsed = utils2.parseTarget(target); if (!parsed) return "无效成员"; if (!roleCheck) return parsed; try { const info = await session.onebot.getGroupMemberInfo(groupId, Number(session.userId), true); return info?.role !== "member" ? parsed : session.userId; } catch { return "获取成员信息失败"; } } __name(getTargetId, "getTargetId"); function createCommandAction(utils2, logger, botRoles, userRoles, actionFn) { return ({ session, options }, ...args) => utils2.withRoleCheck( session, logger, botRoles, userRoles, () => { try { return actionFn(session, options, ...args); } catch (error) { return utils2.handleError(session, error); } } )(); } __name(createCommandAction, "createCommandAction"); function adminAction(set, utils2, logger) { return createCommandAction( utils2, logger, ["owner"], ["owner", "admin"], async (session, options, target) => { if (!target) return "请指定成员"; const groupId = getGroupId(options, session); const targetId = utils2.parseTarget(target); if (!targetId) return "无效的成员ID"; await session.onebot.setGroupAdmin(groupId, Number(targetId), set); return set ? `已设置成员 ${targetId} 为管理` : `已取消成员 ${targetId} 的管理`; } ); } __name(adminAction, "adminAction"); function registerCommands(qgroup, logger, utils2) { qgroup.subcommand("tag [title:string] [target]", "设置专属头衔").option("group", "-g, --group <groupId> 指定群号").usage("设置或清除指定成员的群头衔\n使用引号添加不连续的内容,最多18字符\n英文(标点)和数字1字符,中文和其他符号3字符,Emoji6字符").action(createCommandAction( utils2, logger, ["owner"], [], async (session, options, title = "", target) => { if (title && getTitleLen(title) > 18) return "设置头衔失败: 长度超过18字符"; const groupId = getGroupId(options, session); const targetId = await getTargetId(target, session, utils2, groupId, !!target); await session.onebot.setGroupSpecialTitle(groupId, Number(targetId), title); return `已${title ? "将" : "清除"}${targetId === session.userId ? "您" : `用户 ${targetId}`}的头衔${title ? `设置为:${title}` : ""}`; } )); qgroup.subcommand("membercard [card:string] [target]", "设置群名片").option("group", "-g, --group <groupId> 指定群号").usage("设置或清除指定成员的群名片").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, card = "", target) => { const groupId = getGroupId(options, session); const targetId = await getTargetId(target, session, utils2, groupId, !!target); await session.onebot.setGroupCard(groupId, Number(targetId), card); return `已${card ? "将" : "清除"}${targetId === session.userId ? "您" : `用户 ${targetId}`}的群名片${card ? `设置为:${card}` : ""}`; } )); qgroup.subcommand("groupname <group_name:string>", "设置群名称").option("group", "-g, --group <groupId> 指定群号").usage("设置当前群的名称").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, group_name) => { if (!group_name) return "请输入群名"; const groupId = getGroupId(options, session); await session.onebot.setGroupName(groupId, group_name); return `已将群名设置为:${group_name}`; } )); const essence = qgroup.subcommand("essence [messageId:string]", "设置精华消息").option("group", "-g, --group <groupId> 指定群号").usage("设置指定消息为精华消息").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, messageId) => { messageId = messageId || session.quote?.id; if (!messageId) return "请提供消息ID或引用消息"; await session.onebot.setEssenceMsg(messageId); return "已设置精华消息"; } )); essence.subcommand(".del [messageId:string]", "移除精华消息").option("group", "-g, --group <groupId> 指定群号").usage("移除指定消息的精华消息").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, messageId) => { messageId = messageId || session.quote?.id; if (!messageId) return "请提供消息ID或引用消息"; await session.onebot.deleteEssenceMsg(messageId); return "已移除精华消息"; } )); const admin = qgroup.subcommand("admin <target>", "设置群管理").option("group", "-g, --group <groupId> 指定群号").usage("设置指定成员为群管理").action(adminAction(true, utils2, logger)); admin.subcommand(".del <target>", "取消群管理").option("group", "-g, --group <groupId> 指定群号").usage("取消指定成员的群管理").action(adminAction(false, utils2, logger)); const mute = qgroup.subcommand("mute <target> [duration]", "禁言群成员").option("cancel", "-c, --cancel 取消禁言").option("group", "-g, --group <groupId> 指定群号").usage("禁言指定成员,默认 30 分钟").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, target, duration) => { const groupId = getGroupId(options, session); const targetId = utils2.parseTarget(target); if (!targetId) return "请指定有效成员"; const banDuration = options.cancel ? 0 : duration ? Number(duration) : 1800; await session.onebot.setGroupBan(groupId, Number(targetId), banDuration); return options.cancel ? `已取消禁言成员 ${targetId}` : `已禁言成员 ${targetId} ${banDuration} 秒`; } )); mute.subcommand(".all [enable:boolean]", "全体禁言").option("group", "-g, --group <groupId> 指定群号").usage("开启或关闭全体禁言").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, enable) => { const val = typeof enable === "boolean" ? enable : true; const groupId = getGroupId(options, session); await session.onebot.setGroupWholeBan(groupId, val); return val ? "已开启全体禁言" : "已关闭全体禁言"; } )); qgroup.subcommand("kick <target>", "逐出群成员").option("reject", "-r, --reject 拒绝再次加群").option("group", "-g, --group <groupId> 指定群号").usage("逐出指定成员,使用 -r 拒绝此人再次加群").action(createCommandAction( utils2, logger, ["owner", "admin"], ["owner", "admin"], async (session, options, target) => { const targetId = utils2.parseTarget(target); if (!targetId) return "请指定有效的成员"; const groupId = getGroupId(options, session); await session.onebot.setGroupKick(groupId, Number(targetId), !!options.reject); return `已将成员 ${targetId} 逐出群${options.reject ? ",并拒绝其再次加群" : ""}`; } )); qgroup.subcommand("revoke", "撤回消息").option("group", "-g, --group <groupId> 指定群号").usage("撤回指定回复消息(仅限撤回自己的消息)").action(createCommandAction( utils2, logger, ["owner", "admin"], [], async (session) => { const messageId = session.quote?.id; if (!messageId) return "请回复需要撤回的消息"; let senderId = session.quote.user.id; if (senderId && String(senderId) === session.userId) { await session.onebot.deleteMsg(messageId); return ""; } } )); } __name(registerCommands, "registerCommands"); // src/onebot.ts var Onebot = class _Onebot { static { __name(this, "Onebot"); } static sexMap = { "male": "男", "female": "女" }; /** * 分页处理 * @param session 会话对象 * @param data 数据数组 * @param page 页码或'all' * @param pageSize 每页数量 * @returns 分页结果 */ handlePagination(session, data, page, pageSize = 10) { if (page === "all") return { displayData: data, pageInfo: ":\n", totalPages: 1 }; const totalPages = Math.ceil(data.length / pageSize); const pageNum = parseInt(page) || 1; if (isNaN(pageNum) || pageNum < 1 || (pageNum - 1) * pageSize >= data.length) { utils.handleError(session, new Error("操作失败: 无效页码")); return null; } const start = (pageNum - 1) * pageSize; return { displayData: data.slice(start, start + pageSize), pageInfo: `(第 ${pageNum}/${totalPages} 页): `, totalPages }; } /** * 格式化用户信息 * @param info 用户信息对象 * @returns 格式化字符串 */ static formatUserInfo(info) { let result = `${info.nickname || info.nick}(${info.user_id || info.uin}) `; if (info.qid) result += `QID: ${info.qid} `; if (info.uid) result += `UID: ${info.uid} `; if (info.long_nick || info.longNick) result += `个性签名: ${info.long_nick || info.longNick} `; result += "\n个人信息: \n"; const personalInfo = [ info.sex && info.sex !== "unknown" ? _Onebot.sexMap[info.sex] || info.sex : "", info.age ? `${info.age}岁` : "", info.birthday_year && info.birthday_month && info.birthday_day ? `${info.birthday_year}-${info.birthday_month}-${info.birthday_day}` : "" ].filter(Boolean); if (personalInfo.length) result += `${personalInfo.join(" | ")} `; const shengXiaos = ["", "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"]; const constellations = ["", "水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座"]; const bloodTypes = ["", "A型", "B型", "AB型", "O型"]; const zodiacInfo = [ info.shengXiao && shengXiaos[info.shengXiao], info.constellation && constellations[info.constellation], info.kBloodType && bloodTypes[info.kBloodType] ].filter(Boolean); if (zodiacInfo.length) result += `${zodiacInfo.join(" | ")} `; const contactInfo = [info.phoneNum, info.eMail].filter((x) => x && x !== "-"); if (contactInfo.length) result += `${contactInfo.join(" | ")} `; let locationLine = [info.country, info.province, info.city].filter(Boolean).join(" "); if (info.homeTown && info.homeTown !== "0-0-0") { const [province, city] = info.homeTown.split("-").map(Number); if (province > 0 || city > 0) locationLine += (locationLine ? " | " : "") + `家乡: ${province}-${city}`; } if (locationLine) result += `${locationLine} `; const educationInfo = [info.college, info.pos].filter(Boolean); if (educationInfo.length) result += `${educationInfo.join(" | ")} `; result += "\n账号信息: \n"; const statusMap = { 10: "离线", 20: "在线", 30: "离开", 40: "忙碌", 50: "请勿打扰", 60: "隐身" }; const termTypes = ["", "电脑", "手机", "网页", "平板"]; const netTypes = ["", "WiFi", "移动网络", "有线网络"]; const eNetworkTypes = { 1: "2G网络", 2: "3G网络", 3: "4G网络", 4: "5G网络", 5: "WiFi" }; const accountInfo = [ info.is_vip || info.vip_level ? info.is_years_vip ? `年VIP${info.vip_level || ""}` : `VIP${info.vip_level || ""}` : "", info.qqLevel ? `Lv:${info.qqLevel}` : "", info.status !== void 0 && statusMap[info.status] ? statusMap[info.status] : "", info.batteryStatus >= 0 && info.batteryStatus <= 100 ? `电量${info.batteryStatus}%` : "", info.termType && termTypes[info.termType] ? info.termDesc ? `${termTypes[info.termType]}(${info.termDesc})` : termTypes[info.termType] : "", [info.netType && netTypes[info.netType], info.eNetworkType && eNetworkTypes[info.eNetworkType]].filter(Boolean).join("-") ].filter(Boolean); if (accountInfo.length) result += `${accountInfo.join(" | ")} `; if (info.regTime || info.reg_time) { const regDate = new Date((info.regTime || info.reg_time) * 1e3); result += `注册于: ${regDate.toLocaleDateString()}${info.login_days ? ` (登录${info.login_days}天)` : ""} `; } return result; } /** * 格式化好友信息 * @param friend 好友信息对象 * @returns 格式化字符串 */ static formatFriendInfo(friend) { let result = `${friend.nickname}(${friend.user_id})${friend.level ? ` | LV:${friend.level}` : ""} `; const personalInfo = [friend.remark, friend.sex && friend.sex !== "unknown" ? _Onebot.sexMap[friend.sex] || friend.sex : "", friend.age > 0 ? `${friend.age}岁` : ""].filter(Boolean); if (friend.birthday_year || friend.birthday_month || friend.birthday_day) { personalInfo.push(`${friend.birthday_year || "?"}-${friend.birthday_month || "?"}-${friend.birthday_day || "?"}`); } if (personalInfo.length) result += `- ${personalInfo.join(" | ")} `; const contactInfo = [friend.phone_num, friend.email].filter((x) => x && x.trim() && x !== "-"); if (contactInfo.length) result += `- ${contactInfo.join(" | ")} `; return result; } /** * 格式化群信息 * @param info 群信息对象 * @returns 格式化字符串 */ static formatGroupInfo(info) { return `${info.group_name}(${info.group_id}) [${info.member_count}/${info.max_member_count}]${info.group_remark?.trim() ? ` 备注: ${info.group_remark}` : ""}`; } /** * 格式化群成员信息 * @param member 群成员信息对象 * @returns 格式化字符串 */ static formatGroupMemberInfo(member) { const roleMap = { owner: "群主", admin: "管理员", member: "成员" }; let result = `成员 ${member.card?.trim() ? `[${member.card}]` : ""}${member.nickname}(${member.user_id}) 信息: `; const identityInfo = [ member.level && member.level !== "0" ? `LV${member.level}` : "", member.title?.trim() || "", member.card?.trim() || "", member.role !== "member" ? roleMap[member.role] || member.role : "", member.is_robot ? "Bot" : "" ].filter(Boolean); if (identityInfo.length) result += `- ${identityInfo.join(" | ")} `; const personalInfo = [ member.qq_level > 0 ? `LV${member.qq_level}` : "", member.sex && member.sex !== "unknown" ? _Onebot.sexMap[member.sex] || member.sex : "", member.age > 0 ? `${member.age}岁` : "", member.area?.trim() || "" ].filter(Boolean); if (personalInfo.length) result += `- ${personalInfo.join(" | ")} `; if (member.shut_up_timestamp > Math.floor(Date.now() / 1e3)) { const shutUpEnd = new Date(member.shut_up_timestamp * 1e3); result += `- 禁言至: ${shutUpEnd.toLocaleDateString()} ${shutUpEnd.toLocaleTimeString()} `; } if (member.join_time) result += `- 入群时间: ${new Date(member.join_time * 1e3).toLocaleString()} `; if (member.last_sent_time) result += `- 最后发言: ${new Date(member.last_sent_time * 1e3).toLocaleString()}`; return result; } /** * 提取语音文件名 * @param content 消息内容 * @returns 文件名或null */ static extractAudioFile(content) { if (!content) return null; return /<audio.*?file="(.*?)".*?\/>/i.exec(content)?.[1] || /\[CQ:record,file=(.*?)(?:,|])/i.exec(content)?.[1] || /"file"\s*:\s*"([^"]+)"/i.exec(content)?.[1] || null; } /** * 提取文件ID * @param content 消息内容 * @returns 文件ID或null */ static extractFileId(content) { if (!content) return null; return /<file.*?id="(.*?)".*?\/>/i.exec(content)?.[1] || /\[CQ:file,file=(?:.*?),id=(.*?)(?:,|])/i.exec(content)?.[1] || /"file_id"\s*:\s*"([^"]+)"/i.exec(content)?.[1] || null; } /** * 提取图片文件名 * @param content 消息内容 * @returns 文件名或null */ static extractImageFile(content) { if (!content) return null; return /<image.*?file="([^"]+)".*?\/>/i.exec(content)?.[1] || /<img.*?file="([^"]+)".*?\/>/i.exec(content)?.[1] || /\[CQ:image,(?:.*?,)?file=([^,\]]+)(?:,|])/i.exec(content)?.[1] || /"file"(?:\s*):(?:\s*)"([^"]+)"/i.exec(content)?.[1] || /https?:\/\/[^\s"'<>]+\.(jpg|jpeg|png|gif|bmp|webp)/i.exec(content)?.[0] || null; } /** * 注册onebot相关命令 * @param qgroup qgroup命令对象 */ registerCommands(qgroup) { qgroup.subcommand(".restart", "重启 OneBot", { authority: 5 }).usage("重启 OneBot 实现和 API 服务").action(async ({ session }) => { try { await session.onebot.setRestart(2e3); return "正在重启 OneBot,请稍候..."; } catch (e) { return utils.handleError(session, e); } }); qgroup.subcommand(".clean", "清理缓存", { authority: 4 }).usage("清理积攒的缓存文件").action(async ({ session }) => { try { await session.onebot.cleanCache(); return "清理缓存成功"; } catch (e) { return utils.handleError(session, e); } }); const get = qgroup.subcommand("get", "获取消息内容及状态").usage("获取指定ID消息的完整内容").option("id", "-i <id:string> 消息ID").action(async ({ session, options }) => { let messageId = options.id || session.quote?.id || session.messageId; try { const msg = await session.onebot.getMsg(messageId); return JSON.stringify(msg, null, 2); } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".forward", "获取合并转发内容").usage("获取指定合并转发ID消息的完整内容").option("id", "-i <id:string> 合并转发ID").action(async ({ session, options }) => { let messageId = options.id || session.quote?.id || session.messageId; try { const msg = await session.onebot.getForwardMsg(messageId); return JSON.stringify(msg, null, 2); } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".record", "获取语音文件", { authority: 2 }).usage("获取指定语音文件并转换格式").option("file", "-f <file:string> 文件名", { type: "string" }).option("format", "-t <format:string> 转换格式 (mp3/amr/wma/m4a/spx/ogg/wav/flac)", { fallback: "mp3" }).action(async ({ session, options }) => { let fileName = options.file || session.quote && _Onebot.extractAudioFile(session.quote.content); if (!fileName) return utils.handleError(session, new Error("未发现语音文件")); try { const result = await session.onebot.getRecord(fileName, options.format); return `语音文件路径: ${result.file}`; } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".image", "获取图片文件", { authority: 2 }).usage("获取指定图片文件的本地路径").option("file", "-f <file:string> 文件名", { type: "string" }).action(async ({ session, options }) => { let fileName = options.file || session.quote && _Onebot.extractImageFile(session.quote.content); if (!fileName) return utils.handleError(session, new Error("未发现图片文件")); try { const result = await session.onebot.getImage(fileName); return `图片文件路径: ${result.file}`; } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".file", "获取文件信息", { authority: 2 }).usage("获取指定文件ID对应的文件信息").option("id", "-i <id:string> 文件ID").action(async ({ session, options }) => { let fileId = options.id || session.quote && _Onebot.extractFileId(session.quote.content); if (!fileId) return utils.handleError(session, new Error("未发现文件")); try { const fileInfo = await session.onebot._request("get_file", { file_id: fileId }); let result = "文件信息:\n"; if (fileInfo.file_name) result += `文件名: ${fileInfo.file_name} `; if (fileInfo.file_size) result += `文件大小: ${fileInfo.file_size} `; if (fileInfo.file) result += `文件路径: ${fileInfo.file} `; if (fileInfo.url) result += `文件链接: ${fileInfo.url} `; if (fileInfo.base64) result += `文件Base64长度: ${fileInfo.base64.length}字符 `; return result; } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".stat", "获取运行状态").usage("获取运行状态信息").action(async ({ session }) => { try { const status = await session.onebot.getStatus(); let result = `运行状态: ${status.online ? "在线" : "离线"} | ${status.good ? "正常" : "异常"} `; Object.entries(status).forEach(([k, v]) => { if (k !== "online" && k !== "good") result += `${k}: ${JSON.stringify(v)} `; }); return result; } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".ver", "获取版本信息").usage("获取版本信息").action(async ({ session }) => { try { const version = await session.onebot.getVersionInfo(); let result = `应用标识: ${version.app_name} 应用版本: ${version.app_version} 协议版本: ${version.protocol_version} `; Object.entries(version).forEach(([k, v]) => { if (!["app_name", "app_version", "protocol_version"].includes(k)) result += `${k}: ${JSON.stringify(v)} `; }); return result; } catch (e) { return utils.handleError(session, e); } }); get.subcommand(".csrf [domain:string]", "获取相关接口凭证", { authority: 4 }).usage("获取指定域名的Cookies和CSRF Token").action(async ({ session }, domain) => { try { const credentials = await session.onebot.getCredentials(domain || ""); return `接口凭证信息: CSRF Token: ${credentials.csrf_token} Cookies: ${credentials.cookies}`; } catch (e) { return utils.handleError(session, e); } }); const info = qgroup.subcommand("info", "查询账号信息", { authority: 5 }).usage("查询当前账号的基本信息").option("no-cache", "-n 不使用缓存", { fallback: false }).action(async ({ session, options }) => { try { const loginInfo = await session.onebot.getLoginInfo(); try { const detailInfo = await session.onebot.getStrangerInfo(loginInfo.user_id, options["no-cache"]); return _Onebot.formatUserInfo(detailInfo); } catch { return `账号信息: ${loginInfo.nickname}(${loginInfo.user_id})`; } } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".user <user_id:string>", "查询其它账号信息").usage("查询指定账号的基本信息").option("no-cache", "-n 不使用缓存", { fallback: false }).action(async ({ session, options }, user_id) => { const parsedId = utils.parseTarget(user_id); if (!parsedId) return utils.handleError(session, new Error("请提供QQ号")); try { const botInfo = await session.onebot.getLoginInfo(); if (parsedId === botInfo.user_id.toString()) return utils.handleError(session, new Error("不允许查询自身信息")); const info2 = await session.onebot.getStrangerInfo(+parsedId, options["no-cache"]); return _Onebot.formatUserInfo(info2); } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".myfriend [page:string]", "获取本账号好友列表", { authority: 4 }).usage("获取本账号的完整好友列表及备注").action(async ({ session }, page) => { try { const friends = await session.onebot.getFriendList(); let result = `好友数量: ${friends.length}`; const pagination = this.handlePagination(session, friends, page); if (!pagination) return; result += pagination.pageInfo; pagination.displayData.forEach((friend) => result += _Onebot.formatFriendInfo(friend)); return result; } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".mygroup [page:string]", "获取本账号群组列表", { authority: 4 }).usage("获取本账号加入的群组列表").action(async ({ session }, page) => { try { const groups = await session.onebot.getGroupList(); let result = `群数量: ${groups.length}`; const pagination = this.handlePagination(session, groups, page); if (!pagination) return; result += pagination.pageInfo; pagination.displayData.forEach((group) => result += _Onebot.formatGroupInfo(group) + "\n"); return result; } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".group [group_id:number]", "查询群信息").usage("查询指定群的基本信息").option("no-cache", "-n 不使用缓存", { fallback: false }).action(async ({ session, options }, group_id) => { group_id = group_id || +session.guildId; if (!group_id) return utils.handleError(session, new Error("请提供群号")); try { const info2 = await session.onebot.getGroupInfo(group_id, options["no-cache"]); return _Onebot.formatGroupInfo(info2); } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".groupuser <user_id:string> [group_id:number]", "查询群成员信息").usage("查询群内指定成员的基本信息").option("no-cache", "-n 不使用缓存", { fallback: false }).action(async ({ session, options }, user_id, group_id) => { const parsedId = utils.parseTarget(user_id); if (!parsedId) return utils.handleError(session, new Error("请提供QQ号")); group_id = group_id || +session.guildId; if (!group_id) return utils.handleError(session, new Error("请提供群号")); try { const info2 = await session.onebot.getGroupMemberInfo(group_id, +parsedId, options["no-cache"]); return _Onebot.formatGroupMemberInfo(info2); } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".memberlist [group_id:number] [page:string]", "获取群成员列表").usage("获取指定群的成员列表").action(async ({ session }, group_id, page) => { group_id = group_id || +session.guildId; if (!group_id) return utils.handleError(session, new Error("请提供群号")); try { const members = await session.onebot.getGroupMemberList(group_id); let result = `群 ${group_id} 成员列表`; members.sort((a, b) => ({ owner: 0, admin: 1, member: 2 })[a.role] - { owner: 0, admin: 1, member: 2 }[b.role]); const pagination = this.handlePagination(session, members, page, 5); if (!pagination) return; result += pagination.pageInfo; pagination.displayData.forEach((member) => result += _Onebot.formatGroupMemberInfo(member) + "\n"); return result; } catch (e) { return utils.handleError(session, e); } }); info.subcommand(".grouphonor [group_id:number]", "查询群荣誉信息").usage("可用参数:\n- talkative: 历史龙王\n- performer: 群聊之火\n- legend: 群聊炽焰\n- strong_newbie: 冒尖小春笋\n- emotion: 快乐之源").option("type", "-t <type> 荣誉类型", { fallback: "all" }).action(async ({ session, options }, group_id) => { group_id = group_id || +session.guildId; if (!group_id) return utils.handleError(session, new Error("请提供群号")); try { const honorInfo = await session.onebot.getGroupHonorInfo(group_id, options.type); let result = `群 ${group_id} 荣誉信息: `; const honorTypeNames = { talkative: "历史龙王", performer: "群聊之火", legend: "群聊炽焰", strong_newbie: "冒尖小春笋", emotion: "快乐之源" }; if (honorInfo.current_talkative) result += `- 龙王: ${honorInfo.current_talkative.nickname}(${honorInfo.current_talkative.user_id}) `; for (const type of Object.keys(honorTypeNames)) { const list = honorInfo[`${type}_list`]; if (list?.length) { result += `${honorTypeNames[type]} (${list.length}名): `; list.slice(0, 5).forEach((item) => result += `- ${item.nickname}(${item.user_id}) | ${item.description} `); } } return result; } catch (e) { return utils.handleError(session, e); } }); } }; // src/index.ts var name = "onebot-manager"; var inject = { optional: ["database"] }; var usage = ` <div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);"> <h2 style="margin-top: 0; color: #4a6ee0;">📌 插件说明</h2> <p>📖 <strong>使用文档</strong>:请点击左上角的 <strong>插件主页</strong> 查看插件使用文档</p> <p>🔍 <strong>更多插件</strong>:可访问 <a href="https://github.com/YisRime" style="color:#4a6ee0;text-decoration:none;">苡淞的 GitHub</a> 查看本人的所有插件</p> </div> <div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);"> <h2 style="margin-top: 0; color: #e0574a;">❤️ 支持与反馈</h2> <p>🌟 喜欢这个插件?请在 <a href="https://github.com/YisRime" style="color:#e0574a;text-decoration:none;">GitHub</a> 上给我一个 Star!</p> <p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p> </div> `; var Config = import_koishi2.Schema.intersect([ import_koishi2.Schema.object({ enable: import_koishi2.Schema.boolean().description("开启请求监听").default(true) }).description("基础配置"), import_koishi2.Schema.object({ friendRequest: import_koishi2.Schema.union([ import_koishi2.Schema.const("accept").description("同意"), import_koishi2.Schema.const("reject").description("拒绝"), import_koishi2.Schema.const("manual").description("手动"), import_koishi2.Schema.const("auto").description("自动") ]).description("处理好友请求").default("reject"), memberRequest: import_koishi2.Schema.union([ import_koishi2.Schema.const("accept").description("同意"), import_koishi2.Schema.const("reject").description("拒绝"), import_koishi2.Schema.const("manual").description("手动"), import_koishi2.Schema.const("auto").description("自动") ]).description("处理加群请求").default("reject"), guildRequest: import_koishi2.Schema.union([ import_koishi2.Schema.const("accept").description("同意"), import_koishi2.Schema.const("reject").description("拒绝"), import_koishi2.Schema.const("manual").description("手动"), import_koishi2.Schema.const("auto").description("自动") ]).description("处理入群邀请").default("reject"), manualTimeout: import_koishi2.Schema.number().description("手动超时时间(分钟)").default(720).min(0), manualTimeoutAction: import_koishi2.Schema.union([ import_koishi2.Schema.const("accept").description("同意"), import_koishi2.Schema.const("reject").description("拒绝") ]).description("超时自动操作").default("reject"), enableNotify: import_koishi2.Schema.boolean().description("开启请求通知").default(false) }).description("请求配置"), import_koishi2.Schema.union([ import_koishi2.Schema.object({ enableNotify: import_koishi2.Schema.const(true).required(), notifyTarget: import_koishi2.Schema.string().description("通知目标(guild/private)").default("private:10000") }), import_koishi2.Schema.object({}) ]), import_koishi2.Schema.union([ import_koishi2.Schema.object({ friendRequest: import_koishi2.Schema.const("auto").required(), FriendRegTime: import_koishi2.Schema.number().description("最短注册年份").default(-1).min(-1), FriendLevel: import_koishi2.Schema.number().description("最低QQ等级").default(-1).min(-1).max(256), FriendVipLevel: import_koishi2.Schema.number().description("最低会员等级").default(-1).min(-1).max(10) }).description("好友请求通过配置"), import_koishi2.Schema.object({}) ]), import_koishi2.Schema.union([ import_koishi2.Schema.object({ memberRequest: import_koishi2.Schema.const("auto").required(), MemberRegTime: import_koishi2.Schema.number().description("最短注册年份").default(-1).min(-1), MemberLevel: import_koishi2.Schema.number().description("最低QQ等级").default(-1).min(-1).max(256), MemberVipLevel: import_koishi2.Schema.number().description("最低会员等级").default(-1).min(-1).max(10) }).description("加群请求通过配置"), import_koishi2.Schema.object({}) ]), import_koishi2.Schema.union([ import_koishi2.Schema.object({ guildRequest: import_koishi2.Schema.const("auto").required(), GuildAllowUsers: import_koishi2.Schema.array(String).description("白名单邀请人ID").default([]), GuildMinMemberCount: import_koishi2.Schema.number().description("最低群成员数量").default(-1).min(-1).max(3e3), GuildMaxCapacity: import_koishi2.Schema.union([ import_koishi2.Schema.const(-1).description("不限制"), import_koishi2.Schema.const(200).description("200"), import_koishi2.Schema.const(500).description("500"), import_koishi2.Schema.const(1e3).description("1000"), import_koishi2.Schema.const(2e3).description("2000"), import_koishi2.Schema.const(3e3).description("3000") ]).description("最低群容量要求").default(-1) }).description("入群邀请通过配置"), import_koishi2.Schema.object({}) ]) ]); function apply(ctx, config = {}) { const logger = ctx.logger("onebot-manager"); if (config.enable !== false) new OnebotRequest(ctx, logger, config).registerEventListeners(); const qgroup = ctx.command("qgroup", "QQ 群管").usage("群管相关功能,需要管理权限"); new Onebot().registerCommands(qgroup); registerCommands(qgroup, logger, utils); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name, usage });