Files
server-configs/openclaw-http-proxy.js
2026-02-13 22:24:27 +08:00

244 lines
8.5 KiB
JavaScript
Executable File

#!/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();