mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
v0.2.6 : heartbeat SSE anti-coupure (agents) + jean link en service (start/stop/restart/code)
- fix "TypeError: Load failed" en mode agent : heartbeat SSE (: ping /15s) pour que les longs silences (exécution d'outil) ne soient pas coupés en transit - jean link devient le pilote du service (comme jean) ; worker = jean link serve - jean link code : codes d'appairage à la demande, usage unique, TTL 10 min (store fichier haché, partagé entre le CLI et le worker) - e2eauth.go ajouté au suivi ; README + aide mis à jour
This commit is contained in:
1 parent
949e261538
commit
97fbed8de2
6 files changed
+549
-34
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -582,6 +583,39 @@ type chatReq struct {
|
||||
Skills *bool `json:"skills"`
|
||||
}
|
||||
|
||||
// sseHeartbeat garde la réponse SSE active en écrivant un commentaire (`: ping`,
|
||||
// ignoré par le parseur côté navigateur, aucun contenu donc rien à chiffrer)
|
||||
// toutes les ~15 s. Sans ça, un long silence (exécution d'outil en mode agent,
|
||||
// gros prefill) laisse la réponse inactive et un proxy intermédiaire (Cloudflare,
|
||||
// ~100 s) la coupe → le fetch navigateur échoue (« Load failed »). Retourne un
|
||||
// mutex à partager avec l'émetteur (writes concurrents sur le même w) et une
|
||||
// fonction d'arrêt à différer.
|
||||
func sseHeartbeat(w http.ResponseWriter, flusher http.Flusher) (*sync.Mutex, func()) {
|
||||
mu := &sync.Mutex{}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
t := time.NewTicker(15 * time.Second)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-t.C:
|
||||
mu.Lock()
|
||||
_, err := w.Write([]byte(": ping\n\n"))
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return mu, func() { close(done) }
|
||||
}
|
||||
|
||||
// runChatStream exécute le chat et pousse chaque événement (delta) via emit, qui
|
||||
// renvoie false pour interrompre. Partagé par handleChat et handleE2EChat.
|
||||
func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any) bool) {
|
||||
@@ -626,8 +660,12 @@ func handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
flusher, _ := w.(http.Flusher)
|
||||
mu, stop := sseHeartbeat(w, flusher)
|
||||
defer stop()
|
||||
emit := func(obj map[string]any) bool {
|
||||
b, _ := json.Marshal(map[string]any{"choices": []any{map[string]any{"delta": obj}}})
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, err := w.Write([]byte("data: " + string(b) + "\n\n")); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user