beast-agent 2.17.0 → 2.18.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/engine.js +64 -18
- package/src/agent/watchers.js +93 -1
- package/src/cron.js +46 -7
- package/src/main.js +132 -8
- package/src/preload.js +4 -0
- package/src/renderer/i18n.js +16 -0
- package/src/renderer/index.html +18 -1
- package/src/renderer/renderer.js +110 -2
- package/src/renderer/style.css +48 -3
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -1830,6 +1830,11 @@ class Engine {
|
|
|
1830
1830
|
: 'ROL: GÖZLEMCİ ANALİST 🔍 — tur tur piyasa tararsın ama OTOMATİK İŞLEM KAPALI: mt5_trade/mt5_pending KULLANMA. Fırsatları net öneri olarak yaz (sembol, yön, giriş, SL, TP, sebep); sahibi onaylarsa açılır.'
|
|
1831
1831
|
: 'ROL: TRADING CO-PİLOT 💬 — sahibinle sohbet ediyorsun. Veri isterse mt5_* araçlarıyla çek, analiz et; işlem istenirse limitler içinde uygula, istenmezse sadece öneri ver.';
|
|
1832
1832
|
const proj = this._projectInstructions(session);
|
|
1833
|
+
/* Skills kataloğu: finance oturumlarında da skill'ler çağrılabilir
|
|
1834
|
+
(skill aracı _chatTurn'de finance oturumlarına enjekte edilir). */
|
|
1835
|
+
const skList = (() => {
|
|
1836
|
+
try { return skills.scan(); } catch { return []; }
|
|
1837
|
+
})();
|
|
1833
1838
|
return (
|
|
1834
1839
|
'Sen BEAST FİNANS\u2019sın — bilgisayardaki MetaTrader 5 (MT5) terminaline köprüyle bağlı trading ajanı.\n' +
|
|
1835
1840
|
'ORTAM: Beast Finance uygulamasındasın — solda sohbet geçmişi + izleyiciler, ortada bu sohbet, SAĞ panelde MT5 işlem panosu (hesap, pozisyonlar, piyasa, AKIŞ) var.\n' +
|
|
@@ -1846,6 +1851,10 @@ class Engine {
|
|
|
1846
1851
|
'- Emin olmadığında İŞLEM YOK — "BEKLE: <sebep>" yaz. Sık işlem > iyi işlem.\n' +
|
|
1847
1852
|
'- Hesap kaldıracı ve serbest marja göre pozisyon boyutunu düşük tut; tek işlemde serbest marjın büyük kısmını riske atma.\n' +
|
|
1848
1853
|
(session && session.financeStrategy ? `SAHİBİNİN STRATEJİ NOTU (önceliklidir):\n${session.financeStrategy}\n` : '') +
|
|
1854
|
+
(skList.length
|
|
1855
|
+
? '# SKILLS\nKurulu skill kataloğu: görev bir skill\u2019in tanımına uyarsa skill aracıyla gövdesini (SKILL.md) yükleyip uygula — tekerleği yeniden icat etme. Analysis/rapor/PDF gibi işlerde skill\u2019in prosedürüne uy.\n' +
|
|
1856
|
+
skList.map((s) => `- ${s.name}: ${s.description} [${s.path}]`).join('\n') + '\n'
|
|
1857
|
+
: '') +
|
|
1849
1858
|
'RAPOR DİSİPLİNİ: kısa ve sayısal — sembol, yön, lot, giriş, SL/TP, sebep tek satırda. Uzun fal açma; tablo/liste kullanabilirsin.\n' +
|
|
1850
1859
|
`Yerel zaman: ${localDate} ${localTime} — piyasa saatlerine dikkat et (borsa/metal seansları kapalıysa spread genişler; hafta sonu FX kapalıdır).\n` +
|
|
1851
1860
|
(proj ? '# PROJE TALİMATLARI\n' + proj + '\n' : '') +
|
|
@@ -3402,6 +3411,16 @@ class Engine {
|
|
|
3402
3411
|
toolsList = await mcp.mergeTools(toolsList);
|
|
3403
3412
|
/* Beast Apps: kurulu app'lerin araçları (app__<id>__<tool>) modele açılır */
|
|
3404
3413
|
toolsList = apps.mergeTools(toolsList);
|
|
3414
|
+
/* Beast Finance: skill aracı da açık — kataloğun SKILL.md gövdesi
|
|
3415
|
+
modele açılır (skill handler'ı zaten tüm oturumlar için çalışır,
|
|
3416
|
+
definition yalnız BC'de vardı; finance chat copilot + trader ikisi de)
|
|
3417
|
+
Not: BC oturumları zaten opencode registry'sinden skill'i görür. */
|
|
3418
|
+
if (session && session.finance) {
|
|
3419
|
+
const skillDef = (opencode.toolmap.definitions() || []).find(
|
|
3420
|
+
(d) => d && d.function && d.function.name === 'skill'
|
|
3421
|
+
);
|
|
3422
|
+
if (skillDef) toolsList = [...toolsList, skillDef];
|
|
3423
|
+
}
|
|
3405
3424
|
/* panel_run yalnız SANDBOX oturumlarında görünsün — diğer panellerde
|
|
3406
3425
|
modelin alet çantasında olmasın (hook olmadan çalışmaz) */
|
|
3407
3426
|
if (!(session && session.sbSandbox)) {
|
|
@@ -4653,6 +4672,26 @@ const skills = require('./skills');
|
|
|
4653
4672
|
return null;
|
|
4654
4673
|
}
|
|
4655
4674
|
|
|
4675
|
+
/* opencode skill aracı gövdesi: katalogdan eşleşen SKILL.md'yi okur.
|
|
4676
|
+
Salt-okur — tüm oturum türlerinde (BC/finance/chat/trader) aynı yol. */
|
|
4677
|
+
_skillBody(args) {
|
|
4678
|
+
const wanted = String((args && args.name) || '').trim();
|
|
4679
|
+
let list = [];
|
|
4680
|
+
try { list = skills.scan(); } catch {}
|
|
4681
|
+
const hit = list.find((s) => s && s.name === wanted) || null;
|
|
4682
|
+
if (!hit) {
|
|
4683
|
+
return {
|
|
4684
|
+
ok: false,
|
|
4685
|
+
error:
|
|
4686
|
+
'skill bulunamadı: ' + (wanted || '(boş)') +
|
|
4687
|
+
(list.length ? ' — mevcut: ' + list.map((s) => s.name).join(', ') : ''),
|
|
4688
|
+
};
|
|
4689
|
+
}
|
|
4690
|
+
let body = '';
|
|
4691
|
+
try { body = fs.readFileSync(hit.path, 'utf8'); } catch {}
|
|
4692
|
+
return { ok: true, name: hit.name, path: hit.path, content: body.slice(0, 24000) };
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4656
4695
|
async _execTool(name, args, signal, sessionId) {
|
|
4657
4696
|
try {
|
|
4658
4697
|
/* opencode mantığı (BC): model opencode builtin dilinde çağırır —
|
|
@@ -4669,6 +4708,12 @@ const skills = require('./skills');
|
|
|
4669
4708
|
/* ANA KOD KİLİDİ: korumalı bölgeye yazma/silme girişimi daha kapıdan geçmez */
|
|
4670
4709
|
const blocked = this._guardTool(name, args);
|
|
4671
4710
|
if (blocked) return blocked;
|
|
4711
|
+
/* opencode skill aracı: katalogdaki SKILL.md gövdesini modele açar.
|
|
4712
|
+
Salt-okur ve güvenli — BC dahil TÜM oturum türlerinde çalışır
|
|
4713
|
+
(Beast Finance chat copilot + trader buradan kullanır). */
|
|
4714
|
+
if (name === 'skill') {
|
|
4715
|
+
return JSON.stringify(this._skillBody(args));
|
|
4716
|
+
}
|
|
4672
4717
|
if (isBc) {
|
|
4673
4718
|
/* opencode permission akışı (tool/external-directory.ts + her aracın
|
|
4674
4719
|
ctx.ask'ı): önce workspace DIŞI erişim izni, sonra araç izni.
|
|
@@ -4695,24 +4740,6 @@ const skills = require('./skills');
|
|
|
4695
4740
|
} catch (permErr) {
|
|
4696
4741
|
return JSON.stringify({ ok: false, error: AskService.errorText(permErr) });
|
|
4697
4742
|
}
|
|
4698
|
-
/* opencode skill aracı: katalogdaki SKILL.md gövdesini modele açar */
|
|
4699
|
-
if (name === 'skill') {
|
|
4700
|
-
const wanted = String((args && args.name) || '').trim();
|
|
4701
|
-
let list = [];
|
|
4702
|
-
try { list = skills.scan(); } catch {}
|
|
4703
|
-
const hit = list.find((s) => s && s.name === wanted) || null;
|
|
4704
|
-
if (!hit) {
|
|
4705
|
-
return JSON.stringify({
|
|
4706
|
-
ok: false,
|
|
4707
|
-
error:
|
|
4708
|
-
'skill bulunamadı: ' + (wanted || '(boş)') +
|
|
4709
|
-
(list.length ? ' — mevcut: ' + list.map((s) => s.name).join(', ') : ''),
|
|
4710
|
-
});
|
|
4711
|
-
}
|
|
4712
|
-
let body = '';
|
|
4713
|
-
try { body = fs.readFileSync(hit.path, 'utf8'); } catch {}
|
|
4714
|
-
return JSON.stringify({ ok: true, name: hit.name, path: hit.path, content: body.slice(0, 24000) });
|
|
4715
|
-
}
|
|
4716
4743
|
} else {
|
|
4717
4744
|
/* onay kapısı: riskli araçta dış onay bekle; reddedilirse araç çalışmaz.
|
|
4718
4745
|
"always" onaylı araçlar doğrudan geçer. (BC dışı oturumlar — opencode
|
|
@@ -5969,6 +5996,25 @@ Engine.prototype.undoLastTodo = function (sid) {
|
|
|
5969
5996
|
return this.undoTodo(sid, j[j.length - 1].todoId);
|
|
5970
5997
|
};
|
|
5971
5998
|
|
|
5999
|
+
/* /deltodo: oturumun todo listesini tamamen temizler. Dosyaya BOŞ t:todo
|
|
6000
|
+
kaydı append edilir — yüklemede son kayıt kazanır, restart sonrası da
|
|
6001
|
+
boş kalır (eski t:todo satırları yeniden canlanamaz). */
|
|
6002
|
+
Engine.prototype.clearTodos = function (sid) {
|
|
6003
|
+
const key = String(sid || '');
|
|
6004
|
+
try {
|
|
6005
|
+
if (!key || !fs.existsSync(this._file(key))) return { ok: false, error: 'oturum bulunamadı' };
|
|
6006
|
+
} catch {
|
|
6007
|
+
return { ok: false, error: 'oturum bulunamadı' };
|
|
6008
|
+
}
|
|
6009
|
+
const prev = this.todos.get(key) || [];
|
|
6010
|
+
this.todos.set(key, []);
|
|
6011
|
+
try {
|
|
6012
|
+
fs.appendFileSync(this._file(key), JSON.stringify({ t: 'todo', items: [] }) + '\n');
|
|
6013
|
+
} catch {}
|
|
6014
|
+
emitSafe(this, key, { type: 'todos', sessionId: key, todos: [] });
|
|
6015
|
+
return { ok: true, count: prev.length };
|
|
6016
|
+
};
|
|
6017
|
+
|
|
5972
6018
|
module.exports = Engine;
|
|
5973
6019
|
module.exports.Engine = Engine;
|
|
5974
6020
|
module.exports.sanitizeTodoItems = sanitizeTodoItems;
|
package/src/agent/watchers.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
kind=logs → Beast log dosyası taranır; son windowMin dakikadaki error/warn sayısı
|
|
7
7
|
değer olur ("10 dk içinde 3+ hata olursa bağır")
|
|
8
8
|
Koşul sağlanınca ilgili oturuma mesaj düşer (WA köprüsüne de otomatik akar).
|
|
9
|
-
Depo: %APPDATA%\beast\watchers.json
|
|
9
|
+
Depo: %APPDATA%\beast\watchers.json
|
|
10
|
+
Loglar: %APPDATA%\beast\watcher-logs.json (ana depodan AYRI; izleyici başına denetim kaydı) */
|
|
10
11
|
|
|
11
12
|
const fs = require('fs');
|
|
12
13
|
const path = require('path');
|
|
@@ -18,7 +19,89 @@ function file() {
|
|
|
18
19
|
return path.join(beastRoot(), 'watchers.json');
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/* İzleyici logları ANA depodan AYRI tutulur: watcher-logs.json
|
|
23
|
+
Yapı: { logs: { [watcherId]: [{ ts, ev: ok|trigger|error, value, error }] } }
|
|
24
|
+
İzleyici başına LOG_CAP kayıt tutulur (yenisi eklenince en eskisi düşer). */
|
|
25
|
+
const LOG_CAP = 200;
|
|
26
|
+
const LOG_FILE = 'watcher-logs.json';
|
|
27
|
+
|
|
28
|
+
function logFile() {
|
|
29
|
+
return path.join(beastRoot(), LOG_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
let items = [];
|
|
33
|
+
let logs = {}; // watcherId -> [{ ts, ev, value, error }]
|
|
34
|
+
let logsDirty = false;
|
|
35
|
+
|
|
36
|
+
function loadLogs() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(fs.readFileSync(logFile(), 'utf8'));
|
|
39
|
+
logs = raw && typeof raw.logs === 'object' && raw.logs !== null ? raw.logs : {};
|
|
40
|
+
} catch {
|
|
41
|
+
logs = {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveLogs(force) {
|
|
46
|
+
if (!logsDirty && !force) return;
|
|
47
|
+
try {
|
|
48
|
+
fs.mkdirSync(beastRoot(), { recursive: true });
|
|
49
|
+
fs.writeFileSync(logFile(), JSON.stringify({ logs }, null, 2));
|
|
50
|
+
logsDirty = false;
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Tek izleyiciye log kaydı düş — değer/seri hale getirilemezse güvenli temsil */
|
|
55
|
+
function pushLog(id, ev, value, error) {
|
|
56
|
+
let v = null;
|
|
57
|
+
if (value !== undefined && value !== null) {
|
|
58
|
+
try {
|
|
59
|
+
v = typeof value === 'object' ? JSON.stringify(value).slice(0, 200) : value;
|
|
60
|
+
} catch {
|
|
61
|
+
v = String(value).slice(0, 200);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const arr = Array.isArray(logs[id]) ? logs[id] : (logs[id] = []);
|
|
65
|
+
arr.push({
|
|
66
|
+
ts: new Date().toISOString(),
|
|
67
|
+
ev, // 'ok' | 'trigger' | 'error'
|
|
68
|
+
value: v,
|
|
69
|
+
error: error ? String(error).slice(0, 300) : '',
|
|
70
|
+
});
|
|
71
|
+
if (arr.length > LOG_CAP) arr.splice(0, arr.length - LOG_CAP);
|
|
72
|
+
logsDirty = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Bir izleyicinin logları — en yeni en başta */
|
|
76
|
+
function logEntries(id) {
|
|
77
|
+
const arr = Array.isArray(logs[String(id)]) ? logs[String(id)] : [];
|
|
78
|
+
return arr.slice().reverse();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Tüm izleyicilerin logları — { [watcherId]: [...] }, en yeni en başta */
|
|
82
|
+
function logsAll() {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const k of Object.keys(logs)) out[k] = logEntries(k);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Logları temizle — id verilirse yalnız o izleyici, yoksa hepsi */
|
|
89
|
+
function logClear(id) {
|
|
90
|
+
if (id) delete logs[String(id)];
|
|
91
|
+
else logs = {};
|
|
92
|
+
saveLogs(true);
|
|
93
|
+
return { ok: true };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Artık var olmayan izleyicilerin loglarını temizle */
|
|
97
|
+
function pruneLogs() {
|
|
98
|
+
const ids = new Set(items.map((w) => w.id));
|
|
99
|
+
let changed = false;
|
|
100
|
+
for (const k of Object.keys(logs)) {
|
|
101
|
+
if (!ids.has(k)) { delete logs[k]; changed = true; }
|
|
102
|
+
}
|
|
103
|
+
if (changed) saveLogs(true);
|
|
104
|
+
}
|
|
22
105
|
|
|
23
106
|
function load() {
|
|
24
107
|
try {
|
|
@@ -27,6 +110,8 @@ function load() {
|
|
|
27
110
|
} catch {
|
|
28
111
|
items = [];
|
|
29
112
|
}
|
|
113
|
+
loadLogs();
|
|
114
|
+
pruneLogs();
|
|
30
115
|
}
|
|
31
116
|
|
|
32
117
|
function save() {
|
|
@@ -189,6 +274,7 @@ function remove(id) {
|
|
|
189
274
|
const before = items.length;
|
|
190
275
|
items = items.filter((w) => w.id !== id);
|
|
191
276
|
save();
|
|
277
|
+
if (items.length < before) logClear(id); /* izleyici gitti → logları da gitsin */
|
|
192
278
|
return { ok: items.length < before };
|
|
193
279
|
}
|
|
194
280
|
|
|
@@ -283,6 +369,7 @@ async function tickOnce(deps = {}) {
|
|
|
283
369
|
const r = applyCheck(w, value, now);
|
|
284
370
|
Object.assign(w, r.patch);
|
|
285
371
|
w.lastError = '';
|
|
372
|
+
pushLog(w.id, r.triggered ? 'trigger' : 'ok', value);
|
|
286
373
|
if (r.triggered) {
|
|
287
374
|
if (typeof hooks.onTrigger === 'function') hooks.onTrigger({ ...w }, value);
|
|
288
375
|
events.push({ id: w.id, name: w.name, value });
|
|
@@ -290,9 +377,11 @@ async function tickOnce(deps = {}) {
|
|
|
290
377
|
} catch (e) {
|
|
291
378
|
w.lastCheckAt = new Date(now).toISOString();
|
|
292
379
|
w.lastError = String((e && e.message) || e).slice(0, 300);
|
|
380
|
+
pushLog(w.id, 'error', undefined, w.lastError);
|
|
293
381
|
}
|
|
294
382
|
}
|
|
295
383
|
save();
|
|
384
|
+
saveLogs();
|
|
296
385
|
return events;
|
|
297
386
|
}
|
|
298
387
|
|
|
@@ -330,4 +419,7 @@ module.exports = {
|
|
|
330
419
|
applyCheck,
|
|
331
420
|
isDue,
|
|
332
421
|
cooldownActive,
|
|
422
|
+
logEntries,
|
|
423
|
+
logsAll,
|
|
424
|
+
logClear,
|
|
333
425
|
};
|
package/src/cron.js
CHANGED
|
@@ -136,12 +136,13 @@ function list() {
|
|
|
136
136
|
return jobs.map((j) => ({ ...j }));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function add({ name, schedule, prompt, once, sessionId }) {
|
|
139
|
+
function add({ name, schedule, prompt, once, sessionId, kind }) {
|
|
140
140
|
const n = String(name || '').trim().slice(0, 80) || 'Görev';
|
|
141
141
|
const s = String(schedule || '').trim();
|
|
142
142
|
const p = String(prompt || '').trim().slice(0, 4000);
|
|
143
143
|
if (!isValidSchedule(s)) return { ok: false, error: 'geçersiz cron ifadesi (örn: */15 * * * *)' };
|
|
144
144
|
if (!p) return { ok: false, error: 'görev metni boş olamaz' };
|
|
145
|
+
const k = String(kind || '').trim().slice(0, 24);
|
|
145
146
|
const job = {
|
|
146
147
|
id: uid(),
|
|
147
148
|
name: n,
|
|
@@ -150,6 +151,7 @@ function add({ name, schedule, prompt, once, sessionId }) {
|
|
|
150
151
|
sessionId: sessionId || null,
|
|
151
152
|
once: !!once,
|
|
152
153
|
enabled: true,
|
|
154
|
+
...(k ? { kind: k } : {}),
|
|
153
155
|
createdAt: new Date().toISOString(),
|
|
154
156
|
lastRunAt: null,
|
|
155
157
|
nextRunAt: nextRunFrom(s, new Date()),
|
|
@@ -186,6 +188,17 @@ function remove(id) {
|
|
|
186
188
|
return { ok: jobs.length < before };
|
|
187
189
|
}
|
|
188
190
|
|
|
191
|
+
/* Koşula uyan TÜM görevleri sil (/autodel, "hatırlatmaları sil"). */
|
|
192
|
+
function removeIf(pred) {
|
|
193
|
+
const before = jobs.length;
|
|
194
|
+
jobs = jobs.filter((j) => {
|
|
195
|
+
try { return !pred(j); } catch { return true; }
|
|
196
|
+
});
|
|
197
|
+
const count = before - jobs.length;
|
|
198
|
+
if (count) save();
|
|
199
|
+
return { ok: true, count };
|
|
200
|
+
}
|
|
201
|
+
|
|
189
202
|
/* Tüm görevleri bellekten ve diskten sil; zamanlayıcıyı durdur.
|
|
190
203
|
"hepsini sil" / "/cron clear" için kullanılır. */
|
|
191
204
|
function clearAll() {
|
|
@@ -233,17 +246,43 @@ function tick() {
|
|
|
233
246
|
}
|
|
234
247
|
}
|
|
235
248
|
|
|
249
|
+
/* Uygulama kapalıyken saati geçen tek-seferlik hatırlatma için teslim penceresi:
|
|
250
|
+
bu kadar süre geçmediyse uyarı GECİKMELİ de olsa teslim edilir. */
|
|
251
|
+
const ONCE_GRACE_MS = 60 * 60 * 1000;
|
|
252
|
+
|
|
253
|
+
/* Açılış uzlaşması (saf): saati geçmiş tek-seferlik hatırlatmalar eski
|
|
254
|
+
davranışta nextRunFrom ile GELECEK YILIN aynı gününe yeniden kuruluyordu —
|
|
255
|
+
bayat kayıt ölümsüzleşip tekrar tekrar önüne düşüyordu. Artık: grace
|
|
256
|
+
içindeyse "fire" listesine (gecikmeli teslim), bayatsa listeden düşer.
|
|
257
|
+
Tekrarlıların nextRunAt'ı her zamanki gibi yeniden hesaplanır. */
|
|
258
|
+
function reconcile(list, now, graceMs) {
|
|
259
|
+
const keep = [];
|
|
260
|
+
const fire = [];
|
|
261
|
+
for (const job of list) {
|
|
262
|
+
if (!job.enabled) { keep.push(job); continue; }
|
|
263
|
+
const nr = job.nextRunAt ? new Date(job.nextRunAt).getTime() : 0;
|
|
264
|
+
if (job.once && nr && nr <= now) {
|
|
265
|
+
if (now - nr <= graceMs) fire.push(job);
|
|
266
|
+
continue; // bayat olan burada düşer
|
|
267
|
+
}
|
|
268
|
+
if (!nr || nr <= now) {
|
|
269
|
+
keep.push({ ...job, nextRunAt: nextRunFrom(job.schedule, new Date(now)) });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
keep.push(job);
|
|
273
|
+
}
|
|
274
|
+
return { jobs: keep, fire };
|
|
275
|
+
}
|
|
276
|
+
|
|
236
277
|
function init(hooks) {
|
|
237
278
|
onFire = (hooks && hooks.onFire) || null;
|
|
238
279
|
load();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
job.nextRunAt = nextRunFrom(job.schedule, new Date());
|
|
242
|
-
}
|
|
243
|
-
}
|
|
280
|
+
const r = reconcile(jobs, Date.now(), ONCE_GRACE_MS);
|
|
281
|
+
jobs = r.jobs;
|
|
244
282
|
save();
|
|
245
283
|
if (timer) clearInterval(timer);
|
|
246
284
|
timer = setInterval(tick, 20 * 1000);
|
|
285
|
+
for (const job of r.fire) fire(job); // grace içindeki bayat hatırlatmayı gecikmeli teslim et
|
|
247
286
|
}
|
|
248
287
|
|
|
249
288
|
/* /stop: zamanlayıcıyı durdur (init ile geri başlar) */
|
|
@@ -252,4 +291,4 @@ function stop() {
|
|
|
252
291
|
timer = null;
|
|
253
292
|
}
|
|
254
293
|
|
|
255
|
-
module.exports = { init, stop, list, add, update, remove, clearAll, toggle, runNow, parseCron, nextRunFrom, isValidSchedule, reminderSchedule };
|
|
294
|
+
module.exports = { init, stop, list, add, update, remove, removeIf, clearAll, toggle, runNow, parseCron, nextRunFrom, isValidSchedule, reminderSchedule, reconcile, ONCE_GRACE_MS };
|
package/src/main.js
CHANGED
|
@@ -1106,6 +1106,8 @@ function waSlashHelp() {
|
|
|
1106
1106
|
'• */usage* – bugünkü kullanım',
|
|
1107
1107
|
'• */backup* – tüm veriyi ŞİFRELİ yedekle (Beast Kodu imzalı, Masaüstü\\Beast-Backups)',
|
|
1108
1108
|
'• */status* – bağlantı ve servis durumu',
|
|
1109
|
+
'• */autodel* – tüm otomatik hatırlatmaları (bayat/eski dahil) tek komutla sil · */autodel all*: cron görevleri dahil hepsi',
|
|
1110
|
+
'• */deltodo* – bu oturumun todo listesini temizle · */deltodo all*: tüm oturumların todoları',
|
|
1109
1111
|
'',
|
|
1110
1112
|
'Gruplarda beni @mention ile çağır; ardışık mesajlarını tek cevapta birleştiririm.',
|
|
1111
1113
|
].join('\n');
|
|
@@ -1646,6 +1648,58 @@ async function tryWaSlash(jid, rawText, senderNum, payload0) {
|
|
|
1646
1648
|
} else {
|
|
1647
1649
|
out = 'Kullanım: `/cron list` · `/cron clear` (hepsini sil) · `/cron del <id>`';
|
|
1648
1650
|
}
|
|
1651
|
+
} else if (cmd === 'autodel') {
|
|
1652
|
+
/* /autodel [all] — TÜM otomatik hatırlatmaları tek komutla sil.
|
|
1653
|
+
Bayat/eski kayıtlar dahil: kind=reminder + eski isim/prompt önekleri eşleşir.
|
|
1654
|
+
/autodel all → cron görevleri dahil her şey (=/cron clear) */
|
|
1655
|
+
const sub = String(arg || '').toLowerCase().trim();
|
|
1656
|
+
if (sub === 'all' || sub === 'hepsi' || sub === 'hepsini') {
|
|
1657
|
+
const r = cron.clearAll();
|
|
1658
|
+
cronEmit();
|
|
1659
|
+
out = r.ok
|
|
1660
|
+
? `*Tüm zamanlanmış görevler silindi* (${r.count} adet).`
|
|
1661
|
+
: 'Temizlenemedi.';
|
|
1662
|
+
} else {
|
|
1663
|
+
const r = cron.removeIf(isReminderJob);
|
|
1664
|
+
cronEmit();
|
|
1665
|
+
const left = cron.list().filter((j) => j.enabled).length;
|
|
1666
|
+
out = r.count
|
|
1667
|
+
? `*${r.count} otomatik hatırlatma silindi.* Kalan aktif görev: ${left}`
|
|
1668
|
+
: 'Silinecek otomatik hatırlatma yok — zaten temiz.';
|
|
1669
|
+
}
|
|
1670
|
+
} else if (cmd === 'deltodo') {
|
|
1671
|
+
/* /deltodo [all] — todo listelerini temizler: arg=all → TÜM oturumlar;
|
|
1672
|
+
yoksa yalnız bu sohbetin oturumu. Panelden de düşsün diye 'todos'
|
|
1673
|
+
olayı emit edilir (engine.clearTodos). */
|
|
1674
|
+
const sub = String(arg || '').toLowerCase().trim();
|
|
1675
|
+
if (sub === 'all' || sub === 'hepsi' || sub === 'hepsini') {
|
|
1676
|
+
let n = 0;
|
|
1677
|
+
let items = 0;
|
|
1678
|
+
try {
|
|
1679
|
+
for (const v of engine.listSessions()) {
|
|
1680
|
+
const r = engine.clearTodos(v.id);
|
|
1681
|
+
if (r && r.ok) {
|
|
1682
|
+
n++;
|
|
1683
|
+
items += r.count || 0;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
} catch {}
|
|
1687
|
+
out = n
|
|
1688
|
+
? `*Tüm oturumların todoları temizlendi* (${n} oturum, ${items} madde).`
|
|
1689
|
+
: 'Temizlenecek todo yok.';
|
|
1690
|
+
} else {
|
|
1691
|
+
const sid = waChats.get(jid) || waSingleSid || '';
|
|
1692
|
+
if (!sid) {
|
|
1693
|
+
out = 'Bu sohbette açık oturum yok — todo listesi de yok.';
|
|
1694
|
+
} else {
|
|
1695
|
+
const r = engine.clearTodos(sid);
|
|
1696
|
+
out = r.ok
|
|
1697
|
+
? (r.count
|
|
1698
|
+
? `*${r.count} todo temizlendi.* (tüm oturumlar için: /deltodo all)`
|
|
1699
|
+
: 'Todo listesi zaten boş. (tüm oturumlar için: /deltodo all)')
|
|
1700
|
+
: 'Oturum bulunamadı — todo listesi yok.';
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1649
1703
|
} else if (cmd === 'beastcode') {
|
|
1650
1704
|
/* /beastcode [görev] — WhatsApp'tan UZAKTAN KODLAMA modu:
|
|
1651
1705
|
masaüstünde GERÇEK Beast Code paneli açılır (IDE ekranı), sohbet
|
|
@@ -2084,6 +2138,22 @@ async function documentToTextAsync(media) {
|
|
|
2084
2138
|
|
|
2085
2139
|
/* ---------- WA hatırlatıcı ---------- */
|
|
2086
2140
|
|
|
2141
|
+
/* Hatırlatma job'ı mı? Yeni kayıtlarda kind='reminder' var; eski (bayat)
|
|
2142
|
+
kayıtlar isim/prompt önekinden tanınır — /autodel ve doğal dil silme
|
|
2143
|
+
bunu kullanır. */
|
|
2144
|
+
function isReminderJob(j) {
|
|
2145
|
+
if (!j || typeof j !== 'object') return false;
|
|
2146
|
+
if (j.kind === 'reminder') return true;
|
|
2147
|
+
const n = String(j.name || '');
|
|
2148
|
+
const p = String(j.prompt || '');
|
|
2149
|
+
return (
|
|
2150
|
+
/^Hatırlatma:/.test(n) ||
|
|
2151
|
+
/^Tekrarlı hatırlatma:/.test(n) ||
|
|
2152
|
+
p.startsWith('[HATIRLATMA ZAMANI]') ||
|
|
2153
|
+
p.startsWith('[TEKRARLI HATIRLATMA]')
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2087
2157
|
function scheduleReminder({ when, message, sessionId, repeat }) {
|
|
2088
2158
|
try {
|
|
2089
2159
|
const msg = String(message || '').trim();
|
|
@@ -2097,6 +2167,7 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
|
|
|
2097
2167
|
name: 'Tekrarlı hatırlatma: ' + msg.slice(0, 40),
|
|
2098
2168
|
schedule: s.schedule,
|
|
2099
2169
|
prompt: `[TEKRARLI HATIRLATMA] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
|
|
2170
|
+
kind: 'reminder',
|
|
2100
2171
|
sessionId: sessionId || undefined,
|
|
2101
2172
|
});
|
|
2102
2173
|
if (!r.ok) return r;
|
|
@@ -2111,6 +2182,7 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
|
|
|
2111
2182
|
name: 'Hatırlatma: ' + msg.slice(0, 40),
|
|
2112
2183
|
schedule: cronExpr,
|
|
2113
2184
|
prompt: `[HATIRLATMA ZAMANI] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
|
|
2185
|
+
kind: 'reminder',
|
|
2114
2186
|
once: true,
|
|
2115
2187
|
sessionId: sessionId || undefined,
|
|
2116
2188
|
});
|
|
@@ -2291,6 +2363,23 @@ async function handleWaIncoming(jid, payload, senderNum) {
|
|
|
2291
2363
|
return;
|
|
2292
2364
|
}
|
|
2293
2365
|
|
|
2366
|
+
/* "hatırlatmaları sil" / "alarm temizle" / "reminders kaldır" → yalnızca
|
|
2367
|
+
otomatik hatırlatmalar silinir (cron görevlerine dokunulmaz). */
|
|
2368
|
+
if (
|
|
2369
|
+
/hat[iı]rlat|alarm|remind/.test(lc) &&
|
|
2370
|
+
/(sil|sıl|temizle|kaldır|iptal|sıfırla|kapat)/.test(lc)
|
|
2371
|
+
) {
|
|
2372
|
+
const r = cron.removeIf(isReminderJob);
|
|
2373
|
+
cronEmit();
|
|
2374
|
+
await sendWaSafe(
|
|
2375
|
+
jid,
|
|
2376
|
+
r.count
|
|
2377
|
+
? `*${r.count} otomatik hatırlatma silindi.* Zamanlanmış kalan görev: ${cron.list().filter((j) => j.enabled).length}`
|
|
2378
|
+
: 'Silinecek otomatik hatırlatma yok — zaten temiz.'
|
|
2379
|
+
).catch(() => {});
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2294
2383
|
if (isGroup) {
|
|
2295
2384
|
const g = settings.waGroups || {};
|
|
2296
2385
|
if (!g.enabled) return;
|
|
@@ -3307,11 +3396,11 @@ function reloadBackend() {
|
|
|
3307
3396
|
}
|
|
3308
3397
|
/* CRON → TÜM AKTİF ENTEGRASYONLAR: cron işi bittiğinde cevap (ya da
|
|
3309
3398
|
hata) sahibin bağlı olduğu her kanala yansıtılır — WA + Telegram +
|
|
3310
|
-
Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır.
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
if (!ev.aborted) {
|
|
3399
|
+
Discord. Aynı kanal hem cron oturumuna bağlıysa TEK cevap alır.
|
|
3400
|
+
Bayat (TTL aşımı) bekleme kaydı Take ile düşer, yansıtılmaz. */
|
|
3401
|
+
if (ev.type === 'done' || ev.type === 'error') {
|
|
3402
|
+
const cjob = cronAnswerPendingTake(ev.sessionId);
|
|
3403
|
+
if (cjob && !ev.aborted) {
|
|
3315
3404
|
(async () => {
|
|
3316
3405
|
try {
|
|
3317
3406
|
let txt = '';
|
|
@@ -6519,8 +6608,40 @@ function cronEmit() {
|
|
|
6519
6608
|
|
|
6520
6609
|
/* Cron cevabı bekleme haritası: sid -> job. İş 'done' olunca cevap
|
|
6521
6610
|
SAHİBİN bağlı olduğu TÜM AKTİF entegrasyonlara yansıtılır (kullanıcı
|
|
6522
|
-
hangi kanaldan ajanla iletişimde belli değil).
|
|
6611
|
+
hangi kanaldan ajanla iletişimde belli değil). Kayıtlar TTL'li —
|
|
6612
|
+
done/error hiç gelmezse bayat bekleme sonraki done'a yansıtılmaz. */
|
|
6523
6613
|
const cronAnswerPending = new Map();
|
|
6614
|
+
const cronAnswerPendingAt = new Map();
|
|
6615
|
+
const CRON_ANSWER_TTL_MS = 30 * 60 * 1000;
|
|
6616
|
+
|
|
6617
|
+
function cronAnswerPendingSet(sid, job) {
|
|
6618
|
+
cronAnswerPendingSweep();
|
|
6619
|
+
cronAnswerPending.set(String(sid), job);
|
|
6620
|
+
cronAnswerPendingAt.set(String(sid), Date.now());
|
|
6621
|
+
}
|
|
6622
|
+
|
|
6623
|
+
function cronAnswerPendingDrop(sid) {
|
|
6624
|
+
cronAnswerPending.delete(String(sid));
|
|
6625
|
+
cronAnswerPendingAt.delete(String(sid));
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
/* Bayat kayıt (TTL aşımı) haritalardan temizlenir */
|
|
6629
|
+
function cronAnswerPendingSweep() {
|
|
6630
|
+
const now = Date.now();
|
|
6631
|
+
for (const [k, at] of cronAnswerPendingAt) {
|
|
6632
|
+
if (now - at > CRON_ANSWER_TTL_MS) cronAnswerPendingDrop(k);
|
|
6633
|
+
}
|
|
6634
|
+
}
|
|
6635
|
+
|
|
6636
|
+
/* Kaydı okuyup düşürür; bayat kayıt ise null döner (yansıtma yok) */
|
|
6637
|
+
function cronAnswerPendingTake(sid) {
|
|
6638
|
+
const k = String(sid);
|
|
6639
|
+
const job = cronAnswerPending.get(k);
|
|
6640
|
+
const at = cronAnswerPendingAt.get(k) || 0;
|
|
6641
|
+
cronAnswerPendingDrop(k);
|
|
6642
|
+
if (!job || Date.now() - at > CRON_ANSWER_TTL_MS) return null;
|
|
6643
|
+
return job;
|
|
6644
|
+
}
|
|
6524
6645
|
|
|
6525
6646
|
/* OTOMATİK YENİ SOHBET YOK: cron/izleyici/fallout tetiklendiğinde oturum
|
|
6526
6647
|
seçimi — kayıtlı id'nin DOSYASI hâlâ duruyorsa O, değilse EN GÜNCEL
|
|
@@ -6600,11 +6721,11 @@ function cronFire(job) {
|
|
|
6600
6721
|
if (sid !== String(job.sessionId || '')) {
|
|
6601
6722
|
cron.update(job.id, { sessionId: sid });
|
|
6602
6723
|
}
|
|
6603
|
-
|
|
6724
|
+
cronAnswerPendingSet(sid, job);
|
|
6604
6725
|
const sent = engine.send(sid, {
|
|
6605
6726
|
text: `[cron: ${job.name}]\n${job.prompt}`,
|
|
6606
6727
|
});
|
|
6607
|
-
if (!sent)
|
|
6728
|
+
if (!sent) cronAnswerPendingDrop(sid); // gönderilemedi — bayat bekleme bırakma
|
|
6608
6729
|
} catch {}
|
|
6609
6730
|
cronEmit();
|
|
6610
6731
|
}
|
|
@@ -7124,6 +7245,9 @@ ipcMain.handle('watchers:toggle', (_e, id) => {
|
|
|
7124
7245
|
watchers.patch(String(id), { enabled: !w.enabled });
|
|
7125
7246
|
return { ok: true, watcher: watchers.get(String(id)) };
|
|
7126
7247
|
});
|
|
7248
|
+
/* izleyici logları — ana depodan ayrı watcher-logs.json üzerinden */
|
|
7249
|
+
ipcMain.handle('watchers:logs', () => watchers.logsAll());
|
|
7250
|
+
ipcMain.handle('watchers:logsClear', (_e, id) => watchers.logClear(id ? String(id) : null));
|
|
7127
7251
|
ipcMain.handle('cron:add', (_e, job) => {
|
|
7128
7252
|
const r = cron.add(job || {});
|
|
7129
7253
|
cronEmit();
|
package/src/preload.js
CHANGED
|
@@ -146,6 +146,10 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
146
146
|
|
|
147
147
|
watchersToggle: (id) => ipcRenderer.invoke('watchers:toggle', id),
|
|
148
148
|
|
|
149
|
+
watchersLogs: () => ipcRenderer.invoke('watchers:logs'),
|
|
150
|
+
|
|
151
|
+
watchersLogsClear: (id) => ipcRenderer.invoke('watchers:logsClear', id),
|
|
152
|
+
|
|
149
153
|
providerKeys: () => ipcRenderer.invoke('providers:keys'),
|
|
150
154
|
|
|
151
155
|
openPath: (p) => ipcRenderer.invoke('shell:openPath', p),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -542,6 +542,14 @@
|
|
|
542
542
|
bot_no_watchers: 'Bu bota ait izleyici yok.',
|
|
543
543
|
w_pane: 'İZLEYİCİLER',
|
|
544
544
|
w_empty: 'Kayıtlı izleyici yok.',
|
|
545
|
+
tipWatchLog: 'İzleyici loglarını gör',
|
|
546
|
+
w_log_title: 'İzleyici Logları',
|
|
547
|
+
w_log_clear: 'Seçili izleyicinin loglarını temizle',
|
|
548
|
+
w_log_cleared: 'Loglar temizlendi',
|
|
549
|
+
w_log_empty: 'Bu izleyici için henüz log kaydı yok.',
|
|
550
|
+
w_log_ev_ok: 'kontrol',
|
|
551
|
+
w_log_ev_trigger: 'tetik',
|
|
552
|
+
w_log_ev_error: 'hata',
|
|
545
553
|
bot_stats_sub: 'Bu botun kullanım özeti.',
|
|
546
554
|
bot_stat_numbers: 'Numara',
|
|
547
555
|
bot_stat_sessions: 'Sohbet',
|
|
@@ -1038,6 +1046,14 @@
|
|
|
1038
1046
|
cr_toggle_pause: 'Pause',
|
|
1039
1047
|
w_empty: 'No saved watchers — add from the form above.',
|
|
1040
1048
|
w_pane: 'WATCHERS',
|
|
1049
|
+
tipWatchLog: 'View watcher logs',
|
|
1050
|
+
w_log_title: 'Watcher Logs',
|
|
1051
|
+
w_log_clear: 'Clear selected watcher logs',
|
|
1052
|
+
w_log_cleared: 'Logs cleared',
|
|
1053
|
+
w_log_empty: 'No log entries for this watcher yet.',
|
|
1054
|
+
w_log_ev_ok: 'check',
|
|
1055
|
+
w_log_ev_trigger: 'trigger',
|
|
1056
|
+
w_log_ev_error: 'error',
|
|
1041
1057
|
wa_vazgec: 'Cancel',
|
|
1042
1058
|
wa_kaydet: 'save',
|
|
1043
1059
|
wa_updated: 'Updated',
|
package/src/renderer/index.html
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
<div id="watchPaneHead">
|
|
18
18
|
<span id="watchPaneTitle" data-i18n="w_pane">İZLEYİCİLER</span>
|
|
19
19
|
<span class="file-spacer"></span>
|
|
20
|
-
<button id="
|
|
20
|
+
<button id="watchPaneLog" title="İzleyici loglarını gör" data-i18n-title="tipWatchLog"><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></button>
|
|
21
|
+
<button id="watchPaneOpen" title="İzleyicileri yönet" data-i18n-title="tipWatch">+</button>
|
|
21
22
|
</div>
|
|
22
23
|
<div id="watchPaneList"></div>
|
|
23
24
|
</div>
|
|
@@ -528,6 +529,22 @@
|
|
|
528
529
|
</div>
|
|
529
530
|
</div>
|
|
530
531
|
|
|
532
|
+
<div id="watchLogOverlay" class="mini-overlay" hidden>
|
|
533
|
+
<div class="mini-dialog">
|
|
534
|
+
<div class="mini-head">
|
|
535
|
+
<span class="mini-title" data-i18n="w_log_title">İzleyici Logları</span>
|
|
536
|
+
<button class="mini-close" id="watchLogClose" title="Kapat" data-i18n-title="btn_close">×</button>
|
|
537
|
+
</div>
|
|
538
|
+
<div class="mini-body">
|
|
539
|
+
<div id="watchLogBar">
|
|
540
|
+
<select id="watchLogSel" class="mini-select"></select>
|
|
541
|
+
<button id="watchLogClear" class="mr-btn" title="Seçili izleyicinin loglarını temizle" data-i18n-title="w_log_clear"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
|
|
542
|
+
</div>
|
|
543
|
+
<div id="watchLogList" class="mini-list"></div>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
531
548
|
<div id="finSymOverlay" class="mini-overlay" hidden>
|
|
532
549
|
<div class="mini-dialog fin-sym-dialog">
|
|
533
550
|
<div class="mini-head">
|
package/src/renderer/renderer.js
CHANGED
|
@@ -45,6 +45,7 @@ const els = {
|
|
|
45
45
|
watchBtn: $('#watchBtn'),
|
|
46
46
|
watchPaneList: $('#watchPaneList'),
|
|
47
47
|
watchPaneOpen: $('#watchPaneOpen'),
|
|
48
|
+
watchPaneLog: $('#watchPaneLog'),
|
|
48
49
|
cronBtn: $('#cronBtn'),
|
|
49
50
|
watchOverlay: $('#watchOverlay'),
|
|
50
51
|
cronOverlay: $('#cronOverlay'),
|
|
@@ -52,6 +53,11 @@ const els = {
|
|
|
52
53
|
cronClose: $('#cronClose'),
|
|
53
54
|
watchList: $('#watchList'),
|
|
54
55
|
cronModalList: $('#cronModalList'),
|
|
56
|
+
watchLogOverlay: $('#watchLogOverlay'),
|
|
57
|
+
watchLogClose: $('#watchLogClose'),
|
|
58
|
+
watchLogSel: $('#watchLogSel'),
|
|
59
|
+
watchLogClear: $('#watchLogClear'),
|
|
60
|
+
watchLogList: $('#watchLogList'),
|
|
55
61
|
browserBar: $('#browserBar'),
|
|
56
62
|
bbBack: $('#bbBack'),
|
|
57
63
|
bbFwd: $('#bbFwd'),
|
|
@@ -5611,6 +5617,11 @@ async function renderWatchersPane(force) {
|
|
|
5611
5617
|
`</div></div>`;
|
|
5612
5618
|
const btns = document.createElement('div');
|
|
5613
5619
|
btns.className = 'wp-btns';
|
|
5620
|
+
const lg = document.createElement('button');
|
|
5621
|
+
lg.className = 'wp-btn';
|
|
5622
|
+
lg.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
|
|
5623
|
+
lg.title = _t('w_log_title');
|
|
5624
|
+
lg.addEventListener('click', (e) => { e.stopPropagation(); openWatchLogModal(w.id); });
|
|
5614
5625
|
const t = document.createElement('button');
|
|
5615
5626
|
t.className = 'wp-btn';
|
|
5616
5627
|
t.textContent = w.enabled ? '❚❚' : '▶';
|
|
@@ -5632,7 +5643,7 @@ async function renderWatchersPane(force) {
|
|
|
5632
5643
|
renderWatchersPane(true);
|
|
5633
5644
|
renderWatchersModal(true);
|
|
5634
5645
|
});
|
|
5635
|
-
btns.append(t, d);
|
|
5646
|
+
btns.append(lg, t, d);
|
|
5636
5647
|
row.appendChild(btns);
|
|
5637
5648
|
row.addEventListener('click', () => openWatchModal());
|
|
5638
5649
|
list.appendChild(row);
|
|
@@ -5669,6 +5680,11 @@ async function renderWatchersModal(force) {
|
|
|
5669
5680
|
`</div></div>`;
|
|
5670
5681
|
const btns = document.createElement('div');
|
|
5671
5682
|
btns.style.cssText = 'display:flex;gap:6px;flex:none';
|
|
5683
|
+
const lg = document.createElement('button');
|
|
5684
|
+
lg.className = 'mr-btn';
|
|
5685
|
+
lg.innerHTML = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>';
|
|
5686
|
+
lg.title = _t('w_log_title');
|
|
5687
|
+
lg.addEventListener('click', () => openWatchLogModal(w.id));
|
|
5672
5688
|
const t = document.createElement('button');
|
|
5673
5689
|
t.className = 'mr-btn';
|
|
5674
5690
|
t.textContent = w.enabled ? '❚❚' : '▶';
|
|
@@ -5679,12 +5695,85 @@ async function renderWatchersModal(force) {
|
|
|
5679
5695
|
d.textContent = '×';
|
|
5680
5696
|
d.title = _t('cr_del');
|
|
5681
5697
|
d.addEventListener('click', async () => { await beast.watchersRemove(w.id); renderWatchersModal(); });
|
|
5682
|
-
btns.append(t, d);
|
|
5698
|
+
btns.append(lg, t, d);
|
|
5683
5699
|
row.appendChild(btns);
|
|
5684
5700
|
els.watchList.appendChild(row);
|
|
5685
5701
|
}
|
|
5686
5702
|
}
|
|
5687
5703
|
|
|
5704
|
+
/* ---------- İZLEYİCİ LOG MODALI ----------
|
|
5705
|
+
Her izleyicinin logu AYRI tutulur (watcher-logs.json) ve AYRI gösterilir:
|
|
5706
|
+
üstteki seçiciyle izleyici seç → yalnız onun denetim kayıtları listelenir. */
|
|
5707
|
+
let watchLogSelId = '';
|
|
5708
|
+
let lastWatchLogKey = '';
|
|
5709
|
+
let lastWatchLogSelKey = '';
|
|
5710
|
+
|
|
5711
|
+
async function renderWatchLogModal(force) {
|
|
5712
|
+
if (!els.watchLogOverlay) return;
|
|
5713
|
+
let rows = []; try { rows = (await beast.watchersList()) || []; } catch {}
|
|
5714
|
+
let logsMap = {}; try { logsMap = (await beast.watchersLogs()) || {}; } catch {}
|
|
5715
|
+
/* seçim: yok olan seçilmişse ilk izleyiciye düş */
|
|
5716
|
+
if (!rows.some((w) => w.id === watchLogSelId)) watchLogSelId = rows.length ? rows[0].id : '';
|
|
5717
|
+
/* seçici — izleyici listesi değişmediyse DOM'a dokunma (odak korunur) */
|
|
5718
|
+
const selKey = JSON.stringify(rows.map((w) => [w.id, w.name, w.enabled]));
|
|
5719
|
+
if (selKey !== lastWatchLogSelKey) {
|
|
5720
|
+
lastWatchLogSelKey = selKey;
|
|
5721
|
+
els.watchLogSel.innerHTML = '';
|
|
5722
|
+
if (!rows.length) {
|
|
5723
|
+
const o = document.createElement('option');
|
|
5724
|
+
o.textContent = _t('w_empty');
|
|
5725
|
+
o.value = '';
|
|
5726
|
+
els.watchLogSel.appendChild(o);
|
|
5727
|
+
} else {
|
|
5728
|
+
for (const w of rows) {
|
|
5729
|
+
const o = document.createElement('option');
|
|
5730
|
+
o.value = w.id;
|
|
5731
|
+
o.textContent = (w.enabled ? '' : '⏸ ') + w.name;
|
|
5732
|
+
els.watchLogSel.appendChild(o);
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
els.watchLogSel.value = watchLogSelId;
|
|
5736
|
+
}
|
|
5737
|
+
/* log satırları — yalnız seçili izleyicinin kayıtları */
|
|
5738
|
+
const entries = watchLogSelId ? (logsMap[watchLogSelId] || []) : [];
|
|
5739
|
+
const key = JSON.stringify([watchLogSelId, selKey, entries]);
|
|
5740
|
+
if (!force && key === lastWatchLogKey) return;
|
|
5741
|
+
lastWatchLogKey = key;
|
|
5742
|
+
els.watchLogList.innerHTML = '';
|
|
5743
|
+
if (!watchLogSelId || !entries.length) {
|
|
5744
|
+
els.watchLogList.innerHTML = '<div class="mini-empty">' + _t('w_log_empty') + '</div>';
|
|
5745
|
+
return;
|
|
5746
|
+
}
|
|
5747
|
+
const evBadge = { ok: ['ok', _t('w_log_ev_ok')], trigger: ['trigger', _t('w_log_ev_trigger')], error: ['error', _t('w_log_ev_error')] };
|
|
5748
|
+
for (const en of entries) {
|
|
5749
|
+
const [cls, label] = evBadge[en.ev] || ['ok', en.ev];
|
|
5750
|
+
const row = document.createElement('div');
|
|
5751
|
+
row.className = 'wl-row';
|
|
5752
|
+
row.innerHTML =
|
|
5753
|
+
`<span class="wl-time">${escapeHtml(fmtWhen(en.ts))}</span>` +
|
|
5754
|
+
`<span class="wl-badge ${cls}">${escapeHtml(label)}</span>` +
|
|
5755
|
+
`<span class="wl-main">` +
|
|
5756
|
+
(en.ev === 'error'
|
|
5757
|
+
? `<span class="wl-err">${escapeHtml(String(en.error || '').slice(0, 120))}</span>`
|
|
5758
|
+
: (en.value !== null && en.value !== undefined ? escapeHtml(String(en.value).slice(0, 120)) : '—')) +
|
|
5759
|
+
`</span>`;
|
|
5760
|
+
els.watchLogList.appendChild(row);
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
function closeWatchLogModal() {
|
|
5765
|
+
toggleMini(els.watchLogOverlay, true);
|
|
5766
|
+
stopMiniRefresh();
|
|
5767
|
+
}
|
|
5768
|
+
|
|
5769
|
+
async function openWatchLogModal(id) {
|
|
5770
|
+
if (id) watchLogSelId = String(id);
|
|
5771
|
+
lastWatchLogSelKey = ''; /* seçici her açılışta TAZE kurulsun */
|
|
5772
|
+
toggleMini(els.watchLogOverlay, false);
|
|
5773
|
+
await renderWatchLogModal(true);
|
|
5774
|
+
startMiniRefresh();
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5688
5777
|
let lastCronKey = '';
|
|
5689
5778
|
async function renderCronModal(force) {
|
|
5690
5779
|
let jobs = [];
|
|
@@ -5746,6 +5835,7 @@ function startMiniRefresh() {
|
|
|
5746
5835
|
stopMiniRefresh();
|
|
5747
5836
|
miniRefreshTimer = setInterval(async () => {
|
|
5748
5837
|
if (els.watchOverlay && !els.watchOverlay.hidden) await renderWatchersModal();
|
|
5838
|
+
else if (els.watchLogOverlay && !els.watchLogOverlay.hidden) await renderWatchLogModal();
|
|
5749
5839
|
else if (els.cronOverlay && !els.cronOverlay.hidden) await renderCronModal();
|
|
5750
5840
|
else stopMiniRefresh();
|
|
5751
5841
|
}, 4000);
|
|
@@ -6487,12 +6577,29 @@ async function init() {
|
|
|
6487
6577
|
/* cron modülü — olay bağlama */
|
|
6488
6578
|
if (els.watchBtn) els.watchBtn.addEventListener('click', openWatchModal);
|
|
6489
6579
|
if (els.watchPaneOpen) els.watchPaneOpen.addEventListener('click', openWatchModal);
|
|
6580
|
+
if (els.watchPaneLog) els.watchPaneLog.addEventListener('click', () => openWatchLogModal());
|
|
6490
6581
|
if (els.cronBtn) els.cronBtn.addEventListener('click', openCronModal);
|
|
6491
6582
|
if (els.watchClose) els.watchClose.addEventListener('click', closeWatchModal);
|
|
6492
6583
|
if (els.cronClose) els.cronClose.addEventListener('click', closeCronModal);
|
|
6584
|
+
if (els.watchLogClose) els.watchLogClose.addEventListener('click', closeWatchLogModal);
|
|
6585
|
+
if (els.watchLogSel) els.watchLogSel.addEventListener('change', () => {
|
|
6586
|
+
watchLogSelId = els.watchLogSel.value;
|
|
6587
|
+
lastWatchLogKey = '';
|
|
6588
|
+
renderWatchLogModal(true);
|
|
6589
|
+
});
|
|
6590
|
+
if (els.watchLogClear) els.watchLogClear.addEventListener('click', async () => {
|
|
6591
|
+
if (!watchLogSelId) return;
|
|
6592
|
+
await beast.watchersLogsClear(watchLogSelId);
|
|
6593
|
+
lastWatchLogKey = '';
|
|
6594
|
+
await renderWatchLogModal(true);
|
|
6595
|
+
toast(_t('w_log_cleared'));
|
|
6596
|
+
});
|
|
6493
6597
|
if (els.watchOverlay) els.watchOverlay.addEventListener('click', (e) => {
|
|
6494
6598
|
if (e.target === els.watchOverlay) closeWatchModal();
|
|
6495
6599
|
});
|
|
6600
|
+
if (els.watchLogOverlay) els.watchLogOverlay.addEventListener('click', (e) => {
|
|
6601
|
+
if (e.target === els.watchLogOverlay) closeWatchLogModal();
|
|
6602
|
+
});
|
|
6496
6603
|
if (els.cronOverlay) els.cronOverlay.addEventListener('click', (e) => {
|
|
6497
6604
|
if (e.target === els.cronOverlay) closeCronModal();
|
|
6498
6605
|
});
|
|
@@ -6579,6 +6686,7 @@ async function init() {
|
|
|
6579
6686
|
document.addEventListener('keydown', (e) => {
|
|
6580
6687
|
if (e.key === 'Escape') {
|
|
6581
6688
|
if ($('#botOverlay') && !$('#botOverlay').hidden) { botOverlaySetOpen(false); return; }
|
|
6689
|
+
if (els.watchLogOverlay && !els.watchLogOverlay.hidden) { closeWatchLogModal(); return; }
|
|
6582
6690
|
if (els.watchOverlay && !els.watchOverlay.hidden) { closeWatchModal(); return; }
|
|
6583
6691
|
if (els.cronOverlay && !els.cronOverlay.hidden) { closeCronModal(); return; }
|
|
6584
6692
|
if (!els.settingsOverlay.hidden) closeSettings();
|
package/src/renderer/style.css
CHANGED
|
@@ -133,7 +133,6 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
|
|
|
133
133
|
}
|
|
134
134
|
#watchPaneTitle { font-size: 10.5px; letter-spacing: 2px; color: var(--muted); font-weight: 800; }
|
|
135
135
|
#watchPaneHead button {
|
|
136
|
-
margin-left: auto;
|
|
137
136
|
background: none;
|
|
138
137
|
border: 1px solid var(--border);
|
|
139
138
|
border-radius: 7px;
|
|
@@ -142,7 +141,13 @@ body:not(.ide-mode):not(.studio-mode):not(.sandbox-mode) #watchPane { display: f
|
|
|
142
141
|
font-size: 12px;
|
|
143
142
|
line-height: 1.2;
|
|
144
143
|
padding: 1px 7px;
|
|
144
|
+
height: 22px;
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
145
148
|
}
|
|
149
|
+
/* LOG ve + butonları sağda YAN YANA BİTİŞİK dursun: yalnız ilkinde itici boşluk */
|
|
150
|
+
#watchPaneHead button:first-of-type { margin-left: auto; }
|
|
146
151
|
#watchPaneHead button:hover { color: var(--text); background: var(--panel2); }
|
|
147
152
|
#watchPaneList { flex: 1; overflow-y: auto; padding: 4px; }
|
|
148
153
|
.wp-row {
|
|
@@ -1464,6 +1469,35 @@ body.browser-open .mini-overlay { right: var(--bw, 480px); }
|
|
|
1464
1469
|
.mr-btn:hover { color: var(--accent); }
|
|
1465
1470
|
.mr-btn.del:hover { color: var(--err); }
|
|
1466
1471
|
|
|
1472
|
+
/* ---------- İzleyici log modalı (her izleyicinin logu ayrı) ---------- */
|
|
1473
|
+
#watchLogBar { display: flex; gap: 6px; align-items: center; margin-bottom: 10px; }
|
|
1474
|
+
#watchLogSel { flex: 1; min-width: 0; }
|
|
1475
|
+
.mini-select {
|
|
1476
|
+
background: var(--panel2);
|
|
1477
|
+
border: 1px solid var(--border);
|
|
1478
|
+
border-radius: 7px;
|
|
1479
|
+
color: var(--text);
|
|
1480
|
+
font-size: 12px;
|
|
1481
|
+
padding: 4px 6px;
|
|
1482
|
+
}
|
|
1483
|
+
.wl-row {
|
|
1484
|
+
display: flex; align-items: baseline; gap: 7px;
|
|
1485
|
+
background: var(--panel); border: 1px solid var(--border);
|
|
1486
|
+
border-radius: 8px; padding: 5px 8px;
|
|
1487
|
+
}
|
|
1488
|
+
.wl-row:hover { background: var(--panel2); }
|
|
1489
|
+
.wl-time { flex: none; color: var(--muted); font-size: 10px; min-width: 56px; }
|
|
1490
|
+
.wl-badge {
|
|
1491
|
+
flex: none; font-size: 9px; font-weight: 800; letter-spacing: 0.5px;
|
|
1492
|
+
padding: 1px 6px; border-radius: 5px; border: 1px solid var(--border);
|
|
1493
|
+
text-transform: uppercase;
|
|
1494
|
+
}
|
|
1495
|
+
.wl-badge.ok { color: var(--muted); }
|
|
1496
|
+
.wl-badge.trigger { color: var(--accent); border-color: var(--accent); }
|
|
1497
|
+
.wl-badge.error { color: var(--err); border-color: var(--err); }
|
|
1498
|
+
.wl-main { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11.5px; }
|
|
1499
|
+
.wl-err { color: var(--err); font-size: 11px; }
|
|
1500
|
+
|
|
1467
1501
|
/* #26 ajan dosya kartı */
|
|
1468
1502
|
.file-card {
|
|
1469
1503
|
display: flex;
|
|
@@ -1916,6 +1950,7 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
|
|
|
1916
1950
|
flex: none;
|
|
1917
1951
|
width: 250px;
|
|
1918
1952
|
min-width: 250px;
|
|
1953
|
+
min-height: 0;
|
|
1919
1954
|
background: var(--panel);
|
|
1920
1955
|
border-left: 1px solid var(--border);
|
|
1921
1956
|
display: flex;
|
|
@@ -1957,9 +1992,19 @@ body.browser-open #settingsDialog { width: min(70vw, calc(100vw - var(--bw, 480p
|
|
|
1957
1992
|
}
|
|
1958
1993
|
#railClear:hover { background: rgba(0, 0, 0, 0.08); }
|
|
1959
1994
|
#railClear svg { display: block; }
|
|
1960
|
-
}
|
|
1961
1995
|
|
|
1962
|
-
|
|
1996
|
+
/* rail listesi: kartlar ekranı aştığında KAYDIRILABİLİR olmalı —
|
|
1997
|
+
flex:1 + min-height:0 + overflow-y:auto üçlüsü zorunlu. overscroll-behavior
|
|
1998
|
+
sayfa geri kaydırma animasyonunu da keser. */
|
|
1999
|
+
#railList {
|
|
2000
|
+
flex: 1 1 auto;
|
|
2001
|
+
min-height: 0;
|
|
2002
|
+
overflow-y: auto;
|
|
2003
|
+
overflow-x: hidden;
|
|
2004
|
+
overscroll-behavior: contain;
|
|
2005
|
+
scrollbar-gutter: stable;
|
|
2006
|
+
padding: 4px 8px;
|
|
2007
|
+
}
|
|
1963
2008
|
|
|
1964
2009
|
.rail-empty {
|
|
1965
2010
|
color: var(--muted);
|