Skip to content

Дизайн бэкенда на Go ​

Бэкенд на Go организует обработку запросов по слоям Handler, Service, Repository и инфраструктуры, а зависимости собирает через uber/dig. cmd/server отвечает за запуск и остановку, internal/ реализует маршрутизацию с проверкой прав, бизнес-сервисы и доступ к хранилищам.

Слоистая архитектура ​

Бэкенд следует классической четырёхуровневой структуре Handler → Service → Repository → база данных; зависимости между слоями полностью развязаны через интерфейсы (internal/types/interfaces/) и собираются DI-контейнером при запуске:

СлойРасположениеНазначение
Router / Middlewareinternal/router/, internal/middleware/Регистрация маршрутов, аутентификация, RBAC, ограничение скорости, логирование, конверт ошибок
Handlerinternal/handler/ (связанное с сессиями — в internal/handler/session/)Разбор параметров запроса (DTO в internal/handler/dto/), вызов Service, запись ответа; без бизнес-логики
Контроль доступаinternal/application/access/Общие правила авторизации, не зависящие от Gin: трёхуровневый разбор доступа к базам знаний, права организационного шаринга, область баз знаний общего агента, авторизация файлов и артефактов сообщений, авторизация клонирования/перемещения между базами
Serviceinternal/application/service/ (около 160+ файлов)Оркестрация бизнес-логики: базы знаний/знания/блоки, сессии и конвейер chat_pipeline/, агент, тенант и участники, модели, синхронизация источников данных, Wiki, аудит и др.
Repositoryinternal/application/repository/ (около 60 файлов)Доступ к данным, единообразно через GORM (type knowledgeRepository struct { db *gorm.DB }, операции через r.db.WithContext(ctx)); реализации репозитория поисковых движков разбиты по движкам в repository/retriever/{postgres,elasticsearch,qdrant,milvus,weaviate,doris,opensearch,tencentvectordb,sqlite,neo4j}
Доменные моделиinternal/types/Сущности GORM, перечисления, context key, определения интерфейсов (types/interfaces)
Инфраструктураinternal/infrastructure/ (gRPC-клиент docparser, web_search), internal/models/ (адаптеры моделей chat/embedding/rerank), internal/stream/, internal/sandbox/, internal/mcp/, internal/im/Адаптация к внешним системам

Ключевые соглашения:

  • Handler зависит только от интерфейсов Service (например interfaces.KnowledgeService), Service зависит только от интерфейсов Repository и других интерфейсов Service;
  • все интерфейсы сосредоточены в internal/types/interfaces/, а реализации связываются через dig;
  • Asynq worker и HTTP-сервер работают в одном процессе; обработчики задач переиспользуют тот же набор Service.

Внедрение зависимостей: internal/container (uber/dig) ​

aihelpme.ru использует go.uber.org/dig v1.19.0 (контейнер с инъекцией через конструкторы, а не codegen-wire). Точка входа — BuildContainer в internal/container/container.go:

go
// cmd/server/main.go
c := container.BuildContainer(runtime.GetContainer())

// internal/container/container.go
func BuildContainer(container *dig.Container) *dig.Container {
    must(container.Provide(NewResourceCleaner, dig.As(new(interfaces.ResourceCleaner))))
    must(container.Provide(config.LoadConfig))
    must(container.Provide(initDatabase))     // *gorm.DB
    must(container.Provide(initRedisClient))  // *redis.Client (может быть nil: режим Lite)
    ...
    must(container.Provide(repository.NewTenantRepository))
    must(container.Provide(service.NewTenantService))
    ...
    must(container.Provide(router.NewRouter)) // итоговый результат *gin.Engine
    return container
}

runtime.GetContainer() (internal/runtime/container.go) держит глобальный синглтон dig.Container; must(err) при ошибке регистрации сразу вызывает panic — ошибки сборки DI являются фатальными на этапе запуска.

Используемые возможности dig ​

