Files
SKEEN-Proyecto/docs/diagnostico-whatsapp-entrante.md

16 KiB

Diagnóstico — WhatsApp entrante "Sofía no contesta"

Fecha: 2026-09-24 (UTC) · VM SKEEN · Autor: diagnóstico técnico automatizado Alcance: por qué no entran mensajes de pacientes al WACRM y por qué la IA responde (o no) cuando entran.


1. Resumen ejecutivo

  1. El camino de entrada (Meta → Cloudflare → cloudflared → nginx → Next.js/WACRM) está sano y verificado hoy: la suscripción del webhook está active=true apuntando a https://skeen.consultoria-as.com/api/whatsapp/webhook, con el campo messages suscrito, y Meta completó un handshake real contra esa URL el 24/Sep 08:51:54 UTC respondiendo 200 + challenge.
  2. No hay ningún evento de mensaje entrante de Meta desde el 3/Sep. En los logs de nginx retenidos (11→24 sep) los únicos POST de webhook son 6 el 20-22 sep, desde user-agent facebookexternalua, con 200 OK, que corresponden a actualizaciones de estado de nuestros propios mensajes de monitoreo (llegan 1-3 s después de insertar la alerta), no a mensajes de pacientes. No hay ni un POST más desde el 22/Sep 12:30.
  3. La aplicación procesa correctamente un mensaje entrante cuando Meta lo entrega. Se inyectó un webhook firmado con el App Secret real y el pipeline completo funcionó: contacto → conversación → mensaje customer en messages → notificación al bridge → respuesta de Sofía enviada.
  4. Se encontró y corrigió un defecto real que hacía que Sofía NO contestara bien: el bridge usaba max_tokens: 500 contra qwen3.6 (modelo de razonamiento en api.nan.builders). El presupuesto se agotaba en reasoning_content, content volvía null con finish_reason="length" y el bridge respondía el texto de disculpa "Lo siento, no pude procesar tu mensaje." a todos los pacientes. Reproducido y corregido (ver §4); la respuesta verificada ya es la de Sofía.
  5. Hipótesis principal de la falta de entrantes: los pacientes no están escribiendo a este número, o el número no está recibiendo. Evidencia de contexto: el número del CRM es un número piloto ("Consultoria Alcaraz Salazar", +52 1 663 700 6400) y el propio código del bridge dice que falta "el cambio al número de WhatsApp de SKEEN". En toda la vida del WACRM sólo existen 2 conversaciones (la de pruebas de Ivan y la del healthcheck), es decir, el CRM nunca ha operado tráfico real de pacientes.

2. Evidencia recolectada

2.1 Lado Meta (Graph API, token del WACRM)

Token: debug_token → type: SYSTEM_USER, app_id: 1607064673936924 ("Consultoria AS Social"), is_valid: true, expires_at: 0 (no expira), scopes whatsapp_business_messaging, whatsapp_business_management, business_management.

Número 1157212604148350:

display_phone_number : +52 1 663 700 6400      status            : CONNECTED
platform_type        : CLOUD_API               quality_rating    : GREEN
code_verification_status : EXPIRED             throughput        : STANDARD
webhook_configuration.application : https://skeen.consultoria-as.com/api/whatsapp/webhook

WABA 773071042535033 ("Consultoria Alcaraz Salazar"): account_review_status: APPROVED, business_verification_status: verified. Es el único número de la WABA.

Suscripción de la app:

GET /1607064673936924/subscriptions
object=whatsapp_business_account | active=True
callback_url=https://skeen.consultoria-as.com/api/whatsapp/webhook
fields=account_alerts,account_review_update,account_update,calls,message_template_quality_update,
       message_template_status_update,messages,phone_number_name_update,phone_number_quality_update,security

GET /{waba-id}/subscribed_apps → la app 1607064673936924 está suscrita a la WABA.

2.2 Verificación real del webhook (handshake)

Handshake con el verify_token real contra la URL pública: 200 + challenge en skeen.consultoria-as.com y en skeencrm.consultoria-as.com. Token inválido → 403 (comportamiento esperado).

Re-registro forzado el 24/Sep:

POST /1607064673936924/subscriptions   → {"success":true}

Ese POST hace que Meta ejecute un handshake inmediato. En /var/log/nginx/access.log:

