fix(danisman): L1-L3, L5, L6 (9) — Türkçe-duyarsız il/bölüm/üniversite eşleşmesi, kısa girdi hatası
- ilAdiNormalize harf katlayarak eşler: IZMIR / izmir / İzmir → İZMİR (program_ara,
il_ozeti, profil evIl). il_ozeti "Afyon"u da bulur.
- program_ara bölüm ve üniversite aramasını katlanmış adla yapar ("isletme",
"bogazici", "ODTÜ"); LIKE jokerleri (%/_) artık her şeyi eşlemez.
- Tek harf / boş girdi ilk kaydı (Abaza Dili, Abdullah Gül) döndürmez; "en az 3
harf" hatası döner.
- Kısaltma tablosunda bozuk satır: dtu → deu (Dokuz Eylül).
- Akış hata logu tam hata nesnesi yerine mesajı basar; cagri.ts kullanılmayan import.
- (8'in devamı) dil seçenekleri veride olan dillerle eşitlendi; doğrulama
"Almanca" süzgecini reddetmesin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -90,7 +90,7 @@ export const ARACLAR: AracTanimi[] = [
|
||||
universite: { type: "string", description: "Üniversite adı parçası" },
|
||||
il: { type: "string", description: "İl adı, ör. 'İzmir'" },
|
||||
tur: { type: "string", enum: ["devlet", "vakif"], description: "Yalnız devlet ya da yalnız vakıf üniversiteleri" },
|
||||
dil: { type: "string", enum: ["Türkçe", "İngilizce"], description: "Öğretim dili" },
|
||||
dil: { type: "string", enum: ["Türkçe", "İngilizce", "Almanca", "Fransızca", "Arapça", "Rusça"], description: "Öğretim dili" },
|
||||
ogretim_turu: {
|
||||
type: "string",
|
||||
enum: ["Örgün Öğretim", "Uzaktan Öğretim", "Açık Öğretim"],
|
||||
@@ -318,23 +318,38 @@ function programOzeti(p: Program, sira: number, o?: ProgramOzellik) {
|
||||
};
|
||||
}
|
||||
|
||||
/** SQLite LIKE Türkçe büyük/küçük harfi bilmez: hem yazıldığı gibi hem başlık düzeni denenir. */
|
||||
function likeVaryantlari(q: string): string[] {
|
||||
const t = q.trim();
|
||||
const baslik = t
|
||||
.toLocaleLowerCase("tr-TR")
|
||||
.split(" ")
|
||||
.map((w) => (w ? w[0].toLocaleUpperCase("tr-TR") + w.slice(1) : w))
|
||||
.join(" ");
|
||||
return Array.from(new Set([t, baslik, t.toLocaleUpperCase("tr-TR")]));
|
||||
/** Arama metninin katlanmış hâli en az bu kadar harf olmalı; daha kısası her kayıtla eşleşir. */
|
||||
const ARAMA_ALT_HARF = 3;
|
||||
|
||||
/** Yaygın üniversite kısaltmaları → adın katlanmış (slug) parçası. */
|
||||
const UNI_KISALTMA: Record<string, string> = {
|
||||
odtu: "orta-dogu-teknik",
|
||||
itu: "istanbul-teknik",
|
||||
ytu: "yildiz-teknik",
|
||||
ktu: "karadeniz-teknik",
|
||||
deu: "dokuz-eylul",
|
||||
};
|
||||
|
||||
const ayrikDegerler = new Map<string, { ham: string; katli: string }[]>();
|
||||
|
||||
/**
|
||||
* Kolonun, katlanmış hâli `katliSorgu`yu içeren değerleri. SQLite LIKE Türkçe
|
||||
* harf katlamayı bilmez ("isletme" İşletme'yi, "bogazici" Boğaziçi'ni bulmaz);
|
||||
* ayrık değerler (birkaç bin) bir kez okunur, eşleşme JS'te yapılır.
|
||||
*/
|
||||
function katliEslesenler(kolon: "isim" | "universite", katliSorgu: string): string[] {
|
||||
let liste = ayrikDegerler.get(kolon);
|
||||
if (!liste) {
|
||||
liste = (getDb().prepare(`SELECT DISTINCT ${kolon} AS v FROM programs`).all() as { v: string | null }[])
|
||||
.filter((x): x is { v: string } => x.v != null)
|
||||
.map((x) => ({ ham: x.v, katli: turkishSlugify(x.v) }));
|
||||
ayrikDegerler.set(kolon, liste);
|
||||
}
|
||||
return liste.filter((x) => x.katli.includes(katliSorgu)).map((x) => x.ham);
|
||||
}
|
||||
|
||||
function likeKosulu(kolon: string, q: string): { sql: string; args: string[] } {
|
||||
const v = likeVaryantlari(q);
|
||||
return {
|
||||
sql: `(${v.map(() => `${kolon} LIKE ?`).join(" OR ")})`,
|
||||
args: v.map((x) => `%${x}%`),
|
||||
};
|
||||
function inKosulu(kolon: string, degerler: string[]): string {
|
||||
return degerler.length ? `${kolon} IN (${degerler.map(() => "?").join(",")})` : "0 = 1";
|
||||
}
|
||||
|
||||
// ── Araç gövdeleri ───────────────────────────────────────────────────────
|
||||
@@ -363,24 +378,28 @@ function programAra(b: AracBaglami, a: ProgramAraArg, profilUygula = true): Prog
|
||||
const args: (string | number)[] = [PUAN_TURLERI[b.tur], b.tur === "tyt" ? 1 : 0];
|
||||
const uygulanan: string[] = [];
|
||||
|
||||
const bos = (not: string): ProgramAraSonuc => ({ toplamEslesen: 0, gosterilen: 0, uygulananFiltreler: [], sonuclar: [], not });
|
||||
if (a.arama) {
|
||||
const k = likeKosulu("isim", a.arama);
|
||||
kosullar.push(k.sql);
|
||||
args.push(...k.args);
|
||||
const katli = turkishSlugify(a.arama);
|
||||
if (katli.length < ARAMA_ALT_HARF) return bos(`Bölüm araması için en az ${ARAMA_ALT_HARF} harf yaz: ${a.arama}`);
|
||||
const adlar = katliEslesenler("isim", katli);
|
||||
kosullar.push(inKosulu("isim", adlar));
|
||||
args.push(...adlar);
|
||||
uygulanan.push(`bölüm ~ "${a.arama}"`);
|
||||
}
|
||||
if (a.universite) {
|
||||
const k = likeKosulu("universite", a.universite);
|
||||
kosullar.push(k.sql);
|
||||
args.push(...k.args);
|
||||
const slug = turkishSlugify(a.universite);
|
||||
const katli = UNI_KISALTMA[slug] ?? slug;
|
||||
if (katli.length < ARAMA_ALT_HARF) return bos(`Üniversite araması için en az ${ARAMA_ALT_HARF} harf yaz: ${a.universite}`);
|
||||
const adlar = katliEslesenler("universite", katli);
|
||||
kosullar.push(inKosulu("universite", adlar));
|
||||
args.push(...adlar);
|
||||
uygulanan.push(`üniversite ~ "${a.universite}"`);
|
||||
}
|
||||
// İl: parametre > profildeki km sınırı
|
||||
if (a.il) {
|
||||
const il = ilAdiNormalize(a.il);
|
||||
if (!il) {
|
||||
return { toplamEslesen: 0, gosterilen: 0, uygulananFiltreler: [], sonuclar: [], not: `İl tanınmadı: ${a.il}` };
|
||||
}
|
||||
if (!il) return bos(`İl tanınmadı: ${a.il}`);
|
||||
kosullar.push("il = ?");
|
||||
args.push(il);
|
||||
uygulanan.push(`il = ${il}`);
|
||||
@@ -594,6 +613,7 @@ function bolumIstihdam(b: AracBaglami, a: { bolumler?: unknown }) {
|
||||
ulkeOrtalamasi: ISTIHDAM_ORTALAMA[duzey],
|
||||
bolumler: adlar.map((ad) => {
|
||||
const slug = bolumSayfaSlug(ad);
|
||||
if (slug.length < ARAMA_ALT_HARF) return { aranan: ad, hata: `Bölüm adı çok kısa; en az ${ARAMA_ALT_HARF} harf yaz.` };
|
||||
// Birebir > başı tutan > içinde geçen ("bilgisayar müh" → bilgisayar-muhendisligi)
|
||||
const anahtar =
|
||||
(slug in tablo ? slug : undefined) ??
|
||||
@@ -619,6 +639,8 @@ function bolumOzeti(_b: AracBaglami, a: { bolum?: string }) {
|
||||
const q = String(a.bolum ?? "").trim();
|
||||
if (!q) return { hata: "Bölüm adı gerekli." };
|
||||
const slug = bolumSayfaSlug(q);
|
||||
// Boş/tek harfli slug her bölümle "başlıyor" sayılır ve ilk kayıt (Abaza Dili) dönerdi
|
||||
if (slug.length < ARAMA_ALT_HARF) return { hata: `Bölüm adı çok kısa; en az ${ARAMA_ALT_HARF} harf yaz: ${q}` };
|
||||
const hepsi = getAllBolumler();
|
||||
const aday =
|
||||
hepsi.find((x) => x.slug === slug) ??
|
||||
@@ -645,7 +667,8 @@ const IL_UNI_UST = 30;
|
||||
function ilOzeti(_b: AracBaglami, a: { il?: string }) {
|
||||
const q = String(a.il ?? "").trim();
|
||||
if (!q) return { hata: "İl adı gerekli." };
|
||||
const il = getIlBySlug(turkishSlugify(q));
|
||||
// "Afyon" → AFYONKARAHİSAR: program_ara ile aynı il eşleşmesi
|
||||
const il = getIlBySlug(turkishSlugify(ilAdiNormalize(q) ?? q));
|
||||
if (!il) return { hata: `İl bulunamadı: ${q}` };
|
||||
return {
|
||||
il: il.ad,
|
||||
@@ -724,14 +747,8 @@ function universiteProfili(_b: AracBaglami, a: { universite?: string }) {
|
||||
if (!q) return { hata: "Üniversite adı gerekli." };
|
||||
const slugQ = turkishSlugify(q);
|
||||
const hepsi = getAllUniversiteler();
|
||||
const kisaltma: Record<string, string> = {
|
||||
odtu: "orta-dogu-teknik",
|
||||
itu: "istanbul-teknik",
|
||||
ytu: "yildiz-teknik",
|
||||
ktu: "karadeniz-teknik",
|
||||
dtu: "deu",
|
||||
};
|
||||
const arananSlug = kisaltma[slugQ] ?? slugQ;
|
||||
const arananSlug = UNI_KISALTMA[slugQ] ?? slugQ;
|
||||
if (arananSlug.length < ARAMA_ALT_HARF) return { hata: `Üniversite adı çok kısa; en az ${ARAMA_ALT_HARF} harf yaz: ${q}` };
|
||||
const aday =
|
||||
hepsi.find((u) => u.slug === arananSlug || u.slug === `${arananSlug}-universitesi`) ??
|
||||
hepsi.find((u) => u.slug.startsWith(arananSlug)) ??
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type OpenAI from "openai";
|
||||
import { z } from "zod";
|
||||
import { getOpenRouter, raporModeli } from "./client";
|
||||
|
||||
|
||||
@@ -117,7 +117,8 @@ export function danismanAkisi(opts: {
|
||||
writer.write({ type: "finish" });
|
||||
} catch (err) {
|
||||
if (!tamamlandi) await opts.onYarim(metin).catch(() => {});
|
||||
console.error("[soru] stream hatası:", err);
|
||||
// Tam hata nesnesi istek gövdesini ve cevap başlıklarını taşır; yalnız mesaj
|
||||
console.error("[soru] stream hatası:", err instanceof Error ? err.message : String(err));
|
||||
// Model hatası akışa zaten "error" parçası olarak yazıldı; yazılmadıysa
|
||||
// (boş cevap, kayıt hatası) istemci hatayı buradan görür.
|
||||
if (akisHatasi == null) throw err;
|
||||
|
||||
Reference in New Issue
Block a user