beast-agent 0.21.0 → 0.23.0

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
@@ -4,6 +4,7 @@ const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu,
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const http = require('http');
7
+ const dns = require('dns');
7
8
  const crypto = require('crypto');
8
9
  const { spawn } = require('child_process');
9
10
  const Engine = require('./agent/engine');
@@ -14,6 +15,7 @@ const memory = require('./agent/memory');
14
15
  const skillsMod = require('./agent/skills');
15
16
  const storeMod = require('./agent/store');
16
17
  const { WhatsAppBridge } = require('./agent/whatsapp');
18
+ const { TelegramBridge } = require('./agent/telegram');
17
19
  const cron = require('./cron');
18
20
  const watchers = require('./agent/watchers');
19
21
  const usageMod = require('./agent/usage');
@@ -193,6 +195,7 @@ const BUILTIN_PROVIDERS = [
193
195
  { id: 'gemini', name: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', hint: 'aistudio.google.com/apikey' },
194
196
  { id: 'zhipu', name: 'Zhipu AI', baseUrl: 'https://api.z.ai/api/paas/v4', hint: 'z.ai model konsolu' },
195
197
  { id: 'groq', name: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', hint: 'console.groq.com/keys' },
198
+ { id: 'nvidia', name: 'NVIDIA NIM', baseUrl: 'https://integrate.api.nvidia.com/v1', hint: 'build.nvidia.com — ücretsiz kredi veriyor, talep yüksek' },
196
199
  ];
197
200
  const SESSIONS_DIR = path.join(APP_DIR, 'sessions');
198
201
  const SETTINGS_FILE = path.join(APP_DIR, 'settings.json');
@@ -200,6 +203,8 @@ const SETTINGS_BACKUP_FILE = path.join(APP_DIR, 'settings.backup.json');
200
203
  const WA_AUTH_DIR = path.join(APP_DIR, 'wa-auth');
201
204
  const WA_CHATS_FILE = path.join(APP_DIR, 'wa-chats.json');
202
205
  const FALLOUT_CRASH_FILE = path.join(APP_DIR, 'fallout-crash.json');
206
+ const CHAT_QUEUE_FILE = path.join(APP_DIR, 'chat_queue.json');
207
+ const TG_CHATS_FILE = path.join(APP_DIR, 'tg-chats.json');
203
208
 
204
209
  for (const d of [APP_DIR, SESSIONS_DIR]) fs.mkdirSync(d, { recursive: true });
205
210
 
@@ -215,6 +220,10 @@ let waChats = new Map(); // jid -> aktif session id
215
220
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
216
221
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
217
222
  const WA_HISTORY_CAP = 20;
223
+ let tg = null;
224
+ let tgChats = new Map(); // telegram chatId -> aktif session id
225
+ let tgHistory = new Map(); // chatId -> [sid,...]
226
+ const TG_HISTORY_CAP = 20;
218
227
  let tray = null;
219
228
  app.isQuitting = false;
220
229
 
@@ -449,6 +458,34 @@ function isWaAllowed(senderNum) {
449
458
  return !!waFind(senderNum);
450
459
  }
451
460
 
461
+ /* ---------- TELEGRAM (FEATURE 3): allow list — WA ile aynı mantık ----------
462
+ Liste formatı: [{ id:'123456789' | '@kullanici_adi', name, perm, bot_id }, '*']
463
+ Eşleşme: sayısal ID birebir, @username büyük/küçük harf duyarsız. */
464
+ function tgLog(line) {
465
+ try { log.info('telegram', line); } catch {}
466
+ }
467
+
468
+ function tgFind(senderId, username) {
469
+ const list = settings.tgAllow || [];
470
+ if (!list.length) return null; // boş liste = kimseye cevap yok
471
+ const id = String(senderId || '').trim();
472
+ const uname = String(username || '').replace(/^@/, '').toLowerCase();
473
+ for (const e of list) {
474
+ if (e === '*') return { id: '*', name: '' };
475
+ const eid = typeof e === 'string' ? e.trim() : String((e && e.id) || '').trim();
476
+ if (!eid) continue;
477
+ if (eid === '*') return { id: '*', name: '' };
478
+ if (eid.startsWith('@')) {
479
+ if (uname && eid.slice(1).toLowerCase() === uname) {
480
+ return typeof e === 'string' ? { id: eid, name: '' } : e;
481
+ }
482
+ } else if (id && eid === id) {
483
+ return typeof e === 'string' ? { id: eid, name: '' } : e;
484
+ }
485
+ }
486
+ return null;
487
+ }
488
+
452
489
  /* Sahip: owner işaretli kayıt; yoksa listedeki ilk kişi. /allow ve /block
453
490
  yalnızca sahip tarafından kullanılabilir (yabancı DM kendi kendini ekleyemesin). */
454
491
  function waOwnerNum() {
@@ -1333,13 +1370,37 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
1333
1370
  /* ---------------- BOT SİSTEMİ yardımcıları ----------------
1334
1371
  Bot eşleştirme, izolasyon, whitelist.json aynası ve bot istatistikleri. */
1335
1372
 
1336
- const PERM_RANK = { all: 3, web: 2, read: 1, chat: 0 }; // küçük = kısıtlı
1373
+ /* İzin değerini araç kümesine çevirir; null = tüm araçlar ('all').
1374
+ 'web' / ['web','read'] / 'web,read' biçimlerini kabul eder. */
1375
+ function permToToolSet(p) {
1376
+ const { PERM_TOOL_SETS } = require('./agent/engine');
1377
+ const set = new Set();
1378
+ for (const raw of Array.isArray(p) ? p : String(p == null ? 'all' : p).split(',')) {
1379
+ const k = String(raw).trim() || 'all';
1380
+ if (k === 'all' || !PERM_TOOL_SETS[k]) return null;
1381
+ for (const t of PERM_TOOL_SETS[k]) set.add(t);
1382
+ }
1383
+ return set;
1384
+ }
1337
1385
 
1386
+ /* Hangi izin daha kısıtlıysa o kazanır. Dizi (çoklu bot izni) destekler:
1387
+ araç kümesi diğerinin alt kümesiyse o geçer; ikisi de alt küme değilse
1388
+ daha küçük küme kazanır (eşitse kişi yetkisi). */
1338
1389
  function moreRestrictivePerm(a, b) {
1339
- const ra = PERM_RANK[a] ?? 3;
1340
- const rb = PERM_RANK[b] ?? 3;
1341
- const keys = Object.keys(PERM_RANK);
1342
- return keys.find((k) => PERM_RANK[k] === Math.min(ra, rb)) || 'all';
1390
+ const sa = permToToolSet(a);
1391
+ const sb = permToToolSet(b);
1392
+ if (sa === null && sb === null) return 'all';
1393
+ if (sa === null) return b; // b kısıtlı
1394
+ if (sb === null) return a; // a kısıtlı
1395
+ const aSubB = [...sa].every((t) => sb.has(t));
1396
+ const bSubA = [...sb].every((t) => sa.has(t));
1397
+ if (aSubB && !bSubA) return a;
1398
+ if (bSubA && !aSubB) return b;
1399
+ return sa.size <= sb.size ? a : b;
1400
+ }
1401
+
1402
+ function fmtPerm(p) {
1403
+ return Array.isArray(p) ? '[' + p.join('+') + ']' : String(p);
1343
1404
  }
1344
1405
 
1345
1406
  /* Bot skill checkbox'ları → oturumun görebileceği araç adları.
@@ -1623,7 +1684,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
1623
1684
  } else {
1624
1685
  engine.setSessionTools(sid, null);
1625
1686
  }
1626
- waLog(`perm=${perm} bot=${botId} sid=${sid}`);
1687
+ waLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
1627
1688
 
1628
1689
  const participantName = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
1629
1690
  /* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
@@ -1734,6 +1795,206 @@ function ensureWa() {
1734
1795
  return wa;
1735
1796
  }
1736
1797
 
1798
+ /* ---------- TELEGRAM ENTEGRASYONU (FEATURE 3) ----------
1799
+ WhatsApp ile aynı akış: gelen mesaj → allow list kontrolü → oturuma bağla
1800
+ (bot eşleme + granül izin) → engine.send; cevap done/error olayında geri
1801
+ gider. Anti-spam: 4.5 sn birleştirme penceresi (WA ile aynı). */
1802
+
1803
+ (function tgChatsLoad() {
1804
+ try {
1805
+ const raw = JSON.parse(fs.readFileSync(TG_CHATS_FILE, 'utf8'));
1806
+ if (raw && typeof raw.chats === 'object') {
1807
+ for (const [c, s] of Object.entries(raw.chats)) {
1808
+ if (typeof s === 'string') tgChats.set(c, s);
1809
+ }
1810
+ }
1811
+ if (raw && typeof raw.history === 'object') {
1812
+ for (const [c, arr] of Object.entries(raw.history)) {
1813
+ if (Array.isArray(arr)) tgHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-TG_HISTORY_CAP));
1814
+ }
1815
+ }
1816
+ for (const [c, s] of tgChats.entries()) {
1817
+ const h = tgHistory.get(c) || [];
1818
+ if (!h.includes(s)) h.push(s);
1819
+ tgHistory.set(c, h.slice(-TG_HISTORY_CAP));
1820
+ }
1821
+ } catch {}
1822
+ })();
1823
+
1824
+ function saveTgChats() {
1825
+ try {
1826
+ fs.writeFileSync(
1827
+ TG_CHATS_FILE,
1828
+ JSON.stringify({
1829
+ chats: Object.fromEntries(tgChats),
1830
+ history: Object.fromEntries([...tgHistory.entries()].map(([c, a]) => [c, a.slice(-TG_HISTORY_CAP)])),
1831
+ })
1832
+ );
1833
+ } catch {}
1834
+ }
1835
+
1836
+ function tgRememberSession(chatId, sid) {
1837
+ const h = tgHistory.get(chatId) || [];
1838
+ if (!h.includes(sid)) h.push(sid);
1839
+ tgHistory.set(chatId, h.slice(-TG_HISTORY_CAP));
1840
+ }
1841
+
1842
+ const TG_DEBOUNCE_MS = 4500;
1843
+ const tgQueue = new Map(); // chatId -> { timer, payloads[] }
1844
+
1845
+ function tgQueuePush(chatId, payload) {
1846
+ let q = tgQueue.get(chatId);
1847
+ if (!q) {
1848
+ q = { payloads: [] };
1849
+ tgQueue.set(chatId, q);
1850
+ }
1851
+ q.payloads.push(payload);
1852
+ clearTimeout(q.timer);
1853
+ tgLog(`queue: mesaj kuyruğa girdi chat=${chatId} toplam=${q.payloads.length} (4.5 sn birleştirme)`);
1854
+ q.timer = setTimeout(() => {
1855
+ tgFlush(chatId).catch((e) => tgLog(`flush KRASİ: ${String((e && e.stack) || e)}`));
1856
+ }, TG_DEBOUNCE_MS);
1857
+ }
1858
+
1859
+ async function tgFlush(chatId) {
1860
+ const q = tgQueue.get(chatId);
1861
+ if (!q) return;
1862
+ tgQueue.delete(chatId);
1863
+ const merged = { text: '', senderId: '', username: '', senderName: '' };
1864
+ for (const p of q.payloads) {
1865
+ if (p.text) merged.text += (merged.text ? '\n' : '') + p.text;
1866
+ if (!merged.senderId && p.senderId) { merged.senderId = p.senderId; merged.username = p.username; merged.senderName = p.senderName; }
1867
+ }
1868
+ await processTgMessage(chatId, merged);
1869
+ }
1870
+
1871
+ async function handleTgIncoming(chatId, payload) {
1872
+ try {
1873
+ if (!engine) return;
1874
+ /* v1: yalnız birebir sohbetler — grup davranışı WA'daki gibi ayrı toggle ile gelir */
1875
+ if (payload.isGroup) {
1876
+ tgLog(`skip: grup mesajı chat=${chatId} (grup desteği kapalı)`);
1877
+ return;
1878
+ }
1879
+ const hit = tgFind(payload.senderId, payload.username);
1880
+ tgLog(
1881
+ `incoming chat=${chatId} sender=${payload.senderId || '?'} user=${payload.username || '-'} allowed=${!!hit}` +
1882
+ (hit && hit.name ? ' name=' + hit.name : '')
1883
+ );
1884
+ if (!hit) return; // allowlist dışı yoksay
1885
+ /* İsimsiz kayıt: güvenlik için cevap verme — kullanıcıyı ayarlara yönlendir */
1886
+ if (hit.id !== '*' && !hit.name) {
1887
+ tgLog(`skip: isimsiz kayıt (${hit.id}) — cevap verilmedi, Entegrasyonlar'da isim ekle`);
1888
+ return;
1889
+ }
1890
+ resumeServices(); // pause durumunda gelen mesaj servisleri canlandırır
1891
+ tgQueuePush(String(chatId), payload);
1892
+ } catch (e) {
1893
+ tgLog(`handleTgIncoming KRASİ: ${String((e && e.stack) || e)}`);
1894
+ }
1895
+ }
1896
+
1897
+ async function processTgMessage(chatId, payload, requeues = 0) {
1898
+ const hit = tgFind(payload.senderId, payload.username);
1899
+ if (!hit) {
1900
+ tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
1901
+ return;
1902
+ }
1903
+ let sid = tgChats.get(chatId);
1904
+ if (sid && engine.isBusy(sid)) {
1905
+ /* oturum meşgul — WA ile aynı: kaybetme, iş bitene dek yeniden dene */
1906
+ await new Promise((r) => setTimeout(r, TG_DEBOUNCE_MS));
1907
+ return processTgMessage(chatId, payload, requeues + 1);
1908
+ }
1909
+ if (!sid) {
1910
+ const v = engine.createSession();
1911
+ sid = v.id;
1912
+ tgChats.set(chatId, sid);
1913
+ tgRememberSession(chatId, sid);
1914
+ saveTgChats();
1915
+ } else {
1916
+ tgRememberSession(chatId, sid);
1917
+ }
1918
+ /* Kişi bazlı granül izin: all/web/read/chat */
1919
+ let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
1920
+ engine.setSessionPerm(sid, perm);
1921
+
1922
+ /* BOT SİSTEMİ: izinli kayıtta bot_id yoksa beast'e düşer (WA ile aynı) */
1923
+ let botId = hit && hit.bot_id ? String(hit.bot_id) : 'beast';
1924
+ if (!bots.get(botId)) {
1925
+ if (botId !== 'beast') tgLog(`bot="${botId}" yok — kayıt botsuz, beast (admin) botuna yönlendirildi`);
1926
+ botId = 'beast';
1927
+ }
1928
+ engine.setSessionBot(sid, botId);
1929
+ const botCfg = bots.get(botId);
1930
+ if (botCfg && !botCfg.admin) {
1931
+ const eff = moreRestrictivePerm(perm, botCfg.perm || 'all');
1932
+ if (eff !== perm) {
1933
+ perm = eff;
1934
+ engine.setSessionPerm(sid, eff);
1935
+ }
1936
+ engine.setSessionTools(sid, botToolSet(botCfg));
1937
+ } else {
1938
+ engine.setSessionTools(sid, null);
1939
+ }
1940
+ tgLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
1941
+
1942
+ /* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
1943
+ const isOwner = !!hit.owner;
1944
+ const roleTag = isOwner
1945
+ ? 'SAHİBİN (talepleri önceliklidir)'
1946
+ : 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
1947
+ const label =
1948
+ (hit.name || payload.senderName || '?') +
1949
+ (payload.username ? ` (@${payload.username})` : '') +
1950
+ ` — ${roleTag}`;
1951
+ let text = `[Telegram — gönderen: ${label}]`;
1952
+ if (!isOwner) {
1953
+ text += `\n[NOT: Bu kişi SAHİP DEĞİL, misafirdir. Sahibin ayarlarını/verilerini değiştirme; kalıcı hafızaya misafire özel bilgi yazma.]`;
1954
+ }
1955
+ text += `\n${String(payload.text || '').slice(0, 6000)}`;
1956
+ engine.send(sid, { text: text.slice(0, 8000), attachments: [] });
1957
+ }
1958
+
1959
+ async function sendTgSafe(chatId, text) {
1960
+ if (!tg) return false;
1961
+ try {
1962
+ return !!(await tg.send(chatId, text));
1963
+ } catch (e) {
1964
+ tgLog(`send hata chat=${chatId}: ${String((e && e.message) || e)}`);
1965
+ return false;
1966
+ }
1967
+ }
1968
+
1969
+ function ensureTg() {
1970
+ if (!tg) {
1971
+ tg = new TelegramBridge({
1972
+ token: settings.tgToken || '',
1973
+ emit: (ev) => {
1974
+ if (ev.type === 'status') tgLog(`status=${ev.status}${ev.user ? ' user=' + ev.user : ''}`);
1975
+ if (win && !win.isDestroyed()) win.webContents.send('tg:event', ev);
1976
+ },
1977
+ onIncoming: handleTgIncoming,
1978
+ });
1979
+ }
1980
+ return tg;
1981
+ }
1982
+
1983
+ /* token değişimi / yeniden başlatma: eski köprüyü kapat, yenisini aç */
1984
+ async function restartTg() {
1985
+ if (tg) {
1986
+ try { await tg.stop(); } catch {}
1987
+ tg = null;
1988
+ }
1989
+ if (!settings.tgToken) return;
1990
+ const b = ensureTg();
1991
+ try {
1992
+ await b.start();
1993
+ } catch (e) {
1994
+ tgLog(`start başarısız: ${String((e && e.message) || e)}`);
1995
+ }
1996
+ }
1997
+
1737
1998
  function reloadBackend() {
1738
1999
  if (engine && typeof engine.dispose === 'function') {
1739
2000
  try { engine.dispose(); } catch {}
@@ -1861,6 +2122,27 @@ function reloadBackend() {
1861
2122
  })();
1862
2123
  }
1863
2124
  }
2125
+ // Telegram oturumlarının son cevabını geri gönder (WA ile aynı akış)
2126
+ if ((ev.type === 'done' || ev.type === 'error') && tg && tg.connected) {
2127
+ const hitT = [...tgChats.entries()].find(([, s]) => s === ev.sessionId);
2128
+ if (hitT) {
2129
+ const tgid = hitT[0];
2130
+ (async () => {
2131
+ try {
2132
+ if (ev.type === 'error') {
2133
+ await sendTgSafe(tgid, 'Bir aksilik oldu: ' + String(ev.error || '').slice(0, 200));
2134
+ return;
2135
+ }
2136
+ if (!ev.aborted) {
2137
+ const s = engine.openSession(ev.sessionId);
2138
+ const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
2139
+ const txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
2140
+ if (txt.trim()) await sendTgSafe(tgid, txt);
2141
+ }
2142
+ } catch {}
2143
+ })();
2144
+ }
2145
+ }
1864
2146
  },