127.0.0.1 - - [24/Sep/2026:08:51:54 +0000] "GET /api/whatsapp/webhook?hub.mode=subscribe&hub.challenge=790688494
  &hub.verify_token=skeen_webhook_verify_2026&hub_mode=subscribe&... HTTP/1.1" 200 40 "-"
  "facebookplatform/1.0 (+http://developers.facebook.com)"

→ Meta sí alcanza nuestro endpoint hoy, atravesando Cloudflare → cloudflared → nginx → Next.js.

Nota: los webhooks entran por el túnel cloudflared, por eso $remote_addr es 127.0.0.1; no es tráfico local.

2.3 Logs nginx (todo el histórico retenido: 11→24 sep)

6 POST /api/whatsapp/webhook  200  UA "facebookexternalua"   20/Sep 15:45:21, 16:00:03;
                                                             21/Sep 05:48:04, 08:35:58;
                                                             22/Sep 12:15:37, 12:30:13
2 GET  /api/whatsapp/webhook  403  UA "curl/8.5.0"           pruebas manuales 24/Sep 08:48

Los 6 POST coinciden en el tiempo con las alertas del healthcheck insertadas en messages (p. ej. fila creada 22/Sep 12:30:12 → POST 12:30:13) y esas filas terminaron en status='failed', consistente con status updates de Meta, no con eventos de mensaje entrante (si lo fueran, existirían filas de contacto/conversación/mensaje en la base).

2.4 Base de datos (Supabase supabase-db/postgres)

  • Último mensaje real de paciente: 2026-09-03 00:14:43 ("Hola", conversación f9a45f24). Sólo hay 2 conversaciones en total: la de Ivan (f9a45f24) y la del healthcheck (4557417e).
  • ai_autoreply_disabled = false en ambas conversaciones. No hay filas en ai_configs → la auto-respuesta propia del WACRM no está configurada; el único respondedor de IA es el bridge de Hermes.
  • whatsapp_config: 1 fila, status=connected, phone_number_id=1157212604148350, verify_token coincide con el del .env.local y con el de wacrm.service.

2.5 Logs de la aplicación

journalctl -u wacrm.service (desde 1/Sep):

Sep 21 03:15:50  [send-message] Meta send failed for all variants: fetch failed
... (cada 15 min) ...
Sep 23 07:15:15  [send-message] Meta send failed for all variants: fetch failed

→ El proceso de Next.js no pudo salir a graph.facebook.com entre el 21/Sep 03:15 y el 23/Sep 07:15. Coincide con las alertas del healthcheck del 20-21/Sep ("Acceso web NO responde") y con el reinicio de la VM del 20/Sep 07:38. Hoy la salida a Meta funciona (Graph API 200, y un envío de prueba devolvió wamid).

journalctl -u hermes-bridge.service: tras reiniciar el servicio sólo aparece el arranque normal; no hay kill-switch activo (REMINDERS_ENABLED no está definido → el heartbeat de recordatorios está desactivado por diseño, no afecta a las respuestas).

2.6 Defecto del motor de respuesta (reproducido)

Petición idéntica a la del bridge (node fetch, mismo body) contra https://api.nan.builders/v1/chat/completions:

model=qwen3.6, max_tokens=500  → status 200, finish_reason="length", content=null, reasoning_content=1956 chars
model=qwen3.6, max_tokens=1500 → status 200, finish_reason="stop",  content="\n\n¡Hola! 👋 Soy Sofía, tu asistente virtual en SKEEN Derma Experts..."

Con max_tokens: 500 el modelo gastaba todo el presupuesto razonando y nunca escribía la respuesta; callNanBuilders devolvía entonces 'Lo siento, no pude procesar tu mensaje.' (webhook-bridge.js:187). Verificado en vivo con un webhook firmado: antes del arreglo, el mensaje del paciente produjo como respuesta "Lo siento, no pude procesar tu mensaje."; después, "¡Hola, buenas tardes! Soy Sofía, tu asistente virtual en SKEEN Derma Experts…".