ВозможностьПример использования
dig.AsПривязка конкретного типа к интерфейсу: container.Provide(NewResourceCleaner, dig.As(new(interfaces.ResourceCleaner))); router.NewAsyncqClient привязывается к interfaces.TaskEnqueuer
dig.Name именованные зависимостиНесколько экземпляров одного интерфейса: 4 сервиса извлечения (chunkExtractor/dataTableSummary/imageMultimodal/knowledgePostProcess), 6 серверов Asynq (coreAsynqServer/postProcessAsynqServer/enrichmentAsynqServer/maintenanceAsynqServer/sharedAsynqServer/wikiAsynqServer), wikiIngest
dig.In структура параметровrouter.RouterParams встраивает dig.In и разом внедряет около 60 зависимостей Handler/Service, избегая слишком длинных сигнатур конструкторов
container.Invoke побочные эффекты выполненияФоновые компоненты, стартующие при регистрации: registerPoolCleanup, registerWebSearchProviders, startDataSourceScheduler, startHousekeepingService, startAuditLogRetention, startTemporaryDocumentCleanup, 15 плагинов chatpipeline.NewPluginXxx (плагины Search/Rerank/WebFetch/Merge/DataAnalysis/QueryUnderstand/LoadHistory/ChatCompletionStream и др. сами регистрируются в EventManager), router.RunAsynqServer, recoverPendingWikiTasks и др.
Provide-адаптерыПреобразование интерфейсов через замыкания: func(s *service.StorageBackendService) interfaces.StorageBackendService { return s }; RetrieveEngineRegistry тем же экземпляром одновременно выставляется как StoreRegistry

Порядок регистрации и условная сборка ​

Регистрация в BuildContainer разделена на девять этапов (в исходном коде им соответствуют логи): ① базовая инфраструктура (config/langfuse/db/file/redis/пул ants) → ② реестр поисковых движков → ③ внешние клиенты (docreader gRPC, Ollama, Neo4j, StreamManager, DuckDB) → ④ слой Repository (30+) → ⑤ слой Service (50+, включая MCP Manager, шину событий, шлюз утверждения агента approval.Gate) → ⑥ условная сборка исполнителя задач → ⑦ плагины chat_pipeline → ⑧ слой Handler (40+) и адаптеры IM → ⑨ запуск Router и Asynq server.

Шаг ⑥ — самая важная условная ветка во всём репозитории: наличие Redis определяет форму работы:

go
redisAvailable := os.Getenv("REDIS_ADDR") != ""
if redisAvailable {
    must(container.Provide(router.NewAsyncqClient, dig.As(new(interfaces.TaskEnqueuer))))
    must(container.Provide(router.NewCoreAsynqServer, dig.Name("coreAsynqServer")))
    ... // всего 6 пулов worker + AsynqInspector
    must(container.Invoke(registerModelConcurrencyLimiter))   // распределённый в Redis шлюз конкурентности per-model
} else {
    syncExec := router.NewSyncTaskExecutor()                  // режим Lite: внутрипроцессный синхронный исполнитель
    must(container.Provide(func() interfaces.TaskEnqueuer { return syncExec }))
    must(container.Provide(router.NewNoopTaskInspector))
    must(container.Invoke(registerLiteModelConcurrencyLimiter)) // внутрипроцессный семафор
}

Конкурентность 6 пулов worker Asynq настраивается через system settings / переменные окружения (по умолчанию Core=8, PostProcess=2, Enrichment=12, Maintenance=4, Shared=6, Wiki=8, WEKNORA_ASYNQ_*_CONCURRENCY); топология очередей определена в internal/types/task.go (default, chat_attachment, postprocess, summary, multimodal, graph, question, memory, sync, low/maintenance, wiki и др., включая автотеги и извлечение памяти).

Очистка ресурсов и фабрики ​

  • ResourceCleaner (internal/container/cleanup.go): компоненты регистрируют деструкторы через RegisterWithName(name, cleanupFunc) (пул ants, flush Langfuse, планировщик источников данных, Housekeeping и др.), при выходе единый Cleanup(ctx);
  • EngineFactory (internal/container/engine_factory.go): во время работы создаёт экземпляры поисковых движков по строкам таблицы vector_stores (createQdrantEngine / createMilvusEngine / createDorisEngine / createOpenSearchEngine...), а не статически привязывает один движок при запуске;
  • initDatabase помимо подключения отвечает за: автоматическую миграцию golang-migrate (AUTO_MIGRATE, при сбое только предупреждение без блокировки), бэкфилл провайдера хранилища __pending_env__, миграцию устаревших StorageBackend, синхронизацию последовательностей, сброс pending-задач в режиме Lite, UPSERT декларативных встроенных моделей из config/builtin_models.yaml; для SQLite принудительно SetMaxOpenConns(1) для сериализации записи.

