Octocrawl
Web scraping para agentes de IA: páginas públicas como Markdown, tablas y JSON, cada una con un Registro de Evidencia (URL final, estado HTTP, decisión de robots.txt, hashes). Las páginas bloqueadas o vacías devuelven el motivo. Alojado (sin cuenta, 20 páginas/día) o local mediante npx.
Servidor MCP alojado
npx add-mcp 'https://mcp.octocrawl.dev/mcp'Se instala en Claude Code, Codex, Cursor y más
Documentación
Octocrawl — Extracción de Contexto Web a LLM
Un sistema de extracción web transparente y verificable, construido para flujos de trabajo RAG y de Agentes.
Pruébalo sin instalar nada en octocrawl.dev, lee la documentación, o conecta tu agente al Octocrawl alojado en una línea, sin necesidad de clave para empezar (cómo, para Claude Code, Cursor, OpenCode y Codex):
claude mcp add --transport http octocrawl https://mcp.octocrawl.dev/mcp
Por Qué Existe Esto
La mayoría de los rastreadores informan "éxito" cuando devuelven páginas vacías, pantallas de desafío o el contenido incorrecto. Octocrawl hace que el fallo sea visible y reparable:
Antes (rastreador típico):
✓ Fetched example.com/article
Status: 200 OK
Content: 953 bytes
Después (Octocrawl):
✗ Fetched example.com/article
Status: blocked (cloudflare_challenge)
Lane: http → escalated to browser_local
Evidence: artifacts=[] (no screenshot or DOM snapshot was produced)
Cost: 847 tokens, 2.3s, $0.0042
Fix: needs user login or proxy (tier 1b/2)
Qué es Diferente
- El fallo es un resultado de primera clase —
empty_verified,blocked,failedcon razones, no vacíos silenciosos; una página que respondió con un estado de error conserva suhttpStatusy Markdown como evidencia, nunca como éxito, y también lo hace una página en la que Octocrawl no encuentra contenido principal (failed/empty_unverifiedcon el Markdown de toda la página), una región principal que la página oculta conhiddeny una región de artículo que solo nombra la página incluida (encabezados de menos de 100 caracteres y casi nada más junto a ellos, ninguna imagen que el Markdown contenga, enlaces de salto dentro de la página como "Saltar a Filtros" apartados) - Cinco comprobaciones de falso éxito — texto de desafío, contenido de página incorrecto, hechos faltantes, truncamiento, rendimiento por debajo del mínimo
- Escalera de ejecución — HTTP → navegador → autenticación de usuario → proxy, con enrutamiento automático, rastreo por intento y contabilidad de costos a nivel de tarea
- Punto de referencia de verdad fundamental — una suite de 56 casos (soft 404, páginas de desafío, SPA, tiempos de espera, bombas zip, tablas) con tasas verificadas de falso éxito
- Evidencia honesta —
artifacts: []es una lista explícita de artefactos vacíos, no una promesa de que cada página fallida tiene una captura de pantalla o una instantánea del DOM; elbytesWire: nulldel navegador significa que los bytes de la red no se midieron - Un Registro de Evidencia — cada resultado de raspado, elemento de lote y página de rastreo lleva
evidenceRecord(URL final, cadena de redirecciones, tiempo de recuperación, estado y razón, carril, decisión de robots.txt, hashes de entrada y salida, evidencia de campo), expresado de la misma manera en cada carril y descrito por un JSON Schema versionado; consulta la referencia
Inicio Rápido
La vista previa web de una sola página, sin instalación, se ejecuta en octocrawl.dev
(cinco vistas previas al día; consulta Vista previa pública para saber cómo se implementa). Además de Markdown,
devuelve los enlaces y metadatos de una página, y hasta 20 campos leídos de la página sin un modelo. El mismo sitio sirve
la documentación: pasos de conexión MCP para cuatro clientes, cuatro guías de tareas y límites. Las páginas se generan a partir de
Markdown en este repositorio durante la compilación web pública;
npm run public:preview:local las sirve en http://127.0.0.1:8798/docs/.
No instales nada primero: con Node.js 22.13+ o 24+,
npx octocrawl scrape https://example.com --markdown
pip install octocrawl-client # the Python client of a running API (npx octocrawl serve)
npm install @octocrawl/sdk # the TypeScript client; @octocrawl/mcp is the MCP server
Los paquetes se publican desde este repositorio (0.3.0 el 2026-10-05); el flujo de trabajo Install check ejecuta la línea npx desde una caché vacía en macOS, Windows y Linux cada semana. Para trabajar en el código, usa Node.js 22.13+ o 24+ y npm (el motor de texto PDF, pdf.js, necesita 22.13 o posterior) y compila desde este checkout. Para el flujo de trabajo completo Monitor → resultado → evento HTTPS → reinicio, sigue la guía de incorporación y la lista de verificación de aceptación independiente para desarrolladores.
git clone https://github.com/77777R7/w2l.git
cd w2l
npm ci
npx playwright install chromium
npm run typecheck
npm test
npm run scrape -- https://example.com --markdown
npm run crawl -- https://example.com --max-pages 20
octocrawl (el paquete @octocrawl/cli, también publicado como octocrawl; npm run w2l -- <command> en un checkout) ejecuta el motor de la API en su propio proceso, por lo que cada opción de la API REST funciona en la línea de comandos. Cada opción es una bandera bajo su nombre kebab-case: maxAge es --max-age, onlyMainContent: false es --no-only-main-content, includeTags toma a,b y puede repetirse, formats toma markdown,tables o un array JSON para una entrada con opciones, parsers toma pdf, none o JSON, headers toma JSON o --header name=value (repetible), y las URLs son argumentos. La solicitud es verificada por el propio analizador de la API REST, por lo que un valor que la API rechaza aquí se rechaza con el mismo mensaje (código de salida 2).
octocrawl scrape <url>imprime la respuesta de raspado como JSON (compacto, como lo recibe MCP;--debugpara el completo), o solo el Markdown con--markdown.octocrawl batch <url>...(o--urls-file <file>, una URL por línea) yoctocrawl crawl <url>se ejecutan hasta el final e imprimen{ report, items }. Ctrl-C deja el trabajo en pausa:octocrawl crawl --resume <taskId>continúa un rastreo (un rastreo registrado como pendiente o en ejecución se rechaza, ya que otro proceso podría estar ejecutándolo), y un lote se reanuda cuando la API se inicia en la misma raíz de tarea.--webhookno se ofrece, ya que un comando no ejecuta un trabajador de entrega: envía el trabajo aoctocrawl serveen su lugar.octocrawl map <url>imprime el mapa.--out <dir>escriberesults.jsonl(un resultado por línea),results.csv(una fila por página con su evidencia, páginas fallidas incluidas: las columnas delto_pandas()del cliente de Python sin el Markdown, luegomarkdown_file),report.jsonpara un trabajo, y por página<n>-<host-path>.mdcon su Markdown y<n>-<host-path>.table-<i>.csvpara cada tabla del formatotables.octocrawl serveejecuta la API local, como lo hacenpm run api, con las mismas banderas (--port,--host,--hosted,--token, ...).octocrawl login import <site>guarda tu inicio de sesión en un sitio desde el Chrome que ya usas, para que--mode authedlea sus páginas con tu sesión iniciada;octocrawl login listyoctocrawl login remove <site>muestran y olvidan inicios de sesión guardados sin imprimir una cookie. Consulta Tu propio inicio de sesión (modo authed).
Códigos de salida: 0 para una página leída como contenido o un trabajo completado, 1 para cualquier otra cosa que Octocrawl respondió, 2 para una línea de comandos rechazada, 130 cuando se interrumpe. La raíz de tarea, donde viven las tareas, los archivos guardados y la caché de páginas, es --task-root, si no W2L_TASK_ROOT, si no .w2l/cli, aparte del .w2l/api de la API; nunca apuntes un comando a la raíz de tarea de un servidor API en ejecución, que podría ejecutar el mismo trabajo dos veces. Un comando nunca reanuda los trabajos anteriores de la raíz de tarea, como hace la API cuando se inicia. La herramienta de escalera anterior en proceso es w2l-ladder (y w2l-fetch) en @w2l/bench.
Tu propio inicio de sesión (modo authed)
El modo authed lee una página con el inicio de sesión que guardaste para su sitio, en el propio navegador de Octocrawl. Para guardar uno, inicia sesión en el sitio en Google Chrome (144 o posterior), en el perfil predeterminado y una ventana normal; abre chrome://inspect/#remote-debugging y activa "Permitir depuración remota para esta instancia del navegador" una vez; luego ejecuta octocrawl login import example.com (un dominio o una URL de página). Chrome pregunta "¿Permitir depuración remota?" por cada conexión: haz clic en Permitir. Octocrawl se conecta una vez, lee las cookies de ese sitio (las propias, las de un dominio padre y las de sus subdominios cuando el sitio establece cookies en el propio nombre) y el localStorage de las pestañas del sitio que tienes abiertas, las guarda y se desconecta; nunca lee los archivos de Chrome en el disco y lee el almacenamiento de una pestaña sin cargar nada ni ejecutar scripts en ella. Los inicios de sesión se guardan en W2L_SESSIONS_FILE, si no ~/.w2l/sessions.json, legibles solo por ti, y la línea de comandos, la API local y el servicio MCP local leen el mismo archivo. Los registros llevan el SHA-256 del inicio de sesión, el recuento de cookies y los orígenes de localStorage y el recuento de elementos, nunca un valor. La misma importación funciona sin la línea de comandos en un servidor que se ejecuta en tu máquina: POST /v1/logins/import con { "site": "example.com" } (y approveTimeoutMs, cuánto tiempo esperar tu clic en Permitir, de 10 s a 10 min, predeterminado 2 min) responde { domain, savedAt, cookieCount, localStorage, localStorageRead, sessionSha256 } una vez que haces clic en Permitir (localStorage: { origins, itemCount } guardado, o null; localStorageRead false cuando ninguna pestaña del sitio estaba abierta, por lo que no se leyó ninguna; localStorageUnread, los orígenes de las pestañas abiertas cuyo almacenamiento Chrome no dio, una pestaña bloqueada o descartada, que una recarga recupera). GET /v1/logins lista los inicios de sesión guardados y DELETE /v1/logins/:site olvida uno. Los importLogin, listLogins y removeLogin del SDK, y las herramientas MCP locales import_login, list_logins y remove_login, llaman a estas rutas. Un servidor MCP local ofrece las tres. import_login permite que un agente elija qué inicio de sesión de sitio guardar, por lo que su descripción le dice al agente que te pregunte primero, y el clic en Permitir de Chrome sigue siendo tuyo. Ninguna ruta responde una cookie. Un servidor que no está en loopback, el servicio alojado y un servidor sin tu Chrome rechazan una importación con 409.
- Un inicio de sesión para
example.comcubrewww.example.comy otros subdominios. Cuando uno aplica, el peldaño authed va primero, antes de los peldaños públicos, que tomarían una página con sesión cerrada como respuesta; si el sitio rechaza el inicio de sesión (login_wall, por ejemplo, cuando expiró), la ejecución termina allí en lugar de devolver la página con sesión cerrada. Un rechazo es una páginalogin_wall, una redirección a la página de inicio de sesión del sitio, o una página que pide un inicio de sesión en el lugar: un encabezado o línea de texto plano entre sus primeros ocho que comienza con la solicitud, como "Inicia sesión para ver tus listas de deseos", "Por favor inicia sesión", "Debes haber iniciado sesión" o "Inicio de sesión requerido". El enlace de Iniciar sesión de un encabezado, una fila de tabla, un elemento de lista, una línea con un enlace, una línea donde las palabras vienen después de otras ("Paso 2: Inicia sesión para continuar") y la prosa más abajo son el contenido de la página, no un rechazo; también lo es un encabezado que el título de la página nombra (un problema o una pregunta cuyo asunto comienza con esas palabras, "Por favor inicia sesión de nuevo #1411"). La regla coincide solo con inglés.ladder_session_rejectednombra cuáles:redirectedToosignInPrompt. Importa el sitio de nuevo para reemplazar un inicio de sesión expirado; un servidor en ejecución usa el nuevo de inmediato. - El modo
authedfunciona en raspado y lote, no en rastreo: un rastreo sigue cada enlace, y un enlace de cierre de sesión terminaría tu sesión en Chrome también, ya que las cookies guardadas son esa sesión. Envía las páginas como un lote. - Un sitio que mantiene su inicio de sesión en
localStorage(un token que su script lee) necesita una pestaña abierta cuando importas: Chrome lee el almacenamiento de un origen solo a través de una página que lo muestra, por lo que sin ninguna abierta la importación guarda solo las cookies y dicelocalStorageRead: false. Una pestaña cuyo almacenamiento Chrome no da (se bloqueó, se descartó o se cerró mientras tanto) se guarda sin él y se nombra enlocalStorageUnread, con la solicitud a Chrome que falló y la respuesta de Chrome enlocalStorageUnreadReasons. Solo se leen las pestañas en el perfil del que provienen las cookies, nunca las de una ventana de Incógnito, y solo el origen de cada pestaña, no un marco de otro origen dentro de ella;sessionStoragee IndexedDB no se guardan. La depuración remota de Chrome alcanza solo el perfil predeterminado. - Un sitio puede vincular su inicio de sesión a más que las cookies (una sesión del lado del servidor, el navegador, la dirección de red); Octocrawl no imita tu navegador, por lo que ese sitio responde como si hubieras cerrado sesión (consulta la ejecución I1).
Acceso mejorado (una concesión de acceso)
Para personas que no quieren los interruptores técnicos, una opción elige cómo se alcanzan las páginas: "access": "standard", "enhanced" o "my-browser" en POST /v1/scrape y POST /v1/batches (standard o enhanced en POST /v1/crawl), las herramientas MCP scrape, batch_scrape y crawl, y --access en la línea de comandos.
standard: la propia obtención de Octocrawl y su navegador local. Ningún peldaño que cueste a un tercero se ejecuta; la auditoría de enrutamiento indica cuáles descartó (ladder_channels_filtered, motivoaccess standard).enhanced: también lo que aprueba la concesión de acceso de nivel mejorado del servidor: los proveedores de pago a continuación, en cualquier modo, incluido el modo estándar. El presupuesto de ejecución de la concesión limita un lote o un rastreo; una extracción única llama a cada proveedor que el servidor nombra como máximo una vez. Un servidor sin dicha concesión lo rechaza por nombre (unsupported_parameter).my-browser: tu propio Chrome, igual que"lane": "my-browser"(abajo). Un rastreo no lo utiliza.
Sin access, una solicitud se ejecuta según la configuración del servidor. Un lote o rastreo mantiene su elección, por lo que una ejecución reanudada hace la misma. Las opciones por ruta a continuación permanecen para desarrolladores.
Por defecto, un servidor no utiliza ninguna de las capacidades que ADR 0005 pone detrás de una concesión: sin navegador de proveedor, sin resolución de desafíos, sin sigilo de proveedor. Un operador que las quiera inicia el servidor con una concesión, un archivo JSON que el servidor verifica al inicio; una concesión con cualquier problema detiene el inicio con todos los problemas enumerados.
octocrawl serve --access-grant grant.json
{
"tier": "enhanced",
"capabilities": ["vendor_remote_browser", "vendor_captcha_solving"],
"budget": { "perRunUsd": 5, "perRequestUsd": 0.5 },
"tariffs": { "browserbase": { "perHourUsd": 0.12, "maxSessionMs": 120000, "minBilledMs": 60000, "billingIncrementMs": 60000 } },
"attestation": { "principal": "you@example.com", "at": "2026-10-05T12:00:00Z", "statement": "I accept the provider's terms and the cost of these routes." }
}
W2L_ACCESS_GRANTtoma lo mismo, como ruta de archivo o el propio JSON.- Una capacidad que ADR 0005 rechaza (rotación de identidad, parchear tu propio Chrome y las demás que enumera) o difiere (un motor de navegador propio, Camoufox y las demás) es un error, no se ignora; una diferida nombra la fila de ROADMAP que indica cuándo se reinicia.
- Una concesión
standardomy_browsersolo puede nombrarcompatible_transportyegress_sessions. Una capacidad que puede costar dinero necesita unperRunUsdpositivo, y unaenhancednecesita una atestación. - Los peldaños de proveedor (
W2L_VENDORScon su clave, en modoresearchoauthed) se construyen solo cuando la concesión nombravendor_remote_browser; la resolución de desafíos y el sigilo del proveedor siguen avendor_captcha_solvingyvendor_stealth. - La escalera continúa a su siguiente peldaño (el navegador, luego un proveedor) por un bloqueo o verificación que reconoce, y también por un 403 o 405 respondido sin uno (la defensa antibots de un sitio a menudo responde así), por una página que un navegador renderizó sin contenido principal que pudiera verificar, y por la conexión rechazada del peldaño HTTP (su siguiente peldaño es el navegador, cuyo propio fallo de red termina la ejecución); el rastro dice por qué (
ladder_stepconescalate). Cuando los peldaños más fuertes luego fallan sin una página (un error de red, un error propio del proveedor, el plazo), la página que superó es la respuesta (ladder_evidence_kept). Se detiene en un tiempo de espera (un sitio lento o muerto retendría la extracción también por la espera del navegador), en un límite de tasa (429, cuyoRetry-Afterun lote o rastreo honra), en un estado de error que es la respuesta propia de la página (404, 410, 5xx), en el peldaño de tu inicio de sesión guardado (los peldaños posteriores no llevan el inicio de sesión), y después de que un peldaño anterior encontró contenido, que sigue siendo la respuesta. - Un proveedor se llama solo con un techo de precio conocido (elemento 4 de PA de ROADMAP).
tariffsnombra, por proveedor, los precios que aceptaste de su página de precios (perCallUsd,perHourUsd) y qué limita el tiempo de una llamada: un precio por hora necesitamaxSessionMs, la sesión más larga que una llamada puede mantener;minBilledMses el tiempo más corto que el proveedor factura una sesión, ybillingIncrementMsel paso en que factura (un minuto, redondeado hacia arriba). El techo de una llamada esperCallUsdmásperHourUsdpor su sesión más larga, al menosminBilledMsy al menos el tiempo de espera más corto que toma el proveedor (60 s para Browserbase, 15 s para Steel), redondeado al paso. Bajo una tarifa, cada llamada abre una conexión y una sesión propias, nunca compartidas con otra llamada, las termina enmaxSessionMsy las libera; la sesión también se crea con el tiempo de espera propio del proveedor y sus proxies desactivados, por lo que termina en el lado del proveedor si la liberación falla, y no factura ancho de banda. Un precio por GB se rechaza: los bytes que el navegador de un proveedor recibe a través de cada objetivo que abre no se pueden contar desde aquí, por lo que un costo de ancho de banda no tiene techo. Un proveedor sin tarifa no se llama (ladder_channel_skipped,no price ceiling). - El gasto se reserva antes de cada llamada y se liquida después, en un libro mayor por tarea que comparten cada página, reintento y proveedor de un lote o rastreo, y que una ejecución reanudada o ampliada abre con lo que la tarea ya fue cargada: una llamada se hace solo cuando su techo cabe en lo que
perRunUsd(y el tope propio de la tarea) ha dejado, por lo que las páginas concurrentes no pueden pasarlo, y se liquida al precio que el proveedor informó o, cuando no informó ninguno (Browserbase y Steel no indican precio por solicitud), a su techo, nunca por debajo del costo; una llamada que lanzó una excepción o que el plazo cortó se carga a su techo.perRequestUsdlimita las llamadas de una página dentro de la ejecución, y las de una extracción única (si no,perRunUsdlo hace). Una llamada cuyo techo no cabe se omite y el rastro lo dice (budget); una tarea cuyo tope se agota se detiene (budget_exceededconcost). La respuesta mantieneusage.externalCostUsdpara el costo exacto (nulo cuando se desconoce) y agregaexternalCostChargedUsd, lo que el libro mayor cargó a todas las llamadas de la ejecución, con un evento de rastrospend_settledpor llamada. Un proveedor llamado sin presupuesto alguno se reserva y liquida en un libro mayor sin tope propio de la ejecución, por lo que sus llamadas también quedan registradas. El Registro de Evidencia de cada página los enumera enaccess.paidCalls, en orden: el proveedor (provider), el peldaño, las capacidades de ADR 0005 con las que se creó su sesión (capabilities), el techo reservado (ceilingUsd), lo que el libro mayor cargó (chargedUsd), el precio que el proveedor indicó (reportedCostUsd, nulo cuando no indicó ninguno), youtcomeyreason, lo que Octocrawl hizo de la página que la llamada devolvió por sus propias verificaciones (una página de bloqueo, una lectura vacía o no verificada, una identidad que no envió), nunca la palabra del proveedor de que tuvo éxito, nulo cuando la llamada no devolvió página;answermarca la llamada cuya página es la del registro.access.grantnombra la concesión bajo la que se hicieron por el SHA-256 de su texto (shasum -a 256 grant.jsonda lo mismo), su nivel y su tiempo de atestación, nunca el principal o la declaración de la atestación. Una página leída de nuevo en otra salida mantiene las llamadas de la lectura que abandonó, una página que leíste en tu Chrome después de una verificación mantiene las de la ejecución que la verificación detuvo, y una página de lote o rastreo cuya ejecución lanzó una excepción después de una llamada de pago la mantiene (ninguna de estas es suanswer); una página de la caché enumera las de la obtención que reutiliza. Ambos sonnullcuando no se llamó a ningún proveedor. El costo de un modelo para la extracción JSON se informa aparte (modelUsage) y no se cuenta. Una concesión que nombrescope.hostsse rechaza, ya que nada limita las rutas a esos hosts todavía. W2L_BROWSER_ENGINE=patchrightejecuta el peldaño de navegador público en Patchright, un fork mantenido de Playwright, cuando la concesión nombraenhanced_browser; un servidor alojado lo rechaza, y el peldaño de inicio de sesión guardado y una sesión gestionada mantienen Playwright estándar. Patchright no se instala con Octocrawl: instala los dos juntos (npm install octocrawl patchright, onpx -p octocrawl -p patchright octocrawl ...), luegonpx patchright install chromium. Un pasoexecuteJavascriptaún se ejecuta en el mundo JavaScript propio de la página sobre él. Una página obtenida sobre él lo dice en su rastro (browser_engine). Sigue siendo un experimento: el 2026-10-06, en una salida, no alcanzó más páginas que Playwright estándar y no perdió ninguna (registro), por lo que Octocrawl no lo activa por defecto.W2L_COMPAT_HOSTS=example.com,shop.exampleenvía las páginas de esos hosts, y sus subdominios, sobre un transporte HTTP compatible con navegador (impit) cuando la concesión nombracompatible_transport: el peldañohttpse convierte enhttp_compat, en modo estándar en un servidor local; un servidor alojado lo rechaza. La solicitud lleva los encabezados y el protocolo de enlace TLS propios de Chrome, por lo que la página registra esa identidad de Chrome (identity_sent) y el transporte (transport). Una solicitud conheadersomobilepersonalizados mantiene el peldañohttp, ya que el transporte no puede enviarlos sin cambiar el conjunto de encabezados de Chrome, y también lo hace la página de inicio de un mapa, leída bajo la identidad que el mapa informa. impit decodifica cuerpos comprimidos por sí mismo, por lo que dicha página informa su tamaño de cable como desconocido. SinW2L_COMPAT_HOSTS, una concesión que nombrecompatible_transportlo usa para los hosts que su aceptación mostró que ayuda: cinco hosts (research/access/benefit-hosts.v1.json) cuya tarea bloqueada verificó en ambas ventanas G1 sin regresión (research/access/runs/2026-10-07-g1-acceptance-5afe577.md). No es para cada host: en todo el conjunto de tareas también convirtió rechazos en respuestas cuyos datos eran incorrectos.W2L_COMPAT_HOSTS=nonelo desactiva; nombrar hosts reemplaza la lista.egress_sessionsda a cada lote y rastreo (no a una extracción única, ni al modoauthed, que tiene tu inicio de sesión guardado) una sesión de cookies: las cookies que sus páginas establecen se envían de nuevo a su sitio en sus páginas posteriores, por el peldaño HTTP, el transporte compatible y el navegador por igual, por lo que una página cuya verificación el navegador despejó permite que la siguiente página de la tarea de ese sitio vaya por HTTP. Las cookies se emparejan por dominio y ruta como hace un navegador, se mantienen en el directorio de la tarea (cookie-session.<route>.json, una por ruta de salida, legible solo por ti) para que un lote o rastreo reanudado tras un reinicio continúe con ellas, se eliminan cuando la tarea termina y nunca se registran: el rastro de una página nombra el id aleatorio de la sesión y los conteos (session_cookies). Una página leída con una sesión no se almacena en caché.W2L_EGRESS_PROXIES=http://user:pass@proxy-a:8080,http://proxy-b:3128(conegress_sessions; un servidor alojado lo rechaza) envía cada obtención a través de tus propios proxies. Un lote o rastreo mantiene uno para su ejecución, y su sesión de cookies pertenece a ese; se mueve al siguiente proxy saludable solo cuando el proxy mismo falla: después de que una página no obtuvo respuesta HTTP de ningún peldaño, Octocrawl pide al proxy un túnel a un nombre que no existe, y continúa solo si el proxy no responde o rechaza sus credenciales (407), como máximo dos veces por ejecución, con una nueva sesión de cookies, y la página se lee de nuevo allí (egress_switcheden su rastro). Nunca se mueve por lo que un sitio hizo: un bloqueo, un desafío, un 429 o una conexión que el sitio reinició permanecen en su proxy, ya que moverse a otra dirección para superarlo es rotación de identidad, que Octocrawl no hace. Un proxy que falló se aparta durante 10 minutos; una extracción toma el siguiente saludable. Una tarea reanudada en otra ruta (el grupo o el proxy cambiaron) inicia una nueva sesión de cookies; el modoauthednunca usa el grupo. Los registros nombran un proxy porhost:port(proxy), nunca sus credenciales. ConW2L_EGRESS_ECHO_URLestablecido a un servicio que responde con la dirección del llamante (https://ipinfo.io/json,https://api.ipify.org,https://httpbin.org/ip), cada proxy se le pregunta a través de sí mismo una vez, de nuevo después de que falló o después de 10 minutos, y cada página leída a través de ese proxy registra de dónde salió comoaccess.egress.exit({ ip, country, observedAt }, el país cuando el servicio da uno); sin él, o cuando el eco no respondió,exitesnull. La solicitud de eco va al servicio que nombras, a través de tu proxy, en una conexión propia: una puerta de enlace que da a cada conexión o sesión una nueva salida puede haber enviado la página desde otra dirección, por lo queexites de dónde se vio salir a ese proxy, no prueba de la dirección propia de la página. Una página de la caché mantiene la salida que su lectura original registró, y una página nunca espera el eco más allá de sutimeout(la salida es entoncesnull).vendor_unlock_htmlythird_party_captcha_solverpueden concederse, pero las rutas que los usan aún no están construidas (elementos 4 y 6 de PA de ROADMAP).- Los comandos
octocrawlleenW2L_ACCESS_GRANTtambién, ya que su motor se ejecuta en su propio proceso. - La concesión es parte de la clave de caché de página, por lo que una página obtenida bajo una concesión no se reutiliza bajo otra.
Una verificación que superas tú mismo (traspaso)
Por defecto, Octocrawl no resuelve captchas ni desafíos y no se disfraza (consulta la concesión de acceso anterior para ver qué cambia una concesión). Cuando una página de un lote se detiene en uno (blocked con captcha, cloudflare_challenge, bot_detected_generic o login_wall), Octocrawl en tu propia máquina puede entregártela en el Chrome que ya usas: octocrawl batch <urls> --handoff, POST /v1/batches/:id/handoff en un servidor local (octocrawl serve en loopback), o la herramienta MCP local hand_off_batch. La depuración remota debe estar activada, como para octocrawl login import, y Chrome pregunta "¿Permitir depuración remota?" una vez por cada entrega. Mientras la depuración remota esté activada, cada página que Chrome abre ve navigator.webdriver como true, ya sea que Octocrawl esté conectado o no (visto el 2026-10-07 en Chrome 153 con el interruptor chrome://inspect, y en Chrome 154 iniciado con --remote-debugging-port): un sitio que lo busca, como pueden hacerlo los controles de bots, toma tu Chrome por uno automatizado, y Chrome muestra "Chrome está siendo controlado por software de prueba automatizada". Octocrawl no lo oculta, ya que nunca cambia tu Chrome. Desactiva la depuración remota en chrome://inspect/#remote-debugging cuando termines.
Para una página, pídela en la solicitud: octocrawl scrape <url> --handoff, o "handoff": true (o { "waitMs": 60000 }) en POST /v1/scrape, el scrape del SDK y la herramienta MCP scrape. Cuando la propia búsqueda de Octocrawl se detiene en tal verificación, la página se abre en tu Chrome de la misma manera, y el raspado responde con la página a la que llegas. Esa respuesta se registra como se indica a continuación, y su auditoría de enrutamiento es la de la ejecución detenida. Una página que no se lee responde como detenida, con una advertencia handoff_not_through que dice por qué. El timeout del raspado limita la búsqueda de Octocrawl, no tu tiempo; el SDK espera la respuesta tanto como tarde la entrega. Sin la opción, una página detenida en un servidor que ofrece la entrega lleva handoff: { reason, liveViewUrl: null, rationale }, indicando cómo pedirla. Un servidor que no ofrece la entrega rechaza la opción (unsupported_parameter), como lo hace junto a actions o una captura de pantalla.
-
Mientras el lote espera. Se ejecuta hasta el final como de costumbre. El estado del lote cuenta los elementos detenidos en
waitingForPerson. Cada uno de esos elementos llevahandoff: { reason, liveViewUrl: null, rationale }, dondereasonescaptcha_required,bot_gateologin_required. Un límite de velocidad o un bloqueo regional no se entrega. -
Qué sucede cuando entregas. Cada página detenida se abre en una nueva pestaña de tu Chrome, una a la vez. Superas la verificación allí, como lo harías por tu cuenta. Octocrawl luego lee la página y cierra la pestaña. Una página cuenta como superada cuando, en tres lecturas con un segundo de diferencia, todo lo siguiente se cumple:
- se ha cargado y su documento respondió 2xx;
- la puerta de Octocrawl, dado el estado y los encabezados del propio documento, no encuentra ninguna verificación en él;
- está en el sitio solicitado (ese host, un subdominio o un dominio padre) y no en una ruta de inicio de sesión;
- no estás en un paso propio: sin campo de contraseña o código de un solo uso visible en la página, y sin campo de formulario cuyo valor estés cambiando (un cuadro de búsqueda que solo tiene el foco no cuenta).
La dirección de la página es la que Chrome muestra, no lo que dice el script de la página. Una página que se recarga a sí misma (un desafío que ejecuta un script y luego se recarga) se espera, no se toma como una pestaña cerrada. Una vía que termina en otro lugar del sitio (un inicio de sesión que aterriza en la página de inicio) es seguida por Octocrawl llevando la pestaña de vuelta a la página solicitada, como máximo dos veces; una pestaña que aún esté en otro lugar después de eso no se lee. La página solicitada es la URL en sí, o donde la URL lleva cuando Octocrawl devuelve la pestaña a ella, o esa página con su dirección reescrita por su propio script una vez que llegó, sin nuevo documento y antes de que hicieras clic o escribieras en ella (Indeed deja caer su token de paginación y nombra el trabajo que muestra): la misma ruta, el mismo valor para cada parámetro que ambas direcciones nombran, y ningún número omitido (un
start=10opage=2que desaparece puede significar que el sitio retrocedió a su primera página). Una dirección que tu propio clic o tecla movió (Siguiente, una ordenación) no es la página solicitada, ni tampoco otra página de una lista; la pestaña se devuelve.Octocrawl lee una página en tu Chrome solo después de que actúes en su pestaña: un clic o una pulsación de tecla que el propio Chrome cuenta como de un usuario (la activación de usuario del documento, leída en un mundo propio de Octocrawl al que el script de la página no puede acceder, o una navegación que Chrome marca como hecha con un gesto de usuario). Nada que la página haga por sí misma cuenta: ni una recarga, una redirección, una verificación que pasa por sí sola, o un script que llena un campo. Una página que no muestra verificación en tu Chrome (ya estás conectado allí, por ejemplo) se lee solo una vez que haces clic en ella;
octocrawl batch --handoffte lo dice, y hasta que lo hagas, el elemento mantiene su resultado detenido. Un clic cuenta solo en la pestaña que Octocrawl abrió: cuando esa pestaña permanece fuera de vista durante 3 s (otra pestaña o ventana delante de ella),octocrawl scrape --handoffyoctocrawl batch --handoffte dicen que cambies a ella. Así que un Permitir nunca permite que un llamador lea los sitios en los que estás conectado sin ti; el carril de mi navegador a continuación lee sin clic solo en los sitios que permitiste en la página propia de Octocrawl en tu Chrome. Para leer páginas con tu inicio de sesión y sin entrega, impórtalo para el sitio (octocrawl login import) y ejecuta el lote en modoauthed. -
Qué reemplaza el resultado detenido. Solo una lectura que sea la página (
successopartial) reemplaza el resultado detenido del elemento, en los formatos que el lote pidió, bajo el id propio del elemento; una lectura que aún muestra una verificación, o es un error o está vacía, deja el resultado detenido en pie. La página se registra como lo que es: carrilbrowser_local_authed, modoauthed,compliance: null(Octocrawl no envió nada, así que no firma nada),usage.requestCount: 0, el User-Agent desconocido (identity_unobserved: tu navegador envió la solicitud), el estado del documento yContent-Typecomo tu navegador los recibió, y la decisión de robots.txt de la búsqueda detenida. El rastro registra el resultado detenido enhandoff_fromy la lectura enuser_browser_read, con cómo actuaste (act:user_activationogesture_navigation) y la verificación que Octocrawl vio (sawGate); la auditoría de enrutamiento de la ejecución detenida se descarta con ella. -
Cuando una página no se lee. Puede que no llegues a tiempo (
waitMs, 10 s a 30 min, por defecto 10 min por página; una página dejada en otro sitio, o una en la que no hiciste clic, se abandona cuando ese tiempo termina), cierres la pestaña o salgas de Chrome; o el llamador puede irse (la conexión de la solicitud se cierra, Ctrl-C en la CLI), o Octocrawl puede apagarse; cada uno termina la espera y cierra la pestaña. Una página que Chrome se niega a abrir o responder no se lee, y las demás aún se entregan. Ese elemento mantiene su resultado detenido, y la respuesta dice por qué:{ id, handedOff, through, notThrough, items: [{ id, url, through, status, reason? }] }. -
Una lista que se detuvo en una verificación. Para un lote cuyo único paso es
paginatecon unitemSelector(los elementos son lo que distingue una página de la lista de otra página que abras en esa pestaña), la página en la que estaba la verificación se abre en tu Chrome; para un paginador cuyas páginas no tienen dirección propia, esa es la página propia de la lista, y las páginas leídas antes de la verificación se muestran de nuevo en el camino sin contar contramaxPages. La superas allí, luego paginas tú mismo haciendo clic en Siguiente: Octocrawl solo lee esa pestaña (un script de solo lectura; no hace clic en nada y no envía nada), y se detiene cuando Siguiente ha desaparecido, oculto o deshabilitado en la última página que leyó durante 5 s, en elmaxPagesdel paso, después de 60 s sin una página nueva, o en elwaitMsde la entrega (por defecto 10 minutos en todos); espera la palabra de la terminal o de la herramienta de que la página de la verificación se leyó antes de hacer clic en Siguiente, y no cierres la pestaña (eso, o salir de Chrome, abandona el elemento, páginas leídas incluidas); si superaste pero no mostraste ninguna página después de la verificación dentro de ese tiempo, el elemento mantiene su resultado detenido y una entrega posterior continúa desde allí. Las páginas que el propio navegador de Octocrawl leyó antes de la verificación y las páginas que le mostraste se fusionan en una sola lista, cada página una vez (actions.scrapes[].byesuser_browserpara las tuyas); el elemento se convierte en la lista completa,actions.lists[].continuedes{ from, pages, by: "user_browser" }, ystoppedBydice cómo terminó la lectura (end,max, odeadlinecon una advertencialist_not_exhausted). -
Dónde se ofrece. Solo en un servidor que se ejecuta en tu máquina y te responde solo a ti: un servidor que no está en loopback, y el servicio alojado, responden 409. Un lote que pidió la página
actionso unscreenshottampoco se entrega (409, sinhandoffen sus elementos): esos son para que los tome el navegador de Octocrawl, y una página leída en tu Chrome no puede darlos. Cancelar mientras Chrome aún pregunta "¿Permitir depuración remota?" corta la conexión, así que un Permitir hecho clic después no se adjunta a nada. La entrega se ejecuta en un lote terminado, no mientras se ejecuta. No envía ningún evento de webhook para un elemento que reemplaza: lee los elementos de nuevo. -
octocrawl serve(ynpm run api) lee inicios de sesión guardados solo cuando escucha en loopback, y entonces responde solo a solicitudes dirigidas a127.0.0.1,localhosto[::1]sinOriginextranjero, así que otra máquina o una página web que use un nombre DNS reasignado no puede leer páginas como tú. Escuchando en otra dirección, no lee ninguna y lo dice al iniciar. Un servidor alojado nunca las lee.
Tu propio Chrome (carril my-browser)
La depuración remota, que este carril necesita, hace que cada página vea navigator.webdriver como true mientras esté activada (ver la entrega anterior). La única excepción es un lote cuyo único paso es paginate con un itemSelector: un elemento cuya lista se detuvo en una verificación (actions.lists[].stoppedBy es challenge) se entrega como arriba; sus otros elementos detenidos no. Una página que solo necesita tu inicio de sesión, tu dirección o un navegador real se lee; un sitio cuya verificación de bots mira navigator.webdriver puede rechazar tu Chrome como rechaza los carriles propios de Octocrawl. En la ejecución de aceptación del 2026-10-07, studylib.net e imf.org se leyeron de esta manera, mientras que crunchbase.com (una página de bloqueo de Cloudflare) y stackoverflow.com (un desafío de Cloudflare que no se despejó) no; por qué esos dos se negaron no está aislado.
En un servidor que se ejecuta en tu máquina, un raspado puede leer su página en tu propio Chrome en lugar de buscarla: octocrawl scrape <url> --lane my-browser, o "lane": "my-browser" en POST /v1/scrape, el scrape del SDK y la herramienta MCP scrape. Octocrawl no busca nada por sí mismo; tu Chrome carga la página, conectado como tú donde estés.
- Lo permites dos veces por conexión. Chrome pregunta "¿Permitir depuración remota?" (depuración remota activada, como para
octocrawl login import). Octocrawl entonces abre una página propia en tu Chrome que lista el sitio y la tarea, y espera a que hagas clic en Permitir lectura de estos sitios allí. Solo ese clic, que Chrome cuenta como tuyo, permite el sitio: un script no puede. Cerrar esa página, o hacer clic en Revocar, lo detiene, y una página que se está leyendo termina comocancelled. No permitir el sitio a tiempo (10 minutos), cerrar la página o hacer clic en Revocar primero rechaza la solicitud (409), y el rechazo dice qué respondió la página por última vez (no se hizo clic, o se permitió sin un clic que Chrome contara). Un lote que espera que permitas sus sitios lo dice en su estado (waitingForApproval: true), y el registro del servidor muestra cada paso de la aprobación (my_browser_approval), nunca el contenido de una página. - La página se lee sin un clic, solo en ese host. Se lee cuando ha cargado, respondió 2xx, no muestra verificación y está en el host que permitiste, exactamente: una página que lleva a un subdominio o a un dominio padre del mismo no se lee sin ti, sin importar cómo el sitio los enlace. De lo contrario, se juzga como un traspaso juzga una página (tres lecturas seguidas). Su contenido se lee tal como se muestra en tu Chrome: lo que la página oculta con
display: noneovisibility: hidden, como un panel de ayuda o una pestaña no seleccionada, se omite; si muestra una verificación, y su HTML sin procesar, son el documento completo. Lo que una página dibuja en un lienzo, como una hoja de Lark dibuja sus celdas, no está en ella para leer. Una página que muestra una verificación espera a que la superes (handoff.waitMs, por defecto 10 min). Una página no leída respondecancelled,blockedcon la verificación que aún mostraba,failed/connection_errorcuando Chrome rechaza un comando (una pestaña que no abrirá),failed/redirect_limitcuando el sitio lleva la pestaña a otra página suya cada vez que Octocrawl la recupera (dos veces), ofailed/timeout, con una advertenciamy_browser_not_readque dice por qué. - Cómo se registra. Carril
my_browser, nunca en caché;compliance: nullyusage.requestCount: 0(Octocrawl no envió nada), sin decisión de robots.txt (no obtuvo nada), y elaccessdel Registro de Evidencia diceroute: user_browser, el navegador que lo leyó, ycompletion:user_browser, ohanded_to_personcuando la página mostró una verificación que superaste. - Dónde se ofrece. Como el traspaso: solo en un servidor en tu máquina, respondiéndote solo a ti; en otro lugar, y con la página
actions, unscreenshot,lockdowno un modo distinto al estándar, la solicitud se rechaza por nombre (unsupported_parameter). - Lotes.
"lane": "my-browser"enPOST /v1/batches, la herramienta MCPbatch_scrapeooctocrawl batch <urls> --lane my-browserlee cada página de esa manera, una a la vez. La página de Octocrawl en tu Chrome lista cada sitio del lote (host y puerto) y los permites una vez para la ejecución; una página en un sitio que no esté entre ellos no se lee, ni ninguna página después de que los revoques. No permitirlos responde a cada páginacancelled, y Chrome no alcanzado respondefailed/connection_error, cada uno con la razón. Una ejecución reanudada más tarde (un reinicio, una pausa) te pregunta de nuevo: un servidor reiniciado mientras tal lote se ejecutaba abre el aviso de Chrome al iniciar, y las páginas restantes cuando no respondes terminancancelled. Una URL añadida mientras el lote se ejecuta, en un sitio que no está en la lista de esa ejecución, terminacancelledtambién, y no se lee más tarde. Cancelar el lote, o que su presupuesto de tiempo termine, cierra la pestaña que se está leyendo y la conexión. Una página aquí no tienetimeoutpropio (tu tiempo es tuyo), solo el del lote. Un lote en este carril no toma webhooks (las páginas leídas mientras no estás no se envían a otro lugar) nimaxConcurrencysuperior a 1.
El access.completion de cada Registro de Evidencia cuenta cómo se leyó una página: unattended (los carriles propios de Octocrawl), authorized_session (tu inicio de sesión guardado, modo authed), user_browser (tu Chrome, en un sitio que permitiste, sin un paso tuyo) o handed_to_person (tu Chrome, después de superar una verificación). Es nulo cuando no se leyó ninguna página.
Para investigadores, dos guías recorren una ejecución real: De una lista de URL a un CSV con evidencia (la línea de comandos y el cliente Python, cada columna de evidencia, y por qué las filas fallidas permanecen) y Citar datos web en un artículo (una sección de métodos, una referencia con su fecha de acceso y hash, y datos personales).
Los puertos en los que escuchan los servicios locales, todos en 127.0.0.1:
| Puerto | Servicio | Iniciado por | Cambiado con |
|---|---|---|---|
| 8787 | La API REST, y la API que el servidor MCP stdio (octocrawl-mcp) y el cliente Python llaman por defecto (el SDK de TypeScript toma un baseUrl) | octocrawl serve, npm run api | --port, W2L_API_PORT; esos clientes W2L_API_URL |
| 8791 | El servicio MCP local (API, programador de Monitor, trabajador de entrega y endpoint MCP en /mcp) | npm run local:mcp, o el LaunchAgent de npm run local:mcp:install | W2L_LOCAL_MCP_PORT |
| 8788 | El receptor de webhooks HTTPS del tutorial de primer uso | npm run first-use:local | fijo |
| 8798 | La vista previa local del sitio público | npm run public:preview:local | W2L_PUBLIC_PREVIEW_PORT |
Para uso MCP hay tres formas, de menor a mayor configuración; las configuraciones para Cursor, OpenCode y Codex, y una primera tarea, están en Conectar MCP.
Alojado (raspar y mapear; sin clave dentro de un límite diario sobre HTTP, una clave para más páginas y el carril del navegador; ver docs/hosted-api.md):
claude mcp add --transport http octocrawl https://mcp.octocrawl.dev/mcp
curl -sS -X POST https://api.octocrawl.dev/v1/scrape -H 'content-type: application/json' -d '{"url":"https://example.com"}'
En tu computadora (todo: raspar, mapear, rastrear, lotes, la herramienta de producto Amazon.sg y las herramientas de Monitor; sin límite; nada de este checkout es necesario):
npx octocrawl serve # keep it running: the API on 127.0.0.1:8787
claude mcp add octocrawl -- npx -y @octocrawl/mcp # the published stdio server, a client of that API
Autoalojado para otros: npx octocrawl serve --hosted --token <token> escucha en todas las interfaces detrás de un token portador, con direcciones privadas, anulaciones de robots, inicios de sesión guardados, traspaso y webhooks no HTTPS rechazados; apunta @octocrawl/mcp a él con --base-url y --token (o W2L_API_URL y W2L_API_TOKEN).
El servicio local gestionado del checkout es para el flujo de entrega Monitor → HTTPS: un servicio en segundo plano ejecuta la API, el programador de Monitor, el trabajador de entrega y un endpoint MCP Streamable HTTP en http://127.0.0.1:8791/mcp. En macOS, instálalo como LaunchAgent y conecta Codex a su URL de loopback:
npm run local:mcp:install
codex mcp add w2l-local --url http://127.0.0.1:8791/mcp
npm run local:mcp:status
Se reinicia después de un fallo del proceso y al iniciar sesión. No se necesita alojamiento ni cuenta de inicio de sesión para esta ruta local. npm run local:mcp:uninstall elimina el agente; codex mcp remove w2l-local elimina la entrada del cliente. En otros sistemas, ejecuta npm run local:mcp en una terminal. El estado permanece en .w2l/api por defecto. Consulta el tutorial de primer uso de MCP para el flujo real de Monitor y entrega HTTPS y la configuración de secretos. Mantén este checkout mientras el LaunchAgent apunte a él.
Para recibir eventos firmados en el mismo Mac con una URL HTTPS de loopback fija, ejecuta npm run local:receiver:install, luego reinstala el servicio MCP con W2L_LOCAL_DELIVERY_LOOPBACK=1 npm run local:mcp:install. La opción solo permite la entrega de loopback y fija la confianza al certificado local generado. El receptor y su bandeja de entrada SQLite se ejecutan como un LaunchAgent separado; ningún servicio se vuelve accesible desde otra máquina.
La API REST independiente heredada sigue disponible para clientes SDK y Firecrawl-shim:
npm run api
Para conectar un proceso MCP stdio independiente a esa API, ejecuta:
npm run mcp
npm run api vincula 127.0.0.1 y permite loopback/RFC1918 para que los servidores de prueba funcionen. El modo alojado es explícito: npm run api -- --hosted --token $W2L_API_TOKEN. Eso vincula 0.0.0.0, requiere Authorization: Bearer, niega IPs privadas/de metadatos, y limita un rastreo a 100 páginas: un maxPages omitido o null toma 100, y uno más grande se rechaza con invalid_request. Obedece robots.txt para cada URL y no ofrece forma de evitarlo: robotsOverride, robotsOverrides y ignoreRobotsTxt se rechazan con unsupported_parameter (ver abajo).
Un servidor iniciado con tokens, alojado o local, acepta cualquiera de ellos: repite --token, o establece W2L_API_TOKEN y el W2L_API_TOKENS separado por comas. Los tokens en la línea de comandos reemplazan los del entorno. Un --token sin valor (el último argumento, seguido de otra bandera, o en blanco) detiene el servidor al inicio, y el error nunca repite un token. Da a cada cliente su propio token; reiniciar el servidor sin un token lo revoca. Los tokens se comparan como resúmenes SHA-256 de longitud fija en tiempo constante, y un token faltante o desconocido obtiene HTTP 401 con { "error": "unauthorized", "code": "unauthorized" }. El SDK envía su opción token, o W2L_API_TOKEN del entorno cuando no se pasa ninguna; token: '' no envía ninguna.
Un operador puede limitar cuántas solicitudes que inician trabajo puede hacer cada llamador: W2L_RATE_LIMIT_PER_MINUTE=<n> o --rate-limit-per-minute <n> (un entero de 1 a 100,000; sin establecer o vacío significa sin límite, cualquier otra cosa detiene el inicio con W2L_RATE_LIMIT_PER_MINUTE must be an integer between 1 and 100000) cuenta POST /v1/scrape, /v1/crawl, /v1/batches, /v1/map, /fc/v1/scrape, /fc/v1/crawl y /fc/v1/map en una ventana deslizante de 60 segundos por token portador (para el único llamador local cuando el servidor no toma token); las lecturas de estado son gratuitas. Sobre el límite, la respuesta es HTTP 429 con un encabezado Retry-After (segundos completos, al menos 1) y { "error": "rate limit exceeded: <n> requests per minute", "code": "rate_limited", "retryAfterSeconds": <s>, "agentHints": ["wait <s> s before the next request"] }; /fc responde { success: false, error, code: "rate_limited", agent_hints } con el mismo encabezado. rate_limited no es uno de los códigos de error de solicitud: la solicitud estaba bien formada, el presupuesto del llamador se gastó. El SDK lanza W2LError con status 429, code rate_limited, retryAfterMs leído del encabezado (delta-seconds o una fecha HTTP) y agentHints, y no reintenta nada, como los SDK de Firecrawl no lo hacen; las llamadas a herramientas MCP fallan con rate limited: retry after <s> s (rate_limited). La ventana está en memoria y por proceso, por lo que un reinicio la restablece, y se basa en el resumen del token, no en la dirección del cliente, por lo que los tokens rotados tienen presupuestos separados. Nada sobre la cortesía saliente cambia: la puerta por origen y los enfriamientos Retry-After hacia los sitios permanecen como están.
Detrás de un proxy, el modo local (npm run api, el servicio MCP local, npm run scrape/crawl) envía sus solicitudes salientes, incluidos robots.txt y el navegador local, a través de HTTPS_PROXY para URLs https: y HTTP_PROXY para URLs http: (nombres en minúsculas también), con las reglas de curl: hosts NO_PROXY y sus subdominios, host:port, entradas IP y CIDR van directas, * desactiva el proxy, y loopback siempre es directo. El proxy debe ser http:// o https://, y ambas variables deben nombrar el mismo. El proxy resuelve los nombres que obtiene, por lo que para solicitudes proxy, Octocrawl confía en él para la resolución y verifica solo la URL en sí (esquema, credenciales, literales IP, nombres de metadatos); las solicitudes directas aún se resuelven, validan y fijan. Los resultados nombran el host:port del proxy en evidence.envProxy y un evento de rastreo egress_proxy, nunca sus credenciales. W2L_PROXY=off ignora las variables; el modo alojado nunca las usa. Sin el proxy del entorno, el navegador local se conecta directamente como el carril HTTP: nunca recurre a la configuración de proxy del sistema operativo, una ruta que ningún resultado registraría. El LaunchAgent de macOS no hereda tu shell, así que pon estas variables en .w2l/local-mcp.env. Octocrawl verifica certificados por defecto, también a través del proxy (el proxy tunela TLS de extremo a extremo); skipTlsVerification lo desactiva para una solicitud local, se registra, y se rechaza en modo alojado (ver las opciones de raspado abajo).
robots.txt se lee para cada URL que Octocrawl obtiene, y su veredicto se registra; lo que decide depende de quién eligió la URL (decidido 2026-10-05). robots.txt se dirige a los rastreadores que descubren enlaces, así que en un servidor local una URL que la solicitud nombra (un scrape, una entrada de lote, la lista de URLs de la CLI, MCP scrape y batch_scrape, /fc/v1/scrape) se obtiene sin importar lo que diga robots.txt, como lo haría una visita de navegador: el resultado conserva el veredicto (robotsDecision.decision: "disallowed", userOverride: true, overrideBasis: "user_named_url"), una advertencia robots_overridden y los eventos de rastreo a continuación. Los enlaces que descubre un crawl o un mapa obedecen robots.txt, a menos que el crawl o el mapa se hayan iniciado con ignoreRobotsTxt en un servidor local (overrideBasis: "ignore_robots_txt"; un crawl entonces también lee los archivos de sitemap que robots.txt desautoriza, y un mapa devuelve las URLs que desautoriza, diciendo el robots de cada enlace disallowed o unreachable). Las relecturas programadas de un Monitor también lo obedecen. Un servidor alojado obedece robots.txt para cada URL, ya que obtiene desde las direcciones del operador. El propietario de un sitio puede dirigirse a Octocrawl mismo: las líneas User-agent de robots.txt se comparan con el User-Agent de la solicitud con el token de producto Octocrawl añadido, en todos los modos, así que un grupo para Octocrawl (o el w2l-research del modo de investigación) lo gobierna sea cual sea la cabecera enviada. Tal regla es la exclusión selectiva del propietario: una URL nombrada y ignoreRobotsTxt no la dejan de lado, y solo un robotsOverride con tu razón registrada lo hace, en un servidor local. El ritmo no cambia por nada de esto: un lote y un crawl espacian las páginas de un host según su Crawl-delay, permita robots.txt la página o no, y un 429 enfría el host para cada solicitud. Un robots.txt 4xx significa sin restricciones. Un robots.txt que no se puede obtener (un 5xx, un error de red o sin respuesta en 5 segundos) es una desautorización completa, como exige RFC 9309 §2.3.1.4: donde se obedece robots.txt la página no se obtiene, el resultado es failed con policy_denied, y los eventos de rastreo robots_checked y robots_disallowed y la decisión de robots del registro de cumplimiento (carriles de navegador y proveedor) llevan unreachable: "server_error", "network_error" o "timeout", para que nunca parezca una regla que el editor escribió. Octocrawl pide ese robots.txt de nuevo después de cinco minutos (robotsUnreachableTtlMs en la política de red). En una conexión directa, un nombre que no se resuelve sigue siendo dns_error; a través del proxy del entorno, que resuelve nombres él mismo, su solicitud de robots.txt falla primero, así que la página es policy_denied con unreachable: "network_error". Donde una URL se obtiene a pesar de eso (una URL nombrada, ignoreRobotsTxt, robotsOverride), la razón inalcanzable permanece en el rastreo y la advertencia robots_overridden dice que el archivo no se pudo leer. En un servidor local, un scrape o una entrada de lote también pueden llevar tu propia razón registrada (robotsOverride, descrita con las opciones de scrape a continuación); ignoreRobotsTxt en un scrape o lote se rechaza con HTTP 400 nombrándolo.
El modo de investigación (mode: "research", --mode research en la línea de comandos) declara a Octocrawl como un bot en su User-Agent. Establece W2L_CONTACT para decir quién lo ejecuta, como un nombre y dirección de correo o una URL, por ejemplo W2L_CONTACT="Jane Doe jane@example.org": ASCII imprimible, como máximo 200 caracteres, sin paréntesis ni barras invertidas. El User-Agent de investigación entonces termina en ; contact: Jane Doe jane@example.org). Para sec.gov y sus subdominios toma el formato que prescribe la política de acceso justo de SEC, <Company or name> <email>, en su lugar: W2L Research Jane Doe jane@example.org (así que da una dirección de correo). Su robots.txt se solicita también con él, un grupo de robots.txt para w2l-research sigue aplicándose allí, y el identity.contact del Registro de Evidencia lee el contacto de cualquiera de los formatos. Los resultados conservan el User-Agent que se envió (el evento de rastreo identity_sent en el carril HTTP, el sentHeaders del registro de cumplimiento en el carril de navegador). El modo estándar envía un User-Agent de navegador y no declara contacto. SEC.gov responde 403 a clientes automatizados que no declaran contacto; en el carril HTTP, tal 403 de un host de SEC lleva un evento de rastreo declared_contact_hint que dice usar mode: "research" con W2L_CONTACT establecido. npm run api, el servicio MCP local (en .w2l/local-mcp.env) y npm run scrape/crawl leen la variable.
El MCP local unificado cubre scrape, mapa, Crawl, lotes persistentes de arrays de URLs y
Monitor/Delivery sin terminales de trabajo separadas. Un servicio unificado también
implementa Streamable HTTP autenticado para el Monitor de documentos públicos revisado y
los flujos anónimos de JSON/lotes de productos de Amazon.sg (experimental; su configuración está
archivada en docs/archive/hosted-mcp-pilot.md); el alojamiento está en pausa en la hoja de ruta
(ROADMAP.md). Para ambos flujos en un Mac, ejecuta
npm run first-use:local después de npm ci; consulta la
guía de primer uso de dos flujos,
recorrido de primer uso de MCP y
estado C2/C3. Los clientes avanzados pueden
aún lanzar el adaptador stdio heredado desde este repositorio:
{
"mcpServers": {
"w2l": {
"command": "npm",
"args": ["run", "mcp"],
"env": { "W2L_API_URL": "http://127.0.0.1:8787" }
}
}
}
El MCP scrape es compacto por defecto: devuelve el contenido seleccionado, metadatos de documento/producto, uso agregado y errores sin repetir el cuerpo bajo summary.attempts, con el estado de la respuesta y la cabecera content-type en snapshot (httpStatus, contentType) y la URL final y cada salto de redirección en evidenceRecord. Pasa debug: true cuando necesites la ruta completa, el rastreo y la auditoría por intento. Las llamadas REST y SDK que omiten formats y debug mantienen la respuesta Markdown completa heredada.
Para muchas URLs conocidas, usa batch_scrape en MCP, luego get_batch, get_batch_items o wait_batch. REST y SDK soportan la misma tarea duradera y elementos paginados. REST transmite un crawl o un lote mientras se ejecuta (GET /v1/crawl/:id/events y GET /v1/batches/:id/events como eventos enviados por el servidor, /ws en cada uno para un WebSocket), y el watcher(jobId, { kind }) del SDK sigue un trabajo a través de esas rutas o, donde están apagadas, mediante sondeo (el párrafo del observador a continuación). Consulta scraping por lotes. El límite de concurrencia por origen es configurable hasta cuatro, con un enfriamiento compartido de Retry-After y un intervalo mínimo de solicitud. La comparación controlada 1/2/4 es evidencia de fixture local, no una afirmación de velocidad de Amazon. Un lote también toma maxConcurrency (un entero de 1 a 4): el máximo de sus páginas en vuelo a la vez, en todos sus hosts. Solo baja el recuento de trabajadores del servicio (4 localmente, 2 en el host MCP alojado; GET /v1/batches/:id informa el límite en vigor como maxConcurrency), el límite por host y el intervalo mínimo siguen aplicándose, y el límite se almacena con la tarea, así que un lote reanudado tras un reinicio se ejecuta bajo él. ignoreInvalidURLs: true inicia el lote con las entradas de urls que son URLs http(s) e informa el resto como invalidURLs en el 202 y en el estado, donde sin él una de esas entradas rechaza toda la solicitud por su índice (urls[2] must be http(s)); una entrada que no es una cadena, o un duplicado, se rechaza de cualquier manera. Los indicadores de alcance de extracción de Firecrawl allowExternalLinks y includeSubdomains se toman en un lote como false solamente, que ya se mantiene (un lote obtiene las URLs dadas y no sigue ningún enlace); true es HTTP 400 nombrando la opción de crawl que lo hace (allowExternalLinks: true is not offered on a batch: a batch fetches only the URLs given; a crawl takes allowExternalLinks, and extraction across links is the M5 multi-URL extract; includeSubdomains apunta al allowSubdomains del crawl). GET /v1/batches/:id informa succeeded (elementos registrados success, partial o empty_verified) y failed (los elementos que listan los informes de errores) junto a completed. GET /v1/batches/:id/errors (SDK getBatchErrors, MCP get_batch_errors) lista los elementos que no tuvieron éxito en cada intento del lote, así que un lote interrumpido y reanudado mantiene sus fallos anteriores en el registro, cada uno como { id, timestamp, url, status, code, error, httpStatus } (los nombres de Firecrawl; code es el failureReason, blockReason o budgetExceeded del elemento), en páginas de hasta 1000, con robotsBlocked, las URLs que robots.txt rechazó: un elemento policy_denied con un evento de rastreo robots_disallowed que ninguna anulación registrada dejó de lado. Una negativa de gobernanza o SSRF es policy_denied también, pero no robots.txt, y permanece solo en errors. Un lote también toma idempotencyKey (1 a 200 caracteres sin caracteres de control; también la cabecera x-idempotency-key o Idempotency-Key que envían los clientes de Firecrawl, en POST /v1/batches, POST /v1/crawl y /fc/v1/crawl; un inicio de crawl toma el mismo campo): una sumisión enviada de nuevo con la misma clave y el mismo cuerpo responde al taskId de la primera sumisión con replayed: true y no inicia nada, así que un reintento tras una conexión caída no obtiene cada URL dos veces; la misma clave con otro cuerpo es HTTP 409 conflict (idempotency key was used for a different request), una clave de cuerpo que difiere de la cabecera es HTTP 400, y una clave vive 24 horas, en un índice junto a los directorios de tareas (<task root>/idempotency.sqlite) mantenido por el proceso API único que ejecuta esa raíz de tareas. appendToId: "<taskId>" añade urls a un lote existente en lugar de iniciar un nuevo trabajo (SDK appendToBatch(id, urls, options), MCP batch_scrape con appendToId): el trabajo conserva su mode, formats, includeLinks, maxConcurrency y opciones de página (enviar uno es HTTP 400 appendToId keeps the job's options; formats cannot be changed; ignoreInvalidURLs, idempotencyKey y robotsOverrides para las nuevas URLs pueden venir), las URLs van al final de su lista (como máximo 1000 en total; una URL ya en el lote se rechaza por nombre, appended url is already in the batch: <url>; una URL en un host nuevo se obtiene como las demás, bajo sus propias comprobaciones de robots.txt y SSRF), un lote en ejecución las recoge en el mismo intento (no se añade ningún trabajo, así que un límite de lote activo no cuenta la anexión), uno completado se ejecuta de nuevo para ellas en un nuevo intento (activo de nuevo, cuenta contra tal límite como lo haría un lote nuevo: en un servidor que ejecuta un lote a la vez, la anexión es HTTP 400 active batch limit reached mientras otro lote está activo, y el lote se deja como estaba), y uno cancelado o fallido es HTTP 409 conflict (batch is cancelled); el 202 lleva requested (las URLs del trabajo ahora) y appended, y GET /v1/batches/:id cuenta la lista más larga. Para una lista de más de 1000 URLs, el batchScrapeChunked(urls, options, { chunkSize, itemLimit, pollIntervalMs, timeoutMs }) del SDK ejecuta un lote por fragmento de chunkSize URLs (por defecto 100), cada uno iniciado, esperado y listado antes de que el siguiente comience (un idempotencyKey del llamante se convierte en <key>:<chunk index> por trabajo), y devuelve { jobs, items, invalidURLs } con los elementos en el orden en que se enviaron las URLs; chunkUrls(urls, chunkSize) divide una lista sola. El batch_scrape del host MCP alojado rechaza idempotencyKey y appendToId como rechaza las otras opciones de lote.
Un crawl (POST /v1/crawl, MCP crawl) toma el mismo formats y includeLinks que scrape, más includePaths / excludePaths: expresiones regulares comparadas contra la ruta de URL de cada enlace descubierto. La URL de inicio siempre se obtiene y una coincidencia excludePaths gana. Un patrón que puede hacer retroceso catastrófico en una ruta de enlace manipulada, como ^/(a+)+$ o .*a.*b, se rechaza con invalid_request: un grupo repetido con una parte repetida y sin separador, una elección repetida cuyas alternativas pueden comenzar igual, o tres o más partes repetidas superpuestas en fila. El resto se ejecuta en el motor de expresiones regulares de tiempo lineal de V8 donde puede ejecutarlas; uno con una mirada alrededor, una referencia inversa o una repetición contada por encima de 16 (como {3,40}) se ejecuta en el motor de retroceso, solo en rutas de hasta 2,048 caracteres y con un límite de 100 ms por enlace. Un enlace que un filtro no puede decidir no se sigue, y un filtro que se quedó sin tiempo no decide ningún enlace posterior tampoco. Scrape, lote y crawl rechazan un campo desconocido o un formato no soportado con HTTP 400 nombrándolo.
Un rastreo sigue enlaces en el host de la URL inicial, el gemelo www. de ese host (example.com y www.example.com) y el host al que redirige la URL inicial, y solo dentro del subárbol de ruta de la URL inicial: una URL inicial que termina en / limita el rastreo a ese directorio, una que nombra un archivo (/3/tutorial/index.html) al directorio del archivo, cualquier otra a sí misma y a las rutas bajo ella (/search admite /search?page=2 y /search/x, no /searching); una redirección de la URL inicial añade el subárbol de la URL final. crawlEntireDomain: true sigue enlaces en cualquier parte de ese host, como hacían los rastreos antes de que existiera la opción. allowSubdomains: true añade cada host bajo el ápice de la URL inicial (el host con un www. inicial eliminado; no hay lista de sufijos públicos, por lo que una URL inicial en www.gov.uk admite cada host *.gov.uk), allowExternalLinks: true añade cada host y no puede combinarse con allowlistedDomains, y un allowlistedDomains no vacío añade los hosts que nombra (exactos o *.domain) al propio de la URL inicial. Los hosts distintos del de la URL inicial no están limitados por ruta, y cada página en un host nuevo recibe su propia lectura de robots.txt, verificación de SSRF y registro de identidad en el carril que la sirva. La gobernanza sigue la misma regla: con allowlistedDomains la escalera puede obtener el host de la URL inicial, su gemelo, los hosts listados y *.apex bajo allowSubdomains; de lo contrario, solo la frontera limita el rastreo. Un rastreo no sigue enlaces cuya ruta termine en una extensión de imagen, fuente, hoja de estilo, script, audio, video o programa (.png, .woff2, .css, .js, .mp4, .exe y similares); se siguen documentos y archivos de datos como PDF, CSV, XLSX, JSON, XML y ZIP.
includePaths / excludePaths coinciden con el nombre de ruta de un enlace; con regexOnFullURL: true coinciden con su URL canónica (scheme://host/path?query, con el host en minúsculas, el puerto predeterminado y los parámetros de seguimiento eliminados y la consulta ordenada), por lo que un patrón puede nombrar un host o una consulta. ignoreQueryParameters: true trata las URLs que difieren solo en su cadena de consulta como una sola página: se obtiene la primera variante vista (su url conserva la consulta, su canonicalUrl no) y las posteriores se informan como colapsadas. deduplicateSimilarURLs (predeterminado true) hace lo mismo para /a y /a/, / y /index.html (también .htm, .php), www. y el ápice, y http y https; el canonicalUrl de una página sigue siendo una URL real, y un sitio que sirve páginas diferentes en /a y /a/ necesita la opción desactivada. Una página obtenida y luego encontrada para repetir el cuerpo de una página anterior (el mismo rawBodySha256) mantiene el estado duplicate en el punto de control, se excluye de GET /v1/crawl/:id/pages a menos que includeDuplicates=true (MCP get_crawl_pages, SDK getCrawlPages) y de un estado de rastreo /fc's data, y aún cuenta en el total de ese estado. Cada rastreo informa qué fue de los enlaces que encontró: GET /v1/crawl/:id lleva discovery (offered, enqueued, duplicate, collapsed, hostDenied, subtreeDenied, pathDenied, depthDenied, duplicateContent; null para un lote), escrito después de cada página, y el rastreo de cada página (pages?debug=true) lleva un evento discovered (via: seed o link, from: la página que enlaza) y un evento links_offered con los contadores de esa página y hasta 20 enlaces colapsados (url, into) y rechazados por el host como muestras. Una tarea de rastreo almacena todas estas opciones; una almacenada antes de que existieran se reanuda con su regla original, todo el host y URLs canónicas exactas. El CLI octocrawl crawl aún no tiene banderas para estas opciones: sigue siguiendo todo el host (crawlEntireDomain) y toma los otros valores predeterminados.
Un rastreo lee el sitemap del sitio por defecto (sitemap: "include", como hace Firecrawl): los archivos que el robots.txt de la URL inicial nombra en líneas Sitemap:, o /sitemap.xml cuando no nombra ninguno. Un <sitemapindex> se sigue un nivel, sus hijos en orden listado; un archivo .gz, o uno cuyos bytes comiencen con el número mágico gzip, se infla bajo el límite de descompresión de 50 MiB; se leen como máximo 20 archivos por intento, y la carga se detiene una vez que contiene tantas entradas como maxPages (50 000 cuando el rastreo es ilimitado). Las entradas van a la frontera en profundidad 1, después de la URL inicial y antes de sus enlaces, bajo las mismas reglas de host, subárbol, includePaths / excludePaths y maxDepth que un enlace (por lo que maxDepth: 0 las elimina todas), por lo que un rastreo limitado de un sitio con sitemap devuelve páginas diferentes que antes de la opción. sitemap: "only" no sigue ningún enlace de página: las páginas son la URL inicial y las entradas del sitemap, y los links de cada página aún se devuelven cuando se solicitan. sitemap: "skip" no lee ninguno. Un archivo de sitemap es una obtención auxiliar como robots.txt, nunca una página: se solicita con la identidad http propia del modo de rastreo (su User-Agent y sugerencias de cliente, las móviles cuando las páginas del rastreo las declaran, y nada que un llamador haya añadido), después de la verificación de SSRF en su URL y cada salto de redirección, a través de la misma ruta fijada por DNS o proxy del operador, con el ritmo del programador de origen, dentro del límite de redirección de la política y el límite de cable de 10 MiB, y solo después de que su propia URL pasó el robots.txt de su host bajo esa identidad (un robots.txt no permitido o inalcanzable lo rechaza, como lo haría con una página, a menos que el rastreo se haya iniciado con ignoreRobotsTxt). Nunca pasa por la escalera o el extractor, y no tiene registro de cumplimiento firmado: el registro de estas obtenciones es el discovery.sitemap del informe (mode, sources: robots o guess, files, listed, enqueued, truncated, error), donde cada archivo lleva su url, finalUrl, status, contentType, bytes, sha256, kind (index, urlset, absent para un 4xx, not_sitemap para un cuerpo 2xx que no es ninguno, unreadable con la razón, como body_too_large o decompressed_too_large, o refused), entries, veredicto robots y proxyUsed. Un 4xx o un cuerpo HTML nunca es un error; un hijo ilegible se registra y la carga pasa al siguiente. Las obtenciones de sitemap se espacian por el intervalo mínimo del programador; el Crawl-delay de robots.txt gobierna los inicios de página del rastreo desde la primera página en adelante. Una página encontrada a través del sitemap lleva discovered { via: "sitemap", from: <the file's URL> } en su rastreo, y las entradas del sitemap cuentan en discovery junto a los enlaces. El CLI octocrawl crawl aún no lee sitemap, y una tarea de rastreo almacenada antes de la opción se reanuda sin uno.
maxConcurrency limita las páginas que un rastreo obtiene a la vez: un entero de 1 al número de trabajadores del servicio (W2L_WORKER_COUNT, 4 por defecto en una API local; 2 en el host MCP alojado; un valor mayor se rechaza con maxConcurrency must be at most N on this service). Solo reduce el paralelismo de un rastreo: el límite por origen (W2L_PER_HOST_CONCURRENCY, como máximo 4) y el intervalo mínimo aún se aplican, por lo que nunca eleva el límite del host. La evidencia de la configuración está en las propias páginas: el createdAt y el usage.wallMs de cada página dan su intervalo de obtención. Una reanudación conserva el valor.
GET /v1/crawl/active (SDK getActiveCrawls(), MCP list_active_crawls, solo local) lista los rastreos que este proceso de API está ejecutando, los que inició y los que reanudó al arrancar, primero el inicio más antiguo: { crawls: [{ id, url, status, startedAt, pagesFetched, options }] }, donde options son las opciones almacenadas del rastreo (maxPages, maxDepth, allowlistedDomains, includePaths, excludePaths, useCached, sitemap, las opciones de alcance de URL, maxConcurrency y scrapeOptions, las opciones por página con formats y includeLinks). Siempre es 200, { "crawls": [] } cuando no se ejecuta nada; un lote nunca se lista (tiene GET /v1/batches/:id), y no hay id de equipo, ya que Octocrawl no tiene equipos. La lista son los rastreos propios de este proceso: un rastreo que otro proceso ejecuta en la misma raíz de tarea no está en ella.
Un rastreo o lote toma webhook (REST, SDK, MCP crawl y batch_scrape): donde el trabajo publica sus eventos como entregas duraderas y reintentadas, una cadena URL o { url, headers, metadata, events, secretEnv }. Cinco eventos: started (secuencia 0) cuando el trabajo es aceptado, un page por página registrada (cada resultado: éxito, parcial, fallido, bloqueado, duplicado; el page del payload es la página como GET /v1/crawl/:id/pages o /v1/batches/:id/items la lista, traza vacía, sin auditoría, su json incluido cuando se solicitó un formato json), luego completed, failed o cancelled con el estado del trabajo como GET /v1/crawl/:id o GET /v1/batches/:id lo reporta entonces (report, y error en failed). events los reduce (por defecto los cinco; un evento filtrado nunca se encola, por lo que nada pendiente o en letra muerta aparece para él; un trabajo cancelado es cancelled, nunca failed). Cada payload es { schemaVersion: "w2l.job-event/v1", eventId, sequence, jobId, jobKind: "crawl" | "batch", event, at, metadata, page?, report?, error? }: eventId es <taskId>:started, <taskId>:page:<stepId> o <taskId>:<status> (un trabajo que se ejecuta de nuevo, un rastreo reanudado o un lote anexado tras completarse, sufija su evento terminal posterior con el id de intento), o <taskId>:handoff:<stepId> para un elemento de lote que un traspaso reemplazó con la página a la que la persona llegó, en un lote registrado antes de 2026-10-05 (un lote con webhook ya no se entrega: una página leída en el Chrome de la persona se lee con sesión iniciada como ella), y sequence cuenta los eventos del trabajo en orden, 0 y luego uno por página, evento terminal y de traspaso, por lo que un trabajo de n páginas termina en n+1 y cada elemento entregado después añade uno; metadata es la solicitud (como máximo 32 cadenas de hasta 1000 caracteres, 8 KiB en total), {} cuando no hay ninguna, copiada en cada payload cuando el evento se encola, por lo que un reintento reenvía el cuerpo idéntico. Cada solicitud lleva content-type: application/json, x-w2l-event-id, x-w2l-event-version (la secuencia) y x-w2l-delivery-id, más x-w2l-timestamp y x-w2l-signature (sha256= HMAC sobre <timestamp>.<body>) cuando secretEnv nombra una variable de operador W2L_WEBHOOK_SECRET_* (nunca un secreto literal), y el headers de la solicitud (como máximo 32, 8 KiB en total, nombres de tokens RFC 7230 en minúsculas, sin saltos de línea; content-type, content-length, host, connection, transfer-encoding y cada nombre x-w2l-* son rechazados por nombre, webhook.headers: content-length is reserved), enviado en cada intento, reintentos incluidos, después de los propios de Octocrawl, que nunca sobrescriben. Los valores de cabecera se almacenan en la base de datos de control (section-b-control.sqlite, modo 0600) únicamente: la tarea, el estado y cada ruta de entrega muestran solo sus nombres (headerNames), por lo que un token portador para el receptor pertenece allí y en ninguna parte de una respuesta; secretEnv sigue siendo la ruta de firma recomendada. Un evento terminal se encola solo después de que la fila de la tarea se escriba como terminal, por lo que una entrega completed llega después de que GET /v1/crawl/:id ya diga completed. El destino es job:<taskId> (GET /v1/delivery/destinations?jobId=<taskId>); GET /v1/deliveries?jobId=<taskId> y GET /v1/deliveries/page?jobId= listan las entregas, cada una con sus intentos en GET /v1/deliveries/:id, reintentados con retroceso y Retry-After y en letra muerta después del presupuesto de intentos como los de un Monitor (POST /v1/deliveries/:id/retry reproduce uno), y el estado del trabajo reporta webhook: { destinationId, url (origin and path, no query), events, pending, delivered, deadLetter }. Los ids de evento son deterministas y una entrega es única por evento, por lo que una reanudación o un reinicio ofrece cada página persistida de nuevo y no envía ninguna dos veces, y un trabajo terminado cuyos eventos un fallo cortó se completa cuando la API inicia, un elemento de lote que un traspaso reemplazó incluido; un evento ofrecido de nuevo toma el número que habría tenido, o, cuando otro evento tiene ese número, el siguiente después de cada número enviado. El receptor debe ser https; un servidor local (npm run api, el servicio MCP local) también acepta http plano a un receptor de bucle local (127.0.0.1, ::1, localhost, enviado directo sobre node:http), como se permiten servidores de prueba, y rechaza cualquier otra URL http con HTTP 400 webhook.url must be https (http is accepted only for a loopback receiver of a local service); un servidor alojado (--hosted) rechaza cada receptor http de la misma manera y una dirección privada o de metadatos con webhook.url must be a public address, y deja en letra muerta un nombre que resuelve privadamente (webhook egress denied: <violation>). w2l-api los entrega él mismo: ejecuta el trabajador de entrega que el runtime MCP ejecuta, bajo la política de entrega que imprime al inicio (TLS siempre verificado, el HTTPS_PROXY del shell nunca usado, W2L_DELIVERY_PROXY_URL para un proxy explícito, W2L_DELIVERY_CA_FILE confiable, W2L_DELIVERY_PRIVATE_ALLOWLIST para receptores https privados en modo alojado), por lo que npm run delivery:worker ya no es necesario junto a él, y un segundo trabajador en la misma base de datos de control es seguro, ya que una entrega se arrienda y se cerca. El host MCP alojado batch_scrape rechaza webhook (unsupported remote tool option) y no ofrece rastreo. En /fc/v1/crawl el webhook de Firecrawl (una cadena o { url, headers, metadata, events }) se mapea a la opción nativa y su receptor obtiene la forma de Firecrawl, { success, type: "crawl.started" | "crawl.page" | "crawl.completed" | "crawl.failed", id, data: [page], metadata, error? } (un rastreo cancelado es crawl.failed con error: "cancelled"), la identidad de evento de Octocrawl en las cabeceras (docs/firecrawl-shim.md). Una entrega es contabilidad sobre el trabajo: nada sobre la obtención, traza o registro de cumplimiento de una página cambia con un webhook.
El SDK sigue los cursores de un listado por ti: listCrawlPages(id, options) y listBatchItems(id, options) toman, junto a limit, attemptId, debug y includeDuplicates, los límites maxPages (páginas leídas después de la primera), maxResults (elementos en total) y maxWaitMs (ninguna página adicional después de ese tiempo), y se detienen silenciosamente cuando uno se alcanza; el valor de retorno del generador dice dónde (nextCursor, stoppedBy: end, maxPages, maxResults o maxWait). Con maxResults cada página se solicita no más grande de lo que aún se quiere, por lo que el cursor donde el listado se detiene continúa exactamente después del último elemento devuelto. getCrawlDocuments(id, options) devuelve { report, pages, nextCursor, stoppedBy } y getBatchDocuments(id, options) { report, items, nextCursor, stoppedBy }, el estado y los documentos en una respuesta, cada documento a menos que un límite detenga el listado; collectCrawlPages y collectBatchItems devuelven solo los documentos. Un listado es del último intento a menos que attemptId nombre otro (una reanudación con useCached re-registra las páginas que reutiliza en su nuevo intento, por lo que ese intento normalmente tiene cada página). Los tamaños de página del servidor no cambian: páginas de rastreo de 1 a 1 000 por solicitud (por defecto 50), elementos de lote como máximo 50. MCP get_crawl_pages y get_batch_items toman maxResults (1 a 200) y luego siguen los cursores ellos mismos, respondiendo { items, nextCursor, hasMore, stoppedBy }; get_crawl_pages también toma includeDuplicates. El estado de rastreo del shim /fc aún pagina con next solo.
Una tarea de rastreo almacena cada opción con la que se inició. Un rastreo pausado por un apagado o dejado corriendo por un fallo se reanuda cuando la API inicia de nuevo, POST /v1/crawl/:id/resume (SDK resumeCrawl, MCP resume_crawl) reinicia uno pausado o fallido, y octocrawl crawl --resume <taskId> continúa uno desde la línea de comandos; los tres se ejecutan con las opciones almacenadas. Una reanudación vuelve a obtener las páginas que el rastreo ya tiene, o las reutiliza cuando el rastreo se inició con useCached: true (solo las páginas propias del rastreo; maxAge reutiliza la caché entre solicitudes). maxPages cuenta las páginas distintas de la tarea a través de reanudaciones, por lo que un rastreo reanudado nunca lo excede. GET /v1/crawl/:id cuenta páginas mientras el rastreo corre; los elementos /v1/crawl/:id/pages llevan la auditoría de enrutamiento y la traza solo con debug=true, como los elementos de lote. Entre dos inicios de página en un host, el rastreo espera el robots.txt de ese host Crawl-delay o el intervalo mínimo (W2L_PER_HOST_MIN_DELAY_MS), el que sea más largo, y hasta que una página en un host haya reportado su robots.txt, el rastreo inicia una página a la vez allí. Cada página obtenida registra la espera en un evento de traza crawl_delay: startedAt, previousStartedAt, observedDelayMs, requiredDelayMs y robotsCrawlDelayMs.
Para esperar un rastreo o un lote desde el SDK, waitCrawl(taskId) y waitBatch(taskId) lo consultan hasta que esté completado, fallido o cancelado (una tarea pausada aún se espera) y devuelven su estado; crawlAndWait(url, options, wait) y batchAndWait(urls, options, wait) inician la tarea, esperan, y también devuelven cada página y error del rastreo, o cada elemento del lote. Las opciones de espera son pollIntervalMs (por defecto 500), timeoutMs (sin límite por defecto), maxRetries (por defecto 5) y signal. Cuando timeoutMs se agota, una solicitud de estado aún en vuelo incluida, la espera lanza WaitTimeoutError con taskId, timeoutMs y last, el último estado leído (null cuando ninguno respondió a tiempo), y la tarea sigue corriendo. Una solicitud de estado que falla con un error de red, HTTP 408, 429 o 5xx se reintenta después de 1, 2, 4, 8, luego 10 s, o después de su Retry-After cuando eso pide 60 s o menos; la espera lanza cualquier otro error de inmediato, y uno transitorio una vez que maxRetries reintentos consecutivos han fallado.
Para seguir un trabajo mientras se ejecuta en lugar de esperarlo, GET /v1/crawl/:id/events y GET /v1/batches/:id/events lo transmiten como eventos enviados por el servidor: catchup con el informe al abrirse la transmisión, un document por página registrada, sea cual sea su resultado (la lista compacta de páginas GET /v1/crawl/:id/pages o /v1/batches/:id/items, rastreo vacío, sin auditoría, cada intento del trabajo, cada paso una vez; su id: es el cursor de paso que toman las rutas de listado), un snapshot con el informe después de cada página, luego done con el informe final, tras lo cual la transmisión se cierra; error lleva { code, message }. ?after=<cursor> o una cabecera Last-Event-ID reanuda tras un documento (un cursor que la API no emitió es HTTP 400 cursor is not one this API issued); un id que no es un trabajo es 404. GET /v1/crawl/:id/ws y GET /v1/batches/:id/ws ascienden a un WebSocket que lleva los mismos marcos como JSON ({ type, data | error, cursor? }), se cierra con 1000 después de done, con 4404 para un trabajo ausente y 4400 para un cursor incorrecto; en un servidor con tokens, la ascensión presenta el token de portador en la cabecera Authorization o, donde la API WebSocket no da cabecera, como el subprotocolo w2l.token.<token>, reflejado de vuelta como el protocolo seleccionado (nunca en la URL; un token con caracteres que un subprotocolo no puede llevar usa la ruta SSE en su lugar). El servidor lee el punto de control una vez por suscriptor al abrirse la transmisión y luego una vez por página para todos los suscriptores de ese trabajo juntos; una transmisión es una vista del trabajo, no registra nada, y W2L_JOB_STREAMS=off convierte las cuatro rutas en 404. El watcher(jobId, { kind: 'crawl' | 'batch', transport, pollIntervalMs, timeoutMs, after, signal, WebSocket }) del SDK devuelve un JobWatcher (un EventTarget): eventos document (CustomEvent<CrawlPage>), snapshot, done (el informe) y error ({ code, message }), también producidos por for await (const event of watcher); campos jobId, kind, status, data (cada documento, cada uno una vez) y transport; close() deja de observar y deja el trabajo en ejecución. transport: 'auto' (por defecto) intenta la ruta WebSocket, luego eventos enviados por el servidor, luego sondeo (getCrawlPages con includeDuplicates, getCrawlErrors y getBatchItems con el cursor del último documento, el estado cada pollIntervalMs, por defecto 2000, al menos 250: un valor menor es un TypeError), cambiando una vez por nivel cuando un transporte no está disponible (un 404 en las rutas de transmisión, sin constructor WebSocket) o termina antes de done, y continuando desde el último cursor, de modo que un documento se emite una vez por id de paso a través de la puesta al día, la entrega en vivo y cualquier cambio; un 401 o 403 es definitivo (error unauthorized), y timeoutMs termina la observación con error { code: "watcher_timeout", message: "job <id> did not finish within <ms> ms (last status: <status>)" } mientras el trabajo sigue ejecutándose. crawlAndWatch(url, options, watch) y batchScrapeAndWatch(urls, options, watch) inician el trabajo y devuelven su observador. MCP no tiene superficie de transmisión (solo solicitud y respuesta): wait_batch y get_batch_items son el camino allí. El shim congelado v1 de Firecrawl no añade ruta WebSocket.
Un mapa (POST /v1/map, SDK map(url, opts)) lista las URLs de un sitio sin obtener cada página: lee el robots.txt del host inicial, como máximo un cuerpo de página (la URL inicial, solo en el peldaño http; no se inicia ningún navegador) y los sitemaps que el sitio declara (las líneas Sitemap: del robots.txt de la URL inicial, si no /sitemap.xml; un índice se sigue un nivel, los archivos .gz se inflan, como máximo 50 archivos), dentro de un plazo. Toma url, mode (standard o research; authed se rechaza: un mapa lee sitemaps públicos y una página pública), limit (un entero de 1 a 100,000, por defecto 5,000, el máximo y por defecto documentados de Firecrawl), timeout (milisegundos, 1,000 a 300,000, por defecto 60,000, para todo el mapa), las opciones siguientes, ignoreRobotsTxt (arriba; solo un servidor local), origin y integration; cualquier otra clave se rechaza por nombre con unsupported_parameter (useIndex con la pista de que Octocrawl no mantiene un índice de URLs; una opción de página como headers, mobile o skipTlsVerification, así que un mapa no tiene nada que aflojar; el allowSubdomains y allowExternalLinks del rastreo, que un mapa llama includeSubdomains y no ofrece). Cada candidato pasa por las reglas de alcance del rastreo (el host inicial, su gemelo www. y donde redirige la URL inicial; el subárbol de ruta de la URL inicial; activos excluidos; URLs similares plegadas, como hace deduplicateSimilarURLs en un rastreo, excepto que un enlace visto primero sobre http: cede ante su variante https: cuando esa variante también llega, en un origen cuyo robots.txt el mapa leyó de todos modos y que lo permite (no se lee más robots.txt para él); la URL inicial permanece como se dio) y el robots.txt de su host bajo la identidad declarada del mapa: una URL no permitida, o una cuyo robots.txt no puede leerse, no se devuelve a menos que el mapa se iniciara con ignoreRobotsTxt, y el robots.txt se lee para el host inicial y como máximo otros 20. La respuesta (HTTP 200) es { id, url, status, stoppedBy, links, sources, refused, identity, warnings, agentHints?, elapsedMs }: links contienen la URL inicial, luego los enlaces de la página inicial en orden de documento, luego las entradas de sitemap no encontradas ya, cada una { url, title?, description?, titleSource?, via, sitemapFile?, lastmod?, robots }. Un título nunca se inventa: el de la URL inicial es el <title> de su página (y solo ella tiene un description), el de un enlace de página es su texto de ancla (espacios en blanco colapsados, como máximo 300 caracteres; si no aria-label, title o el alt de una imagen interna), el de una entrada de sitemap es su <news:title>; ninguna otra URL se obtiene para uno. via dice cómo se encontró una URL (start, link, sitemap; una URL encontrada de ambas maneras es un enlace), lastmod es el del sitemap tal como está escrito. sources registra la lectura de la página inicial (httpStatus, status, robots, rawBodySha256, linksFound) y cada archivo de sitemap (como en un rastreo), y refused cuenta lo que se omitió y por qué (duplicate, collapsed, hostDenied, subtreeDenied, pathDenied, assetDenied, robots, robotsUnchecked, searchFiltered, overLimit, con muestras; una vez que solo los enlaces de la página inicial llenan limit el mapa responde de inmediato y no lee ningún sitemap, así que overLimit entonces cuenta solo los candidatos vistos antes de detenerse). status es completed cuando cada fuente se leyó o está definitivamente ausente (alcanzar limit está completado, con stoppedBy: "limit"), partial cuando llegaron enlaces pero el plazo cortó la ejecución o una fuente falló, y failed cuando nada volvió y una fuente falló o el plazo se disparó; en el plazo la respuesta sigue siendo HTTP 200 con lo encontrado, stoppedBy: "timeout" y una advertencia map_timeout que nombra los enlaces encontrados y los archivos de sitemap no leídos, nunca un 408 y nunca una lista de aspecto completo. Otras advertencias: start_page_unreadable, start_page_client_rendered (el carril http encontró la página llena por script; la pista es rasparla con formats: ["links"]), sitemap_unreadable, sitemap_files_capped, robots_host_cap, robots_unreachable (un robots.txt que no pudo leerse, que cuenta como una no permitida completa; la advertencia nombra el host y la razón, y una URL inicial en tal host tiene sources.startPage.robots: "unreachable" con robotsUnreachable, nunca disallowed, que se guarda para una regla que el editor escribió). Cada mapa se registra en <taskRoot>/maps/<id>.json antes de responderse y se lee de vuelta con GET /v1/maps/:id (SDK getMap(id)); un cliente que se desconecta cancela el mapa y no deja registro. Un servidor alojado toma limit hasta 5,000 y timeout hasta 60,000, y rechaza una URL que su política de canal sirve solo con el carril del navegador. El SDK espera timeout + 5000 ms por la respuesta. Lo que un mapa no hace, frente al de Firecrawl: sin índice de URLs, así que un sitio sin sitemap mapea solo los enlaces de su página inicial (docs.python.org/3/ dio 24 enlaces el 2026-10-03: su sitemap lista 8 raíces de versión); search filtra y no clasifica; un título es texto de ancla, no el <title> del destino; sin description excepto el de la URL inicial; sin location.
Las opciones de un mapa: sitemap es include (por defecto: los enlaces de la página inicial y los sitemaps), skip (no se solicita ningún archivo de sitemap; los enlaces son la URL inicial y los de su página, sources.sitemap es nulo) o only (no se lee ningún cuerpo de página en absoluto, sources.startPage es nulo; los enlaces son las entradas de sitemap que el alcance, robots.txt y search admiten, en orden listado, hasta limit, y la URL inicial está entre ellos solo cuando un sitemap la lista; ningún sitemap leído, ausente incluido, es failed con sitemap_unreadable). Con only la política de canal solo-navegador del operador no rechaza la URL, ya que no se lee ninguna página. search (una cadena de 1 a 200 caracteres con como máximo 10 palabras, después de recortar; si no HTTP 400 search must be a string of 1 to 200 characters with at most 10 words) mantiene una URL cuando cada palabra aparece, sin distinguir mayúsculas, en su URL decodificada en porcentaje o su título en mano (el de la página inicial misma, el texto de un ancla, el <news:title> de un sitemap); se ejecuta antes de robots.txt y limit, así que limit cuenta coincidencias, mantiene el orden de descubrimiento (Firecrawl ordena por relevancia; Octocrawl filtra), no obtiene nada más, y cuenta lo que omitió en refused.searchFiltered. includeSubdomains (por defecto false; Firecrawl v2 documenta true) admite cada host bajo el ápice de la URL inicial, la regla allowSubdomains del rastreo: el host inicial con un www. inicial eliminado y sin lista de sufijos públicos; el robots.txt de cada nuevo host se lee una vez bajo la identidad del mapa, para como máximo 20 hosts además del host inicial, y las URLs en hosts adicionales se cuentan en refused.robotsUnchecked con una advertencia robots_host_cap. Sin él, cada enlace está en el host inicial, su gemelo www. (python.org y www.python.org son un sitio) o el host al que redirigió la URL inicial. ignoreQueryParameters (por defecto false; Firecrawl v2 documenta true) pliega URLs que difieren solo en su cadena de consulta en la primera vista y la devuelve sin su consulta; cada pliegue se cuenta en refused.collapsed con hasta 20 muestras { url, into }, nunca fusionado en silencio. Sin él, las variantes de consulta permanecen separadas, con parámetros de seguimiento (utm_*, gclid, ...) eliminados y el resto ordenado. El includePaths, excludePaths del rastreo (como máximo 1,000 regex de 1 a 2,000 caracteres, excluir gana; un patrón que puede retroceder catastróficamente se rechaza), regexOnFullURL, crawlEntireDomain (levanta el subárbol de ruta de la URL inicial) y deduplicateSimilarURLs (por defecto true) se aplican con sus nombres, mensajes y reglas de rastreo. MCP tiene el mismo mapa que la herramienta local map (anotada solo-lectura, idempotente, mundo-abierto, con un esquema de salida): compacta por defecto, { id, status, stoppedBy, links: [{ url, title?, description? }], warning?, agentHints?, counts: { returned, refused } } con los mensajes de advertencias unidos, la respuesta nativa con debug: true, cada una respondida como texto y como structuredContent; el host MCP alojado no la ofrece, ya que no toma ninguna URL arbitraria. /fc/v1/map toma la solicitud de mapa de Firecrawl (shim).
Scrape, lote y rastreo también toman doce opciones de página y las cuatro opciones de caché (abajo); lote y rastreo las aplican a cada página y las almacenan con la tarea, así que una tarea reanudada las mantiene:
onlyMainContent(por defectotrue).falsedevuelve el Markdown de toda la página: el cuerpo del documento sin scripts, estilos, controles de formulario ni medios incrustados, y con el encabezado, la navegación y el pie conservados, mediante el mismo conversor y la misma URL base. La evidencia (hashes, estado) es la misma en ambos modos, el enrutamiento de carriles sigue leyendo el contenido principal, y el evento de trazaextractregistraonlyMainContent: false. Una excepción: en una página donde el extractor no encuentra ningún bloque principal, el valor por defecto esfailed/empty_unverifiedcon el Markdown de toda la página conservado como evidencia, mientras quefalsedevuelve ese Markdown comosuccess; en cualquier caso, el peldaño del navegador aún se intenta y responde cuando renderiza más.linkssiempre provienen de toda la página.includeTags(hasta 100 selectores CSS de 1 a 200 caracteres, con como máximo 100 partes de selector en total; ver más abajo). El contenido son los elementos que nombran los selectores, en orden de documento, un elemento dentro de otro nombrado una sola vez. Se toman de la página tal como se recibió, antes de la selección y limpieza del contenido principal, por lo que una navegación, encabezado o pie nombrado permanece, yonlyMainContentya no elige el contenido; los scripts, estilos, controles de formulario y medios incrustados se omiten como siempre. El tipo de página, el título,metadataylinksaún se leen de toda la página, la extracción JSON aún lee los hechos propios de la página y los pares etiqueta/valor de su contenido principal, ydocument.confidencees 1 cuando los elementos nombrados contienen texto o imágenes, ya que lo que nombraste es el contenido. Cuando no se nombra nada, la respuesta essuccesscon Markdown vacío del peldaño que leyó la página, no un fallo; el peldaño del navegador se intenta solo cuando la página en sí se lee como escasa o llena de scripts, como para cualquier página de ese tipo; cuando ese peldaño encuentra la página bloqueada, el bloqueo es la respuesta y se abandona la vacía (la auditoría de escalera registraladder_empty_answer_dropped), y cuando tampoco nombra nada, se confirma la respuesta vacía (confirmsEmptyen suladder_step) y no se consulta ningún peldaño adicional, incluido uno de proveedor. Una página bloqueada permaneceblockedsin importar lo que nombren los selectores, en cualquier peldaño donde se encuentre el bloqueo: el encabezado de un muro de inicio de sesión no es la página solicitada. Nombrarbodynombra toda la página.excludeTags(el mismo tipo de lista). Los elementos que nombran los selectores se eliminan, con todo lo que contienen, antes de tomar el contenido: del contenido principal, de toda la página (onlyMainContent: false), de una selecciónincludeTags, y de la página que un resultado fallido o bloqueado conserva como evidencia. Ambas listas se comparan con toda la página tal como se recibió, por lo quefooter penexcludeTagssaca los párrafos del pie de una selecciónincludeTags: ["p"]. El evento de trazaextractregistra ambas listas.waitFor(milisegundos, un entero de 0 a 60 000, por defecto 0). El peldaño del navegador espera este tiempo después de que la página se haya cargado y estabilizado, y luego la captura; un documento al que la página pasó mientras tanto (un script, una actualización meta) se estabiliza antes de la captura, y el resultado informa la URL y el estado de ese documento. El peldaño HTTP no puede ejecutar scripts, por lo que una solicitud conwaitForcomienza en el peldaño del navegador, y la auditoría de escalera registra el peldaño omitido (ladder_channel_skipped). Donde no hay peldaño de navegador configurado, el resultado esfailedconpolicy_deniedy un evento de trazawait_for_unavailable, nunca una respuesta que ignoró la espera. SinwaitForoactions, una página que se ha estabilizado se lee de inmediato, a menos que tenga poco texto (como máximo 4,000 caracteres) y aún muestre que sus datos están en camino: un elemento visible marcadoaria-busy="true", o un texto corto visible, no en un botón o enlace, que sea o termine en un mensaje de carga ("Cargando...", "Obteniendo resultados...", "Por favor espere"). Una página así se espera hasta 8 s en total (dentro deltimeoutde la solicitud), en el peldaño del navegador y en el de un proveedor, hasta que desaparezca la señal; la traza dice cuánto tiempo y si desapareció (loading_wait), y una página leída como contenido mientras aún mostraba una lleva una advertenciapage_still_loading. Las barras de progreso y los estilos de esqueleto no se toman como señales, ya que las páginas terminadas también los usan (un histograma de calificaciones, una barra de idiomas), y tampoco un cargador dejado en una página con más texto (más comentarios, la siguiente página de un feed).timeout(milisegundos, un entero de 1 000 a 300 000, por defecto 300 000). El plazo para todo el raspado, incluidowaitFor. Cuando se dispara, la API aún responde HTTP 200:partialcon el mejor contenido que un peldaño produjo hasta ahora (por ejemplo, el contenido HTTP mientras el peldaño del navegador aún cargaba), ofailedconfailureReason: "timeout"cuando no existe nada utilizable. Ambos llevanusage.deadlineExceeded: truey un evento de trazadeadline_exceeded. Cuando unwaitForse ejecutaría más allá del plazo, el navegador deja de esperar aproximadamente un segundo antes y captura la página tal como está entonces:partialcuando esa página tiene contenido, de lo contrariofailed/timeout. Las esperas de los carriles por un servidor lento siguen eltimeoutque estableciste: el carril HTTP espera los encabezados de respuesta y cada fragmento del cuerpo, y el navegador espera la navegación, hasta el plazo. Sin untimeoutmantienen sus valores predeterminados dentro del plazo de 300 000 ms: 10 s para encabezados, 30 s entre fragmentos del cuerpo y 20 s para navegación. Un carril que se detiene en uno de estos falla contimeout, sinusage.deadlineExceeded, y la escalera no avanza al siguiente peldaño para él: la respuesta es el contenido que produjo un peldaño anterior, o esefailed/timeout. El evento de trazanavigatedel carril del navegador registra la espera que permitió (timeoutMs). Un cliente que se desconecta aún cancela el raspado. Un cliente MCP que cancela su llamadascrape(notifications/cancelled), o cierra la solicitud HTTP de la llamada antes del resultado, también la cancela, por stdio y por los servicios HTTP locales y alojados; por HTTP solo el mismo cliente puede cancelar una llamada (el mismo id de sesión, y en el servicio alojado el mismo token de portador), ver cancelar una llamada MCP. Elscrapedel SDK espera la respuesta hastatimeoutmás 30 s; en Node esto reemplaza la espera propia de fetch de 300 s por los encabezados de respuesta, que una respuesta en un plazo de 300 000 ms puede superar. La extracción JSON lee campos de una páginapartialpero la informaincompletecon un problemapage_partial, y nunca llama al modelo para ella.maxFileBytes(bytes, un entero de 1 hasta elW2L_MAX_FILE_BYTESdel servidor). Un límite de tamaño más bajo para un archivo (PDF, CSV, XLSX, ZIP, JSON, texto) que el del servidor; ver Archivos.headers(un objeto de como máximo 32 nombres de encabezado con valores de cadena de como máximo 4,096 caracteres, sin saltos de línea). Se envía a la URL solicitada, sus saltos de redirección del mismo origen y, en el peldaño del navegador, los archivos que la página carga de ese origen, después de la identidad declarada de Octocrawl, que nunca pueden anular:User-Agent, cualquier encabezadosec-ch-*osec-fetch-*, las credencialesAuthorization,Proxy-AuthorizationyCookie, y los encabezados de transporte (Host,Accept-Encoding,Connection,Content-Length, ...) se rechazan con HTTP 400 nombrándolos (headers.user-agent is refused: the User-Agent and client hints are Octocrawl's declared identity;headers.cookie is refused: credentials are not sent as headers; mode 'authed' carries your own session on the record;headers.accept-encoding is refused: transport headers are set by the lane). Los nombres se convierten a minúsculas y un nombre dado dos veces se rechaza;Accept,Accept-Language,Referer,Cache-Control,If-None-MatchyX-*son los usos comunes. Una redirección a otro origen se obtiene solo con la identidad en ambos peldaños y la traza lo dice (custom_headers_withheldcon la URL y los nombres); en el peldaño del navegador, los encabezados se agregan por solicitud mediante la intercepción de solicitudes propia de Chromium, que juzga cada salto y cada archivo que la página carga por su origen, por lo que una navegación que la propia página hace a otro origen (un script, una actualización meta) y un archivo del mismo origen que redirige a otro lugar tampoco reciben ninguno. robots.txt siempre se obtiene solo con la identidad. Todo enheadersqueda registrado: la traza (request_headers_added, valores incluidos), la listaidentity_sentdel peldaño HTTP y el cumplimiento firmadosentHeadersdel peldaño del navegador (que nunca llevan un encabezadoCookieoAuthorization: una sesión queda registrada como hash), por lo que los secretos pertenecen a la ruta de sesión autenticada, no a los encabezados. En el peldaño del navegador, la configuración regional del contexto permaneceen-US, por lo que unAccept-Languageque establezcas puede diferir delnavigator.languagede la página; eso es visible para la página, no oculto. Una solicitud conheadersnunca pasa a un peldaño de proveedor (ladder_channels_filtereden la auditoría de escalera nombra los peldaños descartados).mobile(por defectofalse).trueobtiene como la segunda identidad de navegador declarada de Octocrawl: Android Chrome (Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<major>.0.0.0 Mobile Safari/537.36, el mismo Chrome mayor que la identidad de escritorio), sugerencias de cliente alineadas (sec-ch-ua-mobile: ?1,sec-ch-ua-platform: "Android"), una ventana gráfica de 412x915 a 2.625 píxeles de dispositivo por píxel CSS, y táctil. Pasa las mismas verificaciones de coherencia y honestidad que la identidad de escritorio, robots.txt se evalúa contra su User-Agent, y la traza lo registra (identity_sentcondevice: "mobile"en el peldaño HTTP,identity_declareden el peldaño del navegador); el registro de cumplimiento del peldaño del navegador lleva el User-Agent móvil y las sugerencias en susentHeaderstal como se envió, y no tiene campo de dispositivo (schemaVersion 2; agregar uno sería schemaVersion 3 y un nuevo hash para cada registro, una decisión del propietario). En el peldaño del navegador, la identidad también se establece como metadatos de user-agent propios de Chromium, por lo que las sugerencias de cliente que Chromium genera por sí mismo (en un salto de redirección, en las solicitudes propias de la página) ynavigator.userAgentDatallevan las mismas marcas, plataforma y bandera móvil que los encabezados, tanto para la identidad de escritorio como para la móvil. La página es lo que el sitio sirve a esa identidad: una redirección a su host móvil, un diseño receptivo a 412 píxeles CSS, o una página sin meta de ventana gráfica diseñada a 980 píxeles CSS y escalada, como en un teléfono; Octocrawl no reescribe nada. La identidad móvil afirma Android en Chromium de escritorio de la misma manera que la identidad de escritorio afirma macOS en cualquier host: internamente coherente, no una declaración sobre la máquina. Se rechaza conmode: "research"(mobile is not available in research mode: the research identity declares a bot, not a device) y nunca se envía a un peldaño de proveedor. El Registro de Evidencia dice qué identidad respondió:identity.deviceesmobileodesktopsegún lo declaró el carril que respondió (nulo en modo investigación, que no declara dispositivo, y cuando no se envió ninguna solicitud), yidentity.requestHeadersenumera losheadersque envió ese carril, nombres en minúsculas y ordenados, cada uno con el SHA-256 de su valor (valueSha256), nunca el valor ([]cuando no hay ninguno).skipTlsVerification(por defectofalse). Por defecto, un certificado que no se verifica (autofirmado, caducado, nombre incorrecto) esfailedconfailureReason: "tls_error"en ambos peldaños, con el código de error en el rastreo (request_failedconreason: "tls_error"ycode, comoDEPTH_ZERO_SELF_SIGNED_CERToCERT_HAS_EXPIRED), también cuando es la solicitud de robots.txt la que falla por ello, y la escalera no asciende por este motivo.true, en un servidor local, carga la página de todos modos: el peldaño HTTP utiliza rutas propias que no verifican, para esta única descarga y su búsqueda de robots.txt, y las cierra después (las rutas compartidas, las propias de la caché de robots y el trabajador de entrega siguen verificando); el peldaño del navegador abre su contexto conignoreHTTPSErrors. Cada resultado de dicha descarga lleva un evento de rastreotls_verification_skippedy una advertenciatls_unverified("El certificado de no fue verificado a petición del llamante; el contenido no puede atribuirse a ese host con certeza."), después de una advertenciarobots_overriddencuando la hay, también en elementos de lote y páginas de rastreo; el registro de cumplimiento del peldaño del navegador (schemaVersion 2) no tiene campo TLS, por lo que el rastreo y la advertencia son el registro. Un servidor alojado (--hosted, el host MCP alojado) rechaza la opción con HTTP 400skipTlsVerification is not available in hosted modeantes de que se descargue nada, y ejecuta una tarea almacenada sin ella. Nunca se envía a un peldaño de proveedor.fastMode(por defectofalse).truemantiene solo el peldaño HTTP: no se lanza Chromium,channelsTriedes["http"],summary.browserMses 0, y la auditoría de la escalera registra los peldaños que descartó (ladder_channels_filteredconreason: "fastMode"). El veredicto propio del peldaño HTTP es la respuesta: una página cuyo contenido escriben sus scripts esfailed/empty_unverifiedcon la página como evidencia, nunca renderizada, y un éxito delgado o con apariencia de renderizado por cliente mantiene sus advertencias. Cuando ese peldaño solicitó el peldaño del navegador, la respuesta (completa y compacta) llevaagentHints: ["the http lane asked for the browser lane; fastMode declined it; retry without fastMode"]. Omite el peldaño del navegador; no hace más rápida una página que el peldaño HTTP ya sirve, ywaitForno tiene efecto bajo él. Una URL que el servidor vincula al carril del navegador (el flujo alojado de Amazon) la rechaza con HTTP 400fastMode is not available for this URL: it is served by the browser lane only.blockAds(por defectotrue). Por defecto el extractor elimina contenedores de anuncios (elementos cuyo id o clase tiene uno de los tokensad,ads,advert,advertisement,sponsored,promo) y banners de consentimiento de cookies (ids concookie,consent,gdpr,cmp,onetrust,didomi,usercentricsy similares, y elementos cuyoaria-labelnombra cookies o consentimiento) antes de la extracción en cada peldaño, y el peldaño local del navegador aborta, antes de cualquier conexión, cada solicitud a una lista incluida de unos cincuenta hosts revisados que sirven anuncios (doubleclick.net,googlesyndication.com,adnxs.com,criteo.com,taboola.com,outbrain.com,amazon-adsystem.com, ...;packages/bench/src/subjects/adHosts.ts, coincidentes por host o sufijo de punto), registrando su número y los primeros 20 hosts en un evento de rastreoads_blocked. El peldaño HTTP no descarga recursos de página, por lo que solo el interruptor de extracción se aplica allí. La lista de tokens es heurística: un elemento cuya clase resulta serpromotambién se elimina, por lo que el interruptor existe; la lista de hosts está curada, no es EasyList, y no se descarga ni actualiza, por lo que los anuncios de hosts fuera de ella se cargan (se esperan falsos negativos).falsemantiene anuncios y banners de cookies enmarkdownyhtmly carga cada host;rawHtmles siempre la página tal como se recibió. La lista de permitidos de hosts del navegador alojado y su regla de imágenes, fuentes y medios permanecen en vigor diga lo que digablockAds, por lo quefalseno puede ampliarlos.removeBase64Images(por defectotrue). Por defecto un<img>cuyosrces un URIdata:se deja fuera del Markdown y su texto alternativo se conserva, en cada peldaño y en la página que un resultado fallido conserva como evidencia: Octocrawl siempre ha hecho esto, que es también lo que hace elremoveBase64Imagesde Firecrawl por defecto (Octocrawl conserva el texto alternativo donde Firecrawl escribe un marcador de posición(<Base64-Image-Removed>)).falsemantiene la imagen como, yusage.contentTokensentonces la cuenta.htmlyrawHtmlnunca se reescriben: son la página. Un enlace cuyo destino es un URIdata:siempre se escribe como su texto. Una elección de renderizado, no un hecho de descarga: sin evento de rastreo, advertencia o cambio de cumplimiento; el servicio MCP alojado rechaza la opción como toda opción fuera de su lista de permitidos.includeTagsyexcludeTagsaceptan selectores de etiqueta, clase, id y atributo, los combinadores de descendiente e hijo,:root,:emptyy:not(),:is()y:where()alrededor de selectores sin combinadores, por ejemplotable.wikitable,main > article p:not(.note)oa[href$=".pdf"]. Un selector que no se analiza es HTTP 400invalid_request(includeTags entry is not a valid CSS selector: div[[). Uno que se analiza y usa cualquier otra cosa es rechazado conunsupported_parameter, nombrando lo que usa y su lugar (excludeTags entry uses :nth-child, which Octocrawl does not match: li:nth-child(2), luego la lista admitida;details.parameters: ["excludeTags[1]"]): los combinadores hermanos+y~, pseudo-clases posicionales como:nth-childy:first-child,:has(),:contains()y cualquier otra pseudo-clase. Octocrawl no las ejecuta porque la biblioteca DOM que usa las empareja a un costo que el tamaño de la página no acota: en una página de 200 párrafosx ~ p ~ p ~ ptomó 4.7 s y una más~ pno terminó en 20 s, y eltimeoutde un raspado no puede detener un selector mientras está emparejando. Los selectores que Octocrawl acepta se emparejan en tiempo proporcional al tamaño de la página, sin importar cuántos combinadores encadenen, y una lista está limitada a 100 partes de selector en total para que la proporción también esté acotada: un nombre de etiqueta,*, una clase, un id, una prueba de atributo y una pseudo-clase cuentan cada uno como uno, los que están dentro de:not(),:is()y:where()incluidos, así quemain > article p:not(.note)tiene cinco; una lista más larga es HTTP 400invalid_requestnombrando el conteo (includeTags must hold at most 100 selector parts in all, and holds 102). Cada parte cuesta una prueba de cada elemento y cada selector compuesto un pase sobre la página: en una página de 1.2 MB y 55,000 elementos, una lista de 50 cadenas descendientes con 100 selectores compuestos distintos tomó 0.53 s,div:not()con 98 alternativas 0.06 s, y la extracción con tal lista en ambosincludeTagsyexcludeTags1.6 s frente a 0.3 s sin ellos. Los selectores se leen con el analizador propio de la biblioteca DOM, así que un nombre escapado es lo que es para la biblioteca:#\31 23, queCSS.escapeescribe para el id123, nombra ese elemento, y.\32 xl\:gridla clase2xl:grid.
Las opciones de caché. Octocrawl almacena el último resultado exitoso de cada página bajo la raíz de la tarea (page-cache.sqlite) y lo reutiliza solo cuando una solicitud lo pide. Un resultado almacenado por página y conjunto de opciones: la clave es la URL sin su fragmento, el modo, cada opción que los carriles reciben excepto timeout (formats como html o screenshot, onlyMainContent, includeTags, excludeTags, waitFor, headers, mobile, ...), los peldaños que la solicitud puede usar (así que la política de canal del servidor nunca se cruza), fastMode, una anulación de robots registrada y la compilación (las versiones del extractor y W2L_SOURCE_COMMIT), así que un resultado se reutiliza solo para una solicitud que lo habría moldeado de la misma manera, y su Registro de Evidencia nombra la compilación que lo produjo; después de una actualización, las páginas se obtienen de nuevo. Solo un success con un tiempo de obtención registrado se almacena, nunca una página parcial, fallida o bloqueada; una obtención posterior de la misma página y opciones lo reemplaza, una más antigua nunca lo hace. Una solicitud con headers personalizado se almacena solo cuando dice storeInCache: true, ya que el rastro almacenado conserva los valores de los encabezados. La caché no tiene límite de tamaño ni caducidad propia: elimina page-cache.sqlite para vaciarla.
maxAge(milisegundos, 0 a 315 360 000 000, predeterminado 0). Reutiliza un resultado almacenado obtenido hace como máximo este tiempo en lugar de obtener la página. 0 no busca nada: la página se obtiene en vivo, como siempre fue.minAge(milisegundos, mismo rango, como máximomaxAge). Reutiliza solo un resultado almacenado de al menos esta antigüedad; sinmaxAge, de cualquier edad desde este en adelante.storeInCache(predeterminadotrue, excepto para una solicitud conheaderspersonalizado, que almacena solo contrue).falseno almacena nada de esta solicitud.lockdown(predeterminadofalse). Responde solo desde un resultado almacenado, nunca obtiene: una página sin uno esfailedconfailureReason: "cache_miss"y una entradaagentHints, y no se solicita nada, robots.txt incluido.maxAgeyminAgeaún limitan la antigüedad cuando se dan;lockdownconmaxAge: 0es HTTP 400. Un rastreo en bloqueo no lee ningún sitemap, así que necesitasitemap: "skip"(HTTP 400 de lo contrario).
Un resultado reutilizado es el de la obtención original, sin cambios: su contenido, su evidenceRecord (fetchedAt, hashes, decisión de robots.txt, identidad) y su rastro, con un evento cache_hit añadido al final. La respuesta lo dice en metadata.cacheState: "hit" con metadata.cachedAt, el fetchedAt de la obtención reutilizada; channelsTried es [] y usage no cuenta ninguna solicitud, intento, byte o tiempo de navegador. Una solicitud que buscó una página y no encontró nada que encaje dice cacheState: "miss" y la obtiene (un evento cache_miss, y cache_stored cuando el resultado se almacenó). Una solicitud que no buscó nada (sin maxAge mayor que 0, sin minAge, sin lockdown) no lleva cacheState en absoluto: nunca se reporta como fallo. Los elementos de lote y las páginas de rastreo llevan cacheState y cachedAt de la misma manera, y una página reutilizada tiene cached: true y cuenta en el cachedPages del informe de rastreo; espera el ritmo de su host como cualquier página pero deja el robots.txt del host Crawl-delay como estaba. Una URL que el allowlistedDomains de la solicitud rechaza nunca se responde desde la caché. El modo authed ni almacena ni reutiliza (una página leída con tu sesión sigue siendo tuya): una búsqueda es HTTP 400 allí. Las capturas de un Monitor ni leen ni llenan la caché. El useCached de un rastreo es algo diferente: una reanudación reutiliza las páginas que ese rastreo ya obtuvo (arriba); maxAge alcanza los resultados de cualquier solicitud en el mismo servidor. /fc mapea las cuatro opciones bajo los mismos nombres (ver docs/firecrawl-shim.md).
formats toma markdown, links, json, html, rawHtml, images, tables y screenshot, una entrada de { "type": "attributes", "selectors": [{ "selector", "attribute" }] } y una entrada de { "type": "screenshot", "fullPage", "quality", "viewport" }, en scrape, batch y crawl. images es cada URL de imagen de todo el documento tal como se recibe, como links: img src y cada candidato de srcset (por lo que una variante de 1.5x/2x o 480w es su propia entrada), <picture> <source srcset> candidatos, los atributos de carga diferida data-src, data-srcset, data-lazy-src y data-original en img y source, video[poster], <link rel="image_src">, og:image (y sus formas :url / :secure_url) y twitter:image; cada uno resuelto contra la base del documento (<base href> o la URL final), solo http(s) absolutos, sin el fragmento, cada URL una vez, en orden de documento, con las URIs data: omitidas y contadas. No hay filtro de extensión de archivo, por lo que una URL de CDN sin extensión permanece, y includeTags, excludeTags y onlyMainContent no reducen la lista. En el escalón del navegador, la lista se lee del DOM renderizado, por lo que una imagen que un cargador diferido ya ha movido de data-src a src aparece una vez. El rastro registra images_collected con count, srcsetCandidates, lazy y dataUrisDropped. attributes lee, para cada selector (de 1 a 50 entradas, un selector de 1 a 200 caracteres bajo las mismas reglas y el mismo límite de 100 partes que includeTags, un nombre de atributo que coincida con ^[A-Za-z_][A-Za-z0-9_:.-]*$ de como máximo 100 caracteres), los valores del atributo nombrado en cada elemento que el selector coincide en el documento tal como se recibe, como está escrito en el HTML (no resuelto; links y images llevan las formas resueltas), elementos sin el atributo omitidos, en orden de documento, [] cuando nada coincide: [{ "selector", "attribute", "values" }] en orden de solicitud, con un evento de rastro attributes_extracted (selectors, counts). Un selector que no se analiza es HTTP 400 attributes selectors[i].selector is not a valid CSS selector: …, uno que Octocrawl no coincide unsupported_parameter nombrando formats[j].selectors[i], y una solicitud puede llevar una entrada de atributos (formats must contain at most one attributes entry). Ambos se devuelven solo cuando se solicitan, por las respuestas de scrape completas y compactas (cuya lista formats los nombra), elementos de batch, páginas de crawl y /fc (data.images, data.attributes), y solo para una página leída como contenido: un archivo, una página fallida o bloqueada y un resultado sin HTML no llevan ninguno; una página sin imágenes da images: []. summary.attempts y las auditorías almacenadas nunca los repiten. El escalón del proveedor (vendedor) también los lleva, leídos de la página que el proveedor devolvió, ya que lleva html y rawHtml. html es el HTML limpiado del que se escribe el Markdown: la región de contenido principal; con onlyMainContent: false el <body> de la página con su encabezado, navegación y pie de página, sin scripts, estilos, controles de formulario, medios incrustados y los elementos excludeTags; con includeTags un <body> que contiene los elementos nombrados. Es el marcado propio de la página: los destinos de enlaces e imágenes permanecen como la página los escribió (el Markdown los resuelve), y los marcadores de diseño del escalón del navegador, que dan forma al Markdown, no están en él. rawHtml es la página tal como el escalón que responde la recibió, scripts y todo: el cuerpo de la respuesta en el escalón HTTP, el DOM renderizado en un escalón de navegador. Sus bytes UTF-8 se convierten en hash a snapshot.rawBodySha256 (el rawSha256 del Registro de Evidencia). Ambos se devuelven solo cuando se solicitan, por las respuestas de scrape completas y compactas, elementos de batch, páginas de crawl y /fc, y summary.attempts los repite solo con debug: true (las auditorías de batch y crawl almacenadas nunca lo hacen). Son null para un archivo y para un resultado que no es success o partial: una página fallida o bloqueada mantiene su Markdown como evidencia, no su HTML, y la página duplicate de un crawl entrega ambos con su Markdown. El outputSha256 del Registro de Evidencia cubre Markdown y JSON, no html. screenshot (la cadena, el screenshot@fullPage de Firecrawl v1, o una entrada { "type": "screenshot", "fullPage", "quality", "viewport" } por solicitud: formats must contain at most one screenshot entry) captura la página renderizada como una imagen en el escalón del navegador local, que tal solicitud entonces selecciona solo: channelsTried es ["browser_local"], no se hace ningún intento HTTP, la auditoría de escalera nombra los escalones eliminados (ladder_channels_filtered, razón screenshot), un servidor sin un escalón de navegador rechaza el formato con HTTP 400 screenshot requires the browser lane, which this deployment does not offer, y fastMode junto a él se rechaza de la misma manera (which fastMode declines). La captura se toma después de la carga, la estabilidad y waitFor y antes de que se lea el DOM, por lo que la imagen y el Markdown muestran la misma página, con el scale: "css" de Playwright: la imagen tiene el tamaño de píxeles CSS, 1280x800 para la ventana gráfica de escritorio declarada (el factor de escala de dispositivo declarado 2 se informa, no se incorpora en la imagen) o el viewport solicitado (enteros 320..1920 por 240..1080, dentro de la pantalla declarada de 1920x1080; con mobile, dentro de la declarada 412x915), que es un tamaño de ventana y no un cambio de identidad: el User-Agent, las sugerencias de cliente, la configuración regional, la zona horaria, la pantalla y el factor de escala permanecen como se declararon, y el rastro registra screenshot_viewport. fullPage: true captura la altura completa del documento al ancho de la ventana gráfica sin desplazarse primero, por lo que las secciones que una página carga al desplazarse pueden mostrarse sin cargar (el Chromium que Playwright 1.62.1 instala capturó accesorios de 100,000 px de alto completos, sin recorte). quality (1 a 100) da un JPEG a esa calidad, de lo contrario un PNG. El screenshot de la respuesta es { contentType, width, height, fullPage, viewport, deviceScaleFactor, quality, bytes, sha256, path, base64 }: los bytes en línea, con hash a sha256; path es <sha256>.png o .jpg bajo W2L_CAPTURE_RAW_DIR cuando eso está configurado (también listado en snapshot.artifacts, y en el Registro de Evidencia como kind: "screenshot" con su tamaño y tipo), de lo contrario null; el rastro registra screenshot_captured con el tamaño, los bytes, el hash y captureMs. La captura va en lo que sea que la página renderizada resulte ser, un éxito, una página de error o una puerta mantenida como evidencia; es null cuando el navegador no pudo tomarla (screenshot_failed en el rastro, una advertencia screenshot_unavailable y una entrada agentHints, el resultado de la página mantenido) y cuando no se renderizó ninguna página (un archivo, una denegación de robots.txt). summary.attempts y las auditorías de batch y crawl almacenadas llevan screenshot: null, por lo que la imagen viaja una vez por respuesta; un batch o crawl con el formato toma cada página en el escalón del navegador y almacena cada captura en línea en su punto de control, por lo que las capturas de ventana gráfica son la opción más ligera allí. /fc lo devuelve como la cadena de datos URI data.screenshot de Firecrawl. El escalón del proveedor (vendedor) nunca lo lleva: una solicitud de captura de pantalla mantiene los escalones del navegador local solos, por lo que los escalones del proveedor se eliminan con el escalón http (ladder_channels_filtered, razón screenshot).
El formato list ({ "type": "list", "itemSelector": "article.product", "fields": [{ "name": "title", "selector": "h3 a" }, { "name": "url", "selector": "h3 a", "attribute": "href" }, { "name": "price", "selector": ".price" }] }, uno por solicitud, de 1 a 50 campos, nombres únicos) convierte una página de elementos repetidos en registros: cada elemento que itemSelector coincide es uno (un elemento dentro de otro coincidente es parte de él, no un registro propio), y cada campo se lee de él: el texto de su primera coincidencia dentro del registro como un lector lo ve (scripts y estilos omitidos, bloques mantenidos separados, espacios en blanco colapsados); el elemento del registro mismo cuando el campo no tiene selector; o el atributo nombrado en lugar del texto, un enlace o fuente (href, src, data-src, ...) hecho absoluto contra la página, su <base href> incluido. Los nombres de campo son únicos, y source_url, page y index son las columnas propias del CSV, por lo que ningún campo las toma. Los selectores siguen las reglas includeTags y se verifican antes de que se obtenga cualquier cosa. El list de la respuesta es { itemSelector, fields, records: [{ values, missing, source: { url, page, index } }], pages, incomplete, truncated, csv, csvSha256 }: un valor que el registro no tiene es nulo y se nombra en su missing, nunca se completa o adivina, y incomplete cuenta los registros con uno; csv tiene los campos, luego source_url, page y index, para que cada fila pueda rastrearse hasta la página y el lugar del que provino. Leído de la página tal como se recibe (el DOM renderizado en un escalón de navegador), sin modelo. Con una acción paginate, los registros son los de cada página que leyó, cada uno con su número de página, también cuando un paso posterior falló; una página cuyos registros repiten una página ya fusionada no se cuenta de nuevo. De lo contrario, son los de la página tal como está. Una lista se detiene en 10,000 registros o 5,000,000 de caracteres de valores: truncated es entonces verdadero y una advertencia list_truncated lo dice. Una página con al menos un registro que contiene un valor no falla por no tener contenido principal (una lista de productos o citas no es un artículo): responde success, su Markdown la página completa; los elementos que coinciden pero no contienen nada (un esqueleto de carga) no cuentan. La página duplicada de un crawl no lleva registros. Los elementos de batch llevan el list de su página. /fc no lo ofrece.
{ "type": "list" } solo encuentra la lista de la página misma, y { "type": "list", "itemSelector": "..." } los campos de los elementos nombrados (fields sin itemSelector se rechaza). La lista son los elementos que se repiten uno al lado del otro con la misma etiqueta y clases bajo padres de la misma etiqueta y clases (las filas de una cuadrícula juntas), puntuados por cuántos son, cuánto texto contienen y cuán similares son internamente. Una lista en nav, header, footer o aside, bajo un rol de menú, u oculta (hidden, aria-hidden, un <details> cerrado) nunca se elige y ninguno de sus elementos se lee como un registro (elementos ocultos por su propio atributo hidden o aria-hidden, como esqueletos de carga, se mantienen fuera con :not([hidden]):not([aria-hidden="true"]) en el itemSelector); un menú por su clase (menu, dropdown, tabs, pagination), o elementos que son cada uno un enlace corto, cuentan poco. Sus campos son lo que al menos el 60% de los elementos contienen en el mismo lugar dentro de ellos: el texto de un elemento, el texto de un enlace y href, el src de una imagen; un texto que cada elemento tiene igual (una etiqueta, un botón) no es un campo. Cada campo se nombra después de su clase (price para una suma de dinero), nunca source_url, page o index; un elemento que carece de un campo lo tiene faltante, nunca el valor de otro elemento (el selector de cada campo se verifica en cada elemento, y uno que el trabajo limitado no puede verificar se omite). Los selectores elegidos permanecen dentro de lo que una solicitud puede enviar (200 caracteres cada uno, 100 partes de selector en total), y el trabajo está limitado en cualquier página. El itemSelector de la respuesta es el elegido y list.detected es { fields, alternatives: [{ itemSelector, count }] }: los campos como una solicitud los nombra, para enviarlos de vuelta tal como están o editados, y las otras listas encontradas, mejor primero. Una página sin lista responde itemSelector: null, sin registros y una advertencia list_not_detected. Con paginate, la lista se encuentra en la primera página y cada página se lee de la misma manera. Es una suposición de la estructura de la página, sin modelo: verifica detected antes de confiar en ella.
tables entrega cada tabla de datos del contenido del que se escribió el Markdown (el contenido principal, la página completa con onlyMainContent: false, o la selección de includeTags) como datos: una entrada por cada tabla GFM de ese Markdown, en su orden, por lo que tableIndex N es la N-ésima tabla GFM. Las tablas de diseño y las tablas de una sola fila no son tablas de datos, y una tabla anidada en una celda es el texto de esa celda, como en el Markdown. Cada entrada es { tableIndex, caption, sourceUrl, headerRows, columns, rows, csv, csvSha256 }: caption es el <caption> como texto plano (null cuando no hay ninguno; un título escrito encima de la tabla no es un subtítulo), sourceUrl la URL final de la página, headerRows las filas iniciales en <thead> o compuestas únicamente de celdas <th>, y rows las celdas como texto plano (un enlace es su texto, una imagen su texto alternativo, los espacios en blanco colapsados, nada escapado) con una celda que abarca filas o columnas repetida en cada espacio que cubre, por lo que cada fila tiene columns celdas y ninguna está desplazada. csv son las filas como CSV RFC 4180 (fines de línea CRLF, un campo citado cuando contiene una coma, una comilla o un salto de línea, comillas duplicadas) y csvSha256 su SHA-256. Los abarcamientos se leen y limitan como los leen los navegadores: los dígitos iniciales del atributo (2.5 es 2), 1 cuando no tiene ninguno, es negativo o es un colspan de 0, un rowspan de 0 hasta el final de su <thead>, <tbody> o <tfoot> (o de la serie de filas directamente en la tabla), como máximo colspan 1,000 y rowspan 65,534. Un rowspan cubre cada fila que abarca, también una cuyas celdas terminan antes de su columna, y nunca pasa más allá del final de su grupo de filas. Las filas están en el orden en que los navegadores las disponen: la primera <thead> primero y la primera <tfoot> al final (incluida una vacía), dondequiera que estén escritas; un <thead> o <tfoot> posterior permanece donde está. Una página se analiza mediante la construcción de árboles del estándar HTML (parse5), por lo que una tabla contiene las filas y celdas que un navegador construye a partir de las mismas etiquetas, incluidas las mal anidadas: una fila o grupo de filas escrito dentro de una celda cierra la celda, el texto escrito entre filas viene antes de la tabla, una etiqueta de cierre que un navegador ignora no cierra nada, y un <tr> o <td> propio de un svg no es una fila o celda. Un fragmento (como el contenido principal) se lee como el contenido de un <template>, por lo que una fila o celda de una tabla sigue siendo una. Una tabla cuyas celdas, abarcamientos repetidos y cada fila rellenada al más ancho, excedería 2,000,000 de caracteres, o lo que queda de 5,000,000 para las tablas de la página juntas (una celda cuenta su texto y su escape CSV y JSON), se da como { tableIndex, …, rows: [], csv: "", omitted: "too_large" }, por lo que una página pequeña no puede hacer un CSV enorme. El rastro registra tables_extracted con el recuento, las filas y columnas de cada tabla, y los índices de las tablas omitidas. Se devuelve solo cuando se solicita, por las respuestas de raspado completas y compactas, los elementos de lote y las páginas de rastreo, y solo para una página leída como contenido ([] para una página sin tablas); /fc rechaza el formato, que Firecrawl no tiene. Las tablas de un PDF no se reconstruyen (ver texto PDF).
Un raspado o un lote también toma actions: pasos que el navegador local ejecuta en la página después de la carga, estabilidad y waitFor, y antes del formato de captura de pantalla y de que se lea el contenido, por lo que una página cuyos datos aparecen solo después de una interacción puede leerse (un botón "cargar más", un formulario de búsqueda, una lista infinita, una pestaña). Los pasos son los de Firecrawl: wait (milliseconds hasta 60,000, o un selector esperado hasta 60 s), click (selector; all: true hace clic en cada coincidencia; un control que otra cosa sigue cubriendo, un modal o un banner de consentimiento, falla el paso en unos 10 s, nombrando lo que lo cubre, una vez que ha permanecido cubierto durante un intento completo de 5 s del clic, como también lo hacen los clics de loadMore y paginate; una cubierta que desaparece antes se espera), write (text, escrito en el elemento que tiene el foco, así que haga clic primero), press (key: Enter, Tab, ArrowDown, ...), scroll (direction up o down, una pantalla de la página o del elemento que un selector nombra), screenshot (fullPage, quality, viewport), scrape (el HTML de la página en ese punto), executeJavascript (script, un cuerpo de función cuyo valor return se conserva; se ejecuta a través del protocolo DevTools, por lo que la Política de Seguridad de Contenido de la página no lo detiene) y pdf (format A0 a A6, Carta, Oficio, Tabloide o Ledger, landscape, scale 0.1 a 2); como máximo 50, verificados antes de que se obtenga cualquier cosa. Se ejecutan en orden, cada uno registrado en el rastro como un evento action con su índice, tipo, resultado y tiempo (y navigatedTo cuando movió la página). El actions de la respuesta contiene lo que produjeron: screenshots (como evidencia del formato de captura de pantalla), scrapes ({ url, html }), javascriptReturns ({ type, value }) y pdfs ({ format, landscape, scale, bytes, sha256, base64 }), cada uno en orden de paso. Un paso que falla detiene los pasos posteriores: actions.failed lo nombra (index, type, code, message; los códigos son selector_not_found, selector_timeout, script_error, navigation_refused, deadline_exceeded y action_error), y una página leída como contenido es failed con action_failed, su Markdown la página tal como estaba, ya que no es la página a la que los pasos debían llegar. Cada paso está limitado por el timeout del raspado menos el segundo reservado para leer la página: un paso que no termina a tiempo falla como deadline_exceeded, y lo que los pasos anteriores produjeron se conserva. Desde el primer paso hasta que se lee la página, una navegación de la página (un clic en un enlace, una redirección de un script, un formulario) pasa por robots.txt y la política de salida como lo hizo la URL solicitada, antes de que se envíe su solicitud: una que Octocrawl no obtiene se responde 204 No Content dentro del navegador, por lo que nunca se solicita y la página permanece donde estaba, con un evento de rastro navigation_refused y el paso que llevó a ella fallando como navigation_refused (una página que los pasos dejaron en tal URL a través de una redirección del servidor no se lee). Una página a la que la navegación de un paso llega a través de una redirección del servidor se verifica cuando carga, y un paso que aterriza en una que Octocrawl no obtiene falla allí, sin conservar nada de lo que leyó de ella; una redirección de la propia URL solicitada es de la obtención, no de un paso, y la URL solicitada, cuando robots.txt se dejó de lado para ella (una URL que la solicitud nombró en un servidor local, o un robotsOverride), se obtiene como dijo la solicitud; cualquier otra URL a la que un paso navega se verifica contra robots.txt. Una ventana que un paso abre se protege de la misma manera y se cierra de inmediato (popup_closed en el rastro). Un cambio de la URL dentro de la página (history.pushState) no solicita nada y no es una navegación. Una URL que responde con un archivo no tiene página: sus pasos se informan como no ejecutados (action_error en el primero), nunca omitidos en silencio. El pageActions del Registro de Evidencia enumera los pasos que se ejecutaron con su resultado y dice si un script se ejecutó (scriptRan): los hashes son de la página tal como los pasos la dejaron, por lo que una página que un script reescribió está en el registro como tal. Una solicitud con actions selecciona solo los peldaños del navegador local (ladder_channels_filtered nombra los demás), se rechaza con fastMode y con las opciones de caché (una página después de acciones nunca se almacena ni se reutiliza), y es rechazada por un servidor alojado y el endpoint MCP alojado; un rastreo y un mapa no toman ninguno. /fc mapea el actions de un raspado (ver docs/firecrawl-shim.md).
Tres pasos más son propios de Octocrawl, para listas que terminan solo cuando la página lo dice; cada uno se detiene solo, nunca hace un bucle en un control que permanece, y dice por qué se detuvo:
scrollToEnd(selectorpara desplazar un elemento en lugar de la página,itemSelectorpara contar elementos,maxScrolls1 a 200, predeterminado 50,waitMs100 a 10,000, predeterminado 1,000): se desplaza hasta el final, espera, y de nuevo, hasta que dos rondas seguidas no agreguen ni altura ni elementos (end).loadMore(selectordel control "cargar más",itemSelector,maxClicks1 a 200, predeterminado 50,waitMs): hace clic en él, espera, y de nuevo, hasta que desaparezca, esté oculto o deshabilitado (el atributo,aria-disabled, o una clasedisableden él o alrededor) (end), o dos clics seguidos no agreguen nada (no_growth); un control que nunca estuvo allí esselector_not_found. Un control oculto o deshabilitado antes del primer clic, o mientras cargan los elementos que solicitó, se espera hasta 10 s antes de que termine la lista: muchas páginas muestran el suyo solo una vez que su script se ejecuta.paginate(nextSelector,itemSelector,maxPages1 a 100, predeterminado 10,waitMs): lee la página, guarda su HTML enactions.scrapes, sigue Siguiente y lee esa página, hasta que Siguiente desaparezca, esté oculto o deshabilitado (end) o la página en una URL muestre lo que mostró antes (repeat: Siguiente llevó de vuelta o no hizo nada). Una página se lee dos veces con 300 ms de diferencia y solo lo que se lee igual cuenta, por lo que un reloj o un precio que cambia no la hace parecer nueva. ConitemSelector, los elementos son los registros (cada uno sus enlaces y fuentes de imágenes y sus palabras estables): una página cuyos registros ya se leyeron bajo otra URL (una primera página tanto en/listcomo en/list?page=1) se omite, y dos páginas así seguidas (un sitio que responde cada página pasada la última con la última) terminan la lista. SinitemSelectorninguna página se omite, por lo que tal sitio se lee hastamaxPages, con la advertencia a continuación. Cada página a la que llega pasa por las verificaciones de navegación antes de leerse; una que Octocrawl no obtiene falla el paso comonavigation_refused, sin conservar ninguna de sus páginas. Un clic que nunca aterriza falla el paso comoaction_error, y las páginas leídas antes conservan sus registros. En un lote, cada página que el paso lee se guarda en el punto de control de la tarea en el momento en que se lee: un lote cortado en la página N (apagado, bloqueo) se reanuda allí en el siguiente inicio, pasando por las páginas guardadas a lo largo de los propios enlaces Siguiente del sitio y leyendo el resto una vez;actions.lists[].resumeddice cuántas páginas vinieron del punto de control. Una página guardada se conoce de nuevo por su dirección (cuando las páginas tienen direcciones propias), por sus elementos, o solo por los enlaces de sus elementos cuando su texto cambió mientras tanto y esos enlaces difieren de página a página; una página guardada conocida por ninguno de esos (filas sin enlaces cuyo texto cambió) se lee de nuevo, sus registros se repiten, ymaxPagesla cuenta dos veces. Una verificación que el sitio pone donde debería estar la página siguiente detiene el paso allí comochallenge: un intersticial (Cloudflare, un mantener presionado de PerimeterX, un formulario de verificación) por sus propias marcas antes de que se lea la página, una página de nada más que un widget CAPTCHA por el veredicto de la página después de los pasos (una página con registros, o con otro contenido, es una página sea cual sea el widget que lleva). Las páginas anteriores conservan sus registros, el resultado esblockedcon la razón de la verificación,actions.lists[].challengenombra la página, y en un lote las páginas anteriores permanecen en el punto de control de la tarea; la entrega del lote luego le permite pasar la verificación y continuar en su propio Chrome (ver la sección de entrega).actions.listscontiene, por cada paso de lista,{ index, type, stoppedBy, rounds, items }(itemslos elementositemSelectorcoinciden al final, null sin uno;itemsReadparapaginate, en cada página). Un paso de lista que se detuvo en su límite (max), porque llegó la fecha límite (deadline) o en una verificación que el sitio presentó (challenge, conchallenge: { page, url, reason, signals }) no es la lista completa, y el resultado lleva una advertencialist_not_exhaustedque así lo indica. El Markdown es la página tal como los pasos la dejaron (parapaginate, la última página); extraer registros de cada página es un paso posterior.
Un scrape también toma robotsOverride ({ reason, recordedBy? }; reason de 1 a 500 caracteres, recordedBy de 1 a 200) en REST, el SDK y MCP: tu propia razón registrada para obtener esta URL aunque el robots.txt de su host la desautorice o no se haya podido leer, por ejemplo un informe que el editor enlaza desde sus propias páginas en un host de archivos cuyas reglas se dirigen a rastreadores. Un servidor local obtiene una URL que un scrape nombra de todos modos (ver robots.txt arriba); la anulación pone tu razón y nombre en el registro en lugar de user_named_url. robots.txt aún se lee y su veredicto se registra. Cuando desautoriza la URL, la obtención se realiza y lo dice en todas partes: el rastro lleva robots_disallowed y luego robots_overridden (url, appliedRules, reason, recordedBy), los warnings del resultado comienzan con { code: "robots_overridden", message } nombrando la URL de robots.txt, la regla, la razón y quién la registró (respuestas de scrape completas y compactas, elementos de lote), el registro de cumplimiento del carril del navegador mantiene la desautorización con skippedFetch: false y un override que su hash cubre, y el robotsDecision del Registro de Evidencia permanece disallowed con userOverride: true y overrideBasis: "robots_override". Cada resultado de tal scrape lleva la advertencia y estos eventos, sea cual sea el peldaño o la fecha límite que lo produjo: cuando la fecha límite pasa mientras la solicitud está fuera (failed/timeout), o cuando un peldaño posterior que no obtuvo nada responde (en modo authed, el salto del peldaño authed_session cuando no hay sesión), Octocrawl los agrega a ese resultado con el evento robots_checked del carril, cada evento nombrando el carril que dejó de lado la regla, y el robotsDecision de ese resultado se lee de ellos. La excepción es un error propio de Octocrawl después de que la regla se dejó de lado: el scrape responde HTTP 500 internal_error, y un elemento de lote que falla con internal_error registra solo ese error. La anulación es para obtenciones desde tu propia máquina: los peldaños HTTP y de navegador local la aplican, el carril del proveedor no toma ninguna, y un scrape que dejó de lado una regla no continúa a un peldaño de proveedor (ladder_channel_skipped en la auditoría de escalera), por lo que no se abre ninguna sesión de proveedor para esa URL. Cuando robots.txt permite la URL, la anulación no hace nada y no deja rastro. Un robots.txt inalcanzable también se deja de lado, su razón se mantiene en el rastro y la advertencia. Un lote toma robotsOverrides, una lista de { url, reason, recordedBy? } en la que cada url es uno de los urls del lote, nombrado una vez; solo esa URL se obtiene más allá de su regla, las otras URLs del lote no se ven afectadas, y la lista se almacena con la tarea, por lo que un lote reanudado la mantiene. Un crawl y un mapa no toman ninguna (toman ignoreRobotsTxt), y tampoco /fc, el endpoint MCP alojado y la vista previa pública, que aceptan solo sus propias opciones. Un servidor alojado (npm run api -- --hosted) tampoco toma ninguna, ni ignoreRobotsTxt: quien sea que tenga uno de sus tokens, estos campos se rechazan con HTTP 400 unsupported_parameter nombrando el campo antes de que se obtenga cualquier cosa, y un lote o crawl almacenado que reanuda se ejecuta sin sus anulaciones. En un scrape o lote, ignoreRobotsTxt se rechaza con HTTP 400 unsupported_parameter que lo nombra, al igual que cualquier otra clave dentro de una anulación.
Scrape, lote y crawl también toman dos etiquetas que van a los registros propios de Octocrawl y a ningún otro lugar: integration, la tuya ("nightly-prices"), y origin, la del cliente. El SDK envía origin: "js-sdk@<version>" (su SDK_VERSION, fijado a su versión de paquete) con cada scrape, crawl y lote a menos que la llamada establezca una; el servidor MCP registra mcp-<client name>@<client version> del initialize del cliente (caracteres fuera del ASCII imprimible escritos como _, recortados a 100; mcp@0.3.0 para un cliente que no declaró ninguna) y rechaza un origin que una llamada de herramienta nombre, por lo que las herramientas exponen integration solo; /fc mapea el origin que los SDK de Firecrawl envían y también toma integration. Cada una es de 1 a 100 caracteres ASCII imprimibles sin espacios, de lo contrario HTTP 400 integration must be a string of 1 to 100 printable characters without spaces (lo mismo para origin). Nada enviado al objetivo cambia, y ninguna respuesta los refleja: el registro de un scrape lleva ambos (abajo), y un lote o crawl los almacena con su tarea, que GET /v1/batches/:id y GET /v1/crawl/:id informan como attribution: { origin?, integration? } (ausente cuando la solicitud no nombró ninguna).
Un servidor puede comprimir su respuesta aunque Octocrawl no haya solicitado compresión: la identidad de Octocrawl no envía Accept-Encoding, y algunos sitios (www.python.org) responden gzip de todos modos. El carril HTTP decodifica el cuerpo por su Content-Encoding, solicitado o no: gzip (y x-gzip), deflate (envuelto en zlib o crudo) y br, una lista de codificaciones en orden inverso, después del límite de cable de 10 MiB (el límite de archivo para un archivo) y bajo el límite de descompresión de 50 MiB (failed con decompressed_too_large más allá de él). usage.bytesWire es el tamaño recibido y bytesDecompressed el tamaño decodificado; rawSha256 y los bytes guardados de un archivo son del cuerpo decodificado; evidence.contentEncoding y el contentEncoding del Registro de Evidencia nombran las codificaciones (identity cuando no hubo ninguna; null en los carriles de navegador y proveedor, que no lo informan). Cualquier otra codificación es failed con unsupported_content_encoding, y los bytes que no se decodifican como su codificación son failed con parse_error (rastro content_decoding_failed): ninguno se lee como la página. El lector de sitemaps del crawl decodifica un archivo de sitemap de la misma manera antes de inflar un archivo .gz.
Un resultado también dice cuándo la página que leyó parece un caparazón para datos que sus scripts completan. El extractor lee la página como se recibió para un <table> sin celdas además de scripts, una raíz de aplicación (#root, #app, #__next y sus afines) con casi nada de texto, una página de scripts con poco texto, un elemento que la página oculta una vez que los scripts se ejecutan (una clase hide-if-js-enabled y sus afines), un blob de hidratación (__NEXT_DATA__, window.__STATE__ =, window.__data = y similares) en una página delgada, aria-busy, un mensaje de carga en la región extraída ("Loading...", "Fetching quotes…", "Please wait": el texto completo de un elemento, con su elipsis, no el de un botón o un enlace, ni dentro de hidden, aria-hidden="true" o una clase solo para lectores de pantalla) en una página de como máximo 4,000 caracteres visibles, o, en una página enrutada como listado o colección, una lista en su JSON de hidratación de al menos 10 registros nombrados distintos (name o title, de 12 caracteres o más) de los cuales el texto visible muestra al menos 3 y como máximo la mitad (una página de categoría de Walmart muestra 9 de los 49 productos que su __NEXT_DATA__ lista); cada regla empareja una brecha estructural con presencia de scripts, por lo que una página estática con una tabla vacía nunca activa una, y un aviso noscript "enable JavaScript" solo cuenta en una página delgada (menos de 1,500 caracteres visibles) o junto a estado de hidratación, ya que los sitios estáticos llevan uno junto a sus scripts de análisis. Cuando la página del peldaño HTTP se lee de esa manera, su estado permanece lo que el contenido ganó, un success o el resultado failed/empty_unverified que mantiene toda la página como evidencia cuando no se encontró una región principal (una página de marco del sitio alrededor del script que escribe su contenido se lee de esa manera), y el resultado lleva { code: "client_rendered_suspected", message } en warnings (después de una advertencia robots_overridden cuando hay una) nombrando la regla: empty_table_with_scripts, empty_app_root, script_shell, js_fallback, hydration_shell, aria_busy, loading_text o hydration_list_partial. Su rastro lleva quality_client_rendered con la regla, los marcadores encontrados, el número de tablas vacías y los tamaños de texto visible y scripts (y, para hydration_list_partial, listRecords: los registros que la lista declara y cuántos se muestran), y la escalera ofrece la página al peldaño del navegador como lo hace con un resultado delgado (quality_low_yield): la página renderizada es la respuesta cuando contiene más, de lo contrario la página HTTP permanece, advertencia incluida, con ese salto marcado improved: false (trigger: "quality_client_rendered"). Un caparazón failed/empty_unverified va al peldaño del navegador en su propia solicitud extract_low_confidence, y la evidencia mantenida cuando ese peldaño luego falla sin una página (ladder_evidence_kept) lleva la advertencia. El peldaño del navegador nunca la plantea: su captura es la página renderizada. La propia lectura del extractor es render en su salida (@w2l/extract-tf): clientRendered, reason, markers, emptyTables, textChars, scriptChars y, con hydration_list_partial, listRecords. Cuando una respuesta HTTP delgada o similar a un caparazón permanece como la respuesta, también lleva { code: "low_content_yield", message }, después de sus otras advertencias: The http lane extracted N tokens at confidence C; the browser lane did not improve it. cuando el peldaño del navegador respondió sin más, o falló, y la página HTTP se mantuvo (ladder_best_kept, el salto de calidad improved: false), y …; the browser lane was not available to this request. cuando no se permitió ningún peldaño adicional (fastMode, un servidor vinculado al peldaño HTTP, sin peldaño de navegador configurado); N y C son las propias cifras quality_low_yield del peldaño HTTP (si no, su confianza extract), y en un caparazón failed/empty_unverified la frase se lee The http lane found no main content at confidence C; …. Una página renderizada que respondió no lleva tal advertencia. Cada respuesta que lleva warnings también lleva warning, sus mensajes unidos con un espacio, el nombre de Firecrawl para ello: respuestas de scrape completas y compactas, elementos de lote, páginas de crawl y data.warning en /fc, que así pasa las advertencias nativas.
Una respuesta también dice qué cambiar en la solicitud la próxima vez, en agentHints (respuestas de raspado completas y compactas, elementos de lote, páginas de rastreo y el registro de raspado; agent_hints en páginas /fc), una frase cada una, presente solo cuando algo aplica y nunca citando texto de la página, solo un host, un estado, una regla o un tiempo: para un policy_denied con un evento robots_disallowed, robots.txt of <host> disallows this URL for Octocrawl's identity (rule <patterns>). A local Octocrawl server fetches a URL a scrape or batch names whatever robots.txt says, and a crawl or map started there with ignoreRobotsTxt fetches the links it disallows, each on the record; a hosted server obeys robots.txt for every URL (un robots.txt inalcanzable dice que cuenta como una denegación completa y que Octocrawl lo solicita de nuevo después de cinco minutos, luego nombra las mismas rutas); para un login_wall, the page asks for a login; Octocrawl does not create accounts; use mode authed with your own session; para cloudflare_challenge, captcha y bot_detected_generic, <host> gates automated access on the lanes tried (<lanes>); Octocrawl does not solve challenges or change its identity; a proxy or session you own is the supported route, or, on your own machine, getting through the check yourself in your own Chrome: handoff: true on a scrape (octocrawl scrape --handoff), or a batch handoff (octocrawl batch --handoff, POST /v1/batches/:id/handoff); para un retryAt, wait until <ISO time> before asking <host> again (un bloqueo rate_limit sin un Retry-After lo dice); para un corte, the content was cut at character <n>; ask for rawHtml or a narrower includeTags; para una advertencia client_rendered_suspected, the page fills its data with JavaScript; the browser lane was tried o ... was not tried; para una advertencia low_content_yield, the http lane's content was thin and the browser lane did not improve it (o was not available) ; pass waitFor (up to 60000 ms) or a longer timeout with the browser lane available, or actions (a click, a scroll, a wait for a selector) when the data appears after an interaction; bajo fastMode la única frase de esa opción se mantiene sola cuando el escalón HTTP pidió el escalón del navegador; para un http_error que conservó su página, the server answered <status>; the markdown is that error page, not the requested page (un 404 agrega ; check the link, y un 404 sin página dice the server answered 404; check the link); para un policy_denied que fue de la política de salida en lugar de robots.txt (un evento de traza ssrf_denied o governance_refusal), the egress policy refused <host> (<reason>) and nothing was fetched; Octocrawl reaches public addresses, and a local server the addresses its policy allowlists; para una página que el carril http obtuvo blocked o un error HTTP y luego el carril del navegador local sirvió, the http lane got <status>/<reason> (HTTP <n>) from <host> and the local browser lane served the page; expect other pages of <host> to need the browser lane too (leído del intento http del resumen de escalera; el salto ordinario de la escalera después de una respuesta http delgada o vacía no lleva ninguno); para un tls_error, the certificate of <host> did not verify and Octocrawl keeps verification on; a local server takes skipTlsVerification for one request, recorded in the trace and a tls_unverified warning, and a hosted server refuses it; para un timeout, no lane answered within the request's deadline; raise timeout (up to 300000 ms); para un resultado partial, the result is partial: the deadline passed with this much of the page read; raise timeout (up to 300000 ms) for the rest; para empty_unverified sin una advertencia de shell o contenido delgado, Octocrawl found no main content on the page; onlyMainContent: false returns the whole page's Markdown as content, and includeTags names the elements to read instead (un PDF sin capa de texto: the PDF has no text layer, and Octocrawl runs no OCR); para un json incompleto, json is incomplete: the required field(s) <paths> ... not found on the page; modelFallback fills what the page does not state when the server has W2L_EXTRACT_BASE_URL and W2L_EXTRACT_MODEL, y para una alternativa de modelo que no se ejecutó, the json model fallback did not run: <reason>; para un archivo, the response was a <kind> file kept at <path>; markdown is its text layer (un archivo CSV, JSON o texto: markdown is its text as received; un archivo XLSX, XLS o ZIP: it has no markdown; not saved en lugar de la ruta cuando el servidor no guarda archivos). Una advertencia tls_unverified no agrega ninguna, y ninguna sugerencia insinúa sigilo o un cambio de identidad. Las sugerencias provienen de una tabla fija claveada por código de advertencia, estado, evento de traza, problema json y tipo de archivo, nunca del texto de la página, y como máximo cinco permanecen, en el orden de la tabla. Una opción rechazada que Octocrawl no ofrece recibe una sugerencia también en el cuerpo del error (agentHints; agent_hints en /fc): stealth y proxy: "stealth" o "enhanced" (Octocrawl does not offer a stealth mode or stealth proxies; a proxy or session you own (mode authed) is the supported route), ignoreRobotsTxt en un raspado o lote (robots.txt is always read and recorded; on a local server a URL a scrape or batch names is fetched whatever it says, and ignoreRobotsTxt on a crawl or map fetches the links it disallows, on the record) y, en un servidor alojado, skipTlsVerification (a hosted server verifies every certificate; run Octocrawl locally to use skipTlsVerification, which is recorded in the trace and a tls_unverified warning). El W2LError.agentHints del SDK los lleva (vacío cuando no hay ninguno).
Markdown omite los URI data:, como el removeBase64Images de Firecrawl hace por defecto para imágenes: una imagen conserva su texto alternativo y un enlace su texto; removeBase64Images: false (arriba) conserva el URI data: de una imagen como su destino, el de un enlace nunca. Las celdas de tabla y los pies de foto conservan sus enlaces e imágenes con destinos absolutos, como un párrafo, en una línea (un | en una celda, destino incluido, se escribe \|); el énfasis y el código en una celda son texto plano. Una tabla cuyas tablas anidadas contienen al menos la mitad de su texto, o que tiene una sola fila, presenta la página en lugar de contener datos: sus celdas se convierten en párrafos, y solo las tablas de datos dentro de ella se convierten en tablas GFM. Una tabla de datos con una tabla pequeña en una celda sigue siendo una tabla GFM, con la tabla pequeña como texto de esa celda. Una página que Octocrawl extrae por sus tablas (document.strategy: "table", para páginas cuyas tablas contienen al menos la mitad de su texto) conserva como contenido principal el elemento más bajo que contiene todas sus tablas de datos, de modo que los encabezados, pies de foto y texto entre ellas permanecen y lo que queda fuera de ese elemento, generalmente el encabezado, pie de página y columna lateral de la página, no; un menú presentado como tabla (mayormente enlaces, y sin figuras fuera de ellos) cuenta solo cuando es la tabla más grande de la página, y un <h1> solitario que comparte un contenedor con las tablas debajo <body> se conserva con ellas. Cuando el escalón HTTP no encuentra contenido principal y el escalón del navegador luego falla sin una página, la respuesta es el resultado failed/empty_unverified del escalón HTTP con su página (la auditoría de escalera registra ladder_evidence_kept); cuando el plazo termina el escalón del navegador en su lugar, es failed/timeout con esa página, y el evento de traza deadline_exceeded nombra el escalón del que provino (evidence).
Un resultado cuya página fue extraída lleva metadata, lo que el HTML de la página dice sobre sí misma, en respuestas de raspado (completas, compactas y MCP), elementos de lote y páginas de rastreo: title (su <title>), description (<meta name="description">), language (<html lang>, o <meta http-equiv="content-language"> cuando <html> no tiene lang), keywords y robots (esos valores <meta> como están escritos), favicon (el primer <link rel="icon">, como una URL http(s) absoluta resuelta contra la base del documento) y canonicalUrl (<link rel="canonical">, igualmente). Un valor que la página no declara es null; Octocrawl no sustituye og:description, un encabezado o /favicon.ico. Junto a esos siete, metadata lleva las etiquetas Open Graph, Dublin Core y artículo que la página declara, bajo los nombres de Firecrawl y solo cuando se declaran (ausentes de otro modo, nunca null): ogTitle, ogDescription, ogUrl, ogImage (og:image, si no og:image:secure_url, si no og:image:url), ogAudio, ogVideo (igualmente), ogDeterminer, ogLocale, ogLocaleAlternate (cada og:locale:alternate, como lista) y ogSiteName, leído de <meta property="og:…"> luego <meta name="og:…">, ganando la primera etiqueta no vacía, los campos de URL resueltos contra la base del documento cuando se analizan; dcTermsCreated, dcDateCreated, dcDate, dcTermsType, dcType, dcTermsAudience, dcTermsSubject, dcSubject, dcDescription y dcTermsKeywords de <meta name="dcterms.…"> y <meta name="dc.…"> (nombres coincidentes sin distinción de mayúsculas); y publishedTime (article:published_time), modifiedTime (article:modified_time), articleSection y articleTag (cada article:tag, como lista). Cada valor son las propias palabras de la página con espacios en blanco colapsados: sin normalización de fechas, sin cálculos de zona horaria y sin respaldo de etiquetas twitter:*, govuk:*, citation_*, JSON-LD o elementos <time>, de modo que una página que no declara ninguno no recibe ninguno. Un elemento de lote o página de rastreo fallido o bloqueado no tiene metadata. Una respuesta de raspado siempre tiene uno, porque también lleva los hechos de la llamada (párrafo siguiente); en una página que no se leyó como contenido (una página de bloqueo, una página de error, un archivo) sus campos de página son todos null: Octocrawl no declara nada de una página que es evidencia en lugar de la página solicitada. metadata.title es el <title> de la página, a menudo con el nombre del sitio agregado; document.title no cambia: el título del contenido, generalmente el primer encabezado del contenido principal, con <title> solo como su respaldo. /fc pone title, description, language, keywords, robots y favicon en data.metadata cuando la página los declara, y los campos Open Graph, Dublin Core y artículo bajo los mismos nombres, articleTag unido con , como Firecrawl lo escribe y ogLocaleAlternate conservado como lista.
Cada respuesta de raspado (completa y compacta; /fc pone las mismas claves en data.metadata, con creditsUsed: null) lleva los hechos de la llamada bajo los nombres de Firecrawl, junto a los campos de página: scrapeId (un UUID acuñado por llamada POST /v1/scrape o /fc/v1/scrape; la respuesta completa lo repite en el nivel superior), sourceURL (la URL solicitada), url (la URL final, evidence.finalUrl), statusCode y contentType (de la respuesta que respondió url, como en snapshot), proxyUsed ("operator" cuando la solicitud pasó por el proxy de entorno del servidor, leído de evidence.envProxy y los eventos de traza egress_proxy; "user" cuando el registro de cumplimiento del carril del navegador nombra tu propia salida; null de otro modo, nunca una suposición), timezone (la zona IANA que el carril del navegador declara, America/Los_Angeles para la identidad de escritorio y móvil por igual; null para un resultado del carril HTTP, donde no va ninguna zona horaria en el cable), y concurrencyLimited con concurrencyQueueDurationMs (abajo). cacheState y cachedAt no están allí: Octocrawl aún no tiene caché y no informa un miss inventado. El registro de la llamada, sin ningún cuerpo de página, se escribe en <task root>/scrapes/<scrapeId>.json antes de que se envíe la respuesta y lo sirve GET /v1/scrapes/:id (SDK getScrape(id), MCP get_scrape): scrapeId, requestedAt, request (las opciones analizadas, el valor de cada encabezado personalizado reemplazado por su nombre), origin, integration, status, failureReason, blockReason, budgetExceeded, lane, channelsTried, metadata, snapshot, usage (wallMs, totalMs, requestCount, attemptCount, browserMs), warnings y agentHints. Un id desconocido o malformado es HTTP 404 not_found. Cuando el registro no se puede escribir, la respuesta aún lleva su id, el servidor registra scrape_record_unwritten y la traza de la respuesta completa registra ese evento. Los registros se conservan sin retención: un operador alojado poda el directorio. Una captura de Monitor acuña un id para su respuesta pero no escribe ningún registro (una vista previa no persiste nada; la observación de una ejecución es propia del Monitor).
concurrencyLimited es true cuando el techo de concurrencia por origen (W2L_PER_HOST_CONCURRENCY, 1 a 4, por origen objetivo) retuvo un intento del raspado porque cada ranura estaba ocupada, y concurrencyQueueDurationMs es cuánto tiempo esperaron sus intentos por una ranura en total, excluyendo un enfriamiento concurrente y el intervalo mínimo entre solicitudes (cada carril que intentó adquirió su propio permiso, por lo que un raspado que escaló agrega las esperas de ambos carriles). Cada resultado de carril lleva su propia parte como usage.timings.concurrencyWaitMs, presente solo cuando su permiso fue retenido, también en elementos de lote y páginas de rastreo; usage.timings.queueMs conserva su significado, cada espera de programador menos un enfriamiento, e incluye ese tiempo. A diferencia del concurrencyLimited de Firecrawl, que informa un límite por cuenta, el de Octocrawl es la puerta de cortesía por origen, y la propia programación de frontera de un rastreo (su espaciado por host) no se cuenta.
Solicita datos estructurados deterministas con un JSON Schema junto con, o en lugar de, Markdown:
const product = await w2l.scrape('https://www.amazon.com/dp/B08KT2Z93D', {
debug: false,
formats: [{
type: 'json',
schema: {
type: 'object',
properties: {
asin: { type: 'string' },
title: { type: 'string' },
price: { type: ['number', 'null'] },
currency: { type: ['string', 'null'] },
seller: { type: ['string', 'null'] }
},
required: ['asin', 'title', 'price', 'currency', 'seller'],
additionalProperties: false
}
}]
})
Octocrawl mapea los campos de producto admitidos directamente desde HTML vinculado al sujeto, JSON-LD, metadatos y evidencia del DOM. Una clave de nivel superior que ningún hecho de este tipo cubre se empareja con las etiquetas de la propia página, filas de tabla de dos celdas th/td y pares dt/dd en el contenido principal, comparadas sin distinguir mayúsculas, espacios ni puntuación (Number of reviews rellena numberOfReviews; Price (excl. tax) rellena price cuando ninguna etiqueta es exactamente Price). title (o pageTitle) toma el título del contenido, url (o finalUrl) y requestUrl la URL final y solicitada de la obtención, y pageType el tipo de página que Octocrawl clasificó. Un número se lee solo de texto que es una cantidad, como £51.77 o 1.299,00 €, nunca de texto como HL-1, 4.7 out of 5 o una URL (ver más abajo); las etiquetas que indican valores diferentes dejan el campo fuera con un problema field_ambiguous. Un campo anulable faltante es null con un problema field_unavailable; un campo obligatorio faltante se omite con un problema missing_required y el resultado es incomplete. Cada valor tiene una entrada evidence (el mismo mapa es evidenceRecord.fieldEvidence): la tabla o lista de la etiqueta, fila y etiqueta; dom con h1[0] (el primer encabezado del contenido principal) o title (el <title> de la página) para el título; fetch con finalUrl o requestedUrl para una URL; inferred con document.pageType para el tipo de página, y con document.product.images (prices, variants, specifications) para una lista o mapa que el extractor de producto informó vacío, ya que nada en la página localiza una ausencia; model para un valor que escribió el respaldo del modelo. json.evidence también cita el texto del que se leyó cada número (text, con espacios en blanco colapsados); el Registro de Evidencia conserva source y locator.
Los números se leen tal como los escribe la página. Una cantidad es un signo opcional, un símbolo o código de moneda antes o después del número (€, EUR, US$, kr, 円, o un símbolo antes y un código después, como en $12.99 USD) y un número: su separador decimal es . o ,, sus miles se agrupan con ., ,, un espacio (también espacios sin separación y sin separación estrechos) o un apóstrofo en grupos de tres, o en grupos lakh de India, y ,- o .– después de él termina una cantidad completa. Así, 12,99 € es 12.99; 1.299,00 €, 1 299,00 €, CHF 1'299.– y $1,299.00 son 1299; ₹1,29,999 es 129999. Un solo . o , antes de exactamente tres dígitos (1.299 €, $1,299) es 1299 en una notación y 1.299 en la otra, por lo que se lee solo cuando el valor lo resuelve: un recuento de reseñas es entero, también lo es una cantidad en una moneda sin unidades menores (JPY, KRW, ISK, VND, CLP, ₩, 円, en el texto o como priceCurrency de la página), y un precio JSON-LD o product:price:amount escribe . como su punto decimal. Octocrawl no adivina a partir del idioma, la moneda o el dominio de la página: una página en inglés de una tienda alemana puede escribir 1.299 €, las tiendas irlandesas escriben €1,299, y una página en alemán puede citar $1,299. Tal número, y un precio de producto que no es un número en absoluto (Call for price), se omite, o null cuando el campo es anulable, con un problema field_unavailable que cita el texto y dónde está (uno obligatorio también recibe missing_required); solicitado como cadena, el campo es el texto. Una cantidad en la lista prices del adaptador de Amazon que no se puede leer permanece como su texto, con un problema field_unavailable en /prices/<i>/amount.
El esquema puede usar el subconjunto de JSON Schema que Octocrawl puede honrar, que cubre lo que Pydantic model_json_schema() y zod-to-json-schema suelen escribir:
- Estructura:
type(uno o una lista),properties,required,items(un esquema),additionalProperties,enum,const,$reflocal (#,#/$defs/…,#/definitions/…u otro puntero al esquema) con$defsodefinitions, yanyOf/oneOfde un esquema y{ "type": "null" }(elOptionalde Pydantic) o de tipos primitivos solamente. - Verificado en el resultado, nunca usado para rellenar un valor:
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf,minLength,maxLength,pattern,minItems,maxItems,uniqueItems, yenum/const. Un valor de página que rompe uno permanece endatay el resultado esincompletecon un problemafield_unavailableque cita la verificación (el respaldo del modelo, cuando está activado, puede reemplazarlo). Unpatterntiene como máximo 2,000 caracteres, y uno que puede retroceder catastróficamente, como^(a+)+$, se rechaza coninvalid_request. El resto se ejecuta en el motor de tiempo lineal de V8, excepto un patrón con una búsqueda alrededor, una referencia inversa, una repetición contada por encima de 16 (como[0-9a-f]{32}), un escape\p{…}o\u{…}o un carácter fuera del Plano Multilingüe Básico, y cualquier texto que contenga tal carácter (un emoji): esos se comparan solo contra texto de hasta 2,048 unidades de código UTF-16 y dentro de 100 ms, y el texto que no pueden decidir cuenta como romper el patrón. - Aceptado y no aplicado:
title,description,$comment,examples,deprecated,readOnly,writeOnly,format(no verificado),default(nunca rellenado: un campo que la página no proporciona permanece faltante), y solo en la raíz$schema(draft-07, 2019-09 o 2020-12) y$id. - Límites: 64 KiB, 8 niveles de anidamiento y 100 propiedades.
Cualquier otra cosa se rechaza con HTTP 400 unsupported_parameter, cuyo details.parameters nombra la palabra clave donde se envió (por ejemplo formats[0].schema.properties.author.allOf): allOf, not, if, patternProperties, prefixItems, el nullable de OpenAPI, una unión de objetos, matrices o referencias, una palabra clave distinta de una anotación junto a $ref, $schema o $id debajo de la raíz. Un valor malformado, como un pattern inválido o un $ref que no se resuelve, es invalid_request. Un formato json necesita un esquema: { "type": "json", "prompt": "…" } solo se rechaza con invalid_request, porque Octocrawl no extrae JSON sin uno (eso requeriría un modelo para cada página); prompt solo instruye al respaldo del modelo.
El respaldo del modelo es opcional con modelFallback: true y se ejecuta solo cuando falta un campo obligatorio o un valor rompe el esquema; configure un endpoint compatible con OpenAI a través de W2L_EXTRACT_BASE_URL, W2L_EXTRACT_MODEL y W2L_EXTRACT_API_KEY opcional. Sin esas variables, el contenido de la página nunca se envía a un modelo y el resultado JSON informa model_unavailable. El modelo recibe el Markdown del contenido principal y los valores ya leídos. Rellena solo lo que falta, o reemplaza un valor de página que rompe el esquema; cada valor leído de la página conserva su valor y evidencia sea cual sea la respuesta del modelo, y cada valor que el modelo escribió tiene evidencia model. La solicitud usa salidas estructuradas estrictas (json_schema con strict: true) con una copia segura para modo estricto del esquema: cada objeto cerrado, cada propiedad obligatoria y las opcionales anulables, aserciones y anotaciones omitidas. La respuesta aún se verifica contra su esquema, con una ronda de reparación, y un null que su esquema no permite se descarta como "no encontrado". Un esquema que el modo estricto no puede expresar, como un objeto sin properties, se envía tal cual sin modo estricto; json.modelUsage.strict dice cuál se usó y strictReason por qué no.
Ejecute la línea base fija de 10 productos, tres rondas de Amazon MCP con:
node scripts/section-b/amazon-public-state.mjs
npm run baseline:amazon -- --concurrency 1
npm run baseline:amazon -- --concurrency 2
# After 1 and 2 are comparable and unblocked:
npm run baseline:amazon -- --concurrency 4
La configuración usa una preferencia de entrega pública anónima de Singapur solo para este punto de referencia. La ronda 1 fija el contexto observado; los registros posteriores no observados o de región/moneda no coincidentes permanecen en el informe y no cuentan como comparables. Los informes y el HTML sin procesar permanecen bajo .w2l/amazon-baseline/ ignorado; el manifiesto de URL y el esquema están versionados. El resultado firmado de diez productos pasó con concurrencia limitada, pero Amazon sigue en beta pendiente de las puertas de promoción 100/1000. La línea base anterior es histórica.
El comando de concurrencia 1 puede salir con código distinto de cero porque su mediana de diez páginas supera los 20 segundos; inspeccione su informe para comparabilidad y bloqueo antes de continuar a 2. La ejecución firmada tuvo 37.93 segundos a 1, 19.92 a 2 y 12.39 a 4.
Esta porción firmada de Amazon se fusionó en main mediante PR #52, después del prelanzamiento de la fuente v0.4.0-rc.1, por lo que ese prelanzamiento no lo contiene.
Clientes Firecrawl v1 (compatibilidad parcial): establezca la URL base en http://127.0.0.1:8787/fc para que /v1/scrape y /v1/crawl lleguen al shim. El shim de scrape mapea url, los formatos markdown, links, html, rawHtml, images y screenshot (screenshot@fullPage y una entrada { type: "screenshot", fullPage, quality, viewport } también, devuelta como el URI de datos data.screenshot) y una entrada { type: "attributes", selectors }, onlyMainContent, includeTags, excludeTags, waitFor, timeout, headers, mobile, skipTlsVerification, fastMode, blockAds, removeBase64Images las opciones de caché maxAge, minAge, storeInCache y lockdown (un maxAge omitido no reutiliza nada; un scrape lockdown sin resultado almacenado es HTTP 404 SCRAPE_LOCKDOWN_CACHE_MISS), y proxy como la elección de acceso (basic es standard; stealth y auto son enhanced, que un servidor sin una concesión de acceso de nivel mejorado rechaza con HTTP 400); el shim de crawl mapea url, limit, maxDepth, includePaths, excludePaths, regexOnFullURL, ignoreQueryParameters, deduplicateSimilarURLs, crawlEntireDomain (y el allowBackwardLinks de v1), allowSubdomains, allowExternalLinks, sitemap (y el ignoreSitemap de v1: true es skip, false es include; sitemapOnly: true es only), maxConcurrency, webhook (sobre el webhook de trabajo nativo, recibiendo el receptor la forma de carga útil de Firecrawl) y el mismo scrapeOptions. Las cinco opciones de ejecución siguen las reglas de Octocrawl, no las de Firecrawl: un User-Agent o Cookie en headers es HTTP 400, skipTlsVerification se rechaza en un servidor alojado, y fastMode devuelve el veredicto del nivel HTTP en lugar de un render. removeBase64Images (predeterminado true) conserva el texto alternativo de una imagen donde Firecrawl escribe un marcador de posición, y false conserva el URI data:. Cualquier otro parámetro o formato (location, json, un scrapeOptions.actions de crawl, ...) se rechaza con HTTP 400 y success: false, nombrándolo. Instantánea 2026-09-18; diferencias conocidas en docs/firecrawl-shim.md. La compatibilidad de Firecrawl Search / Interact / Agent / Monitor no está implementada. Las API nativas de Monitor y Delivery de Octocrawl usan sus propios contratos.
Monitores continuos y entrega de eventos
El SDK nativo incluye paginación/cancelación de Crawl, creación/revisiones/ejecuciones/control de Monitor, y destinos de entrega/estado/reintento. El ejemplo ejecutable utiliza una fuente de precios controlada, captureMode explícito, líneas base validadas, solicitudes HTTP condicionales y eventos persistidos. El receptor de webhooks almacena recibos de eventos y aplica una proyección de producto versionada de forma transaccional.
La API, el programador de Monitor y el trabajador de entrega comparten una base de datos de control persistente.
npm run local:mcp:install gestiona los tres para usuarios locales de MCP. w2l-api
(npm run api) ejecuta un trabajador de entrega propio desde que llegaron los webhooks de trabajos,
por lo que las entregas de Monitor y de trabajos también salen del proceso de la API; el trabajador
independiente que aparece a continuación sigue ahí para un despliegue que lo quiera separado, y ejecutar
ambos es seguro (una entrega se arrienda y se protege con fencing). Para un despliegue de API independiente, ejecute
el trabajador de Monitor en una terminal separada con el mismo W2L_TASK_ROOT que la API:
export W2L_TASK_ROOT="$PWD/.w2l/api"
npm run monitors:worker
El trabajador de Monitor usa por defecto la política de red de fuentes públicas. Para la fuente local controlada en el ejemplo de incorporación, establezca explícitamente W2L_MONITOR_NETWORK_MODE=local en esa terminal del trabajador. Un trabajador que se ejecuta localmente no hereda un acceso de red más amplio de la API o la base de datos.
export W2L_TASK_ROOT="$PWD/.w2l/api"
npm run delivery:worker
Consulte incorporación para el receptor HTTPS, la autenticación, la configuración del trabajador y el ejercicio de reinicio de entregas pendientes. La congelación de fuentes de la Puerta 2–4 99894bd636ecafd254a7c7bc79d26e9a97fa9199 está en main a través de PR #50 y se publica como prelanzamiento de fuente v0.4.0-rc.1. Clone main o verifique esa etiqueta. Los paquetes del espacio de trabajo siguen siendo privados y la instalación humana independiente sigue pendiente.
El registro de aceptación de la Puerta 2–4 enlaza la evidencia de caída de proceso, reclamación concurrente, HTTPS público e instalación limpia del agente. La aceptación de ingeniería de las Puertas 2/3 pasó; la Puerta 4 espera a un humano que no sea el autor, y la validación externa de dos semanas/uso repetido de la Puerta 5 no ha comenzado. npm run package:handoff captura la fuente de revisión con hashes por archivo. El archivo probado existente es una instantánea pre-commit conservada, no un paquete de ediciones posteriores de la hoja de ruta.
El MCP de Monitor/Entrega C2 y su flujo de trabajo local de primer uso HTTPS están implementados. C3 tiene un proceso unificado e implementación autenticada de Streamable HTTP, experimental y no desplegada (configuración archivada). B1/B2 y C1 siguen en_progreso para sus puertas operativas/de adopción más amplias. Consulte el tutorial de primer uso y la evidencia local fechada.
Archivos: PDF, CSV, XLSX, ZIP, JSON
Scrape, elementos por lotes, páginas de rastreo, MCP y /fc toman una URL que responde con un archivo de la misma manera que una página web. Una respuesta 2xx es un archivo cuando su Content-Type dice PDF, CSV (incluidos los tipos +csv como el SDMX-CSV de Eurostat), JSON (y +json), texto plano, XLSX, XLS o ZIP; cuando no dice nada útil (application/octet-stream y similares, o ninguno), los bytes deciden: un encabezado %PDF- en los primeros 1024 bytes, un encabezado ZIP (un XLSX cuando el archivo se llama .xlsx), un encabezado OLE llamado .xls, o texto llamado .csv, .json o .txt. Un encabezado PDF al inicio anula text/html o text/plain, y un documento HTML enviado como text/plain sigue siendo una página. El nombre es el nombre de archivo Content-Disposition, si no, la ruta de la URL.
- Guardado tal como se recibe, nunca enviado al navegador. El carril HTTP guarda cada byte en
<W2L_TASK_ROOT>/files/<sha256>.<ext>(.w2l/api/files/por defecto;npm run scrapeusa el mismo lugar,npm run crawlsu directorio de tareasfiles/), de modo que los mismos bytes se almacenan una vez por muchas URLs que los sirvan. Un archivo nunca escala al navegador, sea cual sea su resultado. El bloquefiledel resultado indica el tipo, cómo se detectó, elContent-Typetal como se recibió, el tamaño declarado y recibido, el SHA-256, la ruta y, para un PDF, sus páginas;evidence.rawBodySha256y elrawSha256del Registro de Evidencia son el SHA-256 de los bytes. Una solicitud rechazada antes de obtener cualquier cosa (robots.txt, política, DNS) no guarda nada, y tampoco lo hace una respuesta no 2xx. - El carril del navegador captura la descarga. Cuando el navegador es el primer peldaño (
waitFor) o de otro modo llega a un archivo, toma el archivo de la descarga que inicia la navegación, o de la respuesta que muestra (JSON, texto), en lugar de fallar conDownload is starting, y guarda los mismos bytes de la misma manera. - Límite de tamaño.
W2L_MAX_FILE_BYTESestablece el archivo más grande en bytes (por defecto 52 428 800, 50 MiB; como máximo 524 288 000, 500 MiB; cualquier otra cosa detiene el servicio al inicio). Una solicitud, lote o rastreo puede reducirlo conmaxFileBytes, nunca aumentarlo (un valor mayor es HTTP 400invalid_request). Un archivo que supera el límite esfailedconbody_too_large, con su tamaño declarado enfile.declaredBytescuando el servidor envió uno; no se lee más y no se guarda ni trunca nada. Las páginas web mantienen el límite de cuerpo de 10 MiB. - Otros tipos binarios (imágenes, audio, video, fuentes, documentos de Word y PowerPoint, otros archivos) son
failedconunsupported_content_type: no se descargan, no se guardan, no se envían al navegador. - Un cuerpo que se detiene o se interrumpe después de los encabezados es
failedcontimeoutoconnection_error, sin guardar nada (fue un error interno antes).
Qué devuelve cada tipo:
| Tipo | markdown | Estado |
|---|---|---|
La capa de texto, una línea <!-- page N --> antes de cada página (abajo) | success con texto; partial cuando el límite de páginas (1000), el presupuesto de tiempo (60 s, o menos cuando el timeout del scrape está más cerca) o una página ilegible lo detuvieron antes; failed/empty_unverified cuando ninguna página tiene capa de texto (un escaneo: sin OCR); failed/parse_error cuando no se puede abrir (sin encabezado PDF, cifrado, malformado); failed/timeout cuando no se abrió a tiempo | |
| CSV, JSON, texto | El texto tal como se recibió, decodificado por su marca de orden de bytes, su juego de caracteres declarado o UTF-8 (la marca eliminada) | success; sin texto y con una advertencia text_not_decoded cuando los bytes no son válidos en esa codificación |
| XLSX, XLS, ZIP | null | success; el archivo es el entregable. Tablas → El análisis CSV y XLSX viene más tarde |
| Cualquiera, sin bytes | null | empty_verified |
onlyMainContent, includeTags y excludeTags no se aplican a archivos, y waitFor no se espera una vez que llega el archivo. Un resultado de archivo no tiene document ni metadata ni links, y su html y rawHtml, cuando se solicitan, son null. El Registro de Evidencia lista el archivo en artifacts como { kind: "file", path, sha256, bytes, contentType } y nombra el extractor pdf-text (PDF_TEXT_VERSION) para un PDF o file-text (FILE_TEXT_VERSION) para otro archivo; outputSha256.markdown cubre el Markdown entregado.
La extracción JSON lee un PDF de forma determinista: una clave de esquema se compara, como en una página web, con las etiquetas de las líneas Label: value del PDF (por ejemplo, KPI 2: Reduction of carbon intensity llena kpi2), y fieldEvidence da a cada campo { source: "pdf", locator: "page N \"label\"" }. Las etiquetas que indican valores diferentes dejan el campo fuera con field_ambiguous; la prosa y las celdas de tabla no se leen, los metadatos del PDF no se usan y modelFallback no se aplica al texto PDF (un problema model_unavailable lo dice), por lo que un campo no encontrado se informa como faltante, nunca se adivina. El texto PDF se ejecuta en el hilo del proceso de la API: el informe de 304 páginas de la IEA tarda menos de un segundo.
Texto PDF
pdfToMarkdown(bytes, options?) en packages/extract-tf convierte los bytes de un PDF en Markdown con números de página, de modo que una figura citada de un informe pueda rastrearse hasta su página. Scrape, batch, crawl, MCP y /fc lo usan para cada PDF que obtienen (consulte Archivos).
Qué hace:
- Lee la capa de texto propia del PDF con Mozilla pdf.js (
pdfjs-dist6.3.289, Apache-2.0), en Node, sin renderizado. - Comienza cada página con una línea
<!-- page N -->, siendo N la posición de la página en el archivo, y devuelvepages[]: eltextde cada página, sulabelimpreso cuando el PDF declara uno, y los desplazamientosstart/endde ese texto en el Markdown.pdfPagesForSpan(pages, start, end)nombra las páginas de las que proviene cualquier tramo del Markdown. - Reconstruye líneas, espacios y párrafos a partir de posiciones de texto, lee páginas de varias columnas columna por columna y mantiene las filas de tabla como líneas. Una palabra con guion al final de línea se une; el guion se elimina solo donde el documento escribe la palabra sin él en otro lugar.
- Informa
infotal como el PDF lo declara (título, autor, productor, fechas, idioma), nulo donde no declara nada.
Qué no hace:
- Sin OCR: una página sin capa de texto (un escaneo) vuelve vacía con una advertencia
no_text_layer. - Sin reconstrucción de tablas: las celdas se convierten en líneas de texto, y cada resultado con texto lleva
tables_unverified. - Los encabezados y pies de página permanecen en el texto a menos que
repeatedLines: 'remove', que lista las líneas eliminadas por página. maxPages(por defecto 1000) ytimeBudgetMs(por defecto 60 000, verificado antes de cada página) se detienen con una advertenciapage_capotime_budgety las páginas leídas hasta ahora. La entrada cifrada, malformada y no PDF devuelve{ ok: false, error: { code, message } }en lugar de lanzar una excepción.
Se verifica en 10 informes públicos, seis de ellos los PDF del usuario semilla: manifiesto, node research/pdf-corpus/run.mjs, ejecuciones en research/pdf-corpus/runs/.
Scrape, batch y crawl toman el parsers de Firecrawl para elegir cómo se lee un PDF (REST, SDK, MCP y /fc); otros archivos no se ven afectados:
- Ausente: la capa de texto de cada PDF, con los valores predeterminados anteriores y marcadores de página.
[]: sin texto PDF. El archivo se guarda tal como se recibe y el resultado essuccessconmarkdown: nully una advertencia de archivopdf_not_parsed, como para una hoja de cálculo.- Una entrada
pdf, la cadena"pdf"o{ "type": "pdf", "mode", "maxPages", "pages", "pageMarkers" }:modeesfastoauto, ambos el lector de capa de texto;ocry el analizadorimagese rechazan con HTTP 400 por nombre, ya que Octocrawl no ejecuta OCR.maxPages(1 a 10 000) lee las primeras páginas. Un documento cortado por el propiomaxPagesde la solicitud permanecesuccess, confile.pdf.pagesRead,pageCounty una advertenciapage_capque dice cuánto se leyó; solo el corte del límite predeterminado espartial.pages: trueagregapages: [{ pageNumber, markdown }], el texto de cada página tal como el Markdown lo tiene, sin su marcador (respuestas de scrape completas y compactas, elementos de lote, páginas de rastreo,/fcdata.pages).pageMarkers: falsedeja fuera las líneas<!-- page N -->; los desplazamientosfile.pdf.pagesaún ubican cada página en el Markdown. De forma nativa están activados por defecto; en/fcestán desactivados a menos que se soliciten, como en Firecrawl.
Una segunda entrada, una clave desconocida o un valor fuera de rango es HTTP 400 nombrándolo.
Benchmark
Ejecute la suite de pruebas completa contra la línea base HTTP simple:
npm run bench
Salida esperada:
Subject: bare-http
Cases: 30
Status matches: 17/30
Contentful: 20
False successes: 12
False success rate: 60.0%
La línea base HTTP simple tiene intencionalmente una alta tasa de falsos positivos (sin extracción de contenido, sin detección de desafíos, sin manejo de redirecciones). Un sujeto de producción debería superar estos números.
Rendimiento y recuperación de caídas
npm run bench:throughput mide páginas por minuto y tiempo por página a través del proceso de API en un sitio de bucle local de 20 hosts: el carril HTTP con 32 trabajadores sobre 1,000 URLs, y el carril de navegador con 8 sobre 200. Los primeros resultados, con lo que omiten, están en docs/benchmarks/2026-10-03-throughput.md. npm run verify:batch-crash-1000 mata la API con kill -9 a mitad de un lote de 1,000 URLs sobre 20 hosts, la reinicia en la misma raíz de tarea y verifica que el lote reanudado no pierde ninguna URL ni registra ninguna dos veces. npm run verify:serve-smoke inicia octocrawl serve, extrae una página y un PDF, detiene el servidor a mitad de un lote, lo reinicia y verifica que el lote se complete. CI ejecuta la prueba de bloqueo en Linux y la prueba de servicio en Windows. Las tres necesitan los paquetes compilados (npx tsc -b).
El motor de la API ejecuta W2L_WORKER_COUNT páginas a la vez, un entero de 1 a 64 (predeterminado 4). Ese límite está por encima de los límites por host: W2L_PER_HOST_CONCURRENCY y W2L_PER_HOST_MIN_DELAY_MS aún se aplican a cada host, por lo que más trabajadores solo ayudan entre hosts. El maxConcurrency de un rastreo puede llegar hasta el número de trabajadores.
Estructura del Repositorio
packages/
contracts/ TypeScript types and ground-truth schema (MIT)
fixtures/ HTTP server with 56 ground-truth test cases
http-core/ robots.txt parser (ReDoS-resistant)
runtime/ TaskStore, frontier, bounded crawl orchestrator
bench/ Benchmark runner, ladder CLI (w2l-ladder), scoring
cli/ w2l: scrape, crawl, batch, map, serve (AGPL)
api/ REST server (AGPL)
sdk/ TypeScript client (MIT)
mcp/ stdio and restricted Streamable HTTP MCP server (AGPL)
python/ Python client octocrawl-client (MIT)
examples/monitor-workflow.ts Runnable Monitor + Delivery SDK workflow
examples/webhook-receiver.ts Durable idempotent sample receiver
ROADMAP.md Current phase plan
docs/
onboarding.md Install, Crawl, Monitor, HTTPS events and recovery
mcp-first-use.md Conversational Monitor/Delivery and hosted pilot setup
independent-developer-acceptance.md Pending human Gate 4 run sheet
roadmap/section-a-foundation.md Section A phases and A4 gate
roadmap/section-b-continuous-data.md Section B future direction
roadmap/section-c-delivery.md Section C future delivery direction
archive/ Earlier plans and the hosted-pilot setup: PHASE1_ENGINEERING_NOTES.md (decision log), PRODUCT_PLAN_V2.md, PRODUCT_STRATEGY.md, hosted-mcp-pilot.md, render.yaml
firecrawl-shim.md Firecrawl v1 scrape/crawl snapshot + diffs
benchmark-gate.md Phase 3 comparator versions, evidence contract, and blockers
Hoja de Ruta
- Contratos y esquema de verdad fundamental
- Servidor de accesorios con 56 casos de verdad fundamental
- Corrección de ReDoS en robots.txt (coincidencia de globos basada en tokens)
- Canalización de referencia con línea base HTTP pura
- extract-tf + HTML→Markdown después de la extracción
- Carril de navegador (Playwright) y escalera HTTP → navegador → proveedor
- Paquete de identidad honesta (UA / sugerencias / locale / viewport deben coincidir)
- CLI de producto
octocrawl(@octocrawl/cli,octocrawl):scrape,crawl,batch,mapyserve, cada opción de API como una bandera -
octocrawl crawl+ reanudación de punto de control SQLite - API REST + SDK de TypeScript (
POST /v1/scrape,POST /v1/crawl,GET /v1/crawl/:id, páginas/errores de rastreo paginados, cancelar) - Servidor MCP (
scrape,crawl,get_crawl, páginas/errores paginados y cancelar sobre REST) - Respuestas de extracción MCP compactas, extracción directa de JSON/JSON Schema estructurado y adaptador/línea base de sujeto de Amazon
- Shim de migración
/scrape/crawlde Firecrawl (instantánea 2026-09-18; no es una capa de compatibilidad) - Contabilidad de escalera a nivel de tarea, intentos por canal preservados y campos honestos de costo/evidencia desconocidos
- Trabajadores de múltiples páginas limitados, programación de host compartida, asentamiento condicional del navegador y reutilización de recursos en tiempo de ejecución
- Fase 1 Puerta de Fiabilidad Local: suite de pruebas completa con Chromium y GitHub Actions
- Fase 2 Referencia de calidad L0-L2: escalera Octocrawl, finalización verificada, éxito falso, P95, escalada e informes por niveles
- Fase A4 arnés de tareas reales: manifiestos de conocimiento de IA e información de producto, aserciones de campo, consistencia de repetición, retención y registros de costo/evidencia
- Fase A4 puerta de tareas reales: 100-200 páginas permitidas, tiempo de corrección humana, evidencia de tareas repetidas y taxonomía completa de fallos
- Fase A4 expansión diagnóstica: 20 tareas reales, 11 dominios, 40 ejecuciones repetidas y resultados de retención
- A6 rebanada de escala: 100 páginas, 10 dominios, dos ejecuciones; retención etiquetada no es independiente
- A6 evidencia de recuperación/instalación registrada: interrupción-reanudación perdió 0 URLs; primera tarea de clonación limpia en la misma máquina; registro histórico de corrección de 18 minutos carece de confirmación humana; USD facturado desconocido
- A6 excepciones diferidas registradas: la instalación del segundo desarrollador está diferida, no aprobada; USD facturado es desconocido, no cero
- A6 aprobación incondicional aún necesita una segunda instalación humana
- Informe de puerta A5/A6: alfa condicional; USD facturado sigue siendo desconocido
- Contrato de ejecución de Puerta 2, recuperación real de procesos, cambios controlados/caché y aislamiento de Monitor
- Entrega HTTPS duradera de Puerta 3, reintento del mismo evento, deduplicación y recuperación de reinicio
- Puerta 4 SDK, documentación, ejemplos e instalación limpia de agentes
- Puerta 4 instalación humana independiente de no autor y flujo de trabajo completo
- C2 Monitor/Delivery MCP y flujo conversacional local de primer uso
- C2 n8n e interfaz de tareas estrecha
- C3 proceso unificado de instancia única e implementación autenticada de Streamable HTTP
- C3 MCP alojado: implementación, aceptación de inicio de sesión y simulacro de reinicio alojado (código experimental; configuración archivada en docs/archive/hosted-mcp-pilot.md; el alojamiento es un elemento P5)
- Puerta 5 dos usuarios de prueba externos, dos semanas, uso repetido y consumo real posterior
- Arnés de Puerta de Referencia de Fase 3: ejecución fija de Octocrawl, evidencia de comparador y decisión bloqueada hasta comparadores reales
- Puerta de Egreso Alojado: aplicación de política de subrecursos del navegador y vinculación de DNS a conexión
Ver ROADMAP.md para el plan de fase actual; la hoja de ruta de la Sección A/B/C está archivada en docs/roadmap/sections-abc-roadmap-2026-09-28.md. PRODUCT_PLAN_V2.md sigue siendo el plan detallado histórico.
Contribuciones
Usamos el Certificado de Origen del Desarrollador (DCO) en lugar de un CLA. Cada commit necesita una línea Signed-off-by:
git commit -s -m "Your commit message"
Ver CONTRIBUTING.md para detalles.
Licencia
Código del lado del servidor, el CLI (@octocrawl/cli, octocrawl) y el servidor MCP (@octocrawl/mcp): AGPL-3.0
Bibliotecas de cliente: MIT: el SDK de TypeScript (@octocrawl/sdk, que incluye el @w2l/contracts del espacio de trabajo, también MIT) y el cliente de Python (octocrawl-client)
Ver PHASE1_ENGINEERING_NOTES.md §1.3 para la justificación.
¿Por qué AGPL?
AGPL requiere que las modificaciones implementadas en red permanezcan abiertas. Cualquiera puede bifurcar, modificar y alojar Octocrawl, siempre que comparta esas modificaciones. El diferenciador real es el nombre (marca registrada) y el servicio alojado, no el bloqueo de licencia.