Mensajes rápidos por bot
Respuestas preconfiguradas por bot para el agente humano en la bandeja. El uso está en el manual de usuario, en Conversaciones → Mensajes rápidos (otro sitio: no se enlaza desde aquí).
Datos
Tabla public.bot_quick_messages (migración 20261114000001_bot_quick_messages.sql):
| Columna | Nota |
|---|---|
tenant_id, bot_id | FK con ON DELETE CASCADE. El bot tiene que ser del mismo tenant; lo exige la política de escritura |
title | 1 a 80 caracteres |
shortcut | Opcional, ^[a-z0-9][a-z0-9_-]{0,29}$. Único por bot entre las filas vivas (índice parcial), así que el atajo de una fila borrada se puede reutilizar |
body | 1 a 4096 caracteres, con variables {{grupo.campo}} |
sort_order, is_active | Orden y visibilidad en la bandeja |
deleted_at | Borrado lógico. El DELETE físico no se concede |
Por qué una tabla y no flow_definition.settings: los mensajes son del bot, no de una versión
del flujo. Sirven igual para bots por prompt y por flujo, y publicar o restaurar una versión no
debe borrarlos. Por eso los ajustes del editor de flujos los guardan al momento, sin pasar por
«Guardar» ni por publicar.
Accesos
| Operación | Quién |
|---|---|
SELECT | Miembro del tenant con bot.view o conversation.reply |
INSERT / UPDATE | Miembro con bot.update, y EXISTS (bots WHERE id = bot_id AND tenant_id = tenant_id) |
DELETE | Nadie desde el panel (no hay GRANT) |
Se reutilizan permisos existentes. Uno nuevo obligaría a un backfill por rol en todos los tenants y dejaría sin mensajes a los agentes que hoy responden.
Todo va con la sesión (createServerSupabase) desde frontend/src/actions/quick-messages.ts, sin
service-role. Un UPDATE bloqueado por RLS vuelve con 0 filas y error: null, así que las
acciones piden .select() y tratan 0 filas como forbidden.
Pruebas por rol en supabase/tests/bot_quick_messages/: rojo sin la migración, verde con ella.
Sustitución de variables
El catálogo de variables y la sustitución están en frontend/src/lib/quick-messages/, y es código
puro con tests:
quick-message-variables.ts: el catálogo. Cada variable tiene su grupo, cómo se resuelve y la etiqueta i18n enbot.quickMessages.vars.*. Es la única fuente para el editor y la bandeja.render-quick-message.ts:renderQuickMessage(body, ctx)→{ text, missing }. Una variable sin dato queda literal y se reporta; las llaves que no son del catálogo no se tocan.hasUnresolvedTokenses lo que bloquea «Aprobar y enviar».build-quick-message-context.ts: arma el contexto desde la cita elegida (por defecto la próxima) o desde un recurso suelto.
getQuickMessageConversationData(conversationId) lee el contacto de la conversación (con el
número de channel_identities como respaldo del móvil) y hasta 10 citas próximas y 5 pasadas del
contacto, sin cancelled ni no_show. La especialidad sale de
scheduling_resources.metadata.specialty y la fecha se formatea en el timezone de la sede.
La sustitución ocurre en el panel, a propósito: el agente tiene que ver y aprobar el texto final.
El envío es el sendMessage de siempre (frontend/src/actions/chat.ts). No hay un endpoint nuevo
en el backend.
Límites conocidos
sendMessageno marcasource = 'agent'ni comprueba la ventana de 24 h de WhatsApp. Eso ya pasaba con cualquier respuesta escrita a mano; no es propio de esta función.- Solo texto: no hay adjuntos en un mensaje rápido.
- El composer es de una línea: un mensaje con saltos de línea pasa siempre por el preview para no perderlos.