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 +1 -1
- package/src/agent/mem0.js +6 -1
- package/src/agent/perception.js +447 -0
- package/src/agent/progressbus.js +41 -0
- package/src/agent/tools.js +10 -3
- package/src/agent/whatsapp.js +97 -2
- package/src/main.js +287 -6
- package/src/preload.js +4 -0
- package/src/renderer/i18n.js +68 -0
- package/src/renderer/index.html +2 -0
- package/src/renderer/renderer.js +176 -1
package/package.json
CHANGED
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', {
|
|
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 };
|
package/src/agent/tools.js
CHANGED
|
@@ -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
|
|
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`,
|
package/src/agent/whatsapp.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|