'use strict'; // 참고 사이트/기존 사이트 본문을 서버에서 직접 가져와 텍스트로 저장한다(.refs/ref-N.txt). // 인증서 검증 실패(self-signed 등)나 EUC-KR 인코딩 사이트도 읽을 수 있게 하여, Claude 의 WebFetch 실패에 의존하지 않는다. const { safeFetch } = require('./netguard'); const fs = require('fs'); const path = require('path'); const REFS_DIR = '.refs'; const MAX_BYTES = 3 * 1024 * 1024; const MAX_CHARS = 30000; const TIMEOUT = 15000; /** * 참고 주소 내려받기. v1.3.17: lib/netguard 로 단계마다 공개 주소인지 확인하고 그 주소로만 접속하며(S08 — 내부망 요청 차단), * 잘못된 리다이렉트 주소에도 프로세스가 죽지 않는다(S12). 관리자 소유 사이트(allowPrivate)만 사설 주소를 허용한다. * 인증서 검증 실패(self-signed 등) 사이트도 읽을 수 있게 rejectUnauthorized 는 끈다(본문 텍스트만 참고용으로 쓰므로). */ async function fetchRaw(url, opts = {}) { const r = await safeFetch(url, { allowPrivate: !!opts.allowPrivate, maxBytes: MAX_BYTES, timeout: TIMEOUT, rejectUnauthorized: false, accept: 'text/html,*/*;q=0.8', headers: { 'Accept-Language': 'ko,en;q=0.8' }, userAgent: 'Mozilla/5.0 (compatible; WebHostingAgent/1.0)' }); if (r.error) throw new Error(r.error); if (r.status >= 400) throw new Error(`HTTP ${r.status}`); return { buf: r.buf, contentType: r.type, finalUrl: r.finalUrl }; } function decode(buf, contentType) { let cs = (contentType.match(/charset=([\w-]+)/i) || [])[1]; if (!cs) { const head = buf.slice(0, 4096).toString('latin1'); cs = (head.match(/charset=["']?([\w-]+)/i) || [])[1]; } cs = (cs || 'utf-8').toLowerCase(); if (cs === 'ks_c_5601-1987' || cs === 'euc_kr' || cs === 'ksc5601') cs = 'euc-kr'; try { return new TextDecoder(cs).decode(buf); } catch (_) { return buf.toString('utf8'); } } function htmlToText(html) { const title = (html.match(/