beast-agent 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.js CHANGED
@@ -8,7 +8,7 @@ const dns = require('dns');
8
8
  const crypto = require('crypto');
9
9
  const { spawn } = require('child_process');
10
10
  const { Engine, OBSERVE_MARK } = require('./agent/engine');
11
- const { loadBeastConfig, beastDir } = require('./agent/config');
11
+ const { loadBeastConfig, parseEnvFile, beastDir } = require('./agent/config');
12
12
  const bots = require('./agent/bots');
13
13
  const mqueue = require('./agent/mqueue');
14
14
  const memory = require('./agent/memory');
@@ -560,20 +560,114 @@ function isOwnerSender(senderNum) {
560
560
 
561
561
  let sttPipeline = null;
562
562
  let sttLoading = null;
563
+ let sttModel = null;
564
+
565
+ function sttModelName() {
566
+ /* Yerel motor SADECE whisper-large-v3-turbo: Hermes'in bulut varsayılanıyla
567
+ aynı model (q4f16 ONNX). Başka bir yerel STT (tiny/base/small) KULLANILMAZ. */
568
+ return String(settings.sttModel || process.env.BEAST_STT_MODEL || 'onnx-community/whisper-large-v3-turbo').trim();
569
+ }
570
+
571
+ /* ---------- STT sağlayıcı zinciri (Hermes transcription_tools.py port) ----------
572
+ Hermes otodetect sırası: groq → openai → local; varsayılan bulut motoru
573
+ Groq whisper-large-v3-turbo (STT_GROQ_MODEL varsayılanı). Anahtar sırası:
574
+ settings → %APPDATA%\beast\.env → process.env (Hermes env adları birebir). */
575
+ const STT_GROQ_BASE = 'https://api.groq.com/openai/v1';
576
+ const STT_OPENAI_BASE = 'https://api.openai.com/v1';
577
+
578
+ function sttEnvKey(name) {
579
+ try {
580
+ const env = parseEnvFile(path.join(beastDir(), '.env'));
581
+ return String(env[name] || '').trim();
582
+ } catch {
583
+ return '';
584
+ }
585
+ }
586
+ function sttGroqKey() {
587
+ return String(settings.sttGroqKey || sttEnvKey('GROQ_API_KEY') || process.env.GROQ_API_KEY || '').trim();
588
+ }
589
+ function sttOpenaiKey() {
590
+ return String(settings.sttOpenaiKey || sttEnvKey('OPENAI_API_KEY') || process.env.OPENAI_API_KEY || '').trim();
591
+ }
592
+ function sttBaseUrl() {
593
+ return String(settings.sttBaseUrl || process.env.STT_OPENAI_BASE_URL || '').trim();
594
+ }
595
+ function sttProvider() {
596
+ const explicit = String(settings.sttProvider || '').trim().toLowerCase();
597
+ if (explicit === 'groq' || explicit === 'openai' || explicit === 'local') return explicit;
598
+ if (sttGroqKey()) return 'groq';
599
+ if (sttOpenaiKey() || sttBaseUrl()) return 'openai';
600
+ return 'local';
601
+ }
602
+
603
+ function sttEngineLabel() {
604
+ const p = sttProvider();
605
+ if (p === 'groq') {
606
+ const model = String(settings.sttGroqModel || process.env.STT_GROQ_MODEL || 'whisper-large-v3-turbo').trim();
607
+ return 'Groq ' + model + ' (Hermes STT motoru)';
608
+ }
609
+ if (p === 'openai') {
610
+ const base = sttBaseUrl() || STT_OPENAI_BASE;
611
+ const model = String(settings.sttOpenaiModel || process.env.STT_OPENAI_MODEL || 'whisper-1').trim();
612
+ return 'OpenAI-multipart ' + model + ' @ ' + base + ' (Hermes STT motoru)';
613
+ }
614
+ return 'yerel whisper-large-v3-turbo (' + (sttModel || sttModelName()) + ')';
615
+ }
616
+
617
+ /* Hermes _transcribe_groq / _transcribe_openai karşılığı: OpenAI-multipart upload.
618
+ Ham webm/ogg blobu gider; Groq whisper-large-v3-turbo webm/opus'u yerli yerinde alır. */
619
+ async function transcribeCloud(provider, buf, mimetype, lang) {
620
+ const key = provider === 'groq' ? sttGroqKey() : sttOpenaiKey();
621
+ const base = (provider === 'groq' ? STT_GROQ_BASE : sttBaseUrl() || STT_OPENAI_BASE).replace(/\/+$/, '');
622
+ const model = provider === 'groq'
623
+ ? String(settings.sttGroqModel || process.env.STT_GROQ_MODEL || 'whisper-large-v3-turbo').trim()
624
+ : String(settings.sttOpenaiModel || process.env.STT_OPENAI_MODEL || 'whisper-1').trim();
625
+ const mime = String(mimetype || 'audio/webm');
626
+ const ext = /mp3/.test(mime) ? 'mp3' : /wav|x-wav/.test(mime) ? 'wav' : /ogg/.test(mime) ? 'ogg' : /mp4|m4a/.test(mime) ? 'm4a' : 'webm';
627
+ const form = new FormData();
628
+ form.append('file', new Blob([buf], { type: mime }), 'audio.' + ext);
629
+ form.append('model', model);
630
+ form.append('response_format', 'json');
631
+ if (lang === 'tr' || lang === 'en') form.append('language', lang);
632
+ const res = await fetch(base + '/audio/transcriptions', {
633
+ method: 'POST',
634
+ headers: { Authorization: 'Bearer ' + key },
635
+ body: form,
636
+ });
637
+ if (!res.ok) throw new Error(provider + ' ' + res.status + ': ' + String(await res.text()).slice(0, 180));
638
+ const data = await res.json();
639
+ return String((data && data.text) || '').trim();
640
+ }
563
641
 
564
642
  async function ensureStt() {
565
- if (sttPipeline) return sttPipeline;
566
- if (!sttLoading) {
643
+ const model = sttModelName();
644
+ if (sttPipeline && sttModel === model) return sttPipeline;
645
+ if (!sttLoading || sttModel !== model) {
646
+ const wanted = model;
567
647
  sttLoading = (async () => {
568
- waLog('STT: yerel whisper modeli hazırlanıyor (ilk kullanımda indirilir)');
569
- const { pipeline, env } = require('@xenova/transformers');
648
+ waLog('STT: whisper-large-v3-turbo hazırlanıyor (ilk kurulumda arka planda iner)');
570
649
  const modelsDir = path.join(APP_DIR, 'models');
571
650
  fs.mkdirSync(modelsDir, { recursive: true });
572
- env.cacheDir = modelsDir;
573
- env.allowLocalModels = false;
574
- sttPipeline = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny', { quantized: true });
575
- waLog('STT hazır');
576
- return sttPipeline;
651
+ /* @huggingface/transformers: turbo repo yalnız bu paketle uyumlu.
652
+ Sırayla en sıkıştırılmış dtype denenir; BAŞKA MODEL falllback YOK —
653
+ yüklenemezse STT temiz hata verir (dandık STT'ye düşülmez). */
654
+ const hf = require('@huggingface/transformers');
655
+ hf.env.cacheDir = modelsDir;
656
+ hf.env.allowLocalModels = false;
657
+ let lastErr = null;
658
+ for (const dtype of ['q4f16', 'q4', 'q8']) {
659
+ try {
660
+ const p = await hf.pipeline('automatic-speech-recognition', wanted, { dtype });
661
+ sttPipeline = p;
662
+ sttModel = wanted;
663
+ waLog('STT hazır: ' + wanted + ' (dtype ' + dtype + ')');
664
+ return sttPipeline;
665
+ } catch (e) {
666
+ lastErr = e;
667
+ waLog('STT dtype ' + dtype + ' başarısız: ' + String((e && e.message) || e).slice(0, 140));
668
+ }
669
+ }
670
+ throw lastErr || new Error('whisper-large-v3-turbo yüklenemedi');
577
671
  })().catch((e) => {
578
672
  sttLoading = null;
579
673
  throw e;
@@ -610,27 +704,206 @@ function decodeAudioToPcm16k(buf) {
610
704
  });
611
705
  }
612
706
 
613
- async function transcribeAudio(buf, langOverride /* 'tr' | 'en' | 'auto' */) {
707
+ /* ---------- Hermes anti-hallosinasyon katmanı (tools/voice_mode.py + transcription_tools.py portu) ----------
708
+
709
+ Whisper sessizlik/gürültüde "Thank you.", "You" gibi junk üretir. Üç katman:
710
+ 1) Enerji kapısı: konuşma enerjisi yoksa model HİÇ çağrılmaz (Silero VAD muadili)
711
+ 2) condition_on_previous_text=False muadili: transformers.js chunk çağrısında
712
+ birikimli bağlam yok — tek chunk tek karar
713
+ 3) Hallosinasyon filtresi: bilinen junk cümleleri + tekrar kalıpları elenir */
714
+
715
+ const STT_SILENCE_RMS = 200 / 32768; // Hermes SILENCE_RMS_THRESHOLD (int16 200)
716
+ const STT_MIN_SPEECH_MS = 200; // bu kadar konuşma yoksa "sessiz" say
717
+
718
+ function hasSpeechEnergy(f32) {
719
+ if (!f32 || !f32.length) return false;
720
+ const win = 1600; // 100ms @ 16kHz
721
+ let speechFrames = 0;
722
+ for (let i = 0; i < f32.length; i += win) {
723
+ let sum = 0;
724
+ const end = Math.min(i + win, f32.length);
725
+ for (let j = i; j < end; j++) sum += f32[j] * f32[j];
726
+ const rms = Math.sqrt(sum / Math.max(1, end - i));
727
+ if (rms >= STT_SILENCE_RMS) speechFrames++;
728
+ }
729
+ return speechFrames * 100 >= STT_MIN_SPEECH_MS;
730
+ }
731
+
732
+ /* Whisper sessizlikte sıkça uydurduğu cümleler (Hermes WHISPER_HALLUCINATIONS birebir
733
+ + Türkçe set: "thanks for watching" modeli Türkçeleşmiş halüsinasyonlardır) */
734
+ const STT_HALLUCINATIONS = new Set([
735
+ 'thank you.', 'thank you', 'thanks for watching.', 'thanks for watching',
736
+ 'subscribe to my channel.', 'subscribe to my channel', 'like and subscribe.', 'like and subscribe',
737
+ 'please subscribe.', 'please subscribe', 'thank you for watching.', 'thank you for watching',
738
+ 'bye.', 'bye', 'you', 'the end.', 'the end',
739
+ 'продолжение следует', 'продолжение следует...',
740
+ 'sous-titres', "sous-titres réalisés par la communauté d'amara.org",
741
+ 'sottotitoli creati dalla comunità amara.org', 'untertitel von stephanie geiges',
742
+ 'amara.org', 'www.mooji.org', 'ご視聴ありがとうございました',
743
+ // Türkçe junk (sessizlikte gözlenenler)
744
+ 'harika', 'tamam.', 'tamam',
745
+ 'izlediğiniz için teşekkür ederim', 'izlediğiniz için teşekkürler', 'izlediğiniz için teşekkür ederiz',
746
+ 'izlediğiniz için çok teşekkür ederim', 'izlediğiniz için teşekkür ederim.', 'izlemeniz için teşekkür ederim',
747
+ 'teşekkür ederim', 'teşekkürler', 'teşekkür ederiz', 'çok teşekkürler', 'çok teşekkür ederim',
748
+ 'abone olmayı unutmayın', 'kanalıma abone olun', 'abone olun', 'beğenmeyi unutmayın', 'like atmayı unutmayın',
749
+ 'hoş geldiniz', 'bir sonraki videoda görüşürüz', 'bir sonraki videoda görüşmek üzere',
750
+ 'sorusu olan var mı', 'izlemeye devam edin', 'altyazılar amara.org', 'altyazı: amara.org',
751
+ // alt yazı türevleri (sessizlikte en sık görülen hayalet transkript)
752
+ 'altyazı mk', 'altyazı: mk', 'altyazı mk.', 'altyazi mk', 'altyazi', 'altyazı', 'altyazılar',
753
+ 'altyazı: mk efendi', 'mk', 'm.k', 'm.k.', 'altyazı ekibi',
754
+ ]);
755
+
756
+ /* Türkçe'ye özgü fold: whisper bazen diakritikleri DÜŞÜRÜR ("Izlediginiz icin"),
757
+ bazen İ→i̇ (combining dot) çıkarır — karşılaştırma ASCII-fold üzerinden yapılır */
758
+ function sttFold(s) {
759
+ return String(s || '')
760
+ .toLowerCase()
761
+ .replace(/[çğıöşüâîûİÇĞÖŞÜ]/g, (ch) => ({ ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u', İ: 'i', Ç: 'c', Ğ: 'g', Ö: 'o', Ş: 's', Ü: 'u' }[ch] || ch))
762
+ .normalize('NFD')
763
+ .replace(/[\u0300-\u036f]/g, '');
764
+ }
765
+
766
+ const STT_HALLUCINATION_FOLDED = new Set(
767
+ [...STT_HALLUCINATIONS].flatMap((p) => {
768
+ const f = sttFold(p);
769
+ return [f, f.replace(/[.!]+$/, '')];
770
+ })
771
+ );
772
+
773
+ /* Tekrarlı junk kalıbı: "Thank you. Thank you. Thank you." / "Teşekkürler. Teşekkürler." (Hermes _HALLUCINATION_REPEAT_RE + TR) */
774
+ const STT_REPEAT_RE = /^(?:thank you|thanks|bye|you|ok|okay|the end|tamam|harika|tesekkur|tesekkurler|ederim|izlediginiz|icin|cok|\.|\s|,|!)+$/i;
775
+
776
+ /* junk ÖNEKLERİ: bu kelimelerle BAŞLAYAN transkriptler alt yazı hayaletidir
777
+ ("Altyazı M.K. ..." gibi) — gerçek komutlar asla bunlarla başlamaz */
778
+ const STT_JUNK_PREFIXES = ['altyazi', 'altyazılar', 'altyazilar', 'altyazı'];
779
+
780
+ function isWhisperHallucination(transcript) {
781
+ const cleaned = String(transcript || '').trim().toLowerCase();
782
+ if (!cleaned) return true;
783
+ if (STT_HALLUCINATIONS.has(cleaned) || STT_HALLUCINATIONS.has(cleaned.replace(/[.!]+$/, ''))) return true;
784
+ const folded = sttFold(cleaned);
785
+ if (STT_HALLUCINATION_FOLDED.has(folded) || STT_HALLUCINATION_FOLDED.has(folded.replace(/[.!]+$/, ''))) return true;
786
+ if (STT_REPEAT_RE.test(folded)) return true;
787
+ if (STT_JUNK_PREFIXES.some((p) => folded.startsWith(p))) return true;
788
+ return false;
789
+ }
790
+
791
+ /* STT ÖNCELİKLİ SIRASI: whisper CPU'da yavaş — final transkript (öncelik 1)
792
+ kuyruktaki önizleme işlerinin (0) Önüne geçer; birikse bile konuşanın
793
+ cevabı önce gelir. Tek seferde tek iş (pipeline güvenliği korunur). */
794
+ const sttQ = [];
795
+ let sttRunning = false;
796
+ function enqueueSttTask(task, priority) {
797
+ return new Promise((resolve, reject) => {
798
+ sttQ.push({ task, priority: priority || 0, resolve, reject });
799
+ if (!sttRunning) drainSttQ();
800
+ });
801
+ }
802
+ async function drainSttQ() {
803
+ sttRunning = true;
804
+ while (sttQ.length) {
805
+ let best = 0;
806
+ for (let i = 1; i < sttQ.length; i++) {
807
+ if ((sttQ[i].priority || 0) > (sttQ[best].priority || 0)) best = i;
808
+ }
809
+ const job = sttQ.splice(best, 1)[0];
810
+ try { job.resolve(await job.task()); } catch (e) { job.reject(e); }
811
+ }
812
+ sttRunning = false;
813
+ }
814
+
815
+ async function transcribeAudio(buf, langOverride /* 'tr' | 'en' | 'auto' */, mimetype, priority) {
816
+ return enqueueSttTask(() => transcribeAudioInner(buf, langOverride, mimetype), priority);
817
+ }
818
+
819
+ async function transcribeAudioInner(buf, langOverride /* 'tr' | 'en' | 'auto' */, mimetype) {
614
820
  try {
821
+ /* WAV HIZLI YOLU: eller-serbest segmentleri 16k mono WAV gelir —
822
+ ffmpeg decode atlanır, doğrudan PCM okunur */
823
+ if (buf.length > 44 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46
824
+ && buf.readUInt32LE(24) === 16000 && buf.readUInt16LE(22) === 1 && buf.readUInt16LE(34) === 16) {
825
+ try {
826
+ const dataLen = buf.readUInt32LE(40);
827
+ const f32 = new Float32Array(Math.floor(dataLen / 2));
828
+ for (let i = 0; i < f32.length; i++) f32[i] = buf.readInt16LE(44 + i * 2) / 32768;
829
+ if (f32.length) return await transcribePcm(f32, langOverride);
830
+ } catch {}
831
+ }
615
832
  const audio = await decodeAudioToPcm16k(buf);
616
833
  if (!audio || !audio.length) return null;
834
+ if (!hasSpeechEnergy(audio)) return null;
835
+
836
+ const lang = langOverride || settings.sttLang || 'tr';
837
+ const provider = sttProvider();
838
+ let text = '';
839
+
840
+ if (provider === 'groq' || provider === 'openai') {
841
+ try {
842
+ text = await transcribeCloud(provider, buf, mimetype, lang);
843
+ } catch (e) {
844
+ waLog('stt bulut (' + provider + ') hata: ' + String((e && e.message) || e).slice(0, 180));
845
+ /* yerel whisper YÜKLÜYSE sessizce devam et; yüklü değilse indirim BAŞLATMAZ */
846
+ if (!sttPipeline && !sttLoading) return null;
847
+ text = '';
848
+ }
849
+ }
850
+
851
+ if (!text) {
852
+ const asr = await ensureStt();
853
+ /* dil: arayüz diline bağlı — UI Türkçe ise Türkçe, İngilizce ise İngilizce algılar */
854
+ const opts = { task: 'transcribe', chunk_length_s: 30, stride_length_s: 5, return_timestamps: true };
855
+ if (lang === 'en') opts.language = 'english';
856
+ else if (lang === 'tr') opts.language = 'turkish';
857
+ const out = await asr(audio, opts);
858
+ text = String((out && out.text) || '').trim();
859
+ }
860
+
861
+ if (!text) return null;
862
+ /* katman 3: bilinen junk cümleleri elenir (her sağlayıcıda geçerli) */
863
+ if (isWhisperHallucination(text)) {
864
+ waLog('stt hallosinasyon filtrelendi: ' + text.slice(0, 60));
865
+ return null;
866
+ }
867
+ return text;
868
+ } catch (e) {
869
+ waLog('stt hata: ' + String((e && e.message) || e));
870
+ return null;
871
+ }
872
+ }
873
+
874
+ /* PCM zaten çözülmüşse model çağrısını doğrudan yap */
875
+ async function transcribePcm(audio, langOverride) {
876
+ try {
877
+ if (!hasSpeechEnergy(audio)) return null;
617
878
  const asr = await ensureStt();
618
- /* dil: arayüz diline bağlı — UI Türkçe ise Türkçe, İngilizce ise İngilizce algılar */
619
879
  const lang = langOverride || settings.sttLang || 'tr';
620
- const opts = { task: 'transcribe', chunk_length_s: 30, stride_length_s: 5 };
880
+ const opts = { task: 'transcribe', chunk_length_s: 30, stride_length_s: 5, return_timestamps: true };
621
881
  if (lang === 'en') opts.language = 'english';
622
882
  else if (lang === 'tr') opts.language = 'turkish';
623
883
  const out = await asr(audio, opts);
624
- return String((out && out.text) || '').trim() || null;
884
+ const text = String((out && out.text) || '').trim();
885
+ if (!text) return null;
886
+ if (isWhisperHallucination(text)) return null;
887
+ return text;
625
888
  } catch (e) {
626
- waLog('stt hata: ' + String((e && e.message) || e));
889
+ waLog('stt pcm hata: ' + String((e && e.message) || e));
627
890
  return null;
628
891
  }
629
892
  }
630
893
 
631
894
  async function synthesizeSpeech(text) {
632
895
  const cfg = settings.waTts || {};
633
- if (!cfg.enabled || !cfg.baseUrl || !cfg.key) return null; try {
896
+ if (!cfg.enabled) return null;
897
+ try {
898
+ /* EDGE TTS: ücretsiz yerel motor — baseUrl/key GEREKMEZ */
899
+ if ((cfg.engine || 'edge') === 'edge') {
900
+ const edge = require('./agent/edgetts');
901
+ const audio = await edge.synthesize(String(text).slice(0, 4000), {
902
+ voice: cfg.edgeVoice || 'tr-TR-AhmetNeural',
903
+ });
904
+ return audio;
905
+ }
906
+ if (!cfg.baseUrl || !cfg.key) return null;
634
907
  const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
635
908
  const res = await fetch(url, {
636
909
  method: 'POST',
@@ -703,7 +976,7 @@ function waSlashHelp() {
703
976
  '• */block* – allow listesini numaralarıyla listele (*/block 3*: 3. kişiyi çıkar; 1 = sahip, silinemez)',
704
977
  '• */approve* – bekleyen riskli işlemi onayla (*/approve always*: bir daha sorulmasın · */deny*: reddet)',
705
978
  '• */update* – yeni sürüm kontrolü (*/update now*: indirileni hemen kur)',
706
- '• */model* – aktif modeli göster (*/model* <isim> ile değiştir)',
979
+ '• */model* – aktif modeli göster (*/model* <isim> ile değiştir · */model refresh* – modelleri yeniden çek)',
707
980
  '• */skills* – kurulu skill\u2019ler',
708
981
  '• */usage* – bugünkü kullanım',
709
982
  '• */backup* – tüm veriyi ŞİFRELİ yedekle (Beast Kodu imzalı, Masaüstü\\Beast-Backups)',
@@ -1151,26 +1424,35 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1151
1424
  ? `*Şifreli yedek alındı*\n${r.path}\n(${Math.round(r.size / 1024)} KB)\nBeast Kodu: \`${r.code}\``
1152
1425
  : 'Yedek hata: ' + (r.error || '?');
1153
1426
  } else if (cmd === 'model') {
1154
- const st = engine.publicState();
1155
- if (arg) {
1156
- const hit = st.models.find((m) => m.sel === arg || m.model.toLowerCase().includes(arg.toLowerCase()));
1157
- if (hit) {
1158
- settings.modelOverride = hit.sel;
1159
- saveSettings();
1160
- engine.setModelOverride ? engine.setModelOverride(hit.sel) : null;
1161
- const st2 = engine.publicState();
1162
- out = st2.activeModel
1163
- ? `*Model değişti:* ${st2.activeModel.providerName} · ${st2.activeModel.model}`
1164
- : '*Model değişti.*';
1427
+ if (arg === 'refresh') {
1428
+ out = '*Modeller yeniden çekiliyor…* (tüm providerlar)';
1429
+ sendWaSafe(jid, out).catch(() => {});
1430
+ const st2 = await refreshModelsAll();
1431
+ out =
1432
+ `*Modeller tazelendi* — ${st2.models.length} model\n` +
1433
+ (st2.activeModel ? `Aktif: ${st2.activeModel.providerName} · ${st2.activeModel.model}` : 'Aktif model yok');
1434
+ } else {
1435
+ const st = engine.publicState();
1436
+ if (arg) {
1437
+ const hit = st.models.find((m) => m.sel === arg || m.model.toLowerCase().includes(arg.toLowerCase()));
1438
+ if (hit) {
1439
+ settings.modelOverride = hit.sel;
1440
+ saveSettings();
1441
+ engine.setModelOverride ? engine.setModelOverride(hit.sel) : null;
1442
+ const st2 = engine.publicState();
1443
+ out = st2.activeModel
1444
+ ? `*Model değişti:* ${st2.activeModel.providerName} · ${st2.activeModel.model}`
1445
+ : '*Model değişti.*';
1446
+ } else {
1447
+ out =
1448
+ 'Eşleşen model yok. *Kurulu modeller:*\n' +
1449
+ st.models.slice(0, 10).map((m) => `- ${m.providerName} · ${m.model}`).join('\n');
1450
+ }
1451
+ } else if (st.activeModel) {
1452
+ out = `*Aktif model:* ${st.activeModel.providerName} · ${st.activeModel.model}\nDeğiştirmek için: /model <isim-parçası>`;
1165
1453
  } else {
1166
- out =
1167
- 'Eşleşen model yok. *Kurulu modeller:*\n' +
1168
- st.models.slice(0, 10).map((m) => `- ${m.providerName} · ${m.model}`).join('\n');
1454
+ out = 'Model seçilmemiş.';
1169
1455
  }
1170
- } else if (st.activeModel) {
1171
- out = `*Aktif model:* ${st.activeModel.providerName} · ${st.activeModel.model}\nDeğiştirmek için: /model <isim-parçası>`;
1172
- } else {
1173
- out = 'Model seçilmemiş.';
1174
1456
  }
1175
1457
  } else if (cmd === 'skills') {
1176
1458
  const sk = skillsMod.scan();
@@ -2018,7 +2300,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2018
2300
  attachments.push({ type: 'image', dataUrl: `data:${md.mimetype};base64,${md.buf.toString('base64')}`, name: md.name });
2019
2301
  text += `\n[resim eki alındı]`;
2020
2302
  } else if (md.kind === 'audio') {
2021
- const tr = await transcribeAudio(md.buf, md.mimetype);
2303
+ /* mimetype langOverride olarak geçilemez — 3. parametre olarak medial türü gider */
2304
+ const tr = await transcribeAudio(md.buf, undefined, md.mimetype);
2022
2305
  text += tr
2023
2306
  ? `\n[sesli mesaj transkripti]\n${tr}`
2024
2307
  : `\n[sesli mesaj alındı ama transkripte çevrilemedi — Entegrasyonlar'da sesli mesaj (STT) ayarı gerekli]`;
@@ -2578,6 +2861,9 @@ function reloadBackend() {
2578
2861
  /* OTOMATİK SKİLL SİSTEMİ: ayarlardan kapatılmadıysa öğrenilen prosedürler
2579
2862
  otomatik skill olur, mevcutların daha iyisi bulunursa güncellenir */
2580
2863
  autoSkills: settings.autoSkills !== false,
2864
+ /* GECE YANSIMASI: her gece hafıza sıkılaştırma + öğrenme journal'i */
2865
+ nightReflect: settings.nightReflect !== false,
2866
+ nightReflectAt: settings.nightReflectAt || null,
2581
2867
  approvals: settings.security && settings.security.approvals ? approvalsBridge : null,
2582
2868
  alwaysAllowTools: (settings.security && settings.security.alwaysAllow) || [],
2583
2869
  crashFile: FALLOUT_CRASH_FILE,
@@ -2895,6 +3181,9 @@ function ensureDesktopShortcut() {
2895
3181
  }
2896
3182
  }
2897
3183
 
3184
+ /* TTS otomatik seslendirme: kullanıcı jesti olmadan Audio.play() çalışsın */
3185
+ app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
3186
+
2898
3187
  app.whenReady().then(() => {
2899
3188
  // Tailscale modu: paketli uygulamada Windows ile otomatik başlat (sessiz, tepside)
2900
3189
  if (app.isPackaged) {
@@ -2945,13 +3234,18 @@ app.whenReady().then(() => {
2945
3234
  netCheck().catch(() => {});
2946
3235
  setInterval(() => { netCheck().catch(() => {}); }, NET_CHECK_MS).unref();
2947
3236
 
2948
- // #12 STT prefetch: whisper modelini arka planda hazırla (ilk sesli mesajda bekleme olmasın)
2949
- if (settings.sttPrefetch !== false) {
2950
- setTimeout(() => {
2951
- ensureStt()
2952
- .then(() => waLog('STT prefetch tamam'))
2953
- .catch((e) => waLog('STT prefetch atlandı: ' + String((e && e.message) || e)));
2954
- }, 10000);
3237
+ // STT motoru: bulut anahtarı varsa Hermes zinciri (groq/openai) devrede
3238
+ // yerel model o zaman HEM indirme HEM prefetch yapılmaz. Prefetch yalnız local'da.
3239
+ if (sttProvider() === 'local') {
3240
+ if (settings.sttPrefetch !== false) {
3241
+ setTimeout(() => {
3242
+ ensureStt()
3243
+ .then(() => waLog('STT prefetch tamam: ' + sttEngineLabel()))
3244
+ .catch((e) => waLog('STT prefetch atlandı: ' + String((e && e.message) || e)));
3245
+ }, 10000);
3246
+ }
3247
+ } else {
3248
+ waLog('STT aktif: ' + sttEngineLabel() + ' — yerel model kullanılmayacak');
2955
3249
  }
2956
3250
 
2957
3251
  // WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
@@ -3024,12 +3318,59 @@ const PHONE_UA =
3024
3318
  'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/' +
3025
3319
  process.versions.chrome +
3026
3320
  ' Mobile Safari/537.36';
3321
+ /* TELEFON SİLUETİ (mobil önizleme): WebContentsView'ı cihaz ekranı boyutuna
3322
+ küçültür, çerçeve DOM'da çizilir. Değerler renderer'daki PF_* sabitleriyle
3323
+ BİREBİR aynı olmalı (PF_BEZEL / PF_STATUS / PF_HOME). */
3324
+ const PHONE_BEZEL = 12; // cihaz çerçevesi kenar kalınlığı (üst/alt/yan hep aynı — halka çerçeve)
3325
+ const PHONE_STATUS = 0; // çentik/durum çubuğu KALDIRILDI — ekran tam görünür
3326
+ const PHONE_HOME = 0; // home çubuğu çerçeve halkasının içine taşındı (ekrandan yer almaz)
3327
+ const DEV_PORTS = [8081, 19006, 5173, 3000, 5174, 19001, 4200, 4321];
3328
+ const DEV_URL_RE = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]|(?:192\.168|10\.\d+|172\.(?:1[6-9]|2\d|3[01]))\.\d+\.\d+):(\d{2,5})/;
3329
+
3330
+ /* CİHAZ KATALOĞU: gerçek CSS piksel ölçüleri (portrait). apple=true → çentik
3331
+ (Dynamic Island), değilse punch-hole kamera noktası çizilir. UA cihaz başına. */
3332
+ const PHONE_DEVICES = {
3333
+ iphone15promax: { name: 'iPhone 15 Pro Max', w: 430, h: 932, apple: true },
3334
+ iphone15pro: { name: 'iPhone 15 Pro', w: 393, h: 852, apple: true },
3335
+ iphone14: { name: 'iPhone 14', w: 390, h: 844, apple: true },
3336
+ iphonese: { name: 'iPhone SE', w: 375, h: 667, apple: true },
3337
+ pixel8: { name: 'Pixel 8', w: 412, h: 915, apple: false },
3338
+ galaxys23: { name: 'Galaxy S23', w: 360, h: 780, apple: false },
3339
+ android: { name: 'Android (genel)', w: 360, h: 800, apple: false },
3340
+ };
3341
+ const PHONE_UA_IOS =
3342
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) ' +
3343
+ 'Version/17.5 Mobile/15E148 Safari/604.1';
3344
+
3345
+ function phoneDevice() {
3346
+ return PHONE_DEVICES[browser.deviceKey] || PHONE_DEVICES.iphone14;
3347
+ }
3348
+
3349
+ function isDevServerUrl(url) {
3350
+ const m = DEV_URL_RE.exec(String(url || ''));
3351
+ if (!m) return false;
3352
+ const port = Number(m[1]);
3353
+ /* expo native/log portları web önizleme değildir */
3354
+ return port !== 19000 && port !== 19001 && port !== 19002;
3355
+ }
3356
+
3027
3357
  /* Tarayıcı state: visible=false → ajanlar GİZLİ kullanır (headless);
3028
3358
  göz ikonuyla görünür mod açılır. open=view aktif, visible=panelde görünürlük */
3029
- const browser = { view: null, open: false, visible: false, width: 0, attached: false, started: false, phone: false, desktopUA: '' };
3359
+ const browser = { view: null, open: false, visible: false, width: 0, attached: false, started: false, phone: false, mobile: false, mobileRect: null, deviceKey: 'iphone14', bottomInset: 0, desktopUA: '' };
3360
+ browser.mobile = settings.browserMobile === true; // mobil önizleme tercihi kalıcı
3361
+ if (PHONE_DEVICES[settings.browserDevice]) browser.deviceKey = settings.browserDevice;
3030
3362
 
3031
3363
  function browserEmit(payload) {
3032
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'browser', visible: browser.visible, phone: browser.phone, ...payload });
3364
+ /* phoneRect/device HER olayda taşınır site değişse bile siluet bozulmaz */
3365
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', {
3366
+ type: 'browser',
3367
+ visible: browser.visible,
3368
+ phone: browser.phone,
3369
+ mobile: browser.mobile,
3370
+ phoneRect: browser.mobileRect,
3371
+ device: browser.deviceKey,
3372
+ ...payload,
3373
+ });
3033
3374
  }
3034
3375
 
3035
3376
  function browserWidthFor(w) {
@@ -3044,9 +3385,146 @@ function browserW() {
3044
3385
 
3045
3386
  function browserShownWidth(w) {
3046
3387
  const avail = Math.max(320, w - 320);
3388
+ /* MOBİL ÖNİZLEME: siluet + çerçeve payı — siluet tam otursun (phone-mode'dan önce) */
3389
+ if (browser.mobile) return Math.min(470, avail);
3047
3390
  /* TELEFON MODU: dock KENDİSİ daralır (~430px) — sayfa mobil düzenle
3048
3391
  kenarlara tam oturur; kapatınca kullanıcı genişliği geri gelir */
3049
- return browser.phone ? Math.min(430, avail) : Math.min(browser.width, avail);
3392
+ if (browser.phone) return Math.min(430, avail);
3393
+ return Math.min(browser.width, avail);
3394
+ }
3395
+
3396
+ /* Telefon siluetinin EKRAN dikdörtgeni (WebContentsView buraya oturur;
3397
+ çerçeve/çentik/home bar DOM'da çizilir). ÖNCE cihazın GERÇEK ölçüsü denenir
3398
+ (ör. tam 393×844 — uygulama device-width'i gerçekten cihaz genişliği görür,
3399
+ mobile TAM uyum); sığmazsa orana göre küçültülür, tavan dock'un %80'i,
3400
+ dikeyde ortalanır. */
3401
+ function browserPhoneRect(w, h) {
3402
+ const dev = phoneDevice();
3403
+ const shownWidth = browserShownWidth(w);
3404
+ const dockX = Math.max(0, w - shownWidth);
3405
+ const availH = Math.max(240, h - BROWSER_TOOLBAR_H - (browser.bottomInset || 0));
3406
+ const chromeH = PHONE_BEZEL * 2 + PHONE_STATUS + PHONE_HOME;
3407
+ const outerCap = Math.max(320, Math.round(availH * 0.8));
3408
+ let ph = Math.min(availH - chromeH, outerCap - chromeH);
3409
+ let pw;
3410
+ if (dev.h + chromeH <= availH && dev.w + PHONE_BEZEL * 2 + 8 <= shownWidth) {
3411
+ /* 1) doğal boyut: cihaz ekranı sığıyorsa BİREBİR o ölçü kullanılır */
3412
+ ph = dev.h;
3413
+ pw = dev.w;
3414
+ } else {
3415
+ /* 2) sığmıyor: yükseklik bütçesine göre orana küçült */
3416
+ pw = Math.round(ph * (dev.w / dev.h));
3417
+ const maxPw = shownWidth - PHONE_BEZEL * 2 - 8;
3418
+ if (pw > maxPw) {
3419
+ pw = Math.max(200, maxPw);
3420
+ ph = Math.round(pw * (dev.h / dev.w));
3421
+ }
3422
+ }
3423
+ const outerH = ph + chromeH;
3424
+ const y0 = BROWSER_TOOLBAR_H + Math.max(0, Math.round((availH - outerH) / 2));
3425
+ return {
3426
+ x: dockX + Math.round((shownWidth - pw) / 2),
3427
+ y: y0 + PHONE_BEZEL + PHONE_STATUS,
3428
+ width: pw,
3429
+ height: Math.max(240, ph),
3430
+ };
3431
+ }
3432
+
3433
+ /* Mobil önizleme scrollbar'ı gizle: kaydırma yine de siluetin İÇİNDE çalışır,
3434
+ sadece masaüstü çubuğu görünmez (telefon hissi). Sayfa değişiminde yeniden enjekte edilir. */
3435
+ const PHONE_SCROLL_CSS =
3436
+ '::-webkit-scrollbar{width:0!important;height:0!important}' +
3437
+ '::-webkit-scrollbar-thumb{background:transparent!important}' +
3438
+ 'html{scrollbar-width:none!important} body{overscroll-behavior:contain}';
3439
+
3440
+ /* DOKUNMATİK SÜRÜKLE-KAYDIR: tut + aşağı çek = sayfa telefonda olduğu gibi
3441
+ akar (atalet/momentum dahil). Bağlantı/buton/giriş alanlarına dokunmaz —
3442
+ tıklamalar normal çalışır; 4px eşikini aşan sürükleme tıkı yutar. */
3443
+ const PHONE_SCROLL_JS = `(function(){
3444
+ if (window.__beastTouchScroll) return; window.__beastTouchScroll = true;
3445
+ var S = null;
3446
+ var INTERACTIVE = 'a,button,input,textarea,select,label,summary,option,[contenteditable="true"],video,audio,iframe,canvas,svg,[draggable="true"]';
3447
+ function scrollableAt(x, y) {
3448
+ var el = document.elementFromPoint(x, y);
3449
+ while (el && el !== document.documentElement) {
3450
+ var cs = getComputedStyle(el);
3451
+ var oy = cs.overflowY;
3452
+ if ((oy === 'auto' || oy === 'scroll' || oy === 'overlay') && el.scrollHeight > el.clientHeight + 2) return el;
3453
+ el = el.parentElement;
3454
+ }
3455
+ return document.scrollingElement || document.documentElement;
3456
+ }
3457
+ window.addEventListener('mousedown', function (e) {
3458
+ if (e.button !== 0 || e.defaultPrevented) return;
3459
+ var t = e.target;
3460
+ if (t && t.closest && t.closest(INTERACTIVE)) return;
3461
+ var el = scrollableAt(e.clientX, e.clientY);
3462
+ if (!el) return;
3463
+ S = { el: el, y0: e.clientY, y: e.clientY, top: el.scrollTop, moved: false, vel: 0, lt: performance.now() };
3464
+ }, true);
3465
+ window.addEventListener('mousemove', function (e) {
3466
+ if (!S) return;
3467
+ var dy = e.clientY - S.y; S.y = e.clientY;
3468
+ if (!S.moved && Math.abs(e.clientY - S.y0) < 4) return;
3469
+ if (!S.moved) { S.moved = true; document.body.style.userSelect = 'none'; }
3470
+ var now = performance.now();
3471
+ var dt = Math.max(1, now - S.lt); S.lt = now;
3472
+ S.vel = 0.8 * (-dy / dt * 16.7) + 0.2 * S.vel;
3473
+ S.el.scrollTop = S.top - (e.clientY - S.y0);
3474
+ e.preventDefault(); e.stopPropagation();
3475
+ }, true);
3476
+ window.addEventListener('click', function (e) {
3477
+ if (S && S.moved) { e.preventDefault(); e.stopPropagation(); }
3478
+ }, true);
3479
+ window.addEventListener('mouseup', function (e) {
3480
+ if (!S) return;
3481
+ var s = S; S = null;
3482
+ document.body.style.userSelect = '';
3483
+ if (!s.moved) return;
3484
+ e.preventDefault();
3485
+ var v = s.vel;
3486
+ (function step() {
3487
+ v *= 0.94;
3488
+ if (Math.abs(v) < 0.6) return;
3489
+ s.el.scrollTop -= v;
3490
+ requestAnimationFrame(step);
3491
+ })();
3492
+ }, true);
3493
+ var st = document.createElement('style');
3494
+ st.textContent = '*{touch-action:pan-y}';
3495
+ document.documentElement.appendChild(st);
3496
+ })();`;
3497
+
3498
+ function injectPhoneScrollCss() {
3499
+ try {
3500
+ const wc = browser.view && browser.view.webContents;
3501
+ if (wc && browser.mobile) {
3502
+ wc.insertCSS(PHONE_SCROLL_CSS, { cssOrigin: 'user' }).catch(() => {});
3503
+ wc.executeJavaScript(PHONE_SCROLL_JS, false).catch(() => {});
3504
+ }
3505
+ } catch {}
3506
+ }
3507
+
3508
+ /* GERÇEK TELEFON DOKUNMASI: Chromium dokunma emülasyonu — fare sürükleme gerçek
3509
+ dokunma olayına döner; sayfa NATİVE momentumla kayar (tut-aşağı-çek tam telefon
3510
+ gibi). RN web/Pressable da gerçek touch alır. CDP oturumu gezinmelerde kalıcıdır. */
3511
+ async function applyPhoneTouchEmulation() {
3512
+ try {
3513
+ const wc = browser.view && browser.view.webContents;
3514
+ if (!wc || !browser.mobile) return;
3515
+ if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
3516
+ await wc.debugger.sendCommand('Emulation.setEmitTouchEventsForMouse', { enabled: true, configuration: 'mobile' });
3517
+ } catch (e) {
3518
+ waLog('telefon dokunma emülasyonu: ' + String((e && e.message) || e).slice(0, 120));
3519
+ }
3520
+ }
3521
+
3522
+ function clearPhoneTouchEmulation() {
3523
+ try {
3524
+ const wc = browser.view && browser.view.webContents;
3525
+ if (!wc || !wc.debugger.isAttached()) return;
3526
+ wc.debugger.sendCommand('Emulation.setEmitTouchEventsForMouse', { enabled: false }).catch(() => {});
3527
+ } catch {}
3050
3528
  }
3051
3529
 
3052
3530
  function layoutBrowser() {
@@ -3069,8 +3547,26 @@ function layoutBrowser() {
3069
3547
  } catch {}
3070
3548
  }
3071
3549
  try {
3072
- view.setBounds({ x: Math.max(0, w - shownWidth), y: BROWSER_TOOLBAR_H, width: shownWidth, height: Math.max(0, h - BROWSER_TOOLBAR_H) });
3550
+ /* MOBİL ÖNİZLEME: view cihaz ekranı boyutuna küçülür çerçeve DOM'da çizilir */
3551
+ let bounds = { x: Math.max(0, w - shownWidth), y: BROWSER_TOOLBAR_H, width: shownWidth, height: Math.max(0, h - BROWSER_TOOLBAR_H - (browser.bottomInset || 0)) };
3552
+ if (browser.mobile && browser.visible) {
3553
+ const r = browserPhoneRect(w, h);
3554
+ browser.mobileRect = r;
3555
+ bounds = r;
3556
+ } else {
3557
+ browser.mobileRect = null;
3558
+ }
3559
+ view.setBounds(bounds);
3073
3560
  view.setVisible(browser.open && browser.visible);
3561
+ /* pencere boyutu değişince (maximize/restore) dock genişliği clamp'lenir —
3562
+ renderer'a YENİ genişlik bildirilmezse DOM'un ayırdığı --bw alanı bayat
3563
+ kalır ve Beast Code chat native view'ın ALTINA girer (iç içe geçme bug'ı) */
3564
+ if (browser._lastEmittedW !== shownWidth) {
3565
+ browser._lastEmittedW = shownWidth;
3566
+ browserEmit({ open: true, width: shownWidth });
3567
+ }
3568
+ /* siluet çerçevesinin hizası için renderer'a ekran dikdörtgeni bildirilir */
3569
+ browserEmit({ mobile: browser.mobile, phoneRect: browser.mobileRect });
3074
3570
  } catch {}
3075
3571
  }
3076
3572
 
@@ -3110,7 +3606,11 @@ function ensureBrowser() {
3110
3606
  browserEmit({ open: true, width: browserShownWidth(browserW()), ...extra });
3111
3607
  }
3112
3608
  };
3113
- wc.on('did-navigate', (_e, url) => notify({ url, loading: false }));
3609
+ wc.on('did-navigate', (_e, url) => {
3610
+ /* mobil önizleme: her sayfada scrollbar gizleme CSS'i yeniden enjekte edilir */
3611
+ injectPhoneScrollCss();
3612
+ notify({ url, loading: false });
3613
+ });
3114
3614
  wc.on('did-navigate-in-page', (_e, url) => notify({ url, loading: false }));
3115
3615
  wc.on('did-start-loading', () => notify({ loading: true }));
3116
3616
  wc.on('did-stop-loading', () => {
@@ -3147,6 +3647,14 @@ function setBrowserOpen(v, forceVisible) {
3147
3647
  const wasOpen = browser.open;
3148
3648
  browser.open = false;
3149
3649
  browser.visible = false;
3650
+ browser.mobileRect = null; /* telefon silueti de kaybolur — bayat rect taşınmaz */
3651
+ /* mobil önizleme modu da KAPANIR — siluet + cihaz seçici + emülasyon temizlenir */
3652
+ if (browser.mobile) {
3653
+ browser.mobile = false;
3654
+ settings.browserMobile = false;
3655
+ saveSettings();
3656
+ clearPhoneTouchEmulation();
3657
+ }
3150
3658
  detachBrowser();
3151
3659
  browserEmit({ open: false });
3152
3660
  return;
@@ -3193,7 +3701,8 @@ function browserGate(job) {
3193
3701
  basar (browser:toggle gizli paneli görünür kılar). Zaten açıksa (kullanıcı
3194
3702
  paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
3195
3703
  function setBrowserOpenForAgent() {
3196
- if (!browser.open) setBrowserOpen(true, false);
3704
+ /* mobil önizleme açıkken ajan gezinmeleri GÖRÜNÜR açılır — siluet canlı kalsın */
3705
+ if (!browser.open) setBrowserOpen(true, browser.mobile ? true : false);
3197
3706
  }
3198
3707
 
3199
3708
  async function browserNavigate(raw, signal, ctx) {
@@ -4002,15 +4511,34 @@ function createWindow() {
4002
4511
  }, 1200);
4003
4512
  }
4004
4513
  });
4005
- win.on('resize', layoutBrowser);
4514
+ /* maximize/fullscreen geçişlerinde dock hizası + görünürlük KESİN senkronlanır —
4515
+ aksi halde tam ekranda tarayıcı "kapanmış" gibi görünür */
4516
+ function resyncBrowserUi() {
4517
+ try {
4518
+ if (browser.view) browser.view.setVisible(browser.open && browser.visible);
4519
+ } catch {}
4520
+ if (browser.open) {
4521
+ const wShown = browserShownWidth(browserW());
4522
+ browser._lastEmittedW = wShown;
4523
+ browserEmit({ open: true, width: wShown });
4524
+ } else {
4525
+ browserEmit({ open: false });
4526
+ }
4527
+ }
4528
+
4529
+ win.on('resize', layoutBrowser);
4006
4530
  win.on('maximize', () => {
4007
4531
  layoutBrowser();
4532
+ resyncBrowserUi();
4008
4533
  try { if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'win-max', maximized: true }); } catch {}
4009
4534
  });
4010
4535
  win.on('unmaximize', () => {
4011
4536
  layoutBrowser();
4537
+ resyncBrowserUi();
4012
4538
  try { if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'win-max', maximized: false }); } catch {}
4013
4539
  });
4540
+ win.on('enter-full-screen', () => { layoutBrowser(); resyncBrowserUi(); });
4541
+ win.on('leave-full-screen', () => { layoutBrowser(); resyncBrowserUi(); });
4014
4542
  win.on('show', layoutBrowser);
4015
4543
  // X'e basınca gizle — tepside yaşamaya devam, WhatsApp bağlantısı sürer
4016
4544
  win.on('close', (e) => {
@@ -4114,6 +4642,21 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
4114
4642
  }
4115
4643
  if (t === '/model' || t.startsWith('/model ')) {
4116
4644
  const arg = t.slice(6).trim();
4645
+ if (arg === 'refresh') {
4646
+ /* tüm provider modellerini yeniden çek — sağ üstteki yenile düğmesiyle aynı */
4647
+ desktopEcho(sessionId, t, '**Modeller yeniden çekiliyor…** (tüm providerlar)');
4648
+ refreshModelsAll()
4649
+ .then((st) => {
4650
+ desktopEcho(
4651
+ sessionId,
4652
+ t,
4653
+ `**Modeller tazelendi** — ${st.models.length} model · ${st.activeModel ? `aktif: ${st.activeModel.providerName} · ${st.activeModel.model}` : 'aktif model yok'}`
4654
+ );
4655
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'modelChanged', sessionId: String(sessionId || '') });
4656
+ })
4657
+ .catch((e) => desktopEcho(sessionId, t, 'Model yenileme başarısız: ' + String((e && e.message) || e)));
4658
+ return true;
4659
+ }
4117
4660
  const st = engine.publicState();
4118
4661
  if (arg) {
4119
4662
  const hit = st.models.find(
@@ -4272,7 +4815,7 @@ function desktopSlashHelp() {
4272
4815
  '**/restart** – uygulamayı yeniden başlat',
4273
4816
  '**/stop** – koşan işleri durdur · **/start** – devam ettir',
4274
4817
  '**/change [n]** – modelleri listele · n. modele geç',
4275
- '**/model [isim]** – aktif modeli göster / değiştir',
4818
+ '**/model [isim]** – aktif modeli göster / değiştir · **/model refresh** – tüm provider modellerini yeniden çek',
4276
4819
  '**/think 0-5** – düşünme seviyesi (0 kapalı · 5 max)',
4277
4820
  '**/agent [isim]** – özel ajan bağla / listele (%APPDATA%\\beast\\agents\\*.md)',
4278
4821
  '**/clear** – oturum geçmişini gerçekten sil (kod korunur)',
@@ -4415,7 +4958,7 @@ function handleGlobalStopStart(sessionId, cmd) {
4415
4958
  }
4416
4959
  }
4417
4960
 
4418
- ipcMain.handle('agent:interrupt', (_e, sessionId) => {
4961
+ ipcMain.handle('agent:interrupt', (_e, sessionId, reason) => {
4419
4962
  /* durdurma: bekleyen birleştirme kuyruğunu da boşalt (kullanıcı vazgeçti) */
4420
4963
  try {
4421
4964
  const q = desktopQueue.get(String(sessionId));
@@ -4424,7 +4967,10 @@ ipcMain.handle('agent:interrupt', (_e, sessionId) => {
4424
4967
  desktopQueue.delete(String(sessionId));
4425
4968
  }
4426
4969
  } catch {}
4427
- return engine.interrupt(sessionId, 'kullanıcı sohbetteki durdurma (■) düğmesiyle iptal etti');
4970
+ return engine.interrupt(
4971
+ sessionId,
4972
+ String(reason || '').trim() || 'kullanıcı sohbetteki durdurma (■) düğmesiyle iptal etti'
4973
+ );
4428
4974
  });
4429
4975
 
4430
4976
  /* ---------- masaüstü sohbet birleştirme ----------
@@ -4833,6 +5379,37 @@ ipcMain.handle('skills:auto:set', (_e, v) => {
4833
5379
  return settings.autoSkills;
4834
5380
  });
4835
5381
 
5382
+ /* GECE YANSIMASI: elle tetikleme + son rapor/öğrenme günlükleri */
5383
+ ipcMain.handle('reflect:run', async () => {
5384
+ if (!engine) return { ok: false, error: 'motor hazır değil' };
5385
+ const r = await engine.runNightReflection({ manual: true });
5386
+ if (r && r.ok) waLog(`gece yansıması (manuel): ${r.memory.before}→${r.memory.after} kayıt`);
5387
+ return r;
5388
+ });
5389
+ ipcMain.handle('reflect:last', () => {
5390
+ try {
5391
+ return JSON.parse(require('fs').readFileSync(require('path').join(memory.memDir(), 'reflections', 'last.json'), 'utf8'));
5392
+ } catch {
5393
+ return null;
5394
+ }
5395
+ });
5396
+ ipcMain.handle('reflect:reports', () => {
5397
+ try {
5398
+ const dir = require('path').join(memory.memDir(), 'reflections');
5399
+ return require('fs').readdirSync(dir)
5400
+ .filter((f) => f.endsWith('.json') && f !== 'last.json')
5401
+ .sort()
5402
+ .reverse()
5403
+ .slice(0, 30)
5404
+ .map((f) => {
5405
+ try { return JSON.parse(require('fs').readFileSync(require('path').join(dir, f), 'utf8')); } catch { return null; }
5406
+ })
5407
+ .filter(Boolean);
5408
+ } catch {
5409
+ return [];
5410
+ }
5411
+ });
5412
+
4836
5413
  /* taslak skill'ler (#2 yansıma ürünleri) */
4837
5414
  ipcMain.handle('skills:drafts:list', () => skillsMod.listDrafts());
4838
5415
  ipcMain.handle('skills:drafts:accept', (_e, id) => {
@@ -5189,11 +5766,11 @@ async function runUpdateCommand(reply /* fn(text) */) {
5189
5766
 
5190
5767
  /* #STT: sohbet mikrofonu — MediaRecorder sesini (webm/opus) yerel whisper'a çevir */
5191
5768
  /* #STT: sohbet mikrofonu — MediaRecorder sesini (webm/opus) yerel whisper'a çevir */
5192
- ipcMain.handle('stt:transcribe', async (_e, b64, lang) => {
5769
+ ipcMain.handle('stt:transcribe', async (_e, b64, lang, priority) => {
5193
5770
  try {
5194
5771
  const buf = Buffer.from(String(b64 || '').split(',').pop() || '', 'base64');
5195
5772
  if (!buf.length) return { ok: false, error: 'boş ses kaydı' };
5196
- const text = await transcribeAudio(buf, lang === 'en' || lang === 'auto' ? lang : undefined);
5773
+ const text = await transcribeAudio(buf, lang === 'en' || lang === 'auto' ? lang : undefined, 'audio/webm', priority === 1 ? 1 : 0);
5197
5774
  return text ? { ok: true, text } : { ok: false, error: 'konuşma algılanamadı' };
5198
5775
  } catch (e) {
5199
5776
  return { ok: false, error: String((e && e.message) || e) };
@@ -5521,6 +6098,109 @@ function setBrowserPhone(on) {
5521
6098
  return { ok: true, phone: browser.phone };
5522
6099
  }
5523
6100
  ipcMain.handle('browser:phone', (_e, on) => setBrowserPhone(on));
6101
+
6102
+ /* MOBİL ÖNİZLEME (telefon silueti): view cihaz ekranı boyutuna küçülür, çerçeve
6103
+ DOM'da çizilir; açıkken dev sunucu (Expo/Metro/Vite) aranır ve bulunursa
6104
+ doğrudan ona gidilir. Ajan bir dev sunucu başlattığında renderer tool
6105
+ çıktısından URL yakalayıp browserNavigate ile buraya yönlendirir. */
6106
+ function setBrowserMobile(on) {
6107
+ const want = !!on;
6108
+ if (browser.mobile === want && (want ? browser.mobileRect : true)) return { ok: true, mobile: want };
6109
+ browser.mobile = want;
6110
+ settings.browserMobile = want;
6111
+ saveSettings();
6112
+ try {
6113
+ const wc = browser.view && browser.view.webContents;
6114
+ if (wc) wc.setUserAgent(want ? phoneDeviceUA() : browser.desktopUA || PHONE_UA);
6115
+ } catch {}
6116
+ if (want) setBrowserOpen(true, true); // tarayıcı kapalıysa görünür aç
6117
+ if (browser.open && win && !win.isDestroyed()) {
6118
+ layoutBrowser();
6119
+ const [w] = win.getContentSize();
6120
+ browserEmit({ open: true, width: browserShownWidth(w), mobile: browser.mobile, phoneRect: browser.mobileRect });
6121
+ if (want) {
6122
+ injectPhoneScrollCss();
6123
+ applyPhoneTouchEmulation();
6124
+ } else {
6125
+ clearPhoneTouchEmulation();
6126
+ }
6127
+ }
6128
+ if (want) {
6129
+ /* dev sunucu ara: canlı mobil uygulama önizlemesine git */
6130
+ devServerDetect()
6131
+ .then((url) => {
6132
+ if (!url || !browser.mobile || !win || win.isDestroyed()) return;
6133
+ let cur = '';
6134
+ try { cur = browser.view.webContents.getURL(); } catch {}
6135
+ if (!isDevServerUrl(cur)) browserNavigate(url).catch(() => {});
6136
+ })
6137
+ .catch(() => {});
6138
+ }
6139
+ return { ok: true, mobile: browser.mobile };
6140
+ }
6141
+ ipcMain.handle('browser:mobile', (_e, on) => setBrowserMobile(on));
6142
+
6143
+ /* CİHAZ SEÇİMİ: iPhone/Android ölçüleri — oran değişir, UA tazelenir */
6144
+ function phoneDeviceUA() {
6145
+ const d = phoneDevice();
6146
+ return d.apple ? PHONE_UA_IOS : PHONE_UA;
6147
+ }
6148
+
6149
+ function setBrowserDevice(key) {
6150
+ const k = String(key || '').trim();
6151
+ if (!PHONE_DEVICES[k]) return { ok: false, error: 'bilinmeyen cihaz' };
6152
+ if (browser.deviceKey === k) return { ok: true, device: k, mobile: browser.mobile };
6153
+ browser.deviceKey = k;
6154
+ settings.browserDevice = k;
6155
+ saveSettings();
6156
+ try {
6157
+ const wc = browser.view && browser.view.webContents;
6158
+ if (wc && browser.mobile) {
6159
+ wc.setUserAgent(phoneDeviceUA());
6160
+ let url = '';
6161
+ try { url = wc.getURL(); } catch {}
6162
+ /* UA değişimi için sayfa taze yüklenir (setBrowserPhone ile aynı mantık) */
6163
+ if (url && /^https?:/i.test(url)) wc.loadURL(url).catch(() => {});
6164
+ }
6165
+ } catch {}
6166
+ if (browser.open && win && !win.isDestroyed()) {
6167
+ layoutBrowser(); // yeni ekran oranı — siluet yeniden hizalanır
6168
+ const [w] = win.getContentSize();
6169
+ browserEmit({ open: true, width: browserShownWidth(w) });
6170
+ }
6171
+ return { ok: true, device: k, mobile: browser.mobile };
6172
+ }
6173
+ ipcMain.handle('browser:device', (_e, key) => setBrowserDevice(key));
6174
+
6175
+ async function probeDevUrl(url, timeoutMs = 1200) {
6176
+ try {
6177
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
6178
+ return res.status < 500;
6179
+ } catch {
6180
+ return false;
6181
+ }
6182
+ }
6183
+
6184
+ async function devServerDetect() {
6185
+ for (const port of DEV_PORTS) {
6186
+ const url = 'http://localhost:' + port + '/';
6187
+ /* eslint-disable-next-line no-await-in-loop */
6188
+ if (await probeDevUrl(url)) return url;
6189
+ }
6190
+ return null;
6191
+ }
6192
+ ipcMain.handle('devserver:detect', () => devServerDetect());
6193
+
6194
+ /* ALT TERMINAL PAYI: alt dock terminal açıkken view yüksekliğini kısar —
6195
+ DOM terminali native view'ın altında kalmaz */
6196
+ ipcMain.handle('browser:bottomInset', (_e, px) => {
6197
+ /* terminal yüksekliği pencerenin %70'i olabildiğinden tavan 1200 */
6198
+ const v = Math.max(0, Math.min(Math.round(Number(px) || 0), 1200));
6199
+ if (browser.bottomInset === v) return { ok: true, inset: v };
6200
+ browser.bottomInset = v;
6201
+ layoutBrowser();
6202
+ return { ok: true, inset: v };
6203
+ });
5524
6204
  ipcMain.handle('browser:screenshot', async () => {
5525
6205
  const r = await browserScreenshot();
5526
6206
  if (!r.ok) return { ok: false, error: r.error };
@@ -6491,6 +7171,113 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
6491
7171
  servis edilir; ajan kendi dev sunucusunu başlattıysa o adres önceliklidir. */
6492
7172
  let bcLastServerUrl = ''; /* ajanın başlattığı dev server adresi (bc-preview'dan yakalanır) */
6493
7173
  const bcStatic = { server: null, root: '', base: '' };
7174
+ /* MOBİL PROJE önizleme: npm run web (expo start --web) süreci + canlı adres */
7175
+ const bcMobile = { proc: null, url: '', root: '' };
7176
+
7177
+ /* workspace bir MOBİL UYGULAMA projesi mi? (expo / react-native / app.json) */
7178
+ function ideMobileProject(root) {
7179
+ try {
7180
+ const pkgPath = path.join(root, 'package.json');
7181
+ if (!fs.existsSync(pkgPath)) return { mobile: false };
7182
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
7183
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
7184
+ const isExpo = !!(deps.expo || deps['react-native'] || deps['react-native-web']);
7185
+ const hasAppJson = fs.existsSync(path.join(root, 'app.json')) || fs.existsSync(path.join(root, 'app.config.js'));
7186
+ const scripts = pkg.scripts || {};
7187
+ const hasReact = !!(deps.react || deps.next);
7188
+ const mobile = (isExpo || hasAppJson) && hasReact;
7189
+ let cmd = '';
7190
+ if (mobile) {
7191
+ if (scripts.web) cmd = 'npm run web';
7192
+ else if (deps.expo) cmd = 'npx expo start --web';
7193
+ else if (scripts.start) cmd = 'npm start';
7194
+ }
7195
+ return { mobile, cmd };
7196
+ } catch {
7197
+ return { mobile: false, cmd: '' };
7198
+ }
7199
+ }
7200
+
7201
+ function killMobilePreview() {
7202
+ const p = bcMobile.proc;
7203
+ if (p) {
7204
+ try { spawn('taskkill', ['/pid', String(p.pid), '/T', '/F'], { windowsHide: true }); } catch {}
7205
+ try { p.kill(); } catch {}
7206
+ }
7207
+ bcMobile.proc = null;
7208
+ bcMobile.url = '';
7209
+ }
7210
+
7211
+ /* npm run web'i başlat; dev sunucu ayağa kalkınca adresi döner (çıktı taraması +
7212
+ port yoklaması çift kanal). Süreç UZUN ÖMÜRLÜDÜR — yeni preview onu öldürür. */
7213
+ function mobilePreviewStart(root) {
7214
+ return new Promise((resolve) => {
7215
+ const info = ideMobileProject(root);
7216
+ if (!info.mobile || !info.cmd) return resolve({ ok: false, error: 'mobil web scripti yok — package.json scripts.web ekleyin' });
7217
+ /* zaten çalışıyor mu? */
7218
+ if (bcMobile.proc && bcMobile.url && bcMobile.root === root) {
7219
+ probeDevUrl(bcMobile.url, 800).then((alive) => {
7220
+ if (alive) resolve({ ok: true, url: bcMobile.url });
7221
+ else {
7222
+ killMobilePreview();
7223
+ mobilePreviewStart(root).then(resolve, () => resolve({ ok: false, error: 'başlatılamadı' }));
7224
+ }
7225
+ });
7226
+ return;
7227
+ }
7228
+ if (bcMobile.root !== root) killMobilePreview();
7229
+ bcMobile.url = '';
7230
+ bcMobile.root = root;
7231
+ let out = '';
7232
+ let settled = false;
7233
+ waLog('mobil önizleme başlatılıyor: ' + info.cmd + ' @ ' + root);
7234
+ const child = spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', info.cmd], { cwd: root, windowsHide: true });
7235
+ bcMobile.proc = child;
7236
+ const scan = () => {
7237
+ const m = /https?:\/\/(?:localhost|127\.0\.0\.1):(\d{2,5})/.exec(out);
7238
+ if (m && Number(m[1]) !== 19000 && Number(m[1]) !== 19001 && Number(m[1]) !== 19002) {
7239
+ bcMobile.url = m[0].replace(/\/+$/, '') + '/';
7240
+ }
7241
+ };
7242
+ child.stdout.on('data', (d) => { out += String(d); if (out.length > 60000) out = out.slice(-30000); scan(); });
7243
+ child.stderr.on('data', (d) => { out += String(d); if (out.length > 60000) out = out.slice(-30000); scan(); });
7244
+ child.on('exit', () => { if (bcMobile.proc === child) bcMobile.proc = null; });
7245
+ const t0 = Date.now();
7246
+ const timer = setInterval(async () => {
7247
+ scan();
7248
+ let url = bcMobile.url;
7249
+ if (!url) {
7250
+ for (const port of [8081, 19006, 5173]) {
7251
+ /* eslint-disable-next-line no-await-in-loop */
7252
+ if (await probeDevUrl('http://localhost:' + port + '/', 500)) { url = 'http://localhost:' + port + '/'; break; }
7253
+ }
7254
+ }
7255
+ if (url) {
7256
+ bcMobile.url = url;
7257
+ clearInterval(timer);
7258
+ settled = true;
7259
+ resolve({ ok: true, url });
7260
+ } else if (Date.now() - t0 > 120000) {
7261
+ clearInterval(timer);
7262
+ if (!settled) resolve({ ok: false, error: 'dev sunucu 120 sn içinde ayağa kalkmadı — çıktıyı terminalden kontrol et' });
7263
+ }
7264
+ }, 1200);
7265
+ });
7266
+ }
7267
+
7268
+ /* ajana sessiz bilgi: dev sunucu zaten çalışıyor — ikinci sunucu başlatmasın */
7269
+ function bcTellMobileServe(url) {
7270
+ try {
7271
+ const ws = ideRoot();
7272
+ const sid = bcSessions.get(ws);
7273
+ if (!sid) return;
7274
+ engine.observe(sid,
7275
+ '[PREVIEW] Kullanıcı mobil önizlemeyi açtı — `npm run web` dev sunucusu şu adreste ÇALIŞIYOR: ' + url + '\n' +
7276
+ 'KENDİ dev sunucunu BAŞLATMA (port çakışır); uygulamayı bu adres üzerinden değerlendir,\n' +
7277
+ 'kod düzenlemeleri hot-reload ile siluette canlı görünür.'
7278
+ );
7279
+ } catch {}
7280
+ }
6494
7281
  const BC_MIME = {
6495
7282
  '.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
6496
7283
  '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
@@ -6582,6 +7369,27 @@ ipcMain.handle('ide:previewFile', async (_e, rel) => {
6582
7369
  ipcMain.handle('ide:preview', async () => {
6583
7370
  try {
6584
7371
  const root = ideRoot();
7372
+ /* MOBİL UYGULAMA PROJESİ (expo/react-native): statik sunucu değil,
7373
+ `npm run web` başlat → telefon silueti içinde canlı önizleme */
7374
+ const mob = ideMobileProject(root);
7375
+ if (mob.mobile) {
7376
+ let url = '';
7377
+ /* ajanın kendi dev sunucusu canlıysa öncelikli */
7378
+ if (bcLastServerUrl && isDevServerUrl(bcLastServerUrl)) {
7379
+ const alive = await probeDevUrl(bcLastServerUrl, 800).catch(() => false);
7380
+ if (alive) url = bcLastServerUrl;
7381
+ }
7382
+ if (!url) {
7383
+ const r = await mobilePreviewStart(root);
7384
+ if (!r.ok) return { ok: false, error: r.error };
7385
+ url = r.url;
7386
+ }
7387
+ setBrowserMobile(true); /* siluet + görünür tarayıcı */
7388
+ try { browser.view.webContents.loadURL(url).catch(() => {}); } catch {}
7389
+ browserEmit({ open: true, width: browserShownWidth(browserW()), url, mobile: true, phoneRect: browser.mobileRect });
7390
+ bcTellMobileServe(url);
7391
+ return { ok: true, url, mobile: true };
7392
+ }
6585
7393
  const pick = (name) => {
6586
7394
  const p = path.join(root, name);
6587
7395
  try { return fs.existsSync(p) ? p : null; } catch { return null; }
@@ -6667,7 +7475,10 @@ ipcMain.handle('custom:set', (_e, list) => {
6667
7475
 
6668
7476
  /* #Model refresh: taban zinciri tazele + kayıtlı tüm custom providerların
6669
7477
  modellerini yeniden çek (picker anında güncellenir). */
6670
- ipcMain.handle('models:refresh', async () => {
7478
+ /* Tüm provider modellerini yeniden çek: taban zincir (config.yaml) + custom
7479
+ provider /models listeleri. UI düğmesi, /model refresh (chat + WhatsApp)
7480
+ aynı bu fonksiyonu kullanır. */
7481
+ async function refreshModelsAll() {
6671
7482
  try {
6672
7483
  const cfg = loadBeastConfig();
6673
7484
  if (cfg && engine.refreshBaseChain) engine.refreshBaseChain(cfg);
@@ -6711,7 +7522,9 @@ ipcMain.handle('models:refresh', async () => {
6711
7522
  }
6712
7523
  log.info('main', `model refresh: taban tazelendi, ${updated} custom provider güncellendi`);
6713
7524
  return engine.publicState();
6714
- });
7525
+ }
7526
+
7527
+ ipcMain.handle('models:refresh', async () => refreshModelsAll());
6715
7528
 
6716
7529
  ipcMain.handle('providers:builtin', () => BUILTIN_PROVIDERS);
6717
7530
 
@@ -6907,6 +7720,35 @@ async function testZenModels(key, models) {
6907
7720
  return results;
6908
7721
  }
6909
7722
 
7723
+ /* REASONING PROBU: model düşünüyor mu? reasoning_effort:'low' ile minik istek —
7724
+ 200 → destekliyor (bir şey yapma), 400 → desteklemiyor (model-caps'e yazılır,
7725
+ kullanıcı düşünmeyi açsa bile o modelde parametre hiç gönderilmez).
7726
+ Diğer statuslar (429/5xx) kararsızdır — hüküm verilmez. */
7727
+ async function testZenReasoning(key, model) {
7728
+ try {
7729
+ const res = await fetch(ZEN_BASE + '/chat/completions', {
7730
+ method: 'POST',
7731
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + key },
7732
+ body: JSON.stringify({
7733
+ model,
7734
+ messages: [{ role: 'user', content: 'ping' }],
7735
+ max_tokens: 8,
7736
+ stream: false,
7737
+ reasoning_effort: 'low',
7738
+ }),
7739
+ signal: AbortSignal.timeout(25000),
7740
+ });
7741
+ if (res.ok) return true;
7742
+ if (res.status === 400) {
7743
+ try { await res.text(); } catch {}
7744
+ return false;
7745
+ }
7746
+ return null;
7747
+ } catch {
7748
+ return null;
7749
+ }
7750
+ }
7751
+
6910
7752
  ipcMain.handle('zen:oneClick', async () => {
6911
7753
  try {
6912
7754
  /* 1) token zaten var mı? (env / auth.json) */
@@ -6947,6 +7789,25 @@ ipcMain.handle('zen:oneClick', async () => {
6947
7789
  };
6948
7790
  }
6949
7791
  const entry = applyZenProvider(key, models);
7792
+ /* reasoning probu: desteklemeyen free modelleri ÖNCEDEN model-caps.json'a
7793
+ yaz — kullanıcının düşünme seviyesi açıkken bile bu modellerde
7794
+ reasoning_effort hiç gönderilmez, ilk istekte 400 turu yaşanmaz */
7795
+ try {
7796
+ const llmMod = require('./agent/llm');
7797
+ llmMod.setCapsFile(path.join(beastDir(), 'model-caps.json'));
7798
+ const noReason = [];
7799
+ const RBATCH = 4;
7800
+ for (let i = 0; i < models.length; i += RBATCH) {
7801
+ const rs = await Promise.all(models.slice(i, i + RBATCH).map((m) => testZenReasoning(key, m)));
7802
+ rs.forEach((r, j) => { if (r === false) noReason.push(models[i + j]); });
7803
+ }
7804
+ for (const m of noReason) {
7805
+ llmMod.learnCaps({ providerId: 'custom:' + ZEN_PRESET_ID, model: m }, { noReasoning: true });
7806
+ }
7807
+ if (noReason.length) {
7808
+ log.info('main', 'zen: reasoning desteklemeyenler (caps yazıldı) → ' + noReason.join(', '));
7809
+ }
7810
+ } catch {}
6950
7811
  /* hiç model seçili değilse ilk ÇALIŞAN modeli varsayılan yap */
6951
7812
  try {
6952
7813
  if (!engine.publicState().activeModel) {
@@ -7026,6 +7887,10 @@ ipcMain.handle('wa:tts:get', () => settings.waTts || {});
7026
7887
  ipcMain.handle('wa:tts:set', (_e, cfg) => {
7027
7888
  settings.waTts = {
7028
7889
  enabled: !!(cfg && cfg.enabled),
7890
+ /* motor: 'edge' (ücretsiz yerel, varsayılan) | 'openai' (OpenAI-uyumlu API) */
7891
+ engine: (cfg && cfg.engine) === 'openai' ? 'openai' : 'edge',
7892
+ edgeVoice: String((cfg && cfg.edgeVoice) || 'tr-TR-AhmetNeural').trim(),
7893
+ chatAutoSpeak: !!(cfg && cfg.chatAutoSpeak),
7029
7894
  baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
7030
7895
  key: String((cfg && cfg.key) || '').trim(),
7031
7896
  model: String((cfg && cfg.model) || '').trim(),
@@ -7035,6 +7900,22 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
7035
7900
  return settings.waTts;
7036
7901
  });
7037
7902
 
7903
+ /* CHAT TTS: masaüstü sohbetinde ajanın son yazısını seslendirir (base64 mp3).
7904
+ chatAutoSpeak açıkken renderer 'done' olayında bu kanalı çağırır. */
7905
+ ipcMain.handle('tts:synthesize', async (_e, text) => {
7906
+ const cfg = settings.waTts || {};
7907
+ if (!cfg.enabled) return { ok: false, error: 'tts kapalı' };
7908
+ const t = String(text || '').trim();
7909
+ if (!t) return { ok: false, error: 'boş metin' };
7910
+ const audio = await synthesizeSpeech(t.slice(0, 4000));
7911
+ if (!audio) {
7912
+ waLog('tts chat seslendirilemedi — motor: ' + (cfg.engine || 'edge'));
7913
+ return { ok: false, error: 'seslendirilemedi' };
7914
+ }
7915
+ waLog('tts chat ok: ' + audio.length + ' bayt, motor: ' + (cfg.engine || 'edge') + ', ses: ' + (cfg.edgeVoice || '-'));
7916
+ return { ok: true, audioB64: audio.toString('base64'), mime: 'audio/mpeg' };
7917
+ });
7918
+
7038
7919
  /* ---------- Telegram IPC (FEATURE 3) ---------- */
7039
7920
 
7040
7921
  ipcMain.handle('tg:status:get', () => {