Add network diagnostic tool for domain connectivity troubleshooting

DNS lookup (DoH via Cloudflare/Google), Korean ISP block detection
(KT/LG/SK via TCP DNS), HTTP/HTTPS check, and TCP port test — all
run in parallel with per-check timeouts. Integrated as diagnose_domain
tool in troubleshoot-agent with updated patterns for network keywords.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-12 02:33:29 +09:00
parent 0e338aa0fa
commit d261d01981
4 changed files with 712 additions and 34 deletions

View File

@@ -11,6 +11,8 @@
import type { ToolDefinition, TroubleshootSession } from '../types'; import type { ToolDefinition, TroubleshootSession } from '../types';
import type { AgentToolContext } from './base-agent'; import type { AgentToolContext } from './base-agent';
import { BaseAgent } from './base-agent'; import { BaseAgent } from './base-agent';
import { DiagramAgent } from './diagram-agent';
import { runNetworkDiagnostic } from '../services/network-diagnostic';
import { SessionManager } from '../utils/session-manager'; import { SessionManager } from '../utils/session-manager';
import { getSessionConfig } from '../constants/agent-config'; import { getSessionConfig } from '../constants/agent-config';
import { createLogger } from '../utils/logger'; import { createLogger } from '../utils/logger';
@@ -51,19 +53,20 @@ export class TroubleshootAgent extends BaseAgent<TroubleshootSession> {
protected getSystemPrompt(session: TroubleshootSession): string { protected getSystemPrompt(session: TroubleshootSession): string {
return `당신은 호스팅/인프라 서비스의 기술 문제 해결 전문가입니다. return `당신은 호스팅/인프라 서비스의 기술 문제 해결 전문가입니다.
고객의 문제를 체계적으로 진단하고 해결책을 제시합니다. 고객의 문제를 신속하게 진단하고 해결책을 제시합니다.
## 핵심 원칙: 도구 먼저, 질문은 나중에
고객이 문제를 말하면 **먼저 도구를 호출하여 상태를 확인**하세요. 질문부터 하지 마세요.
- "서버 접속이 안 돼요" → 바로 check_server_status 호출
- "도메인이 안 열려요" → 바로 check_domain_status 호출
- "VPN 연결이 안 돼요" → 바로 check_service_status 호출
도구 결과를 확인한 후, 추가 정보가 필요하면 그때 질문하세요.
## 문제 해결 프로세스 ## 문제 해결 프로세스
1. **상태 파악**: 현재 상태 확인 (서버 상태, 도메인 상태, 서비스 상태) 1. **즉시 상태 조회**: 고객 메시지에서 키워드를 파악하여 관련 도구 즉시 호출
2. **원인 분석**: 수집된 정보를 기반으로 근본 원인 분석 2. **결과 기반 진단**: 조회 결과를 분석하여 원인 파악
3. **해결책 제시**: 단계별 해결 방법 안내 3. **해결책 제시**: 단계별 해결 방법 안내
4. **예측**: 재발 방지 및 모니터링 권고 4. **추가 확인**: 필요 시 추가 질문 또는 에스컬레이션
## 정보 수집 항목
- **카테고리**: 서버, 도메인, DDoS, VPN, 네트워크, 기타
- **증상**: 구체적 증상 (접속 불가, 느림, 오류 메시지 등)
- **환경**: OS, 브라우저, 리전, 사용 중인 서비스
- **에러 메시지**: 정확한 에러 메시지 또는 코드
## 현재 세션 상태: ${session.status} ## 현재 세션 상태: ${session.status}
## 에스컬레이션 카운트: ${session.escalation_count || 0} ## 에스컬레이션 카운트: ${session.escalation_count || 0}
@@ -71,15 +74,23 @@ export class TroubleshootAgent extends BaseAgent<TroubleshootSession> {
## 대화 원칙 ## 대화 원칙
- 항상 한국어로 응답하세요. - 항상 한국어로 응답하세요.
- 전문적이지만 이해하기 쉽게 설명하세요. - 전문적이지만 이해하기 쉽게 설명하세요.
- 진단 결과를 단정 짓지 말고 추정형으로 이야기하세요.
좋은 예: "DB 서버가 중지 상태로 보이는데, 이 부분이 원인일 가능성이 있습니다"
나쁜 예: "DB 서버가 중지 상태입니다. 이것이 원인입니다"
조회 결과는 시스템 기록이므로 실제 상황과 다를 수 있음을 인지하세요.
- 문제와 무관한 메시지가 오면 __PASSTHROUGH__를 응답하세요. - 문제와 무관한 메시지가 오면 __PASSTHROUGH__를 응답하세요.
- 상담이 완료되면 __SESSION_END__를 응답 끝에 추가하세요. - 상담이 완료되면 __SESSION_END__를 응답 끝에 추가하세요.
- 3라운드 이내에 해결이 어려운 경우 __ESCALATE__를 응답에 포함하세요. - 3라운드 이내에 해결이 어려운 경우 __ESCALATE__를 응답에 포함하세요.
이 경우 고객에게 "전문 엔지니어에게 전달하겠습니다"라고 안내하세요. 이 경우 고객에게 "전문 엔지니어에게 전달하겠습니다"라고 안내하세요.
## 도구 사용 ## 도구 사용
- check_server_status: 서버 상태 확인 - check_server_status: 서버 상태 확인 (server_id 없으면 전체 목록)
- check_domain_status: 도메인 상태 확인 - check_domain_status: 도메인 상태 확인 (domain 없으면 전체 목록)
- check_service_status: DDoS/VPN 서비스 상태 확인`; - check_service_status: DDoS/VPN 서비스 상태 확인
- diagnose_domain: 도메인 네트워크 진단 (DNS 조회, ISP 차단 감지, HTTP/TCP 연결 테스트)
- "접속이 안 돼요", "사이트가 안 열려요", "차단된 것 같아요" 등의 문의 시 즉시 호출
- DNS, HTTP, ISP 차단 여부를 한번에 확인하여 원인을 빠르게 파악
- generate_diagram: 문제 상황을 Mermaid 다이어그램으로 시각화 (충분한 정보 수집 후)`;
} }
protected getTools(): ToolDefinition[] { protected getTools(): ToolDefinition[] {
@@ -137,6 +148,50 @@ export class TroubleshootAgent extends BaseAgent<TroubleshootSession> {
}, },
}, },
}, },
{
type: 'function',
function: {
name: 'diagnose_domain',
description: '도메인의 네트워크 상태를 종합 진단합니다. DNS 조회, 한국 ISP(KT/LG/SK) 차단 감지, HTTP/HTTPS 연결, TCP 포트 연결을 테스트합니다.',
parameters: {
type: 'object',
properties: {
domain: {
type: 'string',
description: '진단할 도메인 (예: example.com)',
},
ports: {
type: 'array',
items: { type: 'number' },
description: '추가로 테스트할 TCP 포트 목록 (기본: 80, 443)',
},
},
required: ['domain'],
},
},
},
{
type: 'function',
function: {
name: 'generate_diagram',
description: '문제 상황을 Mermaid 다이어그램으로 시각화하여 고객에게 전송합니다. 충분한 정보를 수집한 후에 호출하세요.',
parameters: {
type: 'object',
properties: {
description: {
type: 'string',
description: '다이어그램에 표현할 문제 상황 설명',
},
diagram_type: {
type: 'string',
enum: ['flow', 'sequence', 'architecture'],
description: '다이어그램 유형 (선택, 기본값: flow)',
},
},
required: ['description'],
},
},
},
]; ];
} }
@@ -160,6 +215,17 @@ export class TroubleshootAgent extends BaseAgent<TroubleshootSession> {
args.service_id as number | undefined, args.service_id as number | undefined,
db db
); );
case 'diagnose_domain':
return this.handleDiagnoseDomain(
args.domain as string,
args.ports as number[] | undefined
);
case 'generate_diagram':
return this.handleGenerateDiagram(
args.description as string,
args.diagram_type as string | undefined,
context
);
default: default:
return JSON.stringify({ error: `알 수 없는 도구: ${name}` }); return JSON.stringify({ error: `알 수 없는 도구: ${name}` });
} }
@@ -292,4 +358,47 @@ export class TroubleshootAgent extends BaseAgent<TroubleshootSession> {
return JSON.stringify({ error: '서비스 상태 조회에 실패했습니다.' }); return JSON.stringify({ error: '서비스 상태 조회에 실패했습니다.' });
} }
} }
private async handleDiagnoseDomain(
domain: string,
ports?: number[]
): Promise<string> {
// Validate domain format
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,251}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) {
return JSON.stringify({ error: '올바른 도메인 형식이 아닙니다. 예: example.com' });
}
try {
const report = await runNetworkDiagnostic(domain, { ports });
return JSON.stringify(report);
} catch (error) {
logger.error('네트워크 진단 오류', error as Error, { domain });
return JSON.stringify({ error: '네트워크 진단 중 오류가 발생했습니다.' });
}
}
private async handleGenerateDiagram(
description: string,
diagramType: string | undefined,
context: AgentToolContext
): Promise<string> {
if (!context.chatId) {
return JSON.stringify({ error: '채팅 정보가 없어 다이어그램을 전송할 수 없습니다.' });
}
const diagramAgent = new DiagramAgent();
const result = await diagramAgent.generateAndSend(
{
env: context.env,
chatId: context.chatId,
telegramUserId: context.userId,
messageId: context.messageId,
},
description,
diagramType
);
return JSON.stringify(result);
}
} }

