# 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`): ```bash tail -f /var/log/nginx/access.log | grep --line-buffered api/whatsapp/webhook ``` 2. Ver el pipeline y las respuestas de Sofía: ```bash journalctl -u hermes-bridge -f # "[WACRM] Webhook recibido: message.received", "[Intent] ...", "[WACRM] sent message to ..." journalctl -u wacrm -f # errores de envío / webhook ``` 3. Ver el mensaje en la base: ```bash 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;" ``` 4. Re-verificar la suscripción de Meta (si se duda de ella): ```bash 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" ``` 5. Prueba funcional sin depender de Meta (inyecta un entrante firmado; **borrar después**): generar un payload con `phone_number_id=1157212604148350`, firmarlo con `sha256=` 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.