beast-agent 0.22.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beast-agent",
3
3
  "productName": "Beast Agent",
4
- "version": "0.22.0",
4
+ "version": "0.23.0",
5
5
  "description": "Ultra-fast local agent shell for Windows.",
6
6
  "author": "algokodcom (AlgoKod)",
7
7
  "license": "MIT",
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ /* TELEGRAM KÖPRÜSÜ (FEATURE 3)
4
+ WhatsApp köprüsünün Telegram hali — bağımlılık yok (saf Node https):
5
+ - Bot API long polling (getUpdates) ile gelen mesajlar onIncoming'e düşer
6
+ - send(chatId, text) cevap döner; 4096 karakter sınırında bölerek gönderir
7
+ - Aynı allow list mantığı: main tarafındaki tgFind() listesindeki kişilere cevap verir */
8
+
9
+ const https = require('https');
10
+
11
+ const API_BASE = 'https://api.telegram.org/bot';
12
+ const SEND_CHUNK = 3800; // Telegram mesaj sınırı 4096 — güvenli pay
13
+
14
+ class TelegramBridge {
15
+ constructor({ token, emit, onIncoming }) {
16
+ this.token = String(token || '').trim();
17
+ this.emit = emit || (() => {});
18
+ this.onIncoming = onIncoming || null;
19
+ this.connected = false;
20
+ this.stopping = false;
21
+ this.status = 'disconnected';
22
+ this.offset = 0;
23
+ this.me = null;
24
+ this._req = null; // aktif long-poll isteği (iptal için)
25
+ }
26
+
27
+ _setStatus(status, user) {
28
+ this.status = status;
29
+ this.connected = status === 'connected';
30
+ this.emit({ type: 'status', status, user: user || null });
31
+ }
32
+
33
+ /* Bot API çağrısı — JSON POST, promise sarmalı */
34
+ api(method, body = {}, opts = {}) {
35
+ return new Promise((resolve, reject) => {
36
+ const payload = JSON.stringify(body);
37
+ const req = https.request(
38
+ `${API_BASE}${this.token}/${method}`,
39
+ {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
42
+ timeout: opts.timeout || 15000,
43
+ },
44
+ (res) => {
45
+ let data = '';
46
+ res.setEncoding('utf8');
47
+ res.on('data', (c) => { data += c; });
48
+ res.on('end', () => {
49
+ try {
50
+ const j = JSON.parse(data || '{}');
51
+ if (j.ok) resolve(j.result);
52
+ else reject(new Error(`telegram ${method}: ${j.description || 'hata ' + res.statusCode}`));
53
+ } catch (e) {
54
+ reject(new Error(`telegram ${method}: bozuk yanıt`));
55
+ }
56
+ });
57
+ }
58
+ );
59
+ req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
60
+ req.on('error', reject);
61
+ this._req = req;
62
+ req.write(payload);
63
+ req.end();
64
+ });
65
+ }
66
+
67
+ async start() {
68
+ if (!this.token) {
69
+ this._setStatus('error');
70
+ return false;
71
+ }
72
+ this.stopping = false;
73
+ this.offset = 0;
74
+ this._setStatus('connecting');
75
+ try {
76
+ const me = await this.api('getMe', {});
77
+ this.me = me;
78
+ this._setStatus('connected', '@' + (me.username || me.first_name || 'bot'));
79
+ } catch (e) {
80
+ this._setStatus('error');
81
+ throw e;
82
+ }
83
+ this._pollLoop().catch(() => {});
84
+ return true;
85
+ }
86
+
87
+ /* long polling döngüsü — bağlantı koparsa 3 sn bekleyip devam */
88
+ async _pollLoop() {
89
+ while (!this.stopping) {
90
+ try {
91
+ const updates = await this.api(
92
+ 'getUpdates',
93
+ { timeout: 25, offset: this.offset, allowed_updates: ['message'] },
94
+ { timeout: 35000 }
95
+ );
96
+ if (this.stopping) break;
97
+ for (const u of Array.isArray(updates) ? updates : []) {
98
+ this.offset = Math.max(this.offset, (u.update_id || 0) + 1);
99
+ const msg = u.message;
100
+ if (!msg || !msg.text || !msg.from || msg.from.is_bot) continue;
101
+ const chatId = msg.chat && msg.chat.id;
102
+ if (chatId === undefined || chatId === null) continue;
103
+ const payload = {
104
+ text: String(msg.text).slice(0, 6000),
105
+ senderId: String(msg.from.id || ''),
106
+ username: String(msg.from.username || ''),
107
+ senderName: String(msg.from.first_name || msg.from.username || ''),
108
+ isGroup: !!(msg.chat && (msg.chat.type === 'group' || msg.chat.type === 'supergroup')),
109
+ };
110
+ try {
111
+ if (this.onIncoming) this.onIncoming(String(chatId), payload);
112
+ } catch {}
113
+ }
114
+ } catch (e) {
115
+ if (this.stopping) break;
116
+ this.emit({ type: 'poll-error', error: String((e && e.message) || e) });
117
+ await new Promise((r) => setTimeout(r, 3000));
118
+ }
119
+ }
120
+ }
121
+
122
+ async stop() {
123
+ this.stopping = true;
124
+ try { if (this._req) this._req.destroy(new Error('stop')); } catch {}
125
+ this._req = null;
126
+ this.connected = false;
127
+ this.status = 'disconnected';
128
+ }
129
+
130
+ snapshot() {
131
+ return {
132
+ status: this.status,
133
+ user: this.me ? '@' + (this.me.username || this.me.first_name || 'bot') : null,
134
+ connected: this.connected,
135
+ };
136
+ }
137
+
138
+ /* Metin gönder — 4096 sınırı için parçalara böl */
139
+ async send(chatId, text) {
140
+ const t = String(text || '');
141
+ if (!t.trim()) return false;
142
+ const chunks = [];
143
+ for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
144
+ for (const part of chunks) {
145
+ await this.api('sendMessage', {
146
+ chat_id: chatId,
147
+ text: part,
148
+ disable_web_page_preview: true,
149
+ });
150
+ }
151
+ return true;
152
+ }
153
+ }
154
+
155
+ module.exports = { TelegramBridge };
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');
@@ -201,6 +203,8 @@ const SETTINGS_BACKUP_FILE = path.join(APP_DIR, 'settings.backup.json');
201
203
  const WA_AUTH_DIR = path.join(APP_DIR, 'wa-auth');
