beast-agent 0.25.6 → 0.25.8

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/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));
@@ -3470,7 +3723,13 @@ ipcMain.handle('agent:send', (_e, { sessionId, text }) => {
3470
3723
  if (!sess) {
3471
3724
  try { sess = engine._load(sid); } catch {}
3472
3725
  }
3473
- if (sess && !sess.botId) engine.setSessionBot(sid, actBot.id);
3726
+ if (sess && !sess.botId) {
3727
+ engine.setSessionBot(sid, actBot.id);
3728
+ /* tam bağlama: izin + araç seti + botun kendi modeli (sessions:create ile aynı) */
3729
+ engine.setSessionPerm(sid, actBot.perm || 'all');
3730
+ engine.setSessionTools(sid, botToolSet(actBot));
3731
+ engine.setSessionModel(sid, actBot.model || null);
3732
+ }
3474
3733
  }
3475
3734
  } catch {}
3476
3735
  if (t === '/stop' || t === '/start') {
@@ -4062,9 +4321,21 @@ async function netCheck() {
4062
4321
  }
4063
4322
 
4064
4323
  ipcMain.handle('model:set', (_e, sel) => {
4065
- settings.modelOverride = sel;
4066
- saveSettings();
4067
- engine.setModelOverride(sel);
4324
+ /* MÜŞTERİ botu aktifken picker seçimi O BOTUN modelini değiştirir;
4325
+ Beast (admin) aktifken global seçim değişir. */
4326
+ const act = settings.activeBotId ? bots.get(settings.activeBotId) : null;
4327
+ if (act && !act.admin) {
4328
+ try { bots.update(act.id, { model: String(sel || '') }); } catch {}
4329
+ try {
4330
+ for (const v of engine.listSessions()) {
4331
+ if (v.botId === act.id) engine.setSessionModel(v.id, sel || null);
4332
+ }
4333
+ } catch {}
4334
+ } else {
4335
+ settings.modelOverride = sel;
4336
+ saveSettings();
4337
+ engine.setModelOverride(sel);
4338
+ }
4068
4339
  return engine.publicState();
4069
4340
  });
4070
4341
 
@@ -5542,6 +5813,42 @@ ipcMain.handle('tg:allow:set', (_e, list) => {
5542
5813
  });
5543
5814
  ipcMain.handle('tg:sessions', () => [...tgChats.values()]);
5544
5815
 
5816
+ /* ---------- Discord IPC ---------- */
5817
+ ipcMain.handle('dc:status:get', () => {
5818
+ if (!dc) return { configured: !!settings.dcToken, status: 'disconnected', user: null, connected: false };
5819
+ return { configured: true, ...dc.snapshot() };
5820
+ });
5821
+
5822
+ /* token kaydet + köprüyü (yeniden) başlat */
5823
+ ipcMain.handle('dc:set', async (_e, token) => {
5824
+ const t = String(token || '').trim();
5825
+ if (t) settings.dcToken = t;
5826
+ saveSettings();
5827
+ await restartDc();
5828
+ return { configured: !!settings.dcToken, ...(dc ? dc.snapshot() : { status: 'disconnected', user: null }) };
5829
+ });
5830
+
5831
+ ipcMain.handle('dc:start', async () => {
5832
+ if (!settings.dcToken) return { ok: false, error: 'token yok — önce bot tokenı gir' };
5833
+ await restartDc();
5834
+ return { ok: true, ...(dc ? dc.snapshot() : {}) };
5835
+ });
5836
+
5837
+ ipcMain.handle('dc:stop', async () => {
5838
+ if (dc) {
5839
+ try { await dc.stop(); } catch {}
5840
+ }
5841
+ return { ok: true };
5842
+ });
5843
+
5844
+ ipcMain.handle('dc:allow:get', () => settings.dcAllow || []);
5845
+ ipcMain.handle('dc:allow:set', (_e, list) => {
5846
+ settings.dcAllow = Array.isArray(list) ? list : [];
5847
+ saveSettings();
5848
+ return settings.dcAllow;
5849
+ });
5850
+ ipcMain.handle('dc:sessions', () => [...dcChats.values()]);
5851
+
5545
5852
  /* ---------- e-posta IPC ---------- */
5546
5853
 
5547
5854
  ipcMain.handle('email:get', () => {
@@ -5803,6 +6110,7 @@ ipcMain.handle('bots:update', (_e, { id, patch }) => {
5803
6110
  if ((v.botId || 'beast') === String(id || '')) {
5804
6111
  const cfg = bots.get(String(id));
5805
6112
  engine.setSessionTools(v.id, cfg && !cfg.admin ? botToolSet(cfg) : null);
6113
+ engine.setSessionModel(v.id, cfg && !cfg.admin ? (cfg.model || null) : null);
5806
6114
  }
5807
6115
  }
5808
6116
  } 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'),
@@ -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',