beast-agent 2.2.1 → 2.3.2
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/package.json +7 -1
- package/src/agent/edgetts.js +201 -0
- package/src/agent/engine.js +93 -1
- package/src/agent/nightref.js +404 -0
- package/src/main.js +937 -57
- package/src/preload.js +9 -2
- package/src/renderer/handsfree.js +633 -0
- package/src/renderer/i18n.js +32 -6
- package/src/renderer/index.html +21 -7
- package/src/renderer/renderer.js +506 -81
- package/src/renderer/style.css +114 -24
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
|
-
|
|
566
|
-
if (
|
|
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:
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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) };
|
|
@@ -5201,6 +5778,53 @@ ipcMain.handle('stt:transcribe', async (_e, b64, lang) => {
|
|
|
5201
5778
|
});
|
|
5202
5779
|
|
|
5203
5780
|
ipcMain.handle('stt:lang:get', () => settings.sttLang || 'tr');
|
|
5781
|
+
|
|
5782
|
+
/* STT DURUMU: ayarlar ekranı için — model indirildi mi/in-progress mi,
|
|
5783
|
+
hangi motor, ne kadar disk */
|
|
5784
|
+
ipcMain.handle('stt:status', () => {
|
|
5785
|
+
const provider = sttProvider();
|
|
5786
|
+
const model = sttModelName();
|
|
5787
|
+
const out = { provider, engineLabel: sttEngineLabel(), model };
|
|
5788
|
+
if (provider !== 'local') { out.state = 'cloud'; return out; }
|
|
5789
|
+
const dir = path.join(APP_DIR, 'models', ...model.split('/'));
|
|
5790
|
+
let files = [];
|
|
5791
|
+
try {
|
|
5792
|
+
files = fs.readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
5793
|
+
.filter((d) => d.isFile())
|
|
5794
|
+
.map((d) => path.join(d.parentPath || d.path, d.name));
|
|
5795
|
+
} catch {}
|
|
5796
|
+
let bytes = 0;
|
|
5797
|
+
let tmp = 0;
|
|
5798
|
+
let onnx = 0;
|
|
5799
|
+
let hasConfig = false;
|
|
5800
|
+
for (const p of files) {
|
|
5801
|
+
try {
|
|
5802
|
+
bytes += fs.statSync(p).size;
|
|
5803
|
+
if (/\.tmp/i.test(p)) tmp++;
|
|
5804
|
+
else if (/\.onnx$/i.test(p)) onnx++;
|
|
5805
|
+
else if (/config\.json$/i.test(p)) hasConfig = true;
|
|
5806
|
+
} catch {}
|
|
5807
|
+
}
|
|
5808
|
+
let state;
|
|
5809
|
+
if (sttPipeline) state = 'ready';
|
|
5810
|
+
else if (sttLoading) state = 'loading';
|
|
5811
|
+
else if (hasConfig && onnx >= 2 && tmp === 0) state = 'downloaded';
|
|
5812
|
+
else if (files.length) state = 'partial';
|
|
5813
|
+
else state = 'missing';
|
|
5814
|
+
out.state = state;
|
|
5815
|
+
out.mb = Math.round(bytes / (1024 * 1024));
|
|
5816
|
+
out.onnx = onnx;
|
|
5817
|
+
out.tmp = tmp;
|
|
5818
|
+
return out;
|
|
5819
|
+
});
|
|
5820
|
+
|
|
5821
|
+
/* istenirse modeli şimdi indir/yükle (bloklamaz — arka planda) */
|
|
5822
|
+
ipcMain.handle('stt:prefetch', () => {
|
|
5823
|
+
ensureStt()
|
|
5824
|
+
.then(() => waLog('STT prefetch: hazır'))
|
|
5825
|
+
.catch((e) => waLog('STT prefetch hata: ' + String((e && e.message) || e)));
|
|
5826
|
+
return { ok: true, loading: true };
|
|
5827
|
+
});
|
|
5204
5828
|
ipcMain.handle('stt:lang:set', (_e, lang) => {
|
|
5205
5829
|
const v = ['auto', 'tr', 'en'].includes(String(lang)) ? String(lang) : 'tr';
|
|
5206
5830
|
settings.sttLang = v;
|
|
@@ -5521,6 +6145,109 @@ function setBrowserPhone(on) {
|
|
|
5521
6145
|
return { ok: true, phone: browser.phone };
|
|
5522
6146
|
}
|
|
5523
6147
|
ipcMain.handle('browser:phone', (_e, on) => setBrowserPhone(on));
|
|
6148
|
+
|
|
6149
|
+
/* MOBİL ÖNİZLEME (telefon silueti): view cihaz ekranı boyutuna küçülür, çerçeve
|
|
6150
|
+
DOM'da çizilir; açıkken dev sunucu (Expo/Metro/Vite) aranır ve bulunursa
|
|
6151
|
+
doğrudan ona gidilir. Ajan bir dev sunucu başlattığında renderer tool
|
|
6152
|
+
çıktısından URL yakalayıp browserNavigate ile buraya yönlendirir. */
|
|
6153
|
+
function setBrowserMobile(on) {
|
|
6154
|
+
const want = !!on;
|
|
6155
|
+
if (browser.mobile === want && (want ? browser.mobileRect : true)) return { ok: true, mobile: want };
|
|
6156
|
+
browser.mobile = want;
|
|
6157
|
+
settings.browserMobile = want;
|
|
6158
|
+
saveSettings();
|
|
6159
|
+
try {
|
|
6160
|
+
const wc = browser.view && browser.view.webContents;
|
|
6161
|
+
if (wc) wc.setUserAgent(want ? phoneDeviceUA() : browser.desktopUA || PHONE_UA);
|
|
6162
|
+
} catch {}
|
|
6163
|
+
if (want) setBrowserOpen(true, true); // tarayıcı kapalıysa görünür aç
|
|
6164
|
+
if (browser.open && win && !win.isDestroyed()) {
|
|
6165
|
+
layoutBrowser();
|
|
6166
|
+
const [w] = win.getContentSize();
|
|
6167
|
+
browserEmit({ open: true, width: browserShownWidth(w), mobile: browser.mobile, phoneRect: browser.mobileRect });
|
|
6168
|
+
if (want) {
|
|
6169
|
+
injectPhoneScrollCss();
|
|
6170
|
+
applyPhoneTouchEmulation();
|
|
6171
|
+
} else {
|
|
6172
|
+
clearPhoneTouchEmulation();
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
if (want) {
|
|
6176
|
+
/* dev sunucu ara: canlı mobil uygulama önizlemesine git */
|
|
6177
|
+
devServerDetect()
|
|
6178
|
+
.then((url) => {
|
|
6179
|
+
if (!url || !browser.mobile || !win || win.isDestroyed()) return;
|
|
6180
|
+
let cur = '';
|
|
6181
|
+
try { cur = browser.view.webContents.getURL(); } catch {}
|
|
6182
|
+
if (!isDevServerUrl(cur)) browserNavigate(url).catch(() => {});
|
|
6183
|
+
})
|
|
6184
|
+
.catch(() => {});
|
|
6185
|
+
}
|
|
6186
|
+
return { ok: true, mobile: browser.mobile };
|
|
6187
|
+
}
|
|
6188
|
+
ipcMain.handle('browser:mobile', (_e, on) => setBrowserMobile(on));
|
|
6189
|
+
|
|
6190
|
+
/* CİHAZ SEÇİMİ: iPhone/Android ölçüleri — oran değişir, UA tazelenir */
|
|
6191
|
+
function phoneDeviceUA() {
|
|
6192
|
+
const d = phoneDevice();
|
|
6193
|
+
return d.apple ? PHONE_UA_IOS : PHONE_UA;
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
function setBrowserDevice(key) {
|
|
6197
|
+
const k = String(key || '').trim();
|
|
6198
|
+
if (!PHONE_DEVICES[k]) return { ok: false, error: 'bilinmeyen cihaz' };
|
|
6199
|
+
if (browser.deviceKey === k) return { ok: true, device: k, mobile: browser.mobile };
|
|
6200
|
+
browser.deviceKey = k;
|
|
6201
|
+
settings.browserDevice = k;
|
|
6202
|
+
saveSettings();
|
|
6203
|
+
try {
|
|
6204
|
+
const wc = browser.view && browser.view.webContents;
|
|
6205
|
+
if (wc && browser.mobile) {
|
|
6206
|
+
wc.setUserAgent(phoneDeviceUA());
|
|
6207
|
+
let url = '';
|
|
6208
|
+
try { url = wc.getURL(); } catch {}
|
|
6209
|
+
/* UA değişimi için sayfa taze yüklenir (setBrowserPhone ile aynı mantık) */
|
|
6210
|
+
if (url && /^https?:/i.test(url)) wc.loadURL(url).catch(() => {});
|
|
6211
|
+
}
|
|
6212
|
+
} catch {}
|
|
6213
|
+
if (browser.open && win && !win.isDestroyed()) {
|
|
6214
|
+
layoutBrowser(); // yeni ekran oranı — siluet yeniden hizalanır
|
|
6215
|
+
const [w] = win.getContentSize();
|
|
6216
|
+
browserEmit({ open: true, width: browserShownWidth(w) });
|
|
6217
|
+
}
|
|
6218
|
+
return { ok: true, device: k, mobile: browser.mobile };
|
|
6219
|
+
}
|
|
6220
|
+
ipcMain.handle('browser:device', (_e, key) => setBrowserDevice(key));
|
|
6221
|
+
|
|
6222
|
+
async function probeDevUrl(url, timeoutMs = 1200) {
|
|
6223
|
+
try {
|
|
6224
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
6225
|
+
return res.status < 500;
|
|
6226
|
+
} catch {
|
|
6227
|
+
return false;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
async function devServerDetect() {
|
|
6232
|
+
for (const port of DEV_PORTS) {
|
|
6233
|
+
const url = 'http://localhost:' + port + '/';
|
|
6234
|
+
/* eslint-disable-next-line no-await-in-loop */
|
|
6235
|
+
if (await probeDevUrl(url)) return url;
|
|
6236
|
+
}
|
|
6237
|
+
return null;
|
|
6238
|
+
}
|
|
6239
|
+
ipcMain.handle('devserver:detect', () => devServerDetect());
|
|
6240
|
+
|
|
6241
|
+
/* ALT TERMINAL PAYI: alt dock terminal açıkken view yüksekliğini kısar —
|
|
6242
|
+
DOM terminali native view'ın altında kalmaz */
|
|
6243
|
+
ipcMain.handle('browser:bottomInset', (_e, px) => {
|
|
6244
|
+
/* terminal yüksekliği pencerenin %70'i olabildiğinden tavan 1200 */
|
|
6245
|
+
const v = Math.max(0, Math.min(Math.round(Number(px) || 0), 1200));
|
|
6246
|
+
if (browser.bottomInset === v) return { ok: true, inset: v };
|
|
6247
|
+
browser.bottomInset = v;
|
|
6248
|
+
layoutBrowser();
|
|
6249
|
+
return { ok: true, inset: v };
|
|
6250
|
+
});
|
|
5524
6251
|
ipcMain.handle('browser:screenshot', async () => {
|
|
5525
6252
|
const r = await browserScreenshot();
|
|
5526
6253
|
if (!r.ok) return { ok: false, error: r.error };
|
|
@@ -6491,6 +7218,113 @@ ipcMain.handle('studio:delete', async (_e, rel) => {
|
|
|
6491
7218
|
servis edilir; ajan kendi dev sunucusunu başlattıysa o adres önceliklidir. */
|
|
6492
7219
|
let bcLastServerUrl = ''; /* ajanın başlattığı dev server adresi (bc-preview'dan yakalanır) */
|
|
6493
7220
|
const bcStatic = { server: null, root: '', base: '' };
|
|
7221
|
+
/* MOBİL PROJE önizleme: npm run web (expo start --web) süreci + canlı adres */
|
|
7222
|
+
const bcMobile = { proc: null, url: '', root: '' };
|
|
7223
|
+
|
|
7224
|
+
/* workspace bir MOBİL UYGULAMA projesi mi? (expo / react-native / app.json) */
|
|
7225
|
+
function ideMobileProject(root) {
|
|
7226
|
+
try {
|
|
7227
|
+
const pkgPath = path.join(root, 'package.json');
|
|
7228
|
+
if (!fs.existsSync(pkgPath)) return { mobile: false };
|
|
7229
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
7230
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
7231
|
+
const isExpo = !!(deps.expo || deps['react-native'] || deps['react-native-web']);
|
|
7232
|
+
const hasAppJson = fs.existsSync(path.join(root, 'app.json')) || fs.existsSync(path.join(root, 'app.config.js'));
|
|
7233
|
+
const scripts = pkg.scripts || {};
|
|
7234
|
+
const hasReact = !!(deps.react || deps.next);
|
|
7235
|
+
const mobile = (isExpo || hasAppJson) && hasReact;
|
|
7236
|
+
let cmd = '';
|
|
7237
|
+
if (mobile) {
|
|
7238
|
+
if (scripts.web) cmd = 'npm run web';
|
|
7239
|
+
else if (deps.expo) cmd = 'npx expo start --web';
|
|
7240
|
+
else if (scripts.start) cmd = 'npm start';
|
|
7241
|
+
}
|
|
7242
|
+
return { mobile, cmd };
|
|
7243
|
+
} catch {
|
|
7244
|
+
return { mobile: false, cmd: '' };
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
|
|
7248
|
+
function killMobilePreview() {
|
|
7249
|
+
const p = bcMobile.proc;
|
|
7250
|
+
if (p) {
|
|
7251
|
+
try { spawn('taskkill', ['/pid', String(p.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
7252
|
+
try { p.kill(); } catch {}
|
|
7253
|
+
}
|
|
7254
|
+
bcMobile.proc = null;
|
|
7255
|
+
bcMobile.url = '';
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
/* npm run web'i başlat; dev sunucu ayağa kalkınca adresi döner (çıktı taraması +
|
|
7259
|
+
port yoklaması çift kanal). Süreç UZUN ÖMÜRLÜDÜR — yeni preview onu öldürür. */
|
|
7260
|
+
function mobilePreviewStart(root) {
|
|
7261
|
+
return new Promise((resolve) => {
|
|
7262
|
+
const info = ideMobileProject(root);
|
|
7263
|
+
if (!info.mobile || !info.cmd) return resolve({ ok: false, error: 'mobil web scripti yok — package.json scripts.web ekleyin' });
|
|
7264
|
+
/* zaten çalışıyor mu? */
|
|
7265
|
+
if (bcMobile.proc && bcMobile.url && bcMobile.root === root) {
|
|
7266
|
+
probeDevUrl(bcMobile.url, 800).then((alive) => {
|
|
7267
|
+
if (alive) resolve({ ok: true, url: bcMobile.url });
|
|
7268
|
+
else {
|
|
7269
|
+
killMobilePreview();
|
|
7270
|
+
mobilePreviewStart(root).then(resolve, () => resolve({ ok: false, error: 'başlatılamadı' }));
|
|
7271
|
+
}
|
|
7272
|
+
});
|
|
7273
|
+
return;
|
|
7274
|
+
}
|
|
7275
|
+
if (bcMobile.root !== root) killMobilePreview();
|
|
7276
|
+
bcMobile.url = '';
|
|
7277
|
+
bcMobile.root = root;
|
|
7278
|
+
let out = '';
|
|
7279
|
+
let settled = false;
|
|
7280
|
+
waLog('mobil önizleme başlatılıyor: ' + info.cmd + ' @ ' + root);
|
|
7281
|
+
const child = spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', info.cmd], { cwd: root, windowsHide: true });
|
|
7282
|
+
bcMobile.proc = child;
|
|
7283
|
+
const scan = () => {
|
|
7284
|
+
const m = /https?:\/\/(?:localhost|127\.0\.0\.1):(\d{2,5})/.exec(out);
|
|
7285
|
+
if (m && Number(m[1]) !== 19000 && Number(m[1]) !== 19001 && Number(m[1]) !== 19002) {
|
|
7286
|
+
bcMobile.url = m[0].replace(/\/+$/, '') + '/';
|
|
7287
|
+
}
|
|
7288
|
+
};
|
|
7289
|
+
child.stdout.on('data', (d) => { out += String(d); if (out.length > 60000) out = out.slice(-30000); scan(); });
|
|
7290
|
+
child.stderr.on('data', (d) => { out += String(d); if (out.length > 60000) out = out.slice(-30000); scan(); });
|
|
7291
|
+
child.on('exit', () => { if (bcMobile.proc === child) bcMobile.proc = null; });
|
|
7292
|
+
const t0 = Date.now();
|
|
7293
|
+
const timer = setInterval(async () => {
|
|
7294
|
+
scan();
|
|
7295
|
+
let url = bcMobile.url;
|
|
7296
|
+
if (!url) {
|
|
7297
|
+
for (const port of [8081, 19006, 5173]) {
|
|
7298
|
+
/* eslint-disable-next-line no-await-in-loop */
|
|
7299
|
+
if (await probeDevUrl('http://localhost:' + port + '/', 500)) { url = 'http://localhost:' + port + '/'; break; }
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
if (url) {
|
|
7303
|
+
bcMobile.url = url;
|
|
7304
|
+
clearInterval(timer);
|
|
7305
|
+
settled = true;
|
|
7306
|
+
resolve({ ok: true, url });
|
|
7307
|
+
} else if (Date.now() - t0 > 120000) {
|
|
7308
|
+
clearInterval(timer);
|
|
7309
|
+
if (!settled) resolve({ ok: false, error: 'dev sunucu 120 sn içinde ayağa kalkmadı — çıktıyı terminalden kontrol et' });
|
|
7310
|
+
}
|
|
7311
|
+
}, 1200);
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
|
|
7315
|
+
/* ajana sessiz bilgi: dev sunucu zaten çalışıyor — ikinci sunucu başlatmasın */
|
|
7316
|
+
function bcTellMobileServe(url) {
|
|
7317
|
+
try {
|
|
7318
|
+
const ws = ideRoot();
|
|
7319
|
+
const sid = bcSessions.get(ws);
|
|
7320
|
+
if (!sid) return;
|
|
7321
|
+
engine.observe(sid,
|
|
7322
|
+
'[PREVIEW] Kullanıcı mobil önizlemeyi açtı — `npm run web` dev sunucusu şu adreste ÇALIŞIYOR: ' + url + '\n' +
|
|
7323
|
+
'KENDİ dev sunucunu BAŞLATMA (port çakışır); uygulamayı bu adres üzerinden değerlendir,\n' +
|
|
7324
|
+
'kod düzenlemeleri hot-reload ile siluette canlı görünür.'
|
|
7325
|
+
);
|
|
7326
|
+
} catch {}
|
|
7327
|
+
}
|
|
6494
7328
|
const BC_MIME = {
|
|
6495
7329
|
'.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
|
|
6496
7330
|
'.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
|
|
@@ -6582,6 +7416,27 @@ ipcMain.handle('ide:previewFile', async (_e, rel) => {
|
|
|
6582
7416
|
ipcMain.handle('ide:preview', async () => {
|
|
6583
7417
|
try {
|
|
6584
7418
|
const root = ideRoot();
|
|
7419
|
+
/* MOBİL UYGULAMA PROJESİ (expo/react-native): statik sunucu değil,
|
|
7420
|
+
`npm run web` başlat → telefon silueti içinde canlı önizleme */
|
|
7421
|
+
const mob = ideMobileProject(root);
|
|
7422
|
+
if (mob.mobile) {
|
|
7423
|
+
let url = '';
|
|
7424
|
+
/* ajanın kendi dev sunucusu canlıysa öncelikli */
|
|
7425
|
+
if (bcLastServerUrl && isDevServerUrl(bcLastServerUrl)) {
|
|
7426
|
+
const alive = await probeDevUrl(bcLastServerUrl, 800).catch(() => false);
|
|
7427
|
+
if (alive) url = bcLastServerUrl;
|
|
7428
|
+
}
|
|
7429
|
+
if (!url) {
|
|
7430
|
+
const r = await mobilePreviewStart(root);
|
|
7431
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
7432
|
+
url = r.url;
|
|
7433
|
+
}
|
|
7434
|
+
setBrowserMobile(true); /* siluet + görünür tarayıcı */
|
|
7435
|
+
try { browser.view.webContents.loadURL(url).catch(() => {}); } catch {}
|
|
7436
|
+
browserEmit({ open: true, width: browserShownWidth(browserW()), url, mobile: true, phoneRect: browser.mobileRect });
|
|
7437
|
+
bcTellMobileServe(url);
|
|
7438
|
+
return { ok: true, url, mobile: true };
|
|
7439
|
+
}
|
|
6585
7440
|
const pick = (name) => {
|
|
6586
7441
|
const p = path.join(root, name);
|
|
6587
7442
|
try { return fs.existsSync(p) ? p : null; } catch { return null; }
|
|
@@ -6667,7 +7522,10 @@ ipcMain.handle('custom:set', (_e, list) => {
|
|
|
6667
7522
|
|
|
6668
7523
|
/* #Model refresh: taban zinciri tazele + kayıtlı tüm custom providerların
|
|
6669
7524
|
modellerini yeniden çek (picker anında güncellenir). */
|
|
6670
|
-
|
|
7525
|
+
/* Tüm provider modellerini yeniden çek: taban zincir (config.yaml) + custom
|
|
7526
|
+
provider /models listeleri. UI düğmesi, /model refresh (chat + WhatsApp)
|
|
7527
|
+
aynı bu fonksiyonu kullanır. */
|
|
7528
|
+
async function refreshModelsAll() {
|
|
6671
7529
|
try {
|
|
6672
7530
|
const cfg = loadBeastConfig();
|
|
6673
7531
|
if (cfg && engine.refreshBaseChain) engine.refreshBaseChain(cfg);
|
|
@@ -6711,7 +7569,9 @@ ipcMain.handle('models:refresh', async () => {
|
|
|
6711
7569
|
}
|
|
6712
7570
|
log.info('main', `model refresh: taban tazelendi, ${updated} custom provider güncellendi`);
|
|
6713
7571
|
return engine.publicState();
|
|
6714
|
-
}
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7574
|
+
ipcMain.handle('models:refresh', async () => refreshModelsAll());
|
|
6715
7575
|
|
|
6716
7576
|
ipcMain.handle('providers:builtin', () => BUILTIN_PROVIDERS);
|
|
6717
7577
|
|
|
@@ -7074,6 +7934,10 @@ ipcMain.handle('wa:tts:get', () => settings.waTts || {});
|
|
|
7074
7934
|
ipcMain.handle('wa:tts:set', (_e, cfg) => {
|
|
7075
7935
|
settings.waTts = {
|
|
7076
7936
|
enabled: !!(cfg && cfg.enabled),
|
|
7937
|
+
/* motor: 'edge' (ücretsiz yerel, varsayılan) | 'openai' (OpenAI-uyumlu API) */
|
|
7938
|
+
engine: (cfg && cfg.engine) === 'openai' ? 'openai' : 'edge',
|
|
7939
|
+
edgeVoice: String((cfg && cfg.edgeVoice) || 'tr-TR-AhmetNeural').trim(),
|
|
7940
|
+
chatAutoSpeak: !!(cfg && cfg.chatAutoSpeak),
|
|
7077
7941
|
baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
|
|
7078
7942
|
key: String((cfg && cfg.key) || '').trim(),
|
|
7079
7943
|
model: String((cfg && cfg.model) || '').trim(),
|
|
@@ -7083,6 +7947,22 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
|
|
|
7083
7947
|
return settings.waTts;
|
|
7084
7948
|
});
|
|
7085
7949
|
|
|
7950
|
+
/* CHAT TTS: masaüstü sohbetinde ajanın son yazısını seslendirir (base64 mp3).
|
|
7951
|
+
chatAutoSpeak açıkken renderer 'done' olayında bu kanalı çağırır. */
|
|
7952
|
+
ipcMain.handle('tts:synthesize', async (_e, text) => {
|
|
7953
|
+
const cfg = settings.waTts || {};
|
|
7954
|
+
if (!cfg.enabled) return { ok: false, error: 'tts kapalı' };
|
|
7955
|
+
const t = String(text || '').trim();
|
|
7956
|
+
if (!t) return { ok: false, error: 'boş metin' };
|
|
7957
|
+
const audio = await synthesizeSpeech(t.slice(0, 4000));
|
|
7958
|
+
if (!audio) {
|
|
7959
|
+
waLog('tts chat seslendirilemedi — motor: ' + (cfg.engine || 'edge'));
|
|
7960
|
+
return { ok: false, error: 'seslendirilemedi' };
|
|
7961
|
+
}
|
|
7962
|
+
waLog('tts chat ok: ' + audio.length + ' bayt, motor: ' + (cfg.engine || 'edge') + ', ses: ' + (cfg.edgeVoice || '-'));
|
|
7963
|
+
return { ok: true, audioB64: audio.toString('base64'), mime: 'audio/mpeg' };
|
|
7964
|
+
});
|
|
7965
|
+
|
|
7086
7966
|
/* ---------- Telegram IPC (FEATURE 3) ---------- */
|
|
7087
7967
|
|
|
7088
7968
|
ipcMain.handle('tg:status:get', () => {
|