feat: İhaleAB'den taşınan özellikler — MCP sunucusu, RSS, haftalık brief, teklif asistanı, yeterlilik rozeti, 5 yeni kırmızı bayrak
- /mcp (JSON-RPC, ih_live_ anahtarı): search_tenders, get_tender, search_results, search_items, upcoming_deadlines - /api/feeds/tenders.rss (anahtarlı) ve /api/feeds/sonuc.rss (açık) - Haftalık brief (Pazartesi 08:00, Telegram + e-posta), /api/me/brief tercihi + önizleme, sağ panelde yaklaşan ihale tarihleri - Detayda Teklif Asistanı sekmesi: belgeye sor (atıflı), taslak denetimi, taslak üretimi (analyzeSpec zinciri) - Derin analiz belge listesi × Doküman Deposu eşleştirme rozeti - Kırmızı bayrak: kısa süre, hafta sonu tarih, geç sözleşme, kazananı gizli, DT parçalama şüphesi - Kullanım kılavuzu güncellendi Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ import { readHistory, historyStats, havuzOlc, havuzGecmisi, havuzOzet,
|
||||
import { startScheduler, schedulerStatus } from './scheduler.mjs';
|
||||
import { searchKik, kararMetni } from './kik.mjs';
|
||||
import { documentUrl, ilanMetni, documentText, browserAvailable } from './documents.mjs';
|
||||
import { ihaleRss, sonucRss, mcpIstek, yaklasanIhaleler } from './feeds-mcp.mjs';
|
||||
import { briefOlustur, briefMetni, haftalikBriefGonder } from './brief.mjs';
|
||||
import { belgeyeSor, teklifDenetle, teklifTaslak } from './ai-teklif.mjs';
|
||||
import { subscribe, list as listSubs, remove as removeSub, available as notifyAvailable } from './notify-store.mjs';
|
||||
import { bildirimleriIsle } from './notifier.mjs';
|
||||
import { scanSite } from './site-scan.mjs';
|
||||
@@ -1321,6 +1324,67 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- RSS beslemeleri + MCP sunucusu + haftalık brief + teklif asistanı (İhaleAB'den taşındı) ----
|
||||
if (url.pathname === '/api/feeds/tenders.rss') {
|
||||
const key = url.searchParams.get('key') || '';
|
||||
const sahip = key.startsWith('ih_live_') ? store.verifyApiKey(key) : null;
|
||||
if (!sahip) { res.writeHead(401, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('API anahtarı gerekli: ?key=ih_live_… (API & Webhooks)'); }
|
||||
const out = await tendersPayload();
|
||||
const xml = ihaleRss(out.tenders, { q: url.searchParams.get('q'), il: url.searchParams.get('il'), sektor: url.searchParams.get('sektor'), sadeceAcik: url.searchParams.get('acik') !== '0', limit: url.searchParams.get('limit') || 50 });
|
||||
res.writeHead(200, { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=600' });
|
||||
return res.end(xml);
|
||||
}
|
||||
if (url.pathname === '/api/feeds/sonuc.rss') {
|
||||
const xml = sonucRss({ firma: url.searchParams.get('firma'), idare: url.searchParams.get('idare'), il: url.searchParams.get('il') });
|
||||
res.writeHead(200, { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=900' });
|
||||
return res.end(xml);
|
||||
}
|
||||
if (url.pathname === '/mcp') {
|
||||
return mcpIstek(req, res, { readBody, json, tenderGetir: async () => (await tendersPayload()).tenders });
|
||||
}
|
||||
if (url.pathname === '/api/me/yaklasan') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||
const out = await tendersPayload();
|
||||
return json(res, 200, { success: true, tenders: yaklasanIhaleler(user.email, out.tenders, Number(url.searchParams.get('gun')) || 14) });
|
||||
}
|
||||
if (url.pathname === '/api/me/brief') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||
store.ensureUser(user.email, user.name);
|
||||
if (req.method === 'POST') {
|
||||
const body = await readBody(req).catch(() => ({}));
|
||||
store.setData(user.email, 'brief', { kapali: body.kapali === true });
|
||||
return json(res, 200, { success: true, kapali: body.kapali === true });
|
||||
}
|
||||
const out = await tendersPayload();
|
||||
const b = briefOlustur(user.email, out.tenders);
|
||||
return json(res, 200, { success: true, kapali: !!(store.getData(user.email, 'brief')?.kapali), brief: b, metin: briefMetni(b) });
|
||||
}
|
||||
if (url.pathname === '/api/admin/brief/gonder' && req.method === 'POST') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user?.isAdmin) return json(res, 403, { success: false, error: 'yetki yok' });
|
||||
const r = await haftalikBriefGonder(async () => (await tendersPayload()).tenders, { zorla: url.searchParams.get('zorla') === '1' });
|
||||
return json(res, 200, { success: true, ...r });
|
||||
}
|
||||
if ((url.pathname === '/api/ai/belge-sor' || url.pathname === '/api/ai/teklif-denetim' || url.pathname === '/api/ai/teklif-taslak') && 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 bir sağlayıcı anahtarı ekleyin.' });
|
||||
try {
|
||||
const body = await readBody(req, 2 * 1024 * 1024);
|
||||
const ortak = { ikn: body.ikn, tender: body.tender, chain: body.chain, text: body.text };
|
||||
const out = url.pathname === '/api/ai/belge-sor'
|
||||
? await belgeyeSor({ ...ortak, soru: body.soru })
|
||||
: url.pathname === '/api/ai/teklif-denetim'
|
||||
? await teklifDenetle({ ...ortak, taslak: body.taslak, gereklilikler: body.gereklilikler })
|
||||
: await teklifTaslak({ ...ortak, tur: body.tur, firma: body.firma, belgeler: body.belgeler, fiyat: body.fiyat, gereklilikler: body.gereklilikler });
|
||||
return json(res, 200, { success: true, ...out });
|
||||
} catch (e) {
|
||||
return json(res, e.attempts ? 502 : 400, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/tenders') {
|
||||
const out = await tendersPayload(url.searchParams.get('refresh') === '1');
|
||||
return json(res, 200, out);
|
||||
|
||||
Reference in New Issue
Block a user