diff --git a/ai_agent.py b/ai_agent.py index f4038b8..a5bb9d6 100644 --- a/ai_agent.py +++ b/ai_agent.py @@ -59,12 +59,11 @@ DIRETRIZES: {tools_desc} ### REGRAS DE OURO: -- FOCO NO PRESENTE: O histórico é para CONTEXTO. Foque SEMPRE no pedido ATUAL (última mensagem). Se o usuário disser "bom dia" ou mudar de assunto, não repita tarefas técnicas anteriores. -- COOLIFY: NUNCA tente adivinhar caminhos de logs ou usar comandos `psql` genéricos. Use SEMPRE a ferramenta `coolify_status`. Ela é a fonte da verdade para deploies. -- NUNCA tente instalar pacotes (`apt`, `npm install`, etc) ou usar tokens fictícios como ``. -- Se o usuário perguntar sobre o "app mais recente", use `coolify_status` e analise a coluna `application` e `created_at`. +- CONSENTIMENTO PARA SCRIPTS: Se você não conseguir realizar uma tarefa técnica e precisar passar instruções, scripts ou tutoriais, você DEVE primeiro relatar o problema e PERGUNTAR se o usuário deseja receber o passo a passo técnico. Só envie se ele consentir. +- FOCO NO PRESENTE: O histórico é para CONTEXTO. Foque SEMPRE no pedido ATUAL (última mensagem). +- COOLIFY: NUNCA tente adivinhar caminhos de logs. Use SEMPRE a ferramenta `coolify_status`. +- NUNCA INVENTE DADOS. Se não conseguir ler algo, reporte o erro de forma honesta. - Seja direto e técnico. Menos conversa, mais execução. -- NUNCA INVENTE DADOS. Se não conseguir ler algo, reporte o erro. ### FORMATO DE RESPOSTA FINAL (OBRIGATÓRIO): - Use SEMPRE o prefixo `RESUMO:` para sua conclusão final amigável. diff --git a/bridge_telegram.py b/bridge_telegram.py index 264bf1c..780b5d0 100644 --- a/bridge_telegram.py +++ b/bridge_telegram.py @@ -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) diff --git a/ecosystem.config.js b/ecosystem.config.js index fa93541..1086093 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -6,7 +6,7 @@ module.exports = { interpreter: "python3", cwd: "/root/Apps/BotVPS", env: { - PORT: "8001", + PORT: "8089", NODE_ENV: "production", }, restart_delay: 3000 diff --git a/watchdog.py b/watchdog.py index c27cb8b..d73277a 100644 --- a/watchdog.py +++ b/watchdog.py @@ -21,6 +21,7 @@ class Watchdog: self.cpu_streak = 0 self.last_alert_time = 0 self.alert_cooldown = 300 # 5 minutos entre alertas do mesmo tipo + self.previous_status = {} # Guarda o último estado conhecido de cada processo async def send_telegram_message(self, message: str): if not TOKEN or not CHAT_ID: @@ -40,22 +41,35 @@ class Watchdog: except Exception as e: print(f"[WATCHDOG] Erro ao enviar Telegram: {e}") - def get_pm2_status(self): + def monitor_pm2(self): try: result = subprocess.run(["pm2", "jlist"], capture_output=True, text=True) if result.returncode == 0: data = json.loads(result.stdout) - issues = [] + alerts = [] for proc in data: - if proc['pm2_env']['status'] != 'online': - issues.append(f"🔴 App '{proc['name']}' está {proc['pm2_env']['status']}!") - return issues + name = proc['name'] + current_status = proc['pm2_env']['status'] + prev_status = self.previous_status.get(name) + + # Detecta Queda (Transição de online para qualquer outra coisa) + if current_status != 'online' and prev_status == 'online': + alerts.append(f"🔴 App '{name}' ACABOU DE CAIR! (Status: {current_status})") + + # Detecta Recuperação (Transição de offline para online) + elif current_status == 'online' and prev_status is not None and prev_status != 'online': + alerts.append(f"🟢 App '{name}' ESTÁ ONLINE NOVAMENTE! 🚀") + + # Atualiza memória de estado + self.previous_status[name] = current_status + + return alerts except Exception as e: print(f"[WATCHDOG] Erro PM2: {e}") return [] async def run(self): - print("[WATCHDOG] Iniciado. Vigilância ativa...") + print("[WATCHDOG] Iniciado. Vigilância ativa (Ciclagem Online/Offline)...") while True: try: @@ -73,10 +87,10 @@ class Watchdog: ) self.last_alert_time = time.time() - # 2. Monitoramento de PM2 - pm2_issues = self.get_pm2_status() - if pm2_issues: - await self.send_telegram_message("\n".join(pm2_issues)) + # 2. Monitoramento de PM2 (Alertas de mudança de estado) + pm2_alerts = self.monitor_pm2() + if pm2_alerts: + await self.send_telegram_message("\n".join(pm2_alerts)) # 3. Monitoramento de Espaço em Disco disk = psutil.disk_usage('/')