beast-agent 2.3.3 → 2.4.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": "2.3.3",
4
+ "version": "2.4.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
package/src/agent/mem0.js CHANGED
@@ -101,11 +101,16 @@ function loadPipeline() {
101
101
  _pipeLoading = (async () => {
102
102
  try {
103
103
  const { pipeline, env } = require('@xenova/transformers');
104
+ const bus = require('./progressbus');
104
105
  const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
105
106
  fs.mkdirSync(modelsDir, { recursive: true });
106
107
  env.cacheDir = modelsDir;
107
108
  env.allowLocalModels = false;
108
- const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
109
+ const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
110
+ quantized: true,
111
+ progress_callback: bus.fileProgressAggregator('emb'),
112
+ });
113
+ bus.emitInstallProgress('emb', { pct: 100 });
109
114
  _pipe = async (texts) => {
110
115
  const out = [];
111
116
  for (const t of texts) {
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ /* Empati Loop — proaktif algı/event alt sistemi (ana sohbet motorundan bağımsız).
4
+ Boru hattı: SİNYAL → NORMALİZE → DEDUP → UCUZ FİLTRE (LLM) → ÖNCELİK → KUYRUK
5
+ Bu modül saf mantık + olay deposudur; LLM çağrıları (llmFilter) ve sinyal
6
+ toplayıcıları (signals) main.js tarafından enjekte edilir.
7
+ İlke: her sinyal event olmaz, her event büyük modele gitmez, her event
8
+ kullanıcıya bildirilmez. Depo: %APPDATA%\beast\perception\events.json */
9
+
10
+ const crypto = require('crypto');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { beastRoot } = require('./memory');
14
+
15
+ const DEFAULTS = {
16
+ enabled: false,
17
+ intervalMin: 15, // tarama aralığı (3-1440 dk)
18
+ cooldownMin: 30, // aynı konu için sessizlik (0-10080 dk)
19
+ minNotifyPriority: 60, // bu kompozit puanın altı yalnız depoya yazılır (0-100)
20
+ maxNotifyPerCycle: 1, // döngü başına en fazla bildirim (maliyet freni)
21
+ notifyTarget: '', // bildirim hedefi: '' = bağlı entegrasyonlar | whatsapp | telegram | discord
22
+ filterModel: '', // ucuz filtre modeli ('provider::model'); boşsa ana model
23
+ interests: '', // kullanıcı ilgi alanları (relevance ağırlığı)
24
+ newsTopics: '', // Google News RSS sorguları (virgülle)
25
+ weights: { importance: 0.40, relevance: 0.25, urgency: 0.20, novelty: 0.15 },
26
+ };
27
+
28
+ const EVENT_CAP = 250; // depo tavanı — eskiler düşer
29
+ const SEEN_CAP = 800; // dedup parmakizi havuzu
30
+ const SEEN_TTL = 6 * 3600 * 1000; // aynı olay 6 saat içinde tekrar gelirse yut
31
+ const FILTER_MAX = 12; // tek filtre çağrısında en fazla olay
32
+ const NEWS_MAX = 12; // konu başına en fazla haber adayı
33
+
34
+ /* ---------- config ---------- */
35
+
36
+ function clampW(v, d) {
37
+ const n = Number(v);
38
+ return Number.isFinite(n) && n > 0 && n < 1 ? n : d;
39
+ }
40
+
41
+ function mergeCfg(raw) {
42
+ const r = raw || {};
43
+ const num = (v, min, max, d) => {
44
+ const n = Math.round(Number(v));
45
+ return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : d;
46
+ };
47
+ const w = r.weights || {};
48
+ return {
49
+ enabled: r.enabled === true,
50
+ intervalMin: num(r.intervalMin, 3, 1440, DEFAULTS.intervalMin),
51
+ cooldownMin: num(r.cooldownMin, 0, 10080, DEFAULTS.cooldownMin),
52
+ minNotifyPriority: num(r.minNotifyPriority, 0, 100, DEFAULTS.minNotifyPriority),
53
+ maxNotifyPerCycle: num(r.maxNotifyPerCycle, 1, 5, DEFAULTS.maxNotifyPerCycle),
54
+ notifyTarget: ['whatsapp', 'telegram', 'discord'].includes(String(r.notifyTarget || ''))
55
+ ? String(r.notifyTarget)
56
+ : '',
57
+ filterModel: String(r.filterModel || '').trim(),
58
+ interests: String(r.interests || '').slice(0, 400),
59
+ newsTopics: String(r.newsTopics || '').slice(0, 400),
60
+ weights: {
61
+ importance: clampW(w.importance, DEFAULTS.weights.importance),
62
+ relevance: clampW(w.relevance, DEFAULTS.weights.relevance),
63
+ urgency: clampW(w.urgency, DEFAULTS.weights.urgency),
64
+ novelty: clampW(w.novelty, DEFAULTS.weights.novelty),
65
+ },
66
+ };
67
+ }
68
+
69
+ /* ---------- depo ---------- */
70
+
71
+ function stateFile() {
72
+ return path.join(beastRoot(), 'perception', 'events.json');
73
+ }
74
+
75
+ function loadState() {
76
+ try {
77
+ const raw = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
78
+ return {
79
+ events: Array.isArray(raw.events) ? raw.events : [],
80
+ seen: raw.seen && typeof raw.seen === 'object' ? raw.seen : {},
81
+ cooldowns: raw.cooldowns && typeof raw.cooldowns === 'object' ? raw.cooldowns : {},
82
+ lastRunAt: raw.lastRunAt || null,
83
+ };
84
+ } catch {
85
+ return { events: [], seen: {}, cooldowns: {}, lastRunAt: null };
86
+ }
87
+ }
88
+
89
+ function saveState(st) {
90
+ try {
91
+ fs.mkdirSync(path.dirname(stateFile()), { recursive: true });
92
+ fs.writeFileSync(stateFile(), JSON.stringify(st, null, 2));
93
+ } catch {}
94
+ }
95
+
96
+ /* ---------- normalize / dedup ---------- */
97
+
98
+ function normTitle(t) {
99
+ const base = String(t || '')
100
+ .toLowerCase()
101
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
102
+ .replace(/\s+/g, ' ')
103
+ .trim();
104
+ return base.split(' ').slice(0, 12).join(' ');
105
+ }
106
+
107
+ function fingerprint(type, title) {
108
+ return crypto.createHash('sha1').update(normTitle(type + '|' + title)).digest('hex').slice(0, 14);
109
+ }
110
+
111
+ /* cooldown anahtarı: konunun ilk 5 kelimesi — varyasyonlar aynı kovaya düşer */
112
+ function topicKey(type, title) {
113
+ return normTitle(type + ' ' + title).split(' ').slice(0, 5).join(' ');
114
+ }
115
+
116
+ function uid() {
117
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
118
+ }
119
+
120
+ function normalizeRaw(item) {
121
+ const title = String((item && item.title) || '').replace(/\s+/g, ' ').trim().slice(0, 300);
122
+ if (!title) return null;
123
+ const type = String((item && item.type) || 'news').slice(0, 24);
124
+ return {
125
+ id: uid(),
126
+ ts: item.ts || new Date().toISOString(),
127
+ source: String((item && item.source) || type).slice(0, 40),
128
+ type,
129
+ title,
130
+ detail: String((item && item.detail) || '').slice(0, 300),
131
+ sources: [(item && item.source) || type],
132
+ scores: {},
133
+ priority: 0,
134
+ reason: '',
135
+ status: 'new',
136
+ level: '',
137
+ text: '',
138
+ notifiedAt: null,
139
+ };
140
+ }
141
+
142
+ /* ---------- haber kaynağı (Google News RSS — anahtar gerektirmez,
143
+ Reuters/BBC/AP gibi kaynakları zaten tek beslemede harmanlar) ---------- */
144
+
145
+ async function fetchNews(topics) {
146
+ const out = [];
147
+ for (const q of (topics || []).slice(0, 4)) {
148
+ try {
149
+ const url = 'https://news.google.com/rss/search?q=' + encodeURIComponent(q) + '&hl=tr&gl=TR&ceid=TR:tr';
150
+ const res = await fetch(url, { signal: AbortSignal.timeout(12000) });
151
+ if (!res.ok) continue;
152
+ const xml = await res.text();
153
+ const re = /<item>[\s\S]*?<\/item>/g;
154
+ let m;
155
+ let count = 0;
156
+ while ((m = re.exec(xml)) && count < NEWS_MAX) {
157
+ const t = /<title>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/title>/.exec(m[0]);
158
+ const title = t ? String(t[1]).replace(/\s+/g, ' ').trim() : '';
159
+ if (!title) continue;
160
+ count++;
161
+ out.push({ type: 'news', title, detail: 'Google News · ' + q, source: 'news:' + q });
162
+ }
163
+ } catch {}
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /* ---------- ucuz deterministik puanlayıcı (LLM yoksa/çökerse fallback) ---------- */
169
+
170
+ const URGENT_RE = /acil|critical|son dakika|outage|arıza|crash|down|kesinti|kapatıldı|breach|güvenlik|saldırı|risk/i;
171
+
172
+ function heuristicScores(ev, cfg) {
173
+ const t = normTitle(ev.title);
174
+ const URGENT = URGENT_RE.test(t);
175
+ let importance = ev.type === 'news' ? 40 : 65;
176
+ if (URGENT) importance += 20;
177
+ const words = String(cfg.interests || '').toLowerCase().split(/[,\s]+/).filter((w) => w.length > 2);
178
+ let hits = 0;
179
+ for (const w of words) if (t.includes(w)) hits++;
180
+ const relevance = hits ? Math.min(100, 55 + hits * 15) : (ev.type === 'news' ? 30 : 60);
181
+ const urgency = URGENT ? 75 : (ev.type === 'news' ? 35 : 45);
182
+ const novelty = ev.type === 'news' ? 80 : 60;
183
+ return { importance, relevance, urgency, novelty };
184
+ }
185
+
186
+ function compositePriority(scores, cfg) {
187
+ const w = cfg.weights;
188
+ const s = (x) => Math.max(0, Math.min(100, Number(x) || 0));
189
+ const sum = w.importance + w.relevance + w.urgency + w.novelty || 1;
190
+ return Math.round(
191
+ (s(scores.importance) * w.importance +
192
+ s(scores.relevance) * w.relevance +
193
+ s(scores.urgency) * w.urgency +
194
+ s(scores.novelty) * w.novelty) / sum
195
+ );
196
+ }
197
+
198
+ /* ---------- LLM filtre promptu / ayrıştırıcı ---------- */
199
+
200
+ const FILTER_SYSTEM =
201
+ 'Sen bir bilgi filtresisin; sohbet etmezsin. Sana verilen olayları değerlendirip YALNIZCA JSON döndürürsün. ' +
202
+ 'Skorlar 0-100 arası tamsayı. "relevant": kullanıcının ilgi alanlarıyla alakalı mı. ' +
203
+ '"notify": kullanıcıya bildirmeye gerçekten değer mi (önemsiz/spam/klasik haberlerde false). ' +
204
+ '"reason": en fazla 60 karakter kısa gerekçe.';
205
+
206
+ function filterPrompt(events, interests) {
207
+ const list = events.map((e) => ({
208
+ id: e.id,
209
+ type: e.type,
210
+ title: e.title,
211
+ detail: e.detail,
212
+ }));
213
+ return (
214
+ 'KULLANICI İLGİLERİ: ' + (String(interests || '').trim() || '(belirtilmemiş)') + '\n' +
215
+ 'OLAYLAR:\n' + JSON.stringify(list) + '\n\n' +
216
+ 'Her olay için {"id","relevant","importance","urgency","novelty","notify","reason"} içeren ' +
217
+ 'TEK bir JSON dizisi döndür. Sadece JSON, başka metin yok.'
218
+ );
219
+ }
220
+
221
+ function parseFilterJson(text) {
222
+ try {
223
+ let s = String(text || '').replace(/```(?:json)?/gi, '');
224
+ const a = s.indexOf('[');
225
+ const b = s.lastIndexOf(']');
226
+ if (a < 0 || b <= a) return [];
227
+ const arr = JSON.parse(s.slice(a, b + 1));
228
+ if (!Array.isArray(arr)) return [];
229
+ const map = new Map();
230
+ for (const it of arr) {
231
+ if (it && it.id !== undefined) map.set(String(it.id).toLowerCase(), it);
232
+ }
233
+ return map;
234
+ } catch {
235
+ return [];
236
+ }
237
+ }
238
+
239
+ /* ---------- ana LLM compose promptu / fallback ---------- */
240
+
241
+ const COMPOSE_SYSTEM =
242
+ 'Beast adlı yerel yardımcı için proaktif bildirim metni yazarsın. Türkçe, samimi "kanka" tonunda, ' +
243
+ 'EN FAZLA 2 kısa cümle: ne olduğunu soyutla, neden önemli olabileceğini söyle, istersen tek kısa soru sor. ' +
244
+ 'Alarm spam\'i yok; başlık/emoji/Markdown ekleme, yalnız düz metin yaz.';
245
+
246
+ function composePrompt(ev, cfg) {
247
+ return (
248
+ 'OLAY: ' + ev.title + '\n' +
249
+ (ev.detail ? 'DETAY: ' + ev.detail + '\n' : '') +
250
+ 'KAYNAK: ' + ev.source + '\n' +
251
+ 'ÖNCELİK: ' + ev.priority + '/100 · ' + (ev.reason || '') + '\n' +
252
+ 'KULLANICI İLGİLERİ: ' + (String((cfg && cfg.interests) || '').trim() || '(belirtilmemiş)') + '\n\n' +
253
+ 'Bu olay için kullanıcıya gönderilecek proaktif kısa mesajı yaz.'
254
+ );
255
+ }
256
+
257
+ function composeFallback(ev) {
258
+ return ev.title + (ev.detail ? ' — ' + ev.detail : '');
259
+ }
260
+
261
+ /* ---------- döngü ---------- */
262
+
263
+ /* signals: { self: async()=>[raw], news: async()=>[raw] } — her biri bağımsız;
264
+ biri çökerse diğerleri çalışmaya devam eder. llmFilter: async(prompt)=>text. */
265
+ async function runCycle({ cfg, signals, llmFilter, now = new Date(), log = () => {} }) {
266
+ const t0 = Date.now();
267
+ log('cycle başladı');
268
+ const st = loadState();
269
+ const raws = [];
270
+ const srcStats = {};
271
+ for (const [name, fn] of Object.entries(signals || {})) {
272
+ if (typeof fn !== 'function') continue;
273
+ try {
274
+ const items = (await fn()) || [];
275
+ srcStats[name] = items.length;
276
+ for (const it of items) {
277
+ const ev = normalizeRaw(it);
278
+ if (ev) raws.push(ev);
279
+ }
280
+ } catch (e) {
281
+ srcStats[name] = 'hata: ' + String((e && e.message) || e).slice(0, 60);
282
+ }
283
+ }
284
+ log('sinyaller: ' + JSON.stringify(srcStats));
285
+
286
+ /* dedup: havuzda taze parmakizi varsa yut; toplu içinde tekrar varsa birleştir */
287
+ const pending = [];
288
+ let dups = 0;
289
+ for (const ev of raws) {
290
+ const fp = fingerprint(ev.type, ev.title);
291
+ const seenAt = st.seen[fp];
292
+ if (seenAt && now.getTime() - seenAt < SEEN_TTL) {
293
+ dups++;
294
+ continue;
295
+ }
296
+ st.seen[fp] = now.getTime();
297
+ const same = pending.find((p) => p._fp === fp);
298
+ if (same) {
299
+ if (!same.sources.includes(ev.source)) same.sources.push(ev.source);
300
+ dups++;
301
+ continue;
302
+ }
303
+ ev._fp = fp;
304
+ pending.push(ev);
305
+ }
306
+ /* parmakizi havuzu tavanı — en eskiyi düşür */
307
+ const seenKeys = Object.keys(st.seen);
308
+ if (seenKeys.length > SEEN_CAP) {
309
+ seenKeys.sort((a, b) => st.seen[a] - st.seen[b]);
310
+ for (const k of seenKeys.slice(0, seenKeys.length - SEEN_CAP)) delete st.seen[k];
311
+ }
312
+
313
+ /* UCUZ FİLTRE: tüm adaylar TEK toplu çağrıda (maliyet freni). Çökerse deterministik. */
314
+ let graded = null;
315
+ if (typeof llmFilter === 'function' && pending.length) {
316
+ try {
317
+ const out = await llmFilter(filterPrompt(pending.slice(0, FILTER_MAX), cfg.interests));
318
+ graded = parseFilterJson(out);
319
+ if (graded.size) log('filtre: LLM ' + graded.size + ' olayı puanladı');
320
+ } catch {
321
+ graded = null;
322
+ }
323
+ }
324
+ if (!graded || !graded.size) log('filtre: deterministik puanlama');
325
+
326
+ const counts = { ignored: 0, stored: 0, queued: 0, dup: dups, raw: raws.length };
327
+ const actions = [];
328
+ for (const ev of pending) {
329
+ const g = graded && graded.get(String(ev.id).toLowerCase());
330
+ ev.scores = g
331
+ ? {
332
+ importance: Math.max(0, Math.min(100, Math.round(Number(g.importance) || 0))),
333
+ relevance: Math.max(0, Math.min(100, Math.round(Number(g.relevance) || 0))),
334
+ urgency: Math.max(0, Math.min(100, Math.round(Number(g.urgency) || 0))),
335
+ novelty: Math.max(0, Math.min(100, Math.round(Number(g.novelty) || 0))),
336
+ }
337
+ : heuristicScores(ev, cfg);
338
+ ev.priority = compositePriority(ev.scores, cfg);
339
+ ev.reason = g ? String(g.reason || '').slice(0, 80) : 'deterministik puan';
340
+ if (g && g.notify === false && ev.priority < cfg.minNotifyPriority) {
341
+ ev.status = 'ignored';
342
+ counts.ignored++;
343
+ } else if (ev.priority < 30) {
344
+ ev.status = 'ignored';
345
+ counts.ignored++;
346
+ } else {
347
+ const key = topicKey(ev.type, ev.title);
348
+ const cd = st.cooldowns[key];
349
+ if (cd && now.getTime() < cd.until && ev.priority < (cd.lastPriority || 0) + 15) {
350
+ ev.status = 'stored';
351
+ ev.reason = (ev.reason || '') + ' · sessizlik penceresi';
352
+ counts.stored++;
353
+ } else if (ev.priority < cfg.minNotifyPriority) {
354
+ ev.status = 'stored';
355
+ counts.stored++;
356
+ } else {
357
+ ev.status = 'queued';
358
+ counts.queued++;
359
+ actions.push({
360
+ event: ev,
361
+ level: ev.priority >= 80 ? 'high' : 'medium',
362
+ });
363
+ }
364
+ }
365
+ st.events.push(ev);
366
+ }
367
+
368
+ /* kuyruğu önceliğe göre kes — döngü başına en fazla maxNotifyPerCycle bildirim */
369
+ actions.sort((a, b) => b.event.priority - a.event.priority);
370
+ const kept = actions.slice(0, cfg.maxNotifyPerCycle);
371
+ const dropped = actions.slice(cfg.maxNotifyPerCycle);
372
+ for (const a of dropped) {
373
+ a.event.status = 'stored';
374
+ a.event.reason += ' · kuyruk tavanı';
375
+ counts.stored++;
376
+ }
377
+ counts.queued = kept.length;
378
+
379
+ /* depo tavanı */
380
+ if (st.events.length > EVENT_CAP) st.events = st.events.slice(st.events.length - EVENT_CAP);
381
+
382
+ st.lastRunAt = now.toISOString();
383
+ saveState(st);
384
+ log(
385
+ 'cycle tamam: raw=' + counts.raw + ' dup=' + dups +
386
+ ' ignored=' + counts.ignored + ' stored=' + counts.stored + ' queued=' + counts.queued +
387
+ ' (' + (Date.now() - t0) + 'ms)'
388
+ );
389
+ return { summary: counts, actions: kept };
390
+ }
391
+
392
+ function markNotified(id, level, text, cooldownMin) {
393
+ const st = loadState();
394
+ const ev = st.events.find((e) => e.id === id);
395
+ if (ev) {
396
+ ev.status = 'notified';
397
+ ev.level = level || 'medium';
398
+ ev.text = String(text || '').slice(0, 600);
399
+ ev.notifiedAt = new Date().toISOString();
400
+ const key = topicKey(ev.type, ev.title);
401
+ st.cooldowns[key] = {
402
+ until: Date.now() + Math.max(0, Number(cooldownMin) || 0) * 60000,
403
+ lastPriority: ev.priority,
404
+ notifiedAt: ev.notifiedAt,
405
+ };
406
+ saveState(st);
407
+ }
408
+ }
409
+
410
+ function listEvents(limit) {
411
+ const st = loadState();
412
+ const n = Math.max(1, Math.min(200, Number(limit) || 60));
413
+ return st.events.slice(-n).reverse().map((e) => ({
414
+ id: e.id,
415
+ ts: e.ts,
416
+ source: e.source,
417
+ type: e.type,
418
+ title: e.title,
419
+ detail: e.detail,
420
+ priority: e.priority,
421
+ scores: e.scores || {},
422
+ reason: e.reason,
423
+ status: e.status,
424
+ level: e.level,
425
+ text: e.text,
426
+ }));
427
+ }
428
+
429
+ function lastRunAt() {
430
+ return loadState().lastRunAt;
431
+ }
432
+
433
+ module.exports = {
434
+ DEFAULTS,
435
+ mergeCfg,
436
+ fetchNews,
437
+ runCycle,
438
+ markNotified,
439
+ listEvents,
440
+ lastRunAt,
441
+ FILTER_SYSTEM,
442
+ COMPOSE_SYSTEM,
443
+ filterPrompt,
444
+ composePrompt,
445
+ composeFallback,
446
+ fingerprint,
447
+ };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ /* Kurulum progress bus — agent modülleri (whisper / mem0 / tools) yüzde üretir,
4
+ main abone olup renderer'a 'install-progress' agent:event olarak aktarır.
5
+ transformers.js progress_callback'i dosya bazlı geldiği için per-dosya
6
+ loaded/total toplamından genel yüzde hesaplayan aggregator da burada. */
7
+
8
+ const listeners = new Set();
9
+
10
+ function onInstallProgress(fn) {
11
+ if (typeof fn === 'function') listeners.add(fn);
12
+ return () => listeners.delete(fn);
13
+ }
14
+
15
+ function emitInstallProgress(id, data) {
16
+ for (const fn of listeners) {
17
+ try { fn(id, data); } catch {}
18
+ }
19
+ }
20
+
21
+ /* transformers.js benzeri dosya bazlı akışlar için: per-dosya loaded/total'dan
22
+ genel yüzde üreten progress_callback üretir. */
23
+ function fileProgressAggregator(id) {
24
+ const files = new Map(); // file -> { loaded, total }
25
+ return (d) => {
26
+ try {
27
+ const key = d && (d.file || d.name);
28
+ if (!key || typeof d.loaded !== 'number' || typeof d.total !== 'number' || d.total <= 0) return;
29
+ files.set(String(key), { loaded: d.loaded, total: d.total });
30
+ let loaded = 0;
31
+ let total = 0;
32
+ for (const v of files.values()) {
33
+ loaded += v.loaded;
34
+ total += v.total;
35
+ }
36
+ if (total > 0) emitInstallProgress(id, { pct: (loaded / total) * 100, loaded, total });
37
+ } catch {}
38
+ };
39
+ }
40
+
41
+ module.exports = { onInstallProgress, emitInstallProgress, fileProgressAggregator };
@@ -695,21 +695,25 @@ async function findSystemPython() {
695
695
  return null;
696
696
  }
697
697
 
698
- function httpsGetBuffer(url, redirectsLeft = 4, signal) {
698
+ function httpsGetBuffer(url, redirectsLeft = 4, signal, onProgress) {
699
699
  return new Promise((resolve, reject) => {
700
700
  const req = https.get(url, { headers: { 'User-Agent': 'BeastAgent/1.0 (+python bootstrap)' } }, (res) => {
701
701
  if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && redirectsLeft > 0) {
702
702
  res.resume();
703
- return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal));
703
+ return resolve(httpsGetBuffer(new URL(res.headers.location, url).toString(), redirectsLeft - 1, signal, onProgress));
704
704
  }
705
705
  if (res.statusCode !== 200) {
706
706
  res.resume();
707
707
  return reject(new Error(`indirme başarısız: HTTP ${res.statusCode}`));
708
708
  }
709
+ const total = parseInt(res.headers['content-length'] || '0', 10) || 0;
709
710
  const chunks = [];
710
711
  let size = 0;
711
712
  res.on('data', (c) => {
712
713
  size += c.length;
714
+ if (typeof onProgress === 'function') {
715
+ try { onProgress(size, total); } catch {}
716
+ }
713
717
  if (size > 80 * 1024 * 1024) {
714
718
  req.destroy(new Error('dosya çok büyük'));
715
719
  return;
@@ -731,7 +735,10 @@ async function installEmbeddedPython(signal) {
731
735
  const dest = path.join(beastAppDir(), 'py');
732
736
  fs.mkdirSync(dest, { recursive: true });
733
737
  const zipPath = path.join(os.tmpdir(), 'beast-py-embed.zip');
734
- const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal);
738
+ const bus = require('./progressbus');
739
+ const buf = await httpsGetBuffer(PYTHON_EMBED_URL, 4, signal, (size, total) => {
740
+ if (total > 0) bus.emitInstallProgress('python', { pct: (size / total) * 100, loaded: size, total });
741
+ });
735
742
  fs.writeFileSync(zipPath, buf);
736
743
  const r = await runCommand(
737
744
  `Expand-Archive -LiteralPath "${zipPath}" -DestinationPath "${dest}" -Force`,
@@ -36,6 +36,83 @@ function waLogSafe(line) {
36
36
 
37
37
  const TRACK_CAP = 500;
38
38
 
39
+ /* ---------- TTS → WhatsApp sesli not (OGG/Opus) ---------- */
40
+
41
+ let _ffmpegPath = null;
42
+ function getFfmpegPath() {
43
+ if (_ffmpegPath !== null) return _ffmpegPath;
44
+ try { _ffmpegPath = require('ffmpeg-static') || ''; } catch { _ffmpegPath = ''; }
45
+ return _ffmpegPath;
46
+ }
47
+
48
+ function runFfmpeg(args, input) {
49
+ return new Promise((resolve, reject) => {
50
+ try {
51
+ const { spawn } = require('child_process');
52
+ const p = spawn(getFfmpegPath(), args, { windowsHide: true });
53
+ const out = [];
54
+ let err = '';
55
+ p.stdout.on('data', (c) => out.push(c));
56
+ p.stderr.on('data', (c) => { if (err.length < 400) err += c.toString(); });
57
+ p.on('error', reject);
58
+ p.on('close', (code) => {
59
+ if (code !== 0) return reject(new Error(`ffmpeg ${code}: ${err.slice(0, 200)}`));
60
+ resolve(Buffer.concat(out));
61
+ });
62
+ p.stdin.on('error', () => {});
63
+ p.stdin.write(input);
64
+ p.stdin.end();
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ });
69
+ }
70
+
71
+ /* Herhangi bir ses buffer'ı → OGG/Opus (WhatsApp sesli not formatı). Zaten OggS ise olduğu gibi döner. */
72
+ async function toVoiceOggOpus(buf) {
73
+ if (buf.length > 4 && buf.subarray(0, 4).toString('latin1') === 'OggS') return buf;
74
+ const ff = getFfmpegPath();
75
+ if (!ff) throw new Error('ffmpeg bulunamadı');
76
+ return runFfmpeg([
77
+ '-hide_banner', '-loglevel', 'error',
78
+ '-i', 'pipe:0',
79
+ '-c:a', 'libopus', '-b:a', '48k', '-ar', '24000', '-ac', '1',
80
+ '-application', 'voip', '-vbr', 'on',
81
+ '-f', 'ogg', 'pipe:1',
82
+ ], buf);
83
+ }
84
+
85
+ /* Sesli not dalga formu: 64 segmentlik normalize genlik histogramı (Uint8Array) */
86
+ async function audioWaveform(buf) {
87
+ try {
88
+ const ff = getFfmpegPath();
89
+ if (!ff) return undefined;
90
+ const pcm = await runFfmpeg([
91
+ '-hide_banner', '-loglevel', 'error',
92
+ '-i', 'pipe:0', '-f', 's16le', '-ar', '16000', '-ac', '1', 'pipe:1',
93
+ ], buf);
94
+ const SEG = 64;
95
+ const step = Math.max(1, Math.floor(pcm.length / 2 / SEG));
96
+ const wave = new Uint8Array(SEG);
97
+ let max = 1;
98
+ for (let i = 0; i < SEG; i++) {
99
+ let sum = 0;
100
+ const start = i * step;
101
+ for (let j = 0; j < step; j++) {
102
+ const off = (start + j) * 2;
103
+ if (off + 1 < pcm.length) sum += Math.abs(pcm.readInt16LE(off));
104
+ }
105
+ const v = Math.round(sum / step);
106
+ wave[i] = v;
107
+ if (v > max) max = v;
108
+ }
109
+ for (let i = 0; i < SEG; i++) wave[i] = Math.min(255, Math.round((wave[i] / max) * 255));
110
+ return wave;
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
115
+
39
116
  /* lastKnownPresence etiketleri */
40
117
  const PRESENCE_LABELS = {
41
118
  available: 'çevrimiçi',
@@ -470,11 +547,29 @@ class WhatsAppBridge {
470
547
  return id ? { id } : true;
471
548
  }
472
549
 
473
- /* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer) */
550
+ /* Sesli not olarak yanıtla (TTS çıktısı mp3 buffer → WhatsApp sesli not formatı OGG/Opus'a çevrilir;
551
+ ptt sesli notlar mp3 ile açılmaz, mutlaka audio/ogg; codecs=opus olmalı) */
474
552
  async sendAudio(jid, audioBuf) {
475
553
  if (!this.sock || !this.connected || !audioBuf) return false;
476
554
  try {
477
- const ret = await this.sock.sendMessage(jid, { audio: audioBuf, ptt: true, mimetype: 'audio/mpeg' });
555
+ let voice = audioBuf;
556
+ let mime = 'audio/mpeg';
557
+ let ptt = false;
558
+ let waveform;
559
+ try {
560
+ voice = await toVoiceOggOpus(audioBuf);
561
+ mime = 'audio/ogg; codecs=opus';
562
+ ptt = true;
563
+ waveform = await audioWaveform(voice);
564
+ } catch {
565
+ /* ffmpeg dönüşümü başarısızsa mp3'ü ptt'siz ses mesajı olarak dene */
566
+ voice = audioBuf;
567
+ mime = 'audio/mpeg';
568
+ ptt = false;
569
+ }
570
+ const msg = { audio: voice, ptt, mimetype: mime };
571
+ if (waveform) msg.waveform = waveform;
572
+ const ret = await this.sock.sendMessage(jid, msg);
478
573
  this._trackOutgoing(jid, '[sesli yanıt]', ret);
479
574
  return true;
480
575
  } catch (e) {