🚀 Auto-deploy: BotVPS atualizado em 02/05/2026 15:37:40

This commit is contained in:
2026-05-02 15:37:40 +00:00
parent 1c1fac3735
commit 912763b3f1
613 changed files with 169969 additions and 60 deletions

View File

@@ -31,19 +31,44 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
import uuid
import time
import asyncio
from session_manager import get_history, add_message, clear_history as sm_clear_history, set_orchestrator_pending, get_orchestrator_pending, clear_orchestrator_pending
# Dicionário global para manter o histórico (Em um sistema de produção, usar Redis ou DB)
chat_histories = {}
# ============================================================
# CIRCUIT BREAKER — API degradation state
# ============================================================
_api_failure_count = 0
_api_degraded_until = 0.0 # timestamp when degraded period ends
DEGRADED_DURATION = 60.0 # seconds
FAILURE_THRESHOLD = 3
# ============================================================
# RATE LIMITING — per chat_id
# ============================================================
_processing_chats: dict[int, float] = {}
RATE_LIMIT_SECONDS = 30
# ============================================================
# HANDLERS
# ============================================================
async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Limpa o histórico do usuário."""
chat_id = update.effective_chat.id
if chat_id in chat_histories:
chat_histories[chat_id] = []
sm_clear_history(chat_id)
await update.message.reply_text("🧹 Histórico de conversa limpo! Como posso ajudar agora?")
async def call_antigravity_api(endpoint: str, payload: dict) -> str:
"""Faz a chamada para a API interna do BotVPS com retry automático."""
"""Faz a chamada para a API interna do BotVPS com circuit breaker."""
global _api_failure_count, _api_degraded_until
# Circuit breaker: check if API is degraded
now = time.time()
if now < _api_degraded_until:
remaining = int(_api_degraded_until - now)
return f"❌ *API em Modo Degradado:* Muitas falhas recentes. Tente novamente em {remaining}s."
max_retries = 3
for attempt in range(max_retries):
try:
@@ -52,12 +77,21 @@ async def call_antigravity_api(endpoint: str, payload: dict) -> str:
response = await client.post(f"{API_BASE_URL}{endpoint}", json=payload)
response.raise_for_status()
data = response.json()
# Success: reset circuit breaker
_api_failure_count = 0
return data.get("reply") or data.get("message") or str(data)
except (httpx.ConnectError, httpx.HTTPStatusError) as e:
logger.error(f"Erro de conexão na API (Tentativa {attempt+1}): {e}")
if attempt < max_retries - 1:
await asyncio.sleep(2) # Espera 2s antes de tentar novamente
await asyncio.sleep(2)
else:
# Final failure: trip circuit breaker
_api_failure_count += 1
if _api_failure_count >= FAILURE_THRESHOLD:
_api_degraded_until = time.time() + DEGRADED_DURATION
logger.warning(f"CIRCUIT BREAKER ATIVADO — API degradada por {DEGRADED_DURATION}s")
return "❌ *Erro de Conexão:* A API do BotVPS parece estar offline ou reiniciando. Tente novamente em instantes."
except Exception as e:
logger.error(f"Erro inesperado na chamada API: {str(e)}")
@@ -80,41 +114,84 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
text = update.message.text
logger.info(f"Mensagem recebida: {text[:50]}...")
# Rate limiting: block if a message from this chat is already processing
now = time.time()
if chat_id in _processing_chats:
elapsed = now - _processing_chats[chat_id]
if elapsed < RATE_LIMIT_SECONDS:
remaining = int(RATE_LIMIT_SECONDS - elapsed)
await update.message.reply_text(f"⏳ Aguarde {remaining}s antes de enviar outra mensagem.")
return
_processing_chats[chat_id] = now
# Lógica de reset por texto
cmd_limpar = text.lower().strip()
if cmd_limpar in ["reset", "limpar histórico", "limpar"]:
chat_histories[chat_id] = []
sm_clear_history(chat_id)
await update.message.reply_text("🧹 Memória limpa. O que deseja fazer?")
return
# Inicializa histórico
if chat_id not in chat_histories:
chat_histories[chat_id] = []
# Carrega histórico persistente
chat_history = get_history(chat_id)
# Normaliza texto (remove barra inicial se existir)
text_normalized = text.strip()
starts_with_slash = text_normalized.startswith('/')
text_clean = text_normalized.lstrip('/')
is_hermes = text_clean.lower().startswith('hermes')
is_cmd = text_clean.lower().startswith(('bash', 'vps', 'cmd'))
text_lower = text.lower().strip()
# Verifica se há orchestrator pendente para confirmação
pending = get_orchestrator_pending(chat_id)
is_confirmation = text_lower in ('sim', 's', 'confirmar', 'confirma', 'yes', 'y', 'confirme', 'ok', 'conf', 'aceito', 'sim!', 'sim,', 'yes!')
# Processamento
if text.startswith(('/bash', '/vps', '/cmd')):
task = text.replace('/bash', '').replace('/vps', '').replace('/cmd', '').strip()
if starts_with_slash and is_cmd:
task = text_clean[len(text_clean.split()[0]):].strip()
if not task:
await update.message.reply_text("❓ Envie o comando após o prefixo.")
return
await update.message.reply_text("⚙️ *Processando tarefa...*", parse_mode='Markdown')
reply = await call_antigravity_api("/api/orchestrate", {"task": task})
elif text.startswith('/hermes'):
task = text.replace('/hermes', '').strip()
reply = await call_antigravity_api("/api/orchestrate", {"task": task, "chat_id": chat_id})
elif pending and is_confirmation:
# Há um plano pendente — continua com confirmação
clear_orchestrator_pending(chat_id)
await update.message.reply_text("✅ *Confirmado! Executando o plano...*", parse_mode='Markdown')
reply = await call_antigravity_api("/api/orchestrate", {
"task": pending.get("task", ""),
"plan": pending.get("plan"),
"chat_id": chat_id,
"confirmed": True
})
elif pending:
# Há um plano pendente mas usuário não confirmou — lembra dele
clear_orchestrator_pending(chat_id)
await update.message.reply_text("🛑 Execução cancelada. Enviando nova requisição...")
payload = {"text": text, "history": chat_history[-10:]}
reply = await call_antigravity_api("/api/chat", payload)
add_message(chat_id, text, reply)
elif is_hermes:
# Extrai a tarefa (remove "hermes" do início, com ou sem barra)
task = text_clean[6:].strip() if len(text_clean) > 6 else ""
if not task:
await update.message.reply_text("Envie a tarefa para o Hermes após o comando /hermes.")
await update.message.reply_text("Digite sua tarefa após 'hermes'. Ex: `hermes Instale o nginx`")
return
await update.message.reply_text("🤖 *Hermes assumindo o controle. Isso pode demorar alguns minutos...*", parse_mode='Markdown')
# Bypass history and call hermes endpoint directly
reply = await call_antigravity_api("/api/hermes", {"task": task})
# Passa contexto completo: user_id, chat_id e histórico
reply = await call_antigravity_api("/api/hermes", {
"task": task,
"user_id": user_id,
"chat_id": chat_id,
"history": chat_history[-10:]
})
else:
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
payload = {"text": text, "history": chat_histories[chat_id][-10:]}
payload = {"text": text, "history": chat_history[-10:]}
reply = await call_antigravity_api("/api/chat", payload)
# Atualiza histórico se for chat natural
chat_histories[chat_id].append({"user": text, "bot": reply})
if len(chat_histories[chat_id]) > 15: chat_histories[chat_id].pop(0)
# Salva no histórico persistente
add_message(chat_id, text, reply)
# Envia resposta
if len(reply) > 4000: reply = reply[:3900] + "... [Truncado]"
@@ -173,8 +250,7 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
else:
logger.error(f"Arquivo de áudio não encontrado: {audio_path}")
if chat_id not in chat_histories: chat_histories[chat_id] = []
chat_histories[chat_id].append({"user": user_text, "bot": bot_reply})
add_message(chat_id, user_text, bot_reply)
except Exception as e:
logger.error(f"FALHA NO HANDLE_VOICE: {e}")