feat: 도메인 관리 기능 개선
- get_price: ccSLD(it.com, uk.com 등) 가격 조회 지원 - check_domains: 도메인 가용성 확인 기능 추가 - whois_lookup: 공개 RDAP API로 WHOIS 조회 (com/net/org/io/me/info/biz) - 읽기 작업(get_domain_info, get_nameservers)은 누구나 조회 가능 - 한국어 질문 시 원화(KRW)만 표시하도록 개선 - README.md, CLAUDE.md 도메인 관리 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -168,8 +168,9 @@ async function callNamecheapApi(funcName: string, funcArgs: Record<string, 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)) {
|
||||
// 도메인 권한 체크 (쓰기 작업만)
|
||||
// 읽기 작업(get_domain_info, get_nameservers)은 누구나 조회 가능
|
||||
if (['set_nameservers', 'create_child_ns', 'delete_child_ns'].includes(funcName)) {
|
||||
if (!allowedDomains.includes(funcArgs.domain)) {
|
||||
return { error: `권한 없음: ${funcArgs.domain}은 관리할 수 없는 도메인입니다.` };
|
||||
}
|
||||
@@ -250,6 +251,95 @@ async function callNamecheapApi(funcName: string, funcArgs: Record<string, any>,
|
||||
return fetch(`${apiUrl}/account/balance`, {
|
||||
headers: { 'X-API-Key': apiKey },
|
||||
}).then(r => r.json());
|
||||
case 'get_price': {
|
||||
const tld = funcArgs.tld?.replace(/^\./, ''); // .com → com
|
||||
return fetch(`${apiUrl}/prices/${tld}`, {
|
||||
headers: { 'X-API-Key': apiKey },
|
||||
}).then(r => r.json());
|
||||
}
|
||||
case 'check_domains': {
|
||||
return fetch(`${apiUrl}/domains/check`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ domains: funcArgs.domains }),
|
||||
}).then(r => r.json());
|
||||
}
|
||||
case 'whois_lookup': {
|
||||
// 공개 RDAP API 사용 (누구나 조회 가능)
|
||||
const domain = funcArgs.domain;
|
||||
try {
|
||||
// TLD별 RDAP 서버 (먼저 직접 시도)
|
||||
const tld = domain.split('.').pop()?.toLowerCase();
|
||||
const rdapServers: Record<string, string> = {
|
||||
'com': 'https://rdap.verisign.com/com/v1',
|
||||
'net': 'https://rdap.verisign.com/net/v1',
|
||||
'org': 'https://rdap.publicinterestregistry.org/rdap',
|
||||
'io': 'https://rdap.nic.io',
|
||||
'me': 'https://rdap.nic.me',
|
||||
'info': 'https://rdap.afilias.net/rdap/info',
|
||||
'biz': 'https://rdap.nic.biz',
|
||||
};
|
||||
|
||||
let rdapRes: Response | null = null;
|
||||
|
||||
// 1. TLD별 직접 서버 시도
|
||||
if (tld && rdapServers[tld]) {
|
||||
try {
|
||||
rdapRes = await fetch(`${rdapServers[tld]}/domain/${domain}`);
|
||||
} catch {
|
||||
rdapRes = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 실패하면 rdap.org 프록시 시도 (리다이렉트 따라가기)
|
||||
if (!rdapRes || !rdapRes.ok) {
|
||||
const proxyRes = await fetch(`https://rdap.org/domain/${domain}`, { redirect: 'manual' });
|
||||
if (proxyRes.status === 302 || proxyRes.status === 301) {
|
||||
const location = proxyRes.headers.get('location');
|
||||
if (location) {
|
||||
rdapRes = await fetch(location);
|
||||
}
|
||||
} else if (proxyRes.ok) {
|
||||
rdapRes = proxyRes;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rdapRes || !rdapRes.ok) {
|
||||
return {
|
||||
error: `WHOIS 조회 실패: .${tld} TLD는 RDAP를 지원하지 않습니다.`,
|
||||
suggestion: '지원 TLD: com, net, org, io, me, info, biz'
|
||||
};
|
||||
}
|
||||
const rdap = await rdapRes.json() as any;
|
||||
|
||||
// RDAP 응답 파싱
|
||||
const result: Record<string, any> = {
|
||||
domain: rdap.ldhName || domain,
|
||||
status: rdap.status || [],
|
||||
};
|
||||
|
||||
// 등록일/만료일
|
||||
for (const event of rdap.events || []) {
|
||||
if (event.eventAction === 'registration') result.created = event.eventDate;
|
||||
if (event.eventAction === 'expiration') result.expires = event.eventDate;
|
||||
if (event.eventAction === 'last changed') result.updated = event.eventDate;
|
||||
}
|
||||
|
||||
// 네임서버
|
||||
result.nameservers = (rdap.nameservers || []).map((ns: any) => ns.ldhName).filter(Boolean);
|
||||
|
||||
// 등록기관
|
||||
for (const entity of rdap.entities || []) {
|
||||
if (entity.roles?.includes('registrar')) {
|
||||
result.registrar = entity.vcardArray?.[1]?.find((v: any) => v[0] === 'fn')?.[3] || entity.handle;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { error: `WHOIS 조회 오류: ${String(error)}` };
|
||||
}
|
||||
}
|
||||
default:
|
||||
return { error: `Unknown function: ${funcName}` };
|
||||
}
|
||||
@@ -276,8 +366,16 @@ async function callDomainAgent(
|
||||
if (!threadRes.ok) return `Thread 생성 실패 (${threadRes.status})`;
|
||||
const thread = await threadRes.json() as { id: string };
|
||||
|
||||
// 2. 메시지 추가 (허용 도메인 명시)
|
||||
// 2. 메시지 추가 (허용 도메인 명시 + 응답 스타일 지시)
|
||||
const domainList = allowedDomains.join(', ');
|
||||
const instructions = `[시스템 지시]
|
||||
- 관리 가능 도메인: ${domainList}
|
||||
- 한국어로 질문하면 한국어로 답변하고, 가격은 원화(KRW)만 표시
|
||||
- 영어로 질문하면 영어로 답변하고, 가격은 달러(USD)로 표시
|
||||
- 가격 응답 시 불필요한 달러 환산 정보 생략
|
||||
|
||||
[사용자 질문]
|
||||
${query}`;
|
||||
await fetch(`https://api.openai.com/v1/threads/${thread.id}/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -287,7 +385,7 @@ async function callDomainAgent(
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: 'user',
|
||||
content: `[관리 가능 도메인: ${domainList}]\n\n${query}`
|
||||
content: instructions
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user