diff --git a/.env.example b/.env.example index fa640f15..c70db5fd 100644 --- a/.env.example +++ b/.env.example @@ -26,7 +26,7 @@ REDIS_ENABLE_TLS= # 粘性会话TTL配置(小时),默认1小时 STICKY_SESSION_TTL_HOURS=1 # 续期阈值(分钟),默认0分钟(不续期) -STICKY_SESSION_RENEWAL_THRESHOLD_MINUTES=0 +STICKY_SESSION_RENEWAL_THRESHOLD_MINUTES=15 # 🎯 Claude API 配置 CLAUDE_API_URL=https://api.anthropic.com/v1/messages diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index 22ffa6c0..584398ee 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -244,8 +244,8 @@ class UnifiedClaudeScheduler { effectiveModel ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天(续期正确的 unified 映射键) + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -806,9 +806,11 @@ class UnifiedClaudeScheduler { async _setSessionMapping(sessionHash, accountId, accountType) { const client = redis.getClientSafe() const mappingData = JSON.stringify({ accountId, accountType }) - - // 设置1小时过期 - await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, mappingData) + // 依据配置设置TTL(小时) + const appConfig = require('../../config/config') + const ttlHours = appConfig.session?.stickyTtlHours || 1 + const ttlSeconds = Math.max(1, Math.floor(ttlHours * 60 * 60)) + await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, ttlSeconds, mappingData) } // 🗑️ 删除会话映射 @@ -817,6 +819,44 @@ class UnifiedClaudeScheduler { await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`) } + // 🔁 续期统一调度会话映射TTL(针对 unified_claude_session_mapping:* 键),遵循会话配置 + async _extendSessionMappingTTL(sessionHash) { + try { + const client = redis.getClientSafe() + const key = `${this.SESSION_MAPPING_PREFIX}${sessionHash}` + const remainingTTL = await client.ttl(key) + + // -2: key 不存在;-1: 无过期时间 + if (remainingTTL === -2) return false + if (remainingTTL === -1) return true + + const appConfig = require('../../config/config') + const ttlHours = appConfig.session?.stickyTtlHours || 1 + const renewalThresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0 + + // 阈值为0则不续期 + if (!renewalThresholdMinutes) return true + + const fullTTL = Math.max(1, Math.floor(ttlHours * 60 * 60)) + const threshold = Math.max(0, Math.floor(renewalThresholdMinutes * 60)) + + if (remainingTTL < threshold) { + await client.expire(key, fullTTL) + logger.debug( + `🔄 Renewed unified session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}m, renewed to ${ttlHours}h)` + ) + } else { + logger.debug( + `✅ Unified session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}m)` + ) + } + return true + } catch (error) { + logger.error('❌ Failed to extend unified session TTL:', error) + return false + } + } + // 🚫 标记账户为限流状态 async markAccountRateLimited( accountId, @@ -989,8 +1029,8 @@ class UnifiedClaudeScheduler { requestedModel ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期:续期 unified 映射键 + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -1131,8 +1171,8 @@ class UnifiedClaudeScheduler { effectiveModel ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期:续期 unified 映射键 + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky CCR session account: ${mappedAccount.accountId} for session ${sessionHash}` ) diff --git a/src/services/unifiedGeminiScheduler.js b/src/services/unifiedGeminiScheduler.js index 27c2387f..890803a9 100644 --- a/src/services/unifiedGeminiScheduler.js +++ b/src/services/unifiedGeminiScheduler.js @@ -61,8 +61,8 @@ class UnifiedGeminiScheduler { mappedAccount.accountType ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期(续期 unified 映射键,按配置) + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -285,9 +285,11 @@ class UnifiedGeminiScheduler { async _setSessionMapping(sessionHash, accountId, accountType) { const client = redis.getClientSafe() const mappingData = JSON.stringify({ accountId, accountType }) - - // 设置1小时过期 - await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, mappingData) + // 依据配置设置TTL(小时) + const appConfig = require('../../config/config') + const ttlHours = appConfig.session?.stickyTtlHours || 1 + const ttlSeconds = Math.max(1, Math.floor(ttlHours * 60 * 60)) + await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, ttlSeconds, mappingData) } // 🗑️ 删除会话映射 @@ -296,6 +298,41 @@ class UnifiedGeminiScheduler { await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`) } + // 🔁 续期统一调度会话映射TTL(针对 unified_gemini_session_mapping:* 键),遵循会话配置 + async _extendSessionMappingTTL(sessionHash) { + try { + const client = redis.getClientSafe() + const key = `${this.SESSION_MAPPING_PREFIX}${sessionHash}` + const remainingTTL = await client.ttl(key) + + if (remainingTTL === -2) return false + if (remainingTTL === -1) return true + + const appConfig = require('../../config/config') + const ttlHours = appConfig.session?.stickyTtlHours || 1 + const renewalThresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0 + if (!renewalThresholdMinutes) return true + + const fullTTL = Math.max(1, Math.floor(ttlHours * 60 * 60)) + const threshold = Math.max(0, Math.floor(renewalThresholdMinutes * 60)) + + if (remainingTTL < threshold) { + await client.expire(key, fullTTL) + logger.debug( + `🔄 Renewed unified Gemini session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}m, renewed to ${ttlHours}h)` + ) + } else { + logger.debug( + `✅ Unified Gemini session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}m)` + ) + } + return true + } catch (error) { + logger.error('❌ Failed to extend unified Gemini session TTL:', error) + return false + } + } + // 🚫 标记账户为限流状态 async markAccountRateLimited(accountId, accountType, sessionHash = null) { try { @@ -384,8 +421,8 @@ class UnifiedGeminiScheduler { mappedAccount.accountType ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期(续期 unified 映射键,按配置) + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) diff --git a/src/services/unifiedOpenAIScheduler.js b/src/services/unifiedOpenAIScheduler.js index 99c21f32..ddc691be 100644 --- a/src/services/unifiedOpenAIScheduler.js +++ b/src/services/unifiedOpenAIScheduler.js @@ -90,8 +90,8 @@ class UnifiedOpenAIScheduler { mappedAccount.accountType ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期(续期 unified 映射键,按配置) + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -291,9 +291,11 @@ class UnifiedOpenAIScheduler { async _setSessionMapping(sessionHash, accountId, accountType) { const client = redis.getClientSafe() const mappingData = JSON.stringify({ accountId, accountType }) - - // 设置1小时过期 - await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, mappingData) + // 依据配置设置TTL(小时) + const appConfig = require('../../config/config') + const ttlHours = appConfig.session?.stickyTtlHours || 1 + const ttlSeconds = Math.max(1, Math.floor(ttlHours * 60 * 60)) + await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, ttlSeconds, mappingData) } // 🗑️ 删除会话映射 @@ -302,6 +304,41 @@ class UnifiedOpenAIScheduler { await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`) } + // 🔁 续期统一调度会话映射TTL(针对 unified_openai_session_mapping:* 键),遵循会话配置 + async _extendSessionMappingTTL(sessionHash) { + try { + const client = redis.getClientSafe() + const key = `${this.SESSION_MAPPING_PREFIX}${sessionHash}` + const remainingTTL = await client.ttl(key) + + if (remainingTTL === -2) return false + if (remainingTTL === -1) return true + + const appConfig = require('../../config/config') + const ttlHours = appConfig.session?.stickyTtlHours || 1 + const renewalThresholdMinutes = appConfig.session?.renewalThresholdMinutes || 0 + if (!renewalThresholdMinutes) return true + + const fullTTL = Math.max(1, Math.floor(ttlHours * 60 * 60)) + const threshold = Math.max(0, Math.floor(renewalThresholdMinutes * 60)) + + if (remainingTTL < threshold) { + await client.expire(key, fullTTL) + logger.debug( + `🔄 Renewed unified OpenAI session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 60)}m, renewed to ${ttlHours}h)` + ) + } else { + logger.debug( + `✅ Unified OpenAI session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 60)}m)` + ) + } + return true + } catch (error) { + logger.error('❌ Failed to extend unified OpenAI session TTL:', error) + return false + } + } + // 🚫 标记账户为限流状态 async markAccountRateLimited(accountId, accountType, sessionHash = null, resetsInSeconds = null) { try { @@ -408,8 +445,8 @@ class UnifiedOpenAIScheduler { mappedAccount.accountType ) if (isAvailable) { - // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 - await redis.extendSessionAccountMappingTTL(sessionHash) + // 🚀 智能会话续期(续期 unified 映射键,按配置) + await this._extendSessionMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})` )