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:
kappa
2026-01-19 22:06:01 +09:00
parent 4f68dd3ebb
commit 45e0677ab0
15 changed files with 196 additions and 189 deletions

20
package-lock.json generated
View File

@@ -7,6 +7,9 @@
"": { "": {
"name": "telegram-summary-bot", "name": "telegram-summary-bot",
"version": "1.0.0", "version": "1.0.0",
"dependencies": {
"zod": "^4.3.5"
},
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20241127.0", "@cloudflare/workers-types": "^4.20241127.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
@@ -1294,6 +1297,16 @@
"node": ">=18.0.0" "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": { "node_modules/path-to-regexp": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@@ -1525,10 +1538,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.76", "version": "4.3.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -24,5 +24,8 @@
"workers", "workers",
"d1", "d1",
"ai" "ai"
] ],
"dependencies": {
"zod": "^4.3.5"
}
} }

View File

@@ -343,127 +343,3 @@ export async function executeDepositFunction(
return { error: `알 수 없는 함수: ${funcName}` }; 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)}`;
}
}

View File

@@ -1,8 +1,25 @@
import { z } from 'zod';
import { Env } from './types'; import { Env } from './types';
import { createLogger } from './utils/logger'; import { createLogger } from './utils/logger';
const logger = createLogger('domain-register'); 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 { interface RegisterResult {
success: boolean; success: boolean;
domain?: string; domain?: string;
@@ -22,7 +39,7 @@ export async function executeDomainRegister(
price: number price: number
): Promise<RegisterResult> { ): Promise<RegisterResult> {
const apiKey = env.NAMECHEAP_API_KEY; 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) { if (!apiKey) {
return { success: false, error: 'API 키가 설정되지 않았습니다.' }; return { success: false, error: 'API 키가 설정되지 않았습니다.' };
@@ -58,12 +75,15 @@ export async function executeDomainRegister(
}), }),
}); });
const registerResult = await registerResponse.json() as { const jsonData = await registerResponse.json();
registered?: boolean; const parseResult = NamecheapRegisterResponseSchema.safeParse(jsonData);
domain?: string;
error?: string; if (!parseResult.success) {
detail?: string; logger.error('Namecheap register response schema validation failed', parseResult.error);
}; return { success: false, error: '도메인 등록 응답 형식이 올바르지 않습니다.' };
}
const registerResult = parseResult.data;
if (!registerResponse.ok || !registerResult.registered) { if (!registerResponse.ok || !registerResult.registered) {
const errorMsg = registerResult.error || registerResult.detail || '도메인 등록에 실패했습니다.'; const errorMsg = registerResult.error || registerResult.detail || '도메인 등록에 실패했습니다.';
@@ -112,22 +132,36 @@ export async function executeDomainRegister(
headers: { 'X-API-Key': apiKey } headers: { 'X-API-Key': apiKey }
}); });
if (infoResponse.ok) { if (infoResponse.ok) {
const infoResult = await infoResponse.json() as { expires?: string }; 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) { if (infoResult.expires) {
// MM/DD/YYYY → YYYY-MM-DD 변환 // MM/DD/YYYY → YYYY-MM-DD 변환
const [month, day, year] = infoResult.expires.split('/'); const [month, day, year] = infoResult.expires.split('/');
expiresAt = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; expiresAt = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
} }
} }
}
// 네임서버 조회 // 네임서버 조회
const nsResponse = await fetch(`${apiUrl}/domains/${domain}/nameservers`, { const nsResponse = await fetch(`${apiUrl}/domains/${domain}/nameservers`, {
headers: { 'X-API-Key': apiKey } headers: { 'X-API-Key': apiKey }
}); });
if (nsResponse.ok) { if (nsResponse.ok) {
const nsResult = await nsResponse.json() as { nameservers?: string[] }; 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 || []; nameservers = nsResult.nameservers || [];
} }
}
} catch (infoError) { } catch (infoError) {
console.log(`[DomainRegister] 도메인 정보 조회 실패 (무시):`, infoError); console.log(`[DomainRegister] 도메인 정보 조회 실패 (무시):`, infoError);
} }

View File

@@ -1,4 +1,14 @@
import { z } from 'zod';
import { Env, IntentAnalysis, N8nResponse } from './types'; 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으로 처리할 기능 목록 (참고용) // n8n으로 처리할 기능 목록 (참고용)
// - weather: 날씨 // - weather: 날씨
@@ -104,8 +114,15 @@ export async function callN8n(
return { error: `n8n 호출 실패 (${response.status})` }; return { error: `n8n 호출 실패 (${response.status})` };
} }
const data = await response.json() as N8nResponse; const jsonData = await response.json();
return data; 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) { } catch (error) {
console.error('n8n fetch error:', error); console.error('n8n fetch error:', error);
return { error: 'n8n 연결 실패' }; return { error: 'n8n 연결 실패' };

View File

@@ -8,7 +8,10 @@ import { metrics } from './utils/metrics';
const logger = createLogger('openai'); const logger = createLogger('openai');
// Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) // 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 인스턴스 (전역 공유) // Circuit Breaker 인스턴스 (전역 공유)
export const openaiCircuitBreaker = new CircuitBreaker({ export const openaiCircuitBreaker = new CircuitBreaker({
@@ -42,6 +45,7 @@ interface OpenAIResponse {
// OpenAI API 호출 (retry + circuit breaker 적용) // OpenAI API 호출 (retry + circuit breaker 적용)
async function callOpenAI( async function callOpenAI(
env: Env,
apiKey: string, apiKey: string,
messages: OpenAIMessage[], messages: OpenAIMessage[],
selectedTools?: typeof tools // undefined = 도구 없음, 배열 = 해당 도구만 사용 selectedTools?: typeof tools // undefined = 도구 없음, 배열 = 해당 도구만 사용
@@ -51,7 +55,7 @@ async function callOpenAI(
try { try {
return await retryWithBackoff( return await retryWithBackoff(
async () => { async () => {
const response = await fetch(OPENAI_API_URL, { const response = await fetch(getOpenAIUrl(env), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -115,7 +119,7 @@ export async function generateOpenAIResponse(
const selectedTools = selectToolsForMessage(userMessage); 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; let assistantMessage = response.choices[0].message;
logger.info('tool_calls', { logger.info('tool_calls', {
@@ -155,7 +159,7 @@ export async function generateOpenAIResponse(
messages.push(...toolResults); messages.push(...toolResults);
// 다시 호출 (도구 없이 응답 생성) // 다시 호출 (도구 없이 응답 생성)
response = await callOpenAI(apiKey, messages, undefined); response = await callOpenAI(env, apiKey, messages, undefined);
assistantMessage = response.choices[0].message; assistantMessage = response.choices[0].message;
} }
@@ -196,6 +200,7 @@ export async function generateProfileWithOpenAI(
// Circuit Breaker로 실행 감싸기 // Circuit Breaker로 실행 감싸기
return await openaiCircuitBreaker.execute(async () => { return await openaiCircuitBreaker.execute(async () => {
const response = await callOpenAI( const response = await callOpenAI(
env,
apiKey, apiKey,
[{ role: 'user', content: prompt }], [{ role: 'user', content: prompt }],
undefined // 도구 없이 호출 undefined // 도구 없이 호출

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import { Env } from '../types'; import { Env } from '../types';
import { sendMessage } from '../telegram'; import { sendMessage } from '../telegram';
import { import {
@@ -11,6 +12,26 @@ import { createLogger } from '../utils/logger';
const logger = createLogger('api'); 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( async function getOrCreateUser(
db: D1Database, db: D1Database,
@@ -117,20 +138,18 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
return Response.json({ error: 'Unauthorized' }, { status: 401 }); return Response.json({ error: 'Unauthorized' }, { status: 401 });
} }
const body = await request.json() as { const jsonData = await request.json();
telegram_id: string; const parseResult = DepositDeductBodySchema.safeParse(jsonData);
amount: number;
reason: string;
reference_id?: string;
};
if (!body.telegram_id || !body.amount || !body.reason) { if (!parseResult.success) {
return Response.json({ error: 'telegram_id, amount, reason required' }, { status: 400 }); 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) { const body = parseResult.data;
return Response.json({ error: 'Amount must be positive' }, { status: 400 });
}
// 사용자 조회 // 사용자 조회
const user = await env.DB.prepare( const user = await env.DB.prepare(
@@ -203,7 +222,18 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
// 테스트 API - 메시지 처리 후 응답 직접 반환 // 테스트 API - 메시지 처리 후 응답 직접 반환
if (url.pathname === '/api/test' && request.method === 'POST') { if (url.pathname === '/api/test' && request.method === 'POST') {
try { 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) { if (body.secret !== env.WEBHOOK_SECRET) {
@@ -261,15 +291,15 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
// 문의 폼 API (웹사이트용) // 문의 폼 API (웹사이트용)
if (url.pathname === '/api/contact' && request.method === 'POST') { if (url.pathname === '/api/contact' && request.method === 'POST') {
// CORS: hosting.anvil.it.com만 허용 // CORS: hosting.anvil.it.com만 허용
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
const corsHeaders = { 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-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Headers': 'Content-Type',
}; };
// Origin 헤더 검증 (curl 우회 방지) // Origin 헤더 검증 (curl 우회 방지)
const origin = request.headers.get('Origin'); const origin = request.headers.get('Origin');
const allowedOrigin = 'https://hosting.anvil.it.com';
if (!origin || origin !== allowedOrigin) { if (!origin || origin !== allowedOrigin) {
logger.warn('Contact API - 허용되지 않은 Origin', { origin }); logger.warn('Contact API - 허용되지 않은 Origin', { origin });
@@ -280,20 +310,23 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
} }
try { try {
const body = await request.json() as { const jsonData = await request.json();
email: string; const parseResult = ContactFormBodySchema.safeParse(jsonData);
message: string;
};
// 필수 필드 검증 if (!parseResult.success) {
if (!body.email || !body.message) { logger.warn('Contact form - Invalid request body', { errors: parseResult.error.issues });
return Response.json( return Response.json(
{ error: '이메일과 메시지는 필수 항목입니다.' }, {
error: '올바르지 않은 요청 형식입니다.',
details: parseResult.error.issues
},
{ status: 400, headers: corsHeaders } { status: 400, headers: corsHeaders }
); );
} }
// 이메일 형식 검증 const body = parseResult.data;
// 이메일 형식 검증 (Zod로 이미 검증됨, 추가 체크)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(body.email)) { if (!emailRegex.test(body.email)) {
return Response.json( return Response.json(
@@ -341,9 +374,10 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
// CORS preflight for contact API // CORS preflight for contact API
if (url.pathname === '/api/contact' && request.method === 'OPTIONS') { if (url.pathname === '/api/contact' && request.method === 'OPTIONS') {
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
return new Response(null, { return new Response(null, {
headers: { headers: {
'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com', 'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Headers': 'Content-Type',
}, },

View File

@@ -60,8 +60,9 @@ async function handleMessage(
// /start 명령어는 미니앱 버튼과 함께 전송 // /start 명령어는 미니앱 버튼과 함께 전송
if (command === '/start') { if (command === '/start') {
const hostingUrl = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, responseText, [ 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' }], [{ text: '💬 문의하기', url: 'https://t.me/AnvilForgeBot' }],
]); ]);
return; return;

View File

@@ -161,7 +161,8 @@ ${text.slice(0, 500)}
// 1. OpenAI 시도 // 1. OpenAI 시도
if (env.OPENAI_API_KEY) { if (env.OPENAI_API_KEY) {
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -5,7 +5,10 @@ import { createLogger, maskUserId } from '../utils/logger';
const logger = createLogger('domain-tool'); const logger = createLogger('domain-tool');
// Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) // 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 캐싱 인터페이스 // KV 캐싱 인터페이스
interface CachedTLDPrice { interface CachedTLDPrice {
@@ -157,7 +160,7 @@ async function callNamecheapApi(
return { error: 'Namecheap API 키가 설정되지 않았습니다.' }; return { error: 'Namecheap API 키가 설정되지 않았습니다.' };
} }
const apiKey = env.NAMECHEAP_API_KEY_INTERNAL; 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)은 누구나 조회 가능 // 읽기 작업(get_domain_info, get_nameservers)은 누구나 조회 가능
@@ -320,7 +323,7 @@ async function callNamecheapApi(
const domain = funcArgs.domain; const domain = funcArgs.domain;
try { try {
const whoisRes = await retryWithBackoff( 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 } { maxRetries: 3 }
); );
if (!whoisRes.ok) { if (!whoisRes.ok) {
@@ -778,7 +781,7 @@ export async function executeSuggestDomains(args: { keywords: string }, env?: En
} }
try { 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 TARGET_COUNT = 10;
const MAX_RETRIES = 3; const MAX_RETRIES = 3;
@@ -793,7 +796,7 @@ export async function executeSuggestDomains(args: { keywords: string }, env?: En
// Step 1: GPT에게 도메인 아이디어 생성 요청 // Step 1: GPT에게 도메인 아이디어 생성 요청
const ideaResponse = await retryWithBackoff( const ideaResponse = await retryWithBackoff(
() => fetch(OPENAI_API_URL, { () => fetch(getOpenAIUrl(env), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -80,13 +80,13 @@ export async function executeTool(
): Promise<string> { ): Promise<string> {
switch (name) { switch (name) {
case 'get_weather': case 'get_weather':
return executeWeather(args as { city: string }); return executeWeather(args as { city: string }, env);
case 'search_web': case 'search_web':
return executeSearchWeb(args as { query: string }, env); return executeSearchWeb(args as { query: string }, env);
case 'lookup_docs': case 'lookup_docs':
return executeLookupDocs(args as { library: string; query: string }); return executeLookupDocs(args as { library: string; query: string }, env);
case 'get_current_time': case 'get_current_time':
return executeGetCurrentTime(args as { timezone?: string }); return executeGetCurrentTime(args as { timezone?: string });

View File

@@ -5,7 +5,10 @@ import { createLogger } from '../utils/logger';
const logger = createLogger('search-tool'); const logger = createLogger('search-tool');
// Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회) // 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 = { export const searchWebTool = {
type: 'function', type: 'function',
@@ -61,7 +64,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom
if (hasKorean && env?.OPENAI_API_KEY) { if (hasKorean && env?.OPENAI_API_KEY) {
try { try {
const translateRes = await retryWithBackoff( const translateRes = await retryWithBackoff(
() => fetch(OPENAI_API_URL, { () => fetch(getOpenAIUrl(env), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -101,7 +104,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom
const response = await retryWithBackoff( const response = await retryWithBackoff(
() => fetch( () => 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: { headers: {
'Accept': 'application/json', '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; const { library, query } = args;
try { try {
// Context7 REST API 직접 호출 // Context7 REST API 직접 호출
// 1. 라이브러리 검색 // 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( const searchResponse = await retryWithBackoff(
() => fetch(searchUrl), () => fetch(searchUrl),
{ maxRetries: 3 } { maxRetries: 3 }
@@ -160,7 +163,7 @@ export async function executeLookupDocs(args: { library: string; query: string }
const libraryId = searchData.libraries[0].id; const libraryId = searchData.libraries[0].id;
// 2. 문서 조회 // 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( const docsResponse = await retryWithBackoff(
() => fetch(docsUrl), () => fetch(docsUrl),
{ maxRetries: 3 } { maxRetries: 3 }

View File

@@ -1,4 +1,5 @@
// Weather Tool - wttr.in integration // Weather Tool - wttr.in integration
import type { Env } from '../types';
export const weatherTool = { export const weatherTool = {
type: 'function', 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'; const city = args.city || 'Seoul';
try { try {
const wttrUrl = env?.WTTR_IN_URL || 'https://wttr.in';
const response = await fetch( const response = await fetch(
`https://wttr.in/${encodeURIComponent(city)}?format=j1` `${wttrUrl}/${encodeURIComponent(city)}?format=j1`
); );
const data = await response.json() as any; const data = await response.json() as any;
const current = data.current_condition[0]; const current = data.current_condition[0];

View File

@@ -13,6 +13,13 @@ export interface Env {
DEPOSIT_ADMIN_ID?: string; DEPOSIT_ADMIN_ID?: string;
BRAVE_API_KEY?: string; BRAVE_API_KEY?: string;
DEPOSIT_API_SECRET?: 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; RATE_LIMIT_KV: KVNamespace;
} }

View File

@@ -12,6 +12,15 @@ N8N_WEBHOOK_URL = "https://n8n.anvil.it.com" # n8n 연동 (선택)
DOMAIN_OWNER_ID = "821596605" # 도메인 관리 권한 Telegram ID DOMAIN_OWNER_ID = "821596605" # 도메인 관리 권한 Telegram ID
DEPOSIT_ADMIN_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]] [[d1_databases]]
binding = "DB" binding = "DB"
database_name = "telegram-conversations" database_name = "telegram-conversations"