zmx / app.js
rr1's picture
Update app.js
8f770b2 verified
Raw
History Blame Contribute Delete
7.35 kB
const express = require('express');
const axios = require('axios');
const app = express();
const PORT = process.env.PORT || 7860;
// 从环境变量获取目标 API 基础 URL
const TARGET_API = process.env.TARGET_API || "https://huggingface.co";
// 1. 客户端访问此代理所需的 API Key(若未设置则不进行校验)
const AUTH_API_KEY = process.env.AUTH_API_KEY;
// 2. 解析请求目标服务器时需要注入的 API Key 数组
let DEST_API_KEYS = [];
if (process.env.DEST_API_KEY) {
try {
DEST_API_KEYS = JSON.parse(process.env.DEST_API_KEY);
if (!Array.isArray(DEST_API_KEYS)) {
DEST_API_KEYS = [process.env.DEST_API_KEY];
}
} catch (e) {
DEST_API_KEYS = process.env.DEST_API_KEY.split(',').map(k => k.trim()).filter(Boolean);
}
}
// 解析路径映射环境变量
let PATH_MAPPINGS = { "/": "/" };
try {
if (process.env.PATH_MAPPINGS) {
PATH_MAPPINGS = JSON.parse(process.env.PATH_MAPPINGS);
}
} catch (e) {
console.error("解析 PATH_MAPPINGS 失败,使用默认配置:", e.message);
PATH_MAPPINGS = { "/": "/" };
}
// 明确定义的路由放在最上面:根路径 GET 请求直接响应(用于健康检查,免 Key 校验)
app.get('/', (req, res) => {
res.send("service running.");
});
// 使用不带路径参数的 app.use() 作为万能兜底代理
app.use(async (req, res) => {
// 校验客户端的 AUTH_API_KEY
if (AUTH_API_KEY) {
let clientKey = '';
// 判断路径是否包含 /v1/messages 或 /v1/responses
if (req.path.includes('/v1/messages') || req.path.includes('/v1/responses')) {
// 优先从 x-api-key 获取,没有则尝试从 Authorization 获取
clientKey = req.headers['x-api-key'] || (req.headers['authorization'] || '').replace(/^Bearer\s+/i, '').trim();
} else {
// 其他路径走标准的 Authorization Bearer 认证
const authHeader = req.headers['authorization'] || '';
clientKey = authHeader.replace(/^Bearer\s+/i, '').trim();
}
if (clientKey !== AUTH_API_KEY) {
return res.status(401).send("Unauthorized: Invalid or missing API Key");
}
}
let fullPath = req.path;
// 应用路径映射(仅替换一次)
for (const [originalPath, newPath] of Object.entries(PATH_MAPPINGS)) {
if (fullPath.startsWith(originalPath)) {
fullPath = fullPath.replace(originalPath, newPath);
break;
}
}
// 拼接最终目标 URL
const targetUrl = `${TARGET_API}${fullPath}`;
// 复制请求头并过滤掉 host
const headers = { ...req.headers };
delete headers.host;
// 请求体缓存:为了支持重试,必须将 req 流缓存为 Buffer,否则流只能读一次,重试时将没有数据可发
let bodyBuffer = [];
req.on('data', chunk => bodyBuffer.push(chunk));
// 等待请求体接收完毕再执行后续逻辑
await new Promise(resolve => req.on('end', resolve));
const bodyData = Buffer.concat(bodyBuffer);
// 根据目标路径动态注入认证头
if (DEST_API_KEYS.length > 0) {
const randomKey = DEST_API_KEYS[Math.floor(Math.random() * DEST_API_KEYS.length)];
// 检查映射后的路径并细分处理
if (fullPath.includes('messages')) {
// messages 路径:仅使用 x-api-key
headers['x-api-key'] = randomKey;
delete headers['authorization']; // 强行移除原 Authorization,防止双认证头冲突
} else if (fullPath.includes('responses')) {
// responses 路径:同时保留并设置两个认证头
headers['x-api-key'] = randomKey;
headers['authorization'] = `Bearer ${randomKey}`;
} else {
// 传统 /v1/chat 等路由,走 OpenAI 标准的 Authorization 头
headers['authorization'] = `Bearer ${randomKey}`;
delete headers['x-api-key'];
}
}
// 【排查日志】打印每次转发的真实细节
//console.log(`[Proxy Request] Method: ${req.method} | URL: ${targetUrl}`);
//console.log(`[Proxy Headers] Authorization: ${headers['authorization'] ? 'Present' : 'None'} | x-api-key: ${headers['x-api-key'] ? 'Present' : 'None'}`);
const MAX_RETRIES = 60;
let attempt = 0;
// 重试循环
while (attempt < MAX_RETRIES) {
attempt++;
try {
// 发起代理请求
const response = await axios({
method: req.method,
url: targetUrl,
headers: headers,
params: req.query,
data: bodyData.length > 0 ? bodyData : undefined, // 使用缓存的请求体
responseType: 'stream',
validateStatus: () => true // 允许捕获所有状态码,交给我们手动判断
});
// 打印目标服务器返回的状态码
//console.log(`[Proxy Response] Attempt ${attempt} | Status Code From Target: ${response.status}`);
// 如果状态码是 200,则将其响应流管道输送给客户端并退出循环
if (response.status === 200) {
// 复制响应状态码
res.status(response.status);
// 复制并过滤响应头
const skipHeaders = ['content-length', 'transfer-encoding', 'connection'];
Object.entries(response.headers).forEach(([key, value]) => {
if (!skipHeaders.includes(key.toLowerCase())) {
res.setHeader(key, value);
}
});
// 将目标响应流管道输送给客户端
response.data.pipe(res);
return; // 成功响应,退出函数
} else {
// 状态码不是 200,销毁当前流,进入下一次重试
response.data.destroy();
// 如果已达最大重试次数,直接把最后一次的非200状态码透传给客户端并返回错误信息
if (attempt >= MAX_RETRIES) {
console.error(`[Proxy Failed] Reached max retries (${MAX_RETRIES}). Last status: ${response.status}`);
if (!res.headersSent) {
res.status(response.status).send(`Proxy Error: Target server returned status ${response.status} after ${MAX_RETRIES} attempts.`);
}
return;
}
// 继续下一次循环重试
}
} catch (error) {
// 只有发生网络断开、DNS 解析失败等底层错误时才会走到这里
console.error(`[Proxy Fatal Error on attempt ${attempt}]: ${error.message}`);
// 如果已达最大重试次数,返回 500 错误
if (attempt >= MAX_RETRIES) {
if (!res.headersSent) {
res.status(500).send(`Proxy Error: ${error.message}`);
}
return;
}
// 继续下一次循环重试
}
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`服务已在端口 ${PORT} 启动`);
});