diff --git a/apps/ai.js b/apps/ai.js index 9b65037..c545761 100644 --- a/apps/ai.js +++ b/apps/ai.js @@ -102,7 +102,9 @@ function extractUserMessage(msg, nickname,e) { const regex = new RegExp(`^${nickname}\\s*([\\s\\S]*)?$`); const match = msg.match(regex); if (match && match[1]) { - return match[1].trim(); + let message = match[1].trim(); + message = `[${e.sender?.nickname},id:${e.user_id}]说:${message}`; + return message; } else { if(e.message){ let text; @@ -111,7 +113,7 @@ function extractUserMessage(msg, nickname,e) { text = message.text; } }) - if(text) return text; + if(text) return `[${e.sender?.nickname},id:${e.user_id}]说:${text}`; } } logger.warn('[crystelf-ai] 字符串匹配失败,使用空字符串操作'); @@ -210,10 +212,11 @@ async function callAiForResponse(userMessage, e, aiConfig) { return null; } //搜索相关记忆 - const memories = await MemorySystem.searchMemories([userMessage], 5); + const memories = await MemorySystem.searchMemories(e.user_id,[userMessage], 5); //构建聊天历史 - const chatHistory = session.chatHistory.slice(-10); - const aiResult = await AiCaller.callAi(userMessage, chatHistory, memories); + const historyLen = aiConfig.chatHistory; + const chatHistory = session.chatHistory.slice(-historyLen|-10); + const aiResult = await AiCaller.callAi(userMessage, chatHistory, memories,e); if (!aiResult.success) { logger.error(`[crystelf-ai] AI调用失败: ${aiResult.error}`); return [ @@ -227,8 +230,10 @@ async function callAiForResponse(userMessage, e, aiConfig) { const processedResponse = await ResponseHandler.processResponse( aiResult.response, userMessage, - e.group_id + e.group_id, + e.user_id ); + //更新session const newChatHistory = [ ...chatHistory, diff --git a/config/ai.json b/config/ai.json index f5f3773..23dbb26 100644 --- a/config/ai.json +++ b/config/ai.json @@ -28,6 +28,8 @@ "timeout": 30, "?maxSessions": "最大同时存在的sessions群聊数量", "maxSessions": 10, + "?chatHistory": "聊天上下文最大长度", + "chatHistory": 10, "?keywordCache": "是否缓存关键词到本地", "keywordCache": true, "?pinyinMatch": "是否启用拼音匹配", diff --git a/lib/ai/aiCaller.js b/lib/ai/aiCaller.js index 74a1874..6b0306c 100644 --- a/lib/ai/aiCaller.js +++ b/lib/ai/aiCaller.js @@ -43,9 +43,10 @@ class AiCaller { * @param prompt 用户输入 * @param chatHistory 聊天历史 * @param memories 记忆 + * @param e * @returns {Promise<{success: boolean, response: (*|string), rawResponse: (*|string)}|{success: boolean, error: string}|{success: boolean, error}>} */ - async callAi(prompt, chatHistory = [], memories = []) { + async callAi(prompt, chatHistory = [], memories = [],e) { if (!this.isInitialized || !this.config) { logger.error('[crystelf-ai] 未初始化或配置无效'); return { success: false, error: 'AI调用器未初始化' }; @@ -59,7 +60,7 @@ class AiCaller { chatHistory: chatHistory, model: this.config.modelType, temperature: this.config.temperature, - customPrompt: await this.getSystemPrompt(), + customPrompt: await this.getSystemPrompt(e), }); if (result.success) { @@ -89,9 +90,10 @@ class AiCaller { * @param chatHistory 聊天记录 * @param memories 记忆 * @param onChunk 流式数据回调函数 + * @param e * @returns {Promise} */ - async callAiStream(prompt, chatHistory = [], memories = [], onChunk = null) { + async callAiStream(prompt, chatHistory = [], memories = [], onChunk = null,e) { if (!this.isInitialized || !this.config) { logger.error('[crystelf-ai] 未初始化或配置无效'); return { success: false, error: 'AI调用器未初始化' }; @@ -99,7 +101,7 @@ class AiCaller { if (!this.config.stream) { logger.warn('[crystelf-ai] 流式输出未启用,使用普通调用'); - return await this.callAi(prompt, chatHistory, memories); + return await this.callAi(prompt, chatHistory, memories,e); } try { @@ -137,27 +139,60 @@ class AiCaller { */ buildPrompt(prompt, memories = []) { let fullPrompt = ''; - // TODO 加入标准信息 if (memories && memories.length > 0) { - fullPrompt += '相关记忆:\n'; + fullPrompt += '你可能会用到的记忆,请按情况使用,如果不合语境请忽略:\n'; memories.forEach((memory, index) => { - fullPrompt += `${index + 1}. ${memory.data}\n`; + fullPrompt += `${index + 1}. 关键词:${memory.keywords},内容:${memory.data}\n`; }); fullPrompt += '\n'; } - fullPrompt += `用户说: ${prompt}\n`; - fullPrompt += '请根据以上信息进行回复:\n'; + fullPrompt += `以下是用户说的内容,会以[用户昵称,用户qq号]的形式给你,但是请注意,你回复message块的时候不需要带[]以及里面的内容,正常回复你想说的话即可:\n${prompt}\n`; return fullPrompt; } /** * 获取系统提示词 + * @param {object} e 上下文事件对象 * @returns {Promise} 系统提示词 */ - async getSystemPrompt() { - return this.config?.stream ? await getStreamSystemPrompt() : await getSystemPrompt(); + async getSystemPrompt(e) { + try { + const basePrompt = this.config?.stream + ? await getStreamSystemPrompt() + : await getSystemPrompt(); + const config = await ConfigControl.get(); + const botInfo = { + id: e.bot?.uin || '未知', + name: config?.profile?.nickName || '晶灵' + }; + + const userInfo = { + id: e.user_id || e.sender?.user_id || '未知', + name: e.sender?.card || e.sender?.nickname || '用户', + isMaster: e.isMaster, + }; + const contextIntro = [ + `以下是当前对话的上下文信息(仅供你理解对话背景,请勿泄露):`, + `[你的信息]`, + `- 你的昵称:${botInfo.name}`, + `- 你的qq号:${botInfo.id}`, + ``, + `[跟你对话的用户的信息]`, + `- 他的名字:${userInfo.name}`, + `- 他的qq号(id):${userInfo.id}`, + `- 他${userInfo.isMaster ? '是':'不是'}你的主人`, + ``, + `请基于以上上下文进行理解,这些信息是当你需要的时候使用的,绝对不能泄露这些信息,也不能主动提起`, + ``, + ].join('\n'); + return `${contextIntro}${basePrompt}`; + } catch (error) { + logger.error(`[crystelf-ai] 生成系统提示词失败: ${error.message}`); + return await getSystemPrompt(); + } } + /** * 将回复分割成多个块用于流式输出 * @param {string} response 完整回复 diff --git a/lib/ai/memorySystem.js b/lib/ai/memorySystem.js index b373f61..f11f894 100644 --- a/lib/ai/memorySystem.js +++ b/lib/ai/memorySystem.js @@ -4,7 +4,7 @@ import path from 'path'; class MemorySystem { constructor() { - this.memoryFile = path.join(process.cwd(), 'data', 'crystelf', 'ai_memory.json'); + this.baseDir = path.join(process.cwd(), 'data', 'crystelf', 'memories'); this.memories = new Map(); // 内存中的记忆存储 this.defaultTimeout = 30; // 默认超时时间(天) } @@ -13,40 +13,60 @@ class MemorySystem { try { const config = await ConfigControl.get('ai'); this.defaultTimeout = config?.timeout || 30; - await this.loadMemories(); + await this.loadAllMemories(); await this.cleanExpiredMemories(); } catch (error) { logger.error(`[crystelf-ai] 记忆系统初始化失败: ${error.message}`); } - } // TODO 群聊id/用户id分组保存 + } - async loadMemories() { + async loadAllMemories() { try { - if (fs.existsSync(this.memoryFile)) { - const data = fs.readFileSync(this.memoryFile, 'utf8'); - const memoriesData = JSON.parse(data); - for (const [key, memory] of Object.entries(memoriesData)) { - this.memories.set(key, memory); - } - logger.info(`[crystelf-ai] 加载了 ${this.memories.size} 条记忆`); - } else { - const memoryDir = path.dirname(this.memoryFile); - if (!fs.existsSync(memoryDir)) { - fs.mkdirSync(memoryDir, { recursive: true }); - } - fs.writeFileSync(this.memoryFile, '{}'); - logger.info('[crystelf-ai] 创建新的记忆文件'); + if (!fs.existsSync(this.baseDir)) { + fs.mkdirSync(this.baseDir, { recursive: true }); } + + const groupDirs = fs.readdirSync(this.baseDir); + for (const groupId of groupDirs) { + const groupPath = path.join(this.baseDir, 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} 条记忆`); } catch (error) { logger.error(`[crystelf-ai] 加载记忆失败: ${error.message}`); } } - async saveMemories() { + async saveMemories(groupId, userId) { try { - const memoriesData = Object.fromEntries(this.memories); - fs.writeFileSync(this.memoryFile, JSON.stringify(memoriesData, null, 2)); - logger.info('[crystelf-ai] 记忆已保存到文件'); + const groupPath = path.join(this.baseDir, groupId); + const filePath = path.join(groupPath, `${userId}.json`); + if (!fs.existsSync(groupPath)) { + fs.mkdirSync(groupPath, { recursive: true }); + } + + const userMemories = {}; + for (const [key, memory] of this.memories) { + if (key.startsWith(`${groupId}_${userId}_`)) { + const memoryId = key.split(`${groupId}_${userId}_`)[1]; + userMemories[memoryId] = memory; + } + } + + fs.writeFileSync(filePath, JSON.stringify(userMemories, null, 2)); + logger.info(`[crystelf-ai] 记忆已保存到 ${groupId}/${userId}.json`); } catch (error) { logger.error(`[crystelf-ai] 保存记忆失败: ${error.message}`); } @@ -54,12 +74,14 @@ class MemorySystem { /** * 添加记忆 + * @param groupId 群聊id + * @param userId 用户id * @param data 内容 * @param keywords 关键词 * @param timeout 超时时间 * @returns {Promise} */ - async addMemory(data, keywords = [], timeout = null) { + async addMemory(groupId, userId, data, keywords = [], timeout = null) { try { const memoryId = this.generateMemoryId(); const expireTime = timeout || this.defaultTimeout; @@ -72,10 +94,10 @@ class MemorySystem { accessCount: 0, lastAccessed: Date.now() }; - this.memories.set(memoryId, memory); - await this.saveMemories(); + this.memories.set(`${groupId}_${userId}_${memoryId}`, memory); + await this.saveMemories(groupId, userId); - logger.info(`[crystelf-ai] 添加新记忆: ${memoryId}`); + logger.info(`[crystelf-ai] 添加新记忆: ${groupId}/${userId}/${memoryId}`); return memoryId; } catch (error) { logger.error(`[crystelf-ai] 添加记忆失败: ${error.message}`); @@ -85,35 +107,52 @@ class MemorySystem { /** * 搜索记忆 + * @param userId 用户id * @param keywords 关键词 * @param limit 数量限制 * @returns {Promise<*[]>} */ - async searchMemories(keywords = [], limit = 10) { + async searchMemories(userId, keywords = [], limit = 10) { try { const results = []; - - for (const [memoryId, memory] of this.memories) { - if (Date.now() > memory.expireAt) { - continue; + 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 (keywords.length > 0) { - const hasMatch = keywords.some(keyword => - memory.keywords.includes(keyword) || - memory.data.includes(keyword) - ); - if (!hasMatch) { - continue; + if (searchText) { + for (const mk of memory.keywords) { + if (searchText.includes(mk)) matchScore += 8; } } - memory.accessCount++; - memory.lastAccessed = Date.now(); - results.push({ - id: memory.id, - data: memory.data, - keywords: memory.keywords, - relevance: this.calculateRelevance(memory, keywords) - }); + 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); @@ -123,6 +162,7 @@ class MemorySystem { } } + calculateRelevance(memory, keywords) { let score = 0; for (const keyword of keywords) { @@ -147,14 +187,16 @@ class MemorySystem { const now = Date.now(); let cleanedCount = 0; - for (const [memoryId, memory] of this.memories) { + for (const [memoryKey, memory] of this.memories) { if (now > memory.expireAt) { - this.memories.delete(memoryId); + this.memories.delete(memoryKey); cleanedCount++; + const [groupId, userId] = memoryKey.split('_'); + await this.saveMemories(groupId, userId); } } + if (cleanedCount > 0) { - await this.saveMemories(); logger.info(`[crystelf-ai] 清理了 ${cleanedCount} 条过期记忆`); } } catch (error) { diff --git a/lib/ai/responseHandler.js b/lib/ai/responseHandler.js index 9a1c747..808cfb5 100644 --- a/lib/ai/responseHandler.js +++ b/lib/ai/responseHandler.js @@ -13,10 +13,11 @@ class ResponseHandler { * 处理ai响应 * @param rawResponse ai原始回复 * @param userMessage 用户消息 - * @param groupId 群聊ai + * @param groupId 群聊id + * @param user_id 用户id * @returns {Promise<[{type: string, data: string, at: boolean, quote: boolean, recall: number}]|Array|*[]>} */ - async processResponse(rawResponse, userMessage, groupId) { + async processResponse(rawResponse, userMessage, groupId,user_id) { try { const parsedResponse = this.parseAiResponse(rawResponse); if (!parsedResponse.success) { @@ -26,7 +27,7 @@ class ResponseHandler { const messages = parsedResponse.messages; const processedMessages = []; for (const message of messages) { - const processedMessage = await this.processMessage(message, userMessage, groupId); + const processedMessage = await this.processMessage(message, userMessage, groupId,user_id); if (processedMessage) { processedMessages.push(processedMessage); } @@ -73,15 +74,15 @@ class ResponseHandler { return cleaned; } - async processMessage(message, userMessage, groupId) { + async processMessage(message, userMessage, groupId,userId) { try { if (!this.validateMessage(message)) { - logger.warn(`[响应处理器] 无效消息格式: ${JSON.stringify(message)}`); + logger.warn(`[crystelf-ai] 无效消息格式: ${JSON.stringify(message)}`); return null; } switch (message.type) { case 'memory': - await this.handleMemoryMessage(message, groupId); + await this.handleMemoryMessage(message, groupId,userId); return null; case 'recall': return this.handleRecallMessage(message); @@ -137,11 +138,14 @@ class ResponseHandler { * 记忆消息 * @param message 记忆 * @param groupId 群聊id + * @param user_id 用户id * @returns {Promise} */ - async handleMemoryMessage(message, groupId) { + async handleMemoryMessage(message, groupId,user_id) { try { const memoryId = await this.memorySystem.addMemory( + groupId, + user_id, message.data, message.key || [], message.timeout || 30