Agrega uMind: chat con IA embebible por tenant (F1 — widget web + RAG)
Multi-tenant dentro de soft_usite, reutilizando la infraestructura ya existente (AiConfig, motor de function-calling del agente de Telegram) en vez de un servicio nuevo aparte: - UmindTenant: sitio/cliente con dominios permitidos, config de IA para el chat y personalidad/tono. - Ingesta: crawler simple (mismo dominio, N páginas) + chunking + embeddings (config global con módulo "umind_embeddings", pensada para OpenAI ya que Claude no ofrece embeddings) guardados como JSON, con búsqueda por similitud coseno en memoria (sin pgvector todavía). - Agente acotado: única herramienta buscar_conocimiento, sin acceso a nada interno — si no encuentra la respuesta, lo dice en vez de inventar. - Widget público (/widget/umind.js + /widget/:site_key/*), autenticado por site_key + validación de dominio (Origin/Referer), no por secreto, ya que la key viaja en el HTML público del sitio instalado. - Panel /app/umind: tenants, estado de ingesta, historial de conversaciones por sesión.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
func tenantDeContexto(c *fiber.Ctx) (*models.UmindTenant, error) {
|
||||
tenant, ok := c.Locals("umind_tenant").(*models.UmindTenant)
|
||||
if !ok || tenant == nil {
|
||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "no autenticado")
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func nuevaSessionID() string {
|
||||
b := make([]byte, 12)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// UmindWidgetInit devuelve una sesión nueva y el mensaje de bienvenida del
|
||||
// tenant, sin gastar una llamada al LLM solo para saludar.
|
||||
// Ruta: GET /widget/:site_key/init
|
||||
func UmindWidgetInit(c *fiber.Ctx) error {
|
||||
tenant, err := tenantDeContexto(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": true, "message": err.Error()})
|
||||
}
|
||||
bienvenida := strings.TrimSpace(tenant.MensajeBienvenida)
|
||||
if bienvenida == "" {
|
||||
bienvenida = "¡Hola! ¿En qué puedo ayudarte?"
|
||||
}
|
||||
return c.JSON(fiber.Map{
|
||||
"session_id": nuevaSessionID(),
|
||||
"nombre": tenant.Nombre,
|
||||
"mensaje_bienvenida": bienvenida,
|
||||
})
|
||||
}
|
||||
|
||||
// UmindWidgetMensaje procesa un mensaje del visitante y devuelve la respuesta del agente.
|
||||
// Ruta: POST /widget/:site_key/mensaje
|
||||
func UmindWidgetMensaje(c *fiber.Ctx) error {
|
||||
tenant, err := tenantDeContexto(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": true, "message": err.Error()})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Mensaje string `json:"mensaje"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": true, "message": "body inválido"})
|
||||
}
|
||||
if strings.TrimSpace(req.Mensaje) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": true, "message": "mensaje requerido"})
|
||||
}
|
||||
sessionID := strings.TrimSpace(req.SessionID)
|
||||
if sessionID == "" {
|
||||
sessionID = nuevaSessionID()
|
||||
}
|
||||
// Límite generoso pero real: evita que alguien mande un mensaje gigante al LLM.
|
||||
if len(req.Mensaje) > 4000 {
|
||||
req.Mensaje = req.Mensaje[:4000]
|
||||
}
|
||||
|
||||
respuesta, err := services.ProcessWidgetMessage(tenant, sessionID, strings.TrimSpace(req.Mensaje))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": true, "message": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"session_id": sessionID,
|
||||
"respuesta": respuesta,
|
||||
})
|
||||
}
|
||||
|
||||
// UmindWidgetScript sirve el script embebible que renderiza la burbuja de
|
||||
// chat. Se instala con: <script src="https://admin.u-site.app/widget/umind.js" data-site="SITE_KEY" defer></script>
|
||||
// Ruta: GET /widget/umind.js
|
||||
func UmindWidgetScript(c *fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
c.Set("Cache-Control", "public, max-age=300")
|
||||
return c.SendString(umindWidgetJS)
|
||||
}
|
||||
|
||||
const umindWidgetJS = `(function () {
|
||||
var script = document.currentScript;
|
||||
var siteKey = script.getAttribute('data-site');
|
||||
if (!siteKey) { console.error('[uMind] falta data-site en el <script>'); return; }
|
||||
var apiBase = script.src.replace(/\/widget\/umind\.js.*$/, '') + '/widget/' + siteKey;
|
||||
var storageKey = 'umind_session_' + siteKey;
|
||||
|
||||
var bubble = document.createElement('div');
|
||||
bubble.innerHTML = '💬';
|
||||
bubble.setAttribute('style', 'position:fixed;bottom:20px;right:20px;width:56px;height:56px;border-radius:50%;background:#8eb02f;color:#fff;display:flex;align-items:center;justify-content:center;font-size:26px;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.2);z-index:999999;');
|
||||
document.body.appendChild(bubble);
|
||||
|
||||
var panel = document.createElement('div');
|
||||
panel.setAttribute('style', 'display:none;position:fixed;bottom:86px;right:20px;width:340px;max-width:92vw;height:460px;max-height:70vh;background:#fff;border-radius:14px;box-shadow:0 8px 30px rgba(0,0,0,.25);z-index:999999;flex-direction:column;overflow:hidden;font-family:system-ui,sans-serif;');
|
||||
panel.innerHTML =
|
||||
'<div style="background:#8eb02f;color:#fff;padding:12px 14px;font-weight:600;font-size:14px;">' +
|
||||
'<span id="umind-title">Asistente</span>' +
|
||||
'</div>' +
|
||||
'<div id="umind-msgs" style="flex:1;overflow-y:auto;padding:12px;font-size:13px;line-height:1.4;"></div>' +
|
||||
'<div style="display:flex;border-top:1px solid #eee;">' +
|
||||
'<input id="umind-input" type="text" placeholder="Escribe tu mensaje..." style="flex:1;border:none;padding:10px 12px;font-size:13px;outline:none;">' +
|
||||
'<button id="umind-send" style="border:none;background:#8eb02f;color:#fff;padding:0 16px;cursor:pointer;">Enviar</button>' +
|
||||
'</div>';
|
||||
document.body.appendChild(panel);
|
||||
|
||||
var msgsEl = panel.querySelector('#umind-msgs');
|
||||
var inputEl = panel.querySelector('#umind-input');
|
||||
var sendEl = panel.querySelector('#umind-send');
|
||||
var sessionId = null;
|
||||
var abierto = false;
|
||||
var iniciado = false;
|
||||
|
||||
function agregarMensaje(texto, quien) {
|
||||
var burbuja = document.createElement('div');
|
||||
burbuja.textContent = texto;
|
||||
var esUser = quien === 'user';
|
||||
burbuja.setAttribute('style', 'margin-bottom:8px;padding:8px 10px;border-radius:10px;max-width:80%;white-space:pre-wrap;' +
|
||||
(esUser ? 'background:#8eb02f;color:#fff;margin-left:auto;' : 'background:#f1f1f1;color:#222;'));
|
||||
msgsEl.appendChild(burbuja);
|
||||
msgsEl.scrollTop = msgsEl.scrollHeight;
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (iniciado) return;
|
||||
iniciado = true;
|
||||
sessionId = sessionStorage.getItem(storageKey) || null;
|
||||
fetch(apiBase + '/init')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!sessionId) { sessionId = data.session_id; sessionStorage.setItem(storageKey, sessionId); }
|
||||
if (data.nombre) panel.querySelector('#umind-title').textContent = data.nombre;
|
||||
agregarMensaje(data.mensaje_bienvenida, 'bot');
|
||||
})
|
||||
.catch(function () { agregarMensaje('No se pudo conectar el asistente.', 'bot'); });
|
||||
}
|
||||
|
||||
function enviar() {
|
||||
var texto = inputEl.value.trim();
|
||||
if (!texto) return;
|
||||
inputEl.value = '';
|
||||
agregarMensaje(texto, 'user');
|
||||
fetch(apiBase + '/mensaje', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_id: sessionId, mensaje: texto })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.session_id) { sessionId = data.session_id; sessionStorage.setItem(storageKey, sessionId); }
|
||||
agregarMensaje(data.respuesta || data.message || 'No pude responder eso.', 'bot');
|
||||
})
|
||||
.catch(function () { agregarMensaje('Error de conexión, intenta de nuevo.', 'bot'); });
|
||||
}
|
||||
|
||||
bubble.addEventListener('click', function () {
|
||||
abierto = !abierto;
|
||||
panel.style.display = abierto ? 'flex' : 'none';
|
||||
if (abierto) iniciar();
|
||||
});
|
||||
sendEl.addEventListener('click', enviar);
|
||||
inputEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') enviar(); });
|
||||
})();
|
||||
`
|
||||
@@ -0,0 +1,215 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// UmindIndex renderiza el panel de administración de uMind.
|
||||
func UmindIndex(c *fiber.Ctx) error {
|
||||
return c.Render("umind", fiber.Map{
|
||||
"user": c.Locals("user"),
|
||||
"modules": c.Locals("userModules"),
|
||||
}, "layouts/main")
|
||||
}
|
||||
|
||||
// ─── Tenants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func GetUmindTenants(c *fiber.Ctx) error {
|
||||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
limit := 20
|
||||
offset := (page - 1) * limit
|
||||
items, total, err := models.GetAllUmindTenants(limit, offset)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
||||
"page": page,
|
||||
})
|
||||
}
|
||||
|
||||
type umindTenantReq struct {
|
||||
Nombre string `json:"nombre"`
|
||||
DominiosPermitidos []string `json:"dominios_permitidos"`
|
||||
AiConfigID *uint `json:"ai_config_id"`
|
||||
Tono string `json:"tono"`
|
||||
MensajeBienvenida string `json:"mensaje_bienvenida"`
|
||||
Activo bool `json:"activo"`
|
||||
}
|
||||
|
||||
func (r umindTenantReq) dominiosLimpios() []string {
|
||||
var out []string
|
||||
for _, d := range r.DominiosPermitidos {
|
||||
d = strings.ToLower(strings.TrimSpace(d))
|
||||
if d != "" {
|
||||
out = append(out, d)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func CreateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
var req umindTenantReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if strings.TrimSpace(req.Nombre) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
|
||||
}
|
||||
dominios := req.dominiosLimpios()
|
||||
if len(dominios) == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agrega al menos un dominio permitido"})
|
||||
}
|
||||
|
||||
tenant := &models.UmindTenant{
|
||||
Nombre: strings.TrimSpace(req.Nombre),
|
||||
DominiosPermitidos: strings.Join(dominios, ","),
|
||||
AiConfigID: req.AiConfigID,
|
||||
Tono: req.Tono,
|
||||
MensajeBienvenida: req.MensajeBienvenida,
|
||||
Activo: true,
|
||||
CreadoPorID: extraerUserID(c),
|
||||
}
|
||||
if err := models.CreateUmindTenant(tenant); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": tenant.ID, "site_key": tenant.SiteKey})
|
||||
}
|
||||
|
||||
func UpdateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
var req umindTenantReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
dominios := req.dominiosLimpios()
|
||||
updates := map[string]interface{}{
|
||||
"nombre": strings.TrimSpace(req.Nombre),
|
||||
"dominios_permitidos": strings.Join(dominios, ","),
|
||||
"ai_config_id": req.AiConfigID,
|
||||
"tono": req.Tono,
|
||||
"mensaje_bienvenida": req.MensajeBienvenida,
|
||||
"activo": req.Activo,
|
||||
}
|
||||
if err := models.UpdateUmindTenant(uint(id), updates); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
func DeleteUmindTenantHandler(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := models.DeleteUmindTenant(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Documentos / ingesta ────────────────────────────────────────────────────
|
||||
|
||||
func GetUmindDocumentosHandler(c *fiber.Ctx) error {
|
||||
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
|
||||
if err != nil || tenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
}
|
||||
items, err := models.GetUmindDocumentosByTenant(uint(tenantID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": items})
|
||||
}
|
||||
|
||||
// CreateUmindDocumentoHandler registra una nueva fuente (por ahora solo URL a
|
||||
// crawlear) y dispara la ingesta en segundo plano — puede tardar varios
|
||||
// segundos/minutos según cuántas páginas tenga el sitio.
|
||||
func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
TenantID uint `json:"tenant_id"`
|
||||
URL string `json:"url"`
|
||||
MaxPaginas int `json:"max_paginas"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if req.TenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
}
|
||||
if strings.TrimSpace(req.URL) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url requerida"})
|
||||
}
|
||||
if _, err := models.GetUmindTenantByID(req.TenantID); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
|
||||
}
|
||||
|
||||
doc := &models.UmindDocumento{
|
||||
TenantID: req.TenantID,
|
||||
Tipo: "url",
|
||||
Origen: strings.TrimSpace(req.URL),
|
||||
Estado: "procesando",
|
||||
}
|
||||
if err := models.CreateUmindDocumento(doc); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
go services.IngestarTenant(req.TenantID, doc.ID, doc.Origen, req.MaxPaginas)
|
||||
|
||||
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "id": doc.ID, "estado": "procesando"})
|
||||
}
|
||||
|
||||
func DeleteUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := models.DeleteUmindDocumento(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Conversaciones ───────────────────────────────────────────────────────────
|
||||
|
||||
func GetUmindSesionesHandler(c *fiber.Ctx) error {
|
||||
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
|
||||
if err != nil || tenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
}
|
||||
items, err := models.GetUmindSesiones(uint(tenantID), 50)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": items})
|
||||
}
|
||||
|
||||
func GetUmindHistorialHandler(c *fiber.Ctx) error {
|
||||
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
|
||||
if err != nil || tenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
}
|
||||
sessionID := c.Query("session_id")
|
||||
if sessionID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "session_id requerido"})
|
||||
}
|
||||
items, err := models.GetUmindHistorial(uint(tenantID), sessionID, 200)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": items})
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// AuthUmindWidget resuelve el tenant a partir de la site_key en la URL y
|
||||
// valida que la petición venga de uno de sus dominios permitidos (Origin, o
|
||||
// Referer si el navegador no manda Origin). La site_key no es un secreto —
|
||||
// cualquiera puede verla en el HTML público del sitio — la protección real es
|
||||
// el chequeo de dominio, igual que una site key de reCAPTCHA/Analytics.
|
||||
func AuthUmindWidget(c *fiber.Ctx) error {
|
||||
siteKey := c.Params("site_key")
|
||||
if siteKey == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": true, "message": "site_key requerida"})
|
||||
}
|
||||
tenant, err := models.GetUmindTenantBySiteKey(siteKey)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"})
|
||||
}
|
||||
|
||||
origen := c.Get("Origin")
|
||||
if origen == "" {
|
||||
origen = c.Get("Referer")
|
||||
}
|
||||
host := hostDeOrigen(origen)
|
||||
if !tenant.DominioPermitido(host) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": true, "message": "dominio no autorizado para este sitio"})
|
||||
}
|
||||
|
||||
if origen != "" {
|
||||
c.Set("Access-Control-Allow-Origin", origen)
|
||||
c.Set("Vary", "Origin")
|
||||
}
|
||||
c.Locals("umind_tenant", tenant)
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// UmindWidgetCORS responde el preflight OPTIONS que el navegador manda antes
|
||||
// del POST real (el widget corre en un dominio distinto al de la API).
|
||||
func UmindWidgetCORS(c *fiber.Ctx) error {
|
||||
origen := c.Get("Origin")
|
||||
if origen != "" {
|
||||
c.Set("Access-Control-Allow-Origin", origen)
|
||||
c.Set("Vary", "Origin")
|
||||
}
|
||||
c.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
c.Set("Access-Control-Max-Age", "600")
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func hostDeOrigen(origenURL string) string {
|
||||
if origenURL == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := url.Parse(origenURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ToLower(u.Hostname())
|
||||
}
|
||||
@@ -270,6 +270,17 @@ func AdminApiRoutes(api fiber.Router) {
|
||||
h.Post("/pagos-externos/servicios/:id/regenerar-token", controllers.RegenerarTokenServicioPagoHandler)
|
||||
h.Get("/pagos-externos/solicitudes", controllers.GetSolicitudesPagoExternoHandler)
|
||||
|
||||
// ─── uMind: chat con IA embebible por tenant ───────────────────────────────
|
||||
h.Get("/umind/tenants", controllers.GetUmindTenants)
|
||||
h.Post("/umind/tenants", controllers.CreateUmindTenantHandler)
|
||||
h.Put("/umind/tenants/:id", controllers.UpdateUmindTenantHandler)
|
||||
h.Delete("/umind/tenants/:id", controllers.DeleteUmindTenantHandler)
|
||||
h.Get("/umind/documentos", controllers.GetUmindDocumentosHandler)
|
||||
h.Post("/umind/documentos", controllers.CreateUmindDocumentoHandler)
|
||||
h.Delete("/umind/documentos/:id", controllers.DeleteUmindDocumentoHandler)
|
||||
h.Get("/umind/sesiones", controllers.GetUmindSesionesHandler)
|
||||
h.Get("/umind/historial", controllers.GetUmindHistorialHandler)
|
||||
|
||||
// ─── OSS API (almacenamiento) ────────────────────────────────────────────
|
||||
h.Get("/oss-api", controllers.GetOssApiConfigs)
|
||||
h.Get("/oss-api/active", controllers.GetActiveOssApiList)
|
||||
|
||||
@@ -96,4 +96,14 @@ func RutasPublicas(web fiber.Router) {
|
||||
web.Get("/landing/download/:token", apiControllers.LandingDownload)
|
||||
// Config de IA activa (para Landing Generator)
|
||||
web.Get("/landing/ai-config", apiControllers.LandingGetAiConfig)
|
||||
|
||||
// ─── uMind: widget de chat embebible por tenant ───────────────────────────
|
||||
// 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.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -340,6 +340,21 @@ func UserRoutes(app fiber.Router) {
|
||||
protected.Post("/pagos-externos/servicios/:id/regenerar-token", middlewares.SoloAdmin, controllers.RegenerarTokenServicioPagoHandler)
|
||||
protected.Get("/pagos-externos/solicitudes", controllers.GetSolicitudesPagoExternoHandler)
|
||||
|
||||
// ─── uMind: chat con IA embebible por tenant ───────────────────────────────
|
||||
// Sensible: un tenant queda ligado a una config de IA (API key) y controla
|
||||
// desde qué dominios se puede llamar al widget, así que crear/editar es
|
||||
// solo para administradores.
|
||||
protected.Get("/umind", middlewares.MenuMiddleware, controllers.UmindIndex)
|
||||
protected.Get("/umind/tenants", controllers.GetUmindTenants)
|
||||
protected.Post("/umind/tenants", middlewares.SoloAdmin, controllers.CreateUmindTenantHandler)
|
||||
protected.Put("/umind/tenants/:id", middlewares.SoloAdmin, controllers.UpdateUmindTenantHandler)
|
||||
protected.Delete("/umind/tenants/:id", middlewares.SoloAdmin, controllers.DeleteUmindTenantHandler)
|
||||
protected.Get("/umind/documentos", controllers.GetUmindDocumentosHandler)
|
||||
protected.Post("/umind/documentos", middlewares.SoloAdmin, controllers.CreateUmindDocumentoHandler)
|
||||
protected.Delete("/umind/documentos/:id", middlewares.SoloAdmin, controllers.DeleteUmindDocumentoHandler)
|
||||
protected.Get("/umind/sesiones", controllers.GetUmindSesionesHandler)
|
||||
protected.Get("/umind/historial", controllers.GetUmindHistorialHandler)
|
||||
|
||||
// ─── OSS API (Alibaba Cloud + S3/MinIO) ────────────────────────────────────
|
||||
protected.Get("/oss-api", middlewares.MenuMiddleware, controllers.OssApiIndex)
|
||||
protected.Get("/loadossapi", controllers.GetOssApiConfigs)
|
||||
|
||||
Reference in New Issue
Block a user