/** * OpenClaw Gateway Server para Hugging Face Spaces * * Arquitetura: * - Servidor Express otimizado para produção * - Proxy transparente para API do OpenClaw.ai * - Suporte a múltiplas APIs de IA gratuitas (OpenRouter, Gemini, Groq) * - Rate limiting, compression, segurança via helmet * - Healthcheck endpoint para monitoramento do HF * - Graceful shutdown com cleanup de recursos * * @author Senior DevOps Audit * @version 1.0.0 */ 'use strict'; // Carrega variáveis de ambiente antes de qualquer import require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); const compression = require('compression'); const { createProxyMiddleware } = require('http-proxy-middleware'); const rateLimit = require('express-rate-limit'); // ============================================================================= // CONFIGURAÇÃO CENTRALIZADA - Validada na inicialização // ============================================================================= const CONFIG = { port: parseInt(process.env.PORT || '7860', 10), host: process.env.HOST || '0.0.0.0', nodeEnv: process.env.NODE_ENV || 'production', openclaw: { apiKey: process.env.OPENCLAW_API_KEY || '', baseUrl: process.env.OPENCLAW_BASE_URL || 'https://api.openclaw.ai', }, ai: { openrouterKey: process.env.OPENROUTER_API_KEY || '', geminiKey: process.env.GEMINI_API_KEY || '', groqKey: process.env.GROQ_API_KEY || '', }, }; // Validação de configuração crítica na inicialização // Fail-fast: melhor crashar com mensagem clara do que funcionar incorretamente function validateConfig() { const warnings = []; if (!CONFIG.openclaw.apiKey) { warnings.push('OPENCLAW_API_KEY não configurada. Proxy para API externa desabilitado.'); } const hasAnyAI = CONFIG.ai.openrouterKey || CONFIG.ai.geminiKey || CONFIG.ai.groqKey; if (!hasAnyAI) { warnings.push('Nenhuma chave de API de IA configurada. Configure OPENROUTER_API_KEY, GEMINI_API_KEY ou GROQ_API_KEY.'); } warnings.forEach(w => console.warn(`[CONFIG WARN] ${w}`)); return true; } // ============================================================================= // INICIALIZAÇÃO DO SERVIDOR EXPRESS // ============================================================================= const app = express(); // Helmet: Configura headers de segurança HTTP // Desabilitamos contentSecurityPolicy para não bloquear o iframe do HF app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, })); // Compression: gzip/brotli em todas as respostas // Reduz bandwidth significativamente para payloads de IA (JSON grandes) app.use(compression({ level: 6, // Balanço entre CPU e compressão threshold: 1024, // Comprimir apenas respostas > 1KB })); // Body parser: Limite de 10MB para suportar payloads grandes de IA app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Rate limiting: Protege contra abuse e evita esgotar quotas de APIs gratuitas // 100 requests por 15 minutos por IP é generoso para uso normal const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 100, standardHeaders: true, legacyHeaders: false, message: { error: 'Muitas requisições. Tente novamente em 15 minutos.' }, }); app.use(limiter); // ============================================================================= // ENDPOINT: HEALTHCHECK // Usado pelo HEALTHCHECK do Dockerfile e pelo HF para verificar se o Space // está vivo. Deve responder em < 1 segundo e nunca falhar se o servidor está up. // ============================================================================= app.get('/health', (req, res) => { // Coleta de métricas de memória para debug const memUsage = process.memoryUsage(); res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: Math.floor(process.uptime()), environment: CONFIG.nodeEnv, memory: { rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, }, services: { openclaw: CONFIG.openclaw.apiKey ? 'configured' : 'not_configured', openrouter: CONFIG.ai.openrouterKey ? 'configured' : 'not_configured', gemini: CONFIG.ai.geminiKey ? 'configured' : 'not_configured', groq: CONFIG.ai.groqKey ? 'configured' : 'not_configured', }, }); }); // ============================================================================= // ENDPOINT: INTERFACE WEB (Dashboard) // Página HTML para visualizar o status e configurar integrações // ============================================================================= app.get('/', (req, res) => { const html = ` OpenClaw Gateway - HF Space

🦅 OpenClaw Gateway

Hugging Face Space · Node.js ${process.version} · ${CONFIG.nodeEnv}

Status dos Serviços

OpenClaw API
OpenRouter
Google Gemini
Groq

Métricas em Tempo Real

Carregando...

Configuração Rápida

