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/main.js
ADDED
|
@@ -0,0 +1,3622 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu, nativeImage, desktopCapturer, session } = require('electron');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const Engine = require('./agent/engine');
|
|
10
|
+
const { loadBeastConfig } = require('./agent/config');
|
|
11
|
+
const memory = require('./agent/memory');
|
|
12
|
+
const skillsMod = require('./agent/skills');
|
|
13
|
+
const { WhatsAppBridge } = require('./agent/whatsapp');
|
|
14
|
+
const cron = require('./cron');
|
|
15
|
+
const watchers = require('./agent/watchers');
|
|
16
|
+
const usageMod = require('./agent/usage');
|
|
17
|
+
const bus = require('./agent/bus');
|
|
18
|
+
const computeruse = require('./agent/computeruse');
|
|
19
|
+
const log = require('./agent/logger');
|
|
20
|
+
|
|
21
|
+
/* #3 otomatik updater: sessiz — indirir, kapanışta kurar, kullanıcıya soru sormaz.
|
|
22
|
+
Paketlenmemiş (npm start) modda devre dışı. */
|
|
23
|
+
let autoUpdater = null;
|
|
24
|
+
try { if (app.isPackaged) autoUpdater = require('electron-updater').autoUpdater; } catch {}
|
|
25
|
+
|
|
26
|
+
function startAutoUpdater() {
|
|
27
|
+
if (!autoUpdater || !app.isPackaged) return;
|
|
28
|
+
try {
|
|
29
|
+
autoUpdater.autoDownload = true; // sessiz indir
|
|
30
|
+
autoUpdater.autoInstallOnAppQuit = true; // kapanışta sessiz kur
|
|
31
|
+
autoUpdater.logger = {
|
|
32
|
+
info: (m) => waLog('[UPD] ' + m),
|
|
33
|
+
warn: (m) => waLog('[UPD] ' + m),
|
|
34
|
+
error: (m) => waLog('[UPD] ' + m),
|
|
35
|
+
debug: () => {},
|
|
36
|
+
};
|
|
37
|
+
autoUpdater.on('update-downloaded', () => {
|
|
38
|
+
try { fs.appendFileSync(path.join(APP_DIR, 'wa.log'), `[${new Date().toISOString()}] [UPD] güncelleme indirildi — kapanışta kurulacak\n`); } catch {}
|
|
39
|
+
});
|
|
40
|
+
const check = () => autoUpdater.checkForUpdates().catch(() => {});
|
|
41
|
+
check();
|
|
42
|
+
setInterval(check, 6 * 60 * 60 * 1000); // 6 saatte bir sessiz kontrol
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
const { htmlToText, setExaKey, setTinyfishKey } = require('./agent/tools');
|
|
46
|
+
const { waToolLine } = require('./agent/watext');
|
|
47
|
+
|
|
48
|
+
/* #3 merkezî log sistemine process-seviye hataları da düşsün */
|
|
49
|
+
process.on('uncaughtException', (e) => { try { log.error('main', 'uncaughtException: ' + ((e && e.stack) || e)); } catch {} });
|
|
50
|
+
process.on('unhandledRejection', (e) => { try { log.error('main', 'unhandledRejection: ' + ((e && e.stack) || e)); } catch {} });
|
|
51
|
+
|
|
52
|
+
/* #1 Splash /health — her zaman açık uç nokta: uygulamanın ayakta olduğunu bildirir */
|
|
53
|
+
function startHealthServer() {
|
|
54
|
+
const port = Number(settings.healthPort) || 8788;
|
|
55
|
+
const server = http.createServer((req, res) => {
|
|
56
|
+
if (req.url === '/health' || req.url.startsWith('/health')) {
|
|
57
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
58
|
+
res.end(JSON.stringify({ status: 'ok', uptime: Math.floor(process.uptime()), version: app.getVersion() }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(404).end();
|
|
62
|
+
});
|
|
63
|
+
server.on('error', (e) => log.error('health', 'başlatılamadı: ' + ((e && e.message) || e)));
|
|
64
|
+
server.listen(port, '127.0.0.1', () => log.info('health', `health endpoint hazır: http://127.0.0.1:${port}/health`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* WA tool-ping throttle: sid -> son bildirim zamanı */
|
|
68
|
+
const lastWaToolPing = new Map();
|
|
69
|
+
|
|
70
|
+
/* ilk QR eşlemesi sonrası otomatik restart bekleyeni (#v13) */
|
|
71
|
+
let waAwaitingRestart = false;
|
|
72
|
+
|
|
73
|
+
let ImapFlow = null;
|
|
74
|
+
let nodemailer = null;
|
|
75
|
+
try { ImapFlow = require('imapflow'); } catch {}
|
|
76
|
+
try { nodemailer = require('nodemailer'); } catch {}
|
|
77
|
+
|
|
78
|
+
const APP_DIR = path.join(app.getPath('appData'), 'beast');
|
|
79
|
+
const SESSIONS_DIR = path.join(APP_DIR, 'sessions');
|
|
80
|
+
const SETTINGS_FILE = path.join(APP_DIR, 'settings.json');
|
|
81
|
+
const SETTINGS_BACKUP_FILE = path.join(APP_DIR, 'settings.backup.json');
|
|
82
|
+
const WA_AUTH_DIR = path.join(APP_DIR, 'wa-auth');
|
|
83
|
+
const WA_CHATS_FILE = path.join(APP_DIR, 'wa-chats.json');
|
|
84
|
+
const FALLOUT_CRASH_FILE = path.join(APP_DIR, 'fallout-crash.json');
|
|
85
|
+
|
|
86
|
+
for (const d of [APP_DIR, SESSIONS_DIR]) fs.mkdirSync(d, { recursive: true });
|
|
87
|
+
|
|
88
|
+
let win = null;
|
|
89
|
+
let engine = null;
|
|
90
|
+
let settings = loadSettings();
|
|
91
|
+
ensureBeastCode();
|
|
92
|
+
startHealthServer(); /* splash/boot aşamasından itibaren /health ayakta */
|
|
93
|
+
try { setExaKey(settings.exaKey || null); } catch {}
|
|
94
|
+
try { setTinyfishKey(settings.tinyfishKey || null); } catch {}
|
|
95
|
+
let wa = null;
|
|
96
|
+
let waChats = new Map(); // jid -> aktif session id
|
|
97
|
+
let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
|
|
98
|
+
let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
|
|
99
|
+
const WA_HISTORY_CAP = 20;
|
|
100
|
+
let tray = null;
|
|
101
|
+
app.isQuitting = false;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const raw = JSON.parse(fs.readFileSync(WA_CHATS_FILE, 'utf8'));
|
|
105
|
+
/* yeni format {chats, history}; eski düz dizi de kabul edilir */
|
|
106
|
+
if (Array.isArray(raw)) {
|
|
107
|
+
for (const [j, s] of raw) {
|
|
108
|
+
if (typeof j === 'string' && j.includes('@') && typeof s === 'string') {
|
|
109
|
+
waChats.set(j, s);
|
|
110
|
+
waHistory.set(j, [s]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else if (raw && typeof raw === 'object') {
|
|
114
|
+
if (raw.chats && typeof raw.chats === 'object') {
|
|
115
|
+
for (const [j, s] of Object.entries(raw.chats)) {
|
|
116
|
+
if (typeof s === 'string') waChats.set(j, s);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (raw.history && typeof raw.history === 'object') {
|
|
120
|
+
for (const [j, arr] of Object.entries(raw.history)) {
|
|
121
|
+
if (Array.isArray(arr)) {
|
|
122
|
+
waHistory.set(
|
|
123
|
+
j,
|
|
124
|
+
arr.filter((x) => typeof x === 'string').slice(-WA_HISTORY_CAP)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/* history'siz kalan chatler için geçmişe ekle */
|
|
130
|
+
for (const [j, s] of waChats.entries()) {
|
|
131
|
+
const h = waHistory.get(j) || [];
|
|
132
|
+
if (!h.includes(s)) h.push(s);
|
|
133
|
+
waHistory.set(j, h.slice(-WA_HISTORY_CAP));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
function saveWaChats() {
|
|
139
|
+
try {
|
|
140
|
+
fs.writeFileSync(
|
|
141
|
+
WA_CHATS_FILE,
|
|
142
|
+
JSON.stringify({
|
|
143
|
+
chats: Object.fromEntries(waChats),
|
|
144
|
+
history: Object.fromEntries([...waHistory.entries()].map(([j, a]) => [j, a.slice(-WA_HISTORY_CAP)])),
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* jid'in oturum geçmişine sid ekler (tekilleştirilmiş, kırpılmış) */
|
|
151
|
+
function waRememberSession(jid, sid) {
|
|
152
|
+
const h = waHistory.get(jid) || [];
|
|
153
|
+
if (!h.includes(sid)) h.push(sid);
|
|
154
|
+
waHistory.set(jid, h.slice(-WA_HISTORY_CAP));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function settingsLog(line) {
|
|
158
|
+
try {
|
|
159
|
+
fs.appendFileSync(
|
|
160
|
+
path.join(APP_DIR, 'settings-recovery.log'),
|
|
161
|
+
`[${new Date().toISOString()}] ${line}\n`
|
|
162
|
+
);
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseSettingsText(text) {
|
|
167
|
+
const parsed = JSON.parse(text);
|
|
168
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('bozuk yapı');
|
|
169
|
+
if (!parsed.roleModels) parsed.roleModels = {};
|
|
170
|
+
return parsed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Öncelik: canlı dosya → yedek. Bozulma olursa log'a yaz, veri kaybı yaşanmaz. */
|
|
174
|
+
function loadSettings() {
|
|
175
|
+
try {
|
|
176
|
+
const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
|
177
|
+
const out = parseSettingsText(raw);
|
|
178
|
+
try { fs.copyFileSync(SETTINGS_FILE, SETTINGS_BACKUP_FILE); } catch {}
|
|
179
|
+
return out;
|
|
180
|
+
} catch (e) {
|
|
181
|
+
if (e && e.code !== 'ENOENT') {
|
|
182
|
+
settingsLog(`settings.json okunamadı (${String((e && e.message) || e)}) — yedek deneniyor`);
|
|
183
|
+
try { fs.copyFileSync(SETTINGS_FILE, SETTINGS_FILE + '.corrupt'); } catch {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const out = parseSettingsText(fs.readFileSync(SETTINGS_BACKUP_FILE, 'utf8'));
|
|
188
|
+
settingsLog('settings.json yedekten kurtarıldı');
|
|
189
|
+
return out;
|
|
190
|
+
} catch (e2) {
|
|
191
|
+
if (e2 && e2.code !== 'ENOENT') settingsLog(`yedek da bozuk: ${String((e2 && e2.message) || e2)}`);
|
|
192
|
+
}
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* #her makineye özgü Beast Kodu (IMEI benzeri, 15 haneli sayı).
|
|
197
|
+
İlk açılışta bir kez üretilir ve settings.json'a yazılır — sonra değişmez. */
|
|
198
|
+
function genBeastCode() {
|
|
199
|
+
const b = crypto.randomBytes(7);
|
|
200
|
+
let base = '';
|
|
201
|
+
for (let i = 0; i < 14; i++) base += String(b[i] % 10);
|
|
202
|
+
let sum = 0;
|
|
203
|
+
for (let i = 0; i < 14; i++) {
|
|
204
|
+
let d = base.charCodeAt(13 - i) - 48;
|
|
205
|
+
if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
|
|
206
|
+
sum += d;
|
|
207
|
+
}
|
|
208
|
+
const check = (10 - (sum % 10)) % 10;
|
|
209
|
+
return base + String(check);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensureBeastCode() {
|
|
213
|
+
if (settings.beastCode && /^\d{15}$/.test(String(settings.beastCode))) return;
|
|
214
|
+
settings.beastCode = genBeastCode();
|
|
215
|
+
try { saveSettings(); } catch {}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function saveSettings() {
|
|
219
|
+
try {
|
|
220
|
+
/* Atomik yazım: yarım kalırsa mevcut dosya asla bozulmaz */
|
|
221
|
+
const tmp = SETTINGS_FILE + '.tmp';
|
|
222
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2));
|
|
223
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
224
|
+
try { fs.copyFileSync(SETTINGS_FILE, SETTINGS_BACKUP_FILE); } catch {}
|
|
225
|
+
}
|
|
226
|
+
fs.renameSync(tmp, SETTINGS_FILE);
|
|
227
|
+
/* ilk kayıtta da yedeği mutlaka oluştur */
|
|
228
|
+
try { if (!fs.existsSync(SETTINGS_BACKUP_FILE)) fs.copyFileSync(SETTINGS_FILE, SETTINGS_BACKUP_FILE); } catch {}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
settingsLog(`saveSettings hata: ${String((e && e.message) || e)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function waLog(line) {
|
|
235
|
+
try {
|
|
236
|
+
console.log(`[WA] ${line}`); // Terminal: tüm WA hareketleri buraya düşer
|
|
237
|
+
fs.appendFileSync(path.join(APP_DIR, 'wa.log'), `[${new Date().toISOString()}] ${line}\n`);
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function watcherLog(line) {
|
|
242
|
+
try {
|
|
243
|
+
console.log(`[WATCH] ${line}`);
|
|
244
|
+
fs.appendFileSync(path.join(APP_DIR, 'watchers.log'), `[${new Date().toISOString()}] ${line}\n`);
|
|
245
|
+
} catch {}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ---------- Olay Merkezi (#4) ---------- */
|
|
249
|
+
|
|
250
|
+
const WEBHOOK_TOKEN_FILE = () => path.join(APP_DIR, 'webhook-token.txt');
|
|
251
|
+
|
|
252
|
+
function webhookToken() {
|
|
253
|
+
try {
|
|
254
|
+
let t = fs.readFileSync(WEBHOOK_TOKEN_FILE(), 'utf8').trim();
|
|
255
|
+
if (!t) throw new Error('bos');
|
|
256
|
+
return t;
|
|
257
|
+
} catch {
|
|
258
|
+
const t = crypto.randomBytes(16).toString('hex');
|
|
259
|
+
try { fs.writeFileSync(WEBHOOK_TOKEN_FILE(), t); } catch {}
|
|
260
|
+
return t;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function startEventBus() {
|
|
265
|
+
const e = settings.eventBus || {};
|
|
266
|
+
if (!e.enabled) {
|
|
267
|
+
bus.stopAll();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
bus.start(
|
|
271
|
+
{
|
|
272
|
+
notify: (sub, text) => {
|
|
273
|
+
try {
|
|
274
|
+
waLog(`olay bildirimi → sid=${sub.sessionId} (${sub.type})`);
|
|
275
|
+
engine.send(sub.sessionId, { text });
|
|
276
|
+
} catch {}
|
|
277
|
+
},
|
|
278
|
+
log: (line) => {
|
|
279
|
+
console.log(line);
|
|
280
|
+
try { fs.appendFileSync(path.join(APP_DIR, 'bus.log'), `[${new Date().toISOString()}] ${line}\n`); } catch {}
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
mailIdle: !!e.mailIdle,
|
|
285
|
+
getCfg: () => settings.email || {},
|
|
286
|
+
fsWatch: !!e.fsWatch,
|
|
287
|
+
workspace: settings.workspace || app.getPath('home'),
|
|
288
|
+
webhookPort: e.webhookPort || 8787,
|
|
289
|
+
webhookToken: webhookToken(),
|
|
290
|
+
priceSymbol: e.priceSymbol || null,
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* WA tepki/presence köprüsü: whatsapp.js emit'lerinden bus'a */
|
|
296
|
+
function bridgeWaToBus(ev) {
|
|
297
|
+
if (ev && ev.type === 'presence' && ev.presence === 'composing') {
|
|
298
|
+
try {
|
|
299
|
+
bus.emitEvent('wa:presence', { status: 'yazıyor', sender: ev.participant || '' }, Date.now());
|
|
300
|
+
} catch {}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
function waPrettyJid(jid) {
|
|
306
|
+
const n = String(jid || '').split('@')[0].split(':')[0];
|
|
307
|
+
return /^\d+$/.test(n) ? '+' + n : String(jid || '?');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function waEntryDigits(e) {
|
|
311
|
+
return typeof e === 'string' ? e.replace(/\D/g, '') : String((e && e.num) || '').replace(/\D/g, '');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Allow listesi: [{num:'905...', name:'Quantum Algo'}, '*'] formatı. Eski string
|
|
315
|
+
kayıtlar da kabul görür. Eşlenen kaydı döndürür, yoksa null. */
|
|
316
|
+
function waFind(senderNum) {
|
|
317
|
+
const list = settings.waAllow || [];
|
|
318
|
+
if (!list.length) return null; // boş liste = kimseye cevap yok
|
|
319
|
+
const num = String(senderNum || '');
|
|
320
|
+
for (const e of list) {
|
|
321
|
+
if (e === '*') return { num: '*', name: '' };
|
|
322
|
+
const d = waEntryDigits(e);
|
|
323
|
+
if (!d) continue;
|
|
324
|
+
if (!num || !(num === d || num.endsWith(d))) continue;
|
|
325
|
+
return typeof e === 'string' ? { num: d, name: '' } : e;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isWaAllowed(senderNum) {
|
|
331
|
+
return !!waFind(senderNum);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Sahip: owner işaretli kayıt; yoksa listedeki ilk kişi. /allow ve /block
|
|
335
|
+
yalnızca sahip tarafından kullanılabilir (yabancı DM kendi kendini ekleyemesin). */
|
|
336
|
+
function waOwnerNum() {
|
|
337
|
+
const list = settings.waAllow || [];
|
|
338
|
+
for (const e of list) {
|
|
339
|
+
if (e !== '*' && typeof e === 'object' && e.owner) return waEntryDigits(e);
|
|
340
|
+
}
|
|
341
|
+
const first = list.find((e) => e !== '*');
|
|
342
|
+
return first ? waEntryDigits(first) : '';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isOwnerSender(senderNum) {
|
|
346
|
+
const own = waOwnerNum();
|
|
347
|
+
if (!own) return true; // liste boşsa kurulum modu
|
|
348
|
+
const num = String(senderNum || '');
|
|
349
|
+
return !!num && (num === own || num.endsWith(own));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ---------- WA ses: ÜCRETSİZ yerel Whisper STT + opsiyonel TTS ---------- */
|
|
353
|
+
|
|
354
|
+
let sttPipeline = null;
|
|
355
|
+
let sttLoading = null;
|
|
356
|
+
|
|
357
|
+
async function ensureStt() {
|
|
358
|
+
if (sttPipeline) return sttPipeline;
|
|
359
|
+
if (!sttLoading) {
|
|
360
|
+
sttLoading = (async () => {
|
|
361
|
+
waLog('STT: yerel whisper modeli hazırlanıyor (ilk kullanımda indirilir)');
|
|
362
|
+
const { pipeline, env } = require('@xenova/transformers');
|
|
363
|
+
const modelsDir = path.join(APP_DIR, 'models');
|
|
364
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
365
|
+
env.cacheDir = modelsDir;
|
|
366
|
+
env.allowLocalModels = false;
|
|
367
|
+
sttPipeline = await pipeline('automatic-speech-recognition', 'Xenova/whisper-tiny', { quantized: true });
|
|
368
|
+
waLog('STT hazır');
|
|
369
|
+
return sttPipeline;
|
|
370
|
+
})().catch((e) => {
|
|
371
|
+
sttLoading = null;
|
|
372
|
+
throw e;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return sttLoading;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ogg/opus/mp3 → mono 16kHz Float32 PCM (ffmpeg ile) */
|
|
379
|
+
function decodeAudioToPcm16k(buf) {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
try {
|
|
382
|
+
const ffmpegPath = require('ffmpeg-static');
|
|
383
|
+
if (!ffmpegPath) return reject(new Error('ffmpeg bulunamadı'));
|
|
384
|
+
const { spawn } = require('child_process');
|
|
385
|
+
const p = spawn(ffmpegPath, ['-hide_banner', '-loglevel', 'error', '-i', 'pipe:0', '-f', 's16le', '-ar', '16000', '-ac', '1', 'pipe:1']);
|
|
386
|
+
const chunks = [];
|
|
387
|
+
let err = '';
|
|
388
|
+
p.stdout.on('data', (c) => chunks.push(c));
|
|
389
|
+
p.stderr.on('data', (c) => { err += c.toString(); });
|
|
390
|
+
p.on('error', reject);
|
|
391
|
+
p.on('close', (code) => {
|
|
392
|
+
if (code !== 0) return reject(new Error('ffmpeg ' + code + ': ' + err.slice(0, 200)));
|
|
393
|
+
const pcm = Buffer.concat(chunks);
|
|
394
|
+
const f32 = new Float32Array(Math.floor(pcm.length / 2));
|
|
395
|
+
for (let i = 0; i < f32.length; i++) f32[i] = pcm.readInt16LE(i * 2) / 32768;
|
|
396
|
+
resolve(f32);
|
|
397
|
+
});
|
|
398
|
+
p.stdin.write(buf);
|
|
399
|
+
p.stdin.end();
|
|
400
|
+
} catch (e) {
|
|
401
|
+
reject(e);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function transcribeAudio(buf /* , mimetype */) {
|
|
407
|
+
try {
|
|
408
|
+
const audio = await decodeAudioToPcm16k(buf);
|
|
409
|
+
if (!audio || !audio.length) return null;
|
|
410
|
+
const asr = await ensureStt();
|
|
411
|
+
const out = await asr(audio, { language: 'turkish', task: 'transcribe', chunk_length_s: 30, stride_length_s: 5 });
|
|
412
|
+
return String((out && out.text) || '').trim() || null;
|
|
413
|
+
} catch (e) {
|
|
414
|
+
waLog('stt hata: ' + String((e && e.message) || e));
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function synthesizeSpeech(text) {
|
|
420
|
+
const cfg = settings.waTts || {};
|
|
421
|
+
if (!cfg.enabled || !cfg.baseUrl || !cfg.key) return null; try {
|
|
422
|
+
const url = String(cfg.baseUrl).replace(/\/+$/, '') + '/audio/speech';
|
|
423
|
+
const res = await fetch(url, {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + cfg.key },
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
model: cfg.model || 'tts-1',
|
|
428
|
+
input: String(text).slice(0, 4000),
|
|
429
|
+
voice: cfg.voice || 'alloy',
|
|
430
|
+
response_format: 'mp3',
|
|
431
|
+
}),
|
|
432
|
+
signal: AbortSignal.timeout(90000),
|
|
433
|
+
});
|
|
434
|
+
if (!res.ok) {
|
|
435
|
+
waLog(`tts http ${res.status}`);
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
return Buffer.from(await res.arrayBuffer());
|
|
439
|
+
} catch (e) {
|
|
440
|
+
waLog('tts hata: ' + String((e && e.message) || e));
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const TEXT_DOC_EXT = /\.(txt|md|markdown|csv|json|log|js|mjs|ts|py|ps1|bat|cmd|html|css|xml|yaml|yml|ini|cfg)$/i;
|
|
446
|
+
|
|
447
|
+
function emitWaEventSafe(ev) {
|
|
448
|
+
try {
|
|
449
|
+
if (win && !win.isDestroyed()) win.webContents.send('wa:event', ev);
|
|
450
|
+
} catch {}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
function waSlashHelp() {
|
|
456
|
+
return [
|
|
457
|
+
'*Beast komutları*',
|
|
458
|
+
'• /help – bu liste',
|
|
459
|
+
'• /new – yeni oturum aç (kod verilir)',
|
|
460
|
+
'• /open <kod> – o koddaki oturuma geç',
|
|
461
|
+
'• /sessions – bu sohbetin oturumları',
|
|
462
|
+
'• /stop – koşan işleri durdur (ajanlar+turlar; cron/izleyici/olay sürer)',
|
|
463
|
+
'• /start – durdurulan servisleri devam ettir',
|
|
464
|
+
'• /restart – uygulamayı yeniden başlat',
|
|
465
|
+
'• /change – modelleri listele (/change 5 ile 5.modele geç)',
|
|
466
|
+
'• /notes – bu oturumun notlarını göster',
|
|
467
|
+
'• /notify on|off – hata mail bildirimini aç/kapa',
|
|
468
|
+
'• /think <0-5> – düşünme seviyesi (0 kapalı · 1 low · 2 medium · 3 high · 4 xhigh · 5 max)',
|
|
469
|
+
'• /clear – bu oturumun geçmişini temizle',
|
|
470
|
+
'• /screenshot – masaüstü ekran görüntüsünü gönder',
|
|
471
|
+
'• /rule <metin> – kalıcı kural ekle (/rules: liste)',
|
|
472
|
+
'• /allow <isim> <numara> – WhatsApp allow listesine kişi ekle (örn: /allow batu 905414178456)',
|
|
473
|
+
'• /block – allow listesini numaralarıyla listele (/block 3: 3. kişiyi çıkar; 1 = sahip, silinemez)',
|
|
474
|
+
'• /approve – bekleyen riskli işlemi onayla (/approve always: bir daha sorulmasın · /deny: reddet)',
|
|
475
|
+
'• /model – aktif modeli göster (/model <isim> ile değiştir)',
|
|
476
|
+
'• /skills – kurulu skill\u2019ler',
|
|
477
|
+
'• /usage – bugünkü kullanım',
|
|
478
|
+
'• /backup – tüm veriyi ŞİFRELİ yedekle (Beast Kodu imzalı, Masaüstü\\Beast-Backups)',
|
|
479
|
+
'• /status – bağlantı ve servis durumu',
|
|
480
|
+
'',
|
|
481
|
+
'Gruplarda beni @mention ile çağır; ardışık mesajlarını tek cevapta birleştiririm.',
|
|
482
|
+
].join('\n');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function fmtNum(n) {
|
|
486
|
+
n = Number(n) || 0;
|
|
487
|
+
return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function escapeWa(t) {
|
|
491
|
+
return String(t || '').replace(/[*_`~\[\]]/g, '');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ---------- #6 tek tık yedek: %APPDATA%\beast → ŞİFRELİ .beastbak ----------
|
|
495
|
+
Format: [BEASTBAK1][beastCode:15][iv:16][AES-256-CBC(zip)]
|
|
496
|
+
Anahtar Beast Kodu'ndan (scrypt) türetilir; dosya adında kod görünür —
|
|
497
|
+
yedeğin hangi makineye ait olduğu belli olur, başka makinede geri yüklenemez. */
|
|
498
|
+
|
|
499
|
+
const BACKUP_MAGIC = 'BEASTBAK1';
|
|
500
|
+
|
|
501
|
+
function backupKey(beastCode) {
|
|
502
|
+
return crypto.scryptSync(String(beastCode), 'beast-backup-v1', 32, { N: 32768, maxmem: 128 * 1024 * 1024 });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function createBackup() {
|
|
506
|
+
try {
|
|
507
|
+
const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19);
|
|
508
|
+
const outDir = path.join(app.getPath('desktop'), 'Beast-Backups');
|
|
509
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
510
|
+
const code = String(settings.beastCode || '');
|
|
511
|
+
const zipPath = path.join(outDir, `beast-${stamp}.zip`);
|
|
512
|
+
/* zip yoksa PowerShell Compress-Archive ile */
|
|
513
|
+
await new Promise((resolve, reject) => {
|
|
514
|
+
const { execFile } = require('child_process');
|
|
515
|
+
execFile(
|
|
516
|
+
'powershell.exe',
|
|
517
|
+
[
|
|
518
|
+
'-NoProfile', '-Command',
|
|
519
|
+
`Compress-Archive -Path '${APP_DIR}\\*' -DestinationPath '${zipPath}' -Force`,
|
|
520
|
+
],
|
|
521
|
+
{ timeout: 180000, windowsHide: true },
|
|
522
|
+
(err) => (err ? reject(err) : resolve())
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
/* zip → AES-256-CBC ile şifrele, Beast Kodu'nu başlığa yaz */
|
|
526
|
+
const iv = crypto.randomBytes(16);
|
|
527
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', backupKey(code), iv);
|
|
528
|
+
const enc = Buffer.concat([cipher.update(fs.readFileSync(zipPath)), cipher.final()]);
|
|
529
|
+
fs.unlinkSync(zipPath);
|
|
530
|
+
const outPath = path.join(outDir, `beast-yedek-${code}-${stamp}.beastbak`);
|
|
531
|
+
const header = Buffer.concat([
|
|
532
|
+
Buffer.from(BACKUP_MAGIC, 'utf8'),
|
|
533
|
+
Buffer.from(code.padEnd(15, '0'), 'utf8'),
|
|
534
|
+
iv,
|
|
535
|
+
]);
|
|
536
|
+
fs.writeFileSync(outPath, Buffer.concat([header, enc]));
|
|
537
|
+
const size = fs.statSync(outPath).size;
|
|
538
|
+
waLog(`şifreli yedek alındı: ${outPath} (${Math.round(size / 1024)} KB) — Beast Kodu ${code}`);
|
|
539
|
+
return { ok: true, path: outPath, size, code };
|
|
540
|
+
} catch (e) {
|
|
541
|
+
waLog('yedek hata: ' + String((e && e.message) || e));
|
|
542
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* Şifreli yedeği geri yükle: Beast Kodu bu makineye aitse çöz → aç → APP_DIR'e kopyala */
|
|
547
|
+
async function restoreBackup() {
|
|
548
|
+
try {
|
|
549
|
+
const r = await dialog.showOpenDialog(win, {
|
|
550
|
+
title: 'Beast Yedeğini Geri Yükle',
|
|
551
|
+
filters: [{ name: 'Beast Yedek', extensions: ['beastbak'] }],
|
|
552
|
+
properties: ['openFile'],
|
|
553
|
+
});
|
|
554
|
+
if (r.canceled || !r.filePaths || !r.filePaths.length) return { ok: false, canceled: true };
|
|
555
|
+
const file = r.filePaths[0];
|
|
556
|
+
const raw = fs.readFileSync(file);
|
|
557
|
+
if (raw.length < 40 || raw.subarray(0, 9).toString('utf8') !== BACKUP_MAGIC) {
|
|
558
|
+
return { ok: false, error: 'bu dosya geçerli bir Beast yedeği değil' };
|
|
559
|
+
}
|
|
560
|
+
const code = raw.subarray(9, 24).toString('utf8').replace(/0+$/, '').trim();
|
|
561
|
+
const iv = raw.subarray(24, 40);
|
|
562
|
+
const data = raw.subarray(40);
|
|
563
|
+
if (!code || code !== String(settings.beastCode || '')) {
|
|
564
|
+
waLog(`geri yükleme reddedildi: yedek ${code || '?'} kodlu Beast'e ait, bu makine ${settings.beastCode}`);
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
foreign: true,
|
|
568
|
+
code: code || '?',
|
|
569
|
+
error: `bu yedek başka bir Beast'e ait (${code || '?'}) — bu makinenin kodu: ${settings.beastCode}`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
let zip;
|
|
573
|
+
try {
|
|
574
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', backupKey(code), iv);
|
|
575
|
+
zip = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
576
|
+
} catch {
|
|
577
|
+
return { ok: false, error: 'yedek çözülemedi — dosya bozuk olabilir' };
|
|
578
|
+
}
|
|
579
|
+
const tmp = path.join(app.getPath('temp'), `beast-restore-${Date.now()}`);
|
|
580
|
+
const tmpZip = tmp + '.zip';
|
|
581
|
+
fs.writeFileSync(tmpZip, zip);
|
|
582
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
583
|
+
await new Promise((resolve, reject) => {
|
|
584
|
+
const { execFile } = require('child_process');
|
|
585
|
+
execFile(
|
|
586
|
+
'powershell.exe',
|
|
587
|
+
['-NoProfile', '-Command', `Expand-Archive -Path '${tmpZip}' -DestinationPath '${tmp}' -Force`],
|
|
588
|
+
{ timeout: 180000, windowsHide: true },
|
|
589
|
+
(err) => (err ? reject(err) : resolve())
|
|
590
|
+
);
|
|
591
|
+
});
|
|
592
|
+
/* açılan içeriği %APPDATA%\beast üzerine kopyala */
|
|
593
|
+
const copyAll = (src, dest) => {
|
|
594
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
595
|
+
for (const e of fs.readdirSync(src, { withFileTypes: true })) {
|
|
596
|
+
const s = path.join(src, e.name);
|
|
597
|
+
const d = path.join(dest, e.name);
|
|
598
|
+
if (e.isDirectory()) copyAll(s, d);
|
|
599
|
+
else fs.copyFileSync(s, d);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
copyAll(tmp, APP_DIR);
|
|
603
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
604
|
+
try { fs.unlinkSync(tmpZip); } catch {}
|
|
605
|
+
waLog(`yedek geri yüklendi: ${file} — yeniden başlatma önerilir`);
|
|
606
|
+
return { ok: true, restart: true };
|
|
607
|
+
} catch (e) {
|
|
608
|
+
waLog('geri yükleme hata: ' + String((e && e.message) || e));
|
|
609
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* ---------- global DURDUR (#16): /stop her şeyi keser ---------- */
|
|
614
|
+
|
|
615
|
+
let servicesPaused = false;
|
|
616
|
+
|
|
617
|
+
function stopEverything() {
|
|
618
|
+
let n = 0;
|
|
619
|
+
try { n = engine.stopAll(); } catch {}
|
|
620
|
+
/* bekleyen masaüstü/WA birleştirme kuyrukları temizlenir */
|
|
621
|
+
try { for (const [, q] of desktopQueue) clearTimeout(q.timer); } catch {}
|
|
622
|
+
desktopQueue.clear();
|
|
623
|
+
try { for (const [, q] of waQueue) clearTimeout(q.timer); } catch {}
|
|
624
|
+
waQueue.clear();
|
|
625
|
+
/* cron, izleyici ve olay merkezi ÇALIŞMAYA DEVAM EDER — /stop yalnızca
|
|
626
|
+
koşan ajan/turları keser; servisleri durdurmak isteyen app'i kapatır. */
|
|
627
|
+
return n;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function resumeServices() {
|
|
631
|
+
if (!servicesPaused) return;
|
|
632
|
+
try { cron.init({ onFire: cronFire }); } catch {}
|
|
633
|
+
try { watchers.start({ onTrigger: watcherFire }); } catch {}
|
|
634
|
+
try { startEventBus(); } catch {}
|
|
635
|
+
servicesPaused = false;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* ---------- #Güvenlik: riskli araç onay kapısı (varsayılan KAPALI — her şey serbest) ----------
|
|
639
|
+
Ayarlar → Güvenlik'ten açılır. Açıksa run_command / write_file / python_run /
|
|
640
|
+
email_send / watcher_add çalışmadan önce onay bekler (UI kartı + WA mesajı).
|
|
641
|
+
Onay: /approve · bir daha sorulmasın: /approve always · Reddet: /deny */
|
|
642
|
+
const pendingApprovals = new Map(); // id -> { resolve, sessionId, tool, args, timer }
|
|
643
|
+
const approvalsBridge = { request: (r) => requestApproval(r) };
|
|
644
|
+
|
|
645
|
+
function approvalArgsSummary(args) {
|
|
646
|
+
try {
|
|
647
|
+
const s = JSON.stringify(args || {});
|
|
648
|
+
return s.length > 260 ? s.slice(0, 260) + '…' : s;
|
|
649
|
+
} catch {
|
|
650
|
+
return '{}';
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function requestApproval({ sessionId, tool, args }) {
|
|
655
|
+
return new Promise((resolve) => {
|
|
656
|
+
try {
|
|
657
|
+
const id = 'ap' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
658
|
+
const entry = { sessionId, tool, args, resolve, timer: null };
|
|
659
|
+
pendingApprovals.set(id, entry);
|
|
660
|
+
/* 3 dk içinde onay gelmezse otomatik reddet */
|
|
661
|
+
entry.timer = setTimeout(() => {
|
|
662
|
+
if (pendingApprovals.delete(id)) {
|
|
663
|
+
resolve(false);
|
|
664
|
+
log.info('sec', `onay zaman aşımı: ${tool}`);
|
|
665
|
+
}
|
|
666
|
+
}, 180000);
|
|
667
|
+
if (win && !win.isDestroyed()) {
|
|
668
|
+
win.webContents.send('agent:event', {
|
|
669
|
+
sessionId: String(sessionId || ''),
|
|
670
|
+
type: 'approval',
|
|
671
|
+
requestId: id,
|
|
672
|
+
tool,
|
|
673
|
+
argsPreview: approvalArgsSummary(args),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const own = waOwnerNum();
|
|
677
|
+
if (wa && own) {
|
|
678
|
+
sendWaSafe(
|
|
679
|
+
own + '@s.whatsapp.net',
|
|
680
|
+
`\u26A0\uFE0F *Onay bekleniyor* — \`${tool}\`\n${approvalArgsSummary(args).slice(0, 180)}\n\nOnayla: \`/approve\`\nBu araç için bir daha sorma: \`/approve always\`\nReddet: \`/deny\``
|
|
681
|
+
).catch(() => {});
|
|
682
|
+
}
|
|
683
|
+
log.info('sec', `onay istendi: ${tool} (${id})`);
|
|
684
|
+
} catch {
|
|
685
|
+
resolve(false);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function resolveApproval(id, ok, always) {
|
|
691
|
+
const key = String(id || '');
|
|
692
|
+
const entry = pendingApprovals.get(key);
|
|
693
|
+
if (!entry) return { ok: false, error: 'bekleyen onay yok' };
|
|
694
|
+
pendingApprovals.delete(key);
|
|
695
|
+
clearTimeout(entry.timer);
|
|
696
|
+
if (ok && always) {
|
|
697
|
+
const sec = settings.security || (settings.security = { approvals: false, alwaysAllow: [] });
|
|
698
|
+
if (!Array.isArray(sec.alwaysAllow)) sec.alwaysAllow = [];
|
|
699
|
+
if (!sec.alwaysAllow.includes(entry.tool)) sec.alwaysAllow.push(entry.tool);
|
|
700
|
+
saveSettings();
|
|
701
|
+
if (engine) engine.alwaysAllowTools = new Set(sec.alwaysAllow);
|
|
702
|
+
}
|
|
703
|
+
entry.resolve(!!ok);
|
|
704
|
+
log.info('sec', `onay ${ok ? 'VERİLDİ' : 'reddedildi'}: ${entry.tool}${always ? ' (always)' : ''}`);
|
|
705
|
+
return { ok: true, tool: entry.tool };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function resolveFirstApproval(ok, always) {
|
|
709
|
+
const first = pendingApprovals.keys().next();
|
|
710
|
+
if (first.done) return { ok: false, error: 'bekleyen onay yok' };
|
|
711
|
+
return resolveApproval(first.value, ok, always);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/* true dönerse mesaj slash komutuydu ve cevap gönderildi */
|
|
715
|
+
async function tryWaSlash(jid, rawText, senderNum) {
|
|
716
|
+
const t = String(rawText || '').trim();
|
|
717
|
+
if (!t.startsWith('/') || t.includes('\n')) return false;
|
|
718
|
+
const parts = t.slice(1).split(/\s+/);
|
|
719
|
+
const cmd = String(parts[0] || '').toLowerCase();
|
|
720
|
+
const arg = parts.slice(1).join(' ').trim();
|
|
721
|
+
let out = '';
|
|
722
|
+
try {
|
|
723
|
+
if (cmd === 'help') {
|
|
724
|
+
out = waSlashHelp();
|
|
725
|
+
} else if (cmd === 'new') {
|
|
726
|
+
const v = engine.createSession();
|
|
727
|
+
waChats.set(jid, v.id);
|
|
728
|
+
waRememberSession(jid, v.id);
|
|
729
|
+
saveWaChats();
|
|
730
|
+
wa.setWatchJids([...waChats.keys()]);
|
|
731
|
+
out = `*Yeni oturum* \`${v.code}\` açıldı — anlatabilirsin.\nDiğer oturumlara geçiş: \`/open <kod>\``;
|
|
732
|
+
} else if (cmd === 'open') {
|
|
733
|
+
if (!arg) {
|
|
734
|
+
out = 'Kullanım: `/open <kod>` — kodları görmek için /sessions';
|
|
735
|
+
} else {
|
|
736
|
+
const hit = engine.findByCode(arg);
|
|
737
|
+
if (!hit) {
|
|
738
|
+
out = `\`${arg.toUpperCase()}\` kodlu oturum bulunamadı. Listeyi görmek için /sessions`;
|
|
739
|
+
} else {
|
|
740
|
+
waChats.set(jid, hit.id);
|
|
741
|
+
waRememberSession(jid, hit.id);
|
|
742
|
+
saveWaChats();
|
|
743
|
+
wa.setWatchJids([...waChats.keys()]);
|
|
744
|
+
out = `*Geçildi:* \`${hit.code}\` — ${escapeWa(hit.title)}`;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} else if (cmd === 'sessions') {
|
|
748
|
+
const h = [...new Set([...(waHistory.get(jid) || []), ...(waChats.has(jid) ? [waChats.get(jid)] : [])])];
|
|
749
|
+
const activeSid = waChats.get(jid);
|
|
750
|
+
const rows = [];
|
|
751
|
+
for (const sid of h.slice(-8).reverse()) {
|
|
752
|
+
try {
|
|
753
|
+
const s = engine.openSession(sid);
|
|
754
|
+
rows.push(`${s.id === activeSid ? '\u2726' : '-'} \`${s.code || '?'}\` ${escapeWa(s.title).slice(0, 40)}${s.id === activeSid ? ' *(aktif)*' : ''}`);
|
|
755
|
+
} catch {}
|
|
756
|
+
}
|
|
757
|
+
out = rows.length
|
|
758
|
+
? '*Bu sohbetin oturumları*\n' + rows.join('\n') + '\n\nGeçmek: `/open <kod>` · Yeni: /new'
|
|
759
|
+
: 'Kayıtlı oturum yok.';
|
|
760
|
+
} else if (cmd === 'rule') {
|
|
761
|
+
if (!arg) {
|
|
762
|
+
out = 'Kullanım:\n`/rule <kural metni>` — genel kural\n`/rule <skill-adı> <madde>` — o skill\u2019e madde';
|
|
763
|
+
} else {
|
|
764
|
+
const m = arg.match(/^([\w.-]+)\s+(.+)$/);
|
|
765
|
+
const skTry = m ? skillsMod.appendRuleToSkill(m[1], m[2]) : { ok: false };
|
|
766
|
+
if (skTry.ok) {
|
|
767
|
+
out = `*Skill güncellendi:* ${skTry.skill}\n- ${m[2].slice(0, 120)}`;
|
|
768
|
+
} else {
|
|
769
|
+
const r = memory.addRule(arg);
|
|
770
|
+
out = r.ok
|
|
771
|
+
? (r.duplicate ? 'Bu kural zaten kayıtlı.' : '*Kural kalıcıya eklendi* 🧷\n' + arg.slice(0, 150))
|
|
772
|
+
: 'Hata: ' + (r.error || '?');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} else if (cmd === 'rules') {
|
|
776
|
+
const rules = memory.listRules();
|
|
777
|
+
out = rules.length
|
|
778
|
+
? '*Kalıcı kurallar*\n' + rules.map((r, i) => `${i + 1}. ${escapeWa(r)}`).join('\n')
|
|
779
|
+
: 'Henüz kural yok. Eklemek için: `/rule <metin>`';
|
|
780
|
+
} else if (cmd === 'allow') {
|
|
781
|
+
/* /allow <isim> <numara> — yalnız sahip ekleyebilir */
|
|
782
|
+
if (!isOwnerSender(senderNum)) {
|
|
783
|
+
out = 'Bu komutu yalnız *sahip* kullanabilir.';
|
|
784
|
+
} else {
|
|
785
|
+
const m = String(arg || '').trim().match(/^(.+?)\s+([\d+\s()-]{7,})$/);
|
|
786
|
+
if (!m) {
|
|
787
|
+
out = 'Kullanım: `/allow <isim> <numara>`\nÖrnek: `/allow batu 905414178456`';
|
|
788
|
+
} else {
|
|
789
|
+
const name = m[1].trim().slice(0, 40);
|
|
790
|
+
const num = m[2].replace(/\D/g, '');
|
|
791
|
+
if (num.length < 7) {
|
|
792
|
+
out = 'Numara çok kısa/geçersiz — örnek: `/allow batu 905414178456`';
|
|
793
|
+
} else {
|
|
794
|
+
const list = settings.waAllow || [];
|
|
795
|
+
if (list.some((e) => waEntryDigits(e) === num)) {
|
|
796
|
+
out = `*+${num}* zaten allow listesinde.`;
|
|
797
|
+
} else {
|
|
798
|
+
settings.waAllow = [...list, { num, name, perm: 'all' }];
|
|
799
|
+
saveSettings();
|
|
800
|
+
emitWaEventSafe({ type: 'allow' });
|
|
801
|
+
waLog(`slash /allow: +${num} (${name}) eklendi`);
|
|
802
|
+
out = `*Allow\u2019a eklendi:* ${name} (+${num})\nListe: \`/block\``;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
} else if (cmd === 'block') {
|
|
808
|
+
/* arg yok: listele · /block <no>: o kişiyi allowdan çıkar (1 = sahip, silinemez) */
|
|
809
|
+
if (!isOwnerSender(senderNum)) {
|
|
810
|
+
out = 'Bu komutu yalnız *sahip* kullanabilir.';
|
|
811
|
+
} else {
|
|
812
|
+
const list = settings.waAllow || [];
|
|
813
|
+
const rows = list.filter((e) => e !== '*');
|
|
814
|
+
if (!arg) {
|
|
815
|
+
out = rows.length
|
|
816
|
+
? '*Allow listesi*\n' +
|
|
817
|
+
rows
|
|
818
|
+
.map((e, i) => {
|
|
819
|
+
const nm = typeof e === 'string' ? '' : String(e.name || '');
|
|
820
|
+
const own = typeof e === 'object' && !!e.owner;
|
|
821
|
+
return `${i + 1}. ${nm ? escapeWa(nm) + ' ' : ''}+${waEntryDigits(e)}${own ? ' 👑 *sahip*' : ''}`;
|
|
822
|
+
})
|
|
823
|
+
.join('\n') +
|
|
824
|
+
'\n\nÇıkarmak için: `/block <no>` — *1. kişi sahip, asla silinemez*'
|
|
825
|
+
: 'Allow listesi boş. Eklemek için: `/allow <isim> <numara>`';
|
|
826
|
+
} else {
|
|
827
|
+
const n = parseInt(String(arg).trim(), 10);
|
|
828
|
+
if (!Number.isFinite(n) || n < 1 || n > rows.length) {
|
|
829
|
+
out = `Kullanım: \`/block <no>\` (1-${rows.length}) — listeyi görmek için /block`;
|
|
830
|
+
} else if (n === 1 || (typeof rows[n - 1] === 'object' && rows[n - 1].owner)) {
|
|
831
|
+
out = '*Sahip allowdan çıkarılamaz.* 😤';
|
|
832
|
+
} else {
|
|
833
|
+
const victim = rows[n - 1];
|
|
834
|
+
settings.waAllow = list.filter((e) => e !== victim);
|
|
835
|
+
saveSettings();
|
|
836
|
+
emitWaEventSafe({ type: 'allow' });
|
|
837
|
+
const vName = typeof victim === 'string' ? '' : String(victim.name || '');
|
|
838
|
+
waLog(`slash /block: +${waEntryDigits(victim)} (${vName}) çıkarıldı`);
|
|
839
|
+
out = `*Allowdan çıkarıldı:* ${vName ? vName + ' ' : ''}+${waEntryDigits(victim)}`;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} else if (cmd === 'approve' || cmd === 'deny') {
|
|
844
|
+
/* onay kapısı: /approve · /approve always · /deny */
|
|
845
|
+
const a = String(arg || '').toLowerCase();
|
|
846
|
+
const always = cmd === 'approve' && a === 'always';
|
|
847
|
+
const r = resolveFirstApproval(cmd === 'approve', always);
|
|
848
|
+
out = r.ok
|
|
849
|
+
? `*${cmd === 'deny' ? 'Reddedildi' : 'Onaylandı'}:* ${r.tool}${always ? ' — bu araç için bir daha sorulmayacak' : ''}`
|
|
850
|
+
: 'Bekleyen onay yok.';
|
|
851
|
+
} else if (cmd === 'think') {
|
|
852
|
+
const r = arg ? applyThinkLevel(arg) : null;
|
|
853
|
+
out = r && r.error ? r.error : r ? r.text : thinkStatusText();
|
|
854
|
+
} else if (cmd === 'stop') {
|
|
855
|
+
/* /stop: koşan paralel ajanlar + turlar + kuyruklar; cron/izleyici/olay SÜRER */
|
|
856
|
+
const stopped = stopEverything();
|
|
857
|
+
out =
|
|
858
|
+
`*Durdu* — ${stopped} koşan iş kesildi.\n` +
|
|
859
|
+
`Paralel ajanlar ve bekleyen kuyruklar temizlendi. Cron, izleyici ve olay merkezi çalışmaya devam ediyor.\n` +
|
|
860
|
+
`Devam için bir şeyler yaz ya da /start`;
|
|
861
|
+
} else if (cmd === 'start') {
|
|
862
|
+
if (servicesPaused) {
|
|
863
|
+
resumeServices();
|
|
864
|
+
out = '*Devam* — tüm servisler ve zamanlayıcılar yeniden başladı.';
|
|
865
|
+
} else {
|
|
866
|
+
out = 'Zaten çalışıyor — durdurulmuş bir şey yok.';
|
|
867
|
+
}
|
|
868
|
+
} else if (cmd === 'restart') {
|
|
869
|
+
out = '*\u21BB Yeniden başlatılıyor…* Uygulama birkaç saniye içinde kapanıp açılacak.';
|
|
870
|
+
setTimeout(() => {
|
|
871
|
+
try { app.relaunch(); } catch {}
|
|
872
|
+
try { app.exit(0); } catch {}
|
|
873
|
+
}, 1500);
|
|
874
|
+
} else if (cmd === 'change') {
|
|
875
|
+
out = modelChangeText(arg);
|
|
876
|
+
} else if (cmd === 'notes') {
|
|
877
|
+
const sid = waChats.get(jid);
|
|
878
|
+
out = sid ? notesText(sid) : 'Bu sohbetin oturumu yok — önce bir şeyler yaz.';
|
|
879
|
+
} else if (cmd === 'notify') {
|
|
880
|
+
const a = String(arg || '').toLowerCase();
|
|
881
|
+
if (a === 'on' || a === 'off') {
|
|
882
|
+
settings.notifyOwnerFail = a === 'on';
|
|
883
|
+
saveSettings();
|
|
884
|
+
engine.notifyOwnerFail = settings.notifyOwnerFail;
|
|
885
|
+
}
|
|
886
|
+
out = `Hata mail bildirimi: ${settings.notifyOwnerFail !== false ? 'AÇIK' : 'KAPALI'} (değiştirmek için /notify on|off)`;
|
|
887
|
+
} else if (cmd === 'clear') {
|
|
888
|
+
const sid = waChats.get(jid);
|
|
889
|
+
if (sid && engine.clearMessages(sid)) {
|
|
890
|
+
out = '*Bu oturumun geçmişi temizlendi.* Aynı kodla sıfırdan devam edebilirsin.';
|
|
891
|
+
} else {
|
|
892
|
+
out = 'Temizlenecek oturum bulunamadı.';
|
|
893
|
+
}
|
|
894
|
+
} else if (cmd === 'screenshot') {
|
|
895
|
+
const img = await captureScreenDataUrl();
|
|
896
|
+
if (!img) {
|
|
897
|
+
out = 'Ekran görüntüsü alınamadı.';
|
|
898
|
+
} else {
|
|
899
|
+
const buf = Buffer.from(String(img).split(',')[1] || '', 'base64');
|
|
900
|
+
const sent = await wa.sendImage(jid, buf, 'Masaüstü ekran görüntüsü');
|
|
901
|
+
out = sent ? '' : 'Görsel gönderilemedi.';
|
|
902
|
+
}
|
|
903
|
+
} else if (cmd === 'backup') {
|
|
904
|
+
const r = await createBackup();
|
|
905
|
+
out = r.ok
|
|
906
|
+
? `*Şifreli yedek alındı*\n${r.path}\n(${Math.round(r.size / 1024)} KB)\nBeast Kodu: \`${r.code}\``
|
|
907
|
+
: 'Yedek hata: ' + (r.error || '?');
|
|
908
|
+
} else if (cmd === 'model') {
|
|
909
|
+
const st = engine.publicState();
|
|
910
|
+
if (arg) {
|
|
911
|
+
const hit = st.models.find((m) => m.sel === arg || m.model.toLowerCase().includes(arg.toLowerCase()));
|
|
912
|
+
if (hit) {
|
|
913
|
+
settings.modelOverride = hit.sel;
|
|
914
|
+
saveSettings();
|
|
915
|
+
engine.setModelOverride ? engine.setModelOverride(hit.sel) : null;
|
|
916
|
+
const st2 = engine.publicState();
|
|
917
|
+
out = st2.activeModel
|
|
918
|
+
? `*Model değişti:* ${st2.activeModel.providerName} · ${st2.activeModel.model}`
|
|
919
|
+
: '*Model değişti.*';
|
|
920
|
+
} else {
|
|
921
|
+
out =
|
|
922
|
+
'Eşleşen model yok. *Kurulu modeller:*\n' +
|
|
923
|
+
st.models.slice(0, 10).map((m) => `- ${m.providerName} · ${m.model}`).join('\n');
|
|
924
|
+
}
|
|
925
|
+
} else if (st.activeModel) {
|
|
926
|
+
out = `*Aktif model:* ${st.activeModel.providerName} · ${st.activeModel.model}\nDeğiştirmek için: /model <isim-parçası>`;
|
|
927
|
+
} else {
|
|
928
|
+
out = 'Model seçilmemiş.';
|
|
929
|
+
}
|
|
930
|
+
} else if (cmd === 'skills') {
|
|
931
|
+
const sk = skillsMod.scan();
|
|
932
|
+
out = sk.length
|
|
933
|
+
? '*Skill\u2019ler:*\n' + sk.map((s) => '- ' + s.name).join('\n')
|
|
934
|
+
: 'Henüz skill yok — SKILL.md dosyası atarsan kurarım.';
|
|
935
|
+
} else if (cmd === 'usage') {
|
|
936
|
+
const rep = usageMod.report();
|
|
937
|
+
const f = (r) =>
|
|
938
|
+
`${r.calls} çağrı · ${fmtNum(r.pin)}+${fmtNum(r.pout)} token${r.cost ? ' · ~$' + r.cost.toFixed(4) : ''}`;
|
|
939
|
+
out = `*Bugün:* ${f(rep.today.total)}\n*Bu ay:* ${f(rep.month.total)}\n\nDetay: Ayarlar → Maliyet`;
|
|
940
|
+
} else if (cmd === 'status') {
|
|
941
|
+
const wst = wa ? wa.snapshot() : { status: 'disconnected' };
|
|
942
|
+
const jobs = cron.list().filter((j) => j.enabled).length;
|
|
943
|
+
out =
|
|
944
|
+
`*WA:* ${wst.status}${wst.user ? ' (' + wst.user + ')' : ''}\n` +
|
|
945
|
+
`*İzleyici:* ${watchers.list().length} adet\n*Cron:* ${jobs} aktif görev`;
|
|
946
|
+
} else {
|
|
947
|
+
out = `Bilinmeyen komut: /${cmd}\nListe için /help yaz.`;
|
|
948
|
+
}
|
|
949
|
+
} catch (e) {
|
|
950
|
+
out = 'Komut hatası: ' + String((e && e.message) || e);
|
|
951
|
+
}
|
|
952
|
+
if (out) await wa.send(jid, out).catch(() => {});
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/* ---------- WA anti-spam birleştirme ----------
|
|
957
|
+
Aynı sohbetten gelen ardışık mesajları WA_DEBOUNCE_MS boyunca toplar,
|
|
958
|
+
tek pakette işler. Medya varsa ilk medya esastır; metinler birleşir. */
|
|
959
|
+
|
|
960
|
+
const WA_DEBOUNCE_MS = 4500;
|
|
961
|
+
const waQueue = new Map(); // jid -> { timer, payloads[] }
|
|
962
|
+
|
|
963
|
+
function waQueuePush(jid, payload, senderNum) {
|
|
964
|
+
let q = waQueue.get(jid);
|
|
965
|
+
if (!q) {
|
|
966
|
+
q = { texts: [], payloads: [] };
|
|
967
|
+
waQueue.set(jid, q);
|
|
968
|
+
}
|
|
969
|
+
q.payloads.push({ payload, senderNum });
|
|
970
|
+
clearTimeout(q.timer);
|
|
971
|
+
waLog(`queue: mesaj kuyruğa girdi jid=${waPrettyJid(jid)} toplam=${q.payloads.length} (4.5 sn birleştirme penceresi)`);
|
|
972
|
+
q.timer = setTimeout(() => {
|
|
973
|
+
waLog(`queue: birleştirme penceresi kapandı, flush başlıyor jid=${waPrettyJid(jid)}`);
|
|
974
|
+
/* KUYRUĞU BURADA SİLME — waFlush içindeki veriyi alıp kendisi siliyor */
|
|
975
|
+
waFlush(jid).catch((e) => {
|
|
976
|
+
waLog(`flush KRASİ: ${String((e && e.stack) || e)}`);
|
|
977
|
+
});
|
|
978
|
+
}, WA_DEBOUNCE_MS);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function waFlush(jid) {
|
|
982
|
+
const q = waQueue.get(jid);
|
|
983
|
+
waLog(`flush: çağrıldı jid=${waPrettyJid(jid)} kuyrukta=${q ? q.payloads.length : 0}`);
|
|
984
|
+
if (!q) return;
|
|
985
|
+
waQueue.delete(jid);
|
|
986
|
+
const merged = { text: '', media: null, isGroup: false, participant: '', mentioned: false };
|
|
987
|
+
let senderNum = '';
|
|
988
|
+
/* push tarafıyla AYNI alan adı: { payload, senderNum } */
|
|
989
|
+
for (const { payload, senderNum: sn } of q.payloads) {
|
|
990
|
+
if (payload.text) merged.text += (merged.text ? '\n' : '') + payload.text;
|
|
991
|
+
if (payload.media && !merged.media) merged.media = payload.media;
|
|
992
|
+
if (payload.isGroup) merged.isGroup = true;
|
|
993
|
+
if (payload.participant) merged.participant = payload.participant;
|
|
994
|
+
if (payload.mentioned) merged.mentioned = true;
|
|
995
|
+
if (!senderNum && sn) senderNum = sn;
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
await processWaMessage(jid, merged, senderNum);
|
|
999
|
+
} catch (e) {
|
|
1000
|
+
waLog(`waFlush/processWaMessage hata: ${String((e && e.stack) || e)}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/* ---------- WA'dan skill kurulumu ----------
|
|
1005
|
+
SKILL.md (veya *.skill.md) belgesi atılırsa skills altına kurulur. */
|
|
1006
|
+
|
|
1007
|
+
function installSkillFromDoc(media) {
|
|
1008
|
+
const name = String(media.name || '').trim();
|
|
1009
|
+
const isSkill = /^SKILL\.md$/i.test(name) || /\.skill\.md$/i.test(name);
|
|
1010
|
+
if (!isSkill) return null;
|
|
1011
|
+
try {
|
|
1012
|
+
const fm = skillsMod.parseFrontmatter(media.buf.toString('utf8'));
|
|
1013
|
+
const fname = String(fm.name || '').replace(/[^\w.-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '') ||
|
|
1014
|
+
name.replace(/\.skill\.md$/i, '').replace(/[^a-z0-9_-]+/gi, '-') ||
|
|
1015
|
+
'yeni-skill';
|
|
1016
|
+
const dir = path.join(skillsMod.dir(), fname.toLowerCase());
|
|
1017
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1018
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), media.buf.toString('utf8'));
|
|
1019
|
+
return { folder: fname.toLowerCase() };
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
waLog('skill kurulum hata: ' + String((e && e.message) || e));
|
|
1022
|
+
return { error: true };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
/* ---------- E-posta köprüsü (IMAP/SMTP, Gmail uygulama şifresi) ---------- */
|
|
1028
|
+
|
|
1029
|
+
function emailCfg() {
|
|
1030
|
+
return settings.email || {};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function emailList(opts = {}) {
|
|
1034
|
+
const cfg = emailCfg();
|
|
1035
|
+
if (!cfg.host || !cfg.user || !cfg.pass) return { ok: false, error: 'e-posta ayarlanmamış — Entegrasyonlar\u0027da gir' };
|
|
1036
|
+
if (!ImapFlow) return { ok: false, error: 'imap modülü yok' };
|
|
1037
|
+
const limit = Math.max(1, Math.min(Number(opts.limit) || 10, 30));
|
|
1038
|
+
const client = new ImapFlow({
|
|
1039
|
+
host: cfg.host,
|
|
1040
|
+
port: Number(cfg.port) || 993,
|
|
1041
|
+
secure: true,
|
|
1042
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
1043
|
+
logger: false,
|
|
1044
|
+
});
|
|
1045
|
+
try {
|
|
1046
|
+
await client.connect();
|
|
1047
|
+
const lock = await client.getMailboxLock(opts.folder || 'INBOX');
|
|
1048
|
+
try {
|
|
1049
|
+
let uids = opts.unread ? await client.search({ seen: false }) : await client.search({ all: true });
|
|
1050
|
+
uids = Array.isArray(uids) ? uids.slice(-limit) : [];
|
|
1051
|
+
const messages = [];
|
|
1052
|
+
for await (const m of client.fetch(uids, { uid: true, envelope: true })) {
|
|
1053
|
+
const from = (m.envelope.from || [])
|
|
1054
|
+
.map((a) => (a.name ? `${a.name} <${a.address}>` : a.address))
|
|
1055
|
+
.join(', ');
|
|
1056
|
+
messages.push({ uid: m.uid, from, subject: m.envelope.subject || '(konu yok)', date: m.envelope.date });
|
|
1057
|
+
}
|
|
1058
|
+
return { ok: true, messages: messages.reverse() };
|
|
1059
|
+
} finally {
|
|
1060
|
+
lock.release();
|
|
1061
|
+
}
|
|
1062
|
+
} catch (e) {
|
|
1063
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
1064
|
+
} finally {
|
|
1065
|
+
try { await client.logout(); } catch {}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function emailRead(uid) {
|
|
1070
|
+
const cfg = emailCfg();
|
|
1071
|
+
if (!cfg.host || !cfg.user || !cfg.pass) return { ok: false, error: 'e-posta ayarlanmamış' };
|
|
1072
|
+
if (!ImapFlow) return { ok: false, error: 'imap modülü yok' };
|
|
1073
|
+
const client = new ImapFlow({
|
|
1074
|
+
host: cfg.host,
|
|
1075
|
+
port: Number(cfg.port) || 993,
|
|
1076
|
+
secure: true,
|
|
1077
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
1078
|
+
logger: false,
|
|
1079
|
+
});
|
|
1080
|
+
try {
|
|
1081
|
+
await client.connect();
|
|
1082
|
+
const lock = await client.getMailboxLock('INBOX');
|
|
1083
|
+
try {
|
|
1084
|
+
for await (const m of client.fetch([Number(uid)], { uid: true, source: true })) {
|
|
1085
|
+
const raw = m.source.toString('utf8');
|
|
1086
|
+
const bodyPart = raw.split(/\r?\n\r?\n/).slice(1).join('\n\n') || raw;
|
|
1087
|
+
const text = htmlToText(bodyPart);
|
|
1088
|
+
return { ok: true, uid: Number(uid), content: String(text || '').slice(0, 8000) };
|
|
1089
|
+
}
|
|
1090
|
+
return { ok: false, error: 'mail bulunamadı' };
|
|
1091
|
+
} finally {
|
|
1092
|
+
lock.release();
|
|
1093
|
+
}
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
1096
|
+
} finally {
|
|
1097
|
+
try { await client.logout(); } catch {}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function emailSend({ to, subject, body }) {
|
|
1102
|
+
const cfg = emailCfg();
|
|
1103
|
+
if (!cfg.smtpHost || !cfg.user || !cfg.pass) return { ok: false, error: 'gönderim ayarlanmamış (SMTP)' };
|
|
1104
|
+
if (!nodemailer) return { ok: false, error: 'mail modülü yok' };
|
|
1105
|
+
if (!to || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(to))) return { ok: false, error: 'geçersiz alıcı' };
|
|
1106
|
+
try {
|
|
1107
|
+
const port = Number(cfg.smtpPort) || 465;
|
|
1108
|
+
const tr = nodemailer.createTransport({
|
|
1109
|
+
host: cfg.smtpHost,
|
|
1110
|
+
port,
|
|
1111
|
+
/* 465 = implicit TLS, 587 = STARTTLS (nodemailer otomatik yükseltir) */
|
|
1112
|
+
secure: port === 465,
|
|
1113
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
1114
|
+
});
|
|
1115
|
+
await tr.sendMail({ from: cfg.user, to: String(to), subject: String(subject || '(konusuz)'), text: String(body || '') });
|
|
1116
|
+
return { ok: true };
|
|
1117
|
+
} catch (e) {
|
|
1118
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const PDF_EXT = /\.pdf$/i;
|
|
1123
|
+
|
|
1124
|
+
function documentToText(media) {
|
|
1125
|
+
try {
|
|
1126
|
+
const mime = String(media.mimetype || '');
|
|
1127
|
+
const name = String(media.name || '');
|
|
1128
|
+
if (mime.startsWith('text/') || TEXT_DOC_EXT.test(name)) {
|
|
1129
|
+
return media.buf.toString('utf8');
|
|
1130
|
+
}
|
|
1131
|
+
} catch {}
|
|
1132
|
+
return null; // pdf gibi asenkron türler documentToTextAsync'e düşer
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function documentToTextAsync(media) {
|
|
1136
|
+
const dt = documentToText(media);
|
|
1137
|
+
if (dt) return dt;
|
|
1138
|
+
try {
|
|
1139
|
+
const mime = String(media.mimetype || '');
|
|
1140
|
+
const name = String(media.name || '');
|
|
1141
|
+
if (mime === 'application/pdf' || PDF_EXT.test(name)) {
|
|
1142
|
+
const r = await require('./agent/pdf').extract(media.buf);
|
|
1143
|
+
return String((r && r.text) || '');
|
|
1144
|
+
}
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
waLog('pdf okuma hata: ' + String((e && e.message) || e));
|
|
1147
|
+
}
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/* ---------- WA hatırlatıcı ---------- */
|
|
1152
|
+
|
|
1153
|
+
function scheduleReminder({ when, message, sessionId, repeat }) {
|
|
1154
|
+
try {
|
|
1155
|
+
const msg = String(message || '').trim();
|
|
1156
|
+
if (!msg) return { ok: false, error: 'hatırlatma metni boş' };
|
|
1157
|
+
const rep = String(repeat || '').trim();
|
|
1158
|
+
if (rep) {
|
|
1159
|
+
/* tekrarlı görev: cron.json'da kalıcı job (once değil) */
|
|
1160
|
+
const s = cron.reminderSchedule(when, rep);
|
|
1161
|
+
if (!s.ok) return s;
|
|
1162
|
+
const r = cron.add({
|
|
1163
|
+
name: 'Tekrarlı hatırlatma: ' + msg.slice(0, 40),
|
|
1164
|
+
schedule: s.schedule,
|
|
1165
|
+
prompt: `[TEKRARLI HATIRLATMA] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
|
|
1166
|
+
sessionId: sessionId || undefined,
|
|
1167
|
+
});
|
|
1168
|
+
if (!r.ok) return r;
|
|
1169
|
+
waLog(`tekrarlı hatırlatma kuruldu id=${r.job.id} schedule=${s.schedule} sid=${sessionId || '-'}`);
|
|
1170
|
+
return { ok: true, repeat: rep, schedule: s.schedule, message: msg };
|
|
1171
|
+
}
|
|
1172
|
+
const d = new Date(String(when || '').trim());
|
|
1173
|
+
if (isNaN(d.getTime())) return { ok: false, error: 'tarih anlaşılamadı — format YYYY-MM-DDTHH:mm' };
|
|
1174
|
+
if (d.getTime() <= Date.now()) return { ok: false, error: 'geçmiş bir zaman verildi' };
|
|
1175
|
+
const cronExpr = `${d.getMinutes()} ${d.getHours()} ${d.getDate()} ${d.getMonth() + 1} *`;
|
|
1176
|
+
const r = cron.add({
|
|
1177
|
+
name: 'Hatırlatma: ' + msg.slice(0, 40),
|
|
1178
|
+
schedule: cronExpr,
|
|
1179
|
+
prompt: `[HATIRLATMA ZAMANI] Kullanıcıya şunu hatırlat: "${msg}". Kısaca ve nazikçe bildir.`,
|
|
1180
|
+
once: true,
|
|
1181
|
+
sessionId: sessionId || undefined,
|
|
1182
|
+
});
|
|
1183
|
+
if (!r.ok) return r;
|
|
1184
|
+
waLog(`reminder kuruldu id=${r.job.id} at=${d.toISOString()} sid=${sessionId || '-'}`);
|
|
1185
|
+
return { ok: true, at: d.toISOString(), message: msg };
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/* Giriş kapısı: slash komutlar hemen işlenir; DM allowlist'e bakar;
|
|
1192
|
+
grup sohbetleri mention-gated ayara göre kabul edilir. Uygun her mesaj
|
|
1193
|
+
anti-spam kuyruğuna düşürülür (WA_DEBOUNCE_MS birleştirme). */
|
|
1194
|
+
async function handleWaIncoming(jid, payload, senderNum) {
|
|
1195
|
+
try {
|
|
1196
|
+
if (!engine) return;
|
|
1197
|
+
if (typeof payload === 'string') payload = { text: payload };
|
|
1198
|
+
const isGroup = !!payload.isGroup || jid.endsWith('@g.us');
|
|
1199
|
+
|
|
1200
|
+
/* slash komutları: DM'de her zaman; grupta sadece bot mention edildiyse */
|
|
1201
|
+
const txt0 = String(payload.text || '').trim();
|
|
1202
|
+
if (txt0.startsWith('/') && !txt0.includes('\n') && (!isGroup || payload.mentioned)) {
|
|
1203
|
+
if (await tryWaSlash(jid, txt0, senderNum)) return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (isGroup) {
|
|
1207
|
+
const g = settings.waGroups || {};
|
|
1208
|
+
if (!g.enabled) return;
|
|
1209
|
+
if (g.mentionOnly !== false && !payload.mentioned) return;
|
|
1210
|
+
waLog(`grup mesajı jid=${waPrettyJid(jid)} participant=+${senderNum || '?'} mention=${!!payload.mentioned}`);
|
|
1211
|
+
} else {
|
|
1212
|
+
const hit = waFind(senderNum);
|
|
1213
|
+
waLog(
|
|
1214
|
+
`incoming jid=${jid} sender=${senderNum || '?'} allowed=${!!hit}` +
|
|
1215
|
+
(hit && hit.name ? ' name=' + hit.name : '') +
|
|
1216
|
+
(hit && hit.lockdown ? ' locked=1' : '') +
|
|
1217
|
+
(payload && payload.media ? ' media=' + payload.media.kind : '')
|
|
1218
|
+
);
|
|
1219
|
+
if (!hit) return; // allowlist dışı yoksay
|
|
1220
|
+
// İsimsiz kayıt: güvenlik için cevap verme — kullanıcıyı ayarlara yönlendir
|
|
1221
|
+
if (hit.num !== '*' && !hit.name) {
|
|
1222
|
+
waLog(`skip: isimsiz kayıt (${hit.num}) — cevap verilmedi, Entegrasyonlar'da isim ekle`);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
/* LID jidleri için gerçek PN'yi sakla — gönderim fallback'i buradan beslenir */
|
|
1226
|
+
if (senderNum) waJidPn.set(jid, String(senderNum));
|
|
1227
|
+
}
|
|
1228
|
+
waLog(`gate: kuyruğa geçiliyor jid=${waPrettyJid(jid)} sender=+${senderNum || '?'}`);
|
|
1229
|
+
resumeServices(); // pause durumunda gelen mesaj servisleri canlandırır
|
|
1230
|
+
waQueuePush(jid, { ...payload, isGroup }, senderNum);
|
|
1231
|
+
} catch (e) {
|
|
1232
|
+
waLog(`handleWaIncoming KRASİ: ${String((e && e.stack) || e)}`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/* Güvenli WA gönderimi: hedef adrese dener, LID gibi adresler patlarsa
|
|
1237
|
+
bilinen gerçek numaraya (@s.whatsapp.net) tek kez düşer. Sonucu loglar. */
|
|
1238
|
+
async function sendWaSafe(jid, text) {
|
|
1239
|
+
let r = null;
|
|
1240
|
+
try {
|
|
1241
|
+
r = await wa.send(jid, text);
|
|
1242
|
+
} catch {}
|
|
1243
|
+
if (r) return true;
|
|
1244
|
+
const pn = waJidPn.get(jid);
|
|
1245
|
+
const pnJid = pn ? `${pn}@s.whatsapp.net` : '';
|
|
1246
|
+
if (pnJid && pnJid !== jid) {
|
|
1247
|
+
try {
|
|
1248
|
+
r = await wa.send(pnJid, text);
|
|
1249
|
+
} catch {}
|
|
1250
|
+
if (r) {
|
|
1251
|
+
waLog(`out fallback → ${waPrettyJid(pnJid)} (lid/pn denemelerinden sonra)`);
|
|
1252
|
+
return true;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
waLog(`out BAŞARISIZ jid=${jid} — send-error loguna bak`);
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/* Kuyruktan tek paket halinde gelir: asıl işleme burada */
|
|
1260
|
+
|
|
1261
|
+
async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
1262
|
+
const isGroup = !!payload.isGroup;
|
|
1263
|
+
const hit = isGroup ? null : waFind(senderNum);
|
|
1264
|
+
if (!isGroup && !hit) {
|
|
1265
|
+
waLog(`skip flush: izinli eşleşme yok (sender=+${senderNum || '?'})`);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
let sid = waChats.get(jid);
|
|
1269
|
+
if (sid && engine.isBusy(sid)) {
|
|
1270
|
+
/* oturum hâlâ önceki işle uğraşıyor — KAYBETME: sınırı kaldırdık,
|
|
1271
|
+
iş bitene dek her debounce turunda yeniden deneriz; kullanıcıya
|
|
1272
|
+
"işleyemedim" denmez, mesaj boşa gitmez. */
|
|
1273
|
+
if (requeues === 1) {
|
|
1274
|
+
waLog(`flush: oturum meşgul, mesaj kuyrukta bekliyor sid=${sid}`);
|
|
1275
|
+
}
|
|
1276
|
+
await new Promise((r) => setTimeout(r, WA_DEBOUNCE_MS));
|
|
1277
|
+
return processWaMessage(jid, payload, senderNum, requeues + 1);
|
|
1278
|
+
}
|
|
1279
|
+
if (!isGroup && requeues > 0) {
|
|
1280
|
+
// retry sonrası izin yeniden kontrol edilir (yukarıda hit zaten alınıyor)
|
|
1281
|
+
}
|
|
1282
|
+
if (!sid) {
|
|
1283
|
+
const v = engine.createSession();
|
|
1284
|
+
sid = v.id;
|
|
1285
|
+
waChats.set(jid, sid);
|
|
1286
|
+
waRememberSession(jid, sid);
|
|
1287
|
+
saveWaChats();
|
|
1288
|
+
if (wa) wa.setWatchJids([...waChats.keys()]);
|
|
1289
|
+
} else {
|
|
1290
|
+
waRememberSession(jid, sid);
|
|
1291
|
+
}
|
|
1292
|
+
// Cevap verilecek — karşı telefonda "yazıyor…" göstergesi (medya işlenene dek sürer)
|
|
1293
|
+
wa.setComposing(jid, true);
|
|
1294
|
+
// Kişi bazlı granül izin: all/web/read/chat
|
|
1295
|
+
const perm = isGroup ? 'all' : hit.perm || (hit.lockdown ? 'chat' : 'all');
|
|
1296
|
+
engine.setSessionPerm(sid, perm);
|
|
1297
|
+
waLog(`perm=${perm} sid=${sid}`);
|
|
1298
|
+
|
|
1299
|
+
const participantName = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
|
|
1300
|
+
/* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
|
|
1301
|
+
const isOwner = !isGroup && !!hit.owner;
|
|
1302
|
+
const roleTag = isOwner
|
|
1303
|
+
? 'SAHİBİN (talepleri önceliklidir)'
|
|
1304
|
+
: 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
|
|
1305
|
+
const label = isGroup
|
|
1306
|
+
? `Grup ${jid.split('@')[0]}${participantName ? ' · ' + participantName : ''}`
|
|
1307
|
+
: hit.name
|
|
1308
|
+
? `${hit.name} (+${senderNum || '?'}) — ${roleTag}`
|
|
1309
|
+
: `+${senderNum || '?'} — ${roleTag}`;
|
|
1310
|
+
let text = `[WhatsApp${isGroup ? ' grup' : ''} — gönderen: ${label}]`;
|
|
1311
|
+
if (!isGroup && !isOwner) {
|
|
1312
|
+
text += `\n[NOT: Bu kişi SAHİP DEĞİL, misafirdir. Sahibin ayarlarını/verilerini değiştirme; kalıcı hafızaya misafire özel bilgi yazma.]`;
|
|
1313
|
+
}
|
|
1314
|
+
const attachments = [];
|
|
1315
|
+
|
|
1316
|
+
// Medya işleme
|
|
1317
|
+
if (payload.media) {
|
|
1318
|
+
const md = payload.media;
|
|
1319
|
+
try {
|
|
1320
|
+
if (md.kind === 'image') {
|
|
1321
|
+
attachments.push({ type: 'image', dataUrl: `data:${md.mimetype};base64,${md.buf.toString('base64')}`, name: md.name });
|
|
1322
|
+
text += `\n[resim eki alındı]`;
|
|
1323
|
+
} else if (md.kind === 'audio') {
|
|
1324
|
+
const tr = await transcribeAudio(md.buf, md.mimetype);
|
|
1325
|
+
text += tr
|
|
1326
|
+
? `\n[sesli mesaj transkripti]\n${tr}`
|
|
1327
|
+
: `\n[sesli mesaj alındı ama transkripte çevrilemedi — Entegrasyonlar'da sesli mesaj (STT) ayarı gerekli]`;
|
|
1328
|
+
} else if (md.kind === 'document') {
|
|
1329
|
+
/* SKILL.md atıldıysa skill olarak kurulur */
|
|
1330
|
+
const inst = installSkillFromDoc(md);
|
|
1331
|
+
if (inst && inst.folder) {
|
|
1332
|
+
await wa.send(
|
|
1333
|
+
jid,
|
|
1334
|
+
`*Skill kuruldu:* ${inst.folder}\nArtık kullanabilirim — /skills ile listeleyebilirsin.`
|
|
1335
|
+
).catch(() => {});
|
|
1336
|
+
try { await wa.setComposing(jid, false); } catch {}
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (inst && inst.error) {
|
|
1340
|
+
text += `\n[SKILL.md alındı ama kaydedilemedi]`;
|
|
1341
|
+
} else {
|
|
1342
|
+
const dt = documentToText(md);
|
|
1343
|
+
if (dt && dt.trim()) {
|
|
1344
|
+
attachments.push({ type: 'file', name: md.name, content: dt.slice(0, 20000) });
|
|
1345
|
+
text += `\n[belge alındı: ${md.name}]`;
|
|
1346
|
+
} else {
|
|
1347
|
+
text += `\n[belge alındı ama okunamadı: ${md.name}]`;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
waLog(`medya hata: ${String((e && e.message) || e)}`);
|
|
1353
|
+
text += `\n[medya işlenemedi]`;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (payload.text) text += `\n${String(payload.text).slice(0, 6000)}`;
|
|
1358
|
+
engine.send(sid, { text: text.slice(0, 8000), attachments });
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function ensureWa() {
|
|
1362
|
+
if (!wa) {
|
|
1363
|
+
wa = new WhatsAppBridge({
|
|
1364
|
+
authDir: WA_AUTH_DIR,
|
|
1365
|
+
emit: (ev) => {
|
|
1366
|
+
if (ev.type === 'status') {
|
|
1367
|
+
waLog(`status=${ev.status}${ev.user ? ' user=' + ev.user : ''}`);
|
|
1368
|
+
if (ev.status === 'qr') waAwaitingRestart = true; // eşleme süreci başladı
|
|
1369
|
+
/* #v13: ilk QR eşlemesi sonrası ilk bağlantı — temiz durum için
|
|
1370
|
+
otomatik restart (auth artık diskte, ikinci açılış sessiz bağlanır) */
|
|
1371
|
+
if (ev.status === 'connected' && waAwaitingRestart) {
|
|
1372
|
+
waAwaitingRestart = false;
|
|
1373
|
+
waLog('ilk eşleme tamam — uygulama 3 sn içinde otomatik yeniden başlatılıyor');
|
|
1374
|
+
if (win && !win.isDestroyed()) {
|
|
1375
|
+
win.webContents.send('wa:event', { type: 'status', status: 'restarting' });
|
|
1376
|
+
}
|
|
1377
|
+
setTimeout(() => {
|
|
1378
|
+
flushBrowserStorage(); // app.exit before-quit'i atlar — elle flush
|
|
1379
|
+
app.relaunch(); // aynı argümanlarla yeniden başlat
|
|
1380
|
+
app.exit(0); // before-quit tetiklenmeden temiz çıkış
|
|
1381
|
+
}, 3000);
|
|
1382
|
+
}
|
|
1383
|
+
} else if (ev.type === 'send') {
|
|
1384
|
+
waLog(`out → ${waPrettyJid(ev.jid)} "${ev.preview || ''}"`);
|
|
1385
|
+
} else if (ev.type === 'tick') {
|
|
1386
|
+
waLog(`tick ${ev.label} ← ${waPrettyJid(ev.jid)} "${ev.preview || ''}"`);
|
|
1387
|
+
} else if (ev.type === 'receipt') {
|
|
1388
|
+
waLog(`receipt ${ev.detail} ← "${ev.preview || ''}"`);
|
|
1389
|
+
} else if (ev.type === 'presence') {
|
|
1390
|
+
waLog(`presence ${ev.label || ev.presence} ← ${waPrettyJid(ev.jid)}`);
|
|
1391
|
+
bridgeWaToBus(ev);
|
|
1392
|
+
} else if (ev.type === 'send-error') {
|
|
1393
|
+
waLog(`SEND HATA → ${waPrettyJid(ev.jid)} "${ev.preview || ''}" sebep: ${ev.error}`);
|
|
1394
|
+
}
|
|
1395
|
+
if (win && !win.isDestroyed()) win.webContents.send('wa:event', ev);
|
|
1396
|
+
},
|
|
1397
|
+
onIncoming: handleWaIncoming,
|
|
1398
|
+
});
|
|
1399
|
+
wa.setWatchJids([...waChats.keys()]);
|
|
1400
|
+
}
|
|
1401
|
+
return wa;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function reloadBackend() {
|
|
1405
|
+
if (engine && typeof engine.dispose === 'function') {
|
|
1406
|
+
try { engine.dispose(); } catch {}
|
|
1407
|
+
}
|
|
1408
|
+
const cfg = loadBeastConfig();
|
|
1409
|
+
engine = new Engine(cfg, {
|
|
1410
|
+
sessionsDir: SESSIONS_DIR,
|
|
1411
|
+
workspace: settings.workspace || app.getPath('home'),
|
|
1412
|
+
modelOverride: settings.modelOverride || null,
|
|
1413
|
+
customProviders: settings.customProviders || [],
|
|
1414
|
+
roleModels: settings.roleModels || {},
|
|
1415
|
+
deletedModels: settings.deletedModels || [],
|
|
1416
|
+
lockdown: !!settings.waLockdown,
|
|
1417
|
+
ceoMode: settings.ceoMode !== false,
|
|
1418
|
+
thinkLevel: settings.thinkLevel || 0,
|
|
1419
|
+
fallout: settings.fallout || null,
|
|
1420
|
+
limits: settings.limits || null,
|
|
1421
|
+
approvals: settings.security && settings.security.approvals ? approvalsBridge : null,
|
|
1422
|
+
alwaysAllowTools: (settings.security && settings.security.alwaysAllow) || [],
|
|
1423
|
+
crashFile: FALLOUT_CRASH_FILE,
|
|
1424
|
+
notifyOwnerFail: settings.notifyOwnerFail !== false,
|
|
1425
|
+
fileSend: deliverFile,
|
|
1426
|
+
reminders: { add: scheduleReminder },
|
|
1427
|
+
watchers: {
|
|
1428
|
+
list: () => watchers.list(),
|
|
1429
|
+
add: (input) => watchers.add(input),
|
|
1430
|
+
remove: (id) => watchers.remove(id),
|
|
1431
|
+
},
|
|
1432
|
+
bus: {
|
|
1433
|
+
list: () => bus.listSubs(),
|
|
1434
|
+
add: (i) => bus.addSub(i),
|
|
1435
|
+
remove: (id) => bus.removeSub(id),
|
|
1436
|
+
},
|
|
1437
|
+
computer: {
|
|
1438
|
+
look: captureScreenDataUrl,
|
|
1439
|
+
act: (op, args) => computeruse.act(op, args),
|
|
1440
|
+
},
|
|
1441
|
+
ocr: (o) => ocrRead(o),
|
|
1442
|
+
email: { list: emailList, read: emailRead, send: emailSend },
|
|
1443
|
+
browser: {
|
|
1444
|
+
openUrl: (u, s) => browserNavigate(u, s),
|
|
1445
|
+
search: (q, s) => browserSearch(q, s),
|
|
1446
|
+
readText: (s) => browserRead(s),
|
|
1447
|
+
screenshot: (s) => browserScreenshot(s),
|
|
1448
|
+
snapshot: (s) => browserSnapshot(s),
|
|
1449
|
+
act: (k, a, s) => browserAct(k, a, s),
|
|
1450
|
+
},
|
|
1451
|
+
emit: (ev) => {
|
|
1452
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
|
|
1453
|
+
flushDesktopOnDone(ev); /* biriken desktop mesajlarını sıraya bas */
|
|
1454
|
+
/* WA canlı iş takibi: oturum bir WhatsApp sohbetine bağlıysa araç
|
|
1455
|
+
hareketlerini (terminal, web, dosya…) kısa satırla bildir.
|
|
1456
|
+
Spam olmasın: sohbet başına en az 7 sn'de bir tek satır. */
|
|
1457
|
+
if (ev.type === 'tool-start' && wa && wa.connected) {
|
|
1458
|
+
try {
|
|
1459
|
+
const hit = [...waChats.entries()].find(([, s]) => s === ev.sessionId);
|
|
1460
|
+
if (hit) {
|
|
1461
|
+
const now = Date.now();
|
|
1462
|
+
const last = lastWaToolPing.get(ev.sessionId) || 0;
|
|
1463
|
+
if (now - last >= 7000) {
|
|
1464
|
+
lastWaToolPing.set(ev.sessionId, now);
|
|
1465
|
+
sendWaSafe(hit[0], '\u203A ' + String(waToolLine(ev.name, ev.args || {})).replace(/^\u203A /, '')).catch(() => {});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
} catch {}
|
|
1469
|
+
}
|
|
1470
|
+
/* #14 paralel ajan: arka plan oturumu bitince ana sohbete özet bas.
|
|
1471
|
+
aborted (öz-kurtarma/wrap-up kick) turları BİTİŞ değildir — rapor basma */
|
|
1472
|
+
if (ev.type === 'done' && ev.sessionId && !ev.aborted) {
|
|
1473
|
+
try { engine.reportBackgroundDone(ev.sessionId).catch(() => {}); } catch {}
|
|
1474
|
+
}
|
|
1475
|
+
/* maliyet sayacı: tamamlanan her turda gerçek usage'ı işle */
|
|
1476
|
+
if (ev.type === 'done' && ev.usage && !ev.aborted) {
|
|
1477
|
+
try {
|
|
1478
|
+
usageMod.record({
|
|
1479
|
+
providerId: ev.meta ? ev.meta.providerId : null,
|
|
1480
|
+
model: ev.meta ? ev.meta.model : null,
|
|
1481
|
+
promptTokens: ev.usage.prompt_tokens || 0,
|
|
1482
|
+
completionTokens: ev.usage.completion_tokens || 0,
|
|
1483
|
+
costIn: ev.meta ? ev.meta.costIn : null,
|
|
1484
|
+
costOut: ev.meta ? ev.meta.costOut : null,
|
|
1485
|
+
});
|
|
1486
|
+
} catch {}
|
|
1487
|
+
}
|
|
1488
|
+
// WhatsApp oturumlarının son cevabını geri gönder (metin + opsiyonel ses)
|
|
1489
|
+
if ((ev.type === 'done' || ev.type === 'error') && wa && wa.connected) {
|
|
1490
|
+
const hit = [...waChats.entries()].find(([, s]) => s === ev.sessionId);
|
|
1491
|
+
if (hit) {
|
|
1492
|
+
const wajid = hit[0];
|
|
1493
|
+
(async () => {
|
|
1494
|
+
try {
|
|
1495
|
+
if (ev.type === 'error') {
|
|
1496
|
+
await sendWaSafe(wajid, 'Bir aksilik oldu: ' + String(ev.error || '').slice(0, 200));
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (!ev.aborted) {
|
|
1500
|
+
const s = engine.openSession(ev.sessionId);
|
|
1501
|
+
const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
|
|
1502
|
+
const txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
|
|
1503
|
+
if (txt.trim()) {
|
|
1504
|
+
const okOut = await sendWaSafe(wajid, txt);
|
|
1505
|
+
if (!okOut) {
|
|
1506
|
+
try { await wa.setComposing(wajid, false); } catch {}
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const voice = await synthesizeSpeech(txt);
|
|
1510
|
+
if (voice) await wa.sendAudio(wajid, voice).catch(() => {});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
} catch {}
|
|
1514
|
+
finally {
|
|
1515
|
+
try { await wa.setComposing(wajid, false); } catch {} // "yazıyor…" kapansın
|
|
1516
|
+
}
|
|
1517
|
+
})();
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
});
|
|
1522
|
+
return engine.publicState();
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const gotLock = app.requestSingleInstanceLock();
|
|
1526
|
+
|
|
1527
|
+
/* FALLOUT: çökme sonrası otomatik kurtarma — kayıtlı durumu bul,
|
|
1528
|
+
aynı oturuma "kaldığın yerden devam" görevi gönder. */
|
|
1529
|
+
let falloutResumed = false;
|
|
1530
|
+
function falloutResume() {
|
|
1531
|
+
if (falloutResumed) return;
|
|
1532
|
+
falloutResumed = true;
|
|
1533
|
+
try {
|
|
1534
|
+
const f = settings.fallout || {};
|
|
1535
|
+
if (!f.enabled || f.autoResume === false) return;
|
|
1536
|
+
const st = JSON.parse(fs.readFileSync(FALLOUT_CRASH_FILE, 'utf8'));
|
|
1537
|
+
if (!st || !st.sessionId) return;
|
|
1538
|
+
const exists = engine.listSessions().some((s) => s.id === st.sessionId);
|
|
1539
|
+
const sid = exists ? st.sessionId : engine.createSession().id;
|
|
1540
|
+
const when = st.at ? new Date(st.at).toLocaleString('tr-TR') : '?';
|
|
1541
|
+
setTimeout(() => {
|
|
1542
|
+
try {
|
|
1543
|
+
engine.send(sid, {
|
|
1544
|
+
text:
|
|
1545
|
+
`[FALLOUT KURTARMA] Önceki çalışma ${when}'de bir hata yüzünden yarıda kesildi ` +
|
|
1546
|
+
`(hata: ${String(st.error || '').slice(0, 160)}). ` +
|
|
1547
|
+
`Sohbet geçmişini incele ve kaldığın yerden devam et; yarım kalan işleri tamamla.`,
|
|
1548
|
+
});
|
|
1549
|
+
} catch {}
|
|
1550
|
+
}, 2500);
|
|
1551
|
+
} catch {}
|
|
1552
|
+
}
|
|
1553
|
+
/* "Nerede kaldım?" (#5): açılışta son oturumun durumu + yarım todolar.
|
|
1554
|
+
Ayarlardan kapatılabilir (settings.whereWasI.enabled). */
|
|
1555
|
+
function whereWasISummary() {
|
|
1556
|
+
try {
|
|
1557
|
+
const last = engine.lastWhereWasI();
|
|
1558
|
+
if (!last) return null;
|
|
1559
|
+
const lines = [];
|
|
1560
|
+
lines.push(`Son oturum: \`${last.code}\` — ${last.title}`);
|
|
1561
|
+
lines.push(`Tarih: ${last.updatedAt} · ${last.msgCount} mesaj`);
|
|
1562
|
+
if (last.lastAssistant) lines.push(`Kalınan nokta: ${last.lastAssistant}`);
|
|
1563
|
+
if (last.pendingTodos.length) {
|
|
1564
|
+
lines.push('');
|
|
1565
|
+
lines.push('Yarım kalan görevler:');
|
|
1566
|
+
for (const t of last.pendingTodos) {
|
|
1567
|
+
const mark = t.status === 'active' ? '▶' : '·';
|
|
1568
|
+
lines.push(`${mark} ${t.title}`);
|
|
1569
|
+
}
|
|
1570
|
+
if (last.doneCount) lines.push(`(${last.doneCount} görev zaten tamamlanmış)`);
|
|
1571
|
+
} else {
|
|
1572
|
+
lines.push('Bekleyen görev yok.');
|
|
1573
|
+
}
|
|
1574
|
+
return { text: lines.join('\n'), sessionId: last.sessionId };
|
|
1575
|
+
} catch {
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/* Açılış + 2 sn sonra: çıktı masaüstündeki o oturuma düşer (WA'ya sızmaz:
|
|
1581
|
+
done akışı yalnızca waChats'te eşleşen sid'lere gider ve bu yeni bir
|
|
1582
|
+
oturumsa eşleşmez; varsa da zararsız tekil özettir). */
|
|
1583
|
+
function maybeRunWhereWasI() {
|
|
1584
|
+
try {
|
|
1585
|
+
const cfg = settings.whereWasI || {};
|
|
1586
|
+
if (cfg.enabled === false) return;
|
|
1587
|
+
const sum = whereWasISummary();
|
|
1588
|
+
if (!sum) return;
|
|
1589
|
+
settingsLog(`nerede-kaldım: ${String(sum.text).split('\n')[0]}`);
|
|
1590
|
+
// kullanıcı görür diye konuya ek olarak sohbete de yansıt
|
|
1591
|
+
setTimeout(() => {
|
|
1592
|
+
try {
|
|
1593
|
+
engine.send(sum.sessionId, {
|
|
1594
|
+
text:
|
|
1595
|
+
`[NEREDE KALDIM ÖZETİ] Aşağıdaki özeti kullanıcıya kısaca göster:\n` +
|
|
1596
|
+
sum.text +
|
|
1597
|
+
`\n\n(Sadece bunu bildir; ek iş başlatma. Kullanıcı isterse kaldığın yerden devam et.)`,
|
|
1598
|
+
});
|
|
1599
|
+
} catch {}
|
|
1600
|
+
}, 2000);
|
|
1601
|
+
} catch {}
|
|
1602
|
+
}
|
|
1603
|
+
if (!gotLock) {
|
|
1604
|
+
app.quit();
|
|
1605
|
+
} else {
|
|
1606
|
+
app.on('second-instance', () => {
|
|
1607
|
+
if (win) {
|
|
1608
|
+
if (win.isMinimized()) win.restore();
|
|
1609
|
+
win.show();
|
|
1610
|
+
win.focus();
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
/* npm (global) kurulumda masaüstü kısayolu — yoksa bir kez oluşturulur.
|
|
1615
|
+
(NSIS packaged modda kısayolu electron-builder zaten yapar.) */
|
|
1616
|
+
function ensureDesktopShortcut() {
|
|
1617
|
+
try {
|
|
1618
|
+
const desktop = app.getPath('desktop');
|
|
1619
|
+
const lnk = path.join(desktop, 'Beast Agent.lnk');
|
|
1620
|
+
if (fs.existsSync(lnk)) return;
|
|
1621
|
+
const ok = shell.writeShortcutLink(lnk, 'create', {
|
|
1622
|
+
target: process.execPath,
|
|
1623
|
+
args: app.getAppPath(),
|
|
1624
|
+
cwd: app.getAppPath(),
|
|
1625
|
+
description: 'Beast Agent — hızlı, hafif ve becerikli',
|
|
1626
|
+
icon: process.execPath,
|
|
1627
|
+
iconIndex: 0,
|
|
1628
|
+
});
|
|
1629
|
+
log.info('main', ok ? 'Masaüstü kısayolu oluşturuldu (npm modu)' : 'Masaüstü kısayolu oluşturulamadı');
|
|
1630
|
+
} catch (e) {
|
|
1631
|
+
log.info('main', 'Kısayol hatası: ' + String((e && e.message) || e));
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
app.whenReady().then(() => {
|
|
1636
|
+
// Tailscale modu: paketli uygulamada Windows ile otomatik başlat (sessiz, tepside)
|
|
1637
|
+
if (app.isPackaged) {
|
|
1638
|
+
try {
|
|
1639
|
+
// portable exe Temp'e açılır; gerçek yolu PORTABLE_EXECUTABLE_FILE verir
|
|
1640
|
+
const exe = process.env.PORTABLE_EXECUTABLE_FILE || process.execPath;
|
|
1641
|
+
app.setLoginItemSettings({
|
|
1642
|
+
openAtLogin: true,
|
|
1643
|
+
path: exe,
|
|
1644
|
+
args: ['--hidden'],
|
|
1645
|
+
});
|
|
1646
|
+
log.info('main', `Startup kaydı açık: ${exe} (--hidden, açılışta tepside)`);
|
|
1647
|
+
} catch (e) {
|
|
1648
|
+
log.error('main', 'Startup kaydı başarısız: ' + String((e && e.message) || e));
|
|
1649
|
+
}
|
|
1650
|
+
} else if (!app.isPackaged && /node_modules[\\/]beast-agent/i.test(String(app.getAppPath()))) {
|
|
1651
|
+
/* npm (global) kurulum modu: startup kaydı + masaüstü kısayolu */
|
|
1652
|
+
try {
|
|
1653
|
+
app.setLoginItemSettings({
|
|
1654
|
+
openAtLogin: true,
|
|
1655
|
+
path: process.execPath,
|
|
1656
|
+
args: [app.getAppPath(), '--hidden'],
|
|
1657
|
+
});
|
|
1658
|
+
log.info('main', 'Startup kaydı açık (npm modu): ' + app.getAppPath());
|
|
1659
|
+
} catch (e) {
|
|
1660
|
+
log.error('main', 'Startup kaydı (npm) başarısız: ' + String((e && e.message) || e));
|
|
1661
|
+
}
|
|
1662
|
+
ensureDesktopShortcut();
|
|
1663
|
+
}
|
|
1664
|
+
reloadBackend();
|
|
1665
|
+
createWindow();
|
|
1666
|
+
log.info('main', 'Beast Agent başlatıldı');
|
|
1667
|
+
createTray();
|
|
1668
|
+
cron.init({ onFire: cronFire });
|
|
1669
|
+
watchers.start({ onTrigger: watcherFire });
|
|
1670
|
+
startEventBus();
|
|
1671
|
+
maybeRunWhereWasI();
|
|
1672
|
+
falloutResume();
|
|
1673
|
+
startAutoUpdater(); // #3 sessiz güncelleme
|
|
1674
|
+
|
|
1675
|
+
// #12 STT prefetch: whisper modelini arka planda hazırla (ilk sesli mesajda bekleme olmasın)
|
|
1676
|
+
if (settings.sttPrefetch !== false) {
|
|
1677
|
+
setTimeout(() => {
|
|
1678
|
+
ensureStt()
|
|
1679
|
+
.then(() => waLog('STT prefetch tamam'))
|
|
1680
|
+
.catch((e) => waLog('STT prefetch atlandı: ' + String((e && e.message) || e)));
|
|
1681
|
+
}, 10000);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
|
|
1685
|
+
ensureWa().start().catch((e) => waLog('autostart failed: ' + (e && e.message)));
|
|
1686
|
+
|
|
1687
|
+
app.on('activate', () => {
|
|
1688
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
app.on('before-quit', () => {
|
|
1692
|
+
app.isQuitting = true;
|
|
1693
|
+
flushBrowserStorage(); // x.com/google oturumları (cookies) diske yazılsın
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
if (process.argv.includes('--smoke')) {
|
|
1697
|
+
win.webContents.on('did-finish-load', () => setTimeout(() => app.exit(0), 1500));
|
|
1698
|
+
setTimeout(() => app.exit(1), 15000);
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function createTray() {
|
|
1704
|
+
try {
|
|
1705
|
+
const icon = nativeImage.createFromPath(path.join(__dirname, '..', 'assets', 'tray.png'));
|
|
1706
|
+
tray = new Tray(icon);
|
|
1707
|
+
tray.setToolTip('Beast Agent — arka planda çalışıyor');
|
|
1708
|
+
const menu = Menu.buildFromTemplate([
|
|
1709
|
+
{ label: 'Beast\'i Göster', click: () => showWin() },
|
|
1710
|
+
{ type: 'separator' },
|
|
1711
|
+
{
|
|
1712
|
+
label: 'Tamamen Kapat',
|
|
1713
|
+
click: () => {
|
|
1714
|
+
app.isQuitting = true;
|
|
1715
|
+
app.quit();
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
]);
|
|
1719
|
+
tray.setContextMenu(menu);
|
|
1720
|
+
tray.on('double-click', () => showWin());
|
|
1721
|
+
} catch {}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function showWin() {
|
|
1725
|
+
if (!win || win.isDestroyed()) createWindow();
|
|
1726
|
+
else {
|
|
1727
|
+
win.show();
|
|
1728
|
+
win.focus();
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/* ---------------- dahili tarayıcı ---------------- */
|
|
1733
|
+
|
|
1734
|
+
const BROWSER_TOOLBAR_H = 46;
|
|
1735
|
+
const BROWSER_START_URL = 'https://www.google.com/';
|
|
1736
|
+
/* Tarayıcı state: visible=false → ajanlar GİZLİ kullanır (headless);
|
|
1737
|
+
göz ikonuyla görünür mod açılır. open=view aktif, visible=panelde görünürlük */
|
|
1738
|
+
const browser = { view: null, open: false, visible: false, width: 0, attached: false, started: false };
|
|
1739
|
+
|
|
1740
|
+
function browserEmit(payload) {
|
|
1741
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'browser', visible: browser.visible, ...payload });
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function browserWidthFor(w) {
|
|
1745
|
+
return Math.max(380, Math.min(800, Math.floor(w * 0.46)));
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function layoutBrowser() {
|
|
1749
|
+
if (!win || win.isDestroyed()) return;
|
|
1750
|
+
const [w, h] = win.getContentSize();
|
|
1751
|
+
/* GENİŞLİĞİ KALICI OLARAK EZME: kullanıcı tercih edilen genişliği korunsun.
|
|
1752
|
+
Küçük pencerede sadece GÖRÜNÜM kırpılır; büyüyünce tercih geri gelir.
|
|
1753
|
+
Tercih settings.json'da saklanır — uygulama kapansın/açılsın kaybolmaz. */
|
|
1754
|
+
if (!browser.width) {
|
|
1755
|
+
const saved = Math.round(Number(settings.browserWidth) || 0);
|
|
1756
|
+
browser.width = saved >= 300 ? saved : browserWidthFor(w);
|
|
1757
|
+
}
|
|
1758
|
+
const shownWidth = Math.min(browser.width, Math.max(320, w - 320));
|
|
1759
|
+
if (!browser.view || !browser.open) return;
|
|
1760
|
+
const view = browser.view;
|
|
1761
|
+
if (!browser.attached) {
|
|
1762
|
+
try {
|
|
1763
|
+
win.contentView.addChildView(view);
|
|
1764
|
+
browser.attached = true;
|
|
1765
|
+
} catch {}
|
|
1766
|
+
}
|
|
1767
|
+
try {
|
|
1768
|
+
view.setBounds({ x: Math.max(0, w - shownWidth), y: BROWSER_TOOLBAR_H, width: shownWidth, height: Math.max(0, h - BROWSER_TOOLBAR_H) });
|
|
1769
|
+
view.setVisible(browser.open && browser.visible);
|
|
1770
|
+
} catch {}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function ensureBrowser() {
|
|
1774
|
+
if (browser.view) return browser.view;
|
|
1775
|
+
const view = new WebContentsView({
|
|
1776
|
+
webPreferences: {
|
|
1777
|
+
partition: 'persist:browser',
|
|
1778
|
+
contextIsolation: true,
|
|
1779
|
+
nodeIntegration: false,
|
|
1780
|
+
sandbox: true,
|
|
1781
|
+
spellcheck: false,
|
|
1782
|
+
backgroundThrottling: false, // gizli (headless) kullanımda da tam hız
|
|
1783
|
+
preload: path.join(__dirname, 'renderer', 'browserPreload.js'),
|
|
1784
|
+
},
|
|
1785
|
+
});
|
|
1786
|
+
view.setBackgroundColor(settings.theme === 'dark' ? '#0d0d0f' : '#ffffff');
|
|
1787
|
+
const wc = view.webContents;
|
|
1788
|
+
/* Google (ve bazı siteler) Electron UA'sını "güvenli olmayan tarayıcı / bilinmeyen
|
|
1789
|
+
cihaz" diyerek girişi engeller. Motor zaten aynı Chromium — gerçek Chrome
|
|
1790
|
+
kimliği takınıyoruz; böylece Google/X oturum açma sorunsuz çalışır. */
|
|
1791
|
+
try {
|
|
1792
|
+
const chromeUA = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome} Safari/537.36`;
|
|
1793
|
+
const bses = session.fromPartition('persist:browser');
|
|
1794
|
+
if (bses && bses.setUserAgent) bses.setUserAgent(chromeUA, 'tr-TR,tr;q=0.9,en;q=0.8');
|
|
1795
|
+
wc.setUserAgent(chromeUA);
|
|
1796
|
+
} catch {}
|
|
1797
|
+
wc.setWindowOpenHandler(({ url }) => {
|
|
1798
|
+
if (/^https?:\/\//i.test(url)) wc.loadURL(url).catch(() => {});
|
|
1799
|
+
return { action: 'deny' };
|
|
1800
|
+
});
|
|
1801
|
+
// Tarayıcı kapalıyken olayların UI'ı geri açmamasını garantile
|
|
1802
|
+
const notify = (extra) => {
|
|
1803
|
+
if (browser.open && win && !win.isDestroyed()) {
|
|
1804
|
+
browserEmit({ open: true, width: browser.width, ...extra });
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
wc.on('did-navigate', (_e, url) => notify({ url, loading: false }));
|
|
1808
|
+
wc.on('did-navigate-in-page', (_e, url) => notify({ url, loading: false }));
|
|
1809
|
+
wc.on('did-start-loading', () => notify({ loading: true }));
|
|
1810
|
+
wc.on('did-stop-loading', () => {
|
|
1811
|
+
let url = '';
|
|
1812
|
+
try { url = wc.getURL(); } catch {}
|
|
1813
|
+
notify({ url, loading: false });
|
|
1814
|
+
});
|
|
1815
|
+
wc.on('render-process-gone', () => {
|
|
1816
|
+
detachBrowser();
|
|
1817
|
+
browser.view = null;
|
|
1818
|
+
});
|
|
1819
|
+
browser.view = view;
|
|
1820
|
+
return view;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/* View'i her koşulda sök — bayrak ne olursa olsun */
|
|
1824
|
+
function detachBrowser() {
|
|
1825
|
+
const view = browser.view;
|
|
1826
|
+
if (view) {
|
|
1827
|
+
try { view.setVisible(false); } catch {}
|
|
1828
|
+
if (win && !win.isDestroyed()) {
|
|
1829
|
+
try { win.contentView.removeChildView(view); } catch {}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
browser.attached = false;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
function setBrowserOpen(v, forceVisible) {
|
|
1836
|
+
if (!win || win.isDestroyed()) return;
|
|
1837
|
+
const want = !!v;
|
|
1838
|
+
|
|
1839
|
+
// KAPATMA: bayrak desync olsa bile view'i zorla sök
|
|
1840
|
+
if (!want) {
|
|
1841
|
+
const wasOpen = browser.open;
|
|
1842
|
+
browser.open = false;
|
|
1843
|
+
browser.visible = false;
|
|
1844
|
+
detachBrowser();
|
|
1845
|
+
browserEmit({ open: false });
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// AÇMA — görünürlük: kullanıcı kendi açtıysa (forceVisible) MUTLAKA görünür;
|
|
1850
|
+
// ajan açtıysa göz ikonu tercihine bak (varsayılan: GİZLİ/headless)
|
|
1851
|
+
browser.open = true;
|
|
1852
|
+
browser.visible = forceVisible === true ? true : settings.browserHeadless === false;
|
|
1853
|
+
ensureBrowser();
|
|
1854
|
+
if (!browser.started) {
|
|
1855
|
+
browser.started = true;
|
|
1856
|
+
browser.view.webContents.loadURL(BROWSER_START_URL).catch(() => {});
|
|
1857
|
+
}
|
|
1858
|
+
layoutBrowser();
|
|
1859
|
+
let url = '';
|
|
1860
|
+
try { url = browser.view.webContents.getURL(); } catch {}
|
|
1861
|
+
browserEmit({ open: true, width: browser.width, url });
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
async function browserNavigate(raw, signal) {
|
|
1865
|
+
let url = String(raw || '').trim();
|
|
1866
|
+
if (!url) return { ok: false, error: 'boş adres' };
|
|
1867
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
1868
|
+
// kelime ise arama, alan adıysa doğrudan aç
|
|
1869
|
+
url = /^[\w-]+(\.[\w-]+)+(:\d+)?(\/.*)?$/.test(url)
|
|
1870
|
+
? 'https://' + url
|
|
1871
|
+
: 'https://duckduckgo.com/?q=' + encodeURIComponent(url);
|
|
1872
|
+
}
|
|
1873
|
+
setBrowserOpen(true);
|
|
1874
|
+
const wc = browser.view.webContents;
|
|
1875
|
+
await new Promise((resolve) => {
|
|
1876
|
+
let settled = false;
|
|
1877
|
+
const finish = () => { settled = true; clearTimeout(timer); cleanup(); resolve(); };
|
|
1878
|
+
const timer = setTimeout(finish, 25000);
|
|
1879
|
+
const onFail = (_e, code, desc) => { if (code !== -3 && !settled) finish(); };
|
|
1880
|
+
function cleanup() {
|
|
1881
|
+
clearTimeout(timer);
|
|
1882
|
+
wc.removeListener('did-finish-load', finish);
|
|
1883
|
+
wc.removeListener('did-fail-load', onFail);
|
|
1884
|
+
}
|
|
1885
|
+
wc.once('did-finish-load', finish);
|
|
1886
|
+
wc.on('did-fail-load', onFail);
|
|
1887
|
+
wc.loadURL(url).catch(() => {});
|
|
1888
|
+
});
|
|
1889
|
+
let title = '';
|
|
1890
|
+
let finalUrl = url;
|
|
1891
|
+
try { title = wc.getTitle(); finalUrl = wc.getURL() || url; } catch {}
|
|
1892
|
+
browserEmit({ open: true, width: browser.width, url: finalUrl });
|
|
1893
|
+
flushBrowserStorage(); // oturum çerezleri diske — ani kapanışta kaybolmasın
|
|
1894
|
+
return { ok: true, url: finalUrl, title };
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
async function browserRead(signal) {
|
|
1898
|
+
if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
|
|
1899
|
+
const wc = browser.view.webContents;
|
|
1900
|
+
try {
|
|
1901
|
+
const txt = await wc.executeJavaScript('(document.body&&document.body.innerText)||""', true);
|
|
1902
|
+
const t = String(txt || '').replace(/\n{3,}/g, '\n\n').trim();
|
|
1903
|
+
const url = wc.getURL();
|
|
1904
|
+
if (!t) return { ok: false, error: 'sayfa metni boş', url };
|
|
1905
|
+
const cap = 9000;
|
|
1906
|
+
return { ok: true, url, title: wc.getTitle(), truncated: t.length > cap, content: t.slice(0, cap) };
|
|
1907
|
+
} catch (e) {
|
|
1908
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/* Oturum kalıcılığı: persist:browser bölümü cookies/localStorage'i diske yazar;
|
|
1913
|
+
ani app.exit() yollarında flush atlanmasın diye elle tetiklenir */
|
|
1914
|
+
function flushBrowserStorage() {
|
|
1915
|
+
try {
|
|
1916
|
+
const s = session.fromPartition('persist:browser');
|
|
1917
|
+
if (s && s.flushStorageData) s.flushStorageData();
|
|
1918
|
+
} catch {}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/* Dahili OCR: görsel desteklemeyen modeller için tesseract.js ile metin okuma.
|
|
1922
|
+
Dil verisi ilk kullanımda %APPDATA%\beast\tessdata'ya iner, sonra offline çalışır. */
|
|
1923
|
+
const _ocrWorkers = new Map(); // lang -> worker (her çağrıda yeniden init olmasın)
|
|
1924
|
+
async function ocrRead({ image, lang = 'tur+eng' } = {}) {
|
|
1925
|
+
try {
|
|
1926
|
+
if (!image) return { ok: false, error: 'görüntü yok' };
|
|
1927
|
+
const t = require('tesseract.js');
|
|
1928
|
+
const langKey = String(lang || 'tur+eng');
|
|
1929
|
+
let worker = _ocrWorkers.get(langKey);
|
|
1930
|
+
if (!worker) {
|
|
1931
|
+
const tessDir = path.join(APP_DIR, 'tessdata');
|
|
1932
|
+
fs.mkdirSync(tessDir, { recursive: true });
|
|
1933
|
+
worker = await t.createWorker(langKey, 1, { cachePath: tessDir, logger: () => {} });
|
|
1934
|
+
_ocrWorkers.set(langKey, worker);
|
|
1935
|
+
}
|
|
1936
|
+
let input = image;
|
|
1937
|
+
if (typeof input === 'string' && input.startsWith('data:')) {
|
|
1938
|
+
input = Buffer.from(input.split(',')[1] || '', 'base64');
|
|
1939
|
+
}
|
|
1940
|
+
const { data } = await worker.recognize(input);
|
|
1941
|
+
const text = String((data && data.text) || '')
|
|
1942
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
1943
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
1944
|
+
.trim();
|
|
1945
|
+
return { ok: !!text, chars: text.length, text: text.slice(0, 8000), lang: langKey };
|
|
1946
|
+
} catch (e) {
|
|
1947
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
/* Dahili tarayıcıyla arama (web_search'ün İLK adımı): gerçek Chromium +
|
|
1952
|
+
gerçek cookie'ler olduğu için Google bot koruması uygulamaz.
|
|
1953
|
+
aep=1 → AI Modu/AI Overview açık: Google cevabi hazırlanmış şekilde döner,
|
|
1954
|
+
hem AI metni ('ai' alanı) hem kaynak linkleri çekilir.
|
|
1955
|
+
Numara: view'i google.com'a bir kez açıp aramaları SAYFA İÇİ fetch() ile
|
|
1956
|
+
yapmak (kullanıcının console'da yaptığı gibi) — sayfa bile değişmeden
|
|
1957
|
+
DOMParser ile sonuç çekilir. Fetch olmazsa direk gezinme fallback'i var. */
|
|
1958
|
+
async function browserSearch(query, signal) {
|
|
1959
|
+
try {
|
|
1960
|
+
if (!win || win.isDestroyed()) return null;
|
|
1961
|
+
if (signal && signal.aborted) return null;
|
|
1962
|
+
const q = String(query || '').trim();
|
|
1963
|
+
if (!q) return null;
|
|
1964
|
+
setBrowserOpen(true);
|
|
1965
|
+
const wc = browser.view && browser.view.webContents;
|
|
1966
|
+
if (!wc) return null;
|
|
1967
|
+
|
|
1968
|
+
/* fetch() same-origin olsun diye önce google.com kökü yüklü olsun */
|
|
1969
|
+
let cur = '';
|
|
1970
|
+
try { cur = wc.getURL() || ''; } catch {}
|
|
1971
|
+
if (!cur.startsWith('https://www.google.com')) {
|
|
1972
|
+
await browserNavigate('https://www.google.com/', signal);
|
|
1973
|
+
}
|
|
1974
|
+
if (!browser.view || !browser.open) return null;
|
|
1975
|
+
|
|
1976
|
+
const fetchJs = `(async () => {
|
|
1977
|
+
const q = ${JSON.stringify(q)};
|
|
1978
|
+
const AI_SEL = ['#m-x-content', '[data-attrid="wa:/ai/action"]', 'div.WaaZC', '[data-mcpr]', 'div[data-iap]'];
|
|
1979
|
+
const squash = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
|
|
1980
|
+
const aiText = (doc) => {
|
|
1981
|
+
for (const s of AI_SEL) {
|
|
1982
|
+
const el = doc.querySelector(s);
|
|
1983
|
+
const t = squash(el && el.textContent);
|
|
1984
|
+
if (t.length > 80) return t.slice(0, 3500);
|
|
1985
|
+
}
|
|
1986
|
+
const first = doc.querySelector('#rso > div, #search > div');
|
|
1987
|
+
if (first && !first.querySelector('h3')) {
|
|
1988
|
+
const t = squash(first.textContent);
|
|
1989
|
+
if (t.length > 120) return t.slice(0, 3500);
|
|
1990
|
+
}
|
|
1991
|
+
return '';
|
|
1992
|
+
};
|
|
1993
|
+
const extract = (doc) => {
|
|
1994
|
+
const out = [];
|
|
1995
|
+
const seen = new Set();
|
|
1996
|
+
const nodes = doc.querySelectorAll('#search div.g, #rso > div, #search div[data-sokoban-container], div.g');
|
|
1997
|
+
for (const el of nodes) {
|
|
1998
|
+
if (out.length >= 10) break;
|
|
1999
|
+
const h3 = el.querySelector('a[href] h3');
|
|
2000
|
+
if (!h3 || !h3.parentElement) continue;
|
|
2001
|
+
const link = h3.parentElement;
|
|
2002
|
+
let url = link.getAttribute('href') || '';
|
|
2003
|
+
const title = (h3.textContent || '').trim();
|
|
2004
|
+
if (!title || !url) continue;
|
|
2005
|
+
try { url = new URL(url, 'https://www.google.com').toString(); } catch (e) { continue; }
|
|
2006
|
+
if (!url.startsWith('http')) continue;
|
|
2007
|
+
if (url.includes('google.com/url?')) {
|
|
2008
|
+
try { url = new URL(url).searchParams.get('q') || url; } catch (e) {}
|
|
2009
|
+
}
|
|
2010
|
+
const key = url.split('#')[0];
|
|
2011
|
+
if (!key || seen.has(key)) continue;
|
|
2012
|
+
seen.add(key);
|
|
2013
|
+
const sn = el.querySelector('.VwiC3b, div[data-sncf], .IsZvec');
|
|
2014
|
+
out.push({ title, url, snippet: squash(sn && sn.textContent).slice(0, 300), engine: 'browser-google' });
|
|
2015
|
+
}
|
|
2016
|
+
return out;
|
|
2017
|
+
};
|
|
2018
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2019
|
+
for (let i = 0; i < 3; i++) {
|
|
2020
|
+
try {
|
|
2021
|
+
const res = await fetch('/search?q=' + encodeURIComponent(q) + '&num=10&hl=tr&pws=0&aep=1', { credentials: 'include' });
|
|
2022
|
+
if (res.ok) {
|
|
2023
|
+
const html = await res.text();
|
|
2024
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
2025
|
+
const rows = extract(doc);
|
|
2026
|
+
const ai = aiText(doc);
|
|
2027
|
+
if (rows.length || ai) return JSON.stringify({ ai, rows });
|
|
2028
|
+
}
|
|
2029
|
+
} catch (e) {}
|
|
2030
|
+
await sleep(600);
|
|
2031
|
+
}
|
|
2032
|
+
return JSON.stringify({ ai: '', rows: [] });
|
|
2033
|
+
})()`;
|
|
2034
|
+
let raw = '';
|
|
2035
|
+
try { raw = await wc.executeJavaScript(fetchJs, true); } catch {}
|
|
2036
|
+
let parsed = { ai: '', rows: [] };
|
|
2037
|
+
try { parsed = JSON.parse(raw || '{}'); } catch {}
|
|
2038
|
+
let results = parsed.rows || [];
|
|
2039
|
+
|
|
2040
|
+
/* fallback: fetch yerine direk gezinme + DOM'dan çek */
|
|
2041
|
+
if (!results.length) {
|
|
2042
|
+
await browserNavigate(
|
|
2043
|
+
'https://www.google.com/search?q=' + encodeURIComponent(q) + '&num=10&hl=tr&pws=0&aep=1',
|
|
2044
|
+
signal
|
|
2045
|
+
);
|
|
2046
|
+
const navJs = `(async () => {
|
|
2047
|
+
const AI_SEL = ['#m-x-content', '[data-attrid="wa:/ai/action"]', 'div.WaaZC', '[data-mcpr]', 'div[data-iap]'];
|
|
2048
|
+
const squash = (s) => String(s || '').replace(/\\s+/g, ' ').trim();
|
|
2049
|
+
const aiText = () => {
|
|
2050
|
+
for (const s of AI_SEL) {
|
|
2051
|
+
const el = document.querySelector(s);
|
|
2052
|
+
const t = squash(el && el.textContent);
|
|
2053
|
+
if (t.length > 80) return t.slice(0, 3500);
|
|
2054
|
+
}
|
|
2055
|
+
const first = document.querySelector('#rso > div, #search > div');
|
|
2056
|
+
if (first && !first.querySelector('h3')) {
|
|
2057
|
+
const t = squash(first.textContent);
|
|
2058
|
+
if (t.length > 120) return t.slice(0, 3500);
|
|
2059
|
+
}
|
|
2060
|
+
return '';
|
|
2061
|
+
};
|
|
2062
|
+
const extract = () => {
|
|
2063
|
+
const out = [];
|
|
2064
|
+
const seen = new Set();
|
|
2065
|
+
const nodes = document.querySelectorAll('#search div.g, #rso > div, #search div[data-sokoban-container], div.g');
|
|
2066
|
+
for (const el of nodes) {
|
|
2067
|
+
if (out.length >= 10) break;
|
|
2068
|
+
const h3 = el.querySelector('a[href^="http"] h3');
|
|
2069
|
+
if (!h3 || !h3.parentElement) continue;
|
|
2070
|
+
const link = h3.parentElement;
|
|
2071
|
+
let url = link.href || '';
|
|
2072
|
+
const title = (h3.textContent || '').trim();
|
|
2073
|
+
if (!title || !url.startsWith('http')) continue;
|
|
2074
|
+
if (url.includes('google.com/url?')) {
|
|
2075
|
+
try { url = new URL(url).searchParams.get('q') || url; } catch (e) {}
|
|
2076
|
+
}
|
|
2077
|
+
const key = url.split('#')[0];
|
|
2078
|
+
if (!key || seen.has(key)) continue;
|
|
2079
|
+
seen.add(key);
|
|
2080
|
+
const sn = el.querySelector('.VwiC3b, div[data-sncf], .IsZvec');
|
|
2081
|
+
out.push({ title, url, snippet: squash(sn && sn.textContent).slice(0, 300), engine: 'browser-google' });
|
|
2082
|
+
}
|
|
2083
|
+
return out;
|
|
2084
|
+
};
|
|
2085
|
+
for (let i = 0; i < 8; i++) {
|
|
2086
|
+
const rows = extract();
|
|
2087
|
+
const ai = aiText();
|
|
2088
|
+
if (rows.length || ai) return JSON.stringify({ ai, rows });
|
|
2089
|
+
await new Promise((res) => setTimeout(res, 700));
|
|
2090
|
+
}
|
|
2091
|
+
return JSON.stringify({ ai: '', rows: [] });
|
|
2092
|
+
})()`;
|
|
2093
|
+
try { raw = await wc.executeJavaScript(navJs, true); } catch {}
|
|
2094
|
+
try { parsed = JSON.parse(raw || '{}'); } catch {}
|
|
2095
|
+
results = parsed.rows || [];
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (!results.length && !parsed.ai) return null;
|
|
2099
|
+
flushBrowserStorage();
|
|
2100
|
+
const out = { ok: true, engine: 'browser-google', query: q, results };
|
|
2101
|
+
if (parsed.ai) out.ai = parsed.ai;
|
|
2102
|
+
return out;
|
|
2103
|
+
} catch {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
async function browserScreenshot(signal) {
|
|
2109
|
+
if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
|
|
2110
|
+
const wc = browser.view.webContents;
|
|
2111
|
+
try {
|
|
2112
|
+
const img = await wc.capturePage();
|
|
2113
|
+
let out = img;
|
|
2114
|
+
const sz = img.getSize();
|
|
2115
|
+
const maxW = 1000; // vision bütçesi için küçült
|
|
2116
|
+
if (sz.width > maxW) out = img.resize({ width: maxW });
|
|
2117
|
+
const jpeg = out.toJPEG(72);
|
|
2118
|
+
if (!jpeg || !jpeg.length) return { ok: false, error: 'görüntü alınamadı' };
|
|
2119
|
+
return {
|
|
2120
|
+
ok: true,
|
|
2121
|
+
note: 'ekran görüntüsü bir sonraki adımda sana gösterilecek',
|
|
2122
|
+
__injectImage: 'data:image/jpeg;base64,' + jpeg.toString('base64'),
|
|
2123
|
+
url: wc.getURL(),
|
|
2124
|
+
title: wc.getTitle(),
|
|
2125
|
+
bytes: jpeg.length,
|
|
2126
|
+
};
|
|
2127
|
+
} catch (e) {
|
|
2128
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
const BROWSER_JS_HELPERS = `
|
|
2133
|
+
function __vis(el){const r=el.getBoundingClientRect();const s=getComputedStyle(el);return r.width>0&&r.height>0&&s.visibility!=='hidden'&&s.display!=='none';}
|
|
2134
|
+
function __label(e){
|
|
2135
|
+
const al=e.getAttribute&&(e.getAttribute('aria-label')||e.getAttribute('title')||e.getAttribute('placeholder'));
|
|
2136
|
+
const txt=(e.innerText||e.value||'').trim().replace(/\\s+/g,' ');
|
|
2137
|
+
return String(al||txt||'').slice(0,60);
|
|
2138
|
+
}
|
|
2139
|
+
function __resolve(sel){
|
|
2140
|
+
if(!sel) return null;
|
|
2141
|
+
if(sel.indexOf('text=')===0){
|
|
2142
|
+
const t=sel.slice(5).trim().toLowerCase();
|
|
2143
|
+
const els=[...document.querySelectorAll('a,button,[role="button"],input[type="submit"],input[type="button"],summary,label,li,td,th,span,div,p,h1,h2,h3,h4')];
|
|
2144
|
+
let el=els.find(e=>__vis(e)&&e.innerText&&e.innerText.trim().toLowerCase()===t)
|
|
2145
|
+
||els.find(e=>__vis(e)&&e.innerText&&e.innerText.trim().toLowerCase().includes(t));
|
|
2146
|
+
if(el) el=el.closest('a,button,[role="button"],summary,label')||el;
|
|
2147
|
+
return el||null;
|
|
2148
|
+
}
|
|
2149
|
+
return document.querySelector(sel);
|
|
2150
|
+
}
|
|
2151
|
+
function __resolveRef(n){
|
|
2152
|
+
n=Number(n);
|
|
2153
|
+
const m=window.__beMap;
|
|
2154
|
+
if(!m||!m[n]) return null;
|
|
2155
|
+
const el=m[n];
|
|
2156
|
+
return (el&&el.isConnected)?el:null;
|
|
2157
|
+
}
|
|
2158
|
+
`;
|
|
2159
|
+
|
|
2160
|
+
const BROWSER_SNAPSHOT_JS = `(function(){
|
|
2161
|
+
${BROWSER_JS_HELPERS}
|
|
2162
|
+
const sel='a[href],button,input:not([type="hidden"]),textarea,select,[role="button"],[role="link"],[role="tab"],[contenteditable="true"],summary';
|
|
2163
|
+
const els=[...document.querySelectorAll(sel)];
|
|
2164
|
+
window.__beMap={};
|
|
2165
|
+
const lines=[];
|
|
2166
|
+
let i=0;
|
|
2167
|
+
for(const e of els){
|
|
2168
|
+
if(!__vis(e)) continue;
|
|
2169
|
+
if(e.disabled||e.getAttribute('aria-disabled')==='true') continue;
|
|
2170
|
+
i++;
|
|
2171
|
+
window.__beMap[i]=e;
|
|
2172
|
+
const tag=e.tagName.toLowerCase();
|
|
2173
|
+
const type=e.getAttribute('type');
|
|
2174
|
+
let s='['+i+'] <'+tag+(type?' type='+type:'')+'>';
|
|
2175
|
+
const l=__label(e);
|
|
2176
|
+
if(l) s+=' "'+l+'"';
|
|
2177
|
+
if(tag==='input'&&(type==='text'||type==='email'||type==='password'||type==='search'||type==='tel'||type==='url'||type==='number')&&e.value) s+=' deger="'+String(e.value).slice(0,30)+'"';
|
|
2178
|
+
if(tag==='select'){s+=' secenekler=['+[...e.options].slice(0,6).map(o=>o.text.trim()).filter(Boolean).join('|').slice(0,60)+']';}
|
|
2179
|
+
lines.push(s);
|
|
2180
|
+
if(i>=100) break;
|
|
2181
|
+
}
|
|
2182
|
+
return JSON.stringify({count:i,title:document.title,url:location.href,snapshot:lines.join('\\n')});
|
|
2183
|
+
})()`;
|
|
2184
|
+
|
|
2185
|
+
function browserActionJs(kind, args) {
|
|
2186
|
+
const sel = JSON.stringify(String(args.selector || ''));
|
|
2187
|
+
const ref = JSON.stringify(args.ref === undefined ? null : Number(args.ref));
|
|
2188
|
+
|
|
2189
|
+
const resolveTarget = `
|
|
2190
|
+
function __target(){
|
|
2191
|
+
if(${ref}!==null){
|
|
2192
|
+
const el=__resolveRef(${ref});
|
|
2193
|
+
return el?{el,how:'ref['+${ref}+']'}:null;
|
|
2194
|
+
}
|
|
2195
|
+
const el=__resolve(${sel});
|
|
2196
|
+
return el?{el,how:'selector'}:null;
|
|
2197
|
+
}`;
|
|
2198
|
+
|
|
2199
|
+
if (kind === 'click') {
|
|
2200
|
+
return `(function(){${BROWSER_JS_HELPERS}${resolveTarget};return new Promise((res)=>{try{
|
|
2201
|
+
const t=__target();
|
|
2202
|
+
if(!t) return res(JSON.stringify({clicked:false,reason:'eleman bulunamadi (ref eski olabilir - browser_snapshot al)'}));
|
|
2203
|
+
const how=t.how,el=t.el;
|
|
2204
|
+
el.scrollIntoView({block:'center'});
|
|
2205
|
+
setTimeout(()=>{try{
|
|
2206
|
+
el.click();
|
|
2207
|
+
res(JSON.stringify({clicked:true,target:how+' <'+el.tagName.toLowerCase()+'> "'+__label(el)+'"'}));
|
|
2208
|
+
}catch(e){res(JSON.stringify({clicked:false,reason:String(e)}));}},80);
|
|
2209
|
+
}catch(e){res(JSON.stringify({clicked:false,reason:String(e)}));}});})()`;
|
|
2210
|
+
}
|
|
2211
|
+
if (kind === 'type') {
|
|
2212
|
+
const text = JSON.stringify(String(args.text ?? ''));
|
|
2213
|
+
const submit = args.submit ? 'true' : 'false';
|
|
2214
|
+
return `(function(){${BROWSER_JS_HELPERS}${resolveTarget};return new Promise((res)=>{try{
|
|
2215
|
+
const t=__target();
|
|
2216
|
+
if(!t) return res(JSON.stringify({typed:false,reason:'eleman bulunamadi (ref eski olabilir - browser_snapshot al)'}));
|
|
2217
|
+
const how=t.how,el=t.el;
|
|
2218
|
+
el.scrollIntoView({block:'center'});el.focus();
|
|
2219
|
+
setTimeout(()=>{try{
|
|
2220
|
+
if(el.isContentEditable){el.textContent=${text};}
|
|
2221
|
+
else{
|
|
2222
|
+
const proto=el.tagName==='TEXTAREA'?HTMLTextAreaElement.prototype:HTMLInputElement.prototype;
|
|
2223
|
+
const setter=Object.getOwnPropertyDescriptor(proto,'value').set;
|
|
2224
|
+
setter.call(el,${text});
|
|
2225
|
+
el.dispatchEvent(new Event('input',{bubbles:true}));
|
|
2226
|
+
el.dispatchEvent(new Event('change',{bubbles:true}));
|
|
2227
|
+
}
|
|
2228
|
+
const done=()=>res(JSON.stringify({typed:true,target:how+' <'+el.tagName.toLowerCase()+'>',value:${text}}));
|
|
2229
|
+
if(${submit}){
|
|
2230
|
+
el.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true,cancelable:true}));
|
|
2231
|
+
const form=el.closest('form');
|
|
2232
|
+
if(form&&typeof form.requestSubmit==='function'){setTimeout(()=>{try{form.requestSubmit();}catch(e){}done();},40);}
|
|
2233
|
+
else{setTimeout(done,40);}
|
|
2234
|
+
} else done();
|
|
2235
|
+
}catch(e){res(JSON.stringify({typed:false,reason:String(e)}));}},60);
|
|
2236
|
+
}catch(e){res(JSON.stringify({typed:false,reason:String(e)}));}});})()`;
|
|
2237
|
+
}
|
|
2238
|
+
if (kind === 'press') {
|
|
2239
|
+
const key = JSON.stringify(String(args.key || 'Enter'));
|
|
2240
|
+
const refFocus = JSON.stringify(args.ref === undefined ? null : Number(args.ref));
|
|
2241
|
+
return `(function(){${BROWSER_JS_HELPERS}
|
|
2242
|
+
const KEY=${key};
|
|
2243
|
+
const map={Enter:['Enter','Enter',13],Tab:['Tab','Tab',9],Escape:['Escape','Escape',27],ArrowDown:['ArrowDown','ArrowDown',40],ArrowUp:['ArrowUp','ArrowUp',38],PageDown:['PageDown','PageDown',34],PageUp:['PageUp','PageUp',33]};
|
|
2244
|
+
const m=map[KEY]||[KEY,KEY,(KEY.charCodeAt(0)||0)];
|
|
2245
|
+
let el=null;
|
|
2246
|
+
try{const r=${refFocus};if(r!==null&&window.__beMap&&window.__beMap[r])el=window.__beMap[r];}catch(e){}
|
|
2247
|
+
el=(el&&el.isConnected)?el:(document.activeElement||document.body);
|
|
2248
|
+
['keydown','keypress','keyup'].forEach(t=>el.dispatchEvent(new KeyboardEvent(t,{key:m[0],code:m[1],keyCode:m[2],which:m[2],bubbles:true,cancelable:true})));
|
|
2249
|
+
return JSON.stringify({pressed:KEY,focused:(el.tagName||'').toLowerCase(),url:location.href});
|
|
2250
|
+
})()`;
|
|
2251
|
+
}
|
|
2252
|
+
if (kind === 'scroll') {
|
|
2253
|
+
const dir = args.direction === 'up' ? -1 : 1;
|
|
2254
|
+
const amount = JSON.stringify(Number(args.amount) || 0);
|
|
2255
|
+
return `(function(){
|
|
2256
|
+
const d=${dir};const a=${amount}||Math.round(window.innerHeight*0.9);
|
|
2257
|
+
window.scrollBy({top:d*a,behavior:'instant'});
|
|
2258
|
+
return JSON.stringify({scrolled:true,y:Math.round(window.scrollY),max:Math.round(document.documentElement.scrollHeight-window.innerHeight)});
|
|
2259
|
+
})()`;
|
|
2260
|
+
}
|
|
2261
|
+
if (kind === 'select') {
|
|
2262
|
+
const value = JSON.stringify(String(args.value ?? ''));
|
|
2263
|
+
return `(function(){${BROWSER_JS_HELPERS}${resolveTarget};return new Promise((res)=>{try{
|
|
2264
|
+
const t=__target();
|
|
2265
|
+
if(!t) return res(JSON.stringify({selected:false,reason:'eleman bulunamadi'}));
|
|
2266
|
+
const el=t.el;
|
|
2267
|
+
if(el.tagName!=='SELECT') return res(JSON.stringify({selected:false,reason:'bu eleman bir select degil: '+el.tagName}));
|
|
2268
|
+
const want=String(${value}).trim().toLowerCase();
|
|
2269
|
+
let opt=[...el.options].find(o=>o.value.toLowerCase()===want)||[...el.options].find(o=>o.text.trim().toLowerCase()===want)||[...el.options].find(o=>o.text.trim().toLowerCase().includes(want));
|
|
2270
|
+
if(!opt) return res(JSON.stringify({selected:false,reason:'secenek bulunamadi',options:[...el.options].map(o=>o.text.trim()).slice(0,20)}));
|
|
2271
|
+
el.value=opt.value;
|
|
2272
|
+
el.dispatchEvent(new Event('change',{bubbles:true}));
|
|
2273
|
+
res(JSON.stringify({selected:true,target:t.how,value:opt.text.trim()}));
|
|
2274
|
+
}catch(e){res(JSON.stringify({selected:false,reason:String(e)}));}});})()`;
|
|
2275
|
+
}
|
|
2276
|
+
return `JSON.stringify({ok:false,error:'bilinmeyen eylem'})`;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
/* eylem günlüğü — her yanıtın sonuna son hamleler eklenir */
|
|
2280
|
+
function blog(kind, detail) {
|
|
2281
|
+
if (!Array.isArray(browser.history)) browser.history = [];
|
|
2282
|
+
browser.history.push({
|
|
2283
|
+
t: new Date().toISOString().slice(11, 19),
|
|
2284
|
+
kind,
|
|
2285
|
+
detail: String(detail || '').slice(0, 120),
|
|
2286
|
+
});
|
|
2287
|
+
if (browser.history.length > 40) browser.history.shift();
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
function recentLog() {
|
|
2291
|
+
return Array.isArray(browser.history) ? browser.history.slice(-6) : [];
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
async function browserSnapshot(signal) {
|
|
2295
|
+
if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
|
|
2296
|
+
const wc = browser.view.webContents;
|
|
2297
|
+
try {
|
|
2298
|
+
const raw = await wc.executeJavaScript(BROWSER_SNAPSHOT_JS, true);
|
|
2299
|
+
const obj = JSON.parse(raw);
|
|
2300
|
+
blog('snapshot', obj.title + ' — ' + obj.count + ' eleman');
|
|
2301
|
+
return {
|
|
2302
|
+
ok: true,
|
|
2303
|
+
url: obj.url,
|
|
2304
|
+
title: obj.title,
|
|
2305
|
+
count: obj.count,
|
|
2306
|
+
snapshot: obj.snapshot,
|
|
2307
|
+
note: 'eylemlerde ref numarasini kullan (orn: browser_click {ref:3})',
|
|
2308
|
+
};
|
|
2309
|
+
} catch (e) {
|
|
2310
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
async function browserAct(kind, args, signal) {
|
|
2315
|
+
if (!browser.view || !browser.open) return { ok: false, error: 'tarayıcı açık değil' };
|
|
2316
|
+
const wc = browser.view.webContents;
|
|
2317
|
+
try {
|
|
2318
|
+
const raw = await wc.executeJavaScript(browserActionJs(kind, args || {}), true);
|
|
2319
|
+
let obj = {};
|
|
2320
|
+
try { obj = JSON.parse(raw); } catch { obj = { result: String(raw).slice(0, 300) }; }
|
|
2321
|
+
|
|
2322
|
+
// tıklama/form gönderimi sonrası kısa gezinme bekleme
|
|
2323
|
+
let navigated = false;
|
|
2324
|
+
if (kind === 'click' || (kind === 'type' && args && args.submit) || kind === 'press') {
|
|
2325
|
+
await new Promise((resolve) => {
|
|
2326
|
+
let done = false;
|
|
2327
|
+
const onNav = () => { navigated = true; setTimeout(fin, 400); };
|
|
2328
|
+
const fin = () => { if (!done) { done = true; clearTimeout(t); wc.removeListener('did-navigate', onNav); resolve(); } };
|
|
2329
|
+
const t = setTimeout(fin, 1800);
|
|
2330
|
+
wc.on('did-navigate', onNav);
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
blog(
|
|
2335
|
+
kind,
|
|
2336
|
+
obj.clicked ? obj.target
|
|
2337
|
+
: obj.typed ? ('"' + obj.value + '" → ' + obj.target)
|
|
2338
|
+
: obj.selected ? ('"' + obj.value + '" seçildi')
|
|
2339
|
+
: obj.pressed ? ('tuş ' + obj.pressed)
|
|
2340
|
+
: obj.scrolled ? ('kaydır y=' + obj.y)
|
|
2341
|
+
: (obj.reason || 'tamam')
|
|
2342
|
+
);
|
|
2343
|
+
|
|
2344
|
+
return {
|
|
2345
|
+
ok: true,
|
|
2346
|
+
action: kind,
|
|
2347
|
+
...obj,
|
|
2348
|
+
url: wc.getURL(),
|
|
2349
|
+
title: wc.getTitle(),
|
|
2350
|
+
navigated,
|
|
2351
|
+
...(navigated ? { note: 'sayfa degisti — gerekirse yeni browser_snapshot al' } : {}),
|
|
2352
|
+
recent: recentLog(),
|
|
2353
|
+
};
|
|
2354
|
+
} catch (e) {
|
|
2355
|
+
return { ok: false, action: kind, error: String((e && e.message) || e), recent: recentLog() };
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
/* oturum açılışında --hidden ile başlarsa pencere gösterme, tepside yaşa */
|
|
2360
|
+
const startHidden =
|
|
2361
|
+
process.argv.includes('--hidden') ||
|
|
2362
|
+
process.argv.includes('--silent') ||
|
|
2363
|
+
String(process.env.BEAST_HIDDEN || '') === '1';
|
|
2364
|
+
|
|
2365
|
+
function createWindow() {
|
|
2366
|
+
log.info('main', 'Pencere oluşturuluyor…');
|
|
2367
|
+
const dark = settings.theme === 'dark';
|
|
2368
|
+
win = new BrowserWindow({
|
|
2369
|
+
width: 1280,
|
|
2370
|
+
height: 820,
|
|
2371
|
+
minWidth: 760,
|
|
2372
|
+
minHeight: 500,
|
|
2373
|
+
show: false,
|
|
2374
|
+
backgroundColor: dark ? '#0d0d0f' : '#f7f7f8',
|
|
2375
|
+
titleBarStyle: 'hidden',
|
|
2376
|
+
titleBarOverlay: {
|
|
2377
|
+
color: 'transparent',
|
|
2378
|
+
symbolColor: dark ? '#9a9aa2' : '#707078',
|
|
2379
|
+
height: 46,
|
|
2380
|
+
},
|
|
2381
|
+
webPreferences: {
|
|
2382
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
2383
|
+
contextIsolation: true,
|
|
2384
|
+
nodeIntegration: false,
|
|
2385
|
+
sandbox: false,
|
|
2386
|
+
spellcheck: false,
|
|
2387
|
+
},
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
win.setMenuBarVisibility(false);
|
|
2391
|
+
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
|
2392
|
+
win.once('ready-to-show', () => {
|
|
2393
|
+
if (startHidden) win.hide();
|
|
2394
|
+
else win.show();
|
|
2395
|
+
});
|
|
2396
|
+
win.on('resize', layoutBrowser);
|
|
2397
|
+
win.on('maximize', layoutBrowser);
|
|
2398
|
+
win.on('unmaximize', layoutBrowser);
|
|
2399
|
+
win.on('show', layoutBrowser);
|
|
2400
|
+
// X'e basınca gizle — tepside yaşamaya devam, WhatsApp bağlantısı sürer
|
|
2401
|
+
win.on('close', (e) => {
|
|
2402
|
+
if (!app.isQuitting) {
|
|
2403
|
+
e.preventDefault();
|
|
2404
|
+
win.hide();
|
|
2405
|
+
}
|
|
2406
|
+
});
|
|
2407
|
+
win.on('closed', () => {
|
|
2408
|
+
win = null;
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/* ---------------- IPC ---------------- */
|
|
2413
|
+
|
|
2414
|
+
ipcMain.handle('app:state', () => engine.publicState());
|
|
2415
|
+
|
|
2416
|
+
/* #3 log sistemi: ayarlar → Log sekmesinde görüntülenir */
|
|
2417
|
+
ipcMain.handle('logs:get', () => {
|
|
2418
|
+
try { return { dir: log.dir(), lines: log.tail(600) }; } catch (e) { return { dir: '', lines: [], error: String(e && e.message || e) }; }
|
|
2419
|
+
});
|
|
2420
|
+
ipcMain.handle('logs:clear', () => {
|
|
2421
|
+
try { log.clear(); } catch {}
|
|
2422
|
+
return { ok: true };
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
ipcMain.handle('sessions:list', () => engine.listSessions());
|
|
2426
|
+
ipcMain.handle('sessions:create', () => engine.createSession());
|
|
2427
|
+
ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
|
|
2428
|
+
ipcMain.handle('sessions:delete', (_e, id) => engine.deleteSession(id));
|
|
2429
|
+
|
|
2430
|
+
ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
|
|
2431
|
+
const raw = text && typeof text === 'object' ? String(text.text || '') : String(text ?? '');
|
|
2432
|
+
const t = raw.trim();
|
|
2433
|
+
if (t === '/stop' || t === '/start') {
|
|
2434
|
+
handleGlobalStopStart(sessionId, t);
|
|
2435
|
+
return true;
|
|
2436
|
+
}
|
|
2437
|
+
if (t === '/restart') {
|
|
2438
|
+
handleRestart(sessionId);
|
|
2439
|
+
return true;
|
|
2440
|
+
}
|
|
2441
|
+
if (t === '/think' || t.startsWith('/think ')) {
|
|
2442
|
+
const arg = t.slice(6).trim();
|
|
2443
|
+
const r = arg ? applyThinkLevel(arg) : null;
|
|
2444
|
+
desktopEcho(sessionId, t, r && r.error ? r.error : r ? r.text : thinkStatusText());
|
|
2445
|
+
return true;
|
|
2446
|
+
}
|
|
2447
|
+
if (t === '/clear') {
|
|
2448
|
+
/* #25 artık GERÇEK silme: oturum dosyasındaki mesajlar + notlar silinir,
|
|
2449
|
+
kod/meta/todolar korunur. Ekran da temizlenir ('clear' olayı). */
|
|
2450
|
+
const ok = engine.clearMessages(String(sessionId || ''));
|
|
2451
|
+
if (win && !win.isDestroyed()) {
|
|
2452
|
+
win.webContents.send('agent:event', { sessionId: String(sessionId || ''), type: 'clear' });
|
|
2453
|
+
}
|
|
2454
|
+
desktopEcho(sessionId, '/clear', ok ? 'Sohbet geçmişi gerçekten silindi — bu oturumda sıfırdan devam ediyorsun.' : 'Oturum bulunamadı.');
|
|
2455
|
+
return true;
|
|
2456
|
+
}
|
|
2457
|
+
if (t === '/change' || t.startsWith('/change ')) {
|
|
2458
|
+
const arg = t.slice(7).trim();
|
|
2459
|
+
desktopEcho(sessionId, '/change' + (arg ? ' ' + arg : ''), modelChangeText(arg));
|
|
2460
|
+
if (win && !win.isDestroyed()) {
|
|
2461
|
+
win.webContents.send('agent:event', { type: 'modelChanged', sessionId: String(sessionId || '') });
|
|
2462
|
+
}
|
|
2463
|
+
return true;
|
|
2464
|
+
}
|
|
2465
|
+
if (t === '/notes') {
|
|
2466
|
+
desktopEcho(sessionId, '/notes', notesText(sessionId));
|
|
2467
|
+
return true;
|
|
2468
|
+
}
|
|
2469
|
+
if (t === '/notify' || t.startsWith('/notify ')) {
|
|
2470
|
+
const a = t.slice(7).trim().toLowerCase();
|
|
2471
|
+
if (a === 'on' || a === 'off') {
|
|
2472
|
+
settings.notifyOwnerFail = a === 'on';
|
|
2473
|
+
saveSettings();
|
|
2474
|
+
engine.notifyOwnerFail = settings.notifyOwnerFail;
|
|
2475
|
+
}
|
|
2476
|
+
desktopEcho(sessionId, t, `Hata mail bildirimi: ${settings.notifyOwnerFail !== false ? 'AÇIK' : 'KAPALI'} (değiştir: /notify on|off)`);
|
|
2477
|
+
return true;
|
|
2478
|
+
}
|
|
2479
|
+
if (t === '/approve' || t === '/deny' || t.startsWith('/approve ')) {
|
|
2480
|
+
const a = t.slice(8).trim().toLowerCase();
|
|
2481
|
+
const always = t !== '/deny' && a === 'always';
|
|
2482
|
+
const r = resolveFirstApproval(t !== '/deny', always);
|
|
2483
|
+
desktopEcho(
|
|
2484
|
+
sessionId,
|
|
2485
|
+
t,
|
|
2486
|
+
r.ok
|
|
2487
|
+
? `*${t === '/deny' ? 'Reddedildi' : 'Onaylandı'}:* ${r.tool}${always ? ' — bu araç için bir daha sorulmayacak' : ''}`
|
|
2488
|
+
: 'Bekleyen onay yok.'
|
|
2489
|
+
);
|
|
2490
|
+
return true;
|
|
2491
|
+
}
|
|
2492
|
+
resumeServices(); // pause durumunda gerçek mesaj her şeyi canlandırır
|
|
2493
|
+
queueDesktopMessage(sessionId, text);
|
|
2494
|
+
return true;
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
/* #25 aktif oturumun notlarını düz metin döndürür (chat + WA) */
|
|
2498
|
+
function notesText(sessionId) {
|
|
2499
|
+
try {
|
|
2500
|
+
const s = engine.openSession(String(sessionId || ''));
|
|
2501
|
+
if (s && s.notes) {
|
|
2502
|
+
return `Notlar (oturum ${s.code}):\n${s.notes}`;
|
|
2503
|
+
}
|
|
2504
|
+
} catch {}
|
|
2505
|
+
return 'Bu oturumda henüz not yok — her 14 mesajda otomatik oluşur/güncellenir.';
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
/* masaüstünde komut → kullanıcı+asistan balonu olarak yansıt */
|
|
2509
|
+
function desktopEcho(sessionId, cmd, reply) {
|
|
2510
|
+
const sid = String(sessionId || '');
|
|
2511
|
+
if (win && !win.isDestroyed()) {
|
|
2512
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'user', content: cmd } });
|
|
2513
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'assistant', content: reply } });
|
|
2514
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
/* /change: kayıtlı modelleri numaralı listeler; /change <n> ile o modele geçer.
|
|
2519
|
+
Hem masaüstü hem WhatsApp aynı metni üretir. */
|
|
2520
|
+
function modelChangeText(arg) {
|
|
2521
|
+
const st = engine.publicState();
|
|
2522
|
+
const models = st.models || [];
|
|
2523
|
+
if (!String(arg || '').trim()) {
|
|
2524
|
+
if (!models.length) return 'Kayıtlı model yok — Ayarlar → Provider.';
|
|
2525
|
+
const lines = models.map((m, i) => {
|
|
2526
|
+
const active = st.activeModel && st.activeModel.sel === m.sel;
|
|
2527
|
+
return `${i + 1}. ${m.providerName} · ${m.model}${active ? ' \u2190 aktif' : ''}`;
|
|
2528
|
+
});
|
|
2529
|
+
return '*Modeller:*\n' + lines.join('\n') + '\n\nGeçiş için: /change <numara> (ör. /change 2)';
|
|
2530
|
+
}
|
|
2531
|
+
const idx = parseInt(String(arg).trim(), 10);
|
|
2532
|
+
if (!idx || idx < 1 || idx > models.length) {
|
|
2533
|
+
return `Geçersiz numara: ${arg} — 1..${models.length} arası olmalı. Liste: /change`;
|
|
2534
|
+
}
|
|
2535
|
+
const hit = models[idx - 1];
|
|
2536
|
+
settings.modelOverride = hit.sel;
|
|
2537
|
+
saveSettings();
|
|
2538
|
+
if (typeof engine.setModelOverride === 'function') engine.setModelOverride(hit.sel);
|
|
2539
|
+
const st2 = engine.publicState();
|
|
2540
|
+
return st2.activeModel
|
|
2541
|
+
? `*Model değişti (#${idx}):* ${st2.activeModel.providerName} · ${st2.activeModel.model}`
|
|
2542
|
+
: `*Model değişti (#${idx}):* ${hit.sel}`;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
/* /think: düşünme (reasoning) seviyesi — sağlayıcıların GERÇEK değerleri.
|
|
2546
|
+
0 Kapalı (param gönderilmez) · 1 low · 2 medium · 3 high · 4 xhigh · 5 max */
|
|
2547
|
+
const THINK_LABELS = ['Kapalı', 'Low', 'Medium', 'High', 'X-High', 'Max'];
|
|
2548
|
+
const THINK_EFFORTS = [null, 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
2549
|
+
|
|
2550
|
+
function clampThink(v) {
|
|
2551
|
+
return Math.min(5, Math.max(0, Math.round(Number(v) || 0)));
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function setThinkLevel(v) {
|
|
2555
|
+
settings.thinkLevel = clampThink(v);
|
|
2556
|
+
saveSettings();
|
|
2557
|
+
if (engine && typeof engine.setThinkLevel === 'function') engine.setThinkLevel(settings.thinkLevel);
|
|
2558
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'think', level: settings.thinkLevel });
|
|
2559
|
+
return settings.thinkLevel;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
function applyThinkLevel(arg) {
|
|
2563
|
+
const a = String(arg || '').trim().toLowerCase();
|
|
2564
|
+
let v = -1;
|
|
2565
|
+
if (/^[0-5]$/.test(a)) v = Number(a);
|
|
2566
|
+
else {
|
|
2567
|
+
const idx = THINK_LABELS.findIndex((l) => l.toLowerCase() === a);
|
|
2568
|
+
if (idx >= 0) v = idx;
|
|
2569
|
+
}
|
|
2570
|
+
if (v < 0) return { error: `Geçersiz seviye: ${a}\nKullanım: /think <0-5> — 0 Kapalı · 1 Low · 2 Medium · 3 High · 4 X-High · 5 Max` };
|
|
2571
|
+
const nv = setThinkLevel(v);
|
|
2572
|
+
return {
|
|
2573
|
+
level: nv,
|
|
2574
|
+
text:
|
|
2575
|
+
`Düşünme seviyesi: ${THINK_LABELS[nv]}` +
|
|
2576
|
+
(THINK_EFFORTS[nv] ? ` (reasoning_effort: ${THINK_EFFORTS[nv]})` : ' — parametre gönderilmez'),
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
function thinkStatusText() {
|
|
2581
|
+
const v = engine && typeof engine.thinkLevel === 'number' ? engine.thinkLevel : clampThink(settings.thinkLevel);
|
|
2582
|
+
return (
|
|
2583
|
+
`Düşünme seviyesi: ${THINK_LABELS[v] || 'Kapalı'}\n` +
|
|
2584
|
+
`0 Kapalı · 1 Low · 2 Medium · 3 High · 4 X-High · 5 Max\n` +
|
|
2585
|
+
`Değiştirmek için: /think <0-5> (üst bardaki Düşünme menüsünden de seçilir)`
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
/* /restart: uygulama kendini yeniden başlatır (relaunch + exit) */
|
|
2590
|
+
function handleRestart(sessionId) {
|
|
2591
|
+
const sid = String(sessionId || ''); if (win && !win.isDestroyed()) {
|
|
2592
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'user', content: '/restart' } });
|
|
2593
|
+
win.webContents.send('agent:event', {
|
|
2594
|
+
sessionId: sid,
|
|
2595
|
+
type: 'message',
|
|
2596
|
+
message: { role: 'assistant', content: '\u21BB **Yeniden başlatılıyor…** Birkaç saniye içinde pencere geri açılacak.' },
|
|
2597
|
+
});
|
|
2598
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
|
|
2599
|
+
}
|
|
2600
|
+
setTimeout(() => {
|
|
2601
|
+
try { app.relaunch(); } catch {}
|
|
2602
|
+
try { app.exit(0); } catch {}
|
|
2603
|
+
}, 800);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
/* Masaüstünde /stop ve /start: engine'e gitmez; tüm sistemde etki eder ve
|
|
2607
|
+
sohbete görünür bir teyit düşer (kullanıcı + asistan balonu + done). */
|
|
2608
|
+
function handleGlobalStopStart(sessionId, cmd) {
|
|
2609
|
+
const sid = String(sessionId || '');
|
|
2610
|
+
let reply = '';
|
|
2611
|
+
if (cmd === '/stop') {
|
|
2612
|
+
const n = stopEverything();
|
|
2613
|
+
reply =
|
|
2614
|
+
`\u25A0 **Durdu** — ${n} koşan iş kesildi.\n` +
|
|
2615
|
+
'Paralel ajanlar ve bekleyen kuyruklar temizlendi. Cron, izleyici ve olay merkezi çalışmaya devam ediyor.\n' +
|
|
2616
|
+
'Devam için bir şeyler yaz ya da `/start`';
|
|
2617
|
+
} else {
|
|
2618
|
+
const wasPaused = servicesPaused;
|
|
2619
|
+
resumeServices();
|
|
2620
|
+
reply = wasPaused ? '\u25B6 **Devam** — tüm servisler yeniden başladı.' : 'Zaten çalışıyor — durdurulmuş bir şey yok.';
|
|
2621
|
+
}
|
|
2622
|
+
if (win && !win.isDestroyed()) {
|
|
2623
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'user', content: cmd } });
|
|
2624
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'message', message: { role: 'assistant', content: reply } });
|
|
2625
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'done', usage: null });
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
ipcMain.handle('agent:interrupt', (_e, sessionId) => {
|
|
2630
|
+
/* durdurma: bekleyen birleştirme kuyruğunu da boşalt (kullanıcı vazgeçti) */
|
|
2631
|
+
try {
|
|
2632
|
+
const q = desktopQueue.get(String(sessionId));
|
|
2633
|
+
if (q) {
|
|
2634
|
+
clearTimeout(q.timer);
|
|
2635
|
+
desktopQueue.delete(String(sessionId));
|
|
2636
|
+
}
|
|
2637
|
+
} catch {}
|
|
2638
|
+
return engine.interrupt(sessionId);
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
/* ---------- masaüstü sohbet birleştirme ----------
|
|
2642
|
+
WA'daki anti-spam'in desktop hali: hızlı ard arda gelen mesajlar
|
|
2643
|
+
DEBOUNCE_MS penceresinde toplanır, tek paket olarak engine'e gider.
|
|
2644
|
+
Arka arkaya yazımda cevap gecikmez; ancak agent ÇALIŞIYORKEN gelen
|
|
2645
|
+
yeni mesajlar pencereyle değil, mevcut iş bitene dek toplanır ve
|
|
2646
|
+
işin ardından TEK mesaj olarak sıradaki tura girer. */
|
|
2647
|
+
|
|
2648
|
+
const DESKTOP_DEBOUNCE_MS = 1200;
|
|
2649
|
+
const desktopQueue = new Map(); // sid -> { timer, msgs[] }
|
|
2650
|
+
|
|
2651
|
+
/* payload: renderer'dan string YA DA {text, attachments} nesnesi gelir.
|
|
2652
|
+
Engine.send iki formu da kabul eder; birleştirme yalnızca metinlerde yapılır,
|
|
2653
|
+
ekler olduğu gibi korunur. */
|
|
2654
|
+
function queueDesktopMessage(sessionId, text) {
|
|
2655
|
+
const sid = String(sessionId || '');
|
|
2656
|
+
const isObj = text && typeof text === 'object';
|
|
2657
|
+
const t = isObj ? String((text && text.text) || '') : String(text ?? '');
|
|
2658
|
+
const hasAtts = isObj && Array.isArray(text.attachments) && text.attachments.length > 0;
|
|
2659
|
+
if (!sid || (!t.trim() && !hasAtts)) return; // boş içerik kuyruğa girmez
|
|
2660
|
+
let q = desktopQueue.get(sid);
|
|
2661
|
+
if (!q) {
|
|
2662
|
+
q = { timer: null, msgs: [] };
|
|
2663
|
+
desktopQueue.set(sid, q);
|
|
2664
|
+
}
|
|
2665
|
+
q.msgs.push({ text: t, attachments: hasAtts ? text.attachments : undefined });
|
|
2666
|
+
|
|
2667
|
+
const busy = engine.isBusy(sid);
|
|
2668
|
+
if (!busy) {
|
|
2669
|
+
/* agent boşta: kısa pencere — hızlı ikinci mesaj ilkine eklenir */
|
|
2670
|
+
clearTimeout(q.timer);
|
|
2671
|
+
q.timer = setTimeout(() => flushDesktop(sid).catch(() => {}), DESKTOP_DEBOUNCE_MS);
|
|
2672
|
+
} else {
|
|
2673
|
+
/* agent çalışıyor: pencere yok, iş bitince hepsi birlikte gider.
|
|
2674
|
+
Bekleyen paket olduğunda 'done' olayı flushDesktop'u çağırır. */
|
|
2675
|
+
waLog(`desktop: oturum meşgul, mesaj beklemede (${q.msgs.length}) sid=${sid}`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
async function flushDesktop(sessionId) {
|
|
2680
|
+
const sid = String(sessionId || '');
|
|
2681
|
+
const q = desktopQueue.get(sid);
|
|
2682
|
+
if (!q || !q.msgs.length) {
|
|
2683
|
+
desktopQueue.delete(sid);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (engine.isBusy(sid)) return; // hâlâ çalışıyor — done eventini bekle
|
|
2687
|
+
desktopQueue.delete(sid);
|
|
2688
|
+
clearTimeout(q.timer);
|
|
2689
|
+
|
|
2690
|
+
/* çoklu paketi birleştir: metinler \n ile, ilk paketin ekleri esas alınır
|
|
2691
|
+
(aynı turda çift görsel enjeksiyonunu önlemek için) */
|
|
2692
|
+
let mergedText = '';
|
|
2693
|
+
let mergedAtts = null;
|
|
2694
|
+
for (const m of q.msgs) {
|
|
2695
|
+
if (m.text) mergedText += (mergedText ? '\n' : '') + m.text;
|
|
2696
|
+
if (!mergedAtts && Array.isArray(m.attachments)) mergedAtts = m.attachments;
|
|
2697
|
+
}
|
|
2698
|
+
if (!mergedText.trim() && !mergedAtts) return;
|
|
2699
|
+
engine.send(sid, mergedAtts ? { text: mergedText, attachments: mergedAtts } : mergedText);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/* agent işi bitince biriken masaüstü mesajlarını göndere bastır.
|
|
2703
|
+
'error' bitişinde de boşaltılır — sıradaki mesaj boşa gitmesin. */
|
|
2704
|
+
function flushDesktopOnDone(ev) {
|
|
2705
|
+
if (ev && (ev.type === 'done' || ev.type === 'error') && ev.sessionId) {
|
|
2706
|
+
const q = desktopQueue.get(String(ev.sessionId));
|
|
2707
|
+
if (q && q.msgs.length) {
|
|
2708
|
+
setTimeout(() => flushDesktop(ev.sessionId).catch(() => {}), 150);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
ipcMain.handle('model:set', (_e, sel) => {
|
|
2714
|
+
settings.modelOverride = sel;
|
|
2715
|
+
saveSettings();
|
|
2716
|
+
engine.setModelOverride(sel);
|
|
2717
|
+
return engine.publicState();
|
|
2718
|
+
});
|
|
2719
|
+
|
|
2720
|
+
ipcMain.handle('model:role', (_e, map) => {
|
|
2721
|
+
const roleModels = {};
|
|
2722
|
+
const allowed = {};
|
|
2723
|
+
for (const k of ['vision', 'terminal', 'coding', 'subagent']) {
|
|
2724
|
+
const v = map && map[k];
|
|
2725
|
+
if (v && typeof v === 'object' && v.providerId && v.model) {
|
|
2726
|
+
roleModels[k] = v.providerId + '::' + v.model;
|
|
2727
|
+
} else if (v === null || v === undefined) {
|
|
2728
|
+
roleModels[k] = null; // default to main
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
settings.roleModels = roleModels;
|
|
2732
|
+
saveSettings();
|
|
2733
|
+
engine.setRoleModels(roleModels);
|
|
2734
|
+
return engine.publicState();
|
|
2735
|
+
});
|
|
2736
|
+
|
|
2737
|
+
ipcMain.handle('wa:lockdown:set', (_e, v) => {
|
|
2738
|
+
settings.waLockdown = !!v;
|
|
2739
|
+
saveSettings();
|
|
2740
|
+
if (engine) engine.setLockdown(settings.waLockdown);
|
|
2741
|
+
return { waLockdown: settings.waLockdown };
|
|
2742
|
+
});
|
|
2743
|
+
|
|
2744
|
+
ipcMain.handle('model:delete', (_e, sel) => {
|
|
2745
|
+
const list = new Set(settings.deletedModels || []);
|
|
2746
|
+
if (typeof sel === 'string' && sel.includes('::')) list.add(sel);
|
|
2747
|
+
settings.deletedModels = [...list];
|
|
2748
|
+
saveSettings();
|
|
2749
|
+
engine.setDeletedModels(settings.deletedModels);
|
|
2750
|
+
return engine.publicState();
|
|
2751
|
+
});
|
|
2752
|
+
|
|
2753
|
+
ipcMain.handle('model:restore', (_e, sel) => {
|
|
2754
|
+
settings.deletedModels = (settings.deletedModels || []).filter((s) => s !== sel);
|
|
2755
|
+
saveSettings();
|
|
2756
|
+
engine.setDeletedModels(settings.deletedModels);
|
|
2757
|
+
return engine.publicState();
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
ipcMain.handle('cwd:set', async () => {
|
|
2761
|
+
const res = await dialog.showOpenDialog(win, {
|
|
2762
|
+
properties: ['openDirectory'],
|
|
2763
|
+
defaultPath: engine.workspace,
|
|
2764
|
+
title: 'Beast çalışma klasörü seç',
|
|
2765
|
+
});
|
|
2766
|
+
if (res.canceled || !res.filePaths[0]) return engine.publicState();
|
|
2767
|
+
settings.workspace = res.filePaths[0];
|
|
2768
|
+
saveSettings();
|
|
2769
|
+
engine.setWorkspace(res.filePaths[0]);
|
|
2770
|
+
return engine.publicState();
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
ipcMain.on('open-external', (_e, url) => {
|
|
2774
|
+
if (/^https?:\/\//i.test(url)) shell.openExternal(url);
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
/* ---------------- settings IPC ---------------- */
|
|
2778
|
+
|
|
2779
|
+
ipcMain.handle('memory:get', () => memory.loadAll());
|
|
2780
|
+
|
|
2781
|
+
/* paralel ajanlar: canlı izleme (#14) */
|
|
2782
|
+
ipcMain.handle('agents:list', () => engine.listBgJobs());
|
|
2783
|
+
ipcMain.handle('agents:detail', (_e, id) => engine.bgDetail(id));
|
|
2784
|
+
ipcMain.handle('agents:cancel', (_e, id) => ({ ok: engine.interrupt(String(id || '')) }));
|
|
2785
|
+
ipcMain.handle('ceo:get', () => !!engine.ceoMode);
|
|
2786
|
+
ipcMain.handle('ceo:set', (_e, v) => {
|
|
2787
|
+
settings.ceoMode = !!v;
|
|
2788
|
+
saveSettings();
|
|
2789
|
+
if (engine) engine.setCeoMode(!!v);
|
|
2790
|
+
return true;
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
ipcMain.handle('notes:list', () => engine.listNotes());
|
|
2794
|
+
ipcMain.handle('notes:clear', (_e, id) => engine.clearNotes(id));
|
|
2795
|
+
|
|
2796
|
+
ipcMain.handle('memory:save', (_e, { file, content }) => memory.save(file, content));
|
|
2797
|
+
|
|
2798
|
+
ipcMain.handle('skills:list', () => skillsMod.scan());
|
|
2799
|
+
|
|
2800
|
+
/* taslak skill'ler (#2 yansıma ürünleri) */
|
|
2801
|
+
ipcMain.handle('skills:drafts:list', () => skillsMod.listDrafts());
|
|
2802
|
+
ipcMain.handle('skills:drafts:accept', (_e, id) => {
|
|
2803
|
+
const r = skillsMod.acceptDraft(id);
|
|
2804
|
+
if (r.ok) waLog(`skill taslağı kabul edildi: ${id}`);
|
|
2805
|
+
return r;
|
|
2806
|
+
});
|
|
2807
|
+
ipcMain.handle('skills:drafts:drop', (_e, id) => skillsMod.dropDraft(id));
|
|
2808
|
+
|
|
2809
|
+
/* kalıcı kurallar (#3) */
|
|
2810
|
+
ipcMain.handle('rules:get', () => memory.listRules());
|
|
2811
|
+
ipcMain.handle('rules:add', (_e, text) => memory.addRule(text));
|
|
2812
|
+
ipcMain.handle('rules:remove', (_e, idOrText) => memory.removeRule(idOrText));
|
|
2813
|
+
|
|
2814
|
+
/* olay merkezi ayarları (#4) */
|
|
2815
|
+
function normEventBus(e) {
|
|
2816
|
+
const cur = settings.eventBus || {};
|
|
2817
|
+
return {
|
|
2818
|
+
enabled: !!(e && e.enabled),
|
|
2819
|
+
mailIdle: !!(e ? e.mailIdle : cur.mailIdle),
|
|
2820
|
+
fsWatch: !!(e ? e.fsWatch : cur.fsWatch),
|
|
2821
|
+
webhookPort: Number((e && e.webhookPort) || cur.webhookPort || 8787),
|
|
2822
|
+
priceSymbol: String((e && e.priceSymbol) || cur.priceSymbol || '').trim() || null,
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
ipcMain.handle('events:config:get', () => {
|
|
2826
|
+
const e = normEventBus(null);
|
|
2827
|
+
return { ...e, enabled: !!(settings.eventBus && settings.eventBus.enabled), token: webhookToken(), port: e.webhookPort };
|
|
2828
|
+
});
|
|
2829
|
+
ipcMain.handle('events:config:set', (_e, cfg) => {
|
|
2830
|
+
settings.eventBus = normEventBus(cfg);
|
|
2831
|
+
saveSettings();
|
|
2832
|
+
startEventBus();
|
|
2833
|
+
return { ...settings.eventBus, token: webhookToken() };
|
|
2834
|
+
});
|
|
2835
|
+
ipcMain.handle('events:subs:list', () => bus.listSubs());
|
|
2836
|
+
ipcMain.handle('events:subs:remove', (_e, id) => bus.removeSub(id));
|
|
2837
|
+
|
|
2838
|
+
ipcMain.handle('skills:openFolder', () => {
|
|
2839
|
+
shell.openPath(skillsMod.dir());
|
|
2840
|
+
return true;
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
/* ---------------- kullanım/maliyet IPC ---------------- */
|
|
2844
|
+
|
|
2845
|
+
ipcMain.handle('usage:get', () => usageMod.report());
|
|
2846
|
+
ipcMain.handle('usage:reset', () => {
|
|
2847
|
+
usageMod.reset();
|
|
2848
|
+
return usageMod.report();
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
/* #6 yedekleme */
|
|
2852
|
+
ipcMain.handle('backup:create', async () => createBackup());
|
|
2853
|
+
ipcMain.handle('backup:restore', async () => restoreBackup());
|
|
2854
|
+
|
|
2855
|
+
/* ---------------- #2 kurulum sihirbazı ---------------- */
|
|
2856
|
+
|
|
2857
|
+
ipcMain.handle('setup:status', () => {
|
|
2858
|
+
try {
|
|
2859
|
+
const st = engine.publicState();
|
|
2860
|
+
return {
|
|
2861
|
+
done: !!settings.setupDone,
|
|
2862
|
+
hasModel: !!st.hasModel || (st.models || []).length > 0,
|
|
2863
|
+
waConnected: !!(wa && wa.connected),
|
|
2864
|
+
customCount: (settings.customProviders || []).length,
|
|
2865
|
+
};
|
|
2866
|
+
} catch {
|
|
2867
|
+
return { done: false, hasModel: false, waConnected: false, customCount: 0 };
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
|
|
2871
|
+
/* Sihirbazdan gelen key+baseUrl'i kalıcı custom provider olarak kaydet */
|
|
2872
|
+
ipcMain.handle('setup:saveProvider', (_e, { name, baseUrl, key }) => {
|
|
2873
|
+
try {
|
|
2874
|
+
const id = 'setup-' + Date.now().toString(36);
|
|
2875
|
+
const list = settings.customProviders || [];
|
|
2876
|
+
list.push({
|
|
2877
|
+
id,
|
|
2878
|
+
name: String(name || '').trim().slice(0, 40) || 'Sağlayıcı',
|
|
2879
|
+
baseUrl: String(baseUrl || '').trim(),
|
|
2880
|
+
key: String(key || '').trim(),
|
|
2881
|
+
models: [],
|
|
2882
|
+
});
|
|
2883
|
+
settings.customProviders = list;
|
|
2884
|
+
saveSettings();
|
|
2885
|
+
if (engine) engine.setCustomProviders(settings.customProviders);
|
|
2886
|
+
return { ok: true, id };
|
|
2887
|
+
} catch (e) {
|
|
2888
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
/* Model seçimi + sihirbazı bitir */
|
|
2893
|
+
ipcMain.handle('setup:complete', (_e, { sel }) => {
|
|
2894
|
+
try {
|
|
2895
|
+
if (sel) {
|
|
2896
|
+
settings.modelOverride = String(sel);
|
|
2897
|
+
saveSettings();
|
|
2898
|
+
if (engine) engine.setModelOverride(settings.modelOverride);
|
|
2899
|
+
}
|
|
2900
|
+
settings.setupDone = true;
|
|
2901
|
+
saveSettings();
|
|
2902
|
+
return { ok: true };
|
|
2903
|
+
} catch (e) {
|
|
2904
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
ipcMain.handle('setup:skip', () => {
|
|
2908
|
+
settings.setupDone = true; // bir daha açılmasın
|
|
2909
|
+
saveSettings();
|
|
2910
|
+
return { ok: true };
|
|
2911
|
+
});
|
|
2912
|
+
|
|
2913
|
+
/* #5 "nerede kaldım" */
|
|
2914
|
+
ipcMain.handle('wherewasi:get', () => whereWasISummary());
|
|
2915
|
+
ipcMain.handle('wherewasi:set', (_e, cfg) => {
|
|
2916
|
+
settings.whereWasI = { enabled: !!(cfg && cfg.enabled) };
|
|
2917
|
+
saveSettings();
|
|
2918
|
+
return settings.whereWasI;
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
/* WhatsApp grup ayarı: { enabled, mentionOnly } */
|
|
2922
|
+
ipcMain.handle('wa:groups:get', () => settings.waGroups || { enabled: false, mentionOnly: true });
|
|
2923
|
+
ipcMain.handle('wa:groups:set', (_e, cfg) => {
|
|
2924
|
+
settings.waGroups = {
|
|
2925
|
+
enabled: !!(cfg && cfg.enabled),
|
|
2926
|
+
mentionOnly: !(cfg && cfg.mentionOnly === false),
|
|
2927
|
+
};
|
|
2928
|
+
saveSettings();
|
|
2929
|
+
return settings.waGroups;
|
|
2930
|
+
});
|
|
2931
|
+
|
|
2932
|
+
ipcMain.handle('settings:get', () => {
|
|
2933
|
+
const out = JSON.parse(JSON.stringify(settings));
|
|
2934
|
+
/* Sırların renderera düz metin gitmesini engelle */
|
|
2935
|
+
if (out.email && out.email.pass) out.email.pass = '***';
|
|
2936
|
+
if (out.waTts && out.waTts.key) out.waTts.key = '***';
|
|
2937
|
+
return out;
|
|
2938
|
+
});
|
|
2939
|
+
|
|
2940
|
+
/* ---------------- FALLOUT IPC ---------------- */
|
|
2941
|
+
|
|
2942
|
+
function defaultFallout() {
|
|
2943
|
+
return { enabled: false, autoResume: true, slots: Array.from({ length: 10 }, () => null) };
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
ipcMain.handle('fallout:get', () => {
|
|
2947
|
+
const f = settings.fallout || {};
|
|
2948
|
+
const out = defaultFallout();
|
|
2949
|
+
out.enabled = !!f.enabled;
|
|
2950
|
+
out.autoResume = f.autoResume !== false;
|
|
2951
|
+
if (Array.isArray(f.slots)) {
|
|
2952
|
+
for (let i = 0; i < Math.min(10, f.slots.length); i++) out.slots[i] = f.slots[i] || null;
|
|
2953
|
+
}
|
|
2954
|
+
return out;
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
ipcMain.handle('fallout:set', (_e, cfg) => {
|
|
2958
|
+
const cur = settings.fallout || defaultFallout();
|
|
2959
|
+
const next = defaultFallout();
|
|
2960
|
+
next.enabled = !!(cfg && cfg.enabled);
|
|
2961
|
+
next.autoResume = !cfg || cfg.autoResume !== false;
|
|
2962
|
+
const slots = Array.isArray(cfg && cfg.slots) ? cfg.slots : [];
|
|
2963
|
+
for (let i = 0; i < Math.min(10, slots.length); i++) {
|
|
2964
|
+
const s = slots[i];
|
|
2965
|
+
if (!s || typeof s !== 'object' || !s.providerId || !s.model || !s.key) continue;
|
|
2966
|
+
next.slots[i] = {
|
|
2967
|
+
providerId: String(s.providerId),
|
|
2968
|
+
providerName: String(s.providerName || s.providerId),
|
|
2969
|
+
model: String(s.model),
|
|
2970
|
+
key: String(s.key),
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
settings.fallout = next;
|
|
2974
|
+
saveSettings();
|
|
2975
|
+
if (engine) engine.setFallout(next);
|
|
2976
|
+
return JSON.parse(JSON.stringify(next));
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
/* #Limit: provider bazlı max input token limiti + bağlam sıkıştırma */
|
|
2980
|
+
/* #Güvenlik IPC */
|
|
2981
|
+
ipcMain.handle('sec:get', () => ({
|
|
2982
|
+
approvals: !!(settings.security && settings.security.approvals),
|
|
2983
|
+
alwaysAllow: (settings.security && Array.isArray(settings.security.alwaysAllow) ? settings.security.alwaysAllow : []),
|
|
2984
|
+
}));
|
|
2985
|
+
ipcMain.handle('sec:set', (_e, cfg) => {
|
|
2986
|
+
const prevAlways = settings.security && Array.isArray(settings.security.alwaysAllow) ? settings.security.alwaysAllow : [];
|
|
2987
|
+
settings.security = {
|
|
2988
|
+
approvals: !!(cfg && cfg.approvals),
|
|
2989
|
+
alwaysAllow: cfg && Array.isArray(cfg.alwaysAllow) ? cfg.alwaysAllow : prevAlways,
|
|
2990
|
+
};
|
|
2991
|
+
saveSettings();
|
|
2992
|
+
if (engine) {
|
|
2993
|
+
engine.approvals = settings.security.approvals ? approvalsBridge : null;
|
|
2994
|
+
engine.alwaysAllowTools = new Set(settings.security.alwaysAllow);
|
|
2995
|
+
}
|
|
2996
|
+
log.info('sec', `güvenlik: onay kapısı ${settings.security.approvals ? 'AÇIK' : 'KAPALI (her şey serbest)'}`);
|
|
2997
|
+
return { approvals: settings.security.approvals, alwaysAllow: settings.security.alwaysAllow };
|
|
2998
|
+
});
|
|
2999
|
+
ipcMain.handle('approval:respond', (_e, { id, ok, always }) => resolveApproval(id, ok, always));
|
|
3000
|
+
|
|
3001
|
+
/* #STT: sohbet mikrofonu — MediaRecorder sesini (webm/opus) yerel whisper'a çevir */
|
|
3002
|
+
ipcMain.handle('stt:transcribe', async (_e, b64) => {
|
|
3003
|
+
try {
|
|
3004
|
+
const buf = Buffer.from(String(b64 || '').split(',').pop() || '', 'base64');
|
|
3005
|
+
if (!buf.length) return { ok: false, error: 'boş ses kaydı' };
|
|
3006
|
+
const text = await transcribeAudio(buf);
|
|
3007
|
+
return text ? { ok: true, text } : { ok: false, error: 'konuşma algılanamadı' };
|
|
3008
|
+
} catch (e) {
|
|
3009
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
ipcMain.handle('limits:get', () => {
|
|
3014
|
+
const l = settings.limits || {};
|
|
3015
|
+
return {
|
|
3016
|
+
enabled: !!l.enabled,
|
|
3017
|
+
compress: l.compress !== false,
|
|
3018
|
+
default: Math.max(0, Math.round(Number(l.default) || 0)),
|
|
3019
|
+
perProvider: l.perProvider && typeof l.perProvider === 'object' ? { ...l.perProvider } : {},
|
|
3020
|
+
};
|
|
3021
|
+
});
|
|
3022
|
+
|
|
3023
|
+
ipcMain.handle('limits:set', (_e, cfg) => {
|
|
3024
|
+
const per = {};
|
|
3025
|
+
const raw = cfg && typeof cfg.perProvider === 'object' && cfg.perProvider ? cfg.perProvider : {};
|
|
3026
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
3027
|
+
const n = Math.max(0, Math.round(Number(v) || 0));
|
|
3028
|
+
if (n > 0 && k) per[String(k).slice(0, 80)] = n;
|
|
3029
|
+
}
|
|
3030
|
+
const next = {
|
|
3031
|
+
enabled: !!(cfg && cfg.enabled),
|
|
3032
|
+
compress: !cfg || cfg.compress !== false,
|
|
3033
|
+
default: Math.max(0, Math.round(Number(cfg && cfg.default) || 0)),
|
|
3034
|
+
perProvider: per,
|
|
3035
|
+
};
|
|
3036
|
+
settings.limits = next;
|
|
3037
|
+
saveSettings();
|
|
3038
|
+
if (engine) engine.setLimits(next);
|
|
3039
|
+
return JSON.parse(JSON.stringify(next));
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
ipcMain.handle('visible-models:set', (_e, list) => {
|
|
3043
|
+
settings.visibleModels = Array.isArray(list) ? list : null;
|
|
3044
|
+
saveSettings();
|
|
3045
|
+
return true;
|
|
3046
|
+
});
|
|
3047
|
+
|
|
3048
|
+
ipcMain.handle('theme:set', (_e, t) => {
|
|
3049
|
+
settings.theme = t === 'dark' ? 'dark' : 'light';
|
|
3050
|
+
saveSettings();
|
|
3051
|
+
if (win && !win.isDestroyed() && typeof win.setTitleBarOverlay === 'function') {
|
|
3052
|
+
try {
|
|
3053
|
+
win.setTitleBarOverlay(
|
|
3054
|
+
settings.theme === 'dark'
|
|
3055
|
+
? { color: '#0d0d0f', symbolColor: '#9a9aa2' }
|
|
3056
|
+
: { color: '#f7f7f8', symbolColor: '#707078' }
|
|
3057
|
+
);
|
|
3058
|
+
} catch {}
|
|
3059
|
+
}
|
|
3060
|
+
return true;
|
|
3061
|
+
});
|
|
3062
|
+
|
|
3063
|
+
/* ---------------- cron IPC ---------------- */
|
|
3064
|
+
|
|
3065
|
+
function cronEmit() {
|
|
3066
|
+
if (win && !win.isDestroyed()) {
|
|
3067
|
+
win.webContents.send('agent:event', { type: 'cron', jobs: cron.list() });
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function cronFire(job) {
|
|
3072
|
+
try {
|
|
3073
|
+
let sid = job.sessionId;
|
|
3074
|
+
const exists = sid && engine.listSessions().some((s) => s.id === sid);
|
|
3075
|
+
if (!exists) {
|
|
3076
|
+
const s = engine.createSession();
|
|
3077
|
+
sid = s.id;
|
|
3078
|
+
cron.update(job.id, { sessionId: sid });
|
|
3079
|
+
}
|
|
3080
|
+
engine.send(sid, {
|
|
3081
|
+
text: `[cron: ${job.name}]\n${job.prompt}`,
|
|
3082
|
+
});
|
|
3083
|
+
} catch {}
|
|
3084
|
+
cronEmit();
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
/* İzleyici tetiklendiğinde ilgili sohbete kullanıcı mesajı gibi düşer */
|
|
3088
|
+
function watcherFire(w, value) {
|
|
3089
|
+
try {
|
|
3090
|
+
watcherLog(`tetiklendi id=${w.id} name="${w.name}" kind=${w.kind} op=${w.op} value=${value}`);
|
|
3091
|
+
let sid = w.sessionId;
|
|
3092
|
+
const exists = sid && engine.listSessions().some((s) => s.id === sid);
|
|
3093
|
+
if (!exists) {
|
|
3094
|
+
sid = engine.createSession().id;
|
|
3095
|
+
watchers.patch(w.id, { sessionId: sid });
|
|
3096
|
+
}
|
|
3097
|
+
const target =
|
|
3098
|
+
w.op === 'changed'
|
|
3099
|
+
? 'izlenen değer değişti'
|
|
3100
|
+
: `kural sağlandı (son değer ${value}, koşul ${w.op} ${w.value ?? ''})`;
|
|
3101
|
+
engine.send(sid, {
|
|
3102
|
+
text:
|
|
3103
|
+
`[IZLEYICI: ${w.name}] ${target}. ` +
|
|
3104
|
+
'Kullanıcıya bunu kısaca ve net biçimde haber ver; detay gerekirse http_fetch ile güncel durumu kontrol et.',
|
|
3105
|
+
});
|
|
3106
|
+
} catch {}
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
ipcMain.handle('cron:list', () => cron.list());
|
|
3110
|
+
/* #23 Fallout: provider → kayıtlı API key haritası.
|
|
3111
|
+
Birincil kaynak: engine chain (config+custom+env çözülmüş).
|
|
3112
|
+
Yedek: config.yaml/.env + settings.customProviders. */
|
|
3113
|
+
ipcMain.handle('providers:keys', () => {
|
|
3114
|
+
const map = {};
|
|
3115
|
+
try {
|
|
3116
|
+
Object.assign(map, engine.providerKeyMap ? engine.providerKeyMap() : {});
|
|
3117
|
+
} catch {}
|
|
3118
|
+
if (!Object.keys(map).length) {
|
|
3119
|
+
try {
|
|
3120
|
+
for (const p of loadBeastConfig().providers || []) {
|
|
3121
|
+
if (p.id && p.key) map[p.id] = p.key;
|
|
3122
|
+
}
|
|
3123
|
+
} catch {}
|
|
3124
|
+
try {
|
|
3125
|
+
for (const p of settings.customProviders || []) {
|
|
3126
|
+
if (p.id && p.key) map['custom:' + p.id] = p.key;
|
|
3127
|
+
}
|
|
3128
|
+
} catch {}
|
|
3129
|
+
}
|
|
3130
|
+
return map;
|
|
3131
|
+
});
|
|
3132
|
+
/* #22 izleyici paneli IPC */
|
|
3133
|
+
ipcMain.handle('watchers:list', () => watchers.list());
|
|
3134
|
+
ipcMain.handle('watchers:add', (_e, input) => watchers.add(input || {}));
|
|
3135
|
+
ipcMain.handle('watchers:remove', (_e, id) => watchers.remove(String(id || '')));
|
|
3136
|
+
ipcMain.handle('watchers:toggle', (_e, id) => {
|
|
3137
|
+
const w = watchers.get(String(id || ''));
|
|
3138
|
+
if (!w) return { ok: false, error: 'izleyici yok' };
|
|
3139
|
+
watchers.patch(String(id), { enabled: !w.enabled });
|
|
3140
|
+
return { ok: true, watcher: watchers.get(String(id)) };
|
|
3141
|
+
});
|
|
3142
|
+
ipcMain.handle('cron:add', (_e, job) => {
|
|
3143
|
+
const r = cron.add(job || {});
|
|
3144
|
+
cronEmit();
|
|
3145
|
+
return r;
|
|
3146
|
+
});
|
|
3147
|
+
ipcMain.handle('cron:update', (_e, { id, patch }) => {
|
|
3148
|
+
const r = cron.update(id, patch || {});
|
|
3149
|
+
cronEmit();
|
|
3150
|
+
return r;
|
|
3151
|
+
});
|
|
3152
|
+
ipcMain.handle('cron:delete', (_e, id) => {
|
|
3153
|
+
const r = cron.remove(id);
|
|
3154
|
+
cronEmit();
|
|
3155
|
+
return r;
|
|
3156
|
+
});
|
|
3157
|
+
ipcMain.handle('cron:toggle', (_e, id) => {
|
|
3158
|
+
const r = cron.toggle(id);
|
|
3159
|
+
cronEmit();
|
|
3160
|
+
return r;
|
|
3161
|
+
});
|
|
3162
|
+
ipcMain.handle('cron:runNow', (_e, id) => {
|
|
3163
|
+
const r = cron.runNow(id);
|
|
3164
|
+
cronEmit();
|
|
3165
|
+
return r;
|
|
3166
|
+
});
|
|
3167
|
+
|
|
3168
|
+
/* ---------------- tarayıcı IPC ---------------- */
|
|
3169
|
+
ipcMain.handle('browser:toggle', () => {
|
|
3170
|
+
/* kullanıcı kendi açıyor → mutlaka görünür */
|
|
3171
|
+
setBrowserOpen(!browser.open, true);
|
|
3172
|
+
return { open: browser.open, visible: browser.visible };
|
|
3173
|
+
});
|
|
3174
|
+
/* göz ikonu: ajan tarayıcısını görünür/gizli yap */
|
|
3175
|
+
ipcMain.handle('browser:shown:get', () => ({ shown: settings.browserHeadless === false }));
|
|
3176
|
+
ipcMain.handle('browser:shown:set', (_e, v) => {
|
|
3177
|
+
settings.browserHeadless = !v;
|
|
3178
|
+
saveSettings();
|
|
3179
|
+
if (browser.open) {
|
|
3180
|
+
browser.visible = !!v;
|
|
3181
|
+
layoutBrowser();
|
|
3182
|
+
browserEmit({ open: true, width: browser.width });
|
|
3183
|
+
}
|
|
3184
|
+
return { shown: !!v };
|
|
3185
|
+
});
|
|
3186
|
+
ipcMain.handle('browser:navigate', (_e, url) => browserNavigate(url));
|
|
3187
|
+
ipcMain.handle('browser:ctrl', (_e, action) => {
|
|
3188
|
+
if (action === 'close') {
|
|
3189
|
+
setBrowserOpen(false);
|
|
3190
|
+
return { ok: true };
|
|
3191
|
+
}
|
|
3192
|
+
const wc = browser.view && browser.view.webContents;
|
|
3193
|
+
if (!wc) return { ok: false };
|
|
3194
|
+
try {
|
|
3195
|
+
if (action === 'back') wc.goBack();
|
|
3196
|
+
else if (action === 'forward') wc.goForward();
|
|
3197
|
+
else if (action === 'reload') wc.reload();
|
|
3198
|
+
return { ok: true };
|
|
3199
|
+
} catch {
|
|
3200
|
+
return { ok: false };
|
|
3201
|
+
}
|
|
3202
|
+
});
|
|
3203
|
+
ipcMain.handle('browser:setIgnoreMouse', (_e, flag) => {
|
|
3204
|
+
try {
|
|
3205
|
+
if (browser.view && browser.open) browser.view.webContents.setIgnoreMouseEvents(!!flag);
|
|
3206
|
+
return { ok: true };
|
|
3207
|
+
} catch {
|
|
3208
|
+
return { ok: false };
|
|
3209
|
+
}
|
|
3210
|
+
});
|
|
3211
|
+
ipcMain.handle('browser:setWidth', (_e, wpx) => {
|
|
3212
|
+
if (!win || win.isDestroyed()) return { ok: false };
|
|
3213
|
+
const [w] = win.getContentSize();
|
|
3214
|
+
/* kullanıcı sürükleyerek gerçek tercihini belirler — kalıcı saklanır */
|
|
3215
|
+
browser.width = Math.max(300, Math.min(Number(wpx) || 480, Math.max(340, w - 320)));
|
|
3216
|
+
settings.browserWidth = browser.width;
|
|
3217
|
+
saveSettings();
|
|
3218
|
+
layoutBrowser();
|
|
3219
|
+
browserEmit({ open: true, width: browser.width });
|
|
3220
|
+
return { ok: true, width: browser.width };
|
|
3221
|
+
});
|
|
3222
|
+
ipcMain.handle('browser:screenshot', async () => {
|
|
3223
|
+
const r = await browserScreenshot();
|
|
3224
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
3225
|
+
return { ok: true, image: r.__injectImage, url: r.url, title: r.title };
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3228
|
+
/* ---------------- terminal panel IPC ----------------
|
|
3229
|
+
Sağ dock paneli renderer'da; main sadece PowerShell komutlarını
|
|
3230
|
+
çalıştırır ve çıktıyı canlı akıtır. Terminal ile tarayıcı aynı
|
|
3231
|
+
dock'u paylaştığı için ikisi aynı anda açık kalamaz. */
|
|
3232
|
+
let termChild = null;
|
|
3233
|
+
const TERM_FWD_CAP = 1024 * 1024; /* iletilecek çıktı üst sınırı (1 MB) */
|
|
3234
|
+
|
|
3235
|
+
function termSend(ev) {
|
|
3236
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', ev);
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
ipcMain.handle('terminal:toggle', () => {
|
|
3240
|
+
if (browser.open) setBrowserOpen(false);
|
|
3241
|
+
return { ok: true, cwd: (engine && engine.workspace) || settings.workspace || app.getPath('home') };
|
|
3242
|
+
});
|
|
3243
|
+
|
|
3244
|
+
/* Git Bash kuruluysa yolunu bulur; yoksa null döner */
|
|
3245
|
+
function findGitBash() {
|
|
3246
|
+
const cands = [];
|
|
3247
|
+
if (process.env['ProgramFiles']) cands.push(path.join(process.env['ProgramFiles'], 'Git', 'bin', 'bash.exe'));
|
|
3248
|
+
if (process.env['ProgramFiles(x86)']) cands.push(path.join(process.env['ProgramFiles(x86)'], 'Git', 'bin', 'bash.exe'));
|
|
3249
|
+
if (process.env['LocalAppData']) cands.push(path.join(process.env['LocalAppData'], 'Programs', 'Git', 'bin', 'bash.exe'));
|
|
3250
|
+
cands.push('C:\\Program Files\\Git\\bin\\bash.exe');
|
|
3251
|
+
for (const c of cands) {
|
|
3252
|
+
try { if (fs.existsSync(c)) return c; } catch {}
|
|
3253
|
+
}
|
|
3254
|
+
return null;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
ipcMain.handle('terminal:run', (_e, payload) => {
|
|
3258
|
+
const cmd = String((payload && payload.cmd) || '').trim();
|
|
3259
|
+
const shell = String((payload && payload.shell) || 'powershell');
|
|
3260
|
+
if (!cmd) return { ok: false, error: 'boş komut' };
|
|
3261
|
+
if (termChild) return { ok: false, error: 'önceki komut sürüyor — ■ ile durdurabilirsin' };
|
|
3262
|
+
const cwd = (engine && engine.workspace) || settings.workspace || app.getPath('home');
|
|
3263
|
+
const id = 't' + Date.now().toString(36);
|
|
3264
|
+
let file, args;
|
|
3265
|
+
if (shell === 'cmd') {
|
|
3266
|
+
file = 'cmd.exe';
|
|
3267
|
+
args = ['/d', '/s', '/c', cmd];
|
|
3268
|
+
} else if (shell === 'bash') {
|
|
3269
|
+
const bash = findGitBash();
|
|
3270
|
+
if (!bash) return { ok: false, error: 'Git Bash bulunamadı — Git for Windows kurulu mu? (https://git-scm.com)' };
|
|
3271
|
+
file = bash;
|
|
3272
|
+
args = ['-c', cmd];
|
|
3273
|
+
} else {
|
|
3274
|
+
file = 'powershell.exe';
|
|
3275
|
+
args = ['-NoProfile', '-NoLogo', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd];
|
|
3276
|
+
}
|
|
3277
|
+
let child;
|
|
3278
|
+
try {
|
|
3279
|
+
child = spawn(file, args, { cwd, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
3280
|
+
} catch (e) {
|
|
3281
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
3282
|
+
}
|
|
3283
|
+
termChild = child;
|
|
3284
|
+
child.stdout.setEncoding('utf8');
|
|
3285
|
+
child.stderr.setEncoding('utf8');
|
|
3286
|
+
let forwarded = 0;
|
|
3287
|
+
let capNotified = false;
|
|
3288
|
+
const push = (stream, d) => {
|
|
3289
|
+
if (forwarded > TERM_FWD_CAP) {
|
|
3290
|
+
if (!capNotified) {
|
|
3291
|
+
capNotified = true;
|
|
3292
|
+
termSend({ type: 'term-out', id, stream, chunk: '\n[beast] çıktı çok büyük — iletim durduruldu (komut sürüyor)\n' });
|
|
3293
|
+
}
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
forwarded += String(d).length;
|
|
3297
|
+
termSend({ type: 'term-out', id, stream, chunk: String(d) });
|
|
3298
|
+
};
|
|
3299
|
+
child.stdout.on('data', (d) => push('out', d));
|
|
3300
|
+
child.stderr.on('data', (d) => push('err', d));
|
|
3301
|
+
child.on('error', (err) => {
|
|
3302
|
+
if (termChild === child) termChild = null;
|
|
3303
|
+
termSend({ type: 'term-end', id, code: -1, error: String((err && err.message) || err) });
|
|
3304
|
+
});
|
|
3305
|
+
child.on('close', (code) => {
|
|
3306
|
+
if (termChild === child) termChild = null;
|
|
3307
|
+
termSend({ type: 'term-end', id, code: code == null ? -1 : code });
|
|
3308
|
+
});
|
|
3309
|
+
return { ok: true, id };
|
|
3310
|
+
});
|
|
3311
|
+
|
|
3312
|
+
ipcMain.handle('terminal:stop', () => {
|
|
3313
|
+
if (!termChild) return { ok: false };
|
|
3314
|
+
try { spawn('taskkill', ['/pid', String(termChild.pid), '/T', '/F'], { windowsHide: true }); } catch {}
|
|
3315
|
+
return { ok: true };
|
|
3316
|
+
});
|
|
3317
|
+
|
|
3318
|
+
/* düşünme (reasoning) seviyesi */
|
|
3319
|
+
ipcMain.handle('think:set', (_e, v) => {
|
|
3320
|
+
setThinkLevel(v);
|
|
3321
|
+
return engine.publicState();
|
|
3322
|
+
});
|
|
3323
|
+
|
|
3324
|
+
/* Exa web arama anahtarı (Ayarlar → Web Arama) */
|
|
3325
|
+
ipcMain.handle('exa:get', () => {
|
|
3326
|
+
const k = settings.exaKey || '';
|
|
3327
|
+
return { set: !!k, masked: k ? '••••••••' + k.slice(-4) : '' };
|
|
3328
|
+
});
|
|
3329
|
+
ipcMain.handle('exa:set', (_e, key) => {
|
|
3330
|
+
const v = String(key || '').trim();
|
|
3331
|
+
if (v) {
|
|
3332
|
+
settings.exaKey = v;
|
|
3333
|
+
saveSettings();
|
|
3334
|
+
setExaKey(settings.exaKey);
|
|
3335
|
+
}
|
|
3336
|
+
const k = settings.exaKey || '';
|
|
3337
|
+
return { ok: true, set: !!k, masked: k ? '••••••••' + k.slice(-4) : '' };
|
|
3338
|
+
});
|
|
3339
|
+
ipcMain.handle('exa:clear', () => {
|
|
3340
|
+
settings.exaKey = '';
|
|
3341
|
+
saveSettings();
|
|
3342
|
+
setExaKey(null);
|
|
3343
|
+
return { ok: true, set: false, masked: '' };
|
|
3344
|
+
});
|
|
3345
|
+
|
|
3346
|
+
/* #TinyFish: anahtar girilirse web_search zincirinin BAŞINDA kullanılır */
|
|
3347
|
+
ipcMain.handle('tinyfish:get', () => {
|
|
3348
|
+
const k = settings.tinyfishKey || '';
|
|
3349
|
+
return { set: !!k, masked: k ? '••••••••' + k.slice(-4) : '' };
|
|
3350
|
+
});
|
|
3351
|
+
ipcMain.handle('tinyfish:set', (_e, key) => {
|
|
3352
|
+
const v = String(key || '').trim();
|
|
3353
|
+
if (v) {
|
|
3354
|
+
settings.tinyfishKey = v;
|
|
3355
|
+
saveSettings();
|
|
3356
|
+
setTinyfishKey(settings.tinyfishKey);
|
|
3357
|
+
}
|
|
3358
|
+
const k = settings.tinyfishKey || '';
|
|
3359
|
+
return { ok: true, set: !!k, masked: k ? '••••••••' + k.slice(-4) : '' };
|
|
3360
|
+
});
|
|
3361
|
+
ipcMain.handle('tinyfish:clear', () => {
|
|
3362
|
+
settings.tinyfishKey = '';
|
|
3363
|
+
saveSettings();
|
|
3364
|
+
setTinyfishKey(null);
|
|
3365
|
+
return { ok: true, set: false, masked: '' };
|
|
3366
|
+
});
|
|
3367
|
+
|
|
3368
|
+
ipcMain.handle('custom:set', (_e, list) => {
|
|
3369
|
+
settings.customProviders = Array.isArray(list) ? list : [];
|
|
3370
|
+
saveSettings();
|
|
3371
|
+
engine.setCustomProviders(settings.customProviders);
|
|
3372
|
+
return engine.publicState();
|
|
3373
|
+
});
|
|
3374
|
+
|
|
3375
|
+
/* #Model refresh: taban zinciri tazele + kayıtlı tüm custom providerların
|
|
3376
|
+
modellerini yeniden çek (picker anında güncellenir). */
|
|
3377
|
+
ipcMain.handle('models:refresh', async () => {
|
|
3378
|
+
try {
|
|
3379
|
+
const cfg = loadBeastConfig();
|
|
3380
|
+
if (cfg && engine.refreshBaseChain) engine.refreshBaseChain(cfg);
|
|
3381
|
+
} catch (e) {
|
|
3382
|
+
log.error('main', 'model refresh (base): ' + String((e && e.message) || e));
|
|
3383
|
+
}
|
|
3384
|
+
let updated = 0;
|
|
3385
|
+
const list = Array.isArray(settings.customProviders) ? settings.customProviders : [];
|
|
3386
|
+
for (const p of list) {
|
|
3387
|
+
if (!p || !p.baseUrl || !p.key) continue;
|
|
3388
|
+
try {
|
|
3389
|
+
const b = String(p.baseUrl).trim().replace(/\/+$/, '');
|
|
3390
|
+
const url = /\/v\d+$/.test(b) ? b + '/models' : b + '/v1/models';
|
|
3391
|
+
const res = await fetch(url, {
|
|
3392
|
+
headers: { Authorization: 'Bearer ' + p.key },
|
|
3393
|
+
signal: AbortSignal.timeout(15000),
|
|
3394
|
+
});
|
|
3395
|
+
if (!res.ok) continue;
|
|
3396
|
+
const json = await res.json();
|
|
3397
|
+
const models = (json.data || json.models || [])
|
|
3398
|
+
.map((m) => (typeof m === 'string' ? m : m.id || m.name))
|
|
3399
|
+
.filter(Boolean);
|
|
3400
|
+
const uniq = [...new Set(models)].sort();
|
|
3401
|
+
if (uniq.length && JSON.stringify(uniq) !== JSON.stringify(p.models || [])) {
|
|
3402
|
+
p.models = uniq;
|
|
3403
|
+
updated++;
|
|
3404
|
+
}
|
|
3405
|
+
} catch {}
|
|
3406
|
+
}
|
|
3407
|
+
if (updated) {
|
|
3408
|
+
settings.customProviders = list;
|
|
3409
|
+
saveSettings();
|
|
3410
|
+
engine.setCustomProviders(list);
|
|
3411
|
+
}
|
|
3412
|
+
log.info('main', `model refresh: taban tazelendi, ${updated} custom provider güncellendi`);
|
|
3413
|
+
return engine.publicState();
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
ipcMain.handle('custom:fetchModels', async (_e, { baseUrl, key }) => {
|
|
3417
|
+
try {
|
|
3418
|
+
let b = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
3419
|
+
if (!/^https?:\/\//i.test(b)) return { ok: false, error: 'URL http(s) ile başlamalı' };
|
|
3420
|
+
const url = /\/v\d+$/.test(b) ? b + '/models' : b + '/v1/models';
|
|
3421
|
+
const res = await fetch(url, {
|
|
3422
|
+
headers: key ? { Authorization: 'Bearer ' + key } : {},
|
|
3423
|
+
signal: AbortSignal.timeout(15000),
|
|
3424
|
+
});
|
|
3425
|
+
if (!res.ok) return { ok: false, error: 'HTTP ' + res.status };
|
|
3426
|
+
const json = await res.json();
|
|
3427
|
+
const models = (json.data || json.models || [])
|
|
3428
|
+
.map((m) => (typeof m === 'string' ? m : m.id || m.name))
|
|
3429
|
+
.filter(Boolean);
|
|
3430
|
+
return { ok: true, models: [...new Set(models)].sort() };
|
|
3431
|
+
} catch (e) {
|
|
3432
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
3433
|
+
}
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
/* ---------------- WhatsApp IPC ---------------- */
|
|
3437
|
+
|
|
3438
|
+
ipcMain.handle('wa:start', () => {
|
|
3439
|
+
ensureWa().start().catch(() => {});
|
|
3440
|
+
return wa.snapshot();
|
|
3441
|
+
});
|
|
3442
|
+
|
|
3443
|
+
ipcMain.handle('wa:stop', async () => {
|
|
3444
|
+
if (wa) await wa.stop();
|
|
3445
|
+
return { status: 'disconnected' };
|
|
3446
|
+
});
|
|
3447
|
+
|
|
3448
|
+
ipcMain.handle('wa:reset', async () => {
|
|
3449
|
+
if (!wa) ensureWa();
|
|
3450
|
+
await wa.resetAuth();
|
|
3451
|
+
return { status: 'disconnected' };
|
|
3452
|
+
});
|
|
3453
|
+
|
|
3454
|
+
ipcMain.handle('wa:status', () => (wa ? wa.snapshot() : { status: 'disconnected', available: true }));
|
|
3455
|
+
|
|
3456
|
+
ipcMain.handle('wa:allow:get', () => settings.waAllow || []);
|
|
3457
|
+
|
|
3458
|
+
ipcMain.handle('wa:sessions', () => [...waChats.values()]);
|
|
3459
|
+
|
|
3460
|
+
ipcMain.handle('wa:tts:get', () => settings.waTts || {});
|
|
3461
|
+
ipcMain.handle('wa:tts:set', (_e, cfg) => {
|
|
3462
|
+
settings.waTts = {
|
|
3463
|
+
enabled: !!(cfg && cfg.enabled),
|
|
3464
|
+
baseUrl: String((cfg && cfg.baseUrl) || '').trim(),
|
|
3465
|
+
key: String((cfg && cfg.key) || '').trim(),
|
|
3466
|
+
model: String((cfg && cfg.model) || '').trim(),
|
|
3467
|
+
voice: String((cfg && cfg.voice) || '').trim(),
|
|
3468
|
+
};
|
|
3469
|
+
saveSettings();
|
|
3470
|
+
return settings.waTts;
|
|
3471
|
+
});
|
|
3472
|
+
|
|
3473
|
+
/* ---------- e-posta IPC ---------- */
|
|
3474
|
+
|
|
3475
|
+
ipcMain.handle('email:get', () => {
|
|
3476
|
+
const cfg = { ...emailCfg() };
|
|
3477
|
+
/* Şifreyi renderera düz metin sızdırma; boş alan = değişmedi anlamında */
|
|
3478
|
+
if (cfg.pass) cfg.pass = '***';
|
|
3479
|
+
return cfg;
|
|
3480
|
+
});
|
|
3481
|
+
ipcMain.handle('email:set', (_e, cfg) => {
|
|
3482
|
+
const cur = settings.email || {};
|
|
3483
|
+
const newPass = String((cfg && cfg.pass) ?? '').trim();
|
|
3484
|
+
settings.email = {
|
|
3485
|
+
host: String((cfg && cfg.host) || '').trim() || 'imap.gmail.com',
|
|
3486
|
+
port: Number(cfg && cfg.port) || 993,
|
|
3487
|
+
user: String((cfg && cfg.user) || '').trim(),
|
|
3488
|
+
/* Boş veya maskeli gelen şifre = mevcut anahtarı koru */
|
|
3489
|
+
pass: newPass && newPass !== '***' ? newPass : String(cur.pass || ''),
|
|
3490
|
+
smtpHost: String((cfg && cfg.smtpHost) || '').trim() || 'smtp.gmail.com',
|
|
3491
|
+
smtpPort: Number(cfg && cfg.smtpPort) || 465,
|
|
3492
|
+
};
|
|
3493
|
+
saveSettings();
|
|
3494
|
+
return { ...settings.email, pass: settings.email.pass ? '***' : '' };
|
|
3495
|
+
});
|
|
3496
|
+
|
|
3497
|
+
/* ---------- ekran görüntüsü ---------- */
|
|
3498
|
+
|
|
3499
|
+
/* Ana ekrandan JPEG dataURL yakalar (computer_use ve screen:capture ortak) */
|
|
3500
|
+
async function captureScreenDataUrl() {
|
|
3501
|
+
try {
|
|
3502
|
+
const sources = await desktopCapturer.getSources({
|
|
3503
|
+
types: ['screen'],
|
|
3504
|
+
thumbnailSize: { width: 1600, height: 900 },
|
|
3505
|
+
});
|
|
3506
|
+
if (!sources.length) return null;
|
|
3507
|
+
let best = sources[0];
|
|
3508
|
+
for (const s of sources) {
|
|
3509
|
+
const a = s.thumbnail.getSize();
|
|
3510
|
+
const b = best.thumbnail.getSize();
|
|
3511
|
+
if (a.width * a.height > b.width * b.height) best = s;
|
|
3512
|
+
}
|
|
3513
|
+
const img = best.thumbnail.resize({ width: 1280 });
|
|
3514
|
+
const jpeg = img.toJPEG(72);
|
|
3515
|
+
if (!jpeg || !jpeg.length) return null;
|
|
3516
|
+
return 'data:image/jpeg;base64,' + jpeg.toString('base64');
|
|
3517
|
+
} catch {
|
|
3518
|
+
return null;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
ipcMain.handle('screen:capture', async () => {
|
|
3523
|
+
const image = await captureScreenDataUrl();
|
|
3524
|
+
return image ? { ok: true, image } : { ok: false, error: 'ekran görüntüsü alınamadı' };
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
/* #26 ajanın send_file aracı: dosyayı doğru kanala (WA veya chat) ulaştırır.
|
|
3528
|
+
Paralel ajan işiyse parent sohbete, masaüstünde dosya kartı olarak düşer. */
|
|
3529
|
+
const FILE_IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
|
|
3530
|
+
async function deliverFile(sessionId, filePath, caption) {
|
|
3531
|
+
try {
|
|
3532
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.join(engine.workspace || process.cwd(), filePath);
|
|
3533
|
+
if (!fs.existsSync(abs)) return { ok: false, error: 'dosya bulunamadı: ' + abs };
|
|
3534
|
+
const name = path.basename(abs);
|
|
3535
|
+
const ext = path.extname(abs).toLowerCase();
|
|
3536
|
+
let sid = String(sessionId || '');
|
|
3537
|
+
try {
|
|
3538
|
+
const job = engine.listBgJobs().find((j) => j.id === sid);
|
|
3539
|
+
if (job && job.parentId) sid = job.parentId; // paralel ajan → parent sohbet
|
|
3540
|
+
} catch {}
|
|
3541
|
+
/* WhatsApp hedefi: bu oturum bir WA sohbetine bağlıysa oraya gönder */
|
|
3542
|
+
let jid = null;
|
|
3543
|
+
for (const [j, s] of waChats) {
|
|
3544
|
+
if (s === sid) { jid = j; break; }
|
|
3545
|
+
}
|
|
3546
|
+
if (jid && wa) {
|
|
3547
|
+
const buf = fs.readFileSync(abs);
|
|
3548
|
+
const ok = FILE_IMAGE_EXT.has(ext)
|
|
3549
|
+
? await wa.sendImage(jid, buf, caption || name)
|
|
3550
|
+
: await wa.sendFile(jid, buf, name, caption);
|
|
3551
|
+
if (ok) return { ok: true, channel: 'whatsapp', name };
|
|
3552
|
+
}
|
|
3553
|
+
/* masaüstü: sohbete dosya kartı bas */
|
|
3554
|
+
if (win && !win.isDestroyed()) {
|
|
3555
|
+
win.webContents.send('agent:event', { sessionId: sid, type: 'file', path: abs, name, caption: caption || '' });
|
|
3556
|
+
return { ok: true, channel: 'chat', name };
|
|
3557
|
+
}
|
|
3558
|
+
return { ok: false, error: 'gönderim hedefi bulunamadı (WA kapalı, pencere gizli)' };
|
|
3559
|
+
} catch (e) {
|
|
3560
|
+
return { ok: false, error: String((e && e.message) || e) };
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
ipcMain.handle('shell:openPath', (_e, p) => shell.openPath(String(p || '')));
|
|
3565
|
+
ipcMain.handle('shell:showItem', (_e, p) => {
|
|
3566
|
+
shell.showItemInFolder(String(p || ''));
|
|
3567
|
+
return true;
|
|
3568
|
+
});
|
|
3569
|
+
|
|
3570
|
+
ipcMain.handle('wa:allow:set', (_e, list) => {
|
|
3571
|
+
const before = JSON.stringify(settings.waAllow || []);
|
|
3572
|
+
const out = [];
|
|
3573
|
+
let ownerCount = 0;
|
|
3574
|
+
for (const item of Array.isArray(list) ? list : []) {
|
|
3575
|
+
// '*' herkese açık bayrağı
|
|
3576
|
+
if (item === '*' || (item && item.num === '*')) { out.push('*'); continue; }
|
|
3577
|
+
const rawNum = typeof item === 'string' ? item : String((item && item.num) || '');
|
|
3578
|
+
const name = typeof item === 'string' ? '' : String((item && item.name) || '').trim().slice(0, 40);
|
|
3579
|
+
const permRaw = typeof item === 'object' && item.perm;
|
|
3580
|
+
const perm = ['all', 'web', 'read', 'chat'].includes(permRaw)
|
|
3581
|
+
? permRaw
|
|
3582
|
+
: typeof item === 'object' && item.lockdown ? 'chat' : 'all';
|
|
3583
|
+
/* #v13.1: owner bayrağı — yalnız BİR kişi sahip olabilir */
|
|
3584
|
+
const wantsOwner = typeof item === 'object' && !!item.owner;
|
|
3585
|
+
const d = rawNum.replace(/\D/g, '');
|
|
3586
|
+
if (d.length >= 6) {
|
|
3587
|
+
const isOwner = wantsOwner && ownerCount === 0;
|
|
3588
|
+
if (isOwner) ownerCount++;
|
|
3589
|
+
out.push({ num: d, name, lockdown: perm === 'chat', perm, owner: isOwner });
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
/* tek izinli kişi varsa MECBURİ sahip (kimseye seçtirmeden) */
|
|
3593
|
+
const nonStar = out.filter((o) => o !== '*');
|
|
3594
|
+
if (nonStar.length === 1 && !nonStar[0].owner) {
|
|
3595
|
+
nonStar[0].owner = true;
|
|
3596
|
+
}
|
|
3597
|
+
settings.waAllow = out;
|
|
3598
|
+
saveSettings();
|
|
3599
|
+
/* sahibi MEMORY.md + USER.md'ye işle (ajan kiminle konuştuğunu bilsin) */
|
|
3600
|
+
try {
|
|
3601
|
+
const owner = out.find((o) => o !== '*' && o.owner);
|
|
3602
|
+
if (owner) {
|
|
3603
|
+
memory.addRule(`Beast'in SAHİBİ ${owner.name} (+${owner.num}). Onun talepleri önceliklidir.`);
|
|
3604
|
+
memory.save('USER.md', `${owner.name} (WhatsApp: +${owner.num}) — Beast'in sahibi.\nDiğer izinli numaralar MISAFİRDİR; sahibiyle çelişirse sahibin sözü geçer.`);
|
|
3605
|
+
}
|
|
3606
|
+
} catch {}
|
|
3607
|
+
/* #v13: izinli listeye YENİ numara eklendiyse temiz durum için otomatik restart */
|
|
3608
|
+
if (!before.includes(JSON.stringify(out)) ) {
|
|
3609
|
+
const addedNew = out.some((o) => {
|
|
3610
|
+
if (o === '*') return !JSON.parse(before).includes('*');
|
|
3611
|
+
return !JSON.parse(before).some((b) => b && b.num === o.num);
|
|
3612
|
+
});
|
|
3613
|
+
if (addedNew) {
|
|
3614
|
+
waLog('yeni izinli kişi eklendi — uygulama 3 sn içinde otomatik yeniden başlatılıyor');
|
|
3615
|
+
setTimeout(() => {
|
|
3616
|
+
app.relaunch();
|
|
3617
|
+
app.exit(0);
|
|
3618
|
+
}, 3000);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
return settings.waAllow;
|
|
3622
|
+
});
|