1865
2147
  });
1866
2148
  return engine.publicState();
@@ -2017,6 +2299,10 @@ app.whenReady().then(() => {
2017
2299
  startNpmUpdateWatch(); // npm kurulumunda registry üzerinden otomatik sürüm kontrolü
2018
2300
  /* FEATURE 2: offline kuyruk işçisi — 30 sn'de bir bağlantı kontrolü + kuyruk boşaltma */
2019
2301
  setInterval(() => { mqueueTick().catch(() => {}); }, 30000).unref();
2302
+ /* OFFLINE MESAJ KUYRUĞU: gerçek bağlantı yoklaması — 8 sn'de bir DNS probe.
2303
+ Bağlantı dönünce kuyruktaki chat mesajları otomatik gönderilir. */
2304
+ netCheck().catch(() => {});
2305
+ setInterval(() => { netCheck().catch(() => {}); }, NET_CHECK_MS).unref();
2020
2306
 
2021
2307
  // #12 STT prefetch: whisper modelini arka planda hazırla (ilk sesli mesajda bekleme olmasın)
2022
2308
  if (settings.sttPrefetch !== false) {
@@ -2030,6 +2316,11 @@ app.whenReady().then(() => {
2030
2316
  // WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
2031
2317
  ensureWa().start().catch((e) => waLog('autostart failed: ' + (e && e.message)));
2032
2318
 
2319
+ // Telegram köprüsünü otomatik başlat (token kayıtlıysa)
2320
+ if (settings.tgToken) {
2321
+ ensureTg().start().catch((e) => tgLog('autostart failed: ' + String((e && e.message) || e)));
2322
+ }
2323
+
2033
2324
  app.on('activate', () => {
2034
2325
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
2035
2326
  });
@@ -3198,6 +3489,12 @@ function queueDesktopMessage(sessionId, text) {
3198
3489
  const t = isObj ? String((text && text.text) || '') : String(text ?? '');
3199
3490
  const hasAtts = isObj && Array.isArray(text.attachments) && text.attachments.length > 0;
3200
3491
  if (!sid || (!t.trim() && !hasAtts)) return; // boş içerik kuyruğa girmez
3492
+ /* FEATURE: OFFLINE MESAJ KUYRUĞU — internet yokken gelen mesaj diskte bekler,
3493
+ bağlantı geri gelince otomatik gönderilir */
3494
+ if (!netOnline) {
3495
+ chatQueueOfflineAdd(sid, { text: t, attachments: hasAtts ? text.attachments : undefined });
3496
+ return;
3497
+ }
3201
3498
  let q = desktopQueue.get(sid);
3202
3499
  if (!q) {
3203
3500
  q = { timer: null, msgs: [] };
@@ -3225,6 +3522,13 @@ async function flushDesktop(sessionId) {
3225
3522
  return;
3226
3523
  }
3227
3524
  if (engine.isBusy(sid)) return; // hâlâ çalışıyor — done eventini bekle
3525
+ /* debounce penceresinde internet koptuysa mesajlar offline kuyruğa düşer */
3526
+ if (!netOnline) {
3527
+ desktopQueue.delete(sid);
3528
+ clearTimeout(q.timer);
3529
+ for (const m of q.msgs) chatQueueOfflineAdd(sid, { text: m.text, attachments: m.attachments });
3530
+ return;
3531
+ }
3228
3532
  desktopQueue.delete(sid);
3229
3533
  clearTimeout(q.timer);
3230
3534
 
@@ -3251,6 +3555,144 @@ function flushDesktopOnDone(ev) {
3251
3555
  }
3252
3556
  }
3253
3557
 
3558
+ /* ---------- OFFLINE MESAJ KUYRUĞU (masaüstü sohbet) ----------
3559
+ İnternet yokken/kopukken gönderilen chat mesajları kaybolmasın:
3560
+ - Mesaj diskteki kuyruğa yazılır (chat_queue.json — elektrik kesintisine dayanıklı)
3561
+ - Bağlantı geri gelince (DNS kontrolü) sırayla otomatik gönderilir
3562
+ - Renderer'a 'net' / 'netQueue' olayları gider: ⏳ kuyruk balonu + toast */
3563
+ const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
3564
+ const NET_CHECK_MS = 8000;
3565
+ const NET_CHECK_TIMEOUT = 4000;
3566
+ const CHAT_QUEUE_MAX = 50; // kuyruk üst sınırı — taşarsa en eski düşer
3567
+
3568
+ let netOnline = true; // son bilinen bağlantı durumu (başlangıçta iyimser)
3569
+ let netCheckedOnce = false;
3570
+ let netCheckBusy = false;
3571
+ let chatQueueFlushing = false;
3572
+ const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
3573
+
3574
+ /* diskten yükle (app restart sonrası kuyruk korunur) */
3575
+ (function chatQueueLoad() {
3576
+ try {
3577
+ const j = JSON.parse(fs.readFileSync(CHAT_QUEUE_FILE, 'utf8'));
3578
+ const items = Array.isArray(j.items) ? j.items : [];
3579
+ for (const it of items) {
3580
+ if (it && typeof it === 'object' && it.sessionId && (String(it.text || '').trim() || (Array.isArray(it.attachments) && it.attachments.length))) {
3581
+ chatOfflineQueue.push(it);
3582
+ }
3583
+ }
3584
+ } catch {}
3585
+ })();
3586
+
3587
+ function chatQueueSave() {
3588
+ try {
3589
+ const tmp = CHAT_QUEUE_FILE + '.tmp';
3590
+ fs.writeFileSync(tmp, JSON.stringify({ items: chatOfflineQueue }, null, 2));
3591
+ fs.renameSync(tmp, CHAT_QUEUE_FILE); // atomik yazım — yarı kalmış dosya olmaz
3592
+ } catch {}
3593
+ }
3594
+
3595
+ function chatQueueEmit(extra = {}) {
3596
+ try {
3597
+ if (win && !win.isDestroyed()) {
3598
+ win.webContents.send('agent:event', {
3599
+ type: 'netQueue',
3600
+ online: netOnline,
3601
+ count: chatOfflineQueue.length,
3602
+ ...extra,
3603
+ });
3604
+ }
3605
+ } catch {}
3606
+ }
3607
+
3608
+ /* gönderilemeyen mesajı kuyruğa al */
3609
+ function chatQueueOfflineAdd(sessionId, { text, attachments }) {
3610
+ const item = {
3611
+ key: 'oq' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
3612
+ sessionId: String(sessionId || ''),
3613
+ text: String(text || '').slice(0, 100000),
3614
+ attachments: Array.isArray(attachments) ? attachments.slice(0, 5) : undefined,
3615
+ at: new Date().toISOString(),
3616
+ };
3617
+ chatOfflineQueue.push(item);
3618
+ while (chatOfflineQueue.length > CHAT_QUEUE_MAX) chatOfflineQueue.shift();
3619
+ chatQueueSave();
3620
+ log.info('main', `offline kuyruk: mesaj eklendi (${chatOfflineQueue.length} bekliyor) sid=${item.sessionId}`);
3621
+ chatQueueEmit({
3622
+ queued: true,
3623
+ key: item.key,
3624
+ sessionId: item.sessionId,
3625
+ text: item.text,
3626
+ attCount: item.attachments ? item.attachments.length : 0,
3627
+ });
3628
+ }
3629
+
3630
+ /* kuyruğu normal akışa (debounce → engine) verir */
3631
+ async function flushChatQueue() {
3632
+ if (chatQueueFlushing) return;
3633
+ if (!chatOfflineQueue.length) return;
3634
+ if (!netOnline) return;
3635
+ chatQueueFlushing = true;
3636
+ try {
3637
+ const keys = [];
3638
+ while (chatOfflineQueue.length) {
3639
+ const it = chatOfflineQueue.shift();
3640
+ keys.push(it.key);
3641
+ const payload = it.attachments && it.attachments.length ? { text: it.text, attachments: it.attachments } : it.text;
3642
+ queueDesktopMessage(it.sessionId, payload);
3643
+ }
3644
+ chatQueueSave();
3645
+ if (keys.length) {
3646
+ log.info('main', `offline kuyruk boşaltıldı: ${keys.length} mesaj gönderiliyor`);
3647
+ chatQueueEmit({ flushed: keys.length, keys });
3648
+ }
3649
+ } finally {
3650
+ chatQueueFlushing = false;
3651
+ }
3652
+ }
3653
+
3654
+ /* gerçek internet kontrolü: DNS çözümlemesi (sadece ağ arayüzü değil,
3655
+ paket gerçekten çıkıyor mu test eder). Adaylar sırayla denenir. */
3656
+ function dnsProbe(host) {
3657
+ return new Promise((resolve) => {
3658
+ const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
3659
+ dns.resolve(host, 'A', (err) => {
3660
+ clearTimeout(t);
3661
+ resolve(!err);
3662
+ });
3663
+ });
3664
+ }
3665
+
3666
+ async function netCheck() {
3667
+ if (netCheckBusy) return;
3668
+ netCheckBusy = true;
3669
+ try {
3670
+ let ok = false;
3671
+ for (const h of NET_CHECK_HOSTS) {
3672
+ if (await dnsProbe(h)) { ok = true; break; }
3673
+ }
3674
+ const first = !netCheckedOnce;
3675
+ const was = netOnline;
3676
+ netOnline = ok;
3677
+ netCheckedOnce = true;
3678
+ if (was !== ok || first) {
3679
+ try {
3680
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: ok });
3681
+ } catch {}
3682
+ if (ok) {
3683
+ log.info('main', 'bağlantı geri geldi — offline kuyruk kontrol ediliyor');
3684
+ chatQueueEmit(); // renderer: pill/toast güncellensin
3685
+ flushChatQueue().catch(() => {});
3686
+ } else {
3687
+ log.info('main', 'internet bağlantısı yok — mesajlar kuyruğa alınacak');
3688
+ chatQueueEmit();
3689
+ }
3690
+ }
3691
+ } catch {} finally {
3692
+ netCheckBusy = false;
3693
+ }
3694
+ }
3695
+
3254
3696
  ipcMain.handle('model:set', (_e, sel) => {
3255
3697
  settings.modelOverride = sel;
3256
3698
  saveSettings();
@@ -3585,24 +4027,82 @@ ipcMain.handle('update:check', async () => {
3585
4027
  });
3586
4028
 
3587
4029
  /* npm kurulumunda KENDİ KENDİNİ GÜNCELLEME:
3588
- detached helper bırakır (uygulama çıkınca npm install + yeniden başlatma), sonra app.quit() */
4030
+ helper script %APPDATA%\beast'a yazılır (paket dizini değişse de yaşar) ve
4031
+ detached çalışır: 1) uygulama PID'i tamamen çıkana kadar bekler (en çok 30 sn,
4032
+ sonra zorla kapatır — EBUSY dosya kilidinin numarası), 2) npm install -g
4033
+ beast-agent@latest — kilide karşı 5 deneme, 3) tüm çıktı update.log'a yazılır,
4034
+ 4) beast-agent komut shim'i üzerinden yeniden başlatır (electron sürümü
4035
+ değişse de yol bozulmaz). Sonra app.quit(). */
3589
4036
  function npmSelfUpdate() {
3590
4037
  try {
3591
- const electronExe = process.execPath;
3592
- const appPath = app.getAppPath();
4038
+ fs.mkdirSync(APP_DIR, { recursive: true });
4039
+ const pid = String(process.pid);
3593
4040
  if (process.platform === 'win32') {
3594
- const ps =
3595
- 'Start-Sleep -Seconds 3;' +
3596
- 'npm install -g beast-agent@latest;' +
3597
- 'Start-Sleep -Seconds 1;' +
3598
- `Start-Process -FilePath '${electronExe}' -ArgumentList '\"${appPath}\"'`;
3599
- spawn('powershell.exe', ['-NoProfile', '-Command', ps], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
4041
+ const ps = [
4042
+ "param([int]$ProcId = 0)",
4043
+ "$ErrorActionPreference = 'Continue'",
4044
+ "$Log = Join-Path $env:APPDATA 'beast\\update.log'",
4045
+ "function L([string]$m) { try { Add-Content -LiteralPath $Log -Value (\"[\" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + \"] \" + $m) } catch {} }",
4046
+ "L \"=== self-update basladi (app pid: $ProcId) ===\"",
4047
+ "if ($ProcId -gt 0) {",
4048
+ " $deadline = (Get-Date).AddSeconds(30)",
4049
+ " while ((Get-Date) -lt $deadline) {",
4050
+ " if (-not (Get-Process -Id $ProcId -ErrorAction SilentlyContinue)) { break }",
4051
+ " Start-Sleep -Milliseconds 500",
4052
+ " }",
4053
+ " if (Get-Process -Id $ProcId -ErrorAction SilentlyContinue) {",
4054
+ " L 'uygulama hala acik - zorla kapatiliyor'",
4055
+ " try { Stop-Process -Id $ProcId -Force } catch {}",
4056
+ " Start-Sleep -Seconds 2",
4057
+ " }",
4058
+ "}",
4059
+ "L 'uygulama kapandi, npm install basliyor'",
4060
+ "$npmCmd = (Get-Command 'npm.cmd' -ErrorAction SilentlyContinue).Source",
4061
+ "if (-not $npmCmd) { $npmCmd = Join-Path $env:APPDATA 'npm\\npm.cmd' }",
4062
+ "$ok = $false",
4063
+ "for ($i = 1; $i -le 5; $i++) {",
4064
+ " if ($ok) { break }",
4065
+ " L \"npm install -g beast-agent@latest (deneme $i)\"",
4066
+ " $out = & $npmCmd install -g beast-agent@latest 2>&1",
4067
+ " $code = $LASTEXITCODE",
4068
+ " foreach ($line in @($out)) { L \" npm: $line\" }",
4069
+ " if ($code -eq 0) { $ok = $true } else { Start-Sleep -Seconds 3 }",
4070
+ "}",
4071
+ "if (-not $ok) {",
4072
+ " L 'HATA: npm install 5 denemede basarisiz - uygulama yeniden baslatilmiyor'",
4073
+ " exit 1",
4074
+ "}",
4075
+ "L 'npm install tamam, yeniden baslatma'",
4076
+ "$shim = Get-Command 'beast-agent.cmd' -ErrorAction SilentlyContinue",
4077
+ "if ($shim) {",
4078
+ " L \"shim uzerinden: $($shim.Source)\"",
4079
+ " Start-Process -FilePath $shim.Source -WindowStyle Hidden",
4080
+ "} else {",
4081
+ " $prefix = (& $npmCmd prefix -g 2>$null)",
4082
+ " if (-not $prefix) { $prefix = Join-Path $env:APPDATA 'npm' }",
4083
+ " $exe = Join-Path $prefix 'node_modules\\electron\\dist\\electron.exe'",
4084
+ " if (-not (Test-Path $exe)) { $exe = Join-Path $prefix 'node_modules\\beast-agent\\node_modules\\electron\\dist\\electron.exe' }",
4085
+ " $appDir = Join-Path $prefix 'node_modules\\beast-agent'",
4086
+ " L \"shim bulunamadi - dogrudan: $exe\"",
4087
+ " Start-Process -FilePath $exe -ArgumentList \"`\"$appDir`\"\" -WindowStyle Hidden",
4088
+ "}",
4089
+ "L '=== self-update bitti ==='",
4090
+ ].join('\r\n');
4091
+ const psFile = path.join(APP_DIR, 'update-helper.ps1');
4092
+ fs.writeFileSync(psFile, ps, 'utf8');
4093
+ spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', psFile, '-ProcId', pid],
4094
+ { detached: true, stdio: 'ignore', windowsHide: true }).unref();
3600
4095
  } else {
3601
- const sh = `sleep 3; npm install -g beast-agent@latest; sleep 1; '${electronExe}' '${appPath}' &`;
4096
+ const sh =
4097
+ `i=0; while [ $i -lt 60 ] && kill -0 ${pid} 2>/dev/null; do i=$((i+1)); sleep 0.5; done; ` +
4098
+ 'ok=0; for n in 1 2 3 4 5; do npm install -g beast-agent@latest && ok=1 && break; sleep 3; done; ' +
4099
+ 'if [ $ok -eq 1 ]; then nohup beast-agent >/dev/null 2>&1 & fi';
3602
4100
  spawn('sh', ['-c', sh], { detached: true, stdio: 'ignore' }).unref();
3603
4101
  }
3604
4102
  log.info('main', 'npm self-update: helper bırakıldı, uygulama kapatılıyor');
3605
- } catch {}
4103
+ } catch (e) {
4104
+ log.error('main', 'npm self-update hatası: ' + String((e && e.message) || e));
4105
+ }
3606
4106
  setTimeout(() => { try { app.quit(); } catch {} }, 400);
