- Nueva config "whisper" reutilizando /app/ai-config (credenciales/endpoint
validables) y servicio de transcripción compatible con OpenAI/Groq.
- El webhook de Telegram ahora detecta notas de voz/audio, las transcribe
y procesa el texto resultante como si el usuario lo hubiera escrito.
- Fix: subir un archivo adjunto en un comentario de tarea sin escribir
texto era rechazado por el backend ("contenido requerido") aunque el
frontend sí lo permitía.
197 lines
7.1 KiB
Go
197 lines
7.1 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// AiConfig almacena las configuraciones de proveedores de IA (Qwen, OpenAI, etc.)
|
|
// que son usadas por el Landing Generator y otros módulos.
|
|
type AiConfig struct {
|
|
gorm.Model
|
|
Nombre string `gorm:"size:100;not null" json:"nombre"` // Alias amigable, ej: "Qwen 2.5 Producción"
|
|
Provider string `gorm:"size:50;not null" json:"provider"` // qwen | openai | anthropic | etc
|
|
ApiKey string `gorm:"type:text;not null" json:"api_key"` // Clave de API
|
|
BaseURL string `gorm:"type:text" json:"base_url"` // URL base (override), vacío = default del provider
|
|
ModelName string `gorm:"size:100" json:"model_name"` // ej: qwen2.5-72b-instruct
|
|
IsActive bool `gorm:"default:true" json:"is_active"` // Solo uno activo a la vez
|
|
Notes string `gorm:"type:text" json:"notes"`
|
|
// Modulo indica a qué servicio pertenece esta config.
|
|
// "" = global (disponible para todos como fallback)
|
|
// "landing" = exclusivo para Landing Generator
|
|
// "query_runner" = exclusivo para Query Runner SQL
|
|
Modulo string `gorm:"size:50;default:''" json:"modulo"`
|
|
// Agente Telegram: si EsAgenteBot=true, esta config es el cerebro del bot administrador.
|
|
// Solo debe haber una config activa como agente a la vez.
|
|
EsAgenteBot bool `gorm:"default:false" json:"es_agente_bot"`
|
|
TelegramConfigID *uint `gorm:"index" json:"telegram_config_id"`
|
|
}
|
|
|
|
func (AiConfig) TableName() string { return "ai_configs" }
|
|
|
|
func GetAllAiConfigs(limit, offset int, search string) ([]AiConfig, int64, error) {
|
|
var items []AiConfig
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&AiConfig{})
|
|
if search != "" {
|
|
db = db.Where("nombre LIKE ? OR provider LIKE ?", "%"+search+"%", "%"+search+"%")
|
|
}
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func CreateAiConfig(item *AiConfig) error {
|
|
return app.Http.Database.DB.Create(item).Error
|
|
}
|
|
|
|
func UpdateAiConfig(id uint, updates map[string]interface{}) error {
|
|
return app.Http.Database.DB.Model(&AiConfig{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
func DeleteAiConfig(id uint) error {
|
|
return app.Http.Database.DB.Delete(&AiConfig{}, id).Error
|
|
}
|
|
|
|
func GetAiConfigByID(id uint, out *AiConfig) error {
|
|
return app.Http.Database.DB.First(out, id).Error
|
|
}
|
|
|
|
// GetActiveAiConfig retorna la primera configuración activa del provider indicado.
|
|
// Si provider está vacío, retorna cualquier config activa.
|
|
func GetActiveAiConfig(provider string) (*AiConfig, error) {
|
|
var item AiConfig
|
|
db := app.Http.Database.DB.Where("is_active = ?", true)
|
|
if provider != "" {
|
|
db = db.Where("provider = ?", provider)
|
|
}
|
|
if err := db.First(&item).Error; err != nil {
|
|
log.Printf("[AI_CONFIG] No se encontró config activa para provider '%s': %v", provider, err)
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
// SplitModulos parte el campo Modulo (comma-separated) en un slice limpio.
|
|
// "" → [] (config global), "landing,query_runner" → ["landing","query_runner"]
|
|
func SplitModulos(modulo string) []string {
|
|
var out []string
|
|
for _, s := range strings.Split(modulo, ",") {
|
|
if s = strings.TrimSpace(s); s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// JoinModulos normaliza y une un slice de módulos en comma-separated.
|
|
func JoinModulos(modules []string) string {
|
|
var clean []string
|
|
for _, s := range modules {
|
|
if s = strings.TrimSpace(s); s != "" {
|
|
clean = append(clean, s)
|
|
}
|
|
}
|
|
return strings.Join(clean, ",")
|
|
}
|
|
|
|
func GetAiConfigSelect() ([]AiConfig, error) {
|
|
var items []AiConfig
|
|
if err := app.Http.Database.DB.Model(&AiConfig{}).Select("id, nombre, provider").Order("nombre ASC").Find(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// GetAgenteBotConfig retorna la config marcada como agente Telegram, con su TelegramConfig cargada.
|
|
func GetAgenteBotConfig() (*AiConfig, *TelegramConfig, error) {
|
|
var ai AiConfig
|
|
if err := app.Http.Database.DB.Where("es_agente_bot = ? AND is_active = ?", true, true).First(&ai).Error; err != nil {
|
|
return nil, nil, fmt.Errorf("no hay agente bot configurado: %w", err)
|
|
}
|
|
if ai.TelegramConfigID == nil {
|
|
return &ai, nil, fmt.Errorf("el agente no tiene bot de Telegram asignado")
|
|
}
|
|
tg, err := GetTelegramConfigByID(*ai.TelegramConfigID)
|
|
if err != nil {
|
|
return &ai, nil, fmt.Errorf("bot de Telegram no encontrado: %w", err)
|
|
}
|
|
return &ai, tg, nil
|
|
}
|
|
|
|
// GetAgenteBotAiConfig retorna solo la config de IA marcada como agente (el mismo
|
|
// "cerebro" que usa el bot de Telegram), sin exigir que tenga un bot de Telegram
|
|
// asignado. La usa el chat propio del dashboard para compartir el mismo motor.
|
|
func GetAgenteBotAiConfig() (*AiConfig, error) {
|
|
var ai AiConfig
|
|
if err := app.Http.Database.DB.Where("es_agente_bot = ? AND is_active = ?", true, true).First(&ai).Error; err != nil {
|
|
return nil, fmt.Errorf("no hay agente configurado: %w", err)
|
|
}
|
|
return &ai, nil
|
|
}
|
|
|
|
// GetAiConfigForService retorna la config activa asignada al módulo indicado.
|
|
// Lógica de prioridad:
|
|
// 1. Config activa con modulo conteniendo service (puede ser comma-separated)
|
|
// 2. Config activa con modulo == "" (global, fallback)
|
|
// 3. Cualquier config activa (último recurso)
|
|
func GetAiConfigForService(service string) (*AiConfig, error) {
|
|
var items []AiConfig
|
|
if err := app.Http.Database.DB.Where("is_active = ?", true).Order("id ASC").Find(&items).Error; err != nil {
|
|
return nil, fmt.Errorf("error leyendo ai_configs: %w", err)
|
|
}
|
|
if len(items) == 0 {
|
|
return nil, fmt.Errorf("no hay configs de IA activas")
|
|
}
|
|
|
|
// 1. Config específica para el servicio (puede tener varios módulos)
|
|
if service != "" {
|
|
for i := range items {
|
|
for _, m := range SplitModulos(items[i].Modulo) {
|
|
if m == service {
|
|
return &items[i], nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Config global (modulo vacío = sin restricción de servicio)
|
|
for i := range items {
|
|
if strings.TrimSpace(items[i].Modulo) == "" {
|
|
return &items[i], nil
|
|
}
|
|
}
|
|
|
|
// 3. Cualquier config activa como último recurso
|
|
log.Printf("[AI_CONFIG] No se encontró config para servicio '%s', usando cualquier activa", service)
|
|
return &items[0], nil
|
|
}
|
|
|
|
// GetWhisperConfig retorna la config de IA activa etiquetada específicamente con
|
|
// el módulo "whisper" (transcripción de audio). A diferencia de
|
|
// GetAiConfigForService, NO cae a una config global: si el admin no configuró
|
|
// una explícitamente para whisper, es mejor avisar con claridad que intentar
|
|
// transcribir contra un proveedor que no soporta ese endpoint (ej. Anthropic).
|
|
func GetWhisperConfig() (*AiConfig, error) {
|
|
var items []AiConfig
|
|
if err := app.Http.Database.DB.Where("is_active = ?", true).Find(&items).Error; err != nil {
|
|
return nil, fmt.Errorf("error leyendo ai_configs: %w", err)
|
|
}
|
|
for i := range items {
|
|
for _, m := range SplitModulos(items[i].Modulo) {
|
|
if m == "whisper" {
|
|
return &items[i], nil
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("no hay ninguna configuración activa con el módulo 'whisper' en /app/ai-config")
|
|
}
|