feat: agente Telegram con IA + Coolify multi-instancia
- Coolify: soporte multi-instancia (CRUD de configs, ?config_id= en todos los endpoints, endpoints expandidos para services/databases/teams/envs) - AiConfig: campos es_agente_bot + telegram_config_id para marcar qué config de IA actúa como cerebro del bot administrador - TelegramAgentHistory + TelegramAgentAuth: historial de conversación por chat_id y whitelist de chats autorizados - Agent Engine: function calling OpenAI-compatible con 25+ herramientas (clientes, contratos, contabilidad, proyectos, tickets, tareas, Coolify multi-instancia, servidores, monitores URL) - Webhook POST /webhooks/telegram-agent/:bot_token (público, sin sesión) - API /api/v2/agent/auth y /api/v2/agent/history para administrar el agente - AutoMigrate: AiConfig, TelegramAgentHistory, TelegramAgentAuth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
50812749ea
commit
33cfe4fdb0
@@ -0,0 +1,154 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// ─── Tipos del update de Telegram ────────────────────────────────────────────
|
||||
|
||||
type tgUser struct {
|
||||
ID int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type tgChat struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type tgMessage struct {
|
||||
MessageID int `json:"message_id"`
|
||||
From tgUser `json:"from"`
|
||||
Chat tgChat `json:"chat"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type tgUpdate struct {
|
||||
UpdateID int `json:"update_id"`
|
||||
Message *tgMessage `json:"message"`
|
||||
}
|
||||
|
||||
// ─── Webhook handler ──────────────────────────────────────────────────────────
|
||||
|
||||
// TelegramAgentWebhook recibe updates del bot administrador.
|
||||
// Telegram llama aquí cuando alguien escribe al bot.
|
||||
// Ruta: POST /webhooks/telegram-agent/:bot_token
|
||||
func TelegramAgentWebhook(c *fiber.Ctx) error {
|
||||
botToken := c.Params("bot_token")
|
||||
if botToken == "" {
|
||||
return c.SendStatus(400)
|
||||
}
|
||||
|
||||
// Parsear el update
|
||||
var update tgUpdate
|
||||
if err := c.BodyParser(&update); err != nil {
|
||||
return c.SendStatus(200) // siempre 200 a Telegram
|
||||
}
|
||||
if update.Message == nil || strings.TrimSpace(update.Message.Text) == "" {
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
|
||||
msg := update.Message
|
||||
chatID := msg.Chat.ID
|
||||
text := strings.TrimSpace(msg.Text)
|
||||
|
||||
// Buscar el config del agente que tenga este bot token
|
||||
ai, tgCfg, err := models.GetAgenteBotConfig()
|
||||
if err != nil || tgCfg == nil || tgCfg.BotToken != botToken {
|
||||
log.Printf("[AGENT_WEBHOOK] Token no corresponde a ningún agente activo")
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
|
||||
// Verificar autorización del chat (si hay whitelist configurada)
|
||||
if !models.IsAgentAuthChat(chatID) {
|
||||
// Si no hay ningún auth configurado, solo responder al chat_id del config
|
||||
tgChatIDStr := strings.TrimSpace(tgCfg.ChatID)
|
||||
tgChatID, _ := strconv.ParseInt(tgChatIDStr, 10, 64)
|
||||
if tgChatID != 0 && chatID != tgChatID {
|
||||
sendAgentReply(tgCfg.BotToken, chatID, "No tienes autorización para usar este agente.")
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
}
|
||||
|
||||
// Procesar en goroutine para responder 200 inmediatamente a Telegram
|
||||
go func() {
|
||||
response, err := services.ProcessAgentMessage(chatID, text, ai)
|
||||
if err != nil {
|
||||
log.Printf("[AGENT] Error procesando mensaje: %v", err)
|
||||
response = fmt.Sprintf("Error interno: %s", err.Error())
|
||||
}
|
||||
if response == "" {
|
||||
return
|
||||
}
|
||||
if sendErr := sendAgentReply(tgCfg.BotToken, chatID, response); sendErr != nil {
|
||||
log.Printf("[AGENT] Error enviando respuesta: %v", sendErr)
|
||||
}
|
||||
}()
|
||||
|
||||
return c.SendStatus(200)
|
||||
}
|
||||
|
||||
func sendAgentReply(botToken string, chatID int64, text string) error {
|
||||
svc := &services.TelegramService{BotToken: botToken}
|
||||
return svc.SendMessage(chatID, text)
|
||||
}
|
||||
|
||||
// ─── CRUD de chats autorizados ────────────────────────────────────────────────
|
||||
|
||||
func AgentAuthList(c *fiber.Ctx) error {
|
||||
items, err := models.GetAllAgentAuth()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": items})
|
||||
}
|
||||
|
||||
func AgentAuthCreate(c *fiber.Ctx) error {
|
||||
type Req struct {
|
||||
ChatID int64 `json:"chat_id"`
|
||||
Nombre string `json:"nombre"`
|
||||
}
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if req.ChatID == 0 {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "chat_id requerido"})
|
||||
}
|
||||
if err := models.CreateAgentAuth(req.ChatID, req.Nombre); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(201).JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
func AgentAuthDelete(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := models.DeleteAgentAuth(uint(id)); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// AgentHistoryClear borra el historial de conversación de un chat.
|
||||
func AgentHistoryClear(c *fiber.Ctx) error {
|
||||
chatID, err := strconv.ParseInt(c.Params("chat_id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "chat_id inválido"})
|
||||
}
|
||||
if err := models.ClearAgentHistory(chatID); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
Reference in New Issue
Block a user