mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
169 lines
5.0 KiB
JavaScript
169 lines
5.0 KiB
JavaScript
const crypto = require('crypto')
|
||
const logger = require('./logger')
|
||
|
||
class SessionHelper {
|
||
/**
|
||
* 生成会话哈希,用于sticky会话保持
|
||
* 基于Anthropic的prompt caching机制,优先使用metadata中的session ID
|
||
* @param {Object} requestBody - 请求体
|
||
* @returns {string|null} - 32字符的会话哈希,如果无法生成则返回null
|
||
*/
|
||
generateSessionHash(requestBody) {
|
||
if (!requestBody || typeof requestBody !== 'object') {
|
||
return null
|
||
}
|
||
|
||
// 1. 最高优先级:使用metadata中的session ID(直接使用,无需hash)
|
||
if (requestBody.metadata && requestBody.metadata.user_id) {
|
||
// 提取 session_xxx 部分
|
||
const userIdString = requestBody.metadata.user_id
|
||
const sessionMatch = userIdString.match(/session_([a-f0-9-]{36})/)
|
||
if (sessionMatch && sessionMatch[1]) {
|
||
const sessionId = sessionMatch[1]
|
||
// 直接返回session ID
|
||
logger.debug(`📋 Session ID extracted from metadata.user_id: ${sessionId}`)
|
||
return sessionId
|
||
}
|
||
}
|
||
|
||
let cacheableContent = ''
|
||
const system = requestBody.system || ''
|
||
const messages = requestBody.messages || []
|
||
|
||
// 2. 提取带有cache_control: {"type": "ephemeral"}的内容
|
||
// 检查system中的cacheable内容
|
||
if (Array.isArray(system)) {
|
||
for (const part of system) {
|
||
if (part && part.cache_control && part.cache_control.type === 'ephemeral') {
|
||
cacheableContent += part.text || ''
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查messages中的cacheable内容
|
||
for (const msg of messages) {
|
||
const content = msg.content || ''
|
||
let hasCacheControl = false
|
||
|
||
if (Array.isArray(content)) {
|
||
for (const part of content) {
|
||
if (part && part.cache_control && part.cache_control.type === 'ephemeral') {
|
||
hasCacheControl = true
|
||
break
|
||
}
|
||
}
|
||
} else if (
|
||
typeof content === 'string' &&
|
||
msg.cache_control &&
|
||
msg.cache_control.type === 'ephemeral'
|
||
) {
|
||
hasCacheControl = true
|
||
}
|
||
|
||
if (hasCacheControl) {
|
||
for (const message of messages) {
|
||
let messageText = ''
|
||
if (typeof message.content === 'string') {
|
||
messageText = message.content
|
||
} else if (Array.isArray(message.content)) {
|
||
messageText = message.content
|
||
.filter((part) => part.type === 'text')
|
||
.map((part) => part.text || '')
|
||
.join('')
|
||
}
|
||
|
||
if (messageText) {
|
||
cacheableContent += messageText
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 3. 如果有cacheable内容,直接使用
|
||
if (cacheableContent) {
|
||
const hash = crypto
|
||
.createHash('sha256')
|
||
.update(cacheableContent)
|
||
.digest('hex')
|
||
.substring(0, 32)
|
||
logger.debug(`📋 Session hash generated from cacheable content: ${hash}`)
|
||
return hash
|
||
}
|
||
|
||
// 4. Fallback: 使用system内容
|
||
if (system) {
|
||
let systemText = ''
|
||
if (typeof system === 'string') {
|
||
systemText = system
|
||
} else if (Array.isArray(system)) {
|
||
systemText = system.map((part) => part.text || '').join('')
|
||
}
|
||
|
||
if (systemText) {
|
||
const hash = crypto.createHash('sha256').update(systemText).digest('hex').substring(0, 32)
|
||
logger.debug(`📋 Session hash generated from system content: ${hash}`)
|
||
return hash
|
||
}
|
||
}
|
||
|
||
// 5. 最后fallback: 使用第一条消息内容
|
||
if (messages.length > 0) {
|
||
const firstMessage = messages[0]
|
||
let firstMessageText = ''
|
||
|
||
if (typeof firstMessage.content === 'string') {
|
||
firstMessageText = firstMessage.content
|
||
} else if (Array.isArray(firstMessage.content)) {
|
||
if (!firstMessage.content) {
|
||
logger.error('📋 Session hash generated from first message failed: ', firstMessage)
|
||
}
|
||
|
||
firstMessageText = firstMessage.content
|
||
.filter((part) => part.type === 'text')
|
||
.map((part) => part.text || '')
|
||
.join('')
|
||
}
|
||
|
||
if (firstMessageText) {
|
||
const hash = crypto
|
||
.createHash('sha256')
|
||
.update(firstMessageText)
|
||
.digest('hex')
|
||
.substring(0, 32)
|
||
logger.debug(`📋 Session hash generated from first message: ${hash}`)
|
||
return hash
|
||
}
|
||
}
|
||
|
||
// 无法生成会话哈希
|
||
logger.debug('📋 Unable to generate session hash - no suitable content found')
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 获取会话的Redis键名
|
||
* @param {string} sessionHash - 会话哈希
|
||
* @returns {string} - Redis键名
|
||
*/
|
||
getSessionRedisKey(sessionHash) {
|
||
return `sticky_session:${sessionHash}`
|
||
}
|
||
|
||
/**
|
||
* 验证会话哈希格式
|
||
* @param {string} sessionHash - 会话哈希
|
||
* @returns {boolean} - 是否有效
|
||
*/
|
||
isValidSessionHash(sessionHash) {
|
||
return (
|
||
typeof sessionHash === 'string' &&
|
||
sessionHash.length === 32 &&
|
||
/^[a-f0-9]{32}$/.test(sessionHash)
|
||
)
|
||
}
|
||
}
|
||
|
||
module.exports = new SessionHelper()
|