beast-agent 2.5.1 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.js CHANGED
@@ -229,17 +229,20 @@ startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
229
229
  try { setSearchChain(settings.searchChain); } catch {}
230
230
  try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
231
231
  let wa = null;
232
- let waChats = new Map(); // jid -> aktif session id
232
+ let waChats = new Map(); // jid -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
233
+ let waSingleSid = null; // KANAL TEK OTURUMU: WhatsApp için BİR oturum — silinmedikçe hep aynı
233
234
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
234
235
  let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
235
236
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
236
237
  const WA_HISTORY_CAP = 20;
237
238
  let tg = null;
238
- let tgChats = new Map(); // telegram chatId -> aktif session id
239
+ let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
240
+ let tgSingleSid = null; // KANAL TEK OTURUMU: Telegram için BİR oturum
239
241
  let tgHistory = new Map(); // chatId -> [sid,...]
240
242
  const TG_HISTORY_CAP = 20;
241
243
  let dc = null;
242
- let dcChats = new Map(); // discord channelId -> aktif session id
244
+ let dcChats = new Map(); // discord channelId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
245
+ let dcSingleSid = null; // KANAL TEK OTURUMU: Discord için BİR oturum
243
246
  let dcHistory = new Map(); // channelId -> [sid,...]
244
247
  const DC_HISTORY_CAP = 20;
245
248
  const DC_CHATS_FILE = path.join(APP_DIR, 'dc-chats.json');
@@ -278,6 +281,8 @@ try {
278
281
  if (!h.includes(s)) h.push(s);
279
282
  waHistory.set(j, h.slice(-WA_HISTORY_CAP));
280
283
  }
284
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
285
+ if (typeof raw.active === 'string' && raw.active) waSingleSid = raw.active;
281
286
  /* BeastCode modu: hangi sohbetler uzaktan kodlama yapıyor */
282
287
  if (Array.isArray(raw.bcMode)) {
283
288
  for (const j of raw.bcMode) if (typeof j === 'string') waBcMode.add(j);
@@ -293,6 +298,7 @@ function saveWaChats() {
293
298
  chats: Object.fromEntries(waChats),
294
299
  history: Object.fromEntries([...waHistory.entries()].map(([j, a]) => [j, a.slice(-WA_HISTORY_CAP)])),
295
300
  bcMode: [...waBcMode],
301
+ active: waSingleSid || '',
296
302
  })
297
303
  );
298
304
  } catch {}
