feat: uMind pasa a multi-agente por tenant
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8eba3ab97f
commit
f3f2f421d6
@@ -21,7 +21,22 @@ func UmindIndex(c *fiber.Ctx) error {
|
||||
}, "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"))
|
||||
@@ -45,26 +60,9 @@ func GetUmindTenants(c *fiber.Ctx) error {
|
||||
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"`
|
||||
Color string `json:"color"`
|
||||
Activo bool `json:"activo"`
|
||||
}
|
||||
|
||||
var umindColorHexRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
||||
|
||||
// colorTenant 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 colorTenant(color string) string {
|
||||
color = strings.TrimSpace(color)
|
||||
if umindColorHexRegex.MatchString(color) {
|
||||
return color
|
||||
}
|
||||
return "#8eb02f"
|
||||
}
|
||||
|
||||
func (r umindTenantReq) dominiosLimpios() []string {
|
||||
var out []string
|
||||
for _, d := range r.DominiosPermitidos {
|
||||
@@ -92,17 +90,13 @@ func CreateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
tenant := &models.UmindTenant{
|
||||
Nombre: strings.TrimSpace(req.Nombre),
|
||||
DominiosPermitidos: strings.Join(dominios, ","),
|
||||
AiConfigID: req.AiConfigID,
|
||||
Tono: req.Tono,
|
||||
MensajeBienvenida: req.MensajeBienvenida,
|
||||
Color: colorTenant(req.Color),
|
||||
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})
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": tenant.ID})
|
||||
}
|
||||
|
||||
func UpdateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
@@ -118,10 +112,6 @@ func UpdateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
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,
|
||||
"color": colorTenant(req.Color),
|
||||
"activo": req.Activo,
|
||||
}
|
||||
if err := models.UpdateUmindTenant(uint(id), updates); err != nil {
|
||||
@@ -141,14 +131,105 @@ func DeleteUmindTenantHandler(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// ─── Documentos / ingesta ────────────────────────────────────────────────────
|
||||
// ─── 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 GetUmindDocumentosHandler(c *fiber.Ctx) error {
|
||||
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.GetUmindDocumentosByTenant(uint(tenantID))
|
||||
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()})
|
||||
}
|
||||
@@ -160,25 +241,25 @@ func GetUmindDocumentosHandler(c *fiber.Ctx) error {
|
||||
// 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"`
|
||||
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.TenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
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.GetUmindTenantByID(req.TenantID); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
|
||||
if _, err := models.GetUmindAgenteByID(req.AgenteID); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
|
||||
}
|
||||
|
||||
doc := &models.UmindDocumento{
|
||||
TenantID: req.TenantID,
|
||||
AgenteID: req.AgenteID,
|
||||
Tipo: "url",
|
||||
Origen: strings.TrimSpace(req.URL),
|
||||
Estado: "procesando",
|
||||
@@ -187,7 +268,7 @@ func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
go services.IngestarTenant(req.TenantID, doc.ID, doc.Origen, req.MaxPaginas)
|
||||
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"})
|
||||
}
|
||||
@@ -206,11 +287,11 @@ func DeleteUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
// ─── 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"})
|
||||
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(tenantID), 50)
|
||||
items, err := models.GetUmindSesiones(uint(agenteID), 50)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -218,15 +299,15 @@ func GetUmindSesionesHandler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
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"})
|
||||
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(tenantID), sessionID, 200)
|
||||
items, err := models.GetUmindHistorial(uint(agenteID), sessionID, 200)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -238,11 +319,11 @@ func GetUmindHistorialHandler(c *fiber.Ctx) error {
|
||||
var umindNombreToolRegex = regexp.MustCompile(`^[a-z][a-z0-9_]{2,63}$`)
|
||||
|
||||
func GetUmindHerramientasHandler(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"})
|
||||
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.GetUmindHerramientasByTenant(uint(tenantID))
|
||||
items, err := models.GetUmindHerramientasByAgente(uint(agenteID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -250,7 +331,7 @@ func GetUmindHerramientasHandler(c *fiber.Ctx) error {
|
||||
out := make([]fiber.Map, len(items))
|
||||
for i, h := range items {
|
||||
out[i] = fiber.Map{
|
||||
"ID": h.ID, "tenant_id": h.TenantID, "nombre": h.Nombre, "descripcion": h.Descripcion,
|
||||
"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,
|
||||
}
|
||||
@@ -259,7 +340,7 @@ func GetUmindHerramientasHandler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type umindHerramientaReq struct {
|
||||
TenantID uint `json:"tenant_id"`
|
||||
AgenteID uint `json:"agente_id"`
|
||||
Nombre string `json:"nombre"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
Parametros []models.UmindHerramientaParametro `json:"parametros"`
|
||||
@@ -274,8 +355,8 @@ func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
|
||||
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 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)"})
|
||||
@@ -297,7 +378,7 @@ func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
h := &models.UmindHerramienta{
|
||||
TenantID: req.TenantID, Nombre: req.Nombre, Descripcion: strings.TrimSpace(req.Descripcion),
|
||||
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,
|
||||
}
|
||||
@@ -363,11 +444,11 @@ func DeleteUmindHerramientaHandler(c *fiber.Ctx) error {
|
||||
// ─── Canales (Telegram / WhatsApp) ─────────────────────────────────────────
|
||||
|
||||
func GetUmindCanalesHandler(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"})
|
||||
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.GetUmindCanalesByTenant(uint(tenantID))
|
||||
items, err := models.GetUmindCanalesByAgente(uint(agenteID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -380,7 +461,7 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
|
||||
webhookURL = fmt.Sprintf("%s/webhooks/umind-whatsapp/%s", app.Http.Server.Url, canal.WebhookSecret)
|
||||
}
|
||||
out[i] = fiber.Map{
|
||||
"ID": canal.ID, "tenant_id": canal.TenantID, "tipo": canal.Tipo, "activo": canal.Activo,
|
||||
"ID": canal.ID, "agente_id": canal.AgenteID, "tipo": canal.Tipo, "activo": canal.Activo,
|
||||
"webhook_url": webhookURL, "ultimo_error": canal.UltimoError,
|
||||
}
|
||||
}
|
||||
@@ -388,7 +469,7 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
type umindCanalReq struct {
|
||||
TenantID uint `json:"tenant_id"`
|
||||
AgenteID uint `json:"agente_id"`
|
||||
Tipo string `json:"tipo"`
|
||||
Credenciales map[string]string `json:"credenciales"`
|
||||
Activo bool `json:"activo"`
|
||||
@@ -399,8 +480,8 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
|
||||
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 req.AgenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
switch req.Tipo {
|
||||
case "telegram":
|
||||
@@ -421,7 +502,7 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
canal := &models.UmindCanal{TenantID: req.TenantID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc}
|
||||
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()})
|
||||
}
|
||||
@@ -472,12 +553,12 @@ func DeleteUmindCanalHandler(c *fiber.Ctx) error {
|
||||
|
||||
// ─── Chat de prueba ─────────────────────────────────────────────────────────
|
||||
|
||||
// UmindChatPruebaHandler deja que el staff pruebe el agente de un tenant
|
||||
// directo desde el panel, sin pasar por site_key/dominio (ya está gateado
|
||||
// por la sesión con la que se llega acá).
|
||||
// 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 {
|
||||
TenantID uint `json:"tenant_id"`
|
||||
AgenteID uint `json:"agente_id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Mensaje string `json:"mensaje"`
|
||||
}
|
||||
@@ -487,15 +568,15 @@ func UmindChatPruebaHandler(c *fiber.Ctx) error {
|
||||
if strings.TrimSpace(req.Mensaje) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "mensaje requerido"})
|
||||
}
|
||||
tenant, err := models.GetUmindTenantByID(req.TenantID)
|
||||
agente, err := models.GetUmindAgenteByID(req.AgenteID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
|
||||
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(tenant, sessionID, strings.TrimSpace(req.Mensaje))
|
||||
respuesta, err := services.ProcessWidgetMessage(agente, sessionID, strings.TrimSpace(req.Mensaje))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user