244 lines
8.5 KiB
JavaScript
Executable File
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();
|