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:
Loki committed 2026-08-15 11:54:52 +00:00
1 parent 54c5eb1334
commit 6fe94b9a8d
10 files changed
+315 -5

No files matched your search

+4 -1
View File
@@ -162,7 +162,10 @@ async function loadCfg(){
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
// toujours le chemin complet, quel que soit le libellé.
let v;
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='llama.cpp précompilé';
// En conteneur, le moteur vient de l'image : le dire, plutôt que de le
// ranger dans « personnalisé » qui laisse croire à un bricolage.
if(lc && lc.provided && sameBinPath(c.BIN, lc.provided_bin)) v='llama.cpp de l\'image';
else if(lc && lc.prebuilt && lc.prebuilt.in_use) v='llama.cpp précompilé';
else if(lc && lc.in_use) v='llama.cpp compilé';
else v='llama.cpp personnalisé : '+c.BIN;
rows.push(row('MOTEUR', v, c.BIN));
+28 -1
View File
@@ -140,7 +140,34 @@ function setStats(el, text){
}
function bodyOf(el){ return el.querySelector('.body'); }
// Render markdown into a message body in place; safe because md() escapes HTML.
function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(encodeMdLinkSpaces(text)); markNotices(b); addCopyButtons(b); markFileLinks(b); scrollMaybe(); }
function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(encodeMdLinkSpaces(text)); markNotices(b); addCopyButtons(b); markFileLinks(b); hydrateImages(b); scrollMaybe(); }
// Images servies par Loki (/api/chat/image) : une balise <img> ne peut pas
// porter d'en-tête Authorization, or /api/* exige la clé de pilotage dès qu'elle
// est définie. On récupère donc l'image par fetch authentifié et on la pose en
// blob:. Sans ça, toute instance protégée par une clé n'affichait que des images
// cassées. Les URLs externes (http…) ne sont pas touchées.
function hydrateImages(root){
root.querySelectorAll('img[src*="/api/chat/image"]').forEach(async img => {
if(img.dataset.hydrated) return;
img.dataset.hydrated = '1';
const src = img.getAttribute('src');
try{
const r = await jfetch(src.startsWith('/') ? src : '/' + src);
if(!r.ok) throw new Error(r.status);
const url = URL.createObjectURL(await r.blob());
img.src = url;
img.classList.add('chatimg');
// La révocation attend le chargement : révoquer tout de suite laisserait
// une image vide sur les navigateurs qui décodent en différé.
img.addEventListener('load', () => URL.revokeObjectURL(url), {once:true});
}catch(e){
const note = document.createElement('span');
note.className = 'muted';
note.textContent = '[image indisponible : ' + src.replace(/^.*path=/, '') + ']';
img.replaceWith(note);
}
});
}
// Render a tool call as its own conversation message: the command the model
// wrote, then the response it got back. textContent keeps it injection-safe.
function renderToolMsg(el, tu){