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 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-07 19:40:58 +09:00
parent 4c1f2f3852
commit ab314b10c4
8 changed files with 660 additions and 99 deletions

103
CLAUDE.md
View File

@@ -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 표시 |

View File

@@ -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);

View File

@@ -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);

469
src/agents/server-agent.ts Normal file
View File

@@ -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<ServerSession>(getSessionConfig('server'));
/**
* 서버 세션 존재 여부 확인 (라우팅용)
*/
export async function hasServerSession(db: D1Database, userId: string): Promise<boolean> {
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<string, unknown>;
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<string, unknown>,
telegramUserId: string,
env: Env
): Promise<string> {
logger.info('서버 도구 실행', { toolName, args });
// Map agent tool names to server-tool action names
const actionMap: Record<string, string> = {
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<string> {
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 '죄송합니다. 서버 관리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
}
}

View File

@@ -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) {

View File

@@ -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<string> {
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 '🚫 서버 관리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';

View File

@@ -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<string, unknown>;
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
created_at: number;
updated_at: number;
expires_at: number;
}
// DDoS Defense Tool Args
export interface ManageDdosArgs {
action:

View File

@@ -313,30 +313,5 @@ export class DomainSessionManager extends SessionManager<DomainSession> {
}
}
/**
* 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<ServerSession> {
// protected parseAdditionalFields(result: Record<string, unknown>): Partial<ServerSession> {
// return {
// last_recommendation: result.last_recommendation
// ? JSON.parse(result.last_recommendation as string)
// : undefined,
// };
// }
//
// protected getAdditionalColumns(session: ServerSession): Record<string, unknown> {
// 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';