UNPKG

@shangxueink/koishi-plugin-content-guard

Version:

[<ruby>**审核输入调用**<rp>(</rp><rt>点我查看使用说明</rt><rp>)</rp></ruby>](https://www.npmjs.com/package/@shangxueink/koishi-plugin-content-guard) 设置对应指令的文本输入审核,实现违规输入禁止调用,适用于任何指令的调用审核。让你的机器人更安全地面向用户开放服务。

342 lines (280 loc) 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.apply = exports.Config = exports.usage = exports.inject = exports.name = void 0; const fs = require('node:fs'); const path = require("node:path"); const { Schema, Logger, h } = require("koishi"); exports.reusable = true; // 声明此插件可重用 exports.name = 'content-guard'; const logger = new Logger('content-guard'); exports.usage = ` --- ## 违禁词库 违禁词库来自 [Sensitive-lexicon](https://github.com/konsheng/Sensitive-lexicon)。 感谢该项目提供的违禁词资源。 你也可以使用这个项目的 txt 作为 [text-censor](/market?keyword=text-censor) 的违禁词库, 与本插件不同,text-censor 提供了 censor 服务以供其他插件调用。 --- ## 免责说明 本插件仅用于演示和学习目的。 使用者应自行承担使用本插件所带来的风险和责任。 违禁词库来源于第三方项目,插件开发者对其内容不承担任何责任。 请确保在遵守相关法律法规的前提下使用本插件。 --- 本插件仅适用于由用户进行指令调用输入参数的文本审核 仅需在 Audit_Configuration 配置项里填入对应的特征前缀和右侧的返回文本,即可对应的开启审核。 `; const Whitelist_input_default = [ { "input": "4", "texttip": "这个是白名单示例,不会被屏蔽。" } ]; const Blacklist_input_default = [ { "input": "牛魔", "texttip": "检测到屏蔽词:牛魔" }, { "input": "啊米诺斯", "texttip": "检测到屏蔽词:啊米诺斯" }, { "input": "你妈", "texttip": "检测到屏蔽词:你妈" } ]; exports.Config = Schema.intersect([ Schema.object({ Audit: Schema.boolean().default(true).description('确认开启审核(开启后,本插件生效)'), Audit_Configuration: Schema.array(Schema.object({ commandname: Schema.string().description("指令名称"), Audit_value_blacklist: Schema.union([ Schema.const('1').description('关闭审核,直接通过'), Schema.const('2').description('检测到黑名单词汇,禁止通过'), Schema.const('3').description('黑名单词汇替换为`***`后,通过审核'), ]).description('黑名单处理').default("3"), Audit_value_whitelist: Schema.union([ Schema.const('1').description('关闭白名单功能'), Schema.const('2').description('检测到白名单词汇,直接通过'), Schema.const('3').description('保留白名单词汇,防止被黑名单词汇替换掉'), ]).description('白名单处理').default("3"), })).role('table').description("审核配置<br>左侧写需要审核功能的指令名称(即触发审核的输入,填入对应的指令名称。)<br>(`@`代表`@机器人`开头的消息 `这里是示例,可以删掉这一行`)<br>").default( [ { "commandname": "say", "Audit_value_blacklist": "2", "Audit_value_whitelist": "3" }, { "commandname": "绘画", "Audit_value_blacklist": "3", "Audit_value_whitelist": "3" }, { "commandname": "@", "Audit_value_blacklist": "2", "Audit_value_whitelist": "3" } ] ), Return_Audit_Result_true: Schema.boolean().default(false).description('返回发送`审核通过`的文字提示 `不影响审核功能`'), Return_Audit_Result_false: Schema.boolean().default(true).description('返回发送`审核不通过`的文字提示 `不影响审核功能`'), }).description('基础设置'), Schema.object({ Audit_Vocabulary_txt: Schema.boolean().default(true).description('启用自带的 [Vocabulary 词库](https://github.com/konsheng/Sensitive-lexicon)<br>➩关闭后,仅使用下面配置项的内容'), replace_text: Schema.string().default("***").description("`黑名单词汇`的替换文本:`***`"), Blacklist_input: Schema.array(Schema.object({ input: Schema.string().description("关键词"), texttip: Schema.string().description("返回提示词").default("输入文本违规,不予调用。"), })).role('table').description('关键词-黑名单(优先)<br>对于一些额外的违禁词屏蔽').default(Blacklist_input_default), Whitelist_input: Schema.array(Schema.object({ input: Schema.string().description("关键词"), texttip: Schema.string().description("返回提示词,该项无实际作用"), })).role('table').description('关键词-白名单<br>对于一些不合逻辑的关键词的取消屏蔽').default(Whitelist_input_default), }).description('违禁词调整设置'), Schema.object({ loggerinfo: Schema.boolean().default(false).description('日志调试模式') }).description('调试设置'), ]); function apply(ctx, config) { function logInfo(message, message2) { if (config.loggerinfo) { if (message2) { logger.info(`${message} ${message2}`) } else { logger.info(message); } } } async function loadVocabulary() { if (!config.Audit_Vocabulary_txt) return []; const vocabPath = path.join(__dirname, '../Vocabulary'); const files = fs.readdirSync(vocabPath); let vocabulary = []; for (const file of files) { const filePath = path.join(vocabPath, file); const data = fs.readFileSync(filePath, 'utf8'); const words = data.split('\n').map(word => word.trim()).filter(word => word); vocabulary.push(...words); } return vocabulary; } // 转义正则表达式特殊字符的辅助函数 function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& 表示整个匹配的字符串 } async function auditText(text, vocabulary, whitelist, blacklist, auditConfig) { const lowerText = text.toLowerCase(); // 检查白名单(仅在白名单功能开启时) const matchedWhitelist = []; if (auditConfig.Audit_value_whitelist !== '1') { for (const wordObj of whitelist) { if (lowerText.includes(wordObj.input.toLowerCase())) { matchedWhitelist.push(wordObj.input); } } } // 检查黑名单 (包括词库和自定义黑名单) const matchedBlacklist = []; const matchedBlacklistTips = {}; // 用于存储匹配到的黑名单词汇及其对应的提示 for (const word of vocabulary) { if (lowerText.includes(word.toLowerCase())) { matchedBlacklist.push(word); matchedBlacklistTips[word] = "输入文本违规,不予调用。"; // 默认提示 } } for (const wordObj of blacklist) { if (lowerText.includes(wordObj.input.toLowerCase())) { matchedBlacklist.push(wordObj.input); matchedBlacklistTips[wordObj.input] = wordObj.texttip; // 使用自定义提示 } } // 处理黑名单逻辑 if (matchedBlacklist.length > 0) { if (auditConfig.Audit_value_blacklist === '2') { logInfo(`匹配到黑名单词汇: ${matchedBlacklist.join(', ')},禁止通过`); // 返回第一个匹配到的黑名单词汇及其提示 const firstMatchedWord = matchedBlacklist[0]; return { result: false, matchedWord: firstMatchedWord, texttip: matchedBlacklistTips[firstMatchedWord] }; } else if (auditConfig.Audit_value_blacklist === '3') { logInfo(`匹配到黑名单词汇: ${matchedBlacklist.join(', ')},替换为 ${config.replace_text} 后通过`); let modifiedText = text; // 构建正则表达式, 优先匹配更长的词, 并且先处理白名单 const sortedBlacklist = matchedBlacklist.sort((a, b) => b.length - a.length); // 如果白名单功能开启,先排除白名单中的词 if (auditConfig.Audit_value_whitelist !== '1') { const sortedWhitelist = matchedWhitelist.sort((a, b) => b.length - a.length); // 先将白名单中的词替换成一个唯一的临时占位符 for (const whiteWord of sortedWhitelist) { const whiteWordRegex = new RegExp(escapeRegExp(whiteWord), 'gi'); modifiedText = modifiedText.replace(whiteWordRegex, (match) => { return `__WHITELIST_PLACEHOLDER_${whiteWord}__`; }); } } // 然后替换黑名单中的词 const blacklistPattern = sortedBlacklist.map(escapeRegExp).join('|'); modifiedText = modifiedText.replace( new RegExp(`(${blacklistPattern})`, 'gi'), (match) => config.replace_text ); // 最后将临时占位符替换回白名单的词 if (auditConfig.Audit_value_whitelist !== '1') { modifiedText = modifiedText.replace(/__WHITELIST_PLACEHOLDER_(.*?)__/g, (match, word) => { return word; }); } logInfo("modifiedText:", modifiedText) return { result: true, modifiedText }; } } return { result: true }; // 通过 } ctx.middleware(async (session, next) => { if (!config.Audit) { return next(); // 如果审核功能未开启,直接通过消息 } // 如果 at 了其他人,不处理本次消息 if (session.stripped.hasAt && !session.stripped.atSelf) { logInfo("用户 at 了其他人,不处理本次输入"); return next(); } let originalContent = session.content; // 存储原始的 session.content let inputContent = session.stripped.content.trim(); const [command, ...args] = inputContent.split(/\s+/); // 分割指令和参数 const lowerCommand = command.toLowerCase(); const prefixes = session.app.koishi.config.prefix || ctx.root.options.prefix || []; const isDirectMessage = session.isDirect; // 处理 @ 机器人的情况 if (session.stripped.hasAt && session.stripped.atSelf) { const atConfig = config.Audit_Configuration.find(item => item.commandname === "@"); if (atConfig) { const anothercontent = session.stripped.content.trim(); // 使用原始的,未小写的content logInfo(`用户输入内容为\n${anothercontent}`); const vocabulary = await loadVocabulary(); const whitelist = config.Whitelist_input; const blacklist = config.Blacklist_input; const auditResult = await auditText(anothercontent, vocabulary, whitelist, blacklist, atConfig); if (auditResult.result) { if (auditResult.modifiedText) { session.content = auditResult.modifiedText; // 更新会话内容为审核后的文本 } if (config.Return_Audit_Result_true) { await session.send(h.text('审核通过')); } logInfo(`审核通过`); //return next(); // 通过消息,允许处理 // 注释掉这里的 return } else { if (config.Return_Audit_Result_false && auditResult.texttip) { await session.send(h.text(auditResult.texttip)); } logInfo(`输入文本违规,不予交互。`); return; // 屏蔽消息 } } } // 遍历配置,查找匹配的指令 for (const commandConfig of config.Audit_Configuration) { if (commandConfig.Audit_value_blacklist === '1') continue; //跳过关闭审核的配置 const commandName = commandConfig.commandname.toLowerCase(); const expectedCommand = isDirectMessage ? [commandName, ...prefixes.map(p => p + commandName)] : prefixes.map(p => p + commandName); if (expectedCommand.includes(lowerCommand)) { const anothercontent = inputContent; // 使用原始输入进行违禁词检查, 不需要 toLowerCase() logInfo(`用户输入内容为\n${anothercontent}`); const vocabulary = await loadVocabulary(); const whitelist = config.Whitelist_input; const blacklist = config.Blacklist_input; const auditResult = await auditText(anothercontent, vocabulary, whitelist, blacklist, commandConfig); if (auditResult.result) { if (auditResult.modifiedText) { // 直接使用审核后的文本 session.content = `${auditResult.modifiedText}`; logInfo(`auditResult.modifiedText: ${auditResult.modifiedText}`); logInfo(`session.content: ${session.content}`); } if (config.Return_Audit_Result_true) { await session.send(h.text('审核通过')); } logInfo(`审核通过`); //return next(); // 通过消息,允许处理 // 注释掉这里的 return } else { if (config.Return_Audit_Result_false && auditResult.texttip) { await session.send(h.text(auditResult.texttip)); } logInfo(`输入文本违规,不予调用。`); return; // 屏蔽消息 } } } // 在所有审核逻辑之后,调用 next() if (session.content !== originalContent) { // 只有session.content被修改过,才继续 return next(); } else { return next(); // 如果没有修改,也继续 } }, true); } exports.apply = apply;