feat: modo de velocidad de sesión (browser_set_mode) + acciones más rápidas en auto
Añade un flag de velocidad por sesión para que el manejo del navegador sea muy rápido por defecto, conservando un modo sigiloso para cuando haya detección anti-bot fuerte. - Nueva tool browser_set_mode (tools_session.go): fija el modo de la sesión por puerto en el pool. 'auto' (default del MCP) = rápido; 'human' = sigiloso anti-detección; también admite 'fast'/'instant'. Cada tool de acción puede overridearlo con su arg mode. - pool.go: estado de modo por puerto (modes map + setMode/getMode), limpiado en drop y closeAll. - tools_dom.go: effectiveMode resuelve el modo (arg de la llamada > modo de sesión > 'auto'). settleForMode reemplaza el sleep ciego fijo de 400ms tras cada acción mutante: 60ms en auto/fast, aleatorio 250-650ms en human (ritmo no-máquina), 0 en instant. dom_type_ref gana arg mode y rutea a CdpTypeRefFast (insertText, un round-trip) en auto o CdpTypeRef (carácter a carácter) en human. Descripciones del arg mode actualizadas (el default ya no es human). - tools_lifecycle.go: browser_launch_profile reemplaza el sleep(1s) ciego por un poll del puerto CDP (waitCDPPort). - .gitignore: ignora registry.db/operations.db (no deben vivir en la app; regla db_locations). Doctrina invertida respecto a la anterior 'humanizado siempre': ahora rápido por defecto, sigiloso bajo demanda.
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
/browser_mcp
|
||||
*.log
|
||||
# registry.db sólo existe en la raíz del repo (regla db_locations). Si un test o el
|
||||
# binario lo crea aquí por un path relativo, es basura: ignorarlo evita trackearlo.
|
||||
registry.db
|
||||
operations.db*
|
||||
|
||||
@@ -23,6 +23,7 @@ type connPool struct {
|
||||
pids map[int]int // puerto -> PID del Chrome lanzado por el MCP (solo los SUYOS)
|
||||
cancels map[int]func() // cancels de handlers persistentes (handle_dialog)
|
||||
dialogLogs map[int]*browser.DialogLog // log de diálogos auto-respondidos por puerto
|
||||
modes map[int]string // puerto -> modo de velocidad de sesión ("auto"|"human"|...)
|
||||
}
|
||||
|
||||
func newConnPool() *connPool {
|
||||
@@ -31,9 +32,25 @@ func newConnPool() *connPool {
|
||||
pids: map[int]int{},
|
||||
cancels: map[int]func(){},
|
||||
dialogLogs: map[int]*browser.DialogLog{},
|
||||
modes: map[int]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// setMode fija el modo de velocidad de sesión para un puerto (lo lee
|
||||
// effectiveMode cuando una tool de acción no trae su propio arg `mode`).
|
||||
func (p *connPool) setMode(port int, mode string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.modes[port] = mode
|
||||
}
|
||||
|
||||
// getMode devuelve el modo de sesión del puerto ("" si no se fijó ninguno).
|
||||
func (p *connPool) getMode(port int) string {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.modes[port]
|
||||
}
|
||||
|
||||
func (p *connPool) get(port int) (*browser.CDPConn, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
@@ -121,6 +138,7 @@ func (p *connPool) drop(port int) {
|
||||
_ = browser.CdpClose(c, pid)
|
||||
delete(p.conns, port)
|
||||
delete(p.pids, port)
|
||||
delete(p.modes, port)
|
||||
}
|
||||
|
||||
// connectTarget descarta la conexión actual del puerto y reconecta a un target
|
||||
@@ -188,6 +206,7 @@ func (p *connPool) closeAll() {
|
||||
p.pids = map[int]int{}
|
||||
p.cancels = map[int]func(){}
|
||||
p.dialogLogs = map[int]*browser.DialogLog{}
|
||||
p.modes = map[int]string{}
|
||||
}
|
||||
|
||||
// isConnErr reconoce errores de conexión CDP muerta para reintentar UNA vez.
|
||||
|
||||
+65
-15
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
@@ -29,9 +30,40 @@ func registerDomTools(s *server.MCPServer, d *deps) {
|
||||
}
|
||||
}
|
||||
|
||||
// settleDelay es la espera breve tras una acción mutante antes de re-percibir,
|
||||
// dando tiempo a que el DOM se asiente (navegación, focus, repaint).
|
||||
const settleDelay = 400 * time.Millisecond
|
||||
// defaultMode es el modo de velocidad cuando ni la llamada ni la sesión fijan uno.
|
||||
// "auto" = rápido (movimiento de ratón mínimo, escritura en un solo evento, settle
|
||||
// breve) — el modo por defecto del MCP. "human" (Bézier + esperas aleatorias) se
|
||||
// activa explícitamente vía browser_set_mode o el arg `mode` cuando un sitio
|
||||
// aplique detección anti-bot fuerte.
|
||||
const defaultMode = "auto"
|
||||
|
||||
// effectiveMode resuelve el modo de velocidad de una acción: el arg de la llamada
|
||||
// gana; si está vacío, el modo de sesión fijado por browser_set_mode; si tampoco
|
||||
// hay, defaultMode.
|
||||
func (d *deps) effectiveMode(port int, callMode string) string {
|
||||
if callMode != "" {
|
||||
return callMode
|
||||
}
|
||||
if m := d.pool.getMode(port); m != "" {
|
||||
return m
|
||||
}
|
||||
return defaultMode
|
||||
}
|
||||
|
||||
// settleForMode es la espera tras una acción mutante antes de re-percibir, dando
|
||||
// tiempo a que el DOM se asiente (navegación, focus, repaint). En "human" es
|
||||
// ALEATORIA (250-650ms) para no exhibir un ritmo de máquina; en auto/fast es breve
|
||||
// y fija (60ms); en "instant" es nula.
|
||||
func settleForMode(mode string) time.Duration {
|
||||
switch mode {
|
||||
case "human", "":
|
||||
return time.Duration(250+rand.Intn(401)) * time.Millisecond // 250..650
|
||||
case "instant":
|
||||
return 0
|
||||
default: // auto, fast
|
||||
return 60 * time.Millisecond
|
||||
}
|
||||
}
|
||||
|
||||
// ---- dom_click_ref (MUTA) — bucle percibir→actuar ----
|
||||
|
||||
@@ -46,19 +78,22 @@ func domClickRefTool() mcp.Tool {
|
||||
mcp.WithDescription("Click sobre el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable). Devuelve el outline actualizado tras la acción (auto-observe)."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
|
||||
mcp.WithNumber("ref", mcp.Required(), mcp.Description("#ref del elemento (backendDOMNodeId) leído del outline de page_perceive.")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'human' (default, Bézier+jitter anti-bot), 'fast' (movimiento reducido, scraping masivo), 'instant' (element.click() JS, sin eventos de ratón; también fallback si el elemento no tiene geometría).")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión: movimiento de ratón reducido, rápido), 'human' (Bézier+jitter+pausas aleatorias anti-bot, para detección fuerte), 'instant' (element.click() JS, sin eventos de ratón; también fallback si el elemento no tiene geometría). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleDomClickRef(_ context.Context, _ mcp.CallToolRequest, a domClickRefArgs) (*mcp.CallToolResult, error) {
|
||||
port := portOr(a.Port)
|
||||
mode := d.effectiveMode(port, a.Mode)
|
||||
err := d.withConn(port, func(c *browser.CDPConn) error {
|
||||
return browser.CdpClickRef(c, a.Ref, browser.MouseProfileForMode(a.Mode))
|
||||
return browser.CdpClickRef(c, a.Ref, browser.MouseProfileForMode(mode))
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
time.Sleep(settleDelay)
|
||||
if dl := settleForMode(mode); dl > 0 {
|
||||
time.Sleep(dl)
|
||||
}
|
||||
outline, _ := d.perceiveOutline(port, 8000)
|
||||
return mcp.NewToolResultText("clicked ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
|
||||
}
|
||||
@@ -69,6 +104,7 @@ type domTypeRefArgs struct {
|
||||
Port int `json:"port"`
|
||||
Ref int `json:"ref"`
|
||||
Text string `json:"text"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
func domTypeRefTool() mcp.Tool {
|
||||
@@ -77,6 +113,7 @@ func domTypeRefTool() mcp.Tool {
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
|
||||
mcp.WithNumber("ref", mcp.Required(), mcp.Description("#ref del elemento (backendDOMNodeId) leído del outline de page_perceive.")),
|
||||
mcp.WithString("text", mcp.Required(), mcp.Description("Texto a escribir en el elemento.")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión, escribe en un solo evento Input.insertText — rápido) o 'human' (caracter a caracter con pausas aleatorias, anti-detección). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -85,14 +122,21 @@ func (d *deps) handleDomTypeRef(_ context.Context, _ mcp.CallToolRequest, a domT
|
||||
return mcp.NewToolResultError("text is required"), nil
|
||||
}
|
||||
port := portOr(a.Port)
|
||||
// TODO: preset de humanización por sesión (human/fast/instant)
|
||||
mode := d.effectiveMode(port, a.Mode)
|
||||
err := d.withConn(port, func(c *browser.CDPConn) error {
|
||||
return browser.CdpTypeRef(c, a.Ref, a.Text)
|
||||
// human => teclea caracter a caracter (eventos de tecla reales + ritmo
|
||||
// irregular). auto/fast/instant => inserta todo en un solo round-trip.
|
||||
if mode == "human" {
|
||||
return browser.CdpTypeRef(c, a.Ref, a.Text)
|
||||
}
|
||||
return browser.CdpTypeRefFast(c, a.Ref, a.Text)
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
time.Sleep(settleDelay)
|
||||
if dl := settleForMode(mode); dl > 0 {
|
||||
time.Sleep(dl)
|
||||
}
|
||||
outline, _ := d.perceiveOutline(port, 8000)
|
||||
return mcp.NewToolResultText("typed into ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
|
||||
}
|
||||
@@ -110,19 +154,22 @@ func domHoverRefTool() mcp.Tool {
|
||||
mcp.WithDescription("Hover sobre el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable). Devuelve el outline actualizado tras la acción (auto-observe)."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
|
||||
mcp.WithNumber("ref", mcp.Required(), mcp.Description("#ref del elemento (backendDOMNodeId) leído del outline de page_perceive.")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'human' (default, Bézier+jitter), 'fast' (movimiento reducido), 'instant' (sin movimiento de ratón).")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión: movimiento reducido, rápido), 'human' (Bézier+jitter+pausas aleatorias anti-bot), 'instant' (sin movimiento de ratón). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleDomHoverRef(_ context.Context, _ mcp.CallToolRequest, a domHoverRefArgs) (*mcp.CallToolResult, error) {
|
||||
port := portOr(a.Port)
|
||||
mode := d.effectiveMode(port, a.Mode)
|
||||
err := d.withConn(port, func(c *browser.CDPConn) error {
|
||||
return browser.CdpHoverRef(c, a.Ref, browser.MouseProfileForMode(a.Mode))
|
||||
return browser.CdpHoverRef(c, a.Ref, browser.MouseProfileForMode(mode))
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
time.Sleep(settleDelay)
|
||||
if dl := settleForMode(mode); dl > 0 {
|
||||
time.Sleep(dl)
|
||||
}
|
||||
outline, _ := d.perceiveOutline(port, 8000)
|
||||
return mcp.NewToolResultText("hovered ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
|
||||
}
|
||||
@@ -142,19 +189,22 @@ func domClickXYTool() mcp.Tool {
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
|
||||
mcp.WithNumber("x", mcp.Required(), mcp.Description("Coordenada X absoluta en CSS pixels del viewport.")),
|
||||
mcp.WithNumber("y", mcp.Required(), mcp.Description("Coordenada Y absoluta en CSS pixels del viewport.")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'human' (default, Bézier+jitter anti-bot), 'fast' (movimiento reducido, scraping masivo), 'instant' (sin movimiento de ratón).")),
|
||||
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión: movimiento reducido, rápido), 'human' (Bézier+jitter+pausas aleatorias anti-bot), 'instant' (sin movimiento de ratón). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleDomClickXY(_ context.Context, _ mcp.CallToolRequest, a domClickXYArgs) (*mcp.CallToolResult, error) {
|
||||
port := portOr(a.Port)
|
||||
mode := d.effectiveMode(port, a.Mode)
|
||||
err := d.withConn(port, func(c *browser.CDPConn) error {
|
||||
return browser.CdpClickXYHuman(c, a.X, a.Y, browser.MouseProfileForMode(a.Mode))
|
||||
return browser.CdpClickXYHuman(c, a.X, a.Y, browser.MouseProfileForMode(mode))
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
time.Sleep(settleDelay)
|
||||
if dl := settleForMode(mode); dl > 0 {
|
||||
time.Sleep(dl)
|
||||
}
|
||||
outline, _ := d.perceiveOutline(port, 8000)
|
||||
return mcp.NewToolResultText(fmt.Sprintf("clicked at (%g, %g)\n\n%s", a.X, a.Y, outline)), nil
|
||||
}
|
||||
|
||||
+20
-7
@@ -324,16 +324,16 @@ func (d *deps) handleBrowserLaunchProfile(_ context.Context, _ mcp.CallToolReque
|
||||
pid := cmd.Process.Pid
|
||||
_ = cmd.Process.Release()
|
||||
|
||||
// Give Chromium a moment to come up. If it forwarded to an existing master the
|
||||
// child exits fast; the launched pid is still informative.
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// When cdp=true, opportunistically confirm the port responds (best-effort: a
|
||||
// forwarded launch may not bind the port if the master had no CDP).
|
||||
// Give Chromium a moment to come up. With CDP we poll the port instead of a
|
||||
// blind 1s sleep: we return as soon as it responds (best-effort: a forwarded
|
||||
// launch may not bind the port if the master had no CDP). Without CDP there's
|
||||
// no port to poll, so we give the window a short margin to appear / forward.
|
||||
if a.CDP && note == "" {
|
||||
if !cdpPortResponds(cdpPort) {
|
||||
if !waitCDPPort(cdpPort, 5*time.Second) {
|
||||
note = "cdp port not confirmed listening yet"
|
||||
}
|
||||
} else {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
out := map[string]any{
|
||||
@@ -452,6 +452,19 @@ func processAlive(pid int) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// waitCDPPort polls the CDP port until it accepts a TCP connection or the timeout
|
||||
// elapses. Replaces a blind sleep: returns as soon as Chromium binds the port.
|
||||
func waitCDPPort(port int, timeout time.Duration) bool {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if cdpPortResponds(port) {
|
||||
return true
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return cdpPortResponds(port)
|
||||
}
|
||||
|
||||
// cdpPortResponds reports whether something is listening on the CDP port on
|
||||
// 127.0.0.1. Single TCP dial with a short timeout; best-effort confirmation only.
|
||||
func cdpPortResponds(port int) bool {
|
||||
|
||||
+30
-1
@@ -12,13 +12,15 @@ import (
|
||||
"fn-registry/functions/browser"
|
||||
)
|
||||
|
||||
// registerSessionTools wires browser_launch (MUTA), browser_connect, browser_disconnect.
|
||||
// registerSessionTools wires browser_launch (MUTA), browser_connect, browser_disconnect,
|
||||
// browser_set_mode.
|
||||
func registerSessionTools(s *server.MCPServer, d *deps) {
|
||||
if !d.readOnly {
|
||||
s.AddTool(launchTool(), mcp.NewTypedToolHandler(d.handleLaunch))
|
||||
}
|
||||
s.AddTool(connectTool(), mcp.NewTypedToolHandler(d.handleConnect))
|
||||
s.AddTool(disconnectTool(), mcp.NewTypedToolHandler(d.handleDisconnect))
|
||||
s.AddTool(setModeTool(), mcp.NewTypedToolHandler(d.handleSetMode))
|
||||
}
|
||||
|
||||
// maxLaunchedChromes es el tope duro de instancias Chrome que el MCP puede tener
|
||||
@@ -142,3 +144,30 @@ func (d *deps) handleDisconnect(_ context.Context, _ mcp.CallToolRequest, a disc
|
||||
}
|
||||
return mcp.NewToolResultText(msg), nil
|
||||
}
|
||||
|
||||
// ---- browser_set_mode ----
|
||||
|
||||
type setModeArgs struct {
|
||||
Port int `json:"port"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
func setModeTool() mcp.Tool {
|
||||
return mcp.NewTool("browser_set_mode",
|
||||
mcp.WithDescription("Fija el modo de velocidad de SESIÓN de las acciones del navegador en este puerto. 'auto' (default del MCP) = rápido: movimiento de ratón mínimo, escritura en un solo evento (Input.insertText) y esperas breves — para scraping y automatización propia. 'human' = sigiloso anti-detección: trayectoria de ratón Bézier con jitter, escritura carácter a carácter y esperas ALEATORIAS entre acción y percepción — actívalo cuando un sitio aplique detección anti-bot fuerte. El arg 'mode' de cada tool de acción (dom_click_ref, dom_type_ref, dom_hover_ref, dom_click_xy) sigue ganando puntualmente sobre este ajuste de sesión."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
|
||||
mcp.WithString("mode", mcp.Required(), mcp.Description("'auto' (rápido, default) o 'human' (sigiloso, anti-detección). También admite 'fast' (alias de auto) e 'instant' (sin movimiento de ratón) para casos puntuales.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleSetMode(_ context.Context, _ mcp.CallToolRequest, a setModeArgs) (*mcp.CallToolResult, error) {
|
||||
switch a.Mode {
|
||||
case "auto", "human", "fast", "instant":
|
||||
// válido
|
||||
default:
|
||||
return mcp.NewToolResultError("mode debe ser 'auto' o 'human' (también 'fast'/'instant')"), nil
|
||||
}
|
||||
port := portOr(a.Port)
|
||||
d.pool.setMode(port, a.Mode)
|
||||
return mcp.NewToolResultText(fmt.Sprintf("session mode set to %q for port=%d (cada tool de acción puede overridearlo con su arg mode)", a.Mode, port)), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user