View File

@@ -0,0 +1,529 @@
/**
* Network Diagnostic Service
*
* 도메인 접속 문제 진단을 위한 네트워크 체크:
* - DoH DNS 조회 (Cloudflare, Google)
* - 한국 ISP DNS 차단 감지 (KT, LG, SK) — TCP DNS 쿼리
* - HTTP/HTTPS 연결 테스트
* - TCP 포트 연결 테스트
*/
import type { NetworkDiagnosticReport, DnsResolverResult, IspDnsBlockResult, HttpCheckResult, TcpCheckResult } from '../types';
import { createLogger } from '../utils/logger';
const logger = createLogger('network-diagnostic');
const CHECK_TIMEOUT_MS = 5000;
// warning.or.kr 및 알려진 ISP 차단 페이지 IP
const KNOWN_BLOCK_IPS = new Set([
'121.189.57.82', // warning.or.kr (KT)
'121.189.57.83', // warning.or.kr
'211.234.62.24', // KCSC block page
'211.234.62.25',
'175.158.50.11', // block redirect
'175.158.50.12',
'210.91.57.228', // LGU+ block
'61.97.65.4', // SKT block
'121.167.36.1', // common block redirect
]);
// ============================================================================
// DoH DNS Resolution (Cloudflare & Google)
// ============================================================================
interface DohAnswer {
name: string;
type: number;
TTL: number;
data: string;
}
interface DohResponse {
Status: number;
Answer?: DohAnswer[];
}
const DOH_RESOLVERS = [
{ name: 'Cloudflare', url: 'https://1.1.1.1/dns-query', ip: '1.1.1.1' },
{ name: 'Google', url: 'https://8.8.8.8/resolve', ip: '8.8.8.8' },
];
async function resolveDoh(domain: string): Promise<DnsResolverResult[]> {
return Promise.all(
DOH_RESOLVERS.map(async (resolver) => {
const start = Date.now();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
const isCloudflare = resolver.name === 'Cloudflare';
const url = isCloudflare
? `${resolver.url}?name=${encodeURIComponent(domain)}&type=A`
: `${resolver.url}?name=${encodeURIComponent(domain)}&type=A`;
const headers: Record<string, string> = isCloudflare
? { Accept: 'application/dns-json' }
: {};
try {
const response = await fetch(url, {
headers,
signal: controller.signal,
});
if (!response.ok) {
return {
resolver: resolver.name,
ip: resolver.ip,
records: [],
responseTimeMs: Date.now() - start,
error: `HTTP ${response.status}`,
};
}
const data = (await response.json()) as DohResponse;
const records = (data.Answer || [])
.filter((a) => a.type === 1) // A records
.map((a) => ({
type: 'A',
value: a.data,
ttl: a.TTL,
}));
return {
resolver: resolver.name,
ip: resolver.ip,
records,
responseTimeMs: Date.now() - start,
};
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
return {
resolver: resolver.name,
ip: resolver.ip,
records: [],
responseTimeMs: Date.now() - start,
error: (error as Error).message,
};
}
})
);
}
// ============================================================================
// Korean ISP DNS Block Detection (TCP DNS Query)
// ============================================================================
const ISP_DNS_SERVERS = [
{ isp: 'KT', ip: '168.126.63.1' },
{ isp: 'LG U+', ip: '164.124.101.2' },
{ isp: 'SK Broadband', ip: '210.220.163.82' },
];
/**
* DNS 와이어 포맷 패킷 생성 (TCP용: 2바이트 길이 프리픽스 포함)
*/
function buildDnsQuery(domain: string): Uint8Array {
// DNS 헤더 (12 bytes)
const header = new Uint8Array([
0xAB, 0xCD, // Transaction ID
0x01, 0x00, // Flags: standard query, recursion desired
0x00, 0x01, // Questions: 1
0x00, 0x00, // Answer RRs: 0
0x00, 0x00, // Authority RRs: 0
0x00, 0x00, // Additional RRs: 0
]);
// Question section: encode domain labels
const labels = domain.split('.');
const questionParts: number[] = [];
for (const label of labels) {
questionParts.push(label.length);
for (let i = 0; i < label.length; i++) {
questionParts.push(label.charCodeAt(i));
}
}
questionParts.push(0); // Root label
questionParts.push(0x00, 0x01); // Type A
questionParts.push(0x00, 0x01); // Class IN
const question = new Uint8Array(questionParts);
const dnsMessage = new Uint8Array(header.length + question.length);
dnsMessage.set(header, 0);
dnsMessage.set(question, header.length);
// TCP DNS: 2-byte length prefix
const tcpMessage = new Uint8Array(2 + dnsMessage.length);
tcpMessage[0] = (dnsMessage.length >> 8) & 0xFF;
tcpMessage[1] = dnsMessage.length & 0xFF;
tcpMessage.set(dnsMessage, 2);
return tcpMessage;
}
/**
* TCP DNS 응답에서 A 레코드 IP 파싱
*/
function parseDnsResponse(raw: Uint8Array): string[] {
const ips: string[] = [];
// Skip 2-byte TCP length prefix
const offset = 2;
if (raw.length < offset + 12) return ips;
// Read answer count from header
const answerCount = (raw[offset + 6] << 8) | raw[offset + 7];
if (answerCount === 0) return ips;
// Skip header (12 bytes) + question section
let pos = offset + 12;
// Skip question section
while (pos < raw.length && raw[pos] !== 0) {
if ((raw[pos] & 0xC0) === 0xC0) {
pos += 2;
break;
}
pos += raw[pos] + 1;
}
if (pos < raw.length && raw[pos] === 0) pos++; // null terminator
pos += 4; // skip QTYPE + QCLASS
// Parse answer records
for (let i = 0; i < answerCount && pos < raw.length; i++) {
// Skip name (may be pointer)
if ((raw[pos] & 0xC0) === 0xC0) {
pos += 2;
} else {
while (pos < raw.length && raw[pos] !== 0) {
pos += raw[pos] + 1;
}
pos++; // null terminator
}
if (pos + 10 > raw.length) break;
const rtype = (raw[pos] << 8) | raw[pos + 1];
const rdlength = (raw[pos + 8] << 8) | raw[pos + 9];
pos += 10;
if (rtype === 1 && rdlength === 4 && pos + 4 <= raw.length) {
ips.push(`${raw[pos]}.${raw[pos + 1]}.${raw[pos + 2]}.${raw[pos + 3]}`);
}
pos += rdlength;
}
return ips;
}
/**
* TCP 소켓으로 DNS 서버에 질의
*/
async function queryDnsTcp(serverIp: string, query: Uint8Array): Promise<string[]> {
const socket = (globalThis as unknown as {
connect: (opts: { hostname: string; port: number }) => {
opened: Promise<{ readable: ReadableStream; writable: WritableStream }>;
close: () => void;
};
}).connect({ hostname: serverIp, port: 53 });
const { readable, writable } = await socket.opened;
const writer = writable.getWriter();
await writer.write(query);
writer.releaseLock();
const reader = readable.getReader();
const chunks: Uint8Array[] = [];
let totalLength = 0;
try {
while (totalLength < 512) { // DNS response shouldn't exceed 512 bytes for simple queries
const { value, done } = await reader.read();
if (done) break;
chunks.push(value as Uint8Array);
totalLength += (value as Uint8Array).length;
// Check if we have enough data (2-byte length + message)
if (totalLength >= 2) {
const firstChunk = chunks[0];
const expectedLen = (firstChunk[0] << 8) | firstChunk[1];
if (totalLength >= expectedLen + 2) break;
}
}
} finally {
reader.releaseLock();
socket.close();
}
// Combine chunks
const raw = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
raw.set(chunk, offset);
offset += chunk.length;
}
return parseDnsResponse(raw);
}
async function checkIspBlocking(domain: string): Promise<IspDnsBlockResult[]> {
const query = buildDnsQuery(domain);
return Promise.all(
ISP_DNS_SERVERS.map(async (server) => {
const start = Date.now();
try {
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('timeout')), CHECK_TIMEOUT_MS)
);
const resolvedIps = await Promise.race([
queryDnsTcp(server.ip, query),
timeoutPromise,
]);
const blocked = resolvedIps.some((ip) => KNOWN_BLOCK_IPS.has(ip));
return {
isp: server.isp,
serverIp: server.ip,
resolvedIps,
blocked,
blockReason: blocked ? 'warning.or.kr 차단 페이지 IP 감지' : undefined,
responseTimeMs: Date.now() - start,
};
} catch (error) {
return {
isp: server.isp,
serverIp: server.ip,
resolvedIps: [],
blocked: false,
responseTimeMs: Date.now() - start,
error: (error as Error).message,
};
}
})
);
}
// ============================================================================
// HTTP/HTTPS Connection Test
// ============================================================================
async function checkHttp(domain: string): Promise<HttpCheckResult[]> {
const urls = [`https://${domain}`, `http://${domain}`];
return Promise.all(
urls.map(async (url) => {
const start = Date.now();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
try {
const response = await fetch(url, {
method: 'HEAD',
redirect: 'manual',
signal: controller.signal,
});
const redirectUrl = response.headers.get('location') || undefined;
return {
url,
statusCode: response.status,
responseTimeMs: Date.now() - start,
redirectUrl,
};
} finally {
clearTimeout(timeoutId);
}
} catch (error) {
return {
url,
responseTimeMs: Date.now() - start,
error: (error as Error).message,
};
}
})
);
}
// ============================================================================
// TCP Port Test (connect API — ping substitute)
// ============================================================================
const DEFAULT_PORTS = [80, 443];
async function checkTcpPorts(domain: string, ports?: number[]): Promise<TcpCheckResult[]> {
// Resolve domain first to get IP for connect()
const dohResult = await resolveDoh(domain);
const ip = dohResult[0]?.records[0]?.value;
if (!ip) {
return (ports || DEFAULT_PORTS).map((port) => ({
host: domain,
port,
connected: false,
responseTimeMs: 0,
error: 'DNS 조회 실패로 TCP 테스트를 수행할 수 없습니다',
}));
}
return Promise.all(
(ports || DEFAULT_PORTS).map(async (port) => {
const start = Date.now();
try {
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('timeout')), CHECK_TIMEOUT_MS)
);
const socket = (globalThis as unknown as {
connect: (opts: { hostname: string; port: number }) => {
opened: Promise<unknown>;
close: () => void;
};
}).connect({ hostname: ip, port });
await Promise.race([socket.opened, timeoutPromise]);
socket.close();
return {
host: domain,
port,
connected: true,
responseTimeMs: Date.now() - start,
};
} catch (error) {
return {
host: domain,
port,
connected: false,
responseTimeMs: Date.now() - start,
error: (error as Error).message,
};
}
})
);
}
// ============================================================================
// Summary Generation
// ============================================================================
function generateSummary(
dns: DnsResolverResult[],
ispBlocking: IspDnsBlockResult[],
http: HttpCheckResult[],
tcp: TcpCheckResult[]
): string[] {
const summary: string[] = [];
// DNS resolution check
const dnsOk = dns.some((r) => r.records.length > 0);
const dnsIps = dns.flatMap((r) => r.records.map((rec) => rec.value));
if (!dnsOk) {
summary.push('DNS 조회 실패: 도메인이 어떤 DNS 서버에서도 조회되지 않습니다. 도메인 등록 또는 DNS 설정을 확인하세요.');
} else {
summary.push(`DNS 정상: ${[...new Set(dnsIps)].join(', ')}로 확인됨`);
}
// ISP blocking check
const blockedIsps = ispBlocking.filter((r) => r.blocked);
if (blockedIsps.length > 0) {
const names = blockedIsps.map((r) => r.isp).join(', ');
summary.push(`ISP 차단 감지: ${names}에서 차단 페이지(warning.or.kr)로 리다이렉트됩니다. 방통위/KCSC 차단일 가능성이 높습니다.`);
} else {
const testedIsps = ispBlocking.filter((r) => !r.error);
if (testedIsps.length > 0) {
summary.push(`ISP 차단 없음: 테스트한 통신사(${testedIsps.map((r) => r.isp).join(', ')})에서 차단이 감지되지 않았습니다.`);
}
const failedIsps = ispBlocking.filter((r) => r.error);
if (failedIsps.length > 0) {
summary.push(`ISP DNS 테스트 실패: ${failedIsps.map((r) => `${r.isp}(${r.error})`).join(', ')}`);
}
}
// HTTP check
const httpsResult = http.find((r) => r.url.startsWith('https://'));
const httpResult = http.find((r) => r.url.startsWith('http://'));
if (httpsResult?.statusCode) {
if (httpsResult.statusCode >= 200 && httpsResult.statusCode < 400) {
summary.push(`HTTPS 정상: ${httpsResult.statusCode} (${httpsResult.responseTimeMs}ms)`);
} else if (httpsResult.statusCode >= 500) {
summary.push(`HTTPS 서버 오류: ${httpsResult.statusCode} (서버 내부 문제)`);
} else {
summary.push(`HTTPS 응답: ${httpsResult.statusCode} (${httpsResult.responseTimeMs}ms)`);
}
} else if (httpsResult?.error) {
summary.push(`HTTPS 연결 실패: ${httpsResult.error}`);
}
if (httpResult?.statusCode) {
if (httpResult.statusCode >= 300 && httpResult.statusCode < 400 && httpResult.redirectUrl?.startsWith('https')) {
summary.push('HTTP→HTTPS 리다이렉트 설정됨');
}
}
// TCP check
const tcpFailed = tcp.filter((r) => !r.connected);
if (tcpFailed.length > 0) {
summary.push(`TCP 연결 실패 포트: ${tcpFailed.map((r) => `${r.port}(${r.error || '연결 불가'})`).join(', ')}`);
}
const tcpOk = tcp.filter((r) => r.connected);
if (tcpOk.length > 0) {
summary.push(`TCP 연결 성공: ${tcpOk.map((r) => `포트 ${r.port} (${r.responseTimeMs}ms)`).join(', ')}`);
}
return summary;
}
// ============================================================================
// Main Entry Point
// ============================================================================
export interface NetworkDiagnosticOptions {
ports?: number[];
skipIspCheck?: boolean;
skipHttpCheck?: boolean;
skipTcpCheck?: boolean;
}
export async function runNetworkDiagnostic(
domain: string,
options?: NetworkDiagnosticOptions
): Promise<NetworkDiagnosticReport> {
logger.info('네트워크 진단 시작', { domain });
const startTime = Date.now();
// Run all checks in parallel
const [dns, ispBlocking, http, tcp] = await Promise.all([
resolveDoh(domain),
options?.skipIspCheck ? Promise.resolve([]) : checkIspBlocking(domain),
options?.skipHttpCheck ? Promise.resolve([]) : checkHttp(domain),
options?.skipTcpCheck ? Promise.resolve([]) : checkTcpPorts(domain, options?.ports),
]);
const summary = generateSummary(dns, ispBlocking, http, tcp);
logger.info('네트워크 진단 완료', {
domain,
durationMs: Date.now() - startTime,
summaryCount: summary.length,
});
return {
domain,
timestamp: new Date().toISOString(),
dns,
ispBlocking,
http,
tcp,
summary,
};
}

