/** * İhale dokümanları — EKAP doküman URL'i üretimi + (Playwright varsa) metin çıkarma. * * Neden Playwright: doküman URL'i EKAP v2 API'sinden imzalı olarak üretilebiliyor, * ancak eski EKAP host'u (ekap.kik.gov.tr) otomatik istemcilere bağlantıyı kesiyor * (BIG-IP anti-bot → ECONNRESET). Kullanıcı tarayıcısında link sorunsuz açılır; * sunucu tarafında metin çıkarmak için gerçek tarayıcı gerekir. * * Chromium yoksa: URL yine döner (kullanıcı kendi tarayıcısında açar), * yalnızca sunucu tarafı metin çıkarma devre dışı kalır. */ import { searchTenders, ekapPost, getIhaleDetay } from './ekap-v2.mjs'; import { officialDateFields } from './tender-quality.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 documentCount = (record, fallback = 0) => { const declared = Number(record?.dokumanSayisi); const listed = Array.isArray(record?.dokumanListe) ? record.dokumanListe.length : 0; const fallbackCount = Number(fallback); return Math.max(Number.isFinite(declared) ? declared : 0, listed, Number.isFinite(fallbackCount) ? fallbackCount : 0); }; /** IKN'den taze bir EKAP kaydı bulur (id token anlıktır, saklanamaz). */ export async function findByIkn(ikn) { const [yil, sayi] = String(ikn || '').replace(/[\s\u00a0]+/g, '').split('/'); if (!yil || !sayi) throw new Error('Geçersiz İKN'); const r = await searchTenders({ iknYili: Number(yil), iknSayi: Number(sayi), take: 1 }); const t = r.list[0]; if (!t) throw new Error('İhale EKAP’ta bulunamadı'); return t; } /** İhale dokümanlarının EKAP görüntüleme/indirme adresini üretir. */ export async function documentUrl(ikn) { const t = await findByIkn(ikn); // Arama yanıtındaki doküman sayısı gecikebilir; detay kaydı güncel // doküman listesini taşıyorsa kullanıcıya onu göster. let detail = null; try { detail = await getIhaleDetay(t.id); } catch { /* yönlendirme yine üretilebilir */ } const d = await ekapPost('/b_ihalearama/api/EkapDokumanYonlendirme/GetDokumanUrl', { islemId: '1', ihaleId: t.id, }); if (!d?.success || !d?.url) { throw new Error(d?.message || 'Bu ihale için doküman bağlantısı üretilemedi'); } return { url: d.url, ikn: t.ikn, ihaleAdi: t.ihaleAdi, idareAdi: t.idareAdi, dokumanSayisi: documentCount(detail, t.dokumanSayisi), dokumanListe: detail?.dokumanListe || t.dokumanListe || [], }; } /** İhale ilan metni + kalem listesi (dokümana gerek kalmadan analiz için). */ export async function ilanMetni(ikn) { const t = await findByIkn(ikn); const d = await getIhaleDetay(t.id); const bilgi = d?.ihaleBilgi || {}; const officialDate = officialDateFields(bilgi.ihaleTarihSaat); const okas = d?.ihtiyacKalemiOkasList || []; const parcalar = [ `İhale adı: ${d?.ihaleAdi || t.ihaleAdi || ''}`, `İKN: ${d?.ikn || t.ikn || ''}`, `İdare: ${d?.idareAdi || t.idareAdi || ''}`, `Usul: ${bilgi.ihaleUsulAciklama || d?.ihaleUsul || ''}`, `Tür: ${bilgi.ihaleTipiAciklama || ''}`, `İhale tarihi: ${bilgi.ihaleTarihSaat || ''}`, `İşin yapılacağı yer: ${bilgi.isinYapilacagiYer || ''}`, `İhale yeri: ${bilgi.ihaleYeri || ''}`, bilgi.itirazenSikayetBasvuruBedeli ? `İtirazen şikâyet bedeli: ${bilgi.itirazenSikayetBasvuruBedeli}` : '', okas.length ? `İhtiyaç kalemleri (OKAS):\n` + okas.map((k) => ` - ${k.kodu} ${k.adi || k.koduAdi}`).join('\n') : '', Array.isArray(d?.ihaleOzellikList) && d.ihaleOzellikList.length ? 'Özellikler:\n' + d.ihaleOzellikList.map((o) => ` - ${o.ihaleOzellik}`).join('\n') : '', ].filter(Boolean); // İlan HTML'i varsa düz metne çevir (en zengin kaynak) const ilanlar = d?.ilanList || []; for (const il of ilanlar) { if (il.veriHtml) { parcalar.push(`\n--- ${il.ilanTipAciklama || 'İLAN'} (${il.ilanTarihi || ''}) ---`); parcalar.push(htmlToText(il.veriHtml)); } } return { ikn: t.ikn, ihaleAdi: d?.ihaleAdi || t.ihaleAdi, idareAdi: d?.idareAdi || t.idareAdi, ilanSayisi: ilanlar.length, okasSayisi: okas.length, dokumanSayisi: documentCount(d, t.dokumanSayisi), dokumanListe: d?.dokumanListe || t.dokumanListe || [], ...officialDate, metin: parcalar.join('\n'), }; } const ENT = { ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" }; function htmlToText(html) { return String(html || '') .replace(/<\s*(script|style)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ' ') .replace(/<\s*\/\s*(tr|p|div|h\d)\s*>/gi, '\n') .replace(/<\s*\/\s*t[dh]\s*>/gi, ' | ') .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(); } /** Chromium var mı? (DT worker ile aynı bağımlılık) */ export async function browserAvailable() { try { await import('playwright'); return true; } catch { return false; } } /** * Doküman sayfasını gerçek tarayıcıyla açıp metnini çıkarır. * Chromium yoksa anlaşılır hata verir. */ export async function documentText(ikn, { timeout = 60000 } = {}) { let chromium; try { ({ chromium } = await import('playwright')); } catch { throw new Error('Sunucuda tarayıcı bileşeni yok — doküman metni çıkarılamıyor. Bağlantıyı kendi tarayıcınızda açabilirsiniz.'); } const { url, ihaleAdi } = await documentUrl(ikn); const browser = await chromium.launch({ args: ['--no-sandbox', '--disable-dev-shm-usage'] }); try { const ctx = await browser.newContext({ userAgent: UA, locale: 'tr-TR' }); const page = await ctx.newPage(); await page.goto(url, { waitUntil: 'domcontentloaded', timeout }); await page.waitForTimeout(1500); const metin = await page.evaluate(() => document.body?.innerText || ''); return { ikn, ihaleAdi, url, metin: String(metin).replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim() }; } finally { await browser.close().catch(() => {}); } }