|
import express from 'express'; |
|
import fetch from 'node-fetch'; |
|
import dotenv from 'dotenv'; |
|
import { v4 as uuidv4 } from 'uuid'; |
|
import cors from 'cors'; |
|
|
|
|
|
dotenv.config(); |
|
|
|
|
|
|
|
|
|
class Config { |
|
constructor() { |
|
this.initializeApiKeys(); |
|
this.initializeAuth(); |
|
|
|
|
|
this.usedApiKeys = []; |
|
|
|
|
|
this.invalidApiKeys = []; |
|
|
|
|
|
this.geminiSafety = [ |
|
{ |
|
category: 'HARM_CATEGORY_HARASSMENT', |
|
threshold: 'OFF', |
|
}, |
|
{ |
|
category: 'HARM_CATEGORY_HATE_SPEECH', |
|
threshold: 'OFF', |
|
}, |
|
{ |
|
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', |
|
threshold: 'OFF', |
|
}, |
|
{ |
|
category: 'HARM_CATEGORY_DANGEROUS_CONTENT', |
|
threshold: 'OFF', |
|
}, |
|
{ |
|
category: 'HARM_CATEGORY_CIVIC_INTEGRITY', |
|
threshold: 'OFF', |
|
}, |
|
]; |
|
} |
|
|
|
|
|
|
|
|
|
initializeApiKeys() { |
|
const apiKeysEnv = process.env.GEMINI_API_KEYS; |
|
if (!apiKeysEnv) { |
|
console.error('❌ 错误: 未找到 GEMINI_API_KEYS 环境变量'); |
|
process.exit(1); |
|
} |
|
|
|
|
|
this.apiKeys = apiKeysEnv |
|
.split('\n') |
|
.map(key => key.trim()) |
|
.filter(key => key.length > 0); |
|
|
|
if (this.apiKeys.length === 0) { |
|
console.error('❌ 错误: 没有找到有效的API Keys'); |
|
process.exit(1); |
|
} |
|
|
|
console.log(`✅ 成功加载 ${this.apiKeys.length} 个API Keys`); |
|
} |
|
|
|
|
|
|
|
|
|
initializeAuth() { |
|
this.authToken = process.env.AUTH_TOKEN || 'sk-123456'; |
|
} |
|
|
|
|
|
|
|
|
|
getApiKey() { |
|
if (this.apiKeys.length === 0) { |
|
if (this.usedApiKeys.length > 0) { |
|
this.apiKeys.push(...this.usedApiKeys); |
|
this.usedApiKeys = []; |
|
} else { |
|
return null; |
|
} |
|
} |
|
|
|
const apiKey = this.apiKeys.shift(); |
|
this.usedApiKeys.push(apiKey); |
|
return apiKey; |
|
} |
|
|
|
|
|
|
|
|
|
getFirstAvailableApiKey() { |
|
if (this.apiKeys.length > 0) { |
|
return this.apiKeys[0]; |
|
} |
|
if (this.usedApiKeys.length > 0) { |
|
return this.usedApiKeys[0]; |
|
} |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
markKeyAsInvalid(apiKey) { |
|
const usedIndex = this.usedApiKeys.indexOf(apiKey); |
|
if (usedIndex !== -1) { |
|
this.usedApiKeys.splice(usedIndex, 1); |
|
} |
|
|
|
const mainIndex = this.apiKeys.indexOf(apiKey); |
|
if (mainIndex !== -1) { |
|
this.apiKeys.splice(mainIndex, 1); |
|
} |
|
|
|
if (!this.invalidApiKeys.includes(apiKey)) { |
|
this.invalidApiKeys.push(apiKey); |
|
} |
|
|
|
console.warn(`⚠️ API Key 已标记为失效: ${apiKey.substring(0, 10)}...`); |
|
} |
|
|
|
|
|
|
|
|
|
moveToUsed(apiKey) { |
|
if (!this.usedApiKeys.includes(apiKey)) { |
|
this.usedApiKeys.push(apiKey); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
validateAuth(authHeader) { |
|
if (!authHeader) { |
|
return false; |
|
} |
|
|
|
const token = authHeader.replace('Bearer ', ''); |
|
return token === this.authToken; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class ImageProcessor { |
|
|
|
|
|
|
|
static parseDataUrl(dataUrl) { |
|
try { |
|
|
|
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/); |
|
if (!match) { |
|
throw new Error('无效的data URL格式'); |
|
} |
|
|
|
const mimeType = match[1]; |
|
const base64Data = match[2]; |
|
|
|
|
|
const supportedMimeTypes = [ |
|
'image/jpeg', |
|
'image/jpg', |
|
'image/png', |
|
'image/gif', |
|
'image/webp', |
|
'image/bmp', |
|
'image/tiff' |
|
]; |
|
|
|
if (!supportedMimeTypes.includes(mimeType.toLowerCase())) { |
|
throw new Error(`不支持的图片格式: ${mimeType}`); |
|
} |
|
|
|
return { |
|
mimeType, |
|
data: base64Data |
|
}; |
|
} catch (error) { |
|
console.error('解析图片data URL错误:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
static validateBase64(base64String) { |
|
try { |
|
|
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64String)) { |
|
return false; |
|
} |
|
|
|
|
|
return base64String.length % 4 === 0; |
|
} catch (error) { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
static async fetchImageAsBase64(imageUrl) { |
|
try { |
|
const response = await fetch(imageUrl, { |
|
timeout: 30000, |
|
headers: { |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' |
|
} |
|
}); |
|
|
|
if (!response.ok) { |
|
throw new Error(`获取图片失败: HTTP ${response.status}`); |
|
} |
|
|
|
const contentType = response.headers.get('content-type'); |
|
if (!contentType || !contentType.startsWith('image/')) { |
|
throw new Error(`URL返回的不是图片类型: ${contentType}`); |
|
} |
|
|
|
const buffer = await response.buffer(); |
|
const base64Data = buffer.toString('base64'); |
|
|
|
return { |
|
mimeType: contentType, |
|
data: base64Data |
|
}; |
|
} catch (error) { |
|
console.error('下载图片错误:', error); |
|
throw error; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class MessageConverter { |
|
|
|
|
|
|
|
static async convertMessages(openaiMessages) { |
|
const geminiMessages = []; |
|
let currentRole = null; |
|
let currentParts = []; |
|
|
|
for (const message of openaiMessages) { |
|
let role = message.role; |
|
let content = message.content; |
|
|
|
|
|
if (role === 'system') { |
|
role = 'user'; |
|
} |
|
|
|
if (role === 'assistant') { |
|
role = 'model'; |
|
} |
|
|
|
|
|
let parts = []; |
|
|
|
if (typeof content === 'string') { |
|
|
|
parts = [{ text: content }]; |
|
} else if (Array.isArray(content)) { |
|
|
|
parts = await this.convertContentArray(content); |
|
} else { |
|
|
|
parts = [{ text: String(content) }]; |
|
} |
|
|
|
|
|
if (role === currentRole) { |
|
currentParts.push(...parts); |
|
} else { |
|
|
|
if (currentRole !== null && currentParts.length > 0) { |
|
geminiMessages.push({ |
|
role: currentRole, |
|
parts: currentParts |
|
}); |
|
} |
|
|
|
|
|
currentRole = role; |
|
currentParts = [...parts]; |
|
} |
|
} |
|
|
|
|
|
if (currentRole !== null && currentParts.length > 0) { |
|
geminiMessages.push({ |
|
role: currentRole, |
|
parts: currentParts |
|
}); |
|
} |
|
|
|
return geminiMessages; |
|
} |
|
|
|
|
|
|
|
|
|
static async convertContentArray(contentArray) { |
|
const parts = []; |
|
|
|
for (const item of contentArray) { |
|
try { |
|
if (item.type === 'text') { |
|
|
|
parts.push({ text: item.text || '' }); |
|
} else if (item.type === 'image_url') { |
|
|
|
const imagePart = await this.convertImageContent(item); |
|
if (imagePart) { |
|
parts.push(imagePart); |
|
} |
|
} else { |
|
|
|
console.warn(`未知的内容类型: ${item.type},将转为文本处理`); |
|
parts.push({ text: JSON.stringify(item) }); |
|
} |
|
} catch (error) { |
|
console.error('转换内容项错误:', error); |
|
|
|
continue; |
|
} |
|
} |
|
|
|
return parts; |
|
} |
|
|
|
|
|
|
|
|
|
static async convertImageContent(imageItem) { |
|
try { |
|
const imageUrl = imageItem.image_url?.url; |
|
if (!imageUrl) { |
|
throw new Error('缺少图片URL'); |
|
} |
|
|
|
let imageData; |
|
|
|
if (imageUrl.startsWith('data:')) { |
|
|
|
imageData = ImageProcessor.parseDataUrl(imageUrl); |
|
|
|
|
|
if (!ImageProcessor.validateBase64(imageData.data)) { |
|
throw new Error('无效的base64图片数据'); |
|
} |
|
} else if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { |
|
|
|
imageData = await ImageProcessor.fetchImageAsBase64(imageUrl); |
|
} else { |
|
throw new Error(`不支持的图片URL格式: ${imageUrl}`); |
|
} |
|
|
|
|
|
return { |
|
inlineData: { |
|
mimeType: imageData.mimeType, |
|
data: imageData.data |
|
} |
|
}; |
|
} catch (error) { |
|
console.error('转换图片内容错误:', error); |
|
|
|
return { text: `[图片处理失败: ${error.message}]` }; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
static extractParams(openaiRequest) { |
|
return { |
|
model: openaiRequest.model || 'gemini-1.5-flash', |
|
messages: openaiRequest.messages || [], |
|
stream: openaiRequest.stream || false, |
|
temperature: openaiRequest.temperature, |
|
maxTokens: openaiRequest.max_tokens, |
|
topP: openaiRequest.top_p |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class ModelManager { |
|
constructor(config) { |
|
this.config = config; |
|
this.cachedModels = null; |
|
this.cacheExpiry = null; |
|
this.cacheTimeout = 5 * 60 * 1000; |
|
} |
|
|
|
|
|
|
|
|
|
async getModels() { |
|
if (this.cachedModels && this.cacheExpiry && Date.now() < this.cacheExpiry) { |
|
return { success: true, data: this.cachedModels }; |
|
} |
|
|
|
const apiKey = this.config.getFirstAvailableApiKey(); |
|
if (!apiKey) { |
|
return { |
|
success: false, |
|
error: '没有可用的API Key', |
|
status: 503 |
|
}; |
|
} |
|
|
|
try { |
|
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, { |
|
method: 'GET', |
|
headers: { |
|
'Content-Type': 'application/json' |
|
} |
|
}); |
|
|
|
if (!response.ok) { |
|
return { |
|
success: false, |
|
error: `获取模型列表失败: ${response.status}`, |
|
status: response.status |
|
}; |
|
} |
|
|
|
const geminiResponse = await response.json(); |
|
const filteredModels = this.filterModels(geminiResponse.models || []); |
|
|
|
this.cachedModels = filteredModels; |
|
this.cacheExpiry = Date.now() + this.cacheTimeout; |
|
|
|
return { success: true, data: filteredModels }; |
|
|
|
} catch (error) { |
|
console.error('获取模型列表错误:', error); |
|
return { |
|
success: false, |
|
error: '网络请求失败', |
|
status: 500 |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
filterModels(models) { |
|
const allowedPrefixes = [ |
|
'models/gemini-2.5-flash', |
|
'models/gemini-2.0-flash', |
|
'models/gemini-1.5-flash' |
|
]; |
|
|
|
const excludedModels = [ |
|
'models/gemini-1.5-flash-8b' |
|
]; |
|
|
|
const filteredModels = models.filter(model => { |
|
const modelName = model.name; |
|
|
|
if (excludedModels.some(excluded => modelName.startsWith(excluded))) { |
|
return false; |
|
} |
|
if(modelName == "models/gemini-2.5-pro"){ |
|
return true; |
|
} |
|
|
|
return allowedPrefixes.some(prefix => modelName.startsWith(prefix)); |
|
}); |
|
|
|
|
|
const processedModels = filteredModels.map(model => { |
|
const modelId = model.name.replace('models/', ''); |
|
|
|
return { |
|
id: modelId, |
|
object: 'model', |
|
created: Math.floor(Date.now() / 1000), |
|
owned_by: 'google', |
|
permission: [ |
|
{ |
|
id: `modelperm-${modelId}`, |
|
object: 'model_permission', |
|
created: Math.floor(Date.now() / 1000), |
|
allow_create_engine: false, |
|
allow_sampling: true, |
|
allow_logprobs: false, |
|
allow_search_indices: false, |
|
allow_view: true, |
|
allow_fine_tuning: false, |
|
organization: '*', |
|
group: null, |
|
is_blocking: false |
|
} |
|
], |
|
root: modelId, |
|
parent: null |
|
}; |
|
}); |
|
|
|
return { |
|
object: 'list', |
|
data: processedModels |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class GeminiRequestBuilder { |
|
constructor(config) { |
|
this.config = config; |
|
} |
|
|
|
|
|
|
|
|
|
buildRequestBody(geminiMessages, params) { |
|
const requestBody = { |
|
contents: geminiMessages, |
|
safetySettings: this.config.geminiSafety, |
|
generationConfig: {} |
|
}; |
|
|
|
if (params.temperature !== undefined) { |
|
requestBody.generationConfig.temperature = params.temperature; |
|
} |
|
|
|
if (params.maxTokens !== undefined) { |
|
requestBody.generationConfig.maxOutputTokens = params.maxTokens; |
|
} |
|
|
|
if (params.topP !== undefined) { |
|
requestBody.generationConfig.topP = params.topP; |
|
} |
|
|
|
return requestBody; |
|
} |
|
|
|
|
|
|
|
|
|
buildApiUrl(model, apiKey, isStream = false) { |
|
const method = isStream ? 'streamGenerateContent' : 'generateContent'; |
|
return `https://generativelanguage.googleapis.com/v1beta/models/${model}:${method}?key=${apiKey}`; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class ResponseConverter { |
|
|
|
|
|
|
|
static convertStreamChunk(geminiData, requestId, model) { |
|
try { |
|
if (geminiData.candidates && geminiData.candidates[0]) { |
|
const candidate = geminiData.candidates[0]; |
|
if (candidate.content && candidate.content.parts) { |
|
const text = candidate.content.parts[0]?.text || ''; |
|
const openaiChunk = { |
|
id: requestId, |
|
object: 'chat.completion.chunk', |
|
created: Math.floor(Date.now() / 1000), |
|
model: model, |
|
choices: [{ |
|
index: 0, |
|
delta: { content: text }, |
|
finish_reason: candidate.finishReason === 'STOP' ? 'stop' : null |
|
}] |
|
}; |
|
return `data: ${JSON.stringify(openaiChunk)}\n\n`; |
|
} |
|
} |
|
return ''; |
|
} catch (error) { |
|
console.error('转换流响应块错误:', error); |
|
return ''; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
static convertNormalResponse(geminiResponse, requestId, model) { |
|
const openaiResponse = { |
|
id: requestId, |
|
object: 'chat.completion', |
|
created: Math.floor(Date.now() / 1000), |
|
model: model, |
|
choices: [], |
|
usage: { |
|
prompt_tokens: 0, |
|
completion_tokens: 0, |
|
total_tokens: 0 |
|
} |
|
}; |
|
|
|
if (geminiResponse.candidates && geminiResponse.candidates[0]) { |
|
const candidate = geminiResponse.candidates[0]; |
|
if (candidate.content && candidate.content.parts) { |
|
const text = candidate.content.parts.map(part => part.text).join(''); |
|
openaiResponse.choices.push({ |
|
index: 0, |
|
message: { |
|
role: 'assistant', |
|
content: text |
|
}, |
|
finish_reason: candidate.finishReason === 'STOP' ? 'stop' : 'length' |
|
}); |
|
} |
|
} |
|
|
|
|
|
if (geminiResponse.usageMetadata) { |
|
openaiResponse.usage = { |
|
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0, |
|
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0, |
|
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0 |
|
}; |
|
} |
|
|
|
return openaiResponse; |
|
} |
|
|
|
|
|
|
|
|
|
static splitTextToFakeStream(text, requestId, model) { |
|
const chunks = []; |
|
const chunkSize = 3; |
|
|
|
for (let i = 0; i < text.length; i += chunkSize) { |
|
const chunk = text.slice(i, i + chunkSize); |
|
const isLast = i + chunkSize >= text.length; |
|
|
|
const openaiChunk = { |
|
id: requestId, |
|
object: 'chat.completion.chunk', |
|
created: Math.floor(Date.now() / 1000), |
|
model: model, |
|
choices: [{ |
|
index: 0, |
|
delta: { content: chunk }, |
|
finish_reason: isLast ? 'stop' : null |
|
}] |
|
}; |
|
|
|
chunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`); |
|
} |
|
|
|
return chunks; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class GeminiRealtimeStreamParser { |
|
constructor(response, onChunk) { |
|
this.response = response; |
|
this.onChunk = onChunk; |
|
this.buffer = ''; |
|
this.bufferLv = 0; |
|
this.inString = false; |
|
this.escapeNext = false; |
|
this.decoder = new TextDecoder(); |
|
} |
|
|
|
async start() { |
|
try { |
|
for await (const chunk of this.response.body) { |
|
const text = this.decoder.decode(chunk, { stream: true }); |
|
await this.processText(text); |
|
} |
|
|
|
await this.handleRemainingBuffer(); |
|
} catch (error) { |
|
console.error('流式解析错误:', error); |
|
throw error; |
|
} |
|
} |
|
|
|
async processText(text) { |
|
for (const char of text) { |
|
if (this.escapeNext) { |
|
if (this.bufferLv > 1) { |
|
this.buffer += char; |
|
} |
|
this.escapeNext = false; |
|
continue; |
|
} |
|
|
|
if (char === '\\' && this.inString) { |
|
this.escapeNext = true; |
|
if (this.bufferLv > 1) { |
|
this.buffer += char; |
|
} |
|
continue; |
|
} |
|
|
|
if (char === '"') { |
|
this.inString = !this.inString; |
|
} |
|
|
|
if (!this.inString) { |
|
if (char === '{' || char === '[') { |
|
this.bufferLv++; |
|
} else if (char === '}' || char === ']') { |
|
this.bufferLv--; |
|
} |
|
} |
|
|
|
if (this.bufferLv > 1) { |
|
if (this.inString && char === '\n') { |
|
this.buffer += '\\n'; |
|
} else { |
|
this.buffer += char; |
|
} |
|
} else if (this.bufferLv === 1 && this.buffer) { |
|
this.buffer += '}'; |
|
|
|
try { |
|
const bufferJson = JSON.parse(this.buffer); |
|
await this.onChunk(bufferJson); |
|
} catch (parseError) { |
|
console.error('解析Gemini流数据错误:', parseError); |
|
} |
|
|
|
this.buffer = ''; |
|
} |
|
} |
|
} |
|
|
|
async handleRemainingBuffer() { |
|
if (this.buffer.trim() && this.bufferLv >= 1) { |
|
try { |
|
if (!this.buffer.endsWith('}')) { |
|
this.buffer += '}'; |
|
} |
|
const bufferJson = JSON.parse(this.buffer); |
|
await this.onChunk(bufferJson); |
|
} catch (parseError) { |
|
console.error('解析最后的缓冲区数据错误:', parseError); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class AuthMiddleware { |
|
constructor(config) { |
|
this.config = config; |
|
} |
|
|
|
middleware() { |
|
return (req, res, next) => { |
|
|
|
if (req.path === '/health' || req.method === 'OPTIONS') { |
|
return next(); |
|
} |
|
|
|
const authHeader = req.headers.authorization; |
|
|
|
if (!this.config.validateAuth(authHeader)) { |
|
return res.status(401).json({ |
|
error: { |
|
message: 'Invalid authentication credentials', |
|
type: 'invalid_request_error', |
|
code: 'invalid_api_key' |
|
} |
|
}); |
|
} |
|
|
|
next(); |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class ApiProxyService { |
|
constructor() { |
|
this.config = new Config(); |
|
this.requestBuilder = new GeminiRequestBuilder(this.config); |
|
this.modelManager = new ModelManager(this.config); |
|
this.authMiddleware = new AuthMiddleware(this.config); |
|
} |
|
|
|
|
|
|
|
|
|
async handleChatRequest(req, res) { |
|
try { |
|
const requestId = `chatcmpl-${uuidv4()}`; |
|
|
|
const params = MessageConverter.extractParams(req.body); |
|
|
|
|
|
const geminiMessages = await MessageConverter.convertMessages(params.messages); |
|
|
|
if (!geminiMessages || geminiMessages.length === 0) { |
|
return res.status(400).json({ |
|
error: { |
|
message: '无效的消息格式或消息为空', |
|
type: 'invalid_request_error', |
|
code: 'invalid_messages' |
|
} |
|
}); |
|
} |
|
|
|
const requestBody = this.requestBuilder.buildRequestBody(geminiMessages, params); |
|
|
|
if (params.stream) { |
|
const result = await this.handleStreamRequest(requestBody, params, requestId, res); |
|
if (!result.success) { |
|
res.status(result.status || 500).json({ error: result.error }); |
|
} |
|
} else { |
|
const result = await this.executeNormalRequest(requestBody, params, requestId); |
|
if (result.success) { |
|
res.json(result.data); |
|
} else { |
|
res.status(result.status || 500).json({ error: result.error }); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('处理聊天请求错误:', error); |
|
res.status(500).json({ |
|
error: { |
|
message: '内部服务器错误: ' + error.message, |
|
type: 'internal_server_error', |
|
code: 'server_error' |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async handleFakeStreamChatRequest(req, res) { |
|
try { |
|
const requestId = `chatcmpl-${uuidv4()}`; |
|
|
|
const params = MessageConverter.extractParams(req.body); |
|
|
|
|
|
const geminiMessages = await MessageConverter.convertMessages(params.messages); |
|
|
|
if (!geminiMessages || geminiMessages.length === 0) { |
|
return res.status(400).json({ |
|
error: { |
|
message: '无效的消息格式或消息为空', |
|
type: 'invalid_request_error', |
|
code: 'invalid_messages' |
|
} |
|
}); |
|
} |
|
console.log("请求中") |
|
const requestBody = this.requestBuilder.buildRequestBody(geminiMessages, params); |
|
|
|
if (params.stream) { |
|
|
|
const result = await this.handleFakeStreamRequest(requestBody, params, requestId, res); |
|
if (!result.success) { |
|
res.status(result.status || 500).json({ error: result.error }); |
|
} |
|
} else { |
|
|
|
const result = await this.executeNormalRequest(requestBody, params, requestId); |
|
if (result.success) { |
|
res.json(result.data); |
|
} else { |
|
res.status(result.status || 500).json({ error: result.error }); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('处理假流式聊天请求错误:', error); |
|
res.status(500).json({ |
|
error: { |
|
message: '内部服务器错误: ' + error.message, |
|
type: 'internal_server_error', |
|
code: 'server_error' |
|
} |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async handleFakeStreamRequest(requestBody, params, requestId, res) { |
|
try { |
|
|
|
res.writeHead(200, { |
|
'Content-Type': 'text/event-stream', |
|
'Cache-Control': 'no-cache', |
|
'Connection': 'keep-alive', |
|
'Access-Control-Allow-Origin': '*' |
|
}); |
|
|
|
|
|
const pingInterval = setInterval(() => { |
|
try { |
|
res.write(': ping\n\n'); |
|
} catch (error) { |
|
clearInterval(pingInterval); |
|
} |
|
}, 1000); |
|
|
|
|
|
const result = await this.executeNormalRequest(requestBody, params, requestId); |
|
|
|
|
|
clearInterval(pingInterval); |
|
|
|
if (!result.success) { |
|
res.write(`data: ${JSON.stringify({ error: result.error })}\n\n`); |
|
res.write('data: [DONE]\n\n'); |
|
res.end(); |
|
return { success: false }; |
|
} |
|
|
|
|
|
const responseText = result.data.choices[0]?.message?.content || ''; |
|
|
|
if (responseText) { |
|
|
|
const chunks = ResponseConverter.splitTextToFakeStream(responseText, requestId, params.model); |
|
|
|
|
|
for (const chunk of chunks) { |
|
res.write(chunk); |
|
|
|
await new Promise(resolve => setTimeout(resolve, 50)); |
|
} |
|
} |
|
|
|
res.write('data: [DONE]\n\n'); |
|
res.end(); |
|
|
|
return { success: true }; |
|
|
|
} catch (error) { |
|
console.error('处理假流式请求错误:', error); |
|
try { |
|
res.write(`data: ${JSON.stringify({ error: '内部服务器错误: ' + error.message })}\n\n`); |
|
res.write('data: [DONE]\n\n'); |
|
res.end(); |
|
} catch (writeError) { |
|
console.error('写入错误响应失败:', writeError); |
|
} |
|
return { success: false, error: error.message }; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async handleStreamRequest(requestBody, params, requestId, res, retryCount = 0) { |
|
const maxRetries = 3; |
|
|
|
const apiKey = this.config.getApiKey(); |
|
if (!apiKey) { |
|
return { success: false, error: '目前暂无可用的API Key', status: 503 }; |
|
} |
|
|
|
try { |
|
const apiUrl = this.requestBuilder.buildApiUrl(params.model, apiKey, true); |
|
|
|
const response = await fetch(apiUrl, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify(requestBody) |
|
}); |
|
|
|
if (response.status === 403) { |
|
this.config.markKeyAsInvalid(apiKey); |
|
if (retryCount < maxRetries) { |
|
return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1); |
|
} |
|
return { success: false, error: 'API Key 无效', status: 403 }; |
|
} |
|
|
|
if (response.status === 429) { |
|
this.config.moveToUsed(apiKey); |
|
if (retryCount < maxRetries) { |
|
return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1); |
|
} |
|
return { success: false, error: '请求频率过高,请稍后重试', status: 429 }; |
|
} |
|
|
|
if (response.status === 500) { |
|
this.config.moveToUsed(apiKey); |
|
return { success: false, error: '目前服务器繁忙,请稍后重试', status: 500 }; |
|
} |
|
|
|
if (!response.ok) { |
|
const errorText = await response.text(); |
|
console.error(`API请求失败: ${response.status}, 错误信息: ${errorText}`); |
|
return { success: false, error: `API请求失败: ${response.status}`, status: response.status }; |
|
} |
|
|
|
res.writeHead(200, { |
|
'Content-Type': 'text/event-stream', |
|
'Cache-Control': 'no-cache', |
|
'Connection': 'keep-alive', |
|
'Access-Control-Allow-Origin': '*' |
|
}); |
|
|
|
const parser = new GeminiRealtimeStreamParser(response, async (geminiData) => { |
|
const convertedChunk = ResponseConverter.convertStreamChunk(geminiData, requestId, params.model); |
|
if (convertedChunk) { |
|
res.write(convertedChunk); |
|
} |
|
}); |
|
|
|
await parser.start(); |
|
res.write('data: [DONE]\n\n'); |
|
res.end(); |
|
|
|
return { success: true }; |
|
|
|
} catch (error) { |
|
console.error('执行流式请求错误:', error); |
|
this.config.moveToUsed(apiKey); |
|
|
|
if (retryCount < maxRetries) { |
|
return await this.handleStreamRequest(requestBody, params, requestId, res, retryCount + 1); |
|
} |
|
|
|
return { success: false, error: '网络请求失败: ' + error.message, status: 500 }; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async executeNormalRequest(requestBody, params, requestId, retryCount = 0) { |
|
const maxRetries = 3; |
|
|
|
const apiKey = this.config.getApiKey(); |
|
if (!apiKey) { |
|
return { success: false, error: '目前暂无可用的API Key', status: 503 }; |
|
} |
|
|
|
try { |
|
const apiUrl = this.requestBuilder.buildApiUrl(params.model, apiKey, false); |
|
|
|
const response = await fetch(apiUrl, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify(requestBody) |
|
}); |
|
|
|
if (response.status === 403) { |
|
this.config.markKeyAsInvalid(apiKey); |
|
if (retryCount < maxRetries) { |
|
return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1); |
|
} |
|
return { success: false, error: 'API Key 无效', status: 403 }; |
|
} |
|
|
|
if (response.status === 429) { |
|
this.config.moveToUsed(apiKey); |
|
if (retryCount < maxRetries) { |
|
return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1); |
|
} |
|
return { success: false, error: '请求频率过高,请稍后重试', status: 429 }; |
|
} |
|
|
|
if (response.status === 500) { |
|
this.config.moveToUsed(apiKey); |
|
return { success: false, error: '目前服务器繁忙,请稍后重试', status: 500 }; |
|
} |
|
|
|
if (!response.ok) { |
|
const errorText = await response.text(); |
|
console.error(`API请求失败: ${response.status}, 错误信息: ${errorText}`); |
|
return { success: false, error: `API请求失败: ${response.status}`, status: response.status }; |
|
} |
|
|
|
const geminiResponse = await response.json(); |
|
const openaiResponse = ResponseConverter.convertNormalResponse(geminiResponse, requestId, params.model); |
|
return { success: true, data: openaiResponse }; |
|
|
|
} catch (error) { |
|
console.error('执行非流式请求错误:', error); |
|
this.config.moveToUsed(apiKey); |
|
|
|
if (retryCount < maxRetries) { |
|
return await this.executeNormalRequest(requestBody, params, requestId, retryCount + 1); |
|
} |
|
|
|
return { success: false, error: '网络请求失败: ' + error.message, status: 500 }; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
async handleModelsRequest(req, res) { |
|
try { |
|
const result = await this.modelManager.getModels(); |
|
|
|
if (result.success) { |
|
res.json(result.data); |
|
} else { |
|
res.status(result.status || 500).json({ error: result.error }); |
|
} |
|
} catch (error) { |
|
console.error('处理模型列表请求错误:', error); |
|
res.status(500).json({ error: '内部服务器错误' }); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
class Server { |
|
constructor() { |
|
this.app = express(); |
|
this.apiProxy = new ApiProxyService(); |
|
this.setupMiddleware(); |
|
this.setupRoutes(); |
|
} |
|
|
|
setupMiddleware() { |
|
|
|
this.app.use(cors({ |
|
origin: '*', |
|
credentials: true, |
|
optionsSuccessStatus: 200 |
|
})); |
|
|
|
|
|
this.app.use(express.json({ limit: '50mb' })); |
|
this.app.use(express.urlencoded({ limit: '50mb', extended: true })); |
|
|
|
|
|
this.app.use(this.apiProxy.authMiddleware.middleware()); |
|
|
|
|
|
this.app.use((req, res, next) => { |
|
const start = Date.now(); |
|
res.on('finish', () => { |
|
const duration = Date.now() - start; |
|
console.log(`${req.method} ${req.path} - ${res.statusCode} [${duration}ms]`); |
|
}); |
|
next(); |
|
}); |
|
} |
|
|
|
setupRoutes() { |
|
|
|
this.app.post('/v1/chat/completions', (req, res) => { |
|
this.apiProxy.handleChatRequest(req, res); |
|
}); |
|
|
|
|
|
this.app.post('/fakestream/v1/chat/completions', (req, res) => { |
|
this.apiProxy.handleFakeStreamChatRequest(req, res); |
|
}); |
|
|
|
|
|
this.app.get('/v1/models', (req, res) => { |
|
this.apiProxy.handleModelsRequest(req, res); |
|
}); |
|
|
|
|
|
this.app.get('/fakestream/v1/models', (req, res) => { |
|
this.apiProxy.handleModelsRequest(req, res); |
|
}); |
|
|
|
|
|
this.app.get('/health', (req, res) => { |
|
res.json({ |
|
status: 'healthy', |
|
timestamp: new Date().toISOString(), |
|
availableKeys: this.apiProxy.config.apiKeys.length, |
|
usedKeys: this.apiProxy.config.usedApiKeys.length, |
|
invalidKeys: this.apiProxy.config.invalidApiKeys.length, |
|
version: '2.0.0', |
|
features: ['text', 'vision', 'stream', 'fake_stream', 'load_balancing'] |
|
}); |
|
}); |
|
|
|
|
|
this.app.use('*', (req, res) => { |
|
res.status(404).json({ |
|
error: { |
|
message: 'Not Found', |
|
type: 'invalid_request_error', |
|
code: 'not_found' |
|
} |
|
}); |
|
}); |
|
|
|
|
|
this.app.use((err, req, res, next) => { |
|
console.error('服务器错误:', err); |
|
res.status(500).json({ |
|
error: { |
|
message: '内部服务器错误', |
|
type: 'internal_server_error', |
|
code: 'server_error' |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
start(port = 3000) { |
|
this.app.listen(port, () => { |
|
console.log(`🚀 OpenAI to Gemini Proxy Server (Enhanced) 启动在端口 ${port}`); |
|
console.log(`📍 聊天API: http://localhost:${port}/v1/chat/completions`); |
|
console.log(`📍 假流式聊天API: http://localhost:${port}/fakestream/v1/chat/completions`); |
|
console.log(`📋 模型列表: http://localhost:${port}/v1/models`); |
|
console.log(`📋 假流式模型列表: http://localhost:${port}/fakestream/v1/models`); |
|
console.log(`🔍 健康检查: http://localhost:${port}/health`); |
|
}); |
|
} |
|
} |
|
|
|
|
|
const server = new Server(); |
|
const port = process.env.PORT || 3000; |
|
server.start(port); |
|
|