beast-agent 2.44.5 → 2.44.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/engine.js +33 -0
- package/src/agent/watext.js +1 -0
- package/src/main.js +141 -0
- package/src/renderer/index.html +1 -1
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -193,6 +193,7 @@ const CEO_EXEC_TOOLS = new Set([
|
|
|
193
193
|
'browser_click', 'browser_type', 'browser_press', 'browser_scroll', 'browser_select',
|
|
194
194
|
'computer_look', 'computer_act',
|
|
195
195
|
'ocr_read',
|
|
196
|
+
'channel_send', // dış kişilere mesaj: CEO devreder, kendisi atmaz
|
|
196
197
|
'delegate_task', // senkron bekler — CEO hep asenkron run_background kullanır
|
|
197
198
|
]);
|
|
198
199
|
|
|
@@ -343,6 +344,7 @@ class Engine {
|
|
|
343
344
|
this.historyTokenBudget = Number(opts.historyTokenBudget) || HISTORY_TOKEN_BUDGET;
|
|
344
345
|
this.browser = opts.browser || null; // dahili tarayıcı kancaları
|
|
345
346
|
this.fileSend = opts.fileSend || null; // #26 dosya gönderim köprüsü (chat/WA)
|
|
347
|
+
this.channelSend = opts.channelSend || null; // kanal ilk mesaj köprüsü (WA/TG/DC)
|
|
346
348
|
this.notifyOwnerFail = opts.notifyOwnerFail !== false; // #25 hata mail bildirimi (runtime /notify)
|
|
347
349
|
/* ENTEGRASYON BİLDİRİMİ: tool yazım/doğrulama adımları WA/TG/Discord'a düşer */
|
|
348
350
|
this.integrationNotify = typeof opts.integrationNotify === 'function' ? opts.integrationNotify : null;
|
|
@@ -2124,6 +2126,7 @@ class Engine {
|
|
|
2124
2126
|
'Basit soruları araç kullanmadan doğrudan cevapla — hız önceliklidir.',
|
|
2125
2127
|
'Kod/dosya işlerinde: içerik araması grep (regex), dosya adı araması glob, VAR OLAN dosyayı değiştirme edit_file (write_file yalnız yeni dosya/tam yeniden yazım). read_file satır numaralı döner; büyük dosyada devamını offset parametresiyle oku, ASLA baştan okuma; bir dosyayı aynı oturumda BİR KEZ okumak yeter — okunan içerik oturum sonuna kadar bağlamda kalır, dosyayı tekrar okuma. Dosya işlemlerinde özel araçları kullan (edit_file/write_file/read_file/grep/glob); run_command terminal işlerindir (build, git, kurulum, paket) — dosya düzenlemeyi komutla değil edit_file ile yap. edit_file/write_file sonucu additions/deletions döner ve değişiklik diske ANINDA uygulanır — doğrulamak için dosyayı TEKRAR OKUMA; önceki okuduğun içerik + kendi değişikliklerin üzerinden devam et.',
|
|
2126
2128
|
'Kullanıcı bir tarihte/saatte hatırlatılmasını isterse set_reminder kullan; when değerini ORTAMdaki bugüne göre hesapla (yerel saat). "Her sabah/gün/hafta" gibi tekrarlı isteklerde repeat alanını da ver (daily/weekly/monthly/weekdays veya cron).',
|
|
2129
|
+
'İLK MESAJ (channel_send): kullanıcı "X kişiye yaz / haber ver / duyur" derse channel_send ile SEN ilk mesajı at — karşıdan mesaj gelmesini BEKLEME. Yalnız allow listteki kişiler hedeflenebilir: isim (kısmi), telefon numarası, Telegram/Discord ID, @kullanıcı adı, "sahip" ya da "all". Kişi listede yoksa gönderme, kullanıcıya söyle. Telegram/Discord\'da karşı taraf bota daha önce hiç yazmadıysa gönderim başarısız olabilir; sonucu dürüstçe bildir.',
|
|
2127
2130
|
'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.',
|
|
2128
2131
|
'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.',
|
|
2129
2132
|
'ARAÇ EKSİKSE ÇEKİNME (CEPHANE): ihtiyacın olan bir araç yoksa/bozuksa tool_request ile TOOL botuna yazdır — tool yazımı onun TEK işidir; task alanına araç adı + ne yapacağı + girdi/çıktı sözleşmesini, context alanına örnek veri/yol/endpoint yaz. ASENKRON: istek anında kabul edilir, Tool botu kendi oturumunda arka planda yazar; sen BEKLEME, işine devam et. Araç doğrulanınca tool__<ad> olarak ANINDA çağrılabilir; rapor bitince sohbete otomatik düşer. Küçük kişisel araçları skill("tool-yazma") prosedürüyle kendin de yazabilirsin — yazdığını run_command ile MUTLAKA doğrula.',
|
|
@@ -5036,6 +5039,7 @@ const skills = require('./skills');
|
|
|
5036
5039
|
(t) =>
|
|
5037
5040
|
t.function.name !== 'delegate_task' &&
|
|
5038
5041
|
t.function.name !== 'set_reminder' &&
|
|
5042
|
+
t.function.name !== 'channel_send' &&
|
|
5039
5043
|
!t.function.name.startsWith('email_')
|
|
5040
5044
|
).sort((a, b) => String(a.function.name).localeCompare(String(b.function.name))); // önek-cache: sabit sıra
|
|
5041
5045
|
system =
|
|
@@ -5557,6 +5561,18 @@ const skills = require('./skills');
|
|
|
5557
5561
|
const r = await this.fileSend(sessionId, p, String((args && args.caption) || ''));
|
|
5558
5562
|
return JSON.stringify(r);
|
|
5559
5563
|
}
|
|
5564
|
+
if (name === 'channel_send') {
|
|
5565
|
+
if (typeof this.channelSend !== 'function') {
|
|
5566
|
+
return JSON.stringify({ ok: false, error: 'kanal gönderim köprüsü yok' });
|
|
5567
|
+
}
|
|
5568
|
+
const r = await this.channelSend({
|
|
5569
|
+
channel: String((args && args.channel) || 'auto'),
|
|
5570
|
+
to: String((args && args.to) || ''),
|
|
5571
|
+
text: String((args && args.text) || ''),
|
|
5572
|
+
sessionId: String(sessionId || ''),
|
|
5573
|
+
});
|
|
5574
|
+
return JSON.stringify(r);
|
|
5575
|
+
}
|
|
5560
5576
|
if (name === 'pdf_write') {
|
|
5561
5577
|
const r = await pdfwrite.write({
|
|
5562
5578
|
outPath: String((args && args.path) || ''),
|
|
@@ -6349,6 +6365,23 @@ const TOOLS = [
|
|
|
6349
6365
|
},
|
|
6350
6366
|
},
|
|
6351
6367
|
},
|
|
6368
|
+
{
|
|
6369
|
+
type: 'function',
|
|
6370
|
+
function: {
|
|
6371
|
+
name: 'channel_send',
|
|
6372
|
+
description:
|
|
6373
|
+
"Send a message FROM YOU to an allow-listed person on WhatsApp/Telegram/Discord — the FIRST message, without waiting for them to write first. Use when the user says 'write to X', 'let X know', 'announce to the list'. channel: whatsapp|telegram|discord|auto (default auto = every connected channel where the target matches). to: allow-list name (partial), phone number, Telegram/Discord numeric id, @username, 'owner'/'sahip' for the owner, or 'all' for everyone on the list. text: the message (max ~4000 chars). Only allow-listed people can be targeted; if the name is not on the list the send is refused. Telegram/Discord can only deliver if that person has already started/permitted the bot; report failures honestly.",
|
|
6374
|
+
parameters: {
|
|
6375
|
+
type: 'object',
|
|
6376
|
+
properties: {
|
|
6377
|
+
channel: { type: 'string', enum: ['auto', 'whatsapp', 'telegram', 'discord'], description: 'Channel (default auto)' },
|
|
6378
|
+
to: { type: 'string', description: "Allow-list name, number, id, @username, 'owner'/'sahip' or 'all'" },
|
|
6379
|
+
text: { type: 'string', description: 'Message text to send' },
|
|
6380
|
+
},
|
|
6381
|
+
required: ['to', 'text'],
|
|
6382
|
+
},
|
|
6383
|
+
},
|
|
6384
|
+
},
|
|
6352
6385
|
{
|
|
6353
6386
|
type: 'function',
|
|
6354
6387
|
function: {
|
package/src/agent/watext.js
CHANGED
|
@@ -58,6 +58,7 @@ const TOOL_LABELS = {
|
|
|
58
58
|
todo_write: (a) => `Görev listesi (${(a.todos || []).length})`,
|
|
59
59
|
delegate_task: (a) => `Alt görev: ${String(a.task || '').slice(0, 80)}`,
|
|
60
60
|
set_reminder: (a) => `Hatırlatma${a.repeat ? ' [' + a.repeat + ']' : ''}: ${String(a.message || '').slice(0, 60)}`,
|
|
61
|
+
channel_send: (a) => `Kanal mesajı → ${a.to || ''}${a.channel && a.channel !== 'auto' ? ' (' + a.channel + ')' : ''}: ${String(a.text || '').slice(0, 60)}`,
|
|
61
62
|
watcher_add: (a) => `İzleyici kur: ${a.name || ''}`,
|
|
62
63
|
watcher_list: () => 'İzleyiciler',
|
|
63
64
|
watcher_remove: (a) => `İzleyici sil: ${a.id || ''}`,
|
package/src/main.js
CHANGED
|
@@ -2271,6 +2271,7 @@ function botToolSet(cfg) {
|
|
|
2271
2271
|
'event_list', 'event_subscribe', 'event_unsubscribe',
|
|
2272
2272
|
'watcher_add', 'watcher_list', 'watcher_remove',
|
|
2273
2273
|
'tool_request', /* CEPHANE: eksik aracı TOOL botuna yazdırma hakkı HER botta */
|
|
2274
|
+
'channel_send', /* İLK MESAJ: allow listteki kişilere kendiliğinden yazma hakkı HER botta */
|
|
2274
2275
|
]);
|
|
2275
2276
|
if (s.web_search) { set.add('web_search'); set.add('http_fetch'); set.add('webfetch'); set.add('deep_search'); }
|
|
2276
2277
|
if (s.browser) {
|
|
@@ -3227,6 +3228,7 @@ function reloadBackend() {
|
|
|
3227
3228
|
/* tool yazım/doğrulama adımları → bağlı entegrasyonlara (WA/TG/Discord) */
|
|
3228
3229
|
integrationNotify: integrationBroadcast,
|
|
3229
3230
|
fileSend: deliverFile,
|
|
3231
|
+
channelSend: (args) => channelSendFromAgent(args),
|
|
3230
3232
|
reminders: { add: scheduleReminder },
|
|
3231
3233
|
watchers: {
|
|
3232
3234
|
list: () => watchers.list(),
|
|
@@ -8964,6 +8966,145 @@ function integrationBroadcast(text) {
|
|
|
8964
8966
|
} catch {}
|
|
8965
8967
|
}
|
|
8966
8968
|
|
|
8969
|
+
/* ---------- AJAN İLK MESAJI (channel_send) ----------
|
|
8970
|
+
Allow listteki kişilere WhatsApp/Telegram/Discord'dan SEN İLK MESAJI atar;
|
|
8971
|
+
karşıdan mesaj gelmesini beklemez. Güvenlik: yalnız allow listteki kişiler
|
|
8972
|
+
hedeflenir; müşteri botu (non-admin) yalnız KENDİ bot_id'sine bağlı
|
|
8973
|
+
kişilere yazabilir. */
|
|
8974
|
+
async function channelSendFromAgent(input) {
|
|
8975
|
+
const a = input || {};
|
|
8976
|
+
const body = stripAiDashes(String(a.text || '').trim()).slice(0, 4000);
|
|
8977
|
+
if (!body) return { ok: false, error: 'text gerekli' };
|
|
8978
|
+
const want = String(a.channel || 'auto').toLowerCase();
|
|
8979
|
+
const q = String(a.to || '').trim();
|
|
8980
|
+
if (!q) return { ok: false, error: 'to gerekli (isim, numara, ID, @kullanıcı adı, "sahip" ya da "all")' };
|
|
8981
|
+
const ql = q.toLowerCase();
|
|
8982
|
+
const isAll = ['all', 'herkes', 'hepsi', 'tümü', 'tumu', 'everyone'].includes(ql);
|
|
8983
|
+
const isOwner = ['sahip', 'owner', 'patron'].includes(ql);
|
|
8984
|
+
|
|
8985
|
+
/* oturumun botu: müşteri botuysa hedef kısıtı (izolasyon) */
|
|
8986
|
+
let botId = '';
|
|
8987
|
+
try {
|
|
8988
|
+
const s = engine.cache.get(String(a.sessionId || '')) || engine._load(String(a.sessionId || ''));
|
|
8989
|
+
botId = String((s && s.botId) || '');
|
|
8990
|
+
} catch {}
|
|
8991
|
+
const bot = botId ? bots.get(botId) : null;
|
|
8992
|
+
const unrestricted = !bot || !!bot.admin;
|
|
8993
|
+
const botOk = (e) => unrestricted || String((e && typeof e === 'object' && e.bot_id) || '') === botId;
|
|
8994
|
+
|
|
8995
|
+
const targets = [];
|
|
8996
|
+
const notes = [];
|
|
8997
|
+
const known = [];
|
|
8998
|
+
|
|
8999
|
+
/* WHATSAPP */
|
|
9000
|
+
if (want === 'auto' || want === 'whatsapp' || want === 'wa') {
|
|
9001
|
+
if (wa && wa.connected) {
|
|
9002
|
+
const own = waOwnerNum();
|
|
9003
|
+
const qd = q.replace(/\D/g, '');
|
|
9004
|
+
for (const e of settings.waAllow || []) {
|
|
9005
|
+
if (e === '*') continue;
|
|
9006
|
+
if (!botOk(e)) continue;
|
|
9007
|
+
const pn = waEntryDigits(e);
|
|
9008
|
+
if (!pn) continue;
|
|
9009
|
+
const name = typeof e === 'object' ? String(e.name || '') : '';
|
|
9010
|
+
if (name) known.push(name + ' (whatsapp)');
|
|
9011
|
+
const hit =
|
|
9012
|
+
isAll ||
|
|
9013
|
+
(isOwner && own && pn === own) ||
|
|
9014
|
+
(qd.length >= 6 && (pn === qd || pn.endsWith(qd))) ||
|
|
9015
|
+
(name && name.toLowerCase().includes(ql));
|
|
9016
|
+
if (hit) targets.push({ channel: 'whatsapp', id: pn + '@s.whatsapp.net', label: name || pn });
|
|
9017
|
+
}
|
|
9018
|
+
} else if (want !== 'auto') notes.push('whatsapp bağlı değil');
|
|
9019
|
+
}
|
|
9020
|
+
|
|
9021
|
+
/* TELEGRAM */
|
|
9022
|
+
if (want === 'auto' || want === 'telegram' || want === 'tg') {
|
|
9023
|
+
if (tg && tg.connected) {
|
|
9024
|
+
const owners = tgOwnerIds();
|
|
9025
|
+
for (const e of settings.tgAllow || []) {
|
|
9026
|
+
if (e === '*') continue;
|
|
9027
|
+
if (!botOk(e)) continue;
|
|
9028
|
+
const id = typeof e === 'string' ? e.trim() : String((e && e.id) || '').trim();
|
|
9029
|
+
if (!id || id === '*') continue;
|
|
9030
|
+
const name = typeof e === 'object' ? String(e.name || '') : '';
|
|
9031
|
+
const owner = typeof e === 'object' && !!e.owner;
|
|
9032
|
+
if (name) known.push(name + ' (telegram)');
|
|
9033
|
+
const hit =
|
|
9034
|
+
isAll ||
|
|
9035
|
+
(isOwner && owner && owners.includes(id)) ||
|
|
9036
|
+
id === q ||
|
|
9037
|
+
id.toLowerCase() === ql ||
|
|
9038
|
+
(name && name.toLowerCase().includes(ql));
|
|
9039
|
+
if (hit) targets.push({ channel: 'telegram', id, label: name || id });
|
|
9040
|
+
}
|
|
9041
|
+
} else if (want !== 'auto') notes.push('telegram bağlı değil');
|
|
9042
|
+
}
|
|
9043
|
+
|
|
9044
|
+
/* DISCORD */
|
|
9045
|
+
if (want === 'auto' || want === 'discord' || want === 'dc') {
|
|
9046
|
+
if (dc && dc.connected) {
|
|
9047
|
+
const owners = dcOwnerIds();
|
|
9048
|
+
for (const e of settings.dcAllow || []) {
|
|
9049
|
+
if (e === '*') continue;
|
|
9050
|
+
if (!botOk(e)) continue;
|
|
9051
|
+
const id = typeof e === 'string' ? e.trim() : String((e && e.id) || '').trim();
|
|
9052
|
+
if (!id || id === '*') continue;
|
|
9053
|
+
const name = typeof e === 'object' ? String(e.name || '') : '';
|
|
9054
|
+
const owner = typeof e === 'object' && !!e.owner;
|
|
9055
|
+
if (name) known.push(name + ' (discord)');
|
|
9056
|
+
const hit =
|
|
9057
|
+
isAll ||
|
|
9058
|
+
(isOwner && owner && owners.includes(id)) ||
|
|
9059
|
+
id === q ||
|
|
9060
|
+
id.toLowerCase() === ql ||
|
|
9061
|
+
(name && name.toLowerCase().includes(ql));
|
|
9062
|
+
if (hit) targets.push({ channel: 'discord', id, label: name || id });
|
|
9063
|
+
}
|
|
9064
|
+
} else if (want !== 'auto') notes.push('discord bağlı değil');
|
|
9065
|
+
}
|
|
9066
|
+
|
|
9067
|
+
if (!targets.length) {
|
|
9068
|
+
return {
|
|
9069
|
+
ok: false,
|
|
9070
|
+
error:
|
|
9071
|
+
'allow listte eşleşen hedef yok: "' + q + '"' +
|
|
9072
|
+
(notes.length ? ' (' + notes.join('; ') + ')' : '') +
|
|
9073
|
+
(known.length ? ' — listedekiler: ' + [...new Set(known)].slice(0, 10).join(', ') : ''),
|
|
9074
|
+
};
|
|
9075
|
+
}
|
|
9076
|
+
/* aynı hedefe çift gönderim olmasın */
|
|
9077
|
+
const uniq = [];
|
|
9078
|
+
const seen = new Set();
|
|
9079
|
+
for (const t of targets) {
|
|
9080
|
+
const k = t.channel + '|' + t.id;
|
|
9081
|
+
if (seen.has(k)) continue;
|
|
9082
|
+
seen.add(k);
|
|
9083
|
+
uniq.push(t);
|
|
9084
|
+
}
|
|
9085
|
+
const sent = [];
|
|
9086
|
+
const failed = [];
|
|
9087
|
+
for (const t of uniq) {
|
|
9088
|
+
let ok = false;
|
|
9089
|
+
try {
|
|
9090
|
+
if (t.channel === 'whatsapp') ok = await sendWaSafe(t.id, body);
|
|
9091
|
+
else if (t.channel === 'telegram') ok = await sendTgSafe(t.id, body);
|
|
9092
|
+
else if (t.channel === 'discord') ok = await sendDcSafe(t.id, body);
|
|
9093
|
+
} catch {}
|
|
9094
|
+
if (ok) sent.push({ channel: t.channel, to: t.label });
|
|
9095
|
+
else failed.push({ channel: t.channel, to: t.label, error: 'gönderilemedi (bağlantı yok ya da kişi bota izin vermemiş olabilir)' });
|
|
9096
|
+
}
|
|
9097
|
+
return {
|
|
9098
|
+
ok: sent.length > 0,
|
|
9099
|
+
sent,
|
|
9100
|
+
failed,
|
|
9101
|
+
...(notes.length ? { notes } : {}),
|
|
9102
|
+
note: sent.length
|
|
9103
|
+
? 'İlk mesaj gönderildi: ' + sent.map((x) => x.to + ' (' + x.channel + ')').join(', ')
|
|
9104
|
+
: 'Hiçbir hedefe gönderilemedi' + (notes.length ? ' — ' + notes.join('; ') : ''),
|
|
9105
|
+
};
|
|
9106
|
+
}
|
|
9107
|
+
|
|
8967
9108
|
/* ---- fiyat alarmları (kalıcı) ---- */
|
|
8968
9109
|
function finAlertsLoad() {
|
|
8969
9110
|
const raw = finReadJson(finFile('alerts.json'), []);
|
package/src/renderer/index.html
CHANGED
|
@@ -101,9 +101,9 @@
|
|
|
101
101
|
</div>
|
|
102
102
|
</div>
|
|
103
103
|
<button id="studioBtn" class="dd-btn dd-icon-btn" title="Beast Studio — video yapma/düzenleme" data-i18n-title="tipStudio"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="13" height="12" rx="2"/><path d="M15 10l7-4v12l-7-4"/></svg></button>
|
|
104
|
+
<button id="mcpBtn" class="dd-btn dd-icon-btn" title="Beast MCP — dış araç sunucuları (Model Context Protocol)" data-i18n-title="tipMcp"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><line x1="7" y1="7.5" x2="7.01" y2="7.5"/><line x1="7" y1="16.5" x2="7.01" y2="16.5"/></svg></button>
|
|
104
105
|
<button id="finBtn" class="dd-btn dd-icon-btn" title="Beast Finance — MT5 trade paneli (bilgisayardaki MetaTrader 5 ile çalışır)"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 15l4-5 3 3 5-7"/></svg></button>
|
|
105
106
|
<button id="sbBtn" class="dd-btn dd-icon-btn" title="Beast Sandbox — GitHub reposu indir & içinde çalış" data-i18n-title="tipSandbox"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg></button>
|
|
106
|
-
<button id="mcpBtn" class="dd-btn dd-icon-btn" title="Beast MCP — dış araç sunucuları (Model Context Protocol)" data-i18n-title="tipMcp"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><line x1="7" y1="7.5" x2="7.01" y2="7.5"/><line x1="7" y1="16.5" x2="7.01" y2="16.5"/></svg></button>
|
|
107
107
|
<button id="railBtn" class="dd-btn dd-icon-btn" title="Paralel Ajan Konsolu" data-i18n-title="tipRail">▤</button>
|
|
108
108
|
<button id="dmBtn" class="dd-btn dd-icon-btn" title="Ajan DM — ajanlar arası konuşma" data-i18n-title="tipAgentDm">✉︎</button>
|
|
109
109
|
<button id="watchBtn" class="dd-btn dd-icon-btn" title="İzleyiciler (Watchers)" data-i18n-title="tipWatch">◎</button>
|