Files
reformix-hackaton/docs/handoff-bot-runtime-simon.md
Carlos Narro 50480b6fc5 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>
2026-06-10 11:17:33 +02:00

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/:id y GET /api/leads/:id/conversacion y GET /api/leads/by-phone. Todos OK. Verificado: POST /perfil con 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 con google/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: WhatsappService escucha un startEmitter y envía el primer mensaje (antes solo registraba la sesión y esperaba al cliente). Persiste estadoWa/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ía msg.key.remoteJidAlt o 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), getOrCreateContext busca el lead en la BD vía GET /api/leads/by-phone y re-registra la sesión.
  • markOnlineOnConnect: true: con false, tras reconectar el dispositivo quedaba "no disponible" y WhatsApp no entregaba los mensajes. Con true empezó 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; ahora anthropic/claude-haiku-4.5 y anthropic/claude-sonnet-4.5 (punto).
  • BAILEYS_AUTH_DIR configurable (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 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).

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ña QR_TOKEN): si connection:open pero inbound no crece cuando el cliente escribe → estás en el estado "zombi".
  • Para volver a recibir: sesión nueva → sube BAILEYS_AUTH_DIR a /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_KEY y FUNNEL_API_KEY en 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.