Процесс запуска cmd/server ​

cmd/server состоит всего из трёх логических файлов: main.go (точка входа и жизненный цикл HTTP), bootstrap.go (одноразовые хуки инициализации), listen.go (повторные попытки привязки порта), плюс signals_unix.go/signals_windows.go, задающие платформенные shutdownSignals.

Ключевые моменты:

  • Сбой инициализации не блокирует запуск: bootstrap.go при сбое пишет logger.Warnf. Инициализация системного администратора срабатывает только когда в развёртывании ещё нет системного администратора; перезапуск не восстанавливает уже отозванные права;
  • Двухфазное корректное завершение: первый SIGTERM/SIGINT вызывает server.Shutdown (он сначала закрывает listener, и порт сразу может быть привязан новым процессом), в пределах ShutdownTimeout на очистку ресурсов резервируется 5s, остальное время идёт дренаж существующих соединений. Не вызывайте listener.Close() вручную до Shutdown: Serve вернёт use of closed network connection, а не ErrServerClosed, и logger.Fatalf убьёт процесс до очистки. Второй сигнал принудительно вызывает Close;
  • Повторные попытки при занятом порте: listenWithRetry начинает с 300ms и делает 10 попыток с экспоненциальным откатом (чтобы при rolling-перезапуске, когда старый процесс ещё не освободил порт, не падать сразу).

Организация маршрутов и сборка RBAC (internal/router) ​

Порядок сборки в NewRouter ​

