База данных
Elftia использует better-sqlite3 + Drizzle ORM в качестве локального решения для хранения данных. Все операции с базой данных выполняются асинхронно через Worker Thread.
Выбор технологий
| Компонент | Решение | Причина |
|---|---|---|
| Движок базы данных | better-sqlite3 | Встраиваемый, без настройки, высокая производительность |
| ORM | Drizzle ORM | Типобезопасный, без накладных расходов во время выполнения, SQL-подобный API |
| Режим | WAL (Write-Ahead Logging) | Поддерживает параллельное чтение; записи не блокируют чтение |
| Паттерн доступа | Worker Thread RPC | Не блокирует главный поток |
Расположение базы данных
{userData}/elftia.db # Основной файл базы данных
{userData}/elftia.db-wal # WAL-журнал
{userData}/elftia.db-shm # Файл разделяемой памяти
Пути {userData} по платформам:
- Windows:
%APPDATA%/elftia/ - macOS:
~/Library/Application Support/elftia/ - Linux:
~/.config/elftia/
Архитектура Worker Thread
Все операции с базой данных выполняются через RPC-паттерн DbClient -> db.worker.ts:
sequenceDiagram
participant S as Service Layer
participant C as DbClient (main thread)
participant W as db.worker.ts (Worker Thread)
participant DB as SQLite (WAL)
Note over S,DB: Initialization
C->>W: Create Worker({dbPath})
W->>DB: new Database(dbPath)
W->>DB: PRAGMA journal_mode = WAL
W->>DB: drizzle(db) + migrate()
W-->>C: ready signal
Note over S,DB: Normal operation
S->>C: db.chatSessionsList()
C->>C: id = ++seq
C->>W: postMessage({id, method: 'chatSessions:list', params})
W->>DB: SELECT * FROM chat_sessions ...
DB-->>W: Result rows
W-->>C: postMessage({id, result: rows})
C->>C: pending[id].resolve(rows)
C-->>S: Promise<Session[]>
Основная реализация DbClient
{/* packages/desktop/app/main/workers/DbClient.ts */}
class DbClient extends EventEmitter implements DbRpc {
private worker: Worker;
private seq = 0;
private pending = Map<number, { resolve, reject }>;
constructor(dbPath: string, options?: DbClientOptions) {
this.worker = new Worker(workerPath, {
workerData: { dbPath, diagnosticsDir },
});
this.worker.on('message', (msg) => this.handleMessage(msg));
}
async ready(): Promise<void>;
private async send(method: string, params: unknown): Promise<unknown> {
const id = ++this.seq;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, method, params });
});
}
// 100+ typed methods
chatSessionsList(): Promise<ChatSession[]>;
chatMessagesInsert(msg: ChatMessageInput): Promise<void>;
auditLogInsert(entry: AuditLogEntry): Promise<void>;
}
Определения типов RPC
{/* packages/desktop/app/main/workers/types.ts */}
interface DbRequest {
id: number;
method: string;
params: unknown;
}
interface DbResponse {
id: number;
result?: unknown;
error?: { message: string; stack?: string };
}
interface DbRpc {
chatSessionsList(): Promise<ChatSession[]>;
chatSessionsGet(id: string): Promise<ChatSession | null>;
chatSessionsCreate(input: ChatSessionInput): Promise<ChatSession>;
chatSessionsUpdate(id: string, data: Partial<ChatSession>): Promise<void>;
chatSessionsDelete(id: string): Promise<void>;
// ... 100+ methods
}
Организация схемы
Схема базы данных определена с помощью Drizzle ORM и расположена в packages/desktop/app/main/db/schema/:
packages/desktop/app/main/db/schema/
├── index.ts # Единая точка экспорта
├── accounts.ts # Пользовательские аккаунты
├── chat.ts # Сообщения чата
├── sessions.ts # Сессии чата
├── llm.ts # Провайдеры/модели/ключи LLM
├── projects.ts # Проекты
├── settings.ts # Настройки приложения
├── attachments.ts # Вложения
├── media.ts # Медиаресурсы
└── templates.ts # Шаблоны
Основные таблицы данных
Связанные с чатом
{/* Упрощённые определения структуры таблиц */}
// Сессия чата
interface ChatSession {
id: string; // UUID
title: string; // Заголовок сессии
type: 'chat' | 'roleplay' | 'agent';
agentId?: string; // ID связанного Agent
personaId?: string; // ID связанной персоны
providerId?: string; // ID провайдера LLM
modelId?: string; // ID модели
rpConfig?: RPConfig; // Конфигурация RP (JSON)
createdAt: number;
updatedAt: number;
lastMessageAt?: number;
messageCount: number;
pinned: boolean;
folderId?: string;
}
// Сообщение чата
interface ChatMessage {
id: string; // UUID
sessionId: string; // ID владеющей сессии
role: 'user' | 'assistant' | 'system';
content: string; // Содержимое сообщения
parentId?: string; // ID родительского сообщения (поддержка веток)
reasoning?: string; // Содержимое рассуждений (thinking)
toolCalls?: ToolCall[]; // Вызовы инструментов (JSON)
meta?: MessageMeta; // Метаданные (providerId, modelId, tokenCount)
createdAt: number;
}
Связанные с аккаунтами
// Пользовательский аккаунт
interface Account {
id: string;
email: string;
displayName?: string;
avatarUrl?: string;
createdAt: number;
}
// Токен аккаунта
interface AccountToken {
id: string;
accountId: string;
provider: string; // OAuth-провайдер
accessToken: string; // Зашифрован при хранении
refreshToken?: string;
expiresAt?: number;
}
Конфигурация LLM
// Провайдер LLM
interface LLMProvider {
id: string;
name: string;
type: string; // openai, anthropic, google и т.д.
apiKey?: string; // Зашифрован при хранении
baseUrl?: string; // Пользовательский API-эндпоинт
enabled: boolean;
models: string[]; // Список доступных моделей
}
// Пул API-ключей (поддержка нескольких ключей)
interface ApiKeyEntry {
id: string;
providerId: string;
label?: string;
apiKey: string; // Зашифрован при хранении
weight: number; // Вес для round-robin
enabled: boolean;
createdAt: number;
}
Связанные с проектами
// Проект
interface Project {
id: string;
name: string;
path: string; // Путь в файловой системе
description?: string;
createdAt: number;
updatedAt: number;
}
Аудит безопасности
// Запись журнала аудита
interface AuditLogEntry {
id: string;
timestamp: number;
eventType: string; // tool_executed, injection_detected и т.д.
severity: 'info' | 'warning' | 'critical';
channelId?: string;
userId?: string;
toolName?: string;
details: string; // Сериализованные в JSON детали
}
Медиа и ресурсы
// Медиаресурс
interface MediaResource {
id: string;
sessionId: string;
messageId: string;
filePath: string;
mimeType: string;
thumbnailPath?: string;
size: number;
createdAt: number;
}
// Вложение
interface Attachment {
id: string;
sessionId: string;
messageId: string;
name: string;
type: string;
url?: string;
size: number;
}
Прочие основные таблицы
| Таблица | Назначение | Файл схемы |
|---|---|---|
settings | Настройки приложения (ключ-значение) | schema/settings.ts |
templates | Шаблоны изображений | schema/templates.ts |
channel_instances | Экземпляры каналов | Worker db/channels.ts |
channel_users | Пользователи каналов | Worker db/channelUsers.ts |
character_cards | Карточки персонажей | Worker db/characterCards.ts |
world_info_books | Книги мировой информации | Worker db/worldInfo.ts |
world_info_entries | Записи мировой информации | Worker db/worldInfo.ts |
custom_agents | Пользовательские Agent'ы | Worker db/customAgents.ts |
personas | Определения персон | Worker db/personas.ts |
user_skills | Пользовательские Skills | Worker db/userSkills.ts |
tags | Теги | Worker db/tags.ts |
notes | Индекс заметок | Worker db/notes.ts |
note_folders | Папки заметок | Worker db/noteFolders.ts |
cron_jobs | Запланированные задачи | Файловая система CronService |
Модули DB Worker
Конкретные реализации операций с базой данных распределены по нескольким модулям в packages/desktop/app/main/workers/db/:
workers/db/
├── index.ts # Главный диспетчер; маршрутизирует по префиксу метода к нужному модулю
├── chatSessions.ts # Методы chatSessions:*
├── chatMessages.ts # Методы chatMessages:*
├── customAgents.ts # Методы customAgents:*
├── personas.ts # Методы personas:*
├── characterCards.ts # Методы characterCards:*
├── worldInfo.ts # Методы worldInfo:*
├── groupChat.ts # Методы groupChat:*
├── channels.ts # Методы channels:*
├── channelUsers.ts # Методы channelUsers:*
├── auditLog.ts # Методы auditLog:*
├── apiKeys.ts # Методы apiKeys:*
├── tags.ts # Методы tags:*
├── notes.ts # Методы notes:*
├── noteFolders.ts # Методы noteFolders:*
├── userSkills.ts # Методы userSkills:*
├── rpDefaults.ts # Методы rpDefaults:*
└── characterSprites.ts # Методы sprites:*
Система миграций
Миграции Drizzle
Изменения схемы применяются автоматически через migrate() от Drizzle:
{/* packages/desktop/app/main/db/index.ts */}
function initDatabase() {
const sqlite = new Database(dbPath);
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
const db = drizzle(sqlite);
migrate(db, { migrationsFolder: 'drizzle' });
}
Пользовательские миграции
Сложная логика миграции данных реализована в migrations.ts:
{/* Пример пользовательской миграции */}
async function migrateApiKeyPool(db: DbRpc) {
const providers = await db.llmProvidersList();
for (const p of providers) {
if (p.apiKey) {
await db.apiKeysInsert({
providerId: p.id,
apiKey: p.apiKey,
weight: 1,
enabled: true,
});
}
}
}
Последовательность инициализации
sequenceDiagram
participant M as Main Process
participant C as DbClient
participant W as db.worker.ts
participant D as SQLite
M->>C: new DbClient(dbPath)
C->>W: Create Worker Thread
W->>D: new Database(dbPath)
W->>D: PRAGMA journal_mode = WAL
W->>D: PRAGMA foreign_keys = ON
W->>W: Register all RPC methods
W-->>C: ready signal
M->>C: await db.ready()
Note over M: Worker initialization complete; continue
M->>M: initDatabase()
Note over M: Drizzle ORM initialization + migrate
M->>M: Create services that depend on db
Ключевой момент: await db.ready() должен завершиться до создания других сервисов, чтобы предотвратить ошибки SQLITE_BUSY во время инициализации Worker.
Оптимизация базы данных
Сервис DatabaseOptimizer периодически выполняет операции обслуживания:
| Операция | Описание | Частота |
|---|---|---|
PRAGMA optimize | Обновление статистики | При закрытии приложения |
VACUUM | Освобождение места | Ручной запуск |
ANALYZE | Обновление плана запросов | Периодически |
| WAL checkpoint | Слияние WAL с основным файлом | Автоматически SQLite |
Связанные файлы
| Файл | Описание |
|---|---|
packages/desktop/app/main/db/index.ts | Инициализация базы данных (WAL + Drizzle + migrate) |
packages/desktop/app/main/db/schema/ | Директория с определениями схемы Drizzle ORM |
packages/desktop/app/main/workers/DbClient.ts | Клиент Worker базы данных |
packages/desktop/app/main/workers/db.worker.ts | Реализация Worker базы данных |
packages/desktop/app/main/workers/db/ | Модули операций с БД, разделённые по доменам |
packages/desktop/app/main/workers/types.ts | Определения интерфейса DbRpc |
packages/desktop/app/main/services/persistence/db-optimizer/DatabaseOptimizer.ts | Сервис оптимизации базы данных |
packages/desktop/app/main/services/platform/migration/MigrationService.ts | Сервис пользовательских миграций |