Además, el modelo local (http://localhost:8080/v1/chat/completions, gateway Hermes) responde correctamente el mismo prompt (finish_reason="stop", sin reasoning_content), por lo que sirve como respaldo real.


3. Causa raíz / hipótesis priorizadas

# Hipótesis Confianza Estado
H1 Los mensajes de los pacientes no llegan a este número: el CRM usa un número piloto ("Consultoria Alcaraz Salazar") mientras el código del bridge documenta que el cambio al número de WhatsApp de SKEEN sigue pendiente; en toda la vida del CRM sólo hay 2 conversaciones (una de pruebas y la del healthcheck). Alta A confirmar por el cliente con una prueba manual (§5.A)
H2 El número no puede recibir: code_verification_status: EXPIRED y estado de registro a revisar. status=CONNECTED no lo descarta por sí solo. Media Requiere panel de Meta (§5.D)
H3 Suscripción del webhook caída / URL mal apuntada (la causa "clásica"). Descartada active=true, URL correcta, handshake 200 hoy
H4 Ventana de fallos de red del VPS (21→23 sep) que rompió la salida a Meta y pudo haber tirado webhooks de esos días. Media (histórica) Resuelta; hoy sale a Meta. No explica la ausencia de entrantes desde el 3/Sep
H5 El motor de respuesta estaba roto: todos los pacientes recibían la disculpa de error. Confirmada Corregida hoy (§4)

Lo único que no se puede ver desde la VM es el historial de entregas de Meta (pestaña de webhooks de la app); ver §5.C.


4. Qué se arregló hoy

4.1 Webhook de Meta re-verificado

POST /{app-id}/subscriptions con la misma URL, token y lista de campos → {"success":true}, y Meta ejecutó el handshake contra nuestro endpoint (200 OK registrado en nginx). Suscripción de la WABA y del número apuntan al mismo callback.

4.2 Respuesta de la IA reparada — /root/skeen-roadmap-hermes/hermes/webhook-bridge.js

callNanBuilders():

  • max_tokens: 500 → 1500 (el modelo alcanza finish_reason="stop" y escribe content).
  • Si Nan devuelve content vacío (o falla por HTTP/red, p. ej. Cloudflare bloqueando la IP del VPS) → fallback real al gateway local http://localhost:8080/v1/chat/completions (verificado funcionando) y se registra [Nan] ... Fallback a Hermes.
  • Sólo si ambos fallan se conserva la disculpa como último recurso.

Servicio reiniciado (systemctl restart hermes-bridge, active, /health OK). node --check OK. Sin commit, según instrucción.

4.3 Datos de prueba: insertados y eliminados

Ver §6 (auditoría).


5. Qué falta hacer del lado de Meta / del cliente

A. Prueba manual de entrada (lo primero, 2 minutos). Desde un WhatsApp personal que no sea el del CRM, enviar Prueba a +52 1 663 700 6400.

  • Si el mensaje aparece en el CRM (o en los logs, §7) → la entrada funciona; el problema era sólo H5 (ya corregido) y/o el cliente estaba mirando otro número.
  • Si no aparece en ningún lado, entonces el mensaje no llega a Meta/nuestro webhook → seguir con C, D, E.

B. WhatsApp Manager → Configuración (Configuration) → Webhook. Confirmar: URL de devolución de llamada https://skeen.consultoria-as.com/api/whatsapp/webhook, token de verificación (valor real en /root/skeen-wacrm/.env.local, variable WHATSAPP_VERIFY_TOKEN) y que el campo messages esté suscrito. Debe pulsarse "Verificar y guardar" para que Meta revalide. (Ya validado por API, pero conviene que el cliente lo confirme visualmente.)

C. Panel de la app (developers.facebook.com/apps/1607064673936924 → Webhooks → entregas recientes). Ahí está el historial de entregas que no se puede consultar por API. Filtrar por el campo messages y revisar:

  • si llegan eventos de tipo mensaje en las últimas 24-72 h y con qué código HTTP respondió el servidor;
  • si sólo hay eventos de estado (statuses), confirma que no están entrando mensajes al número, no que el webhook falle.

D. WhatsApp Manager → Números de teléfono → +52 1 663 700 6400. Completar lo que esté pendiente: la API reporta code_verification_status = EXPIRED. Rehacer la verificación del número y configurar/renovar el PIN de verificación en dos pasos (pantalla "Configuración de dos pasos"). Es el único estado del número que no está "limpio".

E. Confirmar a qué número escriben los pacientes. Preguntar al cliente qué número está publicado (Google Business, Instagram, tarjetas, sitio). Si es distinto de +52 1 663 700 6400, ese es el problema: hay que registrar/migrar ese número a la Cloud API y apuntarlo al WACRM (el bridge sigue esperando "el cambio al número de WhatsApp de SKEEN").


6. Auditoría de datos de prueba (producción)

Se inyectó un webhook firmado real (HMAC-SHA256 con META_APP_SECRET, phone_number_id reales) a la URL pública, dos veces, con el remitente ficticio 5200000000000 (número no entregable → Meta lo marca failed, no se envió nada a ninguna persona real) y texto "Hola" / "Hola, buenas tardes".

Creado: 1 conversación (b0e0c328-a94a-4dd2-b8f6-38aed040758a) y 4 mensajes (2 customer, 2 agent), más memory/_5210000000000.json del bridge.

Eliminado/restaurado (verificado después):

DELETE 4  messages (de esa conversación)     → mensajes creados en la última hora: 0
DELETE 1  conversations                      → conversaciones totales: 2 (estado original)
DELETE 0  message_reactions
UPDATE 1  contacts  → nombre restaurado a '5210000000000'  (el contacto ya existía; mi prueba lo reutilizó
                                                     y le había cambiado el nombre)
rm        /root/skeen-roadmap-hermes/hermes/memory/_5210000000000.json
Sin efectos en Odoo (0 partners creados por la prueba; el intent clasificado fue 'otro')

7. Cómo verificar que quedó funcionando

  1. Ver el webhook en vivo (deja $remote_addr 127.0.0.1 porque entra por el túnel; UA de Meta = facebookexternalua):
tail -f /var/log/nginx/access.log | grep --line-buffered api/whatsapp/webhook
  1. Ver el pipeline y las respuestas de Sofía:
journalctl -u hermes-bridge -f        # "[WACRM] Webhook recibido: message.received", "[Intent] ...", "[WACRM] sent message to ..."
journalctl -u wacrm -f                # errores de envío / webhook
  1. Ver el mensaje en la base:
docker exec supabase-db psql -U postgres -d postgres -c \
"select m.created_at, c.phone, m.sender_type, left(m.content_text,60) txt, m.status
   from messages m join conversations cv on cv.id=m.conversation_id
   join contacts c on c.id=cv.contact_id order by m.created_at desc limit 10;"
  1. Re-verificar la suscripción de Meta (si se duda de ella):
cd /root/skeen-wacrm && set -a && . ./.env.local && set +a
curl -s "https://graph.facebook.com/v20.0/1607064673936924/subscriptions?access_token=1607064673936924|$META_APP_SECRET"
  1. Prueba funcional sin depender de Meta (inyecta un entrante firmado; borrar después): generar un payload con phone_number_id=1157212604148350, firmarlo con sha256=<hmac_sha256_hex> usando META_APP_SECRET y POST a https://skeen.consultoria-as.com/api/whatsapp/webhook.

Criterio de "funcionando": un mensaje real de un paciente produce en <15 s (a) fila customer en messages, (b) log message.received en el bridge, (c) fila agent con la respuesta de Sofía (no "Lo siento, no pude procesar tu mensaje.").


8. Pendientes / recomendaciones (fuera del arreglo de hoy)

  1. webhook-bridge.js usa const CLINIC_PHONE = '+52 (664) 123-4567', un placeholder: el prompt le dice a Sofía un teléfono falso. Confirmar con el cliente el número correcto y corregirlo.
  2. Las alertas del healthcheck se envían como texto libre a 526644990030, fuera de la ventana de 24 h → Meta las marca failed. Para avisos proactivos hace falta una plantilla aprobada y usarla.
  3. El bridge deja de responder si la conversación está asignada a un agente humano (isConversationAssigned → /skeen/frontend/v1/wacrm/conversations/{id}/assigned). Revisar que no haya conversaciones asignadas cuando se espera que conteste Sofía.
  4. La salida a Meta cayó del 21/Sep 03:15 al 23/Sep 07:15 (IPv4/DNS del VPS). Vale la pena añadir un check de conectividad a graph.facebook.com (IPv4) al healthcheck, no sólo al frontend.
  5. La app está suscrita también al objeto user con la misma callback URL; es inocuo, pero puede limpiarse para reducir ruido.
  6. Sin commit: los cambios de webhook-bridge.js están sólo en disco, pendientes de revisión del responsable.