beast-agent 0.23.6 → 0.23.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/package.json +1 -1
- package/src/agent/engine.js +105 -12
- package/src/agent/whatsapp.js +3 -0
- package/src/main.js +38 -6
- package/src/renderer/renderer.js +15 -1
- package/src/renderer/style.css +13 -2
package/package.json
CHANGED
package/src/agent/engine.js
CHANGED
|
@@ -834,6 +834,51 @@ class Engine {
|
|
|
834
834
|
} catch {}
|
|
835
835
|
}
|
|
836
836
|
|
|
837
|
+
/* GÖRSEL DÖNGÜSÜ KORUMASI: görüntü desteklemeyen modele görsel ulaşırsa
|
|
838
|
+
sağlayıcı 400/404 döndürür; görsel oturum geçmişinde kaldığı için sonraki
|
|
839
|
+
HER mesaj aynı hatayla ölür (sonsuz hata döngüsü). Bu metod geçmişteki tüm
|
|
840
|
+
image_url parçalarını metin notuyla değiştirir — hem bellekte hem jsonl'de.
|
|
841
|
+
Döndürür: kaldırılan görsel sayısı */
|
|
842
|
+
_sanitizeSessionImages(session) {
|
|
843
|
+
const NOTE = '[görsel gösterilemedi — bu model görüntü girişini desteklemiyor]';
|
|
844
|
+
const hasImg = (c) => Array.isArray(c) && c.some((p) => p && p.type === 'image_url');
|
|
845
|
+
const clean = (content) => {
|
|
846
|
+
const parts = [];
|
|
847
|
+
for (const p of content) {
|
|
848
|
+
if (p && p.type === 'image_url') continue;
|
|
849
|
+
parts.push(p);
|
|
850
|
+
}
|
|
851
|
+
if (!parts.some((p) => p && p.type === 'text' && String(p.text || '').trim())) {
|
|
852
|
+
parts.push({ type: 'text', text: NOTE });
|
|
853
|
+
}
|
|
854
|
+
return parts;
|
|
855
|
+
};
|
|
856
|
+
let removed = 0;
|
|
857
|
+
for (const m of session.messages) {
|
|
858
|
+
if (hasImg(m.content)) {
|
|
859
|
+
removed += m.content.filter((p) => p && p.type === 'image_url').length;
|
|
860
|
+
m.content = clean(m.content);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (!removed) return 0;
|
|
864
|
+
try {
|
|
865
|
+
const file = this._file(session.id);
|
|
866
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
867
|
+
for (let i = 0; i < lines.length; i++) {
|
|
868
|
+
if (!lines[i].trim()) continue;
|
|
869
|
+
let r;
|
|
870
|
+
try { r = JSON.parse(lines[i]); } catch { continue; }
|
|
871
|
+
if (r.t === 'msg' && r.role === 'user' && hasImg(r.content)) {
|
|
872
|
+
lines[i] = JSON.stringify({ t: 'msg', role: 'user', content: clean(r.content) });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const tmp = file + '.tmp';
|
|
876
|
+
fs.writeFileSync(tmp, lines.join('\n'));
|
|
877
|
+
fs.renameSync(tmp, file);
|
|
878
|
+
} catch {}
|
|
879
|
+
return removed;
|
|
880
|
+
}
|
|
881
|
+
|
|
837
882
|
_load(id) {
|
|
838
883
|
if (this.cache.has(id)) return this.cache.get(id);
|
|
839
884
|
const file = this._file(id);
|
|
@@ -969,11 +1014,27 @@ class Engine {
|
|
|
969
1014
|
const firstUser = s.messages.find((m) => m.role === 'user');
|
|
970
1015
|
const isBg = !!s.bgTitle;
|
|
971
1016
|
const job = isBg ? this._bgJobs.get(String(s.id)) : null;
|
|
1017
|
+
/* Başlık: ilk user mesajının METNİ. Ekli mesajlarda content dizidir —
|
|
1018
|
+
String() doğrudan "[object Object]" üretir; text parçaları birleştirilir.
|
|
1019
|
+
WhatsApp/Telegram transport etiketi ([WhatsApp — gönderen: ...]) başlıkta
|
|
1020
|
+
gürültüdür — kırpılır, gerçek konuşma metni kalsın. */
|
|
1021
|
+
let title = '';
|
|
1022
|
+
if (!isBg && firstUser) {
|
|
1023
|
+
let t = Array.isArray(firstUser.content)
|
|
1024
|
+
? firstUser.content.filter((p) => p && p.type === 'text').map((p) => String(p.text || '')).join(' ')
|
|
1025
|
+
: String(firstUser.content || '');
|
|
1026
|
+
t = t
|
|
1027
|
+
.replace(/^\s*\[(WhatsApp|Telegram)[^\]]*\]\s*/i, '')
|
|
1028
|
+
.replace(/\[görsel gösterilemedi[^\]]*\]/gi, '')
|
|
1029
|
+
.replace(/\[(resim eki alındı|belge alındı[^\]]*|sesli mesaj[^\]]*)\]/gi, '')
|
|
1030
|
+
.trim();
|
|
1031
|
+
title = (t.split('\n')[0] || '').replace(/\s+/g, ' ').trim().slice(0, 48);
|
|
1032
|
+
}
|
|
972
1033
|
return {
|
|
973
1034
|
id: s.id,
|
|
974
1035
|
code: s.code || '',
|
|
975
1036
|
botId: s.botId || null,
|
|
976
|
-
title: (isBg ? s.bgTitle :
|
|
1037
|
+
title: (isBg ? s.bgTitle : title) || 'Yeni Sohbet',
|
|
977
1038
|
createdAt: s.createdAt,
|
|
978
1039
|
updatedAt: s.updatedAt,
|
|
979
1040
|
count: s.messages.length,
|
|
@@ -2113,7 +2174,7 @@ class Engine {
|
|
|
2113
2174
|
`\n\n# OTURUM NOTLARI (oturum ${session.code || '?'} — bu oturumun önceki konuşma özeti; birebir geçmişi buradan hatırla)\n` +
|
|
2114
2175
|
session.notes;
|
|
2115
2176
|
}
|
|
2116
|
-
|
|
2177
|
+
let payload = this._buildPayload(system, session.messages, session.notes);
|
|
2117
2178
|
// Vision model sadece mesajda görsel varsa devreye girer; terminal ise prompt'un
|
|
2118
2179
|
// shell/command işi olduğunda. Aksi halde ana model kullanılır.
|
|
2119
2180
|
let role = this._lastUserHasImage(session) ? 'vision' : null;
|
|
@@ -2124,16 +2185,48 @@ class Engine {
|
|
|
2124
2185
|
if (role && this.roleModels[role]) {
|
|
2125
2186
|
emitSafe(this, session.id, { type: 'status', status: `rol: ${role} → ${sel.providerName} · ${sel.model}` });
|
|
2126
2187
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2188
|
+
let res;
|
|
2189
|
+
try {
|
|
2190
|
+
// FALLOUT destekli çağrı: hata olursa zincirdeki sıradaki sağlayıcıya geç
|
|
2191
|
+
res = await this._streamWithFallbacks(
|
|
2192
|
+
session,
|
|
2193
|
+
payload,
|
|
2194
|
+
activeTools,
|
|
2195
|
+
signal,
|
|
2196
|
+
onDelta,
|
|
2197
|
+
sel,
|
|
2198
|
+
role === 'vision'
|
|
2199
|
+
);
|
|
2200
|
+
} catch (e) {
|
|
2201
|
+
/* GÖRSEL DÖNGÜSÜ KORUMASI: görüntü desteklemeyen model hata verirse görsel
|
|
2202
|
+
geçmişte kalır ve sonraki HER mesaj aynı hatayla ölür. Oturumu görsellerden
|
|
2203
|
+
arındırıp turu BİR KEZ görüntüsüz dener. */
|
|
2204
|
+
const msgStr = String((e && e.message) || '');
|
|
2205
|
+
const aborted = signal && signal.aborted;
|
|
2206
|
+
const imgish =
|
|
2207
|
+
!aborted &&
|
|
2208
|
+
/image|vision|multimodal|no endpoints/i.test(msgStr);
|
|
2209
|
+
const hasImg =
|
|
2210
|
+
!aborted &&
|
|
2211
|
+
session.messages.some((m) => Array.isArray(m.content) && m.content.some((p) => p && p.type === 'image_url'));
|
|
2212
|
+
if (!imgish || !hasImg) throw e;
|
|
2213
|
+
const removed = this._sanitizeSessionImages(session);
|
|
2214
|
+
if (!removed) throw e;
|
|
2215
|
+
emitSafe(this, session.id, {
|
|
2216
|
+
type: 'status',
|
|
2217
|
+
status: `\u{1F5BC} bu model görüntü girişini desteklemiyor — sohbetten ${removed} görsel kaldırıldı, mesaj görüntüsüz yanıtlanıyor (/change ile görsel destekli modele geçebilirsin)`,
|
|
2218
|
+
});
|
|
2219
|
+
payload = this._buildPayload(system, session.messages, session.notes);
|
|
2220
|
+
res = await this._streamWithFallbacks(
|
|
2221
|
+
session,
|
|
2222
|
+
payload,
|
|
2223
|
+
activeTools,
|
|
2224
|
+
signal,
|
|
2225
|
+
onDelta,
|
|
2226
|
+
sel,
|
|
2227
|
+
false
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2137
2230
|
// Gerçek kullanım ile tahmini bütçeyi kalibre et
|
|
2138
2231
|
const actual = res.usage && res.usage.prompt_tokens;
|
|
2139
2232
|
if (actual > 0) {
|
package/src/agent/whatsapp.js
CHANGED
|
@@ -411,6 +411,9 @@ class WhatsAppBridge {
|
|
|
411
411
|
media,
|
|
412
412
|
isGroup,
|
|
413
413
|
participant: participantJid,
|
|
414
|
+
/* LID çağında gerçek telefon: participantAlt; varsa WA kullanıcı adı */
|
|
415
|
+
participantPn: isGroup ? String(msg.key.participantAlt || '') : '',
|
|
416
|
+
participantUsername: isGroup ? String(msg.key.participantUsername || '') : '',
|
|
414
417
|
mentioned,
|
|
415
418
|
}, senderNum);
|
|
416
419
|
} catch {}
|
package/src/main.js
CHANGED
|
@@ -1164,15 +1164,32 @@ async function waFlush(jid) {
|
|
|
1164
1164
|
waLog(`flush: çağrıldı jid=${waPrettyJid(jid)} kuyrukta=${q ? q.payloads.length : 0}`);
|
|
1165
1165
|
if (!q) return;
|
|
1166
1166
|
waQueue.delete(jid);
|
|
1167
|
-
const merged = { text: '', media: null, isGroup: false, participant: '', mentioned: false };
|
|
1167
|
+
const merged = { text: '', media: null, isGroup: false, participant: '', participantPn: '', participantUsername: '', mentioned: false };
|
|
1168
1168
|
let senderNum = '';
|
|
1169
|
-
/* push tarafıyla AYNI alan adı: { payload, senderNum }
|
|
1169
|
+
/* push tarafıyla AYNI alan adı: { payload, senderNum }.
|
|
1170
|
+
Gruplarda farklı kişilerden gelen mesajlar birleşirse, ilk kişi label'da
|
|
1171
|
+
adı geçer; DİĞER kişilerin mesajları metne [kim] etiketiyle eklenir. */
|
|
1172
|
+
let firstParticipant = '';
|
|
1170
1173
|
for (const { payload, senderNum: sn } of q.payloads) {
|
|
1171
|
-
if (payload.text)
|
|
1174
|
+
if (payload.text) {
|
|
1175
|
+
let txt = payload.text;
|
|
1176
|
+
if (payload.isGroup && payload.participant) {
|
|
1177
|
+
const pid = String(payload.participant);
|
|
1178
|
+
if (!firstParticipant) firstParticipant = pid;
|
|
1179
|
+
else if (pid !== firstParticipant) {
|
|
1180
|
+
const tagD = String(payload.participantPn || '').split('@')[0].split(':')[0] || pid.split('@')[0].split(':')[0];
|
|
1181
|
+
txt = `[+${tagD}]: ${txt}`;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
merged.text += (merged.text ? '\n' : '') + txt;
|
|
1185
|
+
}
|
|
1172
1186
|
if (payload.media && !merged.media) merged.media = payload.media;
|
|
1173
1187
|
if (payload.isGroup) merged.isGroup = true;
|
|
1174
|
-
|
|
1175
|
-
if (payload.
|
|
1188
|
+
/* ilk gönderen esas alınır — sonrakiler metne [kim] etiketiyle gelir */
|
|
1189
|
+
if (payload.participant && !merged.participant) merged.participant = payload.participant;
|
|
1190
|
+
if (payload.participantPn && !merged.participantPn) merged.participantPn = payload.participantPn;
|
|
1191
|
+
if (payload.participantUsername && !merged.participantUsername) merged.participantUsername = payload.participantUsername;
|
|
1192
|
+
if (payload.mentioned) merged.mentioned = true;
|
|
1176
1193
|
if (!senderNum && sn) senderNum = sn;
|
|
1177
1194
|
}
|
|
1178
1195
|
try {
|
|
@@ -1703,19 +1720,34 @@ async function processWaMessage(jid, payload, senderNum, requeues = 0) {
|
|
|
1703
1720
|
waLog(`perm=${fmtPerm(perm)} bot=${botId} sid=${sid}`);
|
|
1704
1721
|
|
|
1705
1722
|
const participantName = payload.participant ? '+' + String(payload.participant).split('@')[0].split(':')[0] : '';
|
|
1723
|
+
/* GRUP GÖNDERENİ: LID çağında gerçek telefon participantAlt'ta gelir.
|
|
1724
|
+
Kimlik çözümleme sırası: izin listesindeki isim → @kullanıcı adı → gerçek PN → LID.
|
|
1725
|
+
Ajan böylece grubun içinde mesajın KİMDEN geldiğini görür. */
|
|
1726
|
+
let groupSender = '';
|
|
1727
|
+
if (isGroup) {
|
|
1728
|
+
const pnDigits = String(payload.participantPn || '').split('@')[0].split(':')[0];
|
|
1729
|
+
const hitP = /^\d+$/.test(pnDigits) ? waFind(pnDigits) : null;
|
|
1730
|
+
const uname = String(payload.participantUsername || '').trim();
|
|
1731
|
+
if (hitP && hitP.name) groupSender = `${hitP.name} (+${pnDigits})`;
|
|
1732
|
+
else if (uname) groupSender = `@${uname}${/^\d+$/.test(pnDigits) ? ' (+' + pnDigits + ')' : ''}`;
|
|
1733
|
+
else if (/^\d+$/.test(pnDigits)) groupSender = '+' + pnDigits;
|
|
1734
|
+
else groupSender = participantName || '';
|
|
1735
|
+
}
|
|
1706
1736
|
/* #v13.1 rol: SAHİP vs MİSAFİR — ajan kime konuştuğunu net bilsin */
|
|
1707
1737
|
const isOwner = !isGroup && !!hit.owner;
|
|
1708
1738
|
const roleTag = isOwner
|
|
1709
1739
|
? 'SAHİBİN (talepleri önceliklidir)'
|
|
1710
1740
|
: 'MİSAFİR (izinli ama sahibin sözü önceliklidir)';
|
|
1711
1741
|
const label = isGroup
|
|
1712
|
-
? `Grup ${jid.split('@')[0]}${
|
|
1742
|
+
? `Grup ${jid.split('@')[0]}${groupSender ? ' — gönderen: ' + groupSender : ''}`
|
|
1713
1743
|
: hit.name
|
|
1714
1744
|
? `${hit.name} (+${senderNum || '?'}) — ${roleTag}`
|
|
1715
1745
|
: `+${senderNum || '?'} — ${roleTag}`;
|
|
1716
1746
|
let text = `[WhatsApp${isGroup ? ' grup' : ''} — gönderen: ${label}]`;
|
|
1717
1747
|
if (!isGroup && !isOwner) {
|
|
1718
1748
|
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.]`;
|
|
1749
|
+
} else if (isGroup) {
|
|
1750
|
+
text += `\n[NOT: Grup mesajı — gönderen grubun üyesidir, izin listendeki kişi olmayabilir. Grup üyelerine karşı temkinli konuş.]`;
|
|
1719
1751
|
}
|
|
1720
1752
|
const attachments = [];
|
|
1721
1753
|
|
package/src/renderer/renderer.js
CHANGED
|
@@ -458,9 +458,13 @@ function finishToolCard(callId, ok, result) {
|
|
|
458
458
|
|
|
459
459
|
async function renderSessions(list) {
|
|
460
460
|
let waSet = new Set();
|
|
461
|
+
let tgSet = new Set();
|
|
461
462
|
try {
|
|
462
463
|
waSet = new Set(await beast.waListSessions());
|
|
463
464
|
} catch {}
|
|
465
|
+
try {
|
|
466
|
+
tgSet = new Set(await beast.tgListSessions());
|
|
467
|
+
} catch {}
|
|
464
468
|
els.sessList.innerHTML = '';
|
|
465
469
|
/* aktif botun oturumları — botlar arası geçişte liste de o bota göre değişir */
|
|
466
470
|
for (const s of list) {
|
|
@@ -469,6 +473,7 @@ async function renderSessions(list) {
|
|
|
469
473
|
row.className = 'sess' + (s.id === activeId ? ' active' : '');
|
|
470
474
|
row.innerHTML =
|
|
471
475
|
(waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
|
|
476
|
+
(tgSet.has(s.id) ? '<span class="sess-tg" title="Telegram">T</span>' : '') +
|
|
472
477
|
`<span class="sess-title">${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
|
|
473
478
|
`<span class="sess-code" title="Oturum kodu">${escapeHtml(s.code || '')}</span>` +
|
|
474
479
|
`<button class="sess-del" title="Sil">×</button>`;
|
|
@@ -1619,7 +1624,9 @@ async function renderDashboardPane() {
|
|
|
1619
1624
|
let sessions = [];
|
|
1620
1625
|
try { sessions = (await beast.listSessions()) || []; } catch {}
|
|
1621
1626
|
let waSet = new Set();
|
|
1627
|
+
let tgSet = new Set();
|
|
1622
1628
|
try { waSet = new Set(await beast.waListSessions()); } catch {}
|
|
1629
|
+
try { tgSet = new Set(await beast.tgListSessions()); } catch {}
|
|
1623
1630
|
|
|
1624
1631
|
const totalMsgs = sessions.reduce((a, s) => a + (Number(s.count) || 0), 0);
|
|
1625
1632
|
const today = new Date().toDateString();
|
|
@@ -1655,6 +1662,7 @@ async function renderDashboardPane() {
|
|
|
1655
1662
|
row.className = 'usage-row' + (s.id === activeId ? ' dash-active' : '');
|
|
1656
1663
|
row.innerHTML =
|
|
1657
1664
|
(waSet.has(s.id) ? '<span class="sess-wa" title="WhatsApp">W</span>' : '') +
|
|
1665
|
+
(tgSet.has(s.id) ? '<span class="sess-tg" title="Telegram">T</span>' : '') +
|
|
1658
1666
|
`<span class="sess-code" title="Oturum kodu">${escapeHtml(s.code || '')}</span>` +
|
|
1659
1667
|
`<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">` +
|
|
1660
1668
|
`${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
|
|
@@ -2630,7 +2638,9 @@ async function renderBotChats(b) {
|
|
|
2630
2638
|
const box = $('#botChatMsgs');
|
|
2631
2639
|
box.innerHTML = '';
|
|
2632
2640
|
let waSet = new Set();
|
|
2641
|
+
let tgSet = new Set();
|
|
2633
2642
|
try { waSet = new Set(await beast.waListSessions()); } catch {}
|
|
2643
|
+
try { tgSet = new Set(await beast.tgListSessions()); } catch {}
|
|
2634
2644
|
const sessions = [];
|
|
2635
2645
|
try {
|
|
2636
2646
|
for (const s of await beast.listSessions()) {
|
|
@@ -2648,7 +2658,11 @@ async function renderBotChats(b) {
|
|
|
2648
2658
|
const row = document.createElement('div');
|
|
2649
2659
|
row.className = 'bot-chat-row';
|
|
2650
2660
|
row.innerHTML =
|
|
2651
|
-
(waSet.has(s.id)
|
|
2661
|
+
(waSet.has(s.id)
|
|
2662
|
+
? '<span class="sess-wa" title="WhatsApp">W</span>'
|
|
2663
|
+
: tgSet.has(s.id)
|
|
2664
|
+
? '<span class="sess-tg" title="Telegram">T</span>'
|
|
2665
|
+
: '<span class="sess-wa" style="opacity:.35">D</span>') +
|
|
2652
2666
|
`<span class="bt">${escapeHtml(s.title || 'Yeni Sohbet')}</span>` +
|
|
2653
2667
|
`<span class="sess-code">${escapeHtml(s.code || '')}</span>`;
|
|
2654
2668
|
row.addEventListener('click', async () => {
|
package/src/renderer/style.css
CHANGED
|
@@ -130,8 +130,19 @@ body {
|
|
|
130
130
|
display: flex;
|
|
131
131
|
align-items: center;
|
|
132
132
|
justify-content: center;
|
|
133
|
-
flex-shrink: 0;
|
|
134
133
|
}
|
|
134
|
+
.sess-tg {
|
|
135
|
+
width: 15px;
|
|
136
|
+
height: 15px;
|
|
137
|
+
border-radius: 4px;
|
|
138
|
+
background: #229ed9;
|
|
139
|
+
color: #fff;
|
|
140
|
+
font-size: 10px;
|
|
141
|
+
font-weight: 800;
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
}
|
|
135
146
|
.sess-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; }
|
|
136
147
|
.sess-code {
|
|
137
148
|
color: var(--muted);
|
|
@@ -1525,7 +1536,7 @@ body.browser-open #toast { left: calc((100vw - var(--bw, 480px)) / 2); }
|
|
|
1525
1536
|
|
|
1526
1537
|
#toast {
|
|
1527
1538
|
position: fixed;
|
|
1528
|
-
bottom:
|
|
1539
|
+
bottom: 84px; /* chat input alanının hemen üstünde */
|
|
1529
1540
|
left: 50%;
|
|
1530
1541
|
transform: translateX(-50%) translateY(16px);
|
|
1531
1542
|
opacity: 0;
|