refactor: apply SessionManager to all agents
- domain-agent: use DomainSessionManager (handles target_domain) - deposit-agent: use SessionManager<DepositSession> - troubleshoot-agent: use SessionManager<TroubleshootSession> - server-agent: use ServerSessionManager (handles last_recommendation) - Remove ~760 lines of duplicated session CRUD code - Use centralized constants from agent-config.ts - Import OpenAI types from types.ts instead of local definitions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,74 +15,18 @@
|
||||
* 4. Expected: Order confirmation
|
||||
*/
|
||||
|
||||
import type { Env, ServerSession, ServerSessionStatus, BandwidthInfo, RecommendResponse } from '../types';
|
||||
import type { Env, ServerSession, ServerSessionStatus, BandwidthInfo, RecommendResponse, OpenAIToolCall, OpenAIMessage, OpenAIAPIResponse } from '../types';
|
||||
import { createLogger } from '../utils/logger';
|
||||
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
||||
import { formatTrafficInfo } from '../utils/formatters';
|
||||
import { SERVER_CONSULTATION_STATUS, LANGUAGE_CODE } from '../constants';
|
||||
import { ServerSessionManager } from '../utils/session-manager';
|
||||
import { getSessionConfig } from '../constants/agent-config';
|
||||
|
||||
const logger = createLogger('server-agent');
|
||||
|
||||
// D1 Session Management
|
||||
const SERVER_SESSION_TTL_MS = 60 * 60 * 1000; // 1시간
|
||||
|
||||
/**
|
||||
* 새 서버 세션 생성
|
||||
*
|
||||
* @param userId - Telegram User ID
|
||||
* @param status - 세션 상태
|
||||
* @returns 새로운 ServerSession 객체
|
||||
*/
|
||||
export function createServerSession(
|
||||
userId: string,
|
||||
status: ServerSessionStatus = 'gathering'
|
||||
): ServerSession {
|
||||
const now = Date.now();
|
||||
return {
|
||||
user_id: userId,
|
||||
status,
|
||||
collected_info: {},
|
||||
messages: [],
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
expires_at: now + SERVER_SESSION_TTL_MS,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 만료 여부 확인
|
||||
*
|
||||
* @param session - ServerSession
|
||||
* @returns true if expired, false otherwise
|
||||
*/
|
||||
export function isSessionExpired(session: ServerSession): boolean {
|
||||
return session.expires_at < Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션에 메시지 추가
|
||||
*
|
||||
* @param session - ServerSession
|
||||
* @param role - 메시지 역할 ('user' | 'assistant')
|
||||
* @param content - 메시지 내용
|
||||
*/
|
||||
export function addMessageToSession(
|
||||
session: ServerSession,
|
||||
role: 'user' | 'assistant',
|
||||
content: string
|
||||
): void {
|
||||
session.messages.push({ role, content });
|
||||
|
||||
// 최대 메시지 수 제한 (20개)
|
||||
const MAX_MESSAGES = 20;
|
||||
if (session.messages.length > MAX_MESSAGES) {
|
||||
session.messages = session.messages.slice(-MAX_MESSAGES);
|
||||
logger.warn('세션 메시지 최대 개수 초과, 오래된 메시지 제거', {
|
||||
userId: session.user_id,
|
||||
maxMessages: MAX_MESSAGES,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Session manager instance
|
||||
const sessionManager = new ServerSessionManager(getSessionConfig('server'));
|
||||
|
||||
/**
|
||||
* 서버 세션 존재 여부 확인 (라우팅용)
|
||||
@@ -92,122 +36,7 @@ export function addMessageToSession(
|
||||
* @returns true if active session exists, false otherwise
|
||||
*/
|
||||
export async function hasServerSession(db: D1Database, userId: string): Promise<boolean> {
|
||||
const session = await getServerSession(db, userId);
|
||||
return session !== null && !isSessionExpired(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* D1에서 서버 세션 조회
|
||||
*
|
||||
* @param db - D1 Database
|
||||
* @param userId - Telegram User ID
|
||||
* @returns ServerSession 또는 null (세션 없거나 만료)
|
||||
*/
|
||||
export async function getServerSession(
|
||||
db: D1Database,
|
||||
userId: string
|
||||
): Promise<ServerSession | null> {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const result = await db.prepare(
|
||||
'SELECT * FROM server_sessions WHERE user_id = ? AND expires_at > ?'
|
||||
).bind(userId, now).first<{
|
||||
user_id: string;
|
||||
status: string;
|
||||
collected_info: string | null;
|
||||
last_recommendation: string | null;
|
||||
messages: string | null;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
expires_at: number;
|
||||
}>();
|
||||
|
||||
if (!result) {
|
||||
logger.info('세션 없음', { userId });
|
||||
return null;
|
||||
}
|
||||
|
||||
const session: ServerSession = {
|
||||
user_id: result.user_id,
|
||||
status: result.status as ServerSessionStatus,
|
||||
collected_info: result.collected_info ? JSON.parse(result.collected_info) : {},
|
||||
last_recommendation: result.last_recommendation ? JSON.parse(result.last_recommendation) : undefined,
|
||||
messages: result.messages ? JSON.parse(result.messages) : [],
|
||||
created_at: result.created_at,
|
||||
updated_at: result.updated_at,
|
||||
expires_at: result.expires_at,
|
||||
};
|
||||
|
||||
logger.info('세션 조회 성공', { userId, status: session.status, hasLastRecommendation: !!session.last_recommendation });
|
||||
return session;
|
||||
} catch (error) {
|
||||
logger.error('세션 조회 실패', error as Error, { userId });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 세션 저장 (생성 또는 업데이트)
|
||||
*
|
||||
* @param db - D1 Database
|
||||
* @param session - ServerSession
|
||||
*/
|
||||
export async function saveServerSession(
|
||||
db: D1Database,
|
||||
session: ServerSession
|
||||
): Promise<void> {
|
||||
try {
|
||||
const now = Date.now();
|
||||
const expiresAt = now + SERVER_SESSION_TTL_MS;
|
||||
|
||||
await db.prepare(`
|
||||
INSERT INTO server_sessions
|
||||
(user_id, status, collected_info, last_recommendation, messages, created_at, updated_at, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
status = excluded.status,
|
||||
collected_info = excluded.collected_info,
|
||||
last_recommendation = excluded.last_recommendation,
|
||||
messages = excluded.messages,
|
||||
updated_at = excluded.updated_at,
|
||||
expires_at = excluded.expires_at
|
||||
`).bind(
|
||||
session.user_id,
|
||||
session.status,
|
||||
JSON.stringify(session.collected_info || {}),
|
||||
session.last_recommendation ? JSON.stringify(session.last_recommendation) : null,
|
||||
JSON.stringify(session.messages || []),
|
||||
session.created_at || now,
|
||||
now,
|
||||
expiresAt
|
||||
).run();
|
||||
|
||||
logger.info('세션 저장 성공', { userId: session.user_id, status: session.status });
|
||||
} catch (error) {
|
||||
logger.error('세션 저장 실패', error as Error, { userId: session.user_id });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 세션 삭제
|
||||
*
|
||||
* @param db - D1 Database
|
||||
* @param userId - Telegram User ID
|
||||
*/
|
||||
export async function deleteServerSession(
|
||||
db: D1Database,
|
||||
userId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db.prepare('DELETE FROM server_sessions WHERE user_id = ?')
|
||||
.bind(userId)
|
||||
.run();
|
||||
logger.info('세션 삭제 성공', { userId });
|
||||
} catch (error) {
|
||||
logger.error('세션 삭제 실패', error as Error, { userId });
|
||||
throw error;
|
||||
}
|
||||
return await sessionManager.has(db, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -516,28 +345,6 @@ function inferExpectedUsers(scale: string, techStack?: string[]): number {
|
||||
return 10; // Default to personal
|
||||
}
|
||||
|
||||
// OpenAI API 응답 타입
|
||||
interface OpenAIToolCall {
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenAIMessage {
|
||||
role: 'assistant';
|
||||
content: string | null;
|
||||
tool_calls?: OpenAIToolCall[];
|
||||
}
|
||||
|
||||
interface OpenAIAPIResponse {
|
||||
choices: Array<{
|
||||
message: OpenAIMessage;
|
||||
finish_reason: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server Expert AI 호출 (Function Calling 지원)
|
||||
@@ -778,11 +585,11 @@ export async function processServerConsultation(
|
||||
|
||||
try {
|
||||
// 1. Check for existing session
|
||||
let session = await getServerSession(db, userId);
|
||||
let session = await sessionManager.get(db, userId);
|
||||
|
||||
// 2. Create new session if none exists
|
||||
if (!session) {
|
||||
session = createServerSession(userId, 'gathering');
|
||||
session = sessionManager.create(userId, 'gathering');
|
||||
}
|
||||
|
||||
// ordering 상태에서 "신청" 외 메시지 입력 시 세션 정리
|
||||
@@ -790,7 +597,7 @@ export async function processServerConsultation(
|
||||
// "신청"은 message-handler에서 처리, 여기까지 오면 다른 메시지임
|
||||
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
||||
await env.SESSION_KV?.delete(orderConfirmKey);
|
||||
await deleteServerSession(db, session.user_id);
|
||||
await sessionManager.delete(db, session.user_id);
|
||||
|
||||
logger.info('주문 확인 세션 취소 (다른 메시지 입력)', { userId: session.user_id });
|
||||
return '__PASSTHROUGH__'; // 일반 대화로 전환
|
||||
@@ -800,7 +607,7 @@ export async function processServerConsultation(
|
||||
// "취소", "다시", "처음", "리셋", "초기화" 등
|
||||
if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) ||
|
||||
/취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) {
|
||||
await deleteServerSession(db, session.user_id);
|
||||
await sessionManager.delete(db, session.user_id);
|
||||
logger.info('사용자 요청으로 상담 취소', {
|
||||
userId: session.user_id,
|
||||
previousStatus: session.status,
|
||||
@@ -811,14 +618,14 @@ export async function processServerConsultation(
|
||||
|
||||
// "서버 추천" 키워드로 새로 시작 요청 (기존 세션 리셋)
|
||||
if (/서버\s*추천/.test(userMessage)) {
|
||||
await deleteServerSession(db, session.user_id);
|
||||
await sessionManager.delete(db, session.user_id);
|
||||
logger.info('서버 추천 키워드로 세션 리셋', {
|
||||
userId: session.user_id,
|
||||
previousStatus: session.status
|
||||
});
|
||||
// 새 세션 생성하고 시작 메시지 반환
|
||||
const newSession = createServerSession(session.user_id, 'gathering');
|
||||
await saveServerSession(db, newSession);
|
||||
const newSession = sessionManager.create(session.user_id, 'gathering');
|
||||
await sessionManager.save(db, newSession);
|
||||
return '안녕하세요! 서버 추천을 도와드리겠습니다. 😊\n\n어떤 서비스를 운영하실 건가요?\n\n1. 웹 서비스 (SaaS, 랜딩페이지)\n2. 모바일 앱 백엔드\n3. AI/ML 서비스 (챗봇, 모델 서빙)\n4. 게임 서버\n5. Discord/Telegram 봇\n6. 자동화 서버 (n8n, 크롤링)\n7. 미디어 스트리밍\n8. 개발/테스트 환경\n9. 데이터베이스 서버\n10. 기타 (직접 입력)\n\n번호나 용도를 말씀해주세요!';
|
||||
}
|
||||
|
||||
@@ -836,7 +643,7 @@ export async function processServerConsultation(
|
||||
// 명확히 다른 기능 요청인 경우 세션 종료하고 일반 처리로 전환
|
||||
const unrelatedPatterns = /기억|날씨|계산|검색|도메인|입금|충전|잔액|시간|문서/;
|
||||
if (unrelatedPatterns.test(userMessage)) {
|
||||
await deleteServerSession(db, session.user_id);
|
||||
await sessionManager.delete(db, session.user_id);
|
||||
logger.info('무관한 요청으로 세션 자동 종료', {
|
||||
userId: session.user_id,
|
||||
message: userMessage.slice(0, 30)
|
||||
@@ -867,7 +674,7 @@ export async function processServerConsultation(
|
||||
|
||||
// Mark session as ordering
|
||||
session.status = 'ordering';
|
||||
await saveServerSession(db, session);
|
||||
await sessionManager.save(db, session);
|
||||
|
||||
// 주문 확인 세션 저장 (텍스트 기반 확인)
|
||||
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
||||
@@ -942,7 +749,7 @@ export async function processServerConsultation(
|
||||
|
||||
// Mark session as recommending
|
||||
session.status = SERVER_CONSULTATION_STATUS.RECOMMENDING;
|
||||
await saveServerSession(db, session);
|
||||
await sessionManager.save(db, session);
|
||||
|
||||
// 1. Call recommendation API (추천 먼저 받기)
|
||||
logger.info('추천 API 호출', { collectedInfo: session.collected_info });
|
||||
@@ -1064,7 +871,7 @@ export async function processServerConsultation(
|
||||
|
||||
// Mark session as selecting (사용자 선택 대기)
|
||||
session.status = SERVER_CONSULTATION_STATUS.SELECTING;
|
||||
await saveServerSession(db, session);
|
||||
await sessionManager.save(db, session);
|
||||
|
||||
// 4. 추천 결과 + AI 검토 코멘트 (검토 코멘트는 마지막에)
|
||||
// __DIRECT__ 마커가 앞에 와야 제대로 처리됨
|
||||
@@ -1072,14 +879,14 @@ export async function processServerConsultation(
|
||||
} else {
|
||||
// 추천 결과 없음 - 세션 삭제
|
||||
session.status = SERVER_CONSULTATION_STATUS.COMPLETED;
|
||||
await deleteServerSession(db, session.user_id);
|
||||
await sessionManager.delete(db, session.user_id);
|
||||
|
||||
return `${aiResult.message}\n\n조건에 맞는 서버를 찾지 못했습니다.`;
|
||||
}
|
||||
} else {
|
||||
// Continue gathering information
|
||||
session.status = SERVER_CONSULTATION_STATUS.GATHERING;
|
||||
await saveServerSession(db, session);
|
||||
await sessionManager.save(db, session);
|
||||
|
||||
return aiResult.message;
|
||||
}
|
||||
@@ -1088,7 +895,7 @@ export async function processServerConsultation(
|
||||
|
||||
// Clean up session on error (if exists)
|
||||
try {
|
||||
await deleteServerSession(db, userId);
|
||||
await sessionManager.delete(db, userId);
|
||||
} catch (deleteError) {
|
||||
logger.error('세션 삭제 실패 (무시)', deleteError as Error, { userId });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user