Merge branch 'dev' into main

This commit is contained in:
Wesley Liddick
2025-09-08 16:14:54 +08:00
committed by GitHub
23 changed files with 1398 additions and 65 deletions

View File

@@ -2059,7 +2059,9 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
groupId,
groupIds,
autoStopOnWarning,
useUnifiedUserAgent
useUnifiedUserAgent,
useUnifiedClientId,
unifiedClientId
} = req.body
if (!name) {
@@ -2100,7 +2102,9 @@ router.post('/claude-accounts', authenticateAdmin, async (req, res) => {
platform,
priority: priority || 50, // 默认优先级为50
autoStopOnWarning: autoStopOnWarning === true, // 默认为false
useUnifiedUserAgent: useUnifiedUserAgent === true // 默认为false
useUnifiedUserAgent: useUnifiedUserAgent === true, // 默认为false
useUnifiedClientId: useUnifiedClientId === true, // 默认为false
unifiedClientId: unifiedClientId || '' // 统一的客户端标识
})
// 如果是分组类型,将账户添加到分组
@@ -2703,6 +2707,23 @@ router.post(
}
)
// 重置Claude Console账户状态清除所有异常状态
router.post(
'/claude-console-accounts/:accountId/reset-status',
authenticateAdmin,
async (req, res) => {
try {
const { accountId } = req.params
const result = await claudeConsoleAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for Claude Console account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset Claude Console account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
}
)
// 手动重置所有Claude Console账户的每日使用量
router.post('/claude-console-accounts/reset-all-usage', authenticateAdmin, async (req, res) => {
try {
@@ -5419,6 +5440,7 @@ router.get('/oem-settings', async (req, res) => {
siteName: 'Claude Relay Service',
siteIcon: '',
siteIconData: '', // Base64编码的图标数据
showAdminButton: true, // 是否显示管理后台按钮
updatedAt: new Date().toISOString()
}
@@ -5448,7 +5470,7 @@ router.get('/oem-settings', async (req, res) => {
// 更新OEM设置
router.put('/oem-settings', authenticateAdmin, async (req, res) => {
try {
const { siteName, siteIcon, siteIconData } = req.body
const { siteName, siteIcon, siteIconData, showAdminButton } = req.body
// 验证输入
if (!siteName || typeof siteName !== 'string' || siteName.trim().length === 0) {
@@ -5479,6 +5501,7 @@ router.put('/oem-settings', authenticateAdmin, async (req, res) => {
siteName: siteName.trim(),
siteIcon: (siteIcon || '').trim(),
siteIconData: (siteIconData || '').trim(), // Base64数据
showAdminButton: showAdminButton !== false, // 默认为true
updatedAt: new Date().toISOString()
}
@@ -6190,6 +6213,21 @@ router.put('/openai-accounts/:id/toggle', authenticateAdmin, async (req, res) =>
}
})
// 重置 OpenAI 账户状态(清除所有异常状态)
router.post('/openai-accounts/:accountId/reset-status', authenticateAdmin, async (req, res) => {
try {
const { accountId } = req.params
const result = await openaiAccountService.resetAccountStatus(accountId)
logger.success(`✅ Admin reset status for OpenAI account: ${accountId}`)
return res.json({ success: true, data: result })
} catch (error) {
logger.error('❌ Failed to reset OpenAI account status:', error)
return res.status(500).json({ error: 'Failed to reset status', message: error.message })
}
})
// 切换 OpenAI 账户调度状态
router.put(
'/openai-accounts/:accountId/toggle-schedulable',

View File

@@ -31,8 +31,8 @@ router.post('/api/get-key-id', async (req, res) => {
})
}
// 验证API Key
const validation = await apiKeyService.validateApiKey(apiKey)
// 验证API Key(使用不触发激活的验证方法)
const validation = await apiKeyService.validateApiKeyForStats(apiKey)
if (!validation.valid) {
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'
@@ -146,6 +146,11 @@ router.post('/api/user-stats', async (req, res) => {
enableClientRestriction: keyData.enableClientRestriction === 'true',
allowedClients,
permissions: keyData.permissions || 'all',
// 添加激活相关字段
expirationMode: keyData.expirationMode || 'fixed',
isActivated: keyData.isActivated === 'true',
activationDays: parseInt(keyData.activationDays || 0),
activatedAt: keyData.activatedAt || null,
usage // 使用完整的 usage 数据,而不是只有 total
}
} else if (apiKey) {
@@ -158,8 +163,8 @@ router.post('/api/user-stats', async (req, res) => {
})
}
// 验证API Key重用现有的验证逻辑
const validation = await apiKeyService.validateApiKey(apiKey)
// 验证API Key使用不触发激活的验证方法
const validation = await apiKeyService.validateApiKeyForStats(apiKey)
if (!validation.valid) {
const clientIP = req.ip || req.connection?.remoteAddress || 'unknown'
@@ -335,6 +340,11 @@ router.post('/api/user-stats', async (req, res) => {
isActive: true, // 如果能通过validateApiKey验证说明一定是激活的
createdAt: keyData.createdAt,
expiresAt: keyData.expiresAt,
// 添加激活相关字段
expirationMode: keyData.expirationMode || 'fixed',
isActivated: keyData.isActivated === 'true',
activationDays: parseInt(keyData.activationDays || 0),
activatedAt: keyData.activatedAt || null,
permissions: fullKeyData.permissions,
// 使用统计(使用验证结果中的完整数据)

View File

@@ -87,7 +87,8 @@ async function getOpenAIAuthToken(apiKeyData, sessionId = null, requestedModel =
}
}
router.post('/responses', authenticateApiKey, async (req, res) => {
// 主处理函数,供两个路由共享
const handleResponses = async (req, res) => {
let upstream = null
try {
// 从中间件获取 API Key 数据
@@ -205,6 +206,96 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
axiosConfig
)
}
// 处理 429 限流错误
if (upstream.status === 429) {
logger.warn(`🚫 Rate limit detected for OpenAI account ${accountId} (Codex API)`)
// 解析响应体中的限流信息
let resetsInSeconds = null
let errorData = null
try {
// 对于429错误无论是否是流式请求响应都会是完整的JSON错误对象
if (isStream && upstream.data) {
// 流式响应需要先收集数据
const chunks = []
await new Promise((resolve, reject) => {
upstream.data.on('data', (chunk) => chunks.push(chunk))
upstream.data.on('end', resolve)
upstream.data.on('error', reject)
// 设置超时防止无限等待
setTimeout(resolve, 5000)
})
const fullResponse = Buffer.concat(chunks).toString()
try {
errorData = JSON.parse(fullResponse)
} catch (e) {
logger.error('Failed to parse 429 error response:', e)
logger.debug('Raw response:', fullResponse)
}
} else {
// 非流式响应直接使用data
errorData = upstream.data
}
// 提取重置时间
if (errorData && errorData.error && errorData.error.resets_in_seconds) {
resetsInSeconds = errorData.error.resets_in_seconds
logger.info(
`🕐 Codex rate limit will reset in ${resetsInSeconds} seconds (${Math.ceil(resetsInSeconds / 60)} minutes / ${Math.ceil(resetsInSeconds / 3600)} hours)`
)
} else {
logger.warn(
'⚠️ Could not extract resets_in_seconds from 429 response, using default 60 minutes'
)
}
} catch (e) {
logger.error('⚠️ Failed to parse rate limit error:', e)
}
// 标记账户为限流状态
await unifiedOpenAIScheduler.markAccountRateLimited(
accountId,
'openai',
sessionId ? crypto.createHash('sha256').update(sessionId).digest('hex') : null,
resetsInSeconds
)
// 返回错误响应给客户端
const errorResponse = errorData || {
error: {
type: 'usage_limit_reached',
message: 'The usage limit has been reached',
resets_in_seconds: resetsInSeconds
}
}
if (isStream) {
// 流式响应也需要设置正确的状态码
res.status(429)
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.write(`data: ${JSON.stringify(errorResponse)}\n\n`)
res.end()
} else {
res.status(429).json(errorResponse)
}
return
} else if (upstream.status === 200 || upstream.status === 201) {
// 请求成功,检查并移除限流状态
const isRateLimited = await unifiedOpenAIScheduler.isAccountRateLimited(accountId)
if (isRateLimited) {
logger.info(
`✅ Removing rate limit for OpenAI account ${accountId} after successful request`
)
await unifiedOpenAIScheduler.removeAccountRateLimit(accountId, 'openai')
}
}
res.status(upstream.status)
if (isStream) {
@@ -239,6 +330,8 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
let usageData = null
let actualModel = null
let usageReported = false
let rateLimitDetected = false
let rateLimitResetsInSeconds = null
if (!isStream) {
// 非流式响应处理
@@ -317,6 +410,17 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
logger.debug('📊 Captured OpenAI usage data:', usageData)
}
}
// 检查是否有限流错误
if (eventData.error && eventData.error.type === 'usage_limit_reached') {
rateLimitDetected = true
if (eventData.error.resets_in_seconds) {
rateLimitResetsInSeconds = eventData.error.resets_in_seconds
logger.warn(
`🚫 Rate limit detected in stream, resets in ${rateLimitResetsInSeconds} seconds`
)
}
}
} catch (e) {
// 忽略解析错误
}
@@ -388,6 +492,26 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
}
}
// 如果在流式响应中检测到限流
if (rateLimitDetected) {
logger.warn(`🚫 Processing rate limit for OpenAI account ${accountId} from stream`)
await unifiedOpenAIScheduler.markAccountRateLimited(
accountId,
'openai',
sessionId ? crypto.createHash('sha256').update(sessionId).digest('hex') : null,
rateLimitResetsInSeconds
)
} else if (upstream.status === 200) {
// 流式请求成功,检查并移除限流状态
const isRateLimited = await unifiedOpenAIScheduler.isAccountRateLimited(accountId)
if (isRateLimited) {
logger.info(
`✅ Removing rate limit for OpenAI account ${accountId} after successful stream`
)
await unifiedOpenAIScheduler.removeAccountRateLimit(accountId, 'openai')
}
}
res.end()
})
@@ -419,7 +543,11 @@ router.post('/responses', authenticateApiKey, async (req, res) => {
res.status(status).json({ error: { message } })
}
}
})
}
// 注册两个路由路径,都使用相同的处理函数
router.post('/responses', authenticateApiKey, handleResponses)
router.post('/v1/responses', authenticateApiKey, handleResponses)
// 使用情况统计端点
router.get('/usage', authenticateApiKey, async (req, res) => {