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 +1 -1
- package/src/agent/telegram.js +155 -0
- package/src/main.js +454 -0
- package/src/preload.js +8 -0
- package/src/renderer/i18n.js +26 -0
- package/src/renderer/renderer.js +311 -6
- package/src/renderer/style.css +28 -0
package/package.json
CHANGED
|
@@ -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'),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -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',
|
package/src/renderer/renderer.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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);
|
package/src/renderer/style.css
CHANGED
|
@@ -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 {
|