beast-agent 0.26.0 → 0.26.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/src/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, session } = require('electron');
3
+ const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, session, net: electronNet } = require('electron');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const http = require('http');
@@ -16,6 +16,7 @@ const skillsMod = require('./agent/skills');
16
16
  const storeMod = require('./agent/store');
17
17
  const { WhatsAppBridge } = require('./agent/whatsapp');
18
18
  const { TelegramBridge } = require('./agent/telegram');
19
+ const { DiscordBridge } = require('./agent/discord');
19
20
  const cron = require('./cron');
20
21
  const watchers = require('./agent/watchers');
21
22
  const usageMod = require('./agent/usage');
@@ -229,6 +230,11 @@ let tg = null;
229
230
  let tgChats = new Map(); // telegram chatId -> aktif session id
230
231
  let tgHistory = new Map(); // chatId -> [sid,...]
231
232
  const TG_HISTORY_CAP = 20;
233
+ let dc = null;
234
+ let dcChats = new Map(); // discord channelId -> aktif session id
235
+ let dcHistory = new Map(); // channelId -> [sid,...]
236
+ const DC_HISTORY_CAP = 20;
237
+ const DC_CHATS_FILE = path.join(APP_DIR, 'dc-chats.json');
232
238
  let tray = null;
233
239
  app.isQuitting = false;
234
240
 
@@ -1183,7 +1189,7 @@ async function waFlush(jid) {
1183
1189
  if (payload.participant && !merged.participant) merged.participant = payload.participant;
1184
1190
  if (payload.participantPn && !merged.participantPn) merged.participantPn = payload.participantPn;
1185
1191
  if (payload.participantUsername && !merged.participantUsername) merged.participantUsername = payload.participantUsername;
1186
- if (payload.mentioned) merged.mentioned = true;
1192
+ if (payload.mentioned) merged.mentioned = true;
1187
1193
  if (!senderNum && sn) senderNum = sn;
1188
1194
  }
1189
1195
  try {
@@ -1442,7 +1448,7 @@ function botToolSet(cfg) {
1442
1448
  'event_list', 'event_subscribe', 'event_unsubscribe',
1443
1449
  'watcher_add', 'watcher_list', 'watcher_remove',
1444
1450
  ]);
1445
- if (s.web_search) { set.add('web_search'); set.add('http_fetch'); }
1451
+ if (s.web_search) { set.add('web_search'); set.add('http_fetch'); set.add('deep_search'); }
1446
1452
  if (s.browser) {
1447
1453
  for (const t of ['browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot', 'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select', 'ocr_read']) set.add(t);
1448
1454
  }
@@ -1711,6 +1717,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
1711
1717
  } else {
1712
1718
  engine.setSessionTools(sid, null);
1713
1719
  }
1720
+ engine.setSessionModel(sid, botCfg && !botCfg.admin ? (botCfg.model || null) : null);
1714
1721
  waLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
1715
1722
 
1716
1723
  const participantName = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
@@ -1979,6 +1986,7 @@ async function processTgMessage(chatId, payload, requeues = 0) {
1979
1986
  } else {
1980
1987
  engine.setSessionTools(sid, null);
1981
1988
  }
1989
+ engine.setSessionModel(sid, botCfg && !botCfg.admin ? (botCfg.model || null) : null);
1982
1990
  tgLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
1983
1991
 
