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:
kappa
2026-01-16 13:10:34 +09:00
parent e98bfd3a68
commit 42c57747a7
3 changed files with 168 additions and 10 deletions

View File

@@ -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
}),
});