🚀 Auto-deploy: BotVPS atualizado em 30/04/2026 12:26:39

This commit is contained in:
2026-04-30 12:26:39 +00:00
parent bd0fbbbeb3
commit cec191377f
4 changed files with 120 additions and 127 deletions

View File

@@ -43,115 +43,102 @@ async def clear_history(update: Update, context: ContextTypes.DEFAULT_TYPE):
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."""
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
"""Faz a chamada para a API interna do BotVPS com retry automático."""
max_retries = 3
for attempt in range(max_retries):
try:
logger.info(f"Enviando payload para {endpoint}: {payload.get('text', payload.get('task'))}")
response = await client.post(f"{API_BASE_URL}{endpoint}", json=payload)
response.raise_for_status()
data = response.json()
# Tenta extrair a resposta de diferentes formatos possíveis
reply = data.get("reply") or data.get("message") or str(data)
return reply
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
logger.info(f"Enviando payload para {endpoint} (Tentativa {attempt+1})")
response = await client.post(f"{API_BASE_URL}{endpoint}", json=payload)
response.raise_for_status()
data = response.json()
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
else:
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 ao chamar API Antigravity: {str(e)}")
return f"❌ *Erro na API:* {str(e)}"
logger.error(f"Erro inesperado na chamada API: {str(e)}")
return f"❌ *Erro Interno:* {str(e)}"
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Manipulador central de mensagens."""
if not update.message or not update.message.text:
return
chat_id = update.effective_chat.id
user_id = update.effective_user.id
# Filtro de Segurança
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID:
logger.warning(f"Acesso negado para o ID: {user_id}. Esperado: {ALLOWED_USER_ID}")
return
text = update.message.text
logger.info(f"Mensagem recebida de {user_id}: {text}")
# Lógica de reset por texto
cmd_limpar = text.lower().strip()
if cmd_limpar in ["reset", "limpar histórico", "limpar"]:
chat_histories[chat_id] = []
await update.message.reply_text("🧹 Memória limpa. O que deseja fazer?")
return
# Inicializa histórico se não existir
if chat_id not in chat_histories:
chat_histories[chat_id] = []
# Diferenciação entre comandos de sistema e conhecimento geral
if text.startswith(('/bash', '/vps', '/cmd')):
# Comandos de sistema geralmente não precisam de histórico de chat natural
task = text.replace('/bash', '').replace('/vps', '').replace('/cmd', '').strip()
if not task:
await update.message.reply_text("❓ Por favor, envie o comando após o prefixo.")
"""Manipulador central de mensagens com blindagem contra crashes."""
try:
if not update.message or not update.message.text:
return
await update.message.reply_text("⚙️ *Processando tarefa no Claw System...*", parse_mode='Markdown')
reply = await call_antigravity_api("/api/orchestrate", {"task": task})
else:
# Chat Natural com contexto
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
chat_id = update.effective_chat.id
user_id = update.effective_user.id
# Prepara payload com histórico
payload = {
"text": text,
"history": chat_histories[chat_id][-10:] # Envia os últimos 10 turnos
}
reply = await call_antigravity_api("/api/chat", payload)
# Atualiza histórico Local
chat_histories[chat_id].append({"user": text, "bot": reply})
# Mantém apenas os últimos 15 para não crescer infinito no middleware
if len(chat_histories[chat_id]) > 15:
chat_histories[chat_id].pop(0)
# Filtro de Segurança
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID:
logger.warning(f"Acesso negado para o ID: {user_id}")
return
text = update.message.text
logger.info(f"Mensagem recebida: {text[:50]}...")
# Lógica de reset por texto
cmd_limpar = text.lower().strip()
if cmd_limpar in ["reset", "limpar histórico", "limpar"]:
chat_histories[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] = []
# Processamento
if text.startswith(('/bash', '/vps', '/cmd')):
task = text.replace('/bash', '').replace('/vps', '').replace('/cmd', '').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})
else:
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
payload = {"text": text, "history": chat_histories[chat_id][-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)
# Envia resposta
if len(reply) > 4000: reply = reply[:3900] + "... [Truncado]"
try:
await update.message.reply_text(reply, parse_mode='Markdown')
except Exception:
await update.message.reply_text(reply)
# Envia a resposta de volta para o usuário
if len(reply) > 4000:
reply = reply[:3900] + "... [Texto truncado]"
try:
await update.message.reply_text(reply, parse_mode='Markdown')
except Exception as e:
logger.error(f"Erro ao enviar Markdown: {e}. Tentando texto puro.")
await update.message.reply_text(reply)
logger.error(f"FALHA NO HANDLE_MESSAGE: {e}")
try:
await update.message.reply_text("⚠️ Ocorreu um erro ao processar sua mensagem. O sistema foi notificado.")
except: pass
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Manipula mensagens de voz do Telegram."""
if not update.message or not update.message.voice:
return
"""Manipula mensagens de voz com blindagem contra crashes."""
try:
if not update.message or not update.message.voice:
return
chat_id = update.effective_chat.id
user_id = update.effective_user.id
# Filtro de Segurança
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID:
return
chat_id = update.effective_chat.id
user_id = update.effective_user.id
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID: return
await context.bot.send_chat_action(chat_id=chat_id, action="record_voice")
# 1. Download do áudio do Telegram
voice_file = await update.message.voice.get_file()
temp_path = f"/tmp/tg_voice_{uuid.uuid4().hex}.ogg"
await voice_file.download_to_drive(temp_path)
logger.info(f"Voz recebida de {user_id}. Enviando para API de Áudio...")
# 2. Envia para a API interna de áudio
# Como o bridge e API estão na mesma máquina, compartilhamos o /tmp se necessário
# Mas vamos usar multipart para ser fiel à API
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
try:
await context.bot.send_chat_action(chat_id=chat_id, action="record_voice")
voice_file = await update.message.voice.get_file()
temp_path = f"/tmp/tg_voice_{uuid.uuid4().hex}.ogg"
await voice_file.download_to_drive(temp_path)
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
with open(temp_path, "rb") as f:
# O parâmetro history pode ser adicionado futuramente similar ao chat
files = {"audio": (os.path.basename(temp_path), f, "audio/ogg")}
response = await client.post(f"{API_BASE_URL}/api/chat-audio", files=files)
response.raise_for_status()
@@ -159,20 +146,14 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_text = data.get("text", "[Voz não transcrita]")
bot_reply = data.get("reply", "Erro no processamento.")
audio_url = data.get("audio_url") # Ex: /api/audio/file.mp3
audio_url = data.get("audio_url")
# Envia transcrição do usuário
await update.message.reply_text(f"🎤 *Sua mensagem:* {user_text}")
# Envia resposta em texto
try:
await update.message.reply_text(bot_reply, parse_mode='Markdown')
except Exception as e:
logger.error(f"Erro Markdown na voz: {e}")
except:
await update.message.reply_text(bot_reply)
# 3. Envia resposta em áudio (TTS)
if audio_url:
filename = audio_url.split("/")[-1]
audio_path = os.path.join("/tmp", filename)
@@ -180,19 +161,18 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
with open(audio_path, "rb") as audio_file:
await context.bot.send_voice(chat_id=chat_id, voice=audio_file)
# Atualiza histórico local
if chat_id not in chat_histories: chat_histories[chat_id] = []
chat_histories[chat_id].append({"user": user_text, "bot": bot_reply})
except Exception as e:
logger.error(f"Erro ao processar áudio: {str(e)}")
await update.message.reply_text(f"❌ *Erro no áudio:* {str(e)}")
finally:
if os.path.exists(temp_path): os.remove(temp_path)
except Exception as e:
logger.error(f"FALHA NO HANDLE_VOICE: {e}")
await update.message.reply_text("⚠️ Erro ao processar áudio.")
finally:
if 'temp_path' in locals() and os.path.exists(temp_path): os.remove(temp_path)
if __name__ == '__main__':
if not TOKEN:
logger.error("ERRO: TELEGRAM_BOT_TOKEN não encontrado no .env!")
logger.error("ERRO: TOKEN ausente!")
exit(1)
# Inicializa o Bot (python-telegram-bot v20+)
@@ -204,5 +184,5 @@ if __name__ == '__main__':
application.add_handler(MessageHandler(filters.TEXT | filters.COMMAND, handle_message))
application.add_handler(MessageHandler(filters.VOICE, handle_voice))
logger.info("Bot Ponte Antigravity (Middleware - Texto & Voz) iniciado...")
application.run_polling()
logger.info("Ponte Iniciada. Modo: Resiliente.")
application.run_polling(drop_pending_updates=True)