Перейти к основному содержимому

База данных

Elftia использует better-sqlite3 + Drizzle ORM в качестве локального решения для хранения данных. Все операции с базой данных выполняются асинхронно через Worker Thread.

Выбор технологий

КомпонентРешениеПричина
Движок базы данныхbetter-sqlite3Встраиваемый, без настройки, высокая производительность
ORMDrizzle 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Пользовательские SkillsWorker 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Сервис пользовательских миграций