diff --git a/config/config.example.js b/config/config.example.js index 433ecd1f..a5aa2c7b 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -32,6 +32,14 @@ const config = { enableTLS: process.env.REDIS_ENABLE_TLS === 'true' }, + // 🔗 会话管理配置 + session: { + // 粘性会话TTL配置(天) + stickyTtlDays: parseInt(process.env.STICKY_SESSION_TTL_DAYS) || 15, + // 续期阈值(天) + renewalThresholdDays: parseInt(process.env.STICKY_SESSION_RENEWAL_THRESHOLD_DAYS) || 14 + }, + // 🎯 Claude API配置 claude: { apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages', diff --git a/src/models/redis.js b/src/models/redis.js index 145f94cd..c862fb57 100644 --- a/src/models/redis.js +++ b/src/models/redis.js @@ -1356,9 +1356,11 @@ class RedisClient { } // 🔗 会话sticky映射管理 - async setSessionAccountMapping(sessionHash, accountId, ttl = 3600) { + async setSessionAccountMapping(sessionHash, accountId, ttl = null) { + const appConfig = require('../../config/config') + const defaultTTL = ttl !== null ? ttl : (appConfig.session?.stickyTtlDays || 15) * 24 * 60 * 60 const key = `sticky_session:${sessionHash}` - await this.client.set(key, accountId, 'EX', ttl) + await this.client.set(key, accountId, 'EX', defaultTTL) } async getSessionAccountMapping(sessionHash) { @@ -1366,6 +1368,52 @@ class RedisClient { return await this.client.get(key) } + // 🚀 智能会话TTL续期:剩余时间少于阈值时自动续期 + async extendSessionAccountMappingTTL(sessionHash) { + const appConfig = require('../../config/config') + const key = `sticky_session:${sessionHash}` + + // 📊 从配置获取参数 + const ttlDays = appConfig.session?.stickyTtlDays || 15 + const thresholdDays = appConfig.session?.renewalThresholdDays || 14 + + const fullTTL = ttlDays * 24 * 60 * 60 // 转换为秒 + const renewalThreshold = thresholdDays * 24 * 60 * 60 // 转换为秒 + + try { + // 获取当前剩余TTL(秒) + const remainingTTL = await this.client.ttl(key) + + // 键不存在或已过期 + if (remainingTTL === -2) { + return false + } + + // 键存在但没有TTL(永不过期,不需要处理) + if (remainingTTL === -1) { + return true + } + + // 🎯 智能续期策略:仅在剩余时间少于阈值时才续期 + if (remainingTTL < renewalThreshold) { + await this.client.expire(key, fullTTL) + logger.debug( + `🔄 Renewed sticky session TTL: ${sessionHash} (was ${Math.round(remainingTTL / 24 / 3600)}d, renewed to ${ttlDays}d)` + ) + return true + } + + // 剩余时间充足,无需续期 + logger.debug( + `✅ Sticky session TTL sufficient: ${sessionHash} (remaining ${Math.round(remainingTTL / 24 / 3600)}d)` + ) + return true + } catch (error) { + logger.error('❌ Failed to extend session TTL:', error) + return false + } + } + async deleteSessionAccountMapping(sessionHash) { const key = `sticky_session:${sessionHash}` return await this.client.del(key) diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 86e595e5..29dee2bd 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -695,6 +695,8 @@ class ClaudeAccountService { // 验证映射的账户是否仍然可用 const mappedAccount = activeAccounts.find((acc) => acc.id === mappedAccountId) if (mappedAccount) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}` ) @@ -815,6 +817,8 @@ class ClaudeAccountService { ) await redis.deleteSessionAccountMapping(sessionHash) } else { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session shared account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}` ) diff --git a/src/services/unifiedClaudeScheduler.js b/src/services/unifiedClaudeScheduler.js index c373d1f5..7bceb040 100644 --- a/src/services/unifiedClaudeScheduler.js +++ b/src/services/unifiedClaudeScheduler.js @@ -177,6 +177,8 @@ class UnifiedClaudeScheduler { requestedModel ) if (isAvailable) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -789,6 +791,8 @@ class UnifiedClaudeScheduler { requestedModel ) if (isAvailable) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) diff --git a/src/services/unifiedGeminiScheduler.js b/src/services/unifiedGeminiScheduler.js index face2c81..27c2387f 100644 --- a/src/services/unifiedGeminiScheduler.js +++ b/src/services/unifiedGeminiScheduler.js @@ -61,6 +61,8 @@ class UnifiedGeminiScheduler { mappedAccount.accountType ) if (isAvailable) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -382,6 +384,8 @@ class UnifiedGeminiScheduler { mappedAccount.accountType ) if (isAvailable) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(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 85404543..3ccc435b 100644 --- a/src/services/unifiedOpenAIScheduler.js +++ b/src/services/unifiedOpenAIScheduler.js @@ -90,6 +90,8 @@ class UnifiedOpenAIScheduler { mappedAccount.accountType ) if (isAvailable) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}` ) @@ -388,6 +390,8 @@ class UnifiedOpenAIScheduler { mappedAccount.accountType ) if (isAvailable) { + // 🚀 智能会话续期:剩余时间少于14天时自动续期到15天 + await redis.extendSessionAccountMappingTTL(sessionHash) logger.info( `🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})` )