v2 #10
@@ -7,16 +7,24 @@
|
|||||||
* araç çağrılarını aynen döner. Gerçek danismanAkisi + gerçek araçlar koşar
|
* araç çağrılarını aynen döner. Gerçek danismanAkisi + gerçek araçlar koşar
|
||||||
* (profil_guncelle data/app.db'nin geçici kopyasına yazar). Doğrulananlar:
|
* (profil_guncelle data/app.db'nin geçici kopyasına yazar). Doğrulananlar:
|
||||||
* vaat regex'i notu (Jev anahtarı yokken), "kaydettim" deyip kaydetmeme notu,
|
* vaat regex'i notu (Jev anahtarı yokken), "kaydettim" deyip kaydetmeme notu,
|
||||||
* araç çağrısı kotası. Model davranışını ÖLÇMEZ; onu eval:sohbet ölçer.
|
* araç çağrısı kotası, rehber bağlantısı doğrulama (uydurma slug akışta
|
||||||
|
* gerçek slug'a döner, kayıt da düzeltilmiş metni alır). Model davranışını
|
||||||
|
* ÖLÇMEZ; onu eval:sohbet ölçer.
|
||||||
* Beklenti tutmazsa çıkış kodu 1.
|
* Beklenti tutmazsa çıkış kodu 1.
|
||||||
*/
|
*/
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { copyFileSync } from "node:fs";
|
import { copyFileSync } from "node:fs";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
|
|
||||||
type Beklenen = { vaatNotu: boolean; kayitNotu: boolean; reddedilenArac: number };
|
type Beklenen = { vaatNotu: boolean; kayitNotu: boolean; reddedilenArac: number; baglantiDuzeltme: number };
|
||||||
type Senaryo = { ad: string; beklenen: Beklenen; adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[] };
|
type Senaryo = {
|
||||||
const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac });
|
ad: string;
|
||||||
|
beklenen: Beklenen;
|
||||||
|
adimlar: ({ metin: string } | { araclar: { name: string; arguments: string }[] })[];
|
||||||
|
/** Kaydedilen metinde olması / olmaması gerekenler (rehber bağlantısı senaryoları). */
|
||||||
|
metinde?: { var?: string[]; yok?: string[] };
|
||||||
|
};
|
||||||
|
const B = (vaatNotu: boolean, kayitNotu: boolean, reddedilenArac = 0, baglantiDuzeltme = 0): Beklenen => ({ vaatNotu, kayitNotu, reddedilenArac, baglantiDuzeltme });
|
||||||
const SENARYOLAR: Senaryo[] = [
|
const SENARYOLAR: Senaryo[] = [
|
||||||
{ ad: "kaydettim-aracsiz", beklenen: B(false, true), adimlar: [{ metin: "Kaydettim: Vakıfta yıllık ücret üst sınırı 400.000 TL. Aramalar buna göre süzülecek." }] },
|
{ ad: "kaydettim-aracsiz", beklenen: B(false, true), adimlar: [{ metin: "Kaydettim: Vakıfta yıllık ücret üst sınırı 400.000 TL. Aramalar buna göre süzülecek." }] },
|
||||||
{ ad: "kaydettim-aracli", beklenen: B(false, false), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "ucretUst", deger: 400000 }) }] }, { metin: "Kaydettim: yıllık ücret en fazla 400.000 TL." }] },
|
{ ad: "kaydettim-aracli", beklenen: B(false, false), adimlar: [{ araclar: [{ name: "profil_guncelle", arguments: JSON.stringify({ alan: "ucretUst", deger: 400000 }) }] }, { metin: "Kaydettim: yıllık ücret en fazla 400.000 TL." }] },
|
||||||
@@ -25,6 +33,19 @@ const SENARYOLAR: Senaryo[] = [
|
|||||||
{ ad: "vaat", beklenen: B(true, false), adimlar: [{ metin: "Bu programa yerleşme olasılığın düşük ama yine de yazabilirsin." }] },
|
{ ad: "vaat", beklenen: B(true, false), adimlar: [{ metin: "Bu programa yerleşme olasılığın düşük ama yine de yazabilirsin." }] },
|
||||||
{ ad: "temiz", beklenen: B(false, false), adimlar: [{ metin: "Bu program son yerleştirmede 178.278. sırada kapandı; etiketi Riskli. Garanti verilemez." }] },
|
{ ad: "temiz", beklenen: B(false, false), adimlar: [{ metin: "Bu program son yerleştirmede 178.278. sırada kapandı; etiketi Riskli. Garanti verilemez." }] },
|
||||||
{ ad: "kota", beklenen: B(false, false, 2), adimlar: [{ araclar: Array.from({ length: 6 }, (_, i) => ({ name: "il_ozeti", arguments: JSON.stringify({ il: ["İzmir", "Ankara", "Bursa", "Konya", "Adana", "Samsun"][i] }) })) }, { metin: "İllere baktım." }] },
|
{ ad: "kota", beklenen: B(false, false, 2), adimlar: [{ araclar: Array.from({ length: 6 }, (_, i) => ({ name: "il_ozeti", arguments: JSON.stringify({ il: ["İzmir", "Ankara", "Bursa", "Konya", "Adana", "Samsun"][i] }) })) }, { metin: "İllere baktım." }] },
|
||||||
|
// Model araç çağırmadan ezberden iki yanlış rehber adresi yazar; ikisi de akışta gerçek slug'a döner
|
||||||
|
{
|
||||||
|
ad: "rehber-uydurma",
|
||||||
|
beklenen: B(false, false, 0, 2),
|
||||||
|
adimlar: [{ metin: "Şu yazılara bak: [OBP](/rehber/obp-nedir) ve [Yatay geçiş](/rehber/yatay-gecis-rehberi). İkisi de sana göre." }],
|
||||||
|
metinde: { var: ["/rehber/obp-nedir-nasil-hesaplanir", "/rehber/yatay-gecis-cap-yandal"], yok: ["/rehber/obp-nedir)", "/rehber/yatay-gecis-rehberi"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "rehber-gercek",
|
||||||
|
beklenen: B(false, false, 0, 0),
|
||||||
|
adimlar: [{ metin: "Bak: [Devlet mi vakıf mı](/rehber/devlet-mi-vakif-mi) yazısı bunu anlatıyor." }],
|
||||||
|
metinde: { var: ["/rehber/devlet-mi-vakif-mi"] },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let aktif: Senaryo = SENARYOLAR[0];
|
let aktif: Senaryo = SENARYOLAR[0];
|
||||||
let adimNo = 0;
|
let adimNo = 0;
|
||||||
@@ -61,6 +82,9 @@ async function main() {
|
|||||||
const Database = (await import("better-sqlite3")).default;
|
const Database = (await import("better-sqlite3")).default;
|
||||||
const sqlite = new Database(db);
|
const sqlite = new Database(db);
|
||||||
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
|
const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi");
|
||||||
|
// Gerçek content/rehber listesi: bağlantı doğrulama tamponu aktif
|
||||||
|
const { getRehberAramaKayitlari } = await import("@/lib/rehber");
|
||||||
|
const rehberler = getRehberAramaKayitlari();
|
||||||
const info = console.info;
|
const info = console.info;
|
||||||
console.info = () => {};
|
console.info = () => {};
|
||||||
let hata = 0;
|
let hata = 0;
|
||||||
@@ -76,6 +100,7 @@ async function main() {
|
|||||||
system: "test",
|
system: "test",
|
||||||
mesajlar: [{ role: "user", content: "test" }],
|
mesajlar: [{ role: "user", content: "test" }],
|
||||||
aracBaglami: { userId, sira: 100000, tur: "say", profil: {} },
|
aracBaglami: { userId, sira: 100000, tur: "say", profil: {} },
|
||||||
|
rehberler,
|
||||||
onTamam: async (m) => {
|
onTamam: async (m) => {
|
||||||
tam = m;
|
tam = m;
|
||||||
},
|
},
|
||||||
@@ -91,7 +116,9 @@ async function main() {
|
|||||||
if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70));
|
if (v.type === "tool-output-available") araclar.push(JSON.stringify(v.output).slice(0, 70));
|
||||||
}
|
}
|
||||||
const d = denetim as Beklenen | null;
|
const d = denetim as Beklenen | null;
|
||||||
const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac;
|
const kayit = String(tam);
|
||||||
|
const metinTuttu = (s.metinde?.var ?? []).every((m) => kayit.includes(m)) && (s.metinde?.yok ?? []).every((m) => !kayit.includes(m));
|
||||||
|
const tuttu = d != null && d.vaatNotu === s.beklenen.vaatNotu && d.kayitNotu === s.beklenen.kayitNotu && d.reddedilenArac === s.beklenen.reddedilenArac && d.baglantiDuzeltme === s.beklenen.baglantiDuzeltme && metinTuttu;
|
||||||
if (!tuttu) hata++;
|
if (!tuttu) hata++;
|
||||||
info(`\n${tuttu ? "geçti" : "KALDI"} [${s.ad}]\n denetim: ${JSON.stringify(denetim)}\n araç çıktıları: ${araclar.length} → ${araclar.join(" | ")}\n son metin: ${String(tam).replace(/\n+/g, " ⏎ ")}`);
|
info(`\n${tuttu ? "geçti" : "KALDI"} [${s.ad}]\n denetim: ${JSON.stringify(denetim)}\n araç çıktıları: ${araclar.length} → ${araclar.join(" | ")}\n son metin: ${String(tam).replace(/\n+/g, " ⏎ ")}`);
|
||||||
}
|
}
|
||||||
|
|||||||
127
scripts/eval/baglanti-denetim.ts
Normal file
127
scripts/eval/baglanti-denetim.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* Rehber bağlantısı doğrulamasının MEKANİK testi — parasız, anahtarsız, DB'siz.
|
||||||
|
*
|
||||||
|
* pnpm tsx --conditions=react-server scripts/eval/baglanti-denetim.ts
|
||||||
|
*
|
||||||
|
* lib/ai/baglanti-denetimi.ts gerçek content/rehber listesiyle sınanır:
|
||||||
|
* kayıtlı slug dokunulmaz, yakın slug yeniden yazılır (başlık kayıttan),
|
||||||
|
* uydurma slug düz metne iner; akış tamponunun parça parça çıktısı tek
|
||||||
|
* seferlik sonuçla birebir aynı olmalı. Beklenti tutmazsa çıkış kodu 1.
|
||||||
|
*/
|
||||||
|
import { BaglantiTamponu, rehberBaglantilariniDuzelt, type RehberKaydi } from "@/lib/ai/baglanti-denetimi";
|
||||||
|
import { getRehberAramaKayitlari } from "@/lib/rehber";
|
||||||
|
|
||||||
|
type Senaryo = {
|
||||||
|
ad: string;
|
||||||
|
metin: string;
|
||||||
|
/** Beklenen tam çıktı (string) ya da çıktıyı denetleyen fonksiyon. */
|
||||||
|
bekle: string | ((cikti: string) => boolean);
|
||||||
|
duzeltme: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function senaryolar(rehberler: readonly RehberKaydi[]): Senaryo[] {
|
||||||
|
const kayit = (slug: string) => {
|
||||||
|
const r = rehberler.find((x) => x.slug === slug);
|
||||||
|
if (!r) throw new Error(`content/rehber içinde yok: ${slug}`);
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
const yatay = kayit("yatay-gecis-cap-yandal");
|
||||||
|
const obp = kayit("obp-nedir-nasil-hesaplanir");
|
||||||
|
const gercek = kayit("devlet-mi-vakif-mi");
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
ad: "gercek-slug-aynen",
|
||||||
|
metin: `Şu yazıya bak: [${gercek.baslik}](/rehber/${gercek.slug}) — orada ayrıntı var.`,
|
||||||
|
bekle: `Şu yazıya bak: [${gercek.baslik}](/rehber/${gercek.slug}) — orada ayrıntı var.`,
|
||||||
|
duzeltme: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "yakin-eslesme-markdown",
|
||||||
|
metin: "Detay için [Yatay geçiş](/rehber/yatay-gecis-rehberi) yazısına bak.",
|
||||||
|
bekle: `Detay için [${yatay.baslik}](/rehber/${yatay.slug}) yazısına bak.`,
|
||||||
|
duzeltme: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "uydurma-duz-metin",
|
||||||
|
metin: "Bunu [Tamamen uydurma yazı](/rehber/tamamen-uydurma-seyler) anlatıyor.",
|
||||||
|
bekle: "Bunu Tamamen uydurma yazı anlatıyor.",
|
||||||
|
duzeltme: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "ters-tirnak-ve-ciplak",
|
||||||
|
metin: "Bkz. `/rehber/obp-nedir` ve ayrıca /rehber/yatay-gecis-rehberi. Uydurma: /rehber/tamamen-uydurma-seyler bitti.",
|
||||||
|
bekle: `Bkz. [${obp.baslik}](/rehber/${obp.slug}) ve ayrıca [${yatay.baslik}](/rehber/${yatay.slug}). Uydurma: bitti.`,
|
||||||
|
duzeltme: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "koseli-parantez-kayipsiz",
|
||||||
|
metin: "[2024] verisi ve [not] şöyle; `kod` de olduğu gibi kalır.",
|
||||||
|
bekle: "[2024] verisi ve [not] şöyle; `kod` de olduğu gibi kalır.",
|
||||||
|
duzeltme: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "baska-yol-dokunulmaz",
|
||||||
|
metin: "[Ankara Üniversitesi](/universite/ankara-universitesi) ve [Tıp](/bolum/tip) kalır.",
|
||||||
|
bekle: "[Ankara Üniversitesi](/universite/ankara-universitesi) ve [Tıp](/bolum/tip) kalır.",
|
||||||
|
duzeltme: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "site-koku-ile-markdown",
|
||||||
|
metin: "Oku: [OBP](https://kolaytercih.com/rehber/obp-nedir).",
|
||||||
|
bekle: `Oku: [${obp.baslik}](/rehber/${obp.slug}).`,
|
||||||
|
duzeltme: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Metni verilen parça sayısına rastgele olmayan, sabit noktalardan böler. */
|
||||||
|
function parcala(metin: string, adet: number): string[] {
|
||||||
|
const boy = Math.max(1, Math.ceil(metin.length / adet));
|
||||||
|
const parcalar: string[] = [];
|
||||||
|
for (let i = 0; i < metin.length; i += boy) parcalar.push(metin.slice(i, i + boy));
|
||||||
|
return parcalar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const rehberler = getRehberAramaKayitlari();
|
||||||
|
if (rehberler.length < 10) throw new Error(`rehber listesi şüpheli derecede kısa: ${rehberler.length}`);
|
||||||
|
let hata = 0;
|
||||||
|
const kontrol = (ad: string, tuttu: boolean, detay: string) => {
|
||||||
|
if (!tuttu) hata++;
|
||||||
|
console.log(`${tuttu ? "geçti" : "KALDI"} [${ad}]${tuttu ? "" : `\n ${detay}`}`);
|
||||||
|
};
|
||||||
|
for (const s of senaryolar(rehberler)) {
|
||||||
|
const tek = rehberBaglantilariniDuzelt(s.metin, rehberler);
|
||||||
|
const beklendi = typeof s.bekle === "string" ? tek.metin === s.bekle : s.bekle(tek.metin);
|
||||||
|
kontrol(s.ad, beklendi && tek.duzeltmeler.length === s.duzeltme, `çıktı: ${tek.metin}\n düzeltme: ${JSON.stringify(tek.duzeltmeler)}`);
|
||||||
|
// Tampon: metin 1, 3, 4, 7 ve karakter karakter parçalanınca da aynı sonuç
|
||||||
|
for (const adet of [1, 3, 4, 7, s.metin.length]) {
|
||||||
|
const tampon = new BaglantiTamponu(rehberler);
|
||||||
|
let akis = "";
|
||||||
|
for (const p of parcala(s.metin, adet)) akis += tampon.besle(p);
|
||||||
|
akis += tampon.bitir();
|
||||||
|
kontrol(
|
||||||
|
`${s.ad} · tampon ${adet} parça`,
|
||||||
|
akis === tek.metin && tampon.duzeltmeler.length === tek.duzeltmeler.length,
|
||||||
|
`tampon: ${akis}\n tek seferlik: ${tek.metin}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tampon bir bağlantı adayını kapanmadan bırakmamalı (yanlış hap ekrana gitmesin)
|
||||||
|
{
|
||||||
|
const tampon = new BaglantiTamponu(rehberler);
|
||||||
|
const ilk = tampon.besle("Bak: [Yatay geçiş](/rehber/yatay-gecis");
|
||||||
|
const son = tampon.besle("-rehberi) iyi.") + tampon.bitir();
|
||||||
|
kontrol("tampon-adayi-tutuyor", ilk === "Bak: " && !ilk.includes("/rehber/") && son.includes("/rehber/yatay-gecis-cap-yandal"), `ilk: ${ilk}\n son: ${son}`);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const tampon = new BaglantiTamponu(rehberler);
|
||||||
|
const ilk = tampon.besle("Ayrıntı /reh");
|
||||||
|
const son = tampon.besle("ber/obp-nedir yazısında.") + tampon.bitir();
|
||||||
|
kontrol("tampon-yarim-onek", ilk === "Ayrıntı " && son.includes("/rehber/obp-nedir-nasil-hesaplanir"), `ilk: ${ilk}\n son: ${son}`);
|
||||||
|
}
|
||||||
|
const toplam = senaryolar(rehberler).length * 6 + 2;
|
||||||
|
console.log(`\n${toplam - hata}/${toplam} senaryo geçti (mekanik test; gerçek model davranışını ölçmez)`);
|
||||||
|
process.exit(hata ? 1 : 0);
|
||||||
|
}
|
||||||
|
main();
|
||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
type RevizyonOzeti,
|
type RevizyonOzeti,
|
||||||
type SohbetMeta,
|
type SohbetMeta,
|
||||||
} from "@/lib/ai/sohbet-meta";
|
} from "@/lib/ai/sohbet-meta";
|
||||||
import { MAX_REVIZYON } from "@/lib/credits";
|
|
||||||
import { profilGetir } from "@/lib/danisman-profil-db";
|
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||||
|
import { getRehberAramaKayitlari } from "@/lib/rehber";
|
||||||
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
import { listeOzetiCikar } from "@/lib/ai/rapor";
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||||
import type { RaporParams } from "@/features/rapor/types/rapor";
|
import type { RaporParams } from "@/features/rapor/types/rapor";
|
||||||
@@ -196,7 +196,7 @@ export async function POST(request: NextRequest) {
|
|||||||
uyarilar: raporSonuc.uyarilar,
|
uyarilar: raporSonuc.uyarilar,
|
||||||
kapsam: raporSonuc.kapsam ? { toplam: raporSonuc.kapsam.toplam, uyarlandi: raporSonuc.kapsam.uyarlandi } : undefined,
|
kapsam: raporSonuc.kapsam ? { toplam: raporSonuc.kapsam.toplam, uyarlandi: raporSonuc.kapsam.uyarlandi } : undefined,
|
||||||
},
|
},
|
||||||
revizyon: { kalanHak: Math.max(0, MAX_REVIZYON - rapor.revisionCount), paketli },
|
revizyon: { paketli },
|
||||||
});
|
});
|
||||||
// Araçların bağlamı: aday sırası/puan türü (arama süzgeci), profil
|
// Araçların bağlamı: aday sırası/puan türü (arama süzgeci), profil
|
||||||
// (profil_guncelle yazınca aynı tur içinde tazelenir) ve sihirbaz seçimleri
|
// (profil_guncelle yazınca aynı tur içinde tazelenir) ve sihirbaz seçimleri
|
||||||
@@ -207,7 +207,6 @@ export async function POST(request: NextRequest) {
|
|||||||
tur: raporParams.tur,
|
tur: raporParams.tur,
|
||||||
hasPaket: paketli,
|
hasPaket: paketli,
|
||||||
mesajId: kullaniciMesajId,
|
mesajId: kullaniciMesajId,
|
||||||
kalanRevizyon: Math.max(0, MAX_REVIZYON - rapor.revisionCount),
|
|
||||||
profil,
|
profil,
|
||||||
oncelikler: raporParams.oncelikler ?? [],
|
oncelikler: raporParams.oncelikler ?? [],
|
||||||
ozelDurum: raporParams.ozelDurum ?? [],
|
ozelDurum: raporParams.ozelDurum ?? [],
|
||||||
@@ -297,6 +296,9 @@ export async function POST(request: NextRequest) {
|
|||||||
system,
|
system,
|
||||||
mesajlar,
|
mesajlar,
|
||||||
aracBaglami,
|
aracBaglami,
|
||||||
|
// Cevaptaki /rehber/<slug> adresleri gerçek yazı listesiyle doğrulanır
|
||||||
|
// (prod'da cache'li, lib/rehber.ts)
|
||||||
|
rehberler: getRehberAramaKayitlari(),
|
||||||
onAracSonucu: (ad, cikti) => {
|
onAracSonucu: (ad, cikti) => {
|
||||||
const c = cikti as Record<string, unknown>;
|
const c = cikti as Record<string, unknown>;
|
||||||
if (ad === "soru_sor" && c.anket) cevapMeta.anket = c.anket as SohbetMeta["anket"];
|
if (ad === "soru_sor" && c.anket) cevapMeta.anket = c.anket as SohbetMeta["anket"];
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
SectionEyebrow,
|
SectionEyebrow,
|
||||||
} from "@/components/pixel-decor";
|
} from "@/components/pixel-decor";
|
||||||
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
||||||
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
|
import { URUNLER } from "@/lib/credits";
|
||||||
import {
|
import {
|
||||||
PROGRAM_SAYISI_CUMLE,
|
PROGRAM_SAYISI_CUMLE,
|
||||||
PROGRAM_SAYISI_METNI,
|
PROGRAM_SAYISI_METNI,
|
||||||
@@ -244,7 +244,7 @@ const paketOzellikleri = [
|
|||||||
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
||||||
},
|
},
|
||||||
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
||||||
{ icon: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` },
|
{ icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" },
|
||||||
{
|
{
|
||||||
icon: MessageCircleQuestion,
|
icon: MessageCircleQuestion,
|
||||||
text: `${URUNLER.paket.credits} yapay zekâ danışman sorusu`,
|
text: `${URUNLER.paket.credits} yapay zekâ danışman sorusu`,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const OZELLIKLER = [
|
|||||||
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
||||||
},
|
},
|
||||||
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
||||||
{ icon: RefreshCcw, text: "2 liste revizyon hakkı" },
|
{ icon: RefreshCcw, text: "Sınırsız liste revizyonu (danışmanla sohbetten)" },
|
||||||
{
|
{
|
||||||
icon: MessageCircleQuestion,
|
icon: MessageCircleQuestion,
|
||||||
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,
|
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export function ListemGovde({
|
|||||||
danismanProfili,
|
danismanProfili,
|
||||||
ozet,
|
ozet,
|
||||||
ilkYorum,
|
ilkYorum,
|
||||||
kalanHak,
|
|
||||||
paketFiyati,
|
paketFiyati,
|
||||||
paketKredi,
|
paketKredi,
|
||||||
}: {
|
}: {
|
||||||
@@ -99,7 +98,6 @@ export function ListemGovde({
|
|||||||
ozet?: DanismanOzeti;
|
ozet?: DanismanOzeti;
|
||||||
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
/** Sohbet boşken açılışta gösterilen 1. tercih yorumu */
|
||||||
ilkYorum?: IlkTercihYorumu;
|
ilkYorum?: IlkTercihYorumu;
|
||||||
kalanHak: number;
|
|
||||||
paketFiyati: string;
|
paketFiyati: string;
|
||||||
paketKredi: number;
|
paketKredi: number;
|
||||||
}) {
|
}) {
|
||||||
@@ -216,12 +214,12 @@ export function ListemGovde({
|
|||||||
}
|
}
|
||||||
if (hasPaket) {
|
if (hasPaket) {
|
||||||
satirlar.push(
|
satirlar.push(
|
||||||
`Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar (kalan hak: ${kalanHak})`,
|
`Listende değişiklik isteyebilirsin: danışman gerekirse kısa bir formla sorar, sonra listeyi yeniden kurar`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
satirlar.push("Bu sohbetteki son mesajların");
|
satirlar.push("Bu sohbetteki son mesajların");
|
||||||
return satirlar;
|
return satirlar;
|
||||||
}, [adaySira, params, hasPaket, acikSatir, kalanHak]);
|
}, [adaySira, params, hasPaket, acikSatir]);
|
||||||
|
|
||||||
// Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez).
|
// Kapı ölçümü: paywall yüzeyleri viewport'a girince (oturumda 1'er kez).
|
||||||
// Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları
|
// Ref'ler yalnızca paketsiz dallarda bağlanır; paketlide DOM'da olmadıkları
|
||||||
@@ -331,8 +329,7 @@ export function ListemGovde({
|
|||||||
{/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman
|
{/* Revizyon sohbette yaşar (7 Eki 2026): düğme danışman
|
||||||
kutusunu odaklar, aday ne değişsin istediğini yazar;
|
kutusunu odaklar, aday ne değişsin istediğini yazar;
|
||||||
danışman gerekirse soru formuyla netleştirip listeyi
|
danışman gerekirse soru formuyla netleştirip listeyi
|
||||||
yeniden kurar. Hak bitince düğme kalkar. */}
|
yeniden kurar. Sayı sınırı yok (7 Eki 2026). */}
|
||||||
{kalanHak > 0 ? (
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -342,7 +339,6 @@ export function ListemGovde({
|
|||||||
<PencilLine className="size-4" aria-hidden />
|
<PencilLine className="size-4" aria-hidden />
|
||||||
Revize et
|
Revize et
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -447,7 +443,8 @@ export function ListemGovde({
|
|||||||
bildikleri={danismaninBildikleri}
|
bildikleri={danismaninBildikleri}
|
||||||
genis={sohbetGenis}
|
genis={sohbetGenis}
|
||||||
onGenisDegistir={sohbetGenisligiAyarla}
|
onGenisDegistir={sohbetGenisligiAyarla}
|
||||||
revizyonHakki={kalanHak}
|
// 0/1 bayrak; prop adı entegrasyonda revizyonAcik olur
|
||||||
|
revizyonHakki={hasPaket ? 1 : 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
import { kullanicininRaporu, sohbetGecmisiGetir } from "../rapor-queries";
|
||||||
import { profilGetir } from "@/lib/danisman-profil-db";
|
import { profilGetir } from "@/lib/danisman-profil-db";
|
||||||
import { profilSatirlari } from "@/lib/danisman-profil";
|
import { profilSatirlari } from "@/lib/danisman-profil";
|
||||||
import { URUNLER, MAX_REVIZYON, RAPOR_KREDI } from "@/lib/credits";
|
import { URUNLER, RAPOR_KREDI } from "@/lib/credits";
|
||||||
import type {
|
import type {
|
||||||
MaskeliRapor,
|
MaskeliRapor,
|
||||||
RaporParams,
|
RaporParams,
|
||||||
@@ -177,7 +177,6 @@ export async function ListemIcerik({
|
|||||||
danismanProfili={profilSatirlari(danismanProfili)}
|
danismanProfili={profilSatirlari(danismanProfili)}
|
||||||
ozet={danismanOzeti}
|
ozet={danismanOzeti}
|
||||||
ilkYorum={ilkYorum}
|
ilkYorum={ilkYorum}
|
||||||
kalanHak={Math.max(0, MAX_REVIZYON - (satir!.revisionCount ?? 0))}
|
|
||||||
paketFiyati={PAKET_FIYATI}
|
paketFiyati={PAKET_FIYATI}
|
||||||
paketKredi={URUNLER.paket.credits}
|
paketKredi={URUNLER.paket.credits}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export const SENARYOLAR: Senaryo[] = [
|
|||||||
ok: true,
|
ok: true,
|
||||||
toplam: 24,
|
toplam: 24,
|
||||||
dagilim: { hayal: 5, dengeli: 13, garanti: 6 },
|
dagilim: { hayal: 5, dengeli: 13, garanti: 6 },
|
||||||
kalanHak: 1,
|
|
||||||
ilkTercihler: [
|
ilkTercihler: [
|
||||||
"Bilgisayar Mühendisliği — İstanbul Teknik Üniversitesi (İSTANBUL)",
|
"Bilgisayar Mühendisliği — İstanbul Teknik Üniversitesi (İSTANBUL)",
|
||||||
"Bilgisayar Mühendisliği — Boğaziçi Üniversitesi (İSTANBUL)",
|
"Bilgisayar Mühendisliği — Boğaziçi Üniversitesi (İSTANBUL)",
|
||||||
@@ -86,8 +85,7 @@ export const SENARYOLAR: Senaryo[] = [
|
|||||||
});
|
});
|
||||||
writer.text(
|
writer.text(
|
||||||
"Listeni yeniden kurdum: ilk 5 tercihin artık İstanbul'dan, kalan 19 tercih Ankara ve İzmir'le devam ediyor. Dağılım yine 5 hayal / 13 dengeli / 6 güvenli.\n\n" +
|
"Listeni yeniden kurdum: ilk 5 tercihin artık İstanbul'dan, kalan 19 tercih Ankara ve İzmir'le devam ediyor. Dağılım yine 5 hayal / 13 dengeli / 6 güvenli.\n\n" +
|
||||||
"1. Bilgisayar Mühendisliği — İTÜ\n2. Bilgisayar Mühendisliği — Boğaziçi\n3. Yazılım Mühendisliği — YTÜ\n4. Bilgisayar Mühendisliği — Marmara\n5. Bilgisayar Mühendisliği — İÜ-Cerrahpaşa\n\n" +
|
"1. Bilgisayar Mühendisliği — İTÜ\n2. Bilgisayar Mühendisliği — Boğaziçi\n3. Yazılım Mühendisliği — YTÜ\n4. Bilgisayar Mühendisliği — Marmara\n5. Bilgisayar Mühendisliği — İÜ-Cerrahpaşa",
|
||||||
"Kalan revizyon hakkın: 1.",
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.transport({ delayMs: 20 }),
|
.transport({ delayMs: 20 }),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { appDb, schema } from "@/lib/appdb";
|
|||||||
import {
|
import {
|
||||||
grantCredits,
|
grantCredits,
|
||||||
krediBittiIsaretle,
|
krediBittiIsaretle,
|
||||||
MAX_REVIZYON,
|
|
||||||
RAPOR_KREDI,
|
RAPOR_KREDI,
|
||||||
spendCredits,
|
spendCredits,
|
||||||
} from "@/lib/credits";
|
} from "@/lib/credits";
|
||||||
@@ -21,15 +20,15 @@ import type { RaporParams, RaporSonuc, RevizyonKurali } from "./types/rapor";
|
|||||||
* (lib/ai/araclar.ts) burayı çağırır; eski /listem revizyon kutusu ve
|
* (lib/ai/araclar.ts) burayı çağırır; eski /listem revizyon kutusu ve
|
||||||
* `listeRevize` sunucu aksiyonu kaldırıldı — tek yol budur.
|
* `listeRevize` sunucu aksiyonu kaldırıldı — tek yol budur.
|
||||||
*
|
*
|
||||||
* Koruma kapıları eski aksiyonla aynı: paket şartı, MAX_REVIZYON sayacı,
|
* Koruma kapıları: paket şartı (sayı sınırı yok — 7 Eki 2026; `revision_count`
|
||||||
* kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
|
* yalnız sayaç/istatistik), kullanıcı başına üretim kilidi, `requestId` (= adayın sohbet mesajının
|
||||||
* kimliği) ile idempotent defter kaydı, başarısız üretimde iade. Bedel
|
* kimliği) ile idempotent defter kaydı, başarısız üretimde iade. Bedel
|
||||||
* paketliye 0 (liste ve revizyon pakete dâhil); satır yine yazılır ki aynı
|
* paketliye 0 (liste ve revizyon pakete dâhil); satır yine yazılır ki aynı
|
||||||
* mesaj ikinci kez revizyon tetiklemesin.
|
* mesaj ikinci kez revizyon tetiklemesin.
|
||||||
*/
|
*/
|
||||||
export type RevizyonSonucu =
|
export type RevizyonSonucu =
|
||||||
| { ok: true; ozet: Extract<RevizyonOzeti, { ok: true }>; rapor: RaporSonuc }
|
| { ok: true; ozet: Extract<RevizyonOzeti, { ok: true }>; rapor: RaporSonuc }
|
||||||
| { ok: false; neden: string; kod: "PAKET" | "HAK" | "RAPOR" | "SURUYOR" | "KREDI" | "HATA" };
|
| { ok: false; neden: string; kod: "PAKET" | "RAPOR" | "SURUYOR" | "KREDI" | "HATA" };
|
||||||
|
|
||||||
function hataMesaji(err: unknown): string {
|
function hataMesaji(err: unknown): string {
|
||||||
if (err instanceof RaporUretimHatasi) return err.message;
|
if (err instanceof RaporUretimHatasi) return err.message;
|
||||||
@@ -37,19 +36,18 @@ function hataMesaji(err: unknown): string {
|
|||||||
err instanceof Error &&
|
err instanceof Error &&
|
||||||
/abort|timeout|timed out/i.test(`${err.name} ${err.constructor.name} ${err.message}`)
|
/abort|timeout|timed out/i.test(`${err.name} ${err.constructor.name} ${err.message}`)
|
||||||
) {
|
) {
|
||||||
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; revizyon hakkın kullanılmadı.";
|
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; kredin/paketin etkilenmedi.";
|
||||||
}
|
}
|
||||||
console.error("[revizyon] üretim hatası:", err instanceof Error ? err.message : String(err));
|
console.error("[revizyon] üretim hatası:", err instanceof Error ? err.message : String(err));
|
||||||
return "Liste yeniden kurulurken bir sorun oluştu; revizyon hakkın kullanılmadı.";
|
return "Liste yeniden kurulurken bir sorun oluştu; kredin/paketin etkilenmedi.";
|
||||||
}
|
}
|
||||||
|
|
||||||
function ozetCikar(rapor: RaporSonuc, revisionCount: number): Extract<RevizyonOzeti, { ok: true }> {
|
function ozetCikar(rapor: RaporSonuc): Extract<RevizyonOzeti, { ok: true }> {
|
||||||
const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length;
|
const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
toplam: rapor.tercihler.length,
|
toplam: rapor.tercihler.length,
|
||||||
dagilim: { hayal: say("hayal"), dengeli: say("dengeli"), garanti: say("garanti") },
|
dagilim: { hayal: say("hayal"), dengeli: say("dengeli"), garanti: say("garanti") },
|
||||||
kalanHak: Math.max(0, MAX_REVIZYON - revisionCount),
|
|
||||||
ilkTercihler: rapor.tercihler.slice(0, 5).map((t) => {
|
ilkTercihler: rapor.tercihler.slice(0, 5).map((t) => {
|
||||||
const p = rapor.programlar[t.programId];
|
const p = rapor.programlar[t.programId];
|
||||||
return `${p?.isim ?? "?"} — ${p?.universite ?? "?"}${p?.il ? ` (${p.il})` : ""}`;
|
return `${p?.isim ?? "?"} — ${p?.universite ?? "?"}${p?.il ? ` (${p.il})` : ""}`;
|
||||||
@@ -79,14 +77,6 @@ export async function listeRevizeCekirdek(input: {
|
|||||||
if (!rapor?.result) {
|
if (!rapor?.result) {
|
||||||
return { ok: false, kod: "RAPOR", neden: "Adayın kayıtlı listesi yok; önce liste oluşturulmalı." };
|
return { ok: false, kod: "RAPOR", neden: "Adayın kayıtlı listesi yok; önce liste oluşturulmalı." };
|
||||||
}
|
}
|
||||||
if (rapor.revisionCount >= MAX_REVIZYON) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
kod: "HAK",
|
|
||||||
neden: `Revizyon hakkı doldu (en fazla ${MAX_REVIZYON}).`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const kilit = await uretimKilidiAl(input.userId, input.requestId);
|
const kilit = await uretimKilidiAl(input.userId, input.requestId);
|
||||||
if (!kilit.ok) {
|
if (!kilit.ok) {
|
||||||
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
||||||
@@ -123,7 +113,7 @@ export async function listeRevizeCekirdek(input: {
|
|||||||
const guncel = await kullanicininRaporu(input.userId);
|
const guncel = await kullanicininRaporu(input.userId);
|
||||||
if (guncel?.result) {
|
if (guncel?.result) {
|
||||||
const sonuc = guncel.result as RaporSonuc;
|
const sonuc = guncel.result as RaporSonuc;
|
||||||
return { ok: true, ozet: ozetCikar(sonuc, guncel.revisionCount), rapor: sonuc };
|
return { ok: true, ozet: ozetCikar(sonuc), rapor: sonuc };
|
||||||
}
|
}
|
||||||
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
return { ok: false, kod: "SURUYOR", neden: "Liste şu anda zaten yeniden kuruluyor." };
|
||||||
}
|
}
|
||||||
@@ -157,7 +147,7 @@ export async function listeRevizeCekirdek(input: {
|
|||||||
})
|
})
|
||||||
.where(eq(schema.reports.id, rapor.id));
|
.where(eq(schema.reports.id, rapor.id));
|
||||||
|
|
||||||
return { ok: true, ozet: ozetCikar(sonuc, rapor.revisionCount + 1), rapor: sonuc };
|
return { ok: true, ozet: ozetCikar(sonuc), rapor: sonuc };
|
||||||
} finally {
|
} finally {
|
||||||
await uretimKilidiBirak(input.userId, input.requestId);
|
await uretimKilidiBirak(input.userId, input.requestId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,8 +100,6 @@ export type AracBaglami = {
|
|||||||
hasPaket?: boolean;
|
hasPaket?: boolean;
|
||||||
/** Adayın bu turdaki mesajının kimliği: liste_revize'nin idempotency anahtarı. */
|
/** Adayın bu turdaki mesajının kimliği: liste_revize'nin idempotency anahtarı. */
|
||||||
mesajId?: string;
|
mesajId?: string;
|
||||||
/** Kalan revizyon hakkı (prompt'a ve araç hatasına gider). */
|
|
||||||
kalanRevizyon?: number;
|
|
||||||
/** Sohbet başındaki profil; profil_guncelle sonrası yenilenir */
|
/** Sohbet başındaki profil; profil_guncelle sonrası yenilenir */
|
||||||
profil: DanismanProfili;
|
profil: DanismanProfili;
|
||||||
/** Sihirbaz öncelik çipleri (uyum puanı / öne çıkan için) */
|
/** Sihirbaz öncelik çipleri (uyum puanı / öne çıkan için) */
|
||||||
@@ -469,7 +467,7 @@ export const ARACLAR: AracTanimi[] = [
|
|||||||
function: {
|
function: {
|
||||||
name: "liste_revize",
|
name: "liste_revize",
|
||||||
description:
|
description:
|
||||||
"Adayın 24'lük listesini verilen YAPISAL kurallarla yeniden kurar (paketli adayda, en fazla 2 hak). Kural kodla uygulanır: il/alan/tip farkı aday havuzunu değiştirir, 'ilkTercihler' listenin başını o ölçütle doldurur, 'cikar' havuzdan düşer, 'ekle' program kimliklerini listeye alır, 'iskelet' dilim dağılımını değiştirir. En az bir kural ver; adayın isteği belirsizse ÖNCE soru_sor. Çalışması 20-60 sn sürer; sonuç ok ise liste ekranda yenilenir, sen ne değiştiğini 2-3 cümleyle söyle ve ilk 5 tercihi yaz. 'not' alanına adayın isteğini bir cümleyle özetle.",
|
"Adayın 24'lük listesini verilen YAPISAL kurallarla yeniden kurar (paketli adayda; sınır yok). Kural kodla uygulanır: il/alan/tip farkı aday havuzunu değiştirir, 'ilkTercihler' listenin başını o ölçütle doldurur, 'cikar' havuzdan düşer, 'ekle' program kimliklerini listeye alır, 'iskelet' dilim dağılımını değiştirir. En az bir kural ver; adayın isteği belirsizse ÖNCE soru_sor. Çalışması 20-60 sn sürer; sonuç ok ise liste ekranda yenilenir, sen ne değiştiğini 2-3 cümleyle söyle ve ilk 5 tercihi yaz. 'not' alanına adayın isteğini bir cümleyle özetle.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -1759,15 +1757,12 @@ async function listeRevize(b: AracBaglami, a: Record<string, unknown>): Promise<
|
|||||||
if (!sonuc.ok) {
|
if (!sonuc.ok) {
|
||||||
return {
|
return {
|
||||||
hata: sonuc.neden,
|
hata: sonuc.neden,
|
||||||
not:
|
not: "Adaya listenin yeniden kurulamadığını ve nedenini söyle; 'yeniden kurdum' deme.",
|
||||||
sonuc.kod === "HAK"
|
|
||||||
? "Adaya revizyon hakkının dolduğunu söyle; sohbetten listesi hakkında soru sormaya devam edebilir."
|
|
||||||
: "Adaya listenin yeniden kurulamadığını ve nedenini söyle; 'yeniden kurdum' deme.",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...sonuc.ozet,
|
...sonuc.ozet,
|
||||||
not: "Liste ekranda yenilendi. Adaya ne değiştiğini 2-3 cümleyle söyle ve ilk 5 tercihi numaralı yaz; kalan revizyon hakkını belirt. Sayı uydurma; yukarıdaki alanları kullan.",
|
not: "Liste ekranda yenilendi. Adaya ne değiştiğini 2-3 cümleyle söyle ve ilk 5 tercihi numaralı yaz. Sayı uydurma; yukarıdaki alanları kullan.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
221
src/lib/ai/baglanti-denetimi.ts
Normal file
221
src/lib/ai/baglanti-denetimi.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Danışman cevabındaki rehber bağlantılarının doğrulanması (7 Eki 2026).
|
||||||
|
*
|
||||||
|
* Model araç çağırmadan ezberden `[Yatay geçiş](/rehber/yatay-gecis-rehberi)`
|
||||||
|
* yazabiliyor; istemci her `/rehber/<slug>` adresini "Rehber yazısı" hapı
|
||||||
|
* yapıyor ve hap 404'e gidiyordu. Prompt "adres uydurma" diyor ama asıl
|
||||||
|
* savunma koddur: metindeki her `/rehber/<slug>` gerçek yazı listesiyle
|
||||||
|
* eşlenir. Slug kayıtlıysa dokunulmaz; değilse en yakın gerçek slug'a
|
||||||
|
* (başlığı kayıttan) yeniden yazılır; yakın aday yoksa bağlantı düz metne
|
||||||
|
* iner, çıplak adres silinir.
|
||||||
|
*
|
||||||
|
* Cevap `text-delta` parçalarıyla anında ekrana gittiği için düzeltme cevap
|
||||||
|
* bittikten sonra yapılamaz (yanlış hap ekrandan geri alınamaz). Bu yüzden
|
||||||
|
* `BaglantiTamponu` akış sırasında çalışır: bir bağlantı adayı başlayınca
|
||||||
|
* metni tutar, aday kapanınca doğrulayıp bırakır. Bağlantılar kısadır,
|
||||||
|
* gecikme hissedilmez. Tamponun parça parça çıktısı tek seferlik
|
||||||
|
* `rehberBaglantilariniDuzelt` sonucuyla birebir aynıdır
|
||||||
|
* (scripts/eval/baglanti-denetim.ts bunu sınar).
|
||||||
|
*
|
||||||
|
* `/universite/…` ve `/bolum/…` bu turda doğrulanmaz (backlog).
|
||||||
|
*
|
||||||
|
* Saf modül: DB/ağ/Next yok, `server-only` import etmez; akış modülü gibi
|
||||||
|
* betiklerden de koşar.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type RehberKaydi = { slug: string; baslik: string };
|
||||||
|
|
||||||
|
export type BaglantiDuzeltme = {
|
||||||
|
/** Modelin yazdığı (kayıtlı olmayan) slug. */
|
||||||
|
slug: string;
|
||||||
|
/** "yakin": gerçek bir yazıya yeniden yazıldı; "duz": bağlantı düz metne indi / adres silindi. */
|
||||||
|
sonuc: "yakin" | "duz";
|
||||||
|
yeniSlug?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Yakın eşleşme için en düşük Dice benzerliği. */
|
||||||
|
const YAKIN_ESIK = 0.5;
|
||||||
|
/** En iyi adayın ikinciden en az bu kadar önde olması gerekir (belirsiz eşleşme yeniden yazılmaz). */
|
||||||
|
const ACIK_ARA_FARK = 0.15;
|
||||||
|
/** `[` sonrası bu kadar karakter içinde `](` gelmezse sıradan köşeli parantezdir. */
|
||||||
|
const ETIKET_UST = 160;
|
||||||
|
/** `](` sonrası bu kadar karakter içinde `)` gelmezse bağlantı değildir. */
|
||||||
|
const ADRES_UST = 200;
|
||||||
|
/** Ters tırnak bu kadar karakter içinde kapanmazsa bağlantı adayı değildir. */
|
||||||
|
const TIRNAK_UST = 160;
|
||||||
|
|
||||||
|
const SITE_KOKU = "(?:https?:\\/\\/(?:www\\.)?kolaytercih\\.com)?";
|
||||||
|
const SLUG = "[a-z0-9-]+";
|
||||||
|
/**
|
||||||
|
* Üç biçim, soldan sağa tek geçişte: markdown bağlantısı, ters tırnak içi
|
||||||
|
* adres, çıplak adres. Markdown etiketi `]` içeremez; etiket ve adres
|
||||||
|
* uzunluk sınırları tamponun bekleme sınırlarıyla aynıdır ki parça parça
|
||||||
|
* çıktı tek seferlik sonuçla aynı kalsın.
|
||||||
|
*/
|
||||||
|
const BAGLANTI = new RegExp(
|
||||||
|
`\\[([^\\]]{0,${ETIKET_UST}})\\]\\(\\s*${SITE_KOKU}\\/rehber\\/(${SLUG})\\s*\\)` +
|
||||||
|
`|\`\\s*\\/rehber\\/(${SLUG})\\s*\`` +
|
||||||
|
`|\\/rehber\\/(${SLUG})`,
|
||||||
|
"g",
|
||||||
|
);
|
||||||
|
|
||||||
|
function parcalar(slug: string): Set<string> {
|
||||||
|
return new Set(slug.split("-").filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sørensen–Dice: iki slug'ın `-` ile ayrılmış parça kümeleri üzerinden. */
|
||||||
|
function dice(a: Set<string>, b: Set<string>): number {
|
||||||
|
if (!a.size || !b.size) return 0;
|
||||||
|
let ortak = 0;
|
||||||
|
for (const p of a) if (b.has(p)) ortak++;
|
||||||
|
return (2 * ortak) / (a.size + b.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kayıtlı olmayan slug'a en yakın gerçek kayıt: benzerlik eşiği geçilmeli
|
||||||
|
* ve tek aday açık ara önde olmalı; aksi hâlde null (yeniden yazma yapılmaz,
|
||||||
|
* bağlantı düz metne iner).
|
||||||
|
*/
|
||||||
|
export function enYakinRehber(slug: string, rehberler: readonly RehberKaydi[]): RehberKaydi | null {
|
||||||
|
const hedef = parcalar(slug);
|
||||||
|
let birinci: { kayit: RehberKaydi; puan: number } | null = null;
|
||||||
|
let ikinciPuan = 0;
|
||||||
|
for (const kayit of rehberler) {
|
||||||
|
const puan = dice(hedef, parcalar(kayit.slug));
|
||||||
|
if (!birinci || puan > birinci.puan) {
|
||||||
|
ikinciPuan = birinci?.puan ?? 0;
|
||||||
|
birinci = { kayit, puan };
|
||||||
|
} else if (puan > ikinciPuan) {
|
||||||
|
ikinciPuan = puan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!birinci || birinci.puan < YAKIN_ESIK) return null;
|
||||||
|
if (birinci.puan - ikinciPuan < ACIK_ARA_FARK) return null;
|
||||||
|
return birinci.kayit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metindeki `/rehber/<slug>` adreslerini (markdown bağlantısı, ters tırnak ve
|
||||||
|
* çıplak adres) gerçek kayıtlarla eşler. Kayıtlı slug dokunulmaz; yakın
|
||||||
|
* kayıt varsa `[başlık](/rehber/yeni-slug)` olur (başlık kayıttan); yoksa
|
||||||
|
* markdown bağlantısı etiketine iner, ters tırnak içi ve çıplak adres
|
||||||
|
* silinir ("bkz." gibi öncesindeki bağlaç kalır).
|
||||||
|
*/
|
||||||
|
export function rehberBaglantilariniDuzelt(
|
||||||
|
metin: string,
|
||||||
|
rehberler: readonly RehberKaydi[],
|
||||||
|
): { metin: string; duzeltmeler: BaglantiDuzeltme[] } {
|
||||||
|
const duzeltmeler: BaglantiDuzeltme[] = [];
|
||||||
|
const kayitli = new Set(rehberler.map((r) => r.slug));
|
||||||
|
const yeni = metin.replace(BAGLANTI, (eslesme, etiket: string | undefined, mdSlug?: string, tirnakSlug?: string, ciplakSlug?: string) => {
|
||||||
|
const slug = mdSlug ?? tirnakSlug ?? ciplakSlug ?? "";
|
||||||
|
if (kayitli.has(slug)) return eslesme;
|
||||||
|
const yakin = enYakinRehber(slug, rehberler);
|
||||||
|
if (yakin) {
|
||||||
|
duzeltmeler.push({ slug, sonuc: "yakin", yeniSlug: yakin.slug });
|
||||||
|
return `[${yakin.baslik}](/rehber/${yakin.slug})`;
|
||||||
|
}
|
||||||
|
duzeltmeler.push({ slug, sonuc: "duz" });
|
||||||
|
return mdSlug != null ? (etiket ?? "") : "";
|
||||||
|
});
|
||||||
|
return { metin: yeni, duzeltmeler };
|
||||||
|
}
|
||||||
|
|
||||||
|
const SLUG_KARAKTERI = /[a-z0-9-]/;
|
||||||
|
const ADRES_ONEKI = "/rehber/";
|
||||||
|
|
||||||
|
/** Metnin sonundaki, `/rehber/` önekinin gerçek öneki olan kuyruk uzunluğu ("/reh" → 4). */
|
||||||
|
function kuyrukOnEki(metin: string): number {
|
||||||
|
const ust = Math.min(metin.length, ADRES_ONEKI.length - 1);
|
||||||
|
for (let n = ust; n > 0; n--) {
|
||||||
|
if (metin.endsWith(ADRES_ONEKI.slice(0, n))) return n;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Akış tamponu. `besle(delta)` güvenle bırakılabilen metni döner (bağlantı
|
||||||
|
* adayı dışındaki metin olduğu gibi, kapanan aday doğrulanmış hâliyle);
|
||||||
|
* `bitir()` kalanı doğrulayıp boşaltır. Tutma noktaları: `[`, ters tırnak,
|
||||||
|
* `/rehber/` (ve metin sonunda yarım kalmış "/reh" gibi öneki).
|
||||||
|
*/
|
||||||
|
export class BaglantiTamponu {
|
||||||
|
readonly duzeltmeler: BaglantiDuzeltme[] = [];
|
||||||
|
private bekleyen = "";
|
||||||
|
|
||||||
|
constructor(private readonly rehberler: readonly RehberKaydi[]) {}
|
||||||
|
|
||||||
|
besle(delta: string): string {
|
||||||
|
this.bekleyen += delta;
|
||||||
|
let cikti = "";
|
||||||
|
for (;;) {
|
||||||
|
const t = this.tetik();
|
||||||
|
if (t === -1) {
|
||||||
|
const kuyruk = kuyrukOnEki(this.bekleyen);
|
||||||
|
const kes = this.bekleyen.length - kuyruk;
|
||||||
|
cikti += this.bekleyen.slice(0, kes);
|
||||||
|
this.bekleyen = this.bekleyen.slice(kes);
|
||||||
|
return cikti;
|
||||||
|
}
|
||||||
|
cikti += this.bekleyen.slice(0, t);
|
||||||
|
this.bekleyen = this.bekleyen.slice(t);
|
||||||
|
const son = this.adayinSonu();
|
||||||
|
if (son === "bekle") return cikti;
|
||||||
|
if (son === "degil") {
|
||||||
|
// Sıradan köşeli parantez / ters tırnak / "/rehber/" artığı: ilk
|
||||||
|
// karakter bırakılır, kalan yeniden taranır.
|
||||||
|
cikti += this.bekleyen[0];
|
||||||
|
this.bekleyen = this.bekleyen.slice(1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cikti += this.duzelt(this.bekleyen.slice(0, son));
|
||||||
|
this.bekleyen = this.bekleyen.slice(son);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bitir(): string {
|
||||||
|
const kalan = this.bekleyen;
|
||||||
|
this.bekleyen = "";
|
||||||
|
return kalan ? this.duzelt(kalan) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private duzelt(parca: string): string {
|
||||||
|
const s = rehberBaglantilariniDuzelt(parca, this.rehberler);
|
||||||
|
this.duzeltmeler.push(...s.duzeltmeler);
|
||||||
|
return s.metin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bekleyen metinde en erken tutma noktası; yoksa -1. */
|
||||||
|
private tetik(): number {
|
||||||
|
const adaylar = [this.bekleyen.indexOf("["), this.bekleyen.indexOf("`"), this.bekleyen.indexOf(ADRES_ONEKI)].filter((i) => i >= 0);
|
||||||
|
return adaylar.length ? Math.min(...adaylar) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bekleyen metin bir tutma noktasıyla başlar. Aday kapandıysa bitiş
|
||||||
|
* indeksi (hariç); daha veri gerekiyorsa "bekle"; aday olmadığı
|
||||||
|
* anlaşıldıysa "degil".
|
||||||
|
*/
|
||||||
|
private adayinSonu(): number | "bekle" | "degil" {
|
||||||
|
const m = this.bekleyen;
|
||||||
|
if (m[0] === "[") {
|
||||||
|
const kapanis = m.indexOf("]", 1);
|
||||||
|
if (kapanis === -1 || kapanis > ETIKET_UST + 1) return m.length > ETIKET_UST + 1 ? "degil" : "bekle";
|
||||||
|
if (kapanis + 1 >= m.length) return "bekle";
|
||||||
|
if (m[kapanis + 1] !== "(") return "degil";
|
||||||
|
const parantez = m.indexOf(")", kapanis + 2);
|
||||||
|
if (parantez === -1) return m.length - (kapanis + 2) > ADRES_UST ? "degil" : "bekle";
|
||||||
|
return parantez + 1;
|
||||||
|
}
|
||||||
|
if (m[0] === "`") {
|
||||||
|
const kapanis = m.indexOf("`", 1);
|
||||||
|
if (kapanis === -1) return m.length > TIRNAK_UST + 1 ? "degil" : "bekle";
|
||||||
|
return kapanis + 1;
|
||||||
|
}
|
||||||
|
// "/rehber/" ile başlıyor: slug slug-dışı bir karakterle kapanır
|
||||||
|
let i = ADRES_ONEKI.length;
|
||||||
|
while (i < m.length && SLUG_KARAKTERI.test(m[i])) i++;
|
||||||
|
if (i >= m.length) return "bekle";
|
||||||
|
return i === ADRES_ONEKI.length ? "degil" : i;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,13 +72,11 @@ programlarında başarı sırası barajı yoktur.`;
|
|||||||
* netleştirir (soru_sor formu), sonra liste_revize ile YAPISAL kural verir.
|
* netleştirir (soru_sor formu), sonra liste_revize ile YAPISAL kural verir.
|
||||||
* Serbest metin kural değildir: "ilk 5 İstanbul olsun" → ilkTercihler.
|
* Serbest metin kural değildir: "ilk 5 İstanbul olsun" → ilkTercihler.
|
||||||
*/
|
*/
|
||||||
function revizyonBolumu(r: { kalanHak: number; paketli: boolean }, params: RaporParams): string {
|
function revizyonBolumu(r: { paketli: boolean }, params: RaporParams): string {
|
||||||
const mevcut = kuralOzeti(params.revizyon);
|
const mevcut = kuralOzeti(params.revizyon);
|
||||||
const durum = !r.paketli
|
const durum = !r.paketli
|
||||||
? `Adayın paketi YOK: liste_revize çağırma; listeyi değiştirmek isterse tam liste ve revizyonun Tercih Dönemi Paketi'yle (/paket) açıldığını sempatik biçimde söyle.`
|
? `Adayın paketi YOK: liste_revize çağırma; listeyi değiştirmek isterse tam liste ve revizyonun Tercih Dönemi Paketi'yle (/paket) açıldığını sempatik biçimde söyle.`
|
||||||
: r.kalanHak <= 0
|
: `Revizyon sınırsız; her kurulum 20-60 sn sürdüğü için isteği yine tek seferde doğru kur.`;
|
||||||
? `Adayın revizyon hakkı DOLDU (${r.kalanHak} kaldı): liste_revize çağırma; değişiklik isterse hakkın dolduğunu söyle, listesi hakkında soru cevaplamaya devam et.`
|
|
||||||
: `Adayın kalan revizyon hakkı: ${r.kalanHak}. Her başarılı liste_revize bir hak düşer; bu yüzden isteği tek seferde doğru kur.`;
|
|
||||||
return `LİSTE REVİZYONU (sohbetten): aday listesinde değişiklik istediğinde ("ilk 5
|
return `LİSTE REVİZYONU (sohbetten): aday listesinde değişiklik istediğinde ("ilk 5
|
||||||
İstanbul olsun", "Ankara da olsun", "vakıf olmasın", "X üniversitesini çıkar",
|
İstanbul olsun", "Ankara da olsun", "vakıf olmasın", "X üniversitesini çıkar",
|
||||||
"daha güvenli olsun", "şu programı ekle") şu sırayla çalış:
|
"daha güvenli olsun", "şu programı ekle") şu sırayla çalış:
|
||||||
@@ -98,8 +96,8 @@ function revizyonBolumu(r: { kalanHak: number; paketli: boolean }, params: Rapor
|
|||||||
kısa cümleyle bitir. Cevap "Form cevaplarım:" diye gelince liste_revize
|
kısa cümleyle bitir. Cevap "Form cevaplarım:" diye gelince liste_revize
|
||||||
çağır; tekrar sorma.
|
çağır; tekrar sorma.
|
||||||
3. liste_revize ok dönünce: liste ekranda zaten yenilendi. Ne değiştiğini 2-3
|
3. liste_revize ok dönünce: liste ekranda zaten yenilendi. Ne değiştiğini 2-3
|
||||||
cümleyle söyle, ilk 5 tercihi numaralı yaz (aracın verdiği adlarla), kalan
|
cümleyle söyle, ilk 5 tercihi numaralı yaz (aracın verdiği adlarla).
|
||||||
hakkı belirt. Dilim sırası "ilk N" kuralında bilerek bozulur; aday sorarsa
|
Dilim sırası "ilk N" kuralında bilerek bozulur; aday sorarsa
|
||||||
bunu söyle. Araç hata dönerse "yeniden kurdum" DEME; nedenini söyle.
|
bunu söyle. Araç hata dönerse "yeniden kurdum" DEME; nedenini söyle.
|
||||||
4. REVİZYON DÜRÜSTLÜĞÜ: "listeni yeniden kurdum / güncelledim / revize ettim"
|
4. REVİZYON DÜRÜSTLÜĞÜ: "listeni yeniden kurdum / güncelledim / revize ettim"
|
||||||
cümlesini ancak BU cevapta liste_revize çağırdıysan ve ok döndüyse yaz.
|
cümlesini ancak BU cevapta liste_revize çağırdıysan ve ok döndüyse yaz.
|
||||||
@@ -123,10 +121,10 @@ export function sistemPromptu(opts: {
|
|||||||
*/
|
*/
|
||||||
rapor?: { genelDegerlendirme?: string; uyarilar?: string[]; kapsam?: { toplam: number; uyarlandi: boolean } };
|
rapor?: { genelDegerlendirme?: string; uyarilar?: string[]; kapsam?: { toplam: number; uyarlandi: boolean } };
|
||||||
/**
|
/**
|
||||||
* Sohbetten liste revizyonu (7 Eki 2026): kalan hak ve paket durumu.
|
* Sohbetten liste revizyonu (7 Eki 2026): paket durumu (sayı sınırı yok).
|
||||||
* Verilmezse revizyon bölümü yazılmaz (eval betikleri eski davranışı korur).
|
* Verilmezse revizyon bölümü yazılmaz (eval betikleri eski davranışı korur).
|
||||||
*/
|
*/
|
||||||
revizyon?: { kalanHak: number; paketli: boolean };
|
revizyon?: { paketli: boolean };
|
||||||
}): string {
|
}): string {
|
||||||
const { params } = opts;
|
const { params } = opts;
|
||||||
const secimVar =
|
const secimVar =
|
||||||
@@ -249,7 +247,9 @@ KURALLAR:
|
|||||||
[Yazının başlığı](/rehber/slug), [Üniversite adı](/universite/slug),
|
[Yazının başlığı](/rehber/slug), [Üniversite adı](/universite/slug),
|
||||||
[Bölüm adı](/bolum/slug); adresi düz metin ya da ters tırnak içinde yazma. Adres ve
|
[Bölüm adı](/bolum/slug); adresi düz metin ya da ters tırnak içinde yazma. Adres ve
|
||||||
başlık araç sonucundaki "sayfa" / "baslik" / "ad" alanından gelir; adres
|
başlık araç sonucundaki "sayfa" / "baslik" / "ad" alanından gelir; adres
|
||||||
uydurma. Ekranda bu bağlantı ikonlu bir hap olarak görünür.
|
uydurma. Rehber adresi yalnız bu cevapta rehber_ara / rehber_oku sonucunda
|
||||||
|
gördüğün "sayfa" alanından gelir; görmediğin bir yazıya bağlantı verme, önce
|
||||||
|
rehber_ara çağır. Ekranda bu bağlantı ikonlu bir hap olarak görünür.
|
||||||
- PROFİL: aday bir tercih söylerse (yaşadığı il, evden en fazla kaç km,
|
- PROFİL: aday bir tercih söylerse (yaşadığı il, evden en fazla kaç km,
|
||||||
vakıfta yıllık ücret sınırı, burs beklentisi, öğretim dili, uzaktan/açık
|
vakıfta yıllık ücret sınırı, burs beklentisi, öğretim dili, uzaktan/açık
|
||||||
kabul mü, kampüs büyüklüğü, özel kontenjan durumu) ${PROFIL_ONAY_SOR
|
kabul mü, kampüs büyüklüğü, özel kontenjan durumu) ${PROFIL_ONAY_SOR
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type UIMessageChunk,
|
type UIMessageChunk,
|
||||||
} from "ai";
|
} from "ai";
|
||||||
import { ARACLAR, aracSeti, type AracBaglami } from "./araclar";
|
import { ARACLAR, aracSeti, type AracBaglami } from "./araclar";
|
||||||
|
import { BaglantiTamponu, type RehberKaydi } from "./baglanti-denetimi";
|
||||||
import { kaliciDislananSaglayicilar, sohbetDilModeli } from "./client";
|
import { kaliciDislananSaglayicilar, sohbetDilModeli } from "./client";
|
||||||
import {
|
import {
|
||||||
KAYIT_DUZELTME_NOTU,
|
KAYIT_DUZELTME_NOTU,
|
||||||
@@ -69,6 +70,8 @@ export type CiktiDenetimi = {
|
|||||||
revizeNotu: boolean;
|
revizeNotu: boolean;
|
||||||
/** Cevapta araç adları / şema anahtarları var (yalnız gözlem). */
|
/** Cevapta araç adları / şema anahtarları var (yalnız gözlem). */
|
||||||
semaSizintisi: boolean;
|
semaSizintisi: boolean;
|
||||||
|
/** Akış sırasında düzeltilen rehber bağlantısı sayısı (baglanti-denetimi.ts). */
|
||||||
|
baglantiDuzeltme: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function danismanAkisi(opts: {
|
export function danismanAkisi(opts: {
|
||||||
@@ -88,6 +91,13 @@ export function danismanAkisi(opts: {
|
|||||||
* revizyon sonucunu kayda (chat_messages.meta) yazar.
|
* revizyon sonucunu kayda (chat_messages.meta) yazar.
|
||||||
*/
|
*/
|
||||||
onAracSonucu?: (ad: string, cikti: unknown) => void;
|
onAracSonucu?: (ad: string, cikti: unknown) => void;
|
||||||
|
/**
|
||||||
|
* Gerçek rehber yazıları (slug + başlık). Verilirse cevaptaki
|
||||||
|
* `/rehber/<slug>` adresleri akış sırasında bu listeyle doğrulanır
|
||||||
|
* (baglanti-denetimi.ts); verilmezse tampon pasiftir (betikler/evaller
|
||||||
|
* eski davranışı korur).
|
||||||
|
*/
|
||||||
|
rehberler?: readonly RehberKaydi[];
|
||||||
}): ReadableStream<UIMessageChunk> {
|
}): ReadableStream<UIMessageChunk> {
|
||||||
return createUIMessageStream<UIMessage>({
|
return createUIMessageStream<UIMessage>({
|
||||||
execute: async ({ writer }) => {
|
execute: async ({ writer }) => {
|
||||||
@@ -109,6 +119,10 @@ export function danismanAkisi(opts: {
|
|||||||
let basariliRevizyon = 0;
|
let basariliRevizyon = 0;
|
||||||
// soru_sor çağrıldı: form adayda; model aynı cevapta başka araç çağırmasın
|
// soru_sor çağrıldı: form adayda; model aynı cevapta başka araç çağırmasın
|
||||||
let formAcildi = false;
|
let formAcildi = false;
|
||||||
|
// Rehber bağlantısı doğrulama: her metin parçası için ayrı tampon
|
||||||
|
let tampon: BaglantiTamponu | null = null;
|
||||||
|
let baglantiDuzeltme = 0;
|
||||||
|
const duzeltilenSluglar: string[] = [];
|
||||||
try {
|
try {
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: sohbetDilModeli(),
|
model: sohbetDilModeli(),
|
||||||
@@ -183,11 +197,30 @@ export function danismanAkisi(opts: {
|
|||||||
if (done) break;
|
if (done) break;
|
||||||
// Kesilme istemciye "abort" değil hata olarak gider (aşağıda fırlatılır)
|
// Kesilme istemciye "abort" değil hata olarak gider (aşağıda fırlatılır)
|
||||||
if (parca.type === "abort") continue;
|
if (parca.type === "abort") continue;
|
||||||
if (parca.type === "text-start" && metin && !metin.endsWith("\n")) {
|
if (parca.type === "text-start") {
|
||||||
|
if (metin && !metin.endsWith("\n")) {
|
||||||
// Araç turundan sonra yeni metin parçası: kayıtta yapışmasın
|
// Araç turundan sonra yeni metin parçası: kayıtta yapışmasın
|
||||||
metin += "\n\n";
|
metin += "\n\n";
|
||||||
|
}
|
||||||
|
if (opts.rehberler) tampon = new BaglantiTamponu(opts.rehberler);
|
||||||
} else if (parca.type === "text-delta") {
|
} else if (parca.type === "text-delta") {
|
||||||
metin += parca.delta;
|
// Tampon bir bağlantı adayı görünce metni tutar, aday kapanınca
|
||||||
|
// doğrulanmış hâlini bırakır; ekran ve kayıt aynı metni alır.
|
||||||
|
const cikti = tampon ? tampon.besle(parca.delta) : parca.delta;
|
||||||
|
metin += cikti;
|
||||||
|
if (cikti) writer.write({ ...parca, delta: cikti });
|
||||||
|
continue;
|
||||||
|
} else if (parca.type === "text-end") {
|
||||||
|
const kalan = tampon?.bitir() ?? "";
|
||||||
|
if (kalan) {
|
||||||
|
metin += kalan;
|
||||||
|
writer.write({ type: "text-delta", id: parca.id, delta: kalan });
|
||||||
|
}
|
||||||
|
if (tampon) {
|
||||||
|
baglantiDuzeltme += tampon.duzeltmeler.length;
|
||||||
|
for (const d of tampon.duzeltmeler) duzeltilenSluglar.push(d.yeniSlug ? `${d.slug}→${d.yeniSlug}` : `${d.slug}→(düz)`);
|
||||||
|
tampon = null;
|
||||||
|
}
|
||||||
} else if (parca.type === "error") {
|
} else if (parca.type === "error") {
|
||||||
akisHatasi = parca.errorText;
|
akisHatasi = parca.errorText;
|
||||||
}
|
}
|
||||||
@@ -244,11 +277,15 @@ export function danismanAkisi(opts: {
|
|||||||
const semaSizintisi = semaSizintisiVar(metin, ARAC_ADLARI);
|
const semaSizintisi = semaSizintisiVar(metin, ARAC_ADLARI);
|
||||||
if (semaSizintisi) console.warn("[soru] olası iç şema sızıntısı: cevapta araç adları / şema anahtarları var");
|
if (semaSizintisi) console.warn("[soru] olası iç şema sızıntısı: cevapta araç adları / şema anahtarları var");
|
||||||
if (reddedilenArac) console.info("[soru] araç sınırı:", reddedilenArac, "çağrı çalıştırılmadı");
|
if (reddedilenArac) console.info("[soru] araç sınırı:", reddedilenArac, "çağrı çalıştırılmadı");
|
||||||
opts.onDenetim?.({ jevP, vaatRegex, vaatNotu, reddedilenArac, kayitNotu, revizeNotu, semaSizintisi });
|
// Slug kişisel veri değildir; hangi uydurma adresin neye döndüğü loglanır
|
||||||
|
if (baglantiDuzeltme) console.info("[soru] rehber bağlantısı düzeltildi:", baglantiDuzeltme, duzeltilenSluglar.join(", "));
|
||||||
|
opts.onDenetim?.({ jevP, vaatRegex, vaatNotu, reddedilenArac, kayitNotu, revizeNotu, semaSizintisi, baglantiDuzeltme });
|
||||||
await opts.onTamam(metin);
|
await opts.onTamam(metin);
|
||||||
tamamlandi = true;
|
tamamlandi = true;
|
||||||
writer.write({ type: "finish" });
|
writer.write({ type: "finish" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// Tamponda tutulan bağlantı adayı yarım kayda doğrulanmış hâliyle girsin
|
||||||
|
if (tampon) metin += tampon.bitir();
|
||||||
if (!tamamlandi) await opts.onYarim(metin).catch(() => {});
|
if (!tamamlandi) await opts.onYarim(metin).catch(() => {});
|
||||||
// Tam hata nesnesi istek gövdesini ve cevap başlıklarını taşır; yalnız mesaj
|
// 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));
|
console.error("[soru] stream hatası:", err instanceof Error ? err.message : String(err));
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export type RevizyonOzeti =
|
|||||||
ok: true;
|
ok: true;
|
||||||
toplam: number;
|
toplam: number;
|
||||||
dagilim: { hayal: number; dengeli: number; garanti: number };
|
dagilim: { hayal: number; dengeli: number; garanti: number };
|
||||||
kalanHak: number;
|
|
||||||
ilkTercihler: string[];
|
ilkTercihler: string[];
|
||||||
}
|
}
|
||||||
| { ok: false; neden: string };
|
| { ok: false; neden: string };
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ export const DENEME_KREDISI = 5;
|
|||||||
// Rapor üretimi/revizyonu başına düşen kredi
|
// Rapor üretimi/revizyonu başına düşen kredi
|
||||||
export const RAPOR_KREDI = 3;
|
export const RAPOR_KREDI = 3;
|
||||||
|
|
||||||
/** Paketle gelen geri bildirimli liste revizyonu hakkı */
|
|
||||||
export const MAX_REVIZYON = 2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UNIQUE ihlali mi? drizzle sürücü hatasını DrizzleQueryError'a sarar: üst
|
* UNIQUE ihlali mi? drizzle sürücü hatasını DrizzleQueryError'a sarar: üst
|
||||||
* mesaj "Failed query: …" olur, "UNIQUE" yalnız `cause` zincirinde kalır. Yalnız
|
* mesaj "Failed query: …" olur, "UNIQUE" yalnız `cause` zincirinde kalır. Yalnız
|
||||||
|
|||||||
Reference in New Issue
Block a user