Handoff bot: hallazgos reales de logs (conflict/503, persist parcial, sin trigger)

Vía SSH al VPS + docker logs: la conexión Baileys está en bucle de reconexión
(conflict:replaced + 503); persistirTurno funciona (→ ok) pero solo se llama en
algunos turnos y la máquina de estados se descuadra; y el bot NUNCA llama a
ingesta/perfilCompleto, así que la generación de presupuesto/render/entrega no
se dispara (Problema C, el que rompe el end-to-end).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-10 11:17:33 +02:00
parent face2d3d1b
commit 50480b6fc5

View File

@@ -41,12 +41,19 @@ a los logs en Dokploy). La parte de la app (EPs) está verificada y no es el pro
para ver `remoteJid`/`remoteJidAlt`, si llega algo, y el resultado del matching. para ver `remoteJid`/`remoteJidAlt`, si llega algo, y el resultado del matching.
- Subido el límite de body del worker a 30 MB (las fotos en data URI rompían el 100kb por defecto). - Subido el límite de body del worker a 30 MB (las fotos en data URI rompían el 100kb por defecto).
## 3. Problema A — Baileys deja de recibir tras reconectar ## 3. Problema A — conexión Baileys inestable (bucle de reconexión)
**Síntoma:** justo tras un **escaneo fresco** del QR, el bot recibe mensajes (conversación OK). Pero **Evidencia en logs del contenedor (`docker logs`):** el socket se cae cada pocos minutos con
tras cualquier **reconexión** (redeploy, o auto-reconexión de Baileys al caerse el socket), reporta `stream:error → conflict {type:"replaced"}` y `stream:error code 503`, y reconecta en bucle
`connection: "open"` pero **no recibe nada** (`/debug``inbound: []` aunque el cliente escriba). (`AwaitingInitialSync → Transitioning to Online → opened connection to WA → ✅ conectado`, repetido).
`markOnlineOnConnect: true` ayudó al primer connect pero no lo blinda contra reconexiones. El `conflict: replaced` indica que **otra sesión reclama la misma cuenta** — típico del **solapamiento
en el deploy** (Swarm arranca el contenedor nuevo antes de matar el viejo y ambos usan la misma
sesión del volumen). `markOnlineOnConnect: true` mejoró la recepción pero no arregla una conexión que
se cae sola.
**Mitigación parcial (Baileys):** configurar el deploy del bot como **stop-first / sin solapamiento**
(1 réplica, recreate) para evitar el `conflict`. Pero los `503` son de WhatsApp y seguirán.
**Vía robusta (acordada): Evolution API.**
**Cómo reproducir/diagnosticar:** **Cómo reproducir/diagnosticar:**
- `GET https://reformix-bot.dv3.com.es/debug` (Basic, contraseña `QR_TOKEN`): si `connection:open` - `GET https://reformix-bot.dv3.com.es/debug` (Basic, contraseña `QR_TOKEN`): si `connection:open`
@@ -61,24 +68,31 @@ conexión y entrega los mensajes por **webhook**, sin un socket Baileys en-proce
zombi. El bot pasaría a: (1) recibir mensajes por webhook de Evolution, (2) enviar por su REST. La zombi. El bot pasaría a: (1) recibir mensajes por webhook de Evolution, (2) enviar por su REST. La
lógica de Claude + los EPs de la app se quedan igual; solo cambia la capa de transporte WhatsApp. lógica de Claude + los EPs de la app se quedan igual; solo cambia la capa de transporte WhatsApp.
## 4. Problema B — el perfil extraído no se persiste en el lead durante la conversación ## 4. Problema B — el perfil se persiste solo a medias + máquina de estados errática
**Síntoma:** la conversación avanza (Luisa pregunta espacio→tamaño→estilo→urgencia) pero el lead **Verificado en vivo:** `persistirTurno` SÍ funciona cuando se llama —
queda con `botStep` desfasado y `espacio/rangoM2/...` vacíos. `Lead ... persistido via API: {"rangoM2":"10a20","botStep":"estilo"} → ok`. Pero en una conversación
completa **solo apareció UN `persistido`** (rangoM2); `espacio`, `urgencia` y `presupuesto` no se
guardaron. Y la conversación se descuadra (rechaza a 4500€ → con 8500€ vuelve a preguntar el tamaño →
"preparo presupuesto"). Causa probable: `claudeService.llamarClaude` solo devuelve `entidad`/`nuevoEstado`
en algunos turnos, y la lógica de estado/viabilidad no es determinista.
**Qué sé:** el EP `/perfil` **funciona** con el payload del bot (probado). Así que el fallo es en la **A revisar:** que cada turno con dato extraído llame a `persistirTurno`, que los valores encajen con
llamada del bot. [`LeadsService.persistirTurno`](../mvp/Whatsapp-bot/src/leads/leads.service.ts#L68) los enums de la app (`urgencia` alta|media|baja, etc., o `POST /perfil` da 422 y no guarda nada), y
ya loguea: `Lead X persistido via API: {...} → ok/fallo`. endurecer la máquina de estados (no re-preguntar lo ya respondido; viabilidad estable).
**Qué mirar (logs del bot en Dokploy) durante una conversación:** ## 4bis. Problema C (el que rompe el end-to-end) — el bot NUNCA dispara la generación
- Si aparece `→ fallo`: mira el payload logueado. Sospechosos: un valor de enum inválido (p.ej.
`urgencia` fuera de `alta|media|baja`, o `calidadGlobal` fuera de `basica|media|premium`) hace que **Verificado en logs:** Luisa termina diciendo *"en un momento recibes tu presupuesto"* pero **no hay
TODO el `POST /perfil``422` y no persista nada de ese turno. Mapea los valores que extrae Claude ninguna llamada a `ingesta` / `perfilCompleto`** en toda la sesión (grep vacío). Es decir, al cerrar la
a los enums de la app antes de enviar. cualificación el bot **no dispara nada**: ni render, ni PDF, ni entrega. Es una promesa vacía.
- Si no aparece la línea: `persistirTurno` no se está llamando → revisa que `claudeService.llamarClaude`
devuelva `entidad`/`nuevoEstado`. **Qué falta (lado bot):** cuando la cualificación se completa (estado `presupuesto`/`fin_viable`), el
- Ojo también a si el bot se **reinició a media conversación** (Problema A): tras reiniciar deja de bot debe (1) **pedir las fotos** del espacio por WhatsApp, (2) subirlas vía `POST /api/leads/:id/ingesta`
recibir y el flujo se corta. (items `foto`, `momento:"antes"`), y (3) marcar `perfilCompleto:true` (y/o `finalizar`). Eso dispara en
la app: `PERFIL_WEBHOOK` → worker genera renders → `ingesta finalizar` → PDF + email + entrega WhatsApp.
**La app y el worker ya están listos para esto; solo falta que el bot llame al EP.** Contrato:
[`mvp/b2c/api-docs/README.md`](../mvp/b2c/api-docs/README.md).
## 5. Infra (referencia rápida) ## 5. Infra (referencia rápida)
@@ -94,6 +108,8 @@ ya loguea: `Lead X persistido via API: {...} → ok/fallo`.
--- ---
**Resumen:** el flujo está demostrado funcionando; los 2 problemas son de la capa de transporte/runtime **Resumen:** la conversación de Luisa funciona (recibe, resuelve `@lid`, cualifica, responde). Quedan 3
del bot. Recomendación: cerrar el Problema B mirando los logs de `persistirTurno`, y para el Problema A cosas del bot: **A)** conexión inestable (`conflict`+`503`, bucle de reconexión) → Evolution API;
(estabilidad) migrar a **Evolution API**. Mientras tanto, para una demo: escaneo fresco + no redeployar. **B)** persistencia parcial del perfil + estado errático; **C)** el bot **nunca dispara la generación**
(no llama a `ingesta`/`perfilCompleto`), así que el presupuesto/render/entrega no llega — esto es lo que
rompe el end-to-end y lo que más conviene cerrar. App y worker ya están listos esperando esa llamada.