refactor: code quality improvements (P3)
## Type Safety
- Add zod runtime validation for external API responses
* Namecheap API responses (domain-register.ts)
* n8n webhook responses (n8n-service.ts)
* User request bodies (routes/api.ts)
* Replaced unsafe type assertions with safeParse()
* Proper error handling and logging
## Dead Code Removal
- Remove unused callDepositAgent function (127 lines)
* Legacy Assistants API code no longer needed
* Now using direct code execution
* File reduced from 469 → 345 lines (26.4% reduction)
## Configuration Management
- Extract hardcoded URLs to environment variables
* Added 7 new vars in wrangler.toml:
OPENAI_API_BASE, NAMECHEAP_API_URL, WHOIS_API_URL,
CONTEXT7_API_BASE, BRAVE_API_BASE, WTTR_IN_URL, HOSTING_SITE_URL
* Updated Env interface in types.ts
* All URLs have fallback to current production values
* Enables environment-specific configuration (dev/staging/prod)
## Dependencies
- Add zod 4.3.5 for runtime type validation
## Files Modified
- Configuration: wrangler.toml, types.ts, package.json
- Services: 11 TypeScript files with URL/validation updates
- Total: 15 files, +196/-189 lines
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string> {
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RegisterResult> {
|
||||
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);
|
||||
|
||||
@@ -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 연결 실패' };
|
||||
|
||||
@@ -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 // 도구 없이 호출
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -80,13 +80,13 @@ export async function executeTool(
|
||||
): Promise<string> {
|
||||
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 });
|
||||
|
||||
@@ -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<string> {
|
||||
export async function executeLookupDocs(args: { library: string; query: string }, env?: Env): Promise<string> {
|
||||
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 }
|
||||
|
||||
@@ -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<string> {
|
||||
export async function executeWeather(args: { city: string }, env?: Env): Promise<string> {
|
||||
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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user