mirror of
https://github.com/Jerryplusy/crystelf-plugin.git
synced 2025-12-05 15:41:56 +00:00
fix:memory
This commit is contained in:
parent
5aecaebfdc
commit
75b6d1a348
@ -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;
|
||||
|
||||
@ -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<null|string>}
|
||||
*/
|
||||
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)}`;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user