diff --git a/docs/DEVIR.md b/docs/DEVIR.md
index 6cec79a..c720514 100644
--- a/docs/DEVIR.md
+++ b/docs/DEVIR.md
@@ -134,7 +134,12 @@ düğmesiyle metni çıkarılıp şartname analizine gidiyor. DT kayıtlarında
- **Volume:** `/app/server/data` → `history.sqlite`, `dt.sqlite`, `users.sqlite`
- **Girilmiş sırlar:** `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `RESEND_API_KEY`,
- `OPENROUTER_API_KEY`, `GOOGLE_CLIENT_ID`
+ `ZAI_API_KEY`, `OPENROUTER_API_KEY` (yalnız yedek), `GOOGLE_CLIENT_ID`
+- **Yapay zekâ (2026-09-10):** birincil sağlayıcı Z.ai Coding Plan. `ZAI_BASE_URL`
+ = `https://api.z.ai/api/coding/paas/v4/chat/completions`; aynı anda en fazla 2 istek
+ (`ZAI_MAX_CONCURRENCY`), GLM 5.x'te `reasoning_effort: low`. Şartname analizi
+ GLM 5.3 → GLM 5.3 Flash, TED çevirisi GLM 5.3 Flash → GLM 5.3; ikisi de bitince
+ OpenRouter zincirine düşer. Ayrıntı: `server/zai.mjs`.
- **E-posta:** `bildirim@ihaleist.com` (Resend, eu-west-1, doğrulanmış)
- **Zamanlayıcı:** geçmiş tarama 6 sa · DT 4 sa · bildirim 6 sa
diff --git a/server/ai.mjs b/server/ai.mjs
index 2eea7aa..1700abd 100644
--- a/server/ai.mjs
+++ b/server/ai.mjs
@@ -1,16 +1,20 @@
import { analyzeInParts } from './analysis-parts.mjs';
import { qualifyAnalysis } from './analysis-quality.mjs';
+import { zaiChat } from './zai.mjs';
/**
- * Gerçek AI şartname analizi — OpenRouter öncelikli, çoklu sağlayıcı fallback.
+ * Gerçek AI şartname analizi — Z.ai (GLM) birincil, çoklu sağlayıcı fallback.
* Bağımlılık yok (Node 18+ built-in fetch).
*
- * EN KOLAY KURULUM — tek anahtar, tüm modeller:
- * OPENROUTER_API_KEY=sk-or-v1-...
- * Bu tanımlıysa zincirdeki tüm modeller OpenRouter üzerinden çağrılır.
+ * BİRİNCİL KURULUM (2026-09'dan beri):
+ * ZAI_API_KEY=... GLM 5.3 ve GLM 5.3 Flash doğrudan Z.ai'den
+ * ZAI_BASE_URL=... Coding Plan ucu (bkz. zai.mjs)
*
- * İsteğe bağlı doğrudan sağlayıcı anahtarları (OpenRouter'ı ezer):
- * ZAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY,
- * XAI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY
+ * YEDEK — tek anahtar, kalan tüm modeller:
+ * OPENROUTER_API_KEY=sk-or-v1-...
+ * Doğrudan anahtarı olmayan sağlayıcılar OpenRouter üzerinden çağrılır.
+ *
+ * İsteğe bağlı diğer doğrudan anahtarlar (OpenRouter'ı ezer):
+ * DEEPSEEK_API_KEY, GEMINI_API_KEY, XAI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY
*
* Model kimlikleri env ile değiştirilebilir (ör. GLM_MODEL, GLM_OR_MODEL).
*/
@@ -18,11 +22,17 @@ import { qualifyAnalysis } from './analysis-quality.mjs';
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
export const PROVIDERS = {
+ zai: {
+ label: 'GLM 5.3', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
+ model: () => process.env.GLM_MODEL || 'glm-5.3',
+ or: () => process.env.GLM_OR_MODEL || '~z-ai/glm-latest',
+ style: 'zai',
+ },
glmflash: {
label: 'GLM 5.3 Flash', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
model: () => process.env.GLM_FLASH_MODEL || 'glm-5.3-flash',
or: () => process.env.GLM_FLASH_OR_MODEL || 'z-ai/glm-5.3-flash',
- url: 'https://api.z.ai/api/paas/v4/chat/completions', style: 'openai',
+ style: 'zai',
},
gemini: {
label: 'Gemini', provider: 'Google', envKey: 'GEMINI_API_KEY',
@@ -331,7 +341,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
// modeller (ör. ücretsiz/limitli olanlar) tıkanınca analiz boş kalmasın;
// yalnız üsttekilerin hepsi başarısız olursa devreye girer.
const YEDEK = 'gemini';
- const secim = (Array.isArray(chain) && chain.length ? chain : ['glmflash', 'gemini', 'deepseek', 'grok', 'gpt', 'claude'])
+ const secim = (Array.isArray(chain) && chain.length ? chain : ['zai', 'glmflash', 'gemini', 'deepseek', 'grok', 'gpt', 'claude'])
.filter((id) => PROVIDERS[id]);
const order = secim.includes(YEDEK) ? secim : [...secim, YEDEK];
// Derin analiz hem daha çok metin okur hem daha uzun JSON üretir; çıktı
@@ -358,6 +368,12 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
let raw;
if (route.yol === 'openrouter') {
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal, sistem, maxTokens);
+ } else if (p.style === 'zai') {
+ // Kullanıcı bekliyor: eşzamanlılık sırasında arka plan çevirisinin önüne geçer.
+ raw = await zaiChat(route.model, [
+ { role: 'system', content: sistem },
+ { role: 'user', content: userPrompt },
+ ], { signal: ac.signal, maxTokens, temperature: 0.2, json: true, oncelik: 'yuksek' });
} else if (p.style === 'gemini') {
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
} else if (p.style === 'anthropic') {
diff --git a/server/site-scan.mjs b/server/site-scan.mjs
index 698b564..dcfb9df 100644
--- a/server/site-scan.mjs
+++ b/server/site-scan.mjs
@@ -92,7 +92,7 @@ KURALLAR:
*/
export async function scanSite(inputUrl) {
if (!aiConfigured()) {
- throw new Error('Site analizi için AI yapılandırılmamış (OPENROUTER_API_KEY gerekli).');
+ throw new Error('Site analizi için AI yapılandırılmamış (ZAI_API_KEY veya OPENROUTER_API_KEY gerekli).');
}
const url = normalizeUrl(inputUrl);
const ana = await fetchPage(url.href);
diff --git a/server/ted-ceviri.mjs b/server/ted-ceviri.mjs
index 889723d..88532ea 100644
--- a/server/ted-ceviri.mjs
+++ b/server/ted-ceviri.mjs
@@ -1,7 +1,8 @@
/**
* TED ilan başlık/açıklamalarının Türkçesi.
* 1) TED çok dilli alanında Türkçe varsa (tur) doğrudan o kullanılır — model gerekmez.
- * 2) Yoksa OpenRouter ile toplu çevrilir (bir sayfa = tek istek), sonuç
+ * 2) Yoksa model ile toplu çevrilir (6 ilan = tek istek): önce Z.ai GLM
+ * (ZAI_TRANSLATE_MODELS), o da olmazsa OpenRouter. Sonuç
* server/data/ted.sqlite'ta ilan numarasına göre saklanır; ikinci görüntüleme ücretsizdir.
* Çeviri başarısız olursa özgün metin döner; liste asla bekletilmez (üst sınır ~40 sn).
*/
@@ -9,6 +10,7 @@ import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { openRouterKey } from './ai.mjs';
+import { zaiKey, zaiChat, zaiTranslateModels } from './zai.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, 'data');
@@ -44,33 +46,51 @@ function yaz(ikn, baslik_tr, aciklama_tr, kaynak) {
else bellek.set(ikn, { baslik_tr, aciklama_tr });
}
-// Ölçüm (6 ilan): deepseek-v4-flash 10 sn/851 token; glm-5.3-flash 71 sn boyunca
-// 3000 "düşünme" tokenı tüketip boş dönüyor; gemini-flash-latest OpenRouter'da
-// geçersiz kimlik. Bu yüzden sıra: deepseek → gemini 3.7 flash → glm.
-const MODELLER = ['deepseek/deepseek-v4-flash', 'google/gemini-3.7-flash', 'z-ai/glm-5.3-flash'];
+// OpenRouter yedek sırası (yalnız Z.ai başarısız olursa ve OPENROUTER_API_KEY
+// varsa). Ölçüm (6 ilan, OpenRouter): deepseek-v4-flash 10 sn/851 token;
+// glm-5.3-flash OpenRouter üzerinden 71 sn düşünüp boş dönüyor (doğrudan
+// Z.ai'de reasoning_effort:low ile bu sorun yok, bkz. zai.mjs).
+const OR_MODELLER = ['deepseek/deepseek-v4-flash', 'google/gemini-3.7-flash', 'z-ai/glm-5.3-flash'];
const SISTEM = 'Sen kamu ihale terminolojisine hâkim bir çevirmensin. Verilen AB (TED) ihale başlık ve açıklamalarını doğal, resmî Türkçeye çevir. Başlık "Ülke – Konu – İlan adı" biçimindedir: ülke adını da Türkçeleştir (Austria→Avusturya). Kurum adlarını çevirme, sayıları/tarihleri aynen koru. Yalnızca JSON döndür: {"ceviriler":[{"id":"...","baslik":"...","aciklama":"..."}]} — id alanını aynen kopyala; açıklama yoksa boş string.';
+/** Çeviri yapılabilir mi: Z.ai ya da OpenRouter anahtarından biri yeter. */
+export const ceviriMumkun = () => Boolean(zaiKey() || openRouterKey());
+
+function cevirileriAyikla(icerik) {
+ const s = icerik.slice(icerik.indexOf('{'), icerik.lastIndexOf('}') + 1);
+ const out = JSON.parse(s);
+ if (!Array.isArray(out?.ceviriler)) throw new Error('beklenen JSON değil');
+ return out.ceviriler;
+}
+
+async function openRouterdanCevir(model, key, mesajlar, signal) {
+ const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
+ method: 'POST', signal,
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://ihaleist.com', 'X-Title': 'ihaleist' },
+ body: JSON.stringify({ model, temperature: 0.1, max_tokens: 2500, response_format: { type: 'json_object' }, messages: mesajlar }),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return data?.choices?.[0]?.message?.content || '';
+}
+
+/** Sırayla dener: Z.ai modelleri → OpenRouter modelleri. İlk geçerli JSON döner. */
async function modeldenCevir(kalemler, signal) {
- const key = openRouterKey();
- if (!key) throw new Error('OPENROUTER_API_KEY yok');
+ if (!ceviriMumkun()) throw new Error('ZAI_API_KEY veya OPENROUTER_API_KEY yok');
const girdi = JSON.stringify({ ilanlar: kalemler.map((k) => ({ id: k.id, baslik: k.baslik, aciklama: (k.aciklama || '').slice(0, 600) })) });
+ const mesajlar = [{ role: 'system', content: SISTEM }, { role: 'user', content: girdi }];
+ const adimlar = [];
+ if (zaiKey()) for (const model of zaiTranslateModels()) {
+ // Arka plan işi: kullanıcının beklediği analiz istekleri sırada öne geçer.
+ adimlar.push(() => zaiChat(model, mesajlar, { signal, temperature: 0.1, maxTokens: 2500, json: true, oncelik: 'dusuk' }));
+ }
+ if (openRouterKey()) for (const model of OR_MODELLER) {
+ adimlar.push(() => openRouterdanCevir(model, openRouterKey(), mesajlar, signal));
+ }
let sonHata = null;
- for (const model of MODELLER) {
- try {
- const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
- method: 'POST', signal,
- headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://ihaleist.com', 'X-Title': 'ihaleist' },
- body: JSON.stringify({ model, temperature: 0.1, max_tokens: 2500, response_format: { type: 'json_object' },
- messages: [{ role: 'system', content: SISTEM }, { role: 'user', content: girdi }] }),
- });
- const data = await res.json().catch(() => ({}));
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
- const icerik = data?.choices?.[0]?.message?.content || '';
- const s = icerik.slice(icerik.indexOf('{'), icerik.lastIndexOf('}') + 1);
- const out = JSON.parse(s);
- if (!Array.isArray(out?.ceviriler)) throw new Error('beklenen JSON değil');
- return out.ceviriler;
- } catch (e) { sonHata = e; }
+ for (const adim of adimlar) {
+ signal?.throwIfAborted();
+ try { return cevirileriAyikla(await adim()); } catch (e) { if (e?.name === 'AbortError') throw e; sonHata = e; }
}
throw sonHata || new Error('çeviri başarısız');
}
@@ -111,12 +131,13 @@ export async function tedCevir(tenders, { blockMs = 12000 } = {}) {
if (c && c.baslik_tr) { t.baslikTr = c.baslik_tr; t.aciklamaTr = c.aciklama_tr || null; t.ceviriKaynagi = 'onbellek'; continue; }
if (!devamEden.has(t.ikn)) bekleyen.push(t);
}
- if (!openRouterKey()) return 0;
+ if (!ceviriMumkun()) return 0;
if (!bekleyen.length) return tenders.filter((t) => !t.baslikTr).length;
for (const t of bekleyen) devamEden.add(t.ikn);
const parcalar = [];
for (let i = 0; i < bekleyen.length; i += 6) parcalar.push(bekleyen.slice(i, i + 6));
- // Parçalar 3'lü eşzamanlı işlenir: 30 ilanlık liste ~5 yerine ~2 model turunda biter.
+ // Parçalar 2'li eşzamanlı işlenir: Z.ai Coding Plan aynı anda en fazla 2
+ // isteğe izin veriyor; fazlası zaten zai.mjs'teki sınırda beklerdi.
const is = (async () => {
const kuyruk = [...parcalar];
const isci = async () => {
@@ -125,7 +146,7 @@ export async function tedCevir(tenders, { blockMs = 12000 } = {}) {
try { await parcayiCevir(parca); } catch (e) { console.warn('[ted-ceviri] parça atlandı:', e.message); }
}
};
- await Promise.all(Array.from({ length: Math.min(3, parcalar.length) }, isci));
+ await Promise.all(Array.from({ length: Math.min(2, parcalar.length) }, isci));
})();
await Promise.race([is, new Promise((r) => setTimeout(r, blockMs))]);
return tenders.filter((t) => !t.baslikTr).length;
diff --git a/server/zai.mjs b/server/zai.mjs
new file mode 100644
index 0000000..8435f0a
--- /dev/null
+++ b/server/zai.mjs
@@ -0,0 +1,138 @@
+/**
+ * Z.ai (GLM) doğrudan istemcisi — uygulamanın BİRİNCİL yapay zekâ sağlayıcısı.
+ *
+ * Neden: 2026-09'da OpenRouter anahtarı iptal olup hesap bakiyesi eksiye
+ * düşünce şartname analizi, TED çevirisi ve site taraması durdu. Z.ai'nin
+ * kendi API'si (Coding Plan aboneliği) ayrı faturalanır; OpenRouter'daki bir
+ * sorun artık uygulamayı durdurmaz. OpenRouter yalnız yedek olarak kalır.
+ *
+ * Ortam değişkenleri:
+ * ZAI_API_KEY zorunlu (yoksa Z.ai devre dışı)
+ * ZAI_BASE_URL varsayılan genel API; Coding Plan için
+ * https://api.z.ai/api/coding/paas/v4/chat/completions
+ * ZAI_REASONING_EFFORT glm-5.x düşünme seviyesi (varsayılan low)
+ * ZAI_MAX_CONCURRENCY eşzamanlı istek sınırı (varsayılan 2)
+ * ZAI_RETRIES_429 oran sınırında aynı modelle yeniden deneme (varsayılan 4)
+ * ZAI_RETRY_BASE_MS yeniden deneme taban beklemesi (varsayılan 3000)
+ * ZAI_TRANSLATE_MODELS çeviri model listesi (virgülle; varsayılan glm-5.3-flash,glm-5.3)
+ */
+
+const VARSAYILAN_URL = 'https://api.z.ai/api/paas/v4/chat/completions';
+export const DEFAULT_ZAI_TRANSLATE_MODELS = ['glm-5.3-flash', 'glm-5.3'];
+
+/** Anahtar çağrı anında okunur; süreç yeniden başlatılmadan da ayar değişebilir. */
+export const zaiKey = () => (process.env.ZAI_API_KEY || '').trim() || null;
+export const zaiUrl = () => (process.env.ZAI_BASE_URL || '').trim() || VARSAYILAN_URL;
+
+export function zaiTranslateModels() {
+ const liste = (process.env.ZAI_TRANSLATE_MODELS || '').split(',').map((m) => m.trim()).filter(Boolean);
+ return liste.length ? liste : DEFAULT_ZAI_TRANSLATE_MODELS;
+}
+
+/**
+ * Düşünme (reasoning) ayarı modele göre değişir — 2026-09-10 canlı ölçümü:
+ * - glm-4.x: `thinking: {type:"disabled"}` kabul edilir.
+ * - glm-5.x: düşünme KAPATILAMAZ (hata 1210). `reasoning_effort: "low"` ile
+ * 0 düşünme tokenı ve kusursuz kaliteyle, varsayılan ayarın ~5 katı hızda
+ * çalışır. null → sağlayıcı varsayılanı (en yavaş).
+ */
+export function dusunmeAyari(model, effort) {
+ if (/^glm-5/i.test(model)) {
+ if (effort === null) return {};
+ return { reasoning_effort: effort || (process.env.ZAI_REASONING_EFFORT || '').trim() || 'low' };
+ }
+ return { thinking: { type: 'disabled' } };
+}
+
+/**
+ * Z.ai 429 yanıtını ikiye ayırır: 1113 (bakiye/abonelik yetersiz) beklemekle
+ * düzelmez; 1302/1305 (oran sınırı / sunucu yoğun) kısa beklemeyle geçebilir.
+ */
+export const gecici429 = (govde) => !/"code"\s*:\s*"?1113"?/.test(String(govde || ''));
+
+/**
+ * Süreç genelinde eşzamanlı Z.ai isteği sınırı. Coding Plan aynı anda en
+ * fazla 2 isteğe izin veriyor (3. istek 1302 alıyor). TED çevirisi, analiz ve
+ * site taraması aynı anda çalışabildiği için sınır tek noktada uygulanır.
+ * Öncelik: boşalan yer önce kullanıcının beklediği isteklere ("yuksek")
+ * verilir; arka plan çevirisi ("dusuk") en fazla bir istek süresi bekler.
+ */
+let aktifIstek = 0;
+const kuyruk = { yuksek: [], dusuk: [] };
+const eszamanliSinir = () => Math.max(1, Number(process.env.ZAI_MAX_CONCURRENCY ?? 2) || 2);
+async function yerAl(oncelik) {
+ if (aktifIstek < eszamanliSinir()) { aktifIstek++; return; }
+ await new Promise((r) => (kuyruk[oncelik] || kuyruk.dusuk).push(r));
+}
+function yerBirak() {
+ const siradaki = kuyruk.yuksek.shift() ?? kuyruk.dusuk.shift();
+ if (siradaki) siradaki(); // yer doğrudan sıradakine devredilir
+ else aktifIstek = Math.max(0, aktifIstek - 1);
+}
+/** Test ve gözlem için: o an açık istek sayısı. */
+export const zaiAktifIstek = () => aktifIstek;
+
+export class ZaiError extends Error {
+ constructor(message, status) { super(message); this.name = 'ZaiError'; this.status = status; }
+}
+
+const bekle = (ms, signal) => new Promise((resolve, reject) => {
+ if (signal?.aborted) return reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
+ const t = setTimeout(() => { signal?.removeEventListener('abort', iptal); resolve(); }, ms);
+ const iptal = () => { clearTimeout(t); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); };
+ signal?.addEventListener('abort', iptal, { once: true });
+});
+
+/**
+ * Tek bir modele istek atar, yanıt metnini döndürür.
+ * - Geçici 429'da artan beklemeyle AYNI modelle yeniden dener; 1113'te
+ * beklemeden hata fırlatır (çağıran sıradaki modele geçer).
+ * - Yer yalnız HTTP isteği sürerken tutulur; 429 beklemesinde bırakılır.
+ * - `signal` iptal edilirse sırada beklerken bile istek atılmaz.
+ */
+export async function zaiChat(model, messages, opts = {}) {
+ const {
+ temperature = 0.2, json = true, maxTokens = 4000, signal, reasoningEffort,
+ oncelik = 'yuksek',
+ retries429 = Math.max(0, Number(process.env.ZAI_RETRIES_429 ?? 4) || 0),
+ } = opts;
+ const key = zaiKey();
+ if (!key) throw new ZaiError('ZAI_API_KEY yok');
+
+ for (let deneme = 0; ; deneme++) {
+ signal?.throwIfAborted();
+ let durum = 0, govde = '';
+ await yerAl(oncelik);
+ try {
+ signal?.throwIfAborted();
+ const res = await fetch(zaiUrl(), {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model, messages, temperature, max_tokens: maxTokens, stream: false,
+ ...dusunmeAyari(model, reasoningEffort),
+ ...(json ? { response_format: { type: 'json_object' } } : {}),
+ }),
+ signal,
+ });
+ durum = res.status;
+ govde = await res.text();
+ } finally {
+ yerBirak();
+ }
+ if (durum < 200 || durum >= 300) {
+ const kisa = govde.slice(0, 200);
+ if (durum === 429 && deneme < retries429 && gecici429(kisa)) {
+ const taban = Math.max(0, Number(process.env.ZAI_RETRY_BASE_MS ?? 3000) || 0);
+ await bekle(taban * (deneme + 1) + Math.floor(Math.random() * (taban / 3)), signal);
+ continue;
+ }
+ throw new ZaiError(`${model}: HTTP ${durum} ${kisa}`, durum);
+ }
+ let mesaj;
+ try { mesaj = JSON.parse(govde)?.choices?.[0]?.message; } catch { throw new ZaiError(`${model}: yanıt JSON değil`); }
+ const icerik = typeof mesaj?.content === 'string' ? mesaj.content : '';
+ if (!icerik.trim()) throw new ZaiError(`${model}: boş içerik`);
+ return icerik;
+ }
+}
diff --git a/src/components/tools/AiModelSettings.jsx b/src/components/tools/AiModelSettings.jsx
index 07b8906..129f10a 100644
--- a/src/components/tools/AiModelSettings.jsx
+++ b/src/components/tools/AiModelSettings.jsx
@@ -70,7 +70,7 @@ export const AiModelSettings = () => {
- AI anahtarı yok. Sunucuya tek bir OPENROUTER_API_KEY ekleyin —
- aşağıdaki tüm modeller bu tek anahtarla çalışır. Aşağıdaki sıralama yalnızca tercih kaydeder;
- çağrılar anahtar eklendiğinde başlar.
+ AI anahtarı yok. Sunucuya ZAI_API_KEY (birincil, GLM modelleri)
+ ve isteğe bağlı yedek OPENROUTER_API_KEY ekleyin.
+ Aşağıdaki sıralama yalnızca tercih kaydeder; çağrılar anahtar eklendiğinde başlar.