beast-agent 0.25.6 → 0.25.7
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/bots.js +8 -0
- package/src/agent/discord.js +234 -0
- package/src/agent/engine.js +63 -31
- package/src/main.js +290 -0
- package/src/preload.js +8 -0
- package/src/renderer/i18n.js +18 -0
- package/src/renderer/renderer.js +299 -13
- package/tests/bg-jobs.test.js +5 -10
package/package.json
CHANGED
package/src/agent/bots.js
CHANGED
|
@@ -327,6 +327,14 @@ function update(id, patch) {
|
|
|
327
327
|
changes.push('dış tarayıcı komutu güncellendi');
|
|
328
328
|
b.extCommand = String(patch.extCommand).slice(0, 200);
|
|
329
329
|
}
|
|
330
|
+
if (typeof patch.model === 'string') {
|
|
331
|
+
/* bot bazlı model override — boş string = global seçim */
|
|
332
|
+
const next = patch.model.trim().slice(0, 160);
|
|
333
|
+
if (next !== (b.model || '')) {
|
|
334
|
+
changes.push('model güncellendi');
|
|
335
|
+
b.model = next;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
330
338
|
if (Array.isArray(patch.plugins)) {
|
|
331
339
|
const next = [...new Set(patch.plugins.map((p) => String(p).slice(0, 40)).filter((p) => PLUGIN_LIST.includes(p)))];
|
|
332
340
|
if (JSON.stringify(next) !== JSON.stringify(b.plugins || [])) {
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* DISCORD KÖPRÜSÜ — Telegram köprüsünün Discord hali.
|
|
4
|
+
Bağımlılık: yalnızca `ws` (paket zaten kurulu — baileys ile geliyor).
|
|
5
|
+
- Gateway WebSocket: Identify → READY → MESSAGE_CREATE dispatch
|
|
6
|
+
- REST (api/v10): mesaj gönderme (2000 karakter sınırında bölerek)
|
|
7
|
+
- Sunucu (guild) mesajlarında yalnız @mention'a cevap verilir (spam koruması);
|
|
8
|
+
DM'de her izinli kullanıcıya cevap verilir.
|
|
9
|
+
- Bağlantı koparsa backoff ile yeniden bağlanır (yeniden Identify;
|
|
10
|
+
çevrimdışıyken gelen mesajlar backfill edilmez — Telegram long-poll'un aksine).
|
|
11
|
+
ÖNEMLİ: Discord Developer Portal'da bot için "MESSAGE CONTENT INTENT"
|
|
12
|
+
açılmalıdır — yoksa mesaj içerikleri boş gelir. */
|
|
13
|
+
|
|
14
|
+
const WebSocket = require('ws');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
|
|
17
|
+
const API_BASE = 'https://discord.com/api/v10';
|
|
18
|
+
const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';
|
|
19
|
+
const SEND_CHUNK = 1900; // Discord mesaj sınırı 2000 — güvenli pay
|
|
20
|
+
/* GUILDS(1) | GUILD_MESSAGES(512) | DIRECT_MESSAGES(4096) | MESSAGE_CONTENT(32768) */
|
|
21
|
+
const INTENTS = 1 | 512 | 4096 | 32768;
|
|
22
|
+
|
|
23
|
+
class DiscordBridge {
|
|
24
|
+
constructor({ token, emit, onIncoming }) {
|
|
25
|
+
this.token = String(token || '').trim();
|
|
26
|
+
this.emit = emit || (() => {});
|
|
27
|
+
this.onIncoming = onIncoming || null;
|
|
28
|
+
this.connected = false;
|
|
29
|
+
this.stopping = false;
|
|
30
|
+
this.status = 'disconnected';
|
|
31
|
+
this.user = null; // { id, username, ... }
|
|
32
|
+
this._ws = null;
|
|
33
|
+
this._hbTimer = null;
|
|
34
|
+
this._seq = null;
|
|
35
|
+
this._backoff = 2000;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_setStatus(status, user) {
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.connected = status === 'connected';
|
|
41
|
+
this.emit({ type: 'status', status, user: user || null });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* REST çağrısı — JSON (token: "Bot <token>") */
|
|
45
|
+
api(method, path, body) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
48
|
+
const req = https.request(
|
|
49
|
+
`${API_BASE}${path}`,
|
|
50
|
+
{
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
Authorization: 'Bot ' + this.token,
|
|
55
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
56
|
+
},
|
|
57
|
+
timeout: 15000,
|
|
58
|
+
},
|
|
59
|
+
(res) => {
|
|
60
|
+
let data = '';
|
|
61
|
+
res.setEncoding('utf8');
|
|
62
|
+
res.on('data', (c) => (data += c));
|
|
63
|
+
res.on('end', () => {
|
|
64
|
+
try {
|
|
65
|
+
const j = data ? JSON.parse(data) : null;
|
|
66
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve(j);
|
|
67
|
+
else reject(new Error(`discord ${path}: HTTP ${res.statusCode} ${(j && j.message) || ''}`.trim()));
|
|
68
|
+
} catch {
|
|
69
|
+
reject(new Error(`discord ${path}: bozuk yanıt`));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
req.on('timeout', () => req.destroy(new Error('zaman aşımı')));
|
|
75
|
+
req.on('error', reject);
|
|
76
|
+
if (payload) req.write(payload);
|
|
77
|
+
req.end();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async start() {
|
|
82
|
+
if (!this.token) {
|
|
83
|
+
this._setStatus('error');
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
this.stopping = false;
|
|
87
|
+
this._setStatus('connecting');
|
|
88
|
+
try {
|
|
89
|
+
const me = await this.api('GET', '/users/@me');
|
|
90
|
+
this.user = me; // { id, username, ... }
|
|
91
|
+
this._setStatus('connected', '@' + (me.username || 'bot'));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
this._setStatus('error');
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
this._connect();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_connect() {
|
|
101
|
+
if (this.stopping) return;
|
|
102
|
+
clearInterval(this._hbTimer);
|
|
103
|
+
this._hbTimer = null;
|
|
104
|
+
let identified = false;
|
|
105
|
+
const ws = new WebSocket(GATEWAY_URL);
|
|
106
|
+
this._ws = ws;
|
|
107
|
+
|
|
108
|
+
ws.on('message', (raw) => {
|
|
109
|
+
let p = null;
|
|
110
|
+
try { p = JSON.parse(String(raw)); } catch { return; }
|
|
111
|
+
const op = p.op;
|
|
112
|
+
const d = p.d;
|
|
113
|
+
const t = p.t;
|
|
114
|
+
if (typeof p.s === 'number') this._seq = p.s;
|
|
115
|
+
|
|
116
|
+
if (op === 10) {
|
|
117
|
+
/* Hello: heartbeat aralığı gelıyor → hafif erken at (güvenli pay) */
|
|
118
|
+
const iv = (d && d.heartbeat_interval) || 41250;
|
|
119
|
+
clearInterval(this._hbTimer);
|
|
120
|
+
this._hbTimer = setInterval(() => {
|
|
121
|
+
try {
|
|
122
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
|
|
123
|
+
} catch {}
|
|
124
|
+
}, Math.max(5000, Math.floor(iv * 0.8)));
|
|
125
|
+
if (!identified) {
|
|
126
|
+
identified = true;
|
|
127
|
+
try {
|
|
128
|
+
ws.send(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
op: 2,
|
|
131
|
+
d: {
|
|
132
|
+
token: this.token,
|
|
133
|
+
intents: INTENTS,
|
|
134
|
+
properties: { os: 'windows', browser: 'beast-agent', device: 'beast-agent' },
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (op === 1) {
|
|
143
|
+
/* sunucu heartbeat istedi — hemen at */
|
|
144
|
+
try {
|
|
145
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 1, d: this._seq }));
|
|
146
|
+
} catch {}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (op === 9 || op === 7) {
|
|
150
|
+
/* invalid session / reconnect isteği — kapat, close handler yeniden bağlanır */
|
|
151
|
+
try { ws.close(); } catch {}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (op === 0 && t === 'READY') {
|
|
155
|
+
this._backoff = 2000;
|
|
156
|
+
this._setStatus('connected', '@' + ((d && d.user && d.user.username) || 'bot'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (op === 0 && t === 'MESSAGE_CREATE' && this.onIncoming) {
|
|
160
|
+
try {
|
|
161
|
+
const m = d || {};
|
|
162
|
+
if (!m.author || m.author.bot || m.webhook_id) return;
|
|
163
|
+
let text = String(m.content || '').trim();
|
|
164
|
+
if (!text) return;
|
|
165
|
+
const botId = this.user && this.user.id;
|
|
166
|
+
const mentioned = !!(botId && text.includes('<@' + botId + '>'));
|
|
167
|
+
if (botId) text = text.split('<@' + botId + '>').join(' ').replace(/\s+/g, ' ').trim();
|
|
168
|
+
const payload = {
|
|
169
|
+
text: text.slice(0, 6000),
|
|
170
|
+
senderId: String((m.author && m.author.id) || ''),
|
|
171
|
+
username: String((m.author && m.author.username) || ''),
|
|
172
|
+
senderName: String((m.author && (m.author.global_name || m.author.username)) || ''),
|
|
173
|
+
isGroup: !!m.guild_id,
|
|
174
|
+
mentioned,
|
|
175
|
+
channelId: String(m.channel_id || ''),
|
|
176
|
+
};
|
|
177
|
+
if (!payload.channelId) return;
|
|
178
|
+
/* Sunucu mesajlarında yalnız @mention (spam koruması); DM'de hepsi */
|
|
179
|
+
if (payload.isGroup && !mentioned) return;
|
|
180
|
+
this.onIncoming(payload.channelId, payload);
|
|
181
|
+
} catch {}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
ws.on('close', () => {
|
|
187
|
+
clearInterval(this._hbTimer);
|
|
188
|
+
this._hbTimer = null;
|
|
189
|
+
if (this.stopping) return;
|
|
190
|
+
this._setStatus('connecting');
|
|
191
|
+
const wait = this._backoff;
|
|
192
|
+
this._backoff = Math.min(30000, this._backoff * 2);
|
|
193
|
+
setTimeout(() => this._connect(), wait);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
ws.on('error', () => {
|
|
197
|
+
/* close tetiklenir — yeniden bağlanma orada */
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async stop() {
|
|
202
|
+
this.stopping = true;
|
|
203
|
+
clearInterval(this._hbTimer);
|
|
204
|
+
this._hbTimer = null;
|
|
205
|
+
try {
|
|
206
|
+
if (this._ws) this._ws.close();
|
|
207
|
+
} catch {}
|
|
208
|
+
this._ws = null;
|
|
209
|
+
this.connected = false;
|
|
210
|
+
this.status = 'disconnected';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
snapshot() {
|
|
214
|
+
return {
|
|
215
|
+
status: this.status,
|
|
216
|
+
user: this.user ? '@' + (this.user.username || 'bot') : null,
|
|
217
|
+
connected: this.connected,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Metin gönder — 2000 karakter sınırı için parçalara böl */
|
|
222
|
+
async send(channelId, text) {
|
|
223
|
+
const t = String(text || '');
|
|
224
|
+
if (!t.trim() || !channelId) return false;
|
|
225
|
+
const chunks = [];
|
|
226
|
+
for (let i = 0; i < t.length; i += SEND_CHUNK) chunks.push(t.slice(i, i + SEND_CHUNK));
|
|
227
|
+
for (const part of chunks) {
|
|
228
|
+
await this.api('POST', `/channels/${channelId}/messages`, { content: part });
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = { DiscordBridge };
|
package/src/agent/engine.js
CHANGED
|
@@ -35,9 +35,10 @@ const SUP_IDLE_MIN = 2; // ...bu kadar süredir hiç aktivite yoksa TAKILDI
|
|
|
35
35
|
const SUP_LONG_MIN = 6; // bu kadar süredir koşuyorsa ara kontrol nödü
|
|
36
36
|
const SUP_NUDGE_COOLDOWN_MIN = 4; // aynı iş için iki uyarı arası min süre
|
|
37
37
|
const BG_FIX_MAX = 2; // paralel ajanın otomatik öz-kurtarma hakkı (bitince CEO devreye girer)
|
|
38
|
-
/*
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/* SÜRE SINIRI YOK: bg ajanlar dakika bazında kesilmez. Sonsuz döngü koruması
|
|
39
|
+
üç katmanla sağlanır: 1) aktivite yoksa öz-kurtarma + CEO (stuck denetimi),
|
|
40
|
+
2) MAX_TURNS sert tur tavanı, 3) tur limitine yaklaşınca zarif "raporu yaz"
|
|
41
|
+
uyarısı (_run içinde). Dakika bazlı wrap-up KALDIRILDI (BG_WRAP_MIN). */
|
|
41
42
|
/* #5 hız: eşzamanlı arka plan LLM turu sınırı — rate-limit yemeden maksimum paralellik */
|
|
42
43
|
const BG_MAX_CONCURRENT_DEFAULT = 4;
|
|
43
44
|
/* #17 kalıcı başarısızlıkta owner'a anlık uyarı */
|
|
@@ -203,6 +204,7 @@ class Engine {
|
|
|
203
204
|
this.lockdown = !!opts.lockdown; // varsayılan kısıt (oturum bazlı override edilmezse)
|
|
204
205
|
this.sessionPerm = new Map(); // sessionId -> ['web'] | ['web','read'] | ['chat'] (kişi/bot bazlı izin)
|
|
205
206
|
this.sessionTools = new Map(); // sessionId -> Set(araç adları) — bot skill kısıtı
|
|
207
|
+
this.sessionModel = new Map(); // sessionId -> chain entry — bot bazlı model override (setSessionModel)
|
|
206
208
|
this.resolveBot = opts.resolveBot || null; // botId -> bot bilgisi (main enjekte eder)
|
|
207
209
|
/* bot oturumu hafıza köprüsü: botun kendi SOUL/USER/MEMORY dosyaları */
|
|
208
210
|
this.botMemory = opts.botMemory || null;
|
|
@@ -334,6 +336,16 @@ class Engine {
|
|
|
334
336
|
else this.sessionTools.delete(id);
|
|
335
337
|
}
|
|
336
338
|
|
|
339
|
+
/* BOT MODEL OVERRIDE: her bot farklı model kullanabilir (sel boşsa global seçim).
|
|
340
|
+
sel biçimi: 'providerId::model' — zincirde yoksa sessizce global'e düşer. */
|
|
341
|
+
setSessionModel(sessionId, sel) {
|
|
342
|
+
const id = String(sessionId || '');
|
|
343
|
+
if (!id) return;
|
|
344
|
+
const resolved = sel ? this._resolve(String(sel)) : null;
|
|
345
|
+
if (resolved) this.sessionModel.set(id, resolved);
|
|
346
|
+
else this.sessionModel.delete(id);
|
|
347
|
+
}
|
|
348
|
+
|
|
337
349
|
/* Oturumun bağlı olduğu MÜŞTERİ botu (admin/seasız → null → global hafıza) */
|
|
338
350
|
_sessionBotCtx(session) {
|
|
339
351
|
if (!session || !session.botId || typeof this.resolveBot !== 'function') return null;
|
|
@@ -1286,7 +1298,8 @@ class Engine {
|
|
|
1286
1298
|
'Kullanıcı kalıcı bir arka plan takibi isterse (fiyat eşiği, pil seviyesi, sayfa değişikliği) watcher_add ile izleyici kur; kurduktan sonra watcher_list ile doğrula ve kullanıcıya koşulu + kontrol sıklığını kısaca bildir.',
|
|
1287
1299
|
'Anlık olay takipleri için (yeni mail, fiyat eşiği, dosya değişimi, webhook) event_subscribe kullan — cron/polling gerekmez; listeyi event_list ile göster, vazgeçirirse event_unsubscribe.',
|
|
1288
1300
|
'Kullanıcı "artık hep böyle yap / bunu unutma" tarzı kalıcı talimat verirse kural olarak kaydet: sohbette /rule <metin> kullanmasını söyle VEYA kullanıcı isterse event_subscribe ile olaya bağlan (mail/fiyat/dosya/webhook).',
|
|
1289
|
-
'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle.',
|
|
1301
|
+
'Kullanıcının mesajında 2+ ayrı iş/hedef varsa (örn "X yap ve sonra Y\u2019i kontrol et") KODLAMAYA/İŞE BAŞLAMADAN önce todo_write ile plan çıkar ve sırayla yürüt; her adımı tamamlarken güncelle. LİSTE DİSİPLİNİ: her adım bittiği AN status:"done" yap; son cevabını vermeden önce tüm maddeler done olmalı — yapılmayacaksa listeden düş. Listeyi yarım bırakma.',
|
|
1302
|
+
'HIZ KURALI: Bağımsız işleri AYNI turda birden çok tool_calls ile PARALEL ver. Küçük işleri tek tek çağırma — her ayrı araç turu 5-15 sn LLM gecikmesidir: 3+ küçük komutu TEK run_command\u2019te `;` ile zincirle (örn `git status; node -v; dir`), döngülü/çoklu işleri TEK python_run betiğinde topla, çok dosyalık değişikliği TEK script ile yap. Her küçük işlem için ayrı araç çağrısı açmak yavaşlığın 1 numaralı sebebidir.',
|
|
1290
1303
|
this.ceoMode
|
|
1291
1304
|
? 'Bağımsız alt-işleri run_background ile PARALEL ajana devret; işi KENDİN YÜRÜTME — emri ver, takip et, raporla.'
|
|
1292
1305
|
: 'Bağımsız alt-işleri delegate_task ile devret; kendi başına halledebileceğin işleri devretme.',
|
|
@@ -1910,7 +1923,7 @@ class Engine {
|
|
|
1910
1923
|
job.lastActivityAt = nowIso();
|
|
1911
1924
|
}
|
|
1912
1925
|
|
|
1913
|
-
/* Saf sınıflandırma (test edilebilir): null | 'stuck' | '
|
|
1926
|
+
/* Saf sınıflandırma (test edilebilir): null | 'stuck' | 'long' */
|
|
1914
1927
|
static superviseReason(job, nowMs) {
|
|
1915
1928
|
if (!job || job.status !== 'running') return null;
|
|
1916
1929
|
const started = Date.parse(job.startedAt || '') || nowMs;
|
|
@@ -1922,7 +1935,6 @@ class Engine {
|
|
|
1922
1935
|
: Infinity;
|
|
1923
1936
|
if (nudgedMin < SUP_NUDGE_COOLDOWN_MIN) return null; // yeni uyardık — boğmayalım
|
|
1924
1937
|
if (runMin >= SUP_STUCK_START_MIN && idleMin >= SUP_IDLE_MIN) return 'stuck';
|
|
1925
|
-
if (!job.wrapAt && runMin >= BG_WRAP_MIN) return 'wrapup';
|
|
1926
1938
|
if (runMin >= SUP_LONG_MIN) return 'long';
|
|
1927
1939
|
return null;
|
|
1928
1940
|
}
|
|
@@ -1939,11 +1951,6 @@ class Engine {
|
|
|
1939
1951
|
this._bgSelfHeal(job, now);
|
|
1940
1952
|
continue;
|
|
1941
1953
|
}
|
|
1942
|
-
/* zarif bitirme: süre dolunca yeni araştırma yasak → SON RAPORU yazsın */
|
|
1943
|
-
if (reason === 'wrapup') {
|
|
1944
|
-
this._bgWrapUp(job, now);
|
|
1945
|
-
continue;
|
|
1946
|
-
}
|
|
1947
1954
|
job.lastNudgeAt = nowIso();
|
|
1948
1955
|
job.checks = (job.checks || 0) + 1;
|
|
1949
1956
|
const det = this.bgDetail(job.id);
|
|
@@ -1982,24 +1989,6 @@ class Engine {
|
|
|
1982
1989
|
this._bgKick(job, text);
|
|
1983
1990
|
}
|
|
1984
1991
|
|
|
1985
|
-
/* Zarif bitirme: süre dolunca ajanı kesip SON RAPORU yazmasını iste.
|
|
1986
|
-
Uzatma yok, öldürme yok — mevcut bulgularla düzgün kapanış. */
|
|
1987
|
-
_bgWrapUp(job, now) {
|
|
1988
|
-
job.wrapAt = nowIso();
|
|
1989
|
-
job.lastNudgeAt = nowIso();
|
|
1990
|
-
job.checks = (job.checks || 0) + 1;
|
|
1991
|
-
const runMin = Math.max(1, Math.round((now - Date.parse(job.startedAt)) / 60000));
|
|
1992
|
-
const det = this.bgDetail(job.id);
|
|
1993
|
-
const tail = det.ok && det.messages.length ? det.messages.slice(-3).join('\n') : '';
|
|
1994
|
-
const text =
|
|
1995
|
-
`[SÜRE UYARISI] "${job.title}" görevi ${runMin} dk'dır sürüyor — süre doldu.\n` +
|
|
1996
|
-
`YENİ arama/fetch/sayfa açma YAPMA. Şu ana kadar bulduklarınla SON RAPORU şimdi yaz:\n` +
|
|
1997
|
-
`- 3-5 madde: net sonuç + yapılamayanlar açıkça "bulunamadı" diye.\n` +
|
|
1998
|
-
`Raporu yazınca görev tamamlanır — fazladan tur harcama.` +
|
|
1999
|
-
(tail ? `\n\nSon çıktın:\n${tail}` : '');
|
|
2000
|
-
this._bgKick(job, text);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
1992
|
/* Arka plan oturumuna müdahale: asılı turu kes (ctrl.abort), sonra
|
|
2004
1993
|
kurtarma mesajını bas. abort → eski _run sonlanır → send yeni tur açar.
|
|
2005
1994
|
revive bayrağı: /stop veya silme sonrası bekleyen müdahaleler patlamasın. */
|
|
@@ -2225,6 +2214,11 @@ class Engine {
|
|
|
2225
2214
|
role = 'terminal';
|
|
2226
2215
|
}
|
|
2227
2216
|
let sel = this.modelFor(role);
|
|
2217
|
+
/* bot model override: rol modeli (vision/terminal) yoksa botun kendi seçimi kazanır */
|
|
2218
|
+
if (!role) {
|
|
2219
|
+
const ssel = this.sessionModel.get(String(session.id));
|
|
2220
|
+
if (ssel) sel = ssel;
|
|
2221
|
+
}
|
|
2228
2222
|
if (role && this.roleModels[role]) {
|
|
2229
2223
|
emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
|
|
2230
2224
|
}
|
|
@@ -2288,9 +2282,26 @@ class Engine {
|
|
|
2288
2282
|
const sid = session.id;
|
|
2289
2283
|
const emit = (ev) => this.emit({ ...ev, sessionId: sid });
|
|
2290
2284
|
try {
|
|
2291
|
-
if (!this.sel) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
|
|
2285
|
+
if (!this.sel && !this.sessionModel.get(String(session.id))) throw new Error('Model yapılandırılmadı — %APPDATA%\\beast\\config.yaml ve .env kontrol et');
|
|
2292
2286
|
|
|
2287
|
+
let nudged = false; // görev listesi disiplini: run başına en fazla 1 hatırlatma
|
|
2288
|
+
let wrapNudged = false; // tur limitine yaklaşınca zarif kapanış (bg ajanlar)
|
|
2293
2289
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
2290
|
+
/* SÜRE SINIRI YOK — ama sonsuz tur da yok: bg ajan son 3 tura gelirken
|
|
2291
|
+
"raporu yaz ve bitir" uyarısı alır; MAX_TURNS sert tavan olarak kalır. */
|
|
2292
|
+
if (session.bgJob && turn === MAX_TURNS - 3 && !wrapNudged) {
|
|
2293
|
+
wrapNudged = true;
|
|
2294
|
+
const wmsg = {
|
|
2295
|
+
role: 'user',
|
|
2296
|
+
content:
|
|
2297
|
+
'[TUR LİMİTİ YAKLAŞIYOR] Kalın son turlar: yeni araştırma/iş AÇMA — şu ana kadarki bulgularınla SON RAPORU yaz ve bitir. Yapılamayanları açıkça "bulunamadı" diye belirt.',
|
|
2298
|
+
};
|
|
2299
|
+
session.messages.push(wmsg);
|
|
2300
|
+
try {
|
|
2301
|
+
this._append(session, wmsg);
|
|
2302
|
+
} catch {}
|
|
2303
|
+
emit({ type: 'message', message: wmsg });
|
|
2304
|
+
}
|
|
2294
2305
|
this._bgTrim(session); // #21 bg geçmişini olabildiğince ince tut
|
|
2295
2306
|
emit({ type: 'status', status: 'thinking' });
|
|
2296
2307
|
const res = await this._chatTurn(session, ctrl.signal, (delta) =>
|
|
@@ -2306,6 +2317,27 @@ class Engine {
|
|
|
2306
2317
|
emit({ type: 'message', message: assistant });
|
|
2307
2318
|
|
|
2308
2319
|
if (!res.toolCalls || !res.toolCalls.length) {
|
|
2320
|
+
/* GÖREV LİSTESİ DİSİPLİNİ: ajan işi bitti sanıyor ama listede hâlâ
|
|
2321
|
+
bekleyen/aktif madde varsa BİR KEZ hatırlat ve tura devam et —
|
|
2322
|
+
liste ya tamamlanmalı ya kalan maddeler listeden düşürülmeli. */
|
|
2323
|
+
const todos = session.todos || this.todos.get(sid) || [];
|
|
2324
|
+
const pending = todos.filter((t) => t && t.status !== 'done');
|
|
2325
|
+
if (pending.length && !nudged) {
|
|
2326
|
+
nudged = true;
|
|
2327
|
+
const nudge = {
|
|
2328
|
+
role: 'user',
|
|
2329
|
+
content:
|
|
2330
|
+
'[OTOMATİK HATIRLATMA] Görev listende hâlâ tamamlanmamış maddeler var: ' +
|
|
2331
|
+
pending.map((t) => '"' + t.title + '"').join(', ') +
|
|
2332
|
+
'. Görevi yarım bırakma: eksik adımları ŞİMDİ tamamla; gerçekten yapılmayacaksa listeden düş. todo_write ile listeyi güncelle (status:"done") ve işi kapat.',
|
|
2333
|
+
};
|
|
2334
|
+
session.messages.push(nudge);
|
|
2335
|
+
try {
|
|
2336
|
+
this._append(session, nudge);
|
|
2337
|
+
} catch {}
|
|
2338
|
+
emit({ type: 'message', message: nudge });
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2309
2341
|
/* cevap tamam → done HEMEN: kullanıcı beklemeden yeni iş yazabilsin.
|
|
2310
2342
|
Not/memory/skill bakımı artık arka planda (_postRunHousekeeping). */
|
|
2311
2343
|
this._clearCrash();
|
|
@@ -3282,7 +3314,7 @@ const TOOLS = [
|
|
|
3282
3314
|
function: {
|
|
3283
3315
|
name: 'todo_write',
|
|
3284
3316
|
description:
|
|
3285
|
-
'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done.',
|
|
3317
|
+
'Replace the visible task checklist for this chat. Use ONLY for multi-step work (3+ steps); do not use for simple questions. Keep titles short; update statuses as you progress; clear the list when done. DISCIPLINE: mark each step done THE MOMENT it is completed; NEVER end your reply while items are still pending/active — the system bounces unfinished lists back.',
|
|
3286
3318
|
parameters: {
|
|
3287
3319
|
type: 'object',
|
|
3288
3320
|
properties: {
|
package/src/main.js
CHANGED
|
@@ -16,6 +16,7 @@ const skillsMod = require('./agent/skills');
|
|
|
16
16
|
const storeMod = require('./agent/store');
|
|
17
17
|
const { WhatsAppBridge } = require('./agent/whatsapp');
|
|
18
18
|
const { TelegramBridge } = require('./agent/telegram');
|
|
19
|
+
const { DiscordBridge } = require('./agent/discord');
|
|
19
20
|
const cron = require('./cron');
|
|
20
21
|
const watchers = require('./agent/watchers');
|
|
21
22
|
const usageMod = require('./agent/usage');
|
|
@@ -229,6 +230,11 @@ let tg = null;
|
|
|
229
230
|
let tgChats = new Map(); // telegram chatId -> aktif session id
|
|
230
231
|
let tgHistory = new Map(); // chatId -> [sid,...]
|
|
231
232
|
const TG_HISTORY_CAP = 20;
|
|
233
|
+
let dc = null;
|
|
234
|
+
let dcChats = new Map(); // discord channelId -> aktif session id
|
|
235
|
+
let dcHistory = new Map(); // channelId -> [sid,...]
|
|
236
|
+
const DC_HISTORY_CAP = 20;
|
|
237
|
+
const DC_CHATS_FILE = path.join(APP_DIR, 'dc-chats.json');
|
|
232
238
|
let tray = null;
|
|
233
239
|
app.isQuitting = false;
|
|
234
240
|
|
|
@@ -1711,6 +1717,7 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
|
1711
1717
|
} else {
|
|
1712
1718
|
engine.setSessionTools(sid, null);
|
|
1713
1719
|
}
|
|
1720
|
+
engine.setSessionModel(sid, botCfg && !botCfg.admin ? (botCfg.model || null) : null);
|
|
1714
1721
|
waLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
|
|
1715
1722
|
|
|
1716
1723
|
const participantName = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
|
|
@@ -1979,6 +1986,7 @@ async function processTgMessage(chatId, payload, requeues = 0) {
|
|
|
1979
1986
|
} else {
|
|
1980
1987
|
engine.setSessionTools(sid, null);
|
|
1981
1988
|
}
|
|
1989
|
+
engine.setSessionModel(sid, botCfg && !botCfg.admin ? (botCfg.model || null) : null);
|
|
1982
1990
|
tgLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
|
|
1983
1991
|
|
|
1984
1992
|
/* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
|
|
@@ -2037,6 +2045,224 @@ async function restartTg() {
|
|
|
2037
2045
|
}
|
|
2038
2046
|
}
|
|
2039
2047
|
|
|
2048
|
+
/* ---------- DISCORD: allow list — WA/TG ile aynı mantık ----------
|
|
2049
|
+
Liste formatı: [{ id:'123456789' | '@kullanici_adi', name, perm, bot_id }, '*']
|
|
2050
|
+
Eşleşme: sayısal ID birebir, @username büyük/küçük harf duyarsız. */
|
|
2051
|
+
function dcLog(line) {
|
|
2052
|
+
try { log.info('discord', line); } catch {}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function dcFind(senderId, username) {
|
|
2056
|
+
const list = settings.dcAllow || [];
|
|
2057
|
+
if (!list.length) return null; // boş liste = kimseye cevap yok
|
|
2058
|
+
const id = String(senderId || '').trim();
|
|
2059
|
+
const uname = String(username || '').replace(/^@/, '').toLowerCase();
|
|
2060
|
+
for (const e of list) {
|
|
2061
|
+
if (e === '*') return { id: '*', name: '' };
|
|
2062
|
+
const eid = typeof e === 'string' ? e.trim() : String((e && e.id) || '').trim();
|
|
2063
|
+
if (!eid) continue;
|
|
2064
|
+
if (eid === '*') return { id: '*', name: '' };
|
|
2065
|
+
if (eid.startsWith('@')) {
|
|
2066
|
+
if (uname && eid.slice(1).toLowerCase() === uname) {
|
|
2067
|
+
return typeof e === 'string' ? { id: eid, name: '' } : e;
|
|
2068
|
+
}
|
|
2069
|
+
} else if (id && eid === id) {
|
|
2070
|
+
return typeof e === 'string' ? { id: eid, name: '' } : e;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return null;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
(function dcChatsLoad() {
|
|
2077
|
+
try {
|
|
2078
|
+
const raw = JSON.parse(fs.readFileSync(DC_CHATS_FILE, 'utf8'));
|
|
2079
|
+
if (raw && typeof raw.chats === 'object') {
|
|
2080
|
+
for (const [c, s] of Object.entries(raw.chats)) {
|
|
2081
|
+
if (typeof s === 'string') dcChats.set(c, s);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (raw && typeof raw.history === 'object') {
|
|
2085
|
+
for (const [c, arr] of Object.entries(raw.history)) {
|
|
2086
|
+
if (Array.isArray(arr)) dcHistory.set(c, arr.filter((x) => typeof x === 'string').slice(-DC_HISTORY_CAP));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
for (const [c, s] of dcChats.entries()) {
|
|
2090
|
+
const h = dcHistory.get(c) || [];
|
|
2091
|
+
if (!h.includes(s)) h.push(s);
|
|
2092
|
+
dcHistory.set(c, h.slice(-DC_HISTORY_CAP));
|
|
2093
|
+
}
|
|
2094
|
+
} catch {}
|
|
2095
|
+
})();
|
|
2096
|
+
|
|
2097
|
+
function saveDcChats() {
|
|
2098
|
+
try {
|
|
2099
|
+
fs.writeFileSync(
|
|
2100
|
+
DC_CHATS_FILE,
|
|
2101
|
+
JSON.stringify({
|
|
2102
|
+
chats: Object.fromEntries(dcChats),
|
|
2103
|
+
history: Object.fromEntries([...dcHistory.entries()].map(([c, a]) => [c, a.slice(-DC_HISTORY_CAP)])),
|
|
2104
|
+
})
|
|
2105
|
+
);
|
|
2106
|
+
} catch {}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function dcRememberSession(channelId, sid) {
|
|
2110
|
+
const h = dcHistory.get(channelId) || [];
|
|
2111
|
+
if (!h.includes(sid)) h.push(sid);
|
|
2112
|
+
dcHistory.set(channelId, h.slice(-DC_HISTORY_CAP));
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const DC_DEBOUNCE_MS = 4500;
|
|
2116
|
+
const dcQueue = new Map(); // channelId -> { payloads[] }
|
|
2117
|
+
|
|
2118
|
+
function dcQueuePush(channelId, payload) {
|
|
2119
|
+
let q = dcQueue.get(channelId);
|
|
2120
|
+
if (!q) {
|
|
2121
|
+
q = { payloads: [] };
|
|
2122
|
+
dcQueue.set(channelId, q);
|
|
2123
|
+
}
|
|
2124
|
+
q.payloads.push(payload);
|
|
2125
|
+
clearTimeout(q.timer);
|
|
2126
|
+
dcLog(`queue: mesaj kuyruğa girdi channel=${channelId} toplam=${q.payloads.length} (4.5 sn birleştirme)`);
|
|
2127
|
+
q.timer = setTimeout(() => {
|
|
2128
|
+
dcFlush(channelId).catch((e) => dcLog(`flush KRASİ: ${String((e && e.stack) || e)}`));
|
|
2129
|
+
}, DC_DEBOUNCE_MS);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
async function dcFlush(channelId) {
|
|
2133
|
+
const q = dcQueue.get(channelId);
|
|
2134
|
+
if (!q) return;
|
|
2135
|
+
dcQueue.delete(channelId);
|
|
2136
|
+
const merged = { text: '', senderId: '', username: '', senderName: '' };
|
|
2137
|
+
for (const p of q.payloads) {
|
|
2138
|
+
if (p.text) merged.text += (merged.text ? '\n' : '') + p.text;
|
|
2139
|
+
if (!merged.senderId && p.senderId) { merged.senderId = p.senderId; merged.username = p.username; merged.senderName = p.senderName; }
|
|
2140
|
+
}
|
|
2141
|
+
await processDcMessage(channelId, merged);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
async function handleDcIncoming(channelId, payload) {
|
|
2145
|
+
try {
|
|
2146
|
+
const hit = dcFind(payload.senderId, payload.username);
|
|
2147
|
+
dcLog(
|
|
2148
|
+
`incoming channel=${channelId} sender=${payload.senderId || '?'} user=${payload.username || '-'} allowed=${!!hit}` +
|
|
2149
|
+
(hit && hit.name ? ' name=' + hit.name : '')
|
|
2150
|
+
);
|
|
2151
|
+
if (!hit) return; // allowlist dışı yoksay
|
|
2152
|
+
/* İsimsiz kayıt: güvenlik için cevap verme — kullanıcıyı ayarlara yönlendir */
|
|
2153
|
+
if (hit.id !== '*' && !hit.name) {
|
|
2154
|
+
dcLog(`skip: isimsiz kayıt (${hit.id}) — cevap verilmedi, Entegrasyonlar'da isim ekle`);
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
resumeServices(); // pause durumunda gelen mesaj servisleri canlandırır
|
|
2158
|
+
dcQueuePush(String(channelId), payload);
|
|
2159
|
+
} catch (e) {
|
|
2160
|
+
dcLog(`handleDcIncoming KRASİ: ${String((e && e.stack) || e)}`);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
async function processDcMessage(channelId, payload) {
|
|
2165
|
+
const hit = dcFind(payload.senderId, payload.username);
|
|
2166
|
+
if (!hit) {
|
|
2167
|
+
dcLog(`skip flush: izinli eşleşme yok (sender=${payload.senderId || '?'})`);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
let sid = dcChats.get(channelId);
|
|
2171
|
+
if (sid && engine.isBusy(sid)) {
|
|
2172
|
+
/* oturum meşgul — WA/TG ile aynı: kaybetme, iş bitene dek yeniden dene */
|
|
2173
|
+
await new Promise((r) => setTimeout(r, DC_DEBOUNCE_MS));
|
|
2174
|
+
return processDcMessage(channelId, payload, 1);
|
|
2175
|
+
}
|
|
2176
|
+
if (!sid) {
|
|
2177
|
+
const v = engine.createSession();
|
|
2178
|
+
sid = v.id;
|
|
2179
|
+
dcChats.set(channelId, sid);
|
|
2180
|
+
dcRememberSession(channelId, sid);
|
|
2181
|
+
saveDcChats();
|
|
2182
|
+
} else {
|
|
2183
|
+
dcRememberSession(channelId, sid);
|
|
2184
|
+
}
|
|
2185
|
+
/* Kişi bazlı granül izin: all/web/read/chat */
|
|
2186
|
+
let perm = hit.perm || (hit.lockdown ? 'chat' : 'all');
|
|
2187
|
+
engine.setSessionPerm(sid, perm);
|
|
2188
|
+
|
|
2189
|
+
/* BOT SİSTEMİ: izinli kayıtta bot_id yoksa beast'e düşer (WA/TG ile aynı) */
|
|
2190
|
+
let botId = hit && hit.bot_id ? String(hit.bot_id) : 'beast';
|
|
2191
|
+
if (!bots.get(botId)) {
|
|
2192
|
+
if (botId !== 'beast') dcLog(`bot="${botId}" yok — kayıt botsuz, beast (admin) botuna yönlendirildi`);
|
|
2193
|
+
botId = 'beast';
|
|
2194
|
+
}
|
|
2195
|
+
engine.setSessionBot(sid, botId);
|
|
2196
|
+
const botCfg = bots.get(botId);
|
|
2197
|
+
if (botCfg && !botCfg.admin) {
|
|
2198
|
+
const eff = moreRestrictivePerm(perm, botCfg.perm || 'all');
|
|
2199
|
+
if (eff !== perm) {
|
|
2200
|
+
perm = eff;
|
|
2201
|
+
engine.setSessionPerm(sid, eff);
|
|
2202
|
+
}
|
|
2203
|
+
engine.setSessionTools(sid, botToolSet(botCfg));
|
|
2204
|
+
} else {
|
|
2205
|
+
engine.setSessionTools(sid, null);
|
|
2206
|
+
}
|
|
2207
|
+
engine.setSessionModel(sid, botCfg && !botCfg.admin ? (botCfg.model || null) : null);
|
|
2208
|
+
dcLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
|
|
2209
|
+
|
|
2210
|
+
/* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
|
|
2211
|
+
const isOwner = !!hit.owner;
|
|
2212
|
+
const roleTag = isOwner
|
|
2213
|
+
? 'SAHİBİN (talepleri önceliklidir)'
|
|
2214
|
+
: 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
|
|
2215
|
+
const label =
|
|
2216
|
+
(hit.name || payload.senderName || '?') +
|
|
2217
|
+
(payload.username ? ` (@${payload.username})` : '') +
|
|
2218
|
+
` — ${roleTag}`;
|
|
2219
|
+
let text = `[Discord — gönderen: ${label}]`;
|
|
2220
|
+
if (!isOwner) {
|
|
2221
|
+
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.]`;
|
|
2222
|
+
}
|
|
2223
|
+
text += `\n${String(payload.text || '').slice(0, 6000)}`;
|
|
2224
|
+
engine.send(sid, { text: text.slice(0, 8000), attachments: [] });
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
async function sendDcSafe(channelId, text) {
|
|
2228
|
+
if (!dc) return false;
|
|
2229
|
+
try {
|
|
2230
|
+
return !!(await dc.send(channelId, text));
|
|
2231
|
+
} catch (e) {
|
|
2232
|
+
dcLog(`send hata channel=${channelId}: ${String((e && e.message) || e)}`);
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function ensureDc() {
|
|
2238
|
+
if (!dc) {
|
|
2239
|
+
dc = new DiscordBridge({
|
|
2240
|
+
token: settings.dcToken || '',
|
|
2241
|
+
emit: (ev) => {
|
|
2242
|
+
if (ev.type === 'status') dcLog(`status=${ev.status}${ev.user ? ' user=' + ev.user : ''}`);
|
|
2243
|
+
if (win && !win.isDestroyed()) win.webContents.send('dc:event', ev);
|
|
2244
|
+
},
|
|
2245
|
+
onIncoming: handleDcIncoming,
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
return dc;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/* token değişimi / yeniden başlatma: eski köprüyü kapat, yenisini aç */
|
|
2252
|
+
async function restartDc() {
|
|
2253
|
+
if (dc) {
|
|
2254
|
+
try { await dc.stop(); } catch {}
|
|
2255
|
+
dc = null;
|
|
2256
|
+
}
|
|
2257
|
+
if (!settings.dcToken) return;
|
|
2258
|
+
const b = ensureDc();
|
|
2259
|
+
try {
|
|
2260
|
+
await b.start();
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
dcLog(`start başarısız: ${String((e && e.message) || e)}`);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2040
2266
|
function reloadBackend() {
|
|
2041
2267
|
if (engine && typeof engine.dispose === 'function') {
|
|
2042
2268
|
try { engine.dispose(); } catch {}
|
|
@@ -2206,6 +2432,27 @@ function reloadBackend() {
|
|
|
2206
2432
|
})();
|
|
2207
2433
|
}
|
|
2208
2434
|
}
|
|
2435
|
+
// Discord oturumlarının son cevabını geri gönder (TG ile aynı akış)
|
|
2436
|
+
if ((ev.type === 'done' || ev.type === 'error') && dc && dc.connected) {
|
|
2437
|
+
const hitD = [...dcChats.entries()].find(([, s]) => s === ev.sessionId);
|
|
2438
|
+
if (hitD) {
|
|
2439
|
+
const dchid = hitD[0];
|
|
2440
|
+
(async () => {
|
|
2441
|
+
try {
|
|
2442
|
+
if (ev.type === 'error') {
|
|
2443
|
+
await sendDcSafe(dchid, 'Bir aksilik oldu: ' + String(ev.error || '').slice(0, 200));
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (!ev.aborted) {
|
|
2447
|
+
const s = engine.openSession(ev.sessionId);
|
|
2448
|
+
const lastA = [...s.messages].reverse().find((m) => m.role === 'assistant' && m.content);
|
|
2449
|
+
const txt = typeof (lastA && lastA.content) === 'string' ? lastA.content : '';
|
|
2450
|
+
if (txt.trim()) await sendDcSafe(dchid, txt);
|
|
2451
|
+
}
|
|
2452
|
+
} catch {}
|
|
2453
|
+
})();
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2209
2456
|
},
|
|
2210
2457
|
});
|
|
2211
2458
|
return engine.publicState();
|
|
@@ -2396,6 +2643,11 @@ app.whenReady().then(() => {
|
|
|
2396
2643
|
ensureTg().start().catch((e) => tgLog('autostart failed: ' + String((e && e.message) || e)));
|
|
2397
2644
|
}
|
|
2398
2645
|
|
|
2646
|
+
// Discord köprüsünü otomatik başlat (token kayıtlıysa)
|
|
2647
|
+
if (settings.dcToken) {
|
|
2648
|
+
ensureDc().start().catch((e) => dcLog('autostart failed: ' + String((e && e.message) || e)));
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2399
2651
|
app.on('activate', () => {
|
|
2400
2652
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
2401
2653
|
});
|
|
@@ -3451,6 +3703,7 @@ ipcMain.handle('sessions:create', () => {
|
|
|
3451
3703
|
} else {
|
|
3452
3704
|
engine.setSessionTools(v.id, null);
|
|
3453
3705
|
}
|
|
3706
|
+
engine.setSessionModel(v.id, b && !b.admin ? (b.model || null) : null);
|
|
3454
3707
|
return v;
|
|
3455
3708
|
});
|
|
3456
3709
|
ipcMain.handle('sessions:open', (_e, id) => engine.openSession(id));
|
|
@@ -5542,6 +5795,42 @@ ipcMain.handle('tg:allow:set', (_e, list) => {
|
|
|
5542
5795
|
});
|
|
5543
5796
|
ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
|
|
5544
5797
|
|
|
5798
|
+
/* ---------- Discord IPC ---------- */
|
|
5799
|
+
ipcMain.handle('dc:status:get', () => {
|
|
5800
|
+
if (!dc) return { configured: !!settings.dcToken, status: 'disconnected', user: null, connected: false };
|
|
5801
|
+
return { configured: true, ...dc.snapshot() };
|
|
5802
|
+
});
|
|
5803
|
+
|
|
5804
|
+
/* token kaydet + köprüyü (yeniden) başlat */
|
|
5805
|
+
ipcMain.handle('dc:set', async (_e, token) => {
|
|
5806
|
+
const t = String(token || '').trim();
|
|
5807
|
+
if (t) settings.dcToken = t;
|
|
5808
|
+
saveSettings();
|
|
5809
|
+
await restartDc();
|
|
5810
|
+
return { configured: !!settings.dcToken, ...(dc ? dc.snapshot() : { status: 'disconnected', user: null }) };
|
|
5811
|
+
});
|
|
5812
|
+
|
|
5813
|
+
ipcMain.handle('dc:start', async () => {
|
|
5814
|
+
if (!settings.dcToken) return { ok: false, error: 'token yok — önce bot tokenı gir' };
|
|
5815
|
+
await restartDc();
|
|
5816
|
+
return { ok: true, ...(dc ? dc.snapshot() : {}) };
|
|
5817
|
+
});
|
|
5818
|
+
|
|
5819
|
+
ipcMain.handle('dc:stop', async () => {
|
|
5820
|
+
if (dc) {
|
|
5821
|
+
try { await dc.stop(); } catch {}
|
|
5822
|
+
}
|
|
5823
|
+
return { ok: true };
|
|
5824
|
+
});
|
|
5825
|
+
|
|
5826
|
+
ipcMain.handle('dc:allow:get', () => settings.dcAllow || []);
|
|
5827
|
+
ipcMain.handle('dc:allow:set', (_e, list) => {
|
|
5828
|
+
settings.dcAllow = Array.isArray(list) ? list : [];
|
|
5829
|
+
saveSettings();
|
|
5830
|
+
return settings.dcAllow;
|
|
5831
|
+
});
|
|
5832
|
+
ipcMain.handle('dc:sessions', () => [...dcChats.values()]);
|
|
5833
|
+
|
|
5545
5834
|
/* ---------- e-posta IPC ---------- */
|
|
5546
5835
|
|
|
5547
5836
|
ipcMain.handle('email:get', () => {
|
|
@@ -5803,6 +6092,7 @@ ipcMain.handle('bots:update', (_e, { id, patch }) => {
|
|
|
5803
6092
|
if ((v.botId || 'beast') === String(id || '')) {
|
|
5804
6093
|
const cfg = bots.get(String(id));
|
|
5805
6094
|
engine.setSessionTools(v.id, cfg && !cfg.admin ? botToolSet(cfg) : null);
|
|
6095
|
+
engine.setSessionModel(v.id, cfg && !cfg.admin ? (cfg.model || null) : null);
|
|
5806
6096
|
}
|
|
5807
6097
|
}
|
|
5808
6098
|
} catch {}
|
package/src/preload.js
CHANGED
|
@@ -125,6 +125,14 @@ contextBridge.exposeInMainWorld('beast', {
|
|
|
125
125
|
tgSetAllow: (list) => ipcRenderer.invoke('tg:allow:set', list),
|
|
126
126
|
tgListSessions: () => ipcRenderer.invoke('tg:sessions'),
|
|
127
127
|
onTgEvent: (cb) => ipcRenderer.on('tg:event', (_e, ev) => cb(ev)),
|
|
128
|
+
dcGetStatus: () => ipcRenderer.invoke('dc:status:get'),
|
|
129
|
+
dcSetToken: (token) => ipcRenderer.invoke('dc:set', token),
|
|
130
|
+
dcStart: () => ipcRenderer.invoke('dc:start'),
|
|
131
|
+
dcStop: () => ipcRenderer.invoke('dc:stop'),
|
|
132
|
+
dcGetAllow: () => ipcRenderer.invoke('dc:allow:get'),
|
|
133
|
+
dcSetAllow: (list) => ipcRenderer.invoke('dc:allow:set', list),
|
|
134
|
+
dcListSessions: () => ipcRenderer.invoke('dc:sessions'),
|
|
135
|
+
onDcEvent: (cb) => ipcRenderer.on('dc:event', (_e, ev) => cb(ev)),
|
|
128
136
|
getUsage: () => ipcRenderer.invoke('usage:get'),
|
|
129
137
|
resetUsage: () => ipcRenderer.invoke('usage:reset'),
|
|
130
138
|
createBackup: () => ipcRenderer.invoke('backup:create'),
|
package/src/renderer/i18n.js
CHANGED
|
@@ -209,6 +209,12 @@
|
|
|
209
209
|
it_tg_token_bad: 'Token doğrulanamadı — @BotFather\u2019dan aldığın tokenı kontrol et',
|
|
210
210
|
it_tg_id_ph: 'Kullanıcı ID veya @kullanıcı_adı',
|
|
211
211
|
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.',
|
|
212
|
+
it_dc_sub: 'Bot tokenı ile çalışır — izin listendeki kişilere Discord\u2019dan cevap verir (sunucularda @mention bekler)',
|
|
213
|
+
it_dc_token_ph: 'Bot tokenı (Developer Portal → Bot → Reset Token)',
|
|
214
|
+
it_dc_connecting: 'Discord\u2019a bağlanıyor…',
|
|
215
|
+
it_dc_token_bad: 'Token doğrulanamadı — Developer Portal\u2019dan aldığın tokenı kontrol et',
|
|
216
|
+
it_dc_id_ph: 'Kullanıcı ID veya @kullanıcı_adı',
|
|
217
|
+
it_dc_note: 'İzin listesi boşsa kimse cevap almaz. Bot oluşturma: discord.com/developers → New Application → Bot → Token. "MESSAGE CONTENT INTENT"i AÇ (yoksa mesajlar boş gelir). Sunucu daveti: OAuth2 → bot yetkisiyle. Kendi ID\u2019ni öğrenmek için geliştirici modunu açıp kendine sağ tıkla → ID\u2019yi kopyala.',
|
|
212
218
|
ev_h2: 'Olay Merkezi',
|
|
213
219
|
ev_sub: 'Cron\u2019suz canlı olaylar — kaynakları aç, abonelikleri agent kendisi kurar (event_subscribe). Tetiklenen olay ilgili sohbete düşer, cevap WhatsApp\u2019a gider.',
|
|
214
220
|
ev_on: 'Olay merkezi açık',
|
|
@@ -409,6 +415,9 @@
|
|
|
409
415
|
bot_perm: 'Yetki seviyesi',
|
|
410
416
|
bot_perm_note: 'all tek başına tüm araçları açar (özel). Web / Okuma / Sohbet birden fazla seçilebilir — seçilenlerin araçları birleşir.',
|
|
411
417
|
bot_skills: 'Skill erişimi',
|
|
418
|
+
bot_model: 'Model',
|
|
419
|
+
bot_model_global: 'Global seçim (üstteki picker)',
|
|
420
|
+
bot_model_note: 'Bu bot için farklı model seçebilirsin; boş bırakırsan üstteki global model kullanılır. Seçim sadece bu bota bağlı sohbetlerde geçerli.',
|
|
412
421
|
bot_browser: 'Tarayıcı ayarları',
|
|
413
422
|
bot_ext_browser: 'Dış tarayıcı yetkisi (kullanıcı isterse)',
|
|
414
423
|
bot_def_browser: 'Varsayılan tarayıcı',
|
|
@@ -727,6 +736,12 @@
|
|
|
727
736
|
it_tg_token_bad: 'Token could not be verified — check the token from @BotFather',
|
|
728
737
|
it_tg_id_ph: 'User ID or @username',
|
|
729
738
|
it_tg_note: 'Empty allow list = nobody gets a reply. Create a bot: @BotFather → /newbot. To learn your ID, message @userinfobot.',
|
|
739
|
+
it_dc_sub: 'Works with a bot token — replies on Discord to people in your allow list (mentions only in servers)',
|
|
740
|
+
it_dc_token_ph: 'Bot token (Developer Portal → Bot → Reset Token)',
|
|
741
|
+
it_dc_connecting: 'Connecting to Discord…',
|
|
742
|
+
it_dc_token_bad: 'Token could not be verified — check the token from the Developer Portal',
|
|
743
|
+
it_dc_id_ph: 'User ID or @username',
|
|
744
|
+
it_dc_note: 'Empty allow list = nobody gets a reply. Create a bot: discord.com/developers → New Application → Bot → Token. Turn ON "MESSAGE CONTENT INTENT". Invite via OAuth2 bot URL. To learn your ID: enable developer mode, right-click yourself → Copy ID.',
|
|
730
745
|
ev_h2: 'Event Center',
|
|
731
746
|
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.',
|
|
732
747
|
ev_on: 'Event center on',
|
|
@@ -985,6 +1000,9 @@
|
|
|
985
1000
|
bot_perm: 'Permission level',
|
|
986
1001
|
bot_perm_note: 'all alone grants every tool (exclusive). Web / Read / Chat can be multi-selected — their tool sets are combined.',
|
|
987
1002
|
bot_skills: 'Skill access',
|
|
1003
|
+
bot_model: 'Model',
|
|
1004
|
+
bot_model_global: 'Global selection (main picker)',
|
|
1005
|
+
bot_model_note: 'You can pick a different model for this bot; leave empty to use the global selection. Applies only to chats bound to this bot.',
|
|
988
1006
|
bot_browser: 'Browser settings',
|
|
989
1007
|
bot_ext_browser: 'External browser permission (when user asks)',
|
|
990
1008
|
bot_def_browser: 'Default browser',
|
package/src/renderer/renderer.js
CHANGED
|
@@ -1535,6 +1535,35 @@ async function renderIntegrationsPane() {
|
|
|
1535
1535
|
<button id="tgAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
|
|
1536
1536
|
</div>
|
|
1537
1537
|
<div class="sub" style="margin-top:8px">${_t('it_tg_note')}</div>
|
|
1538
|
+
</div>
|
|
1539
|
+
|
|
1540
|
+
<div class="wa-card" style="margin-top:14px">
|
|
1541
|
+
<div class="wa-head">
|
|
1542
|
+
<div class="wa-logo tg">D</div>
|
|
1543
|
+
<div>
|
|
1544
|
+
<div class="wa-title">Discord</div>
|
|
1545
|
+
<div class="wa-sub">${_t('it_dc_sub')}</div>
|
|
1546
|
+
</div>
|
|
1547
|
+
</div>
|
|
1548
|
+
<div class="wa-status"><span id="dcDot" class="wa-dot"></span><span id="dcStatText">—</span></div>
|
|
1549
|
+
<div id="dcUser" class="wa-user" hidden></div>
|
|
1550
|
+
<div class="form-grid" style="grid-template-columns:1fr auto;align-items:center">
|
|
1551
|
+
<input id="dcTokenInp" class="inp" type="password" style="margin:6px 0 0" placeholder="${_t('it_dc_token_ph')}" autocomplete="off" />
|
|
1552
|
+
<button id="dcSaveBtn" class="btn" style="margin-top:6px">${_t('it_tg_save')}</button>
|
|
1553
|
+
</div>
|
|
1554
|
+
<div class="wa-actions" style="margin-top:6px">
|
|
1555
|
+
<button id="dcStopBtn" class="btn ghost">${_t('it_disconnect')}</button>
|
|
1556
|
+
</div>
|
|
1557
|
+
<div class="divider"></div>
|
|
1558
|
+
<label class="mem-label" style="margin-top:0">${_t('it_allow_label')} — Discord</label>
|
|
1559
|
+
<div id="dcAllowChips" class="chips-inline"></div>
|
|
1560
|
+
<div class="form-grid" style="grid-template-columns:1.2fr 1fr 1fr auto;align-items:center">
|
|
1561
|
+
<input id="dcAllowNameInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_name_ph')}" autocomplete="off" />
|
|
1562
|
+
<input id="dcAllowIdInp" class="inp" style="margin:6px 0 0" placeholder="${_t('it_dc_id_ph')}" autocomplete="off" />
|
|
1563
|
+
<select id="dcAllowBotSel" class="perm-select" style="margin:6px 0 0;min-width:105px" title="${_t('bot_bind_title')}"></select>
|
|
1564
|
+
<button id="dcAllowAdd" class="btn ghost" style="margin-top:6px">${_t('it_add')}</button>
|
|
1565
|
+
</div>
|
|
1566
|
+
<div class="sub" style="margin-top:8px">${_t('it_dc_note')}</div>
|
|
1538
1567
|
</div>`;
|
|
1539
1568
|
|
|
1540
1569
|
const waGroupsOn = $('#waGroupsOn');
|
|
@@ -1597,6 +1626,29 @@ async function renderIntegrationsPane() {
|
|
|
1597
1626
|
tgUI.user = ts.user || null;
|
|
1598
1627
|
updateTgPane();
|
|
1599
1628
|
} catch {}
|
|
1629
|
+
|
|
1630
|
+
/* DISCORD: token + allow list + durum */
|
|
1631
|
+
$('#dcSaveBtn').addEventListener('click', async () => {
|
|
1632
|
+
const tok = $('#dcTokenInp').value.trim();
|
|
1633
|
+
if (!tok) { toast(_t('it_dc_token_ph')); return; }
|
|
1634
|
+
toast(_t('it_dc_connecting'));
|
|
1635
|
+
const r = await beast.dcSetToken(tok).catch((e) => ({ status: 'error', error: String(e) }));
|
|
1636
|
+
$('#dcTokenInp').value = '';
|
|
1637
|
+
updateDcPane();
|
|
1638
|
+
toast(r.status === 'connected' ? 'Discord bağlı: ' + (r.user || '') : r.status === 'error' ? _t('it_dc_token_bad') : _t('it_dc_connecting'));
|
|
1639
|
+
});
|
|
1640
|
+
$('#dcStopBtn').addEventListener('click', async () => {
|
|
1641
|
+
await beast.dcStop().catch(() => {});
|
|
1642
|
+
updateDcPane();
|
|
1643
|
+
toast('Kesildi');
|
|
1644
|
+
});
|
|
1645
|
+
await renderDcAllow();
|
|
1646
|
+
try {
|
|
1647
|
+
const ds = await beast.dcGetStatus();
|
|
1648
|
+
dcUI.status = ds.status || 'disconnected';
|
|
1649
|
+
dcUI.user = ds.user || null;
|
|
1650
|
+
updateDcPane();
|
|
1651
|
+
} catch {}
|
|
1600
1652
|
}
|
|
1601
1653
|
|
|
1602
1654
|
/* ---------------- Olay Merkezi (ayrı sekme) ---------------- */
|
|
@@ -2439,6 +2491,199 @@ async function renderTgAllow() {
|
|
|
2439
2491
|
}
|
|
2440
2492
|
}
|
|
2441
2493
|
|
|
2494
|
+
/* ---------------- Discord izin listesi + durum — TG ile aynı mantık ---------------- */
|
|
2495
|
+
|
|
2496
|
+
const dcUI = { status: 'disconnected', user: null };
|
|
2497
|
+
const DC_STATUS_TEXT = {
|
|
2498
|
+
disconnected: 'Bağlı değil',
|
|
2499
|
+
connecting: 'Bağlanıyor…',
|
|
2500
|
+
connected: 'Bağlı',
|
|
2501
|
+
error: 'Hata — tokenı kontrol et',
|
|
2502
|
+
};
|
|
2503
|
+
|
|
2504
|
+
function onDcEvent(ev) {
|
|
2505
|
+
if (ev.type !== 'status') return;
|
|
2506
|
+
dcUI.status = ev.status;
|
|
2507
|
+
if (ev.user) dcUI.user = ev.user;
|
|
2508
|
+
updateDcPane();
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function updateDcPane() {
|
|
2512
|
+
const pane = $('#tab-integrations');
|
|
2513
|
+
const dot = pane && pane.querySelector('#dcDot');
|
|
2514
|
+
if (!dot) return;
|
|
2515
|
+
dot.className = 'wa-dot' + (dcUI.status === 'connected' ? ' on' : dcUI.status === 'error' ? ' qr' : '');
|
|
2516
|
+
pane.querySelector('#dcStatText').textContent = DC_STATUS_TEXT[dcUI.status] || dcUI.status;
|
|
2517
|
+
const u = pane.querySelector('#dcUser');
|
|
2518
|
+
u.hidden = !(dcUI.status === 'connected' && dcUI.user);
|
|
2519
|
+
u.textContent = dcUI.user ? '🤖 ' + dcUI.user : '';
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
async function renderDcAllow() {
|
|
2523
|
+
const wrap = $('#dcAllowChips');
|
|
2524
|
+
if (!wrap) return;
|
|
2525
|
+
const list = await beast.dcGetAllow();
|
|
2526
|
+
let botChoices = [];
|
|
2527
|
+
try { botChoices = (await beast.botsList()) || []; } catch {}
|
|
2528
|
+
const dcLabel = (e) => {
|
|
2529
|
+
if (e === '*') return '* herkes';
|
|
2530
|
+
if (typeof e === 'string') return e;
|
|
2531
|
+
const name = String((e && e.name) || '').trim();
|
|
2532
|
+
const id = String((e && e.id) || '');
|
|
2533
|
+
const bot = e && e.bot_id ? botChoices.find((bb) => bb.id === e.bot_id) : null;
|
|
2534
|
+
return (name ? name + ' ' : '') + id + (bot ? ' → ' + bot.name : '');
|
|
2535
|
+
};
|
|
2536
|
+
wrap.innerHTML = '';
|
|
2537
|
+
if (!list.length) wrap.innerHTML = '<span class="sub">— boş —</span>';
|
|
2538
|
+
list.forEach((entry, idx) => {
|
|
2539
|
+
const curPerm = (typeof entry === 'object' && entry.perm) || (entry && entry.lockdown ? 'chat' : 'all');
|
|
2540
|
+
const c = document.createElement('span');
|
|
2541
|
+
c.className = 'chip';
|
|
2542
|
+
c.style.margin = '0 6px 6px 0';
|
|
2543
|
+
const txt = document.createElement('span');
|
|
2544
|
+
txt.className = 'chip-txt';
|
|
2545
|
+
txt.textContent = dcLabel(entry);
|
|
2546
|
+
c.appendChild(txt);
|
|
2547
|
+
if (entry !== '*') {
|
|
2548
|
+
const selP = document.createElement('select');
|
|
2549
|
+
selP.className = 'perm-select';
|
|
2550
|
+
selP.title = _t('wa_perm_title');
|
|
2551
|
+
const PERMS = [
|
|
2552
|
+
['all', _t('wa_perm_all')],
|
|
2553
|
+
['web', 'web'],
|
|
2554
|
+
['read', _t('wa_perm_read')],
|
|
2555
|
+
['chat', _t('wa_perm_chat')],
|
|
2556
|
+
];
|
|
2557
|
+
for (const [v, lbl] of PERMS) {
|
|
2558
|
+
const o = document.createElement('option');
|
|
2559
|
+
o.value = v;
|
|
2560
|
+
o.textContent = lbl;
|
|
2561
|
+
if (curPerm === v) o.selected = true;
|
|
2562
|
+
selP.appendChild(o);
|
|
2563
|
+
}
|
|
2564
|
+
selP.addEventListener('change', async () => {
|
|
2565
|
+
const cur = await beast.dcGetAllow();
|
|
2566
|
+
const next = cur.map((e, i) => {
|
|
2567
|
+
if (i !== idx) return e;
|
|
2568
|
+
const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
|
|
2569
|
+
base.lockdown = selP.value === 'chat';
|
|
2570
|
+
base.perm = selP.value;
|
|
2571
|
+
return base;
|
|
2572
|
+
});
|
|
2573
|
+
await beast.dcSetAllow(next);
|
|
2574
|
+
renderDcAllow();
|
|
2575
|
+
toast(dcLabel(entry) + ': ' + selP.selectedOptions[0].textContent);
|
|
2576
|
+
});
|
|
2577
|
+
c.appendChild(selP);
|
|
2578
|
+
|
|
2579
|
+
if (botChoices.length) {
|
|
2580
|
+
const sel = document.createElement('select');
|
|
2581
|
+
sel.className = 'perm-select';
|
|
2582
|
+
sel.title = _t('bot_bind_title');
|
|
2583
|
+
const empty = document.createElement('option');
|
|
2584
|
+
empty.value = '';
|
|
2585
|
+
empty.textContent = _t('bot_sel_empty');
|
|
2586
|
+
sel.appendChild(empty);
|
|
2587
|
+
for (const bb of botChoices) {
|
|
2588
|
+
const o = document.createElement('option');
|
|
2589
|
+
o.value = bb.id;
|
|
2590
|
+
o.textContent = `${bb.icon} ${bb.name}`;
|
|
2591
|
+
if ((typeof entry === 'object' && entry.bot_id) === bb.id) o.selected = true;
|
|
2592
|
+
sel.appendChild(o);
|
|
2593
|
+
}
|
|
2594
|
+
sel.addEventListener('change', async () => {
|
|
2595
|
+
const cur = await beast.dcGetAllow();
|
|
2596
|
+
const next = cur.map((e, i) => {
|
|
2597
|
+
if (i !== idx) return e;
|
|
2598
|
+
const base = typeof e === 'string' ? { id: e, name: '' } : { ...e };
|
|
2599
|
+
base.bot_id = sel.value || undefined;
|
|
2600
|
+
return base;
|
|
2601
|
+
});
|
|
2602
|
+
await beast.dcSetAllow(next);
|
|
2603
|
+
renderDcAllow();
|
|
2604
|
+
const bt = botChoices.find((bb) => bb.id === sel.value);
|
|
2605
|
+
toast(dcLabel(entry) + ' → ' + (bt ? bt.name : '?'));
|
|
2606
|
+
});
|
|
2607
|
+
c.appendChild(sel);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
const isOwnerEntry = typeof entry === 'object' && !!entry.owner;
|
|
2611
|
+
const ownerBtn = document.createElement('span');
|
|
2612
|
+
ownerBtn.className = 'lk';
|
|
2613
|
+
ownerBtn.style.cssText = 'font-size:11px;padding:1px 6px;' + (isOwnerEntry ? 'color:#c9a227;font-weight:800' : '');
|
|
2614
|
+
ownerBtn.title = isOwnerEntry ? _t('wa_owner_title_on') : _t('wa_owner_title_off');
|
|
2615
|
+
ownerBtn.textContent = isOwnerEntry ? _t('wa_owner_on') : _t('wa_owner_off');
|
|
2616
|
+
ownerBtn.addEventListener('click', async () => {
|
|
2617
|
+
if (isOwnerEntry) return;
|
|
2618
|
+
const cur = await beast.dcGetAllow();
|
|
2619
|
+
const next = cur.map((e, i) => {
|
|
2620
|
+
if (typeof e === 'object' && e && e.owner) return { ...e, owner: false };
|
|
2621
|
+
if (i === idx) {
|
|
2622
|
+
const obj = typeof e === 'string' ? { id: e, name: '' } : { ...e };
|
|
2623
|
+
obj.owner = true;
|
|
2624
|
+
return obj;
|
|
2625
|
+
}
|
|
2626
|
+
return e;
|
|
2627
|
+
});
|
|
2628
|
+
const tgt = next[idx];
|
|
2629
|
+
await beast.dcSetAllow(next);
|
|
2630
|
+
renderDcAllow();
|
|
2631
|
+
toast('Sahip: ' + dcLabel(tgt));
|
|
2632
|
+
});
|
|
2633
|
+
c.appendChild(ownerBtn);
|
|
2634
|
+
}
|
|
2635
|
+
const x = document.createElement('span');
|
|
2636
|
+
x.className = 'x';
|
|
2637
|
+
x.textContent = '×';
|
|
2638
|
+
x.addEventListener('click', async () => {
|
|
2639
|
+
const next = (await beast.dcGetAllow()).filter((_v, i) => i !== idx);
|
|
2640
|
+
await beast.dcSetAllow(next);
|
|
2641
|
+
renderDcAllow();
|
|
2642
|
+
toast('Kaldırıldı: ' + dcLabel(entry));
|
|
2643
|
+
});
|
|
2644
|
+
c.appendChild(x);
|
|
2645
|
+
wrap.appendChild(c);
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
const inp = $('#dcAllowIdInp');
|
|
2649
|
+
const nameInp = $('#dcAllowNameInp');
|
|
2650
|
+
const add = $('#dcAllowAdd');
|
|
2651
|
+
const botSel = $('#dcAllowBotSel');
|
|
2652
|
+
if (botSel) {
|
|
2653
|
+
const prev = botSel.value;
|
|
2654
|
+
botSel.innerHTML =
|
|
2655
|
+
`<option value="">${_t('bot_sel_empty')}</option>` +
|
|
2656
|
+
botChoices
|
|
2657
|
+
.map((bb) => `<option value="${bb.id}">${bb.icon} ${escapeHtml(bb.name)}${bb.admin ? ' (admin)' : ''}</option>`)
|
|
2658
|
+
.join('');
|
|
2659
|
+
if (prev) botSel.value = prev;
|
|
2660
|
+
}
|
|
2661
|
+
if (!add.dataset.bound) {
|
|
2662
|
+
add.dataset.bound = '1';
|
|
2663
|
+
const addEntry = async () => {
|
|
2664
|
+
let v = inp.value.trim();
|
|
2665
|
+
if (!v) return;
|
|
2666
|
+
const name = nameInp.value.trim().slice(0, 40);
|
|
2667
|
+
if (!name) { toast('İsim zorunlu — kimin yazdığını bilmek için'); nameInp.focus(); return; }
|
|
2668
|
+
v = v.startsWith('@') ? '@' + v.slice(1).replace(/[^\w]/g, '') : v.replace(/[^\d]/g, '');
|
|
2669
|
+
if (!v) { toast(_t('it_dc_id_ph')); return; }
|
|
2670
|
+
const botId = botSel && botSel.value ? { bot_id: botSel.value } : {};
|
|
2671
|
+
const next = [...(await beast.dcGetAllow()), { id: v, name, ...botId }];
|
|
2672
|
+
if (!next.some((e) => e && typeof e === 'object' && e.owner)) {
|
|
2673
|
+
if (next.length && typeof next[0] === 'object') next[0].owner = true;
|
|
2674
|
+
}
|
|
2675
|
+
await beast.dcSetAllow(next);
|
|
2676
|
+
inp.value = '';
|
|
2677
|
+
nameInp.value = '';
|
|
2678
|
+
renderDcAllow();
|
|
2679
|
+
toast('Eklendi: ' + name);
|
|
2680
|
+
};
|
|
2681
|
+
add.addEventListener('click', addEntry);
|
|
2682
|
+
inp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
|
|
2683
|
+
nameInp.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addEntry(); } });
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2442
2687
|
/* ---------------- BOT SİSTEMİ (BÖLÜM 2-3-4) ----------------
|
|
2443
2688
|
Sol panel altında bot kartları; her bot için sohbet geçmişi + sekmeli yönetim.
|
|
2444
2689
|
İlk bot hep Beast (admin, silinemez). Max 5 bot. */
|
|
@@ -2767,6 +3012,15 @@ function renderBotSettings(pane, b) {
|
|
|
2767
3012
|
const numRows = (b.numbers || [])
|
|
2768
3013
|
.map((n) => `<div class="bot-num-row" data-num="${n.num}"><span class="n">+${escapeHtml(n.num)}${n.name ? ' · ' + escapeHtml(n.name) : ''}</span><span class="x" title="${_t('bot_num_del')}">×</span></div>`)
|
|
2769
3014
|
.join('');
|
|
3015
|
+
/* bot bazlı model seçenekleri: ana picker'daki listeyle aynı (state.models) */
|
|
3016
|
+
const modelOpts = ['<option value="">' + escapeHtml(_t('bot_model_global')) + '</option>']
|
|
3017
|
+
.concat(
|
|
3018
|
+
(state.models || []).map((m) => {
|
|
3019
|
+
const s = m.sel || (m.providerId || '') + '::' + (m.model || '');
|
|
3020
|
+
return `<option value="${escapeHtml(s)}" ${b.model === s ? 'selected' : ''}>${escapeHtml(m.providerName || m.providerId || '')} · ${escapeHtml(m.model || '')}</option>`;
|
|
3021
|
+
})
|
|
3022
|
+
)
|
|
3023
|
+
.join('');
|
|
2770
3024
|
/* Numara ekleme KALDIRILDI — girişler yalnız Ayarlar → Entegrasyonlar (WhatsApp
|
|
2771
3025
|
izin listesi) üzerinden yapılır. Bu bölüm salt-okunur bağlı-numara listesidir. */
|
|
2772
3026
|
f.innerHTML = `
|
|
@@ -2789,7 +3043,10 @@ function renderBotSettings(pane, b) {
|
|
|
2789
3043
|
<div class="bot-checks" id="bPerm">${permChecks}</div>
|
|
2790
3044
|
<div class="sub" style="margin-top:4px">${_t('bot_perm_note')}</div>
|
|
2791
3045
|
<label class="mem-label">${_t('bot_skills')}</label>
|
|
2792
|
-
<div class="bot-checks" id="bSkills">${skillChecks}</div
|
|
3046
|
+
<div class="bot-checks" id="bSkills">${skillChecks}</div>
|
|
3047
|
+
<label class="mem-label">${_t('bot_model')}</label>
|
|
3048
|
+
<select id="bModel" class="inp">${modelOpts}</select>
|
|
3049
|
+
<div class="sub" style="margin-top:4px">${_t('bot_model_note')}</div>`}
|
|
2793
3050
|
<label class="mem-label">${_t('bot_browser')}</label>
|
|
2794
3051
|
<div class="bot-checks" style="margin-bottom:6px">
|
|
2795
3052
|
<label><input type="checkbox" id="bExtBrowser" ${b.extBrowser ? 'checked' : ''}/> ${_t('bot_ext_browser')}</label>
|
|
@@ -2864,6 +3121,8 @@ function renderBotSettings(pane, b) {
|
|
|
2864
3121
|
const sk = {};
|
|
2865
3122
|
f.querySelectorAll('#bSkills input').forEach((c) => { sk[c.dataset.skill] = c.checked; });
|
|
2866
3123
|
patch.skills = sk;
|
|
3124
|
+
const bm = $('#bModel');
|
|
3125
|
+
if (bm) patch.model = bm.value;
|
|
2867
3126
|
}
|
|
2868
3127
|
const r = await beast.botsUpdate(b.id, patch);
|
|
2869
3128
|
if (r.ok) { toast(_t('bot_saved')); await refreshBots(); renderBotPage(); }
|
|
@@ -3087,6 +3346,39 @@ function fmtAgo(iso) {
|
|
|
3087
3346
|
return s >= 60 ? Math.floor(s / 60) + 'dk' : s + 'sn';
|
|
3088
3347
|
}
|
|
3089
3348
|
|
|
3349
|
+
/* canlı süre sayacı: 0:43 · 2:05 · 1:02:15 biçimi (koşan ajan kartlarında) */
|
|
3350
|
+
function fmtElapsed(ms) {
|
|
3351
|
+
const s = Math.max(0, Math.floor(ms / 1000));
|
|
3352
|
+
const h = Math.floor(s / 3600);
|
|
3353
|
+
const m = Math.floor((s % 3600) / 60);
|
|
3354
|
+
const sec = s % 60;
|
|
3355
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
3356
|
+
return h > 0 ? `${h}:${p(m)}:${p(sec)}` : `${m}:${p(sec)}`;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
/* koşan ajanların ⏱ sayaçları — saniyede bir yalnızca sayaç span'leri güncellenir
|
|
3360
|
+
(tüm rail yeniden çizilmez). Koşan kart kalmayınca interval kapanır. */
|
|
3361
|
+
let agentTimerInt = null;
|
|
3362
|
+
function ensureAgentTimers() {
|
|
3363
|
+
const any = !!document.querySelector('.agent-timer[data-start]');
|
|
3364
|
+
if (any && !agentTimerInt) {
|
|
3365
|
+
agentTimerInt = setInterval(() => {
|
|
3366
|
+
document.querySelectorAll('.agent-timer[data-start]').forEach((el) => {
|
|
3367
|
+
const t0 = Date.parse(el.dataset.start || '');
|
|
3368
|
+
if (t0) el.textContent = fmtElapsed(Date.now() - t0);
|
|
3369
|
+
});
|
|
3370
|
+
}, 1000);
|
|
3371
|
+
} else if (!any && agentTimerInt) {
|
|
3372
|
+
clearInterval(agentTimerInt);
|
|
3373
|
+
agentTimerInt = null;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
function agentTimerHtml(startedAt) {
|
|
3377
|
+
const t0 = Date.parse(startedAt || '');
|
|
3378
|
+
const val = t0 ? fmtElapsed(Date.now() - t0) : '0:00';
|
|
3379
|
+
return `<span class="agent-timer" data-start="${escapeHtml(startedAt || '')}">⏱ ${val}</span>`;
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3090
3382
|
/* sağ panel — paralel ajanların arka plan işleri */
|
|
3091
3383
|
function renderAgentRail() {
|
|
3092
3384
|
const list = els.railList;
|
|
@@ -3117,7 +3409,7 @@ function renderAgentRail() {
|
|
|
3117
3409
|
`<span class="ag-dot"></span>` +
|
|
3118
3410
|
`<span class="sess-title">${escapeHtml(j.title)}</span>` +
|
|
3119
3411
|
(j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
|
|
3120
|
-
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
|
|
3412
|
+
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
|
|
3121
3413
|
(j.status === 'running'
|
|
3122
3414
|
? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
|
|
3123
3415
|
: `<button class="rj-cancel" title="${_t('ag_delete')}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
|
|
@@ -3162,6 +3454,7 @@ function renderAgentRail() {
|
|
|
3162
3454
|
/* açık sohbet dökümünü re-render sonrası GERİ GETİR (kapanma bug'ının fix'i) */
|
|
3163
3455
|
if (open && agentState.chatOpen.has(j.id)) maybeLoadRailChat(j.id);
|
|
3164
3456
|
}
|
|
3457
|
+
ensureAgentTimers();
|
|
3165
3458
|
}
|
|
3166
3459
|
|
|
3167
3460
|
/* re-render storm'da IPC fırtınası olmasın: döküm 2 sn'de bir tazelenir */
|
|
@@ -3247,7 +3540,7 @@ function renderAgentsPane() {
|
|
|
3247
3540
|
`<span class="ag-dot"></span>` +
|
|
3248
3541
|
`<span class="ag-title">${escapeHtml(j.title)}</span>` +
|
|
3249
3542
|
`<span class="ag-st">${agStText(j.status) || j.status}</span>` +
|
|
3250
|
-
`<span class="ag-time">${when}${j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
|
|
3543
|
+
`<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
|
|
3251
3544
|
(j.status === 'running'
|
|
3252
3545
|
? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
|
|
3253
3546
|
: ``) +
|
|
@@ -3285,16 +3578,8 @@ function renderAgentsPane() {
|
|
|
3285
3578
|
list.appendChild(card);
|
|
3286
3579
|
}
|
|
3287
3580
|
|
|
3288
|
-
/* canlı süre sayacı */
|
|
3289
|
-
|
|
3290
|
-
agentState.tick = setInterval(() => {
|
|
3291
|
-
if (setTab !== 'agents') { clearInterval(agentState.tick); return; }
|
|
3292
|
-
for (const el of document.querySelectorAll('.agent-card.st-running .ag-time')) {
|
|
3293
|
-
/* sadece saati yenile */
|
|
3294
|
-
const t = el.textContent.split(' ')[0];
|
|
3295
|
-
el.textContent = `${t} · ${_t('ag_working')}…`;
|
|
3296
|
-
}
|
|
3297
|
-
}, 5000);
|
|
3581
|
+
/* canlı süre sayacı — saniyede bir yalnız ⏱ sayaç span'leri güncellenir */
|
|
3582
|
+
ensureAgentTimers();
|
|
3298
3583
|
}
|
|
3299
3584
|
|
|
3300
3585
|
/* ---------------- events from engine ---------------- */
|
|
@@ -4262,6 +4547,7 @@ async function init() {
|
|
|
4262
4547
|
|
|
4263
4548
|
beast.onWaEvent(onWaEvent);
|
|
4264
4549
|
beast.onTgEvent(onTgEvent);
|
|
4550
|
+
beast.onDcEvent(onDcEvent);
|
|
4265
4551
|
|
|
4266
4552
|
els.gearBtn.addEventListener('click', openSettings);
|
|
4267
4553
|
els.setClose.addEventListener('click', closeSettings);
|
package/tests/bg-jobs.test.js
CHANGED
|
@@ -121,18 +121,14 @@ function fakeJob(over) {
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
test('superviseReason: takılan stuck,
|
|
124
|
+
test('superviseReason: takılan stuck, uzun koşan iş CEO kontrolü (long), yeni uyarılan null', () => {
|
|
125
125
|
assert.strictEqual(Engine.superviseReason(fakeJob(), Date.now()), 'stuck');
|
|
126
126
|
|
|
127
|
+
// süre sınırı KALDIRILDI: aktivite süren uzun iş artık kesilmez → CEO ara kontrolü (long)
|
|
127
128
|
assert.strictEqual(
|
|
128
129
|
Engine.superviseReason(fakeJob({ lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
|
|
129
|
-
'wrapup'
|
|
130
|
-
); // aktivite sürüyor ama 10 dk koştu — önce zarif bitirme uyarısı
|
|
131
|
-
|
|
132
|
-
assert.strictEqual(
|
|
133
|
-
Engine.superviseReason(fakeJob({ wrapAt: new Date().toISOString(), lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
|
|
134
130
|
'long'
|
|
135
|
-
);
|
|
131
|
+
);
|
|
136
132
|
|
|
137
133
|
assert.strictEqual(
|
|
138
134
|
Engine.superviseReason(fakeJob({ startedAt: new Date(Date.now() - 1 * MIN).toISOString() }), Date.now()),
|
|
@@ -182,12 +178,11 @@ test('_supervise önce ajana ÖZ-KURTARMA verir, haklar bitince CEO\'ya uyarır'
|
|
|
182
178
|
eng._supervise();
|
|
183
179
|
assert.strictEqual(job.checks, 1);
|
|
184
180
|
|
|
185
|
-
/* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (
|
|
186
|
-
|
|
181
|
+
/* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (aktivite sürüyor,
|
|
182
|
+
süre sınırı yok) → CEO uyarısı ('long') */
|
|
187
183
|
job.status = 'running';
|
|
188
184
|
job.endedAt = null;
|
|
189
185
|
job.fixes = 2;
|
|
190
|
-
job.wrapAt = new Date().toISOString();
|
|
191
186
|
job.lastActivityAt = new Date(Date.now() - 30 * 1000).toISOString();
|
|
192
187
|
job.lastNudgeAt = new Date(Date.now() - 5 * MIN).toISOString();
|
|
193
188
|
eng._supervise();
|