202
204
  const WA_CHATS_FILE = path.join(APP_DIR, 'wa-chats.json');
203
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');
204
208
 
205
209
  for (const d of [APP_DIR, SESSIONS_DIR]) fs.mkdirSync(d, { recursive: true });
206
210
 
@@ -216,6 +220,10 @@ let waChats = new Map(); // jid -> aktif session id
216
220
  let waHistory = new Map(); // jid -> [sid,...] bu sohbete ait tüm oturumlar
217
221
  let waJidPn = new Map(); // jid -> gerçek telefon numarası (LID fallback için)
218
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;
219
227
  let tray = null;
220
228
  app.isQuitting = false;
221
229
 
@@ -450,6 +458,34 @@ function isWaAllowed(senderNum) {
450
458
  return !!waFind(senderNum);
451
459
  }
452
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
+
453
489
  /* Sahip: owner işaretli kayıt; yoksa listedeki ilk kişi. /allow ve /block
454
490
  yalnızca sahip tarafından kullanılabilir (yabancı DM kendi kendini ekleyemesin). */
455
491
  function waOwnerNum() {
@@ -1759,6 +1795,206 @@ function ensureWa() {
1759
1795
  return wa;
1760
1796
  }
1761
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
+
1762
1998
  function reloadBackend() {
1763
1999
  if (engine && typeof engine.dispose === 'function') {
1764
2000
  try { engine.dispose(); } catch {}
@@ -1886,6 +2122,27 @@ function reloadBackend() {
1886
2122
  })();
1887
2123
  }
1888
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
+ }
1889
2146
  },
1890
2147
  });
1891
2148
  return engine.publicState();
