mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
Conversation tenue par la façade : le Go lui remet ce qui lui arrive
Avec LOKI_CHAT=rust, la façade sert les routes du chat. Deux chemins arrivent pourtant au Go sans passer par elle — le tunnel du relais et sa redistribution /api/e2e/req — et liraient la conversation du Go, périmée, voire l'écriraient en base par-dessus celle de la façade. facade_chat.go : - routes du chat remises à la façade (après requireWebAuth), avec le jeton interne qui y tient lieu d'authentification, flux compris ; - /api/e2e/chat lit le flux de la façade et le rechiffre événement par événement (nombres repris tels quels) ; - les événements d'affichage nés ici (bascule du mode Code, critères) vont au journal de la façade (/internal/facade/v1/delta) ; - l'export lit la discussion enregistrée ; - le déverrouillage de la mémoire fait recharger la façade. L'outil d'exécution ne rend plus les critères : critNotify les pousse lui- même, d'où qu'il soit appelé. Sans LOKI_CHAT=rust, rien ne change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
10 files changed
+374
-52
No files matched your search
@@ -0,0 +1,203 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// facade_chat.go — conversation tenue par la façade Rust (LOKI_CHAT=rust, lot 6).
|
||||
//
|
||||
// La façade sert elle-même les routes du chat sur le port public. Deux chemins
|
||||
// arrivent pourtant ICI sans passer par elle : le tunnel du relais, qui sert ce
|
||||
// mux nu (/api/e2e/*), et sa redistribution /api/e2e/req dans le mux local. Ils
|
||||
// liraient la conversation du Go — périmée — et pourraient même l'écrire en
|
||||
// base par-dessus celle de la façade. Quand la façade tient la conversation :
|
||||
//
|
||||
// - les routes du chat lui sont remises, authentifiées par le jeton interne
|
||||
// (la requête l'a déjà été ici) ;
|
||||
// - /api/e2e/chat lit son flux et le rechiffre événement par événement ;
|
||||
// - les événements d'affichage nés ici (bascule du mode Code, critères) vont
|
||||
// dans SON journal ;
|
||||
// - l'export lit la discussion enregistrée ;
|
||||
// - le déverrouillage de la mémoire lui fait recharger la discussion.
|
||||
|
||||
const (
|
||||
facadeChatEnv = "LOKI_CHAT"
|
||||
facadeAddrEnv = "LOKI_FACADE_ADDR"
|
||||
)
|
||||
|
||||
// facadeChat : la façade tient-elle la conversation ? Il faut aussi son
|
||||
// adresse et le jeton interne, sans lesquels rien ne peut lui être remis.
|
||||
func facadeChat() bool {
|
||||
return strings.EqualFold(strings.TrimSpace(os.Getenv(facadeChatEnv)), "rust") &&
|
||||
facadeAddr() != "" && internalToken() != ""
|
||||
}
|
||||
|
||||
func facadeAddr() string { return strings.TrimSpace(os.Getenv(facadeAddrEnv)) }
|
||||
|
||||
// facadeChatRoutes : les routes que la façade sert quand elle tient la
|
||||
// conversation (chatapi.rs). Le reste (mode Code, critères, pièces jointes,
|
||||
// fichiers, export) reste ici.
|
||||
var facadeChatRoutes = map[string]bool{
|
||||
"/api/chat": true,
|
||||
"/api/chat/send": true,
|
||||
"/api/chat/stop": true,
|
||||
"/api/chat/reset": true,
|
||||
"/api/chat/state": true,
|
||||
"/api/chat/compact": true,
|
||||
"/api/chat/tool-result": true,
|
||||
"/api/conversations": true,
|
||||
"/api/conversations/new": true,
|
||||
"/api/conversations/switch": true,
|
||||
"/api/conversations/rename": true,
|
||||
"/api/conversations/delete": true,
|
||||
}
|
||||
|
||||
// withFacadeChat remet la requête à la façade quand elle tient la
|
||||
// conversation. Posé APRÈS requireWebAuth : seule une requête authentifiée
|
||||
// ici part, avec le jeton interne qui en tient lieu là-bas.
|
||||
func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc {
|
||||
if !facadeChatRoutes[path] {
|
||||
return h
|
||||
}
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !facadeChat() {
|
||||
h(w, r)
|
||||
return
|
||||
}
|
||||
facadeRelay(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// facadeRelay : relais transparent (flux compris) vers la façade.
|
||||
func facadeRelay(w http.ResponseWriter, r *http.Request) {
|
||||
rp := &httputil.ReverseProxy{
|
||||
Rewrite: func(pr *httputil.ProxyRequest) {
|
||||
pr.SetURL(&url.URL{Scheme: "http", Host: facadeAddr()})
|
||||
pr.Out.Header.Set(internalTokenHeader, internalToken())
|
||||
},
|
||||
FlushInterval: -1, // SSE : chaque trame part aussitôt
|
||||
ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) {
|
||||
sendJSON(w, http.StatusBadGateway, map[string]any{"ok": false, "error": "conversation indisponible : " + err.Error()})
|
||||
},
|
||||
}
|
||||
rp.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// facadeClient : appels courts vers l'API interne de la façade.
|
||||
var facadeClient = &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
// facadePost appelle l'API interne de la façade.
|
||||
func facadePost(path string, body any) error {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, "http://"+facadeAddr()+path, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(internalTokenHeader, internalToken())
|
||||
resp, err := facadeClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("façade : %s", resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// facadeDelta : un événement d'affichage né ici, journalisé par la façade
|
||||
// (tous les appareils le reçoivent). Rend false si la façade ne tient pas la
|
||||
// conversation : l'appelant journalise alors comme avant.
|
||||
func facadeDelta(delta map[string]any) bool {
|
||||
if !facadeChat() {
|
||||
return false
|
||||
}
|
||||
if err := facadePost("/internal/facade/v1/delta", delta); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[chat] événement non transmis à la façade : %v\n", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// facadeReload : la mémoire vient d'être déverrouillée ; la façade recharge la
|
||||
// discussion si elle l'avait chargée vide.
|
||||
func facadeReload() {
|
||||
if !facadeChat() {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := facadePost("/internal/facade/v1/reload", map[string]any{}); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[chat] rechargement de la discussion par la façade : %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// facadeChatStream : /api/e2e/chat quand la façade tient la conversation. Son
|
||||
// flux est lu ligne par ligne et chaque événement repart par out (scellé, un
|
||||
// sceau par événement). Les battements de la façade sont ignorés : ce flux a
|
||||
// les siens (sseHeartbeat).
|
||||
func facadeChatStream(ctx context.Context, body []byte, out *sseStream) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://"+facadeAddr()+"/api/chat", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set(internalTokenHeader, internalToken())
|
||||
resp, err := http.DefaultClient.Do(req) // sans délai : un flux dure ce qu'il dure
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
out.emit(map[string]any{"error": "conversation indisponible : " + err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
sc := bufio.NewScanner(resp.Body)
|
||||
sc.Buffer(make([]byte, 0, 64<<10), 64<<20)
|
||||
for sc.Scan() {
|
||||
data, ok := strings.CutPrefix(sc.Text(), "data: ")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var ev struct {
|
||||
Choices []struct {
|
||||
Delta map[string]any `json:"delta"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
dec := json.NewDecoder(strings.NewReader(data))
|
||||
dec.UseNumber() // les nombres repartent tels quels
|
||||
if dec.Decode(&ev) != nil || len(ev.Choices) == 0 {
|
||||
continue
|
||||
}
|
||||
if !out.emit(ev.Choices[0].Delta) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exportSource : la conversation à exporter. Tenue par la façade, celle du Go
|
||||
// est périmée : on lit la discussion enregistrée (à jour à chaque fin de tour).
|
||||
func exportSource() *Conversation {
|
||||
if !facadeChat() {
|
||||
return conv
|
||||
}
|
||||
c := &Conversation{}
|
||||
c.cond = sync.NewCond(&c.mu)
|
||||
if b := storeBytes(bkChat, convKey(convEnsureActive())); len(b) > 0 {
|
||||
_ = json.Unmarshal(b, c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
Reference in new issue
Block a user