/**
* 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}
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);
});