From 75b6d1a3485811547e3cdff5f14c27a23a248bbf Mon Sep 17 00:00:00 2001 From: Jerrypluay Date: Wed, 22 Oct 2025 13:49:44 +0800 Subject: [PATCH] fix:memory --- apps/ai.js | 2 +- lib/ai/memorySystem.js | 172 ++++++++++++++++---------------------- lib/ai/responseHandler.js | 44 ++-------- 3 files changed, 81 insertions(+), 137 deletions(-) diff --git a/apps/ai.js b/apps/ai.js index 86e9b7e..6f0a769 100644 --- a/apps/ai.js +++ b/apps/ai.js @@ -248,7 +248,7 @@ async function callAiForResponse(userMessage, e, aiConfig) { return null; } //搜索相关记忆 - const memories = await MemorySystem.searchMemories(e.user_id,[userMessage], 5); + const memories = await MemorySystem.searchMemories(e.user_id,e.msg||'',5); logger.info(`[crystelf-ai] ${memories}`) //构建聊天历史 const historyLen = aiConfig.chatHistory; diff --git a/lib/ai/memorySystem.js b/lib/ai/memorySystem.js index 02fef21..cd95b74 100644 --- a/lib/ai/memorySystem.js +++ b/lib/ai/memorySystem.js @@ -5,50 +5,42 @@ import path from 'path'; class MemorySystem { constructor() { this.baseDir = path.join(process.cwd(), 'data', 'crystelf', 'memories'); - this.memories = new Map(); + this.memories = new Map();//缓存个别加载的记忆 this.defaultTimeout = 30; } - async init() { try { const config = await ConfigControl.get('ai'); this.defaultTimeout = config?.timeout || 30; - await this.loadAllMemories(); - await this.cleanExpiredMemories(); + if (!fs.existsSync(this.baseDir)) { + fs.mkdirSync(this.baseDir, { recursive: true }); + } + logger.info('[crystelf-ai] 记忆系统初始化完成'); } catch (error) { logger.error(`[crystelf-ai] 记忆系统初始化失败: ${error.message}`); } } - async loadAllMemories() { + /** + * 动态加载单个用户的记忆 + */ + async loadUserMemories(groupId, userId) { try { - if (!fs.existsSync(this.baseDir)) { - fs.mkdirSync(this.baseDir, { recursive: true }); + const filePath = path.join(this.baseDir, String(groupId), `${String(userId)}.json`); + if (!fs.existsSync(filePath)) return {}; + const data = await fs.promises.readFile(filePath, 'utf8'); + const json = JSON.parse(data || '{}'); + for (const [key, memory] of Object.entries(json)) { + this.memories.set(`${groupId}_${userId}_${key}`, memory); } - - const groupDirs = fs.readdirSync(this.baseDir); - for (const groupId of groupDirs) { - const groupPath = path.join(this.baseDir, String(groupId)); - if (!fs.statSync(groupPath).isDirectory()) continue; - - const userFiles = fs.readdirSync(groupPath); - for (const file of userFiles) { - if (!file.endsWith('.json')) continue; - const userId = path.basename(file, '.json'); - const filePath = path.join(groupPath, file); - const data = fs.readFileSync(filePath, 'utf8'); - const memoriesData = JSON.parse(data || '{}'); - for (const [key, memory] of Object.entries(memoriesData)) { - this.memories.set(`${groupId}_${userId}_${key}`, memory); - } - } - } - logger.info(`[crystelf-ai] 加载了 ${this.memories.size} 条记忆`); + return json; } catch (error) { - logger.error(`[crystelf-ai] 加载记忆失败: ${error.message}`); + logger.error(`[crystelf-ai] 加载用户记忆失败(${groupId}/${userId}): ${error.message}`); + return {}; } } + //保存指定用户记忆 async saveMemories(groupId, userId) { try { const groupPath = path.join(this.baseDir, String(groupId)); @@ -56,7 +48,6 @@ class MemorySystem { if (!fs.existsSync(groupPath)) { fs.mkdirSync(groupPath, { recursive: true }); } - const userMemories = {}; for (const [key, memory] of this.memories) { if (key.startsWith(`${groupId}_${userId}_`)) { @@ -64,14 +55,22 @@ class MemorySystem { userMemories[memoryId] = memory; } } - - fs.writeFileSync(filePath, JSON.stringify(userMemories, null, 2)); + await fs.promises.writeFile(filePath, JSON.stringify(userMemories, null, 2), 'utf8'); logger.info(`[crystelf-ai] 记忆已保存到 ${groupId}/${userId}.json`); } catch (error) { logger.error(`[crystelf-ai] 保存记忆失败: ${error.message}`); } } + /** + * 添加新的记忆 + * @param groupId + * @param userId + * @param data 内容 + * @param keywords 关键词数组 + * @param timeout 超时 + * @returns {Promise} + */ async addMemory(groupId, userId, data, keywords = [], timeout = null) { try { const memoryId = this.generateMemoryId(); @@ -87,7 +86,6 @@ class MemorySystem { }; this.memories.set(`${groupId}_${userId}_${memoryId}`, memory); await this.saveMemories(groupId, userId); - logger.info(`[crystelf-ai] 添加新记忆: ${groupId}/${userId}/${memoryId}`); return memoryId; } catch (error) { @@ -96,47 +94,40 @@ class MemorySystem { } } - async searchMemories(userId, keywords = [], limit = 10) { + /** + * 搜索记忆 + * @param userId 用户id + * @param input 输入 + * @param limit 最大记忆 + * @returns {Promise<*[]>} + */ + async searchMemories(userId, input = '', limit = 10) { try { + if(input === '') return null; + const keywords = this.extractKeywords(input); const results = []; const now = Date.now(); - let searchText = ''; - if (keywords.length === 1 && keywords[0].length > 6) { - searchText = keywords[0].toLowerCase(); - const words = searchText.match(/[\u4e00-\u9fa5]{1,2}|[a-zA-Z0-9]+/g) || []; - keywords = Array.from(new Set(words.filter(w => w.length > 1))); - } - const userMemories = []; - for (const [key, memory] of this.memories) { - const parts = key.split('_'); - if (parts.length < 3) continue; - const uid = parts[1]; - if (uid !== userId) continue; - if (now > memory.expireAt) continue; - userMemories.push(memory); - } - if (userMemories.length === 0) return []; - for (const memory of userMemories) { - let matchScore = 0; - for (const kw of keywords) { - if (memory.keywords.some(k => k.includes(kw) || kw.includes(k))) matchScore += 10; - else if (memory.data.includes(kw)) matchScore += 5; - } - if (searchText) { - for (const mk of memory.keywords) { - if (searchText.includes(mk)) matchScore += 8; + //遍历所有群聊目录 + const groupDirs = fs.existsSync(this.baseDir) ? fs.readdirSync(this.baseDir) : []; + for (const groupId of groupDirs) { + const filePath = path.join(this.baseDir, String(groupId), `${String(userId)}.json`); + if (!fs.existsSync(filePath)) continue; + const data = await fs.promises.readFile(filePath, 'utf8'); + const json = JSON.parse(data || '{}'); + for (const memory of Object.values(json)) { + if (now > memory.expireAt) continue; // 跳过过期 + const matchScore = this.calculateMatchScore(memory, keywords); + if (matchScore > 0) { + memory.accessCount = (memory.accessCount || 0) + 1; + memory.lastAccessed = now; + results.push({ + id: memory.id, + data: memory.data, + keywords: memory.keywords, + relevance: matchScore + }); } } - if (matchScore > 0) { - memory.accessCount++; - memory.lastAccessed = now; - results.push({ - id: memory.id, - data: memory.data, - keywords: memory.keywords, - relevance: matchScore + this.calculateRelevance(memory, keywords) - }); - } } results.sort((a, b) => b.relevance - a.relevance); return results.slice(0, limit); @@ -146,44 +137,29 @@ class MemorySystem { } } - calculateRelevance(memory, keywords) { + //提取关键词 + extractKeywords(text) { + if (!text) return []; + text = text.toLowerCase(); + const words = text.match(/[\u4e00-\u9fa5]{1,2}|[a-zA-Z0-9]+/g) || []; + return Array.from(new Set(words.filter(w => w.length > 0))); + } + + //记忆匹配分数 + calculateMatchScore(memory, keywords) { let score = 0; - for (const keyword of keywords) { - if (memory.keywords.includes(keyword)) { - score += 10; - } - if (memory.data.includes(keyword)) { - score += 5; + const text = (memory.data || '').toLowerCase(); + for (const kw of keywords) { + for (const mk of memory.keywords || []) { + if (mk.includes(kw) || kw.includes(mk)) score += 10; } + if (text.includes(kw)) score += 6; } - score += Math.min(memory.accessCount * 0.1, 5); + score += Math.min((memory.accessCount || 0) * 0.2, 5); const daysSinceCreated = (Date.now() - memory.createdAt) / (24 * 60 * 60 * 1000); score += Math.max(10 - daysSinceCreated * 0.1, 0); return score; } - - async cleanExpiredMemories() { - try { - const now = Date.now(); - let cleanedCount = 0; - - for (const [memoryKey, memory] of this.memories) { - if (now > memory.expireAt) { - this.memories.delete(memoryKey); - cleanedCount++; - const [groupId, userId] = memoryKey.split('_'); - await this.saveMemories(groupId, userId); - } - } - - if (cleanedCount > 0) { - logger.info(`[crystelf-ai] 清理了 ${cleanedCount} 条过期记忆`); - } - } catch (error) { - logger.error(`[crystelf-ai] 清理过期记忆失败: ${error.message}`); - } - } - generateMemoryId() { return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } diff --git a/lib/ai/responseHandler.js b/lib/ai/responseHandler.js index 4b4e5d9..8882419 100644 --- a/lib/ai/responseHandler.js +++ b/lib/ai/responseHandler.js @@ -86,8 +86,6 @@ class ResponseHandler { return null; case 'recall': return this.handleRecallMessage(message); - case 'function': - return this.handleFunctionMessage(message); default: return this.handleNormalMessage(message); } @@ -114,28 +112,7 @@ class ResponseHandler { if (!validTypes.includes(message.type)) { logger.info(`[crystelf-ai] ai返回未知的type类型:${message.type}`) return false; - } - /** - switch (message.type) { - case 'message': - case 'code': - case 'markdown': - case 'meme': - case 'ai-record': - case 'file': - case 'memory': - case 'at': - case 'poke': - case 'emoji-like': - case 'like': - return !!message.id; - case 'recall': - return !!message.seq; - case 'function': - return !!(message.data && message.data.name); - default: - return true; - }*/return true; + }return true; } /** @@ -169,20 +146,6 @@ class ResponseHandler { }; } - /** - * 函数调用消息 - * @param message 函数消息 - * @returns {{type: string, data}} - */ - handleFunctionMessage(message) { - // TOdO 具体的函数调用逻辑 - logger.info(`[crystelf-ai] 函数调用: ${message.data.name}(${message.data.params?.join(', ') || ''})`); - return { - type: 'function', - data: message.data - }; - } - //普通消息 handleNormalMessage(message) { // 设置默认值 @@ -202,6 +165,11 @@ class ResponseHandler { return processedMessage; } + //对上下文消息进行处理 + handleChatHistory(message) { + let messageToHistory = []; + } + createErrorResponse(error) { return [{ type: 'message',