Files
prompt-optimizer/node-proxy/server.js
T
linshen 80977ec8a1 feat(proxy): 优化 Docker 代理功能并完善错误处理 (develop)
本次提交主要对 Docker 代理功能进行了优化,并完善了相关的错误处理逻辑。具体内容包括:

- 精确匹配 API 代理路径,使用 `location =` 确保 Nginx 配置中的代理路径精确匹配,避免被其他正则匹配拦截。
- 添加对本地主机 URL 的自动转换支持,在 Docker 容器内部访问宿主机时,自动将 `localhost`、`127.0.0.1` 等地址转换为 `host.docker.internal`。
- 优化 ModelManager 组件,将 `useDockerProxy` 选项添加到模型配置中,调整 API URL 和 API Key 的输入位置,增强用户体验。
- 完善错误处理逻辑,为 Node Proxy 服务增加更详细的错误处理,并提供统一的 JSON 错误响应格式。
- 在开发环境的 `docker-compose.dev.yml` 文件中添加 `host.docker.internal` 映射,允许容器访问宿主机。
- 更新了相关文档,包括架构对比、实施计划和设计文档,详细描述了 Docker 代理功能的实现原理和使用方法。
- 调整了文档的分类和归档方式,将经验记录归档到 archives 目录。
2025-08-17 10:37:12 +08:00

209 lines
7.1 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';
let targetUrl = url.searchParams.get('targetUrl');
// 验证targetUrl参数
if (!targetUrl) {
return json(res, 400, { error: 'Missing targetUrl parameter' });
}
// 自动将本地主机地址替换为 host.docker.internal 以便在 Docker 容器内访问宿主机
// 同时验证 URL 的有效性
try {
const targetUrlObject = new URL(targetUrl);
const localhostNames = ['localhost', '127.0.0.1', '[::1]'];
if (localhostNames.includes(targetUrlObject.hostname)) {
const originalHostname = targetUrlObject.hostname;
targetUrlObject.hostname = 'host.docker.internal';
targetUrl = targetUrlObject.toString();
console.log(`[${new Date().toISOString()}] [${requestId}] Remapped localhost URL from ${originalHostname} to host.docker.internal. New target: ${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);
});
});