beast-agent 0.26.0 → 0.26.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
@@ -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,224 @@ 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 (win && !win.isDestroyed()) win.webContents.send('dc:event', ev);
2244
+ },
2245
+ onIncoming: handleDcIncoming,
2246
+ });
2247
+ }
2248
+ return dc;
2249
+ }
2250
+
2251
+ /* token değişimi / yeniden başlatma: eski köprüyü kapat, yenisini aç */
2252
+ async function restartDc() {
2253
+ if (dc) {
2254
+ try { await dc.stop(); } catch {}
2255
+ dc = null;
2256
+ }
2257
+ if (!settings.dcToken) return;
2258
+ const b = ensureDc();
2259
+ try {
2260
+ await b.start();
2261
+ } catch (e) {
2262
+ dcLog(`start başarısız: ${String((e && e.message) || e)}`);
2263
+ }
2264
+ }
2265
+
2040
2266
  function reloadBackend() {
2041
2267
  if (engine && typeof engine.dispose === 'function') {
2042
2268
  try { engine.dispose(); } catch {}
@@ -2091,13 +2317,18 @@ function reloadBackend() {
2091
2317
  ocr: (o) => ocrRead(o),
2092
2318
  email: { list: emailList, read: emailRead, send: emailSend },
2093
2319
  browser: {
2094
- openUrl: (u, s) => browserNavigate(u, s),
2095
- search: (q, s) => browserSearch(q, s),
2320
+ openUrl: (u, s, ctx) => browserNavigate(u, s, ctx),
2321
+ search: (q, s, ctx) => browserSearch(q, s, ctx),
2096
2322
  readText: (s) => browserRead(s),
2097
2323
  screenshot: (s) => browserScreenshot(s),
2098
2324
  snapshot: (s) => browserSnapshot(s),
2099
2325
  act: (k, a, s) => browserAct(k, a, s),
2100
2326
  },
2327
+ research: {
2328
+ /* deep_search'ün "sayfayı GİZLİ tarayıcıda açıp oku" parçası (Electron-only).
2329
+ Arama zinciri engine._webSearchChain içinde web_search ile birebir aynı. */
2330
+ readPage: (u, s) => researchRead(u, s),
2331
+ },
2101
2332
  emit: (ev) => {
2102
2333
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
2103
2334
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
@@ -2201,6 +2432,27 @@ function reloadBackend() {
2201
2432
  })();
2202
2433
  }
2203
2434
  }
2435
+ // Discord oturumlarının son cevabını geri gönder (TG ile aynı akış)
2436
+ if ((ev.type === 'done' || ev.type === 'error') && dc && dc.connected) {
2437
+ const hitD = [...dcChats.entries()].find(([, s]) => s === ev.sessionId);
2438
+ if (hitD) {
2439
+ const dchid = hitD[0];
2440
+ (async () => {
2441
+ try {
2442
+ if (ev.type === 'error') {
2443
+ await sendDcSafe(dchid, 'Bir aksilik oldu: ' + String(ev.error || '').slice(0, 200));
2444
+ return;
2445
+ }
2446
+ if (!ev.aborted) {
2447
+ const s = engine.openSession(ev.sessionId);
2448
+ const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
2449
+ const txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
2450
+ if (txt.trim()) await sendDcSafe(dchid, txt);
2451
+ }
2452
+ } catch {}
2453
+ })();
2454
+ }
2455
+ }
2204
2456
  },
2205
2457
  });
2206
2458
  return engine.publicState();
@@ -2327,10 +2579,6 @@ function ensureDesktopShortcut() {
2327
2579
  }
2328
2580
 
2329
2581
  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
2582
  // Tailscale modu: paketli uygulamada Windows ile otomatik başlat (sessiz, tepside)
