UNPKG

koishi-plugin-satori-ai

Version:

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

1,358 lines (1,344 loc) 236 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"(exports2, 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_koishi16 = require("koishi"); var path8 = __toESM(require("path")); var import_fs = require("fs"); // src/emoji.ts var import_koishi2 = require("koishi"); var fs2 = __toESM(require("fs")); var path2 = __toESM(require("path")); // 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, location: user.location || "", 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 updateUserLocation(ctx, user, location) { if (!user) return; await ctx.database.set("p_system", { userid: user.userid }, { location }); } __name(updateUserLocation, "updateUserLocation"); 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, location: "", 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", location: "string", 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, options) { const applyBracketFilter = options?.applyBracketFilter ?? true; const applyTagFilter = options?.applyTagFilter ?? true; let working = prompt; if (applyBracketFilter) { const parts = working.split(/([(\[【《(][^))]{0,500}[)\]】》)])/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("…"); working = filtered.replace(/\s+/g, "."); if (!working) { working = prompt; } } if (!applyTagFilter) { return working ? { content: working, error: false } : { content: "返回内容为空,请重置对话", error: true }; } const pTagRegex = /<p>[\s\S]*?<\/p>/g; const doubaoRegex = /<doubaothinking>[\s\S]*?<\/doubaothinking>/g; const answerTagRegex = /<answer>[\s\S]*?<\/answer>/g; const pMatches = working.match(pTagRegex) || []; const doubaoMatches = working.match(doubaoRegex) || []; const answerTagMatches = working.match(answerTagRegex) || []; let combined = null; if (pMatches.length > 0) { combined = pMatches.map((s) => s.replace(/^<p>/i, "").replace(/<\/p>$/i, "")).join(","); } else if (doubaoMatches.length > 0) { combined = doubaoMatches.map((s) => s.replace(/<doubaothinking>/i, "").replace(/<\/doubaothinking>/i, "")).join(","); } else if (answerTagMatches.length > 0) { combined = answerTagMatches.map((s) => s.replace(/<answer>/i, "").replace(/<\/answer>/i, "")).join(","); } if (!combined) { return { content: "内容抽取失败,请重置对话", error: true }; } const cleanupRegex = /<p>|<\/p>|<doubaothinking>|<\/doubaothinking>|<answer>|<\/answer>|<br>|<\/br>|<br\/>/gi; const cleanedContent = combined.replace(cleanupRegex, "").trim(); 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"); function findLongestCommonSubstring(str1, str2) { if (!str1 || !str2) return 0; const m = str1.length; const n = str2.length; const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); let maxLength = 0; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (str1[i - 1] === str2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; maxLength = Math.max(maxLength, dp[i][j]); } } } return maxLength; } __name(findLongestCommonSubstring, "findLongestCommonSubstring"); function countCommonChars(str1, str2) { const chars1 = /* @__PURE__ */ new Map(); const chars2 = /* @__PURE__ */ new Map(); for (const char of str1) { if (/[\u4e00-\u9fff]/.test(char)) { chars1.set(char, (chars1.get(char) || 0) + 1); } } for (const char of str2) { if (/[\u4e00-\u9fff]/.test(char)) { chars2.set(char, (chars2.get(char) || 0) + 1); } } let commonCount = 0; for (const [char, count1] of chars1) { const count2 = chars2.get(char) || 0; commonCount += Math.min(count1, count2); } const countDigits = /* @__PURE__ */ __name((s) => (s.match(/\d/g) || []).length, "countDigits"); const digits1 = countDigits(str1); const digits2 = countDigits(str2); if (digits1 >= 3 || digits2 >= 3) { return Math.max(commonCount, 2); } return commonCount; } __name(countCommonChars, "countCommonChars"); async function wrapInHTML(str, width = 20) { if (!puppeteer) { logger.warn("puppeteer未就绪"); return "出现错误,请联系管理员"; } const html = `<!doctype html> <html> <head> <meta charset="utf-8" /> <style> /* 让页面宽度随内容收缩,这样 puppeteer 渲染产物不会有大片空白 */ html, body { margin: 0; padding: 0; width: auto; height: auto; display: inline-block; /* shrink-to-fit */ background: transparent; } .satori-text { padding: 10px; display: inline-block; box-sizing: border-box; font-family: "Noto Sans SC", "Microsoft YaHei", Arial, sans-serif; font-size: 16px; line-height: 1.4; /* 最大约 20 个字符宽度(可调整为 10-20ch) */ max-width: ${width}ch; /* 同时允许内容根据文字宽度收缩(fit-content 在一些旧浏览器需备份) */ width: -moz-fit-content; width: fit-content; /* 保留输入中的换行符并在需要时换行 */ white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; -webkit-font-smoothing: antialiased; } </style> </head> <body> <div class="satori-text">${str.replaceAll(/\n/g, "<br/>")}</div> </body> </html>`; try { const renderPromise = puppeteer.render(html); const timeoutPromise = new Promise( (_, reject) => setTimeout(() => reject(new Error("Puppeteer 渲染超时")), 3e4) ); return await Promise.race([renderPromise, timeoutPromise]); } catch (error) { logger.error(`Puppeteer 渲染失败: ${error}`); return "渲染失败,请联系管理员"; } } __name(wrapInHTML, "wrapInHTML"); // src/emoji.ts var logger2 = new import_koishi2.Logger("satori-emoji"); var EmojiManager = class { constructor(ctx, config) { this.ctx = ctx; this.config = config; this.emojiDir = path2.resolve(this.config.dataDir, "emojis"); this.loadEmojis(); } static { __name(this, "EmojiManager"); } categories = []; emojiDir; async loadEmojis() { try { if (!fs2.existsSync(this.emojiDir)) { logger2.warn(`表情包目录不存在: ${this.emojiDir}`); return; } const entries = await fs2.promises.readdir(this.emojiDir, { withFileTypes: true }); const folders = entries.filter((entry) => entry.isDirectory()); for (const folder of folders) { const folderPath = path2.join(this.emojiDir, folder.name); const files = await fs2.promises.readdir(folderPath); const imageFiles = files.filter((file) => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)); if (imageFiles.length > 0) { this.categories.push({ name: folder.name, files: imageFiles }); } } logger2.info(`已加载表情包类别: ${this.categories.map((c) => c.name).join(", ")}`); } catch (error) { logger2.error(`加载表情包失败: ${error}`); } } shouldSendEmoji() { if (!this.config.enable_emoji) return false; return Math.random() < this.config.emoji_probability; } async getEmoji(session, user, recentMessages) { if (this.categories.length === 0) return null; const contextMessages = recentMessages.slice(-5).map((msg) => { if (msg.role === "system") return ""; return `${msg.role === "user" ? "用户" : "AI"}: ${msg.content}`; }).filter((s) => s).join("\n"); const categoryNames = this.categories.map((c) => c.name).join("、"); const systemPrompt = `你是一个表情包推荐助手。请根据提供的对话上下文,从给定的表情包类别中为AI选择一个最合适的表情包类别。 表情包类别列表:[${categoryNames}] 如果不适合发送表情包,请返回 "none"。 请只返回类别名称或 "none",不要包含其他内容。`; const userPrompt = `对话上下文: ${contextMessages} 请推荐一个最合适的表情包类别,只输出类别名称。`; const fullPrompt = `${systemPrompt} ${userPrompt}`; const messages = [ { role: "user", content: fullPrompt } ]; logger2.info(`表情包推荐提示词: ${fullPrompt}`); const { baseURL, key, model } = this.getAPIConfig(user); try { const response = await this.callLLM(baseURL, key, model, messages); const categoryName = response.trim(); logger2.info(`模型推荐的表情包类别: ${categoryName}`); if (!categoryName || categoryName === "none") return null; const category = this.categories.find((c) => c.name === categoryName); if (category) { const randomFile = category.files[Math.floor(Math.random() * category.files.length)]; const filePath = path2.join(this.emojiDir, category.name, randomFile); logger2.info(`选择表情包: ${category.name}/${randomFile}`); return filePath; } else { logger2.info(`模型返回了无效的表情包类别: ${categoryName}`); return null; } } catch (error) { logger2.warn(`获取表情包失败: ${error}`); return null; } } getAPIConfig(user) { const enableUserKey = user?.items?.["地灵殿通行证"]?.description === "on"; if (enableUserKey) { const ticket = user.items["地灵殿通行证"].metadata; if (ticket?.not_reasoner_model) { return { baseURL: ticket.baseURL, key: ticket.key, model: ticket.not_reasoner_model }; } return { baseURL: ticket.baseURL, key: ticket.key, model: ticket.model }; } return { baseURL: this.config.not_reasoner_LLM_URL, key: this.config.not_reasoner_LLM_key[0], // 简化处理,使用第一个key model: this.config.not_reasoner_LLM }; } async callLLM(baseURL, key, model, messages) { const url = `${trimSlash(baseURL)}/chat/completions`; const headers = { "Authorization": `Bearer ${key}`, "Content-Type": "application/json" }; const payload = { model, messages, temperature: 0.6, max_tokens: 500 // 增加到500以适应可能的思考过程 }; try { const response = await this.ctx.http.post(url, payload, { headers, timeout: 1e4 }); logger2.info(`LLM响应: ${JSON.stringify(response)}`); if (response.choices && response.choices.length > 0) { const content = response.choices[0].message?.content; return content || ""; } throw new Error("API响应格式错误"); } catch (error) { if (isErrorWithMessage(error)) { const status = error.response?.status || 0; const message = error.response?.data?.error?.message || error.message; logger2.warn(`API Error [${status}]: ${message}`); } throw error; } } }; // src/api.ts var import_koishi3 = require("koishi"); var logger3 = new import_koishi3.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) { const enableUserKey = user?.items?.["地灵殿通行证"]?.description && user.items["地灵殿通行证"].description == "on"; let keys; let modle; let baseURL; let thinkingType = "enabled"; 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) { thinkingType = "disabled"; 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; thinkingType = useNotReasoner ? "disabled" : "enabled"; } const payload = this.createPayload(messages, modle, thinkingType); 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); } logger3.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((resolve4) => setTimeout(resolve4, this.config.retry_delay_time || 5e3)); logger3.warn(`生成画像时API请求失败(${error}),重试(第${i}次)中...`); continue; } } return { content: "unreachable", error: true }; } // 生成请求体 createPayload(messages, model, thinkingType = "enabled") { return { model, messages, temperature: this.config.temperature, top_p: 1, max_tokens: this.config.max_output_tokens, frequency_penalty: this.config.frequency_penalty, presence_penalty: this.config.presence_penalty, thinking: { type: thinkingType } }; } // 生成辅助请求体 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 }); logger3.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) logger3.info(`思维链: ${reasoning_content}`); if (!content && reasoning_content) { logger3.warn("返回内容为空,但存在推理内容"); } if (reasoning_content != "无" && content && content.length > reasoning_content.length) { logger3.warn("返回内容疑似包含推理内容"); } if (response.usage.completion_tokens > this.config.max_output_tokens) { logger3.warn("返回内容过长导致被截断"); continue; } return { content, error: false, reasoning_content }; } catch (error) { if (i == this.config.maxRetryTimes) { return this.handleAPIError(error); } await new Promise((resolve4) => setTimeout(resolve4, this.config.retry_delay_time || 5e3)); logger3.warn(`API请求失败(${error}),重试(第${i}次)中...`); continue; } } return { content: "请求失败,请重试", 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; logger3.error(`API Error [${status}]: ${errorCode} - ${message}`); switch (status) { case 400: return { content: "请求体格式错误:" + message, error: true }; case 401: return { content: "API key 错误,认证失败:" + message, error: true }; case 402: return { content: "账号余额不足:" + message, error: true }; case 422: return { content: "请求体参数错误:" + message, error: true }; case 429: return { content: "请求速率(TPM 或 RPM)达到上限:" + message, error: true }; case 500: return { content: `api服务器内部故障:` + message, error: true }; case 503: return { content: "api服务器负载过高:" + message, error: true }; default: return { content: message, error: true }; } } // 切换API密钥 rotateKey() { this.currentKeyIndex = (this.currentKeyIndex + 1) % this.config.keys.length; logger3.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) }); logger3.info("API connection test succeeded"); return true; } catch (error) { logger3.warn("API connection test failed"); return false; } } }; // src/memory.ts var import_koishi4 = require("koishi"); var fs3 = __toESM(require("fs")); var path3 = __toESM(require("path")); var logger4 = new import_koishi4.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="]; 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[session.channelId] = 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); } if (!response) { response = "…"; } 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.enhanceReasoningProtection) { if (this.config.enable_self_memory) { memory.dialogues.push({ role: "assistant", content: "<p>" + response + "</p>" }); } else { memory.dialogues.push({ role: "assistant", content: "<p>…</p>" }); } } else { if (this.config.enable_self_memory) { memory.dialogues.push({ role: "assistant", content: response }); } else { memory.dialogues.push({ role: "assistant", content: "…" }); } } 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); } await fs3.promises.writeFile(filePath, JSON.stringify(updated, null, 2), "utf-8"); } // 记忆检索 async searchMemories(session, type = "user") { const filePathMap = { "user": this.getUserMemoryPath(session.userId), "common": path3.join(this.config.dataDir, "common_sense.txt"), "group": path3.join(this.config.dataDir, "group_sense", `${session.channelId}.txt`) }; const topNMap = { "user": this.config.dailogues_topN, "common": this.config.common_topN, "group": this.config.common_topN // 群常识和常识使用同样的topN }; const filePath = filePathMap[type]; if (!fs3.existsSync(filePath)) { return ""; } const channelId = this.config.personal_memory ? session.userId : session.channelId; const recentMemories = this.getChannelContext(channelId); const userDialogues = recentMemories.filter((entry) => entry.role === "user").map((entry) => entry.content).join(""); const keywords = userDialogues.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]; await fs3.promises.writeFile(filePath, JSON.stringify(updatedEntries, null, 2), "utf-8"); } 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 regex = new RegExp(`[${this.charactersToRemove.join("")}]`, "g"); content = content.replace(regex, ""); const Keyword = keywords.map((k) => escapeRegExp(k)).join(""); const length = findLongestCommonSubstring(content, Keyword); const count = countCommonChars(content, Keyword); const ratio = (length * length + count) / content.length; return { score: ratio, count }; } // 格式化匹配结果 formatMatches(matched, type, topN = 5) { const prefixMap = { "common": "这是你可能需要知道的信息:", "user": "以下是较久之前用户说过的话和对话时间:", "group": "以下是本群聊你可能需要知道的信息:" }; 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 if (type === "user") { 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 === "group") { 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 path3.join(this.config.dataDir, "dialogues", `${userId}.txt`); } // 确保记忆文件存在 async ensureMemoryFile(filePath) { await fs3.promises.mkdir(path3.dirname(filePath), { recursive: true }); if (!fs3.existsSync(filePath)) { await fs3.promises.writeFile(filePath, "[]", "utf-8"); } } // 加载记忆文件 async loadMemoryFile(filePath) { try { const content = await fs3.promises.readFile(filePath, "utf-8"); return JSON.parse(content); } catch { return []; } } // 获取频道上下文 getChannelContext(channelId) { return this.channelMemories.get(channelId)?.dialogues || []; } }; // src/fixed-dialogues.ts var fs4 = __toESM(require("fs")); var path4 = __toESM(require("path")); async function handleFixedDialogues(ctx, session, user, prompt, config) { if (!config.enable_fixed_dialogues) return null; const filePath = path4.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 (!fs4.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: "再见!希望很快再见到你。" } ]; fs4.writeFileSync(filePath, JSON.stringify(defaultDialogues, null, 2)); } } __name(ensureFixedDialoguesFile, "ensureFixedDialoguesFile"); async function loadFixedDialogues(filePath) { await ensureFixedDialoguesFile(filePath); try { return JSON.parse(fs4.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 fs5 = __toESM(require("fs")); var path5 = __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.favorability >= config.favorability_div_3 && user?.items["镇定贴"]?.count > 0 && user?.items["镇定贴"]?.description && user?.items["镇定贴"]?.description == "on") return "朋友"; 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 = path5.join(config.dataDir, "output_censor.txt"); if (!fs5.existsSync(filePath)) { return 0; } const censorWords = fs5.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 = path5.join(basePath, "output_censor.txt"); fs5.mkdirSync(path5.dirname(filePath), { recursive: true }); if (!fs5.existsSync(filePath)) { fs5.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_koishi5 = require("koishi"); var logger5 = new import_koishi5.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); await sat.broadcastManager.seedBroadcast(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); } if (!isSpecialMessage(session)) await sat.handleChannelMemoryManager(session); 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.inc