From 8e89311dacb98710646e168e832473e90c2ea9c9 Mon Sep 17 00:00:00 2001 From: shaw Date: Thu, 31 Jul 2025 16:29:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8API=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=A4=B4=E4=B8=AD=E7=9A=84=E5=87=86=E7=A1=AE=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3=E4=BF=AE=E6=AD=A3=E4=BC=9A=E8=AF=9D=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E5=92=8C=E9=99=90=E6=B5=81=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从429响应中提取 anthropic-ratelimit-unified-reset 响应头 - 使用准确的重置时间戳设置限流结束时间和会话窗口 - 会话窗口开始时间 = 重置时间戳 - 5小时 - 兼容旧逻辑:无响应头时使用预估的会话窗口时间 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/services/claudeAccountService.js | 46 ++++++++++++++++++-------- src/services/claudeRelayService.js | 45 ++++++++++++++++++------- src/services/unifiedClaudeScheduler.js | 4 +-- 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 6fa25947..5d849058 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -732,32 +732,50 @@ class ClaudeAccountService { } // 🚫 标记账号为限流状态 - async markAccountRateLimited(accountId, sessionHash = null) { + async markAccountRateLimited(accountId, sessionHash = null, rateLimitResetTimestamp = null) { try { const accountData = await redis.getClaudeAccount(accountId); if (!accountData || Object.keys(accountData).length === 0) { throw new Error('Account not found'); } - // 获取或创建会话窗口 - const updatedAccountData = await this.updateSessionWindow(accountId, accountData); - // 设置限流状态和时间 + const updatedAccountData = { ...accountData }; updatedAccountData.rateLimitedAt = new Date().toISOString(); updatedAccountData.rateLimitStatus = 'limited'; - // 限流结束时间 = 会话窗口结束时间 - if (updatedAccountData.sessionWindowEnd) { - updatedAccountData.rateLimitEndAt = updatedAccountData.sessionWindowEnd; - const windowEnd = new Date(updatedAccountData.sessionWindowEnd); + // 如果提供了准确的限流重置时间戳(来自API响应头) + if (rateLimitResetTimestamp) { + // 将Unix时间戳(秒)转换为毫秒并创建Date对象 + const resetTime = new Date(rateLimitResetTimestamp * 1000); + updatedAccountData.rateLimitEndAt = resetTime.toISOString(); + + // 计算当前会话窗口的开始时间(重置时间减去5小时) + const windowStartTime = new Date(resetTime.getTime() - (5 * 60 * 60 * 1000)); + updatedAccountData.sessionWindowStart = windowStartTime.toISOString(); + updatedAccountData.sessionWindowEnd = resetTime.toISOString(); + const now = new Date(); - const minutesUntilEnd = Math.ceil((windowEnd - now) / (1000 * 60)); - logger.warn(`🚫 Account marked as rate limited until session window ends: ${accountData.name} (${accountId}) - ${minutesUntilEnd} minutes remaining`); + const minutesUntilEnd = Math.ceil((resetTime - now) / (1000 * 60)); + logger.warn(`🚫 Account marked as rate limited with accurate reset time: ${accountData.name} (${accountId}) - ${minutesUntilEnd} minutes remaining until ${resetTime.toISOString()}`); } else { - // 如果没有会话窗口,使用默认1小时(兼容旧逻辑) - const oneHourLater = new Date(Date.now() + 60 * 60 * 1000); - updatedAccountData.rateLimitEndAt = oneHourLater.toISOString(); - logger.warn(`🚫 Account marked as rate limited (1 hour default): ${accountData.name} (${accountId})`); + // 获取或创建会话窗口(预估方式) + const windowData = await this.updateSessionWindow(accountId, updatedAccountData); + Object.assign(updatedAccountData, windowData); + + // 限流结束时间 = 会话窗口结束时间 + if (updatedAccountData.sessionWindowEnd) { + updatedAccountData.rateLimitEndAt = updatedAccountData.sessionWindowEnd; + const windowEnd = new Date(updatedAccountData.sessionWindowEnd); + const now = new Date(); + const minutesUntilEnd = Math.ceil((windowEnd - now) / (1000 * 60)); + logger.warn(`🚫 Account marked as rate limited until estimated session window ends: ${accountData.name} (${accountId}) - ${minutesUntilEnd} minutes remaining`); + } else { + // 如果没有会话窗口,使用默认1小时(兼容旧逻辑) + const oneHourLater = new Date(Date.now() + 60 * 60 * 1000); + updatedAccountData.rateLimitEndAt = oneHourLater.toISOString(); + logger.warn(`🚫 Account marked as rate limited (1 hour default): ${accountData.name} (${accountId})`); + } } await redis.setClaudeAccount(accountId, updatedAccountData); diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index f246063d..0a8ac6b9 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -142,23 +142,37 @@ class ClaudeRelayService { // 检查响应是否为限流错误 if (response.statusCode !== 200 && response.statusCode !== 201) { let isRateLimited = false; - try { - const responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; - if (responseBody && responseBody.error && responseBody.error.message && - responseBody.error.message.toLowerCase().includes('exceed your account\'s rate limit')) { - isRateLimited = true; + let rateLimitResetTimestamp = null; + + // 检查是否为429状态码 + if (response.statusCode === 429) { + isRateLimited = true; + + // 提取限流重置时间戳 + if (response.headers && response.headers['anthropic-ratelimit-unified-reset']) { + rateLimitResetTimestamp = parseInt(response.headers['anthropic-ratelimit-unified-reset']); + logger.info(`🕐 Extracted rate limit reset timestamp: ${rateLimitResetTimestamp} (${new Date(rateLimitResetTimestamp * 1000).toISOString()})`); } - } catch (e) { - // 如果解析失败,检查原始字符串 - if (response.body && response.body.toLowerCase().includes('exceed your account\'s rate limit')) { - isRateLimited = true; + } else { + // 检查响应体中的错误信息 + try { + const responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; + if (responseBody && responseBody.error && responseBody.error.message && + responseBody.error.message.toLowerCase().includes('exceed your account\'s rate limit')) { + isRateLimited = true; + } + } catch (e) { + // 如果解析失败,检查原始字符串 + if (response.body && response.body.toLowerCase().includes('exceed your account\'s rate limit')) { + isRateLimited = true; + } } } if (isRateLimited) { logger.warn(`🚫 Rate limit detected for account ${accountId}, status: ${response.statusCode}`); - // 标记账号为限流状态并删除粘性会话映射 - await claudeAccountService.markAccountRateLimited(accountId, sessionHash); + // 标记账号为限流状态并删除粘性会话映射,传递准确的重置时间戳 + await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp); } } else if (response.statusCode === 200 || response.statusCode === 201) { // 如果请求成功,检查并移除限流状态 @@ -832,8 +846,15 @@ class ClaudeRelayService { // 处理限流状态 if (rateLimitDetected || res.statusCode === 429) { + // 提取限流重置时间戳 + let rateLimitResetTimestamp = null; + if (res.headers && res.headers['anthropic-ratelimit-unified-reset']) { + rateLimitResetTimestamp = parseInt(res.headers['anthropic-ratelimit-unified-reset']); + logger.info(`🕐 Extracted rate limit reset timestamp from stream: ${rateLimitResetTimestamp} (${new Date(rateLimitResetTimestamp * 1000).toISOString()})`); + } + // 标记账号为限流状态并删除粘性会话映射 - await claudeAccountService.markAccountRateLimited(accountId, sessionHash); + await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp); } else if (res.statusCode === 200) { // 如果请求成功,检查并移除限流状态 const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId); diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 0a818101..4d5634e6 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -299,10 +299,10 @@ class UnifiedClaudeScheduler { } // 🚫 标记账户为限流状态 - async markAccountRateLimited(accountId, accountType, sessionHash = null) { + async markAccountRateLimited(accountId, accountType, sessionHash = null, rateLimitResetTimestamp = null) { try { if (accountType === 'claude-official') { - await claudeAccountService.markAccountRateLimited(accountId, sessionHash); + await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp); } else if (accountType === 'claude-console') { await claudeConsoleAccountService.markAccountRateLimited(accountId); }