Skip to content

Conception backend Go ​

Le backend Go organise le traitement des requêtes en couches Handler, Service, Repository et infrastructure, avec assemblage des dépendances via uber/dig. cmd/server gère le démarrage et l’arrêt ; internal/ implémente routage, authentification, services métier et accès aux données.

Module assistant et exécution de tâches ​

internal/agent/policy unifie les décisions lecture, sonde, écriture et opérations destructives ; agent_prompts et agent_run_suspensions conservent les interactions humaines reprises ; agent_grants stocke les autorisations à portée limitée. agent_collaborations enregistre l’état des sous-agents ; le service d’automatisations maintient plans, exécutions et notifications. Le shell d’arrière-plan utilise un identifiant de travail distinct avec interfaces d’attente et d’annulation, distinct du terminal interactif.

Ces capacités d’exécution ont un périmètre différent de la file d’indexation documentaire classique : reprendre un état en attente d’approbation ne signifie pas réexécuter arbitrairement des écritures externes.

Architecture en couches ​

Le backend suit la structure classique Handler → Service → Repository → base de données. Les dépendances inter-couches passent par des interfaces (internal/types/interfaces/), assemblées au démarrage par le conteneur DI :

CoucheEmplacementRôle
Router / Middlewareinternal/router/、internal/middleware/Enregistrement des routes, auth, RBAC, rate limiting, logs, enveloppe d’erreur
Handlerinternal/handler/ (session dans internal/handler/session/)Parsing des requêtes (DTO dans internal/handler/dto/), appel Service, réponse ; sans logique métier
Contrôle d’accèsinternal/application/access/Règles d’autorisation partagées hors Gin : résolution d’accès KB à trois niveaux, partage organisation, portée KB des agents partagés, autorisation fichiers et artefacts de messages, clonage/déplacement inter-bases
Serviceinternal/application/service/ (~160+ fichiers)Orchestration : KB/connaissances/chunks, sessions et pipeline chat_pipeline/, Agent, tenant et membres, modèles, sync sources de données, Wiki, audit, etc.
Repositoryinternal/application/repository/ (~60 fichiers)Accès données via GORM (type knowledgeRepository struct { db *gorm.DB }, r.db.WithContext(ctx)) ; implémentations moteur de recherche par package sous repository/retriever/{postgres,elasticsearch,qdrant,milvus,weaviate,doris,opensearch,tencentvectordb,sqlite,neo4j}
Modèle de domaineinternal/types/Entités GORM, énumérations, clés context, interfaces (types/interfaces)
Infrastructureinternal/infrastructure/ (client gRPC docparser, web_search), internal/models/, internal/stream/, internal/sandbox/, internal/mcp/, internal/im/Adaptateurs systèmes externes

Conventions clés :

  • Un Handler ne dépend que d’interfaces Service (ex. interfaces.KnowledgeService) ; un Service ne dépend que d’interfaces Repository et d’autres Services ;
  • Toutes les interfaces sont déclarées dans internal/types/interfaces/, liées via dig ;
  • Les workers Asynq et le serveur HTTP tournent dans le même processus, les handlers réutilisent les mêmes Services.

Injection de dépendances : internal/container (uber/dig) ​

WeKnora utilise go.uber.org/dig v1.19.0 (conteneur par constructeurs, sans wire généré). Point d’entrée : BuildContainer dans 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 (peut être nil : mode Lite)
    ...
    must(container.Provide(repository.NewTenantRepository))
    must(container.Provide(service.NewTenantService))
    ...
    must(container.Provide(router.NewRouter)) // produit final *gin.Engine
    return container
}

runtime.GetContainer() (internal/runtime/container.go) détient le singleton global dig.Container ; must(err) panic si l’enregistrement échoue — erreur DI fatale au démarrage.

Fonctionnalités dig utilisées ​

FonctionnalitéExemple
dig.AsLier une implémentation à une interface : container.Provide(NewResourceCleaner, dig.As(new(interfaces.ResourceCleaner))) ; router.NewAsyncqClient lié à interfaces.TaskEnqueuer
Dépendances nommées dig.NamePlusieurs instances d’une même interface : 4 services d’extraction, 6 serveurs Asynq, wikiIngest
Struct param dig.Inrouter.RouterParams avec dig.In, ~60 handlers/services injectés
container.Invoke pour effets de bordComposants démarrés à l’enregistrement : cleanup pool, web search providers, schedulers, plugins chatpipeline, router.RunAsynqServer, etc.
Provide adaptateurClosures de conversion d’interface ; RetrieveEngineRegistry aussi exposé comme StoreRegistry

Ordre d’enregistrement et assemblage conditionnel ​

