AI katmanını OpenRouter tek kapısına geçir ve rapor üretimini hızlandır
All checks were successful
Deploy / deploy (push) Successful in 9m30s

- Tüm AI akışı OpenRouter üzerinden (openai SDK, varsayılan model
  deepseek/deepseek-v4-flash, OPENROUTER_MODEL ile değiştirilebilir);
  Anthropic/Gemini/lokal sağlayıcı seçimi ve SDK'ları kaldırıldı.
- Rapor: aday havuzu dilim başına 20 (60 program, ~8k token prompt);
  AI şeması sabit uzunluklu üç liste (hayal 5 / dengeli 13 / garanti 6 —
  dağılım şema düzeyinde zorlanır), sıra + dilim + riskNotu + trendOzeti
  gerçek sıra geçmişinden deterministik üretilir; 184 sn → ~12 sn.
- Sohbet: akış ortası zaman aşımı artık sessizce yutulmaz (kesik cevap
  tam cevap gibi kaydedilmiyordu, kredi iadesi çalışmıyordu); istemci
  hata anında durumu sunucudan senkronlar, boş balon bırakmaz.
- aiHataMesaji, openai SDK'nın .name'i "Error" kalan abort/timeout
  hatalarını da yakalar; meraklisina şeması yeni akışla senkronlandı.
- CI: OPENROUTER_API_KEY / OPENROUTER_MODEL secret'ları; eski AI
  secret'ları kaldırıldı. next.config serverExternalPackages sadeleşti.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-07 00:37:54 +03:00
parent 4c618781f6
commit 18d5b42136
15 changed files with 366 additions and 540 deletions

View File

@@ -15,10 +15,12 @@ import { ListemGovde } from "./listem-govde";
import { ListemBosCta } from "./listem-bos-cta";
import { ListeUretici } from "./liste-uretici";
// listeRevize aksiyonu 2 AI denemesi yapabiliyor (deneme başına 50 sn abort
// sınırı, bkz. lib/ai/cagri.ts); platform limiti AI zaman aşımından SONRA
// dolmalı ki kredi iadesi her zaman çalışabilsin.
export const maxDuration = 120;
// listeRevize aksiyonu 2 AI denemesi yapabiliyor (deneme başına 60 sn abort
// sınırı, bkz. lib/ai/cagri.ts) → en kötü ~120 sn. Platform limiti bundan
// SONRA dolmalı ki kredi iadesi her zaman çalışabilsin. Self-hosted docker'da
// bu değer etkisiz ama invaryant belgeli kalsın; öndeki reverse proxy'nin
// istek süresi limiti de >= 200 sn olmalı.
export const maxDuration = 300;
export const metadata: Metadata = {
title: "Listem",

View File

@@ -121,6 +121,7 @@ export function SohbetClient({
{ id: asistanId, role: "assistant", content: "" },
]);
let birikmis = "";
try {
const res = await fetch("/api/soru", {
method: "POST",
@@ -151,7 +152,6 @@ export function SohbetClient({
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let birikmis = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
@@ -165,7 +165,26 @@ export function SohbetClient({
// harcamasını görmez; bakiye orada da güncellensin.
router.refresh();
} catch {
toast.error("Bağlantı koptu. Cevabı sayfayı yenileyerek görebilirsin.");
// Sunucu hata anında krediyi iade edip yarım cevabı kaydetmiş olabilir;
// tahmin etme: kayıtlı geçmişi ve bakiyeyi sunucudan çek (boş balon da
// bu senkronla temizlenir). Senkron da düşerse boş balonu elle kaldır.
try {
const res = await fetch("/api/soru");
const data = res.ok ? await res.json() : null;
if (data?.mesajlar) setMesajlar(data.mesajlar);
if (typeof data?.kredi === "number") setKredi(data.kredi);
} catch {}
if (!birikmis) {
setMesajlar((m) => m.filter((x) => x.id !== asistanId));
// Cevap hiç gelmedi; kullanıcının yazdığı soru kaybolmasın
setGirdi(mesaj);
}
router.refresh();
toast.error(
birikmis
? "Bağlantı koptu — cevabın buraya kadar üretilen kısmı kaydedildi."
: "Bağlantı koptu, cevap alınamadı. Kredin harcandıysa otomatik iade edildi.",
);
} finally {
setBekliyor(false);
}

View File

@@ -18,8 +18,9 @@ flowchart TD
H --> S{"Havuzda 24'ten az program mı kaldı?"}
S -- "evet" --> G("Filtreler adım adım gevşetilir<br/>önce il, sonra alan — listede açıkça belirtilir")
G --> H
S -- "hayır" --> Y("Yapay zekâ havuzdan tam 24 tercih seçer<br/>havuzda olmayan bölümü yazamaz, her satıra gerekçe + risk notu ekler")
Y --> L("24 tercihlik liste")
S -- "hayır" --> Y("Yapay zekâ havuzdan tam 24 tercih seçer<br/>havuzda olmayan bölümü yazamaz, her satıra gerekçe yazar")
Y --> R("Risk notu ve trend özeti<br/>gerçek sıralama geçmişinden hesaplanır, yapay zekâ yorumu değildir")
R --> L("24 tercihlik liste")
L --> L1("üstte 46 hayal")
L --> L2("ortada 1214 dengeli")
L --> L3("sonda 57 garanti")
@@ -39,7 +40,7 @@ export function HuniSemasi() {
return (
<ElCizimiSema
tanim={SEMA}
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuz 24'ün altına düşerse filtreler gevşetilir, sonra yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda garanti olacak şekilde 24 tercih seçer."
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; havuz 24'ün altına düşerse filtreler gevşetilir, sonra yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda garanti olacak şekilde 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
figcaption={
<>
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}

View File

@@ -43,8 +43,15 @@ function siraTurGecerli(sira: number, tur: string): tur is PuanTuruKey {
function aiHataMesaji(err: unknown): string {
if (err instanceof RaporUretimHatasi) return err.message;
// AbortSignal.timeout: DOMException "TimeoutError"; SDK'lar abort'u kendi
// hata sınıflarıyla sarabiliyor (ör. APIUserAbortError) — ada göre yakala.
if (err instanceof Error && /abort|timeout/i.test(err.name)) {
// hata sınıflarıyla sarabiliyor (ör. APIUserAbortError) ve openai/anthropic
// bu sınıflarda .name'i "Error" bırakıyor — ada ek olarak sınıf adına ve
// mesaja da bak ("Request was aborted." / "Request timed out.").
if (
err instanceof Error &&
/abort|timeout|timed out/i.test(
`${err.name} ${err.constructor.name} ${err.message}`,
)
) {
return "Liste üretimi bu sefer çok uzun sürdü ve durduruldu; harcanan kredin otomatik iade edildi. Tekrar dener misin?";
}
if (err instanceof Error && err.message === "AI_KEY_MISSING") {