diff --git a/src/agents/domain-agent.ts b/src/agents/domain-agent.ts new file mode 100644 index 0000000..0e4b159 --- /dev/null +++ b/src/agents/domain-agent.ts @@ -0,0 +1,212 @@ +/** + * Domain Agent - 도메인 추천 상담 시스템 + * + * 기능: + * - 대화형 도메인 추천 상담 + * - 세션 기반 정보 수집 (키워드, 용도, 예산) + * - 충분한 정보 수집 시 자동 추천 + * - 추천 후 사용자 선택 및 등록 흐름 + */ + +import type { Env, DomainSession, DomainSessionStatus } from '../types'; +import { createLogger } from '../utils/logger'; + +const logger = createLogger('domain-agent'); + +// D1 Session Management +const DOMAIN_SESSION_TTL = 60 * 60 * 1000; // 1시간 (도메인 작업은 시간이 더 필요) +const MAX_MESSAGES = 20; // 세션당 최대 메시지 수 + +/** + * D1에서 도메인 세션 조회 + * + * @param db - D1 Database + * @param userId - Telegram User ID + * @returns DomainSession 또는 null (세션 없거나 만료) + */ +export async function getDomainSession( + db: D1Database, + userId: string +): Promise { + try { + const now = Date.now(); + const result = await db.prepare( + 'SELECT * FROM domain_sessions WHERE user_id = ? AND expires_at > ?' + ).bind(userId, now).first<{ + user_id: string; + status: string; + collected_info: string | null; + target_domain: string | null; + messages: string | null; + created_at: number; + updated_at: number; + expires_at: number; + }>(); + + if (!result) { + logger.info('도메인 세션 없음', { userId }); + return null; + } + + const session: DomainSession = { + user_id: result.user_id, + status: result.status as DomainSessionStatus, + collected_info: result.collected_info ? JSON.parse(result.collected_info) : {}, + target_domain: result.target_domain || 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 }); + return session; + } catch (error) { + logger.error('도메인 세션 조회 실패', error as Error, { userId }); + return null; + } +} + +/** + * 도메인 세션 저장 (생성 또는 업데이트) + * + * @param db - D1 Database + * @param session - DomainSession + */ +export async function saveDomainSession( + db: D1Database, + session: DomainSession +): Promise { + try { + const now = Date.now(); + const expiresAt = now + DOMAIN_SESSION_TTL; + + await db.prepare(` + INSERT INTO domain_sessions + (user_id, status, collected_info, target_domain, messages, created_at, updated_at, expires_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user_id) DO UPDATE SET + status = excluded.status, + collected_info = excluded.collected_info, + target_domain = excluded.target_domain, + 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.target_domain || 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 deleteDomainSession( + db: D1Database, + userId: string +): Promise { + try { + await db.prepare('DELETE FROM domain_sessions WHERE user_id = ?') + .bind(userId) + .run(); + logger.info('도메인 세션 삭제 성공', { userId }); + } catch (error) { + logger.error('도메인 세션 삭제 실패', error as Error, { userId }); + throw error; + } +} + +/** + * 새 도메인 세션 생성 + * + * @param userId - Telegram User ID + * @param status - 세션 상태 + * @returns 새로운 DomainSession 객체 + */ +export function createDomainSession( + userId: string, + status: DomainSessionStatus = 'gathering' +): DomainSession { + const now = Date.now(); + return { + user_id: userId, + status, + collected_info: {}, + messages: [], + created_at: now, + updated_at: now, + expires_at: now + DOMAIN_SESSION_TTL, + }; +} + +/** + * 세션 만료 여부 확인 + * + * @param session - DomainSession + * @returns true if expired, false otherwise + */ +export function isSessionExpired(session: DomainSession): boolean { + return session.expires_at < Date.now(); +} + +/** + * 세션에 메시지 추가 + * + * @param session - DomainSession + * @param role - 메시지 역할 ('user' | 'assistant') + * @param content - 메시지 내용 + */ +export function addMessageToSession( + session: DomainSession, + role: 'user' | 'assistant', + content: string +): void { + session.messages.push({ role, content }); + + // 최대 메시지 수 제한 + if (session.messages.length > MAX_MESSAGES) { + session.messages = session.messages.slice(-MAX_MESSAGES); + logger.warn('세션 메시지 최대 개수 초과, 오래된 메시지 제거', { + userId: session.user_id, + maxMessages: MAX_MESSAGES, + }); + } +} + +/** + * 도메인 추천 상담 처리 (메인 함수) + * + * @param db - D1 Database + * @param userId - Telegram User ID + * @param userMessage - 사용자 메시지 + * @param env - Environment + * @returns AI 응답 메시지 + */ +export async function processDomainConsultation( + db: D1Database, + userId: string, + userMessage: string, + env: Env +): Promise { + // TODO: Implement in Task 8 + logger.info('도메인 상담 처리 요청 (미구현)', { + userId, + message: userMessage.slice(0, 50), + }); + return '__PASSTHROUGH__'; +} diff --git a/src/tools/server-tool.ts b/src/tools/server-tool.ts index e894698..e870d88 100644 --- a/src/tools/server-tool.ts +++ b/src/tools/server-tool.ts @@ -1343,7 +1343,7 @@ export async function executeServerDelete( // Clear server consultation session (if any) try { - const { deleteServerSession } = await import('../server-agent'); + const { deleteServerSession } = await import('../agents/server-agent'); await deleteServerSession(env.DB, telegramUserId); } catch (error) { provisionLogger.error('서버 세션 삭제 실패 (무시)', error as Error); @@ -1432,7 +1432,7 @@ export async function executeServerOrder( // Clear server consultation session try { - const { deleteServerSession } = await import('../server-agent'); + const { deleteServerSession } = await import('../agents/server-agent'); await deleteServerSession(env.DB, telegramUserId); } catch (error) { provisionLogger.error('서버 세션 삭제 실패 (무시)', error as Error);