@@ -1268,12 +1274,26 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1268
1274
  if (cmd === 'help') {
1269
1275
  out = waSlashHelp();
1270
1276
  } else if (cmd === 'new') {
1277
+ /* /new: kanalın TEK oturumunu kapat, yenisini aç — aynı anda iki
1278
+ WhatsApp sohbet oturumu olmaz; eski oturum silinir */
1279
+ const oldSid = waSingleSid || waChats.get(jid) || '';
1280
+ if (oldSid) {
1281
+ let olds = null;
1282
+ try { olds = engine.cache.get(oldSid) || null; } catch {}
1283
+ if (!olds || !olds.bcCode) {
1284
+ /* Beast Code panel oturumuysa dokunma — yalnız pointer taşınır */
1285
+ try { engine.deleteSession(oldSid); } catch {}
1286
+ for (const [j, h] of waHistory.entries()) waHistory.set(j, h.filter((x) => x !== oldSid));
1287
+ }
1288
+ }
1271
1289
  const v = engine.createSession();
1290
+ waSingleSid = v.id;
1291
+ for (const j of [...waChats.keys()]) waChats.set(j, v.id);
1272
1292
  waChats.set(jid, v.id);
1273
1293
  waRememberSession(jid, v.id);
1274
1294
  saveWaChats();
1275
1295
  wa.setWatchJids([...waChats.keys()]);
1276
- out = `*Yeni oturum* \`${v.code}\` açıldı — anlatabilirsin.\nDiğer oturumlara geçiş: \`/open <kod>\``;
1296
+ out = `*Yeni oturum* \`${v.code}\` açıldı — eski oturum kapatıldı, artık buradan devam.\nVar olan başka oturuma geçiş: \`/open <kod>\``;
1277
1297
  } else if (cmd === 'open') {
1278
1298
  if (!arg) {
1279
1299
  out = 'Kullanım: `/open <kod>` — kodları görmek için /sessions';
@@ -1282,7 +1302,9 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1282
1302
  if (!hit) {
1283
1303
  out = `\`${arg.toUpperCase()}\` kodlu oturum bulunamadı. Listeyi görmek için /sessions`;
1284
1304
  } else {
1285
- waChats.set(jid, hit.id);
1305
+ /* /open: kanal TEK oturumunu bu oturuma çevirir — tüm jid'ler bağlanır */
1306
+ waSingleSid = hit.id;
1307
+ for (const j of [...waChats.keys()]) waChats.set(j, hit.id);
1286
1308
  waRememberSession(jid, hit.id);
1287
1309
  saveWaChats();
1288
1310
  wa.setWatchJids([...waChats.keys()]);
@@ -1564,14 +1586,15 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
1564
1586
  try { engine.interrupt(sid, 'kullanıcı /beastagent ile sohbet moduna döndü'); } catch {}
1565
1587
  }
1566
1588
  waBcMode.delete(jid);
1567
- const v = engine.createSession();
1568
- waChats.set(jid, v.id);
1569
- waRememberSession(jid, v.id);
1589
+ /* KANAL TEK OTURUMU: yeni oturum AÇILMAZ — sohbet oturumuna dönülür
1590
+ (silinmediyse hep aynı; ilk kez ise burada oluşur) */
1591
+ const chatSid = ensureWaSession(jid);
1592
+ const chatS = engine.cache.get(chatSid);
1570
1593
  saveWaChats();
1571
1594
  if (wa) wa.setWatchJids([...waChats.keys()]);
1572
1595
  emitWaEventSafe({ type: 'bc-screen', on: false });
1573
1596
  out =
1574
- `*💬 Sohbet moduna dönüldü* — masaüstünde chat ekranı açıldı, yeni oturum \`${v.code}\`.\n` +
1597
+ `*💬 Sohbet moduna dönüldü* — masaüstünde chat ekranı açıldı, sohbet oturumu \`${(chatS && chatS.code) || '?'}\`.\n` +
1575
1598
  `Kodlar duruyor: \`${waBcWorkspace()}\`\n` +
1576
1599
  `Tekrar kodlamak için: /beastcode`;
1577
1600
  }
@@ -1682,19 +1705,58 @@ function waGroupSenderInfo(payload) {
1682
1705
  return { label, name: (hitP && hitP.name) || '', isOwner: !!(hitP && hitP.owner) };
1683
1706
  }
1684
1707
 
1685
- /* Oturum yoksa oluştur (processWaMessage ile aynı kalıp) */
1708
+ /* ---------- KANAL TEK OTURUMU (WA/TG/DC) ----------
1709
+ Her kanalın BİR aktif sohbet oturumu vardır; eskisi silinmedikçe hep
1710
+ AYNI oturumdan devam edilir — Beast kendi kendine oturum kapatıp
1711
+ yenisiyle değiştirmez. Yenisi yalnızca: (1) kayıtlı oturum dosyası
1712
+ yoksa (kullanıcı UI'dan sildi), (2) kullanıcı WA'da /new yazarsa.
1713
+ Masaüstü chat UI'daki "+ Yeni Sohbet" bu kuralın dışındadır. */
1714
+ function sessionFileAlive(sid) {
1715
+ try {
1716
+ return !!sid && fs.existsSync(path.join(engine.sessionsDir, String(sid) + '.jsonl'));
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+
1722
+ /* kanalın yaşayan tek oturumunu döndürür; geçersizse eski bağlamdan devralır,
1723
+ o da yoksa (ilk kullanım) yeni açar ve tüm jid'leri ona bağlar */
1686
1724
  function ensureWaSession(jid) {
1687
- let sid = waChats.get(jid);
1688
- if (!sid) {
1689
- const v = engine.createSession();
1690
- sid = v.id;
1691
- waChats.set(jid, sid);
1725
+ if (!waSingleSid || !sessionFileAlive(waSingleSid)) {
1726
+ let best = '';
1727
+ let bestTs = '';
1728
+ for (const c of new Set([...waChats.values(), waSingleSid])) {
1729
+ if (!c || !sessionFileAlive(c)) continue;
1730
+ let ts = '';
1731
+ try {
1732
+ const s = engine.cache.get(c) || engine._load(c);
1733
+ ts = String((s && s.updatedAt) || '');
1734
+ } catch {}
1735
+ if (!best || ts > bestTs) {
1736
+ best = c;
1737
+ bestTs = ts;
1738
+ }
1739
+ }
1740
+ waSingleSid = best || engine.createSession().id;
1741
+ saveWaChats();
1742
+ }
1743
+ let dirty = false;
1744
+ for (const [j, s] of waChats.entries()) {
1745
+ if (s !== waSingleSid) {
1746
+ waChats.set(j, waSingleSid);
1747
+ dirty = true;
1748
+ }
1749
+ }
1750
+ if (jid != null && waChats.get(String(jid)) !== waSingleSid) {
1751
+ waChats.set(String(jid), waSingleSid);
1752
+ dirty = true;
1753
+ }
1754
+ waRememberSession(String(jid), waSingleSid);
1755
+ if (dirty) {
1692
1756
  saveWaChats();
1693
1757
  if (wa) wa.setWatchJids([...waChats.keys()]);
1694
- } else {
1695
- waRememberSession(jid, sid);
1696
1758
  }
1697
- return sid;
1759
+ return waSingleSid;
1698
1760
  }
1699
1761
 
1700
1762
  /* ---------- GRUP BAĞLAM AKIŞI (mentionOnly + seeAll) ----------
@@ -2232,7 +2294,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2232
2294
  waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
2233
2295
  return;
2234
2296
  }
2235
- let sid = waChats.get(jid);
2297
+ let sid = waChats.get(jid) || waSingleSid;
2236
2298
  if (sid && engine.isBusy(sid)) {
2237
2299
  /* oturum hâlâ önceki işle uğraşıyor — KAYBETME: sınırı kaldırdık,
2238
2300
  iş bitene dek her debounce turunda yeniden deneriz; kullanıcıya
@@ -2246,16 +2308,8 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
2246
2308
  if (!isGroup && requeues > 0) {
2247
2309
  // retry sonrası izin yeniden kontrol edilir (yukarıda hit zaten alınıyor)
2248
2310
  }
2249
- if (!sid) {
2250
- const v = engine.createSession();
2251
- sid = v.id;
2252
- waChats.set(jid, sid);
2253
- waRememberSession(jid, sid);
2254
- saveWaChats();
2255
- if (wa) wa.setWatchJids([...waChats.keys()]);
2256
- } else {
2257
- waRememberSession(jid, sid);
2258
- }
2311
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2312
+ sid = ensureWaSession(jid);
2259
2313
  // Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
2260
2314
  wa.setComposing(jid, true);
2261
2315
  // Kişi bazlı granül izin: all/web/read/chat
@@ -2462,6 +2516,8 @@ function ensureWa() {
2462
2516
  if (Array.isArray(arr)) tgHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-TG_HISTORY_CAP));
2463
2517
  }
2464
2518
  }
2519
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2520
+ if (raw && typeof raw.active === 'string' && raw.active) tgSingleSid = raw.active;
2465
2521
  for (const [c, s] of tgChats.entries()) {
2466
2522
  const h = tgHistory.get(c) || [];
2467
2523
  if (!h.includes(s)) h.push(s);
@@ -2477,6 +2533,7 @@ function saveTgChats() {
2477
2533
  JSON.stringify({
2478
2534
  chats: Object.fromEntries(tgChats),
2479
2535
  history: Object.fromEntries([...tgHistory.entries()].map(([c, a]) => [c, a.slice(-TG_HISTORY_CAP)])),
2536
+ active: tgSingleSid || '',
2480
2537
  })
2481
2538
  );
2482
2539
  } catch {}
@@ -2549,21 +2606,14 @@ async function processTgMessage(chatId, payload, requeues = 0) {
2549
2606
  tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2550
2607
  return;
2551
2608
  }
2552
- let sid = tgChats.get(chatId);
2609
+ let sid = tgChats.get(chatId) || tgSingleSid;
2553
2610
  if (sid && engine.isBusy(sid)) {
2554
2611
  /* oturum meşgul — WA ile aynı: kaybetme, iş bitene dek yeniden dene */
2555
2612
  await new Promise((r) => setTimeout(r, TG_DEBOUNCE_MS));
2556
2613
  return processTgMessage(chatId, payload, requeues + 1);
2557
2614
  }
2558
- if (!sid) {
2559
- const v = engine.createSession();
2560
- sid = v.id;
2561
- tgChats.set(chatId, sid);
2562
- tgRememberSession(chatId, sid);
2563
- saveTgChats();
2564
- } else {
2565
- tgRememberSession(chatId, sid);
2566
- }
2615
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2616
+ sid = ensureTgSession(chatId);
2567
2617
  /* Kişi bazlı granül izin: all/web/read/chat */
2568
2618
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2569
2619
  engine.setSessionPerm(sid, perm);
@@ -2686,6 +2736,8 @@ function dcFind(senderId, username) {
2686
2736
  if (Array.isArray(arr)) dcHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-DC_HISTORY_CAP));
2687
2737
  }
2688
2738
  }
