Files
hikmettanandClaude Fable 5.1 7fa71f1a4e
Quality gates / test-build (push) Canceled after 0s
Quality gates / container (push) Canceled after 0s
feat(ai): Z.ai GLM birincil sağlayıcı; OpenRouter yalnız yedek
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>
2026-09-10 23:11:41 +03:00

136 lines
5.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = { '&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" };
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,
};
}