feat(umind): vigilancia de APIs — avisa cuando pasa algo, sin quemar plata
"Avisame cuando esta API devuelva stock en cero." La vigilancia apunta a una HERRAMIENTA ya configurada del agente y nunca a una URL libre: así hereda entero el blindaje de LlamarHerramientaWebhook —DNS resuelto en el connect, IPs internas rechazadas, sin redirects, respuesta acotada— sin reimplementar una línea de eso. Hay un test que falla si alguien le agrega un campo url. El ahorro que hace viable la feature: se hashea la respuesta y, si no cambió, no hay nada que evaluar y no se gasta un token. Preguntarle a la IA en cada chequeo serían 1440 consultas diarias por cliente para responder algo que ya sabíamos. El test verifica que el corte por hash esté ANTES de la evaluación. Y se avisa al ENTRAR en condición, no en cada chequeo que la siga cumpliendo: una alerta que llega cada media hora deja de leerse a la segunda. Es la lección que el monitor de sitios ya había aprendido con las transiciones de estado. Solo un SI explícito dispara el aviso. Un modelo que devuelve una explicación, un error o cualquier otra cosa se toma como que no: un aviso que no llega molesta menos que uno que despierta a alguien a las 3 AM sin motivo. Si el envío del aviso falla, no se marca como avisado — se reintenta al siguiente ciclo. Y tras diez fallos seguidos la vigilancia se apaga sola: una API que dejó de existir no puede consultarse para siempre ni seguir gastando el plan de quien la configuró. Máximo 5 por agente, mínimo 5 minutos de intervalo. Solo canal interno: cada vigilancia es una llamada saliente recurrente y consultas a la IA, o sea plan del dueño gastado por quien la pida. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
047837dd23
commit
7ddc3f5286
@@ -0,0 +1,106 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UmindVigilancia mira una API cada tanto y avisa cuando pasa algo:
|
||||
// "avisame por correo cuando esta API devuelva stock en cero".
|
||||
//
|
||||
// Apunta a una HERRAMIENTA ya configurada del agente y no a una URL libre.
|
||||
// Así hereda entero el blindaje de LlamarHerramientaWebhook —resolución de DNS
|
||||
// en el connect, IPs internas rechazadas, sin redirects, respuesta acotada—
|
||||
// sin escribir una línea de eso de nuevo.
|
||||
type UmindVigilancia struct {
|
||||
gorm.Model
|
||||
AgenteID uint `json:"agente_id" gorm:"column:agente_id;index;not null"`
|
||||
TenantID uint `json:"tenant_id" gorm:"column:tenant_id;index"`
|
||||
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
||||
Herramienta string `json:"herramienta" gorm:"column:herramienta;size:60;not null"`
|
||||
// Condicion en lenguaje natural: "el stock está en cero", "hay pedidos sin
|
||||
// despachar hace más de dos días".
|
||||
Condicion string `json:"condicion" gorm:"column:condicion;type:text;not null"`
|
||||
IntervaloMin int `json:"intervalo_min" gorm:"column:intervalo_min;default:30"`
|
||||
Destino string `json:"destino" gorm:"column:destino;size:120"`
|
||||
Activa bool `json:"activa" gorm:"column:activa;default:true"`
|
||||
// HashUltimo es lo que evita quemar plata: si la respuesta no cambió, no
|
||||
// hay nada que evaluar y no se gasta un token.
|
||||
HashUltimo string `json:"-" gorm:"column:hash_ultimo;size:64"`
|
||||
// EnCondicion recuerda si la última evaluación dio que sí. Se avisa al
|
||||
// ENTRAR en condición, no en cada chequeo que la siga cumpliendo — es la
|
||||
// lección que el monitor de sitios ya aprendió con las transiciones.
|
||||
EnCondicion bool `json:"en_condicion" gorm:"column:en_condicion;default:false"`
|
||||
UltimoCheckAt *time.Time `json:"ultimo_check_at" gorm:"column:ultimo_check_at"`
|
||||
UltimoError string `json:"ultimo_error" gorm:"column:ultimo_error;type:text"`
|
||||
FallosNoOk int `json:"-" gorm:"column:fallos_no_ok;default:0"`
|
||||
}
|
||||
|
||||
func (UmindVigilancia) TableName() string { return "umind_vigilancias" }
|
||||
|
||||
// UmindVigilanciaMax acota cuántas puede tener un agente: cada una es una
|
||||
// llamada saliente y, cuando cambia, una consulta a la IA.
|
||||
const UmindVigilanciaMax = 5
|
||||
|
||||
// UmindVigilanciaIntervaloMin es el piso. Menos que esto es martillarle la API
|
||||
// a un tercero para ganar segundos que un aviso no necesita.
|
||||
const UmindVigilanciaIntervaloMin = 5
|
||||
|
||||
func CreateUmindVigilancia(v *UmindVigilancia) error {
|
||||
var n int64
|
||||
app.Http.Database.DB.Model(&UmindVigilancia{}).
|
||||
Where("agente_id = ? AND activa = ? AND deleted_at IS NULL", v.AgenteID, true).Count(&n)
|
||||
if n >= UmindVigilanciaMax {
|
||||
return gorm.ErrInvalidData
|
||||
}
|
||||
if v.IntervaloMin < UmindVigilanciaIntervaloMin {
|
||||
v.IntervaloMin = 30
|
||||
}
|
||||
return app.Http.Database.DB.Create(v).Error
|
||||
}
|
||||
|
||||
func GetUmindVigilanciasByAgente(agenteID uint) ([]UmindVigilancia, error) {
|
||||
var items []UmindVigilancia
|
||||
err := app.Http.Database.DB.Where("agente_id = ?", agenteID).
|
||||
Order("created_at DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func GetUmindVigilanciaByID(id uint) (*UmindVigilancia, error) {
|
||||
var v UmindVigilancia
|
||||
if err := app.Http.Database.DB.First(&v, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func DeleteUmindVigilancia(id uint) error {
|
||||
return app.Http.Database.DB.Delete(&UmindVigilancia{}, id).Error
|
||||
}
|
||||
|
||||
// GetUmindVigilanciasPendientes devuelve las que ya les toca según su propio
|
||||
// intervalo, igual que los canales de correo.
|
||||
func GetUmindVigilanciasPendientes() ([]UmindVigilancia, error) {
|
||||
var items []UmindVigilancia
|
||||
err := app.Http.Database.DB.
|
||||
Where("activa = ? AND deleted_at IS NULL", true).
|
||||
Where("ultimo_check_at IS NULL OR ultimo_check_at < NOW() - (intervalo_min || ' minutes')::interval").
|
||||
Limit(200).Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func ActualizarVigilancia(id uint, updates map[string]interface{}) {
|
||||
ahora := time.Now()
|
||||
updates["ultimo_check_at"] = &ahora
|
||||
app.Http.Database.DB.Model(&UmindVigilancia{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
|
||||
// DesactivarVigilancia la apaga tras demasiados fallos seguidos. Una API que
|
||||
// dejó de existir no puede seguir consultándose para siempre — ni gastando el
|
||||
// plan de quien la configuró.
|
||||
func DesactivarVigilancia(id uint, motivo string) {
|
||||
app.Http.Database.DB.Model(&UmindVigilancia{}).Where("id = ?", id).
|
||||
Updates(map[string]interface{}{"activa": false, "ultimo_error": motivo})
|
||||
}
|
||||
Reference in New Issue
Block a user