1984
1992
  /* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
@@ -2037,6 +2045,225 @@ async function restartTg() {
2037
2045
  }
2038
2046
  }
2039
2047
 
2048
+ /* ---------- DISCORD: allow list — WA/TG ile aynı mantık ----------
2049
+ Liste formatı: [{ id:'123456789' | '@kullanici_adi', name, perm, bot_id }, '*']
2050
+ Eşleşme: sayısal ID birebir, @username büyük/küçük harf duyarsız. */
2051
+ function dcLog(line) {
2052
+ try { log.info('discord', line); } catch {}
2053
+ }
2054
+
2055
+ function dcFind(senderId, username) {
2056
+ const list = settings.dcAllow || [];
2057
+ if (!list.length) return null; // boş liste = kimseye cevap yok
2058
+ const id = String(senderId || '').trim();
2059
+ const uname = String(username || '').replace(/^@/, '').toLowerCase();
2060
+ for (const e of list) {
2061
+ if (e === '*') return { id: '*', name: '' };
2062
+ const eid = typeof e === 'string' ? e.trim() : String((e && e.id) || '').trim();
2063
+ if (!eid) continue;
2064
+ if (eid === '*') return { id: '*', name: '' };
2065
+ if (eid.startsWith('@')) {
2066
+ if (uname && eid.slice(1).toLowerCase() === uname) {
2067
+ return typeof e === 'string' ? { id: eid, name: '' } : e;
2068
+ }
2069
+ } else if (id && eid === id) {
2070
+ return typeof e === 'string' ? { id: eid, name: '' } : e;
2071
+ }
2072
+ }
2073
+ return null;
2074
+ }
2075
+
2076
+ (function dcChatsLoad() {
2077
+ try {
2078
+ const raw = JSON.parse(fs.readFileSync(DC_CHATS_FILE, 'utf8'));
2079
+ if (raw && typeof raw.chats === 'object') {
2080
+ for (const [c, s] of Object.entries(raw.chats)) {
2081
+ if (typeof s === 'string') dcChats.set(c, s);
2082
+ }
2083
+ }
2084
+ if (raw && typeof raw.history === 'object') {
2085
+ for (const [c, arr] of Object.entries(raw.history)) {
2086
+ if (Array.isArray(arr)) dcHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-DC_HISTORY_CAP));
2087
+ }
2088
+ }
2089
+ for (const [c, s] of dcChats.entries()) {
2090
+ const h = dcHistory.get(c) || [];
2091
+ if (!h.includes(s)) h.push(s);
2092
+ dcHistory.set(c, h.slice(-DC_HISTORY_CAP));
2093
+ }
2094
+ } catch {}
2095
+ })();
2096
+
2097
+ function saveDcChats() {
2098
+ try {
2099
+ fs.writeFileSync(
2100
+ DC_CHATS_FILE,
2101
+ JSON.stringify({
2102
+ chats: Object.fromEntries(dcChats),
2103
+ history: Object.fromEntries([...dcHistory.entries()].map(([c, a]) => [c, a.slice(-DC_HISTORY_CAP)])),
2104
+ })
2105
+ );
2106
+ } catch {}
2107
+ }
2108
+
2109
+ function dcRememberSession(channelId, sid) {
2110
+ const h = dcHistory.get(channelId) || [];
2111
+ if (!h.includes(sid)) h.push(sid);
2112
+ dcHistory.set(channelId, h.slice(-DC_HISTORY_CAP));
2113
+ }
2114
+
2115
+ const DC_DEBOUNCE_MS = 4500;
2116
+ const dcQueue = new Map(); // channelId -> { payloads[] }
2117
+
2118
+ function dcQueuePush(channelId, payload) {
2119
+ let q = dcQueue.get(channelId);
2120
+ if (!q) {
2121
+ q = { payloads: [] };
2122
+ dcQueue.set(channelId, q);
2123
+ }
2124
+ q.payloads.push(payload);
2125
+ clearTimeout(q.timer);
2126
+ dcLog(`queue: mesaj kuyruğa girdi channel=${channelId} toplam=${q.payloads.length} (4.5 sn birleştirme)`);
2127
+ q.timer = setTimeout(() => {
2128
+ dcFlush(channelId).catch((e) => dcLog(`flush KRASİ: ${String((e && e.stack) || e)}`));
2129
+ }, DC_DEBOUNCE_MS);
2130
+ }
2131
+
2132
+ async function dcFlush(channelId) {
2133
+ const q = dcQueue.get(channelId);
2134
+ if (!q) return;
2135
+ dcQueue.delete(channelId);
2136
+ const merged = { text: '', senderId: '', username: '', senderName: '' };
2137
+ for (const p of q.payloads) {
2138
+ if (p.text) merged.text += (merged.text ? '\n' : '') + p.text;
2139
+ if (!merged.senderId && p.senderId) { merged.senderId = p.senderId; merged.username = p.username; merged.senderName = p.senderName; }
2140
+ }
2141
+ await processDcMessage(channelId, merged);
2142
+ }
2143
+
2144
+ async function handleDcIncoming(channelId, payload) {
2145
+ try {
2146
+ const hit = dcFind(payload.senderId, payload.username);
2147
+ dcLog(
2148
+ `incoming channel=${channelId} sender=${payload.senderId || '?'} user=${payload.username || '-'} allowed=${!!hit}` +
2149
+ (hit && hit.name ? ' name=' + hit.name : '')
2150
+ );
2151
+ if (!hit) return; // allowlist dışı yoksay
2152
+ /* İsimsiz kayıt: güvenlik için cevap verme — kullanıcıyı ayarlara yönlendir */
2153
+ if (hit.id !== '*' && !hit.name) {
2154
+ dcLog(`skip: isimsiz kayıt (${hit.id}) — cevap verilmedi, Entegrasyonlar'da isim ekle`);
2155
+ return;
2156
+ }
2157
+ resumeServices(); // pause durumunda gelen mesaj servisleri canlandırır
2158
+ dcQueuePush(String(channelId), payload);
2159
+ } catch (e) {
2160
+ dcLog(`handleDcIncoming KRASİ: ${String((e && e.stack) || e)}`);
2161
+ }
2162
+ }
2163
+
2164
+ async function processDcMessage(channelId, payload) {
2165
+ const hit = dcFind(payload.senderId, payload.username);
2166
+ if (!hit) {
2167
+ dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
2168
+ return;
2169
+ }
2170
+ let sid = dcChats.get(channelId);
2171
+ if (sid && engine.isBusy(sid)) {
2172
+ /* oturum meşgul — WA/TG ile aynı: kaybetme, iş bitene dek yeniden dene */
2173
+ await new Promise((r) => setTimeout(r, DC_DEBOUNCE_MS));
2174
+ return processDcMessage(channelId, payload, 1);
2175
+ }
2176
+ if (!sid) {
2177
+ const v = engine.createSession();
2178
+ sid = v.id;
2179
+ dcChats.set(channelId, sid);
2180
+ dcRememberSession(channelId, sid);
2181
+ saveDcChats();
2182
+ } else {
2183
+ dcRememberSession(channelId, sid);
2184
+ }
2185
+ /* Kişi bazlı granül izin: all/web/read/chat */
2186
+ let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
2187
+ engine.setSessionPerm(sid, perm);
2188
+
2189
+ /* BOT SİSTEMİ: izinli kayıtta bot_id yoksa beast'e düşer (WA/TG ile aynı) */
2190
+ let botId = hit && hit.bot_id ? String(hit.bot_id) : 'beast';
2191
+ if (!bots.get(botId)) {
2192
+ if (botId !== 'beast') dcLog(`bot="${botId}" yok — kayıt botsuz, beast (admin) botuna yönlendirildi`);
2193
+ botId = 'beast';
2194
+ }
2195
+ engine.setSessionBot(sid, botId);
2196
+ const botCfg = bots.get(botId);
2197
+ if (botCfg && !botCfg.admin) {
2198
+ const eff = moreRestrictivePerm(perm, botCfg.perm || 'all');
2199
+ if (eff !== perm) {
2200
+ perm = eff;
2201
+ engine.setSessionPerm(sid, eff);
2202
+ }
2203
+ engine.setSessionTools(sid, botToolSet(botCfg));
2204
+ } else {
2205
+ engine.setSessionTools(sid, null);
2206
+ }
2207
+ engine.setSessionModel(sid, botCfg && !botCfg.admin ? (botCfg.model || null) : null);
2208
+ dcLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
2209
+
2210
+ /* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
2211
+ const isOwner = !!hit.owner;
2212
+ const roleTag = isOwner
2213
+ ? 'SAHİBİN (talepleri önceliklidir)'
2214
+ : 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
2215
+ const label =
2216
+ (hit.name || payload.senderName || '?') +
2217
+ (payload.username ? ` (@${payload.username})` : '') +
2218
+ ` — ${roleTag}`;
2219
+ let text = `[Discord — gönderen: ${label}]`;
2220
+ if (!isOwner) {
2221
+ 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.]`;
2222
+ }
2223
+ text += `\n${String(payload.text || '').slice(0, 6000)}`;
2224
+ engine.send(sid, { text: text.slice(0, 8000), attachments: [] });
2225
+ }
2226
+
2227
+ async function sendDcSafe(channelId, text) {
2228
+ if (!dc) return false;
2229
+ try {
2230
+ return !!(await dc.send(channelId, text));
2231
+ } catch (e) {
2232
+ dcLog(`send hata channel=${channelId}: ${String((e && e.message) || e)}`);
2233
+ return false;
2234
+ }
2235
+ }
2236
+
2237
+ function ensureDc() {
2238
+ if (!dc) {
2239
+ dc = new DiscordBridge({
2240
+ token: settings.dcToken || '',
2241
+ emit: (ev) => {
2242
+ if (ev.type === 'status') dcLog(`status=${ev.status}${ev.user ? ' user=' + ev.user : ''}`);
2243
+ if (ev.type === 'warn') dcLog('⚠ ' + String(ev.text || ''));
2244
+ if (win && !win.isDestroyed()) win.webContents.send('dc:event', ev);
2245
+ },
2246
+ onIncoming: handleDcIncoming,
2247
+ });
2248
+ }
2249
+ return dc;
2250
+ }
2251
+
2252
+ /* token değişimi / yeniden başlatma: eski köprüyü kapat, yenisini aç */
2253
+ async function restartDc() {
2254
+ if (dc) {
2255
+ try { await dc.stop(); } catch {}
2256
+ dc = null;
2257
+ }
2258
+ if (!settings.dcToken) return;
2259
+ const b = ensureDc();
2260
+ try {
2261
+ await b.start();
2262
+ } catch (e) {
2263
+ dcLog(`start başarısız: ${String((e && e.message) || e)}`);
2264
+ }
2265
+ }
2266
+
2040
2267
  function reloadBackend() {
2041
2268
  if (engine && typeof engine.dispose === 'function') {
2042
2269
  try { engine.dispose(); } catch {}
@@ -2091,13 +2318,18 @@ function reloadBackend() {
2091
2318
  ocr: (o) => ocrRead(o),
2092
2319
  email: { list: emailList, read: emailRead, send: emailSend },
2093
2320
  browser: {
2094
- openUrl: (u, s) => browserNavigate(u, s),
2095
- search: (q, s) => browserSearch(q, s),
2321
+ openUrl: (u, s, ctx) => browserNavigate(u, s, ctx),
2322
+ search: (q, s, ctx) => browserSearch(q, s, ctx),
2096
2323
  readText: (s) => browserRead(s),
2097
2324
  screenshot: (s) => browserScreenshot(s),
2098
2325
  snapshot: (s) => browserSnapshot(s),
2099
2326
  act: (k, a, s) => browserAct(k, a, s),
2100
2327
  },
2328
+ research: {
2329
+ /* deep_search'ün "sayfayı GİZLİ tarayıcıda açıp oku" parçası (Electron-only).
2330
+ Arama zinciri engine._webSearchChain içinde web_search ile birebir aynı. */
2331
+ readPage: (u, s) => researchRead(u, s),
2332
+ },
2101
2333
  emit: (ev) => {
2102
2334
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
2103
2335
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
@@ -2201,6 +2433,27 @@ function reloadBackend() {
2201
2433
  })();
2202
2434
  }
2203
2435
  }
2436
+ // Discord oturumlarının son cevabını geri gönder (TG ile aynı akış)
2437
+ if ((ev.type === 'done' || ev.type === 'error') && dc && dc.connected) {
2438
+ const hitD = [...dcChats.entries()].find(([, s]) => s === ev.sessionId);
2439
+ if (hitD) {
2440
+ const dchid = hitD[0];
2441
+ (async () => {
2442
+ try {
2443
+ if (ev.type === 'error') {
2444
+ await sendDcSafe(dchid, 'Bir aksilik oldu: ' + String(ev.error || '').slice(0, 200));
2445
+ return;
2446
+ }
2447
+ if (!ev.aborted) {
2448
+ const s = engine.openSession(ev.sessionId);
2449
+ const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
2450
+ const txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
2451
+ if (txt.trim()) await sendDcSafe(dchid, txt);
2452
+ }
2453
+ } catch {}
2454
+ })();
2455
+ }
2456
+ }
2204
2457
  },
2205
2458
  });
