diff --git a/src/middleware/auth.js b/src/middleware/auth.js index aeaf8112..241ae568 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -3,7 +3,7 @@ const userService = require('../services/userService') const logger = require('../utils/logger') const redis = require('../models/redis') // const { RateLimiterRedis } = require('rate-limiter-flexible') // 暂时未使用 -const config = require('../../config/config') +const ClientValidator = require('../validators/clientValidator') // 🔑 API Key验证中间件(优化版) const authenticateApiKey = async (req, res, next) => { @@ -47,65 +47,34 @@ const authenticateApiKey = async (req, res, next) => { }) } - // 🔒 检查客户端限制 + // 🔒 检查客户端限制(使用新的验证器) if ( validation.keyData.enableClientRestriction && validation.keyData.allowedClients?.length > 0 ) { - const userAgent = req.headers['user-agent'] || '' - const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' - - // 记录客户端限制检查开始 - logger.api( - `🔍 Checking client restriction for key: ${validation.keyData.id} (${validation.keyData.name})` + // 使用新的 ClientValidator 进行验证 + const validationResult = ClientValidator.validateRequest( + validation.keyData.allowedClients, + req ) - logger.api(` User-Agent: "${userAgent}"`) - logger.api(` Allowed clients: ${validation.keyData.allowedClients.join(', ')}`) - let clientAllowed = false - let matchedClient = null - - // 获取预定义客户端列表,如果配置不存在则使用默认值 - const predefinedClients = config.clientRestrictions?.predefinedClients || [] - const allowCustomClients = config.clientRestrictions?.allowCustomClients || false - - // 遍历允许的客户端列表 - for (const allowedClientId of validation.keyData.allowedClients) { - // 在预定义客户端列表中查找 - const predefinedClient = predefinedClients.find((client) => client.id === allowedClientId) - - if (predefinedClient) { - // 使用预定义的正则表达式匹配 User-Agent - if ( - predefinedClient.userAgentPattern && - predefinedClient.userAgentPattern.test(userAgent) - ) { - clientAllowed = true - matchedClient = predefinedClient.name - break - } - } else if (allowCustomClients) { - // 如果允许自定义客户端,这里可以添加自定义客户端的验证逻辑 - // 目前暂时跳过自定义客户端 - continue - } - } - - if (!clientAllowed) { + if (!validationResult.allowed) { + const clientIP = req.ip || req.connection?.remoteAddress || 'unknown' logger.security( - `🚫 Client restriction failed for key: ${validation.keyData.id} (${validation.keyData.name}) from ${clientIP}, User-Agent: ${userAgent}` + `🚫 Client restriction failed for key: ${validation.keyData.id} (${validation.keyData.name}) from ${clientIP}` ) return res.status(403).json({ error: 'Client not allowed', message: 'Your client is not authorized to use this API key', - allowedClients: validation.keyData.allowedClients + allowedClients: validation.keyData.allowedClients, + userAgent: validationResult.userAgent }) } + // 验证通过 logger.api( - `✅ Client validated: ${matchedClient} for key: ${validation.keyData.id} (${validation.keyData.name})` + `✅ Client validated: ${validationResult.clientName} (${validationResult.matchedClient}) for key: ${validation.keyData.id} (${validation.keyData.name})` ) - logger.api(` Matched client: ${matchedClient} with User-Agent: "${userAgent}"`) } // 检查并发限制 diff --git a/src/routes/admin.js b/src/routes/admin.js index 07ec8134..c21727eb 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -467,29 +467,22 @@ router.get('/api-keys', authenticateAdmin, async (req, res) => { } }) -// 获取支持的客户端列表 +// 获取支持的客户端列表(使用新的验证器) router.get('/supported-clients', authenticateAdmin, async (req, res) => { try { - // 检查配置是否存在,如果不存在则使用默认值 - const predefinedClients = config.clientRestrictions?.predefinedClients || [ - { - id: 'claude_code', - name: 'ClaudeCode', - description: 'Official Claude Code CLI' - }, - { - id: 'gemini_cli', - name: 'Gemini-CLI', - description: 'Gemini Command Line Interface' - } - ] + // 使用新的 ClientValidator 获取所有可用客户端 + const ClientValidator = require('../validators/clientValidator') + const availableClients = ClientValidator.getAvailableClients() - const clients = predefinedClients.map((client) => ({ + // 格式化返回数据 + const clients = availableClients.map((client) => ({ id: client.id, name: client.name, - description: client.description + description: client.description, + icon: client.icon })) + logger.info(`📱 Returning ${clients.length} supported clients`) return res.json({ success: true, data: clients }) } catch (error) { logger.error('❌ Failed to get supported clients:', error) diff --git a/src/routes/openaiRoutes.js b/src/routes/openaiRoutes.js index 0d664cd7..eed05275 100644 --- a/src/routes/openaiRoutes.js +++ b/src/routes/openaiRoutes.js @@ -33,7 +33,9 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = ) if (!result || !result.accountId) { - throw new Error('No available OpenAI account found') + const error = new Error('No available OpenAI account found') + error.statusCode = 402 // Payment Required - 资源耗尽 + throw error } // 根据账户类型获取账户详情 @@ -45,7 +47,9 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = // 处理 OpenAI-Responses 账户 account = await openaiResponsesAccountService.getAccount(result.accountId) if (!account || !account.apiKey) { - throw new Error(`OpenAI-Responses account ${result.accountId} has no valid apiKey`) + const error = new Error(`OpenAI-Responses account ${result.accountId} has no valid apiKey`) + error.statusCode = 403 // Forbidden - 账户配置错误 + throw error } // OpenAI-Responses 账户不需要 accessToken,直接返回账户信息 @@ -65,7 +69,9 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = // 处理普通 OpenAI 账户 account = await openaiAccountService.getAccount(result.accountId) if (!account || !account.accessToken) { - throw new Error(`OpenAI account ${result.accountId} has no valid accessToken`) + const error = new Error(`OpenAI account ${result.accountId} has no valid accessToken`) + error.statusCode = 403 // Forbidden - 账户配置错误 + throw error } // 检查 token 是否过期并自动刷新(双重保护) @@ -79,19 +85,25 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel = logger.info(`✅ Token refreshed successfully in route handler`) } catch (refreshError) { logger.error(`Failed to refresh token for ${account.name}:`, refreshError) - throw new Error(`Token expired and refresh failed: ${refreshError.message}`) + const error = new Error(`Token expired and refresh failed: ${refreshError.message}`) + error.statusCode = 403 // Forbidden - 认证失败 + throw error } } else { - throw new Error( + const error = new Error( `Token expired and no refresh token available for account ${account.name}` ) + error.statusCode = 403 // Forbidden - 认证失败 + throw error } } // 解密 accessToken(account.accessToken 是加密的) accessToken = openaiAccountService.decrypt(account.accessToken) if (!accessToken) { - throw new Error('Failed to decrypt OpenAI accessToken') + const error = new Error('Failed to decrypt OpenAI accessToken') + error.statusCode = 403 // Forbidden - 配置/权限错误 + throw error } // 解析代理配置 @@ -580,7 +592,8 @@ const handleResponses = async (req, res) => { req.on('aborted', cleanup) } catch (error) { logger.error('Proxy to ChatGPT codex/responses failed:', error) - const status = error.response?.status || 500 + // 优先使用主动设置的 statusCode,然后是上游响应的状态码,最后默认 500 + const status = error.statusCode || error.response?.status || 500 const message = error.response?.data || error.message || 'Internal server error' if (!res.headersSent) { res.status(status).json({ error: { message } }) diff --git a/src/services/unifiedOpenAIScheduler.js b/src/services/unifiedOpenAIScheduler.js index efe08ac9..b3516dc3 100644 --- a/src/services/unifiedOpenAIScheduler.js +++ b/src/services/unifiedOpenAIScheduler.js @@ -79,7 +79,9 @@ class UnifiedOpenAIScheduler { if (isRateLimited) { const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited` logger.warn(`⚠️ ${errorMsg}`) - throw new Error(errorMsg) + const error = new Error(errorMsg) + error.statusCode = 429 // Too Many Requests - 限流 + throw error } } else if ( accountType === 'openai-responses' && @@ -92,7 +94,9 @@ class UnifiedOpenAIScheduler { if (!isRateLimitCleared) { const errorMsg = `Dedicated account ${boundAccount.name} is currently rate limited` logger.warn(`⚠️ ${errorMsg}`) - throw new Error(errorMsg) + const error = new Error(errorMsg) + error.statusCode = 429 // Too Many Requests - 限流 + throw error } } @@ -108,7 +112,9 @@ class UnifiedOpenAIScheduler { if (!modelSupported) { const errorMsg = `Dedicated account ${boundAccount.name} does not support model ${requestedModel}` logger.warn(`⚠️ ${errorMsg}`) - throw new Error(errorMsg) + const error = new Error(errorMsg) + error.statusCode = 400 // Bad Request - 请求参数错误 + throw error } } @@ -133,7 +139,9 @@ class UnifiedOpenAIScheduler { ? `Dedicated account ${boundAccount.name} is not available (inactive or error status)` : `Dedicated account ${apiKeyData.openaiAccountId} not found` logger.warn(`⚠️ ${errorMsg}`) - throw new Error(errorMsg) + const error = new Error(errorMsg) + error.statusCode = boundAccount ? 403 : 404 // Forbidden 或 Not Found + throw error } } @@ -170,11 +178,15 @@ class UnifiedOpenAIScheduler { if (availableAccounts.length === 0) { // 提供更详细的错误信息 if (requestedModel) { - throw new Error( + const error = new Error( `No available OpenAI accounts support the requested model: ${requestedModel}` ) + error.statusCode = 400 // Bad Request - 模型不支持 + throw error } else { - throw new Error('No available OpenAI accounts') + const error = new Error('No available OpenAI accounts') + error.statusCode = 402 // Payment Required - 资源耗尽 + throw error } } @@ -562,11 +574,15 @@ class UnifiedOpenAIScheduler { // 获取分组信息 const group = await accountGroupService.getGroup(groupId) if (!group) { - throw new Error(`Group ${groupId} not found`) + const error = new Error(`Group ${groupId} not found`) + error.statusCode = 404 // Not Found - 资源不存在 + throw error } if (group.platform !== 'openai') { - throw new Error(`Group ${group.name} is not an OpenAI group`) + const error = new Error(`Group ${group.name} is not an OpenAI group`) + error.statusCode = 400 // Bad Request - 请求参数错误 + throw error } logger.info(`👥 Selecting account from OpenAI group: ${group.name}`) @@ -601,7 +617,9 @@ class UnifiedOpenAIScheduler { // 获取分组成员 const memberIds = await accountGroupService.getGroupMembers(groupId) if (memberIds.length === 0) { - throw new Error(`Group ${group.name} has no members`) + const error = new Error(`Group ${group.name} has no members`) + error.statusCode = 402 // Payment Required - 资源耗尽 + throw error } // 获取可用的分组成员账户 @@ -653,7 +671,9 @@ class UnifiedOpenAIScheduler { } if (availableAccounts.length === 0) { - throw new Error(`No available accounts in group ${group.name}`) + const error = new Error(`No available accounts in group ${group.name}`) + error.statusCode = 402 // Payment Required - 资源耗尽 + throw error } // 按最后使用时间排序(最久未使用的优先,与 Claude 保持一致)