UNPKG

@shangxueink/koishi-plugin-keyword-dialogue

Version:

[<ruby>**关键词回复**<rp>(</rp><rt>点我查看使用说明</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/@shangxueink/koishi-plugin-keyword-dialogue) 设置关键词回复,json存放便于编辑。适用于多段落图文的内容添加。自用小插件哦,[更强大的功能请使用dialogue](/market?keyword=dialogue)

1,015 lines (1,009 loc) 50.8 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; 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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); 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_koishi = require("koishi"); var import_node_url = __toESM(require("node:url")); var import_node_path = __toESM(require("node:path")); var import_node_fs = __toESM(require("node:fs")); var name = "keyword-dialogue"; var lastTriggerTimes = {}; var logger = new import_koishi.Logger("keyword-dialogue"); var inject = { optional: ["puppeteer"] }; var usage = ` <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>插件使用说明</title> </head> <body> <h1>插件使用说明</h1> <p>本插件是专门为图文教程和问答场景设计的,能让你的机器人根据关键词自动回复内容哦。</p> <p>如果你使用过云崽的喵喵插件的添加/删除功能,那你会更容易上手本插件的!</p> <h2>核心功能</h2> <ul> <li><strong>关键词管理:</strong>你可以通过指令添加或删除关键词及其回复内容。</li> <ul> <li>添加关键词:使用指令 <code>添加 [关键词]</code> 来添加关键词。</li> <li>删除关键词:使用指令 <code>删除 [关键词]</code> 来删除关键词。<code>删除 -q 2 你好 </code>来删除“你好”的第二条回复</li> <li>全局添加/删除:使用指令 <code>全局添加 [关键词]</code> 和 <code>全局删除 [关键词]</code> 来在全局范围内管理关键词。<code>全局删除 -q 2 你好 </code>来删除“你好”的第二条回复。</li> </ul> <li><strong>回复内容:</strong>支持文本和图片回复,图片会被保存到本地,避免失效。</li> <li><strong>正则表达式支持:</strong>你可以使用正则表达式来匹配关键词。例如,输入 <code>添加 你好 -x</code>,可以匹配任何包含“你好”的消息,如“你好,包含了你好的句子就可以触发哦”。</li> <li><strong>自定义输入时限:</strong>你可以设置添加回复的输入时限,超过时限将视为超时,自动取消添加操作。</li> <li><strong>多段回复效果:</strong>你可以选择多段输入的回复效果,支持逐条发送或合并为一条消息。</li> <li><strong>后缀支持空格匹配:</strong>使用数字指定回复序号,空格和无空格都能正常匹配。例如,<code>你好 2</code> 和 <code>你好2</code> 都会触发【你好】的第二个回复。</li> <li><strong>修改指定序号的回复内容:</strong>使用 <code>修改 [关键词] -q [序号]</code>,可以修改某个关键词的指定回复。例如,输入 <code>修改 你好 -q 2</code>,将会修改“你好”的第二条回复,并显示当前内容,供你确认后再输入新的回复内容。暂时仅支持修改指令一次性输入回复,暂不支持多段添加。</li> <h2>使用示例</h2> <ul> <li>添加关键词:<code>添加 你好</code></li> <li>添加正则关键词:<code>添加 你好 -x</code></li> <li>删除关键词:<code>删除 你好</code>、<code>删除 -q 2 你好 </code>(删除“你好”的第二条回复)</li> <li>全局添加关键词:<code>全局添加 你好</code></li> <li>全局删除关键词:<code>全局删除 你好</code>、<code>全局删除 -q 2 你好 </code>(删除“你好”的第二条回复)</li> <li>修改指定回复:<code>修改 -q 2 你好 </code>(修改“你好”的第二条回复)</li> </ul> <hr> <h2>修改插件回复内容</h2> <p>如果你需要修改这个插件的回复内容,你需要具备一定的编辑 JSON 文件的能力。</p> <p>你可以在 Koishi 控制台左侧的活动栏找到【资源管理器】页面,</p> <p>然后依次找到文件夹【data】-&gt;【keyword-dialogue】文件夹下的各种【***(群号)***.json】文件。</p> <p>你可以在里面编辑和修改 JSON 内容。</p> <hr> <h2>注意事项</h2> <ul> <li>默认情况下,<code>添加/删除</code> 的问答是按群组分隔的。只有 <code>全局添加/删除</code> 才支持多群组同样的回复。</li> <li>请确保解析到的图片链接是可以访问的。</li> <li>这个插件需要确保 接入平台 端和 koishi 端运行在同一台机器上,以确保本地图片能够正确保存和发送。</li> <li>在使用正则表达式时,请确保输入的模式是有效的,以避免匹配错误。</li> <li>如果你添加了语音/视频消息回复,请确保已经安装了 silk、ffmpeg 等服务!</li> </ul> </body> </html> `; var Config = import_koishi.Schema.intersect([ import_koishi.Schema.object({ command: import_koishi.Schema.object({ TriggerPrefix: import_koishi.Schema.string().default("添加").description("触发`添加关键词`功能的指令"), DeleteKeyword: import_koishi.Schema.string().default("删除").description("触发`删除关键词`功能的指令"), KeywordOfFix: import_koishi.Schema.string().default("修改").description("触发`修改问答`功能的指令"), KeywordOfEsc: import_koishi.Schema.string().default("取消添加").description("取消`添加关键词`功能的关键词"), KeywordOfEnd: import_koishi.Schema.string().default("结束添加").description("退出`添加关键词`功能的关键词"), GlobalTriggerPrefix: import_koishi.Schema.string().default("全局添加").description("触发`全局添加关键词`功能的指令(可在全局范围内生效)"), GlobalDeleteKeyword: import_koishi.Schema.string().default("全局删除").description("触发`全局删除关键词`功能的指令"), KeywordOfSearch: import_koishi.Schema.string().default("查找关键词").description("触发`搜索关键词`功能的指令"), GlobalKeywordOfFix: import_koishi.Schema.string().default("全局修改").description("触发`全局修改问答`功能的指令"), ViewKeywordList: import_koishi.Schema.string().default("查看关键词列表").description("触发`查看关键词列表`功能的指令(仅返回当前群组的关键词)") }).collapse().description("指令注册设置"), addKeywordTime: import_koishi.Schema.number().role("slider").min(1).max(30).step(1).default(5).description("添加回复的输入时限,超过则视为超时,取消添加。`单位 分钟`"), defaultImageExtension: import_koishi.Schema.union(["jpg", "png", "gif"]).default("png").description("输入图片保存的后缀名") }).description("基础设置"), import_koishi.Schema.object({ admin_list: import_koishi.Schema.array(import_koishi.Schema.object({ adminID: import_koishi.Schema.string().description("管理员用户ID"), allowcommand: import_koishi.Schema.array(import_koishi.Schema.union(["添加", "删除", "全局添加", "全局删除", "查找关键词", "修改", "查看关键词列表", "全局修改"])).default(["添加", "删除", "修改", "查找关键词"]).description("可以使用的指令") })).role("table").description("管理员列表( 0 代表所有用户)<br>独立于 channel_admin_auth ").default([ { "adminID": "0", "allowcommand": [ "添加", "删除", "查找关键词", "修改", "查看关键词列表" ] } ]), channel_admin_auth: import_koishi.Schema.boolean().default(false).description("开启后 自动允许 管理员/群主 使用本插件的全部指令 `须确保适配器支持获取群员角色`").experimental(), Delete_Branch_Only: import_koishi.Schema.boolean().default(true).description("开启后 在删除多段回复的关键词时,必须指定需要删除的序号,而不会直接删除掉这个关键词"), Treat_all_as_lowercase: import_koishi.Schema.boolean().default(true).description("开启后 英文关键词匹配无视大写字母`解决英文大小写匹配问题`"), Prompt: import_koishi.Schema.string().role("textarea", { rows: [2, 4] }).default("请输入回复内容(输入 取消添加 以取消,输入 结束添加 以结束):").description("添加时,返回的文字提示"), picture_save_to_local_send: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("不保存图片,使用平台的图片链接"), import_koishi.Schema.const("2").description("保存图片为文件,发送时使用图片文件绝对路径"), import_koishi.Schema.const("3").description("保存图片为文件,发送时图片文件转换为base64"), import_koishi.Schema.const("4").description("保存图片为base64到json,发送时使用base64 `json会变得好长唔,不推荐`") ]).role("radio").default("2").description("开启后 图片回复保存到本地路径`防止平台图片链接失效`"), MatchPatternForExit: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("不使用KeywordOfEnd(不使用多段输入),仅接受一次性的输入"), import_koishi.Schema.const("2").description("完全匹配KeywordOfEnd时退出"), import_koishi.Schema.const("3").description("包含KeywordOfEnd时退出"), import_koishi.Schema.const("4").description("包含KeywordOfEnd,或者完全匹配KeywordOfEnd均可退出") ]).role("radio").default("4").description("如何退出添加"), AlwayPrompt: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("不返回文字提示(真的很难用)"), import_koishi.Schema.const("2").description("仅返回一次文字提示"), import_koishi.Schema.const("3").description("每次输入都返回文字提示(有点太吵了)") ]).role("radio").default("2").description("如何返回文字提示"), HandleDuplicateKeywords: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("不返回文字提示,直接替换/覆盖"), import_koishi.Schema.const("2").description("不返回文字提示,直接在原关键词回答上添加并列回答(每次仅从中选择随机一种回复)"), import_koishi.Schema.const("3").description("返回文字提示,不允许重复添加(删除后才可添加)") ]).role("radio").default("2").description("如何处理待添加的重复关键词"), MultisegmentAdditionRecoveryEffect: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("按照原版输入,原版输出(多段消息发送)"), import_koishi.Schema.const("2").description("合为图文消息/多行消息(一次发出,不是合并转发)"), import_koishi.Schema.const("3").description("合为一条图文消息,并且合并转发发送 `需要适配器支持哦~`"), import_koishi.Schema.const("4").description("按照原版输入,合并转发发送 `需要适配器支持哦~`") ]).role("radio").default("2").description("多段添加的回复效果") }).description("进阶设置"), import_koishi.Schema.object({ prefix: import_koishi.Schema.array(String).role("table").default(["", "/", "#"]).description("指令前缀。将被用于指令的匹配。<br>与全局设置的那个,效果差不多,但是仅针对本插件的关键词。") }).description("关键词设置"), import_koishi.Schema.object({ Frequency_limitation: import_koishi.Schema.number().description("同一问答的最小触发间隔 单位:秒").default(0), Type_of_restriction: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("对同一个问题(全部对象)"), import_koishi.Schema.const("2").description("仅对同一个频道(不同频道独立记数间隔)") ]).role("radio").default("2").description("最小间隔时间的限制对象"), Type_of_ViewKeywordList: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("返回文字列表"), import_koishi.Schema.const("2").description("返回图片列表(需要puppeteer服务)") ]).role("radio").default("2").description("`查看关键词列表`的返回设置") }).description("回复设置"), import_koishi.Schema.object({ Search_Range: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("仅在当前频道搜索问答"), import_koishi.Schema.const("2").description("搜索全部频道的问答"), import_koishi.Schema.const("3").description("当前频道搜索问答 + 全局问答") ]).role("radio").default("3").description("搜索范围,`查看关键词列表`和`查找关键词`指令的搜索范围"), Find_Return_Preset: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("仅返回问答的内容"), import_koishi.Schema.const("2").description("仅返回问答所在的频道ID/位置"), import_koishi.Schema.const("3").description("返回问答的内容,并且返回问答所在的频道ID/位置") ]).role("radio").default("1").description("搜索关键词的 返回信息。"), Return_Limit: import_koishi.Schema.union([ import_koishi.Schema.const("1").description("返回查找到的全部问答"), import_koishi.Schema.const("2").description("仅返回一条问答(模糊匹配)") ]).role("radio").default("2").description("返回限制") }).description("查找问答设置"), import_koishi.Schema.object({ Preposition_middleware: import_koishi.Schema.boolean().default(false).description("开启后 使用前置中间件`匹配到关键词后,不会触发 同实例下 同名称的指令`<br>可以实现回复“指令正在维护中”的效果"), Unified_at_field: import_koishi.Schema.boolean().default(false).description('统一at消息的内容,统一为:`<at id="11514"/>`<br>因为有时候at内容是`<at id="11514" name="臭"/>`<br>这样可以防止更换了实现端导致的at字段不一致'), consoleInfo: import_koishi.Schema.boolean().default(false).description("日志调试模式") }).description("调试设置") ]); function apply(ctx, config) { const add_command = config.command.TriggerPrefix; const global_add_command = config.command.GlobalTriggerPrefix; const delete_command = config.command.DeleteKeyword; const global_delete_command = config.command.GlobalDeleteKeyword; const KeywordOfEsc = config.command.KeywordOfEsc; const KeywordOfEnd = config.command.KeywordOfEnd; const KeywordOfSearch = config.command.KeywordOfSearch; const KeywordOfFix = config.command.KeywordOfFix; const GlobalKeywordOfFix = config.command.GlobalKeywordOfFix; const ViewKeywordList = config.command.ViewKeywordList; const zh_CN_default = { commands: { [ViewKeywordList]: { description: `查看关键词列表`, messages: { "channel_admin_auth": "你没有权限操作此指令。" } }, [add_command]: { description: `添加关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Input_Timeout": "输入超时。", "Cancel_operation": "添加操作已取消。", "Keyword_exists": '关键词 "{0}" 已存在,不能添加重复的关键词。\n或者请删除这个关键词后重新添加。', "Reply_added": '关键词 "{0}" 的回复已添加。' } }, [global_add_command]: { description: `添加全局关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Input_Timeout": "输入超时。", "Cancel_operation": "添加操作已取消。", "Keyword_exists": '关键词 "{0}" 已存在,不能添加重复的关键词。\n或者请删除这个关键词后重新添加。', "Reply_added": '关键词 "{0}" 的回复已添加。' } }, [delete_command]: { description: `删除关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Keyword_does_not_exist": '关键词 "{0}" 不存在。', "Reply_deleted": '关键词 "{0}" 的回复已删除。' } }, [global_delete_command]: { description: `删除全局关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Keyword_does_not_exist": '关键词 "{0}" 不存在。', "Reply_deleted": '关键词 "{0}" 的回复已删除。' } }, [KeywordOfSearch]: { description: `查找关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Keyword_not_found": '未找到关键词 "{0}" 的相关问答。', "Keyword_found": "在 {0} 下找到:\n关键词:{1}\n回复:\n{2}", "Keyword_found_in_channel": "在 {0} 下找到关键词:{1}", "Keyword_found_content_only": "关键词:{0}\n回复:\n{1}" } }, [KeywordOfFix]: { description: `修改关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Keyword_not_found": '未找到关键词 "{0}" 的相关问答。', "Keyword_found": "在 {0} 下找到:\n关键词:{1}\n回复:\n{2}", "Keyword_found_in_channel": "在 {0} 下找到关键词:{1}", "Keyword_found_content_only": "关键词:{0}\n回复:\n{1}", "Input_Timeout": "输入超时。", "Cancel_operation": "添加操作已取消。", "Keyword_exists": '关键词 "{0}" 已存在,不能添加重复的关键词。\n或者请删除这个关键词后重新添加。', "Reply_added": '关键词 "{0}" 的回复已修改。' } }, [GlobalKeywordOfFix]: { description: `全局修改关键词`, messages: { "channel_admin_auth": "你没有权限操作此指令。", "no_Valid_Keyword": "请提供一个有效的关键词。", "Keyword_not_found": '未找到关键词 "{0}" 的相关问答。', "Input_Timeout": "输入超时。", "Cancel_operation": "修改操作已取消。", "Reply_added": '关键词 "{0}" 的回复已修改。' } } } }; ctx.i18n.define("zh-CN", zh_CN_default); const root = import_node_path.default.join(ctx.baseDir, "data", "keyword-dialogue"); if (!import_node_fs.default.existsSync(root)) { import_node_fs.default.mkdirSync(root, { recursive: true }); } ctx.command("keyword-dialogue"); function logInfo(message) { if (config.consoleInfo) { logger.info(message); } } __name(logInfo, "logInfo"); async function parseReplyContent(reply, root2, session, options) { const elements = import_koishi.h.parse(reply); logInfo("Parsed elements: "); logInfo(elements); const replyData = []; for (const element of elements) { let item = null; const replyway = options.forward?.toString() || config.MultisegmentAdditionRecoveryEffect; if (element.type === "img" || element.type === "image") { let localPath; switch (config.picture_save_to_local_send) { case "1": localPath = element.attrs.src; break; case "2": case "3": localPath = await downloadImage(element.attrs.src, root2, session, !!options.global); break; case "4": localPath = await downloadImageAsBase64(element.attrs.src) || ""; break; default: localPath = element.attrs.src; } item = { type: "image", text: localPath, fileSize: element.attrs.fileSize, replyway }; } else if (element.type === "text") { item = { type: "text", text: element.attrs.content, replyway }; } else if (element.type === "at") { item = { type: "at", text: element.attrs.id, replyway }; } else if (element.type === "audio") { item = { type: "audio", text: element.attrs.src || element.attrs.url, replyway }; } else if (element.type === "video") { item = { type: "video", text: element.attrs.src, replyway }; } else if (element.type === "mface") { item = { type: "mface", text: element.attrs.url || "无法获取该 mface 的图片链接", replyway }; } else { item = { type: "unknown", text: import_koishi.h.select([element], "text").map((e) => e.attrs.content).join(""), replyway }; } if (item) { replyData.push(item); } } return replyData; } __name(parseReplyContent, "parseReplyContent"); function isAdmin(session) { const sessionRoles = session.event.member?.roles; return !!sessionRoles && (sessionRoles.includes("admin") || sessionRoles.includes("owner")); } __name(isAdmin, "isAdmin"); function hasPermission(session, command) { const userId = session.userId; const adminConfig = config.admin_list.find((admin) => admin.adminID === userId); const defaultConfig = config.admin_list.find((admin) => admin.adminID === "0"); if (defaultConfig && defaultConfig.allowcommand.includes(command)) { return true; } if (adminConfig) { return adminConfig.allowcommand.includes(command); } if (config.channel_admin_auth) { if (isAdmin(session)) { if (adminConfig) { return adminConfig.allowcommand.includes(command); } return true; } if (!adminConfig) { if (defaultConfig && defaultConfig.allowcommand.includes(command)) { return true; } } } return false; } __name(hasPermission, "hasPermission"); async function deleteKeywordReply(session, filePath, keyword, config2, specifiedIndex) { if (!import_node_fs.default.existsSync(filePath)) { await session.send(import_koishi.h.unescape(session.text(`.Keyword_does_not_exist`))); return; } let data = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf-8")); if (config2.Treat_all_as_lowercase) { keyword = keyword.toLowerCase(); } let found = false; let replies; let matchedKey; for (const key in data) { const normalizedKey = config2.Treat_all_as_lowercase ? key.toLowerCase() : key; const strippedKey = normalizedKey.startsWith("regex:") ? normalizedKey.slice(6) : normalizedKey; if (strippedKey === keyword) { replies = data[key]; matchedKey = key; found = true; break; } } if (!found || !replies || !matchedKey) { await session.send(import_koishi.h.unescape(session.text(`.Keyword_does_not_exist`, [keyword]))); return; } if (config2.Delete_Branch_Only && replies.length > 1) { if (specifiedIndex === null || specifiedIndex === void 0) { await session.send(`关键词 "${keyword}" 有多个回复,请指定需要删除的分支。 请使用如下指令来删除指定序号的回复: 删除 -q [数字] ${keyword}`); return; } else if (specifiedIndex > 0 && specifiedIndex <= replies.length) { replies.splice(specifiedIndex - 1, 1); if (replies.length === 0) { delete data[matchedKey]; await session.send(import_koishi.h.unescape(session.text(`.Reply_deleted`, [keyword]))); } else { await session.send(import_koishi.h.unescape(`已删除关键词 "${keyword}" 的第 ${specifiedIndex} 条回复。`)); } import_node_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); return; } else { await session.send(`无效的回复序号,请指定正确的序号。`); return; } } else { delete data[matchedKey]; import_node_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); await session.send(import_koishi.h.unescape(session.text(`.Reply_deleted`, [keyword]))); } } __name(deleteKeywordReply, "deleteKeywordReply"); function escapeRegExp(string) { return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&").replace(/\s/g, "\\s").replace(/\\\\/g, "\\\\"); } __name(escapeRegExp, "escapeRegExp"); async function addKeywordReply(session, filePath, keyword, config2, options) { let data; try { data = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf-8")); } catch (err) { data = {}; } if (config2.Treat_all_as_lowercase) { keyword = keyword.toLowerCase(); } const addKeyword = options.unescape ? `regex:${keyword}` : `regex:${escapeRegExp(keyword)}`; const key = options.regex || options.unescape ? addKeyword : keyword; if (!data[key]) { data[key] = []; } if (data[key].length > 0 && config2.HandleDuplicateKeywords === "3") { await session.send(import_koishi.h.unescape(session.text(`.Keyword_exists`, [keyword]))); return; } if (config2.HandleDuplicateKeywords === "1") { data[key] = []; } if (config2.AlwayPrompt === "2" || config2.AlwayPrompt === "3") { await session.send(import_koishi.h.text(config2.Prompt)); } let currentReplies = []; if (config2.MatchPatternForExit === "1") { const timeout = config2.addKeywordTime * 6e4; const reply = await session.prompt(timeout); if (reply.includes(KeywordOfEsc)) { await session.send(import_koishi.h.text(session.text(`.Cancel_operation`))); return; } if (!reply) { await session.send(import_koishi.h.text(session.text(`.Input_Timeout`))); return; } const replyData = await parseReplyContent(reply, root, session, options); currentReplies.push(...replyData); data[key].push(currentReplies); import_node_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); await session.send(import_koishi.h.unescape(session.text(`.Reply_added`, [keyword]))); return; } while (true) { if (config2.AlwayPrompt === "3") { await session.send(import_koishi.h.text(config2.Prompt)); } const timeout = config2.addKeywordTime * 6e4; const reply = await session.prompt(timeout); if (reply.includes(KeywordOfEsc)) { await session.send(import_koishi.h.text(session.text(`.Cancel_operation`))); return; } if (config2.MatchPatternForExit === "2" && reply === KeywordOfEnd || config2.MatchPatternForExit === "3" && reply.includes(KeywordOfEnd) || config2.MatchPatternForExit === "4" && (reply === KeywordOfEnd || reply.includes(KeywordOfEnd))) { break; } if (!reply) { await session.send(import_koishi.h.text(session.text(`.Input_Timeout`))); return; } const replyData = await parseReplyContent(reply, root, session, options); currentReplies.push(...replyData); } data[key].push(currentReplies); import_node_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); await session.send(import_koishi.h.unescape(session.text(`.Reply_added`, [keyword]))); } __name(addKeywordReply, "addKeywordReply"); async function downloadImage(url, outputPath, session, isGlobal) { try { let absoluteOutputPath; if (isGlobal) { absoluteOutputPath = import_node_path.default.resolve(outputPath, "global"); } else { absoluteOutputPath = import_node_path.default.resolve(outputPath, session.channelId); } if (!import_node_fs.default.existsSync(absoluteOutputPath)) { import_node_fs.default.mkdirSync(absoluteOutputPath, { recursive: true }); } const timestamp = Date.now(); let fileName = `${timestamp}.${config.defaultImageExtension}`; absoluteOutputPath = import_node_path.default.join(absoluteOutputPath, fileName); const response = await ctx.http.get(url, { responseType: "arraybuffer" }); const buffer = Buffer.from(response); await import_node_fs.default.promises.writeFile(absoluteOutputPath, buffer); const imageURL = import_node_url.default.pathToFileURL(absoluteOutputPath).href; logInfo(imageURL); return imageURL; } catch (error) { logger.error(`下载图片失败: ${error.message}`); throw error; } } __name(downloadImage, "downloadImage"); async function downloadImageAsBase64(imagePath) { try { if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) { const response = await ctx.http.get(imagePath, { responseType: "arraybuffer" }); const buffer = Buffer.from(response); return buffer.toString("base64"); } else { let localPath = imagePath; if (localPath.startsWith("file:///")) { localPath = import_node_url.default.fileURLToPath(localPath); } const imageBuffer = await import_node_fs.default.promises.readFile(localPath); return imageBuffer.toString("base64"); } } catch (error) { logger.error("Error converting image to base64:", error); return null; } } __name(downloadImageAsBase64, "downloadImageAsBase64"); async function formatReply(reply, forwardreturn = false) { let formattedReply; if (reply.type === "image") { if (config.picture_save_to_local_send === "3") { const base64fileData = await downloadImageAsBase64(reply.text); formattedReply = import_koishi.h.image("data:image/png;base64," + base64fileData); } else if (config.picture_save_to_local_send === "4") { formattedReply = import_koishi.h.image("data:image/png;base64," + reply.text); } else { formattedReply = import_koishi.h.image(reply.text); } } else if (reply.type === "mface") { formattedReply = import_koishi.h.image(reply.text); } else if (reply.type === "text") { formattedReply = import_koishi.h.text(reply.text); } else if (reply.type === "audio") { formattedReply = import_koishi.h.audio(reply.text); } else if (reply.type === "video") { formattedReply = import_koishi.h.video(reply.text); } else if (reply.type === "at") { formattedReply = import_koishi.h.at(reply.text); } else { formattedReply = reply.text; } const returnformatReply = forwardreturn ? (0, import_koishi.h)("message", {}, formattedReply) : formattedReply; logInfo("returnformatReply:"); logInfo(returnformatReply); return returnformatReply; } __name(formatReply, "formatReply"); function isValidKeyword(keyword) { return !!keyword && keyword.trim().length > 0; } __name(isValidKeyword, "isValidKeyword"); function calculateFontSize(numKeywords, maxFontSize, minFontSize) { const m = (minFontSize - maxFontSize) / (40 - 1); const b = maxFontSize - m; return m * numKeywords + b; } __name(calculateFontSize, "calculateFontSize"); ctx.command(`keyword-dialogue/${ViewKeywordList}`).action(async ({ session }) => { if (!session) return; if (!hasPermission(session, "查看关键词列表")) { await session.send(session.text(".channel_admin_auth")); return; } const searchFiles = (() => { switch (config.Search_Range) { case "1": return [`${session.channelId}.json`]; // 仅搜索当前频道 case "2": return import_node_fs.default.readdirSync(root).filter((file) => file.endsWith(".json")); // 搜索所有文件 case "3": return [`${session.channelId}.json`, "global.json"]; // 搜索当前频道和全局 default: return []; } })(); let keywords = []; for (const file of searchFiles) { const filePath = import_node_path.default.join(root, file); if (import_node_fs.default.existsSync(filePath)) { const data = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf-8")); keywords = keywords.concat(Object.keys(data)); } } keywords = Array.from(new Set(keywords)); if (keywords.length === 0) { await session.send("当前没有关键词。"); return; } if (config.Type_of_ViewKeywordList === "1") { const message = keywords.join("\n"); await session.send(import_koishi.h.text(message)); } else if (config.Type_of_ViewKeywordList === "2") { const puppeteer = ctx.get("puppeteer"); if (!puppeteer) { await session.send("puppeteer 服务未启用。"); return; } const pages = []; for (let i = 0; i < keywords.length; i += 40) { pages.push(keywords.slice(i, i + 40)); } for (const pageKeywords of pages) { const page = await puppeteer.page(); const fontSize = calculateFontSize(pageKeywords.length, 120, 40); const content = ` <html> <head> <style> body { background-color: black; color: white; font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } .container { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 20px; width: 1080px; height: 1920px; box-sizing: border-box; } .keyword { word-wrap: break-word; text-align: left; white-space: pre-wrap; background-color: #333; padding: 10px; border-radius: 5px; font-size: ${fontSize}px; /* 使用计算出的字体大小 */ } </style> </head> <body> <div class="container"> ${pageKeywords.map((keyword) => `<div class="keyword">${keyword}</div>`).join("")} </div> </body> </html> `; await page.setContent(content); await page.setViewport({ width: 1080, height: 1920 }); const imageBuffer = await page.screenshot({ encoding: "binary" }); await page.close(); const imageMessage = import_koishi.h.image(imageBuffer, "image/png"); await session.send(imageMessage); } } }); ctx.command(`keyword-dialogue/${KeywordOfSearch} [Keyword]`).action(async ({ session }, Keyword) => { if (!session) return; if (!hasPermission(session, "查找关键词")) { await session.send(session.text(".channel_admin_auth")); return; } if (!Keyword) { await session.send(import_koishi.h.text(session.text(`.no_Valid_Keyword`))); return; } const searchRange = config.Search_Range; const returnPreset = config.Find_Return_Preset; const returnLimit = config.Return_Limit; const searchFiles = (() => { switch (searchRange) { case "1": return [`${session.channelId}.json`]; case "2": return import_node_fs.default.readdirSync(root).filter((file) => file.endsWith(".json")); case "3": return [`${session.channelId}.json`, "global.json"]; default: return [`${session.channelId}.json`]; } })(); let results = []; for (const file of searchFiles) { const filePath = import_node_path.default.join(root, file); if (import_node_fs.default.existsSync(filePath)) { const data = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf-8")); for (const key in data) { const normalizedKey = config.Treat_all_as_lowercase ? key.toLowerCase() : key; if (normalizedKey.includes(Keyword.toLowerCase())) { const channelId = file.replace(".json", ""); const formattedReplies = await Promise.all(data[key].map(async (replyGroup) => { const formattedParts = await Promise.all(replyGroup.map((reply) => formatReply(reply, false))); return import_koishi.h.normalize(formattedParts).join(""); })); if (returnPreset === "1") { results.push(import_koishi.h.unescape(session.text(`.Keyword_found_content_only`, [key, formattedReplies.join("\n\n")]))); } else if (returnPreset === "2") { results.push(import_koishi.h.unescape(session.text(`.Keyword_found_in_channel`, [channelId, key]))); } else if (returnPreset === "3") { results.push(import_koishi.h.unescape(session.text(`.Keyword_found`, [channelId, key, formattedReplies.join("\n\n")]))); } if (returnLimit === "2") { break; } } } } if (returnLimit === "2" && results.length > 0) { break; } } if (results.length === 0) { await session.send(session.text(`.Keyword_not_found`, [Keyword])); } else { await session.send(import_koishi.h.normalize(results).join("\n\n")); } }); ctx.command(`keyword-dialogue/${delete_command} [Keyword]`).alias("删除关键词").option("question", "-q [number] 指定删除回复的序号").action(async ({ session, options }, Keyword) => { if (!session || !options) return; if (!hasPermission(session, "删除")) { await session.send(session.text(".channel_admin_auth")); return; } if (!Keyword) { await session.send(import_koishi.h.text(session.text(`.no_Valid_Keyword`))); return; } const filePath = import_node_path.default.join(root, `${session.channelId}.json`); const specifiedIndex = options.question ?? null; await deleteKeywordReply(session, filePath, Keyword, config, specifiedIndex); }); ctx.command(`keyword-dialogue/${global_delete_command} [Keyword]`).alias("全局删除关键词").option("question", "-q [number] 指定删除回复的序号").action(async ({ session, options }, Keyword) => { if (!session || !options) return; if (!hasPermission(session, "全局删除")) { await session.send(session.text(".channel_admin_auth")); return; } if (!Keyword) { await session.send(import_koishi.h.text(session.text(`.no_Valid_Keyword`))); return; } const filePath = import_node_path.default.join(root, "global.json"); const specifiedIndex = options.question ?? null; await deleteKeywordReply(session, filePath, Keyword, config, specifiedIndex); }); ctx.command(`keyword-dialogue/${global_add_command} [Keyword]`).alias("全局添加关键词").option("regex", "-x 添加正则关键词").option("global", "-g 添加全局关键词").option("unescape", "-u 取消转义以使用自定义正则").option("forward", "-f <number> 指定回复方式:1.按照原版输入,原版输出 2.合为图文消息 3.合为一条图文消息的合并转发 4.按照原版输入,合并转发发送").example("全局添加关键词 使用教程 -x -f 2").action(async ({ session, options }, Keyword) => { if (!session || !options) return; if (!hasPermission(session, "全局添加")) { await session.send(session.text(".channel_admin_auth")); return; } if (!isValidKeyword(Keyword)) { await session.send(import_koishi.h.text(session.text(`.no_Valid_Keyword`))); return; } options.global = true; const filePath = import_node_path.default.join(root, "global.json"); if (!import_node_fs.default.existsSync(filePath)) { import_node_fs.default.writeFileSync(filePath, JSON.stringify({}, null, 2), "utf-8"); } await addKeywordReply(session, filePath, Keyword.trim(), config, options); }); ctx.command(`keyword-dialogue/${add_command} [Keyword]`).alias("添加关键词").option("global", "-g 添加全局关键词").option("regex", "-x 添加正则关键词").option("unescape", "-u 取消转义以使用自定义正则").option("forward", "-f <number> 指定回复方式:1.按照原版输入,原版输出 2.合为图文消息 3.合为一条图文消息的合并转发 4.按照原版输入,合并转发发送").example("添加关键词 使用教程 -x -f 2").action(async ({ session, options }, Keyword) => { if (!session || !options) return; if (!hasPermission(session, "添加")) { await session.send(session.text(".channel_admin_auth")); return; } if (!isValidKeyword(Keyword)) { await session.send(import_koishi.h.text(session.text(`.no_Valid_Keyword`))); return; } const filePath = options.global ? import_node_path.default.join(root, "global.json") : import_node_path.default.join(root, `${session.channelId}.json`); if (!import_node_fs.default.existsSync(filePath)) { import_node_fs.default.writeFileSync(filePath, JSON.stringify({}, null, 2), "utf-8"); } await addKeywordReply(session, filePath, Keyword.trim(), config, options); }); async function modifyKeywordReply(session, options, Keyword, isGlobal) { if (!session || !options) return; const permissionCommand = isGlobal ? "全局修改" : "修改"; if (!hasPermission(session, permissionCommand)) { await session.send(session.text(".channel_admin_auth")); return; } if (!isValidKeyword(Keyword)) { await session.send(import_koishi.h.text(session.text(`.no_Valid_Keyword`))); return; } const filePath = isGlobal ? import_node_path.default.join(root, "global.json") : import_node_path.default.join(root, `${session.channelId}.json`); if (!import_node_fs.default.existsSync(filePath)) { await session.send(import_koishi.h.text("未找到相关问答数据。")); return; } const data = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf-8")); const key = config.Treat_all_as_lowercase ? Keyword.toLowerCase() : Keyword; const replies = data[key]; if (!replies) { await session.send(import_koishi.h.text(`关键词 "${Keyword}" 不存在。`)); return; } const index = options.question ? options.question - 1 : 0; if (index < 0 || index >= replies.length) { await session.send(import_koishi.h.text(`指定的回复序号无效,请提供正确的序号。`)); return; } await session.send(import_koishi.h.unescape(`您正在修改的是【${Keyword}】的第【${index + 1}】条回复`)); const currentReply = replies[index]; let fullReplyContent = []; for (const replyPart of currentReply) { const formattedReply = await formatReply(replyPart, false); fullReplyContent.push(formattedReply); } await session.send(import_koishi.h.normalize(fullReplyContent)); await session.send(import_koishi.h.text("请一次性将回复内容完整输入以修改:\n➣输入 取消添加 以取消")); const timeout = config.addKeywordTime * 6e4; const reply = await session.prompt(timeout); if (!reply) { await session.send(import_koishi.h.text(session.text(`.Input_Timeout`))); return; } if (reply.includes(KeywordOfEsc)) { await session.send(import_koishi.h.text(session.text(`.Cancel_operation`))); return; } const replyData = await parseReplyContent(reply, root, session, options); data[key][index] = replyData; import_node_fs.default.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); await session.send(import_koishi.h.unescape(session.text(`.Reply_added`, [Keyword]))); } __name(modifyKeywordReply, "modifyKeywordReply"); ctx.command(`keyword-dialogue/${KeywordOfFix} [Keyword]`).option("question", "-q [number] 指定回复序号").action(async ({ session, options }, Keyword) => { await modifyKeywordReply(session, options, Keyword, false); }); ctx.command(`keyword-dialogue/${GlobalKeywordOfFix} [Keyword]`).option("question", "-q [number] 指定回复序号").action(async ({ session, options }, Keyword) => { await modifyKeywordReply(session, options, Keyword, true); }); const middlewareFunction = /* @__PURE__ */ __name(async (session, next) => { const { channelId, platform } = session; let anothercontent = unescapeHtml(session.content).trim(); if (platform === "qq" || platform === "qqguild") { anothercontent = unescapeHtml(session.stripped.content).trim(); } if (config.Treat_all_as_lowercase) { anothercontent = anothercontent.toLowerCase(); } if (config.Unified_at_field) { anothercontent = anothercontent.replace(/<at id="(\d+)" name="[^"]*"\s*\/>/g, '<at id="$1"/>'); } logInfo(`用户输入内容为 ${anothercontent}`); const globalFilePath = import_node_path.default.join(root, "global.json"); const channelFilePath = import_node_path.default.join(root, `${channelId}.json`); const getPrefixedKeywords = /* @__PURE__ */ __name((keyword, prefixes) => { return prefixes.map((prefix) => prefix + keyword); }, "getPrefixedKeywords"); const getSuffixIndex = /* @__PURE__ */ __name((inputKeyword, baseKeyword) => { const escapedBaseKeyword = escapeRegExp(baseKeyword); const suffixPattern = new RegExp(`^${escapedBaseKeyword}\\s*(\\d+)$`); const match = inputKeyword.match(suffixPattern); if (match) { return parseInt(match[1], 10); } return null; }, "getSuffixIndex"); const sendReplies = /* @__PURE__ */ __name(async (session2, replyGroup) => { if (!replyGroup || replyGroup.length === 0) return; const replyway = replyGroup[0].replyway || config.MultisegmentAdditionRecoveryEffect; if (replyway === "1") { for (const reply of replyGroup) { logInfo(reply); const formattedReply = await formatReply(reply, false); await session2.send(formattedReply); } } else if (replyway === "2") { const formattedParts = await Promise.all(replyGroup.map((reply) => formatReply(reply, false))); const combinedReply = import_koishi.h.normalize(formattedParts); logInfo(combinedReply); await session2.send(combinedReply); } else if (replyway === "3" || replyway === "4") { const isForward = replyway === "4"; const result = (0, import_koishi.h)("figure"); for (const reply of replyGroup) { const formattedReply = await formatReply(reply, isForward); if (typeof formattedReply === "string") { result.children.push(import_koishi.h.text(formattedReply)); } else { result.children.push(formattedReply); } } logInfo(result); await session2.send(result); } }, "sendReplies"); const checkAndSendRandomReply = /* @__PURE__ */ __name(async (filePath) => { if (import_node_fs.default.existsSync(filePath)) { const data = JSON.parse(import_node_fs.default.readFileSync(filePath, "utf-8")); for (const keyword in data) { const replies = data[keyword]; if (!replies || replies.length === 0) continue; let isMatch = false; const prefixedKeywords = getPrefixedKeywords(keyword, config.prefix || ["", "/", "#"]); for (const prefixedKeyword of prefixedKeywords) { const suffixIndex = getSuffixIndex(anothercontent, prefixedKeyword); if (keyword.startsWith("regex:")) { const regexPattern = keyword.substring(6); const regex = new RegExp(regexPattern); isMatch = regex.test(anothercontent); } else { if (anothercontent === prefixedKeyword) { isMatch = true; } else if (suffixIndex !== null && suffixIndex > 0 && suffixIndex <= replies.length) { await sendReplies(session, replies[suffixIndex - 1]); return true; } } if (isMatch) { const now = Date.now(); const key = config.Type_of_restriction === "2" ? `${keyword}:${channelId}` : keyword; if (lastTriggerTimes[key] && now - lastTriggerTimes[key] < config.Frequency_limitation * 1e3) { logInfo("间隔时间未到,不触发回复"); return true; } lastTriggerTimes[key] = now; const randomReplyGroup = replies[Math.floor(Math.random() * replies.length)]; await sendReplies(session, randomReplyGroup); return true; } } } } return false; }, "checkAndSendRandomReply"); if (await checkAndSendRandomReply(globalFilePath) || await checkAndSendRandomReply(channelFilePath)) { return; } return next(); }, "middlewareFunction"); const unescapeHtml = /* @__PURE__ */ __name((str) => { return str.replace(/&amp;|&lt;|&gt;|&quot;|&#039;/g, (match) => { const unescapeMap = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#039;": "'" }; return unescapeMap[match]; }); }, "unescapeHtml"); if (config.Preposition_middleware) { ctx.middleware(middlewareFunction, true); } else { ctx.middleware(middlewareFunction); } } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, apply, inject, name, usage });