diff --git a/captcha_sniff.go b/captcha_sniff.go new file mode 100644 index 0000000..4e45ce4 --- /dev/null +++ b/captcha_sniff.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "strings" + + "fn-registry/functions/browser" +) + +// captchaMarker corre la detección de captcha/challenge anti-bot sobre el tab +// del puerto dado (vía browser.DetectCaptcha) y, si detecta un widget conocido +// (reCAPTCHA, hCaptcha, Cloudflare Turnstile o un JS-challenge de Cloudflare), +// devuelve un marcador de texto para appendear al output de la tool que la +// invoca. Si no hay captcha devuelve "". +// +// Es best-effort por diseño: cualquier error de conexión o de evaluación se +// ignora y devuelve "" — la detección NUNCA debe romper ni bloquear la tool de +// navegación/percepción de la que cuelga. Garantiza que el captcha se detecta +// SIEMPRE en los puntos de carga (navegar, esperar load/idle, percibir) sin que +// el agente tenga que acordarse de comprobarlo. +// +// El MCP solo detecta y marca. La reacción (avisar al humano por PushNotification +// al móvil, traer la ventana del Chrome al frente y PARAR la automatización) la +// dispara Claude al leer el marcador. El binario no resuelve el captcha ni +// notifica por sí mismo: el captcha lo resuelve el humano a mano en este mismo +// navegador, porque el token va atado a la sesión, cookies y fingerprint de ESTE +// Chrome y no es transferible a otra ventana. +func (d *deps) captchaMarker(port int) string { + var marker string + _ = d.withConn(port, func(c *browser.CDPConn) error { + detected, types, url, err := browser.DetectCaptcha(c) + if err != nil || !detected { + return nil + } + marker = fmt.Sprintf( + "\n\n⚠️ CAPTCHA-DETECTED type=%s url=%s\n"+ + "HANDOFF HUMANO: PARA aquí. Avisa al humano (PushNotification al móvil) y trae "+ + "la ventana del Chrome al frente. NO intentes resolver el captcha ni sigas "+ + "clicando — el humano lo resuelve a mano en este mismo navegador y luego dice "+ + "\"sigue\".", + strings.Join(types, ","), url) + return nil + }) + return marker +} diff --git a/tools_nav.go b/tools_nav.go index 84bdc32..e7e955b 100644 --- a/tools_nav.go +++ b/tools_nav.go @@ -54,7 +54,7 @@ func (d *deps) handleTabNavigate(_ context.Context, _ mcp.CallToolRequest, a tab if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText("navigated to " + a.URL), nil + return mcp.NewToolResultText("navigated to " + a.URL + d.captchaMarker(portOr(a.Port))), nil } // ---- tab_list ---- @@ -88,14 +88,18 @@ type tabNewArgs struct { func tabNewTool() mcp.Tool { return mcp.NewTool("tab_new", - mcp.WithDescription("Open a new tab via PUT /json/new. Returns the new tab's JSON."), + mcp.WithDescription("Open a new tab in the BACKGROUND via Target.createTarget (background:true). No roba el foco del WM: la ventana del navegador no se eleva, el usuario sigue escribiendo donde estaba. Returns the new tab's JSON."), 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("url", mcp.Description("Optional start URL. Empty = about:blank.")), ) } func (d *deps) handleTabNew(_ context.Context, _ mcp.CallToolRequest, a tabNewArgs) (*mcp.CallToolResult, error) { - tab, err := browser.CdpNewTab("localhost", portOr(a.Port), a.URL) + // CdpNewTabBackground usa Target.createTarget{background:true} en vez de + // PUT /json/new. El endpoint HTTP /json/new SIEMPRE trae la pestaña al + // frente y eleva la ventana del SO, robando el foco del usuario que está + // escribiendo en otra ventana. La variante background no eleva la ventana. + tab, err := browser.CdpNewTabBackground("localhost", portOr(a.Port), a.URL) if err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -247,7 +251,7 @@ func (d *deps) handlePageWaitLoad(_ context.Context, _ mcp.CallToolRequest, a pa if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText("page loaded"), nil + return mcp.NewToolResultText("page loaded" + d.captchaMarker(portOr(a.Port))), nil } // ---- page_wait_idle ---- @@ -279,5 +283,5 @@ func (d *deps) handlePageWaitIdle(_ context.Context, _ mcp.CallToolRequest, a pa if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText("network idle"), nil + return mcp.NewToolResultText("network idle" + d.captchaMarker(portOr(a.Port))), nil } diff --git a/tools_read.go b/tools_read.go index ca0e74a..5500916 100644 --- a/tools_read.go +++ b/tools_read.go @@ -173,7 +173,7 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa if err != nil { return mcp.NewToolResultError(err.Error()), nil } - return mcp.NewToolResultText(outline), nil + return mcp.NewToolResultText(outline + d.captchaMarker(port)), nil } // perceiveOutline genera el outline AX accionable de la página entera sobre la