Spaces:
Running
Running
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'}`); | |
}); | |