feat: orquestador uMind (SPA Vue) + tools custom + canales Telegram/WhatsApp

SPA nueva en /orchestrator (Vue 3 + Vite, servida por el mismo binario Go
bajo /orchestrator para que la cookie de sesión funcione sin tocar CORS),
reemplaza al panel Alpine.js como punto de entrada del menú.

Backend, todo aditivo sobre el motor de uMind ya existente:
- UmindHerramienta: tools custom por tenant que llaman un webhook HTTP,
  integradas al loop de function-calling existente. Cliente HTTP con
  guardas SSRF (bloqueo de IPs privadas/loopback/link-local resuelto en el
  momento de conectar, no antes, para cerrar la ventana de DNS rebinding)
  que no existían en el proyecto.
- UmindCanal: Telegram y WhatsApp Business Cloud API como canales
  adicionales del mismo agente que ya atiende el widget web, ambos
  reusando ProcessWidgetMessage. WhatsApp valida X-Hub-Signature-256.
  Credenciales cifradas en reposo con el mismo AES-GCM+APP_KEY que ya usa
  el proyecto para la contraseña SMTP (primer uso para secretos de uMind).
- Se conecta middlewares.Limit() (rate limiter que existía pero no se
  usaba en ningún lado) al widget público y a los webhooks nuevos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-11 22:16:20 -05:00
co-authored by Claude Sonnet 5
parent aaf36b33ce
commit da0bffe661
30 changed files with 2105 additions and 26 deletions
+14 -1
View File
@@ -1,6 +1,8 @@
package routes
import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/rest/controllers"
apiControllers "github.com/sujit-baniya/fiber-boilerplate/rest/controllers/api"
@@ -101,9 +103,20 @@ func RutasPublicas(web fiber.Router) {
// Público por diseño (lo llama el navegador del visitante de un sitio de
// terceros) — la protección es site_key + validación de dominio, no
// sesión. Excluido de AuthApi() por vivir fuera del grupo /api.
// middlewares.Limit por IP: cada mensaje dispara una llamada al LLM del
// tenant, sin límite era gasto libre para cualquiera con la site_key.
umindMsgLimit := middlewares.Limit(20, 1*time.Minute)
web.Get("/widget/umind.js", apiControllers.UmindWidgetScript)
widget := web.Group("/widget/:site_key")
widget.Options("*", middlewares.UmindWidgetCORS)
widget.Get("/init", middlewares.AuthUmindWidget, apiControllers.UmindWidgetInit)
widget.Post("/mensaje", middlewares.AuthUmindWidget, apiControllers.UmindWidgetMensaje)
widget.Post("/mensaje", umindMsgLimit, middlewares.AuthUmindWidget, apiControllers.UmindWidgetMensaje)
// ─── uMind: canales adicionales (Telegram, WhatsApp) ──────────────────────
// Igual que el widget: público por diseño, la autenticación real es
// específica de cada proveedor (firma de WhatsApp; Telegram no firma, el
// webhook_secret en la URL es lo único que lo protege).
web.Post("/webhooks/umind-telegram/:webhook_secret", umindMsgLimit, apiControllers.UmindTelegramWebhook)
web.Get("/webhooks/umind-whatsapp/:webhook_secret", apiControllers.UmindWhatsAppVerify)
web.Post("/webhooks/umind-whatsapp/:webhook_secret", umindMsgLimit, apiControllers.UmindWhatsAppWebhook)
}
+19
View File
@@ -365,6 +365,25 @@ func UserRoutes(app fiber.Router) {
protected.Delete("/umind/documentos/:id", middlewares.SoloAdmin, controllers.DeleteUmindDocumentoHandler)
protected.Get("/umind/sesiones", controllers.GetUmindSesionesHandler)
protected.Get("/umind/historial", controllers.GetUmindHistorialHandler)
protected.Get("/umind/tools", controllers.GetUmindHerramientasHandler)
protected.Post("/umind/tools", middlewares.SoloAdmin, controllers.CreateUmindHerramientaHandler)
protected.Put("/umind/tools/:id", middlewares.SoloAdmin, controllers.UpdateUmindHerramientaHandler)
protected.Delete("/umind/tools/:id", middlewares.SoloAdmin, controllers.DeleteUmindHerramientaHandler)
protected.Get("/umind/canales", controllers.GetUmindCanalesHandler)
protected.Post("/umind/canales", middlewares.SoloAdmin, controllers.CreateUmindCanalHandler)
protected.Put("/umind/canales/:id", middlewares.SoloAdmin, controllers.UpdateUmindCanalHandler)
protected.Delete("/umind/canales/:id", middlewares.SoloAdmin, controllers.DeleteUmindCanalHandler)
protected.Post("/umind/chat", controllers.UmindChatPruebaHandler)
// ─── uMind Orquestador (SPA Vue) ────────────────────────────────────────────
// Estáticos reales (JS/CSS del build) ya los sirve el Static("/") general
// registrado en config.LoadStatic — esto es solo el fallback para las rutas
// del lado del cliente (vue-router en modo history), protegido con sesión.
orchestratorFallback := func(c *fiber.Ctx) error {
return c.SendFile("./public/orchestrator/index.html")
}
app.Get("/orchestrator", middlewares.AuthWeb(), orchestratorFallback)
app.Get("/orchestrator/*", middlewares.AuthWeb(), orchestratorFallback)
// ─── OSS API (Alibaba Cloud + S3/MinIO) ────────────────────────────────────
protected.Get("/oss-api", middlewares.MenuMiddleware, controllers.OssApiIndex)