#!/usr/bin/env node /** * OpenClaw HTTP → WebSocket 代理 * 把 OpenAI 格式的 HTTP 请求转成 WebSocket 发送给 Gateway */ const http = require('http'); const WebSocket = require('ws'); const url = require('url'); const fs = require('fs'); const path = require('path'); // ============= 配置 ============= const GATEWAY_URL = process.env.GATEWAY_URL || 'ws://localhost:18789'; const API_PORT = process.env.API_PORT || 8081; const API_KEY = process.env.OPENCLAW_API_KEY || 'your-api-key-change-me'; let gatewayWs = null; // ============= 连接 Gateway ============= function connectGateway() { return new Promise((resolve, reject) => { console.log(`🔌 正在连接 Gateway: ${GATEWAY_URL}...`); gatewayWs = new WebSocket(GATEWAY_URL); gatewayWs.on('open', () => { console.log('✅ 已连接到 Gateway'); resolve(); }); gatewayWs.on('error', (err) => { console.error('❌ Gateway 连接失败:', err.message); reject(err); }); gatewayWs.on('message', (data) => { // Gateway 主动推送的消息(如定时任务) try { const msg = JSON.parse(data); console.log('📨 Gateway 消息:', JSON.stringify(msg).substring(0, 200)); } catch(e) { console.log('📨 Gateway 消息:', data.toString().substring(0, 200)); } }); gatewayWs.on('close', () => { console.log('⚠️ Gateway 连接关闭,尝试重连...'); setTimeout(() => connectGateway().catch(console.error), 3000); }); }); } // ============= 发送消息到 Gateway ============= function sendToGateway(message) { return new Promise((resolve, reject) => { if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) { reject(new Error('Gateway 未连接')); return; } const requestId = Date.now().toString(); const payload = { jsonrpc: "2.0", method: "agent.turn", params: { message: message, model: "default" }, id: requestId }; // 临时监听器 const timeout = setTimeout(() => { gatewayWs.removeListener('message', handler); reject(new Error('Gateway 超时')); }, 60000); const handler = (data) => { try { const msg = JSON.parse(data); if (msg.id === requestId) { clearTimeout(timeout); gatewayWs.removeListener('message', handler); resolve(msg.result || msg); } } catch(e) {} }; gatewayWs.on('message', handler); gatewayWs.send(JSON.stringify(payload)); }); } // ============= HTTP 服务器 ============= const server = http.createServer((req, res) => { // CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const parsedUrl = url.parse(req.url, true); const path = parsedUrl.pathname; // 认证 const authHeader = req.headers['authorization'] || ''; const apiKey = authHeader.replace('Bearer ', ''); if (apiKey && apiKey !== API_KEY) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Invalid API Key', type: 'authentication_error' } })); return; } // 路由 if (path === '/' || path === '/health') { // 健康检查 const status = gatewayWs && gatewayWs.readyState === WebSocket.OPEN ? 'healthy' : 'unhealthy'; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status, gateway: gatewayWs && gatewayWs.readyState === WebSocket.OPEN ? 'connected' : 'disconnected', timestamp: new Date().toISOString() })); } else if (path === '/v1/models') { // 模型列表 res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ object: 'list', data: [{ id: 'openclaw', object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'openclaw' }] })); } else if (path === '/v1/chat/completions' && req.method === 'POST') { // Chat Completions let body = ''; req.on('data', chunk => body += chunk); req.on('end', async () => { try { const data = JSON.parse(body); // 提取用户消息 let userMessage = ''; const messages = data.messages || []; for (const msg of messages) { if (msg.role === 'user') { userMessage = msg.content; break; } } if (!userMessage) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'No user message', type: 'invalid_request_error' } })); return; } // 发送到 Gateway let result; try { result = await sendToGateway(userMessage); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: err.message } })); return; } // 构造 OpenAI 格式回复 const replyContent = result.text || result.content || JSON.stringify(result); const response = { id: 'chatcmpl-' + Math.random().toString(36).substring(2, 15), object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: data.model || 'openclaw', choices: [{ index: 0, message: { role: 'assistant', content: replyContent }, finish_reason: 'stop' }], usage: { prompt_tokens: userMessage.split(' ').length, completion_tokens: replyContent.split(' ').length, total_tokens: userMessage.split(' ').length + replyContent.split(' ').length } }; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(response)); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: err.message } })); } }); } else { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found' })); } }); // ============= 启动 ============= async function main() { try { // 先连接 Gateway await connectGateway(); // 启动 HTTP 服务 server.listen(API_PORT, '0.0.0.0', () => { console.log(`\n🚀 OpenClaw HTTP Proxy 启动成功!`); console.log(` API 地址: http://localhost:${API_PORT}/v1`); console.log(` Gateway: ${GATEWAY_URL}`); console.log(` API Key: ${API_KEY}`); console.log(`\n📡 端点:`); console.log(` GET /health - 健康检查`); console.log(` GET /v1/models - 模型列表`); console.log(` POST /v1/chat/completions - 对话`); console.log(`\n💡 ChatBox 配置:`); console.log(` API 地址: http://43.163.195.176:${API_PORT}/v1`); console.log(` API Key: ${API_KEY}`); }); } catch (err) { console.error('启动失败:', err.message); process.exit(1); } } main();