// Tool Registry - All tools exported from here import { z } from 'zod'; import { createLogger } from '../utils/logger'; const logger = createLogger('tools'); import { weatherTool, executeWeather } from './weather-tool'; import { searchWebTool, lookupDocsTool, executeSearchWeb, executeLookupDocs } from './search-tool'; import { manageDomainTool, suggestDomainsTool, executeManageDomain, executeSuggestDomains } from './domain-tool'; import { manageDepositTool, executeManageDeposit } from './deposit-tool'; import { manageServerTool, executeManageServer } from './server-tool'; import { getCurrentTimeTool, calculateTool, executeGetCurrentTime, executeCalculate } from './utility-tools'; import { redditSearchTool, executeRedditSearch } from './reddit-tool'; import type { Env } from '../types'; // Zod validation schemas for tool arguments const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,251}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$/; const ManageDomainArgsSchema = z.object({ action: z.enum(['register', 'check', 'whois', 'list', 'info', 'get_ns', 'set_ns', 'price', 'cheapest']), domain: z.string().max(253).regex(DOMAIN_REGEX, 'Invalid domain format').optional(), nameservers: z.array(z.string().max(255)).max(10).optional(), tld: z.string().max(20).optional(), }); const ManageDepositArgsSchema = z.object({ action: z.enum(['balance', 'account', 'request', 'history', 'cancel', 'pending', 'confirm', 'reject']), depositor_name: z.string().max(100).optional(), amount: z.number().positive().max(100_000_000).optional(), // 1억원 상한 transaction_id: z.number().int().positive().optional(), limit: z.number().int().positive().max(100).optional(), }); const SearchWebArgsSchema = z.object({ query: z.string().min(1).max(500), }); const GetWeatherArgsSchema = z.object({ city: z.string().min(1).max(100), }); const CalculateArgsSchema = z.object({ expression: z.string().min(1).max(200), }); const GetCurrentTimeArgsSchema = z.object({ timezone: z.string().max(50).optional(), }); const LookupDocsArgsSchema = z.object({ library: z.string().min(1).max(100), query: z.string().min(1).max(500), }); const SuggestDomainsArgsSchema = z.object({ keywords: z.string().min(1).max(500), }); const RedditSearchArgsSchema = z.object({ query: z.string().min(1).max(500), limit: z.number().int().positive().max(25).optional(), sort: z.enum(['hot', 'new', 'top', 'relevance']).optional(), }); const ManageServerArgsSchema = z.object({ action: z.enum(['recommend', 'order', 'start', 'stop', 'delete', 'list', 'start_consultation', 'continue_consultation', 'cancel_consultation']), tech_stack: z.array(z.string().min(1).max(100)).max(20).optional(), expected_users: z.number().int().positive().optional(), use_case: z.string().min(1).max(500).optional(), traffic_pattern: z.enum(['steady', 'burst', 'high']).optional(), region_preference: z.array(z.string().min(1).max(50)).max(10).optional(), budget_limit: z.number().positive().optional(), lang: z.enum(['ko', 'en']).optional(), server_id: z.string().min(1).max(100).optional(), region_code: z.string().min(1).max(50).optional(), label: z.string().min(1).max(100).optional(), message: z.string().min(1).max(500).optional(), // For continue_consultation }); // All tools array (used by OpenAI API) export const tools = [ weatherTool, searchWebTool, getCurrentTimeTool, calculateTool, lookupDocsTool, manageDomainTool, manageDepositTool, manageServerTool, suggestDomainsTool, redditSearchTool, ]; // Tool categories for dynamic loading (auto-generated from tool definitions) export const TOOL_CATEGORIES: Record = { domain: [manageDomainTool.function.name, suggestDomainsTool.function.name], deposit: [manageDepositTool.function.name], server: [manageServerTool.function.name], weather: [weatherTool.function.name], search: [searchWebTool.function.name, lookupDocsTool.function.name], reddit: [redditSearchTool.function.name], utility: [getCurrentTimeTool.function.name, calculateTool.function.name], }; // Category detection patterns export const CATEGORY_PATTERNS: Record = { domain: /도메인|네임서버|whois|dns|tld|등록|\.com|\.net|\.io|\.kr|\.org/i, deposit: /입금|충전|잔액|계좌|예치금|송금|돈/i, server: /서버|VPS|클라우드|호스팅|인스턴스|linode|vultr/i, weather: /날씨|기온|비|눈|맑|흐림|더워|추워/i, search: /검색|찾아|뭐야|뉴스|최신/i, reddit: /레딧|reddit|서브레딧|subreddit/i, }; // Message-based tool selection export function selectToolsForMessage(message: string): typeof tools { const selectedCategories = new Set(['utility']); // 항상 포함 for (const [category, pattern] of Object.entries(CATEGORY_PATTERNS)) { if (pattern.test(message)) { selectedCategories.add(category); } } // 패턴 매칭 없으면 전체 도구 사용 (폴백) if (selectedCategories.size === 1) { logger.info('패턴 매칭 없음 → 전체 도구 사용'); return tools; } const selectedNames = new Set( [...selectedCategories].flatMap(cat => TOOL_CATEGORIES[cat] || []) ); const selectedTools = tools.filter(t => selectedNames.has(t.function.name)); logger.info('도구 선택 완료', { message, categories: [...selectedCategories].join(', '), selectedTools: selectedTools.map(t => t.function.name).join(', ') }); return selectedTools; } // Tool execution dispatcher with validation export async function executeTool( name: string, args: Record, env?: Env, telegramUserId?: string, db?: D1Database ): Promise { try { switch (name) { case 'get_weather': { const result = GetWeatherArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid weather args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeWeather(result.data, env); } case 'search_web': { const result = SearchWebArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid search args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeSearchWeb(result.data, env); } case 'lookup_docs': { const result = LookupDocsArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid lookup_docs args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeLookupDocs(result.data, env); } case 'get_current_time': { const result = GetCurrentTimeArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid time args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeGetCurrentTime(result.data); } case 'calculate': { const result = CalculateArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid calculate args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeCalculate(result.data); } case 'manage_domain': { const result = ManageDomainArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid domain args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeManageDomain(result.data, env, telegramUserId, db); } case 'suggest_domains': { const result = SuggestDomainsArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid suggest_domains args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeSuggestDomains(result.data, env); } case 'manage_deposit': { const result = ManageDepositArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid deposit args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeManageDeposit(result.data, env, telegramUserId, db); } case 'manage_server': { const result = ManageServerArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid server args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeManageServer(result.data, env, telegramUserId); } case 'search_reddit': { const result = RedditSearchArgsSchema.safeParse(args); if (!result.success) { logger.error('Invalid reddit args', new Error(result.error.message), { args }); return `❌ Invalid arguments: ${result.error.issues.map(e => e.message).join(', ')}`; } return executeRedditSearch(result.data, env); } default: return `알 수 없는 도구: ${name}`; } } catch (error) { logger.error('Tool execution error', error as Error, { name, args }); return `⚠️ 도구 실행 중 오류가 발생했습니다.`; } }