2335
2583
  if (app.isPackaged) {
2336
2584
  /* DAĞITIM KARARI: EXE/portable kurulum desteklenmiyor — tek yol npm.
@@ -2395,6 +2643,11 @@ app.whenReady().then(() => {
2395
2643
  ensureTg().start().catch((e) => tgLog('autostart failed: ' + String((e && e.message) || e)));
2396
2644
  }
2397
2645
 
2646
+ // Discord köprüsünü otomatik başlat (token kayıtlıysa)
2647
+ if (settings.dcToken) {
2648
+ ensureDc().start().catch((e) => dcLog('autostart failed: ' + String((e && e.message) || e)));
2649
+ }
2650
+
2398
2651
  app.on('activate', () => {
2399
2652
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
2400
2653
  });
@@ -2402,7 +2655,6 @@ app.whenReady().then(() => {
2402
2655
  app.on('before-quit', () => {
2403
2656
  app.isQuitting = true;
2404
2657
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
2405
- try { stopWarp(); } catch {} // warp-plus yardımcısını da kapat
2406
2658
  });
2407
2659
 
2408
2660
  if (process.argv.includes('--smoke')) {
@@ -2558,10 +2810,13 @@ function setBrowserOpen(v, forceVisible) {
2558
2810
  return;
2559
2811
  }
2560
2812
 
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)
2813
+ // AÇMA — görünürlük: forceVisible true/false ise onu uygula;
2814
+ // belirtilmemişse kullanıcı tercihi (settings.browserHeadless) belirler.
2815
+ // PARALEL AJANLAR (bg oturum) her zaman forceVisible=false ile çağırır →
2816
+ // tarayıcı gizli modda çalışır, kullanıcı ekranı ve ajan konsolu rahatsız edilmez.
2563
2817
  browser.open = true;
2564
- browser.visible = forceVisible === true ? true : settings.browserHeadless !== true;
2818
+ browser.visible =
2819
+ forceVisible === true ? true : forceVisible === false ? false : settings.browserHeadless !== true;
2565
2820
  ensureBrowser();
2566
2821
  if (!browser.started) {
2567
2822
  browser.started = true;
@@ -2590,14 +2845,23 @@ function browserGate(job) {
2590
2845
  __trafficTail = p.catch(() => {});
2591
2846
  return p;
2592
2847
  }
2593
- async function browserNavigate(raw, signal) {
2848
+ /* AJAN TARAYICI STRATEJİSİ — TÜM oturumlar (ana sohbet + WhatsApp botları + paralel ajanlar):
2849
+ ajan tarayıcıyı KENDİ açıyorsa hep GİZLİ açılır — panel ekrana fırlamaz,
2850
+ Paralel Ajan Konsolu kapanmaz. Kullanıcı izlemek isterse tarayıcı düğmesine
2851
+ basar (browser:toggle gizli paneli görünür kılar). Zaten açıksa (kullanıcı
2852
+ paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
2853
+ function setBrowserOpenForAgent() {
2854
+ if (!browser.open) setBrowserOpen(true, false);
2855
+ }
2856
+
2857
+ async function browserNavigate(raw, signal, ctx) {
2594
2858
  return browserGate(async () => {
2595
2859
  await browserTrafficWait();
2596
- return browserNavigateNow(raw, signal);
2860
+ return browserNavigateNow(raw, signal, ctx);
2597
2861
  });
2598
2862
  }
2599
2863
 
2600
- async function browserNavigateNow(raw, signal) {
2864
+ async function browserNavigateNow(raw, signal, ctx) {
2601
2865
  let url = String(raw || '').trim();
2602
2866
  if (!url) return { ok: false, error: 'boş adres' };
2603
2867
  if (!/^https?:\/\//i.test(url)) {
@@ -2606,7 +2870,7 @@ async function browserNavigateNow(raw, signal) {
2606
2870
  ? 'https://' + url
2607
2871
  : 'https://duckduckgo.com/?q=' + encodeURIComponent(url);
2608
2872
  }
2609
- setBrowserOpen(true);
2873
+ setBrowserOpenForAgent();
2610
2874
  const wc = browser.view.webContents;
2611
2875
  await new Promise((resolve) => {
2612
2876
  let settled = false;
@@ -2668,6 +2932,120 @@ function flushBrowserStorage() {
2668
2932
  } catch {}
2669
2933
  }
2670
2934
 
2935
+ /* ---------- GİZLİ ARAŞTIRMA TARAYICISI (deep_search) ----------
2936
+ deep_search'ün "sayfayı açıp oku" adımı burada çalışır: WebContentsView
2937
+ HİÇbir pencereye eklenmez (kullanıcı hiçbir şey görmez) ama gerçek Chromium
2938
+ çalışır — JS/SPA sayfalar render olur, innerText okunur. Panel (browser.view)
2939
+ hiç meşgul edilmez. Görsel/font/media indirme hız için iptal edilir.
2940
+ 2 slot = en fazla 2 sayfa aynı anda okunur (ayrı view, çakışma yok). */
2941
+ const researchPool = { views: [null, null], queue: [], slots: [false, false] };
2942
+ const RESEARCH_PAGE_TIMEOUT = 25000;
2943
+ const RESEARCH_SETTLE_MS = 900;
2944
+ const RESEARCH_CONTENT_CAP = 4500;
2945
+
2946
+ function researchAcquire() {
2947
+ return new Promise((resolve) => {
2948
+ researchPool.queue.push(resolve);
2949
+ researchPump();
2950
+ });
2951
+ }
2952
+
2953
+ function researchRelease(idx) {
2954
+ researchPool.slots[idx] = false;
2955
+ researchPump();
2956
+ }
2957
+
2958
+ function researchPump() {
2959
+ for (let i = 0; i < researchPool.slots.length; i++) {
2960
+ if (!researchPool.slots[i] && researchPool.queue.length) {
2961
+ researchPool.slots[i] = true;
2962
+ researchPool.queue.shift()(i);
2963
+ }
2964
+ }
2965
+ }
2966
+
2967
+ function researchViewAt(idx) {
2968
+ let v = researchPool.views[idx];
2969
+ if (v && v.webContents && !v.webContents.isDestroyed()) return v;
2970
+ v = new WebContentsView({
2971
+ webPreferences: {
2972
+ partition: 'research', // kalıcı olmayan bölüm — çerez birikmez
2973
+ contextIsolation: true,
2974
+ nodeIntegration: false,
2975
+ sandbox: true,
2976
+ spellcheck: false,
2977
+ backgroundThrottling: false, // gizli çalışırken zamanlayıcılar yavaşlamasın
2978
+ },
2979
+ });
2980
+ const wc = v.webContents;
2981
+ try {
2982
+ /* Google/bazı siteler Electron UA'yı reddeder — gerçek Chrome kimliği */
2983
+ const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome} Safari/537.36`;
2984
+ wc.setUserAgent(chromeUA);
2985
+ const ses = session.fromPartition('research');
2986
+ if (ses && ses.setUserAgent) ses.setUserAgent(chromeUA, 'tr-TR,tr;q=0.9,en;q=0.8');
2987
+ } catch {}
2988
+ try { wc.setWindowOpenHandler(() => ({ action: 'deny' })); } catch {}
2989
+ try {
2990
+ session.fromPartition('research').setPermissionRequestHandler((_w, _p, cb) => cb(false));
2991
+ } catch {}
2992
+ try {
2993
+ /* hız: araştırma okuması için görsel/font/media gereksiz — iptal et */
2994
+ session.fromPartition('research').webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, cb) => {
2995
+ cb({ cancel: ['image', 'media', 'font'].includes(details.resourceType) });
2996
+ });
2997
+ } catch {}
2998
+ try {
2999
+ session.fromPartition('research').on('will-download', (_e, item) => { try { item.cancel(); } catch {} });
3000
+ } catch {}
3001
+ wc.on('render-process-gone', () => { researchPool.views[idx] = null; });
3002
+ researchPool.views[idx] = v;
3003
+ return v;
3004
+ }
3005
+
3006
+ async function researchRead(rawUrl, signal) {
3007
+ const url = String(rawUrl || '').trim();
3008
+ if (!/^https?:\/\//i.test(url)) return { ok: false, url, error: 'geçersiz adres' };
3009
+ if (signal && signal.aborted) return { ok: false, url, error: 'iptal edildi' };
3010
+ const idx = await researchAcquire();
3011
+ try {
3012
+ const view = researchViewAt(idx);
3013
+ if (!view) return { ok: false, url, error: 'araştırma tarayıcısı oluşturulamadı' };
3014
+ const wc = view.webContents;
3015
+ const loaded = await new Promise((resolve) => {
3016
+ let settled = false;
3017
+ const finish = (v) => { if (!settled) { settled = true; clearTimeout(timer); wc.removeListener('did-finish-load', onDone); wc.removeListener('did-fail-load', onFail); resolve(v); } };
3018
+ const timer = setTimeout(() => finish(false), RESEARCH_PAGE_TIMEOUT);
3019
+ const onDone = () => finish(true);
3020
+ const onFail = (_e, code) => { if (code !== -3) finish(false); };
3021
+ wc.once('did-finish-load', onDone);
3022
+ wc.on('did-fail-load', onFail);
3023
+ wc.loadURL(url).catch(() => {});
3024
+ });
3025
+ /* SPA hidrasyonu için kısa settle; metin boşsa bir kez daha dene */
3026
+ await new Promise((r) => setTimeout(r, RESEARCH_SETTLE_MS));
3027
+ let title = '';
3028
+ let finalUrl = url;
3029
+ try { title = wc.getTitle() || ''; finalUrl = wc.getURL() || url; } catch {}
3030
+ const grab = async () => {
3031
+ try { return String((await wc.executeJavaScript('(document.body&&document.body.innerText)||""', true)) || ''); } catch { return ''; }
3032
+ };
3033
+ let text = (await grab()).replace(/\n{3,}/g, '\n\n').trim();
3034
+ if (!text) {
3035
+ await new Promise((r) => setTimeout(r, 1500));
3036
+ text = (await grab()).replace(/\n{3,}/g, '\n\n').trim();
3037
+ }
3038
+ if (!text) {
3039
+ 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)' };
3040
+ }
3041
+ return { ok: true, url: finalUrl, title, truncated: text.length > RESEARCH_CONTENT_CAP, content: text.slice(0, RESEARCH_CONTENT_CAP) };
3042
+ } catch (e) {
3043
+ return { ok: false, url, error: String((e && e.message) || e) };
3044
+ } finally {
3045
+ researchRelease(idx);
3046
+ }
3047
+ }
3048
+
2671
3049
  /* Dahili OCR: görsel desteklemeyen modeller için tesseract.js ile metin okuma.
2672
3050
  Dil verisi ilk kullanımda %APPDATA%\beast\tessdata'ya iner, sonra offline çalışır. */
2673
3051
  const _ocrWorkers = new Map(); // lang -> worker (her çağrıda yeniden init olmasın)
@@ -2705,21 +3083,21 @@ async function ocrRead({ image, lang = 'tur+eng' } = {}) {
2705
3083
  Numara: view'i google.com'a bir kez açıp aramaları SAYFA İÇİ fetch() ile
2706
3084
  yapmak (kullanıcının console'da yaptığı gibi) — sayfa bile değişmeden
2707
3085
  DOMParser ile sonuç çekilir. Fetch olmazsa direk gezinme fallback'i var. */
2708
- async function browserSearch(query, signal) {
3086
+ async function browserSearch(query, signal, ctx) {
2709
3087
  /* paralel ajan sorguları trafik kapısından sırayla geçer */
2710
3088
  return browserGate(async () => {
2711
3089
  await browserTrafficWait();
2712
- return browserSearchNow(query, signal);
3090
+ return browserSearchNow(query, signal, ctx);
2713
3091
  });
2714
3092
  }
2715
3093
 
2716
- async function browserSearchNow(query, signal) {
3094
+ async function browserSearchNow(query, signal, ctx) {
2717
3095
  try {
2718
3096
  if (!win || win.isDestroyed()) return null;
2719
3097
  if (signal && signal.aborted) return null;
2720
3098
  const q = String(query || '').trim();
2721
3099
  if (!q) return null;
2722
- setBrowserOpen(true);
3100
+ setBrowserOpenForAgent();
2723
3101
  const wc = browser.view && browser.view.webContents;
2724
3102
  if (!wc) return null;
2725
3103
 
@@ -3325,6 +3703,7 @@ ipcMain.handle('sessions:create', () => {
3325
3703
  } else {
3326
3704
  engine.setSessionTools(v.id, null);
3327
3705
  }
3706
+ engine.setSessionModel(v.id, b && !b.admin ? (b.model || null) : null);
3328
3707
  return v;
3329
3708
  });
3330
3709
  ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
@@ -3344,7 +3723,13 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
3344
3723
  if (!sess) {
3345
3724
  try { sess = engine._load(sid); } catch {}
3346
3725
  }
3347
- if (sess && !sess.botId) engine.setSessionBot(sid, actBot.id);
3726
+ if (sess && !sess.botId) {
3727
+ engine.setSessionBot(sid, actBot.id);
3728
+ /* tam bağlama: izin + araç seti + botun kendi modeli (sessions:create ile aynı) */
3729
+ engine.setSessionPerm(sid, actBot.perm || 'all');
3730
+ engine.setSessionTools(sid, botToolSet(actBot));
3731
+ engine.setSessionModel(sid, actBot.model || null);
3732
+ }
3348
3733
  }
3349
3734
  } catch {}
3350
3735
  if (t === '/stop' || t === '/start') {
@@ -3748,27 +4133,21 @@ function flushDesktopOnDone(ev) {
3748
4133
  - Bağlantı geri gelince (DNS kontrolü) sırayla otomatik gönderilir
3749
4134
  - Renderer'a 'net' / 'netQueue' olayları gider: ⏳ kuyruk balonu + toast */
3750
4135
  const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
4136
+ const NET_LOOKUP_HOSTS = ['www.google.com', 'www.microsoft.com'];
4137
+ const NET_HTTP_PROBES = [
4138
+ 'http://www.msftconnecttest.com/connecttest.txt',
4139
+ 'http://cp.cloudflare.com/generate_204',
4140
+ 'http://connectivitycheck.gstatic.com/generate_204',
4141
+ ];
3751
4142
  const NET_CHECK_MS = 8000;
3752
4143
  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
- }
4144
+ const NET_OFFLINE_STRIKES = 2; // üst üste bu kadar başarısız turda offline ilan edilir
3767
4145
  const CHAT_QUEUE_MAX = 50; // kuyruk üst sınırı — taşarsa en eski düşer
3768
4146
 
3769
4147
  let netOnline = true; // son bilinen bağlantı durumu (başlangıçta iyimser)
3770
4148
  let netCheckedOnce = false;
3771
4149
  let netCheckBusy = false;
4150
+ let netFailStreak = 0;
3772
4151
  let chatQueueFlushing = false;
3773
4152
  const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
3774
4153
 
@@ -3852,8 +4231,42 @@ async function flushChatQueue() {
3852
4231
  }
3853
4232
  }
3854
4233
 
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. */
4234
+ /* gerçek internet kontrolü TEK yöntem yanıltıcı olabilir:
4235
+ dns.resolve (c-ares) sistem çözümleyicisini atlar; mobil ağ/hotspot/VPN ve
4236
+ ISS DNS engellemelerinde (ör. 1.1.1.1, dns.google) internet VARken bile
4237
+ başarısız çıkar. Bu yüzden katmanlı deniyoruz; HERHANGİ bir katman başarılıysa
4238
+ internet VAR sayılır:
4239
+ 1) HTTP connectivity endpoint'leri (http modülü dns.lookup = OS çözümleyicisi
4240
+ kullanır — tarayıcı gibi; captive portal/proxy/mobil ağ hepsinde çalışır)
4241
+ 2) dns.lookup (Windows sistem çözümleyicisi — hosts dosyası/VPN/NRPT dahil)
4242
+ + dns.resolve (doğrudan DNS sunucusu)
4243
+ 3) OS'in kendi bağlantı durumu (Electron net.isOnline — Windows NCSI)
4244
+ Ayrıca tek başarısız tur offline ilan etmez (2 üst üste başarısız tur gerekir):
4245
+ geçici DNS gecikmesi mesajları gereksiz kuyruğa atmaz. */
4246
+ function httpProbe(url) {
4247
+ return new Promise((resolve) => {
4248
+ let done = false;
4249
+ const fin = (v) => { if (!done) { done = true; resolve(v); } };
4250
+ try {
4251
+ const req = http.get(url, { timeout: NET_CHECK_TIMEOUT }, (res) => {
4252
+ res.resume(); // gövdeyi tüket — soket serbest kalsın
4253
+ const ok = !!res.statusCode && res.statusCode < 500;
4254
+ try { res.destroy(); } catch {}
4255
+ fin(ok);
4256
+ });
4257
+ req.on('timeout', () => { try { req.destroy(); } catch {} fin(false); });
4258
+ req.on('error', () => fin(false));
4259
+ } catch { fin(false); }
4260
+ });
4261
+ }
4262
+
4263
+ function lookupProbe(host) {
4264
+ return new Promise((resolve) => {
4265
+ const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
4266
+ dns.lookup(host, (err) => { clearTimeout(t); resolve(!err); });
4267
+ });
4268
+ }
4269
+
3857
4270
  function dnsProbe(host) {
3858
4271
  return new Promise((resolve) => {
3859
4272
  const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
@@ -3868,37 +4281,37 @@ async function netCheck() {
3868
4281
  if (netCheckBusy) return;
3869
4282
  netCheckBusy = true;
3870
4283
  try {
3871
- let ok = false;
3872
- for (const h of NET_CHECK_HOSTS) {
3873
- if (await dnsProbe(h)) { ok = true; break; }
4284
+ let ok = (await Promise.all(NET_HTTP_PROBES.map(httpProbe))).some(Boolean);
4285
+ if (!ok) {
4286
+ const lookups = [
4287
+ ...NET_LOOKUP_HOSTS.map(lookupProbe),
4288
+ ...NET_CHECK_HOSTS.map(dnsProbe),
4289
+ ];
4290
+ ok = (await Promise.all(lookups)).some(Boolean);
3874
4291
  }
3875
4292
  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();
4293
+ try { ok = electronNet.isOnline() === true; } catch {}
3879
4294
  }
3880
4295
  const first = !netCheckedOnce;
3881
4296
  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; }
4297
+ netCheckedOnce = true;
4298
+ if (ok) {
4299
+ netFailStreak = 0;
4300
+ netOnline = true;
3887
4301
  } else {
3888
- __netFailStreak = 0;
4302
+ netFailStreak++;
4303
+ if (!was || netFailStreak >= NET_OFFLINE_STRIKES) netOnline = false;
3889
4304
  }
3890
- netOnline = ok;
3891
- netCheckedOnce = true;
3892
- if (was !== ok || first) {
4305
+ if (netOnline !== was || first) {
3893
4306
  try {
3894
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: ok });
4307
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: netOnline });
3895
4308
  } catch {}
3896
- if (ok) {
4309
+ if (netOnline) {
3897
4310
  log.info('main', 'bağlantı geri geldi — offline kuyruk kontrol ediliyor');
3898
4311
  chatQueueEmit(); // renderer: pill/toast güncellensin
3899
4312
  flushChatQueue().catch(() => {});
3900
4313
  } else {
3901
- log.info('main', 'internet bağlantısı yok — mesajlar kuyruğa alınacak');
4314
+ log.info('main', `internet bağlantısı yok — mesajlar kuyruğa alınacak (streak=${netFailStreak})`);
3902
4315
  chatQueueEmit();
3903
4316
  }
3904
4317
  }
@@ -3908,9 +4321,21 @@ async function netCheck() {
3908
4321
  }
3909
4322
 
3910
4323
  ipcMain.handle('model:set', (_e, sel) => {
3911
- settings.modelOverride = sel;
3912
- saveSettings();
3913
- engine.setModelOverride(sel);
4324
+ /* MÜŞTERİ botu aktifken picker seçimi O BOTUN modelini değiştirir;
4325
+ Beast (admin) aktifken global seçim değişir. */
4326
+ const act = settings.activeBotId ? bots.get(settings.activeBotId) : null;
4327
+ if (act && !act.admin) {
4328
+ try { bots.update(act.id, { model: String(sel || '') }); } catch {}
4329
+ try {
4330
+ for (const v of engine.listSessions()) {
4331
+ if (v.botId === act.id) engine.setSessionModel(v.id, sel || null);
4332
+ }
4333
+ } catch {}
4334
+ } else {
4335
+ settings.modelOverride = sel;
4336
+ saveSettings();
4337
+ engine.setModelOverride(sel);
4338
+ }
3914
4339
  return engine.publicState();
3915
4340
  });
3916
4341
 
@@ -4136,294 +4561,6 @@ ipcMain.handle('wa:groups:set', (_e, cfg) => {
4136
4561
  return settings.waGroups;
4137
4562
  });
4138
4563
 
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
4564
  ipcMain.handle('settings:get', () => {
4428
4565
  const out = JSON.parse(JSON.stringify(settings));
4429
4566
  /* Sırların renderera düz metin gitmesini engelle */
@@ -4546,7 +4683,7 @@ ipcMain.handle('update:install', () => {
4546
4683
  if (!isNpmMode()) return { ok: false, error: NPM_ONLY_TEXT };
4547
4684
  npmUpdateViaCmd();
4548
4685
  return { ok: true, npm: true };
4549
- });
4686
+ });
4550
4687
 
4551
4688
  ipcMain.handle('update:setAuto', (_e, cfg) => {
4552
4689
  if (cfg && typeof cfg.autoCheck === 'boolean') settings.autoCheckUpdate = cfg.autoCheck;
@@ -4781,7 +4918,12 @@ ipcMain.handle('cron:runNow', (_e, id) => {
4781
4918
 
4782
4919
  /* ---------------- tarayıcı IPC ---------------- */
4783
4920
  ipcMain.handle('browser:toggle', () => {
4784
- /* kullanıcı kendi açıyor mutlaka görünür */
4921
+ /* gizli çalışan ajan paneli varsa düğme ONU GÖRÜNÜR yapar; değilse aç/kapa.
4922
+ (ajanlar tarayıcıyı hep gizli açar — kullanıcı izlemek isterse buradan gösterir) */
4923
+ if (browser.open && !browser.visible) {
4924
+ setBrowserOpen(true, true);
4925
+ return { open: browser.open, visible: browser.visible };
4926
+ }
4785
4927
  setBrowserOpen(!browser.open, true);
4786
4928
  return { open: browser.open, visible: browser.visible };
4787
4929
  });
@@ -5438,12 +5580,15 @@ function runNpmGlobalInstall() {
5438
5580
  });
5439
5581
  }
5440
5582
 
5441
- /* auth login etkileşimlidir (tarayıcı açar) — kullanıcı görsün diye ayrı pencere */
5583
+ /* auth login etkileşimlidir (tarayıcı açar) — kullanıcı görsün diye ayrı pencere.
5584
+ -ExecutionPolicy Bypass: SADECE bu pencere için — Windows varsayılan "Restricted"
5585
+ politikası npm'in opencode.ps1 shim'ini bloklar ("running scripts is disabled")
5586
+ → taze makinede tek tık kurulum patlamasın; sistem politikası DEĞİŞMEZ. */
5442
5587
  function openZenAuthWindow() {
5443
5588
  try {
5444
5589
  spawn(
5445
5590
  'cmd.exe',
5446
- ['/c', 'start', '', 'powershell.exe', '-NoExit', '-Command', 'opencode auth login'],
5591
+ ['/c', 'start', '', 'powershell.exe', '-NoExit', '-ExecutionPolicy', 'Bypass', '-Command', 'opencode auth login'],
5447
5592
  { detached: true, windowsHide: false, stdio: 'ignore' }
5448
5593
  ).unref();
5449
5594
  return true;
@@ -5668,6 +5813,42 @@ ipcMain.handle('tg:allow:set', (_e, list) => {
5668
5813
  });
5669
5814
  ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
5670
5815
 
5816
+ /* ---------- Discord IPC ---------- */
5817
+ ipcMain.handle('dc:status:get', () => {
5818
+ if (!dc) return { configured: !!settings.dcToken, status: 'disconnected', user: null, connected: false };
5819
+ return { configured: true, ...dc.snapshot() };
5820
+ });
5821
+
5822
+ /* token kaydet + köprüyü (yeniden) başlat */
5823
+ ipcMain.handle('dc:set', async (_e, token) => {
5824
+ const t = String(token || '').trim();
5825
+ if (t) settings.dcToken = t;
5826
+ saveSettings();
5827
+ await restartDc();
5828
+ return { configured: !!settings.dcToken, ...(dc ? dc.snapshot() : { status: 'disconnected', user: null }) };
5829
+ });
5830
+
5831
+ ipcMain.handle('dc:start', async () => {
5832
+ if (!settings.dcToken) return { ok: false, error: 'token yok — önce bot tokenı gir' };
5833
+ await restartDc();
5834
+ return { ok: true, ...(dc ? dc.snapshot() : {}) };
5835
+ });
5836
+
5837
+ ipcMain.handle('dc:stop', async () => {
5838
+ if (dc) {
5839
+ try { await dc.stop(); } catch {}
5840
+ }
5841
+ return { ok: true };
5842
+ });
5843
+
5844
+ ipcMain.handle('dc:allow:get', () => settings.dcAllow || []);
5845
+ ipcMain.handle('dc:allow:set', (_e, list) => {
5846
+ settings.dcAllow = Array.isArray(list) ? list : [];
5847
+ saveSettings();
5848
+ return settings.dcAllow;
5849
+ });
5850
+ ipcMain.handle('dc:sessions', () => [...dcChats.values()]);
5851
+
5671
5852
  /* ---------- e-posta IPC ---------- */
5672
5853
 
5673
5854
  ipcMain.handle('email:get', () => {
@@ -5929,6 +6110,7 @@ ipcMain.handle('bots:update', (_e, { id, patch }) => {
5929
6110
  if ((v.botId || 'beast') === String(id || '')) {
5930
6111
  const cfg = bots.get(String(id));
5931
6112
  engine.setSessionTools(v.id, cfg && !cfg.admin ? botToolSet(cfg) : null);
6113
+ engine.setSessionModel(v.id, cfg && !cfg.admin ? (cfg.model || null) : null);
5932
6114
  }
5933
6115
  }
5934
6116
  } catch {}