feat: geçmiş veri motoru + gerçek AI + KİK kararları + TED + kırmızı bayrak + planlar
Roadmap'in 6 maddesi de gerçek çalışır halde:
1) Geçmiş veri motoru (EKAP v2)
- server/ekap-v2.mjs: AES-256-CBC imzalı EKAP v2 istemcisi (zero-dep),
ihale arama + sonuç ilanı parse (kazanan firma, sözleşme bedeli,
yaklaşık maliyet, teklif sayısı), TR firma adı normalize
- server/history-store.mjs: SQLite deposu (IKN+kısım anahtarlı, upsert)
- server/ekap-scan.mjs: tarih aralıklı artımlı tarayıcı (6 paralel)
- server/scheduler.mjs: sunucu içi periyodik tarama (SCAN_* env ile)
- /api/tenders artık geçmiş sonuçları da birleştiriyor → kazanan firma,
rakip ve kırmızı bayrak analizleri gerçek veriyle doluyor
2) Gerçek AI şartname analizi
- server/ai.mjs: 6 sağlayıcılı fallback zinciri (GLM/DeepSeek/Gemini/
Grok/GPT/Claude), JSON şemalı çıktı, uydurma yasak promptu
- pdfjs-dist ile tarayıcıda PDF metin çıkarma (dinamik import)
- AiSpecAnalyzerModal: sahte sonuç kaldırıldı, gerçek dosya + dürüst hata
3) KİK kurul kararları
- server/kik.mjs: EKAP v2 JSON API (Sec-1..6 imzalı), uyuşmazlık/
düzenleyici/mahkeme; karar tam metni (ayrı AES anahtarıyla URL)
- KikDecisions sekmesi: arama + takipteki idarelerden hızlı öneri
4) Fiyatlandırma + ödeme
- server/billing.mjs: Stripe Checkout (zero-dep, form-encoded)
- Pricing sekmesi: 3 plan, aylık/yıllık, yapılandırılmadıysa dürüst uyarı
5) TED (AB ihaleleri)
- server/ted.mjs: anonim TED v3 API, 3 mod (açık/tüm TR/AB'de kazanan TR)
- rate-limit korumalı (429 HTML gövdesi ele alınır), çok dilli alan çözümü
6) Kırmızı bayrak & şeffaflık
- analytics.js: redFlags (tek teklif, düşük kırım, tahmin üstü bedel,
aşırı düşük teklif, tedarikçi yoğunlaşması) + marketConcentration (HHI)
- RedFlags sekmesi: seviyeli uyarılar + yöntem şeffaflığı notu
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* EKAP geçmiş sonuç tarayıcısı — kazanan firma + sözleşme bedeli + teklif sayısı biriktirir.
|
||||
*
|
||||
* Kullanım:
|
||||
* node server/ekap-scan.mjs --gun 7 # son 7 günün sonuçlananları
|
||||
* node server/ekap-scan.mjs --bas 2026-01-01 --son 2026-06-30
|
||||
* node server/ekap-scan.mjs --gun 1 --limit 200 # cron için (günlük artımlı)
|
||||
*
|
||||
* Notlar:
|
||||
* - EKAP arama indeksi sonuç ilanlarını kapsamaz; bu yüzden sonuçlanmış ihaleler
|
||||
* sayfalanır ve HER ihalenin ilanları ayrıca çekilir (N+1 kaçınılmaz).
|
||||
* - Eşzamanlılık 6 (repo referansı 8) — EKAP'ı yormamak için.
|
||||
* - Zaten kayıtlı IKN'ler atlanır, böylece tekrar çalıştırmak ucuzdur.
|
||||
*/
|
||||
|
||||
import {
|
||||
searchTenders, fetchResults, normalizeCompanyName, DURUM_SONUC_ILANI,
|
||||
} from './ekap-v2.mjs';
|
||||
import { openDb, saveResults, hasIkn, logScan, available } from './history-store.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const arg = (n, d = null) => {
|
||||
const i = args.indexOf(`--${n}`);
|
||||
return i >= 0 && args[i + 1] ? args[i + 1] : d;
|
||||
};
|
||||
const has = (n) => args.includes(`--${n}`);
|
||||
|
||||
const iso = (d) => d.toISOString().slice(0, 10);
|
||||
|
||||
const gun = Number(arg('gun', 0));
|
||||
let bas = arg('bas');
|
||||
let son = arg('son');
|
||||
if (!bas || !son) {
|
||||
const bugun = new Date();
|
||||
const geri = new Date(bugun.getTime() - (gun || 7) * 86400000);
|
||||
bas = bas || iso(geri);
|
||||
son = son || iso(bugun);
|
||||
}
|
||||
const LIMIT = Number(arg('limit', 500));
|
||||
const SAYFA = 50;
|
||||
const ESZAMANLI = Number(arg('paralel', 6));
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function havuz(items, worker, n) {
|
||||
const sonuc = [];
|
||||
let i = 0;
|
||||
const calisanlar = Array.from({ length: Math.min(n, items.length) }, async () => {
|
||||
while (i < items.length) {
|
||||
const idx = i++;
|
||||
try { sonuc.push(await worker(items[idx])); }
|
||||
catch (e) { sonuc.push({ hata: e.message }); }
|
||||
}
|
||||
});
|
||||
await Promise.all(calisanlar);
|
||||
return sonuc;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!available()) {
|
||||
console.error('[ekap-scan] node:sqlite yok — bu Node sürümünde tarama yapılamaz.');
|
||||
process.exit(1);
|
||||
}
|
||||
const db = openDb({ create: true });
|
||||
const baslangic = new Date().toISOString();
|
||||
console.log(`[ekap-scan] aralık ${bas} → ${son}, limit ${LIMIT}, paralel ${ESZAMANLI}`);
|
||||
|
||||
let skip = 0;
|
||||
let taranan = 0;
|
||||
let eklenen = 0;
|
||||
let hata = 0;
|
||||
let toplamHavuz = null;
|
||||
|
||||
while (taranan < LIMIT) {
|
||||
let sayfa;
|
||||
try {
|
||||
sayfa = await searchTenders({
|
||||
ihaleDurumIdList: [DURUM_SONUC_ILANI],
|
||||
ihaleTarihBaslangic: bas,
|
||||
ihaleTarihBitis: son,
|
||||
orderBy: 'ihaleTarihi',
|
||||
siralamaTipi: 'desc',
|
||||
skip,
|
||||
take: Math.min(SAYFA, LIMIT - taranan),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[ekap-scan] arama hatası:', e.message);
|
||||
hata++;
|
||||
break;
|
||||
}
|
||||
if (toplamHavuz == null) {
|
||||
toplamHavuz = sayfa.totalCount;
|
||||
console.log(`[ekap-scan] aralıkta ${toplamHavuz} sonuçlanmış ihale bulundu`);
|
||||
}
|
||||
if (!sayfa.list.length) break;
|
||||
|
||||
// Zaten kayıtlı olanları atla (IKN kalıcı anahtar)
|
||||
const yeni = sayfa.list.filter((t) => !hasIkn(db, t.ikn));
|
||||
const atlanan = sayfa.list.length - yeni.length;
|
||||
|
||||
const sonuclar = await havuz(yeni, async (t) => {
|
||||
const rs = await fetchResults(t.id);
|
||||
if (!rs.length) return { bos: true };
|
||||
for (const r of rs) r.kazananNorm = normalizeCompanyName(r.kazananFirma);
|
||||
const n = saveResults(db, t, rs);
|
||||
return { kayit: n, ikn: t.ikn, firma: rs[0].kazananFirma };
|
||||
}, ESZAMANLI);
|
||||
|
||||
for (const s of sonuclar) {
|
||||
if (s?.hata) hata++;
|
||||
else if (s?.kayit) eklenen += s.kayit;
|
||||
}
|
||||
taranan += sayfa.list.length;
|
||||
skip += sayfa.list.length;
|
||||
console.log(`[ekap-scan] ${taranan}/${Math.min(LIMIT, toplamHavuz)} tarandı · ${eklenen} kayıt · ${atlanan} atlandı · ${hata} hata`);
|
||||
|
||||
if (skip >= toplamHavuz) break;
|
||||
await sleep(300); // EKAP'ı yormamak için nazik ara
|
||||
}
|
||||
|
||||
logScan(db, {
|
||||
baslangic, bitis: new Date().toISOString(),
|
||||
aralikBas: bas, aralikSon: son, taranan, eklenen, hata,
|
||||
not: `havuz=${toplamHavuz}`,
|
||||
});
|
||||
db.close();
|
||||
console.log(`[ekap-scan] BİTTİ — ${taranan} ihale tarandı, ${eklenen} sonuç kaydı yazıldı, ${hata} hata.`);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('[ekap-scan] ölümcül hata:', e); process.exit(1); });
|
||||
Reference in New Issue
Block a user