2739
+ /* kanal tek oturumu: kayıtlı aktif id (geçersizse ensure yeniden seçer) */
2740
+ if (raw && typeof raw.active === 'string' && raw.active) dcSingleSid = raw.active;
2689
2741
  for (const [c, s] of dcChats.entries()) {
2690
2742
  const h = dcHistory.get(c) || [];
2691
2743
  if (!h.includes(s)) h.push(s);
@@ -2701,6 +2753,7 @@ function saveDcChats() {
2701
2753
  JSON.stringify({
2702
2754
  chats: Object.fromEntries(dcChats),
2703
2755
  history: Object.fromEntries([...dcHistory.entries()].map(([c, a]) => [c, a.slice(-DC_HISTORY_CAP)])),
2756
+ active: dcSingleSid || '',
2704
2757
  })
2705
2758
  );
2706
2759
  } catch {}
@@ -2767,21 +2820,14 @@ async function processDcMessage(channelId, payload) {
2767
2820
  dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2768
2821
  return;
2769
2822
  }
2770
- let sid = dcChats.get(channelId);
2823
+ let sid = dcChats.get(channelId) || dcSingleSid;
2771
2824
  if (sid && engine.isBusy(sid)) {
2772
2825
  /* oturum meşgul — WA/TG ile aynı: kaybetme, iş bitene dek yeniden dene */
2773
2826
  await new Promise((r) => setTimeout(r, DC_DEBOUNCE_MS));
2774
2827
  return processDcMessage(channelId, payload, 1);
2775
2828
  }
