Files
Lizandro GuarnizoandClaude Opus 5 d7c266f111 feat(soporte): filtro con IA para los correos entrantes, y que un correo no pueda abrir dos tickets
Filtro (opcional, apagado por defecto): antes de abrir ticket, la IA lee el
correo y decide si es soporte o ruido —newsletters, notificaciones de
plataformas, facturas de proveedores, spam—. Falla abierto: si la IA no está
configurada o se cae, el ticket se abre igual. Ignorar a un cliente es peor que
tener un ticket de más. Hay un campo de contexto del negocio para los casos
raros de cada uno.

Los correos masivos se descartan por sus propios encabezados (List-Unsubscribe,
Precedence, Auto-Submitted) sin gastar una llamada de IA.

Y el motivo por el que el mismo correo se leía y se contestaba de nuevo: la
deduplicación era solo un SELECT previo, que no sirve si el flag de leído no
llegó a guardarse o si dos instancias leen el buzón a la vez. Ahora hay un
índice único parcial sobre message_id: la base rechaza el segundo, y ese rechazo
se trata como "ya estaba", no como error — sin ticket repetido y sin segunda
auto-respuesta.

Si algún correo no se pudo marcar como leído, "Revisar buzón ahora" lo dice en
vez de dejarlo solo en el log: es la señal de que van a volver a leerse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:36:01 -05:00

251 lines
8.5 KiB
Go
Executable File

