feat: externalize apps/analysis to Gitea repos, add analysis table
- Migration 007: repo_url on apps table + analysis table with FTS5 - Analysis struct, parser, CRUD, validation, hash computation - Selective purge: remote-only apps/analysis preserved across fn index - CLI: fn app list/clone/pull, fn analysis list/clone/pull - search/show/list now include analysis results - Apps removed from git tracking (content lives in Gitea repos) - .gitkeep for apps/ and analysis/ dirs - Bash functions: jupyter analysis pipeline, shell utilities - Browser domain: CDP functions moved from infra to browser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CdpClick hace click en el primer elemento que coincide con el selector CSS.
|
||||
// Obtiene las coordenadas del elemento via Runtime.evaluate y luego despacha
|
||||
// eventos mousedown, mouseup y click via Input.dispatchMouseEvent.
|
||||
func CdpClick(c *CDPConn, selector string) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp click: conexion nula")
|
||||
}
|
||||
|
||||
// Obtener coordenadas del centro del elemento
|
||||
js := fmt.Sprintf(`(function() {
|
||||
var el = document.querySelector(%q);
|
||||
if (!el) return null;
|
||||
var r = el.getBoundingClientRect();
|
||||
return JSON.stringify({x: r.left + r.width/2, y: r.top + r.height/2});
|
||||
})()`, selector)
|
||||
|
||||
coordStr, err := CdpEvaluate(c, js)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp click: obtener coordenadas de %q: %w", selector, err)
|
||||
}
|
||||
if coordStr == "" || coordStr == "null" {
|
||||
return fmt.Errorf("cdp click: elemento %q no encontrado en el DOM", selector)
|
||||
}
|
||||
|
||||
// Parsear "{x:...,y:...}" — CdpEvaluate ya retorna el JSON como string
|
||||
coordStr = strings.Trim(coordStr, `"`)
|
||||
x, y, err := parseCoords(coordStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp click: parsear coordenadas %q: %w", coordStr, err)
|
||||
}
|
||||
|
||||
// Hacer scroll al elemento para que este visible
|
||||
scrollJS := fmt.Sprintf(`document.querySelector(%q).scrollIntoView({block:'center'})`, selector)
|
||||
if _, err := CdpEvaluate(c, scrollJS); err != nil {
|
||||
// No es fatal si el scroll falla
|
||||
_ = err
|
||||
}
|
||||
|
||||
// Despachar mousedown
|
||||
mouseParams := map[string]any{
|
||||
"type": "mousePressed",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1,
|
||||
}
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", mouseParams); err != nil {
|
||||
return fmt.Errorf("cdp click: mousedown: %w", err)
|
||||
}
|
||||
|
||||
// Despachar mouseup
|
||||
mouseParams["type"] = "mouseReleased"
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", mouseParams); err != nil {
|
||||
return fmt.Errorf("cdp click: mouseup: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCoords extrae x e y de un string JSON como {"x":100,"y":200}.
|
||||
func parseCoords(s string) (float64, float64, error) {
|
||||
// Buscar valores x e y manualmente para evitar dependencia de encoding/json
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimPrefix(s, "{")
|
||||
s = strings.TrimSuffix(s, "}")
|
||||
|
||||
var x, y float64
|
||||
for part := range strings.SplitSeq(s, ",") {
|
||||
kv := strings.SplitN(strings.TrimSpace(part), ":", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.Trim(strings.TrimSpace(kv[0]), `"`)
|
||||
v, err := strconv.ParseFloat(strings.TrimSpace(kv[1]), 64)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("parsear valor %q: %w", kv[1], err)
|
||||
}
|
||||
switch k {
|
||||
case "x":
|
||||
x = v
|
||||
case "y":
|
||||
y = v
|
||||
}
|
||||
}
|
||||
return x, y, nil
|
||||
}
|
||||
Reference in New Issue
Block a user