Configure suas chaves em:
HF Space Settings
→ Variables and Secrets

Endpoints Disponíveis

GET/health · Healthcheck
POST/api/chat · Chat com IA (OpenRouter/Gemini/Groq)
POST/api/openclaw/execute · Executar task OpenClaw
GET/api/models · Listar modelos disponíveis
`; res.status(200).send(html); }); // ============================================================================= // ENDPOINT: CHAT COM IA // Roteamento inteligente: usa a primeira API configurada disponível // Ordem de prioridade: OpenRouter > Gemini > Groq // ============================================================================= app.post('/api/chat', async (req, res) => { // Import dinâmico de node-fetch (ESM no CommonJS) const fetch = (await import('node-fetch')).default; const { message, model, provider } = req.body; if (!message) { return res.status(400).json({ error: 'Campo "message" é obrigatório.' }); } // Seleção de provider com fallback automático let selectedProvider = provider || 'auto'; if (selectedProvider === 'auto') { if (CONFIG.ai.openrouterKey) selectedProvider = 'openrouter'; else if (CONFIG.ai.geminiKey) selectedProvider = 'gemini'; else if (CONFIG.ai.groqKey) selectedProvider = 'groq'; else { return res.status(503).json({ error: 'Nenhuma API de IA configurada. Adicione OPENROUTER_API_KEY, GEMINI_API_KEY ou GROQ_API_KEY nos Secrets do Space.' }); } } try { let response; // --- OpenRouter --- if (selectedProvider === 'openrouter') { const r = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${CONFIG.ai.openrouterKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': `https://huggingface.co/spaces`, 'X-Title': 'OpenClaw HF Gateway', }, body: JSON.stringify({ model: model || 'google/gemma-2-9b-it:free', // Modelo gratuito de alta qualidade messages: [{ role: 'user', content: message }], max_tokens: 4096, }), }); if (!r.ok) { const err = await r.text(); throw new Error(`OpenRouter error ${r.status}: ${err}`); } const data = await r.json(); response = { provider: 'openrouter', model: data.model, content: data.choices[0]?.message?.content || '', usage: data.usage, }; } // --- Google Gemini --- else if (selectedProvider === 'gemini') { const geminiModel = model || 'gemini-1.5-flash'; const r = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${CONFIG.ai.geminiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: message }] }], generationConfig: { maxOutputTokens: 8192 }, }), } ); if (!r.ok) { const err = await r.text(); throw new Error(`Gemini error ${r.status}: ${err}`); } const data = await r.json(); response = { provider: 'gemini', model: geminiModel, content: data.candidates[0]?.content?.parts[0]?.text || '', usage: data.usageMetadata, }; } // --- Groq --- else if (selectedProvider === 'groq') { const r = await fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${CONFIG.ai.groqKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: model || 'llama-3.1-8b-instant', // Ultra-rápido e gratuito messages: [{ role: 'user', content: message }], max_tokens: 8192, }), }); if (!r.ok) { const err = await r.text(); throw new Error(`Groq error ${r.status}: ${err}`); } const data = await r.json(); response = { provider: 'groq', model: data.model, content: data.choices[0]?.message?.content || '', usage: data.usage, }; } else { return res.status(400).json({ error: `Provider desconhecido: ${selectedProvider}` }); } res.json({ success: true, ...response }); } catch (error) { console.error('[API CHAT ERROR]', error.message); res.status(500).json({ error: 'Erro ao chamar API de IA', details: error.message, }); } }); // ============================================================================= // ENDPOINT: EXECUTAR TASK OPENCLAW // Proxy para a API real do OpenClaw.ai com autenticação injetada // ============================================================================= app.post('/api/openclaw/execute', async (req, res) => { if (!CONFIG.openclaw.apiKey) { return res.status(503).json({ error: 'OPENCLAW_API_KEY não configurada. Adicione nos Secrets do Space.' }); } const fetch = (await import('node-fetch')).default; try { const r = await fetch(`${CONFIG.openclaw.baseUrl}/v1/execute`, { method: 'POST', headers: { 'Authorization': `Bearer ${CONFIG.openclaw.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(req.body), }); const data = await r.json(); res.status(r.status).json(data); } catch (error) { console.error('[OPENCLAW EXECUTE ERROR]', error.message); res.status(500).json({ error: 'Erro ao conectar com OpenClaw API', details: error.message }); } }); // ============================================================================= // ENDPOINT: LISTAR MODELOS DISPONÍVEIS // ============================================================================= app.get('/api/models', (req, res) => { res.json({ providers: { openrouter: { configured: !!CONFIG.ai.openrouterKey, freeModels: [ 'google/gemma-2-9b-it:free', 'meta-llama/llama-3.1-8b-instruct:free', 'mistralai/mistral-7b-instruct:free', 'microsoft/phi-3-mini-128k-instruct:free', 'nousresearch/hermes-3-llama-3.1-405b:free', ], }, gemini: { configured: !!CONFIG.ai.geminiKey, freeModels: [ 'gemini-1.5-flash', 'gemini-1.5-flash-8b', 'gemini-1.0-pro', ], }, groq: { configured: !!CONFIG.ai.groqKey, freeModels: [ 'llama-3.1-8b-instant', 'llama-3.1-70b-versatile', 'mixtral-8x7b-32768', 'gemma2-9b-it', ], }, }, }); }); // ============================================================================= // PROXY PARA OPENCLAW API (todas as rotas /api/openclaw/*) // Transparente: injeta autenticação e repassa para o servidor real // ============================================================================= if (CONFIG.openclaw.apiKey && CONFIG.openclaw.baseUrl) { app.use('/api/openclaw', createProxyMiddleware({ target: CONFIG.openclaw.baseUrl, changeOrigin: true, pathRewrite: { '^/api/openclaw': '' }, on: { proxyReq: (proxyReq) => { proxyReq.setHeader('Authorization', `Bearer ${CONFIG.openclaw.apiKey}`); }, error: (err, req, res) => { console.error('[PROXY ERROR]', err.message); res.status(502).json({ error: 'Erro no proxy para OpenClaw API', details: err.message }); }, }, })); } // ============================================================================= // HANDLER DE ERROS GLOBAL // Captura qualquer erro não tratado e retorna resposta JSON padronizada // CRÍTICO: Sem isso, erros causam crash do processo // ============================================================================= app.use((err, req, res, _next) => { console.error('[UNHANDLED ERROR]', err.stack); res.status(500).json({ error: 'Erro interno do servidor', message: CONFIG.nodeEnv === 'production' ? 'Verifique os logs.' : err.message, }); }); // ============================================================================= // HANDLER DE ROTAS NÃO ENCONTRADAS // ============================================================================= app.use((req, res) => { res.status(404).json({ error: `Rota não encontrada: ${req.method} ${req.path}` }); }); // ============================================================================= // INICIALIZAÇÃO COM GRACEFUL SHUTDOWN // Graceful shutdown: fecha conexões existentes antes de matar o processo. // Evita requests interrompidas no meio quando o HF reinicia o Space. // ============================================================================= validateConfig(); const server = app.listen(CONFIG.port, CONFIG.host, () => { console.log(`✅ OpenClaw Gateway iniciado`); console.log(`🌐 Endereço: http://${CONFIG.host}:${CONFIG.port}`); console.log(`🔧 Ambiente: ${CONFIG.nodeEnv}`); console.log(`📊 Node.js: ${process.version}`); console.log(`💾 Memória máx: ${process.env.NODE_OPTIONS || 'padrão'}`); }); // Timeout para requests longas (30s para APIs de IA que podem ser lentas) server.timeout = 30000; server.keepAliveTimeout = 65000; // Maior que timeout de load balancer do HF (60s) server.headersTimeout = 66000; // Graceful shutdown handler function gracefulShutdown(signal) { console.log(`\n[SHUTDOWN] Recebido ${signal}. Encerrando servidor...`); server.close((err) => { if (err) { console.error('[SHUTDOWN ERROR]', err); process.exit(1); } // Garbage collection manual se disponível (via --expose-gc) if (global.gc) { global.gc(); } console.log('[SHUTDOWN] Servidor encerrado com sucesso.'); process.exit(0); }); // Force kill após 10s se o servidor não fechar sozinho setTimeout(() => { console.error('[SHUTDOWN TIMEOUT] Forçando encerramento após 10s.'); process.exit(1); }, 10000); } process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Handler para exceções não capturadas // Loga o erro mas NÃO derruba o processo (resilência em produção) process.on('uncaughtException', (err) => { console.error('[UNCAUGHT EXCEPTION]', err.stack); // Em produção: log e continua. Em desenvolvimento: considera derrubar }); process.on('unhandledRejection', (reason) => { console.error('[UNHANDLED REJECTION]', reason); });