Some checks failed
Deploy / deploy (push) Failing after 1h28m6s
- Added new scripts for database operations and temporary production in package.json. - Integrated KaydetBannerLazy component into the layout for improved user notifications. - Enhanced API error handling in the soru route to mark credit exhaustion. - Updated the IletisimPage for better button styling and user experience. - Refactored ListemPage to streamline user flow and improve session handling. - Removed unused ListePaneli component to clean up the codebase.
264 lines
9.2 KiB
TypeScript
264 lines
9.2 KiB
TypeScript
/**
|
||
* Tadımlık havuzu batch üretimi: kova × puan türü × (kategori + "genel")
|
||
* kombinasyonları için 1'er örnek tercih satırı (gerekce/riskNotu/trendOzeti)
|
||
* üretip app.db'deki tadimlik_havuzu tablosuna yazar.
|
||
*
|
||
* pnpm tadimlik # boş anahtarları doldurur (idempotent)
|
||
* pnpm tadimlik --force # mevcut satırları da yeniden üretir
|
||
* pnpm tadimlik --kova=10-50k --tur=say --kategori=muhendislik-teknoloji
|
||
* pnpm tadimlik --mock # anahtar olsa da mock metin (dev/test)
|
||
* pnpm tadimlik --bekleme=13000 # LLM çağrıları arası bekleme (ms)
|
||
*
|
||
* AI anahtarı yoksa (dev) mock metin üretir — akış uçtan uca test edilebilir.
|
||
* Gemini ücretsiz katmanı dakikada ~5 istek kabul eder; gerçek üretimde
|
||
* --bekleme=13000 gibi bir tempo kullan.
|
||
* Anonim trafik LLM'e asla istek atmaz; tek maliyet bu script'in koşusudur.
|
||
*/
|
||
import { randomUUID } from "node:crypto";
|
||
import { existsSync, readFileSync } from "node:fs";
|
||
import { z } from "zod";
|
||
import { appDb, schema } from "../src/lib/appdb";
|
||
import { dilimAra, type DilimKey, type Program, type PuanTuruKey, PUAN_TURLERI } from "../src/lib/db";
|
||
import { KATEGORILER, kategoriEslesir } from "../src/lib/kategoriler";
|
||
import { havuzuKompaktJson, turLabel } from "../src/lib/rapor-havuzu";
|
||
import { GENEL_KATEGORI, KOVALAR, kategoriSlugBul } from "../src/lib/tadimlik-havuzu";
|
||
import { anahtarVar } from "../src/lib/ai/client";
|
||
import { yapilandirilmisUret } from "../src/lib/ai/cagri";
|
||
|
||
// Next dışında koşuyoruz; .env dosyalarını elle yükle (mevcut env kazanır).
|
||
function envYukle(dosya: string): void {
|
||
if (!existsSync(dosya)) return;
|
||
for (const satir of readFileSync(dosya, "utf8").split("\n")) {
|
||
const m = satir.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||
if (m && process.env[m[1]] === undefined) {
|
||
process.env[m[1]] = m[2].replace(/^"(.*)"$/, "$1").trim();
|
||
}
|
||
}
|
||
}
|
||
envYukle(".env.local");
|
||
envYukle(".env");
|
||
|
||
const TadimlikSchema = z.object({
|
||
gerekce: z.string(),
|
||
riskNotu: z.string(),
|
||
trendOzeti: z.string(),
|
||
});
|
||
|
||
// rapor.ts'teki SISTEM kurallarının tek satırlık versiyonu.
|
||
const SISTEM = `Sen KolayTercih'in kıdemli YKS tercih danışmanısın. Sana GERÇEK
|
||
YÖK Atlas verisiyle TEK bir program ve temsili bir aday tanımı verilecek; bu
|
||
program için tercih listesi satırı metinleri üreteceksin.
|
||
|
||
KURALLAR (kesin):
|
||
- METİN DİYETİ: her alan TEK kısa cümle, dolgu söz yok.
|
||
- gerekce: bu programın bu aday için neden mantıklı olduğu (TEK cümle,
|
||
en fazla ~15 kelime, Türkçe).
|
||
- riskNotu: yerleşme ihtimali değerlendirmesi (TEK kısa cümle). ASLA "garanti",
|
||
"kesin", "yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan.
|
||
- trendOzeti: son 4 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den
|
||
beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor").
|
||
- Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`;
|
||
|
||
function mockAktif(): boolean {
|
||
if (process.argv.includes("--mock")) return true;
|
||
return process.env.NODE_ENV !== "production" && !anahtarVar();
|
||
}
|
||
|
||
function mockMetin(p: Program, temsiliSira: number, kategori: string | null) {
|
||
return {
|
||
gerekce: `(Dev mock) ${p.isim}, ${kategori ? `${kategori} ilgisiyle ve ` : ""}~${temsiliSira.toLocaleString("tr-TR")} bandındaki sıralamanla uyumlu bir aday.`,
|
||
riskNotu:
|
||
"(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir.",
|
||
trendOzeti:
|
||
"(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.",
|
||
};
|
||
}
|
||
|
||
/** rapor.ts programBilgisi ile aynı snapshot şekli. */
|
||
function programBilgisi(p: Program) {
|
||
const gecmis = [p.sira2021, p.sira2022, p.sira2023, p.sira2024, p.sira2025];
|
||
return {
|
||
isim: p.isim,
|
||
universite: p.universite,
|
||
il: p.il,
|
||
unitur: p.unitur,
|
||
efektifSira: p.sira2025 ?? p.sira2024,
|
||
siraGecmisi: gecmis.some((s) => s != null) ? gecmis : undefined,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Deterministik program seçimi: kovanın temsili sırası için "dengeli" dilimden
|
||
* (boşsa "garanti") kategoriye uyan, 2025'te en çok yerleşen alan program.
|
||
* Tanınır/dolu programlar tadımlıkta daha ikna edici.
|
||
*/
|
||
function programSec(
|
||
temsiliSira: number,
|
||
tur: PuanTuruKey,
|
||
kategori: string | null,
|
||
): { program: Program; dilim: DilimKey } | null {
|
||
for (const dilim of ["dengeli", "garanti"] as const) {
|
||
const { programlar } = dilimAra(temsiliSira, tur, dilim, { limit: 60 });
|
||
const uygunlar = kategori
|
||
? programlar.filter((p) => kategoriEslesir(p.isim, [kategori]))
|
||
: programlar;
|
||
if (uygunlar.length === 0) continue;
|
||
const secilen = [...uygunlar].sort(
|
||
(a, b) => (b.yerlesen2025 ?? -1) - (a.yerlesen2025 ?? -1),
|
||
)[0];
|
||
return { program: secilen, dilim };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function metinUret(
|
||
p: Program,
|
||
dilim: DilimKey,
|
||
temsiliSira: number,
|
||
tur: PuanTuruKey,
|
||
kategori: string | null,
|
||
): Promise<z.infer<typeof TadimlikSchema> | null> {
|
||
if (mockAktif()) return mockMetin(p, temsiliSira, kategori);
|
||
|
||
const kullanici = [
|
||
`Temsili aday: başarı sıralaması ~${temsiliSira.toLocaleString("tr-TR")} bandında, puan türü ${turLabel(tur)}.`,
|
||
kategori ? `Adayın ilgi alanı: ${kategori}.` : null,
|
||
`PROGRAM (yalnızca bunun için metin üret): ${havuzuKompaktJson([{ ...p, dilim }])}`,
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n\n");
|
||
|
||
// Not: Gemini'de thinking bütçesi de bu sınırdan düşer; dar tutulursa
|
||
// çıktı kırpılıp şema doğrulaması boşa düşüyor.
|
||
return yapilandirilmisUret({
|
||
system: SISTEM,
|
||
kullanici,
|
||
sema: TadimlikSchema,
|
||
maxTokens: 4000,
|
||
});
|
||
}
|
||
|
||
function bayrak(ad: string): string | undefined {
|
||
const arg = process.argv.find((a) => a.startsWith(`--${ad}=`));
|
||
return arg?.slice(ad.length + 3);
|
||
}
|
||
|
||
async function main() {
|
||
const force = process.argv.includes("--force");
|
||
const kovaFiltre = bayrak("kova");
|
||
const turFiltre = bayrak("tur");
|
||
const kategoriFiltre = bayrak("kategori");
|
||
|
||
const turlar = (Object.keys(PUAN_TURLERI) as PuanTuruKey[]).filter(
|
||
(t) => !turFiltre || t === turFiltre,
|
||
);
|
||
const kategoriler: { ad: string | null; slug: string }[] = [
|
||
{ ad: null, slug: GENEL_KATEGORI },
|
||
...KATEGORILER.map((ad) => ({ ad: ad as string, slug: kategoriSlugBul(ad) })),
|
||
].filter((k) => !kategoriFiltre || k.slug === kategoriFiltre);
|
||
const kovalar = KOVALAR.filter((k) => !kovaFiltre || k.slug === kovaFiltre);
|
||
|
||
const mevcutlar = new Set(
|
||
(
|
||
await appDb
|
||
.select({
|
||
kovaSlug: schema.tadimlikHavuzu.kovaSlug,
|
||
tur: schema.tadimlikHavuzu.tur,
|
||
kategoriSlug: schema.tadimlikHavuzu.kategoriSlug,
|
||
})
|
||
.from(schema.tadimlikHavuzu)
|
||
).map((s) => `${s.kovaSlug}|${s.tur}|${s.kategoriSlug}`),
|
||
);
|
||
|
||
if (mockAktif()) {
|
||
console.log("AI anahtarı yok — mock metinlerle üretiliyor (dev).");
|
||
}
|
||
|
||
let uretilen = 0;
|
||
let atlananMevcut = 0;
|
||
let atlananBos = 0;
|
||
|
||
for (const kova of kovalar) {
|
||
for (const tur of turlar) {
|
||
for (const kategori of kategoriler) {
|
||
const anahtar = `${kova.slug}|${tur}|${kategori.slug}`;
|
||
if (!force && mevcutlar.has(anahtar)) {
|
||
atlananMevcut++;
|
||
continue;
|
||
}
|
||
|
||
const secim = programSec(kova.temsiliSira, tur, kategori.ad);
|
||
if (!secim) {
|
||
atlananBos++;
|
||
continue;
|
||
}
|
||
|
||
const metin = await metinUret(
|
||
secim.program,
|
||
secim.dilim,
|
||
kova.temsiliSira,
|
||
tur,
|
||
kategori.ad,
|
||
);
|
||
if (!metin) {
|
||
console.warn(`Şemaya uymayan çıktı, atlandı: ${anahtar}`);
|
||
atlananBos++;
|
||
continue;
|
||
}
|
||
|
||
const now = new Date();
|
||
await appDb
|
||
.insert(schema.tadimlikHavuzu)
|
||
.values({
|
||
id: randomUUID(),
|
||
kovaSlug: kova.slug,
|
||
tur,
|
||
kategoriSlug: kategori.slug,
|
||
programId: secim.program.id,
|
||
dilim: secim.dilim,
|
||
gerekce: metin.gerekce,
|
||
riskNotu: metin.riskNotu,
|
||
trendOzeti: metin.trendOzeti,
|
||
program: programBilgisi(secim.program),
|
||
ornekSira: kova.temsiliSira,
|
||
uretimAt: now,
|
||
})
|
||
.onConflictDoUpdate({
|
||
target: [
|
||
schema.tadimlikHavuzu.kovaSlug,
|
||
schema.tadimlikHavuzu.tur,
|
||
schema.tadimlikHavuzu.kategoriSlug,
|
||
],
|
||
set: {
|
||
programId: secim.program.id,
|
||
dilim: secim.dilim,
|
||
gerekce: metin.gerekce,
|
||
riskNotu: metin.riskNotu,
|
||
trendOzeti: metin.trendOzeti,
|
||
program: programBilgisi(secim.program),
|
||
ornekSira: kova.temsiliSira,
|
||
uretimAt: now,
|
||
},
|
||
});
|
||
|
||
uretilen++;
|
||
console.log(`✓ ${anahtar} → ${secim.program.isim} (${secim.dilim})`);
|
||
// Gerçek LLM'de rate limit dostu ardışık tempo
|
||
if (!mockAktif()) {
|
||
const bekleme = Number.parseInt(bayrak("bekleme") ?? "500", 10);
|
||
await new Promise((r) => setTimeout(r, bekleme));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(
|
||
`Bitti: ${uretilen} üretildi, ${atlananMevcut} mevcut atlandı, ${atlananBos} içeriksiz/şemasız atlandı.`,
|
||
);
|
||
}
|
||
|
||
main().catch((hata) => {
|
||
console.error(hata);
|
||
process.exit(1);
|
||
});
|