diff --git a/LLM_BROWSER_GUIDE.md b/LLM_BROWSER_GUIDE.md new file mode 100644 index 0000000..38746db --- /dev/null +++ b/LLM_BROWSER_GUIDE.md @@ -0,0 +1,457 @@ +# Guía de control de navegador para LLMs (CDP anti-colapso) + +Guía operativa para que un agente LLM (Claude u otro) controle los navegadores del equipo via +Chrome DevTools Protocol (CDP) **sin saturar su propio contexto**. El problema central que esta +guía resuelve: una página web cruda (HTML completo, `outerHTML`, screenshots como datos) puede +ocupar decenas de miles de tokens y colapsar la ventana de contexto del modelo. La solución es +operar siempre sobre representaciones compactas — sobre todo el **accessibility tree (AX tree) +recortado** — y devolver únicamente lo extraído, nunca la página entera. + +Complementa, no sustituye: +- `project.md` — qué es el proyecto y para qué sirve. +- `CONVENTIONS.md` — las 9 reglas operativas (ventana fija, perfil dedicado, esperas inteligentes, + ratón humano, perfiles solo-uBlock, CDP global). +- `CHROMIUM_SYSTEM.md` — mapa físico de la configuración de Chromium en este equipo (puertos, + `/etc/chromium.d/`, perfiles, proxy mitm, gotchas de sistema). + +Todas las rutas de esta guía son relativas a la raíz del repo `fn_registry`. + +--- + +## TL;DR — la regla de oro + +1. **Nunca vuelques la página entera al contexto.** Prohibido `script-navegador html` (devuelve el + `outerHTML` completo) salvo que vayas a procesarlo fuera del contexto (a archivo, a un pipe). + Prohibido pegar screenshots como "datos". +2. **Para *entender* una página, usa un AX snapshot recortado** (`cdp_get_ax_tree` + `trim_ax_tree`, + render a outline de texto). Es la vista semántica de la página (roles + nombres accesibles), + ya sin ruido de divs/spans vacíos. Típicamente 10-50x más pequeña que el HTML. +3. **Para *extraer* datos, ejecuta JavaScript que devuelva solo el resultado** (`script-navegador + eval`), no el DOM. Devuelve un JSON pequeño con los campos que necesitas. +4. **Para páginas enormes**, trocea el AX tree con `chunk_ax_tree` y procesa chunk a chunk. +5. **Espera condiciones reales** (carga, red en reposo, selector presente), nunca `sleep` ciegos. + +--- + +## 0. Prerrequisitos y conexión + +### Atajos de ruta usados en esta guía + +```bash +# Desde la raíz del repo fn_registry: +SN=projects/web_scraping/apps/script_navegador/script-navegador # binario Go ya compilado +PY=python/.venv/bin/python3 # venv del registry (tiene websocket-client) +``` + +Si el binario `script-navegador` no existiera, recompílalo: +`cd projects/web_scraping/apps/script_navegador && CGO_ENABLED=1 go build -o script-navegador .` + +### Mapa de puertos CDP (ver `CHROMIUM_SYSTEM.md` para el detalle) + +| Puerto | Quién | Cuándo usarlo | +|---|---|---| +| `9222` | Chromium **diario** del usuario (CDP global loopback, siempre activo) | Tareas sobre sitios donde el usuario ya tiene sesión/cookies/login. Opera sobre su navegación real. | +| `9333` (o el que pidas) | Navegador de **automatización** dedicado, perfil aislado | Scraping limpio que no debe tocar la sesión personal. Empieza solo con uBlock. | + +Dos procesos Chromium no pueden compartir el mismo `--remote-debugging-port`. + +### Comprobar que hay un Chrome al que conectarse + +```bash +curl -s --max-time 2 http://127.0.0.1:9222/json/version | head -c 300 # vacío => no hay CDP en 9222 +``` + +Si no responde, lanza uno (perfil dedicado de automatización recomendado para no tocar lo del usuario): + +```bash +# Visible (recomendado al desarrollar el flujo), perfil dedicado del proyecto, ventana fija 1366x768: +$SN launch --port 9333 --profile-directory Automation + +# Conectarse a un Chrome ya vivo no requiere launch: los comandos rápidos usan --port. +``` + +> Gotcha de sistema: lanzar chromium directamente desde una tool de shell del agente puede dar +> exit-144 (el harness mata el cgroup). `script-navegador launch` ya esquiva esto. Si lanzas +> chromium a mano, usa `systemd-run --user --unit= --collect chromium ...`. Ver +> `CHROMIUM_SYSTEM.md` y la memoria `harness-exit-144-chromium`. + +### Elegir la pestaña (tab) sobre la que operar + +Los comandos rápidos del binario (`open`, `click`, `eval`...) actúan sobre la **primera tab de tipo +`page`**. Las funciones AX en cambio reciben un `tab_id` explícito. Para listarlos: + +```bash +$SN tabs # legible: título | url de cada page +curl -s http://127.0.0.1:9222/json | $PY -c 'import sys,json; [print(t["id"], t["url"]) for t in json.load(sys.stdin) if t["type"]=="page"]' +``` + +--- + +## 1. Presupuesto de contexto (el corazón anti-colapso) + +| Acción | Coste típico en tokens | Veredicto | +|---|---|---| +| `script-navegador html` (outerHTML completo) | 5.000 – 100.000+ | ❌ nunca al contexto | +| Screenshot pegado como imagen/base64 | enorme | ❌ solo a archivo (`shot /tmp/x.png`), descríbelo, no lo pegues | +| AX tree completo (`cdp_get_ax_tree` sin recortar) | 5.000 – 50.000 | ⚠️ recórtalo siempre antes de mirarlo | +| **AX tree recortado** (`trim_ax_tree`, render outline) | 200 – 3.000 | ✅ vista por defecto para entender | +| `eval` que devuelve un JSON de campos extraídos | 50 – 1.000 | ✅ vista por defecto para extraer | + +**Reglas duras:** +- Si necesitas el HTML para procesarlo (regex, parser), mándalo a archivo y trabájalo con + herramientas, no lo leas entero: `$SN html > /tmp/page.html` y luego `grep`/funciones del registry. +- Antes de mirar un AX tree, pásalo por `trim_ax_tree`. Si sigue siendo grande, `chunk_ax_tree` y + procesa por partes. +- Para extraer datos estructurados, prefiere un `eval` con un snippet JS que ya devuelva el array de + objetos final. El DOM se queda en el navegador; al contexto solo llega el resultado. + +--- + +## 2. Capacidades — tabla maestra + +| Eje | Qué hay hoy | Cómo se invoca | Gap (hoy a mano via CDP crudo) | +|---|---|---|---| +| **Ejecutar JS** | `cdp_evaluate` | `$SN eval ''` | — | +| **AX tree (semántico)** | `cdp_get_ax_tree` + `trim_ax_tree` + `chunk_ax_tree` + `llm_propose_scraping_schema` | heredoc Python (§3.6) | render a outline texto (no existe función; snippet en §3.6) | +| **DOM leer/interactuar** | `cdp_get_html` `cdp_find_by_text` `cdp_click` `cdp_click_text` `cdp_type_text` `cdp_wait_element` | `$SN open/click/type/wait/html` | set-attribute / remove-node dedicados (se hacen via `eval`) | +| **Pestañas** | crear `cdp_new_tab` `cdp_open_url_and_wait`; listar `cdp_list_tabs` | `$SN tabs`; heredoc | **cerrar** y **activar/focus** tab (CDP crudo, §3.2) | +| **Network** | capturar `cdp_har_record`; cookies `cdp_set_cookie`; HLS `extract_hls_from_cdp_tab` | heredoc / `fn run` | **interceptar / bloquear / modificar** (Fetch domain, §3.3) | +| **Ventanas** | — | — | **todo** (get/set bounds, min/max/fullscreen) via CDP crudo (§3.1) | + +Los gaps están listados como candidatos a función / tool MCP en §6. + +--- + +## 3. Recetas por capacidad + +### 3.1 Ventanas (CRUD) — hoy via CDP crudo + +No hay función del registry todavía. El control de ventana es **browser-level** (no de página), así +que se habla por el WebSocket de `/json/version` (`webSocketDebuggerUrl`), no por el de una tab. +Secuencia: `Browser.getWindowForTarget` → `windowId` → `Browser.setWindowBounds`. + +```bash +# Mover/redimensionar la ventana de un target (tab). Requiere el targetId de la tab. +PORT=9222 PY=$PY $PY - <<'PYEOF' +import os, json, urllib.request, websocket +port = int(os.environ["PORT"]) +# 1) targetId de la primera page +tabs = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json")) +page = next(t for t in tabs if t["type"] == "page") +target_id = page["id"] +# 2) ws browser-level +ver = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json/version")) +ws = websocket.create_connection(ver["webSocketDebuggerUrl"]) +def call(mid, method, params): + ws.send(json.dumps({"id": mid, "method": method, "params": params})) + while True: + m = json.loads(ws.recv()) + if m.get("id") == mid: + return m +win = call(1, "Browser.getWindowForTarget", {"targetId": target_id})["result"] +wid = win["windowId"] +# 3) set bounds (normal). Para maximizar: {"windowState":"maximized"} solo. +call(2, "Browser.setWindowBounds", {"windowId": wid, + "bounds": {"left": 0, "top": 0, "width": 1366, "height": 768, "windowState": "normal"}}) +print(json.dumps(call(3, "Browser.getWindowForTarget", {"targetId": target_id})["result"])) +ws.close() +PYEOF +``` + +Estados válidos de `windowState`: `normal`, `minimized`, `maximized`, `fullscreen`. Para +maximizar/minimizar, manda solo `{"windowState": "maximized"}` sin width/height. + +### 3.2 Pestañas (CRUD) + +```bash +# CREATE — abrir URL en tab nuevo y esperar a que cargue (devuelve tab_id): +TAB_ID="$(PORT=9222 URL='https://example.com' $PY - <<'PYEOF' +import os, sys; sys.path.insert(0, "python/functions") +from pipelines.cdp_open_url_and_wait import cdp_open_url_and_wait +print(cdp_open_url_and_wait(int(os.environ["PORT"]), os.environ["URL"], timeout_s=20)) +PYEOF +)"; echo "tab=$TAB_ID" + +# READ — listar tabs: +$SN tabs +curl -s http://127.0.0.1:9222/json | $PY -c 'import sys,json; [print(t["id"], t["title"][:40], t["url"]) for t in json.load(sys.stdin) if t["type"]=="page"]' + +# DELETE — cerrar un tab por id (endpoint HTTP, sin WebSocket): +curl -s "http://127.0.0.1:9222/json/close/$TAB_ID" >/dev/null && echo "cerrado $TAB_ID" + +# UPDATE — activar / traer al frente un tab por id: +curl -s "http://127.0.0.1:9222/json/activate/$TAB_ID" >/dev/null && echo "activado $TAB_ID" +``` + +`cerrar` y `activar` son gaps de función (se hacen con `curl` al endpoint `/json/`). Candidatos a +`cdp_close_tab` / `cdp_activate_tab` en §6. + +### 3.3 Network (capturar / interceptar) + +**Capturar tráfico** (mapear las APIs reales que usa una página — recon/pentesting). La función +`cdp_har_record` registra todas las peticiones HTTP/WS que ocurren mientras corre una acción y +devuelve un HAR 1.2: + +```bash +# Capturar el tráfico de una recarga de página y guardarlo como HAR: +PORT=9222 OUT=/tmp/capture.har $PY - <<'PYEOF' +import os, sys, json; sys.path.insert(0, "functions") # las cdp_*_go_browser son Go; ver nota +# NOTA: cdp_har_record es Go. Para HAR desde Python usa el flujo de web_proxy (mitm) o +# llama al binario que la compone. Aquí el patrón equivalente en CDP crudo: +PYEOF +echo "Para HAR: usa la app web_proxy (proxy mitm 8889) o cdp_har_record vía un binario Go." +``` + +> Nota de stack: `cdp_har_record`, `cdp_set_cookie`, etc. son funciones **Go** (`*_go_browser`). +> Desde el agente se consumen compiladas dentro de un binario (como `script-navegador`) o, para +> captura de tráfico de largo recorrido, via la app **`web_proxy`** (mitmproxy, proxy `127.0.0.1:8889`, +> UI `http://127.0.0.1:8081`). Ver `CHROMIUM_SYSTEM.md` §"Proxy / captura mitm". + +**Interceptar / bloquear / modificar** peticiones (ej. bloquear imágenes y trackers para acelerar y +limpiar el scraping) — gap de función, hoy via CDP crudo con el dominio `Fetch`: + +```bash +# Bloquear patrones de URL en la tab actual mientras dure la conexión (Ctrl-C para soltar): +PORT=9222 TAB_ID="$TAB_ID" BLOCK='*.png,*.jpg,*.gif,*doubleclick*,*googlesyndication*' $PY - <<'PYEOF' +import os, json, fnmatch, urllib.request, websocket +port=int(os.environ["PORT"]); tab=os.environ["TAB_ID"] +patterns=[p.strip() for p in os.environ["BLOCK"].split(",") if p.strip()] +tabs=json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json")) +ws_url=next(t["webSocketDebuggerUrl"] for t in tabs if t["id"]==tab) +ws=websocket.create_connection(ws_url) +ws.send(json.dumps({"id":1,"method":"Fetch.enable","params":{"patterns":[{"urlPattern":"*"}]}})) +print("interceptando; Ctrl-C para parar") +while True: + m=json.loads(ws.recv()) + if m.get("method")=="Fetch.requestPaused": + rid=m["params"]["requestId"]; url=m["params"]["request"]["url"] + if any(fnmatch.fnmatch(url, p) for p in patterns): + ws.send(json.dumps({"id":99,"method":"Fetch.failRequest","params":{"requestId":rid,"errorReason":"BlockedByClient"}})) + else: + ws.send(json.dumps({"id":99,"method":"Fetch.continueRequest","params":{"requestId":rid}})) +PYEOF +``` + +Candidato a `cdp_intercept_requests` / `cdp_block_urls` en §6. + +### 3.4 DOM — leer e interactuar + +```bash +$SN open https://example.com # navega la tab activa, espera carga + red en reposo (SPAs) +$SN wait '#resultados' # espera a que un selector exista (timeout-ms configurable) +$SN click '.boton-aceptar' # click con trayectoria de ratón Bézier (anti-detección) +$SN click '.x' --instant # click directo (si el elemento no tiene bounding box) +$SN type 'input[name=q]' 'mi búsqueda' # enfoca con click y escribe carácter a carácter +$SN shot /tmp/page.png # screenshot a ARCHIVO (no al contexto). --full = página entera +``` + +**Modificar el DOM** (set attribute, quitar nodos, rellenar valores) — vía `eval`: + +```bash +$SN eval 'document.querySelector("#banner")?.remove(); "removed"' +$SN eval 'document.querySelector("input#token").value="abc"; "set"' +``` + +Click robusto por texto visible (cuando el selector CSS es frágil) — usa `cdp_find_by_text` / +`cdp_click_text` (Go, dentro del binario) o, equivalente en `eval`: + +```bash +$SN eval '[...document.querySelectorAll("button")].find(b=>b.innerText.trim()==="Siguiente")?.click(); "clicked"' +``` + +### 3.5 Ejecutar JavaScript — la vía de extracción compacta + +`eval` devuelve el resultado serializado. **Haz que el JS devuelva solo lo extraído**, no el DOM: + +```bash +# Título y nº de resultados — devuelve 2 valores, no la página: +$SN eval 'JSON.stringify({title: document.title, n: document.querySelectorAll(".item").length})' + +# Extraer una tabla a array de objetos (esto es lo que llega al contexto, ~compacto): +$SN eval ' +JSON.stringify([...document.querySelectorAll("table.precios tbody tr")].map(tr => { + const td = tr.querySelectorAll("td"); + return { producto: td[0]?.innerText.trim(), precio: td[1]?.innerText.trim() }; +}))' +``` + +### 3.6 AX tree — la vista semántica anti-colapso (receta estrella) + +El accessibility tree es cómo los lectores de pantalla "ven" la página: una jerarquía de roles +(`button`, `link`, `heading`, `textbox`, `list`...) con su nombre accesible. `trim_ax_tree` ya +descarta nodos ignorados, `generic`/`none` vacíos y `StaticText` vacíos, y colapsa cadenas de un +solo hijo. El resultado es una vista compacta y navegable de la página. + +**Receta canónica — AX snapshot a outline de texto** (compón `cdp_get_ax_tree` + `trim_ax_tree` y +renderiza a líneas indentadas `role "nombre"`): + +```bash +# Requiere TAB_ID (ver §3.2). Devuelve un outline compacto, NO el HTML. +PORT=9222 TAB_ID="$TAB_ID" MAXLINES=400 $PY - <<'PYEOF' +import os, sys; sys.path.insert(0, "python/functions") +from pipelines.cdp_get_ax_tree import cdp_get_ax_tree +from core.trim_ax_tree import trim_ax_tree + +port=int(os.environ["PORT"]); tab=os.environ["TAB_ID"]; maxlines=int(os.environ.get("MAXLINES","400")) +nodes = trim_ax_tree(cdp_get_ax_tree(port, tab)) + +by_id = {n["nodeId"]: n for n in nodes} +children = set(c for n in nodes for c in n.get("childIds", [])) +roots = [n for n in nodes if n["nodeId"] not in children] or nodes[:1] + +def field(n, k): + v = n.get(k, {}) + return v.get("value", "") if isinstance(v, dict) else (v or "") + +out, count = [], 0 +def walk(n, depth): + global count + if count >= maxlines: return + role = field(n, "role"); name = field(n, "name") + if role: # omite nodos sin role + line = " "*depth + role + (f' "{name}"' if name else "") + out.append(line[:200]); count += 1 + for cid in n.get("childIds", []): + ch = by_id.get(cid) + if ch: walk(ch, depth+1) +for r in roots: walk(r, 0) + +print("\n".join(out)) +if count >= maxlines: + print(f"... [truncado a {maxlines} líneas; sube MAXLINES o usa chunk_ax_tree]") +PYEOF +``` + +Salida típica (compacta, lista para razonar): + +``` +WebArea "Example Domain" + heading "Example Domain" + paragraph "This domain is for use in illustrative examples..." + link "More information..." +``` + +**Página enorme** — trocea con `chunk_ax_tree` y procesa chunk a chunk (cada chunk ≤ max_chars y +arranca con un nodo `context` que da el path desde la raíz): + +```bash +PORT=9222 TAB_ID="$TAB_ID" $PY - <<'PYEOF' +import os, sys, json; sys.path.insert(0, "python/functions") +from pipelines.cdp_get_ax_tree import cdp_get_ax_tree +from core.trim_ax_tree import trim_ax_tree +from core.chunk_ax_tree import chunk_ax_tree +nodes = trim_ax_tree(cdp_get_ax_tree(int(os.environ["PORT"]), os.environ["TAB_ID"])) +chunks = chunk_ax_tree(nodes, max_chars=25000) +print(f"{len(nodes)} nodos -> {len(chunks)} chunks") # procesa chunks[i] de uno en uno +PYEOF +``` + +**Proponer un schema de scraping automáticamente** desde el AX tree (orquesta trim → chunk → LLM): +función `llm_propose_scraping_schema(url, ax_tree, ...)` (`*_py_infra`). + +--- + +## 4. Recetas de tarea end-to-end + +### 4.1 Entender una página desconocida sin colapsar + +1. `$SN open ` (o crea tab con `cdp_open_url_and_wait`, §3.2) y captura el `TAB_ID`. +2. AX snapshot a outline (§3.6). **Esto** es lo que lees, no el HTML. +3. Si necesitas un detalle puntual, `$SN eval ''`. + +### 4.2 Extraer datos estructurados (scraping) + +1. Abre y espera (`$SN open` ya espera carga + red en reposo; para SPAs añade `$SN wait ''`). +2. AX snapshot para localizar los contenedores/roles relevantes. +3. Un único `$SN eval` que devuelva el array de objetos final (§3.5). Solo el resultado llega al + contexto. Persiste a `vaults/` o a archivo si es grande. + +### 4.3 Login + scrape de un SPA + +1. `$SN open `; `$SN type 'input[name=user]' ''`; `$SN type 'input[name=pass]' '

