UNPKG

koishi-plugin-onebot-tool

Version:

OneBot 工具集,带有点赞、打卡、拍一拍、表情回应和 AI 语音等功能,可独立开关和自由配置

1,513 lines (1,501 loc) 51.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 index_exports = {}; __export(index_exports, { Config: () => Config4, PokeMode: () => PokeMode, SignMode: () => SignMode, StickMode: () => StickMode, ZanwoMode: () => ZanwoMode, apply: () => apply, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(index_exports); var import_koishi5 = require("koishi"); // src/utils.ts var import_koishi = require("koishi"); var import_promises = require("fs/promises"); var import_fs = require("fs"); var import_path = require("path"); var utils = { /** * 解析目标用户ID (支持@元素、@数字格式或纯数字) */ parseTarget(target) { if (!target) return null; try { const at = import_koishi.h.select(import_koishi.h.parse(target), "at")[0]?.attrs?.id; if (at) return at; } catch { } const m = target.match(/@?(\d{5,10})/); return m ? m[1] : null; }, /** * 自动撤回消息 */ async autoRecall(session, message, delay = 1e4) { if (!message) return; setTimeout(() => session.bot?.deleteMessage(session.channelId, message.toString()), delay); }, /** * 读取所有模块数据 */ async getAllModuleData(baseDir, logger) { const filePath = (0, import_path.resolve)(baseDir, "data", "onebot-tool.json"); if (!(0, import_fs.existsSync)(filePath)) return {}; try { const data = JSON.parse(await (0, import_promises.readFile)(filePath, "utf8")); return typeof data === "object" && data ? data : {}; } catch (e) { logger.error("读取数据文件失败:", e); return {}; } }, /** * 保存所有模块数据 */ async saveAllModuleData(baseDir, data, logger) { try { await (0, import_promises.writeFile)((0, import_path.resolve)(baseDir, "data", "onebot-tool.json"), JSON.stringify(data, null, 2)); return true; } catch (e) { logger.error("保存数据文件失败:", e); return false; } }, /** * 加载指定模块的数据 */ async loadModuleData(baseDir, moduleName, logger) { return (await this.getAllModuleData(baseDir, logger))[moduleName] ?? []; }, /** * 保存指定模块的数据 */ async saveModuleData(baseDir, moduleName, data, logger) { const allData = await this.getAllModuleData(baseDir, logger); allData[moduleName] = data; return this.saveAllModuleData(baseDir, allData, logger); }, /** * 检查文件是否为图片 */ isImageFile(filename) { const ext = (0, import_path.extname)(filename).toLowerCase(); return [".jpg", ".jpeg", ".png", ".gif", ".webp"].includes(ext); }, /** * 获取本地目录中的图片文件列表 */ async getLocalImages(dirPath, logger) { try { if (!(0, import_fs.existsSync)(dirPath) || !(0, import_fs.statSync)(dirPath).isDirectory()) return []; const files = await (0, import_promises.readdir)(dirPath); const imagePaths = files.filter((file) => this.isImageFile(file)).map((file) => (0, import_path.resolve)(dirPath, file)); return imagePaths; } catch (e) { logger.error(`读取图片目录失败: ${e.message}`); return []; } }, /** * 获取Pixiv图片链接数组(支持网络JSON或本地目录) */ async getPixivLinks(baseDir, path, logger) { if (path.startsWith("http://") || path.startsWith("https://")) { const filePath = (0, import_path.resolve)(baseDir, "data", "pixiv.json"); if (!(0, import_fs.existsSync)(filePath)) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3e4); const res = await fetch(path, { signal: controller.signal }); clearTimeout(timeout); if (!res.ok) throw new Error(`下载失败: ${res.status}`); await (0, import_promises.writeFile)(filePath, await res.text(), "utf8"); } catch (e) { logger.error("下载JSON文件失败:", e); return []; } } try { const arr = JSON.parse(await (0, import_promises.readFile)(filePath, "utf8")); return Array.isArray(arr) ? arr : []; } catch (e) { logger.error("读取Pixiv链接失败:", e); return []; } } else { return this.getLocalImages(path, logger); } } }; // src/zanwo.ts var Zanwo = class { static { __name(this, "Zanwo"); } targets = /* @__PURE__ */ new Set(); moduleName = "zanwo"; logger; ctx; cronJob; timer; config; /** * 构造函数 * @param ctx Koishi 上下文 * @param config 插件配置 * @param logger 日志记录器 */ constructor(ctx, config, logger) { this.ctx = ctx; this.config = config; this.logger = logger; this.loadTargetsFromFile().catch((err) => this.logger.error("加载点赞列表失败:", err)); if (this.config.zanwoMode === "auto" /* Auto */) this.startAutoLikeTimer(); } /** * 从文件加载点赞目标列表 * @private */ async loadTargetsFromFile() { this.targets = new Set(await utils.loadModuleData(this.ctx.baseDir, this.moduleName, this.logger)); } /** * 启动自动点赞定时器 * @private */ startAutoLikeTimer() { this.cronJob?.dispose(); this.timer && clearInterval(this.timer); this.cronJob = this.timer = null; if (this.config.zanwoMode !== "auto" /* Auto */ || !this.targets.size) return; if (typeof this.ctx.cron === "function") { this.cronJob = this.ctx.cron("0 0 * * *", () => this.executeAutoLike()); this.logger.info("已设置每日自动点赞定时任务"); } else { this.timer = setInterval(() => this.executeAutoLike(), 864e5); this.logger.info("已设置每日自动点赞定时器"); } } /** * 执行自动点赞 * @param session 可选,Koishi 会话对象 * @private */ async executeAutoLike(session) { const targets = [...this.targets]; if (!targets.length) return; try { let successCount = 0; for (const userId of targets) { if (await this.sendLike(session, userId)) successCount++; await new Promise((r) => setTimeout(r, 1e3)); } this.logger.info(`自动点赞完成:成功 ${successCount}/${targets.length} 人`); } catch (error) { this.logger.error("自动点赞出错:", error); } } /** * 处理点赞目标列表的增删查清 * @param action 操作类型:'add'添加, 'remove'移除, 'get'获取, 'clear'清空 * @param userId 用户ID,用于add和remove操作 * @returns 操作结果。get返回目标数组,其他返回布尔值表示成功与否 */ async handleTargets(action, userId) { if (action === "get") return [...this.targets]; if (action === "clear") { const isEmpty = !this.targets.size; if (!isEmpty) { this.targets.clear(); await utils.saveModuleData(this.ctx.baseDir, this.moduleName, [], this.logger); } return !isEmpty; } if (!userId || !/^\d+$/.test(userId)) return false; const result = action === "add" ? !!this.targets.add(userId) : this.targets.delete(userId); if (result) await utils.saveModuleData(this.ctx.baseDir, this.moduleName, [...this.targets], this.logger); return result; } /** * 发送点赞请求 * @param session Koishi 会话对象 * @param userId QQ用户ID * @returns 是否点赞成功 */ async sendLike(session, userId) { let success = false; for (let i = 0; i < 5; i++) { try { await session.bot.internal.sendLike(userId, 10); success = true; } catch { break; } } return success; } /** * 注册点赞相关命令 * @param parentCmd 父命令对象 */ registerCommands(parentCmd) { const handleReply = /* @__PURE__ */ __name(async (session, message) => { const msg = await session.send(message); await utils.autoRecall(session, Array.isArray(msg) ? msg[0] : msg); return ""; }, "handleReply"); const zanwo = parentCmd.subcommand("zanwo", "点赞功能").alias("赞我").usage("点赞自己 50 下").action(async ({ session }) => handleReply(session, await this.sendLike(session, session.userId) ? `点赞完成,记得回赞哦~` : "点赞失败,请尝试添加好友")); zanwo.subcommand(".list", "查看列表", { authority: 3 }).usage("查看自动点赞用户列表").action(async () => { const targets = await this.handleTargets("get"); return targets.length ? `当前点赞列表(共${targets.length}人):${targets.join(", ")}` : "点赞列表为空"; }); zanwo.subcommand(".add <target:text>", "添加用户", { authority: 2 }).usage("添加用户到点赞列表").action(async ({ session }, target) => { const userId = utils.parseTarget(target); return handleReply(session, userId && await this.handleTargets("add", userId) ? `已添加 ${userId} 到点赞列表` : "添加失败"); }); zanwo.subcommand(".remove <target:text>", "移除用户", { authority: 2 }).usage("从列表移除指定用户").action(async ({ session }, target) => { const userId = utils.parseTarget(target); return handleReply(session, userId && await this.handleTargets("remove", userId) ? `已从点赞列表移除 ${userId}` : "移除失败"); }); zanwo.subcommand(".user <target:text>", "点赞用户").usage("给指定用户点赞").action(async ({ session }, target) => { const userId = utils.parseTarget(target); return handleReply(session, userId && await this.sendLike(session, userId) ? `点赞完成,记得回赞哦~` : "点赞失败,请尝试添加好友"); }); zanwo.subcommand(".all", "全部点赞", { authority: 3 }).usage("点赞列表中所有用户").action(async ({ session }) => (this.executeAutoLike(session), "已开始点赞")); zanwo.subcommand(".clear", "清空列表", { authority: 4 }).usage("清空点赞列表").action(async () => (this.handleTargets("clear"), "已清空点赞列表")); } /** * 释放资源 */ dispose() { this.cronJob?.dispose(); this.cronJob = null; if (this.timer) clearInterval(this.timer); this.timer = null; } }; // src/poke.ts var import_koishi2 = require("koishi"); var import_promises2 = require("fs/promises"); var import_fs2 = require("fs"); var import_path2 = require("path"); var Poke = class { /** * 构造函数 * @param ctx Koishi 上下文 * @param config 插件配置 * @param logger 日志记录器 */ constructor(ctx, config, logger) { this.ctx = ctx; this.config = config; this.logger = logger; if (config?.responses?.length) { this.totalWeight = config.responses.reduce((sum, resp) => sum + resp.weight, 0); if (this.totalWeight > 100) { const scale = 100 / this.totalWeight; config.responses.forEach((resp) => resp.weight *= scale); this.totalWeight = 100; } } this.imagesPath = config.imagesPath; } static { __name(this, "Poke"); } cache = /* @__PURE__ */ new Map(); totalWeight = 0; imagesPath; logger; /** * 释放资源 */ dispose() { this.cache.clear(); } /** * 获取一言内容 * @param params 可选参数 * @returns 一言内容字符串 * @private */ async getHitokoto(params) { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3e3); const url = `https://v1.hitokoto.cn/${params ? `?${params}` : ""}`; const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeout); if (!response.ok) return ""; const data = await response.json(); if (!data?.hitokoto) return ""; if (!data.from) return data.hitokoto; const showAuthor = data.from_who && data.from_who !== data.from; const citation = `——${showAuthor ? ` ${data.from_who}` : ""}《${data.from}》`; const getTextWidth = /* @__PURE__ */ __name((text) => [...text].reduce((w, c) => w + (/[\u4e00-\u9fa5\u3000-\u30ff\u3130-\u318f\uac00-\ud7af]/.test(c) ? 2 : 1), 0), "getTextWidth"); const contentWidth = getTextWidth(data.hitokoto), citationWidth = getTextWidth(citation), maxWidth = 36; const spaces = Math.max(0, Math.min(contentWidth, maxWidth) - citationWidth); return `${data.hitokoto} ${" ".repeat(spaces)}${citation}`; } catch { return ""; } } /** * 替换响应内容中的占位符 * @param content 响应内容 * @param session Koishi 会话对象 * @returns 替换后的内容 * @private */ async replacePlaceholders(content, session) { if (!content.includes("{")) return content; const hitokotoMatches = [...content.matchAll(/{hitokoto(?::([^}]*))?}/g)]; const pixivMatches = [...content.matchAll(/{pixiv}/g)]; let hitokotoContents = hitokotoMatches.length ? await Promise.all(hitokotoMatches.map((m) => this.getHitokoto(m[1]).then((content2) => ({ pattern: m[0], content: content2 })))) : []; let pixivContents = []; if (pixivMatches.length) { const arr = await utils.getPixivLinks(this.ctx.baseDir, this.imagesPath, this.logger); pixivContents = await Promise.all(pixivMatches.map(async (m) => { let content2 = ""; if (Array.isArray(arr) && arr.length) { const imagePath = arr[Math.floor(Math.random() * arr.length)]; try { if (!imagePath.startsWith("http")) { if ((0, import_fs2.existsSync)(imagePath)) { const buffer = await (0, import_promises2.readFile)(imagePath); const ext = (0, import_path2.extname)(imagePath).toLowerCase().substring(1) || "jpg"; const mime = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg"; content2 = `<image src="base64://${buffer.toString("base64")}" type="${mime}"/>`; } else { this.logger.warn(`图片文件不存在: ${imagePath}`); } } else { const res = await fetch(imagePath, { headers: { "Referer": "https://www.pixiv.net/" } }); if (res.ok) { const buffer = Buffer.from(await res.arrayBuffer()); const ext = imagePath.split(".").pop()?.toLowerCase() || "jpg"; const mime = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : "image/jpeg"; content2 = `<image src="base64://${buffer.toString("base64")}" type="${mime}"/>`; } } } catch (e) { this.logger.error("图片发送失败:", e); } } return { pattern: m[0], content: content2 }; })); } let result = content.replace(/{at}/g, `<at id="${session.userId}"/>`).replace(/{username}/g, session.username).replace(/{image:([^}]+)}/g, '<image url="$1"/>'); hitokotoContents.forEach(({ pattern, content: content2 }) => result = result.replace(pattern, content2)); pixivContents.forEach(({ pattern, content: content2 }) => result = result.replace(pattern, content2)); return result; } /** * 注册拍一拍命令 * @param parentCmd 父命令对象 */ registerCommand(parentCmd) { parentCmd.subcommand("poke [times:number] [target:string]", "拍一拍").usage("发送拍一拍,可指定次数和目标").example("poke - 拍自己一次").example("poke 3 @12345 - 拍用户@12345三次").action(async ({ session }, times, target) => { try { if (typeof times === "string" && !target) [target, times] = [times, 1]; times = Math.max(1, Math.floor(Number(times))); if (isNaN(times)) times = 1; if (times > this.config.maxTimes) { const msgId = await session.send(`单次拍一拍请求不能超过${this.config.maxTimes}次哦~`); utils.autoRecall(session, Array.isArray(msgId) ? msgId[0] : msgId); return; } const parsedId = target ? utils.parseTarget(target) : null; const targetId = !target || !parsedId ? session.userId : parsedId; for (let i = 0; i < times; i++) { await session.onebot._request("send_poke", { user_id: targetId, group_id: session.isDirect ? void 0 : session.guildId }); if (times > 1 && i < times - 1) await new Promise((r) => setTimeout(r, this.config.actionInterval)); } return ""; } catch { return; } }); } /** * 处理拍一拍通知事件 * @param session Koishi 会话对象 * @returns 是否已响应 */ async processNotice(session) { if (session.subtype !== "poke" || session.targetId !== session.selfId) return false; const lastTime = this.cache.get(session.userId); if (lastTime && session.timestamp - lastTime < 1e3) return false; this.cache.set(session.userId, session.timestamp); if (!this.config?.responses?.length) return false; const response = this.randomResponse(); if (!response) return false; try { if (response.type === "command") { session._responseTriggered = true; await session.execute(response.content); delete session._responseTriggered; } else { for (const seg of response.content.split("{~}")) { await session.sendQueued(import_koishi2.h.parse(await this.replacePlaceholders(seg, session), session)); } } return true; } catch { return false; } } /** * 随机选择一个拍一拍响应 * @returns 响应对象 * @private */ randomResponse() { if (!this.config?.responses?.length) return null; const responses = this.config.responses; let sum = 0, random = Math.random() * this.totalWeight; for (const response of responses) if ((sum += response.weight) > random) return response; return responses[0]; } }; // src/stick.ts var import_koishi3 = require("koishi"); // src/emojimap.ts var EMOJI_MAP = { "惊讶": "0", "撇嘴": "1", "色": "2", "发呆": "3", "得意": "4", "流泪": "5", "害羞": "6", "闭嘴": "7", "睡": "8", "大哭": "9", "尴尬": "10", "发怒": "11", "调皮": "12", "呲牙": "13", "微笑": "14", "难过": "15", "酷": "16", "抓狂": "18", "吐": "19", "偷笑": "20", "可爱": "21", "白眼": "22", "傲慢": "23", "饥饿": "24", "困": "25", "惊恐": "26", "流汗": "27", "憨笑": "28", "悠闲": "29", "奋斗": "30", "咒骂": "31", "疑问": "32", "嘘": "33", "晕": "34", "折磨": "35", "衰": "36", "骷髅": "37", "敲打": "38", "再见": "39", "发抖": "41", "爱情": "42", "跳跳": "43", "猪头": "46", "拥抱": "49", "蛋糕": "53", "55": "55", "刀": "56", "便便": "59", "咖啡": "60", "玫瑰": "63", "凋谢": "64", "爱心": "66", "心碎": "67", "太阳": "74", "月亮": "75", "赞": "76", "踩": "77", "握手": "78", "胜利": "79", "飞吻": "85", "怄火": "86", "西瓜": "89", "冷汗": "96", "擦汗": "97", "抠鼻": "98", "鼓掌": "99", "糗大了": "100", "坏笑": "101", "左哼哼": "102", "右哼哼": "103", "哈欠": "104", "鄙视": "105", "委屈": "106", "快哭了": "107", "阴险": "108", "左亲亲": "109", "吓": "110", "可怜": "111", "菜刀": "112", "篮球": "114", "示爱": "116", "抱拳": "118", "勾引": "119", "拳头": "120", "差劲": "121", "122": "122", "NO": "123", "OK": "124", "转圈": "125", "挥手": "129", "鞭炮": "137", "喝彩": "144", "爆筋": "146", "棒棒糖": "147", "148": "148", "手枪": "169", "茶": "171", "眨眼睛": "172", "泪奔": "173", "无奈": "174", "卖萌": "175", "小纠结": "176", "喷血": "177", "斜眼笑": "178", "doge": "179", "180": "180", "戳一戳": "181", "笑哭": "182", "我最美": "183", "羊驼": "185", "幽灵": "187", "194": "194", "198": "198", "200": "200", "点赞": "201", "202": "202", "203": "203", "204": "204", "206": "206", "210": "210", "211": "211", "托腮": "212", "214": "214", "215": "215", "216": "216", "217": "217", "218": "218", "219": "219", "221": "221", "222": "222", "223": "223", "224": "224", "225": "225", "226": "226", "227": "227", "229": "229", "230": "230", "231": "231", "232": "232", "233": "233", "235": "235", "237": "237", "238": "238", "239": "239", "240": "240", "241": "241", "243": "243", "244": "244", "脑阔疼": "262", "沧桑": "263", "捂脸": "264", "辣眼睛": "265", "哦哟": "266", "头秃": "267", "问号脸": "268", "暗中观察": "269", "emm": "270", "吃瓜": "271", "呵呵哒": "272", "我酸了": "273", "汪汪": "277", "279": "279", "无眼笑": "281", "敬礼": "282", "狂笑": "283", "面无表情": "284", "摸鱼": "285", "魔鬼笑": "286", "哦": "287", "288": "288", "睁眼": "289", "290": "290", "292": "292", "摸锦鲤": "293", "期待": "294", "拿到红包": "295", "拜谢": "297", "元宝": "298", "牛啊": "299", "胖三斤": "300", "301": "301", "左拜年": "302", "右拜年": "303", "右亲亲": "305", "牛气冲天": "306", "喵喵": "307", "打call": "311", "变形": "312", "仔细分析": "314", "菜汪": "317", "崇拜": "318", "比心": "319", "庆祝": "320", "322": "322", "嫌弃": "323", "吃糖": "324", "惊吓": "325", "生气": "326", "举牌牌": "332", "烟花": "333", "虎虎生威": "334", "335": "335", "豹富": "336", "花朵脸": "337", "我想开了": "338", "舔屏": "339", "打招呼": "341", "酸Q": "342", "我方了": "343", "大怨种": "344", "红包多多": "345", "你真棒棒": "346", "大展宏兔": "347", "348": "348", "坚强": "349", "贴贴": "350", "敲敲": "351", "咦": "352", "拜托": "353", "尊嘟假嘟": "354", "耶": "355", "666": "356", "裂开": "357", "358": "358", "359": "359", "360": "360", "361": "361", "362": "362", "363": "363", "364": "364", "365": "365", "366": "366", "367": "367", "368": "368", "369": "369", "370": "370", "371": "371", "372": "372", "373": "373", "374": "374", "375": "375", "376": "376", "377": "377", "378": "378", "379": "379", "380": "380", "381": "381", "382": "382", "383": "383", "384": "384", "385": "385", "386": "386", "387": "387", "388": "388", "389": "389", "390": "390", "391": "391", "龙年快乐": "392", "新年中龙": "393", "新年大龙": "394", "略略略": "395", "396": "396", "397": "397", "398": "398", "399": "399", "400": "400", "401": "401", "402": "402", "403": "403", "404": "404", "405": "405", "406": "406", "407": "407", "408": "408", "409": "409", "410": "410", "411": "411", "412": "412", "413": "413", "划龙舟": "415", "划龙舟2": "416", "划龙舟3": "417", "火车": "419", "火车2": "420", "火车3": "421", "龙舟": "422", "复兴号": "423", "424": "424", "425": "425", "426": "426", "427": "427", "428": "428", "灵蛇献瑞": "432", "嘿嘿": "😊", "羞涩": "😌", "亲亲": "😚", "汗": "😓", "紧张": "😰", "吐舌": "😝", "呲牙2": "😁", "淘气": "😜", "花痴": "😍", "失落": "😔", "高兴": "😄", "哼哼": "😏", "不屑": "😒", "瞪眼": "😳", "大哭2": "😭", "害怕": "😱", "激动": "😂", "肌肉": "💪", "厉害": "👍", "合十": "🙏", "好的": "👌", "向上": "👆", "眼睛": "👀", "拉面": "🍜", "刨冰": "🍧", "面包": "🍞", "啤酒": "🍺", "干杯": "🍻", "苹果": "🍎", "草莓": "🍓", "吸烟": "🚬", "庆祝2": "🎉", "礼物": "💝", "炸弹": "💣", "闪光": "✨", "吹气": "💨", "水": "💦", "火": "🔥", "睡觉": "💤", "打针": "💉", "邮箱": "📫", "骑马": "🐎", "女孩": "👧", "男孩": "👦", "猴": "🐵", "猪": "🐷", "牛": "🐮", "公鸡": "🐔", "青蛙": "🐸", "虫": "🐛", "狗": "🐶", "鲸鱼": "🐳", "靴子": "👢", "晴天": "☀", "问号": "❔", "手枪2": "🔫", "爱心2": "💓", "便利店": "🏪", "默认表情": "default" }; var COMMON_EMOJIS = [ "😊", "😌", "😚", "😓", "😰", "😝", "😁", "😜", "😍", "😔", "😄", "😏", "😒", "😳", "😭", "😱", "😂", "💪", "👍", "🙏", "👌", "👆", "👀", "🍜", "🍧", "🍞", "🍺", "🍻", "🍎", "🍓", "🚬", "🎉", "💝", "💣", "✨", "💨", "💦", "🔥", "💤", "💉", "📫", "🐎", "👧", "👦", "🐵", "🐷", "🐮", "🐔", "🐸", "🐛", "🐶", "🐳", "👢", "☀", "❔", "🔫", "💓", "🏪" ]; // src/stick.ts var Stick = class { static { __name(this, "Stick"); } logger; numericEmojiIds; keywordMap = /* @__PURE__ */ new Map(); mode; /** * 构造函数 * @param ctx Koishi 上下文 * @param config 插件配置 * @param logger 日志记录器 */ constructor(ctx, config, logger) { this.logger = logger || ctx.logger("stick"); this.numericEmojiIds = Object.values(EMOJI_MAP).filter((id) => /^\d+$/.test(id)); this.mode = config?.stickMode || "off" /* Off */; config?.keywordEmojis?.forEach((item) => { const emojiId = this.resolveEmojiId(item.emojiId); emojiId ? this.keywordMap.set(item.keyword, emojiId) : this.logger.warn(`无效的表情ID或名称: ${item.emojiId}`); }); } /** * 解析表情ID - 将名称或ID转换为有效的表情ID * @param input 表情名称或ID * @returns 有效的表情ID或null * @private */ resolveEmojiId(input) { if (EMOJI_MAP[input]) return EMOJI_MAP[input]; if (/^\d+$/.test(input) || input === "default" || COMMON_EMOJIS.some((e) => input.startsWith(e))) return input; return null; } /** * 处理消息中的表情回应 * @param session Koishi 会话对象 * @returns 是否已作出表情回应 */ async processMessage(session) { if (session.userId === session.selfId) return false; try { let responded = false; for (const [keyword, emojiId] of this.keywordMap) if (session.content.includes(keyword)) { await this.addReaction(session, session.messageId, emojiId); responded = true; } if (this.mode === "keyword" /* KeywordOnly */) return responded; for (const element of import_koishi3.h.select(import_koishi3.h.parse(session.content), "face")) if (element.attrs?.id) { await this.addReaction(session, session.messageId, element.attrs.id); responded = true; } return responded; } catch (error) { this.logger.warn("表情处理失败:", error); return false; } } /** * 格式化表情列表 * @param emojiList 表情列表 * @param page 页码 * @param keyword 搜索关键词 * @returns 格式化后的表情列表字符串 * @private */ formatEmojiList(emojiList, page = 1, keyword = "") { emojiList.sort((a, b) => { const numA = parseInt(a[1]), numB = parseInt(b[1]); if (!isNaN(numA) && !isNaN(numB)) return numA - numB; if (!isNaN(numA)) return -1; if (!isNaN(numB)) return 1; return a[1].localeCompare(b[1]); }); const total = emojiList.length; if (!total) return keyword ? `没有找到表情"${keyword}"` : "没有可用的表情"; const itemsPerRow = 4, rowsPerPage = 9, pageSize = itemsPerRow * rowsPerPage; const totalPages = Math.ceil(total / pageSize) || 1; const validPage = Math.min(Math.max(1, page), totalPages); const startIdx = (validPage - 1) * pageSize; const current = emojiList.slice(startIdx, startIdx + pageSize); const rows = []; for (let i = 0; i < current.length; i += itemsPerRow) rows.push(current.slice(i, i + itemsPerRow).map(([n, id]) => `${n}-${id}`).join("|")); const header = keyword ? `表情"${keyword}"(共${total}个)` : `表情列表(第${validPage}/${totalPages}页)`; return header + "\n" + rows.join("\n"); } /** * 注册表情回应命令 * @param parentCmd 父命令对象 */ registerCommand(parentCmd) { const handleError = /* @__PURE__ */ __name((e, msg) => (this.logger.warn(msg, e), msg.replace(":", "")), "handleError"); const stick = parentCmd.subcommand("stick [faceId:string]", "表情回应").usage("对消息进行表情回应,默认点赞").example("stick 76,77 - 使用表情ID 76和77回应").example('stick 赞,踩 - 使用"赞"和"踩"表情回应').action(async ({ session }, faceId) => { try { const targetId = session.quote?.messageId || session.messageId; if (!faceId) return this.addReaction(session, targetId, "76"); const ids = faceId.split(",").map((s) => this.resolveEmojiId(s.trim())).filter(Boolean); if (!ids.length) return this.addReaction(session, targetId, "76"); for (const id of ids.slice(0, 20)) { await this.addReaction(session, targetId, id); await new Promise((r) => setTimeout(r, 500)); } } catch (e) { return handleError(e, "表情回应失败:"); } }); stick.subcommand(".random [count:number]", "随机表情").usage("使用随机表情回应消息").action(async ({ session }, count = 1) => { try { const targetId = session.quote?.messageId || session.messageId; return this.sendRandomFaces(session, Math.min(count, 20), targetId); } catch (e) { return handleError(e, "随机表情发送失败:"); } }); stick.subcommand(".search [keyword:string]", "搜索表情").usage("搜索指定关键词的表情").action(({}, keyword) => { try { if (!keyword) return "请输入要搜索的关键词"; return this.formatEmojiList(Object.entries(EMOJI_MAP).filter(([n]) => n.includes(keyword)), 1, keyword); } catch (e) { return handleError(e, "表情搜索失败:"); } }); stick.subcommand(".list [page:number]", "表情列表").usage("分页查看表情列表").action(({}, page = 1) => { try { return this.formatEmojiList(Object.entries(EMOJI_MAP), page); } catch (e) { return handleError(e, "表情列表获取失败:"); } }); } /** * 添加表情回应 * @param session Koishi 会话对象 * @param messageId 消息ID * @param emojiId 表情ID * @private */ async addReaction(session, messageId, emojiId) { await session.onebot._request("set_msg_emoji_like", { message_id: messageId, emoji_id: emojiId }); await new Promise((r) => setTimeout(r, 100)); } /** * 发送多个随机表情 * @param session Koishi 会话对象 * @param count 数量 * @param messageId 消息ID * @private */ async sendRandomFaces(session, count, messageId) { if (!this.numericEmojiIds.length) return; const shuffled = [...this.numericEmojiIds]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } for (const id of shuffled.slice(0, Math.min(count, 20, shuffled.length))) { await this.addReaction(session, messageId, id); await new Promise((r) => setTimeout(r, 500)); } } }; // src/sign.ts var Sign = class { static { __name(this, "Sign"); } /** 打卡目标群ID集合 */ targets = /* @__PURE__ */ new Set(); /** 模块名称,用于数据存储 */ moduleName = "sign"; /** 日志记录器 */ logger; /** Koishi上下文 */ ctx; /** 定时任务对象 */ cronJob = null; /** 定时器 */ timer = null; /** 插件配置 */ config; /** * 创建群打卡管理实例 * @param ctx Koishi上下文 * @param config 插件配置 * @param logger 日志记录器 */ constructor(ctx, config, logger) { this.ctx = ctx; this.config = config; this.logger = logger; this.loadTargetsFromFile().catch((err) => this.logger.error("加载群打卡列表失败:", err)); if (this.config.signMode === "auto" /* Auto */) this.startAutoSignTimer(); } /** * 从文件加载打卡目标列表 * @private */ async loadTargetsFromFile() { this.targets = new Set(await utils.loadModuleData(this.ctx.baseDir, this.moduleName, this.logger)); } /** * 启动自动打卡定时器 * @private */ startAutoSignTimer() { this.cronJob?.dispose(); this.timer && clearInterval(this.timer); this.cronJob = this.timer = null; if (this.config.signMode !== "auto" /* Auto */) return; if (typeof this.ctx.cron === "function") { this.cronJob = this.ctx.cron("0 0 * * *", () => this.executeAutoSign()); this.logger.info("已设置每日自动群打卡定时任务"); } else { this.timer = setInterval(() => this.executeAutoSign(), 864e5); this.logger.info("已设置每日自动群打卡定时器"); } } /** * 获取所有群列表 * @param session Koishi 会话对象 * @returns 群ID数组 * @private */ async getAllGroups(session) { try { return (await session.bot.internal.getGroupList()).map((g) => String(g.group_id)); } catch (e) { this.logger.error("获取群列表失败:", e); return []; } } /** * 执行自动群打卡 * @param session 可选,Koishi 会话对象 * @private */ async executeAutoSign(session) { try { let targets = []; if (this.config.signMode === "auto" /* Auto */) { if (!session?.bot) return; targets = await this.getAllGroups(session); } else { targets = [...this.targets]; } if (!targets.length) return; let successCount = 0; for (const groupId of targets) if (await this.sendGroupSign(session, groupId)) successCount++; else this.logger.warn(`群 ${groupId} 打卡失败`); this.logger.info(`自动群打卡完成:成功 ${successCount}/${targets.length} 个群`); } catch (e) { this.logger.error("自动群打卡出错:", e); } } /** * 处理打卡目标列表的增删查清 * @param action 操作类型:'add'添加, 'remove'移除, 'get'获取, 'clear'清空 * @param groupId 群ID,用于add和remove操作 * @returns 操作结果 */ async handleTargets(action, groupId) { if (action === "get") return [...this.targets]; if (action === "clear") { const changed2 = !!this.targets.size; this.targets.clear(); if (changed2) await utils.saveModuleData(this.ctx.baseDir, this.moduleName, [], this.logger); return changed2; } if (!groupId || !/^\d+$/.test(groupId)) return false; const changed = action === "add" ? this.targets.add(groupId) : this.targets.delete(groupId); if (changed) await utils.saveModuleData(this.ctx.baseDir, this.moduleName, [...this.targets], this.logger); return changed; } /** * 发送群打卡请求 * @param session Koishi 会话对象 * @param groupId 群ID * @returns 是否打卡成功 */ async sendGroupSign(session, groupId) { try { await session.bot.internal.sendGroupSign(groupId); return true; } catch { return false; } } /** * 注册群打卡相关命令 * @param parentCmd 父命令对象 */ registerCommands(parentCmd) { const handleReply = /* @__PURE__ */ __name(async (session, message) => { const msg = await session.send(message); await utils.autoRecall(session, Array.isArray(msg) ? msg[0] : msg); return ""; }, "handleReply"); const sign = parentCmd.subcommand("gsign", "群打卡").usage("在当前群进行打卡").action(async ({ session }) => { if (!session.guildId) return handleReply(session, "请在群内使用该命令"); const success = await this.sendGroupSign(session, session.guildId); return handleReply(session, success ? `群 ${session.guildId} 打卡成功~` : "群打卡失败"); }); sign.subcommand(".list", "查看列表", { authority: 3 }).usage("查看打卡群列表").action(async () => { const targets = await this.handleTargets("get"); return targets.length ? `手动模式 - 当前群打卡列表(共${targets.length}个群)` : "手动模式 - 群打卡列表为空"; }); sign.subcommand(".group <target:text>", "指定打卡").usage("打卡指定群").action(async ({ session }, target) => { const groupId = target.trim(); if (!groupId || !/^\d+$/.test(groupId)) return handleReply(session, "请输入有效的群号"); const success = await this.sendGroupSign(session, groupId); return handleReply(session, success ? `群 ${groupId} 打卡成功~` : "群打卡失败"); }); sign.subcommand(".all", "全部打卡", { authority: 3 }).usage("打卡所有列表中的群").action(async ({ session }) => { await handleReply(session, `已开始群打卡,请稍候...`); await this.executeAutoSign(session); return "群打卡完成"; }); sign.subcommand(".add <target:text>", "添加群", { authority: 2 }).usage("添加群到打卡列表").action(async ({ session }, target) => { const groupId = target.trim(); if (!groupId || !/^\d+$/.test(groupId)) return handleReply(session, "请输入有效的群号"); const success = await this.handleTargets("add", groupId); return handleReply(session, success ? `已添加群 ${groupId} 到打卡列表` : "添加失败"); }); sign.subcommand(".remove <target:text>", "移除群", { authority: 2 }).usage("从打卡列表移除群").action(async ({ session }, target) => { const groupId = target.trim(); if (!groupId || !/^\d+$/.test(groupId)) return handleReply(session, "请输入有效的群号"); const success = await this.handleTargets("remove", groupId); return handleReply(session, success ? `已从打卡列表移除群 ${groupId}` : "移除失败"); }); sign.subcommand(".clear", "清空列表", { authority: 4 }).usage("清空打卡列表").action(async () => { await this.handleTargets("clear"); return "已清空群打卡列表"; }); } /** * 释放资源,清理定时任务和计时器 */ dispose() { this.cronJob?.dispose(); this.cronJob = null; this.timer && clearInterval(this.timer); this.timer = null; } }; // src/voice.ts var import_koishi4 = require("koishi"); var Voice = class { static { __name(this, "Voice"); } ctx; logger; /** * 构造函数 * @param ctx Koishi 上下文 * @param logger 日志记录器 */ constructor(ctx, logger) { this.ctx = ctx; this.logger = logger; } /** * 注册AI语音相关命令 * @param parentCmd 父命令对象 */ registerCommands(parentCmd) { const fetchAllCharacters = /* @__PURE__ */ __name(async (session) => { const typeMap = {}; for (const type of [1, 2]) { try { const res = await session.onebot._request("get_ai_characters", { group_id: session.guildId, chat_type: type }); res?.data?.forEach((group) => { if (!group.characters?.length) return; typeMap[group.type] ??= []; group.characters.forEach((c) => { if (!typeMap[group.type].some((x) => x.character_id === c.character_id)) { typeMap[group.type].push(c); } }); }); } catch (e) { this.logger.warn("获取AI语音角色失败:", e); } } return typeMap; }, "fetchAllCharacters"); const checkParams = /* @__PURE__ */ __name(async (session, ...params) => { if (params.some((p) => !p)) { const m = await session.send("请输入正确的参数"); await utils.autoRecall(session, Array.isArray(m) ? m[0] : m); return false; } return true; }, "checkParams"); const voice = parentCmd.subcommand("aisay <character:string> <text:text>", "AI语音").channelFields(["guildId"]).usage("使用指定角色发送AI语音").action(async ({ session }, character, text) => { if (!await checkParams(session, character, text)) return ""; try { await session.onebot._request("send_group_ai_record", { character: character.startsWith("lucy-voice-") ? character : "lucy-voice-" + character, group_id: Number(session.guildId), text }); } catch (e) { this.logger.warn("发送AI语音失败:", e); } return ""; }); voice.subcommand(".list", "角色列表").channelFields(["guildId"]).usage("显示所有AI语音角色").action(async ({ session }) => { try { const typeMap = await fetchAllCharacters(session); return Object.entries(typeMap).map( ([type, characters]) => `${type}: ${characters.map((c) => `${c.character_name}[${c.character_id.replace(/^lucy-voice-/, "")}]`).join("、")}` ).join("\n").trim(); } catch (e) { this.logger.warn("获取语音角色列表失败:", e); return ""; } }); voice.subcommand(".text <character:string> <text:text>", "转换语音").channelFields(["guildId"]).usage("将文字转换为语音消息").action(async ({ session }, character, text) => { if (!await checkParams(session, character, text)) return ""; try { const res = await session.onebot._request("get_ai_record", { character: character.startsWith("lucy-voice-") ? character : "lucy-voice-" + character, group_id: Number(session.guildId), text }); return (0, import_koishi4.h)("audio", { src: res.data }); } catch (e) { this.logger.warn("文字转AI语音失败:", e); return "语音生成失败"; } }); voice.subcommand(".view <key:string>", "角色预览").channelFields(["guildId"]).usage("发送指定角色的预览音频").action(async ({ session }, key) => { if (!key) { const m = await session.send("请输入角色ID或名称"); await utils.autoRecall(session, Array.isArray(m) ? m[0] : m); return ""; } try { const typeMap = await fetchAllCharacters(session); let found; for (const characters of Object.values(typeMap)) { found = characters.find( (c) => c.character_id === key || c.character_id.replace(/^lucy-voice-/, "") === key || c.character_name === key ); if (found) break; } if (!found) { const m = await session.send("未找到该角色"); await utils.autoRecall(session, Array.isArray(m) ? m[0] : m); return ""; } await session.send((0, import_koishi4.h)("audio", { src: found.preview_url })); } catch (e) { this.logger.warn("语音角色预览失败:", e); } return ""; }); } }; // src/index.ts var name = "onebot-tool"; var inject = { optional: ["cron"] }; 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 StickMode = /* @__PURE__ */ ((StickMode2) => { StickMode2["Off"] = "off"; StickMode2["KeywordOnly"] = "keyword"; StickMode2["EmojiOnly"] = "emoji"; StickMode2["All"] = "all"; StickMode2["Manual"] = "manual"; return StickMode2; })(StickMode || {}); var SignMode = /* @__PURE__ */ ((SignMode2) => { SignMode2["Off"] = "off"; SignMode2["Manual"] = "manual"; SignMode2["Auto"] = "auto"; return SignMode2; })(SignMode || {}); var ZanwoMode = /* @__PURE__ */ ((ZanwoMode2) => { ZanwoMode2["Off"] = "off"; ZanwoMode2["Manual"] = "manual"; ZanwoMode2["Auto"] = "auto"; return ZanwoMode2; })(ZanwoMode || {}); var PokeMode = /* @__PURE__ */ ((PokeMode2) => { PokeMode2["Off"] = "off"; PokeMode2["Manual"] = "manual"; PokeMode2["Auto"] = "auto"; return PokeMode2; })(PokeMode || {}); var Config4 = import_koishi5.Schema.intersect([ import_koishi5.Schema.object({ enableVoice: import_koishi5.Schema.boolean().description("启用 AI 语音").default(true), zanwoMode: import_koishi5.Schema.union([ import_koishi5.Schema.const("off" /* Off */).description("关闭"), import_koishi5.Schema.const("manual" /* Manual */).description("手动"), import_koishi5.Schema.const("auto" /* Auto */).description("自动") ]).description("点赞模式").default("manual" /* Manual */), signMode: import_koishi5.Schema.union([ import_koishi5.Schema.const("off" /* Off */).description("关闭"), import_koishi5.Schema.const("manual" /* Manual */).description("手动"), import_koishi5.Schema.const("auto" /* Auto */).description("自动") ]).description("群打卡模式").default("manual" /* Manual */), pokeMode: import_koishi5.Schema.union([ import_koishi5.Schema.const("off" /* Off */).description("关闭"), import_koishi5.Schema.const("manual" /* Manual */).description("手动"), import_koishi5.Schema.const("auto" /* Auto */).description("自动") ]).description("拍一拍模式").default("manual" /* Manual */), stickMode: import_koishi5.Schema.union([ import_koishi5.Schema.const("off" /* Off */).description("关闭"), import_koishi5.Schema.const("manual" /* Manual */).description("手动"), import_koishi5.Schema.const("all" /* All */).description("二者"), import_koishi5.Schema.const("keyword" /* KeywordOnly */).description("仅关键词"), import_koishi5.Schema.const("emoji" /* EmojiOnly */).description("仅同表情") ]).description("表情回应模式").default("manual" /* Manual */) }).description("功能配置"), import_koishi5.Schema.object({ maxTimes: import_koishi5.Schema.number().description("拍一拍单次限制").default(3).min(1).max(200), actionInterval: import_koishi5.Schema.number().description("拍一拍单次间隔(毫秒)").default(500).min(100), imagesPath: import_koishi5.Schema.string().description('占位符"{pixiv}"数据地址').default("https://raw.githubusercontent.com/YisRime/koishi-plugin-onebot-tool/main/resource/pixiv.json"), responses: import_koishi5.Schema.array(import_koishi5.Schema.object({ type: import_koishi5.Schema.union([ import_koishi5.Schema.const("command").description("执行命令"), import_koishi5.Schema.const("message").description("发送消息") ]).description("响应类型"), content: import_koishi5.Schema.string().description("响应内容"), weight: import_koishi5.Schema.number().description("触发权重").default(50).min(0).max(100) })).default([ { type: "message", content: "{at}你干嘛~!", weight: 0 }, { type: "message", content: "{hitokoto}", weight: 0 }, { type: "message", content: "稍等哦~插画一会就来~{~}{pixiv}", weight: 100 }, { type: "command", content: "poke", weight: 0 } ]).description("拍一拍响应列表").role("table"), keywordEmojis: import_koishi5.Schema.array(import_koishi5.Schema.object({ keyword: import_koishi5.Schema.string().description("触发关键词"), emojiId: import_koishi5.Schema.string().description("表情名称/ID") })).default([{ keyword: "点赞", emojiId: "76" }]).description("表情回应关键词列表").role("table") }).description("响应配置") ]); function apply(ctx, config) { const logger = ctx.logger("onebot-tool"); const zanwo = new Zanwo(ctx, config, logger); const poke = new Poke(ctx, config, logger); const stick = new Stick(ctx, config, logger); const sign = new Sign(ctx, config, logger); const voice = new Voice(ctx, logger); const qtool = ctx.command("qtool", "QQ 工具").usage("点赞、打卡、拍一拍、表情回应和AI语音"); config.enableVoice !== false && voice.registerCommands(qtool); if (config.zanwoMode !== "off" /* Off */) zanwo.registerCommands(qtool); if (config.pokeMode !== "off" /* Off */) poke.registerCommand(qtool); if (config.stickMode !== "off" /* Off */) stick.registerCommand(qtool); if (config.signMode !== "off" /* Off */) sign.registerCommands(qtool); if (config.pokeMode === "auto" /* Auto */) ctx.on("notice", poke.processNotice.bind(poke)); if (config.stickMode !== "off" /* Off */ && config.stickMode !== "manual" /* Manual */) { ctx.middleware(async (session, next) => { await stick.processMessage(session); return next(); }); } ctx.on("dispose", () => { zanwo.dispose(); poke.dispose(); sign.dispose(); }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, PokeMode, SignMode, StickMode, ZanwoMode, apply, inject, name, usage });