3607
4107
  }
3608
4108
 
@@ -4713,6 +5213,43 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
4713
5213
  return settings.waTts;
4714
5214
  });
4715
5215
 
5216
+ /* ---------- Telegram IPC (FEATURE 3) ---------- */
5217
+
5218
+ ipcMain.handle('tg:status:get', () => {
5219
+ if (!tg) return { configured: !!settings.tgToken, status: 'disconnected', user: null, connected: false };
5220
+ return { configured: true, ...tg.snapshot() };
5221
+ });
5222
+
5223
+ /* token kaydet + köprüyü (yeniden) başlat */
5224
+ ipcMain.handle('tg:set', async (_e, token) => {
5225
+ const t = String(token || '').trim();
5226
+ if (t) settings.tgToken = t;
5227
+ saveSettings();
5228
+ await restartTg();
5229
+ return { configured: !!settings.tgToken, ...(tg ? tg.snapshot() : { status: 'disconnected', user: null }) };
5230
+ });
5231
+
5232
+ ipcMain.handle('tg:start', async () => {
5233
+ if (!settings.tgToken) return { ok: false, error: 'token yok — önce bot tokenı gir' };
5234
+ await restartTg();
5235
+ return { ok: true, ...(tg ? tg.snapshot() : {}) };
5236
+ });
5237
+
5238
+ ipcMain.handle('tg:stop', async () => {
5239
+ if (tg) {
5240
+ try { await tg.stop(); } catch {}
5241
+ }
5242
+ return { ok: true };
5243
+ });
5244
+
5245
+ ipcMain.handle('tg:allow:get', () => settings.tgAllow || []);
5246
+ ipcMain.handle('tg:allow:set', (_e, list) => {
5247
+ settings.tgAllow = Array.isArray(list) ? list : [];
5248
+ saveSettings();
5249
+ return settings.tgAllow;
5250
+ });
5251
+ ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
5252
+
4716
5253
  /* ---------- e-posta IPC ---------- */
4717
5254
 
4718
5255
  ipcMain.handle('email:get', () => {