mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
L'amont (AJEAN 0.15) donne à l'IA le contrôle d'un navigateur ; Loki embarquait déjà un Chromium pour `web_screenshot` sans jamais le piloter. Contrôle du navigateur (computer_use.go, computer_cdp.go, browser_grid.go, portés d'AJEAN) : l'IA ouvre une page en CDP, en reçoit les éléments interactifs NUMÉROTÉS et agit par numéro (browser_open/snapshot/find/click/ type/key/scroll). Aucune vision requise — un petit modèle texte s'en sort. Avec un projecteur chargé s'ajoutent browser_screenshot (image quadrillée) et browser_click_xy. Interrupteur dédié (Réglages, `loki computer`, /api/computer), sous le mode agent : cliquer et taper dans une page sont des actions réelles, même niveau de confiance que bash. Adaptation au conteneur : chromePath cherche D'ABORD le Chromium de Playwright (PLAYWRIGHT_BROWSERS_PATH, /opt/pw-browsers), le seul navigateur de l'image — sinon la fonctionnalité se déclarait absente là où le navigateur est présent. LOKI_CHROME force un chemin, LOKI_CU_HEADFUL ouvre une vraie fenêtre. Préparation des images (web_upload_orient.go, porté d'AJEAN) : l'orientation EXIF est cuite dans les pixels — le projecteur l'ignore et voyait les photos de téléphone couchées — et le grand côté ramené sous 1568 px. Appliquée aux pièces jointes, à see_image et aux captures. Dédup des appels d'outils : bash, bash_bg, bash_tail, see_image et les browser_* ne sont plus court-circuités sur un appel identique. Relancer la même commande après avoir modifié un fichier est légitime, et un outil qui porte une image dans un message à part renvoyait « [déjà fait] » SANS l'image — le modèle tournait en boucle. Mode code : le builder ne publie plus de lui-même (pas de commit/push/reset/ rebase ni de redémarrage de service sans demande explicite), reprise de la leçon d'AJEAN 0.15.4 ; l'inspection en lecture seule reste encouragée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
package loki
|
||
|
||
// browser_grid.go — surimpose une GRILLE DE COORDONNÉES sur le screenshot envoyé
|
||
// au MODÈLE (pas sur celui sauvegardé pour l'utilisateur). Un petit modèle vise
|
||
// mal au pixel près ; avec des lignes graduées tous les 100 px et des étiquettes
|
||
// chiffrées, il lit x/y précisément et browser_click_xy tombe juste du premier
|
||
// coup (bandeaux cookies en iframe, canvas, jeux…). Sans dépendance externe : la
|
||
// police de chiffres est un bitmap 3×5 codé ici.
|
||
|
||
import (
|
||
"bytes"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"image/png"
|
||
"strconv"
|
||
)
|
||
|
||
const gridStep = 100 // espacement des lignes, en pixels
|
||
|
||
// digits5x3 : bitmap 5 lignes × 3 colonnes pour chaque chiffre (# = pixel plein).
|
||
var digits5x3 = map[rune][5]string{
|
||
'0': {"###", "# #", "# #", "# #", "###"},
|
||
'1': {" # ", "## ", " # ", " # ", "###"},
|
||
'2': {"###", " #", "###", "# ", "###"},
|
||
'3': {"###", " #", "###", " #", "###"},
|
||
'4': {"# #", "# #", "###", " #", " #"},
|
||
'5': {"###", "# ", "###", " #", "###"},
|
||
'6': {"###", "# ", "###", "# #", "###"},
|
||
'7': {"###", " #", " #", " #", " #"},
|
||
'8': {"###", "# #", "###", "# #", "###"},
|
||
'9': {"###", "# #", "###", " #", "###"},
|
||
}
|
||
|
||
// overlayGrid décode un PNG, y dessine la grille + les graduations, et renvoie le
|
||
// PNG résultant. En cas d'erreur (image illisible), renvoie l'entrée inchangée.
|
||
func overlayGrid(raw []byte) []byte {
|
||
src, err := png.Decode(bytes.NewReader(raw))
|
||
if err != nil {
|
||
return raw
|
||
}
|
||
b := src.Bounds()
|
||
img := image.NewRGBA(b)
|
||
draw.Draw(img, b, src, b.Min, draw.Src)
|
||
|
||
line := color.RGBA{255, 0, 200, 130} // magenta translucide pour les lignes
|
||
labelBG := color.RGBA{0, 0, 0, 200} // fond sombre derrière les chiffres
|
||
labelFG := color.RGBA{255, 255, 0, 255} // chiffres jaunes (contraste sur toute page)
|
||
w, h := b.Dx(), b.Dy()
|
||
|
||
// Lignes verticales + étiquette x en haut.
|
||
for x := gridStep; x < w; x += gridStep {
|
||
for y := 0; y < h; y++ {
|
||
blend(img, b.Min.X+x, b.Min.Y+y, line)
|
||
}
|
||
drawLabel(img, b.Min.X+x+2, b.Min.Y+2, strconv.Itoa(x), labelFG, labelBG)
|
||
}
|
||
// Lignes horizontales + étiquette y à gauche.
|
||
for y := gridStep; y < h; y += gridStep {
|
||
for x := 0; x < w; x++ {
|
||
blend(img, b.Min.X+x, b.Min.Y+y, line)
|
||
}
|
||
drawLabel(img, b.Min.X+2, b.Min.Y+y+2, strconv.Itoa(y), labelFG, labelBG)
|
||
}
|
||
|
||
var out bytes.Buffer
|
||
if err := png.Encode(&out, img); err != nil {
|
||
return raw
|
||
}
|
||
return out.Bytes()
|
||
}
|
||
|
||
// blend applique une couleur semi-transparente (alpha) sur un pixel.
|
||
func blend(img *image.RGBA, x, y int, c color.RGBA) {
|
||
if !(image.Pt(x, y).In(img.Bounds())) {
|
||
return
|
||
}
|
||
a := float64(c.A) / 255
|
||
o := img.RGBAAt(x, y)
|
||
img.SetRGBA(x, y, color.RGBA{
|
||
R: uint8(float64(c.R)*a + float64(o.R)*(1-a)),
|
||
G: uint8(float64(c.G)*a + float64(o.G)*(1-a)),
|
||
B: uint8(float64(c.B)*a + float64(o.B)*(1-a)),
|
||
A: 255,
|
||
})
|
||
}
|
||
|
||
// drawLabel écrit un nombre (police 3×5, échelle ×2) avec un fond sombre pour
|
||
// rester lisible sur n'importe quel fond.
|
||
func drawLabel(img *image.RGBA, x, y int, s string, fg, bg color.RGBA) {
|
||
const sc = 2
|
||
glyphW, glyphH, gap := 3*sc, 5*sc, sc
|
||
boxW := len(s)*(glyphW+gap) + gap
|
||
boxH := glyphH + 2*gap
|
||
// Fond.
|
||
for dy := -gap; dy < boxH-gap; dy++ {
|
||
for dx := -gap; dx < boxW-gap; dx++ {
|
||
setOpaque(img, x+dx, y+dy, bg)
|
||
}
|
||
}
|
||
// Chiffres.
|
||
cx := x
|
||
for _, r := range s {
|
||
g, ok := digits5x3[r]
|
||
if !ok {
|
||
cx += glyphW + gap
|
||
continue
|
||
}
|
||
for row := 0; row < 5; row++ {
|
||
for col := 0; col < 3; col++ {
|
||
if g[row][col] != '#' {
|
||
continue
|
||
}
|
||
for iy := 0; iy < sc; iy++ {
|
||
for ix := 0; ix < sc; ix++ {
|
||
setOpaque(img, cx+col*sc+ix, y+row*sc+iy, fg)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
cx += glyphW + gap
|
||
}
|
||
}
|
||
|
||
func setOpaque(img *image.RGBA, x, y int, c color.RGBA) {
|
||
if image.Pt(x, y).In(img.Bounds()) {
|
||
img.SetRGBA(x, y, color.RGBA{c.R, c.G, c.B, 255})
|
||
}
|
||
}
|