UNPKG

koishi-plugin-satori-ai

Version:

基于 Satori 适配器的智能对话插件,具备用户画像、长期记忆、常识管理和好感度系统

1,184 lines (1,172 loc) 166 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 __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], 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/locales/zh.yml var require_zh = __commonJS({ "src/locales/zh.yml"(exports, module2) { module2.exports = { commands: { sat: { description: "AI聊天", messages: { "no-prompt": "你好。使用说明可以看我的空间哦。", tooManyEnglishLetters: "请不要用这么多英文......", err: "寄!{0}", tooLong: "你的话太多了。", content_tooLong: "这个问题有点复杂,你还是去问别人吧。", "duplicate-dialogue": "这个刚刚说过了吧......", online: "思考中,请再等一会!", "no-response": "我不知道你在说什么……", update_level_succeed: "用户等级已更新为{0}", exceeds: "已经差不多够了吧?请明天再来。", usage: "今日次数{0}/{1}", hatMan: "叮!帽子先生保护了你的好感度,快说谢谢帽子先生吧!", rockBottom: "叮!谷底小石保护了你的好感度,快说谢谢谷底小石吧!", not_good_mood: "别得寸进尺……(好感↓)", pocket_money: "心情不错,给你点零花钱吧~(获得了{0}p点)", warning: "<at id={0}/>", block1: "我讨厌你!", block2: "我讨厌你们!" } }, "sat.clear": { description: "清空当前频道会话", messages: { clean: "已清空当前会话。", Allclean: "已清空所有会话。" } }, "sat.common_sense": { description: "添加常识", messages: { "no-prompt": "你想让我知道什么?", succeed: "我知道了,{0}" } }, "sat.broadcast": { description: "广播一条消息,在每个生效群收到首条消息时会推送", options: { delete: "删除一条广播,参数为广播的ID", list: "查看当前所有广播", img: "使用puppeteer图片化广播消息" } } } }; } }); // src/index.tsx var src_exports = {}; __export(src_exports, { SAT: () => SAT, default: () => src_default, puppeteer: () => puppeteer, refreshPuppeteer: () => refreshPuppeteer }); module.exports = __toCommonJS(src_exports); var import_koishi14 = require("koishi"); var path7 = __toESM(require("path")); // src/api.ts var import_koishi2 = require("koishi"); // src/utils.ts var import_koishi = require("koishi"); // src/database.ts async function isTargetIdNotExists(ctx, userId) { const users = await ctx.database.get("p_system", { userid: userId }); return users.length === 0; } __name(isTargetIdNotExists, "isTargetIdNotExists"); async function createUser(ctx, user) { await ctx.database.create("p_system", { userid: user.userid || "", usersname: user.usersname || "", p: user.p || 0, favorability: user.favorability || 0, userlevel: user.userlevel || 0, usage: user.usage || 0, lastChatTime: user.lastChatTime || (/* @__PURE__ */ new Date()).getDate(), items: user.items || {} }); } __name(createUser, "createUser"); async function updateFavorability(ctx, user, adjustment) { if (!user) return; let newValue; if (typeof adjustment === "number") newValue = user.favorability + adjustment; else newValue = user.favorability; await ctx.database.set("p_system", { userid: user.userid }, { favorability: newValue }); } __name(updateFavorability, "updateFavorability"); async function updateUserLevel(ctx, user, level) { if (!user) return; level = level < 5 ? level : 4; await ctx.database.set("p_system", { userid: user.userid }, { userlevel: level }); } __name(updateUserLevel, "updateUserLevel"); async function updateUserUsage(ctx, user, adjustment = 1) { if (!user) return; if (user.lastChatTime && (/* @__PURE__ */ new Date()).getDate() !== user.lastChatTime) user.usage = 0; await ctx.database.set("p_system", { userid: user.userid }, { usage: user.usage + adjustment }); await ctx.database.set("p_system", { userid: user.userid }, { lastChatTime: (/* @__PURE__ */ new Date()).getDate() }); return user.usage + adjustment; } __name(updateUserUsage, "updateUserUsage"); async function updateUserItems(ctx, user) { if (!user) return; await ctx.database.set("p_system", { userid: user.userid }, { items: user.items }); } __name(updateUserItems, "updateUserItems"); async function updateUserP(ctx, user, adjustment) { if (!user) return; user.p = await ctx.database.get("p_system", { userid: user.userid })[0]?.p || user.p; if (user.p + adjustment < 0) adjustment = -user.p; await ctx.database.set("p_system", { userid: user.userid }, { p: user.p + adjustment }); } __name(updateUserP, "updateUserP"); async function getUser(ctx, userId) { const users = await ctx.database.get("p_system", { userid: userId }); return users[0] || null; } __name(getUser, "getUser"); async function ensureUserExists(ctx, userId, username) { const notExists = await isTargetIdNotExists(ctx, userId); if (notExists) { await createUser(ctx, { userid: userId, usersname: username, p: 0, favorability: 0, userlevel: 0, usage: 0, lastChatTime: (/* @__PURE__ */ new Date()).getDate(), items: {} }); } return getUser(ctx, userId); } __name(ensureUserExists, "ensureUserExists"); function extendDatabase(ctx) { ctx.model.extend("p_system", { id: "unsigned", userid: "string", usersname: "string", p: "integer", favorability: "integer", userlevel: "integer", usage: "integer", lastChatTime: "integer", items: "object" }, { autoInc: true }); } __name(extendDatabase, "extendDatabase"); // src/utils.ts var fs = __toESM(require("fs")); var path = __toESM(require("path")); var logger = new import_koishi.Logger("satori-utils"); function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } __name(escapeRegExp, "escapeRegExp"); function parseTimeToMinutes(timeStr) { const [hours, minutes] = timeStr.split(":").map(Number); return hours * 60 + (minutes || 0); } __name(parseTimeToMinutes, "parseTimeToMinutes"); function parseTime(timestamp) { const date = new Date(timestamp); return date.getHours() * 60 + date.getMinutes(); } __name(parseTime, "parseTime"); function getTimeOfDay(hours) { if (hours >= 5 && hours < 9) return "清晨"; if (hours < 12) return "上午"; if (hours < 14) return "中午"; if (hours < 17) return "下午"; if (hours < 19) return "傍晚"; if (hours < 22) return "晚上"; return "深夜"; } __name(getTimeOfDay, "getTimeOfDay"); function trimSlash(url) { return url.replace(/\/$/, ""); } __name(trimSlash, "trimSlash"); function splitSentences(text, MIN_LENGTH, MAX_LENGTH) { const PUNCTUATION_REGEX = /([。!?!?…]+)/g; const rawSegments = text.split(PUNCTUATION_REGEX); const initialSentences = []; for (let i = 0; i < rawSegments.length; i += 2) { const sentence = rawSegments[i]?.trim() || ""; const punctuation = rawSegments[i + 1]?.trim() || ""; if (sentence) initialSentences.push(sentence + punctuation); } const finalSentences = []; let currentSentence = ""; for (const sentence of initialSentences) { const potentialLength = currentSentence.length + sentence.length; if (sentence.length > MAX_LENGTH) { if (currentSentence) finalSentences.push(currentSentence); finalSentences.push(sentence); currentSentence = ""; continue; } if (potentialLength <= MAX_LENGTH) { currentSentence += sentence; if (currentSentence.length >= MIN_LENGTH && potentialLength >= MAX_LENGTH * 0.8) { finalSentences.push(currentSentence); currentSentence = ""; } } else { finalSentences.push(currentSentence); currentSentence = sentence; } } if (currentSentence) finalSentences.push(currentSentence); return finalSentences.reduce((result, sentence) => { if (sentence.length < MIN_LENGTH && result.length > 0) { result[result.length - 1] += sentence; } else { result.push(sentence); } return result; }, []); } __name(splitSentences, "splitSentences"); function probabilisticCheck(probability) { return Math.random() < probability; } __name(probabilisticCheck, "probabilisticCheck"); function isErrorWithMessage(error) { return typeof error === "object" && error !== null && "message" in error; } __name(isErrorWithMessage, "isErrorWithMessage"); function processPrompt(prompt) { if (!prompt) return ""; if (prompt.includes(":poke")) return "戳戳"; prompt = prompt.replace(/<[^>]*?avatar[^>]*>/g, "。回复:"); prompt = prompt.replace(/<[^>]*?img[^>]*>/g, "[图片]"); prompt = prompt.replace(/<[^>]*?name="([^\"]*)"[^>]*>/g, (_, name) => `${name}`); prompt = prompt.replace(/\*\*/g, ""); if (!prompt) return "**"; return prompt.trim(); } __name(processPrompt, "processPrompt"); function filterResponse(prompt, words) { const parts = prompt.split(/([(\[【《(][^))]*[)\]】》)])/g); const filtered = parts.map((part) => { if (part.startsWith("(") && part.endsWith(")") || part.startsWith("(") && part.endsWith(")") || part.startsWith("[") && part.endsWith("]") || part.startsWith("【") && part.endsWith("】") || part.startsWith("《") && part.endsWith("》")) { return words.some((word) => part.includes(word)) ? "" : part; } return part; }).join(""); const cleaned = filtered.replace(/\s+/g, ""); const answerRegex = /<p>((?!<\/?p>)[\s\S])*?<\/p/g; const answerRegex2 = /<doubaothinking>((?!<\/?doubaothinking>)[\s\S])*?<\/p/g; const answerRegex3 = /<doubaothinking>((?!<\/?doubaothinking>)[\s\S])*?<\/doubaothinking>/g; const answerRegex4 = /<answer>((?!<\/?answer>)[\s\S])*?<\/answer>/g; const answerRegex5 = /<doubaothinking>((?!<\/?doubaothinking>)[\s\S])*?<\/answer>/g; const answerMatches = cleaned.match(answerRegex); const answerMatches2 = cleaned.match(answerRegex2); const answerMatches3 = cleaned.match(answerRegex3); const answerMatches4 = cleaned.match(answerRegex4); const answerMatches5 = cleaned.match(answerRegex5); const lastAnswer = answerMatches ? answerMatches[answerMatches.length - 1] : null; const lastDoubao = answerMatches2 ? answerMatches2[answerMatches2.length - 1] : null; const lastAnswer3 = answerMatches3 ? answerMatches3[answerMatches3.length - 1] : null; const lastAnswer4 = answerMatches4 ? answerMatches4[answerMatches4.length - 1] : null; const lastAnswer5 = answerMatches5 ? answerMatches5[answerMatches5.length - 1] : null; const answerContent = lastAnswer || lastDoubao || lastAnswer3 || lastAnswer4 || lastAnswer5; if (!answerContent) { return { content: "有点问题,稍后再来吧", error: true }; } const regex = /<p>|<\/p>|<doubaothinking>|<\/doubaothinking>|<\/p|<answer>|<\/answer>|<br>|<\/br>|<br\/>/g; const cleanedContent = answerContent.replace(regex, ""); return cleanedContent ? { content: cleanedContent, error: false } : { content: "有点问题,稍后再来吧", error: true }; } __name(filterResponse, "filterResponse"); function addOutputCensor(session, word, baseURL) { const blockWordsPath = path.resolve(baseURL, "output_censor.txt"); if (!fs.existsSync(blockWordsPath)) { fs.mkdirSync(blockWordsPath); fs.writeFileSync(blockWordsPath, word); } let blockWords = fs.readFileSync(blockWordsPath, "utf-8").split(","); blockWords.push(word); fs.writeFileSync(blockWordsPath, blockWords.join(",")); session.send(`添加"${word}"成功`); } __name(addOutputCensor, "addOutputCensor"); async function updateUserPWithTicket(ctx, user, adjustment) { if (!user) return; if (user?.items?.["地灵殿通行证"]?.description && user.items["地灵殿通行证"].description === "on") { await updateUserP(ctx, user, adjustment); } } __name(updateUserPWithTicket, "updateUserPWithTicket"); // src/api.ts var logger2 = new import_koishi2.Logger("satori-api"); var APIClient = class { constructor(ctx, config) { this.ctx = ctx; this.config = config; this.testConnection(); } static { __name(this, "APIClient"); } currentKeyIndex = 0; // 发送聊天请求 async chat(user, messages) { if (user.userid == "Alice") return { content: "<p>(系统)这是一个测试句子。这个句子稍长一些,包含多个标点符号!这是一个特别长的句子,需要超过最大长度限制的句子应该被保留原样,但这种情况在实际使用中应该尽量避免。最后?这是一个需要合并的短句!;</p>", error: false }; const enableUserKey = user?.items?.["地灵殿通行证"]?.description && user.items["地灵殿通行证"].description == "on"; let keys; let modle; let baseURL; if (enableUserKey) { const ticket = user.items["地灵殿通行证"].metadata; keys = [ticket?.key]; modle = ticket?.model; baseURL = ticket?.baseURL; const not_reasoner_model = ticket?.not_reasoner_model; const length = ticket?.use_not_reasoner_LLM_length; if (not_reasoner_model && length && messages[messages.length - 1].content.length <= length) modle = not_reasoner_model; } else { const config = this.config; const useNotReasoner = messages[messages.length - 1].content.length <= config.use_not_reasoner_LLM_length; keys = useNotReasoner ? config.not_reasoner_LLM_key : config.keys; modle = useNotReasoner ? config.not_reasoner_LLM : config.appointModel; baseURL = useNotReasoner ? config.not_reasoner_LLM_URL : config.baseURL; } const payload = this.createPayload(messages, modle); for (let i = 0; i < keys.length; i++) { try { return await this.tryRequest(baseURL, payload, keys); } catch (error) { if (i == keys.length - 1) { return this.handleAPIError(error); } this.rotateKey(); } } } // 发送辅助聊天请求 async auxiliaryChat(messages) { const AuxiliaryPayload = this.createAuxiliaryPayload(messages, this.config.auxiliary_LLM); const url = `${trimSlash(this.config.auxiliary_LLM_URL)}/chat/completions`; const headers = this.createHeaders(this.config.auxiliary_LLM_key); let content; for (let i = 1; i <= this.config.maxRetryTimes; i++) { try { const response = await this.ctx.http.post(url, AuxiliaryPayload, { headers, timeout: 36e5 }); content = response.choices[0].message.content; return { content, error: false }; } catch (error) { if (i == this.config.maxRetryTimes) { return this.handleAPIError(error); } logger2.warn(`辅助模型API请求失败(${error}),重试(第${i}次)中...`); continue; } } return { content: "unreachable", error: true }; } // 发送生成用户画像请求 async generateUserPortrait(user, messages) { if (user.userid == "Alice") return { content: "测试画像", error: false }; const enableUserKey = user?.items?.["地灵殿通行证"]?.description && user.items["地灵殿通行证"].description == "on"; let keys; let modle; let baseURL; if (enableUserKey) { const key = user.items["地灵殿通行证"].metadata?.key; keys = [key]; modle = user.items["地灵殿通行证"].metadata?.model; baseURL = user.items["地灵殿通行证"].metadata?.baseURL; } else { keys = this.config.keys; modle = this.config.appointModel; baseURL = this.config.baseURL; } const payload = this.createAuxiliaryPayload(messages, modle); const url = `${trimSlash(baseURL)}/chat/completions`; const headers = this.createHeaders(keys); let content; for (let i = 1; i <= this.config.maxRetryTimes; i++) { try { const response = await this.ctx.http.post(url, payload, { headers, timeout: 36e5 }); content = response.choices[0].message.content; return { content, error: false }; } catch (error) { if (i == this.config.maxRetryTimes) { return this.handleAPIError(error); } await new Promise((resolve3) => setTimeout(resolve3, this.config.retry_delay_time || 5e3)); logger2.warn(`生成画像时API请求失败(${error}),重试(第${i}次)中...`); continue; } } return { content: "unreachable", error: true }; } // 生成请求体 createPayload(messages, model) { return { model, messages, temperature: this.config.temperature, top_p: 1, frequency_penalty: this.config.frequency_penalty, presence_penalty: this.config.presence_penalty }; } // 生成辅助请求体 createAuxiliaryPayload(messages, model) { return { model, messages, temperature: 0.1 }; } // 尝试请求 async tryRequest(URL, payload, keys) { const url = `${trimSlash(URL)}/chat/completions`; const headers = this.createHeaders(keys); let content; for (let i = 1; i <= this.config.maxRetryTimes; i++) { try { const response = await this.ctx.http.post(url, payload, { headers, timeout: 36e5 }); logger2.info(`APIID: ${response.id}, 输入token: ${response.usage.prompt_tokens}, 输出token: ${response.usage.completion_tokens}, 总token: ${response.usage.total_tokens}`); content = response.choices[0].message.content; const reasoning_content = response.choices[0].message.reasoning_content || "无"; if (this.config.reasoning_content) logger2.info(`思维链: ${reasoning_content}`); if (!content && reasoning_content) { logger2.warn("返回内容为空,但存在推理内容"); } if (reasoning_content != "无" && content && content.length > reasoning_content.length) { logger2.warn("返回内容疑似包含推理内容"); } const responseMsg = { role: "assistant", content }; if (payload.messages.some((msg) => msg === responseMsg) && content.length > 5) { const lastMsg = payload.messages[payload.messages.length - 1]; payload.messages[payload.messages.length - 1].content = lastMsg.content + "(注意不要重复之前的内容)"; logger2.warn(`返回内容与之前内容相同,重试(第${i}次)中...`); continue; } return { content, error: false }; } catch (error) { if (i == this.config.maxRetryTimes) { return this.handleAPIError(error); } await new Promise((resolve3) => setTimeout(resolve3, this.config.retry_delay_time || 5e3)); logger2.warn(`API请求失败(${error}),重试(第${i}次)中...`); continue; } } return { content: "unreachable", error: true }; } // 生成请求头 createHeaders(keys) { return { Authorization: `Bearer ${keys[this.currentKeyIndex]}`, "Content-Type": "application/json" }; } // 处理API错误 handleAPIError(error) { if (!isErrorWithMessage(error)) throw error; const status = error.response?.status || 0; const errorCode = error.response?.data?.error?.code || "unknown"; const message = error.response?.data?.error?.message || error.message; logger2.error(`API Error [${status}]: ${errorCode} - ${message}`); switch (status) { case 400: return { content: "请求体格式错误", error: true }; case 401: return { content: "API key 错误,认证失败", error: true }; case 402: return { content: "账号余额不足", error: true }; case 422: return { content: "请求体参数错误", error: true }; case 429: return { content: "请求速率(TPM 或 RPM)达到上限", error: true }; case 500: return { content: `api服务器内部故障`, error: true }; case 503: return { content: "api服务器负载过高", error: true }; default: return { content: message, error: true }; } } // 切换API密钥 rotateKey() { this.currentKeyIndex = (this.currentKeyIndex + 1) % this.config.keys.length; logger2.debug(`Switched to API key index: ${this.currentKeyIndex}`); } // 测试连接 async testConnection() { try { await this.ctx.http.get(`${trimSlash(this.config.baseURL)}/models`, { headers: this.createHeaders(this.config.keys) }); logger2.info("API connection test succeeded"); return true; } catch (error) { logger2.error("API connection test failed"); return false; } } }; // src/memory.ts var import_koishi3 = require("koishi"); var fs2 = __toESM(require("fs")); var path2 = __toESM(require("path")); var logger3 = new import_koishi3.Logger("satori-memory"); var MemoryManager = class { constructor(ctx, config) { this.ctx = ctx; this.config = config; } static { __name(this, "MemoryManager"); } channelMemories = /* @__PURE__ */ new Map(); channelDialogues = /* @__PURE__ */ new Map(); charactersToRemove = ["的", "一", "是", "了", "什", "么", "我", "谁", "不", "人", "在", "他", "有", "这", "个", "上", "们", "来", "到", "时", "大", "地", "为", "子", "中", "你", "说", "生", "国", "年", "着", "就", "那", "和", "要", "她", "出", "也", "得", "里", "后", "自", "以", "会", "id=", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]; MAX_MEMORY_LENGTH = 5e3; // 更新记忆 async updateMemories(session, prompt, config, response) { if (response.error) return; this.updateChannelMemory(session, prompt, config, response.content); const date = ` (对话日期和时间:${(/* @__PURE__ */ new Date()).toLocaleString()})`; const userFavourbility = (await getUser(this.ctx, session.userId)).favorability; if (this.shouldRemember(prompt, userFavourbility)) { await this.saveLongTermMemory(session, [{ role: date, content: prompt }]); } } // 是否应当记忆 shouldRemember(content, userFavourbility) { return (content.length >= this.config.remember_min_length || content.includes("记住") && userFavourbility >= 50) && !this.config.memory_block_words.some((word) => content.includes(word)); } // 更新频道对话 async updateChannelDialogue(session, prompt, name) { if (!this.config.channel_dialogues) return ""; if (!this.channelDialogues[session.channelId]) this.channelDialogues[session.channelId] = []; this.channelDialogues[session.channelId].push('"' + name + '" 说: ' + prompt); if (this.channelDialogues[session.channelId]?.length > this.config.channel_dialogues_max_length) { this.channelDialogues = this.channelDialogues[session.channelId].slice(-this.config.channel_dialogues_max_length); } } async getChannelDialogue(session) { if (!this.config.channel_dialogues) return ""; const Dialogue = this.channelDialogues[session.channelId]?.join("\n") || ""; const result = "以下是群聊内群友最近的聊天记录,当用户话题涉及其中内容时你可以参考这些信息:{\n" + Dialogue + "\n}\n"; return result; } // 括号引号过滤 bracketFilter(content, config) { if (!config.bracket_filter) return content; let filtered = content.replace(/["'‘“]|[’”'"]/g, ""); let previous; do { previous = filtered; filtered = filtered.replace(/[(({\[][^()\]})]*[))\]\}]/g, ""); } while (filtered !== previous); return filtered.trim() || content.replace(/[(({\[]|[))\]\}]/g, ""); } // 内容过滤 memoryFilter(content, config) { if (!config.memory_filter) return content; const filterWords = config.memory_filter.split("-").map((word) => word.trim()).filter((word) => word.length > 0); if (!filterWords.length) return content; const sentenceSplitRegex = /([。!?;!?;…]|\.{3})/g; const sentences = content.split(sentenceSplitRegex).reduce((acc, cur, i, arr) => { if (i % 2 === 0) acc.push(cur + (arr[i + 1] || "")); return acc; }, []); const filtered = sentences.filter((sentence) => !filterWords.some((word) => sentence.includes(word))).join(""); const result = filtered.replace(/([,、])\1+/g, "$1").replace(/^[,。!?;,.!?]+/, "").trim(); return result || content; } // 更新短期记忆 updateChannelMemory(session, prompt, config, response) { if (response) { response = this.bracketFilter(response, config); response = this.memoryFilter(response, config); } let channelId = session.channelId; if (config.personal_memory) channelId = session.userId; if (!this.channelMemories.has(channelId)) { this.channelMemories.set(channelId, { dialogues: [], updatedAt: Date.now() }); } const memory = this.channelMemories.get(channelId); memory.dialogues.push({ role: "user", content: prompt }); this.updateChannelDialogue(session, prompt, session.username); if (this.config.enable_self_memory && response) { memory.dialogues.push({ role: "assistant", content: "<p>" + response + "</p>" }); } else { memory.dialogues.push({ role: "assistant", content: "<p>…</p>" }); } if (memory.dialogues.length > this.config.message_max_length) { memory.dialogues = memory.dialogues.slice(-this.config.message_max_length); } } // 清除频道记忆 clearChannelMemory(channelId) { this.channelMemories.delete(channelId); } // 清除频道对话 clearChannelDialogue(channelId) { this.channelDialogues[channelId] = []; } // 清除全部记忆 clearAllMemories() { this.channelMemories.clear(); this.channelDialogues = /* @__PURE__ */ new Map(); } // 返回频道记忆 getChannelMemory(channelId) { return this.channelMemories.get(channelId)?.dialogues || []; } // 长期记忆存储 async saveLongTermMemory(session, dialogues, filePath = "") { if (filePath === "") filePath = this.getUserMemoryPath(session.userId); await this.ensureMemoryFile(filePath); const filtered = dialogues.filter((entry) => !this.config.memory_block_words.some((word) => entry.content.includes(word))); if (filtered.length === 0) return; const existing = await this.loadMemoryFile(filePath); let updated = [...existing, ...filtered]; if (updated.length > this.MAX_MEMORY_LENGTH) { updated = updated.slice(-this.MAX_MEMORY_LENGTH); } fs2.writeFileSync(filePath, JSON.stringify(updated, null, 2)); } // 记忆检索 async searchMemories(session, prompt, type = "user") { const filePathMap = { "user": this.getUserMemoryPath(session.userId), "common": path2.join(this.config.dataDir, "common_sense.txt") }; const topNMap = { "user": this.config.dailogues_topN, "common": this.config.common_topN }; const filePath = filePathMap[type]; if (!fs2.existsSync(filePath)) { return ""; } const keywords = prompt.split("").filter((word) => !this.charactersToRemove.includes(word)); let entries = await this.loadMemoryFile(filePath); const matched = this.findBestMatches(entries, keywords).slice(0, topNMap[type] * 5); if (type === "user") { const remainingEntries = entries.filter((entry) => !matched.includes(entry)); let updatedEntries = [...remainingEntries, ...matched]; fs2.writeFileSync(filePath, JSON.stringify(updatedEntries, null, 2)); } const result = this.formatMatches(matched, type, topNMap[type]); return result; } // 记忆检索 findBestMatches(entries, keywords) { return entries.map((entry) => ({ entry, ...this.calculateMatchScore(entry.content, keywords) })).filter(({ count }) => count > 1).sort((a, b) => b.score - a.score).map(({ entry }) => entry); } // 匹配度计算 calculateMatchScore(content, keywords) { if (keywords.length === 0) return { score: 0, count: 0 }; const escapedKeywords = keywords.map((k) => escapeRegExp(k)); const regex = new RegExp(escapedKeywords.join("|"), "gi"); const chineseRegex = /[\u4e00-\u9fa5]/g; const regex2 = new RegExp(`[${this.charactersToRemove.join("")}]`, "g"); content = content.replace(regex2, ""); const matches = content.match(regex) || []; let count = 0; matches.forEach((match) => { const chineseCount = (match.match(chineseRegex) || []).length; const englishCount = match.length - chineseCount; count += chineseCount * 2 + englishCount; }); const totalChinese = (content.match(chineseRegex) || []).length; const totalEnglish = content.length - totalChinese; const totalWeight = totalChinese * 2 + totalEnglish; const ratio = totalWeight > 0 ? count / totalWeight : 0; return { score: ratio, count }; } // 格式化匹配结果 formatMatches(matched, type, topN = 5) { const prefixMap = { "common": "这是你可能用到的信息:", "user": "以下是较久之前用户说过的话和对话时间:" }; const time = `时段:${getTimeOfDay((/* @__PURE__ */ new Date()).getHours())}`; const date = `当前日期和时间:${(/* @__PURE__ */ new Date()).toLocaleString()} ${time}`; if (matched.length > 0) { matched = matched.slice(0, topN < matched.length ? topN : matched.length); if (type === "common") { const result = `${prefixMap[type]}{ ${matched.map((entry) => entry.content).join("\n")} ${date} } `; return result; } else { matched.forEach((entry) => entry.content = entry.content + (entry.role === "user" ? "(未记录时间)" : entry.role)); const result = `${prefixMap[type]}{ ${matched.map((entry) => entry.content).join("\n")} } `; return result; } } else { if (type === "common") { return "这是你可能用到的信息:{\n" + date + "\n}\n"; } else { return ""; } } } // 获取用户记忆文件路径 getUserMemoryPath(userId) { return path2.join(this.config.dataDir, "dialogues", `${userId}.txt`); } // 确保记忆文件存在 async ensureMemoryFile(filePath) { fs2.mkdirSync(path2.dirname(filePath), { recursive: true }); if (!fs2.existsSync(filePath)) { fs2.writeFileSync(filePath, "[]", "utf-8"); } } // 加载记忆文件 async loadMemoryFile(filePath) { try { return JSON.parse(fs2.readFileSync(filePath, "utf-8")); } catch { return []; } } // 获取频道上下文 getChannelContext(channelId) { return this.channelMemories.get(channelId)?.dialogues || []; } }; // src/fixed-dialogues.ts var fs3 = __toESM(require("fs")); var path3 = __toESM(require("path")); async function handleFixedDialogues(ctx, session, user, prompt, config) { if (!config.enable_fixed_dialogues) return null; const filePath = path3.join(config.dataDir, "fixed_dialogues.json"); const dialogues = await loadFixedDialogues(filePath); const currentTime = parseTime(session.timestamp); const matched = dialogues.filter((dialogue) => matchDialogue(dialogue, prompt, user, currentTime)); if (matched.length === 0) return null; const selected = selectDialogueByProbability(matched); await processFavorability(ctx, user, selected); return selected.response; } __name(handleFixedDialogues, "handleFixedDialogues"); async function ensureFixedDialoguesFile(filePath) { if (!fs3.existsSync(filePath)) { const defaultDialogues = [ { triggers: ["你好", "您好"], favorabilityRange: [0, 100], probability: 1, timeRange: ["06:00", "08:00"], response: "早上好!很高兴见到你。" }, { triggers: ["再见", "拜拜"], favorabilityRange: [0, 100], probability: 1, timeRange: ["18:00", "20:00"], response: "再见!希望很快再见到你。" } ]; fs3.writeFileSync(filePath, JSON.stringify(defaultDialogues, null, 2)); } } __name(ensureFixedDialoguesFile, "ensureFixedDialoguesFile"); async function loadFixedDialogues(filePath) { await ensureFixedDialoguesFile(filePath); try { return JSON.parse(fs3.readFileSync(filePath, "utf-8")); } catch (error) { console.error("Error loading fixed dialogues:", error); return []; } } __name(loadFixedDialogues, "loadFixedDialogues"); function matchDialogue(dialogue, content, user, currentTime) { const triggerMatch = dialogue.triggers.some((t) => content === t); const favorabilityMatch = matchFavorability(dialogue, user); const timeMatch = matchTimeRange(dialogue, currentTime); return triggerMatch && favorabilityMatch && timeMatch; } __name(matchDialogue, "matchDialogue"); function matchFavorability(dialogue, user) { if (!dialogue.favorabilityRange) return true; const [min, max] = dialogue.favorabilityRange; return user.favorability >= min && user.favorability <= max; } __name(matchFavorability, "matchFavorability"); function matchTimeRange(dialogue, currentTime) { if (!dialogue.timeRange) return true; const [start, end] = dialogue.timeRange.map(parseTimeToMinutes); return currentTime >= start && currentTime <= end; } __name(matchTimeRange, "matchTimeRange"); function selectDialogueByProbability(dialogues) { const total = dialogues.reduce((sum, d) => sum + d.probability, 0); let random = Math.random() * total; for (const dialogue of dialogues) { random -= dialogue.probability; if (random <= 0) return dialogue; } return dialogues[0]; } __name(selectDialogueByProbability, "selectDialogueByProbability"); async function processFavorability(ctx, user, dialogue) { if (dialogue.favorability) { await updateFavorability(ctx, user, dialogue.favorability); } } __name(processFavorability, "processFavorability"); // src/favorability.ts var fs4 = __toESM(require("fs")); var path4 = __toESM(require("path")); async function handleFavorabilitySystem(ctx, session, config) { const user = await ensureUserExists(ctx, session.userId, session.username); const level = getFavorabilityLevel(user, config); if (user.favorability < config.favorability_div_1 - 20 && user.favorability > -900 && level !== "夫妻") { return session.text("commands.sat.messages.block1"); } const processedPrompt = processPrompt(session.content); const englishCount = (processedPrompt.match(/[a-zA-Z]/g) || []).length; if (user.favorability < 50 && englishCount > 8 && level !== "夫妻") { return session.text("commands.sat.messages.tooManyEnglishLetters"); } if (englishCount > 30 && level !== "夫妻") { return session.text("commands.sat.messages.tooManyEnglishLetters"); } if (englishCount > 60) { return session.text("commands.sat.messages.tooManyEnglishLetters"); } return; } __name(handleFavorabilitySystem, "handleFavorabilitySystem"); function getFavorabilityLevel(user, config) { if (user?.items["订婚戒指"]?.count > 0 && user?.items["订婚戒指"]?.description && user?.items["订婚戒指"]?.description == "已使用") return "夫妻"; if (user.favorability < config.favorability_div_1) return "厌恶"; if (user.favorability < config.favorability_div_2) return "陌生"; if (user.favorability < config.favorability_div_3) return "朋友"; if (user.favorability < config.favorability_div_4) return "暧昧"; return "恋人"; } __name(getFavorabilityLevel, "getFavorabilityLevel"); function generateLevelPrompt(level, config, user) { const prompts = { "厌恶": config.prompt_0, "陌生": config.prompt_1, "朋友": config.prompt_2, "暧昧": config.prompt_3, "恋人": config.prompt_4, "夫妻": config.prompt_5 }; return `${prompts[level]} `; } __name(generateLevelPrompt, "generateLevelPrompt"); function generateAuxiliaryPrompt(prompt, responseContent, user, config) { const messages = []; messages.push({ role: "system", content: "请你评价我之后给你的对话,你需要从回答者的角度,猜测回答者听到此问题和做出此回答的感受好坏,然后返回打分。你需要谨慎判断回答者是在警告还是在调情。你返回的值应当是从0到9之间的一个数字,数字越大代表感受越幸福,数字越小代表感受越恶心。你只需要返回一个数字,不要补充其他内容" }); messages.push({ role: "user", content: `问题:${prompt},回答:${responseContent}` }); return messages; } __name(generateAuxiliaryPrompt, "generateAuxiliaryPrompt"); async function handleAuxiliaryResult(ctx, session, config, responseContent) { const user = await ensureUserExists(ctx, session.userId, session.username); const regex = /\d+/g; const value = parseInt(responseContent.match(regex)[0]) ? parseInt(responseContent.match(regex)[0]) : 5; let favorabilityEffect = value - config.offset_of_fafavorability; await applyFavorabilityEffect(ctx, user, favorabilityEffect ? favorabilityEffect : 0, session); if (favorabilityEffect < 0) { return "(好感↓)"; } if (favorabilityEffect > 0) { return "(好感↑)"; } return; } __name(handleAuxiliaryResult, "handleAuxiliaryResult"); async function inputContentCheck(ctx, content, userid, config, session, moodManager) { const user = await getUser(ctx, userid); if (!user) return 0; const regex = /\*\*/g; const hasCensor = regex.test(content); if (hasCensor && config.input_censor_favorability) { moodManager.handleInputMoodChange(user, getFavorabilityLevel(user, config)); await applyFavorabilityEffect(ctx, user, -1 * config.value_of_input_favorability, session); return -1 * config.value_of_input_favorability; } moodManager.applyMoodChange(user, 1); if (config.enable_auxiliary_LLM || user.usage > config.max_favorability_perday) return 0; const mood = moodManager.getMoodValue(user.userid); if (mood >= 0) await applyFavorabilityEffect(ctx, user, 1, session); return 1; } __name(inputContentCheck, "inputContentCheck"); async function outputContentCheck(ctx, response, userid, config, session, moodManager) { if (response.error) return 0; const user = await getUser(ctx, userid); if (!user) return 0; if (config.output_censor_favorability) { const content = response.content; const filePath = path4.join(config.dataDir, "output_censor.txt"); if (!fs4.existsSync(filePath)) { return 0; } const censorWords = fs4.readFileSync(filePath, "utf-8").split(","); const censoredContent = censorWords.reduce((acc, cur) => acc.replace(cur, "**"), content); const regex = /\*\*/g; const hasCensor = regex.test(censoredContent); const moodLevel = moodManager.getMoodLevel(user.userid); if (hasCensor) { const mood = moodManager.getMoodValue(user.userid); moodManager.handleOutputMoodChange(user, getFavorabilityLevel(user, config)); if (mood <= 0) { await updateFavorability(ctx, user, -1 * config.value_of_output_favorability); return -config.value_of_output_favorability; } } if (moodLevel === "angry") { await updateFavorability(ctx, user, -1 * config.value_of_output_favorability); return -config.value_of_output_favorability; } } return 0; } __name(outputContentCheck, "outputContentCheck"); async function ensureCensorFileExists(basePath) { const filePath = path4.join(basePath, "output_censor.txt"); fs4.mkdirSync(path4.dirname(filePath), { recursive: true }); if (!fs4.existsSync(filePath)) { fs4.writeFileSync(filePath, "示例屏蔽词1,示例屏蔽词2,示例屏蔽词3", "utf-8"); } } __name(ensureCensorFileExists, "ensureCensorFileExists"); async function applyFavorabilityEffect(ctx, user, effect, session) { if (effect < 0 && user.items["谷底小石"]?.count > 0) { session.send(session.text("commands.sat.messages.rockBottom")); return; } if (effect < 0 && user.items["帽子先生"]?.count > 0) { user.items["帽子先生"].count--; if (user.items["帽子先生"].count === 0) delete user.items["帽子先生"]; await updateUserItems(ctx, user); session.send(session.text("commands.sat.messages.hatMan")); return; } await updateFavorability(ctx, user, effect); } __name(applyFavorabilityEffect, "applyFavorabilityEffect"); // src/middleware.ts var import_koishi4 = require("koishi"); var logger4 = new import_koishi4.Logger("satori-ai-middleware"); function createMiddleware(ctx, sat, config) { return async (session, next) => { if (config.enable_favorability && config.enable_warning && session.channelId === config.warning_group) sat.getWarningList(session); sat.broadcastManager.seedBroadcast(session); if (!isSpecialMessage(session)) await sat.handleChannelMemoryManager(session); if (config.private && isPrivateSession(session)) { return await handlePrivateMessage(sat, session); } if (config.nick_name && await hasNickName(ctx, session, config)) { return await handleNickNameMessage(sat, session); } if (shouldRandomTrigger(session, config)) { return await sat.handleRandomMiddleware(session, session.content); } return next(); }; } __name(createMiddleware, "createMiddleware"); function isPrivateSession(session) { if (isSpecialMessage(session)) return false; return session.subtype === "private" || session.channelId.includes("private"); } __name(isPrivateSession, "isPrivateSession"); async function handlePrivateMessage(SAT2, session) { const content = session.content.trim(); if (content) return await SAT2.handleNickNameMiddleware(session, content); } __name(handlePrivateMessage, "handlePrivateMessage"); async function hasNickName(ctx, session, config) { if (session.userId === session.selfId) return false; if (config.nick_name_block_words.some((word) => session.content.includes(word))) return false; const user = await ensureUserExists(ctx, session.userId, session.username); let names = config.nick_name_list; if (user?.items?.["情侣合照"]?.metadata?.botNickName) { names = names.concat(user.items["情侣合照"].metadata.botNickName); } return names.some((name) => session.content.includes(name)); } __name(hasNickName, "hasNickName"); async function handleNickNameMessage(SAT2, session) { const content = session.content.trim(); if (content) return await SAT2.handleNickNameMiddleware(session, content); } __name(handleNickNameMessage, "handleNickNameMessage"); function shouldRandomTrigger(session, config) { const { content } = session; return !isSpecialMessage(session) && content.length >= config.random_min_tokens && content.length < config.max_tokens && probabilisticCheck(config.randnum); } __name(shouldRandomTrigger, "shouldRandomTrigger"); function isSpecialMessage(session) { const firstElement = session.elements[0]; return ["img", "at", "file"].includes(firstElement?.type) || session.content.includes(":poke") || session.content.includes("file://") || session.content.includes("http://") || session.content.includes("https://"); } __name(isSpecialMessage, "isSpecialMessage"); // src/types.ts var import_koishi5 = require("koishi"); var Sat = class extends import_koishi5.Service { static { __name(this, "Sat"); } static inject = { required: ["console", "database"], optional: ["censor", "puppeteer"] }; constructor(ctx, config) { super(ctx, "sat", true); } }; ((Sat2) => { Sat2.Config = import_koishi5.Schema.intersect([ import_koishi5.Schema.object({ baseURL: import_koishi5.Schema.string().default("https://api.deepseek.com").description("深度思考模型请求地址"), key: import_koishi5.Schema.union([ import_koishi5.Schema.array(String).role("secret"), import_koishi5.Schema.transform(String, (value) => [value]) ]).default([]).role("secret").description("深度思考模型api_key"), appointModel: import_koishi5.Schema.string().default("deepseek-reasoner").description("深度思考模型"), not_reasoner_LLM_URL: import_koishi5.Schema.string().default("https://api.deepseek.com").description("非深度思考模型请求地址"), not_reasoner_LLM: import_koishi5.Schema.string().default("deepseek-chat").description("非深度思考模型(用于节省成本"), not_reasoner_LLM_key: import_koishi5.Schema.union([ import_koishi5.Schema.array(String).role("secret"), import_koishi5.Schema.transform(String, (value) => [value]) ]).default([]).role("secret").description("非深度思考模型api_key"), use_not_reasoner_LLM_length: import_koishi5.Schema.number().default(8).description("触发使用非深度思考模型的字数(输入少于此字数时),如果你希望始终使用非深度思考模型进行对话,请将此值设置成一个较大的值,反之亦然"), enable_reasoner_like: import_koishi5.Schema.boolean().default(true).description("是否启用非深度思考模型模仿思维链(可以在节省成本的同时提高效果)"), auxiliary_LLM_URL: import_koishi5.Schema.string().default("https://api.deepseek.com").description("辅助模型请求地址"), auxiliary_LLM: import_koishi5.Schema.string().default("deepseek-chat").description("辅助模型(用于好感度调整等功能,如不需要可不填,建议使用低成本模型"), auxiliary_LLM_key: import_koishi5.Schema.union([ import_koishi5.Schema.array(String).role("secret"), import_koishi5.Schema.transform(String, (value) => [value]) ]).default([]).role("secret").description("辅助模型api_key"), prompt: import_koishi5.Schema.string().role("textarea").description("人格设定"), reasoner_prompt: import_koishi5.Schema.string().role("textarea").description("思考提示词(对于非深度思考模型模仿思维链时生效)").default("你在思考时应当逐步分析以下内容:1.当前对话的禁止事项;2.是否与群聊中其他人的发言有关联;3.是否与当前用户之前说的话有关联;4.对话的具体要求;5.用户的意图如何;6.最终的发言的主要内容。") }).description("基础设置"), import_koishi5.Schema.object({ alias: import_koishi5.Schema.array(String).default(["ai"]).description("触发命令;别名"), authority: import_koishi5.Schema.number().role("slider").min(0).max(5).step(1).description("允许使用的最低权限").default(1), no_system_prompt: import_koishi5.Schema.boolean().default(false).description("是否将系统提示的system替换为user(用于具有思维链的模型)"), max_tokens: import_koishi5.Schema.number().description("最大请求长度(字符数)").default(100), message_max_length: import_koishi5.Schema.number().description("最大频道上下文长度(条数)").default(10), temperature: import_koishi5.Schema.number().role("slider").min(0).max(2).step(0.01).default(0.5).description("温度"), frequency_penalty: import_koishi5.Schema.number().default(0).description("频率惩罚"), presence_penalty: import_koishi5.Schema.number().default(0).description("存在惩罚"), maxRetryTimes: import_koishi5.Schema.number().default(10).description("报错后最大重试次数"), retry_delay_time: import_koishi5.Schema.number().default(5e3).description("每次重试延迟时间"), max_parallel_count: import_koishi5.Schema.number().default(2).description("频道最大并行请求数"), log_system_prompt: import_koishi5.Schema.boolean().default(false).description("是否在日志中输出系统提示"), log_reasoning_content: import_koishi5.Schema.boolean().default(true).description("是否在日志中输出思维链"), log_ask_response: import_koishi5.Schema.boolean().default(true).description("是否在日志中输出问答") }).description("请求设置"), import_koishi5.Schema.object({ enable_self_memory: import_koishi5.Schema.boolean().default(true).description("是否启用模型自发言记忆(仅短期)"), personal_memory: import_koishi5.Schema.boolean().default(true).description("是否启用按人记忆(否则将群内所有人视为同一个用户)"), channel_dialogues: import_koishi5.Schema.boolean().default(true).description("是否获取群聊内最近对话(包括不对bot说的)"), channel_dialogues_max_length: import_koishi5.Schema.number().default(20).description("群聊内最近对话最大长度(条数)"), bracket_filter: import_koishi5.Schema.boolean().default(false).description("是否启用括号过滤,开启后在写入短期记忆时会过滤掉括号内的内容,用于缓解复读问题"), memory_filter: import_koishi5.Schema.string().role("textarea").default("示例1-示例2").description("短期记忆过滤词,使用“-”分隔,含有过滤词的那一句不会被记忆,用于缓解复读问题"), dataDir: import_koishi5.Schema.string().default("./data/satori_ai").description("聊天记录保存位置(长期记忆)"), memory_block_words: import_koishi5.Schema.array(String).default(["好感"]).description("记忆屏蔽词"), remember_min_length: import_koishi5.Schema.number().description("触发保存到记忆的长度").default(20), common_topN: import_koishi5.Schema.number().default(5).description("常识记忆检索最大匹配数"), dailogues_topN: import_koishi5.Schema.number().default(5).description("对话记忆检索最大匹配数"), enable_fixed_dialogues: import_koishi5.Schema.boolean().default(false).description("是否启用固定对话(在dataDir中的fixed_dialogues.json修改)") }).description("记忆设置"), import_koishi5.Schema.object({ max_usage: import_koishi5.Schema.tuple([Number, Number, Number, Number, Number]).default([40, 240, 3e3, 9999, 0]).description("每日最大使用次数(对应用户level0~level4)(0为不限制)"), private: import_koishi5.Schema.boolean().default(false).description("开启后私聊AI可触发对话, 不需要使用指令"), nick_name: import_koishi5.Schema.boolean().default(true).description("是否使用昵称触发对话(发言中含有昵称时)(把qq号加入昵称可以被at和戳一戳触发)"), nick_name_list: import_koishi5.Schema.array(String).default(["昵称1"]).description("昵称列表"), nick_name_block_words: import_koishi5.Schema.array(String).default(["屏蔽词1"]).description("昵称屏蔽词(含有屏蔽词的消息不会触发昵称对话)"), reasoner_filter: import_koishi5.Schema.boolean().default(true).description("是否启用返回内容过滤,开启后在对话时会过滤掉在括号内且含有过滤词的那一句,用于缓解思维链溢出问题"), reasoner_filter_word: import_koishi5.Schema.string().role("textarea").default("系统-提示-用户-设定-回复").description("返回内容过滤词,使用“-”分隔,在括号内且含有过滤词的那一句会被过滤,用于缓解思维链溢出问题"), duplicateDialogueCheck: import_koishi5.Schema.boolean().default(true).description("是否检查重复对话"), enable_online_user_check: import_koishi5.Schema.boolean().default(true).description("在未回答而再次提问时是否提示用户有未完成的对话"), random_min_tokens: import_koishi5.Schema.number().default(20).description("随机触发对话的最小长度"), randnum: import_koishi5.Schema.number().role("slider").min(0).max(1).step(0.01).default(0).description("在群聊中随机触发对话的概率,如需关闭可设置为 0"), sentences_divide: import_koishi5.Schema.boolean().default(true).description("是否分句发送"), min_sentences_length: import_koishi5.Schema.number().default(10).description("每个分句的最小长度"), max_sentences_length: import_koishi5.Schema.number().default(20).description("每个分句的最大长度"), time_interval: import_koishi5.Schema.number().default(1e3).description("每句话的时间间隔"), reply_pointing: import_koishi5.Schema.boolean().default(true).description("是否在与多人同时对话时显示回复指向") }).description("对话设置"), import_koishi5.Schema.object({ enable_favorability: import_koishi5.Schema.boolean().default(false).description("是否开启好感度系统(每次对话默认+1好感度)"), max_favorability_perday: import_koishi5.Schema.number().default(100).description("每日有效(引发好感度增长)对话次数上限"), input_censor_favorability: import_koishi5.Schema.boolean().default(false).description("是否开启好感度审查(通过输入屏蔽词扣除好感)"), value_of_input_favorability: import_koishi5.Schema.number().default(15).description("输入触发输入屏蔽词每次扣除的好感度"), output_censor_favorability: import_koishi5.Schema.boolean().default(false).description("通过输出屏蔽词扣除好感,在dataDir中的output_censor.txt修改)"), value_of_output_favorability: import_koishi5.Schema.number().default(15).description("输出触发输出屏蔽词每次扣除的好感度"), enable_auxiliary_LLM: import_koishi5.Sche