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.
- package/bin/beast-agent.js +34 -17
- package/package.json +2 -1
- package/scripts/fix-electron.js +126 -0
- package/src/agent/engine.js +25 -13
- package/src/agent/research.js +133 -0
- package/src/agent/tools.js +33 -0
- package/src/agent/watext.js +4 -0
- package/src/main.js +230 -26
- package/src/renderer/renderer.js +5 -2
- package/tests/research.test.js +116 -0
package/bin/beast-agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/agent/engine.js
CHANGED
|
@@ -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 };
|
package/src/agent/tools.js
CHANGED
|
@@ -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 */
|
package/src/agent/watext.js
CHANGED
|
@@ -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:
|
|
2557
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3837
|
-
|
|
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 =
|
|
3853
|
-
|
|
3854
|
-
|
|
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 (
|
|
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:
|
|
4066
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: netOnline });
|
|
3863
4067
|
} catch {}
|
|
3864
|
-
if (
|
|
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',
|
|
4073
|
+
log.info('main', `internet bağlantısı yok — mesajlar kuyruğa alınacak (streak=${netFailStreak})`);
|
|
3870
4074
|
chatQueueEmit();
|
|
3871
4075
|
}
|
|
3872
4076
|
}
|
package/src/renderer/renderer.js
CHANGED
|
@@ -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
|
+
});
|