route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
334 lines • 13 kB
JavaScript
"use strict";
/**
* Session Manager for Multi-Turn Conversation Support
* Manages conversation state and context across multiple requests
* 项目所有者: Jason Zhang
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.sessionManager = exports.SessionManager = void 0;
const logger_1 = require("@/utils/logger");
class SessionManager {
sessions = new Map();
clientFingerprintSessions = new Map(); // fingerprint -> sessionId
maxSessions = 1000;
sessionTimeout = 2 * 60 * 60 * 1000; // 2 hours
constructor() {
// Cleanup expired sessions every 10 minutes
setInterval(() => this.cleanupExpiredSessions(), 10 * 60 * 1000);
}
/**
* Extract session ID from request headers or generate new one
*/
extractSessionId(headers) {
// Look for session ID in various headers
const sessionId = headers['x-conversation-id'] ||
headers['x-session-id'] ||
headers['claude-conversation-id'];
if (sessionId) {
logger_1.logger.debug('Found existing session ID', { sessionId });
return sessionId;
}
// For Claude Code clients without session headers, try to identify by client fingerprint
const clientFingerprint = this.generateClientFingerprint(headers);
const existingSessionByFingerprint = this.clientFingerprintSessions.get(clientFingerprint);
logger_1.logger.debug('Client fingerprint analysis', {
fingerprint: clientFingerprint,
existingSession: existingSessionByFingerprint,
hasExistingSession: !!existingSessionByFingerprint,
sessionExists: existingSessionByFingerprint ? this.sessions.has(existingSessionByFingerprint) : false,
totalFingerprintMappings: this.clientFingerprintSessions.size,
totalSessions: this.sessions.size
});
if (existingSessionByFingerprint && this.sessions.has(existingSessionByFingerprint)) {
logger_1.logger.debug('Found existing session by client fingerprint', {
sessionId: existingSessionByFingerprint,
fingerprint: clientFingerprint
});
return existingSessionByFingerprint;
}
// Generate new session ID and associate with fingerprint
const newSessionId = this.generateSessionId();
this.clientFingerprintSessions.set(clientFingerprint, newSessionId);
logger_1.logger.debug('Generated new session ID with client fingerprint', {
sessionId: newSessionId,
fingerprint: clientFingerprint,
fingerprintDataForDebug: this.generateFingerprintDataForDebug(headers)
});
return newSessionId;
}
/**
* Get or create conversation session
*/
getOrCreateSession(sessionId) {
const existing = this.sessions.get(sessionId);
if (existing) {
existing.lastAccessedAt = new Date();
logger_1.logger.debug('Retrieved existing session', {
sessionId,
messageCount: existing.messageHistory.length
});
return existing;
}
// Create new session
const newSession = {
sessionId,
conversationId: this.generateConversationId(),
providerConversationIds: {},
messageHistory: [],
metadata: {},
createdAt: new Date(),
lastAccessedAt: new Date()
};
this.sessions.set(sessionId, newSession);
logger_1.logger.debug('Created new session', { sessionId, conversationId: newSession.conversationId });
return newSession;
}
/**
* Update session with new message
*/
addMessage(sessionId, message) {
const session = this.sessions.get(sessionId);
if (!session) {
logger_1.logger.warn('Attempted to add message to non-existent session', { sessionId });
return;
}
session.messageHistory.push(message);
session.lastAccessedAt = new Date();
logger_1.logger.debug('Added message to session', {
sessionId,
role: message.role,
messageCount: session.messageHistory.length
});
}
/**
* Get provider-specific conversation ID
*/
getProviderConversationId(sessionId, providerName) {
const session = this.sessions.get(sessionId);
if (!session) {
logger_1.logger.warn('Session not found for provider conversation ID', { sessionId, providerName });
return this.generateConversationId();
}
if (!session.providerConversationIds[providerName]) {
session.providerConversationIds[providerName] = this.generateConversationId();
logger_1.logger.debug('Generated provider conversation ID', {
sessionId,
providerName,
conversationId: session.providerConversationIds[providerName]
});
}
return session.providerConversationIds[providerName];
}
/**
* Get conversation history for provider
*/
getConversationHistory(sessionId, maxMessages) {
const session = this.sessions.get(sessionId);
if (!session) {
logger_1.logger.debug('No session found for history', { sessionId });
return [];
}
const history = session.messageHistory;
if (maxMessages && history.length > maxMessages) {
// Keep recent messages within limit
return history.slice(-maxMessages);
}
return [...history]; // Return copy to prevent modification
}
/**
* Update session metadata
*/
updateSessionMetadata(sessionId, metadata) {
const session = this.sessions.get(sessionId);
if (!session) {
logger_1.logger.warn('Attempted to update metadata for non-existent session', { sessionId });
return;
}
session.metadata = { ...session.metadata, ...metadata };
session.lastAccessedAt = new Date();
logger_1.logger.debug('Updated session metadata', { sessionId, metadata });
}
/**
* Update session tools
*/
updateSessionTools(sessionId, tools) {
const session = this.sessions.get(sessionId);
if (!session) {
logger_1.logger.warn('Attempted to update tools for non-existent session', { sessionId });
return;
}
session.tools = tools;
session.lastAccessedAt = new Date();
logger_1.logger.debug('Updated session tools', { sessionId, toolCount: tools.length });
}
/**
* Update session system messages
*/
updateSessionSystem(sessionId, system) {
const session = this.sessions.get(sessionId);
if (!session) {
logger_1.logger.warn('Attempted to update system for non-existent session', { sessionId });
return;
}
session.system = system;
session.lastAccessedAt = new Date();
logger_1.logger.debug('Updated session system', { sessionId, systemCount: system.length });
}
/**
* Get session tools
*/
getSessionTools(sessionId) {
const session = this.sessions.get(sessionId);
return session?.tools || [];
}
/**
* Get session system messages
*/
getSessionSystem(sessionId) {
const session = this.sessions.get(sessionId);
return session?.system || [];
}
/**
* Clear specific session
*/
clearSession(sessionId) {
const deleted = this.sessions.delete(sessionId);
if (deleted) {
logger_1.logger.debug('Cleared session', { sessionId });
}
return deleted;
}
/**
* Get session statistics
*/
getSessionStats() {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
let activeInLastHour = 0;
for (const session of this.sessions.values()) {
if (session.lastAccessedAt > oneHourAgo) {
activeInLastHour++;
}
}
return {
totalSessions: this.sessions.size,
activeInLastHour
};
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Generate client fingerprint based on request headers
* This helps identify the same Claude Code client instance across requests
*/
generateClientFingerprint(headers) {
// Use stable client characteristics for fingerprinting
const userAgent = headers['user-agent'] || '';
const authorization = headers['authorization'] || '';
const xApp = headers['x-app'] || '';
const packageVersion = headers['x-stainless-package-version'] || '';
const runtime = headers['x-stainless-runtime'] || '';
const runtimeVersion = headers['x-stainless-runtime-version'] || '';
const os = headers['x-stainless-os'] || '';
const arch = headers['x-stainless-arch'] || '';
// Create a stable hash from these characteristics
const fingerprintData = [
userAgent,
authorization, // Claude Code uses consistent auth tokens
xApp,
packageVersion,
runtime,
runtimeVersion,
os,
arch
].join('|');
// Simple hash function (for production, consider using crypto.createHash)
let hash = 0;
for (let i = 0; i < fingerprintData.length; i++) {
const char = fingerprintData.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return `fp_${Math.abs(hash).toString(36)}`;
}
/**
* Generate fingerprint data for debugging (not hashed)
*/
generateFingerprintDataForDebug(headers) {
const userAgent = headers['user-agent'] || '';
const authorization = headers['authorization'] || '';
const xApp = headers['x-app'] || '';
const packageVersion = headers['x-stainless-package-version'] || '';
const runtime = headers['x-stainless-runtime'] || '';
const runtimeVersion = headers['x-stainless-runtime-version'] || '';
const os = headers['x-stainless-os'] || '';
const arch = headers['x-stainless-arch'] || '';
return [
`userAgent: ${userAgent}`,
`auth: ${authorization.substring(0, 20)}...`,
`xApp: ${xApp}`,
`pkgVer: ${packageVersion}`,
`runtime: ${runtime}`,
`runtimeVer: ${runtimeVersion}`,
`os: ${os}`,
`arch: ${arch}`
].join(' | ');
}
/**
* Generate unique conversation ID
*/
generateConversationId() {
return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Clean up expired sessions
*/
cleanupExpiredSessions() {
const now = new Date();
const expiredSessions = [];
for (const [sessionId, session] of this.sessions.entries()) {
const age = now.getTime() - session.lastAccessedAt.getTime();
if (age > this.sessionTimeout) {
expiredSessions.push(sessionId);
}
}
// Remove expired sessions and their fingerprint mappings
for (const sessionId of expiredSessions) {
this.sessions.delete(sessionId);
// Remove fingerprint mapping for this session
for (const [fingerprint, mappedSessionId] of this.clientFingerprintSessions.entries()) {
if (mappedSessionId === sessionId) {
this.clientFingerprintSessions.delete(fingerprint);
break;
}
}
}
if (expiredSessions.length > 0) {
logger_1.logger.debug('Cleaned up expired sessions', {
expiredCount: expiredSessions.length,
remainingCount: this.sessions.size
});
}
// Enforce maximum session limit
if (this.sessions.size > this.maxSessions) {
const sessionsToRemove = this.sessions.size - this.maxSessions;
const sortedSessions = Array.from(this.sessions.entries())
.sort((a, b) => a[1].lastAccessedAt.getTime() - b[1].lastAccessedAt.getTime());
for (let i = 0; i < sessionsToRemove; i++) {
this.sessions.delete(sortedSessions[i][0]);
}
logger_1.logger.debug('Enforced session limit', {
removedCount: sessionsToRemove,
currentCount: this.sessions.size
});
}
}
}
exports.SessionManager = SessionManager;
// Global session manager instance
exports.sessionManager = new SessionManager();
//# sourceMappingURL=manager.js.map