2776
- if (!sid) {
2777
- const v = engine.createSession();
2778
- sid = v.id;
2779
- dcChats.set(channelId, sid);
2780
- dcRememberSession(channelId, sid);
2781
- saveDcChats();
2782
- } else {
2783
- dcRememberSession(channelId, sid);
2784
- }
2829
+ /* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
2830
+ sid = ensureDcSession(channelId);
2785
2831
  /* Kişi bazlı granül izin: all/web/read/chat */
2786
2832
  let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2787
2833
  engine.setSessionPerm(sid, perm);
@@ -3430,6 +3476,11 @@ function browserW() {
3430
3476
  try { return win.getContentSize()[0]; } catch { return 0; }
3431
3477
  }
3432
3478
 
3479
+ /* Tarayıcı gizleme ÖZELLİĞİ: Ayarlar → Web Arama'dan açılır, VARSAYILAN KAPALI.
3480
+ Kapalıyken tarayıcı her zaman görünür; açıkken göz ikonu gizle/göster yapar. */
3481
+ function browserHideEnabled() { return settings.browserHide === true; }
3482
+ function browserHeadlessPref() { return browserHideEnabled() && settings.browserHeadless === true; }
3483
+
3433
3484
  function browserShownWidth(w) {
3434
3485
  const avail = Math.max(320, w - 320);
3435
3486
  /* MOBİL ÖNİZLEME: siluet + çerçeve payı — siluet tam otursun (phone-mode'dan önce) */
@@ -3708,12 +3759,13 @@ function setBrowserOpen(v, forceVisible) {
3708
3759
  }
3709
3760
 
3710
3761
  // AÇMA — görünürlük: forceVisible true/false ise onu uygula;
3711
- // belirtilmemişse kullanıcı tercihi (settings.browserHeadless) belirler.
3762
+ // belirtilmemişse kullanıcı tercihi belirler. Tarayıcı gizleme ÖZELLİĞİ
3763
+ // (settings.browserHide) kapalıysa tercih ne olursa olsun tarayıcı GÖRÜNÜR açılır.
3712
3764
  // PARALEL AJANLAR (bg oturum) her zaman forceVisible=false ile çağırır →
3713
3765
  // tarayıcı gizli modda çalışır, kullanıcı ekranı ve ajan konsolu rahatsız edilmez.
3714
3766
  browser.open = true;
3715
3767
  browser.visible =
3716
- forceVisible === true ? true : forceVisible === false ? false : settings.browserHeadless !== true;
3768
+ forceVisible === true ? true : forceVisible === false ? false : !browserHeadlessPref();
3717
3769
  ensureBrowser();
3718
3770
  if (!browser.started) {
3719
3771
  browser.started = true;
@@ -4614,7 +4666,7 @@ function resyncBrowserUi() {
4614
4666
  layoutBrowser(); resyncBrowserUi();
4615
4667
  setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 80);
4616
4668
  setTimeout(() => { try { layoutBrowser(); resyncBrowserUi(); } catch {} }, 300);
4617
- });
4669
+ });
4618
4670
  win.on('show', layoutBrowser);
