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:
kappa
2026-02-05 11:44:06 +09:00
parent ffd310c903
commit edf1bbc9a2
4 changed files with 57 additions and 819 deletions

View File

@@ -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 });
}