mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 17:49:16 +00:00
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:
@@ -732,33 +732,51 @@ class ClaudeAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🚫 标记账号为限流状态
|
// 🚫 标记账号为限流状态
|
||||||
async markAccountRateLimited(accountId, sessionHash = null) {
|
async markAccountRateLimited(accountId, sessionHash = null, rateLimitResetTimestamp = null) {
|
||||||
try {
|
try {
|
||||||
const accountData = await redis.getClaudeAccount(accountId);
|
const accountData = await redis.getClaudeAccount(accountId);
|
||||||
if (!accountData || Object.keys(accountData).length === 0) {
|
if (!accountData || Object.keys(accountData).length === 0) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取或创建会话窗口
|
|
||||||
const updatedAccountData = await this.updateSessionWindow(accountId, accountData);
|
|
||||||
|
|
||||||
// 设置限流状态和时间
|
// 设置限流状态和时间
|
||||||
|
const updatedAccountData = { ...accountData };
|
||||||
updatedAccountData.rateLimitedAt = new Date().toISOString();
|
updatedAccountData.rateLimitedAt = new Date().toISOString();
|
||||||
updatedAccountData.rateLimitStatus = 'limited';
|
updatedAccountData.rateLimitStatus = 'limited';
|
||||||
|
|
||||||
|
// 如果提供了准确的限流重置时间戳(来自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((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 {
|
||||||
|
// 获取或创建会话窗口(预估方式)
|
||||||
|
const windowData = await this.updateSessionWindow(accountId, updatedAccountData);
|
||||||
|
Object.assign(updatedAccountData, windowData);
|
||||||
|
|
||||||
// 限流结束时间 = 会话窗口结束时间
|
// 限流结束时间 = 会话窗口结束时间
|
||||||
if (updatedAccountData.sessionWindowEnd) {
|
if (updatedAccountData.sessionWindowEnd) {
|
||||||
updatedAccountData.rateLimitEndAt = updatedAccountData.sessionWindowEnd;
|
updatedAccountData.rateLimitEndAt = updatedAccountData.sessionWindowEnd;
|
||||||
const windowEnd = new Date(updatedAccountData.sessionWindowEnd);
|
const windowEnd = new Date(updatedAccountData.sessionWindowEnd);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const minutesUntilEnd = Math.ceil((windowEnd - now) / (1000 * 60));
|
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`);
|
logger.warn(`🚫 Account marked as rate limited until estimated session window ends: ${accountData.name} (${accountId}) - ${minutesUntilEnd} minutes remaining`);
|
||||||
} else {
|
} else {
|
||||||
// 如果没有会话窗口,使用默认1小时(兼容旧逻辑)
|
// 如果没有会话窗口,使用默认1小时(兼容旧逻辑)
|
||||||
const oneHourLater = new Date(Date.now() + 60 * 60 * 1000);
|
const oneHourLater = new Date(Date.now() + 60 * 60 * 1000);
|
||||||
updatedAccountData.rateLimitEndAt = oneHourLater.toISOString();
|
updatedAccountData.rateLimitEndAt = oneHourLater.toISOString();
|
||||||
logger.warn(`🚫 Account marked as rate limited (1 hour default): ${accountData.name} (${accountId})`);
|
logger.warn(`🚫 Account marked as rate limited (1 hour default): ${accountData.name} (${accountId})`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await redis.setClaudeAccount(accountId, updatedAccountData);
|
await redis.setClaudeAccount(accountId, updatedAccountData);
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,19 @@ class ClaudeRelayService {
|
|||||||
// 检查响应是否为限流错误
|
// 检查响应是否为限流错误
|
||||||
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
||||||
let isRateLimited = false;
|
let isRateLimited = false;
|
||||||
|
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()})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 检查响应体中的错误信息
|
||||||
try {
|
try {
|
||||||
const responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
|
const responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
|
||||||
if (responseBody && responseBody.error && responseBody.error.message &&
|
if (responseBody && responseBody.error && responseBody.error.message &&
|
||||||
@@ -154,11 +167,12 @@ class ClaudeRelayService {
|
|||||||
isRateLimited = true;
|
isRateLimited = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isRateLimited) {
|
if (isRateLimited) {
|
||||||
logger.warn(`🚫 Rate limit detected for account ${accountId}, status: ${response.statusCode}`);
|
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) {
|
} else if (response.statusCode === 200 || response.statusCode === 201) {
|
||||||
// 如果请求成功,检查并移除限流状态
|
// 如果请求成功,检查并移除限流状态
|
||||||
@@ -832,8 +846,15 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
// 处理限流状态
|
// 处理限流状态
|
||||||
if (rateLimitDetected || res.statusCode === 429) {
|
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) {
|
} else if (res.statusCode === 200) {
|
||||||
// 如果请求成功,检查并移除限流状态
|
// 如果请求成功,检查并移除限流状态
|
||||||
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId);
|
const isRateLimited = await claudeAccountService.isAccountRateLimited(accountId);
|
||||||
|
|||||||
@@ -299,10 +299,10 @@ class UnifiedClaudeScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🚫 标记账户为限流状态
|
// 🚫 标记账户为限流状态
|
||||||
async markAccountRateLimited(accountId, accountType, sessionHash = null) {
|
async markAccountRateLimited(accountId, accountType, sessionHash = null, rateLimitResetTimestamp = null) {
|
||||||
try {
|
try {
|
||||||
if (accountType === 'claude-official') {
|
if (accountType === 'claude-official') {
|
||||||
await claudeAccountService.markAccountRateLimited(accountId, sessionHash);
|
await claudeAccountService.markAccountRateLimited(accountId, sessionHash, rateLimitResetTimestamp);
|
||||||
} else if (accountType === 'claude-console') {
|
} else if (accountType === 'claude-console') {
|
||||||
await claudeConsoleAccountService.markAccountRateLimited(accountId);
|
await claudeConsoleAccountService.markAccountRateLimited(accountId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user