View File

@@ -16,11 +16,14 @@ export interface Env {
DEPOSIT_BANK_HOLDER?: string; DEPOSIT_BANK_HOLDER?: string;
// API URLs // API URLs
OPENAI_API_BASE?: string; OPENAI_API_BASE?: string;
D2_RENDER_URL?: string;
NAMECHEAP_API_URL?: string; NAMECHEAP_API_URL?: string;
WHOIS_API_URL?: string; WHOIS_API_URL?: string;
CLOUD_ORCHESTRATOR_URL?: string; CLOUD_ORCHESTRATOR_URL?: string;
CLOUD_ORCHESTRATOR?: Fetcher; CLOUD_ORCHESTRATOR?: Fetcher;
// Kroki
KROKI_URL?: string;
// Queues
WORK_QUEUE?: Queue;
// KV Namespaces // KV Namespaces
RATE_LIMIT_KV: KVNamespace; RATE_LIMIT_KV: KVNamespace;
SESSION_KV: KVNamespace; SESSION_KV: KVNamespace;
@@ -401,10 +404,6 @@ export interface CheckServiceArgs {
service_id?: number; service_id?: number;
} }
export interface RenderD2Args {
source: string;
format?: 'svg' | 'png';
}
export interface AdminArgs { export interface AdminArgs {
action: 'block_user' | 'unblock_user' | 'set_role' | 'broadcast' | 'confirm_deposit' | 'reject_deposit' | 'list_pending'; action: 'block_user' | 'unblock_user' | 'set_role' | 'broadcast' | 'confirm_deposit' | 'reject_deposit' | 'list_pending';
@@ -421,6 +420,60 @@ export interface ApproveActionArgs {
reason?: string; reason?: string;
} }
// ============================================
// Network Diagnostic Types
// ============================================
export interface DnsRecord {
type: string;
value: string;
ttl?: number;
}
export interface DnsResolverResult {
resolver: string;
ip: string;
records: DnsRecord[];
responseTimeMs: number;
error?: string;
}
export interface IspDnsBlockResult {
isp: string;
serverIp: string;
resolvedIps: string[];
blocked: boolean;
blockReason?: string;
responseTimeMs: number;
error?: string;
}
export interface HttpCheckResult {
url: string;
statusCode?: number;
responseTimeMs: number;
redirectUrl?: string;
error?: string;
}
export interface TcpCheckResult {
host: string;
port: number;
connected: boolean;
responseTimeMs: number;
error?: string;
}
export interface NetworkDiagnosticReport {
domain: string;
timestamp: string;
dns: DnsResolverResult[];
ispBlocking: IspDnsBlockResult[];
http: HttpCheckResult[];
tcp: TcpCheckResult[];
summary: string[];
}
// ============================================ // ============================================
// Inline Keyboard Data // Inline Keyboard Data
// ============================================ // ============================================
@@ -448,21 +501,6 @@ export type KeyboardCallbackData =
| ActionApprovalKeyboardData | ActionApprovalKeyboardData
| EscalationKeyboardData; | EscalationKeyboardData;
// ============================================
// D2 Rendering
// ============================================
export interface D2RenderRequest {
source: string;
format: 'svg' | 'png';
}
export interface D2RenderResponse {
success: boolean;
image?: ArrayBuffer;
error?: string;
}
// ============================================ // ============================================
// Request Context // Request Context
// ============================================ // ============================================

View File

@@ -16,7 +16,7 @@ export const BILLING_PATTERNS = /입금|충전|잔액|계좌|예치금|송금|
export const SERVER_PATTERNS = /서버|VPS|클라우드|호스팅|인스턴스|linode|vultr|\d+번\s*(?:시작|중지|정지|재시작|리셋|리부팅|삭제|해지)|#\d+\s*(?:시작|중지|정지|재시작|리셋|리부팅|삭제|해지)|reboot|server/i; export const SERVER_PATTERNS = /서버|VPS|클라우드|호스팅|인스턴스|linode|vultr|\d+번\s*(?:시작|중지|정지|재시작|리셋|리부팅|삭제|해지)|#\d+\s*(?:시작|중지|정지|재시작|리셋|리부팅|삭제|해지)|reboot|server/i;
export const TROUBLESHOOT_PATTERNS = /문제|에러|오류|안[돼되]|느려|트러블|장애|버그|실패|안\s*됨|error|problem|down|slow|timeout|crash|접속.*안|연결.*안|불안정/i; export const TROUBLESHOOT_PATTERNS = /문제|에러|오류|안[돼되]|느려|트러블|장애|버그|실패|안\s*됨|error|problem|down|slow|timeout|crash|접속.*안|연결.*안|불안정|사이트.*안.*열|안.*열려|차단|block|ISP|통신사|포트.*안|포트.*막|ping|핑/i;
export const ONBOARDING_PATTERNS = /신규|가입|서비스\s*소개|뭐하는|어떤\s*서비스|요금|플랜|가격|plan|pricing|시작하려|처음|어떻게\s*이용|회원가입|시작/i; export const ONBOARDING_PATTERNS = /신규|가입|서비스\s*소개|뭐하는|어떤\s*서비스|요금|플랜|가격|plan|pricing|시작하려|처음|어떻게\s*이용|회원가입|시작/i;
@@ -24,6 +24,8 @@ export const SECURITY_PATTERNS = /ddos|DDoS|디도스|vpn|VPN|보안|방어|공
export const ASSET_PATTERNS = /자산|현황|대시보드|내\s*서버|내\s*도메인|보유|목록|리스트|내\s*서비스|내\s*계정|asset|my\s*server|my\s*domain/i; export const ASSET_PATTERNS = /자산|현황|대시보드|내\s*서버|내\s*도메인|보유|목록|리스트|내\s*서비스|내\s*계정|asset|my\s*server|my\s*domain/i;
export const DIAGRAM_PATTERNS = /다이어그램|diagram|구조.*그려|그림.*그려|시각화|visuali[sz]e|흐름도|flow.*chart|아키텍처.*보여|구성도/i;
// ============================================================================ // ============================================================================
// Pattern Matching Functions // Pattern Matching Functions
// ============================================================================ // ============================================================================