beast-agent 2.7.1 → 2.7.3
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 +1 -1
- package/src/agent/engine.js +3 -0
- package/src/main.js +149 -35
- package/src/renderer/renderer.js +13 -0
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -2350,6 +2350,9 @@ class Engine {
|
|
|
2350
2350
|
) {
|
|
2351
2351
|
lastMsg.content = (lastMsg.content + '\n\n' + body).slice(0, USER_MAX);
|
|
2352
2352
|
this._rewriteLastMsg(s.id, lastMsg.content, null, 'assistant');
|
|
2353
|
+
/* UI'ya bildir: birleştirme yeni bubble basmaz — son asistan balonu
|
|
2354
|
+
güncellenir (yoksa proaktif not ekranda hiç görünmezdi) */
|
|
2355
|
+
this.emit({ type: 'message-merge', sessionId: s.id, message: lastMsg });
|
|
2353
2356
|
this.emit({ type: 'sessions' });
|
|
2354
2357
|
return true;
|
|
2355
2358
|
}
|
package/src/main.js
CHANGED
|
@@ -234,6 +234,7 @@ let waSingleSid = null; // KANAL TEK OTURUMU: WhatsApp için BİR oturum — sil
|
|
|
234
234
|
let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
|
|
235
235
|
let waBcMode = new Set(); // jid -> BeastCode modu AKTİF (WhatsApp'tan uzaktan kodlama)
|
|
236
236
|
let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
|
|
237
|
+
let waLastActiveJid = ''; // en son mesaj gelen WA sohbeti — kanal tek oturumunda cevap/dosya hedefi
|
|
237
238
|
const WA_HISTORY_CAP = 20;
|
|
238
239
|
let tg = null;
|
|
239
240
|
let tgChats = new Map(); // telegram chatId -> aktif session id (hepsi KANAL TEK OTURUMUNA bağlı)
|
|
@@ -1759,6 +1760,19 @@ function ensureWaSession(jid) {
|
|
|
1759
1760
|
return waSingleSid;
|
|
1760
1761
|
}
|
|
1761
1762
|
|
|
1763
|
+
/* Kanal tek oturumunda cevap/dosya hedefi: bu oturuma bağlı sohbetler arasında
|
|
1764
|
+
EN SON mesaj gelen önceliklidir (kullanıcı o pencerede bekliyor); o bilgi
|
|
1765
|
+
yoksa (restart vb.) ilk bağlı sohbet kullanılır. null = bağlı sohbet yok. */
|
|
1766
|
+
function waReplyJid(sid) {
|
|
1767
|
+
try {
|
|
1768
|
+
if (waLastActiveJid && waChats.get(waLastActiveJid) === sid) return waLastActiveJid;
|
|
1769
|
+
} catch {}
|
|
1770
|
+
for (const [j, s] of waChats) {
|
|
1771
|
+
if (s === sid) return j;
|
|
1772
|
+
}
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1762
1776
|
/* ---------- GRUP BAĞLAM AKIŞI (mentionOnly + seeAll) ----------
|
|
1763
1777
|
Bot grubun tüm konuşmasını okur ama CEVAP ÜRETMEZ; mesajlar sessizce
|
|
1764
1778
|
oturum geçmişine bağlam olarak düşer. @mention gelince bot tüm bu
|
|
@@ -2311,6 +2325,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
|
2311
2325
|
}
|
|
2312
2326
|
/* KANAL TEK OTURUMU: eskisi silinmedikçe hep aynı oturumdan devam */
|
|
2313
2327
|
sid = ensureWaSession(jid);
|
|
2328
|
+
waLastActiveJid = String(jid); // cevap/dosya hedefi: en son yazan sohbet
|
|
2314
2329
|
// Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
|
|
2315
2330
|
wa.setComposing(jid, true);
|
|
2316
2331
|
// Kişi bazlı granül izin: all/web/read/chat
|
|
@@ -2985,6 +3000,7 @@ function reloadBackend() {
|
|
|
2985
3000
|
emit: (ev) => {
|
|
2986
3001
|
if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
|
|
2987
3002
|
try { empatiRememberFromEvent(ev); } catch {} /* empati hafızası: sohbet → kayıt + ilgi öğrenme */
|
|
3003
|
+
empatiFlushInjectsOnDone(ev); /* tur ortasında kuyruğa düşen proaktif bildirimleri iş bitince geçmişe bas */
|
|
2988
3004
|
flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
|
|
2989
3005
|
bcFlushOnDone(ev); /* Beast Code kuyruğunu iş bitiminde boşalt */
|
|
2990
3006
|
stFlushOnDone(ev); /* Beast Studio kuyruğunu iş bitiminde boşalt */
|
|
@@ -2998,13 +3014,13 @@ function reloadBackend() {
|
|
|
2998
3014
|
Spam olmasın: sohbet başına en az 7 sn'de bir tek satır. */
|
|
2999
3015
|
if (ev.type === 'tool-start' && wa && wa.connected) {
|
|
3000
3016
|
try {
|
|
3001
|
-
const
|
|
3002
|
-
if (
|
|
3017
|
+
const pingJid = waReplyJid(ev.sessionId);
|
|
3018
|
+
if (pingJid) {
|
|
3003
3019
|
const now = Date.now();
|
|
3004
3020
|
const last = lastWaToolPing.get(ev.sessionId) || 0;
|
|
3005
3021
|
if (now - last >= 7000) {
|
|
3006
3022
|
lastWaToolPing.set(ev.sessionId, now);
|
|
3007
|
-
sendWaSafe(
|
|
3023
|
+
sendWaSafe(pingJid, '\u203A ' + String(waToolLine(ev.name, ev.args || {})).replace(/^\u203A /, '')).catch(() => {});
|
|
3008
3024
|
}
|
|
3009
3025
|
}
|
|
3010
3026
|
} catch {}
|
|
@@ -3017,8 +3033,8 @@ function reloadBackend() {
|
|
|
3017
3033
|
const isInterim = Array.isArray(ev.message.tool_calls) && ev.message.tool_calls.length > 0;
|
|
3018
3034
|
if (mtxt && isInterim) {
|
|
3019
3035
|
try {
|
|
3020
|
-
const
|
|
3021
|
-
if (
|
|
3036
|
+
const interJid = waReplyJid(ev.sessionId);
|
|
3037
|
+
if (interJid) sendWaSafe(interJid, mtxt).catch(() => {});
|
|
3022
3038
|
} catch {}
|
|
3023
3039
|
}
|
|
3024
3040
|
}
|
|
@@ -3042,9 +3058,8 @@ function reloadBackend() {
|
|
|
3042
3058
|
}
|
|
3043
3059
|
// WhatsApp oturumlarının son cevabını geri gönder (metin + opsiyonel ses)
|
|
3044
3060
|
if ((ev.type === 'done' || ev.type === 'error') && wa && wa.connected) {
|
|
3045
|
-
const
|
|
3046
|
-
if (
|
|
3047
|
-
const wajid = hit[0];
|
|
3061
|
+
const wajid = waReplyJid(ev.sessionId);
|
|
3062
|
+
if (wajid) {
|
|
3048
3063
|
(async () => {
|
|
3049
3064
|
try {
|
|
3050
3065
|
if (ev.type === 'error') {
|
|
@@ -3762,8 +3777,9 @@ function setBrowserOpen(v, forceVisible) {
|
|
|
3762
3777
|
// AÇMA — görünürlük: forceVisible true/false ise onu uygula;
|
|
3763
3778
|
// belirtilmemişse kullanıcı tercihi belirler. Tarayıcı gizleme ÖZELLİĞİ
|
|
3764
3779
|
// (settings.browserHide) kapalıysa tercih ne olursa olsun tarayıcı GÖRÜNÜR açılır.
|
|
3765
|
-
//
|
|
3766
|
-
//
|
|
3780
|
+
// AJAN gezinmeleri setBrowserOpenForAgent üzerinden gelir: gizleme özelliği
|
|
3781
|
+
// kapalıysa görünür, özellik + göz kapalıyken gizli çalışır (kullanıcı ekranı
|
|
3782
|
+
// rahatsız edilmez; tarayıcı düğmesi gizli paneli görünür kılar).
|
|
3767
3783
|
browser.open = true;
|
|
3768
3784
|
browser.visible =
|
|
3769
3785
|
forceVisible === true ? true : forceVisible === false ? false : !browserHeadlessPref();
|
|
@@ -3796,13 +3812,15 @@ function browserGate(job) {
|
|
|
3796
3812
|
return p;
|
|
3797
3813
|
}
|
|
3798
3814
|
/* AJAN TARAYICI STRATEJİSİ — TÜM oturumlar (ana sohbet + WhatsApp botları + paralel ajanlar):
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3815
|
+
GİZLEME ÖZELLİĞİ (Ayarlar → Web Arama) KAPALIYSA ajan tarayıcıyı GİZLİ açamaz —
|
|
3816
|
+
dahili panel GÖRÜNÜR açılır, kullanıcı aramayı canlı izler. Özellik açıksa göz
|
|
3817
|
+
durumuna bakılır: göz açık (headless tercihi kapalı) → görünür, göz kapalı →
|
|
3818
|
+
gizli. Mobil önizleme varsa siluet canlı kalsın diye her koşulda görünür.
|
|
3819
|
+
Zaten açıksa (kullanıcı paneli açık tutuyorsa) görünürlüğe dokunulmaz. */
|
|
3803
3820
|
function setBrowserOpenForAgent() {
|
|
3804
|
-
|
|
3805
|
-
|
|
3821
|
+
if (!browser.open) {
|
|
3822
|
+
setBrowserOpen(true, !browserHideEnabled() || !browserHeadlessPref() || browser.mobile);
|
|
3823
|
+
}
|
|
3806
3824
|
}
|
|
3807
3825
|
|
|
3808
3826
|
async function browserNavigate(raw, signal, ctx) {
|
|
@@ -6455,11 +6473,51 @@ function empatiSourceLineWa(ev) {
|
|
|
6455
6473
|
return title ? '🔗 ' + cap(title, 80) : '';
|
|
6456
6474
|
}
|
|
6457
6475
|
|
|
6476
|
+
/* Enjeksiyon kuyruğu: oturum TUR ORTASINDAYKEN (ajan bir işle uğraşırken)
|
|
6477
|
+
gelen proaktif bildirimi sessizce DÜŞÜRME — eskisi tam olarak buydu:
|
|
6478
|
+
"bazen ajan konudan haber olmuyor". Metin kuyrukta bekler, oturumun
|
|
6479
|
+
işi bitince ('done'/'error') geçmişe asistan mesajı olarak yazılır;
|
|
6480
|
+
model bir sonraki turda "ben bu bildirimi attım" der, kullanıcı sorusuna
|
|
6481
|
+
hazırdır. */
|
|
6482
|
+
const empatiInjectQueue = new Map(); // sid -> [text]
|
|
6483
|
+
const EMPATI_INJECT_QUEUE_CAP = 20;
|
|
6484
|
+
|
|
6458
6485
|
function empatiInjectToSession(sid, out) {
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6486
|
+
const target = String(sid || '');
|
|
6487
|
+
const body = String(out || '');
|
|
6488
|
+
if (!target || !body.trim()) return;
|
|
6489
|
+
let busy = false;
|
|
6490
|
+
try { busy = engine.isBusy(target); } catch {}
|
|
6491
|
+
if (busy) {
|
|
6492
|
+
let q = empatiInjectQueue.get(target);
|
|
6493
|
+
if (!q) {
|
|
6494
|
+
q = [];
|
|
6495
|
+
empatiInjectQueue.set(target, q);
|
|
6496
|
+
}
|
|
6497
|
+
if (q.length < EMPATI_INJECT_QUEUE_CAP) {
|
|
6498
|
+
q.push(body);
|
|
6499
|
+
empatiLog(`bildirim enjeksiyonu kuyrukta (ajan tur sürüyor) sid=${target} bekleyen=${q.length}`);
|
|
6500
|
+
}
|
|
6501
|
+
return;
|
|
6502
|
+
}
|
|
6503
|
+
try { engine.injectAssistant(target, body); } catch {}
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
/* iş bitince (done/error) biriken proaktif enjeksiyonları geçmişe bas.
|
|
6507
|
+
'done' eventi isBusy temizlenmeden ÖNCE gelir (engine.js: ctrl delete
|
|
6508
|
+
finally bloğunda) — flushDesktopOnDone gibi macrotask'a ertele. */
|
|
6509
|
+
function empatiFlushInjectsOnDone(ev) {
|
|
6510
|
+
if (!ev || !ev.sessionId || (ev.type !== 'done' && ev.type !== 'error')) return;
|
|
6511
|
+
const q = empatiInjectQueue.get(String(ev.sessionId));
|
|
6512
|
+
if (!q || !q.length) return;
|
|
6513
|
+
empatiInjectQueue.delete(String(ev.sessionId));
|
|
6514
|
+
setTimeout(() => {
|
|
6515
|
+
/* empatiInjectToSession üzerinden: arada yeni tur başladıysa yeniden kuyruğa düşer */
|
|
6516
|
+
for (const text of q) {
|
|
6517
|
+
try { empatiInjectToSession(String(ev.sessionId), text); } catch {}
|
|
6518
|
+
}
|
|
6519
|
+
empatiLog(`kuyrukta bekleyen ${q.length} proaktif bildirim geçmişe işlendi sid=${ev.sessionId}`);
|
|
6520
|
+
}, 250);
|
|
6463
6521
|
}
|
|
6464
6522
|
|
|
6465
6523
|
/* kanala giden metin + Beast'in İÇ bağlamı ayrıdır: kullanıcı linki silse bile
|
|
@@ -6618,12 +6676,16 @@ function empatiNotify(text, ev) {
|
|
|
6618
6676
|
for (const fn of senders) {
|
|
6619
6677
|
try { fn(); sent++; } catch {}
|
|
6620
6678
|
}
|
|
6621
|
-
/*
|
|
6622
|
-
|
|
6679
|
+
/* MASAÜSTÜ CHAT UI: entegrasyon kullanılsa da kullanılmasa da proaktif mesaj
|
|
6680
|
+
sohbette MESAJ olarak görünür (asistan balonu) + toast çıkar. Ajan da
|
|
6681
|
+
desktop oturumu geçmişinde attığı bildirimi GÖRÜR — hangi kanala gittiğine
|
|
6682
|
+
bakmaksızın konudan haberdar olur. Kanal oturumlarına ise BAĞLAM'lı `inject`
|
|
6683
|
+
gider (kanal kullanıcısı ham BAĞLAM bloğunu görmez, ajan görür). */
|
|
6623
6684
|
try {
|
|
6624
|
-
if (
|
|
6625
|
-
|
|
6626
|
-
|
|
6685
|
+
if (win && !win.isDestroyed()) {
|
|
6686
|
+
const dsid = empatiDesktopSid();
|
|
6687
|
+
empatiInjectToSession(dsid, out);
|
|
6688
|
+
win.webContents.send('agent:event', { type: 'proactive', sessionId: dsid, id: ev.id, level: ev.level, title: ev.title, text: out });
|
|
6627
6689
|
}
|
|
6628
6690
|
} catch {}
|
|
6629
6691
|
}
|
|
@@ -6761,7 +6823,8 @@ ipcMain.handle('cron:runNow', (_e, id) => {
|
|
|
6761
6823
|
/* ---------------- tarayıcı IPC ---------------- */
|
|
6762
6824
|
ipcMain.handle('browser:toggle', () => {
|
|
6763
6825
|
/* gizli çalışan ajan paneli varsa düğme ONU GÖRÜNÜR yapar; değilse aç/kapa.
|
|
6764
|
-
(
|
|
6826
|
+
(ajan gizleme özelliği + göz kapalıyken gizli çalışır — kullanıcı izlemek
|
|
6827
|
+
isterse buradan gösterir) */
|
|
6765
6828
|
if (browser.open && !browser.visible) {
|
|
6766
6829
|
setBrowserOpen(true, true);
|
|
6767
6830
|
return { open: browser.open, visible: browser.visible };
|
|
@@ -8876,8 +8939,42 @@ ipcMain.handle('screen:capture', async () => {
|
|
|
8876
8939
|
});
|
|
8877
8940
|
|
|
8878
8941
|
/* #26 ajanın send_file aracı: dosyayı doğru kanala (WA veya chat) ulaştırır.
|
|
8879
|
-
Paralel ajan işiyse parent sohbete, masaüstünde dosya kartı olarak düşer.
|
|
8942
|
+
Paralel ajan işiyse parent sohbete, masaüstünde dosya kartı olarak düşer.
|
|
8943
|
+
WhatsApp hedefinde resim/video/ses → medya mesajı, PDF/belge → doğru
|
|
8944
|
+
mimetype ile belge mesajı olarak gider; LID adresi patlarsa gerçek numaraya
|
|
8945
|
+
(@s.whatsapp.net) tek kez düşülür, yine başarısızsa ajan'a HATA döner. */
|
|
8880
8946
|
const FILE_IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
8947
|
+
const FILE_MIME = {
|
|
8948
|
+
'.pdf': 'application/pdf',
|
|
8949
|
+
'.doc': 'application/msword',
|
|
8950
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
8951
|
+
'.xls': 'application/vnd.ms-excel',
|
|
8952
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
8953
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
8954
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
8955
|
+
'.txt': 'text/plain',
|
|
8956
|
+
'.csv': 'text/csv',
|
|
8957
|
+
'.md': 'text/markdown',
|
|
8958
|
+
'.json': 'application/json',
|
|
8959
|
+
'.html': 'text/html',
|
|
8960
|
+
'.zip': 'application/zip',
|
|
8961
|
+
'.rar': 'application/vnd.rar',
|
|
8962
|
+
'.7z': 'application/x-7z-compressed',
|
|
8963
|
+
'.mp3': 'audio/mpeg',
|
|
8964
|
+
'.m4a': 'audio/mp4',
|
|
8965
|
+
'.wav': 'audio/wav',
|
|
8966
|
+
'.ogg': 'audio/ogg',
|
|
8967
|
+
'.opus': 'audio/ogg; codecs=opus',
|
|
8968
|
+
'.mp4': 'video/mp4',
|
|
8969
|
+
'.mkv': 'video/x-matroska',
|
|
8970
|
+
'.webm': 'video/webm',
|
|
8971
|
+
'.svg': 'image/svg+xml',
|
|
8972
|
+
'.apk': 'application/vnd.android.package-archive',
|
|
8973
|
+
};
|
|
8974
|
+
function mimeForFile(ext) {
|
|
8975
|
+
return FILE_MIME[String(ext || '').toLowerCase()] || 'application/octet-stream';
|
|
8976
|
+
}
|
|
8977
|
+
|
|
8881
8978
|
async function deliverFile(sessionId, filePath, caption) {
|
|
8882
8979
|
try {
|
|
8883
8980
|
const abs = path.isAbsolute(filePath) ? filePath : path.join(engine.workspace || process.cwd(), filePath);
|
|
@@ -8889,17 +8986,34 @@ async function deliverFile(sessionId, filePath, caption) {
|
|
|
8889
8986
|
const job = engine.listBgJobs().find((j) => j.id === sid);
|
|
8890
8987
|
if (job && job.parentId) sid = job.parentId; // paralel ajan → parent sohbet
|
|
8891
8988
|
} catch {}
|
|
8892
|
-
/* WhatsApp hedefi: bu oturum bir WA sohbetine bağlıysa oraya gönder
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
if (s === sid) { jid = j; break; }
|
|
8896
|
-
}
|
|
8989
|
+
/* WhatsApp hedefi: bu oturum bir WA sohbetine bağlıysa oraya gönder.
|
|
8990
|
+
Birden çok sohbet aynı oturuma bağlıysa EN SON yazan kazanır. */
|
|
8991
|
+
const jid = waReplyJid(sid);
|
|
8897
8992
|
if (jid && wa) {
|
|
8898
8993
|
const buf = fs.readFileSync(abs);
|
|
8899
|
-
const
|
|
8900
|
-
|
|
8901
|
-
|
|
8994
|
+
const sendMedia = async (target) =>
|
|
8995
|
+
FILE_IMAGE_EXT.has(ext)
|
|
8996
|
+
? await wa.sendImage(target, buf, caption || name)
|
|
8997
|
+
: await wa.sendFile(target, buf, name, caption, mimeForFile(ext));
|
|
8998
|
+
let ok = false;
|
|
8999
|
+
try { ok = !!(await sendMedia(jid)); } catch {}
|
|
9000
|
+
if (!ok) {
|
|
9001
|
+
/* LID/adres fallback: bilinen gerçek numaraya (@s.whatsapp.net) dene */
|
|
9002
|
+
const pn = waJidPn.get(jid);
|
|
9003
|
+
const pnJid = pn ? `${pn}@s.whatsapp.net` : '';
|
|
9004
|
+
if (pnJid && pnJid !== jid) {
|
|
9005
|
+
try { ok = !!(await sendMedia(pnJid)); } catch {}
|
|
9006
|
+
if (ok) waLog(`dosya fallback → ${waPrettyJid(pnJid)} (lid/pn denemelerinden sonra)`);
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
8902
9009
|
if (ok) return { ok: true, channel: 'whatsapp', name };
|
|
9010
|
+
/* WA hedefi var ama gönderim başarısız — sessizce "masaüstüne düştü"
|
|
9011
|
+
SANMA: ajan durumu bilsin, kullanıcıya söyleyip tekrar deneyebilsin */
|
|
9012
|
+
waLog(`dosya gönderim HATA → ${waPrettyJid(jid)} "${name}"`);
|
|
9013
|
+
return {
|
|
9014
|
+
ok: false,
|
|
9015
|
+
error: `whatsapp gönderimi başarısız: ${name} — bağlantı/medya hatası; biraz sonra tekrar dene ya da kullanıcıya söyle`,
|
|
9016
|
+
};
|
|
8903
9017
|
}
|
|
8904
9018
|
/* masaüstü: sohbete dosya kartı bas */
|
|
8905
9019
|
if (win && !win.isDestroyed()) {
|
package/src/renderer/renderer.js
CHANGED
|
@@ -5019,6 +5019,19 @@ function onEvent(ev) {
|
|
|
5019
5019
|
if (!hasTools) closeChatToolGroup();
|
|
5020
5020
|
}
|
|
5021
5021
|
break;
|
|
5022
|
+
case 'message-merge':
|
|
5023
|
+
/* proaktif not son asistan mesajına BİRLEŞTİRİLDİ (injectAssistant) —
|
|
5024
|
+
yeni bubble basma; son asistan balonunun içeriğini güncelle */
|
|
5025
|
+
{
|
|
5026
|
+
const bubbles = els.msgs.querySelectorAll('.msg-assistant .md');
|
|
5027
|
+
const el = bubbles[bubbles.length - 1];
|
|
5028
|
+
const txt = String((ev.message && ev.message.content) || '');
|
|
5029
|
+
if (el && txt) {
|
|
5030
|
+
el.innerHTML = md(txt);
|
|
5031
|
+
scrollDown();
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
break;
|
|
5022
5035
|
case 'token':
|
|
5023
5036
|
ensureStreamBubble();
|
|
5024
5037
|
streamRaw += ev.delta;
|