import { HandlerContext } from './types'; import { buildUrlContext } from '../../lib/contextBuilders/urlContext'; /** * Action 15: Fetch URL (read-only — transaction record 불필요) * * LLM 이 작업 중 웹 페이지의 실제 내용이 필요할 때 emit 하는 태그. * 결과 블록을 chatHistory 에 internal push — read_file 과 동일 패턴이라 * 일반 챗의 continuation loop(컨텍스트 주입 → 자동 재호출)가 그대로 작동해 * fetch 직후 모델이 내용을 분석한다. * * buildUrlContext 재사용: Bridge 추출 우선 → 직접 fetch 폴백 → 정직 실패 * 블록 + 5분 캐시. 실패 블록도 chatHistory 에 push 한다 — 모델이 "가져왔다"고 * 지어내지 않고 실패 사실을 사용자에게 전달해야 하므로. */ const FETCH_URL_MAX_PER_TURN = 2; export async function applyWebFetchActions(ctx: HandlerContext): Promise { const { aiMessage, report } = ctx; const fetchRegex = /\s]+)['"]?\s*\/?>(?:<\/fetch_url>)?/gi; let match; let handled = 0; const seen = new Set(); while ((match = fetchRegex.exec(aiMessage)) !== null) { if (handled >= FETCH_URL_MAX_PER_TURN) { report.push(`⚠️ fetch_url 한도 초과 — 회당 최대 ${FETCH_URL_MAX_PER_TURN}개만 처리합니다.`); break; } const url = match[1].trim(); if (seen.has(url)) continue; seen.add(url); handled++; try { const block = await buildUrlContext(url); const ok = block.includes('실데이터'); report.push(ok ? `🌐 Fetched: ${url}` : `⚠️ Fetch failed: ${url}`); ctx.chatHistory.push({ role: 'system', content: block, internal: true }); } catch (err: any) { // buildUrlContext 는 throw 하지 않지만 방어적으로. report.push(`⚠️ Fetch error: ${url} — ${String(err?.message ?? err).slice(0, 100)}`); } } }