mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 优化codex错误抛出 增强客户端限制条件
This commit is contained in:
@@ -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}"`)
|
||||
}
|
||||
|
||||
// 检查并发限制
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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 保持一致)
|
||||
|
||||
Reference in New Issue
Block a user