🚀 Auto-deploy: BotVPS atualizado em 30/04/2026 12:26:39
This commit is contained in:
@@ -59,12 +59,11 @@ DIRETRIZES:
|
|||||||
{tools_desc}
|
{tools_desc}
|
||||||
|
|
||||||
### REGRAS DE OURO:
|
### 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.
|
- 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.
|
||||||
- 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.
|
- FOCO NO PRESENTE: O histórico é para CONTEXTO. Foque SEMPRE no pedido ATUAL (última mensagem).
|
||||||
- NUNCA tente instalar pacotes (`apt`, `npm install`, etc) ou usar tokens fictícios como `<YOUR_TOKEN>`.
|
- COOLIFY: NUNCA tente adivinhar caminhos de logs. Use SEMPRE a ferramenta `coolify_status`.
|
||||||
- Se o usuário perguntar sobre o "app mais recente", use `coolify_status` e analise a coluna `application` e `created_at`.
|
- 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.
|
- 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):
|
### FORMATO DE RESPOSTA FINAL (OBRIGATÓRIO):
|
||||||
- Use SEMPRE o prefixo `RESUMO:` para sua conclusão final amigável.
|
- Use SEMPRE o prefixo `RESUMO:` para sua conclusão final amigável.
|
||||||
|
|||||||
@@ -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?")
|
await update.message.reply_text("🧹 Histórico de conversa limpo! Como posso ajudar agora?")
|
||||||
|
|
||||||
async def call_antigravity_api(endpoint: str, payload: dict) -> str:
|
async def call_antigravity_api(endpoint: str, payload: dict) -> str:
|
||||||
"""Faz a chamada para a API interna do BotVPS."""
|
"""Faz a chamada para a API interna do BotVPS com retry automático."""
|
||||||
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
logger.info(f"Enviando payload para {endpoint}: {payload.get('text', payload.get('task'))}")
|
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
|
||||||
response = await client.post(f"{API_BASE_URL}{endpoint}", json=payload)
|
logger.info(f"Enviando payload para {endpoint} (Tentativa {attempt+1})")
|
||||||
response.raise_for_status()
|
response = await client.post(f"{API_BASE_URL}{endpoint}", json=payload)
|
||||||
data = response.json()
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
# Tenta extrair a resposta de diferentes formatos possíveis
|
return data.get("reply") or data.get("message") or str(data)
|
||||||
reply = data.get("reply") or data.get("message") or str(data)
|
except (httpx.ConnectError, httpx.HTTPStatusError) as e:
|
||||||
return reply
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Erro ao chamar API Antigravity: {str(e)}")
|
logger.error(f"Erro inesperado na chamada API: {str(e)}")
|
||||||
return f"❌ *Erro na API:* {str(e)}"
|
return f"❌ *Erro Interno:* {str(e)}"
|
||||||
|
|
||||||
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Manipulador central de mensagens."""
|
"""Manipulador central de mensagens com blindagem contra crashes."""
|
||||||
if not update.message or not update.message.text:
|
try:
|
||||||
return
|
if not update.message or not update.message.text:
|
||||||
|
|
||||||
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.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
await update.message.reply_text("⚙️ *Processando tarefa no Claw System...*", parse_mode='Markdown')
|
chat_id = update.effective_chat.id
|
||||||
reply = await call_antigravity_api("/api/orchestrate", {"task": task})
|
user_id = update.effective_user.id
|
||||||
else:
|
|
||||||
# Chat Natural com contexto
|
|
||||||
await context.bot.send_chat_action(chat_id=chat_id, action="typing")
|
|
||||||
|
|
||||||
# Prepara payload com histórico
|
# Filtro de Segurança
|
||||||
payload = {
|
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID:
|
||||||
"text": text,
|
logger.warning(f"Acesso negado para o ID: {user_id}")
|
||||||
"history": chat_histories[chat_id][-10:] # Envia os últimos 10 turnos
|
return
|
||||||
}
|
|
||||||
|
|
||||||
reply = await call_antigravity_api("/api/chat", payload)
|
text = update.message.text
|
||||||
|
logger.info(f"Mensagem recebida: {text[:50]}...")
|
||||||
|
|
||||||
# Atualiza histórico Local
|
# Lógica de reset por texto
|
||||||
chat_histories[chat_id].append({"user": text, "bot": reply})
|
cmd_limpar = text.lower().strip()
|
||||||
# Mantém apenas os últimos 15 para não crescer infinito no middleware
|
if cmd_limpar in ["reset", "limpar histórico", "limpar"]:
|
||||||
if len(chat_histories[chat_id]) > 15:
|
chat_histories[chat_id] = []
|
||||||
chat_histories[chat_id].pop(0)
|
await update.message.reply_text("🧹 Memória limpa. O que deseja fazer?")
|
||||||
|
return
|
||||||
|
|
||||||
# Envia a resposta de volta para o usuário
|
# Inicializa histórico
|
||||||
if len(reply) > 4000:
|
if chat_id not in chat_histories:
|
||||||
reply = reply[:3900] + "... [Texto truncado]"
|
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)
|
||||||
|
|
||||||
try:
|
|
||||||
await update.message.reply_text(reply, parse_mode='Markdown')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erro ao enviar Markdown: {e}. Tentando texto puro.")
|
logger.error(f"FALHA NO HANDLE_MESSAGE: {e}")
|
||||||
await update.message.reply_text(reply)
|
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):
|
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Manipula mensagens de voz do Telegram."""
|
"""Manipula mensagens de voz com blindagem contra crashes."""
|
||||||
if not update.message or not update.message.voice:
|
try:
|
||||||
return
|
if not update.message or not update.message.voice:
|
||||||
|
return
|
||||||
|
|
||||||
chat_id = update.effective_chat.id
|
chat_id = update.effective_chat.id
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
|
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID: return
|
||||||
|
|
||||||
# Filtro de Segurança
|
await context.bot.send_chat_action(chat_id=chat_id, action="record_voice")
|
||||||
if ALLOWED_USER_ID and user_id != ALLOWED_USER_ID:
|
|
||||||
return
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
# 1. Download do áudio do Telegram
|
async with httpx.AsyncClient(timeout=GLOBAL_TIMEOUT) as client:
|
||||||
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:
|
|
||||||
with open(temp_path, "rb") as f:
|
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")}
|
files = {"audio": (os.path.basename(temp_path), f, "audio/ogg")}
|
||||||
response = await client.post(f"{API_BASE_URL}/api/chat-audio", files=files)
|
response = await client.post(f"{API_BASE_URL}/api/chat-audio", files=files)
|
||||||
response.raise_for_status()
|
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]")
|
user_text = data.get("text", "[Voz não transcrita]")
|
||||||
bot_reply = data.get("reply", "Erro no processamento.")
|
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")
|
audio_url = data.get("audio_url")
|
||||||
|
|
||||||
# Envia transcrição do usuário
|
|
||||||
await update.message.reply_text(f"🎤 *Sua mensagem:* {user_text}")
|
await update.message.reply_text(f"🎤 *Sua mensagem:* {user_text}")
|
||||||
|
|
||||||
# Envia resposta em texto
|
|
||||||
try:
|
try:
|
||||||
await update.message.reply_text(bot_reply, parse_mode='Markdown')
|
await update.message.reply_text(bot_reply, parse_mode='Markdown')
|
||||||
except Exception as e:
|
except:
|
||||||
logger.error(f"Erro Markdown na voz: {e}")
|
|
||||||
await update.message.reply_text(bot_reply)
|
await update.message.reply_text(bot_reply)
|
||||||
|
|
||||||
# 3. Envia resposta em áudio (TTS)
|
|
||||||
if audio_url:
|
if audio_url:
|
||||||
filename = audio_url.split("/")[-1]
|
filename = audio_url.split("/")[-1]
|
||||||
audio_path = os.path.join("/tmp", filename)
|
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:
|
with open(audio_path, "rb") as audio_file:
|
||||||
await context.bot.send_voice(chat_id=chat_id, voice=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] = []
|
if chat_id not in chat_histories: chat_histories[chat_id] = []
|
||||||
chat_histories[chat_id].append({"user": user_text, "bot": bot_reply})
|
chat_histories[chat_id].append({"user": user_text, "bot": bot_reply})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erro ao processar áudio: {str(e)}")
|
logger.error(f"FALHA NO HANDLE_VOICE: {e}")
|
||||||
await update.message.reply_text(f"❌ *Erro no áudio:* {str(e)}")
|
await update.message.reply_text("⚠️ Erro ao processar áudio.")
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(temp_path): os.remove(temp_path)
|
if 'temp_path' in locals() and os.path.exists(temp_path): os.remove(temp_path)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if not TOKEN:
|
if not TOKEN:
|
||||||
logger.error("ERRO: TELEGRAM_BOT_TOKEN não encontrado no .env!")
|
logger.error("ERRO: TOKEN ausente!")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Inicializa o Bot (python-telegram-bot v20+)
|
# 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.TEXT | filters.COMMAND, handle_message))
|
||||||
application.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
application.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
||||||
|
|
||||||
logger.info("Bot Ponte Antigravity (Middleware - Texto & Voz) iniciado...")
|
logger.info("Ponte Iniciada. Modo: Resiliente.")
|
||||||
application.run_polling()
|
application.run_polling(drop_pending_updates=True)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = {
|
|||||||
interpreter: "python3",
|
interpreter: "python3",
|
||||||
cwd: "/root/Apps/BotVPS",
|
cwd: "/root/Apps/BotVPS",
|
||||||
env: {
|
env: {
|
||||||
PORT: "8001",
|
PORT: "8089",
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
},
|
},
|
||||||
restart_delay: 3000
|
restart_delay: 3000
|
||||||
|
|||||||
34
watchdog.py
34
watchdog.py
@@ -21,6 +21,7 @@ class Watchdog:
|
|||||||
self.cpu_streak = 0
|
self.cpu_streak = 0
|
||||||
self.last_alert_time = 0
|
self.last_alert_time = 0
|
||||||
self.alert_cooldown = 300 # 5 minutos entre alertas do mesmo tipo
|
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):
|
async def send_telegram_message(self, message: str):
|
||||||
if not TOKEN or not CHAT_ID:
|
if not TOKEN or not CHAT_ID:
|
||||||
@@ -40,22 +41,35 @@ class Watchdog:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WATCHDOG] Erro ao enviar Telegram: {e}")
|
print(f"[WATCHDOG] Erro ao enviar Telegram: {e}")
|
||||||
|
|
||||||
def get_pm2_status(self):
|
def monitor_pm2(self):
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(["pm2", "jlist"], capture_output=True, text=True)
|
result = subprocess.run(["pm2", "jlist"], capture_output=True, text=True)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
data = json.loads(result.stdout)
|
data = json.loads(result.stdout)
|
||||||
issues = []
|
alerts = []
|
||||||
for proc in data:
|
for proc in data:
|
||||||
if proc['pm2_env']['status'] != 'online':
|
name = proc['name']
|
||||||
issues.append(f"🔴 App '{proc['name']}' está {proc['pm2_env']['status']}!")
|
current_status = proc['pm2_env']['status']
|
||||||
return issues
|
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:
|
except Exception as e:
|
||||||
print(f"[WATCHDOG] Erro PM2: {e}")
|
print(f"[WATCHDOG] Erro PM2: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
print("[WATCHDOG] Iniciado. Vigilância ativa...")
|
print("[WATCHDOG] Iniciado. Vigilância ativa (Ciclagem Online/Offline)...")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -73,10 +87,10 @@ class Watchdog:
|
|||||||
)
|
)
|
||||||
self.last_alert_time = time.time()
|
self.last_alert_time = time.time()
|
||||||
|
|
||||||
# 2. Monitoramento de PM2
|
# 2. Monitoramento de PM2 (Alertas de mudança de estado)
|
||||||
pm2_issues = self.get_pm2_status()
|
pm2_alerts = self.monitor_pm2()
|
||||||
if pm2_issues:
|
if pm2_alerts:
|
||||||
await self.send_telegram_message("\n".join(pm2_issues))
|
await self.send_telegram_message("\n".join(pm2_alerts))
|
||||||
|
|
||||||
# 3. Monitoramento de Espaço em Disco
|
# 3. Monitoramento de Espaço em Disco
|
||||||
disk = psutil.disk_usage('/')
|
disk = psutil.disk_usage('/')
|
||||||
|
|||||||
Reference in New Issue
Block a user