EasyAgent/src/storage/conversation_store.js

152 lines
4.6 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { toISO } = require('../utils/time');
const { normalizeTokenUsage } = require('../utils/token_usage');
function getWorkspaceStore(workspace) {
const base = path.join(workspace, '.easyagent');
const convDir = path.join(base, 'conversations');
const indexFile = path.join(base, 'index.json');
if (!fs.existsSync(convDir)) fs.mkdirSync(convDir, { recursive: true });
if (!fs.existsSync(indexFile)) fs.writeFileSync(indexFile, JSON.stringify({}, null, 2), 'utf8');
return { base, convDir, indexFile };
}
function newConversationId() {
return crypto.randomUUID();
}
function loadIndex(indexFile) {
try {
const raw = fs.readFileSync(indexFile, 'utf8');
return raw ? JSON.parse(raw) : {};
} catch (_) {
return {};
}
}
function saveIndex(indexFile, index) {
fs.writeFileSync(indexFile, JSON.stringify(index, null, 2), 'utf8');
}
function extractTitle(messages) {
for (const msg of messages) {
if (msg.role === 'user') {
let content = '';
if (typeof msg.content === 'string') {
content = msg.content.trim();
} else if (Array.isArray(msg.content)) {
content = msg.content
.filter((part) => part && part.type === 'text' && typeof part.text === 'string')
.map((part) => part.text)
.join(' ')
.trim();
}
if (content) return content.length > 50 ? `${content.slice(0, 50)}...` : content;
}
}
return '新对话';
}
function countTools(messages) {
let count = 0;
for (const msg of messages) {
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) count += msg.tool_calls.length;
if (msg.role === 'tool') count += 1;
}
return count;
}
function saveConversation(workspace, conversation) {
const { convDir, indexFile } = getWorkspaceStore(workspace);
const filePath = path.join(convDir, `${conversation.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(conversation, null, 2), 'utf8');
const index = loadIndex(indexFile);
index[conversation.id] = {
title: conversation.title || '新对话',
created_at: conversation.created_at,
updated_at: conversation.updated_at,
total_messages: (conversation.messages || []).length,
total_tools: countTools(conversation.messages || []),
thinking_mode: conversation.metadata?.thinking_mode || false,
run_mode: conversation.metadata?.thinking_mode ? 'thinking' : 'fast',
model_key: conversation.metadata?.model_key || '',
};
saveIndex(indexFile, index);
}
function createConversation(workspace, metadata = {}) {
const id = newConversationId();
const now = toISO();
const conversation = {
id,
title: '新对话',
created_at: now,
updated_at: now,
metadata: {
model_key: metadata.model_key || '',
model_id: metadata.model_id || '',
thinking_mode: !!metadata.thinking_mode,
allow_mode: metadata.allow_mode || 'full_access',
token_usage: normalizeTokenUsage(metadata.token_usage),
cwd: metadata.cwd || '',
},
messages: [],
};
saveConversation(workspace, conversation);
return conversation;
}
function loadConversation(workspace, id) {
const { convDir } = getWorkspaceStore(workspace);
const filePath = path.join(convDir, `${id}.json`);
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, 'utf8');
if (!raw) return null;
return JSON.parse(raw);
}
function listConversations(workspace) {
const { indexFile } = getWorkspaceStore(workspace);
const index = loadIndex(indexFile);
const entries = Object.entries(index).map(([id, meta]) => ({ id, ...meta }));
entries.sort((a, b) => {
const ta = new Date(a.updated_at || a.created_at || 0).getTime();
const tb = new Date(b.updated_at || b.created_at || 0).getTime();
return tb - ta;
});
return entries;
}
function updateConversation(workspace, conversation, messages, metadataUpdates = {}) {
const now = toISO();
const updated = {
...conversation,
messages,
updated_at: now,
};
updated.title = extractTitle(messages);
updated.metadata = {
...conversation.metadata,
...metadataUpdates,
token_usage: metadataUpdates && Object.prototype.hasOwnProperty.call(metadataUpdates, 'token_usage')
? normalizeTokenUsage(metadataUpdates.token_usage)
: normalizeTokenUsage(conversation.metadata?.token_usage),
};
saveConversation(workspace, updated);
return updated;
}
module.exports = {
getWorkspaceStore,
createConversation,
loadConversation,
listConversations,
updateConversation,
saveConversation,
};