'`; + `$SN click 'button[type=submit]'`. +2. `$SN wait ''` (espera real, no sleep). +3. Navega y extrae con `eval` (§4.2). Sobre el puerto **9222** reaprovechas la sesión ya logueada del + usuario; sobre un perfil dedicado, el login persiste en su `user-data-dir` entre ejecuciones. + +### 4.4 Mapear la API oculta de un sitio (recon) + +1. Arranca la captura: app `web_proxy` (proxy mitm `127.0.0.1:8889`) o `cdp_har_record` envolviendo + la acción. Para acelerar, bloquea imágenes/trackers con el interceptor `Fetch` (§3.3). +2. Navega/interactúa para disparar las llamadas (`$SN open`, `$SN click`). +3. Consulta el tráfico: `./web_proxy query "~d dominio.com" --last`, `./web_proxy har salida.har`. + Los endpoints XHR/fetch que aparecen son las APIs reales del sitio. + +--- + +## 5. Gotchas (además de los de `CHROMIUM_SYSTEM.md`) + +- **No leas el HTML al contexto.** Es el error de colapso nº 1. AX outline o `eval` con resultado. +- **`tab_id` ≠ `targetId` de ventana.** Las funciones AX usan el `id` de `/json` (la tab). El control + de ventana (§3.1) usa el `webSocketDebuggerUrl` browser-level de `/json/version`. +- **`Page.loadEventFired` no dispara en SPAs** con routing sin recarga. Usa `$SN wait ''` + o `cdp_wait_idle` (red en reposo) en lugar de fiarte del load event. +- **Selector de perfil:** si lanzas chromium con varios perfiles sin `--profile-directory`, Chrome se + atasca en el picker y CDP opera sobre una ventana vacía. Siempre pasa el perfil. Ver + `CHROMIUM_SYSTEM.md`. +- **`--remote-allow-origins=*` entre comillas en zsh** (el `*` se expande como glob). +- **Lock por user-data-dir:** dos chromium no comparten el mismo `--user-data-dir`. Para automatizar + en paralelo a la sesión del usuario, usa un `--user-data-dir` dedicado. +- **El proxy mitm (8889) puede ralentizar sitios externos.** Si una tab se cuelga durante captura, + sube timeouts o revisa el upstream del proxy. Ver `CHROMIUM_SYSTEM.md`. +- **AX tree obsoleto tras cambios JS:** el snapshot es del momento. Tras un click que muta el DOM, + vuelve a pedir el AX tree. + +--- + +## 6. Gaps → hoja de ruta del MCP + +Cuando montemos el servidor MCP de navegador (para que cualquier LLM use estas capacidades como +tools nativas, sin recordar comandos), cada fila se vuelve una tool. Las marcadas "función pendiente" +hoy se hacen con el CDP crudo de §3 y son candidatas a `fn-constructor` antes/junto al MCP. + +| Tool MCP propuesta | Estado de la capacidad subyacente | Origen | +|---|---|---| +| `browser_tabs_list` / `_new` / `_open_wait` | ✅ función existe | `cdp_list_tabs`, `cdp_new_tab`, `cdp_open_url_and_wait` | +| `browser_tab_close` / `_activate` | ⏳ función pendiente (`/json/close`, `/json/activate`) | §3.2 | +| `browser_ax_snapshot` (outline compacto) | ⏳ función pendiente (compón get+trim+render) | §3.6 | +| `browser_ax_chunks` | ✅ `chunk_ax_tree` | §3.6 | +| `browser_eval` | ✅ `cdp_evaluate` | §3.5 | +| `browser_click` / `_type` / `_wait` | ✅ funciones Go | §3.4 | +| `browser_screenshot` (a archivo, devuelve ruta) | ✅ `cdp_screenshot` | §3.4 | +| `browser_window_get` / `_set_bounds` | ⏳ función pendiente (`Browser.*WindowBounds`) | §3.1 | +| `browser_net_capture_har` | ✅ `cdp_har_record` (Go) | §3.3 | +| `browser_net_block` / `_intercept` | ⏳ función pendiente (`Fetch` domain) | §3.3 | +| `browser_set_cookie` | ✅ `cdp_set_cookie` (Go) | — | + +**Principio de diseño del MCP:** las tools devuelven **siempre representaciones compactas** — +AX outline, JSON de campos, rutas de archivo para screenshots/HAR — nunca el HTML o la imagen cruda. +El anti-colapso se hace cumplir en el borde de la tool, no en la disciplina del LLM. + +--- + +## 7. Cheatsheet + +```bash +SN=projects/web_scraping/apps/script_navegador/script-navegador +PY=python/.venv/bin/python3 + +$SN launch --port 9333 --profile-directory Automation # chrome dedicado aislado +$SN tabs # listar pestañas +$SN open https://sitio.com # navegar + esperar +$SN wait '#listo' # esperar selector +$SN click '.btn' # click humano (Bézier) +$SN type 'input[name=q]' 'texto' # escribir +$SN eval 'JSON.stringify({t:document.title})' # extraer compacto +$SN shot /tmp/x.png # screenshot a archivo + +# AX snapshot compacto (NO html) -> ver §3.6 para el heredoc completo +# tab nuevo: cdp_open_url_and_wait(port, url) (§3.2) +# cerrar tab: curl .../json/close/ (§3.2) +# bloquear net: Fetch.enable + failRequest (§3.3) +# ventana: Browser.getWindowForTarget/setWindowBounds (§3.1) +``` + +**Norma única que lo resume todo:** para *mirar* una página → AX outline; para *sacar* datos → +`eval` que devuelve solo el resultado; el HTML crudo y los screenshots van a archivo, jamás al +contexto. diff --git a/project.md b/project.md index a070214..93a74fe 100644 --- a/project.md +++ b/project.md @@ -47,3 +47,9 @@ crudo a través de las funciones del dominio `browser` del registry, no con Play Las reglas operativas (tamaño de ventana, perfil del proyecto, headless, jitter, captura, movimientos realistas, proxys rotativos, CDP en el navegador del usuario) están en `CONVENTIONS.md`. + +Para **controlar el navegador desde un agente LLM** (Claude u otro) sin saturar su contexto, la +guía operativa es `LLM_BROWSER_GUIDE.md`: cómo conectar via CDP, qué comandos usar por capacidad +(ventanas, pestañas, network, DOM, JS) y, sobre todo, cómo leer páginas via accessibility tree +recortado en lugar de volcar el HTML crudo (anti-colapso). Incluye la hoja de ruta del futuro +servidor MCP de navegador.