From 98002473f9aff44a092381ebb81feb3174ec3e6d Mon Sep 17 00:00:00 2001 From: kappa Date: Thu, 5 Feb 2026 18:30:08 +0900 Subject: [PATCH] refactor: remove recommendation actions from server-tool Removed: - start_consultation, continue_consultation, cancel_consultation, recommend actions - getRecommendationData(), formatRecommendations() - CDN cache hit rate estimation (CDN_CACHE_HIT_RATES, estimateCdnCacheHitRate) - Language detection (detectLanguage) - callCloudOrchestratorApi() function - isErrorResult() type guard - Recommendation-related parameters (tech_stack, expected_users, use_case, traffic_pattern, region_preference, budget_limit, lang, message) - RecommendResponse type import - formatTrafficInfo import Retained: - order, list, info, delete, images, start, stop, reboot, rename actions - callProvisionAPI() for provision operations - Server management core functionality File size reduced from 1484 to 1057 lines (427 lines removed). Co-Authored-By: Claude Opus 4.5 --- src/tools/server-tool.ts | 434 +-------------------------------------- 1 file changed, 4 insertions(+), 430 deletions(-) diff --git a/src/tools/server-tool.ts b/src/tools/server-tool.ts index 79cd861..6b54fe9 100644 --- a/src/tools/server-tool.ts +++ b/src/tools/server-tool.ts @@ -3,12 +3,10 @@ import type { ProvisionResponse, ProvisionOrder, OSImage, - RecommendResponse } from '../types'; import { retryWithBackoff, RetryError } from '../utils/retry'; import { createLogger, maskUserId } from '../utils/logger'; import { ERROR_MESSAGES } from '../constants/messages'; -import { formatTrafficInfo } from '../utils/formatters'; const logger = createLogger('server-tool'); const provisionLogger = createLogger('provision'); @@ -21,55 +19,6 @@ function generateIdempotencyKey(userId: string): string { return `tg-order-${userId}-${timestamp}-${random}`; } -// CDN 캐시 히트율 상수 -const CDN_CACHE_HIT_RATES = { - VIDEO_STREAMING: 0.92, - STATIC_SITE: 0.95, - API: 0.30, - ECOMMERCE: 0.70, - DEFAULT: 0.85, -} as const; - -// 언어 감지 (한글/일본어/중국어/영어) -function detectLanguage(text: string): 'ko' | 'ja' | 'zh' | 'en' { - if (/[가-힣]/.test(text)) return 'ko'; - if (/[\u3040-\u309f\u30a0-\u30ff]/.test(text)) return 'ja'; // 히라가나/가타카나 - if (/[\u4e00-\u9fff]/.test(text)) return 'zh'; // 한자 (일본어 감지 후) - return 'en'; -} - -// CDN 캐시 히트율 추정 (tech_stack + use_case 기반) -function estimateCdnCacheHitRate(techStack: string[], useCase: string): number | null { - const stackLower = techStack.map(s => s.toLowerCase()); - const useCaseLower = useCase.toLowerCase(); - - // CDN 키워드 감지 - const hasCdn = stackLower.some(s => - ['cloudflare', 'cdn', 'fastly', 'akamai', 'bunny', 'cf'].includes(s) - ); - - if (!hasCdn) return null; // CDN 없으면 null 반환 - - // use_case 기반 히트율 조정 - const isVideoStreaming = /video|streaming|vod|media|동영상|스트리밍|미디어/.test(useCaseLower); - const isStaticSite = /static|blog|portfolio|landing|정적|블로그/.test(useCaseLower); - const isApi = /api|backend|서버|백엔드/.test(useCaseLower); - const isEcommerce = /shop|store|commerce|쇼핑|이커머스/.test(useCaseLower); - - // 콘텐츠 타입별 예상 캐시 히트율 - if (isVideoStreaming) return CDN_CACHE_HIT_RATES.VIDEO_STREAMING; - if (isStaticSite) return CDN_CACHE_HIT_RATES.STATIC_SITE; - if (isApi) return CDN_CACHE_HIT_RATES.API; - if (isEcommerce) return CDN_CACHE_HIT_RATES.ECOMMERCE; - - return CDN_CACHE_HIT_RATES.DEFAULT; -} - -// Type guards -function isErrorResult(result: unknown): result is { error: string } { - return typeof result === 'object' && result !== null && 'error' in result; -} - // 진행 중인 주문 확인 (중복 주문 방지) async function checkExistingOrder( db: D1Database, @@ -89,47 +38,14 @@ export const manageServerTool = { type: 'function', function: { name: 'manage_server', - description: '클라우드 서버 관리. 반드시 사용: 서버 시작(action="start", order_id), 서버 중지(action="stop", order_id), 서버 재시작(action="reboot", order_id), 서버 삭제/해지(action="delete", order_id - 만료일과 무관하게 즉시 삭제 가능), 내 서버 목록(action="list"), 서버 추천(action="start_consultation"), 서버 이름 변경(action="rename"). "N번 시작/중지/재시작/삭제/해지/취소", "#N 시작/재시작" 패턴 감지 시 반드시 호출.', + description: '클라우드 서버 관리. 반드시 사용: 서버 시작(action="start", order_id), 서버 중지(action="stop", order_id), 서버 재시작(action="reboot", order_id), 서버 삭제/해지(action="delete", order_id - 만료일과 무관하게 즉시 삭제 가능), 내 서버 목록(action="list"), 서버 이름 변경(action="rename"). "N번 시작/중지/재시작/삭제/해지/취소", "#N 시작/재시작" 패턴 감지 시 반드시 호출.', parameters: { type: 'object', properties: { action: { type: 'string', - enum: ['recommend', 'order', 'list', 'info', 'delete', 'images', 'start', 'stop', 'reboot', - 'start_consultation', 'continue_consultation', 'cancel_consultation', 'rename'], - description: 'start: 서버 시작, stop: 서버 중지, reboot: 서버 재시작, delete: 서버 삭제, list: 내 서버 목록, info: 서버 상세, start_consultation: 상담 시작, rename: 이름 변경', - }, - tech_stack: { - type: 'array', - items: { type: 'string' }, - description: '기술 스택. 용도에서 추론 (블로그→wordpress, 쇼핑몰→ecommerce, 커뮤니티→php,mysql). 모르면 ["web"]', - }, - expected_users: { - type: 'number', - description: '예상 사용자 수. 모르면 개인용=100, 사업용=500 사용', - }, - use_case: { - type: 'string', - description: '용도 (예: "블로그", "쇼핑몰", "커뮤니티")', - }, - traffic_pattern: { - type: 'string', - enum: ['steady', 'spiky', 'growing'], - description: '생략 가능. 기본값: steady', - }, - region_preference: { - type: 'array', - items: { type: 'string' }, - description: '선호 리전 (예: ["tokyo", "seoul"]). recommend action에서 선택', - }, - budget_limit: { - type: 'number', - description: '월 예산 한도 (원). recommend action에서 선택', - }, - lang: { - type: 'string', - enum: ['ko', 'ja', 'zh', 'en'], - description: '응답 언어. 자동 감지됨', + enum: ['order', 'list', 'info', 'delete', 'images', 'start', 'stop', 'reboot', 'rename'], + description: 'start: 서버 시작, stop: 서버 중지, reboot: 서버 재시작, delete: 서버 삭제, list: 내 서버 목록, info: 서버 상세, order: 서버 주문, rename: 이름 변경, images: OS 이미지 목록', }, server_id: { type: 'string', @@ -143,17 +59,13 @@ export const manageServerTool = { type: 'string', description: '서버 라벨 (예: "myapp-prod"). order action에서 필수', }, - message: { - type: 'string', - description: '사용자 메시지. continue_consultation action에서 필수', - }, pricing_id: { type: 'number', description: 'Pricing ID (서버 스펙 ID). order action에서 필수', }, order_id: { type: 'number', - description: '주문 번호. info, delete, rename action에서 필수', + description: '주문 번호. info, delete, rename, start, stop, reboot action에서 필수', }, new_label: { type: 'string', @@ -169,65 +81,6 @@ export const manageServerTool = { }, }; -// Cloud Orchestrator API 호출 -async function callCloudOrchestratorApi( - endpoint: string, - method: string, - body?: Record, - env?: Env -): Promise { - logger.info('API 호출 시작', { - endpoint, - method, - useServiceBinding: !!env?.CLOUD_ORCHESTRATOR - }); - - try { - const response = await retryWithBackoff( - () => { - const requestInit = { - method, - headers: { - 'Content-Type': 'application/json', - }, - body: body ? JSON.stringify(body) : undefined, - }; - - // Service Binding 우선, fallback: URL - if (env?.CLOUD_ORCHESTRATOR) { - logger.info('Service Binding 사용', { endpoint }); - return env.CLOUD_ORCHESTRATOR.fetch(`https://internal${endpoint}`, requestInit); - } else { - const apiUrl = env?.CLOUD_ORCHESTRATOR_URL || 'https://cloud-orchestrator.kappa-d8e.workers.dev'; - const url = `${apiUrl}${endpoint}`; - logger.info('HTTP 요청 사용', { url }); - return fetch(url, requestInit); - } - }, - { maxRetries: 3, serviceName: 'cloud-orchestrator' } - ); - - if (!response.ok) { - const errorText = await response.text(); - logger.error('API 호출 실패', new Error(errorText), { - endpoint, - status: response.status, - }); - return { error: `서버 API 호출 실패: HTTP ${response.status}` }; - } - - const data = await response.json(); - logger.info('API 호출 성공', { endpoint }); - return data; - } catch (error) { - logger.error('API 호출 에러', error as Error, { endpoint }); - if (error instanceof RetryError) { - return { error: ERROR_MESSAGES.SERVER_SERVICE_UNAVAILABLE }; - } - return { error: '서버 API 호출 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }; - } -} - // Cloud Orchestrator Provision API 호출 async function callProvisionAPI( endpoint: string, @@ -328,107 +181,6 @@ async function callProvisionAPI( } } -// 추천 데이터 조회 (포맷팅 없이 원본 반환) -export async function getRecommendationData( - args: { - tech_stack: string[]; - expected_users: number; - use_case: string; - traffic_pattern?: string; - region_preference?: string[]; - budget_limit?: number; - lang?: string; - }, - env?: Env -): Promise { - const { tech_stack, expected_users, use_case, traffic_pattern, region_preference, budget_limit, lang } = args; - - // 언어 자동 감지 - const detectedLang = lang || detectLanguage(use_case); - - // CDN 캐시 히트율 추정 - const cdnCacheHitRate = estimateCdnCacheHitRate(tech_stack, use_case); - - // API 요청 body 구성 - const requestBody: Record = { - tech_stack, - expected_users, - use_case, - lang: detectedLang, - }; - - if (traffic_pattern) requestBody.traffic_pattern = traffic_pattern; - if (region_preference) requestBody.region_preference = region_preference; - if (budget_limit) requestBody.budget_limit = budget_limit; - if (cdnCacheHitRate !== null) requestBody.cdn_cache_hit_rate = cdnCacheHitRate; - - // API 호출 - const result = await callCloudOrchestratorApi('/api/recommend', 'POST', requestBody, env); - - if (isErrorResult(result)) { - logger.error('추천 데이터 조회 실패', new Error(result.error)); - return null; - } - - return result as RecommendResponse; -} - -// 서버 추천 결과 포맷팅 (필수 정보만) -// __DIRECT__ 마커로 AI 재해석 없이 바로 반환 -function formatRecommendations(data: RecommendResponse): string { - if (!data.recommendations || data.recommendations.length === 0) { - return '🔍 조건에 맞는 서버를 찾지 못했습니다.\n다른 조건으로 다시 시도해주세요.'; - } - - const { recommendations } = data; - - // __DIRECT__ 마커 추가 - AI 재해석 방지 - let response = '__DIRECT__\n🖥️ 서버 추천 결과\n\n'; - - recommendations.slice(0, 3).forEach((rec, index) => { - const server = rec.server; - - // 가격 포맷팅 (항상 KRW로 표시) - const price = `₩${Math.round(server.monthly_price).toLocaleString()}`; - - // 대역폭 포맷팅 - const bandwidth = server.transfer_tb ? `${server.transfer_tb}TB` : '무제한'; - - response += `${index + 1}️⃣ ${server.instance_name} (${server.provider_name})\n`; - response += ` • 스펙: ${server.vcpu}vCPU / ${server.memory_gb}GB / ${server.storage_gb}GB SSD\n`; - response += ` • 리전: ${server.region_name} (${server.country_code})\n`; - response += ` • 가격: ${price}/월 (대역폭 ${bandwidth})\n`; - - // 대역폭 정보 (항상 표시) - if (rec.bandwidth_info) { - const trafficInfo = formatTrafficInfo(rec.bandwidth_info); - response += ` • ${trafficInfo}\n`; - - // 총 예상 비용 (초과 있을 때만 표시, 항상 KRW로 표시) - if (rec.bandwidth_info.estimated_overage_tb > 0 && rec.bandwidth_info.estimated_overage_cost > 0) { - const totalCost = `₩${Math.round(rec.bandwidth_info.total_estimated_cost).toLocaleString()}`; - response += ` • 총 예상 비용: ${totalCost}/월\n`; - } - } - - response += ` • 점수: ${rec.score}점`; - if (rec.estimated_capacity) { - response += ` / 최대 ${rec.estimated_capacity.max_concurrent_users.toLocaleString()}명\n`; - } else { - response += '\n'; - } - response += '\n'; - }); - - // 환불 정책 안내 추가 - response += '\n💡 환불 정책: 720시간 = 1개월 기준\n'; - response += ' 해지 시 미사용 시간은 시간당 요금으로 환불\n\n'; - - // 선택 가이드 추가 - response += '💡 원하는 서버를 선택하려면 번호를 입력하세요 (예: 1번)'; - - return response.trim(); -} // 서버 상태 이모지 function getStatusEmoji(status: string): string { @@ -636,17 +388,9 @@ const DEPOSIT_ACCOUNT_INFO = `💳 입금 계좌 export async function executeServerAction( action: string, args: { - tech_stack?: string[]; - expected_users?: number; - use_case?: string; - traffic_pattern?: string; - region_preference?: string[]; - budget_limit?: number; - lang?: string; server_id?: string; region_code?: string; label?: string; - message?: string; pricing_id?: number; order_id?: number; new_label?: string; @@ -662,168 +406,6 @@ export async function executeServerAction( }); switch (action) { - case 'start_consultation': { - if (!telegramUserId) { - return '🚫 사용자 인증이 필요합니다.'; - } - - if (!env?.DB) { - return '🚫 세션 저장소가 설정되지 않았습니다.'; - } - - // Note: Session is created automatically in processServerConsultation when first message arrives - logger.info('상담 시작 요청', { userId: maskUserId(telegramUserId) }); - - return '안녕하세요! 서버 추천을 도와드리겠습니다. 😊\n\n어떤 서비스를 운영하실 건가요?\n\n1. 웹 서비스 (SaaS, 랜딩페이지)\n2. 모바일 앱 백엔드\n3. AI/ML 서비스 (챗봇, 모델 서빙)\n4. 게임 서버\n5. Discord/Telegram 봇\n6. 자동화 서버 (n8n, 크롤링)\n7. 미디어 스트리밍\n8. 개발/테스트 환경\n9. 데이터베이스 서버\n10. 기타 (직접 입력)\n\n번호나 용도를 말씀해주세요!'; - } - - case 'continue_consultation': { - const { processServerConsultation } = await import('../agents/server-agent'); - - if (!telegramUserId) { - return '🚫 사용자 인증이 필요합니다.'; - } - - if (!env?.DB) { - return '🚫 세션 저장소가 설정되지 않았습니다.'; - } - - if (!args.message) { - return '🚫 메시지가 필요합니다.'; - } - - const result = await processServerConsultation(env.DB, telegramUserId, args.message, env); - return result; - } - - case 'cancel_consultation': { - if (!telegramUserId) { - return '🚫 사용자 인증이 필요합니다.'; - } - - if (!env?.DB) { - return '🚫 세션 저장소가 설정되지 않았습니다.'; - } - - // Import sessionManager to delete session - const { ServerSessionManager } = await import('../utils/session-manager'); - const { getSessionConfig } = await import('../constants/agent-config'); - const sessionManager = new ServerSessionManager(getSessionConfig('server')); - await sessionManager.delete(env.DB, telegramUserId); - - logger.info('상담 세션 취소', { userId: maskUserId(telegramUserId) }); - - return '상담이 취소되었습니다. 다시 시작하려면 "서버 추천"이라고 말씀해주세요.'; - } - - case 'recommend': { - const { tech_stack, expected_users, use_case, traffic_pattern, region_preference, budget_limit, lang } = args; - - // 필수 파라미터 검증 - if (!tech_stack || !expected_users || !use_case) { - return '🚫 서버 추천에는 tech_stack, expected_users, use_case가 필요합니다.'; - } - - // 언어 자동 감지 (use_case 기반) - const detectedLang = lang || detectLanguage(use_case); - - // CDN 캐시 히트율 추정 - const cdnCacheHitRate = estimateCdnCacheHitRate(tech_stack, use_case); - - // API 요청 body 구성 - const requestBody: Record = { - tech_stack, - expected_users, - use_case, - lang: detectedLang, - }; - - if (traffic_pattern) requestBody.traffic_pattern = traffic_pattern; - if (region_preference) requestBody.region_preference = region_preference; - if (budget_limit) requestBody.budget_limit = budget_limit; - if (cdnCacheHitRate !== null) requestBody.cdn_cache_hit_rate = cdnCacheHitRate; - - // API 호출 - const result = await callCloudOrchestratorApi('/api/recommend', 'POST', requestBody, env); - - if (isErrorResult(result)) { - return `🚫 ${result.error}`; - } - - const recommendationData = result as RecommendResponse; - - // 세션에 추천 결과 저장 (선택 기능 활성화) - if (telegramUserId && env?.DB && recommendationData.recommendations && recommendationData.recommendations.length > 0) { - try { - const { ServerSessionManager } = await import('../utils/session-manager'); - const { getSessionConfig } = await import('../constants/agent-config'); - const sessionManager = new ServerSessionManager(getSessionConfig('server')); - - // 기존 세션 조회 또는 새로 생성 - let session = await sessionManager.get(env.DB, telegramUserId); - - if (!session) { - // 세션이 없으면 새로 생성 - session = sessionManager.create(telegramUserId, 'selecting'); - session.collected_info = { - useCase: use_case, - scale: expected_users <= 50 ? 'personal' : 'business', - expectedConcurrent: expected_users, - }; - logger.info('새 세션 생성 (추천 결과 저장용)', { userId: telegramUserId }); - } - - // last_recommendation 저장 - session.last_recommendation = { - recommendations: recommendationData.recommendations.slice(0, 3).map(rec => ({ - pricing_id: rec.server.id, - plan_name: rec.server.instance_name, - provider: rec.server.provider_name, - specs: { - vcpu: rec.server.vcpu, - ram_gb: rec.server.memory_gb, - storage_gb: rec.server.storage_gb - }, - region: { - code: rec.server.region_code, - name: rec.server.region_name - }, - price: { - monthly_krw: Math.round(rec.server.monthly_price), - bandwidth_tb: rec.server.transfer_tb, - estimated_monthly_tb: rec.bandwidth_info?.estimated_monthly_tb, - gross_monthly_tb: rec.bandwidth_info?.gross_monthly_tb, - cdn_cache_hit_rate: rec.bandwidth_info?.cdn_cache_hit_rate, - overage_tb: rec.bandwidth_info?.estimated_overage_tb, - overage_cost_krw: rec.bandwidth_info?.estimated_overage_cost, - currency: rec.server.currency, - }, - score: rec.score, - max_users: rec.estimated_capacity?.max_concurrent_users || 0 - })), - created_at: Date.now() - }; - - // status를 'selecting'으로 변경 - session.status = 'selecting'; - session.updated_at = Date.now(); - - await sessionManager.save(env.DB, session); - logger.info('추천 결과 세션 저장 완료', { - userId: telegramUserId, - recommendationCount: session.last_recommendation.recommendations.length, - status: session.status - }); - } catch (sessionError) { - logger.error('세션 저장 실패 (무시)', sessionError as Error, { userId: telegramUserId }); - // 세션 저장 실패해도 추천 결과는 반환 - } - } - - // 결과 포맷팅 - return formatRecommendations(recommendationData); - } - case 'order': { const { pricing_id, label, image } = args; @@ -1447,17 +1029,9 @@ export async function executeServerOrder( export async function executeManageServer( args: { action: string; - tech_stack?: string[]; - expected_users?: number; - use_case?: string; - traffic_pattern?: string; - region_preference?: string[]; - budget_limit?: number; - lang?: string; server_id?: string; region_code?: string; label?: string; - message?: string; pricing_id?: number; order_id?: number; new_label?: string;