beast-agent 0.21.0 → 0.23.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/README.md +1 -1
- package/bin/beast-agent.js +16 -6
- package/package.json +3 -2
- package/src/agent/bots.js +17 -3
- package/src/agent/engine.js +35 -17
- package/src/agent/telegram.js +155 -0
- package/src/main.js +554 -17
- package/src/preload.js +8 -0
- package/src/renderer/i18n.js +34 -2
- package/src/renderer/index.html +1 -0
- package/src/renderer/renderer.js +370 -14
- package/src/renderer/style.css +28 -0
package/src/main.js
CHANGED
|
@@ -4,6 +4,7 @@ const { app, BrowserWindow, WebContentsView, ipcMain, shell, dialog, Tray, Menu,
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const http = require('http');
|
|
7
|
+
const dns = require('dns');
|
|
7
8
|
const crypto = require('crypto');
|
|
8
9
|
const { spawn } = require('child_process');
|
|
9
10
|
const Engine = require('./agent/engine');
|
|
@@ -14,6 +15,7 @@ const memory = require('./agent/memory');
|
|
|
14
15
|
const skillsMod = require('./agent/skills');
|
|
15
16
|
const storeMod = require('./agent/store');
|
|
16
17
|
const { WhatsAppBridge } = require('./agent/whatsapp');
|
|
18
|
+
const { TelegramBridge } = require('./agent/telegram');
|
|
17
19
|
const cron = require('./cron');
|
|
18
20
|
const watchers = require('./agent/watchers');
|
|
19
21
|
const usageMod = require('./agent/usage');
|
|
@@ -193,6 +195,7 @@ const BUILTIN_PROVIDERS = [
|
|
|
193
195
|
{ id: 'gemini', name: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', hint: 'aistudio.google.com/apikey' },
|
|
194
196
|
{ id: 'zhipu', name: 'Zhipu AI', baseUrl: 'https://api.z.ai/api/paas/v4', hint: 'z.ai model konsolu' },
|
|
195
197
|
{ id: 'groq', name: 'Groq', baseUrl: 'https://api.groq.com/openai/v1', hint: 'console.groq.com/keys' },
|
|
198
|
+
{ id: 'nvidia', name: 'NVIDIA NIM', baseUrl: 'https://integrate.api.nvidia.com/v1', hint: 'build.nvidia.com — ücretsiz kredi veriyor, talep yüksek' },
|
|
196
199
|
];
|
|
197
200
|
const SESSIONS_DIR = path.join(APP_DIR, 'sessions');
|
|
198
201
|
const SETTINGS_FILE = path.join(APP_DIR, 'settings.json');
|
|
@@ -200,6 +203,8 @@ const SETTINGS_BACKUP_FILE = path.join(APP_DIR, 'settings.backup.json');
|
|
|
200
203
|
const WA_AUTH_DIR = path.join(APP_DIR, 'wa-auth');
|
|
201
204
|
const WA_CHATS_FILE = path.join(APP_DIR, 'wa-chats.json');
|
|
202
205
|
const FALLOUT_CRASH_FILE = path.join(APP_DIR, 'fallout-crash.json');
|
|
206
|
+
const CHAT_QUEUE_FILE = path.join(APP_DIR, 'chat_queue.json');
|
|
207
|
+
const TG_CHATS_FILE = path.join(APP_DIR, 'tg-chats.json');
|
|
203
208
|
|
|
204
209
|
for (const d of [APP_DIR, SESSIONS_DIR]) fs.mkdirSync(d, { recursive: true });
|
|
205
210
|
|
|
@@ -215,6 +220,10 @@ let waChats = new Map(); // jid -> aktif session id
|
|
|
215
220
|
let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
|
|
216
221
|
let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
|
|
217
222
|
const WA_HISTORY_CAP = 20;
|
|
223
|
+
let tg = null;
|
|
224
|
+
let tgChats = new Map(); // telegram chatId -> aktif session id
|
|
225
|
+
let tgHistory = new Map(); // chatId -> [sid,...]
|
|
226
|
+
const TG_HISTORY_CAP = 20;
|
|
218
227
|
let tray = null;
|
|
219
228
|
app.isQuitting = false;
|
|
220
229
|
|
|
@@ -449,6 +458,34 @@ function isWaAllowed(senderNum) {
|
|
|
449
458
|
return !!waFind(senderNum);
|
|
450
459
|
}
|
|
451
460
|
|
|
461
|
+
/* ---------- TELEGRAM (FEATURE 3): allow list — WA ile aynı mantık ----------
|
|
462
|
+
Liste formatı: [{ id:'123456789' | '@kullanici_adi', name, perm, bot_id }, '*']
|
|
463
|
+
Eşleşme: sayısal ID birebir, @username büyük/küçük harf duyarsız. */
|
|
464
|
+
function tgLog(line) {
|
|
465
|
+
try { log.info('telegram', line); } catch {}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function tgFind(senderId, username) {
|
|
469
|
+
const list = settings.tgAllow || [];
|
|
470
|
+
if (!list.length) return null; // boş liste = kimseye cevap yok
|
|
471
|
+
const id = String(senderId || '').trim();
|
|
472
|
+
const uname = String(username || '').replace(/^@/, '').toLowerCase();
|
|
473
|
+
for (const e of list) {
|
|
474
|
+
if (e === '*') return { id: '*', name: '' };
|
|
475
|
+
const eid = typeof e === 'string' ? e.trim() : String((e && e.id) || '').trim();
|
|
476
|
+
if (!eid) continue;
|
|
477
|
+
if (eid === '*') return { id: '*', name: '' };
|
|
478
|
+
if (eid.startsWith('@')) {
|
|
479
|
+
if (uname && eid.slice(1).toLowerCase() === uname) {
|
|
480
|
+
return typeof e === 'string' ? { id: eid, name: '' } : e;
|
|
481
|
+
}
|
|
482
|
+
} else if (id && eid === id) {
|
|
483
|
+
return typeof e === 'string' ? { id: eid, name: '' } : e;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
452
489
|
/* Sahip: owner işaretli kayıt; yoksa listedeki ilk kişi. /allow ve /block
|
|
453
490
|
yalnızca sahip tarafından kullanılabilir (yabancı DM kendi kendini ekleyemesin). */
|
|
454
491
|
function waOwnerNum() {
|
|
@@ -1333,13 +1370,37 @@ function scheduleReminder({ when, message, sessionId, repeat }) {
|
|
|
1333
1370
|
/* ---------------- BOT SİSTEMİ yardımcıları ----------------
|
|
1334
1371
|
Bot eşleştirme, izolasyon, whitelist.json aynası ve bot istatistikleri. */
|
|
1335
1372
|
|
|
1336
|
-
|
|
1373
|
+
/* İzin değerini araç kümesine çevirir; null = tüm araçlar ('all').
|
|
1374
|
+
'web' / ['web','read'] / 'web,read' biçimlerini kabul eder. */
|
|
1375
|
+
function permToToolSet(p) {
|
|
1376
|
+
const { PERM_TOOL_SETS } = require('./agent/engine');
|
|
1377
|
+
const set = new Set();
|
|
1378
|
+
for (const raw of Array.isArray(p) ? p : String(p == null ? 'all' : p).split(',')) {
|
|
1379
|
+
const k = String(raw).trim() || 'all';
|
|
1380
|
+
if (k === 'all' || !PERM_TOOL_SETS[k]) return null;
|
|
1381
|
+
for (const t of PERM_TOOL_SETS[k]) set.add(t);
|
|
1382
|
+
}
|
|
1383
|
+
return set;
|
|
1384
|
+
}
|
|
1337
1385
|
|
|
1386
|
+
/* Hangi izin daha kısıtlıysa o kazanır. Dizi (çoklu bot izni) destekler:
|
|
1387
|
+
araç kümesi diğerinin alt kümesiyse o geçer; ikisi de alt küme değilse
|
|
1388
|
+
daha küçük küme kazanır (eşitse kişi yetkisi). */
|
|
1338
1389
|
function moreRestrictivePerm(a, b) {
|
|
1339
|
-
const
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1390
|
+
const sa = permToToolSet(a);
|
|
1391
|
+
const sb = permToToolSet(b);
|
|
1392
|
+
if (sa === null && sb === null) return 'all';
|
|
1393
|
+
if (sa === null) return b; // b kısıtlı
|
|
1394
|
+
if (sb === null) return a; // a kısıtlı
|
|
1395
|
+
const aSubB = [...sa].every((t) => sb.has(t));
|
|
1396
|
+
const bSubA = [...sb].every((t) => sa.has(t));
|
|
1397
|
+
if (aSubB && !bSubA) return a;
|
|
1398
|
+
if (bSubA && !aSubB) return b;
|
|
1399
|
+
return sa.size <= sb.size ? a : b;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function fmtPerm(p) {
|
|
1403
|
+
return Array.isArray(p) ? '[' + p.join('+') + ']' : String(p);
|
|
1343
1404
|
}
|
|
1344
1405
|
|
|
1345
1406
|
/* Bot skill checkbox'ları → oturumun görebileceği araç adları.
|
|
@@ -1623,7 +1684,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
|
1623
1684
|
} else {
|
|
1624
1685
|
engine.setSessionTools(sid, null);
|
|
1625
1686
|
}
|
|
1626
|
-
waLog(`perm=${perm} bot=${botId} sid=${sid}`);
|
|
1687
|
+
waLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
|
|
1627
1688
|
|
|
1628
1689
|
const participantName = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
|
|
1629
1690
|
/* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
|
|
@@ -1734,6 +1795,206 @@ function ensureWa() {
|
|
|
1734
1795
|
return wa;
|
|
1735
1796
|
}
|
|
1736
1797
|
|
|
1798
|
+
/* ---------- TELEGRAM ENTEGRASYONU (FEATURE 3) ----------
|
|
1799
|
+
WhatsApp ile aynı akış: gelen mesaj → allow list kontrolü → oturuma bağla
|
|
1800
|
+
(bot eşleme + granül izin) → engine.send; cevap done/error olayında geri
|
|
1801
|
+
gider. Anti-spam: 4.5 sn birleştirme penceresi (WA ile aynı). */
|
|
1802
|
+
|
|
1803
|
+
(function tgChatsLoad() {
|
|
1804
|
+
try {
|
|
1805
|
+
const raw = JSON.parse(fs.readFileSync(TG_CHATS_FILE, 'utf8'));
|
|
1806
|
+
if (raw && typeof raw.chats === 'object') {
|
|
1807
|
+
for (const [c, s] of Object.entries(raw.chats)) {
|
|
1808
|
+
if (typeof s === 'string') tgChats.set(c, s);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (raw && typeof raw.history === 'object') {
|
|
1812
|
+
for (const [c, arr] of Object.entries(raw.history)) {
|
|
1813
|
+
if (Array.isArray(arr)) tgHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-TG_HISTORY_CAP));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
for (const [c, s] of tgChats.entries()) {
|
|
1817
|
+
const h = tgHistory.get(c) || [];
|
|
1818
|
+
if (!h.includes(s)) h.push(s);
|
|
1819
|
+
tgHistory.set(c, h.slice(-TG_HISTORY_CAP));
|
|
1820
|
+
}
|
|
1821
|
+
} catch {}
|
|
1822
|
+
})();
|
|
1823
|
+
|
|
1824
|
+
function saveTgChats() {
|
|
1825
|
+
try {
|
|
1826
|
+
fs.writeFileSync(
|
|
1827
|
+
TG_CHATS_FILE,
|
|
1828
|
+
JSON.stringify({
|
|
1829
|
+
chats: Object.fromEntries(tgChats),
|
|
1830
|
+
history: Object.fromEntries([...tgHistory.entries()].map(([c, a]) => [c, a.slice(-TG_HISTORY_CAP)])),
|
|
1831
|
+
})
|
|
1832
|
+
);
|
|
1833
|
+
} catch {}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function tgRememberSession(chatId, sid) {
|
|
1837
|
+
const h = tgHistory.get(chatId) || [];
|
|
1838
|
+
if (!h.includes(sid)) h.push(sid);
|
|
1839
|
+
tgHistory.set(chatId, h.slice(-TG_HISTORY_CAP));
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const TG_DEBOUNCE_MS = 4500;
|
|
1843
|
+
const tgQueue = new Map(); // chatId -> { timer, payloads[] }
|
|
1844
|
+
|
|
1845
|
+
function tgQueuePush(chatId, payload) {
|
|
1846
|
+
let q = tgQueue.get(chatId);
|
|
1847
|
+
if (!q) {
|
|
1848
|
+
q = { payloads: [] };
|
|
1849
|
+
tgQueue.set(chatId, q);
|
|
1850
|
+
}
|
|
1851
|
+
q.payloads.push(payload);
|
|
1852
|
+
clearTimeout(q.timer);
|
|
1853
|
+
tgLog(`queue: mesaj kuyruğa girdi chat=${chatId} toplam=${q.payloads.length} (4.5 sn birleştirme)`);
|
|
1854
|
+
q.timer = setTimeout(() => {
|
|
1855
|
+
tgFlush(chatId).catch((e) => tgLog(`flush KRASİ: ${String((e && e.stack) || e)}`));
|
|
1856
|
+
}, TG_DEBOUNCE_MS);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
async function tgFlush(chatId) {
|
|
1860
|
+
const q = tgQueue.get(chatId);
|
|
1861
|
+
if (!q) return;
|
|
1862
|
+
tgQueue.delete(chatId);
|
|
1863
|
+
const merged = { text: '', senderId: '', username: '', senderName: '' };
|
|
1864
|
+
for (const p of q.payloads) {
|
|
1865
|
+
if (p.text) merged.text += (merged.text ? '\n' : '') + p.text;
|
|
1866
|
+
if (!merged.senderId && p.senderId) { merged.senderId = p.senderId; merged.username = p.username; merged.senderName = p.senderName; }
|
|
1867
|
+
}
|
|
1868
|
+
await processTgMessage(chatId, merged);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
async function handleTgIncoming(chatId, payload) {
|
|
1872
|
+
try {
|
|
1873
|
+
if (!engine) return;
|
|
1874
|
+
/* v1: yalnız birebir sohbetler — grup davranışı WA'daki gibi ayrı toggle ile gelir */
|
|
1875
|
+
if (payload.isGroup) {
|
|
1876
|
+
tgLog(`skip: grup mesajı chat=${chatId} (grup desteği kapalı)`);
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const hit = tgFind(payload.senderId, payload.username);
|
|
1880
|
+
tgLog(
|
|
1881
|
+
`incoming chat=${chatId} sender=${payload.senderId || '?'} user=${payload.username || '-'} allowed=${!!hit}` +
|
|
1882
|
+
(hit && hit.name ? ' name=' + hit.name : '')
|
|
1883
|
+
);
|
|
1884
|
+
if (!hit) return; // allowlist dışı yoksay
|
|
1885
|
+
/* İsimsiz kayıt: güvenlik için cevap verme — kullanıcıyı ayarlara yönlendir */
|
|
1886
|
+
if (hit.id !== '*' && !hit.name) {
|
|
1887
|
+
tgLog(`skip: isimsiz kayıt (${hit.id}) — cevap verilmedi, Entegrasyonlar'da isim ekle`);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
resumeServices(); // pause durumunda gelen mesaj servisleri canlandırır
|
|
1891
|
+
tgQueuePush(String(chatId), payload);
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
tgLog(`handleTgIncoming KRASİ: ${String((e && e.stack) || e)}`);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
async function processTgMessage(chatId, payload, requeues = 0) {
|
|
1898
|
+
const hit = tgFind(payload.senderId, payload.username);
|
|
1899
|
+
if (!hit) {
|
|
1900
|
+
tgLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
let sid = tgChats.get(chatId);
|
|
1904
|
+
if (sid && engine.isBusy(sid)) {
|
|
1905
|
+
/* oturum meşgul — WA ile aynı: kaybetme, iş bitene dek yeniden dene */
|
|
1906
|
+
await new Promise((r) => setTimeout(r, TG_DEBOUNCE_MS));
|
|
1907
|
+
return processTgMessage(chatId, payload, requeues + 1);
|
|
1908
|
+
}
|
|
1909
|
+
if (!sid) {
|
|
1910
|
+
const v = engine.createSession();
|
|
1911
|
+
sid = v.id;
|
|
1912
|
+
tgChats.set(chatId, sid);
|
|
1913
|
+
tgRememberSession(chatId, sid);
|
|
1914
|
+
saveTgChats();
|
|
1915
|
+
} else {
|
|
1916
|
+
tgRememberSession(chatId, sid);
|
|
1917
|
+
}
|
|
1918
|
+
/* Kişi bazlı granül izin: all/web/read/chat */
|
|
1919
|
+
let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
|
|
1920
|
+
engine.setSessionPerm(sid, perm);
|
|
1921
|
+
|
|
1922
|
+
/* BOT SİSTEMİ: izinli kayıtta bot_id yoksa beast'e düşer (WA ile aynı) */
|
|
1923
|
+
let botId = hit && hit.bot_id ? String(hit.bot_id) : 'beast';
|
|
1924
|
+
if (!bots.get(botId)) {
|
|
1925
|
+
if (botId !== 'beast') tgLog(`bot="${botId}" yok — kayıt botsuz, beast (admin) botuna yönlendirildi`);
|
|
1926
|
+
botId = 'beast';
|
|
1927
|
+
}
|
|
1928
|
+
engine.setSessionBot(sid, botId);
|
|
1929
|
+
const botCfg = bots.get(botId);
|
|
1930
|
+
if (botCfg && !botCfg.admin) {
|
|
1931
|
+
const eff = moreRestrictivePerm(perm, botCfg.perm || 'all');
|
|
1932
|
+
if (eff !== perm) {
|
|
1933
|
+
perm = eff;
|
|
1934
|
+
engine.setSessionPerm(sid, eff);
|
|
1935
|
+
}
|
|
1936
|
+
engine.setSessionTools(sid, botToolSet(botCfg));
|
|
1937
|
+
} else {
|
|
1938
|
+
engine.setSessionTools(sid, null);
|
|
1939
|
+
}
|
|
1940
|
+
tgLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
|
|
1941
|
+
|
|
1942
|
+
/* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
|
|
1943
|
+
const isOwner = !!hit.owner;
|
|
1944
|
+
const roleTag = isOwner
|
|
1945
|
+
? 'SAHİBİN (talepleri önceliklidir)'
|
|
1946
|
+
: 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
|
|
1947
|
+
const label =
|
|
1948
|
+
(hit.name || payload.senderName || '?') +
|
|
1949
|
+
(payload.username ? ` (@${payload.username})` : '') +
|
|
1950
|
+
` — ${roleTag}`;
|
|
1951
|
+
let text = `[Telegram — gönderen: ${label}]`;
|
|
1952
|
+
if (!isOwner) {
|
|
1953
|
+
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.]`;
|
|
1954
|
+
}
|
|
1955
|
+
text += `\n${String(payload.text || '').slice(0, 6000)}`;
|
|
1956
|
+
engine.send(sid, { text: text.slice(0, 8000), attachments: [] });
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
async function sendTgSafe(chatId, text) {
|
|
1960
|
+
if (!tg) return false;
|
|
1961
|
+
try {
|
|
1962
|
+
return !!(await tg.send(chatId, text));
|
|
1963
|
+
} catch (e) {
|
|
1964
|
+
tgLog(`send hata chat=${chatId}: ${String((e && e.message) || e)}`);
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function ensureTg() {
|
|
1970
|
+
if (!tg) {
|
|
1971
|
+
tg = new TelegramBridge({
|
|
1972
|
+
token: settings.tgToken || '',
|
|
1973
|
+
emit: (ev) => {
|
|
1974
|
+
if (ev.type === 'status') tgLog(`status=${ev.status}${ev.user ? ' user=' + ev.user : ''}`);
|
|
1975
|
+
if (win && !win.isDestroyed()) win.webContents.send('tg:event', ev);
|
|
1976
|
+
},
|
|
1977
|
+
onIncoming: handleTgIncoming,
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
return tg;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/* token değişimi / yeniden başlatma: eski köprüyü kapat, yenisini aç */
|
|
1984
|
+
async function restartTg() {
|
|
1985
|
+
if (tg) {
|
|
1986
|
+
try { await tg.stop(); } catch {}
|
|
1987
|
+
tg = null;
|
|
1988
|
+
}
|
|
1989
|
+
if (!settings.tgToken) return;
|
|
1990
|
+
const b = ensureTg();
|
|
1991
|
+
try {
|
|
1992
|
+
await b.start();
|
|
1993
|
+
} catch (e) {
|
|
1994
|
+
tgLog(`start başarısız: ${String((e && e.message) || e)}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1737
1998
|
function reloadBackend() {
|
|
1738
1999
|
if (engine && typeof engine.dispose === 'function') {
|
|
1739
2000
|
try { engine.dispose(); } catch {}
|
|
@@ -1861,6 +2122,27 @@ function reloadBackend() {
|
|
|
1861
2122
|
})();
|
|
1862
2123
|
}
|
|
1863
2124
|
}
|
|
2125
|
+
// Telegram oturumlarının son cevabını geri gönder (WA ile aynı akış)
|
|
2126
|
+
if ((ev.type === 'done' || ev.type === 'error') && tg && tg.connected) {
|
|
2127
|
+
const hitT = [...tgChats.entries()].find(([, s]) => s === ev.sessionId);
|
|
2128
|
+
if (hitT) {
|
|
2129
|
+
const tgid = hitT[0];
|
|
2130
|
+
(async () => {
|
|
2131
|
+
try {
|
|
2132
|
+
if (ev.type === 'error') {
|
|
2133
|
+
await sendTgSafe(tgid, 'Bir aksilik oldu: ' + String(ev.error || '').slice(0, 200));
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (!ev.aborted) {
|
|
2137
|
+
const s = engine.openSession(ev.sessionId);
|
|
2138
|
+
const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
|
|
2139
|
+
const txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
|
|
2140
|
+
if (txt.trim()) await sendTgSafe(tgid, txt);
|
|
2141
|
+
}
|
|
2142
|
+
} catch {}
|
|
2143
|
+
})();
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
1864
2146
|
},
|
|
1865
2147
|
});
|
|
1866
2148
|
return engine.publicState();
|
|
@@ -2017,6 +2299,10 @@ app.whenReady().then(() => {
|
|
|
2017
2299
|
startNpmUpdateWatch(); // npm kurulumunda registry üzerinden otomatik sürüm kontrolü
|
|
2018
2300
|
/* FEATURE 2: offline kuyruk işçisi — 30 sn'de bir bağlantı kontrolü + kuyruk boşaltma */
|
|
2019
2301
|
setInterval(() => { mqueueTick().catch(() => {}); }, 30000).unref();
|
|
2302
|
+
/* OFFLINE MESAJ KUYRUĞU: gerçek bağlantı yoklaması — 8 sn'de bir DNS probe.
|
|
2303
|
+
Bağlantı dönünce kuyruktaki chat mesajları otomatik gönderilir. */
|
|
2304
|
+
netCheck().catch(() => {});
|
|
2305
|
+
setInterval(() => { netCheck().catch(() => {}); }, NET_CHECK_MS).unref();
|
|
2020
2306
|
|
|
2021
2307
|
// #12 STT prefetch: whisper modelini arka planda hazırla (ilk sesli mesajda bekleme olmasın)
|
|
2022
2308
|
if (settings.sttPrefetch !== false) {
|
|
@@ -2030,6 +2316,11 @@ app.whenReady().then(() => {
|
|
|
2030
2316
|
// WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
|
|
2031
2317
|
ensureWa().start().catch((e) => waLog('autostart failed: ' + (e && e.message)));
|
|
2032
2318
|
|
|
2319
|
+
// Telegram köprüsünü otomatik başlat (token kayıtlıysa)
|
|
2320
|
+
if (settings.tgToken) {
|
|
2321
|
+
ensureTg().start().catch((e) => tgLog('autostart failed: ' + String((e && e.message) || e)));
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2033
2324
|
app.on('activate', () => {
|
|
2034
2325
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
2035
2326
|
});
|
|
@@ -3198,6 +3489,12 @@ function queueDesktopMessage(sessionId, text) {
|
|
|
3198
3489
|
const t = isObj ? String((text && text.text) || '') : String(text ?? '');
|
|
3199
3490
|
const hasAtts = isObj && Array.isArray(text.attachments) && text.attachments.length > 0;
|
|
3200
3491
|
if (!sid || (!t.trim() && !hasAtts)) return; // boş içerik kuyruğa girmez
|
|
3492
|
+
/* FEATURE: OFFLINE MESAJ KUYRUĞU — internet yokken gelen mesaj diskte bekler,
|
|
3493
|
+
bağlantı geri gelince otomatik gönderilir */
|
|
3494
|
+
if (!netOnline) {
|
|
3495
|
+
chatQueueOfflineAdd(sid, { text: t, attachments: hasAtts ? text.attachments : undefined });
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3201
3498
|
let q = desktopQueue.get(sid);
|
|
3202
3499
|
if (!q) {
|
|
3203
3500
|
q = { timer: null, msgs: [] };
|
|
@@ -3225,6 +3522,13 @@ async function flushDesktop(sessionId) {
|
|
|
3225
3522
|
return;
|
|
3226
3523
|
}
|
|
3227
3524
|
if (engine.isBusy(sid)) return; // hâlâ çalışıyor — done eventini bekle
|
|
3525
|
+
/* debounce penceresinde internet koptuysa mesajlar offline kuyruğa düşer */
|
|
3526
|
+
if (!netOnline) {
|
|
3527
|
+
desktopQueue.delete(sid);
|
|
3528
|
+
clearTimeout(q.timer);
|
|
3529
|
+
for (const m of q.msgs) chatQueueOfflineAdd(sid, { text: m.text, attachments: m.attachments });
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3228
3532
|
desktopQueue.delete(sid);
|
|
3229
3533
|
clearTimeout(q.timer);
|
|
3230
3534
|
|
|
@@ -3251,6 +3555,144 @@ function flushDesktopOnDone(ev) {
|
|
|
3251
3555
|
}
|
|
3252
3556
|
}
|
|
3253
3557
|
|
|
3558
|
+
/* ---------- OFFLINE MESAJ KUYRUĞU (masaüstü sohbet) ----------
|
|
3559
|
+
İnternet yokken/kopukken gönderilen chat mesajları kaybolmasın:
|
|
3560
|
+
- Mesaj diskteki kuyruğa yazılır (chat_queue.json — elektrik kesintisine dayanıklı)
|
|
3561
|
+
- Bağlantı geri gelince (DNS kontrolü) sırayla otomatik gönderilir
|
|
3562
|
+
- Renderer'a 'net' / 'netQueue' olayları gider: ⏳ kuyruk balonu + toast */
|
|
3563
|
+
const NET_CHECK_HOSTS = ['one.one.one.one', 'dns.google'];
|
|
3564
|
+
const NET_CHECK_MS = 8000;
|
|
3565
|
+
const NET_CHECK_TIMEOUT = 4000;
|
|
3566
|
+
const CHAT_QUEUE_MAX = 50; // kuyruk üst sınırı — taşarsa en eski düşer
|
|
3567
|
+
|
|
3568
|
+
let netOnline = true; // son bilinen bağlantı durumu (başlangıçta iyimser)
|
|
3569
|
+
let netCheckedOnce = false;
|
|
3570
|
+
let netCheckBusy = false;
|
|
3571
|
+
let chatQueueFlushing = false;
|
|
3572
|
+
const chatOfflineQueue = []; // { key, sessionId, text, attachments, at }
|
|
3573
|
+
|
|
3574
|
+
/* diskten yükle (app restart sonrası kuyruk korunur) */
|
|
3575
|
+
(function chatQueueLoad() {
|
|
3576
|
+
try {
|
|
3577
|
+
const j = JSON.parse(fs.readFileSync(CHAT_QUEUE_FILE, 'utf8'));
|
|
3578
|
+
const items = Array.isArray(j.items) ? j.items : [];
|
|
3579
|
+
for (const it of items) {
|
|
3580
|
+
if (it && typeof it === 'object' && it.sessionId && (String(it.text || '').trim() || (Array.isArray(it.attachments) && it.attachments.length))) {
|
|
3581
|
+
chatOfflineQueue.push(it);
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
} catch {}
|
|
3585
|
+
})();
|
|
3586
|
+
|
|
3587
|
+
function chatQueueSave() {
|
|
3588
|
+
try {
|
|
3589
|
+
const tmp = CHAT_QUEUE_FILE + '.tmp';
|
|
3590
|
+
fs.writeFileSync(tmp, JSON.stringify({ items: chatOfflineQueue }, null, 2));
|
|
3591
|
+
fs.renameSync(tmp, CHAT_QUEUE_FILE); // atomik yazım — yarı kalmış dosya olmaz
|
|
3592
|
+
} catch {}
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
function chatQueueEmit(extra = {}) {
|
|
3596
|
+
try {
|
|
3597
|
+
if (win && !win.isDestroyed()) {
|
|
3598
|
+
win.webContents.send('agent:event', {
|
|
3599
|
+
type: 'netQueue',
|
|
3600
|
+
online: netOnline,
|
|
3601
|
+
count: chatOfflineQueue.length,
|
|
3602
|
+
...extra,
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
} catch {}
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
/* gönderilemeyen mesajı kuyruğa al */
|
|
3609
|
+
function chatQueueOfflineAdd(sessionId, { text, attachments }) {
|
|
3610
|
+
const item = {
|
|
3611
|
+
key: 'oq' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
3612
|
+
sessionId: String(sessionId || ''),
|
|
3613
|
+
text: String(text || '').slice(0, 100000),
|
|
3614
|
+
attachments: Array.isArray(attachments) ? attachments.slice(0, 5) : undefined,
|
|
3615
|
+
at: new Date().toISOString(),
|
|
3616
|
+
};
|
|
3617
|
+
chatOfflineQueue.push(item);
|
|
3618
|
+
while (chatOfflineQueue.length > CHAT_QUEUE_MAX) chatOfflineQueue.shift();
|
|
3619
|
+
chatQueueSave();
|
|
3620
|
+
log.info('main', `offline kuyruk: mesaj eklendi (${chatOfflineQueue.length} bekliyor) sid=${item.sessionId}`);
|
|
3621
|
+
chatQueueEmit({
|
|
3622
|
+
queued: true,
|
|
3623
|
+
key: item.key,
|
|
3624
|
+
sessionId: item.sessionId,
|
|
3625
|
+
text: item.text,
|
|
3626
|
+
attCount: item.attachments ? item.attachments.length : 0,
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
/* kuyruğu normal akışa (debounce → engine) verir */
|
|
3631
|
+
async function flushChatQueue() {
|
|
3632
|
+
if (chatQueueFlushing) return;
|
|
3633
|
+
if (!chatOfflineQueue.length) return;
|
|
3634
|
+
if (!netOnline) return;
|
|
3635
|
+
chatQueueFlushing = true;
|
|
3636
|
+
try {
|
|
3637
|
+
const keys = [];
|
|
3638
|
+
while (chatOfflineQueue.length) {
|
|
3639
|
+
const it = chatOfflineQueue.shift();
|
|
3640
|
+
keys.push(it.key);
|
|
3641
|
+
const payload = it.attachments && it.attachments.length ? { text: it.text, attachments: it.attachments } : it.text;
|
|
3642
|
+
queueDesktopMessage(it.sessionId, payload);
|
|
3643
|
+
}
|
|
3644
|
+
chatQueueSave();
|
|
3645
|
+
if (keys.length) {
|
|
3646
|
+
log.info('main', `offline kuyruk boşaltıldı: ${keys.length} mesaj gönderiliyor`);
|
|
3647
|
+
chatQueueEmit({ flushed: keys.length, keys });
|
|
3648
|
+
}
|
|
3649
|
+
} finally {
|
|
3650
|
+
chatQueueFlushing = false;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
/* gerçek internet kontrolü: DNS çözümlemesi (sadece ağ arayüzü değil,
|
|
3655
|
+
paket gerçekten çıkıyor mu test eder). Adaylar sırayla denenir. */
|
|
3656
|
+
function dnsProbe(host) {
|
|
3657
|
+
return new Promise((resolve) => {
|
|
3658
|
+
const t = setTimeout(() => resolve(false), NET_CHECK_TIMEOUT);
|
|
3659
|
+
dns.resolve(host, 'A', (err) => {
|
|
3660
|
+
clearTimeout(t);
|
|
3661
|
+
resolve(!err);
|
|
3662
|
+
});
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
async function netCheck() {
|
|
3667
|
+
if (netCheckBusy) return;
|
|
3668
|
+
netCheckBusy = true;
|
|
3669
|
+
try {
|
|
3670
|
+
let ok = false;
|
|
3671
|
+
for (const h of NET_CHECK_HOSTS) {
|
|
3672
|
+
if (await dnsProbe(h)) { ok = true; break; }
|
|
3673
|
+
}
|
|
3674
|
+
const first = !netCheckedOnce;
|
|
3675
|
+
const was = netOnline;
|
|
3676
|
+
netOnline = ok;
|
|
3677
|
+
netCheckedOnce = true;
|
|
3678
|
+
if (was !== ok || first) {
|
|
3679
|
+
try {
|
|
3680
|
+
if (win && !win.isDestroyed()) win.webContents.send('agent:event', { type: 'net', online: ok });
|
|
3681
|
+
} catch {}
|
|
3682
|
+
if (ok) {
|
|
3683
|
+
log.info('main', 'bağlantı geri geldi — offline kuyruk kontrol ediliyor');
|
|
3684
|
+
chatQueueEmit(); // renderer: pill/toast güncellensin
|
|
3685
|
+
flushChatQueue().catch(() => {});
|
|
3686
|
+
} else {
|
|
3687
|
+
log.info('main', 'internet bağlantısı yok — mesajlar kuyruğa alınacak');
|
|
3688
|
+
chatQueueEmit();
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
} catch {} finally {
|
|
3692
|
+
netCheckBusy = false;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3254
3696
|
ipcMain.handle('model:set', (_e, sel) => {
|
|
3255
3697
|
settings.modelOverride = sel;
|
|
3256
3698
|
saveSettings();
|
|
@@ -3585,24 +4027,82 @@ ipcMain.handle('update:check', async () => {
|
|
|
3585
4027
|
});
|
|
3586
4028
|
|
|
3587
4029
|
/* npm kurulumunda KENDİ KENDİNİ GÜNCELLEME:
|
|
3588
|
-
|
|
4030
|
+
helper script %APPDATA%\beast'a yazılır (paket dizini değişse de yaşar) ve
|
|
4031
|
+
detached çalışır: 1) uygulama PID'i tamamen çıkana kadar bekler (en çok 30 sn,
|
|
4032
|
+
sonra zorla kapatır — EBUSY dosya kilidinin numarası), 2) npm install -g
|
|
4033
|
+
beast-agent@latest — kilide karşı 5 deneme, 3) tüm çıktı update.log'a yazılır,
|
|
4034
|
+
4) beast-agent komut shim'i üzerinden yeniden başlatır (electron sürümü
|
|
4035
|
+
değişse de yol bozulmaz). Sonra app.quit(). */
|
|
3589
4036
|
function npmSelfUpdate() {
|
|
3590
4037
|
try {
|
|
3591
|
-
|
|
3592
|
-
const
|
|
4038
|
+
fs.mkdirSync(APP_DIR, { recursive: true });
|
|
4039
|
+
const pid = String(process.pid);
|
|
3593
4040
|
if (process.platform === 'win32') {
|
|
3594
|
-
const ps =
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
4041
|
+
const ps = [
|
|
4042
|
+
"param([int]$ProcId = 0)",
|
|
4043
|
+
"$ErrorActionPreference = 'Continue'",
|
|
4044
|
+
"$Log = Join-Path $env:APPDATA 'beast\\update.log'",
|
|
4045
|
+
"function L([string]$m) { try { Add-Content -LiteralPath $Log -Value (\"[\" + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + \"] \" + $m) } catch {} }",
|
|
4046
|
+
"L \"=== self-update basladi (app pid: $ProcId) ===\"",
|
|
4047
|
+
"if ($ProcId -gt 0) {",
|
|
4048
|
+
" $deadline = (Get-Date).AddSeconds(30)",
|
|
4049
|
+
" while ((Get-Date) -lt $deadline) {",
|
|
4050
|
+
" if (-not (Get-Process -Id $ProcId -ErrorAction SilentlyContinue)) { break }",
|
|
4051
|
+
" Start-Sleep -Milliseconds 500",
|
|
4052
|
+
" }",
|
|
4053
|
+
" if (Get-Process -Id $ProcId -ErrorAction SilentlyContinue) {",
|
|
4054
|
+
" L 'uygulama hala acik - zorla kapatiliyor'",
|
|
4055
|
+
" try { Stop-Process -Id $ProcId -Force } catch {}",
|
|
4056
|
+
" Start-Sleep -Seconds 2",
|
|
4057
|
+
" }",
|
|
4058
|
+
"}",
|
|
4059
|
+
"L 'uygulama kapandi, npm install basliyor'",
|
|
4060
|
+
"$npmCmd = (Get-Command 'npm.cmd' -ErrorAction SilentlyContinue).Source",
|
|
4061
|
+
"if (-not $npmCmd) { $npmCmd = Join-Path $env:APPDATA 'npm\\npm.cmd' }",
|
|
4062
|
+
"$ok = $false",
|
|
4063
|
+
"for ($i = 1; $i -le 5; $i++) {",
|
|
4064
|
+
" if ($ok) { break }",
|
|
4065
|
+
" L \"npm install -g beast-agent@latest (deneme $i)\"",
|
|
4066
|
+
" $out = & $npmCmd install -g beast-agent@latest 2>&1",
|
|
4067
|
+
" $code = $LASTEXITCODE",
|
|
4068
|
+
" foreach ($line in @($out)) { L \" npm: $line\" }",
|
|
4069
|
+
" if ($code -eq 0) { $ok = $true } else { Start-Sleep -Seconds 3 }",
|
|
4070
|
+
"}",
|
|
4071
|
+
"if (-not $ok) {",
|
|
4072
|
+
" L 'HATA: npm install 5 denemede basarisiz - uygulama yeniden baslatilmiyor'",
|
|
4073
|
+
" exit 1",
|
|
4074
|
+
"}",
|
|
4075
|
+
"L 'npm install tamam, yeniden baslatma'",
|
|
4076
|
+
"$shim = Get-Command 'beast-agent.cmd' -ErrorAction SilentlyContinue",
|
|
4077
|
+
"if ($shim) {",
|
|
4078
|
+
" L \"shim uzerinden: $($shim.Source)\"",
|
|
4079
|
+
" Start-Process -FilePath $shim.Source -WindowStyle Hidden",
|
|
4080
|
+
"} else {",
|
|
4081
|
+
" $prefix = (& $npmCmd prefix -g 2>$null)",
|
|
4082
|
+
" if (-not $prefix) { $prefix = Join-Path $env:APPDATA 'npm' }",
|
|
4083
|
+
" $exe = Join-Path $prefix 'node_modules\\electron\\dist\\electron.exe'",
|
|
4084
|
+
" if (-not (Test-Path $exe)) { $exe = Join-Path $prefix 'node_modules\\beast-agent\\node_modules\\electron\\dist\\electron.exe' }",
|
|
4085
|
+
" $appDir = Join-Path $prefix 'node_modules\\beast-agent'",
|
|
4086
|
+
" L \"shim bulunamadi - dogrudan: $exe\"",
|
|
4087
|
+
" Start-Process -FilePath $exe -ArgumentList \"`\"$appDir`\"\" -WindowStyle Hidden",
|
|
4088
|
+
"}",
|
|
4089
|
+
"L '=== self-update bitti ==='",
|
|
4090
|
+
].join('\r\n');
|
|
4091
|
+
const psFile = path.join(APP_DIR, 'update-helper.ps1');
|
|
4092
|
+
fs.writeFileSync(psFile, ps, 'utf8');
|
|
4093
|
+
spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', psFile, '-ProcId', pid],
|
|
4094
|
+
{ detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
3600
4095
|
} else {
|
|
3601
|
-
const sh =
|
|
4096
|
+
const sh =
|
|
4097
|
+
`i=0; while [ $i -lt 60 ] && kill -0 ${pid} 2>/dev/null; do i=$((i+1)); sleep 0.5; done; ` +
|
|
4098
|
+
'ok=0; for n in 1 2 3 4 5; do npm install -g beast-agent@latest && ok=1 && break; sleep 3; done; ' +
|
|
4099
|
+
'if [ $ok -eq 1 ]; then nohup beast-agent >/dev/null 2>&1 & fi';
|
|
3602
4100
|
spawn('sh', ['-c', sh], { detached: true, stdio: 'ignore' }).unref();
|
|
3603
4101
|
}
|
|
3604
4102
|
log.info('main', 'npm self-update: helper bırakıldı, uygulama kapatılıyor');
|
|
3605
|
-
} catch {
|
|
4103
|
+
} catch (e) {
|
|
4104
|
+
log.error('main', 'npm self-update hatası: ' + String((e && e.message) || e));
|
|
4105
|
+
}
|
|
3606
4106
|
setTimeout(() => { try { app.quit(); } catch {} }, 400);
|
|
3607
4107
|
}
|
|
3608
4108
|
|
|
@@ -4713,6 +5213,43 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
|
|
|
4713
5213
|
return settings.waTts;
|
|
4714
5214
|
});
|
|
4715
5215
|
|
|
5216
|
+
/* ---------- Telegram IPC (FEATURE 3) ---------- */
|
|
5217
|
+
|
|
5218
|
+
ipcMain.handle('tg:status:get', () => {
|
|
5219
|
+
if (!tg) return { configured: !!settings.tgToken, status: 'disconnected', user: null, connected: false };
|
|
5220
|
+
return { configured: true, ...tg.snapshot() };
|
|
5221
|
+
});
|
|
5222
|
+
|
|
5223
|
+
/* token kaydet + köprüyü (yeniden) başlat */
|
|
5224
|
+
ipcMain.handle('tg:set', async (_e, token) => {
|
|
5225
|
+
const t = String(token || '').trim();
|
|
5226
|
+
if (t) settings.tgToken = t;
|
|
5227
|
+
saveSettings();
|
|
5228
|
+
await restartTg();
|
|
5229
|
+
return { configured: !!settings.tgToken, ...(tg ? tg.snapshot() : { status: 'disconnected', user: null }) };
|
|
5230
|
+
});
|
|
5231
|
+
|
|
5232
|
+
ipcMain.handle('tg:start', async () => {
|
|
5233
|
+
if (!settings.tgToken) return { ok: false, error: 'token yok — önce bot tokenı gir' };
|
|
5234
|
+
await restartTg();
|
|
5235
|
+
return { ok: true, ...(tg ? tg.snapshot() : {}) };
|
|
5236
|
+
});
|
|
5237
|
+
|
|
5238
|
+
ipcMain.handle('tg:stop', async () => {
|
|
5239
|
+
if (tg) {
|
|
5240
|
+
try { await tg.stop(); } catch {}
|
|
5241
|
+
}
|
|
5242
|
+
return { ok: true };
|
|
5243
|
+
});
|
|
5244
|
+
|
|
5245
|
+
ipcMain.handle('tg:allow:get', () => settings.tgAllow || []);
|
|
5246
|
+
ipcMain.handle('tg:allow:set', (_e, list) => {
|
|
5247
|
+
settings.tgAllow = Array.isArray(list) ? list : [];
|
|
5248
|
+
saveSettings();
|
|
5249
|
+
return settings.tgAllow;
|
|
5250
|
+
});
|
|
5251
|
+
ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
|
|
5252
|
+
|
|
4716
5253
|
/* ---------- e-posta IPC ---------- */
|
|
4717
5254
|
|
|
4718
5255
|
ipcMain.handle('email:get', () => {
|