beast-agent 0.26.5 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/engine.js +127 -53
- package/src/agent/obscura.js +245 -0
- package/src/agent/skills.js +14 -10
- package/src/agent/tools.js +105 -51
- package/src/main.js +144 -37
- package/src/preload.js +5 -3
- package/src/renderer/i18n.js +48 -24
- package/src/renderer/renderer.js +132 -44
- package/src/renderer/style.css +11 -0
- package/tests/bg-jobs.test.js +71 -3
- package/tests/engine.test.js +76 -13
- package/tests/obscura.test.js +124 -0
package/src/renderer/renderer.js
CHANGED
|
@@ -383,6 +383,17 @@ function addErrorBubble(text) {
|
|
|
383
383
|
scrollDown(true);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
/* ajan durduruldu/iptal edildi — SEBEP zorunlu, sohbete sönük not olarak düşer */
|
|
387
|
+
function addStopNote(reason) {
|
|
388
|
+
streamEl = null;
|
|
389
|
+
showEmpty(false);
|
|
390
|
+
const div = document.createElement('div');
|
|
391
|
+
div.className = 'msg msg-sys';
|
|
392
|
+
div.textContent = '\u23F9 Durduruldu — sebep: ' + String(reason || 'sebep belirtilmedi');
|
|
393
|
+
els.msgs.appendChild(div);
|
|
394
|
+
scrollDown(true);
|
|
395
|
+
}
|
|
396
|
+
|
|
386
397
|
/* ---------------- tool cards ---------------- */
|
|
387
398
|
/* Ard arda gelen araç kartları TEK kutuda toplanır (.tool-box): çalışırken
|
|
388
399
|
lacivert outline döner, gövde max 420px scroll'lu. Grup; tool_calls İÇEREN
|
|
@@ -738,12 +749,30 @@ async function refreshFalloutPane() {
|
|
|
738
749
|
renderFalloutPane();
|
|
739
750
|
}
|
|
740
751
|
|
|
741
|
-
/* Web Arama sekmesi: TinyFish (
|
|
752
|
+
/* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
|
|
742
753
|
async function renderWebSearchPane() {
|
|
743
754
|
const pane = $('#tab-websearch');
|
|
744
755
|
if (!pane) return;
|
|
745
756
|
const tf = await beast.tinyfishGet().catch(() => ({ set: false, masked: '' }));
|
|
746
757
|
pane.innerHTML =
|
|
758
|
+
'<h2>' + _t('ws_h2') + '</h2>' +
|
|
759
|
+
'<div class="sub">' + _t('ws_sub') + '</div>' +
|
|
760
|
+
/* --- Obscura --- */
|
|
761
|
+
'<div class="divider"></div>' +
|
|
762
|
+
'<h2>' + _t('oc_h2') + '</h2>' +
|
|
763
|
+
'<div class="sub">' + _t('oc_sub') + '</div>' +
|
|
764
|
+
'<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
765
|
+
'<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
|
|
766
|
+
'<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
|
|
767
|
+
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:10px">' +
|
|
768
|
+
'<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
|
|
769
|
+
/* --- Arama sırası --- */
|
|
770
|
+
'<div class="divider"></div>' +
|
|
771
|
+
'<h2>' + _t('so_h2') + '</h2>' +
|
|
772
|
+
'<div class="sub">' + _t('so_sub') + '</div>' +
|
|
773
|
+
'<div id="soList" style="margin-top:10px"></div>' +
|
|
774
|
+
/* --- TinyFish --- */
|
|
775
|
+
'<div class="divider"></div>' +
|
|
747
776
|
'<h2>' + _t('tf_h2') + '</h2>' +
|
|
748
777
|
'<div class="sub">' + _t('tf_sub') + '</div>' +
|
|
749
778
|
'<div id="tfStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
@@ -751,26 +780,80 @@ async function renderWebSearchPane() {
|
|
|
751
780
|
'<input id="tfKeyInp" class="inp" type="password" placeholder="tf_..." autocomplete="new-password" spellcheck="false" />' +
|
|
752
781
|
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
753
782
|
'<button id="tfSave" class="btn">' + _t('ws_save') + '</button>' +
|
|
754
|
-
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>'
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
783
|
+
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>';
|
|
784
|
+
|
|
785
|
+
/* --- Obscura durumu --- */
|
|
786
|
+
const ocSt = $('#ocStatus');
|
|
787
|
+
const ocChk = $('#ocEnabled');
|
|
788
|
+
const ocBtn = $('#ocInstall');
|
|
789
|
+
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '' }));
|
|
790
|
+
ocChk.checked = oc.enabled !== false;
|
|
791
|
+
ocSt.textContent = oc.installed ? _t('oc_status_ok') : _t('oc_status_missing');
|
|
792
|
+
ocChk.addEventListener('change', async () => {
|
|
793
|
+
await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
|
|
794
|
+
toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
|
|
795
|
+
});
|
|
796
|
+
ocBtn.addEventListener('click', async () => {
|
|
797
|
+
ocBtn.disabled = true;
|
|
798
|
+
ocBtn.textContent = _t('oc_installing');
|
|
799
|
+
ocSt.textContent = _t('oc_installing');
|
|
800
|
+
const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
|
|
801
|
+
ocBtn.disabled = false;
|
|
802
|
+
ocBtn.textContent = _t('oc_install');
|
|
803
|
+
ocSt.textContent = r && r.ok ? _t('oc_status_ok') : _t('oc_install_fail') + (r && r.error ? r.error : '?');
|
|
804
|
+
toast(r && r.ok ? _t('oc_installed_toast') : _t('oc_install_fail') + (r && r.error ? r.error : '?'));
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
/* --- Arama sırası --- */
|
|
808
|
+
const soBox = $('#soList');
|
|
809
|
+
let rows = [];
|
|
810
|
+
const soRes = await beast.searchOrderGet().catch(() => ({ chain: [] }));
|
|
811
|
+
rows = (soRes && Array.isArray(soRes.chain) && soRes.chain.length) ? soRes.chain.map((x) => ({ id: x.id, on: x.on !== false })) : [];
|
|
812
|
+
const engName = (id) => _t('so_engine_' + id) || id;
|
|
813
|
+
function renderRows() {
|
|
814
|
+
soBox.innerHTML = '';
|
|
815
|
+
rows.forEach((row, i) => {
|
|
816
|
+
const el = document.createElement('div');
|
|
817
|
+
el.className = 'so-row' + (row.on ? '' : ' off');
|
|
818
|
+
el.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px';
|
|
819
|
+
el.innerHTML =
|
|
820
|
+
'<span class="so-idx" style="min-width:18px;color:var(--muted);font-size:12px">' + (i + 1) + '.</span>' +
|
|
821
|
+
'<input type="checkbox" class="so-chk" style="width:auto" ' + (row.on ? 'checked' : '') + ' />' +
|
|
822
|
+
'<span style="flex:1">' + escapeHtml(engName(row.id)) + '</span>' +
|
|
823
|
+
'<button class="so-up" title="Yukarı" style="width:auto;padding:2px 8px">↑</button>' +
|
|
824
|
+
'<button class="so-down" title="Aşağı" style="width:auto;padding:2px 8px">↓</button>';
|
|
825
|
+
el.querySelector('.so-chk').addEventListener('change', async (e) => {
|
|
826
|
+
row.on = e.target.checked;
|
|
827
|
+
el.classList.toggle('off', !row.on);
|
|
828
|
+
await saveOrder();
|
|
829
|
+
});
|
|
830
|
+
el.querySelector('.so-up').addEventListener('click', async () => {
|
|
831
|
+
if (i === 0) return;
|
|
832
|
+
[rows[i - 1], rows[i]] = [rows[i], rows[i - 1]];
|
|
833
|
+
renderRows();
|
|
834
|
+
await saveOrder();
|
|
835
|
+
});
|
|
836
|
+
el.querySelector('.so-down').addEventListener('click', async () => {
|
|
837
|
+
if (i >= rows.length - 1) return;
|
|
838
|
+
[rows[i + 1], rows[i]] = [rows[i], rows[i + 1]];
|
|
839
|
+
renderRows();
|
|
840
|
+
await saveOrder();
|
|
841
|
+
});
|
|
842
|
+
soBox.appendChild(el);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
async function saveOrder() {
|
|
846
|
+
const r = await beast.searchOrderSet(rows).catch(() => null);
|
|
847
|
+
if (r && r.ok) toast(_t('so_saved_toast'));
|
|
848
|
+
else toast(_t('ws_fail_toast'));
|
|
849
|
+
}
|
|
850
|
+
if (rows.length) renderRows();
|
|
851
|
+
|
|
852
|
+
/* --- TinyFish --- */
|
|
769
853
|
const tfSt = $('#tfStatus');
|
|
770
854
|
const setTfSt = (r) => {
|
|
771
855
|
tfSt.textContent = r.set ? _t('tf_status_set') + r.masked : _t('tf_status_unset');
|
|
772
856
|
};
|
|
773
|
-
setExaSt(await beast.exaGet().catch(() => ({ set: false, masked: '' })));
|
|
774
857
|
setTfSt(tf);
|
|
775
858
|
$('#tfSave').addEventListener('click', async () => {
|
|
776
859
|
const v = $('#tfKeyInp').value.trim();
|
|
@@ -790,24 +873,6 @@ async function renderWebSearchPane() {
|
|
|
790
873
|
setTfSt({ set: false, masked: '' });
|
|
791
874
|
toast(_t('tf_cleared_toast'));
|
|
792
875
|
});
|
|
793
|
-
$('#exaSave').addEventListener('click', async () => {
|
|
794
|
-
const v = $('#exaKeyInp').value.trim();
|
|
795
|
-
if (!v) { toast(_t('ws_empty_toast')); return; }
|
|
796
|
-
const rr = await beast.exaSet(v).catch(() => null);
|
|
797
|
-
$('#exaKeyInp').value = '';
|
|
798
|
-
if (rr && rr.set) {
|
|
799
|
-
setExaSt(rr);
|
|
800
|
-
toast(_t('ws_saved_toast'));
|
|
801
|
-
} else {
|
|
802
|
-
toast(_t('ws_fail_toast'));
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
$('#exaClear').addEventListener('click', async () => {
|
|
806
|
-
await beast.exaClear().catch(() => {});
|
|
807
|
-
$('#exaKeyInp').value = '';
|
|
808
|
-
setExaSt({ set: false, masked: '' });
|
|
809
|
-
toast(_t('ws_cleared_toast'));
|
|
810
|
-
});
|
|
811
876
|
}
|
|
812
877
|
|
|
813
878
|
async function renderProviderPane() {
|
|
@@ -1473,7 +1538,7 @@ const WA_STATUS_TEXT = {
|
|
|
1473
1538
|
|
|
1474
1539
|
async function renderIntegrationsPane() {
|
|
1475
1540
|
const pane = $('#tab-integrations');
|
|
1476
|
-
const g = await beast.waGetGroups().catch(() => ({ enabled: false, mentionOnly: true }));
|
|
1541
|
+
const g = await beast.waGetGroups().catch(() => ({ enabled: false, mentionOnly: true, seeAll: false }));
|
|
1477
1542
|
pane.innerHTML =
|
|
1478
1543
|
'<h2>' + _t('it_h2') + '</h2><div class="sub">' + _t('it_sub') + '</div>' +
|
|
1479
1544
|
`<div class="wa-card">
|
|
@@ -1500,6 +1565,7 @@ async function renderIntegrationsPane() {
|
|
|
1500
1565
|
<label class="lock-row"><input type="checkbox" id="waGroupsOn" ${g.enabled ? 'checked' : ''}/><span>${_t('it_groups_on')}</span></label>
|
|
1501
1566
|
<label class="lock-row" style="${g.enabled ? '' : 'opacity:.45'}"><input type="radio" name="waGroupMode" id="waGroupsMention" ${g.mentionOnly !== false ? 'checked' : ''}/><span>${_t('it_groups_mention')}</span></label>
|
|
1502
1567
|
<label class="lock-row" style="${g.enabled ? '' : 'opacity:.45'}"><input type="radio" name="waGroupMode" id="waGroupsAll" ${g.mentionOnly === false ? 'checked' : ''}/><span>${_t('it_groups_all')}</span></label>
|
|
1568
|
+
<label class="lock-row" id="waSeeAllRow" style="${g.enabled && g.mentionOnly !== false ? '' : 'opacity:.45'}"><input type="checkbox" id="waGroupsSeeAll" ${g.seeAll ? 'checked' : ''}/><span>${_t('it_groups_seeall')}</span></label>
|
|
1503
1569
|
<div class="divider"></div>
|
|
1504
1570
|
<label class="mem-label" style="margin-top:0">${_t('it_allow_label')}</label>
|
|
1505
1571
|
<div id="waAllowChips" class="chips-inline"></div>
|
|
@@ -1574,22 +1640,41 @@ async function renderIntegrationsPane() {
|
|
|
1574
1640
|
const waGroupsOn = $('#waGroupsOn');
|
|
1575
1641
|
const waGroupsMention = $('#waGroupsMention');
|
|
1576
1642
|
const waGroupsAll = $('#waGroupsAll');
|
|
1577
|
-
|
|
1643
|
+
const waGroupsSeeAll = $('#waGroupsSeeAll');
|
|
1644
|
+
const waSeeAllRow = $('#waSeeAllRow');
|
|
1645
|
+
/* iki mod BİRBİRİNE HARIÇTİR: ya @mention ya her mesaja karışma (radio)
|
|
1646
|
+
seeAll yalnız mention modunda anlamlı — o mod seçiliyken aktifleşir */
|
|
1578
1647
|
const syncGroupMode = () => {
|
|
1579
1648
|
const op = waGroupsOn.checked ? '' : '.45';
|
|
1580
1649
|
if (waGroupsMention) waGroupsMention.parentElement.style.opacity = op;
|
|
1581
1650
|
if (waGroupsAll) waGroupsAll.parentElement.style.opacity = op;
|
|
1651
|
+
if (waSeeAllRow) waSeeAllRow.style.opacity = waGroupsOn.checked && waGroupsMention && waGroupsMention.checked ? '' : '.45';
|
|
1582
1652
|
};
|
|
1583
1653
|
const saveGroups = async () => {
|
|
1584
|
-
const r = await beast.waSetGroups({ enabled: waGroupsOn.checked, mentionOnly: !waGroupsAll.checked });
|
|
1585
|
-
toast(
|
|
1654
|
+
const r = await beast.waSetGroups({ enabled: waGroupsOn.checked, mentionOnly: !waGroupsAll.checked, seeAll: !!(waGroupsSeeAll && waGroupsSeeAll.checked) });
|
|
1655
|
+
toast(
|
|
1656
|
+
r.enabled
|
|
1657
|
+
? r.mentionOnly
|
|
1658
|
+
? r.seeAll
|
|
1659
|
+
? 'Gruplar açık — tüm konuşmalar bağlam olarak okunuyor, sadece @mention\'a cevap verir'
|
|
1660
|
+
: 'Gruplar açık — @mention bekler'
|
|
1661
|
+
: 'Gruplar açık — tüm mesajlara karışır'
|
|
1662
|
+
: 'Gruplar kapalı'
|
|
1663
|
+
);
|
|
1586
1664
|
};
|
|
1587
1665
|
waGroupsOn.addEventListener('change', async () => {
|
|
1588
1666
|
syncGroupMode();
|
|
1589
1667
|
await saveGroups();
|
|
1590
1668
|
});
|
|
1591
|
-
waGroupsMention.addEventListener('change',
|
|
1592
|
-
|
|
1669
|
+
waGroupsMention.addEventListener('change', () => {
|
|
1670
|
+
syncGroupMode();
|
|
1671
|
+
saveGroups();
|
|
1672
|
+
});
|
|
1673
|
+
waGroupsAll.addEventListener('change', () => {
|
|
1674
|
+
syncGroupMode();
|
|
1675
|
+
saveGroups();
|
|
1676
|
+
});
|
|
1677
|
+
if (waGroupsSeeAll) waGroupsSeeAll.addEventListener('change', saveGroups);
|
|
1593
1678
|
syncGroupMode();
|
|
1594
1679
|
|
|
1595
1680
|
$('#waStartBtn').addEventListener('click', async () => {
|
|
@@ -3427,7 +3512,7 @@ function renderAgentRail() {
|
|
|
3427
3512
|
`<span class="ag-dot"></span>` +
|
|
3428
3513
|
`<span class="sess-title">${escapeHtml(j.title)}</span>` +
|
|
3429
3514
|
(j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
|
|
3430
|
-
`<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>` +
|
|
3515
|
+
`<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) + (j.status === 'aborted' && j.error ? ' — ' + escapeHtml(String(j.error).slice(0, 70)) : ''))}</span>` +
|
|
3431
3516
|
(j.status === 'running'
|
|
3432
3517
|
? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
|
|
3433
3518
|
: `<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>`) +
|
|
@@ -3557,7 +3642,7 @@ function renderAgentsPane() {
|
|
|
3557
3642
|
`<div class="ag-head">` +
|
|
3558
3643
|
`<span class="ag-dot"></span>` +
|
|
3559
3644
|
`<span class="ag-title">${escapeHtml(j.title)}</span>` +
|
|
3560
|
-
`<span class="ag-st">${agStText(j.status) || j.status}</span>` +
|
|
3645
|
+
`<span class="ag-st" title="${j.error ? escapeHtml(String(j.error)) : ''}">${agStText(j.status) || j.status}</span>` +
|
|
3561
3646
|
`<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
|
|
3562
3647
|
(j.status === 'running'
|
|
3563
3648
|
? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
|
|
@@ -3744,6 +3829,8 @@ function onEvent(ev) {
|
|
|
3744
3829
|
break;
|
|
3745
3830
|
case 'done':
|
|
3746
3831
|
closeChatToolGroup();
|
|
3832
|
+
/* iptal/durdurma sebebini SOHBETE yaz — sebepsiz durdurma yok */
|
|
3833
|
+
if (ev.aborted) addStopNote(ev.reason);
|
|
3747
3834
|
setBusy(false);
|
|
3748
3835
|
setStatus(''); /* son durum balonu ("düşünüyor…" vb.) yapışmasın */
|
|
3749
3836
|
/* cevabın SONU görünsün: markdown son render sonrası iki kez en alta kilitle */
|
|
@@ -6083,7 +6170,8 @@ function bcIngest(ev) {
|
|
|
6083
6170
|
bcFlushStream();
|
|
6084
6171
|
bcCloseToolGroup();
|
|
6085
6172
|
bcSetBusy(false);
|
|
6086
|
-
|
|
6173
|
+
/* iptal/durdurma SEBEBİ panelde de görünür */
|
|
6174
|
+
bcLine(ev.aborted ? 't-err' : 't-dim', ev.aborted ? '[durduruldu — sebep: ' + (ev.reason || 'sebep belirtilmedi') + ']' : '(tamamlandı)');
|
|
6087
6175
|
bcRefreshPreview();
|
|
6088
6176
|
if (ideModeOn() && els.bcInput) els.bcInput.focus();
|
|
6089
6177
|
break;
|
package/src/renderer/style.css
CHANGED
|
@@ -759,6 +759,17 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
|
|
|
759
759
|
padding: 10px 14px;
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
+
/* durdurma/iptal notu: sebep zorunlu — nötr, sönük ton */
|
|
763
|
+
.msg-sys {
|
|
764
|
+
border: 1px dashed var(--border);
|
|
765
|
+
background: var(--panel);
|
|
766
|
+
color: var(--muted);
|
|
767
|
+
border-radius: 10px;
|
|
768
|
+
padding: 8px 12px;
|
|
769
|
+
font-size: 12.5px;
|
|
770
|
+
word-break: break-word;
|
|
771
|
+
}
|
|
772
|
+
|
|
762
773
|
/* markdown */
|
|
763
774
|
.md p { margin: 0 0 8px; }
|
|
764
775
|
.md p:last-child { margin-bottom: 0; }
|
package/tests/bg-jobs.test.js
CHANGED
|
@@ -271,18 +271,86 @@ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO
|
|
|
271
271
|
assert.deepEqual(st, ['queued', 'queued', 'running', 'running']);
|
|
272
272
|
assert.strictEqual(sent.length, 2);
|
|
273
273
|
|
|
274
|
-
/* slot açılır → sıradaki queued iş running olur ve gönderilir
|
|
274
|
+
/* slot açılır → sıradaki queued iş running olur ve gönderilir.
|
|
275
|
+
_bgFinish('aborted') artık üst sohbete [ARKA PLAN İPTAL + SEBEP] raporu da düşürür. */
|
|
275
276
|
eng._bgFinish(ids[0], 'aborted');
|
|
276
277
|
await sleep(30);
|
|
277
278
|
const queued = [...eng._bgJobs.values()].filter((j) => j.status === 'queued').length;
|
|
278
279
|
const running = [...eng._bgJobs.values()].filter((j) => j.status === 'running').length;
|
|
279
280
|
assert.strictEqual(queued, 1);
|
|
280
281
|
assert.strictEqual(running, 2);
|
|
281
|
-
|
|
282
|
+
/* 3 gönderim = kuyruktan başlayan iş + 1 rapor = üst sohbete düşen iptal sebebi */
|
|
283
|
+
assert.strictEqual(sent.length, 4);
|
|
284
|
+
assert.ok(eng._pendingReports.length === 0, 'iptal raporu üst sohbete düştü');
|
|
282
285
|
});
|
|
283
286
|
|
|
284
|
-
/* ----------
|
|
287
|
+
/* ---------- İPTAL SEBEBİ DİSİPLİNİ ---------- */
|
|
288
|
+
|
|
289
|
+
test('iptal SEBEBİ zorunlu: interrupt sebebi iş kaydına ve üst sohbete yazar', async () => {
|
|
285
290
|
const { eng } = tmpEngine();
|
|
291
|
+
const sent = [];
|
|
292
|
+
eng.send = (sid, payload) => { sent.push({ sid, text: String((payload && payload.text) || '') }); return true; };
|
|
293
|
+
const r = eng.runBackground('pr', 'koşan iş', 'Sebep İş');
|
|
294
|
+
const id = r.backgroundId;
|
|
295
|
+
const c = new AbortController();
|
|
296
|
+
eng.ctrls.set(id, c);
|
|
297
|
+
|
|
298
|
+
eng.interrupt(id, 'kullanıcı panelden iptal etti');
|
|
299
|
+
assert.ok(c.signal.aborted, 'ctrl kesildi');
|
|
300
|
+
assert.strictEqual(eng._abortReasons.get(id), 'kullanıcı panelden iptal etti', 'sebep haritada');
|
|
301
|
+
|
|
302
|
+
/* _run catch'inin yaptığı iş: haritadan sebebi okuyup _bgFinish'e verir */
|
|
303
|
+
const why = eng._abortReasons.get(id);
|
|
304
|
+
eng._abortReasons.delete(id);
|
|
305
|
+
eng._bgFinish(id, 'aborted', why);
|
|
306
|
+
|
|
307
|
+
const j = eng._bgJobs.get(id);
|
|
308
|
+
assert.strictEqual(j.status, 'aborted');
|
|
309
|
+
assert.strictEqual(j.error, 'kullanıcı panelden iptal etti', 'sebep iş kaydına yazıldı');
|
|
310
|
+
const rep = sent.find((x) => x.sid === 'pr' && x.text.includes('[ARKA PLAN İPTAL'));
|
|
311
|
+
assert.ok(rep, 'üst sohbete İPTAL raporu düştü');
|
|
312
|
+
assert.ok(/Sebep: kullanıcı panelden/.test(rep.text), 'rapor SEBEPİ içerir');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('kuyruktaki ajan interrupt ile SEBEPİYLE kesilir', async () => {
|
|
316
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-qc-'));
|
|
317
|
+
const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
|
|
318
|
+
eng._bgLimit = 0; // yapıcı min 1 kelepçeler; sıfırla → her iş queued kalır
|
|
319
|
+
eng.send = () => true;
|
|
320
|
+
const r = eng.runBackground('pq2', 'kuyruk işi', 'Kuyruk İş');
|
|
321
|
+
await sleep(80); // admit → bgLimit 0 → queued
|
|
322
|
+
assert.strictEqual(eng._bgJobs.get(r.backgroundId).status, 'queued');
|
|
323
|
+
|
|
324
|
+
assert.strictEqual(eng.interrupt(r.backgroundId, 'sıra iptal edildi'), true);
|
|
325
|
+
const j = eng._bgJobs.get(r.backgroundId);
|
|
326
|
+
assert.strictEqual(j.status, 'aborted');
|
|
327
|
+
assert.strictEqual(j.error, 'sıra iptal edildi', 'kuyruktaki işin iptal sebebi kayda geçti');
|
|
328
|
+
eng.deleteSession(r.backgroundId);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('task_cancel reason ZORUNLU: sebep iş kaydına ve note mesajına düşer', async () => {
|
|
332
|
+
const { eng } = tmpEngine();
|
|
333
|
+
const r = eng.runBackground('pc', 'iş', 'İptal Edilen');
|
|
334
|
+
const id = r.backgroundId;
|
|
335
|
+
const c = new AbortController();
|
|
336
|
+
eng.ctrls.set(id, c);
|
|
337
|
+
|
|
338
|
+
const out = JSON.parse(await eng._execTool('task_cancel', { id, reason: 'yanlış kaynağa baktı' }, {}));
|
|
339
|
+
assert.ok(out.ok, 'cancel başarılı');
|
|
340
|
+
assert.ok(/yanlış kaynağa baktı/.test(out.note), 'note ceza sebep içerir');
|
|
341
|
+
assert.strictEqual(eng._abortReasons.get(id), 'yanlış kaynağa baktı');
|
|
342
|
+
assert.ok(c.signal.aborted);
|
|
343
|
+
|
|
344
|
+
/* reason verilmezse varsayılan sebep yine kayda düşer */
|
|
345
|
+
const id2 = eng.runBackground('pc', 'iş 2', 'İptal 2').backgroundId;
|
|
346
|
+
eng.ctrls.set(id2, new AbortController());
|
|
347
|
+
const out2 = JSON.parse(await eng._execTool('task_cancel', { id: id2 }, {}));
|
|
348
|
+
assert.ok(out2.ok);
|
|
349
|
+
assert.ok(eng._abortReasons.get(id2).includes('task_cancel'), 'varsayılan sebep CEO kaynağını söyler');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/* ---------- /stop anahtarı ---------- */
|
|
353
|
+
test('stopAll: koşan turları keser, paralel ajanı aborted işaretler', async () => { const { eng } = tmpEngine();
|
|
286
354
|
const r = eng.runBackground('pS', 'koşan iş', 'Koşu');
|
|
287
355
|
const id = r.backgroundId;
|
|
288
356
|
|
package/tests/engine.test.js
CHANGED
|
@@ -113,28 +113,91 @@ test('eski görseller metne indirilir, son mesajın görseli korunur', () => {
|
|
|
113
113
|
assert.ok(Array.isArray(lastUser.content));
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
/* ---------- oturum notlar� (ge�ici haf�za) ---------- */
|
|
116
|
+
/* ---------- oturum notlar� (ge�ici haf�za) ---------- */
|
|
117
117
|
|
|
118
|
-
test('oturum notlar�: dosyadan y�klenir, mesaj listesine kar��maz', () => {
|
|
118
|
+
test('oturum notlar�: dosyadan y�klenir, mesaj listesine kar��maz', () => {
|
|
119
119
|
const eng = makeEngine();
|
|
120
120
|
const id = eng.createSession().id;
|
|
121
121
|
for (let i = 0; i < 5; i++) {
|
|
122
122
|
fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'msg', role: 'user', content: 'mesaj ' + i }) + '\n');
|
|
123
123
|
}
|
|
124
|
-
fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'notes', text: 'hedef: v7 �al�smas�', at: 3 }) + '\n');
|
|
125
|
-
eng.cache.delete(id); // diskten taze y�kle
|
|
124
|
+
fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'notes', text: 'hedef: v7 �al�smas�', at: 3 }) + '\n');
|
|
125
|
+
eng.cache.delete(id); // diskten taze y�kle
|
|
126
126
|
const s = eng._load(id);
|
|
127
|
-
assert.equal(s.notes, 'hedef: v7 �al�smas�');
|
|
127
|
+
assert.equal(s.notes, 'hedef: v7 �al�smas�');
|
|
128
128
|
assert.equal(s.notesAt, 3);
|
|
129
129
|
assert.equal(s.messages.length, 5);
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
test('oturum notlar� varken payload s�k�la��r', () => {
|
|
133
|
-
const eng = makeEngine({ historyTokenBudget: 100000 });
|
|
134
|
-
const msgs = [];
|
|
135
|
-
for (let i = 0; i < 30; i++) msgs.push({ role: 'user', content: 'kisa mesaj #' + i });
|
|
136
|
-
const withoutNotes = eng._buildPayload('sys', msgs, null);
|
|
137
|
-
const withNotes = eng._buildPayload('sys', msgs, '�zet: test');
|
|
138
|
-
assert.ok(withNotes.length < withoutNotes.length);
|
|
139
|
-
assert.equal(withNotes[withNotes.length - 1].content, 'kisa mesaj #29'); // son mesaj hep kal�r
|
|
132
|
+
test('oturum notlar� varken payload s�k�la��r', () => {
|
|
133
|
+
const eng = makeEngine({ historyTokenBudget: 100000 });
|
|
134
|
+
const msgs = [];
|
|
135
|
+
for (let i = 0; i < 30; i++) msgs.push({ role: 'user', content: 'kisa mesaj #' + i });
|
|
136
|
+
const withoutNotes = eng._buildPayload('sys', msgs, null);
|
|
137
|
+
const withNotes = eng._buildPayload('sys', msgs, '�zet: test');
|
|
138
|
+
assert.ok(withNotes.length < withoutNotes.length);
|
|
139
|
+
assert.equal(withNotes[withNotes.length - 1].content, 'kisa mesaj #29'); // son mesaj hep kal�r
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/* ---------- observe(): sessiz bağlam enjeksiyonu ---------- */
|
|
143
|
+
|
|
144
|
+
test('observe: mesaj geçmişe düşer ama run tetiklenmez', async () => {
|
|
145
|
+
const events = [];
|
|
146
|
+
const eng = makeEngine({ emit: (ev) => events.push(ev) });
|
|
147
|
+
const id = eng.createSession().id;
|
|
148
|
+
const ok = eng.observe(id, '[BAĞLAM — grup] ali: selam');
|
|
149
|
+
assert.ok(ok);
|
|
150
|
+
const s = eng._load(id);
|
|
151
|
+
assert.equal(s.messages.length, 1);
|
|
152
|
+
assert.equal(s.messages[0].role, 'user');
|
|
153
|
+
assert.ok(s.messages[0].content.startsWith('[BAĞLAM'));
|
|
154
|
+
// _run tetiklenmedi: LLM hatası/done olayı yok
|
|
155
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
156
|
+
assert.ok(!events.some((e) => e.type === 'error' || e.type === 'done'));
|
|
157
|
+
// observe olayı yayıldı
|
|
158
|
+
assert.ok(events.some((e) => e.type === 'observe' && e.sessionId === id));
|
|
159
|
+
// boş metin false
|
|
160
|
+
assert.equal(eng.observe(id, ' '), false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('observe: ardışık bağlam mesajları tek mesajda birleşir', () => {
|
|
164
|
+
const eng = makeEngine();
|
|
165
|
+
const id = eng.createSession().id;
|
|
166
|
+
eng.observe(id, '[BAĞLAM — grup] ali: selam');
|
|
167
|
+
eng.observe(id, '[BAĞLAM — grup] ayşe: nasılsın');
|
|
168
|
+
const s = eng._load(id);
|
|
169
|
+
assert.equal(s.messages.length, 1);
|
|
170
|
+
assert.ok(s.messages[0].content.includes('ali: selam'));
|
|
171
|
+
assert.ok(s.messages[0].content.includes('ayşe: nasılsın'));
|
|
172
|
+
// dosyaya da tek satır yazıldı
|
|
173
|
+
const lines = fs.readFileSync(eng._file(id), 'utf8').split('\n').filter((l) => l.trim());
|
|
174
|
+
const msgLines = lines.filter((l) => { try { return JSON.parse(l).t === 'msg'; } catch { return false; } });
|
|
175
|
+
assert.equal(msgLines.length, 1);
|
|
140
176
|
});
|
|
177
|
+
|
|
178
|
+
test('observe: normal user mesajıyla birleşmez', () => {
|
|
179
|
+
const eng = makeEngine();
|
|
180
|
+
const id = eng.createSession().id;
|
|
181
|
+
const s = eng._load(id);
|
|
182
|
+
s.messages.push({ role: 'user', content: 'gerçek soru' });
|
|
183
|
+
fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'msg', role: 'user', content: 'gerçek soru' }) + '\n');
|
|
184
|
+
eng.observe(id, '[BAĞLAM — grup] ali: selam');
|
|
185
|
+
assert.equal(s.messages.length, 2);
|
|
186
|
+
assert.equal(s.messages[0].content, 'gerçek soru');
|
|
187
|
+
assert.ok(s.messages[1].content.startsWith('[BAĞLAM'));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('send: mention mesajı bekleyen bağlam mesajıyla birleşir ve run başlar', async () => {
|
|
191
|
+
const events = [];
|
|
192
|
+
const eng = makeEngine({ emit: (ev) => events.push(ev) });
|
|
193
|
+
const id = eng.createSession().id;
|
|
194
|
+
eng.observe(id, '[BAĞLAM — grup] ali: selam\nayşe: nasılsın');
|
|
195
|
+
eng.send(id, { text: '@beast bana bugün hava nasıl?' });
|
|
196
|
+
const s = eng._load(id);
|
|
197
|
+
assert.equal(s.messages.length, 1);
|
|
198
|
+
assert.ok(s.messages[0].content.includes('ayşe: nasılsın'));
|
|
199
|
+
assert.ok(s.messages[0].content.includes('@beast bana bugün hava nasıl?'));
|
|
200
|
+
// run başladı → sağlayıcı yoksa hata olayı düşer (tetiklenme kanıtı)
|
|
201
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
202
|
+
assert.ok(events.some((e) => e.type === 'error' || e.type === 'done'));
|
|
203
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Obscura entegrasyonu + sıralı web arama zinciri testleri (ağ çağrısı yok) */
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert');
|
|
7
|
+
const tools = require('../src/agent/tools');
|
|
8
|
+
const obscura = require('../src/agent/obscura');
|
|
9
|
+
|
|
10
|
+
test('varsayılan zincir: 4 motor, hepsi açık, obscura AKTİF', () => {
|
|
11
|
+
const c = tools.getSearchChain();
|
|
12
|
+
assert.deepEqual(c.map((x) => x.id), ['browser', 'obscura', 'tinyfish', 'python']);
|
|
13
|
+
for (const r of c) assert.strictEqual(r.on, true, r.id + ' varsayılan açık');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('setSearchChain: sıra yeniden düzenlenir, eksik motorlar sona eklenir (açık)', () => {
|
|
17
|
+
const c = tools.setSearchChain([
|
|
18
|
+
{ id: 'obscura', on: true },
|
|
19
|
+
{ id: 'python', on: true },
|
|
20
|
+
{ id: 'browser', on: false },
|
|
21
|
+
]);
|
|
22
|
+
assert.deepEqual(c.map((x) => x.id), ['obscura', 'python', 'browser', 'tinyfish']);
|
|
23
|
+
assert.strictEqual(c.find((x) => x.id === 'browser').on, false);
|
|
24
|
+
assert.strictEqual(c.find((x) => x.id === 'tinyfish').on, true, 'listede olmayan motor varsayılan açık');
|
|
25
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN); /* testler arası temizlik */
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('setSearchChain: hepsi kapalıysa tarayıcı zorunlu açılır (zincir boş kalmasın)', () => {
|
|
29
|
+
const c = tools.setSearchChain(['obscura', 'tinyfish', 'python', 'browser'].map((id) => ({ id, on: false })));
|
|
30
|
+
assert.strictEqual(c.find((x) => x.id === 'browser').on, true);
|
|
31
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('setSearchChain: bilinmeyen id ve tekrar elenir', () => {
|
|
35
|
+
const c = tools.setSearchChain([{ id: 'exa', on: true }, { id: 'obscura' }, { id: 'obscura', on: false }, 'python']);
|
|
36
|
+
assert.deepEqual(c.map((x) => x.id), ['obscura', 'python', 'browser', 'tinyfish']);
|
|
37
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('searchChainWeb: sırayı uygular, dolu motorda durur, boş tarayıcı banlanır', async () => {
|
|
41
|
+
tools.setSearchChain([
|
|
42
|
+
{ id: 'browser', on: true },
|
|
43
|
+
{ id: 'obscura', on: false },
|
|
44
|
+
{ id: 'tinyfish', on: false },
|
|
45
|
+
{ id: 'python', on: false },
|
|
46
|
+
]);
|
|
47
|
+
const calls = [];
|
|
48
|
+
const r = await tools.searchChainWeb('test sorgu', 5, {
|
|
49
|
+
browser: async () => {
|
|
50
|
+
calls.push('browser');
|
|
51
|
+
return { ok: true, results: [{ title: 'T', url: 'https://x.com', snippet: 's', engine: 'browser' }] };
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
assert.ok(r.ok, 'sonuç geldi');
|
|
55
|
+
assert.strictEqual(calls.length, 1, 'zincir ilk dolu motorda durdu');
|
|
56
|
+
assert.ok(!tools.browserBanned(), 'başarılı tarayıcı banlanmaz');
|
|
57
|
+
|
|
58
|
+
/* tarayıcı boş dönerse 10 dk ban + zincir başarısız (diğer motorlar kapalı) */
|
|
59
|
+
const r2 = await tools.searchChainWeb('test 2', 5, {
|
|
60
|
+
browser: async () => {
|
|
61
|
+
calls.push('browser');
|
|
62
|
+
return { ok: true, results: [] };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
assert.strictEqual(r2.ok, false);
|
|
66
|
+
assert.ok(tools.browserBanned(), 'boş sonuç tarayıcıyı banladı');
|
|
67
|
+
tools.banBrowser(0); /* banı temizle */
|
|
68
|
+
const r3 = await tools.searchChainWeb('test 3', 5, {
|
|
69
|
+
browser: async () => {
|
|
70
|
+
calls.push('browser');
|
|
71
|
+
return { ok: true, results: [{ title: 'T3', url: 'https://y.com' }] };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
assert.ok(r3.ok && calls.length === 3, 'ban süresi dolunca tarayıcı geri döner');
|
|
75
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('searchChainWeb: browser hook yoksa obscura/tinyfish/python sırası izlenir', async () => {
|
|
79
|
+
tools.setSearchChain([{ id: 'browser', on: true }, { id: 'obscura', on: false }, { id: 'tinyfish', on: false }, { id: 'python', on: false }]);
|
|
80
|
+
const r = await tools.searchChainWeb('test', 5, {});
|
|
81
|
+
/* tüm motorlar kapalı/atlanmış → normalize browser'ı zorunlu açtı ama hook yok → başarısız */
|
|
82
|
+
assert.strictEqual(r.ok, false);
|
|
83
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('searchChainWeb: boş sorgu hata döner', async () => {
|
|
87
|
+
const r = await tools.searchChainWeb(' ', 5, {});
|
|
88
|
+
assert.strictEqual(r.ok, false);
|
|
89
|
+
assert.ok(/boş sorgu/.test(r.error));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('obscura parseDdgLite: lite tablosundan sonuç çıkarır', () => {
|
|
93
|
+
const html =
|
|
94
|
+
'<table><tr><td><a rel="nofollow" class="result-link" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fornek.com%2Fa&rut=1">Örnek A</a></td></tr>' +
|
|
95
|
+
'<tr><td><a rel="nofollow" class="result-link" href="https://ornek.com/b">Örnek B</a></td></tr></table>';
|
|
96
|
+
const rows = obscura.parseDdgLite(html, 5);
|
|
97
|
+
assert.strictEqual(rows.length, 2);
|
|
98
|
+
assert.strictEqual(rows[0].url, 'https://ornek.com/a');
|
|
99
|
+
assert.strictEqual(rows[0].title, 'Örnek A');
|
|
100
|
+
assert.strictEqual(rows[1].url, 'https://ornek.com/b');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('obscura yolları ve kurulum durumu', () => {
|
|
104
|
+
assert.ok(/beast[\\/]obscura$/.test(obscura.obscuraDir()));
|
|
105
|
+
assert.ok(/obscura\.exe$/.test(obscura.obscuraExe()));
|
|
106
|
+
/* test makinesinde kurulu olabilir — sadece tipleri doğrula */
|
|
107
|
+
assert.strictEqual(typeof obscura.obscuraInstalled(), 'boolean');
|
|
108
|
+
assert.strictEqual(typeof obscura.obscuraSearch, 'function');
|
|
109
|
+
assert.strictEqual(typeof obscura.installObscura, 'function');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('obscuraSearch: kurulu değilse null döner (zincir atlar)', async () => {
|
|
113
|
+
const wasInstalled = obscura.obscuraInstalled();
|
|
114
|
+
if (wasInstalled) return; /* kurulu makinede atla */
|
|
115
|
+
const r = await obscura.obscuraSearch('sorgu', {});
|
|
116
|
+
assert.strictEqual(r, null);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Exa sistemden tamamen kalktı', () => {
|
|
120
|
+
for (const k of ['setExaKey', 'exaSearch']) assert.ok(!(k in tools), k + ' export yok');
|
|
121
|
+
const desc = JSON.stringify(tools.definitions);
|
|
122
|
+
assert.ok(!/exa\.ai/i.test(desc), 'araç tanımlarında exa yok');
|
|
123
|
+
assert.ok(!/api\.exa\.ai/i.test(desc));
|
|
124
|
+
});
|