- Show "신청 가능한 서버보기" link when no servers - Show link at bottom of server list Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
371 lines
11 KiB
TypeScript
371 lines
11 KiB
TypeScript
import { Env } from './types';
|
|
import { getConversationContext, getLatestSummary } from './summary-service';
|
|
import {
|
|
getConversationHistory,
|
|
searchConversations,
|
|
getConversationStats,
|
|
extractKeywords
|
|
} from './services/conversation-storage';
|
|
|
|
export async function handleCommand(
|
|
env: Env,
|
|
userId: number,
|
|
chatId: string,
|
|
command: string,
|
|
_args: string
|
|
): Promise<string> {
|
|
|
|
switch (command) {
|
|
case '/start':
|
|
return `👋 <b>AnvilHosting 고객센터</b>입니다!
|
|
|
|
<b>제공 서비스:</b>
|
|
• 🌐 도메인 등록/관리
|
|
• 🖥️ 클라우드 서버 (서울/도쿄/오사카/싱가폴)
|
|
• 🛡️ DDoS 방어
|
|
• 🔐 PhantomX VPN (Xray 기반 차세대 보안)
|
|
|
|
<b>명령어:</b>
|
|
/help - 도움말
|
|
/deposit - 예치금 잔액
|
|
/domain - 내 도메인 목록
|
|
/server - 내 서버 목록
|
|
/security - DDoS 방어 현황
|
|
/phantomx - PhantomX VPN
|
|
/history - 대화 기록
|
|
/search - 대화 검색
|
|
|
|
무엇을 도와드릴까요?`;
|
|
|
|
case '/help':
|
|
return `📖 <b>도움말</b>
|
|
|
|
<b>명령어:</b>
|
|
/deposit - 예치금 잔액
|
|
/domain - 내 도메인 목록
|
|
/server - 내 서버 목록
|
|
/security - DDoS 방어 서비스
|
|
/phantomx - PhantomX VPN 서비스
|
|
/history [N] - 대화 기록 (기본 20개)
|
|
/search 키워드 - 대화 검색
|
|
|
|
<b>자연어로 요청:</b>
|
|
• "도메인 등록" - 도메인 검색/등록
|
|
|
|
궁금한 점은 편하게 물어보세요!`;
|
|
|
|
case '/deposit': {
|
|
const deposit = await env.DB
|
|
.prepare('SELECT balance FROM user_deposits WHERE user_id = ?')
|
|
.bind(userId)
|
|
.first<{ balance: number }>();
|
|
|
|
const balance = deposit?.balance ?? 0;
|
|
|
|
return `💰 <b>예치금 잔액</b>
|
|
|
|
현재 잔액: <b>${balance.toLocaleString()}원</b>
|
|
|
|
<b>입금 계좌:</b>
|
|
하나은행 427-910018-27104
|
|
예금주: (주)아이언클래드
|
|
|
|
입금 후 "홍길동 10000원 입금" 형식으로 알려주세요.`;
|
|
}
|
|
|
|
case '/context': {
|
|
const ctx = await getConversationContext(env.DB, userId, chatId);
|
|
|
|
return `📊 <b>현재 컨텍스트</b>
|
|
|
|
총 메시지: ${ctx.totalMessages}개
|
|
버퍼: ${ctx.recentMessages.length}개`;
|
|
}
|
|
|
|
case '/profile':
|
|
case '/summary': {
|
|
const summary = await getLatestSummary(env.DB, userId, chatId);
|
|
if (!summary) {
|
|
return '📭 아직 프로필이 없습니다.\n대화를 더 나눠보세요!';
|
|
}
|
|
const createdAt = new Date(summary.created_at).toLocaleString('ko-KR', {
|
|
timeZone: 'Asia/Seoul',
|
|
});
|
|
return `👤 <b>내 프로필</b> (v${summary.generation})
|
|
|
|
${summary.summary}
|
|
|
|
<i>분석된 메시지: ${summary.message_count}개</i>
|
|
<i>업데이트: ${createdAt}</i>`;
|
|
}
|
|
|
|
case '/history': {
|
|
const limit = _args ? parseInt(_args, 10) : 20;
|
|
const validLimit = Math.min(Math.max(limit, 1), 100); // 1-100 제한
|
|
|
|
const messages = await getConversationHistory(env.DB, chatId, validLimit);
|
|
|
|
if (messages.length === 0) {
|
|
return '📜 저장된 대화가 없습니다.';
|
|
}
|
|
|
|
const formatted = messages.map(m => {
|
|
const time = m.created_at
|
|
? new Date(m.created_at).toLocaleString('ko-KR', {
|
|
timeZone: 'Asia/Seoul',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
: '';
|
|
const role = m.role === 'user' ? '나' : '봇';
|
|
const content = m.content.length > 50
|
|
? m.content.substring(0, 50) + '...'
|
|
: m.content;
|
|
return `[${time}] ${role}: ${content}`;
|
|
}).join('\n');
|
|
|
|
return `📜 <b>최근 대화</b> (${messages.length}개)\n\n${formatted}\n\n/history 50 으로 더 보기`;
|
|
}
|
|
|
|
case '/search': {
|
|
if (!_args || _args.trim().length < 2) {
|
|
return '🔍 검색어를 입력해주세요.\n예: /search 도메인';
|
|
}
|
|
|
|
const keywords = extractKeywords(_args);
|
|
if (keywords.length === 0) {
|
|
return '🔍 유효한 검색어가 없습니다.';
|
|
}
|
|
|
|
const results = await searchConversations(env.DB, chatId, keywords, 15);
|
|
|
|
if (results.length === 0) {
|
|
return `🔍 "${_args}" 검색 결과가 없습니다.`;
|
|
}
|
|
|
|
const formatted = results.map(m => {
|
|
const date = m.created_at
|
|
? new Date(m.created_at).toLocaleDateString('ko-KR')
|
|
: '';
|
|
const content = m.content.length > 60
|
|
? m.content.substring(0, 60) + '...'
|
|
: m.content;
|
|
return `[${date}] ${content}`;
|
|
}).join('\n');
|
|
|
|
return `🔍 <b>"${_args}"</b> 검색 결과 (${results.length}건)\n\n${formatted}`;
|
|
}
|
|
|
|
case '/stats': {
|
|
const stats = await getConversationStats(env.DB, chatId);
|
|
|
|
if (!stats) {
|
|
return '📈 아직 대화 기록이 없습니다.';
|
|
}
|
|
|
|
const firstDate = stats.first_message_at
|
|
? new Date(stats.first_message_at).toLocaleDateString('ko-KR')
|
|
: '없음';
|
|
const lastDate = stats.last_message_at
|
|
? new Date(stats.last_message_at).toLocaleDateString('ko-KR')
|
|
: '없음';
|
|
|
|
return `📈 <b>대화 통계</b>
|
|
|
|
총 메시지: ${stats.message_count.toLocaleString()}개
|
|
첫 대화: ${firstDate}
|
|
최근 대화: ${lastDate}
|
|
아카이브된 요약: ${stats.archived_summaries}개
|
|
|
|
/history - 대화 기록 보기
|
|
/search 키워드 - 대화 검색`;
|
|
}
|
|
|
|
case '/domain': {
|
|
const domains = await env.DB
|
|
.prepare(`
|
|
SELECT domain, created_at
|
|
FROM user_domains
|
|
WHERE user_id = ? AND verified = 1
|
|
ORDER BY created_at DESC
|
|
`)
|
|
.bind(userId)
|
|
.all<{ domain: string; created_at: string }>();
|
|
|
|
if (!domains.results || domains.results.length === 0) {
|
|
return `🌐 <b>내 도메인</b>
|
|
|
|
등록된 도메인이 없습니다.
|
|
|
|
"도메인 등록" 또는 "example.com 등록"으로 시작하세요!`;
|
|
}
|
|
|
|
const domainList = domains.results.map((d, i) => {
|
|
const date = new Date(d.created_at).toLocaleDateString('ko-KR');
|
|
return `${i + 1}. ${d.domain} (${date})`;
|
|
}).join('\n');
|
|
|
|
return `🌐 <b>내 도메인</b> (${domains.results.length}개)
|
|
|
|
${domainList}
|
|
|
|
도메인 관리: "도메인명 네임서버 변경"`;
|
|
}
|
|
|
|
case '/server': {
|
|
// Cloud Orchestrator API를 통해 스펙 정보 포함된 서버 목록 조회
|
|
const telegramUserId = chatId; // chatId가 실제로는 telegram_user_id
|
|
|
|
interface ServerWithSpecs {
|
|
id: number;
|
|
label: string | null;
|
|
status: string;
|
|
region: string;
|
|
vcpu?: number;
|
|
memory_gb?: number;
|
|
bandwidth_tb?: number;
|
|
spec_name?: string;
|
|
}
|
|
|
|
let servers: ServerWithSpecs[] = [];
|
|
|
|
if (env.CLOUD_ORCHESTRATOR) {
|
|
try {
|
|
const response = await env.CLOUD_ORCHESTRATOR.fetch(`https://internal/api/provision/orders?user_id=${telegramUserId}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json() as { orders?: ServerWithSpecs[] };
|
|
servers = data.orders || [];
|
|
}
|
|
} catch {
|
|
// API 실패 시 로컬 DB 폴백
|
|
}
|
|
}
|
|
|
|
// API 실패 시 로컬 DB에서 조회 (스펙 정보 없이)
|
|
if (servers.length === 0) {
|
|
const localServers = await env.DB
|
|
.prepare(`
|
|
SELECT id, label, status, region
|
|
FROM server_orders
|
|
WHERE telegram_user_id = ? AND status IN ('active', 'stopped', 'provisioning')
|
|
ORDER BY created_at DESC
|
|
`)
|
|
.bind(telegramUserId)
|
|
.all<{ id: number; label: string; status: string; region: string }>();
|
|
|
|
servers = localServers.results || [];
|
|
}
|
|
|
|
if (servers.length === 0) {
|
|
return `🖥️ <b>내 서버</b>
|
|
|
|
보유한 서버가 없습니다.
|
|
|
|
<a href="https://hosting.anvil.it.com/servers">🛒 신청 가능한 서버보기</a>`;
|
|
}
|
|
|
|
const statusIcon: Record<string, string> = {
|
|
active: '🟢',
|
|
stopped: '🔴',
|
|
provisioning: '🟡',
|
|
};
|
|
|
|
const serverList = servers
|
|
.filter(s => ['active', 'stopped', 'provisioning'].includes(s.status))
|
|
.map((s) => {
|
|
const icon = statusIcon[s.status] || '⚪';
|
|
const label = s.label || '(이름없음)';
|
|
|
|
// 스펙 정보가 있으면 표시
|
|
let specInfo = '';
|
|
if (s.vcpu && s.memory_gb) {
|
|
specInfo = `\n ${s.vcpu}vCPU / ${s.memory_gb}GB RAM`;
|
|
if (s.bandwidth_tb) {
|
|
specInfo += ` / ${s.bandwidth_tb}TB`;
|
|
}
|
|
}
|
|
|
|
return `#${s.id} ${icon} <b>${label}</b> (${s.region})${specInfo}`;
|
|
}).join('\n\n');
|
|
|
|
return `🖥️ <b>내 서버</b> (${servers.filter(s => ['active', 'stopped', 'provisioning'].includes(s.status)).length}개)
|
|
|
|
${serverList}
|
|
|
|
서버 관리: "N번 시작/중지" 또는 "#N 재시작"
|
|
<a href="https://hosting.anvil.it.com/servers">🛒 신청 가능한 서버보기</a>`;
|
|
}
|
|
|
|
case '/security': {
|
|
return `🛡️ <b>DDoS 방어 서비스</b>
|
|
|
|
<b>AnvilShield</b> - 엔터프라이즈급 DDoS 방어
|
|
|
|
• 🌐 L3/L4 네트워크 공격 방어
|
|
• 🔒 L7 애플리케이션 공격 방어
|
|
• 📊 실시간 트래픽 모니터링
|
|
• 🤖 자동 위협 탐지 및 차단
|
|
|
|
<b>요금제:</b>
|
|
• 💚 Basic: 10Gbps 방어 - ₩99,000/월
|
|
• 💙 Pro: 100Gbps 방어 - ₩299,000/월
|
|
• 💜 Enterprise: 무제한 - 별도 문의
|
|
|
|
🔜 <i>서비스 준비 중입니다. 문의: @AnvilSupport</i>`;
|
|
}
|
|
|
|
case '/phantomx': {
|
|
return `🔐 <b>PhantomX VPN</b>
|
|
|
|
<b>Xray 기반 차세대 보안 VPN</b>
|
|
|
|
• 🚀 초고속 연결 (Xray-core 엔진)
|
|
• 👻 트래픽 위장 (탐지 우회)
|
|
• 🌍 글로벌 서버 (한국/일본/미국/유럽)
|
|
• 📱 멀티 디바이스 지원
|
|
• 🔒 제로 로그 정책
|
|
|
|
<b>요금제:</b>
|
|
• 월간: ₩9,900/월
|
|
• 연간: ₩79,000/년 (33% 할인)
|
|
|
|
🔜 <i>서비스 준비 중입니다. 문의: @AnvilSupport</i>`;
|
|
}
|
|
|
|
case '/debug': {
|
|
// Admin only - exposes internal debug info
|
|
const adminId = env.DEPOSIT_ADMIN_ID ? parseInt(env.DEPOSIT_ADMIN_ID, 10) : null;
|
|
if (!adminId || userId !== adminId) {
|
|
return '🔒 이 명령어는 관리자만 사용할 수 있습니다.';
|
|
}
|
|
|
|
// 디버그용 명령어 (개발 시 유용)
|
|
const ctx = await getConversationContext(env.DB, userId, chatId);
|
|
const recentMsgs = ctx.recentMessages.slice(-5).map((m, i) =>
|
|
`${i + 1}. [${m.role}] ${m.message.substring(0, 30)}...`
|
|
).join('\n');
|
|
|
|
return `🔧 <b>디버그 정보</b>
|
|
|
|
User ID: ${userId}
|
|
Chat ID: ${chatId}
|
|
Buffer Count: ${ctx.recentMessages.length}
|
|
Summary Gen: ${ctx.previousSummary?.generation ?? 0}
|
|
|
|
<b>최근 버퍼 (5개):</b>
|
|
${recentMsgs || '(비어있음)'}`;
|
|
}
|
|
|
|
default:
|
|
return '❓ 알 수 없는 명령어입니다.\n/help를 입력해보세요.';
|
|
}
|
|
}
|