|
|
|
|
@@ -1,4 +1,4 @@
|
|
|
|
|
import { Env } from './types';
|
|
|
|
|
import type { Env } from './types';
|
|
|
|
|
|
|
|
|
|
interface OpenAIMessage {
|
|
|
|
|
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
|
|
|
@@ -114,10 +114,233 @@ const tools = [
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'function',
|
|
|
|
|
function: {
|
|
|
|
|
name: 'manage_domain',
|
|
|
|
|
description: '도메인을 관리합니다. 도메인 목록 조회, 도메인 정보 확인, 네임서버 조회/변경, 도메인 가용성 확인, 계정 잔액 조회, TLD 가격 조회 등을 수행할 수 있습니다.',
|
|
|
|
|
parameters: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
query: {
|
|
|
|
|
type: 'string',
|
|
|
|
|
description: '도메인 관리 요청 (예: 도메인 목록 보여줘, anvil.it.com 네임서버 확인, example.com 등록 가능한지 확인)',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
required: ['query'],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Namecheap API 호출 (allowedDomains로 필터링)
|
|
|
|
|
async function callNamecheapApi(funcName: string, funcArgs: Record<string, any>, allowedDomains: string[]): Promise<any> {
|
|
|
|
|
const apiKey = '05426957210b42e752950f565ea82a3f48df9cccfdce9d82cd9817011968076e';
|
|
|
|
|
const apiUrl = 'https://namecheap-api.anvil.it.com';
|
|
|
|
|
|
|
|
|
|
// 도메인 권한 체크
|
|
|
|
|
if (['get_domain_info', 'get_nameservers', 'set_nameservers', 'create_child_ns', 'get_child_ns', 'delete_child_ns'].includes(funcName)) {
|
|
|
|
|
if (!allowedDomains.includes(funcArgs.domain)) {
|
|
|
|
|
return { error: `권한 없음: ${funcArgs.domain}은 관리할 수 없는 도메인입니다.` };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (funcName) {
|
|
|
|
|
case 'list_domains': {
|
|
|
|
|
const result = await fetch(`${apiUrl}/domains?page=${funcArgs.page || 1}&page_size=${funcArgs.page_size || 100}`, {
|
|
|
|
|
headers: { 'X-API-Key': apiKey },
|
|
|
|
|
}).then(r => r.json()) as any[];
|
|
|
|
|
// 허용된 도메인만 필터링
|
|
|
|
|
return result.filter((d: any) => allowedDomains.includes(d.name));
|
|
|
|
|
}
|
|
|
|
|
case 'get_domain_info':
|
|
|
|
|
return fetch(`${apiUrl}/domains/${funcArgs.domain}`, {
|
|
|
|
|
headers: { 'X-API-Key': apiKey },
|
|
|
|
|
}).then(r => r.json());
|
|
|
|
|
case 'get_nameservers':
|
|
|
|
|
return fetch(`${apiUrl}/dns/${funcArgs.domain}/nameservers`, {
|
|
|
|
|
headers: { 'X-API-Key': apiKey },
|
|
|
|
|
}).then(r => r.json());
|
|
|
|
|
case 'set_nameservers': {
|
|
|
|
|
const res = await fetch(`${apiUrl}/dns/${funcArgs.domain}/nameservers`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ domain: funcArgs.domain, nameservers: funcArgs.nameservers }),
|
|
|
|
|
});
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
// Namecheap 에러 메시지 파싱
|
|
|
|
|
if (text.includes('subordinate hosts') || text.includes('Non existen')) {
|
|
|
|
|
return {
|
|
|
|
|
error: `네임서버 변경 실패: ${funcArgs.nameservers.join(', ')}는 등록되지 않은 네임서버입니다. 자기 도메인을 네임서버로 사용하려면 먼저 Namecheap에서 Child Nameserver(글루 레코드)를 IP 주소와 함께 등록해야 합니다.`
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { error: `네임서버 변경 실패: ${text}` };
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(text);
|
|
|
|
|
} catch {
|
|
|
|
|
return { success: true, message: text };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case 'create_child_ns': {
|
|
|
|
|
const res = await fetch(`${apiUrl}/dns/${funcArgs.domain}/childns`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ nameserver: funcArgs.nameserver, ip: funcArgs.ip }),
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json() as any;
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
return { error: data.detail || `Child NS 생성 실패` };
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
case 'get_child_ns': {
|
|
|
|
|
const res = await fetch(`${apiUrl}/dns/${funcArgs.domain}/childns/${funcArgs.nameserver}`, {
|
|
|
|
|
headers: { 'X-API-Key': apiKey },
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json() as any;
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
return { error: data.detail || `Child NS 조회 실패` };
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
case 'delete_child_ns': {
|
|
|
|
|
const res = await fetch(`${apiUrl}/dns/${funcArgs.domain}/childns/${funcArgs.nameserver}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: { 'X-API-Key': apiKey },
|
|
|
|
|
});
|
|
|
|
|
const data = await res.json() as any;
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
return { error: data.detail || `Child NS 삭제 실패` };
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
case 'get_balance':
|
|
|
|
|
return fetch(`${apiUrl}/account/balance`, {
|
|
|
|
|
headers: { 'X-API-Key': apiKey },
|
|
|
|
|
}).then(r => r.json());
|
|
|
|
|
default:
|
|
|
|
|
return { error: `Unknown function: ${funcName}` };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 도메인 에이전트 (Assistants API - 기존 Agent 활용)
|
|
|
|
|
async function callDomainAgent(
|
|
|
|
|
apiKey: string,
|
|
|
|
|
assistantId: string,
|
|
|
|
|
query: string,
|
|
|
|
|
allowedDomains: string[] = []
|
|
|
|
|
): 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 domainList = allowedDomains.join(', ');
|
|
|
|
|
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: `[관리 가능 도메인: ${domainList}]\n\n${query}`
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
const result = await callNamecheapApi(funcName, funcArgs, allowedDomains);
|
|
|
|
|
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) {
|
|
|
|
|
return `도메인 에이전트 오류: ${String(error)}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 도구 실행
|
|
|
|
|
async function executeTool(name: string, args: Record<string, string>): Promise<string> {
|
|
|
|
|
async function executeTool(name: string, args: Record<string, string>, env?: Env, telegramUserId?: string, db?: D1Database): Promise<string> {
|
|
|
|
|
switch (name) {
|
|
|
|
|
case 'get_weather': {
|
|
|
|
|
const city = args.city || 'Seoul';
|
|
|
|
|
@@ -215,6 +438,62 @@ async function executeTool(name: string, args: Record<string, string>): Promise<
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'manage_domain': {
|
|
|
|
|
const query = args.query;
|
|
|
|
|
console.log('[manage_domain] 시작:', { query, telegramUserId, hasDb: !!db });
|
|
|
|
|
|
|
|
|
|
// 소유권 검증 (DB 조회)
|
|
|
|
|
if (!telegramUserId || !db) {
|
|
|
|
|
console.log('[manage_domain] 실패: telegramUserId 또는 db 없음');
|
|
|
|
|
return '🚫 도메인 관리 권한이 없습니다.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let userDomains: string[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const user = await db.prepare(
|
|
|
|
|
'SELECT id FROM users WHERE telegram_id = ?'
|
|
|
|
|
).bind(telegramUserId).first<{ id: number }>();
|
|
|
|
|
console.log('[manage_domain] user 조회 결과:', user);
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
return '🚫 도메인 관리 권한이 없습니다.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 사용자 소유 도메인 전체 목록 조회
|
|
|
|
|
const domains = await db.prepare(
|
|
|
|
|
'SELECT domain FROM user_domains WHERE user_id = ? AND verified = 1'
|
|
|
|
|
).bind(user.id).all<{ domain: string }>();
|
|
|
|
|
userDomains = domains.results?.map(d => d.domain) || [];
|
|
|
|
|
console.log('[manage_domain] 소유 도메인:', userDomains);
|
|
|
|
|
|
|
|
|
|
if (userDomains.length === 0) {
|
|
|
|
|
return '🚫 등록된 도메인이 없습니다. 먼저 도메인을 등록해주세요.';
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log('[manage_domain] DB 오류:', error);
|
|
|
|
|
return '🚫 권한 확인 중 오류가 발생했습니다.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!env?.OPENAI_API_KEY || !env?.DOMAIN_AGENT_ID) {
|
|
|
|
|
console.log('[manage_domain] env 설정 없음');
|
|
|
|
|
return '🌐 도메인 관리 기능이 설정되지 않았습니다.';
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
console.log('[manage_domain] callDomainAgent 호출 시작');
|
|
|
|
|
const result = await callDomainAgent(env.OPENAI_API_KEY, env.DOMAIN_AGENT_ID, query, userDomains);
|
|
|
|
|
console.log('[manage_domain] callDomainAgent 완료:', result?.slice(0, 100));
|
|
|
|
|
// Markdown → HTML 변환
|
|
|
|
|
const htmlResult = result
|
|
|
|
|
.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
|
|
|
|
|
.replace(/\*(.+?)\*/g, '<i>$1</i>')
|
|
|
|
|
.replace(/`(.+?)`/g, '<code>$1</code>');
|
|
|
|
|
return `🌐 ${htmlResult}`;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log('[manage_domain] callDomainAgent 오류:', error);
|
|
|
|
|
return `🌐 도메인 관리 오류: ${String(error)}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return `알 수 없는 도구: ${name}`;
|
|
|
|
|
}
|
|
|
|
|
@@ -254,7 +533,9 @@ export async function generateOpenAIResponse(
|
|
|
|
|
env: Env,
|
|
|
|
|
userMessage: string,
|
|
|
|
|
systemPrompt: string,
|
|
|
|
|
recentContext: { role: 'user' | 'assistant'; content: string }[]
|
|
|
|
|
recentContext: { role: 'user' | 'assistant'; content: string }[],
|
|
|
|
|
telegramUserId?: string,
|
|
|
|
|
db?: D1Database
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
if (!env.OPENAI_API_KEY) {
|
|
|
|
|
throw new Error('OPENAI_API_KEY not configured');
|
|
|
|
|
@@ -282,7 +563,7 @@ export async function generateOpenAIResponse(
|
|
|
|
|
const toolResults: OpenAIMessage[] = [];
|
|
|
|
|
for (const toolCall of assistantMessage.tool_calls) {
|
|
|
|
|
const args = JSON.parse(toolCall.function.arguments);
|
|
|
|
|
const result = await executeTool(toolCall.function.name, args);
|
|
|
|
|
const result = await executeTool(toolCall.function.name, args, env, telegramUserId, db);
|
|
|
|
|
toolResults.push({
|
|
|
|
|
role: 'tool',
|
|
|
|
|
tool_call_id: toolCall.id,
|
|
|
|
|
|