@@ -2042,6 +2299,10 @@ app.whenReady().then(() => {
2042
2299
  startNpmUpdateWatch(); // npm kurulumunda registry üzerinden otomatik sürüm kontrolü
2043
2300
  /* FEATURE 2: offline kuyruk işçisi — 30 sn'de bir bağlantı kontrolü + kuyruk boşaltma */
2044
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();
2045
2306
 
2046
2307
  // #12 STT prefetch: whisper modelini arka planda hazırla (ilk sesli mesajda bekleme olmasın)
2047
2308
  if (settings.sttPrefetch !== false) {
@@ -2055,6 +2316,11 @@ app.whenReady().then(() => {
2055
2316
  // WhatsApp köprüsünü otomatik başlat (eşleme varsa direkt bağlanır)
2056
2317
  ensureWa().start().catch((e) => waLog('autostart failed: ' + (e && e.message)));
2057
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
+
2058
2324
  app.on('activate', () => {
2059
2325
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
2060
2326
  });
@@ -3223,6 +3489,12 @@ function queueDesktopMessage(sessionId, text) {
3223
3489
  const t = isObj ? String((text && text.text) || '') : String(text ?? '');
3224
3490
  const hasAtts = isObj && Array.isArray(text.attachments) && text.attachments.length > 0;
3225
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
+ }
3226
3498
  let q = desktopQueue.get(sid);
3227
3499
  if (!q) {
3228
3500
  q = { timer: null, msgs: [] };
@@ -3250,6 +3522,13 @@ async function flushDesktop(sessionId) {
3250
3522
  return;
3251
3523
  }
3252
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
+ }
3253
3532
  desktopQueue.delete(sid);
3254
3533
  clearTimeout(q.timer);
3255
3534
 
@@ -3276,6 +3555,144 @@ function flushDesktopOnDone(ev) {
3276
3555
  }
3277
3556
  }
3278
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
+
3279
3696
  ipcMain.handle('model:set', (_e, sel) => {
3280
3697
  settings.modelOverride = sel;
3281
3698
  saveSettings();
@@ -4796,6 +5213,43 @@ ipcMain.handle('wa:tts:set', (_e, cfg) => {
4796
5213
  return settings.waTts;
4797
5214
  });
4798
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
+
4799
5253
  /* ---------- e-posta IPC ---------- */
4800
5254
 
4801
5255
  ipcMain.handle('email:get', () => {
package/src/preload.js CHANGED
@@ -115,6 +115,14 @@ contextBridge.exposeInMainWorld('beast', {
115
115
  waSetTts: (cfg) => ipcRenderer.invoke('wa:tts:set', cfg),
116
116
  waGetGroups: () => ipcRenderer.invoke('wa:groups:get'),
117
117
  waSetGroups: (cfg) => ipcRenderer.invoke('wa:groups:set', cfg),
118
+ tgGetStatus: () => ipcRenderer.invoke('tg:status:get'),
119
+ tgSetToken: (token) => ipcRenderer.invoke('tg:set', token),
120
+ tgStart: () => ipcRenderer.invoke('tg:start'),
121
+ tgStop: () => ipcRenderer.invoke('tg:stop'),
122
+ tgGetAllow: () => ipcRenderer.invoke('tg:allow:get'),
123
+ tgSetAllow: (list) => ipcRenderer.invoke('tg:allow:set', list),
124
+ tgListSessions: () => ipcRenderer.invoke('tg:sessions'),
125
+ onTgEvent: (cb) => ipcRenderer.on('tg:event', (_e, ev) => cb(ev)),
118
126
  getUsage: () => ipcRenderer.invoke('usage:get'),
119
127
  resetUsage: () => ipcRenderer.invoke('usage:reset'),
120
128
  createBackup: () => ipcRenderer.invoke('backup:create'),
@@ -200,6 +200,13 @@
200
200
  it_num_ph: 'Numara 905xxxxxxxxx',
201
201
  it_add: 'Ekle',
202
202
  it_allow_note: 'İsim + numara zorunlu. Her kişinin yanındaki serbest/web/okuma/kısıtlı seçimiyle kişiye özel yetki ver.',
203
+ it_tg_sub: 'Bot tokenı ile çalışır — izin listendeki kişilere Telegram\u2019dan cevap verir',
204
+ it_tg_token_ph: 'Bot tokenı (örn. 123456789:AAH…)',
205
+ it_tg_save: 'Kaydet & Bağlan',
206
+ it_tg_connecting: 'Telegram\u2019a bağlanıyor…',
207
+ it_tg_token_bad: 'Token doğrulanamadı — @BotFather\u2019dan aldığın tokenı kontrol et',
208
+ it_tg_id_ph: 'Kullanıcı ID veya @kullanıcı_adı',
209
+ it_tg_note: 'İzin listesi boşsa kimse cevap almaz. Bot oluşturma: @BotFather → /newbot. Kendi ID\u2019ni öğrenmek için @userinfobot\u2019a mesaj at.',
203
210
  ev_h2: 'Olay Merkezi',
204
211
  ev_sub: 'Cron\u2019suz canlı olaylar — kaynakları aç, abonelikleri agent kendisi kurar (event_subscribe). Tetiklenen olay ilgili sohbete düşer, cevap WhatsApp\u2019a gider.',
205
212
  ev_on: 'Olay merkezi açık',
@@ -452,6 +459,12 @@
452
459
  p_zen_ok: '${n} free model yüklendi',
453
460
  p_zen_skipped: '${n} model yanıt vermedi, listeye alınmadı',
454
461
  p_zen_fail: 'Model kurulumu başarısız',
462
+ net_offline: 'İnternet bağlantısı yok — mesajların kuyruğa alındı',
463
+ net_online: 'Bağlantı geri geldi',
464
+ net_pill_offline: 'İnternet yok — ${n} mesaj kuyrukta',
465
+ net_pill_online: 'İnternet yok',
466
+ q_pending: 'kuyrukta — internet bekleniyor',
467
+ q_flushed: '${n} kuyruktaki mesaj gönderiliyor',
455
468
  bot_num_ro_hint: 'Numara ekleme/kişi adı Ayarlar → Entegrasyonlar (WhatsApp izin listesi) üzerinden yapılır; burada yalnızca bağlı numaralar görünür.',
456
469
  tipStore: 'Skills Store',
457
470
  tipIde: 'IDE Modu — dosyalar + chat + preview',
@@ -701,6 +714,13 @@
701
714
  it_num_ph: 'Number 905xxxxxxxxx',
702
715
  it_add: 'Add',
703
716
  it_allow_note: 'Name + number required. Grant per-person permission (free/web/read/restricted) via the selector next to each.',
717
+ it_tg_sub: 'Works with a bot token — replies on Telegram to people in your allow list',
718
+ it_tg_token_ph: 'Bot token (e.g. 123456789:AAH…)',
719
+ it_tg_save: 'Save & Connect',
720
+ it_tg_connecting: 'Connecting to Telegram…',
721
+ it_tg_token_bad: 'Token could not be verified — check the token from @BotFather',
722
+ it_tg_id_ph: 'User ID or @username',
723
+ it_tg_note: 'Empty allow list = nobody gets a reply. Create a bot: @BotFather → /newbot. To learn your ID, message @userinfobot.',
704
724
  ev_h2: 'Event Center',
705
725
  ev_sub: 'Live events without cron — open sources, the agent sets up subscriptions itself (event_subscribe). The triggered event drops into the relevant chat, the reply goes to WhatsApp.',
706
726
  ev_on: 'Event center on',
@@ -1011,6 +1031,12 @@
1011
1031
  p_zen_ok: '${n} free models installed',
1012
1032
  p_zen_skipped: '${n} models did not respond, excluded',
1013
1033
  p_zen_fail: 'Model setup failed',
1034
+ net_offline: 'No internet connection — messages queued',
1035
+ net_online: 'Connection restored',
1036
+ net_pill_offline: 'No internet — ${n} message(s) queued',
1037
+ net_pill_online: 'No internet',
1038
+ q_pending: 'queued — waiting for internet',
1039
+ q_flushed: '${n} queued message(s) being sent',
1014
1040
  bot_num_ro_hint: 'Add numbers / person names via Settings → Integrations (WhatsApp allow list); only linked numbers are shown here.',
1015
1041
  tipStore: 'Skills Store',
1016
1042
  tipIde: 'IDE Mode — files + chat + preview',
@@ -193,6 +193,72 @@ function setStatus(text) {
193
193
  els.statusPill.textContent = text;
194
194
  }
195
195
 
196
+ /* ---------------- OFFLINE MESAJ KUYRUĞU (chat) ----------------
197
+ İnternet yokken gönderilen mesajlar main'deki diske yazılan kuyruğa düşer;
198
+ burada ⏳ "kuyrukta" balonu gösterilir. Bağlantı gelince kuyruk otomatik
199
+ boşaltılır ('netQueue flushed') ve balonlar gerçek mesajla değişir. */
200
+ let netOnline = true;
201
+ let netQueueCount = 0;
202
+ const netPending = []; // { key, el } — bekleyen mesaj balonları
203
+
204
+ function setNetBadge(el, text) {
205
+ if (!el || !el.isConnected) return;
206
+ let badge = el.querySelector('.q-badge');
207
+ if (!badge) {
208
+ badge = document.createElement('span');
209
+ badge.className = 'q-badge';
210
+ el.appendChild(badge);
211
+ }
212
+ badge.textContent = '⏳ ' + text;
213
+ }
214
+
215
+ function updateNetQueueUi() {
216
+ if (busy) return;
217
+ if (!netOnline) {
218
+ setStatus(netQueueCount > 0 ? _ti('net_pill_offline', netQueueCount) : _t('net_pill_online'));
219
+ } else if (!netQueueCount) {
220
+ setStatus('');
221
+ }
222
+ }
223
+
224
+ function onNetEvent(ev) {
225
+ if (ev.type === 'net') {
226
+ const was = netOnline;
227
+ netOnline = ev.online !== false;
228
+ if (was !== netOnline) {
229
+ if (netOnline) {
230
+ toast(_t('net_online'));
231
+ if (!netQueueCount) setStatus('');
232
+ } else {
233
+ toast(_t('net_offline'));
234
+ updateNetQueueUi();
235
+ }
236
+ }
237
+ return;
238
+ }
239
+ if (ev.type === 'netQueue') {
240
+ netQueueCount = typeof ev.count === 'number' ? ev.count : netQueueCount;
241
+ if (ev.queued) {
242
+ if (ev.sessionId === activeId) {
243
+ addUserBubble(ev.text || (ev.attCount ? `[${ev.attCount} ek]` : ''));
244
+ const el = els.msgs.querySelector('.msg-user:last-of-type');
245
+ if (el && ev.key) {
246
+ setNetBadge(el, _t('q_pending'));
247
+ netPending.push({ key: ev.key, el });
248
+ }
249
+ }
250
+ } else if (ev.flushed) {
251
+ /* kuyruk boşaltılıyor: ⏳ balonlarını kaldır — gerçek mesaj echo ile gelir */
252
+ for (const p of netPending) {
253
+ if (p.el && p.el.isConnected) p.el.remove();
254
+ }
255
+ netPending.length = 0;
256
+ toast(_ti('q_flushed', ev.flushed));
257
+ }
258
+ updateNetQueueUi();
259
+ }
260
+ }
261
+
196
262
  const TODO_GLYPH = { done: '✓', active: '▸', pending: '○' };
197
263
 
198
264
  function renderTodos(items) {
@@ -878,7 +944,7 @@ async function renderProviderPane() {
878
944
  form.innerHTML =
879
945
  /* TEK TİKLA MODEL (OpenCode Zen Free) */
880
946
  `<div class="zen-hero">` +
881
- `<button id="zenOneClick" class="btn">⚡ ${_t('p_zen_btn')}</button>` +
947
+ `<button id="zenOneClick" class="btn"><span class="zen-load" hidden></span><span class="zen-label">⚡ ${_t('p_zen_btn')}</span></button>` +
882
948
  `<div class="sub" style="margin-top:6px">${_t('p_zen_hint')}</div>` +
883
949
  `</div>` +
884
950
  `<label class="mem-label">${_t('p_preset')}</label>` +
@@ -928,13 +994,19 @@ async function renderProviderPane() {
928
994
  /* TEK TİKLA MODEL: OpenCode Zen free kurulumu */
929
995
  $('#zenOneClick').addEventListener('click', async () => {
930
996
  const btn = $('#zenOneClick');
931
- const old = btn.textContent;
997
+ const label = btn.querySelector('.zen-label');
998
+ const spinner = btn.querySelector('.zen-load');
999
+ const old = label.textContent;
932
1000
  btn.disabled = true;
933
- btn.textContent = _t('p_zen_busy');
1001
+ btn.classList.add('loading');
1002
+ spinner.hidden = false;
1003
+ label.textContent = _t('p_zen_busy');
934
1004
  let r;
935
1005
  try { r = await beast.zenOneClick(); } catch (e) { r = { ok: false, error: String((e && e.message) || e) }; }
936
1006
  btn.disabled = false;
937
- btn.textContent = old;
1007
+ btn.classList.remove('loading');
1008
+ spinner.hidden = true;
1009
+ label.textContent = old;
938
1010
  if (r && r.ok) {
939
1011
  let msg = _ti('p_zen_ok', (r.models || []).length) + ' (OpenCode Zen)';
940
1012
  if (r.failed && r.failed.length) msg += ' — ' + _ti('p_zen_skipped', r.failed.length);
@@ -1414,6 +1486,35 @@ async function renderIntegrationsPane() {
1414
1486
  <button id="waAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1415
1487
  </div>
1416
1488
  <div class="sub" style="margin-top:8px">${_t('it_allow_note')}</div>
1489
+ </div>
1490
+
1491
+ <div class="wa-card" style="margin-top:14px">
1492
+ <div class="wa-head">
1493
+ <div class="wa-logo tg">T</div>
1494
+ <div>
1495
+ <div class="wa-title">Telegram</div>
1496
+ <div class="wa-sub">${_t('it_tg_sub')}</div>
1497
+ </div>
1498
+ </div>
1499
+ <div class="wa-status"><span id="tgDot" class="wa-dot"></span><span id="tgStatText">—</span></div>
1500
+ <div id="tgUser" class="wa-user" hidden></div>
1501
+ <div class="form-grid" style="grid-template-columns:1fr auto;align-items:center">
1502
+ <input id="tgTokenInp" class="inp" type="password" style="margin:6px 0 0" placeholder="${_t('it_tg_token_ph')}" autocomplete="off" />
1503
+ <button id="tgSaveBtn" class="btn" style="margin-top:6px">${_t('it_tg_save')}</button>
1504
+ </div>
1505
+ <div class="wa-actions" style="margin-top:6px">
1506
+ <button id="tgStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
1507
+ </div>
1508
+ <div class="divider"></div>
1509
+ <label class="mem-label" style="margin-top:0">${_t('it_allow_label')} — Telegram</label>
1510
+ <div id="tgAllowChips" class="chips-inline"></div>
1511
+ <div class="form-grid" style="grid-template-columns:1.2fr 1fr 1fr auto;align-items:center">
1512
+ <input id="tgAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
1513
+ <input id="tgAllowIdInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_tg_id_ph')}" autocomplete="off" />
1514
+ <select id="tgAllowBotSel" class="perm-select" style="margin:6px 0 0;min-width:105px" title="${_t('bot_bind_title')}"></select>
1515
+ <button id="tgAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
1516
+ </div>
1517
+ <div class="sub" style="margin-top:8px">${_t('it_tg_note')}</div>
1417
1518
  </div>`;
1418
1519
 
1419
1520
  const waGroupsOn = $('#waGroupsOn');
@@ -1453,6 +1554,29 @@ async function renderIntegrationsPane() {
1453
1554
  waUI.status = snap.status || 'disconnected';
1454
1555
  if (snap.user) waUI.user = snap.user;
1455
1556
  updateWaPane();
1557
+
1558
+ /* TELEGRAM (FEATURE 3): token + allow list + durum */
1559
+ $('#tgSaveBtn').addEventListener('click', async () => {
1560
+ const tok = $('#tgTokenInp').value.trim();
1561
+ if (!tok) { toast(_t('it_tg_token_ph')); return; }
1562
+ toast(_t('it_tg_connecting'));
1563
+ const r = await beast.tgSetToken(tok).catch((e) => ({ status: 'error', error: String(e) }));
1564
+ $('#tgTokenInp').value = '';
1565
+ updateTgPane();
1566
+ toast(r.status === 'connected' ? 'Telegram bağlı: ' + (r.user || '') : r.status === 'error' ? _t('it_tg_token_bad') : _t('it_tg_connecting'));
1567
+ });
1568
+ $('#tgStopBtn').addEventListener('click', async () => {
1569
+ await beast.tgStop().catch(() => {});
1570
+ updateTgPane();
1571
+ toast('Kesildi');
1572
+ });
1573
+ await renderTgAllow();
1574
+ try {
1575
+ const ts = await beast.tgGetStatus();
1576
+ tgUI.status = ts.status || 'disconnected';
1577
+ tgUI.user = ts.user || null;
1578
+ updateTgPane();
1579
+ } catch {}
1456
1580
  }
1457
1581
 
1458
1582
  /* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
@@ -2086,6 +2210,172 @@ function updateWaPane() {
2086
2210
  u.textContent = waUI.user ? '👤 ' + waUI.user : '';
2087
2211
  }
2088
2212
 
2213
+ /* ---------------- integrations (Telegram — FEATURE 3) ---------------- */
2214
+
2215
+ const tgUI = { status: 'disconnected', user: null };
2216
+ const TG_STATUS_TEXT = {
2217
+ disconnected: 'Bağlı değil',
2218
+ connecting: 'Bağlanıyor…',
2219
+ connected: 'Bağlı',
2220
+ error: 'Hata — tokenı kontrol et',
2221
+ };
2222
+
2223
+ function onTgEvent(ev) {
2224
+ if (ev.type !== 'status') return;
2225
+ tgUI.status = ev.status;
2226
+ if (ev.user) tgUI.user = ev.user;
2227
+ updateTgPane();
2228
+ }
2229
+
2230
+ function updateTgPane() {
2231
+ const pane = $('#tab-integrations');
2232
+ const dot = pane && pane.querySelector('#tgDot');
2233
+ if (!dot) return;
2234
+ dot.className = 'wa-dot' + (tgUI.status === 'connected' ? ' on' : tgUI.status === 'error' ? ' qr' : '');
2235
+ pane.querySelector('#tgStatText').textContent = TG_STATUS_TEXT[tgUI.status] || tgUI.status;
2236
+ const u = pane.querySelector('#tgUser');
2237
+ u.hidden = !(tgUI.status === 'connected' && tgUI.user);
2238
+ u.textContent = tgUI.user ? '🤖 ' + tgUI.user : '';
2239
+ }
2240
+
2241
+ /* Telegram izin listesi — WA ile aynı mantık: isim zorunlu, kişi bazlı izin,
2242
+ bot eşleme. Chip × ile silinir; düzenlemek için silip yeniden ekle. */
2243
+ async function renderTgAllow() {
2244
+ const wrap = $('#tgAllowChips');
2245
+ if (!wrap) return;
2246
+ const list = await beast.tgGetAllow();
2247
+ let botChoices = [];
2248
+ try { botChoices = (await beast.botsList()) || []; } catch {}
2249
+ const multiBot = botChoices.length >= 2;
2250
+ const tgLabel = (e) => {
2251
+ if (e === '*') return '* herkes';
2252
+ if (typeof e === 'string') return e;
2253
+ const name = String((e && e.name) || '').trim();
2254
+ const id = String((e && e.id) || '');
2255
+ return (name ? name + ' ' : '') + id;
2256
+ };
2257
+ wrap.innerHTML = '';
2258
+ if (!list.length) wrap.innerHTML = '<span class="sub">— boş —</span>';
2259
+ list.forEach((entry, idx) => {
2260
+ const curPerm = (typeof entry === 'object' && entry.perm) || (entry && entry.lockdown ? 'chat' : 'all');
2261
+ const c = document.createElement('span');
2262
+ c.className = 'chip';
2263
+ c.style.margin = '0 6px 6px 0';
2264
+ const txt = document.createElement('span');
2265
+ txt.className = 'chip-txt';
2266
+ txt.textContent = tgLabel(entry);
2267
+ c.appendChild(txt);
2268
+ if (entry !== '*') {
2269
+ /* kişi bazlı granül izin: serbest/web/okuma/kısıtlı (WA ile aynı) */
2270
+ const selP = document.createElement('select');
2271
+ selP.className = 'perm-select';
2272
+ selP.title = _t('wa_perm_title');
2273
+ const PERMS = [
2274
+ ['all', _t('wa_perm_all')],
2275
+ ['web', 'web'],
2276
+ ['read', _t('wa_perm_read')],
2277
+ ['chat', _t('wa_perm_chat')],
2278
+ ];
2279
+ for (const [v, lbl] of PERMS) {
2280
+ const o = document.createElement('option');
2281
+ o.value = v;
2282
+ o.textContent = lbl;
2283
+ if (curPerm === v) o.selected = true;
2284
+ selP.appendChild(o);
2285
+ }
2286
+ selP.addEventListener('change', async () => {
2287
+ const cur = await beast.tgGetAllow();
2288
+ const next = cur.map((e, i) => {
2289
+ if (i !== idx) return e;
2290
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2291
+ base.lockdown = selP.value === 'chat';
2292
+ base.perm = selP.value;
2293
+ return base;
2294
+ });
2295
+ await beast.tgSetAllow(next);
2296
+ renderTgAllow();
2297
+ toast(tgLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
2298
+ });
2299
+ c.appendChild(selP);
2300
+
2301
+ /* BOT SİSTEMİ: 2+ bot varsa kaydı hangi botun karşılayacağını seç */
2302
+ if (multiBot) {
2303
+ const sel = document.createElement('select');
2304
+ sel.className = 'perm-select';
2305
+ sel.title = _t('bot_bind_title');
2306
+ for (const bb of botChoices) {
2307
+ const o = document.createElement('option');
2308
+ o.value = bb.id;
2309
+ o.textContent = `${bb.icon} ${bb.name}`;
2310
+ if ((typeof entry === 'object' && entry.bot_id) === bb.id) o.selected = true;
2311
+ sel.appendChild(o);
2312
+ }
2313
+ sel.addEventListener('change', async () => {
2314
+ const cur = await beast.tgGetAllow();
2315
+ const next = cur.map((e, i) => {
2316
+ if (i !== idx) return e;
2317
+ const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
2318
+ base.bot_id = sel.value || undefined;
2319
+ return base;
2320
+ });
2321
+ await beast.tgSetAllow(next);
2322
+ renderTgAllow();
2323
+ const bt = botChoices.find((bb) => bb.id === sel.value);
2324
+ toast(tgLabel(entry) + ' → ' + (bt ? bt.name : '?'));
2325
+ });
2326
+ c.appendChild(sel);
2327
+ }
2328
+ }
2329
+ const x = document.createElement('span');
2330
+ x.className = 'x';
2331
+ x.textContent = '×';
2332
+ x.addEventListener('click', async () => {
2333
+ const next = (await beast.tgGetAllow()).filter((_v, i) => i !== idx);
2334
+ await beast.tgSetAllow(next);
2335
+ renderTgAllow();
2336
+ toast('Kaldırıldı: ' + tgLabel(entry));
2337
+ });
2338
+ c.appendChild(x);
2339
+ wrap.appendChild(c);
2340
+ });
2341
+
2342
+ /* ekleme formu: isim + (ID veya @username) + bot seçici */
2343
+ const inp = $('#tgAllowIdInp');
2344
+ const nameInp = $('#tgAllowNameInp');
2345
+ const add = $('#tgAllowAdd');
2346
+ const botSel = $('#tgAllowBotSel');
2347
+ if (botSel) {
2348
+ const prev = botSel.value;
2349
+ botSel.innerHTML =
2350
+ `<option value="">${_t('bot_sel_empty')}</option>` +
2351
+ botChoices
2352
+ .map((bb) => `<option value="${bb.id}">${bb.icon} ${escapeHtml(bb.name)}${bb.admin ? ' (admin)' : ''}</option>`)
2353
+ .join('');
2354
+ if (prev) botSel.value = prev;
2355
+ }
2356
+ if (!add.dataset.bound) {
2357
+ add.dataset.bound = '1';
2358
+ const addEntry = async () => {
2359
+ let v = inp.value.trim();
2360
+ if (!v) return;
2361
+ const name = nameInp.value.trim().slice(0, 40);
2362
+ if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
2363
+ /* @username olduğu gibi; sayısal ID'den boşluk/nokta temizle */
2364
+ v = v.startsWith('@') ? '@' + v.slice(1).replace(/[^\w]/g, '') : v.replace(/[^\d]/g, '');
2365
+ if (!v) { toast(_t('it_tg_id_ph')); return; }
2366
+ const botId = botSel && botSel.value ? { bot_id: botSel.value } : {};
2367
+ await beast.tgSetAllow([...(await beast.tgGetAllow()), { id: v, name, ...botId }]);
2368
+ inp.value = '';
2369
+ nameInp.value = '';
2370
+ renderTgAllow();
2371
+ toast('Eklendi: ' + name);
2372
+ };
2373
+ add.addEventListener('click', addEntry);
2374
+ inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2375
+ nameInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
2376
+ }
2377
+ }
2378
+
2089
2379
  /* ---------------- BOT SİSTEMİ (BÖLÜM 2-3-4) ----------------
2090
2380
  Sol panel altında bot kartları; her bot için sohbet geçmişi + sekmeli yönetim.
2091
2381
  İlk bot hep Beast (admin, silinemez). Max 5 bot. */
@@ -2118,6 +2408,9 @@ async function refreshBots() {
2118
2408
  } catch { botsCache = []; }
2119
2409
  try { botPageStats = (await beast.botsStats()) || []; } catch { botPageStats = []; }
2120
2410
  renderBotCards();
2411
+ /* BOT PICKER SENKRONU: liste her yenilendiğinde üstteki rozet de aktif bota
2412
+ ayarlanır — startup yarışında (liste boşken çizilen) eski rozet düzelir */
2413
+ updateBotChip();
2121
2414
  if (!$('#botOverlay').hidden) renderBotPage();
2122
2415
  }
2123
2416
 
@@ -2160,11 +2453,20 @@ async function switchBot(id) {
2160
2453
  const b = botsCache.find((x) => x.id === id);
2161
2454
  if (!b) return;
2162
2455
  if (id === activeBotId) return;
2456
+ /* UI hemen dönsün: rozet + kartlar IPC beklemeden aktif bota geçer */
2163
2457
  activeBotId = id;
2164
- try { await beast.botsActivate(id); } catch {}
2165
- try { localStorage.setItem('beast.activeBot', id); } catch {}
2166
2458
  updateBotChip();
2167
2459
  renderBotCards();
2460
+ try {
2461
+ const r = await beast.botsActivate(id);
2462
+ /* main'inkiyle eşleş: normalizasyon ('beast' fallback) varsa düzelt */
2463
+ if (r && r.activeBotId && r.activeBotId !== activeBotId) {
2464
+ activeBotId = r.activeBotId;
2465
+ updateBotChip();
2466
+ renderBotCards();
2467
+ }
2468
+ } catch {}
2469
+ try { localStorage.setItem('beast.activeBot', id); } catch {}
2168
2470
  /* o botun en son oturumuna geç; hiç yoksa o bot için yeni sohbet aç */
2169
2471
  try {
2170
2472
  const list = (await beast.listSessions()).filter((s) => (s.botId || 'beast') === id);
@@ -2898,6 +3200,8 @@ function onEvent(ev) {
2898
3200
  scheduleAgentsRender();
2899
3201
  return;
2900
3202
  }
3203
+ /* OFFLINE MESAJ KUYRUĞU: bağlantı + kuyruk olayları (sessionId filtresinden önce) */
3204
+ if (ev.type === 'net' || ev.type === 'netQueue') { onNetEvent(ev); return; }
2901
3205
  /* Beast Code oturumu (IDE modu ortasındaki panel): olayları panele akıt,
2902
3206
  ana sohbeti kirletme */
2903
3207
  if (bcSessionId && ev.sessionId === bcSessionId) {
@@ -3826,6 +4130,7 @@ async function init() {
3826
4130
  }
3827
4131
 
3828
4132
  beast.onWaEvent(onWaEvent);
4133
+ beast.onTgEvent(onTgEvent);
3829
4134
 
3830
4135
  els.gearBtn.addEventListener('click', openSettings);
3831
4136
  els.setClose.addEventListener('click', closeSettings);
@@ -603,6 +603,7 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
603
603
  display: flex; align-items: center; justify-content: center;
604
604
  font-weight: 900; font-size: 17px;
605
605
  }
606
+ .wa-logo.tg { background: #229ED9; }
606
607
  .wa-title { font-weight: 800; font-size: 14.5px; }
607
608
  .wa-sub { color: var(--muted); font-size: 12px; }
608
609
  .wa-status { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; margin: 10px 0; }
@@ -720,6 +721,20 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
720
721
  }
721
722
  .msg-assistant .who { color: var(--accent); }
722
723
 
724
+ /* offline kuyruk: ⏳ bekleyen mesaj balonu etiketi */
725
+ .msg-user .q-badge {
726
+ display: inline-block;
727
+ margin-top: 7px;
728
+ font-size: 10.5px;
729
+ font-weight: 700;
730
+ letter-spacing: 0.3px;
731
+ color: var(--muted);
732
+ background: var(--panel);
733
+ border: 1px dashed var(--border);
734
+ border-radius: 6px;
735
+ padding: 2px 8px;
736
+ }
737
+
723
738
  .msg-error {
724
739
  border: 1px solid rgba(19, 19, 21, 0.3);
725
740
  background: rgba(19, 19, 21, 0.05);
@@ -1736,6 +1751,19 @@ body.browser-open #toast { left: calc((100vw - var(--bw, 480px)) / 2); }
1736
1751
  background: var(--panel);
1737
1752
  }
1738
1753
  .zen-hero .btn { min-width: 220px; }
1754
+ .zen-hero .btn .zen-load {
1755
+ display: inline-block;
1756
+ width: 13px;
1757
+ height: 13px;
1758
+ margin-right: 7px;
1759
+ vertical-align: -2px;
1760
+ border: 2px solid rgba(255, 255, 255, 0.35);
1761
+ border-top-color: currentColor;
1762
+ border-radius: 50%;
1763
+ animation: beast-spin 0.8s linear infinite;
1764
+ }
1765
+ .zen-hero .btn .zen-load[hidden] { display: none; }
1766
+ .zen-hero .btn.loading { opacity: 0.75; pointer-events: none; }
1739
1767
 
1740
1768
  /* bot sayfası overlay */
1741
1769
  #botOverlay {