BuildContainer comporte neuf phases (logs correspondants dans les sources) : ① infra cœur → ② registre moteurs de recherche → ③ clients externes → ④ Repository → ⑤ Service → ⑥ exécuteur de tâches conditionnel → ⑦ plugins chat_pipeline → ⑧ Handlers + IM → ⑨ Router et démarrage Asynq.

L’étape ⑥ est la branche la plus importante — présence ou absence de 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 pools worker + AsynqInspector
    must(container.Invoke(registerModelConcurrencyLimiter))   // Redis : porte de concurrence distribuée par modèle
} else {
    syncExec := router.NewSyncTaskExecutor()                  // mode Lite : exécuteur synchrone in-process
    must(container.Provide(func() interfaces.TaskEnqueuer { return syncExec }))
    must(container.Provide(router.NewNoopTaskInspector))
    must(container.Invoke(registerLiteModelConcurrencyLimiter)) // sémaphore in-process
}

Concurrence des 6 pools Asynq ajustable via system settings / env (Core=8, PostProcess=2, Enrichment=12, Maintenance=4, Shared=6, Wiki=8 par défaut, WEKNORA_ASYNQ_*_CONCURRENCY) ; topologie des files dans internal/types/task.go.

Nettoyage des ressources et usines ​

  • ResourceCleaner : enregistrement de destructeurs, Cleanup(ctx) à l’arrêt ;
  • EngineFactory : création runtime des moteurs selon vector_stores ;
  • initDatabase : migrations, backfill, modèles builtin UPSERT ; SQLite SetMaxOpenConns(1).

Flux de démarrage cmd/server ​

cmd/server : main.go, bootstrap.go, listen.go, signaux signals_unix.go / signals_windows.go.

Points clés :

  • Échec du bootstrap ne bloque pas le démarrage : logger.Warnf ; promotion admin système uniquement s’il n’y en a pas encore ;
  • Arrêt gracieux en deux temps : premier SIGTERM/SIGINT → server.Shutdown ; second signal → Close forcé ;
  • Retry port occupé : listenWithRetry, 10 tentatives, backoff exponentiel à partir de 300 ms.

Organisation des routes et RBAC (internal/router) ​

Ordre d’assemblage NewRouter ​

NewRouter(params RouterParams) dans internal/router/router.go — l’ordre est sémantique pour la sécurité :

  1. gin.New() + MultipartFormCleanup + SetTrustedProxies (WEKNORA_TRUSTED_PROXIES) ;
  2. Middleware global : cors → RequestID → Language → Logger → Recovery → ErrorHandler ;
  3. Endpoints sans auth : GET /health ; /swagger hors release ;
  4. CSP Embed ; statiques Lite si handler.Edition == "lite" ;
  5. Avant auth : callbacks IM, routes Embed publiques, MCP Server, grants URL, WebSocket sandbox/ticket, extension navigateur local ;
  6. middleware.Auth ; proxy fichiers authentifié ; presigned ; Langfuse ; AuditServiceProvider ;
  7. v1 := r.Group("/api/v1") : apiKeyAuthorizer puis ~30 RegisterXxxRoutes ;
  8. Self-check : assertAPIKeyPoliciesMatchRoutes — panic si politique API Key pointe vers une route inexistante.

Aperçu des groupes de routes ​

