beast-agent 0.25.2 → 0.26.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "0.25.2",
4
+ "version": "0.26.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -54,6 +54,7 @@
54
54
  "pdfkit": "^0.20.1",
55
55
  "qrcode": "^1.5.4",
56
56
  "tesseract.js": "^7.0.0",
57
+ "undici": "^7.29.0",
57
58
  "ws": "^8.21.3",
58
59
  "electron": "40.10.2"
59
60
  },
package/src/agent/llm.js CHANGED
@@ -135,6 +135,7 @@ 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;
138
139
 
139
140
  const reader = res.body.getReader();
140
141
  const decoder = new TextDecoder();
@@ -151,7 +152,8 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
151
152
  buf = buf.slice(idx + 1);
152
153
  if (!line.startsWith('data:')) continue;
153
154
  const data = line.slice(5).trim();
154
- if (!data || data === '[DONE]') continue;
155
+ if (!data) continue;
156
+ if (data === '[DONE]') { sawDone = true; continue; }
155
157
 
156
158
  let json;
157
159
  try {
@@ -185,6 +187,20 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReas
185
187
  }
186
188
  }
187
189
 
190
+ /* YARIDA KESİLEN AKIŞ: stream erişilmeyen bir yerde koptuysa reader sessizce
191
+ done=true döner — yarım metni "tamamlanmış" sanmak yerine işaretle.
192
+ ([DONE] geldi ama finish_reason yok = sağlayıcı tuhaflığı → kabul) */
193
+ if (!finishReason && !sawDone) {
194
+ if (toolCalls.length) {
195
+ const e = new Error('cevap akışı yarıda kesildi (bağlantı koptu) — araç çağrısı tamamlanamadı, tekrar deneyin');
196
+ e.partialStream = true;
197
+ throw e;
198
+ }
199
+ if (content) {
200
+ content += '\n\n⚠ _akış yarıda kesildi (bağlantı koptu) — devam etmesini istersen tekrar yaz._';
201
+ }
202
+ }
203
+
188
204
  return { content, reasoning, toolCalls, usage, finishReason };
189
205
  }
190
206
 
package/src/main.js CHANGED
@@ -2327,6 +2327,10 @@ function ensureDesktopShortcut() {
2327
2327
  }
2328
2328
 
