fix(codex-bridge): audit findings: visible permanent errors, stricter model command, unbuffered stream
From ten independent read-only audits (11 Oct 2026), verified against the code: - a permanent gateway error (no tools, 401/402/403, bare upstream 400) ends the turn with a visible assistant message instead of a silent retry and an empty turn; - "modeli ... yapma", "X modeli için test yap" and general "hangi model ..." questions are no longer commands; a name whose version does not fit, or that several models share, is asked back; - the stream is forwarded as it arrives (read1) and an upstream drop is no longer logged as a client stop nor saved as an interrupted answer; - a lone "." joining a running turn is removed without the hook messages after it; - the interrupted-answer note finds the user message behind hook messages and detects completion by event type; a per-minute rate limit is not learned as a context window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e2b8e2b9c
commit
e2793fa38f
7 files changed
+129
-32
No files matched your search
@@ -15,7 +15,7 @@ Data directory (bridge.json, log/): --home <dir> or AITURK_CODEX_BRIDGE_HOME,
|
||||
otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME.
|
||||
Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop.
|
||||
"""
|
||||
import http.server, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
|
||||
import http.server, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
|
||||
from gemini_context import coalesce_context
|
||||
try:
|
||||
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
|
||||
@@ -502,6 +502,25 @@ class SseRewriter:
|
||||
if d is not None and not d["item"].get("phase"):
|
||||
d["item"]["phase"] = phase
|
||||
return d
|
||||
def _gorunur_hata(self, mesaj):
|
||||
"""Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026)
|
||||
hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda
|
||||
kullanici hatayi duyar."""
|
||||
damga = "%d" % int(time.time() * 1000)
|
||||
rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga
|
||||
oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
|
||||
"content": [{"type": "output_text", "text": mesaj, "annotations": []}]}
|
||||
kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
|
||||
"output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
|
||||
return [
|
||||
{"type": "response.output_item.added", "output_index": 0,
|
||||
"item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}},
|
||||
{"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj},
|
||||
{"type": "response.output_item.done", "output_index": 0, "item": oge},
|
||||
{"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
|
||||
"output": [oge], "usage": kullanim}},
|
||||
]
|
||||
|
||||
def _sirala(self, d):
|
||||
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
|
||||
t = d.get("type", "")
|
||||
@@ -517,10 +536,7 @@ class SseRewriter:
|
||||
if pencere.aracsiz_mi(metin):
|
||||
if self.aracsiz is not None:
|
||||
self.aracsiz(self.hedef)
|
||||
return [{"type": "response.failed", "response": {
|
||||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||||
"error": {"code": "invalid_request_error", "message":
|
||||
"Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin."}}}]
|
||||
return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
|
||||
if self.dolu and pencere.kapali_hata_mi(metin):
|
||||
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
|
||||
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
|
||||
@@ -535,11 +551,12 @@ class SseRewriter:
|
||||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||||
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
|
||||
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
|
||||
kod = "insufficient_quota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "invalid_api_key"
|
||||
d = {"type": "response.failed", "response": {
|
||||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||||
"error": {"code": kod, "message": "AITURK ağ geçidi isteği reddetti: " + metin[:300]}}}
|
||||
t = "response.failed"
|
||||
neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
|
||||
return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
|
||||
f"Ayrıntı: {metin[:200]}")
|
||||
if pencere.kapali_hata_mi(metin):
|
||||
return self._gorunur_hata("Seçili model isteği reddetti ve nedenini bildirmedi. Başka bir model seçip "
|
||||
"yeniden deneyin.")
|
||||
item = d.get("item") if isinstance(d.get("item"), dict) else None
|
||||
if item and item.get("type") in ARAC_OGELERI:
|
||||
self.arac_goruldu = True
|
||||
@@ -768,7 +785,15 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
|
||||
try:
|
||||
while True:
|
||||
chunk = resp.read(4096)
|
||||
try:
|
||||
# read1: gelen kadarini hemen verir; read(4096) kucuk parcalari 4096 bayt dolana kadar bekletiyordu.
|
||||
chunk = resp.read1(4096)
|
||||
except (OSError, http.client.HTTPException) as e:
|
||||
# Ust kaynak koptu (istemci degil): yarim cevap kaydi yapilmaz, baglanti kapatilir ki Codex
|
||||
# yarim akisi tam sanmasin ve yeniden denesin.
|
||||
log(f"{tag} ust kaynak koptu ({total} bayt): {type(e).__name__}: {e}")
|
||||
self.close_connection = True
|
||||
return
|
||||
if not chunk: break
|
||||
total += len(chunk)
|
||||
if len(bas) < 600:
|
||||
@@ -1142,7 +1167,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
|
||||
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
|
||||
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
|
||||
del req["input"][model_gecis.son_sira(req["input"]):]
|
||||
del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir
|
||||
govde_degisti = True
|
||||
log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi")
|
||||
if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)):
|
||||
|
||||
@@ -26,7 +26,14 @@ EN_COK_KELIME = 14
|
||||
_KILIT = threading.Lock()
|
||||
|
||||
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
|
||||
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla")
|
||||
# Yalniz olumlu emir/istek bicimleri: "yapma", "gecme", "yapay", "secili" gibi sozler komut fiili degildir.
|
||||
FIILLER = {"gec", "gecir", "gecelim", "gecirelim", "gecer", "gecirir", "gecsene", "degis", "degistir", "degistirelim",
|
||||
"degistirir", "yap", "yapalim", "yapar", "yapsana", "sec", "secelim", "secer", "kullan", "kullanalim",
|
||||
"cevir", "cevirelim", "olsun", "ayarla", "ayarlayalim"}
|
||||
OLUMSUZ = ("yapma", "gecme", "gecirme", "degistirme", "secme", "kullanma", "cevirme", "olmasin", "ayarlama", "degisme")
|
||||
# "Hangi model daha ucuz?" genel sorudur; yalniz bu sohbetin modelini soran soz kopruden cevaplanir.
|
||||
SORU_ISARETI = {"secili", "aktif", "anki", "suanki", "sohbet", "sohbetin", "sohbette", "chat", "chatte", "chatin", "ana",
|
||||
"modelsin", "modeldesin", "modeldeyiz", "modeldeyim", "kullaniyorsun", "kullaniliyor", "calisiyor"}
|
||||
SORU = ("hangi", "hangisi", "kim", "kimdir")
|
||||
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
|
||||
"olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
|
||||
@@ -100,8 +107,10 @@ def komut_coz(metin):
|
||||
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
|
||||
return None
|
||||
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
|
||||
return {"tur": "sor"}
|
||||
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")]
|
||||
return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None
|
||||
if any(k.startswith(OLUMSUZ) for k in kelimeler):
|
||||
return None
|
||||
fiil = [k for k in kelimeler if k in FIILLER]
|
||||
if fiil:
|
||||
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil]
|
||||
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
|
||||
@@ -131,15 +140,33 @@ def eslestir(ad, modeller):
|
||||
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
|
||||
harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume))
|
||||
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
|
||||
puanli.append((tutan, -len(parcalar), slug, gorunen))
|
||||
kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")]
|
||||
hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0
|
||||
puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi))
|
||||
if not puanli:
|
||||
return None
|
||||
en = max(p[0] for p in puanli)
|
||||
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3):
|
||||
return None # sozun cogu hicbir modele uymuyor: komut degil
|
||||
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3]))
|
||||
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]:
|
||||
harfler = [s for s in ad if not s.isdigit()]
|
||||
en_harf = max(p[4] for p in puanli)
|
||||
if en_harf < len(harfler):
|
||||
return None # adin bir sozcugu hicbir modelde yok: komut degil
|
||||
iyi = sorted((p for p in puanli if p[0] == en and p[4] == len(harfler)), key=lambda p: (-p[1], p[3]))
|
||||
if not iyi:
|
||||
return None
|
||||
if en < len(ad): # sozcukler tuttu ama surum rakami uymadi: sor, tahmin etme
|
||||
return {"adaylar": [p[3] for p in iyi[:4]]}
|
||||
if len(iyi) == 1:
|
||||
return {"slug": iyi[0][2], "ad": iyi[0][3]}
|
||||
tam = [p for p in iyi if p[5]] # adinin butun parcalari soylenen model
|
||||
if tam:
|
||||
# Ayni adi tasiyanlar arasinda sira: Codex'in kendi abonelik modeli, sonra adi en uzun (en ozgul) olan,
|
||||
# sonra saglayici/model bicimindeki tekil model. Hala esitse sorulur.
|
||||
def oncelik(p):
|
||||
return (p[2].startswith("ide/"), -p[5], "/" not in p[3])
|
||||
tam.sort(key=oncelik)
|
||||
if len(tam) == 1 or oncelik(tam[0]) != oncelik(tam[1]):
|
||||
return {"slug": tam[0][2], "ad": tam[0][3]}
|
||||
return {"adaylar": [p[3] for p in tam[:4]]}
|
||||
return {"adaylar": [p[3] for p in iyi[:4]]}
|
||||
|
||||
|
||||
|
||||
@@ -146,12 +146,22 @@ def kapali_hata_mi(metin):
|
||||
return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I))
|
||||
|
||||
|
||||
_HIZ_SINIRI = re.compile(r"per minute|rate.?limit|tokens? per|\bTPM\b|\b429\b|too many requests", re.I)
|
||||
|
||||
|
||||
def hiz_siniri_mi(metin):
|
||||
"""Dakikalik hiz siniri hatasi baglam tasmasi degildir ("rate limit of 32768 tokens per minute")."""
|
||||
return bool(_HIZ_SINIRI.search(metin or ""))
|
||||
|
||||
|
||||
def tasma_mi(metin):
|
||||
return bool(_TASMA.search(metin or ""))
|
||||
return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
|
||||
|
||||
|
||||
def sinir_oku(metin):
|
||||
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
|
||||
if hiz_siniri_mi(metin):
|
||||
return None
|
||||
for kalip in _SINIR:
|
||||
m = kalip.search(metin or "")
|
||||
if m and int(m.group(1)) >= TABAN:
|
||||
|
||||
@@ -32,8 +32,11 @@ class Ek(unittest.TestCase):
|
||||
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
|
||||
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
|
||||
out = feed(flat, hedef="m", aracsiz=seen.append)
|
||||
self.assertEqual(out[0]["type"], "response.failed")
|
||||
self.assertIn("araç çağrısını desteklemiyor", out[0]["response"]["error"]["message"])
|
||||
# A permanent error ends the turn with a visible answer instead of a silent retry.
|
||||
self.assertEqual([e["type"] for e in out], ["response.output_item.added", "response.output_text.delta",
|
||||
"response.output_item.done", "response.completed"])
|
||||
self.assertIn("araç çağrısını desteklemiyor", out[1]["delta"])
|
||||
self.assertEqual(out[2]["item"]["phase"], "final_answer")
|
||||
self.assertEqual(seen, ["m"])
|
||||
home = tempfile.mkdtemp()
|
||||
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
|
||||
|
||||
@@ -29,14 +29,24 @@ class Komut(unittest.TestCase):
|
||||
def test_spoken_names_resolve_to_one_model(self):
|
||||
for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
|
||||
("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
|
||||
("modeli klod opus yap", "ide/9router-cc-claude-opus-5-5"),
|
||||
("modeli opusa geç", "ide/9router-cc-claude-opus-5-5"),
|
||||
("modeli klod opus 5 5 yap", "ide/9router-cc-claude-opus-5-5"),
|
||||
("modeli opus 5 5'e geç", "ide/9router-cc-claude-opus-5-5"),
|
||||
("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"),
|
||||
("modeli glm flaş sansürsüz yap", "ide/9router-nano-z-ai-glm-5.3-flash-uncensored")]:
|
||||
self.assertEqual((pick(text) or {}).get("slug"), slug, text)
|
||||
|
||||
def test_ambiguous_name_lists_candidates_and_changes_nothing(self):
|
||||
self.assertEqual(pick("modeli agnes yap"), {"adaylar": ["agnes/agnes-2.0-flash", "agnes/agnes-2.5-flash"]})
|
||||
self.assertIn("adaylar", pick("modeli klod opus yap")) # two Opus entries: ask
|
||||
self.assertIn("adaylar", pick("modeli sol yap")) # GPT-6-Sol or GPT-6.1-Sol: ask
|
||||
self.assertIn("adaylar", pick("modeli gpt 999 astra yap")) # words fit, version does not: ask
|
||||
|
||||
def test_negations_and_general_questions_are_not_commands(self):
|
||||
for text in ("modeli gemini flash low yapma", "modeli değiştirme opus kalsın", "GPT 6 Astra modeli için test yap",
|
||||
"hangi model daha ucuz", "en iyi model hangisi", "modeli seçili dosyaya göre gemini flash low yap"):
|
||||
result = pick(text)
|
||||
self.assertFalse(result and ("slug" in result or result.get("tur") == "sor"), text)
|
||||
self.assertEqual(g.komut_coz("hangi modelsin"), {"tur": "sor"})
|
||||
|
||||
def test_ordinary_sentences_are_not_commands(self):
|
||||
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
|
||||
@@ -77,7 +87,7 @@ class TurOrtasi(unittest.TestCase):
|
||||
class Gecmis(unittest.TestCase):
|
||||
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
|
||||
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
|
||||
voice("modeli opus yap"), user("modeli gpt 6 astra yap")]}
|
||||
voice("modeli opus 5 5 yap"), user("modeli gpt 6 astra yap")]}
|
||||
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
|
||||
self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"])
|
||||
self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
|
||||
|
||||
@@ -17,7 +17,7 @@ for node in tree.body:
|
||||
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
|
||||
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
|
||||
nodes.insert(0, node)
|
||||
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": pencere}
|
||||
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
|
||||
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
||||
|
||||
NANO = pencere.nano_sadelestir({"data": [
|
||||
@@ -196,9 +196,13 @@ class GatewayErrors(unittest.TestCase):
|
||||
adapter = env["SseRewriter"](set())
|
||||
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
|
||||
out = parse(adapter.feed(raw) + adapter.flush())
|
||||
self.assertEqual(out[0]["type"], "response.failed")
|
||||
self.assertEqual(out[0]["response"]["error"]["code"], "insufficient_quota")
|
||||
self.assertIn("upstream 402", out[0]["response"]["error"]["message"])
|
||||
self.assertEqual(out[-1]["type"], "response.completed") # visible answer, turn ends normally
|
||||
self.assertIn("kredi ya da kota", out[1]["delta"])
|
||||
self.assertIn("upstream 402", out[1]["delta"])
|
||||
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 400"}})
|
||||
out = parse(env["SseRewriter"](set()).feed(raw))
|
||||
self.assertIn("nedenini bildirmedi", out[1]["delta"])
|
||||
self.assertEqual(out[-1]["type"], "response.completed")
|
||||
# A transient error is left alone so that Codex retries it.
|
||||
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
|
||||
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
|
||||
@@ -238,6 +242,11 @@ class ClaudeOrani(unittest.TestCase):
|
||||
self.assertEqual((trimmed, fits), (1, True))
|
||||
self.assertLess(estimate, 194_000)
|
||||
self.assertTrue(p.kapali_hata_mi("upstream 400"))
|
||||
# A per-minute rate limit is neither an overflow nor a window size to learn.
|
||||
rate = "Rate limit reached: limit of 32768 tokens per minute, context window ok"
|
||||
self.assertFalse(p.tasma_mi(rate))
|
||||
self.assertIsNone(p.sinir_oku(rate))
|
||||
self.assertTrue(p.tasma_mi("maximum context length is 65536 tokens"))
|
||||
self.assertFalse(p.kapali_hata_mi("upstream 400: invalid tool schema"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -48,7 +48,15 @@ class Izleyici:
|
||||
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
|
||||
self.parcalar.append(d["delta"])
|
||||
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
|
||||
self.bitti = True
|
||||
# Bitis, olayin TURUNDEN anlasilir; arac argumaninda gecen "response.completed" sozu bitis degildir.
|
||||
for satir in blok.split(b"\n"):
|
||||
if satir.startswith(b"data:"):
|
||||
try:
|
||||
tur = json.loads(satir[5:]).get("type")
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
if tur in ("response.completed", "response.failed", "response.incomplete"):
|
||||
self.bitti = True
|
||||
|
||||
@property
|
||||
def metin(self):
|
||||
@@ -89,7 +97,12 @@ def uygula(req, anahtar, simdi=None):
|
||||
girdi = req.get("input")
|
||||
if not kayit or not isinstance(girdi, list) or not girdi:
|
||||
return 0
|
||||
son = girdi[-1]
|
||||
son = None
|
||||
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
|
||||
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
|
||||
continue
|
||||
son = oge
|
||||
break
|
||||
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
|
||||
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
|
||||
metin = kayit["metin"]
|
||||
|
||||
Reference in new issue
Block a user