package main
import (
"flag"
"log"
"os"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/pyroscope-io/pyroscope/pkg/agent/profiler"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/config"
"github.com/sujit-baniya/fiber-boilerplate/migrations"
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
"github.com/sujit-baniya/fiber-boilerplate/rest/routes"
"github.com/sujit-baniya/fiber-boilerplate/utils"
)
func main() {
configFile := flag.String("config", "config.yml", "User Config file from user")
migrate := flag.Bool("migrate", false, "Update db structure")
flag.Parse()
// Cargar configuración
app.Load(*configFile)
// Configurar Pyroscope profiler
if app.Http.Profiler.Enabled {
_, _ = profiler.Start(profiler.Config{
ApplicationName: app.Http.Server.Name,
ServerAddress: app.Http.Profiler.Server,
})
}
// Actualizar versión del servidor
app.Http.Server.Version = app.Version
// Los archivos de /uploads dejan de ser públicos: solo se sirven a usuarios
// con sesión iniciada en el panel. Los clientes del portal siguen bajando sus
// documentos por los endpoints dedicados, que validan que les pertenezcan.
config.UploadsGuard = func(c *fiber.Ctx) bool {
return auth.IsLoggedIn(c)
}
// Habilitar CORS
app.Http.Server.App.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Content-Type, Authorization",
}))
if *migrate {
// Ejecutar migraciones
migrations.Migrate()
} else {
// Crear/actualizar tablas de Renovaciones e idempotente)
migrations.MigrateRenovaciones()
// Crear/actualizar tablas de Portal, Telegram y Proyectos
migrations.MigratePortal()
// Crear tabla de Landing Generator
migrations.MigrateLanding()
// Crear tablas de integraciones y pasarelas si no existen.
// AutoMigrate(a, b, c...) se detiene en el primer modelo que falle: todo lo
// que viene después en la lista se queda sin tabla, en silencio (fue la
// causa real de que plantillas_documento/tarifas desaparecieran de
// producción — un modelo antes en la lista falló y todo lo posterior no se
// llegó a crear). Migrando uno por uno, un modelo roto no bloquea al resto.
modelosBase := []interface{}{
// Tablas base del sistema (columnas nuevas)
&models.Users{},
&models.Roles{},
&models.Submodules{},
&models.QueryHistory{},
&models.OssApi{},
&models.QrVcard{},
&models.VcfVcard{},
&models.HostingerConfig{},
&models.CloudflareConfig{},
&models.BoldConfig{},
&models.BoldWebhookLog{},
&models.BoldCallbackLog{},
&models.DlocalPaymentLog{},
// Documentación por SaaS
&models.SaasProducto{},
&models.DocCategoria{},
&models.DocPagina{},
// Integraciones SaaS (dispatcher de pagos)
&models.SaasApiConfig{},
&models.SaasDispatchLog{},
// Notificaciones
&models.NotifEventoConfig{},
&models.SistemaNotificacion{},
&models.ServidorAlertaUmbral{},
// Shield
&models.ShieldConfig{},
// Partner
&models.PartnerRecurso{},
&models.PartnerComunicado{},
// Portal: recuperación de contraseña
&models.PortalPasswordResetToken{},
// Integración VCard API Admin
&models.VcardApiConfig{},
// Integración Coolify (multi-instancia)
&models.CoolifyConfig{},
// AI Config: campos del agente Telegram
&models.AiConfig{},
// Agente Telegram: historial y chats autorizados
&models.TelegramAgentHistory{},
&models.TelegramAgentAuth{},
// Servidores: nuevos campos de agente + tabla join de integraciones
&models.Servidor{},
&models.ConxDb{},
&models.ProvServidor{},
// SMTP
&models.SmtpConfig{},
// WebSMS (LabsMobile)
&models.WebSmsConfig{},
&models.WebSmsLog{},
&models.WebSmsWebhookLog{},
// Contabilidad
&models.Cuenta{},
&models.Entidad{},
&models.Transaccion{},
&models.CuentaCobro{},
&models.CuentaPagar{},
&models.ConsolidadoMensual{},
// Historial de métricas del agente
&models.ServidorMetricasHistory{},
// Monitor de disponibilidad de URLs
&models.UrlMonitor{},
&models.UrlMonitorLog{},
// Tablero de tareas
&models.Tarea{},
&models.TareaComentario{},
// Pagos externos: API de cobros para aplicaciones de terceros
&models.ServicioPagoExterno{},
&models.SolicitudPagoExterna{},
// Soporte: webhook de correo entrante
&models.SoporteWebhookConfig{},
// Automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
&models.PlantillaDocumento{},
&models.Tarifa{},
&models.DocumentoGenerado{},
&models.Arquitectura{},
// Vinculación de Telegram para staff interno
&models.TelegramStaffToken{},
// uMind: chat con IA embebible por tenant (widget web + RAG)
&models.UmindTenant{},
&models.UmindAgente{},
&models.UmindDocumento{},
&models.UmindChunk{},
&models.UmindMensaje{},
&models.UmindHerramienta{},
&models.UmindCanal{},
&models.UmindConexion{},
&models.UmindEventoLog{},
&models.UmindPlan{},
&models.UmindUso{},
// API Keys de /api/v2 (token + IP obligatoria + scopes)
&models.ApiKey{},
// Integraciones: OCR y transcripción de audio (servicios propios)
&models.OcrConfig{},
&models.WhisperAsrConfig{},
}
for _, m := range modelosBase {
if err := app.Http.Database.DB.AutoMigrate(m); err != nil {
log.Printf("[MIGRATE] Error migrando %T: %v", m, err)
}
}
// Seed automático (idempotente) de módulos del sistema
migrations.SeedRenovaciones()
migrations.SeedIntegraciones()
migrations.SeedPasarelas()
migrations.SeedSaas()
migrations.SeedServidores()
migrations.SeedAdministracion()
migrations.SeedPlantillasBase()
migrations.SeedTelegram()
migrations.SeedPortalClientes()
migrations.SeedNotifDefaults()
migrations.SeedShield()
migrations.SeedPartnerRecursos()
models.SeedContabilidad()
// SeedBalanceData carga transacciones contables reales de 2026. Ya no se
// ejecuta en cada arranque: si el contador editaba o borraba una de esas
// filas, el siguiente reinicio la recreaba y el balance quedaba duplicado.
// Para recargarla puntualmente: SEED_BALANCE=1 ./apiv2
if os.Getenv("SEED_BALANCE") == "1" {
log.Println("[SEED] SEED_BALANCE=1 — cargando datos de balance")
models.SeedBalanceData()
}
migrations.SeedContabilidadMenu()
migrations.SeedWebSms()
migrations.SeedUrlMonitor()
migrations.SeedTareas()
migrations.SeedPagosExternos()
migrations.SeedAutomatizacionIA()
migrations.SeedUmind()
migrations.SeedDocumentacion()
// Va antes de MigrarUmindAgentes: libera las columnas huérfanas que
// dejó el refactor multi-agente. Sin esto no se puede insertar nada en
// uMind (ver el comentario de la función).
migrations.LiberarColumnasHuerfanasUmind()
migrations.IndicesUnicosMessageID()
migrations.MigrarUmindAgentes()
migrations.SeedApiKeys()
if n, err := models.RepararEstadosTareaInvalidos(); err != nil {
log.Printf("[FIX] Error reparando estados de tareas: %v", err)
} else if n > 0 {
log.Printf("[FIX] %d tarea(s) con estado inválido reparadas (ahora visibles en el Kanban)", n)
}
// Cargar config SMTP desde BD (sobreescribe valores del .env/config.yml si hay registro activo)
if smtpCfg, err := models.GetSmtpConfig(); err == nil {
app.Http.Mail.Host = smtpCfg.Host
app.Http.Mail.Port = smtpCfg.Port
app.Http.Mail.Username = smtpCfg.Username
app.Http.Mail.Password = utils.Decrypt(smtpCfg.Password, app.Http.Server.Key)
app.Http.Mail.Encryption = smtpCfg.Encryption
app.Http.Mail.FromAddress = smtpCfg.FromAddress
app.Http.Mail.FromName = smtpCfg.FromName
app.Http.Mail.SetupMailer()
log.Println("[SMTP] Configuración cargada desde base de datos")
} else {
log.Println("[SMTP] Sin config en BD, usando valores del entorno")
}
// Iniciar cron de vencimientos
services.IniciarCron()
defer services.DetenerCron()
// Cargar rutas
routes.LoadRoutes(app.Http.Server.App)
// Con las rutas ya montadas se puede avisar de los ítems del menú que
// apuntan a una URL inexistente — el síntoma es un 404 que parece un
// problema de permisos.
rutasGET := map[string]bool{}
for _, capa := range app.Http.Server.Stack() {
for _, r := range capa {
if r.Method == "GET" {
rutasGET[r.Path] = true
}
}
}
migrations.VerificarUrlsDeSubmodulos(rutasGET)
app.Http.Route404()
log.Fatal(app.Http.Server.ServeWithGraceFullShutdown())
}
}