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