beast-agent 0.27.0 → 0.29.0

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.
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ /* ---------- Obscura entegrasyonu ----------
4
+ Obscura (github.com/h4ckf0r0day/obscura): Rust tabanlı stealth headless
5
+ tarayıcı — anti-detect + V8 JS + gerçek rendering, Chromium'suz.
6
+ Beast kullanımı:
7
+ 1) Kurulum: releases'taki Windows stealth zip'i %APPDATA%\beast\obscura'ya
8
+ açılır (obscura.exe + obscura-worker.exe). İlk açılışta OTOMATİK kurulur.
9
+ 2) Arama: `obscura fetch https://html.duckduckgo.com/html/?q=...` çıktısı
10
+ DDG sonuç ayrıştırıcısıyla results'a döner — web_search zincirinin
11
+ sıralı bir motoru (Ayarlar → Web Arama'dan sıra değiştirilebilir). */
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const https = require('https');
17
+ const { execFile, spawn } = require('child_process');
18
+
19
+ const RELEASE_BASE = 'https://github.com/h4ckf0r0day/obscura/releases/latest/download/';
20
+ const WINDOWS_ZIP = 'obscura-x86_64-windows-stealth.zip';
21
+ const MAX_ZIP_BYTES = 250 * 1024 * 1024;
22
+ const FETCH_TIMEOUT_MS = 25000;
23
+ /* content-length gelmezse yüzde tahmini için yaklaşık boyut (stealth zip ≈74 MB) */
24
+ const EST_ZIP_BYTES = 74 * 1024 * 1024;
25
+
26
+ function obscuraDir() {
27
+ const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
28
+ return path.join(base, 'beast', 'obscura');
29
+ }
30
+
31
+ function obscuraExe() {
32
+ return path.join(obscuraDir(), 'obscura.exe');
33
+ }
34
+
35
+ function obscuraInstalled() {
36
+ try { return fs.existsSync(obscuraExe()); } catch { return false; }
37
+ }
38
+
39
+ /* kurulan varyant işareti: stealth zip'te --stealth bayrağı geçerli */
40
+ function markVariant(v) {
41
+ try { fs.writeFileSync(path.join(obscuraDir(), 'variant.txt'), String(v || 'stealth') + '\n', 'utf8'); } catch {}
42
+ }
43
+
44
+ function readVariant() {
45
+ try { return String(fs.readFileSync(path.join(obscuraDir(), 'variant.txt'), 'utf8')).trim() || 'stealth'; } catch { return 'stealth'; }
46
+ }
47
+
48
+ /* bina sağ mı: --help 10 sn içinde cevap veriyor mu (oturum başına tek deneme) */
49
+ let _okProbe = null;
50
+ function obscuraOk() {
51
+ if (!obscuraInstalled()) return Promise.resolve(false);
52
+ if (_okProbe) return _okProbe;
53
+ _okProbe = new Promise((resolve) => {
54
+ try {
55
+ execFile(obscuraExe(), ['--help'], { timeout: 10000, windowsHide: true }, (err) => resolve(!err));
56
+ } catch {
57
+ resolve(false);
58
+ }
59
+ });
60
+ return _okProbe;
61
+ }
62
+
63
+ function httpsDownload(url, redirectsLeft = 5, signal, onProgress) {
64
+ return new Promise((resolve, reject) => {
65
+ const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+obscura bootstrap)' } }, (res) => {
66
+ if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
67
+ res.resume();
68
+ return resolve(httpsDownload(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal, onProgress));
69
+ }
70
+ if (res.statusCode !== 200) {
71
+ res.resume();
72
+ return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
73
+ }
74
+ const total = Number(res.headers['content-length']) || EST_ZIP_BYTES;
75
+ const chunks = [];
76
+ let size = 0;
77
+ let lastPct = -1;
78
+ const tick = () => {
79
+ /* yüzdeyi INDIRME fazının %1-88 aralığına map et; %1'lik adımda bildir */
80
+ const pct = 1 + Math.min(88, Math.floor((size / total) * 88));
81
+ if (typeof onProgress === 'function' && pct !== lastPct) {
82
+ lastPct = pct;
83
+ try { onProgress({ pct, phase: 'indiriliyor', loaded: size, total }); } catch {}
84
+ }
85
+ };
86
+ res.on('data', (c) => {
87
+ size += c.length;
88
+ if (size > MAX_ZIP_BYTES) {
89
+ req.destroy(new Error('dosya çok büyük'));
90
+ return;
91
+ }
92
+ chunks.push(c);
93
+ tick();
94
+ });
95
+ res.on('end', () => {
96
+ tick();
97
+ resolve(Buffer.concat(chunks));
98
+ });
99
+ res.on('error', reject);
100
+ });
101
+ req.on('error', reject);
102
+ if (signal) {
103
+ if (signal.aborted) return reject(new Error('iptal'));
104
+ signal.addEventListener('abort', () => req.destroy(new Error('iptal')), { once: true });
105
+ }
106
+ });
107
+ }
108
+
109
+ function psExpand(zipPath, destDir) {
110
+ return new Promise((resolve) => {
111
+ const cmd = `Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${destDir}" -Force`;
112
+ const child = spawn(
113
+ 'powershell.exe',
114
+ ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd],
115
+ { windowsHide: true }
116
+ );
117
+ let err = '';
118
+ const finish = (ok) => {
119
+ clearTimeout(timer);
120
+ resolve(ok);
121
+ };
122
+ const timer = setTimeout(() => {
123
+ try { spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true }); } catch {}
124
+ finish(false);
125
+ }, 180000);
126
+ child.stderr.setEncoding('utf8');
127
+ child.stderr.on('data', (d) => { err += d; });
128
+ child.on('error', () => finish(false));
129
+ child.on('close', (code) => finish(code === 0 && !/Expand-Archive :/i.test(err)));
130
+ });
131
+ }
132
+
133
+ /* Obscura'yı kur / güncelle (yeniden indirip üzerine yazar).
134
+ onProgress({pct, phase}) — 1-88 indirme, 89-96 açma, 97-99 doğrulama. */
135
+ async function installObscura(signal, onProgress) {
136
+ const tick = (pct, phase) => {
137
+ if (typeof onProgress === 'function') {
138
+ try { onProgress({ pct, phase: phase || 'indiriliyor' }); } catch {}
139
+ }
140
+ };
141
+ try {
142
+ tick(0, 'hazırlanıyor');
143
+ const dest = obscuraDir();
144
+ fs.mkdirSync(dest, { recursive: true });
145
+ const zipPath = path.join(os.tmpdir(), 'beast-obscura.zip');
146
+ const buf = await httpsDownload(RELEASE_BASE + WINDOWS_ZIP, 5, signal, onProgress);
147
+ tick(89, 'kuruluyor');
148
+ fs.writeFileSync(zipPath, buf);
149
+ tick(90, 'kuruluyor');
150
+ const ok = await psExpand(zipPath, dest);
151
+ try { fs.unlinkSync(zipPath); } catch {}
152
+ tick(97, 'doğrulanıyor');
153
+ if (!ok || !obscuraInstalled()) {
154
+ return { ok: false, error: 'obscura kurulumu başarısız (zip açılamadı)' };
155
+ }
156
+ markVariant('stealth');
157
+ _okProbe = null; /* yeniden doğrula */
158
+ const good = await obscuraOk();
159
+ tick(good ? 100 : 98, good ? 'tamamlandı' : 'doğrulanıyor');
160
+ return { ok: good, dir: dest, error: good ? null : 'obscura.exe doğrulanamadı' };
161
+ } catch (e) {
162
+ return { ok: false, error: String((e && e.message) || e) };
163
+ }
164
+ }
165
+
166
+ /* obscura fetch — sayfa HTML'i (abort ile süreç kesilir) */
167
+ function obscuraFetchHtml(url, signal, timeoutMs = FETCH_TIMEOUT_MS) {
168
+ return new Promise((resolve, reject) => {
169
+ const args = ['fetch', url, '--dump', 'html', '--quiet', '--timeout', '20'];
170
+ if (readVariant() === 'stealth') args.push('--stealth');
171
+ let settled = false;
172
+ const finish = (err, html) => {
173
+ if (settled) return;
174
+ settled = true;
175
+ clearTimeout(timer);
176
+ if (signal) signal.removeEventListener('abort', onAbort);
177
+ err ? reject(err) : resolve(String(html || ''));
178
+ };
179
+ let child = null;
180
+ const onAbort = () => {
181
+ try {
182
+ if (child && child.pid) spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
183
+ } catch {}
184
+ finish(new Error('obscura fetch iptal edildi'));
185
+ };
186
+ const timer = setTimeout(() => {
187
+ try {
188
+ if (child && child.pid) spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true });
189
+ } catch {}
190
+ finish(new Error('obscura fetch zaman aşımı'));
191
+ }, timeoutMs);
192
+ if (signal) {
193
+ if (signal.aborted) return finish(new Error('iptal'));
194
+ signal.addEventListener('abort', onAbort, { once: true });
195
+ }
196
+ try {
197
+ child = execFile(
198
+ obscuraExe(),
199
+ args,
200
+ { timeout: timeoutMs, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
201
+ (err, stdout) => finish(err, stdout)
202
+ );
203
+ } catch (e) {
204
+ finish(e);
205
+ }
206
+ });
207
+ }
208
+
209
+ /* lite.duckduckgo sonuç ayrıştırıcı (tablo düzeni, result-link çıpaları) */
210
+ function parseDdgLite(html, limit = 8) {
211
+ const out = [];
212
+ const re = /<a\s+[^>]*class="[^"]*result-link[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
213
+ let m;
214
+ while ((m = re.exec(String(html || ''))) && out.length < limit) {
215
+ let href = m[1] || '';
216
+ try {
217
+ const u = new URL(href, 'https://duckduckgo.com');
218
+ const uddg = u.searchParams.get('uddg');
219
+ href = uddg ? decodeURIComponent(uddg) : u.toString();
220
+ } catch {}
221
+ const title = String(m[2] || '').replace(/<[^>]+>/g, '').trim();
222
+ if (!title || !/^https?:\/\//i.test(href)) continue;
223
+ if (out.some((r) => r.url === href)) continue;
224
+ out.push({ title, url: href, snippet: '' });
225
+ }
226
+ return out;
227
+ }
228
+
229
+ /* Obscura arama motoru: DDG html (önce) → DDG lite (yedek), stealth ile.
230
+ Döndüremiyorsa null — zincir sıradaki motora geçer. */
231
+ async function obscuraSearch(query, { maxResults = 8, signal } = {}) {
232
+ const q = String(query || '').trim().slice(0, 400);
233
+ if (!q || !obscuraInstalled()) return null;
234
+ const cap = Math.max(1, Math.min(12, Number(maxResults) || 8));
235
+ const targets = [
236
+ 'https://html.duckduckgo.com/html/?q=' + encodeURIComponent(q),
237
+ 'https://lite.duckduckgo.com/lite/?q=' + encodeURIComponent(q),
238
+ ];
239
+ for (const url of targets) {
240
+ let html = '';
241
+ try {
242
+ html = await obscuraFetchHtml(url, signal);
243
+ } catch {
244
+ continue;
245
+ }
246
+ if (!html) continue;
247
+ let results = [];
248
+ const { parseDdgResults } = require('./tools'); /* döngüsel require — çağrı anında */
249
+ if (/result__a/.test(html)) results = parseDdgResults(html, cap);
250
+ if (!results.length) results = parseDdgLite(html, cap);
251
+ if (results.length) {
252
+ return {
253
+ ok: true,
254
+ engine: 'obscura',
255
+ query: q,
256
+ count: results.length,
257
+ results: results.map((r) => ({ ...r, engine: 'obscura' })),
258
+ };
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+
264
+ module.exports = {
265
+ ENGINE: 'obscura',
266
+ RELEASE_BASE,
267
+ WINDOWS_ZIP,
268
+ obscuraDir,
269
+ obscuraExe,
270
+ obscuraInstalled,
271
+ obscuraOk,
272
+ installObscura,
273
+ obscuraFetchHtml,
274
+ obscuraSearch,
275
+ parseDdgLite,
276
+ };
@@ -164,19 +164,27 @@ Send-MailMessage -SmtpServer smtp.gmail.com -Port 587 -UseSsl -From 'adres@gmail
164
164
  force: true,
165
165
  body: `---
166
166
  name: python-web-search
167
- description: VARSAYILAN web arama zinciri — 1) dahili tarayıcı DİREK GOOGLE (gerçek Chromium, bot koruması yok; Google AI cevabi 'ai' alanında hazır gelir) 2) python çoklu-motor (ddgs / DDG+Bing+Mojeek, anahtarsız) 3) Exa (son çare, anahtar varsa). Takılmama kuralları içerir.
168
- version: 1.3.2
167
+ description: VARSAYILAN web arama zinciri — 1) dahili tarayıcı DİREK GOOGLE (gerçek Chromium, bot koruması yok; Google AI cevabi 'ai' alanında hazır gelir) 2) Obscura (stealth headless → DuckDuckGo; otomatik kurulur, varsayılan AKTİF) 3) TinyFish (anahtar varsa) 4) python çoklu-motor (ddgs / DDG+Bing+Mojeek, anahtarsız). Sıra Ayarlar Web Arama'dan değiştirilir. Takılmama kuralları içerir.
168
+ version: 1.4.0
169
169
  ---
170
170
 
171
171
  # Web Arama Zinciri (VARSAYILAN)
172
172
 
173
- web_search aracı çağrıldığında zincir OTOMATİK çalışır — elle motor seçmen gerekmez:
173
+ web_search aracı çağrıldığında zincir OTOMATİK çalışır — elle motor seçmen gerekmez. Sıra Ayarlar → Web Arama sekmesinden değiştirilebilir:
174
174
 
175
175
  ## 1. Dahili tarayıcı (ilk tercih — Google AI)
176
176
 
177
- Gerçek Chromium olduğu için Google'a bot koruması uygulamaz. Arama aep=1 ile açılır: Google'ın KENDİ AI cevabı yanıtın 'ai' alanında HAZIR gelir — "kimdir/nedir" sorularında önce onu kullan, kaynak linkleri de 'results' alanında gelir. Sağ panelde açılır, kullanıcı da görebilir.
177
+ Gerçek Chromium olduğu için Google'a bot koruması uygulamaz. Arama aep=1 ile açılır: Google'ın KENDİ AI cevabı yanıtın 'ai' alanında HAZIR gelir — "kimdir/nedir" sorularında önce onu kullan, kaynak linkleri de 'results' alanında gelir. Sağ panelde açılır, kullanıcı da görebilir. CAPTCHA/trafik uyarısı gelirse tarayıcı 10 dk atlanır, sıradaki motor devreye girer.
178
178
 
179
- ## 2. Python çoklu-motor (tarayıcı sonuç vermezse)
179
+ ## 2. Obscura (stealth headless — DuckDuckGo)
180
+
181
+ Rust tabanlı gizli tarayıcı: anti-detect parmak izi + V8 JS. Kurulu değilse uygulama açılışında OTOMATİK indirilir (%APPDATA%\\beast\\obscura). Varsayılan AKTİF — tarayıcı engellenirse bot korumasını aşarak DuckDuckGo'dan sonuç getirir.
182
+
183
+ ## 3. TinyFish API (anahtar varsa)
184
+
185
+ Ayarlar → Web Arama'dan anahtar girildiyse kendi sırasında devreye girer; anahtar yoksa otomatik atlanır.
186
+
187
+ ## 4. Python çoklu-motor
180
188
 
181
189
  ddgs kütüphanesi (github.com/deedy5/ddgs — anti-bot korumalı metasearch) veya DuckDuckGo + Bing + Mojeek PARALEL — anahtar gerekmez. Gömülü Python runtime otomatik yönetilir.
182
190
 
@@ -187,10 +195,6 @@ web_search {"query": "sorgu", "max_results": 6}
187
195
  - Çoklu sorgu gerekiyorsa: aynı turda birden ÇOK web_search çağrısını PARALEL ver — toplam süre tek arama kadar olur.
188
196
  - Sonuç listesinden 1-2 URL'nin içeriğini okumak için http_fetch kullan.
189
197
 
190
- ## 3. Exa (son çare)
191
-
192
- Yukarıdakiler boş dönerse ve Ayarlar → Web Arama'ya Exa anahtarı girildiyse devreye girer.
193
-
194
198
  ## Toplu / derin işler (python_run)
195
199
 
196
200
  - Haber başlıkları: python_run script=news.py args ["--limit","8","--json"]
@@ -208,7 +212,7 @@ for q in ["sorgu 1", "sorgu 2", "sorgu 3"]:
208
212
 
209
213
  - Bir bilgi 2-3 farklı sorguyla bulunamıyorsa PEŞİNİ BIRAK: bulduğun kısmi bilgiyle devam et, neyi bulamadığını raporda açıkça yaz.
210
214
  - Kapalı/gizli içerik (Instagram gizli hesap, private profil, login arkası veri) ARANMAZ — bulunamayacağı belliyse hemen alternatif kaynağa/geçişe geç.
211
- - DDG boş/engelli dönerse "DuckDuckGo kapandı" DEME: bot koruması devrededir (~1 saatte kendiliğinden açılır) ve zincir otomatik olarak Bing/Mojeek/tarayıcıya geçer — bu normal çalışmadır.
215
+ - DDG boş/engelli dönerse "DuckDuckGo kapandı" DEME: bot koruması devrededir (~1 saatte kendiliğinden açılır) ve zincir otomatik olarak Obscura/Bing/Mojeek/tarayıcıya geçer — bu normal çalışmadır.
212
216
  - Araştırma hedefi: ~3 dakika, 3-5 kaynak. Derinleşme = zaman kaybı.
213
217
  - Motor hata verirse bir kez farklı sorguyla dene, sonra elindekiyle raporu kapat.`,
214
218
  },
@@ -7,6 +7,7 @@ const https = require('https');
7
7
  const { execFile } = require('child_process');
8
8
  const { spawn } = require('child_process');
9
9
  const research = require('./research');
10
+ const obscura = require('./obscura');
10
11
 
11
12
  const MAX_CMD_OUTPUT = 16000;
12
13
  const MAX_FILE_CHARS = 200000;
@@ -627,7 +628,7 @@ const definitions = [
627
628
  function: {
628
629
  name: 'web_search',
629
630
  description:
630
- 'FAST web search with an automatic chain: built-in browser first (real Chromium searching GOOGLE directly with AI Mode — no bot protection; the response may include an `ai` field holding Google\'s own AI answer, ideal for "who/what is X" questions), then TinyFish API (if a key is configured, used IMMEDIATELY whenever the browser hits CAPTCHA/unusual traffic — the browser is then skipped for 10 minutes), then Python multi-engine (ddgs / DuckDuckGo+Bing+Mojeek parallel), then Exa API as last resort if a key is configured (Ayarlar Web Arama). Returns {ai?, results[{title,url,snippet}]}. Use when fresh or external info is needed; skip for things you already know.',
631
+ 'FAST web search with an automatic chain (order is configurable in Ayarlar → Web Arama): built-in browser (real Chromium searching GOOGLE directly with AI Mode — no bot protection; the response may include an `ai` field holding Google\'s own AI answer), Obscura stealth headless browser (anti-detect; searches DuckDuckGo, installed automatically and ACTIVE by default), TinyFish API (only if a key is configured), then Python multi-engine (ddgs / DuckDuckGo+Bing+Mojeek). If the browser hits CAPTCHA/unusual traffic it is skipped for 10 minutes and the next engine takes over. Returns {ai?, results[{title,url,snippet}]}. Use when fresh or external info is needed; skip for things you already know.',
631
632
  parameters: {
632
633
  type: 'object',
633
634
  properties: {
@@ -758,13 +759,9 @@ async function exec(name, args, ctx) {
758
759
  case 'web_search': {
759
760
  const q = String(args.query || '');
760
761
  const n = Number(args.max_results) || 8;
761
- /* önce ücretsiz python çoklu-motor; Exa SON ÇARE (dahili tarayıcı zincirin
762
- üstünde engine._execTool'da çalışır) */
763
- let r = await webSearchFast(q, { maxResults: n, signal: ctx.signal });
764
- if (!r || !r.ok || !(r.results || []).length) {
765
- const exa = await exaSearch(q, n, ctx.signal);
766
- if (exa) return JSON.stringify(exa);
767
- }
762
+ /* sıralı zincir (obscura/tinyfish/python dahili tarayıcı engine
763
+ tarafındaki hook'tan gelir); Exa KALDIRILDI */
764
+ const r = await searchChainWeb(q, n, { signal: ctx.signal });
768
765
  return JSON.stringify(r);
769
766
  }
770
767
  case 'deep_search': {
@@ -772,7 +769,7 @@ async function exec(name, args, ctx) {
772
769
  Gizli tarayıcı okuması engine.research hook'undan gelir (main process). */
773
770
  const r = await research.deepSearch(
774
771
  args,
775
- { search: (q) => webSearchFast(q, { maxResults: 10, signal: ctx.signal }) },
772
+ { search: (q) => searchChainWeb(q, 10, { signal: ctx.signal }) },
776
773
  ctx.signal
777
774
  );
778
775
  return JSON.stringify(r);
@@ -836,53 +833,101 @@ async function exec(name, args, ctx) {
836
833
  }
837
834
  }
838
835
 
839
- /* ---------- Exa (opsiyonel ücretli arama) ----------
840
- Ayarlar Web Arama'dan anahtar girilirse web_search ÖNCE Exa'yı dener;
841
- hata/sonuç yoksa ücretsiz python çoklu-motor devreye girer. */
842
- let _exaKey = null;
836
+ /* ---------- Sıralı arama zinciri (Ayarlar Web Arama'dan değiştirilir) ----------
837
+ Motorlar: browser (dahili Chromium Google) · obscura (stealth headless →
838
+ DuckDuckGo) · tinyfish (API, anahtar gerekir) · python (ddgs/DDG+Bing+Mojeek).
839
+ Sıra + aç/kapa ayarı settings.json'da (searchChain) saklanır; Obscura
840
+ varsayılan AKTİF. Tarayıcı CAPTCHA/trafik verirse 10 dk atlanır. */
841
+ const SEARCH_ENGINE_IDS = ['browser', 'obscura', 'tinyfish', 'python'];
842
+ const DEFAULT_SEARCH_CHAIN = SEARCH_ENGINE_IDS.map((id) => ({ id, on: true }));
843
+
844
+ let _searchChain = DEFAULT_SEARCH_CHAIN.map((x) => ({ ...x }));
845
+ let _obscuraEnabled = true; /* Obscura varsayılan AKTİF */
846
+ let _browserBanUntil = 0;
847
+
848
+ function normalizeSearchChain(list) {
849
+ const arr = Array.isArray(list) ? list : [];
850
+ const rows = [];
851
+ const seen = new Set();
852
+ for (const item of arr) {
853
+ const id = String((item && item.id) || item || '').trim();
854
+ if (!SEARCH_ENGINE_IDS.includes(id) || seen.has(id)) continue;
855
+ seen.add(id);
856
+ rows.push({ id, on: !(item && item.on === false) });
857
+ }
858
+ for (const id of SEARCH_ENGINE_IDS) {
859
+ if (!seen.has(id)) rows.push({ id, on: true }); /* listede eksik motor varsayılan AÇIK */
860
+ }
861
+ /* obscura varsayılan AKTİF; zincir boş kalmasın: hepsi kapalıysa browser'ı aç */
862
+ if (!rows.some((r) => r.on)) {
863
+ const b = rows.find((r) => r.id === 'browser');
864
+ if (b) b.on = true;
865
+ }
866
+ return rows;
867
+ }
843
868
 
844
- function setExaKey(key) {
845
- _exaKey = String(key || '').trim() || null;
869
+ function setSearchChain(list) {
870
+ _searchChain = normalizeSearchChain(list);
871
+ return _searchChain;
846
872
  }
847
873
 
848
- async function exaSearch(query, maxResults, signal) {
849
- if (!_exaKey) return null;
850
- try {
851
- const sig =
852
- signal && typeof AbortSignal !== 'undefined' && AbortSignal.any
853
- ? AbortSignal.any([signal, AbortSignal.timeout(20000)])
854
- : AbortSignal.timeout(20000);
855
- const r = await fetch('https://api.exa.ai/search', {
856
- method: 'POST',
857
- headers: { 'x-api-key': _exaKey, 'Content-Type': 'application/json' },
858
- body: JSON.stringify({
859
- query,
860
- numResults: Math.max(1, Math.min(12, Number(maxResults) || 8)),
861
- contents: { text: { maxCharacters: 1200 } },
862
- }),
863
- signal: sig,
864
- });
865
- if (!r.ok) return null;
866
- const j = await r.json();
867
- const results = (j.results || [])
868
- .map((x) => ({
869
- title: String(x.title || ''),
870
- url: String(x.url || ''),
871
- snippet: String(x.text || '').replace(/\s+/g, ' ').slice(0, 400),
872
- engine: 'exa',
873
- }))
874
- .filter((x) => x.url && x.title);
875
- if (!results.length) return null;
876
- return { ok: true, engine: 'exa', query, results };
877
- } catch {
878
- return null;
874
+ function setSearchObscuraEnabled(v) {
875
+ _obscuraEnabled = v !== false;
876
+ }
877
+
878
+ function getSearchChain() {
879
+ return _searchChain.map((x) => ({ ...x }));
880
+ }
881
+
882
+ function browserBanned() {
883
+ return Date.now() < _browserBanUntil;
884
+ }
885
+
886
+ function banBrowser(minutes = 10) {
887
+ _browserBanUntil = Date.now() + minutes * 60 * 1000;
888
+ }
889
+
890
+ /* Zinciri sırayla koştur: her motor boş/hata dönerse sıradakine geç.
891
+ browser motoru yalnız engine'den gelir (hook); araç-bağımsız çağrılarda atlanır. */
892
+ async function searchChainWeb(query, maxResults, { signal, browser } = {}) {
893
+ const q = String(query || '').trim().slice(0, 400);
894
+ if (!q) return { ok: false, error: 'boş sorgu' };
895
+ const cap = Math.max(1, Math.min(12, Number(maxResults) || 8));
896
+ let out = null;
897
+ const banned = browserBanned();
898
+ for (const row of _searchChain) {
899
+ if (out && out.ok && (out.results || []).length) break;
900
+ if (!row.on) continue;
901
+ try {
902
+ if (row.id === 'browser') {
903
+ if (typeof browser !== 'function' || banned) continue;
904
+ out = await browser();
905
+ if (!out || !out.ok || !(out.results || []).length) {
906
+ banBrowser(); /* tarayıcı sorunlu — 10 dk atla, alternatifler devrede */
907
+ out = null;
908
+ }
909
+ } else if (row.id === 'obscura') {
910
+ if (!_obscuraEnabled) continue;
911
+ out = await obscura.obscuraSearch(q, { maxResults: cap, signal });
912
+ } else if (row.id === 'tinyfish') {
913
+ out = await tinyfishSearch(q, cap, signal);
914
+ } else if (row.id === 'python') {
915
+ out = await webSearchFast(q, { maxResults: cap, signal });
916
+ }
917
+ } catch {
918
+ out = null;
919
+ }
920
+ if (out && out.ok && !(out.results || []).length) out = null; /* boş → sıradaki motor */
921
+ }
922
+ if (out && out.ok && banned && (out.results || []).length) {
923
+ out.note = 'dahili tarayıcı 10 dk askıda (CAPTCHA/trafik) — alternatif motor kullanıldı';
879
924
  }
925
+ return out || { ok: false, error: 'web arama başarısız — tüm motorlar boş döndü' };
880
926
  }
881
927
 
882
- /* ---------- TinyFish (ücretsiz arama; anahtar girilirse ZİNCİRİN BAŞI) ----------
928
+ /* ---------- TinyFish (anahtar girilirse zincirdeki kendi sırasında) ----------
883
929
  GET https://api.search.tinyfish.ai?query=... · Header: X-API-Key
884
- Ayarlar Web Arama'dan anahtar girilirse web_search ÖNCE TinyFish'i dener;
885
- hata/sonuç yoksa eski zincir (tarayıcı → python çoklu-motor → Exa) devam eder. */
930
+ Anahtar yoksa bu motor otomatik atlanır; sıradaki motor devreye girer. */
886
931
  let _tinyfishKey = null;
887
932
 
888
933
  function setTinyfishKey(key) {
@@ -936,10 +981,19 @@ module.exports = {
936
981
  webSearchFast,
937
982
  seedScript,
938
983
  bundledScriptPath,
939
- setExaKey,
940
- exaSearch,
984
+ /* arama zinciri (sıra + aç/kapa Ayarlar → Web Arama'dan) */
985
+ SEARCH_ENGINE_IDS,
986
+ DEFAULT_SEARCH_CHAIN,
987
+ setSearchChain,
988
+ setSearchObscuraEnabled,
989
+ getSearchChain,
990
+ searchChainWeb,
991
+ browserBanned,
992
+ banBrowser,
941
993
  setTinyfishKey,
942
994
  tinyfishSearch,
995
+ /* obscura geçişi */
996
+ obscura,
943
997
  /* python altyapısı */
944
998
  ensurePython,
945
999
  findSystemPython,