Equivalentes .command (doble-clic en Finder) de los .bat de Windows: - setup_mac.command: bootstrap con un click (detecta Python 3.10+, crea .venv, instala requirements + Chromium de Playwright, copia .env.example -> .env). - start/stop/restart/start_persistent_profile.command: espejo de los .bat, lanzan el server con nohup usando el python del .venv. - mp_common.sh: helper compartido (raíz, venv, banners). runtime_control.py ahora es cross-platform (IS_WINDOWS): lsof/ps/pgrep/kill en POSIX, netstat/PowerShell/taskkill en Windows. _kill_tree_posix mata el árbol padre+worker de uvicorn con SIGTERM. .venv/ añadido a .gitignore. Docs actualizadas (CLAUDE.md, AGENTS.md, PLAYWRIGHT_SESSION.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
Sesión persistente de Bucéfalo en Playwright
Este documento explica cómo funcionan los scripts del repo que automatizan la UI de Bucéfalo CRM con Playwright (toggle de workflows, renombrar, eliminar, etc.), por qué a veces se cae la sesión, y cómo recuperarla.
Audiencia: quien mantiene scripts/ghl_browser_*.py o usa el dashboard para mutar workflows. Si alguien (humano o Claude) llega aquí buscando "por qué falla el delete/toggle/rename de workflows", este es el lugar.
Por qué necesitamos Playwright
La API oficial de GHL (services.leadconnectorhq.com) no expone todas las operaciones — en particular:
- Toggle Borrador ↔ Publicado de un workflow
- Renombrar un workflow
- Eliminar un workflow
Para esas tres acciones, los scripts automatizan la UI web con Playwright sobre Chromium en modo headless. Toda la lógica vive en scripts/ghl_browser_workflow_manager.py; la sesión se genera con scripts/ghl_browser_session_generator.py.
Dos modos de sesión
Los scripts soportan dos modos de persistencia. Se elige con la variable de entorno GHL_BROWSER_PROFILE_DIR.
Modo 1 — Shared storage_state (por defecto)
- Cuándo se usa: cuando
GHL_BROWSER_PROFILE_DIRno está definida (el caso normal). - Cómo funciona: el archivo
generated/browser/session.jsoncontiene cookies + localStorage. Cada script abre un browser limpio con esas cookies, hace lo suyo, y guarda las cookies actualizadas de vuelta engenerated/browser/session.json. - Pros: simple, soporta scripts en paralelo (el archivo se lee al inicio y se escribe al final, no hay locks).
- Cons: GHL puede interpretar cada arranque como un "navegador nuevo" porque no hay IndexedDB ni cache compartido. Si GHL invalida la sesión por detectar dispositivos múltiples, este modo se cae más rápido.
Modo 2 — Perfil de Chrome persistente
- Cuándo se usa: cuando defines
GHL_BROWSER_PROFILE_DIRapuntando a un directorio. Lo más fácil es lanzar el servidor con start_persistent_profile.bat (Windows) o start_persistent_profile.command (macOS/Linux). - Cómo funciona: Playwright usa
launch_persistent_context()con un perfil completo en disco — igual que un Chrome real. Persiste cookies HttpOnly, IndexedDB, cache, localStorage, service workers. - Pros: GHL trata el perfil como un "dispositivo" estable. Sesión mucho más duradera. Login dura semanas en vez de horas/días.
- Cons: no puedes correr dos scripts en paralelo contra el mismo perfil — Chrome bloquea el directorio mientras un proceso lo usa. Si necesitas ejecuciones concurrentes (raro en este repo), no uses este modo.
Auto-login con 2FA por correo (IMAP)
Si configuras un archivo .env con las credenciales de Bucéfalo + IMAP de tu correo, el ghl_browser_session_generator.py hace todo el login solo: llena email + contraseña, selecciona "Email" en el selector de método 2FA, lee el código del correo vía IMAP y lo pega.
Configuración
- Copia
.env.examplea.env:copy .env.example .env - Edita
.envy rellena:BUCEFALO_LOGIN_EMAIL/BUCEFALO_LOGIN_PASSWORD— credenciales de tu usuario de Bucéfalo.EMAIL_IMAP_HOST— servidor IMAP de tu correo (típicamente el dominio de tu hosting, ej.c1101854.sgvps.net).EMAIL_IMAP_PORT— 993 (IMAPS) por defecto.EMAIL_IMAP_USER/EMAIL_IMAP_PASSWORD— credenciales de la cuenta de correo a la que llega el código MFA.
- Verifica que el
.envesté excluido de Mega/Git (el.megaignorey.gitignoredel repo ya lo cubren).
Comportamiento
- Si están todas las credenciales en
.env, el session generator corre enheadless=True(sin abrir ventana) y completa el login en ~30-60 s. - Si falta alguna, cae al modo manual: abre el navegador visible para que tú completes el login.
- Si quieres forzar el modo manual aunque haya credenciales, usa
--no-auto:python scripts/ghl_browser_session_generator.py --no-auto
Probar el lector IMAP por separado
Si solo quieres validar que IMAP funciona y el parser extrae el código:
python scripts/email_otp_reader.py
Te pide que provoques un código (intenta loggearte a Bucéfalo) y al recibirlo lo imprime.
Seguridad
- El archivo
.envcontiene credenciales en texto plano. Manténlo solo en tu equipo local. - Si crees que el
.envpudo haberse copiado a un lugar no seguro (cloud público, repo público, captura de pantalla), cambia las contraseñas inmediatamente. - Los
.env.exampley.megaignoredel repo están diseñados para evitar que esto pase por descuido.
Cómo arrancar cada modo
Modo 1 (default)
start.bat
Si nunca generaste sesión, en el dashboard ve a la pestaña Workflows GHL y dale al botón "Renovar sesión Bucéfalo". Inicia sesión + MFA en la ventana que se abre — el archivo generated/browser/session.json se crea solo.
Modo 2 (perfil persistente)
start_persistent_profile.bat
El .bat:
- Setea
GHL_BROWSER_PROFILE_DIR=<repo>/generated/browser/profile. - Lanza
python main.pycon esa variable en el entorno.
La primera vez te toca generar el perfil:
- En el dashboard, dale a "Renovar sesión Bucéfalo".
- Login + MFA en la ventana.
- El perfil queda guardado en
generated/browser/profile/. Próximos arranques no piden login.
⚠️ Si quieres volver al modo 1, basta con cerrar el server y abrirlo con
start.bat. Los dos modos no se mezclan: el modo 1 usagenerated/browser/session.json, el 2 usagenerated/browser/profile/.
Síntomas y diagnóstico
"Chromium me cerró la sesión de mi navegador personal"
GHL detectó dos sesiones activas (la tuya y la de Playwright) e invalidó la más vieja.
- Modo 1: muy probable. Cada arranque "huele" a dispositivo nuevo.
- Modo 2: poco probable. El perfil persistente se ve como el mismo dispositivo.
Solución: cambia al modo 2 con start_persistent_profile.bat.
"ERROR: No se pudo interceptar la sesión de usuario de GHL"
El script (ya no usa este flujo, pero el mensaje puede aparecer en versiones viejas) o el flujo DOM no pudo cargar la página porque te redirigieron al login.
Solución: renueva la sesión desde el botón del dashboard. Si pasa muy seguido, cambia al modo 2.
"Sesión Bucéfalo: 49.5 h de antigüedad" en el dashboard
El indicador muestra la antigüedad de generated/browser/session.json. Una sesión vieja no garantiza que esté caducada — GHL puede aceptarla. Pero a partir de 24 h, el dashboard advierte.
- Si la corrida falla, renueva la sesión.
- Si la corrida funciona, el archivo se actualiza solo (el script refresca cookies al cerrar).
"ERROR de navegador: BrowserType.launch: Executable doesn't exist at …"
Faltan los binarios de Chromium. El script intenta instalarlos solo en el primer fallo:
python -m playwright install chromium
"El cambio NO persistió en GHL"
Tras un toggle exitoso visual, la API de GHL sigue devolviendo el estado viejo. Causas:
- El workflow tiene un trigger inválido (Webhook sin URL, etc.) y GHL revierte el cambio en silencio.
- Bug visual de la UI — el script ya hace un reload + revalida automáticamente.
Si el script reporta este error después de los reintentos, abre el workflow en el navegador, revisa los triggers, y vuelve a intentar.
"Proceso interrumpido (signal …)"
El subprocess de Playwright fue matado externamente — típicamente porque reiniciaste el server o cancelaste el task desde el dashboard. El handler de señales:
- Cierra Chromium limpio (sin zombies).
- Refresca cookies en
generated/browser/session.json. - Consulta la API y te dice si el cambio sí se aplicó pese a la interrupción.
Si fue antes del click del switch: no pasa nada, reintenta.
Si fue después: revisa el log — la línea [INTERRUPCIÓN] El cambio SÍ se aplicó te lo dice.
Reglas para mantener los scripts
Si tocas scripts/ghl_browser_*.py:
- No abras un
browser = p.chromium.launch(...)manual. Usa_open_browser(p)— respeta el modo (shared / persistent). - No cierres con
browser.close(). Usa_close_and_save(browser, context)— refresca cookies y limpia el estado del handler de interrupciones. - Registra el browser y context en
_INTERRUPT_STATEjusto después de abrirlos. Si no, los handlers de interrupción no podrán cerrarlos. - No confíes en la UI para validar mutaciones. La fuente de verdad es la API de GHL. Tras cualquier guardado visual, llama a
_verify_status_via_api(...)(o un equivalente) con reintentos. - Selectores frágiles: si GHL cambia el HTML, los selectores hardcodeados (
#cmp-header__btn--save-workflow,.n-switch, textos "Eliminar flujo de trabajo"/"Delete workflow") pueden romperse. Tomar screenshot de debug es el primer paso de diagnóstico (_save_debug_screenshot(page, "label")).
Cómo reportar un error útilmente
Cuando un script de Playwright falla y necesitas ayuda (humano o IA), incluir esta información acorta el diagnóstico de horas a minutos:
- El
error_idcompleto. Aparece en los logs comoerror_id=550af7d5-…. Permite consultar error_log con todo el contexto (return_code, comando exacto, últimas 80 líneas de output). - El
task_idsi lo ves (aparece como(task d819f354-…)). Permite cruzar conscript_runspara ver el estado de la auditoría. - Las últimas 20-30 líneas del log que viste. Aunque el
error_logya las guarda, a veces el truncado pierde la línea más informativa. - Si hay screenshots en
generated/browser/screenshots/con timestamp cercano al fallo: nómbralos. Los scripts guardan capturas en cada punto crítico (delete_no_row_*,switch_no_change_*,post_save_state_*, etc.). - El estado de la sesión: ¿cuánto tiene
generated/browser/session.json? El dashboard te lo dice arriba de la tabla de Workflows. Si tiene más de 24 h, renueva primero y reintenta antes de pedir ayuda. - Qué intentaste hacer: acción (
toggle-status/delete/rename),location_id,workflow_id, y qué esperabas que pasara.
Ejemplo de un buen reporte:
El toggle-status del workflow
67f98059-…enZ64WQKORPVwXb5mn68Effalló a las 14:03. Log diceerror_id=9f58fa5d-…y[ERROR] La tabla de workflows no cargó. Screenshot:generated/browser/screenshots/delete_list_failed_20260523_142713.png. La sesión tiene 12 h, renovada hoy en la mañana.
Con eso ya se puede empezar a diagnosticar sin tener que pedir info de vuelta.
Comportamiento conocido de la UI de Bucéfalo
Documentado también en memory/ghl_ui_quirks.md para sesiones de Claude:
- El builder de workflows carga dentro de un iframe en
client-app-automation-workflows.leadconnectorhq.com. Cualquier selector debe operar sobre ese frame, no sobre la página principal. - El listado de workflows está en otro iframe distinto.
- Al cargar el builder, el switch de Publicar/Borrador arranca en
aria-checked=falsey luego cambia cuando el cliente recibe el estado real. Esperar 25-30 s antes de leer. - El botón "Guardar/Save" dice "Guardado/Saved" por defecto incluso sin cambios. No es señal de estado cargado.
- Aparece un modal "AI Builder habilitado" con botón "Entendido" que tapa el switch — hay que cerrarlo preemptivamente, y a veces reaparece tras el click. Por eso el script reintenta el toggle hasta 3 veces.
- Tras "Guardar", GHL puede tardar hasta 20 s en propagar el cambio a la API. La validación contra API hace 6 reintentos con backoff incremental.
- A veces se necesita un F5 manual para destrabar bugs visuales — el script lo hace automático tras los primeros reintentos.