beast-agent 0.26.0 → 0.26.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/beast-agent.js +34 -17
- package/package.json +2 -2
- package/scripts/fix-electron.js +126 -0
- package/src/agent/bots.js +8 -0
- package/src/agent/discord.js +268 -0
- package/src/agent/engine.js +135 -77
- package/src/agent/llm.js +46 -17
- package/src/agent/research.js +133 -0
- package/src/agent/skills.js +8 -1
- package/src/agent/tools.js +53 -14
- package/src/agent/watext.js +4 -0
- package/src/main.js +534 -351
- package/src/preload.js +8 -5
- package/src/renderer/i18n.js +19 -55
- package/src/renderer/index.html +1 -3
- package/src/renderer/renderer.js +336 -155
- package/src/renderer/style.css +1 -60
- package/tests/bg-jobs.test.js +5 -10
- package/tests/llm.test.js +81 -1
- package/tests/python.test.js +3 -2
- package/tests/research.test.js +116 -0
package/src/agent/engine.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const { chatStream, chatOnce } = require('./llm');
|
|
9
|
+
const { chatStream, chatStreamAuto, chatOnce } = require('./llm');
|
|
10
10
|
const { chatCompletionsUrl } = require('./config');
|
|
11
11
|
const tools = require('./tools');
|
|
12
|
+
const research = require('./research');
|
|
12
13
|
const memory = require('./memory');
|
|
13
14
|
const skills = require('./skills');
|
|
14
15
|
const { estTokens, estMsgTokens } = require('./tokens');
|
|
@@ -34,9 +35,10 @@ const SUP_IDLE_MIN = 2; // ...bu kadar süredir hiç aktivite yoksa TAKILDI
|
|
|
34
35
|
const SUP_LONG_MIN = 6; // bu kadar süredir koşuyorsa ara kontrol nödü
|
|
35
36
|
const SUP_NUDGE_COOLDOWN_MIN = 4; // aynı iş için iki uyarı arası min süre
|
|
36
37
|
const BG_FIX_MAX = 2; // paralel ajanın otomatik öz-kurtarma hakkı (bitince CEO devreye girer)
|
|
37
|
-
/*
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/* SÜRE SINIRI YOK: bg ajanlar dakika bazında kesilmez. Sonsuz döngü koruması
|
|
39
|
+
üç katmanla sağlanır: 1) aktivite yoksa öz-kurtarma + CEO (stuck denetimi),
|
|
40
|
+
2) MAX_TURNS sert tur tavanı, 3) tur limitine yaklaşınca zarif "raporu yaz"
|
|
41
|
+
uyarısı (_run içinde). Dakika bazlı wrap-up KALDIRILDI (BG_WRAP_MIN). */
|
|
40
42
|
/* #5 hız: eşzamanlı arka plan LLM turu sınırı — rate-limit yemeden maksimum paralellik */
|
|
41
43
|
const BG_MAX_CONCURRENT_DEFAULT = 4;
|
|
42
44
|
/* #17 kalıcı başarısızlıkta owner'a anlık uyarı */
|
|
@@ -69,13 +71,13 @@ const FORMAT_RULES =
|
|
|
69
71
|
const PERM_TOOL_SETS = {
|
|
70
72
|
all: null, // tüm araçlar (filtre yok)
|
|
71
73
|
web: new Set([
|
|
72
|
-
'web_search', 'http_fetch',
|
|
74
|
+
'web_search', 'http_fetch', 'deep_search',
|
|
73
75
|
'browser_open', 'browser_read', 'browser_snapshot', 'browser_screenshot',
|
|
74
76
|
'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
|
|
75
77
|
'ocr_read',
|
|
76
78
|
]),
|
|
77
79
|
read: new Set([
|
|
78
|
-
'web_search', 'http_fetch',
|
|
80
|
+
'web_search', 'http_fetch', 'deep_search',
|
|
79
81
|
'browser_open', 'browser_read', 'browser_snapshot',
|
|
80
82
|
'list_dir', 'read_file',
|
|
81
83
|
]),
|
|
@@ -99,7 +101,7 @@ function normalizePerms(p) {
|
|
|
99
101
|
const CEO_EXEC_TOOLS = new Set([
|
|
100
102
|
'run_command', 'read_file', 'write_file', 'list_dir',
|
|
101
103
|
'python_run',
|
|
102
|
-
'web_search', 'http_fetch',
|
|
104
|
+
'web_search', 'http_fetch', 'deep_search',
|
|
103
105
|
'browser_open', 'browser_read', 'browser_screenshot', 'browser_snapshot',
|
|
104
106
|
'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
|
|
105
107
|
'computer_look', 'computer_act',
|
|
@@ -202,6 +204,7 @@ class Engine {
|
|
|
202
204
|
this.lockdown = !!opts.lockdown; // varsayılan kısıt (oturum bazlı override edilmezse)
|
|
203
205
|
this.sessionPerm = new Map(); // sessionId -> ['web'] | ['web','read'] | ['chat'] (kişi/bot bazlı izin)
|
|
204
206
|
this.sessionTools = new Map(); // sessionId -> Set(araç adları) — bot skill kısıtı
|
|
207
|
+
this.sessionModel = new Map(); // sessionId -> chain entry — bot bazlı model override (setSessionModel)
|
|
205
208
|
this.resolveBot = opts.resolveBot || null; // botId -> bot bilgisi (main enjekte eder)
|
|
206
209
|
/* bot oturumu hafıza köprüsü: botun kendi SOUL/USER/MEMORY dosyaları */
|
|
207
210
|
this.botMemory = opts.botMemory || null;
|
|
@@ -333,6 +336,16 @@ class Engine {
|
|
|
333
336
|
else this.sessionTools.delete(id);
|
|
334
337
|
}
|
|
335
338
|
|
|
339
|
+
/* BOT MODEL OVERRIDE: her bot farklı model kullanabilir (sel boşsa global seçim).
|
|
340
|
+
sel biçimi: 'providerId::model' — zincirde yoksa sessizce global'e düşer. */
|
|
341
|
+
setSessionModel(sessionId, sel) {
|
|
342
|
+
const id = String(sessionId || '');
|
|
343
|
+
if (!id) return;
|
|
344
|
+
const resolved = sel ? this._resolve(String(sel)) : null;
|
|
345
|
+
if (resolved) this.sessionModel.set(id, resolved);
|
|
346
|
+
else this.sessionModel.delete(id);
|
|
347
|
+
}
|
|
348
|
+
|
|
336
349
|
/* Oturumun bağlı olduğu MÜŞTERİ botu (admin/seasız → null → global hafıza) */
|
|
337
350
|
_sessionBotCtx(session) {
|
|
338
351
|
if (!session || !session.botId || typeof this.resolveBot !== 'function') return null;
|
|
@@ -608,7 +621,7 @@ class Engine {
|
|
|
608
621
|
}
|
|
609
622
|
}
|
|
610
623
|
}
|
|
611
|
-
const res = await
|
|
624
|
+
const res = await chatStreamAuto(
|
|
612
625
|
c,
|
|
613
626
|
{ messages: msgs, tools: activeTools, reasoningEffort: this._thinkEffortFor(session) },
|
|
614
627
|
{
|
|
@@ -1285,11 +1298,13 @@ class Engine {
|
|
|
1285
1298
|
'Kullanıcı kalıcı bir arka plan takibi isterse (fiyat eşiği, pil seviyesi, sayfa değişikliği) watcher_add ile izleyici kur; kurduktan sonra watcher_list ile doğrula ve kullanıcıya koşulu + kontrol sıklığını kısaca bildir.',
|
|
1286
1299
|
'Anlık olay takipleri için (yeni mail, fiyat eşiği, dosya değişimi, webhook) event_subscribe kullan — cron/polling gerekmez; listeyi event_list ile göster, vazgeçirirse event_unsubscribe.',
|
|
1287
1300
|
'Kullanıcı "artık hep böyle yap / bunu unutma" tarzı kalıcı talimat verirse kural olarak kaydet: sohbette /rule <metin> kullanmasını söyle VEYA kullanıcı isterse event_subscribe ile olaya bağlan (mail/fiyat/dosya/webhook).',
|
|
1288
|
-
'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle.',
|
|
1301
|
+
'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle. LİSTE DİSİPLİNİ: her adım bittiği AN status:"done" yap; son cevabını vermeden önce tüm maddeler done olmalı — yapılmayacaksa listeden düş. Listeyi yarım bırakma.',
|
|
1302
|
+
'HIZ KURALI: Bağımsız işleri AYNI turda birden çok tool_calls ile PARALEL ver. Küçük işleri tek tek çağırma — her ayrı araç turu 5-15 sn LLM gecikmesidir: 3+ küçük komutu TEK run_command\u2019te `;` ile zincirle (örn `git status; node -v; dir`), döngülü/çoklu işleri TEK python_run betiğinde topla, çok dosyalık değişikliği TEK script ile yap. Her küçük işlem için ayrı araç çağrısı açmak yavaşlığın 1 numaralı sebebidir.',
|
|
1289
1303
|
this.ceoMode
|
|
1290
1304
|
? '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
1305
|
: 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
|
|
1292
1306
|
'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.',
|
|
1307
|
+
'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
1308
|
'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
1309
|
'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
1310
|
' 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.' +
|
|
@@ -1299,6 +1314,7 @@ class Engine {
|
|
|
1299
1314
|
'CONUŞMA ODAĞI SENDE KALSIN: kullanıcı seninle konuşurken iş çıkmışsa — uzun da olsa UFACIK da (tek komutluk dizin listesi, tek dosya okuma, tek arama…) — run_background ile PARALEL ajana devret; ana sohbet hiçbir işi beklemez; bittiğinde özet otomatik düşer.',
|
|
1300
1315
|
'Python işleri için python_run kullan: küçük betikler inline code ile; tekrarlayan işler %APPDATA%\\beast\\scripts klasöründeki dosyalarla (ör. news.py = RSS haber toplayıcı: args ["--limit","8","--json"]). Python kurulu olmasa bile ilk çağrıda taşınabilir gömülü runtime otomatik iner.',
|
|
1301
1316
|
'PYTHON DURUMU: makinede sistem Python\'u görünmese bile ŞAŞIRMA ve "python yok" DEME — python_run aracı kendi taşınabilir runtime\'ını (%APPDATA%\\beast\\py\\python.exe) otomatik indirir/kullanır ve bu klasör run_command PATH\'inde önceliklidir; yani run_command içinde de `python` çalışır. Ham Google/Bing scrape yerine önce web_search aracını kullan (zaten Python çoklu-motor + Exa destekli), script gerektiğinde python_run yaz.',
|
|
1317
|
+
'PDF ÇIKTI KURALI: PDF üretirken pip\u2019ten pdf paketi (fpdf, fpdf2, markdown-pdf, weasyprint, reportlab vb.) KURMA/KULLANMA — bunlar Türkçe karakterleri bozar. Doğru kit Node tarafında ZATEN kurulu: `pdf-lib` + `@pdf-lib/fontkit` (Türkçe font gömme) ve `pdfkit`. python_run ile DEĞİL; write_file ile .js script yazıp run_command ile `node script.js` çalıştır. md→pdf çevirici YOKTUR ve kurulmaz: kullanıcıya rapor/özet/belge çıktısı vereceksen .md dosyası gönderme — aynı içeriği DOĞRUDAN pdf-lib/pdfkit ile PDF olarak üret ve send_file ile o PDF\u2019i gönder. Ayrıntılı örnekler: pdf skill\u2019i (SKILL.md).',
|
|
1302
1318
|
'Eski bir hafıza kaydına ihtiyacın olursa memory_search ile ara; kalıcı bilgi/birikim için kb_search kullan, yeni bilgi öğrenirsen kb_add ile kaynak belirt.',
|
|
1303
1319
|
'Bir oturumda 3+ kez memory_write yaptıysan iş bitince memory_hygiene çağır (duplike/eskime temizliği).',
|
|
1304
1320
|
'BEAST KAYNAK KORUMASI: Beast\u2019in kendi kurulum/kaynak kod klasörü KİLİTLİDİR — oraya dosya yazamaz, silamaz, komut/betikle değiştiremezsin; okumak serbest. Kullanıcı kodu ancak kendi eliyle dışarıdan değiştirir; böyle bir istek gelirse yapamayacağını söyle ve kullanıcının elle yapması için yol göster.',
|
|
@@ -1907,7 +1923,7 @@ class Engine {
|
|
|
1907
1923
|
job.lastActivityAt = nowIso();
|
|
1908
1924
|
}
|
|
1909
1925
|
|
|
1910
|
-
/* Saf sınıflandırma (test edilebilir): null | 'stuck' | '
|
|
1926
|
+
/* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'long' */
|
|
1911
1927
|
static superviseReason(job, nowMs) {
|
|
1912
1928
|
if (!job || job.status !== 'running') return null;
|
|
1913
1929
|
const started = Date.parse(job.startedAt || '') || nowMs;
|
|
@@ -1919,7 +1935,6 @@ class Engine {
|
|
|
1919
1935
|
: Infinity;
|
|
1920
1936
|
if (nudgedMin < SUP_NUDGE_COOLDOWN_MIN) return null; // yeni uyardık — boğmayalım
|
|
1921
1937
|
if (runMin >= SUP_STUCK_START_MIN && idleMin >= SUP_IDLE_MIN) return 'stuck';
|
|
1922
|
-
if (!job.wrapAt && runMin >= BG_WRAP_MIN) return 'wrapup';
|
|
1923
1938
|
if (runMin >= SUP_LONG_MIN) return 'long';
|
|
1924
1939
|
return null;
|
|
1925
1940
|
}
|
|
@@ -1936,11 +1951,6 @@ class Engine {
|
|
|
1936
1951
|
this._bgSelfHeal(job, now);
|
|
1937
1952
|
continue;
|
|
1938
1953
|
}
|
|
1939
|
-
/* zarif bitirme: süre dolunca yeni araştırma yasak → SON RAPORU yazsın */
|
|
1940
|
-
if (reason === 'wrapup') {
|
|
1941
|
-
this._bgWrapUp(job, now);
|
|
1942
|
-
continue;
|
|
1943
|
-
}
|
|
1944
1954
|
job.lastNudgeAt = nowIso();
|
|
1945
1955
|
job.checks = (job.checks || 0) + 1;
|
|
1946
1956
|
const det = this.bgDetail(job.id);
|
|
@@ -1979,24 +1989,6 @@ class Engine {
|
|
|
1979
1989
|
this._bgKick(job, text);
|
|
1980
1990
|
}
|
|
1981
1991
|
|
|
1982
|
-
/* Zarif bitirme: süre dolunca ajanı kesip SON RAPORU yazmasını iste.
|
|
1983
|
-
Uzatma yok, öldürme yok — mevcut bulgularla düzgün kapanış. */
|
|
1984
|
-
_bgWrapUp(job, now) {
|
|
1985
|
-
job.wrapAt = nowIso();
|
|
1986
|
-
job.lastNudgeAt = nowIso();
|
|
1987
|
-
job.checks = (job.checks || 0) + 1;
|
|
1988
|
-
const runMin = Math.max(1, Math.round((now - Date.parse(job.startedAt)) / 60000));
|
|
1989
|
-
const det = this.bgDetail(job.id);
|
|
1990
|
-
const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '';
|
|
1991
|
-
const text =
|
|
1992
|
-
`[SÜRE UYARISI] "${job.title}" görevi ${runMin} dk'dır sürüyor — süre doldu.\n` +
|
|
1993
|
-
`YENİ arama/fetch/sayfa açma YAPMA. Şu ana kadar bulduklarınla SON RAPORU şimdi yaz:\n` +
|
|
1994
|
-
`- 3-5 madde: net sonuç + yapılamayanlar açıkça "bulunamadı" diye.\n` +
|
|
1995
|
-
`Raporu yazınca görev tamamlanır — fazladan tur harcama.` +
|
|
1996
|
-
(tail ? `\n\nSon çıktın:\n${tail}` : '');
|
|
1997
|
-
this._bgKick(job, text);
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
1992
|
/* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
|
|
2001
1993
|
kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
|
|
2002
1994
|
revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
|
|
@@ -2113,8 +2105,9 @@ class Engine {
|
|
|
2113
2105
|
`Ortam: Windows + PowerShell; çalışma klasörü: ${this.workspace}\n` +
|
|
2114
2106
|
`HIZ KURALLARI:\n` +
|
|
2115
2107
|
`- 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` +
|
|
2108
|
+
`- 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
2109
|
`- Bağımsız araç çağrılarını aynı turda PARALEL ver.\n` +
|
|
2110
|
+
`- PDF gerekirse pip\u2019ten paket kurma (Türkçe bozar); Node\u2019un kurulu \`pdf-lib\`+fontkit\u2019iyle .js script yazıp \`node\` ile çalıştır. md→pdf çevirme: belge çıktısını doğrudan PDF olarak üret.\n` +
|
|
2118
2111
|
`- 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
2112
|
FORMAT_RULES + '\n' +
|
|
2120
2113
|
`SON ÇIKTI: 3-5 satırlık net sonuç raporu, madde madde. Soru sorma, sohbet etme.`
|
|
@@ -2221,6 +2214,11 @@ class Engine {
|
|
|
2221
2214
|
role = 'terminal';
|
|
2222
2215
|
}
|
|
2223
2216
|
let sel = this.modelFor(role);
|
|
2217
|
+
/* bot model override: rol modeli (vision/terminal) yoksa botun kendi seçimi kazanır */
|
|
2218
|
+
if (!role) {
|
|
2219
|
+
const ssel = this.sessionModel.get(String(session.id));
|
|
2220
|
+
if (ssel) sel = ssel;
|
|
2221
|
+
}
|
|
2224
2222
|
if (role && this.roleModels[role]) {
|
|
2225
2223
|
emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
|
|
2226
2224
|
}
|
|
@@ -2284,9 +2282,26 @@ class Engine {
|
|
|
2284
2282
|
const sid = session.id;
|
|
2285
2283
|
const emit = (ev) => this.emit({ ...ev, sessionId: sid });
|
|
2286
2284
|
try {
|
|
2287
|
-
if (!this.sel) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
|
|
2285
|
+
if (!this.sel && !this.sessionModel.get(String(session.id))) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
|
|
2288
2286
|
|
|
2287
|
+
let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
|
|
2288
|
+
let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
|
|
2289
2289
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
2290
|
+
/* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
|
|
2291
|
+
"raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
|
|
2292
|
+
if (session.bgJob && turn === MAX_TURNS - 3 && !wrapNudged) {
|
|
2293
|
+
wrapNudged = true;
|
|
2294
|
+
const wmsg = {
|
|
2295
|
+
role: 'user',
|
|
2296
|
+
content:
|
|
2297
|
+
'[TUR LİMİTİ YAKLAŞIYOR] Kalın son turlar: yeni araştırma/iş AÇMA — şu ana kadarki bulgularınla SON RAPORU yaz ve bitir. Yapılamayanları açıkça "bulunamadı" diye belirt.',
|
|
2298
|
+
};
|
|
2299
|
+
session.messages.push(wmsg);
|
|
2300
|
+
try {
|
|
2301
|
+
this._append(session, wmsg);
|
|
2302
|
+
} catch {}
|
|
2303
|
+
emit({ type: 'message', message: wmsg });
|
|
2304
|
+
}
|
|
2290
2305
|
this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
|
|
2291
2306
|
emit({ type: 'status', status: 'thinking' });
|
|
2292
2307
|
const res = await this._chatTurn(session, ctrl.signal, (delta) =>
|
|
@@ -2302,6 +2317,27 @@ class Engine {
|
|
|
2302
2317
|
emit({ type: 'message', message: assistant });
|
|
2303
2318
|
|
|
2304
2319
|
if (!res.toolCalls || !res.toolCalls.length) {
|
|
2320
|
+
/* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
|
|
2321
|
+
bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
|
|
2322
|
+
liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
|
|
2323
|
+
const todos = session.todos || this.todos.get(sid) || [];
|
|
2324
|
+
const pending = todos.filter((t) => t && t.status !== 'done');
|
|
2325
|
+
if (pending.length && !nudged) {
|
|
2326
|
+
nudged = true;
|
|
2327
|
+
const nudge = {
|
|
2328
|
+
role: 'user',
|
|
2329
|
+
content:
|
|
2330
|
+
'[OTOMATİK HATIRLATMA] Görev listende hâlâ tamamlanmamış maddeler var: ' +
|
|
2331
|
+
pending.map((t) => '"' + t.title + '"').join(', ') +
|
|
2332
|
+
'. Görevi yarım bırakma: eksik adımları ŞİMDİ tamamla; gerçekten yapılmayacaksa listeden düş. todo_write ile listeyi güncelle (status:"done") ve işi kapat.',
|
|
2333
|
+
};
|
|
2334
|
+
session.messages.push(nudge);
|
|
2335
|
+
try {
|
|
2336
|
+
this._append(session, nudge);
|
|
2337
|
+
} catch {}
|
|
2338
|
+
emit({ type: 'message', message: nudge });
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2305
2341
|
/* cevap tamam → done HEMEN: kullanıcı beklemeden yeni iş yazabilsin.
|
|
2306
2342
|
Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
|
|
2307
2343
|
this._clearCrash();
|
|
@@ -2694,7 +2730,7 @@ class Engine {
|
|
|
2694
2730
|
];
|
|
2695
2731
|
try {
|
|
2696
2732
|
for (let turn = 0; turn < SUB_MAX_TURNS; turn++) {
|
|
2697
|
-
const res = await
|
|
2733
|
+
const res = await chatStreamAuto(
|
|
2698
2734
|
sel,
|
|
2699
2735
|
{ messages: [{ role: 'system', content: system }, ...msgs.slice(1)], tools: subTools, reasoningEffort: this._thinkEffort() },
|
|
2700
2736
|
{ signal: ctrl.signal }
|
|
@@ -2719,9 +2755,43 @@ class Engine {
|
|
|
2719
2755
|
}
|
|
2720
2756
|
}
|
|
2721
2757
|
|
|
2758
|
+
/* web_search zinciri — web_search VE deep_search aynı zinciri kullanır:
|
|
2759
|
+
1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
|
|
2760
|
+
2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
|
|
2761
|
+
3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
|
|
2762
|
+
4) Exa (anahtar girildiyse, son çare)
|
|
2763
|
+
Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
|
|
2764
|
+
tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
|
|
2765
|
+
async _webSearchChain(q, n, sessionId, signal) {
|
|
2766
|
+
let r = null;
|
|
2767
|
+
let banned = false;
|
|
2768
|
+
const browserUsable = this.browser && typeof this.browser.search === 'function';
|
|
2769
|
+
if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
|
|
2770
|
+
if (browserUsable && !banned) {
|
|
2771
|
+
try { r = await this.browser.search(q, signal, { sessionId }); } catch {}
|
|
2772
|
+
if (!r || !r.ok || !(r.results || []).length) {
|
|
2773
|
+
/* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
|
|
2774
|
+
this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
|
|
2775
|
+
r = null;
|
|
2776
|
+
banned = true;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (!r || !r.ok || !(r.results || []).length) {
|
|
2780
|
+
try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
|
|
2781
|
+
}
|
|
2782
|
+
if (!r || !r.ok || !(r.results || []).length) {
|
|
2783
|
+
try {
|
|
2784
|
+
r = JSON.parse(await tools.exec('web_search', { query: q, max_results: n }, { cwd: this._sessionWorkspace(sessionId), signal }));
|
|
2785
|
+
} catch {}
|
|
2786
|
+
}
|
|
2787
|
+
const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
|
|
2788
|
+
if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
|
|
2789
|
+
return out;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2722
2792
|
/* Dahili OCR aracı: görüntüden metin çıkarır (görsel desteklemeyen modeller için).
|
|
2723
2793
|
Kaynak: 'browser' (dahili tarayıcı görüntüsü, varsayılan) | 'screen' (masaüstü) | dosya yolu. */
|
|
2724
|
-
async _ocrRead(args, signal) {
|
|
2794
|
+
async _ocrRead(args, signal, sessionId) {
|
|
2725
2795
|
if (!this.ocr) return { ok: false, error: 'OCR kullanılamıyor' };
|
|
2726
2796
|
const src = String((args && args.source) || 'browser');
|
|
2727
2797
|
let image = null;
|
|
@@ -2732,7 +2802,7 @@ class Engine {
|
|
|
2732
2802
|
if (!this.browser || typeof this.browser.screenshot !== 'function') {
|
|
2733
2803
|
return { ok: false, error: 'dahili tarayıcı kullanılamıyor' };
|
|
2734
2804
|
}
|
|
2735
|
-
const shot = await this.browser.screenshot(signal);
|
|
2805
|
+
const shot = await this.browser.screenshot(signal, { sessionId });
|
|
2736
2806
|
if (!shot || !shot.ok) return { ok: false, error: (shot && shot.error) || 'görüntü alınamadı' };
|
|
2737
2807
|
image = shot.__injectImage || shot.image || null;
|
|
2738
2808
|
} else if (src === 'screen') {
|
|
@@ -3067,76 +3137,64 @@ class Engine {
|
|
|
3067
3137
|
const r = await this.computer.act(op, args || {});
|
|
3068
3138
|
return JSON.stringify(r);
|
|
3069
3139
|
}
|
|
3140
|
+
if (name === 'deep_search') {
|
|
3141
|
+
/* agentic derin araştırma: çoklu sorgu + gizli tarayıcıda sayfa okuma.
|
|
3142
|
+
ARAMA zinciri web_search ile BİREBİR AYNI — eski sürüm yalnız python
|
|
3143
|
+
motorlarını (ddgs/DDG/Bing) kullanıyordu; CAPTCHA/engel sonucu motorlar
|
|
3144
|
+
boş dönünce "tüm motorlar boş döndü" hatası veriyordu. Artık önce
|
|
3145
|
+
DAHİLİ TARAYICI (Google) denenir, sonra TinyFish → python → Exa. */
|
|
3146
|
+
try {
|
|
3147
|
+
emitSafe(this, sessionId, { type: 'status', status: 'derin araştırma: çoklu sorgu + gizli sayfa okuma' });
|
|
3148
|
+
} catch {}
|
|
3149
|
+
const deps = {
|
|
3150
|
+
search: (q) => this._webSearchChain(q, 10, sessionId, signal),
|
|
3151
|
+
};
|
|
3152
|
+
if (this.research && typeof this.research.readPage === 'function') {
|
|
3153
|
+
deps.readPage = (u) => this.research.readPage(u, signal);
|
|
3154
|
+
}
|
|
3155
|
+
return JSON.stringify(await research.deepSearch(args || {}, deps, signal));
|
|
3156
|
+
}
|
|
3070
3157
|
if (name === 'web_search') {
|
|
3071
|
-
/* SIRA: 1) dahili tarayıcı (DİREK GOOGLE — ücretsiz + AI cevabı)
|
|
3072
|
-
2) TinyFish (tarayıcı sorun çıkarsa HEMEN devreye girer — anahtar girildiyse)
|
|
3073
|
-
3) python çoklu-motor (ddgs / DDG+Bing+Mojeek)
|
|
3074
|
-
4) Exa (anahtar girildiyse, son çare)
|
|
3075
|
-
Tarayıcıda CAPTCHA/"olağandışı trafik"/boş sonuç algılanırsa 10 dk boyunca
|
|
3076
|
-
tarayıcı ATLANIR — her arama TinyFish'ten başlar, süre dolunca tarayıcı geri döner. */
|
|
3077
3158
|
const q = String((args && args.query) || '');
|
|
3078
3159
|
const n = Number((args && args.max_results) || 8);
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
const browserUsable = this.browser && typeof this.browser.search === 'function';
|
|
3082
|
-
if (Date.now() < (this._webSearchBrowserBanUntil || 0)) banned = true;
|
|
3083
|
-
if (browserUsable && !banned) {
|
|
3084
|
-
try { r = await this.browser.search(q, signal); } catch {}
|
|
3085
|
-
if (!r || !r.ok || !(r.results || []).length) {
|
|
3086
|
-
/* tarayıcı sorunlu — 10 dk atla, alternatif zincir hemen devrede */
|
|
3087
|
-
this._webSearchBrowserBanUntil = Date.now() + 10 * 60 * 1000;
|
|
3088
|
-
r = null;
|
|
3089
|
-
banned = true;
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
if (!r || !r.ok || !(r.results || []).length) {
|
|
3093
|
-
try { r = await tools.tinyfishSearch(q, n, signal); } catch {}
|
|
3094
|
-
}
|
|
3095
|
-
if (!r || !r.ok || !(r.results || []).length) {
|
|
3096
|
-
try {
|
|
3097
|
-
r = JSON.parse(await tools.exec('web_search', args, { cwd: this._sessionWorkspace(sessionId), signal }));
|
|
3098
|
-
} catch {}
|
|
3099
|
-
}
|
|
3100
|
-
const out = r || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
|
|
3101
|
-
if (out && out.ok && banned) out.note = 'tarayıcı araması 10 dk askıda (CAPTCHA/trafik) — TinyFish/alternatif zincir kullanıldı';
|
|
3102
|
-
return JSON.stringify(out);
|
|
3103
|
-
}
|
|
3160
|
+
return JSON.stringify(await this._webSearchChain(q, n, sessionId, signal));
|
|
3161
|
+
}
|
|
3104
3162
|
if (name === 'ocr_read') {
|
|
3105
|
-
return JSON.stringify(await this._ocrRead(args || {}, signal));
|
|
3163
|
+
return JSON.stringify(await this._ocrRead(args || {}, signal, sessionId));
|
|
3106
3164
|
}
|
|
3107
3165
|
if (name === 'browser_open') {
|
|
3108
3166
|
if (!this.browser || typeof this.browser.openUrl !== 'function') {
|
|
3109
3167
|
return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
|
|
3110
3168
|
}
|
|
3111
|
-
const r = await this.browser.openUrl(String(args.url || ''), signal);
|
|
3169
|
+
const r = await this.browser.openUrl(String(args.url || ''), signal, { sessionId });
|
|
3112
3170
|
return JSON.stringify(r);
|
|
3113
3171
|
}
|
|
3114
3172
|
if (name === 'browser_read') {
|
|
3115
3173
|
if (!this.browser || typeof this.browser.readText !== 'function') {
|
|
3116
3174
|
return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
|
|
3117
3175
|
}
|
|
3118
|
-
const r = await this.browser.readText(signal);
|
|
3176
|
+
const r = await this.browser.readText(signal, { sessionId });
|
|
3119
3177
|
return JSON.stringify(r);
|
|
3120
3178
|
}
|
|
3121
3179
|
if (name === 'browser_screenshot') {
|
|
3122
3180
|
if (!this.browser || typeof this.browser.screenshot !== 'function') {
|
|
3123
3181
|
return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
|
|
3124
3182
|
}
|
|
3125
|
-
const r = await this.browser.screenshot(signal);
|
|
3183
|
+
const r = await this.browser.screenshot(signal, { sessionId });
|
|
3126
3184
|
return JSON.stringify(r);
|
|
3127
3185
|
}
|
|
3128
3186
|
if (name === 'browser_snapshot') {
|
|
3129
3187
|
if (!this.browser || typeof this.browser.snapshot !== 'function') {
|
|
3130
3188
|
return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
|
|
3131
3189
|
}
|
|
3132
|
-
const r = await this.browser.snapshot(signal);
|
|
3190
|
+
const r = await this.browser.snapshot(signal, { sessionId });
|
|
3133
3191
|
return JSON.stringify(r);
|
|
3134
3192
|
}
|
|
3135
3193
|
if (name === 'browser_click' || name === 'browser_type' || name === 'browser_press' || name === 'browser_scroll' || name === 'browser_select') {
|
|
3136
3194
|
if (!this.browser || typeof this.browser.act !== 'function') {
|
|
3137
3195
|
return JSON.stringify({ ok: false, error: 'dahili tarayıcı kullanılamıyor' });
|
|
3138
3196
|
}
|
|
3139
|
-
const r = await this.browser.act(name.slice(8), args, signal);
|
|
3197
|
+
const r = await this.browser.act(name.slice(8), args, signal, { sessionId });
|
|
3140
3198
|
return JSON.stringify(r);
|
|
3141
3199
|
}
|
|
3142
3200
|
if (!(name === 'run_command' || name === 'read_file' || name === 'write_file' || name === 'list_dir' ||
|
|
@@ -3256,7 +3314,7 @@ const TOOLS = [
|
|
|
3256
3314
|
function: {
|
|
3257
3315
|
name: 'todo_write',
|
|
3258
3316
|
description:
|
|
3259
|
-
'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done.',
|
|
3317
|
+
'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done. DISCIPLINE: mark each step done THE MOMENT it is completed; NEVER end your reply while items are still pending/active — the system bounces unfinished lists back.',
|
|
3260
3318
|
parameters: {
|
|
3261
3319
|
type: 'object',
|
|
3262
3320
|
properties: {
|
package/src/agent/llm.js
CHANGED
|
@@ -135,7 +135,6 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
|
|
|
135
135
|
const toolCalls = [];
|
|
136
136
|
let usage = null;
|
|
137
137
|
let finishReason = null;
|
|
138
|
-
let sawDone = false;
|
|
139
138
|
|
|
140
139
|
const reader = res.body.getReader();
|
|
141
140
|
const decoder = new TextDecoder();
|
|
@@ -152,8 +151,7 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
|
|
|
152
151
|
buf = buf.slice(idx + 1);
|
|
153
152
|
if (!line.startsWith('data:')) continue;
|
|
154
153
|
const data = line.slice(5).trim();
|
|
155
|
-
if (!data) continue;
|
|
156
|
-
if (data === '[DONE]') { sawDone = true; continue; }
|
|
154
|
+
if (!data || data === '[DONE]') continue;
|
|
157
155
|
|
|
158
156
|
let json;
|
|
159
157
|
try {
|
|
@@ -187,21 +185,52 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
|
|
|
187
185
|
}
|
|
188
186
|
}
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
}
|
|
188
|
+
return { content, reasoning, toolCalls, usage, finishReason };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* usage toplama: devam turları gerçek faturalamayı yansıtsın (prompt yeniden sayılır) */
|
|
192
|
+
function sumUsage(a, b) {
|
|
193
|
+
if (!a) return b || null;
|
|
194
|
+
if (!b) return a;
|
|
195
|
+
const out = {};
|
|
196
|
+
for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
197
|
+
if (typeof a[k] === 'number' && typeof b[k] === 'number') out[k] = a[k] + b[k];
|
|
198
|
+
else out[k] = b[k] !== undefined ? b[k] : a[k];
|
|
202
199
|
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
203
202
|
|
|
204
|
-
|
|
203
|
+
/* OTOMATİK DEVAM: model cevabı çıkış limiti (finish_reason='length') yüzünden
|
|
204
|
+
kelime ortasında kesilirse — özellikle free modellerin düşük varsayılan
|
|
205
|
+
limitlerinde olur — yarım cümle kullanıcıya gitmesin. Kaldığı yerden devam
|
|
206
|
+
istenip metin birleştirilir (en fazla 2 devam turu). Araç çağrılı yanıtlarda
|
|
207
|
+
ve içerik hiç gelmediyse devreye girmez; orijinal mesaj dizisi DEĞİŞTİRİLMEZ. */
|
|
208
|
+
const CONTINUE_PROMPT =
|
|
209
|
+
'Yanıtın çıkış limiti nedeniyle ortadan kesildi. Kaldığın yerden DEVAM ET — baştan başlama, özetleme, açıklama yapma; yalnızca kesilen kısmın DEVAMINI yaz.';
|
|
210
|
+
|
|
211
|
+
async function chatStreamAuto(sel, body, opts = {}) {
|
|
212
|
+
const first = await chatStream(sel, body, opts);
|
|
213
|
+
if (first.finishReason !== 'length' || !first.content || (first.toolCalls && first.toolCalls.length)) {
|
|
214
|
+
return first;
|
|
215
|
+
}
|
|
216
|
+
let full = first.content;
|
|
217
|
+
let last = first;
|
|
218
|
+
for (let i = 0; i < 2; i++) {
|
|
219
|
+
const msgs2 = body.messages.slice();
|
|
220
|
+
msgs2.push({ role: 'assistant', content: full });
|
|
221
|
+
msgs2.push({ role: 'user', content: CONTINUE_PROMPT });
|
|
222
|
+
let cont;
|
|
223
|
+
try {
|
|
224
|
+
cont = await chatStream(sel, { ...body, messages: msgs2 }, opts);
|
|
225
|
+
} catch {
|
|
226
|
+
break; // devam turu patlarsa elimizdeki kısmi yanıtla döneriz
|
|
227
|
+
}
|
|
228
|
+
if (!cont.content || (cont.toolCalls && cont.toolCalls.length)) break;
|
|
229
|
+
full += cont.content;
|
|
230
|
+
last = { ...cont, content: full, usage: sumUsage(first.usage, cont.usage) };
|
|
231
|
+
if (cont.finishReason !== 'length') break;
|
|
232
|
+
}
|
|
233
|
+
return last;
|
|
205
234
|
}
|
|
206
235
|
|
|
207
236
|
async function chatOnce(sel, body, { signal, onRetry } = {}) {
|
|
@@ -232,4 +261,4 @@ async function chatOnce(sel, body, { signal, onRetry } = {}) {
|
|
|
232
261
|
};
|
|
233
262
|
}
|
|
234
263
|
|
|
235
|
-
module.exports = { chatStream, chatOnce, withRetries, friendlyError };
|
|
264
|
+
module.exports = { chatStream, chatStreamAuto, chatOnce, withRetries, friendlyError, sumUsage };
|
|
@@ -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 };
|