/** * Gerçek firma web sitesi tarama + AI ile yetkinlik çıkarımı. * * Eski webScraperService.js sabit bir anahtar-kelime sözlüğüydü (domainde * "medikal" geçince tıbbi ürün listesi uyduruyordu). Bu modül siteyi * GERÇEKTEN çeker, metnini çıkarır ve AI ile ne tedarik edebildiğini belirler. * * Bağımlılık yok (Node 18+ fetch). */ import { analyzeSpec, aiConfigured } from './ai.mjs'; const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'; const MAX_BYTE = 1.5 * 1024 * 1024; const ENT = { ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" }; function htmlToText(html) { return String(html || '') .replace(/<\s*(script|style|noscript|svg)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ' ') .replace(/<\s*\/\s*(p|div|li|h[1-6]|tr|section)\s*>/gi, '\n') .replace(//gi, '\n') .replace(/<[^>]+>/g, ' ') .replace(/&[a-z#0-9]+;/gi, (m) => ENT[m.toLowerCase()] ?? ' ') .replace(/[ \t]+/g, ' ') .replace(/\n{3,}/g, '\n\n') .trim(); } function normalizeUrl(input) { let u = String(input || '').trim(); if (!u) throw new Error('Adres gerekli'); if (!/^https?:\/\//i.test(u)) u = 'https://' + u; const url = new URL(u); if (!/^https?:$/.test(url.protocol)) throw new Error('Yalnızca http/https desteklenir'); return url; } async function fetchPage(url, timeout = 20000) { const res = await fetch(url, { headers: { 'User-Agent': UA, 'Accept-Language': 'tr,en;q=0.8', Accept: 'text/html,application/xhtml+xml' }, redirect: 'follow', signal: AbortSignal.timeout(timeout), }); if (!res.ok) throw new Error(`Sayfa alınamadı (HTTP ${res.status})`); const tip = res.headers.get('content-type') || ''; if (!/text\/html|application\/xhtml/i.test(tip)) throw new Error('Adres bir web sayfası değil'); const buf = await res.arrayBuffer(); if (buf.byteLength > MAX_BYTE) throw new Error('Sayfa çok büyük'); return new TextDecoder('utf-8').decode(buf); } // Ana sayfadan iç sayfa adaylarını çıkar (hakkımızda, ürünler, hizmetler…) const IC_SAYFA = /(urun|ürün|hizmet|product|service|hakk|about|katalog|cozum|çözüm|referans|belge|sertifika)/i; function icLinkler(html, base) { const out = new Set(); const re = /]+href=["']([^"'#]+)["']/gi; let m; while ((m = re.exec(html)) && out.size < 40) { try { const u = new URL(m[1], base); if (u.hostname !== new URL(base).hostname) continue; if (!IC_SAYFA.test(u.pathname)) continue; u.hash = ''; out.add(u.href); } catch { /* geçersiz link */ } } return [...out].slice(0, 4); } const SISTEM = `Sen Türkiye kamu ihalelerine katılan firmaların yetkinliklerini belirleyen bir analistsin. Sana bir firmanın web sitesinden çıkarılmış metin verilecek. YALNIZCA geçerli JSON döndür, başka metin yazma. Şema: { "firmaAdi": "sitede geçen firma adı veya null", "ozet": "firmanın ne yaptığına dair 1-2 cümle", "sektorler": ["firmanın faaliyet gösterdiği sektörler"], "urunler": ["tedarik edebildiği somut ürün/hizmetler"], "sertifikalar": ["sitede belirtilen belge/sertifikalar"], "ihaleKonulari": ["bu firmanın teklif verebileceği ihale konu başlıkları — kısa, aranabilir terimler"] } KURALLAR: - Yalnızca metinde GERÇEKTEN geçen bilgileri yaz. Uydurma. - Metinde yoksa boş dizi veya null bırak. - ihaleKonulari en fazla 12 madde, her biri 1-4 kelime, ihale ilanlarında aranabilecek terimler olsun (ör. "tıbbi sarf malzeme", "iş güvenliği ekipmanı", "temizlik kimyasalı").`; /** * Siteyi çeker ve AI ile yetkinlik çıkarır. * @returns {{url, tarananSayfa, karakter, sonuc}} */ export async function scanSite(inputUrl) { if (!aiConfigured()) { throw new Error('Site analizi için AI yapılandırılmamış (OPENROUTER_API_KEY gerekli).'); } const url = normalizeUrl(inputUrl); const ana = await fetchPage(url.href); const sayfalar = [{ url: url.href, metin: htmlToText(ana) }]; // Birkaç iç sayfayı da tara (ürün/hizmet sayfaları en bilgilendirici) for (const link of icLinkler(ana, url.href)) { try { const html = await fetchPage(link, 15000); const metin = htmlToText(html); if (metin.length > 200) sayfalar.push({ url: link, metin }); } catch { /* iç sayfa atlanabilir */ } } const birlesik = sayfalar .map((s) => `--- ${s.url} ---\n${s.metin.slice(0, 12000)}`) .join('\n\n') .slice(0, 45000); if (birlesik.replace(/\s/g, '').length < 200) { throw new Error('Siteden yeterli metin çıkarılamadı (JavaScript ile yüklenen bir site olabilir).'); } const out = await analyzeSpec({ text: birlesik, fileName: url.hostname, tender: null, chain: null, systemOverride: SISTEM, }); return { url: url.href, tarananSayfa: sayfalar.length, sayfalar: sayfalar.map((s) => s.url), karakter: birlesik.length, model: out.model, sonuc: out.result, }; }