beast-agent 0.25.2 → 0.25.4

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.
@@ -82,27 +82,44 @@ if (process.argv[2] === 'update') {
82
82
  process.exit(1);
83
83
  }
84
84
  console.log('\n\u2713 beast-agent g\u00FCncellendi \u2014 uygulama ba\u015Flat\u0131l\u0131yor\u2026');
85
- try {
86
- const electron = require('electron');
87
- if (typeof electron === 'string') {
88
- spawn(electron, [path.resolve(__dirname, '..')], { stdio: 'ignore', detached: true }).unref();
89
- }
90
- } catch {}
85
+ launchDetached([]);
91
86
  if (isWin) spawnSync('cmd.exe', ['/c', 'timeout /t 3'], { stdio: 'ignore' });
92
87
  process.exit(0);
93
88
  }
94
89
 
95
- const electron = require('electron');
96
- if (typeof electron !== 'string') {
97
- /* electron runtime içindeyiz bu script için anlamsız */
98
- process.exit(1);
90
+ /* detached başlatıcı + kendini onaran electron:
91
+ taze makinalarda npm postinstall sırasında electron binary indirmesi
92
+ sessizce başarısız olmuş olabilir burada otomatik tamir edilir. */
93
+ function launchDetached(extraArgs) {
94
+ let electron = null;
95
+ try { electron = require('electron'); } catch {}
96
+ if (typeof electron !== 'string') {
97
+ let fix = null;
98
+ try { fix = require('../scripts/fix-electron'); } catch {}
99
+ if (fix) {
100
+ console.log('\u27F3 electron \u00e7al\u0131\u015Fma dosyalar\u0131 eksik bulundu \u2014 otomatik onar\u0131l\u0131yor\u2026');
101
+ const r = fix.repair({ quiet: false });
102
+ if (r.ok && !r.skipped) console.log('\u2713 electron onar\u0131ld\u0131');
103
+ if (r.ok) {
104
+ try { electron = require('electron'); } catch {}
105
+ }
106
+ }
107
+ }
108
+ if (typeof electron !== 'string') {
109
+ console.log('\n\u2717 Electron \u00e7al\u0131\u015Fma dosyas\u0131 kurulamad\u0131.');
110
+ console.log(' Elle \u00e7\u00f6z\u00fcm \u2014 \u015fu 2 komutu \u00e7al\u0131\u015Ft\u0131r:');
111
+ console.log(' npm config set ignore-scripts false');
112
+ console.log(' npm install -g beast-agent');
113
+ return false;
114
+ }
115
+ const child = spawn(electron, [path.resolve(__dirname, '..'), ...extraArgs], {
116
+ stdio: 'ignore',
117
+ detached: true,
118
+ /* windowsHide KULLANMA: Chromium ilk pencereyi gizli başlatıyor (tray-only bug) */
119
+ });
120
+ child.unref();
121
+ return true;
99
122
  }
100
123
 
101
- const appPath = path.resolve(__dirname, '..');
102
- const child = spawn(electron, [appPath, ...process.argv.slice(2)], {
103
- stdio: 'ignore',
104
- detached: true,
105
- /* windowsHide KULLANMA: Chromium ilk pencereyi gizli başlatıyor (tray-only bug) */
106
- });
107
- child.unref();
124
+ if (!launchDetached(process.argv.slice(2))) process.exit(1);
108
125
  process.exit(0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "0.25.2",
4
+ "version": "0.25.4",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -33,6 +33,7 @@
33
33
  "dist": "electron-builder --win nsis portable",
34
34
  "test": "node --test \"tests/**/*.test.js\"",
35
35
  "release": "node scripts/release.js",
36
+ "postinstall": "node scripts/fix-electron.js --soft",
36
37
  "prepublishOnly": "node scripts/swap-electron.js deps",
37
38
  "postpublish": "node scripts/swap-electron.js devdeps"
38
39
  },
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /* BEAST — electron binary onarıcı.
4
+ SORUN: `electron` npm paketi binary'yi (dist/electron.exe) postinstall'da
5
+ GitHub'dan indirir. Taze makinalarda bu indirme SESSİZCE başarısız olabilir
6
+ (proxy/GitHub engeli, npm ignore-scripts=true, ağ kesintisi) — paket kurulmuş
7
+ görünür ama `require('electron')` "Electron failed to install correctly" fırlatır.
8
+ ÇÖZÜM: install.js'i elle çalıştır; olmazsa yedek aynadan (npmmirror) dene.
9
+ Kullanım yerleri:
10
+ - package.json postinstall → kurulumda otomatik (npm i -g beast-agent)
11
+ - bin/beast-agent.js → her başlatmada bozuksa otomatik onar */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { spawnSync } = require('child_process');
16
+
17
+ /* electron paket dizini (index.js'in yeri); paket hiç yoksa null */
18
+ function electronDir() {
19
+ try {
20
+ return path.dirname(require.resolve('electron'));
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /* binary sağlam mı? electron/index.js'in beklentisiyle aynı kontrol:
27
+ path.txt + dist/<exe> varlığı */
28
+ function status() {
29
+ const dir = electronDir();
30
+ if (!dir) return { ok: false, missing: true };
31
+ const exe = process.platform === 'win32' ? 'electron.exe' : 'electron';
32
+ const distOk = fs.existsSync(path.join(dir, 'dist', exe));
33
+ const txtOk = fs.existsSync(path.join(dir, 'path.txt'));
34
+ return distOk && txtOk ? { ok: true, dir } : { ok: false, dir };
35
+ }
36
+
37
+ /* platforma göre dist içindeki çalıştırılabilirin göreli yolu (electron/index.js ile aynı) */
38
+ function platformPath() {
39
+ switch (process.platform) {
40
+ case 'win32':
41
+ return 'electron.exe';
42
+ case 'darwin':
43
+ case 'mas':
44
+ return 'Electron.app/Contents/MacOS/Electron';
45
+ default:
46
+ return 'electron';
47
+ }
48
+ }
49
+
50
+ /* onarım — 3 katman:
51
+ 0) binary sağlam, yalnız path.txt eksik/bozuk → indirimsiz anında yazar
52
+ (install.js bu durumu "zaten kurulu" sanıp atlayabiliyor)
53
+ 1) dist bozuk/eksik → dist'i sil + install.js (GitHub) ile taze indir
54
+ 2) GitHub engelli → ELECTRON_MIRROR=npmmirror ile tekrar */
55
+ function repair({ quiet } = {}) {
56
+ let st = status();
57
+ if (st.ok) return { ok: true, dir: st.dir, skipped: true };
58
+ if (st.missing) return { ok: false, error: 'electron paketi hi\u00e7 yok — npm install -g beast-agent' };
59
+ const dir = st.dir;
60
+ const exeRel = platformPath();
61
+ const exeAbs = path.join(dir, 'dist', ...exeRel.split('/'));
62
+ const verFile = path.join(dir, 'dist', 'version');
63
+ let wantVer = null;
64
+ try { wantVer = String(require(path.join(dir, 'package.json')).version || ''); } catch {}
65
+ const distIntact =
66
+ fs.existsSync(exeAbs) &&
67
+ (!wantVer ||
68
+ (fs.existsSync(verFile) &&
69
+ fs.readFileSync(verFile, 'utf8').replace(/^v/, '').trim() === wantVer));
70
+
71
+ if (distIntact) {
72
+ /* KATMAN 0: hızlı onarım — path.txt'yi kendimiz yazıyoruz (indirim yok) */
73
+ try { fs.writeFileSync(path.join(dir, 'path.txt'), exeRel, 'utf8'); } catch {}
74
+ if (status().ok) return { ok: true, dir, fast: true };
75
+ }
76
+
77
+ /* KATMAN 1-2: tam indirim */
78
+ try { fs.rmSync(path.join(dir, 'dist'), { recursive: true, force: true }); } catch {}
79
+ try { fs.unlinkSync(path.join(dir, 'path.txt')); } catch {}
80
+ const installJs = path.join(dir, 'install.js');
81
+ if (!fs.existsSync(installJs)) {
82
+ return { ok: false, error: 'install.js yok — yeniden kur: npm install -g beast-agent' };
83
+ }
84
+
85
+ const sources = [null, 'https://npmmirror.com/mirrors/electron/'];
86
+ for (let i = 0; i < sources.length; i++) {
87
+ const env = { ...process.env };
88
+ if (sources[i]) {
89
+ env.ELECTRON_MIRROR = sources[i];
90
+ if (!quiet) console.log(' \u2022 varsay\u0131lan kaynak ba\u015Far\u0131s\u0131z \u2014 yedek ayna deneniyor (npmmirror)\u2026');
91
+ } else if (!quiet) {
92
+ console.log(' \u2022 electron binary indiriliyor (ilk sefer ~110 MB, 1-2 dk)\u2026');
93
+ }
94
+ const r = spawnSync(process.execPath, [installJs], {
95
+ cwd: dir,
96
+ env,
97
+ stdio: quiet ? 'ignore' : 'inherit',
98
+ timeout: 600000,
99
+ });
100
+ if (!r.error && r.status === 0 && status().ok) {
101
+ return { ok: true, dir, mirror: sources[i] || 'github' };
102
+ }
103
+ if (!quiet && r.error) {
104
+ console.log(' ! deneme ba\u015far\u0131s\u0131z: ' + String(r.error.message || r.error).slice(0, 160));
105
+ }
106
+ }
107
+ return { ok: false, error: 'electron binary indirilemedi (a\u011f/proxy engeli olabilir)' };
108
+ }
109
+
110
+ module.exports = { electronDir, status, repair };
111
+
112
+ /* CLI: package.json postinstall → `node scripts/fix-electron.js --soft`
113
+ (her durumda exit 0 — npm install bozulmasın, başlatma anında tekrar denenir)
114
+ elle çağrı → hata durumunda exit 1 */
115
+ if (require.main === module) {
116
+ const soft = process.argv.includes('--soft');
117
+ const r = repair({ quiet: false });
118
+ if (r.ok) {
119
+ console.log(r.skipped ? '\u2713 electron binary tamam' : '\u2713 electron binary onar\u0131ld\u0131');
120
+ process.exit(0);
121
+ }
122
+ console.error('\u2717 electron onar\u0131lamad\u0131: ' + (r.error || '?'));
123
+ console.error(' elle \u00e7\u00f6z\u00fcm: npm config set ignore-scripts false');
124
+ console.error(' npm install -g beast-agent');
125
+ process.exit(soft ? 0 : 1);
126
+ }
@@ -69,13 +69,13 @@ const FORMAT_RULES =
69
69
  const PERM_TOOL_SETS = {
70
70
  all: null, // tüm araçlar (filtre yok)
71
71
  web: new Set([
72
- 'web_search', 'http_fetch',
72
+ 'web_search', 'http_fetch', 'deep_search',
73
73
  'browser_open', 'browser_read', 'browser_snapshot', 'browser_screenshot',
74
74
  'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
75
75
  'ocr_read',
76
76
  ]),
77
77
  read: new Set([
78
- 'web_search', 'http_fetch',
78
+ 'web_search', 'http_fetch', 'deep_search',
79
79
  'browser_open', 'browser_read', 'browser_snapshot',
80
80
  'list_dir', 'read_file',
81
81
  ]),
@@ -99,7 +99,7 @@ function normalizePerms(p) {
99
99
  const CEO_EXEC_TOOLS = new Set([
100
100
  'run_command', 'read_file', 'write_file', 'list_dir',
101
101
  'python_run',
102
- 'web_search', 'http_fetch',
102
+ 'web_search', 'http_fetch', 'deep_search',
103
103
  'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
104
104
  'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
105
105
  'computer_look', 'computer_act',
@@ -1290,6 +1290,7 @@ class Engine {
1290
1290
  ? 'Bağımsız alt-işleri run_background ile PARALEL ajana devret; işi KENDİN YÜRÜTME — emri ver, takip et, raporla.'
1291
1291
  : 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
1292
1292
  'Güncel/dış bilgi gerekiyorsa web_search kullan (zincir DAHİLİ tarayıcıyla başlar — gerçek Chromium ile Google); hızlı ham metin okuması için http_fetch kullan.',
1293
+ 'DERİN ARAŞTIRMA: web_search\u2019in sonucu yetersizse/istenen bilgi listede YOKSA aramayı tekrar tekrar deneme yerine deep_search kullan — 1-4 sorgu varyantını (eş anlamlı, Türkçe+İngilizce yazımlar) PARALEL aratır ve ilk sayfaları GİZLİ gerçek Chromium\u2019da açıp tam metin okur (paneli açmaz, kullanıcıyı rahatsız etmez; JS/SPA sayfalar çalışır). Fiyat karşılaştırma, çok kaynaklı araştırma, "her şeyi bul" işleri ve Türkçe sorgularda sonuç zayıfsa doğrudan deep_search seç. read_top=0 verirsen yalnız harmanlanmış sonuç listesi döner.',
1293
1294
  'ARAMA-DİSİPLİN: bir bilgiyi 2-3 denemede bulamazsan TAKILMA — farklı bir açıya/sorguya geç, yine yoksa bulabildiğin kısmi bilgiyle cevap ver ve neyi bulamadığını açıkça söyle. Kapalı/gizli içerik (private profil, login arkası veri) peşinde KOŞMA — bulunamayacağı belliyse hemen vazgeç.',
1294
1295
  'TARAYICI VARSAYILANI (ZORUNLU): kullanıcı "şu siteyi aç", "bunu ara / google\u2019da ara", "şu sayfaya git" gibi bir web isteği verdiğinde HEP DAHİLİ TARAYICIYI KULLAN (browser_open ile aç → açılış yanıtında hazır snapshot gelir → ref numaralarıyla browser_click/browser_type/browser_select ile hareket et; HER eylem yanıtında taze snapshot döner — ayrıca browser_snapshot çağırma, yanıttaki refleri kullan; browser_read ile metin oku; sadece görsel yerleşim/grafik gerekiyorsa browser_screenshot çek). Tarih/saat alanlarını (type=date/time) browser_type ile DÜZ METİN yaz ("15.03.2026", "2026-03-15") — takvimden tıklamaya çalışma, alan programatik ayarlanır. Panel ekranın sağında açılır ve kullanıcı da sayfayı anında görür; JS/login/SPA/dinamik içerik için idealdir. Bu kural TÜM oturumları kapsar — masaüstü sohbeti ve çok kullanıcılı botlar (WhatsApp vb.) dahil.' +
1295
1296
  ' DIŞ TARAYICI İSTİSNASI: kullanıcı AÇIKÇA "chrome\u2019da aç", "firefox\u2019ta aç", "başka tarayıcıda aç", "normal tarayıcıda aç", "kendi tarayıcımda aç" derse O ZAMAN run_command ile `start "" <url>` çalıştır — sistem varsayılan (dış) tarayıcısında açılır. Kullanıcı dış tarayıcı istemedikçe ASLA dış tarayıcı açma.' +
@@ -2113,7 +2114,7 @@ class Engine {
2113
2114
  `Ortam: Windows + PowerShell; çalışma klasörü: ${this.workspace}\n` +
2114
2115
  `HIZ KURALLARI:\n` +
2115
2116
  `- Döngülü işleri (çok URL/dosya/sayfa, tekrarlı parse-hesap) TEK python_run betiğinde topluca bitir.\n` +
2116
- `- Web için web_search kullan (zincir dahili tarayıcıyla başlar — gerçek Chromium ile Google); sayfa açma/göstermenin VARSAYILANI DAHİLİ tarayıcıdır: browser_open → browser_snapshot → browser_click/type/read. Kullanıcı açıkça dış tarayıcı (chrome/firefox/başka/normal/kendi tarayıcım) istediyse run_command ile \`start "" <url>\` çalıştır. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
2117
+ `- Web için web_search kullan (zincir dahili tarayıcıyla başlar — gerçek Chromium ile Google); tek aramada bulunamazsa veya çok kaynaklı derin araştırma gerekiyorsa deep_search kullan (çoklu sorgu paralel + gizli tarayıcıda tam sayfa okuma). Sayfa açma/göstermenin VARSAYILANI DAHİLİ tarayıcıdır: browser_open → browser_snapshot → browser_click/type/read. Kullanıcı açıkça dış tarayıcı (chrome/firefox/başka/normal/kendi tarayıcım) istediyse run_command ile \`start "" <url>\` çalıştır. Görseli göremiyorsan metni ocr_read ile oku (source:"browser").\n` +
2117
2118
  `- Bağımsız araç çağrılarını aynı turda PARALEL ver.\n` +
2118
2119
  `- ARAŞTIRMA SINIRI: 3-5 kaynak yeter; süre hedefi ~3 dakika. 2-3 denemede bulunamayan bilgiyi BIRAK — bulabildiğin kısmi sonucu raporla ve neyi bulamadığını yaz. Kapalı/gizli içerik peşinde koşma.\n` +
2119
2120
  FORMAT_RULES + '\n' +
@@ -2721,7 +2722,7 @@ class Engine {
2721
2722
 
2722
2723
  /* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
2723
2724
  Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
2724
- async _ocrRead(args, signal) {
2725
+ async _ocrRead(args, signal, sessionId) {
2725
2726
  if (!this.ocr) return { ok: false, error: 'OCR kullanılamıyor' };
2726
2727
  const src = String((args && args.source) || 'browser');
2727
2728
  let image = null;
@@ -2732,7 +2733,7 @@ class Engine {
2732
2733
  if (!this.browser || typeof this.browser.screenshot !== 'function') {
2733
2734
  return { ok: false, error: 'dahili tarayıcı kullanılamıyor' };
2734
2735
  }
2735
- const shot = await this.browser.screenshot(signal);
2736
+ const shot = await this.browser.screenshot(signal, { sessionId });
2736
2737
  if (!shot || !shot.ok) return { ok: false, error: (shot && shot.error) || 'görüntü alınamadı' };
2737
2738
  image = shot.__injectImage || shot.image || null;
2738
2739
  } else if (src === 'screen') {
@@ -3067,6 +3068,17 @@ class Engine {
3067
3068
  const r = await this.computer.act(op, args || {});
3068
3069
  return JSON.stringify(r);
3069
3070
  }
3071
+ if (name === 'deep_search') {
3072
+ /* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
3073
+ Hook (main process) yoksa tools.exec'teki arama-tek yol devreye girer. */
3074
+ try {
3075
+ emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
3076
+ } catch {}
3077
+ const run = this.research && typeof this.research.deepSearch === 'function'
3078
+ ? this.research.deepSearch(args || {}, signal)
3079
+ : tools.exec('deep_search', args || {}, { cwd: this._sessionWorkspace(sessionId), signal });
3080
+ return JSON.stringify(await run);
3081
+ }
3070
3082
  if (name === 'web_search') {
3071
3083
  /* SIRA: 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
3072
3084
  2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
@@ -3081,7 +3093,7 @@ class Engine {
3081
3093
  const browserUsable = this.browser && typeof this.browser.search === 'function';
3082
3094
  if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
3083
3095
  if (browserUsable && !banned) {
3084
- try { r = await this.browser.search(q, signal); } catch {}
3096
+ try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
3085
3097
  if (!r || !r.ok || !(r.results || []).length) {
3086
3098
  /* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
3087
3099
  this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
@@ -3102,41 +3114,41 @@ class Engine {
3102
3114
  return JSON.stringify(out);
3103
3115
  }
3104
3116
  if (name === 'ocr_read') {
3105
- return JSON.stringify(await this._ocrRead(args || {}, signal));
3117
+ return JSON.stringify(await this._ocrRead(args || {}, signal, sessionId));
3106
3118
  }
3107
3119
  if (name === 'browser_open') {
3108
3120
  if (!this.browser || typeof this.browser.openUrl !== 'function') {
3109
3121
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3110
3122
  }
3111
- const r = await this.browser.openUrl(String(args.url || ''), signal);
3123
+ const r = await this.browser.openUrl(String(args.url || ''), signal, { sessionId });
3112
3124
  return JSON.stringify(r);
3113
3125
  }
3114
3126
  if (name === 'browser_read') {
3115
3127
  if (!this.browser || typeof this.browser.readText !== 'function') {
3116
3128
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3117
3129
  }
3118
- const r = await this.browser.readText(signal);
3130
+ const r = await this.browser.readText(signal, { sessionId });
3119
3131
  return JSON.stringify(r);
3120
3132
  }
3121
3133
  if (name === 'browser_screenshot') {
3122
3134
  if (!this.browser || typeof this.browser.screenshot !== 'function') {
3123
3135
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3124
3136
  }
3125
- const r = await this.browser.screenshot(signal);
3137
+ const r = await this.browser.screenshot(signal, { sessionId });
3126
3138
  return JSON.stringify(r);
3127
3139
  }
3128
3140
  if (name === 'browser_snapshot') {
3129
3141
  if (!this.browser || typeof this.browser.snapshot !== 'function') {
3130
3142
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3131
3143
  }
3132
- const r = await this.browser.snapshot(signal);
3144
+ const r = await this.browser.snapshot(signal, { sessionId });
3133
3145
  return JSON.stringify(r);
3134
3146
  }
3135
3147
  if (name === 'browser_click' || name === 'browser_type' || name === 'browser_press' || name === 'browser_scroll' || name === 'browser_select') {
3136
3148
  if (!this.browser || typeof this.browser.act !== 'function') {
3137
3149
  return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
3138
3150
  }
3139
- const r = await this.browser.act(name.slice(8), args, signal);
3151
+ const r = await this.browser.act(name.slice(8), args, signal, { sessionId });
3140
3152
  return JSON.stringify(r);
3141
3153
  }
3142
3154
  if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ /* BEAST — deep_search: agentic araştırma katmanı.
4
+ web_search snippet'te kalır; bu katman derine iner:
5
+ 1) 1-4 sorgu varyantını PARALEL aratır (toplam süre ≈ tek arama)
6
+ 2) Sonuçları URL bazlı tekilleştirip round-robin harmanlar (kaynak çeşitliliği)
7
+ 3) İlk N sonucu GİZLİ gerçek tarayıcıda açıp tam metin okur
8
+ (deps.readPage verilmişse; yoksa yalnız sonuç listesi döner)
9
+ Saf modüldür: search/readPage enjekte edilir — test edilebilir. */
10
+
11
+ /* İçerik okumada anlamsız/ikili dosyalar — tarayıcıda açmanın anlamı yok */
12
+ const SKIP_FILE_RE = /\.(pdf|docx?|xlsx?|pptx?|zip|rar|7z|exe|dmg|apk|mp[34]|m4a|avi|mkv|webm|jpe?g|png|gif|webp|svg|ico|csv)([?#]|$)/i;
13
+
14
+ /* URL anahtarı: scheme/www/slash/hash farklarını yok sayar — tekilleştirme için */
15
+ function normKey(u) {
16
+ try {
17
+ const x = new URL(u);
18
+ x.hash = '';
19
+ let s = x.toString();
20
+ s = s.replace(/^http:\/\//i, 'https://');
21
+ s = s.replace(/^https:\/\/www\./i, 'https://');
22
+ return s.replace(/\/+$/, '').toLowerCase();
23
+ } catch {
24
+ return String(u || '').toLowerCase();
25
+ }
26
+ }
27
+
28
+ /* search() geri dönüşünü normalize eder: dizi YA DA {results:[...]} kabul;
29
+ satır anahtarları title/url/snippet VEYA name/href/link/body/description olabilir */
30
+ function normalizeRows(out) {
31
+ if (!out) return [];
32
+ const rows = Array.isArray(out) ? out : Array.isArray(out.results) ? out.results : [];
33
+ return rows
34
+ .map((r) => ({
35
+ title: String((r && (r.title || r.name)) || '').replace(/\s+/g, ' ').trim(),
36
+ url: String((r && (r.url || r.href || r.link)) || '').trim(),
37
+ snippet: String((r && (r.snippet || r.body || r.description)) || '').replace(/\s+/g, ' ').trim(),
38
+ }))
39
+ .filter((r) => r.url && /^https?:\/\//i.test(r.url) && r.title);
40
+ }
41
+
42
+ /* Liste harmanı: her listenin i. elemanı sırayla alınır (round-robin) —
43
+ tek motorun sonuçları öne çökmez, kaynak çeşitliliği korunur */
44
+ function mergeResults(lists, limit) {
45
+ const seen = new Set();
46
+ const out = [];
47
+ const max = lists.reduce((m, l) => Math.max(m, l.length), 0);
48
+ for (let i = 0; i < max && out.length < limit; i++) {
49
+ for (const list of lists) {
50
+ if (out.length >= limit) break;
51
+ const r = list[i];
52
+ if (!r) continue;
53
+ const key = normKey(r.url);
54
+ if (seen.has(key)) continue;
55
+ seen.add(key);
56
+ out.push(r);
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /* Eşzamanlılığı sınırlı paralel map — hata tek elemanı bozar, diziyi bozmaz */
63
+ function mapLimit(items, n, fn) {
64
+ const out = new Array(items.length);
65
+ let i = 0;
66
+ const worker = async () => {
67
+ while (i < items.length) {
68
+ const idx = i++;
69
+ try {
70
+ out[idx] = await fn(items[idx], idx);
71
+ } catch (e) {
72
+ out[idx] = { ok: false, url: items[idx] && items[idx].url, error: String((e && e.message) || e) };
73
+ }
74
+ }
75
+ };
76
+ const workers = Array.from({ length: Math.max(1, Math.min(n, items.length)) }, worker);
77
+ return Promise.all(workers).then(() => out);
78
+ }
79
+
80
+ async function deepSearch(args, deps, signal) {
81
+ const a = args || {};
82
+ let queries = a.queries;
83
+ if (typeof queries === 'string') queries = [queries];
84
+ if (!Array.isArray(queries) || !queries.length) {
85
+ const q = String(a.query || '').trim();
86
+ if (q) queries = [q];
87
+ }
88
+ queries = [...new Set((queries || []).map((s) => String(s || '').trim()).filter(Boolean))].slice(0, 4);
89
+ if (!queries.length) return { ok: false, error: 'sorgu yok — queries: ["...", "..."] ver' };
90
+ if (signal && signal.aborted) return { ok: false, error: 'iptal edildi', queries };
91
+
92
+ const maxResults = Math.max(5, Math.min(25, Number(a.max_results) || 16));
93
+ const readTop = Math.max(0, Math.min(6, Number(a.read_top != null ? a.read_top : (a.readTop != null ? a.readTop : 3))));
94
+ const search = deps && typeof deps.search === 'function' ? deps.search : null;
95
+ if (!search) return { ok: false, error: 'arama altyapısı yok', queries };
96
+
97
+ /* 1) paralel arama — bir sorgu çökse diğerleri devam eder */
98
+ const lists = await Promise.all(
99
+ queries.map((q) =>
100
+ Promise.resolve()
101
+ .then(() => search(q))
102
+ .then(normalizeRows)
103
+ .catch(() => [])
104
+ )
105
+ );
106
+ const results = mergeResults(lists, maxResults);
107
+
108
+ /* 2) ilk N sonucu gizli tarayıcıda açıp oku (ikili dosyaları atla) */
109
+ let pages = [];
110
+ if (readTop && results.length && deps && typeof deps.readPage === 'function') {
111
+ const targets = results.filter((r) => !SKIP_FILE_RE.test(r.url)).slice(0, readTop);
112
+ if (targets.length) {
113
+ pages = (await mapLimit(targets, 2, (r) => deps.readPage(r.url))).filter(Boolean);
114
+ }
115
+ }
116
+ const anyPage = pages.some((p) => p && p.ok);
117
+
118
+ return {
119
+ ok: !!(results.length || anyPage),
120
+ queries,
121
+ results,
122
+ ...(pages.length ? { pages } : {}),
123
+ ...(results.length || anyPage
124
+ ? {
125
+ note: anyPage
126
+ ? 'sayfalar gizli tarayıcıda açılıp okundu — excerpt\u2019ler pages içinde; bir sayfayı kullanıcıya da göstermek istersen browser_open ile panelde aç'
127
+ : 'sonuç listesi hazır — tam metin okuma kapalı/başarısız; URL\u2019leri http_fetch ile okuyabilirsin',
128
+ }
129
+ : { error: 'tüm motorlar boş döndü — sorguları farklı ifade etmeyi dene' }),
130
+ };
131
+ }
132
+
133
+ module.exports = { deepSearch, mergeResults, normalizeRows, normKey, mapLimit, SKIP_FILE_RE };
@@ -6,6 +6,7 @@ const path = require('path');
6
6
  const https = require('https');
7
7
  const { execFile } = require('child_process');
8
8
  const { spawn } = require('child_process');
9
+ const research = require('./research');
9
10
 
10
11
  const MAX_CMD_OUTPUT = 16000;
11
12
  const MAX_FILE_CHARS = 200000;
@@ -647,6 +648,28 @@ const definitions = [
647
648
  },
648
649
  },
649
650
  },
651
+ {
652
+ type: 'function',
653
+ function: {
654
+ name: 'deep_search',
655
+ description:
656
+ 'AGENTIC DEEP RESEARCH — use when web_search is not enough or the answer was NOT found on the first try. Runs 1-4 query variants IN PARALLEL (rephrase, synonyms, Turkish + English spellings of the same question), merges + dedupes results, then AUTOMATICALLY opens and reads the top pages with a HIDDEN real-Chromium browser (JS/SPA pages work; the visible panel is NOT touched) and returns full-text excerpts. Ideal for: multi-angle questions (price comparison, reviews, specs, "find everything about X"), Turkish queries that miss results, pages that need JS rendering. Returns {queries, results[{title,url,snippet}], pages[{url,title,content}]}.',
657
+ parameters: {
658
+ type: 'object',
659
+ properties: {
660
+ queries: {
661
+ type: 'array',
662
+ maxItems: 4,
663
+ items: { type: 'string' },
664
+ description: '1-4 query variants; e.g. ["iphone 16 fiyat", "iphone 16 price turkey", "iphone 16 technosa"]',
665
+ },
666
+ max_results: { type: 'number', description: 'merged result cap, default 16' },
667
+ read_top: { type: 'number', description: 'how many top results to auto-read fully (0-6, default 3); 0 = results only' },
668
+ },
669
+ required: ['queries'],
670
+ },
671
+ },
672
+ },
650
673
  {
651
674
  type: 'function',
652
675
  function: {
@@ -738,6 +761,16 @@ async function exec(name, args, ctx) {
738
761
  }
739
762
  return JSON.stringify(r);
740
763
  }
764
+ case 'deep_search': {
765
+ /* hook'suz bağlamda bile çalışsın: yalnız arama zinciri (sayfa okuma yok).
766
+ Gizli tarayıcı okuması engine.research hook'undan gelir (main process). */
767
+ const r = await research.deepSearch(
768
+ args,
769
+ { search: (q) => webSearchFast(q, { maxResults: 10, signal: ctx.signal }) },
770
+ ctx.signal
771
+ );
772
+ return JSON.stringify(r);
773
+ }
741
774
  case 'python_run': {
742
775
  const t0 = Date.now();
743
776
  /* önce parametreler — interpreter araması gerektirmeyen hatalar erken dönsün */
@@ -36,6 +36,10 @@ const TOOL_LABELS = {
36
36
  write_file: (a) => `Dosya yaz: ${a.path || ''}`,
37
37
  list_dir: (a) => `Klasör: ${a.path || '.'}`,
38
38
  web_search: (a) => `Web arama: ${String(a.query || '').slice(0, 100)}`,
39
+ deep_search: (a) => {
40
+ const qs = Array.isArray(a.queries) ? a.queries : [a.query || ''];
41
+ return `Derin araştırma (${qs.length} sorgu): ${String(qs[0] || '').slice(0, 80)}`;
42
+ },
39
43
  http_fetch: (a) => `Sayfa: ${a.url || ''}`,
40
44
  browser_open: (a) => `Tarayıcı aç: ${a.url || ''}`,
41
45
  browser_click: (a) => `Tıkla: ${a.ref ?? ''}`,
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');
@@ -149,7 +149,8 @@ function startNpmUpdateWatch() {
149
149
  check();
150
150
  setInterval(check, 6 * 60 * 60 * 1000);
151
151
  }
152
- const { htmlToText, setExaKey, setTinyfishKey } = require('./agent/tools');
152
+ const { htmlToText, setExaKey, setTinyfishKey, webSearchFast } = require('./agent/tools');
153
+ const research = require('./agent/research');
153
154
  const { waToolLine } = require('./agent/watext');
154
155
 
155
156
  /* #3 merkezî log sistemine process-seviye hataları da düşsün */
@@ -1442,7 +1443,7 @@ function botToolSet(cfg) {
1442
1443
  'event_list', 'event_subscribe', 'event_unsubscribe',
1443
1444
  'watcher_add', 'watcher_list', 'watcher_remove',
1444
1445
  ]);
1445
- if (s.web_search) { set.add('web_search'); set.add('http_fetch'); }
1446
+ if (s.web_search) { set.add('web_search'); set.add('http_fetch'); set.add('deep_search'); }
1446
1447
  if (s.browser) {
1447
1448
  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
1449
  }
@@ -2091,13 +2092,22 @@ function reloadBackend() {
2091
2092
  ocr: (o) => ocrRead(o),
2092
2093
  email: { list: emailList, read: emailRead, send: emailSend },
2093
2094
  browser: {
2094
- openUrl: (u, s) => browserNavigate(u, s),
2095
- search: (q, s) => browserSearch(q, s),
2095
+ openUrl: (u, s, ctx) => browserNavigate(u, s, ctx),
2096
+ search: (q, s, ctx) => browserSearch(q, s, ctx),
2096
2097
  readText: (s) => browserRead(s),
2097
2098
  screenshot: (s) => browserScreenshot(s),
2098
2099
  snapshot: (s) => browserSnapshot(s),
2099
2100
  act: (k, a, s) => browserAct(k, a, s),
2100
2101
  },
2102
+ research: {
2103
+ /* deep_search hook'u: çoklu sorgu araması + GİZLİ tarayıcıda tam sayfa okuma.
2104
+ Panel (browser.*) hiç kullanılmaz — kullanıcı ekranı rahatsız edilmez. */
2105
+ deepSearch: (args, signal) =>
2106
+ research.deepSearch(args || {}, {
2107
+ search: (q) => webSearchFast(q, { maxResults: 10, signal }),
2108
+ readPage: (u) => researchRead(u, signal),
2109
+ }, signal),
2110
+ },
2101
2111
  emit: (ev) => {
2102
2112
  if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
2103
2113
  flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
@@ -2553,10 +2563,13 @@ function setBrowserOpen(v, forceVisible) {
2553
2563
  return;
2554
2564
  }
2555
2565
 
2556
- // AÇMA — görünürlük: kullanıcı kendi açtıysa (forceVisible) MUTLAKA görünür;
2557
- // ajan açtıysa DA VARSAYILAN GÖRÜNÜR (göz ikonuyla gizli mod seçilmedikçe)
2566
+ // AÇMA — görünürlük: forceVisible true/false ise onu uygula;
2567
+ // belirtilmemişse kullanıcı tercihi (settings.browserHeadless) belirler.
2568
+ // PARALEL AJANLAR (bg oturum) her zaman forceVisible=false ile çağırır →
2569
+ // tarayıcı gizli modda çalışır, kullanıcı ekranı ve ajan konsolu rahatsız edilmez.
2558
2570
  browser.open = true;
2559
- browser.visible = forceVisible === true ? true : settings.browserHeadless !== true;
2571
+ browser.visible =
2572
+ forceVisible === true ? true : forceVisible === false ? false : settings.browserHeadless !== true;
2560
2573
  ensureBrowser();
2561
2574
  if (!browser.started) {
2562
2575
  browser.started = true;
@@ -2585,14 +2598,36 @@ function browserGate(job) {
2585
2598
  __trafficTail = p.catch(() => {});
2586
2599
  return p;
2587
2600
  }
2588
- async function browserNavigate(raw, signal) {
2601
+ /* çağıran oturum paralel ajan mı? (run_background ile açılan bg oturumu)
2602
+ → tarayıcıyı GİZLİ kullan: panel ekrana fırlamaz, ajan konsolu kapanmaz */
2603
+ function isBgBrowserCtx(ctx) {
2604
+ const sid = ctx && ctx.sessionId ? String(ctx.sessionId) : '';
2605
+ if (!sid) return false;
2606
+ try {
2607
+ return !!(engine && engine._bgJobs && engine._bgJobs.has(sid));
2608
+ } catch {
2609
+ return false;
2610
+ }
2611
+ }
2612
+
2613
+ /* bg ajan için açma stratejisi: kapalıysa GİZLİ aç; zaten açıksa mevcut
2614
+ görünürlüğe dokunma (kullanıcı paneli izliyorsa aniden saklamayalım) */
2615
+ function setBrowserOpenForAgent(ctx) {
2616
+ if (isBgBrowserCtx(ctx)) {
2617
+ if (!browser.open) setBrowserOpen(true, false);
2618
+ return;
2619
+ }
2620
+ setBrowserOpen(true);
2621
+ }
2622
+
2623
+ async function browserNavigate(raw, signal, ctx) {
2589
2624
  return browserGate(async () => {
2590
2625
  await browserTrafficWait();
2591
- return browserNavigateNow(raw, signal);
2626
+ return browserNavigateNow(raw, signal, ctx);
2592
2627
  });
2593
2628
  }
2594
2629
 
2595
- async function browserNavigateNow(raw, signal) {
2630
+ async function browserNavigateNow(raw, signal, ctx) {
2596
2631
  let url = String(raw || '').trim();
2597
2632
  if (!url) return { ok: false, error: 'boş adres' };
2598
2633
  if (!/^https?:\/\//i.test(url)) {
@@ -2601,7 +2636,7 @@ async function browserNavigateNow(raw, signal) {
2601
2636
  ? 'https://' + url
2602
2637
  : 'https://duckduckgo.com/?q=' + encodeURIComponent(url);
2603
2638
  }
2604
- setBrowserOpen(true);
2639
+ setBrowserOpenForAgent(ctx);
2605
2640
  const wc = browser.view.webContents;
2606
2641
  await new Promise((resolve) => {
2607
2642
  let settled = false;
@@ -2663,6 +2698,120 @@ function flushBrowserStorage() {
2663
2698
  } catch {}
2664
2699
  }
2665
2700
 
2701
+ /* ---------- GİZLİ ARAŞTIRMA TARAYICISI (deep_search) ----------
2702
+ deep_search'ün "sayfayı açıp oku" adımı burada çalışır: WebContentsView
2703
+ HİÇbir pencereye eklenmez (kullanıcı hiçbir şey görmez) ama gerçek Chromium
2704
+ çalışır — JS/SPA sayfalar render olur, innerText okunur. Panel (browser.view)
2705
+ hiç meşgul edilmez. Görsel/font/media indirme hız için iptal edilir.
2706
+ 2 slot = en fazla 2 sayfa aynı anda okunur (ayrı view, çakışma yok). */
2707
+ const researchPool = { views: [null, null], queue: [], slots: [false, false] };
2708
+ const RESEARCH_PAGE_TIMEOUT = 25000;
2709
+ const RESEARCH_SETTLE_MS = 900;
2710
+ const RESEARCH_CONTENT_CAP = 4500;
2711
+
2712
+ function researchAcquire() {
2713
+ return new Promise((resolve) => {
2714
+ researchPool.queue.push(resolve);
2715
+ researchPump();
2716
+ });
2717
+ }
2718
+
2719
+ function researchRelease(idx) {
2720
+ researchPool.slots[idx] = false;
2721
+ researchPump();
2722
+ }
2723
+
2724
+ function researchPump() {
2725
+ for (let i = 0; i < researchPool.slots.length; i++) {
2726
+ if (!researchPool.slots[i] && researchPool.queue.length) {
2727
+ researchPool.slots[i] = true;
2728
+ researchPool.queue.shift()(i);
2729
+ }
2730
+ }
2731
+ }
2732
+
2733
+ function researchViewAt(idx) {
2734
+ let v = researchPool.views[idx];
2735
+ if (v && v.webContents && !v.webContents.isDestroyed()) return v;
2736
+ v = new WebContentsView({
2737
+ webPreferences: {
2738
+ partition: 'research', // kalıcı olmayan bölüm — çerez birikmez
2739
+ contextIsolation: true,
2740
+ nodeIntegration: false,
2741
+ sandbox: true,
2742
+ spellcheck: false,
2743
+ backgroundThrottling: false, // gizli çalışırken zamanlayıcılar yavaşlamasın
2744
+ },
2745
+ });
2746
+ const wc = v.webContents;
2747
+ try {
2748
+ /* Google/bazı siteler Electron UA'yı reddeder — gerçek Chrome kimliği */
2749
+ const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome} Safari/537.36`;
2750
+ wc.setUserAgent(chromeUA);
2751
+ const ses = session.fromPartition('research');
2752
+ if (ses && ses.setUserAgent) ses.setUserAgent(chromeUA, 'tr-TR,tr;q=0.9,en;q=0.8');
2753
+ } catch {}
2754
+ try { wc.setWindowOpenHandler(() => ({ action: 'deny' })); } catch {}
2755
+ try {
2756
+ session.fromPartition('research').setPermissionRequestHandler((_w, _p, cb) => cb(false));
2757
+ } catch {}
2758
+ try {
2759
+ /* hız: araştırma okuması için görsel/font/media gereksiz — iptal et */
2760
+ session.fromPartition('research').webRequest.onBeforeRequest({ urls: ['*://*/*'] }, (details, cb) => {
2761
+ cb({ cancel: ['image', 'media', 'font'].includes(details.resourceType) });
2762
+ });
2763
+ } catch {}
2764
+ try {
2765
+ session.fromPartition('research').on('will-download', (_e, item) => { try { item.cancel(); } catch {} });
2766
+ } catch {}
2767
+ wc.on('render-process-gone', () => { researchPool.views[idx] = null; });
2768
+ researchPool.views[idx] = v;
2769
+ return v;
2770
+ }
2771
+
2772
+ async function researchRead(rawUrl, signal) {
2773
+ const url = String(rawUrl || '').trim();
2774
+ if (!/^https?:\/\//i.test(url)) return { ok: false, url, error: 'geçersiz adres' };
2775
+ if (signal && signal.aborted) return { ok: false, url, error: 'iptal edildi' };
2776
+ const idx = await researchAcquire();
2777
+ try {
2778
+ const view = researchViewAt(idx);
2779
+ if (!view) return { ok: false, url, error: 'araştırma tarayıcısı oluşturulamadı' };
2780
+ const wc = view.webContents;
2781
+ const loaded = await new Promise((resolve) => {
2782
+ let settled = false;
2783
+ const finish = (v) => { if (!settled) { settled = true; clearTimeout(timer); wc.removeListener('did-finish-load', onDone); wc.removeListener('did-fail-load', onFail); resolve(v); } };
2784
+ const timer = setTimeout(() => finish(false), RESEARCH_PAGE_TIMEOUT);
2785
+ const onDone = () => finish(true);
2786
+ const onFail = (_e, code) => { if (code !== -3) finish(false); };
2787
+ wc.once('did-finish-load', onDone);
2788
+ wc.on('did-fail-load', onFail);
2789
+ wc.loadURL(url).catch(() => {});
2790
+ });
2791
+ /* SPA hidrasyonu için kısa settle; metin boşsa bir kez daha dene */
2792
+ await new Promise((r) => setTimeout(r, RESEARCH_SETTLE_MS));
2793
+ let title = '';
2794
+ let finalUrl = url;
2795
+ try { title = wc.getTitle() || ''; finalUrl = wc.getURL() || url; } catch {}
2796
+ const grab = async () => {
2797
+ try { return String((await wc.executeJavaScript('(document.body&&document.body.innerText)||""', true)) || ''); } catch { return ''; }
2798
+ };
2799
+ let text = (await grab()).replace(/\n{3,}/g, '\n\n').trim();
2800
+ if (!text) {
2801
+ await new Promise((r) => setTimeout(r, 1500));
2802
+ text = (await grab()).replace(/\n{3,}/g, '\n\n').trim();
2803
+ }
2804
+ if (!text) {
2805
+ 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)' };
2806
+ }
2807
+ return { ok: true, url: finalUrl, title, truncated: text.length > RESEARCH_CONTENT_CAP, content: text.slice(0, RESEARCH_CONTENT_CAP) };
2808
+ } catch (e) {
2809
+ return { ok: false, url, error: String((e && e.message) || e) };
2810
+ } finally {
2811
+ researchRelease(idx);
2812
+ }
2813
+ }
2814
+
2666
2815
  /* Dahili OCR: görsel desteklemeyen modeller için tesseract.js ile metin okuma.
2667
2816
  Dil verisi ilk kullanımda %APPDATA%\beast\tessdata'ya iner, sonra offline çalışır. */
2668
2817
  const _ocrWorkers = new Map(); // lang -> worker (her çağrıda yeniden init olmasın)
@@ -2700,21 +2849,21 @@ async function ocrRead({ image, lang = 'tur+eng' } = {}) {
2700
2849
  Numara: view'i google.com'a bir kez açıp aramaları SAYFA İÇİ fetch() ile
2701
2850
  yapmak (kullanıcının console'da yaptığı gibi) — sayfa bile değişmeden
2702
2851
  DOMParser ile sonuç çekilir. Fetch olmazsa direk gezinme fallback'i var. */
2703
- async function browserSearch(query, signal) {
2852
+ async function browserSearch(query, signal, ctx) {
2704
2853
  /* paralel ajan sorguları trafik kapısından sırayla geçer */
2705
2854
  return browserGate(async () => {
2706
2855
  await browserTrafficWait();
2707
- return browserSearchNow(query, signal);
2856
+ return browserSearchNow(query, signal, ctx);
2708
2857
  });
2709
2858
  }
2710
2859
 
2711
- async function browserSearchNow(query, signal) {
2860
+ async function browserSearchNow(query, signal, ctx) {
2712
2861
  try {
2713
2862
  if (!win || win.isDestroyed()) return null;
2714
2863
  if (signal && signal.aborted) return null;
2715
2864
  const q = String(query || '').trim();
2716
2865
  if (!q) return null;
2717
- setBrowserOpen(true);
2866
+ setBrowserOpenForAgent(ctx);
2718
2867
  const wc = browser.view && browser.view.webContents;
2719
2868
  if (!wc) return null;
2720
2869
 
@@ -3743,13 +3892,21 @@ function flushDesktopOnDone(ev) {
3743
3892
  - Bağlantı geri gelince (DNS kontrolü) sırayla otomatik gönderilir
3744
3893
  - Renderer'a 'net' / 'netQueue' olayları gider: ⏳ kuyruk balonu + toast */
3745
3894
  const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
3895
+ const NET_LOOKUP_HOSTS = ['www.google.com', 'www.microsoft.com'];
3896
+ const NET_HTTP_PROBES = [
3897
+ 'http://www.msftconnecttest.com/connecttest.txt',
3898
+ 'http://cp.cloudflare.com/generate_204',
3899
+ 'http://connectivitycheck.gstatic.com/generate_204',
3900
+ ];
3746
3901
  const NET_CHECK_MS = 8000;
3747
3902
  const NET_CHECK_TIMEOUT = 4000;
3903
+ const NET_OFFLINE_STRIKES = 2; // üst üste bu kadar başarısız turda offline ilan edilir
3748
3904
  const CHAT_QUEUE_MAX = 50; // kuyruk üst sınırı — taşarsa en eski düşer
3749
3905
 
3750
3906
  let netOnline = true; // son bilinen bağlantı durumu (başlangıçta iyimser)
3751
3907
  let netCheckedOnce = false;
3752
3908
  let netCheckBusy = false;
3909
+ let netFailStreak = 0;
3753
3910
  let chatQueueFlushing = false;
3754
3911
  const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
3755
3912
 
@@ -3833,8 +3990,42 @@ async function flushChatQueue() {
3833
3990
  }
3834
3991
  }
3835
3992
 
3836
- /* gerçek internet kontrolü: DNS çözümlemesi (sadece arayüzü değil,
3837
- paket gerçekten çıkıyor mu test eder). Adaylar sırayla denenir. */
3993
+ /* gerçek internet kontrolü TEK yöntem yanıltıcı olabilir:
3994
+ dns.resolve (c-ares) sistem çözümleyicisini atlar; mobil ağ/hotspot/VPN ve
3995
+ ISS DNS engellemelerinde (ör. 1.1.1.1, dns.google) internet VARken bile
3996
+ başarısız çıkar. Bu yüzden katmanlı deniyoruz; HERHANGİ bir katman başarılıysa
3997
+ internet VAR sayılır:
3998
+ 1) HTTP connectivity endpoint'leri (http modülü dns.lookup = OS çözümleyicisi
3999
+ kullanır — tarayıcı gibi; captive portal/proxy/mobil ağ hepsinde çalışır)
4000
+ 2) dns.lookup (Windows sistem çözümleyicisi — hosts dosyası/VPN/NRPT dahil)
4001
+ + dns.resolve (doğrudan DNS sunucusu)
4002
+ 3) OS'in kendi bağlantı durumu (Electron net.isOnline — Windows NCSI)
4003
+ Ayrıca tek başarısız tur offline ilan etmez (2 üst üste başarısız tur gerekir):
4004
+ geçici DNS gecikmesi mesajları gereksiz kuyruğa atmaz. */
4005
+ function httpProbe(url) {
4006
+ return new Promise((resolve) => {
4007
+ let done = false;
4008
+ const fin = (v) => { if (!done) { done = true; resolve(v); } };
4009
+ try {
4010
+ const req = http.get(url, { timeout: NET_CHECK_TIMEOUT }, (res) => {
4011
+ res.resume(); // gövdeyi tüket — soket serbest kalsın
4012
+ const ok = !!res.statusCode && res.statusCode < 500;
4013
+ try { res.destroy(); } catch {}
4014
+ fin(ok);
4015
+ });
4016
+ req.on('timeout', () => { try { req.destroy(); } catch {} fin(false); });
4017
+ req.on('error', () => fin(false));
4018
+ } catch { fin(false); }
4019
+ });
4020
+ }
4021
+
4022
+ function lookupProbe(host) {
4023
+ return new Promise((resolve) => {
4024
+ const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
4025
+ dns.lookup(host, (err) => { clearTimeout(t); resolve(!err); });
4026
+ });
4027
+ }
4028
+
3838
4029
  function dnsProbe(host) {
3839
4030
  return new Promise((resolve) => {
3840
4031
  const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
@@ -3849,24 +4040,37 @@ async function netCheck() {
3849
4040
  if (netCheckBusy) return;
3850
4041
  netCheckBusy = true;
3851
4042
  try {
3852
- let ok = false;
3853
- for (const h of NET_CHECK_HOSTS) {
3854
- if (await dnsProbe(h)) { ok = true; break; }
4043
+ let ok = (await Promise.all(NET_HTTP_PROBES.map(httpProbe))).some(Boolean);
4044
+ if (!ok) {
4045
+ const lookups = [
4046
+ ...NET_LOOKUP_HOSTS.map(lookupProbe),
4047
+ ...NET_CHECK_HOSTS.map(dnsProbe),
4048
+ ];
4049
+ ok = (await Promise.all(lookups)).some(Boolean);
4050
+ }
4051
+ if (!ok) {
4052
+ try { ok = electronNet.isOnline() === true; } catch {}
3855
4053
  }
3856
4054
  const first = !netCheckedOnce;
3857
4055
  const was = netOnline;
3858
- netOnline = ok;
3859
4056
  netCheckedOnce = true;
3860
- if (was !== ok || first) {
4057
+ if (ok) {
4058
+ netFailStreak = 0;
4059
+ netOnline = true;
4060
+ } else {
4061
+ netFailStreak++;
4062
+ if (!was || netFailStreak >= NET_OFFLINE_STRIKES) netOnline = false;
4063
+ }
4064
+ if (netOnline !== was || first) {
3861
4065
  try {
3862
- if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: ok });
4066
+ if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: netOnline });
3863
4067
  } catch {}
3864
- if (ok) {
4068
+ if (netOnline) {
3865
4069
  log.info('main', 'bağlantı geri geldi — offline kuyruk kontrol ediliyor');
3866
4070
  chatQueueEmit(); // renderer: pill/toast güncellensin
3867
4071
  flushChatQueue().catch(() => {});
3868
4072
  } else {
3869
- log.info('main', 'internet bağlantısı yok — mesajlar kuyruğa alınacak');
4073
+ log.info('main', `internet bağlantısı yok — mesajlar kuyruğa alınacak (streak=${netFailStreak})`);
3870
4074
  chatQueueEmit();
3871
4075
  }
3872
4076
  }
@@ -3395,6 +3395,7 @@ function onEvent(ev) {
3395
3395
  case 'browser': {
3396
3396
  /* visible=false → ajan tarayıcıyı GİZLİ kullanıyor: UI yer açmaz */
3397
3397
  const shown = !!ev.open && ev.visible !== false;
3398
+ const wasShown = document.body.classList.contains('browser-open');
3398
3399
  document.body.classList.toggle('browser-open', shown);
3399
3400
  if (shown && ev.width) document.body.style.setProperty('--bw', ev.width + 'px');
3400
3401
  els.browserBar.hidden = !shown;
@@ -3402,11 +3403,13 @@ function onEvent(ev) {
3402
3403
  /* terminal de sağ dock'u kullanır — tarayıcı açılınca yerini bırak */
3403
3404
  if (shown && termOpen) termSetOpen(false);
3404
3405
  /* #19 tarayıcı açılınca paralel ajan konsolu (sağ panel) yerini bırakır;
3405
- kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır */
3406
+ kapanınca önceki durumuna döner — istenirse railBtn ile elle açılır.
3407
+ GİZLİ ajan gezinmeleri (shown=false, wasShown=false) rail'e DOKUNMAZ —
3408
+ aksi halde paralel ajan konsolu kendi kendine kapanırdı */
3406
3409
  if (shown) {
3407
3410
  railPrefBeforeBrowser = !document.body.classList.contains('rail-hidden');
3408
3411
  toggleRail(true);
3409
- } else {
3412
+ } else if (wasShown) {
3410
3413
  toggleRail(!railPrefBeforeBrowser);
3411
3414
  }
3412
3415
  /* IDE modunda tarayıcı açılır/kapanır/genişlik değişirse row yeniden bölünür —
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+
7
+ const research = require('../src/agent/research');
8
+
9
+ test('normKey: scheme/www/slash/hash farklarını yok sayar', () => {
10
+ assert.equal(research.normKey('http://www.Example.com/a/'), research.normKey('https://example.com/a#x'));
11
+ });
12
+
13
+ test('mergeResults: round-robin harman + tekilleştirme', () => {
14
+ const a = [
15
+ { title: 'A1', url: 'https://a.com/1' },
16
+ { title: 'A2', url: 'https://a.com/2' },
17
+ ];
18
+ const b = [
19
+ { title: 'B1', url: 'https://a.com/1/' }, // A1 ile aynı (slash)
20
+ { title: 'B1x', url: 'http://www.b.com/x' },
21
+ ];
22
+ const merged = research.mergeResults([a, b], 10);
23
+ assert.deepEqual(merged.map((r) => r.url), ['https://a.com/1', 'https://a.com/2', 'http://www.b.com/x']);
24
+ assert.equal(merged[2].title, 'B1x'); // ilk gören kazanır, orijinal url korunur
25
+ });
26
+
27
+ test('normalizeRows: {results} veya dizi + alternatif anahtarlar', () => {
28
+ const r1 = research.normalizeRows({ results: [{ name: 'X', href: 'https://x.com', body: 'b' }] });
29
+ assert.equal(r1[0].title, 'X');
30
+ assert.equal(r1[0].url, 'https://x.com');
31
+ const r2 = research.normalizeRows([{ title: 'Y', link: 'https://y.com', description: 'd' }]);
32
+ assert.equal(r2[0].url, 'https://y.com');
33
+ assert.equal(research.normalizeRows(null).length, 0);
34
+ assert.equal(research.normalizeRows([{ title: '', url: 'https://z.com' }]).length, 0); // başlıksız elenir
35
+ });
36
+
37
+ test('deepSearch: paralel sorgular harmanlanır, ilk N sayfa okunur', async () => {
38
+ const searches = {
39
+ 'elma': [{ title: 'A', url: 'https://a.com' }, { title: 'B', url: 'https://b.com' }],
40
+ 'apple': [{ title: 'B2', url: 'https://b.com' }, { title: 'C', url: 'https://c.com' }],
41
+ };
42
+ const readCalls = [];
43
+ const r = await research.deepSearch(
44
+ { queries: ['elma', 'apple'], read_top: 2 },
45
+ {
46
+ search: async (q) => searches[q],
47
+ readPage: async (u) => { readCalls.push(u); return { ok: true, url: u, title: 'T:' + u, content: 'icerik ' + u }; },
48
+ }
49
+ );
50
+ assert.ok(r.ok);
51
+ assert.deepEqual(r.queries, ['elma', 'apple']);
52
+ assert.equal(r.results.length, 3); // b.com duplike düştü
53
+ assert.deepEqual(readCalls, ['https://a.com', 'https://b.com']); // ilk 2 hedef
54
+ assert.equal(r.pages.length, 2);
55
+ assert.ok(r.pages[0].content.includes('https://a.com'));
56
+ assert.match(r.note, /gizli tarayıcı/);
57
+ });
58
+
59
+ test('deepSearch: ikili dosya hedefleri okumadan atlanır', async () => {
60
+ const readCalls = [];
61
+ const r = await research.deepSearch(
62
+ { queries: ['q'], read_top: 3 },
63
+ {
64
+ search: async () => [
65
+ { title: 'PDF', url: 'https://x.com/doc.pdf' },
66
+ { title: 'Sayfa', url: 'https://x.com/page' },
67
+ ],
68
+ readPage: async (u) => { readCalls.push(u); return { ok: true, url: u, content: 'c' }; },
69
+ }
70
+ );
71
+ assert.deepEqual(readCalls, ['https://x.com/page']);
72
+ assert.equal(r.pages.length, 1);
73
+ });
74
+
75
+ test('deepSearch: readPage yoksa yalnız sonuç listesiyle döner', async () => {
76
+ const r = await research.deepSearch(
77
+ { queries: ['q'] },
78
+ { search: async () => [{ title: 'A', url: 'https://a.com' }] }
79
+ );
80
+ assert.ok(r.ok);
81
+ assert.equal(r.results.length, 1);
82
+ assert.equal(r.pages, undefined);
83
+ });
84
+
85
+ test('deepSearch: bir sorgu çökerse diğerleri kurtarır; boşsa ok:false', async () => {
86
+ const r1 = await research.deepSearch(
87
+ { queries: ['patlar', 'iyi'] },
88
+ {
89
+ search: async (q) => { if (q === 'patlar') throw new Error('boom'); return [{ title: 'A', url: 'https://a.com' }]; },
90
+ readPage: async () => ({ ok: false }),
91
+ }
92
+ );
93
+ assert.ok(r1.ok && r1.results.length === 1);
94
+ const r2 = await research.deepSearch(
95
+ { queries: ['yok1', 'yok2'] },
96
+ { search: async () => [], readPage: async () => ({ ok: false }) }
97
+ );
98
+ assert.equal(r2.ok, false);
99
+ assert.match(r2.error, /boş/);
100
+ });
101
+
102
+ test('deepSearch: sorgu yoksa net hata; queries string de kabul edilir', async () => {
103
+ const bad = await research.deepSearch({}, { search: async () => [] });
104
+ assert.equal(bad.ok, false);
105
+ const r = await research.deepSearch(
106
+ { queries: 'tek sorgu' },
107
+ { search: async () => [{ title: 'A', url: 'https://a.com' }] }
108
+ );
109
+ assert.deepEqual(r.queries, ['tek sorgu']);
110
+ });
111
+
112
+ test('SKIP_FILE_RE: pdf/görsel/arşiv yakalanır, sayfa yakalanmaz', () => {
113
+ assert.ok(research.SKIP_FILE_RE.test('https://x.com/a.pdf'));
114
+ assert.ok(research.SKIP_FILE_RE.test('https://x.com/img.png?w=9'));
115
+ assert.ok(!research.SKIP_FILE_RE.test('https://x.com/article'));
116
+ });