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/watchers.js
CHANGED
|
@@ -1,312 +1,312 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* Beast izleyiciler (watchers): arka planda periyodik kontrol.
|
|
4
|
-
kind=web → URL periyodik çekilir, değer çıkarılır (json path / regex), koşul karşılaştırılır
|
|
5
|
-
kind=battery → yerel pil yüzdesi izlenir
|
|
6
|
-
Koşul sağlanınca ilgili oturuma mesaj düşer (WA köprüsüne de otomatik akar).
|
|
7
|
-
Depo: %APPDATA%\beast\watchers.json */
|
|
8
|
-
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const { execFile } = require('child_process');
|
|
12
|
-
const { beastRoot } = require('./memory');
|
|
13
|
-
|
|
14
|
-
function file() {
|
|
15
|
-
return path.join(beastRoot(), 'watchers.json');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let items = [];
|
|
19
|
-
|
|
20
|
-
function load() {
|
|
21
|
-
try {
|
|
22
|
-
const raw = JSON.parse(fs.readFileSync(file(), 'utf8'));
|
|
23
|
-
items = Array.isArray(raw.watchers) ? raw.watchers : [];
|
|
24
|
-
} catch {
|
|
25
|
-
items = [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function save() {
|
|
30
|
-
try {
|
|
31
|
-
fs.mkdirSync(beastRoot(), { recursive: true });
|
|
32
|
-
fs.writeFileSync(file(), JSON.stringify({ watchers: items }, null, 2));
|
|
33
|
-
} catch {}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function uid() {
|
|
37
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const OPS = ['lt', 'lte', 'gt', 'gte', 'eq', 'neq', 'changed'];
|
|
41
|
-
|
|
42
|
-
/* Girdiyi doğrula/normalleştir — saf fonksiyon */
|
|
43
|
-
function normalize(input) {
|
|
44
|
-
const i = input || {};
|
|
45
|
-
const name = String(i.name || '').trim().slice(0, 80);
|
|
46
|
-
const kind = String(i.kind || '').trim().toLowerCase();
|
|
47
|
-
if (!name) return { error: 'isim gerekli' };
|
|
48
|
-
if (kind !== 'web' && kind !== 'battery') return { error: "kind 'web' ya da 'battery' olmalı" };
|
|
49
|
-
let url = '';
|
|
50
|
-
if (kind === 'web') {
|
|
51
|
-
url = String(i.url || '').trim();
|
|
52
|
-
if (!/^https?:\/\//i.test(url)) return { error: 'web izleyicisi için geçerli http(s) url gerekli' };
|
|
53
|
-
if (url.length > 2000) return { error: 'url çok uzun' };
|
|
54
|
-
}
|
|
55
|
-
const op = OPS.includes(String(i.op || '').toLowerCase()) ? String(i.op).toLowerCase() : 'lte';
|
|
56
|
-
const value = i.value === undefined || i.value === null || i.value === '' ? null : i.value;
|
|
57
|
-
if (op !== 'changed' && value === null) return { error: 'bu op için value (eşik) gerekli' };
|
|
58
|
-
let re = '';
|
|
59
|
-
if (i.re !== undefined && i.re !== null && String(i.re).trim() !== '') {
|
|
60
|
-
try {
|
|
61
|
-
new RegExp(String(i.re));
|
|
62
|
-
re = String(i.re);
|
|
63
|
-
} catch {
|
|
64
|
-
return { error: 'geçersiz regex (re)' };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/* #22 aralık: saniye (10-3600) YA DA dakika (1-1440). everySec verilirse o esas. */
|
|
68
|
-
let everySec = 0;
|
|
69
|
-
const rawSec = Number(i.everySec ?? i.every_sec);
|
|
70
|
-
const rawMin = Number(i.everyMin ?? i.every_min ?? 15);
|
|
71
|
-
if (Number.isFinite(rawSec) && rawSec > 0) {
|
|
72
|
-
everySec = Math.min(Math.max(Math.round(rawSec), 10), 86400);
|
|
73
|
-
} else {
|
|
74
|
-
const everyMin = Math.min(Math.max(Math.round(rawMin) || 15, 1), 1440);
|
|
75
|
-
everySec = everyMin * 60;
|
|
76
|
-
}
|
|
77
|
-
/* 0 geçerlidir (cooldown yok); sadece sayılamayan değerler varsayılana döner */
|
|
78
|
-
const cdRaw = Number(i.cooldownMin ?? i.cooldown_min ?? 60);
|
|
79
|
-
const cooldownMin = Number.isFinite(cdRaw) ? Math.min(Math.max(Math.round(cdRaw), 0), 10080) : 60;
|
|
80
|
-
const prompt = String(i.prompt || '').trim().slice(0, 2000);
|
|
81
|
-
const jpath = String(i.path || '').trim().slice(0, 300);
|
|
82
|
-
return {
|
|
83
|
-
watcher: {
|
|
84
|
-
id: uid(),
|
|
85
|
-
name,
|
|
86
|
-
kind,
|
|
87
|
-
url,
|
|
88
|
-
path: jpath,
|
|
89
|
-
re,
|
|
90
|
-
op,
|
|
91
|
-
value,
|
|
92
|
-
everyMin: Math.max(1, Math.round(everySec / 60)), // geriye dönük uyum
|
|
93
|
-
everySec,
|
|
94
|
-
cooldownMin,
|
|
95
|
-
prompt,
|
|
96
|
-
sessionId: i.sessionId || null,
|
|
97
|
-
enabled: true,
|
|
98
|
-
createdAt: new Date().toISOString(),
|
|
99
|
-
lastCheckAt: null,
|
|
100
|
-
lastValue: null,
|
|
101
|
-
lastTriggeredAt: null,
|
|
102
|
-
lastError: '',
|
|
103
|
-
armed: true,
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/* Koşul karşılaştırması — sayısal op'lar Number'a zorlar, dönemezse false */
|
|
109
|
-
function compare(op, current, target) {
|
|
110
|
-
if (op === 'changed') return String(current ?? '') !== String(target ?? '');
|
|
111
|
-
const c = Number(current);
|
|
112
|
-
const t = Number(target);
|
|
113
|
-
if (!Number.isFinite(c) || !Number.isFinite(t)) return false;
|
|
114
|
-
switch (op) {
|
|
115
|
-
case 'lt': return c < t;
|
|
116
|
-
case 'lte': return c <= t;
|
|
117
|
-
case 'gt': return c > t;
|
|
118
|
-
case 'gte': return c >= t;
|
|
119
|
-
case 'eq': return c === t;
|
|
120
|
-
case 'neq': return c !== t;
|
|
121
|
-
default: return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* Tek kontrol uygula — saf: tetik kararı + kaydedilecek yama.
|
|
126
|
-
Kenar-tetiklemeli: koşul doğruyken tek kez ateşlenir, koşul normale
|
|
127
|
-
dönünce yeniden kurulur (armed). */
|
|
128
|
-
function applyCheck(w, value, now) {
|
|
129
|
-
const cond = compare(w.op, value, w.value);
|
|
130
|
-
const triggered = cond && w.armed;
|
|
131
|
-
const patch = {
|
|
132
|
-
lastCheckAt: new Date(now).toISOString(),
|
|
133
|
-
lastValue: value === undefined ? null : value,
|
|
134
|
-
armed: !cond,
|
|
135
|
-
};
|
|
136
|
-
if (triggered) patch.lastTriggeredAt = new Date(now).toISOString();
|
|
137
|
-
return { triggered, patch };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* Kontrol zamanı geldi mi — everySec varsa saniye bazlı, yoksa dakika */
|
|
141
|
-
function isDue(w, now) {
|
|
142
|
-
if (!w.enabled) return false;
|
|
143
|
-
if (!w.lastCheckAt) return true;
|
|
144
|
-
const ms = w.everySec ? w.everySec * 1000 : (w.everyMin || 15) * 60000;
|
|
145
|
-
return now - new Date(w.lastCheckAt).getTime() >= ms;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/* Son bildirimden beri cooldown sürüyor mu */
|
|
149
|
-
function cooldownActive(w, now) {
|
|
150
|
-
if (!w.lastTriggeredAt || !w.cooldownMin) return false;
|
|
151
|
-
return now - new Date(w.lastTriggeredAt).getTime() < w.cooldownMin * 60000;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/* ---------- CRUD ---------- */
|
|
155
|
-
|
|
156
|
-
function list() {
|
|
157
|
-
return items.map((w) => ({ ...w }));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function get(id) {
|
|
161
|
-
const w = items.find((x) => x.id === id);
|
|
162
|
-
return w ? { ...w } : null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function add(input) {
|
|
166
|
-
const n = normalize(input);
|
|
167
|
-
if (n.error) return { ok: false, error: n.error };
|
|
168
|
-
items.push(n.watcher);
|
|
169
|
-
save();
|
|
170
|
-
return { ok: true, watcher: { ...n.watcher } };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function remove(id) {
|
|
174
|
-
const before = items.length;
|
|
175
|
-
items = items.filter((w) => w.id !== id);
|
|
176
|
-
save();
|
|
177
|
-
return { ok: items.length < before };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function patch(id, p) {
|
|
181
|
-
const w = items.find((x) => x.id === id);
|
|
182
|
-
if (!w) return false;
|
|
183
|
-
Object.assign(w, p || {});
|
|
184
|
-
save();
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ---------- arka plan izleyici ---------- */
|
|
189
|
-
|
|
190
|
-
let timer = null;
|
|
191
|
-
let hooks = {};
|
|
192
|
-
|
|
193
|
-
/* JSON dot-path ("price.usd", "data.0.close") ya da regex ile değer çıkarımı — saf */
|
|
194
|
-
function extractValue(text, w) {
|
|
195
|
-
const body = String(text ?? '');
|
|
196
|
-
if (w.path) {
|
|
197
|
-
let j;
|
|
198
|
-
try {
|
|
199
|
-
j = JSON.parse(body);
|
|
200
|
-
} catch {
|
|
201
|
-
throw new Error('yanıt JSON olmadığı için path uygulanamadı (re kullan)');
|
|
202
|
-
}
|
|
203
|
-
let cur = j;
|
|
204
|
-
for (const part of String(w.path).split('.')) {
|
|
205
|
-
if (cur !== null && cur !== undefined && typeof cur === 'object' && part in cur) cur = cur[part];
|
|
206
|
-
else return null;
|
|
207
|
-
}
|
|
208
|
-
return cur;
|
|
209
|
-
}
|
|
210
|
-
if (w.re) {
|
|
211
|
-
const m = body.match(new RegExp(w.re));
|
|
212
|
-
if (!m) return null;
|
|
213
|
-
return m.length > 1 && m[1] !== undefined ? m[1] : m[0];
|
|
214
|
-
}
|
|
215
|
-
throw new Error('web izleyicisi için path (JSON) veya re (regex) gerekli');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function webValue(w, fetchImpl) {
|
|
219
|
-
const f = fetchImpl || global.fetch;
|
|
220
|
-
const res = await f(w.url, {
|
|
221
|
-
signal: AbortSignal.timeout(20000),
|
|
222
|
-
headers: { 'user-agent': 'BeastAgent/0.1 (+watcher)', accept: '*/*' },
|
|
223
|
-
});
|
|
224
|
-
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
225
|
-
return extractValue((await res.text()).slice(0, 2000000), w);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/* Windows yerel pil yüzdesi; pil yoksa hata fırlatır */
|
|
229
|
-
function batteryLevel() {
|
|
230
|
-
return new Promise((resolve, reject) => {
|
|
231
|
-
execFile(
|
|
232
|
-
'powershell.exe',
|
|
233
|
-
['-NoProfile', '-Command', '(Get-CimInstance Win32_Battery).EstimatedChargeRemaining'],
|
|
234
|
-
{ timeout: 15000, windowsHide: true },
|
|
235
|
-
(err, stdout) => {
|
|
236
|
-
if (err) return reject(err);
|
|
237
|
-
const n = Number((String(stdout).match(/\d+/) || [])[0]);
|
|
238
|
-
if (!Number.isFinite(n)) return reject(new Error('pil bilgisi bulunamadı'));
|
|
239
|
-
resolve(n);
|
|
240
|
-
}
|
|
241
|
-
);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function defaultCheck(w, deps = {}) {
|
|
246
|
-
/* tek noktadan taklit (testler / özel köprüler) */
|
|
247
|
-
if (typeof deps.check === 'function') return deps.check({ ...w });
|
|
248
|
-
if (w.kind === 'battery') return deps.battery ? deps.battery() : batteryLevel();
|
|
249
|
-
return deps.web ? deps.web({ ...w }) : webValue(w, deps.fetch);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/* Tek tur kontrol: vakti gelen + cooldown'u bitmiş izleyicileri denetler.
|
|
253
|
-
Kenar-tetiklemeli: tetiklenenler onTrigger(watcherCopy, value) ile bildirilir.
|
|
254
|
-
Testlerde check/web/battery/fetch enjekte edilebilir. */
|
|
255
|
-
async function tickOnce(deps = {}) {
|
|
256
|
-
const now = Date.now();
|
|
257
|
-
const events = [];
|
|
258
|
-
for (const w of items.slice()) {
|
|
259
|
-
if (!isDue(w, now) || cooldownActive(w, now)) continue;
|
|
260
|
-
try {
|
|
261
|
-
const value = await defaultCheck(w, deps);
|
|
262
|
-
const r = applyCheck(w, value, now);
|
|
263
|
-
Object.assign(w, r.patch);
|
|
264
|
-
w.lastError = '';
|
|
265
|
-
if (r.triggered) {
|
|
266
|
-
if (typeof hooks.onTrigger === 'function') hooks.onTrigger({ ...w }, value);
|
|
267
|
-
events.push({ id: w.id, name: w.name, value });
|
|
268
|
-
}
|
|
269
|
-
} catch (e) {
|
|
270
|
-
w.lastCheckAt = new Date(now).toISOString();
|
|
271
|
-
w.lastError = String((e && e.message) || e).slice(0, 300);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
save();
|
|
275
|
-
return events;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function start(h) {
|
|
279
|
-
hooks = h || hooks;
|
|
280
|
-
load();
|
|
281
|
-
if (timer) return;
|
|
282
|
-
timer = setInterval(() => {
|
|
283
|
-
tickOnce().catch(() => {});
|
|
284
|
-
}, 5 * 1000); // #22 saniyelik aralıklar için ince tick
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function stop() {
|
|
288
|
-
if (timer) clearInterval(timer);
|
|
289
|
-
timer = null;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function init() {
|
|
293
|
-
load();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
module.exports = {
|
|
297
|
-
init,
|
|
298
|
-
start,
|
|
299
|
-
stop,
|
|
300
|
-
tickOnce,
|
|
301
|
-
extractValue,
|
|
302
|
-
list,
|
|
303
|
-
get,
|
|
304
|
-
add,
|
|
305
|
-
remove,
|
|
306
|
-
patch,
|
|
307
|
-
normalize,
|
|
308
|
-
compare,
|
|
309
|
-
applyCheck,
|
|
310
|
-
isDue,
|
|
311
|
-
cooldownActive,
|
|
312
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast izleyiciler (watchers): arka planda periyodik kontrol.
|
|
4
|
+
kind=web → URL periyodik çekilir, değer çıkarılır (json path / regex), koşul karşılaştırılır
|
|
5
|
+
kind=battery → yerel pil yüzdesi izlenir
|
|
6
|
+
Koşul sağlanınca ilgili oturuma mesaj düşer (WA köprüsüne de otomatik akar).
|
|
7
|
+
Depo: %APPDATA%\beast\watchers.json */
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execFile } = require('child_process');
|
|
12
|
+
const { beastRoot } = require('./memory');
|
|
13
|
+
|
|
14
|
+
function file() {
|
|
15
|
+
return path.join(beastRoot(), 'watchers.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let items = [];
|
|
19
|
+
|
|
20
|
+
function load() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(fs.readFileSync(file(), 'utf8'));
|
|
23
|
+
items = Array.isArray(raw.watchers) ? raw.watchers : [];
|
|
24
|
+
} catch {
|
|
25
|
+
items = [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function save() {
|
|
30
|
+
try {
|
|
31
|
+
fs.mkdirSync(beastRoot(), { recursive: true });
|
|
32
|
+
fs.writeFileSync(file(), JSON.stringify({ watchers: items }, null, 2));
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function uid() {
|
|
37
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const OPS = ['lt', 'lte', 'gt', 'gte', 'eq', 'neq', 'changed'];
|
|
41
|
+
|
|
42
|
+
/* Girdiyi doğrula/normalleştir — saf fonksiyon */
|
|
43
|
+
function normalize(input) {
|
|
44
|
+
const i = input || {};
|
|
45
|
+
const name = String(i.name || '').trim().slice(0, 80);
|
|
46
|
+
const kind = String(i.kind || '').trim().toLowerCase();
|
|
47
|
+
if (!name) return { error: 'isim gerekli' };
|
|
48
|
+
if (kind !== 'web' && kind !== 'battery') return { error: "kind 'web' ya da 'battery' olmalı" };
|
|
49
|
+
let url = '';
|
|
50
|
+
if (kind === 'web') {
|
|
51
|
+
url = String(i.url || '').trim();
|
|
52
|
+
if (!/^https?:\/\//i.test(url)) return { error: 'web izleyicisi için geçerli http(s) url gerekli' };
|
|
53
|
+
if (url.length > 2000) return { error: 'url çok uzun' };
|
|
54
|
+
}
|
|
55
|
+
const op = OPS.includes(String(i.op || '').toLowerCase()) ? String(i.op).toLowerCase() : 'lte';
|
|
56
|
+
const value = i.value === undefined || i.value === null || i.value === '' ? null : i.value;
|
|
57
|
+
if (op !== 'changed' && value === null) return { error: 'bu op için value (eşik) gerekli' };
|
|
58
|
+
let re = '';
|
|
59
|
+
if (i.re !== undefined && i.re !== null && String(i.re).trim() !== '') {
|
|
60
|
+
try {
|
|
61
|
+
new RegExp(String(i.re));
|
|
62
|
+
re = String(i.re);
|
|
63
|
+
} catch {
|
|
64
|
+
return { error: 'geçersiz regex (re)' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/* #22 aralık: saniye (10-3600) YA DA dakika (1-1440). everySec verilirse o esas. */
|
|
68
|
+
let everySec = 0;
|
|
69
|
+
const rawSec = Number(i.everySec ?? i.every_sec);
|
|
70
|
+
const rawMin = Number(i.everyMin ?? i.every_min ?? 15);
|
|
71
|
+
if (Number.isFinite(rawSec) && rawSec > 0) {
|
|
72
|
+
everySec = Math.min(Math.max(Math.round(rawSec), 10), 86400);
|
|
73
|
+
} else {
|
|
74
|
+
const everyMin = Math.min(Math.max(Math.round(rawMin) || 15, 1), 1440);
|
|
75
|
+
everySec = everyMin * 60;
|
|
76
|
+
}
|
|
77
|
+
/* 0 geçerlidir (cooldown yok); sadece sayılamayan değerler varsayılana döner */
|
|
78
|
+
const cdRaw = Number(i.cooldownMin ?? i.cooldown_min ?? 60);
|
|
79
|
+
const cooldownMin = Number.isFinite(cdRaw) ? Math.min(Math.max(Math.round(cdRaw), 0), 10080) : 60;
|
|
80
|
+
const prompt = String(i.prompt || '').trim().slice(0, 2000);
|
|
81
|
+
const jpath = String(i.path || '').trim().slice(0, 300);
|
|
82
|
+
return {
|
|
83
|
+
watcher: {
|
|
84
|
+
id: uid(),
|
|
85
|
+
name,
|
|
86
|
+
kind,
|
|
87
|
+
url,
|
|
88
|
+
path: jpath,
|
|
89
|
+
re,
|
|
90
|
+
op,
|
|
91
|
+
value,
|
|
92
|
+
everyMin: Math.max(1, Math.round(everySec / 60)), // geriye dönük uyum
|
|
93
|
+
everySec,
|
|
94
|
+
cooldownMin,
|
|
95
|
+
prompt,
|
|
96
|
+
sessionId: i.sessionId || null,
|
|
97
|
+
enabled: true,
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
lastCheckAt: null,
|
|
100
|
+
lastValue: null,
|
|
101
|
+
lastTriggeredAt: null,
|
|
102
|
+
lastError: '',
|
|
103
|
+
armed: true,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Koşul karşılaştırması — sayısal op'lar Number'a zorlar, dönemezse false */
|
|
109
|
+
function compare(op, current, target) {
|
|
110
|
+
if (op === 'changed') return String(current ?? '') !== String(target ?? '');
|
|
111
|
+
const c = Number(current);
|
|
112
|
+
const t = Number(target);
|
|
113
|
+
if (!Number.isFinite(c) || !Number.isFinite(t)) return false;
|
|
114
|
+
switch (op) {
|
|
115
|
+
case 'lt': return c < t;
|
|
116
|
+
case 'lte': return c <= t;
|
|
117
|
+
case 'gt': return c > t;
|
|
118
|
+
case 'gte': return c >= t;
|
|
119
|
+
case 'eq': return c === t;
|
|
120
|
+
case 'neq': return c !== t;
|
|
121
|
+
default: return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Tek kontrol uygula — saf: tetik kararı + kaydedilecek yama.
|
|
126
|
+
Kenar-tetiklemeli: koşul doğruyken tek kez ateşlenir, koşul normale
|
|
127
|
+
dönünce yeniden kurulur (armed). */
|
|
128
|
+
function applyCheck(w, value, now) {
|
|
129
|
+
const cond = compare(w.op, value, w.value);
|
|
130
|
+
const triggered = cond && w.armed;
|
|
131
|
+
const patch = {
|
|
132
|
+
lastCheckAt: new Date(now).toISOString(),
|
|
133
|
+
lastValue: value === undefined ? null : value,
|
|
134
|
+
armed: !cond,
|
|
135
|
+
};
|
|
136
|
+
if (triggered) patch.lastTriggeredAt = new Date(now).toISOString();
|
|
137
|
+
return { triggered, patch };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Kontrol zamanı geldi mi — everySec varsa saniye bazlı, yoksa dakika */
|
|
141
|
+
function isDue(w, now) {
|
|
142
|
+
if (!w.enabled) return false;
|
|
143
|
+
if (!w.lastCheckAt) return true;
|
|
144
|
+
const ms = w.everySec ? w.everySec * 1000 : (w.everyMin || 15) * 60000;
|
|
145
|
+
return now - new Date(w.lastCheckAt).getTime() >= ms;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Son bildirimden beri cooldown sürüyor mu */
|
|
149
|
+
function cooldownActive(w, now) {
|
|
150
|
+
if (!w.lastTriggeredAt || !w.cooldownMin) return false;
|
|
151
|
+
return now - new Date(w.lastTriggeredAt).getTime() < w.cooldownMin * 60000;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ---------- CRUD ---------- */
|
|
155
|
+
|
|
156
|
+
function list() {
|
|
157
|
+
return items.map((w) => ({ ...w }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function get(id) {
|
|
161
|
+
const w = items.find((x) => x.id === id);
|
|
162
|
+
return w ? { ...w } : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function add(input) {
|
|
166
|
+
const n = normalize(input);
|
|
167
|
+
if (n.error) return { ok: false, error: n.error };
|
|
168
|
+
items.push(n.watcher);
|
|
169
|
+
save();
|
|
170
|
+
return { ok: true, watcher: { ...n.watcher } };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function remove(id) {
|
|
174
|
+
const before = items.length;
|
|
175
|
+
items = items.filter((w) => w.id !== id);
|
|
176
|
+
save();
|
|
177
|
+
return { ok: items.length < before };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function patch(id, p) {
|
|
181
|
+
const w = items.find((x) => x.id === id);
|
|
182
|
+
if (!w) return false;
|
|
183
|
+
Object.assign(w, p || {});
|
|
184
|
+
save();
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ---------- arka plan izleyici ---------- */
|
|
189
|
+
|
|
190
|
+
let timer = null;
|
|
191
|
+
let hooks = {};
|
|
192
|
+
|
|
193
|
+
/* JSON dot-path ("price.usd", "data.0.close") ya da regex ile değer çıkarımı — saf */
|
|
194
|
+
function extractValue(text, w) {
|
|
195
|
+
const body = String(text ?? '');
|
|
196
|
+
if (w.path) {
|
|
197
|
+
let j;
|
|
198
|
+
try {
|
|
199
|
+
j = JSON.parse(body);
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error('yanıt JSON olmadığı için path uygulanamadı (re kullan)');
|
|
202
|
+
}
|
|
203
|
+
let cur = j;
|
|
204
|
+
for (const part of String(w.path).split('.')) {
|
|
205
|
+
if (cur !== null && cur !== undefined && typeof cur === 'object' && part in cur) cur = cur[part];
|
|
206
|
+
else return null;
|
|
207
|
+
}
|
|
208
|
+
return cur;
|
|
209
|
+
}
|
|
210
|
+
if (w.re) {
|
|
211
|
+
const m = body.match(new RegExp(w.re));
|
|
212
|
+
if (!m) return null;
|
|
213
|
+
return m.length > 1 && m[1] !== undefined ? m[1] : m[0];
|
|
214
|
+
}
|
|
215
|
+
throw new Error('web izleyicisi için path (JSON) veya re (regex) gerekli');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function webValue(w, fetchImpl) {
|
|
219
|
+
const f = fetchImpl || global.fetch;
|
|
220
|
+
const res = await f(w.url, {
|
|
221
|
+
signal: AbortSignal.timeout(20000),
|
|
222
|
+
headers: { 'user-agent': 'BeastAgent/0.1 (+watcher)', accept: '*/*' },
|
|
223
|
+
});
|
|
224
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
225
|
+
return extractValue((await res.text()).slice(0, 2000000), w);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Windows yerel pil yüzdesi; pil yoksa hata fırlatır */
|
|
229
|
+
function batteryLevel() {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
execFile(
|
|
232
|
+
'powershell.exe',
|
|
233
|
+
['-NoProfile', '-Command', '(Get-CimInstance Win32_Battery).EstimatedChargeRemaining'],
|
|
234
|
+
{ timeout: 15000, windowsHide: true },
|
|
235
|
+
(err, stdout) => {
|
|
236
|
+
if (err) return reject(err);
|
|
237
|
+
const n = Number((String(stdout).match(/\d+/) || [])[0]);
|
|
238
|
+
if (!Number.isFinite(n)) return reject(new Error('pil bilgisi bulunamadı'));
|
|
239
|
+
resolve(n);
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function defaultCheck(w, deps = {}) {
|
|
246
|
+
/* tek noktadan taklit (testler / özel köprüler) */
|
|
247
|
+
if (typeof deps.check === 'function') return deps.check({ ...w });
|
|
248
|
+
if (w.kind === 'battery') return deps.battery ? deps.battery() : batteryLevel();
|
|
249
|
+
return deps.web ? deps.web({ ...w }) : webValue(w, deps.fetch);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Tek tur kontrol: vakti gelen + cooldown'u bitmiş izleyicileri denetler.
|
|
253
|
+
Kenar-tetiklemeli: tetiklenenler onTrigger(watcherCopy, value) ile bildirilir.
|
|
254
|
+
Testlerde check/web/battery/fetch enjekte edilebilir. */
|
|
255
|
+
async function tickOnce(deps = {}) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
const events = [];
|
|
258
|
+
for (const w of items.slice()) {
|
|
259
|
+
if (!isDue(w, now) || cooldownActive(w, now)) continue;
|
|
260
|
+
try {
|
|
261
|
+
const value = await defaultCheck(w, deps);
|
|
262
|
+
const r = applyCheck(w, value, now);
|
|
263
|
+
Object.assign(w, r.patch);
|
|
264
|
+
w.lastError = '';
|
|
265
|
+
if (r.triggered) {
|
|
266
|
+
if (typeof hooks.onTrigger === 'function') hooks.onTrigger({ ...w }, value);
|
|
267
|
+
events.push({ id: w.id, name: w.name, value });
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
w.lastCheckAt = new Date(now).toISOString();
|
|
271
|
+
w.lastError = String((e && e.message) || e).slice(0, 300);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
save();
|
|
275
|
+
return events;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function start(h) {
|
|
279
|
+
hooks = h || hooks;
|
|
280
|
+
load();
|
|
281
|
+
if (timer) return;
|
|
282
|
+
timer = setInterval(() => {
|
|
283
|
+
tickOnce().catch(() => {});
|
|
284
|
+
}, 5 * 1000); // #22 saniyelik aralıklar için ince tick
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function stop() {
|
|
288
|
+
if (timer) clearInterval(timer);
|
|
289
|
+
timer = null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function init() {
|
|
293
|
+
load();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
init,
|
|
298
|
+
start,
|
|
299
|
+
stop,
|
|
300
|
+
tickOnce,
|
|
301
|
+
extractValue,
|
|
302
|
+
list,
|
|
303
|
+
get,
|
|
304
|
+
add,
|
|
305
|
+
remove,
|
|
306
|
+
patch,
|
|
307
|
+
normalize,
|
|
308
|
+
compare,
|
|
309
|
+
applyCheck,
|
|
310
|
+
isDue,
|
|
311
|
+
cooldownActive,
|
|
312
|
+
};
|