mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
Merge branch 'dev' of github.com:Wei-Shaw/claude-relay-service into dev
This commit is contained in:
@@ -26,7 +26,7 @@ REDIS_ENABLE_TLS=
|
|||||||
# 粘性会话TTL配置(小时),默认1小时
|
# 粘性会话TTL配置(小时),默认1小时
|
||||||
STICKY_SESSION_TTL_HOURS=1
|
STICKY_SESSION_TTL_HOURS=1
|
||||||
# 续期阈值(分钟),默认0分钟(不续期)
|
# 续期阈值(分钟),默认0分钟(不续期)
|
||||||
STICKY_SESSION_RENEWAL_THRESHOLD_MINUTES=0
|
STICKY_SESSION_RENEWAL_THRESHOLD_MINUTES=15
|
||||||
|
|
||||||
# 🎯 Claude API 配置
|
# 🎯 Claude API 配置
|
||||||
CLAUDE_API_URL=https://api.anthropic.com/v1/messages
|
CLAUDE_API_URL=https://api.anthropic.com/v1/messages
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const {
|
|||||||
globalRateLimit,
|
globalRateLimit,
|
||||||
requestSizeLimit
|
requestSizeLimit
|
||||||
} = require('./middleware/auth')
|
} = require('./middleware/auth')
|
||||||
|
const { browserFallbackMiddleware } = require('./middleware/browserFallback')
|
||||||
|
|
||||||
class Application {
|
class Application {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -109,6 +110,9 @@ class Application {
|
|||||||
this.app.use(corsMiddleware)
|
this.app.use(corsMiddleware)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 兜底中间件:处理Chrome插件兼容性(必须在认证之前)
|
||||||
|
this.app.use(browserFallbackMiddleware)
|
||||||
|
|
||||||
// 📦 压缩 - 排除流式响应(SSE)
|
// 📦 压缩 - 排除流式响应(SSE)
|
||||||
this.app.use(
|
this.app.use(
|
||||||
compression({
|
compression({
|
||||||
|
|||||||
@@ -757,7 +757,7 @@ const requireAdmin = (req, res, next) => {
|
|||||||
// 注意:使用统计现在直接在/api/v1/messages路由中处理,
|
// 注意:使用统计现在直接在/api/v1/messages路由中处理,
|
||||||
// 以便从Claude API响应中提取真实的usage数据
|
// 以便从Claude API响应中提取真实的usage数据
|
||||||
|
|
||||||
// 🚦 CORS中间件(优化版)
|
// 🚦 CORS中间件(优化版,支持Chrome插件)
|
||||||
const corsMiddleware = (req, res, next) => {
|
const corsMiddleware = (req, res, next) => {
|
||||||
const { origin } = req.headers
|
const { origin } = req.headers
|
||||||
|
|
||||||
@@ -769,8 +769,11 @@ const corsMiddleware = (req, res, next) => {
|
|||||||
'https://127.0.0.1:3000'
|
'https://127.0.0.1:3000'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 🆕 检查是否为Chrome插件请求
|
||||||
|
const isChromeExtension = origin && origin.startsWith('chrome-extension://')
|
||||||
|
|
||||||
// 设置CORS头
|
// 设置CORS头
|
||||||
if (allowedOrigins.includes(origin) || !origin) {
|
if (allowedOrigins.includes(origin) || !origin || isChromeExtension) {
|
||||||
res.header('Access-Control-Allow-Origin', origin || '*')
|
res.header('Access-Control-Allow-Origin', origin || '*')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,7 +788,9 @@ const corsMiddleware = (req, res, next) => {
|
|||||||
'Authorization',
|
'Authorization',
|
||||||
'x-api-key',
|
'x-api-key',
|
||||||
'api-key',
|
'api-key',
|
||||||
'x-admin-token'
|
'x-admin-token',
|
||||||
|
'anthropic-version',
|
||||||
|
'anthropic-dangerous-direct-browser-access'
|
||||||
].join(', ')
|
].join(', ')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
52
src/middleware/browserFallback.js
Normal file
52
src/middleware/browserFallback.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览器/Chrome插件兜底中间件
|
||||||
|
* 专门处理第三方插件的兼容性问题
|
||||||
|
*/
|
||||||
|
const browserFallbackMiddleware = (req, res, next) => {
|
||||||
|
const userAgent = req.headers['user-agent'] || ''
|
||||||
|
const origin = req.headers['origin'] || ''
|
||||||
|
const authHeader = req.headers['authorization'] || req.headers['x-api-key'] || ''
|
||||||
|
|
||||||
|
// 检查是否为Chrome插件或浏览器请求
|
||||||
|
const isChromeExtension = origin.startsWith('chrome-extension://')
|
||||||
|
const isBrowserRequest = userAgent.includes('Mozilla/') && userAgent.includes('Chrome/')
|
||||||
|
const hasApiKey = authHeader.startsWith('cr_') // 我们的API Key格式
|
||||||
|
|
||||||
|
if ((isChromeExtension || isBrowserRequest) && hasApiKey) {
|
||||||
|
// 为Chrome插件请求添加特殊标记
|
||||||
|
req.isBrowserFallback = true
|
||||||
|
req.originalUserAgent = userAgent
|
||||||
|
|
||||||
|
// 🆕 关键修改:伪装成claude-cli请求以绕过客户端限制
|
||||||
|
req.headers['user-agent'] = 'claude-cli/1.0.110 (external, cli, browser-fallback)'
|
||||||
|
|
||||||
|
// 确保设置正确的认证头
|
||||||
|
if (!req.headers['authorization'] && req.headers['x-api-key']) {
|
||||||
|
req.headers['authorization'] = `Bearer ${req.headers['x-api-key']}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加必要的Anthropic头
|
||||||
|
if (!req.headers['anthropic-version']) {
|
||||||
|
req.headers['anthropic-version'] = '2023-06-01'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.headers['anthropic-dangerous-direct-browser-access']) {
|
||||||
|
req.headers['anthropic-dangerous-direct-browser-access'] = 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.api(
|
||||||
|
`🔧 Browser fallback activated for ${isChromeExtension ? 'Chrome extension' : 'browser'} request`
|
||||||
|
)
|
||||||
|
logger.api(` Original User-Agent: "${req.originalUserAgent}"`)
|
||||||
|
logger.api(` Origin: "${origin}"`)
|
||||||
|
logger.api(` Modified User-Agent: "${req.headers['user-agent']}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
browserFallbackMiddleware
|
||||||
|
}
|
||||||
@@ -629,8 +629,30 @@ class ClaudeRelayService {
|
|||||||
'transfer-encoding'
|
'transfer-encoding'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 🆕 需要移除的浏览器相关 headers(避免CORS问题)
|
||||||
|
const browserHeaders = [
|
||||||
|
'origin',
|
||||||
|
'referer',
|
||||||
|
'sec-fetch-mode',
|
||||||
|
'sec-fetch-site',
|
||||||
|
'sec-fetch-dest',
|
||||||
|
'sec-ch-ua',
|
||||||
|
'sec-ch-ua-mobile',
|
||||||
|
'sec-ch-ua-platform',
|
||||||
|
'accept-language',
|
||||||
|
'accept-encoding',
|
||||||
|
'accept',
|
||||||
|
'cache-control',
|
||||||
|
'pragma',
|
||||||
|
'anthropic-dangerous-direct-browser-access' // 这个头可能触发CORS检查
|
||||||
|
]
|
||||||
|
|
||||||
// 应该保留的 headers(用于会话一致性和追踪)
|
// 应该保留的 headers(用于会话一致性和追踪)
|
||||||
const allowedHeaders = ['x-request-id']
|
const allowedHeaders = [
|
||||||
|
'x-request-id',
|
||||||
|
'anthropic-version', // 保留API版本
|
||||||
|
'anthropic-beta' // 保留beta功能
|
||||||
|
]
|
||||||
|
|
||||||
const filteredHeaders = {}
|
const filteredHeaders = {}
|
||||||
|
|
||||||
@@ -641,8 +663,8 @@ class ClaudeRelayService {
|
|||||||
if (allowedHeaders.includes(lowerKey)) {
|
if (allowedHeaders.includes(lowerKey)) {
|
||||||
filteredHeaders[key] = clientHeaders[key]
|
filteredHeaders[key] = clientHeaders[key]
|
||||||
}
|
}
|
||||||
// 如果不在敏感列表中,也保留
|
// 如果不在敏感列表和浏览器列表中,也保留
|
||||||
else if (!sensitiveHeaders.includes(lowerKey)) {
|
else if (!sensitiveHeaders.includes(lowerKey) && !browserHeaders.includes(lowerKey)) {
|
||||||
filteredHeaders[key] = clientHeaders[key]
|
filteredHeaders[key] = clientHeaders[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ class UnifiedClaudeScheduler {
|
|||||||
effectiveModel
|
effectiveModel
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天(续期正确的 unified 映射键)
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -806,9 +806,11 @@ class UnifiedClaudeScheduler {
|
|||||||
async _setSessionMapping(sessionHash, accountId, accountType) {
|
async _setSessionMapping(sessionHash, accountId, accountType) {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const mappingData = JSON.stringify({ accountId, accountType })
|
const mappingData = JSON.stringify({ accountId, accountType })
|
||||||
|
// 依据配置设置TTL(小时)
|
||||||
// 设置1小时过期
|
const appConfig = require('../../config/config')
|
||||||
await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, mappingData)
|
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,50 @@ class UnifiedClaudeScheduler {
|
|||||||
await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
|
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(
|
async markAccountRateLimited(
|
||||||
accountId,
|
accountId,
|
||||||
@@ -989,8 +1035,8 @@ class UnifiedClaudeScheduler {
|
|||||||
requestedModel
|
requestedModel
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期:续期 unified 映射键
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -1131,8 +1177,8 @@ class UnifiedClaudeScheduler {
|
|||||||
effectiveModel
|
effectiveModel
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期:续期 unified 映射键
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky CCR session account: ${mappedAccount.accountId} for session ${sessionHash}`
|
`🎯 Using sticky CCR session account: ${mappedAccount.accountId} for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ class UnifiedGeminiScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期(续期 unified 映射键,按配置)
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -285,9 +285,11 @@ class UnifiedGeminiScheduler {
|
|||||||
async _setSessionMapping(sessionHash, accountId, accountType) {
|
async _setSessionMapping(sessionHash, accountId, accountType) {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const mappingData = JSON.stringify({ accountId, accountType })
|
const mappingData = JSON.stringify({ accountId, accountType })
|
||||||
|
// 依据配置设置TTL(小时)
|
||||||
// 设置1小时过期
|
const appConfig = require('../../config/config')
|
||||||
await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, mappingData)
|
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,47 @@ class UnifiedGeminiScheduler {
|
|||||||
await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
|
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) {
|
async markAccountRateLimited(accountId, accountType, sessionHash = null) {
|
||||||
try {
|
try {
|
||||||
@@ -384,8 +427,8 @@ class UnifiedGeminiScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期(续期 unified 映射键,按配置)
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ class UnifiedOpenAIScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期(续期 unified 映射键,按配置)
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
`🎯 Using sticky session account: ${mappedAccount.accountId} (${mappedAccount.accountType}) for session ${sessionHash}`
|
||||||
)
|
)
|
||||||
@@ -380,9 +380,11 @@ class UnifiedOpenAIScheduler {
|
|||||||
async _setSessionMapping(sessionHash, accountId, accountType) {
|
async _setSessionMapping(sessionHash, accountId, accountType) {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
const mappingData = JSON.stringify({ accountId, accountType })
|
const mappingData = JSON.stringify({ accountId, accountType })
|
||||||
|
// 依据配置设置TTL(小时)
|
||||||
// 设置1小时过期
|
const appConfig = require('../../config/config')
|
||||||
await client.setex(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`, 3600, mappingData)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🗑️ 删除会话映射
|
// 🗑️ 删除会话映射
|
||||||
@@ -391,6 +393,47 @@ class UnifiedOpenAIScheduler {
|
|||||||
await client.del(`${this.SESSION_MAPPING_PREFIX}${sessionHash}`)
|
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) {
|
async markAccountRateLimited(accountId, accountType, sessionHash = null, resetsInSeconds = null) {
|
||||||
try {
|
try {
|
||||||
@@ -520,8 +563,8 @@ class UnifiedOpenAIScheduler {
|
|||||||
mappedAccount.accountType
|
mappedAccount.accountType
|
||||||
)
|
)
|
||||||
if (isAvailable) {
|
if (isAvailable) {
|
||||||
// 🚀 智能会话续期:剩余时间少于14天时自动续期到15天
|
// 🚀 智能会话续期(续期 unified 映射键,按配置)
|
||||||
await redis.extendSessionAccountMappingTTL(sessionHash)
|
await this._extendSessionMappingTTL(sessionHash)
|
||||||
logger.info(
|
logger.info(
|
||||||
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})`
|
`🎯 Using sticky session account from group: ${mappedAccount.accountId} (${mappedAccount.accountType})`
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user