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

Потоки данных

В этом документе описано, как данные перемещаются между слоями Elftia: отправка сообщений, вызов инструментов, управление состоянием и стратегии кэширования.

Полный процесс отправки сообщения

Полный путь данных — от ввода пользователем текста до окончательного отображения ответа AI:

sequenceDiagram
participant U as User Input
participant UI as UnifiedInput
participant UCC as UnifiedChatContext
participant CBC as ChatBackendContext
participant IPC as IPC Layer
participant CR as CompletionRouter
participant CS as CompletionService
participant ED as EngineDispatcher
participant Engine as Engine (Chat/SDK/TinyElf/CLI)
participant LLM as LLM API
participant DB as DbClient (Worker)
participant Store as chatStore (Zustand)
participant Render as MessageRenderer

U->>UI: Type message, press Enter
UI->>UCC: sendMessage(content, attachments)
UCC->>UCC: Create user message, update local state
UCC->>CBC: Send message via IPC
CBC->>IPC: window.api.completion.chatInSession(params)
IPC->>CR: secureHandle validates token
CR->>CS: completion.chatInSession(sessionId, messages, config)
CS->>ED: engineDispatcher.chat(session)
ED->>Engine: Route to appropriate engine
Engine->>LLM: HTTP request (SSE stream)

loop SSE streaming response
LLM-->>Engine: data chunk
Engine-->>CR: IPC event (stream:delta)
CR-->>IPC: mainWindow.send('stream:delta', chunk)
IPC-->>CBC: onStreamDelta callback
CBC-->>Store: setStreamingState({content})
Store-->>Render: Re-render StreamingMessage
end

Engine-->>CR: stream complete
CR->>DB: Persist assistant message
CR-->>IPC: stream:end event
IPC-->>CBC: onStreamEnd callback
CBC-->>UCC: Update message list
UCC-->>Store: clearStreamingState, addMessage
Store-->>Render: Render final message

Описание ключевых шагов

  1. Ввод пользователя — компонент UnifiedInput перехватывает ввод, вложения и выбор модели
  2. UnifiedChatContext — создаёт объект сообщения типа user, оптимистично обновляет локальное состояние
  3. Вызов IPC — передаётся в главный процесс через window.api.completion.chatInSession(), открытый через Preload
  4. CompletionRouter — проверяет токен, разбирает параметры, вызывает CompletionService
  5. EngineDispatcher — маршрутизирует запрос к нужному движку на основе engineType Agent
  6. LLM API — движок выполняет HTTP-запрос и получает потоковый SSE-ответ
  7. Потоковая передача — каждая дельта отправляется на фронтенд через IPC-событие; Zustand store обновляется в реальном времени
  8. Сохранение — после завершения бэкенд записывает сообщение ассистента в базу данных
  9. Синхронизация состояния — фронтенд очищает потоковое состояние, отображает итоговое сообщение

Процесс вызова инструментов Agent

Обработка ситуации, когда LLM возвращает вызов инструмента (tool_use):

flowchart TB
LLM[LLM returns tool_use] --> Parse[Parse tool_call]
Parse --> FW{ExecutionFirewall<br/>path check}

FW -->|deny| Block[Return denied result to LLM]
FW -->|pass| Guardian{GuardianAgent<br/>AI safety review}

Guardian -->|risk: high/critical| PermGate{ChannelPermissionGate<br/>human confirmation}
Guardian -->|risk: low/none| Execute[Execute tool]
Guardian -->|monitor mode| LogOnly[Log and execute]

PermGate -->|user denies| Block
PermGate -->|user approves| Execute

Execute --> Result[Tool execution result]
Result --> Audit[AuditLogger records]
Result --> BackToLLM[Result returned to LLM]
BackToLLM --> LLM

LogOnly --> Execute
{/* Tool call pipeline in TinyElf Agent Loop */}
interface ToolCallPipeline {
firewall: ExecutionFirewall; // 1. Deterministic check (zero LLM overhead)
guardian: GuardianAgent; // 2. AI review (mode-dependent)
permissionGate: ChannelPermissionGate; // 3. Human confirmation (Channel sources only)
executor: ToolExecutor; // 4. Execute
auditLogger: AuditLogger; // 5. Audit
}

Иерархия управления состоянием

Управление состоянием на фронтенде разделено на три слоя с чётко определёнными зонами ответственности:

graph TB
subgraph "Layer 1: Zustand Store (core state)"
CS[chatStore — sessions/messages/streaming/branches]
SS[settingsStore — settings state]
end

subgraph "Layer 2: React Context (domain state)"
CDC[ChatDataContext — data cache wrapper]
UCC2[UnifiedChatContext — unified chat API]
TC[ThemeContext — theme]
AC[AuthContext — authentication]
EC[ElfiContext — Elfi assistant]
end

subgraph "Layer 3: Feature Context (feature state)"
CTC[ChatTabsContext — multi-tab]
WIC[WorldInfoHighlightContext — WI highlights]
MSC[MessageSelectionContext — multi-select]
end

CS --> CDC
CDC --> UCC2
UCC2 --> CTC

Зоны ответственности слоёв

СлойТехнологияХарактеристикиСценарии использования
Слой 1ZustandВысокочастотные обновления, точные подписки, без вложенности ProviderПотоковые сообщения, переключение веток, список сессий
Слой 2React ContextСреднечастотные обновления, предоставляет методы API, внедрение зависимостейОперации чата (отправка/регенерация), авторизация, тема
Слой 3React ContextНизкочастотные обновления, изоляция функцийМноговкладочность, подсветка ключевых слов, выбор сообщений

