feat(danisman): profil onayı kredi harcamaz; yapay zekâ sağlayıcı adresi .env'den (AI_BASE_URL/AI_API_KEY)

- Bilal kararı (26 Eyl 2026): 'kaydedeyim mi?' sorusuna kısa evet/hayır
  cevabı bedelsiz. Sunucu son danışman mesajında onay ibaresini arar,
  mesaj ≤40 karakter ve evet/hayır kalıbıyla başlıyorsa kredi düşmez,
  ledger'a yazılmaz, iade yolu atlanır; X-Kredi-Dusuldu başlığıyla istemci
  bakiyeyi buna göre günceller. Gerçek soru içeren 'evet ama …' bedava geçmez.
- Bilal kararı: model Türkiye'de barındırılacak. client.ts baseURL ve
  anahtar AI_BASE_URL/AI_API_KEY'den okunur (OPENROUTER_* geriye uyumlu);
  sağlayıcı değişince kod değil .env değişir, /gizlilik aynı commit'te.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-25 10:44:44 +03:00
parent 5a04db7e4b
commit 70423853f3
4 changed files with 108 additions and 30 deletions

View File

@@ -4,6 +4,7 @@ import { getSession } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import {
spendCreditForMessage,
bedelsizMesajKaydet,
grantCredits,
krediBittiIsaretle,
} from "@/lib/credits";
@@ -28,6 +29,22 @@ const MAX_GECMIS = 12;
* C.3/10). Bedeli: "evet" cevabı bir kredi. Kapatılırsa model doğrudan kaydeder.
*/
const PROFIL_ONAY_SOR = true;
/** Danışmanın onay sorusunda geçen sabit ibare; istemci de bunu görmez, yalnız sunucu arar. */
const ONAY_SORUSU = "profiline kaydedeyim mi";
/**
* Onay mesajı mı: son danışman mesajı "kaydedeyim mi?" diye sormuş VE aday
* kısa bir evet/hayır yazmış. Bu mesaj kredi harcamaz (Bilal, 26 Eyl 2026).
* Kısa tutulur ki "evet ama İzmir'de hukuk kaç sırayla alıyor" gibi gerçek
* sorular bedava geçmesin.
*/
function onayMesajiMi(mesaj: string, sonAsistan: string | undefined): boolean {
if (!sonAsistan || !sonAsistan.toLocaleLowerCase("tr-TR").includes(ONAY_SORUSU)) return false;
const m = mesaj.trim().toLocaleLowerCase("tr-TR");
if (m.length > 40) return false;
return /^(evet|olur|tamam|kaydet|kaydedebilirsin|kaydedelim|hayır|hayir|kaydetme|gerek yok|istemiyorum|şimdilik hayır)\b/.test(m);
}
const MAX_MESAJ_UZUNLUK = 2000;
function sistemPromptu(opts: {
@@ -71,10 +88,11 @@ KURALLAR:
- 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
kabul mü, kampüs büyüklüğü) ${PROFIL_ONAY_SOR
? `HEMEN KAYDETME: cevabının sonunda tek cümleyle sor: "Bunu profiline
kaydedeyim mi? (Listen ve aramalar buna göre süzülür; istediğin zaman
silebilirsin.)" Aday evet/kaydet derse o zaman profil_guncelle ile kaydet
ve "Kaydettim: …" de. Aday açıkça "kaydet" diye yazdıysa sormadan kaydet.`
? `HEMEN KAYDETME: cevabının sonunda tek cümleyle, tam bu ibareyle sor:
"Bunu profiline kaydedeyim mi? (Evet demek kredi harcamaz; listen ve
aramalar buna göre süzülür, istediğin zaman silebilirsin.)" Aday
evet/kaydet derse o zaman profil_guncelle ile kaydet ve "Kaydettim: …" de.
Aday açıkça "kaydet" diye yazdıysa sormadan kaydet.`
: `profil_guncelle ile kaydet ve cevabının sonunda tek cümleyle söyle:
"Şunu kaydettim: …". Emin değilsen önce sor, tahminle kaydetme.`}
Bunların DIŞINDA hiçbir şeyi kaydetme ve sorma:
@@ -217,12 +235,18 @@ export async function POST(request: NextRequest) {
}
const gecmis = gecmisHam.reverse();
// Profil onayı ("kaydedeyim mi?" → "evet") kredi harcamaz; iade de yok.
const sonAsistan = [...gecmis].reverse().find((m) => m.role === "assistant")?.content;
const bedelsiz = onayMesajiMi(mesaj, sonAsistan);
// Kredi düşümü stream'den önce — yetersizse hiç başlama
const harcama = await spendCreditForMessage({
userId: session.user.id,
content: mesaj,
clientMessageId,
});
const harcama = bedelsiz
? await bedelsizMesajKaydet({ userId: session.user.id, content: mesaj, clientMessageId })
: await spendCreditForMessage({
userId: session.user.id,
content: mesaj,
clientMessageId,
});
if (!harcama.ok) {
if (harcama.error === "INSUFFICIENT") {
await krediBittiIsaretle(session.user.id);
@@ -302,12 +326,14 @@ export async function POST(request: NextRequest) {
}
if (!anahtarVar()) {
await grantCredits({
userId: session.user.id,
delta: 1,
reason: "refund",
refId: kullaniciMesajId,
});
if (!bedelsiz) {
await grantCredits({
userId: session.user.id,
delta: 1,
reason: "refund",
refId: kullaniciMesajId,
});
}
console.error("[soru] Yapay Zeka anahtarı tanımlı değil");
return Response.json(
{
@@ -371,12 +397,14 @@ export async function POST(request: NextRequest) {
});
} catch {}
}
await grantCredits({
userId,
delta: 1,
reason: "refund",
refId: kullaniciMesajId,
}).catch(() => {});
if (!bedelsiz) {
await grantCredits({
userId,
delta: 1,
reason: "refund",
refId: kullaniciMesajId,
}).catch(() => {});
}
}
console.error("[soru] stream hatası:", err);
controller.error(err);
@@ -389,6 +417,8 @@ export async function POST(request: NextRequest) {
"Content-Type": "text/plain; charset=utf-8",
"X-Content-Type-Options": "nosniff",
"Cache-Control": "no-store",
// İstemci bakiyeyi buna göre düşürür (onay mesajı bedelsiz)
"X-Kredi-Dusuldu": bedelsiz ? "0" : "1",
},
});
}