beast-agent 0.15.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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/assets/app.ico +0 -0
- package/assets/tray.png +0 -0
- package/bin/beast-agent.js +23 -0
- package/config.example.yaml +26 -0
- package/eng.traineddata +0 -0
- package/package.json +101 -0
- package/scripts/swap-electron.js +40 -0
- package/src/agent/bus.js +384 -0
- package/src/agent/computeruse.js +200 -0
- package/src/agent/config.js +284 -0
- package/src/agent/engine.js +3118 -0
- package/src/agent/kb.js +123 -0
- package/src/agent/llm.js +219 -0
- package/src/agent/logger.js +90 -0
- package/src/agent/memory.js +383 -0
- package/src/agent/pdf.js +20 -0
- package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
- package/src/agent/scripts/news.py +113 -0
- package/src/agent/scripts/websearch.py +225 -0
- package/src/agent/skills.js +522 -0
- package/src/agent/tokens.js +39 -0
- package/src/agent/tools.js +911 -0
- package/src/agent/usage.js +125 -0
- package/src/agent/watchers.js +312 -0
- package/src/agent/watext.js +75 -0
- package/src/agent/whatsapp.js +494 -0
- package/src/cron.js +245 -0
- package/src/main.js +3622 -0
- package/src/preload.js +122 -0
- package/src/renderer/browserPreload.js +73 -0
- package/src/renderer/i18n.js +757 -0
- package/src/renderer/index.html +227 -0
- package/src/renderer/renderer.js +3250 -0
- package/src/renderer/style.css +1559 -0
- package/tests/approval.test.js +56 -0
- package/tests/bg-jobs.test.js +315 -0
- package/tests/bus.test.js +46 -0
- package/tests/computeruse-context.test.js +69 -0
- package/tests/cron.test.js +95 -0
- package/tests/engine.test.js +140 -0
- package/tests/eval.test.js +91 -0
- package/tests/fallout.test.js +188 -0
- package/tests/llm.test.js +56 -0
- package/tests/memory.test.js +33 -0
- package/tests/memoryloop.test.js +26 -0
- package/tests/notegen.test.js +63 -0
- package/tests/owner.test.js +32 -0
- package/tests/python.test.js +75 -0
- package/tests/scenarios.json +81 -0
- package/tests/sessioncode.test.js +50 -0
- package/tests/setup.js +9 -0
- package/tests/tokens.test.js +42 -0
- package/tests/tools.test.js +84 -0
- package/tests/usage.test.js +44 -0
- package/tests/v11.test.js +76 -0
- package/tests/watchers.test.js +206 -0
- package/tests/watext.test.js +53 -0
- package/tests/whatsapp.test.js +103 -0
- package/tests/wherewasi.test.js +40 -0
package/src/agent/kb.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast knowledge store (#3 derin öğrenme katmanı)
|
|
4
|
+
sqlite-vec yerine sıfır bağımlılık: JSON deposu + TF-IDF benzerlik.
|
|
5
|
+
Kayıtlar kaynak taşır → kb_search sonucu citation'lı döner.
|
|
6
|
+
|
|
7
|
+
Depo: %APPDATA%\beast\knowledge.json
|
|
8
|
+
{ items: [{ id, title, source, tags[], text, createdAt }] } */
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { beastRoot } = require('./memory');
|
|
13
|
+
|
|
14
|
+
function file() {
|
|
15
|
+
return path.join(beastRoot(), 'knowledge.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let items = null;
|
|
19
|
+
|
|
20
|
+
function load() {
|
|
21
|
+
if (items) return;
|
|
22
|
+
try {
|
|
23
|
+
const raw = JSON.parse(fs.readFileSync(file(), 'utf8'));
|
|
24
|
+
items = Array.isArray(raw.items) ? raw.items : [];
|
|
25
|
+
} catch {
|
|
26
|
+
items = [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function save() {
|
|
31
|
+
try {
|
|
32
|
+
fs.mkdirSync(beastRoot(), { recursive: true });
|
|
33
|
+
const tmp = file() + '.tmp';
|
|
34
|
+
fs.writeFileSync(tmp, JSON.stringify({ items }, null, 2));
|
|
35
|
+
fs.renameSync(tmp, file());
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const KB_CAP = 2000;
|
|
40
|
+
|
|
41
|
+
function uid() {
|
|
42
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* memory.js ile aynı Türkçe duyarsız tokenleştirme */
|
|
46
|
+
const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
|
|
47
|
+
function fold(s) {
|
|
48
|
+
return String(s || '').toLowerCase().replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
|
|
49
|
+
}
|
|
50
|
+
function tokenize(q) {
|
|
51
|
+
return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** add(title, text, {source, tags}) → { ok, id } */
|
|
55
|
+
function add(title, text, meta = {}) {
|
|
56
|
+
load();
|
|
57
|
+
const t = String(text || '').trim();
|
|
58
|
+
if (!t) return { ok: false, error: 'metin boş' };
|
|
59
|
+
if (items.length >= KB_CAP) items.splice(0, items.length - KB_CAP + 1); // en eskiyi düşür
|
|
60
|
+
const it = {
|
|
61
|
+
id: uid(),
|
|
62
|
+
title: String(title || '').trim().slice(0, 120) || t.slice(0, 60),
|
|
63
|
+
source: String(meta.source || '').trim().slice(0, 300) || 'agent',
|
|
64
|
+
tags: Array.isArray(meta.tags) ? meta.tags.map((x) => String(x).slice(0, 40)).slice(0, 8) : [],
|
|
65
|
+
text: t.slice(0, 20000),
|
|
66
|
+
createdAt: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
items.push(it);
|
|
69
|
+
save();
|
|
70
|
+
return { ok: true, id: it.id };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function get(id) {
|
|
74
|
+
load();
|
|
75
|
+
return items.find((x) => x.id === id) ? { ...items.find((x) => x.id === id) } : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function count() {
|
|
79
|
+
load();
|
|
80
|
+
return items.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** TF-IDF benzerlik araması; sonuçlar citation alanıyla döner */
|
|
84
|
+
function search(query, limit = 5) {
|
|
85
|
+
load();
|
|
86
|
+
const qTokens = tokenize(query);
|
|
87
|
+
if (!qTokens.length || !items.length) return [];
|
|
88
|
+
const N = items.length;
|
|
89
|
+
const docs = items.map((it) => tokenize(it.title + ' ' + it.text));
|
|
90
|
+
/* df */
|
|
91
|
+
const df = new Map();
|
|
92
|
+
for (const toks of docs) {
|
|
93
|
+
for (const t of new Set(toks)) df.set(t, (df.get(t) || 0) + 1);
|
|
94
|
+
}
|
|
95
|
+
const scored = [];
|
|
96
|
+
for (let i = 0; i < N; i++) {
|
|
97
|
+
const toks = docs[i];
|
|
98
|
+
if (!toks.length) continue;
|
|
99
|
+
const tf = new Map();
|
|
100
|
+
for (const t of toks) tf.set(t, (tf.get(t) || 0) + 1);
|
|
101
|
+
let score = 0;
|
|
102
|
+
for (const q of qTokens) {
|
|
103
|
+
const f = tf.get(q) || 0;
|
|
104
|
+
if (!f) continue;
|
|
105
|
+
const idf = Math.log(1 + N / (1 + (df.get(q) || 0)));
|
|
106
|
+
score += (f / toks.length) * idf;
|
|
107
|
+
}
|
|
108
|
+
/* tag/başlık eşleşmesine bonus */
|
|
109
|
+
const hayT = fold(items[i].title + ' ' + (items[i].tags || []).join(' '));
|
|
110
|
+
for (const q of qTokens) if (hayT.includes(q)) score += 0.35;
|
|
111
|
+
if (score > 0) scored.push({ i, score });
|
|
112
|
+
}
|
|
113
|
+
scored.sort((a, b) => b.score - a.score);
|
|
114
|
+
return scored.slice(0, Math.max(1, Math.min(limit, 10))).map(({ i, score }) => ({
|
|
115
|
+
id: items[i].id,
|
|
116
|
+
title: items[i].title,
|
|
117
|
+
snippet: items[i].text.slice(0, 400),
|
|
118
|
+
score: Number(score.toFixed(3)),
|
|
119
|
+
citation: `[${items[i].title}] (${items[i].source}, ${String(items[i].createdAt).slice(0, 10)})`,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { add, get, search, count, tokenize };
|
package/src/agent/llm.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* OpenAI-compatible streaming chat client with SSE parsing.
|
|
4
|
+
Geçici sağlayıcı hatalarında (5xx/429/ağ) üstel beklemeyle otomatik retry. */
|
|
5
|
+
|
|
6
|
+
const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
|
|
7
|
+
const MAX_RETRIES = 3;
|
|
8
|
+
|
|
9
|
+
const HINTS = {
|
|
10
|
+
401: 'API anahtarı geçersiz',
|
|
11
|
+
402: 'kredi/bakiye bitti',
|
|
12
|
+
403: 'erişim reddedildi (anahtar yetkisi)',
|
|
13
|
+
404: 'model adı ya da endpoint yanlış',
|
|
14
|
+
429: 'hız limitine takıldın',
|
|
15
|
+
500: 'sağlayıcı iç hatası',
|
|
16
|
+
502: 'sağlayıcı upstream bağlantısı koptu',
|
|
17
|
+
503: 'sağlayıcı şu an kullanılamıyor (aşırı yük veya kapalı)',
|
|
18
|
+
504: 'sağlayıcı zaman aşımı',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function friendlyError(status, statusText, detail) {
|
|
22
|
+
const hint = HINTS[status];
|
|
23
|
+
let msg = `HTTP ${status} ${statusText}`;
|
|
24
|
+
if (hint) msg += ` — ${hint}`;
|
|
25
|
+
if (detail) msg += `\n${detail}`;
|
|
26
|
+
if ([500, 502, 503, 504].includes(status)) {
|
|
27
|
+
msg += '\n(geçici olabilir: birkaç saniye sonra tekrar dene ya da model seçiciyi açıp başka bir model dene)';
|
|
28
|
+
}
|
|
29
|
+
return msg;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sleep(ms, signal) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const t = setTimeout(resolve, ms);
|
|
35
|
+
if (signal) {
|
|
36
|
+
signal.addEventListener(
|
|
37
|
+
'abort',
|
|
38
|
+
() => {
|
|
39
|
+
clearTimeout(t);
|
|
40
|
+
const e = new Error('iptal');
|
|
41
|
+
e.name = 'AbortError';
|
|
42
|
+
reject(e);
|
|
43
|
+
},
|
|
44
|
+
{ once: true }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* fn: denenecek istek. Ağ hataları ve RETRYABLE_STATUS durumlarında
|
|
51
|
+
0.8s → 1.6s → 3.2s bekleyip tekrar dener. */
|
|
52
|
+
async function withRetries(fn, { signal, onRetry } = {}) {
|
|
53
|
+
let attempt = 0;
|
|
54
|
+
for (;;) {
|
|
55
|
+
try {
|
|
56
|
+
return await fn();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
const aborted = e && (e.name === 'AbortError' || (signal && signal.aborted));
|
|
59
|
+
const status = e && e.status;
|
|
60
|
+
const retriable =
|
|
61
|
+
!aborted && (status === undefined || RETRYABLE_STATUS.has(status));
|
|
62
|
+
if (!retriable || attempt >= MAX_RETRIES) throw e;
|
|
63
|
+
attempt++;
|
|
64
|
+
try {
|
|
65
|
+
onRetry && onRetry(attempt, status);
|
|
66
|
+
} catch {}
|
|
67
|
+
await sleep(800 * Math.pow(2, attempt - 1), signal);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function openChat(sel, body, { stream, signal, omitReasoning } = {}) {
|
|
73
|
+
const payload = {
|
|
74
|
+
model: sel.model,
|
|
75
|
+
messages: body.messages,
|
|
76
|
+
stream,
|
|
77
|
+
...(body.tools && body.tools.length ? { tools: body.tools } : {}),
|
|
78
|
+
temperature: body.temperature ?? 0.6,
|
|
79
|
+
};
|
|
80
|
+
/* Düşünme (reasoning) seviyesi: OpenAI-style reasoning_effort —
|
|
81
|
+
OpenRouter ve GPT-5 ailesi dahil uyumlu sağlayıcılar kabul eder. */
|
|
82
|
+
if (body.reasoningEffort && !omitReasoning) {
|
|
83
|
+
payload.reasoning_effort = String(body.reasoningEffort);
|
|
84
|
+
}
|
|
85
|
+
return fetch(sel.url, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
signal,
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
Authorization: `Bearer ${sel.key}`,
|
|
91
|
+
'HTTP-Referer': 'https://localhost/beast',
|
|
92
|
+
'X-Title': 'Beast Agent',
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify(payload),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function chatStream(sel, body, { signal, onDelta, onRetry } = {}) {
|
|
99
|
+
try {
|
|
100
|
+
return await streamOnce(sel, body, { signal, onDelta });
|
|
101
|
+
} catch (e) {
|
|
102
|
+
/* Model reasoning_effort'u desteklemiyorsa (400) parametreyi atlayıp
|
|
103
|
+
TEK seferlik sessiz retry — kullanıcı hatayı görmez. */
|
|
104
|
+
if (
|
|
105
|
+
body.reasoningEffort &&
|
|
106
|
+
e && e.status === 400 &&
|
|
107
|
+
/reason|effort|thinking/i.test(String(e.message || ''))
|
|
108
|
+
) {
|
|
109
|
+
return await streamOnce(sel, body, { signal, onDelta }, true);
|
|
110
|
+
}
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function streamOnce(sel, body, { signal, onDelta, onRetry } = {}, omitReasoning = false) {
|
|
116
|
+
const res = await withRetries(
|
|
117
|
+
async () => {
|
|
118
|
+
const r = await openChat(sel, body, { stream: true, signal, omitReasoning });
|
|
119
|
+
if (!r.ok) {
|
|
120
|
+
let detail = '';
|
|
121
|
+
try {
|
|
122
|
+
detail = (await r.text()).slice(0, 300);
|
|
123
|
+
} catch {}
|
|
124
|
+
const err = new Error(friendlyError(r.status, r.statusText, detail));
|
|
125
|
+
err.status = r.status;
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
return r;
|
|
129
|
+
},
|
|
130
|
+
{ signal, onRetry }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
let content = '';
|
|
134
|
+
let reasoning = '';
|
|
135
|
+
const toolCalls = [];
|
|
136
|
+
let usage = null;
|
|
137
|
+
let finishReason = null;
|
|
138
|
+
|
|
139
|
+
const reader = res.body.getReader();
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
let buf = '';
|
|
142
|
+
|
|
143
|
+
while (true) {
|
|
144
|
+
const { done, value } = await reader.read();
|
|
145
|
+
if (done) break;
|
|
146
|
+
buf += decoder.decode(value, { stream: true });
|
|
147
|
+
|
|
148
|
+
let idx;
|
|
149
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
150
|
+
const line = buf.slice(0, idx).trim();
|
|
151
|
+
buf = buf.slice(idx + 1);
|
|
152
|
+
if (!line.startsWith('data:')) continue;
|
|
153
|
+
const data = line.slice(5).trim();
|
|
154
|
+
if (!data || data === '[DONE]') continue;
|
|
155
|
+
|
|
156
|
+
let json;
|
|
157
|
+
try {
|
|
158
|
+
json = JSON.parse(data);
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (json.usage) usage = json.usage;
|
|
164
|
+
const ch = json.choices && json.choices[0];
|
|
165
|
+
if (!ch) continue;
|
|
166
|
+
if (ch.finish_reason) finishReason = ch.finish_reason;
|
|
167
|
+
const d = ch.delta || {};
|
|
168
|
+
if (d.content) {
|
|
169
|
+
content += d.content;
|
|
170
|
+
onDelta && onDelta(d.content, content);
|
|
171
|
+
}
|
|
172
|
+
if (d.reasoning_content) reasoning += d.reasoning_content;
|
|
173
|
+
if (d.reasoning) reasoning += d.reasoning;
|
|
174
|
+
for (const tc of d.tool_calls || []) {
|
|
175
|
+
const i = typeof tc.index === 'number' ? tc.index : toolCalls.length;
|
|
176
|
+
while (toolCalls.length <= i) {
|
|
177
|
+
toolCalls.push({ id: '', type: 'function', function: { name: '', arguments: '' } });
|
|
178
|
+
}
|
|
179
|
+
if (tc.id) toolCalls[i].id = tc.id;
|
|
180
|
+
if (tc.function) {
|
|
181
|
+
if (tc.function.name) toolCalls[i].function.name += tc.function.name;
|
|
182
|
+
if (tc.function.arguments) toolCalls[i].function.arguments += tc.function.arguments;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { content, reasoning, toolCalls, usage, finishReason };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function chatOnce(sel, body, { signal, onRetry } = {}) {
|
|
192
|
+
const res = await withRetries(
|
|
193
|
+
async () => {
|
|
194
|
+
const r = await openChat(sel, body, { stream: false, signal });
|
|
195
|
+
if (!r.ok) {
|
|
196
|
+
let detail = '';
|
|
197
|
+
try {
|
|
198
|
+
detail = (await r.text()).slice(0, 300);
|
|
199
|
+
} catch {}
|
|
200
|
+
const err = new Error(friendlyError(r.status, r.statusText, detail));
|
|
201
|
+
err.status = r.status;
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
return r;
|
|
205
|
+
},
|
|
206
|
+
{ signal, onRetry }
|
|
207
|
+
);
|
|
208
|
+
const json = await res.json();
|
|
209
|
+
const msg = json.choices?.[0]?.message || {};
|
|
210
|
+
return {
|
|
211
|
+
content: msg.content || '',
|
|
212
|
+
reasoning: msg.reasoning_content || msg.reasoning || '',
|
|
213
|
+
toolCalls: msg.tool_calls || [],
|
|
214
|
+
usage: json.usage || null,
|
|
215
|
+
finishReason: json.choices?.[0]?.finish_reason,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { chatStream, chatOnce, withRetries, friendlyError };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Merkezî log sistemi:
|
|
4
|
+
- Günlük dosyalar: %APPDATA%\beast\logs\beast-YYYY-MM-DD.log
|
|
5
|
+
- Bellekte ring buffer (UI'da hızlı gösterim için)
|
|
6
|
+
- 14 günden eski günlük dosyaları saatlik temizlikle silinir
|
|
7
|
+
Kullanım: const log = require('./logger'); log.info('wa', 'bağlandı'); */
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
const ROOT = process.env.APPDATA
|
|
14
|
+
? path.join(process.env.APPDATA, 'beast')
|
|
15
|
+
: path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
|
|
16
|
+
const LOG_DIR = path.join(ROOT, 'logs');
|
|
17
|
+
const KEEP_DAYS = 14;
|
|
18
|
+
const RING_MAX = 1000;
|
|
19
|
+
|
|
20
|
+
const ring = [];
|
|
21
|
+
let lastCleanup = 0;
|
|
22
|
+
|
|
23
|
+
function fileFor(d) {
|
|
24
|
+
const t = d || new Date();
|
|
25
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
26
|
+
return path.join(LOG_DIR, `beast-${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())}.log`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cleanup() {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (now - lastCleanup < 60 * 60 * 1000) return;
|
|
32
|
+
lastCleanup = now;
|
|
33
|
+
try {
|
|
34
|
+
for (const f of fs.readdirSync(LOG_DIR)) {
|
|
35
|
+
const m = /^beast-(\d{4})-(\d{2})-(\d{2})\.log$/.exec(f);
|
|
36
|
+
if (!m) continue;
|
|
37
|
+
const age = (now - Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))) / 86400000;
|
|
38
|
+
if (age > KEEP_DAYS) { try { fs.unlinkSync(path.join(LOG_DIR, f)); } catch {} }
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function write(level, tag, msg) {
|
|
44
|
+
const line =
|
|
45
|
+
`[${new Date().toISOString()}] [${String(level).toUpperCase()}] [${tag}] ` +
|
|
46
|
+
String(msg == null ? '' : msg).replace(/\r?\n/g, ' | ').slice(0, 4000);
|
|
47
|
+
ring.push(line);
|
|
48
|
+
if (ring.length > RING_MAX) ring.splice(0, ring.length - RING_MAX);
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
51
|
+
cleanup();
|
|
52
|
+
fs.appendFileSync(fileFor(), line + '\n');
|
|
53
|
+
} catch {}
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const info = (tag, msg) => write('info', tag, msg);
|
|
58
|
+
const warn = (tag, msg) => write('warn', tag, msg);
|
|
59
|
+
const error = (tag, msg) => write('error', tag, msg);
|
|
60
|
+
|
|
61
|
+
/* Bugün + dünün son n satırı (UI ve hata raporları için) */
|
|
62
|
+
function tail(n = 300) {
|
|
63
|
+
const lines = [];
|
|
64
|
+
try { lines.push(...fs.readFileSync(fileFor(new Date(Date.now() - 86400000)), 'utf8').split('\n')); } catch {}
|
|
65
|
+
try { lines.push(...fs.readFileSync(fileFor(), 'utf8').split('\n')); } catch {}
|
|
66
|
+
return lines.filter((l) => l.trim()).slice(-Math.max(1, Number(n) || 300));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function dir() {
|
|
70
|
+
try { fs.mkdirSync(LOG_DIR, { recursive: true }); } catch {}
|
|
71
|
+
return LOG_DIR;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function recent() {
|
|
75
|
+
return ring.slice(-200);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function clear() {
|
|
79
|
+
try { ring.length = 0; } catch {}
|
|
80
|
+
/* tail() bugün + dünün dosyasını okur; ikisini de boşalt ki
|
|
81
|
+
UI'daki "Logları Temizle" gerçekten temizlesin */
|
|
82
|
+
try {
|
|
83
|
+
for (const d of [new Date(), new Date(Date.now() - 86400000)]) {
|
|
84
|
+
try { fs.writeFileSync(fileFor(d), ''); } catch {}
|
|
85
|
+
}
|
|
86
|
+
} catch {}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { info, warn, error, tail, dir, recent, clear, LOG_DIR };
|