From d261d01981f1a90618ef95715f0e648f0890f1a1 Mon Sep 17 00:00:00 2001 From: kappa Date: Thu, 12 Feb 2026 02:33:29 +0900 Subject: [PATCH] Add network diagnostic tool for domain connectivity troubleshooting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/agents/troubleshoot-agent.ts | 135 +++++++- src/services/network-diagnostic.ts | 529 +++++++++++++++++++++++++++++ src/types.ts | 78 +++-- src/utils/patterns.ts | 4 +- 4 files changed, 712 insertions(+), 34 deletions(-) create mode 100644 src/services/network-diagnostic.ts diff --git a/src/agents/troubleshoot-agent.ts b/src/agents/troubleshoot-agent.ts index 37e5e77..ea2d3ef 100644 --- a/src/agents/troubleshoot-agent.ts +++ b/src/agents/troubleshoot-agent.ts @@ -11,6 +11,8 @@ import type { ToolDefinition, TroubleshootSession } from '../types'; import type { AgentToolContext } 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 { getSessionConfig } from '../constants/agent-config'; import { createLogger } from '../utils/logger'; @@ -51,19 +53,20 @@ export class TroubleshootAgent extends BaseAgent { protected getSystemPrompt(session: TroubleshootSession): string { return `당신은 호스팅/인프라 서비스의 기술 문제 해결 전문가입니다. -고객의 문제를 체계적으로 진단하고 해결책을 제시합니다. +고객의 문제를 신속하게 진단하고 해결책을 제시합니다. + +## 핵심 원칙: 도구 먼저, 질문은 나중에 +고객이 문제를 말하면 **먼저 도구를 호출하여 상태를 확인**하세요. 질문부터 하지 마세요. +- "서버 접속이 안 돼요" → 바로 check_server_status 호출 +- "도메인이 안 열려요" → 바로 check_domain_status 호출 +- "VPN 연결이 안 돼요" → 바로 check_service_status 호출 +도구 결과를 확인한 후, 추가 정보가 필요하면 그때 질문하세요. ## 문제 해결 프로세스 -1. **상태 파악**: 현재 상태 확인 (서버 상태, 도메인 상태, 서비스 상태) -2. **원인 분석**: 수집된 정보를 기반으로 근본 원인 분석 +1. **즉시 상태 조회**: 고객 메시지에서 키워드를 파악하여 관련 도구 즉시 호출 +2. **결과 기반 진단**: 조회 결과를 분석하여 원인 파악 3. **해결책 제시**: 단계별 해결 방법 안내 -4. **예측**: 재발 방지 및 모니터링 권고 - -## 정보 수집 항목 -- **카테고리**: 서버, 도메인, DDoS, VPN, 네트워크, 기타 -- **증상**: 구체적 증상 (접속 불가, 느림, 오류 메시지 등) -- **환경**: OS, 브라우저, 리전, 사용 중인 서비스 -- **에러 메시지**: 정확한 에러 메시지 또는 코드 +4. **추가 확인**: 필요 시 추가 질문 또는 에스컬레이션 ## 현재 세션 상태: ${session.status} ## 에스컬레이션 카운트: ${session.escalation_count || 0} @@ -71,15 +74,23 @@ export class TroubleshootAgent extends BaseAgent { ## 대화 원칙 - 항상 한국어로 응답하세요. - 전문적이지만 이해하기 쉽게 설명하세요. +- 진단 결과를 단정 짓지 말고 추정형으로 이야기하세요. + 좋은 예: "DB 서버가 중지 상태로 보이는데, 이 부분이 원인일 가능성이 있습니다" + 나쁜 예: "DB 서버가 중지 상태입니다. 이것이 원인입니다" + 조회 결과는 시스템 기록이므로 실제 상황과 다를 수 있음을 인지하세요. - 문제와 무관한 메시지가 오면 __PASSTHROUGH__를 응답하세요. - 상담이 완료되면 __SESSION_END__를 응답 끝에 추가하세요. - 3라운드 이내에 해결이 어려운 경우 __ESCALATE__를 응답에 포함하세요. 이 경우 고객에게 "전문 엔지니어에게 전달하겠습니다"라고 안내하세요. ## 도구 사용 -- check_server_status: 서버 상태 확인 -- check_domain_status: 도메인 상태 확인 -- check_service_status: DDoS/VPN 서비스 상태 확인`; +- check_server_status: 서버 상태 확인 (server_id 없으면 전체 목록) +- check_domain_status: 도메인 상태 확인 (domain 없으면 전체 목록) +- check_service_status: DDoS/VPN 서비스 상태 확인 +- diagnose_domain: 도메인 네트워크 진단 (DNS 조회, ISP 차단 감지, HTTP/TCP 연결 테스트) + - "접속이 안 돼요", "사이트가 안 열려요", "차단된 것 같아요" 등의 문의 시 즉시 호출 + - DNS, HTTP, ISP 차단 여부를 한번에 확인하여 원인을 빠르게 파악 +- generate_diagram: 문제 상황을 Mermaid 다이어그램으로 시각화 (충분한 정보 수집 후)`; } protected getTools(): ToolDefinition[] { @@ -137,6 +148,50 @@ export class TroubleshootAgent extends BaseAgent { }, }, }, + { + 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 { args.service_id as number | undefined, 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: return JSON.stringify({ error: `알 수 없는 도구: ${name}` }); } @@ -292,4 +358,47 @@ export class TroubleshootAgent extends BaseAgent { return JSON.stringify({ error: '서비스 상태 조회에 실패했습니다.' }); } } + + private async handleDiagnoseDomain( + domain: string, + ports?: number[] + ): Promise { + // 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 { + 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); + } } diff --git a/src/services/network-diagnostic.ts b/src/services/network-diagnostic.ts new file mode 100644 index 0000000..d5b01b3 --- /dev/null +++ b/src/services/network-diagnostic.ts @@ -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 { + 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 = 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 { + 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 { + const query = buildDnsQuery(domain); + + return Promise.all( + ISP_DNS_SERVERS.map(async (server) => { + const start = Date.now(); + try { + const timeoutPromise = new Promise((_, 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 { + 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 { + // 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((_, reject) => + setTimeout(() => reject(new Error('timeout')), CHECK_TIMEOUT_MS) + ); + + const socket = (globalThis as unknown as { + connect: (opts: { hostname: string; port: number }) => { + opened: Promise; + 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 { + 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, + }; +} diff --git a/src/types.ts b/src/types.ts index 11e055a..758e28c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,11 +16,14 @@ export interface Env { DEPOSIT_BANK_HOLDER?: string; // API URLs OPENAI_API_BASE?: string; - D2_RENDER_URL?: string; NAMECHEAP_API_URL?: string; WHOIS_API_URL?: string; CLOUD_ORCHESTRATOR_URL?: string; CLOUD_ORCHESTRATOR?: Fetcher; + // Kroki + KROKI_URL?: string; + // Queues + WORK_QUEUE?: Queue; // KV Namespaces RATE_LIMIT_KV: KVNamespace; SESSION_KV: KVNamespace; @@ -401,10 +404,6 @@ export interface CheckServiceArgs { service_id?: number; } -export interface RenderD2Args { - source: string; - format?: 'svg' | 'png'; -} export interface AdminArgs { action: 'block_user' | 'unblock_user' | 'set_role' | 'broadcast' | 'confirm_deposit' | 'reject_deposit' | 'list_pending'; @@ -421,6 +420,60 @@ export interface ApproveActionArgs { 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 // ============================================ @@ -448,21 +501,6 @@ export type KeyboardCallbackData = | ActionApprovalKeyboardData | EscalationKeyboardData; -// ============================================ -// D2 Rendering -// ============================================ - -export interface D2RenderRequest { - source: string; - format: 'svg' | 'png'; -} - -export interface D2RenderResponse { - success: boolean; - image?: ArrayBuffer; - error?: string; -} - // ============================================ // Request Context // ============================================ diff --git a/src/utils/patterns.ts b/src/utils/patterns.ts index 6218137..d290de1 100644 --- a/src/utils/patterns.ts +++ b/src/utils/patterns.ts @@ -16,7 +16,7 @@ export const BILLING_PATTERNS = /입금|충전|잔액|계좌|예치금|송금| 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; @@ -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 DIAGRAM_PATTERNS = /다이어그램|diagram|구조.*그려|그림.*그려|시각화|visuali[sz]e|흐름도|flow.*chart|아키텍처.*보여|구성도/i; + // ============================================================================ // Pattern Matching Functions // ============================================================================