refactor: unify server-agent to new pattern
- Change field names to snake_case (user_id, collected_info, last_recommendation, created_at, updated_at, expires_at) - Extract system prompts to constants (SERVER_EXPERT_PROMPT, SERVER_REVIEW_PROMPT) - Add __PASSTHROUGH__/__SESSION_END__ marker support - Change handler signature to match other agents (db, userId, userMessage, env, options) - Add helper functions for consistency (createServerSession, isSessionExpired, addMessageToSession, hasServerSession) - Update saveSe rverSession signature to not need userId separately - Rename tools constant from serverExpertTools to serverExpertTools (camelCase) - Change AI call parameter order for consistency - Add performance logging - Update openai-service.ts routing to use hasServerSession - Update server-tool.ts to use new session creation helpers - Update message-handler.ts and api/chat.ts field references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,19 @@
|
|||||||
*
|
*
|
||||||
* 기능:
|
* 기능:
|
||||||
* - 대화형 서버 추천 상담
|
* - 대화형 서버 추천 상담
|
||||||
* - 세션 기반 정보 수집
|
* - 세션 기반 정보 수집 (D1)
|
||||||
* - 충분한 정보 수집 시 자동 추천
|
* - 충분한 정보 수집 시 자동 추천
|
||||||
* - 추천 후 사용자 선택 및 주문 흐름
|
* - 추천 후 사용자 선택 및 주문 흐름
|
||||||
* - Brave Search / Context7 도구로 최신 트렌드 반영
|
* - Brave Search / Context7 도구로 최신 트렌드 반영
|
||||||
|
*
|
||||||
|
* Manual Test:
|
||||||
|
* 1. User: "서버 추천"
|
||||||
|
* 2. Expected: Category detection → 1-2 questions → Recommendation
|
||||||
|
* 3. User: "1번"
|
||||||
|
* 4. Expected: Order confirmation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Env, ServerSession, BandwidthInfo, RecommendResponse } from '../types';
|
import type { Env, ServerSession, ServerSessionStatus, BandwidthInfo, RecommendResponse } from '../types';
|
||||||
import { createLogger } from '../utils/logger';
|
import { createLogger } from '../utils/logger';
|
||||||
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
import { executeSearchWeb, executeLookupDocs } from '../tools/search-tool';
|
||||||
import { formatTrafficInfo } from '../utils/formatters';
|
import { formatTrafficInfo } from '../utils/formatters';
|
||||||
@@ -18,8 +24,85 @@ import { SERVER_CONSULTATION_STATUS, LANGUAGE_CODE } from '../constants';
|
|||||||
const logger = createLogger('server-agent');
|
const logger = createLogger('server-agent');
|
||||||
|
|
||||||
// D1 Session Management
|
// D1 Session Management
|
||||||
const SESSION_TTL_MS = 3600 * 1000; // 1 hour in milliseconds
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서버 세션 존재 여부 확인 (라우팅용)
|
||||||
|
*
|
||||||
|
* @param db - D1 Database
|
||||||
|
* @param userId - Telegram User ID
|
||||||
|
* @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(
|
export async function getServerSession(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
userId: string
|
userId: string
|
||||||
@@ -36,6 +119,7 @@ export async function getServerSession(
|
|||||||
messages: string | null;
|
messages: string | null;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
|
expires_at: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -44,16 +128,17 @@ export async function getServerSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session: ServerSession = {
|
const session: ServerSession = {
|
||||||
telegramUserId: result.user_id,
|
user_id: result.user_id,
|
||||||
status: result.status as ServerSession['status'],
|
status: result.status as ServerSessionStatus,
|
||||||
collectedInfo: result.collected_info ? JSON.parse(result.collected_info) : {},
|
collected_info: result.collected_info ? JSON.parse(result.collected_info) : {},
|
||||||
lastRecommendation: result.last_recommendation ? JSON.parse(result.last_recommendation) : undefined,
|
last_recommendation: result.last_recommendation ? JSON.parse(result.last_recommendation) : undefined,
|
||||||
messages: result.messages ? JSON.parse(result.messages) : [],
|
messages: result.messages ? JSON.parse(result.messages) : [],
|
||||||
createdAt: result.created_at,
|
created_at: result.created_at,
|
||||||
updatedAt: result.updated_at,
|
updated_at: result.updated_at,
|
||||||
|
expires_at: result.expires_at,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info('세션 조회 성공', { userId, status: session.status, hasLastRecommendation: !!session.lastRecommendation });
|
logger.info('세션 조회 성공', { userId, status: session.status, hasLastRecommendation: !!session.last_recommendation });
|
||||||
return session;
|
return session;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('세션 조회 실패', error as Error, { userId });
|
logger.error('세션 조회 실패', error as Error, { userId });
|
||||||
@@ -61,14 +146,19 @@ export async function getServerSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서버 세션 저장 (생성 또는 업데이트)
|
||||||
|
*
|
||||||
|
* @param db - D1 Database
|
||||||
|
* @param session - ServerSession
|
||||||
|
*/
|
||||||
export async function saveServerSession(
|
export async function saveServerSession(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
userId: string,
|
|
||||||
session: ServerSession
|
session: ServerSession
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expiresAt = now + SESSION_TTL_MS;
|
const expiresAt = now + SERVER_SESSION_TTL_MS;
|
||||||
|
|
||||||
await db.prepare(`
|
await db.prepare(`
|
||||||
INSERT INTO server_sessions
|
INSERT INTO server_sessions
|
||||||
@@ -82,23 +172,29 @@ export async function saveServerSession(
|
|||||||
updated_at = excluded.updated_at,
|
updated_at = excluded.updated_at,
|
||||||
expires_at = excluded.expires_at
|
expires_at = excluded.expires_at
|
||||||
`).bind(
|
`).bind(
|
||||||
userId,
|
session.user_id,
|
||||||
session.status,
|
session.status,
|
||||||
JSON.stringify(session.collectedInfo || {}),
|
JSON.stringify(session.collected_info || {}),
|
||||||
session.lastRecommendation ? JSON.stringify(session.lastRecommendation) : null,
|
session.last_recommendation ? JSON.stringify(session.last_recommendation) : null,
|
||||||
JSON.stringify(session.messages || []),
|
JSON.stringify(session.messages || []),
|
||||||
session.createdAt || now,
|
session.created_at || now,
|
||||||
now,
|
now,
|
||||||
expiresAt
|
expiresAt
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
logger.info('세션 저장 성공', { userId, status: session.status });
|
logger.info('세션 저장 성공', { userId: session.user_id, status: session.status });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('세션 저장 실패', error as Error, { userId });
|
logger.error('세션 저장 실패', error as Error, { userId: session.user_id });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서버 세션 삭제
|
||||||
|
*
|
||||||
|
* @param db - D1 Database
|
||||||
|
* @param userId - Telegram User ID
|
||||||
|
*/
|
||||||
export async function deleteServerSession(
|
export async function deleteServerSession(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
userId: string
|
userId: string
|
||||||
@@ -114,6 +210,12 @@ export async function deleteServerSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 만료된 서버 세션 정리 (Cron 또는 수동 실행)
|
||||||
|
*
|
||||||
|
* @param db - D1 Database
|
||||||
|
* @returns 삭제된 세션 개수
|
||||||
|
*/
|
||||||
export async function cleanupExpiredSessions(db: D1Database): Promise<number> {
|
export async function cleanupExpiredSessions(db: D1Database): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const result = await db.prepare(
|
const result = await db.prepare(
|
||||||
@@ -131,6 +233,92 @@ export async function cleanupExpiredSessions(db: D1Database): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server Expert System Prompts
|
||||||
|
const SERVER_EXPERT_PROMPT = `당신은 30년 경력의 시니어 클라우드 아키텍트입니다.
|
||||||
|
|
||||||
|
## 전문성 (30년 경력)
|
||||||
|
- 서버 엔지니어: Linux, Windows Server, 가상화, 컨테이너 마스터
|
||||||
|
- 네트워크 엔지니어: 로드밸런싱, CDN, DNS, 보안 설계 전문
|
||||||
|
- 클라우드 아키텍트: 모든 클라우드 플랫폼 경험
|
||||||
|
- 수천 개의 서버 구축 경험으로 용도만 들으면 최적 스펙을 바로 판단 가능
|
||||||
|
|
||||||
|
## 성격
|
||||||
|
- 따뜻하고 친근하지만 전문적인 어조
|
||||||
|
- 비기술자도 이해하기 쉽게 설명
|
||||||
|
- 고객의 예산과 상황을 항상 배려
|
||||||
|
- 불필요한 기술 용어 사용 자제
|
||||||
|
|
||||||
|
## 금지 사항 (절대 위반 금지)
|
||||||
|
- AWS, GCP, Azure, Vultr, Linode, DigitalOcean 등 다른 클라우드 프로바이더 언급 금지
|
||||||
|
- 경쟁사 서비스 추천 금지
|
||||||
|
- 우리 서비스(Anvil)만 추천
|
||||||
|
- "다른 곳도 고려해보세요" 같은 멘트 금지
|
||||||
|
|
||||||
|
## 도구 사용 가이드 (적극적으로 활용할 것)
|
||||||
|
- 고객이 특정 프레임워크/기술을 언급하면 (예: Next.js, Laravel, Django, Astro, Bun, Rust 등) → 반드시 lookup_framework_docs 호출하여 최신 공식 권장 스펙 확인
|
||||||
|
- "최신", "트렌드", "2024", "2025", "요즘" 등 시의성 있는 키워드 → 반드시 search_trends 호출
|
||||||
|
- SaaS, 모바일 앱 백엔드 같은 일반적 용도는 경험으로 바로 답변
|
||||||
|
- 도구 결과를 자연스럽게 메시지에 포함 (예: "공식 문서에 따르면...")
|
||||||
|
|
||||||
|
## 대화 흐름
|
||||||
|
1. 용도 파악: "어떤 서비스를 운영하실 건가요? (예: SaaS, 앱 백엔드, AI 서비스)"
|
||||||
|
2. 규모 파악: "개인용인가요, 사업용인가요?"
|
||||||
|
3. 사용자 수 확인 (필요 시): "방문자나 사용자 수는 어느 정도 예상하시나요?"
|
||||||
|
4. 정보가 충분하면 즉시 추천 (추가 질문 없이)
|
||||||
|
|
||||||
|
## 핵심 규칙 (반드시 준수)
|
||||||
|
- 기술 스택, 트래픽 패턴은 절대 묻지 않음 (30년 경험으로 알아서 추론)
|
||||||
|
- 사용자 수를 언급하면 DAU인지 동시접속자인지 반드시 한 번 확인
|
||||||
|
- "방문자 1000명", "유저 500명" 등 언급 시 → "말씀하신 방문자는 일일 방문자(DAU)인가요, 동시접속자인가요?"
|
||||||
|
- DAU와 동시접속자를 구분해서 설명: "일반적으로 동시접속자는 일일 방문자의 5-10% 정도입니다"
|
||||||
|
- "모르겠어요", "아무거나", "글쎄요" → 즉시 action="recommend" (기본값: 개인용 웹서비스)
|
||||||
|
- 용도+규모 한번에 말하면 → 즉시 action="recommend"
|
||||||
|
- 용도만 말해도 → 개인용으로 가정하고 action="recommend" 가능
|
||||||
|
- 질문은 최대 2번까지, 그 이후는 무조건 action="recommend"
|
||||||
|
|
||||||
|
## 사용자 수 관련 용어 정리
|
||||||
|
- **DAU (일일 활성 사용자)**: 하루 동안 서비스를 사용하는 전체 사용자 수
|
||||||
|
- **동시접속자 (Concurrent Users)**: 같은 시간에 동시에 접속해 있는 사용자 수
|
||||||
|
- **중요**: 서버 스펙은 동시접속자를 기준으로 계산해야 합니다
|
||||||
|
- **일반 공식**: 동시접속자 = DAU × 5-10%
|
||||||
|
|
||||||
|
예시:
|
||||||
|
- "하루 방문자 1000명" → DAU 1000명 → 동시접속자 50-100명
|
||||||
|
- "동시 접속 100명" → 그대로 동시접속자 100명 사용
|
||||||
|
|
||||||
|
## 추론 규칙 (30년 경험 기반)
|
||||||
|
- 블로그 → WordPress, 1GB RAM이면 충분, DAU 100명 (동시접속자 10명)
|
||||||
|
- 쇼핑몰 → 2GB+ RAM, DB 분리 고려, DAU 500명 (동시접속자 50명)
|
||||||
|
- 커뮤니티 → PHP+MySQL, 트래픽에 따라 2~4GB
|
||||||
|
- 게임서버 → 고사양 CPU, 낮은 레이턴시 리전
|
||||||
|
- SaaS/B2B/Enterprise → 최소 4GB+ RAM, PostgreSQL+Redis 권장, 500명+ 동시접속 가정
|
||||||
|
- API 서버 → 트래픽에 따라 2~8GB, Redis 캐시 권장
|
||||||
|
- 실시간 서비스 (WebSocket) → 최소 4GB RAM, Redis 권장
|
||||||
|
- 고성능 DB (PostgreSQL, MongoDB) → 최소 4GB+ RAM, 높은 IOPS
|
||||||
|
- 규모: personal→DAU 100명 (동접 10명), business→DAU 500명 (동접 50명), SaaS→DAU 2000명 (동접 200명)
|
||||||
|
|
||||||
|
## 특수 지시
|
||||||
|
- 서버/호스팅과 무관한 메시지가 들어오면 반드시 "__PASSTHROUGH__"만 응답
|
||||||
|
- 상담 종료가 필요하면 "__SESSION_END__"를 응답 끝에 추가`;
|
||||||
|
|
||||||
|
const SERVER_REVIEW_PROMPT = `당신은 Cloud Orchestrator가 추천한 서버를 검토하는 30년 경력의 시니어 클라우드 아키텍트입니다.
|
||||||
|
|
||||||
|
## 전문성 (30년 경력)
|
||||||
|
- 서버 엔지니어: Linux, Windows Server, 가상화, 컨테이너 마스터
|
||||||
|
- 네트워크 엔지니어: 로드밸런싱, CDN, DNS, 보안 설계 전문
|
||||||
|
- 클라우드 아키텍트: 모든 클라우드 플랫폼 경험
|
||||||
|
- 수천 개의 서버 구축 경험
|
||||||
|
|
||||||
|
## 검토 작업
|
||||||
|
다음을 검토하고 간결하게 2-3문장으로 코멘트해주세요:
|
||||||
|
1. 추천된 서버가 용도와 규모에 적합한지
|
||||||
|
2. 스펙이 충분한지 (RAM, CPU, 스토리지)
|
||||||
|
3. DAU/동시접속자 기준이 적절한지
|
||||||
|
4. 대역폭 경고(overage)가 있다면 언급
|
||||||
|
5. 더 적합한 스펙이 필요하다면 제안
|
||||||
|
|
||||||
|
중요: 검토 코멘트만 작성하세요. 추천 결과 나열은 하지 마세요.`;
|
||||||
|
|
||||||
// Server Expert AI Tools
|
// Server Expert AI Tools
|
||||||
const serverExpertTools = [
|
const serverExpertTools = [
|
||||||
{
|
{
|
||||||
@@ -351,13 +539,21 @@ interface OpenAIAPIResponse {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI 호출 (서버 전문가 AI with Function Calling)
|
/**
|
||||||
|
* Server Expert AI 호출 (Function Calling 지원)
|
||||||
|
*
|
||||||
|
* @param session - ServerSession
|
||||||
|
* @param userMessage - 사용자 메시지
|
||||||
|
* @param env - Environment
|
||||||
|
* @param recommendationData - 추천 결과 (검토 모드용)
|
||||||
|
* @returns AI 응답 및 수집된 정보
|
||||||
|
*/
|
||||||
async function callServerExpertAI(
|
async function callServerExpertAI(
|
||||||
env: Env,
|
|
||||||
session: ServerSession,
|
session: ServerSession,
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
|
env: Env,
|
||||||
recommendationData?: RecommendResponse
|
recommendationData?: RecommendResponse
|
||||||
): Promise<{ action: 'question' | 'recommend'; message: string; collectedInfo: ServerSession['collectedInfo'] }> {
|
): Promise<{ action: 'question' | 'recommend'; message: string; collectedInfo: ServerSession['collected_info'] }> {
|
||||||
if (!env.OPENAI_API_KEY) {
|
if (!env.OPENAI_API_KEY) {
|
||||||
throw new Error('OPENAI_API_KEY not configured');
|
throw new Error('OPENAI_API_KEY not configured');
|
||||||
}
|
}
|
||||||
@@ -374,110 +570,35 @@ async function callServerExpertAI(
|
|||||||
const isReviewMode = !!recommendationData;
|
const isReviewMode = !!recommendationData;
|
||||||
|
|
||||||
const systemPrompt = isReviewMode
|
const systemPrompt = isReviewMode
|
||||||
? `당신은 Cloud Orchestrator가 추천한 서버를 검토하는 30년 경력의 시니어 클라우드 아키텍트입니다.
|
? `${SERVER_REVIEW_PROMPT}
|
||||||
|
|
||||||
## 전문성 (30년 경력)
|
|
||||||
- 서버 엔지니어: Linux, Windows Server, 가상화, 컨테이너 마스터
|
|
||||||
- 네트워크 엔지니어: 로드밸런싱, CDN, DNS, 보안 설계 전문
|
|
||||||
- 클라우드 아키텍트: 모든 클라우드 플랫폼 경험
|
|
||||||
- 수천 개의 서버 구축 경험
|
|
||||||
|
|
||||||
## 검토 대상 추천 결과
|
## 검토 대상 추천 결과
|
||||||
${JSON.stringify(recommendationData?.recommendations, null, 2)}
|
${JSON.stringify(recommendationData?.recommendations, null, 2)}
|
||||||
|
|
||||||
## 사용자 요구사항
|
## 사용자 요구사항
|
||||||
- 용도: ${session.collectedInfo.useCase || '웹 서비스'}
|
- 용도: ${session.collected_info.useCase || '웹 서비스'}
|
||||||
- 규모: ${session.collectedInfo.scale === 'business' ? '사업용' : '개인용'}
|
- 규모: ${session.collected_info.scale === 'business' ? '사업용' : '개인용'}
|
||||||
${session.collectedInfo.expectedDau ? `- 일일 방문자(DAU): ${session.collectedInfo.expectedDau}명` : ''}
|
${session.collected_info.expectedDau ? `- 일일 방문자(DAU): ${session.collected_info.expectedDau}명` : ''}
|
||||||
${session.collectedInfo.expectedConcurrent ? `- 동시접속자: ${session.collectedInfo.expectedConcurrent}명` : ''}
|
${session.collected_info.expectedConcurrent ? `- 동시접속자: ${session.collected_info.expectedConcurrent}명` : ''}
|
||||||
${session.collectedInfo.budgetLimit ? `- 예산: ${session.collectedInfo.budgetLimit}원` : ''}
|
${session.collected_info.budgetLimit ? `- 예산: ${session.collected_info.budgetLimit}원` : ''}
|
||||||
|
|
||||||
## 사용자 수 관련 참고사항
|
## 사용자 수 관련 참고사항
|
||||||
- DAU(일일 활성 사용자)와 동시접속자는 다른 개념입니다
|
- DAU(일일 활성 사용자)와 동시접속자는 다른 개념입니다
|
||||||
- 일반적으로 동시접속자는 DAU의 5-10% 수준입니다
|
- 일반적으로 동시접속자는 DAU의 5-10% 수준입니다
|
||||||
- 서버 스펙은 동시접속자 기준으로 계산됩니다
|
- 서버 스펙은 동시접속자 기준으로 계산됩니다
|
||||||
|
|
||||||
## 검토 작업
|
|
||||||
다음을 검토하고 간결하게 2-3문장으로 코멘트해주세요:
|
|
||||||
1. 추천된 서버가 용도와 규모에 적합한지
|
|
||||||
2. 스펙이 충분한지 (RAM, CPU, 스토리지)
|
|
||||||
3. DAU/동시접속자 기준이 적절한지
|
|
||||||
4. 대역폭 경고(overage)가 있다면 언급
|
|
||||||
5. 더 적합한 스펙이 필요하다면 제안
|
|
||||||
|
|
||||||
## 응답 형식 (반드시 JSON만 반환)
|
## 응답 형식 (반드시 JSON만 반환)
|
||||||
{
|
{
|
||||||
"action": "recommend",
|
"action": "recommend",
|
||||||
"message": "검토 코멘트 (자연스럽고 친근한 어조, 2-3문장)",
|
"message": "검토 코멘트 (자연스럽고 친근한 어조, 2-3문장)",
|
||||||
"collectedInfo": ${JSON.stringify(session.collectedInfo)}
|
"collectedInfo": ${JSON.stringify(session.collected_info)}
|
||||||
}
|
}
|
||||||
|
|
||||||
중요: 검토 코멘트만 작성하세요. 추천 결과 나열은 하지 마세요.`
|
중요: 검토 코멘트만 작성하세요. 추천 결과 나열은 하지 마세요.`
|
||||||
: `당신은 30년 경력의 시니어 클라우드 아키텍트입니다.
|
: `${SERVER_EXPERT_PROMPT}
|
||||||
|
|
||||||
## 전문성 (30년 경력)
|
|
||||||
- 서버 엔지니어: Linux, Windows Server, 가상화, 컨테이너 마스터
|
|
||||||
- 네트워크 엔지니어: 로드밸런싱, CDN, DNS, 보안 설계 전문
|
|
||||||
- 클라우드 아키텍트: 모든 클라우드 플랫폼 경험
|
|
||||||
- 수천 개의 서버 구축 경험으로 용도만 들으면 최적 스펙을 바로 판단 가능
|
|
||||||
|
|
||||||
## 성격
|
|
||||||
- 따뜻하고 친근하지만 전문적인 어조
|
|
||||||
- 비기술자도 이해하기 쉽게 설명
|
|
||||||
- 고객의 예산과 상황을 항상 배려
|
|
||||||
- 불필요한 기술 용어 사용 자제
|
|
||||||
|
|
||||||
## 금지 사항 (절대 위반 금지)
|
|
||||||
- AWS, GCP, Azure, Vultr, Linode, DigitalOcean 등 다른 클라우드 프로바이더 언급 금지
|
|
||||||
- 경쟁사 서비스 추천 금지
|
|
||||||
- 우리 서비스(Anvil)만 추천
|
|
||||||
- "다른 곳도 고려해보세요" 같은 멘트 금지
|
|
||||||
|
|
||||||
## 도구 사용 가이드 (적극적으로 활용할 것)
|
|
||||||
- 고객이 특정 프레임워크/기술을 언급하면 (예: Next.js, Laravel, Django, Astro, Bun, Rust 등) → 반드시 lookup_framework_docs 호출하여 최신 공식 권장 스펙 확인
|
|
||||||
- "최신", "트렌드", "2024", "2025", "요즘" 등 시의성 있는 키워드 → 반드시 search_trends 호출
|
|
||||||
- SaaS, 모바일 앱 백엔드 같은 일반적 용도는 경험으로 바로 답변
|
|
||||||
- 도구 결과를 자연스럽게 메시지에 포함 (예: "공식 문서에 따르면...")
|
|
||||||
|
|
||||||
## 대화 흐름
|
|
||||||
1. 용도 파악: "어떤 서비스를 운영하실 건가요? (예: SaaS, 앱 백엔드, AI 서비스)"
|
|
||||||
2. 규모 파악: "개인용인가요, 사업용인가요?"
|
|
||||||
3. 사용자 수 확인 (필요 시): "방문자나 사용자 수는 어느 정도 예상하시나요?"
|
|
||||||
4. 정보가 충분하면 즉시 추천 (추가 질문 없이)
|
|
||||||
|
|
||||||
## 핵심 규칙 (반드시 준수)
|
|
||||||
- 기술 스택, 트래픽 패턴은 절대 묻지 않음 (30년 경험으로 알아서 추론)
|
|
||||||
- 사용자 수를 언급하면 DAU인지 동시접속자인지 반드시 한 번 확인
|
|
||||||
- "방문자 1000명", "유저 500명" 등 언급 시 → "말씀하신 방문자는 일일 방문자(DAU)인가요, 동시접속자인가요?"
|
|
||||||
- DAU와 동시접속자를 구분해서 설명: "일반적으로 동시접속자는 일일 방문자의 5-10% 정도입니다"
|
|
||||||
- "모르겠어요", "아무거나", "글쎄요" → 즉시 action="recommend" (기본값: 개인용 웹서비스)
|
|
||||||
- 용도+규모 한번에 말하면 → 즉시 action="recommend"
|
|
||||||
- 용도만 말해도 → 개인용으로 가정하고 action="recommend" 가능
|
|
||||||
- 질문은 최대 2번까지, 그 이후는 무조건 action="recommend"
|
|
||||||
|
|
||||||
## 사용자 수 관련 용어 정리
|
|
||||||
- **DAU (일일 활성 사용자)**: 하루 동안 서비스를 사용하는 전체 사용자 수
|
|
||||||
- **동시접속자 (Concurrent Users)**: 같은 시간에 동시에 접속해 있는 사용자 수
|
|
||||||
- **중요**: 서버 스펙은 동시접속자를 기준으로 계산해야 합니다
|
|
||||||
- **일반 공식**: 동시접속자 = DAU × 5-10%
|
|
||||||
|
|
||||||
예시:
|
|
||||||
- "하루 방문자 1000명" → DAU 1000명 → 동시접속자 50-100명
|
|
||||||
- "동시 접속 100명" → 그대로 동시접속자 100명 사용
|
|
||||||
|
|
||||||
## 추론 규칙 (30년 경험 기반)
|
|
||||||
- 블로그 → WordPress, 1GB RAM이면 충분, DAU 100명 (동시접속자 10명)
|
|
||||||
- 쇼핑몰 → 2GB+ RAM, DB 분리 고려, DAU 500명 (동시접속자 50명)
|
|
||||||
- 커뮤니티 → PHP+MySQL, 트래픽에 따라 2~4GB
|
|
||||||
- 게임서버 → 고사양 CPU, 낮은 레이턴시 리전
|
|
||||||
- SaaS/B2B/Enterprise → 최소 4GB+ RAM, PostgreSQL+Redis 권장, 500명+ 동시접속 가정
|
|
||||||
- API 서버 → 트래픽에 따라 2~8GB, Redis 캐시 권장
|
|
||||||
- 실시간 서비스 (WebSocket) → 최소 4GB RAM, Redis 권장
|
|
||||||
- 고성능 DB (PostgreSQL, MongoDB) → 최소 4GB+ RAM, 높은 IOPS
|
|
||||||
- 규모: personal→DAU 100명 (동접 10명), business→DAU 500명 (동접 50명), SaaS→DAU 2000명 (동접 200명)
|
|
||||||
|
|
||||||
## 현재 수집된 정보
|
## 현재 수집된 정보
|
||||||
${JSON.stringify(session.collectedInfo, null, 2)}
|
${JSON.stringify(session.collected_info, null, 2)}
|
||||||
|
|
||||||
## 응답 형식 (반드시 JSON만 반환, 다른 텍스트 절대 금지)
|
## 응답 형식 (반드시 JSON만 반환, 다른 텍스트 절대 금지)
|
||||||
{
|
{
|
||||||
@@ -597,7 +718,7 @@ ${JSON.stringify(session.collectedInfo, null, 2)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AI 응답에서 리전 정보가 없으면 사용자 메시지에서 추출 시도
|
// AI 응답에서 리전 정보가 없으면 사용자 메시지에서 추출 시도
|
||||||
const finalCollectedInfo = parsed.collectedInfo || session.collectedInfo;
|
const finalCollectedInfo = parsed.collectedInfo || session.collected_info;
|
||||||
|
|
||||||
if (!finalCollectedInfo.regionPreference) {
|
if (!finalCollectedInfo.regionPreference) {
|
||||||
// 전체 대화 히스토리에서 리전 감지
|
// 전체 대화 히스토리에서 리전 감지
|
||||||
@@ -611,7 +732,7 @@ ${JSON.stringify(session.collectedInfo, null, 2)}
|
|||||||
finalCollectedInfo.regionPreference = detectedRegions;
|
finalCollectedInfo.regionPreference = detectedRegions;
|
||||||
logger.info('사용자 메시지에서 리전 자동 감지', {
|
logger.info('사용자 메시지에서 리전 자동 감지', {
|
||||||
regions: detectedRegions,
|
regions: detectedRegions,
|
||||||
userId: session.telegramUserId
|
userId: session.user_id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,7 +749,7 @@ ${JSON.stringify(session.collectedInfo, null, 2)}
|
|||||||
return {
|
return {
|
||||||
action: 'recommend',
|
action: 'recommend',
|
||||||
message: '분석이 완료되었습니다. 최적의 서버를 추천해 드리겠습니다.',
|
message: '분석이 완료되었습니다. 최적의 서버를 추천해 드리겠습니다.',
|
||||||
collectedInfo: session.collectedInfo,
|
collectedInfo: session.collected_info,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Server Expert AI 호출 실패', error as Error);
|
logger.error('Server Expert AI 호출 실패', error as Error);
|
||||||
@@ -636,28 +757,42 @@ ${JSON.stringify(session.collectedInfo, null, 2)}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main consultation processing
|
/**
|
||||||
|
* 서버 상담 처리 (메인 함수)
|
||||||
|
*
|
||||||
|
* @param db - D1 Database
|
||||||
|
* @param userId - Telegram User ID
|
||||||
|
* @param userMessage - 사용자 메시지
|
||||||
|
* @param env - Environment
|
||||||
|
* @param options - Optional settings
|
||||||
|
* @returns AI 응답 메시지
|
||||||
|
*/
|
||||||
export async function processServerConsultation(
|
export async function processServerConsultation(
|
||||||
|
db: D1Database,
|
||||||
|
userId: string,
|
||||||
userMessage: string,
|
userMessage: string,
|
||||||
session: ServerSession,
|
|
||||||
env: Env,
|
env: Env,
|
||||||
sendIntermediateMessage?: (message: string) => Promise<void>
|
options?: { sendIntermediateMessage?: (msg: string) => Promise<void> }
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
logger.info('서버 상담 시작', { userId, message: userMessage.substring(0, 100) });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info('상담 처리 시작', {
|
// 1. Check for existing session
|
||||||
userId: session.telegramUserId,
|
let session = await getServerSession(db, userId);
|
||||||
message: userMessage.slice(0, 50),
|
|
||||||
status: session.status
|
// 2. Create new session if none exists
|
||||||
});
|
if (!session) {
|
||||||
|
session = createServerSession(userId, 'gathering');
|
||||||
|
}
|
||||||
|
|
||||||
// ordering 상태에서 "신청" 외 메시지 입력 시 세션 정리
|
// ordering 상태에서 "신청" 외 메시지 입력 시 세션 정리
|
||||||
if (session.status === 'ordering') {
|
if (session.status === 'ordering') {
|
||||||
// "신청"은 message-handler에서 처리, 여기까지 오면 다른 메시지임
|
// "신청"은 message-handler에서 처리, 여기까지 오면 다른 메시지임
|
||||||
const orderConfirmKey = `server_order_confirm:${session.telegramUserId}`;
|
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
||||||
await env.SESSION_KV?.delete(orderConfirmKey);
|
await env.SESSION_KV?.delete(orderConfirmKey);
|
||||||
await deleteServerSession(env.DB, session.telegramUserId);
|
await deleteServerSession(db, session.user_id);
|
||||||
|
|
||||||
logger.info('주문 확인 세션 취소 (다른 메시지 입력)', { userId: session.telegramUserId });
|
logger.info('주문 확인 세션 취소 (다른 메시지 입력)', { userId: session.user_id });
|
||||||
return '__PASSTHROUGH__'; // 일반 대화로 전환
|
return '__PASSTHROUGH__'; // 일반 대화로 전환
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,9 +800,9 @@ export async function processServerConsultation(
|
|||||||
// "취소", "다시", "처음", "리셋", "초기화" 등
|
// "취소", "다시", "처음", "리셋", "초기화" 등
|
||||||
if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) ||
|
if (/^(취소|다시|처음|리셋|초기화)/.test(userMessage.trim()) ||
|
||||||
/취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) {
|
/취소할[게래]|다시\s*시작|처음부터/.test(userMessage)) {
|
||||||
await deleteServerSession(env.DB, session.telegramUserId);
|
await deleteServerSession(db, session.user_id);
|
||||||
logger.info('사용자 요청으로 상담 취소', {
|
logger.info('사용자 요청으로 상담 취소', {
|
||||||
userId: session.telegramUserId,
|
userId: session.user_id,
|
||||||
previousStatus: session.status,
|
previousStatus: session.status,
|
||||||
trigger: userMessage.slice(0, 20)
|
trigger: userMessage.slice(0, 20)
|
||||||
});
|
});
|
||||||
@@ -676,41 +811,34 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// "서버 추천" 키워드로 새로 시작 요청 (기존 세션 리셋)
|
// "서버 추천" 키워드로 새로 시작 요청 (기존 세션 리셋)
|
||||||
if (/서버\s*추천/.test(userMessage)) {
|
if (/서버\s*추천/.test(userMessage)) {
|
||||||
await deleteServerSession(env.DB, session.telegramUserId);
|
await deleteServerSession(db, session.user_id);
|
||||||
logger.info('서버 추천 키워드로 세션 리셋', {
|
logger.info('서버 추천 키워드로 세션 리셋', {
|
||||||
userId: session.telegramUserId,
|
userId: session.user_id,
|
||||||
previousStatus: session.status
|
previousStatus: session.status
|
||||||
});
|
});
|
||||||
// 새 세션 생성하고 시작 메시지 반환
|
// 새 세션 생성하고 시작 메시지 반환
|
||||||
const newSession: ServerSession = {
|
const newSession = createServerSession(session.user_id, 'gathering');
|
||||||
telegramUserId: session.telegramUserId,
|
await saveServerSession(db, newSession);
|
||||||
status: SERVER_CONSULTATION_STATUS.GATHERING,
|
|
||||||
collectedInfo: {},
|
|
||||||
messages: [],
|
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedAt: Date.now()
|
|
||||||
};
|
|
||||||
await saveServerSession(env.DB, session.telegramUserId, newSession);
|
|
||||||
return '안녕하세요! 서버 추천을 도와드리겠습니다. 😊\n\n어떤 서비스를 운영하실 건가요?\n\n1. 웹 서비스 (SaaS, 랜딩페이지)\n2. 모바일 앱 백엔드\n3. AI/ML 서비스 (챗봇, 모델 서빙)\n4. 게임 서버\n5. Discord/Telegram 봇\n6. 자동화 서버 (n8n, 크롤링)\n7. 미디어 스트리밍\n8. 개발/테스트 환경\n9. 데이터베이스 서버\n10. 기타 (직접 입력)\n\n번호나 용도를 말씀해주세요!';
|
return '안녕하세요! 서버 추천을 도와드리겠습니다. 😊\n\n어떤 서비스를 운영하실 건가요?\n\n1. 웹 서비스 (SaaS, 랜딩페이지)\n2. 모바일 앱 백엔드\n3. AI/ML 서비스 (챗봇, 모델 서빙)\n4. 게임 서버\n5. Discord/Telegram 봇\n6. 자동화 서버 (n8n, 크롤링)\n7. 미디어 스트리밍\n8. 개발/테스트 환경\n9. 데이터베이스 서버\n10. 기타 (직접 입력)\n\n번호나 용도를 말씀해주세요!';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 선택 단계 처리
|
// 선택 단계 처리
|
||||||
logger.info('[SESSION DEBUG] 선택 단계 체크', {
|
logger.info('[SESSION DEBUG] 선택 단계 체크', {
|
||||||
userId: session.telegramUserId,
|
userId: session.user_id,
|
||||||
status: session.status,
|
status: session.status,
|
||||||
hasLastRecommendation: !!session.lastRecommendation,
|
hasLastRecommendation: !!session.last_recommendation,
|
||||||
recommendationCount: session.lastRecommendation?.recommendations?.length || 0,
|
recommendationCount: session.last_recommendation?.recommendations?.length || 0,
|
||||||
willProcessSelection: session.status === SERVER_CONSULTATION_STATUS.SELECTING && !!session.lastRecommendation
|
willProcessSelection: session.status === SERVER_CONSULTATION_STATUS.SELECTING && !!session.last_recommendation
|
||||||
});
|
});
|
||||||
|
|
||||||
if (session.status === SERVER_CONSULTATION_STATUS.SELECTING && session.lastRecommendation) {
|
if (session.status === SERVER_CONSULTATION_STATUS.SELECTING && session.last_recommendation) {
|
||||||
// 상담과 무관한 키워드 감지 (selecting 상태에서만)
|
// 상담과 무관한 키워드 감지 (selecting 상태에서만)
|
||||||
// 명확히 다른 기능 요청인 경우 세션 종료하고 일반 처리로 전환
|
// 명확히 다른 기능 요청인 경우 세션 종료하고 일반 처리로 전환
|
||||||
const unrelatedPatterns = /기억|날씨|계산|검색|도메인|입금|충전|잔액|시간|문서/;
|
const unrelatedPatterns = /기억|날씨|계산|검색|도메인|입금|충전|잔액|시간|문서/;
|
||||||
if (unrelatedPatterns.test(userMessage)) {
|
if (unrelatedPatterns.test(userMessage)) {
|
||||||
await deleteServerSession(env.DB, session.telegramUserId);
|
await deleteServerSession(db, session.user_id);
|
||||||
logger.info('무관한 요청으로 세션 자동 종료', {
|
logger.info('무관한 요청으로 세션 자동 종료', {
|
||||||
userId: session.telegramUserId,
|
userId: session.user_id,
|
||||||
message: userMessage.slice(0, 30)
|
message: userMessage.slice(0, 30)
|
||||||
});
|
});
|
||||||
// 'PASSTHROUGH' 반환하여 상위에서 일반 처리로 전환
|
// 'PASSTHROUGH' 반환하여 상위에서 일반 처리로 전환
|
||||||
@@ -734,24 +862,24 @@ export async function processServerConsultation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 유효성 검증
|
// 유효성 검증
|
||||||
if (selectedIndex >= 0 && selectedIndex < session.lastRecommendation.recommendations.length) {
|
if (selectedIndex >= 0 && selectedIndex < session.last_recommendation.recommendations.length) {
|
||||||
const selected = session.lastRecommendation.recommendations[selectedIndex];
|
const selected = session.last_recommendation.recommendations[selectedIndex];
|
||||||
|
|
||||||
// Mark session as ordering
|
// Mark session as ordering
|
||||||
session.status = 'ordering';
|
session.status = 'ordering';
|
||||||
await saveServerSession(env.DB, session.telegramUserId, session);
|
await saveServerSession(db, session);
|
||||||
|
|
||||||
// 주문 확인 세션 저장 (텍스트 기반 확인)
|
// 주문 확인 세션 저장 (텍스트 기반 확인)
|
||||||
const orderConfirmKey = `server_order_confirm:${session.telegramUserId}`;
|
const orderConfirmKey = `server_order_confirm:${session.user_id}`;
|
||||||
const orderConfirmData = JSON.stringify({
|
const orderConfirmData = JSON.stringify({
|
||||||
userId: session.telegramUserId,
|
userId: session.user_id,
|
||||||
index: selectedIndex,
|
index: selectedIndex,
|
||||||
plan: selected.plan_name,
|
plan: selected.plan_name,
|
||||||
pricingId: selected.pricing_id,
|
pricingId: selected.pricing_id,
|
||||||
region: selected.region.code,
|
region: selected.region.code,
|
||||||
label: `${selected.plan_name.toLowerCase().replace(/\s+/g, '-')}-server`,
|
label: `${selected.plan_name.toLowerCase().replace(/\s+/g, '-')}-server`,
|
||||||
});
|
});
|
||||||
logger.info('주문 확인 세션 저장', { orderConfirmKey, userId: session.telegramUserId });
|
logger.info('주문 확인 세션 저장', { orderConfirmKey, userId: session.user_id });
|
||||||
await env.SESSION_KV.put(orderConfirmKey, orderConfirmData, { expirationTtl: 300 });
|
await env.SESSION_KV.put(orderConfirmKey, orderConfirmData, { expirationTtl: 300 });
|
||||||
logger.info('주문 확인 세션 저장 완료', { orderConfirmKey });
|
logger.info('주문 확인 세션 저장 완료', { orderConfirmKey });
|
||||||
|
|
||||||
@@ -786,7 +914,7 @@ export async function processServerConsultation(
|
|||||||
`\n⚠️ 정말 신청하시려면 '신청'이라고 입력하세요.\n` +
|
`\n⚠️ 정말 신청하시려면 '신청'이라고 입력하세요.\n` +
|
||||||
`(5분 내 응답 없으면 자동 취소됩니다)`;
|
`(5분 내 응답 없으면 자동 취소됩니다)`;
|
||||||
} else {
|
} else {
|
||||||
return `번호를 다시 확인해주세요. 1번부터 ${session.lastRecommendation.recommendations.length}번 중에서 선택해주세요.`;
|
return `번호를 다시 확인해주세요. 1번부터 ${session.last_recommendation.recommendations.length}번 중에서 선택해주세요.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,26 +926,26 @@ export async function processServerConsultation(
|
|||||||
session.messages.push({ role: 'user', content: userMessage });
|
session.messages.push({ role: 'user', content: userMessage });
|
||||||
|
|
||||||
// Call Server Expert AI
|
// Call Server Expert AI
|
||||||
const aiResult = await callServerExpertAI(env, session, userMessage);
|
const aiResult = await callServerExpertAI(session, userMessage, env);
|
||||||
|
|
||||||
// Update collected info
|
// Update collected info
|
||||||
session.collectedInfo = { ...session.collectedInfo, ...aiResult.collectedInfo };
|
session.collected_info = { ...session.collected_info, ...aiResult.collectedInfo };
|
||||||
|
|
||||||
// Add AI response to history
|
// Add AI response to history
|
||||||
session.messages.push({ role: 'assistant', content: aiResult.message });
|
session.messages.push({ role: 'assistant', content: aiResult.message });
|
||||||
|
|
||||||
if (aiResult.action === 'recommend') {
|
if (aiResult.action === 'recommend') {
|
||||||
// Send intermediate message to user
|
// Send intermediate message to user
|
||||||
if (sendIntermediateMessage) {
|
if (options?.sendIntermediateMessage) {
|
||||||
await sendIntermediateMessage('🔍 요청하신 조건에 맞는 서버를 분석 중입니다...\n잠시만 기다려 주세요.');
|
await options?.sendIntermediateMessage('🔍 요청하신 조건에 맞는 서버를 분석 중입니다...\n잠시만 기다려 주세요.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark session as recommending
|
// Mark session as recommending
|
||||||
session.status = SERVER_CONSULTATION_STATUS.RECOMMENDING;
|
session.status = SERVER_CONSULTATION_STATUS.RECOMMENDING;
|
||||||
await saveServerSession(env.DB, session.telegramUserId, session);
|
await saveServerSession(db, session);
|
||||||
|
|
||||||
// 1. Call recommendation API (추천 먼저 받기)
|
// 1. Call recommendation API (추천 먼저 받기)
|
||||||
logger.info('추천 API 호출', { collectedInfo: session.collectedInfo });
|
logger.info('추천 API 호출', { collectedInfo: session.collected_info });
|
||||||
|
|
||||||
const { executeServerAction, getRecommendationData } = await import('../tools/server-tool');
|
const { executeServerAction, getRecommendationData } = await import('../tools/server-tool');
|
||||||
|
|
||||||
@@ -825,8 +953,8 @@ export async function processServerConsultation(
|
|||||||
const allMessages = session.messages.map(m => m.content).join(' ');
|
const allMessages = session.messages.map(m => m.content).join(' ');
|
||||||
|
|
||||||
// Tech Stack: useCase에서 추론 + 전체 메시지에서 추출한 것 병합
|
// Tech Stack: useCase에서 추론 + 전체 메시지에서 추출한 것 병합
|
||||||
let techStack = session.collectedInfo.useCase
|
let techStack = session.collected_info.useCase
|
||||||
? inferTechStack(session.collectedInfo.useCase)
|
? inferTechStack(session.collected_info.useCase)
|
||||||
: ['web'];
|
: ['web'];
|
||||||
|
|
||||||
// 전체 메시지에서 추가 tech stack 추출
|
// 전체 메시지에서 추가 tech stack 추출
|
||||||
@@ -841,32 +969,32 @@ export async function processServerConsultation(
|
|||||||
logger.info('메시지에서 tech stack 추출', {
|
logger.info('메시지에서 tech stack 추출', {
|
||||||
extracted: extractedTech,
|
extracted: extractedTech,
|
||||||
merged: techStack,
|
merged: techStack,
|
||||||
userId: session.telegramUserId
|
userId: session.user_id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 동시접속자 우선 사용, 없으면 scale 기반 추론
|
// 동시접속자 우선 사용, 없으면 scale 기반 추론
|
||||||
let expectedUsers = 10; // Default
|
let expectedUsers = 10; // Default
|
||||||
const concurrent = Number(session.collectedInfo.expectedConcurrent) || 0;
|
const concurrent = Number(session.collected_info.expectedConcurrent) || 0;
|
||||||
const dau = Number(session.collectedInfo.expectedDau) || 0;
|
const dau = Number(session.collected_info.expectedDau) || 0;
|
||||||
|
|
||||||
if (concurrent > 0) {
|
if (concurrent > 0) {
|
||||||
expectedUsers = concurrent;
|
expectedUsers = concurrent;
|
||||||
} else if (dau > 0) {
|
} else if (dau > 0) {
|
||||||
// DAU가 있으면 10% 비율로 동시접속자 계산
|
// DAU가 있으면 10% 비율로 동시접속자 계산
|
||||||
expectedUsers = Math.ceil(dau * 0.1);
|
expectedUsers = Math.ceil(dau * 0.1);
|
||||||
} else if (session.collectedInfo.scale) {
|
} else if (session.collected_info.scale) {
|
||||||
expectedUsers = inferExpectedUsers(session.collectedInfo.scale, techStack);
|
expectedUsers = inferExpectedUsers(session.collected_info.scale, techStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 리전 선호도 최종 확인 (세션에 없으면 메시지에서 재추출)
|
// 리전 선호도 최종 확인 (세션에 없으면 메시지에서 재추출)
|
||||||
let finalRegionPreference = session.collectedInfo.regionPreference;
|
let finalRegionPreference = session.collected_info.regionPreference;
|
||||||
if (!finalRegionPreference) {
|
if (!finalRegionPreference) {
|
||||||
finalRegionPreference = extractRegionPreference(allMessages);
|
finalRegionPreference = extractRegionPreference(allMessages);
|
||||||
if (finalRegionPreference) {
|
if (finalRegionPreference) {
|
||||||
logger.info('추천 직전 리전 재감지', {
|
logger.info('추천 직전 리전 재감지', {
|
||||||
regions: finalRegionPreference,
|
regions: finalRegionPreference,
|
||||||
userId: session.telegramUserId
|
userId: session.user_id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,9 +1003,9 @@ export async function processServerConsultation(
|
|||||||
{
|
{
|
||||||
tech_stack: techStack,
|
tech_stack: techStack,
|
||||||
expected_users: expectedUsers,
|
expected_users: expectedUsers,
|
||||||
use_case: session.collectedInfo.useCase || '웹 서비스',
|
use_case: session.collected_info.useCase || '웹 서비스',
|
||||||
region_preference: finalRegionPreference,
|
region_preference: finalRegionPreference,
|
||||||
budget_limit: session.collectedInfo.budgetLimit,
|
budget_limit: session.collected_info.budgetLimit,
|
||||||
lang: LANGUAGE_CODE.KOREAN,
|
lang: LANGUAGE_CODE.KOREAN,
|
||||||
},
|
},
|
||||||
env
|
env
|
||||||
@@ -885,7 +1013,7 @@ export async function processServerConsultation(
|
|||||||
|
|
||||||
// 추천 결과를 세션에 저장
|
// 추천 결과를 세션에 저장
|
||||||
if (recommendationData && recommendationData.recommendations && recommendationData.recommendations.length > 0) {
|
if (recommendationData && recommendationData.recommendations && recommendationData.recommendations.length > 0) {
|
||||||
session.lastRecommendation = {
|
session.last_recommendation = {
|
||||||
recommendations: recommendationData.recommendations.slice(0, 3).map(rec => ({
|
recommendations: recommendationData.recommendations.slice(0, 3).map(rec => ({
|
||||||
pricing_id: rec.server.id, // cloud-instances-db.anvil_pricing.id
|
pricing_id: rec.server.id, // cloud-instances-db.anvil_pricing.id
|
||||||
plan_name: rec.server.instance_name,
|
plan_name: rec.server.instance_name,
|
||||||
@@ -912,12 +1040,12 @@ export async function processServerConsultation(
|
|||||||
score: rec.score,
|
score: rec.score,
|
||||||
max_users: rec.estimated_capacity?.max_concurrent_users || 0
|
max_users: rec.estimated_capacity?.max_concurrent_users || 0
|
||||||
})),
|
})),
|
||||||
createdAt: Date.now()
|
created_at: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. AI에게 추천 결과 전달하여 검토 요청
|
// 2. AI에게 추천 결과 전달하여 검토 요청
|
||||||
logger.info('AI 검토 요청', { recommendationCount: recommendationData.recommendations.length });
|
logger.info('AI 검토 요청', { recommendationCount: recommendationData.recommendations.length });
|
||||||
const reviewResult = await callServerExpertAI(env, session, userMessage, recommendationData);
|
const reviewResult = await callServerExpertAI(session, userMessage, env, recommendationData);
|
||||||
|
|
||||||
// 3. 포맷팅된 추천 결과 생성
|
// 3. 포맷팅된 추천 결과 생성
|
||||||
const formattedRecommendation = await executeServerAction(
|
const formattedRecommendation = await executeServerAction(
|
||||||
@@ -925,18 +1053,18 @@ export async function processServerConsultation(
|
|||||||
{
|
{
|
||||||
tech_stack: techStack,
|
tech_stack: techStack,
|
||||||
expected_users: expectedUsers,
|
expected_users: expectedUsers,
|
||||||
use_case: session.collectedInfo.useCase || '웹 서비스',
|
use_case: session.collected_info.useCase || '웹 서비스',
|
||||||
region_preference: session.collectedInfo.regionPreference,
|
region_preference: session.collected_info.regionPreference,
|
||||||
budget_limit: session.collectedInfo.budgetLimit,
|
budget_limit: session.collected_info.budgetLimit,
|
||||||
lang: LANGUAGE_CODE.KOREAN,
|
lang: LANGUAGE_CODE.KOREAN,
|
||||||
},
|
},
|
||||||
env,
|
env,
|
||||||
session.telegramUserId
|
session.user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark session as selecting (사용자 선택 대기)
|
// Mark session as selecting (사용자 선택 대기)
|
||||||
session.status = SERVER_CONSULTATION_STATUS.SELECTING;
|
session.status = SERVER_CONSULTATION_STATUS.SELECTING;
|
||||||
await saveServerSession(env.DB, session.telegramUserId, session);
|
await saveServerSession(db, session);
|
||||||
|
|
||||||
// 4. 추천 결과 + AI 검토 코멘트 (검토 코멘트는 마지막에)
|
// 4. 추천 결과 + AI 검토 코멘트 (검토 코멘트는 마지막에)
|
||||||
// __DIRECT__ 마커가 앞에 와야 제대로 처리됨
|
// __DIRECT__ 마커가 앞에 와야 제대로 처리됨
|
||||||
@@ -944,22 +1072,26 @@ export async function processServerConsultation(
|
|||||||
} else {
|
} else {
|
||||||
// 추천 결과 없음 - 세션 삭제
|
// 추천 결과 없음 - 세션 삭제
|
||||||
session.status = SERVER_CONSULTATION_STATUS.COMPLETED;
|
session.status = SERVER_CONSULTATION_STATUS.COMPLETED;
|
||||||
await deleteServerSession(env.DB, session.telegramUserId);
|
await deleteServerSession(db, session.user_id);
|
||||||
|
|
||||||
return `${aiResult.message}\n\n조건에 맞는 서버를 찾지 못했습니다.`;
|
return `${aiResult.message}\n\n조건에 맞는 서버를 찾지 못했습니다.`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Continue gathering information
|
// Continue gathering information
|
||||||
session.status = SERVER_CONSULTATION_STATUS.GATHERING;
|
session.status = SERVER_CONSULTATION_STATUS.GATHERING;
|
||||||
await saveServerSession(env.DB, session.telegramUserId, session);
|
await saveServerSession(db, session);
|
||||||
|
|
||||||
return aiResult.message;
|
return aiResult.message;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('상담 처리 실패', error as Error, { userId: session.telegramUserId });
|
logger.error('상담 처리 실패', error as Error, { userId });
|
||||||
|
|
||||||
// Clean up session on error
|
// Clean up session on error (if exists)
|
||||||
await deleteServerSession(env.DB, session.telegramUserId);
|
try {
|
||||||
|
await deleteServerSession(db, userId);
|
||||||
|
} catch (deleteError) {
|
||||||
|
logger.error('세션 삭제 실패 (무시)', deleteError as Error, { userId });
|
||||||
|
}
|
||||||
|
|
||||||
return '죄송합니다. 서버 추천 중 오류가 발생했습니다.\n다시 시도하려면 "서버 추천"이라고 말씀해주세요.';
|
return '죄송합니다. 서버 추천 중 오류가 발생했습니다.\n다시 시도하려면 "서버 추천"이라고 말씀해주세요.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createLogger } from './utils/logger';
|
|||||||
import { metrics } from './utils/metrics';
|
import { metrics } from './utils/metrics';
|
||||||
import { getOpenAIUrl } from './utils/api-urls';
|
import { getOpenAIUrl } from './utils/api-urls';
|
||||||
import { ERROR_MESSAGES } from './constants/messages';
|
import { ERROR_MESSAGES } from './constants/messages';
|
||||||
import { getServerSession, processServerConsultation } from './agents/server-agent';
|
import { hasServerSession, processServerConsultation } from './agents/server-agent';
|
||||||
import { processTroubleshootConsultation, hasTroubleshootSession } from './agents/troubleshoot-agent';
|
import { processTroubleshootConsultation, hasTroubleshootSession } from './agents/troubleshoot-agent';
|
||||||
import { processDomainConsultation, hasDomainSession } from './agents/domain-agent';
|
import { processDomainConsultation, hasDomainSession } from './agents/domain-agent';
|
||||||
import { processDepositConsultation, hasDepositSession } from './agents/deposit-agent';
|
import { processDepositConsultation, hasDepositSession } from './agents/deposit-agent';
|
||||||
@@ -210,13 +210,11 @@ export async function generateOpenAIResponse(
|
|||||||
// Check if server consultation session is active
|
// Check if server consultation session is active
|
||||||
if (telegramUserId && env.DB) {
|
if (telegramUserId && env.DB) {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession(env.DB, telegramUserId);
|
const hasSession = await hasServerSession(env.DB, telegramUserId);
|
||||||
|
|
||||||
if (session && session.status !== 'completed') {
|
if (hasSession) {
|
||||||
logger.info('Active server session detected, routing to consultation', {
|
logger.info('Active server session detected, routing to consultation', {
|
||||||
userId: telegramUserId,
|
userId: telegramUserId
|
||||||
status: session.status,
|
|
||||||
hasLastRecommendation: !!session.lastRecommendation
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create callback for intermediate messages
|
// Create callback for intermediate messages
|
||||||
@@ -229,7 +227,13 @@ export async function generateOpenAIResponse(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await processServerConsultation(userMessage, session, env, sendIntermediateMessage);
|
const result = await processServerConsultation(
|
||||||
|
env.DB,
|
||||||
|
telegramUserId,
|
||||||
|
userMessage,
|
||||||
|
env,
|
||||||
|
{ sendIntermediateMessage }
|
||||||
|
);
|
||||||
|
|
||||||
// PASSTHROUGH: 무관한 메시지는 일반 처리로 전환
|
// PASSTHROUGH: 무관한 메시지는 일반 처리로 전환
|
||||||
if (result !== '__PASSTHROUGH__') {
|
if (result !== '__PASSTHROUGH__') {
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ async function handleChatApi(request: Request, env: Env): Promise<Response> {
|
|||||||
const { getServerSession, deleteServerSession } = await import('../../agents/server-agent');
|
const { getServerSession, deleteServerSession } = await import('../../agents/server-agent');
|
||||||
const session = await getServerSession(env.DB, telegramUserId);
|
const session = await getServerSession(env.DB, telegramUserId);
|
||||||
|
|
||||||
if (!session || !session.lastRecommendation) {
|
if (!session || !session.last_recommendation) {
|
||||||
await env.SESSION_KV.delete(orderSessionKey);
|
await env.SESSION_KV.delete(orderSessionKey);
|
||||||
const processingTimeMs = Date.now() - startTime;
|
const processingTimeMs = Date.now() - startTime;
|
||||||
|
|
||||||
@@ -345,7 +345,7 @@ async function handleChatApi(request: Request, env: Env): Promise<Response> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = session.lastRecommendation.recommendations[orderData.index];
|
const selected = session.last_recommendation.recommendations[orderData.index];
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
await env.SESSION_KV.delete(orderSessionKey);
|
await env.SESSION_KV.delete(orderSessionKey);
|
||||||
await deleteServerSession(env.DB, telegramUserId);
|
await deleteServerSession(env.DB, telegramUserId);
|
||||||
@@ -402,7 +402,7 @@ async function handleChatApi(request: Request, env: Env): Promise<Response> {
|
|||||||
selected.region.code,
|
selected.region.code,
|
||||||
'anvil',
|
'anvil',
|
||||||
price,
|
price,
|
||||||
`${selected.plan_name} - ${orderData.label || session.collectedInfo?.useCase || 'server'}`
|
`${selected.plan_name} - ${orderData.label || session.collected_info?.useCase || 'server'}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendProvisionMessage(env.SERVER_PROVISION_QUEUE, orderId, userId, telegramUserId);
|
await sendProvisionMessage(env.SERVER_PROVISION_QUEUE, orderId, userId, telegramUserId);
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export async function handleMessage(
|
|||||||
const { getServerSession, deleteServerSession } = await import('../../agents/server-agent');
|
const { getServerSession, deleteServerSession } = await import('../../agents/server-agent');
|
||||||
const session = await getServerSession(env.DB, telegramUserId);
|
const session = await getServerSession(env.DB, telegramUserId);
|
||||||
|
|
||||||
if (!session || !session.lastRecommendation) {
|
if (!session || !session.last_recommendation) {
|
||||||
await env.SESSION_KV.delete(orderSessionKey);
|
await env.SESSION_KV.delete(orderSessionKey);
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
env.BOT_TOKEN,
|
env.BOT_TOKEN,
|
||||||
@@ -134,7 +134,7 @@ export async function handleMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = session.lastRecommendation.recommendations[orderData.index];
|
const selected = session.last_recommendation.recommendations[orderData.index];
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
await env.SESSION_KV.delete(orderSessionKey);
|
await env.SESSION_KV.delete(orderSessionKey);
|
||||||
await deleteServerSession(env.DB, telegramUserId);
|
await deleteServerSession(env.DB, telegramUserId);
|
||||||
@@ -183,7 +183,7 @@ export async function handleMessage(
|
|||||||
selected.region.code,
|
selected.region.code,
|
||||||
'anvil',
|
'anvil',
|
||||||
price,
|
price,
|
||||||
`${selected.plan_name} - ${orderData.label || session.collectedInfo?.useCase || 'server'}`
|
`${selected.plan_name} - ${orderData.label || session.collected_info?.useCase || 'server'}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendProvisionMessage(env.SERVER_PROVISION_QUEUE, orderId, userId, telegramUserId);
|
await sendProvisionMessage(env.SERVER_PROVISION_QUEUE, orderId, userId, telegramUserId);
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ export async function executeServerAction(
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start_consultation': {
|
case 'start_consultation': {
|
||||||
// Import session functions
|
// Import session functions
|
||||||
const { saveServerSession } = await import('../agents/server-agent');
|
const { createServerSession, saveServerSession } = await import('../agents/server-agent');
|
||||||
|
|
||||||
if (!telegramUserId) {
|
if (!telegramUserId) {
|
||||||
return '🚫 사용자 인증이 필요합니다.';
|
return '🚫 사용자 인증이 필요합니다.';
|
||||||
@@ -674,16 +674,8 @@ export async function executeServerAction(
|
|||||||
return '🚫 세션 저장소가 설정되지 않았습니다.';
|
return '🚫 세션 저장소가 설정되지 않았습니다.';
|
||||||
}
|
}
|
||||||
|
|
||||||
const session: import('../types').ServerSession = {
|
const session = createServerSession(telegramUserId, 'gathering');
|
||||||
telegramUserId,
|
await saveServerSession(env.DB, session);
|
||||||
status: 'gathering',
|
|
||||||
collectedInfo: {},
|
|
||||||
messages: [],
|
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await saveServerSession(env.DB, telegramUserId, session);
|
|
||||||
|
|
||||||
logger.info('상담 세션 생성', { userId: maskUserId(telegramUserId) });
|
logger.info('상담 세션 생성', { userId: maskUserId(telegramUserId) });
|
||||||
|
|
||||||
@@ -691,7 +683,7 @@ export async function executeServerAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'continue_consultation': {
|
case 'continue_consultation': {
|
||||||
const { getServerSession, processServerConsultation } = await import('../agents/server-agent');
|
const { processServerConsultation } = await import('../agents/server-agent');
|
||||||
|
|
||||||
if (!telegramUserId) {
|
if (!telegramUserId) {
|
||||||
return '🚫 사용자 인증이 필요합니다.';
|
return '🚫 사용자 인증이 필요합니다.';
|
||||||
@@ -705,12 +697,7 @@ export async function executeServerAction(
|
|||||||
return '🚫 메시지가 필요합니다.';
|
return '🚫 메시지가 필요합니다.';
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await getServerSession(env.DB, telegramUserId);
|
const result = await processServerConsultation(env.DB, telegramUserId, args.message, env);
|
||||||
if (!session) {
|
|
||||||
return '세션이 만료되었습니다. 다시 시작하려면 "서버 추천"이라고 말씀해주세요.';
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await processServerConsultation(args.message, session, env);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,30 +758,24 @@ export async function executeServerAction(
|
|||||||
// 세션에 추천 결과 저장 (선택 기능 활성화)
|
// 세션에 추천 결과 저장 (선택 기능 활성화)
|
||||||
if (telegramUserId && env?.DB && recommendationData.recommendations && recommendationData.recommendations.length > 0) {
|
if (telegramUserId && env?.DB && recommendationData.recommendations && recommendationData.recommendations.length > 0) {
|
||||||
try {
|
try {
|
||||||
const { getServerSession, saveServerSession } = await import('../agents/server-agent');
|
const { getServerSession, saveServerSession, createServerSession } = await import('../agents/server-agent');
|
||||||
|
|
||||||
// 기존 세션 조회 또는 새로 생성
|
// 기존 세션 조회 또는 새로 생성
|
||||||
let session = await getServerSession(env.DB, telegramUserId);
|
let session = await getServerSession(env.DB, telegramUserId);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
// 세션이 없으면 새로 생성
|
// 세션이 없으면 새로 생성
|
||||||
session = {
|
session = createServerSession(telegramUserId, 'selecting');
|
||||||
telegramUserId,
|
session.collected_info = {
|
||||||
status: 'selecting',
|
useCase: use_case,
|
||||||
collectedInfo: {
|
scale: expected_users <= 50 ? 'personal' : 'business',
|
||||||
useCase: use_case,
|
expectedConcurrent: expected_users,
|
||||||
scale: expected_users <= 50 ? 'personal' : 'business',
|
|
||||||
expectedConcurrent: expected_users,
|
|
||||||
},
|
|
||||||
messages: [],
|
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
};
|
||||||
logger.info('새 세션 생성 (추천 결과 저장용)', { userId: telegramUserId });
|
logger.info('새 세션 생성 (추천 결과 저장용)', { userId: telegramUserId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// lastRecommendation 저장
|
// last_recommendation 저장
|
||||||
session.lastRecommendation = {
|
session.last_recommendation = {
|
||||||
recommendations: recommendationData.recommendations.slice(0, 3).map(rec => ({
|
recommendations: recommendationData.recommendations.slice(0, 3).map(rec => ({
|
||||||
pricing_id: rec.server.id,
|
pricing_id: rec.server.id,
|
||||||
plan_name: rec.server.instance_name,
|
plan_name: rec.server.instance_name,
|
||||||
@@ -821,17 +802,17 @@ export async function executeServerAction(
|
|||||||
score: rec.score,
|
score: rec.score,
|
||||||
max_users: rec.estimated_capacity?.max_concurrent_users || 0
|
max_users: rec.estimated_capacity?.max_concurrent_users || 0
|
||||||
})),
|
})),
|
||||||
createdAt: Date.now()
|
created_at: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// status를 'selecting'으로 변경
|
// status를 'selecting'으로 변경
|
||||||
session.status = 'selecting';
|
session.status = 'selecting';
|
||||||
session.updatedAt = Date.now();
|
session.updated_at = Date.now();
|
||||||
|
|
||||||
await saveServerSession(env.DB, telegramUserId, session);
|
await saveServerSession(env.DB, session);
|
||||||
logger.info('추천 결과 세션 저장 완료', {
|
logger.info('추천 결과 세션 저장 완료', {
|
||||||
userId: telegramUserId,
|
userId: telegramUserId,
|
||||||
recommendationCount: session.lastRecommendation.recommendations.length,
|
recommendationCount: session.last_recommendation.recommendations.length,
|
||||||
status: session.status
|
status: session.status
|
||||||
});
|
});
|
||||||
} catch (sessionError) {
|
} catch (sessionError) {
|
||||||
|
|||||||
25
src/types.ts
25
src/types.ts
@@ -244,11 +244,19 @@ export interface ManageMemoryArgs {
|
|||||||
memory_id?: number;
|
memory_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server Consultation Session
|
// Server Consultation Session Status
|
||||||
|
export type ServerSessionStatus =
|
||||||
|
| 'gathering' // 정보 수집 중
|
||||||
|
| 'recommending' // 추천 중
|
||||||
|
| 'selecting' // 선택 대기
|
||||||
|
| 'ordering' // 주문 대기
|
||||||
|
| 'completed'; // 완료
|
||||||
|
|
||||||
|
// Server Consultation Session (D1)
|
||||||
export interface ServerSession {
|
export interface ServerSession {
|
||||||
telegramUserId: string;
|
user_id: string;
|
||||||
status: 'gathering' | 'recommending' | 'selecting' | 'ordering' | 'completed';
|
status: ServerSessionStatus;
|
||||||
collectedInfo: {
|
collected_info: {
|
||||||
useCase?: string;
|
useCase?: string;
|
||||||
scale?: 'personal' | 'business';
|
scale?: 'personal' | 'business';
|
||||||
budgetLimit?: number;
|
budgetLimit?: number;
|
||||||
@@ -257,9 +265,10 @@ export interface ServerSession {
|
|||||||
expectedConcurrent?: number; // 동시접속자 (Concurrent Users)
|
expectedConcurrent?: number; // 동시접속자 (Concurrent Users)
|
||||||
};
|
};
|
||||||
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
|
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||||
createdAt: number;
|
created_at: number;
|
||||||
updatedAt: number;
|
updated_at: number;
|
||||||
lastRecommendation?: {
|
expires_at: number;
|
||||||
|
last_recommendation?: {
|
||||||
recommendations: Array<{
|
recommendations: Array<{
|
||||||
pricing_id: number; // cloud-instances-db.anvil_pricing.id
|
pricing_id: number; // cloud-instances-db.anvil_pricing.id
|
||||||
plan_name: string;
|
plan_name: string;
|
||||||
@@ -279,7 +288,7 @@ export interface ServerSession {
|
|||||||
score: number;
|
score: number;
|
||||||
max_users: number;
|
max_users: number;
|
||||||
}>;
|
}>;
|
||||||
createdAt: number;
|
created_at: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user