feat: konu bazlı bildirim + gerçek site tarama

1) Bildirim artık KONU odaklı (il değil)
   - Kullanıcı hangi konularda ihale alabildiğini seçer; varsayılan tüm Türkiye
   - İl/alım türü/favori kurum 'daha da daralt' altında isteğe bağlı
   - notify-store: konular kolonu + otomatik şema geçişi
   - notifier: konu başlıkları ihale başlığı/sektör/branş/kalemlerde aranır

2) Gerçek firma sitesi tarama (webScraperService mock'unun yerine)
   - server/site-scan.mjs: siteyi GERÇEKTEN çeker, ana sayfa + ürün/hizmet/
     hakkımızda iç sayfalarını tarar, HTML'i metne çevirir, AI ile firmanın
     tedarik edebildiği ihale konularını çıkarır ('uydurma' yasaklı prompt)
   - /api/company/scan (auth'lu)
   - ai.mjs: systemOverride desteği (farklı görevler için ayrı sistem promptu)
   - Arayüz: 'Sitenizi tarayıp konuları otomatik önerelim' → tıklayarak ekleme

Not: eski webScraperService.js sabit anahtar-kelime sözlüğüydü; domain adında
'tanriverdi' geçince tıbbi ürün listesi uyduruyordu.
This commit is contained in:
2026-08-29 10:19:44 +03:00
parent b44b45c67e
commit f73c7bd6cb
6 changed files with 350 additions and 114 deletions
+14 -13
View File
@@ -118,7 +118,7 @@ function buildUserPrompt({ text, fileName, tender }) {
}
// --- OpenRouter (OpenAI uyumlu) ---
async function callOpenRouter(model, key, userPrompt, signal) {
async function callOpenRouter(model, key, userPrompt, signal, sistem = SYSTEM_PROMPT) {
const res = await fetch(OPENROUTER_URL, {
method: 'POST',
headers: {
@@ -131,7 +131,7 @@ async function callOpenRouter(model, key, userPrompt, signal) {
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'system', content: sistem },
{ role: 'user', content: userPrompt },
],
temperature: 0.2,
@@ -146,14 +146,14 @@ async function callOpenRouter(model, key, userPrompt, signal) {
return data?.choices?.[0]?.message?.content || '';
}
async function callOpenAiStyle(p, key, model, userPrompt, signal) {
async function callOpenAiStyle(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT) {
const res = await fetch(p.url, {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'system', content: sistem },
{ role: 'user', content: userPrompt },
],
temperature: 0.2,
@@ -166,13 +166,13 @@ async function callOpenAiStyle(p, key, model, userPrompt, signal) {
return data?.choices?.[0]?.message?.content || '';
}
async function callGemini(p, key, model, userPrompt, signal) {
async function callGemini(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
systemInstruction: { parts: [{ text: sistem }] },
contents: [{ role: 'user', parts: [{ text: userPrompt }] }],
generationConfig: { temperature: 0.2, maxOutputTokens: 4000, responseMimeType: 'application/json' },
}),
@@ -183,14 +183,14 @@ async function callGemini(p, key, model, userPrompt, signal) {
return data?.candidates?.[0]?.content?.parts?.map((x) => x.text).join('') || '';
}
async function callAnthropic(p, key, model, userPrompt, signal) {
async function callAnthropic(p, key, model, userPrompt, signal, sistem = SYSTEM_PROMPT) {
const res = await fetch(p.url, {
method: 'POST',
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
max_tokens: 4000,
system: SYSTEM_PROMPT,
system: sistem,
messages: [{ role: 'user', content: userPrompt }],
}),
signal,
@@ -212,10 +212,11 @@ function extractJson(raw) {
}
/** Zinciri sırayla dener; ilk başarılı sonucu döndürür. */
export async function analyzeSpec({ text, fileName, tender, chain }) {
export async function analyzeSpec({ text, fileName, tender, chain, systemOverride }) {
const order = (Array.isArray(chain) && chain.length ? chain : ['glmflash', 'gemini', 'deepseek', 'grok', 'gpt', 'claude'])
.filter((id) => PROVIDERS[id]);
const userPrompt = buildUserPrompt({ text, fileName, tender });
const sistem = systemOverride || SYSTEM_PROMPT;
const attempts = [];
for (const id of order) {
@@ -230,13 +231,13 @@ export async function analyzeSpec({ text, fileName, tender, chain }) {
try {
let raw;
if (route.yol === 'openrouter') {
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal);
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal, sistem);
} else if (p.style === 'gemini') {
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal);
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal, sistem);
} else if (p.style === 'anthropic') {
raw = await callAnthropic(p, route.key, route.model, userPrompt, ac.signal);
raw = await callAnthropic(p, route.key, route.model, userPrompt, ac.signal, sistem);
} else {
raw = await callOpenAiStyle(p, route.key, route.model, userPrompt, ac.signal);
raw = await callOpenAiStyle(p, route.key, route.model, userPrompt, ac.signal, sistem);
}
const result = extractJson(raw);
attempts.push({ id, ok: true, yol: route.yol });