4619
4671
  // X'e basınca gizle — tepside yaşamaya devam, WhatsApp bağlantısı sürer
4620
4672
  win.on('close', (e) => {
@@ -6330,13 +6382,19 @@ function empatiLlmCompose(prompt) {
6330
6382
  const PROACTIVE_MARK = '🫡 *Beast proaktif:*';
6331
6383
 
6332
6384
  /* bildirimin ALTINA YAZILAN KAYNAK: haberin gerçek linki varsa o, yoksa kaynak adı.
6333
- Kullanıcı "bu proje neymiş?" diye sorduğunda linkten bulabilsin. */
6385
+ Kullanıcı "bu proje neymiş?" diye sorduğunda linkten bulabilsin.
6386
+ TEK SATIR kuralı: satır sonu/boşluklar temizlenir, uzunsa sona "..." gelir. */
6334
6387
  function empatiSourceLine(ev) {
6335
6388
  if (!ev) return '';
6336
- const url = String(ev.url || '').trim();
6337
- if (/^https?:\/\//i.test(url)) return '🔗 ' + url;
6338
- const src = String(ev.source || '').trim();
6339
- return src ? '🔗 kaynak: ' + src.slice(0, 80) : '';
6389
+ const oneLine = (s) =>
6390
+ String(s || '').replace(/\s*\n+\s*/g, ' ').replace(/\s{2,}/g, ' ').trim();
6391
+ const ELLIPSIS = '...';
6392
+ const cap = (s, max) =>
6393
+ oneLine(s).length > max ? oneLine(s).slice(0, max - ELLIPSIS.length) + ELLIPSIS : oneLine(s);
6394
+ const url = oneLine(ev.url);
6395
+ if (/^https?:\/\//i.test(url)) return '🔗 ' + cap(url, 100);
6396
+ const src = oneLine(ev.source);
6397
+ return src ? '🔗 kaynak: ' + cap(src, 80) : '';
6340
6398
  }
6341
6399
 
6342
6400
  function empatiInjectToSession(sid, out) {
@@ -6360,28 +6418,74 @@ function empatiInjectText(ev, out) {
6360
6418
  );
6361
6419
  }
6362
6420
 
6363
- /* Oturum yoksa oluştur (processTgMessage/processDcMessage ile aynı kalıp) */
6421
+ /* kanalın yaşayan tek oturumu (WA kuralının aynısı — Telegram/Discord) */
6364
6422
  function ensureTgSession(chatId) {
6365
- let sid = tgChats.get(chatId);
6366
- if (!sid) {
6367
- const v = engine.createSession();
6368
- sid = v.id;
6369
- tgChats.set(chatId, sid);
6370
- tgRememberSession(chatId, sid);
6423
+ if (!tgSingleSid || !sessionFileAlive(tgSingleSid)) {
6424
+ let best = '';
6425
+ let bestTs = '';
6426
+ for (const c of new Set([...tgChats.values(), tgSingleSid])) {
6427
+ if (!c || !sessionFileAlive(c)) continue;
6428
+ let ts = '';
6429
+ try {
6430
+ const s = engine.cache.get(c) || engine._load(c);
6431
+ ts = String((s && s.updatedAt) || '');
6432
+ } catch {}
6433
+ if (!best || ts > bestTs) {
6434
+ best = c;
6435
+ bestTs = ts;
6436
+ }
6437
+ }
6438
+ tgSingleSid = best || engine.createSession().id;
6371
6439
  saveTgChats();
6372
6440
  }
6373
- return sid;
6441
+ let dirty = false;
6442
+ for (const [c, s] of tgChats.entries()) {
6443
+ if (s !== tgSingleSid) {
6444
+ tgChats.set(c, tgSingleSid);
6445
+ dirty = true;
6446
+ }
6447
+ }
6448
+ if (chatId != null && tgChats.get(String(chatId)) !== tgSingleSid) {
6449
+ tgChats.set(String(chatId), tgSingleSid);
6450
+ dirty = true;
6451
+ }
6452
+ tgRememberSession(String(chatId), tgSingleSid);
6453
+ if (dirty) saveTgChats();
6454
+ return tgSingleSid;
6374
6455
  }
6375
6456
  function ensureDcSession(channelId) {
6376
- let sid = dcChats.get(channelId);
6377
- if (!sid) {
6378
- const v = engine.createSession();
6379
- sid = v.id;
6380
- dcChats.set(channelId, sid);
6381
- dcRememberSession(channelId, sid);
6457
+ if (!dcSingleSid || !sessionFileAlive(dcSingleSid)) {
6458
+ let best = '';
6459
+ let bestTs = '';
6460
+ for (const c of new Set([...dcChats.values(), dcSingleSid])) {
6461
+ if (!c || !sessionFileAlive(c)) continue;
6462
+ let ts = '';
6463
+ try {
6464
+ const s = engine.cache.get(c) || engine._load(c);
6465
+ ts = String((s && s.updatedAt) || '');
6466
+ } catch {}
6467
+ if (!best || ts > bestTs) {
6468
+ best = c;
6469
+ bestTs = ts;
6470
+ }
6471
+ }
6472
+ dcSingleSid = best || engine.createSession().id;
6382
6473
  saveDcChats();
6383
6474
  }
6384
- return sid;
6475
+ let dirty = false;
6476
+ for (const [c, s] of dcChats.entries()) {
6477
+ if (s !== dcSingleSid) {
6478
+ dcChats.set(c, dcSingleSid);
6479
+ dirty = true;
6480
+ }
6481
+ }
6482
+ if (channelId != null && dcChats.get(String(channelId)) !== dcSingleSid) {
6483
+ dcChats.set(String(channelId), dcSingleSid);
6484
+ dirty = true;
6485
+ }
6486
+ dcRememberSession(String(channelId), dcSingleSid);
6487
+ if (dirty) saveDcChats();
6488
+ return dcSingleSid;
6385
6489
  }
6386
6490
 
6387
6491
  /* Masaüstü yedeği: en güncel (bg/bot-DM'siz, meşgul olmayan) sohbet — yoksa yeni */
@@ -6601,9 +6705,11 @@ ipcMain.handle('browser:toggle', () => {
6601
6705
  setBrowserOpen(!browser.open, true);
6602
6706
  return { open: browser.open, visible: browser.visible };
6603
6707
  });
6604
- /* göz ikonu: ajan tarayıcısını görünür/gizli yap */
6605
- ipcMain.handle('browser:shown:get', () => ({ shown: settings.browserHeadless === false }));
6708
+ /* göz ikonu: ajan tarayıcısını görünür/gizli yap — yalnızca gizleme özelliği
6709
+ (Ayarlar → Web Arama) açıkken etkilidir; özellik kapalıysa hep görünür */
6710
+ ipcMain.handle('browser:shown:get', () => ({ shown: !browserHeadlessPref(), enabled: browserHideEnabled() }));
6606
6711
  ipcMain.handle('browser:shown:set', (_e, v) => {
6712
+ if (!browserHideEnabled()) return { shown: true, enabled: false };
6607
6713
  settings.browserHeadless = !v;
6608
6714
  saveSettings();
6609
6715
  if (browser.open) {
@@ -6611,7 +6717,22 @@ ipcMain.handle('browser:shown:set', (_e, v) => {
6611
6717
  layoutBrowser();
6612
6718
  browserEmit({ open: true, width: browserShownWidth(browserW()) });
6613
6719
  }
6614
- return { shown: !!v };
6720
+ return { shown: !!v, enabled: true };
6721
+ });
6722
+ /* gizleme özelliği anahtarı: kapalıyken göz ikonu yok + tarayıcı zorla görünür */
6723
+ ipcMain.handle('browser:hide:set', (_e, v) => {
6724
+ settings.browserHide = !!v;
6725
+ if (settings.browserHide) {
6726
+ /* özellik yeni açıldı → görünür başla, kullanıcı gözle istediğinde gizler */
6727
+ settings.browserHeadless = false;
6728
+ }
6729
+ saveSettings();
6730
+ if (!browserHideEnabled() && browser.open) {
6731
+ browser.visible = true;
6732
+ layoutBrowser();
6733
+ browserEmit({ open: true, width: browserShownWidth(browserW()) });
6734
+ }
6735
+ return { enabled: browserHideEnabled(), shown: !browserHeadlessPref() };
6615
6736
  });
6616
6737
  ipcMain.handle('browser:navigate', (_e, url) => browserNavigate(url));
6617
6738
  ipcMain.handle('browser:ctrl', (_e, action) => {
package/src/preload.js CHANGED
@@ -73,6 +73,7 @@ contextBridge.exposeInMainWorld('beast', {
73
73
  toggleBrowser: () => ipcRenderer.invoke('browser:toggle'),
74
74
  browserShownGet: () => ipcRenderer.invoke('browser:shown:get'),
75
75
  browserShownSet: (v) => ipcRenderer.invoke('browser:shown:set', v),
76
+ browserHideSet: (v) => ipcRenderer.invoke('browser:hide:set', v),
76
77
  terminalToggle: () => ipcRenderer.invoke('terminal:toggle'),
77
78
  terminalRun: (cmd, shell) => ipcRenderer.invoke('terminal:run', { cmd, shell }),
78
79
  terminalStop: () => ipcRenderer.invoke('terminal:stop'),
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ /* Beast App webview köprüsü: app UI'ları (file://) contextBridge üzerinden
4
+ window.beastApp API'sini görür. App kimliği webview src'sindeki ?appid=
5
+ parametresinden okunur; main tarafı id'yi yeniden doğrular. */
6
+
7
+ const { contextBridge, ipcRenderer } = require('electron');
8
+
9
+ let appId = '';
10
+ try {
11
+ appId = String(new URLSearchParams(window.location.search).get('appid') || '');
12
+ } catch {}
13
+
14
+ contextBridge.exposeInMainWorld('beastApp', {
15
+ id: appId,
16
+ storageGet: (key, dflt) => ipcRenderer.invoke('appui:storage:get', { appId, key, dflt }),
17
+ storageSet: (key, value) => ipcRenderer.invoke('appui:storage:set', { appId, key, value }),
18
+ notify: (text) => ipcRenderer.invoke('appui:notify', { appId, text }),
19
+ info: () => ipcRenderer.invoke('appui:info', appId),
20
+ });
@@ -311,7 +311,6 @@
311
311
  ev_price_ph: 'Fiyat sembolü (örn PAXGUSDT)',
312
312
  ev_save: 'Kaydet',
313
313
  ev_price_sub: 'boş sembol = fiyat feed kapalı (Binance miniTicker)',
314
- ev_token_copy: 'token kopyala',
315
314
  ev_subs_h3: 'Aktif Abonelikler',
316
315
  ev_no_sub: 'Abonelik yok — agent\u2019a "mail gelince haber ver" ya da "fiyat X altına inerse bağır" de, kendisi kurar.',
317
316
  ev_on_toast: 'Olay merkezi açık',
@@ -354,6 +353,11 @@
354
353
  so_engine_searxng: 'SearXNG (yerel — beast searxng)',
355
354
  so_engine_python: 'Python Çoklu-Motor (ddgs/DDG/Bing/Mojeek)',
356
355
  so_saved_toast: 'Arama sırası kaydedildi',
356
+ bh_h2: 'Tarayıcı Gizleme',
357
+ bh_sub: 'Ajan aramalarını görünür panel yerine gizli pencerede çalıştırır. Açıkken üst çubukta göz ikonu çıkar — göz açıkken aramalar panelde izlenir, kapalıyken gizli çalışır.',
358
+ bh_label: 'Tarayıcı gizleme özelliği',
359
+ bh_on_toast: 'Tarayıcı gizleme açık — göz ikonu üst çubukta',
360
+ bh_off_toast: 'Tarayıcı gizleme kapandı — tarayıcı her zaman görünür',
357
361
  ws_save: 'Kaydet',
358
362
  ws_clear: 'Anahtarı Sil',
359
363
  ws_note: 'Anahtar maskeli tutulur; yenisini yazıp kaydettiğinde eskisinin yerine geçer. Boş kaydet: mevcut anahtar korunur.',
@@ -936,7 +940,6 @@
936
940
  ev_price_ph: 'Price symbol (e.g. PAXGUSDT)',
937
941
  ev_save: 'Save',
938
942
  ev_price_sub: 'empty symbol = price feed off (Binance miniTicker)',
939
- ev_token_copy: 'copy token',
940
943
  ev_subs_h3: 'Active Subscriptions',
941
944
  ev_no_sub: 'No subscriptions — tell the agent "notify me on mail" or "shout if price drops below X" and it sets one up.',
942
945
  ev_on_toast: 'Event center on',
@@ -979,6 +982,11 @@
979
982
  so_engine_searxng: 'SearXNG (local — beast searxng)',
980
983
  so_engine_python: 'Python Multi-Engine (ddgs/DDG/Bing/Mojeek)',
981
984
  so_saved_toast: 'Search order saved',
985
+ bh_h2: 'Browser Hiding',
986
+ bh_sub: 'Run agent searches in a hidden window instead of the visible panel. When on, an eye icon appears in the top bar — eye open searches are watched in the panel, eye closed runs hidden.',
987
+ bh_label: 'Browser hiding feature',
988
+ bh_on_toast: 'Browser hiding on — eye icon in the top bar',
989
+ bh_off_toast: 'Browser hiding off — browser always visible',
982
990
  ws_save: 'Save',
983
991
  ws_clear: 'Delete Key',
984
992
  ws_note: 'The key is kept masked; saving a new one replaces the old. Saving empty keeps the current key.',
@@ -59,7 +59,7 @@
59
59
  <header id="topbar">
60
60
  <span id="botChip" title="Aktif bot">🦁 Beast</span>
61
61
  <div id="modelDD" class="dd">
62
- <button id="modelBtn" class="dd-btn" title="Model seç" data-i18n-title="tipModel"><span id="modelBtnLabel">Model</span><span class="dd-caret">Ⓘ</span></button>
62
+ <button id="modelBtn" class="dd-btn" title="Model seç" data-i18n-title="tipModel"><span id="modelBtnLabel">Model</span></button>
63
63
  <div id="modelMenu" class="dd-menu" hidden>
64
64
  <input id="modelFilter" class="dd-filter" placeholder="Model ara…" autocomplete="off" />
65
65
  <div id="modelList" class="dd-list"></div>
@@ -87,7 +87,7 @@
87
87
  <button id="cronBtn" class="dd-btn dd-icon-btn" title="Cron Görevler" data-i18n-title="tipCron">&#x23F1;&#xFE0E;</button>
88
88
  <button id="termCBtn" class="dd-btn dd-icon-btn" title="CMD terminali" data-i18n-title="tipTermC">&#x276F;&#xFE0E;</button>
89
89
  <button id="browserBtn" class="dd-btn dd-icon-btn" title="Dahili tarayıcı" data-i18n-title="tipBrowser">&#x29C9;</button>
90
- <button id="eyeBtn" class="dd-btn dd-icon-btn" title="Ajan tarayıcısı görünür/gizli — göz açıkken aramalar panelde izlenir, kapalıyken gizli çalışır" data-i18n-title="tipEye"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/><line class="eye-slash" x1="4" y1="4" x2="20" y2="20"/></svg></button>
90
+ <button id="eyeBtn" class="dd-btn dd-icon-btn" hidden title="Ajan tarayıcısı görünür/gizli — göz açıkken aramalar panelde izlenir, kapalıyken gizli çalışır" data-i18n-title="tipEye"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/><line class="eye-slash" x1="4" y1="4" x2="20" y2="20"/></svg></button>
91
91
  <div class="drag-spacer"></div>
92
92
  <div class="win-controls" id="winControls">
93
93
  <button id="winMin" title="Simge durumunda küçült" data-i18n-title="tip_win_min">&#x2500;&#xFE0E;</button>