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:
+122
-1
@@ -18,6 +18,12 @@ import { fileURLToPath } from 'node:url';
|
||||
import { readDtTenders } from './dt-store.mjs';
|
||||
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
|
||||
import { emailConfigured, sendEmail, tenderDigestHtml } from './email.mjs';
|
||||
import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs';
|
||||
import { billingConfigured, planCatalog, createCheckoutSession } from './billing.mjs';
|
||||
import { searchTed } from './ted.mjs';
|
||||
import { readHistory, historyStats } from './history-store.mjs';
|
||||
import { startScheduler } from './scheduler.mjs';
|
||||
import { searchKik, kararMetni } from './kik.mjs';
|
||||
|
||||
// ilan.gov.tr sunucusu ara sertifikayı (GeoTrust TLS RSA CA G1) göndermiyor;
|
||||
// zinciri tamamlamak için PEM'i yerelden ekliyoruz.
|
||||
@@ -337,6 +343,111 @@ const server = http.createServer(async (req, res) => {
|
||||
return json(res, 200, { success: true, user });
|
||||
}
|
||||
|
||||
// --- KİK kurul kararları ---
|
||||
if (url.pathname === '/api/kik/search') {
|
||||
try {
|
||||
const q = url.searchParams.get('q') || '';
|
||||
const out = await searchKik(
|
||||
{
|
||||
metin: url.searchParams.get('metin') || (url.searchParams.get('alan') ? null : q),
|
||||
idare: url.searchParams.get('idare'),
|
||||
basvuran: url.searchParams.get('basvuran'),
|
||||
kararNo: url.searchParams.get('kararNo'),
|
||||
yil: url.searchParams.get('yil'),
|
||||
},
|
||||
url.searchParams.get('tur') || 'uyusmazlik',
|
||||
Number(url.searchParams.get('limit')) || 25
|
||||
);
|
||||
return json(res, 200, { success: true, ...out });
|
||||
} catch (e) {
|
||||
return json(res, 502, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
if (url.pathname === '/api/kik/karar') {
|
||||
try {
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) return json(res, 400, { success: false, error: 'id gerekli' });
|
||||
const metin = await kararMetni(id);
|
||||
return json(res, 200, { success: true, id, metin });
|
||||
} catch (e) {
|
||||
return json(res, 502, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Geçmiş sonuç ilanları (kazanan firma arşivi) ---
|
||||
if (url.pathname === '/api/history') {
|
||||
const out = readHistory(Number(url.searchParams.get('limit')) || 300, {
|
||||
firma: url.searchParams.get('firma'),
|
||||
idare: url.searchParams.get('idare'),
|
||||
il: url.searchParams.get('il'),
|
||||
});
|
||||
return json(res, 200, { success: true, ...out });
|
||||
}
|
||||
if (url.pathname === '/api/history/stats') {
|
||||
return json(res, 200, { success: true, ...historyStats() });
|
||||
}
|
||||
|
||||
// --- TED (AB ihaleleri) ---
|
||||
if (url.pathname === '/api/ted') {
|
||||
try {
|
||||
const out = await searchTed({
|
||||
mod: url.searchParams.get('mod') || 'turkiye',
|
||||
since: url.searchParams.get('since') || '20240101',
|
||||
page: Number(url.searchParams.get('page')) || 1,
|
||||
limit: Number(url.searchParams.get('limit')) || 25,
|
||||
});
|
||||
return json(res, 200, { success: true, ...out });
|
||||
} catch (e) {
|
||||
return json(res, 502, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Abonelik / ödeme ---
|
||||
if (url.pathname === '/api/billing/plans') {
|
||||
return json(res, 200, { configured: billingConfigured(), plans: planCatalog() });
|
||||
}
|
||||
if (url.pathname === '/api/billing/checkout' && req.method === 'POST') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const out = await createCheckoutSession({ planId: body.planId, email: user.email });
|
||||
return json(res, 200, { success: true, ...out });
|
||||
} catch (e) {
|
||||
return json(res, billingConfigured() ? 400 : 503, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Gerçek AI şartname analizi ---
|
||||
if (url.pathname === '/api/ai/config') {
|
||||
return json(res, 200, { configured: aiConfigured(), providers: availableProviders() });
|
||||
}
|
||||
if (url.pathname === '/api/ai/analyze' && req.method === 'POST') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||
if (!aiConfigured()) {
|
||||
return json(res, 503, {
|
||||
success: false,
|
||||
error: 'AI yapılandırılmamış — sunucuya en az bir sağlayıcı anahtarı ekleyin (ZAI_API_KEY / DEEPSEEK_API_KEY / GEMINI_API_KEY …)',
|
||||
});
|
||||
}
|
||||
try {
|
||||
const body = await readBody(req, 2 * 1024 * 1024);
|
||||
if (!body.text || String(body.text).trim().length < 40) {
|
||||
return json(res, 400, { success: false, error: 'Analiz için yeterli metin yok (en az 40 karakter).' });
|
||||
}
|
||||
const out = await analyzeSpec({
|
||||
text: body.text,
|
||||
fileName: body.fileName,
|
||||
tender: body.tender,
|
||||
chain: body.chain,
|
||||
});
|
||||
return json(res, 200, { success: true, ...out, analyzedAt: new Date().toISOString() });
|
||||
} catch (e) {
|
||||
return json(res, 502, { success: false, error: e.message, attempts: e.attempts || null });
|
||||
}
|
||||
}
|
||||
|
||||
// --- E-posta bildirimi (Resend) ---
|
||||
if (url.pathname === '/api/notify/email/config') {
|
||||
return json(res, 200, { configured: emailConfigured() });
|
||||
@@ -370,8 +481,16 @@ const server = http.createServer(async (req, res) => {
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] DT okuma hatası:', e.message);
|
||||
}
|
||||
// Geçmiş sonuç ilanları (kazanan firma + bedel + teklif sayısı)
|
||||
let hist = { tenders: [], totalCount: 0, available: false };
|
||||
try {
|
||||
hist = readHistory(600);
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message);
|
||||
}
|
||||
const sources = ['ilan.gov.tr (Basın İlan Kurumu) — resmi ihale ilanları'];
|
||||
if (dt.available) sources.push('EKAP Doğrudan Temin (22/d) worker');
|
||||
if (hist.available && hist.totalCount) sources.push('EKAP Sonuç İlanları (geçmiş sonuçlar)');
|
||||
return json(res, 200, {
|
||||
success: true,
|
||||
source: sources.join(' + '),
|
||||
@@ -379,7 +498,8 @@ const server = http.createServer(async (req, res) => {
|
||||
totalCount: c.totalCount + dt.totalCount,
|
||||
todayCount: c.todayCount,
|
||||
dtCount: dt.totalCount,
|
||||
tenders: [...dt.tenders, ...c.tenders],
|
||||
historyCount: hist.totalCount,
|
||||
tenders: [...dt.tenders, ...c.tenders, ...hist.tenders],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -428,4 +548,5 @@ const server = http.createServer(async (req, res) => {
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[ihaleist-api] http://localhost:${PORT} — canlı veri: ilan.gov.tr`);
|
||||
getTenders().catch((e) => console.error('[ihaleist-api] ilk yükleme hatası:', e.message));
|
||||
startScheduler();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user