beast-agent 1.9.0 → 2.0.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/LICENSE +21 -21
- package/README.md +130 -130
- package/bin/beast-agent.js +137 -137
- package/package.json +1 -1
- package/scripts/fix-electron.js +138 -138
- package/scripts/release.js +137 -131
- package/scripts/swap-electron.js +40 -40
- package/src/agent/agentdefs.js +122 -122
- package/src/agent/bots.js +580 -580
- package/src/agent/bus.js +389 -389
- package/src/agent/computeruse.js +200 -200
- package/src/agent/config.js +284 -284
- package/src/agent/discord.js +332 -332
- package/src/agent/engine.js +75 -10
- package/src/agent/kb.js +123 -123
- package/src/agent/llm.js +430 -430
- package/src/agent/logger.js +90 -90
- package/src/agent/mcp.js +427 -427
- package/src/agent/mem0.js +605 -605
- package/src/agent/memory.js +427 -427
- package/src/agent/mqueue.js +124 -124
- package/src/agent/pdf.js +20 -20
- package/src/agent/research.js +133 -133
- package/src/agent/scripts/news.py +113 -113
- package/src/agent/scripts/stealthsearch.py +30 -30
- package/src/agent/scripts/websearch.py +225 -225
- package/src/agent/searxng.js +325 -325
- package/src/agent/seeds/brainstorming/SKILL.md +90 -90
- package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
- package/src/agent/seeds/executing-plans/SKILL.md +60 -60
- package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
- package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
- package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
- package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
- package/src/agent/seeds/writing-plans/SKILL.md +162 -162
- package/src/agent/seeds/writing-skills/SKILL.md +229 -229
- package/src/agent/skills.js +652 -652
- package/src/agent/store.js +378 -378
- package/src/agent/telegram.js +155 -155
- package/src/agent/tokens.js +39 -39
- package/src/agent/usage.js +125 -125
- package/src/agent/watchers.js +312 -312
- package/src/agent/watext.js +80 -80
- package/src/agent/whatsapp.js +555 -555
- package/src/cron.js +255 -255
- package/src/main.js +348 -6
- package/src/preload.js +9 -0
- package/src/renderer/browserPreload.js +73 -73
- package/src/renderer/i18n.js +4 -0
- package/src/renderer/index.html +448 -409
- package/src/renderer/renderer.js +824 -41
- package/src/renderer/style.css +264 -5
package/src/agent/bus.js
CHANGED
|
@@ -1,389 +1,389 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* Beast Olay Merkezi (#4 EventBus proaktif mod)
|
|
4
|
-
Cron'suz olay mimarisi: kaynaklar olay üretir, abonelikler bildirim alır.
|
|
5
|
-
|
|
6
|
-
Kaynaklar:
|
|
7
|
-
mail:new — IMAP IDLE (ayarlı hesapta yeni mail)
|
|
8
|
-
fs:changed — workspace fs.watch (dosya/klasör değişimi)
|
|
9
|
-
webhook — 127.0.0.1 POST /beast-event <token> {type,data}
|
|
10
|
-
price:tick — Binance miniTicker WebSocket (sembol ayarlı)
|
|
11
|
-
wa:presence — WhatsApp karşı taraf yazıyor/kaydediyor köprüsü
|
|
12
|
-
|
|
13
|
-
Abonelik: { id, type, sessionId, op?, value?, cooldownMin?, path? }
|
|
14
|
-
Eşleşme: type birebir; varsa sayısal op/value filtresi event.value üzerinde.
|
|
15
|
-
Bildirim teslimi main'e devredilir (engine.send) — bus saf kalır. */
|
|
16
|
-
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const http = require('http');
|
|
20
|
-
const crypto = require('crypto');
|
|
21
|
-
const { beastRoot } = require('./memory');
|
|
22
|
-
|
|
23
|
-
function subsFile() {
|
|
24
|
-
return path.join(beastRoot(), 'subscriptions.json');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let subs = []; // aktif abonelikler
|
|
28
|
-
let loaded = false;
|
|
29
|
-
let hooks = {}; // { notify(sub, event), log(line) }
|
|
30
|
-
let running = {};
|
|
31
|
-
|
|
32
|
-
function uid() {
|
|
33
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function load() {
|
|
37
|
-
if (loaded) return;
|
|
38
|
-
loaded = true;
|
|
39
|
-
try {
|
|
40
|
-
const raw = JSON.parse(fs.readFileSync(subsFile(), 'utf8'));
|
|
41
|
-
subs = Array.isArray(raw.subscriptions) ? raw.subscriptions : [];
|
|
42
|
-
} catch {
|
|
43
|
-
subs = [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function save() {
|
|
48
|
-
try {
|
|
49
|
-
fs.mkdirSync(beastRoot(), { recursive: true });
|
|
50
|
-
const tmp = subsFile() + '.tmp';
|
|
51
|
-
fs.writeFileSync(tmp, JSON.stringify({ subscriptions: subs }, null, 2));
|
|
52
|
-
fs.renameSync(tmp, subsFile());
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function log(line) {
|
|
57
|
-
if (typeof hooks.log === 'function') hooks.log(`[BUS] ${line}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* ---------- abonelik CRUD ---------- */
|
|
61
|
-
|
|
62
|
-
function listSubs() {
|
|
63
|
-
load();
|
|
64
|
-
return subs.map((s) => ({ ...s }));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function addSub({ type, sessionId, op, value, cooldownMin }) {
|
|
68
|
-
load();
|
|
69
|
-
const t = String(type || '').trim();
|
|
70
|
-
if (!t) return { ok: false, error: 'type gerekli' };
|
|
71
|
-
if (!sessionId) return { ok: false, error: 'sessionId gerekli' };
|
|
72
|
-
const KNOWN = ['mail:new', 'fs:changed', 'webhook', 'price:tick', 'wa:presence'];
|
|
73
|
-
if (!KNOWN.includes(t)) return { ok: false, error: `bilinmeyen olay tipi: ${t} (${KNOWN.join(', ')})` };
|
|
74
|
-
const sub = {
|
|
75
|
-
id: uid(),
|
|
76
|
-
type: t,
|
|
77
|
-
sessionId,
|
|
78
|
-
op: ['lt', 'lte', 'gt', 'gte', 'eq'].includes(op) ? op : null,
|
|
79
|
-
value: Number.isFinite(Number(value)) ? Number(value) : null,
|
|
80
|
-
cooldownMin: Math.max(0, Math.min(Math.round(Number(cooldownMin ?? 10)) || 10, 1440)),
|
|
81
|
-
createdAt: new Date().toISOString(),
|
|
82
|
-
lastNotifiedAt: 0,
|
|
83
|
-
};
|
|
84
|
-
if ((t === 'price:tick') && sub.op && sub.value === null) {
|
|
85
|
-
return { ok: false, error: 'fiyat aboneliğinde op verildiyse value de gerekli' };
|
|
86
|
-
}
|
|
87
|
-
subs.push(sub);
|
|
88
|
-
save();
|
|
89
|
-
log(`abonelik eklendi ${t} sid=${sessionId}`);
|
|
90
|
-
return { ok: true, subscription: { ...sub } };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function removeSub(id) {
|
|
94
|
-
load();
|
|
95
|
-
const before = subs.length;
|
|
96
|
-
subs = subs.filter((s) => s.id !== String(id));
|
|
97
|
-
save();
|
|
98
|
-
return { ok: subs.length < before };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/* ---------- eşleştirme + cooldown (saf, test edilebilir) ---------- */
|
|
102
|
-
|
|
103
|
-
function compareOp(op, a, b) {
|
|
104
|
-
switch (op) {
|
|
105
|
-
case 'lt': return a < b;
|
|
106
|
-
case 'lte': return a <= b;
|
|
107
|
-
case 'gt': return a > b;
|
|
108
|
-
case 'gte': return a >= b;
|
|
109
|
-
case 'eq': return a === b;
|
|
110
|
-
default: return false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Olay bu aboneliği tetikler mi + cooldown'a takıldı mı döner */
|
|
115
|
-
function decide(sub, event, now) {
|
|
116
|
-
if (sub.type !== event.type) return { hit: false };
|
|
117
|
-
if (sub.op !== null && sub.value !== null) {
|
|
118
|
-
const v = Number(event.value);
|
|
119
|
-
if (!Number.isFinite(v) || !compareOp(sub.op, v, sub.value)) return { hit: false };
|
|
120
|
-
}
|
|
121
|
-
const cd = (sub.cooldownMin || 0) * 60000;
|
|
122
|
-
if (cd && now - (sub.lastNotifiedAt || 0) < cd) return { hit: true, cooled: true };
|
|
123
|
-
return { hit: true, cooled: false };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Bir olayı tüm aboneliklere dağıt; tetiklenenleri döndürür */
|
|
127
|
-
function emitEvent(type, data = {}, now = Date.now()) {
|
|
128
|
-
load();
|
|
129
|
-
const event = { type, data, value: Number(data.value), at: now };
|
|
130
|
-
const fired = [];
|
|
131
|
-
for (const s of subs.slice()) {
|
|
132
|
-
const d = decide(s, event, now);
|
|
133
|
-
if (!d.hit || d.cooled) continue;
|
|
134
|
-
s.lastNotifiedAt = now;
|
|
135
|
-
fired.push(s);
|
|
136
|
-
if (typeof hooks.notify === 'function') {
|
|
137
|
-
try {
|
|
138
|
-
hooks.notify({ ...s }, summarize(type, data));
|
|
139
|
-
} catch {}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (fired.length) save();
|
|
143
|
-
return fired.length;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function summarize(type, data) {
|
|
147
|
-
switch (type) {
|
|
148
|
-
case 'mail:new':
|
|
149
|
-
return `[OLAY] Yeni e-posta geldi${data.from ? ': ' + data.from : ''}${data.subject ? ' — ' + data.subject : ''}. Kullanıcıya kısaca haber ver; email_read ile gövdeyi isteyebilir.`;
|
|
150
|
-
case 'fs:changed':
|
|
151
|
-
return `[OLAY] Workspace'te dosya değişimi: ${data.path || '?'}. Kullanıcıya kısaca haber ver.`;
|
|
152
|
-
case 'webhook':
|
|
153
|
-
return `[OLAY] Webhook: ${JSON.stringify(data).slice(0, 300)}. Kullanıcıya anlamlı biçimde raporla.`;
|
|
154
|
-
case 'price:tick':
|
|
155
|
-
return `[OLAY] Fiyat güncellemesi ${data.symbol || '?'}: ${data.price} (koşul sağlandı). Kullanıcıya hemen haber ver.`;
|
|
156
|
-
case 'wa:presence':
|
|
157
|
-
return `[OLAY] WhatsApp durum değişimi: ${data.status} (+${data.sender || '?'}). Kısa not düş.`;
|
|
158
|
-
default:
|
|
159
|
-
return `[OLAY] ${type}: ${JSON.stringify(data).slice(0, 200)}`;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/* ---------- kaynaklar ---------- */
|
|
164
|
-
|
|
165
|
-
/* IMAP IDLE: yeni mailleri anında yakalar. Koparsa 60 sn sonra yeniden. */
|
|
166
|
-
async function startMailIdle(getCfg) {
|
|
167
|
-
if (running.mailIdle) return;
|
|
168
|
-
running.mailIdle = true;
|
|
169
|
-
const attempt = async () => {
|
|
170
|
-
while (running.mailIdle) {
|
|
171
|
-
let ImapFlow = null;
|
|
172
|
-
try {
|
|
173
|
-
/* imapflow yeni sürümlerde named export ({ ImapFlow }) — her şekli çöz */
|
|
174
|
-
const _if = require('imapflow');
|
|
175
|
-
ImapFlow = (_if && (_if.ImapFlow || _if.default)) || (typeof _if === 'function' ? _if : null);
|
|
176
|
-
} catch {
|
|
177
|
-
ImapFlow = null;
|
|
178
|
-
}
|
|
179
|
-
if (!ImapFlow) {
|
|
180
|
-
log('imapflow yok — mail:new kapalı');
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const cfg = getCfg() || {};
|
|
184
|
-
if (!cfg.host || !cfg.user || !cfg.pass) {
|
|
185
|
-
log('mail credential yok — IDLE beklemeye çekildi');
|
|
186
|
-
await sleep(120000);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
let client = null;
|
|
190
|
-
try {
|
|
191
|
-
client = new ImapFlow({
|
|
192
|
-
host: cfg.host,
|
|
193
|
-
port: Number(cfg.port) || 993,
|
|
194
|
-
secure: true,
|
|
195
|
-
auth: { user: cfg.user, pass: cfg.pass },
|
|
196
|
-
logger: false,
|
|
197
|
-
});
|
|
198
|
-
await client.connect();
|
|
199
|
-
const lock = await client.getMailboxLock('INBOX');
|
|
200
|
-
log('IMAP IDLE bağlı');
|
|
201
|
-
/* IDLE akışı: EXISTS görünce kısa IDLE'dan çık, olay üret, tekrar gir */
|
|
202
|
-
for (;;) {
|
|
203
|
-
if (!running.mailIdle) break;
|
|
204
|
-
const lockPromise = new Promise((resolve) => client.once && client.once('exists', resolve));
|
|
205
|
-
// imapflow otomatik IDLE yapar; event tabanlı bekleme + periyodik nocop uyanma
|
|
206
|
-
await Promise.race([lockPromise, sleep(5 * 60000)]);
|
|
207
|
-
if (!running.mailIdle) break;
|
|
208
|
-
emitEvent('mail:new', {}, Date.now());
|
|
209
|
-
}
|
|
210
|
-
try { lock.release(); } catch {}
|
|
211
|
-
try { await client.logout(); } catch {}
|
|
212
|
-
} catch (e) {
|
|
213
|
-
log(`IDLE koptu: ${String((e && e.message) || e)} — 60 sn sonra yeniden`);
|
|
214
|
-
try { if (client) await client.logout(); } catch {}
|
|
215
|
-
await sleep(60000);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
attempt().catch(() => {});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function stopMailIdle() {
|
|
223
|
-
running.mailIdle = false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function sleep(ms) {
|
|
227
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/* fs.watch: workspace kökünde hafif izleme */
|
|
231
|
-
function startFsWatch(watchPath) {
|
|
232
|
-
if (running.fsWatch) return;
|
|
233
|
-
try {
|
|
234
|
-
running.fsWatch = fs.watch(
|
|
235
|
-
watchPath,
|
|
236
|
-
{ recursive: true },
|
|
237
|
-
debounceEmit((eventType, filename) => {
|
|
238
|
-
emitEvent('fs:changed', { eventType, path: String(filename || '') }, Date.now());
|
|
239
|
-
}, 5000)
|
|
240
|
-
);
|
|
241
|
-
log(`fs.watch açık: ${watchPath}`);
|
|
242
|
-
} catch (e) {
|
|
243
|
-
log(`fs.watch hata: ${String((e && e.message) || e)}`);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function stopFsWatch() {
|
|
248
|
-
if (running.fsWatch) {
|
|
249
|
-
try { running.fsWatch.close(); } catch {}
|
|
250
|
-
running.fsWatch = null;
|
|
251
|
-
log('fs.watch kapandı');
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/* aynı anda onlarca olayı tek bildirime indirger */
|
|
256
|
-
function debounceEmit(fn, ms) {
|
|
257
|
-
let t = null;
|
|
258
|
-
let lastArgs = null;
|
|
259
|
-
return (...args) => {
|
|
260
|
-
lastArgs = args;
|
|
261
|
-
clearTimeout(t);
|
|
262
|
-
t = setTimeout(() => {
|
|
263
|
-
fn(...lastArgs);
|
|
264
|
-
lastArgs = null;
|
|
265
|
-
}, ms);
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/* local webhook: 127.0.0.1 bind, token doğrulaması zorunlu */
|
|
270
|
-
function startWebhook(port, token) {
|
|
271
|
-
if (running.webhookServer) return;
|
|
272
|
-
const server = http.createServer((req, res) => {
|
|
273
|
-
if (req.method !== 'POST' || !req.url.startsWith('/beast-event')) {
|
|
274
|
-
res.writeHead(404).end();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (!token || req.headers['x-beast-token'] !== token) {
|
|
278
|
-
res.writeHead(401).end('bad token');
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
let body = '';
|
|
282
|
-
req.on('data', (c) => {
|
|
283
|
-
body += c;
|
|
284
|
-
if (body.length > 100000) req.destroy();
|
|
285
|
-
});
|
|
286
|
-
req.on('end', () => {
|
|
287
|
-
try {
|
|
288
|
-
const j = JSON.parse(body || '{}');
|
|
289
|
-
emitEvent(String(j.type || 'webhook'), j.data ?? j, Date.now());
|
|
290
|
-
res.writeHead(200).end('ok');
|
|
291
|
-
} catch {
|
|
292
|
-
res.writeHead(400).end('bad json');
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
try {
|
|
297
|
-
server.listen(port, '127.0.0.1', () => log(`webhook dinliyor: 127.0.0.1:${port}/beast-event`));
|
|
298
|
-
running.webhookServer = server;
|
|
299
|
-
} catch (e) {
|
|
300
|
-
log(`webhook başlatılamadı: ${String((e && e.message) || e)}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function stopWebhook() {
|
|
305
|
-
if (running.webhookServer) {
|
|
306
|
-
try { running.webhookServer.close(); } catch {}
|
|
307
|
-
running.webhookServer = null;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/* fiyat feed'i: Binance miniTicker WS (örn PAXGUSDT ≈ altın peg'i) */
|
|
312
|
-
function startPriceFeed(symbol = 'PAXGUSDT') {
|
|
313
|
-
if (running.priceWs) stopPriceFeed();
|
|
314
|
-
let Ws;
|
|
315
|
-
try {
|
|
316
|
-
Ws = require('ws');
|
|
317
|
-
} catch {
|
|
318
|
-
log('ws modülü yok — fiyat feed kapalı');
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const sym = String(symbol).toLowerCase();
|
|
322
|
-
try {
|
|
323
|
-
const ws = new Ws(`wss://stream.binance.com:9443/ws/${sym}@miniTicker`);
|
|
324
|
-
ws.on('open', () => log(`fiyat WS açık: ${symbol}`));
|
|
325
|
-
ws.on('message', (buf) => {
|
|
326
|
-
try {
|
|
327
|
-
const m = JSON.parse(buf.toString());
|
|
328
|
-
const close = Number(m.c);
|
|
329
|
-
if (Number.isFinite(close)) {
|
|
330
|
-
emitEvent('price:tick', { symbol, price: close, value: close }, Date.now());
|
|
331
|
-
}
|
|
332
|
-
} catch {}
|
|
333
|
-
});
|
|
334
|
-
ws.on('error', (e) => log(`fiyat WS hata: ${String((e && e.message) || e)}`));
|
|
335
|
-
ws.on('close', () => {
|
|
336
|
-
if (running.priceWs === ws) {
|
|
337
|
-
running.priceWs = null;
|
|
338
|
-
if (running.priceWanted) setTimeout(() => startPriceFeed(symbol), 15000);
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
running.priceWanted = true;
|
|
342
|
-
running.priceSymbol = symbol;
|
|
343
|
-
running.priceWs = ws;
|
|
344
|
-
} catch (e) {
|
|
345
|
-
log(`fiyat WS kurulamadı: ${String((e && e.message) || e)}`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function stopPriceFeed() {
|
|
350
|
-
running.priceWanted = false;
|
|
351
|
-
if (running.priceWs) {
|
|
352
|
-
try { running.priceWs.close(); } catch {}
|
|
353
|
-
running.priceWs = null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/* ---------- master ---------- */
|
|
358
|
-
|
|
359
|
-
function start(hooksIn, opts = {}) {
|
|
360
|
-
hooks = { ...hooks, ...(hooksIn || {}) };
|
|
361
|
-
load();
|
|
362
|
-
const o = opts || {};
|
|
363
|
-
if (o.mailIdle) startMailIdle(o.getCfg || (() => null));
|
|
364
|
-
else stopMailIdle();
|
|
365
|
-
if (o.fsWatch && o.workspace) startFsWatch(o.workspace);
|
|
366
|
-
else stopFsWatch();
|
|
367
|
-
if (o.webhookPort && o.webhookToken) startWebhook(o.webhookPort, o.webhookToken);
|
|
368
|
-
else stopWebhook();
|
|
369
|
-
if (o.priceSymbol) startPriceFeed(o.priceSymbol);
|
|
370
|
-
else stopPriceFeed();
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function stopAll() {
|
|
374
|
-
stopMailIdle();
|
|
375
|
-
stopFsWatch();
|
|
376
|
-
stopWebhook();
|
|
377
|
-
stopPriceFeed();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
module.exports = {
|
|
381
|
-
start,
|
|
382
|
-
stopAll,
|
|
383
|
-
listSubs,
|
|
384
|
-
addSub,
|
|
385
|
-
removeSub,
|
|
386
|
-
emitEvent,
|
|
387
|
-
decide,
|
|
388
|
-
compareOp,
|
|
389
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast Olay Merkezi (#4 EventBus proaktif mod)
|
|
4
|
+
Cron'suz olay mimarisi: kaynaklar olay üretir, abonelikler bildirim alır.
|
|
5
|
+
|
|
6
|
+
Kaynaklar:
|
|
7
|
+
mail:new — IMAP IDLE (ayarlı hesapta yeni mail)
|
|
8
|
+
fs:changed — workspace fs.watch (dosya/klasör değişimi)
|
|
9
|
+
webhook — 127.0.0.1 POST /beast-event <token> {type,data}
|
|
10
|
+
price:tick — Binance miniTicker WebSocket (sembol ayarlı)
|
|
11
|
+
wa:presence — WhatsApp karşı taraf yazıyor/kaydediyor köprüsü
|
|
12
|
+
|
|
13
|
+
Abonelik: { id, type, sessionId, op?, value?, cooldownMin?, path? }
|
|
14
|
+
Eşleşme: type birebir; varsa sayısal op/value filtresi event.value üzerinde.
|
|
15
|
+
Bildirim teslimi main'e devredilir (engine.send) — bus saf kalır. */
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const http = require('http');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const { beastRoot } = require('./memory');
|
|
22
|
+
|
|
23
|
+
function subsFile() {
|
|
24
|
+
return path.join(beastRoot(), 'subscriptions.json');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let subs = []; // aktif abonelikler
|
|
28
|
+
let loaded = false;
|
|
29
|
+
let hooks = {}; // { notify(sub, event), log(line) }
|
|
30
|
+
let running = {};
|
|
31
|
+
|
|
32
|
+
function uid() {
|
|
33
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function load() {
|
|
37
|
+
if (loaded) return;
|
|
38
|
+
loaded = true;
|
|
39
|
+
try {
|
|
40
|
+
const raw = JSON.parse(fs.readFileSync(subsFile(), 'utf8'));
|
|
41
|
+
subs = Array.isArray(raw.subscriptions) ? raw.subscriptions : [];
|
|
42
|
+
} catch {
|
|
43
|
+
subs = [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function save() {
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(beastRoot(), { recursive: true });
|
|
50
|
+
const tmp = subsFile() + '.tmp';
|
|
51
|
+
fs.writeFileSync(tmp, JSON.stringify({ subscriptions: subs }, null, 2));
|
|
52
|
+
fs.renameSync(tmp, subsFile());
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function log(line) {
|
|
57
|
+
if (typeof hooks.log === 'function') hooks.log(`[BUS] ${line}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ---------- abonelik CRUD ---------- */
|
|
61
|
+
|
|
62
|
+
function listSubs() {
|
|
63
|
+
load();
|
|
64
|
+
return subs.map((s) => ({ ...s }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function addSub({ type, sessionId, op, value, cooldownMin }) {
|
|
68
|
+
load();
|
|
69
|
+
const t = String(type || '').trim();
|
|
70
|
+
if (!t) return { ok: false, error: 'type gerekli' };
|
|
71
|
+
if (!sessionId) return { ok: false, error: 'sessionId gerekli' };
|
|
72
|
+
const KNOWN = ['mail:new', 'fs:changed', 'webhook', 'price:tick', 'wa:presence'];
|
|
73
|
+
if (!KNOWN.includes(t)) return { ok: false, error: `bilinmeyen olay tipi: ${t} (${KNOWN.join(', ')})` };
|
|
74
|
+
const sub = {
|
|
75
|
+
id: uid(),
|
|
76
|
+
type: t,
|
|
77
|
+
sessionId,
|
|
78
|
+
op: ['lt', 'lte', 'gt', 'gte', 'eq'].includes(op) ? op : null,
|
|
79
|
+
value: Number.isFinite(Number(value)) ? Number(value) : null,
|
|
80
|
+
cooldownMin: Math.max(0, Math.min(Math.round(Number(cooldownMin ?? 10)) || 10, 1440)),
|
|
81
|
+
createdAt: new Date().toISOString(),
|
|
82
|
+
lastNotifiedAt: 0,
|
|
83
|
+
};
|
|
84
|
+
if ((t === 'price:tick') && sub.op && sub.value === null) {
|
|
85
|
+
return { ok: false, error: 'fiyat aboneliğinde op verildiyse value de gerekli' };
|
|
86
|
+
}
|
|
87
|
+
subs.push(sub);
|
|
88
|
+
save();
|
|
89
|
+
log(`abonelik eklendi ${t} sid=${sessionId}`);
|
|
90
|
+
return { ok: true, subscription: { ...sub } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function removeSub(id) {
|
|
94
|
+
load();
|
|
95
|
+
const before = subs.length;
|
|
96
|
+
subs = subs.filter((s) => s.id !== String(id));
|
|
97
|
+
save();
|
|
98
|
+
return { ok: subs.length < before };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ---------- eşleştirme + cooldown (saf, test edilebilir) ---------- */
|
|
102
|
+
|
|
103
|
+
function compareOp(op, a, b) {
|
|
104
|
+
switch (op) {
|
|
105
|
+
case 'lt': return a < b;
|
|
106
|
+
case 'lte': return a <= b;
|
|
107
|
+
case 'gt': return a > b;
|
|
108
|
+
case 'gte': return a >= b;
|
|
109
|
+
case 'eq': return a === b;
|
|
110
|
+
default: return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Olay bu aboneliği tetikler mi + cooldown'a takıldı mı döner */
|
|
115
|
+
function decide(sub, event, now) {
|
|
116
|
+
if (sub.type !== event.type) return { hit: false };
|
|
117
|
+
if (sub.op !== null && sub.value !== null) {
|
|
118
|
+
const v = Number(event.value);
|
|
119
|
+
if (!Number.isFinite(v) || !compareOp(sub.op, v, sub.value)) return { hit: false };
|
|
120
|
+
}
|
|
121
|
+
const cd = (sub.cooldownMin || 0) * 60000;
|
|
122
|
+
if (cd && now - (sub.lastNotifiedAt || 0) < cd) return { hit: true, cooled: true };
|
|
123
|
+
return { hit: true, cooled: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Bir olayı tüm aboneliklere dağıt; tetiklenenleri döndürür */
|
|
127
|
+
function emitEvent(type, data = {}, now = Date.now()) {
|
|
128
|
+
load();
|
|
129
|
+
const event = { type, data, value: Number(data.value), at: now };
|
|
130
|
+
const fired = [];
|
|
131
|
+
for (const s of subs.slice()) {
|
|
132
|
+
const d = decide(s, event, now);
|
|
133
|
+
if (!d.hit || d.cooled) continue;
|
|
134
|
+
s.lastNotifiedAt = now;
|
|
135
|
+
fired.push(s);
|
|
136
|
+
if (typeof hooks.notify === 'function') {
|
|
137
|
+
try {
|
|
138
|
+
hooks.notify({ ...s }, summarize(type, data));
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (fired.length) save();
|
|
143
|
+
return fired.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function summarize(type, data) {
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'mail:new':
|
|
149
|
+
return `[OLAY] Yeni e-posta geldi${data.from ? ': ' + data.from : ''}${data.subject ? ' — ' + data.subject : ''}. Kullanıcıya kısaca haber ver; email_read ile gövdeyi isteyebilir.`;
|
|
150
|
+
case 'fs:changed':
|
|
151
|
+
return `[OLAY] Workspace'te dosya değişimi: ${data.path || '?'}. Kullanıcıya kısaca haber ver.`;
|
|
152
|
+
case 'webhook':
|
|
153
|
+
return `[OLAY] Webhook: ${JSON.stringify(data).slice(0, 300)}. Kullanıcıya anlamlı biçimde raporla.`;
|
|
154
|
+
case 'price:tick':
|
|
155
|
+
return `[OLAY] Fiyat güncellemesi ${data.symbol || '?'}: ${data.price} (koşul sağlandı). Kullanıcıya hemen haber ver.`;
|
|
156
|
+
case 'wa:presence':
|
|
157
|
+
return `[OLAY] WhatsApp durum değişimi: ${data.status} (+${data.sender || '?'}). Kısa not düş.`;
|
|
158
|
+
default:
|
|
159
|
+
return `[OLAY] ${type}: ${JSON.stringify(data).slice(0, 200)}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ---------- kaynaklar ---------- */
|
|
164
|
+
|
|
165
|
+
/* IMAP IDLE: yeni mailleri anında yakalar. Koparsa 60 sn sonra yeniden. */
|
|
166
|
+
async function startMailIdle(getCfg) {
|
|
167
|
+
if (running.mailIdle) return;
|
|
168
|
+
running.mailIdle = true;
|
|
169
|
+
const attempt = async () => {
|
|
170
|
+
while (running.mailIdle) {
|
|
171
|
+
let ImapFlow = null;
|
|
172
|
+
try {
|
|
173
|
+
/* imapflow yeni sürümlerde named export ({ ImapFlow }) — her şekli çöz */
|
|
174
|
+
const _if = require('imapflow');
|
|
175
|
+
ImapFlow = (_if && (_if.ImapFlow || _if.default)) || (typeof _if === 'function' ? _if : null);
|
|
176
|
+
} catch {
|
|
177
|
+
ImapFlow = null;
|
|
178
|
+
}
|
|
179
|
+
if (!ImapFlow) {
|
|
180
|
+
log('imapflow yok — mail:new kapalı');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const cfg = getCfg() || {};
|
|
184
|
+
if (!cfg.host || !cfg.user || !cfg.pass) {
|
|
185
|
+
log('mail credential yok — IDLE beklemeye çekildi');
|
|
186
|
+
await sleep(120000);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
let client = null;
|
|
190
|
+
try {
|
|
191
|
+
client = new ImapFlow({
|
|
192
|
+
host: cfg.host,
|
|
193
|
+
port: Number(cfg.port) || 993,
|
|
194
|
+
secure: true,
|
|
195
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
196
|
+
logger: false,
|
|
197
|
+
});
|
|
198
|
+
await client.connect();
|
|
199
|
+
const lock = await client.getMailboxLock('INBOX');
|
|
200
|
+
log('IMAP IDLE bağlı');
|
|
201
|
+
/* IDLE akışı: EXISTS görünce kısa IDLE'dan çık, olay üret, tekrar gir */
|
|
202
|
+
for (;;) {
|
|
203
|
+
if (!running.mailIdle) break;
|
|
204
|
+
const lockPromise = new Promise((resolve) => client.once && client.once('exists', resolve));
|
|
205
|
+
// imapflow otomatik IDLE yapar; event tabanlı bekleme + periyodik nocop uyanma
|
|
206
|
+
await Promise.race([lockPromise, sleep(5 * 60000)]);
|
|
207
|
+
if (!running.mailIdle) break;
|
|
208
|
+
emitEvent('mail:new', {}, Date.now());
|
|
209
|
+
}
|
|
210
|
+
try { lock.release(); } catch {}
|
|
211
|
+
try { await client.logout(); } catch {}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
log(`IDLE koptu: ${String((e && e.message) || e)} — 60 sn sonra yeniden`);
|
|
214
|
+
try { if (client) await client.logout(); } catch {}
|
|
215
|
+
await sleep(60000);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
attempt().catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stopMailIdle() {
|
|
223
|
+
running.mailIdle = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sleep(ms) {
|
|
227
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* fs.watch: workspace kökünde hafif izleme */
|
|
231
|
+
function startFsWatch(watchPath) {
|
|
232
|
+
if (running.fsWatch) return;
|
|
233
|
+
try {
|
|
234
|
+
running.fsWatch = fs.watch(
|
|
235
|
+
watchPath,
|
|
236
|
+
{ recursive: true },
|
|
237
|
+
debounceEmit((eventType, filename) => {
|
|
238
|
+
emitEvent('fs:changed', { eventType, path: String(filename || '') }, Date.now());
|
|
239
|
+
}, 5000)
|
|
240
|
+
);
|
|
241
|
+
log(`fs.watch açık: ${watchPath}`);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
log(`fs.watch hata: ${String((e && e.message) || e)}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function stopFsWatch() {
|
|
248
|
+
if (running.fsWatch) {
|
|
249
|
+
try { running.fsWatch.close(); } catch {}
|
|
250
|
+
running.fsWatch = null;
|
|
251
|
+
log('fs.watch kapandı');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* aynı anda onlarca olayı tek bildirime indirger */
|
|
256
|
+
function debounceEmit(fn, ms) {
|
|
257
|
+
let t = null;
|
|
258
|
+
let lastArgs = null;
|
|
259
|
+
return (...args) => {
|
|
260
|
+
lastArgs = args;
|
|
261
|
+
clearTimeout(t);
|
|
262
|
+
t = setTimeout(() => {
|
|
263
|
+
fn(...lastArgs);
|
|
264
|
+
lastArgs = null;
|
|
265
|
+
}, ms);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* local webhook: 127.0.0.1 bind, token doğrulaması zorunlu */
|
|
270
|
+
function startWebhook(port, token) {
|
|
271
|
+
if (running.webhookServer) return;
|
|
272
|
+
const server = http.createServer((req, res) => {
|
|
273
|
+
if (req.method !== 'POST' || !req.url.startsWith('/beast-event')) {
|
|
274
|
+
res.writeHead(404).end();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (!token || req.headers['x-beast-token'] !== token) {
|
|
278
|
+
res.writeHead(401).end('bad token');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
let body = '';
|
|
282
|
+
req.on('data', (c) => {
|
|
283
|
+
body += c;
|
|
284
|
+
if (body.length > 100000) req.destroy();
|
|
285
|
+
});
|
|
286
|
+
req.on('end', () => {
|
|
287
|
+
try {
|
|
288
|
+
const j = JSON.parse(body || '{}');
|
|
289
|
+
emitEvent(String(j.type || 'webhook'), j.data ?? j, Date.now());
|
|
290
|
+
res.writeHead(200).end('ok');
|
|
291
|
+
} catch {
|
|
292
|
+
res.writeHead(400).end('bad json');
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
server.listen(port, '127.0.0.1', () => log(`webhook dinliyor: 127.0.0.1:${port}/beast-event`));
|
|
298
|
+
running.webhookServer = server;
|
|
299
|
+
} catch (e) {
|
|
300
|
+
log(`webhook başlatılamadı: ${String((e && e.message) || e)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function stopWebhook() {
|
|
305
|
+
if (running.webhookServer) {
|
|
306
|
+
try { running.webhookServer.close(); } catch {}
|
|
307
|
+
running.webhookServer = null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* fiyat feed'i: Binance miniTicker WS (örn PAXGUSDT ≈ altın peg'i) */
|
|
312
|
+
function startPriceFeed(symbol = 'PAXGUSDT') {
|
|
313
|
+
if (running.priceWs) stopPriceFeed();
|
|
314
|
+
let Ws;
|
|
315
|
+
try {
|
|
316
|
+
Ws = require('ws');
|
|
317
|
+
} catch {
|
|
318
|
+
log('ws modülü yok — fiyat feed kapalı');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const sym = String(symbol).toLowerCase();
|
|
322
|
+
try {
|
|
323
|
+
const ws = new Ws(`wss://stream.binance.com:9443/ws/${sym}@miniTicker`);
|
|
324
|
+
ws.on('open', () => log(`fiyat WS açık: ${symbol}`));
|
|
325
|
+
ws.on('message', (buf) => {
|
|
326
|
+
try {
|
|
327
|
+
const m = JSON.parse(buf.toString());
|
|
328
|
+
const close = Number(m.c);
|
|
329
|
+
if (Number.isFinite(close)) {
|
|
330
|
+
emitEvent('price:tick', { symbol, price: close, value: close }, Date.now());
|
|
331
|
+
}
|
|
332
|
+
} catch {}
|
|
333
|
+
});
|
|
334
|
+
ws.on('error', (e) => log(`fiyat WS hata: ${String((e && e.message) || e)}`));
|
|
335
|
+
ws.on('close', () => {
|
|
336
|
+
if (running.priceWs === ws) {
|
|
337
|
+
running.priceWs = null;
|
|
338
|
+
if (running.priceWanted) setTimeout(() => startPriceFeed(symbol), 15000);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
running.priceWanted = true;
|
|
342
|
+
running.priceSymbol = symbol;
|
|
343
|
+
running.priceWs = ws;
|
|
344
|
+
} catch (e) {
|
|
345
|
+
log(`fiyat WS kurulamadı: ${String((e && e.message) || e)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function stopPriceFeed() {
|
|
350
|
+
running.priceWanted = false;
|
|
351
|
+
if (running.priceWs) {
|
|
352
|
+
try { running.priceWs.close(); } catch {}
|
|
353
|
+
running.priceWs = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ---------- master ---------- */
|
|
358
|
+
|
|
359
|
+
function start(hooksIn, opts = {}) {
|
|
360
|
+
hooks = { ...hooks, ...(hooksIn || {}) };
|
|
361
|
+
load();
|
|
362
|
+
const o = opts || {};
|
|
363
|
+
if (o.mailIdle) startMailIdle(o.getCfg || (() => null));
|
|
364
|
+
else stopMailIdle();
|
|
365
|
+
if (o.fsWatch && o.workspace) startFsWatch(o.workspace);
|
|
366
|
+
else stopFsWatch();
|
|
367
|
+
if (o.webhookPort && o.webhookToken) startWebhook(o.webhookPort, o.webhookToken);
|
|
368
|
+
else stopWebhook();
|
|
369
|
+
if (o.priceSymbol) startPriceFeed(o.priceSymbol);
|
|
370
|
+
else stopPriceFeed();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function stopAll() {
|
|
374
|
+
stopMailIdle();
|
|
375
|
+
stopFsWatch();
|
|
376
|
+
stopWebhook();
|
|
377
|
+
stopPriceFeed();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
start,
|
|
382
|
+
stopAll,
|
|
383
|
+
listSubs,
|
|
384
|
+
addSub,
|
|
385
|
+
removeSub,
|
|
386
|
+
emitEvent,
|
|
387
|
+
decide,
|
|
388
|
+
compareOp,
|
|
389
|
+
};
|