2329
2329
  app.whenReady().then(() => {
2330
+ try { applyProxy(); } catch {} // proxy ayarı yüklüyse tüm çıkışa uygula
2331
+ if (settings.warp && settings.warp.enabled) {
2332
+ startWarp().catch((e) => log.info('main', 'warp açılışta başlatılamadı: ' + String((e && e.message) || e)));
2333
+ }
2330
2334
  // Tailscale modu: paketli uygulamada Windows ile otomatik başlat (sessiz, tepside)
2331
2335
  if (app.isPackaged) {
2332
2336
  /* DAĞITIM KARARI: EXE/portable kurulum desteklenmiyor — tek yol npm.
@@ -2398,6 +2402,7 @@ app.whenReady().then(() => {
2398
2402
  app.on('before-quit', () => {
2399
2403
  app.isQuitting = true;
2400
2404
  flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
2405
+ try { stopWarp(); } catch {} // warp-plus yardımcısını da kapat
2401
2406
  });
2402
2407
 
2403
2408
  if (process.argv.includes('--smoke')) {
@@ -3745,6 +3750,20 @@ function flushDesktopOnDone(ev) {
3745
3750
  const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
3746
3751
  const NET_CHECK_MS = 8000;
3747
3752
  const NET_CHECK_TIMEOUT = 4000;
3753
+ const NET_PROBE_URLS = ['https://1.1.1.1/cdn-cgi/trace', 'https://www.google.com/generate_204'];
3754
+ let __netFailStreak = 0;
3755
+
3756
+ /* Gerçek HTTP yoklaması: DNS yerine sayfa çekmeyi dener — proxy açıksa proxy
3757
+ üzerinden gider (kullanıcı trafiğinin gördüğü interneti ölçer) */
3758
+ async function httpProbe() {
3759
+ for (const u of NET_PROBE_URLS) {
3760
+ try {
3761
+ const res = await (__origFetch || fetch)(u, { signal: AbortSignal.timeout(5000) });
3762
+ if (res.ok) return true;
3763
+ } catch {}
3764
+ }
3765
+ return false;
3766
+ }
3748
3767
  const CHAT_QUEUE_MAX = 50; // kuyruk üst sınırı — taşarsa en eski düşer
3749
3768
 
3750
3769
  let netOnline = true; // son bilinen bağlantı durumu (başlangıçta iyimser)
@@ -3853,8 +3872,21 @@ async function netCheck() {
3853
3872
  for (const h of NET_CHECK_HOSTS) {
3854
3873
  if (await dnsProbe(h)) { ok = true; break; }
3855
3874
  }
3875
+ if (!ok) {
3876
+ /* DNS probe yanıltıcı olabilir (proxy, DoH, kurumsal ağ, VPN):
3877
+ HTTP yoklamasıyla DOĞRULA — tarayıcının gördüğü internete yakın sinyal */
3878
+ ok = await httpProbe();
3879
+ }
3856
3880
  const first = !netCheckedOnce;
3857
3881
  const was = netOnline;
3882
+ /* Histerezis: tek-iki başarısız yoklama (wifi tıkı, anlık dns) durumu
3883
+ bozmasın — üst üste 3 yoklama (≈24+ sn) başarısızsa offline ilan et */
3884
+ if (!ok) {
3885
+ __netFailStreak++;
3886
+ if (netOnline && __netFailStreak < 3) { netCheckedOnce = true; return; }
3887
+ } else {
3888
+ __netFailStreak = 0;
3889
+ }
3858
3890
  netOnline = ok;
3859
3891
  netCheckedOnce = true;
3860
3892
  if (was !== ok || first) {
@@ -4104,6 +4136,294 @@ ipcMain.handle('wa:groups:set', (_e, cfg) => {
4104
4136
  return settings.waGroups;
4105
4137
  });
4106
4138
 
4139
+ /* ---------------- Proxy (Ayarlar → Proxy) ----------------
4140
+ Kullanıcının verdiği proxy(ler) üzerinden çıkış: LLM API'leri, http_fetch,
4141
+ TinyFish/Exa, python motorları (env) ve dahili tarayıcı (Chromium proxyRules).
4142
+ Çoklu adres = Node fetch tarafında round-robin rotasyon. SOCKS5 yalnız tarayıcıda. */
4143
+ let __proxyIdx = 0;
4144
+ let __proxyAgents = null; // url -> undici.ProxyAgent
4145
+ let __origFetch = null;
4146
+
4147
+ function proxyUrlList() {
4148
+ return String((settings.proxy && settings.proxy.urls) || '')
4149
+ .split(/[\n,;]+/)
4150
+ .map((s) => s.trim())
4151
+ .filter((s) => /^(https?|socks5):\/\//i.test(s));
4152
+ }
4153
+
4154
+ function proxyAgentFor(url) {
4155
+ const undici = require('undici');
4156
+ if (!__proxyAgents) __proxyAgents = new Map();
4157
+ if (!__proxyAgents.has(url)) {
4158
+ if (/^socks5:/i.test(url)) __proxyAgents.set(url, new undici.Agent({ connect: socks5Connector(url), connections: 64 }));
4159
+ else __proxyAgents.set(url, new undici.ProxyAgent(url));
4160
+ }
4161
+ return __proxyAgents.get(url);
4162
+ }
4163
+
4164
+ /* Minimal SOCKS5 CONNECT connector (undici Agent.connect imzası).
4165
+ WARP (warp-plus) dahil tüm socks5 proxy'leri Node fetch'te çalıştırır. */
4166
+ function socks5Connector(socksUrl) {
4167
+ const net = require('net');
4168
+ const tls = require('tls');
4169
+ const u = new URL(socksUrl);
4170
+ const shost = u.hostname;
4171
+ const sport = Number(u.port) || 1080;
4172
+ const suser = decodeURIComponent(u.username || '');
4173
+ const spass = decodeURIComponent(u.password || '');
4174
+ return function connect(opts, cb) {
4175
+ let stage = 0; // 0 greet · 1 auth · 2 conn
4176
+ let buf = Buffer.alloc(0);
4177
+ const socket = net.connect({ host: shost, port: sport });
4178
+ const fail = (err) => { try { socket.destroy(); } catch {} cb(err); };
4179
+ const onData = (d) => {
4180
+ buf = Buffer.concat([buf, d]);
4181
+ if (stage === 0) {
4182
+ if (buf.length < 2) return;
4183
+ if (buf[0] !== 5) return fail(new Error('socks5: geçersiz yanıt'));
4184
+ const method = buf[1];
4185
+ buf = buf.subarray(2);
4186
+ if (method === 2) {
4187
+ stage = 1;
4188
+ const ub = Buffer.from(suser), pb = Buffer.from(spass);
4189
+ socket.write(Buffer.concat([Buffer.from([1, ub.length]), ub, Buffer.from([pb.length]), pb]));
4190
+ } else if (method === 0) sendReq();
4191
+ else return fail(new Error('socks5: desteklenmeyen kimlik doğrulama'));
4192
+ } else if (stage === 1) {
4193
+ if (buf.length < 2) return;
4194
+ buf = buf.subarray(2);
4195
+ sendReq();
4196
+ } else if (stage === 2) {
4197
+ if (buf.length < 5) return;
4198
+ if (buf[0] !== 5 || buf[1] !== 0) return fail(new Error('socks5: bağlantı reddedildi (' + buf[1] + ')'));
4199
+ const need = 6 + (buf[3] === 1 ? 4 : buf[3] === 4 ? 16 : 1 + buf[4]);
4200
+ if (buf.length < need) return;
4201
+ buf = Buffer.alloc(0);
4202
+ socket.off('data', onData);
4203
+ if (opts.protocol === 'https:') {
4204
+ const tlsSock = tls.connect({ socket, servername: opts.servername || opts.hostname });
4205
+ tlsSock.on('error', fail);
4206
+ tlsSock.once('secureConnect', () => cb(null, tlsSock));
4207
+ } else cb(null, socket);
4208
+ }
4209
+ };
4210
+ socket.on('error', fail);
4211
+ socket.on('connect', () => {
4212
+ /* SOCKS5 greeting: şifre varsa user/pass(2), yoksa no-auth(0) yöntemi öner */
4213
+ socket.write(suser ? Buffer.from([5, 2, 0, 2]) : Buffer.from([5, 1, 0]));
4214
+ });
4215
+ socket.on('data', onData);
4216
+ function sendReq() {
4217
+ stage = 2;
4218
+ const host = opts.hostname || opts.host;
4219
+ const port = Number(opts.port) || (opts.protocol === 'https:' ? 443 : 80);
4220
+ const hb = Buffer.from(String(host), 'utf8');
4221
+ socket.write(Buffer.concat([Buffer.from([5, 1, 0, 3, hb.length]), hb, Buffer.from([(port >> 8) & 255, port & 255])]));
4222
+ }
4223
+ };
4224
+ }
4225
+
4226
+ function applyProxy() {
4227
+ const enabled = !!(settings.proxy && settings.proxy.enabled);
4228
+ const allUrls = enabled ? proxyUrlList() : [];
4229
+ const httpUrls = allUrls.filter((u) => /^https?:\/\//i.test(u));
4230
+
4231
+ /* 1) Node fetch (LLM API + http_fetch + TinyFish/Exa + GitHub...) — global sarmalayıcı.
4232
+ http(s) VE socks5 (socks5Connector) destekli — http(s) proxy'lerde rotasyon var. */
4233
+ if (enabled && allUrls.length) {
4234
+ if (!__origFetch) {
4235
+ __origFetch = globalThis.fetch;
4236
+ globalThis.fetch = (input, init = {}) => {
4237
+ const list = proxyUrlList();
4238
+ if (!list.length) return __origFetch(input, init);
4239
+ const p = list[Math.abs(__proxyIdx++) % list.length];
4240
+ return __origFetch(input, { ...init, dispatcher: proxyAgentFor(p) });
4241
+ };
4242
+ }
4243
+ } else if (__origFetch) {
4244
+ globalThis.fetch = __origFetch;
4245
+ __origFetch = null;
4246
+ }
4247
+
4248
+ /* 2) python/cmd çocuk süreçleri (ddgs, requests... env'den okur) */
4249
+ if (enabled && httpUrls.length) {
4250
+ process.env.HTTP_PROXY = httpUrls[0];
4251
+ process.env.HTTPS_PROXY = httpUrls[0];
4252
+ process.env.ALL_PROXY = httpUrls[0];
4253
+ process.env.NO_PROXY = 'localhost,127.0.0.1';
4254
+ } else {
4255
+ delete process.env.HTTP_PROXY;
4256
+ delete process.env.HTTPS_PROXY;
4257
+ delete process.env.ALL_PROXY;
4258
+ delete process.env.NO_PROXY;
4259
+ }
4260
+
4261
+ /* 3) dahili tarayıcı (Chromium; socks5 destekler) */
4262
+ try {
4263
+ const ses = session.fromPartition('persist:browser');
4264
+ if (ses) {
4265
+ if (enabled && allUrls.length) ses.setProxy({ proxyRules: allUrls[0] }).catch(() => {});
4266
+ else ses.setProxy({ mode: 'direct' }).catch(() => {});
4267
+ }
4268
+ } catch {}
4269
+ }
4270
+
4271
+ ipcMain.handle('proxy:get', () => ({
4272
+ enabled: !!(settings.proxy && settings.proxy.enabled),
4273
+ urls: String((settings.proxy && settings.proxy.urls) || ''),
4274
+ }));
4275
+
4276
+ ipcMain.handle('proxy:set', (_e, cfg) => {
4277
+ settings.proxy = {
4278
+ enabled: !!(cfg && cfg.enabled),
4279
+ urls: String((cfg && cfg.urls) || '').slice(0, 2000),
4280
+ };
4281
+ saveSettings();
4282
+ applyProxy();
4283
+ return { ok: true, enabled: settings.proxy.enabled, urls: settings.proxy.urls };
4284
+ });
4285
+
4286
+ ipcMain.handle('proxy:test', async () => {
4287
+ const allUrls = proxyUrlList();
4288
+ if (!allUrls.length) return { ok: false, error: 'proxy adresi girilmemiş' };
4289
+ const u = allUrls[Math.abs(__proxyIdx++) % allUrls.length];
4290
+ const t0 = Date.now();
4291
+ try {
4292
+ const res = await (__origFetch || fetch)('https://api.ipify.org?format=json', {
4293
+ dispatcher: proxyAgentFor(u),
4294
+ signal: AbortSignal.timeout(15000),
4295
+ });
4296
+ const j = await res.json();
4297
+ return { ok: true, ip: j.ip, proxy: u, ms: Date.now() - t0 };
4298
+ } catch (e) {
4299
+ return { ok: false, error: String((e && e.message) || e).slice(0, 160), proxy: u };
4300
+ }
4301
+ });
4302
+
4303
+ /* ---------------- Cloudflare WARP (warp-plus) ----------------
4304
+ Açık kaynak warp-plus tek binary: yerel socks5 (127.0.0.1:8086) açar, o port
4305
+ Cloudflare WARP ağına tünel olur. Bedava, üyeliksiz (profil otomatik oluşur).
4306
+ Beast: indirme + başlatma + watchdog + kapatma. Sadece Beast trafiği geçer. */
4307
+ const WARP_PORT = 8086;
4308
+ const WARP_PROXY_LINE = 'socks5://127.0.0.1:' + WARP_PORT;
4309
+ let warpChild = null;
4310
+ let warpWatchdog = null;
4311
+ let warpStopping = false;
4312
+
4313
+ function warpBinPath() { return path.join(APP_DIR, 'bin', 'warp-plus.exe'); }
4314
+ function warpDir() { return path.join(APP_DIR, 'warp'); }
4315
+ function warpEnabled() { return !!(settings.warp && settings.warp.enabled); }
4316
+
4317
+ async function ensureWarpPlusBinary() {
4318
+ const bin = warpBinPath();
4319
+ if (fs.existsSync(bin)) return bin;
4320
+ fs.mkdirSync(path.dirname(bin), { recursive: true });
4321
+ log.info('main', 'warp-plus indiriliyor…');
4322
+ const rel = await (__origFetch || fetch)('https://api.github.com/repos/bepass-org/warp-plus/releases/latest', {
4323
+ headers: { 'User-Agent': 'beast-agent', Accept: 'application/vnd.github+json' },
4324
+ signal: AbortSignal.timeout(20000),
4325
+ });
4326
+ if (!rel.ok) throw new Error('sürüm bilgisi alınamadı (' + rel.status + ')');
4327
+ const j = await rel.json();
4328
+ const asset = (j.assets || []).find((a) => /warp-plus_windows-amd64\.zip$/i.test(a.name || ''));
4329
+ if (!asset) throw new Error('windows paketi bulunamadı');
4330
+ const zipPath = bin + '.zip';
4331
+ const res = await (__origFetch || fetch)(asset.browser_download_url, { signal: AbortSignal.timeout(300000) });
4332
+ if (!res.ok) throw new Error('indirme başarısız (' + res.status + ')');
4333
+ fs.writeFileSync(zipPath, Buffer.from(await res.arrayBuffer()));
4334
+ await new Promise((resolve, reject) => {
4335
+ spawn('powershell.exe', ['-NoProfile', '-Command', `Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${path.dirname(bin)}'`])
4336
+ .on('exit', (c) => (c === 0 ? resolve() : reject(new Error('zip açılamadı'))))
4337
+ .on('error', reject);
4338
+ });
4339
+ try { fs.unlinkSync(zipPath); } catch {}
4340
+ if (!fs.existsSync(bin)) throw new Error('warp-plus.exe açılamadı');
4341
+ log.info('main', 'warp-plus indirildi: ' + bin);
4342
+ return bin;
4343
+ }
4344
+
4345
+ function warpPortAlive() {
4346
+ const net = require('net');
4347
+ return new Promise((resolve) => {
4348
+ const s = net.connect({ host: '127.0.0.1', port: WARP_PORT, timeout: 1200 }, () => { s.destroy(); resolve(true); });
4349
+ s.on('error', () => resolve(false));
4350
+ s.on('timeout', () => { s.destroy(); resolve(false); });
4351
+ });
4352
+ }
4353
+
4354
+ async function startWarp() {
4355
+ if (warpChild) return { ok: true };
4356
+ const bin = await ensureWarpPlusBinary();
4357
+ fs.mkdirSync(warpDir(), { recursive: true });
4358
+ try {
4359
+ warpChild = spawn(bin, [], { windowsHide: true, cwd: warpDir() });
4360
+ } catch (e) {
4361
+ return { ok: false, error: String((e && e.message) || e) };
4362
+ }
4363
+ warpChild.stdout.on('data', () => {});
4364
+ warpChild.stderr.on('data', () => {});
4365
+ warpChild.on('exit', () => {
4366
+ warpChild = null;
4367
+ if (warpEnabled() && !warpStopping) {
4368
+ /* watchdog: beklenmedik çıkışta 5 sn sonra yeniden başlat */
4369
+ clearTimeout(warpWatchdog);
4370
+ warpWatchdog = setTimeout(() => { startWarp().catch(() => {}); }, 5000);
4371
+ }
4372
+ });
4373
+ for (let i = 0; i < 180; i++) {
4374
+ if (await warpPortAlive()) { log.info('main', 'warp-plus hazır (127.0.0.1:' + WARP_PORT + ')'); return { ok: true }; }
4375
+ if (!warpChild) break;
4376
+ await new Promise((r) => setTimeout(r, 500));
4377
+ }
4378
+ return { ok: false, error: 'warp-plus başlatılamadı (port açılmadı)' };
4379
+ }
4380
+
4381
+ function stopWarp() {
4382
+ warpStopping = true;
4383
+ clearTimeout(warpWatchdog);
4384
+ try { if (warpChild) spawn('taskkill', ['/pid', String(warpChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
4385
+ try { if (warpChild) warpChild.kill(); } catch {}
4386
+ warpChild = null;
4387
+ setTimeout(() => { warpStopping = false; }, 1500);
4388
+ }
4389
+
4390
+ ipcMain.handle('warp:get', () => ({
4391
+ enabled: warpEnabled(),
4392
+ running: !!warpChild,
4393
+ installed: fs.existsSync(warpBinPath()),
4394
+ line: WARP_PROXY_LINE,
4395
+ }));
4396
+
4397
+ ipcMain.handle('warp:set', async (_e, on) => {
4398
+ const want = !!on;
4399
+ settings.warp = { enabled: want };
4400
+ saveSettings();
4401
+ if (want) {
4402
+ const r = await startWarp();
4403
+ if (!r.ok) {
4404
+ settings.warp = { enabled: false };
4405
+ saveSettings();
4406
+ return { ok: false, error: r.error || 'başlatılamadı' };
4407
+ }
4408
+ if (!settings.proxy) settings.proxy = { enabled: true, urls: '' };
4409
+ const lines = proxyUrlList();
4410
+ if (!lines.includes(WARP_PROXY_LINE)) lines.push(WARP_PROXY_LINE);
4411
+ settings.proxy.urls = lines.join('\n');
4412
+ settings.proxy.enabled = true; // WARP açıldıysa trafik ondan geçsin
4413
+ saveSettings();
4414
+ applyProxy();
4415
+ return { ok: true, enabled: true };
4416
+ }
4417
+ /* kapat: satırı listeden çıkar, başka proxy yoksa master'ı da kapat */
4418
+ const rest = proxyUrlList().filter((u) => u !== WARP_PROXY_LINE);
4419
+ settings.proxy.urls = rest.join('\n');
4420
+ if (!rest.length) settings.proxy.enabled = false;
4421
+ saveSettings();
4422
+ applyProxy();
4423
+ stopWarp();
4424
+ return { ok: true, enabled: false };
4425
+ });
4426
+
4107
4427
  ipcMain.handle('settings:get', () => {
4108
4428
  const out = JSON.parse(JSON.stringify(settings));
4109
4429
  /* Sırların renderera düz metin gitmesini engelle */
package/src/preload.js CHANGED
@@ -39,6 +39,11 @@ contextBridge.exposeInMainWorld('beast', {
39
39
  eventsSubs: () => ipcRenderer.invoke('events:subs:list'),
40
40
  eventUnsub: (id) => ipcRenderer.invoke('events:subs:remove', id),
41
41
  getSettings: () => ipcRenderer.invoke('settings:get'),
42
+ proxyGet: () => ipcRenderer.invoke('proxy:get'),
43
+ proxySet: (cfg) => ipcRenderer.invoke('proxy:set', cfg),
44
+ proxyTest: () => ipcRenderer.invoke('proxy:test'),
45
+ warpGet: () => ipcRenderer.invoke('warp:get'),
46
+ warpSet: (on) => ipcRenderer.invoke('warp:set', on),
42
47
  getFallout: () => ipcRenderer.invoke('fallout:get'),
43
48
  setFallout: (cfg) => ipcRenderer.invoke('fallout:set', cfg),
44
49
  getLimits: () => ipcRenderer.invoke('limits:get'),
@@ -61,6 +61,33 @@
61
61
  tab_websearch: 'Web Arama',
62
62
  tab_limits: 'Limit Ayarları',
63
63
  tab_security: 'Güvenlik',
64
+ tab_proxy: 'Proxy',
65
+ proxy_h2: 'Proxy',
66
+ proxy_sub: 'Tüm çıkış trafiğini (LLM API, dahili tarayıcı, web arama, http_fetch) proxy üzerinden geçir. Gizlilik, IP rotasyonu ve erişim engelleri için.',
67
+ proxy_enabled: 'Proxy aktif',
68
+ proxy_urls: 'Proxy listesi (çoklu satır = rotasyon)',
69
+ proxy_add: '+ Ekle',
70
+ proxy_host: 'IP / sunucu',
71
+ proxy_port: 'Port',
72
+ proxy_user: 'Kullanıcı (opsiyonel)',
73
+ proxy_pass: 'Şifre (opsiyonel)',
74
+ proxy_none: 'Henüz proxy eklenmedi — alanları doldurup + Ekle\u2019ye bas.',
75
+ proxy_invalid: 'IP ve Port zorunlu (port sayı olmalı)',
76
+ proxy_dup: 'Bu proxy zaten listede',
77
+ proxy_urls_ph: 'http://127.0.0.1:8080\nhttp://kullanici:sifre@sunucu:port\nsocks5://sunucu:1080 (socks5 yalnız dahili tarayıcı)',
78
+ proxy_save: 'Kaydet',
79
+ proxy_test: 'Test Et (çıkış IP)',
80
+ proxy_testing: 'Test ediliyor…',
81
+ proxy_test_ok: 'Bağlantı OK — çıkış IP:',
82
+ proxy_test_fail: 'Test başarısız:',
83
+ proxy_saved: 'Proxy ayarları kaydedildi',
84
+ proxy_note: 'Not: LLM API, http_fetch, TinyFish/Exa (http + socks5) ve dahili tarayıcı (http/socks5) destekler. Python motorları ve terminal süreçleri ortam değişkenleriyle yönlendirilir. Ayar anında uygulanır, yeniden başlatma gerekmez.',
85
+ warp_on: '☁ Cloudflare WARP Aç',
86
+ warp_off: '☁ Cloudflare WARP Kapat',
87
+ warp_state_on: 'çalışıyor ✓',
88
+ warp_state_starting: 'başlatılıyor…',
89
+ warp_state_off: 'kapalı',
90
+ warp_note: 'Bedava + üyeliksiz: Beast trafiğini Cloudflare ağından geçirir (yerel socks5). İlk açılışta ~10MB otomatik indirilir; kapatınca her şey normale döner.',
64
91
  tab_update: 'Güncelleme',
65
92
  up_h2: 'Güncelleme',
66
93
  up_sub: 'Sürümler GitHub Releases\u2019ten gelir. Otomatik kontrol açılışta ve 6 saatte bir çalışır.',
@@ -579,6 +606,33 @@
579
606
  tab_websearch: 'Web Search',
580
607
  tab_limits: 'Limits',
581
608
  tab_security: 'Security',
609
+ tab_proxy: 'Proxy',
610
+ proxy_h2: 'Proxy',
611
+ proxy_sub: 'Route all outgoing traffic (LLM APIs, built-in browser, web search, http_fetch) through a proxy. For privacy, IP rotation and access blocks.',
612
+ proxy_enabled: 'Proxy enabled',
613
+ proxy_urls: 'Proxy list (multiple lines = rotation)',
614
+ proxy_add: '+ Add',
615
+ proxy_host: 'IP / server',
616
+ proxy_port: 'Port',
617
+ proxy_user: 'Username (optional)',
618
+ proxy_pass: 'Password (optional)',
619
+ proxy_none: 'No proxy added yet — fill the fields and press + Add.',
620
+ proxy_invalid: 'IP and Port are required (port must be numeric)',
621
+ proxy_dup: 'This proxy is already in the list',
622
+ proxy_urls_ph: 'http://127.0.0.1:8080\nhttp://user:pass@server:port\nsocks5://server:1080 (socks5 built-in browser only)',
623
+ proxy_save: 'Save',
624
+ proxy_test: 'Test (exit IP)',
625
+ proxy_testing: 'Testing…',
626
+ proxy_test_ok: 'Connection OK — exit IP:',
627
+ proxy_test_fail: 'Test failed:',
628
+ proxy_saved: 'Proxy settings saved',
629
+ proxy_note: 'Note: LLM APIs, http_fetch, TinyFish/Exa (http + socks5) and the built-in browser (http/socks5) are supported. Python engines and terminal processes are routed via environment variables. Applied instantly — no restart needed.',
630
+ warp_on: '☁ Cloudflare WARP On',
631
+ warp_off: '☁ Cloudflare WARP Off',
632
+ warp_state_on: 'running ✓',
633
+ warp_state_starting: 'starting…',
634
+ warp_state_off: 'off',
635
+ warp_note: 'Free + no account: routes Beast traffic through the Cloudflare network (local socks5). First enable downloads ~10MB automatically; turning it off restores everything.',
582
636
  tab_update: 'Update',
583
637
  up_h2: 'Update',
584
638
  up_sub: 'Versions come from GitHub Releases. Automatic check runs on startup and every 6 hours.',
@@ -164,6 +164,7 @@
164
164
  <button class="tab" data-tab="dash" data-i18n="tab_dash">Dashboard</button>
165
165
  <button class="tab" data-tab="limits" data-i18n="tab_limits">Limit</button>
166
166
  <button class="tab" data-tab="sec" data-i18n="tab_security">Güvenlik</button>
167
+ <button class="tab" data-tab="proxy" data-i18n="tab_proxy">Proxy</button>
167
168
  <button class="tab" data-tab="update" data-i18n="tab_update">Güncelleme</button>
168
169
  </nav>
169
170
  <div class="set-tabs-foot">
@@ -191,6 +192,7 @@
191
192
  <div id="tab-dash" class="pane" hidden></div>
192
193
  <div id="tab-limits" class="pane" hidden></div>
193
194
  <div id="tab-sec" class="pane" hidden></div>
195
+ <div id="tab-proxy" class="pane" hidden></div>
194
196
  <div id="tab-update" class="pane" hidden></div>
195
197
  <div id="tab-cron" class="pane" hidden>
196
198
  <h2 data-i18n="cron_h2">Cron Görevler</h2>
@@ -635,7 +635,7 @@ function switchTab(name) {
635
635
  document.querySelectorAll('#setTabs .tab').forEach((b) =>
636
636
  b.classList.toggle('active', b.dataset.tab === name)
637
637
  );
638
- for (const p of ['provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'events', 'cron', 'usage', 'logs', 'dash', 'limits', 'sec', 'update']) {
638
+ for (const p of ['provider', 'fallout', 'skills', 'agents', 'tts', 'email', 'integrations', 'websearch', 'events', 'cron', 'usage', 'logs', 'dash', 'limits', 'sec', 'proxy', 'update']) {
639
639
  const el = $('#tab-' + p);
640
640
  if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
641
641
  }
@@ -646,6 +646,7 @@ function switchTab(name) {
646
646
  if (name === 'dash') renderDashboardPane();
647
647
  if (name === 'limits') renderLimitsPane();
648
648
  if (name === 'sec') renderSecurityPane();
649
+ if (name === 'proxy') renderProxyPane();
649
650
  if (name === 'update') renderUpdatePane(true);
650
651
  if (name === 'agents') refreshAgentsPane();
651
652
  if (name === 'websearch') renderWebSearchPane();
@@ -1790,8 +1791,134 @@ async function renderSecurityPane() {
1790
1791
  }
1791
1792
  }
1792
1793
 
1793
- /* ---------------- Update: sürüm kontrol + otomatik güncelleme ---------------- */
1794
+ /* ---------------- Proxy sekmesi ----------------
1795
+ Kullanıcı yalnız IP/port/kullanıcı/şifre alanlarını doldurur; URL'i biz kurarız.
1796
+ Eklenen her satır bir proxy — çoklu satır = Node tarafında round-robin rotasyon. */
1797
+ let proxyDraft = null; // { enabled, items:[line,...] } — sekme açıkken çalışma kopyası
1798
+
1799
+ function parseProxyLine(line) {
1800
+ const m = String(line || '').trim().match(/^(https?|socks5):\/\/(?:([^:@/]+):([^@/]*)@)?([^:/]+):(\d+)$/i);
1801
+ if (!m) return null;
1802
+ return { proto: m[1].toLowerCase(), user: decodeURIComponent(m[2] || ''), pass: decodeURIComponent(m[3] || ''), host: m[4], port: m[5] };
1803
+ }
1804
+
1805
+ function proxyLineDisplay(line) {
1806
+ const p = parseProxyLine(line);
1807
+ if (!p) return escapeHtml(line);
1808
+ const creds = p.user ? p.user + ':' + (p.pass ? '•••' : '') + '@' : '';
1809
+ return `${p.proto}://${escapeHtml(creds)}${escapeHtml(p.host)}:${p.port}`;
1810
+ }
1811
+
1812
+ async function renderProxyPane() {
1813
+ const pane = $('#tab-proxy');
1814
+ if (!pane) return;
1815
+ if (!proxyDraft) {
1816
+ const [cfg, ws] = await Promise.all([
1817
+ beast.proxyGet().catch(() => ({ enabled: false, urls: '' })),
1818
+ beast.warpGet().catch(() => ({ enabled: false, running: false, installed: false })),
1819
+ ]);
1820
+ proxyDraft = {
1821
+ enabled: !!cfg.enabled,
1822
+ items: String(cfg.urls || '').split(/[\n,;]+/).map((s) => s.trim()).filter(Boolean),
1823
+ warp: ws || {},
1824
+ };
1825
+ }
1826
+ const warp = proxyDraft.warp || {};
1827
+ const warpState = warp.enabled ? (warp.running ? _t('warp_state_on') : _t('warp_state_starting')) : _t('warp_state_off');
1828
+
1829
+ const listHtml = proxyDraft.items.length
1830
+ ? proxyDraft.items.map((line, i) =>
1831
+ `<div class="px-row"><code>${proxyLineDisplay(line)}</code><button class="px-del" data-i="${i}" title="${_t('proxy_remove')}">✕</button></div>`
1832
+ ).join('')
1833
+ : `<div class="gh-empty" style="padding:10px">${_t('proxy_none')}</div>`;
1834
+
1835
+ pane.innerHTML =
1836
+ '<h2>' + _t('proxy_h2') + '</h2>' +
1837
+ '<div class="sub">' + _t('proxy_sub') + '</div>' +
1838
+ '<div class="warp-card">' +
1839
+ `<div class="warp-head"><span>☁ Cloudflare WARP</span><span class="warp-state">${escapeHtml(warpState)}</span></div>` +
1840
+ `<button id="pxWarp" class="btn ${warp.enabled ? 'ghost' : ''}">${warp.enabled ? _t('warp_off') : _t('warp_on')}</button>` +
1841
+ `<div class="sub" style="margin-top:6px">${_t('warp_note')}</div>` +
1842
+ '</div>' +
1843
+ '<div class="fo-toggles" style="margin-top:12px">' +
1844
+ `<label class="lock-row"><input type="checkbox" id="pxOn" ${proxyDraft.enabled ? 'checked' : ''}/><span>${_t('proxy_enabled')}</span></label>` +
1845
+ '</div>' +
1846
+ `<label class="mem-label">${_t('proxy_urls')}</label>` +
1847
+ `<div id="pxList">${listHtml}</div>` +
1848
+ '<div class="px-add-row">' +
1849
+ `<select id="pxProto"><option value="http">http</option><option value="socks5">socks5</option></select>` +
1850
+ `<input id="pxHost" placeholder="${_t('proxy_host')}" spellcheck="false"/>` +
1851
+ `<input id="pxPort" placeholder="${_t('proxy_port')}" inputmode="numeric"/>` +
1852
+ `<input id="pxUser" placeholder="${_t('proxy_user')}" autocomplete="off" spellcheck="false"/>` +
1853
+ `<input id="pxPass" placeholder="${_t('proxy_pass')}" type="password" autocomplete="off"/>` +
1854
+ `<button id="pxAdd" class="btn" style="margin-top:0">${_t('proxy_add')}</button>` +
1855
+ '</div>' +
1856
+ '<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:10px">' +
1857
+ `<button id="pxSave" class="btn">${_t('proxy_save')}</button>` +
1858
+ `<button id="pxTest" class="btn ghost">${_t('proxy_test')}</button>` +
1859
+ '</div>' +
1860
+ `<div id="pxTestOut" class="sub" style="margin-top:8px"></div>` +
1861
+ '<div class="sub" style="margin-top:10px">' + _t('proxy_note') + '</div>';
1862
+
1863
+ pane.querySelector('#pxOn').addEventListener('change', (e) => { proxyDraft.enabled = e.target.checked; });
1794
1864
 
1865
+ pane.querySelector('#pxWarp').addEventListener('click', async () => {
1866
+ const btn = pane.querySelector('#pxWarp');
1867
+ const st = pane.querySelector('#pxWarpState');
1868
+ const want = !warp.enabled;
1869
+ btn.disabled = true;
1870
+ st.textContent = want ? '⏳ ' + _t('warp_state_starting') : '⏳ ' + _t('warp_state_off');
1871
+ const r = await beast.warpSet(want).catch((e) => ({ ok: false, error: String(e) }));
1872
+ if (!(r && r.ok)) {
1873
+ btn.disabled = false;
1874
+ st.textContent = '⚠ ' + ((r && r.error) || 'hata');
1875
+ return;
1876
+ }
1877
+ proxyDraft = null; // listeyi (socks satırı eklenmiş/çıkarılmış) tazele
1878
+ renderProxyPane();
1879
+ });
1880
+
1881
+ const collect = () => {
1882
+ const host = pane.querySelector('#pxHost').value.trim();
1883
+ const port = pane.querySelector('#pxPort').value.trim();
1884
+ const user = pane.querySelector('#pxUser').value.trim();
1885
+ const pass = pane.querySelector('#pxPass').value;
1886
+ const proto = pane.querySelector('#pxProto').value || 'http';
1887
+ if (!host || !/^\d{1,5}$/.test(port)) { toast(_t('proxy_invalid')); return null; }
1888
+ const creds = user ? encodeURIComponent(user) + (pass ? ':' + encodeURIComponent(pass) : '') + '@' : '';
1889
+ return `${proto}://${creds}${host}:${port}`;
1890
+ };
1891
+ pane.querySelector('#pxAdd').addEventListener('click', () => {
1892
+ const line = collect();
1893
+ if (!line) return;
1894
+ if (proxyDraft.items.includes(line)) { toast(_t('proxy_dup')); return; }
1895
+ proxyDraft.items.push(line);
1896
+ renderProxyPane();
1897
+ });
1898
+ pane.querySelectorAll('.px-del').forEach((b) =>
1899
+ b.addEventListener('click', () => {
1900
+ proxyDraft.items.splice(Number(b.dataset.i), 1);
1901
+ renderProxyPane();
1902
+ })
1903
+ );
1904
+
1905
+ pane.querySelector('#pxSave').addEventListener('click', async () => {
1906
+ const r = await beast.proxySet({ enabled: proxyDraft.enabled, urls: proxyDraft.items.join('\n') }).catch(() => null);
1907
+ if (r && r.ok) toast(_t('proxy_saved'));
1908
+ else toast((r && r.error) || 'hata');
1909
+ });
1910
+ pane.querySelector('#pxTest').addEventListener('click', async () => {
1911
+ const out = pane.querySelector('#pxTestOut');
1912
+ out.textContent = '⏳ ' + _t('proxy_testing');
1913
+ /* önce kaydet — test, kayıtlı ayarla çalışır */
1914
+ await beast.proxySet({ enabled: proxyDraft.enabled, urls: proxyDraft.items.join('\n') }).catch(() => null);
1915
+ const r = await beast.proxyTest().catch((e) => ({ ok: false, error: String(e) }));
1916
+ if (r && r.ok) out.innerHTML = `✅ ${_t('proxy_test_ok')} <b>${escapeHtml(r.ip || '?')}</b> · ${escapeHtml(r.proxy || '')} · ${r.ms || '?'}ms`;
1917
+ else out.innerHTML = `⚠ ${_t('proxy_test_fail')} ${escapeHtml((r && r.error) || '?')}`;
1918
+ });
1919
+ }
1920
+
1921
+ /* ---------------- Update: sürüm kontrol + otomatik güncelleme ---------------- */
1795
1922
  let updatePaneTimer = null;
1796
1923
 
1797
1924
  function renderUpdateStateHtml(st) {
@@ -2852,3 +2852,62 @@ body.browser-open #ghOverlay { right: var(--bw, 480px); }
2852
2852
  flex: none;
2853
2853
  text-align: center;
2854
2854
  }
2855
+
2856
+ /* proxy ekleme satırı + liste */
2857
+ .px-add-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 6px; }
2858
+ .px-add-row select,
2859
+ .px-add-row input {
2860
+ background: var(--panel);
2861
+ border: 1px solid var(--border);
2862
+ border-radius: 7px;
2863
+ color: var(--text);
2864
+ font-size: 12.5px;
2865
+ padding: 6px 9px;
2866
+ outline: none;
2867
+ }
2868
+ .px-add-row select { flex: 0 0 auto; }
2869
+ .px-add-row input:focus { border-color: var(--accent); }
2870
+ #pxHost { flex: 2 1 140px; }
2871
+ #pxPort { flex: 0 1 80px; }
2872
+ #pxUser, #pxPass { flex: 1 1 110px; }
2873
+ .px-row {
2874
+ display: flex;
2875
+ align-items: center;
2876
+ gap: 8px;
2877
+ border: 1px solid var(--border);
2878
+ background: var(--panel);
2879
+ border-radius: 8px;
2880
+ padding: 6px 10px;
2881
+ margin-bottom: 6px;
2882
+ }
2883
+ .px-row code {
2884
+ font-family: var(--mono, monospace);
2885
+ font-size: 12px;
2886
+ color: var(--text);
2887
+ overflow: hidden;
2888
+ text-overflow: ellipsis;
2889
+ white-space: nowrap;
2890
+ flex: 1;
2891
+ }
2892
+ .px-row .px-del {
2893
+ background: none;
2894
+ border: none;
2895
+ color: var(--muted);
2896
+ cursor: pointer;
2897
+ font-size: 13px;
2898
+ padding: 2px 6px;
2899
+ border-radius: 6px;
2900
+ flex: none;
2901
+ }
2902
+ .px-row .px-del:hover { color: var(--err); background: var(--panel2); }
2903
+
2904
+ /* WARP kartı */
2905
+ .warp-card {
2906
+ border: 1px solid var(--border);
2907
+ background: var(--panel);
2908
+ border-radius: 10px;
2909
+ padding: 12px 14px;
2910
+ margin-top: 12px;
2911
+ }
2912
+ .warp-head { display: flex; justify-content: space-between; align-items: center; font-weight: 800; margin-bottom: 8px; }
2913
+ .warp-state { color: var(--muted); font-weight: 400; font-size: 12px; }