mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Captures de pages web (Playwright) affichées dans le fil
Outil web_screenshot : Chromium piloté par Playwright photographie une page RENDUE (JavaScript exécuté) dans le dossier de travail, et rend au modèle la ligne markdown exacte à recopier — lui laisser composer l'URL reviendrait à lui faire inventer un chemin, donc une image cassée. INDÉPENDANT DE LA VISION, souvent confondu : visionEnabled() (clé MMPROJ) ne décide que d'une chose, l'envoi d'images AU MODÈLE. Capturer et afficher ne passent pas par le modèle — sans projecteur, l'agent photographie sans regarder, ce qui suffit à illustrer une conversation. - route /api/chat/image : sert UNIQUEMENT des images du dossier de travail, en ligne. handleChatFile force le téléchargement de tout pour qu'un .html du modèle ne s'exécute pas dans l'origine de l'UI ; ici la même règle est tenue autrement — type déduit du CONTENU (pas de l'extension), nosniff, et CSP default-src 'none'. Un faux .png contenant du HTML est refusé en 415. - UI : une balise <img> ne peut pas porter d'en-tête Authorization, or /api/* exige la clé dès qu'elle est définie. Les images sont donc récupérées par fetch authentifié puis posées en blob:. - l'outil n'est déclaré au modèle QUE si Playwright est réellement présent : annoncer un outil absent envoie le modèle en boucle de réessai. - Node 22 (NodeSource) au lieu du Node 18 d'Ubuntu, exigé par Playwright et par la plupart des serveurs MCP. PLAYWRIGHT=0 bâtit une image sans navigateur. - CONFIG ACTIVE affichait « llama.cpp personnalisé » pour le moteur de l'image : il annonce maintenant « llama.cpp de l'image ». Description de l'outil tenue au plus court : TestSystemPromptStaysLean a attrapé le dépassement du budget de préambule (7888 car pour 7500). Vérifié dans l'image : capture réelle d'une page, servie en image/png avec CSP ; faux PNG rejeté (415) ; évasion du workspace bloquée (403). Playwright ajoute 816 Mo à l'image.
This commit is contained in:
10 files changed
+315
-5
No files matched your search
@@ -336,6 +336,55 @@ func handleChatFile(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, abs)
|
||||
}
|
||||
|
||||
// handleChatImage sert une image du dossier de travail POUR AFFICHAGE dans le
|
||||
// fil (captures d'écran, images produites par l'agent). Route distincte de
|
||||
// handleChatFile, qui force le téléchargement de TOUT : la règle « rien ne
|
||||
// s'exécute dans l'origine de l'UI » est ici tenue autrement, en ne servant
|
||||
// jamais que des images —
|
||||
// - le type est déduit du CONTENU (les 512 premiers octets), pas de
|
||||
// l'extension : un .png qui contient du HTML est refusé ;
|
||||
// - nosniff empêche le navigateur de revenir sur ce type ;
|
||||
// - une CSP « default-src 'none' » neutralise le document au cas où.
|
||||
func handleChatImage(w http.ResponseWriter, r *http.Request) {
|
||||
rel := r.URL.Query().Get("path")
|
||||
if strings.TrimSpace(rel) == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "chemin manquant"})
|
||||
return
|
||||
}
|
||||
abs := filepath.Join(agentWorkspace(), filepath.FromSlash(rel))
|
||||
if _, ok := workspaceRel(abs); !ok {
|
||||
sendJSON(w, 403, map[string]any{"ok": false, "error": "hors du dossier de travail"})
|
||||
return
|
||||
}
|
||||
st, err := os.Stat(abs)
|
||||
if err != nil || st.IsDir() {
|
||||
sendJSON(w, 404, map[string]any{"ok": false, "error": "fichier introuvable"})
|
||||
return
|
||||
}
|
||||
f, err := os.Open(abs)
|
||||
if err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
head := make([]byte, 512)
|
||||
n, _ := io.ReadFull(f, head)
|
||||
mime := http.DetectContentType(head[:n])
|
||||
if !strings.HasPrefix(mime, "image/") {
|
||||
sendJSON(w, 415, map[string]any{"ok": false, "error": "ce fichier n'est pas une image"})
|
||||
return
|
||||
}
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", mime)
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; sandbox")
|
||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+url.PathEscape(filepath.Base(abs)))
|
||||
http.ServeContent(w, r, filepath.Base(abs), st.ModTime(), f)
|
||||
}
|
||||
|
||||
type uploadReq struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"` // base64 d'UN morceau (accepte un data: URL complet)
|
||||
|
||||
Reference in new issue
Block a user