beast-agent 2.58.0 → 2.58.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "2.58.0",
4
+ "version": "2.58.2",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -811,9 +811,13 @@ class Engine {
811
811
  payload = squeeze.apply(payload, { cacheKey: String(session.id || '') });
812
812
  }
813
813
  const cands = this._chatCandidates(sel, wasVision);
814
+ /* AYLIK KOTASI DOLAN SAĞLAYICILAR: aynı sağlayıcının diğer modellerini
815
+ denemek boşuna (kota hesap/workspace bazlı) — bu turda atlanır. */
816
+ const quotaOut = new Set();
814
817
  let lastErr = null;
815
818
  for (let i = 0; i < cands.length; i++) {
816
819
  const c = cands[i];
820
+ if (quotaOut.has(String(c.providerId))) continue;
817
821
  let retried413 = false;
818
822
  try {
819
823
  /* provider bazlı girdi limiti: payload aşıyorsa sıkıştırılıp öyle gönderilir.
@@ -903,6 +907,15 @@ class Engine {
903
907
  if (e && (e.name === 'AbortError' || (signal && signal.aborted))) throw e;
904
908
  lastErr = e;
905
909
  const msgStr = String((e && e.message) || '');
910
+ /* AYLIK KOTA: bu sağlayıcı tur boyunca kapalı — kullanıcıya net söyle,
911
+ FALLOUT varsa farklı sağlayıcıya geç */
912
+ if (e && e.usageLimit) {
913
+ quotaOut.add(String(c.providerId));
914
+ emitSafe(this, session.id, {
915
+ type: 'status',
916
+ status: `⛔ ${c.providerName}: aylık kullanım limiti doldu — aynı sağlayıcının diğer modelleri atlanıyor`,
917
+ });
918
+ }
906
919
  /* 413 TPM: sağlayıcı gerçek istek boyutunu söylüyor → kalibre et,
907
920
  aynı modeli yeniden sıkıştırıp BİR KEZ daha dene */
908
921
  const m413 = /Requested\s+([\d,]+)/i.exec(msgStr) || (e && e.status === 413 ? [, 0] : null);
@@ -925,7 +938,11 @@ class Engine {
925
938
  ([400, 404, 422].includes(e.status) ||
926
939
  /\bno endpoints\b/i.test(msgStr) ||
927
940
  /\bimage input\b/i.test(msgStr));
928
- const next = cands[i + 1];
941
+ /* sıradaki aday: kotası dolmuş sağlayıcılar atlanır */
942
+ let next = null;
943
+ for (let j = i + 1; j < cands.length; j++) {
944
+ if (!quotaOut.has(String(cands[j].providerId))) { next = cands[j]; break; }
945
+ }
929
946
  if (!next) break; // zincir bitti — hatayı yukarı fırlat
930
947
  if (imgIssue) {
931
948
  emitSafe(this, session.id, {
@@ -1417,15 +1434,21 @@ class Engine {
1417
1434
  files = fs.readdirSync(this.sessionsDir).filter((f) => f.endsWith('.jsonl'));
1418
1435
  } catch {}
1419
1436
  const out = [];
1437
+ const fin = [];
1420
1438
  for (const f of files) {
1421
1439
  const id = f.replace(/\.jsonl$/, '');
1422
1440
  const v = this._view(this._load(id));
1423
1441
  if (v.isBg) continue;
1424
1442
  if (v.isBotDm) continue; // botlar arası DM — yalnız admin DM Log ekranında
1425
- out.push(v);
1426
- }
1427
- out.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
1428
- return out.slice(0, 100);
1443
+ /* FİNANS GEÇMİŞİ KAYBOLMAZ: normal sohbet listesi 100 ile sınırlı olsa da
1444
+ finance sohbetleri AYRICA listelenir — 100+ oturumu olan kullanıcıda
1445
+ eski finance sohbetleri listeden düşüp "kaybolmuş" görünmez. */
1446
+ (v.finance ? fin : out).push(v);
1447
+ }
1448
+ const byNew = (a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt));
1449
+ out.sort(byNew);
1450
+ fin.sort(byNew);
1451
+ return out.slice(0, 100).concat(fin.slice(0, 60)).sort(byNew);
1429
1452
  }
1430
1453
 
1431
1454
  /* Panel oturumları: belirli bir bgTitle taşıyan gizli oturumlar (Beast Code,
package/src/agent/llm.js CHANGED
@@ -64,7 +64,27 @@ const HINTS = {
64
64
  504: 'sağlayıcı zaman aşımı',
65
65
  };
66
66
 
67
+ /* AYLIK KOTA / KULLANIM LİMİTİ: 429 gövdesindeki "usage limit / quota" sinyali
68
+ hız limitinden FARKLI — tekrar denemek çözmez, aynı sağlayıcının diğer
69
+ modelleri de kapalıdır. Bu ayrım motorun FALLOUT kararını da besler. */
70
+ function isUsageLimitDetail(detail) {
71
+ return /GoUsageLimit|usage[_ ]?limit|insufficient_quota|monthly limit|kullanım limiti/i.test(
72
+ String(detail || '')
73
+ );
74
+ }
75
+
67
76
  function friendlyError(status, statusText, detail) {
77
+ const d = String(detail || '');
78
+ if (status === 429 && isUsageLimitDetail(d)) {
79
+ let msg = 'HTTP 429 — AYLIK KULLANIM LİMİTİ DOLDU (hız limiti değil)';
80
+ const reset = /Resets?\s+in\s+([^."}]+)/i.exec(d);
81
+ if (reset) msg += ` — sıfırlanma: ${reset[1].trim()} sonra`;
82
+ const link = /(https?:\/\/[^\s"}]+)/.exec(d);
83
+ if (link) msg += `\nDevam için: ${link[1]}`;
84
+ msg +=
85
+ '\n(çözüm: sağlayıcı panelinden bakiyeden kullanımı aç, başka model/sağlayıcı seç ya da sıfırlanmayı bekle)';
86
+ return msg;
87
+ }
68
88
  const hint = HINTS[status];
69
89
  let msg = `HTTP ${status} ${statusText}`;
70
90
  if (hint) msg += ` — ${hint}`;
@@ -72,9 +92,33 @@ function friendlyError(status, statusText, detail) {
72
92
  if ([500, 502, 503, 504].includes(status)) {
73
93
  msg += '\n(geçici olabilir: birkaç saniye sonra tekrar dene ya da model seçiciyi açıp başka bir model dene)';
74
94
  }
95
+ if (isTransientDetail(status, d)) {
96
+ msg += '\n(sağlayıcı geçici hatası — otomatik tekrar denenir; sürerse başka model/sağlayıcı dene)';
97
+ }
75
98
  return msg;
76
99
  }
77
100
 
101
+ /* SAĞLAYICI GEÇİCİ HATASI: bazı sağlayıcılar upstream arızasını HTTP 400
102
+ gövdesinde "server_error" olarak döner — istemci hatası değildir, tekrar
103
+ denenmeli. Bu bayrak olmadan 400 kalıcı sayılıp model boşuna "çökmüş" olur. */
104
+ function isTransientDetail(status, detail) {
105
+ if (status !== 400 && status !== 500) return false;
106
+ return /"(type|code)"\s*:\s*"server_error"|Error from provider|Upstream (request failed|response was not valid JSON)|temporar/i.test(
107
+ String(detail || '')
108
+ );
109
+ }
110
+
111
+ /* HTTP hatasını zengin bayraklarla kur — usageLimit/transient bilgisi retry ve
112
+ FALLOUT katmanlarına taşınır (boşa tekrar/boşa çökme olmaz). */
113
+ function httpError(status, statusText, detail, headers) {
114
+ const err = new Error(friendlyError(status, statusText, detail));
115
+ err.status = status;
116
+ if (isUsageLimitDetail(detail)) err.usageLimit = true;
117
+ if (isTransientDetail(status, detail)) err.transient = true;
118
+ err.retryAfterMs = parseRetryAfter(headers && (headers.get('retry-after-ms') || headers.get('retry-after')));
119
+ return err;
120
+ }
121
+
78
122
  /* opencode cache disiplini (provider/transform.ts:1262-1276 port): destekleyen
79
123
  sağlayıcılara oturum-sabit önbellek anahtarı gönderilir — aynı önek tekrar
80
124
  kullanıldığında girdi tokenleri cache'ten okunur (~%90 ucuz + hızlı TTFT) */
@@ -302,9 +346,15 @@ async function withRetries(fn, { signal, onRetry } = {}) {
302
346
  return await fn();
303
347
  } catch (e) {
304
348
  const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
349
+ if (aborted) throw e;
350
+ /* AYLIK KOTA DOLDU: tekrar denemek boşa — hemen fırlat ki motor
351
+ FALLOUT ile sıradaki sağlayıcıya/modele geçebilsin. */
352
+ if (e && e.usageLimit) throw e;
305
353
  const status = e && e.status;
306
- const netErr = !aborted && status === undefined;
307
- const retriable = !aborted && (status === undefined || RETRYABLE_STATUS.has(status));
354
+ const netErr = status === undefined;
355
+ /* 400 gövdesinde "server_error/upstream" diyen sağlayıcı hataları geçicidir
356
+ (transient) — kalıcı 400 gibi davranıp modeli boşuna "çökertme". */
357
+ const retriable = status === undefined || RETRYABLE_STATUS.has(status) || !!(e && e.transient);
308
358
  if (!retriable) throw e;
309
359
  const cap = netErr ? NET_MAX_RETRIES : MAX_RETRIES;
310
360
  if (attempt >= cap) throw e;
@@ -409,12 +459,7 @@ async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, drops =
409
459
  try {
410
460
  detail = (await r.text()).slice(0, 300);
411
461
  } catch {}
412
- const err = new Error(friendlyError(r.status, r.statusText, detail));
413
- err.status = r.status;
414
- err.retryAfterMs = parseRetryAfter(
415
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
416
- );
417
- throw err;
462
+ throw httpError(r.status, r.statusText, detail, r.headers);
418
463
  }
419
464
  return r;
420
465
  },
@@ -590,12 +635,7 @@ async function chatOnce(sel, body, { signal, onRetry, onParamDrop, onEffortDowng
590
635
  try {
591
636
  detail = (await r.text()).slice(0, 300);
592
637
  } catch {}
593
- const err = new Error(friendlyError(r.status, r.statusText, detail));
594
- err.status = r.status;
595
- err.retryAfterMs = parseRetryAfter(
596
- r.headers && (r.headers.get('retry-after-ms') || r.headers.get('retry-after'))
597
- );
598
- throw err;
638
+ throw httpError(r.status, r.statusText, detail, r.headers);
599
639
  }
600
640
  return r;
601
641
  },
@@ -7,6 +7,7 @@
7
7
  - Aynı allow list mantığı: main tarafındaki tgFind() listesindeki kişilere cevap verir */
8
8
 
9
9
  const https = require('https');
10
+ const crypto = require('crypto');
10
11
 
11
12
  const API_BASE = 'https://api.telegram.org/bot';
12
13
  const SEND_CHUNK = 3800; // Telegram mesaj sınırı 4096 — güvenli pay
@@ -30,16 +31,15 @@ class TelegramBridge {
30
31
  this.emit({ type: 'status', status, user: user || null });
31
32
  }
32
33
 
33
- /* Bot API çağrısı — JSON POST, promise sarmalı */
34
- api(method, body = {}, opts = {}) {
34
+ /* Ham POST — JSON ve multipart (görsel) aynı iskeleti kullanır */
35
+ _post(method, payload, headers, timeoutMs) {
35
36
  return new Promise((resolve, reject) => {
36
- const payload = JSON.stringify(body);
37
37
  const req = https.request(
38
38
  `${API_BASE}${this.token}/${method}`,
39
39
  {
40
40
  method: 'POST',
41
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
42
- timeout: opts.timeout || 15000,
41
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) },
42
+ timeout: timeoutMs || 15000,
43
43
  },
44
44
  (res) => {
45
45
  let data = '';
@@ -64,6 +64,16 @@ class TelegramBridge {
64
64
  });
65
65
  }
66
66
 
67
+ /* Bot API çağrısı — JSON POST, promise sarmalı */
68
+ api(method, body = {}, opts = {}) {
69
+ return this._post(
70
+ method,
71
+ JSON.stringify(body),
72
+ { 'Content-Type': 'application/json' },
73
+ opts.timeout || 15000
74
+ );
75
+ }
76
+
67
77
  async start() {
68
78
  if (!this.token) {
69
79
  this._setStatus('error');
@@ -156,6 +166,44 @@ class TelegramBridge {
156
166
  }
157
167
  return true;
158
168
  }
169
+
170
+ /* GÖRSEL gönder — data URL (base64) multipart ile yüklenir. PNG/JPG/WEBP
171
+ sendPhoto, GIF sendDocument; caption 1024, dosya 9.5MB üstü atlanır.
172
+ threadId verilirse forum konusuna gider. */
173
+ async sendPhoto(chatId, dataUrl, caption, threadId) {
174
+ try {
175
+ const m = /^data:image\/(png|jpe?g|webp|gif);base64,([A-Za-z0-9+/=\s]+)$/i.exec(String(dataUrl || '').trim());
176
+ if (!m) return false;
177
+ const isJpg = /^jpe?g$/i.test(m[1]);
178
+ const ext = isJpg ? 'jpg' : m[1].toLowerCase();
179
+ const mime = isJpg ? 'image/jpeg' : 'image/' + ext;
180
+ const buf = Buffer.from(m[2].replace(/\s+/g, ''), 'base64');
181
+ if (!buf.length || buf.length > 9.5 * 1024 * 1024) return false;
182
+ const gif = ext === 'gif';
183
+ const method = gif ? 'sendDocument' : 'sendPhoto';
184
+ const field = gif ? 'document' : 'photo';
185
+ const boundary = '----Beast' + crypto.randomBytes(10).toString('hex');
186
+ const parts = [];
187
+ const addField = (name, value) => {
188
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${String(value)}\r\n`));
189
+ };
190
+ addField('chat_id', String(chatId));
191
+ if (caption) addField('caption', String(caption).slice(0, 1024));
192
+ if (threadId) addField('message_thread_id', String(Number(threadId)));
193
+ parts.push(
194
+ Buffer.from(
195
+ `--${boundary}\r\nContent-Disposition: form-data; name="${field}"; filename="beast.${ext}"\r\nContent-Type: ${mime}\r\n\r\n`
196
+ )
197
+ );
198
+ parts.push(buf);
199
+ parts.push(Buffer.from('\r\n'));
200
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
201
+ await this._post(method, Buffer.concat(parts), { 'Content-Type': 'multipart/form-data; boundary=' + boundary }, 60000);
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
159
207
  }
160
208
 
161
209
  module.exports = { TelegramBridge };
package/src/main.js CHANGED
@@ -2944,6 +2944,7 @@ async function handleTgIncoming(chatId, payload) {
2944
2944
  const allowed = tgFind(payload.senderId, payload.username);
2945
2945
  if (!allowed) {
2946
2946
  tgLog(`skip: grup mesajı chat=${chatId} — gönderici izin listesinde değil`);
2947
+ if (String(dmTg.chat) === String(chatId)) finDmTgNotAllowed(chatId);
2947
2948
  return;
2948
2949
  }
2949
2950
  if (!dmTg.chat) {
@@ -10122,7 +10123,9 @@ function finDmTgBind(chatId, title) {
10122
10123
  id,
10123
10124
  '✅ Beast Finance AJAN DM bu gruba bağlandı.\n' +
10124
10125
  'Bundan sonra ajanların/alarmların mesajları buraya düşer; buraya yazdıkların AJAN DM grubunda görünür ve ajanları uyandırır.\n' +
10125
- 'NOT: Grup Konular (Topics) destekliyorsa her AJAN DM grubu ayrı başlık olarak canlı akar — botu yönetici yapıp Konuları Yönet izni ver.'
10126
+ 'ÖNEMLİ: Normal mesajlarını görebilmem için beni bu grupta YÖNETİCİ yap (Konuları Yönet izniyle) ya da BotFather → /setprivacy → Disable yap. ' +
10127
+ 'Aksi halde yalnız /komutları ve bana yanıtları görebilirim.\n' +
10128
+ 'Grup Konular (Topics) destekliyorsa her AJAN DM grubu ayrı başlık olarak canlı akar.'
10126
10129
  )
10127
10130
  ).catch(() => {});
10128
10131
  }
@@ -10130,6 +10133,25 @@ function finDmTgBind(chatId, title) {
10130
10133
  return true;
10131
10134
  }
10132
10135
 
10136
+ /* İzin listesi dışı gönderici bağlı gruba yazdıysa sessiz kalma: 10 dk'da bir
10137
+ gruba kısa uyarı — kullanıcı mesajının neden ajanlara gitmediğini anlasın. */
10138
+ const finDmTgWarnAt = new Map();
10139
+ function finDmTgNotAllowed(chatId) {
10140
+ const id = String(chatId || '');
10141
+ const last = finDmTgWarnAt.get(id) || 0;
10142
+ if (!id || Date.now() - last < 10 * 60 * 1000) return;
10143
+ finDmTgWarnAt.set(id, Date.now());
10144
+ try {
10145
+ Promise.resolve(
10146
+ tg.send(
10147
+ id,
10148
+ '⚠️ Bu Telegram hesabı izin listesinde yok — mesajın ajanlara iletilemedi.\n' +
10149
+ 'Beast → Entegrasyonlar → Telegram bölümünde bu hesabı (ID ya da @kullanıcı adı) izin listesine ekle.'
10150
+ )
10151
+ ).catch(() => {});
10152
+ } catch {}
10153
+ }
10154
+
10133
10155
  /* ---- TELEGRAM KONU (TOPIC) EŞLEMESİ ----
10134
10156
  Forum destekli Telegram grubunda her AJAN DM grubu AYRI BAŞLIK olarak açılır:
10135
10157
  gid → message_thread_id eşlemesi settings.finance.dmTelegramTopics'ta kalıcı.
@@ -10238,18 +10260,35 @@ function finAgentDmToTelegram(dm) {
10238
10260
  const title = String(dm.groupTitle || (gid === FIN_TEAM_GID ? FIN_TEAM_TITLE : gid));
10239
10261
  const who = String(dm.fromTitle || (dm.system ? 'SİSTEM' : 'Ajan'));
10240
10262
  const text = String(dm.text || '').slice(0, 3800);
10263
+ const img = String(dm.image || '');
10264
+ const hasImg = /^data:image\//i.test(img);
10241
10265
  finDmTgQueue(async () => {
10242
10266
  const tid = await finDmTgThreadEnsure(c.chat, gid, title);
10243
- const plain = `👥 [${title}] ${who}:\n${text}`;
10244
- if (tid > 0) {
10267
+ const target = tid > 0 ? Number(tid) : undefined;
10268
+ const full = `${who}:\n${text}`;
10269
+ /* GÖRSEL: Telegram'a FOTO olarak gider (caption = gönderen + metin);
10270
+ metnin caption'a sığmayan kalanı ayrı mesajla gider. */
10271
+ if (hasImg) {
10272
+ const cap = full.slice(0, 1000);
10273
+ let ok = false;
10274
+ try { ok = await tg.sendPhoto(c.chat, img, cap, target); } catch { ok = false; }
10275
+ if (ok) {
10276
+ const rest = full.slice(1000);
10277
+ if (rest.trim()) await tg.send(c.chat, '…' + rest, target).catch(() => {});
10278
+ return;
10279
+ }
10280
+ /* foto gönderilemedi (bozuk/büyük) → metne düş, görsel notuyla */
10281
+ }
10282
+ const head = hasImg ? `${who}: (görsel gönderilemedi)\n${text}` : full;
10283
+ if (target) {
10245
10284
  try {
10246
- await tg.send(c.chat, `${who}:\n${text}`, tid);
10285
+ await tg.send(c.chat, head, target);
10247
10286
  return;
10248
10287
  } catch {
10249
10288
  /* konu silinmiş/erişim yok → genel akışa düş (mesaj kaybolmasın) */
10250
10289
  }
10251
10290
  }
10252
- await tg.send(c.chat, plain);
10291
+ await tg.send(c.chat, `👥 [${title}] ${head}`);
10253
10292
  });
10254
10293
  } catch {}
10255
10294
  }
@@ -10311,7 +10311,7 @@ function finDmTgPaint() {
10311
10311
  if (els.finDmTgHint) {
10312
10312
  els.finDmTgHint.textContent =
10313
10313
  'Her AJAN DM grubu (Beast Finance EKİP, sembol ekipleri vb.) Telegram\u2019da AYRI BAŞLIK (topic) olarak canlı akar. ' +
10314
- 'Telegram grubunda Konular (Topics) açık ve bot yönetici (Konuları Yönet) olmalı — Konular kapalıysa mesajlar tek akışa grup etiketiyle düşer. ' +
10314
+ 'Telegram grubunda Konular (Topics) açık ve bot YÖNETİCİ (Konuları Yönet) olmalı — yönetici değilse normal mesajlarını bot göremez (BotFather → /setprivacy → Disable da olur); Konular kapalıysa mesajlar tek akışa grup etiketiyle düşer. ' +
10315
10315
  'Bir başlığa yazdığın mesaj ilgili AJAN DM grubunda görünür ve o grubun ajanları uyanır.';
10316
10316
  }
10317
10317
  }