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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown>,
|
||||
env?: Env
|
||||
): Promise<unknown> {
|
||||
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<RecommendResponse | null> {
|
||||
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<string, unknown> = {
|
||||
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<string, unknown> = {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user