feat: 添加智能sticky会话保持功能

- 新增 sessionHelper.js 实现会话哈希生成,基于Anthropic的prompt caching机制
- 扩展 claudeAccountService.selectAvailableAccount 支持会话绑定
- 更新 claudeRelayService 集成会话保持功能
- 添加 Redis 会话映射存储,支持1小时过期
- 支持故障转移:绑定账户不可用时自动重新选择
- 三级fallback策略:cacheable内容 → system内容 → 第一条消息
- 完全向后兼容,自动启用无需配置

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-15 18:15:53 +08:00
parent 2257b42527
commit b2b8d8c719
4 changed files with 180 additions and 13 deletions

View File

@@ -286,8 +286,8 @@ class ClaudeAccountService {
}
}
// 🎯 智能选择可用账户
async selectAvailableAccount() {
// 🎯 智能选择可用账户支持sticky会话
async selectAvailableAccount(sessionHash = null) {
try {
const accounts = await redis.getAllClaudeAccounts();
@@ -300,6 +300,24 @@ class ClaudeAccountService {
throw new Error('No active Claude accounts available');
}
// 如果有会话哈希,检查是否有已映射的账户
if (sessionHash) {
const mappedAccountId = await redis.getSessionAccountMapping(sessionHash);
if (mappedAccountId) {
// 验证映射的账户是否仍然可用
const mappedAccount = activeAccounts.find(acc => acc.id === mappedAccountId);
if (mappedAccount) {
logger.info(`🎯 Using sticky session account: ${mappedAccount.name} (${mappedAccountId}) for session ${sessionHash}`);
return mappedAccountId;
} else {
logger.warn(`⚠️ Mapped account ${mappedAccountId} is no longer available, selecting new account`);
// 清理无效的映射
await redis.deleteSessionAccountMapping(sessionHash);
}
}
}
// 如果没有映射或映射无效,选择新账户
// 优先选择最近刷新过token的账户
const sortedAccounts = activeAccounts.sort((a, b) => {
const aLastRefresh = new Date(a.lastRefreshAt || 0).getTime();
@@ -307,7 +325,15 @@ class ClaudeAccountService {
return bLastRefresh - aLastRefresh;
});
return sortedAccounts[0].id;
const selectedAccountId = sortedAccounts[0].id;
// 如果有会话哈希,建立新的映射
if (sessionHash) {
await redis.setSessionAccountMapping(sessionHash, selectedAccountId, 3600); // 1小时过期
logger.info(`🎯 Created new sticky session mapping: ${sortedAccounts[0].name} (${selectedAccountId}) for session ${sessionHash}`);
}
return selectedAccountId;
} catch (error) {
logger.error('❌ Failed to select available account:', error);
throw error;

View File

@@ -2,6 +2,7 @@ const https = require('https');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpsProxyAgent } = require('https-proxy-agent');
const claudeAccountService = require('./claudeAccountService');
const sessionHelper = require('../utils/sessionHelper');
const logger = require('../utils/logger');
const config = require('../../config/config');
@@ -16,10 +17,13 @@ class ClaudeRelayService {
// 🚀 转发请求到Claude API
async relayRequest(requestBody, apiKeyData) {
try {
// 选择可用的Claude账户
const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount();
// 生成会话哈希用于sticky会话
const sessionHash = sessionHelper.generateSessionHash(requestBody);
logger.info(`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}`);
// 选择可用的Claude账户支持sticky会话
const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount(sessionHash);
logger.info(`📤 Processing API request for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}${sessionHash ? `, session: ${sessionHash}` : ''}`);
// 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId);
@@ -224,10 +228,13 @@ class ClaudeRelayService {
// 🌊 处理流式响应带usage数据捕获
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, usageCallback) {
try {
// 选择可用的Claude账户
const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount();
// 生成会话哈希用于sticky会话
const sessionHash = sessionHelper.generateSessionHash(requestBody);
logger.info(`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}`);
// 选择可用的Claude账户支持sticky会话
const accountId = apiKeyData.claudeAccountId || await claudeAccountService.selectAvailableAccount(sessionHash);
logger.info(`📡 Processing streaming API request with usage capture for key: ${apiKeyData.name || apiKeyData.id}, account: ${accountId}${sessionHash ? `, session: ${sessionHash}` : ''}`);
// 获取有效的访问token
const accessToken = await claudeAccountService.getValidAccessToken(accountId);