import express from "express"; import { spawn } from "child_process"; import dotenv from "dotenv"; dotenv.config(); const app = express(); app.use(express.json()); /* =============================== 1. BASIC AUTH ================================ */ const GATE_KEY = process.env.OPENCLAW_GATE_KEY; app.use((req, res, next) => { if (!GATE_KEY) return next(); if (req.headers["x-openclaw-key"] !== GATE_KEY) { return res.status(401).json({ error: "Unauthorized" }); } next(); }); /* =============================== 2. RATE LIMITING (IP-based) ================================ */ const RATE_LIMIT = 30; // requests const WINDOW_MS = 60_000; const ipHits = new Map(); app.use((req, res, next) => { const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; const now = Date.now(); const record = ipHits.get(ip) || { count: 0, ts: now }; if (now - record.ts > WINDOW_MS) { record.count = 0; record.ts = now; } record.count++; ipHits.set(ip, record); if (record.count > RATE_LIMIT) { return res.status(429).json({ error: "Rate limit exceeded" }); } next(); }); /* =============================== 3. LOAD KEY POOLS ================================ */ function loadPool(prefix) { const keys = Object.keys(process.env) .filter(k => k.startsWith(prefix)) .map(k => process.env[k]) .filter(Boolean); console.log(`Loaded ${keys.length} keys for ${prefix}`); return keys; } const PROVIDERS = { openai: loadPool("OPENAI_API_KEY_"), deepseek: loadPool("DEEPSEEK_API_KEY_"), gemini: loadPool("GEMINI_API_KEY_"), openrouter: loadPool("OPENROUTER_API_KEY_"), dashscope: loadPool("DASHSCOPE_API_KEY_") }; function randomKey(pool) { return pool[Math.floor(Math.random() * pool.length)]; } /* =============================== 4. TASK ROUTING ================================ */ const TASKS = new Set([ "market_research", "summarize", "classify" ]); /* =============================== 5. RUN AGENT ================================ */ app.use((req, res, next) => { if (req.method === "POST" && !req.is("application/json")) { return res.status(400).json({ error: "JSON body required" }); } next(); }); app.get("/", (req, res) => { res.json({ status: "OpenClaw running", service: "market_research_agent" }); }); app.post("/run", async (req, res) => { console.log('OpenClaw: Received request:', { task: req.body.task, keyword: req.body.keyword, timestamp: new Date().toISOString() }); const { task = "market_research", provider, model, ...payload } = req.body; if (!TASKS.has(task)) { return res.status(400).json({ error: "Unknown task" }); } const providersToTry = provider ? [provider, ...Object.keys(PROVIDERS).filter(p => p !== provider)] : Object.keys(PROVIDERS); let lastError; for (const p of providersToTry) { const pool = PROVIDERS[p]; if (!pool || pool.length === 0) continue; for (let i = 0; i < pool.length; i++) { const apiKey = randomKey(pool); const env = { ...process.env, OPENCLAW_PROVIDER: p, OPENCLAW_MODEL: model || "", OPENCLAW_API_KEY: apiKey, OPENCLAW_TASK: task, OPENCLAW_TIMEOUT: "180000" }; try { const result = await runOpenClaw(env, payload); return res.json(result); } catch (err) { lastError = err; if (!isRateLimit(err)) break; } } } res.status(500).json({ error: "All providers failed", details: lastError?.message, keyword: req.body.keyword, timestamp: new Date().toISOString() }); }); // Add specific market research endpoint for n8n compatibility app.post("/api/market-research", async (req, res) => { console.log('OpenClaw: Market research request:', { keyword: req.body.keyword, timestamp: new Date().toISOString() }); const { keyword, api_key, provider = "deepseek" } = req.body; if (!keyword) { return res.status(400).json({ error: "keyword is required" }); } // Use API key from request or fallback to environment let providersToTry; let apiKeys = {}; if (api_key) { // Use API key from n8n request console.log('OpenClaw: Using API key from request'); apiKeys[provider] = [api_key]; providersToTry = [provider]; } else { // Fallback to environment variables (for local testing) console.log('OpenClaw: Using API keys from environment'); providersToTry = Object.keys(PROVIDERS); apiKeys = PROVIDERS; } let lastError; for (const p of providersToTry) { const pool = apiKeys[p]; if (!pool || pool.length === 0) { console.log(`OpenClaw: No API keys for provider ${p}`); continue; } for (let i = 0; i < pool.length; i++) { const apiKey = pool[i]; const env = { ...process.env, OPENCLAW_PROVIDER: p, OPENCLAW_API_KEY: apiKey, OPENCLAW_TASK: "market_research", OPENCLAW_TIMEOUT: "180000" }; try { console.log(`OpenClaw: Trying provider ${p} with key ${apiKey.substring(0, 10)}...`); const result = await runOpenClaw(env, { keyword }); console.log('OpenClaw: Success with provider', p); return res.json(result); } catch (err) { lastError = err; console.error('OpenClaw: Provider failed:', p, err.message); if (!isRateLimit(err)) break; } } } res.status(500).json({ error: "All providers failed", details: lastError?.message || "No API keys configured", keyword: keyword, timestamp: new Date().toISOString() }); }); /* =============================== 6. EXECUTION + JSON PARSE ================================ */ function runOpenClaw(env, payload) { return new Promise((resolve, reject) => { const proc = spawn(process.execPath, ["src/index.js"], { cwd: "/app", env }); let stdout = ""; let stderr = ""; // CRITICAL: handle spawn errors proc.on("error", err => { reject(err); }); proc.stdout.on("data", data => { stdout += data.toString(); }); proc.stderr.on("data", data => { stderr += data.toString(); }); proc.on("close", code => { if (code !== 0) { console.error('OpenClaw: Agent exited with code:', code); console.error('OpenClaw: stderr:', stderr); return reject( new Error(stderr || `Agent exited with code ${code}`) ); } try { console.log('OpenClaw: Agent stdout (first 500 chars):', stdout.substring(0, 500)); const json = JSON.parse(stdout); console.log('OpenClaw: JSON parsed successfully'); resolve(json); } catch (err) { console.error('OpenClaw: JSON parse error:', err.message); console.error('OpenClaw: Raw stdout:', stdout); reject(new Error("Invalid JSON from OpenClaw agent: " + err.message)); } }); // SEND INPUT TO AGENT proc.stdin.write(JSON.stringify(payload)); proc.stdin.end(); }); } function isRateLimit(err) { const msg = err.message?.toLowerCase() || ""; return ( msg.includes("429") || msg.includes("rate") || msg.includes("quota") || msg.includes("limit") ); } /* =============================== 7. START ================================ */ const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`🚀 OpenClaw Agent Gateway running on port ${PORT}`); });