Дизайн бэкенда на Go
Бэкенд на Go организует обработку запросов по слоям Handler, Service, Repository и инфраструктуры, а зависимости собирает через uber/dig. cmd/server отвечает за запуск и остановку, internal/ реализует маршрутизацию с проверкой прав, бизнес-сервисы и доступ к хранилищам.
Слоистая архитектура
Бэкенд следует классической четырёхуровневой структуре Handler → Service → Repository → база данных; зависимости между слоями полностью развязаны через интерфейсы (internal/types/interfaces/) и собираются DI-контейнером при запуске:
| Слой | Расположение | Назначение |
|---|---|---|
| Router / Middleware | internal/router/, internal/middleware/ | Регистрация маршрутов, аутентификация, RBAC, ограничение скорости, логирование, конверт ошибок |
| Handler | internal/handler/ (связанное с сессиями — в internal/handler/session/) | Разбор параметров запроса (DTO в internal/handler/dto/), вызов Service, запись ответа; без бизнес-логики |
| Контроль доступа | internal/application/access/ | Общие правила авторизации, не зависящие от Gin: трёхуровневый разбор доступа к базам знаний, права организационного шаринга, область баз знаний общего агента, авторизация файлов и артефактов сообщений, авторизация клонирования/перемещения между базами |
| Service | internal/application/service/ (около 160+ файлов) | Оркестрация бизнес-логики: базы знаний/знания/блоки, сессии и конвейер chat_pipeline/, агент, тенант и участники, модели, синхронизация источников данных, Wiki, аудит и др. |
| Repository | internal/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:
// 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 определяет форму работы:
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) собирается в следующем порядке, и порядок и есть семантика безопасности:
gin.New()+MultipartFormCleanup(удаление временных файлов разбора multipart после завершения запроса) +SetTrustedProxies(WEKNORA_TRUSTED_PROXIES, по умолчанию доверяет только loopback и приватным подсетям, чтобы подделкаX-Forwarded-Forне обходила ограничение скорости по IP);- глобальные middleware:
cors→RequestID→Language→Logger→Recovery→ErrorHandler; - эндпоинты без аутентификации:
GET /health; в не-release режиме монтируется/swagger/*any; - CSP-middleware
frame-ancestorsдля страниц Embed; в версии Lite встроенные статические ресурсы фронтенда (handler.Edition == "lite"); - публичные маршруты, регистрируемые до аутентификации: обратные вызовы платформ 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/*); middleware.Auth(...)глобальная аутентификация; далее идут требующие аутентификации маршруты файлового прокси, маршруты presigned-файлов без аутентификации но с проверкой подписи, middleware trace Langfuse,AuditServiceProvider;v1 := r.Group("/api/v1"): сначалаv1.Use(rbacGuards.apiKeyAuthorizer.Middleware())(шлюз API Key, JWT-сессии пропускаются сразу), затем последовательно вызываются более 30 функцийRegisterXxxRoutes(v1, handler, rbacGuards);- финальная самопроверка:
rbacGuards.assertAPIKeyPoliciesMatchRoutes(r)— если объявленная политика API Key указывает на несуществующий шаблон маршрута (дрейф пути/опечатка), запуск сразу падает с panic, что исключает появление мёртвой политики, всегда отдающей 403.
Обзор групп маршрутов
| Префикс группы | Функция Register | Пример политики API Key |
|---|---|---|
/auth, /me | RegisterAuthRoutes / RegisterMyInvitationRoutes | большинство без Key |
/tenants, /tenants/:id/* (участники/приглашения/аудит) | RegisterTenantRoutes | manage_members / manage_spaces; группа /:id вешает PathTenantMatch() |
/knowledge-bases, `/knowledge-bases/:id/knowledge | faq | tags |
/knowledge, /chunks | RegisterKnowledgeRoutes / RegisterChunkRoutes | ingest |
/sessions, /knowledge-chat, /agent-chat, /knowledge-search, /messages | RegisterSessionRoutes / RegisterChatRoutes и др. | chat / retrieve |
/models, /evaluation | RegisterModelRoutes / RegisterEvaluationRoutes | manage_models / run_evaluations |
/sandbox-configs, /me/env-vars, /me/browser | RegisterSandboxConfigRoutes / RegisterMyEnvVarRoutes и др. | full_access; /me/* только для самого JWT-пользователя |
/system, /system/admin | RegisterSystemRoutes / RegisterSystemAdminRoutes | группа admin принудительно требует g.SystemAdmin() |
/mcp-services, /agent, /web-search, /web-search-providers | соответствующие функции Register | manage_mcp_services / manage_web_search |
/vector-stores, /storage-backends | RegisterVectorStoreRoutes / RegisterStorageBackendRoutes | manage_vector_stores / manage_storage_backends |
/agents, `/agents/:id/shares | embed-channels | im-channels` |
/organizations, /user/favorites, /skills | соответствующие функции Register | manage_spaces и др. |
/im-channels, /embed-channels, /mcp-endpoints, /wechat | RegisterIMChannelRoutes / RegisterEmbedChannelRoutes / RegisterMCPEndpointRoutes | manage_channels |
/datasource, /knowledgebase/:kb_id/wiki, /chunker/preview | RegisterDataSourceRoutes / RegisterWikiPageRoutes / RegisterChunkerDebugRoutes | manage_datasources / ingest |
rbacGuards: централизованная матрица прав
internal/router/rbac.go определяет rbacGuards, которые NewRouter конструирует один раз и передаёт в каждую функцию Register. Стражей три вида, они используются inline в строках маршрутов, так что требования к правам видны с первого взгляда:
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/langfuse | trace наблюдаемости 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и т. д. при GORMValue()шифруются AES-256-GCM ключомSYSTEM_AES_KEY(32 байта), приScan()происходит снисходительная расшифровка (ошибка расшифровки трактуется как «не настроено», а не как ошибка); - Неизменяемость привязок при создании:
VectorStoreIDKB (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:
// 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), middlewareErrorHandlerв конце единообразно рендерит конверт{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.go | Deduplicate / DeduplicateWithScore, ParseLLMJsonResponse, CleanInvalidUTF8, серия PipelineLog | Обобщённая дедупликация (при слиянии результатов поиска оставляет наивысший балл), разбор блока кода ```json, возвращённого LLM, очистка недопустимого UTF-8, логирование этапов RAG-конвейера |
common/db_retry.go | WithDeadlockRetry(ctx, fn) | Повтор при обнаружении взаимоблокировки БД (максимум 3 раза, откат 50→100→200ms) |
common/redis_tls.go | RedisTLSConfig() | Формирование конфигурации TLS Redis по переменным окружения REDIS_USE_TLS и др. |
utils/crypto.go | EncryptAESGCM / DecryptAESGCM (префикс enc:v1:, идемпотентно) | Базовая реализация шифрования в покое всех упомянутых выше чувствительных полей |
utils/security.go | SanitizeHTML, ValidateFilePath, SanitizeForLog | Очистка XSS, защита от обхода каталогов, маскирование в логах |
utils/inject.go | ValidateSQL (на базе pganalyze/pg_query_go) | Проверка таблиц по белому списку и обнаружение шаблонов инъекций для SQL, генерируемого анализом данных агента |
utils/presign.go | SignFileURL / VerifyFileURLSig, SystemHMACKey | HMAC-SHA256 presigned URL файлов (по умолчанию 2h, используется для встроенных изображений в IM); ключ подписи берётся из SYSTEM_SIGNING_KEY, при отсутствии — из SYSTEM_AES_KEY |
utils/oidc_state.go | GenerateState / ValidateState | HMAC-подпись state авторизации OIDC и TTL 10 минут (защита от CSRF) |
utils/log_sanitize.go | CompactImageDataURLForLog | Обрезка слишком длинных data URL изображений, чтобы логи не «взрывались» |
utils/storage_error.go | SanitizeStorageConnectivityError | Преобразование ошибки подключения к хранилищу в понятное пользователю сообщение со скрытием внутренних имён хостов |
| Остальные | taskid.go / fileutil.go / filesize.go / httputil.go / json.go | Идентификаторы задач, форматирование файлов и размеров, HTTP-загрузка, генерация JSON Schema и др. |
На этом полный путь бэкенда от запуска процесса, сборки зависимостей и приёма запроса до записи данных в хранилище замкнут. Последующие разделы отдельно раскрывают внутреннее устройство конвейера поиска RAG (chat_pipeline), движка агентов (internal/agent) и сервиса разбора документов (docreader).
