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>
7.9 KiB
Handoff runtime del bot WhatsApp (Luisa) — para Simón
Estado tras la sesión de depuración del 09-jun. El flujo conversacional funciona end-to-end; quedan dos problemas de runtime del bot que se diagnostican/cierran desde tu lado (tienes acceso a los logs en Dokploy). La parte de la app (EPs) está verificada y no es el problema.
1. Lo que está PROBADO funcionando
- EPs de la app (en
mvp/b2c):conversacion,perfil,calificacion,intento,ingesta,GET /api/leads/:idyGET /api/leads/:id/conversacionyGET /api/leads/by-phone. Todos OK. Verificado:POST /perfilcon el payload exacto del bot ({espacio,rangoM2,estilo,botStep}) →200 {ok:true, actualizado:[...]}y el lead se actualiza. El EP no es el cuello de botella.
- Bot: apertura proactiva, resolución del
@lid, matching del lead por teléfono, y la conversación de cualificación de Luisa (7 turnos reales: cocina → tamaño → estilo → urgencia). - Worker de render (
mvp/image-worker): genera renders congoogle/gemini-2.5-flash-image.
2. Cambios que hice hoy en el bot (mvp/Whatsapp-bot) — para que no te pillen por sorpresa
- Apertura proactiva al recibir
/whatsapp-start:WhatsappServiceescucha unstartEmittery envía el primer mensaje (antes solo registraba la sesión y esperaba al cliente). PersisteestadoWa/botStep+ intento. - Resolución
@lid(resolverTelefono): WhatsApp entrega los mensajes desde una dirección@lid(p.ej.239225534443615@lid), no desde el número. Se resuelve a número víamsg.key.remoteJidAlto el mapa LID→PN de Baileys. Esto era la causa de que el bot ignorara los mensajes entrantes. - Recuperación del lead por teléfono: si la sesión no está en memoria (reinicio),
getOrCreateContextbusca el lead en la BD víaGET /api/leads/by-phoney re-registra la sesión. markOnlineOnConnect: true: confalse, tras reconectar el dispositivo quedaba "no disponible" y WhatsApp no entregaba los mensajes. Contrueempezó a recibir (la conversación de 7 turnos lo demuestra).- Modelos de Claude corregidos en el env: eran
claude-haiku-4-5(guion) → inválidos en OpenRouter; ahoraanthropic/claude-haiku-4.5yanthropic/claude-sonnet-4.5(punto). BAILEYS_AUTH_DIRconfigurable (subcarpeta del volumen) para empezar una sesión limpia sin perder persistencia. Hoy apunta a/app/auth_info_baileys/v2.- Endpoints de operación (servidor de webhooks, puerto 3001):
GET /qr— QR de vinculación como imagen (HTTP Basic; usuario cualquiera, contraseña =QR_TOKEN, que está en el env del bot en Dokploy).GET /debug— estado de conexión + anillo de los últimos eventos entrantes (mismo auth). Útil para verremoteJid/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).
3. Problema A — conexión Baileys inestable (bucle de reconexión)
Evidencia en logs del contenedor (docker logs): el socket se cae cada pocos minutos con
stream:error → conflict {type:"replaced"} y stream:error code 503, y reconecta en bucle
(AwaitingInitialSync → Transitioning to Online → opened connection to WA → ✅ conectado, repetido).
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:
GET https://reformix-bot.dv3.com.es/debug(Basic, contraseñaQR_TOKEN): siconnection:openperoinboundno crece cuando el cliente escribe → estás en el estado "zombi".- Para volver a recibir: sesión nueva → sube
BAILEYS_AUTH_DIRa/app/auth_info_baileys/v3, redeploy, escanea el QR fresco en/qr. Y evita redeployar después (cada reconexión arriesga la recepción).
Camino robusto (acordado con Carlos): migrar el transporte de WhatsApp del bot a Evolution API (ya está como primaria en el stack — NO la WhatsApp Cloud API oficial). Evolution gestiona la conexión y entrega los mensajes por webhook, sin un socket Baileys en-proceso que se vuelva 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.
4. Problema B — el perfil se persiste solo a medias + máquina de estados errática
Verificado en vivo: persistirTurno SÍ funciona cuando se llama —
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.
A revisar: que cada turno con dato extraído llame a persistirTurno, que los valores encajen con
los enums de la app (urgencia alta|media|baja, etc., o POST /perfil da 422 y no guarda nada), y
endurecer la máquina de estados (no re-preguntar lo ya respondido; viabilidad estable).
4bis. Problema C (el que rompe el end-to-end) — el bot NUNCA dispara la generación
Verificado en logs: Luisa termina diciendo "en un momento recibes tu presupuesto" pero no hay
ninguna llamada a ingesta / perfilCompleto en toda la sesión (grep vacío). Es decir, al cerrar la
cualificación el bot no dispara nada: ni render, ni PDF, ni entrega. Es una promesa vacía.
Qué falta (lado bot): cuando la cualificación se completa (estado presupuesto/fin_viable), el
bot debe (1) pedir las fotos del espacio por WhatsApp, (2) subirlas vía POST /api/leads/:id/ingesta
(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.
5. Infra (referencia rápida)
| Servicio | App Dokploy | Dominio |
|---|---|---|
| Bot | reformix-bot (wY4F14fyEslU-4za_JIbi) |
reformix-bot.dv3.com.es (puerto 3001) |
| Worker | reformix-worker (sMQd9zwoyV14q1vm8Vs8U) |
reformix-worker.dv3.com.es |
| App | reformix-b2c (lzHDAuPuubbJu94OrkNS_) |
reformix.dv3.com.es |
- Build Dockerfile desde Gitea, autodeploy en push a
main. Volumen del bot:/app/auth_info_baileys(sesión WhatsApp).OPENROUTER_API_KEYyFUNNEL_API_KEYen el env de cada app. - Contrato de los EPs y enums:
mvp/b2c/api-docs/README.md.
Resumen: la conversación de Luisa funciona (recibe, resuelve @lid, cualifica, responde). Quedan 3
cosas del bot: A) conexión inestable (conflict+503, bucle de reconexión) → Evolution API;
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.