PréfixeRegisterExemple politique API Key
/auth、/meRegisterAuthRoutes / RegisterMyInvitationRoutessouvent sans clé
/tenants、/tenants/:id/*RegisterTenantRoutesmanage_members / manage_spaces ; PathTenantMatch()
/knowledge-bases…RegisterKnowledgeBaseRoutes, etc.retrieve / ingest
/knowledge、/chunksRegisterKnowledgeRoutes / RegisterChunkRoutesingest
sessions, chat, search, messagesRegisterSessionRoutes / RegisterChatRoutes, etc.chat / retrieve
/models、/evaluationRegisterModelRoutes / RegisterEvaluationRoutesmanage_models / run_evaluations
sandbox, /me/*RegisterSandboxConfigRoutes, etc.full_access ; /me/* JWT utilisateur
/system、/system/adminRegisterSystemRoutes / RegisterSystemAdminRoutesadmin : g.SystemAdmin()
MCP, agent, web-searchdiversmanage_mcp_services / manage_web_search
vector-stores, storage-backendsRegisterVectorStoreRoutes, etc.manage_vector_stores / manage_storage_backends
agents, organizations, skillsdiversfull_access / manage_spaces, etc.
canaux IM/embed/MCP/wechatdiversmanage_channels
datasource, wiki, chunker previewdiversmanage_datasources / ingest

rbacGuards : matrice centralisée ​

internal/router/rbac.go — trois familles de gardes :

go
kb.PUT("/:id", g.OwnedKBOrAdmin(), handler.UpdateKnowledgeBase)
  • Gardes de rôle : Viewer() / Contributor() / Admin() / Owner() / SystemAdmin(), etc. ;
  • Gardes de propriété : OwnedKBOrAdmin(), OwnedAgentOrAdmin(), lookups via URL vers creator_id ;
  • Accès KB : KBAccessRead|Write et variantes From*ID ;
  • Frontière tenant : CrossTenant(), PathTenantMatch().

Tous respectent cfg.Tenant.EnableRBAC (mode grâce avec log si désactivé).

Politiques API Key orthogonales : apiKeyGroup, 17 capacités ; routes non déclarées → refus par défaut pour clé API.

Liste des middleware (internal/middleware) ​

Ordre de passage :

MiddlewareFichierRôle
MultipartFormCleanup()multipart_cleanup.goSupprime fichiers temp multipart après requête
cors.Newrouter.goEn-têtes autorisés incl. MCP Streamable HTTP
RequestID()logger.goUUID ou réutilisation X-Request-ID
Language()language.goLangue traitement doc : WEKNORA_LANGUAGE > Accept-Language > défaut zh-CN
Logger()logger.goLogs requête/réponse, masquage secrets, limite 10 KB
Recovery()recovery.goPanic → 500
ErrorHandler()error_handler.goEnveloppe {success:false, error:{code,message,details}} pour *errors.AppError
EmbedAuth / MCPEndpointAuthembed_auth.go, mcp_endpoint_auth.goAuth canaux publics
AttachAuthenticatedUserws_auth.goTicket court pour WebSocket
PublicAuthRateLimit()auth_public_ratelimit.go30 req/60 s/IP en mémoire
Auth(...)auth.goJWT, API Key, whitelist ; context tenant/rôle
langfuse.GinMiddleware()tracingTrace LLM ou no-op
AuditServiceProvider()audit_provider.goInjection audit
APIKeyRouteAuthorizerapi_key_gate.goGateway par route pour clés API
RequireRole, KB access, etc.rbac.go, kb_access.goRBAC ; réécriture TenantIDContextKey pour KB partagée
asynqdl.Middleware()asynqdl/Dead letter Asynq

Modèle de domaine (internal/types) ​

~26 entités GORM persistées. Relations principales :

Points de conception :

  • Isolation multi-tenant : TenantID quasi partout ; tenant_id=0 = système ;
  • Chiffrement statique : AES-256-GCM via SYSTEM_AES_KEY sur champs sensibles ;
  • Bindings immuables à la création : VectorStoreID, StorageBackendID ;
  • Machine à états async : ParseStatus sept états + PendingSubtasksCount ;
  • Audit append-only : 50+ AuditAction ;
  • Types non entité : SearchResult, Task, configs JSONB, helpers context.

Gestion des erreurs (internal/errors) ​

go
// internal/errors/errors.go
type AppError struct {
    Code     ErrorCode // code métier
    Message  string
    Details  any
    HTTPCode int       // mapping statut HTTP
}
  • Codes segmentés : 1000–1999 HTTP sémantique ; 2000+ tenant ; 2100+ Agent ; 2200+ vector store ;
  • Constructeurs NewBadRequestError, etc. ;
  • Handler : c.Error(appErr) → ErrorHandler → i18n frontend via error.code ;
  • Codes parsing document dans parse_error_codes.go → Knowledge.ErrorMessage.

Journalisation (internal/logger) ​

  • logrus, formatter personnalisé, rotation LOG_PATH ;
  • request_id via context ;
  • LLM debug (llm_logger.go) si LLM_DEBUG_LOG=true, rétention 7 jours.

Utilitaires (internal/common, internal/utils) ​

EmplacementOutilUsage
common/tools.goDeduplicate, ParseLLMJsonResponse, etc.RAG, UTF-8
common/db_retry.goWithDeadlockRetryDeadlock DB
common/redis_tls.goRedisTLSConfig()TLS Redis
utils/crypto.goAES-GCM enc:v1:Secrets
utils/security.goSanitizeHTML, ValidateFilePathXSS, path traversal
utils/inject.goValidateSQLAgent SQL
utils/presign.goHMAC URLs fichiers2 h défaut
utils/oidc_state.goState OIDC 10 minCSRF
autrestaskid, fileutil, httputil, jsondivers

La chaîne complète du démarrage à la persistance est bouclée. Les chapitres suivants détaillent le pipeline RAG (chat_pipeline), le moteur Agent (internal/agent) et docreader.

Documentation issue de WeKnora v0.9.0 · MIT License