OpenRouter anahtarı iptal olup bakiye eksiye düşünce şartname analizi, TED çevirisi ve site taraması durmuştu. Artık ZAI_API_KEY ile GLM 5.3 ve GLM 5.3 Flash doğrudan Z.ai Coding Plan üzerinden çağrılır; kalan modeller OpenRouter anahtarı varsa yedek olarak denenir. - server/zai.mjs: doğrudan istemci; glm-5.x için reasoning_effort:low, glm-4.x için thinking kapalı; süreç genelinde en fazla 2 eşzamanlı istek (kullanıcı istekleri arka plan çevirisinden öncelikli); geçici 429'da aynı modelle yeniden deneme, 1113 bakiye hatasında hemen sıradaki model. - ai.mjs: 'zai' (GLM 5.3) sağlayıcısı eklendi, Z.ai yolu yeni istemciyi kullanıyor; varsayılan sıra zai → glmflash → gemini → … - ted-ceviri.mjs: önce ZAI_TRANSLATE_MODELS (glm-5.3-flash, glm-5.3), sonra OpenRouter; 2'li eşzamanlı parça işleme. - Admin model kataloğuna GLM 5.3 eklendi, kayıtlı zincirler sürüm 4'e taşınır. - tests/zai.test.mjs: düşünme ayarı, 429/1113 davranışı, eşzamanlılık, analyzeSpec ve TED çevirisinin Z.ai'ye gittiği. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
136 lines
5.0 KiB
JavaScript
136 lines
5.0 KiB
JavaScript
/**
|
||
* Gerçek firma web sitesi tarama + AI ile yetkinlik çıkarımı.
|
||
*
|
||
* Eski webScraperService.js sabit bir anahtar-kelime sözlüğüydü (domainde
|
||
* "medikal" geçince tıbbi ürün listesi uyduruyordu). Bu modül siteyi
|
||
* GERÇEKTEN çeker, metnini çıkarır ve AI ile ne tedarik edebildiğini belirler.
|
||
*
|
||
* Bağımlılık yok (Node 18+ fetch).
|
||
*/
|
||
|
||
import { analyzeSpec, aiConfigured } from './ai.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 MAX_BYTE = 1.5 * 1024 * 1024;
|
||
|
||
const ENT = { ' ': ' ', '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" };
|
||
|
||
function htmlToText(html) {
|
||
return String(html || '')
|
||
.replace(/<\s*(script|style|noscript|svg)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ' ')
|
||
.replace(/<\s*\/\s*(p|div|li|h[1-6]|tr|section)\s*>/gi, '\n')
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/<[^>]+>/g, ' ')
|
||
.replace(/&[a-z#0-9]+;/gi, (m) => ENT[m.toLowerCase()] ?? ' ')
|
||
.replace(/[ \t]+/g, ' ')
|
||
.replace(/\n{3,}/g, '\n\n')
|
||
.trim();
|
||
}
|
||
|
||
function normalizeUrl(input) {
|
||
let u = String(input || '').trim();
|
||
if (!u) throw new Error('Adres gerekli');
|
||
if (!/^https?:\/\//i.test(u)) u = 'https://' + u;
|
||
const url = new URL(u);
|
||
if (!/^https?:$/.test(url.protocol)) throw new Error('Yalnızca http/https desteklenir');
|
||
return url;
|
||
}
|
||
|
||
async function fetchPage(url, timeout = 20000) {
|
||
const res = await fetch(url, {
|
||
headers: { 'User-Agent': UA, 'Accept-Language': 'tr,en;q=0.8', Accept: 'text/html,application/xhtml+xml' },
|
||
redirect: 'follow',
|
||
signal: AbortSignal.timeout(timeout),
|
||
});
|
||
if (!res.ok) throw new Error(`Sayfa alınamadı (HTTP ${res.status})`);
|
||
const tip = res.headers.get('content-type') || '';
|
||
if (!/text\/html|application\/xhtml/i.test(tip)) throw new Error('Adres bir web sayfası değil');
|
||
const buf = await res.arrayBuffer();
|
||
if (buf.byteLength > MAX_BYTE) throw new Error('Sayfa çok büyük');
|
||
return new TextDecoder('utf-8').decode(buf);
|
||
}
|
||
|
||
// Ana sayfadan iç sayfa adaylarını çıkar (hakkımızda, ürünler, hizmetler…)
|
||
const IC_SAYFA = /(urun|ürün|hizmet|product|service|hakk|about|katalog|cozum|çözüm|referans|belge|sertifika)/i;
|
||
|
||
function icLinkler(html, base) {
|
||
const out = new Set();
|
||
const re = /<a[^>]+href=["']([^"'#]+)["']/gi;
|
||
let m;
|
||
while ((m = re.exec(html)) && out.size < 40) {
|
||
try {
|
||
const u = new URL(m[1], base);
|
||
if (u.hostname !== new URL(base).hostname) continue;
|
||
if (!IC_SAYFA.test(u.pathname)) continue;
|
||
u.hash = '';
|
||
out.add(u.href);
|
||
} catch { /* geçersiz link */ }
|
||
}
|
||
return [...out].slice(0, 4);
|
||
}
|
||
|
||
const SISTEM = `Sen Türkiye kamu ihalelerine katılan firmaların yetkinliklerini belirleyen bir analistsin.
|
||
Sana bir firmanın web sitesinden çıkarılmış metin verilecek.
|
||
YALNIZCA geçerli JSON döndür, başka metin yazma. Şema:
|
||
{
|
||
"firmaAdi": "sitede geçen firma adı veya null",
|
||
"ozet": "firmanın ne yaptığına dair 1-2 cümle",
|
||
"sektorler": ["firmanın faaliyet gösterdiği sektörler"],
|
||
"urunler": ["tedarik edebildiği somut ürün/hizmetler"],
|
||
"sertifikalar": ["sitede belirtilen belge/sertifikalar"],
|
||
"ihaleKonulari": ["bu firmanın teklif verebileceği ihale konu başlıkları — kısa, aranabilir terimler"]
|
||
}
|
||
KURALLAR:
|
||
- Yalnızca metinde GERÇEKTEN geçen bilgileri yaz. Uydurma.
|
||
- Metinde yoksa boş dizi veya null bırak.
|
||
- ihaleKonulari en fazla 12 madde, her biri 1-4 kelime, ihale ilanlarında aranabilecek terimler olsun
|
||
(ör. "tıbbi sarf malzeme", "iş güvenliği ekipmanı", "temizlik kimyasalı").`;
|
||
|
||
/**
|
||
* Siteyi çeker ve AI ile yetkinlik çıkarır.
|
||
* @returns {{url, tarananSayfa, karakter, sonuc}}
|
||
*/
|
||
export async function scanSite(inputUrl) {
|
||
if (!aiConfigured()) {
|
||
throw new Error('Site analizi için AI yapılandırılmamış (ZAI_API_KEY veya OPENROUTER_API_KEY gerekli).');
|
||
}
|
||
const url = normalizeUrl(inputUrl);
|
||
const ana = await fetchPage(url.href);
|
||
const sayfalar = [{ url: url.href, metin: htmlToText(ana) }];
|
||
|
||
// Birkaç iç sayfayı da tara (ürün/hizmet sayfaları en bilgilendirici)
|
||
for (const link of icLinkler(ana, url.href)) {
|
||
try {
|
||
const html = await fetchPage(link, 15000);
|
||
const metin = htmlToText(html);
|
||
if (metin.length > 200) sayfalar.push({ url: link, metin });
|
||
} catch { /* iç sayfa atlanabilir */ }
|
||
}
|
||
|
||
const birlesik = sayfalar
|
||
.map((s) => `--- ${s.url} ---\n${s.metin.slice(0, 12000)}`)
|
||
.join('\n\n')
|
||
.slice(0, 45000);
|
||
|
||
if (birlesik.replace(/\s/g, '').length < 200) {
|
||
throw new Error('Siteden yeterli metin çıkarılamadı (JavaScript ile yüklenen bir site olabilir).');
|
||
}
|
||
|
||
const out = await analyzeSpec({
|
||
text: birlesik,
|
||
fileName: url.hostname,
|
||
tender: null,
|
||
chain: null,
|
||
systemOverride: SISTEM,
|
||
});
|
||
|
||
return {
|
||
url: url.href,
|
||
tarananSayfa: sayfalar.length,
|
||
sayfalar: sayfalar.map((s) => s.url),
|
||
karakter: birlesik.length,
|
||
model: out.model,
|
||
sonuc: out.result,
|
||
};
|
||
}
|