Направление миграции состояния

ChatDataContext (deprecated) ──migrating──> chatStore (Zustand)

v
UnifiedChatContext (unified API layer)

ChatDataContext — это устаревший Context-обёртка, который находится в процессе миграции в Zustand store. Новый код должен напрямую использовать chatStore или UnifiedChatContext.

Ветвление сообщений

Сообщения чата используют древовидную структуру для поддержки ветвления (регенерация или редактирование создаёт новые ветки):

graph TB
M1[User: Hello] --> M2a[Assistant: Hello! v1]
M1 --> M2b[Assistant: Hi! v2]
M2a --> M3[User: Help me write some code]
M3 --> M4a[Assistant: Sure v1]
M3 --> M4b[Assistant: Of course v2]
{/* Branch data structure */}
interface BranchInfo {
id: string;
parentMessageId: string;
children: string[];
currentIndex: number;
}

{/* Branch operations */}
interface BranchOperations {
switchBranch(messageId: string, index: number): void;
getActivePath(rootId: string): Message[];
regenerate(messageId: string): void;
editMessage(messageId: string, newContent: string): void;
}

Логика переключения веток

  1. Пользователь нажимает стрелки навигации по веткам
  2. switchBranch(parentMessageId, newIndex) обновляет BranchInfo.currentIndex
  3. getActivePath() пересчитывает активный путь сообщений от корня до листа
  4. Список сообщений перерисовывается

Стратегии кэширования

Кэш фронтенда

КэшТехнологияTTLНазначение
Кэш сообщенийZustand messageCacheВремя жизни сессииИзбежать повторной загрузки сообщений
Состояние UIIndexedDB (frontendCache)ПостоянныйЧерновики, свёрнутые элементы, позиция прокрутки
Список сессийZustand sessionsОбновляется при обновленииСписок сессий в боковой панели
Список провайдеровZustand providersОбновляется при обновленииСелектор моделей

Кэш бэкенда

КэшРасположениеTTLНазначение
Список инструментов MCPCacheService5 минутИзбежать повторного перечисления инструментов MCP
Цепочка TransformerTransformerService10 минутСкомпилированная цепочка преобразований
Индекс провайдеровLLMConfigServiceMap O(1)Быстрый поиск провайдера по ID
Кулдаун API-ключаApiKeyPoolServiceЭкспоненциальный откат 60с–15минОхлаждение после ошибок 429/529
Результаты PromptGuardianPromptGuardianКлюч SHA-256Кэшированные результаты проверки промптов
Результаты GuardianAgentGuardianAgentКлюч SHA-256Кэшированные результаты проверки вызовов инструментов

Защита сессии

Предотвращает обновление боковой панели и очистку сообщений чата при получении WebSocket-обновлений проекта в ходе активного разговора:

{/* Session protection flow */}
interface SessionProtection {
activeSessions: Set<string>;
processingSessions: Set<string>;

markActive(sessionId: string): void; // Mark when user sends a message
shouldSkipRefresh(): boolean; // activeSessions.size > 0
markInactive(sessionId: string): void; // Remove after conversation completes
}

Циклический перебор ключей (ApiKeyPoolService)

Бэкенд поддерживает настройку нескольких API-ключей для каждого LLM-провайдера с использованием взвешенного циклического перебора и привязки к сессии:

flowchart LR
Req[Request] --> Check{Session bound?}
Check -->|yes| BoundKey[Use bound key]
Check -->|no| RR[Weighted round-robin selects key]
RR --> Bind[Bind to session]
Bind --> Call[API call]
BoundKey --> Call
Call --> OK{Success?}
OK -->|429/529| Cool[Cool down this key]
Cool --> Retry[Switch to next key and retry]
OK -->|success| Done[Return result]
{/* Simplified ApiKeyPoolService */}
class ApiKeyPoolService {
private sessionBindings: Map<string, string>;
private cooldowns: Map<string, { until: number; backoff: number }>;

resolveApiKeyForRequest(
providerId: string,
sessionId?: string,
): Promise<{ keyId: string; apiKey: string }>;

markKeyError(keyId: string, statusCode: number): void;
}

Связанные файлы

ФайлОписание
packages/renderer/src/shared/state/chatStore.tsZustand-хранилище состояния чата + единый API чата (отправка/регенерация/редактирование, кэш данных; бывшие UnifiedChatContext / ChatDataContext объединены здесь)
packages/renderer/src/features/chat/hooks/useSessionProtection.tsЗащита сессии
packages/renderer/src/shared/utils/frontendCache.tsКэш IndexedDB для фронтенда
packages/desktop/app/main/services/capabilities/llm/completion/CompletionService.tsСервис завершения LLM
packages/desktop/app/main/services/capabilities/llm/completion/ApiKeyPoolService.tsЦиклический перебор ключей
packages/desktop/app/main/services/agent-core/engine/EngineDispatcher.tsДиспетчер движков
packages/desktop/app/main/services/routers/CompletionRouter.tsIPC-роутер завершения
packages/desktop/app/main/services/platform/security/ExecutionFirewall.tsФайрвол путей
packages/desktop/app/main/services/platform/security/GuardianAgent.tsAI-проверка инструментов
packages/desktop/app/main/services/infra/cache/CacheService.tsСервис кэша бэкенда