From ab314b10c4d02b78aaab53ff7f626f6f970030b5 Mon Sep 17 00:00:00 2001 From: kappa Date: Sat, 7 Feb 2026 19:40:58 +0900 Subject: [PATCH] feat: recreate Server Agent for premium VM management Session-based agent with OpenAI Function Calling (9 tools). Follows ddos-agent pattern: execute tools inside loop, feed results back to AI. Includes D1 migration, session routing in openai-service, and doc updates. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 103 +++-- migrations/013_recreate_server_sessions.sql | 14 + schema.sql | 12 + src/agents/server-agent.ts | 469 ++++++++++++++++++++ src/openai-service.ts | 24 + src/tools/server-tool.ts | 98 +++- src/types.ts | 14 + src/utils/session-manager.ts | 25 -- 8 files changed, 660 insertions(+), 99 deletions(-) create mode 100644 migrations/013_recreate_server_sessions.sql create mode 100644 src/agents/server-agent.ts diff --git a/CLAUDE.md b/CLAUDE.md index 4ba73dd..bfc4e77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -203,10 +203,11 @@ Telegram Webhook → Security Validation → Command/Message Router **Agent System (세션 기반 전문가 AI):** | 파일 | 역할 | 상태 관리 | |------|------|----------| -| `agents/server-agent.ts` | 서버 추천 상담 (30년 경력 아키텍트) | KV (1시간) | -| `agents/troubleshoot-agent.ts` | 트러블슈팅 상담 | KV (1시간) | +| `agents/server-agent.ts` | 서버 관리 전문가 (프리미엄 호스팅) | D1 (1시간) | +| `agents/troubleshoot-agent.ts` | 트러블슈팅 상담 | D1 (1시간) | | `agents/domain-agent.ts` | 도메인 추천 상담 (10년 경력 컨설턴트) | D1 (1시간) | | `agents/deposit-agent.ts` | 예치금 입금 신고 상담 (금융 상담사) | D1 (30분) | +| `agents/ddos-agent.ts` | DDoS 방어 보안 전문가 | D1 (1시간) | **Logging & Monitoring:** | 파일 | 역할 | @@ -245,6 +246,9 @@ Telegram Webhook → Security Validation → Command/Message Router | `server_specs` | 서버 스펙 | name, cpu, ram, disk, price | | `domain_sessions` | 도메인 상담 세션 | user_id, status, collected_info, messages | | `deposit_sessions` | 예치금 상담 세션 | user_id, status, collected_info, messages | +| `server_sessions` | 서버 관리 세션 | user_id, status, collected_info, messages | +| `ddos_sessions` | DDoS 방어 세션 | user_id, status, collected_info, messages | +| `troubleshoot_sessions` | 트러블슈팅 세션 | user_id, status, collected_info, messages | **AI Fallback:** OpenAI 미설정 시 Workers AI (Llama 3.1 8B) 자동 전환 @@ -408,73 +412,68 @@ domain-register.ts: ### 4.3 Server System +**서비스 정책:** +- 프리미엄 VPS만 취급 (DDoS 방어 1Tbps+ 포함) +- 저가형 VM, VPN, 프록시 → 별도 서비스 준비 중 +- 추천(recommend) 기능 없음 (성능 문제로 제거) + **manage_server 도구 파라미터:** ```typescript { - action: 'recommend' | 'order' | 'start' | 'stop' | 'delete' | 'list', - tech_stack?: string[], // recommend용 - expected_users?: number, // recommend용 - use_case?: string, // recommend용 - traffic_pattern?: 'steady' | 'spiky' | 'growing', - region_preference?: string[], - budget_limit?: number, - lang?: 'ko' | 'ja' | 'zh' | 'en', // 자동 감지 - server_id?: string, // order/start/stop/delete용 - region_code?: string, // order용 - label?: string // order용 + action: 'order' | 'list' | 'info' | 'delete' | 'images' | 'start' | 'stop' | 'reboot' | 'rename', + order_id?: number, // info/start/stop/reboot/delete/rename용 + pricing_id?: number, // order용 + label?: string, // order용 + new_label?: string, // rename용 + image?: string // order용 (OS 이미지) } ``` **action별 상태:** | action | 설명 | 상태 | |--------|------|------| -| `recommend` | 서버 추천 | ✅ 구현 완료 | -| `order` | 서버 신청 | 🚧 준비 중 | -| `start` | 서버 시작 | 🚧 준비 중 | -| `stop` | 서버 중지 | 🚧 준비 중 | -| `delete` | 서버 해지 | 🚧 준비 중 | -| `list` | 내 서버 목록 | 🚧 준비 중 | +| `order` | 서버 주문 | ✅ 구현 완료 | +| `list` | 내 서버 목록 | ✅ 구현 완료 | +| `info` | 서버 상세 정보 | ✅ 구현 완료 | +| `start` | 서버 시작 | ✅ 구현 완료 | +| `stop` | 서버 중지 | ✅ 구현 완료 | +| `reboot` | 서버 재시작 | ✅ 구현 완료 | +| `delete` | 서버 해지 (환불 포함) | ✅ 구현 완료 | +| `rename` | 서버 이름 변경 | ✅ 구현 완료 | +| `images` | OS 이미지 목록 | ✅ 구현 완료 | -**Server Expert AI Flow (상담 기반 추천):** +**Server Agent Flow (세션 기반 관리):** ``` -사용자: "서버 추천해줘" +사용자: "서버 목록 보여줘" / "1번 서버 시작" ↓ -메인 AI → manage_server(action="start_consultation") +① 메인 AI → manage_server(action="list") 호출 ↓ -KV 세션 생성 (server_session:{userId}, TTL 1h) +② server-tool.ts: action → 자연어 변환 ("내 서버 목록 보여줘") ↓ -Server Expert AI (gpt-4o-mini + Function Calling) - - 페르소나: 30년 경력 클라우드 아키텍트 - - 용도/규모 파악 (최대 2번 질문) - - [선택] search_trends (Brave Search) - - [선택] lookup_framework_docs (Context7) +③ Server Agent (프리미엄 호스팅 전문가 페르소나) + - 세션 생성/조회 (D1, TTL 1시간) + - OpenAI Function Calling (9개 도구) + - 도구 실행 → executeServerAction() ↓ -action="question" → 추가 정보 수집 (세션 유지) -action="recommend" → 자동 스펙 추론 → Cloud Orchestrator API 호출 → 세션 삭제 - ↓ -서버 추천 결과 반환 +④ 응답 반환 + 세션 저장 + +[세션 라우팅] +기존 세션이 있는 경우 → openai-service.ts에서 직접 Server Agent로 라우팅 +세션 없는 경우 → 메인 AI가 manage_server 도구 호출 → Server Agent 위임 ``` -**자동 추론 (30년 경험 기반):** -| 용도 | 추론된 tech_stack | 추론된 expected_users | -|------|-------------------|---------------------| -| 블로그 / WordPress | `['wordpress']` | 100명 | -| 쇼핑몰 / 이커머스 | `['ecommerce']` | 500명 | -| 커뮤니티 / 게시판 | `['php', 'mysql']` | - | -| API / 백엔드 | `['nodejs', 'express']` | - | -| 기본값 | `['web']` | 100명 | - -**추천 결과 포맷:** -``` -🖥️ 서버 추천 결과 - -1️⃣ Standard 8GB (Anvil) - • 스펙: 4vCPU / 8GB / 160GB SSD - • 리전: Tokyo 3 (JP) - • 가격: ₩69,719/월 (대역폭 5TB) - • 예상 트래픽: 1.7TB (포함 범위 내) - • 점수: 95점 / 최대 7,500명 -``` +**Server Agent Function Calling (9개 도구):** +| 함수 | 설명 | 필수 인자 | +|------|------|----------| +| `list_servers` | 서버 목록 | - | +| `get_server_info` | 서버 상세 | order_id | +| `order_server` | 서버 주문 | pricing_id, label | +| `start_server` | 서버 시작 | order_id | +| `stop_server` | 서버 중지 | order_id | +| `reboot_server` | 서버 재시작 | order_id | +| `delete_server` | 서버 삭제 | order_id | +| `rename_server` | 이름 변경 | order_id, new_label | +| `list_images` | OS 이미지 목록 | - | **서버 주문 상태 전이:** | 상태 | 설정 주체 | UI 표시 | diff --git a/migrations/013_recreate_server_sessions.sql b/migrations/013_recreate_server_sessions.sql new file mode 100644 index 0000000..e33be1b --- /dev/null +++ b/migrations/013_recreate_server_sessions.sql @@ -0,0 +1,14 @@ +-- Recreate server management sessions table +-- Previous migration 012 dropped this table when recommendation feature was removed +-- Now recreated for premium VM management agent (no recommendation, management only) + +CREATE TABLE IF NOT EXISTS server_sessions ( + user_id TEXT PRIMARY KEY, + status TEXT NOT NULL DEFAULT 'idle', + collected_info TEXT NOT NULL DEFAULT '{}', + messages TEXT NOT NULL DEFAULT '[]', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_server_sessions_expires ON server_sessions(expires_at); diff --git a/schema.sql b/schema.sql index 91795e6..3c9f78c 100644 --- a/schema.sql +++ b/schema.sql @@ -127,6 +127,17 @@ CREATE TABLE IF NOT EXISTS user_servers ( FOREIGN KEY (order_id) REFERENCES server_orders(id) ); +-- 서버 관리 세션 테이블 +CREATE TABLE IF NOT EXISTS server_sessions ( + user_id TEXT PRIMARY KEY, + status TEXT NOT NULL DEFAULT 'idle', + collected_info TEXT NOT NULL DEFAULT '{}', + messages TEXT NOT NULL DEFAULT '[]', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL +); + -- 인덱스 CREATE INDEX IF NOT EXISTS idx_user_domains_user ON user_domains(user_id); CREATE INDEX IF NOT EXISTS idx_user_domains_domain ON user_domains(domain); @@ -146,3 +157,4 @@ CREATE INDEX IF NOT EXISTS idx_server_orders_status ON server_orders(status); CREATE INDEX IF NOT EXISTS idx_server_orders_idempotency ON server_orders(idempotency_key) WHERE idempotency_key IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_user_servers_user ON user_servers(user_id); CREATE INDEX IF NOT EXISTS idx_user_servers_status ON user_servers(status); +CREATE INDEX IF NOT EXISTS idx_server_sessions_expires ON server_sessions(expires_at); diff --git a/src/agents/server-agent.ts b/src/agents/server-agent.ts new file mode 100644 index 0000000..750e44e --- /dev/null +++ b/src/agents/server-agent.ts @@ -0,0 +1,469 @@ +/** + * Server Agent - 프리미엄 VM 관리 전문가 (세션 기반) + * + * 변경 이력: + * - 2026-01: 서버 추천(recommend) 기능 제거 (성능 문제) + * - 2026-02: 프리미엄 VM 관리 전용 Agent로 재생성 + * + * 기능: + * - [세션] 서버 관리 대화 (목록, 상세, 시작/중지/재시작/삭제/이름변경) + * - [세션] 서버 주문 (pricing_id, label, image 지정) + * - [세션] OS 이미지 목록 조회 + * + * 정책: + * - 프리미엄 VPS만 취급 (고급형 DDoS 방어 포함) + * - 저가형 VM, VPN, 프록시 → "준비 중" 안내 + * - 추천(recommend) 기능 없음 + */ + +import type { Env, ServerSession, OpenAIToolCall, OpenAIAPIResponse } from '../types'; +import { createLogger } from '../utils/logger'; +import { SessionManager } from '../utils/session-manager'; +import { getSessionConfig, AI_CONFIG } from '../constants/agent-config'; +import { executeServerAction } from '../tools/server-tool'; + +const logger = createLogger('server-agent'); + +// Session manager instance +const sessionManager = new SessionManager(getSessionConfig('server')); + +/** + * 서버 세션 존재 여부 확인 (라우팅용) + */ +export async function hasServerSession(db: D1Database, userId: string): Promise { + return await sessionManager.has(db, userId); +} + +// Server Management Expert System Prompt +const SERVER_EXPERT_PROMPT = `당신은 Anvil Hosting의 서버 관리 전문가입니다. + +전문 분야: +- 프리미엄 VPS 호스팅 관리 +- DDoS 방어 (1Tbps+ 보호 포함) +- 즉시 프로비저닝 +- 서버 운영 (시작/중지/재시작/삭제/이름 변경) + +행동 지침: +1. 명확하고 간결하게 답변 +2. 삭제/중지 등 위험 작업은 확인 후 실행 +3. 저가형 VM, VPN, 프록시 문의 → "프리미엄 서버 전용 서비스입니다. 저가형 서비스는 준비 중입니다." + +응답 형식: +- 짧고 명확하게 +- 가격은 원화(원) +- 서버 상태: 🟢 가동 중, ⛔ 중지됨, 🔄 생성 중 + +특수 지시: +- 서버 관리와 무관한 메시지 → "__PASSTHROUGH__"만 응답 +- 세션 종료 필요 → "__SESSION_END__" 추가`; + +// Server Management Tools for Function Calling +const SERVER_TOOLS = [ + { + type: 'function' as const, + function: { + name: 'list_servers', + description: '사용자의 서버 목록 조회', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'get_server_info', + description: '특정 서버 상세 정보 조회', + parameters: { + type: 'object', + properties: { + order_id: { type: 'number', description: '주문 번호' }, + }, + required: ['order_id'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'order_server', + description: '새 서버 주문 (pricing_id와 label 필수)', + parameters: { + type: 'object', + properties: { + pricing_id: { type: 'number', description: 'Pricing ID (서버 스펙 ID)' }, + label: { type: 'string', description: '서버 라벨 (예: myapp-prod)' }, + image: { type: 'string', description: 'OS 이미지 키 (선택, 예: ubuntu_22_04)' }, + }, + required: ['pricing_id', 'label'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'start_server', + description: '서버 시작', + parameters: { + type: 'object', + properties: { + order_id: { type: 'number', description: '주문 번호' }, + }, + required: ['order_id'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'stop_server', + description: '서버 중지', + parameters: { + type: 'object', + properties: { + order_id: { type: 'number', description: '주문 번호' }, + }, + required: ['order_id'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'reboot_server', + description: '서버 재시작', + parameters: { + type: 'object', + properties: { + order_id: { type: 'number', description: '주문 번호' }, + }, + required: ['order_id'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'delete_server', + description: '서버 삭제 (되돌릴 수 없음, 확인 필요)', + parameters: { + type: 'object', + properties: { + order_id: { type: 'number', description: '주문 번호' }, + }, + required: ['order_id'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'rename_server', + description: '서버 이름 변경', + parameters: { + type: 'object', + properties: { + order_id: { type: 'number', description: '주문 번호' }, + new_label: { type: 'string', description: '새 서버 이름' }, + }, + required: ['order_id', 'new_label'], + }, + }, + }, + { + type: 'function' as const, + function: { + name: 'list_images', + description: '사용 가능한 OS 이미지 목록', + parameters: { + type: 'object', + properties: {}, + required: [], + }, + }, + }, +]; + +/** + * Server Expert AI 호출 (Function Calling 지원) + * + * ddos-agent 패턴: 도구 실행 결과를 AI에 다시 전달하여 + * AI가 결과를 해석하고 자연어 응답을 생성 + */ +async function callServerExpertAI( + session: ServerSession, + userMessage: string, + telegramUserId: string, + env: Env +): Promise<{ response: string; calledTools: string[] }> { + if (!env.OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY not configured'); + } + + const { getOpenAIUrl } = await import('../utils/api-urls'); + + // Build conversation history + const conversationHistory = session.messages.map(m => ({ + role: m.role === 'user' ? 'user' as const : 'assistant' as const, + content: m.content, + })); + + const systemPrompt = `${SERVER_EXPERT_PROMPT} + +## 현재 세션 정보 +${JSON.stringify(session.collected_info, null, 2)} + +## 도구 사용 가이드 +- 서버 목록 문의 → list_servers +- 서버 상세 정보 → get_server_info (order_id 필요) +- 서버 주문 → order_server (pricing_id, label 필수) +- 서버 시작 → start_server (order_id 필요) +- 서버 중지 → stop_server (order_id 필요) +- 서버 재시작 → reboot_server (order_id 필요) +- 서버 삭제 → delete_server (order_id 필요, 확인 후) +- 이름 변경 → rename_server (order_id, new_label 필요) +- OS 이미지 목록 → list_images`; + + try { + const messages: Array<{ + role: string; + content: string | null; + tool_calls?: OpenAIToolCall[]; + tool_call_id?: string; + name?: string; + }> = [ + { role: 'system', content: systemPrompt }, + ...conversationHistory, + { role: 'user', content: userMessage }, + ]; + + const MAX_TOOL_CALL_ROUNDS = AI_CONFIG.maxToolCalls; + let toolCallRound = 0; + const calledTools: string[] = []; + + // Loop to handle tool calls (execute tools and feed results back to AI) + while (toolCallRound < MAX_TOOL_CALL_ROUNDS) { + const requestBody = { + model: AI_CONFIG.model, + messages, + tools: SERVER_TOOLS, + tool_choice: 'auto', + max_tokens: AI_CONFIG.maxTokens.server, + temperature: AI_CONFIG.temperature.server, + }; + + const response = await fetch(getOpenAIUrl(env), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${env.OPENAI_API_KEY}`, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`OpenAI API error: ${response.status} - ${error}`); + } + + const data = await response.json() as OpenAIAPIResponse; + const assistantMessage = data.choices[0].message; + + // Check if AI wants to call tools + if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) { + logger.info('도구 호출 요청', { + tools: assistantMessage.tool_calls.map(tc => tc.function.name), + }); + + // Add assistant message with tool_calls to conversation + messages.push({ + role: 'assistant', + content: assistantMessage.content, + tool_calls: assistantMessage.tool_calls, + }); + + // Execute each tool and add results back to conversation + for (const toolCall of assistantMessage.tool_calls) { + let args: Record; + try { + args = JSON.parse(toolCall.function.arguments); + } catch (parseError) { + logger.error('도구 인자 JSON 파싱 실패', parseError as Error, { + toolName: toolCall.function.name, + arguments: toolCall.function.arguments?.slice(0, 200), + }); + messages.push({ + role: 'tool', + tool_call_id: toolCall.id, + name: toolCall.function.name, + content: JSON.stringify({ error: '도구 인자 파싱 실패' }), + }); + continue; + } + + const result = await executeServerToolCall(toolCall.function.name, args, telegramUserId, env); + + messages.push({ + role: 'tool', + tool_call_id: toolCall.id, + name: toolCall.function.name, + content: result, + }); + + calledTools.push(toolCall.function.name); + } + + // Count this round and continue loop for AI to process results + toolCallRound++; + continue; + } + + // No tool calls - return final response + const aiResponse = assistantMessage.content || ''; + logger.info('AI 응답', { response: aiResponse.slice(0, 200) }); + + // Check for special markers + if (aiResponse.includes('__PASSTHROUGH__')) { + return { response: '__PASSTHROUGH__', calledTools }; + } + + // Check for session end marker + const sessionEnd = aiResponse.includes('__SESSION_END__'); + const cleanResponse = aiResponse.replace('__SESSION_END__', '').trim(); + + return { + response: sessionEnd ? `${cleanResponse}\n\n[세션 종료]` : cleanResponse, + calledTools, + }; + } + + // Max tool call rounds reached + logger.warn('최대 도구 호출 라운드 도달', { toolCallRound, totalToolsCalled: calledTools.length }); + return { + response: '서버 정보를 확인했습니다.', + calledTools, + }; + } catch (error) { + logger.error('Server Expert AI 호출 실패', error as Error); + throw error; + } +} + +/** + * 서버 도구 실행 디스패처 + * + * Server Agent AI가 호출한 tool을 실제 executeServerAction으로 매핑 + */ +async function executeServerToolCall( + toolName: string, + args: Record, + telegramUserId: string, + env: Env +): Promise { + logger.info('서버 도구 실행', { toolName, args }); + + // Map agent tool names to server-tool action names + const actionMap: Record = { + list_servers: 'list', + get_server_info: 'info', + order_server: 'order', + start_server: 'start', + stop_server: 'stop', + reboot_server: 'reboot', + delete_server: 'delete', + rename_server: 'rename', + list_images: 'images', + }; + + const action = actionMap[toolName]; + if (!action) { + return `❌ 알 수 없는 도구: ${toolName}`; + } + + try { + const result = await executeServerAction( + action, + { + order_id: args.order_id as number | undefined, + pricing_id: args.pricing_id as number | undefined, + label: args.label as string | undefined, + new_label: args.new_label as string | undefined, + image: args.image as string | undefined, + }, + env, + telegramUserId + ); + + // Strip __DIRECT__ marker (agent formats its own responses) + return result.replace('__DIRECT__\n', ''); + } catch (error) { + logger.error('서버 도구 실행 실패', error as Error, { toolName, args }); + return '❌ 처리 중 오류가 발생했습니다.'; + } +} + +/** + * 서버 관리 상담 처리 (메인 함수) + * + * @param db - D1 Database + * @param userId - Telegram User ID + * @param userMessage - 사용자 메시지 + * @param env - Environment + * @returns AI 응답 메시지 + */ +export async function processServerConsultation( + db: D1Database, + userId: string, + userMessage: string, + env: Env +): Promise { + const startTime = Date.now(); + logger.info('서버 관리 상담 시작', { userId, message: userMessage.substring(0, 100) }); + + try { + // 1. Check for existing session + let session = await sessionManager.get(db, userId); + + // 2. Create new session if none exists + if (!session) { + session = sessionManager.create(userId, 'managing'); + } + + // 3. Add user message to session + sessionManager.addMessage(session, 'user', userMessage); + + // 4. Call AI (tools are executed inside the loop, AI interprets results) + const aiResult = await callServerExpertAI(session, userMessage, userId, env); + + // 5. Handle __PASSTHROUGH__ - not server related + if (aiResult.response === '__PASSTHROUGH__' || aiResult.response.includes('__PASSTHROUGH__')) { + logger.info('서버 관리 패스스루', { userId }); + return '__PASSTHROUGH__'; + } + + // 6. Handle __SESSION_END__ - session complete + if (aiResult.response.includes('[세션 종료]')) { + logger.info('서버 관리 세션 종료', { userId }); + await sessionManager.delete(db, userId); + return aiResult.response.replace('[세션 종료]', '').trim(); + } + + // 7. Add assistant response to session and save + sessionManager.addMessage(session, 'assistant', aiResult.response); + session.updated_at = Date.now(); + await sessionManager.save(db, session); + + logger.info('서버 관리 상담 완료', { + userId, + duration: Date.now() - startTime, + calledTools: aiResult.calledTools.length, + }); + + return aiResult.response; + } catch (error) { + logger.error('서버 관리 상담 오류', error as Error, { userId }); + return '죄송합니다. 서버 관리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; + } +} diff --git a/src/openai-service.ts b/src/openai-service.ts index 9faeb6e..1378ef8 100644 --- a/src/openai-service.ts +++ b/src/openai-service.ts @@ -10,6 +10,7 @@ import { processTroubleshootConsultation, hasTroubleshootSession } from './agent import { processDomainConsultation, hasDomainSession } from './agents/domain-agent'; import { processDepositConsultation, hasDepositSession } from './agents/deposit-agent'; import { processDdosConsultation, hasDdosSession } from './agents/ddos-agent'; +import { processServerConsultation, hasServerSession } from './agents/server-agent'; const logger = createLogger('openai'); @@ -296,6 +297,29 @@ export async function generateOpenAIResponse( }); // Continue with normal flow if session check fails } + + // Check for active server management session + try { + const hasServerSess = await hasServerSession(env.DB, telegramUserId); + + if (hasServerSess) { + logger.info('서버 관리 세션 감지, Server 에이전트로 라우팅', { + userId: telegramUserId + }); + const serverResponse = await processServerConsultation(env.DB, telegramUserId, userMessage, env); + + // PASSTHROUGH: 무관한 메시지는 일반 처리로 전환 + if (serverResponse !== '__PASSTHROUGH__') { + return serverResponse; + } + // Continue to normal flow below + } + } catch (error) { + logger.error('Server session check failed, continuing with normal flow', error as Error, { + telegramUserId + }); + // Continue with normal flow if session check fails + } } if (!env.OPENAI_API_KEY) { diff --git a/src/tools/server-tool.ts b/src/tools/server-tool.ts index e9ff529..d8b582e 100644 --- a/src/tools/server-tool.ts +++ b/src/tools/server-tool.ts @@ -903,15 +903,15 @@ export async function executeServerDelete( } } - // NOTE: Server consultation session cleanup removed (recommendation feature deprecated) - // try { - // 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); - // } catch (error) { - // provisionLogger.error('서버 세션 삭제 실패 (무시)', error as Error); - // } + // Clean up server management session after deletion + try { + const { SessionManager } = await import('../utils/session-manager'); + const { getSessionConfig } = await import('../constants/agent-config'); + const sm = new SessionManager(getSessionConfig('server')); + await sm.delete(env.DB, telegramUserId); + } catch (sessionError) { + provisionLogger.error('서버 세션 삭제 실패 (무시)', sessionError as Error); + } provisionLogger.info('서버 삭제 완료', { orderId }); return { @@ -994,15 +994,15 @@ export async function executeServerOrder( const order = result.order; provisionLogger.info('서버 주문 완료', { orderId: order?.id, plan: orderData.plan }); - // NOTE: Server consultation session cleanup removed (recommendation feature deprecated) - // try { - // 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); - // } catch (error) { - // provisionLogger.error('서버 세션 삭제 실패 (무시)', error as Error); - // } + // Clean up server management session after order + try { + const { SessionManager } = await import('../utils/session-manager'); + const { getSessionConfig } = await import('../constants/agent-config'); + const sm = new SessionManager(getSessionConfig('server')); + await sm.delete(env.DB, telegramUserId); + } catch (sessionError) { + provisionLogger.error('서버 세션 삭제 실패 (무시)', sessionError as Error); + } // Build success message let successMessage = `✅ 서버 신청이 완료되었습니다!\n\n`; @@ -1026,6 +1026,41 @@ export async function executeServerOrder( }; } +/** + * Build natural language message from structured args + */ +function buildServerMessage(args: { + action: string; + order_id?: number; + pricing_id?: number; + label?: string; + new_label?: string; + image?: string; +}): string { + switch (args.action) { + case 'list': + return '내 서버 목록 보여줘'; + case 'info': + return `${args.order_id}번 서버 정보 알려줘`; + case 'start': + return `${args.order_id}번 서버 시작해줘`; + case 'stop': + return `${args.order_id}번 서버 중지해줘`; + case 'reboot': + return `${args.order_id}번 서버 재시작해줘`; + case 'delete': + return `${args.order_id}번 서버 삭제해줘`; + case 'rename': + return `${args.order_id}번 서버 이름을 ${args.new_label}로 변경해줘`; + case 'order': + return `서버 주문: pricing_id=${args.pricing_id}, label=${args.label}${args.image ? `, image=${args.image}` : ''}`; + case 'images': + return 'OS 이미지 목록 보여줘'; + default: + return `서버 ${args.action} 요청`; + } +} + export async function executeManageServer( args: { action: string; @@ -1038,7 +1073,8 @@ export async function executeManageServer( image?: string; }, env?: Env, - telegramUserId?: string + telegramUserId?: string, + db?: D1Database ): Promise { const { action } = args; logger.info('시작', { @@ -1046,10 +1082,28 @@ export async function executeManageServer( userId: maskUserId(telegramUserId), }); + if (!env || !telegramUserId || !db) { + // Fallback to direct execution if missing context + try { + const result = await executeServerAction(action, args, env, telegramUserId); + return result; + } catch (error) { + logger.error('서버 관리 오류', error as Error, { action }); + return '🚫 서버 관리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; + } + } + try { - const result = await executeServerAction(action, args, env, telegramUserId); - logger.info('완료', { result: result?.slice(0, 100) }); - return result; + const userMessage = buildServerMessage(args); + const { processServerConsultation } = await import('../agents/server-agent'); + const response = await processServerConsultation(db, telegramUserId, userMessage, env); + + if (response === '__PASSTHROUGH__') { + return '서버 관련 요청을 처리할 수 없습니다.'; + } + + logger.info('완료', { result: response?.slice(0, 100) }); + return response; } catch (error) { logger.error('서버 관리 오류', error as Error, { action }); return '🚫 서버 관리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; diff --git a/src/types.ts b/src/types.ts index 82cc60d..12e24b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -795,6 +795,20 @@ export interface DdosSession { expires_at: number; } +// Server Management Session Status +export type ServerSessionStatus = 'idle' | 'managing' | 'completed'; + +// Server Management Session (D1) +export interface ServerSession { + user_id: string; + status: ServerSessionStatus; + collected_info: Record; + messages: Array<{ role: 'user' | 'assistant'; content: string }>; + created_at: number; + updated_at: number; + expires_at: number; +} + // DDoS Defense Tool Args export interface ManageDdosArgs { action: diff --git a/src/utils/session-manager.ts b/src/utils/session-manager.ts index 14f9f29..8856bd8 100644 --- a/src/utils/session-manager.ts +++ b/src/utils/session-manager.ts @@ -313,30 +313,5 @@ export class DomainSessionManager extends SessionManager { } } -/** - * Specialized session manager for Server Agent - * Handles last_recommendation field - * - * NOTE: Temporarily commented out during server recommendation removal refactoring. - * This class will be removed in a future commit when server-tool.ts is updated. - */ -// export class ServerSessionManager extends SessionManager { -// protected parseAdditionalFields(result: Record): Partial { -// return { -// last_recommendation: result.last_recommendation -// ? JSON.parse(result.last_recommendation as string) -// : undefined, -// }; -// } -// -// protected getAdditionalColumns(session: ServerSession): Record { -// return { -// last_recommendation: session.last_recommendation -// ? JSON.stringify(session.last_recommendation) -// : null, -// }; -// } -// } - // Import types (avoid circular dependency by importing at end) import type { DomainSession } from '../types';