beast-agent 2.3.4 → 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/perception.js +447 -0
- package/src/main.js +188 -0
- 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 +142 -1
package/package.json
CHANGED
|
@@ -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
|
+
};
|
package/src/main.js
CHANGED
|
@@ -3256,6 +3256,7 @@ app.whenReady().then(() => {
|
|
|
3256
3256
|
createTray();
|
|
3257
3257
|
cron.init({ onFire: cronFire });
|
|
3258
3258
|
watchers.start({ onTrigger: watcherFire });
|
|
3259
|
+
empatiKickoff(); // empati loop: açılış + 90 sn sonra ilk tarama, sonra cfg aralığı
|
|
3259
3260
|
startEventBus();
|
|
3260
3261
|
ideWatchStart(); // soldaki dosya ağacı canlı izlemede
|
|
3261
3262
|
studioWatchStart(); // Beast Studio klasörü canlı izlemede
|
|
@@ -6187,6 +6188,193 @@ function watcherFire(w, value) {
|
|
|
6187
6188
|
} catch {}
|
|
6188
6189
|
}
|
|
6189
6190
|
|
|
6191
|
+
/* ---------- EMPATİ LOOP: proaktif algı/event alt sistemi ----------
|
|
6192
|
+
Ana sohbet motorundan bağımsız: sinyal topla → ucuz filtre modeliyle puanla →
|
|
6193
|
+
kompozit öncelik → değerliyse ANA modelle kısa proaktif mesaj üret →
|
|
6194
|
+
masaüstü + WA'ya bildir. Önemsiz olaylar yalnız depoya yazılır, rahatsız etmez. */
|
|
6195
|
+
|
|
6196
|
+
const empati = require('./agent/perception');
|
|
6197
|
+
const empatiRuntime = { running: false, timer: null };
|
|
6198
|
+
|
|
6199
|
+
function empatiCfg() {
|
|
6200
|
+
return empati.mergeCfg(settings.empati || {});
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
function empatiLog(line) {
|
|
6204
|
+
try { waLog('[EMPATİ] ' + line); } catch {}
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
/* tarama (filtre) modeli: sekmeden seçilmişse onu çöz; seçilmemişse ANA model */
|
|
6208
|
+
function empatiFilterSel() {
|
|
6209
|
+
const fm = empatiCfg().filterModel;
|
|
6210
|
+
if (fm) {
|
|
6211
|
+
try {
|
|
6212
|
+
const r = engine._resolve(fm);
|
|
6213
|
+
if (r) return r;
|
|
6214
|
+
} catch {}
|
|
6215
|
+
}
|
|
6216
|
+
return engine.sel;
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
/* sinyal toplayıcılar — perception modülü saf kalır, engine/köprülerle burada konuşur */
|
|
6220
|
+
async function empatiSignalSelf() {
|
|
6221
|
+
const out = [];
|
|
6222
|
+
try {
|
|
6223
|
+
const w = engine.lastWhereWasI();
|
|
6224
|
+
if (w && w.pendingTodos && w.pendingTodos.length) {
|
|
6225
|
+
out.push({
|
|
6226
|
+
type: 'todo',
|
|
6227
|
+
title: 'Yarım kalan görevler: ' + w.pendingTodos.map((t) => t.title).join(' · ').slice(0, 200),
|
|
6228
|
+
detail: 'oturum ' + (w.code || '') + ' · ' + w.pendingTodos.length + ' görev bekliyor',
|
|
6229
|
+
});
|
|
6230
|
+
}
|
|
6231
|
+
} catch {}
|
|
6232
|
+
return out;
|
|
6233
|
+
}
|
|
6234
|
+
|
|
6235
|
+
async function empatiSignalNews() {
|
|
6236
|
+
const topics = String(empatiCfg().newsTopics || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
6237
|
+
if (!topics.length) return [];
|
|
6238
|
+
return empati.fetchNews(topics);
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
/* tek toplu filtre çağrısı (maliyet freni); model yok/çökerse boş → deterministik puan */
|
|
6242
|
+
function empatiLlmFilter(prompt) {
|
|
6243
|
+
const sel = empatiFilterSel();
|
|
6244
|
+
if (!sel) return Promise.resolve('');
|
|
6245
|
+
const ctrl = new AbortController();
|
|
6246
|
+
const kill = setTimeout(() => ctrl.abort(), 45000);
|
|
6247
|
+
return require('./agent/llm')
|
|
6248
|
+
.chatOnce(sel, {
|
|
6249
|
+
messages: [
|
|
6250
|
+
{ role: 'system', content: empati.FILTER_SYSTEM },
|
|
6251
|
+
{ role: 'user', content: prompt },
|
|
6252
|
+
],
|
|
6253
|
+
temperature: 0.1,
|
|
6254
|
+
}, { signal: ctrl.signal })
|
|
6255
|
+
.then((r) => String(r.content || ''))
|
|
6256
|
+
.catch(() => '')
|
|
6257
|
+
.finally(() => clearTimeout(kill));
|
|
6258
|
+
}
|
|
6259
|
+
|
|
6260
|
+
/* compose her zaman ANA model kullanır — filtre ucuz, anlamlandırma güçlü */
|
|
6261
|
+
function empatiLlmCompose(prompt) {
|
|
6262
|
+
if (!engine.sel) return Promise.resolve('');
|
|
6263
|
+
const ctrl = new AbortController();
|
|
6264
|
+
const kill = setTimeout(() => ctrl.abort(), 60000);
|
|
6265
|
+
return require('./agent/llm')
|
|
6266
|
+
.chatOnce(engine.sel, {
|
|
6267
|
+
messages: [
|
|
6268
|
+
{ role: 'system', content: empati.COMPOSE_SYSTEM },
|
|
6269
|
+
{ role: 'user', content: prompt },
|
|
6270
|
+
],
|
|
6271
|
+
temperature: 0.6,
|
|
6272
|
+
}, { signal: ctrl.signal })
|
|
6273
|
+
.then((r) => String(r.content || '').trim().slice(0, 600))
|
|
6274
|
+
.catch(() => '')
|
|
6275
|
+
.finally(() => clearTimeout(kill));
|
|
6276
|
+
}
|
|
6277
|
+
|
|
6278
|
+
/* bildirim hedefi: sekmeden seçilen entegrasyon; seçilmemişse bağlı olanlar.
|
|
6279
|
+
Hiçbir entegrasyon yazılamazsa masaüstü chat UI (toast) kalır. */
|
|
6280
|
+
function empatiNotify(text, ev) {
|
|
6281
|
+
const cfg = empatiCfg();
|
|
6282
|
+
const senders = [];
|
|
6283
|
+
const tryWa = () => {
|
|
6284
|
+
try {
|
|
6285
|
+
const own = waOwnerNum();
|
|
6286
|
+
if (own && wa && wa.connected) senders.push(() => sendWaSafe(own + '@s.whatsapp.net', '🫡 *Beast proaktif:*\n' + text));
|
|
6287
|
+
} catch {}
|
|
6288
|
+
};
|
|
6289
|
+
const tryTg = () => {
|
|
6290
|
+
try {
|
|
6291
|
+
if (tg && tg.connected) for (const id of tgOwnerIds()) senders.push(() => sendTgSafe(id, '🫡 *Beast proaktif:*\n' + text));
|
|
6292
|
+
} catch {}
|
|
6293
|
+
};
|
|
6294
|
+
const tryDc = () => {
|
|
6295
|
+
try {
|
|
6296
|
+
if (dc && dc.connected) for (const id of dcOwnerIds()) senders.push(() => sendDcSafe(id, '🫡 **Beast proaktif:**\n' + text));
|
|
6297
|
+
} catch {}
|
|
6298
|
+
};
|
|
6299
|
+
if (cfg.notifyTarget === 'whatsapp') tryWa();
|
|
6300
|
+
else if (cfg.notifyTarget === 'telegram') tryTg();
|
|
6301
|
+
else if (cfg.notifyTarget === 'discord') tryDc();
|
|
6302
|
+
else { tryWa(); tryTg(); tryDc(); } // auto: ekli/bağlı entegrasyonlar
|
|
6303
|
+
let sent = 0;
|
|
6304
|
+
for (const fn of senders) {
|
|
6305
|
+
try { fn(); sent++; } catch {}
|
|
6306
|
+
}
|
|
6307
|
+
/* hiçbir entegrasyona yazılamadıysa yalnız masaüstü chat UI'a düş */
|
|
6308
|
+
try {
|
|
6309
|
+
if (!sent && win && !win.isDestroyed()) {
|
|
6310
|
+
win.webContents.send('agent:event', { type: 'proactive', id: ev.id, level: ev.level, title: ev.title, text });
|
|
6311
|
+
}
|
|
6312
|
+
} catch {}
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
async function empatiCycle(manual) {
|
|
6316
|
+
if (empatiRuntime.running) return { ok: false, error: 'döngü zaten çalışıyor' };
|
|
6317
|
+
const cfg = empatiCfg();
|
|
6318
|
+
if (!cfg.enabled && !manual) return { ok: false, error: 'kapalı' };
|
|
6319
|
+
empatiRuntime.running = true;
|
|
6320
|
+
try {
|
|
6321
|
+
const r = await empati.runCycle({
|
|
6322
|
+
cfg,
|
|
6323
|
+
signals: { self: empatiSignalSelf, news: empatiSignalNews },
|
|
6324
|
+
llmFilter: empatiLlmFilter,
|
|
6325
|
+
now: new Date(),
|
|
6326
|
+
log: empatiLog,
|
|
6327
|
+
});
|
|
6328
|
+
let notified = 0;
|
|
6329
|
+
for (const a of r.actions) {
|
|
6330
|
+
let text = '';
|
|
6331
|
+
try { text = await empatiLlmCompose(empati.composePrompt(a.event, cfg)); } catch {}
|
|
6332
|
+
if (!text) text = empati.composeFallback(a.event);
|
|
6333
|
+
empatiNotify(text, a.event);
|
|
6334
|
+
empati.markNotified(a.event.id, a.level, text, cfg.cooldownMin);
|
|
6335
|
+
notified++;
|
|
6336
|
+
}
|
|
6337
|
+
if (notified && win && !win.isDestroyed()) {
|
|
6338
|
+
win.webContents.send('agent:event', { type: 'empati', notified });
|
|
6339
|
+
}
|
|
6340
|
+
return { ok: true, ...r.summary, notified };
|
|
6341
|
+
} finally {
|
|
6342
|
+
empatiRuntime.running = false;
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6346
|
+
function empatiSchedule() {
|
|
6347
|
+
if (empatiRuntime.timer) clearTimeout(empatiRuntime.timer);
|
|
6348
|
+
empatiRuntime.timer = null;
|
|
6349
|
+
const cfg = empatiCfg();
|
|
6350
|
+
if (!cfg.enabled) return;
|
|
6351
|
+
empatiRuntime.timer = setTimeout(async () => {
|
|
6352
|
+
try { await empatiCycle(false); } catch {}
|
|
6353
|
+
empatiSchedule();
|
|
6354
|
+
}, cfg.intervalMin * 60000);
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6357
|
+
/* açılış + 90 sn: ilk tarama (başlangıç fırtınasını önle), sonra cfg aralığı */
|
|
6358
|
+
function empatiKickoff() {
|
|
6359
|
+
setTimeout(() => {
|
|
6360
|
+
empatiCycle(false).catch(() => {});
|
|
6361
|
+
empatiSchedule();
|
|
6362
|
+
}, 90 * 1000);
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6365
|
+
ipcMain.handle('empati:get', () => ({ ...empatiCfg(), running: empatiRuntime.running, lastRunAt: empati.lastRunAt() }));
|
|
6366
|
+
ipcMain.handle('empati:set', (_e, patch) => {
|
|
6367
|
+
const cfg = empati.mergeCfg({ ...empatiCfg(), ...(patch || {}) });
|
|
6368
|
+
settings.empati = cfg;
|
|
6369
|
+
saveSettings();
|
|
6370
|
+
empatiSchedule(); // aralık/model değişmiş olabilir
|
|
6371
|
+
return { ...cfg };
|
|
6372
|
+
});
|
|
6373
|
+
ipcMain.handle('empati:scan', async () => {
|
|
6374
|
+
try { return await empatiCycle(true); } catch (e) { return { ok: false, error: String((e && e.message) || e).slice(0, 200) }; }
|
|
6375
|
+
});
|
|
6376
|
+
ipcMain.handle('empati:events', () => empati.listEvents(80));
|
|
6377
|
+
|
|
6190
6378
|
ipcMain.handle('cron:list', () => cron.list());
|
|
6191
6379
|
/* #23 Fallout: provider → kayıtlı API key haritası.
|
|
6192
6380
|
Birincil kaynak: engine chain (config+custom+env çözülmüş).
|
package/src/preload.js
CHANGED
|
@@ -207,4 +207,8 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
207
207
|
ideDelete: (rel) => ipcRenderer.invoke('ide:delete', rel),
|
|
208
208
|
onWaEvent: (cb) => ipcRenderer.on('wa:event', (_e, ev) => cb(ev)),
|
|
209
209
|
onEvent: (cb) => ipcRenderer.on('agent:event', (_e, ev) => cb(ev)),
|
|
210
|
+
empatiGet: () => ipcRenderer.invoke('empati:get'),
|
|
211
|
+
empatiSet: (patch) => ipcRenderer.invoke('empati:set', patch),
|
|
212
|
+
empatiScan: () => ipcRenderer.invoke('empati:scan'),
|
|
213
|
+
empatiEvents: () => ipcRenderer.invoke('empati:events'),
|
|
210
214
|
});
|
package/src/renderer/i18n.js
CHANGED
|
@@ -114,6 +114,40 @@
|
|
|
114
114
|
up_install_now: 'Yeniden Başlat & Kur',
|
|
115
115
|
up_note: 'Kurulumdan sonra uygulama otomatik yeniden başlar. İstersen WhatsApp\u2019tan /update komutunu da kullanabilirsin.',
|
|
116
116
|
tab_events: 'Olay Merkezi',
|
|
117
|
+
tab_empati: 'Empati Loop',
|
|
118
|
+
em_h2: 'Empati Loop — Proaktif Algı',
|
|
119
|
+
em_sub: 'Beast arka planda periyodik tarar: yarım kalan işler, haber akışı. Boru hattı: sinyal → ucuz filtre modeli → öncelik puanı → bildirim. Önemsiz olaylar yalnız depoya yazılır, seni rahatsız etmez.',
|
|
120
|
+
em_ipc_err: 'Empati servisine ulaşılamadı.',
|
|
121
|
+
em_on: 'Empati Loop açık',
|
|
122
|
+
em_interval: 'Tarama aralığı (dk)',
|
|
123
|
+
em_min_notify: 'Bildirim eşiği (puan)',
|
|
124
|
+
em_cooldown: 'Konu sessizliği (dk)',
|
|
125
|
+
em_model: 'Tarama (filtre) modeli',
|
|
126
|
+
em_model_main: 'Ana model (seçili)',
|
|
127
|
+
em_model_sub: 'Seçmezsen tarama ana modelle yapılır. Anlamlandırma (mesaj yazımı) her zaman ana modelle.',
|
|
128
|
+
em_notify: 'Bildirim hedefi',
|
|
129
|
+
em_notify_auto: 'Otomatik — bağlı entegrasyonlar',
|
|
130
|
+
em_notify_wa: 'WhatsApp',
|
|
131
|
+
em_notify_tg: 'Telegram',
|
|
132
|
+
em_notify_dc: 'Discord',
|
|
133
|
+
em_notify_sub: 'Seçmezsen ekli ve bağlı entegrasyonların hepsine yazar; hiçbiri bağlı değilse masaüstü sohbete düşer.',
|
|
134
|
+
em_interests: 'İlgi alanların — haber filtresi buna göre ağırlıklandırılır',
|
|
135
|
+
em_interests_ph: 'örn: yapay zeka, yazılım, trading, open source',
|
|
136
|
+
em_news: 'Haber kaynağı (Google News)',
|
|
137
|
+
em_news_topics_ph: 'haber konuları, virgülle: yapay zeka, ekonomi…',
|
|
138
|
+
em_scan: 'Şimdi Tara',
|
|
139
|
+
em_scan_ok: 'Tarama: {raw} sinyal · {queued} bildirim · {stored} depo',
|
|
140
|
+
em_events: 'Son olaylar',
|
|
141
|
+
em_no_events: 'Henüz olay yok — Şimdi Tara ile deneyebilirsin.',
|
|
142
|
+
em_saved: 'Empati ayarları kaydedildi',
|
|
143
|
+
em_lastrun: 'Son tarama',
|
|
144
|
+
em_note: 'İzleyiciler (watchers) kendi bildirimlerini zaten yapıyor; Empati Loop onları tekrar bildirmez. Filtre modeli tek toplu çağrıyla çalışır; model yoksa puanlama deterministik yapılır.',
|
|
145
|
+
em_st_notified: 'bildirildi',
|
|
146
|
+
em_st_queued: 'kuyrukta',
|
|
147
|
+
em_st_stored: 'depo',
|
|
148
|
+
em_st_ignored: 'yok sayıldı',
|
|
149
|
+
em_lv_high: 'YÜKSEK',
|
|
150
|
+
em_lv_medium: 'ORTA',
|
|
117
151
|
tab_cron: 'Cron',
|
|
118
152
|
tab_usage: 'Maliyet · Limit',
|
|
119
153
|
tab_logs: 'Log',
|
|
@@ -691,6 +725,40 @@
|
|
|
691
725
|
up_install_now: 'Restart & Install',
|
|
692
726
|
up_note: 'The app restarts itself after installing. You can also use the /update command from WhatsApp.',
|
|
693
727
|
tab_events: 'Event Center',
|
|
728
|
+
tab_empati: 'Empathy Loop',
|
|
729
|
+
em_h2: 'Empathy Loop — Proactive Perception',
|
|
730
|
+
em_sub: 'Beast scans periodically in the background: half-done work, news flow. Pipeline: signal → cheap filter model → priority score → notification. Unimportant events are only stored, they never bother you.',
|
|
731
|
+
em_ipc_err: 'Could not reach the empathy service.',
|
|
732
|
+
em_on: 'Empathy Loop enabled',
|
|
733
|
+
em_interval: 'Scan interval (min)',
|
|
734
|
+
em_min_notify: 'Notify threshold (score)',
|
|
735
|
+
em_cooldown: 'Topic silence (min)',
|
|
736
|
+
em_model: 'Scan (filter) model',
|
|
737
|
+
em_model_main: 'Main model (selected)',
|
|
738
|
+
em_model_sub: 'If left empty, scanning uses the main model. Composing the message always uses the main model.',
|
|
739
|
+
em_notify: 'Notification target',
|
|
740
|
+
em_notify_auto: 'Auto — connected integrations',
|
|
741
|
+
em_notify_wa: 'WhatsApp',
|
|
742
|
+
em_notify_tg: 'Telegram',
|
|
743
|
+
em_notify_dc: 'Discord',
|
|
744
|
+
em_notify_sub: 'If left empty, it writes to all connected integrations; with none connected it lands in the desktop chat.',
|
|
745
|
+
em_interests: 'Your interests — news filtering is weighted by these',
|
|
746
|
+
em_interests_ph: 'e.g. AI, software, trading, open source',
|
|
747
|
+
em_news: 'News source (Google News)',
|
|
748
|
+
em_news_topics_ph: 'news topics, comma separated: AI, economy…',
|
|
749
|
+
em_scan: 'Scan Now',
|
|
750
|
+
em_scan_ok: 'Scan: {raw} signals · {queued} notify · {stored} stored',
|
|
751
|
+
em_events: 'Recent events',
|
|
752
|
+
em_no_events: 'No events yet — try Scan Now.',
|
|
753
|
+
em_saved: 'Empathy settings saved',
|
|
754
|
+
em_lastrun: 'Last scan',
|
|
755
|
+
em_note: 'Watchers already notify on their own; the Empathy Loop does not repeat them. The filter model runs in a single batched call; without a model, scoring is deterministic.',
|
|
756
|
+
em_st_notified: 'notified',
|
|
757
|
+
em_st_queued: 'queued',
|
|
758
|
+
em_st_stored: 'stored',
|
|
759
|
+
em_st_ignored: 'ignored',
|
|
760
|
+
em_lv_high: 'HIGH',
|
|
761
|
+
em_lv_medium: 'MEDIUM',
|
|
694
762
|
tab_cron: 'Cron',
|
|
695
763
|
tab_usage: 'Cost · Limit',
|
|
696
764
|
tab_logs: 'Logs',
|
package/src/renderer/index.html
CHANGED
|
@@ -239,6 +239,7 @@
|
|
|
239
239
|
<button class="tab" data-tab="websearch" data-i18n="tab_websearch">Web Arama</button>
|
|
240
240
|
<button class="tab" data-tab="mcp" data-i18n="tab_mcp">MCP</button>
|
|
241
241
|
<button class="tab" data-tab="events" data-i18n="tab_events">Olay Merkezi</button>
|
|
242
|
+
<button class="tab" data-tab="empati" data-i18n="tab_empati">Empati Loop</button>
|
|
242
243
|
<button class="tab" data-tab="cron" data-i18n="tab_cron">Cron</button>
|
|
243
244
|
<button class="tab" data-tab="usage" data-i18n="tab_usage">Maliyet · Limit</button>
|
|
244
245
|
<button class="tab" data-tab="logs" data-i18n="tab_logs">Log</button>
|
|
@@ -264,6 +265,7 @@
|
|
|
264
265
|
<div id="tab-websearch" class="pane" hidden></div>
|
|
265
266
|
<div id="tab-mcp" class="pane" hidden></div>
|
|
266
267
|
<div id="tab-events" class="pane" hidden></div>
|
|
268
|
+
<div id="tab-empati" class="pane" hidden></div>
|
|
267
269
|
<div id="tab-usage" class="pane" hidden></div>
|
|
268
270
|
<div id="tab-logs" class="pane" hidden></div>
|
|
269
271
|
<div id="tab-dash" class="pane" hidden></div>
|
package/src/renderer/renderer.js
CHANGED
|
@@ -1066,6 +1066,7 @@ async function renderActiveSettingsTab() {
|
|
|
1066
1066
|
case 'websearch': await renderWebSearchPane(); break;
|
|
1067
1067
|
case 'mcp': await renderMcpPane(); break;
|
|
1068
1068
|
case 'events': await renderEventsPane(); break;
|
|
1069
|
+
case 'empati': await renderEmpatiPane(); break;
|
|
1069
1070
|
case 'cron': await openCron(); break;
|
|
1070
1071
|
case 'usage': await renderUsagePane(); break;
|
|
1071
1072
|
case 'agents': await refreshAgentsPane(); break;
|
|
@@ -1151,7 +1152,7 @@ function switchTab(name) {
|
|
|
1151
1152
|
document.querySelectorAll('#setTabs .tab').forEach((b) =>
|
|
1152
1153
|
b.classList.toggle('active', b.dataset.tab === name)
|
|
1153
1154
|
);
|
|
1154
|
-
|
|
1155
|
+
for (const p of ['lang', 'provider', 'fallout', 'skills', 'agents', 'tts', 'install', 'email', 'integrations', 'websearch', 'mcp', 'events', 'empati', 'cron', 'usage', 'logs', 'dash', 'sec', 'update']) {
|
|
1155
1156
|
const el = $('#tab-' + p);
|
|
1156
1157
|
if (el) el.hidden = p !== name; // guard: eksik pane tüm sekmeleri kilitlemesin
|
|
1157
1158
|
}
|
|
@@ -1160,6 +1161,7 @@ function switchTab(name) {
|
|
|
1160
1161
|
if (name === 'usage') renderUsagePane();
|
|
1161
1162
|
if (name === 'install') renderInstallPane();
|
|
1162
1163
|
if (name === 'events') renderEventsPane();
|
|
1164
|
+
if (name === 'empati') renderEmpatiPane();
|
|
1163
1165
|
if (name === 'logs') renderLogPane();
|
|
1164
1166
|
if (name === 'dash') renderDashboardPane();
|
|
1165
1167
|
if (name === 'sec') renderSecurityPane();
|
|
@@ -3022,6 +3024,136 @@ async function renderEventsPane() {
|
|
|
3022
3024
|
});
|
|
3023
3025
|
}
|
|
3024
3026
|
|
|
3027
|
+
/* ---------------- EMPATİ LOOP (proaktif algı sekmesi) ---------------- */
|
|
3028
|
+
async function renderEmpatiPane() {
|
|
3029
|
+
const pane = $('#tab-empati');
|
|
3030
|
+
if (!pane) return;
|
|
3031
|
+
const cfg = await beast.empatiGet().catch(() => null);
|
|
3032
|
+
let events = [];
|
|
3033
|
+
try { events = await beast.empatiEvents(); } catch {}
|
|
3034
|
+
let models = [];
|
|
3035
|
+
try { models = (await beast.getState()).models || []; } catch {}
|
|
3036
|
+
if (!cfg) {
|
|
3037
|
+
pane.innerHTML = '<h2>' + _t('em_h2') + '</h2><div class="sub">' + _t('em_ipc_err') + '</div>';
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
const modelOpts =
|
|
3041
|
+
'<option value="">' + _t('em_model_main') + '</option>' +
|
|
3042
|
+
models.map((m) =>
|
|
3043
|
+
'<option value="' + escapeHtml(m.sel || '') + '"' + (cfg.filterModel === m.sel ? ' selected' : '') + '>' +
|
|
3044
|
+
escapeHtml((m.providerName || '') + ' · ' + (m.model || '')) + '</option>'
|
|
3045
|
+
).join('');
|
|
3046
|
+
const targetOpts = [
|
|
3047
|
+
['whatsapp', _t('em_notify_wa')],
|
|
3048
|
+
['telegram', _t('em_notify_tg')],
|
|
3049
|
+
['discord', _t('em_notify_dc')],
|
|
3050
|
+
].map(([v, lbl]) =>
|
|
3051
|
+
'<option value="' + v + '"' + (cfg.notifyTarget === v ? ' selected' : '') + '>' + escapeHtml(lbl) + '</option>'
|
|
3052
|
+
).join('');
|
|
3053
|
+
const when = (iso) => {
|
|
3054
|
+
try {
|
|
3055
|
+
return new Date(iso).toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
3056
|
+
} catch { return String(iso || ''); }
|
|
3057
|
+
};
|
|
3058
|
+
const statusT = { notified: _t('em_st_notified'), queued: _t('em_st_queued'), stored: _t('em_st_stored'), ignored: _t('em_st_ignored') };
|
|
3059
|
+
const lvBadge = (e) => {
|
|
3060
|
+
if (!e.level) return '';
|
|
3061
|
+
const color = e.level === 'high' ? '#e06c75' : '#d9a441';
|
|
3062
|
+
return ' · <b style="color:' + color + '">' + (e.level === 'high' ? _t('em_lv_high') : _t('em_lv_medium')) + '</b>';
|
|
3063
|
+
};
|
|
3064
|
+
let rows = '';
|
|
3065
|
+
for (const e of events) {
|
|
3066
|
+
rows +=
|
|
3067
|
+
'<div class="usage-row" style="align-items:flex-start">' +
|
|
3068
|
+
'<div style="min-width:0;flex:1">' +
|
|
3069
|
+
'<div style="font-size:12px">' + escapeHtml(e.title || '') + '</div>' +
|
|
3070
|
+
'<div class="ur-meta">' + escapeHtml(e.source || '') + ' · ' + when(e.ts) + ' · %' + (e.priority || 0) +
|
|
3071
|
+
(e.reason ? ' · ' + escapeHtml(e.reason) : '') + '</div>' +
|
|
3072
|
+
(e.text ? '<div class="sub" style="font-size:11px">' + escapeHtml(e.text) + '</div>' : '') +
|
|
3073
|
+
'</div>' +
|
|
3074
|
+
'<span style="flex:none;font-size:11px;color:var(--muted)">' + (statusT[e.status] || e.status) + lvBadge(e) + '</span>' +
|
|
3075
|
+
'</div>';
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
pane.innerHTML =
|
|
3079
|
+
'<h2>' + _t('em_h2') + '</h2>' +
|
|
3080
|
+
'<div class="sub">' + _t('em_sub') + '</div>' +
|
|
3081
|
+
(cfg.lastRunAt ? '<div class="sub">' + _t('em_lastrun') + ': ' + when(cfg.lastRunAt) + (cfg.running ? ' · ⏳' : '') + '</div>' : '') +
|
|
3082
|
+
`<div class="fo-toggles" style="margin-top:10px">
|
|
3083
|
+
<label class="lock-row"><input type="checkbox" id="emOn" ${cfg.enabled ? 'checked' : ''}/><span>${_t('em_on')}</span></label>
|
|
3084
|
+
</div>
|
|
3085
|
+
<div id="emDetail" style="${cfg.enabled ? '' : 'opacity:.5'}">
|
|
3086
|
+
<div class="form-grid" style="grid-template-columns:1fr 1fr 1fr;margin-top:8px">
|
|
3087
|
+
<div><label class="sub">${_t('em_interval')}</label><input id="emInterval" class="inp" type="number" min="3" max="1440" value="${cfg.intervalMin}"/></div>
|
|
3088
|
+
<div><label class="sub">${_t('em_min_notify')}</label><input id="emMin" class="inp" type="number" min="0" max="100" value="${cfg.minNotifyPriority}"/></div>
|
|
3089
|
+
<div><label class="sub">${_t('em_cooldown')}</label><input id="emCd" class="inp" type="number" min="0" max="10080" value="${cfg.cooldownMin}"/></div>
|
|
3090
|
+
</div>
|
|
3091
|
+
<div style="margin-top:8px">
|
|
3092
|
+
<label class="sub">${_t('em_model')}</label>
|
|
3093
|
+
<select id="emModel" class="inp">${modelOpts}</select>
|
|
3094
|
+
<div class="sub">${_t('em_model_sub')}</div>
|
|
3095
|
+
</div>
|
|
3096
|
+
<div style="margin-top:8px">
|
|
3097
|
+
<label class="sub">${_t('em_notify')}</label>
|
|
3098
|
+
<select id="emTarget" class="inp"><option value="">${_t('em_notify_auto')}</option>${targetOpts}</select>
|
|
3099
|
+
<div class="sub">${_t('em_notify_sub')}</div>
|
|
3100
|
+
</div>
|
|
3101
|
+
<div style="margin-top:8px">
|
|
3102
|
+
<label class="sub">${_t('em_interests')}</label>
|
|
3103
|
+
<textarea id="emInterests" class="inp" rows="2" placeholder="${_t('em_interests_ph')}">${escapeHtml(cfg.interests || '')}</textarea>
|
|
3104
|
+
</div>
|
|
3105
|
+
<label class="lock-row" style="margin-top:8px"><input type="checkbox" id="emNews" ${cfg.newsTopics ? 'checked' : ''}/><span>${_t('em_news')}</span></label>
|
|
3106
|
+
<input id="emTopics" class="inp" style="${cfg.newsTopics ? '' : 'opacity:.5'}" placeholder="${_t('em_news_topics_ph')}" value="${escapeHtml(cfg.newsTopics || '')}" spellcheck="false"/>
|
|
3107
|
+
<div style="display:flex;gap:8px;margin-top:10px;align-items:center">
|
|
3108
|
+
<button id="emSave" class="btn">${_t('mcp_save')}</button>
|
|
3109
|
+
<button id="emScan" class="btn ghost">${_t('em_scan')}</button>
|
|
3110
|
+
<span id="emMsg" class="sub" style="margin:0"></span>
|
|
3111
|
+
</div>
|
|
3112
|
+
<div class="sub" style="margin-top:6px">${_t('em_note')}</div>
|
|
3113
|
+
</div>
|
|
3114
|
+
<h3 style="margin-top:16px;color:var(--muted)">${_t('em_events')}</h3>`;
|
|
3115
|
+
const wrap = document.createElement('div');
|
|
3116
|
+
if (!rows) wrap.innerHTML = '<p class="sub">' + _t('em_no_events') + '</p>';
|
|
3117
|
+
else wrap.innerHTML = rows;
|
|
3118
|
+
pane.appendChild(wrap);
|
|
3119
|
+
|
|
3120
|
+
const emOn = $('#emOn');
|
|
3121
|
+
const emDetail = $('#emDetail');
|
|
3122
|
+
const emNews = $('#emNews');
|
|
3123
|
+
const emTopics = $('#emTopics');
|
|
3124
|
+
emOn.addEventListener('change', () => { emDetail.style.opacity = emOn.checked ? '' : '.5'; });
|
|
3125
|
+
emNews.addEventListener('change', () => { emTopics.style.opacity = emNews.checked ? '' : '.5'; });
|
|
3126
|
+
$('#emSave').addEventListener('click', async () => {
|
|
3127
|
+
const patch = {
|
|
3128
|
+
enabled: emOn.checked,
|
|
3129
|
+
intervalMin: Number($('#emInterval').value) || cfg.intervalMin,
|
|
3130
|
+
minNotifyPriority: Number($('#emMin').value),
|
|
3131
|
+
cooldownMin: Number($('#emCd').value),
|
|
3132
|
+
notifyTarget: $('#emTarget') ? $('#emTarget').value : '',
|
|
3133
|
+
filterModel: $('#emModel') ? $('#emModel').value : '',
|
|
3134
|
+
interests: $('#emInterests').value.trim(),
|
|
3135
|
+
newsTopics: emNews.checked ? emTopics.value.trim() : '',
|
|
3136
|
+
};
|
|
3137
|
+
const r = await beast.empatiSet(patch).catch(() => null);
|
|
3138
|
+
toast(r ? _t('em_saved') : 'Hata');
|
|
3139
|
+
if (r) renderEmpatiPane();
|
|
3140
|
+
});
|
|
3141
|
+
$('#emScan').addEventListener('click', async () => {
|
|
3142
|
+
const msg = $('#emMsg');
|
|
3143
|
+
msg.textContent = '⏳';
|
|
3144
|
+
const r = await beast.empatiScan().catch(() => null);
|
|
3145
|
+
if (r && r.ok) {
|
|
3146
|
+
msg.textContent = _t('em_scan_ok')
|
|
3147
|
+
.replace('{raw}', String(r.raw || 0))
|
|
3148
|
+
.replace('{queued}', String(r.queued || 0))
|
|
3149
|
+
.replace('{stored}', String(r.stored || 0));
|
|
3150
|
+
renderEmpatiPane();
|
|
3151
|
+
} else {
|
|
3152
|
+
msg.textContent = (r && r.error) || 'hata';
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3025
3157
|
async function renderWaAllow() {
|
|
3026
3158
|
const wrap = $('#waAllowChips');
|
|
3027
3159
|
if (!wrap) return;
|
|
@@ -4706,6 +4838,15 @@ function onEvent(ev) {
|
|
|
4706
4838
|
return;
|
|
4707
4839
|
}
|
|
4708
4840
|
if (ev.type === 'install-progress') { updateInstallPct(ev); return; }
|
|
4841
|
+
/* EMPATİ LOOP: proaktif bildirim (masaüstü) + sekme canlı yenileme */
|
|
4842
|
+
if (ev.type === 'proactive') {
|
|
4843
|
+
toast('🫡 ' + (ev.text || ev.title || ''));
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
if (ev.type === 'empati') {
|
|
4847
|
+
if (!els.settingsOverlay.hidden && setTab === 'empati') renderEmpatiPane();
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4709
4850
|
if (ev.type === 'update') {
|
|
4710
4851
|
if (ev.downloaded) toast(_t('up_downloaded') + ' (v' + (ev.version || '?') + ') — /update now');
|
|
4711
4852
|
else if (ev.available && ev.version && ev.version !== ev.current) toast(_t('up_available') + ' (v' + ev.version + ')');
|