Files
prompt-optimizer/node-proxy/server.js
T
linshen 8ac9665781 feat(core): 添加 Docker API 代理支持
- 新增 Node.js代理服务实现 API 请求和流式响应的代理
- 修改 nginx配置,增加 API 代理路径
- 更新前端 ModelManager 组件,集成 Docker 代理选项
- 优化错误处理和日志记录,确保生产就绪
- 添加详细的文档和测试报告
2025-08-16 20:55:37 +08:00

201 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import http from 'node:http';
import { Readable } from 'node:stream';
const PORT = process.env.PORT || 3001;
const STREAM_TIMEOUT = parseInt(process.env.STREAM_TIMEOUT || '300000'); // 默认5分钟
const PROXY_TIMEOUT = parseInt(process.env.PROXY_TIMEOUT || '120000'); // 默认2分钟
/**
* 统一JSON响应函数
*/
function json(res, code, obj) {
res.statusCode = code;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(obj));
}
/**
* 增强的请求日志
*/
function logRequest(req, targetUrl, status, startTime, requestId) {
const duration = Date.now() - startTime;
const timestamp = new Date().toISOString();
const clientIP = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection?.remoteAddress || 'unknown';
console.log(`[${timestamp}] [${requestId}] ${clientIP} ${req.method} ${targetUrl} -> ${status} (${duration}ms)`);
}
/**
* 生成简单的请求ID
*/
function generateRequestId() {
return Math.random().toString(36).substr(2, 9);
}
/**
* 主要的代理服务器
*/
const server = http.createServer(async (req, res) => {
const startTime = Date.now();
const requestId = generateRequestId();
try {
const url = new URL(req.url || '', 'http://localhost');
const isStream = url.pathname === '/api/stream';
const targetUrl = url.searchParams.get('targetUrl');
// 验证targetUrl参数
if (!targetUrl) {
return json(res, 400, { error: 'Missing targetUrl parameter' });
}
// 验证targetUrl是否为有效URL
try {
new URL(targetUrl);
} catch {
return json(res, 400, { error: 'Invalid targetUrl parameter' });
}
// 复制请求头(排除会引发问题的头)
const headers = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
if (!['host', 'connection', 'content-length'].includes(k.toLowerCase()) && v) {
headers.set(k, Array.isArray(v) ? v.join(',') : String(v));
}
}
// 读取请求体(仅非GET/HEAD)
let body = undefined;
if (!['GET', 'HEAD'].includes(req.method || 'GET')) {
body = await new Promise((resolve) => {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
});
}
// 发起上游请求(添加超时防护:流式和普通请求不同超时)
const controller = new AbortController();
const timeoutMs = isStream ? STREAM_TIMEOUT : PROXY_TIMEOUT;
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
let upstream;
try {
upstream = await fetch(targetUrl, {
method: req.method,
headers,
body,
signal: controller.signal
});
} finally {
// 确保定时器总是被清理,防止内存泄漏
clearTimeout(timeoutId);
}
// 设置CORS头(简化版本)
const origin = req.headers['origin'];
if (origin && typeof origin === 'string') {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
} else {
res.setHeader('Access-Control-Allow-Origin', '*');
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-KEY');
// 处理OPTIONS预检请求
if (req.method === 'OPTIONS') {
res.statusCode = 204;
logRequest(req, targetUrl, 204, startTime, requestId);
return res.end();
}
// 透传状态码和响应头
res.statusCode = upstream.status;
res.statusMessage = upstream.statusText;
// 透传重要的响应头
upstream.headers.forEach((val, key) => {
// 跳过一些可能冲突的头
if (!['access-control-allow-origin', 'access-control-allow-methods', 'access-control-allow-headers'].includes(key.toLowerCase())) {
res.setHeader(key, val);
}
});
// HEAD请求特殊处理(只返回头部,不处理响应体)
if (req.method === 'HEAD') {
logRequest(req, targetUrl, upstream.status, startTime, requestId);
return res.end();
}
// 处理响应体
if (isStream && upstream.body) {
// 流式响应:WebStream → Node Readable 转换
logRequest(req, targetUrl, upstream.status, startTime, requestId);
Readable.fromWeb(upstream.body).pipe(res);
} else {
// 普通响应
const buf = Buffer.from(await upstream.arrayBuffer());
logRequest(req, targetUrl, upstream.status, startTime, requestId);
res.end(buf);
}
} catch (error) {
// 增强的错误处理
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] [${requestId}] Proxy error:`, error);
logRequest(req, req.url || 'unknown', 'ERROR', startTime, requestId);
if (!res.headersSent) {
// 区分不同类型的错误并提供友好消息
let statusCode = 500;
let errorMessage = 'Internal server error';
if (error.name === 'AbortError' || error.message.includes('aborted')) {
statusCode = 504;
errorMessage = 'Request timeout - the target server took too long to respond';
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
statusCode = 502;
errorMessage = 'Bad gateway - unable to connect to target server';
} else if (error.code === 'ECONNRESET') {
statusCode = 502;
errorMessage = 'Connection reset by target server';
} else if (error instanceof TypeError && error.message.includes('fetch')) {
statusCode = 400;
errorMessage = 'Invalid target URL or request format';
} else {
errorMessage = error instanceof Error ? error.message : String(error);
}
json(res, statusCode, {
error: errorMessage,
...(process.env.NODE_ENV === 'development' && {
details: error.stack,
requestId
})
});
}
}
});
// 启动服务器
server.listen(PORT, '127.0.0.1', () => {
console.log(`[${new Date().toISOString()}] Node Proxy server listening on 127.0.0.1:${PORT}`);
});
// 优雅关闭
process.on('SIGTERM', () => {
console.log(`[${new Date().toISOString()}] Received SIGTERM, shutting down gracefully`);
server.close(() => {
console.log(`[${new Date().toISOString()}] Node Proxy server closed`);
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log(`[${new Date().toISOString()}] Received SIGINT, shutting down gracefully`);
server.close(() => {
console.log(`[${new Date().toISOString()}] Node Proxy server closed`);
process.exit(0);
});
});