Un tenant (negocio/sitio, dueño de los dominios permitidos) puede tener varios UmindAgente independientes (ej. "Ventas", "Soporte"), cada uno con su propia config de IA, tono, base de conocimiento, tools, canales y conexión de correo. El site_key también pasa a ser por agente, así cada uno tiene su propio <script> de widget embebible y su propio color. Backend: - Nuevo modelo UmindAgente (pkg/models/umind_agente.go), con SiteKey, AiConfigID, Tono, MensajeBienvenida y Color — campos que antes vivían en UmindTenant y se sacan de ahí (las columnas viejas quedan huérfanas sin usar, no se hace DROP COLUMN). - UmindDocumento, UmindChunk, UmindHerramienta, UmindCanal, UmindConexion y UmindMensaje pasan de TenantID a AgenteID. El campo se agrega sin "not null" para no romper el ALTER TABLE en Postgres sobre tablas que ya tienen filas (ej. emetropolitana). - migrations.MigrarUmindAgentes(): idempotente, crea un agente "Principal" por cada tenant existente heredando lo que ya tenía configurado, y mueve sus datos de tenant_id a agente_id. Corre en cada arranque normal, mismo criterio que los Seed* — nada se rompe para los tenants ya en producción. - Motor del agente, widget, canales (Telegram/WhatsApp) y OAuth de correo ahora operan sobre UmindAgente; el tenant solo se consulta para el chequeo de dominio permitido y el nombre del negocio que ve el visitante. Frontend: nueva jerarquía de navegación tenant → lista de agentes (TenantAgentes.vue) → detalle de un agente (AgenteDetail.vue, antes TenantDetail.vue) con las mismas 6 tabs de siempre, ahora por agente. El modal de tenant en el sidebar se achica a nombre/dominios/activo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
585 lines
23 KiB
Go
585 lines
23 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"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")
|
|
}
|
|
|
|
var umindColorHexRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
|
|
|
// umindColor valida el hex del color de marca del widget; si viene vacío o
|
|
// inválido cae al verde por defecto en vez de guardar basura (el valor se
|
|
// aplica tal cual como CSS custom property en el widget embebido).
|
|
func umindColor(color string) string {
|
|
color = strings.TrimSpace(color)
|
|
if umindColorHexRegex.MatchString(color) {
|
|
return color
|
|
}
|
|
return "#8eb02f"
|
|
}
|
|
|
|
// ─── Tenants ─────────────────────────────────────────────────────────────────
|
|
// Un tenant es el negocio/sitio dueño de los dominios permitidos — la config
|
|
// de IA, tono, tools, etc. viven en sus UmindAgente (ver más abajo).
|
|
|
|
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"`
|
|
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, ","),
|
|
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})
|
|
}
|
|
|
|
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, ","),
|
|
"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})
|
|
}
|
|
|
|
// ─── Agentes ─────────────────────────────────────────────────────────────────
|
|
// Un tenant puede tener varios agentes independientes (ej. "Ventas",
|
|
// "Soporte"), cada uno con su propia config de IA, base de conocimiento,
|
|
// tools, canales y conexión de correo.
|
|
|
|
func GetUmindAgentesHandler(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.GetUmindAgentesByTenant(uint(tenantID))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
type umindAgenteReq struct {
|
|
TenantID uint `json:"tenant_id"`
|
|
Nombre string `json:"nombre"`
|
|
AiConfigID *uint `json:"ai_config_id"`
|
|
Tono string `json:"tono"`
|
|
MensajeBienvenida string `json:"mensaje_bienvenida"`
|
|
Color string `json:"color"`
|
|
Activo bool `json:"activo"`
|
|
}
|
|
|
|
func CreateUmindAgenteHandler(c *fiber.Ctx) error {
|
|
var req umindAgenteReq
|
|
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.Nombre) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
|
|
}
|
|
if _, err := models.GetUmindTenantByID(req.TenantID); err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
|
|
}
|
|
|
|
agente := &models.UmindAgente{
|
|
TenantID: req.TenantID,
|
|
Nombre: strings.TrimSpace(req.Nombre),
|
|
AiConfigID: req.AiConfigID,
|
|
Tono: req.Tono,
|
|
MensajeBienvenida: req.MensajeBienvenida,
|
|
Color: umindColor(req.Color),
|
|
Activo: true,
|
|
}
|
|
if err := models.CreateUmindAgente(agente); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": agente.ID, "site_key": agente.SiteKey})
|
|
}
|
|
|
|
func UpdateUmindAgenteHandler(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 umindAgenteReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
updates := map[string]interface{}{
|
|
"nombre": strings.TrimSpace(req.Nombre),
|
|
"ai_config_id": req.AiConfigID,
|
|
"tono": req.Tono,
|
|
"mensaje_bienvenida": req.MensajeBienvenida,
|
|
"color": umindColor(req.Color),
|
|
"activo": req.Activo,
|
|
}
|
|
if err := models.UpdateUmindAgente(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindAgenteHandler(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.DeleteUmindAgente(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 {
|
|
agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
|
|
if err != nil || agenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
items, err := models.GetUmindDocumentosByAgente(uint(agenteID))
|
|
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 {
|
|
AgenteID uint `json:"agente_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.AgenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
if strings.TrimSpace(req.URL) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url requerida"})
|
|
}
|
|
if _, err := models.GetUmindAgenteByID(req.AgenteID); err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
|
|
}
|
|
|
|
doc := &models.UmindDocumento{
|
|
AgenteID: req.AgenteID,
|
|
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.IngestarAgente(req.AgenteID, 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 {
|
|
agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
|
|
if err != nil || agenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
items, err := models.GetUmindSesiones(uint(agenteID), 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 {
|
|
agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
|
|
if err != nil || agenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_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(agenteID), sessionID, 200)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
// ─── Tools custom (webhooks) ───────────────────────────────────────────────
|
|
|
|
var umindNombreToolRegex = regexp.MustCompile(`^[a-z][a-z0-9_]{2,63}$`)
|
|
|
|
func GetUmindHerramientasHandler(c *fiber.Ctx) error {
|
|
agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
|
|
if err != nil || agenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
items, err := models.GetUmindHerramientasByAgente(uint(agenteID))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// No se devuelve el secreto cifrado ni el descifrado — solo si hay uno configurado.
|
|
out := make([]fiber.Map, len(items))
|
|
for i, h := range items {
|
|
out[i] = fiber.Map{
|
|
"ID": h.ID, "agente_id": h.AgenteID, "nombre": h.Nombre, "descripcion": h.Descripcion,
|
|
"parametros_json": h.ParametrosJSON, "url": h.URL, "auth_header_nombre": h.AuthHeaderNombre,
|
|
"auth_configurado": h.AuthHeaderValorEnc != "", "activa": h.Activa,
|
|
}
|
|
}
|
|
return c.JSON(fiber.Map{"items": out})
|
|
}
|
|
|
|
type umindHerramientaReq struct {
|
|
AgenteID uint `json:"agente_id"`
|
|
Nombre string `json:"nombre"`
|
|
Descripcion string `json:"descripcion"`
|
|
Parametros []models.UmindHerramientaParametro `json:"parametros"`
|
|
URL string `json:"url"`
|
|
AuthHeaderNombre string `json:"auth_header_nombre"`
|
|
AuthHeaderValor *string `json:"auth_header_valor"` // nil = no tocar (en updates)
|
|
Activa bool `json:"activa"`
|
|
}
|
|
|
|
func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
|
|
var req umindHerramientaReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if req.AgenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
if !umindNombreToolRegex.MatchString(req.Nombre) {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el nombre debe ser minúsculas/números/guion_bajo, empezar con letra (3-64 caracteres)"})
|
|
}
|
|
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(req.URL)), "https://") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "la URL debe ser https"})
|
|
}
|
|
|
|
parametrosJSON, err := models.ParametrosToJSON(req.Parametros)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "parametros inválidos"})
|
|
}
|
|
var authEnc string
|
|
if req.AuthHeaderValor != nil && *req.AuthHeaderValor != "" {
|
|
authEnc, err = services.CifrarSecretoUmind(*req.AuthHeaderValor)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
}
|
|
|
|
h := &models.UmindHerramienta{
|
|
AgenteID: req.AgenteID, Nombre: req.Nombre, Descripcion: strings.TrimSpace(req.Descripcion),
|
|
ParametrosJSON: parametrosJSON, URL: strings.TrimSpace(req.URL),
|
|
AuthHeaderNombre: strings.TrimSpace(req.AuthHeaderNombre), AuthHeaderValorEnc: authEnc, Activa: true,
|
|
}
|
|
if err := models.CreateUmindHerramienta(h); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": h.ID})
|
|
}
|
|
|
|
func UpdateUmindHerramientaHandler(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 umindHerramientaReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if !umindNombreToolRegex.MatchString(req.Nombre) {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el nombre debe ser minúsculas/números/guion_bajo, empezar con letra (3-64 caracteres)"})
|
|
}
|
|
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(req.URL)), "https://") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "la URL debe ser https"})
|
|
}
|
|
parametrosJSON, err := models.ParametrosToJSON(req.Parametros)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "parametros inválidos"})
|
|
}
|
|
|
|
updates := map[string]interface{}{
|
|
"nombre": req.Nombre, "descripcion": strings.TrimSpace(req.Descripcion),
|
|
"parametros_json": parametrosJSON, "url": strings.TrimSpace(req.URL),
|
|
"auth_header_nombre": strings.TrimSpace(req.AuthHeaderNombre), "activa": req.Activa,
|
|
}
|
|
if req.AuthHeaderValor != nil {
|
|
if *req.AuthHeaderValor == "" {
|
|
updates["auth_header_valor_enc"] = ""
|
|
} else {
|
|
enc, err := services.CifrarSecretoUmind(*req.AuthHeaderValor)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
updates["auth_header_valor_enc"] = enc
|
|
}
|
|
}
|
|
if err := models.UpdateUmindHerramienta(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindHerramientaHandler(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.DeleteUmindHerramienta(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Canales (Telegram / WhatsApp) ─────────────────────────────────────────
|
|
|
|
func GetUmindCanalesHandler(c *fiber.Ctx) error {
|
|
agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
|
|
if err != nil || agenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
items, err := models.GetUmindCanalesByAgente(uint(agenteID))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
out := make([]fiber.Map, len(items))
|
|
for i, canal := range items {
|
|
webhookURL := ""
|
|
if canal.Tipo == "telegram" {
|
|
webhookURL = fmt.Sprintf("%s/webhooks/umind-telegram/%s", app.Http.Server.Url, canal.WebhookSecret)
|
|
} else if canal.Tipo == "whatsapp" {
|
|
webhookURL = fmt.Sprintf("%s/webhooks/umind-whatsapp/%s", app.Http.Server.Url, canal.WebhookSecret)
|
|
}
|
|
out[i] = fiber.Map{
|
|
"ID": canal.ID, "agente_id": canal.AgenteID, "tipo": canal.Tipo, "activo": canal.Activo,
|
|
"webhook_url": webhookURL, "ultimo_error": canal.UltimoError,
|
|
}
|
|
}
|
|
return c.JSON(fiber.Map{"items": out})
|
|
}
|
|
|
|
type umindCanalReq struct {
|
|
AgenteID uint `json:"agente_id"`
|
|
Tipo string `json:"tipo"`
|
|
Credenciales map[string]string `json:"credenciales"`
|
|
Activo bool `json:"activo"`
|
|
}
|
|
|
|
func CreateUmindCanalHandler(c *fiber.Ctx) error {
|
|
var req umindCanalReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if req.AgenteID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
|
}
|
|
switch req.Tipo {
|
|
case "telegram":
|
|
if strings.TrimSpace(req.Credenciales["bot_token"]) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bot_token requerido"})
|
|
}
|
|
case "whatsapp":
|
|
for _, k := range []string{"phone_number_id", "access_token", "app_secret", "verify_token"} {
|
|
if strings.TrimSpace(req.Credenciales[k]) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": k + " requerido"})
|
|
}
|
|
}
|
|
default:
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tipo debe ser telegram o whatsapp"})
|
|
}
|
|
|
|
credencialesEnc, err := services.CifrarCredencialesCanal(req.Credenciales)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
canal := &models.UmindCanal{AgenteID: req.AgenteID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc}
|
|
if err := models.CreateUmindCanal(canal); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
if req.Tipo == "telegram" {
|
|
webhookURL := fmt.Sprintf("%s/webhooks/umind-telegram/%s", app.Http.Server.Url, canal.WebhookSecret)
|
|
if err := services.RegistrarWebhookTelegram(req.Credenciales["bot_token"], webhookURL); err != nil {
|
|
models.UpdateUmindCanal(canal.ID, map[string]interface{}{"ultimo_error": err.Error()})
|
|
}
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": canal.ID, "webhook_secret": canal.WebhookSecret})
|
|
}
|
|
|
|
func UpdateUmindCanalHandler(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 umindCanalReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
updates := map[string]interface{}{"activo": req.Activo}
|
|
if len(req.Credenciales) > 0 {
|
|
enc, err := services.CifrarCredencialesCanal(req.Credenciales)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
updates["credenciales_enc"] = enc
|
|
updates["ultimo_error"] = ""
|
|
}
|
|
if err := models.UpdateUmindCanal(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindCanalHandler(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.DeleteUmindCanal(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Chat de prueba ─────────────────────────────────────────────────────────
|
|
|
|
// UmindChatPruebaHandler deja que el staff pruebe un agente puntual directo
|
|
// desde el panel, sin pasar por site_key/dominio (ya está gateado por la
|
|
// sesión con la que se llega acá).
|
|
func UmindChatPruebaHandler(c *fiber.Ctx) error {
|
|
var req struct {
|
|
AgenteID uint `json:"agente_id"`
|
|
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": "body inválido"})
|
|
}
|
|
if strings.TrimSpace(req.Mensaje) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "mensaje requerido"})
|
|
}
|
|
agente, err := models.GetUmindAgenteByID(req.AgenteID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
|
|
}
|
|
sessionID := strings.TrimSpace(req.SessionID)
|
|
if sessionID == "" {
|
|
sessionID = "staff-preview:" + strconv.FormatUint(uint64(extraerUserID(c)), 10)
|
|
}
|
|
respuesta, err := services.ProcessWidgetMessage(agente, sessionID, strings.TrimSpace(req.Mensaje))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"session_id": sessionID, "respuesta": respuesta})
|
|
}
|