docs(browser): guía de control de navegador para LLMs (CDP anti-colapso)
Añade LLM_BROWSER_GUIDE.md: guía operativa para que un agente LLM (Claude u otro) controle los navegadores del equipo via Chrome DevTools Protocol sin saturar su contexto. Cubre la conexión (puertos 9222 diario / 9333 dedicado), las capacidades por eje (ventanas, pestañas, network, DOM, ejecución de JS) con recetas lanzables, y sobre todo la lectura de páginas mediante accessibility tree recortado (cdp_get_ax_tree + trim_ax_tree, render a outline) en lugar de volcar el HTML crudo, que es la causa principal de colapso de contexto. Incluye una tabla de presupuesto de tokens por acción, recetas de tarea end-to-end (entender página, scraping, login+SPA, mapear API oculta), los gotchas heredados de CHROMIUM_SYSTEM.md y la hoja de ruta del futuro servidor MCP de navegador (cada capacidad como tool que devuelve representaciones compactas en el borde). project.md referencia la nueva guía. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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=<x> --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 '<js>'` | — |
|
||||
| **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 <url>` (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 '<js que devuelve solo ese dato>'`.
|
||||
|
||||
### 4.2 Extraer datos estructurados (scraping)
|
||||
|
||||
1. Abre y espera (`$SN open` ya espera carga + red en reposo; para SPAs añade `$SN wait '<selector>'`).
|
||||
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 <login_url>`; `$SN type 'input[name=user]' '<u>'`; `$SN type 'input[name=pass]' '<p>'`;
|
||||
`$SN click 'button[type=submit]'`.
|
||||
2. `$SN wait '<selector que solo existe logueado>'` (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 '<selector>'`
|
||||
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/<id> (§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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user