From 8751ea9df2b8dbd76731c53616579d1eee861a8a Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Fri, 7 Nov 2025 18:47:26 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=90=91?= =?UTF-8?q?=E9=87=8F=E8=AE=B0=E5=BF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/AI.ts | 4 +- src/AI/context.ts | 33 ++-- src/AI/image.ts | 31 ++++ src/AI/memory.ts | 289 +++++++++++++++++++++--------------- src/config/config_memory.ts | 15 ++ src/index.ts | 51 +++++-- src/service.ts | 54 +++++-- src/tool/tool_memory.ts | 71 +++++++-- src/update.ts | 3 +- src/utils/utils.ts | 38 +++++ src/utils/utils_message.ts | 36 ++++- 11 files changed, 450 insertions(+), 175 deletions(-) diff --git a/src/AI/AI.ts b/src/AI/AI.ts index 454b6c8..8b6b65f 100644 --- a/src/AI/AI.ts +++ b/src/AI/AI.ts @@ -147,7 +147,7 @@ export class AI { const MaxRetry = 3; for (let retry = 1; retry <= MaxRetry; retry++) { // 处理messages - const messages = handleMessages(ctx, this); + const messages = await handleMessages(ctx, this); //获取处理后的回复 const raw_reply = await sendChatRequest(ctx, msg, this, messages, "auto"); @@ -195,7 +195,7 @@ export class AI { await this.stopCurrentChatStream(); - const messages = handleMessages(ctx, this); + const messages = await handleMessages(ctx, this); const id = await startStream(messages); if (id === '') { return; diff --git a/src/AI/context.ts b/src/AI/context.ts index 6b3b368..2f598dd 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -8,11 +8,22 @@ import { logger } from "../logger"; import { transformMsgId } from "../utils/utils"; import { getGroupMemberInfo, getStrangerInfo } from "../utils/utils_ob11"; -export interface UserNameInfo { // 用于上下文名字修改相关操作 - uid: string; +export interface SessionInfo { + sessionId: string; + isPrivate: boolean; + sessionName: string; +} + +export interface UserInfo { // 用于上下文名字修改相关操作 + userId: string; name: string; } +export interface GroupInfo { + groupId: string; + groupName: string; +} + export interface MessageInfo { msgId: string; time: number; // 秒 @@ -386,13 +397,13 @@ export class Context { const memoryList = Object.values(ai.memory.memoryMap); for (const m of memoryList) { - if (m.group.groupName === groupName) { - return m.group.groupId; + if (m.sessionInfo.isPrivate && m.sessionInfo.sessionName === groupName) { + return m.sessionInfo.sessionId; } - if (m.group.groupName.length > 4) { - const distance = levenshteinDistance(groupName, m.group.groupName); + if (m.sessionInfo.isPrivate && m.sessionInfo.sessionName.length > 4) { + const distance = levenshteinDistance(groupName, m.sessionInfo.sessionName); if (distance <= 2) { - return m.group.groupId; + return m.sessionInfo.sessionId; } } } @@ -423,13 +434,13 @@ export class Context { return null; } - getUserNameInfo(): UserNameInfo[] { - const userMap: { [key: string]: UserNameInfo } = {}; + getUserInfo(): UserInfo[] { + const userMap: { [key: string]: UserInfo } = {}; this.messages.forEach(message => { if (message.role === 'user' && message.name && message.uid && !message.name.startsWith('_')) { userMap[message.uid] = { - name: message.name, - uid: message.uid, + userId: message.uid, + name: message.name }; } }); diff --git a/src/AI/image.ts b/src/AI/image.ts index 31a1ab4..330bd58 100644 --- a/src/AI/image.ts +++ b/src/AI/image.ts @@ -320,4 +320,35 @@ export class ImageManager { return { base64: '', format: '' }; } } + + static async extractExistingImages(ai: AI, s: string): Promise { + const images = []; + const match = s.match(/[<<][\|│|]img:.+?(?:[\|│|][>>]|[\|│|>>])/g); + if (match) { + for (let i = 0; i < match.length; i++) { + const id = match[i].match(/[<<][\|│|]img:(.+?)(?:[\|│|][>>]|[\|│|>>])/)[1]; + const image = ai.context.findImage(id, ai); + + if (image) { + if (!image.isUrl) { + if (image.base64) { + image.weight += 1; + } + images.push(image); + } else { + const { base64 } = await ImageManager.imageUrlToBase64(image.file); + if (!base64) { + logger.error(`图片${id}转换为base64失败`); + continue; + } + + image.isUrl = false; + image.base64 = base64; + images.push(image); + } + } + } + } + return images; + } } \ No newline at end of file diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 036f18d..36344df 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -1,69 +1,60 @@ import Handlebars from "handlebars"; import { ConfigManager } from "../config/config"; import { AI, AIManager } from "./AI"; -import { Context } from "./context"; -import { generateId, revive } from "../utils/utils"; +import { Context, GroupInfo, SessionInfo, UserInfo } from "./context"; +import { cosineSimilarity, generateId, hasCommonGroup, hasCommonKeyword, hasCommonUser, revive } from "../utils/utils"; import { logger } from "../logger"; -import { fetchData } from "../service"; +import { fetchData, getEmbedding } from "../service"; import { buildContent, parseBody } from "../utils/utils_message"; import { ToolManager } from "../tool/tool"; import { fmtDate } from "../utils/utils_string"; import { Image, ImageManager } from "./image"; +export interface searchOptions { + topK: number; + userList: UserInfo[]; + groupList: GroupInfo[]; + keywords: string[]; + includeImages: boolean; +} + export class Memory { - static validKeys: (keyof Memory)[] = ['id', 'isPrivate', 'player', 'group', 'createTime', 'lastMentionTime', 'keywords', 'weight', 'content', 'images']; + static validKeys: (keyof Memory)[] = ['id', 'vector', 'text', 'sessionInfo', 'userList', 'groupList', 'createTime', 'lastMentionTime', 'keywords', 'weight', 'images']; id: string; // 记忆ID - isPrivate: boolean; - player: { - userId: string; - name: string; - } - group: { - groupId: string; - groupName: string; - } + vector: number[]; // 记忆向量 + text: string; // 记忆内容 + sessionInfo: SessionInfo; + userList: UserInfo[]; + groupList: GroupInfo[]; createTime: number; // 秒级时间戳 lastMentionTime: number; keywords: string[]; weight: number; // 记忆权重,0-10 - content: string; images: Image[]; constructor() { this.id = ''; - this.isPrivate = true; - this.player = { - userId: '', - name: '' - }; - this.group = { - groupId: '', - groupName: '' + this.vector = []; + this.text = ''; + this.sessionInfo = { + sessionId: '', + isPrivate: false, + sessionName: '', }; + this.userList = []; + this.groupList = []; this.createTime = 0; this.lastMentionTime = 0; this.keywords = []; this.weight = 0; - this.content = ''; this.images = []; } - - calcFgtWeight(now: number) { - const d = 24 * 60 * 60; - // 基础新鲜度衰减(按天计算) - const ageDecay = Math.log10((now - this.createTime) / d + 1); - // 活跃度衰减因子(最近接触按小时衰减) - const activityDecay = Math.max(1, (now - this.lastMentionTime) / 3600); - // 权重转换(0-10 → 1.0-3.0 指数曲线) - const importance = Math.pow(1.1161, this.weight); - return (ageDecay * activityDecay) / importance; - } } export class MemoryManager { static validKeys: (keyof MemoryManager)[] = ['persona', 'memoryMap', 'useShortMemory', 'shortMemoryList']; persona: string; - memoryMap: { [key: string]: Memory }; // key: 记忆ID + memoryMap: { [id: string]: Memory }; useShortMemory: boolean; shortMemoryList: string[]; @@ -77,10 +68,13 @@ export class MemoryManager { reviveMemoryMap() { for (const id in this.memoryMap) { this.memoryMap[id] = revive(Memory, this.memoryMap[id]); + if (!this.memoryMap[id].text) { + delete this.memoryMap[id]; + } } } - async addMemory(ctx: seal.MsgContext, ai: AI, kws: string[], content: string) { + async addMemory(ctx: seal.MsgContext, ai: AI, ul: UserInfo[], gl: GroupInfo[], kws: string[], text: string) { let id = generateId(), a = 0; while (this.memoryMap.hasOwnProperty(id)) { id = generateId(); @@ -93,7 +87,7 @@ export class MemoryManager { for (const id of Object.keys(this.memoryMap)) { const m = this.memoryMap[id]; - if (content === m.content && ((!m.isPrivate && ctx.group.groupId === m.group.groupId) || m.isPrivate)) { + if (text === m.text && m.sessionInfo.sessionId === ai.id && hasCommonUser(ul, m.userList) && hasCommonGroup(gl, m.groupList)) { m.keywords = Array.from(new Set([...m.keywords, ...kws])); logger.info(`记忆已存在,id:${id},合并关键词:${m.keywords.join(',')}`); return; @@ -103,49 +97,33 @@ export class MemoryManager { const now = Math.floor(Date.now() / 1000); const m = new Memory(); m.id = id; - m.isPrivate = ctx.isPrivate; - m.player = { - userId: ctx.player.userId, - name: ctx.player.name - }; - m.group = { - groupId: ctx.group.groupId, - groupName: ctx.group.groupName + m.text = text; + m.sessionInfo = { + sessionId: ai.id, + isPrivate: ctx.isPrivate, + sessionName: ctx.isPrivate ? ctx.player.name : ctx.group.groupName, }; + m.userList = ul; + m.groupList = gl; m.createTime = now; m.lastMentionTime = now; - m.keywords = kws || []; - m.weight = 0; - m.content = content || ''; - - const images = []; - const match = content.match(/[<<][\|│|]img:.+?(?:[\|│|][>>]|[\|│|>>])/g); - if (match) { - for (let i = 0; i < match.length; i++) { - const id = match[i].match(/[<<][\|│|]img:(.+?)(?:[\|│|][>>]|[\|│|>>])/)[1]; - const image = ai.context.findImage(id, ai); - - if (image) { - if (!image.isUrl) { - if (image.base64) { - image.weight += 1; - } - images.push(image); - } else { - const { base64 } = await ImageManager.imageUrlToBase64(image.file); - if (!base64) { - logger.error(`图片${id}转换为base64失败`); - continue; - } - - image.isUrl = false; - image.base64 = base64; - images.push(image); - } - } + m.keywords = kws; + m.weight = 5; + m.images = await ImageManager.extractExistingImages(ai, text); + + const { isMemoryVector, embeddingDimension } = ConfigManager.memory; + if (isMemoryVector) { + const vector = await getEmbedding(text); + if (!vector.length) { + logger.error('向量为空'); + return null; + } + if (vector.length !== embeddingDimension) { + logger.error(`向量维度不匹配。期望: ${embeddingDimension}, 实际: ${vector.length}`); + return null; } + m.vector = vector; } - m.images = images || []; this.memoryMap[id] = m; @@ -171,24 +149,23 @@ export class MemoryManager { } } - clearMemory() { - this.memoryMap = {}; - } - - clearShortMemory() { - this.shortMemoryList = []; - } - limitMemory() { const { memoryLimit } = ConfigManager.memory; const now = Math.floor(Date.now() / 1000); const memoryList = Object.values(this.memoryMap); const forgetIdList = memoryList - .map((item) => { + .map((m) => { + const d = 24 * 60 * 60; + // 基础新鲜度衰减(按天计算) + const ageDecay = Math.log10((now - m.createTime) / d + 1); + // 活跃度衰减因子(最近接触按小时衰减) + const activityDecay = Math.max(1, (now - m.lastMentionTime) / 3600); + // 权重转换(0-10 → 1.0-3.0 指数曲线) + const importance = Math.pow(1.1161, m.weight); return { - id: item.id, - fgtWeight: item.calcFgtWeight(now) + id: m.id, + fgtWeight: (ageDecay * activityDecay) / importance } }) .sort((a, b) => b.fgtWeight - a.fgtWeight) @@ -198,6 +175,10 @@ export class MemoryManager { this.delMemory(forgetIdList); } + clearMemory() { + this.memoryMap = {}; + } + limitShortMemory() { const { shortMemoryLimit } = ConfigManager.memory; if (this.shortMemoryList.length > shortMemoryLimit) { @@ -205,6 +186,10 @@ export class MemoryManager { } } + clearShortMemory() { + this.shortMemoryList = []; + } + async updateShortMemory(ctx: seal.MsgContext, msg: seal.Message, ai: AI) { if (!this.useShortMemory) { return; @@ -322,6 +307,61 @@ export class MemoryManager { } } + // 语义搜索 + async search(query: string, options: searchOptions = { + topK: 10, + userList: [], + groupList: [], + keywords: [], + includeImages: false, + }) { + const { isMemoryVector, embeddingDimension } = ConfigManager.memory; + const filteredMemoryList = Object.values(this.memoryMap) + .filter(item => + (!options.userList.length || hasCommonUser(item.userList, options.userList)) && + (!options.groupList.length || hasCommonGroup(item.groupList, options.groupList)) && + (!options.keywords.length || hasCommonKeyword(item.keywords, options.keywords)) && + (!options.includeImages || item.images.length > 0) + ); + if (!filteredMemoryList.length) { + return []; + } + + if (isMemoryVector && query) { + try { + const queryVector = await getEmbedding(query); + if (!queryVector.length) { + logger.error('查询向量为空'); + return []; + } + for (const m of filteredMemoryList) { + if (m.vector.length !== embeddingDimension) { + logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); + m.vector = await getEmbedding(m.text); + } + } + return filteredMemoryList + .sort((a, b) => { + const aScore = cosineSimilarity(queryVector, a.vector); + const bScore = cosineSimilarity(queryVector, b.vector); + return bScore - aScore; + }) + .slice(0, options.topK); + } catch (e) { + logger.error(`语义搜索失败: ${e.message}`); + } + } + return filteredMemoryList + .map(item => { + const mi: Memory = JSON.parse(JSON.stringify(item)); + if (item.keywords.some(kw => query.includes(kw))) { + mi.weight += 10; //提权 + } + return mi; + }) + .sort((a, b) => b.weight - a.weight); + } + updateSingleMemoryWeight(s: string, role: 'user' | 'assistant') { const increase = role === 'user' ? 1 : 0.1; const decrease = role === 'user' ? 0.1 : 0; @@ -365,7 +405,7 @@ export class MemoryManager { } } - buildMemory(isPrivate: boolean, un: string, uid: string, gn: string, gid: string, lastMsg: string = ''): string { + async buildMemory(sessionInfo: SessionInfo, lastMsg: string): Promise { const { showNumber } = ConfigManager.message; const { memoryShowNumber, memoryShowTemplate, memorySingleShowTemplate } = ConfigManager.memory; const memoryList = Object.values(this.memoryMap); @@ -378,28 +418,29 @@ export class MemoryManager { if (memoryList.length === 0) { memoryContent += '无'; } else { - memoryContent += memoryList - .map(item => { - const mi: Memory = JSON.parse(JSON.stringify(item)); - if (item.keywords.some(kw => lastMsg.includes(kw))) { - mi.weight += 10; - } - return mi; - }) - .sort((a, b) => b.weight - a.weight) - .slice(0, memoryShowNumber) - .map((item, i) => { + const searchResult = await this.search(lastMsg, { + topK: memoryShowNumber, + userList: [], + groupList: [], + keywords: [], + includeImages: false, + }); + + memoryContent += searchResult + .map((m, i) => { const data = { "序号": i + 1, - "记忆ID": item.id, - "记忆时间": fmtDate(item.createTime), - "个人记忆": uid, //有uid代表这是个人记忆 - "私聊": item.isPrivate, + "记忆ID": m.id, + "记忆时间": fmtDate(m.createTime), + "个人记忆": sessionInfo.isPrivate, + "私聊": m.sessionInfo.isPrivate, "展示号码": showNumber, - "群聊名称": item.group.groupName, - "群聊号码": item.group.groupId.replace(/^.+:/, ''), - "关键词": item.keywords.join(';'), - "记忆内容": item.content + "群聊名称": m.sessionInfo.sessionName, + "群聊号码": m.sessionInfo.sessionId, + "相关用户": m.userList.map(u => u.name + (showNumber ? `(${u.userId.replace(/^.+:/, '')})` : '')).join(';'), + "相关群聊": m.groupList.map(g => g.groupName + (showNumber ? `(${g.groupId.replace(/^.+:/, '')})` : '')).join(';'), + "关键词": m.keywords.join(';'), + "记忆内容": m.text } const template = Handlebars.compile(memorySingleShowTemplate[0]); @@ -408,12 +449,12 @@ export class MemoryManager { } const data = { - "私聊": isPrivate, + "私聊": sessionInfo.isPrivate, "展示号码": showNumber, - "用户名称": un, - "用户号码": uid.replace(/^.+:/, ''), - "群聊名称": gn, - "群聊号码": gid.replace(/^.+:/, ''), + "用户名称": sessionInfo.sessionName, + "用户号码": sessionInfo.sessionId.replace(/^.+:/, ''), + "群聊名称": sessionInfo.sessionName, + "群聊号码": sessionInfo.sessionId.replace(/^.+:/, ''), "设定": this.persona, "记忆列表": memoryContent } @@ -422,18 +463,30 @@ export class MemoryManager { return template(data) + '\n'; } - buildMemoryPrompt(ctx: seal.MsgContext, context: Context): string { + async buildMemoryPrompt(ctx: seal.MsgContext, context: Context): Promise { const userMessages = context.messages.filter(msg => msg.role === 'user' && !msg.name.startsWith('_')); const lastMsg = userMessages.length > 0 ? userMessages[userMessages.length - 1].msgArray.map(m => m.content).join('') : ''; const ai = AIManager.getAI(ctx.endPoint.userId); - let s = ai.memory.buildMemory(true, seal.formatTmpl(ctx, "核心:骰子名字"), ctx.endPoint.userId, '', '', lastMsg); + let s = await ai.memory.buildMemory({ + isPrivate: true, + sessionName: seal.formatTmpl(ctx, "核心:骰子名字"), + sessionId: ctx.endPoint.userId + }, lastMsg); if (ctx.isPrivate) { - return this.buildMemory(true, ctx.player.name, ctx.player.userId, '', ''); + return this.buildMemory({ + isPrivate: true, + sessionName: ctx.player.name, + sessionId: ctx.player.userId + }, lastMsg); } else { // 群聊记忆 - s += this.buildMemory(false, '', '', ctx.group.groupName, ctx.group.groupId); + s += await this.buildMemory({ + isPrivate: false, + sessionName: ctx.group.groupName, + sessionId: ctx.group.groupId + }, lastMsg); // 群内用户的个人记忆 const arr = []; @@ -445,7 +498,11 @@ export class MemoryManager { } const ai = AIManager.getAI(uid); - s += ai.memory.buildMemory(true, name, uid, '', ''); + s += ai.memory.buildMemory({ + isPrivate: true, + sessionName: name, + sessionId: uid + }, lastMsg); arr.push(uid); } diff --git a/src/config/config_memory.ts b/src/config/config_memory.ts index 1a39587..c855679 100644 --- a/src/config/config_memory.ts +++ b/src/config/config_memory.ts @@ -9,6 +9,14 @@ export class MemoryConfig { seal.ext.registerBoolConfig(MemoryConfig.ext, "是否启用长期记忆", true, ""); seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆上限", 50, ""); seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆展示数量", 5, ""); + seal.ext.registerBoolConfig(MemoryConfig.ext, "长期记忆是否启用向量", false, ""); + seal.ext.registerIntConfig(MemoryConfig.ext, "向量维度", 1024, ""); + seal.ext.registerStringConfig(MemoryConfig.ext, "嵌入url地址", "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings", ''); + seal.ext.registerStringConfig(MemoryConfig.ext, "嵌入API Key", "你的API Key", ''); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "嵌入body", [ + `"model":"text-embedding-v4"`, + `"encoding_format":"float"` + ], "input, dimensions不存在时,将会自动替换。具体参数请参考你所使用模型的接口文档"); seal.ext.registerTemplateConfig(MemoryConfig.ext, "长期记忆展示模板", [ `{{#if 私聊}} ### 关于用户<{{{用户名称}}}>{{#if 展示号码}}({{{用户号码}}}){{/if}}: @@ -25,6 +33,8 @@ export class MemoryConfig { {{#if 个人记忆}} 来源:{{#if 私聊}}私聊{{else}}群聊<{{{群聊名称}}}>{{#if 展示号码}}({{{群聊号码}}}){{/if}}{{/if}} {{/if}} + 相关用户:{{{用户列表}}} + 相关群聊:{{{群聊列表}}} 关键词:{{{关键词}}} 内容:{{{记忆内容}}}` ], ""); @@ -114,6 +124,11 @@ export class MemoryConfig { isMemory: seal.ext.getBoolConfig(MemoryConfig.ext, "是否启用长期记忆"), memoryLimit: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆上限"), memoryShowNumber: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆展示数量"), + isMemoryVector: seal.ext.getBoolConfig(MemoryConfig.ext, "长期记忆是否启用向量"), + embeddingDimension: seal.ext.getIntConfig(MemoryConfig.ext, "向量维度"), + embeddingUrl: seal.ext.getStringConfig(MemoryConfig.ext, "嵌入url地址"), + embeddingApiKey: seal.ext.getStringConfig(MemoryConfig.ext, "嵌入API Key"), + embeddingBodyTemplate: seal.ext.getTemplateConfig(MemoryConfig.ext, "嵌入body"), memoryShowTemplate: seal.ext.getTemplateConfig(MemoryConfig.ext, "长期记忆展示模板"), memorySingleShowTemplate: seal.ext.getTemplateConfig(MemoryConfig.ext, "单条长期记忆展示模板"), isShortMemory: seal.ext.getBoolConfig(MemoryConfig.ext, "是否启用短期记忆"), diff --git a/src/index.ts b/src/index.ts index f715696..e5faaab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -189,9 +189,10 @@ ${HELPMAP["权限限制"]}`); } } case 'prompt': { - const systemMessage = buildSystemMessage(ctx, ai); - logger.info(`system prompt:\n`, systemMessage.msgArray[0].content); - seal.replyToSender(ctx, msg, systemMessage.msgArray[0].content); + buildSystemMessage(ctx, ai).then(systemMessage => { + logger.info(`system prompt:\n`, systemMessage.msgArray[0].content); + seal.replyToSender(ctx, msg, systemMessage.msgArray[0].content); + }); return ret; } case 'status': { @@ -214,7 +215,7 @@ ${HELPMAP["权限限制"]}`); switch (aliasToCmd(val2)) { case 'status': { seal.replyToSender(ctx, msg, `自动修改上下文里的名字状态:${ai.context.autoNameMod} -上下文里的名字有:\n${ai.context.getUserNameInfo().map(uni => `${uni.name}(${uni.uid})`).join('\n')}`); +上下文里的名字有:\n${ai.context.getUserInfo().map(uni => `${uni.name}(${uni.userId})`).join('\n')}`); return ret; } case 'set': { @@ -225,9 +226,9 @@ ${HELPMAP["权限限制"]}`); 【.ai ctxn set [nick/card]】设置上下文里的名字为昵称/群名片`); return ret; } - const promises = ai.context.getUserNameInfo().map(uni => ai.context.setName(epId, gid, uni.uid, mod)); + const promises = ai.context.getUserInfo().map(uni => ai.context.setName(epId, gid, uni.userId, mod)); Promise.all(promises).then(() => { - seal.replyToSender(ctx, msg, `设置完成,上下文里的名字有:\n${ai.context.getUserNameInfo().map(uni => `${uni.name}(${uni.uid})`).join('\n')}`); + seal.replyToSender(ctx, msg, `设置完成,上下文里的名字有:\n${ai.context.getUserInfo().map(uni => `${uni.name}(${uni.userId})`).join('\n')}`); }); return ret; } @@ -632,14 +633,23 @@ ${HELPMAP["权限限制"]}`); return ret; } ai2.memory.delMemory(idList, kw); - const s = ai2.memory.buildMemory(true, mctx.player.name, mctx.player.userId, '', ''); - seal.replyToSender(ctx, msg, s || '无'); - AIManager.saveAI(muid); + ai2.memory.buildMemory({ + isPrivate: true, + sessionName: mctx.player.name, + sessionId: mctx.player.userId + }, '').then(s => { + seal.replyToSender(ctx, msg, s || '无'); + AIManager.saveAI(muid); + } + ); return ret; } case 'show': { - const s = ai2.memory.buildMemory(true, mctx.player.name, mctx.player.userId, '', ''); - seal.replyToSender(ctx, msg, s || '无'); + ai2.memory.buildMemory({ + isPrivate: true, + sessionName: mctx.player.name, + sessionId: mctx.player.userId + }, '').then(s => seal.replyToSender(ctx, msg, s || '无')); return ret; } case 'clear': { @@ -700,14 +710,23 @@ ${HELPMAP["权限限制"]}`); return ret; } ai.memory.delMemory(idList, kw); - const s = ai.memory.buildMemory(false, '', '', ctx.group.groupName, ctx.group.groupId); - seal.replyToSender(ctx, msg, s || '无'); - AIManager.saveAI(id); + ai.memory.buildMemory({ + isPrivate: false, + sessionName: ctx.group.groupName, + sessionId: ctx.group.groupId + }, '').then(s => { + seal.replyToSender(ctx, msg, s || '无'); + AIManager.saveAI(id); + } + ); return ret; } case 'show': { - const s = ai.memory.buildMemory(false, '', '', ctx.group.groupName, ctx.group.groupId); - seal.replyToSender(ctx, msg, s || '无'); + ai.memory.buildMemory({ + isPrivate: false, + sessionName: ctx.group.groupName, + sessionId: ctx.group.groupId + }, '').then(s => seal.replyToSender(ctx, msg, s || '无')); return ret; } case 'clear': { diff --git a/src/service.ts b/src/service.ts index ca5264d..d62e47e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1,7 +1,7 @@ import { AI, AIManager } from "./AI/AI"; import { ToolCall, ToolManager } from "./tool/tool"; import { ConfigManager } from "./config/config"; -import { handleMessages, parseBody } from "./utils/utils_message"; +import { handleMessages, parseBody, parseEmbeddingBody } from "./utils/utils_message"; import { ImageManager } from "./AI/image"; import { logger } from "./logger"; import { withTimeout } from "./utils/utils"; @@ -51,7 +51,7 @@ export async function sendChatRequest(ctx: seal.MsgContext, msg: seal.Message, a return ''; } - const messages = handleMessages(ctx, ai); + const messages = await handleMessages(ctx, ai); return await sendChatRequest(ctx, msg, ai, messages, tool_choice); } } else { @@ -68,7 +68,7 @@ export async function sendChatRequest(ctx: seal.MsgContext, msg: seal.Message, a return ''; } - const messages = handleMessages(ctx, ai); + const messages = await handleMessages(ctx, ai); return await sendChatRequest(ctx, msg, ai, messages, tool_choice); } } @@ -140,15 +140,49 @@ export async function sendITTRequest(messages: { } } +export async function getEmbedding(text: string): Promise { + if (!text) { + logger.warning(`getEmbedding: 文本为空`); + return []; + } + + const { timeout } = ConfigManager.request; + const { embeddingDimension, embeddingUrl, embeddingApiKey, embeddingBodyTemplate } = ConfigManager.memory; + + try { + const bodyObject = parseEmbeddingBody(embeddingBodyTemplate, text, embeddingDimension); + const time = Date.now(); + + const data = await withTimeout(() => fetchData(embeddingUrl, embeddingApiKey, bodyObject), timeout); + + if (data.data && data.data.length > 0) { + AIManager.updateUsage(data.model, data.usage); + + const embedding = data.data[0].embedding; + + logger.info(`响应embedding长度:`, embedding.length, '\nlatency:', Date.now() - time, 'ms'); + + return embedding; + } else { + throw new Error(`服务器响应中没有data或data为空\n响应体:${JSON.stringify(data, null, 2)}`); + } + } catch (e) { + logger.error("在getEmbedding中出错:", e.message); + return []; + } +} + export async function fetchData(url: string, apiKey: string, bodyObject: any): Promise { // 打印请求发送前的上下文 - const s = JSON.stringify(bodyObject.messages, (key, value) => { - if (key === "" && Array.isArray(value)) { - return value.filter(item => item.role !== "system"); - } - return value; - }); - logger.info(`请求发送前的上下文:\n`, s); + if (bodyObject.hasOwnProperty('messages')) { + const s = JSON.stringify(bodyObject.messages, (key, value) => { + if (key === "" && Array.isArray(value)) { + return value.filter(item => item.role !== "system"); + } + return value; + }); + logger.info(`请求发送前的上下文:\n`, s); + } const response = await fetch(url, { method: 'POST', diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 06607f4..56a4d2c 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -1,4 +1,5 @@ import { AIManager } from "../AI/AI"; +import { GroupInfo, UserInfo } from "../AI/context"; import { ConfigManager } from "../config/config"; import { createMsg, createCtx } from "../utils/utils_seal"; import { Tool } from "./tool"; @@ -19,26 +20,33 @@ export function registerMemory() { }, name: { type: 'string', - description: '用户名称或群聊名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号、群号' : '') + ',实际使用时与记忆类型对应' + description: '目标用户名称或群聊名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号、群号' : '') + ',实际使用时与记忆类型对应' + }, + text: { + type: 'string', + description: '记忆内容,尽量简短,无需附带时间与来源' }, keywords: { type: 'array', - description: '记忆关键词', + description: '相关用户名称列表', items: { type: 'string' } }, - content: { - type: 'string', - description: '记忆内容,尽量简短,无需附带时间与来源' + groupList: { + type: 'array', + description: '相关群聊名称列表', + items: { + type: 'string' + } } }, - required: ['memory_type', 'name', 'keywords', 'content'] + required: ['memory_type', 'name', 'text', 'keywords'] } } }); toolAdd.solve = async (ctx, msg, ai, args) => { - const { memory_type, name, keywords, content } = args; + const { memory_type, name, text, keywords, userList = [], groupList = [] } = args; if (memory_type === "private") { const uid = await ai.context.findUserId(ctx, name, true); @@ -64,8 +72,29 @@ export function registerMemory() { return { content: `未知的记忆类型<${memory_type}>`, images: [] }; } + const uiList: UserInfo[] = []; + for (const n of userList) { + const uid = await ai.context.findUserId(ctx, n, true); + if (uid !== null) { + uiList.push({ + userId: uid, + name: n + }); + } + } + const giList: GroupInfo[] = []; + for (const n of groupList) { + const gid = await ai.context.findGroupId(ctx, n); + if (gid !== null) { + giList.push({ + groupId: gid, + groupName: n + }); + } + } + //记忆相关处理 - await ai.memory.addMemory(ctx, ai, Array.isArray(keywords) ? keywords : [], content); + await ai.memory.addMemory(ctx, ai, uiList, giList, Array.isArray(keywords) ? keywords : [], text); AIManager.saveAI(ai.id); return { content: `添加记忆成功`, images: [] }; @@ -88,9 +117,9 @@ export function registerMemory() { type: 'string', description: '用户名称或群聊名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号、群号' : '') + ',实际使用时与记忆类型对应' }, - index_list: { + id_list: { type: 'array', - description: '记忆序号列表,可为空', + description: '记忆ID列表,可为空', items: { type: 'integer' } @@ -103,12 +132,12 @@ export function registerMemory() { } } }, - required: ['memory_type', 'name', 'index_list', 'keywords'] + required: ['memory_type', 'name', 'id_list', 'keywords'] } } }); toolDel.solve = async (ctx, msg, ai, args) => { - const { memory_type, name, index_list, keywords } = args; + const { memory_type, name, id_list, keywords } = args; if (memory_type === "private") { const uid = await ai.context.findUserId(ctx, name, true); @@ -135,7 +164,7 @@ export function registerMemory() { } //记忆相关处理 - ai.memory.delMemory(index_list, keywords); + ai.memory.delMemory(id_list, keywords); AIManager.saveAI(ai.id); return { content: `删除记忆成功`, images: [] }; @@ -179,7 +208,13 @@ export function registerMemory() { ctx = createCtx(ctx.endPoint.userId, msg); ai = AIManager.getAI(uid); - return { content: ai.memory.buildMemory(true, ctx.player.name, ctx.player.userId, '', ''), images: [] }; + return { + content: await ai.memory.buildMemory({ + isPrivate: true, + sessionName: ctx.player.name, + sessionId: ctx.player.userId + }, ''), images: [] + }; } else if (memory_type === "group") { const gid = await ai.context.findGroupId(ctx, name); if (gid === null) { @@ -193,7 +228,13 @@ export function registerMemory() { ctx = createCtx(ctx.endPoint.userId, msg); ai = AIManager.getAI(gid); - return { content: ai.memory.buildMemory(false, '', '', ctx.group.groupName, ctx.group.groupId), images: [] }; + return { + content: await ai.memory.buildMemory({ + isPrivate: false, + sessionName: ctx.group.groupName, + sessionId: ctx.group.groupId + }, ''), images: [] + }; } else { return { content: `未知的记忆类型<${memory_type}>`, images: [] }; } diff --git a/src/update.ts b/src/update.ts index 56fc109..a89781c 100644 --- a/src/update.ts +++ b/src/update.ts @@ -4,7 +4,8 @@ export const updateInfo = { - 修复获取好友、群聊等列表时的bug - 修复了调用函数时,无需cmdArgs的函数也会报错的问题 - 新增了修改上下文里的名字相关功能 -- 活跃时间添加上一条消息时间提示`, +- 活跃时间添加上一条消息时间提示 +- 新增向量记忆`, "4.11.2": `- 增加修复json解析错误的功能`, "4.11.1": `- 修复了戳戳、权限检查、权限设置、帮助文本等相关问题`, "4.11.0": `- 新增请求超时相关 diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5472d66..ec67a07 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -2,6 +2,7 @@ import { AI } from "../AI/AI"; import { logger } from "../logger"; import { ConfigManager } from "../config/config"; import { transformTextToArray } from "./utils_string"; +import { GroupInfo, UserInfo } from "../AI/context"; export function transformMsgId(msgId: string | number | null): string { if (msgId === null) { @@ -160,4 +161,41 @@ export function aliasToCmd(val: string) { "nick": "nickname" } return aliasMap[val] || val; +} + +// 计算余弦相似度 +export function cosineSimilarity(a: number[], b: number[]): number { + if (a.length !== b.length) { + logger.error(`cosineSimilarity: 向量维度必须相同,a: ${a.length}, b: ${b.length}`); + return 0; + } + + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + if (normA === 0 || normB === 0) return 0; + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +export function hasCommonUser(a: UserInfo[], b: UserInfo[]) { + if (a.length === 0 || b.length === 0) return true; + const aid = new Set(a.map(u => u.userId)); + return b.some(u => aid.has(u.userId)); +} +export function hasCommonGroup(a: GroupInfo[], b: GroupInfo[]) { + if (a.length === 0 || b.length === 0) return true; + const aid = new Set(a.map(g => g.groupId)); + return b.some(g => aid.has(g.groupId)); +} +export function hasCommonKeyword(a: string[], b: string[]) { + if (a.length === 0 || b.length === 0) return true; + const aid = new Set(a); + return b.some(k => aid.has(k)); } \ No newline at end of file diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index 2aba4b9..5dacc9f 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -6,7 +6,7 @@ import { ConfigManager } from "../config/config"; import { ToolInfo } from "../tool/tool"; import { fmtDate } from "./utils_string"; -export function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Message { +export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise { const { roleSettingNames, roleSettingTemplate, systemMessageTemplate, isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; const { isTool, usePromptEngineering } = ConfigManager.tool; const { localImagePaths, receiveImage, condition } = ConfigManager.image; @@ -50,7 +50,7 @@ export function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Message { // 记忆 let memoryPrompt = ''; if (isMemory) { - memoryPrompt = ai.memory.buildMemoryPrompt(ctx, ai.context); + memoryPrompt = await ai.memory.buildMemoryPrompt(ctx, ai.context); } // 短期记忆 @@ -176,10 +176,10 @@ function buildContextMessages(systemMessage: Message, messages: Message[]): Mess return contextMessages; } -export function handleMessages(ctx: seal.MsgContext, ai: AI) { +export async function handleMessages(ctx: seal.MsgContext, ai: AI) { const { isMerge } = ConfigManager.message; - const systemMessage = buildSystemMessage(ctx, ai); + const systemMessage = await buildSystemMessage(ctx, ai); const samplesMessages = buildSamplesMessages(ctx); const contextMessages = buildContextMessages(systemMessage, ai.context.messages); @@ -282,6 +282,34 @@ export function parseBody(template: string[], messages: any[], tools: ToolInfo[] return bodyObject; } +export function parseEmbeddingBody(template: string[], input: string, dimensions: number) { + const bodyObject: any = {}; + + for (let i = 0; i < template.length; i++) { + const s = template[i]; + if (s.trim() === '') { + continue; + } + + try { + const obj = JSON.parse(`{${s}}`); + const key = Object.keys(obj)[0]; + bodyObject[key] = obj[key]; + } catch (err) { + throw new Error(`解析body的【${s}】时出现错误:${err}`); + } + } + + if (!bodyObject.hasOwnProperty('input')) { + bodyObject.input = input; + } + if (!bodyObject.hasOwnProperty('dimensions')) { + bodyObject.dimensions = dimensions; + } + + return bodyObject; +} + export function buildContent(message: Message): string { const { isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; const prefix = (isPrefix && message.name) ? ( From e6546b5060f8fc069474f116a556812c6571e896 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sat, 8 Nov 2025 00:08:42 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E4=BF=AE=E6=94=B9memory=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/AI.ts | 52 +++++++---- src/AI/context.ts | 33 ++----- src/AI/memory.ts | 91 ++++++++++--------- src/index.ts | 59 +++++++------ src/timer.ts | 2 +- src/tool/tool_memory.ts | 189 ++++++++++++++++++++++++++++++++++------ src/utils/utils.ts | 11 ++- 7 files changed, 286 insertions(+), 151 deletions(-) diff --git a/src/AI/AI.ts b/src/AI/AI.ts index 8b6b65f..d950c46 100644 --- a/src/AI/AI.ts +++ b/src/AI/AI.ts @@ -10,6 +10,20 @@ import { logger } from "../logger"; import { checkRepeat, handleReply, MessageSegment, transformTextToArray } from "../utils/utils_string"; import { TimerManager } from "../timer"; +export interface GroupInfo { + isPrivate: false; + id: string; + name: string; +} + +export interface UserInfo { + isPrivate: true; + id: string; + name: string; +} + +export type SessionInfo = GroupInfo | UserInfo; + export class Setting { static validKeys: (keyof Setting)[] = ['priv', 'standby', 'counter', 'timer', 'prob', 'activeTimeInfo']; priv: number; @@ -337,7 +351,7 @@ export class AI { } // 若不在活动时间范围内,返回-1 - getCurSegIndex(): number { + get curSegIndex(): number { const now = new Date(); const cur = now.getHours() * 60 + now.getMinutes(); const { start, end, segs } = this.setting.activeTimeInfo; @@ -378,7 +392,7 @@ export class AI { if (segs !== 0 && (start !== 0 || end !== 0)) { const timers = TimerManager.getTimers(this.id, '', ['activeTime']); if (timers.length === 0) { - const curSegIndex = this.getCurSegIndex(); + const curSegIndex = this.curSegIndex; const nextTimePoint = this.getNextTimePoint(curSegIndex); if (nextTimePoint !== -1) { TimerManager.addActiveTimeTimer(ctx, msg, this, nextTimePoint); @@ -390,17 +404,26 @@ export class AI { } } +export interface UsageInfo { + prompt_tokens: number, + completion_tokens: number +} + export class AIManager { static version = "1.0.1"; static cache: { [key: string]: AI } = {}; - static usageMap: { - [key: string]: { // 模型名 - [key: number]: { // 年月日 - prompt_tokens: number, - completion_tokens: number + static usageMapCache: { [model: string]: { [time: number]: UsageInfo } } = {}; + + static get usageMap(): { [model: string]: { [time: number]: UsageInfo } } { + if (!this.usageMapCache) { + try { + this.usageMapCache = JSON.parse(ConfigManager.ext.storageGet('usageMap') || '{}'); + } catch (error) { + logger.error(`从数据库中获取usageMap失败:`, error); } } - } = {}; + return this.usageMapCache; + } static clearCache() { this.cache = {}; @@ -458,7 +481,7 @@ export class AIManager { } static clearUsageMap() { - this.usageMap = {}; + this.usageMapCache = {}; } static clearExpiredUsage(model: string) { @@ -504,17 +527,8 @@ export class AIManager { } } - static getUsageMap() { - try { - const usage = JSON.parse(ConfigManager.ext.storageGet('usageMap') || '{}'); - this.usageMap = usage; - } catch (error) { - logger.error(`从数据库中获取usageMap失败:`, error); - } - } - static saveUsageMap() { - ConfigManager.ext.storageSet('usageMap', JSON.stringify(this.usageMap)); + ConfigManager.ext.storageSet('usageMap', JSON.stringify(this.usageMapCache)); } static updateUsage(model: string, usage: { diff --git a/src/AI/context.ts b/src/AI/context.ts index 2f598dd..173de43 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -3,27 +3,11 @@ import { ConfigManager } from "../config/config"; import { Image } from "./image"; import { createCtx, createMsg } from "../utils/utils_seal"; import { levenshteinDistance, MessageSegment } from "../utils/utils_string"; -import { AI, AIManager } from "./AI"; +import { AI, AIManager, UserInfo } from "./AI"; import { logger } from "../logger"; import { transformMsgId } from "../utils/utils"; import { getGroupMemberInfo, getStrangerInfo } from "../utils/utils_ob11"; -export interface SessionInfo { - sessionId: string; - isPrivate: boolean; - sessionName: string; -} - -export interface UserInfo { // 用于上下文名字修改相关操作 - userId: string; - name: string; -} - -export interface GroupInfo { - groupId: string; - groupName: string; -} - export interface MessageInfo { msgId: string; time: number; // 秒 @@ -397,13 +381,13 @@ export class Context { const memoryList = Object.values(ai.memory.memoryMap); for (const m of memoryList) { - if (m.sessionInfo.isPrivate && m.sessionInfo.sessionName === groupName) { - return m.sessionInfo.sessionId; + if (m.sessionInfo.isPrivate && m.sessionInfo.name === groupName) { + return m.sessionInfo.id; } - if (m.sessionInfo.isPrivate && m.sessionInfo.sessionName.length > 4) { - const distance = levenshteinDistance(groupName, m.sessionInfo.sessionName); + if (m.sessionInfo.isPrivate && m.sessionInfo.name.length > 4) { + const distance = levenshteinDistance(groupName, m.sessionInfo.name); if (distance <= 2) { - return m.sessionInfo.sessionId; + return m.sessionInfo.id; } } } @@ -434,12 +418,13 @@ export class Context { return null; } - getUserInfo(): UserInfo[] { + get userInfoList(): UserInfo[] { const userMap: { [key: string]: UserInfo } = {}; this.messages.forEach(message => { if (message.role === 'user' && message.name && message.uid && !message.name.startsWith('_')) { userMap[message.uid] = { - userId: message.uid, + isPrivate: true, + id: message.uid, name: message.name }; } diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 36344df..bd575b7 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -1,7 +1,7 @@ import Handlebars from "handlebars"; import { ConfigManager } from "../config/config"; -import { AI, AIManager } from "./AI"; -import { Context, GroupInfo, SessionInfo, UserInfo } from "./context"; +import { AI, AIManager, GroupInfo, SessionInfo, UserInfo } from "./AI"; +import { Context } from "./context"; import { cosineSimilarity, generateId, hasCommonGroup, hasCommonKeyword, hasCommonUser, revive } from "../utils/utils"; import { logger } from "../logger"; import { fetchData, getEmbedding } from "../service"; @@ -37,9 +37,9 @@ export class Memory { this.vector = []; this.text = ''; this.sessionInfo = { - sessionId: '', + id: '', isPrivate: false, - sessionName: '', + name: '', }; this.userList = []; this.groupList = []; @@ -87,7 +87,7 @@ export class MemoryManager { for (const id of Object.keys(this.memoryMap)) { const m = this.memoryMap[id]; - if (text === m.text && m.sessionInfo.sessionId === ai.id && hasCommonUser(ul, m.userList) && hasCommonGroup(gl, m.groupList)) { + if (text === m.text && m.sessionInfo.id === ai.id && hasCommonUser(ul, m.userList) && hasCommonGroup(gl, m.groupList)) { m.keywords = Array.from(new Set([...m.keywords, ...kws])); logger.info(`记忆已存在,id:${id},合并关键词:${m.keywords.join(',')}`); return; @@ -99,9 +99,9 @@ export class MemoryManager { m.id = id; m.text = text; m.sessionInfo = { - sessionId: ai.id, + id: ai.id, isPrivate: ctx.isPrivate, - sessionName: ctx.isPrivate ? ctx.player.name : ctx.group.groupName, + name: ctx.isPrivate ? ctx.player.name : ctx.group.groupName, }; m.userList = ul; m.groupList = gl; @@ -307,7 +307,6 @@ export class MemoryManager { } } - // 语义搜索 async search(query: string, options: searchOptions = { topK: 10, userList: [], @@ -405,28 +404,26 @@ export class MemoryManager { } } - async buildMemory(sessionInfo: SessionInfo, lastMsg: string): Promise { - const { showNumber } = ConfigManager.message; - const { memoryShowNumber, memoryShowTemplate, memorySingleShowTemplate } = ConfigManager.memory; - const memoryList = Object.values(this.memoryMap); + async getTopMemoryList(lastMsg: string) { + const { memoryShowNumber } = ConfigManager.memory; + return await this.search(lastMsg, { + topK: memoryShowNumber, + userList: [], + groupList: [], + keywords: [], + includeImages: false, + }); + } - if (memoryList.length === 0 && this.persona === '无') { - return ''; - } + buildMemory(sessionInfo: SessionInfo, memoryList: Memory[]): string { + const { showNumber } = ConfigManager.message; + const { memoryShowTemplate, memorySingleShowTemplate } = ConfigManager.memory; let memoryContent = ''; if (memoryList.length === 0) { - memoryContent += '无'; + memoryContent = '无'; } else { - const searchResult = await this.search(lastMsg, { - topK: memoryShowNumber, - userList: [], - groupList: [], - keywords: [], - includeImages: false, - }); - - memoryContent += searchResult + memoryContent = memoryList .map((m, i) => { const data = { "序号": i + 1, @@ -435,10 +432,10 @@ export class MemoryManager { "个人记忆": sessionInfo.isPrivate, "私聊": m.sessionInfo.isPrivate, "展示号码": showNumber, - "群聊名称": m.sessionInfo.sessionName, - "群聊号码": m.sessionInfo.sessionId, - "相关用户": m.userList.map(u => u.name + (showNumber ? `(${u.userId.replace(/^.+:/, '')})` : '')).join(';'), - "相关群聊": m.groupList.map(g => g.groupName + (showNumber ? `(${g.groupId.replace(/^.+:/, '')})` : '')).join(';'), + "群聊名称": m.sessionInfo.name, + "群聊号码": m.sessionInfo.id, + "相关用户": m.userList.map(u => u.name + (showNumber ? `(${u.id.replace(/^.+:/, '')})` : '')).join(';'), + "相关群聊": m.groupList.map(g => g.name + (showNumber ? `(${g.id.replace(/^.+:/, '')})` : '')).join(';'), "关键词": m.keywords.join(';'), "记忆内容": m.text } @@ -451,10 +448,10 @@ export class MemoryManager { const data = { "私聊": sessionInfo.isPrivate, "展示号码": showNumber, - "用户名称": sessionInfo.sessionName, - "用户号码": sessionInfo.sessionId.replace(/^.+:/, ''), - "群聊名称": sessionInfo.sessionName, - "群聊号码": sessionInfo.sessionId.replace(/^.+:/, ''), + "用户名称": sessionInfo.name, + "用户号码": sessionInfo.id.replace(/^.+:/, ''), + "群聊名称": sessionInfo.name, + "群聊号码": sessionInfo.id.replace(/^.+:/, ''), "设定": this.persona, "记忆列表": memoryContent } @@ -468,25 +465,25 @@ export class MemoryManager { const lastMsg = userMessages.length > 0 ? userMessages[userMessages.length - 1].msgArray.map(m => m.content).join('') : ''; const ai = AIManager.getAI(ctx.endPoint.userId); - let s = await ai.memory.buildMemory({ + let s = ai.memory.buildMemory({ isPrivate: true, - sessionName: seal.formatTmpl(ctx, "核心:骰子名字"), - sessionId: ctx.endPoint.userId - }, lastMsg); + id: ctx.endPoint.userId, + name: seal.formatTmpl(ctx, "核心:骰子名字") + }, await ai.memory.getTopMemoryList(lastMsg)); if (ctx.isPrivate) { return this.buildMemory({ isPrivate: true, - sessionName: ctx.player.name, - sessionId: ctx.player.userId - }, lastMsg); + id: ctx.player.userId, + name: ctx.player.name + }, await ai.memory.getTopMemoryList(lastMsg)); } else { // 群聊记忆 - s += await this.buildMemory({ + s += this.buildMemory({ isPrivate: false, - sessionName: ctx.group.groupName, - sessionId: ctx.group.groupId - }, lastMsg); + id: ctx.group.groupId, + name: ctx.group.groupName + }, await ai.memory.getTopMemoryList(lastMsg)); // 群内用户的个人记忆 const arr = []; @@ -500,9 +497,9 @@ export class MemoryManager { const ai = AIManager.getAI(uid); s += ai.memory.buildMemory({ isPrivate: true, - sessionName: name, - sessionId: uid - }, lastMsg); + id: uid, + name: name + }, await ai.memory.getTopMemoryList(lastMsg)); arr.push(uid); } diff --git a/src/index.ts b/src/index.ts index e5faaab..195baab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,6 @@ import { aliasToCmd } from "./utils/utils"; function main() { ConfigManager.registerConfig(); checkUpdate(); - AIManager.getUsageMap(); ToolManager.registerTool(); TimerManager.init(); PrivilegeManager.reviveCmdPriv(); @@ -215,7 +214,7 @@ ${HELPMAP["权限限制"]}`); switch (aliasToCmd(val2)) { case 'status': { seal.replyToSender(ctx, msg, `自动修改上下文里的名字状态:${ai.context.autoNameMod} -上下文里的名字有:\n${ai.context.getUserInfo().map(uni => `${uni.name}(${uni.userId})`).join('\n')}`); +上下文里的名字有:\n${ai.context.userInfoList.map(ui => `${ui.name}(${ui.id})`).join('\n')}`); return ret; } case 'set': { @@ -226,9 +225,9 @@ ${HELPMAP["权限限制"]}`); 【.ai ctxn set [nick/card]】设置上下文里的名字为昵称/群名片`); return ret; } - const promises = ai.context.getUserInfo().map(uni => ai.context.setName(epId, gid, uni.userId, mod)); + const promises = ai.context.userInfoList.map(ui => ai.context.setName(epId, gid, ui.id, mod)); Promise.all(promises).then(() => { - seal.replyToSender(ctx, msg, `设置完成,上下文里的名字有:\n${ai.context.getUserInfo().map(uni => `${uni.name}(${uni.userId})`).join('\n')}`); + seal.replyToSender(ctx, msg, `设置完成,上下文里的名字有:\n${ai.context.userInfoList.map(uni => `${uni.name}(${uni.id})`).join('\n')}`); }); return ret; } @@ -404,7 +403,7 @@ ${HELPMAP["权限限制"]}`); text += `\n活跃时间段:${Math.floor(start / 60).toString().padStart(2, '0')}:${(start % 60).toString().padStart(2, '0')}至${Math.floor(end / 60).toString().padStart(2, '0')}:${(end % 60).toString().padStart(2, '0')}`; text += `\n活跃次数:${segs}`; - const curSegIndex = ai.getCurSegIndex(); + const curSegIndex = ai.curSegIndex; const nextTimePoint = ai.getNextTimePoint(curSegIndex); if (nextTimePoint !== -1) { TimerManager.addActiveTimeTimer(ctx, msg, ai, nextTimePoint); @@ -633,11 +632,12 @@ ${HELPMAP["权限限制"]}`); return ret; } ai2.memory.delMemory(idList, kw); - ai2.memory.buildMemory({ - isPrivate: true, - sessionName: mctx.player.name, - sessionId: mctx.player.userId - }, '').then(s => { + ai2.memory.getTopMemoryList('').then(memoryList => { + const s = ai2.memory.buildMemory({ + isPrivate: true, + id: mctx.player.userId, + name: mctx.player.name + }, memoryList); seal.replyToSender(ctx, msg, s || '无'); AIManager.saveAI(muid); } @@ -645,11 +645,15 @@ ${HELPMAP["权限限制"]}`); return ret; } case 'show': { - ai2.memory.buildMemory({ - isPrivate: true, - sessionName: mctx.player.name, - sessionId: mctx.player.userId - }, '').then(s => seal.replyToSender(ctx, msg, s || '无')); + ai2.memory.getTopMemoryList('').then(memoryList => { + const s = ai2.memory.buildMemory({ + isPrivate: true, + id: mctx.player.userId, + name: mctx.player.name + }, memoryList); + seal.replyToSender(ctx, msg, s || '无'); + } + ); return ret; } case 'clear': { @@ -710,11 +714,12 @@ ${HELPMAP["权限限制"]}`); return ret; } ai.memory.delMemory(idList, kw); - ai.memory.buildMemory({ - isPrivate: false, - sessionName: ctx.group.groupName, - sessionId: ctx.group.groupId - }, '').then(s => { + ai.memory.getTopMemoryList('').then(memoryList => { + const s = ai.memory.buildMemory({ + isPrivate: false, + id: ctx.group.groupId, + name: ctx.group.groupName + }, memoryList); seal.replyToSender(ctx, msg, s || '无'); AIManager.saveAI(id); } @@ -722,11 +727,15 @@ ${HELPMAP["权限限制"]}`); return ret; } case 'show': { - ai.memory.buildMemory({ - isPrivate: false, - sessionName: ctx.group.groupName, - sessionId: ctx.group.groupId - }, '').then(s => seal.replyToSender(ctx, msg, s || '无')); + ai.memory.getTopMemoryList('').then(memoryList => { + const s = ai.memory.buildMemory({ + isPrivate: false, + id: ctx.group.groupId, + name: ctx.group.groupName + }, memoryList); + seal.replyToSender(ctx, msg, s || '无'); + } + ); return ret; } case 'clear': { diff --git a/src/timer.ts b/src/timer.ts index a3a7e30..293e890 100644 --- a/src/timer.ts +++ b/src/timer.ts @@ -264,7 +264,7 @@ export class TimerManager { const ctx = createCtx(epId, msg); const ai = AIManager.getAI(id); - const curSegIndex = ai.getCurSegIndex(); + const curSegIndex = ai.curSegIndex; const nextTimePoint = ai.getNextTimePoint(curSegIndex); if (curSegIndex === -1) { logger.error(`${id} 不在活跃时间内,触发了 activeTime 定时器,真奇怪\ncurSegIndex:${curSegIndex},setTime:${set},nextTimePoint:${fmtDate(nextTimePoint)}`); diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 56a4d2c..43bb408 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -1,8 +1,8 @@ -import { AIManager } from "../AI/AI"; -import { GroupInfo, UserInfo } from "../AI/context"; +import { AIManager, GroupInfo, SessionInfo, UserInfo } from "../AI/AI"; import { ConfigManager } from "../config/config"; import { createMsg, createCtx } from "../utils/utils_seal"; import { Tool } from "./tool"; +import { searchOptions as SearchOptions } from "../AI/memory"; export function registerMemory() { const toolAdd = new Tool({ @@ -33,6 +33,13 @@ export function registerMemory() { type: 'string' } }, + userList: { + type: 'array', + description: '相关用户名称列表', + items: { + type: 'string' + } + }, groupList: { type: 'array', description: '相关群聊名称列表', @@ -77,7 +84,8 @@ export function registerMemory() { const uid = await ai.context.findUserId(ctx, n, true); if (uid !== null) { uiList.push({ - userId: uid, + isPrivate: true, + id: uid, name: n }); } @@ -87,8 +95,9 @@ export function registerMemory() { const gid = await ai.context.findGroupId(ctx, n); if (gid !== null) { giList.push({ - groupId: gid, - groupName: n + isPrivate: false, + id: gid, + name: n }); } } @@ -170,11 +179,11 @@ export function registerMemory() { return { content: `删除记忆成功`, images: [] }; } - const toolShow = new Tool({ + const toolSearch = new Tool({ type: 'function', function: { - name: 'show_memory', - description: '查看个人记忆或群聊记忆', + name: 'search_memory', + description: '搜索个人记忆或群聊记忆', parameters: { type: 'object', properties: { @@ -186,57 +195,179 @@ export function registerMemory() { name: { type: 'string', description: '用户名称或群聊名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号、群号' : '') + ',实际使用时与记忆类型对应' + }, + query: { + type: 'string', + description: '搜索查询,为空时返回权重靠前的记忆' + }, + topK: { + type: 'number', + description: '返回记忆条数,默认5条' + }, + keywords: { + type: 'array', + description: '相关用户名称列表', + items: { + type: 'string' + } + }, + userList: { + type: 'array', + description: '相关用户名称列表', + items: { + type: 'string' + } + }, + groupList: { + type: 'array', + description: '相关群聊名称列表', + items: { + type: 'string' + } + }, + includeImages: { + type: 'boolean', + description: '是否包含图片' } }, required: ['memory_type', 'name'] } } }); - toolShow.solve = async (ctx, msg, ai, args) => { - const { memory_type, name } = args; + toolSearch.solve = async (ctx, msg, ai, args) => { + const { memory_type, name, query = '', topK = 5, keywords = [], userList = [], groupList = [], includeImages = false } = args; + let si: SessionInfo = { + isPrivate: false, + id: '', + name: '' + }; if (memory_type === "private") { const uid = await ai.context.findUserId(ctx, name, true); if (uid === null) { return { content: `未找到<${name}>`, images: [] }; } - if (uid === ctx.player.userId) { - return { content: `查看该用户记忆无需调用函数`, images: [] }; - } msg = createMsg('private', uid, ''); ctx = createCtx(ctx.endPoint.userId, msg); ai = AIManager.getAI(uid); - return { - content: await ai.memory.buildMemory({ - isPrivate: true, - sessionName: ctx.player.name, - sessionId: ctx.player.userId - }, ''), images: [] - }; + si = { + isPrivate: true, + id: uid, + name: name + } } else if (memory_type === "group") { const gid = await ai.context.findGroupId(ctx, name); if (gid === null) { return { content: `未找到<${name}>`, images: [] }; } - if (gid === ctx.group.groupId) { - return { content: `查看当前群聊记忆无需调用函数`, images: [] }; - } msg = createMsg('group', ctx.player.userId, gid); ctx = createCtx(ctx.endPoint.userId, msg); ai = AIManager.getAI(gid); - return { - content: await ai.memory.buildMemory({ + si = { + isPrivate: false, + id: gid, + name: name + } + } else { + return { content: `未知的记忆类型<${memory_type}>`, images: [] }; + } + + const uiList: UserInfo[] = []; + for (const n of userList) { + const uid = await ai.context.findUserId(ctx, n, true); + if (uid !== null) { + uiList.push({ + isPrivate: true, + id: uid, + name: n + }); + } + } + const giList: GroupInfo[] = []; + for (const n of groupList) { + const gid = await ai.context.findGroupId(ctx, n); + if (gid !== null) { + giList.push({ isPrivate: false, - sessionName: ctx.group.groupName, - sessionId: ctx.group.groupId - }, ''), images: [] - }; + id: gid, + name: n + }); + } + } + + const options: SearchOptions = { + topK: topK, + keywords: keywords, + userList: userList, + groupList: groupList, + includeImages: includeImages + } + + const memoryList = await ai.memory.search(query, options); + const images = Array.from(new Set([].concat(...memoryList.map(m => m.images)))); + + return { + content: ai.memory.buildMemory(si, memoryList), + images: images + }; + } + + const toolClear = new Tool({ + type: 'function', + function: { + name: 'clear_memory', + description: '清除个人记忆或群聊记忆', + parameters: { + type: 'object', + properties: { + memory_type: { + type: "string", + description: "记忆类型,个人或群聊", + enum: ["private", "group"] + }, + name: { + type: 'string', + description: '用户名称或群聊名称' + (ConfigManager.message.showNumber ? '或纯数字QQ号、群号' : '') + ',实际使用时与记忆类型对应' + } + }, + required: ['memory_type', 'name'] + } + } + }); + toolClear.solve = async (ctx, msg, ai, args) => { + const { memory_type, name } = args; + + if (memory_type === "private") { + const uid = await ai.context.findUserId(ctx, name, true); + if (uid === null) { + return { content: `未找到<${name}>`, images: [] }; + } + + msg = createMsg('private', uid, ''); + ctx = createCtx(ctx.endPoint.userId, msg); + + ai = AIManager.getAI(uid); + } else if (memory_type === "group") { + const gid = await ai.context.findGroupId(ctx, name); + if (gid === null) { + return { content: `未找到<${name}>`, images: [] }; + } + + msg = createMsg('group', ctx.player.userId, gid); + ctx = createCtx(ctx.endPoint.userId, msg); + + ai = AIManager.getAI(gid); } else { return { content: `未知的记忆类型<${memory_type}>`, images: [] }; } + + + ai.memory.clearMemory(); + AIManager.saveAI(ai.id); + return { content: `清除记忆成功`, images: [] }; } } \ No newline at end of file diff --git a/src/utils/utils.ts b/src/utils/utils.ts index ec67a07..5d931e5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,8 +1,7 @@ -import { AI } from "../AI/AI"; +import { AI, GroupInfo, UserInfo } from "../AI/AI"; import { logger } from "../logger"; import { ConfigManager } from "../config/config"; import { transformTextToArray } from "./utils_string"; -import { GroupInfo, UserInfo } from "../AI/context"; export function transformMsgId(msgId: string | number | null): string { if (msgId === null) { @@ -186,13 +185,13 @@ export function cosineSimilarity(a: number[], b: number[]): number { export function hasCommonUser(a: UserInfo[], b: UserInfo[]) { if (a.length === 0 || b.length === 0) return true; - const aid = new Set(a.map(u => u.userId)); - return b.some(u => aid.has(u.userId)); + const aid = new Set(a.map(u => u.id)); + return b.some(u => aid.has(u.id)); } export function hasCommonGroup(a: GroupInfo[], b: GroupInfo[]) { if (a.length === 0 || b.length === 0) return true; - const aid = new Set(a.map(g => g.groupId)); - return b.some(g => aid.has(g.groupId)); + const aid = new Set(a.map(g => g.id)); + return b.some(g => aid.has(g.id)); } export function hasCommonKeyword(a: string[], b: string[]) { if (a.length === 0 || b.length === 0) return true; From 29055547a2a2ac99ddc8772c79a2329d10a3e0a7 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sat, 8 Nov 2025 12:58:04 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=88=86=E6=95=B0?= =?UTF-8?q?=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/memory.ts | 92 +++++++++++++++++++++++++++++++--------------- src/utils/utils.ts | 18 ++++----- 2 files changed, 71 insertions(+), 39 deletions(-) diff --git a/src/AI/memory.ts b/src/AI/memory.ts index bd575b7..6981da3 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -2,7 +2,7 @@ import Handlebars from "handlebars"; import { ConfigManager } from "../config/config"; import { AI, AIManager, GroupInfo, SessionInfo, UserInfo } from "./AI"; import { Context } from "./context"; -import { cosineSimilarity, generateId, hasCommonGroup, hasCommonKeyword, hasCommonUser, revive } from "../utils/utils"; +import { cosineSimilarity, generateId, getCommonGroup, getCommonKeyword, getCommonUser, revive } from "../utils/utils"; import { logger } from "../logger"; import { fetchData, getEmbedding } from "../service"; import { buildContent, parseBody } from "../utils/utils_message"; @@ -49,6 +49,56 @@ export class Memory { this.weight = 0; this.images = []; } + + /** + * 计算记忆的基础相似度分数 + * @returns 基础相似度分数(0.5-2.0) + */ + calculateBaseScore() { + // 权重转换(weight: 0-10 → baseScore: 0.5-2.0) + return 0.5 + (this.weight / 0.15); + } + + /** + * 计算记忆的新鲜度衰减因子 + * @returns 衰减因子(1-∞) + */ + calculateDecay() { + const now = Math.floor(Date.now() / 1000); + const w = 7 * 24 * 60 * 60; + // 基础新鲜度衰减: 1 + ln(1 + ageInWeeks) + const ageDecay = Math.log(1 + (now - this.createTime) / w); + // 活跃度衰减因子: 1 + ln(1 + 4hours) + const activityDecay = Math.log(1 + (now - this.lastMentionTime) / 14400); + // 衰减因子 + return Math.max(1, ageDecay * activityDecay); + } + + /** + * 计算记忆与查询的相似度分数 + * @param v 查询向量 + * @param ul 查询用户列表 + * @param gl 查询群组列表 + * @param kws 查询关键词列表 + * @returns 相似度分数(0.7-1.3) + */ + calculateSimiliarity(v: number[], ul: UserInfo[], gl: GroupInfo[], kws: string[]): number { + // 向量相似度分数(如果提供了向量v) + const vectorSimilarity = (v && v.length > 0 && this.vector && this.vector.length > 0) ? (cosineSimilarity(v, this.vector) + 1) / 2 : 0; + // 用户相似度分数 + const commonUser = getCommonUser(this.userList, ul); + const userSimilarity = (ul && ul.length > 0) ? commonUser.length / (this.userList.length + ul.length - commonUser.length) : 0; + // 群组相似度分数 + const commonGroup = getCommonGroup(this.groupList, gl); + const groupSimilarity = (gl && gl.length > 0) ? commonGroup.length / (this.groupList.length + gl.length - commonGroup.length) : 0; + // 关键词匹配分数 + const commonKeyword = getCommonKeyword(this.keywords, kws); + const keywordSimilarity = (kws && kws.length > 0) ? commonKeyword.length / kws.length : 0; + // 综合相似度分数 + const avgSimilarity = vectorSimilarity * 0.4 + userSimilarity * 0.25 + groupSimilarity * 0.25 + keywordSimilarity * 0.1; + // 相似度增强因子 + return avgSimilarity ? 0.7 + avgSimilarity * 0.6 : 1; + } } export class MemoryManager { @@ -87,7 +137,7 @@ export class MemoryManager { for (const id of Object.keys(this.memoryMap)) { const m = this.memoryMap[id]; - if (text === m.text && m.sessionInfo.id === ai.id && hasCommonUser(ul, m.userList) && hasCommonGroup(gl, m.groupList)) { + if (text === m.text && m.sessionInfo.id === ai.id && getCommonUser(ul, m.userList).length > 0 && getCommonGroup(gl, m.groupList).length > 0) { m.keywords = Array.from(new Set([...m.keywords, ...kws])); logger.info(`记忆已存在,id:${id},合并关键词:${m.keywords.join(',')}`); return; @@ -125,9 +175,8 @@ export class MemoryManager { m.vector = vector; } - this.memoryMap[id] = m; - this.limitMemory(); + this.memoryMap[id] = m; } delMemory(idList: string[] = [], kws: string[] = []) { @@ -151,25 +200,17 @@ export class MemoryManager { limitMemory() { const { memoryLimit } = ConfigManager.memory; - const now = Math.floor(Date.now() / 1000); const memoryList = Object.values(this.memoryMap); const forgetIdList = memoryList .map((m) => { - const d = 24 * 60 * 60; - // 基础新鲜度衰减(按天计算) - const ageDecay = Math.log10((now - m.createTime) / d + 1); - // 活跃度衰减因子(最近接触按小时衰减) - const activityDecay = Math.max(1, (now - m.lastMentionTime) / 3600); - // 权重转换(0-10 → 1.0-3.0 指数曲线) - const importance = Math.pow(1.1161, m.weight); return { id: m.id, - fgtWeight: (ageDecay * activityDecay) / importance + fgtWeight: m.calculateDecay() / m.calculateBaseScore() } }) .sort((a, b) => b.fgtWeight - a.fgtWeight) - .slice(0, memoryList.length - memoryLimit) + .slice(0, memoryList.length - memoryLimit + 1) // 预留1个位置用于存储最新记忆 .map(item => item.id); this.delMemory(forgetIdList); @@ -315,17 +356,8 @@ export class MemoryManager { includeImages: false, }) { const { isMemoryVector, embeddingDimension } = ConfigManager.memory; - const filteredMemoryList = Object.values(this.memoryMap) - .filter(item => - (!options.userList.length || hasCommonUser(item.userList, options.userList)) && - (!options.groupList.length || hasCommonGroup(item.groupList, options.groupList)) && - (!options.keywords.length || hasCommonKeyword(item.keywords, options.keywords)) && - (!options.includeImages || item.images.length > 0) - ); - if (!filteredMemoryList.length) { - return []; - } - + const memoryList = Object.values(this.memoryMap); + if (!memoryList.length) return []; if (isMemoryVector && query) { try { const queryVector = await getEmbedding(query); @@ -333,16 +365,16 @@ export class MemoryManager { logger.error('查询向量为空'); return []; } - for (const m of filteredMemoryList) { + for (const m of memoryList) { if (m.vector.length !== embeddingDimension) { logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); m.vector = await getEmbedding(m.text); } } - return filteredMemoryList + return memoryList .sort((a, b) => { - const aScore = cosineSimilarity(queryVector, a.vector); - const bScore = cosineSimilarity(queryVector, b.vector); + const bScore = b.calculateBaseScore() * b.calculateSimiliarity(queryVector, options.userList, options.groupList, options.keywords); + const aScore = a.calculateBaseScore() * a.calculateSimiliarity(queryVector, options.userList, options.groupList, options.keywords); return bScore - aScore; }) .slice(0, options.topK); @@ -350,7 +382,7 @@ export class MemoryManager { logger.error(`语义搜索失败: ${e.message}`); } } - return filteredMemoryList + return memoryList .map(item => { const mi: Memory = JSON.parse(JSON.stringify(item)); if (item.keywords.some(kw => query.includes(kw))) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5d931e5..6555a5f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -183,18 +183,18 @@ export function cosineSimilarity(a: number[], b: number[]): number { return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } -export function hasCommonUser(a: UserInfo[], b: UserInfo[]) { - if (a.length === 0 || b.length === 0) return true; +export function getCommonUser(a: UserInfo[], b: UserInfo[]): UserInfo[] { + if (a.length === 0 || b.length === 0) return []; const aid = new Set(a.map(u => u.id)); - return b.some(u => aid.has(u.id)); + return b.filter(u => aid.has(u.id)); } -export function hasCommonGroup(a: GroupInfo[], b: GroupInfo[]) { - if (a.length === 0 || b.length === 0) return true; +export function getCommonGroup(a: GroupInfo[], b: GroupInfo[]): GroupInfo[] { + if (a.length === 0 || b.length === 0) return []; const aid = new Set(a.map(g => g.id)); - return b.some(g => aid.has(g.id)); + return b.filter(g => aid.has(g.id)); } -export function hasCommonKeyword(a: string[], b: string[]) { - if (a.length === 0 || b.length === 0) return true; +export function getCommonKeyword(a: string[], b: string[]): string[] { + if (a.length === 0 || b.length === 0) return []; const aid = new Set(a); - return b.some(k => aid.has(k)); + return b.filter(k => aid.has(k)); } \ No newline at end of file From 2747cdaca522e4fe5438fccc2b1881446c5e8ae8 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sat, 8 Nov 2025 13:55:13 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=A7=A3=E6=9E=90=20#56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/context.ts | 9 +--- src/AI/image.ts | 40 +++++------------ src/tool/tool_memory.ts | 4 +- src/utils/utils_message.ts | 34 +++------------ src/utils/utils_string.ts | 88 +++++++++++++++++++++++++++++++++++++- 5 files changed, 108 insertions(+), 67 deletions(-) diff --git a/src/AI/context.ts b/src/AI/context.ts index 173de43..21eccfc 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -526,15 +526,10 @@ export class Context { const { localImagePaths } = ConfigManager.image; const localImages: { [key: string]: string } = localImagePaths.reduce((acc: { [key: string]: string }, path: string) => { - if (path.trim() === '') { - return acc; - } + if (path.trim() === '') return acc; try { const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); - if (!name) { - throw new Error(`本地图片路径格式错误:${path}`); - } - + if (!name) throw new Error(`本地图片路径格式错误:${path}`); acc[name] = path; } catch (e) { logger.error(e); diff --git a/src/AI/image.ts b/src/AI/image.ts index 330bd58..1e72526 100644 --- a/src/AI/image.ts +++ b/src/AI/image.ts @@ -82,15 +82,10 @@ export class ImageManager { drawLocalImageFile(): string { const { localImagePaths } = ConfigManager.image; const localImages: { [key: string]: string } = localImagePaths.reduce((acc: { [key: string]: string }, path: string) => { - if (path.trim() === '') { - return acc; - } + if (path.trim() === '') return acc; try { const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); - if (!name) { - throw new Error(`本地图片路径格式错误:${path}`); - } - + if (!name) throw new Error(`本地图片路径格式错误:${path}`); acc[name] = path; } catch (e) { logger.error(e); @@ -99,17 +94,13 @@ export class ImageManager { }, {}); const keys = Object.keys(localImages); - if (keys.length == 0) { - return ''; - } + if (keys.length == 0) return ''; const index = Math.floor(Math.random() * keys.length); return localImages[keys[index]]; } async drawStolenImageFile(): Promise { - if (this.stolenImages.length === 0) { - return ''; - } + if (this.stolenImages.length === 0) return ''; const index = Math.floor(Math.random() * this.stolenImages.length); const image = this.stolenImages.splice(index, 1)[0]; @@ -133,15 +124,10 @@ export class ImageManager { async drawImageFile(): Promise { const { localImagePaths } = ConfigManager.image; const localImages: { [key: string]: string } = localImagePaths.reduce((acc: { [key: string]: string }, path: string) => { - if (path.trim() === '') { - return acc; - } + if (path.trim() === '') return acc; try { const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); - if (!name) { - throw new Error(`本地图片路径格式错误:${path}`); - } - + if (!name) throw new Error(`本地图片路径格式错误:${path}`); acc[name] = path; } catch (e) { logger.error(e); @@ -150,19 +136,13 @@ export class ImageManager { }, {}); const values = Object.values(localImages); - if (this.stolenImages.length == 0 && values.length == 0 && this.savedImages.length == 0) { - return ''; - } + if (this.stolenImages.length == 0 && values.length == 0 && this.savedImages.length == 0) return ''; const index = Math.floor(Math.random() * (values.length + this.stolenImages.length + this.savedImages.length)); - if (index < values.length) { - return values[index]; - } else if (index < values.length + this.stolenImages.length) { - return await this.drawStolenImageFile(); - } else { - return this.drawSavedImageFile(); - } + if (index < values.length) return values[index]; + else if (index < values.length + this.stolenImages.length) return await this.drawStolenImageFile(); + else return this.drawSavedImageFile(); } /** diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 43bb408..6c40398 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -48,12 +48,12 @@ export function registerMemory() { } } }, - required: ['memory_type', 'name', 'text', 'keywords'] + required: ['memory_type', 'name', 'text'] } } }); toolAdd.solve = async (ctx, msg, ai, args) => { - const { memory_type, name, text, keywords, userList = [], groupList = [] } = args; + const { memory_type, name, text, keywords = [], userList = [], groupList = [] } = args; if (memory_type === "private") { const uid = await ai.context.findUserId(ctx, name, true); diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index 5dacc9f..bcd15dd 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -13,15 +13,10 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< const { isMemory, isShortMemory } = ConfigManager.memory; const sandableImagesPrompt: string = localImagePaths .map(path => { - if (path.trim() === '') { - return null; - } + if (path.trim() === '') return null; try { const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); - if (!name) { - throw new Error(`本地图片路径格式错误:${path}`); - } - + if (!name) throw new Error(`本地图片路径格式错误:${path}`); return name; } catch (e) { logger.error(e); @@ -37,33 +32,18 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< let roleIndex = 0; if (exists && roleName !== '' && roleSettingNames.includes(roleName)) { roleIndex = roleSettingNames.indexOf(roleName); - if (roleIndex < 0 || roleIndex >= roleSettingTemplate.length) { - roleIndex = 0; - } + if (roleIndex < 0 || roleIndex >= roleSettingTemplate.length) roleIndex = 0; } else { const [roleIndex2, exists2] = seal.vars.intGet(ctx, "$gSYSPROMPT"); - if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) { - roleIndex = roleIndex2; - } + if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) roleIndex = roleIndex2; } // 记忆 - let memoryPrompt = ''; - if (isMemory) { - memoryPrompt = await ai.memory.buildMemoryPrompt(ctx, ai.context); - } - + const memoryPrompt = isMemory ? await ai.memory.buildMemoryPrompt(ctx, ai.context) : ''; // 短期记忆 - let shortMemoryPrompt = ''; - if (isShortMemory && ai.memory.useShortMemory) { - shortMemoryPrompt = ai.memory.shortMemoryList.map((item, index) => `${index + 1}. ${item}`).join('\n'); - } - + const shortMemoryPrompt = isShortMemory && ai.memory.useShortMemory ? ai.memory.shortMemoryList.map((item, index) => `${index + 1}. ${item}`).join('\n') : ''; // 调用函数 - let toolsPrompt = ''; - if (isTool && usePromptEngineering) { - toolsPrompt = ai.tool.getToolsPrompt(ctx); - } + const toolsPrompt = isTool && usePromptEngineering ? ai.tool.getToolsPrompt(ctx) : ''; const data = { "角色设定": roleSettingTemplate[roleIndex], diff --git a/src/utils/utils_string.ts b/src/utils/utils_string.ts index d6e78ab..0bbdd7b 100644 --- a/src/utils/utils_string.ts +++ b/src/utils/utils_string.ts @@ -4,7 +4,7 @@ import { Image, ImageManager } from "../AI/image"; import { logger } from "../logger"; import { ConfigManager } from "../config/config"; import { transformMsgIdBack } from "./utils"; -import { AI } from "../AI/AI"; +import { AI, GroupInfo, UserInfo } from "../AI/AI"; /* 先丢这一坨东西在这。之所以不用是因为被类型检查整烦了 @@ -620,4 +620,90 @@ export function fixJsonString(s: string): string { return fixed; } +} + +export function parseConfigMemory(s: string): { text: string, userList: UserInfo[], groupList: GroupInfo[], keywords: string[], images: Image[] }[] { + if (!s) return []; + + const result: { text: string, userList: UserInfo[], groupList: GroupInfo[], keywords: string[], images: Image[] }[] = []; + const segs = s.split(/-{3,}/); + segs.forEach(seg => { + if (!seg.trim()) return; + + const item: { text: string, userList: UserInfo[], groupList: GroupInfo[], keywords: string[], images: Image[] } = { + text: '', + userList: [], + groupList: [], + keywords: [], + images: [] + }; + + const lines = seg.split('\n'); + if (lines.length === 0) return; + + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(/^\s*?(用户|群聊|关键词|图片|内容)\s*?[::](.*)/); + if (!match) { + continue; + } + const type = match[1]; + const value = match[2].trim(); + switch (type) { + case '用户': { + item.userList = value.split(/[,,]/).map(s => { + const [name, id] = s.split(/[::]/).map(s => s.trim()).filter(s => s); + if (!name || !id) return null; + return { isPrivate: true, id, name }; + }).filter(ui => ui) as UserInfo[]; + break; + } + case '群聊': { + item.groupList = value.split(/[,,]/).map(s => { + const [name, id] = s.split(/[::]/).map(s => s.trim()).filter(s => s); + if (!name || !id) return null; + return { isPrivate: false, id, name }; + }).filter(ui => ui) as GroupInfo[]; + break; + } + case '关键词': { + item.keywords = value.split(/[,,]/).map(kw => kw.trim()).filter(kw => kw); + break; + } + case '图片': { + const { localImagePaths } = ConfigManager.image; + const localImages: { [key: string]: string } = localImagePaths.reduce((acc: { [key: string]: string }, path: string) => { + if (path.trim() === '') { + return acc; + } + try { + const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); + if (!name) throw new Error(`本地图片路径格式错误:${path}`); + acc[name] = path; + } catch (e) { + logger.error(e); + } + return acc; + }, {}); + + item.images = value.split(/[,,]/).map(id => id.trim()).map(id => { + if (localImages.hasOwnProperty(id)) return new Image(localImages[id]); + logger.error(`图片${id}不存在`); + return null; + }).filter(img => img); + break; + } + case '内容': { + item.text = lines.slice(i).join('\n').trim(); + break; + } + default: continue; + } + } + + if (!item.text) return; + + result.push(item); + }); + + return result; } \ No newline at end of file From bacacc67c92b7bd196a7326ae166a06f83246a10 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sat, 8 Nov 2025 18:29:05 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=20#56?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/memory.ts | 242 +++++++++++++++++++++++++++++++---- src/config/config_memory.ts | 49 ++++++- src/config/config_message.ts | 5 + src/index.ts | 2 + src/service.ts | 12 +- src/tool/tool.ts | 8 +- src/update.ts | 3 +- src/utils/utils_message.ts | 4 + src/utils/utils_string.ts | 88 +------------ 9 files changed, 290 insertions(+), 123 deletions(-) diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 6981da3..f7c9225 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -50,6 +50,22 @@ export class Memory { this.images = []; } + copy(): Memory { + const m = new Memory(); + m.id = this.id; + m.vector = [...this.vector]; + m.text = this.text; + m.sessionInfo = JSON.parse(JSON.stringify(this.sessionInfo)); + m.userList = JSON.parse(JSON.stringify(this.userList)); + m.groupList = JSON.parse(JSON.stringify(this.groupList)); + m.createTime = this.createTime; + m.lastMentionTime = this.lastMentionTime; + m.keywords = [...this.keywords]; + m.weight = this.weight; + m.images = [...this.images]; + return m; + } + /** * 计算记忆的基础相似度分数 * @returns 基础相似度分数(0.5-2.0) @@ -99,6 +115,23 @@ export class Memory { // 相似度增强因子 return avgSimilarity ? 0.7 + avgSimilarity * 0.6 : 1; } + + async updateVector() { + const { isMemoryVector, embeddingDimension } = ConfigManager.memory; + if (isMemoryVector) { + logger.info(`更新记忆向量: ${this.id}`); + const vector = await getEmbedding(this.text); + if (!vector.length) { + logger.error('返回向量为空'); + return null; + } + if (vector.length !== embeddingDimension) { + logger.error(`向量维度不匹配。期望: ${embeddingDimension}, 实际: ${vector.length}`); + return null; + } + this.vector = vector; + } + } } export class MemoryManager { @@ -160,21 +193,7 @@ export class MemoryManager { m.keywords = kws; m.weight = 5; m.images = await ImageManager.extractExistingImages(ai, text); - - const { isMemoryVector, embeddingDimension } = ConfigManager.memory; - if (isMemoryVector) { - const vector = await getEmbedding(text); - if (!vector.length) { - logger.error('向量为空'); - return null; - } - if (vector.length !== embeddingDimension) { - logger.error(`向量维度不匹配。期望: ${embeddingDimension}, 实际: ${vector.length}`); - return null; - } - m.vector = vector; - } - + await m.updateVector(); this.limitMemory(); this.memoryMap[id] = m; } @@ -330,8 +349,10 @@ export class MemoryManager { memories: { memory_type: 'private' | 'group', name: string, - keywords: string[], - content: string + text: string, + keywords?: string[], + userList?: string[], + groupList?: string[], }[] }; @@ -368,10 +389,15 @@ export class MemoryManager { for (const m of memoryList) { if (m.vector.length !== embeddingDimension) { logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); - m.vector = await getEmbedding(m.text); + await m.updateVector(); } } return memoryList + .map(item => { + const m = item.copy(); + if (item.keywords.some(kw => query.includes(kw))) m.weight += 10; //提权 + return m; + }) .sort((a, b) => { const bScore = b.calculateBaseScore() * b.calculateSimiliarity(queryVector, options.userList, options.groupList, options.keywords); const aScore = a.calculateBaseScore() * a.calculateSimiliarity(queryVector, options.userList, options.groupList, options.keywords); @@ -384,13 +410,12 @@ export class MemoryManager { } return memoryList .map(item => { - const mi: Memory = JSON.parse(JSON.stringify(item)); - if (item.keywords.some(kw => query.includes(kw))) { - mi.weight += 10; //提权 - } - return mi; + const m = item.copy(); + if (item.keywords.some(kw => query.includes(kw))) m.weight += 10; //提权 + return m; }) - .sort((a, b) => b.weight - a.weight); + .sort((a, b) => b.weight - a.weight) + .slice(0, options.topK); } updateSingleMemoryWeight(s: string, role: 'user' | 'assistant') { @@ -410,8 +435,8 @@ export class MemoryManager { } updateMemoryWeight(ctx: seal.MsgContext, context: Context, s: string, role: 'user' | 'assistant') { - const ai = AIManager.getAI(ctx.endPoint.userId); - ai.memory.updateSingleMemoryWeight(s, role); + AIManager.getAI(ctx.endPoint.userId).memory.updateSingleMemoryWeight(s, role); + knowledgeMM.updateSingleMemoryWeight(s, role); this.updateSingleMemoryWeight(s, role); if (!ctx.isPrivate) { @@ -549,4 +574,167 @@ export class MemoryManager { } return null; } -} \ No newline at end of file +} + +export class KnowledgeMemoryManager extends MemoryManager { + constructor() { + super(); + } + + init() { + this.memoryMap = JSON.parse(ConfigManager.ext.storageGet('knowledgeMemoryMap') || '{}'); + this.reviveMemoryMap(); + } + + save() { + ConfigManager.ext.storageSet('knowledgeMemoryMap', JSON.stringify(this.memoryMap)); + } + + async updateKnowledgeMemory(index: number) { + const { knowledgeMemoryStringList } = ConfigManager.memory; + if (index < 0 || index >= knowledgeMemoryStringList.length) return; + const s = knowledgeMemoryStringList[index]; + if (!s) return; + + const memoryMap: { [id: string]: Memory } = {} + const segs = s.split(/-{3,}/); + for (const seg of segs) { + if (!seg.trim()) continue; + + const lines = seg.split('\n'); + if (lines.length === 0) continue; + + const now = Math.floor(Date.now() / 1000); + const m = new Memory(); + for (let i = 0; i < lines.length; i++) { + const match = lines[i].match(/^\s*?(ID|用户|群聊|关键词|图片|内容)\s*?[::](.*)/); + if (!match) { + continue; + } + const type = match[1]; + const value = match[2].trim(); + switch (type) { + case 'ID': { + m.id = value; + break; + } + case '用户': { + m.userList = value.split(/[,,]/).map(s => { + const segs = s.split(/[::]/).map(s => s.trim()).filter(s => s); + if (segs.length < 2) return null; + const name = value.replace(/[::].*$/, '').trim(); + const id = segs[segs.length - 1]; + if (!name || !id) return null; + return { isPrivate: true, id, name }; + }).filter(ui => ui) as UserInfo[]; + break; + } + case '群聊': { + m.groupList = value.split(/[,,]/).map(s => { + const segs = s.split(/[::]/).map(s => s.trim()).filter(s => s); + if (segs.length < 2) return null; + const name = value.replace(/[::].*$/, '').trim(); + const id = segs[segs.length - 1]; + if (!name || !id) return null; + return { isPrivate: false, id, name }; + }).filter(ui => ui) as GroupInfo[]; + break; + } + case '关键词': { + m.keywords = value.split(/[,,]/).map(kw => kw.trim()).filter(kw => kw); + break; + } + case '图片': { + const { localImagePaths } = ConfigManager.image; + const localImages: { [key: string]: string } = localImagePaths.reduce((acc: { [key: string]: string }, path: string) => { + if (path.trim() === '') { + return acc; + } + try { + const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); + if (!name) throw new Error(`本地图片路径格式错误:${path}`); + acc[name] = path; + } catch (e) { + logger.error(e); + } + return acc; + }, {}); + + m.images = value.split(/[,,]/).map(id => id.trim()).map(id => { + if (localImages.hasOwnProperty(id)) return new Image(localImages[id]); + logger.error(`图片${id}不存在`); + return null; + }).filter(img => img); + break; + } + case '内容': { + m.text = lines.slice(i).join('\n').trim().replace(/^内容[::]/, ''); + break; + } + default: continue; + } + } + + if (!m.id && !m.text) continue; + if (this.memoryMap.hasOwnProperty(m.id)) { + const m2 = this.memoryMap[m.id]; + m.vector = m2.vector; + if (m2.text !== m.text) await m.updateVector(); + m.createTime = m2.createTime; + m.lastMentionTime = m2.lastMentionTime; + m.weight = m2.weight; + } else { + await m.updateVector(); + m.createTime = now; + m.lastMentionTime = now; + m.weight = 5; + } + memoryMap[m.id] = m; + } + this.memoryMap = memoryMap; + this.save(); + } + + async buildKnowledgeMemoryPrompt(index: number, context: Context): Promise { + await this.updateKnowledgeMemory(index); + if (Object.keys(this.memoryMap).length === 0) return ''; + + const { showNumber } = ConfigManager.message; + const { knowledgeMemoryShowNumber, knowledgeMemorySingleShowTemplate } = ConfigManager.memory; + + const userMessages = context.messages.filter(msg => msg.role === 'user' && !msg.name.startsWith('_')); + const lastMsg = userMessages.length > 0 ? userMessages[userMessages.length - 1].msgArray.map(m => m.content).join('') : ''; + const memoryList = await this.search(lastMsg, { + topK: knowledgeMemoryShowNumber, + userList: [], + groupList: [], + keywords: [], + includeImages: false + }); + if (memoryList.length === 0) return ''; + + let prompt = ''; + if (memoryList.length === 0) { + prompt = '无'; + } else { + prompt = memoryList + .map((m, i) => { + const data = { + "序号": i + 1, + "记忆ID": m.id, + "用户列表": m.userList.map(u => u.name + (showNumber ? `(${u.id.replace(/^.+:/, '')})` : '')).join(';'), + "群聊列表": m.groupList.map(g => g.name + (showNumber ? `(${g.id.replace(/^.+:/, '')})` : '')).join(';'), + "关键词": m.keywords.join(';'), + "记忆内容": m.text + } + + const template = Handlebars.compile(knowledgeMemorySingleShowTemplate[0]); + return template(data); + }).join('\n'); + } + + return prompt; + } +} + +export const knowledgeMM = new KnowledgeMemoryManager(); \ No newline at end of file diff --git a/src/config/config_memory.ts b/src/config/config_memory.ts index c855679..9bc595b 100644 --- a/src/config/config_memory.ts +++ b/src/config/config_memory.ts @@ -6,6 +6,27 @@ export class MemoryConfig { static register() { MemoryConfig.ext = ConfigManager.getExt('aiplugin4_7:记忆'); + seal.ext.registerIntConfig(MemoryConfig.ext, "知识库记忆展示数量", 10, ""); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "知识库记忆", [ + ``, + `ID:测试 +用户:用户1:114514,用户2:1919810 +群聊:群聊1:114514,群聊2:1919810 +关键词:关键词1,关键词2 +图片:本地图片1的名字,本地图片2的名字 +内容:这是内容 +内容放在最后,可以换行 +--- +ID:上面是分割符 +内容:用于多个知识词条的分割` + ], "与角色设定一一对应"); + seal.ext.registerTemplateConfig(MemoryConfig.ext, "单条知识库记忆展示模板", [ + ` {{{序号}}}. 记忆ID:{{{记忆ID}}} + 相关用户:{{{用户列表}}} + 相关群聊:{{{群聊列表}}} + 关键词:{{{关键词}}} + 内容:{{{记忆内容}}}` + ], ""); seal.ext.registerBoolConfig(MemoryConfig.ext, "是否启用长期记忆", true, ""); seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆上限", 50, ""); seal.ext.registerIntConfig(MemoryConfig.ext, "长期记忆展示数量", 5, ""); @@ -101,18 +122,33 @@ export class MemoryConfig { type: 'string', description: '用户名称或群聊名称{{#if 展示号码}}或纯数字QQ号、群号{{/if}},实际使用时与记忆类型对应' }, + "text": { + type: 'string', + description: '记忆内容,尽量简短,无需附带时间与来源' + }, "keywords": { type: 'array', - description: '记忆关键词', + description: '相关用户名称列表', items: { type: 'string' } }, - "content": { - type: 'string', - description: '记忆内容,尽量简短,无需附带时间与来源' + "userList": { + type: 'array', + description: '相关用户名称列表', + items: { + type: 'string' + } + }, + "groupList": { + type: 'array', + description: '相关群聊名称列表', + items: { + type: 'string' + } } - } + }, + "required": ['memory_type', 'name', 'text'] } } }` @@ -121,6 +157,9 @@ export class MemoryConfig { static get() { return { + knowledgeMemoryShowNumber: seal.ext.getIntConfig(MemoryConfig.ext, "知识库记忆展示数量"), + knowledgeMemoryStringList: seal.ext.getTemplateConfig(MemoryConfig.ext, "知识库记忆"), + knowledgeMemorySingleShowTemplate: seal.ext.getTemplateConfig(MemoryConfig.ext, "单条知识库记忆展示模板"), isMemory: seal.ext.getBoolConfig(MemoryConfig.ext, "是否启用长期记忆"), memoryLimit: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆上限"), memoryShowNumber: seal.ext.getIntConfig(MemoryConfig.ext, "长期记忆展示数量"), diff --git a/src/config/config_message.ts b/src/config/config_message.ts index 590450d..3223158 100644 --- a/src/config/config_message.ts +++ b/src/config/config_message.ts @@ -55,6 +55,11 @@ export class MessageConfig { {{#if 可发送图片不为空}} - 可使用<|img:图片名称|>发送表情包,表情名称有:{{{可发送图片列表}}} {{/if}} +{{#if 知识库}} + +## 知识库 +{{{知识库}}} +{{/if}} {{#if 开启长期记忆}} ## 记忆 diff --git a/src/index.ts b/src/index.ts index 195baab..d9f9dd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { TimerManager } from "./timer"; import { createMsg } from "./utils/utils_seal"; import { PrivilegeManager } from "./privilege"; import { aliasToCmd } from "./utils/utils"; +import { knowledgeMM } from "./AI/memory"; function main() { ConfigManager.registerConfig(); @@ -19,6 +20,7 @@ function main() { ToolManager.registerTool(); TimerManager.init(); PrivilegeManager.reviveCmdPriv(); + knowledgeMM.init(); const ext = ConfigManager.ext; diff --git a/src/service.ts b/src/service.ts index d62e47e..9caf629 100644 --- a/src/service.ts +++ b/src/service.ts @@ -140,12 +140,20 @@ export async function sendITTRequest(messages: { } } +const vectorCache: { text: string, vector: number[] } = { text: '', vector: [] }; + export async function getEmbedding(text: string): Promise { if (!text) { logger.warning(`getEmbedding: 文本为空`); return []; } + if (text === vectorCache.text) { + const v = vectorCache.vector; + vectorCache.text = ''; + return v; + } + const { timeout } = ConfigManager.request; const { embeddingDimension, embeddingUrl, embeddingApiKey, embeddingBodyTemplate } = ConfigManager.memory; @@ -160,7 +168,9 @@ export async function getEmbedding(text: string): Promise { const embedding = data.data[0].embedding; - logger.info(`响应embedding长度:`, embedding.length, '\nlatency:', Date.now() - time, 'ms'); + logger.info(`文本:`, text, `\n响应embedding长度:`, embedding.length, '\nlatency:', Date.now() - time, 'ms'); + vectorCache.text = text; + vectorCache.vector = embedding; return embedding; } else { diff --git a/src/tool/tool.ts b/src/tool/tool.ts index 971ae9d..94ec042 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -437,13 +437,17 @@ export class ToolManager { reviveToolStauts() { const { toolsNotAllow, toolsDefaultClosed } = ConfigManager.tool; + const toolStatus: {[key: string]: boolean} = {}; for (const k in ToolManager.toolMap) { if (!this.toolStatus.hasOwnProperty(k)) { - this.toolStatus[k] = !toolsNotAllow.includes(k) && !toolsDefaultClosed.includes(k); + toolStatus[k] = !toolsNotAllow.includes(k) && !toolsDefaultClosed.includes(k); } else if (toolsNotAllow.includes(k)) { - this.toolStatus[k] = false; + toolStatus[k] = false; + } else { + toolStatus[k] = this.toolStatus[k]; } } + this.toolStatus = toolStatus; } getToolsInfo(type: string): ToolInfo[] { diff --git a/src/update.ts b/src/update.ts index a89781c..e94d0e5 100644 --- a/src/update.ts +++ b/src/update.ts @@ -5,7 +5,8 @@ export const updateInfo = { - 修复了调用函数时,无需cmdArgs的函数也会报错的问题 - 新增了修改上下文里的名字相关功能 - 活跃时间添加上一条消息时间提示 -- 新增向量记忆`, +- 新增向量记忆 +- 新增知识库`, "4.11.2": `- 增加修复json解析错误的功能`, "4.11.1": `- 修复了戳戳、权限检查、权限设置、帮助文本等相关问题`, "4.11.0": `- 新增请求超时相关 diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index bcd15dd..c0dd039 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -5,6 +5,7 @@ import { logger } from "../logger"; import { ConfigManager } from "../config/config"; import { ToolInfo } from "../tool/tool"; import { fmtDate } from "./utils_string"; +import { knowledgeMM } from "../AI/memory"; export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise { const { roleSettingNames, roleSettingTemplate, systemMessageTemplate, isPrefix, showNumber, showMsgId, showTime } = ConfigManager.message; @@ -38,6 +39,8 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) roleIndex = roleIndex2; } + // 知识库 + const knowledgePrompt = await knowledgeMM.buildKnowledgeMemoryPrompt(roleIndex, ai.context); // 记忆 const memoryPrompt = isMemory ? await ai.memory.buildMemoryPrompt(ctx, ai.context) : ''; // 短期记忆 @@ -61,6 +64,7 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< "图片条件不为零": condition !== '0', "可发送图片不为空": sandableImagesPrompt, "可发送图片列表": sandableImagesPrompt, + "知识库": knowledgePrompt, "开启长期记忆": isMemory && memoryPrompt, "记忆信息": memoryPrompt, "开启短期记忆": isShortMemory && ai.memory.useShortMemory && shortMemoryPrompt, diff --git a/src/utils/utils_string.ts b/src/utils/utils_string.ts index 0bbdd7b..d6e78ab 100644 --- a/src/utils/utils_string.ts +++ b/src/utils/utils_string.ts @@ -4,7 +4,7 @@ import { Image, ImageManager } from "../AI/image"; import { logger } from "../logger"; import { ConfigManager } from "../config/config"; import { transformMsgIdBack } from "./utils"; -import { AI, GroupInfo, UserInfo } from "../AI/AI"; +import { AI } from "../AI/AI"; /* 先丢这一坨东西在这。之所以不用是因为被类型检查整烦了 @@ -620,90 +620,4 @@ export function fixJsonString(s: string): string { return fixed; } -} - -export function parseConfigMemory(s: string): { text: string, userList: UserInfo[], groupList: GroupInfo[], keywords: string[], images: Image[] }[] { - if (!s) return []; - - const result: { text: string, userList: UserInfo[], groupList: GroupInfo[], keywords: string[], images: Image[] }[] = []; - const segs = s.split(/-{3,}/); - segs.forEach(seg => { - if (!seg.trim()) return; - - const item: { text: string, userList: UserInfo[], groupList: GroupInfo[], keywords: string[], images: Image[] } = { - text: '', - userList: [], - groupList: [], - keywords: [], - images: [] - }; - - const lines = seg.split('\n'); - if (lines.length === 0) return; - - for (let i = 0; i < lines.length; i++) { - const match = lines[i].match(/^\s*?(用户|群聊|关键词|图片|内容)\s*?[::](.*)/); - if (!match) { - continue; - } - const type = match[1]; - const value = match[2].trim(); - switch (type) { - case '用户': { - item.userList = value.split(/[,,]/).map(s => { - const [name, id] = s.split(/[::]/).map(s => s.trim()).filter(s => s); - if (!name || !id) return null; - return { isPrivate: true, id, name }; - }).filter(ui => ui) as UserInfo[]; - break; - } - case '群聊': { - item.groupList = value.split(/[,,]/).map(s => { - const [name, id] = s.split(/[::]/).map(s => s.trim()).filter(s => s); - if (!name || !id) return null; - return { isPrivate: false, id, name }; - }).filter(ui => ui) as GroupInfo[]; - break; - } - case '关键词': { - item.keywords = value.split(/[,,]/).map(kw => kw.trim()).filter(kw => kw); - break; - } - case '图片': { - const { localImagePaths } = ConfigManager.image; - const localImages: { [key: string]: string } = localImagePaths.reduce((acc: { [key: string]: string }, path: string) => { - if (path.trim() === '') { - return acc; - } - try { - const name = path.split('/').pop().replace(/\.[^/.]+$/, ''); - if (!name) throw new Error(`本地图片路径格式错误:${path}`); - acc[name] = path; - } catch (e) { - logger.error(e); - } - return acc; - }, {}); - - item.images = value.split(/[,,]/).map(id => id.trim()).map(id => { - if (localImages.hasOwnProperty(id)) return new Image(localImages[id]); - logger.error(`图片${id}不存在`); - return null; - }).filter(img => img); - break; - } - case '内容': { - item.text = lines.slice(i).join('\n').trim(); - break; - } - default: continue; - } - } - - if (!item.text) return; - - result.push(item); - }); - - return result; } \ No newline at end of file From 7f14c081ef11730b3b319d7f031b555b77534ada Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sun, 9 Nov 2025 01:38:45 +0800 Subject: [PATCH 06/12] bugfix --- src/AI/memory.ts | 30 ++++++++++++++++++++++++------ src/tool/tool_memory.ts | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/AI/memory.ts b/src/AI/memory.ts index f7c9225..3c0d720 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -123,11 +123,11 @@ export class Memory { const vector = await getEmbedding(this.text); if (!vector.length) { logger.error('返回向量为空'); - return null; + return; } if (vector.length !== embeddingDimension) { logger.error(`向量维度不匹配。期望: ${embeddingDimension}, 实际: ${vector.length}`); - return null; + return; } this.vector = vector; } @@ -369,17 +369,17 @@ export class MemoryManager { } } - async search(query: string, options: searchOptions = { + async searchByScore(query: string, options: searchOptions = { topK: 10, userList: [], groupList: [], keywords: [], includeImages: false, }) { - const { isMemoryVector, embeddingDimension } = ConfigManager.memory; + const { embeddingDimension } = ConfigManager.memory; const memoryList = Object.values(this.memoryMap); if (!memoryList.length) return []; - if (isMemoryVector && query) { + if (query) { try { const queryVector = await getEmbedding(query); if (!queryVector.length) { @@ -408,6 +408,23 @@ export class MemoryManager { logger.error(`语义搜索失败: ${e.message}`); } } + return []; + } + + async search(query: string, options: searchOptions = { + topK: 10, + userList: [], + groupList: [], + keywords: [], + includeImages: false, + }) { + const { isMemoryVector } = ConfigManager.memory; + const memoryList = Object.values(this.memoryMap); + if (!memoryList.length) return []; + if (isMemoryVector && query) { + const result = await this.searchByScore(query, options); + if (result.length) return result; + } return memoryList .map(item => { const m = item.copy(); @@ -473,6 +490,7 @@ export class MemoryManager { } buildMemory(sessionInfo: SessionInfo, memoryList: Memory[]): string { + if (this.persona === '无' && memoryList.length === 0) return ''; const { showNumber } = ConfigManager.message; const { memoryShowTemplate, memorySingleShowTemplate } = ConfigManager.memory; @@ -597,7 +615,7 @@ export class KnowledgeMemoryManager extends MemoryManager { if (!s) return; const memoryMap: { [id: string]: Memory } = {} - const segs = s.split(/-{3,}/); + const segs = s.split(/\n-{3,}\n/); for (const seg of segs) { if (!seg.trim()) continue; diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 6c40398..038f737 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -311,7 +311,7 @@ export function registerMemory() { const images = Array.from(new Set([].concat(...memoryList.map(m => m.images)))); return { - content: ai.memory.buildMemory(si, memoryList), + content: ai.memory.buildMemory(si, memoryList) || '暂无记忆', images: images }; } From a2525c8e02680ab1e16dbefee3d8622028f1bbd3 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sun, 9 Nov 2025 16:36:32 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E6=95=B0=E5=80=BC=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/memory.ts | 95 +++++++++++++++++++++++++++--------------------- src/service.ts | 9 ++--- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 3c0d720..51eb656 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -72,22 +72,23 @@ export class Memory { */ calculateBaseScore() { // 权重转换(weight: 0-10 → baseScore: 0.5-2.0) - return 0.5 + (this.weight / 0.15); + return 0.5 + (this.weight * 0.15); } /** - * 计算记忆的新鲜度衰减因子 - * @returns 衰减因子(1-∞) + * 计算记忆的新鲜度衰减因子,越大表示越新鲜 + * @returns 衰减因子(0-1) */ calculateDecay() { const now = Math.floor(Date.now() / 1000); - const w = 7 * 24 * 60 * 60; - // 基础新鲜度衰减: 1 + ln(1 + ageInWeeks) - const ageDecay = Math.log(1 + (now - this.createTime) / w); - // 活跃度衰减因子: 1 + ln(1 + 4hours) - const activityDecay = Math.log(1 + (now - this.lastMentionTime) / 14400); - // 衰减因子 - return Math.max(1, ageDecay * activityDecay); + const ageInDays = (now - this.createTime) / (24 * 60 * 60); + const activityInHours = (now - this.lastMentionTime) / (60 * 60); + // 基础新鲜度: exp(-ageInDays / 7) + const ageDecay = Math.exp(-ageInDays / 7); + // 活跃度: exp(-activityInHours / 4) + const activityDecay = Math.exp(-activityInHours / 4); + // 衰减因子,取年龄衰减和活跃度衰减的较大值 + return Math.max(ageDecay, activityDecay); } /** @@ -96,24 +97,27 @@ export class Memory { * @param ul 查询用户列表 * @param gl 查询群组列表 * @param kws 查询关键词列表 - * @returns 相似度分数(0.7-1.3) + * @returns 相似度分数(0-1) */ - calculateSimiliarity(v: number[], ul: UserInfo[], gl: GroupInfo[], kws: string[]): number { - // 向量相似度分数(如果提供了向量v) + calculateSimilarity(v: number[], ul: UserInfo[], gl: GroupInfo[], kws: string[]): number { + // 总权重 0-1 + const totalWeight = (v.length ? 0.4 : 0) + (ul.length ? 0.2 : 0) + (gl.length ? 0.2 : 0) + (kws.length ? 0.2 : 0); + if (totalWeight === 0) return 0; + // 向量相似度分数(如果提供了向量v) 0-1 const vectorSimilarity = (v && v.length > 0 && this.vector && this.vector.length > 0) ? (cosineSimilarity(v, this.vector) + 1) / 2 : 0; - // 用户相似度分数 + // 用户相似度分数 0-1 const commonUser = getCommonUser(this.userList, ul); const userSimilarity = (ul && ul.length > 0) ? commonUser.length / (this.userList.length + ul.length - commonUser.length) : 0; - // 群组相似度分数 + // 群组相似度分数 0-1 const commonGroup = getCommonGroup(this.groupList, gl); const groupSimilarity = (gl && gl.length > 0) ? commonGroup.length / (this.groupList.length + gl.length - commonGroup.length) : 0; - // 关键词匹配分数 + // 关键词匹配分数 0-1 const commonKeyword = getCommonKeyword(this.keywords, kws); const keywordSimilarity = (kws && kws.length > 0) ? commonKeyword.length / kws.length : 0; - // 综合相似度分数 - const avgSimilarity = vectorSimilarity * 0.4 + userSimilarity * 0.25 + groupSimilarity * 0.25 + keywordSimilarity * 0.1; - // 相似度增强因子 - return avgSimilarity ? 0.7 + avgSimilarity * 0.6 : 1; + // 综合相似度分数 0-1 + const avgSimilarity = vectorSimilarity * 0.4 + userSimilarity * 0.2 + groupSimilarity * 0.2 + keywordSimilarity * 0.2; + // 相似度增强因子 0-1 + return avgSimilarity / totalWeight; } async updateVector() { @@ -219,20 +223,18 @@ export class MemoryManager { limitMemory() { const { memoryLimit } = ConfigManager.memory; - const memoryList = Object.values(this.memoryMap); - - const forgetIdList = memoryList - .map((m) => { - return { - id: m.id, - fgtWeight: m.calculateDecay() / m.calculateBaseScore() - } - }) - .sort((a, b) => b.fgtWeight - a.fgtWeight) - .slice(0, memoryList.length - memoryLimit + 1) // 预留1个位置用于存储最新记忆 - .map(item => item.id); - - this.delMemory(forgetIdList); + const limit = memoryLimit > 0 ? memoryLimit - 1 : 0; // 预留1个位置用于存储最新记忆 + const memoryList = Object.values(this.memoryMap) + if (memoryList.length <= limit) return; + memoryList.map((m) => { + return { + id: m.id, + score: m.calculateDecay() * m.calculateBaseScore() + } + }) + .sort((a, b) => b.score - a.score) // 从大到小排序 + .slice(limit) + .forEach(item => delete this.memoryMap?.[item.id]); } clearMemory() { @@ -386,12 +388,12 @@ export class MemoryManager { logger.error('查询向量为空'); return []; } - for (const m of memoryList) { + await Promise.all(memoryList.map(async m => { if (m.vector.length !== embeddingDimension) { logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); await m.updateVector(); } - } + })) return memoryList .map(item => { const m = item.copy(); @@ -399,8 +401,8 @@ export class MemoryManager { return m; }) .sort((a, b) => { - const bScore = b.calculateBaseScore() * b.calculateSimiliarity(queryVector, options.userList, options.groupList, options.keywords); - const aScore = a.calculateBaseScore() * a.calculateSimiliarity(queryVector, options.userList, options.groupList, options.keywords); + const bScore = b.calculateBaseScore() * b.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); + const aScore = a.calculateBaseScore() * a.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); return bScore - aScore; }) .slice(0, options.topK); @@ -622,7 +624,6 @@ export class KnowledgeMemoryManager extends MemoryManager { const lines = seg.split('\n'); if (lines.length === 0) continue; - const now = Math.floor(Date.now() / 1000); const m = new Memory(); for (let i = 0; i < lines.length; i++) { const match = lines[i].match(/^\s*?(ID|用户|群聊|关键词|图片|内容)\s*?[::](.*)/); @@ -694,6 +695,12 @@ export class KnowledgeMemoryManager extends MemoryManager { } if (!m.id && !m.text) continue; + + memoryMap[m.id] = m; + } + + const now = Math.floor(Date.now() / 1000); + await Promise.all(Object.values(memoryMap).map(async m => { if (this.memoryMap.hasOwnProperty(m.id)) { const m2 = this.memoryMap[m.id]; m.vector = m2.vector; @@ -707,8 +714,8 @@ export class KnowledgeMemoryManager extends MemoryManager { m.lastMentionTime = now; m.weight = 5; } - memoryMap[m.id] = m; - } + })) + this.memoryMap = memoryMap; this.save(); } @@ -755,4 +762,8 @@ export class KnowledgeMemoryManager extends MemoryManager { } } -export const knowledgeMM = new KnowledgeMemoryManager(); \ No newline at end of file +export const knowledgeMM = new KnowledgeMemoryManager(); + +// 可以通过维护一组索引来优化搜索性能。 +// 好麻烦,不想弄 +// 目前数量级应该没什么优化的需求 \ No newline at end of file diff --git a/src/service.ts b/src/service.ts index 9caf629..15410ea 100644 --- a/src/service.ts +++ b/src/service.ts @@ -148,15 +148,14 @@ export async function getEmbedding(text: string): Promise { return []; } - if (text === vectorCache.text) { + const { timeout } = ConfigManager.request; + const { embeddingDimension, embeddingUrl, embeddingApiKey, embeddingBodyTemplate } = ConfigManager.memory; + + if (vectorCache.text === text && vectorCache.vector.length === embeddingDimension) { const v = vectorCache.vector; - vectorCache.text = ''; return v; } - const { timeout } = ConfigManager.request; - const { embeddingDimension, embeddingUrl, embeddingApiKey, embeddingBodyTemplate } = ConfigManager.memory; - try { const bodyObject = parseEmbeddingBody(embeddingBodyTemplate, text, embeddingDimension); const time = Date.now(); From ceb6714a1f1e0e33e0a886822d2c75be983220f8 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Sun, 9 Nov 2025 16:39:55 +0800 Subject: [PATCH 08/12] =?UTF-8?q?=E6=95=B0=E5=80=BC=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/memory.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 51eb656..95e904f 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -66,15 +66,6 @@ export class Memory { return m; } - /** - * 计算记忆的基础相似度分数 - * @returns 基础相似度分数(0.5-2.0) - */ - calculateBaseScore() { - // 权重转换(weight: 0-10 → baseScore: 0.5-2.0) - return 0.5 + (this.weight * 0.15); - } - /** * 计算记忆的新鲜度衰减因子,越大表示越新鲜 * @returns 衰减因子(0-1) @@ -97,7 +88,7 @@ export class Memory { * @param ul 查询用户列表 * @param gl 查询群组列表 * @param kws 查询关键词列表 - * @returns 相似度分数(0-1) + * @returns 相似度分数(1-2) */ calculateSimilarity(v: number[], ul: UserInfo[], gl: GroupInfo[], kws: string[]): number { // 总权重 0-1 @@ -116,8 +107,8 @@ export class Memory { const keywordSimilarity = (kws && kws.length > 0) ? commonKeyword.length / kws.length : 0; // 综合相似度分数 0-1 const avgSimilarity = vectorSimilarity * 0.4 + userSimilarity * 0.2 + groupSimilarity * 0.2 + keywordSimilarity * 0.2; - // 相似度增强因子 0-1 - return avgSimilarity / totalWeight; + // 相似度增强因子 1-2 + return 1 + avgSimilarity / totalWeight; } async updateVector() { @@ -229,7 +220,7 @@ export class MemoryManager { memoryList.map((m) => { return { id: m.id, - score: m.calculateDecay() * m.calculateBaseScore() + score: m.calculateDecay() * m.weight } }) .sort((a, b) => b.score - a.score) // 从大到小排序 @@ -401,8 +392,8 @@ export class MemoryManager { return m; }) .sort((a, b) => { - const bScore = b.calculateBaseScore() * b.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); - const aScore = a.calculateBaseScore() * a.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); + const bScore = b.weight * b.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); + const aScore = a.weight * a.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); return bScore - aScore; }) .slice(0, options.topK); From 0399ca273d06616e68daa08448cc6f931694f719 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Mon, 10 Nov 2025 21:56:59 +0800 Subject: [PATCH 09/12] style --- src/AI/context.ts | 7 +-- src/AI/memory.ts | 102 ++++++++++++++++++---------------------- src/index.ts | 15 ++---- src/tool/tool_memory.ts | 2 +- 4 files changed, 52 insertions(+), 74 deletions(-) diff --git a/src/AI/context.ts b/src/AI/context.ts index 21eccfc..8ede61f 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -192,7 +192,7 @@ export class Context { } //更新记忆权重 - ai.memory.updateMemoryWeight(ctx, ai.context, s, role); + ai.memory.updateRelatedMemoryWeight(ctx, ai.context, s, role); //删除多余的上下文 this.limitMessages(maxRounds); @@ -377,10 +377,7 @@ export class Context { continue; } - const ai = AIManager.getAI(uid); - const memoryList = Object.values(ai.memory.memoryMap); - - for (const m of memoryList) { + for (const m of AIManager.getAI(uid).memory.memoryList) { if (m.sessionInfo.isPrivate && m.sessionInfo.name === groupName) { return m.sessionInfo.id; } diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 95e904f..54c9561 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -50,7 +50,7 @@ export class Memory { this.images = []; } - copy(): Memory { + get copy(): Memory { const m = new Memory(); m.id = this.id; m.vector = [...this.vector]; @@ -68,9 +68,9 @@ export class Memory { /** * 计算记忆的新鲜度衰减因子,越大表示越新鲜 - * @returns 衰减因子(0-1) + * @returns 衰减因子(1→0) */ - calculateDecay() { + get decay() { const now = Math.floor(Date.now() / 1000); const ageInDays = (now - this.createTime) / (24 * 60 * 60); const activityInHours = (now - this.lastMentionTime) / (60 * 60); @@ -152,6 +152,20 @@ export class MemoryManager { } } + get memoryIds() { + return Object.keys(this.memoryMap); + } + + get memoryList() { + return Object.values(this.memoryMap); + } + + get keywords() { + const keywords = new Set(); + this.memoryList.forEach(m => m.keywords.forEach(kw => keywords.add(kw))); + return Array.from(keywords); + } + async addMemory(ctx: seal.MsgContext, ai: AI, ul: UserInfo[], gl: GroupInfo[], kws: string[], text: string) { let id = generateId(), a = 0; while (this.memoryMap.hasOwnProperty(id)) { @@ -163,7 +177,7 @@ export class MemoryManager { } } - for (const id of Object.keys(this.memoryMap)) { + for (const id of this.memoryIds) { const m = this.memoryMap[id]; if (text === m.text && m.sessionInfo.id === ai.id && getCommonUser(ul, m.userList).length > 0 && getCommonGroup(gl, m.groupList).length > 0) { m.keywords = Array.from(new Set([...m.keywords, ...kws])); @@ -193,19 +207,14 @@ export class MemoryManager { this.memoryMap[id] = m; } - delMemory(idList: string[] = [], kws: string[] = []) { - if (idList.length === 0 && kws.length === 0) { - return; - } + deleteMemory(ids: string[] = [], kws: string[] = []) { + if (ids.length === 0 && kws.length === 0) return; - idList.forEach(id => { - delete this.memoryMap?.[id]; - }) + ids.forEach(id => delete this.memoryMap?.[id]) if (kws.length > 0) { for (const id in this.memoryMap) { - const m = this.memoryMap[id]; - if (kws.some(kw => m.keywords.includes(kw))) { + if (kws.some(kw => this.memoryMap[id].keywords.includes(kw))) { delete this.memoryMap[id]; } } @@ -215,12 +224,11 @@ export class MemoryManager { limitMemory() { const { memoryLimit } = ConfigManager.memory; const limit = memoryLimit > 0 ? memoryLimit - 1 : 0; // 预留1个位置用于存储最新记忆 - const memoryList = Object.values(this.memoryMap) - if (memoryList.length <= limit) return; - memoryList.map((m) => { + if (this.memoryList.length <= limit) return; + this.memoryList.map((m) => { return { id: m.id, - score: m.calculateDecay() * m.weight + score: m.decay * m.weight } }) .sort((a, b) => b.score - a.score) // 从大到小排序 @@ -370,8 +378,7 @@ export class MemoryManager { includeImages: false, }) { const { embeddingDimension } = ConfigManager.memory; - const memoryList = Object.values(this.memoryMap); - if (!memoryList.length) return []; + if (!this.memoryList.length) return []; if (query) { try { const queryVector = await getEmbedding(query); @@ -379,15 +386,15 @@ export class MemoryManager { logger.error('查询向量为空'); return []; } - await Promise.all(memoryList.map(async m => { + await Promise.all(this.memoryList.map(async m => { if (m.vector.length !== embeddingDimension) { logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); await m.updateVector(); } })) - return memoryList + return this.memoryList .map(item => { - const m = item.copy(); + const m = item.copy; if (item.keywords.some(kw => query.includes(kw))) m.weight += 10; //提权 return m; }) @@ -412,15 +419,14 @@ export class MemoryManager { includeImages: false, }) { const { isMemoryVector } = ConfigManager.memory; - const memoryList = Object.values(this.memoryMap); - if (!memoryList.length) return []; + if (!this.memoryList.length) return []; if (isMemoryVector && query) { const result = await this.searchByScore(query, options); if (result.length) return result; } - return memoryList + return this.memoryList .map(item => { - const m = item.copy(); + const m = item.copy; if (item.keywords.some(kw => query.includes(kw))) m.weight += 10; //提权 return m; }) @@ -428,7 +434,7 @@ export class MemoryManager { .slice(0, options.topK); } - updateSingleMemoryWeight(s: string, role: 'user' | 'assistant') { + updateMemoryWeight(s: string, role: 'user' | 'assistant') { const increase = role === 'user' ? 1 : 0.1; const decrease = role === 'user' ? 0.1 : 0; const now = Math.floor(Date.now() / 1000); @@ -444,31 +450,15 @@ export class MemoryManager { } } - updateMemoryWeight(ctx: seal.MsgContext, context: Context, s: string, role: 'user' | 'assistant') { - AIManager.getAI(ctx.endPoint.userId).memory.updateSingleMemoryWeight(s, role); - knowledgeMM.updateSingleMemoryWeight(s, role); - this.updateSingleMemoryWeight(s, role); - - if (!ctx.isPrivate) { - // 群内用户的记忆权重更新 - const arr = []; - for (const message of context.messages) { - const uid = message.uid; - if (arr.includes(uid) || message.role !== 'user') { - continue; - } - - const name = message.name; - if (name.startsWith('_')) { - continue; - } - - const ai = AIManager.getAI(uid); - ai.memory.updateSingleMemoryWeight(s, role); - - arr.push(uid); - } - } + updateRelatedMemoryWeight(ctx: seal.MsgContext, context: Context, s: string, role: 'user' | 'assistant') { + // bot记忆权重更新 + AIManager.getAI(ctx.endPoint.userId).memory.updateMemoryWeight(s, role); + // 知识库记忆权重更新 + knowledgeMM.updateMemoryWeight(s, role); + // 会话自身记忆权重更新 + this.updateMemoryWeight(s, role); + // 群内用户的记忆权重更新 + if (!ctx.isPrivate) context.userInfoList.forEach(ui => AIManager.getAI(ui.id).memory.updateMemoryWeight(s, role)); } async getTopMemoryList(lastMsg: string) { @@ -577,11 +567,9 @@ export class MemoryManager { } findImage(id: string): Image | null { - for (const m of Object.values(this.memoryMap)) { + for (const m of this.memoryList) { const image = m.images.find(item => item.id === id); - if (image) { - return image; - } + if (image) return image; } return null; } @@ -713,7 +701,7 @@ export class KnowledgeMemoryManager extends MemoryManager { async buildKnowledgeMemoryPrompt(index: number, context: Context): Promise { await this.updateKnowledgeMemory(index); - if (Object.keys(this.memoryMap).length === 0) return ''; + if (this.memoryIds.length === 0) return ''; const { showNumber } = ConfigManager.message; const { knowledgeMemoryShowNumber, knowledgeMemorySingleShowTemplate } = ConfigManager.memory; diff --git a/src/index.ts b/src/index.ts index d9f9dd5..dc9839d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -582,18 +582,11 @@ ${HELPMAP["权限限制"]}`); if (cmdArgs.at.length > 0 && (cmdArgs.at.length !== 1 || cmdArgs.at[0].userId !== epId)) { ai3 = ai2; } - const { isMemory, isShortMemory } = ConfigManager.memory; - - const keywords = new Set(); - for (const key in ai3.memory.memoryMap) { - ai3.memory.memoryMap[key].keywords.forEach(kw => keywords.add(kw)); - } - seal.replyToSender(ctx, msg, `${ai3.id} 长期记忆开启状态: ${isMemory ? '是' : '否'} -长期记忆条数: ${Object.keys(ai3.memory.memoryMap).length} -关键词库: ${Array.from(keywords).join('、') || '无'} +长期记忆条数: ${ai3.memory.memoryIds.length} +关键词库: ${ai3.memory.keywords.join('、') || '无'} 短期记忆开启状态: ${(isShortMemory && ai3.memory.useShortMemory) ? '是' : '否'} 短期记忆条数: ${ai3.memory.shortMemoryList.length}`); return ret; @@ -633,7 +626,7 @@ ${HELPMAP["权限限制"]}`); seal.replyToSender(ctx, msg, '参数缺失,【.ai memo p del --关键词1 --关键词2】删除个人记忆'); return ret; } - ai2.memory.delMemory(idList, kw); + ai2.memory.deleteMemory(idList, kw); ai2.memory.getTopMemoryList('').then(memoryList => { const s = ai2.memory.buildMemory({ isPrivate: true, @@ -715,7 +708,7 @@ ${HELPMAP["权限限制"]}`); seal.replyToSender(ctx, msg, '参数缺失,【.ai memo g del 】删除群聊记忆'); return ret; } - ai.memory.delMemory(idList, kw); + ai.memory.deleteMemory(idList, kw); ai.memory.getTopMemoryList('').then(memoryList => { const s = ai.memory.buildMemory({ isPrivate: false, diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 038f737..9c4b240 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -173,7 +173,7 @@ export function registerMemory() { } //记忆相关处理 - ai.memory.delMemory(id_list, keywords); + ai.memory.deleteMemory(id_list, keywords); AIManager.saveAI(ai.id); return { content: `删除记忆成功`, images: [] }; From f0ea6f18334cde0e5e6a4929dd9bb57f3aff8dd5 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Mon, 10 Nov 2025 22:08:12 +0800 Subject: [PATCH 10/12] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8D=E5=AD=97=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 好像跑题了 --- src/AI/AI.ts | 5 ++--- src/AI/context.ts | 23 +++++++++++++---------- src/index.ts | 2 +- src/timer.ts | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/AI/AI.ts b/src/AI/AI.ts index d950c46..6f7234a 100644 --- a/src/AI/AI.ts +++ b/src/AI/AI.ts @@ -351,7 +351,7 @@ export class AI { } // 若不在活动时间范围内,返回-1 - get curSegIndex(): number { + get curActiveTimeSegIndex(): number { const now = new Date(); const cur = now.getHours() * 60 + now.getMinutes(); const { start, end, segs } = this.setting.activeTimeInfo; @@ -392,7 +392,7 @@ export class AI { if (segs !== 0 && (start !== 0 || end !== 0)) { const timers = TimerManager.getTimers(this.id, '', ['activeTime']); if (timers.length === 0) { - const curSegIndex = this.curSegIndex; + const curSegIndex = this.curActiveTimeSegIndex; const nextTimePoint = this.getNextTimePoint(curSegIndex); if (nextTimePoint !== -1) { TimerManager.addActiveTimeTimer(ctx, msg, this, nextTimePoint); @@ -410,7 +410,6 @@ export interface UsageInfo { } export class AIManager { - static version = "1.0.1"; static cache: { [key: string]: AI } = {}; static usageMapCache: { [model: string]: { [time: number]: UsageInfo } } = {}; diff --git a/src/AI/context.ts b/src/AI/context.ts index 8ede61f..2c94f53 100644 --- a/src/AI/context.ts +++ b/src/AI/context.ts @@ -81,7 +81,7 @@ export class Context { } async addMessage(ctx: seal.MsgContext, msg: seal.Message, ai: AI, messageArray: MessageSegment[], images: Image[], role: 'user' | 'assistant', msgId: string = '') { - const { showNumber, showMsgId, maxRounds } = ConfigManager.message; + const { showNumber, showMsgId } = ConfigManager.message; const { isShortMemory, shortMemorySummaryRound } = ConfigManager.memory; const messages = this.messages; @@ -124,11 +124,11 @@ export class Context { return; } + const now = Math.floor(Date.now() / 1000); const uid = role == 'user' ? ctx.player.userId : ctx.endPoint.userId; - // 自动更新上下文里的名字 - const exists = messages.some(message => message.uid === uid); - if (!exists) { + // 自动更新上下文里的名字,发言时间一小时内不更新 + if (!messages.some(message => message.uid === uid && message.msgArray.some(msgInfo => msgInfo.time >= now - 3600))) { await this.updateName(ctx.endPoint.userId, ctx.group.groupId, uid); } @@ -162,7 +162,7 @@ export class Context { messages[length - 1].images.push(...images); messages[length - 1].msgArray.push({ msgId: msgId, - time: Math.floor(Date.now() / 1000), + time: now, content: s }); } else { @@ -173,7 +173,7 @@ export class Context { images: images, msgArray: [{ msgId: msgId, - time: Math.floor(Date.now() / 1000), + time: now, content: s }] }; @@ -195,7 +195,7 @@ export class Context { ai.memory.updateRelatedMemoryWeight(ctx, ai.context, s, role); //删除多余的上下文 - this.limitMessages(maxRounds); + this.limitMessages(); } async addToolCallsMessage(tool_calls: ToolCall[]) { @@ -211,6 +211,7 @@ export class Context { } async addToolMessage(tool_call_id: string, s: string, images: Image[]) { + const now = Math.floor(Date.now() / 1000); const message: Message = { role: 'tool', tool_call_id: tool_call_id, @@ -219,7 +220,7 @@ export class Context { images: images, msgArray: [{ msgId: '', - time: Math.floor(Date.now() / 1000), + time: now, content: s }] }; @@ -235,6 +236,7 @@ export class Context { } async addSystemUserMessage(name: string, s: string, images: Image[]) { + const now = Math.floor(Date.now() / 1000); const message: Message = { role: 'user', uid: '', @@ -242,14 +244,15 @@ export class Context { images: images, msgArray: [{ msgId: '', - time: Math.floor(Date.now() / 1000), + time: now, content: s }] }; this.messages.push(message); } - limitMessages(maxRounds: number) { + limitMessages() { + const { maxRounds } = ConfigManager.message; const messages = this.messages; let round = 0; for (let i = messages.length - 1; i >= 0; i--) { diff --git a/src/index.ts b/src/index.ts index dc9839d..2db3256 100644 --- a/src/index.ts +++ b/src/index.ts @@ -405,7 +405,7 @@ ${HELPMAP["权限限制"]}`); text += `\n活跃时间段:${Math.floor(start / 60).toString().padStart(2, '0')}:${(start % 60).toString().padStart(2, '0')}至${Math.floor(end / 60).toString().padStart(2, '0')}:${(end % 60).toString().padStart(2, '0')}`; text += `\n活跃次数:${segs}`; - const curSegIndex = ai.curSegIndex; + const curSegIndex = ai.curActiveTimeSegIndex; const nextTimePoint = ai.getNextTimePoint(curSegIndex); if (nextTimePoint !== -1) { TimerManager.addActiveTimeTimer(ctx, msg, ai, nextTimePoint); diff --git a/src/timer.ts b/src/timer.ts index 293e890..b0fefba 100644 --- a/src/timer.ts +++ b/src/timer.ts @@ -264,7 +264,7 @@ export class TimerManager { const ctx = createCtx(epId, msg); const ai = AIManager.getAI(id); - const curSegIndex = ai.curSegIndex; + const curSegIndex = ai.curActiveTimeSegIndex; const nextTimePoint = ai.getNextTimePoint(curSegIndex); if (curSegIndex === -1) { logger.error(`${id} 不在活跃时间内,触发了 activeTime 定时器,真奇怪\ncurSegIndex:${curSegIndex},setTime:${set},nextTimePoint:${fmtDate(nextTimePoint)}`); From 956c965001f1bb331c9cfa4539457d67679022b3 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 11 Nov 2025 23:18:07 +0800 Subject: [PATCH 11/12] =?UTF-8?q?search=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/AI.ts | 2 +- src/AI/memory.ts | 141 +++++++++++++++++-------------------- src/tool/tool_memory.ts | 64 ++++++++++++++--- src/utils/utils_message.ts | 12 +++- 4 files changed, 128 insertions(+), 91 deletions(-) diff --git a/src/AI/AI.ts b/src/AI/AI.ts index 6f7234a..8666701 100644 --- a/src/AI/AI.ts +++ b/src/AI/AI.ts @@ -411,7 +411,7 @@ export interface UsageInfo { export class AIManager { static cache: { [key: string]: AI } = {}; - static usageMapCache: { [model: string]: { [time: number]: UsageInfo } } = {}; + static usageMapCache: { [model: string]: { [time: number]: UsageInfo } } = null; static get usageMap(): { [model: string]: { [time: number]: UsageInfo } } { if (!this.usageMapCache) { diff --git a/src/AI/memory.ts b/src/AI/memory.ts index 54c9561..9b86fdd 100644 --- a/src/AI/memory.ts +++ b/src/AI/memory.ts @@ -16,6 +16,7 @@ export interface searchOptions { groupList: GroupInfo[]; keywords: string[]; includeImages: boolean; + method: 'weight' | 'similarity' | 'score'; } export class Memory { @@ -370,68 +371,53 @@ export class MemoryManager { } } - async searchByScore(query: string, options: searchOptions = { - topK: 10, - userList: [], - groupList: [], - keywords: [], - includeImages: false, - }) { - const { embeddingDimension } = ConfigManager.memory; - if (!this.memoryList.length) return []; - if (query) { - try { - const queryVector = await getEmbedding(query); - if (!queryVector.length) { - logger.error('查询向量为空'); - return []; - } - await Promise.all(this.memoryList.map(async m => { - if (m.vector.length !== embeddingDimension) { - logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); - await m.updateVector(); - } - })) - return this.memoryList - .map(item => { - const m = item.copy; - if (item.keywords.some(kw => query.includes(kw))) m.weight += 10; //提权 - return m; - }) - .sort((a, b) => { - const bScore = b.weight * b.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); - const aScore = a.weight * a.calculateSimilarity(queryVector, options.userList, options.groupList, options.keywords); - return bScore - aScore; - }) - .slice(0, options.topK); - } catch (e) { - logger.error(`语义搜索失败: ${e.message}`); - } - } - return []; - } - async search(query: string, options: searchOptions = { topK: 10, userList: [], groupList: [], keywords: [], includeImages: false, + method: 'score' }) { - const { isMemoryVector } = ConfigManager.memory; if (!this.memoryList.length) return []; + const { userList: ul, groupList: gl, keywords: kws, includeImages, method } = options; + + const { isMemoryVector, embeddingDimension } = ConfigManager.memory; + let qv: number[] = []; if (isMemoryVector && query) { - const result = await this.searchByScore(query, options); - if (result.length) return result; + qv = await getEmbedding(query); + if (!qv.length) { + logger.error('查询向量为空'); + return []; + } + await Promise.all(this.memoryList.map(async m => { + if (m.vector.length !== embeddingDimension) { + logger.info(`记忆向量维度不匹配,重新获取向量: ${m.id}`); + await m.updateVector(); + } + })) } + return this.memoryList - .map(item => { - const m = item.copy; - if (item.keywords.some(kw => query.includes(kw))) m.weight += 10; //提权 - return m; + .map(m => { + if (includeImages && m.images.length === 0) return null; + const mc = m.copy; + if (mc.keywords.some(kw => query.includes(kw))) mc.weight += 10; //提权 + return mc; }) - .sort((a, b) => b.weight - a.weight) - .slice(0, options.topK); + .filter(m => m) + .sort((a, b) => { + switch (method) { + case 'weight': return b.weight - a.weight; + case 'similarity': return b.calculateSimilarity(qv, ul, gl, kws) - a.calculateSimilarity(qv, ul, gl, kws); + case 'score': { + const aScore = a.weight * a.calculateSimilarity(qv, ul, gl, kws); + const bScore = b.weight * b.calculateSimilarity(qv, ul, gl, kws); + return bScore - aScore; + } + } + }) + .slice(0, options.topK || 10); } updateMemoryWeight(s: string, role: 'user' | 'assistant') { @@ -469,6 +455,7 @@ export class MemoryManager { groupList: [], keywords: [], includeImages: false, + method: 'score' }); } @@ -518,10 +505,7 @@ export class MemoryManager { return template(data) + '\n'; } - async buildMemoryPrompt(ctx: seal.MsgContext, context: Context): Promise { - const userMessages = context.messages.filter(msg => msg.role === 'user' && !msg.name.startsWith('_')); - const lastMsg = userMessages.length > 0 ? userMessages[userMessages.length - 1].msgArray.map(m => m.content).join('') : ''; - + async buildMemoryPrompt(ctx: seal.MsgContext, context: Context, lastMsg: string): Promise { const ai = AIManager.getAI(ctx.endPoint.userId); let s = ai.memory.buildMemory({ isPrivate: true, @@ -544,13 +528,12 @@ export class MemoryManager { }, await ai.memory.getTopMemoryList(lastMsg)); // 群内用户的个人记忆 - const arr = []; - for (const message of userMessages) { - const name = message.name; - const uid = message.uid; - if (arr.includes(uid)) { - continue; - } + const set = new Set(); + for (const ui of context.userInfoList) { + const name = ui.name; + const uid = ui.id; + if (set.has(uid)) continue; + set.add(uid); const ai = AIManager.getAI(uid); s += ai.memory.buildMemory({ @@ -558,8 +541,6 @@ export class MemoryManager { id: uid, name: name }, await ai.memory.getTopMemoryList(lastMsg)); - - arr.push(uid); } return s; @@ -699,22 +680,9 @@ export class KnowledgeMemoryManager extends MemoryManager { this.save(); } - async buildKnowledgeMemoryPrompt(index: number, context: Context): Promise { - await this.updateKnowledgeMemory(index); - if (this.memoryIds.length === 0) return ''; - + buildKnowledgeMemory(memoryList: Memory[]) { const { showNumber } = ConfigManager.message; - const { knowledgeMemoryShowNumber, knowledgeMemorySingleShowTemplate } = ConfigManager.memory; - - const userMessages = context.messages.filter(msg => msg.role === 'user' && !msg.name.startsWith('_')); - const lastMsg = userMessages.length > 0 ? userMessages[userMessages.length - 1].msgArray.map(m => m.content).join('') : ''; - const memoryList = await this.search(lastMsg, { - topK: knowledgeMemoryShowNumber, - userList: [], - groupList: [], - keywords: [], - includeImages: false - }); + const { knowledgeMemorySingleShowTemplate } = ConfigManager.memory; if (memoryList.length === 0) return ''; let prompt = ''; @@ -739,6 +707,23 @@ export class KnowledgeMemoryManager extends MemoryManager { return prompt; } + + async buildKnowledgeMemoryPrompt(index: number, lastMsg: string): Promise { + await this.updateKnowledgeMemory(index); + if (this.memoryIds.length === 0) return ''; + + const { knowledgeMemoryShowNumber } = ConfigManager.memory; + const memoryList = await this.search(lastMsg, { + topK: knowledgeMemoryShowNumber, + userList: [], + groupList: [], + keywords: [], + includeImages: false, + method: 'score' + }); + + return this.buildKnowledgeMemory(memoryList); + } } export const knowledgeMM = new KnowledgeMemoryManager(); diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 9c4b240..6d5f1fd 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -2,7 +2,7 @@ import { AIManager, GroupInfo, SessionInfo, UserInfo } from "../AI/AI"; import { ConfigManager } from "../config/config"; import { createMsg, createCtx } from "../utils/utils_seal"; import { Tool } from "./tool"; -import { searchOptions as SearchOptions } from "../AI/memory"; +import { knowledgeMM, searchOptions as SearchOptions } from "../AI/memory"; export function registerMemory() { const toolAdd = new Tool({ @@ -189,8 +189,8 @@ export function registerMemory() { properties: { memory_type: { type: "string", - description: "记忆类型,个人或群聊", - enum: ["private", "group"] + description: "记忆类型,个人或群聊或知识库,选择知识库时不用填写name", + enum: ["private", "group", "knowledge"] }, name: { type: 'string', @@ -228,14 +228,19 @@ export function registerMemory() { includeImages: { type: 'boolean', description: '是否包含图片' + }, + method: { + type: 'string', + description: '搜索方法,默认score', + enum: ['weight', 'similarity', 'score'] } }, - required: ['memory_type', 'name'] + required: ['memory_type'] } } }); toolSearch.solve = async (ctx, msg, ai, args) => { - const { memory_type, name, query = '', topK = 5, keywords = [], userList = [], groupList = [], includeImages = false } = args; + const { memory_type, name = '', query = '', topK = 5, keywords = [], userList = [], groupList = [], includeImages = false, method = 'score' } = args; let si: SessionInfo = { isPrivate: false, @@ -272,10 +277,51 @@ export function registerMemory() { id: gid, name: name } + } else if (memory_type === "knowledge") { + const giList: GroupInfo[] = []; + for (const n of groupList) { + const gid = await ai.context.findGroupId(ctx, n); + if (gid !== null) { + giList.push({ + isPrivate: false, + id: gid, + name: n + }); + } + } + + const options: SearchOptions = { + topK: topK, + keywords: keywords, + userList: userList, + groupList: groupList, + includeImages: includeImages, + method: method + } + + const { roleSettingNames, roleSettingTemplate } = ConfigManager.message; + const [roleName, exists] = seal.vars.strGet(ctx, "$gSYSPROMPT"); + let roleIndex = 0; + if (exists && roleName !== '' && roleSettingNames.includes(roleName)) { + roleIndex = roleSettingNames.indexOf(roleName); + if (roleIndex < 0 || roleIndex >= roleSettingTemplate.length) roleIndex = 0; + } else { + const [roleIndex2, exists2] = seal.vars.intGet(ctx, "$gSYSPROMPT"); + if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) roleIndex = roleIndex2; + } + await knowledgeMM.updateKnowledgeMemory(roleIndex); + if (knowledgeMM.memoryIds.length === 0) return { content: `暂无记忆`, images: [] }; + + const memoryList = await knowledgeMM.search(query, options); + const images = Array.from(new Set([].concat(...memoryList.map(m => m.images)))); + + return { content: knowledgeMM.buildKnowledgeMemory(memoryList) || '暂无记忆', images: images }; } else { return { content: `未知的记忆类型<${memory_type}>`, images: [] }; } + if (ai.memory.memoryIds.length === 0) return { content: `暂无记忆`, images: [] }; + const uiList: UserInfo[] = []; for (const n of userList) { const uid = await ai.context.findUserId(ctx, n, true); @@ -304,16 +350,14 @@ export function registerMemory() { keywords: keywords, userList: userList, groupList: groupList, - includeImages: includeImages + includeImages: includeImages, + method: method } const memoryList = await ai.memory.search(query, options); const images = Array.from(new Set([].concat(...memoryList.map(m => m.images)))); - return { - content: ai.memory.buildMemory(si, memoryList) || '暂无记忆', - images: images - }; + return { content: ai.memory.buildMemory(si, memoryList) || '暂无记忆', images: images }; } const toolClear = new Tool({ diff --git a/src/utils/utils_message.ts b/src/utils/utils_message.ts index c0dd039..9f782fa 100644 --- a/src/utils/utils_message.ts +++ b/src/utils/utils_message.ts @@ -12,6 +12,8 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< const { isTool, usePromptEngineering } = ConfigManager.tool; const { localImagePaths, receiveImage, condition } = ConfigManager.image; const { isMemory, isShortMemory } = ConfigManager.memory; + + // 可发送的图片提示 const sandableImagesPrompt: string = localImagePaths .map(path => { if (path.trim() === '') return null; @@ -29,6 +31,8 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< .map((prompt, index) => `${index + 1}. ${prompt}`) .join('\n'); + + // 角色设定 const [roleName, exists] = seal.vars.strGet(ctx, "$gSYSPROMPT"); let roleIndex = 0; if (exists && roleName !== '' && roleSettingNames.includes(roleName)) { @@ -39,10 +43,14 @@ export async function buildSystemMessage(ctx: seal.MsgContext, ai: AI): Promise< if (exists2 && roleIndex2 >= 0 && roleIndex2 < roleSettingTemplate.length) roleIndex = roleIndex2; } + // 获取lastMsg + const userMessages = ai.context.messages.filter(msg => msg.role === 'user' && !msg.name.startsWith('_')); + const lastMsg = userMessages.length > 0 ? userMessages[userMessages.length - 1].msgArray.map(m => m.content).join('') : ''; + // 知识库 - const knowledgePrompt = await knowledgeMM.buildKnowledgeMemoryPrompt(roleIndex, ai.context); + const knowledgePrompt = await knowledgeMM.buildKnowledgeMemoryPrompt(roleIndex, lastMsg); // 记忆 - const memoryPrompt = isMemory ? await ai.memory.buildMemoryPrompt(ctx, ai.context) : ''; + const memoryPrompt = isMemory ? await ai.memory.buildMemoryPrompt(ctx, ai.context, lastMsg) : ''; // 短期记忆 const shortMemoryPrompt = isShortMemory && ai.memory.useShortMemory ? ai.memory.shortMemoryList.map((item, index) => `${index + 1}. ${item}`).join('\n') : ''; // 调用函数 From 356a3e6a4c724119bfd2f8a815de6d0642d527d6 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 11 Nov 2025 23:24:34 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E8=B0=83=E6=95=B4tool=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tool/tool_memory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tool/tool_memory.ts b/src/tool/tool_memory.ts index 6d5f1fd..15e2c4d 100644 --- a/src/tool/tool_memory.ts +++ b/src/tool/tool_memory.ts @@ -231,7 +231,7 @@ export function registerMemory() { }, method: { type: 'string', - description: '搜索方法,默认score', + description: '搜索方法,默认similarity', enum: ['weight', 'similarity', 'score'] } }, @@ -240,7 +240,7 @@ export function registerMemory() { } }); toolSearch.solve = async (ctx, msg, ai, args) => { - const { memory_type, name = '', query = '', topK = 5, keywords = [], userList = [], groupList = [], includeImages = false, method = 'score' } = args; + const { memory_type, name = '', query = '', topK = 5, keywords = [], userList = [], groupList = [], includeImages = false, method = 'similarity' } = args; let si: SessionInfo = { isPrivate: false,