isididiidid's picture
Update hf.js
6a707b0 verified
raw
history blame
12.7 kB
const express = require('express');
const morgan = require('morgan');
const { createProxyMiddleware } = require('http-proxy-middleware');
const axios = require('axios');
const url = require('url');
const app = express();
// 启用日志
app.use(morgan('dev'));
// 环境变量配置
const PORT = process.env.HF_PORT || 7860;
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:3010';
const API_PATH = process.env.API_PATH || '/v1';
const TIMEOUT = parseInt(process.env.TIMEOUT) || 30000;
console.log(`Service configuration:
- Port: ${PORT}
- Target URL: ${TARGET_URL}
- API Path: ${API_PATH}
- Timeout: ${TIMEOUT}ms`);
// 解析代理设置
let proxyPool = [];
if (process.env.PROXY) {
proxyPool = process.env.PROXY.split(',').map(p => p.trim()).filter(p => p);
console.log(`Loaded ${proxyPool.length} proxies from environment`);
if (proxyPool.length > 0) {
console.log('Proxy pool initialized:');
proxyPool.forEach((proxy, index) => {
// 隐藏敏感信息的日志
const maskedProxy = proxy.replace(/(https?:\/\/)([^:]+):([^@]+)@/, '$1$2:****@');
console.log(` [${index + 1}] ${maskedProxy}`);
});
}
}
// 从代理池中随机选择一个代理
function getRandomProxy() {
if (proxyPool.length === 0) return null;
const randomIndex = Math.floor(Math.random() * proxyPool.length);
const proxyUrl = proxyPool[randomIndex];
const parsedUrl = url.parse(proxyUrl);
return {
host: parsedUrl.hostname,
port: parsedUrl.port || 80,
auth: parsedUrl.auth ? {
username: parsedUrl.auth.split(':')[0],
password: parsedUrl.auth.split(':')[1]
} : undefined
};
}
// 模型列表 API
app.get('/hf/v1/models', (req, res) => {
const models = {
"object": "list",
"data": [
{
"id": "claude-3.5-sonnet",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gpt-4",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gpt-4o",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "claude-3-opus",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gpt-3.5-turbo",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gpt-4-turbo-2024-04-09",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gpt-4o-128k",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gemini-1.5-flash-500k",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "claude-3-haiku-200k",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "claude-3-5-sonnet-200k",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "claude-3-5-sonnet-20241022",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gpt-4o-mini",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "o1-mini",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "o1-preview",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "o1",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "claude-3.5-haiku",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gemini-exp-1206",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gemini-2.0-flash-thinking-exp",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "gemini-2.0-flash-exp",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "deepseek-v3",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
},
{
"id": "deepseek-r1",
"object": "model",
"created": 1706745938,
"owned_by": "cursor"
}
]
};
res.json(models);
});
// 代理转发,使用动态代理池
app.use('/hf/v1/chat/completions', (req, res, next) => {
const proxy = getRandomProxy();
const targetEndpoint = `${TARGET_URL}${API_PATH}/chat/completions`;
console.log(`Forwarding request to: ${targetEndpoint}`);
const middleware = createProxyMiddleware({
target: targetEndpoint,
changeOrigin: true,
proxy: proxy ? proxy : undefined,
timeout: TIMEOUT,
proxyTimeout: TIMEOUT,
onProxyReq: (proxyReq, req, res) => {
if (req.body) {
const bodyData = JSON.stringify(req.body);
proxyReq.setHeader('Content-Type', 'application/json');
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
proxyReq.write(bodyData);
proxyReq.end();
}
},
onError: (err, req, res) => {
console.error('Proxy error:', err);
res.status(500).json({
error: {
message: 'Proxy error occurred',
type: 'proxy_error',
details: process.env.NODE_ENV === 'development' ? err.message : undefined
}
});
},
onProxyRes: (proxyRes, req, res) => {
console.log(`Proxy response status: ${proxyRes.statusCode}`);
}
});
if (proxy) {
const maskedProxy = `${proxy.host}:${proxy.port}` + (proxy.auth ? ' (with auth)' : '');
console.log(`Using proxy: ${maskedProxy}`);
} else {
console.log('Direct connection (no proxy)');
}
middleware(req, res, next);
});
// 首页
app.get('/', (req, res) => {
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cursor To OpenAI</title>
<style>
:root {
--primary-color: #2563eb;
--bg-color: #f8fafc;
--card-bg: #ffffff;
}
body {
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1000px;
margin: 0 auto;
line-height: 1.6;
background: var(--bg-color);
color: #1a1a1a;
}
.container {
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
color: var(--primary-color);
font-size: 2.5em;
margin-bottom: 10px;
}
.info {
background: #fff;
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
border: 1px solid #e5e7eb;
}
.info-item {
margin: 15px 0;
padding: 10px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.info-label {
color: #4b5563;
font-size: 0.9em;
margin-bottom: 5px;
}
.info-value {
color: var(--primary-color);
font-weight: 500;
}
.service-status {
background: #dcfce7;
border: 1px solid #86efac;
color: #166534;
padding: 8px 12px;
border-radius: 6px;
font-size: 0.95em;
display: inline-block;
margin: 15px 0;
}
.models {
background: var(--card-bg);
padding: 25px;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #e5e7eb;
}
.models h3 {
color: #1a1a1a;
margin-bottom: 20px;
font-size: 1.5em;
border-bottom: 2px solid #e5e7eb;
padding-bottom: 10px;
}
.model-item {
margin: 12px 0;
padding: 15px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e5e7eb;
transition: all 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
}
.model-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.model-name {
font-weight: 500;
color: #1a1a1a;
}
.model-provider {
color: #6b7280;
font-size: 0.9em;
padding: 4px 8px;
background: #f1f5f9;
border-radius: 4px;
}
.details {
margin-top: 20px;
background: #f8fafc;
padding: 15px;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.details pre {
background: #f1f5f9;
padding: 10px;
border-radius: 6px;
overflow-x: auto;
}
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
padding: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Cursor To OpenAI Server</h1>
<p>高性能 AI 模型代理服务</p>
<div class="service-status">服务正在运行</div>
</div>
<div class="info">
<h2>配置信息</h2>
<div class="info-item">
<div class="info-label">服务环境</div>
<div class="info-value">环境变量配置模式</div>
</div>
<div class="info-item">
<div class="info-label">自定义端点(基本URL)</div>
<div class="info-value" id="endpoint-url"></div>
</div>
<div class="info-item">
<div class="info-label">目标服务</div>
<div class="info-value">${TARGET_URL}${API_PATH}</div>
</div>
<div class="info-item">
<div class="info-label">代理状态</div>
<div class="info-value">${proxyPool.length > 0 ? `使用中 (${proxyPool.length}个代理)` : '未启用'}</div>
</div>
</div>
<div class="models">
<h3>支持的模型列表</h3>
<div id="model-list"></div>
</div>
<div class="details">
<h3>使用说明</h3>
<p>在客户端配置以下信息:</p>
<pre>API URL: http://{服务器地址}:${PORT}/hf/v1
API Key: 您的API密钥</pre>
<p>支持兼容OpenAI格式的请求,已预配置转发到${TARGET_URL}${API_PATH}</p>
</div>
</div>
<script>
const url = new URL(window.location.href);
const link = url.protocol + '//' + url.host + '/hf/v1';
document.getElementById('endpoint-url').textContent = link;
fetch(link + '/models')
.then(response => response.json())
.then(data => {
const modelList = document.getElementById('model-list');
data.data.forEach(model => {
const div = document.createElement('div');
div.className = 'model-item';
div.innerHTML = \`
<span class="model-name">\${model.id}</span>
<span class="model-provider">\${model.owned_by}</span>
\`;
modelList.appendChild(div);
});
})
.catch(error => {
console.error('Error fetching models:', error);
document.getElementById('model-list').textContent = '获取模型列表失败';
});
</script>
</body>
</html>
`;
res.send(htmlContent);
});
// 健康检查端点
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
time: new Date().toISOString(),
proxyCount: proxyPool.length,
target: `${TARGET_URL}${API_PATH}`
});
});
// 启动服务
app.listen(PORT, () => {
console.log(`HF Proxy server is running at PORT: ${PORT}`);
console.log(`Target service: ${TARGET_URL}${API_PATH}`);
console.log(`Proxy status: ${proxyPool.length > 0 ? 'Enabled' : 'Disabled'}`);
});