Compare commits

..

No commits in common. "7489cf0197e3657d48286d00e74ad6ac9261a2c5" and "5aecaebfdc7be3446603f8f584549e521eb68f2d" have entirely different histories.

5 changed files with 191 additions and 250 deletions

View File

@ -110,40 +110,32 @@ async function index(e) {
} }
} }
async function extractUserMessage(msg, nickname, e) { function extractUserMessage(msg, nickname,e) {
if (e.message) { if(e.message){
let text = []; let text = [];
let at = []; let at = [];
e.message.forEach(message => { e.message.forEach(message=>{
logger.info(message); logger.info(message);
if (message.type === 'text') { if(message.type === 'text'){
text.push(message.text); text.push(message.text);
} else if (message.type === 'at') { }
at.push(message.qq); else if(message.type === 'at'){
} at.push(message.qq);
}) }
let returnMessage = '';
if (text.length > 0) {
text.forEach(message => {
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]说:${message}\n`;
}) })
let returnMessage = '';
if(text.length > 0){
text.forEach(message=>{
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]说:${message}\n`;
})
}
if(at.length > 0){
at.forEach((at)=>{
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]@(at)了一个人,id是${at}\n`;
});
}
return returnMessage;
} }
if (at.length > 0) {
at.forEach((at) => {
if(at === e.bot.uin){
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]@(at)了你,你的id是${at}\n`;
}
else{
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]@(at)了一个人,id是${at}\n`;
}
});
}
const imgUrls = await YunzaiUtils.getImages(e, 1, true);
if(imgUrls){
returnMessage += `[${e.sender?.nickname},id:${e.user_id}]发送了一张图片(你可能暂时无法查看)\n`;
}
return returnMessage;
}
logger.warn('[crystelf-ai] 字符串匹配失败,使用空字符串操作'); logger.warn('[crystelf-ai] 字符串匹配失败,使用空字符串操作');
return ''; return '';
} }
@ -226,17 +218,12 @@ async function handleMixMode(userMessage, e, aiConfig) {
recall: 0, recall: 0,
}, },
]; ];
let resMessage = { let resToSave = res;
type: 'message', resToSave.data += '[词库预设消息]';
data: matchResult.text + ' [词库预设消息]',
at: false,
quote: false,
recall: 0,
};
const newChatHistory = [ const newChatHistory = [
...chatHistory, ...chatHistory,
{ role: 'user', content: userMessage }, { role: 'user', content: userMessage },
{ role: 'assistant', content: JSON.stringify(resMessage) }, { role: 'assistant', content: resToSave },
]; ];
SessionManager.updateChatHistory(e.group_id, newChatHistory); SessionManager.updateChatHistory(e.group_id, newChatHistory);
SessionManager.deactivateSession(e.group_id,e.user_id); SessionManager.deactivateSession(e.group_id,e.user_id);
@ -261,7 +248,7 @@ async function callAiForResponse(userMessage, e, aiConfig) {
return null; return null;
} }
//搜索相关记忆 //搜索相关记忆
const memories = await MemorySystem.searchMemories(e.user_id,e.msg||'',5); const memories = await MemorySystem.searchMemories(e.user_id,[userMessage], 5);
logger.info(`[crystelf-ai] ${memories}`) logger.info(`[crystelf-ai] ${memories}`)
//构建聊天历史 //构建聊天历史
const historyLen = aiConfig.chatHistory; const historyLen = aiConfig.chatHistory;

View File

@ -5,42 +5,50 @@ import path from 'path';
class MemorySystem { class MemorySystem {
constructor() { constructor() {
this.baseDir = path.join(process.cwd(), 'data', 'crystelf', 'memories'); this.baseDir = path.join(process.cwd(), 'data', 'crystelf', 'memories');
this.memories = new Map();//缓存个别加载的记忆 this.memories = new Map();
this.defaultTimeout = 30; this.defaultTimeout = 30;
} }
async init() { async init() {
try { try {
const config = await ConfigControl.get('ai'); const config = await ConfigControl.get('ai');
this.defaultTimeout = config?.timeout || 30; this.defaultTimeout = config?.timeout || 30;
if (!fs.existsSync(this.baseDir)) { await this.loadAllMemories();
fs.mkdirSync(this.baseDir, { recursive: true }); await this.cleanExpiredMemories();
}
logger.info('[crystelf-ai] 记忆系统初始化完成');
} catch (error) { } catch (error) {
logger.error(`[crystelf-ai] 记忆系统初始化失败: ${error.message}`); logger.error(`[crystelf-ai] 记忆系统初始化失败: ${error.message}`);
} }
} }
/** async loadAllMemories() {
* 动态加载单个用户的记忆
*/
async loadUserMemories(groupId, userId) {
try { try {
const filePath = path.join(this.baseDir, String(groupId), `${String(userId)}.json`); if (!fs.existsSync(this.baseDir)) {
if (!fs.existsSync(filePath)) return {}; fs.mkdirSync(this.baseDir, { recursive: true });
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);
} }
return json;
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} 条记忆`);
} catch (error) { } catch (error) {
logger.error(`[crystelf-ai] 加载用户记忆失败(${groupId}/${userId}): ${error.message}`); logger.error(`[crystelf-ai] 加载记忆失败: ${error.message}`);
return {};
} }
} }
//保存指定用户记忆
async saveMemories(groupId, userId) { async saveMemories(groupId, userId) {
try { try {
const groupPath = path.join(this.baseDir, String(groupId)); const groupPath = path.join(this.baseDir, String(groupId));
@ -48,6 +56,7 @@ class MemorySystem {
if (!fs.existsSync(groupPath)) { if (!fs.existsSync(groupPath)) {
fs.mkdirSync(groupPath, { recursive: true }); fs.mkdirSync(groupPath, { recursive: true });
} }
const userMemories = {}; const userMemories = {};
for (const [key, memory] of this.memories) { for (const [key, memory] of this.memories) {
if (key.startsWith(`${groupId}_${userId}_`)) { if (key.startsWith(`${groupId}_${userId}_`)) {
@ -55,22 +64,14 @@ class MemorySystem {
userMemories[memoryId] = memory; userMemories[memoryId] = memory;
} }
} }
await fs.promises.writeFile(filePath, JSON.stringify(userMemories, null, 2), 'utf8');
fs.writeFileSync(filePath, JSON.stringify(userMemories, null, 2));
logger.info(`[crystelf-ai] 记忆已保存到 ${groupId}/${userId}.json`); logger.info(`[crystelf-ai] 记忆已保存到 ${groupId}/${userId}.json`);
} catch (error) { } catch (error) {
logger.error(`[crystelf-ai] 保存记忆失败: ${error.message}`); 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) { async addMemory(groupId, userId, data, keywords = [], timeout = null) {
try { try {
const memoryId = this.generateMemoryId(); const memoryId = this.generateMemoryId();
@ -86,6 +87,7 @@ class MemorySystem {
}; };
this.memories.set(`${groupId}_${userId}_${memoryId}`, memory); this.memories.set(`${groupId}_${userId}_${memoryId}`, memory);
await this.saveMemories(groupId, userId); await this.saveMemories(groupId, userId);
logger.info(`[crystelf-ai] 添加新记忆: ${groupId}/${userId}/${memoryId}`); logger.info(`[crystelf-ai] 添加新记忆: ${groupId}/${userId}/${memoryId}`);
return memoryId; return memoryId;
} catch (error) { } catch (error) {
@ -94,40 +96,47 @@ class MemorySystem {
} }
} }
/** async searchMemories(userId, keywords = [], limit = 10) {
* 搜索记忆
* @param userId 用户id
* @param input 输入
* @param limit 最大记忆
* @returns {Promise<*[]>}
*/
async searchMemories(userId, input = '', limit = 10) {
try { try {
if(input === '') return null;
const keywords = this.extractKeywords(input);
const results = []; const results = [];
const now = Date.now(); const now = Date.now();
//遍历所有群聊目录 let searchText = '';
const groupDirs = fs.existsSync(this.baseDir) ? fs.readdirSync(this.baseDir) : []; if (keywords.length === 1 && keywords[0].length > 6) {
for (const groupId of groupDirs) { searchText = keywords[0].toLowerCase();
const filePath = path.join(this.baseDir, String(groupId), `${String(userId)}.json`); const words = searchText.match(/[\u4e00-\u9fa5]{1,2}|[a-zA-Z0-9]+/g) || [];
if (!fs.existsSync(filePath)) continue; keywords = Array.from(new Set(words.filter(w => w.length > 1)));
const data = await fs.promises.readFile(filePath, 'utf8'); }
const json = JSON.parse(data || '{}'); const userMemories = [];
for (const memory of Object.values(json)) { for (const [key, memory] of this.memories) {
if (now > memory.expireAt) continue; // 跳过过期 const parts = key.split('_');
const matchScore = this.calculateMatchScore(memory, keywords); if (parts.length < 3) continue;
if (matchScore > 0) { const uid = parts[1];
memory.accessCount = (memory.accessCount || 0) + 1; if (uid !== userId) continue;
memory.lastAccessed = now; if (now > memory.expireAt) continue;
results.push({ userMemories.push(memory);
id: memory.id, }
data: memory.data, if (userMemories.length === 0) return [];
keywords: memory.keywords, for (const memory of userMemories) {
relevance: matchScore 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;
} }
} }
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); results.sort((a, b) => b.relevance - a.relevance);
return results.slice(0, limit); return results.slice(0, limit);
@ -137,29 +146,44 @@ 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; let score = 0;
const text = (memory.data || '').toLowerCase(); for (const keyword of keywords) {
for (const kw of keywords) { if (memory.keywords.includes(keyword)) {
for (const mk of memory.keywords || []) { score += 10;
if (mk.includes(kw) || kw.includes(mk)) score += 10; }
if (memory.data.includes(keyword)) {
score += 5;
} }
if (text.includes(kw)) score += 6;
} }
score += Math.min((memory.accessCount || 0) * 0.2, 5); score += Math.min(memory.accessCount * 0.1, 5);
const daysSinceCreated = (Date.now() - memory.createdAt) / (24 * 60 * 60 * 1000); const daysSinceCreated = (Date.now() - memory.createdAt) / (24 * 60 * 60 * 1000);
score += Math.max(10 - daysSinceCreated * 0.1, 0); score += Math.max(10 - daysSinceCreated * 0.1, 0);
return score; 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() { generateMemoryId() {
return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; return `mem_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
} }

View File

@ -58,27 +58,23 @@ class Renderer {
async renderMarkdown(markdown) { async renderMarkdown(markdown) {
if (!this.isInitialized) await this.init(); if (!this.isInitialized) await this.init();
try { try {
const page = await this.browser.newPage(); const page = await this.browser.newPage();
const html = this.getMarkdownTemplate(markdown, this.config?.markdownRenderer || {}); const html = this.getMarkdownTemplate(markdown, this.config?.markdownRenderer || {});
await page.setContent(html, { waitUntil: 'networkidle0' }); await page.setContent(html, { waitUntil: 'networkidle0' });
await page.waitForSelector('#render-complete', { timeout: 5000 }); await page.waitForSelector('#render-complete', { timeout: 5000 });
//计算页面尺寸
const rect = await page.evaluate(() => { const rect = await page.evaluate(() => {
const body = document.body; const body = document.body;
const main = document.querySelector('.markdown-body') || body; return { width: body.scrollWidth, height: body.scrollHeight };
const rect = main.getBoundingClientRect(); });
return { await page.setViewport({
width: Math.min(Math.ceil(rect.width + 40), 1200), width: Math.ceil(rect.width),
height: Math.ceil(rect.height + 40), height: Math.ceil(rect.height),
};
}); });
await page.setViewport({
width: rect.width,
height: Math.min(rect.height, 3000),
deviceScaleFactor: 2,
});
const tempDir = path.join(process.cwd(), 'temp', 'html'); const tempDir = path.join(process.cwd(), 'temp', 'html');
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true }); if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const filepath = path.join(tempDir, `markdown_${Date.now()}.png`); const filepath = path.join(tempDir, `markdown_${Date.now()}.png`);
@ -226,98 +222,27 @@ class Renderer {
}); });
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&display=swap" rel="stylesheet"> <style>
<style> body { background-color: ${themeColor}; color: #e2e8f0; font-family: 'Noto Sans SC', sans-serif; font-size: ${fontSize}px; line-height: 1.6; margin: 0; padding: 20px; }
body { h1, h2, h3, h4, h5, h6 { color: #f1f5f9; border-bottom: 1px solid #334155; padding-bottom: 5px; }
background-color: ${themeColor}; a { color: #38bdf8; text-decoration: none; }
color: #e2e8f0; a:hover { text-decoration: underline; }
font-family: 'Noto Sans SC', sans-serif; code { background-color: #1e293b; padding: 2px 5px; border-radius: 5px; }
font-size: ${fontSize}px; pre { background-color: #1e293b; padding: 15px; border-radius: 10px; overflow-x: auto; }
line-height: 1.6; blockquote { border-left: 4px solid #334155; padding-left: 15px; color: #9ca3af; }
margin: 0; </style>
padding: 20px; </head>
display: flex; <body>
justify-content: center;
}
.markdown-body {
width: 100%;
max-width: 900px;
box-sizing: border-box;
}
h1, h2, h3, h4, h5, h6 {
color: #f1f5f9;
border-bottom: 1px solid #334155;
padding-bottom: 5px;
margin-top: 1.5em;
}
a {
color: #38bdf8;
text-decoration: none;
}
a:hover { text-decoration: underline; }
code {
background-color: #1e293b;
padding: 2px 5px;
border-radius: 5px;
}
pre {
background-color: #1e293b;
padding: 15px;
border-radius: 10px;
overflow-x: auto;
}
blockquote {
border-left: 4px solid #334155;
padding-left: 15px;
color: #9ca3af;
margin: 1em 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
background-color: #1e293b;
border-radius: 8px;
overflow: hidden;
}
th, td {
border: 1px solid #334155;
padding: 10px 12px;
text-align: left;
}
th {
background-color: #0f172a;
color: #f1f5f9;
}
tr:nth-child(even) {
background-color: #16213d;
}
img {
max-width: 100%;
border-radius: 10px;
display: block;
margin: 10px auto;
}
</style>
</head>
<body>
<div class="markdown-body">
${md.render(markdown)} ${md.render(markdown)}
<div id="render-complete"></div> <div id="render-complete"></div>
</div> </body>
</body> </html>
</html> `;
`;
} }

View File

@ -86,6 +86,8 @@ class ResponseHandler {
return null; return null;
case 'recall': case 'recall':
return this.handleRecallMessage(message); return this.handleRecallMessage(message);
case 'function':
return this.handleFunctionMessage(message);
default: default:
return this.handleNormalMessage(message); return this.handleNormalMessage(message);
} }
@ -112,7 +114,28 @@ class ResponseHandler {
if (!validTypes.includes(message.type)) { if (!validTypes.includes(message.type)) {
logger.info(`[crystelf-ai] ai返回未知的type类型:${message.type}`) logger.info(`[crystelf-ai] ai返回未知的type类型:${message.type}`)
return false; return false;
}return true; }
/**
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;
} }
/** /**
@ -146,6 +169,20 @@ 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) { handleNormalMessage(message) {
// 设置默认值 // 设置默认值
@ -165,11 +202,6 @@ class ResponseHandler {
return processedMessage; return processedMessage;
} }
//对上下文消息进行处理
handleChatHistory(message) {
let messageToHistory = [];
}
createErrorResponse(error) { createErrorResponse(error) {
return [{ return [{
type: 'message', type: 'message',

View File

@ -36,32 +36,5 @@ const Message = {
}); });
} }
}, },
/**
* 获取群聊聊天历史记录
* @param e
* @param group_id
* @param message_seq seq
* @param count 数量
* @param reverseOrder 倒序
* @param adapter 适配器
* @returns {Promise<*>}
*/
async getGroupHistory(e,group_id,message_seq,count = 20,reverseOrder = false,adapter = 'nc'){
if(adapter === 'nc') {
return await e.bot.sendApi('get_group_msg_history', {
group_id: group_id,
message_seq: message_seq,
count: count,
reverseOrder: reverseOrder,
})
} else if (adapter === 'lgr') {
return await e.bot.sendApi('get_group_msg_history', {
group_id: group_id,
message_id:message_seq,
count: count,
})
}
}
}; };
export default Message; export default Message;