diff --git a/README.md b/README.md index 1f21e85..1bfbbec 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,21 @@ Já configuradas via `.env` (gerado automaticamente pelo Lovable Cloud): - `ADMIN_TELEGRAM_CHAT_ID` — chat ID do admin que recebe as notificações (ex: `179720882`). - (opcional, Fase 5) credenciais SSH para Hermes +## Keep-alive dos agentes (substitui UptimeRobot) + +A Edge Function `keep-alive-agents` mantém todos os containers Railway ativos automaticamente — **não há nada para configurar por agente**. A cada 4 minutos: + +1. Carrega todos os `agent_instances` com `status = 'active'` e `telegram_bot_token_vault_id` preenchido. +2. Faz `GET https://api.telegram.org/bot{token}/getMe` para cada um (em paralelo, com falhas isoladas). +3. Esse tráfego de saída do container Hermes evita que o Railway hiberne instâncias ociosas. + +Agendado via `pg_cron` (job `keep-alive-agents-every-4min`, schedule `*/4 * * * *`). Consultar/desagendar: + +```sql +SELECT * FROM cron.job WHERE jobname = 'keep-alive-agents-every-4min'; +SELECT cron.unschedule('keep-alive-agents-every-4min'); +``` + --- ## Fase 3 — Telegram Onboarding (✅ entregue) diff --git a/supabase/config.toml b/supabase/config.toml index 36b7989..296ee11 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -50,3 +50,6 @@ verify_jwt = false [functions.railway-webhook] verify_jwt = false + +[functions.keep-alive-agents] +verify_jwt = false diff --git a/supabase/functions/keep-alive-agents/index.ts b/supabase/functions/keep-alive-agents/index.ts new file mode 100644 index 0000000..f306675 --- /dev/null +++ b/supabase/functions/keep-alive-agents/index.ts @@ -0,0 +1,90 @@ +// keep-alive-agents +// Mantém todos os containers Railway dos agentes ativos "acordados" fazendo +// uma request GET /getMe ao Telegram para cada agente. Isso força tráfego de +// saída no container, evitando que o Railway hiberne instâncias ociosas. +// +// Substitui completamente o UptimeRobot — não precisa de configuração externa +// por agente. Roda via pg_cron a cada 4 minutos. +// +// verify_jwt = false: chamado apenas pelo scheduler interno (pg_cron + pg_net). + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; +import { corsHeaders } from "../_shared/cors.ts"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + +interface AgentRow { + id: string; + user_id: string; + telegram_bot_token_vault_id: string; + telegram_bot_username: string | null; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + // 1) Buscar agentes ativos com token configurado + const { data: agents, error: agentsErr } = await supabase + .from("agent_instances") + .select("id, user_id, telegram_bot_token_vault_id, telegram_bot_username") + .eq("status", "active") + .not("telegram_bot_token_vault_id", "is", null); + + if (agentsErr) { + console.error("keep-alive: failed to load agents:", agentsErr.message); + return jsonResponse(500, { error: "failed to load agents", detail: agentsErr.message }); + } + + const list = (agents ?? []) as AgentRow[]; + let success = 0; + let failed = 0; + + // 2) Para cada agente, decrypt token + GET /getMe (em paralelo, mas sem quebrar o loop) + await Promise.all( + list.map(async (agent) => { + try { + const { data: secret, error: secretErr } = await supabase.rpc("vault_decrypt_secret", { + secret_id: agent.telegram_bot_token_vault_id, + }); + + if (secretErr || !secret?.[0]?.decrypted_secret) { + console.warn(`keep-alive: missing token for agent ${agent.id} (${agent.telegram_bot_username ?? "?"})`); + failed++; + return; + } + + const token = secret[0].decrypted_secret as string; + const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, { method: "GET" }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + console.warn(`keep-alive: getMe failed for agent ${agent.id} (${agent.telegram_bot_username ?? "?"}): ${res.status} ${text.slice(0, 200)}`); + failed++; + return; + } + + success++; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.warn(`keep-alive: exception for agent ${agent.id}: ${msg}`); + failed++; + } + }), + ); + + const summary = { total: list.length, success, failed }; + console.log("keep-alive summary:", JSON.stringify(summary)); + return jsonResponse(200, summary); +}); + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} diff --git a/supabase/migrations/20260423203600_055c8241-3f36-40a3-a450-16ff08b851f8.sql b/supabase/migrations/20260423203600_055c8241-3f36-40a3-a450-16ff08b851f8.sql new file mode 100644 index 0000000..fc25234 --- /dev/null +++ b/supabase/migrations/20260423203600_055c8241-3f36-40a3-a450-16ff08b851f8.sql @@ -0,0 +1,2 @@ +CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA extensions; +CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions; \ No newline at end of file