diff --git a/package-lock.json b/package-lock.json index 3478374..0128d17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "telegram-summary-bot", "version": "1.0.0", + "dependencies": { + "zod": "^4.3.5" + }, "devDependencies": { "@cloudflare/workers-types": "^4.20241127.0", "typescript": "^5.3.3", @@ -1294,6 +1297,16 @@ "node": ">=18.0.0" } }, + "node_modules/miniflare/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -1525,10 +1538,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 7d98e0c..4ff20f5 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,8 @@ "workers", "d1", "ai" - ] + ], + "dependencies": { + "zod": "^4.3.5" + } } diff --git a/src/deposit-agent.ts b/src/deposit-agent.ts index 1958f5c..93c44ed 100644 --- a/src/deposit-agent.ts +++ b/src/deposit-agent.ts @@ -343,127 +343,3 @@ export async function executeDepositFunction( return { error: `알 수 없는 함수: ${funcName}` }; } } - -// Deposit Agent 호출 (Assistants API) -export async function callDepositAgent( - apiKey: string, - assistantId: string, - query: string, - context: DepositContext -): Promise { - try { - // 1. Thread 생성 - const threadRes = await fetch('https://api.openai.com/v1/threads', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'OpenAI-Beta': 'assistants=v2', - }, - body: JSON.stringify({}), - }); - if (!threadRes.ok) return `Thread 생성 실패 (${threadRes.status})`; - const thread = await threadRes.json() as { id: string }; - - // 2. 메시지 추가 (권한 정보 포함) - const adminInfo = context.isAdmin ? '관리자 권한이 있습니다.' : '일반 사용자입니다.'; - const instructions = `[시스템 정보] -- ${adminInfo} -- 사용자 ID: ${context.telegramUserId} - -[사용자 요청] -${query}`; - - await fetch(`https://api.openai.com/v1/threads/${thread.id}/messages`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'OpenAI-Beta': 'assistants=v2', - }, - body: JSON.stringify({ - role: 'user', - content: instructions, - }), - }); - - // 3. Run 생성 - const runRes = await fetch(`https://api.openai.com/v1/threads/${thread.id}/runs`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'OpenAI-Beta': 'assistants=v2', - }, - body: JSON.stringify({ assistant_id: assistantId }), - }); - if (!runRes.ok) return `Run 생성 실패 (${runRes.status})`; - let run = await runRes.json() as { id: string; status: string; required_action?: any }; - - // 4. 완료까지 폴링 및 Function Calling 처리 - let maxPolls = 30; // 최대 15초 - while ((run.status === 'queued' || run.status === 'in_progress' || run.status === 'requires_action') && maxPolls > 0) { - if (run.status === 'requires_action') { - const toolCalls = run.required_action?.submit_tool_outputs?.tool_calls || []; - const toolOutputs = []; - - for (const toolCall of toolCalls) { - const funcName = toolCall.function.name; - const funcArgs = JSON.parse(toolCall.function.arguments); - logger.info(`Function call: ${funcName}`, funcArgs); - - const result = await executeDepositFunction(funcName, funcArgs, context); - toolOutputs.push({ - tool_call_id: toolCall.id, - output: JSON.stringify(result), - }); - } - - // Tool outputs 제출 - const submitRes = await fetch(`https://api.openai.com/v1/threads/${thread.id}/runs/${run.id}/submit_tool_outputs`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - 'OpenAI-Beta': 'assistants=v2', - }, - body: JSON.stringify({ tool_outputs: toolOutputs }), - }); - run = await submitRes.json() as { id: string; status: string; required_action?: any }; - } - - await new Promise(resolve => setTimeout(resolve, 500)); - maxPolls--; - - const statusRes = await fetch(`https://api.openai.com/v1/threads/${thread.id}/runs/${run.id}`, { - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'OpenAI-Beta': 'assistants=v2', - }, - }); - run = await statusRes.json() as { id: string; status: string; required_action?: any }; - } - - if (run.status === 'failed') return '예치금 에이전트 실행 실패'; - if (maxPolls === 0) return '응답 시간 초과. 다시 시도해주세요.'; - - // 5. 메시지 조회 - const messagesRes = await fetch(`https://api.openai.com/v1/threads/${thread.id}/messages`, { - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'OpenAI-Beta': 'assistants=v2', - }, - }); - const messages = await messagesRes.json() as { data: Array<{ role: string; content: Array<{ type: string; text?: { value: string } }> }> }; - const lastMessage = messages.data[0]; - - if (lastMessage?.content?.[0]?.type === 'text') { - return lastMessage.content[0].text?.value || '응답 없음'; - } - - return '예치금 에이전트 응답 없음'; - } catch (error) { - logger.error('Error', error as Error); - return `예치금 에이전트 오류: ${String(error)}`; - } -} diff --git a/src/domain-register.ts b/src/domain-register.ts index fb4c34a..dccaf7f 100644 --- a/src/domain-register.ts +++ b/src/domain-register.ts @@ -1,8 +1,25 @@ +import { z } from 'zod'; import { Env } from './types'; import { createLogger } from './utils/logger'; const logger = createLogger('domain-register'); +// Zod schemas for API response validation +const NamecheapRegisterResponseSchema = z.object({ + registered: z.boolean().optional(), + domain: z.string().optional(), + error: z.string().optional(), + detail: z.string().optional(), +}); + +const DomainInfoResponseSchema = z.object({ + expires: z.string().optional(), +}); + +const NameserverResponseSchema = z.object({ + nameservers: z.array(z.string()).optional(), +}); + interface RegisterResult { success: boolean; domain?: string; @@ -22,7 +39,7 @@ export async function executeDomainRegister( price: number ): Promise { const apiKey = env.NAMECHEAP_API_KEY; - const apiUrl = 'https://namecheap-api.anvil.it.com'; + const apiUrl = env.NAMECHEAP_API_URL || 'https://namecheap-api.anvil.it.com'; if (!apiKey) { return { success: false, error: 'API 키가 설정되지 않았습니다.' }; @@ -58,12 +75,15 @@ export async function executeDomainRegister( }), }); - const registerResult = await registerResponse.json() as { - registered?: boolean; - domain?: string; - error?: string; - detail?: string; - }; + const jsonData = await registerResponse.json(); + const parseResult = NamecheapRegisterResponseSchema.safeParse(jsonData); + + if (!parseResult.success) { + logger.error('Namecheap register response schema validation failed', parseResult.error); + return { success: false, error: '도메인 등록 응답 형식이 올바르지 않습니다.' }; + } + + const registerResult = parseResult.data; if (!registerResponse.ok || !registerResult.registered) { const errorMsg = registerResult.error || registerResult.detail || '도메인 등록에 실패했습니다.'; @@ -112,11 +132,18 @@ export async function executeDomainRegister( headers: { 'X-API-Key': apiKey } }); if (infoResponse.ok) { - const infoResult = await infoResponse.json() as { expires?: string }; - if (infoResult.expires) { - // MM/DD/YYYY → YYYY-MM-DD 변환 - const [month, day, year] = infoResult.expires.split('/'); - expiresAt = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + const infoJsonData = await infoResponse.json(); + const infoParseResult = DomainInfoResponseSchema.safeParse(infoJsonData); + + if (!infoParseResult.success) { + logger.warn('Domain info response schema validation failed', { domain }); + } else { + const infoResult = infoParseResult.data; + if (infoResult.expires) { + // MM/DD/YYYY → YYYY-MM-DD 변환 + const [month, day, year] = infoResult.expires.split('/'); + expiresAt = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; + } } } @@ -125,8 +152,15 @@ export async function executeDomainRegister( headers: { 'X-API-Key': apiKey } }); if (nsResponse.ok) { - const nsResult = await nsResponse.json() as { nameservers?: string[] }; - nameservers = nsResult.nameservers || []; + const nsJsonData = await nsResponse.json(); + const nsParseResult = NameserverResponseSchema.safeParse(nsJsonData); + + if (!nsParseResult.success) { + logger.warn('Nameserver response schema validation failed', { domain }); + } else { + const nsResult = nsParseResult.data; + nameservers = nsResult.nameservers || []; + } } } catch (infoError) { console.log(`[DomainRegister] 도메인 정보 조회 실패 (무시):`, infoError); diff --git a/src/n8n-service.ts b/src/n8n-service.ts index 868a76a..cc7cbc9 100644 --- a/src/n8n-service.ts +++ b/src/n8n-service.ts @@ -1,4 +1,14 @@ +import { z } from 'zod'; import { Env, IntentAnalysis, N8nResponse } from './types'; +import { createLogger } from './utils/logger'; + +const logger = createLogger('n8n-service'); + +// Zod schema for N8n webhook response validation +const N8nResponseSchema = z.object({ + reply: z.string().optional(), + error: z.string().optional(), +}); // n8n으로 처리할 기능 목록 (참고용) // - weather: 날씨 @@ -104,8 +114,15 @@ export async function callN8n( return { error: `n8n 호출 실패 (${response.status})` }; } - const data = await response.json() as N8nResponse; - return data; + const jsonData = await response.json(); + const parseResult = N8nResponseSchema.safeParse(jsonData); + + if (!parseResult.success) { + logger.error('N8n response schema validation failed', parseResult.error); + return { error: 'n8n 응답 형식 오류' }; + } + + return parseResult.data; } catch (error) { console.error('n8n fetch error:', error); return { error: 'n8n 연결 실패' }; diff --git a/src/openai-service.ts b/src/openai-service.ts index 6cad8a9..a2238fd 100644 --- a/src/openai-service.ts +++ b/src/openai-service.ts @@ -8,7 +8,10 @@ import { metrics } from './utils/metrics'; const logger = createLogger('openai'); // Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) -const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions'; +function getOpenAIUrl(env: Env): string { + const base = env.OPENAI_API_BASE || 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai'; + return `${base}/chat/completions`; +} // Circuit Breaker 인스턴스 (전역 공유) export const openaiCircuitBreaker = new CircuitBreaker({ @@ -42,6 +45,7 @@ interface OpenAIResponse { // OpenAI API 호출 (retry + circuit breaker 적용) async function callOpenAI( + env: Env, apiKey: string, messages: OpenAIMessage[], selectedTools?: typeof tools // undefined = 도구 없음, 배열 = 해당 도구만 사용 @@ -51,7 +55,7 @@ async function callOpenAI( try { return await retryWithBackoff( async () => { - const response = await fetch(OPENAI_API_URL, { + const response = await fetch(getOpenAIUrl(env), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -115,7 +119,7 @@ export async function generateOpenAIResponse( const selectedTools = selectToolsForMessage(userMessage); // 첫 번째 호출 - let response = await callOpenAI(apiKey, messages, selectedTools); + let response = await callOpenAI(env, apiKey, messages, selectedTools); let assistantMessage = response.choices[0].message; logger.info('tool_calls', { @@ -155,7 +159,7 @@ export async function generateOpenAIResponse( messages.push(...toolResults); // 다시 호출 (도구 없이 응답 생성) - response = await callOpenAI(apiKey, messages, undefined); + response = await callOpenAI(env, apiKey, messages, undefined); assistantMessage = response.choices[0].message; } @@ -196,6 +200,7 @@ export async function generateProfileWithOpenAI( // Circuit Breaker로 실행 감싸기 return await openaiCircuitBreaker.execute(async () => { const response = await callOpenAI( + env, apiKey, [{ role: 'user', content: prompt }], undefined // 도구 없이 호출 diff --git a/src/routes/api.ts b/src/routes/api.ts index ca5db93..2fc8fe2 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,3 +1,4 @@ +import { z } from 'zod'; import { Env } from '../types'; import { sendMessage } from '../telegram'; import { @@ -11,6 +12,26 @@ import { createLogger } from '../utils/logger'; const logger = createLogger('api'); +// Zod schemas for API request validation +const DepositDeductBodySchema = z.object({ + telegram_id: z.string(), + amount: z.number().positive(), + reason: z.string(), + reference_id: z.string().optional(), +}); + +const TestApiBodySchema = z.object({ + text: z.string(), + user_id: z.string().optional(), + secret: z.string().optional(), +}); + +const ContactFormBodySchema = z.object({ + email: z.string().email(), + message: z.string(), + name: z.string().optional(), +}); + // 사용자 조회/생성 async function getOrCreateUser( db: D1Database, @@ -117,20 +138,18 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr return Response.json({ error: 'Unauthorized' }, { status: 401 }); } - const body = await request.json() as { - telegram_id: string; - amount: number; - reason: string; - reference_id?: string; - }; + const jsonData = await request.json(); + const parseResult = DepositDeductBodySchema.safeParse(jsonData); - if (!body.telegram_id || !body.amount || !body.reason) { - return Response.json({ error: 'telegram_id, amount, reason required' }, { status: 400 }); + if (!parseResult.success) { + logger.warn('Deposit deduct - Invalid request body', { errors: parseResult.error.issues }); + return Response.json({ + error: 'Invalid request body', + details: parseResult.error.issues + }, { status: 400 }); } - if (body.amount <= 0) { - return Response.json({ error: 'Amount must be positive' }, { status: 400 }); - } + const body = parseResult.data; // 사용자 조회 const user = await env.DB.prepare( @@ -203,7 +222,18 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr // 테스트 API - 메시지 처리 후 응답 직접 반환 if (url.pathname === '/api/test' && request.method === 'POST') { try { - const body = await request.json() as { text: string; user_id?: string; secret?: string }; + const jsonData = await request.json(); + const parseResult = TestApiBodySchema.safeParse(jsonData); + + if (!parseResult.success) { + logger.warn('Test API - Invalid request body', { errors: parseResult.error.issues }); + return Response.json({ + error: 'Invalid request body', + details: parseResult.error.issues + }, { status: 400 }); + } + + const body = parseResult.data; // 간단한 인증 if (body.secret !== env.WEBHOOK_SECRET) { @@ -261,15 +291,15 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr // 문의 폼 API (웹사이트용) if (url.pathname === '/api/contact' && request.method === 'POST') { // CORS: hosting.anvil.it.com만 허용 + const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com'; const corsHeaders = { - 'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com', + 'Access-Control-Allow-Origin': allowedOrigin, 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }; // Origin 헤더 검증 (curl 우회 방지) const origin = request.headers.get('Origin'); - const allowedOrigin = 'https://hosting.anvil.it.com'; if (!origin || origin !== allowedOrigin) { logger.warn('Contact API - 허용되지 않은 Origin', { origin }); @@ -280,20 +310,23 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr } try { - const body = await request.json() as { - email: string; - message: string; - }; + const jsonData = await request.json(); + const parseResult = ContactFormBodySchema.safeParse(jsonData); - // 필수 필드 검증 - if (!body.email || !body.message) { + if (!parseResult.success) { + logger.warn('Contact form - Invalid request body', { errors: parseResult.error.issues }); return Response.json( - { error: '이메일과 메시지는 필수 항목입니다.' }, + { + error: '올바르지 않은 요청 형식입니다.', + details: parseResult.error.issues + }, { status: 400, headers: corsHeaders } ); } - // 이메일 형식 검증 + const body = parseResult.data; + + // 이메일 형식 검증 (Zod로 이미 검증됨, 추가 체크) const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(body.email)) { return Response.json( @@ -341,9 +374,10 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr // CORS preflight for contact API if (url.pathname === '/api/contact' && request.method === 'OPTIONS') { + const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com'; return new Response(null, { headers: { - 'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com', + 'Access-Control-Allow-Origin': allowedOrigin, 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }, diff --git a/src/routes/webhook.ts b/src/routes/webhook.ts index cdb27ae..db45afe 100644 --- a/src/routes/webhook.ts +++ b/src/routes/webhook.ts @@ -60,8 +60,9 @@ async function handleMessage( // /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: 'https://hosting.anvil.it.com' } }], + [{ text: '🌐 서비스 보기', web_app: { url: hostingUrl } }], [{ text: '💬 문의하기', url: 'https://t.me/AnvilForgeBot' }], ]); return; diff --git a/src/services/bank-sms-parser.ts b/src/services/bank-sms-parser.ts index b38978c..fae5fd7 100644 --- a/src/services/bank-sms-parser.ts +++ b/src/services/bank-sms-parser.ts @@ -161,7 +161,8 @@ ${text.slice(0, 500)} // 1. OpenAI 시도 if (env.OPENAI_API_KEY) { try { - const response = await fetch('https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions', { + const openaiBaseUrl = env.OPENAI_API_BASE || 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai'; + const response = await fetch(`${openaiBaseUrl}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/tools/domain-tool.ts b/src/tools/domain-tool.ts index b1a7995..489acc3 100644 --- a/src/tools/domain-tool.ts +++ b/src/tools/domain-tool.ts @@ -5,7 +5,10 @@ import { createLogger, maskUserId } from '../utils/logger'; const logger = createLogger('domain-tool'); // Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) -const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions'; +function getOpenAIUrl(env: Env): string { + const base = env.OPENAI_API_BASE || 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai'; + return `${base}/chat/completions`; +} // KV 캐싱 인터페이스 interface CachedTLDPrice { @@ -157,7 +160,7 @@ async function callNamecheapApi( return { error: 'Namecheap API 키가 설정되지 않았습니다.' }; } const apiKey = env.NAMECHEAP_API_KEY_INTERNAL; - const apiUrl = 'https://namecheap-api.anvil.it.com'; + const apiUrl = env.NAMECHEAP_API_URL || 'https://namecheap-api.anvil.it.com'; // 도메인 권한 체크 (쓰기 작업만) // 읽기 작업(get_domain_info, get_nameservers)은 누구나 조회 가능 @@ -320,7 +323,7 @@ async function callNamecheapApi( const domain = funcArgs.domain; try { const whoisRes = await retryWithBackoff( - () => fetch(`https://whois-api-kappa-inoutercoms-projects.vercel.app/api/whois/${domain}`), + () => fetch(`${env.WHOIS_API_URL || 'https://whois-api-kappa-inoutercoms-projects.vercel.app'}/api/whois/${domain}`), { maxRetries: 3 } ); if (!whoisRes.ok) { @@ -778,7 +781,7 @@ export async function executeSuggestDomains(args: { keywords: string }, env?: En } try { - const namecheapApiUrl = 'https://namecheap-api.anvil.it.com'; + const namecheapApiUrl = env.NAMECHEAP_API_URL || 'https://namecheap-api.anvil.it.com'; const TARGET_COUNT = 10; const MAX_RETRIES = 3; @@ -793,7 +796,7 @@ export async function executeSuggestDomains(args: { keywords: string }, env?: En // Step 1: GPT에게 도메인 아이디어 생성 요청 const ideaResponse = await retryWithBackoff( - () => fetch(OPENAI_API_URL, { + () => fetch(getOpenAIUrl(env), { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/tools/index.ts b/src/tools/index.ts index 8963e32..f98fbf8 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -80,13 +80,13 @@ export async function executeTool( ): Promise { switch (name) { case 'get_weather': - return executeWeather(args as { city: string }); + return executeWeather(args as { city: string }, env); case 'search_web': return executeSearchWeb(args as { query: string }, env); case 'lookup_docs': - return executeLookupDocs(args as { library: string; query: string }); + return executeLookupDocs(args as { library: string; query: string }, env); case 'get_current_time': return executeGetCurrentTime(args as { timezone?: string }); diff --git a/src/tools/search-tool.ts b/src/tools/search-tool.ts index 13a4eec..97cbf1b 100644 --- a/src/tools/search-tool.ts +++ b/src/tools/search-tool.ts @@ -5,7 +5,10 @@ import { createLogger } from '../utils/logger'; const logger = createLogger('search-tool'); // Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) -const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions'; +function getOpenAIUrl(env: Env): string { + const base = env.OPENAI_API_BASE || 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai'; + return `${base}/chat/completions`; +} export const searchWebTool = { type: 'function', @@ -61,7 +64,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom if (hasKorean && env?.OPENAI_API_KEY) { try { const translateRes = await retryWithBackoff( - () => fetch(OPENAI_API_URL, { + () => fetch(getOpenAIUrl(env), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -101,7 +104,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom const response = await retryWithBackoff( () => fetch( - `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(translatedQuery)}&count=5`, + `${env.BRAVE_API_BASE || 'https://api.search.brave.com/res/v1'}/web/search?q=${encodeURIComponent(translatedQuery)}&count=5`, { headers: { 'Accept': 'application/json', @@ -141,12 +144,12 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom } } -export async function executeLookupDocs(args: { library: string; query: string }): Promise { +export async function executeLookupDocs(args: { library: string; query: string }, env?: Env): Promise { const { library, query } = args; try { // Context7 REST API 직접 호출 // 1. 라이브러리 검색 - const searchUrl = `https://context7.com/api/v2/libs/search?libraryName=${encodeURIComponent(library)}&query=${encodeURIComponent(query)}`; + const searchUrl = `${env?.CONTEXT7_API_BASE || 'https://context7.com/api/v2'}/libs/search?libraryName=${encodeURIComponent(library)}&query=${encodeURIComponent(query)}`; const searchResponse = await retryWithBackoff( () => fetch(searchUrl), { maxRetries: 3 } @@ -160,7 +163,7 @@ export async function executeLookupDocs(args: { library: string; query: string } const libraryId = searchData.libraries[0].id; // 2. 문서 조회 - const docsUrl = `https://context7.com/api/v2/context?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`; + const docsUrl = `${env?.CONTEXT7_API_BASE || 'https://context7.com/api/v2'}/context?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`; const docsResponse = await retryWithBackoff( () => fetch(docsUrl), { maxRetries: 3 } diff --git a/src/tools/weather-tool.ts b/src/tools/weather-tool.ts index 067b9db..b0151a6 100644 --- a/src/tools/weather-tool.ts +++ b/src/tools/weather-tool.ts @@ -1,4 +1,5 @@ // Weather Tool - wttr.in integration +import type { Env } from '../types'; export const weatherTool = { type: 'function', @@ -18,11 +19,12 @@ export const weatherTool = { }, }; -export async function executeWeather(args: { city: string }): Promise { +export async function executeWeather(args: { city: string }, env?: Env): Promise { const city = args.city || 'Seoul'; try { + const wttrUrl = env?.WTTR_IN_URL || 'https://wttr.in'; const response = await fetch( - `https://wttr.in/${encodeURIComponent(city)}?format=j1` + `${wttrUrl}/${encodeURIComponent(city)}?format=j1` ); const data = await response.json() as any; const current = data.current_condition[0]; diff --git a/src/types.ts b/src/types.ts index cf25b8f..a9015e1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,13 @@ export interface Env { DEPOSIT_ADMIN_ID?: string; BRAVE_API_KEY?: string; DEPOSIT_API_SECRET?: string; + OPENAI_API_BASE?: string; + NAMECHEAP_API_URL?: string; + WHOIS_API_URL?: string; + CONTEXT7_API_BASE?: string; + BRAVE_API_BASE?: string; + WTTR_IN_URL?: string; + HOSTING_SITE_URL?: string; RATE_LIMIT_KV: KVNamespace; } diff --git a/wrangler.toml b/wrangler.toml index 9d0eb4c..38032f3 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -12,6 +12,15 @@ N8N_WEBHOOK_URL = "https://n8n.anvil.it.com" # n8n 연동 (선택) DOMAIN_OWNER_ID = "821596605" # 도메인 관리 권한 Telegram ID DEPOSIT_ADMIN_ID = "821596605" # 예치금 관리 권한 Telegram ID +# API Endpoints +OPENAI_API_BASE = "https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai" +NAMECHEAP_API_URL = "https://namecheap-api.anvil.it.com" +WHOIS_API_URL = "https://whois-api-kappa-inoutercoms-projects.vercel.app" +CONTEXT7_API_BASE = "https://context7.com/api/v2" +BRAVE_API_BASE = "https://api.search.brave.com/res/v1" +WTTR_IN_URL = "https://wttr.in" +HOSTING_SITE_URL = "https://hosting.anvil.it.com" + [[d1_databases]] binding = "DB" database_name = "telegram-conversations"