NewRouter(params RouterParams) из internal/router/router.go (RouterParams — структура dig.In) собирается в следующем порядке, и порядок и есть семантика безопасности:

  1. gin.New() + MultipartFormCleanup (удаление временных файлов разбора multipart после завершения запроса) + SetTrustedProxies (WEKNORA_TRUSTED_PROXIES, по умолчанию доверяет только loopback и приватным подсетям, чтобы подделка X-Forwarded-For не обходила ограничение скорости по IP);
  2. глобальные middleware: cors → RequestID → Language → Logger → Recovery → ErrorHandler;
  3. эндпоинты без аутентификации: GET /health; в не-release режиме монтируется /swagger/*any;
  4. CSP-middleware frame-ancestors для страниц Embed; в версии Lite встроенные статические ресурсы фронтенда (handler.Edition == "lite");
  5. публичные маршруты, регистрируемые до аутентификации: обратные вызовы платформ IM (/api/v1/im, у каждой платформы своя проверка подписи), публичные маршруты Web Embed (/api/v1/embed/:channel_id, аутентификация publish-token через middleware.EmbedAuth + ограничение скорости в Redis), встроенный MCP Server (/mcp/:endpoint_id, аутентификация по Bearer Token через middleware.MCPEndpointAuth), короткоживущие capability URL (resource grants), терминал песочницы/десктопный WebSocket (браузер не может передать заголовок аутентификации при рукопожатии, поэтому используется короткоживущий ticket, получаемый аутентифицированным POST), подключение расширения локального браузера (/api/v1/local-browser/*);
  6. middleware.Auth(...) глобальная аутентификация; далее идут требующие аутентификации маршруты файлового прокси, маршруты presigned-файлов без аутентификации но с проверкой подписи, middleware trace Langfuse, AuditServiceProvider;
  7. v1 := r.Group("/api/v1"): сначала v1.Use(rbacGuards.apiKeyAuthorizer.Middleware()) (шлюз API Key, JWT-сессии пропускаются сразу), затем последовательно вызываются более 30 функций RegisterXxxRoutes(v1, handler, rbacGuards);
  8. финальная самопроверка: rbacGuards.assertAPIKeyPoliciesMatchRoutes(r) — если объявленная политика API Key указывает на несуществующий шаблон маршрута (дрейф пути/опечатка), запуск сразу падает с panic, что исключает появление мёртвой политики, всегда отдающей 403.

Обзор групп маршрутов ​

Префикс группыФункция RegisterПример политики API Key
/auth, /meRegisterAuthRoutes / RegisterMyInvitationRoutesбольшинство без Key
/tenants, /tenants/:id/* (участники/приглашения/аудит)RegisterTenantRoutesmanage_members / manage_spaces; группа /:id вешает PathTenantMatch()
/knowledge-bases, `/knowledge-bases/:id/knowledgefaqtags
/knowledge, /chunksRegisterKnowledgeRoutes / RegisterChunkRoutesingest
/sessions, /knowledge-chat, /agent-chat, /knowledge-search, /messagesRegisterSessionRoutes / RegisterChatRoutes и др.chat / retrieve
/models, /evaluationRegisterModelRoutes / RegisterEvaluationRoutesmanage_models / run_evaluations
/sandbox-configs, /me/env-vars, /me/browserRegisterSandboxConfigRoutes / RegisterMyEnvVarRoutes и др.full_access; /me/* только для самого JWT-пользователя
/system, /system/adminRegisterSystemRoutes / RegisterSystemAdminRoutesгруппа admin принудительно требует g.SystemAdmin()
/mcp-services, /agent, /web-search, /web-search-providersсоответствующие функции Registermanage_mcp_services / manage_web_search
/vector-stores, /storage-backendsRegisterVectorStoreRoutes / RegisterStorageBackendRoutesmanage_vector_stores / manage_storage_backends
/agents, `/agents/:id/sharesembed-channelsim-channels`
/organizations, /user/favorites, /skillsсоответствующие функции Registermanage_spaces и др.
/im-channels, /embed-channels, /mcp-endpoints, /wechatRegisterIMChannelRoutes / RegisterEmbedChannelRoutes / RegisterMCPEndpointRoutesmanage_channels
/datasource, /knowledgebase/:kb_id/wiki, /chunker/previewRegisterDataSourceRoutes / RegisterWikiPageRoutes / RegisterChunkerDebugRoutesmanage_datasources / ingest

rbacGuards: централизованная матрица прав ​

internal/router/rbac.go определяет rbacGuards, которые NewRouter конструирует один раз и передаёт в каждую функцию Register. Стражей три вида, они используются inline в строках маршрутов, так что требования к правам видны с первого взгляда:

go
kb.PUT("/:id", g.OwnedKBOrAdmin(), handler.UpdateKnowledgeBase)
  • Стражи роли (отвечают на вопрос «какая у вызывающего роль внутри тенанта»): Viewer() / Contributor() / Admin() / Owner() / AdminOrSystemAdmin() / SystemAdmin(), под капотом вызывают middleware.RequireRole;
  • Стражи владения (отвечают на вопрос «является ли он создателем ресурса или Admin+»): OwnedKBOrAdmin(), OwnedAgentOrAdmin(), OwnedKnowledgeKBOrAdmin(), OwnedChunkKBOrAdmin(), OwnedWikiKBOrAdmin() и др. — подресурсы (chunk/wiki/FAQ/tag) через замыкания вроде KBCreatorLookupFromKnowledgeID поднимаются по параметрам URL к creator_id своей KB и используют то же правило, что и родительский ресурс;
  • Стражи доступа к базе знаний (трёхуровневый разбор: собственная KB / KB, общая между организациями / KB, видимая общему агенту): KBAccessRead|Write(param) и варианты ...FromKnowledgeIDParam / ...FromChunkIDParam, под капотом middleware.RequireKBAccess;
  • Стражи границы тенанта: CrossTenant() (операции уровня платформы требуют EnableCrossTenantAccess + CanAccessAllTenants), PathTenantMatch() (/tenants/:id должен совпадать с тенантом из контекста).

Комментарии в исходном коде приводят дерево решений выбора стража (для ресурсов с creator используется OwnedXxxOrAdmin; для инфраструктуры уровня тенанта — Admin; для точек создания — Contributor) и прямо указывают, что все стражи уважают переключатель cfg.Tenant.EnableRBAC — при отключении пишется только лог «должно было отказать», после чего запрос пропускается (поведение на период поэтапной миграции).

Политики API Key ортогональны стражам ролей: apiKeyGroup(grp, policy) оборачивает gin RouterGroup и одновременно с регистрацией маршрута пишет (method, fullPath) → APIKeyRoutePolicy в таблицу политик APIKeyRouteAuthorizer; конструкторы политик — apiKeyFullAccess(), apiKeyPlatform(...) и 17 обёрток возможностей (apiKeyRetrieve / apiKeyChat / apiKeyIngest / apiKeyManageModels...). Маршруты без зарегистрированной политики по умолчанию fail-closed отклоняются для субъекта API Key.

Список middleware (internal/middleware) ​

В порядке прохождения запроса:

MiddlewareФайлНазначение и ключевая логика
MultipartFormCleanup()multipart_cleanup.goПосле завершения запроса (включая сбой и восстановление после panic) удаляет файлы, записанные multipart-формой в системный временный каталог, чтобы /tmp контейнера не разрастался
cors.New (gin-contrib)router.goРазрешает заголовки Authorization, X-API-Key, X-Tenant-ID, X-Embed-Session, X-WeKnora-Desktop-Token, а также MCP-Protocol-Version/Mcp-Session-Id, необходимые для MCP Streamable HTTP; MaxAge 12h
RequestID()logger.goПереиспользует заголовок X-Request-ID или генерирует UUID, пишет в gin context и Request.Context(), сквозной для логов/трассировки
Language()language.goОпределяет язык обработки документов: переменная окружения WEKNORA_LANGUAGE > первый тег Accept-Language > по умолчанию zh-CN
Logger()logger.goПолное логирование запроса/ответа; регуляркой маскирует поля паролей/токенов, обрезает base64 data URL изображений, помечает SSE-ответы для пропуска, лимит одной записи 10KB
Recovery()recovery.goПерехват panic + запись стека + ответ 500
ErrorHandler()error_handler.goЧитает последнюю ошибку из c.Errors: *errors.AppError возвращается по своему HTTPCode в едином конверте {success:false, error:{code,message,details}}; остальные — 500
EmbedAuth(...)embed_auth.goВисит только на публичной группе /api/v1/embed/:channel_id: проверяет publish token, внедряет контекст канала Embed; трёхуровневое ограничение скорости в Redis (на IP/минуту, канал глобально/минуту, канал/день)
MCPEndpointAuth(...)mcp_endpoint_auth.goВисит только на /mcp/:endpoint_id: проверяет Bearer Token эндпоинта, внедряет пространство эндпоинта, машинного субъекта и производные от белого списка инструментов и области баз знаний права в стиле API Key
AttachAuthenticatedUser(...)ws_auth.goМаршруты WebSocket, например терминал песочницы: по короткоживущему ticket разбирает пользователя и устанавливает ту же аутентифицированную сессию, что и Auth; участники и роли по-прежнему читаются из БД
PublicAuthRateLimit()auth_public_ratelimit.goМаршруты без аутентификации для запроса приглашений/регистрации по приглашению: скользящее окно в памяти процесса, 60s/30 раз на IP, фоновая очистка устаревших корзин каждые 2 минуты, при превышении 429
Auth(...)auth.goЯдро аутентификации, три состояния: ① JWT (Authorization: Bearer, userService.ValidateToken); ② API Key (X-API-Key, AuthenticateAPIKey); ③ белый список noAuthAPI. Поддерживает переключение тенанта через X-Tenant-ID (трёхуровневая проверка IsTenantAccessible: собственный тенант / кросс-тенантный суперадмин / active membership), resolveTenantRole разбирает роль внутри тенанта. Пишет в context: TenantIDContextKey, TenantInfoContextKey, UserContextKey, UserIDContextKey, TenantRoleContextKey, SystemAdminContextKey, PrincipalContextKey и др.
langfuse.GinMiddleware()tracing/langfusetrace наблюдаемости LLM; при не настроенном LANGFUSE_* — no-op
AuditServiceProvider()audit_provider.goВнедряет AuditLogService в gin context, чтобы пути отказа RBAC писали аудит; при сервисе nil — корректная деградация
APIKeyRouteAuthorizer.Middleware()api_key_gate.goШлюз уровня маршрутов для субъекта API Key: смотрит таблицу политик (method, fullPath), проверяет PlatformOnly / RequireFullAccess / Capabilities; для незадекларированных маршрутов по умолчанию отказ; пользователи JWT пропускаются напрямую
RequireRole(min) и др.rbac.goПроверка нижней границы роли внутри тенанта (owner=40 > admin=30 > contributor=20 > viewer=10); RequireOwnershipOrRole(min, creatorLookup) позволяет создателю ресурса обойти нижнюю границу роли; субъект API Key закорачивается (его авторизацией ведает APIKeyGate); кросс-тенантный суперадмин временно получает Admin; при отказе вызывается AuditService.LogDenied
RequireCrossTenantAccess() / RequirePathTenantMatch()access.goШлюз операций уровня платформы и проверка согласованности тенанта в URL
RequireKBAccess(resolver, perm, ...)kb_access.goАдаптирует трёхуровневый разбор доступа к KB из internal/application/access (собственная → общая в организации → только чтение для общего агента) и перезаписывает TenantIDContextKey в Request.Context() на тенант-источник KB, чтобы последующий поиск автоматически попадал в данные нужного тенанта
asynqdl.Middleware()asynqdl/Не HTTP: при исчерпании бюджета повторных попыток задачи Asynq пишет в таблицу task_dead_letters, можно повесить коллбэк OnDeadLetter для синхронизации бизнес-состояния (например, пометить разбор знания как неуспешный)

Обзор доменных моделей (internal/types) ​

internal/types/ содержит около 26 персистентных сущностей GORM. Ключевые связи:

Ключевые решения:

  • Изоляция мультитенантности: почти все сущности несут TenantID; tenant_id=0 означает уровень системы (например, системный аудит);
  • Шифрование чувствительных полей в покое: Model.Parameters, VectorStore.ConnectionConfig, StorageBackend.Config, DataSource.Config, TenantAPIKey.APIKey и т. д. при GORM Value() шифруются AES-256-GCM ключом SYSTEM_AES_KEY (32 байта), при Scan() происходит снисходительная расшифровка (ошибка расшифровки трактуется как «не настроено», а не как ошибка);
  • Неизменяемость привязок при создании: VectorStoreID KB (gorm-тег <-:create) и StorageBackendID после создания нельзя изменить, что гарантирует согласованность индекса/файлов;
  • Асинхронный конечный автомат: Knowledge.ParseStatus из семи состояний + PendingSubtasksCount отслеживает параллельные подзадачи обогащения этапа finalizing (summary/question/graph);
  • Аудит append-only: AuditLog без полей обновления/мягкого удаления, покрывает 50+ видов AuditAction;
  • Важные не-сущностные типы: SearchResult результат поиска, Pagination, Task/топология очередей (task.go), различные структуры конфигурации JSONB (ChunkingConfig, IndexingStrategy, CustomAgentConfig и др.), context key и хелперы доступа (context_helpers.go).

Соглашения об обработке ошибок (internal/errors) ​

Единый носитель ошибок — AppError:

go
// internal/errors/errors.go
type AppError struct {
    Code     ErrorCode // бизнес-код ошибки
    Message  string
    Details  any
    HTTPCode int       // маппинг HTTP-статуса
}
  • Диапазоны кодов ошибок: 1000–1999 — общая семантика HTTP (ErrBadRequest=1000, ErrUnauthorized=1001, ErrForbidden=1002, ErrNotFound=1003, ErrTooManyRequests=1006, ErrServiceUnavailable=1008); 2000–2099 — тенант; 2100–2199 — агент; 2200–2299 — векторное хранилище;
  • Конструкторы: NewBadRequestError / NewUnauthorizedError / NewForbiddenError / NewNotFoundError / NewValidationError / NewConflictError / NewTooManyRequestsError / NewServiceUnavailableError и др.;
  • Способ применения: Handler/middleware вешают ошибку через c.Error(appErr), middleware ErrorHandler в конце единообразно рендерит конверт {success:false, error:{code,message,details}}, фронтенд делает i18n по error.code; всё, что не AppError, — всегда 500;
  • session.go предоставляет сигнальные ошибки области сессий (ErrSessionNotFound и др.); parse_error_codes.go определяет строковые коды ошибок этапа разбора документов (DOCREADER_TIMEOUT, EMBEDDING_RATE_LIMIT, VECTORSTORE_WRITE_FAILED, TASK_TIMEOUT и др.), которые попадают в Knowledge.ErrorMessage для перевода и показа во фронтенде.

Система логирования (internal/logger) ​

  • На базе logrus, приватный синглтон appLogger + собственный Formatter (цветной вывод в терминал; шаблон можно задать через LOG_FORMAT плейсхолдерами %d %level %traceId %msg и др.; при заданном LOG_PATH идёт запись в файл с ротацией lumberjack и удалением ANSI-цветов);
  • Сквозной request_id: middleware.RequestID пишет в context → logger.GetLogger(ctx) автоматически извлекает и внедряет поле request_id; типовые вызовы — logger.Infof/Warnf/Errorf(ctx, format, ...) и ErrorWithFields;
  • Отладочные логи LLM (llm_logger.go): включаются при LLM_DEBUG_LOG=true, по файлу на request_id записывают каждый вызов LLM (LLMCallRecord: CallType Chat/Embedding/Rerank/VLM, модель, длительность, полные сообщения и вызовы инструментов, ошибки), автоочистка через 7 дней, применяется для разбора проблем с промптами и контекстом.

Ключевые библиотеки утилит (internal/common, internal/utils) ​

РасположениеУтилитаНазначение
common/tools.goDeduplicate / DeduplicateWithScore, ParseLLMJsonResponse, CleanInvalidUTF8, серия PipelineLogОбобщённая дедупликация (при слиянии результатов поиска оставляет наивысший балл), разбор блока кода ```json, возвращённого LLM, очистка недопустимого UTF-8, логирование этапов RAG-конвейера
common/db_retry.goWithDeadlockRetry(ctx, fn)Повтор при обнаружении взаимоблокировки БД (максимум 3 раза, откат 50→100→200ms)
common/redis_tls.goRedisTLSConfig()Формирование конфигурации TLS Redis по переменным окружения REDIS_USE_TLS и др.
utils/crypto.goEncryptAESGCM / DecryptAESGCM (префикс enc:v1:, идемпотентно)Базовая реализация шифрования в покое всех упомянутых выше чувствительных полей
utils/security.goSanitizeHTML, ValidateFilePath, SanitizeForLogОчистка XSS, защита от обхода каталогов, маскирование в логах
utils/inject.goValidateSQL (на базе pganalyze/pg_query_go)Проверка таблиц по белому списку и обнаружение шаблонов инъекций для SQL, генерируемого анализом данных агента
utils/presign.goSignFileURL / VerifyFileURLSig, SystemHMACKeyHMAC-SHA256 presigned URL файлов (по умолчанию 2h, используется для встроенных изображений в IM); ключ подписи берётся из SYSTEM_SIGNING_KEY, при отсутствии — из SYSTEM_AES_KEY
utils/oidc_state.goGenerateState / ValidateStateHMAC-подпись state авторизации OIDC и TTL 10 минут (защита от CSRF)
utils/log_sanitize.goCompactImageDataURLForLogОбрезка слишком длинных data URL изображений, чтобы логи не «взрывались»
utils/storage_error.goSanitizeStorageConnectivityErrorПреобразование ошибки подключения к хранилищу в понятное пользователю сообщение со скрытием внутренних имён хостов
Остальныеtaskid.go / fileutil.go / filesize.go / httputil.go / json.goИдентификаторы задач, форматирование файлов и размеров, HTTP-загрузка, генерация JSON Schema и др.

На этом полный путь бэкенда от запуска процесса, сборки зависимостей и приёма запроса до записи данных в хранилище замкнут. Последующие разделы отдельно раскрывают внутреннее устройство конвейера поиска RAG (chat_pipeline), движка агентов (internal/agent) и сервиса разбора документов (docreader).

Версия v0.8.3 · MIT License