From 53d23418e9050a738e10a860f9dfda514473be20 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 18 Aug 2026 22:09:37 +0200 Subject: [PATCH] CI : gofmt + gopls sous Go 1.25 ; MCP : premier lancement npx/uvx patient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gofmt sur trois fichiers oubliés par le dernier commit (ci/test rouge). - gopls@latest exige Go ≥ 1.26 alors que l'image de build est en 1.25 avec GOTOOLCHAIN=local : l'étape Docker échouait net. GOTOOLCHAIN=auto télécharge le toolchain requis, borné à l'étape de build (build-push GHCR rouge). - Serveurs MCP : npx/uvx téléchargent leur paquet au premier lancement, et le délai de connexion de 20 s tombait dessus (« enregistré mais connexion échouée : context deadline exceeded » depuis le catalogue). Délai à froid de 3 min pour ces deux lanceurs, et erreur de délai réécrite pour dire quoi faire (réessayer : le paquet reste en cache). - README : note sur ce premier lancement, ligne mode Code dans le tableau des différences. Co-Authored-By: Claude Opus 5 --- Dockerfile | 6 +++++- README.md | 6 +++++- internal/loki/llm_client.go | 1 + internal/loki/mcp_client.go | 40 +++++++++++++++++++++++++++++++++++-- internal/loki/web_api.go | 4 ++-- internal/loki/web_server.go | 8 ++++---- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9090cd5..7a8edee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,11 @@ COPY tools/ tools/ RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/loki ./cmd/loki # gopls : serveur LSP Go, consommé par le vérificateur de code du mode code # (lsp.go). Compilé ici parce que l'image finale n'a pas de toolchain Go. -RUN GOBIN=/out go install golang.org/x/tools/gopls@latest +# GOTOOLCHAIN=auto : gopls@latest exige régulièrement un Go plus récent que +# celui de l'image (v0.23 veut ≥ 1.26 quand l'image est en 1.25) ; en local, +# l'installation échouait net. Auto télécharge le toolchain requis — coût +# limité à cette étape de build. +RUN GOBIN=/out GOTOOLCHAIN=auto go install golang.org/x/tools/gopls@latest # ── Étape 2 : runtime sur l'image serveur CUDA officielle ─────────────── FROM ${LLAMACPP_IMAGE} AS runtime diff --git a/README.md b/README.md index e2804e4..135a034 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,10 @@ Héritées d'AJEAN : [Crawl4AI](https://github.com/unclecode/crawl4ai) pour les pages JS. - **Agent** : shell, fichiers, workspace (`agent on`). - **Serveurs MCP** : Node.js (`npx`) et uv (`uvx`) sont inclus dans l'image, pour - les serveurs écrits en JavaScript comme en Python. + les serveurs écrits en JavaScript comme en Python. Au **premier** lancement, + `npx`/`uvx` téléchargent le paquet du serveur — Loki attend jusqu'à 3 minutes + ce coup-là (au lieu d'échouer sur « context deadline exceeded ») ; les + lancements suivants partent du cache en quelques secondes. - **Presets** de configuration par modèle, bench, auto-détection GPU. - **API OpenAI-compatible** exposable, protégée par clé (voir ci-dessous — ce fork la sert autrement que l'amont). @@ -307,6 +310,7 @@ Retirées par ce fork : | Configuration initiale | `ajean edit` ($EDITOR) | entrypoint + `loki config set` | | Choix du modèle | lien Hugging Face collé à la main | recherche intégrée + verdict VRAM + projecteur lié | | Historique de tchat | conversation unique | discussions multiples, titrées et persistées | +| Agent de code | — | mode Code : critères d'acceptation, passe de vérification, LSP, outils git | | Accès distant | relais chiffré ajean.link | retiré de l'interface | | Endpoint OpenAI | `:8080/v1` du moteur, ouvert par `network on` | `/v1` servi par Loki sur le port de l'interface, protégé par la clé API | | Mise à jour | `ajean update` (binaire GitHub) | `docker compose pull` | diff --git a/internal/loki/llm_client.go b/internal/loki/llm_client.go index 9085b3e..e9546f3 100644 --- a/internal/loki/llm_client.go +++ b/internal/loki/llm_client.go @@ -344,6 +344,7 @@ type StreamEvent struct { // ajouter. NewHistory []Message } + // AskEvent : l'outil ask — question structurée posée à l'utilisateur, rendue // par l'UI comme une carte à boutons. Le tour se termine juste après. type AskEvent struct { diff --git a/internal/loki/mcp_client.go b/internal/loki/mcp_client.go index fd932b1..4d16c9e 100644 --- a/internal/loki/mcp_client.go +++ b/internal/loki/mcp_client.go @@ -3,10 +3,12 @@ package loki import ( "context" "encoding/json" + "errors" "fmt" "net/http" "os" "os/exec" + "path/filepath" "regexp" "sort" "strings" @@ -37,6 +39,13 @@ const ( // mcpConnectTimeout borne l'établissement d'une session (handshake initialize // + tools/list). Un serveur qui rame ne doit pas figer le tour de chat. mcpConnectTimeout = 20 * time.Second + // mcpConnectTimeoutCold : npx et uvx TÉLÉCHARGENT le paquet du serveur au + // premier lancement (le catalogue les utilise tous les deux). Sur un réseau + // ou un disque lents, ça dépasse largement 20 s, et l'utilisateur voyait + // « enregistré mais connexion échouée : context deadline exceeded » pour un + // serveur parfaitement sain. Les lancements suivants profitent du cache et + // retombent dans le régime normal. + mcpConnectTimeoutCold = 3 * time.Minute // mcpCallTimeout borne un appel d'outil MCP. mcpCallTimeout = 120 * time.Second // mcpMaxOutput cap la sortie renvoyée au modèle (cohérent avec toolMaxOutput). @@ -174,11 +183,11 @@ func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession { m.mu.Unlock() // Connexion hors-lock (peut être lente). - ctx, cancel := context.WithTimeout(context.Background(), mcpConnectTimeout) + ctx, cancel := context.WithTimeout(context.Background(), mcpTimeoutFor(cfg)) defer cancel() sess, err := mcpConnect(ctx, name, cfg) if err != nil { - s.err = err + s.err = mcpFriendlyErr(err, cfg) } else { s.sess = sess lt, lerr := sess.ListTools(ctx, nil) @@ -509,3 +518,30 @@ func MCPStatus() ([]MCPServerStatus, error) { } return out, nil } + +// mcpTimeoutFor choisit le délai de connexion d'un serveur : long pour un +// lanceur qui télécharge son paquet à la volée (npx, uvx), court sinon. +func mcpTimeoutFor(cfg MCPServerConfig) time.Duration { + base := strings.ToLower(filepath.Base(cfg.Command)) + base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd") + if base == "npx" || base == "uvx" { + return mcpConnectTimeoutCold + } + return mcpConnectTimeout +} + +// mcpFriendlyErr habille l'erreur de connexion la plus vécue : le délai +// dépassé d'un npx/uvx en plein téléchargement. L'erreur brute (« context +// deadline exceeded ») envoyait l'utilisateur chercher un bug de config alors +// qu'il suffit souvent de réessayer une fois le paquet en cache. +func mcpFriendlyErr(err error, cfg MCPServerConfig) error { + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + return err + } + base := strings.ToLower(filepath.Base(cfg.Command)) + base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd") + if base == "npx" || base == "uvx" { + return fmt.Errorf("délai dépassé — %s télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)", base) + } + return fmt.Errorf("délai dépassé — le serveur n'a pas répondu au handshake en %s", mcpConnectTimeout) +} diff --git a/internal/loki/web_api.go b/internal/loki/web_api.go index 8826354..fe3dbb0 100644 --- a/internal/loki/web_api.go +++ b/internal/loki/web_api.go @@ -61,8 +61,8 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { "port": LLMPort(), "ctx": ctx, "version": Version, - "warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien - "load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon + "warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien + "load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon }) } diff --git a/internal/loki/web_server.go b/internal/loki/web_server.go index 53e5bef..534619c 100644 --- a/internal/loki/web_server.go +++ b/internal/loki/web_server.go @@ -267,10 +267,10 @@ func newWebMux() *http.ServeMux { api("/api/conversations/delete", handleConvDelete) api("/api/chat/mode", handleChatMode) // GET mode+critères / POST bascule Chat|Code api("/api/chat/criteria", handleChatCriteria) // édition manuelle des critères (mode code) - api("/api/chat/compact", handleChatCompact) // compaction manuelle du contexte - api("/api/chat/state", handleChatState) // instantané léger {seq, generating, ctx_used} - api("/api/chat/export", handleChatExport) // téléchargement du fil (?format=md|json) - api("/api/e2e/chat", handleE2EChat) // même flux mais chiffré E2E (boîte noire via le relais) + api("/api/chat/compact", handleChatCompact) // compaction manuelle du contexte + api("/api/chat/state", handleChatState) // instantané léger {seq, generating, ctx_used} + api("/api/chat/export", handleChatExport) // téléchargement du fil (?format=md|json) + api("/api/e2e/chat", handleE2EChat) // même flux mais chiffré E2E (boîte noire via le relais) return mux }