@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
JavaScript
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】->【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(/&|<|>|"|'/g, (match) => {
const unescapeMap = {
"&": "&",
"<": "<",
">": ">",
""": '"',
"'": "'"
};
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
});