Files
telegram-bot-workers/src/routes/handlers/message-handler.ts
kappa 87c92e1ed1 refactor: migrate server provisioning to Cloud Orchestrator service
- Remove Queue-based server provisioning (moved to cloud-orchestrator)
- Add manage_server tool with Service Binding to Cloud Orchestrator
- Add CDN cache hit rate estimation based on tech_stack
- Always display bandwidth info (show "포함 범위 내" when no overage)
- Add language auto-detection (ko, ja, zh, en)
- Update system prompt to always call tools fresh
- Add Server System documentation to CLAUDE.md

BREAKING: Server provisioning now requires cloud-orchestrator service

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 12:26:21 +09:00

121 lines
3.8 KiB
TypeScript

import { sendMessage, sendMessageWithKeyboard } from '../../telegram';
import { checkRateLimit } from '../../security';
import { handleCommand } from '../../commands';
import { UserService } from '../../services/user-service';
import { ConversationService } from '../../services/conversation-service';
import { ERROR_MESSAGES } from '../../constants/messages';
import type { Env, TelegramUpdate } from '../../types';
/**
* 메시지 처리 핸들러
*/
export async function handleMessage(
env: Env,
update: TelegramUpdate
): Promise<void> {
if (!update.message?.text) return;
const { message } = update;
const chatId = message.chat.id;
const chatIdStr = chatId.toString();
const text = message.text!;
const telegramUserId = message.from.id.toString();
// 1. Rate Limiting 체크
if (!(await checkRateLimit(env.RATE_LIMIT_KV, telegramUserId))) {
await sendMessage(
env.BOT_TOKEN,
chatId,
'⚠️ 너무 많은 요청입니다. 잠시 후 다시 시도해주세요.'
);
return;
}
// 2. 서비스 인스턴스 초기화
const userService = new UserService(env.DB);
const conversationService = new ConversationService(env);
// 3. 사용자 조회/생성
let userId: number;
try {
userId = await userService.getOrCreateUser(
telegramUserId,
message.from.first_name,
message.from.username
);
} catch (dbError) {
console.error('[handleMessage] 사용자 DB 오류:', dbError);
await sendMessage(
env.BOT_TOKEN,
chatId,
ERROR_MESSAGES.TEMPORARY_ERROR
);
return;
}
try {
// 4. 명령어 처리
if (text.startsWith('/')) {
const [command, ...argParts] = text.split(' ');
const args = argParts.join(' ');
const responseText = await handleCommand(env, userId, chatIdStr, command, args);
// /start 명령어는 미니앱 버튼과 함께 전송
if (command === '/start') {
const hostingUrl = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, responseText, [
[{ text: '🌐 서비스 보기', web_app: { url: hostingUrl } }],
[{ text: '💬 문의하기', url: 'https://t.me/AnvilForgeBot' }],
]);
return;
}
await sendMessage(env.BOT_TOKEN, chatId, responseText);
return;
}
// 5. 일반 대화 처리 (ConversationService 위임)
const result = await conversationService.processUserMessage(
userId,
chatIdStr,
text,
telegramUserId
);
let finalResponse = result.responseText;
if (result.isProfileUpdated) {
finalResponse += '\n\n<i>👤 프로필이 업데이트되었습니다.</i>';
}
// 6. 응답 전송 (키보드 포함 여부 확인)
if (result.keyboardData) {
console.log('[Webhook] Keyboard data received:', result.keyboardData.type);
if (result.keyboardData.type === 'domain_register') {
const { domain, price } = result.keyboardData;
const callbackData = `domain_reg:${domain}:${price}`;
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, finalResponse, [
[
{ text: '✅ 등록하기', callback_data: callbackData },
{ text: '❌ 취소', callback_data: 'domain_cancel' }
]
]);
} else {
// TypeScript exhaustiveness check - should never reach here
console.warn('[Webhook] Unknown keyboard type:', (result.keyboardData as { type: string }).type);
await sendMessage(env.BOT_TOKEN, chatId, finalResponse);
}
} else {
await sendMessage(env.BOT_TOKEN, chatId, finalResponse);
}
} catch (error) {
console.error('[handleMessage] 처리 오류:', error);
await sendMessage(
env.BOT_TOKEN,
chatId,
'⚠️ 메시지 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'
);
}
}