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
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +14,16 @@ import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// filterQueryParam elimina un parámetro específico del querystring.
|
||||
func filterQueryParam(qs, param string) string {
|
||||
vals, err := url.ParseQuery(qs)
|
||||
if err != nil {
|
||||
return qs
|
||||
}
|
||||
vals.Del(param)
|
||||
return vals.Encode()
|
||||
}
|
||||
|
||||
// ─── helpers internos ────────────────────────────────────────────────────────
|
||||
|
||||
// coolifyDo ejecuta una petición a la API de Coolify y devuelve el body crudo.
|
||||
@@ -39,9 +51,21 @@ func coolifyDo(method, endpoint string, reqBody io.Reader, contentType string, c
|
||||
return body, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// coolifyResolveConfig retorna la config a usar según ?config_id= o la activa por defecto.
|
||||
func coolifyResolveConfig(c *fiber.Ctx) (*models.CoolifyConfig, error) {
|
||||
if idStr := c.Query("config_id"); idStr != "" {
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("config_id inválido")
|
||||
}
|
||||
return models.GetCoolifyConfigByID(uint(id))
|
||||
}
|
||||
return models.GetCoolifyConfig()
|
||||
}
|
||||
|
||||
// coolifyProxy resuelve config, aplica qs de la request y responde al frontend.
|
||||
func coolifyProxy(c *fiber.Ctx, method, endpoint string) error {
|
||||
cfg, err := models.GetCoolifyConfig()
|
||||
cfg, err := coolifyResolveConfig(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Configura primero la integración Coolify"})
|
||||
}
|
||||
@@ -49,8 +73,18 @@ func coolifyProxy(c *fiber.Ctx, method, endpoint string) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La integración Coolify está inactiva"})
|
||||
}
|
||||
|
||||
// Filtrar config_id del querystring para no pasarlo a Coolify
|
||||
qs := string(c.Request().URI().QueryString())
|
||||
if qs != "" {
|
||||
filtered := filterQueryParam(qs, "config_id")
|
||||
if filtered != "" {
|
||||
qs = filtered
|
||||
} else {
|
||||
qs = ""
|
||||
}
|
||||
}
|
||||
ep := endpoint
|
||||
if qs := string(c.Request().URI().QueryString()); qs != "" {
|
||||
if qs != "" {
|
||||
ep = endpoint + "?" + qs
|
||||
}
|
||||
|
||||
@@ -317,3 +351,105 @@ func CoolifyTeamMembers(c *fiber.Ctx) error {
|
||||
func CoolifyWebhook(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
// ─── CRUD de instancias Coolify ───────────────────────────────────────────────
|
||||
|
||||
func CoolifyListConfigs(c *fiber.Ctx) error {
|
||||
items, err := models.GetAllCoolifyConfigs()
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
// Ocultar tokens
|
||||
type safe struct {
|
||||
ID uint `json:"id"`
|
||||
Nombre string `json:"nombre"`
|
||||
BaseURL string `json:"base_url"`
|
||||
Activo bool `json:"activo"`
|
||||
}
|
||||
out := make([]safe, len(items))
|
||||
for i, cfg := range items {
|
||||
out[i] = safe{ID: cfg.ID, Nombre: cfg.Nombre, BaseURL: cfg.BaseURL, Activo: cfg.Activo}
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": out})
|
||||
}
|
||||
|
||||
func CoolifyCreateConfig(c *fiber.Ctx) error {
|
||||
type Req struct {
|
||||
Nombre string `json:"nombre"`
|
||||
BaseURL string `json:"base_url"`
|
||||
ApiToken string `json:"api_token"`
|
||||
Activo bool `json:"activo"`
|
||||
}
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
|
||||
if req.BaseURL == "" || req.ApiToken == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "base_url y api_token son requeridos"})
|
||||
}
|
||||
cfg := &models.CoolifyConfig{Nombre: req.Nombre, BaseURL: req.BaseURL, ApiToken: req.ApiToken, Activo: req.Activo}
|
||||
if err := models.CreateCoolifyConfig(cfg); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(201).JSON(fiber.Map{"ok": true, "id": cfg.ID})
|
||||
}
|
||||
|
||||
func CoolifyUpdateConfig(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"})
|
||||
}
|
||||
type Req struct {
|
||||
Nombre string `json:"nombre"`
|
||||
BaseURL string `json:"base_url"`
|
||||
ApiToken string `json:"api_token"`
|
||||
Activo bool `json:"activo"`
|
||||
}
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
|
||||
cfg, err := models.UpdateCoolifyConfig(uint(id), req.Nombre, req.BaseURL, req.ApiToken, req.Activo)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true, "id": cfg.ID})
|
||||
}
|
||||
|
||||
func CoolifyDeleteConfig(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.DeleteCoolifyConfig(uint(id)); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
func CoolifyTestConfig(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"})
|
||||
}
|
||||
cfg, err := models.GetCoolifyConfigByID(uint(id))
|
||||
if err != nil {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "config no encontrada"})
|
||||
}
|
||||
base := strings.TrimRight(cfg.BaseURL, "/")
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(base + "/api/health")
|
||||
if err != nil {
|
||||
return c.Status(502).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||
var result json.RawMessage
|
||||
if jsonErr := json.Unmarshal(body, &result); jsonErr != nil {
|
||||
result = json.RawMessage(fmt.Sprintf(`{"raw":%q}`, string(body)))
|
||||
}
|
||||
c.Status(resp.StatusCode)
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user