2206
2459
  return engine.publicState();
@@ -2327,10 +2580,6 @@ function ensureDesktopShortcut() {
2327
2580
  }
2328
2581
 
2329
2582
  app.whenReady().then(() => {
2330
- try { applyProxy(); } catch {} // proxy ayarı yüklüyse tüm çıkışa uygula
2331
- if (settings.warp && settings.warp.enabled) {
2332
- startWarp().catch((e) => log.info('main', 'warp açılışta başlatılamadı: ' + String((e && e.message) || e)));
2333
- }
2334
2583
  // Tailscale modu: paketli uygulamada Windows ile otomatik başlat (sessiz, tepside)
2335
2584
  if (app.isPackaged) {
2336
2585
  /* DAĞITIM KARARI: EXE/portable kurulum desteklenmiyor — tek yol npm.
@@ -2395,6 +2644,11 @@ app.whenReady().then(() => {
2395
2644
  ensureTg().start().catch((e) => tgLog('autostart failed: ' + String((e && e.message) || e)));
2396
2645
  }
2397
2646
 
2647
+ // Discord köprüsünü otomatik başlat (token kayıtlıysa)
2648
+ if (settings.dcToken) {
2649
+ ensureDc().start().catch((e) => dcLog('autostart failed: ' + String((e && e.message) || e)));
2650
+ }
2651
+
2398
2652
  app.on('activate', () => {
2399
2653
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
2400
2654
  });
@@ -2402,7 +2656,6 @@ app.whenReady().then(() => {
2402
2656
  app.on('before-quit', () => {
2403
2657
  app.isQuitting = true;
2404
2658
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
2405
- try { stopWarp(); } catch {} // warp-plus yardımcısını da kapat
2406
2659
  });
2407
2660
 
2408
2661
  if (process.argv.includes('--smoke')) {
@@ -2558,10 +2811,13 @@ function setBrowserOpen(v, forceVisible) {
2558
2811
  return;
2559
2812
  }
2560
2813
 
2561
- // AÇMA — görünürlük: kullanıcı kendi açtıysa (forceVisible) MUTLAKA görünür;
2562
- // ajan açtıysa DA VARSAYILAN GÖRÜNÜR (göz ikonuyla gizli mod seçilmedikçe)
2814
+ // AÇMA — görünürlük: forceVisible true/false ise onu uygula;
2815
+ // belirtilmemişse kullanıcı tercihi (settings.browserHeadless) belirler.
2816
+ // PARALEL AJANLAR (bg oturum) her zaman forceVisible=false ile çağırır →
2817
+ // tarayıcı gizli modda çalışır, kullanıcı ekranı ve ajan konsolu rahatsız edilmez.
2563
2818
  browser.open = true;
2564
- browser.visible = forceVisible === true ? true : settings.browserHeadless !== true;
2819
+ browser.visible =
2820
+ forceVisible === true ? true : forceVisible === false ? false : settings.browserHeadless !== true;
2565
2821
  ensureBrowser();
2566
2822
  if (!browser.started) {
2567
2823
  browser.started = true;
@@ -2590,14 +2846,23 @@ function browserGate(job) {
2590
2846
  __trafficTail = p.catch(() => {});
2591
2847
  return p;
2592
2848
  }
2593
- async function browserNavigate(raw, signal) {
2849
+ /* AJAN TARAYICI STRATEJİSİ — TÜM oturumlar (ana sohbet + WhatsApp botları + paralel ajanlar):
2850
+ ajan tarayıcıyı KENDİ açıyorsa hep GİZLİ açılır — panel ekrana fırlamaz,
2851
+ Paralel Ajan Konsolu kapanmaz. Kullanıcı izlemek isterse tarayıcı düğmesine
2852
+ basar (browser:toggle gizli paneli görünür kılar). Zaten açıksa (kullanıcı
2853
+ paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
2854
+ function setBrowserOpenForAgent() {
2855
+ if (!browser.open) setBrowserOpen(true, false);
2856
+ }
2857
+
2858
+ async function browserNavigate(raw, signal, ctx) {
2594
2859
  return browserGate(async () => {
2595
2860
  await browserTrafficWait();
2596
- return browserNavigateNow(raw, signal);
2861
+ return browserNavigateNow(raw, signal, ctx);
2597
2862
  });
2598
2863
  }
2599
2864
 
2600
- async function browserNavigateNow(raw, signal) {
2865
+ async function browserNavigateNow(raw, signal, ctx) {
2601
2866
  let url = String(raw || '').trim();
2602
2867
  if (!url) return { ok: false, error: 'boş adres' };
2603
2868
  if (!/^https?:\/\//i.test(url)) {
@@ -2606,7 +2871,7 @@ async function browserNavigateNow(raw, signal) {
2606
2871
  ? 'https://' + url
2607
2872
  : 'https://duckduckgo.com/?q=' + encodeURIComponent(url);
2608
2873
  }
2609
- setBrowserOpen(true);
2874
+ setBrowserOpenForAgent();
2610
2875
  const wc = browser.view.webContents;
2611
2876
  await new Promise((resolve) => {
2612
2877
  let settled = false;
@@ -2668,6 +2933,120 @@ function flushBrowserStorage() {
2668
2933
  } catch {}
2669
2934
  }
2670
2935
 
2936
+ /* ---------- GİZLİ ARAŞTIRMA TARAYICISI (deep_search) ----------
2937
+ deep_search'ün "sayfayı açıp oku" adımı burada çalışır: WebContentsView
2938
+ HİÇbir pencereye eklenmez (kullanıcı hiçbir şey görmez) ama gerçek Chromium
2939
+ çalışır — JS/SPA sayfalar render olur, innerText okunur. Panel (browser.view)
2940
+ hiç meşgul edilmez. Görsel/font/media indirme hız için iptal edilir.
2941
+ 2 slot = en fazla 2 sayfa aynı anda okunur (ayrı view, çakışma yok). */
2942
+ const researchPool = { views: [null, null], queue: [], slots: [false, false] };
2943
+ const RESEARCH_PAGE_TIMEOUT = 25000;
2944
+ const RESEARCH_SETTLE_MS = 900;
2945
+ const RESEARCH_CONTENT_CAP = 4500;
2946
+
2947
+ function researchAcquire() {
2948
+ return new Promise((resolve) => {
2949
+ researchPool.queue.push(resolve);
2950
+ researchPump();
2951
+ });
2952
+ }
2953
+
2954
+ function researchRelease(idx) {
2955
+ researchPool.slots[idx] = false;
2956
+ researchPump();
2957
+ }
2958
+
2959
+ function researchPump() {
2960
+ for (let i = 0; i < researchPool.slots.length; i++) {
2961
+ if (!researchPool.slots[i] && researchPool.queue.length) {
2962
+ researchPool.slots[i] = true;
2963
+ researchPool.queue.shift()(i);
2964
+ }
2965
+ }
2966
+ }
2967
+
2968
+ function researchViewAt(idx) {
2969
+ let v = researchPool.views[idx];
2970
+ if (v && v.webContents && !v.webContents.isDestroyed()) return v;
2971
+ v = new WebContentsView({
2972
+ webPreferences: {
2973
+ partition: 'research', // kalıcı olmayan bölüm — çerez birikmez
2974
+ contextIsolation: true,
2975
+ nodeIntegration: false,
2976
+ sandbox: true,
2977
+ spellcheck: false,
2978
+ backgroundThrottling: false, // gizli çalışırken zamanlayıcılar yavaşlamasın
2979
+ },
2980
+ });
2981
+ const wc = v.webContents;
2982
+ try {
2983
+ /* Google/bazı siteler Electron UA'yı reddeder — gerçek Chrome kimliği */
2984
+ const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome} Safari/537.36`;
2985
+ wc.setUserAgent(chromeUA);
2986
+ const ses = session.fromPartition('research');
2987
+ if (ses && ses.setUserAgent) ses.setUserAgent(chromeUA, 'tr-TR,tr;q=0.9,en;q=0.8');
2988
+ } catch {}
2989
+ try { wc.setWindowOpenHandler(() => ({ action: 'deny' })); } catch {}
2990
+ try {
2991
+ session.fromPartition('research').setPermissionRequestHandler((_w, _p, cb) => cb(false));
2992
+ } catch {}
2993
+ try {
2994
+ /* hız: araştırma okuması için görsel/font/media gereksiz — iptal et */
2995
+ session.fromPartition('research').webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, cb) => {
2996
+ cb({ cancel: ['image', 'media', 'font'].includes(details.resourceType) });
2997
+ });
2998
+ } catch {}
2999
+ try {
3000
+ session.fromPartition('research').on('will-download', (_e, item) => { try { item.cancel(); } catch {} });
3001
+ } catch {}
3002
+ wc.on('render-process-gone', () => { researchPool.views[idx] = null; });
3003
+ researchPool.views[idx] = v;
3004
+ return v;
3005
+ }
3006
+
3007
+ async function researchRead(rawUrl, signal) {
3008
+ const url = String(rawUrl || '').trim();
3009
+ if (!/^https?:\/\//i.test(url)) return { ok: false, url, error: 'geçersiz adres' };
3010
+ if (signal && signal.aborted) return { ok: false, url, error: 'iptal edildi' };
3011
+ const idx = await researchAcquire();
3012
+ try {
3013
+ const view = researchViewAt(idx);
3014
+ if (!view) return { ok: false, url, error: 'araştırma tarayıcısı oluşturulamadı' };
3015
+ const wc = view.webContents;
3016
+ const loaded = await new Promise((resolve) => {
3017
+ let settled = false;
3018
+ const finish = (v) => { if (!settled) { settled = true; clearTimeout(timer); wc.removeListener('did-finish-load', onDone); wc.removeListener('did-fail-load', onFail); resolve(v); } };
3019
+ const timer = setTimeout(() => finish(false), RESEARCH_PAGE_TIMEOUT);
3020
+ const onDone = () => finish(true);
3021
+ const onFail = (_e, code) => { if (code !== -3) finish(false); };
3022
+ wc.once('did-finish-load', onDone);
3023
+ wc.on('did-fail-load', onFail);
3024
+ wc.loadURL(url).catch(() => {});
3025
+ });
3026
+ /* SPA hidrasyonu için kısa settle; metin boşsa bir kez daha dene */
3027
+ await new Promise((r) => setTimeout(r, RESEARCH_SETTLE_MS));
3028
+ let title = '';
3029
+ let finalUrl = url;
3030
+ try { title = wc.getTitle() || ''; finalUrl = wc.getURL() || url; } catch {}
3031
+ const grab = async () => {
3032
+ try { return String((await wc.executeJavaScript('(document.body&&document.body.innerText)||""', true)) || ''); } catch { return ''; }
3033
+ };
3034
+ let text = (await grab()).replace(/\n{3,}/g, '\n\n').trim();
3035
+ if (!text) {
3036
+ await new Promise((r) => setTimeout(r, 1500));
3037
+ text = (await grab()).replace(/\n{3,}/g, '\n\n').trim();
3038
+ }
3039
+ if (!text) {
3040
+ return { ok: false, url: finalUrl, title, error: loaded ? 'sayfa metni boş (tam JS-görsel veya engelli sayfa olabilir)' : 'sayfa yüklenemedi (zaman aşımı/hata)' };
3041
+ }
3042
+ return { ok: true, url: finalUrl, title, truncated: text.length > RESEARCH_CONTENT_CAP, content: text.slice(0, RESEARCH_CONTENT_CAP) };
3043
+ } catch (e) {
3044
+ return { ok: false, url, error: String((e && e.message) || e) };
3045
+ } finally {
3046
+ researchRelease(idx);
3047
+ }
3048
+ }
3049
+
2671
3050
  /* Dahili OCR: görsel desteklemeyen modeller için tesseract.js ile metin okuma.
2672
3051
  Dil verisi ilk kullanımda %APPDATA%\beast\tessdata'ya iner, sonra offline çalışır. */
2673
3052
  const _ocrWorkers = new Map(); // lang -> worker (her çağrıda yeniden init olmasın)
@@ -2705,21 +3084,21 @@ async function ocrRead({ image, lang = 'tur+eng' } = {}) {
2705
3084
  Numara: view'i google.com'a bir kez açıp aramaları SAYFA İÇİ fetch() ile
2706
3085
  yapmak (kullanıcının console'da yaptığı gibi) — sayfa bile değişmeden
2707
3086
  DOMParser ile sonuç çekilir. Fetch olmazsa direk gezinme fallback'i var. */
2708
- async function browserSearch(query, signal) {
3087
+ async function browserSearch(query, signal, ctx) {
2709
3088
  /* paralel ajan sorguları trafik kapısından sırayla geçer */
2710
3089
  return browserGate(async () => {
2711
3090
  await browserTrafficWait();
2712
- return browserSearchNow(query, signal);
3091
+ return browserSearchNow(query, signal, ctx);
2713
3092
  });
2714
3093
  }
2715
3094
 
2716
- async function browserSearchNow(query, signal) {
3095
+ async function browserSearchNow(query, signal, ctx) {
2717
3096
  try {
2718
3097
  if (!win || win.isDestroyed()) return null;
2719
3098
  if (signal && signal.aborted) return null;
2720
3099
  const q = String(query || '').trim();
2721
3100
  if (!q) return null;
2722
- setBrowserOpen(true);
3101
+ setBrowserOpenForAgent();
2723
3102
  const wc = browser.view && browser.view.webContents;
2724
3103
  if (!wc) return null;
2725
3104
 
@@ -3325,6 +3704,7 @@ ipcMain.handle('sessions:create', () => {
3325
3704
  } else {
3326
3705
  engine.setSessionTools(v.id, null);
3327
3706
  }
3707
+ engine.setSessionModel(v.id, b && !b.admin ? (b.model || null) : null);
3328
3708
  return v;
3329
3709
  });
3330
3710
  ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
@@ -3344,7 +3724,13 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
3344
3724
  if (!sess) {
3345
3725
  try { sess = engine._load(sid); } catch {}
3346
3726
  }
3347
- if (sess && !sess.botId) engine.setSessionBot(sid, actBot.id);
3727
+ if (sess && !sess.botId) {
3728
+ engine.setSessionBot(sid, actBot.id);
3729
+ /* tam bağlama: izin + araç seti + botun kendi modeli (sessions:create ile aynı) */
3730
+ engine.setSessionPerm(sid, actBot.perm || 'all');
3731
+ engine.setSessionTools(sid, botToolSet(actBot));
3732
+ engine.setSessionModel(sid, actBot.model || null);
3733
+ }
3348
3734
  }
3349
3735
  } catch {}
3350
3736
  if (t === '/stop' || t === '/start') {
@@ -3748,27 +4134,21 @@ function flushDesktopOnDone(ev) {
3748
4134
  - Bağlantı geri gelince (DNS kontrolü) sırayla otomatik gönderilir
3749
4135
  - Renderer'a 'net' / 'netQueue' olayları gider: ⏳ kuyruk balonu + toast */
3750
4136
  const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
4137
+ const NET_LOOKUP_HOSTS = ['www.google.com', 'www.microsoft.com'];
4138
+ const NET_HTTP_PROBES = [
4139
+ 'http://www.msftconnecttest.com/connecttest.txt',
4140
+ 'http://cp.cloudflare.com/generate_204',
4141
+ 'http://connectivitycheck.gstatic.com/generate_204',
4142
+ ];
3751
4143
  const NET_CHECK_MS = 8000;
3752
4144
  const NET_CHECK_TIMEOUT = 4000;
3753
- const NET_PROBE_URLS = ['https://1.1.1.1/cdn-cgi/trace', 'https://www.google.com/generate_204'];
3754
- let __netFailStreak = 0;
3755
-
3756
- /* Gerçek HTTP yoklaması: DNS yerine sayfa çekmeyi dener — proxy açıksa proxy
3757
- üzerinden gider (kullanıcı trafiğinin gördüğü interneti ölçer) */
3758
- async function httpProbe() {
3759
- for (const u of NET_PROBE_URLS) {
3760
- try {
3761
- const res = await (__origFetch || fetch)(u, { signal: AbortSignal.timeout(5000) });
3762
- if (res.ok) return true;
3763
- } catch {}
3764
- }
3765
- return false;
3766
- }
4145
+ const NET_OFFLINE_STRIKES = 2; // üst üste bu kadar başarısız turda offline ilan edilir
3767
4146
  const CHAT_QUEUE_MAX = 50; // kuyruk üst sınırı — taşarsa en eski düşer
3768
4147
 
3769
4148
  let netOnline = true; // son bilinen bağlantı durumu (başlangıçta iyimser)
3770
4149
  let netCheckedOnce = false;
3771
4150
  let netCheckBusy = false;
4151
+ let netFailStreak = 0;
3772
4152
  let chatQueueFlushing = false;
3773
4153
  const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
3774
4154
 
@@ -3852,8 +4232,42 @@ async function flushChatQueue() {
3852
4232
  }
3853
4233
  }
3854
4234
 
3855
- /* gerçek internet kontrolü: DNS çözümlemesi (sadece arayüzü değil,
3856
- paket gerçekten çıkıyor mu test eder). Adaylar sırayla denenir. */
4235
+ /* gerçek internet kontrolü TEK yöntem yanıltıcı olabilir:
4236
+ dns.resolve (c-ares) sistem çözümleyicisini atlar; mobil ağ/hotspot/VPN ve
4237
+ ISS DNS engellemelerinde (ör. 1.1.1.1, dns.google) internet VARken bile
4238
+ başarısız çıkar. Bu yüzden katmanlı deniyoruz; HERHANGİ bir katman başarılıysa
4239
+ internet VAR sayılır:
4240
+ 1) HTTP connectivity endpoint'leri (http modülü dns.lookup = OS çözümleyicisi
4241
+ kullanır — tarayıcı gibi; captive portal/proxy/mobil ağ hepsinde çalışır)
4242
+ 2) dns.lookup (Windows sistem çözümleyicisi — hosts dosyası/VPN/NRPT dahil)
4243
+ + dns.resolve (doğrudan DNS sunucusu)
4244
+ 3) OS'in kendi bağlantı durumu (Electron net.isOnline — Windows NCSI)
4245
+ Ayrıca tek başarısız tur offline ilan etmez (2 üst üste başarısız tur gerekir):
4246
+ geçici DNS gecikmesi mesajları gereksiz kuyruğa atmaz. */
4247
+ function httpProbe(url) {
4248
+ return new Promise((resolve) => {
4249
+ let done = false;
4250
+ const fin = (v) => { if (!done) { done = true; resolve(v); } };
4251
+ try {
4252
+ const req = http.get(url, { timeout: NET_CHECK_TIMEOUT }, (res) => {
4253
+ res.resume(); // gövdeyi tüket — soket serbest kalsın
4254
+ const ok = !!res.statusCode && res.statusCode < 500;
4255
+ try { res.destroy(); } catch {}
4256
+ fin(ok);
4257
+ });
4258
+ req.on('timeout', () => { try { req.destroy(); } catch {} fin(false); });
4259
+ req.on('error', () => fin(false));
4260
+ } catch { fin(false); }
4261
+ });
4262
+ }
4263
+
4264
+ function lookupProbe(host) {
4265
+ return new Promise((resolve) => {
4266
+ const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
4267
+ dns.lookup(host, (err) => { clearTimeout(t); resolve(!err); });
4268
+ });
4269
+ }
4270
+
3857
4271
  function dnsProbe(host) {
3858
4272
  return new Promise((resolve) => {
3859
4273
  const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
@@ -3868,37 +4282,37 @@ async function netCheck() {
3868
4282
  if (netCheckBusy) return;
3869
4283
  netCheckBusy = true;
3870
4284
  try {
3871
- let ok = false;
3872
- for (const h of NET_CHECK_HOSTS) {
3873
- if (await dnsProbe(h)) { ok = true; break; }
4285
+ let ok = (await Promise.all(NET_HTTP_PROBES.map(httpProbe))).some(Boolean);
4286
+ if (!ok) {
4287
+ const lookups = [
4288
+ ...NET_LOOKUP_HOSTS.map(lookupProbe),
4289
+ ...NET_CHECK_HOSTS.map(dnsProbe),
4290
+ ];
4291
+ ok = (await Promise.all(lookups)).some(Boolean);
3874
4292
  }
3875
4293
  if (!ok) {
3876
- /* DNS probe yanıltıcı olabilir (proxy, DoH, kurumsal ağ, VPN):
3877
- HTTP yoklamasıyla DOĞRULA — tarayıcının gördüğü internete yakın sinyal */
3878
- ok = await httpProbe();
4294
+ try { ok = electronNet.isOnline() === true; } catch {}
3879
4295
  }
3880
4296
  const first = !netCheckedOnce;
3881
4297
  const was = netOnline;
3882
- /* Histerezis: tek-iki başarısız yoklama (wifi tıkı, anlık dns) durumu
3883
- bozmasın — üst üste 3 yoklama (≈24+ sn) başarısızsa offline ilan et */
3884
- if (!ok) {
3885
- __netFailStreak++;
3886
- if (netOnline && __netFailStreak < 3) { netCheckedOnce = true; return; }
4298
+ netCheckedOnce = true;
4299
+ if (ok) {
4300
+ netFailStreak = 0;
4301
+ netOnline = true;
3887
4302
  } else {
3888
- __netFailStreak = 0;
4303
+ netFailStreak++;
4304
+ if (!was || netFailStreak >= NET_OFFLINE_STRIKES) netOnline = false;
3889
4305
  }
3890
- netOnline = ok;
3891
- netCheckedOnce = true;
3892
- if (was !== ok || first) {
4306
+ if (netOnline !== was || first) {
3893
4307
  try {
3894
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: ok });
4308
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: netOnline });
3895
4309
  } catch {}
3896
- if (ok) {
4310
+ if (netOnline) {
3897
4311
  log.info('main', 'bağlantı geri geldi — offline kuyruk kontrol ediliyor');
3898
4312
  chatQueueEmit(); // renderer: pill/toast güncellensin
3899
4313
  flushChatQueue().catch(() => {});
3900
4314
  } else {
3901
- log.info('main', 'internet bağlantısı yok — mesajlar kuyruğa alınacak');
4315
+ log.info('main', `internet bağlantısı yok — mesajlar kuyruğa alınacak (streak=${netFailStreak})`);
3902
4316
  chatQueueEmit();
3903
4317
  }
3904
4318
  }
@@ -3908,9 +4322,21 @@ async function netCheck() {
3908
4322
  }
3909
4323
 
3910
4324
  ipcMain.handle('model:set', (_e, sel) => {
3911
- settings.modelOverride = sel;
3912
- saveSettings();
3913
- engine.setModelOverride(sel);
4325
+ /* MÜŞTERİ botu aktifken picker seçimi O BOTUN modelini değiştirir;
4326
+ Beast (admin) aktifken global seçim değişir. */
4327
+ const act = settings.activeBotId ? bots.get(settings.activeBotId) : null;
4328
+ if (act && !act.admin) {
4329
+ try { bots.update(act.id, { model: String(sel || '') }); } catch {}
4330
+ try {
4331
+ for (const v of engine.listSessions()) {
4332
+ if (v.botId === act.id) engine.setSessionModel(v.id, sel || null);
4333
+ }
4334
+ } catch {}
4335
+ } else {
4336
+ settings.modelOverride = sel;
4337
+ saveSettings();
4338
+ engine.setModelOverride(sel);
4339
+ }
3914
4340
  return engine.publicState();
3915
4341
  });
3916
4342
 
@@ -4136,294 +4562,6 @@ ipcMain.handle('wa:groups:set', (_e, cfg) => {
4136
4562
  return settings.waGroups;
4137
4563
  });
4138
4564
 
4139
- /* ---------------- Proxy (Ayarlar → Proxy) ----------------
4140
- Kullanıcının verdiği proxy(ler) üzerinden çıkış: LLM API'leri, http_fetch,
4141
- TinyFish/Exa, python motorları (env) ve dahili tarayıcı (Chromium proxyRules).
4142
- Çoklu adres = Node fetch tarafında round-robin rotasyon. SOCKS5 yalnız tarayıcıda. */
4143
- let __proxyIdx = 0;
4144
- let __proxyAgents = null; // url -> undici.ProxyAgent
4145
- let __origFetch = null;
4146
-
4147
- function proxyUrlList() {
4148
- return String((settings.proxy && settings.proxy.urls) || '')
4149
- .split(/[\n,;]+/)
4150
- .map((s) => s.trim())
4151
- .filter((s) => /^(https?|socks5):\/\//i.test(s));
4152
- }
4153
-
4154
- function proxyAgentFor(url) {
4155
- const undici = require('undici');
4156
- if (!__proxyAgents) __proxyAgents = new Map();
4157
- if (!__proxyAgents.has(url)) {
4158
- if (/^socks5:/i.test(url)) __proxyAgents.set(url, new undici.Agent({ connect: socks5Connector(url), connections: 64 }));
4159
- else __proxyAgents.set(url, new undici.ProxyAgent(url));
4160
- }
4161
- return __proxyAgents.get(url);
4162
- }
4163
-
4164
- /* Minimal SOCKS5 CONNECT connector (undici Agent.connect imzası).
4165
- WARP (warp-plus) dahil tüm socks5 proxy'leri Node fetch'te çalıştırır. */
4166
- function socks5Connector(socksUrl) {
4167
- const net = require('net');
4168
- const tls = require('tls');
4169
- const u = new URL(socksUrl);
4170
- const shost = u.hostname;
4171
- const sport = Number(u.port) || 1080;
4172
- const suser = decodeURIComponent(u.username || '');
4173
- const spass = decodeURIComponent(u.password || '');
4174
- return function connect(opts, cb) {
4175
- let stage = 0; // 0 greet · 1 auth · 2 conn
4176
- let buf = Buffer.alloc(0);
4177
- const socket = net.connect({ host: shost, port: sport });
4178
- const fail = (err) => { try { socket.destroy(); } catch {} cb(err); };
4179
- const onData = (d) => {
4180
- buf = Buffer.concat([buf, d]);
4181
- if (stage === 0) {
4182
- if (buf.length < 2) return;
4183
- if (buf[0] !== 5) return fail(new Error('socks5: geçersiz yanıt'));
4184
- const method = buf[1];
4185
- buf = buf.subarray(2);
4186
- if (method === 2) {
4187
- stage = 1;
4188
- const ub = Buffer.from(suser), pb = Buffer.from(spass);
4189
- socket.write(Buffer.concat([Buffer.from([1, ub.length]), ub, Buffer.from([pb.length]), pb]));
4190
- } else if (method === 0) sendReq();
4191
- else return fail(new Error('socks5: desteklenmeyen kimlik doğrulama'));
4192
- } else if (stage === 1) {
4193
- if (buf.length < 2) return;
4194
- buf = buf.subarray(2);
4195
- sendReq();
4196
- } else if (stage === 2) {
4197
- if (buf.length < 5) return;
4198
- if (buf[0] !== 5 || buf[1] !== 0) return fail(new Error('socks5: bağlantı reddedildi (' + buf[1] + ')'));
4199
- const need = 6 + (buf[3] === 1 ? 4 : buf[3] === 4 ? 16 : 1 + buf[4]);
4200
- if (buf.length < need) return;
4201
- buf = Buffer.alloc(0);
4202
- socket.off('data', onData);
4203
- if (opts.protocol === 'https:') {
4204
- const tlsSock = tls.connect({ socket, servername: opts.servername || opts.hostname });
4205
- tlsSock.on('error', fail);
4206
- tlsSock.once('secureConnect', () => cb(null, tlsSock));
4207
- } else cb(null, socket);
4208
- }
4209
- };
4210
- socket.on('error', fail);
4211
- socket.on('connect', () => {
4212
- /* SOCKS5 greeting: şifre varsa user/pass(2), yoksa no-auth(0) yöntemi öner */
4213
- socket.write(suser ? Buffer.from([5, 2, 0, 2]) : Buffer.from([5, 1, 0]));
4214
- });
4215
- socket.on('data', onData);
4216
- function sendReq() {
4217
- stage = 2;
4218
- const host = opts.hostname || opts.host;
4219
- const port = Number(opts.port) || (opts.protocol === 'https:' ? 443 : 80);
4220
- const hb = Buffer.from(String(host), 'utf8');
4221
- socket.write(Buffer.concat([Buffer.from([5, 1, 0, 3, hb.length]), hb, Buffer.from([(port >> 8) & 255, port & 255])]));
4222
- }
4223
- };
4224
- }
4225
-
4226
- function applyProxy() {
4227
- const enabled = !!(settings.proxy && settings.proxy.enabled);
4228
- const allUrls = enabled ? proxyUrlList() : [];
4229
- const httpUrls = allUrls.filter((u) => /^https?:\/\//i.test(u));
4230
-
4231
- /* 1) Node fetch (LLM API + http_fetch + TinyFish/Exa + GitHub...) — global sarmalayıcı.
4232
- http(s) VE socks5 (socks5Connector) destekli — http(s) proxy'lerde rotasyon var. */
4233
- if (enabled && allUrls.length) {
4234
- if (!__origFetch) {
4235
- __origFetch = globalThis.fetch;
4236
- globalThis.fetch = (input, init = {}) => {
4237
- const list = proxyUrlList();
4238
- if (!list.length) return __origFetch(input, init);
4239
- const p = list[Math.abs(__proxyIdx++) % list.length];
4240
- return __origFetch(input, { ...init, dispatcher: proxyAgentFor(p) });
4241
- };
4242
- }
4243
- } else if (__origFetch) {
4244
- globalThis.fetch = __origFetch;
4245
- __origFetch = null;
4246
- }
4247
-
4248
- /* 2) python/cmd çocuk süreçleri (ddgs, requests... env'den okur) */
4249
- if (enabled && httpUrls.length) {
4250
- process.env.HTTP_PROXY = httpUrls[0];
4251
- process.env.HTTPS_PROXY = httpUrls[0];
4252
- process.env.ALL_PROXY = httpUrls[0];
4253
- process.env.NO_PROXY = 'localhost,127.0.0.1';
4254
- } else {
4255
- delete process.env.HTTP_PROXY;
4256
- delete process.env.HTTPS_PROXY;
4257
- delete process.env.ALL_PROXY;
4258
- delete process.env.NO_PROXY;
4259
- }
4260
-
4261
- /* 3) dahili tarayıcı (Chromium; socks5 destekler) */
4262
- try {
4263
- const ses = session.fromPartition('persist:browser');
4264
- if (ses) {
4265
- if (enabled && allUrls.length) ses.setProxy({ proxyRules: allUrls[0] }).catch(() => {});
4266
- else ses.setProxy({ mode: 'direct' }).catch(() => {});
4267
- }
4268
- } catch {}
4269
- }
4270
-
4271
- ipcMain.handle('proxy:get', () => ({
4272
- enabled: !!(settings.proxy && settings.proxy.enabled),
4273
- urls: String((settings.proxy && settings.proxy.urls) || ''),
4274
- }));
4275
-
4276
- ipcMain.handle('proxy:set', (_e, cfg) => {
4277
- settings.proxy = {
4278
- enabled: !!(cfg && cfg.enabled),
4279
- urls: String((cfg && cfg.urls) || '').slice(0, 2000),
4280
- };
4281
- saveSettings();
4282
- applyProxy();
4283
- return { ok: true, enabled: settings.proxy.enabled, urls: settings.proxy.urls };
4284
- });
4285
-
4286
- ipcMain.handle('proxy:test', async () => {
4287
- const allUrls = proxyUrlList();
4288
- if (!allUrls.length) return { ok: false, error: 'proxy adresi girilmemiş' };
4289
- const u = allUrls[Math.abs(__proxyIdx++) % allUrls.length];
4290
- const t0 = Date.now();
4291
- try {
4292
- const res = await (__origFetch || fetch)('https://api.ipify.org?format=json', {
4293
- dispatcher: proxyAgentFor(u),
4294
- signal: AbortSignal.timeout(15000),
4295
- });
4296
- const j = await res.json();
4297
- return { ok: true, ip: j.ip, proxy: u, ms: Date.now() - t0 };
4298
- } catch (e) {
4299
- return { ok: false, error: String((e && e.message) || e).slice(0, 160), proxy: u };
4300
- }
4301
- });
4302
-
4303
- /* ---------------- Cloudflare WARP (warp-plus) ----------------
4304
- Açık kaynak warp-plus tek binary: yerel socks5 (127.0.0.1:8086) açar, o port
4305
- Cloudflare WARP ağına tünel olur. Bedava, üyeliksiz (profil otomatik oluşur).
4306
- Beast: indirme + başlatma + watchdog + kapatma. Sadece Beast trafiği geçer. */
4307
- const WARP_PORT = 8086;
4308
- const WARP_PROXY_LINE = 'socks5://127.0.0.1:' + WARP_PORT;
4309
- let warpChild = null;
4310
- let warpWatchdog = null;
4311
- let warpStopping = false;
4312
-
4313
- function warpBinPath() { return path.join(APP_DIR, 'bin', 'warp-plus.exe'); }
4314
- function warpDir() { return path.join(APP_DIR, 'warp'); }
4315
- function warpEnabled() { return !!(settings.warp && settings.warp.enabled); }
4316
-
4317
- async function ensureWarpPlusBinary() {
4318
- const bin = warpBinPath();
4319
- if (fs.existsSync(bin)) return bin;
4320
- fs.mkdirSync(path.dirname(bin), { recursive: true });
4321
- log.info('main', 'warp-plus indiriliyor…');
4322
- const rel = await (__origFetch || fetch)('https://api.github.com/repos/bepass-org/warp-plus/releases/latest', {
4323
- headers: { 'User-Agent': 'beast-agent', Accept: 'application/vnd.github+json' },
4324
- signal: AbortSignal.timeout(20000),
4325
- });
4326
- if (!rel.ok) throw new Error('sürüm bilgisi alınamadı (' + rel.status + ')');
4327
- const j = await rel.json();
4328
- const asset = (j.assets || []).find((a) => /warp-plus_windows-amd64\.zip$/i.test(a.name || ''));
4329
- if (!asset) throw new Error('windows paketi bulunamadı');
4330
- const zipPath = bin + '.zip';
4331
- const res = await (__origFetch || fetch)(asset.browser_download_url, { signal: AbortSignal.timeout(300000) });
4332
- if (!res.ok) throw new Error('indirme başarısız (' + res.status + ')');
4333
- fs.writeFileSync(zipPath, Buffer.from(await res.arrayBuffer()));
4334
- await new Promise((resolve, reject) => {
4335
- spawn('powershell.exe', ['-NoProfile', '-Command', `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${path.dirname(bin)}'`])
4336
- .on('exit', (c) => (c === 0 ? resolve() : reject(new Error('zip açılamadı'))))
4337
- .on('error', reject);
4338
- });
4339
- try { fs.unlinkSync(zipPath); } catch {}
4340
- if (!fs.existsSync(bin)) throw new Error('warp-plus.exe açılamadı');
4341
- log.info('main', 'warp-plus indirildi: ' + bin);
4342
- return bin;
4343
- }
4344
-
4345
- function warpPortAlive() {
4346
- const net = require('net');
4347
- return new Promise((resolve) => {
4348
- const s = net.connect({ host: '127.0.0.1', port: WARP_PORT, timeout: 1200 }, () => { s.destroy(); resolve(true); });
4349
- s.on('error', () => resolve(false));
4350
- s.on('timeout', () => { s.destroy(); resolve(false); });
4351
- });
4352
- }
4353
-
4354
- async function startWarp() {
4355
- if (warpChild) return { ok: true };
4356
- const bin = await ensureWarpPlusBinary();
4357
- fs.mkdirSync(warpDir(), { recursive: true });
4358
- try {
4359
- warpChild = spawn(bin, [], { windowsHide: true, cwd: warpDir() });
4360
- } catch (e) {
4361
- return { ok: false, error: String((e && e.message) || e) };
4362
- }
4363
- warpChild.stdout.on('data', () => {});
4364
- warpChild.stderr.on('data', () => {});
4365
- warpChild.on('exit', () => {
4366
- warpChild = null;
4367
- if (warpEnabled() && !warpStopping) {
4368
- /* watchdog: beklenmedik çıkışta 5 sn sonra yeniden başlat */
4369
- clearTimeout(warpWatchdog);
4370
- warpWatchdog = setTimeout(() => { startWarp().catch(() => {}); }, 5000);
4371
- }
4372
- });
4373
- for (let i = 0; i < 180; i++) {
4374
- if (await warpPortAlive()) { log.info('main', 'warp-plus hazır (127.0.0.1:' + WARP_PORT + ')'); return { ok: true }; }
4375
- if (!warpChild) break;
4376
- await new Promise((r) => setTimeout(r, 500));
4377
- }
4378
- return { ok: false, error: 'warp-plus başlatılamadı (port açılmadı)' };
4379
- }
4380
-
4381
- function stopWarp() {
4382
- warpStopping = true;
4383
- clearTimeout(warpWatchdog);
4384
- try { if (warpChild) spawn('taskkill', ['/pid', String(warpChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
4385
- try { if (warpChild) warpChild.kill(); } catch {}
4386
- warpChild = null;
4387
- setTimeout(() => { warpStopping = false; }, 1500);
4388
- }
4389
-
4390
- ipcMain.handle('warp:get', () => ({
4391
- enabled: warpEnabled(),
4392
- running: !!warpChild,
4393
- installed: fs.existsSync(warpBinPath()),
4394
- line: WARP_PROXY_LINE,
4395
- }));
4396
-
4397
- ipcMain.handle('warp:set', async (_e, on) => {
4398
- const want = !!on;
4399
- settings.warp = { enabled: want };
4400
- saveSettings();
4401
- if (want) {
4402
- const r = await startWarp();
4403
- if (!r.ok) {
4404
- settings.warp = { enabled: false };
4405
- saveSettings();
4406
- return { ok: false, error: r.error || 'başlatılamadı' };
4407
- }
4408
- if (!settings.proxy) settings.proxy = { enabled: true, urls: '' };
4409
- const lines = proxyUrlList();
4410
- if (!lines.includes(WARP_PROXY_LINE)) lines.push(WARP_PROXY_LINE);
4411
- settings.proxy.urls = lines.join('\n');
4412
- settings.proxy.enabled = true; // WARP açıldıysa trafik ondan geçsin
4413
- saveSettings();
4414
- applyProxy();
4415
- return { ok: true, enabled: true };
4416
- }
4417
- /* kapat: satırı listeden çıkar, başka proxy yoksa master'ı da kapat */
4418
- const rest = proxyUrlList().filter((u) => u !== WARP_PROXY_LINE);
4419
- settings.proxy.urls = rest.join('\n');
4420
- if (!rest.length) settings.proxy.enabled = false;
4421
- saveSettings();
4422
- applyProxy();
4423
- stopWarp();
4424
- return { ok: true, enabled: false };
4425
- });
4426
-
4427
4565
  ipcMain.handle('settings:get', () => {
4428
4566
  const out = JSON.parse(JSON.stringify(settings));
4429
4567
  /* Sırların renderera düz metin gitmesini engelle */
@@ -4546,7 +4684,7 @@ ipcMain.handle('update:install', () => {
4546
4684
  if (!isNpmMode()) return { ok: false, error: NPM_ONLY_TEXT };
4547
4685
  npmUpdateViaCmd();
4548
4686
  return { ok: true, npm: true };
4549
- });
4687
+ });
4550
4688
 
4551
4689
  ipcMain.handle('update:setAuto', (_e, cfg) => {
4552
4690
  if (cfg && typeof cfg.autoCheck === 'boolean') settings.autoCheckUpdate = cfg.autoCheck;
@@ -4781,7 +4919,12 @@ ipcMain.handle('cron:runNow', (_e, id) => {
4781
4919
 
4782
4920
  /* ---------------- tarayıcı IPC ---------------- */
4783
4921
  ipcMain.handle('browser:toggle', () => {
4784
- /* kullanıcı kendi açıyor mutlaka görünür */
4922
+ /* gizli çalışan ajan paneli varsa düğme ONU GÖRÜNÜR yapar; değilse aç/kapa.
4923
+ (ajanlar tarayıcıyı hep gizli açar — kullanıcı izlemek isterse buradan gösterir) */
4924
+ if (browser.open && !browser.visible) {
4925
+ setBrowserOpen(true, true);
4926
+ return { open: browser.open, visible: browser.visible };
4927
+ }
4785
4928
  setBrowserOpen(!browser.open, true);
4786
4929
  return { open: browser.open, visible: browser.visible };
4787
4930
  });
@@ -5438,12 +5581,15 @@ function runNpmGlobalInstall() {
5438
5581
  });
5439
5582
  }
5440
5583
 
5441
- /* auth login etkileşimlidir (tarayıcı açar) — kullanıcı görsün diye ayrı pencere */
5584
+ /* auth login etkileşimlidir (tarayıcı açar) — kullanıcı görsün diye ayrı pencere.
5585
+ -ExecutionPolicy Bypass: SADECE bu pencere için — Windows varsayılan "Restricted"
5586
+ politikası npm'in opencode.ps1 shim'ini bloklar ("running scripts is disabled")
5587
+ → taze makinede tek tık kurulum patlamasın; sistem politikası DEĞİŞMEZ. */
5442
5588
  function openZenAuthWindow() {
5443
5589
  try {
5444
5590
  spawn(
5445
5591
  'cmd.exe',
5446
- ['/c', 'start', '', 'powershell.exe', '-NoExit', '-Command', 'opencode auth login'],
5592
+ ['/c', 'start', '', 'powershell.exe', '-NoExit', '-ExecutionPolicy', 'Bypass', '-Command', 'opencode auth login'],
5447
5593
  { detached: true, windowsHide: false, stdio: 'ignore' }
5448
5594
  ).unref();
5449
5595
  return true;
@@ -5668,6 +5814,42 @@ ipcMain.handle('tg:allow:set', (_e, list) => {
5668
5814
  });
5669
5815
  ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
5670
5816
 
5817
+ /* ---------- Discord IPC ---------- */
5818
+ ipcMain.handle('dc:status:get', () => {
5819
+ if (!dc) return { configured: !!settings.dcToken, status: 'disconnected', user: null, connected: false };
5820
+ return { configured: true, ...dc.snapshot() };
5821
+ });
5822
+
5823
+ /* token kaydet + köprüyü (yeniden) başlat */
5824
+ ipcMain.handle('dc:set', async (_e, token) => {
5825
+ const t = String(token || '').trim();
5826
+ if (t) settings.dcToken = t;
5827
+ saveSettings();
5828
+ await restartDc();
5829
+ return { configured: !!settings.dcToken, ...(dc ? dc.snapshot() : { status: 'disconnected', user: null }) };
5830
+ });
5831
+
5832
+ ipcMain.handle('dc:start', async () => {
5833
+ if (!settings.dcToken) return { ok: false, error: 'token yok — önce bot tokenı gir' };
5834
+ await restartDc();
5835
+ return { ok: true, ...(dc ? dc.snapshot() : {}) };
5836
+ });
5837
+
5838
+ ipcMain.handle('dc:stop', async () => {
5839
+ if (dc) {
5840
+ try { await dc.stop(); } catch {}
5841
+ }
5842
+ return { ok: true };
5843
+ });
5844
+
5845
+ ipcMain.handle('dc:allow:get', () => settings.dcAllow || []);
5846
+ ipcMain.handle('dc:allow:set', (_e, list) => {
5847
+ settings.dcAllow = Array.isArray(list) ? list : [];
5848
+ saveSettings();
5849
+ return settings.dcAllow;
5850
+ });
5851
+ ipcMain.handle('dc:sessions', () => [...dcChats.values()]);
5852
+
5671
5853
  /* ---------- e-posta IPC ---------- */
5672
5854
 
5673
5855
  ipcMain.handle('email:get', () => {
@@ -5929,6 +6111,7 @@ ipcMain.handle('bots:update', (_e, { id, patch }) => {
5929
6111
  if ((v.botId || 'beast') === String(id || '')) {
5930
6112
  const cfg = bots.get(String(id));
5931
6113
  engine.setSessionTools(v.id, cfg && !cfg.admin ? botToolSet(cfg) : null);
6114
+ engine.setSessionModel(v.id, cfg && !cfg.admin ? (cfg.model || null) : null);
5932
6115
  }
5933
6116
  }
5934
6117
  } catch {}