| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 'use strict'; |
|
|
| |
| 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'); |
|
|
| |
| |
| |
| 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 || '', |
| }, |
| }; |
|
|
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| const app = express(); |
|
|
| |
| |
| app.use(helmet({ |
| contentSecurityPolicy: false, |
| crossOriginEmbedderPolicy: false, |
| })); |
|
|
| |
| |
| app.use(compression({ |
| level: 6, |
| threshold: 1024, |
| })); |
|
|
| |
| app.use(express.json({ limit: '10mb' })); |
| app.use(express.urlencoded({ extended: true, limit: '10mb' })); |
|
|
| |
| |
| const limiter = rateLimit({ |
| windowMs: 15 * 60 * 1000, |
| max: 100, |
| standardHeaders: true, |
| legacyHeaders: false, |
| message: { error: 'Muitas requisições. Tente novamente em 15 minutos.' }, |
| }); |
| app.use(limiter); |
|
|
| |
| |
| |
| |
| |
| app.get('/health', (req, res) => { |
| |
| 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', |
| }, |
| }); |
| }); |
|
|
| |
| |
| |
| |
| app.get('/', (req, res) => { |
| const html = `<!DOCTYPE html> |
| <html lang="pt-BR"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>OpenClaw Gateway - HF Space</title> |
| <style> |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body { |
| font-family: 'Segoe UI', system-ui, sans-serif; |
| background: #0f0f23; |
| color: #e0e0e0; |
| min-height: 100vh; |
| padding: 2rem; |
| } |
| .container { max-width: 900px; margin: 0 auto; } |
| h1 { |
| font-size: 2rem; |
| background: linear-gradient(135deg, #6366f1, #8b5cf6); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| margin-bottom: 0.5rem; |
| } |
| .subtitle { color: #888; margin-bottom: 2rem; } |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; } |
| .card { |
| background: #1a1a2e; |
| border: 1px solid #2a2a4a; |
| border-radius: 12px; |
| padding: 1.5rem; |
| } |
| .card h3 { color: #6366f1; margin-bottom: 1rem; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.1em; } |
| .status { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; } |
| .dot { width: 8px; height: 8px; border-radius: 50%; } |
| .dot.green { background: #22c55e; box-shadow: 0 0 8px #22c55e; } |
| .dot.red { background: #ef4444; } |
| .dot.yellow { background: #f59e0b; } |
| .endpoint { |
| background: #0f0f23; |
| border: 1px solid #2a2a4a; |
| border-radius: 8px; |
| padding: 1rem; |
| margin-bottom: 0.5rem; |
| font-family: monospace; |
| font-size: 0.85rem; |
| } |
| .method { |
| display: inline-block; |
| padding: 0.2rem 0.6rem; |
| border-radius: 4px; |
| font-size: 0.75rem; |
| font-weight: bold; |
| margin-right: 0.5rem; |
| } |
| .get { background: #065f46; color: #6ee7b7; } |
| .post { background: #1e3a5f; color: #93c5fd; } |
| .footer { color: #555; font-size: 0.8rem; text-align: center; margin-top: 2rem; } |
| #metrics { font-size: 0.85rem; color: #888; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🦅 OpenClaw Gateway</h1> |
| <p class="subtitle">Hugging Face Space · Node.js ${process.version} · ${CONFIG.nodeEnv}</p> |
| |
| <div class="grid"> |
| <div class="card"> |
| <h3>Status dos Serviços</h3> |
| <div class="status"> |
| <div class="dot ${CONFIG.openclaw.apiKey ? 'green' : 'red'}"></div> |
| <span>OpenClaw API</span> |
| </div> |
| <div class="status"> |
| <div class="dot ${CONFIG.ai.openrouterKey ? 'green' : 'yellow'}"></div> |
| <span>OpenRouter</span> |
| </div> |
| <div class="status"> |
| <div class="dot ${CONFIG.ai.geminiKey ? 'green' : 'yellow'}"></div> |
| <span>Google Gemini</span> |
| </div> |
| <div class="status"> |
| <div class="dot ${CONFIG.ai.groqKey ? 'green' : 'yellow'}"></div> |
| <span>Groq</span> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h3>Métricas em Tempo Real</h3> |
| <div id="metrics">Carregando...</div> |
| </div> |
| |
| <div class="card"> |
| <h3>Configuração Rápida</h3> |
| <p style="font-size:0.85rem; color:#888; line-height:1.6"> |
| Configure suas chaves em:<br> |
| <strong style="color:#6366f1">HF Space Settings</strong><br> |
| → Variables and Secrets |
| </p> |
| </div> |
| </div> |
| |
| <div class="card"> |
| <h3>Endpoints Disponíveis</h3> |
| <div style="margin-top: 1rem;"> |
| <div class="endpoint"> |
| <span class="method get">GET</span>/health · Healthcheck |
| </div> |
| <div class="endpoint"> |
| <span class="method post">POST</span>/api/chat · Chat com IA (OpenRouter/Gemini/Groq) |
| </div> |
| <div class="endpoint"> |
| <span class="method post">POST</span>/api/openclaw/execute · Executar task OpenClaw |
| </div> |
| <div class="endpoint"> |
| <span class="method get">GET</span>/api/models · Listar modelos disponíveis |
| </div> |
| </div> |
| </div> |
| |
| <div class="footer"> |
| OpenClaw Gateway v1.0.0 · Rodando em Hugging Face Spaces · Uptime: <span id="uptime">0</span>s |
| </div> |
| </div> |
| |
| <script> |
| async function updateMetrics() { |
| try { |
| const r = await fetch('/health'); |
| const d = await r.json(); |
| document.getElementById('metrics').innerHTML = |
| '<div>RAM: ' + d.memory.heapUsed + ' / ' + d.memory.heapTotal + '</div>' + |
| '<div>RSS: ' + d.memory.rss + '</div>' + |
| '<div>Uptime: ' + d.uptime + 's</div>'; |
| document.getElementById('uptime').textContent = d.uptime; |
| } catch(e) {} |
| } |
| updateMetrics(); |
| setInterval(updateMetrics, 5000); |
| </script> |
| </body> |
| </html>`; |
| |
| res.status(200).send(html); |
| }); |
|
|
| |
| |
| |
| |
| |
| app.post('/api/chat', async (req, res) => { |
| |
| 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.' }); |
| } |
| |
| |
| 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; |
| |
| |
| 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', |
| 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, |
| }; |
| } |
| |
| |
| 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, |
| }; |
| } |
| |
| |
| 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', |
| 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, |
| }); |
| } |
| }); |
|
|
| |
| |
| |
| |
| 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 }); |
| } |
| }); |
|
|
| |
| |
| |
| 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', |
| ], |
| }, |
| }, |
| }); |
| }); |
|
|
| |
| |
| |
| |
| 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 }); |
| }, |
| }, |
| })); |
| } |
|
|
| |
| |
| |
| |
| |
| 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, |
| }); |
| }); |
|
|
| |
| |
| |
| app.use((req, res) => { |
| res.status(404).json({ error: `Rota não encontrada: ${req.method} ${req.path}` }); |
| }); |
|
|
| |
| |
| |
| |
| |
| 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'}`); |
| }); |
|
|
| |
| server.timeout = 30000; |
| server.keepAliveTimeout = 65000; |
| server.headersTimeout = 66000; |
|
|
| |
| function gracefulShutdown(signal) { |
| console.log(`\n[SHUTDOWN] Recebido ${signal}. Encerrando servidor...`); |
| |
| server.close((err) => { |
| if (err) { |
| console.error('[SHUTDOWN ERROR]', err); |
| process.exit(1); |
| } |
| |
| |
| if (global.gc) { |
| global.gc(); |
| } |
| |
| console.log('[SHUTDOWN] Servidor encerrado com sucesso.'); |
| process.exit(0); |
| }); |
| |
| |
| 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')); |
|
|
| |
| |
| process.on('uncaughtException', (err) => { |
| console.error('[UNCAUGHT EXCEPTION]', err.stack); |
| |
| }); |
|
|
| process.on('unhandledRejection', (reason) => { |
| console.error('[UNHANDLED REJECTION]', reason); |
| }); |