feat: 使用API响应头中的准确时间戳修正会话窗口和限流时间

- 从429响应中提取 anthropic-ratelimit-unified-reset 响应头
- 使用准确的重置时间戳设置限流结束时间和会话窗口
- 会话窗口开始时间 = 重置时间戳 - 5小时
- 兼容旧逻辑:无响应头时使用预估的会话窗口时间

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-31 16:29:07 +08:00
parent 2612611795
commit 8e89311dac
3 changed files with 67 additions and 28 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}